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
This commit is contained in:
Lucas Berger
2026-01-30 08:42:43 -05:00
parent 56eea26d44
commit 768d7584e2
+416 -1
View File
@@ -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": [
[