# 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 <token>" \
  -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<string> { /* ... */ }
  async login(): Promise<void> { /* ... */ }
  async uploadMedia(file: Buffer, filename: string, options: MediaUploadOptions): Promise<MediaResponse> { /* ... */ }
  async listMedia(tenantId: number, limit?: number): Promise<MediaListResponse> { /* ... */ }
  async deleteMedia(mediaId: number): Promise<void> { /* ... */ }
}

interface MediaUploadOptions {
  alt: string;
  tenantId: number;
  caption?: string;
}

interface MediaResponse {
  id: number;
  url: string;
  filename: string;
  alt: string;
  sizes: Record<string, { url: string; width: number; height: number }>;
}
```

**WICHTIG – Multi-Tenant:**
- Jeder API-Call der Media betrifft MUSS `tenant` als Feld mitsenden
- Bei Reads: `?where[tenant][equals]=<ID>` 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]=<ID>&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<TOKEN>/<file.file_path>
//
// 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:<ID>"
//
// Beispiel:
// [ [porwoll.de] [C2S]     ]
// [ [Gunshin]    [BlogWoman] ]

// Callback Query Handler:
// Bei "tenant:<ID>" → 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 <promise>BLOCKED</promise>

---

## 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:

<promise>TELEGRAM_BOT_COMPLETE</promise>
