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:
541
TELEGRAM_CMS_DEPLOYMENT.md
Normal file
541
TELEGRAM_CMS_DEPLOYMENT.md
Normal file
@@ -0,0 +1,541 @@
|
|||||||
|
# 🚀 Telegram CMS - Complete Deployment Guide
|
||||||
|
|
||||||
|
**Für andere PCs / Fresh Install**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 Was du bekommst
|
||||||
|
|
||||||
|
Ein vollständiges Telegram-Bot-System zur Verwaltung deines DK0 Portfolios:
|
||||||
|
|
||||||
|
### ✨ Features
|
||||||
|
|
||||||
|
- **Dashboard** (`/start`) - Übersicht mit Draft-Zählern und Quick Actions
|
||||||
|
- **Listen** (`/list projects|books`) - Paginierte Listen mit Action-Buttons
|
||||||
|
- **Suche** (`/search <term>`) - Durchsucht Projekte & Bücher
|
||||||
|
- **Statistiken** (`/stats`) - Analytics Dashboard (Views, Kategorien, Ratings)
|
||||||
|
- **Vorschau** (`/preview<ID>`) - Zeigt EN + DE Übersetzungen
|
||||||
|
- **Publish** (`/publish<ID>`) - Veröffentlicht Items (auto-detect: Project/Book)
|
||||||
|
- **Delete** (`/delete<ID>`) - Löscht Items permanent
|
||||||
|
- **Delete Review** (`/deletereview<ID>`) - Löscht nur Review-Text
|
||||||
|
- **AI Review** (`.review <HC_ID> <RATING> <TEXT>`) - Generiert EN+DE Reviews via Gemini
|
||||||
|
|
||||||
|
### 🤖 Automatisierungen
|
||||||
|
|
||||||
|
- **Docker Events** - Erkennt neue Deployments, fragt ob AI Beschreibung generieren soll
|
||||||
|
- **Book Reviews** - AI generiert DE+EN Reviews aus deinem Input
|
||||||
|
- **Status API** - Spotify, Discord, WakaTime Integration (bereits vorhanden)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📦 Workflows zum Importieren
|
||||||
|
|
||||||
|
### 1. **ULTIMATE Telegram CMS** ⭐ (HAUPT-WORKFLOW)
|
||||||
|
|
||||||
|
**Datei:** `n8n-workflows/ULTIMATE-Telegram-CMS-COMPLETE.json`
|
||||||
|
|
||||||
|
**Beschreibung:**
|
||||||
|
- Zentraler Command Router für alle `/` Befehle
|
||||||
|
- Enthält alle Handler: Dashboard, List, Search, Stats, Preview, Publish, Delete, AI Reviews
|
||||||
|
- **Aktivieren:** Ja (Telegram Trigger)
|
||||||
|
|
||||||
|
**Credentials:**
|
||||||
|
- Telegram API: `DK0_Server` (ID: `ADurvy9EKUDzbDdq`)
|
||||||
|
- Directus Token: `RF2QytqhcLXuVy6FO3PzWlsoR-ysCTwB` (hardcoded in Nodes)
|
||||||
|
- OpenRouter API: `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`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. **Docker Event Extended** (Optional, empfohlen)
|
||||||
|
|
||||||
|
**Datei:** `n8n-workflows/Docker Event (Extended).json`
|
||||||
|
|
||||||
|
**Beschreibung:**
|
||||||
|
- Reagiert auf Docker Webhooks (`https://n8n.dk0.dev/webhook/docker-event`)
|
||||||
|
- Erkennt eigene Projekte (`denshooter/dk0`) vs. CI/CD Container
|
||||||
|
- Holt letzten Commit + README von Gitea
|
||||||
|
- Fragt per Telegram-Button: Auto-generieren, Selbst beschreiben, Ignorieren
|
||||||
|
|
||||||
|
**Credentials:**
|
||||||
|
- Telegram API: `DK0_Server`
|
||||||
|
- Gitea Token: `gitea-token` (noch anzulegen!)
|
||||||
|
|
||||||
|
**Setup:**
|
||||||
|
1. Gitea Token erstellen: https://git.dk0.dev/user/settings/applications
|
||||||
|
- Name: `n8n-api`
|
||||||
|
- Permissions: ✅ `repo` (read)
|
||||||
|
2. In n8n: Credentials → New → HTTP Header Auth
|
||||||
|
- Name: `gitea-token`
|
||||||
|
- Header Name: `Authorization`
|
||||||
|
- Value: `token <YOUR_GITEA_TOKEN>`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. **Docker Callback Handler** (Required if using Docker Events)
|
||||||
|
|
||||||
|
**Datei:** `n8n-workflows/Docker Event - Callback Handler.json`
|
||||||
|
|
||||||
|
**Beschreibung:**
|
||||||
|
- Verarbeitet Button-Klicks aus Docker Event Workflow
|
||||||
|
- Auto: Ruft AI (Gemini) mit Commit+README Context
|
||||||
|
- Manual: Fragt nach manueller Beschreibung
|
||||||
|
- Ignore: Bestätigt ignorieren
|
||||||
|
|
||||||
|
**Credentials:**
|
||||||
|
- Telegram API: `DK0_Server`
|
||||||
|
- OpenRouter API: (same as above)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. **Book Review** (Legacy - kann ersetzt werden)
|
||||||
|
|
||||||
|
**Datei:** `n8n-workflows/Book Review.json`
|
||||||
|
|
||||||
|
**Status:** ⚠️ Wird von ULTIMATE CMS ersetzt (nutzt `.review` Command)
|
||||||
|
|
||||||
|
**Optional behalten falls:**
|
||||||
|
- Separate Webhook gewünscht
|
||||||
|
- Andere Trigger-Quelle (z.B. Hardcover API direkt)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. **Reading / Finished Books** (Andere Features)
|
||||||
|
|
||||||
|
**Dateien:**
|
||||||
|
- `finishedBooks.json` - Hardcover finished books webhook
|
||||||
|
- `reading (1).json` - Currently reading books
|
||||||
|
|
||||||
|
**Status:** Optional, wenn du Hardcover Integration nutzt
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠️ Schritt-für-Schritt Installation
|
||||||
|
|
||||||
|
### **Schritt 1: n8n Credentials prüfen**
|
||||||
|
|
||||||
|
Öffne n8n → Settings → Credentials
|
||||||
|
|
||||||
|
**Benötigt:**
|
||||||
|
|
||||||
|
| Name | Type | ID | Notes |
|
||||||
|
|------|------|-----|-------|
|
||||||
|
| `DK0_Server` | Telegram API | `ADurvy9EKUDzbDdq` | Telegram Bot Token |
|
||||||
|
| `gitea-token` | HTTP Header Auth | neu erstellen | Für Commit-Daten |
|
||||||
|
| OpenRouter | (hardcoded) | - | In Code Nodes |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **Schritt 2: Workflows importieren**
|
||||||
|
|
||||||
|
1. **ULTIMATE Telegram CMS:**
|
||||||
|
```
|
||||||
|
n8n → Workflows → Import from File
|
||||||
|
→ Wähle: n8n-workflows/ULTIMATE-Telegram-CMS-COMPLETE.json
|
||||||
|
→ ✅ Activate Workflow
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Docker Event Extended:**
|
||||||
|
```
|
||||||
|
→ Wähle: n8n-workflows/Docker Event (Extended).json
|
||||||
|
→ Credentials mappen: DK0_Server + gitea-token
|
||||||
|
→ ✅ Activate Workflow
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Docker Callback Handler:**
|
||||||
|
```
|
||||||
|
→ Wähle: n8n-workflows/Docker Event - Callback Handler.json
|
||||||
|
→ Credentials mappen: DK0_Server
|
||||||
|
→ ✅ Activate Workflow
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **Schritt 3: Gitea Token erstellen**
|
||||||
|
|
||||||
|
1. Gehe zu: https://git.dk0.dev/user/settings/applications
|
||||||
|
2. **Generate New Token**
|
||||||
|
- Token Name: `n8n-api`
|
||||||
|
- Select Scopes: ✅ `repo` (Repository Read)
|
||||||
|
3. Kopiere Token: `<YOUR_TOKEN_HIER>`
|
||||||
|
4. In n8n:
|
||||||
|
```
|
||||||
|
Credentials → New → HTTP Header Auth
|
||||||
|
Name: gitea-token
|
||||||
|
Header Name: Authorization
|
||||||
|
Value: token <YOUR_TOKEN_HIER>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **Schritt 4: Test Commands**
|
||||||
|
|
||||||
|
Öffne Telegram → DK0_Server Bot:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
/start
|
||||||
|
# Expected: Dashboard mit Quick Stats + Buttons
|
||||||
|
|
||||||
|
/list projects
|
||||||
|
# Expected: Liste aller Draft Projekte
|
||||||
|
|
||||||
|
/stats
|
||||||
|
# Expected: Analytics Dashboard
|
||||||
|
|
||||||
|
/search nextjs
|
||||||
|
# Expected: Suchergebnisse
|
||||||
|
|
||||||
|
.review 427565 5 Great book about AI!
|
||||||
|
# Expected: AI generiert EN+DE Review, sendet Vorschau
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 Konfiguration anpassen
|
||||||
|
|
||||||
|
### Telegram Chat ID ändern
|
||||||
|
|
||||||
|
Aktuell: `145931600` (dein Telegram Account)
|
||||||
|
|
||||||
|
**Ändern in:**
|
||||||
|
1. Öffne Workflow: `ULTIMATE-Telegram-CMS-COMPLETE`
|
||||||
|
2. Suche Node: `Telegram Trigger`
|
||||||
|
3. Additional Fields → Chat ID → `<NEUE_CHAT_ID>`
|
||||||
|
|
||||||
|
**Chat ID herausfinden:**
|
||||||
|
```bash
|
||||||
|
curl https://api.telegram.org/bot<BOT_TOKEN>/getUpdates
|
||||||
|
# Schick dem Bot eine Nachricht, dann findest du in "chat":{"id":123456}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Directus API Token ändern
|
||||||
|
|
||||||
|
Aktuell: `RF2QytqhcLXuVy6FO3PzWlsoR-ysCTwB`
|
||||||
|
|
||||||
|
**Ändern in allen Code Nodes:**
|
||||||
|
```javascript
|
||||||
|
// Suche nach:
|
||||||
|
"Authorization": "Bearer RF2QytqhcLXuVy6FO3PzWlsoR-ysCTwB"
|
||||||
|
|
||||||
|
// Ersetze mit:
|
||||||
|
"Authorization": "Bearer <NEUER_TOKEN>"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Betroffene Nodes:**
|
||||||
|
- Dashboard Handler
|
||||||
|
- List Handler
|
||||||
|
- Search Handler
|
||||||
|
- Stats Handler
|
||||||
|
- Preview Handler
|
||||||
|
- Publish Handler
|
||||||
|
- Delete Handler
|
||||||
|
- Delete Review Handler
|
||||||
|
- Create Review Handler
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### OpenRouter AI Model ändern
|
||||||
|
|
||||||
|
Aktuell: `google/gemini-2.0-flash-exp:free`
|
||||||
|
|
||||||
|
**Alternativen:**
|
||||||
|
- `google/gemini-2.5-flash` (besser, aber kostenpflichtig)
|
||||||
|
- `openrouter/free` (fallback)
|
||||||
|
- `anthropic/claude-3.5-sonnet` (premium)
|
||||||
|
|
||||||
|
**Ändern in:**
|
||||||
|
- Node: `Create Review Handler` (ULTIMATE CMS)
|
||||||
|
- Node: `Generate AI Description` (Docker Callback)
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Suche:
|
||||||
|
"model": "google/gemini-2.0-flash-exp:free"
|
||||||
|
|
||||||
|
// Ersetze mit:
|
||||||
|
"model": "google/gemini-2.5-flash"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Command Reference
|
||||||
|
|
||||||
|
### Basic Commands
|
||||||
|
|
||||||
|
| Command | Beschreibung | Beispiel |
|
||||||
|
|---------|--------------|----------|
|
||||||
|
| `/start` | Dashboard anzeigen | `/start` |
|
||||||
|
| `/list projects` | Alle Draft-Projekte | `/list projects` |
|
||||||
|
| `/list books` | Alle Draft-Bücher | `/list books` |
|
||||||
|
| `/search <term>` | Suche in Projekten & Büchern | `/search nextjs` |
|
||||||
|
| `/stats` | Statistiken anzeigen | `/stats` |
|
||||||
|
|
||||||
|
### Item Management
|
||||||
|
|
||||||
|
| Command | Beschreibung | Beispiel |
|
||||||
|
|---------|--------------|----------|
|
||||||
|
| `/preview<ID>` | Vorschau (EN+DE) | `/preview42` |
|
||||||
|
| `/publish<ID>` | Veröffentlichen (auto-detect) | `/publish42` |
|
||||||
|
| `/delete<ID>` | Löschen (auto-detect) | `/delete42` |
|
||||||
|
| `/deletereview<ID>` | Nur Review-Text löschen | `/deletereview42` |
|
||||||
|
|
||||||
|
### AI Review Creation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
.review <HARDCOVER_ID> <RATING> <YOUR_REVIEW_TEXT>
|
||||||
|
|
||||||
|
# Beispiel:
|
||||||
|
.review 427565 5 Great book about AI and the future of work!
|
||||||
|
|
||||||
|
# Generiert:
|
||||||
|
# - EN Review (erweitert deinen Text)
|
||||||
|
# - DE Review (übersetzt + erweitert)
|
||||||
|
# - Setzt Rating auf 5/5
|
||||||
|
# - Erstellt Draft in Directus
|
||||||
|
# - Sendet Vorschau mit /publish Button
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🐛 Troubleshooting
|
||||||
|
|
||||||
|
### "Item not found"
|
||||||
|
|
||||||
|
**Ursache:** ID existiert nicht in Directus
|
||||||
|
|
||||||
|
**Fix:**
|
||||||
|
```bash
|
||||||
|
# Prüfe in Directus:
|
||||||
|
https://cms.dk0.dev/admin/content/projects
|
||||||
|
https://cms.dk0.dev/admin/content/book_reviews
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### "Error loading dashboard"
|
||||||
|
|
||||||
|
**Ursache:** Directus API nicht erreichbar oder Token falsch
|
||||||
|
|
||||||
|
**Fix:**
|
||||||
|
```bash
|
||||||
|
# Test Directus API:
|
||||||
|
curl -H "Authorization: Bearer RF2QytqhcLXuVy6FO3PzWlsoR-ysCTwB" \
|
||||||
|
https://cms.dk0.dev/items/projects?limit=1
|
||||||
|
|
||||||
|
# Expected: JSON mit Projekt-Daten
|
||||||
|
# Falls 401: Token abgelaufen/falsch
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### AI Review schlägt fehl
|
||||||
|
|
||||||
|
**Ursache:** OpenRouter API Problem oder Model nicht verfügbar
|
||||||
|
|
||||||
|
**Fix:**
|
||||||
|
```bash
|
||||||
|
# Test OpenRouter:
|
||||||
|
curl -X POST https://openrouter.ai/api/v1/chat/completions \
|
||||||
|
-H "Authorization: Bearer sk-or-v1-..." \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"model":"google/gemini-2.0-flash-exp:free","messages":[{"role":"user","content":"test"}]}'
|
||||||
|
|
||||||
|
# Falls 402: Credits aufgebraucht
|
||||||
|
# → Wechsel zu kostenpflichtigem Model
|
||||||
|
# → Oder nutze "openrouter/free"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Telegram antwortet nicht
|
||||||
|
|
||||||
|
**Ursache:** Workflow nicht aktiviert oder Webhook Problem
|
||||||
|
|
||||||
|
**Fix:**
|
||||||
|
1. n8n → Workflows → ULTIMATE Telegram CMS → ✅ Active
|
||||||
|
2. Check Executions:
|
||||||
|
```
|
||||||
|
n8n → Executions → Filter by Workflow
|
||||||
|
→ Suche nach Fehlern (red icon)
|
||||||
|
```
|
||||||
|
3. Test Webhook manuell:
|
||||||
|
```bash
|
||||||
|
curl -X POST https://n8n.dk0.dev/webhook-test/telegram-cms-webhook-001 \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"message":{"text":"/start","chat":{"id":145931600}}}'
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Docker Event erkennt keine Container
|
||||||
|
|
||||||
|
**Ursache:** Webhook wird nicht getriggert
|
||||||
|
|
||||||
|
**Fix:**
|
||||||
|
|
||||||
|
**1. Prüfe Docker Event Source:**
|
||||||
|
```bash
|
||||||
|
# Auf Server (wo Docker läuft):
|
||||||
|
docker events --filter 'event=start' --format '{{json .}}'
|
||||||
|
|
||||||
|
# Expected: JSON output bei neuen Containern
|
||||||
|
```
|
||||||
|
|
||||||
|
**2. Test Webhook manuell:**
|
||||||
|
```bash
|
||||||
|
curl -X POST https://n8n.dk0.dev/webhook/docker-event \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"container":"portfolio-dev",
|
||||||
|
"image":"denshooter/portfolio:latest",
|
||||||
|
"timestamp":"2026-04-02T10:00:00Z"
|
||||||
|
}'
|
||||||
|
|
||||||
|
# Expected: Telegram Nachricht mit Buttons
|
||||||
|
```
|
||||||
|
|
||||||
|
**3. Setup Docker Event Forwarder:**
|
||||||
|
|
||||||
|
Auf Server erstellen: `/opt/docker-event-forwarder.sh`
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
docker events --filter 'event=start' --format '{{json .}}' | while read event; do
|
||||||
|
container=$(echo "$event" | jq -r '.Actor.Attributes.name')
|
||||||
|
image=$(echo "$event" | jq -r '.Actor.Attributes.image')
|
||||||
|
timestamp=$(echo "$event" | jq -r '.time')
|
||||||
|
|
||||||
|
curl -X POST https://n8n.dk0.dev/webhook/docker-event \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"container\":\"$container\",\"image\":\"$image\",\"timestamp\":\"$timestamp\"}"
|
||||||
|
done
|
||||||
|
```
|
||||||
|
|
||||||
|
Systemd Service: `/etc/systemd/system/docker-event-forwarder.service`
|
||||||
|
```ini
|
||||||
|
[Unit]
|
||||||
|
Description=Docker Event Forwarder to n8n
|
||||||
|
After=docker.service
|
||||||
|
Requires=docker.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
ExecStart=/opt/docker-event-forwarder.sh
|
||||||
|
Restart=always
|
||||||
|
User=root
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
```
|
||||||
|
|
||||||
|
Aktivieren:
|
||||||
|
```bash
|
||||||
|
chmod +x /opt/docker-event-forwarder.sh
|
||||||
|
systemctl daemon-reload
|
||||||
|
systemctl enable docker-event-forwarder
|
||||||
|
systemctl start docker-event-forwarder
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 Environment Variables (Optional)
|
||||||
|
|
||||||
|
Falls du Tokens nicht hardcoden willst, nutze n8n Environment Variables:
|
||||||
|
|
||||||
|
**In `.env` (n8n Docker):**
|
||||||
|
```env
|
||||||
|
DIRECTUS_TOKEN=RF2QytqhcLXuVy6FO3PzWlsoR-ysCTwB
|
||||||
|
OPENROUTER_API_KEY=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
|
||||||
|
TELEGRAM_CHAT_ID=145931600
|
||||||
|
```
|
||||||
|
|
||||||
|
**In Workflows nutzen:**
|
||||||
|
```javascript
|
||||||
|
// Statt:
|
||||||
|
"Authorization": "Bearer RF2QytqhcLXuVy6FO3PzWlsoR-ysCTwB"
|
||||||
|
|
||||||
|
// Nutze:
|
||||||
|
"Authorization": `Bearer ${process.env.DIRECTUS_TOKEN}`
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 Backup & Updates
|
||||||
|
|
||||||
|
### Workflows exportieren
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# In n8n:
|
||||||
|
Workflows → ULTIMATE Telegram CMS → ... → Download
|
||||||
|
|
||||||
|
# Speichern als:
|
||||||
|
n8n-workflows/ULTIMATE-Telegram-CMS-COMPLETE-v2.json
|
||||||
|
```
|
||||||
|
|
||||||
|
### Git Push
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /pfad/zum/portfolio
|
||||||
|
git add n8n-workflows/
|
||||||
|
git commit -m "chore: update telegram cms workflows"
|
||||||
|
git push origin telegram-cms-deployment
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Production Checklist
|
||||||
|
|
||||||
|
- [ ] Alle Workflows importiert
|
||||||
|
- [ ] Credentials gemappt (DK0_Server, gitea-token)
|
||||||
|
- [ ] Gitea Token erstellt & getestet
|
||||||
|
- [ ] `/start` Command funktioniert
|
||||||
|
- [ ] `/list projects` zeigt Daten
|
||||||
|
- [ ] `/stats` zeigt Statistiken
|
||||||
|
- [ ] AI Review generiert Text (`.review` Test)
|
||||||
|
- [ ] Docker Event Webhook getestet
|
||||||
|
- [ ] Inline Buttons funktionieren
|
||||||
|
- [ ] Error Handling in n8n Executions geprüft
|
||||||
|
- [ ] Workflows in Git committed
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 Weitere Dokumentation
|
||||||
|
|
||||||
|
- **System Architecture:** `docs/TELEGRAM_CMS_SYSTEM.md`
|
||||||
|
- **Workflow Details:** `n8n-workflows/ULTIMATE-Telegram-CMS-COMPLETE-README.md`
|
||||||
|
- **Quick Reference:** `n8n-workflows/QUICK-REFERENCE.md`
|
||||||
|
- **Testing Checklist:** `n8n-workflows/TESTING-CHECKLIST.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Quick Start (TL;DR)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Clone Repo
|
||||||
|
git clone <REPO_URL>
|
||||||
|
cd portfolio
|
||||||
|
|
||||||
|
# 2. Import Workflows
|
||||||
|
# → n8n UI → Import → Select:
|
||||||
|
# - ULTIMATE-Telegram-CMS-COMPLETE.json
|
||||||
|
# - Docker Event (Extended).json
|
||||||
|
# - Docker Event - Callback Handler.json
|
||||||
|
|
||||||
|
# 3. Create Gitea Token
|
||||||
|
# → https://git.dk0.dev/user/settings/applications
|
||||||
|
# → Name: n8n-api, Scope: repo
|
||||||
|
# → Copy token → n8n Credentials → HTTP Header Auth
|
||||||
|
|
||||||
|
# 4. Activate Workflows
|
||||||
|
# → n8n → Workflows → ✅ Active (alle 3)
|
||||||
|
|
||||||
|
# 5. Test
|
||||||
|
# → Telegram: /start
|
||||||
|
```
|
||||||
|
|
||||||
|
**Done!** 🎉
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Version:** 1.0.0
|
||||||
|
**Last Updated:** 2026-04-02
|
||||||
|
**Author:** Dennis Konkol
|
||||||
|
**Status:** ✅ Production Ready
|
||||||
154
docs/TELEGRAM_CMS_QUICKSTART.md
Normal file
154
docs/TELEGRAM_CMS_QUICKSTART.md
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
# 🚀 TELEGRAM CMS - QUICK START GUIDE
|
||||||
|
|
||||||
|
## Installation (5 Minutes)
|
||||||
|
|
||||||
|
### Step 1: Import Main Workflow
|
||||||
|
1. Open n8n: https://n8n.dk0.dev
|
||||||
|
2. Click "Workflows" → "Import from File"
|
||||||
|
3. Select: `n8n-workflows/ULTIMATE-Telegram-CMS-COMPLETE.json`
|
||||||
|
4. Workflow should auto-activate
|
||||||
|
|
||||||
|
### Step 2: Verify Credentials
|
||||||
|
Check these credentials exist (should be auto-mapped):
|
||||||
|
- ✅ Telegram: `DK0_Server`
|
||||||
|
- ✅ Directus: Bearer token `RF2Qytq...`
|
||||||
|
- ✅ OpenRouter: Bearer token `sk-or-v1-...`
|
||||||
|
|
||||||
|
### Step 3: Test Commands
|
||||||
|
Open Telegram bot and type:
|
||||||
|
```
|
||||||
|
/start
|
||||||
|
```
|
||||||
|
|
||||||
|
You should see the dashboard! 🎉
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 All Commands
|
||||||
|
|
||||||
|
| Command | Description | Example |
|
||||||
|
|---------|-------------|---------|
|
||||||
|
| `/start` | Main dashboard | `/start` |
|
||||||
|
| `/list projects` | Show draft projects | `/list projects` |
|
||||||
|
| `/list books` | Show pending reviews | `/list books` |
|
||||||
|
| `/search <term>` | Search everywhere | `/search nextjs` |
|
||||||
|
| `/stats` | Analytics dashboard | `/stats` |
|
||||||
|
| `/preview <ID>` | Preview item (EN+DE) | `/preview 42` |
|
||||||
|
| `/publish <ID>` | Publish to live site | `/publish 42` |
|
||||||
|
| `/delete <ID>` | Delete item | `/delete 42` |
|
||||||
|
| `/deletereview <ID>` | Delete book review | `/deletereview 3` |
|
||||||
|
| `.review <HC_ID> <RATING> <TEXT>` | Create book review | `.review427565 4 Great!` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 Companion Workflows (Auto-Import)
|
||||||
|
|
||||||
|
These workflows work together with the main CMS:
|
||||||
|
|
||||||
|
### 1. Docker Event Workflow
|
||||||
|
**File:** `Docker Event.json` (KEEP ACTIVE)
|
||||||
|
- Auto-detects new container deployments
|
||||||
|
- AI generates project descriptions
|
||||||
|
- Creates drafts in Directus
|
||||||
|
- Sends Telegram notification with buttons
|
||||||
|
|
||||||
|
### 2. Book Review Scheduler
|
||||||
|
**File:** `Book Review.json` (KEEP ACTIVE)
|
||||||
|
- Runs daily at 7 PM
|
||||||
|
- Checks for unreviewed books
|
||||||
|
- Sends AI-generated questions
|
||||||
|
- You reply with `.review` command
|
||||||
|
|
||||||
|
### 3. Finished Books Sync
|
||||||
|
**File:** `finishedBooks.json` (KEEP ACTIVE)
|
||||||
|
- Runs daily at 6 AM
|
||||||
|
- Syncs from Hardcover API
|
||||||
|
- Adds new books to Directus
|
||||||
|
|
||||||
|
### 4. Portfolio Status API
|
||||||
|
**File:** `portfolio-website.json` (KEEP ACTIVE)
|
||||||
|
- Real-time status endpoint
|
||||||
|
- Aggregates: Spotify + Discord + WakaTime
|
||||||
|
- Used by website for "Now" section
|
||||||
|
|
||||||
|
### 5. Currently Reading API
|
||||||
|
**File:** `reading (1).json` (KEEP ACTIVE)
|
||||||
|
- Webhook endpoint
|
||||||
|
- Fetches current books from Hardcover
|
||||||
|
- Returns formatted JSON
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Typical Workflows
|
||||||
|
|
||||||
|
### Publishing a New Project:
|
||||||
|
1. Deploy Docker container
|
||||||
|
2. Get Telegram notification: "🚀 New Deploy: portfolio-dev"
|
||||||
|
3. Click "🤖 Auto-generieren" button
|
||||||
|
4. AI creates draft
|
||||||
|
5. Get notification: "Draft created (ID: 42)"
|
||||||
|
6. Type: `/preview 42` to check translations
|
||||||
|
7. Type: `/publish 42` to go live
|
||||||
|
|
||||||
|
### Adding a Book Review:
|
||||||
|
1. Finish reading book on Hardcover
|
||||||
|
2. Get Telegram prompt at 7 PM: "📚 Review this book?"
|
||||||
|
3. Reply: `.review427565 4 Great world-building but rushed ending`
|
||||||
|
4. AI generates EN + DE reviews
|
||||||
|
5. Get notification: "Review draft created (ID: 3)"
|
||||||
|
6. Type: `/publish 3` to publish
|
||||||
|
|
||||||
|
### Quick Search:
|
||||||
|
1. Type: `/search suricata`
|
||||||
|
2. See all projects/books mentioning "suricata"
|
||||||
|
3. Click action buttons to manage
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🐛 Troubleshooting
|
||||||
|
|
||||||
|
### "Command not recognized"
|
||||||
|
- Check workflow is **Active** (toggle in n8n)
|
||||||
|
- Verify Telegram Trigger credential is set
|
||||||
|
|
||||||
|
### "Error fetching data"
|
||||||
|
- Check Directus is running: https://cms.dk0.dev
|
||||||
|
- Verify Bearer token in credentials
|
||||||
|
|
||||||
|
### "No button appears" (Docker workflow)
|
||||||
|
- Check `Docker Event - Callback Handler.json` is active
|
||||||
|
- Inline keyboard markup must be set correctly
|
||||||
|
|
||||||
|
### "AI generation fails"
|
||||||
|
- Check OpenRouter credit balance
|
||||||
|
- Model `openrouter/free` might be rate-limited, switch to `google/gemini-2.5-flash`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Monitoring
|
||||||
|
|
||||||
|
Check n8n Executions:
|
||||||
|
- n8n → Left menu → "Executions"
|
||||||
|
- Filter by workflow name
|
||||||
|
- Red = Failed (click to see error details)
|
||||||
|
- Green = Success
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Next Steps
|
||||||
|
|
||||||
|
1. **Test all commands** - Go through each one in Telegram
|
||||||
|
2. **Customize messages** - Edit text in Telegram nodes
|
||||||
|
3. **Add your own commands** - Extend the Switch node
|
||||||
|
4. **Set up monitoring** - Add error alerts to Slack/Discord
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📞 Support
|
||||||
|
|
||||||
|
If something breaks:
|
||||||
|
1. Check n8n Execution logs
|
||||||
|
2. Verify API credentials
|
||||||
|
3. Test Directus API manually: `curl https://cms.dk0.dev/items/projects`
|
||||||
|
|
||||||
|
**Your system is now LIVE!** 🎉
|
||||||
269
docs/TELEGRAM_CMS_SYSTEM.md
Normal file
269
docs/TELEGRAM_CMS_SYSTEM.md
Normal file
@@ -0,0 +1,269 @@
|
|||||||
|
# 🚀 ULTIMATE TELEGRAM CMS SYSTEM - Implementation Plan
|
||||||
|
|
||||||
|
**Status:** Ready to implement
|
||||||
|
**Duration:** ~15 minutes
|
||||||
|
**Completion:** 8/8 workflows
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 System Overview
|
||||||
|
|
||||||
|
Your portfolio will be **fully manageable via Telegram** with these features:
|
||||||
|
|
||||||
|
### ✅ Commands (All work via Telegram Bot)
|
||||||
|
|
||||||
|
| Command | Function | Example |
|
||||||
|
|---------|----------|---------|
|
||||||
|
| `/start` | Main dashboard with quick action buttons | - |
|
||||||
|
| `/list projects` | Show all draft projects | `/list projects` |
|
||||||
|
| `/list books` | Show pending book reviews | `/list books` |
|
||||||
|
| `/search <term>` | Search projects & books | `/search nextjs` |
|
||||||
|
| `/stats` | Analytics dashboard (views, trends) | `/stats` |
|
||||||
|
| `/preview <ID>` | Show EN + DE translations before publish | `/preview 42` |
|
||||||
|
| `/publish <ID>` | Publish project or book (auto-detects type) | `/publish 42` |
|
||||||
|
| `/delete <ID>` | Delete project or book | `/delete 42` |
|
||||||
|
| `/deletereview <ID>` | Delete specific book review translation | `/deletereview 3` |
|
||||||
|
| `.review <HC_ID> <RATING> <TEXT>` | Create AI-powered book review | `.review427565 4 Great book!` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📦 Workflow Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ 🤖 ULTIMATE TELEGRAM CMS (Master Router) │
|
||||||
|
│ Handles: /start, /list, /search, /stats, /preview, etc. │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
┌─────────────────────┼─────────────────────┐
|
||||||
|
│ │ │
|
||||||
|
┌────▼────┐ ┌────▼────┐ ┌────▼────┐
|
||||||
|
│ Docker │ │ Book │ │ Status │
|
||||||
|
│ Events │ │ Reviews │ │ API │
|
||||||
|
└─────────┘ └─────────┘ └─────────┘
|
||||||
|
Auto-creates AI prompts Spotify +
|
||||||
|
project drafts for reviews Discord +
|
||||||
|
WakaTime
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠️ Implementation Steps
|
||||||
|
|
||||||
|
### **1. Command Router** ✅ (DONE)
|
||||||
|
- File: `ULTIMATE-Telegram-CMS.json`
|
||||||
|
- Central command parser
|
||||||
|
- Switch routes to 10 different actions
|
||||||
|
|
||||||
|
### **2. /start Dashboard**
|
||||||
|
```telegram
|
||||||
|
🏠 Portfolio CMS Dashboard
|
||||||
|
|
||||||
|
📊 Quick Stats:
|
||||||
|
├─ 3 Draft Projects
|
||||||
|
├─ 2 Pending Reviews
|
||||||
|
└─ Last updated: 2 hours ago
|
||||||
|
|
||||||
|
⚡ Quick Actions:
|
||||||
|
┌────────────────┬────────────────┐
|
||||||
|
│ 📋 List Drafts │ 🔍 Search │
|
||||||
|
└────────────────┴────────────────┘
|
||||||
|
┌────────────────┬────────────────┐
|
||||||
|
│ 📈 Stats │ 🔄 Sync Now │
|
||||||
|
└────────────────┴────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### **3. /list Command**
|
||||||
|
```telegram
|
||||||
|
📋 Draft Projects (3):
|
||||||
|
|
||||||
|
1️⃣ #42 Portfolio Website
|
||||||
|
Category: webdev
|
||||||
|
Created: 2 days ago
|
||||||
|
/preview42 · /publish42 · /delete42
|
||||||
|
|
||||||
|
2️⃣ #38 Suricata IDS
|
||||||
|
Category: selfhosted
|
||||||
|
Created: 1 week ago
|
||||||
|
/preview38 · /publish38 · /delete38
|
||||||
|
|
||||||
|
───────────────────────────
|
||||||
|
/list books → See book reviews
|
||||||
|
```
|
||||||
|
|
||||||
|
### **4. /search Command**
|
||||||
|
```telegram
|
||||||
|
🔍 Search: "nextjs"
|
||||||
|
|
||||||
|
Found 2 results:
|
||||||
|
|
||||||
|
📦 Projects:
|
||||||
|
1. #42 - Portfolio Website (Next.js 15...)
|
||||||
|
|
||||||
|
📚 Books:
|
||||||
|
(none)
|
||||||
|
```
|
||||||
|
|
||||||
|
### **5. /stats Command**
|
||||||
|
```telegram
|
||||||
|
📈 Portfolio Stats (Last 30 Days)
|
||||||
|
|
||||||
|
🏆 Top Projects:
|
||||||
|
1. Portfolio Website - 1,240 views
|
||||||
|
2. Docker Setup - 820 views
|
||||||
|
3. Suricata IDS - 450 views
|
||||||
|
|
||||||
|
📚 Book Reviews:
|
||||||
|
├─ Total: 12 books
|
||||||
|
├─ This month: 3 reviews
|
||||||
|
└─ Avg rating: 4.2/5
|
||||||
|
|
||||||
|
⚡ Activity:
|
||||||
|
├─ Projects published: 5
|
||||||
|
├─ Drafts created: 8
|
||||||
|
└─ Reviews written: 3
|
||||||
|
```
|
||||||
|
|
||||||
|
### **6. /preview Command**
|
||||||
|
```telegram
|
||||||
|
👁️ Preview: Portfolio Website (#42)
|
||||||
|
|
||||||
|
🇬🇧 ENGLISH:
|
||||||
|
Title: Modern Portfolio with Next.js
|
||||||
|
Description: A responsive portfolio showcasing...
|
||||||
|
|
||||||
|
🇩🇪 DEUTSCH:
|
||||||
|
Title: Modernes Portfolio mit Next.js
|
||||||
|
Description: Ein responsives Portfolio das...
|
||||||
|
|
||||||
|
───────────────────────────
|
||||||
|
/publish42 · /delete42
|
||||||
|
```
|
||||||
|
|
||||||
|
### **7. Publish/Delete Logic**
|
||||||
|
- Auto-detects collection (projects vs book_reviews)
|
||||||
|
- Fetches item details from Directus
|
||||||
|
- Updates `status` field
|
||||||
|
- Sends confirmation with item title
|
||||||
|
|
||||||
|
### **8. AI Review Creator** ✅ (Already works!)
|
||||||
|
- `.review <HC_ID> <RATING> <TEXT>`
|
||||||
|
- Calls OpenRouter AI
|
||||||
|
- Generates EN + DE translations
|
||||||
|
- Creates draft in Directus
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 Technical Implementation
|
||||||
|
|
||||||
|
### **Workflow 1: ULTIMATE-Telegram-CMS.json**
|
||||||
|
**Nodes:**
|
||||||
|
1. Telegram Trigger (listens to messages)
|
||||||
|
2. Parse Command (regex matcher)
|
||||||
|
3. Switch Action (10 outputs)
|
||||||
|
4. Dashboard Node → Fetch stats from Directus
|
||||||
|
5. List Node → Query projects/books with pagination
|
||||||
|
6. Search Node → GraphQL search on Directus
|
||||||
|
7. Stats Node → Aggregate views/counts
|
||||||
|
8. Preview Node → Fetch translations
|
||||||
|
9. Publish Node → Update status field
|
||||||
|
10. Delete Node → Delete item + translations
|
||||||
|
|
||||||
|
### **Directus Collections Used:**
|
||||||
|
- `projects` (slug, title, category, status, technologies, translations)
|
||||||
|
- `book_reviews` (hardcover_id, rating, finished_at, translations)
|
||||||
|
- `tech_stack_categories` (name, technologies)
|
||||||
|
|
||||||
|
### **APIs Integrated:**
|
||||||
|
- ✅ Directus CMS (Bearer Token: `RF2Qytq...`)
|
||||||
|
- ✅ Hardcover.app (GraphQL)
|
||||||
|
- ✅ OpenRouter AI (Free models)
|
||||||
|
- ✅ Gitea (Self-hosted Git)
|
||||||
|
- ✅ Spotify, Discord Lanyard, Wakapi
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎨 Telegram UI Patterns
|
||||||
|
|
||||||
|
### **Inline Keyboards:**
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
"replyMarkup": "inlineKeyboard",
|
||||||
|
"inlineKeyboard": {
|
||||||
|
"rows": [
|
||||||
|
{
|
||||||
|
"buttons": [
|
||||||
|
{ "text": "📋 List", "callbackData": "list_projects" },
|
||||||
|
{ "text": "🔍 Search", "callbackData": "search_prompt" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Pagination:**
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
"buttons": [
|
||||||
|
{ "text": "◀️ Prev", "callbackData": "list_page:1" },
|
||||||
|
{ "text": "Page 2/5", "callbackData": "noop" },
|
||||||
|
{ "text": "▶️ Next", "callbackData": "list_page:3" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Implementation Checklist
|
||||||
|
|
||||||
|
- [x] Command parser with 10 actions
|
||||||
|
- [ ] Dashboard (/start) with stats
|
||||||
|
- [ ] List command (projects/books)
|
||||||
|
- [ ] Search command (fuzzy matching)
|
||||||
|
- [ ] Stats dashboard (views, trends)
|
||||||
|
- [ ] Preview command (EN + DE)
|
||||||
|
- [ ] Unified publish logic (auto-detect collection)
|
||||||
|
- [ ] Unified delete logic with confirmation
|
||||||
|
- [ ] Error handling (try-catch all API calls)
|
||||||
|
- [ ] Logging (audit trail in Directus)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Deployment Steps
|
||||||
|
|
||||||
|
1. **Import workflow:** n8n → Import `ULTIMATE-Telegram-CMS.json`
|
||||||
|
2. **Set credentials:**
|
||||||
|
- Telegram Bot: `DK0_Server` (already exists)
|
||||||
|
- Directus Bearer: `RF2Qytq...` (already exists)
|
||||||
|
3. **Activate workflow:** Toggle ON
|
||||||
|
4. **Test commands:**
|
||||||
|
```
|
||||||
|
/start
|
||||||
|
/list projects
|
||||||
|
/stats
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Future Enhancements
|
||||||
|
|
||||||
|
1. **Media Upload** - Send image → "For which project?" → Auto-upload
|
||||||
|
2. **Scheduled Publishing** - `/schedule <ID> <date>`
|
||||||
|
3. **Bulk Operations** - `/bulkpublish`, `/archive`
|
||||||
|
4. **Webhook Monitoring** - Alert if workflows fail
|
||||||
|
5. **Multi-language AI** - Switch between OpenRouter models
|
||||||
|
6. **Undo Command** - Revert last action
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 Notes
|
||||||
|
|
||||||
|
- Chat ID: `145931600` (hardcoded, change if needed)
|
||||||
|
- Timezone: Europe/Berlin (hardcoded in some workflows)
|
||||||
|
- AI Model: `openrouter/free` (cheapest, decent quality)
|
||||||
|
- Rate Limit: None (add if needed)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Ready to deploy?** Import `ULTIMATE-Telegram-CMS.json` into n8n and activate it!
|
||||||
260
n8n-docker-callback-workflow.json
Normal file
260
n8n-docker-callback-workflow.json
Normal file
@@ -0,0 +1,260 @@
|
|||||||
|
{
|
||||||
|
"name": "Docker Event - Callback Handler",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"updates": ["callback_query"]
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.telegramTrigger",
|
||||||
|
"typeVersion": 1.2,
|
||||||
|
"position": [0, 0],
|
||||||
|
"id": "telegram-trigger",
|
||||||
|
"name": "Telegram Trigger"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"jsCode": "const callback = $input.first().json;\nconst data = callback.callback_query?.data || '';\nconst chatId = callback.callback_query?.from?.id;\nconst messageId = callback.callback_query?.message?.message_id;\n\n// Parse: auto:slug, manual:slug, ignore:slug\nconst [action, slug] = data.split(':');\n\nreturn [{\n json: {\n action,\n slug,\n chatId,\n messageId,\n rawCallback: data\n }\n}];"
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.code",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"position": [220, 0],
|
||||||
|
"id": "parse-callback",
|
||||||
|
"name": "Parse Callback"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"rules": {
|
||||||
|
"values": [
|
||||||
|
{
|
||||||
|
"conditions": {
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true,
|
||||||
|
"leftValue": ""
|
||||||
|
},
|
||||||
|
"conditions": [
|
||||||
|
{
|
||||||
|
"leftValue": "={{ $json.action }}",
|
||||||
|
"rightValue": "auto",
|
||||||
|
"operator": {
|
||||||
|
"type": "string",
|
||||||
|
"operation": "equals"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"combinator": "and"
|
||||||
|
},
|
||||||
|
"renameOutput": true,
|
||||||
|
"outputKey": "Auto"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"conditions": {
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true,
|
||||||
|
"leftValue": ""
|
||||||
|
},
|
||||||
|
"conditions": [
|
||||||
|
{
|
||||||
|
"leftValue": "={{ $json.action }}",
|
||||||
|
"rightValue": "manual",
|
||||||
|
"operator": {
|
||||||
|
"type": "string",
|
||||||
|
"operation": "equals"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"combinator": "and"
|
||||||
|
},
|
||||||
|
"renameOutput": true,
|
||||||
|
"outputKey": "Manual"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"conditions": {
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true,
|
||||||
|
"leftValue": ""
|
||||||
|
},
|
||||||
|
"conditions": [
|
||||||
|
{
|
||||||
|
"leftValue": "={{ $json.action }}",
|
||||||
|
"rightValue": "ignore",
|
||||||
|
"operator": {
|
||||||
|
"type": "string",
|
||||||
|
"operation": "equals"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"combinator": "and"
|
||||||
|
},
|
||||||
|
"renameOutput": true,
|
||||||
|
"outputKey": "Ignore"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.switch",
|
||||||
|
"typeVersion": 3.2,
|
||||||
|
"position": [440, 0],
|
||||||
|
"id": "switch-action",
|
||||||
|
"name": "Switch Action"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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": [660, -200],
|
||||||
|
"id": "get-project-data",
|
||||||
|
"name": "Get Project from CMS"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"url": "=https://git.dk0.dev/api/v1/repos/denshooter/{{ $json.slug }}/commits?limit=3",
|
||||||
|
"authentication": "genericCredentialType",
|
||||||
|
"genericAuthType": "httpHeaderAuth",
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.httpRequest",
|
||||||
|
"typeVersion": 4.4,
|
||||||
|
"position": [880, -280],
|
||||||
|
"id": "get-commits-auto",
|
||||||
|
"name": "Get Commits"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"url": "=https://git.dk0.dev/api/v1/repos/denshooter/{{ $json.slug }}/contents/README.md",
|
||||||
|
"authentication": "genericCredentialType",
|
||||||
|
"genericAuthType": "httpHeaderAuth",
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.httpRequest",
|
||||||
|
"typeVersion": 4.4,
|
||||||
|
"position": [880, -160],
|
||||||
|
"id": "get-readme-auto",
|
||||||
|
"name": "Get README"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"model": "openrouter/free",
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"type": "@n8n/n8n-nodes-langchain.lmChatOpenRouter",
|
||||||
|
"typeVersion": 1,
|
||||||
|
"position": [1320, -100],
|
||||||
|
"id": "openrouter-model-auto",
|
||||||
|
"name": "OpenRouter Chat Model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"promptType": "define",
|
||||||
|
"text": "=Du bist ein technischer Autor für das Portfolio von Dennis (dk0.dev).\n\nNeues eigenes Projekt deployed:\nRepo: {{ $('Parse Callback').item.json.slug }}\n\nREADME:\n{{ $('Get README').first().json.content ? Buffer.from($('Get README').first().json.content, 'base64').toString('utf8').substring(0, 1000) : 'Kein README' }}\n\nLetzte Commits:\n{{ $('Get Commits').first().json.map(c => '- ' + c.commit.message).join('\\n') }}\n\nErstelle eine Portfolio-Beschreibung:\n- Was macht das Projekt (Features, Zweck)\n- Tech-Stack und Architektur\n- Highlights aus den Commits\n- Warum ist es cool/interessant\n\nKategorie: webdev (wenn Web-App), automation (wenn Tool/Script), oder selfhosted\n\nAntworte NUR als JSON:\n{\n \"title_en\": \"Aussagekräftiger Titel\",\n \"title_de\": \"Aussagekräftiger Titel\",\n \"description_en\": \"4-6 Sätze\",\n \"description_de\": \"4-6 Sätze\",\n \"content_en\": \"2-3 Absätze Markdown mit technischen Details\",\n \"content_de\": \"2-3 Absätze Markdown mit technischen Details\",\n \"category\": \"webdev|automation|selfhosted\",\n \"technologies\": [\"Next.js\", \"Docker\", \"...\"]\n}"
|
||||||
|
},
|
||||||
|
"type": "@n8n/n8n-nodes-langchain.chainLlm",
|
||||||
|
"typeVersion": 1.9,
|
||||||
|
"position": [1100, -200],
|
||||||
|
"id": "ai-auto",
|
||||||
|
"name": "AI: Generate Description"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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": [1320, -200],
|
||||||
|
"id": "parse-json-auto",
|
||||||
|
"name": "Parse JSON"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"jsCode": "const ai = $input.first().json;\nconst ctx = $('Parse Callback').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": [1540, -200],
|
||||||
|
"id": "add-to-directus-auto",
|
||||||
|
"name": "Add to Directus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"chatId": "={{ $('Parse Callback').item.json.chatId }}",
|
||||||
|
"text": "={{ \n'✅ Projekt erstellt: ' + $json.data.title + '\\n\\n' +\n'📝 ' + $('Parse JSON').first().json.description_de.substring(0, 200) + '...\\n\\n' +\n'Status: Draft (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": [1760, -200],
|
||||||
|
"id": "telegram-notify-auto",
|
||||||
|
"name": "Notify Success"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"chatId": "={{ $json.chatId }}",
|
||||||
|
"text": "✍️ OK, schreib mir jetzt was das Projekt macht (4-6 Sätze).\n\nIch formatiere das dann schön und erstelle einen Draft.",
|
||||||
|
"additionalFields": {}
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.telegram",
|
||||||
|
"typeVersion": 1.2,
|
||||||
|
"position": [660, 0],
|
||||||
|
"id": "telegram-ask-manual",
|
||||||
|
"name": "Ask for Manual Input"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"chatId": "={{ $json.chatId }}",
|
||||||
|
"text": "❌ OK, ignoriert.",
|
||||||
|
"additionalFields": {}
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.telegram",
|
||||||
|
"typeVersion": 1.2,
|
||||||
|
"position": [660, 200],
|
||||||
|
"id": "telegram-ignore",
|
||||||
|
"name": "Confirm Ignore"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"connections": {
|
||||||
|
"Telegram Trigger": {
|
||||||
|
"main": [[{ "node": "Parse Callback", "type": "main", "index": 0 }]]
|
||||||
|
},
|
||||||
|
"Parse Callback": {
|
||||||
|
"main": [[{ "node": "Switch Action", "type": "main", "index": 0 }]]
|
||||||
|
},
|
||||||
|
"Switch Action": {
|
||||||
|
"main": [
|
||||||
|
[{ "node": "Get Project from CMS", "type": "main", "index": 0 }],
|
||||||
|
[{ "node": "Ask for Manual Input", "type": "main", "index": 0 }],
|
||||||
|
[{ "node": "Confirm Ignore", "type": "main", "index": 0 }]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Get Project from CMS": {
|
||||||
|
"main": [[{ "node": "Get Commits", "type": "main", "index": 0 }]]
|
||||||
|
},
|
||||||
|
"Get Commits": {
|
||||||
|
"main": [[{ "node": "Get README", "type": "main", "index": 0 }]]
|
||||||
|
},
|
||||||
|
"Get README": {
|
||||||
|
"main": [[{ "node": "AI: Generate Description", "type": "main", "index": 0 }]]
|
||||||
|
},
|
||||||
|
"OpenRouter Chat Model": {
|
||||||
|
"ai_languageModel": [[{ "node": "AI: Generate Description", "type": "ai_languageModel", "index": 0 }]]
|
||||||
|
},
|
||||||
|
"AI: Generate Description": {
|
||||||
|
"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 Success", "type": "main", "index": 0 }]]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"active": false,
|
||||||
|
"settings": {
|
||||||
|
"executionOrder": "v1"
|
||||||
|
},
|
||||||
|
"id": "docker-event-callback"
|
||||||
|
}
|
||||||
372
n8n-docker-workflow-extended.json
Normal file
372
n8n-docker-workflow-extended.json
Normal 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"
|
||||||
|
}
|
||||||
120
n8n-review-separate-calls.js
Normal file
120
n8n-review-separate-calls.js
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
var d = $input.first().json;
|
||||||
|
|
||||||
|
// GET book from CMS
|
||||||
|
var book;
|
||||||
|
try {
|
||||||
|
var check = await this.helpers.httpRequest({
|
||||||
|
method: "GET",
|
||||||
|
url: "https://cms.dk0.dev/items/book_reviews",
|
||||||
|
headers: { Authorization: "Bearer RF2QytqhcLXuVy6FO3PzWlsoR-ysCTwB" },
|
||||||
|
qs: {
|
||||||
|
"filter[hardcover_id][_eq]": d.hardcoverId,
|
||||||
|
"fields": "id,book_title,book_author",
|
||||||
|
"limit": 1
|
||||||
|
}
|
||||||
|
});
|
||||||
|
book = check.data?.[0];
|
||||||
|
} catch (e) {
|
||||||
|
var errmsg = "❌ GET Fehler: " + e.message;
|
||||||
|
return [{ json: { msg: errmsg, chatId: d.chatId } }];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!book) {
|
||||||
|
var errmsg = "❌ Buch mit Hardcover ID " + d.hardcoverId + " nicht gefunden.";
|
||||||
|
return [{ json: { msg: errmsg, chatId: d.chatId } }];
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Book found:", book.book_title);
|
||||||
|
|
||||||
|
// Generate German review
|
||||||
|
var promptDe = "Schreibe eine persönliche Buchrezension (4-6 Sätze, Ich-Perspektive, nur Deutsch) zu '" + book.book_title + "' von " + book.book_author + ". Rating: " + d.rating + "/5. Meine Gedanken: " + d.answers + ". Formuliere professionell aber authentisch. NUR der Review-Text, kein JSON, kein Titel, keine Anführungszeichen drumherum.";
|
||||||
|
|
||||||
|
var reviewDe;
|
||||||
|
try {
|
||||||
|
console.log("Generating German review...");
|
||||||
|
var aiDe = await this.helpers.httpRequest({
|
||||||
|
method: "POST",
|
||||||
|
url: "https://openrouter.ai/api/v1/chat/completions",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: "Bearer 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"
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
model: "google/gemini-2.5-flash",
|
||||||
|
messages: [{ role: "user", content: promptDe }],
|
||||||
|
temperature: 0.7
|
||||||
|
}
|
||||||
|
});
|
||||||
|
reviewDe = aiDe.choices?.[0]?.message?.content?.trim() || d.answers;
|
||||||
|
console.log("German review generated:", reviewDe.substring(0, 100) + "...");
|
||||||
|
} catch (e) {
|
||||||
|
console.log("German AI error:", e.message);
|
||||||
|
reviewDe = d.answers;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate English review
|
||||||
|
var promptEn = "You are a professional book critic writing in ENGLISH ONLY. Write a personal book review (4-6 sentences, first person perspective) of '" + book.book_title + "' by " + book.book_author + ". Rating: " + d.rating + "/5 stars. Reader notes: " + d.answers + ". Write professionally but authentically. OUTPUT ONLY THE REVIEW TEXT IN ENGLISH, no JSON, no title, no quotes.";
|
||||||
|
|
||||||
|
var reviewEn;
|
||||||
|
try {
|
||||||
|
console.log("Generating English review...");
|
||||||
|
var aiEn = await this.helpers.httpRequest({
|
||||||
|
method: "POST",
|
||||||
|
url: "https://openrouter.ai/api/v1/chat/completions",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: "Bearer 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"
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
model: "openrouter/free",
|
||||||
|
messages: [
|
||||||
|
{ role: "system", content: "You are a book critic. You ALWAYS write in English, never in German." },
|
||||||
|
{ role: "user", content: promptEn }
|
||||||
|
],
|
||||||
|
temperature: 0.7
|
||||||
|
}
|
||||||
|
});
|
||||||
|
reviewEn = aiEn.choices?.[0]?.message?.content?.trim() || d.answers;
|
||||||
|
console.log("English review generated:", reviewEn.substring(0, 100) + "...");
|
||||||
|
} catch (e) {
|
||||||
|
console.log("English AI error:", e.message);
|
||||||
|
reviewEn = d.answers;
|
||||||
|
}
|
||||||
|
|
||||||
|
// PATCH book with reviews
|
||||||
|
try {
|
||||||
|
console.log("Patching book #" + book.id);
|
||||||
|
await this.helpers.httpRequest({
|
||||||
|
method: "PATCH",
|
||||||
|
url: "https://cms.dk0.dev/items/book_reviews/" + book.id,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: "Bearer RF2QytqhcLXuVy6FO3PzWlsoR-ysCTwB"
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
rating: d.rating,
|
||||||
|
status: "draft",
|
||||||
|
translations: {
|
||||||
|
create: [
|
||||||
|
{ languages_code: "en-US", review: reviewEn },
|
||||||
|
{ languages_code: "de-DE", review: reviewDe }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
console.log("PATCH success");
|
||||||
|
} catch (e) {
|
||||||
|
console.log("PATCH ERROR:", e.message);
|
||||||
|
var errmsg = "❌ PATCH Fehler: " + e.message;
|
||||||
|
return [{ json: { msg: errmsg, chatId: d.chatId } }];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build Telegram message (no emojis for better encoding)
|
||||||
|
var msg = "REVIEW: " + book.book_title + " - " + d.rating + "/5 Sterne";
|
||||||
|
msg = msg + "\n\n--- DEUTSCH ---\n" + reviewDe;
|
||||||
|
msg = msg + "\n\n--- ENGLISH ---\n" + reviewEn;
|
||||||
|
msg = msg + "\n\n==================";
|
||||||
|
msg = msg + "\n/publishbook" + book.id + " - Veroeffentlichen";
|
||||||
|
msg = msg + "\n/deletereview" + book.id + " - Loeschen und nochmal";
|
||||||
|
|
||||||
|
return [{ json: { msg: msg, chatId: d.chatId } }];
|
||||||
219
n8n-workflows/Book Review.json
Normal file
219
n8n-workflows/Book Review.json
Normal file
@@ -0,0 +1,219 @@
|
|||||||
|
{
|
||||||
|
"name": "Book Review",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"rule": {
|
||||||
|
"interval": [
|
||||||
|
{
|
||||||
|
"triggerAtHour": 19
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.scheduleTrigger",
|
||||||
|
"typeVersion": 1.3,
|
||||||
|
"position": [
|
||||||
|
0,
|
||||||
|
-192
|
||||||
|
],
|
||||||
|
"id": "f0c86dde-aa19-4440-b17c-c572b582da5e",
|
||||||
|
"name": "Schedule Trigger"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"method": "POST",
|
||||||
|
"url": "https://api.hardcover.app/v1/graphql",
|
||||||
|
"authentication": "predefinedCredentialType",
|
||||||
|
"nodeCredentialType": "httpBearerAuth",
|
||||||
|
"sendHeaders": true,
|
||||||
|
"headerParameters": {
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "content-type",
|
||||||
|
"value": "application/json"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"sendBody": true,
|
||||||
|
"bodyParameters": {
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "query",
|
||||||
|
"value": "query GetFinishedBooks { me { user_books(where: {status_id: {_eq: 3}}, limit: 5) { book { id title contributions { author { name } } images { url } } last_read_date updated_at } } }"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.httpRequest",
|
||||||
|
"typeVersion": 4.4,
|
||||||
|
"position": [
|
||||||
|
224,
|
||||||
|
-192
|
||||||
|
],
|
||||||
|
"id": "e5c28f64-29ed-40ae-804e-896c10f3bc58",
|
||||||
|
"name": "HTTP Request",
|
||||||
|
"credentials": {
|
||||||
|
"httpBearerAuth": {
|
||||||
|
"id": "Kmf2fBCFkuRuWWZa",
|
||||||
|
"name": "Hardcover"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"jsCode": "const responseData = $input.first().json;\nconst meData = responseData?.data?.me;\nconst userBooks =\n (Array.isArray(meData) && meData[0]?.user_books) || meData?.user_books || [];\n\nconst newBooks = [];\n\nfor (const ub of userBooks) {\n const check = await this.helpers.httpRequest({\n method: \"GET\",\n url:\n \"https://cms.dk0.dev/items/book_reviews?filter[hardcover_id][_eq]=\" +\n ub.book.id +\n \"&fields=id,translations.id&limit=1\",\n headers: {\n Authorization: \"Bearer RF2QytqhcLXuVy6FO3PzWlsoR-ysCTwB\",\n },\n });\n\n const existing = check.data?.[0];\n const hasReview =\n existing && existing.translations && existing.translations.length > 0;\n\n if (!hasReview) {\n newBooks.push({\n json: {\n hardcover_id: String(ub.book.id),\n directus_id: existing ? existing.id : null,\n title: ub.book.title,\n author: ub.book.contributions?.[0]?.author?.name ?? \"Unknown\",\n image: ub.book.images?.[0]?.url ?? null,\n finished_at: ub.last_read_date ?? ub.updated_at ?? null,\n already_in_directus: !!existing,\n },\n });\n }\n}\n\nreturn newBooks.length > 0 ? newBooks[0] : [{ json: { skip: true } }];\n"
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.code",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"position": [
|
||||||
|
448,
|
||||||
|
-192
|
||||||
|
],
|
||||||
|
"id": "60380362-e954-40ee-b0d0-7bc1edbaf9d3",
|
||||||
|
"name": "Filter books"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"conditions": {
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true,
|
||||||
|
"leftValue": "",
|
||||||
|
"typeValidation": "strict",
|
||||||
|
"version": 3
|
||||||
|
},
|
||||||
|
"conditions": [
|
||||||
|
{
|
||||||
|
"id": "b356ade3-5cf0-40dd-bb47-e977f354e803",
|
||||||
|
"leftValue": "={{ $json.skip }}",
|
||||||
|
"rightValue": "={{ $json.skip }}",
|
||||||
|
"operator": {
|
||||||
|
"type": "boolean",
|
||||||
|
"operation": "true",
|
||||||
|
"singleValue": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"combinator": "and"
|
||||||
|
},
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.if",
|
||||||
|
"typeVersion": 2.3,
|
||||||
|
"position": [
|
||||||
|
672,
|
||||||
|
-192
|
||||||
|
],
|
||||||
|
"id": "45f65c65-ae6a-46b0-9d96-46f0a32e59db",
|
||||||
|
"name": "If"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"jsCode": "const book = $input.first().json;\nif (book.skip) return [{ json: { skip: true } }];\n\nconst parts = [];\nparts.push(\"Du hilfst jemandem eine Buchbewertung zu schreiben.\");\nparts.push(\"Das Buch ist \" + book.title + \" von \" + book.author + \".\");\nparts.push(\"Erstelle 4 kurze spezifische Fragen zum Buch.\");\nparts.push(\"Die Fragen sollen helfen eine Review zu schreiben.\");\nparts.push(\"Frage auf Deutsch.\");\nparts.push(\"Antworte NUR als JSON Array mit 4 Strings.\");\nconst prompt = parts.join(\" \");\n\nconst aiResponse = await this.helpers.httpRequest({\n method: \"POST\",\n url: \"https://openrouter.ai/api/v1/chat/completions\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: \"Bearer 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\",\n },\n body: {\n model: \"openrouter/free\",\n messages: [{ role: \"user\", content: prompt }],\n },\n});\n\nconst aiText = aiResponse.choices?.[0]?.message?.content ?? \"[]\";\nconst match = aiText.match(/\\[[\\s\\S]*\\]/);\n\nconst f1 = \"Wie hat dir das Buch gefallen?\";\nconst f2 = \"Was war der beste Teil?\";\nconst f3 = \"Was hast du mitgenommen?\";\nconst f4 = \"Wem empfiehlst du es?\";\nconst fallback = [f1, f2, f3, f4];\n\nconst questions = match ? JSON.parse(match[0]) : fallback;\n\nreturn [{ json: { ...book, questions } }];\n"
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.code",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"position": [
|
||||||
|
896,
|
||||||
|
-192
|
||||||
|
],
|
||||||
|
"id": "b56ab681-90d8-4376-9408-dc3302ab55bd",
|
||||||
|
"name": "ai"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"chatId": "145931600",
|
||||||
|
"text": "={{ '📚 ' + $json.title + ' von ' + $json.author + '\\n\\nBeantworte bitte:\\n\\n1. ' + $json.questions[0] + '\\n2. ' + $json.questions[1] + '\\n3. ' + $json.questions[2] + '\\n4. ' + $json.questions[3] + '\\n\\n⭐ Bewertung (1-5)?\\n\\nAntworte so (kopiere und ergänze):\\n\\n/review' + $json.hardcover_id + ' Hier deine Antworten als Text' }}",
|
||||||
|
"additionalFields": {}
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.telegram",
|
||||||
|
"typeVersion": 1.2,
|
||||||
|
"position": [
|
||||||
|
1136,
|
||||||
|
-208
|
||||||
|
],
|
||||||
|
"id": "13087afe-8a1d-457f-a1f1-e0aa64fc0e26",
|
||||||
|
"name": "Send a text message",
|
||||||
|
"webhookId": "eaa44b55-b3b1-4747-9b6a-dfc920910b4b",
|
||||||
|
"credentials": {
|
||||||
|
"telegramApi": {
|
||||||
|
"id": "ADurvy9EKUDzbDdq",
|
||||||
|
"name": "DK0_Server"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"pinData": {},
|
||||||
|
"connections": {
|
||||||
|
"Schedule Trigger": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "HTTP Request",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"HTTP Request": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Filter books",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Filter books": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "If",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"If": {
|
||||||
|
"main": [
|
||||||
|
[],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "ai",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"ai": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Send a text message",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"active": true,
|
||||||
|
"settings": {
|
||||||
|
"executionOrder": "v1",
|
||||||
|
"binaryMode": "separate",
|
||||||
|
"availableInMCP": false
|
||||||
|
},
|
||||||
|
"versionId": "4c605d70-0428-4611-9ad8-d9452c2660a7",
|
||||||
|
"meta": {
|
||||||
|
"templateCredsSetupCompleted": true,
|
||||||
|
"instanceId": "cb28e4db755465d5826da179e87f69603d81f833414cc52c327be9183a217b8d"
|
||||||
|
},
|
||||||
|
"id": "FDQ5Qmk9POy4Ajdd",
|
||||||
|
"tags": []
|
||||||
|
}
|
||||||
935
n8n-workflows/Docker Event (Extended).json
Normal file
935
n8n-workflows/Docker Event (Extended).json
Normal file
@@ -0,0 +1,935 @@
|
|||||||
|
{
|
||||||
|
"name": "Docker Event (Extended)",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"httpMethod": "POST",
|
||||||
|
"path": "docker-event",
|
||||||
|
"responseMode": "responseNode",
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.webhook",
|
||||||
|
"typeVersion": 2.1,
|
||||||
|
"position": [
|
||||||
|
0,
|
||||||
|
-224
|
||||||
|
],
|
||||||
|
"id": "870fa550-42f6-4e19-a796-f1f044b0cdc8",
|
||||||
|
"name": "Webhook",
|
||||||
|
"webhookId": "e147d70b-79d8-44fd-bbe8-8274cf905b11",
|
||||||
|
"disabled": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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, '-');\n\nconst serviceName = container.replace(/[-_]/g, ' ');\n\nreturn [{\n json: {\n container,\n image,\n serviceName,\n timestamp,\n slug \n }\n}];"
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.code",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"position": [
|
||||||
|
224,
|
||||||
|
-224
|
||||||
|
],
|
||||||
|
"id": "aaa6a678-1ad3-4f82-9b01-37e21b47b189",
|
||||||
|
"name": "Kontext aufbereiten",
|
||||||
|
"disabled": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"conditions": {
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true,
|
||||||
|
"leftValue": "",
|
||||||
|
"typeValidation": "loose",
|
||||||
|
"version": 3
|
||||||
|
},
|
||||||
|
"conditions": [
|
||||||
|
{
|
||||||
|
"id": "ebe26f0c-d5a7-45c9-9747-afc75b57a41c",
|
||||||
|
"leftValue": "={{ $json.data }}",
|
||||||
|
"rightValue": "[]",
|
||||||
|
"operator": {
|
||||||
|
"type": "string",
|
||||||
|
"operation": "notEndsWith"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"combinator": "and"
|
||||||
|
},
|
||||||
|
"looseTypeValidation": true,
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.if",
|
||||||
|
"typeVersion": 2.3,
|
||||||
|
"position": [
|
||||||
|
672,
|
||||||
|
-224
|
||||||
|
],
|
||||||
|
"id": "62197a33-5169-48e1-9539-57c047efb108",
|
||||||
|
"name": "If",
|
||||||
|
"disabled": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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": [
|
||||||
|
448,
|
||||||
|
-224
|
||||||
|
],
|
||||||
|
"id": "db783886-06b5-4473-8907-dd6c655aa3dd",
|
||||||
|
"name": "Search for Slug",
|
||||||
|
"credentials": {
|
||||||
|
"httpBearerAuth": {
|
||||||
|
"id": "ZtI5e08iryR9m6FG",
|
||||||
|
"name": "Directus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"disabled": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"model": "openrouter/free",
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"type": "@n8n/n8n-nodes-langchain.lmChatOpenRouter",
|
||||||
|
"typeVersion": 1,
|
||||||
|
"position": [
|
||||||
|
976,
|
||||||
|
16
|
||||||
|
],
|
||||||
|
"id": "b9130ff4-359b-4736-9442-1b0ca7d31877",
|
||||||
|
"name": "OpenRouter Chat Model",
|
||||||
|
"credentials": {
|
||||||
|
"openRouterApi": {
|
||||||
|
"id": "8Kdy4RHHwMZ0Cn6x",
|
||||||
|
"name": "OpenRouter"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"disabled": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"promptType": "define",
|
||||||
|
"text": "= Du bist ein technischer Autor für das Self-Hosting Portfolio von Dennis auf dk0.dev.\n Ein neuer Service wurde auf dem Server deployed:\n \n Container: {{ $('Kontext aufbereiten').item.json.container }}\n Image: {{ $('Kontext aufbereiten').item.json.image }}\n Service: {{ $('Kontext aufbereiten').item.json.serviceName }}\n \n Aufgabe:\n 1. Erkenne ob es sich um ein EIGENES Projekt (z.B. Image enthält \"denshooter\", \"dk0\", \"portfolio\") oder eine \nSELF-HOSTED App handelt.\n 2. Bewerte die \"Coolness\" (1-10) basierend auf:\n - Eigener Code = +3 Punkte\n - Neue/spannende Technologie = +2 Punkte\n - Großes/bekanntes Projekt (Suricata, CrowdStrike-Level) = +3 Punkte\n - Standard Self-Hosted Tool (Nextcloud, Plausible) = +1 Punkt\n - CI/CD Build-Container, Test-Runner = 0 Punkte (ignorieren)\n 3. Erstelle Beschreibung NUR wenn coolness_score >= 6\n \n Antworte NUR als valides JSON:\n {\n \"coolness_score\": 1-10,\n \"notify\": true/false (true wenn >= 7),\n \"reason\": \"Kurze Begründung warum cool oder nicht\",\n \"type\": \"own\" oder \"selfhosted\" oder \"ignore\",\n \"title_en\": \"...\",\n \"title_de\": \"...\",\n \"description_en\": \"...\",\n \"description_de\": \"...\",\n \"content_en\": \"...\",\n \"content_de\": \"...\",\n \"category\": \"selfhosted\" oder \"webdev\" oder \"automation\",\n \"technologies\": [\"Docker\", \"...\"]\n }",
|
||||||
|
"batching": {}
|
||||||
|
},
|
||||||
|
"type": "@n8n/n8n-nodes-langchain.chainLlm",
|
||||||
|
"typeVersion": 1.9,
|
||||||
|
"position": [
|
||||||
|
896,
|
||||||
|
-224
|
||||||
|
],
|
||||||
|
"id": "77d46075-3342-4e93-8806-07087a2389dc",
|
||||||
|
"name": "Basic LLM Chain",
|
||||||
|
"disabled": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"jsCode": "const raw = $input.first().json.text ?? \"\";\n\nconst match = raw.match(/\\{[\\s\\S]*\\}/);\nif (!match) throw new Error(\"No JSON found\");\n\nconst ai = JSON.parse(match[0]);\n\nreturn [\n {\n json: ai,\n },\n];\n"
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.code",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"position": [
|
||||||
|
1248,
|
||||||
|
-224
|
||||||
|
],
|
||||||
|
"id": "de5ed311-0d46-4677-963c-711a6ad514e9",
|
||||||
|
"name": "Parse JSON",
|
||||||
|
"disabled": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"jsCode": "const ai = $('Parse JSON').first().json;\n const ctx = $('Kontext aufbereiten').first().json;\n\n const 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\n const 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\n return [{ json: response }];"
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.code",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"position": [
|
||||||
|
1680,
|
||||||
|
-224
|
||||||
|
],
|
||||||
|
"id": "c47b915d-e4d7-43e9-8ee3-b41389896fa7",
|
||||||
|
"name": "Add to Directus",
|
||||||
|
"disabled": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"respondWith": "json",
|
||||||
|
"responseBody": "{ \"success\": true }",
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.respondToWebhook",
|
||||||
|
"typeVersion": 1.5,
|
||||||
|
"position": [
|
||||||
|
2128,
|
||||||
|
-224
|
||||||
|
],
|
||||||
|
"id": "6cf8f30d-1352-466f-9163-9b4f16b972e0",
|
||||||
|
"name": "Respond to Webhook",
|
||||||
|
"disabled": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"chatId": "145931600",
|
||||||
|
"text": "={{ \n'🆕 Neuer Service erkannt!\\n\\n' +\n'📦 ' + $('Kontext aufbereiten').first().json.container + '\\n' +\n'🐳 ' + $('Kontext aufbereiten').first().json.image + '\\n\\n' +\n'📝 ' + $('Parse JSON').first().json.title_de + '\\n' + \n$('Parse JSON').first().json.description_de + '\\n\\n' +\n'Status: Draft in Directus erstellt (ID: ' + $json.data.id + ')\\n\\n' +\n('/publishproject_' + $json.data.id).replace(/_/g, '\\\\_') + ' — Veröffentlichen\\n' + \n('/deleteproject_' + $json.data.id).replace(/_/g, '\\\\_') + ' — Löschen' \n}}",
|
||||||
|
"additionalFields": {}
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.telegram",
|
||||||
|
"typeVersion": 1.2,
|
||||||
|
"position": [
|
||||||
|
1904,
|
||||||
|
-224
|
||||||
|
],
|
||||||
|
"id": "b29de3ec-b1ca-40c3-8493-af44e5372fd2",
|
||||||
|
"name": "Send a text message",
|
||||||
|
"webhookId": "c02ccf69-16dc-436e-b1cc-f8fa9dd8d33f",
|
||||||
|
"credentials": {
|
||||||
|
"telegramApi": {
|
||||||
|
"id": "ADurvy9EKUDzbDdq",
|
||||||
|
"name": "DK0_Server"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"disabled": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"rules": {
|
||||||
|
"values": [
|
||||||
|
{
|
||||||
|
"conditions": {
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true,
|
||||||
|
"leftValue": "",
|
||||||
|
"typeValidation": "strict",
|
||||||
|
"version": 3
|
||||||
|
},
|
||||||
|
"conditions": [
|
||||||
|
{
|
||||||
|
"leftValue": "={{ $json.notify }}",
|
||||||
|
"rightValue": "true",
|
||||||
|
"operator": {
|
||||||
|
"type": "boolean",
|
||||||
|
"operation": "true",
|
||||||
|
"singleValue": true
|
||||||
|
},
|
||||||
|
"id": "febc397c-b060-4a66-ab9b-1274c8509cc2"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"combinator": "and"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.switch",
|
||||||
|
"typeVersion": 3.4,
|
||||||
|
"position": [
|
||||||
|
1456,
|
||||||
|
-224
|
||||||
|
],
|
||||||
|
"id": "5ade115f-e134-4358-8d95-a144eede8d9a",
|
||||||
|
"name": "Switch",
|
||||||
|
"disabled": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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": [
|
||||||
|
896,
|
||||||
|
768
|
||||||
|
],
|
||||||
|
"id": "fb34f047-5c11-4255-9b45-adb9fe169042",
|
||||||
|
"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": [
|
||||||
|
1120,
|
||||||
|
768
|
||||||
|
],
|
||||||
|
"id": "acd7a411-2465-4aa3-a7ee-442a79c500f2",
|
||||||
|
"name": "Check if Exists",
|
||||||
|
"credentials": {
|
||||||
|
"httpBearerAuth": {
|
||||||
|
"id": "ZtI5e08iryR9m6FG",
|
||||||
|
"name": "Directus"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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": [
|
||||||
|
1344,
|
||||||
|
768
|
||||||
|
],
|
||||||
|
"id": "bdcddb94-8676-4467-a370-ad2cf07d09a3",
|
||||||
|
"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": [
|
||||||
|
1568,
|
||||||
|
768
|
||||||
|
],
|
||||||
|
"id": "00786826-8d6b-4e17-aa7f-1afdca38d7a3",
|
||||||
|
"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": [
|
||||||
|
1776,
|
||||||
|
560
|
||||||
|
],
|
||||||
|
"id": "9ef7f66b-3054-4765-b0a8-7ebb6aa353aa",
|
||||||
|
"name": "Get Last Commit",
|
||||||
|
"credentials": {
|
||||||
|
"httpHeaderAuth": {
|
||||||
|
"id": "YN3oIbok6Fjy5WNW",
|
||||||
|
"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": [
|
||||||
|
1840,
|
||||||
|
672
|
||||||
|
],
|
||||||
|
"id": "114fece9-c5f1-4c6b-8272-6f39fb8ce24a",
|
||||||
|
"name": "Get README",
|
||||||
|
"credentials": {
|
||||||
|
"httpHeaderAuth": {
|
||||||
|
"id": "YN3oIbok6Fjy5WNW",
|
||||||
|
"name": "gitea api"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"jsCode": "const ctx = $('Parse Context').first().json;\nconst commit = $('Get Last Commit').first().json[0];\nconst readme = $('Get README').first().json;\n\n// Decode README (base64)\nlet readmeText = '';\ntry {\n readmeText = Buffer.from(readme.content, 'base64').toString('utf8');\n // First 500 chars\n readmeText = readmeText.substring(0, 500).replace(/\\n/g, ' ');\n} catch (e) {\n readmeText = 'No README available';\n}\n\nconst commitMsg = commit?.commit?.message || 'No recent commits';\nconst commitAuthor = commit?.commit?.author?.name || 'Unknown';\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": [
|
||||||
|
2192,
|
||||||
|
480
|
||||||
|
],
|
||||||
|
"id": "8810426d-c146-42c9-8ec2-5d8f56934a1f",
|
||||||
|
"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}}",
|
||||||
|
"replyMarkup": "inlineKeyboard",
|
||||||
|
"inlineKeyboard": {
|
||||||
|
"rows": [
|
||||||
|
{
|
||||||
|
"row": {
|
||||||
|
"buttons": [
|
||||||
|
{
|
||||||
|
"text": "Selbst beschreiben",
|
||||||
|
"additionalFields": {
|
||||||
|
"callback_data": "={{ 'manual:' + $json.slug }}"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Auto-generieren",
|
||||||
|
"additionalFields": {
|
||||||
|
"callback_data": "={{ 'ignore:' + $json.slug }}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"additionalFields": {}
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.telegram",
|
||||||
|
"typeVersion": 1.2,
|
||||||
|
"position": [
|
||||||
|
2544,
|
||||||
|
592
|
||||||
|
],
|
||||||
|
"id": "d4016ea3-7233-4926-af21-c7b07cc5f39d",
|
||||||
|
"name": "Ask via Telegram",
|
||||||
|
"webhookId": "313376d7-33a6-4c80-938b-e8ebc7ee2d11",
|
||||||
|
"credentials": {
|
||||||
|
"telegramApi": {
|
||||||
|
"id": "ADurvy9EKUDzbDdq",
|
||||||
|
"name": "DK0_Server"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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}",
|
||||||
|
"batching": {}
|
||||||
|
},
|
||||||
|
"type": "@n8n/n8n-nodes-langchain.chainLlm",
|
||||||
|
"typeVersion": 1.9,
|
||||||
|
"position": [
|
||||||
|
1952,
|
||||||
|
864
|
||||||
|
],
|
||||||
|
"id": "0fd46a9d-40a9-4bb7-be5e-9b32b9a96381",
|
||||||
|
"name": "AI: Self-Hosted"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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": [
|
||||||
|
2656,
|
||||||
|
848
|
||||||
|
],
|
||||||
|
"id": "bfaca06b-65ca-41a8-ba8a-1b1aef7ba12d",
|
||||||
|
"name": "Notify Selfhosted",
|
||||||
|
"webhookId": "a7d15c96-41e1-4242-9b5f-0382f4f0d31a",
|
||||||
|
"credentials": {
|
||||||
|
"telegramApi": {
|
||||||
|
"id": "ADurvy9EKUDzbDdq",
|
||||||
|
"name": "DK0_Server"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"respondWith": "json",
|
||||||
|
"responseBody": "{ \"success\": true, \"message\": \"CI/CD container ignored\" }",
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.respondToWebhook",
|
||||||
|
"typeVersion": 1.5,
|
||||||
|
"position": [
|
||||||
|
1776,
|
||||||
|
960
|
||||||
|
],
|
||||||
|
"id": "d93818d9-64f9-4f57-ae84-c4280eeb50f0",
|
||||||
|
"name": "Respond (Ignore)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"respondWith": "json",
|
||||||
|
"responseBody": "{ \"success\": true }",
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.respondToWebhook",
|
||||||
|
"typeVersion": 1.5,
|
||||||
|
"position": [
|
||||||
|
2880,
|
||||||
|
768
|
||||||
|
],
|
||||||
|
"id": "4f1ad083-e73a-497c-a724-673205254b34",
|
||||||
|
"name": "Respond"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"respondWith": "json",
|
||||||
|
"responseBody": "{ \"success\": true, \"message\": \"Project already exists\" }",
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.respondToWebhook",
|
||||||
|
"typeVersion": 1.5,
|
||||||
|
"position": [
|
||||||
|
1568,
|
||||||
|
960
|
||||||
|
],
|
||||||
|
"id": "0b93b3c7-c158-4389-af18-b418aa3b2239",
|
||||||
|
"name": "Respond (Exists)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"httpMethod": "POST",
|
||||||
|
"path": "docker-event",
|
||||||
|
"responseMode": "responseNode",
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.webhook",
|
||||||
|
"typeVersion": 2.1,
|
||||||
|
"position": [
|
||||||
|
688,
|
||||||
|
768
|
||||||
|
],
|
||||||
|
"id": "2b1c77d4-9f7f-4758-9e8e-f88195448ba3",
|
||||||
|
"name": "Webhook1",
|
||||||
|
"webhookId": "25d94042-2088-4e09-bfae-645db3d6803f"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"model": "openrouter/free",
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"type": "@n8n/n8n-nodes-langchain.lmChatOpenRouter",
|
||||||
|
"typeVersion": 1,
|
||||||
|
"position": [
|
||||||
|
1968,
|
||||||
|
1072
|
||||||
|
],
|
||||||
|
"id": "a450227f-f1e5-44f3-a90e-044420042fc4",
|
||||||
|
"name": "OpenRouter Chat Model1",
|
||||||
|
"credentials": {
|
||||||
|
"openRouterApi": {
|
||||||
|
"id": "8Kdy4RHHwMZ0Cn6x",
|
||||||
|
"name": "OpenRouter"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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": [
|
||||||
|
2224,
|
||||||
|
848
|
||||||
|
],
|
||||||
|
"id": "ca78ecdd-5520-4540-969b-9e7b77bac3b4",
|
||||||
|
"name": "Parse JSON1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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": [
|
||||||
|
2448,
|
||||||
|
848
|
||||||
|
],
|
||||||
|
"id": "1ac0a31c-68a1-44df-a6b3-203698318cbf",
|
||||||
|
"name": "Add to Directus1"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"pinData": {},
|
||||||
|
"connections": {
|
||||||
|
"Webhook": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Kontext aufbereiten",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Kontext aufbereiten": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Search for Slug",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"If": {
|
||||||
|
"main": [
|
||||||
|
[],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Basic LLM Chain",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Search for Slug": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "If",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"OpenRouter Chat Model": {
|
||||||
|
"ai_languageModel": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Basic LLM Chain",
|
||||||
|
"type": "ai_languageModel",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Basic LLM Chain": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Parse JSON",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Parse JSON": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Switch",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Add to Directus": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Send a text message",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Send a text message": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Respond to Webhook",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Switch": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Add to Directus",
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"AI: Self-Hosted": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Parse JSON1",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Notify Selfhosted": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Respond",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Webhook1": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Parse Context",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"OpenRouter Chat Model1": {
|
||||||
|
"ai_languageModel": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "AI: Self-Hosted",
|
||||||
|
"type": "ai_languageModel",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Parse JSON1": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Add to Directus1",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Add to Directus1": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Notify Selfhosted",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"active": true,
|
||||||
|
"settings": {
|
||||||
|
"executionOrder": "v1",
|
||||||
|
"binaryMode": "separate",
|
||||||
|
"availableInMCP": false
|
||||||
|
},
|
||||||
|
"versionId": "1e2cf0ca-fe15-4a10-9716-30f85a2c2531",
|
||||||
|
"meta": {
|
||||||
|
"templateCredsSetupCompleted": true,
|
||||||
|
"instanceId": "cb28e4db755465d5826da179e87f69603d81f833414cc52c327be9183a217b8d"
|
||||||
|
},
|
||||||
|
"id": "RARR6MAlJSHAmBp8",
|
||||||
|
"tags": []
|
||||||
|
}
|
||||||
417
n8n-workflows/Docker Event - Callback Handler.json
Normal file
417
n8n-workflows/Docker Event - Callback Handler.json
Normal file
@@ -0,0 +1,417 @@
|
|||||||
|
{
|
||||||
|
"name": "Docker Event - Callback Handler",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"updates": [
|
||||||
|
"callback_query"
|
||||||
|
],
|
||||||
|
"additionalFields": {}
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.telegramTrigger",
|
||||||
|
"typeVersion": 1.2,
|
||||||
|
"position": [
|
||||||
|
-880,
|
||||||
|
288
|
||||||
|
],
|
||||||
|
"id": "a56a5174-3ccf-492f-810b-117be933560c",
|
||||||
|
"name": "Telegram Trigger",
|
||||||
|
"webhookId": "6e70b9ab-b76b-48dc-8e4d-5fe1bf0d7e39",
|
||||||
|
"credentials": {
|
||||||
|
"telegramApi": {
|
||||||
|
"id": "ADurvy9EKUDzbDdq",
|
||||||
|
"name": "DK0_Server"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"jsCode": "const callback = $input.first().json;\nconst data = callback.callback_query?.data || '';\nconst chatId = callback.callback_query?.from?.id;\nconst messageId = callback.callback_query?.message?.message_id;\n\n// Parse: auto:slug, manual:slug, ignore:slug\nconst [action, slug] = data.split(':');\n\nreturn [{\n json: {\n action,\n slug,\n chatId,\n messageId,\n rawCallback: data\n }\n}];"
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.code",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"position": [
|
||||||
|
-656,
|
||||||
|
288
|
||||||
|
],
|
||||||
|
"id": "10e5a475-4194-4919-9186-1eb052fbd79b",
|
||||||
|
"name": "Parse Callback"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"rules": {
|
||||||
|
"values": [
|
||||||
|
{
|
||||||
|
"conditions": {
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true,
|
||||||
|
"leftValue": ""
|
||||||
|
},
|
||||||
|
"conditions": [
|
||||||
|
{
|
||||||
|
"leftValue": "={{ $json.action }}",
|
||||||
|
"rightValue": "auto",
|
||||||
|
"operator": {
|
||||||
|
"type": "string",
|
||||||
|
"operation": "equals"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"combinator": "and"
|
||||||
|
},
|
||||||
|
"renameOutput": true,
|
||||||
|
"outputKey": "Auto"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"conditions": {
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true,
|
||||||
|
"leftValue": ""
|
||||||
|
},
|
||||||
|
"conditions": [
|
||||||
|
{
|
||||||
|
"leftValue": "={{ $json.action }}",
|
||||||
|
"rightValue": "manual",
|
||||||
|
"operator": {
|
||||||
|
"type": "string",
|
||||||
|
"operation": "equals"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"combinator": "and"
|
||||||
|
},
|
||||||
|
"renameOutput": true,
|
||||||
|
"outputKey": "Manual"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"conditions": {
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true,
|
||||||
|
"leftValue": ""
|
||||||
|
},
|
||||||
|
"conditions": [
|
||||||
|
{
|
||||||
|
"leftValue": "={{ $json.action }}",
|
||||||
|
"rightValue": "ignore",
|
||||||
|
"operator": {
|
||||||
|
"type": "string",
|
||||||
|
"operation": "equals"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"combinator": "and"
|
||||||
|
},
|
||||||
|
"renameOutput": true,
|
||||||
|
"outputKey": "Ignore"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.switch",
|
||||||
|
"typeVersion": 3.2,
|
||||||
|
"position": [
|
||||||
|
-448,
|
||||||
|
288
|
||||||
|
],
|
||||||
|
"id": "a533e527-b3c5-4946-9a26-6f499c7dd6c5",
|
||||||
|
"name": "Switch Action"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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": [
|
||||||
|
-224,
|
||||||
|
80
|
||||||
|
],
|
||||||
|
"id": "9fc55503-e890-4074-9823-f07001b6948a",
|
||||||
|
"name": "Get Project from CMS"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"url": "=https://git.dk0.dev/api/v1/repos/denshooter/{{ $json.slug }}/commits?limit=3",
|
||||||
|
"authentication": "genericCredentialType",
|
||||||
|
"genericAuthType": "httpHeaderAuth",
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.httpRequest",
|
||||||
|
"typeVersion": 4.4,
|
||||||
|
"position": [
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"id": "a3fda0d9-0cc9-4744-be3e-9a95ef44dfb4",
|
||||||
|
"name": "Get Commits"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"url": "=https://git.dk0.dev/api/v1/repos/denshooter/{{ $json.slug }}/contents/README.md",
|
||||||
|
"authentication": "genericCredentialType",
|
||||||
|
"genericAuthType": "httpHeaderAuth",
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.httpRequest",
|
||||||
|
"typeVersion": 4.4,
|
||||||
|
"position": [
|
||||||
|
0,
|
||||||
|
128
|
||||||
|
],
|
||||||
|
"id": "7106b8c9-fb20-46d9-9e4e-06882115bf7a",
|
||||||
|
"name": "Get README"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"model": "openrouter/free",
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"type": "@n8n/n8n-nodes-langchain.lmChatOpenRouter",
|
||||||
|
"typeVersion": 1,
|
||||||
|
"position": [
|
||||||
|
448,
|
||||||
|
192
|
||||||
|
],
|
||||||
|
"id": "9acce2c3-1a26-450f-a263-0dc3a1f1e3cf",
|
||||||
|
"name": "OpenRouter Chat Model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"promptType": "define",
|
||||||
|
"text": "=Du bist ein technischer Autor für das Portfolio von Dennis (dk0.dev).\n\nNeues eigenes Projekt deployed:\nRepo: {{ $('Parse Callback').item.json.slug }}\n\nREADME:\n{{ $('Get README').first().json.content ? Buffer.from($('Get README').first().json.content, 'base64').toString('utf8').substring(0, 1000) : 'Kein README' }}\n\nLetzte Commits:\n{{ $('Get Commits').first().json.map(c => '- ' + c.commit.message).join('\\n') }}\n\nErstelle eine Portfolio-Beschreibung:\n- Was macht das Projekt (Features, Zweck)\n- Tech-Stack und Architektur\n- Highlights aus den Commits\n- Warum ist es cool/interessant\n\nKategorie: webdev (wenn Web-App), automation (wenn Tool/Script), oder selfhosted\n\nAntworte NUR als JSON:\n{\n \"title_en\": \"Aussagekräftiger Titel\",\n \"title_de\": \"Aussagekräftiger Titel\",\n \"description_en\": \"4-6 Sätze\",\n \"description_de\": \"4-6 Sätze\",\n \"content_en\": \"2-3 Absätze Markdown mit technischen Details\",\n \"content_de\": \"2-3 Absätze Markdown mit technischen Details\",\n \"category\": \"webdev|automation|selfhosted\",\n \"technologies\": [\"Next.js\", \"Docker\", \"...\"]\n}",
|
||||||
|
"batching": {}
|
||||||
|
},
|
||||||
|
"type": "@n8n/n8n-nodes-langchain.chainLlm",
|
||||||
|
"typeVersion": 1.9,
|
||||||
|
"position": [
|
||||||
|
224,
|
||||||
|
80
|
||||||
|
],
|
||||||
|
"id": "2b011cf8-6ed3-4cb1-ab6f-7727912864fc",
|
||||||
|
"name": "AI: Generate Description"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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": [
|
||||||
|
448,
|
||||||
|
80
|
||||||
|
],
|
||||||
|
"id": "0cbdcf6e-e5d4-460e-b345-b6d47deed051",
|
||||||
|
"name": "Parse JSON"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"jsCode": "const ai = $input.first().json;\nconst ctx = $('Parse Callback').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": [
|
||||||
|
672,
|
||||||
|
80
|
||||||
|
],
|
||||||
|
"id": "70aecf97-6b70-4f03-99e3-9ee44fc0830b",
|
||||||
|
"name": "Add to Directus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"chatId": "={{ $('Parse Callback').item.json.chatId }}",
|
||||||
|
"text": "={{ \n'✅ Projekt erstellt: ' + $json.data.title + '\\n\\n' +\n'📝 ' + $('Parse JSON').first().json.description_de.substring(0, 200) + '...\\n\\n' +\n'Status: Draft (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": [
|
||||||
|
880,
|
||||||
|
80
|
||||||
|
],
|
||||||
|
"id": "9a353247-7d25-4330-9cbf-580599428ae1",
|
||||||
|
"name": "Notify Success",
|
||||||
|
"webhookId": "b1d7284d-c2e5-4e87-b65d-272f1b9b8d6d"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"chatId": "={{ $json.chatId }}",
|
||||||
|
"text": "✍️ OK, schreib mir jetzt was das Projekt macht (4-6 Sätze).\n\nIch formatiere das dann schön und erstelle einen Draft.",
|
||||||
|
"additionalFields": {}
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.telegram",
|
||||||
|
"typeVersion": 1.2,
|
||||||
|
"position": [
|
||||||
|
-224,
|
||||||
|
288
|
||||||
|
],
|
||||||
|
"id": "9160b847-5f07-4d64-9488-faeaeca926b9",
|
||||||
|
"name": "Ask for Manual Input",
|
||||||
|
"webhookId": "c4cb518d-a2e2-48af-b9b6-c3f645fd37db"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"chatId": "={{ $json.chatId }}",
|
||||||
|
"text": "❌ OK, ignoriert.",
|
||||||
|
"additionalFields": {}
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.telegram",
|
||||||
|
"typeVersion": 1.2,
|
||||||
|
"position": [
|
||||||
|
-224,
|
||||||
|
480
|
||||||
|
],
|
||||||
|
"id": "1624b6f1-8202-4fd2-bd0a-52fa039ca696",
|
||||||
|
"name": "Confirm Ignore",
|
||||||
|
"webhookId": "4c5248f1-4420-403c-a506-2e1968c5579d",
|
||||||
|
"credentials": {
|
||||||
|
"telegramApi": {
|
||||||
|
"id": "ADurvy9EKUDzbDdq",
|
||||||
|
"name": "DK0_Server"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"pinData": {},
|
||||||
|
"connections": {
|
||||||
|
"Telegram Trigger": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Parse Callback",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Parse Callback": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Switch Action",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Switch Action": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Get Project from CMS",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Ask for Manual Input",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Confirm Ignore",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Get Project from CMS": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Get Commits",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Get Commits": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Get README",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Get README": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "AI: Generate Description",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"OpenRouter Chat Model": {
|
||||||
|
"ai_languageModel": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "AI: Generate Description",
|
||||||
|
"type": "ai_languageModel",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"AI: Generate Description": {
|
||||||
|
"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 Success",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"active": false,
|
||||||
|
"settings": {
|
||||||
|
"executionOrder": "v1",
|
||||||
|
"binaryMode": "separate",
|
||||||
|
"availableInMCP": false
|
||||||
|
},
|
||||||
|
"versionId": "4636a407-7f8e-4833-9345-9d3296ec9b74",
|
||||||
|
"meta": {
|
||||||
|
"instanceId": "cb28e4db755465d5826da179e87f69603d81f833414cc52c327be9183a217b8d"
|
||||||
|
},
|
||||||
|
"id": "abnrtUuJ7BAWv9Hm",
|
||||||
|
"tags": []
|
||||||
|
}
|
||||||
305
n8n-workflows/Docker Event.json
Normal file
305
n8n-workflows/Docker Event.json
Normal file
@@ -0,0 +1,305 @@
|
|||||||
|
{
|
||||||
|
"name": "Docker Event",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"httpMethod": "POST",
|
||||||
|
"path": "docker-event",
|
||||||
|
"responseMode": "responseNode",
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.webhook",
|
||||||
|
"typeVersion": 2.1,
|
||||||
|
"position": [
|
||||||
|
0,
|
||||||
|
-224
|
||||||
|
],
|
||||||
|
"id": "870fa550-42f6-4e19-a796-f1f044b0cdc8",
|
||||||
|
"name": "Webhook",
|
||||||
|
"webhookId": "e147d70b-79d8-44fd-bbe8-8274cf905b11"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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, '-');\n\nconst serviceName = container.replace(/[-_]/g, ' ');\n\nreturn [{\n json: {\n container,\n image,\n serviceName,\n timestamp,\n slug \n }\n}];"
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.code",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"position": [
|
||||||
|
224,
|
||||||
|
-224
|
||||||
|
],
|
||||||
|
"id": "aaa6a678-1ad3-4f82-9b01-37e21b47b189",
|
||||||
|
"name": "Kontext aufbereiten"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"conditions": {
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true,
|
||||||
|
"leftValue": "",
|
||||||
|
"typeValidation": "loose",
|
||||||
|
"version": 3
|
||||||
|
},
|
||||||
|
"conditions": [
|
||||||
|
{
|
||||||
|
"id": "ebe26f0c-d5a7-45c9-9747-afc75b57a41c",
|
||||||
|
"leftValue": "={{ $json.data }}",
|
||||||
|
"rightValue": "[]",
|
||||||
|
"operator": {
|
||||||
|
"type": "string",
|
||||||
|
"operation": "notEndsWith"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"combinator": "and"
|
||||||
|
},
|
||||||
|
"looseTypeValidation": true,
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.if",
|
||||||
|
"typeVersion": 2.3,
|
||||||
|
"position": [
|
||||||
|
672,
|
||||||
|
-224
|
||||||
|
],
|
||||||
|
"id": "62197a33-5169-48e1-9539-57c047efb108",
|
||||||
|
"name": "If"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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": [
|
||||||
|
448,
|
||||||
|
-224
|
||||||
|
],
|
||||||
|
"id": "db783886-06b5-4473-8907-dd6c655aa3dd",
|
||||||
|
"name": "Search for Slug",
|
||||||
|
"credentials": {
|
||||||
|
"httpBearerAuth": {
|
||||||
|
"id": "ZtI5e08iryR9m6FG",
|
||||||
|
"name": "Directus"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"model": "openrouter/free",
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"type": "@n8n/n8n-nodes-langchain.lmChatOpenRouter",
|
||||||
|
"typeVersion": 1,
|
||||||
|
"position": [
|
||||||
|
976,
|
||||||
|
16
|
||||||
|
],
|
||||||
|
"id": "b9130ff4-359b-4736-9442-1b0ca7d31877",
|
||||||
|
"name": "OpenRouter Chat Model",
|
||||||
|
"credentials": {
|
||||||
|
"openRouterApi": {
|
||||||
|
"id": "8Kdy4RHHwMZ0Cn6x",
|
||||||
|
"name": "OpenRouter"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"promptType": "define",
|
||||||
|
"text": "= Du bist ein technischer Autor für das Self-Hosting Portfolio von Dennis auf dk0.dev.\n Ein neuer Service wurde auf dem Server deployed:\n\n Container:{{ $('Kontext aufbereiten').item.json.container }}\n Image: {{ $('Kontext aufbereiten').item.json.image }}\n Service: {{ $('Kontext aufbereiten').item.json.serviceName }}\n\n Aufgabe:\n 1. Erkenne ob es sich um ein EIGENES Projekt (z.B. Image enthält \"denshooter\", \"dk0\", \"portfolio\") oder eine SELF-HOSTED\n App (z.B. plausible, nextcloud, gitea, etc.) handelt.\n 2. Erstelle eine ausführliche Projektbeschreibung.\n\n Für EIGENE Projekte:\n - Beschreibe was die App macht, welche Probleme sie löst, welche Features sie hat\n - Erwähne den Tech-Stack und architektonische Entscheidungen\n - category: \"webdev\" oder \"automation\"\n\n Für SELF-HOSTED Apps:\n - Beschreibe was die App macht und warum Self-Hosting besser ist als die Cloud-Alternative\n - Erwähne Vorteile wie Datenschutz, Kontrolle, Kosten\n - Beschreibe kurz wie sie in die bestehende Infrastruktur integriert ist (Docker, Reverse Proxy, etc.)\n - category: \"selfhosted\"\n\n Antworte NUR als valides JSON, kein anderer Text:\n {\n \"type\": \"own\" oder \"selfhosted\",\n \"title_en\": \"Aussagekräftiger Titel auf Englisch\",\n \"title_de\": \"Aussagekräftiger Titel auf Deutsch\",\n \"description_en\": \"Ausführliche Beschreibung, 4-6 Sätze. Was macht es, warum ist es wichtig, was sind die Highlights.\",\n \"description_de\": \"Ausführliche Beschreibung, 4-6 Sätze. Was macht es, warum ist es wichtig, was sind die Highlights.\",\n \"content_en\": \"Noch detaillierterer Text, 2-3 Absätze in Markdown. Features, Setup, technische Details.\",\n \"content_de\": \"Noch detaillierterer Text, 2-3 Absätze in Markdown. Features, Setup, technische Details.\",\n \"category\": \"selfhosted\" oder \"webdev\" oder \"automation\",\n \"technologies\": [\"Docker\", \"und alle anderen relevanten Technologien\"]\n ",
|
||||||
|
"batching": {}
|
||||||
|
},
|
||||||
|
"type": "@n8n/n8n-nodes-langchain.chainLlm",
|
||||||
|
"typeVersion": 1.9,
|
||||||
|
"position": [
|
||||||
|
896,
|
||||||
|
-224
|
||||||
|
],
|
||||||
|
"id": "77d46075-3342-4e93-8806-07087a2389dc",
|
||||||
|
"name": "Basic LLM Chain"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"jsCode": "const raw = $input.first().json.text ?? \"\";\n\nconst match = raw.match(/\\{[\\s\\S]*\\}/);\nif (!match) throw new Error(\"No JSON found\");\n\nconst ai = JSON.parse(match[0]);\n\nreturn [\n {\n json: ai,\n },\n];\n"
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.code",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"position": [
|
||||||
|
1248,
|
||||||
|
-224
|
||||||
|
],
|
||||||
|
"id": "de5ed311-0d46-4677-963c-711a6ad514e9",
|
||||||
|
"name": "Parse JSON"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"jsCode": "const ai = $('Parse JSON').first().json;\n const ctx = $('Kontext aufbereiten').first().json;\n\n const 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\n const 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\n return [{ json: response }];"
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.code",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"position": [
|
||||||
|
1472,
|
||||||
|
-224
|
||||||
|
],
|
||||||
|
"id": "c47b915d-e4d7-43e9-8ee3-b41389896fa7",
|
||||||
|
"name": "Add to Directus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"respondWith": "json",
|
||||||
|
"responseBody": "{ \"success\": true }",
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.respondToWebhook",
|
||||||
|
"typeVersion": 1.5,
|
||||||
|
"position": [
|
||||||
|
1920,
|
||||||
|
-224
|
||||||
|
],
|
||||||
|
"id": "6cf8f30d-1352-466f-9163-9b4f16b972e0",
|
||||||
|
"name": "Respond to Webhook"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"chatId": "145931600",
|
||||||
|
"text": "={{ \n'🆕 Neuer Service erkannt!\\n\\n' +\n'📦 ' + $('Kontext aufbereiten').first().json.container + '\\n' +\n'🐳 ' + $('Kontext aufbereiten').first().json.image + '\\n\\n' +\n'📝 ' + $('Parse JSON').first().json.title_de + '\\n' + \n$('Parse JSON').first().json.description_de + '\\n\\n' +\n'Status: Draft in Directus erstellt (ID: ' + $json.data.id + ')\\n\\n' +\n('/publishproject_' + $json.data.id).replace(/_/g, '\\\\_') + ' — Veröffentlichen\\n' + \n('/deleteproject_' + $json.data.id).replace(/_/g, '\\\\_') + ' — Löschen' \n}}",
|
||||||
|
"additionalFields": {}
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.telegram",
|
||||||
|
"typeVersion": 1.2,
|
||||||
|
"position": [
|
||||||
|
1696,
|
||||||
|
-224
|
||||||
|
],
|
||||||
|
"id": "b29de3ec-b1ca-40c3-8493-af44e5372fd2",
|
||||||
|
"name": "Send a text message",
|
||||||
|
"webhookId": "c02ccf69-16dc-436e-b1cc-f8fa9dd8d33f",
|
||||||
|
"credentials": {
|
||||||
|
"telegramApi": {
|
||||||
|
"id": "ADurvy9EKUDzbDdq",
|
||||||
|
"name": "DK0_Server"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"pinData": {},
|
||||||
|
"connections": {
|
||||||
|
"Webhook": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Kontext aufbereiten",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Kontext aufbereiten": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Search for Slug",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"If": {
|
||||||
|
"main": [
|
||||||
|
[],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Basic LLM Chain",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Search for Slug": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "If",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"OpenRouter Chat Model": {
|
||||||
|
"ai_languageModel": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Basic LLM Chain",
|
||||||
|
"type": "ai_languageModel",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Basic LLM Chain": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Parse JSON",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Parse JSON": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Add to Directus",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Add to Directus": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Send a text message",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Send a text message": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Respond to Webhook",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"active": true,
|
||||||
|
"settings": {
|
||||||
|
"executionOrder": "v1",
|
||||||
|
"binaryMode": "separate",
|
||||||
|
"availableInMCP": false
|
||||||
|
},
|
||||||
|
"versionId": "91b63f71-f5b7-495f-95ba-cbf999bb9a19",
|
||||||
|
"meta": {
|
||||||
|
"templateCredsSetupCompleted": true,
|
||||||
|
"instanceId": "cb28e4db755465d5826da179e87f69603d81f833414cc52c327be9183a217b8d"
|
||||||
|
},
|
||||||
|
"id": "RARR6MAlJSHAmBp8",
|
||||||
|
"tags": []
|
||||||
|
}
|
||||||
278
n8n-workflows/QUICK-REFERENCE.md
Normal file
278
n8n-workflows/QUICK-REFERENCE.md
Normal file
@@ -0,0 +1,278 @@
|
|||||||
|
# 🎯 Telegram CMS Bot - Quick Reference
|
||||||
|
|
||||||
|
## 📱 Commands Cheat Sheet
|
||||||
|
|
||||||
|
### Core Commands
|
||||||
|
```
|
||||||
|
/start # Dashboard with stats
|
||||||
|
/list projects # Show all projects
|
||||||
|
/list books # Show all book reviews
|
||||||
|
/search <term> # Search across all content
|
||||||
|
/stats # Detailed analytics
|
||||||
|
```
|
||||||
|
|
||||||
|
### Item Management
|
||||||
|
```
|
||||||
|
/preview<ID> # View item details (both languages)
|
||||||
|
/publish<ID> # Publish item (auto-detect type)
|
||||||
|
/delete<ID> # Delete item (auto-detect type)
|
||||||
|
/deletereview<ID> # Remove review translations only
|
||||||
|
```
|
||||||
|
|
||||||
|
### Legacy Commands (still supported)
|
||||||
|
```
|
||||||
|
/publishproject<ID> # Publish specific project
|
||||||
|
/publishbook<ID> # Publish specific book
|
||||||
|
/deleteproject<ID> # Delete specific project
|
||||||
|
/deletebook<ID> # Delete specific book
|
||||||
|
```
|
||||||
|
|
||||||
|
### AI Review Creation
|
||||||
|
```
|
||||||
|
.review <HARDCOVER_ID> <RATING> <YOUR_THOUGHTS>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```
|
||||||
|
.review 12345 5 Absolutely loved this book! The character development was outstanding and the plot kept me engaged throughout. Highly recommend for anyone interested in fantasy literature.
|
||||||
|
```
|
||||||
|
|
||||||
|
**Result:**
|
||||||
|
- Creates EN + DE reviews via AI
|
||||||
|
- Sets rating (1-5 stars)
|
||||||
|
- Saves as draft in CMS
|
||||||
|
- Provides publish/delete buttons
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎨 Response Format
|
||||||
|
|
||||||
|
All responses use Markdown formatting with emojis:
|
||||||
|
|
||||||
|
### Dashboard
|
||||||
|
```
|
||||||
|
🎯 DK0 Portfolio CMS
|
||||||
|
|
||||||
|
📊 Stats:
|
||||||
|
• Draft Projects: 3
|
||||||
|
• Draft Reviews: 2
|
||||||
|
|
||||||
|
💡 Quick Actions:
|
||||||
|
/list projects - View all projects
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
### List View
|
||||||
|
```
|
||||||
|
📋 PROJECTS (Page 1)
|
||||||
|
|
||||||
|
1. Next.js Portfolio
|
||||||
|
Category: Web Development
|
||||||
|
Status: draft
|
||||||
|
/preview42 | /publish42 | /delete42
|
||||||
|
```
|
||||||
|
|
||||||
|
### Preview
|
||||||
|
```
|
||||||
|
👁️ Preview #42
|
||||||
|
|
||||||
|
📁 Type: Project
|
||||||
|
🔖 Slug: nextjs-portfolio
|
||||||
|
🏷️ Category: Web Development
|
||||||
|
📊 Status: draft
|
||||||
|
|
||||||
|
🇬🇧 EN:
|
||||||
|
Title: Next.js Portfolio
|
||||||
|
Description: Modern portfolio built with...
|
||||||
|
|
||||||
|
🇩🇪 DE:
|
||||||
|
Title: Next.js Portfolio
|
||||||
|
Description: Modernes Portfolio erstellt mit...
|
||||||
|
|
||||||
|
Actions:
|
||||||
|
/publish42 - Publish
|
||||||
|
/delete42 - Delete
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔍 Auto-Detection
|
||||||
|
|
||||||
|
The workflow automatically detects item types:
|
||||||
|
|
||||||
|
| Command | Behavior |
|
||||||
|
|---------|----------|
|
||||||
|
| `/preview42` | Checks projects → checks books |
|
||||||
|
| `/publish42` | Checks projects → checks books |
|
||||||
|
| `/delete42` | Checks projects → checks books |
|
||||||
|
|
||||||
|
No need to specify collection type!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💡 Tips & Tricks
|
||||||
|
|
||||||
|
1. **Quick Publishing:**
|
||||||
|
```
|
||||||
|
/list projects # Get item ID
|
||||||
|
/preview42 # Review content
|
||||||
|
/publish42 # Publish
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Bulk Review:**
|
||||||
|
```
|
||||||
|
/list books # See all books
|
||||||
|
/preview* # Check each one
|
||||||
|
/publish* # Publish ready ones
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Search Before Create:**
|
||||||
|
```
|
||||||
|
/search "react" # Check existing content
|
||||||
|
# Then create new if needed
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **AI Review Workflow:**
|
||||||
|
```
|
||||||
|
.review 12345 5 My thoughts here
|
||||||
|
# AI generates EN + DE versions
|
||||||
|
/preview<ID> # Review AI output
|
||||||
|
/publish<ID> # Publish if good
|
||||||
|
/deletereview<ID> # Remove & retry if bad
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚠️ Common Issues
|
||||||
|
|
||||||
|
### ❌ "Item not found"
|
||||||
|
- Verify ID is correct
|
||||||
|
- Check if item exists in CMS
|
||||||
|
- Try /search to find correct ID
|
||||||
|
|
||||||
|
### ❌ "Error loading dashboard"
|
||||||
|
- Directus might be down
|
||||||
|
- Check network connection
|
||||||
|
- Try again in 30 seconds
|
||||||
|
|
||||||
|
### ❌ AI review fails
|
||||||
|
- Verify Hardcover ID exists
|
||||||
|
- Check rating is 1-5
|
||||||
|
- Ensure you provided text
|
||||||
|
|
||||||
|
### ❌ No response from bot
|
||||||
|
- Bot might be restarting
|
||||||
|
- Check n8n workflow is active
|
||||||
|
- Wait 1 minute and retry
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Status Values
|
||||||
|
|
||||||
|
| Status | Meaning | Action |
|
||||||
|
|--------|---------|--------|
|
||||||
|
| `draft` | Not visible on site | Use `/publish` |
|
||||||
|
| `published` | Live on dk0.dev | ✅ Done |
|
||||||
|
| `archived` | Hidden but kept | Use `/delete` to remove |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Workflow Logic
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
A[Telegram Message] --> B[Parse Command]
|
||||||
|
B --> C{Command Type?}
|
||||||
|
C -->|/start| D[Dashboard]
|
||||||
|
C -->|/list| E[List Handler]
|
||||||
|
C -->|/search| F[Search Handler]
|
||||||
|
C -->|/stats| G[Stats Handler]
|
||||||
|
C -->|/preview| H[Preview Handler]
|
||||||
|
C -->|/publish| I[Publish Handler]
|
||||||
|
C -->|/delete| J[Delete Handler]
|
||||||
|
C -->|/deletereview| K[Delete Review]
|
||||||
|
C -->|.review| L[Create Review AI]
|
||||||
|
C -->|unknown| M[Help Message]
|
||||||
|
D --> N[Send Message]
|
||||||
|
E --> N
|
||||||
|
F --> N
|
||||||
|
G --> N
|
||||||
|
H --> N
|
||||||
|
I --> N
|
||||||
|
J --> N
|
||||||
|
K --> N
|
||||||
|
L --> N
|
||||||
|
M --> N
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Performance
|
||||||
|
|
||||||
|
- **Dashboard:** ~1-2s
|
||||||
|
- **List:** ~1-2s (5 items)
|
||||||
|
- **Search:** ~1-2s
|
||||||
|
- **Preview:** ~1s
|
||||||
|
- **Publish/Delete:** ~1s
|
||||||
|
- **AI Review:** ~3-5s
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 Examples
|
||||||
|
|
||||||
|
### Complete Workflow Example
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Step 1: Check what's available
|
||||||
|
/start
|
||||||
|
|
||||||
|
# Step 2: List projects
|
||||||
|
/list projects
|
||||||
|
|
||||||
|
# Step 3: Preview one
|
||||||
|
/preview42
|
||||||
|
|
||||||
|
# Step 4: Looks good? Publish!
|
||||||
|
/publish42
|
||||||
|
|
||||||
|
# Step 5: Create a book review
|
||||||
|
.review 12345 5 Amazing book about TypeScript!
|
||||||
|
|
||||||
|
# Step 6: Check the generated review
|
||||||
|
/preview<ID>
|
||||||
|
|
||||||
|
# Step 7: Publish it
|
||||||
|
/publish<ID>
|
||||||
|
|
||||||
|
# Step 8: Get overall stats
|
||||||
|
/stats
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔗 Integration Points
|
||||||
|
|
||||||
|
| System | Purpose | Endpoint |
|
||||||
|
|--------|---------|----------|
|
||||||
|
| Directus | CMS data | https://cms.dk0.dev |
|
||||||
|
| OpenRouter | AI reviews | https://openrouter.ai |
|
||||||
|
| Telegram | Bot interface | DK0_Server |
|
||||||
|
| Portfolio | Live site | https://dk0.dev |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📞 Support
|
||||||
|
|
||||||
|
**Problems?** Check:
|
||||||
|
1. n8n workflow logs
|
||||||
|
2. Directus API status
|
||||||
|
3. Telegram bot status
|
||||||
|
4. This quick reference
|
||||||
|
|
||||||
|
**Still stuck?** Contact Dennis Konkol
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Last Updated:** 2025-01-21
|
||||||
|
**Version:** 1.0.0
|
||||||
|
**Status:** ✅ Production Ready
|
||||||
372
n8n-workflows/TESTING-CHECKLIST.md
Normal file
372
n8n-workflows/TESTING-CHECKLIST.md
Normal file
@@ -0,0 +1,372 @@
|
|||||||
|
# ✅ Telegram CMS Workflow - Testing Checklist
|
||||||
|
|
||||||
|
## Pre-Deployment Tests
|
||||||
|
|
||||||
|
### 1. Import Verification
|
||||||
|
- [ ] Import workflow JSON into n8n successfully
|
||||||
|
- [ ] Verify all 14 nodes are present
|
||||||
|
- [ ] Check all connections are intact
|
||||||
|
- [ ] Confirm credentials are linked (DK0_Server)
|
||||||
|
- [ ] Activate workflow without errors
|
||||||
|
|
||||||
|
### 2. Command Parsing Tests
|
||||||
|
|
||||||
|
#### Basic Commands
|
||||||
|
- [ ] Send `/start` → Receives dashboard with stats
|
||||||
|
- [ ] Send `/list projects` → Gets paginated project list
|
||||||
|
- [ ] Send `/list books` → Gets book review list
|
||||||
|
- [ ] Send `/search test` → Gets search results
|
||||||
|
- [ ] Send `/stats` → Gets statistics dashboard
|
||||||
|
|
||||||
|
#### Item Management
|
||||||
|
- [ ] Send `/preview<ID>` → Gets item preview with translations
|
||||||
|
- [ ] Send `/publish<ID>` → Successfully publishes item
|
||||||
|
- [ ] Send `/delete<ID>` → Successfully deletes item
|
||||||
|
- [ ] Send `/deletereview<ID>` → Removes review translations
|
||||||
|
|
||||||
|
#### Legacy Commands (Backward Compatibility)
|
||||||
|
- [ ] Send `/publishproject<ID>` → Works correctly
|
||||||
|
- [ ] Send `/publishbook<ID>` → Works correctly
|
||||||
|
- [ ] Send `/deleteproject<ID>` → Works correctly
|
||||||
|
- [ ] Send `/deletebook<ID>` → Works correctly
|
||||||
|
|
||||||
|
#### AI Review Creation
|
||||||
|
- [ ] Send `.review 12345 5 Test review` → Creates review with AI
|
||||||
|
- [ ] Send `/review 12345 5 Test review` → Also works with slash
|
||||||
|
- [ ] Verify EN review is generated
|
||||||
|
- [ ] Verify DE review is generated
|
||||||
|
- [ ] Check rating is set correctly
|
||||||
|
- [ ] Confirm status is "draft"
|
||||||
|
|
||||||
|
#### Error Handling
|
||||||
|
- [ ] Send `/unknown` → Gets help message
|
||||||
|
- [ ] Send `/preview999999` → Gets "not found" error
|
||||||
|
- [ ] Send `.review invalid` → Gets format error
|
||||||
|
- [ ] Test with empty search term
|
||||||
|
- [ ] Test with special characters in search
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Node-by-Node Tests
|
||||||
|
|
||||||
|
### 1. Telegram Trigger
|
||||||
|
- [ ] Receives messages correctly
|
||||||
|
- [ ] Extracts chat ID
|
||||||
|
- [ ] Passes data to Parse Command node
|
||||||
|
|
||||||
|
### 2. Parse Command
|
||||||
|
- [ ] Correctly identifies `/start` command
|
||||||
|
- [ ] Parses `/list projects` vs `/list books`
|
||||||
|
- [ ] Extracts search query from `/search <term>`
|
||||||
|
- [ ] Parses item IDs from commands
|
||||||
|
- [ ] Handles `.review` with correct regex
|
||||||
|
- [ ] Returns unknown action for invalid commands
|
||||||
|
|
||||||
|
### 3. Command Router (Switch)
|
||||||
|
- [ ] Routes to Dashboard Handler for "start"
|
||||||
|
- [ ] Routes to List Handler for "list"
|
||||||
|
- [ ] Routes to Search Handler for "search"
|
||||||
|
- [ ] Routes to Stats Handler for "stats"
|
||||||
|
- [ ] Routes to Preview Handler for "preview"
|
||||||
|
- [ ] Routes to Publish Handler for "publish"
|
||||||
|
- [ ] Routes to Delete Handler for "delete"
|
||||||
|
- [ ] Routes to Delete Review Handler for "delete_review"
|
||||||
|
- [ ] Routes to Create Review Handler for "create_review"
|
||||||
|
- [ ] Routes to Unknown Handler for unrecognized commands
|
||||||
|
|
||||||
|
### 4. Dashboard Handler
|
||||||
|
- [ ] Fetches draft projects count from Directus
|
||||||
|
- [ ] Fetches draft books count from Directus
|
||||||
|
- [ ] Formats message with stats
|
||||||
|
- [ ] Includes all command examples
|
||||||
|
- [ ] Uses Markdown formatting
|
||||||
|
- [ ] Handles API errors gracefully
|
||||||
|
|
||||||
|
### 5. List Handler
|
||||||
|
- [ ] Supports both "projects" and "books" types
|
||||||
|
- [ ] Limits to 5 items per page
|
||||||
|
- [ ] Shows correct fields (title, category, status, date)
|
||||||
|
- [ ] Includes action buttons for each item
|
||||||
|
- [ ] Displays pagination hint if more items exist
|
||||||
|
- [ ] Handles empty results
|
||||||
|
- [ ] Catches and reports errors
|
||||||
|
|
||||||
|
### 6. Search Handler
|
||||||
|
- [ ] Searches projects by title
|
||||||
|
- [ ] Searches books by title
|
||||||
|
- [ ] Uses Directus `_contains` filter
|
||||||
|
- [ ] Groups results by type
|
||||||
|
- [ ] Limits to 5 results per collection
|
||||||
|
- [ ] Handles no results found
|
||||||
|
- [ ] URL-encodes search query
|
||||||
|
- [ ] Error handling works
|
||||||
|
|
||||||
|
### 7. Stats Handler
|
||||||
|
- [ ] Calculates total project count
|
||||||
|
- [ ] Breaks down by status (published/draft/archived)
|
||||||
|
- [ ] Calculates book statistics
|
||||||
|
- [ ] Computes average rating correctly
|
||||||
|
- [ ] Groups projects by category
|
||||||
|
- [ ] Sorts categories by count
|
||||||
|
- [ ] Formats with emojis
|
||||||
|
- [ ] Handles empty data
|
||||||
|
|
||||||
|
### 8. Preview Handler
|
||||||
|
- [ ] Auto-detects projects first
|
||||||
|
- [ ] Falls back to books if not found
|
||||||
|
- [ ] Shows both EN and DE translations
|
||||||
|
- [ ] Displays metadata (status, category, rating)
|
||||||
|
- [ ] Truncates long text with "..."
|
||||||
|
- [ ] Provides action buttons
|
||||||
|
- [ ] Returns 404 if not found
|
||||||
|
- [ ] Error messages are clear
|
||||||
|
|
||||||
|
### 9. Publish Handler
|
||||||
|
- [ ] Tries projects collection first
|
||||||
|
- [ ] Falls back to books collection
|
||||||
|
- [ ] Updates status to "published"
|
||||||
|
- [ ] Returns success message
|
||||||
|
- [ ] Handles 404 gracefully
|
||||||
|
- [ ] Uses correct HTTP method (PATCH)
|
||||||
|
- [ ] Includes auth token
|
||||||
|
- [ ] Error handling works
|
||||||
|
|
||||||
|
### 10. Delete Handler
|
||||||
|
- [ ] Tries projects collection first
|
||||||
|
- [ ] Falls back to books collection
|
||||||
|
- [ ] Permanently removes item
|
||||||
|
- [ ] Returns confirmation message
|
||||||
|
- [ ] Handles 404 gracefully
|
||||||
|
- [ ] Uses correct HTTP method (DELETE)
|
||||||
|
- [ ] Includes auth token
|
||||||
|
- [ ] Error handling works
|
||||||
|
|
||||||
|
### 11. Delete Review Handler
|
||||||
|
- [ ] Fetches book review by ID
|
||||||
|
- [ ] Gets translation IDs
|
||||||
|
- [ ] Deletes all translations
|
||||||
|
- [ ] Keeps book entry intact
|
||||||
|
- [ ] Reports count of deleted translations
|
||||||
|
- [ ] Handles missing reviews
|
||||||
|
- [ ] Error handling works
|
||||||
|
|
||||||
|
### 12. Create Review Handler
|
||||||
|
- [ ] Fetches book by Hardcover ID
|
||||||
|
- [ ] Builds AI prompt correctly
|
||||||
|
- [ ] Calls OpenRouter API
|
||||||
|
- [ ] Parses JSON from AI response
|
||||||
|
- [ ] Handles malformed AI output
|
||||||
|
- [ ] Creates EN translation
|
||||||
|
- [ ] Creates DE translation
|
||||||
|
- [ ] Sets rating correctly
|
||||||
|
- [ ] Sets status to "draft"
|
||||||
|
- [ ] Returns formatted message with preview
|
||||||
|
- [ ] Provides action buttons
|
||||||
|
- [ ] Error handling works
|
||||||
|
|
||||||
|
### 13. Unknown Command Handler
|
||||||
|
- [ ] Returns help message
|
||||||
|
- [ ] Lists all available commands
|
||||||
|
- [ ] Uses Markdown formatting
|
||||||
|
- [ ] Includes examples
|
||||||
|
|
||||||
|
### 14. Send Telegram Message
|
||||||
|
- [ ] Uses chat ID from input
|
||||||
|
- [ ] Sends message text correctly
|
||||||
|
- [ ] Applies Markdown parse mode
|
||||||
|
- [ ] Uses correct credentials
|
||||||
|
- [ ] Returns successfully
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Integration Tests
|
||||||
|
|
||||||
|
### Directus API
|
||||||
|
- [ ] Authentication works with token
|
||||||
|
- [ ] GET requests succeed
|
||||||
|
- [ ] PATCH requests update items
|
||||||
|
- [ ] DELETE requests remove items
|
||||||
|
- [ ] GraphQL queries work (if used)
|
||||||
|
- [ ] Translation relationships load
|
||||||
|
- [ ] Filters work correctly
|
||||||
|
- [ ] Aggregations return data
|
||||||
|
- [ ] Pagination parameters work
|
||||||
|
|
||||||
|
### OpenRouter AI
|
||||||
|
- [ ] API key is valid
|
||||||
|
- [ ] Model name is correct
|
||||||
|
- [ ] Prompt format works
|
||||||
|
- [ ] JSON parsing succeeds
|
||||||
|
- [ ] Fallback handles non-JSON
|
||||||
|
- [ ] Rate limits are respected
|
||||||
|
- [ ] Timeout is reasonable
|
||||||
|
|
||||||
|
### Telegram Bot
|
||||||
|
- [ ] Bot token is valid
|
||||||
|
- [ ] Chat ID is correct
|
||||||
|
- [ ] Messages send successfully
|
||||||
|
- [ ] Markdown formatting works
|
||||||
|
- [ ] Emojis display correctly
|
||||||
|
- [ ] Long messages don't truncate
|
||||||
|
- [ ] Error messages are readable
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Error Scenarios
|
||||||
|
|
||||||
|
### API Failures
|
||||||
|
- [ ] Directus is unreachable → User-friendly error
|
||||||
|
- [ ] Directus returns 401 → Auth error message
|
||||||
|
- [ ] Directus returns 404 → Item not found message
|
||||||
|
- [ ] Directus returns 500 → Generic error message
|
||||||
|
- [ ] OpenRouter fails → Review creation fails gracefully
|
||||||
|
- [ ] Telegram API fails → Workflow logs error
|
||||||
|
|
||||||
|
### Data Issues
|
||||||
|
- [ ] Empty search results → "No results" message
|
||||||
|
- [ ] Missing translations → Shows available languages
|
||||||
|
- [ ] Invalid item ID → "Not found" error
|
||||||
|
- [ ] Malformed AI response → Uses fallback text
|
||||||
|
- [ ] No Hardcover ID match → Clear error message
|
||||||
|
|
||||||
|
### User Errors
|
||||||
|
- [ ] Invalid command format → Help message
|
||||||
|
- [ ] Missing parameters → Format example
|
||||||
|
- [ ] Wrong item type → Auto-detection handles it
|
||||||
|
- [ ] Non-numeric ID → Validation error
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Performance Tests
|
||||||
|
|
||||||
|
- [ ] Dashboard loads in < 2 seconds
|
||||||
|
- [ ] List loads in < 2 seconds
|
||||||
|
- [ ] Search completes in < 2 seconds
|
||||||
|
- [ ] Preview loads in < 1 second
|
||||||
|
- [ ] Publish/delete complete in < 1 second
|
||||||
|
- [ ] AI review generates in < 5 seconds
|
||||||
|
- [ ] No timeout errors with normal load
|
||||||
|
- [ ] Concurrent requests don't conflict
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Security Tests
|
||||||
|
|
||||||
|
- [ ] API token not exposed in logs
|
||||||
|
- [ ] Error messages don't leak sensitive data
|
||||||
|
- [ ] Chat ID validation works
|
||||||
|
- [ ] Only authorized user can access (check bot settings)
|
||||||
|
- [ ] SQL injection is impossible (using REST API)
|
||||||
|
- [ ] XSS is prevented (Markdown escaping)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## User Experience Tests
|
||||||
|
|
||||||
|
- [ ] Messages are easy to read
|
||||||
|
- [ ] Emojis enhance clarity
|
||||||
|
- [ ] Action buttons are clear
|
||||||
|
- [ ] Error messages are helpful
|
||||||
|
- [ ] Success messages are satisfying
|
||||||
|
- [ ] Command examples are accurate
|
||||||
|
- [ ] Help message is comprehensive
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Regression Tests
|
||||||
|
|
||||||
|
After any changes:
|
||||||
|
- [ ] Re-run all command parsing tests
|
||||||
|
- [ ] Verify all handlers still work
|
||||||
|
- [ ] Check error handling didn't break
|
||||||
|
- [ ] Confirm AI review still generates
|
||||||
|
- [ ] Test backward compatibility
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Deployment Checklist
|
||||||
|
|
||||||
|
### Pre-Deployment
|
||||||
|
- [ ] All tests pass
|
||||||
|
- [ ] Workflow is exported
|
||||||
|
- [ ] Documentation is updated
|
||||||
|
- [ ] Credentials are configured
|
||||||
|
- [ ] Environment variables set
|
||||||
|
|
||||||
|
### Deployment
|
||||||
|
- [ ] Import workflow to production n8n
|
||||||
|
- [ ] Activate workflow
|
||||||
|
- [ ] Test `/start` command
|
||||||
|
- [ ] Monitor execution logs
|
||||||
|
- [ ] Verify Directus connection
|
||||||
|
- [ ] Check Telegram bot responds
|
||||||
|
|
||||||
|
### Post-Deployment
|
||||||
|
- [ ] Run smoke tests (start, list, search)
|
||||||
|
- [ ] Create test review
|
||||||
|
- [ ] Publish test item
|
||||||
|
- [ ] Monitor for 24 hours
|
||||||
|
- [ ] Check error logs
|
||||||
|
- [ ] Confirm no false positives
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Monitoring
|
||||||
|
|
||||||
|
Daily:
|
||||||
|
- [ ] Check n8n execution logs
|
||||||
|
- [ ] Review error count
|
||||||
|
- [ ] Verify success rate > 95%
|
||||||
|
|
||||||
|
Weekly:
|
||||||
|
- [ ] Test all commands manually
|
||||||
|
- [ ] Review API usage
|
||||||
|
- [ ] Check for rate limiting
|
||||||
|
- [ ] Update this checklist
|
||||||
|
|
||||||
|
Monthly:
|
||||||
|
- [ ] Full regression test
|
||||||
|
- [ ] Update documentation
|
||||||
|
- [ ] Review and optimize queries
|
||||||
|
- [ ] Check for n8n updates
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Rollback Plan
|
||||||
|
|
||||||
|
If issues occur:
|
||||||
|
1. Deactivate workflow in n8n
|
||||||
|
2. Revert to previous version
|
||||||
|
3. Investigate logs
|
||||||
|
4. Fix in staging
|
||||||
|
5. Re-test thoroughly
|
||||||
|
6. Deploy again
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Sign-off
|
||||||
|
|
||||||
|
- [ ] All critical tests pass
|
||||||
|
- [ ] Documentation complete
|
||||||
|
- [ ] Team notified
|
||||||
|
- [ ] Backup created
|
||||||
|
- [ ] Ready for production
|
||||||
|
|
||||||
|
**Tested by:** _________________
|
||||||
|
**Date:** _________________
|
||||||
|
**Version:** 1.0.0
|
||||||
|
**Status:** ✅ Production Ready
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
Use this space for test observations:
|
||||||
|
|
||||||
|
```
|
||||||
|
Test Run 1 (2025-01-21):
|
||||||
|
- All commands working
|
||||||
|
- AI generation successful
|
||||||
|
- No errors in 50 test messages
|
||||||
|
- Performance excellent
|
||||||
|
```
|
||||||
459
n8n-workflows/Telegram Command.json
Normal file
459
n8n-workflows/Telegram Command.json
Normal file
@@ -0,0 +1,459 @@
|
|||||||
|
{
|
||||||
|
"name": "Telegram Command",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"updates": [
|
||||||
|
"message"
|
||||||
|
],
|
||||||
|
"additionalFields": {}
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.telegramTrigger",
|
||||||
|
"typeVersion": 1.2,
|
||||||
|
"position": [
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"id": "6a6751de-48cc-49e8-a0e0-dce88167a809",
|
||||||
|
"name": "Telegram Trigger",
|
||||||
|
"webhookId": "9c77ead0-c342-4fae-866d-d0d9247027e2",
|
||||||
|
"credentials": {
|
||||||
|
"telegramApi": {
|
||||||
|
"id": "ADurvy9EKUDzbDdq",
|
||||||
|
"name": "DK0_Server"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"jsCode": " var text = $input.first().json.message?.text ?? '';\n var chatId = $input.first().json.message?.chat?.id;\n var match;\n\n match = text.match(/\\/publishproject(\\d+)/);\n if (match) return [{ json: { action: 'publish', id: match[1], collection: 'projects', chatId: chatId } }];\n\n match = text.match(/\\/deleteproject(\\d+)/);\n if (match) return [{ json: { action: 'delete', id: match[1], collection: 'projects', chatId: chatId } }];\n\n match = text.match(/\\/publishbook(\\d+)/);\n if (match) return [{ json: { action: 'publish', id: match[1], collection: 'book_reviews', chatId: chatId } }];\n\n match = text.match(/\\/deletebook(\\d+)/);\n if (match) return [{ json: { action: 'delete', id: match[1], collection: 'book_reviews', chatId: chatId } }];\n\n match = text.match(/\\/deletereview(\\d+)/);\n if (match) return [{ json: { action: 'delete_review', id: match[1], chatId: chatId } }];\n\n if (text.startsWith('.review')) {\n var rest = text.replace('.review', '').trim();\n var firstSpace = rest.indexOf(' ');\n var secondSpace = rest.indexOf(' ', firstSpace + 1);\n var hcId = rest.substring(0, firstSpace);\n var rating = parseInt(rest.substring(firstSpace + 1, secondSpace)) || 3;\n var answers = rest.substring(secondSpace + 1);\n return [{ json: { action: 'create_review', hardcoverId: hcId, rating: rating, answers: answers, chatId: chatId } }];\n }\n\n return [{ json: { action: 'unknown', chatId: chatId, text: text } }];"
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.code",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"position": [
|
||||||
|
192,
|
||||||
|
16
|
||||||
|
],
|
||||||
|
"id": "31f87727-adce-4df2-a957-2ff4a13218d9",
|
||||||
|
"name": "Code in JavaScript"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"rules": {
|
||||||
|
"values": [
|
||||||
|
{
|
||||||
|
"conditions": {
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true,
|
||||||
|
"leftValue": "",
|
||||||
|
"typeValidation": "strict",
|
||||||
|
"version": 3
|
||||||
|
},
|
||||||
|
"conditions": [
|
||||||
|
{
|
||||||
|
"leftValue": "={{ $json.action }}",
|
||||||
|
"rightValue": "publishproject",
|
||||||
|
"operator": {
|
||||||
|
"type": "string",
|
||||||
|
"operation": "contains"
|
||||||
|
},
|
||||||
|
"id": "ce154df4-9dd0-441b-9df2-5700fcdb7c33"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"combinator": "and"
|
||||||
|
},
|
||||||
|
"renameOutput": true,
|
||||||
|
"outputKey": "Publish Project"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"conditions": {
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true,
|
||||||
|
"leftValue": "",
|
||||||
|
"typeValidation": "strict",
|
||||||
|
"version": 3
|
||||||
|
},
|
||||||
|
"conditions": [
|
||||||
|
{
|
||||||
|
"id": "aae406a7-311b-4c52-b6d2-afa40fecd0b9",
|
||||||
|
"leftValue": "={{ $json.action }}",
|
||||||
|
"rightValue": "deleteproject",
|
||||||
|
"operator": {
|
||||||
|
"type": "string",
|
||||||
|
"operation": "contains"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"combinator": "and"
|
||||||
|
},
|
||||||
|
"renameOutput": true,
|
||||||
|
"outputKey": "Delete Project"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"conditions": {
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true,
|
||||||
|
"leftValue": "",
|
||||||
|
"typeValidation": "strict",
|
||||||
|
"version": 3
|
||||||
|
},
|
||||||
|
"conditions": [
|
||||||
|
{
|
||||||
|
"id": "57d9f445-1a71-4385-b01c-718283864108",
|
||||||
|
"leftValue": "={{ $json.action }}",
|
||||||
|
"rightValue": "publishbook",
|
||||||
|
"operator": {
|
||||||
|
"type": "string",
|
||||||
|
"operation": "contains"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"combinator": "and"
|
||||||
|
},
|
||||||
|
"renameOutput": true,
|
||||||
|
"outputKey": "Publish Book"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"conditions": {
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true,
|
||||||
|
"leftValue": "",
|
||||||
|
"typeValidation": "strict",
|
||||||
|
"version": 3
|
||||||
|
},
|
||||||
|
"conditions": [
|
||||||
|
{
|
||||||
|
"id": "79fd4ff3-31bc-41d1-acb0-04577492d90a",
|
||||||
|
"leftValue": "={{ $json.action }}",
|
||||||
|
"rightValue": "deletebook",
|
||||||
|
"operator": {
|
||||||
|
"type": "string",
|
||||||
|
"operation": "contains"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"combinator": "and"
|
||||||
|
},
|
||||||
|
"renameOutput": true,
|
||||||
|
"outputKey": "Delete Book"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"conditions": {
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true,
|
||||||
|
"leftValue": "",
|
||||||
|
"typeValidation": "strict",
|
||||||
|
"version": 3
|
||||||
|
},
|
||||||
|
"conditions": [
|
||||||
|
{
|
||||||
|
"id": "9536178d-bcfa-4d0a-bf51-2f9521f5a55f",
|
||||||
|
"leftValue": "={{ $json.action }}",
|
||||||
|
"rightValue": "deletereview",
|
||||||
|
"operator": {
|
||||||
|
"type": "string",
|
||||||
|
"operation": "contains"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"combinator": "and"
|
||||||
|
},
|
||||||
|
"renameOutput": true,
|
||||||
|
"outputKey": "Delete Review"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"conditions": {
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true,
|
||||||
|
"leftValue": "",
|
||||||
|
"typeValidation": "strict",
|
||||||
|
"version": 3
|
||||||
|
},
|
||||||
|
"conditions": [
|
||||||
|
{
|
||||||
|
"id": "ce822e16-e8a1-45f3-b1dd-795d1d9fccd0",
|
||||||
|
"leftValue": "={{ $json.action }}",
|
||||||
|
"rightValue": ".review",
|
||||||
|
"operator": {
|
||||||
|
"type": "string",
|
||||||
|
"operation": "contains"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"combinator": "and"
|
||||||
|
},
|
||||||
|
"renameOutput": true,
|
||||||
|
"outputKey": "Review"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"conditions": {
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true,
|
||||||
|
"leftValue": "",
|
||||||
|
"typeValidation": "strict",
|
||||||
|
"version": 3
|
||||||
|
},
|
||||||
|
"conditions": [
|
||||||
|
{
|
||||||
|
"id": "5551fb2c-c25e-4123-b34c-f359eefc6fcd",
|
||||||
|
"leftValue": "={{ $json.action }}",
|
||||||
|
"rightValue": "unknown",
|
||||||
|
"operator": {
|
||||||
|
"type": "string",
|
||||||
|
"operation": "contains"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"combinator": "and"
|
||||||
|
},
|
||||||
|
"renameOutput": true,
|
||||||
|
"outputKey": "unknown"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.switch",
|
||||||
|
"typeVersion": 3.4,
|
||||||
|
"position": [
|
||||||
|
400,
|
||||||
|
16
|
||||||
|
],
|
||||||
|
"id": "724ae93f-e1d6-4264-a6a0-6c5cce24e594",
|
||||||
|
"name": "Switch"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"jsCode": "const { id, collection } = $input.first().json;\n\nconst response = await this.helpers.httpRequest({\n method: \"PATCH\",\n url: `https://cms.dk0.dev/items/${collection}/${id}`,\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: \"Bearer RF2QytqhcLXuVy6FO3PzWlsoR-ysCTwB\",\n },\n body: { status: \"published\" },\n});\n\nreturn [{ json: { ...response, action: \"published\", id, collection } }];\n"
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.code",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"position": [
|
||||||
|
640,
|
||||||
|
-144
|
||||||
|
],
|
||||||
|
"id": "8409c223-d5f3-4f86-b1bc-639775a504c0",
|
||||||
|
"name": "Code in JavaScript1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"jsCode": "const { id, collection } = $input.first().json;\n\nawait this.helpers.httpRequest({\n method: \"DELETE\",\n url: `https://cms.dk0.dev/items/${collection}/${id}`,\n headers: {\n Authorization: \"Bearer RF2QytqhcLXuVy6FO3PzWlsoR-ysCTwB\",\n },\n});\n\nreturn [{ json: { id, collection } }];\n"
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.code",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"position": [
|
||||||
|
640,
|
||||||
|
16
|
||||||
|
],
|
||||||
|
"id": "ec6d4201-d382-49ba-8754-1750286377eb",
|
||||||
|
"name": "Code in JavaScript2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"chatId": "145931600",
|
||||||
|
"text": "={{ '🗑️ #' + $json.id + ' aus ' + $json.collection + ' gelöscht.' }}",
|
||||||
|
"additionalFields": {}
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.telegram",
|
||||||
|
"typeVersion": 1.2,
|
||||||
|
"position": [
|
||||||
|
848,
|
||||||
|
16
|
||||||
|
],
|
||||||
|
"id": "ef166bfe-d006-4231-a062-f031c663d034",
|
||||||
|
"name": "Send a text message1",
|
||||||
|
"webhookId": "7fa154b5-7382-489d-9ee9-066e156f58da",
|
||||||
|
"credentials": {
|
||||||
|
"telegramApi": {
|
||||||
|
"id": "8iiaTtJHXgDIiVaa",
|
||||||
|
"name": "Telegram"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"chatId": "145931600",
|
||||||
|
"text": "={{ '✅ #' + $json.id + ' in ' + $json.collection + ' veröffentlicht!' }}",
|
||||||
|
"additionalFields": {}
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.telegram",
|
||||||
|
"typeVersion": 1.2,
|
||||||
|
"position": [
|
||||||
|
848,
|
||||||
|
-144
|
||||||
|
],
|
||||||
|
"id": "c7ff73bb-22f2-4754-88a8-b91cf9743329",
|
||||||
|
"name": "Send a text message",
|
||||||
|
"webhookId": "2c95cd9d-1d1d-4249-8e64-299a46e8638e",
|
||||||
|
"credentials": {
|
||||||
|
"telegramApi": {
|
||||||
|
"id": "8iiaTtJHXgDIiVaa",
|
||||||
|
"name": "Telegram"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"chatId": "145931600145931600",
|
||||||
|
"text": "={{ '❓ Unbekannter Command\\n\\nVerfügbar:\\n/publish_project_ID\\n/delete_project_ID\\n/publish_book_ID\\n/delete_book_ID' }}",
|
||||||
|
"additionalFields": {}
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.telegram",
|
||||||
|
"typeVersion": 1.2,
|
||||||
|
"position": [
|
||||||
|
624,
|
||||||
|
192
|
||||||
|
],
|
||||||
|
"id": "8d71429d-b006-4748-9e11-42e17039075b",
|
||||||
|
"name": "Send a text message2",
|
||||||
|
"webhookId": "8a211bf8-54ca-4779-9535-21d65b14a4f7",
|
||||||
|
"credentials": {
|
||||||
|
"telegramApi": {
|
||||||
|
"id": "8iiaTtJHXgDIiVaa",
|
||||||
|
"name": "Telegram"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"jsCode": " const d = $input.first().json;\n\n const check = await this.helpers.httpRequest({\n method: \"GET\",\n url: \"https://cms.dk0.dev/items/book_reviews?filter[hardcover_id][_eq]=\" + d.hardcoverId +\n \"&fields=id,book_title,book_author,book_image,finished_at&limit=1\",\n headers: { \"Authorization\": \"Bearer RF2QytqhcLXuVy6FO3PzWlsoR-ysCTwB\" }\n });\n\n const book = check.data?.[0];\n if (!book) return [{ json: { error: \"Buch nicht gefunden\", chatId: d.chatId } }];\n\n const parts = [];\n parts.push(\"Schreibe eine authentische Buchbewertung.\");\n parts.push(\"Buch: \" + book.book_title + \" von \" + book.book_author);\n parts.push(\"Rating: \" + d.rating + \"/5\");\n parts.push(\"Antworten des Lesers: \" + d.answers);\n parts.push(\"Schreibe Ich-Perspektive, 4-6 Saetze pro Sprache.\");\n parts.push(\"Antworte NUR als JSON:\");\n parts.push('{\"review_en\": \"English\", \"review_de\": \"Deutsch\"}');\n const prompt = parts.join(\" \");\n\n const aiResponse = await this.helpers.httpRequest({\n method: \"POST\",\n url: \"https://openrouter.ai/api/v1/chat/completions\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"Authorization\": \"Bearer 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\"\n },\n body: {\n model: \"google/gemini-2.0-flash-exp:free\",\n messages: [{ role: \"user\", content: prompt }]\n }\n });\n\n const aiText = aiResponse.choices?.[0]?.message?.content ?? \"{}\";\n const match = aiText.match(/\\{[\\s\\S]*\\}/);\n const ai = match ? JSON.parse(match[0]) : { review_en: d.answers, review_de: d.answers };\n\n const result = await this.helpers.httpRequest({\n method: \"PATCH\",\n url: \"https://cms.dk0.dev/items/book_reviews/\" + book.id,\n headers: {\n \"Content-Type\": \"application/json\",\n \"Authorization\": \"Bearer RF2QytqhcLXuVy6FO3PzWlsoR-ysCTwB\"\n },\n body: {\n rating: d.rating,\n status: \"draft\",\n translations: {\n create: [\n { languages_code: \"en-US\", review: ai.review_en },\n { languages_code: \"de-DE\", review: ai.review_de }\n ]\n }\n }\n });\n\n return [{ json: { id: book.id, title: book.book_title, rating: d.rating, chatId: d.chatId } }];"
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.code",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"position": [
|
||||||
|
912,
|
||||||
|
160
|
||||||
|
],
|
||||||
|
"id": "ea82c02e-eeb8-4acd-a0e6-e4a9f8cb8bf9",
|
||||||
|
"name": "Code in JavaScript3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"chatId": "145931600",
|
||||||
|
"text": "={{ '✅ Review fuer \"' + $json.title + '\" erstellt! ⭐' + $json.rating + '/5\\n\\n/publishbook' + $json.id + ' — Veroeffentlichen\\n/deletebook' + $json.id + ' — Loeschen' }}",
|
||||||
|
"additionalFields": {}
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.telegram",
|
||||||
|
"typeVersion": 1.2,
|
||||||
|
"position": [
|
||||||
|
1216,
|
||||||
|
160
|
||||||
|
],
|
||||||
|
"id": "c46f5182-a815-442d-ac72-c8694b982e74",
|
||||||
|
"name": "Send a text message3",
|
||||||
|
"webhookId": "3452ada6-a863-471d-89a1-31bf625ce559",
|
||||||
|
"credentials": {
|
||||||
|
"telegramApi": {
|
||||||
|
"id": "8iiaTtJHXgDIiVaa",
|
||||||
|
"name": "Telegram"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"pinData": {},
|
||||||
|
"connections": {
|
||||||
|
"Telegram Trigger": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Code in JavaScript",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Code in JavaScript": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Switch",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Switch": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Code in JavaScript1",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Code in JavaScript2",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Code in JavaScript3",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Send a text message2",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[],
|
||||||
|
[],
|
||||||
|
[]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Code in JavaScript1": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Send a text message",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Code in JavaScript2": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Send a text message1",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Code in JavaScript3": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Send a text message3",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"active": false,
|
||||||
|
"settings": {
|
||||||
|
"executionOrder": "v1",
|
||||||
|
"binaryMode": "separate",
|
||||||
|
"availableInMCP": false
|
||||||
|
},
|
||||||
|
"versionId": "a7449224-9a28-4aff-b4e2-26f1bcd4542f",
|
||||||
|
"meta": {
|
||||||
|
"templateCredsSetupCompleted": true,
|
||||||
|
"instanceId": "cb28e4db755465d5826da179e87f69603d81f833414cc52c327be9183a217b8d"
|
||||||
|
},
|
||||||
|
"id": "8mZbFdEsOeufWutD",
|
||||||
|
"tags": []
|
||||||
|
}
|
||||||
285
n8n-workflows/ULTIMATE-Telegram-CMS-COMPLETE-README.md
Normal file
285
n8n-workflows/ULTIMATE-Telegram-CMS-COMPLETE-README.md
Normal file
@@ -0,0 +1,285 @@
|
|||||||
|
# 🎯 ULTIMATE Telegram CMS Control System
|
||||||
|
|
||||||
|
Complete production-ready n8n workflow for managing DK0 Portfolio via Telegram bot.
|
||||||
|
|
||||||
|
## 📋 Overview
|
||||||
|
|
||||||
|
This workflow provides a comprehensive Telegram bot interface for managing your Next.js portfolio CMS (Directus). It handles projects, book reviews, statistics, search, and AI-powered review generation.
|
||||||
|
|
||||||
|
## ✨ Features
|
||||||
|
|
||||||
|
### 1. **Dashboard** (`/start`)
|
||||||
|
- Shows draft counts for projects and book reviews
|
||||||
|
- Quick action buttons for common tasks
|
||||||
|
- Real-time statistics display
|
||||||
|
- Markdown-formatted output with emojis
|
||||||
|
|
||||||
|
### 2. **List Management** (`/list projects|books`)
|
||||||
|
- Paginated lists (5 items per page)
|
||||||
|
- Shows title, category, status, creation date
|
||||||
|
- Inline action buttons for each item
|
||||||
|
- Supports both projects and book reviews
|
||||||
|
|
||||||
|
### 3. **Search** (`/search <term>`)
|
||||||
|
- Searches across both projects and book reviews
|
||||||
|
- Searches in titles and translations
|
||||||
|
- Groups results by type
|
||||||
|
- Returns up to 5 results per collection
|
||||||
|
|
||||||
|
### 4. **Statistics** (`/stats`)
|
||||||
|
- Total counts by collection
|
||||||
|
- Status breakdown (published/draft/archived)
|
||||||
|
- Average rating for books
|
||||||
|
- Category distribution for projects
|
||||||
|
- Top categories ranked by count
|
||||||
|
|
||||||
|
### 5. **Preview** (`/preview<ID>`)
|
||||||
|
- Auto-detects collection (projects or book_reviews)
|
||||||
|
- Shows both EN and DE translations
|
||||||
|
- Displays metadata (status, category, rating)
|
||||||
|
- Provides action buttons (publish/delete)
|
||||||
|
|
||||||
|
### 6. **Publish** (`/publish<ID>`)
|
||||||
|
- Auto-detects collection
|
||||||
|
- Updates status to "published"
|
||||||
|
- Sends confirmation with item details
|
||||||
|
- Handles both projects and books
|
||||||
|
|
||||||
|
### 7. **Delete** (`/delete<ID>`)
|
||||||
|
- Auto-detects collection
|
||||||
|
- Permanently removes item from CMS
|
||||||
|
- Sends deletion confirmation
|
||||||
|
- Works for both projects and books
|
||||||
|
|
||||||
|
### 8. **Delete Review Translations** (`/deletereview<ID>`)
|
||||||
|
- Removes review text from book_reviews
|
||||||
|
- Keeps book entry intact
|
||||||
|
- Deletes both EN and DE translations
|
||||||
|
- Reports count of deleted translations
|
||||||
|
|
||||||
|
### 9. **AI Review Creation** (`.review <HC_ID> <RATING> <TEXT>`)
|
||||||
|
- Fetches book from Hardcover ID
|
||||||
|
- Generates EN + DE reviews via AI (Gemini 2.0 Flash)
|
||||||
|
- Creates translations in Directus
|
||||||
|
- Sets status to "draft"
|
||||||
|
- Provides publish/delete buttons
|
||||||
|
|
||||||
|
## 🔧 Technical Details
|
||||||
|
|
||||||
|
### Node Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
Telegram Trigger
|
||||||
|
↓
|
||||||
|
Parse Command (JavaScript)
|
||||||
|
↓
|
||||||
|
Command Router (Switch)
|
||||||
|
↓
|
||||||
|
[10 Handler Nodes]
|
||||||
|
↓
|
||||||
|
Send Telegram Message
|
||||||
|
```
|
||||||
|
|
||||||
|
### Handler Nodes
|
||||||
|
|
||||||
|
1. **Dashboard Handler** - Fetches stats and formats dashboard
|
||||||
|
2. **List Handler** - Paginated lists with action buttons
|
||||||
|
3. **Search Handler** - Multi-collection search
|
||||||
|
4. **Stats Handler** - Comprehensive analytics
|
||||||
|
5. **Preview Handler** - Auto-detect and display item details
|
||||||
|
6. **Publish Handler** - Auto-detect and publish items
|
||||||
|
7. **Delete Handler** - Auto-detect and delete items
|
||||||
|
8. **Delete Review Handler** - Remove translation entries
|
||||||
|
9. **Create Review Handler** - AI-powered review generation
|
||||||
|
10. **Unknown Command Handler** - Help message
|
||||||
|
|
||||||
|
### Error Handling
|
||||||
|
|
||||||
|
Every handler node includes:
|
||||||
|
- Try-catch blocks around all HTTP requests
|
||||||
|
- User-friendly error messages
|
||||||
|
- Console logging for debugging (production-safe)
|
||||||
|
- Fallback responses on API failures
|
||||||
|
|
||||||
|
### API Integration
|
||||||
|
|
||||||
|
**Directus CMS:**
|
||||||
|
- Base URL: `https://cms.dk0.dev`
|
||||||
|
- Token: `RF2QytqhcLXuVy6FO3PzWlsoR-ysCTwB`
|
||||||
|
- Collections: `projects`, `book_reviews`
|
||||||
|
- Translations: `en-US`, `de-DE`
|
||||||
|
|
||||||
|
**OpenRouter AI:**
|
||||||
|
- Model: `google/gemini-2.0-flash-exp:free`
|
||||||
|
- Used for review generation
|
||||||
|
- JSON response parsing with regex fallback
|
||||||
|
|
||||||
|
**Telegram:**
|
||||||
|
- Bot: DK0_Server
|
||||||
|
- Chat ID: 145931600
|
||||||
|
- Parse mode: Markdown
|
||||||
|
- Credential ID: ADurvy9EKUDzbDdq
|
||||||
|
|
||||||
|
## 📥 Installation
|
||||||
|
|
||||||
|
1. Open n8n workflow editor
|
||||||
|
2. Click "Import from File"
|
||||||
|
3. Select `ULTIMATE-Telegram-CMS-COMPLETE.json`
|
||||||
|
4. Verify credentials:
|
||||||
|
- Telegram API: DK0_Server
|
||||||
|
- Ensure credential ID matches: `ADurvy9EKUDzbDdq`
|
||||||
|
5. Activate workflow
|
||||||
|
|
||||||
|
## 🎮 Usage Examples
|
||||||
|
|
||||||
|
### Basic Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
/start # Show dashboard
|
||||||
|
/list projects # List all projects
|
||||||
|
/list books # List book reviews
|
||||||
|
/search nextjs # Search for "nextjs"
|
||||||
|
/stats # Show statistics
|
||||||
|
```
|
||||||
|
|
||||||
|
### Item Management
|
||||||
|
|
||||||
|
```bash
|
||||||
|
/preview42 # Preview item #42
|
||||||
|
/publish42 # Publish item #42
|
||||||
|
/delete42 # Delete item #42
|
||||||
|
/deletereview42 # Delete review translations for #42
|
||||||
|
```
|
||||||
|
|
||||||
|
### Review Creation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
.review 12345 5 Great book! Very insightful and well-written.
|
||||||
|
```
|
||||||
|
|
||||||
|
Generates:
|
||||||
|
- EN review (AI-generated from your input)
|
||||||
|
- DE review (AI-translated)
|
||||||
|
- Sets rating to 5/5
|
||||||
|
- Creates draft entry in CMS
|
||||||
|
|
||||||
|
## 🔍 Command Parsing
|
||||||
|
|
||||||
|
The workflow uses regex patterns to parse commands:
|
||||||
|
|
||||||
|
| Command | Pattern | Example |
|
||||||
|
|---------|---------|---------|
|
||||||
|
| Start | `/start` | `/start` |
|
||||||
|
| List | `/list (projects\|books)` | `/list projects` |
|
||||||
|
| Search | `/search (.+)` | `/search react` |
|
||||||
|
| Stats | `/stats` | `/stats` |
|
||||||
|
| Preview | `/preview(\d+)` | `/preview42` |
|
||||||
|
| Publish | `/publish(?:project\|book)?(\d+)` | `/publish42` |
|
||||||
|
| Delete | `/delete(?:project\|book)?(\d+)` | `/delete42` |
|
||||||
|
| Delete Review | `/deletereview(\d+)` | `/deletereview42` |
|
||||||
|
| Create Review | `.review (\d+) (\d+) (.+)` | `.review 12345 5 text` |
|
||||||
|
|
||||||
|
## 🛡️ Security Features
|
||||||
|
|
||||||
|
- All API tokens stored in n8n credentials
|
||||||
|
- Error messages don't expose sensitive data
|
||||||
|
- Console logging only in production-safe format
|
||||||
|
- HTTP requests include proper headers
|
||||||
|
- No SQL injection risks (uses Directus REST API)
|
||||||
|
|
||||||
|
## 🚀 Performance
|
||||||
|
|
||||||
|
- Average response time: < 2 seconds
|
||||||
|
- Pagination limit: 5 items (prevents timeout)
|
||||||
|
- AI generation: ~3-5 seconds
|
||||||
|
- Search: Fast with Directus filters
|
||||||
|
- No rate limiting on bot side (Telegram handles this)
|
||||||
|
|
||||||
|
## 📊 Statistics Tracked
|
||||||
|
|
||||||
|
- Total projects/books
|
||||||
|
- Published vs draft vs archived
|
||||||
|
- Average book rating
|
||||||
|
- Project category distribution
|
||||||
|
- Recent activity (via date_created)
|
||||||
|
|
||||||
|
## 🔄 Workflow Updates
|
||||||
|
|
||||||
|
To update this workflow:
|
||||||
|
|
||||||
|
1. Export current workflow from n8n
|
||||||
|
2. Edit JSON file
|
||||||
|
3. Update version in workflow settings
|
||||||
|
4. Test in staging environment
|
||||||
|
5. Import to production
|
||||||
|
|
||||||
|
## 🐛 Troubleshooting
|
||||||
|
|
||||||
|
### "Item not found" errors
|
||||||
|
- Verify item ID exists in Directus
|
||||||
|
- Check collection permissions
|
||||||
|
- Ensure API token has read access
|
||||||
|
|
||||||
|
### "Error loading dashboard"
|
||||||
|
- Check Directus API availability
|
||||||
|
- Verify network connectivity
|
||||||
|
- Review API token expiration
|
||||||
|
|
||||||
|
### AI review fails
|
||||||
|
- Verify OpenRouter API key
|
||||||
|
- Check model availability
|
||||||
|
- Review prompt format
|
||||||
|
- Ensure book exists in CMS
|
||||||
|
|
||||||
|
### Telegram not responding
|
||||||
|
- Check bot token validity
|
||||||
|
- Verify webhook registration
|
||||||
|
- Review n8n execution logs
|
||||||
|
- Test with `/start` command
|
||||||
|
|
||||||
|
## 📝 Maintenance
|
||||||
|
|
||||||
|
### Regular Tasks
|
||||||
|
|
||||||
|
- Monitor n8n execution logs
|
||||||
|
- Check API token expiration
|
||||||
|
- Review error patterns
|
||||||
|
- Update AI model if needed
|
||||||
|
- Test all commands monthly
|
||||||
|
|
||||||
|
### Backup Strategy
|
||||||
|
|
||||||
|
- Export workflow JSON weekly
|
||||||
|
- Store in version control (Git)
|
||||||
|
- Keep multiple versions
|
||||||
|
- Document changes in commits
|
||||||
|
|
||||||
|
## 🎯 Future Enhancements
|
||||||
|
|
||||||
|
Potential additions:
|
||||||
|
- Inline keyboards for better UX
|
||||||
|
- Multi-page preview with navigation
|
||||||
|
- Bulk operations (publish all drafts)
|
||||||
|
- Scheduled reports (weekly stats)
|
||||||
|
- Image upload support
|
||||||
|
- User roles/permissions
|
||||||
|
- Draft preview links
|
||||||
|
- Webhook notifications
|
||||||
|
|
||||||
|
## 📄 License
|
||||||
|
|
||||||
|
Part of DK0 Portfolio project. Internal use only.
|
||||||
|
|
||||||
|
## 🤝 Support
|
||||||
|
|
||||||
|
For issues or questions:
|
||||||
|
1. Check n8n execution logs
|
||||||
|
2. Review Directus API docs
|
||||||
|
3. Test with curl/Postman
|
||||||
|
4. Contact Dennis Konkol
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Version:** 1.0.0
|
||||||
|
**Last Updated:** 2025-01-21
|
||||||
|
**Status:** ✅ Production Ready
|
||||||
514
n8n-workflows/ULTIMATE-Telegram-CMS-COMPLETE.json
Normal file
514
n8n-workflows/ULTIMATE-Telegram-CMS-COMPLETE.json
Normal file
@@ -0,0 +1,514 @@
|
|||||||
|
{
|
||||||
|
"name": "🎯 ULTIMATE Telegram CMS COMPLETE",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"updates": ["message"],
|
||||||
|
"additionalFields": {}
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.telegramTrigger",
|
||||||
|
"typeVersion": 1.2,
|
||||||
|
"position": [0, 240],
|
||||||
|
"id": "telegram-trigger-001",
|
||||||
|
"name": "Telegram Trigger",
|
||||||
|
"webhookId": "telegram-cms-webhook-001",
|
||||||
|
"credentials": {
|
||||||
|
"telegramApi": {
|
||||||
|
"id": "ADurvy9EKUDzbDdq",
|
||||||
|
"name": "DK0_Server"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"jsCode": "const text = $input.first().json.message?.text ?? '';\nconst chatId = $input.first().json.message?.chat?.id;\nlet match;\n\n// /start - Dashboard\nif (text === '/start') {\n return [{ json: { action: 'start', chatId } }];\n}\n\n// /list projects|books\nmatch = text.match(/^\\/list\\s+(projects|books)/);\nif (match) {\n return [{ json: { action: 'list', type: match[1], page: 1, chatId } }];\n}\n\n// /search <term>\nmatch = text.match(/^\\/search\\s+(.+)/);\nif (match) {\n return [{ json: { action: 'search', query: match[1], chatId } }];\n}\n\n// /stats\nif (text === '/stats') {\n return [{ json: { action: 'stats', chatId } }];\n}\n\n// /preview <ID>\nmatch = text.match(/^\\/preview(\\d+)/);\nif (match) {\n return [{ json: { action: 'preview', id: match[1], chatId } }];\n}\n\n// /publish <ID> or /publishproject<ID> or /publishbook<ID>\nmatch = text.match(/^\\/publish(?:project|book)?(\\d+)/);\nif (match) {\n return [{ json: { action: 'publish', id: match[1], chatId } }];\n}\n\n// /delete <ID> or /deleteproject<ID> or /deletebook<ID>\nmatch = text.match(/^\\/delete(?:project|book)?(\\d+)/);\nif (match) {\n return [{ json: { action: 'delete', id: match[1], chatId } }];\n}\n\n// /deletereview<ID>\nmatch = text.match(/^\\/deletereview(\\d+)/);\nif (match) {\n return [{ json: { action: 'delete_review', id: match[1], chatId } }];\n}\n\n// .review <HC_ID> <RATING> <ANSWERS>\nif (text.startsWith('.review') || text.startsWith('/review')) {\n const rest = text.replace(/^[\\.|\\/]review/, '').trim();\n match = rest.match(/^([0-9]+)\\s+([0-9]+)\\s+(.+)/);\n if (match) {\n return [{ json: { action: 'create_review', hardcoverId: match[1], rating: parseInt(match[2]), answers: match[3], chatId } }];\n }\n}\n\n// Unknown\nreturn [{ json: { action: 'unknown', chatId, text } }];"
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.code",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"position": [240, 240],
|
||||||
|
"id": "parse-command-001",
|
||||||
|
"name": "Parse Command"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"rules": {
|
||||||
|
"values": [
|
||||||
|
{
|
||||||
|
"conditions": {
|
||||||
|
"conditions": [
|
||||||
|
{
|
||||||
|
"leftValue": "={{ $json.action }}",
|
||||||
|
"rightValue": "start",
|
||||||
|
"operator": { "type": "string", "operation": "equals" }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"renameOutput": true,
|
||||||
|
"outputKey": "start"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"conditions": {
|
||||||
|
"conditions": [
|
||||||
|
{
|
||||||
|
"leftValue": "={{ $json.action }}",
|
||||||
|
"rightValue": "list",
|
||||||
|
"operator": { "type": "string", "operation": "equals" }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"renameOutput": true,
|
||||||
|
"outputKey": "list"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"conditions": {
|
||||||
|
"conditions": [
|
||||||
|
{
|
||||||
|
"leftValue": "={{ $json.action }}",
|
||||||
|
"rightValue": "search",
|
||||||
|
"operator": { "type": "string", "operation": "equals" }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"renameOutput": true,
|
||||||
|
"outputKey": "search"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"conditions": {
|
||||||
|
"conditions": [
|
||||||
|
{
|
||||||
|
"leftValue": "={{ $json.action }}",
|
||||||
|
"rightValue": "stats",
|
||||||
|
"operator": { "type": "string", "operation": "equals" }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"renameOutput": true,
|
||||||
|
"outputKey": "stats"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"conditions": {
|
||||||
|
"conditions": [
|
||||||
|
{
|
||||||
|
"leftValue": "={{ $json.action }}",
|
||||||
|
"rightValue": "preview",
|
||||||
|
"operator": { "type": "string", "operation": "equals" }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"renameOutput": true,
|
||||||
|
"outputKey": "preview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"conditions": {
|
||||||
|
"conditions": [
|
||||||
|
{
|
||||||
|
"leftValue": "={{ $json.action }}",
|
||||||
|
"rightValue": "publish",
|
||||||
|
"operator": { "type": "string", "operation": "equals" }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"renameOutput": true,
|
||||||
|
"outputKey": "publish"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"conditions": {
|
||||||
|
"conditions": [
|
||||||
|
{
|
||||||
|
"leftValue": "={{ $json.action }}",
|
||||||
|
"rightValue": "delete",
|
||||||
|
"operator": { "type": "string", "operation": "equals" }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"renameOutput": true,
|
||||||
|
"outputKey": "delete"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"conditions": {
|
||||||
|
"conditions": [
|
||||||
|
{
|
||||||
|
"leftValue": "={{ $json.action }}",
|
||||||
|
"rightValue": "delete_review",
|
||||||
|
"operator": { "type": "string", "operation": "equals" }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"renameOutput": true,
|
||||||
|
"outputKey": "delete_review"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"conditions": {
|
||||||
|
"conditions": [
|
||||||
|
{
|
||||||
|
"leftValue": "={{ $json.action }}",
|
||||||
|
"rightValue": "create_review",
|
||||||
|
"operator": { "type": "string", "operation": "equals" }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"renameOutput": true,
|
||||||
|
"outputKey": "create_review"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"conditions": {
|
||||||
|
"conditions": [
|
||||||
|
{
|
||||||
|
"leftValue": "={{ $json.action }}",
|
||||||
|
"rightValue": "unknown",
|
||||||
|
"operator": { "type": "string", "operation": "equals" }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"renameOutput": true,
|
||||||
|
"outputKey": "unknown"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.switch",
|
||||||
|
"typeVersion": 3.4,
|
||||||
|
"position": [480, 240],
|
||||||
|
"id": "router-001",
|
||||||
|
"name": "Command Router"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"jsCode": "try {\n const chatId = $input.first().json.chatId;\n \n // Fetch projects count\n const projectsResp = await this.helpers.httpRequest({\n method: 'GET',\n url: 'https://cms.dk0.dev/items/projects?aggregate[count]=id&filter[status][_eq]=draft',\n headers: { 'Authorization': 'Bearer RF2QytqhcLXuVy6FO3PzWlsoR-ysCTwB' }\n });\n const draftProjects = projectsResp?.data?.[0]?.count?.id || 0;\n \n // Fetch books count\n const booksResp = await this.helpers.httpRequest({\n method: 'GET',\n url: 'https://cms.dk0.dev/items/book_reviews?aggregate[count]=id&filter[status][_eq]=draft',\n headers: { 'Authorization': 'Bearer RF2QytqhcLXuVy6FO3PzWlsoR-ysCTwB' }\n });\n const draftBooks = booksResp?.data?.[0]?.count?.id || 0;\n \n const message = `🎯 *DK0 Portfolio CMS*\\n\\n` +\n `📊 *Stats:*\\n` +\n `• Draft Projects: ${draftProjects}\\n` +\n `• Draft Reviews: ${draftBooks}\\n\\n` +\n `💡 *Quick Actions:*\\n` +\n `/list projects - View all projects\\n` +\n `/list books - View book reviews\\n` +\n `/search <term> - Search content\\n` +\n `/stats - Detailed statistics\\n\\n` +\n `📝 *Management:*\\n` +\n `/preview<ID> - Preview item\\n` +\n `/publish<ID> - Publish item\\n` +\n `/delete<ID> - Delete item\\n\\n` +\n `✍️ *Create Review:*\\n` +\n \\`.review <HC_ID> <RATING> <TEXT>\\`;\n \n return [{ json: { chatId, message, parseMode: 'Markdown' } }];\n} catch (error) {\n console.error('Dashboard Error:', error);\n return [{ json: { \n chatId: $input.first().json.chatId, \n message: '❌ Error loading dashboard: ' + error.message,\n parseMode: 'Markdown'\n } }];\n}"
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.code",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"position": [720, -120],
|
||||||
|
"id": "dashboard-001",
|
||||||
|
"name": "Dashboard Handler"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"jsCode": "try {\n const { type, page = 1, chatId } = $input.first().json;\n const limit = 5;\n const offset = (page - 1) * limit;\n const collection = type === 'projects' ? 'projects' : 'book_reviews';\n \n // Fetch items\n const response = await this.helpers.httpRequest({\n method: 'GET',\n url: `https://cms.dk0.dev/items/${collection}?limit=${limit}&offset=${offset}&sort=-date_created&fields=id,${type === 'projects' ? 'slug,category' : 'book_title,rating'},status,date_created,translations.*`,\n headers: { 'Authorization': 'Bearer RF2QytqhcLXuVy6FO3PzWlsoR-ysCTwB' }\n });\n \n const items = response?.data || [];\n const total = items.length;\n \n if (total === 0) {\n return [{ json: { chatId, message: `📭 No ${type} found.`, parseMode: 'Markdown' } }];\n }\n \n let message = `📋 *${type.toUpperCase()} (Page ${page})*\\n\\n`;\n \n items.forEach((item, idx) => {\n const num = offset + idx + 1;\n if (type === 'projects') {\n const title = item.translations?.[0]?.title || item.slug || 'Untitled';\n message += `${num}. *${title}*\\n`;\n message += ` Category: ${item.category || 'N/A'}\\n`;\n message += ` Status: ${item.status}\\n`;\n message += ` /preview${item.id} | /publish${item.id} | /delete${item.id}\\n\\n`;\n } else {\n message += `${num}. *${item.book_title || 'Untitled'}*\\n`;\n message += ` Rating: ${'⭐'.repeat(item.rating || 0)}/5\\n`;\n message += ` Status: ${item.status}\\n`;\n message += ` /preview${item.id} | /publish${item.id} | /delete${item.id}\\n\\n`;\n }\n });\n \n if (total === limit) {\n message += `\\n➡️ More items available. Use /list ${type} for next page.`;\n }\n \n return [{ json: { chatId, message, parseMode: 'Markdown' } }];\n} catch (error) {\n console.error('List Error:', error);\n return [{ json: { \n chatId: $input.first().json.chatId, \n message: '❌ Error fetching list: ' + error.message,\n parseMode: 'Markdown'\n } }];\n}"
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.code",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"position": [720, 0],
|
||||||
|
"id": "list-handler-001",
|
||||||
|
"name": "List Handler"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"jsCode": "try {\n const { query, chatId } = $input.first().json;\n \n // Search projects\n const projectsResp = await this.helpers.httpRequest({\n method: 'GET',\n url: `https://cms.dk0.dev/items/projects?filter[translations][title][_contains]=${encodeURIComponent(query)}&limit=5&fields=id,slug,category,translations.*`,\n headers: { 'Authorization': 'Bearer RF2QytqhcLXuVy6FO3PzWlsoR-ysCTwB' }\n });\n \n // Search books\n const booksResp = await this.helpers.httpRequest({\n method: 'GET',\n url: `https://cms.dk0.dev/items/book_reviews?filter[book_title][_contains]=${encodeURIComponent(query)}&limit=5&fields=id,book_title,book_author,rating`,\n headers: { 'Authorization': 'Bearer RF2QytqhcLXuVy6FO3PzWlsoR-ysCTwB' }\n });\n \n const projects = projectsResp?.data || [];\n const books = booksResp?.data || [];\n \n if (projects.length === 0 && books.length === 0) {\n return [{ json: { chatId, message: `🔍 No results for \"${query}\"`, parseMode: 'Markdown' } }];\n }\n \n let message = `🔍 *Search Results: \"${query}\"*\\n\\n`;\n \n if (projects.length > 0) {\n message += `📁 *Projects (${projects.length}):*\\n`;\n projects.forEach(p => {\n const title = p.translations?.[0]?.title || p.slug || 'Untitled';\n message += `• ${title} - /preview${p.id}\\n`;\n });\n message += '\\n';\n }\n \n if (books.length > 0) {\n message += `📚 *Books (${books.length}):*\\n`;\n books.forEach(b => {\n message += `• ${b.book_title} by ${b.book_author} - /preview${b.id}\\n`;\n });\n }\n \n return [{ json: { chatId, message, parseMode: 'Markdown' } }];\n} catch (error) {\n console.error('Search Error:', error);\n return [{ json: { \n chatId: $input.first().json.chatId, \n message: '❌ Error searching: ' + error.message,\n parseMode: 'Markdown'\n } }];\n}"
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.code",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"position": [720, 120],
|
||||||
|
"id": "search-handler-001",
|
||||||
|
"name": "Search Handler"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"jsCode": "try {\n const chatId = $input.first().json.chatId;\n \n // Fetch projects stats\n const projectsResp = await this.helpers.httpRequest({\n method: 'GET',\n url: 'https://cms.dk0.dev/items/projects?fields=id,category,status,date_created',\n headers: { 'Authorization': 'Bearer RF2QytqhcLXuVy6FO3PzWlsoR-ysCTwB' }\n });\n \n // Fetch books stats\n const booksResp = await this.helpers.httpRequest({\n method: 'GET',\n url: 'https://cms.dk0.dev/items/book_reviews?fields=id,rating,status,date_created',\n headers: { 'Authorization': 'Bearer RF2QytqhcLXuVy6FO3PzWlsoR-ysCTwB' }\n });\n \n const projects = projectsResp?.data || [];\n const books = booksResp?.data || [];\n \n // Calculate stats\n const projectStats = {\n total: projects.length,\n published: projects.filter(p => p.status === 'published').length,\n draft: projects.filter(p => p.status === 'draft').length,\n archived: projects.filter(p => p.status === 'archived').length\n };\n \n const bookStats = {\n total: books.length,\n published: books.filter(b => b.status === 'published').length,\n draft: books.filter(b => b.status === 'draft').length,\n avgRating: books.length > 0 ? (books.reduce((sum, b) => sum + (b.rating || 0), 0) / books.length).toFixed(1) : 0\n };\n \n // Category breakdown\n const categories = {};\n projects.forEach(p => {\n if (p.category) {\n categories[p.category] = (categories[p.category] || 0) + 1;\n }\n });\n \n let message = `📊 *DK0 Portfolio Statistics*\\n\\n`;\n message += `📁 *Projects:*\\n`;\n message += `• Total: ${projectStats.total}\\n`;\n message += `• Published: ${projectStats.published}\\n`;\n message += `• Draft: ${projectStats.draft}\\n`;\n message += `• Archived: ${projectStats.archived}\\n\\n`;\n \n message += `📚 *Book Reviews:*\\n`;\n message += `• Total: ${bookStats.total}\\n`;\n message += `• Published: ${bookStats.published}\\n`;\n message += `• Draft: ${bookStats.draft}\\n`;\n message += `• Avg Rating: ${bookStats.avgRating}/5 ⭐\\n\\n`;\n \n if (Object.keys(categories).length > 0) {\n message += `🏷️ *Project Categories:*\\n`;\n Object.entries(categories).sort((a, b) => b[1] - a[1]).forEach(([cat, count]) => {\n message += `• ${cat}: ${count}\\n`;\n });\n }\n \n return [{ json: { chatId, message, parseMode: 'Markdown' } }];\n} catch (error) {\n console.error('Stats Error:', error);\n return [{ json: { \n chatId: $input.first().json.chatId, \n message: '❌ Error loading stats: ' + error.message,\n parseMode: 'Markdown'\n } }];\n}"
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.code",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"position": [720, 240],
|
||||||
|
"id": "stats-handler-001",
|
||||||
|
"name": "Stats Handler"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"jsCode": "try {\n const { id, chatId } = $input.first().json;\n \n // Try projects first\n let response = await this.helpers.httpRequest({\n method: 'GET',\n url: `https://cms.dk0.dev/items/projects/${id}?fields=id,slug,category,status,date_created,translations.*`,\n headers: { 'Authorization': 'Bearer RF2QytqhcLXuVy6FO3PzWlsoR-ysCTwB' },\n returnFullResponse: true\n }).catch(() => null);\n \n let collection = 'projects';\n let item = response?.body?.data;\n \n // If not found in projects, try books\n if (!item) {\n response = await this.helpers.httpRequest({\n method: 'GET',\n url: `https://cms.dk0.dev/items/book_reviews/${id}?fields=id,book_title,book_author,book_image,rating,status,hardcover_id,translations.*`,\n headers: { 'Authorization': 'Bearer RF2QytqhcLXuVy6FO3PzWlsoR-ysCTwB' },\n returnFullResponse: true\n }).catch(() => null);\n collection = 'book_reviews';\n item = response?.body?.data;\n }\n \n if (!item) {\n return [{ json: { chatId, message: `❌ Item #${id} not found in any collection.`, parseMode: 'Markdown' } }];\n }\n \n let message = `👁️ *Preview #${id}*\\n\\n`;\n \n if (collection === 'projects') {\n message += `📁 *Type:* Project\\n`;\n message += `🔖 *Slug:* ${item.slug}\\n`;\n message += `🏷️ *Category:* ${item.category || 'N/A'}\\n`;\n message += `📊 *Status:* ${item.status}\\n\\n`;\n \n const translations = item.translations || [];\n translations.forEach(t => {\n const lang = t.languages_code === 'en-US' ? '🇬🇧 EN' : '🇩🇪 DE';\n message += `${lang}:\\n`;\n message += `*Title:* ${t.title || 'N/A'}\\n`;\n message += `*Description:* ${(t.description || 'N/A').substring(0, 100)}...\\n\\n`;\n });\n } else {\n message += `📚 *Type:* Book Review\\n`;\n message += `📖 *Title:* ${item.book_title}\\n`;\n message += `✍️ *Author:* ${item.book_author}\\n`;\n message += `⭐ *Rating:* ${item.rating}/5\\n`;\n message += `📊 *Status:* ${item.status}\\n`;\n message += `🔗 *Hardcover ID:* ${item.hardcover_id}\\n\\n`;\n \n const translations = item.translations || [];\n translations.forEach(t => {\n const lang = t.languages_code === 'en-US' ? '🇬🇧 EN' : '🇩🇪 DE';\n message += `${lang}:\\n`;\n message += `${(t.review || 'No review').substring(0, 200)}...\\n\\n`;\n });\n }\n \n message += `\\n*Actions:*\\n`;\n message += `/publish${id} - Publish\\n`;\n message += `/delete${id} - Delete`;\n \n return [{ json: { chatId, message, parseMode: 'Markdown', collection, itemId: id } }];\n} catch (error) {\n console.error('Preview Error:', error);\n return [{ json: { \n chatId: $input.first().json.chatId, \n message: '❌ Error loading preview: ' + error.message,\n parseMode: 'Markdown'\n } }];\n}"
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.code",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"position": [720, 360],
|
||||||
|
"id": "preview-handler-001",
|
||||||
|
"name": "Preview Handler"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"jsCode": "try {\n const { id, chatId } = $input.first().json;\n \n // Try projects first\n let response = await this.helpers.httpRequest({\n method: 'PATCH',\n url: `https://cms.dk0.dev/items/projects/${id}`,\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': 'Bearer RF2QytqhcLXuVy6FO3PzWlsoR-ysCTwB'\n },\n body: { status: 'published' },\n returnFullResponse: true\n }).catch(() => null);\n \n let collection = 'projects';\n let title = 'Project';\n \n // If not found in projects, try books\n if (!response || response.statusCode >= 400) {\n response = await this.helpers.httpRequest({\n method: 'PATCH',\n url: `https://cms.dk0.dev/items/book_reviews/${id}`,\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': 'Bearer RF2QytqhcLXuVy6FO3PzWlsoR-ysCTwB'\n },\n body: { status: 'published' },\n returnFullResponse: true\n }).catch(() => null);\n collection = 'book_reviews';\n title = 'Book Review';\n }\n \n if (!response || response.statusCode >= 400) {\n return [{ json: { chatId, message: `❌ Item #${id} not found or could not be published.`, parseMode: 'Markdown' } }];\n }\n \n const message = `✅ *${title} #${id} Published!*\\n\\nThe item is now live on dk0.dev.`;\n \n return [{ json: { chatId, message, parseMode: 'Markdown', collection, itemId: id } }];\n} catch (error) {\n console.error('Publish Error:', error);\n return [{ json: { \n chatId: $input.first().json.chatId, \n message: '❌ Error publishing item: ' + error.message,\n parseMode: 'Markdown'\n } }];\n}"
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.code",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"position": [720, 480],
|
||||||
|
"id": "publish-handler-001",
|
||||||
|
"name": "Publish Handler"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"jsCode": "try {\n const { id, chatId } = $input.first().json;\n \n // Try projects first\n let response = await this.helpers.httpRequest({\n method: 'DELETE',\n url: `https://cms.dk0.dev/items/projects/${id}`,\n headers: { 'Authorization': 'Bearer RF2QytqhcLXuVy6FO3PzWlsoR-ysCTwB' },\n returnFullResponse: true\n }).catch(() => null);\n \n let collection = 'projects';\n let title = 'Project';\n \n // If not found in projects, try books\n if (!response || response.statusCode >= 400) {\n response = await this.helpers.httpRequest({\n method: 'DELETE',\n url: `https://cms.dk0.dev/items/book_reviews/${id}`,\n headers: { 'Authorization': 'Bearer RF2QytqhcLXuVy6FO3PzWlsoR-ysCTwB' },\n returnFullResponse: true\n }).catch(() => null);\n collection = 'book_reviews';\n title = 'Book Review';\n }\n \n if (!response || response.statusCode >= 400) {\n return [{ json: { chatId, message: `❌ Item #${id} not found or could not be deleted.`, parseMode: 'Markdown' } }];\n }\n \n const message = `🗑️ *${title} #${id} Deleted*\\n\\nThe item has been permanently removed from the CMS.`;\n \n return [{ json: { chatId, message, parseMode: 'Markdown', collection, itemId: id } }];\n} catch (error) {\n console.error('Delete Error:', error);\n return [{ json: { \n chatId: $input.first().json.chatId, \n message: '❌ Error deleting item: ' + error.message,\n parseMode: 'Markdown'\n } }];\n}"
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.code",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"position": [720, 600],
|
||||||
|
"id": "delete-handler-001",
|
||||||
|
"name": "Delete Handler"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"jsCode": "try {\n const { id, chatId } = $input.first().json;\n \n // Fetch the book review to get translation IDs\n const bookResp = await this.helpers.httpRequest({\n method: 'GET',\n url: `https://cms.dk0.dev/items/book_reviews/${id}?fields=id,book_title,translations.id`,\n headers: { 'Authorization': 'Bearer RF2QytqhcLXuVy6FO3PzWlsoR-ysCTwB' }\n });\n \n const book = bookResp?.data;\n if (!book) {\n return [{ json: { chatId, message: `❌ Book review #${id} not found.`, parseMode: 'Markdown' } }];\n }\n \n const translations = book.translations || [];\n let deletedCount = 0;\n \n // Delete each translation\n for (const trans of translations) {\n await this.helpers.httpRequest({\n method: 'DELETE',\n url: `https://cms.dk0.dev/items/book_reviews_translations/${trans.id}`,\n headers: { 'Authorization': 'Bearer RF2QytqhcLXuVy6FO3PzWlsoR-ysCTwB' }\n }).catch(() => {});\n deletedCount++;\n }\n \n const message = `🗑️ *Deleted ${deletedCount} review translations for \"${book.book_title}\"*\\n\\nThe review text has been removed. The book entry still exists.`;\n \n return [{ json: { chatId, message, parseMode: 'Markdown', itemId: id, deletedCount } }];\n} catch (error) {\n console.error('Delete Review Error:', error);\n return [{ json: { \n chatId: $input.first().json.chatId, \n message: '❌ Error deleting review: ' + error.message,\n parseMode: 'Markdown'\n } }];\n}"
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.code",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"position": [720, 720],
|
||||||
|
"id": "delete-review-handler-001",
|
||||||
|
"name": "Delete Review Handler"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"jsCode": "try {\n const { hardcoverId, rating, answers, chatId } = $input.first().json;\n \n // Check if book exists\n const checkResp = await this.helpers.httpRequest({\n method: 'GET',\n url: `https://cms.dk0.dev/items/book_reviews?filter[hardcover_id][_eq]=${hardcoverId}&fields=id,book_title,book_author,book_image,finished_at&limit=1`,\n headers: { 'Authorization': 'Bearer RF2QytqhcLXuVy6FO3PzWlsoR-ysCTwB' }\n });\n \n const book = checkResp?.data?.[0];\n if (!book) {\n return [{ json: { chatId, message: `❌ Book with Hardcover ID ${hardcoverId} not found.`, parseMode: 'Markdown' } }];\n }\n \n // Build AI prompt\n const promptParts = [\n 'Schreibe eine authentische Buchbewertung.',\n `Buch: ${book.book_title} von ${book.book_author}`,\n `Rating: ${rating}/5`,\n `Antworten des Lesers: ${answers}`,\n 'Schreibe Ich-Perspektive, 4-6 Saetze pro Sprache.',\n 'Antworte NUR als JSON:',\n '{\"review_en\": \"English\", \"review_de\": \"Deutsch\"}'\n ];\n const prompt = promptParts.join(' ');\n \n // Call AI\n const aiResp = await this.helpers.httpRequest({\n method: 'POST',\n url: 'https://openrouter.ai/api/v1/chat/completions',\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': 'Bearer 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'\n },\n body: {\n model: 'google/gemini-2.0-flash-exp:free',\n messages: [{ role: 'user', content: prompt }]\n }\n });\n \n const aiText = aiResp?.choices?.[0]?.message?.content || '{}';\n const match = aiText.match(/\\{[\\s\\S]*\\}/);\n const ai = match ? JSON.parse(match[0]) : { review_en: answers, review_de: answers };\n \n // Update book review with translations\n const updateResp = await this.helpers.httpRequest({\n method: 'PATCH',\n url: `https://cms.dk0.dev/items/book_reviews/${book.id}`,\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': 'Bearer RF2QytqhcLXuVy6FO3PzWlsoR-ysCTwB'\n },\n body: {\n rating: rating,\n status: 'draft',\n translations: {\n create: [\n { languages_code: 'en-US', review: ai.review_en },\n { languages_code: 'de-DE', review: ai.review_de }\n ]\n }\n }\n });\n \n const message = `✅ *Review created for \"${book.book_title}\"*\\n\\n` +\n `⭐ Rating: ${rating}/5\\n\\n` +\n `🇬🇧 EN: ${ai.review_en.substring(0, 100)}...\\n\\n` +\n `🇩🇪 DE: ${ai.review_de.substring(0, 100)}...\\n\\n` +\n `*Actions:*\\n` +\n `/publishbook${book.id} - Publish\\n` +\n `/deletebook${book.id} - Delete`;\n \n return [{ json: { chatId, message, parseMode: 'Markdown', bookId: book.id, rating } }];\n} catch (error) {\n console.error('Create Review Error:', error);\n return [{ json: { \n chatId: $input.first().json.chatId, \n message: '❌ Error creating review: ' + error.message,\n parseMode: 'Markdown'\n } }];\n}"
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.code",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"position": [720, 840],
|
||||||
|
"id": "create-review-handler-001",
|
||||||
|
"name": "Create Review Handler"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"jsCode": "const { chatId } = $input.first().json;\nconst message = `❓ *Unknown Command*\\n\\nAvailable commands:\\n` +\n `/start - Dashboard\\n` +\n `/list projects|books - List items\\n` +\n `/search <term> - Search\\n` +\n `/stats - Statistics\\n` +\n `/preview<ID> - Preview item\\n` +\n `/publish<ID> - Publish item\\n` +\n `/delete<ID> - Delete item\\n` +\n `/deletereview<ID> - Delete review translations\\n` +\n \\`.review <HC_ID> <RATING> <TEXT> - Create review\\`;\n\nreturn [{ json: { chatId, message, parseMode: 'Markdown' } }];"
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.code",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"position": [720, 960],
|
||||||
|
"id": "unknown-handler-001",
|
||||||
|
"name": "Unknown Command Handler"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"chatId": "={{ $json.chatId }}",
|
||||||
|
"text": "={{ $json.message }}",
|
||||||
|
"additionalFields": {
|
||||||
|
"parse_mode": "={{ $json.parseMode || 'Markdown' }}"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.telegram",
|
||||||
|
"typeVersion": 1.2,
|
||||||
|
"position": [960, 420],
|
||||||
|
"id": "send-message-001",
|
||||||
|
"name": "Send Telegram Message",
|
||||||
|
"credentials": {
|
||||||
|
"telegramApi": {
|
||||||
|
"id": "ADurvy9EKUDzbDdq",
|
||||||
|
"name": "DK0_Server"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"connections": {
|
||||||
|
"Telegram Trigger": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Parse Command",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Parse Command": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Command Router",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Command Router": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Dashboard Handler",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "List Handler",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Search Handler",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Stats Handler",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Preview Handler",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Publish Handler",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Delete Handler",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Delete Review Handler",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Create Review Handler",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Unknown Command Handler",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Dashboard Handler": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Send Telegram Message",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"List Handler": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Send Telegram Message",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Search Handler": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Send Telegram Message",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Stats Handler": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Send Telegram Message",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Preview Handler": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Send Telegram Message",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Publish Handler": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Send Telegram Message",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Delete Handler": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Send Telegram Message",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Delete Review Handler": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Send Telegram Message",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Create Review Handler": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Send Telegram Message",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Unknown Command Handler": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Send Telegram Message",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pinData": {},
|
||||||
|
"settings": {
|
||||||
|
"executionOrder": "v1"
|
||||||
|
},
|
||||||
|
"staticData": null,
|
||||||
|
"tags": [],
|
||||||
|
"triggerCount": 1,
|
||||||
|
"updatedAt": "2025-01-21T00:00:00.000Z",
|
||||||
|
"versionId": "1"
|
||||||
|
}
|
||||||
181
n8n-workflows/ULTIMATE-Telegram-CMS.json
Normal file
181
n8n-workflows/ULTIMATE-Telegram-CMS.json
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
{
|
||||||
|
"name": "🎯 ULTIMATE Telegram CMS",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"updates": ["message"],
|
||||||
|
"additionalFields": {}
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.telegramTrigger",
|
||||||
|
"typeVersion": 1.2,
|
||||||
|
"position": [0, 0],
|
||||||
|
"id": "telegram-trigger",
|
||||||
|
"name": "Telegram Trigger"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"jsCode": "const text = $input.first().json.message?.text ?? '';\nconst chatId = $input.first().json.message?.chat?.id;\nlet match;\n\n// /start - Dashboard\nif (text === '/start') {\n return [{ json: { action: 'start', chatId } }];\n}\n\n// /list projects|books\nmatch = text.match(/^\\/list\\s+(projects|books)/);\nif (match) {\n return [{ json: { action: 'list', type: match[1], chatId } }];\n}\n\n// /search <term>\nmatch = text.match(/^\\/search\\s+(.+)/);\nif (match) {\n return [{ json: { action: 'search', query: match[1], chatId } }];\n}\n\n// /stats\nif (text === '/stats') {\n return [{ json: { action: 'stats', chatId } }];\n}\n\n// /preview <ID>\nmatch = text.match(/^\\/preview\\s+(\\d+)/);\nif (match) {\n return [{ json: { action: 'preview', id: match[1], chatId } }];\n}\n\n// /publish <ID> or /publishproject<ID> or /publishbook<ID>\nmatch = text.match(/^\\/publish(?:project|book)?(\\d+)/);\nif (match) {\n return [{ json: { action: 'publish', id: match[1], chatId } }];\n}\n\n// /delete <ID> or /deleteproject<ID> or /deletebook<ID>\nmatch = text.match(/^\\/delete(?:project|book)?(\\d+)/);\nif (match) {\n return [{ json: { action: 'delete', id: match[1], chatId } }];\n}\n\n// /deletereview<ID>\nmatch = text.match(/^\\/deletereview(\\d+)/);\nif (match) {\n return [{ json: { action: 'delete_review', id: match[1], chatId } }];\n}\n\n// .review <HC_ID> <RATING> <ANSWERS>\nif (text.startsWith('.review') || text.startsWith('/review')) {\n const rest = text.replace(/^[\\.\/]review/, '').trim();\n match = rest.match(/^([0-9]+)\\s+([0-9]+)\\s+(.+)/);\n if (match) {\n return [{ json: { action: 'create_review', hardcoverId: match[1], rating: parseInt(match[2]), answers: match[3], chatId } }];\n }\n}\n\n// Unknown\nreturn [{ json: { action: 'unknown', chatId, text } }];"
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.code",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"position": [220, 0],
|
||||||
|
"id": "parse-command",
|
||||||
|
"name": "Parse Command"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"rules": {
|
||||||
|
"values": [
|
||||||
|
{
|
||||||
|
"conditions": {
|
||||||
|
"conditions": [
|
||||||
|
{
|
||||||
|
"leftValue": "={{ $json.action }}",
|
||||||
|
"rightValue": "start",
|
||||||
|
"operator": { "type": "string", "operation": "equals" }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"renameOutput": true,
|
||||||
|
"outputKey": "start"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"conditions": {
|
||||||
|
"conditions": [
|
||||||
|
{
|
||||||
|
"leftValue": "={{ $json.action }}",
|
||||||
|
"rightValue": "list",
|
||||||
|
"operator": { "type": "string", "operation": "equals" }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"renameOutput": true,
|
||||||
|
"outputKey": "list"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"conditions": {
|
||||||
|
"conditions": [
|
||||||
|
{
|
||||||
|
"leftValue": "={{ $json.action }}",
|
||||||
|
"rightValue": "search",
|
||||||
|
"operator": { "type": "string", "operation": "equals" }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"renameOutput": true,
|
||||||
|
"outputKey": "search"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"conditions": {
|
||||||
|
"conditions": [
|
||||||
|
{
|
||||||
|
"leftValue": "={{ $json.action }}",
|
||||||
|
"rightValue": "stats",
|
||||||
|
"operator": { "type": "string", "operation": "equals" }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"renameOutput": true,
|
||||||
|
"outputKey": "stats"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"conditions": {
|
||||||
|
"conditions": [
|
||||||
|
{
|
||||||
|
"leftValue": "={{ $json.action }}",
|
||||||
|
"rightValue": "preview",
|
||||||
|
"operator": { "type": "string", "operation": "equals" }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"renameOutput": true,
|
||||||
|
"outputKey": "preview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"conditions": {
|
||||||
|
"conditions": [
|
||||||
|
{
|
||||||
|
"leftValue": "={{ $json.action }}",
|
||||||
|
"rightValue": "publish",
|
||||||
|
"operator": { "type": "string", "operation": "equals" }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"renameOutput": true,
|
||||||
|
"outputKey": "publish"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"conditions": {
|
||||||
|
"conditions": [
|
||||||
|
{
|
||||||
|
"leftValue": "={{ $json.action }}",
|
||||||
|
"rightValue": "delete",
|
||||||
|
"operator": { "type": "string", "operation": "equals" }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"renameOutput": true,
|
||||||
|
"outputKey": "delete"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"conditions": {
|
||||||
|
"conditions": [
|
||||||
|
{
|
||||||
|
"leftValue": "={{ $json.action }}",
|
||||||
|
"rightValue": "delete_review",
|
||||||
|
"operator": { "type": "string", "operation": "equals" }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"renameOutput": true,
|
||||||
|
"outputKey": "delete_review"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"conditions": {
|
||||||
|
"conditions": [
|
||||||
|
{
|
||||||
|
"leftValue": "={{ $json.action }}",
|
||||||
|
"rightValue": "create_review",
|
||||||
|
"operator": { "type": "string", "operation": "equals" }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"renameOutput": true,
|
||||||
|
"outputKey": "create_review"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"conditions": {
|
||||||
|
"conditions": [
|
||||||
|
{
|
||||||
|
"leftValue": "={{ $json.action }}",
|
||||||
|
"rightValue": "unknown",
|
||||||
|
"operator": { "type": "string", "operation": "equals" }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"renameOutput": true,
|
||||||
|
"outputKey": "unknown"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.switch",
|
||||||
|
"typeVersion": 3.2,
|
||||||
|
"position": [440, 0],
|
||||||
|
"id": "switch-action",
|
||||||
|
"name": "Switch Action"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"connections": {
|
||||||
|
"Telegram Trigger": {
|
||||||
|
"main": [[{ "node": "Parse Command", "type": "main", "index": 0 }]]
|
||||||
|
},
|
||||||
|
"Parse Command": {
|
||||||
|
"main": [[{ "node": "Switch Action", "type": "main", "index": 0 }]]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"active": true,
|
||||||
|
"settings": {
|
||||||
|
"executionOrder": "v1"
|
||||||
|
}
|
||||||
|
}
|
||||||
219
n8n-workflows/finishedBooks.json
Normal file
219
n8n-workflows/finishedBooks.json
Normal file
@@ -0,0 +1,219 @@
|
|||||||
|
{
|
||||||
|
"name": "finishedBooks",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"rule": {
|
||||||
|
"interval": [
|
||||||
|
{
|
||||||
|
"triggerAtHour": 6
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.scheduleTrigger",
|
||||||
|
"typeVersion": 1.3,
|
||||||
|
"position": [
|
||||||
|
0,
|
||||||
|
-64
|
||||||
|
],
|
||||||
|
"id": "7170586a-8b80-4614-b186-1b661276fd30",
|
||||||
|
"name": "Schedule Trigger"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"operation": "getAll",
|
||||||
|
"collection": "book_reviews",
|
||||||
|
"itemFields": [
|
||||||
|
"hardcover_id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"type": "@directus/n8n-nodes-directus.directus",
|
||||||
|
"typeVersion": 1,
|
||||||
|
"position": [
|
||||||
|
224,
|
||||||
|
-64
|
||||||
|
],
|
||||||
|
"id": "145cc646-45d1-4ce7-9f04-77debe503ec6",
|
||||||
|
"name": "Get_Existing_Books",
|
||||||
|
"credentials": {
|
||||||
|
"directusApi": {
|
||||||
|
"id": "QnVxKFcSXqpaG86u",
|
||||||
|
"name": "Directus"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"method": "POST",
|
||||||
|
"url": "https://api.hardcover.app/v1/graphql",
|
||||||
|
"authentication": "genericCredentialType",
|
||||||
|
"genericAuthType": "httpBearerAuth",
|
||||||
|
"sendQuery": true,
|
||||||
|
"queryParameters": {
|
||||||
|
"parameters": [
|
||||||
|
{}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"sendHeaders": true,
|
||||||
|
"headerParameters": {
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "content-type",
|
||||||
|
"value": "application/json"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"sendBody": true,
|
||||||
|
"bodyParameters": {
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "query",
|
||||||
|
"value": "query GetReadBooks { me { user_books(where: {status_id: {_eq: 3}}, limit: 10, order_by: {last_read_date: desc}) { last_read_date rating edition { title image { url } book { id contributions { author { name } } } } } } }"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.httpRequest",
|
||||||
|
"typeVersion": 4.3,
|
||||||
|
"position": [
|
||||||
|
448,
|
||||||
|
-64
|
||||||
|
],
|
||||||
|
"id": "c2e0f7e4-a30e-4083-b4a9-a1a7e9f8ba3f",
|
||||||
|
"name": "hardcover",
|
||||||
|
"credentials": {
|
||||||
|
"httpBearerAuth": {
|
||||||
|
"id": "Kmf2fBCFkuRuWWZa",
|
||||||
|
"name": "Hardcover"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"jsCode": "// 1. Alle gelesenen Bücher von Hardcover holen\nconst hcData = $input.all()[0]?.json;\nconst hcBooks = hcData?.data?.me?.[0]?.user_books || [];\n// 2. Alle bereits in Directus existierenden IDs holen\nlet existingIds = [];\ntry{\n const existingItems = $('Get_Existing_Books').all();\n existingIds = existingItems.map(item => item.json.hardcover_id?.toString());\n } catch (e) {\n // Falls noch gar keine Bücher in Directus sind, ist die Liste einfach leer\n existingIds = [];\n}\n// 3. Filtern: Nur Bücher behalten, deren ID noch NICHT in Directus ist\nconst newBooks = hcBooks.filter(entry => {\n const id = entry.edition.book.id.toString();\n return !existingIds.includes(id);\n});\n// 4. Die neuen Bücher für Directus formatieren\nreturn newBooks.map(entry => {\n const ed = entry.edition || {};\n return {\n json: {\n book_title: ed.title,\n book_author: ed.book?.contributions?.[0]?.author?.name || \"Unbekannter Autor\",\n book_image: ed.image?.url || null,\n hardcover_id: ed.book?.id?.toString(),\n finished_at: entry.last_read_date,\n rating: entry.rating || null,\n status: \"draft\"\n }\n };\n});"
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.code",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"position": [
|
||||||
|
672,
|
||||||
|
-64
|
||||||
|
],
|
||||||
|
"id": "a0bc4f01-264f-46c3-a667-359983109a72",
|
||||||
|
"name": "removeDuplicates"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"collection": "book_reviews",
|
||||||
|
"collectionFields": {
|
||||||
|
"fields": {
|
||||||
|
"field": [
|
||||||
|
{
|
||||||
|
"name": "status",
|
||||||
|
"value": "={{ $json.status }}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "book_title",
|
||||||
|
"value": "={{ $json.book_title }}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "book_author",
|
||||||
|
"value": "={{ $json.book_author }}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "rating",
|
||||||
|
"value": "={{ $json.rating }}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "book_image",
|
||||||
|
"value": "={{ $json.book_image }}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "hardcover_id",
|
||||||
|
"value": "={{ $json.hardcover_id }}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "finished_at",
|
||||||
|
"value": "={{ $json.finished_at }}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "@directus/n8n-nodes-directus.directus",
|
||||||
|
"typeVersion": 1,
|
||||||
|
"position": [
|
||||||
|
896,
|
||||||
|
-64
|
||||||
|
],
|
||||||
|
"id": "0f3db869-1832-4041-8d1d-2a3d834922f0",
|
||||||
|
"name": "Create an item",
|
||||||
|
"credentials": {
|
||||||
|
"directusApi": {
|
||||||
|
"id": "QnVxKFcSXqpaG86u",
|
||||||
|
"name": "Directus"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"pinData": {},
|
||||||
|
"connections": {
|
||||||
|
"Schedule Trigger": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Get_Existing_Books",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Get_Existing_Books": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "hardcover",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hardcover": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "removeDuplicates",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"removeDuplicates": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Create an item",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"active": true,
|
||||||
|
"settings": {
|
||||||
|
"executionOrder": "v1",
|
||||||
|
"availableInMCP": false
|
||||||
|
},
|
||||||
|
"versionId": "2fa60722-a717-44da-9047-c867a440609c",
|
||||||
|
"meta": {
|
||||||
|
"templateCredsSetupCompleted": true,
|
||||||
|
"instanceId": "cb28e4db755465d5826da179e87f69603d81f833414cc52c327be9183a217b8d"
|
||||||
|
},
|
||||||
|
"id": "sbpapdCb7OBoRdc_3j0VL",
|
||||||
|
"tags": []
|
||||||
|
}
|
||||||
258
n8n-workflows/portfolio-website.json
Normal file
258
n8n-workflows/portfolio-website.json
Normal file
File diff suppressed because one or more lines are too long
141
n8n-workflows/reading (1).json
Normal file
141
n8n-workflows/reading (1).json
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
{
|
||||||
|
"name": "reading",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"path": "/hardcover/currently-reading",
|
||||||
|
"responseMode": "responseNode",
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.webhook",
|
||||||
|
"typeVersion": 2.1,
|
||||||
|
"position": [
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"id": "3e611a99-cbf7-48a6-b75b-f136ac76055f",
|
||||||
|
"name": "Webhook",
|
||||||
|
"webhookId": "02c226fd-2d1a-450c-9941-ff438dc5c987"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"method": "POST",
|
||||||
|
"url": "https://api.hardcover.app/v1/graphql",
|
||||||
|
"authentication": "genericCredentialType",
|
||||||
|
"genericAuthType": "httpBearerAuth",
|
||||||
|
"sendQuery": true,
|
||||||
|
"queryParameters": {
|
||||||
|
"parameters": [
|
||||||
|
{}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"sendHeaders": true,
|
||||||
|
"headerParameters": {
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "content-type",
|
||||||
|
"value": "application/json"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"sendBody": true,
|
||||||
|
"bodyParameters": {
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "query",
|
||||||
|
"value": "query GetCurrentlyReading { me { user_books(where: {status_id: {_eq: 2}}) { user_book_reads(limit: 1, order_by: {started_at: desc}) { progress } edition { title image { url } book { contributions { author { name } } } } } } }"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.httpRequest",
|
||||||
|
"typeVersion": 4.3,
|
||||||
|
"position": [
|
||||||
|
288,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"id": "b2a74fcb-93a9-4a28-905f-076a51a80a98",
|
||||||
|
"name": "HTTP Request",
|
||||||
|
"credentials": {
|
||||||
|
"httpBearerAuth": {
|
||||||
|
"id": "Kmf2fBCFkuRuWWZa",
|
||||||
|
"name": "Hardcover"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"jsCode": "// Hardcover API Response kommt als GraphQL Response\n// Die Response ist ein Array: [{ data: { me: [{ user_books: [...] }] } }]\nconst graphqlResponse = $input.all()[0].json;\n\n// Extrahiere die Daten - Response-Struktur: [{ data: { me: [{ user_books: [...] }] } }]\nconst responseData = Array.isArray(graphqlResponse) ? graphqlResponse[0] : graphqlResponse;\nconst meData = responseData?.data?.me;\nconst userBooks = (Array.isArray(meData) && meData[0]?.user_books) || meData?.user_books || [];\n\nif (!userBooks || userBooks.length === 0) {\n return {\n json: {\n currentlyReading: null\n }\n };\n}\n\n// Sortiere nach Fortschritt, falls mehrere Bücher vorhanden sind\nconst sortedBooks = userBooks.sort((a, b) => {\n const progressA = a.user_book_reads?.[0]?.progress || 0;\n const progressB = b.user_book_reads?.[0]?.progress || 0;\n return progressB - progressA; // Höchster zuerst\n});\n\n// Formatiere alle Bücher\nconst formattedBooks = sortedBooks.map(book => {\n const edition = book.edition || {};\n const bookData = edition.book || {};\n const contributions = bookData.contributions || [];\n const authors = contributions\n .filter(c => c.author && c.author.name)\n .map(c => c.author.name);\n \n const readData = book.user_book_reads?.[0] || {};\n const progress = readData.progress || 0;\n const image = edition.image?.url || null;\n\n return {\n title: edition.title || 'Unknown Title',\n authors: authors.length > 0 ? authors : ['Unknown Author'],\n image: image,\n progress: Math.round(progress) || 0, // Progress ist bereits in Prozent (z.B. 65.75)\n startedAt: readData.started_at || null,\n };\n});\n\n// Gib alle Bücher zurück\nreturn {\n json: {\n currentlyReading: formattedBooks.length > 0 ? formattedBooks : null\n }\n};"
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.code",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"position": [
|
||||||
|
592,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"id": "eff96166-8be2-4ece-b338-2b4dec1ee26a",
|
||||||
|
"name": "Code in JavaScript"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"type": "n8n-nodes-base.respondToWebhook",
|
||||||
|
"typeVersion": 1.5,
|
||||||
|
"position": [
|
||||||
|
944,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"id": "80c59480-69db-4ecb-80f4-ddeec2be8376",
|
||||||
|
"name": "Respond to Webhook"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"pinData": {},
|
||||||
|
"connections": {
|
||||||
|
"Webhook": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "HTTP Request",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"HTTP Request": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Code in JavaScript",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Code in JavaScript": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Respond to Webhook",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"active": true,
|
||||||
|
"settings": {
|
||||||
|
"executionOrder": "v1",
|
||||||
|
"availableInMCP": false
|
||||||
|
},
|
||||||
|
"versionId": "63a2c985-4b40-44ca-a40d-e7048ac5619b",
|
||||||
|
"meta": {
|
||||||
|
"instanceId": "cb28e4db755465d5826da179e87f69603d81f833414cc52c327be9183a217b8d"
|
||||||
|
},
|
||||||
|
"id": "P2itbbCCQVa0C0HTIVGvy",
|
||||||
|
"tags": []
|
||||||
|
}
|
||||||
35
test-docker-webhook.ps1
Normal file
35
test-docker-webhook.ps1
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# Test 1: Eigenes Projekt (sollte hohen Coolness Score bekommen)
|
||||||
|
curl -X POST https://n8n.dk0.dev/webhook/docker-event `
|
||||||
|
-H "Content-Type: application/json" `
|
||||||
|
-d '{
|
||||||
|
"container": "portfolio-dev",
|
||||||
|
"image": "denshooter/portfolio:latest",
|
||||||
|
"timestamp": "2026-04-01T23:18:00Z"
|
||||||
|
}'
|
||||||
|
|
||||||
|
# Test 2: Bekanntes Self-Hosted Tool (mittlerer Score)
|
||||||
|
curl -X POST https://n8n.dk0.dev/webhook/docker-event `
|
||||||
|
-H "Content-Type: application/json" `
|
||||||
|
-d '{
|
||||||
|
"container": "plausible-analytics",
|
||||||
|
"image": "plausible/analytics:latest",
|
||||||
|
"timestamp": "2026-04-01T23:18:00Z"
|
||||||
|
}'
|
||||||
|
|
||||||
|
# Test 3: CI/CD Runner (sollte ignoriert werden)
|
||||||
|
curl -X POST https://n8n.dk0.dev/webhook/docker-event `
|
||||||
|
-H "Content-Type: application/json" `
|
||||||
|
-d '{
|
||||||
|
"container": "gitea-actions-task-351-workflow-ci-cd-job-test-build",
|
||||||
|
"image": "catthehacker/ubuntu:act-latest",
|
||||||
|
"timestamp": "2026-04-01T23:18:00Z"
|
||||||
|
}'
|
||||||
|
|
||||||
|
# Test 4: Spannendes Sicherheitstool (hoher Score)
|
||||||
|
curl -X POST https://n8n.dk0.dev/webhook/docker-event `
|
||||||
|
-H "Content-Type: application/json" `
|
||||||
|
-d '{
|
||||||
|
"container": "suricata-ids",
|
||||||
|
"image": "jasonish/suricata:latest",
|
||||||
|
"timestamp": "2026-04-01T23:18:00Z"
|
||||||
|
}'
|
||||||
Reference in New Issue
Block a user