# Telegram Media Upload Bot – Payload CMS Integration ## Kontext - **Projektverzeichnis:** `/home/payload/telegram-media-bot` (auf sv-payload, LXC 700, IP: 10.10.181.100) - **Alternative:** Neues GitHub Repository `complexcaresolutions/telegram-media-bot` - **Tech-Stack:** Node.js 22 LTS, TypeScript, grammy (Telegram Bot Framework), PM2 - **Ziel-System:** Payload CMS Multi-Tenant (Production: `https://cms.c2sgmbh.de`, Staging: `https://pl.porwoll.tech`) - **Betriebssystem:** Debian/Ubuntu (LXC Container im Proxmox-Cluster) - **Package Manager:** pnpm --- ## Projektbeschreibung Erstelle einen Telegram Bot, der es autorisierten Benutzern ermöglicht, Bilder direkt aus dem Telegram-Chat in die Media Collection des Payload CMS hochzuladen. Der Bot authentifiziert sich gegen die Payload REST-API, empfängt Bilder über die Telegram Bot API, lädt sie von den Telegram-Servern herunter und leitet sie per `multipart/form-data` an den Payload Media-Endpoint weiter. Dabei wird die Multi-Tenant-Isolation strikt eingehalten. --- ## Technische Referenzen ### Payload CMS REST-API **Base URLs:** - Production: `https://cms.c2sgmbh.de/api` - Staging: `https://pl.porwoll.tech/api` - Swagger UI: `https://cms.c2sgmbh.de/api/docs` **Authentifizierung – Login:** ```bash curl -X POST "https://cms.c2sgmbh.de/api/users/login" \ -H "Content-Type: application/json" \ -d '{ "email": "admin@example.com", "password": "your-password" }' ``` **Response:** ```json { "message": "Auth Passed", "user": { "id": 1, "email": "admin@example.com", "isSuperAdmin": true }, "token": "eyJhbGciOiJIUzI1NiIs..." } ``` **Token verwenden:** ```bash curl "https://cms.c2sgmbh.de/api/media" \ -H "Authorization: JWT eyJhbGciOiJIUzI1NiIs..." ``` ### Media Upload Endpoint ```bash curl -X POST "https://cms.c2sgmbh.de/api/media" \ -H "Authorization: JWT " \ -F "file=@/path/to/image.jpg" \ -F "alt=Beschreibungstext" \ -F "tenant=1" ``` **WICHTIG:** Das `tenant`-Feld ist **Pflicht** bei jedem Upload. Ohne korrekte Tenant-Zuordnung gibt die API 403 Forbidden zurück. Die Tenant-Isolation ist ein Kernprinzip des gesamten Systems. ### Verfügbare Tenants | ID | Name | Slug | Domain | |----|------|------|--------| | 1 | porwoll.de | porwoll | porwoll.de | | 4 | Complex Care Solutions GmbH | c2s | complexcaresolutions.de | | 5 | Gunshin | gunshin | gunshin.de | | (weitere Tenants ggf. dynamisch über API abrufen) | ### Media Collection – Automatische Bildverarbeitung Payload generiert automatisch folgende responsive Größen beim Upload: | Size | Auflösung | Format | |------|-----------|--------| | thumbnail | 150×150 | Original + AVIF | | small | 300×300 | Original + AVIF | | medium | 600×600 | Original + AVIF | | large | 1200×1200 | Original + AVIF | | xlarge | 1920×1920 | Original + AVIF | | 2k | 2560×2560 | Original + AVIF | | og | 1200×630 | Original (Social Media) | → Der Bot muss sich NICHT um Bildgrößen kümmern. Payload erledigt das serverseitig. ### Rate Limiting (Payload API) | Limiter | Limit | Fenster | |---------|-------|---------| | publicApiLimiter | 60 Requests | 1 Minute | | authLimiter | 5 Requests | 15 Minuten | → Der Bot sollte Token cachen und nicht bei jedem Upload neu einloggen. --- ## Aufgaben ### 1. Projekt-Setup #### 1.1 Projektstruktur erstellen ``` telegram-media-bot/ ├── src/ │ ├── index.ts # Entry Point, Bot-Start │ ├── bot.ts # Grammy Bot-Instanz + Handler │ ├── config.ts # Environment-Konfiguration (typisiert) │ ├── payload/ │ │ ├── client.ts # Payload API Client (Login, Token-Management) │ │ └── media.ts # Media Upload Logik │ ├── telegram/ │ │ ├── handlers.ts # Message Handler (Photo, Document, Commands) │ │ └── keyboards.ts # Inline Keyboards (Tenant-Auswahl etc.) │ ├── middleware/ │ │ └── auth.ts # User-Whitelist Middleware │ └── utils/ │ ├── logger.ts # Logging Utility │ └── download.ts # Telegram File Download Helper ├── .env.example # Template für Environment Variables ├── .env # (gitignored) Actual Config ├── package.json ├── tsconfig.json ├── ecosystem.config.cjs # PM2 Konfiguration ├── .gitignore └── README.md ``` **Akzeptanzkriterien:** - [ ] `pnpm init` ausgeführt - [ ] TypeScript konfiguriert (strict mode) - [ ] Alle Dependencies installiert - [ ] `.gitignore` enthält `node_modules`, `.env`, `dist` #### 1.2 Dependencies installieren ```bash pnpm add grammy dotenv pnpm add -D typescript @types/node tsx ``` **Hinweis:** `grammy` ist das bevorzugte Telegram Bot Framework (modern, TypeScript-first, aktiv maintained). Alternativ wäre `node-telegram-bot-api` möglich, aber `grammy` hat bessere TypeScript-Unterstützung. #### 1.3 TypeScript Konfiguration **Datei:** `tsconfig.json` ```json { "compilerOptions": { "target": "ES2022", "module": "ESNext", "moduleResolution": "bundler", "outDir": "./dist", "rootDir": "./src", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "resolveJsonModule": true, "declaration": true, "declarationMap": true, "sourceMap": true }, "include": ["src/**/*"], "exclude": ["node_modules", "dist"] } ``` #### 1.4 Package.json Scripts ```json { "name": "telegram-media-bot", "version": "1.0.0", "type": "module", "scripts": { "dev": "tsx watch src/index.ts", "build": "tsc", "start": "node dist/index.js", "lint": "tsc --noEmit" } } ``` --- ### 2. Konfiguration #### 2.1 Environment Variables **Datei:** `.env.example` ```env # Telegram Bot TELEGRAM_BOT_TOKEN=123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11 # Zugelassene Telegram User-IDs (kommasepariert) ALLOWED_USER_IDS=123456789,987654321 # Payload CMS PAYLOAD_API_URL=https://cms.c2sgmbh.de/api PAYLOAD_ADMIN_EMAIL=admin@example.com PAYLOAD_ADMIN_PASSWORD=your-secure-password # Standard-Tenant (wird verwendet wenn kein Tenant gewählt) DEFAULT_TENANT_ID=1 # Logging LOG_LEVEL=info # Node Environment NODE_ENV=production ``` #### 2.2 Typisierte Config **Datei:** `src/config.ts` ```typescript // Alle Environment Variables typisiert laden und validieren. // Bei fehlenden Pflicht-Variablen: Prozess mit Error beenden. // ALLOWED_USER_IDS als number[] parsen (kommasepariert). // Validation beim Import, nicht lazy. interface Config { telegram: { botToken: string; allowedUserIds: number[]; }; payload: { apiUrl: string; email: string; password: string; }; defaultTenantId: number; logLevel: string; nodeEnv: string; } ``` **Akzeptanzkriterien:** - [ ] Config wird beim Start validiert - [ ] Fehlende Pflicht-Variablen = sofortiger Exit mit klarer Fehlermeldung - [ ] `ALLOWED_USER_IDS` wird als `number[]` geparst --- ### 3. Payload API Client #### 3.1 Authentifizierung mit Token-Caching **Datei:** `src/payload/client.ts` Implementiere einen Payload API Client mit folgender Logik: 1. **Login:** `POST /api/users/login` mit Email/Password 2. **Token speichern** (In-Memory, NICHT auf Disk) 3. **Token-Expiry tracken:** JWT decodieren (ohne Verifikation, nur Payload lesen), `exp` Feld prüfen 4. **Auto-Refresh:** Vor jedem API-Call prüfen ob Token noch gültig (mit 5 Min. Buffer). Falls abgelaufen → neu einloggen. 5. **Retry-Logik:** Bei 401-Response einmal neu einloggen und Request wiederholen. ```typescript class PayloadClient { private token: string | null = null; private tokenExpiry: number = 0; async getToken(): Promise { /* ... */ } async login(): Promise { /* ... */ } async uploadMedia(file: Buffer, filename: string, options: MediaUploadOptions): Promise { /* ... */ } async listMedia(tenantId: number, limit?: number): Promise { /* ... */ } async deleteMedia(mediaId: number): Promise { /* ... */ } } interface MediaUploadOptions { alt: string; tenantId: number; caption?: string; } interface MediaResponse { id: number; url: string; filename: string; alt: string; sizes: Record; } ``` **WICHTIG – Multi-Tenant:** - Jeder API-Call der Media betrifft MUSS `tenant` als Feld mitsenden - Bei Reads: `?where[tenant][equals]=` als Query-Parameter - Bei Writes: `tenant` im Request-Body **Akzeptanzkriterien:** - [ ] Login funktioniert, Token wird gecacht - [ ] Token wird vor Ablauf automatisch erneuert - [ ] 401 Response triggert Re-Login + Retry - [ ] Alle API-Calls enthalten korrekte Tenant-Filterung #### 3.2 Media Upload Funktion **Datei:** `src/payload/media.ts` ```typescript // Upload einer Bilddatei an POST /api/media // Content-Type: multipart/form-data // // Felder: // file: Die Bilddatei (Buffer) mit korrektem filename + mimetype // alt: Alt-Text (string) // tenant: Tenant-ID (number) // // Die Payload API generiert automatisch alle responsiven Größen. // Response enthält die vollständige Media-Resource inkl. aller Size-URLs. ``` Für den multipart Upload verwende die native `FormData` API (ab Node.js 18+ verfügbar) oder `form-data` Package. **Kein axios nötig** – nutze native `fetch`. **Akzeptanzkriterien:** - [ ] Upload funktioniert mit JPG, PNG, WebP, AVIF - [ ] Alt-Text wird korrekt gesetzt - [ ] Tenant-Zuordnung funktioniert - [ ] Response wird korrekt geparst --- ### 4. Telegram Bot #### 4.1 Bot-Instanz und Middleware **Datei:** `src/bot.ts` ```typescript import { Bot, Context, session } from 'grammy'; interface SessionData { selectedTenantId: number; selectedTenantName: string; } type BotContext = Context & { session: SessionData }; // Bot erstellen mit Grammy // Session-Middleware für Tenant-Auswahl pro User // Auth-Middleware für User-Whitelist ``` #### 4.2 Auth Middleware (User-Whitelist) **Datei:** `src/middleware/auth.ts` ```typescript // Middleware die prüft ob ctx.from.id in ALLOWED_USER_IDS enthalten ist. // Falls nicht: Antwort "⛔ Du bist nicht autorisiert, diesen Bot zu verwenden." // und ctx.next() NICHT aufrufen. // // WICHTIG: Auch in Gruppen-Chats nur auf autorisierte User reagieren. ``` **Akzeptanzkriterien:** - [ ] Nicht-autorisierte User erhalten Fehlermeldung - [ ] Autorisierte User können alle Funktionen nutzen - [ ] Middleware blockiert alle Handler, nicht nur einzelne #### 4.3 Command Handler **Datei:** `src/telegram/handlers.ts` Implementiere folgende Befehle: **`/start`** - Begrüßungsnachricht mit Kurzanleitung - Zeige aktuell gewählten Tenant - Text: ``` 🤖 Payload Media Upload Bot Schicke mir ein Bild und ich lade es in die Payload CMS Media-Bibliothek hoch. 📌 Aktueller Tenant: [Tenant-Name] 📋 Befehle: /tenant - Tenant wechseln /list - Letzte 5 Uploads anzeigen /status - Bot- und API-Status /help - Hilfe anzeigen ``` **`/tenant`** - Zeige Inline-Keyboard mit allen verfügbaren Tenants - Tenants dynamisch von der API laden: `GET /api/tenants` (Auth erforderlich) - Nach Auswahl: Tenant in Session speichern - Bestätigung: `✅ Tenant gewechselt zu: [Name] (ID: [ID])` **`/list`** - Zeige die letzten 5 hochgeladenen Medien des aktuellen Tenants - API: `GET /api/media?where[tenant][equals]=&sort=-createdAt&limit=5` - Ausgabe als Liste mit Thumbnail-URL, Dateiname, Datum **`/status`** - Zeige: - Bot-Uptime - Payload API erreichbar? (Quick-Check: `GET /api/users/me`) - Aktueller Tenant - Token-Status (gültig bis...) **`/help`** - Ausführliche Hilfe mit allen Befehlen und Nutzungshinweisen #### 4.4 Photo Handler (Kern-Funktionalität) **Datei:** `src/telegram/handlers.ts` (fortgesetzt) ```typescript // Handler für bot.on('message:photo') // // Ablauf: // 1. Höchste verfügbare Auflösung wählen: // ctx.message.photo ist ein Array von PhotoSize-Objekten, // sortiert nach Größe. Letztes Element = höchste Auflösung. // // 2. File-Info abrufen: // const file = await ctx.api.getFile(photo.file_id) // Download-URL: https://api.telegram.org/file/bot/ // // 3. Bild herunterladen (als Buffer): // fetch() auf die Download-URL // // 4. Alt-Text bestimmen: // - Falls Caption vorhanden (ctx.message.caption) → als Alt-Text verwenden // - Falls nicht → Generiere: "Upload via Telegram – [Datum] [Uhrzeit]" // // 5. Statusmeldung senden: // "⏳ Bild wird hochgeladen..." // // 6. An Payload API hochladen: // payloadClient.uploadMedia(buffer, filename, { alt, tenantId }) // // 7. Erfolgsmeldung: // "✅ Upload erfolgreich! // 📎 ID: [id] // 📁 Dateiname: [filename] // 🔗 URL: [url] // 🏷️ Tenant: [tenant-name] // 📐 Größen: thumbnail, small, medium, large, xlarge, 2k, og" // // 8. Bei Fehler: // "❌ Upload fehlgeschlagen: [Fehlermeldung]" // Logge den vollständigen Error serverseitig. ``` **Akzeptanzkriterien:** - [ ] Bilder werden in höchster Auflösung heruntergeladen - [ ] Caption wird als Alt-Text verwendet (falls vorhanden) - [ ] Statusmeldung wird gesendet BEVOR der Upload startet - [ ] Erfolgsmeldung enthält Media-ID und URL - [ ] Fehler werden sauber abgefangen und dem User angezeigt - [ ] Tenant-Zuordnung ist korrekt #### 4.5 Document Handler (Erweitert) ```typescript // Handler für bot.on('message:document') // // Akzeptiere nur Bildformate: jpg, jpeg, png, webp, avif, gif, svg // Bei nicht unterstütztem Format: // "⚠️ Format nicht unterstützt. Erlaubt: JPG, PNG, WebP, AVIF, GIF, SVG" // // Vorteil von Document-Upload gegenüber Photo: // Telegram komprimiert Bilder die als Foto gesendet werden. // Als Dokument gesendet bleibt die Originalqualität erhalten. // → Dem User diesen Tipp in /help erklären. ``` #### 4.6 Album/Bulk Handler ```typescript // Handler für mehrere Bilder gleichzeitig (Media Group / Album) // // Telegram sendet Alben als einzelne Messages mit gleicher media_group_id. // Sammle alle Messages mit gleicher media_group_id über ein kurzes Zeitfenster // (500ms Debounce), dann lade alle Bilder sequentiell hoch. // // Status: "⏳ Album erkannt: [N] Bilder werden hochgeladen..." // Pro Bild: Fortschritt melden: "📤 [X]/[N] hochgeladen..." // Am Ende: Zusammenfassung aller Upload-IDs ``` #### 4.7 Inline Keyboards **Datei:** `src/telegram/keyboards.ts` ```typescript // Tenant-Auswahl Keyboard // Dynamisch aus API geladen (GET /api/tenants) // Format: 2 Buttons pro Reihe // Jeder Button: callback_data = "tenant:" // // Beispiel: // [ [porwoll.de] [C2S] ] // [ [Gunshin] [BlogWoman] ] // Callback Query Handler: // Bei "tenant:" → Session updaten, Bestätigung senden ``` --- ### 5. Utilities #### 5.1 Logger **Datei:** `src/utils/logger.ts` ```typescript // Einfacher Logger mit Levels: debug, info, warn, error // Format: [TIMESTAMP] [LEVEL] [MODULE] Message // Beispiel: [2026-03-01 14:30:00] [INFO] [PayloadClient] Login erfolgreich // // Kein externes Logging-Framework nötig – console.log basiert reicht. // LOG_LEVEL aus config bestimmt Mindest-Level. ``` #### 5.2 Download Helper **Datei:** `src/utils/download.ts` ```typescript // Funktion zum Herunterladen einer Datei von einer URL als Buffer. // Nutze native fetch(). // Timeout: 30 Sekunden // Max. Dateigröße: 20 MB (Telegram-Limit) // Bei Fehler: Spezifische Error-Messages (Timeout, Too Large, Network Error) async function downloadFile(url: string): Promise<{ buffer: Buffer; mimeType: string }> { /* ... */ } ``` --- ### 6. PM2 Konfiguration & Deployment #### 6.1 PM2 Ecosystem File **Datei:** `ecosystem.config.cjs` ```javascript module.exports = { apps: [{ name: 'telegram-media-bot', script: './dist/index.js', instances: 1, // NUR 1 Instanz! Telegram Long-Polling verträgt kein Clustering autorestart: true, watch: false, max_memory_restart: '256M', env: { NODE_ENV: 'production' }, error_file: './logs/error.log', out_file: './logs/out.log', merge_logs: true, time: true }] }; ``` **WICHTIG:** Der Bot nutzt Long-Polling (kein Webhook). Deshalb darf nur EINE Instanz laufen. Mehrere Instanzen führen zu Konflikten bei der Telegram API. #### 6.2 Deployment auf sv-payload Der Bot läuft als zusätzlicher PM2-Prozess auf **sv-payload (LXC 700, 10.10.181.100)**, wo bereits das Payload CMS per PM2 managed wird. ```bash # Auf sv-payload (als user 'payload') cd /home/payload git clone git@github.com:complexcaresolutions/telegram-media-bot.git cd telegram-media-bot pnpm install cp .env.example .env # → .env mit echten Werten befüllen # Build und Start pnpm build pm2 start ecosystem.config.cjs pm2 save ``` #### 6.3 GitHub Actions Workflow (Optional) **Datei:** `.github/workflows/deploy.yml` ```yaml name: Deploy Telegram Bot on: push: branches: [main] workflow_dispatch: jobs: deploy: runs-on: ubuntu-latest steps: - name: Deploy via SSH uses: appleboy/ssh-action@v1 with: host: 37.24.237.181 # Externe IP (UDM Pro SE) username: payload key: ${{ secrets.STAGING_SSH_KEY }} port: 22122 # SSH-Port über Port-Forwarding script: | cd /home/payload/telegram-media-bot git pull origin main pnpm install --frozen-lockfile pnpm build pm2 restart telegram-media-bot ``` **SSH-Zugang:** Externer Zugang über UDM Pro SE Port-Forwarding (Port 22122 → sv-payload:22). Der SSH-Key `STAGING_SSH_KEY` ist bereits als GitHub Secret konfiguriert. --- ### 7. Sicherheit #### 7.1 Maßnahmen 1. **User-Whitelist:** Nur explizit erlaubte Telegram User-IDs dürfen den Bot nutzen 2. **Token-Sicherheit:** Payload JWT wird nur im Memory gehalten, nie auf Disk geschrieben 3. **Environment Variables:** Alle Secrets in `.env`, nie im Code 4. **Rate Limiting:** Maximal 10 Uploads pro Minute pro User (Bot-seitig implementiert) 5. **Dateigrößen-Limit:** Telegram begrenzt auf 20 MB, zusätzlich Bot-seitiges Limit von 20 MB 6. **Dateityp-Validierung:** Nur erlaubte MIME-Types akzeptieren (image/jpeg, image/png, image/webp, image/avif, image/gif, image/svg+xml) 7. **Kein Webhook-Modus:** Long-Polling vermeidet die Notwendigkeit eines öffentlich erreichbaren Endpoints #### 7.2 Telegram Bot erstellen 1. Öffne Telegram und suche `@BotFather` 2. Sende `/newbot` 3. Name: `CCS Media Upload Bot` (oder ähnlich) 4. Username: `ccs_media_upload_bot` (muss eindeutig sein und auf `bot` enden) 5. Token sichern → in `.env` als `TELEGRAM_BOT_TOKEN` eintragen 6. Optional via BotFather: - `/setdescription` – Bot-Beschreibung setzen - `/setcommands` – Bot-Befehle registrieren: ``` start - Bot starten und Hilfe anzeigen tenant - Ziel-Tenant wechseln list - Letzte Uploads anzeigen status - Bot- und API-Status prüfen help - Ausführliche Hilfe ``` --- ### 8. Error Handling & Edge Cases #### 8.1 Zu behandelnde Szenarien | Szenario | Verhalten | |----------|-----------| | Payload API nicht erreichbar | User informieren, Retry nach 30s, Log Error | | JWT abgelaufen während Upload | Auto-Relogin + Retry (max. 1x) | | Telegram File Download fehlschlägt | User informieren mit spezifischem Error | | Bild zu groß (>20 MB) | `⚠️ Datei zu groß. Maximum: 20 MB` | | Nicht unterstütztes Format | `⚠️ Format nicht unterstützt. Erlaubt: JPG, PNG, WebP, AVIF, GIF, SVG` | | Kein Tenant gewählt | Default-Tenant verwenden, User informieren | | Payload 403 (Tenant-Problem) | `❌ Zugriff verweigert. Prüfe die Tenant-Zuordnung.` | | Payload 429 (Rate Limit) | `⏳ Zu viele Anfragen. Bitte warte [X] Sekunden.` | | Bot-Start ohne gültige Config | Sofortiger Exit mit klarem Fehlertext | | Album mit >10 Bildern | Hinweis dass max. 10 gleichzeitig verarbeitet werden | #### 8.2 Graceful Shutdown ```typescript // Bei SIGINT/SIGTERM: // 1. Bot-Polling stoppen // 2. Laufende Uploads abwarten (max. 60s Timeout) // 3. Prozess beenden // PM2 sendet SIGINT, dann nach Timeout SIGKILL. ``` --- ## Erfolgskriterien (Gesamt) - [ ] `pnpm lint` (tsc --noEmit) ohne Errors - [ ] `pnpm build` erfolgreich - [ ] Bot startet und verbindet sich mit Telegram - [ ] `/start` zeigt Begrüßung - [ ] `/tenant` zeigt Inline-Keyboard mit Tenants aus der API - [ ] Tenant-Wechsel funktioniert und wird in Session gespeichert - [ ] Bild-Upload (als Foto) → Bild erscheint in Payload CMS Media Collection mit korrektem Tenant - [ ] Bild-Upload (als Dokument) → Bild in Originalqualität hochgeladen - [ ] Caption wird als Alt-Text übernommen - [ ] Album-Upload funktioniert (mehrere Bilder) - [ ] `/list` zeigt letzte 5 Uploads - [ ] `/status` zeigt API-Status und Token-Validität - [ ] Nicht-autorisierte User werden blockiert - [ ] Fehler werden dem User als verständliche Meldungen angezeigt - [ ] PM2 managed den Prozess mit Auto-Restart - [ ] Logs werden in `./logs/` geschrieben --- ## Selbst-Prüfung Nach jeder Iteration: 1. `pnpm lint` (tsc --noEmit) 2. `pnpm build` 3. Bei Fehler: korrigieren und wiederholen 4. Manueller Test-Flow: - Bot starten mit `pnpm dev` - `/start` senden - `/tenant` → Tenant wählen - Bild senden → Upload prüfen - `/list` → Upload in Liste sichtbar 5. Fortschritt in README.md dokumentieren --- ## Escape Hatch Nach 15 Iterationen ohne Fortschritt: - Dokumentiere was blockiert in BLOCKERS.md - Liste alle versuchten Ansätze auf - Schlage 3 alternative Lösungswege vor - Output BLOCKED --- ## Hinweise für die Implementierung 1. **Telegram Bot Token** muss vor dem Start via @BotFather erstellt und in `.env` eingetragen werden. 2. **Payload Admin Credentials** müssen einen User mit SuperAdmin-Rechten referenzieren (für Tenant-übergreifenden Zugriff). 3. **Grammy statt node-telegram-bot-api** – Grammy ist moderner, hat bessere TypeScript-Unterstützung und aktive Wartung. 4. **Native fetch statt axios** – Node.js 22 hat native fetch/FormData. Keine zusätzliche HTTP-Library nötig. 5. **Long-Polling statt Webhooks** – Einfacher zu deployen (kein öffentlicher Endpoint nötig), perfekt für den Use Case. 6. **Kein separater LXC-Container** nötig – der Bot läuft als zusätzlicher PM2-Prozess auf sv-payload. --- ## Fertig? Wenn ALLE Aufgaben erledigt sind UND alle Erfolgskriterien erfüllt sind: TELEGRAM_BOT_COMPLETE