398 lines
18 KiB
JSON
398 lines
18 KiB
JSON
{
|
|
"updatedAt": "2026-05-14T01:53:21.608Z",
|
|
"createdAt": "2026-05-13T21:15:57.655Z",
|
|
"id": "bu0kZAbLo2KUVkTl",
|
|
"name": "Telegram School Intake → Postgres → Paperless v1.2",
|
|
"description": null,
|
|
"active": true,
|
|
"isArchived": false,
|
|
"nodes": [
|
|
{
|
|
"parameters": {
|
|
"httpMethod": "POST",
|
|
"path": "school/intake/upload",
|
|
"responseMode": "lastNode",
|
|
"responseData": "allEntries",
|
|
"options": {
|
|
"rawBody": true
|
|
}
|
|
},
|
|
"id": "school-intake-webhook",
|
|
"name": "School Intake Webhook",
|
|
"type": "n8n-nodes-base.webhook",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
-1180,
|
|
0
|
|
],
|
|
"webhookId": "6251b84e-2ac7-49a3-9b05-7ca6540da134"
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "\nconst req = $input.first().json;\nconst body = req.body || {};\nconst binary = $input.first().binary || {};\nconst upload = binary.data || Object.values(binary)[0];\nif (!upload) throw new Error('Expected multipart file upload in the webhook request.');\nconst required = ['class_name', 'assignment_name', 'submission_kind'];\nfor (const key of required) {\n if (!body[key] || !String(body[key]).trim()) {\n throw new Error('Missing required form field: ' + key + ' . Required fields: class_name, assignment_name, submission_kind.');\n }\n}\nconst sanitize = (value, fallback = 'item') => String(value || fallback)\n .trim()\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '')\n .slice(0, 80) || fallback;\nconst stableHash = (str) => {\n let h1 = 0xdeadbeef;\n let h2 = 0x41c6ce57;\n for (let i = 0; i < str.length; i++) {\n const ch = str.charCodeAt(i);\n h1 = Math.imul(h1 ^ ch, 2654435761);\n h2 = Math.imul(h2 ^ ch, 1597334677);\n }\n h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ Math.imul(h2 ^ (h2 >>> 13), 3266489909);\n h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^ Math.imul(h1 ^ (h1 >>> 13), 3266489909);\n return ((h2 >>> 0).toString(16).padStart(8, '0') + (h1 >>> 0).toString(16).padStart(8, '0'));\n};\nconst originalName = upload.fileName || body.filename || 'upload.bin';\nconst extension = (() => {\n const m = String(originalName).match(/(\\.[A-Za-z0-9]{1,10})$/);\n return m ? m[1].toLowerCase() : '';\n})();\nconst bytes = Number(upload.bytes ?? 0) || (() => {\n const text = String(upload.fileSize || '').trim();\n const m = text.match(/^([0-9.]+)\\s*(B|KB|MB|GB)$/i);\n if (!m) return 0;\n const value = Number(m[1]);\n const unit = m[2].toUpperCase();\n const mult = unit === 'GB' ? 1024**3 : unit === 'MB' ? 1024**2 : unit === 'KB' ? 1024 : 1;\n return Number.isFinite(value) ? Math.round(value * mult) : 0;\n})();\nconst checksum = stableHash([originalName, upload.mimeType || '', String(bytes), String(body.class_name), String(body.assignment_name), String(body.submission_kind)].join('|'));\nconst now = new Date();\nconst intakeId = ['school', now.toISOString().slice(0,10).replace(/-/g,''), sanitize(body.class_name), sanitize(body.assignment_name), checksum.slice(0, 12)].join('-');\nconst storedFilename = intakeId + extension;\nconst title = [body.class_name, body.assignment_name, body.submission_kind].map(v => String(v).trim()).join(' — ');\nreturn [{\n json: {\n intake_id: intakeId,\n class_name: String(body.class_name).trim(),\n assignment_name: String(body.assignment_name).trim(),\n submission_kind: String(body.submission_kind).trim(),\n semester: body.semester ? String(body.semester).trim() : null,\n course_code: body.course_code ? String(body.course_code).trim() : null,\n paper_kind: body.paper_kind ? String(body.paper_kind).trim() : null,\n notes: body.notes ? String(body.notes).trim().slice(0, 4000) : null,\n source: body.source ? String(body.source).trim() : 'telegram',\n telegram_chat_id: body.telegram_chat_id ? String(body.telegram_chat_id).trim() : null,\n telegram_message_id: body.telegram_message_id ? String(body.telegram_message_id).trim() : null,\n original_filename: originalName,\n stored_filename: storedFilename,\n mime_type: upload.mimeType || 'application/octet-stream',\n file_size_bytes: bytes,\n checksum_sha256: checksum,\n paperless_title: title,\n received_at: now.toISOString()\n },\n binary: {\n data: {\n ...upload,\n fileName: storedFilename,\n mimeType: upload.mimeType || 'application/octet-stream'\n }\n }\n}];"
|
|
},
|
|
"id": "normalize-request",
|
|
"name": "Normalize Intake Request",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
-920,
|
|
0
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"method": "POST",
|
|
"url": "https://paperless.paccoco.com/api/documents/post_document/",
|
|
"sendHeaders": true,
|
|
"headerParameters": {
|
|
"parameters": [
|
|
{
|
|
"name": "Authorization",
|
|
"value": "=Token {{ $env.PAPERLESS_API_TOKEN }}"
|
|
}
|
|
]
|
|
},
|
|
"sendBody": true,
|
|
"contentType": "multipart-form-data",
|
|
"bodyParameters": {
|
|
"parameters": [
|
|
{
|
|
"parameterType": "formBinaryData",
|
|
"name": "document",
|
|
"inputDataFieldName": "data"
|
|
},
|
|
{
|
|
"name": "title",
|
|
"value": "={{ $json.paperless_title }}"
|
|
}
|
|
]
|
|
},
|
|
"options": {
|
|
"response": {
|
|
"response": {
|
|
"responseFormat": "text"
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"id": "paperless-upload",
|
|
"name": "Upload to Paperless",
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.2,
|
|
"position": [
|
|
-400,
|
|
0
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "\nconst intake = $('Normalize Intake Request').first().json;\nconst response = $json;\nconst taskId = response.task_id || response.id || response.document?.id || null;\nlet paperlessDocumentId = response.document_id || response.document?.id || null;\nif (!paperlessDocumentId && typeof response === 'object' && response?.id && !response?.task_id) {\n paperlessDocumentId = response.id;\n}\nreturn [{ json: {\n ...intake,\n paperless_task_id: taskId ? String(taskId) : null,\n paperless_document_id: paperlessDocumentId ? String(paperlessDocumentId) : null,\n paperless_response: response,\n uploaded_at: new Date().toISOString(),\n status: 'uploaded'\n}}];"
|
|
},
|
|
"id": "capture-paperless-response",
|
|
"name": "Capture Paperless Response",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
-140,
|
|
0
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "\nreturn [{\n json: {\n ok: true,\n intake_id: $json.intake_id,\n status: $json.status || 'uploaded',\n paperless_task_id: $json.paperless_task_id,\n paperless_document_id: $json.paperless_document_id,\n paperless_title: $json.paperless_title,\n stored_filename: $json.stored_filename,\n received_at: $json.received_at,\n uploaded_at: $json.uploaded_at\n }\n}];"
|
|
},
|
|
"id": "success-response",
|
|
"name": "Return Success Payload",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
360,
|
|
0
|
|
]
|
|
}
|
|
],
|
|
"connections": {
|
|
"School Intake Webhook": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Normalize Intake Request",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Normalize Intake Request": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Upload to Paperless",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Upload to Paperless": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Capture Paperless Response",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Capture Paperless Response": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Return Success Payload",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
}
|
|
},
|
|
"settings": {
|
|
"executionOrder": "v1",
|
|
"binaryMode": "separate"
|
|
},
|
|
"staticData": null,
|
|
"meta": null,
|
|
"pinData": {},
|
|
"versionId": "27a8310d-fb26-4dfe-a093-611d684f4b67",
|
|
"activeVersionId": "27a8310d-fb26-4dfe-a093-611d684f4b67",
|
|
"versionCounter": 35,
|
|
"triggerCount": 1,
|
|
"shared": [
|
|
{
|
|
"updatedAt": "2026-05-13T21:15:57.655Z",
|
|
"createdAt": "2026-05-13T21:15:57.655Z",
|
|
"role": "workflow:owner",
|
|
"workflowId": "bu0kZAbLo2KUVkTl",
|
|
"projectId": "dxCRnBdX5uJizCGa",
|
|
"project": {
|
|
"updatedAt": "2026-05-06T01:10:37.484Z",
|
|
"createdAt": "2026-05-06T01:08:07.721Z",
|
|
"id": "dxCRnBdX5uJizCGa",
|
|
"name": "Wilfred Fizzlepoof <admin@paccoco.com>",
|
|
"type": "personal",
|
|
"icon": null,
|
|
"description": null,
|
|
"creatorId": "47b2227f-2fc2-4a08-bd35-ea157b92df0d"
|
|
}
|
|
}
|
|
],
|
|
"tags": [
|
|
{
|
|
"updatedAt": "2026-05-13T21:15:56.032Z",
|
|
"createdAt": "2026-05-13T21:15:56.032Z",
|
|
"id": "h1moa2N6f4A1ArxD",
|
|
"name": "telegram"
|
|
},
|
|
{
|
|
"updatedAt": "2026-05-07T02:27:25.824Z",
|
|
"createdAt": "2026-05-07T02:27:25.824Z",
|
|
"id": "MfaTegnw8p0SsGGm",
|
|
"name": "postgres"
|
|
},
|
|
{
|
|
"updatedAt": "2026-05-06T22:39:46.476Z",
|
|
"createdAt": "2026-05-06T22:39:46.476Z",
|
|
"id": "NxHjuO4MMd5kdPR7",
|
|
"name": "paperless"
|
|
},
|
|
{
|
|
"updatedAt": "2026-05-07T00:49:56.081Z",
|
|
"createdAt": "2026-05-07T00:49:56.081Z",
|
|
"id": "Q9lCYtCOuy9XmeyL",
|
|
"name": "school"
|
|
}
|
|
],
|
|
"activeVersion": {
|
|
"updatedAt": "2026-05-14T01:53:21.609Z",
|
|
"createdAt": "2026-05-14T01:53:21.609Z",
|
|
"versionId": "27a8310d-fb26-4dfe-a093-611d684f4b67",
|
|
"workflowId": "bu0kZAbLo2KUVkTl",
|
|
"nodes": [
|
|
{
|
|
"parameters": {
|
|
"httpMethod": "POST",
|
|
"path": "school/intake/upload",
|
|
"responseMode": "lastNode",
|
|
"responseData": "allEntries",
|
|
"options": {
|
|
"rawBody": true
|
|
}
|
|
},
|
|
"id": "school-intake-webhook",
|
|
"name": "School Intake Webhook",
|
|
"type": "n8n-nodes-base.webhook",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
-1180,
|
|
0
|
|
],
|
|
"webhookId": "6251b84e-2ac7-49a3-9b05-7ca6540da134"
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "\nconst req = $input.first().json;\nconst body = req.body || {};\nconst binary = $input.first().binary || {};\nconst upload = binary.data || Object.values(binary)[0];\nif (!upload) throw new Error('Expected multipart file upload in the webhook request.');\nconst required = ['class_name', 'assignment_name', 'submission_kind'];\nfor (const key of required) {\n if (!body[key] || !String(body[key]).trim()) {\n throw new Error('Missing required form field: ' + key + ' . Required fields: class_name, assignment_name, submission_kind.');\n }\n}\nconst sanitize = (value, fallback = 'item') => String(value || fallback)\n .trim()\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '')\n .slice(0, 80) || fallback;\nconst stableHash = (str) => {\n let h1 = 0xdeadbeef;\n let h2 = 0x41c6ce57;\n for (let i = 0; i < str.length; i++) {\n const ch = str.charCodeAt(i);\n h1 = Math.imul(h1 ^ ch, 2654435761);\n h2 = Math.imul(h2 ^ ch, 1597334677);\n }\n h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ Math.imul(h2 ^ (h2 >>> 13), 3266489909);\n h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^ Math.imul(h1 ^ (h1 >>> 13), 3266489909);\n return ((h2 >>> 0).toString(16).padStart(8, '0') + (h1 >>> 0).toString(16).padStart(8, '0'));\n};\nconst originalName = upload.fileName || body.filename || 'upload.bin';\nconst extension = (() => {\n const m = String(originalName).match(/(\\.[A-Za-z0-9]{1,10})$/);\n return m ? m[1].toLowerCase() : '';\n})();\nconst bytes = Number(upload.bytes ?? 0) || (() => {\n const text = String(upload.fileSize || '').trim();\n const m = text.match(/^([0-9.]+)\\s*(B|KB|MB|GB)$/i);\n if (!m) return 0;\n const value = Number(m[1]);\n const unit = m[2].toUpperCase();\n const mult = unit === 'GB' ? 1024**3 : unit === 'MB' ? 1024**2 : unit === 'KB' ? 1024 : 1;\n return Number.isFinite(value) ? Math.round(value * mult) : 0;\n})();\nconst checksum = stableHash([originalName, upload.mimeType || '', String(bytes), String(body.class_name), String(body.assignment_name), String(body.submission_kind)].join('|'));\nconst now = new Date();\nconst intakeId = ['school', now.toISOString().slice(0,10).replace(/-/g,''), sanitize(body.class_name), sanitize(body.assignment_name), checksum.slice(0, 12)].join('-');\nconst storedFilename = intakeId + extension;\nconst title = [body.class_name, body.assignment_name, body.submission_kind].map(v => String(v).trim()).join(' — ');\nreturn [{\n json: {\n intake_id: intakeId,\n class_name: String(body.class_name).trim(),\n assignment_name: String(body.assignment_name).trim(),\n submission_kind: String(body.submission_kind).trim(),\n semester: body.semester ? String(body.semester).trim() : null,\n course_code: body.course_code ? String(body.course_code).trim() : null,\n paper_kind: body.paper_kind ? String(body.paper_kind).trim() : null,\n notes: body.notes ? String(body.notes).trim().slice(0, 4000) : null,\n source: body.source ? String(body.source).trim() : 'telegram',\n telegram_chat_id: body.telegram_chat_id ? String(body.telegram_chat_id).trim() : null,\n telegram_message_id: body.telegram_message_id ? String(body.telegram_message_id).trim() : null,\n original_filename: originalName,\n stored_filename: storedFilename,\n mime_type: upload.mimeType || 'application/octet-stream',\n file_size_bytes: bytes,\n checksum_sha256: checksum,\n paperless_title: title,\n received_at: now.toISOString()\n },\n binary: {\n data: {\n ...upload,\n fileName: storedFilename,\n mimeType: upload.mimeType || 'application/octet-stream'\n }\n }\n}];"
|
|
},
|
|
"id": "normalize-request",
|
|
"name": "Normalize Intake Request",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
-920,
|
|
0
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"method": "POST",
|
|
"url": "https://paperless.paccoco.com/api/documents/post_document/",
|
|
"sendHeaders": true,
|
|
"headerParameters": {
|
|
"parameters": [
|
|
{
|
|
"name": "Authorization",
|
|
"value": "=Token {{ $env.PAPERLESS_API_TOKEN }}"
|
|
}
|
|
]
|
|
},
|
|
"sendBody": true,
|
|
"contentType": "multipart-form-data",
|
|
"bodyParameters": {
|
|
"parameters": [
|
|
{
|
|
"parameterType": "formBinaryData",
|
|
"name": "document",
|
|
"inputDataFieldName": "data"
|
|
},
|
|
{
|
|
"name": "title",
|
|
"value": "={{ $json.paperless_title }}"
|
|
}
|
|
]
|
|
},
|
|
"options": {
|
|
"response": {
|
|
"response": {
|
|
"responseFormat": "text"
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"id": "paperless-upload",
|
|
"name": "Upload to Paperless",
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.2,
|
|
"position": [
|
|
-400,
|
|
0
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "\nconst intake = $('Normalize Intake Request').first().json;\nconst response = $json;\nconst taskId = response.task_id || response.id || response.document?.id || null;\nlet paperlessDocumentId = response.document_id || response.document?.id || null;\nif (!paperlessDocumentId && typeof response === 'object' && response?.id && !response?.task_id) {\n paperlessDocumentId = response.id;\n}\nreturn [{ json: {\n ...intake,\n paperless_task_id: taskId ? String(taskId) : null,\n paperless_document_id: paperlessDocumentId ? String(paperlessDocumentId) : null,\n paperless_response: response,\n uploaded_at: new Date().toISOString(),\n status: 'uploaded'\n}}];"
|
|
},
|
|
"id": "capture-paperless-response",
|
|
"name": "Capture Paperless Response",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
-140,
|
|
0
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "\nreturn [{\n json: {\n ok: true,\n intake_id: $json.intake_id,\n status: $json.status || 'uploaded',\n paperless_task_id: $json.paperless_task_id,\n paperless_document_id: $json.paperless_document_id,\n paperless_title: $json.paperless_title,\n stored_filename: $json.stored_filename,\n received_at: $json.received_at,\n uploaded_at: $json.uploaded_at\n }\n}];"
|
|
},
|
|
"id": "success-response",
|
|
"name": "Return Success Payload",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
360,
|
|
0
|
|
]
|
|
}
|
|
],
|
|
"connections": {
|
|
"School Intake Webhook": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Normalize Intake Request",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Normalize Intake Request": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Upload to Paperless",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Upload to Paperless": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Capture Paperless Response",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Capture Paperless Response": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Return Success Payload",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
}
|
|
},
|
|
"authors": "Wilfred Fizzlepoof",
|
|
"name": null,
|
|
"description": null,
|
|
"autosaved": false,
|
|
"workflowPublishHistory": [
|
|
{
|
|
"createdAt": "2026-05-14T01:53:21.640Z",
|
|
"id": 258,
|
|
"workflowId": "bu0kZAbLo2KUVkTl",
|
|
"versionId": "27a8310d-fb26-4dfe-a093-611d684f4b67",
|
|
"event": "deactivated",
|
|
"userId": "47b2227f-2fc2-4a08-bd35-ea157b92df0d"
|
|
},
|
|
{
|
|
"createdAt": "2026-05-14T01:53:21.661Z",
|
|
"id": 259,
|
|
"workflowId": "bu0kZAbLo2KUVkTl",
|
|
"versionId": "27a8310d-fb26-4dfe-a093-611d684f4b67",
|
|
"event": "activated",
|
|
"userId": "47b2227f-2fc2-4a08-bd35-ea157b92df0d"
|
|
}
|
|
]
|
|
}
|
|
}
|