32fd965a2f
n8n IF nodes don't support credentials - $credentials syntax only works in nodes that make external calls. Reverted to direct user ID in the IF conditions and updated README with simpler configuration instructions. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
3040 lines
105 KiB
JSON
3040 lines
105 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": "telegram-credential",
|
|
"name": "Telegram API"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"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 --unix-socket /var/run/docker.sock 'http://localhost/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": "// Get Docker API response and message\nconst dockerOutput = $input.item.json.stdout;\nconst message = $('Keyword Router').item.json.message;\nconst chatId = message.chat.id;\n\n// Parse JSON response - only error if we can't parse valid JSON from stdout\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 }\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// Check if user specified a container name in message (e.g., \"status plex\")\nconst text = (message.text || '').toLowerCase().trim();\nlet requestedName = null;\n\n// Extract container name if message is more than just \"status\"\nconst parts = text.split(/\\s+/);\nif (parts.length > 1) {\n // Join all parts except \"status\" as the container query\n requestedName = parts.filter(p => p !== 'status').join(' ');\n}\n\n// If no container name specified, return summary\nif (!requestedName) {\n const counts = containers.reduce((acc, c) => {\n acc[c.State] = (acc[c.State] || 0) + 1;\n return acc;\n }, {});\n \n const total = containers.length;\n const running = counts['running'] || 0;\n const stopped = counts['exited'] || 0;\n const other = total - running - stopped;\n \n let summary = '<b>Docker Containers</b>\\n\\n';\n summary += 'Running: ' + running + '/' + total + '\\n';\n if (stopped > 0) summary += 'Stopped: ' + stopped + '\\n';\n if (other > 0) summary += 'Other: ' + other + '\\n';\n \n // List running containers\n const runningContainers = containers\n .filter(c => c.State === 'running')\n .map(c => {\n const name = normalizeName(c.Names[0]);\n const uptime = c.Status;\n return ' \\u2022 ' + name + ' - ' + uptime;\n });\n \n if (runningContainers.length > 0) {\n summary += '\\n<b>Running:</b>\\n' + runningContainers.join('\\n');\n }\n \n return [{\n json: {\n chatId: chatId,\n text: summary\n }\n }];\n}\n\n// Find matching containers\nconst matches = containers.filter(c => {\n const containerName = normalizeName(c.Names[0]);\n return containerName.includes(requestedName);\n});\n\nif (matches.length === 0) {\n return [{\n json: {\n chatId: chatId,\n error: true,\n text: 'No container found matching \"' + requestedName + '\"'\n }\n }];\n}\n\nif (matches.length === 1) {\n const container = matches[0];\n const name = normalizeName(container.Names[0]);\n const state = container.State;\n const status = container.Status;\n const image = container.Image;\n \n let text = '<b>' + name + '</b>\\n';\n text += 'State: ' + state + '\\n';\n text += 'Status: ' + status + '\\n';\n text += 'Image: ' + image;\n \n return [{\n json: {\n chatId: chatId,\n text: text\n }\n }];\n}\n\n// Multiple matches - list them\nconst matchList = matches.map(c => {\n const name = normalizeName(c.Names[0]);\n return ' \\u2022 ' + name + ' (' + c.State + ')';\n}).join('\\n');\n\nreturn [{\n json: {\n chatId: chatId,\n text: 'Multiple matches for \"' + requestedName + '\":\\n' + matchList\n }\n}];"
|
|
},
|
|
"id": "code-parse-match",
|
|
"name": "Parse and Match",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
1340,
|
|
100
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Get the data from previous node\nconst data = $input.item.json;\nconst chatId = data.chatId;\n\n// If error or summary, pass through as-is\nif (data.error || data.summary || data.multipleMatches) {\n return [{\n json: {\n chatId: chatId,\n text: data.text\n }\n }];\n}\n\n// Format single container details\nif (data.singleMatch) {\n const container = data.container;\n\n // State indicator mapping\n const stateIndicator = {\n 'running': '[OK]',\n 'exited': '[STOPPED]',\n 'paused': '[PAUSED]',\n 'restarting': '[RESTARTING]',\n 'dead': '[DEAD]'\n };\n\n const indicator = stateIndicator[container.state] || '[?]';\n\n // Format detailed response\n const text = `${indicator} <b>${container.name}</b>\\n\\n` +\n `<b>State:</b> ${container.state}\\n` +\n `<b>Status:</b> ${container.status}\\n` +\n `<b>Image:</b> ${container.image}\\n` +\n `<b>ID:</b> ${container.id.substring(0, 12)}`;\n\n return [{\n json: {\n chatId: chatId,\n text: text\n }\n }];\n}\n\n// Fallback\nreturn [{\n json: {\n chatId: chatId,\n text: \"Unexpected response format\"\n }\n}];"
|
|
},
|
|
"id": "code-format-response",
|
|
"name": "Format Response",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
1560,
|
|
100
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"resource": "message",
|
|
"operation": "sendMessage",
|
|
"chatId": "={{ $json.chatId }}",
|
|
"text": "={{ $json.text }}",
|
|
"additionalFields": {
|
|
"parse_mode": "HTML"
|
|
}
|
|
},
|
|
"id": "telegram-send-docker",
|
|
"name": "Send Docker Response",
|
|
"type": "n8n-nodes-base.telegram",
|
|
"typeVersion": 1.2,
|
|
"position": [
|
|
1560,
|
|
200
|
|
],
|
|
"credentials": {
|
|
"telegramApi": {
|
|
"id": "telegram-credential",
|
|
"name": "Telegram API"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"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 --unix-socket /var/run/docker.sock 'http://localhost/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\n// Find matching containers using fuzzy matching\nconst matches = containers.filter(c => {\n const containerName = normalizeName(c.Names[0]);\n const normalized = containerQuery.toLowerCase();\n return containerName.includes(normalized) || normalized.includes(containerName);\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}\" --unix-socket /var/run/docker.sock -X POST 'http://localhost/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": "telegram-credential",
|
|
"name": "Telegram API"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"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{{ $credentials.telegramApi.accessToken }}/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": "telegram-credential",
|
|
"name": "Telegram API"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"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": "telegram-credential",
|
|
"name": "Telegram API"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"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": "telegram-credential",
|
|
"name": "Telegram API"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"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": "telegram-credential",
|
|
"name": "Telegram API"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Parse callback data from button click (single suggestion or batch)\nconst callback = $json.callback_query;\nlet data;\ntry {\n data = JSON.parse(callback.data);\n} catch (e) {\n data = { a: 'x' }; // Treat parse error as cancel\n}\n\nconst queryId = callback.id;\nconst chatId = callback.message.chat.id;\nconst messageId = callback.message.message_id;\n\n// Check 2-minute timeout (120000ms)\nconst TWO_MINUTES = 120000;\nconst isExpired = data.t && (Date.now() - data.t > TWO_MINUTES);\n\n// Decode action: s=start, t=stop, r=restart, x=cancel\nconst actionMap = { s: 'start', t: 'stop', r: 'restart', x: 'cancel' };\nconst action = actionMap[data.a] || 'cancel';\n\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, // Array for batch support\n containerId: containerIds[0] || null, // For single-container compat\n expired: isExpired,\n isBatch,\n isCancel: action === 'cancel'\n }\n};"
|
|
},
|
|
"id": "code-parse-callback",
|
|
"name": "Parse Callback Data",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
900,
|
|
500
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"rules": {
|
|
"values": [
|
|
{
|
|
"id": "is-cancel",
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "strict"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "cancel-true",
|
|
"leftValue": "={{ $json.isCancel }}",
|
|
"rightValue": true,
|
|
"operator": {
|
|
"type": "boolean",
|
|
"operation": "equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"renameOutput": true,
|
|
"outputKey": "cancel"
|
|
},
|
|
{
|
|
"id": "is-expired",
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "strict"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "expired-true",
|
|
"leftValue": "={{ $json.expired }}",
|
|
"rightValue": true,
|
|
"operator": {
|
|
"type": "boolean",
|
|
"operation": "equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"renameOutput": true,
|
|
"outputKey": "expired"
|
|
},
|
|
{
|
|
"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"
|
|
}
|
|
]
|
|
},
|
|
"options": {
|
|
"fallbackOutput": "extra"
|
|
}
|
|
},
|
|
"id": "switch-route-callback",
|
|
"name": "Route Callback",
|
|
"type": "n8n-nodes-base.switch",
|
|
"typeVersion": 3.2,
|
|
"position": [
|
|
1120,
|
|
500
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Prepare cancel response - answer callback query and delete message\nconst { queryId, chatId, messageId } = $json;\nreturn {\n json: {\n queryId,\n chatId,\n messageId,\n answerText: 'Cancelled'\n }\n};"
|
|
},
|
|
"id": "code-handle-cancel",
|
|
"name": "Handle Cancel",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
1340,
|
|
500
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"method": "POST",
|
|
"url": "=https://api.telegram.org/bot{{ $credentials.telegramApi.accessToken }}/answerCallbackQuery",
|
|
"sendBody": true,
|
|
"specifyBody": "json",
|
|
"jsonBody": "={{ JSON.stringify({ callback_query_id: $json.queryId, text: $json.answerText }) }}",
|
|
"options": {}
|
|
},
|
|
"id": "http-answer-cancel",
|
|
"name": "Answer Cancel Query",
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.2,
|
|
"position": [
|
|
1560,
|
|
500
|
|
],
|
|
"credentials": {
|
|
"telegramApi": {
|
|
"id": "telegram-credential",
|
|
"name": "Telegram API"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"method": "POST",
|
|
"url": "=https://api.telegram.org/bot{{ $credentials.telegramApi.accessToken }}/deleteMessage",
|
|
"sendBody": true,
|
|
"specifyBody": "json",
|
|
"jsonBody": "={{ JSON.stringify({ chat_id: $('Handle Cancel').item.json.chatId, message_id: $('Handle Cancel').item.json.messageId }) }}",
|
|
"options": {}
|
|
},
|
|
"id": "http-delete-cancel-msg",
|
|
"name": "Delete Cancel Message",
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.2,
|
|
"position": [
|
|
1780,
|
|
500
|
|
],
|
|
"credentials": {
|
|
"telegramApi": {
|
|
"id": "telegram-credential",
|
|
"name": "Telegram API"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Prepare expired response\nconst { queryId, chatId, messageId } = $json;\nreturn {\n json: {\n queryId,\n chatId,\n messageId,\n answerText: 'Confirmation expired. Please try again.'\n }\n};"
|
|
},
|
|
"id": "code-handle-expired",
|
|
"name": "Handle Expired",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
1340,
|
|
600
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"method": "POST",
|
|
"url": "=https://api.telegram.org/bot{{ $credentials.telegramApi.accessToken }}/answerCallbackQuery",
|
|
"sendBody": true,
|
|
"specifyBody": "json",
|
|
"jsonBody": "={{ JSON.stringify({ callback_query_id: $json.queryId, text: $json.answerText, show_alert: true }) }}",
|
|
"options": {}
|
|
},
|
|
"id": "http-answer-expired",
|
|
"name": "Answer Expired Query",
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.2,
|
|
"position": [
|
|
1560,
|
|
600
|
|
],
|
|
"credentials": {
|
|
"telegramApi": {
|
|
"id": "telegram-credential",
|
|
"name": "Telegram API"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"method": "POST",
|
|
"url": "=https://api.telegram.org/bot{{ $credentials.telegramApi.accessToken }}/deleteMessage",
|
|
"sendBody": true,
|
|
"specifyBody": "json",
|
|
"jsonBody": "={{ JSON.stringify({ chat_id: $('Handle Expired').item.json.chatId, message_id: $('Handle Expired').item.json.messageId }) }}",
|
|
"options": {}
|
|
},
|
|
"id": "http-delete-expired-msg",
|
|
"name": "Delete Expired Message",
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.2,
|
|
"position": [
|
|
1780,
|
|
600
|
|
],
|
|
"credentials": {
|
|
"telegramApi": {
|
|
"id": "telegram-credential",
|
|
"name": "Telegram API"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Build curl command for callback action execution\nconst data = $input.item.json;\nconst containerId = data.containerId;\nconst action = data.action;\nconst chatId = data.chatId;\nconst messageId = data.messageId;\nconst queryId = data.queryId;\n\n// stop and restart use ?t=10 for graceful timeout\nconst timeout = (action === 'stop' || action === 'restart') ? '?t=10' : '';\n\n// Build curl command that returns HTTP status code\nconst cmd = `curl -s -o /dev/null -w \"%{http_code}\" --unix-socket /var/run/docker.sock -X POST 'http://localhost/v1.47/containers/${containerId}/${action}${timeout}'`;\n\nreturn {\n json: {\n cmd,\n containerId,\n action,\n chatId,\n messageId,\n queryId\n }\n};"
|
|
},
|
|
"id": "code-build-callback-cmd",
|
|
"name": "Build Callback Action",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
1340,
|
|
700
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"command": "={{ $json.cmd }}",
|
|
"options": {}
|
|
},
|
|
"id": "exec-callback-action",
|
|
"name": "Execute Callback Action",
|
|
"type": "n8n-nodes-base.executeCommand",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
1560,
|
|
700
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Parse callback action result and get container name\nconst stdout = $input.item.json.stdout;\nconst stderr = $input.item.json.stderr;\nconst cmdData = $('Build Callback Action').item.json;\nconst containerId = cmdData.containerId;\nconst action = cmdData.action;\nconst chatId = cmdData.chatId;\nconst messageId = cmdData.messageId;\nconst queryId = cmdData.queryId;\n\n// Check for curl-level errors first\nif (stderr && stderr.trim()) {\n return {\n json: {\n success: false,\n chatId,\n messageId,\n queryId,\n text: `Failed to ${action} 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{{ $credentials.telegramApi.accessToken }}/answerCallbackQuery",
|
|
"sendBody": true,
|
|
"specifyBody": "json",
|
|
"jsonBody": "={{ JSON.stringify({ callback_query_id: $json.queryId, text: $json.answerText }) }}",
|
|
"options": {}
|
|
},
|
|
"id": "http-answer-action",
|
|
"name": "Answer Action Query",
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.2,
|
|
"position": [
|
|
2000,
|
|
700
|
|
],
|
|
"credentials": {
|
|
"telegramApi": {
|
|
"id": "telegram-credential",
|
|
"name": "Telegram API"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"method": "POST",
|
|
"url": "=https://api.telegram.org/bot{{ $credentials.telegramApi.accessToken }}/deleteMessage",
|
|
"sendBody": true,
|
|
"specifyBody": "json",
|
|
"jsonBody": "={{ JSON.stringify({ chat_id: $('Parse Callback Result').item.json.chatId, message_id: $('Parse Callback Result').item.json.messageId }) }}",
|
|
"options": {}
|
|
},
|
|
"id": "http-delete-suggestion-msg",
|
|
"name": "Delete Suggestion Message",
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.2,
|
|
"position": [
|
|
2220,
|
|
700
|
|
],
|
|
"credentials": {
|
|
"telegramApi": {
|
|
"id": "telegram-credential",
|
|
"name": "Telegram API"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"resource": "message",
|
|
"operation": "sendMessage",
|
|
"chatId": "={{ $('Parse Callback Result').item.json.chatId }}",
|
|
"text": "={{ $('Parse Callback Result').item.json.text }}",
|
|
"additionalFields": {
|
|
"parse_mode": "HTML"
|
|
}
|
|
},
|
|
"id": "telegram-send-callback-result",
|
|
"name": "Send Callback Result",
|
|
"type": "n8n-nodes-base.telegram",
|
|
"typeVersion": 1.2,
|
|
"position": [
|
|
2440,
|
|
700
|
|
],
|
|
"credentials": {
|
|
"telegramApi": {
|
|
"id": "telegram-credential",
|
|
"name": "Telegram API"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"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}\" --unix-socket /var/run/docker.sock -X POST 'http://localhost/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{{ $credentials.telegramApi.accessToken }}/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": "telegram-credential",
|
|
"name": "Telegram API"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"method": "POST",
|
|
"url": "=https://api.telegram.org/bot{{ $credentials.telegramApi.accessToken }}/deleteMessage",
|
|
"sendBody": true,
|
|
"specifyBody": "json",
|
|
"jsonBody": "={{ JSON.stringify({ chat_id: $('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": "telegram-credential",
|
|
"name": "Telegram API"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"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": "telegram-credential",
|
|
"name": "Telegram API"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"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 --unix-socket /var/run/docker.sock 'http://localhost/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\n// Find matching containers using fuzzy matching\nconst matches = containers.filter(c => {\n const containerName = normalizeName(c.Names[0]);\n const normalized = containerQuery.toLowerCase();\n return containerName.includes(normalized) || normalized.includes(containerName);\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": "telegram-credential",
|
|
"name": "Telegram API"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"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": "telegram-credential",
|
|
"name": "Telegram API"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"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": "telegram-credential",
|
|
"name": "Telegram API"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Build inspect command for the matched container\nconst containerId = $json.matches[0].Id;\nconst containerName = $json.matches[0].Name;\nconst chatId = $json.chatId;\n\nreturn {\n json: {\n cmd: `curl -s --unix-socket /var/run/docker.sock 'http://localhost/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\nconst imageName = inspect.Config.Image;\nconst currentImageId = inspect.Image;\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\nreturn {\n json: {\n cmd: `curl -s --unix-socket /var/run/docker.sock -X POST 'http://localhost/v1.47/images/create?fromImage=${encodeURIComponent(imageName)}'`,\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": {}
|
|
},
|
|
"id": "exec-pull-image",
|
|
"name": "Pull Image",
|
|
"type": "n8n-nodes-base.executeCommand",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
2660,
|
|
1200
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Build inspect image command to get the new image ID\nconst pullData = $('Build Pull Command').item.json;\nconst imageName = pullData.imageName;\n\nreturn {\n json: {\n cmd: `curl -s --unix-socket /var/run/docker.sock 'http://localhost/v1.47/images/${encodeURIComponent(imageName)}/json'`,\n 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-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",
|
|
"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 - stay silent per CONTEXT.md\n return { json: { needsUpdate: false, chatId } };\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": "// Build stop container command\nconst containerId = $json.containerId;\n\nreturn {\n json: {\n cmd: `curl -s -o /dev/null -w \"%{http_code}\" --unix-socket /var/run/docker.sock -X POST 'http://localhost/v1.47/containers/${containerId}/stop?t=10'`,\n containerId,\n containerName: $json.containerName,\n currentVersion: $json.currentVersion,\n newVersion: $json.newVersion,\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}\" --unix-socket /var/run/docker.sock -X DELETE 'http://localhost/v1.47/containers/${containerId}'`,\n containerId,\n containerName: prevData.containerName,\n currentVersion: prevData.currentVersion,\n newVersion: prevData.newVersion,\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 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 --unix-socket /var/run/docker.sock -H \"Content-Type: application/json\" -d '${createBody.replace(/'/g, \"'\\\\''\")}' 'http://localhost/v1.47/containers/create?name=${encodeURIComponent(containerName)}'`;\n\nreturn {\n json: {\n cmd,\n containerName,\n currentVersion: $json.currentVersion,\n newVersion: $json.newVersion,\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 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}\" --unix-socket /var/run/docker.sock -X POST 'http://localhost/v1.47/containers/${newContainerId}/start'`,\n newContainerId,\n containerName: $json.containerName,\n currentVersion: $json.currentVersion,\n newVersion: $json.newVersion,\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 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 }\n };\n}\n\nconst message = `<b>${containerName}</b> updated: ${currentVersion} \\u2192 ${newVersion}`;\n\nreturn {\n json: {\n chatId,\n text: message\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": "telegram-credential",
|
|
"name": "Telegram API"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"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 lineCount: 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 --unix-socket /var/run/docker.sock 'http://localhost/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\n// Find matching containers using fuzzy matching\nconst matches = containers.filter(c => {\n const containerName = normalizeName(c.Names[0]);\n const normalized = containerQuery.toLowerCase();\n return containerName.includes(normalized) || normalized.includes(containerName);\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 --unix-socket /var/run/docker.sock \"http://localhost/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// 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\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>' + truncated + '</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": "telegram-credential",
|
|
"name": "Telegram API"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"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": "telegram-credential",
|
|
"name": "Telegram API"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"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": {
|
|
"method": "POST",
|
|
"url": "=https://api.telegram.org/bot{{ $credentials.telegramApi.accessToken }}/sendMessage",
|
|
"sendBody": true,
|
|
"specifyBody": "json",
|
|
"jsonBody": "={{ JSON.stringify({ \"chat_id\": $json.message.chat.id, \"text\": \"Use buttons below or type commands:\", \"parse_mode\": \"HTML\", \"reply_markup\": { \"keyboard\": [[{\"text\": \"Status\"}], [{\"text\": \"Start\"}, {\"text\": \"Stop\"}], [{\"text\": \"Restart\"}, {\"text\": \"Update\"}], [{\"text\": \"Logs\"}]], \"is_persistent\": true, \"resize_keyboard\": true, \"one_time_keyboard\": false } }) }}",
|
|
"options": {}
|
|
},
|
|
"id": "http-show-menu",
|
|
"name": "Show Menu",
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.2,
|
|
"position": [
|
|
1120,
|
|
300
|
|
],
|
|
"credentials": {
|
|
"telegramApi": {
|
|
"id": "telegram-credential",
|
|
"name": "Telegram API"
|
|
}
|
|
}
|
|
}
|
|
],
|
|
"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": "Build Callback Action",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Handle Cancel": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Answer Cancel Query",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Answer Cancel Query": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Delete Cancel Message",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Handle Expired": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Answer Expired Query",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Answer Expired Query": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Delete Expired Message",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Build Callback Action": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Execute Callback Action",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Execute Callback Action": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Parse Callback Result",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Parse Callback Result": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Answer Action Query",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Answer Action Query": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Delete Suggestion Message",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Delete Suggestion Message": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Send Callback Result",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Docker List Containers": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Parse and Match",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Parse and Match": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Send Docker Response",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Format Response": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Send Docker Response",
|
|
"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": "Build Inspect Command",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Handle Update Multiple",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[]
|
|
]
|
|
},
|
|
"Handle Update Multiple": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Send Update Multiple",
|
|
"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": "Build Image Inspect",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Build Image Inspect": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Inspect New Image",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Inspect New Image": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Compare Digests",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Compare Digests": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Check If Update Needed",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Check If Update Needed": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Build Stop Command",
|
|
"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
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"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": "Parse Action Command",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Parse Action Command",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Parse Action Command",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Parse Update Command",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Parse Logs Command",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Show Menu",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Parse Action Command": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Docker List for Action",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
}
|
|
},
|
|
"pinData": {},
|
|
"settings": {
|
|
"executionOrder": "v1"
|
|
},
|
|
"staticData": null,
|
|
"tags": [],
|
|
"triggerCount": 1,
|
|
"active": false
|
|
} |