{ "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, "leftValue": "", "typeValidation": "loose" }, "conditions": [ { "id": "has-message", "leftValue": "={{ $json.message }}", "rightValue": "", "operator": { "type": "object", "operation": "notEmpty" } } ], "combinator": "and" }, "renameOutput": true, "outputKey": "message" }, { "id": "route-callback", "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "loose" }, "conditions": [ { "id": "has-callback", "leftValue": "={{ $json.callback_query }}", "rightValue": "", "operator": { "type": "object", "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": "docker-query-route", "conditions": { "options": { "caseSensitive": false, "leftValue": "", "typeValidation": "loose" }, "conditions": [ { "id": "contains-status", "leftValue": "={{ $json.message.text.toLowerCase() }}", "rightValue": "status", "operator": { "type": "string", "operation": "contains" } } ], "combinator": "or" }, "renameOutput": false }, { "id": "action-command-route", "conditions": { "options": { "caseSensitive": false, "leftValue": "", "typeValidation": "loose" }, "conditions": [ { "id": "starts-with-start", "leftValue": "={{ $json.message.text.toLowerCase().trim() }}", "rightValue": "start ", "operator": { "type": "string", "operation": "startsWith" } }, { "id": "starts-with-stop", "leftValue": "={{ $json.message.text.toLowerCase().trim() }}", "rightValue": "stop ", "operator": { "type": "string", "operation": "startsWith" } }, { "id": "starts-with-restart", "leftValue": "={{ $json.message.text.toLowerCase().trim() }}", "rightValue": "restart ", "operator": { "type": "string", "operation": "startsWith" } } ], "combinator": "or" }, "renameOutput": false } ] }, "options": { "fallbackOutput": "extra" } }, "id": "switch-route", "name": "Route Message", "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 user input\nconst dockerOutput = $input.item.json.stdout;\nconst userMessage = $('Telegram Trigger').item.json.message.text.toLowerCase().trim();\nconst chatId = $('Telegram Trigger').item.json.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: \"Can't reach Docker - check if n8n has socket access\"\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// Extract container name from user query\n// Remove common query words to get the container name\nconst queryWords = ['status', 'show', 'check', 'container', 'docker', 'what', 'is', 'the', 'of', 'for'];\nconst words = userMessage.split(/\\s+/).filter(word => !queryWords.includes(word));\nconst requestedName = words.join(' ').trim();\n\n// If no container name specified, return summary\nif (!requestedName || requestedName === '') {\n const counts = containers.reduce((acc, c) => {\n acc[c.State] = (acc[c.State] || 0) + 1;\n return acc;\n }, {});\n\n const parts = [];\n if (counts.running) parts.push(`${counts.running} running`);\n if (counts.exited) parts.push(`${counts.exited} stopped`);\n if (counts.paused) parts.push(`${counts.paused} paused`);\n if (counts.restarting) parts.push(`${counts.restarting} restarting`);\n\n const summary = parts.length > 0 ? parts.join(', ') : 'No containers found';\n\n return [{\n json: {\n chatId: chatId,\n summary: true,\n containers: containers,\n text: `Container summary: ${summary}`\n }\n }];\n}\n\n// Find matching containers using fuzzy matching\nconst matches = containers.filter(c => {\n const containerName = normalizeName(c.Names[0]);\n const normalized = requestedName.toLowerCase();\n return containerName.includes(normalized) || normalized.includes(containerName);\n});\n\n// Handle no matches\nif (matches.length === 0) {\n return [{\n json: {\n chatId: chatId,\n error: true,\n text: `No container found matching \"${requestedName}\".\\n\\nTry \"status\" to see all containers.`\n }\n }];\n}\n\n// Handle multiple matches\nif (matches.length > 1) {\n const names = matches.map(c => c.Names[0].replace(/^\\//, '')).join('\\n- ');\n return [{\n json: {\n chatId: chatId,\n multipleMatches: true,\n matches: matches,\n text: `Found ${matches.length} matches:\\n\\n- ${names}\\n\\nPlease be more specific.`\n }\n }];\n}\n\n// Single match - return container details\nconst container = matches[0];\nreturn [{\n json: {\n chatId: chatId,\n singleMatch: true,\n container: {\n id: container.Id,\n name: container.Names[0].replace(/^\\//, ''),\n state: container.State,\n status: container.Status,\n image: container.Image\n }\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} ${container.name}\\n\\n` +\n `State: ${container.state}\\n` +\n `Status: ${container.status}\\n` +\n `Image: ${container.image}\\n` +\n `ID: ${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": "const message = $input.item.json.message;\nconst timestamp = new Date().toISOString();\nconst text = message.text || '(no text)';\n\nreturn {\n json: {\n chatId: message.chat.id,\n text: `Got: ${text}\\n\\nProcessed: ${timestamp}`\n }\n};" }, "id": "code-format-echo", "name": "Format Echo", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [900, 800] }, { "parameters": { "resource": "message", "operation": "sendMessage", "chatId": "={{ $json.chatId }}", "text": "={{ $json.text }}", "additionalFields": { "parse_mode": "HTML" } }, "id": "telegram-send", "name": "Send Echo", "type": "n8n-nodes-base.telegram", "typeVersion": 1.2, "position": [1120, 800], "credentials": { "telegramApi": { "id": "telegram-credential", "name": "Telegram API" } } }, { "parameters": { "jsCode": "// Parse action 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 action pattern: start/stop/restart followed by container name\nconst match = text.match(/^(start|stop|restart)\\s+(.+)$/i);\n\nif (!match) {\n return {\n json: {\n error: true,\n errorMessage: 'Invalid action format. Use: start/stop/restart ',\n chatId: chatId\n }\n };\n}\n\nreturn {\n json: {\n action: match[1].toLowerCase(),\n containerQuery: match[2].trim(),\n chatId: chatId,\n messageId: messageId\n }\n};" }, "id": "code-parse-action", "name": "Parse Action", "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\nconst dockerOutput = $input.item.json.stdout;\nconst actionData = $('Parse Action').item.json;\nconst action = actionData.action;\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: \"Can't reach Docker - check if n8n has socket access\"\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 }\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, "leftValue": "", "typeValidation": "strict" }, "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, "leftValue": "", "typeValidation": "strict" }, "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, "leftValue": "", "typeValidation": "strict" }, "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, "leftValue": "", "typeValidation": "strict" }, "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\nif (stderr && stderr.trim()) {\n return {\n json: {\n success: false,\n chatId: chatId,\n text: `Failed to ${action} ${containerName}: ${stderr.trim()}`\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: `${containerName} ${verb} successfully`\n }\n };\n}\n\n// Handle error codes\nlet errorMsg;\nswitch (statusCode) {\n case 404:\n errorMsg = 'Container not found';\n break;\n case 409:\n errorMsg = 'Container is in a conflicting state';\n break;\n case 500:\n errorMsg = 'Docker server error';\n break;\n default:\n errorMsg = `HTTP ${statusCode}`;\n}\n\nreturn {\n json: {\n success: false,\n chatId: chatId,\n text: `Failed to ${action} ${containerName}: ${errorMsg}`\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": "// No container matched the query\nconst data = $input.item.json;\nreturn {\n json: {\n chatId: data.chatId,\n text: `No container found matching '${data.containerQuery}'`\n }\n};" }, "id": "code-no-match", "name": "Format No Match", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [1780, 300] }, { "parameters": { "resource": "message", "operation": "sendMessage", "chatId": "={{ $json.chatId }}", "text": "={{ $json.text }}", "additionalFields": { "parse_mode": "HTML" } }, "id": "telegram-send-no-match", "name": "Send No Match", "type": "n8n-nodes-base.telegram", "typeVersion": 1.2, "position": [2000, 300], "credentials": { "telegramApi": { "id": "telegram-credential", "name": "Telegram API" } } }, { "parameters": { "jsCode": "// Multiple containers matched - placeholder for confirmation flow (Plan 03-02)\nconst data = $input.item.json;\nconst names = data.matches.map(m => m.Name).join(', ');\nreturn {\n json: {\n chatId: data.chatId,\n text: `Found ${data.matchCount} containers matching '${data.containerQuery}': ${names}\\n\\nConfirmation required. (Coming in next update)`\n }\n};" }, "id": "code-multiple-matches", "name": "Format Multiple Matches", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [1780, 500] }, { "parameters": { "resource": "message", "operation": "sendMessage", "chatId": "={{ $json.chatId }}", "text": "={{ $json.text }}", "additionalFields": { "parse_mode": "HTML" } }, "id": "telegram-send-multiple", "name": "Send Multiple Matches", "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" } } } ], "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": "Route Message", "type": "main", "index": 0 } ], [] ] }, "IF Callback Authenticated": { "main": [ [], [] ] }, "Route Message": { "main": [ [ { "node": "Docker List Containers", "type": "main", "index": 0 } ], [ { "node": "Parse Action", "type": "main", "index": 0 } ], [], [ { "node": "Format Echo", "type": "main", "index": 0 } ] ] }, "Docker List Containers": { "main": [ [ { "node": "Parse and Match", "type": "main", "index": 0 } ] ] }, "Parse and Match": { "main": [ [ { "node": "Format Response", "type": "main", "index": 0 } ] ] }, "Format Response": { "main": [ [ { "node": "Send Docker Response", "type": "main", "index": 0 } ] ] }, "Format Echo": { "main": [ [ { "node": "Send Echo", "type": "main", "index": 0 } ] ] }, "Parse Action": { "main": [ [ { "node": "Docker List for Action", "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": "Format No Match", "type": "main", "index": 0 } ], [ { "node": "Build Action Command", "type": "main", "index": 0 } ], [ { "node": "Format Multiple Matches", "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 } ] ] }, "Format No Match": { "main": [ [ { "node": "Send No Match", "type": "main", "index": 0 } ] ] }, "Format Multiple Matches": { "main": [ [ { "node": "Send Multiple Matches", "type": "main", "index": 0 } ] ] } }, "pinData": {}, "settings": { "executionOrder": "v1" }, "staticData": null, "tags": [], "triggerCount": 1, "active": false }