fd4c614afd
- Add Build Progress Message node for per-container status display - Add Edit Progress Message to update Telegram with current progress - Add Route Batch Loop Action switch for action-specific execution - Add Build Batch Action Command with container lookup support - Add Execute Batch Container Action with error handling - Add Check Batch Action Result for lookup vs direct action - Add Needs Action Call IF node for two-phase execution - Add Execute Batch Action 2 for follow-up action calls - Add Parse Batch Action 2 for result handling - Add Handle Action Result to aggregate success/failure counts - Add Prepare Next Iteration for loop continuation - Add Prepare Batch Stop Exec for confirmed stop callbacks - Add Prepare Batch Exec for bexec callbacks - Connect Check Batch Stop Expired to execution flow - Connect Answer Batch Exec to execution flow
7009 lines
256 KiB
JSON
7009 lines
256 KiB
JSON
{
|
|
"name": "Docker Manager Bot",
|
|
"nodes": [
|
|
{
|
|
"parameters": {
|
|
"updates": [
|
|
"message",
|
|
"callback_query"
|
|
]
|
|
},
|
|
"id": "telegram-trigger",
|
|
"name": "Telegram Trigger",
|
|
"type": "n8n-nodes-base.telegramTrigger",
|
|
"typeVersion": 1.1,
|
|
"position": [
|
|
240,
|
|
300
|
|
],
|
|
"credentials": {
|
|
"telegramApi": {
|
|
"id": "I0xTTiASl7C1NZhJ",
|
|
"name": "Telegram account"
|
|
}
|
|
},
|
|
"webhookId": "86b8d6bb-7c15-4aae-a749-fcaf2dfcb9e0"
|
|
},
|
|
{
|
|
"parameters": {
|
|
"rules": {
|
|
"values": [
|
|
{
|
|
"id": "route-message",
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"typeValidation": "loose"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "has-message",
|
|
"leftValue": "={{ $json.message?.text }}",
|
|
"rightValue": "",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "notEmpty"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"renameOutput": true,
|
|
"outputKey": "message"
|
|
},
|
|
{
|
|
"id": "route-callback",
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"typeValidation": "loose"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "has-callback",
|
|
"leftValue": "={{ $json.callback_query?.id }}",
|
|
"rightValue": "",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "notEmpty"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"renameOutput": true,
|
|
"outputKey": "callback_query"
|
|
}
|
|
]
|
|
},
|
|
"options": {
|
|
"fallbackOutput": "none"
|
|
}
|
|
},
|
|
"id": "switch-update-type",
|
|
"name": "Route Update Type",
|
|
"type": "n8n-nodes-base.switch",
|
|
"typeVersion": 3.2,
|
|
"position": [
|
|
460,
|
|
300
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "strict"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "user-auth-condition",
|
|
"leftValue": "={{ $json.message.from.id.toString() }}",
|
|
"rightValue": "563878771",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"options": {}
|
|
},
|
|
"id": "if-auth",
|
|
"name": "IF User Authenticated",
|
|
"type": "n8n-nodes-base.if",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
680,
|
|
200
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "strict"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "callback-auth-condition",
|
|
"leftValue": "={{ $json.callback_query.from.id.toString() }}",
|
|
"rightValue": "563878771",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"options": {}
|
|
},
|
|
"id": "if-callback-auth",
|
|
"name": "IF Callback Authenticated",
|
|
"type": "n8n-nodes-base.if",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
680,
|
|
500
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"rules": {
|
|
"values": [
|
|
{
|
|
"id": "keyword-menu-start",
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": false,
|
|
"typeValidation": "loose"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "starts-with-start-cmd",
|
|
"leftValue": "={{ $json.message.text }}",
|
|
"rightValue": "/start",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "startsWith"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"renameOutput": true,
|
|
"outputKey": "menu"
|
|
},
|
|
{
|
|
"id": "keyword-status",
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": false,
|
|
"typeValidation": "loose"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "contains-status",
|
|
"leftValue": "={{ $json.message.text }}",
|
|
"rightValue": "status",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "contains"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"renameOutput": true,
|
|
"outputKey": "status"
|
|
},
|
|
{
|
|
"id": "keyword-restart",
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": false,
|
|
"typeValidation": "loose"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "contains-restart",
|
|
"leftValue": "={{ $json.message.text }}",
|
|
"rightValue": "restart",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "contains"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"renameOutput": true,
|
|
"outputKey": "restart"
|
|
},
|
|
{
|
|
"id": "keyword-start",
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": false,
|
|
"typeValidation": "loose"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "contains-start",
|
|
"leftValue": "={{ $json.message.text }}",
|
|
"rightValue": "start",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "contains"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"renameOutput": true,
|
|
"outputKey": "start"
|
|
},
|
|
{
|
|
"id": "keyword-stop",
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": false,
|
|
"typeValidation": "loose"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "contains-stop",
|
|
"leftValue": "={{ $json.message.text }}",
|
|
"rightValue": "stop",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "contains"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"renameOutput": true,
|
|
"outputKey": "stop"
|
|
},
|
|
{
|
|
"id": "keyword-update",
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": false,
|
|
"typeValidation": "loose"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "contains-update",
|
|
"leftValue": "={{ $json.message.text }}",
|
|
"rightValue": "update",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "contains"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"renameOutput": true,
|
|
"outputKey": "update"
|
|
},
|
|
{
|
|
"id": "keyword-logs",
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": false,
|
|
"typeValidation": "loose"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "contains-logs",
|
|
"leftValue": "={{ $json.message.text }}",
|
|
"rightValue": "logs",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "contains"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"renameOutput": true,
|
|
"outputKey": "logs"
|
|
}
|
|
]
|
|
},
|
|
"options": {
|
|
"fallbackOutput": "extra"
|
|
}
|
|
},
|
|
"id": "switch-keyword-router",
|
|
"name": "Keyword Router",
|
|
"type": "n8n-nodes-base.switch",
|
|
"typeVersion": 3.2,
|
|
"position": [
|
|
900,
|
|
200
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"command": "curl -s --max-time 5 'http://docker-socket-proxy:2375/v1.47/containers/json?all=true'",
|
|
"options": {}
|
|
},
|
|
"id": "exec-docker-list",
|
|
"name": "Docker List Containers",
|
|
"type": "n8n-nodes-base.executeCommand",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
1120,
|
|
100
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Parse action from message text directly\nconst message = $json.message;\nconst text = message.text.toLowerCase().trim();\nconst chatId = message.chat.id;\nconst messageId = message.message_id;\n\n// Determine action from the text\nlet requestedAction = null;\nlet containerQuery = '';\n\n// Parse action and container name from message\nif (text.startsWith('start ')) {\n requestedAction = 'start';\n containerQuery = text.substring(6).trim();\n} else if (text.startsWith('stop ')) {\n requestedAction = 'stop';\n containerQuery = text.substring(5).trim();\n} else if (text.startsWith('restart ')) {\n requestedAction = 'restart';\n containerQuery = text.substring(8).trim();\n} else if (text.includes('start')) {\n requestedAction = 'start';\n containerQuery = text.replace('start', '').trim();\n} else if (text.includes('stop')) {\n requestedAction = 'stop';\n containerQuery = text.replace('stop', '').trim();\n} else if (text.includes('restart')) {\n requestedAction = 'restart';\n containerQuery = text.replace('restart', '').trim();\n}\n\nif (!requestedAction || !containerQuery) {\n return {\n json: {\n error: true,\n errorMessage: 'Please specify action and container name (e.g., \"start plex\")',\n chatId: chatId\n }\n };\n}\n\nreturn {\n json: {\n action: requestedAction,\n containerQuery: containerQuery,\n chatId: chatId,\n messageId: messageId\n }\n};"
|
|
},
|
|
"id": "code-parse-action-command",
|
|
"name": "Parse Action Command",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
900,
|
|
400
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"command": "curl -s --max-time 5 'http://docker-socket-proxy:2375/v1.47/containers/json?all=true'",
|
|
"options": {}
|
|
},
|
|
"id": "exec-docker-list-action",
|
|
"name": "Docker List for Action",
|
|
"type": "n8n-nodes-base.executeCommand",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
1120,
|
|
400
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Get Docker API response and action info from Parse Action Command\nconst dockerOutput = $input.item.json.stdout;\nconst actionData = $('Parse Action Command').item.json;\nconst action = actionData.action || 'restart';\nconst containerQuery = actionData.containerQuery || '';\nconst chatId = actionData.chatId;\n\n// Parse JSON response\nlet containers;\ntry {\n if (!dockerOutput || dockerOutput.trim() === '') {\n throw new Error('Empty response');\n }\n containers = JSON.parse(dockerOutput);\n} catch (e) {\n return [{\n json: {\n matchCount: -1,\n error: true,\n chatId: chatId,\n text: \"Cannot connect to Docker\"\n }\n }];\n}\n\n// Function to normalize container names\nfunction normalizeName(name) {\n return name\n .replace(/^\\//, '') // Remove leading slash\n .replace(/^(linuxserver[-_]|binhex[-_])/i, '') // Remove common prefixes\n .toLowerCase();\n}\n\nconst normalized = containerQuery.toLowerCase();\n\n// First check for exact match\nconst exactMatch = containers.find(c => normalizeName(c.Names[0]) === normalized);\nif (exactMatch) {\n return [{\n json: {\n matches: [{ Id: exactMatch.Id, Name: exactMatch.Names[0].replace(/^\\//, ''), State: exactMatch.State }],\n matchCount: 1,\n action: action,\n containerQuery: containerQuery,\n chatId: chatId,\n allContainers: containers\n }\n }];\n}\n\n// Fall back to substring matching (container name contains query)\nconst matches = containers.filter(c => {\n const containerName = normalizeName(c.Names[0]);\n return containerName.includes(normalized);\n});\n\n// Return match results with all necessary context\nreturn [{\n json: {\n matches: matches.map(c => ({\n Id: c.Id,\n Name: c.Names[0].replace(/^\\//, ''),\n State: c.State\n })),\n matchCount: matches.length,\n action: action,\n containerQuery: containerQuery,\n chatId: chatId,\n allContainers: containers\n }\n}];"
|
|
},
|
|
"id": "code-match-container",
|
|
"name": "Match Container",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
1340,
|
|
400
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"rules": {
|
|
"values": [
|
|
{
|
|
"id": "docker-error",
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"typeValidation": "loose"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "count-negative",
|
|
"leftValue": "={{ $json.matchCount }}",
|
|
"rightValue": 0,
|
|
"operator": {
|
|
"type": "number",
|
|
"operation": "lt"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"renameOutput": false
|
|
},
|
|
{
|
|
"id": "no-match",
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"typeValidation": "loose"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "count-zero",
|
|
"leftValue": "={{ $json.matchCount }}",
|
|
"rightValue": 0,
|
|
"operator": {
|
|
"type": "number",
|
|
"operation": "equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"renameOutput": false
|
|
},
|
|
{
|
|
"id": "single-match",
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"typeValidation": "loose"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "count-one",
|
|
"leftValue": "={{ $json.matchCount }}",
|
|
"rightValue": 1,
|
|
"operator": {
|
|
"type": "number",
|
|
"operation": "equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"renameOutput": false
|
|
},
|
|
{
|
|
"id": "multiple-matches",
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"typeValidation": "loose"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "count-gt-one",
|
|
"leftValue": "={{ $json.matchCount }}",
|
|
"rightValue": 1,
|
|
"operator": {
|
|
"type": "number",
|
|
"operation": "gt"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"renameOutput": false
|
|
}
|
|
]
|
|
},
|
|
"options": {
|
|
"fallbackOutput": "extra"
|
|
}
|
|
},
|
|
"id": "switch-match-count",
|
|
"name": "Check Match Count",
|
|
"type": "n8n-nodes-base.switch",
|
|
"typeVersion": 3.2,
|
|
"position": [
|
|
1560,
|
|
400
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Build the curl command for the action\nconst data = $input.item.json;\nconst containerId = data.matches[0].Id;\nconst action = data.action;\nconst containerName = data.matches[0].Name;\nconst chatId = data.chatId;\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}\" --max-time 5 -X POST 'http://docker-socket-proxy:2375/v1.47/containers/${containerId}/${action}${timeout}'`;\n\nreturn {\n json: {\n cmd: cmd,\n containerId: containerId,\n containerName: containerName,\n action: action,\n chatId: chatId\n }\n};"
|
|
},
|
|
"id": "code-build-action-cmd",
|
|
"name": "Build Action Command",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
1780,
|
|
400
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"command": "={{ $json.cmd }}",
|
|
"options": {}
|
|
},
|
|
"id": "exec-action",
|
|
"name": "Execute Action",
|
|
"type": "n8n-nodes-base.executeCommand",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
2000,
|
|
400
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Parse the HTTP status code from curl output\nconst stdout = $input.item.json.stdout;\nconst stderr = $input.item.json.stderr;\nconst actionData = $('Build Action Command').item.json;\nconst containerName = actionData.containerName;\nconst action = actionData.action;\nconst chatId = actionData.chatId;\n\n// Check for curl-level errors first (connection issues)\nif (stderr && stderr.trim()) {\n return {\n json: {\n success: false,\n chatId: chatId,\n text: `Failed to ${action} ${containerName}`\n }\n };\n}\n\nconst statusCode = parseInt(stdout.trim());\n\n// 204: Success, 304: Already in state (also success for user)\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: chatId,\n text: `<b>${containerName}</b> ${verb} successfully`\n }\n };\n}\n\n// All error codes get terse message\nreturn {\n json: {\n success: false,\n chatId: chatId,\n text: `Failed to ${action} ${containerName}`\n }\n};"
|
|
},
|
|
"id": "code-parse-action-result",
|
|
"name": "Parse Action Result",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
2220,
|
|
400
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"resource": "message",
|
|
"operation": "sendMessage",
|
|
"chatId": "={{ $json.chatId }}",
|
|
"text": "={{ $json.text }}",
|
|
"additionalFields": {
|
|
"parse_mode": "HTML"
|
|
}
|
|
},
|
|
"id": "telegram-send-action-result",
|
|
"name": "Send Action Result",
|
|
"type": "n8n-nodes-base.telegram",
|
|
"typeVersion": 1.2,
|
|
"position": [
|
|
2440,
|
|
400
|
|
],
|
|
"credentials": {
|
|
"telegramApi": {
|
|
"id": "I0xTTiASl7C1NZhJ",
|
|
"name": "Telegram account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Find closest match for suggestion\nconst query = $json.containerQuery.toLowerCase();\nconst containers = $json.allContainers;\nconst action = $json.action;\nconst chatId = $json.chatId;\n\n// Simple closest match: longest common substring or starts-with\nlet bestMatch = null;\nlet bestScore = 0;\n\nfor (const container of containers) {\n const name = container.Names[0].replace(/^\\//, '').toLowerCase();\n // Score by: contains query, or query contains name, or matching chars\n let score = 0;\n if (name.includes(query)) score = query.length * 2;\n else if (query.includes(name)) score = name.length * 1.5;\n else {\n // Simple: count matching starting characters\n for (let i = 0; i < Math.min(query.length, name.length); i++) {\n if (query[i] === name[i]) score++;\n else break;\n }\n }\n if (score > bestScore) {\n bestScore = score;\n bestMatch = container;\n }\n}\n\n// Require minimum score of 2 to suggest\nif (!bestMatch || bestScore < 2) {\n return { json: { hasSuggestion: false, query, action, chatId } };\n}\n\nconst suggestedName = bestMatch.Names[0].replace(/^\\//, '');\nconst suggestedId = bestMatch.Id.substring(0, 12); // Short ID for callback_data\n\nreturn {\n json: {\n hasSuggestion: true,\n query,\n action,\n chatId,\n suggestedName,\n suggestedId,\n timestamp: Date.now()\n }\n};"
|
|
},
|
|
"id": "code-find-closest",
|
|
"name": "Find Closest Match",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
1780,
|
|
300
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "strict"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "has-suggestion",
|
|
"leftValue": "={{ $json.hasSuggestion }}",
|
|
"rightValue": true,
|
|
"operator": {
|
|
"type": "boolean",
|
|
"operation": "equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"options": {}
|
|
},
|
|
"id": "if-has-suggestion",
|
|
"name": "Check Suggestion",
|
|
"type": "n8n-nodes-base.if",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
2000,
|
|
300
|
|
]
|
|
},
|
|
{
|
|
"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 '<b>${query}</b>' found.\\n\\nDid you mean <b>${suggestedName}</b>?`,\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};"
|
|
},
|
|
"id": "code-build-suggestion",
|
|
"name": "Build Suggestion Keyboard",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
2220,
|
|
200
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"method": "POST",
|
|
"url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/sendMessage",
|
|
"sendBody": true,
|
|
"specifyBody": "json",
|
|
"jsonBody": "={{ JSON.stringify($json) }}",
|
|
"options": {}
|
|
},
|
|
"id": "http-send-suggestion",
|
|
"name": "Send Suggestion",
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.2,
|
|
"position": [
|
|
2440,
|
|
200
|
|
],
|
|
"credentials": {
|
|
"telegramApi": {
|
|
"id": "I0xTTiASl7C1NZhJ",
|
|
"name": "Telegram account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"resource": "message",
|
|
"operation": "sendMessage",
|
|
"chatId": "={{ $json.chatId }}",
|
|
"text": "=No container found matching '<b>{{ $json.query }}</b>'",
|
|
"additionalFields": {
|
|
"parse_mode": "HTML"
|
|
}
|
|
},
|
|
"id": "telegram-send-no-match",
|
|
"name": "Send No Match",
|
|
"type": "n8n-nodes-base.telegram",
|
|
"typeVersion": 1.2,
|
|
"position": [
|
|
2220,
|
|
400
|
|
],
|
|
"credentials": {
|
|
"telegramApi": {
|
|
"id": "I0xTTiASl7C1NZhJ",
|
|
"name": "Telegram account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"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 <b>${matches.length}</b> containers matching '<b>${query}</b>':\\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};"
|
|
},
|
|
"id": "code-build-batch-keyboard",
|
|
"name": "Build Batch Keyboard",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
1780,
|
|
500
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"resource": "message",
|
|
"operation": "sendMessage",
|
|
"chatId": "={{ $json.chat_id }}",
|
|
"text": "={{ $json.text }}",
|
|
"additionalFields": {
|
|
"parse_mode": "HTML",
|
|
"reply_markup": "={{ JSON.stringify($json.reply_markup) }}"
|
|
}
|
|
},
|
|
"id": "telegram-send-batch-confirm",
|
|
"name": "Send Batch Confirmation",
|
|
"type": "n8n-nodes-base.telegram",
|
|
"typeVersion": 1.2,
|
|
"position": [
|
|
2000,
|
|
500
|
|
],
|
|
"credentials": {
|
|
"telegramApi": {
|
|
"id": "I0xTTiASl7C1NZhJ",
|
|
"name": "Telegram account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"resource": "message",
|
|
"operation": "sendMessage",
|
|
"chatId": "={{ $json.chatId }}",
|
|
"text": "={{ $json.text }}",
|
|
"additionalFields": {
|
|
"parse_mode": "HTML"
|
|
}
|
|
},
|
|
"id": "telegram-send-docker-error",
|
|
"name": "Send Docker Error",
|
|
"type": "n8n-nodes-base.telegram",
|
|
"typeVersion": 1.2,
|
|
"position": [
|
|
1780,
|
|
200
|
|
],
|
|
"credentials": {
|
|
"telegramApi": {
|
|
"id": "I0xTTiASl7C1NZhJ",
|
|
"name": "Telegram account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Parse callback data from button click\n// Supports: select:{name}, list:{page}, action:{action}:{name}, confirm:{action}:{name}:{timestamp}, cancel:{name}, bstop:*, bexec:*, and legacy JSON format\nconst callback = $json.callback_query;\nconst rawData = callback.data;\nconst queryId = callback.id;\nconst chatId = callback.message.chat.id;\nconst messageId = callback.message.message_id;\n\n// Check for new colon-separated format\nif (rawData.startsWith('select:')) {\n // Container selection: select:{containerName}\n const containerName = rawData.substring(7); // Remove 'select:'\n return {\n json: {\n queryId,\n chatId,\n messageId,\n isSelect: true,\n containerName,\n isList: false,\n isCancel: false,\n isBatch: false,\n expired: false\n }\n };\n}\n\nif (rawData.startsWith('list:')) {\n // Pagination: list:{page}\n const page = parseInt(rawData.substring(5)) || 0; // Remove 'list:'\n return {\n json: {\n queryId,\n chatId,\n messageId,\n isList: true,\n page,\n isSelect: false,\n isCancel: false,\n isBatch: false,\n expired: false\n }\n };\n}\n\nif (rawData.startsWith('action:')) {\n // Action execution: action:{action}:{containerName}\n const parts = rawData.substring(7).split(':'); // Remove 'action:'\n const action = parts[0];\n const containerName = parts.slice(1).join(':'); // Handle names with colons\n return {\n json: {\n queryId,\n chatId,\n messageId,\n isAction: true,\n action,\n containerName,\n isSelect: false,\n isList: false,\n isCancel: false,\n isBatch: false,\n expired: false\n }\n };\n}\n\nif (rawData.startsWith('confirm:')) {\n // Confirmation: confirm:{action}:{containerName}:{timestamp}\n const parts = rawData.substring(8).split(':'); // Remove 'confirm:'\n const action = parts[0]; // stop or update\n const timestamp = parseInt(parts[parts.length - 1]); // Last part is timestamp\n const containerName = parts.slice(1, -1).join(':'); // Everything between action and timestamp\n \n // Check 30-second timeout\n const currentTime = Math.floor(Date.now() / 1000);\n const elapsed = currentTime - timestamp;\n const isExpired = elapsed > 30;\n \n return {\n json: {\n queryId,\n chatId,\n messageId,\n isConfirm: true,\n confirmAction: action,\n containerName,\n timestamp,\n expired: isExpired,\n isSelect: false,\n isList: false,\n isAction: false,\n isCancel: false,\n isBatch: false\n }\n };\n}\n\nif (rawData.startsWith('cancel:')) {\n // Cancel confirmation: cancel:{containerName}\n const containerName = rawData.substring(7); // Remove 'cancel:'\n return {\n json: {\n queryId,\n chatId,\n messageId,\n isCancelConfirm: true,\n containerName,\n isSelect: false,\n isList: false,\n isAction: false,\n isCancel: false,\n isBatch: false,\n expired: false\n }\n };\n}\n\n// Batch stop confirmation: bstop:confirm:{names}:{timestamp} or bstop:cancel\nif (rawData.startsWith('bstop:')) {\n const rest = rawData.substring(6); // Remove 'bstop:'\n if (rest === 'cancel') {\n return {\n json: {\n queryId,\n chatId,\n messageId,\n isBatchStopCancel: true,\n isCancel: true,\n isSelect: false,\n isList: false,\n isAction: false,\n isBatch: false,\n expired: false\n }\n };\n }\n // bstop:confirm:{names}:{timestamp}\n const parts = rest.split(':'); // confirm, names, timestamp\n if (parts[0] === 'confirm' && parts.length >= 3) {\n const timestamp = parseInt(parts[parts.length - 1]);\n const namesStr = parts.slice(1, -1).join(':'); // Handle container names with colons\n const containerNames = namesStr.split(',');\n \n // Check 30-second timeout\n const currentTime = Math.floor(Date.now() / 1000);\n const elapsed = currentTime - timestamp;\n const isExpired = elapsed > 30;\n \n return {\n json: {\n queryId,\n chatId,\n messageId,\n isBatchStopConfirm: true,\n containerNames,\n timestamp,\n expired: isExpired,\n isSelect: false,\n isList: false,\n isAction: false,\n isCancel: false,\n isBatch: true\n }\n };\n }\n}\n\n// Batch execution: bexec:{action}:{names}:{timestamp}\nif (rawData.startsWith('bexec:')) {\n const parts = rawData.substring(6).split(':'); // action, names, timestamp\n const action = parts[0];\n const timestamp = parseInt(parts[parts.length - 1]);\n const namesStr = parts.slice(1, -1).join(':');\n const containerNames = namesStr.split(',');\n \n // Check 30-second timeout\n const currentTime = Math.floor(Date.now() / 1000);\n const elapsed = currentTime - timestamp;\n const isExpired = elapsed > 30;\n \n return {\n json: {\n queryId,\n chatId,\n messageId,\n isBatchExec: true,\n batchAction: action,\n containerNames,\n timestamp,\n expired: isExpired,\n isSelect: false,\n isList: false,\n isAction: false,\n isCancel: false,\n isBatch: true\n }\n };\n}\n\nif (rawData === 'noop') {\n // No-op button (like page indicator)\n return {\n json: {\n queryId,\n chatId,\n messageId,\n isNoop: true,\n isSelect: false,\n isList: false,\n isCancel: false,\n isBatch: false,\n expired: false\n }\n };\n}\n\n// Legacy JSON format for backward compatibility\nlet data;\ntry {\n data = JSON.parse(rawData);\n} catch (e) {\n data = { a: 'x' }; // Treat parse error as cancel\n}\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\n// Detect batch (c is array) vs single suggestion (c is string)\nconst isBatch = Array.isArray(data.c);\nconst containerIds = isBatch ? data.c : (data.c ? [data.c] : []);\n\nreturn {\n json: {\n queryId,\n chatId,\n messageId,\n action,\n containerIds,\n containerId: containerIds[0] || null,\n expired: isExpired,\n isBatch,\n isCancel: action === 'cancel',\n isSelect: false,\n isList: false,\n isAction: false\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"
|
|
},
|
|
{
|
|
"id": "is-batch",
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "strict"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "batch-true",
|
|
"leftValue": "={{ $json.isBatch }}",
|
|
"rightValue": true,
|
|
"operator": {
|
|
"type": "boolean",
|
|
"operation": "equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"renameOutput": true,
|
|
"outputKey": "batch"
|
|
},
|
|
{
|
|
"id": "is-select",
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "strict"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "select-true",
|
|
"leftValue": "={{ $json.isSelect }}",
|
|
"rightValue": true,
|
|
"operator": {
|
|
"type": "boolean",
|
|
"operation": "equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"renameOutput": true,
|
|
"outputKey": "select"
|
|
},
|
|
{
|
|
"id": "is-list",
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "strict"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "list-true",
|
|
"leftValue": "={{ $json.isList }}",
|
|
"rightValue": true,
|
|
"operator": {
|
|
"type": "boolean",
|
|
"operation": "equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"renameOutput": true,
|
|
"outputKey": "list"
|
|
},
|
|
{
|
|
"id": "is-action",
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "strict"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "action-true",
|
|
"leftValue": "={{ $json.isAction }}",
|
|
"rightValue": true,
|
|
"operator": {
|
|
"type": "boolean",
|
|
"operation": "equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"renameOutput": true,
|
|
"outputKey": "action"
|
|
},
|
|
{
|
|
"id": "is-noop",
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "strict"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "noop-true",
|
|
"leftValue": "={{ $json.isNoop }}",
|
|
"rightValue": true,
|
|
"operator": {
|
|
"type": "boolean",
|
|
"operation": "equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"renameOutput": true,
|
|
"outputKey": "noop"
|
|
},
|
|
{
|
|
"id": "is-confirm",
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "strict"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "confirm-true",
|
|
"leftValue": "={{ $json.isConfirm }}",
|
|
"rightValue": true,
|
|
"operator": {
|
|
"type": "boolean",
|
|
"operation": "equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"renameOutput": true,
|
|
"outputKey": "confirm"
|
|
},
|
|
{
|
|
"id": "is-cancel-confirm",
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "strict"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "cancel-confirm-true",
|
|
"leftValue": "={{ $json.isCancelConfirm }}",
|
|
"rightValue": true,
|
|
"operator": {
|
|
"type": "boolean",
|
|
"operation": "equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"renameOutput": true,
|
|
"outputKey": "cancelConfirm"
|
|
},
|
|
{
|
|
"id": "is-batch-stop-confirm",
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "strict"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "batch-stop-confirm-true",
|
|
"leftValue": "={{ $json.isBatchStopConfirm }}",
|
|
"rightValue": true,
|
|
"operator": {
|
|
"type": "boolean",
|
|
"operation": "equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"renameOutput": true,
|
|
"outputKey": "batchStopConfirm"
|
|
},
|
|
{
|
|
"id": "is-batch-stop-cancel",
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "strict"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "batch-stop-cancel-true",
|
|
"leftValue": "={{ $json.isBatchStopCancel }}",
|
|
"rightValue": true,
|
|
"operator": {
|
|
"type": "boolean",
|
|
"operation": "equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"renameOutput": true,
|
|
"outputKey": "batchStopCancel"
|
|
},
|
|
{
|
|
"id": "is-batch-exec",
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "strict"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "batch-exec-true",
|
|
"leftValue": "={{ $json.isBatchExec }}",
|
|
"rightValue": true,
|
|
"operator": {
|
|
"type": "boolean",
|
|
"operation": "equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"renameOutput": true,
|
|
"outputKey": "batchExec"
|
|
}
|
|
]
|
|
},
|
|
"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{{ $env.TELEGRAM_BOT_TOKEN }}/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": "I0xTTiASl7C1NZhJ",
|
|
"name": "Telegram account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"method": "POST",
|
|
"url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/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": "I0xTTiASl7C1NZhJ",
|
|
"name": "Telegram account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"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{{ $env.TELEGRAM_BOT_TOKEN }}/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": "I0xTTiASl7C1NZhJ",
|
|
"name": "Telegram account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"method": "POST",
|
|
"url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/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": "I0xTTiASl7C1NZhJ",
|
|
"name": "Telegram account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"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}\" --max-time 5 -X POST 'http://docker-socket-proxy:2375/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} container`,\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// All error codes get terse message\nreturn {\n json: {\n success: false,\n chatId,\n messageId,\n queryId,\n text: `Failed to ${action} container`,\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{{ $env.TELEGRAM_BOT_TOKEN }}/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": "I0xTTiASl7C1NZhJ",
|
|
"name": "Telegram account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"method": "POST",
|
|
"url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/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": "I0xTTiASl7C1NZhJ",
|
|
"name": "Telegram account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"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": "I0xTTiASl7C1NZhJ",
|
|
"name": "Telegram account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"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",
|
|
"url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/deleteMessage",
|
|
"sendBody": true,
|
|
"specifyBody": "json",
|
|
"jsonBody": "={{ JSON.stringify({ chat_id: $('Format Batch Result').item.json.chatId, message_id: $('Format Batch Result').item.json.messageId }) }}",
|
|
"options": {}
|
|
},
|
|
"id": "http-delete-batch-confirm-msg",
|
|
"name": "Delete Batch Confirm Message",
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.2,
|
|
"position": [
|
|
2660,
|
|
800
|
|
],
|
|
"credentials": {
|
|
"telegramApi": {
|
|
"id": "I0xTTiASl7C1NZhJ",
|
|
"name": "Telegram account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"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 <container-name>',\n chatId: chatId\n }\n };\n}\n\nreturn {\n json: {\n containerQuery: match[1].trim(),\n chatId: chatId,\n messageId: messageId\n }\n};"
|
|
},
|
|
"id": "code-parse-update",
|
|
"name": "Parse Update Command",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
900,
|
|
1000
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"command": "curl -s --max-time 5 'http://docker-socket-proxy:2375/v1.47/containers/json?all=true'",
|
|
"options": {}
|
|
},
|
|
"id": "exec-docker-list-update",
|
|
"name": "Docker List for Update",
|
|
"type": "n8n-nodes-base.executeCommand",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
1120,
|
|
1000
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Get Docker API response and update info\nconst dockerOutput = $input.item.json.stdout;\nconst updateData = $('Parse Update Command').item.json;\nconst containerQuery = updateData.containerQuery;\nconst chatId = updateData.chatId;\n\n// Parse JSON response\nlet containers;\ntry {\n if (!dockerOutput || dockerOutput.trim() === '') {\n throw new Error('Empty response');\n }\n containers = JSON.parse(dockerOutput);\n} catch (e) {\n return [{\n json: {\n matchCount: -1,\n error: true,\n chatId: chatId,\n text: \"Cannot connect to Docker\"\n }\n }];\n}\n\n// Function to normalize container names\nfunction normalizeName(name) {\n return name\n .replace(/^\\//, '')\n .replace(/^(linuxserver[-_]|binhex[-_])/i, '')\n .toLowerCase();\n}\n\nconst normalized = containerQuery.toLowerCase();\n\n// First check for exact match\nconst exactMatch = containers.find(c => normalizeName(c.Names[0]) === normalized);\nif (exactMatch) {\n return [{\n json: {\n matches: [{ Id: exactMatch.Id, Name: exactMatch.Names[0].replace(/^\\//, ''), State: exactMatch.State }],\n matchCount: 1,\n containerQuery: containerQuery,\n chatId: chatId,\n allContainers: containers\n }\n }];\n}\n\n// Fall back to substring matching (container name contains query)\nconst matches = containers.filter(c => {\n const containerName = normalizeName(c.Names[0]);\n return containerName.includes(normalized);\n});\n\n// Return match results\nreturn [{\n json: {\n matches: matches.map(c => ({\n Id: c.Id,\n Name: c.Names[0].replace(/^\\//, ''),\n State: c.State\n })),\n matchCount: matches.length,\n containerQuery: containerQuery,\n chatId: chatId,\n allContainers: containers\n }\n}];"
|
|
},
|
|
"id": "code-match-update-container",
|
|
"name": "Match Update Container",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
1340,
|
|
1000
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"rules": {
|
|
"values": [
|
|
{
|
|
"id": "update-docker-error",
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"typeValidation": "loose"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "count-negative",
|
|
"leftValue": "={{ $json.matchCount }}",
|
|
"rightValue": 0,
|
|
"operator": {
|
|
"type": "number",
|
|
"operation": "lt"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"renameOutput": true,
|
|
"outputKey": "error"
|
|
},
|
|
{
|
|
"id": "update-no-match",
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"typeValidation": "loose"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "count-zero",
|
|
"leftValue": "={{ $json.matchCount }}",
|
|
"rightValue": 0,
|
|
"operator": {
|
|
"type": "number",
|
|
"operation": "equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"renameOutput": true,
|
|
"outputKey": "no-match"
|
|
},
|
|
{
|
|
"id": "update-single-match",
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"typeValidation": "loose"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "count-one",
|
|
"leftValue": "={{ $json.matchCount }}",
|
|
"rightValue": 1,
|
|
"operator": {
|
|
"type": "number",
|
|
"operation": "equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"renameOutput": true,
|
|
"outputKey": "single"
|
|
},
|
|
{
|
|
"id": "update-multiple-matches",
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"typeValidation": "loose"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "count-gt-one",
|
|
"leftValue": "={{ $json.matchCount }}",
|
|
"rightValue": 1,
|
|
"operator": {
|
|
"type": "number",
|
|
"operation": "gt"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"renameOutput": true,
|
|
"outputKey": "multiple"
|
|
}
|
|
]
|
|
},
|
|
"options": {
|
|
"fallbackOutput": "extra"
|
|
}
|
|
},
|
|
"id": "switch-update-match-count",
|
|
"name": "Check Update Match Count",
|
|
"type": "n8n-nodes-base.switch",
|
|
"typeVersion": 3.2,
|
|
"position": [
|
|
1560,
|
|
1000
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"resource": "message",
|
|
"operation": "sendMessage",
|
|
"chatId": "={{ $json.chatId }}",
|
|
"text": "={{ $json.text }}",
|
|
"additionalFields": {
|
|
"parse_mode": "HTML"
|
|
}
|
|
},
|
|
"id": "telegram-send-update-error",
|
|
"name": "Send Update Error",
|
|
"type": "n8n-nodes-base.telegram",
|
|
"typeVersion": 1.2,
|
|
"position": [
|
|
1780,
|
|
900
|
|
],
|
|
"credentials": {
|
|
"telegramApi": {
|
|
"id": "I0xTTiASl7C1NZhJ",
|
|
"name": "Telegram account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Update requires exact container name - multiple matches not allowed\nconst matches = $json.matches;\nconst query = $json.containerQuery;\nconst chatId = $json.chatId;\n\nconst names = matches.map(m => m.Name).join(', ');\n\nreturn {\n json: {\n chatId: chatId,\n text: `Update requires an exact container name.\\n\\nFound ${matches.length} matches: ${names}`\n }\n};"
|
|
},
|
|
"id": "code-update-multiple-handler",
|
|
"name": "Handle Update Multiple",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
1780,
|
|
1100
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"resource": "message",
|
|
"operation": "sendMessage",
|
|
"chatId": "={{ $json.chatId }}",
|
|
"text": "={{ $json.text }}",
|
|
"additionalFields": {
|
|
"parse_mode": "HTML"
|
|
}
|
|
},
|
|
"id": "telegram-send-update-multiple",
|
|
"name": "Send Update Multiple",
|
|
"type": "n8n-nodes-base.telegram",
|
|
"typeVersion": 1.2,
|
|
"position": [
|
|
2000,
|
|
1100
|
|
],
|
|
"credentials": {
|
|
"telegramApi": {
|
|
"id": "I0xTTiASl7C1NZhJ",
|
|
"name": "Telegram account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"resource": "message",
|
|
"operation": "sendMessage",
|
|
"chatId": "={{ $json.chatId }}",
|
|
"text": "=No container found matching '<b>{{ $json.containerQuery }}</b>'",
|
|
"additionalFields": {
|
|
"parse_mode": "HTML"
|
|
}
|
|
},
|
|
"id": "telegram-send-update-no-match",
|
|
"name": "Send Update No Match",
|
|
"type": "n8n-nodes-base.telegram",
|
|
"typeVersion": 1.2,
|
|
"position": [
|
|
1780,
|
|
1000
|
|
],
|
|
"credentials": {
|
|
"telegramApi": {
|
|
"id": "I0xTTiASl7C1NZhJ",
|
|
"name": "Telegram account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"resource": "message",
|
|
"operation": "sendMessage",
|
|
"chatId": "={{ $json.chatId }}",
|
|
"text": "=Updating <b>{{ $json.matches[0].Name }}</b>...",
|
|
"additionalFields": {
|
|
"parse_mode": "HTML"
|
|
}
|
|
},
|
|
"id": "telegram-send-update-started",
|
|
"name": "Send Update Started",
|
|
"type": "n8n-nodes-base.telegram",
|
|
"typeVersion": 1.2,
|
|
"position": [
|
|
1560,
|
|
1200
|
|
],
|
|
"credentials": {
|
|
"telegramApi": {
|
|
"id": "I0xTTiASl7C1NZhJ",
|
|
"name": "Telegram account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Build inspect command for the matched container\n// Pass through original data from Match Update Container\nconst matchData = $('Check Update Match Count').item.json;\nconst containerId = matchData.matches[0].Id;\nconst containerName = matchData.matches[0].Name;\nconst chatId = matchData.chatId;\n\nreturn {\n json: {\n cmd: `curl -s --max-time 5 'http://docker-socket-proxy:2375/v1.47/containers/${containerId}/json'`,\n containerId,\n containerName,\n chatId\n }\n};"
|
|
},
|
|
"id": "code-build-inspect-cmd",
|
|
"name": "Build Inspect Command",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
1780,
|
|
1200
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"command": "={{ $json.cmd }}",
|
|
"options": {}
|
|
},
|
|
"id": "exec-inspect-container",
|
|
"name": "Inspect Container",
|
|
"type": "n8n-nodes-base.executeCommand",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
2000,
|
|
1200
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Parse container inspect output and extract config\nconst stdout = $input.item.json.stdout;\nconst cmdData = $('Build Inspect Command').item.json;\nconst containerId = cmdData.containerId;\nconst containerName = cmdData.containerName;\nconst chatId = cmdData.chatId;\n\nlet inspect;\ntry {\n inspect = JSON.parse(stdout);\n} catch (e) {\n return {\n json: {\n error: true,\n chatId,\n text: `Failed to inspect ${containerName}: ${e.message}`\n }\n };\n}\n\nlet imageName = inspect.Config.Image;\nconst currentImageId = inspect.Image;\n\n// CRITICAL: Ensure image has a tag, otherwise Docker pulls ALL tags!\n// If no tag specified, default to :latest\nif (imageName && !imageName.includes(':') && !imageName.includes('@')) {\n imageName = imageName + ':latest';\n}\n\n// Extract version from labels if available\nconst labels = inspect.Config.Labels || {};\nconst currentVersion = labels['org.opencontainers.image.version']\n || labels['version']\n || currentImageId.substring(7, 19);\n\nreturn {\n json: {\n imageName,\n currentImageId,\n currentVersion,\n containerConfig: inspect.Config,\n hostConfig: inspect.HostConfig,\n networkSettings: inspect.NetworkSettings,\n containerName,\n containerId,\n chatId\n }\n};"
|
|
},
|
|
"id": "code-parse-container-config",
|
|
"name": "Parse Container Config",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
2220,
|
|
1200
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Build pull image command\nconst imageName = $json.imageName;\n\n// Pipe through tail to only keep last 10KB - avoids memory issues with large pulls\n// Error/success messages appear at the end of the stream\nreturn {\n json: {\n cmd: `curl -s --max-time 600 -X POST 'http://docker-socket-proxy:2375/v1.47/images/create?fromImage=${encodeURIComponent(imageName)}' | tail -c 10000`,\n imageName,\n currentImageId: $json.currentImageId,\n currentVersion: $json.currentVersion,\n containerConfig: $json.containerConfig,\n hostConfig: $json.hostConfig,\n networkSettings: $json.networkSettings,\n containerName: $json.containerName,\n containerId: $json.containerId,\n chatId: $json.chatId\n }\n};"
|
|
},
|
|
"id": "code-build-pull-cmd",
|
|
"name": "Build Pull Command",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
2440,
|
|
1200
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"command": "={{ $json.cmd }}",
|
|
"options": {
|
|
"timeout": 660
|
|
}
|
|
},
|
|
"id": "exec-pull-image",
|
|
"name": "Pull Image",
|
|
"type": "n8n-nodes-base.executeCommand",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
2660,
|
|
1200
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Check pull response for errors\nconst stdout = $input.item.json.stdout || '';\nconst pullData = $('Build Pull Command').item.json;\nconst chatId = pullData.chatId;\nconst containerName = pullData.containerName;\n\n// Docker pull streams JSON objects, check for error messages\n// Rate limit error: {\"message\":\"toomanyrequests: ...\"}\n// Other errors: {\"message\":\"...\"}\nif (stdout.includes('\"message\"') && (stdout.includes('toomanyrequests') || stdout.includes('error') || stdout.includes('denied'))) {\n // Extract error message\n let errorMsg = 'Pull failed';\n try {\n const match = stdout.match(/\"message\"\\s*:\\s*\"([^\"]+)\"/); if (match) errorMsg = match[1];\n } catch (e) {}\n \n return {\n json: {\n pullError: true,\n chatId,\n text: `Failed to update ${containerName}: ${errorMsg.substring(0, 100)}`\n }\n };\n}\n\n// Success - pass through data for next node\nreturn {\n json: {\n pullError: false,\n imageName: pullData.imageName,\n currentImageId: pullData.currentImageId,\n currentVersion: pullData.currentVersion,\n containerConfig: pullData.containerConfig,\n hostConfig: pullData.hostConfig,\n networkSettings: pullData.networkSettings,\n containerName: pullData.containerName,\n containerId: pullData.containerId,\n chatId: pullData.chatId\n }\n};"
|
|
},
|
|
"id": "code-check-pull-response",
|
|
"name": "Check Pull Response",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
2770,
|
|
1200
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "strict"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "pull-error-check",
|
|
"leftValue": "={{ $json.pullError }}",
|
|
"rightValue": false,
|
|
"operator": {
|
|
"type": "boolean",
|
|
"operation": "equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"options": {}
|
|
},
|
|
"id": "if-pull-success",
|
|
"name": "Check Pull Success",
|
|
"type": "n8n-nodes-base.if",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
2880,
|
|
1200
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"resource": "message",
|
|
"operation": "sendMessage",
|
|
"chatId": "={{ $json.chatId }}",
|
|
"text": "={{ $json.text }}",
|
|
"additionalFields": {
|
|
"parse_mode": "HTML"
|
|
}
|
|
},
|
|
"id": "telegram-send-pull-error",
|
|
"name": "Send Pull Error",
|
|
"type": "n8n-nodes-base.telegram",
|
|
"typeVersion": 1.2,
|
|
"position": [
|
|
3100,
|
|
1350
|
|
],
|
|
"credentials": {
|
|
"telegramApi": {
|
|
"id": "I0xTTiASl7C1NZhJ",
|
|
"name": "Telegram account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Build inspect image command to get the new image ID\nconst imageName = $json.imageName;\n\nreturn {\n json: {\n cmd: `curl -s --max-time 5 'http://docker-socket-proxy:2375/v1.47/images/${encodeURIComponent(imageName)}/json'`,\n imageName,\n currentImageId: $json.currentImageId,\n currentVersion: $json.currentVersion,\n containerConfig: $json.containerConfig,\n hostConfig: $json.hostConfig,\n networkSettings: $json.networkSettings,\n containerName: $json.containerName,\n containerId: $json.containerId,\n chatId: $json.chatId\n }\n};"
|
|
},
|
|
"id": "code-build-inspect-image-cmd",
|
|
"name": "Build Image Inspect",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
2880,
|
|
1200
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"command": "={{ $json.cmd }}",
|
|
"options": {}
|
|
},
|
|
"id": "exec-inspect-image",
|
|
"name": "Inspect New Image (Text)",
|
|
"type": "n8n-nodes-base.executeCommand",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
3100,
|
|
1200
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Compare old and new image IDs to detect if update is needed\nconst stdout = $input.item.json.stdout;\nconst prevData = $('Build Image Inspect').item.json;\nconst currentImageId = prevData.currentImageId;\nconst chatId = prevData.chatId;\n\nlet newImage;\ntry {\n newImage = JSON.parse(stdout);\n} catch (e) {\n return {\n json: {\n error: true,\n chatId,\n text: `Failed to inspect new image: ${e.message}`\n }\n };\n}\n\nconst newImageId = newImage.Id;\n\nif (currentImageId === newImageId) {\n // No update needed - notify user\n return { json: { needsUpdate: false, chatId, containerName: prevData.containerName } };\n}\n\n// Extract new version from labels\nconst labels = newImage.Config?.Labels || {};\nconst newVersion = labels['org.opencontainers.image.version']\n || labels['version']\n || newImageId.substring(7, 19);\n\nreturn {\n json: {\n needsUpdate: true,\n currentImageId,\n newImageId,\n currentVersion: prevData.currentVersion,\n newVersion,\n containerConfig: prevData.containerConfig,\n hostConfig: prevData.hostConfig,\n networkSettings: prevData.networkSettings,\n containerName: prevData.containerName,\n containerId: prevData.containerId,\n chatId\n }\n};"
|
|
},
|
|
"id": "code-compare-digests",
|
|
"name": "Compare Digests",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
3320,
|
|
1200
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "strict"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "needs-update",
|
|
"leftValue": "={{ $json.needsUpdate }}",
|
|
"rightValue": true,
|
|
"operator": {
|
|
"type": "boolean",
|
|
"operation": "equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"options": {}
|
|
},
|
|
"id": "if-needs-update",
|
|
"name": "Check If Update Needed",
|
|
"type": "n8n-nodes-base.if",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
3540,
|
|
1200
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Format 'already up to date' message\nconst containerName = $json.containerName;\nconst chatId = $json.chatId;\n\nreturn {\n json: {\n chatId,\n text: `<b>${containerName}</b> is already up to date`\n }\n};"
|
|
},
|
|
"id": "code-format-no-update",
|
|
"name": "Format No Update",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
3540,
|
|
1400
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"resource": "message",
|
|
"operation": "sendMessage",
|
|
"chatId": "={{ $json.chatId }}",
|
|
"text": "={{ $json.text }}",
|
|
"additionalFields": {
|
|
"parse_mode": "HTML"
|
|
}
|
|
},
|
|
"id": "telegram-send-no-update",
|
|
"name": "Send No Update",
|
|
"type": "n8n-nodes-base.telegram",
|
|
"typeVersion": 1.2,
|
|
"position": [
|
|
3760,
|
|
1400
|
|
],
|
|
"credentials": {
|
|
"telegramApi": {
|
|
"id": "I0xTTiASl7C1NZhJ",
|
|
"name": "Telegram account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Build stop container command\nconst containerId = $json.containerId;\n\nreturn {\n json: {\n cmd: `curl -s -o /dev/null -w \"%{http_code}\" --max-time 5 -X POST 'http://docker-socket-proxy:2375/v1.47/containers/${containerId}/stop?t=10'`,\n containerId,\n containerName: $json.containerName,\n currentVersion: $json.currentVersion,\n newVersion: $json.newVersion,\n currentImageId: $json.currentImageId,\n containerConfig: $json.containerConfig,\n hostConfig: $json.hostConfig,\n networkSettings: $json.networkSettings,\n chatId: $json.chatId\n }\n};"
|
|
},
|
|
"id": "code-build-stop-cmd",
|
|
"name": "Build Stop Command",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
3760,
|
|
1200
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"command": "={{ $json.cmd }}",
|
|
"options": {}
|
|
},
|
|
"id": "exec-stop-container",
|
|
"name": "Stop Container",
|
|
"type": "n8n-nodes-base.executeCommand",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
3980,
|
|
1200
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Verify container stopped and build remove command\nconst stdout = $input.item.json.stdout;\nconst prevData = $('Build Stop Command').item.json;\nconst containerId = prevData.containerId;\nconst chatId = prevData.chatId;\n\nconst statusCode = parseInt(stdout.trim());\n\n// 204: stopped, 304: already stopped - both OK\nif (statusCode !== 204 && statusCode !== 304) {\n return {\n json: {\n error: true,\n chatId,\n text: `Failed to stop container: HTTP ${statusCode}`\n }\n };\n}\n\nreturn {\n json: {\n cmd: `curl -s -o /dev/null -w \"%{http_code}\" --max-time 5 -X DELETE 'http://docker-socket-proxy:2375/v1.47/containers/${containerId}'`,\n containerId,\n containerName: prevData.containerName,\n currentVersion: prevData.currentVersion,\n newVersion: prevData.newVersion,\n currentImageId: prevData.currentImageId,\n containerConfig: prevData.containerConfig,\n hostConfig: prevData.hostConfig,\n networkSettings: prevData.networkSettings,\n chatId\n }\n};"
|
|
},
|
|
"id": "code-verify-stop-build-remove",
|
|
"name": "Verify Stop Build Remove",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
4200,
|
|
1200
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"command": "={{ $json.cmd }}",
|
|
"options": {}
|
|
},
|
|
"id": "exec-remove-container",
|
|
"name": "Remove Container",
|
|
"type": "n8n-nodes-base.executeCommand",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
4420,
|
|
1200
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Build container create request body from saved config\nconst prevData = $('Verify Stop Build Remove').item.json;\nconst config = prevData.containerConfig;\nconst hostConfig = prevData.hostConfig;\nconst networkSettings = prevData.networkSettings;\nconst containerName = prevData.containerName;\nconst chatId = prevData.chatId;\n\n// Build NetworkingConfig from NetworkSettings\nconst networks = {};\nfor (const [name, netConfig] of Object.entries(networkSettings.Networks || {})) {\n networks[name] = {\n IPAMConfig: netConfig.IPAMConfig,\n Links: netConfig.Links,\n Aliases: netConfig.Aliases\n };\n}\n\nconst createBody = {\n ...config,\n HostConfig: hostConfig,\n NetworkingConfig: {\n EndpointsConfig: networks\n }\n};\n\n// Remove fields that shouldn't be in create request\ndelete createBody.Hostname;\ndelete createBody.Domainname;\n\nreturn {\n json: {\n createBody: JSON.stringify(createBody),\n containerName,\n currentVersion: prevData.currentVersion,\n newVersion: prevData.newVersion,\n currentImageId: prevData.currentImageId,\n chatId\n }\n};"
|
|
},
|
|
"id": "code-build-create-body",
|
|
"name": "Build Create Body",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
4640,
|
|
1200
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Build the create container command using a temp file approach\nconst createBody = $json.createBody;\nconst containerName = $json.containerName;\nconst chatId = $json.chatId;\n\n// Write body to command using here-doc to avoid shell escaping issues\nconst cmd = `curl -s -X POST --max-time 5 -H \"Content-Type: application/json\" -d '${createBody.replace(/'/g, \"'\\\\''\")}' 'http://docker-socket-proxy:2375/v1.47/containers/create?name=${encodeURIComponent(containerName)}'`;\n\nreturn {\n json: {\n cmd,\n containerName,\n currentVersion: $json.currentVersion,\n newVersion: $json.newVersion,\n currentImageId: $json.currentImageId,\n chatId\n }\n};"
|
|
},
|
|
"id": "code-build-create-cmd",
|
|
"name": "Build Create Command",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
4860,
|
|
1200
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"command": "={{ $json.cmd }}",
|
|
"options": {}
|
|
},
|
|
"id": "exec-create-container",
|
|
"name": "Create Container",
|
|
"type": "n8n-nodes-base.executeCommand",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
5080,
|
|
1200
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Parse create response and extract new container ID\nconst stdout = $input.item.json.stdout;\nconst prevData = $('Build Create Command').item.json;\nconst chatId = prevData.chatId;\n\nlet response;\ntry {\n response = JSON.parse(stdout);\n} catch (e) {\n return {\n json: {\n error: true,\n chatId,\n text: `Create failed: ${stdout}`\n }\n };\n}\n\nif (response.message) {\n // Error response from Docker\n return {\n json: {\n error: true,\n chatId,\n text: `Create failed: ${response.message}`\n }\n };\n}\n\nreturn {\n json: {\n newContainerId: response.Id,\n containerName: prevData.containerName,\n currentVersion: prevData.currentVersion,\n newVersion: prevData.newVersion,\n currentImageId: prevData.currentImageId,\n chatId\n }\n};"
|
|
},
|
|
"id": "code-parse-create-response",
|
|
"name": "Parse Create Response",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
5300,
|
|
1200
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Build start command for new container\nconst newContainerId = $json.newContainerId;\n\nreturn {\n json: {\n cmd: `curl -s -o /dev/null -w \"%{http_code}\" --max-time 5 -X POST 'http://docker-socket-proxy:2375/v1.47/containers/${newContainerId}/start'`,\n newContainerId,\n containerName: $json.containerName,\n currentVersion: $json.currentVersion,\n newVersion: $json.newVersion,\n currentImageId: $json.currentImageId,\n chatId: $json.chatId\n }\n};"
|
|
},
|
|
"id": "code-build-start-cmd",
|
|
"name": "Build Start Command",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
5520,
|
|
1200
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"command": "={{ $json.cmd }}",
|
|
"options": {}
|
|
},
|
|
"id": "exec-start-new-container",
|
|
"name": "Start New Container",
|
|
"type": "n8n-nodes-base.executeCommand",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
5740,
|
|
1200
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Parse start result and format success message\nconst stdout = $input.item.json.stdout;\nconst prevData = $('Build Start Command').item.json;\nconst containerName = prevData.containerName;\nconst currentVersion = prevData.currentVersion;\nconst newVersion = prevData.newVersion;\nconst currentImageId = prevData.currentImageId;\nconst chatId = prevData.chatId;\n\nconst statusCode = parseInt(stdout.trim());\n\n// 204: started, 304: already running\nif (statusCode !== 204 && statusCode !== 304) {\n return {\n json: {\n chatId,\n text: `Failed to update ${containerName}`,\n currentImageId: null\n }\n };\n}\n\nconst message = `<b>${containerName}</b> updated: ${currentVersion} \\u2192 ${newVersion}`;\n\nreturn {\n json: {\n chatId,\n text: message,\n currentImageId\n }\n};"
|
|
},
|
|
"id": "code-format-update-result",
|
|
"name": "Format Update Result",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
5960,
|
|
1200
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"resource": "message",
|
|
"operation": "sendMessage",
|
|
"chatId": "={{ $json.chatId }}",
|
|
"text": "={{ $json.text }}",
|
|
"additionalFields": {
|
|
"parse_mode": "HTML"
|
|
}
|
|
},
|
|
"id": "telegram-send-update-result",
|
|
"name": "Send Update Result",
|
|
"type": "n8n-nodes-base.telegram",
|
|
"typeVersion": 1.2,
|
|
"position": [
|
|
6180,
|
|
1200
|
|
],
|
|
"credentials": {
|
|
"telegramApi": {
|
|
"id": "I0xTTiASl7C1NZhJ",
|
|
"name": "Telegram account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Build remove old image command (fire and forget)\n// Reference Format Update Result since Telegram node doesn't pass through our data\nconst currentImageId = $('Format Update Result').item.json.currentImageId;\n\n// Skip if no image ID (error case) - use no-op command\nif (!currentImageId) {\n return { json: { cmd: 'true', skip: true } };\n}\n\n// Remove the old image - ignore errors (image might be used by another container)\nreturn {\n json: {\n cmd: `curl -s -o /dev/null -w \"%{http_code}\" --max-time 5 -X DELETE 'http://docker-socket-proxy:2375/v1.47/images/${currentImageId}?force=false'`,\n currentImageId\n }\n};"
|
|
},
|
|
"id": "code-build-remove-image",
|
|
"name": "Build Remove Image Command",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
6400,
|
|
1200
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"command": "={{ $json.cmd }}",
|
|
"options": {}
|
|
},
|
|
"id": "exec-remove-old-image",
|
|
"name": "Remove Old Image",
|
|
"type": "n8n-nodes-base.executeCommand",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
6620,
|
|
1200
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Parse logs command from message\nconst message = $json.message;\nconst text = message.text.toLowerCase().trim();\nconst chatId = message.chat.id;\nconst messageId = message.message_id;\n\n// Extract container name and optional line count\n// Formats: \"logs plex\", \"logs plex 100\", \"show logs plex\"\nlet containerQuery = '';\nlet lineCount = 50; // default\n\n// Remove \"show\" prefix if present\nlet cleanText = text.replace(/^show\\s+/, '');\n// Remove \"logs\" keyword\ncleanText = cleanText.replace(/^logs\\s*/, '');\n\n// Check for line count at the end\nconst parts = cleanText.trim().split(/\\s+/);\nif (parts.length > 1) {\n const lastPart = parts[parts.length - 1];\n if (/^\\d+$/.test(lastPart)) {\n lineCount = Math.min(Math.max(parseInt(lastPart), 1), 1000);\n parts.pop();\n }\n}\ncontainerQuery = parts.join(' ').trim();\n\nif (!containerQuery) {\n return {\n json: {\n error: true,\n chatId: chatId,\n text: 'Please specify a container name (e.g., \"logs plex\" or \"logs plex 100\")'\n }\n };\n}\n\nreturn {\n json: {\n containerQuery: containerQuery,\n lines: lineCount,\n chatId: chatId,\n messageId: messageId\n }\n};"
|
|
},
|
|
"id": "code-parse-logs",
|
|
"name": "Parse Logs Command",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
900,
|
|
600
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"command": "curl -s --max-time 5 'http://docker-socket-proxy:2375/v1.47/containers/json?all=true'",
|
|
"options": {}
|
|
},
|
|
"id": "exec-docker-list-logs",
|
|
"name": "Docker List for Logs",
|
|
"type": "n8n-nodes-base.executeCommand",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
1120,
|
|
600
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Get Docker API response and logs info from Parse Logs\nconst dockerOutput = $input.item.json.stdout;\nconst logsData = $('Parse Logs Command').item.json;\nconst containerQuery = logsData.containerQuery;\nconst lines = logsData.lines;\nconst chatId = logsData.chatId;\n\n// Parse JSON response\nlet containers;\ntry {\n if (!dockerOutput || dockerOutput.trim() === '') {\n throw new Error('Empty response');\n }\n containers = JSON.parse(dockerOutput);\n} catch (e) {\n return [{\n json: {\n matchCount: -1,\n error: true,\n chatId: chatId,\n text: \"Cannot connect to Docker\"\n }\n }];\n}\n\n// Function to normalize container names\nfunction normalizeName(name) {\n return name\n .replace(/^\\//, '') // Remove leading slash\n .replace(/^(linuxserver[-_]|binhex[-_])/i, '') // Remove common prefixes\n .toLowerCase();\n}\n\nconst normalized = containerQuery.toLowerCase();\n\n// First check for exact match\nconst exactMatch = containers.find(c => normalizeName(c.Names[0]) === normalized);\nif (exactMatch) {\n return [{\n json: {\n matches: [{ Id: exactMatch.Id, Name: exactMatch.Names[0].replace(/^\\//, ''), State: exactMatch.State }],\n matchCount: 1,\n containerQuery: containerQuery,\n lines: lines,\n chatId: chatId\n }\n }];\n}\n\n// Fall back to substring matching (container name contains query)\nconst matches = containers.filter(c => {\n const containerName = normalizeName(c.Names[0]);\n return containerName.includes(normalized);\n});\n\n// Return match results with all necessary context\nreturn [{\n json: {\n matches: matches.map(c => ({\n Id: c.Id,\n Name: c.Names[0].replace(/^\\//, ''),\n State: c.State\n })),\n matchCount: matches.length,\n containerQuery: containerQuery,\n lines: lines,\n chatId: chatId\n }\n}];"
|
|
},
|
|
"id": "code-match-logs-container",
|
|
"name": "Match Logs Container",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
1340,
|
|
600
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"rules": {
|
|
"values": [
|
|
{
|
|
"id": "logs-docker-error",
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"typeValidation": "loose"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "count-negative",
|
|
"leftValue": "={{ $json.matchCount }}",
|
|
"rightValue": 0,
|
|
"operator": {
|
|
"type": "number",
|
|
"operation": "lt"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"renameOutput": false
|
|
},
|
|
{
|
|
"id": "logs-no-match",
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"typeValidation": "loose"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "count-zero",
|
|
"leftValue": "={{ $json.matchCount }}",
|
|
"rightValue": 0,
|
|
"operator": {
|
|
"type": "number",
|
|
"operation": "equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"renameOutput": false
|
|
},
|
|
{
|
|
"id": "logs-single-match",
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"typeValidation": "loose"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "count-one",
|
|
"leftValue": "={{ $json.matchCount }}",
|
|
"rightValue": 1,
|
|
"operator": {
|
|
"type": "number",
|
|
"operation": "equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"renameOutput": false
|
|
},
|
|
{
|
|
"id": "logs-multiple-matches",
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"typeValidation": "loose"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "count-many",
|
|
"leftValue": "={{ $json.matchCount }}",
|
|
"rightValue": 1,
|
|
"operator": {
|
|
"type": "number",
|
|
"operation": "gt"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"renameOutput": false
|
|
}
|
|
]
|
|
},
|
|
"options": {}
|
|
},
|
|
"id": "switch-logs-match-count",
|
|
"name": "Check Logs Match Count",
|
|
"type": "n8n-nodes-base.switch",
|
|
"typeVersion": 3.2,
|
|
"position": [
|
|
1560,
|
|
600
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Build Docker logs API curl command\nconst matchData = $input.item.json;\nconst container = matchData.matches[0]; // Single match verified by switch\nconst lines = matchData.lines;\n\nconst cmd = `curl -s --max-time 5 \"http://docker-socket-proxy:2375/v1.47/containers/${container.Id}/logs?stdout=1&stderr=1&tail=${lines}×tamps=1\"`;\n\nreturn {\n json: {\n command: cmd,\n containerName: container.Name,\n containerId: container.Id,\n lines: lines,\n chatId: matchData.chatId\n }\n};"
|
|
},
|
|
"id": "code-build-logs-cmd",
|
|
"name": "Build Logs Command",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
1780,
|
|
500
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"command": "={{ $json.command }}",
|
|
"options": {}
|
|
},
|
|
"id": "exec-logs",
|
|
"name": "Execute Logs",
|
|
"type": "n8n-nodes-base.executeCommand",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
2000,
|
|
500
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Format Docker logs for Telegram\nconst rawOutput = $input.item.json.stdout || '';\nconst containerName = $('Build Logs Command').item.json.containerName;\nconst requestedLines = $('Build Logs Command').item.json.lines;\nconst chatId = $('Build Logs Command').item.json.chatId;\n\n// HTML escape function for log content\nfunction escapeHtml(text) {\n return text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');\n}\n\n// Handle empty logs\nif (!rawOutput || rawOutput.trim() === '') {\n return {\n json: {\n chatId: chatId,\n text: `No logs available for <b>${containerName}</b>`\n }\n };\n}\n\n// Docker API with timestamps returns text lines when using tail parameter\n// But may have 8-byte binary headers we need to strip\nconst lines = rawOutput.split('\\n')\n .filter(line => line.length > 0)\n .map(line => {\n // Check if line starts with binary header (non-printable chars in first 8 bytes)\n if (line.length > 8 && line.charCodeAt(0) <= 2) {\n return line.substring(8);\n }\n return line;\n })\n .join('\\n');\n\n// Truncate for Telegram (4096 char limit, leave room for header)\nconst maxLen = 3800;\nconst truncated = lines.length > maxLen\n ? lines.substring(0, maxLen) + '\\n... (truncated)'\n : lines;\n\n// Escape HTML entities in log content to prevent parse errors\nconst escaped = escapeHtml(truncated);\n\nconst lineCount = lines.split('\\n').length;\nconst header = `Logs for <b>${containerName}</b> (last ${lineCount} lines):\\n\\n`;\n\nreturn {\n json: {\n chatId: chatId,\n text: header + '<pre>' + escaped + '</pre>'\n }\n};"
|
|
},
|
|
"id": "code-format-logs",
|
|
"name": "Format Logs",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
2220,
|
|
500
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"resource": "message",
|
|
"operation": "sendMessage",
|
|
"chatId": "={{ $json.chatId }}",
|
|
"text": "={{ $json.text }}",
|
|
"additionalFields": {
|
|
"parse_mode": "HTML"
|
|
}
|
|
},
|
|
"id": "telegram-send-logs",
|
|
"name": "Send Logs Response",
|
|
"type": "n8n-nodes-base.telegram",
|
|
"typeVersion": 1.2,
|
|
"position": [
|
|
2440,
|
|
500
|
|
],
|
|
"credentials": {
|
|
"telegramApi": {
|
|
"id": "I0xTTiASl7C1NZhJ",
|
|
"name": "Telegram account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"resource": "message",
|
|
"operation": "sendMessage",
|
|
"chatId": "={{ $json.chatId }}",
|
|
"text": "={{ $json.text || 'Error retrieving logs' }}",
|
|
"additionalFields": {
|
|
"parse_mode": "HTML"
|
|
}
|
|
},
|
|
"id": "telegram-send-logs-error",
|
|
"name": "Send Logs Error",
|
|
"type": "n8n-nodes-base.telegram",
|
|
"typeVersion": 1.2,
|
|
"position": [
|
|
1780,
|
|
700
|
|
],
|
|
"credentials": {
|
|
"telegramApi": {
|
|
"id": "I0xTTiASl7C1NZhJ",
|
|
"name": "Telegram account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "const data = $input.item.json;\nreturn {\n json: {\n chatId: data.chatId,\n text: `No container found matching \"<b>${data.containerQuery}</b>\".\\n\\nTry \"status\" to see all containers.`\n }\n};"
|
|
},
|
|
"id": "code-format-logs-no-match",
|
|
"name": "Format Logs No Match",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
1780,
|
|
650
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "const data = $input.item.json;\nconst names = data.matches.map(m => m.Name).join('\\n- ');\nreturn {\n json: {\n chatId: data.chatId,\n text: `Found ${data.matches.length} containers matching \"<b>${data.containerQuery}</b>\":\\n\\n- ${names}\\n\\nPlease be more specific.`\n }\n};"
|
|
},
|
|
"id": "code-format-logs-multiple",
|
|
"name": "Format Logs Multiple",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
1780,
|
|
600
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"resource": "message",
|
|
"operation": "sendMessage",
|
|
"chatId": "={{ $json.message.chat.id }}",
|
|
"text": "<b>Commands:</b>\n\n• status\n• start [name]\n• stop [name]\n• restart [name]\n• update [name]\n• logs [name]",
|
|
"additionalFields": {
|
|
"parse_mode": "HTML"
|
|
}
|
|
},
|
|
"id": "telegram-show-menu",
|
|
"name": "Show Menu",
|
|
"type": "n8n-nodes-base.telegram",
|
|
"typeVersion": 1.2,
|
|
"position": [
|
|
1120,
|
|
300
|
|
],
|
|
"credentials": {
|
|
"telegramApi": {
|
|
"id": "I0xTTiASl7C1NZhJ",
|
|
"name": "Telegram account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Build Container List Keyboard for /status command\nconst dockerOutput = $input.item.json.stdout;\nconst message = $('Keyword Router').item.json.message;\nconst chatId = message.chat.id;\n\n// Parse JSON response\nlet containers;\ntry {\n if (!dockerOutput || dockerOutput.trim() === '') {\n throw new Error('Empty response');\n }\n containers = JSON.parse(dockerOutput);\n} catch (e) {\n return [{\n json: {\n chatId: chatId,\n error: true,\n text: \"Cannot connect to Docker\",\n isSingleContainer: false\n }\n }];\n}\n\n// Function to normalize container names (strip prefixes)\nfunction normalizeName(name) {\n return name\n .replace(/^\\//, '')\n .replace(/^(linuxserver[-_]|binhex[-_])/i, '')\n .toLowerCase();\n}\n\n// Check if user specified a container name (e.g., \"/status plex\")\nconst text = (message.text || '').toLowerCase().trim();\nlet requestedName = null;\nconst parts = text.split(/\\s+/);\nif (parts.length > 1) {\n requestedName = parts.filter(p => p !== 'status' && p !== '/status').join(' ');\n}\n\n// If specific container requested, route to submenu\nif (requestedName) {\n const matches = containers.filter(c => {\n const containerName = normalizeName(c.Names[0]);\n return containerName.includes(requestedName);\n });\n \n if (matches.length === 1) {\n const container = matches[0];\n return [{\n json: {\n isSingleContainer: true,\n chatId: chatId,\n containerName: normalizeName(container.Names[0]),\n containerId: container.Id,\n containerState: container.State,\n containerStatus: container.Status,\n containerImage: container.Image\n }\n }];\n } else if (matches.length === 0) {\n return [{\n json: {\n chatId: chatId,\n error: true,\n text: 'No container found matching \"' + requestedName + '\"',\n isSingleContainer: false\n }\n }];\n }\n // Multiple matches - show them all in keyboard below\n}\n\n// Build paginated container list keyboard\nconst page = 0; // Initial page\nconst containersPerPage = 6;\n\n// Group by state (running first)\nconst running = containers.filter(c => c.State === 'running');\nconst stopped = containers.filter(c => c.State !== 'running');\nconst sortedContainers = [...running, ...stopped];\n\nconst totalPages = Math.ceil(sortedContainers.length / containersPerPage);\nconst start = page * containersPerPage;\nconst pageContainers = sortedContainers.slice(start, start + containersPerPage);\n\n// Build keyboard rows\nconst keyboard = [];\n\npageContainers.forEach(container => {\n const name = normalizeName(container.Names[0]);\n const stateIcon = container.State === 'running' ? '\\u{1F7E2}' : '\\u26AA'; // Green circle or white circle\n const stateText = container.State === 'running' ? 'Running' : 'Stopped';\n keyboard.push([{\n text: `${stateIcon} ${name} - ${stateText}`,\n callback_data: `select:${name}`\n }]);\n});\n\n// Add navigation row if needed\nif (totalPages > 1) {\n const navRow = [];\n if (page > 0) {\n navRow.push({ text: '\\u25C0\\uFE0F Previous', callback_data: `list:${page - 1}` });\n }\n navRow.push({ text: `${page + 1}/${totalPages}`, callback_data: 'noop' });\n if (page < totalPages - 1) {\n navRow.push({ text: 'Next \\u25B6\\uFE0F', callback_data: `list:${page + 1}` });\n }\n keyboard.push(navRow);\n}\n\n// Build header text\nconst runningCount = running.length;\nconst totalCount = sortedContainers.length;\nlet headerText = `<b>\\u{1F5C2} Containers</b> (${runningCount}/${totalCount} running)`;\nif (totalPages > 1) {\n headerText += `\\n\\nPage ${page + 1} of ${totalPages}`;\n}\nheaderText += '\\n\\nTap a container to manage it:';\n\nreturn [{\n json: {\n chatId: chatId,\n text: headerText,\n reply_markup: { inline_keyboard: keyboard },\n isSingleContainer: false\n }\n}];"
|
|
},
|
|
"id": "code-build-container-list-keyboard",
|
|
"name": "Build Container List Keyboard",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
1340,
|
|
0
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"method": "POST",
|
|
"url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/sendMessage",
|
|
"sendBody": true,
|
|
"specifyBody": "json",
|
|
"jsonBody": "={{ JSON.stringify({ chat_id: $json.chatId, text: $json.text, parse_mode: 'HTML', reply_markup: $json.reply_markup }) }}",
|
|
"options": {}
|
|
},
|
|
"id": "http-send-container-list",
|
|
"name": "Send Container List",
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.2,
|
|
"position": [
|
|
1560,
|
|
0
|
|
],
|
|
"credentials": {
|
|
"telegramApi": {
|
|
"id": "I0xTTiASl7C1NZhJ",
|
|
"name": "Telegram account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "strict"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "is-single-container",
|
|
"leftValue": "={{ $json.isSingleContainer }}",
|
|
"rightValue": true,
|
|
"operator": {
|
|
"type": "boolean",
|
|
"operation": "equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"options": {}
|
|
},
|
|
"id": "if-single-container",
|
|
"name": "Check Single Container",
|
|
"type": "n8n-nodes-base.if",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
1560,
|
|
-100
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Build Container Submenu for direct access (/status plex) or callback selection\nconst data = $input.item.json;\nconst chatId = data.chatId;\nconst containerName = data.containerName;\nconst state = data.containerState;\nconst status = data.containerStatus;\nconst image = data.containerImage;\n\n// Build action keyboard based on container state\nconst keyboard = [];\n\nif (state === '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} 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\n// Build status text\nconst stateIcon = state === 'running' ? '\\u{1F7E2}' : '\\u26AA';\nlet text = `${stateIcon} <b>${containerName}</b>\\n\\n`;\ntext += `<b>State:</b> ${state}\\n`;\ntext += `<b>Status:</b> ${status}\\n`;\ntext += `<b>Image:</b> ${image}`;\n\nreturn [{\n json: {\n chatId: chatId,\n text: text,\n reply_markup: { inline_keyboard: keyboard }\n }\n}];"
|
|
},
|
|
"id": "code-build-container-submenu-direct",
|
|
"name": "Build Container Submenu Direct",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
1780,
|
|
-100
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"method": "POST",
|
|
"url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/sendMessage",
|
|
"sendBody": true,
|
|
"specifyBody": "json",
|
|
"jsonBody": "={{ JSON.stringify({ chat_id: $json.chatId, text: $json.text, parse_mode: 'HTML', reply_markup: $json.reply_markup }) }}",
|
|
"options": {}
|
|
},
|
|
"id": "http-send-container-submenu-direct",
|
|
"name": "Send Container Submenu Direct",
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.2,
|
|
"position": [
|
|
2000,
|
|
-100
|
|
],
|
|
"credentials": {
|
|
"telegramApi": {
|
|
"id": "I0xTTiASl7C1NZhJ",
|
|
"name": "Telegram account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"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-select-callback",
|
|
"name": "Answer Select Callback",
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.2,
|
|
"position": [
|
|
1340,
|
|
900
|
|
],
|
|
"credentials": {
|
|
"telegramApi": {
|
|
"id": "I0xTTiASl7C1NZhJ",
|
|
"name": "Telegram account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Prepare container fetch for submenu\nconst data = $(\"Parse Callback Data\").item.json;\nreturn {\n json: {\n queryId: data.queryId,\n chatId: data.chatId,\n messageId: data.messageId,\n containerName: data.containerName\n }\n};"
|
|
},
|
|
"id": "code-prepare-container-fetch",
|
|
"name": "Prepare Container Fetch",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
1560,
|
|
900
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"method": "GET",
|
|
"url": "=http://docker-socket-proxy:2375/containers/json?all=true",
|
|
"options": {}
|
|
},
|
|
"id": "http-get-single-container",
|
|
"name": "Get Single Container",
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.2,
|
|
"position": [
|
|
1780,
|
|
900
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Build Container Submenu for callback selection\nconst containers = $input.all().map(item => item.json);\nconst prevData = $(\"Prepare Container Fetch\").item.json;\nconst chatId = prevData.chatId;\nconst messageId = prevData.messageId;\nconst searchName = prevData.containerName.toLowerCase();\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]) === searchName);\n\nif (!container) {\n return [{\n json: {\n chatId,\n messageId,\n text: `Container \\\"${searchName}\\\" not found`,\n reply_markup: { inline_keyboard: [[{ text: \"\\u25C0\\uFE0F Back to List\", callback_data: \"list:0\" }]] }\n }\n }];\n}\n\nconst containerName = normalizeName(container.Names[0]);\nconst state = container.State;\nconst status = container.Status;\nconst image = container.Image;\n\n// Build action keyboard based on container state\nconst keyboard = [];\n\nif (state === \"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} 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\n// Build status text\nconst stateIcon = state === \"running\" ? \"\\u{1F7E2}\" : \"\\u26AA\";\nlet text = `${stateIcon} <b>${containerName}</b>\\n\\n`;\ntext += `<b>State:</b> ${state}\\n`;\ntext += `<b>Status:</b> ${status}\\n`;\ntext += `<b>Image:</b> ${image}`;\n\nreturn [{\n json: {\n chatId,\n messageId,\n text,\n reply_markup: { inline_keyboard: keyboard }\n }\n}];"
|
|
},
|
|
"id": "code-build-container-submenu",
|
|
"name": "Build Container Submenu",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
2000,
|
|
900
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"method": "POST",
|
|
"url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/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-container-submenu",
|
|
"name": "Send Container Submenu",
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.2,
|
|
"position": [
|
|
2220,
|
|
900
|
|
],
|
|
"credentials": {
|
|
"telegramApi": {
|
|
"id": "I0xTTiASl7C1NZhJ",
|
|
"name": "Telegram account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"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-noop-callback",
|
|
"name": "Answer Noop Callback",
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.2,
|
|
"position": [
|
|
1340,
|
|
1100
|
|
],
|
|
"credentials": {
|
|
"telegramApi": {
|
|
"id": "I0xTTiASl7C1NZhJ",
|
|
"name": "Telegram account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"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-list-callback",
|
|
"name": "Answer List Callback",
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.2,
|
|
"position": [
|
|
1340,
|
|
1000
|
|
],
|
|
"credentials": {
|
|
"telegramApi": {
|
|
"id": "I0xTTiASl7C1NZhJ",
|
|
"name": "Telegram account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Prepare list pagination request\nconst data = $(\"Parse Callback Data\").item.json;\nreturn {\n json: {\n queryId: data.queryId,\n chatId: data.chatId,\n messageId: data.messageId,\n page: data.page || 0\n }\n};"
|
|
},
|
|
"id": "code-prepare-list-fetch",
|
|
"name": "Prepare List Fetch",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
1560,
|
|
1000
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"method": "GET",
|
|
"url": "=http://docker-socket-proxy:2375/containers/json?all=true",
|
|
"options": {}
|
|
},
|
|
"id": "http-get-containers-for-list",
|
|
"name": "Get Containers For List",
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.2,
|
|
"position": [
|
|
1780,
|
|
1000
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Build Paginated Container List Keyboard for callback pagination\nconst containers = $input.all().map(item => item.json);\nconst prevData = $(\"Prepare List Fetch\").item.json;\nconst chatId = prevData.chatId;\nconst messageId = prevData.messageId;\nconst page = prevData.page || 0;\n\n// Function to normalize container names\nfunction normalizeName(name) {\n return name\n .replace(/^\\//, \"\")\n .replace(/^(linuxserver[-_]|binhex[-_])/i, \"\")\n .toLowerCase();\n}\n\nconst containersPerPage = 6;\n\n// Group by state (running first)\nconst running = containers.filter(c => c.State === \"running\");\nconst stopped = containers.filter(c => c.State !== \"running\");\nconst sortedContainers = [...running, ...stopped];\n\nconst totalPages = Math.ceil(sortedContainers.length / containersPerPage);\nconst start = page * containersPerPage;\nconst pageContainers = sortedContainers.slice(start, start + containersPerPage);\n\n// Build keyboard rows\nconst keyboard = [];\n\npageContainers.forEach(container => {\n const name = normalizeName(container.Names[0]);\n const stateIcon = container.State === \"running\" ? \"\\u{1F7E2}\" : \"\\u26AA\";\n const stateText = container.State === \"running\" ? \"Running\" : \"Stopped\";\n keyboard.push([{\n text: `${stateIcon} ${name} - ${stateText}`,\n callback_data: `select:${name}`\n }]);\n});\n\n// Add navigation row if needed\nif (totalPages > 1) {\n const navRow = [];\n if (page > 0) {\n navRow.push({ text: \"\\u25C0\\uFE0F Previous\", callback_data: `list:${page - 1}` });\n }\n navRow.push({ text: `${page + 1}/${totalPages}`, callback_data: \"noop\" });\n if (page < totalPages - 1) {\n navRow.push({ text: \"Next \\u25B6\\uFE0F\", callback_data: `list:${page + 1}` });\n }\n keyboard.push(navRow);\n}\n\n// Build header text\nconst runningCount = running.length;\nconst totalCount = sortedContainers.length;\nlet headerText = `<b>\\u{1F5C2} Containers</b> (${runningCount}/${totalCount} running)`;\nif (totalPages > 1) {\n headerText += `\\n\\nPage ${page + 1} of ${totalPages}`;\n}\nheaderText += \"\\n\\nTap a container to manage it:\";\n\nreturn [{\n json: {\n chatId,\n messageId,\n text: headerText,\n reply_markup: { inline_keyboard: keyboard }\n }\n}];"
|
|
},
|
|
"id": "code-build-paginated-list",
|
|
"name": "Build Paginated List",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
2000,
|
|
1000
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"method": "POST",
|
|
"url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/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-edit-container-list",
|
|
"name": "Edit Container List",
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.2,
|
|
"position": [
|
|
2220,
|
|
1000
|
|
],
|
|
"credentials": {
|
|
"telegramApi": {
|
|
"id": "I0xTTiASl7C1NZhJ",
|
|
"name": "Telegram account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"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-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.all().map(item => 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": "// Build action completion message (success shows back only, error shows retry + back)\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\n// Build message based on action type and result\nconst successMessages = {\n start: `\\u25B6\\uFE0F <b>${containerName}</b> started`,\n restart: `\\u{1F504} <b>${containerName}</b> restarted`\n};\n\nlet text;\nlet keyboard;\n\nif (success) {\n text = successMessages[action] || `Action completed on ${containerName}`;\n // Success: only back button\n keyboard = {\n inline_keyboard: [\n [{ text: '\\u25C0\\uFE0F Back to Containers', callback_data: 'list:0' }]\n ]\n };\n} else {\n text = `\\u274C Failed to ${action} <b>${containerName}</b>`;\n // Error: retry and back buttons\n keyboard = {\n inline_keyboard: [\n [{ text: '\\u{1F504} Try Again', callback_data: `action:${action}:${containerName}` }],\n [{ text: '\\u25C0\\uFE0F Back to Containers', callback_data: 'list:0' }]\n ]\n };\n}\n\nreturn {\n json: {\n chatId,\n messageId,\n text,\n reply_markup: 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{{ $env.TELEGRAM_BOT_TOKEN }}/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.all().map(item => 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';\nconst timestamp = new Date().toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', second: '2-digit' });\nlet text = `${stateIcon} <b>${containerName}</b> - Logs <i>(${timestamp})</i>\\n\\n<pre>${logs}</pre>`;\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{{ $env.TELEGRAM_BOT_TOKEN }}/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 <b>Stop ${containerName}?</b>\\n\\nThis will stop the container immediately.\\n\\n<i>Confirmation expires in 30 seconds.</i>`;\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{{ $env.TELEGRAM_BOT_TOKEN }}/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 <b>Update ${containerName}?</b>\\n\\nThis will pull the latest image and recreate the container.\\n\\n<i>Confirmation expires in 30 seconds.</i>`;\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{{ $env.TELEGRAM_BOT_TOKEN }}/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"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"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-confirm-callback",
|
|
"name": "Answer Confirm Callback",
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.2,
|
|
"position": [
|
|
1340,
|
|
1600
|
|
],
|
|
"credentials": {
|
|
"telegramApi": {
|
|
"id": "I0xTTiASl7C1NZhJ",
|
|
"name": "Telegram account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "strict"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "check-expired",
|
|
"leftValue": "={{ $('Parse Callback Data').item.json.expired }}",
|
|
"rightValue": true,
|
|
"operator": {
|
|
"type": "boolean",
|
|
"operation": "equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"options": {}
|
|
},
|
|
"id": "if-confirm-expired",
|
|
"name": "Check Confirm Expired",
|
|
"type": "n8n-nodes-base.if",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
1560,
|
|
1600
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Confirmation expired - return to submenu\nconst data = $('Parse Callback Data').item.json;\nconst containerName = data.containerName;\nconst chatId = data.chatId;\nconst messageId = data.messageId;\n\n// Build keyboard for expired message\nconst keyboard = {\n inline_keyboard: [\n [\n { text: '\\u25C0\\uFE0F Back to List', callback_data: 'list:0' }\n ]\n ]\n};\n\nreturn {\n json: {\n chatId,\n messageId,\n text: `\\u23F0 Confirmation for <b>${containerName}</b> has expired.\\n\\nPlease try again.`,\n reply_markup: keyboard\n }\n};"
|
|
},
|
|
"id": "code-handle-confirm-expired",
|
|
"name": "Handle Confirm Expired",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
1780,
|
|
1700
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"method": "POST",
|
|
"url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/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-expired-confirm",
|
|
"name": "Send Expired Confirm",
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.2,
|
|
"position": [
|
|
2000,
|
|
1700
|
|
],
|
|
"credentials": {
|
|
"telegramApi": {
|
|
"id": "I0xTTiASl7C1NZhJ",
|
|
"name": "Telegram account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"rules": {
|
|
"values": [
|
|
{
|
|
"id": "confirm-stop",
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"typeValidation": "loose"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "is-stop-confirm",
|
|
"leftValue": "={{ $('Parse Callback Data').item.json.confirmAction }}",
|
|
"rightValue": "stop",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"renameOutput": true,
|
|
"outputKey": "stop"
|
|
},
|
|
{
|
|
"id": "confirm-update",
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"typeValidation": "loose"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "is-update-confirm",
|
|
"leftValue": "={{ $('Parse Callback Data').item.json.confirmAction }}",
|
|
"rightValue": "update",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"renameOutput": true,
|
|
"outputKey": "update"
|
|
}
|
|
]
|
|
},
|
|
"options": {
|
|
"fallbackOutput": "none"
|
|
}
|
|
},
|
|
"id": "switch-route-confirm-action",
|
|
"name": "Route Confirm Action",
|
|
"type": "n8n-nodes-base.switch",
|
|
"typeVersion": 3.2,
|
|
"position": [
|
|
1780,
|
|
1600
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Prepare stop action from confirmation\nconst data = $('Parse Callback Data').item.json;\nreturn {\n json: {\n containerName: data.containerName,\n chatId: data.chatId,\n messageId: data.messageId\n }\n};"
|
|
},
|
|
"id": "code-prepare-confirmed-stop",
|
|
"name": "Prepare Confirmed Stop",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
2000,
|
|
1550
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"method": "GET",
|
|
"url": "=http://docker-socket-proxy:2375/containers/json?all=true",
|
|
"options": {}
|
|
},
|
|
"id": "http-get-container-for-stop",
|
|
"name": "Get Container For Stop",
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.2,
|
|
"position": [
|
|
2220,
|
|
1550
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Find container and build stop command\nconst containers = $input.all().map(item => item.json);\nconst prevData = $('Prepare Confirmed Stop').item.json;\nconst containerName = prevData.containerName.toLowerCase();\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 -o /dev/null -w \"%{http_code}\" --max-time 15 -X POST 'http://docker-socket-proxy:2375/v1.47/containers/${containerId}/stop?t=10'`,\n containerId,\n containerName: normalizeName(container.Names[0]),\n chatId,\n messageId\n }\n};"
|
|
},
|
|
"id": "code-build-confirmed-stop-cmd",
|
|
"name": "Build Confirmed Stop Command",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
2440,
|
|
1550
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"command": "={{ $json.cmd }}",
|
|
"options": {}
|
|
},
|
|
"id": "exec-confirmed-stop",
|
|
"name": "Execute Confirmed Stop",
|
|
"type": "n8n-nodes-base.executeCommand",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
2660,
|
|
1550
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Build stop completion message (success shows back only, error shows retry + back)\nconst stdout = $input.item.json.stdout;\nconst prevData = $('Build Confirmed Stop Command').item.json;\nconst containerName = prevData.containerName;\nconst chatId = prevData.chatId;\nconst messageId = prevData.messageId;\n\nconst statusCode = parseInt(stdout.trim());\n\n// 204: Success, 304: Already stopped\nconst success = statusCode === 204 || statusCode === 304;\n\nlet text;\nlet keyboard;\n\nif (success) {\n text = `\\u23F9\\uFE0F <b>${containerName}</b> stopped`;\n // Success: only back button\n keyboard = {\n inline_keyboard: [\n [{ text: '\\u25C0\\uFE0F Back to Containers', callback_data: 'list:0' }]\n ]\n };\n} else {\n text = `\\u274C Failed to stop <b>${containerName}</b>`;\n // Error: retry and back buttons\n const timestamp = Math.floor(Date.now() / 1000);\n keyboard = {\n inline_keyboard: [\n [{ text: '\\u{1F504} Try Again', callback_data: `confirm:stop:${containerName}:${timestamp}` }],\n [{ text: '\\u25C0\\uFE0F Back to Containers', callback_data: 'list:0' }]\n ]\n };\n}\n\nreturn {\n json: {\n chatId,\n messageId,\n text,\n reply_markup: keyboard\n }\n};"
|
|
},
|
|
"id": "code-format-confirmed-stop-result",
|
|
"name": "Format Confirmed Stop Result",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
2880,
|
|
1550
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"method": "POST",
|
|
"url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/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-confirmed-stop-result",
|
|
"name": "Send Confirmed Stop Result",
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.2,
|
|
"position": [
|
|
3100,
|
|
1550
|
|
],
|
|
"credentials": {
|
|
"telegramApi": {
|
|
"id": "I0xTTiASl7C1NZhJ",
|
|
"name": "Telegram account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Build update progress message - removes buttons during operation\nconst data = $('Parse Callback Data').item.json;\nconst containerName = data.containerName;\nconst chatId = data.chatId;\nconst messageId = data.messageId;\n\n// Progress message with no buttons (prevents duplicate actions)\nconst text = `\\u2B06\\uFE0F <b>Updating ${containerName}...</b>\\n\\nPulling latest image and recreating container.\\nThis may take a few minutes.`;\n\n// Empty keyboard removes all buttons during update\nconst reply_markup = { inline_keyboard: [] };\n\nreturn {\n json: {\n containerName,\n chatId,\n messageId,\n progressText: text,\n reply_markup\n }\n};"
|
|
},
|
|
"id": "code-prepare-confirmed-update",
|
|
"name": "Prepare Confirmed Update",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
2000,
|
|
1650
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"method": "POST",
|
|
"url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/editMessageText",
|
|
"sendBody": true,
|
|
"specifyBody": "json",
|
|
"jsonBody": "={{ JSON.stringify({ chat_id: $json.chatId, message_id: $json.messageId, text: $json.progressText, parse_mode: 'HTML', reply_markup: $json.reply_markup }) }}",
|
|
"options": {}
|
|
},
|
|
"id": "http-show-update-progress",
|
|
"name": "Show Update Progress",
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.2,
|
|
"position": [
|
|
2220,
|
|
1650
|
|
],
|
|
"credentials": {
|
|
"telegramApi": {
|
|
"id": "I0xTTiASl7C1NZhJ",
|
|
"name": "Telegram account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"method": "GET",
|
|
"url": "=http://docker-socket-proxy:2375/containers/json?all=true",
|
|
"options": {}
|
|
},
|
|
"id": "http-get-container-for-update",
|
|
"name": "Get Container For Update",
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.2,
|
|
"position": [
|
|
2440,
|
|
1650
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Find container and get inspect data for update\nconst containers = $input.all().map(item => item.json);\nconst prevData = $('Prepare Confirmed Update').item.json;\nconst containerName = prevData.containerName.toLowerCase();\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 fullName = container.Names[0].replace(/^\\//, '');\n\nreturn {\n json: {\n containerId,\n containerName: normalizeName(container.Names[0]),\n fullContainerName: fullName,\n imageName: container.Image,\n chatId,\n messageId\n }\n};"
|
|
},
|
|
"id": "code-find-container-for-update",
|
|
"name": "Find Container For Update",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
2660,
|
|
1650
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"method": "GET",
|
|
"url": "=http://docker-socket-proxy:2375/containers/{{ $json.containerId }}/json",
|
|
"options": {}
|
|
},
|
|
"id": "http-inspect-container-for-update",
|
|
"name": "Inspect Container For Update",
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.2,
|
|
"position": [
|
|
2880,
|
|
1650
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Parse container config and build pull command\nconst inspectData = $input.item.json;\nconst prevData = $('Find Container For Update').item.json;\nconst containerId = prevData.containerId;\nconst containerName = prevData.containerName;\nconst fullContainerName = prevData.fullContainerName;\nconst chatId = prevData.chatId;\nconst messageId = prevData.messageId;\n\n// Extract image info\nlet imageName = inspectData.Config.Image;\nconst currentImageId = inspectData.Image;\n\n// CRITICAL: Ensure image has a tag, otherwise Docker pulls ALL tags!\nif (imageName && !imageName.includes(':') && !imageName.includes('@')) {\n imageName = imageName + ':latest';\n}\n\n// Extract config for recreation\nconst containerConfig = inspectData.Config;\nconst hostConfig = inspectData.HostConfig;\nconst networkSettings = inspectData.NetworkSettings;\n\n// Get current version from image digest or tag\nconst currentDigest = currentImageId.substring(7, 19);\n\nreturn {\n json: {\n pullCmd: `curl -s --max-time 120 -X POST 'http://docker-socket-proxy:2375/v1.47/images/create?fromImage=${encodeURIComponent(imageName)}'`,\n containerId,\n containerName,\n fullContainerName,\n imageName,\n currentImageId,\n currentDigest,\n containerConfig,\n hostConfig,\n networkSettings,\n chatId,\n messageId\n }\n};"
|
|
},
|
|
"id": "code-parse-update-container-config",
|
|
"name": "Parse Update Container Config",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
3100,
|
|
1650
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"command": "={{ $json.pullCmd }}",
|
|
"options": {}
|
|
},
|
|
"id": "exec-pull-update-image",
|
|
"name": "Pull Update Image",
|
|
"type": "n8n-nodes-base.executeCommand",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
3320,
|
|
1650
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Check pull result and get new image ID\nconst stdout = $input.item.json.stdout || '';\nconst prevData = $('Parse Update Container Config').item.json;\nconst containerId = prevData.containerId;\nconst containerName = prevData.containerName;\nconst fullContainerName = prevData.fullContainerName;\nconst imageName = prevData.imageName;\nconst currentImageId = prevData.currentImageId;\nconst currentDigest = prevData.currentDigest;\nconst containerConfig = prevData.containerConfig;\nconst hostConfig = prevData.hostConfig;\nconst networkSettings = prevData.networkSettings;\nconst chatId = prevData.chatId;\nconst messageId = prevData.messageId;\n\n// Check if pull succeeded (contains status messages)\nconst pullSuccess = stdout.includes('Pulling') || stdout.includes('Downloaded') || stdout.includes('Status:') || stdout.includes('Digest:');\n\nif (!pullSuccess && stdout.includes('error')) {\n return {\n json: {\n error: true,\n chatId,\n messageId,\n containerName,\n text: `Failed to pull image for ${containerName}`\n }\n };\n}\n\nreturn {\n json: {\n inspectCmd: `curl -s --max-time 10 'http://docker-socket-proxy:2375/v1.47/images/${encodeURIComponent(imageName)}/json'`,\n containerId,\n containerName,\n fullContainerName,\n imageName,\n currentImageId,\n currentDigest,\n containerConfig,\n hostConfig,\n networkSettings,\n chatId,\n messageId\n }\n};"
|
|
},
|
|
"id": "code-check-pull-result",
|
|
"name": "Check Pull Result",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
3540,
|
|
1650
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"command": "={{ $json.inspectCmd }}",
|
|
"options": {}
|
|
},
|
|
"id": "exec-inspect-new-image",
|
|
"name": "Inspect New Image",
|
|
"type": "n8n-nodes-base.executeCommand",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
3760,
|
|
1650
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Compare image digests and decide if update needed\nconst stdout = $input.item.json.stdout || '';\nconst prevData = $('Check Pull Result').item.json;\nconst containerId = prevData.containerId;\nconst containerName = prevData.containerName;\nconst fullContainerName = prevData.fullContainerName;\nconst imageName = prevData.imageName;\nconst currentImageId = prevData.currentImageId;\nconst currentDigest = prevData.currentDigest;\nconst containerConfig = prevData.containerConfig;\nconst hostConfig = prevData.hostConfig;\nconst networkSettings = prevData.networkSettings;\nconst chatId = prevData.chatId;\nconst messageId = prevData.messageId;\n\nlet imageData;\ntry {\n imageData = JSON.parse(stdout);\n} catch (e) {\n return {\n json: {\n error: true,\n chatId,\n messageId,\n containerName,\n text: `Failed to inspect new image for ${containerName}`\n }\n };\n}\n\nconst newImageId = imageData.Id;\nconst newDigest = newImageId.substring(7, 19);\n\n// Check if image changed\nconst needsUpdate = newImageId !== currentImageId;\n\nif (!needsUpdate) {\n return {\n json: {\n needsUpdate: false,\n chatId,\n messageId,\n containerName,\n text: `<b>${containerName}</b> is already up to date.`\n }\n };\n}\n\n// Proceed with update\nreturn {\n json: {\n needsUpdate: true,\n stopCmd: `curl -s -o /dev/null -w \"%{http_code}\" --max-time 15 -X POST 'http://docker-socket-proxy:2375/v1.47/containers/${containerId}/stop?t=10'`,\n containerId,\n containerName,\n fullContainerName,\n imageName,\n currentImageId,\n newImageId,\n currentDigest,\n newDigest,\n containerConfig,\n hostConfig,\n networkSettings,\n chatId,\n messageId\n }\n};"
|
|
},
|
|
"id": "code-compare-update-images",
|
|
"name": "Compare Update Images",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
3980,
|
|
1650
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "strict"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "needs-update",
|
|
"leftValue": "={{ $json.needsUpdate }}",
|
|
"rightValue": true,
|
|
"operator": {
|
|
"type": "boolean",
|
|
"operation": "equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"options": {}
|
|
},
|
|
"id": "if-needs-update",
|
|
"name": "Check If Needs Update",
|
|
"type": "n8n-nodes-base.if",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
4200,
|
|
1650
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Container is already up to date - show completion with back button only\nconst data = $input.item.json;\nconst containerName = data.containerName;\nconst chatId = data.chatId;\nconst messageId = data.messageId;\n\n// Completion message shows only navigation button (removes action buttons)\nconst keyboard = {\n inline_keyboard: [\n [{ text: '\\u25C0\\uFE0F Back to Containers', callback_data: 'list:0' }]\n ]\n};\n\nconst text = `\\u2705 <b>${containerName}</b> already up to date\\n\\nNo changes needed.`;\n\nreturn {\n json: {\n chatId,\n messageId,\n text,\n reply_markup: keyboard\n }\n};"
|
|
},
|
|
"id": "code-format-no-update-needed",
|
|
"name": "Format No Update Needed",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
4420,
|
|
1750
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"method": "POST",
|
|
"url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/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-no-update-needed",
|
|
"name": "Send No Update Needed",
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.2,
|
|
"position": [
|
|
4640,
|
|
1750
|
|
],
|
|
"credentials": {
|
|
"telegramApi": {
|
|
"id": "I0xTTiASl7C1NZhJ",
|
|
"name": "Telegram account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"command": "={{ $json.stopCmd }}",
|
|
"options": {}
|
|
},
|
|
"id": "exec-stop-for-update",
|
|
"name": "Stop For Update",
|
|
"type": "n8n-nodes-base.executeCommand",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
4420,
|
|
1650
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Verify stop and build remove command\nconst stdout = $input.item.json.stdout;\nconst prevData = $('Compare Update Images').item.json;\nconst containerId = prevData.containerId;\nconst chatId = prevData.chatId;\nconst messageId = prevData.messageId;\nconst containerName = prevData.containerName;\n\nconst statusCode = parseInt(stdout.trim());\n\n// 204: stopped, 304: already stopped - both OK\nif (statusCode !== 204 && statusCode !== 304) {\n return {\n json: {\n error: true,\n chatId,\n messageId,\n containerName,\n text: `Failed to stop container: HTTP ${statusCode}`\n }\n };\n}\n\nreturn {\n json: {\n removeCmd: `curl -s -o /dev/null -w \"%{http_code}\" --max-time 5 -X DELETE 'http://docker-socket-proxy:2375/v1.47/containers/${containerId}'`,\n containerId,\n containerName: prevData.containerName,\n fullContainerName: prevData.fullContainerName,\n imageName: prevData.imageName,\n currentImageId: prevData.currentImageId,\n newImageId: prevData.newImageId,\n currentDigest: prevData.currentDigest,\n newDigest: prevData.newDigest,\n containerConfig: prevData.containerConfig,\n hostConfig: prevData.hostConfig,\n networkSettings: prevData.networkSettings,\n chatId,\n messageId\n }\n};"
|
|
},
|
|
"id": "code-verify-update-stop",
|
|
"name": "Verify Update Stop",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
4640,
|
|
1650
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"command": "={{ $json.removeCmd }}",
|
|
"options": {}
|
|
},
|
|
"id": "exec-remove-for-update",
|
|
"name": "Remove For Update",
|
|
"type": "n8n-nodes-base.executeCommand",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
4860,
|
|
1650
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Build container create request body\nconst prevData = $('Verify Update Stop').item.json;\nconst config = prevData.containerConfig;\nconst hostConfig = prevData.hostConfig;\nconst networkSettings = prevData.networkSettings;\nconst containerName = prevData.fullContainerName;\nconst chatId = prevData.chatId;\nconst messageId = prevData.messageId;\n\n// Build NetworkingConfig from NetworkSettings\nconst networks = {};\nfor (const [name, netConfig] of Object.entries(networkSettings.Networks || {})) {\n networks[name] = {\n IPAMConfig: netConfig.IPAMConfig,\n Links: netConfig.Links,\n Aliases: netConfig.Aliases\n };\n}\n\nconst createBody = {\n ...config,\n HostConfig: hostConfig,\n NetworkingConfig: {\n EndpointsConfig: networks\n }\n};\n\n// Remove fields that shouldn't be in create request\ndelete createBody.Hostname;\ndelete createBody.Domainname;\n\nreturn {\n json: {\n createBody: JSON.stringify(createBody),\n containerName,\n shortName: prevData.containerName,\n currentDigest: prevData.currentDigest,\n newDigest: prevData.newDigest,\n currentImageId: prevData.currentImageId,\n chatId,\n messageId\n }\n};"
|
|
},
|
|
"id": "code-build-update-create-body",
|
|
"name": "Build Update Create Body",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
5080,
|
|
1650
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Build create container command\nconst createBody = $json.createBody;\nconst containerName = $json.containerName;\nconst chatId = $json.chatId;\nconst messageId = $json.messageId;\n\nconst cmd = `curl -s -X POST --max-time 5 -H \"Content-Type: application/json\" -d '${createBody.replace(/'/g, \"'\\\\''\")}' 'http://docker-socket-proxy:2375/v1.47/containers/create?name=${encodeURIComponent(containerName)}'`;\n\nreturn {\n json: {\n createCmd: cmd,\n containerName,\n shortName: $json.shortName,\n currentDigest: $json.currentDigest,\n newDigest: $json.newDigest,\n currentImageId: $json.currentImageId,\n chatId,\n messageId\n }\n};"
|
|
},
|
|
"id": "code-build-update-create-cmd",
|
|
"name": "Build Update Create Command",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
5300,
|
|
1650
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"command": "={{ $json.createCmd }}",
|
|
"options": {}
|
|
},
|
|
"id": "exec-create-for-update",
|
|
"name": "Create For Update",
|
|
"type": "n8n-nodes-base.executeCommand",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
5520,
|
|
1650
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Parse create response and extract new container ID\nconst stdout = $input.item.json.stdout;\nconst prevData = $('Build Update Create Command').item.json;\nconst chatId = prevData.chatId;\nconst messageId = prevData.messageId;\nconst containerName = prevData.shortName;\n\nlet response;\ntry {\n response = JSON.parse(stdout);\n} catch (e) {\n return {\n json: {\n error: true,\n chatId,\n messageId,\n containerName,\n text: `Create failed: ${stdout}`\n }\n };\n}\n\nif (response.message) {\n return {\n json: {\n error: true,\n chatId,\n messageId,\n containerName,\n text: `Create failed: ${response.message}`\n }\n };\n}\n\nreturn {\n json: {\n startCmd: `curl -s -o /dev/null -w \"%{http_code}\" --max-time 5 -X POST 'http://docker-socket-proxy:2375/v1.47/containers/${response.Id}/start'`,\n newContainerId: response.Id,\n containerName,\n currentDigest: prevData.currentDigest,\n newDigest: prevData.newDigest,\n currentImageId: prevData.currentImageId,\n chatId,\n messageId\n }\n};"
|
|
},
|
|
"id": "code-parse-update-create-response",
|
|
"name": "Parse Update Create Response",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
5740,
|
|
1650
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"command": "={{ $json.startCmd }}",
|
|
"options": {}
|
|
},
|
|
"id": "exec-start-after-update",
|
|
"name": "Start After Update",
|
|
"type": "n8n-nodes-base.executeCommand",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
5960,
|
|
1650
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Build update completion message (success shows back only, error shows retry + back)\nconst stdout = $input.item.json.stdout;\nconst prevData = $('Parse Update Create Response').item.json;\nconst containerName = prevData.containerName;\nconst currentDigest = prevData.currentDigest;\nconst newDigest = prevData.newDigest;\nconst currentImageId = prevData.currentImageId;\nconst chatId = prevData.chatId;\nconst messageId = prevData.messageId;\n\nconst statusCode = parseInt(stdout.trim());\n\n// 204: started, 304: already running\nconst success = statusCode === 204 || statusCode === 304;\n\nlet text;\nlet keyboard;\n\nif (success) {\n text = `\\u2705 <b>${containerName}</b> updated successfully\\n\\n${currentDigest} \\u2192 ${newDigest}`;\n // Success: only back button\n keyboard = {\n inline_keyboard: [\n [{ text: '\\u25C0\\uFE0F Back to Containers', callback_data: 'list:0' }]\n ]\n };\n} else {\n text = `\\u274C Failed to start <b>${containerName}</b> after update`;\n // Error: retry and back buttons\n const timestamp = Math.floor(Date.now() / 1000);\n keyboard = {\n inline_keyboard: [\n [{ text: '\\u{1F504} Try Again', callback_data: `confirm:update:${containerName}:${timestamp}` }],\n [{ text: '\\u25C0\\uFE0F Back to Containers', callback_data: 'list:0' }]\n ]\n };\n}\n\nreturn {\n json: {\n chatId,\n messageId,\n text,\n reply_markup: keyboard,\n currentImageId\n }\n};"
|
|
},
|
|
"id": "code-format-update-complete",
|
|
"name": "Format Update Complete",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
6180,
|
|
1650
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"method": "POST",
|
|
"url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/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-complete",
|
|
"name": "Send Update Complete",
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.2,
|
|
"position": [
|
|
6400,
|
|
1650
|
|
],
|
|
"credentials": {
|
|
"telegramApi": {
|
|
"id": "I0xTTiASl7C1NZhJ",
|
|
"name": "Telegram account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Build remove old image command (fire and forget)\nconst currentImageId = $('Format Update Complete').item.json.currentImageId;\n\n// Skip if no image ID (error case)\nif (!currentImageId) {\n return { json: { cmd: 'true', skip: true } };\n}\n\n// Remove the old image - ignore errors (image might be used by another container)\nreturn {\n json: {\n cmd: `curl -s -o /dev/null -w \"%{http_code}\" --max-time 5 -X DELETE 'http://docker-socket-proxy:2375/v1.47/images/${currentImageId}?force=false'`,\n currentImageId\n }\n};"
|
|
},
|
|
"id": "code-build-callback-remove-image",
|
|
"name": "Build Callback Remove Image",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
6620,
|
|
1650
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"command": "={{ $json.cmd }}",
|
|
"options": {}
|
|
},
|
|
"id": "exec-callback-remove-old-image",
|
|
"name": "Callback Remove Old Image",
|
|
"type": "n8n-nodes-base.executeCommand",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
6840,
|
|
1650
|
|
]
|
|
},
|
|
{
|
|
"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-cancel-confirm-callback",
|
|
"name": "Answer Cancel Confirm Callback",
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.2,
|
|
"position": [
|
|
1340,
|
|
1800
|
|
],
|
|
"credentials": {
|
|
"telegramApi": {
|
|
"id": "I0xTTiASl7C1NZhJ",
|
|
"name": "Telegram account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Cancel confirmation - return to container submenu\nconst data = $(\"Parse Callback Data\").item.json;\nreturn {\n json: {\n containerName: data.containerName,\n chatId: data.chatId,\n messageId: data.messageId\n }\n};"
|
|
},
|
|
"id": "code-prepare-cancel-return",
|
|
"name": "Prepare Cancel Return",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
1560,
|
|
1800
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"method": "GET",
|
|
"url": "=http://docker-socket-proxy:2375/containers/json?all=true",
|
|
"options": {}
|
|
},
|
|
"id": "http-get-container-for-cancel",
|
|
"name": "Get Container For Cancel",
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.2,
|
|
"position": [
|
|
1780,
|
|
1800
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Build submenu for return from cancel\nconst containers = $input.all().map(item => item.json);\nconst prevData = $(\"Prepare Cancel Return\").item.json;\nconst searchName = prevData.containerName.toLowerCase();\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]) === searchName);\n\nif (!container) {\n return [{\n json: {\n chatId,\n messageId,\n text: `Container \\\"${searchName}\\\" not found`,\n reply_markup: { inline_keyboard: [[{ text: \"\\u25C0\\uFE0F Back to List\", callback_data: \"list:0\" }]] }\n }\n }];\n}\n\nconst containerName = normalizeName(container.Names[0]);\nconst state = container.State;\nconst status = container.Status;\nconst image = container.Image;\n\n// Build action keyboard based on container state\nconst keyboard = [];\n\nif (state === \"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} 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\n// Build status text\nconst stateIcon = state === \"running\" ? \"\\u{1F7E2}\" : \"\\u26AA\";\nlet text = `${stateIcon} <b>${containerName}</b>\\n\\n`;\ntext += `<b>State:</b> ${state}\\n`;\ntext += `<b>Status:</b> ${status}\\n`;\ntext += `<b>Image:</b> ${image}`;\n\nreturn [{\n json: {\n chatId,\n messageId,\n text,\n reply_markup: { inline_keyboard: keyboard }\n }\n}];"
|
|
},
|
|
"id": "code-build-cancel-return-submenu",
|
|
"name": "Build Cancel Return Submenu",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
2000,
|
|
1800
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"method": "POST",
|
|
"url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/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-cancel-return-submenu",
|
|
"name": "Send Cancel Return Submenu",
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.2,
|
|
"position": [
|
|
2220,
|
|
1800
|
|
],
|
|
"credentials": {
|
|
"telegramApi": {
|
|
"id": "I0xTTiASl7C1NZhJ",
|
|
"name": "Telegram account"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Detect if this is a batch command (multiple container names)\n// Batch commands: {action} {name1} {name2} ... where action is update/start/stop/restart\nconst message = $json.message;\nconst text = message.text.toLowerCase().trim();\nconst chatId = message.chat.id;\nconst messageId = message.message_id;\n\n// Determine action type from the routed keyword\n// This node receives from Keyword Router which already matched the action\nlet action = null;\nlet containerPart = '';\n\nif (text.startsWith('restart ')) {\n action = 'restart';\n containerPart = text.substring(8).trim();\n} else if (text.includes('restart')) {\n action = 'restart';\n containerPart = text.replace('restart', '').trim();\n} else if (text.startsWith('start ')) {\n action = 'start';\n containerPart = text.substring(6).trim();\n} else if (text.includes('start')) {\n action = 'start';\n containerPart = text.replace('start', '').trim();\n} else if (text.startsWith('stop ')) {\n action = 'stop';\n containerPart = text.substring(5).trim();\n} else if (text.includes('stop')) {\n action = 'stop';\n containerPart = text.replace('stop', '').trim();\n} else if (text.startsWith('update ')) {\n action = 'update';\n containerPart = text.substring(7).trim();\n} else if (text.includes('update')) {\n action = 'update';\n containerPart = text.replace('update', '').trim();\n}\n\nif (!action || !containerPart) {\n // No valid action found, pass through as non-batch (will be handled by existing flow)\n return {\n json: {\n isBatch: false,\n action: action,\n containerNames: [],\n originalMessage: text,\n chatId: chatId,\n messageId: messageId,\n message: message\n }\n };\n}\n\n// Split container names by whitespace\nconst containerNames = containerPart.split(/\\s+/).filter(name => name.length > 0);\n\n// Batch = 2 or more containers\nconst isBatch = containerNames.length >= 2;\n\nreturn {\n json: {\n isBatch: isBatch,\n action: action,\n containerNames: containerNames,\n originalMessage: text,\n chatId: chatId,\n messageId: messageId,\n message: message\n }\n};"
|
|
},
|
|
"id": "code-detect-batch",
|
|
"name": "Detect Batch Command",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
900,
|
|
-200
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "strict"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "is-batch-check",
|
|
"leftValue": "={{ $json.isBatch }}",
|
|
"rightValue": true,
|
|
"operator": {
|
|
"type": "boolean",
|
|
"operation": "equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"options": {}
|
|
},
|
|
"id": "if-is-batch",
|
|
"name": "Is Batch Command",
|
|
"type": "n8n-nodes-base.if",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
1120,
|
|
-200
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"rules": {
|
|
"values": [
|
|
{
|
|
"id": "action-start-stop-restart",
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"typeValidation": "loose"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "is-action-type",
|
|
"leftValue": "={{ $json.action }}",
|
|
"rightValue": "update",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "notEquals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"renameOutput": true,
|
|
"outputKey": "action"
|
|
},
|
|
{
|
|
"id": "action-update",
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"typeValidation": "loose"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "is-update-type",
|
|
"leftValue": "={{ $json.action }}",
|
|
"rightValue": "update",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"renameOutput": true,
|
|
"outputKey": "update"
|
|
}
|
|
]
|
|
},
|
|
"options": {
|
|
"fallbackOutput": "none"
|
|
}
|
|
},
|
|
"id": "switch-route-single-action",
|
|
"name": "Route Single Action",
|
|
"type": "n8n-nodes-base.switch",
|
|
"typeVersion": 3.2,
|
|
"position": [
|
|
1340,
|
|
-100
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"command": "curl -s --max-time 5 'http://docker-socket-proxy:2375/v1.47/containers/json?all=true'",
|
|
"options": {}
|
|
},
|
|
"id": "exec-docker-list-batch",
|
|
"name": "Get Containers for Batch",
|
|
"type": "n8n-nodes-base.executeCommand",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
1340,
|
|
-300
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Match batch container names with exact-match priority\nconst dockerOutput = $input.item.json.stdout;\nconst batchData = $('Detect Batch Command').item.json;\nconst containerNames = batchData.containerNames;\nconst action = batchData.action;\nconst chatId = batchData.chatId;\nconst messageId = batchData.messageId;\n\n// Parse Docker container list\nlet containers;\ntry {\n if (!dockerOutput || dockerOutput.trim() === '') {\n throw new Error('Empty response');\n }\n containers = JSON.parse(dockerOutput);\n} catch (e) {\n return [{\n json: {\n error: true,\n errorMessage: 'Cannot connect to Docker',\n chatId: chatId\n }\n }];\n}\n\n// Function to normalize container names\nfunction normalizeName(name) {\n return name\n .replace(/^\\//, '') // Remove leading slash\n .replace(/^(linuxserver[-_]|binhex[-_])/i, '') // Remove common prefixes\n .toLowerCase();\n}\n\n// Match results\nconst allMatched = []; // Containers that matched exactly\nconst needsDisambiguation = []; // { input, matches: [...] }\nconst notFound = []; // Container names with no matches\n\nfor (const inputName of containerNames) {\n const normalized = inputName.toLowerCase().trim();\n \n // 1. Check for exact match first (highest priority)\n const exactMatch = containers.find(c => {\n const containerName = normalizeName(c.Names[0]);\n return containerName === normalized;\n });\n \n if (exactMatch) {\n // Exact match wins - no disambiguation needed\n allMatched.push({\n Id: exactMatch.Id,\n Name: exactMatch.Names[0].replace(/^\\//, ''),\n State: exactMatch.State,\n Image: exactMatch.Image,\n inputName: inputName,\n matchType: 'exact'\n });\n continue;\n }\n \n // 2. Check for substring matches (fuzzy)\n const fuzzyMatches = containers.filter(c => {\n const containerName = normalizeName(c.Names[0]);\n return containerName.includes(normalized);\n });\n \n if (fuzzyMatches.length === 0) {\n // No matches at all\n notFound.push(inputName);\n } else if (fuzzyMatches.length === 1) {\n // Single fuzzy match - treat as found\n const match = fuzzyMatches[0];\n allMatched.push({\n Id: match.Id,\n Name: match.Names[0].replace(/^\\//, ''),\n State: match.State,\n Image: match.Image,\n inputName: inputName,\n matchType: 'fuzzy'\n });\n } else {\n // Multiple fuzzy matches - needs disambiguation\n needsDisambiguation.push({\n input: inputName,\n matches: fuzzyMatches.map(c => ({\n Id: c.Id,\n Name: c.Names[0].replace(/^\\//, ''),\n State: c.State\n }))\n });\n }\n}\n\nreturn [{\n json: {\n allMatched: allMatched,\n needsDisambiguation: needsDisambiguation,\n notFound: notFound,\n hasDisambiguation: needsDisambiguation.length > 0,\n hasNotFound: notFound.length > 0,\n action: action,\n chatId: chatId,\n messageId: messageId,\n originalContainerNames: containerNames,\n allContainers: containers\n }\n}];"
|
|
},
|
|
"id": "code-match-batch-containers",
|
|
"name": "Match Batch Containers",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
1560,
|
|
-300
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "strict"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "has-disambiguation",
|
|
"leftValue": "={{ $json.hasDisambiguation }}",
|
|
"rightValue": true,
|
|
"operator": {
|
|
"type": "boolean",
|
|
"operation": "equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"options": {}
|
|
},
|
|
"id": "if-needs-disambiguation",
|
|
"name": "Needs Disambiguation",
|
|
"type": "n8n-nodes-base.if",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
1780,
|
|
-300
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Build disambiguation message with inline keyboard\nconst data = $json;\nconst disambig = data.needsDisambiguation;\nconst action = data.action;\nconst chatId = data.chatId;\n\n// Build message text\nlet text = '<b>Multiple matches found:</b>\\n\\n';\n\nfor (const item of disambig) {\n const matchNames = item.matches.map(m => m.Name).join(', ');\n text += `\\u2022 '<b>${item.input}</b>' matches: ${matchNames}\\n`;\n}\n\ntext += '\\nPlease be more specific or select from options below.';\n\n// Build inline keyboard with exact container names\n// Each row has one disambiguation option\nconst keyboard = [];\n\nfor (const item of disambig) {\n // Create a row for each ambiguous input\n const row = [];\n for (const match of item.matches.slice(0, 3)) { // Max 3 options per row\n // Callback format: batch:{action}:{containerName}\n // This will be handled by callback router to add to batch\n row.push({\n text: match.Name,\n callback_data: `bselect:${action}:${match.Name}`\n });\n }\n keyboard.push(row);\n}\n\n// Add cancel button\nkeyboard.push([{ text: 'Cancel', callback_data: '{\"a\":\"x\"}' }]);\n\nreturn {\n json: {\n chat_id: chatId,\n text: text,\n parse_mode: 'HTML',\n reply_markup: {\n inline_keyboard: keyboard\n }\n }\n};"
|
|
},
|
|
"id": "code-build-disambiguation",
|
|
"name": "Build Disambiguation Message",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
2000,
|
|
-400
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"method": "POST",
|
|
"url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/sendMessage",
|
|
"sendBody": true,
|
|
"specifyBody": "json",
|
|
"jsonBody": "={{ JSON.stringify($json) }}",
|
|
"options": {}
|
|
},
|
|
"id": "http-send-disambiguation",
|
|
"name": "Send Disambiguation",
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.2,
|
|
"position": [
|
|
2220,
|
|
-400
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "strict"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "has-not-found",
|
|
"leftValue": "={{ $json.hasNotFound }}",
|
|
"rightValue": true,
|
|
"operator": {
|
|
"type": "boolean",
|
|
"operation": "equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"options": {}
|
|
},
|
|
"id": "if-has-not-found",
|
|
"name": "Has Not Found",
|
|
"type": "n8n-nodes-base.if",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
2000,
|
|
-200
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Build not found message\nconst data = $json;\nconst notFound = data.notFound;\nconst allMatched = data.allMatched;\nconst action = data.action;\nconst chatId = data.chatId;\n\n// Build message\nlet text = '';\n\nif (notFound.length > 0) {\n text += '<b>Container(s) not found:</b>\\n';\n for (const name of notFound) {\n text += `\\u2022 ${name}\\n`;\n }\n text += '\\n';\n}\n\nif (allMatched.length > 0) {\n text += `<b>Found ${allMatched.length} container(s):</b>\\n`;\n for (const c of allMatched) {\n text += `\\u2022 ${c.Name}\\n`;\n }\n text += `\\nProceed with ${action} on found containers?`;\n \n // Build confirmation keyboard\n const names = allMatched.map(c => c.Name).join(',');\n const timestamp = Math.floor(Date.now() / 1000);\n \n return {\n json: {\n chat_id: chatId,\n text: text,\n parse_mode: 'HTML',\n reply_markup: {\n inline_keyboard: [\n [\n { text: `Yes, ${action} ${allMatched.length} containers`, callback_data: `bexec:${action}:${names}:${timestamp}` },\n { text: 'Cancel', callback_data: '{\"a\":\"x\"}' }\n ]\n ]\n },\n hasMatched: true,\n matchedContainers: allMatched\n }\n };\n} else {\n // No containers matched at all\n return {\n json: {\n chat_id: chatId,\n text: text + 'No valid containers to ' + action + '.',\n parse_mode: 'HTML',\n hasMatched: false\n }\n };\n}"
|
|
},
|
|
"id": "code-build-not-found",
|
|
"name": "Build Not Found Message",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
2220,
|
|
-100
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"method": "POST",
|
|
"url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/sendMessage",
|
|
"sendBody": true,
|
|
"specifyBody": "json",
|
|
"jsonBody": "={{ JSON.stringify({ chat_id: $json.chat_id, text: $json.text, parse_mode: $json.parse_mode, reply_markup: $json.reply_markup }) }}",
|
|
"options": {}
|
|
},
|
|
"id": "http-send-not-found",
|
|
"name": "Send Not Found Message",
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.2,
|
|
"position": [
|
|
2440,
|
|
-100
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"rules": {
|
|
"values": [
|
|
{
|
|
"id": "batch-update",
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"typeValidation": "loose"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "is-update",
|
|
"leftValue": "={{ $json.action }}",
|
|
"rightValue": "update",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"renameOutput": true,
|
|
"outputKey": "update"
|
|
},
|
|
{
|
|
"id": "batch-start",
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"typeValidation": "loose"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "is-start",
|
|
"leftValue": "={{ $json.action }}",
|
|
"rightValue": "start",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"renameOutput": true,
|
|
"outputKey": "start"
|
|
},
|
|
{
|
|
"id": "batch-restart",
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"typeValidation": "loose"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "is-restart",
|
|
"leftValue": "={{ $json.action }}",
|
|
"rightValue": "restart",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"renameOutput": true,
|
|
"outputKey": "restart"
|
|
},
|
|
{
|
|
"id": "batch-stop",
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"typeValidation": "loose"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "is-stop",
|
|
"leftValue": "={{ $json.action }}",
|
|
"rightValue": "stop",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"renameOutput": true,
|
|
"outputKey": "stop"
|
|
}
|
|
]
|
|
},
|
|
"options": {
|
|
"fallbackOutput": "none"
|
|
}
|
|
},
|
|
"id": "switch-route-batch-action",
|
|
"name": "Route Batch Action",
|
|
"type": "n8n-nodes-base.switch",
|
|
"typeVersion": 3.2,
|
|
"position": [
|
|
2220,
|
|
-300
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Build batch stop confirmation message\n// Per context: Batch stop confirms due to fuzzy matching risk\nconst data = $json;\nconst allMatched = data.allMatched;\nconst chatId = data.chatId;\n\nconst count = allMatched.length;\nconst names = allMatched.map(c => c.Name);\nconst namesStr = names.join(',');\nconst timestamp = Math.floor(Date.now() / 1000);\n\n// Build confirmation message\nlet text = `<b>Stop ${count} container${count > 1 ? 's' : ''}?</b>\\n\\n`;\nfor (const name of names) {\n text += `\\u2022 ${name}\\n`;\n}\n\n// Callback format: bstop:confirm:{comma-separated-names}:{timestamp}\nconst confirmCallback = `bstop:confirm:${namesStr}:${timestamp}`;\nconst cancelCallback = 'bstop:cancel';\n\nreturn {\n json: {\n chat_id: chatId,\n text: text,\n parse_mode: 'HTML',\n reply_markup: {\n inline_keyboard: [\n [\n { text: 'Confirm', callback_data: confirmCallback },\n { text: 'Cancel', callback_data: cancelCallback }\n ]\n ]\n }\n }\n};"
|
|
},
|
|
"id": "code-build-batch-stop-confirm",
|
|
"name": "Build Batch Stop Confirmation",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
2440,
|
|
-200
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"method": "POST",
|
|
"url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/sendMessage",
|
|
"sendBody": true,
|
|
"specifyBody": "json",
|
|
"jsonBody": "={{ JSON.stringify($json) }}",
|
|
"options": {}
|
|
},
|
|
"id": "http-send-batch-stop-confirm",
|
|
"name": "Send Batch Stop Confirmation",
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.2,
|
|
"position": [
|
|
2660,
|
|
-200
|
|
]
|
|
},
|
|
{
|
|
"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-stop-confirm",
|
|
"name": "Answer Batch Stop Confirm",
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.2,
|
|
"position": [
|
|
1340,
|
|
700
|
|
]
|
|
},
|
|
{
|
|
"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, text: 'Cancelled' }) }}",
|
|
"options": {}
|
|
},
|
|
"id": "http-answer-batch-stop-cancel",
|
|
"name": "Answer Batch Stop Cancel",
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.2,
|
|
"position": [
|
|
1340,
|
|
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-exec",
|
|
"name": "Answer Batch Exec",
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.2,
|
|
"position": [
|
|
1340,
|
|
900
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"method": "POST",
|
|
"url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/deleteMessage",
|
|
"sendBody": true,
|
|
"specifyBody": "json",
|
|
"jsonBody": "={{ JSON.stringify({ chat_id: $json.chatId, message_id: $json.messageId }) }}",
|
|
"options": {}
|
|
},
|
|
"id": "http-delete-batch-stop-cancel",
|
|
"name": "Delete Batch Stop Cancel Message",
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.2,
|
|
"position": [
|
|
1560,
|
|
800
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "strict"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "batch-stop-expired",
|
|
"leftValue": "={{ $json.expired }}",
|
|
"rightValue": true,
|
|
"operator": {
|
|
"type": "boolean",
|
|
"operation": "equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"options": {}
|
|
},
|
|
"id": "if-batch-stop-expired",
|
|
"name": "Check Batch Stop Expired",
|
|
"type": "n8n-nodes-base.if",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
1560,
|
|
700
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Handle expired batch stop confirmation\nconst data = $json;\nreturn {\n json: {\n chat_id: data.chatId,\n message_id: data.messageId,\n text: 'Confirmation expired. Please try again.',\n parse_mode: 'HTML'\n }\n};"
|
|
},
|
|
"id": "code-batch-stop-expired",
|
|
"name": "Build Batch Stop Expired",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
1780,
|
|
600
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"method": "POST",
|
|
"url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/editMessageText",
|
|
"sendBody": true,
|
|
"specifyBody": "json",
|
|
"jsonBody": "={{ JSON.stringify($json) }}",
|
|
"options": {}
|
|
},
|
|
"id": "http-send-batch-stop-expired",
|
|
"name": "Send Batch Stop Expired",
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.2,
|
|
"position": [
|
|
2000,
|
|
600
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Initialize batch state for execution\n// Input comes from Route Batch Action OR batch callbacks (bexec, bstop confirmed)\nconst data = $json;\n\n// Handle different input sources\nlet containers, action, chatId, messageId;\n\nif (data.allMatched) {\n // From Route Batch Action (direct batch command)\n containers = data.allMatched;\n action = data.action;\n chatId = data.chatId;\n messageId = data.messageId || null;\n} else if (data.containerNames) {\n // From batch callback (bexec or bstop:confirm)\n // Need to resolve names to container objects\n containers = data.containerNames.map(name => ({ Name: name, Id: null }));\n action = data.batchAction || 'stop';\n chatId = data.chatId;\n messageId = data.messageId;\n} else {\n throw new Error('Invalid batch state input');\n}\n\nreturn {\n json: {\n containers: containers,\n action: action,\n totalCount: containers.length,\n successCount: 0,\n failureCount: 0,\n warningCount: 0,\n results: [],\n chatId: chatId,\n messageId: messageId,\n currentIndex: 0,\n progressMessageId: null\n }\n};"
|
|
},
|
|
"id": "code-init-batch-state",
|
|
"name": "Initialize Batch State",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
2440,
|
|
-500
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"method": "POST",
|
|
"url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/sendMessage",
|
|
"sendBody": true,
|
|
"specifyBody": "json",
|
|
"jsonBody": "={{ JSON.stringify({ chat_id: $json.chatId, text: '<b>Batch ' + $json.action + '</b>\\n\\nStarting ' + $json.action + ' for ' + $json.totalCount + ' containers...', parse_mode: 'HTML' }) }}",
|
|
"options": {}
|
|
},
|
|
"id": "http-send-batch-start",
|
|
"name": "Send Batch Start Message",
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.2,
|
|
"position": [
|
|
2660,
|
|
-500
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Store progress message ID and prepare containers for loop\nconst batchState = $('Initialize Batch State').item.json;\nconst response = $json;\n\n// Get message_id from Telegram response\nconst progressMessageId = response.result?.message_id || null;\n\n// Return array of items for the loop - each container as separate item\nconst items = batchState.containers.map((container, index) => ({\n json: {\n container: container,\n containerIndex: index,\n action: batchState.action,\n totalCount: batchState.totalCount,\n chatId: batchState.chatId,\n progressMessageId: progressMessageId,\n // Running totals - will be updated as loop progresses\n successCount: 0,\n failureCount: 0,\n warningCount: 0,\n results: []\n }\n}));\n\nreturn items;"
|
|
},
|
|
"id": "code-prepare-batch-loop",
|
|
"name": "Prepare Batch Loop",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
2880,
|
|
-500
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"options": {
|
|
"reset": false
|
|
}
|
|
},
|
|
"id": "loop-batch",
|
|
"name": "Batch Loop",
|
|
"type": "n8n-nodes-base.splitInBatches",
|
|
"typeVersion": 3,
|
|
"position": [
|
|
3100,
|
|
-500
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Build progress message for current container\nconst data = $json;\nconst container = data.container;\nconst action = data.action;\nconst containerIndex = data.containerIndex;\nconst totalCount = data.totalCount;\nconst successCount = data.successCount || 0;\nconst failureCount = data.failureCount || 0;\n\nconst containerName = container.Name || container;\nconst current = containerIndex + 1;\n\nlet progressText = `<b>Batch ${action} in progress...</b>\\n\\n`;\nprogressText += `Current: <b>${containerName}</b>\\n`;\nprogressText += `Progress: ${current}/${totalCount}\\n\\n`;\n\nif (successCount > 0 || failureCount > 0) {\n progressText += `Completed: ${successCount} success`;\n if (failureCount > 0) {\n progressText += `, ${failureCount} failed`;\n }\n}\n\nreturn {\n json: {\n ...data,\n progressText: progressText,\n containerName: containerName\n }\n};"
|
|
},
|
|
"id": "code-build-progress",
|
|
"name": "Build Progress Message",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
3320,
|
|
-500
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"method": "POST",
|
|
"url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/editMessageText",
|
|
"sendBody": true,
|
|
"specifyBody": "json",
|
|
"jsonBody": "={{ JSON.stringify({ chat_id: $json.chatId, message_id: $json.progressMessageId, text: $json.progressText, parse_mode: 'HTML' }) }}",
|
|
"options": {
|
|
"response": {
|
|
"response": {
|
|
"neverError": true
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"id": "http-edit-progress",
|
|
"name": "Edit Progress Message",
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.2,
|
|
"position": [
|
|
3540,
|
|
-500
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"rules": {
|
|
"values": [
|
|
{
|
|
"id": "loop-update",
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"typeValidation": "loose"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "is-update",
|
|
"leftValue": "={{ $json.action }}",
|
|
"rightValue": "update",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"renameOutput": true,
|
|
"outputKey": "update"
|
|
},
|
|
{
|
|
"id": "loop-start",
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"typeValidation": "loose"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "is-start",
|
|
"leftValue": "={{ $json.action }}",
|
|
"rightValue": "start",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"renameOutput": true,
|
|
"outputKey": "start"
|
|
},
|
|
{
|
|
"id": "loop-stop",
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"typeValidation": "loose"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "is-stop",
|
|
"leftValue": "={{ $json.action }}",
|
|
"rightValue": "stop",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"renameOutput": true,
|
|
"outputKey": "stop"
|
|
},
|
|
{
|
|
"id": "loop-restart",
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"typeValidation": "loose"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "is-restart",
|
|
"leftValue": "={{ $json.action }}",
|
|
"rightValue": "restart",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"renameOutput": true,
|
|
"outputKey": "restart"
|
|
}
|
|
]
|
|
},
|
|
"options": {
|
|
"fallbackOutput": "none"
|
|
}
|
|
},
|
|
"id": "switch-route-batch-loop-action",
|
|
"name": "Route Batch Loop Action",
|
|
"type": "n8n-nodes-base.switch",
|
|
"typeVersion": 3.2,
|
|
"position": [
|
|
3760,
|
|
-500
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Get container ID for the action\nconst data = $json;\nconst container = data.container;\nconst action = data.action;\n\n// Container might have Id directly or just Name\nconst containerId = container.Id || null;\nconst containerName = container.Name || container;\n\n// Build the curl command for start/stop/restart\nconst timeout = (action === 'stop' || action === 'restart') ? '?t=10' : '';\n\nlet cmd;\nif (containerId) {\n cmd = `curl -s -o /dev/null -w \"%{http_code}\" --max-time 30 -X POST 'http://docker-socket-proxy:2375/v1.47/containers/${containerId}/${action}${timeout}'`;\n} else {\n // Need to find container by name first - use filters\n cmd = `curl -s --max-time 5 'http://docker-socket-proxy:2375/v1.47/containers/json?all=true&filters=%7B%22name%22%3A%5B%22${containerName}%22%5D%7D'`;\n}\n\nreturn {\n json: {\n ...data,\n cmd: cmd,\n needsLookup: !containerId,\n containerId: containerId\n }\n};"
|
|
},
|
|
"id": "code-build-batch-action-cmd",
|
|
"name": "Build Batch Action Command",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
3980,
|
|
-400
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"command": "={{ $json.cmd }}",
|
|
"options": {}
|
|
},
|
|
"id": "exec-batch-action",
|
|
"name": "Execute Batch Container Action",
|
|
"type": "n8n-nodes-base.executeCommand",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
4200,
|
|
-400
|
|
],
|
|
"onError": "continueRegularOutput"
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Parse action result and check if we need a second call\nconst data = $('Build Batch Action Command').item.json;\nconst stdout = $json.stdout || '';\nconst stderr = $json.stderr || '';\n\nif (data.needsLookup) {\n // First call was a lookup - parse result and make action call\n let containers;\n try {\n containers = JSON.parse(stdout);\n } catch (e) {\n return {\n json: {\n ...data,\n needsAction: false,\n status: 'error',\n reason: 'Container lookup failed'\n }\n };\n }\n \n if (!containers || containers.length === 0) {\n return {\n json: {\n ...data,\n needsAction: false,\n status: 'error',\n reason: 'Container not found'\n }\n };\n }\n \n const containerId = containers[0].Id;\n const action = data.action;\n const timeout = (action === 'stop' || action === 'restart') ? '?t=10' : '';\n const cmd = `curl -s -o /dev/null -w \"%{http_code}\" --max-time 30 -X POST 'http://docker-socket-proxy:2375/v1.47/containers/${containerId}/${action}${timeout}'`;\n \n return {\n json: {\n ...data,\n needsAction: true,\n containerId: containerId,\n cmd: cmd\n }\n };\n}\n\n// Direct action result\nconst statusCode = parseInt(stdout.trim());\nlet status, reason;\n\nif (statusCode === 204) {\n status = 'success';\n reason = null;\n} else if (statusCode === 304) {\n // Already in desired state\n status = 'warning';\n const action = data.action;\n reason = action === 'start' ? 'Already running' : \n action === 'stop' ? 'Already stopped' : 'No change needed';\n} else if (stderr || statusCode >= 400) {\n status = 'error';\n reason = stderr || `HTTP ${statusCode}`;\n} else {\n status = 'error';\n reason = 'Unknown error';\n}\n\nreturn {\n json: {\n ...data,\n needsAction: false,\n status: status,\n reason: reason\n }\n};"
|
|
},
|
|
"id": "code-check-batch-action-result",
|
|
"name": "Check Batch Action Result",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
4420,
|
|
-400
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "strict"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "needs-action",
|
|
"leftValue": "={{ $json.needsAction }}",
|
|
"rightValue": true,
|
|
"operator": {
|
|
"type": "boolean",
|
|
"operation": "equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"options": {}
|
|
},
|
|
"id": "if-needs-batch-action",
|
|
"name": "Needs Action Call",
|
|
"type": "n8n-nodes-base.if",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
4640,
|
|
-400
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"command": "={{ $json.cmd }}",
|
|
"options": {}
|
|
},
|
|
"id": "exec-batch-action-2",
|
|
"name": "Execute Batch Action 2",
|
|
"type": "n8n-nodes-base.executeCommand",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
4860,
|
|
-500
|
|
],
|
|
"onError": "continueRegularOutput"
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Parse the final action result\nconst data = $('Check Batch Action Result').item.json;\nconst stdout = $json.stdout || '';\nconst stderr = $json.stderr || '';\n\nconst statusCode = parseInt(stdout.trim());\nlet status, reason;\n\nif (statusCode === 204) {\n status = 'success';\n reason = null;\n} else if (statusCode === 304) {\n status = 'warning';\n const action = data.action;\n reason = action === 'start' ? 'Already running' : \n action === 'stop' ? 'Already stopped' : 'No change needed';\n} else if (stderr || statusCode >= 400) {\n status = 'error';\n reason = stderr || `HTTP ${statusCode}`;\n} else {\n status = 'error';\n reason = 'Unknown error';\n}\n\nreturn {\n json: {\n ...data,\n status: status,\n reason: reason\n }\n};"
|
|
},
|
|
"id": "code-parse-batch-action-2",
|
|
"name": "Parse Batch Action 2",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
5080,
|
|
-500
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Merge both paths and update running totals\n// This node receives from either direct result or second action call\nconst data = $json;\n\nconst status = data.status;\nconst containerName = data.containerName;\nconst reason = data.reason;\nconst containerIndex = data.containerIndex;\n\n// Get previous results from workflow context or start fresh\nlet results = data.results || [];\nlet successCount = data.successCount || 0;\nlet failureCount = data.failureCount || 0;\nlet warningCount = data.warningCount || 0;\n\n// Add current result\nresults.push({\n name: containerName,\n status: status,\n reason: reason\n});\n\n// Update counters\nif (status === 'success') {\n successCount++;\n} else if (status === 'error') {\n failureCount++;\n} else if (status === 'warning') {\n warningCount++;\n}\n\nreturn {\n json: {\n ...data,\n results: results,\n successCount: successCount,\n failureCount: failureCount,\n warningCount: warningCount\n }\n};"
|
|
},
|
|
"id": "code-handle-batch-result",
|
|
"name": "Handle Action Result",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
5300,
|
|
-400
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Prepare data for next iteration or completion\nconst data = $json;\n\n// Check if more containers to process\nconst containerIndex = data.containerIndex;\nconst totalCount = data.totalCount;\nconst isLastContainer = (containerIndex + 1) >= totalCount;\n\nreturn {\n json: {\n ...data,\n isComplete: isLastContainer\n }\n};"
|
|
},
|
|
"id": "code-prepare-next-iteration",
|
|
"name": "Prepare Next Iteration",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
5520,
|
|
-400
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Prepare batch stop data for execution\n// Input from Check Batch Stop Expired (not expired)\nconst data = $json;\n\n// containerNames is a comma-separated array from callback\nconst containerNames = data.containerNames || [];\n\nreturn {\n json: {\n allMatched: containerNames.map(name => ({ Name: name, Id: null })),\n action: 'stop',\n chatId: data.chatId,\n messageId: data.messageId\n }\n};"
|
|
},
|
|
"id": "code-prepare-batch-stop-exec",
|
|
"name": "Prepare Batch Stop Exec",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
1780,
|
|
700
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Prepare batch exec data for execution\n// Input from bexec callback (start/restart/update)\nconst data = $json;\n\n// containerNames is a comma-separated array from callback\nconst containerNames = data.containerNames || [];\nconst action = data.batchAction || 'start';\n\nreturn {\n json: {\n allMatched: containerNames.map(name => ({ Name: name, Id: null })),\n action: action,\n chatId: data.chatId,\n messageId: data.messageId\n }\n};"
|
|
},
|
|
"id": "code-prepare-batch-exec",
|
|
"name": "Prepare Batch Exec",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
1560,
|
|
900
|
|
]
|
|
}
|
|
],
|
|
"connections": {
|
|
"Telegram Trigger": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Route Update Type",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Route Update Type": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "IF User Authenticated",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "IF Callback Authenticated",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"IF User Authenticated": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Keyword Router",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[]
|
|
]
|
|
},
|
|
"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 Batch Commands",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Answer Select Callback",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Answer List Callback",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Answer Action Callback",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Answer Noop Callback",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Answer Confirm Callback",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Answer Cancel Confirm Callback",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Build Callback Action",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Answer Batch Stop Confirm",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Answer Batch Stop Cancel",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Answer Batch Exec",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Answer Select Callback": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Prepare Container Fetch",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Prepare Container Fetch": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Get Single Container",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Get Single Container": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Build Container Submenu",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Build Container Submenu": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Send Container Submenu",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Answer List Callback": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Prepare List Fetch",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Prepare List Fetch": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Get Containers For List",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Get Containers For List": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Build Paginated List",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Build Paginated List": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Edit Container List",
|
|
"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
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Docker List Containers": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Build Container List Keyboard",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Build Container List Keyboard": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Check Single Container",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Check Single Container": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Build Container Submenu Direct",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Send Container List",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Build Container Submenu Direct": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Send Container Submenu Direct",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Docker List for Action": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Match Container",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Match Container": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Check Match Count",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Check Match Count": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Send Docker Error",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Find Closest Match",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Build Action Command",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Build Batch Keyboard",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[]
|
|
]
|
|
},
|
|
"Find Closest Match": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Check Suggestion",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Check Suggestion": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Build Suggestion Keyboard",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Send No Match",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Build Suggestion Keyboard": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Send Suggestion",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Build Action Command": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Execute Action",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Execute Action": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Parse Action Result",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Parse Action Result": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Send Action Result",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Build Batch Keyboard": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Send Batch Confirmation",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"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": [
|
|
[
|
|
{
|
|
"node": "Docker List for Update",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Docker List for Update": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Match Update Container",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Match Update Container": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Check Update Match Count",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Check Update Match Count": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Send Update Error",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Send Update No Match",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Send Update Started",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Handle Update Multiple",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[]
|
|
]
|
|
},
|
|
"Handle Update Multiple": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Send Update Multiple",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Send Update Started": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Build Inspect Command",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Build Inspect Command": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Inspect Container",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Inspect Container": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Parse Container Config",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Parse Container Config": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Build Pull Command",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Build Pull Command": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Pull Image",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Pull Image": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Check Pull Response",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Check Pull Response": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Check Pull Success",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Check Pull Success": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Build Image Inspect",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Send Pull Error",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Build Image Inspect": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Inspect New Image (Text)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Inspect New Image (Text)": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Compare Digests",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Compare Digests": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Check If Update Needed",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Inspect New Image": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Compare Update Images",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Check If Update Needed": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Build Stop Command",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Format No Update",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Format No Update": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Send No Update",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Build Stop Command": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Stop Container",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Stop Container": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Verify Stop Build Remove",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Verify Stop Build Remove": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Remove Container",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Remove Container": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Build Create Body",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Build Create Body": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Build Create Command",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Build Create Command": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Create Container",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Create Container": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Parse Create Response",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Parse Create Response": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Build Start Command",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Build Start Command": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Start New Container",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Start New Container": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Format Update Result",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Format Update Result": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Send Update Result",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Send Update Result": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Build Remove Image Command",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Build Remove Image Command": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Remove Old Image",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Parse Logs Command": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Docker List for Logs",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Docker List for Logs": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Match Logs Container",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Match Logs Container": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Check Logs Match Count",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Check Logs Match Count": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Send Logs Error",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Format Logs No Match",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Build Logs Command",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Format Logs Multiple",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Build Logs Command": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Execute Logs",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Execute Logs": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Format Logs",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Format Logs": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Send Logs Response",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Format Logs No Match": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Send Logs Error",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Format Logs Multiple": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Send Logs Error",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Keyword Router": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Show Menu",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Docker List Containers",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Detect Batch Command",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Detect Batch Command",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Detect Batch Command",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Detect Batch Command",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Parse Logs Command",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Show Menu",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Detect Batch Command": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Is Batch Command",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Is Batch Command": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Get Containers for Batch",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Route Single Action",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Route Single Action": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Parse Action Command",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Parse Update Command",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Get Containers for Batch": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Match Batch Containers",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Match Batch Containers": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Needs Disambiguation",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Needs Disambiguation": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Build Disambiguation Message",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Has Not Found",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Build Disambiguation Message": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Send Disambiguation",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Has Not Found": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Build Not Found Message",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Route Batch Action",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Build Not Found Message": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Send Not Found Message",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Route Batch Action": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Initialize Batch State",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Initialize Batch State",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Initialize Batch State",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Build Batch Stop Confirmation",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Build Batch Stop Confirmation": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Send Batch Stop Confirmation",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Answer Batch Stop Confirm": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Check Batch Stop Expired",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Check Batch Stop Expired": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Build Batch Stop Expired",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Prepare Batch Stop Exec",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Build Batch Stop Expired": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Send Batch Stop Expired",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Answer Batch Stop Cancel": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Delete Batch Stop Cancel Message",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Parse Action Command": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Docker List for Action",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"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
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Answer Confirm Callback": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Check Confirm Expired",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Check Confirm Expired": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Handle Confirm Expired",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Route Confirm Action",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Handle Confirm Expired": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Send Expired Confirm",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Route Confirm Action": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Prepare Confirmed Stop",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Prepare Confirmed Update",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Prepare Confirmed Stop": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Get Container For Stop",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Get Container For Stop": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Build Confirmed Stop Command",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Build Confirmed Stop Command": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Execute Confirmed Stop",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Execute Confirmed Stop": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Format Confirmed Stop Result",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Format Confirmed Stop Result": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Send Confirmed Stop Result",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Prepare Confirmed Update": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Show Update Progress",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Show Update Progress": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Get Container For Update",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Get Container For Update": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Find Container For Update",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Find Container For Update": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Inspect Container For Update",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Inspect Container For Update": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Parse Update Container Config",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Parse Update Container Config": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Pull Update Image",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Pull Update Image": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Check Pull Result",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Check Pull Result": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Inspect New Image",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Compare Update Images": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Check If Needs Update",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Check If Needs Update": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Stop For Update",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Format No Update Needed",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Format No Update Needed": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Send No Update Needed",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Stop For Update": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Verify Update Stop",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Verify Update Stop": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Remove For Update",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Remove For Update": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Build Update Create Body",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Build Update Create Body": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Build Update Create Command",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Build Update Create Command": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Create For Update",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Create For Update": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Parse Update Create Response",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Parse Update Create Response": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Start After Update",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Start After Update": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Format Update Complete",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Format Update Complete": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Send Update Complete",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Send Update Complete": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Build Callback Remove Image",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Build Callback Remove Image": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Callback Remove Old Image",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Answer Cancel Confirm Callback": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Prepare Cancel Return",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Prepare Cancel Return": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Get Container For Cancel",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Get Container For Cancel": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Build Cancel Return Submenu",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Build Cancel Return Submenu": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Send Cancel Return Submenu",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Initialize Batch State": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Send Batch Start Message",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Send Batch Start Message": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Prepare Batch Loop",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Prepare Batch Loop": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Batch Loop",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Batch Loop": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Build Progress Message",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Build Progress Message": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Edit Progress Message",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Edit Progress Message": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Route Batch Loop Action",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Route Batch Loop Action": {
|
|
"main": [
|
|
[],
|
|
[
|
|
{
|
|
"node": "Build Batch Action Command",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Build Batch Action Command",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Build Batch Action Command",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Build Batch Action Command": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Execute Batch Container Action",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Execute Batch Container Action": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Check Batch Action Result",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Check Batch Action Result": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Needs Action Call",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Needs Action Call": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Execute Batch Action 2",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Handle Action Result",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Execute Batch Action 2": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Parse Batch Action 2",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Parse Batch Action 2": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Handle Action Result",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Handle Action Result": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Prepare Next Iteration",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Prepare Next Iteration": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Batch Loop",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Prepare Batch Stop Exec": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Initialize Batch State",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Answer Batch Exec": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Prepare Batch Exec",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Prepare Batch Exec": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Initialize Batch State",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
}
|
|
},
|
|
"pinData": {},
|
|
"settings": {
|
|
"executionOrder": "v1"
|
|
},
|
|
"staticData": null,
|
|
"tags": [],
|
|
"triggerCount": 1,
|
|
"active": false
|
|
}
|