feat: complete telegram cms system with workflows and deployment guide

- Add ULTIMATE-Telegram-CMS-COMPLETE.json with all commands
- Add Docker Event workflows with Gitea integration
- Add comprehensive deployment guide for fresh installs
- Add quick reference and testing checklist
- Include all n8n workflow exports

Commands:
/start, /list, /search, /stats, /preview, /publish, /delete, .review

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
2026-04-02 12:10:07 +02:00
parent 9d3e7ad44a
commit a36268302c
20 changed files with 6334 additions and 0 deletions

View File

@@ -0,0 +1,372 @@
{
"name": "Docker Event (Extended)",
"nodes": [
{
"parameters": {
"httpMethod": "POST",
"path": "docker-event",
"responseMode": "responseNode",
"options": {}
},
"type": "n8n-nodes-base.webhook",
"typeVersion": 2.1,
"position": [0, 0],
"id": "webhook-main",
"name": "Webhook"
},
{
"parameters": {
"jsCode": "const data = $input.first().json;\n\nconst container = data.container ?? data.body?.container ?? '';\nconst image = data.image ?? data.body?.image ?? '';\nconst timestamp = data.timestamp ?? data.body?.timestamp ?? '';\n\nconst slug = container.toLowerCase().replace(/[^a-z0-9]+/g, '-');\nconst serviceName = container.replace(/[-_]/g, ' ');\n\n// Detect project type\nlet projectType = 'selfhosted';\nif (image.includes('denshooter') || image.includes('dk0')) {\n projectType = 'own';\n} else if (container.match(/^(act-|gitea-actions-|runner-)/)) {\n projectType = 'cicd';\n}\n\n// Extract repo from image for own projects\nlet repo = null;\nif (projectType === 'own') {\n const match = image.match(/([^/]+):(\\w+)/);\n if (match) repo = match[1];\n}\n\nreturn [{\n json: {\n container,\n image,\n serviceName,\n timestamp,\n slug,\n projectType,\n repo\n }\n}];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [220, 0],
"id": "parse-context",
"name": "Parse Context"
},
{
"parameters": {
"url": "=https://cms.dk0.dev/items/projects?filter[slug][_eq]={{ $json.slug }}&limit=1",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "httpBearerAuth",
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.4,
"position": [440, 0],
"id": "search-slug",
"name": "Check if Exists"
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "loose"
},
"conditions": [
{
"leftValue": "={{ $json.data.length }}",
"rightValue": "0",
"operator": {
"type": "number",
"operation": "equals"
}
}
],
"combinator": "and"
},
"options": {}
},
"type": "n8n-nodes-base.if",
"typeVersion": 2.3,
"position": [660, 0],
"id": "if-new",
"name": "If New"
},
{
"parameters": {
"rules": {
"values": [
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": ""
},
"conditions": [
{
"leftValue": "={{ $('Parse Context').item.json.projectType }}",
"rightValue": "own",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "Own Project"
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": ""
},
"conditions": [
{
"leftValue": "={{ $('Parse Context').item.json.projectType }}",
"rightValue": "cicd",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "CI/CD (Ignore)"
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": ""
},
"conditions": [
{
"leftValue": "={{ $('Parse Context').item.json.projectType }}",
"rightValue": "selfhosted",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "Self-Hosted"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.switch",
"typeVersion": 3.2,
"position": [880, 0],
"id": "switch-type",
"name": "Switch Type"
},
{
"parameters": {
"url": "=https://git.dk0.dev/api/v1/repos/denshooter/{{ $('Parse Context').item.json.repo }}/commits?limit=1",
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth",
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.4,
"position": [1100, -200],
"id": "get-commits",
"name": "Get Last Commit",
"credentials": {
"httpHeaderAuth": {
"id": "gitea-token",
"name": "Gitea API"
}
}
},
{
"parameters": {
"url": "=https://git.dk0.dev/api/v1/repos/denshooter/{{ $('Parse Context').item.json.repo }}/contents/README.md",
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth",
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.4,
"position": [1100, -80],
"id": "get-readme",
"name": "Get README"
},
{
"parameters": {
"jsCode": "const ctx = $('Parse Context').first().json;\nconst commits = $('Get Last Commit').first().json;\nconst readme = $('Get README').first().json;\n\n// Get commit data\nconst commit = Array.isArray(commits) ? commits[0] : commits;\nconst commitMsg = commit?.commit?.message || 'No recent commits';\nconst commitAuthor = commit?.commit?.author?.name || 'Unknown';\n\n// Decode README (base64)\nlet readmeText = '';\ntry {\n const content = readme?.content || readme?.data?.content;\n if (content) {\n readmeText = Buffer.from(content, 'base64').toString('utf8');\n // First 500 chars\n readmeText = readmeText.substring(0, 500).replace(/\\n/g, ' ').trim();\n } else {\n readmeText = 'No README available';\n }\n} catch (e) {\n readmeText = 'No README available';\n}\n\nconsole.log('Commit:', commitMsg);\nconsole.log('README excerpt:', readmeText.substring(0, 100));\n\nreturn [{\n json: {\n container: ctx.container,\n image: ctx.image,\n slug: ctx.slug,\n repo: ctx.repo,\n commitMsg,\n commitAuthor,\n readmeExcerpt: readmeText\n }\n}];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [1320, -140],
"id": "merge-git-data",
"name": "Merge Git Data"
},
{
"parameters": {
"chatId": "145931600",
"text": "={{ \n'🚀 Neuer Deploy: ' + $json.container + '\\n' +\n'📦 ' + $json.image + '\\n\\n' +\n'📝 Letzter Commit:\\n' + $json.commitMsg + '\\n' +\n'👤 ' + $json.commitAuthor + '\\n\\n' +\n'📄 README:\\n' + $json.readmeExcerpt + '...\\n\\n' +\n'Was ist das Highlight?' \n}}",
"additionalFields": {
"replyMarkup": "inlineKeyboard",
"inlineKeyboard": {
"rows": [
{
"buttons": [
{
"text": "✍️ Selbst beschreiben",
"callbackData": "={{ 'manual:' + $json.slug }}"
},
{
"text": "🤖 Auto-generieren",
"callbackData": "={{ 'auto:' + $json.slug }}"
}
]
},
{
"buttons": [
{
"text": "❌ Ignorieren",
"callbackData": "={{ 'ignore:' + $json.slug }}"
}
]
}
]
}
}
},
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [1540, -140],
"id": "telegram-ask",
"name": "Ask via Telegram"
},
{
"parameters": {
"model": "openrouter/free",
"options": {}
},
"type": "@n8n/n8n-nodes-langchain.lmChatOpenRouter",
"typeVersion": 1,
"position": [1540, 160],
"id": "openrouter-model",
"name": "OpenRouter Chat Model"
},
{
"parameters": {
"promptType": "define",
"text": "=Du bist ein technischer Autor für dk0.dev.\n\nNeuer Self-Hosted Service:\nContainer: {{ $('Parse Context').item.json.container }}\nImage: {{ $('Parse Context').item.json.image }}\n\nErstelle eine Portfolio-Beschreibung:\n- Was macht die App\n- Warum Self-Hosting besser ist als Cloud\n- Wie sie in die Infrastruktur integriert ist\n\nAntworte NUR als JSON:\n{\n \"title_en\": \"Titel\",\n \"title_de\": \"Titel\",\n \"description_en\": \"4-6 Sätze\",\n \"description_de\": \"4-6 Sätze\",\n \"content_en\": \"2-3 Absätze Markdown\",\n \"content_de\": \"2-3 Absätze Markdown\",\n \"category\": \"selfhosted\",\n \"technologies\": [\"Docker\", \"...\"]\n}"
},
"type": "@n8n/n8n-nodes-langchain.chainLlm",
"typeVersion": 1.9,
"position": [1320, 80],
"id": "ai-selfhosted",
"name": "AI: Self-Hosted"
},
{
"parameters": {
"jsCode": "const raw = $input.first().json.text ?? \"\";\nconst match = raw.match(/\\{[\\s\\S]*\\}/);\nif (!match) throw new Error(\"No JSON found\");\nconst ai = JSON.parse(match[0]);\nreturn [{ json: ai }];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [1540, 80],
"id": "parse-json-selfhosted",
"name": "Parse JSON"
},
{
"parameters": {
"jsCode": "const ai = $input.first().json;\nconst ctx = $('Parse Context').first().json;\n\nconst body = {\n slug: ctx.slug,\n status: \"draft\",\n featured: false,\n title: ai.title_en,\n category: ai.category,\n technologies: ai.technologies,\n tags: ai.technologies,\n date: new Date().toISOString().slice(0, 10),\n translations: {\n create: [\n {\n languages_code: \"en-US\",\n title: ai.title_en,\n description: ai.description_en,\n content: ai.content_en\n },\n {\n languages_code: \"de-DE\",\n title: ai.title_de,\n description: ai.description_de,\n content: ai.content_de\n }\n ]\n }\n};\n\nconst response = await this.helpers.httpRequest({\n method: \"POST\",\n url: \"https://cms.dk0.dev/items/projects\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"Authorization\": \"Bearer RF2QytqhcLXuVy6FO3PzWlsoR-ysCTwB\"\n },\n body\n});\n\nreturn [{ json: response }];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [1760, 80],
"id": "add-to-directus-selfhosted",
"name": "Add to Directus"
},
{
"parameters": {
"chatId": "145931600",
"text": "={{ \n'🆕 Self-Hosted Service: ' + $('Parse Context').first().json.serviceName + '\\n\\n' +\n'📝 ' + $json.data.title + '\\n\\n' +\n'Status: Draft erstellt (ID: ' + $json.data.id + ')\\n\\n' +\n'/publishproject' + $json.data.id + ' — Veröffentlichen\\n' + \n'/deleteproject' + $json.data.id + ' — Löschen' \n}}",
"additionalFields": {}
},
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [1980, 80],
"id": "telegram-notify-selfhosted",
"name": "Notify Selfhosted"
},
{
"parameters": {
"respondWith": "json",
"responseBody": "{ \"success\": true, \"message\": \"CI/CD container ignored\" }",
"options": {}
},
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.5,
"position": [1100, 200],
"id": "respond-ignore",
"name": "Respond (Ignore)"
},
{
"parameters": {
"respondWith": "json",
"responseBody": "{ \"success\": true }",
"options": {}
},
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.5,
"position": [2200, 0],
"id": "respond-success",
"name": "Respond"
},
{
"parameters": {
"respondWith": "json",
"responseBody": "{ \"success\": true, \"message\": \"Project already exists\" }",
"options": {}
},
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.5,
"position": [880, 200],
"id": "respond-exists",
"name": "Respond (Exists)"
}
],
"connections": {
"Webhook": {
"main": [[{ "node": "Parse Context", "type": "main", "index": 0 }]]
},
"Parse Context": {
"main": [[{ "node": "Check if Exists", "type": "main", "index": 0 }]]
},
"Check if Exists": {
"main": [[{ "node": "If New", "type": "main", "index": 0 }]]
},
"If New": {
"main": [
[{ "node": "Switch Type", "type": "main", "index": 0 }],
[{ "node": "Respond (Exists)", "type": "main", "index": 0 }]
]
},
"Switch Type": {
"main": [
[{ "node": "Get Last Commit", "type": "main", "index": 0 }],
[{ "node": "Respond (Ignore)", "type": "main", "index": 0 }],
[{ "node": "AI: Self-Hosted", "type": "main", "index": 0 }]
]
},
"Get Last Commit": {
"main": [[{ "node": "Get README", "type": "main", "index": 0 }]]
},
"Get README": {
"main": [[{ "node": "Merge Git Data", "type": "main", "index": 0 }]]
},
"Merge Git Data": {
"main": [[{ "node": "Ask via Telegram", "type": "main", "index": 0 }]]
},
"Ask via Telegram": {
"main": [[{ "node": "Respond", "type": "main", "index": 0 }]]
},
"OpenRouter Chat Model": {
"ai_languageModel": [[{ "node": "AI: Self-Hosted", "type": "ai_languageModel", "index": 0 }]]
},
"AI: Self-Hosted": {
"main": [[{ "node": "Parse JSON", "type": "main", "index": 0 }]]
},
"Parse JSON": {
"main": [[{ "node": "Add to Directus", "type": "main", "index": 0 }]]
},
"Add to Directus": {
"main": [[{ "node": "Notify Selfhosted", "type": "main", "index": 0 }]]
},
"Notify Selfhosted": {
"main": [[{ "node": "Respond", "type": "main", "index": 0 }]]
}
},
"active": false,
"settings": {
"executionOrder": "v1"
},
"id": "docker-event-extended"
}