diff --git a/n8n-workflow.json b/n8n-workflow.json index 633ea8d..436e51b 100644 --- a/n8n-workflow.json +++ b/n8n-workflow.json @@ -629,7 +629,7 @@ }, { "parameters": { - "jsCode": "// Build suggestion keyboard for Telegram\nconst { chatId, query, action, suggestedName, suggestedId, timestamp } = $json;\n\n// callback_data must be <=64 bytes - use short keys\n// a=action (1 char: s=start, t=stop, r=restart)\n// c=container short ID\n// t=timestamp\nconst actionCode = action === 'start' ? 's' : action === 'stop' ? 't' : 'r';\nconst callbackData = JSON.stringify({ a: actionCode, c: suggestedId, t: timestamp });\n\nreturn {\n json: {\n chat_id: chatId,\n text: `No container '${query}' found.\\n\\nDid you mean ${suggestedName}?`,\n parse_mode: \"HTML\",\n reply_markup: {\n inline_keyboard: [\n [\n { text: `Yes, ${action} ${suggestedName}`, callback_data: callbackData },\n { text: \"Cancel\", callback_data: '{\"a\":\"x\"}' }\n ]\n ]\n }\n }\n};" + "jsCode": "// Build suggestion keyboard for Telegram\nconst { chatId, query, action, suggestedName, suggestedId, timestamp } = $json;\n\n// Use modern action callback format: action:{action}:{containerName}\nconst callbackData = `action:${action}:${suggestedName}`;\n\nreturn {\n json: {\n chat_id: chatId,\n text: `No container '${query}' found.\\n\\nDid you mean ${suggestedName}?`,\n parse_mode: \"HTML\",\n reply_markup: {\n inline_keyboard: [\n [\n { text: `Yes, ${action} ${suggestedName}`, callback_data: callbackData },\n { text: \"Cancel\", callback_data: 'batch:cancel' }\n ]\n ]\n }\n }\n};" }, "id": "code-build-suggestion", "name": "Build Suggestion Keyboard", @@ -691,7 +691,7 @@ }, { "parameters": { - "jsCode": "// Build batch confirmation keyboard for multiple matches\nconst matches = $json.matches;\nconst action = $json.action;\nconst chatId = $json.chatId;\nconst query = $json.containerQuery;\n\n// Validate chatId exists - critical for Telegram API\nif (!chatId) {\n throw new Error('Missing chatId - cannot send batch confirmation. Data: ' + JSON.stringify({ matches: matches?.length, action, query }));\n}\n\n// List matched container names\nconst names = matches.map(m => m.Name);\nconst shortIds = matches.map(m => m.Id.substring(0, 12));\n\n// Build callback_data - must be <=64 bytes\n// For batch: a=action code, c=array of short IDs, t=timestamp\nconst actionCode = action === 'start' ? 's' : action === 'stop' ? 't' : 'r';\nconst timestamp = Date.now();\n\n// Check size - if too many containers, callback_data might exceed 64 bytes\n// Each short ID is 12 chars, plus overhead. Max ~3-4 containers safely\nlet callbackData;\nlet limitedCount = shortIds.length;\nif (shortIds.length <= 4) {\n callbackData = JSON.stringify({ a: actionCode, c: shortIds, t: timestamp });\n} else {\n // Too many containers - limit to first 4\n callbackData = JSON.stringify({ a: actionCode, c: shortIds.slice(0, 4), t: timestamp });\n limitedCount = 4;\n}\n\n// Format container list\nconst listText = names.map(n => ` \\u2022 ${n}`).join('\\n');\n\n// Build action verb for button\nconst actionVerb = action.charAt(0).toUpperCase() + action.slice(1);\n\nreturn {\n json: {\n chat_id: chatId,\n text: `Found ${matches.length} containers matching '${query}':\\n\\n${listText}\\n\\n${actionVerb} all?`,\n parse_mode: \"HTML\",\n reply_markup: {\n inline_keyboard: [\n [\n { text: `Yes, ${action} ${limitedCount} containers`, callback_data: callbackData },\n { text: \"Cancel\", callback_data: '{\"a\":\"x\"}' }\n ]\n ]\n },\n // Store metadata for summary\n _meta: {\n action,\n containers: matches,\n timestamp,\n limitedCount\n }\n }\n};" + "jsCode": "// Build batch confirmation keyboard for multiple matches\nconst matches = $json.matches;\nconst action = $json.action;\nconst chatId = $json.chatId;\nconst query = $json.containerQuery;\n\n// Validate chatId exists - critical for Telegram API\nif (!chatId) {\n throw new Error('Missing chatId - cannot send batch confirmation. Data: ' + JSON.stringify({ matches: matches?.length, action, query }));\n}\n\n// List matched container names\nconst names = matches.map(m => m.Name);\nconst timestamp = Math.floor(Date.now() / 1000); // Unix timestamp for 30s timeout\n\n// Build callback_data using new bexec format\n// Format: bexec:{action}:{comma-separated-names}:{timestamp}\n// Limit to 4 containers to stay within 64-byte callback_data limit\nlet limitedNames = names;\nlet limitedCount = names.length;\nif (names.length > 4) {\n limitedNames = names.slice(0, 4);\n limitedCount = 4;\n}\n\nconst namesStr = limitedNames.join(',');\nconst callbackData = `bexec:${action}:${namesStr}:${timestamp}`;\n\n// Format container list\nconst listText = names.map(n => ` \\u2022 ${n}`).join('\\n');\n\n// Build action verb for button\nconst actionVerb = action.charAt(0).toUpperCase() + action.slice(1);\n\nreturn {\n json: {\n chat_id: chatId,\n text: `Found ${matches.length} containers matching '${query}':\\n\\n${listText}\\n\\n${actionVerb} all?`,\n parse_mode: \"HTML\",\n reply_markup: {\n inline_keyboard: [\n [\n { text: `Yes, ${action} ${limitedCount} containers`, callback_data: callbackData },\n { text: \"Cancel\", callback_data: 'batch:cancel' }\n ]\n ]\n },\n // Store metadata for summary\n _meta: {\n action,\n containers: matches,\n timestamp,\n limitedCount\n }\n }\n};" }, "id": "code-build-batch-keyboard", "name": "Build Batch Keyboard", @@ -1518,96 +1518,6 @@ } } }, - { - "parameters": { - "jsCode": "// Execute batch action on all containers sequentially\nconst containerIds = $json.containerIds;\nconst action = $json.action;\nconst chatId = $json.chatId;\nconst messageId = $json.messageId;\nconst queryId = $json.queryId;\n\nconst timeout = (action === 'stop' || action === 'restart') ? '?t=10' : '';\nconst results = [];\n\n// Execute each container action sequentially using fetch\nfor (const containerId of containerIds) {\n try {\n // Use n8n's built-in $http or construct command for later execution\n // Since we can't use execSync easily, we'll build commands for chained execution\n results.push({\n containerId,\n cmd: `curl -s -o /dev/null -w \"%{http_code}\" --max-time 5 -X POST 'http://docker-socket-proxy:2375/v1.47/containers/${containerId}/${action}${timeout}'`\n });\n } catch (err) {\n results.push({ containerId, error: err.message });\n }\n}\n\nreturn {\n json: {\n commands: results,\n action,\n queryId,\n chatId,\n messageId,\n totalCount: containerIds.length\n }\n};" - }, - "id": "code-build-batch-commands", - "name": "Build Batch Commands", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1340, - 800 - ] - }, - { - "parameters": { - "jsCode": "// Execute all batch commands and collect results\nconst { commands, action, queryId, chatId, messageId, totalCount } = $json;\nconst results = [];\n\n// Build a single shell command that runs all curl commands sequentially\n// and outputs results in a parseable format\nconst allCommands = commands.map((c, i) => \n `echo \"RESULT_${i}:$(${c.cmd})\"`\n).join(' && ');\n\nreturn {\n json: {\n batchCmd: allCommands,\n containerIds: commands.map(c => c.containerId),\n action,\n queryId,\n chatId,\n messageId,\n totalCount\n }\n};" - }, - "id": "code-prepare-batch-exec", - "name": "Prepare Batch Execution", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1560, - 800 - ] - }, - { - "parameters": { - "command": "={{ $json.batchCmd }}", - "options": {} - }, - "id": "exec-batch-action", - "name": "Execute Batch Action", - "type": "n8n-nodes-base.executeCommand", - "typeVersion": 1, - "position": [ - 1780, - 800 - ] - }, - { - "parameters": { - "jsCode": "// Parse batch execution results\nconst stdout = $input.item.json.stdout || '';\nconst stderr = $input.item.json.stderr || '';\nconst prevData = $('Prepare Batch Execution').item.json;\nconst { containerIds, action, queryId, chatId, messageId, totalCount } = prevData;\n\n// Parse results from output like: RESULT_0:204 RESULT_1:204 RESULT_2:304\nconst results = [];\nfor (let i = 0; i < containerIds.length; i++) {\n const match = stdout.match(new RegExp(`RESULT_${i}:(\\\\d+)`));\n if (match) {\n const statusCode = parseInt(match[1]);\n results.push({\n containerId: containerIds[i],\n success: statusCode === 204 || statusCode === 304,\n statusCode\n });\n } else {\n results.push({\n containerId: containerIds[i],\n success: false,\n error: 'No response'\n });\n }\n}\n\nconst successCount = results.filter(r => r.success).length;\nconst failCount = results.length - successCount;\n\nreturn {\n json: {\n results,\n successCount,\n failCount,\n totalCount: results.length,\n action,\n queryId,\n chatId,\n messageId\n }\n};" - }, - "id": "code-parse-batch-result", - "name": "Parse Batch Result", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 2000, - 800 - ] - }, - { - "parameters": { - "jsCode": "// Format batch result message\nconst { successCount, failCount, totalCount, action } = $json;\nconst verb = action === 'start' ? 'started' :\n action === 'stop' ? 'stopped' : 'restarted';\n\nlet message;\nif (failCount === 0) {\n message = `Successfully ${verb} ${successCount} container${successCount > 1 ? 's' : ''}`;\n} else if (successCount === 0) {\n message = `Failed to ${action} all ${totalCount} containers`;\n} else {\n message = `${verb.charAt(0).toUpperCase() + verb.slice(1)} ${successCount}/${totalCount} containers (${failCount} failed)`;\n}\n\nreturn {\n json: {\n message,\n chatId: $json.chatId,\n queryId: $json.queryId,\n messageId: $json.messageId\n }\n};" - }, - "id": "code-format-batch-result", - "name": "Format Batch Result", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 2220, - 800 - ] - }, - { - "parameters": { - "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/answerCallbackQuery", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ callback_query_id: $json.queryId }) }}", - "options": {} - }, - "id": "http-answer-batch-query", - "name": "Answer Batch Query", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 2440, - 800 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, { "parameters": { "method": "POST", @@ -1632,31 +1542,6 @@ } } }, - { - "parameters": { - "resource": "message", - "operation": "sendMessage", - "chatId": "={{ $('Format Batch Result').item.json.chatId }}", - "text": "={{ $('Format Batch Result').item.json.message }}", - "additionalFields": { - "parse_mode": "HTML" - } - }, - "id": "telegram-send-batch-result", - "name": "Send Batch Result", - "type": "n8n-nodes-base.telegram", - "typeVersion": 1.2, - "position": [ - 2880, - 800 - ], - "credentials": { - "telegramApi": { - "id": "I0xTTiASl7C1NZhJ", - "name": "Telegram account" - } - } - }, { "parameters": { "jsCode": "// Parse update command from message\nconst text = $json.message.text.toLowerCase().trim();\nconst chatId = $json.message.chat.id;\nconst messageId = $json.message.message_id;\n\n// Match update pattern: update followed by container name\nconst match = text.match(/^update\\s+(.+)$/i);\n\nif (!match) {\n return {\n json: {\n error: true,\n errorMessage: 'Invalid update format. Use: update ',\n chatId: chatId\n }\n };\n}\n\nreturn {\n json: {\n containerQuery: match[1].trim(),\n chatId: chatId,\n messageId: messageId\n }\n};" @@ -5001,8 +4886,7 @@ "type": "main", "index": 0 } - ], - [] + ] ] }, "IF Callback Authenticated": { @@ -5013,8 +4897,7 @@ "type": "main", "index": 0 } - ], - [] + ] ] }, "Parse Callback Data": { @@ -5058,13 +4941,7 @@ "index": 0 } ], - [ - { - "node": "Build Batch Commands", - "type": "main", - "index": 0 - } - ], + [], [ { "node": "Answer Select Callback", @@ -5461,8 +5338,7 @@ "type": "main", "index": 0 } - ], - [] + ] ] }, "Find Closest Match": { @@ -5549,83 +5425,6 @@ ] ] }, - "Build Batch Commands": { - "main": [ - [ - { - "node": "Prepare Batch Execution", - "type": "main", - "index": 0 - } - ] - ] - }, - "Prepare Batch Execution": { - "main": [ - [ - { - "node": "Execute Batch Action", - "type": "main", - "index": 0 - } - ] - ] - }, - "Execute Batch Action": { - "main": [ - [ - { - "node": "Parse Batch Result", - "type": "main", - "index": 0 - } - ] - ] - }, - "Parse Batch Result": { - "main": [ - [ - { - "node": "Format Batch Result", - "type": "main", - "index": 0 - } - ] - ] - }, - "Format Batch Result": { - "main": [ - [ - { - "node": "Answer Batch Query", - "type": "main", - "index": 0 - } - ] - ] - }, - "Answer Batch Query": { - "main": [ - [ - { - "node": "Delete Batch Confirm Message", - "type": "main", - "index": 0 - } - ] - ] - }, - "Delete Batch Confirm Message": { - "main": [ - [ - { - "node": "Send Batch Result", - "type": "main", - "index": 0 - } - ] - ] - }, "Parse Update Command": { "main": [ [ @@ -5688,8 +5487,7 @@ "type": "main", "index": 0 } - ], - [] + ] ] }, "Handle Update Multiple": {