From 768d7584e2e063cdbeb4981ef9e0764c05162a65 Mon Sep 17 00:00:00 2001 From: Lucas Berger Date: Fri, 30 Jan 2026 08:42:43 -0500 Subject: [PATCH] feat(03-02): handle suggestion callback and execute action - Add Parse Callback Data code node to decode callback_query JSON - Add Route Callback switch for cancel/expired/execute branches - Add Handle Cancel with answer query and delete message - Add Handle Expired with alert message and delete message - Add Build Callback Action to construct curl command from callback - Add Execute Callback Action to run Docker API call - Add Parse Callback Result to check status and build response - Add Answer Action Query, Delete Suggestion Message, Send Callback Result - 2-minute timeout enforced via timestamp in callback_data --- n8n-workflow.json | 417 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 416 insertions(+), 1 deletion(-) diff --git a/n8n-workflow.json b/n8n-workflow.json index 312beba..a638b73 100644 --- a/n8n-workflow.json +++ b/n8n-workflow.json @@ -641,6 +641,279 @@ "name": "Telegram API" } } + }, + { + "parameters": { + "jsCode": "// Parse callback data from suggestion button click\nconst callback = $json.callback_query;\nlet data;\ntry {\n data = JSON.parse(callback.data);\n} catch (e) {\n data = { a: 'x' }; // Treat parse error as cancel\n}\n\nconst queryId = callback.id;\nconst chatId = callback.message.chat.id;\nconst messageId = callback.message.message_id;\n\n// Check 2-minute timeout (120000ms)\nconst TWO_MINUTES = 120000;\nconst isExpired = data.t && (Date.now() - data.t > TWO_MINUTES);\n\n// Decode action: s=start, t=stop, r=restart, x=cancel\nconst actionMap = { s: 'start', t: 'stop', r: 'restart', x: 'cancel' };\nconst action = actionMap[data.a] || 'cancel';\n\nreturn {\n json: {\n queryId,\n chatId,\n messageId,\n action,\n containerId: data.c || null,\n expired: isExpired,\n isCancel: action === 'cancel'\n }\n};" + }, + "id": "code-parse-callback", + "name": "Parse Callback Data", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [900, 500] + }, + { + "parameters": { + "rules": { + "values": [ + { + "id": "is-cancel", + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict" + }, + "conditions": [ + { + "id": "cancel-true", + "leftValue": "={{ $json.isCancel }}", + "rightValue": true, + "operator": { + "type": "boolean", + "operation": "equals" + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "cancel" + }, + { + "id": "is-expired", + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict" + }, + "conditions": [ + { + "id": "expired-true", + "leftValue": "={{ $json.expired }}", + "rightValue": true, + "operator": { + "type": "boolean", + "operation": "equals" + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "expired" + } + ] + }, + "options": { + "fallbackOutput": "extra" + } + }, + "id": "switch-route-callback", + "name": "Route Callback", + "type": "n8n-nodes-base.switch", + "typeVersion": 3.2, + "position": [1120, 500] + }, + { + "parameters": { + "jsCode": "// Prepare cancel response - answer callback query and delete message\nconst { queryId, chatId, messageId } = $json;\nreturn {\n json: {\n queryId,\n chatId,\n messageId,\n answerText: 'Cancelled'\n }\n};" + }, + "id": "code-handle-cancel", + "name": "Handle Cancel", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [1340, 500] + }, + { + "parameters": { + "method": "POST", + "url": "=https://api.telegram.org/bot{{ $credentials.telegramApi.accessToken }}/answerCallbackQuery", + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={{ JSON.stringify({ callback_query_id: $json.queryId, text: $json.answerText }) }}", + "options": {} + }, + "id": "http-answer-cancel", + "name": "Answer Cancel Query", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [1560, 500], + "credentials": { + "telegramApi": { + "id": "telegram-credential", + "name": "Telegram API" + } + } + }, + { + "parameters": { + "method": "POST", + "url": "=https://api.telegram.org/bot{{ $credentials.telegramApi.accessToken }}/deleteMessage", + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={{ JSON.stringify({ chat_id: $('Handle Cancel').item.json.chatId, message_id: $('Handle Cancel').item.json.messageId }) }}", + "options": {} + }, + "id": "http-delete-cancel-msg", + "name": "Delete Cancel Message", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [1780, 500], + "credentials": { + "telegramApi": { + "id": "telegram-credential", + "name": "Telegram API" + } + } + }, + { + "parameters": { + "jsCode": "// Prepare expired response\nconst { queryId, chatId, messageId } = $json;\nreturn {\n json: {\n queryId,\n chatId,\n messageId,\n answerText: 'Confirmation expired. Please try again.'\n }\n};" + }, + "id": "code-handle-expired", + "name": "Handle Expired", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [1340, 600] + }, + { + "parameters": { + "method": "POST", + "url": "=https://api.telegram.org/bot{{ $credentials.telegramApi.accessToken }}/answerCallbackQuery", + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={{ JSON.stringify({ callback_query_id: $json.queryId, text: $json.answerText, show_alert: true }) }}", + "options": {} + }, + "id": "http-answer-expired", + "name": "Answer Expired Query", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [1560, 600], + "credentials": { + "telegramApi": { + "id": "telegram-credential", + "name": "Telegram API" + } + } + }, + { + "parameters": { + "method": "POST", + "url": "=https://api.telegram.org/bot{{ $credentials.telegramApi.accessToken }}/deleteMessage", + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={{ JSON.stringify({ chat_id: $('Handle Expired').item.json.chatId, message_id: $('Handle Expired').item.json.messageId }) }}", + "options": {} + }, + "id": "http-delete-expired-msg", + "name": "Delete Expired Message", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [1780, 600], + "credentials": { + "telegramApi": { + "id": "telegram-credential", + "name": "Telegram API" + } + } + }, + { + "parameters": { + "jsCode": "// Build curl command for callback action execution\nconst data = $input.item.json;\nconst containerId = data.containerId;\nconst action = data.action;\nconst chatId = data.chatId;\nconst messageId = data.messageId;\nconst queryId = data.queryId;\n\n// stop and restart use ?t=10 for graceful timeout\nconst timeout = (action === 'stop' || action === 'restart') ? '?t=10' : '';\n\n// Build curl command that returns HTTP status code\nconst cmd = `curl -s -o /dev/null -w \"%{http_code}\" --unix-socket /var/run/docker.sock -X POST 'http://localhost/v1.47/containers/${containerId}/${action}${timeout}'`;\n\nreturn {\n json: {\n cmd,\n containerId,\n action,\n chatId,\n messageId,\n queryId\n }\n};" + }, + "id": "code-build-callback-cmd", + "name": "Build Callback Action", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [1340, 700] + }, + { + "parameters": { + "command": "={{ $json.cmd }}", + "options": {} + }, + "id": "exec-callback-action", + "name": "Execute Callback Action", + "type": "n8n-nodes-base.executeCommand", + "typeVersion": 1, + "position": [1560, 700] + }, + { + "parameters": { + "jsCode": "// Parse callback action result and get container name\nconst stdout = $input.item.json.stdout;\nconst stderr = $input.item.json.stderr;\nconst cmdData = $('Build Callback Action').item.json;\nconst containerId = cmdData.containerId;\nconst action = cmdData.action;\nconst chatId = cmdData.chatId;\nconst messageId = cmdData.messageId;\nconst queryId = cmdData.queryId;\n\n// Check for curl-level errors first\nif (stderr && stderr.trim()) {\n return {\n json: {\n success: false,\n chatId,\n messageId,\n queryId,\n text: `Failed to ${action}: ${stderr.trim()}`,\n answerText: 'Action failed'\n }\n };\n}\n\nconst statusCode = parseInt(stdout.trim());\n\n// 204: Success, 304: Already in state (also success)\nif (statusCode === 204 || statusCode === 304) {\n const verb = action === 'start' ? 'started' :\n action === 'stop' ? 'stopped' : 'restarted';\n return {\n json: {\n success: true,\n chatId,\n messageId,\n queryId,\n containerId,\n text: `Container ${verb} successfully`,\n answerText: `Container ${verb}`\n }\n };\n}\n\n// Handle error codes\nlet errorMsg;\nswitch (statusCode) {\n case 404:\n errorMsg = 'Container not found';\n break;\n case 409:\n errorMsg = 'Container is in a conflicting state';\n break;\n case 500:\n errorMsg = 'Docker server error';\n break;\n default:\n errorMsg = `HTTP ${statusCode}`;\n}\n\nreturn {\n json: {\n success: false,\n chatId,\n messageId,\n queryId,\n text: `Failed to ${action}: ${errorMsg}`,\n answerText: 'Action failed'\n }\n};" + }, + "id": "code-parse-callback-result", + "name": "Parse Callback Result", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [1780, 700] + }, + { + "parameters": { + "method": "POST", + "url": "=https://api.telegram.org/bot{{ $credentials.telegramApi.accessToken }}/answerCallbackQuery", + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={{ JSON.stringify({ callback_query_id: $json.queryId, text: $json.answerText }) }}", + "options": {} + }, + "id": "http-answer-action", + "name": "Answer Action Query", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [2000, 700], + "credentials": { + "telegramApi": { + "id": "telegram-credential", + "name": "Telegram API" + } + } + }, + { + "parameters": { + "method": "POST", + "url": "=https://api.telegram.org/bot{{ $credentials.telegramApi.accessToken }}/deleteMessage", + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={{ JSON.stringify({ chat_id: $('Parse Callback Result').item.json.chatId, message_id: $('Parse Callback Result').item.json.messageId }) }}", + "options": {} + }, + "id": "http-delete-suggestion-msg", + "name": "Delete Suggestion Message", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [2220, 700], + "credentials": { + "telegramApi": { + "id": "telegram-credential", + "name": "Telegram API" + } + } + }, + { + "parameters": { + "resource": "message", + "operation": "sendMessage", + "chatId": "={{ $('Parse Callback Result').item.json.chatId }}", + "text": "={{ $('Parse Callback Result').item.json.text }}", + "additionalFields": { + "parse_mode": "HTML" + } + }, + "id": "telegram-send-callback-result", + "name": "Send Callback Result", + "type": "n8n-nodes-base.telegram", + "typeVersion": 1.2, + "position": [2440, 700], + "credentials": { + "telegramApi": { + "id": "telegram-credential", + "name": "Telegram API" + } + } } ], "connections": { @@ -687,10 +960,152 @@ }, "IF Callback Authenticated": { "main": [ - [], + [ + { + "node": "Parse Callback Data", + "type": "main", + "index": 0 + } + ], [] ] }, + "Parse Callback Data": { + "main": [ + [ + { + "node": "Route Callback", + "type": "main", + "index": 0 + } + ] + ] + }, + "Route Callback": { + "main": [ + [ + { + "node": "Handle Cancel", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Handle Expired", + "type": "main", + "index": 0 + } + ], + [], + [ + { + "node": "Build Callback Action", + "type": "main", + "index": 0 + } + ] + ] + }, + "Handle Cancel": { + "main": [ + [ + { + "node": "Answer Cancel Query", + "type": "main", + "index": 0 + } + ] + ] + }, + "Answer Cancel Query": { + "main": [ + [ + { + "node": "Delete Cancel Message", + "type": "main", + "index": 0 + } + ] + ] + }, + "Handle Expired": { + "main": [ + [ + { + "node": "Answer Expired Query", + "type": "main", + "index": 0 + } + ] + ] + }, + "Answer Expired Query": { + "main": [ + [ + { + "node": "Delete Expired Message", + "type": "main", + "index": 0 + } + ] + ] + }, + "Build Callback Action": { + "main": [ + [ + { + "node": "Execute Callback Action", + "type": "main", + "index": 0 + } + ] + ] + }, + "Execute Callback Action": { + "main": [ + [ + { + "node": "Parse Callback Result", + "type": "main", + "index": 0 + } + ] + ] + }, + "Parse Callback Result": { + "main": [ + [ + { + "node": "Answer Action Query", + "type": "main", + "index": 0 + } + ] + ] + }, + "Answer Action Query": { + "main": [ + [ + { + "node": "Delete Suggestion Message", + "type": "main", + "index": 0 + } + ] + ] + }, + "Delete Suggestion Message": { + "main": [ + [ + { + "node": "Send Callback Result", + "type": "main", + "index": 0 + } + ] + ] + }, "Route Message": { "main": [ [