add postiz + instagram-poster stacks for IG Stories pipeline
New stacks: - stacks/postiz/ — Postiz scheduler (Helm chart v1.0.5, image v2.21.7) with bundled PG/Redis, /uploads PVC on proxmox-lvm, JWT_SECRET via ESO from secret/instagram-poster. - stacks/instagram-poster/ — custom Python service that polls Immich for the 'instagram' tag, reformats photos to 9:16 with blurred-bg letterbox, exposes /image/<asset_id> publicly so Postiz can fetch. Image: forgejo.viktorbarzin.me/viktor/instagram-poster. n8n: 3 new workflows (discover, approval, post) for the Telegram inline-button approval UX. Adds ExternalSecret + env vars for TELEGRAM_BOT_TOKEN, TELEGRAM_CHAT_ID, IMMICH_API_KEY, plus static URLs for the new service. Vault: seed secret/instagram-poster with telegram_bot_token, telegram_chat_id, immich_api_key, postiz_api_token, postiz_jwt_secret before applying.
This commit is contained in:
parent
badc341669
commit
73eb01f994
14 changed files with 1276 additions and 0 deletions
269
stacks/n8n/workflows/instagram-approval.json
Normal file
269
stacks/n8n/workflows/instagram-approval.json
Normal file
|
|
@ -0,0 +1,269 @@
|
|||
{
|
||||
"name": "Instagram Approval",
|
||||
"active": true,
|
||||
"id": "483773c0-0b62-4ae5-b1b1-345f5df7b133",
|
||||
"versionId": "483773c0-0b62-4ae5-b1b1-345f5df7b133",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {
|
||||
"updates": ["callback_query"],
|
||||
"additionalFields": {}
|
||||
},
|
||||
"id": "telegram-trigger",
|
||||
"name": "Telegram callback_query",
|
||||
"type": "n8n-nodes-base.telegramTrigger",
|
||||
"typeVersion": 1.1,
|
||||
"position": [250, 400],
|
||||
"webhookId": "f2c7c254-ebaf-4f66-b1b4-5c1629c07e08",
|
||||
"credentials": {
|
||||
"telegramApi": {
|
||||
"id": "telegram-bot",
|
||||
"name": "Telegram Bot"
|
||||
}
|
||||
},
|
||||
"notes": "Listens for inline-button taps. Requires a Telegram credential bound to the same bot whose token is in TELEGRAM_BOT_TOKEN."
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "const cb = $input.first().json.callback_query || {};\nconst data = cb.data || '';\nconst [action, assetId] = data.split(':');\nconst message = cb.message || {};\nconst chatId = (message.chat || {}).id;\nconst messageId = message.message_id;\nconst originalCaption = message.caption || '';\nconst callbackQueryId = cb.id;\n\nif (!action || !assetId) {\n throw new Error('Malformed callback_data: ' + data);\n}\n\nreturn [{\n json: {\n action,\n asset_id: assetId,\n chat_id: chatId,\n message_id: messageId,\n original_caption: originalCaption,\n callback_query_id: callbackQueryId\n }\n}];"
|
||||
},
|
||||
"id": "parse-callback",
|
||||
"name": "Parse callback_data",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [470, 400],
|
||||
"notes": "Splits callback_data into action + asset_id, captures chat_id/message_id/caption for later edits and answerCallbackQuery."
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"rules": {
|
||||
"values": [
|
||||
{
|
||||
"conditions": {
|
||||
"options": {"caseSensitive": true, "leftValue": "", "typeValidation": "strict"},
|
||||
"conditions": [{"id": "is-approve", "leftValue": "={{ $json.action }}", "rightValue": "approve", "operator": {"type": "string", "operation": "equals"}}],
|
||||
"combinator": "and"
|
||||
},
|
||||
"outputKey": "approve"
|
||||
},
|
||||
{
|
||||
"conditions": {
|
||||
"options": {"caseSensitive": true, "leftValue": "", "typeValidation": "strict"},
|
||||
"conditions": [{"id": "is-reject", "leftValue": "={{ $json.action }}", "rightValue": "reject", "operator": {"type": "string", "operation": "equals"}}],
|
||||
"combinator": "and"
|
||||
},
|
||||
"outputKey": "reject"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "switch-action",
|
||||
"name": "Switch on action",
|
||||
"type": "n8n-nodes-base.switch",
|
||||
"typeVersion": 3.2,
|
||||
"position": [690, 400],
|
||||
"notes": "Branches on action; unknown actions fall through and are dropped."
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "={{ $env.INSTAGRAM_POSTER_INTERNAL_URL }}/enqueue",
|
||||
"sendHeaders": true,
|
||||
"headerParameters": {
|
||||
"parameters": [
|
||||
{"name": "Authorization", "value": "=Bearer {{ $env.INSTAGRAM_POSTER_TOKEN }}"},
|
||||
{"name": "Content-Type", "value": "application/json"}
|
||||
]
|
||||
},
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={{ JSON.stringify({ asset_id: $json.asset_id }) }}",
|
||||
"options": {"timeout": 30000}
|
||||
},
|
||||
"id": "approve-enqueue",
|
||||
"name": "Approve: enqueue asset",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"position": [910, 250],
|
||||
"onError": "continueErrorOutput",
|
||||
"notes": "Calls instagram-poster /enqueue. continueErrorOutput so we can fall through to a Telegram error message on failure."
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "={{ $env.INSTAGRAM_POSTER_INTERNAL_URL }}/reject",
|
||||
"sendHeaders": true,
|
||||
"headerParameters": {
|
||||
"parameters": [
|
||||
{"name": "Authorization", "value": "=Bearer {{ $env.INSTAGRAM_POSTER_TOKEN }}"},
|
||||
{"name": "Content-Type", "value": "application/json"}
|
||||
]
|
||||
},
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={{ JSON.stringify({ asset_id: $json.asset_id }) }}",
|
||||
"options": {"timeout": 30000}
|
||||
},
|
||||
"id": "reject-mark",
|
||||
"name": "Reject: mark seen",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"position": [910, 550],
|
||||
"onError": "continueErrorOutput",
|
||||
"notes": "Calls instagram-poster /reject so the asset is recorded and not re-surfaced."
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "const upstream = $('Parse callback_data').item.json;\nconst suffix = '\\n\\nQueued for posting';\nreturn [{ json: { ...upstream, new_caption: (upstream.original_caption || '') + suffix } }];"
|
||||
},
|
||||
"id": "approve-caption",
|
||||
"name": "Approve: build new caption",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [1130, 250],
|
||||
"notes": "Append a confirmation suffix to the original caption."
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "const upstream = $('Parse callback_data').item.json;\nconst suffix = '\\n\\nRejected';\nreturn [{ json: { ...upstream, new_caption: (upstream.original_caption || '') + suffix } }];"
|
||||
},
|
||||
"id": "reject-caption",
|
||||
"name": "Reject: build new caption",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [1130, 550],
|
||||
"notes": "Append rejection suffix to the original caption."
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/editMessageCaption",
|
||||
"sendHeaders": true,
|
||||
"headerParameters": {
|
||||
"parameters": [
|
||||
{"name": "Content-Type", "value": "application/json"}
|
||||
]
|
||||
},
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={{ JSON.stringify({ chat_id: $json.chat_id, message_id: $json.message_id, caption: $json.new_caption, parse_mode: 'HTML' }) }}",
|
||||
"options": {"timeout": 30000}
|
||||
},
|
||||
"id": "edit-caption",
|
||||
"name": "Telegram editMessageCaption",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"position": [1350, 400],
|
||||
"notes": "Updates the original DM caption to show the resulting state. Strips inline buttons in the same call by omitting reply_markup combined with the next node."
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/editMessageReplyMarkup",
|
||||
"sendHeaders": true,
|
||||
"headerParameters": {
|
||||
"parameters": [
|
||||
{"name": "Content-Type", "value": "application/json"}
|
||||
]
|
||||
},
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={{ JSON.stringify({ chat_id: $json.chat_id, message_id: $json.message_id, reply_markup: { inline_keyboard: [] } }) }}",
|
||||
"options": {"timeout": 30000}
|
||||
},
|
||||
"id": "edit-reply-markup",
|
||||
"name": "Telegram editMessageReplyMarkup",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"position": [1570, 400],
|
||||
"notes": "Strips the inline approve/reject buttons so the original DM no longer offers them after a decision."
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/answerCallbackQuery",
|
||||
"sendHeaders": true,
|
||||
"headerParameters": {
|
||||
"parameters": [
|
||||
{"name": "Content-Type", "value": "application/json"}
|
||||
]
|
||||
},
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={{ JSON.stringify({ callback_query_id: $json.callback_query_id, text: 'Recorded' }) }}",
|
||||
"options": {"timeout": 15000}
|
||||
},
|
||||
"id": "answer-callback",
|
||||
"name": "Telegram answerCallbackQuery",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"position": [1790, 400],
|
||||
"notes": "Dismisses the loading spinner on the user's tap."
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "const cb = $('Parse callback_data').item.json;\nconst err = $input.first().json.error || $input.first().json;\nconst msg = (err && (err.message || err.description || JSON.stringify(err))) || 'unknown error';\nreturn [{ json: { chat_id: cb.chat_id, text: 'Instagram poster error for ' + cb.asset_id + ':\\n' + msg } }];"
|
||||
},
|
||||
"id": "build-error-msg",
|
||||
"name": "Build error message",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [1130, 750],
|
||||
"notes": "Catches non-2xx from /enqueue or /reject and formats a Telegram alert text."
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/sendMessage",
|
||||
"sendHeaders": true,
|
||||
"headerParameters": {
|
||||
"parameters": [
|
||||
{"name": "Content-Type", "value": "application/json"}
|
||||
]
|
||||
},
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={{ JSON.stringify({ chat_id: $json.chat_id, text: $json.text }) }}",
|
||||
"options": {"timeout": 15000}
|
||||
},
|
||||
"id": "telegram-error-msg",
|
||||
"name": "Telegram error notice",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"position": [1350, 750],
|
||||
"notes": "Sends the error text to the user as a follow-up message."
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"Telegram callback_query": {"main": [[{"node": "Parse callback_data", "type": "main", "index": 0}]]},
|
||||
"Parse callback_data": {"main": [[{"node": "Switch on action", "type": "main", "index": 0}]]},
|
||||
"Switch on action": {
|
||||
"main": [
|
||||
[{"node": "Approve: enqueue asset", "type": "main", "index": 0}],
|
||||
[{"node": "Reject: mark seen", "type": "main", "index": 0}]
|
||||
]
|
||||
},
|
||||
"Approve: enqueue asset": {
|
||||
"main": [
|
||||
[{"node": "Approve: build new caption", "type": "main", "index": 0}],
|
||||
[{"node": "Build error message", "type": "main", "index": 0}]
|
||||
]
|
||||
},
|
||||
"Reject: mark seen": {
|
||||
"main": [
|
||||
[{"node": "Reject: build new caption", "type": "main", "index": 0}],
|
||||
[{"node": "Build error message", "type": "main", "index": 0}]
|
||||
]
|
||||
},
|
||||
"Approve: build new caption": {"main": [[{"node": "Telegram editMessageCaption", "type": "main", "index": 0}]]},
|
||||
"Reject: build new caption": {"main": [[{"node": "Telegram editMessageCaption", "type": "main", "index": 0}]]},
|
||||
"Telegram editMessageCaption": {"main": [[{"node": "Telegram editMessageReplyMarkup", "type": "main", "index": 0}]]},
|
||||
"Telegram editMessageReplyMarkup": {"main": [[{"node": "Telegram answerCallbackQuery", "type": "main", "index": 0}]]},
|
||||
"Build error message": {"main": [[{"node": "Telegram error notice", "type": "main", "index": 0}]]}
|
||||
},
|
||||
"settings": {"executionOrder": "v1", "saveExecutionProgress": false, "saveManualExecutions": true},
|
||||
"staticData": null,
|
||||
"meta": {"templateCredsSetupCompleted": false},
|
||||
"pinData": {}
|
||||
}
|
||||
132
stacks/n8n/workflows/instagram-discover.json
Normal file
132
stacks/n8n/workflows/instagram-discover.json
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
{
|
||||
"name": "Instagram Discover",
|
||||
"active": true,
|
||||
"id": "3bae241e-c693-49aa-b271-51af0ec811dc",
|
||||
"versionId": "3bae241e-c693-49aa-b271-51af0ec811dc",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {
|
||||
"rule": {
|
||||
"interval": [{"field": "minutes", "minutesInterval": 30}]
|
||||
}
|
||||
},
|
||||
"id": "cron-30min",
|
||||
"name": "Every 30 minutes",
|
||||
"type": "n8n-nodes-base.scheduleTrigger",
|
||||
"typeVersion": 1.1,
|
||||
"position": [250, 300],
|
||||
"notes": "Trigger every 30 minutes. Polling cadence is conservative for a personal pipeline; matches instagram-poster scan rate."
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "={{ $env.INSTAGRAM_POSTER_INTERNAL_URL }}/scan",
|
||||
"sendHeaders": true,
|
||||
"headerParameters": {
|
||||
"parameters": [
|
||||
{"name": "Authorization", "value": "=Bearer {{ $env.INSTAGRAM_POSTER_TOKEN }}"},
|
||||
{"name": "Content-Type", "value": "application/json"}
|
||||
]
|
||||
},
|
||||
"sendBody": false,
|
||||
"options": {"timeout": 60000}
|
||||
},
|
||||
"id": "scan-immich",
|
||||
"name": "Scan Immich for new candidates",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"position": [500, 300],
|
||||
"notes": "Calls instagram-poster /scan endpoint. Returns {\"new_items\": [\"asset-uuid\", ...]}. Authorization header is optional (internal cluster service); leave INSTAGRAM_POSTER_TOKEN blank if unused."
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"fieldToSplitOut": "new_items",
|
||||
"options": {}
|
||||
},
|
||||
"id": "split-items",
|
||||
"name": "Split new_items array",
|
||||
"type": "n8n-nodes-base.splitOut",
|
||||
"typeVersion": 1,
|
||||
"position": [750, 300],
|
||||
"notes": "Fan out one candidate per execution branch so each photo gets its own approval message."
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"batchSize": 1,
|
||||
"options": {}
|
||||
},
|
||||
"id": "batch-loop",
|
||||
"name": "Loop one at a time",
|
||||
"type": "n8n-nodes-base.splitInBatches",
|
||||
"typeVersion": 3,
|
||||
"position": [970, 300],
|
||||
"notes": "Process one asset at a time so errors on a single asset don't fan out and spam Telegram."
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "GET",
|
||||
"url": "={{ $env.IMMICH_BASE_URL }}/api/assets/{{ $json.new_items }}",
|
||||
"sendHeaders": true,
|
||||
"headerParameters": {
|
||||
"parameters": [
|
||||
{"name": "x-api-key", "value": "={{ $env.IMMICH_API_KEY }}"},
|
||||
{"name": "Accept", "value": "application/json"}
|
||||
]
|
||||
},
|
||||
"options": {"timeout": 30000}
|
||||
},
|
||||
"id": "fetch-asset-meta",
|
||||
"name": "Fetch Immich asset metadata",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"position": [1190, 300],
|
||||
"notes": "Pull asset metadata from Immich for caption preview (filename, fileCreatedAt, EXIF location)."
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "const item = $input.first().json;\nconst assetId = item.id || $('Loop one at a time').item.json.new_items;\nconst filename = item.originalFileName || item.originalPath || 'unknown';\nconst createdAt = item.fileCreatedAt || item.localDateTime || '';\nconst exif = item.exifInfo || {};\nconst city = exif.city || '';\nconst country = exif.country || '';\nconst location = [city, country].filter(Boolean).join(', ');\n\nconst dateStr = createdAt ? new Date(createdAt).toISOString().slice(0, 10) : '';\n\nconst lines = [\n '<b>New Immich candidate</b>',\n '',\n '<code>' + assetId + '</code>',\n '',\n '<b>File:</b> ' + filename\n];\nif (dateStr) lines.push('<b>Taken:</b> ' + dateStr);\nif (location) lines.push('<b>Where:</b> ' + location);\nlines.push('');\nlines.push('Approve to enqueue for posting, reject to mark seen.');\n\nreturn [{ json: { asset_id: assetId, caption: lines.join('\\n') } }];"
|
||||
},
|
||||
"id": "build-caption",
|
||||
"name": "Build caption + asset id",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [1410, 300],
|
||||
"notes": "Compose the caption from Immich metadata. HTML parse_mode is used downstream."
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/sendPhoto",
|
||||
"sendHeaders": true,
|
||||
"headerParameters": {
|
||||
"parameters": [
|
||||
{"name": "Content-Type", "value": "application/json"}
|
||||
]
|
||||
},
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={{ JSON.stringify({ chat_id: $env.TELEGRAM_CHAT_ID, photo: $env.PUBLIC_INSTAGRAM_POSTER_URL + '/image/' + $json.asset_id, caption: $json.caption, parse_mode: 'HTML', reply_markup: { inline_keyboard: [[ { text: 'Approve', callback_data: 'approve:' + $json.asset_id }, { text: 'Reject', callback_data: 'reject:' + $json.asset_id } ]] } }) }}",
|
||||
"options": {"timeout": 30000}
|
||||
},
|
||||
"id": "telegram-send-photo",
|
||||
"name": "Telegram sendPhoto with buttons",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"position": [1630, 300],
|
||||
"notes": "Telegram needs a public URL for the photo (it fetches the image from outside the cluster). callback_data uses the action:asset_id format consumed by instagram-approval."
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"Every 30 minutes": {"main": [[{"node": "Scan Immich for new candidates", "type": "main", "index": 0}]]},
|
||||
"Scan Immich for new candidates": {"main": [[{"node": "Split new_items array", "type": "main", "index": 0}]]},
|
||||
"Split new_items array": {"main": [[{"node": "Loop one at a time", "type": "main", "index": 0}]]},
|
||||
"Loop one at a time": {"main": [[{"node": "Fetch Immich asset metadata", "type": "main", "index": 0}]]},
|
||||
"Fetch Immich asset metadata": {"main": [[{"node": "Build caption + asset id", "type": "main", "index": 0}]]},
|
||||
"Build caption + asset id": {"main": [[{"node": "Telegram sendPhoto with buttons", "type": "main", "index": 0}]]},
|
||||
"Telegram sendPhoto with buttons": {"main": [[{"node": "Loop one at a time", "type": "main", "index": 0}]]}
|
||||
},
|
||||
"settings": {"executionOrder": "v1", "saveExecutionProgress": false, "saveManualExecutions": true},
|
||||
"staticData": null,
|
||||
"meta": {"templateCredsSetupCompleted": false},
|
||||
"pinData": {}
|
||||
}
|
||||
177
stacks/n8n/workflows/instagram-post.json
Normal file
177
stacks/n8n/workflows/instagram-post.json
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
{
|
||||
"name": "Instagram Post",
|
||||
"active": true,
|
||||
"id": "8964902b-b106-4cea-8965-77724baa71be",
|
||||
"versionId": "8964902b-b106-4cea-8965-77724baa71be",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {
|
||||
"rule": {
|
||||
"interval": [{"field": "days", "daysInterval": 1, "triggerAtHour": 11, "triggerAtMinute": 0}]
|
||||
}
|
||||
},
|
||||
"id": "cron-daily-11",
|
||||
"name": "Daily 11:00 Europe/London",
|
||||
"type": "n8n-nodes-base.scheduleTrigger",
|
||||
"typeVersion": 1.1,
|
||||
"position": [250, 300],
|
||||
"notes": "Fires once a day. Postiz handles per-platform scheduling windows; this just feeds the next approved asset to the poster service."
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "={{ $env.INSTAGRAM_POSTER_INTERNAL_URL }}/post-next",
|
||||
"sendHeaders": true,
|
||||
"headerParameters": {
|
||||
"parameters": [
|
||||
{"name": "Authorization", "value": "=Bearer {{ $env.INSTAGRAM_POSTER_TOKEN }}"},
|
||||
{"name": "Content-Type", "value": "application/json"}
|
||||
]
|
||||
},
|
||||
"sendBody": false,
|
||||
"options": {
|
||||
"timeout": 60000,
|
||||
"response": {"response": {"fullResponse": true, "neverError": true}}
|
||||
}
|
||||
},
|
||||
"id": "post-next",
|
||||
"name": "Call /post-next",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"position": [500, 300],
|
||||
"notes": "neverError + fullResponse gives us the status code so we can branch on 200 / 404 / 5xx without throwing."
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"rules": {
|
||||
"values": [
|
||||
{
|
||||
"conditions": {
|
||||
"options": {"caseSensitive": true, "leftValue": "", "typeValidation": "strict"},
|
||||
"conditions": [{"id": "is-200", "leftValue": "={{ $json.statusCode }}", "rightValue": 200, "operator": {"type": "number", "operation": "equals"}}],
|
||||
"combinator": "and"
|
||||
},
|
||||
"outputKey": "ok"
|
||||
},
|
||||
{
|
||||
"conditions": {
|
||||
"options": {"caseSensitive": true, "leftValue": "", "typeValidation": "strict"},
|
||||
"conditions": [{"id": "is-404", "leftValue": "={{ $json.statusCode }}", "rightValue": 404, "operator": {"type": "number", "operation": "equals"}}],
|
||||
"combinator": "and"
|
||||
},
|
||||
"outputKey": "empty"
|
||||
},
|
||||
{
|
||||
"conditions": {
|
||||
"options": {"caseSensitive": true, "leftValue": "", "typeValidation": "strict"},
|
||||
"conditions": [{"id": "is-5xx", "leftValue": "={{ $json.statusCode }}", "rightValue": 500, "operator": {"type": "number", "operation": "largerEqual"}}],
|
||||
"combinator": "and"
|
||||
},
|
||||
"outputKey": "error"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {"fallbackOutput": "extra", "renameFallbackOutput": "other"}
|
||||
},
|
||||
"id": "switch-status",
|
||||
"name": "Switch on status code",
|
||||
"type": "n8n-nodes-base.switch",
|
||||
"typeVersion": 3.2,
|
||||
"position": [750, 300],
|
||||
"notes": "200 -> success notify, 404 -> silent no-op, 5xx -> alert. Other 4xx falls into the fallback branch and is also alerted."
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "const body = $input.first().json.body || $input.first().json;\nconst assetId = (body && (body.asset_id || body.id)) || 'unknown';\nreturn [{ json: { chat_id: $env.TELEGRAM_CHAT_ID, text: 'Story scheduled: ' + assetId } }];"
|
||||
},
|
||||
"id": "build-success-msg",
|
||||
"name": "Build success message",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [1000, 150],
|
||||
"notes": "Pulls asset_id out of the response body for the confirmation Telegram message."
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/sendMessage",
|
||||
"sendHeaders": true,
|
||||
"headerParameters": {
|
||||
"parameters": [
|
||||
{"name": "Content-Type", "value": "application/json"}
|
||||
]
|
||||
},
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={{ JSON.stringify({ chat_id: $json.chat_id, text: $json.text }) }}",
|
||||
"options": {"timeout": 15000}
|
||||
},
|
||||
"id": "telegram-success",
|
||||
"name": "Telegram success notice",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"position": [1250, 150],
|
||||
"notes": "Confirms the scheduled post to the user."
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "const r = $input.first().json;\nconst body = r.body || {};\nconst err = body.error || JSON.stringify(body) || ('HTTP ' + r.statusCode);\nreturn [{ json: { chat_id: $env.TELEGRAM_CHAT_ID, text: 'Instagram post-next failed (HTTP ' + r.statusCode + '): ' + err } }];"
|
||||
},
|
||||
"id": "build-error-msg",
|
||||
"name": "Build error message",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [1000, 450],
|
||||
"notes": "Formats a Telegram alert with status code + body error message."
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/sendMessage",
|
||||
"sendHeaders": true,
|
||||
"headerParameters": {
|
||||
"parameters": [
|
||||
{"name": "Content-Type", "value": "application/json"}
|
||||
]
|
||||
},
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={{ JSON.stringify({ chat_id: $json.chat_id, text: $json.text }) }}",
|
||||
"options": {"timeout": 15000}
|
||||
},
|
||||
"id": "telegram-error",
|
||||
"name": "Telegram error alert",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"position": [1250, 450],
|
||||
"notes": "Sends the error message to the user."
|
||||
},
|
||||
{
|
||||
"parameters": {},
|
||||
"id": "noop-empty",
|
||||
"name": "Empty queue (no-op)",
|
||||
"type": "n8n-nodes-base.noOp",
|
||||
"typeVersion": 1,
|
||||
"position": [1000, 300],
|
||||
"notes": "404 means there are no approved items waiting; do nothing instead of spamming Telegram."
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"Daily 11:00 Europe/London": {"main": [[{"node": "Call /post-next", "type": "main", "index": 0}]]},
|
||||
"Call /post-next": {"main": [[{"node": "Switch on status code", "type": "main", "index": 0}]]},
|
||||
"Switch on status code": {
|
||||
"main": [
|
||||
[{"node": "Build success message", "type": "main", "index": 0}],
|
||||
[{"node": "Empty queue (no-op)", "type": "main", "index": 0}],
|
||||
[{"node": "Build error message", "type": "main", "index": 0}],
|
||||
[{"node": "Build error message", "type": "main", "index": 0}]
|
||||
]
|
||||
},
|
||||
"Build success message": {"main": [[{"node": "Telegram success notice", "type": "main", "index": 0}]]},
|
||||
"Build error message": {"main": [[{"node": "Telegram error alert", "type": "main", "index": 0}]]}
|
||||
},
|
||||
"settings": {"executionOrder": "v1", "saveExecutionProgress": false, "saveManualExecutions": true},
|
||||
"staticData": null,
|
||||
"meta": {"templateCredsSetupCompleted": false},
|
||||
"pinData": {}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue