From 681683a65c0abc55da5c735e953a5f2336a189ba Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Mon, 22 Jun 2026 00:48:03 +0200 Subject: [PATCH] =?UTF-8?q?chore:=20Projekt-Initialisierung=20=E2=80=94=20?= =?UTF-8?q?Plan,=20Aufgaben,=20README?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 22 ++ docs/BSN-LIVESYSTEM-PLAN.md | 193 +++++++++++ docs/alicia-aufgaben-nacht1.md | 592 +++++++++++++++++++++++++++++++++ 3 files changed, 807 insertions(+) create mode 100644 README.md create mode 100644 docs/BSN-LIVESYSTEM-PLAN.md create mode 100644 docs/alicia-aufgaben-nacht1.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..97f6407 --- /dev/null +++ b/README.md @@ -0,0 +1,22 @@ +# BSN-Livesystem + +Community-Plattform für Brettspiel-News.de — das Live-System auf dem neuen Server. + +## Struktur + +``` +src/ +├── app.py # Flask-App-Factory +├── routes/ # Webhook-Endpunkte +├── models/ # Pydantic-Datenmodelle +├── queue/ # Redis-Queue-System +└── utils/ # Validatoren +tests/ # pytest-Tests +docs/ # Pläne & Aufgaben +``` + +## Qualität + +- Python 3.13+, Pydantic v2, Flask +- Ruff + mypy + pytest (CI-Gate) +- Jeder Push → Gitea Actions-Check diff --git a/docs/BSN-LIVESYSTEM-PLAN.md b/docs/BSN-LIVESYSTEM-PLAN.md new file mode 100644 index 0000000..1a9f74d --- /dev/null +++ b/docs/BSN-LIVESYSTEM-PLAN.md @@ -0,0 +1,193 @@ +# BSN Live-System — Initialer Projektplan + +> **Ziel:** Neuer Server, neues Projekt, alles von Tag 1 an richtig. +> **Erstellt:** 21. Juni 2026 — basierend auf 3 Review-Runden + sys-1/2-Tests + +--- + +## Phase 0 — Server einrichten (1h) + +```bash +# User anlegen +sudo useradd -m -s /bin/bash cody +sudo useradd -m -s /bin/bash basti +sudo useradd -m -s /bin/bash alicia + +# Sudo für alle +echo "cody ALL=(ALL:ALL) NOPASSWD: ALL" | sudo tee /etc/sudoers.d/cody +echo "basti ALL=(ALL:ALL) NOPASSWD: ALL" | sudo tee /etc/sudoers.d/basti +echo "alicia ALL=(ALL:ALL) NOPASSWD: ALL" | sudo tee /etc/sudoers.d/alicia + +# SSH-Keys verteilen +# (Public Keys von Cody, Basti, Alicia in authorized_keys) + +# Basis-Tools +sudo apt-get update && sudo apt-get install -y redis-server git python3-pip curl +sudo systemctl enable --now redis-server + +# Python-Toolchain +pip install ruff mypy pre-commit pytest pytest-cov redis +``` + +--- + +## Phase 1 — Projekt-Repo aufsetzen (30min) + +In Gitea (`git.datenhimmel.work`): +- Neues Repo `BSN-Community` anlegen +- Gitea Actions Runner registrieren + +Im Projekt-Root: + +```bash +git clone git@git.datenhimmel.work:danielkrause/BSN-Community.git +cd BSN-Community +``` + +**Dateien aus dem Prototypen kopieren:** + +| Von (BSN-Chatsystem) | Nach (BSN-Community) | Grund | +|---|---|---| +| `.gitea/workflows/quality.yml` | `.gitea/workflows/quality.yml` | CI-Gate | +| `CODING_STANDARDS.md` | `CODING_STANDARDS.md` | Regelwerk | +| `queue_handler.py` | `src/queue_handler.py` | Queue-System | +| `webhook_queue.py` | `src/webhook_queue.py` | Signatur-Prüfung | +| `queue_worker.py` | `src/queue_worker.py` | Async Worker | +| `pyproject.toml` (aus HTML §13) | `pyproject.toml` | Ruff + mypy + pytest Config | +| `.pre-commit-config.yaml` (aus HTML §13) | `.pre-commit-config.yaml` | Pre-Commit Hooks | +| `docs/bsn-coding-standards.html` | `docs/bsn-coding-standards.html` | IT-Bibel | + +**Git initial:** +```bash +git add -A +git commit -m "chore: Projekt-Setup mit CI-Gate, Queue, Coding-Standards" +git push origin main +# Gitea Actions prüft automatisch! +``` + +--- + +## Phase 2 — Hermes-Agenten einrichten (30min) + +```bash +# Cody-Profil (Entwickler) +hermes profile create cody --clone-from default +hermes config set agent.environment_hint "Du bist Cody, BSN-Entwickler. Befolge skill_view('bsn-coding-standards'). CI: .gitea/workflows/quality.yml" --profile cody + +# Alicia-Profil (Content) +hermes profile create alicia --clone-from default +hermes config set agent.environment_hint "Du bist Alicia, BSN-Content-Managerin. System-Übersicht: read_file('ALICIA.md'). Befolge skill_view('bsn-coding-standards')." --profile alicia + +# Basti-Profil (Redakteur) +hermes profile create basti --clone-from default +hermes config set agent.environment_hint "Du bist Basti, BSN-Redakteur. Befolge skill_view('bsn-coding-standards') bei Code-Arbeit." --profile basti +``` + +**Skills installieren (pro Profil):** +``` +skill_view('bsn-coding-standards') # Coding-Regeln +skill_view('web-research') # Recherche +skill_view('article-management') # Article Server (Alicia/Basti) +``` + +--- + +## Phase 3 — Flask-App-Struktur (1h) + +Statt `app.py` (5599 Zeilen Monolith) → saubere Struktur: + +``` +src/ +├── app.py # Flask-App-Factory + Config (< 100 Zeilen) +├── routes/ +│ ├── __init__.py +│ ├── whatsapp.py # WhatsApp Webhook (→ Queue) +│ ├── telegram.py # Telegram Webhook (→ Queue) +│ ├── admin.py # Admin-Dashboard +│ └── api.py # Öffentliche API +├── services/ +│ ├── __init__.py +│ ├── intake.py # Message-Verarbeitung +│ └── game_service.py # Spiele-Logik +├── models/ +│ ├── __init__.py +│ └── submission.py # DB-Modell +├── queue/ +│ ├── __init__.py +│ ├── handler.py # QueueHandler (von queue_handler.py) +│ ├── webhook.py # Signatur + Intake (von webhook_queue.py) +│ └── worker.py # Queue-Worker (von queue_worker.py) +├── utils/ +│ ├── __init__.py +│ └── validators.py +tests/ +├── test_routes/ +├── test_services/ +├── test_queue/ +└── conftest.py +``` + +--- + +## Phase 4 — Services starten (15min) + +```bash +# Flask-App (systemd) +sudo tee /etc/systemd/system/bsn-chatbot.service << EOF +[Unit] +Description=BSN Chatbot +After=network.target redis-server.service + +[Service] +User=cody +WorkingDirectory=/home/cody/workspace/BSN-Community +ExecStart=/home/cody/venv/bin/python src/app.py +Restart=always + +[Install] +WantedBy=multi-user.target +EOF + +# Queue-Worker (systemd) +sudo tee /etc/systemd/system/bsn-worker.service << EOF +[Unit] +Description=BSN Queue Worker +After=redis-server.service + +[Service] +User=cody +WorkingDirectory=/home/cody/workspace/BSN-Community +ExecStart=/home/cody/venv/bin/python src/queue/worker.py --poll 2 --batch 5 +Restart=always + +[Install] +WantedBy=multi-user.target +EOF + +sudo systemctl daemon-reload +sudo systemctl enable --now bsn-chatbot bsn-worker +``` + +--- + +## Phase 5 — Monitoring (später) + +- `structlog` für strukturiertes Logging +- `GlitchTip` für Error-Tracking +- `/admin/queue` Endpoint für Queue-Status + +--- + +## ✅ Checkliste: Ist alles drin? + +- [ ] CI-Gate (Gitea Actions) prüft jeden Push +- [ ] Pre-commit blockiert lokal +- [ ] Coding-Standards-Skill in allen Profilen +- [ ] Queue puffert Submissions (Tunnel down = nix verloren) +- [ ] Signatur-Prüfung vor Enqueue +- [ ] Dedup über message_id +- [ ] Worker verarbeitet asynchron +- [ ] Flask-Struktur sauber getrennt (Routes ≠ Services ≠ Models) +- [ ] Alle Agenten (Cody, Basti, Alicia) haben environment_hint +- [ ] Redis läuft als systemd-Service +- [ ] Worker läuft als systemd-Service diff --git a/docs/alicia-aufgaben-nacht1.md b/docs/alicia-aufgaben-nacht1.md new file mode 100644 index 0000000..378f7c7 --- /dev/null +++ b/docs/alicia-aufgaben-nacht1.md @@ -0,0 +1,592 @@ +# BSN-Livesystem — Aufgaben für Alicia (Nacht 1) + +> **Jede Aufgabe = neuer Context.** Nach Abschluss: `hermes new` oder neuen Chat starten. +> **Qualität vor Geschwindigkeit.** Lieber 5 saubere Aufgaben als 10 schlampige. +> **Coding-Standards:** `skill_view('bsn-coding-standards')` vor jeder Aufgabe laden. + +--- + +## Aufgabe 1: pyproject.toml — Projekt-Konfiguration + +Erstelle die zentrale Projekt-Konfiguration für das BSN-Livesystem. + +### Was zu tun ist + +Erstelle die Datei `pyproject.toml` im Projekt-Root mit folgenden Abschnitten: + +**[project]** +- name: "bsn-livesystem" +- version: "0.1.0" +- python: ">=3.13" +- dependencies: flask, pydantic, redis, structlog, pytest, pytest-cov, mypy + +**[tool.ruff]** +- line-length: 100 +- target-version: "py313" +- select: ["E", "F", "I", "N", "W", "UP", "PL", "C90", "S"] +- ignore: [] + +**[tool.ruff.format]** +- quote-style: "double" +- indent-style: "space" +- docstring-code-format: true + +**[tool.mypy]** +- strict: true +- python_version: "3.13" + +**[tool.pytest.ini_options]** +- testpaths: ["tests"] +- addopts: "-v --tb=short" + +### Abgabe +- 1 Datei: `pyproject.toml` +- Kein Test nötig (Konfigurationsdatei) +- Commit: `chore: pyproject.toml mit Ruff, mypy, pytest-Konfiguration` +- Push auf `origin/main` + +### ⚠️ NACH DIESER AUFGABE +Starte einen **neuen Context**. NICHT im gleichen Context weiterarbeiten. + +--- + +## Aufgabe 2: .pre-commit-config.yaml — Pre-Commit Hooks + +Erstelle die Pre-Commit-Konfiguration, die vor JEDEM Commit automatisch Qualitätschecks ausführt. + +### Was zu tun ist + +Erstelle `.pre-commit-config.yaml` im Projekt-Root: + +```yaml +repos: + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.11.0 + hooks: + - id: ruff + args: [--fix] + - id: ruff-format +``` + +### Wichtig +- Die Datei MUSS exakt `.pre-commit-config.yaml` heißen (Punkt am Anfang!) +- Nach Installation mit `pre-commit install` werden die Hooks automatisch vor jedem Commit ausgeführt + +### Abgabe +- 1 Datei: `.pre-commit-config.yaml` +- Kein Test nötig +- Commit: `chore: pre-commit hooks mit Ruff Lint + Format` +- Push auf `origin/main` + +### ⚠️ NACH DIESER AUFGABE +Starte einen **neuen Context**. NICHT im gleichen Context weiterarbeiten. + +--- + +## Aufgabe 3: Submission-Modell — Pydantic DB-Modell + +Erstelle das zentrale Datenmodell für eingehende Community-Submissions. + +### Was zu tun ist + +Erstelle zwei Dateien: + +**`src/models/__init__.py`** — leer + +**`src/models/submission.py`** — das Submission-Modell: + +```python +from pydantic import BaseModel, Field +from datetime import datetime + + +class Submission(BaseModel): + """Eine Community-Einreichung (WhatsApp/Telegram).""" + message_id: str = Field(..., description="Eindeutige Nachrichten-ID") + plattform: str = Field(..., description="whatsapp oder telegram") + absender: str = Field(..., description="Absender-ID oder Telefonnummer") + text: str = Field(..., min_length=1, max_length=5000, description="Nachrichtentext") + zeitstempel: datetime = Field(default_factory=datetime.now, description="Eingangszeitpunkt") + status: str = Field(default="neu", description="neu, in_bearbeitung, erledigt, fehler") +``` + +### Besonderheiten +- `message_id` ist der Primärschlüssel für Deduplizierung +- `status` durchläuft: neu → in_bearbeitung → erledigt (oder fehler) +- `text` max 5000 Zeichen (Telegram-Limit) + +### Coding-Standards +- Pydantic v2 mit `Field()`-Validatoren +- Deutsche Docstrings (Google-Style) +- Type Hints auf allen Funktionen +- `ruff check` + `ruff format` + `mypy` müssen grün sein + +### Test-Datei: `tests/test_submission.py` +Schreibe 4 Tests: +1. Gültige Submission erstellen — alle Felder korrekt +2. Leerer Text wirft ValidationError +3. Text > 5000 Zeichen wirft ValidationError +4. zeitstempel wird automatisch gesetzt (nicht None) + +### Abgabe +- 3 Dateien: `src/models/__init__.py`, `src/models/submission.py`, `tests/test_submission.py` +- Commit: `feat: Submission-Pydantic-Modell mit Validierung` +- Push auf `origin/main` + +### ⚠️ NACH DIESER AUFGABE +Starte einen **neuen Context**. NICHT im gleichen Context weiterarbeiten. + +--- + +## Aufgabe 4: Input-Validatoren — Validierungs-Helfer + +Erstelle Hilfsfunktionen zur Validierung von Benutzereingaben. + +### Was zu tun ist + +Erstelle zwei Dateien: + +**`src/utils/__init__.py`** — leer + +**`src/utils/validators.py`** mit drei Funktionen: + +```python +def validiere_telefonnummer(telefon: str) -> str: + """Validiert und normalisiert eine Telefonnummer.""" + # Entferne alles außer + und Ziffern + # Muss mit + beginnen, min 10 Ziffern + # Gibt bereinigte Nummer zurück oder wirft ValueError + ... + +def validiere_spiel_name(name: str) -> str: + """Validiert einen Spielnamen.""" + # 2-200 Zeichen + # Keine HTML-Tags (< >) + # Strip Whitespace + ... + +def validiere_spieleranzahl(anzahl: int) -> int: + """Validiert eine Spieleranzahl.""" + # 1-20 Spieler + ... +``` + +### Verhalten im Detail + +**validiere_telefonnummer:** +- Akzeptiert: "+491****7890", "+49 123 4567890", "00491234567890" +- Entfernt Leerzeichen, Bindestriche, Klammern +- Konvertiert "00" am Anfang zu "+" +- Wirft ValueError bei: weniger als 10 Ziffern, kein + nach Bereinigung + +**validiere_spiel_name:** +- Entfernt HTML-Tags mit Regex `<[^>]*>` +- Strip Whitespace an beiden Enden +- Wirft ValueError bei: < 2 Zeichen, > 200 Zeichen + +**validiere_spieleranzahl:** +- Wirft ValueError bei: < 1, > 20, kein Integer + +### Coding-Standards +- Type Hints auf ALLEN Funktionen +- Deutsche Docstrings mit Args, Returns, Raises +- `ruff check` + `ruff format` + `mypy` müssen grün sein + +### Test-Datei: `tests/test_validators.py` +Schreibe 6 Tests (2 pro Funktion): +1. Gültige Telefonnummer → bereinigt +2. Ungültige Telefonnummer → ValueError +3. Gültiger Spielname → bereinigt +4. HTML-Tags im Spielnamen → entfernt +5. Gültige Spieleranzahl → unverändert +6. Ungültige Spieleranzahl → ValueError + +### Abgabe +- 3 Dateien: `src/utils/__init__.py`, `src/utils/validators.py`, `tests/test_validators.py` +- Commit: `feat: Input-Validatoren für Telefon, Spielname, Spieleranzahl` +- Push auf `origin/main` + +### ⚠️ NACH DIESER AUFGABE +Starte einen **neuen Context**. NICHT im gleichen Context weiterarbeiten. + +--- + +## Aufgabe 5: Queue-Handler — Redis-basierte Message-Queue + +Erstelle den zentralen Queue-Handler für asynchrone Nachrichtenverarbeitung. + +### Was zu tun ist + +Erstelle zwei Dateien: + +**`src/queue/__init__.py`** — leer + +**`src/queue/handler.py`** — QueueHandler-Klasse: + +```python +import redis +from src.models.submission import Submission + + +class QueueHandler: + """Redis-basierte Message-Queue mit Idempotenz-Garantie.""" + + def __init__(self, redis_host: str = "localhost", redis_port: int = 6379): + self.redis = redis.Redis(host=redis_host, port=redis_port, decode_responses=True) + self.stream_key = "bsn:submissions" + self.processed_key = "bsn:processed" + + def enqueue(self, submission: Submission) -> str | None: + """Legt eine Submission in die Queue. Überspringt Duplikate.""" + # Prüfe ob message_id bereits verarbeitet wurde + # Wenn ja → return None (Idempotenz) + # Wenn nein → XADD in Stream, merke ID in processed_set (TTL 24h) + ... + + def dequeue(self, count: int = 5) -> list[Submission]: + """Holt bis zu `count` Submissions aus der Queue (FIFO).""" + ... + + def mark_done(self, submission: Submission) -> None: + """Markiert eine Submission als erfolgreich verarbeitet.""" + ... + + def queue_size(self) -> int: + """Gibt die aktuelle Queue-Größe zurück.""" + ... +``` + +### Anforderungen +- Redis-Stream `bsn:submissions` für persistente Pufferung +- Redis-Set `bsn:processed` für Deduplizierung (24h TTL) +- Consumer-Group `bsn-workers` für zuverlässige Verarbeitung +- Alle Operationen idempotent + +### Coding-Standards +- Type Hints auf ALLEN Methoden +- Deutsche Docstrings (Google-Style) pro Methode +- `ruff check` + `ruff format` + `mypy` müssen grün sein + +### Test-Datei: `tests/test_queue_handler.py` +Schreibe 5 Tests (mit `fakeredis`): +1. enqueue einer neuen Submission → gibt stream_id zurück +2. enqueue eines Duplikats → gibt None zurück +3. dequeue holt eingereihte Submission zurück +4. mark_done entfernt aus Queue +5. queue_size gibt korrekte Anzahl + +### Abgabe +- 3 Dateien: `src/queue/__init__.py`, `src/queue/handler.py`, `tests/test_queue_handler.py` +- Commit: `feat: QueueHandler mit Redis-Streams, Idempotenz, Dedup` +- Push auf `origin/main` + +### ⚠️ NACH DIESER AUFGABE +Starte einen **neuen Context**. NICHT im gleichen Context weiterarbeiten. + +--- + +## Aufgabe 6: Webhook-Signatur-Prüfung + +Erstelle die Signatur-Prüfung für eingehende Webhooks. + +### Was zu tun ist + +Erstelle **`src/queue/webhook.py`**: + +```python +import hashlib +import hmac +import os +from src.queue.handler import QueueHandler +from src.models.submission import Submission + + +def verify_whatsapp_signature(payload: bytes, signature: str, app_secret: str | None = None) -> bool: + """Verifiziert die WhatsApp X-Hub-Signature-256.""" + secret = app_secret or os.environ.get("WHATSAPP_APP_SECRET", "") + if not secret: + return False + expected = hmac.new(secret.encode(), payload, hashlib.sha256).hexdigest() + return hmac.compare_digest(f"sha256={expected}", signature) + + +def verify_telegram_signature(token: str | None = None) -> bool: + """Verifiziert den Telegram Bot-Token.""" + expected = token or os.environ.get("TELEGRAM_BOT_TOKEN", "") + return len(expected) > 20 if expected else False + + +def validate_and_enqueue(payload: bytes, signature: str, plattform: str, qh: QueueHandler) -> bool: + """Validiert Signatur und legt in Queue ab.""" + ... +``` + +### Anforderungen +- `hmac.compare_digest()` für Timing-sicheren Vergleich +- Secrets NUR aus Environment (`os.environ`) +- Plattform-Erkennung über `plattform`-Parameter + +### Coding-Standards +- Type Hints auf ALLEN Funktionen +- Deutsche Docstrings +- Keine Secrets im Code +- `ruff check` + `ruff format` + `mypy` müssen grün sein + +### Test-Datei: `tests/test_webhook.py` +Schreibe 5 Tests: +1. Korrekte WhatsApp-Signatur → True +2. Falsche WhatsApp-Signatur → False +3. Fehlender App-Secret → False (sicherer Fail) +4. Korrekter Telegram-Token → True +5. Ungültiger Telegram-Token (zu kurz) → False + +### Abgabe +- 2 Dateien: `src/queue/webhook.py`, `tests/test_webhook.py` +- Commit: `feat: Webhook-Signatur-Prüfung für WhatsApp + Telegram` +- Push auf `origin/main` + +### ⚠️ NACH DIESER AUFGABE +Starte einen **neuen Context**. NICHT im gleichen Context weiterarbeiten. + +--- + +## Aufgabe 7: Queue-Worker — Async Verarbeitung + +Erstelle den asynchronen Worker für Queue-Verarbeitung. + +### Was zu tun ist + +Erstelle **`src/queue/worker.py`**: + +```python +import time +from src.queue.handler import QueueHandler +from src.models.submission import Submission + + +def verarbeite_submission(submission: Submission) -> bool: + """Verarbeitet eine einzelne Submission (Platzhalter für KI-Pipeline).""" + # Platzhalter: Status auf erledigt setzen + ... + + +def worker_loop(qh: QueueHandler, poll_interval: int = 2, batch_size: int = 5) -> None: + """Endlos-Schleife: Pollt Queue, verarbeitet Batches.""" + # 1. dequeue batch + # 2. Für jede Submission: verarbeite_submission() + # 3. Bei Erfolg: mark_done() + # 4. Bei Fehler: loggen, NICHT crashen + # 5. poll_interval Sekunden warten + ... +``` + +### Anforderungen +- Endlos-Schleife mit `while True` +- Fehler in EINER Submission dürfen Worker NICHT crashen +- Poll-Intervall konfigurierbar (Default: 2 Sekunden) + +### Coding-Standards +- Type Hints auf allen Funktionen +- Deutsche Docstrings +- `ruff check` + `ruff format` + `mypy` müssen grün sein + +### Test-Datei: `tests/test_worker.py` +Schreibe 3 Tests (mit `fakeredis`, mocke `time.sleep`): +1. verarbeite_submission setzt Status auf "erledigt" +2. worker_loop verarbeitet alle Submissions in der Queue +3. worker_loop stoppt bei leerer Queue (ohne Fehler) + +### Abgabe +- 2 Dateien: `src/queue/worker.py`, `tests/test_worker.py` +- Commit: `feat: Queue-Worker mit Batch-Verarbeitung und Fehlertoleranz` +- Push auf `origin/main` + +### ⚠️ NACH DIESER AUFGABE +Starte einen **neuen Context**. NICHT im gleichen Context weiterarbeiten. + +--- + +## Aufgabe 8: Flask-App-Factory — App-Grundgerüst + +Erstelle die Flask-App-Factory — der Einstiegspunkt der Anwendung. + +### Was zu tun ist + +Erstelle **`src/app.py`**: + +```python +from flask import Flask +from src.queue.handler import QueueHandler + + +def create_app() -> Flask: + """Erstellt und konfiguriert die Flask-App.""" + app = Flask(__name__) + app.config.from_mapping( + REDIS_HOST="localhost", + REDIS_PORT=6379, + ) + app.queue_handler = QueueHandler( + redis_host=app.config["REDIS_HOST"], + redis_port=app.config["REDIS_PORT"], + ) + + @app.route("/health") + def health() -> dict: + return {"status": "ok", "queue_size": app.queue_handler.queue_size()} + + return app +``` + +### Anforderungen +- App-Factory-Pattern (`create_app()`) — KEIN globales `app = Flask()` +- QueueHandler als App-Extension (`app.queue_handler`) +- `/health` Endpoint für Monitoring + +### Test-Datei: `tests/test_app.py` +Schreibe 3 Tests mit Flask-Test-Client: +1. create_app() gibt Flask-App zurück +2. GET /health → 200, JSON mit "status": "ok" +3. GET /health → queue_size ist Integer + +### Abgabe +- 2 Dateien: `src/app.py`, `tests/test_app.py` +- Commit: `feat: Flask-App-Factory mit Health-Check und QueueHandler` +- Push auf `origin/main` + +### ⚠️ NACH DIESER AUFGABE +Starte einen **neuen Context**. NICHT im gleichen Context weiterarbeiten. + +--- + +## Aufgabe 9: WhatsApp-Webhook — Route + Signatur + Queue + +Erstelle den WhatsApp-Webhook-Endpunkt als Flask-Blueprint. + +### Was zu tun ist + +Erstelle zwei Dateien: + +**`src/routes/__init__.py`** — leer + +**`src/routes/whatsapp.py`** als Flask-Blueprint: + +```python +from flask import Blueprint, request, jsonify, current_app +from src.queue.webhook import verify_whatsapp_signature +from src.models.submission import Submission + +whatsapp_bp = Blueprint("whatsapp", __name__) + + +@whatsapp_bp.route("/webhook/whatsapp", methods=["GET", "POST"]) +def whatsapp_webhook(): + """WhatsApp Webhook-Endpunkt.""" + + # GET = Verifikation (Hub Challenge) + if request.method == "GET": + mode = request.args.get("hub.mode") + token = request.args.get("hub.verify_token") + challenge = request.args.get("hub.challenge") + if mode == "subscribe" and token == "bsn_verify_token": + return challenge, 200 + return "Verification failed", 403 + + # POST = Eingehende Nachricht + signature = request.headers.get("X-Hub-Signature-256", "") + payload = request.get_data() + + if not verify_whatsapp_signature(payload, signature): + return jsonify({"error": "Invalid signature"}), 401 + + qh = current_app.queue_handler + # Submission bauen und enqueuen + ... + return jsonify({"status": "accepted"}), 200 +``` + +### Anforderungen +- Blueprint, keine direkte @app.route +- GET = Hub-Challenge, POST = Signatur → Queue → 200 +- Sofort 200 OK, KEINE synchrone Verarbeitung + +### Test-Datei: `tests/test_whatsapp.py` +Schreibe 4 Tests: +1. GET mit korrektem verify_token → 200 + Challenge +2. GET mit falschem verify_token → 403 +3. POST mit korrekter Signatur → 200 +4. POST mit falscher Signatur → 401 + +### Abgabe +- 3 Dateien: `src/routes/__init__.py`, `src/routes/whatsapp.py`, `tests/test_whatsapp.py` +- Commit: `feat: WhatsApp-Webhook mit Signatur-Prüfung und Queue-Enqueue` +- Push auf `origin/main` + +### ⚠️ NACH DIESER AUFGABE +Starte einen **neuen Context**. NICHT im gleichen Context weiterarbeiten. + +--- + +## Aufgabe 10: Telegram-Webhook — Route + Queue + +Erstelle den Telegram-Webhook-Endpunkt als Flask-Blueprint. + +### Was zu tun ist + +Erstelle **`src/routes/telegram.py`**: + +```python +from flask import Blueprint, request, jsonify, current_app +from src.models.submission import Submission + +telegram_bp = Blueprint("telegram", __name__) + + +@telegram_bp.route("/webhook/telegram", methods=["POST"]) +def telegram_webhook(): + """Telegram Webhook-Endpunkt.""" + + data = request.get_json(silent=True) + if not data: + return jsonify({"error": "Invalid JSON"}), 400 + + qh = current_app.queue_handler + + message = data.get("message", {}) + message_id = str(message.get("message_id", "")) + chat_id = str(message.get("chat", {}).get("id", "")) + text = message.get("text", "") + + if not message_id or not text: + return jsonify({"error": "Missing message_id or text"}), 400 + + submission = Submission( + message_id=message_id, + plattform="telegram", + absender=chat_id, + text=text, + ) + qh.enqueue(submission) + + return jsonify({"status": "accepted"}), 200 +``` + +### Anforderungen +- Nur POST (Telegram nutzt setWebhook einmalig) +- `request.get_json(silent=True)` mit None-Check +- Sofort 200 OK nach Enqueue + +### Test-Datei: `tests/test_telegram.py` +Schreibe 4 Tests: +1. POST mit gültigem Payload → 200 +2. POST ohne JSON → 400 +3. POST ohne message_id → 400 +4. POST mit leerem text → 400 + +### Abgabe +- 2 Dateien: `src/routes/telegram.py`, `tests/test_telegram.py` +- Commit: `feat: Telegram-Webhook mit JSON-Validierung und Queue-Enqueue` +- Push auf `origin/main` + +### ⚠️ NACH DIESER AUFGABE — FERTIG FÜR HEUTE +Alle 10 Aufgaben abgeschlossen. Gute Nacht! 🌙