fix: remove review truncation and show full reviews; fix telegram-cms workflow bugs

- ReadBooks.tsx: remove line-clamp-3, readMore button, and review modal
- Show full review text inline instead of truncated snippets
- Remove unused AnimatePresence, X import, selectedReview state
- Fix  typo in 6 handler nodes
- Fix Markdown/HTML mix (*text*</b> → <b>text</b>)
- Fix Switch condition syntax (.action → .action)
- Fix position collision (Review Info Handler)
- Hardcode Telegram bot token, fix response handling in Publish Handler
- Add AI-generated questions for .review flow (was .review HC_ID TEXT)
- New .answer command for submitting review answers
- Create/Refine Review: POST new translations if missing instead of skipping
- Remove all substring truncations from Telegram messages
This commit is contained in:
2026-04-09 17:22:23 +02:00
parent a36268302c
commit a958008add
22 changed files with 746 additions and 4283 deletions

View File

@@ -1,305 +0,0 @@
{
"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": []
}

View File

@@ -1,278 +0,0 @@
# 🎯 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

View File

@@ -1,372 +0,0 @@
# ✅ 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
```

View File

@@ -1,459 +0,0 @@
{
"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": []
}

View File

@@ -1,285 +0,0 @@
# 🎯 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

View File

@@ -1,514 +0,0 @@
{
"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"
}

View File

@@ -1,181 +0,0 @@
{
"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"
}
}

View File

@@ -0,0 +1,740 @@
{
"name": "🎯 ULTIMATE Telegram CMS COMPLETE",
"nodes": [
{
"parameters": {
"updates": [
"message",
"callback_query"
],
"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 input = $input.first().json;\nconst token = '8166414331:AAGNQ6fn2juD5esaTRxPjtTdSMkwq_oASIc';\n\nif (input.callback_query) {\n const cbq = input.callback_query;\n const chatId = cbq.message.chat.id;\n const data = cbq.data;\n const callbackQueryId = cbq.id;\n \n if (token) {\n try {\n await this.helpers.httpRequest({ \n method: 'POST', \n url: 'https://api.telegram.org/bot' + token + '/answerCallbackQuery', \n headers: { 'Content-Type': 'application/json' }, \n body: { callback_query_id: callbackQueryId } \n });\n } catch(e) {}\n }\n \n const parts = data.split(':');\n const action = parts[0];\n \n if (action === 'start') return [{ json: { action: 'start', chatId } }];\n if (action === 'stats') return [{ json: { action: 'stats', chatId } }];\n if (action === 'list') return [{ json: { action: 'list', type: parts[1], page: parseInt(parts[2] || '1'), chatId } }];\n if (action === 'preview') return [{ json: { action: 'preview', collectionType: parts[1], id: parts[2], chatId } }];\n if (action === 'publish') return [{ json: { action: 'publish', collectionType: parts[1], id: parts[2], chatId } }];\n if (action === 'delete') return [{ json: { action: 'delete', collectionType: parts[1], id: parts[2], chatId } }];\n if (action === 'review_info') return [{ json: { action: 'review_info', id: parts[1], chatId } }];\n \n return [{ json: { action: 'unknown', chatId } }];\n}\n\nconst text = input.message?.text ?? '';\nconst chatId = input.message?.chat?.id;\nlet match;\n\nif (text === '/start') return [{ json: { action: 'start', chatId } }];\nif (text === '/stats') return [{ json: { action: 'stats', chatId } }];\n\nmatch = text.match(/^\\/list\\s+(projects|books)(?:\\s+(\\d+))?/);\nif (match) return [{ json: { action: 'list', type: match[1], page: parseInt(match[2] || '1'), chatId } }];\n\nmatch = text.match(/^\\/preview\\s*(project|book)?(\\d+)/);\nif (match) {\n const typePrefix = match[1] === 'project' ? 'projects' : match[1] === 'book' ? 'book_reviews' : 'projects';\n return [{ json: { action: 'preview', collectionType: typePrefix, id: match[2], chatId } }];\n}\n\nmatch = text.match(/^\\/search\\s+(.+)/);\nif (match) return [{ json: { action: 'search', query: match[1].trim(), chatId } }];\n\nmatch = text.match(/^\\/publish\\s*(project|book)?(\\d+)/);\nif (match) {\n const typePrefix = match[1] === 'book' ? 'books' : 'projects';\n return [{ json: { action: 'publish', collectionType: typePrefix, id: match[2], chatId } }];\n}\n\nmatch = text.match(/^\\/delete\\s*(project|book)?(\\d+)/);\nif (match) {\n const typePrefix = match[1] === 'book' ? 'books' : 'projects';\n return [{ json: { action: 'delete', collectionType: typePrefix, id: match[2], chatId } }];\n}\n\n// .review HC_ID [RATING] -> starts review process with AI questions\nmatch = text.match(/^\\.review\\s+(\\d+)(?:\\s+([1-5]))?/);\nif (match) return [{ json: { action: 'review_info', hardcoverId: match[1], rating: match[2] ? parseInt(match[2]) : 0, chatId } }];\n\n// .answer BOOK_ID RATING your answers -> submit review answers\nmatch = text.match(/^\\.answer\\s+(\\d+)\\s+([1-5])\\s+(.+)/);\nif (match) return [{ json: { action: 'answer_review', bookId: match[1], rating: parseInt(match[2]), answers: match[3].trim(), chatId } }];\n\nmatch = text.match(/^\\.refine\\s+(\\d+)\\s+(.+)/);\nif (match) return [{ json: { action: 'refine_review', id: match[1], feedback: match[2].trim(), chatId } }];\n\nreturn [{ json: { action: 'unknown', chatId } }];\n"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
240,
240
],
"id": "global-parser-001",
"name": "Global Parser"
},
{
"parameters": {
"rules": {
"values": [
{
"conditions": {
"conditions": [
{
"leftValue": "={{ $json.action }}",
"rightValue": "start",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and",
"options": {
"caseSensitive": true,
"leftValue": ""
}
},
"renameOutput": true,
"outputKey": "start"
},
{
"conditions": {
"conditions": [
{
"leftValue": "={{ $json.action }}",
"rightValue": "list",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and",
"options": {
"caseSensitive": true,
"leftValue": ""
}
},
"renameOutput": true,
"outputKey": "list"
},
{
"conditions": {
"conditions": [
{
"leftValue": "={{ $json.action }}",
"rightValue": "search",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and",
"options": {
"caseSensitive": true,
"leftValue": ""
}
},
"renameOutput": true,
"outputKey": "search"
},
{
"conditions": {
"conditions": [
{
"leftValue": "={{ $json.action }}",
"rightValue": "stats",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and",
"options": {
"caseSensitive": true,
"leftValue": ""
}
},
"renameOutput": true,
"outputKey": "stats"
},
{
"conditions": {
"conditions": [
{
"leftValue": "={{ $json.action }}",
"rightValue": "preview",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and",
"options": {
"caseSensitive": true,
"leftValue": ""
}
},
"renameOutput": true,
"outputKey": "preview"
},
{
"conditions": {
"conditions": [
{
"leftValue": "={{ $json.action }}",
"rightValue": "publish",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and",
"options": {
"caseSensitive": true,
"leftValue": ""
}
},
"renameOutput": true,
"outputKey": "publish"
},
{
"conditions": {
"conditions": [
{
"leftValue": "={{ $json.action }}",
"rightValue": "delete",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and",
"options": {
"caseSensitive": true,
"leftValue": ""
}
},
"renameOutput": true,
"outputKey": "delete"
},
{
"conditions": {
"conditions": [
{
"leftValue": "={{ $json.action }}",
"rightValue": "delete_review",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and",
"options": {
"caseSensitive": true,
"leftValue": ""
}
},
"renameOutput": true,
"outputKey": "delete_review"
},
{
"conditions": {
"conditions": [
{
"leftValue": "={{ $json.action }}",
"rightValue": "answer_review",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and",
"options": {
"caseSensitive": true,
"leftValue": ""
}
},
"renameOutput": true,
"outputKey": "answer_review"
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": ""
},
"conditions": [
{
"leftValue": "={{ $json.action }}",
"rightValue": "refine_review",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "refine_review"
},
{
"conditions": {
"conditions": [
{
"leftValue": "={{ $json.action }}",
"rightValue": "unknown",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and",
"options": {
"caseSensitive": true,
"leftValue": ""
}
},
"renameOutput": true,
"outputKey": "unknown"
},
{
"conditions": {
"conditions": [
{
"leftValue": "={{ $json.action }}",
"rightValue": "review_info",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and",
"options": {
"caseSensitive": true,
"leftValue": ""
}
},
"renameOutput": true,
"outputKey": "review_info"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.switch",
"typeVersion": 3.2,
"position": [
480,
240
],
"id": "router-001",
"name": "Command Router"
},
{
"parameters": {
"jsCode": "\ntry {\n var chatId = $input.first().json.chatId;\n var projectsResp = await this.helpers.httpRequest({ method: 'GET', url: 'https://cms.dk0.dev/items/projects?aggregate[count]=id&filter[status][_eq]=draft', headers: { 'Authorization': 'Bearer RF2QytqhcLXuVy6FO3PzWlsoR-ysCTwB' } });\n var draftProjects = (projectsResp && projectsResp.data && projectsResp.data[0] && projectsResp.data[0].count && projectsResp.data[0].count.id) || 0;\n var booksResp = await this.helpers.httpRequest({ method: 'GET', url: 'https://cms.dk0.dev/items/book_reviews?aggregate[count]=id&filter[status][_eq]=draft', headers: { 'Authorization': 'Bearer RF2QytqhcLXuVy6FO3PzWlsoR-ysCTwB' } });\n var draftBooks = (booksResp && booksResp.data && booksResp.data[0] && booksResp.data[0].count && booksResp.data[0].count.id) || 0;\n var message = '\\u{1F3AF} <b>DK0 Portfolio CMS</b>\\n\\n\\u{1F4CA} <b>Status:</b>\\n\\u2022 Draft Projects: ' + draftProjects + '\\n\\u2022 Draft Reviews: ' + draftBooks + '\\n\\nTap a button to navigate.';\n var keyboard = [\n [{ text: '\\u{1F4CB} Projects', callback_data: 'list:projects:1' }, { text: '\\u{1F4DA} Books', callback_data: 'list:books:1' }],\n [{ text: '\\u{1F4CA} Stats', callback_data: 'stats' }]\n ];\n return [{ json: { chatId: chatId, message: message, parseMode: 'HTML', keyboard: keyboard } }];\n} catch (error) {\n return [{ json: { chatId: $input.first().json.chatId, message: '\\u274C Error loading dashboard: ' + error.message, parseMode: 'HTML' } }];\n}\n"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
720,
-120
],
"id": "dashboard-001",
"name": "Dashboard Handler"
},
{
"parameters": {
"jsCode": "\ntry {\n var input = $input.first().json;\n var type = input.type;\n var page = input.page || 1;\n var chatId = input.chatId;\n var limit = 5;\n var offset = (page - 1) * limit;\n var collection = type === 'projects' ? 'projects' : 'book_reviews';\n var fields = type === 'projects' ? 'id,slug,category,status,date_created,translations.*' : 'id,book_title,rating,status,finished_at';\n var url = 'https://cms.dk0.dev/items/' + collection + '?limit=' + limit + '&offset=' + offset + '&sort=' + (type === 'projects' ? '-date_created' : '-finished_at') + '&fields=' + fields;\n var response = await this.helpers.httpRequest({ method: 'GET', url: url, headers: { 'Authorization': 'Bearer RF2QytqhcLXuVy6FO3PzWlsoR-ysCTwB' } });\n var items = (response && response.data) || [];\n if (items.length === 0) {\n return [{ json: { chatId: chatId, message: 'No ' + type + ' found.', parseMode: 'HTML', keyboard: [[{ text: '\\u{1F3E0} Home', callback_data: 'start' }]] } }];\n }\n var message = '<b>' + type.toUpperCase() + ' (Page ' + page + ')</b>\\n\\n';\n var keyboard = [];\n items.forEach(function(item, idx) {\n var num = idx + 1;\n var displayNum = (offset || 0) + num;\n if (type === 'projects') {\n var title = (item.translations && item.translations[0] && item.translations[0].title) || item.slug || 'Untitled';\n message += displayNum + '. <b>' + title + '</b>\\n ' + (item.category || 'N/A') + ' | ' + item.status + '\\n\\n';\n } else {\n var stars = '';\n for (var s = 0; s < (item.rating || 0); s++) { stars += '\\u2B50'; }\n message += displayNum + '. <b>' + (item.book_title || 'Untitled') + '</b>\\n ' + stars + ' | ' + item.status + '\\n\\n';\n }\n var row = [\n { text: '\\u{1F441} #' + displayNum, callback_data: 'preview:' + type + ':' + item.id },\n { text: '\\u2705 Pub #' + displayNum, callback_data: 'publish:' + type + ':' + item.id }\n ];\n if (type === 'books' && item.status === 'draft') {\n row.push({ text: '\\u270D\\uFE0F Review #' + displayNum, callback_data: 'review_info:' + item.id });\n }\n row.push({ text: '\\u{1F5D1} Del #' + displayNum, callback_data: 'delete:' + type + ':' + item.id });\n keyboard.push(row);\n });\n var navRow = [];\n if (page > 1) { navRow.push({ text: '\\u2190 Prev', callback_data: 'list:' + type + ':' + (page - 1) }); }\n if (items.length === limit) { navRow.push({ text: 'Next \\u2192', callback_data: 'list:' + type + ':' + (page + 1) }); }\n navRow.push({ text: '\\u{1F3E0} Home', callback_data: 'start' });\n keyboard.push(navRow);\n return [{ json: { chatId: chatId, message: message, parseMode: 'HTML', keyboard: keyboard } }];\n} catch (error) {\n return [{ json: { chatId: $input.first().json.chatId, message: '\\u274C Error fetching list: ' + error.message, parseMode: 'HTML' } }];\n}\n"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
720,
0
],
"id": "list-handler-001",
"name": "List Handler"
},
{
"parameters": {
"jsCode": "try {\n var input = $input.first().json;\n var query = input.query;\n var chatId = input.chatId;\n var encoded = encodeURIComponent(query);\n var projectsResp = await this.helpers.httpRequest({ method: 'GET', url: 'https://cms.dk0.dev/items/projects?filter[translations][title][_contains]=' + encoded + '&limit=5&fields=id,slug,category,translations.*', headers: { 'Authorization': 'Bearer RF2QytqhcLXuVy6FO3PzWlsoR-ysCTwB' } });\n var booksResp = await this.helpers.httpRequest({ method: 'GET', url: 'https://cms.dk0.dev/items/book_reviews?filter[book_title][_contains]=' + encoded + '&limit=5&fields=id,book_title,book_author,rating', headers: { 'Authorization': 'Bearer RF2QytqhcLXuVy6FO3PzWlsoR-ysCTwB' } });\n var projects = (projectsResp && projectsResp.data) || [];\n var books = (booksResp && booksResp.data) || [];\n if (projects.length === 0 && books.length === 0) {\n return [{ json: { chatId: chatId, message: '\\u{1F50D} No results for \"' + query + '\"', parseMode: 'HTML', keyboard: [[{ text: '\\u{1F3E0} Home', callback_data: 'start' }]] } }];\n }\n var message = '\\u{1F50D} <b>Search: \"' + query + '\"</b>\\n\\n';\n var keyboard = [];\n if (projects.length > 0) {\n message += '\\u{1F4C1} <b>Projects (' + projects.length + '):</b>\\n';\n projects.forEach(function(p) {\n var title = (p.translations && p.translations[0] && p.translations[0].title) || p.slug || 'Untitled';\n message += '\\u2022 ' + title + '\\n';\n keyboard.push([{ text: '\\u{1F441} ' + title, callback_data: 'preview:projects:' + p.id }]);\n });\n message += '\\n';\n }\n if (books.length > 0) {\n message += '\\u{1F4DA} <b>Books (' + books.length + '):</b>\\n';\n books.forEach(function(b) {\n message += '\\u2022 ' + b.book_title + ' by ' + b.book_author + '\\n';\n keyboard.push([{ text: '\\u{1F441} ' + b.book_title, callback_data: 'preview:books:' + b.id }]);\n });\n }\n keyboard.push([{ text: '\\u{1F3E0} Home', callback_data: 'start' }]);\n return [{ json: { chatId: chatId, message: message, parseMode: 'HTML', keyboard: keyboard } }];\n} catch (error) {\n return [{ json: { chatId: $input.first().json.chatId, message: '\\u274C Error searching: ' + error.message, parseMode: 'HTML' } }];\n}"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
720,
120
],
"id": "search-handler-001",
"name": "Search Handler"
},
{
"parameters": {
"jsCode": "try {\n var chatId = $input.first().json.chatId;\n var projectsResp = await this.helpers.httpRequest({ method: 'GET', url: 'https://cms.dk0.dev/items/projects?fields=id,category,status,date_created', headers: { 'Authorization': 'Bearer RF2QytqhcLXuVy6FO3PzWlsoR-ysCTwB' } });\n var booksResp = await this.helpers.httpRequest({ method: 'GET', url: 'https://cms.dk0.dev/items/book_reviews?fields=id,rating,status,finished_at', headers: { 'Authorization': 'Bearer RF2QytqhcLXuVy6FO3PzWlsoR-ysCTwB' } });\n var projects = (projectsResp && projectsResp.data) || [];\n var books = (booksResp && booksResp.data) || [];\n var pPublished = projects.filter(function(p) { return p.status === 'published'; }).length;\n var pDraft = projects.filter(function(p) { return p.status === 'draft'; }).length;\n var pArchived = projects.filter(function(p) { return p.status === 'archived'; }).length;\n var bPublished = books.filter(function(b) { return b.status === 'published'; }).length;\n var bDraft = books.filter(function(b) { return b.status === 'draft'; }).length;\n var bAvg = books.length > 0 ? (books.reduce(function(sum, b) { return sum + (b.rating || 0); }, 0) / books.length).toFixed(1) : 0;\n var categories = {};\n projects.forEach(function(p) { if (p.category) { categories[p.category] = (categories[p.category] || 0) + 1; } });\n var message = '\\u{1F4CA} <b>DK0 Portfolio Statistics</b>\\n\\n\\u{1F4C1} <b>Projects:</b>\\n\\u2022 Total: ' + projects.length + '\\n\\u2022 Published: ' + pPublished + '\\n\\u2022 Draft: ' + pDraft + '\\n\\u2022 Archived: ' + pArchived + '\\n\\n\\u{1F4DA} <b>Book Reviews:</b>\\n\\u2022 Total: ' + books.length + '\\n\\u2022 Published: ' + bPublished + '\\n\\u2022 Draft: ' + bDraft + '\\n\\u2022 Avg Rating: ' + bAvg + '/5\\n';\n var catEntries = Object.entries(categories).sort(function(a, b) { return b[1] - a[1]; });\n if (catEntries.length > 0) {\n message += '\\n\\u{1F3F7}\\uFE0F <b>Categories:</b>\\n';\n catEntries.forEach(function(entry) { message += '\\u2022 ' + entry[0] + ': ' + entry[1] + '\\n'; });\n }\n var keyboard = [\n [{ text: '\\u{1F4CB} Projects', callback_data: 'list:projects:1' }, { text: '\\u{1F4DA} Books', callback_data: 'list:books:1' }],\n [{ text: '\\u{1F3E0} Home', callback_data: 'start' }]\n ];\n return [{ json: { chatId: chatId, message: message, parseMode: 'HTML', keyboard: keyboard } }];\n} catch (error) {\n return [{ json: { chatId: $input.first().json.chatId, message: '\\u274C Error loading stats: ' + error.message, parseMode: 'HTML' } }];\n}"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
720,
240
],
"id": "stats-handler-001",
"name": "Stats Handler"
},
{
"parameters": {
"jsCode": "\ntry {\n var input = $input.first().json;\n var id = input.id;\n var chatId = input.chatId;\n var collectionType = input.collectionType;\n \n var response, collection;\n \n if (collectionType === 'projects' || collectionType === 'project') {\n response = await this.helpers.httpRequest({ method: 'GET', url: 'https://cms.dk0.dev/items/projects/' + id + '?fields=id,slug,category,status,date_created,translations.*', headers: { 'Authorization': 'Bearer RF2QytqhcLXuVy6FO3PzWlsoR-ysCTwB' }, returnFullResponse: true }).catch(function() { return null; });\n collection = 'projects';\n } else if (collectionType === 'books' || collectionType === 'book') {\n response = await this.helpers.httpRequest({ method: 'GET', url: 'https://cms.dk0.dev/items/book_reviews/' + id + '?fields=id,book_title,book_author,rating,status,hardcover_id,translations.*', headers: { 'Authorization': 'Bearer RF2QytqhcLXuVy6FO3PzWlsoR-ysCTwB' }, returnFullResponse: true }).catch(function() { return null; });\n collection = 'book_reviews';\n } else {\n response = await this.helpers.httpRequest({ method: 'GET', url: 'https://cms.dk0.dev/items/projects/' + id + '?fields=id,slug,category,status,date_created,translations.*', headers: { 'Authorization': 'Bearer RF2QytqhcLXuVy6FO3PzWlsoR-ysCTwB' }, returnFullResponse: true }).catch(function() { return null; });\n collection = 'projects';\n var itemTry = response && response.body && response.body.data;\n if (!itemTry) {\n response = await this.helpers.httpRequest({ method: 'GET', url: 'https://cms.dk0.dev/items/book_reviews/' + id + '?fields=id,book_title,book_author,rating,status,hardcover_id,translations.*', headers: { 'Authorization': 'Bearer RF2QytqhcLXuVy6FO3PzWlsoR-ysCTwB' }, returnFullResponse: true }).catch(function() { return null; });\n collection = 'book_reviews';\n }\n }\n\n var item = response && response.body && response.body.data;\n if (!item) {\n return [{ json: { chatId: chatId, message: '\\u274C Item #' + id + ' not found.', parseMode: 'HTML', keyboard: [[{ text: '\\u{1F3E0} Home', callback_data: 'start' }]] } }];\n }\n \n var message = '\\u{1F441}\\uFE0F <b>Preview #' + id + '</b>\\n\\n';\n if (collection === 'projects') {\n message += '\\u{1F4C1} <b>Type:</b> Project\\n\\u{1F516} <b>Slug:</b> ' + item.slug + '\\n\\u{1F3F7}\\uFE0F <b>Category:</b> ' + (item.category || 'N/A') + '\\n\\u{1F4CA} <b>Status:</b> ' + item.status + '\\n\\n';\n var translations = item.translations || [];\n translations.forEach(function(t) {\n var lang = t.languages_code === 'en-US' ? '\\u{1F1EC}\\u{1F1E7} EN' : '\\u{1F1E9}\\u{1F1EA} DE';\n message += lang + ':\\n<b>Title:</b> ' + (t.title || 'N/A') + '\\n<b>Desc:</b> ' + ((t.description || 'N/A')) + '...\\n\\n';\n });\n } else {\n message += '\\u{1F4DA} <b>Type:</b> Book Review\\n\\u{1F4D6} <b>Title:</b> ' + item.book_title + '\\n\\u270D\\uFE0F <b>Author:</b> ' + item.book_author + '\\n\\u2B50 <b>Rating:</b> ' + item.rating + '/5\\n\\u{1F4CA} <b>Status:</b> ' + item.status + '\\n\\u{1F517} <b>HC-ID:</b> ' + item.hardcover_id + '\\n\\n';\n var translations = item.translations || [];\n translations.forEach(function(t) {\n var lang = t.languages_code === 'en-US' ? '\\u{1F1EC}\\u{1F1E7} EN' : '\\u{1F1E9}\\u{1F1EA} DE';\n message += lang + ':\\n' + ((t.review || 'No review')) + '...\\n\\n';\n });\n }\n var listType = collection === 'projects' ? 'projects' : 'books';\n var keyboard = [\n [{ text: '\\u2705 Publish', callback_data: 'publish:' + listType + ':' + id }, { text: '\\u{1F5D1} Delete', callback_data: 'delete:' + listType + ':' + id }],\n [{ text: '\\u2190 Back', callback_data: 'list:' + listType + ':1' }, { text: '\\u{1F3E0} Home', callback_data: 'start' }]\n ];\n return [{ json: { chatId: chatId, message: message, parseMode: 'HTML', keyboard: keyboard } }];\n} catch (error) {\n return [{ json: { chatId: $input.first().json.chatId, message: '\\u274C Error loading preview: ' + error.message, parseMode: 'HTML' } }];\n}\n"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
720,
360
],
"id": "preview-handler-001",
"name": "Preview Handler"
},
{
"parameters": {
"jsCode": "try {\n var input = $input.first().json;\n var id = input.id;\n var chatId = input.chatId;\n var collectionType = input.collectionType;\n \n var url, title, listType;\n \n if (collectionType === 'projects' || collectionType === 'project') {\n url = 'https://cms.dk0.dev/items/projects/' + id;\n title = 'Project';\n listType = 'projects';\n } else {\n url = 'https://cms.dk0.dev/items/book_reviews/' + id;\n title = 'Book Review';\n listType = 'books';\n }\n \n var response;\n try {\n response = await this.helpers.httpRequest({\n method: 'PATCH',\n url: url,\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': 'Bearer RF2QytqhcLXuVy6FO3PzWlsoR-ysCTwB'\n },\n body: { status: 'published' }\n });\n } catch(e) {\n return [{ json: { chatId: chatId, message: '\\u274C <b>Publish fehlgeschlagen</b>\\n\\n' + e.message, parseMode: 'HTML', keyboard: [[{ text: '\\u{1F3E0} Home', callback_data: 'start' }]] } }];\n }\n \n var result = response.data || response;\n if (!result || !result.id) {\n return [{ json: { chatId: chatId, message: '\\u274C <b>Publish fehlgeschlagen</b>\\n\\nKeine Bestaetigung von Directus.', parseMode: 'HTML', keyboard: [[{ text: '\\u{1F3E0} Home', callback_data: 'start' }]] } }];\n }\n \n var keyboard = [[{ text: '\\u{1F4CB} ' + (listType === 'projects' ? 'Projects' : 'Books'), callback_data: 'list:' + listType + ':1' }, { text: '\\u{1F3E0} Home', callback_data: 'start' }]];\n return [{ json: { chatId: chatId, message: '\\u2705 <b>' + title + ' #' + id + ' Published!</b>\\n\\nNow live on dk0.dev.', parseMode: 'HTML', keyboard: keyboard } }];\n} catch (error) {\n return [{ json: { chatId: $input.first().json.chatId, message: '\\u274C Error publishing: ' + error.message, parseMode: 'HTML' } }];\n}"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
720,
480
],
"id": "publish-handler-001",
"name": "Publish Handler"
},
{
"parameters": {
"jsCode": "try {\n var input = $input.first().json;\n var id = input.id;\n var chatId = input.chatId;\n var collectionType = input.collectionType;\n \n var response, collection, title;\n \n if (collectionType === 'projects' || collectionType === 'project') {\n response = await this.helpers.httpRequest({ method: 'DELETE', url: 'https://cms.dk0.dev/items/projects/' + id, headers: { 'Authorization': 'Bearer RF2QytqhcLXuVy6FO3PzWlsoR-ysCTwB' }, returnFullResponse: true }).catch(function() { return null; });\n collection = 'projects';\n title = 'Project';\n } else if (collectionType === 'books' || collectionType === 'book') {\n response = await this.helpers.httpRequest({ method: 'DELETE', url: 'https://cms.dk0.dev/items/book_reviews/' + id, headers: { 'Authorization': 'Bearer RF2QytqhcLXuVy6FO3PzWlsoR-ysCTwB' }, returnFullResponse: true }).catch(function() { return null; });\n collection = 'book_reviews';\n title = 'Book Review';\n } else {\n // Fallback\n response = await this.helpers.httpRequest({ method: 'DELETE', url: 'https://cms.dk0.dev/items/projects/' + id, headers: { 'Authorization': 'Bearer RF2QytqhcLXuVy6FO3PzWlsoR-ysCTwB' }, returnFullResponse: true }).catch(function() { return null; });\n collection = 'projects';\n title = 'Project';\n if (!response || response.statusCode >= 400) {\n response = await this.helpers.httpRequest({ method: 'DELETE', url: 'https://cms.dk0.dev/items/book_reviews/' + id, headers: { 'Authorization': 'Bearer RF2QytqhcLXuVy6FO3PzWlsoR-ysCTwB' }, returnFullResponse: true }).catch(function() { return null; });\n collection = 'book_reviews';\n title = 'Book Review';\n }\n }\n\n if (!response || response.statusCode >= 400) {\n return [{ json: { chatId: chatId, message: '\\u274C Item #' + id + ' could not be deleted.', parseMode: 'HTML', keyboard: [[{ text: '\\u{1F3E0} Home', callback_data: 'start' }]] } }];\n }\n var listType = collection === 'projects' ? 'projects' : 'books';\n var keyboard = [[{ text: (collection === 'projects' ? '\\u{1F4CB} Projects' : '\\u{1F4DA} Books'), callback_data: 'list:' + listType + ':1' }, { text: '\\u{1F3E0} Home', callback_data: 'start' }]];\n return [{ json: { chatId: chatId, message: '\\u{1F5D1}\\uFE0F *' + title + ' #' + id + ' Deleted*', parseMode: 'HTML', keyboard: keyboard, collection: collection, itemId: id } }];\n} catch (error) {\n return [{ json: { chatId: $input.first().json.chatId, message: '\\u274C Error deleting: ' + error.message, parseMode: 'HTML' } }];\n}"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
720,
600
],
"id": "delete-handler-001",
"name": "Delete Handler"
},
{
"parameters": {
"jsCode": "try {\n var input = $input.first().json;\n var id = input.id;\n var chatId = input.chatId;\n var bookResp = await this.helpers.httpRequest({ method: 'GET', url: 'https://cms.dk0.dev/items/book_reviews/' + id + '?fields=id,book_title,translations.id', headers: { 'Authorization': 'Bearer RF2QytqhcLXuVy6FO3PzWlsoR-ysCTwB' } });\n var book = bookResp && bookResp.data;\n if (!book) {\n return [{ json: { chatId: chatId, message: '\\u274C Book review #' + id + ' not found.', parseMode: 'HTML', keyboard: [[{ text: '\\u{1F3E0} Home', callback_data: 'start' }]] } }];\n }\n var translations = book.translations || [];\n var deletedCount = 0;\n for (var i = 0; i < translations.length; i++) {\n await this.helpers.httpRequest({ method: 'DELETE', url: 'https://cms.dk0.dev/items/book_reviews_translations/' + translations[i].id, headers: { 'Authorization': 'Bearer RF2QytqhcLXuVy6FO3PzWlsoR-ysCTwB' } }).catch(function() {});\n deletedCount++;\n }\n var keyboard = [[{ text: '\\u{1F4DA} Books', callback_data: 'list:books:1' }, { text: '\\u{1F3E0} Home', callback_data: 'start' }]];\n return [{ json: { chatId: chatId, message: '\\u{1F5D1}\\uFE0F Deleted ' + deletedCount + ' review translations for \"' + book.book_title + '\".\\n\\nBook entry still exists.', parseMode: 'HTML', keyboard: keyboard, itemId: id, deletedCount: deletedCount } }];\n} catch (error) {\n return [{ json: { chatId: $input.first().json.chatId, message: '\\u274C Error deleting review: ' + error.message, parseMode: 'HTML' } }];\n}"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
720,
720
],
"id": "delete-review-handler-001",
"name": "Delete Review Handler"
},
{
"parameters": {
"jsCode": "try {\n var input = $input.first().json;\n var bookId = input.bookId;\n var rating = input.rating;\n var answers = input.answers;\n var chatId = input.chatId;\n\n var bookResp = await this.helpers.httpRequest({ method: 'GET', url: 'https://cms.dk0.dev/items/book_reviews/' + bookId + '?fields=id,book_title,book_author,rating,translations.id,translations.languages_code,translations.review', headers: { 'Authorization': 'Bearer RF2QytqhcLXuVy6FO3PzWlsoR-ysCTwB' } });\n var bookData = bookResp && bookResp.data ? bookResp.data : bookResp;\n if (!bookData || !bookData.id) {\n return [{ json: { chatId: chatId, message: '\\u274C Buch #' + bookId + ' nicht gefunden.', parseMode: 'HTML', keyboard: [[{ text: '\\u{1F3E0} Home', callback_data: 'start' }]] } }];\n }\n\n var prompt = 'Schreibe eine authentische Buchbewertung. Buch: ' + bookData.book_title + ' von ' + bookData.book_author + '. Rating: ' + rating + '/5. Antworten des Lesers auf Fragen zum Buch: ' + answers + ' Schreibe Ich-Perspektive, 4-6 Saetze pro Sprache. Verwende keine Bindestriche, Em-Dashes oder Gedankenstriche. Antworte NUR als JSON: {\"review_en\": \"English review\", \"review_de\": \"Deutsche Bewertung\"}';\n\n var aiResp = 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: 'user', content: prompt }] } });\n var aiText = (aiResp && aiResp.choices && aiResp.choices[0] && aiResp.choices[0].message && aiResp.choices[0].message.content) || '{}';\n var jsonMatch = aiText.match(/\\{[\\s\\S]*\\}/);\n var ai = jsonMatch ? JSON.parse(jsonMatch[0]) : { review_en: answers, review_de: answers };\n\n // Update rating\n await this.helpers.httpRequest({ method: 'PATCH', url: 'https://cms.dk0.dev/items/book_reviews/' + bookData.id, headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer RF2QytqhcLXuVy6FO3PzWlsoR-ysCTwB' }, body: { rating: rating } });\n\n var translations = bookData.translations || [];\n var enTrans = null, deTrans = null;\n for (var i = 0; i < translations.length; i++) {\n if (translations[i].languages_code === 'en-US') enTrans = translations[i];\n if (translations[i].languages_code === 'de-DE') deTrans = translations[i];\n }\n\n var reviewEn = ai.review_en || answers;\n var reviewDe = ai.review_de || answers;\n\n // Update existing translations (PATCH) or create new ones (POST)\n if (enTrans) {\n await this.helpers.httpRequest({ method: 'PATCH', url: 'https://cms.dk0.dev/items/book_reviews_translations/' + enTrans.id, headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer RF2QytqhcLXuVy6FO3PzWlsoR-ysCTwB' }, body: { review: reviewEn } });\n } else {\n await this.helpers.httpRequest({ method: 'POST', url: 'https://cms.dk0.dev/items/book_reviews_translations', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer RF2QytqhcLXuVy6FO3PzWlsoR-ysCTwB' }, body: { book_reviews_id: bookData.id, languages_code: 'en-US', review: reviewEn } });\n }\n\n if (deTrans) {\n await this.helpers.httpRequest({ method: 'PATCH', url: 'https://cms.dk0.dev/items/book_reviews_translations/' + deTrans.id, headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer RF2QytqhcLXuVy6FO3PzWlsoR-ysCTwB' }, body: { review: reviewDe } });\n } else {\n await this.helpers.httpRequest({ method: 'POST', url: 'https://cms.dk0.dev/items/book_reviews_translations', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer RF2QytqhcLXuVy6FO3PzWlsoR-ysCTwB' }, body: { book_reviews_id: bookData.id, languages_code: 'de-DE', review: reviewDe } });\n }\n\n var showEn = reviewEn;\n var showDe = reviewDe;\n var msg = '\\u2705 <b>Review erstellt!</b>\\n\\n\\u{1F4DA} ' + bookData.book_title + ' (' + rating + '/5)\\n\\n<b>EN:</b> ' + showEn + '\\n\\n<b>DE:</b> ' + showDe;\n var keyboard = [[{ text: '\\u{1F441} Preview', callback_data: 'preview:books:' + bookData.id }, { text: '\\u2705 Publish', callback_data: 'publish:books:' + bookData.id }], [{ text: '\\u{1F4DA} Books', callback_data: 'list:books:1' }]];\n return [{ json: { chatId: chatId, message: msg, parseMode: 'HTML', keyboard: keyboard } }];\n} catch (error) {\n return [{ json: { chatId: $input.first().json.chatId, message: '\\u274C Fehler beim Erstellen der Review: ' + error.message, parseMode: 'HTML' } }];\n}"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
720,
840
],
"id": "create-review-handler-001",
"name": "Create Review Handler"
},
{
"parameters": {
"jsCode": "var chatId = $input.first().json.chatId;\nvar message = '\\u2753 <b>Unknown Command</b>\\n\\nUse the buttons below or type:\\n<code>.review HC_ID [RATING]</code> - Start review with AI questions\\n<code>.answer BOOK_ID RATING your answers</code> - Submit review answers\\n<code>.refine ID FEEDBACK</code> - Refine existing review';\nvar keyboard = [\n [{ text: '\\u{1F4CB} Projects', callback_data: 'list:projects:1' }, { text: '\\u{1F4DA} Books', callback_data: 'list:books:1' }],\n [{ text: '\\u{1F4CA} Stats', callback_data: 'stats' }, { text: '\\u{1F3E0} Dashboard', callback_data: 'start' }]\n];\nreturn [{ json: { chatId: chatId, message: message, parseMode: 'HTML', keyboard: keyboard } }];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
720,
960
],
"id": "unknown-handler-001",
"name": "Unknown Command Handler"
},
{
"parameters": {
"method": "POST",
"url": "={{ 'https://api.telegram.org/bot8166414331:AAGNQ6fn2juD5esaTRxPjtTdSMkwq_oASIc/sendMessage' }}",
"authentication": "none",
"sendBody": true,
"contentType": "json",
"specifyBody": "json",
"jsonBody": "={{ { chat_id: $json.chatId, text: $json.message, parse_mode: $json.parseMode || 'HTML', reply_markup: ($json.keyboard && $json.keyboard.length > 0) ? { inline_keyboard: $json.keyboard } : undefined } }}"
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
960,
420
],
"id": "send-message-001",
"name": "Send Message",
"options": {}
},
{
"parameters": {
"jsCode": "try {\n var input = $input.first().json;\n var id = input.id;\n var feedback = input.feedback;\n var chatId = input.chatId;\n var bookResp = await this.helpers.httpRequest({ method: 'GET', url: 'https://cms.dk0.dev/items/book_reviews/' + id + '?fields=id,book_title,book_author,rating,translations.id,translations.languages_code,translations.review', headers: { 'Authorization': 'Bearer RF2QytqhcLXuVy6FO3PzWlsoR-ysCTwB' } });\n var bookData = bookResp && bookResp.data ? bookResp.data : bookResp;\n if (!bookData || !bookData.id) {\n return [{ json: { chatId: chatId, message: 'Review #' + id + ' nicht gefunden.', parseMode: 'HTML', keyboard: [[{ text: '\\u{1F3E0} Home', callback_data: 'start' }]] } }];\n }\n var translations = bookData.translations || [];\n var enTrans = null, deTrans = null;\n for (var i = 0; i < translations.length; i++) {\n if (translations[i].languages_code === 'en-US') enTrans = translations[i];\n if (translations[i].languages_code === 'de-DE') deTrans = translations[i];\n }\n var currentEn = enTrans ? enTrans.review : '';\n var currentDe = deTrans ? deTrans.review : '';\n var prompt = 'Du hast eine Buchbewertung fuer \"' + bookData.book_title + '\" von \"' + bookData.book_author + '\" geschrieben. Rating: ' + bookData.rating + '/5. Aktuelle EN-Bewertung: ' + currentEn + ' Aktuelle DE-Bewertung: ' + currentDe + ' Feedback des Lesers: ' + feedback + ' Wichtig: EN und DE sind immer inhaltlich identisch, nur die Sprache unterscheidet sich. Feedback gilt fuer BEIDE Versionen, auch wenn es nur eine Sprache erwaehnt. Ueberarbeite daher immer beide synchron. Ich-Perspektive, 4-6 Saetze pro Sprache. Verwende keine Bindestriche, Em-Dashes oder Gedankenstriche. Antworte NUR als JSON: {\"review_en\": \"...\", \"review_de\": \"...\"}';\n var aiResp = 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: 'user', content: prompt }] } });\n var aiText = (aiResp && aiResp.choices && aiResp.choices[0] && aiResp.choices[0].message && aiResp.choices[0].message.content) || '{}';\n var jsonMatch = aiText.match(/\\{[\\s\\S]*\\}/);\n var ai = jsonMatch ? JSON.parse(jsonMatch[0]) : { review_en: feedback, review_de: feedback };\n var reviewEn = ai.review_en || feedback;\n var reviewDe = ai.review_de || feedback;\n\n // Update existing translations (PATCH) or create new ones (POST)\n if (enTrans) {\n await this.helpers.httpRequest({ method: 'PATCH', url: 'https://cms.dk0.dev/items/book_reviews_translations/' + enTrans.id, headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer RF2QytqhcLXuVy6FO3PzWlsoR-ysCTwB' }, body: { review: reviewEn } });\n } else {\n await this.helpers.httpRequest({ method: 'POST', url: 'https://cms.dk0.dev/items/book_reviews_translations', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer RF2QytqhcLXuVy6FO3PzWlsoR-ysCTwB' }, body: { book_reviews_id: bookData.id, languages_code: 'en-US', review: reviewEn } });\n }\n if (deTrans) {\n await this.helpers.httpRequest({ method: 'PATCH', url: 'https://cms.dk0.dev/items/book_reviews_translations/' + deTrans.id, headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer RF2QytqhcLXuVy6FO3PzWlsoR-ysCTwB' }, body: { review: reviewDe } });\n } else {\n await this.helpers.httpRequest({ method: 'POST', url: 'https://cms.dk0.dev/items/book_reviews_translations', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer RF2QytqhcLXuVy6FO3PzWlsoR-ysCTwB' }, body: { book_reviews_id: bookData.id, languages_code: 'de-DE', review: reviewDe } });\n }\n\n var showEn = reviewEn;\n var showDe = reviewDe;\n var msg = '\\u270F\\uFE0F <b>Review aktualisiert!</b>\\n\\n\\u{1F4DA} ' + bookData.book_title + '\\n\\n<b>EN:</b> ' + showEn + '\\n\\n<b>DE:</b> ' + showDe;\n var keyboard = [[{ text: '\\u{1F441} Preview', callback_data: 'preview:books:' + id }, { text: '\\u2705 Publish', callback_data: 'publish:books:' + id }], [{ text: '\\u{1F4DA} Books', callback_data: 'list:books:1' }]];\n return [{ json: { chatId: chatId, message: msg, parseMode: 'HTML', keyboard: keyboard } }];\n} catch (error) {\n return [{ json: { chatId: $input.first().json.chatId, message: '\\u274C Fehler beim Aktualisieren: ' + error.message, parseMode: 'HTML' } }];\n}"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
720,
1080
],
"id": "refine-review-handler-001",
"name": "Refine Review Handler"
},
{
"parameters": {
"jsCode": "try {\n var input = $input.first().json;\n var chatId = input.chatId;\n var bookId = input.id;\n var hardcoverId = input.hardcoverId;\n var rating = input.rating || 0;\n var book;\n\n if (bookId) {\n var resp = await this.helpers.httpRequest({ method: 'GET', url: 'https://cms.dk0.dev/items/book_reviews/' + bookId + '?fields=id,book_title,book_author,hardcover_id,rating', headers: { 'Authorization': 'Bearer RF2QytqhcLXuVy6FO3PzWlsoR-ysCTwB' } });\n book = resp && resp.data;\n } else if (hardcoverId) {\n var resp = await this.helpers.httpRequest({ method: 'GET', url: 'https://cms.dk0.dev/items/book_reviews?filter[hardcover_id][_eq]=' + hardcoverId + '&fields=id,book_title,book_author,hardcover_id,rating&limit=1', headers: { 'Authorization': 'Bearer RF2QytqhcLXuVy6FO3PzWlsoR-ysCTwB' } });\n book = resp && resp.data && resp.data[0];\n }\n\n if (!book) {\n return [{ json: { chatId: chatId, message: '\\u274C Buch nicht gefunden. Pr\\u00fcfe die ID.', parseMode: 'HTML', keyboard: [[{ text: '\\u{1F4DA} Books', callback_data: 'list:books:1' }]] } }];\n }\n\n var prompt = 'Du bist ein Leseberater. Generiere genau 4 persoenliche, tiefgruendige Fragen zum Buch \"' + book.book_title + '\" von ' + book.book_author + ', die einem helfen, eine authentische Bewertung zu schreiben. Die Fragen sollen spezifisch zum Buch sein und zum Nachdenken anregen. Antworte NUR als JSON-Array, keine Erklaerung davor: [\"Frage 1\", \"Frage 2\", \"Frage 3\", \"Frage 4\"]';\n\n var aiResp = 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: 'user', content: prompt }] } });\n\n var aiText = (aiResp && aiResp.choices && aiResp.choices[0] && aiResp.choices[0].message && aiResp.choices[0].message.content) || '[]';\n var questions;\n try {\n var jsonMatch = aiText.match(/\\[[\\s\\S]*\\]/);\n questions = jsonMatch ? JSON.parse(jsonMatch[0]) : ['Was hat dir am besten gefallen?', 'Was hat dich gestoert?', 'Wuerdest du es weiterempfehlen?', 'Welche Szene ist dir im Gedaechtnis geblieben?'];\n } catch(e) {\n questions = ['Was hat dir am besten gefallen?', 'Was hat dich gestoert?', 'Wuerdest du es weiterempfehlen?', 'Welche Szene ist dir im Gedaechtnis geblieben?'];\n }\n\n var ratingInfo = rating > 0 ? '\\n\\u2B50 Dein Rating: ' + rating + '/5' : '\\n\\u2B50 Gib dein Rating (1-5) an';\n var msg = '\\u{1F4D6} <b>Review: ' + book.book_title + '</b>\\n' + book.book_author + ratingInfo + '\\n\\n\\u2753 <b>Beantworte diese Fragen:</b>\\n\\n';\n for (var i = 0; i < questions.length; i++) {\n msg += (i + 1) + '. ' + questions[i] + '\\n';\n }\n msg += '\\n\\u270D\\uFE0F Antworte mit:\\n<code>.answer ' + book.id + ' ' + (rating > 0 ? rating : '5') + ' deine Antworten hier</code>';\n msg += '\\n\\n<i>Beispiel: .answer ' + book.id + ' 4 Die Charakterentwicklung war super...</i>';\n\n var keyboard = [[{ text: '\\u{1F4DA} Books', callback_data: 'list:books:1' }, { text: '\\u{1F3E0} Home', callback_data: 'start' }]];\n return [{ json: { chatId: chatId, message: msg, parseMode: 'HTML', keyboard: keyboard } }];\n} catch(e) {\n return [{ json: { chatId: $input.first().json.chatId, message: '\\u274C Error: ' + e.message, parseMode: 'HTML' } }];\n}\n"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
720,
960
],
"id": "review-info-handler-001",
"name": "Review Info Handler"
}
],
"connections": {
"Telegram Trigger": {
"main": [
[
{
"node": "Global Parser",
"type": "main",
"index": 0
}
]
]
},
"Global Parser": {
"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": "Refine Review Handler",
"type": "main",
"index": 0
}
],
[
{
"node": "Unknown Command Handler",
"type": "main",
"index": 0
}
],
[
{
"node": "Review Info Handler",
"type": "main",
"index": 0
}
]
]
},
"Dashboard Handler": {
"main": [
[
{
"node": "Send Message",
"type": "main",
"index": 0
}
]
]
},
"List Handler": {
"main": [
[
{
"node": "Send Message",
"type": "main",
"index": 0
}
]
]
},
"Search Handler": {
"main": [
[
{
"node": "Send Message",
"type": "main",
"index": 0
}
]
]
},
"Stats Handler": {
"main": [
[
{
"node": "Send Message",
"type": "main",
"index": 0
}
]
]
},
"Preview Handler": {
"main": [
[
{
"node": "Send Message",
"type": "main",
"index": 0
}
]
]
},
"Publish Handler": {
"main": [
[
{
"node": "Send Message",
"type": "main",
"index": 0
}
]
]
},
"Delete Handler": {
"main": [
[
{
"node": "Send Message",
"type": "main",
"index": 0
}
]
]
},
"Delete Review Handler": {
"main": [
[
{
"node": "Send Message",
"type": "main",
"index": 0
}
]
]
},
"Create Review Handler": {
"main": [
[
{
"node": "Send Message",
"type": "main",
"index": 0
}
]
]
},
"Unknown Command Handler": {
"main": [
[
{
"node": "Send Message",
"type": "main",
"index": 0
}
]
]
},
"Refine Review Handler": {
"main": [
[
{
"node": "Send Message",
"type": "main",
"index": 0
}
]
]
},
"Review Info Handler": {
"main": [
[
{
"node": "Send Message",
"type": "main",
"index": 0
}
]
]
}
},
"pinData": {},
"settings": {
"executionOrder": "v1"
},
"staticData": null,
"tags": [],
"triggerCount": 1,
"updatedAt": "2025-01-21T00:00:00.000Z",
"versionId": "1"
}