diff --git a/n8n-workflow.json b/n8n-workflow.json index 4bfcd59..f3effce 100644 --- a/n8n-workflow.json +++ b/n8n-workflow.json @@ -2659,6 +2659,422 @@ "name": "Telegram account" } } + }, + { + "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 }) }}", + "options": {} + }, + "id": "http-answer-action-callback", + "name": "Answer Action Callback", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 1340, + 1200 + ], + "credentials": { + "telegramApi": { + "id": "I0xTTiASl7C1NZhJ", + "name": "Telegram account" + } + } + }, + { + "parameters": { + "rules": { + "values": [ + { + "id": "action-start", + "conditions": { + "options": { + "caseSensitive": true, + "typeValidation": "loose" + }, + "conditions": [ + { + "id": "is-start", + "leftValue": "={{ $('Parse Callback Data').item.json.action }}", + "rightValue": "start", + "operator": { + "type": "string", + "operation": "equals" + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "start" + }, + { + "id": "action-restart", + "conditions": { + "options": { + "caseSensitive": true, + "typeValidation": "loose" + }, + "conditions": [ + { + "id": "is-restart", + "leftValue": "={{ $('Parse Callback Data').item.json.action }}", + "rightValue": "restart", + "operator": { + "type": "string", + "operation": "equals" + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "restart" + }, + { + "id": "action-stop", + "conditions": { + "options": { + "caseSensitive": true, + "typeValidation": "loose" + }, + "conditions": [ + { + "id": "is-stop", + "leftValue": "={{ $('Parse Callback Data').item.json.action }}", + "rightValue": "stop", + "operator": { + "type": "string", + "operation": "equals" + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "stop" + }, + { + "id": "action-update", + "conditions": { + "options": { + "caseSensitive": true, + "typeValidation": "loose" + }, + "conditions": [ + { + "id": "is-update", + "leftValue": "={{ $('Parse Callback Data').item.json.action }}", + "rightValue": "update", + "operator": { + "type": "string", + "operation": "equals" + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "update" + }, + { + "id": "action-logs", + "conditions": { + "options": { + "caseSensitive": true, + "typeValidation": "loose" + }, + "conditions": [ + { + "id": "is-logs", + "leftValue": "={{ $('Parse Callback Data').item.json.action }}", + "rightValue": "logs", + "operator": { + "type": "string", + "operation": "equals" + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "logs" + } + ] + }, + "options": { + "fallbackOutput": "none" + } + }, + "id": "switch-route-action-type", + "name": "Route Action Type", + "type": "n8n-nodes-base.switch", + "typeVersion": 3.2, + "position": [ + 1560, + 1200 + ] + }, + { + "parameters": { + "jsCode": "// Build start/restart action command for inline keyboard\nconst data = $('Parse Callback Data').item.json;\nconst containerName = data.containerName;\nconst action = data.action; // 'start' or 'restart'\nconst chatId = data.chatId;\nconst messageId = data.messageId;\n\n// For restart, use ?t=10 for graceful timeout\nconst timeout = action === 'restart' ? '?t=10' : '';\n\nreturn {\n json: {\n containerName,\n action,\n chatId,\n messageId\n }\n};" + }, + "id": "code-prepare-immediate-action", + "name": "Prepare Immediate Action", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1780, + 1100 + ] + }, + { + "parameters": { + "method": "GET", + "url": "=http://docker-socket-proxy:2375/containers/json?all=true", + "options": {} + }, + "id": "http-get-container-for-action", + "name": "Get Container For Action", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 2000, + 1100 + ] + }, + { + "parameters": { + "jsCode": "// Find container and execute action\nconst containers = $input.item.json;\nconst prevData = $('Prepare Immediate Action').item.json;\nconst containerName = prevData.containerName.toLowerCase();\nconst action = prevData.action;\nconst chatId = prevData.chatId;\nconst messageId = prevData.messageId;\n\n// Function to normalize container names\nfunction normalizeName(name) {\n return name\n .replace(/^\\//, '')\n .replace(/^(linuxserver[-_]|binhex[-_])/i, '')\n .toLowerCase();\n}\n\n// Find the matching container\nconst container = containers.find(c => normalizeName(c.Names[0]) === containerName);\n\nif (!container) {\n return {\n json: {\n error: true,\n chatId,\n messageId,\n text: `Container \"${containerName}\" not found`\n }\n };\n}\n\nconst containerId = container.Id;\nconst timeout = action === 'restart' ? '?t=10' : '';\n\nreturn {\n json: {\n cmd: `curl -s -o /dev/null -w \"%{http_code}\" --max-time 15 -X POST 'http://docker-socket-proxy:2375/v1.47/containers/${containerId}/${action}${timeout}'`,\n containerId,\n containerName: normalizeName(container.Names[0]),\n action,\n chatId,\n messageId\n }\n};" + }, + "id": "code-build-immediate-action-cmd", + "name": "Build Immediate Action Command", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 2220, + 1100 + ] + }, + { + "parameters": { + "command": "={{ $json.cmd }}", + "options": {} + }, + "id": "exec-immediate-action", + "name": "Execute Immediate Action", + "type": "n8n-nodes-base.executeCommand", + "typeVersion": 1, + "position": [ + 2440, + 1100 + ] + }, + { + "parameters": { + "jsCode": "// Parse immediate action result and update submenu\nconst stdout = $input.item.json.stdout;\nconst prevData = $('Build Immediate Action Command').item.json;\nconst containerName = prevData.containerName;\nconst action = prevData.action;\nconst chatId = prevData.chatId;\nconst messageId = prevData.messageId;\n\nconst statusCode = parseInt(stdout.trim());\n\n// 204: Success, 304: Already in state (also success)\nconst success = statusCode === 204 || statusCode === 304;\n\nconst verb = action === 'start' ? 'started' : 'restarted';\nconst icon = success ? '\\u2705' : '\\u274C';\nconst resultText = success ? `${containerName} ${verb} successfully` : `Failed to ${action} ${containerName}`;\n\n// Build updated keyboard based on new state (assume action succeeded)\nconst keyboard = [];\nif (action === 'start') {\n // Container is now running\n keyboard.push([\n { text: '\\u23F9\\uFE0F Stop', callback_data: `action:stop:${containerName}` },\n { text: '\\u{1F504} Restart', callback_data: `action:restart:${containerName}` }\n ]);\n} else {\n // Restart - container is still running\n keyboard.push([\n { text: '\\u23F9\\uFE0F Stop', callback_data: `action:stop:${containerName}` },\n { text: '\\u{1F504} Restart', callback_data: `action:restart:${containerName}` }\n ]);\n}\n\nkeyboard.push([\n { text: '\\u{1F4CB} Logs', callback_data: `action:logs:${containerName}` },\n { text: '\\u2B06\\uFE0F Update', callback_data: `action:update:${containerName}` }\n]);\n\nkeyboard.push([\n { text: '\\u25C0\\uFE0F Back to List', callback_data: 'list:0' }\n]);\n\nconst stateIcon = action === 'start' || success ? '\\u{1F7E2}' : '\\u26AA';\nlet text = `${stateIcon} ${containerName}\\n\\n`;\ntext += `${icon} ${resultText}`;\n\nreturn {\n json: {\n chatId,\n messageId,\n text,\n reply_markup: { inline_keyboard: keyboard }\n }\n};" + }, + "id": "code-format-immediate-result", + "name": "Format Immediate Result", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 2660, + 1100 + ] + }, + { + "parameters": { + "method": "POST", + "url": "=https://api.telegram.org/bot{{ $credentials.telegramApi.accessToken }}/editMessageText", + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={{ JSON.stringify({ chat_id: $json.chatId, message_id: $json.messageId, text: $json.text, parse_mode: 'HTML', reply_markup: $json.reply_markup }) }}", + "options": {} + }, + "id": "http-send-immediate-result", + "name": "Send Immediate Result", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 2880, + 1100 + ], + "credentials": { + "telegramApi": { + "id": "I0xTTiASl7C1NZhJ", + "name": "Telegram account" + } + } + }, + { + "parameters": { + "jsCode": "// Prepare logs action\nconst data = $('Parse Callback Data').item.json;\nreturn {\n json: {\n containerName: data.containerName,\n chatId: data.chatId,\n messageId: data.messageId,\n lines: 30\n }\n};" + }, + "id": "code-prepare-logs-action", + "name": "Prepare Logs Action", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1780, + 1300 + ] + }, + { + "parameters": { + "method": "GET", + "url": "=http://docker-socket-proxy:2375/containers/json?all=true", + "options": {} + }, + "id": "http-get-container-for-logs", + "name": "Get Container For Logs", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 2000, + 1300 + ] + }, + { + "parameters": { + "jsCode": "// Find container and build logs command\nconst containers = $input.item.json;\nconst prevData = $('Prepare Logs Action').item.json;\nconst containerName = prevData.containerName.toLowerCase();\nconst lines = prevData.lines;\nconst chatId = prevData.chatId;\nconst messageId = prevData.messageId;\n\n// Function to normalize container names\nfunction normalizeName(name) {\n return name\n .replace(/^\\//, '')\n .replace(/^(linuxserver[-_]|binhex[-_])/i, '')\n .toLowerCase();\n}\n\n// Find the matching container\nconst container = containers.find(c => normalizeName(c.Names[0]) === containerName);\n\nif (!container) {\n return {\n json: {\n error: true,\n chatId,\n messageId,\n text: `Container \"${containerName}\" not found`\n }\n };\n}\n\nconst containerId = container.Id;\n\nreturn {\n json: {\n cmd: `curl -s --max-time 10 'http://docker-socket-proxy:2375/v1.47/containers/${containerId}/logs?stdout=true&stderr=true&tail=${lines}'`,\n containerId,\n containerName: normalizeName(container.Names[0]),\n containerState: container.State,\n lines,\n chatId,\n messageId\n }\n};" + }, + "id": "code-build-logs-action-cmd", + "name": "Build Logs Action Command", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 2220, + 1300 + ] + }, + { + "parameters": { + "command": "={{ $json.cmd }}", + "options": {} + }, + "id": "exec-logs-action", + "name": "Execute Logs Action", + "type": "n8n-nodes-base.executeCommand", + "typeVersion": 1, + "position": [ + 2440, + 1300 + ] + }, + { + "parameters": { + "jsCode": "// Parse and format logs output\nconst stdout = $input.item.json.stdout || '';\nconst prevData = $('Build Logs Action Command').item.json;\nconst containerName = prevData.containerName;\nconst containerState = prevData.containerState;\nconst lines = prevData.lines;\nconst chatId = prevData.chatId;\nconst messageId = prevData.messageId;\n\n// Docker logs API returns binary stream with 8-byte header per line\n// Strip non-printable characters and clean up\nlet logs = stdout\n .split('\\n')\n .map(line => {\n // Remove Docker stream header (first 8 bytes of each frame)\n // The header contains stream type and length info\n if (line.length > 8) {\n const cleaned = line.substring(8).replace(/[\\x00-\\x08\\x0B\\x0C\\x0E-\\x1F]/g, '');\n return cleaned;\n }\n return line.replace(/[\\x00-\\x08\\x0B\\x0C\\x0E-\\x1F]/g, '');\n })\n .filter(line => line.trim().length > 0)\n .slice(-lines)\n .join('\\n');\n\nif (!logs || logs.trim().length === 0) {\n logs = '(no recent logs)';\n}\n\n// Truncate if too long for Telegram (max ~4096 chars)\nif (logs.length > 3800) {\n logs = '...' + logs.slice(-3800);\n}\n\n// Build keyboard for navigation\nconst keyboard = [];\nif (containerState === 'running') {\n keyboard.push([\n { text: '\\u23F9\\uFE0F Stop', callback_data: `action:stop:${containerName}` },\n { text: '\\u{1F504} Restart', callback_data: `action:restart:${containerName}` }\n ]);\n} else {\n keyboard.push([\n { text: '\\u25B6\\uFE0F Start', callback_data: `action:start:${containerName}` }\n ]);\n}\n\nkeyboard.push([\n { text: '\\u{1F4CB} Refresh Logs', callback_data: `action:logs:${containerName}` },\n { text: '\\u2B06\\uFE0F Update', callback_data: `action:update:${containerName}` }\n]);\n\nkeyboard.push([\n { text: '\\u25C0\\uFE0F Back to List', callback_data: 'list:0' }\n]);\n\nconst stateIcon = containerState === 'running' ? '\\u{1F7E2}' : '\\u26AA';\nlet text = `${stateIcon} ${containerName} - Logs\\n\\n
${logs}
`;\n\nreturn {\n json: {\n chatId,\n messageId,\n text,\n reply_markup: { inline_keyboard: keyboard }\n }\n};" + }, + "id": "code-format-logs-action-result", + "name": "Format Logs Action Result", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 2660, + 1300 + ] + }, + { + "parameters": { + "method": "POST", + "url": "=https://api.telegram.org/bot{{ $credentials.telegramApi.accessToken }}/editMessageText", + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={{ JSON.stringify({ chat_id: $json.chatId, message_id: $json.messageId, text: $json.text, parse_mode: 'HTML', reply_markup: $json.reply_markup }) }}", + "options": {} + }, + "id": "http-send-logs-result", + "name": "Send Logs Result", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 2880, + 1300 + ], + "credentials": { + "telegramApi": { + "id": "I0xTTiASl7C1NZhJ", + "name": "Telegram account" + } + } + }, + { + "parameters": { + "jsCode": "// Build Stop Confirmation dialog with timestamp for 30-second timeout\nconst data = $('Parse Callback Data').item.json;\nconst containerName = data.containerName;\nconst chatId = data.chatId;\nconst messageId = data.messageId;\nconst timestamp = Math.floor(Date.now() / 1000); // Unix timestamp\n\nconst keyboard = {\n inline_keyboard: [\n [\n { text: '\\u2705 Yes, Stop', callback_data: `confirm:stop:${containerName}:${timestamp}` },\n { text: '\\u274C Cancel', callback_data: `cancel:${containerName}` }\n ]\n ]\n};\n\nconst text = `\\u26A0\\uFE0F Stop ${containerName}?\\n\\nThis will stop the container immediately.\\n\\nConfirmation expires in 30 seconds.`;\n\nreturn {\n json: {\n chatId,\n messageId,\n text,\n reply_markup: keyboard\n }\n};" + }, + "id": "code-build-stop-confirmation", + "name": "Build Stop Confirmation", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1780, + 1400 + ] + }, + { + "parameters": { + "method": "POST", + "url": "=https://api.telegram.org/bot{{ $credentials.telegramApi.accessToken }}/editMessageText", + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={{ JSON.stringify({ chat_id: $json.chatId, message_id: $json.messageId, text: $json.text, parse_mode: 'HTML', reply_markup: $json.reply_markup }) }}", + "options": {} + }, + "id": "http-send-stop-confirmation", + "name": "Send Stop Confirmation", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 2000, + 1400 + ], + "credentials": { + "telegramApi": { + "id": "I0xTTiASl7C1NZhJ", + "name": "Telegram account" + } + } + }, + { + "parameters": { + "jsCode": "// Build Update Confirmation dialog with timestamp for 30-second timeout\nconst data = $('Parse Callback Data').item.json;\nconst containerName = data.containerName;\nconst chatId = data.chatId;\nconst messageId = data.messageId;\nconst timestamp = Math.floor(Date.now() / 1000); // Unix timestamp\n\nconst keyboard = {\n inline_keyboard: [\n [\n { text: '\\u2705 Yes, Update', callback_data: `confirm:update:${containerName}:${timestamp}` },\n { text: '\\u274C Cancel', callback_data: `cancel:${containerName}` }\n ]\n ]\n};\n\nconst text = `\\u26A0\\uFE0F Update ${containerName}?\\n\\nThis will pull the latest image and recreate the container.\\n\\nConfirmation expires in 30 seconds.`;\n\nreturn {\n json: {\n chatId,\n messageId,\n text,\n reply_markup: keyboard\n }\n};" + }, + "id": "code-build-update-confirmation", + "name": "Build Update Confirmation", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1780, + 1500 + ] + }, + { + "parameters": { + "method": "POST", + "url": "=https://api.telegram.org/bot{{ $credentials.telegramApi.accessToken }}/editMessageText", + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={{ JSON.stringify({ chat_id: $json.chatId, message_id: $json.messageId, text: $json.text, parse_mode: 'HTML', reply_markup: $json.reply_markup }) }}", + "options": {} + }, + "id": "http-send-update-confirmation", + "name": "Send Update Confirmation", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 2000, + 1500 + ], + "credentials": { + "telegramApi": { + "id": "I0xTTiASl7C1NZhJ", + "name": "Telegram account" + } + } } ], "connections": { @@ -2763,7 +3179,13 @@ "index": 0 } ], - [], + [ + { + "node": "Answer Action Callback", + "type": "main", + "index": 0 + } + ], [ { "node": "Answer Noop Callback", @@ -3823,6 +4245,188 @@ } ] ] + }, + "Answer Action Callback": { + "main": [ + [ + { + "node": "Route Action Type", + "type": "main", + "index": 0 + } + ] + ] + }, + "Route Action Type": { + "main": [ + [ + { + "node": "Prepare Immediate Action", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Prepare Immediate Action", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Build Stop Confirmation", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Build Update Confirmation", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Prepare Logs Action", + "type": "main", + "index": 0 + } + ] + ] + }, + "Prepare Immediate Action": { + "main": [ + [ + { + "node": "Get Container For Action", + "type": "main", + "index": 0 + } + ] + ] + }, + "Get Container For Action": { + "main": [ + [ + { + "node": "Build Immediate Action Command", + "type": "main", + "index": 0 + } + ] + ] + }, + "Build Immediate Action Command": { + "main": [ + [ + { + "node": "Execute Immediate Action", + "type": "main", + "index": 0 + } + ] + ] + }, + "Execute Immediate Action": { + "main": [ + [ + { + "node": "Format Immediate Result", + "type": "main", + "index": 0 + } + ] + ] + }, + "Format Immediate Result": { + "main": [ + [ + { + "node": "Send Immediate Result", + "type": "main", + "index": 0 + } + ] + ] + }, + "Prepare Logs Action": { + "main": [ + [ + { + "node": "Get Container For Logs", + "type": "main", + "index": 0 + } + ] + ] + }, + "Get Container For Logs": { + "main": [ + [ + { + "node": "Build Logs Action Command", + "type": "main", + "index": 0 + } + ] + ] + }, + "Build Logs Action Command": { + "main": [ + [ + { + "node": "Execute Logs Action", + "type": "main", + "index": 0 + } + ] + ] + }, + "Execute Logs Action": { + "main": [ + [ + { + "node": "Format Logs Action Result", + "type": "main", + "index": 0 + } + ] + ] + }, + "Format Logs Action Result": { + "main": [ + [ + { + "node": "Send Logs Result", + "type": "main", + "index": 0 + } + ] + ] + }, + "Build Stop Confirmation": { + "main": [ + [ + { + "node": "Send Stop Confirmation", + "type": "main", + "index": 0 + } + ] + ] + }, + "Build Update Confirmation": { + "main": [ + [ + { + "node": "Send Update Confirmation", + "type": "main", + "index": 0 + } + ] + ] } }, "pinData": {},