diff --git a/docs/alicia-aufgaben-nacht2.md b/docs/alicia-aufgaben-nacht2.md new file mode 100644 index 0000000..e3b1316 --- /dev/null +++ b/docs/alicia-aufgaben-nacht2.md @@ -0,0 +1,270 @@ +# Alicia — Aufgaben 6–10: BSN-Livesystem + +> **Repo:** `git@git.datenhimmel.work:danielkrause/BSN-Livesystem.git` +> **Branch:** `main` +> **Deine Basis:** Aufgaben 1–5 sind fertig und gepusht. +> **Qualität:** Vor JEDEM Commit: `ruff check . && ruff format . && mypy --config-file pyproject.toml . && pytest -v` +> **Fehler?** Code korrigieren, Checks wiederholen. Nicht aufgeben bis ALLE grün. + +--- + +## Aufgabe 6 — Webhook-Signatur-Prüfung + +### Datei: `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. + + Args: + payload: Der rohe Request-Body. + signature: Der X-Hub-Signature-256 Header-Wert. + app_secret: Der WhatsApp App-Secret (aus Env wenn None). + + Returns: + True wenn Signatur gültig. + """ + 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. + + Args: + token: Bot-Token (aus Env wenn None). + + Returns: + True wenn Token gültig. + """ + expected = token or os.environ.get("TELEGRAM_BOT_TOKEN", "") + if not expected: + return False + return len(expected) > 20 + + +def validate_and_enqueue( + payload: bytes, signature: str, plattform: str, qh: QueueHandler +) -> bool: + """Validiert plattformspezifische Signatur und legt Submission in Queue. + + Args: + payload: Der rohe Request-Body. + signature: Signatur-Header. + plattform: "whatsapp" oder "telegram". + qh: QueueHandler-Instanz. + + Returns: + True wenn validiert und erfolgreich enqueued. + """ + if plattform == "whatsapp": + if not verify_whatsapp_signature(payload, signature): + return False + elif plattform == "telegram": + if not verify_telegram_signature(): + return False + else: + return False + + import json + data = json.loads(payload) + submission = Submission( + message_id=str(data.get("message_id", "")), + plattform=plattform, + absender=str(data.get("absender", "")), + text=str(data.get("text", "")), + ) + return qh.enqueue(submission) is not None +``` + +### Test-Datei: `tests/test_webhook.py` + +5 Tests: +1. Korrekte WhatsApp-Signatur → True +2. Falsche WhatsApp-Signatur → False +3. Fehlender App-Secret → False +4. Korrekter Telegram-Token → True (setze `TELEGRAM_BOT_TOKEN` mit ≥21 Zeichen) +5. Zu kurzer Telegram-Token → False + +**Commit:** `feat: Webhook-Signatur-Prüfung für WhatsApp + Telegram` + +--- + +## Aufgabe 7 — Queue-Worker + +### Datei: `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). + + Args: + submission: Die zu verarbeitende Submission. + + Returns: + Immer True im Erfolgsfall. + """ + submission.status = "erledigt" + return True + + +def worker_loop( + qh: QueueHandler, poll_interval: int = 2, batch_size: int = 5 +) -> None: + """Endlos-Schleife: Pollt Queue, verarbeitet Batches. + + Args: + qh: QueueHandler-Instanz. + poll_interval: Sekunden zwischen Polls wenn Queue leer. + batch_size: Maximale Submissions pro Batch. + """ + while True: + submissions = qh.dequeue(batch_size) + if not submissions: + time.sleep(poll_interval) + continue + for submission in submissions: + try: + if verarbeite_submission(submission): + qh.mark_done(submission) + except Exception: + pass # Fehler in einer Submission → weitermachen +``` + +### Test-Datei: `tests/test_worker.py` + +3 Tests mit fakeredis (MOCKE `time.sleep` mit `unittest.mock.patch`): +1. `verarbeite_submission` setzt Status auf "erledigt" +2. `worker_loop` verarbeitet alle Submissions in der Queue (nach Batch prüfen) +3. `worker_loop` crashed nicht bei leerer Queue + +**Commit:** `feat: Queue-Worker mit Batch-Verarbeitung und Fehlertoleranz` + +--- + +## Aufgabe 8 — Flask-App-Factory + +### Datei: `src/app.py` + +```python +from flask import Flask +from src.queue.handler import QueueHandler + + +def create_app() -> Flask: + """Erstellt und konfiguriert die Flask-App. + + Returns: + Flask-App-Instanz mit QueueHandler. + """ + 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[str, object]: + return { + "status": "ok", + "queue_size": app.queue_handler.queue_size(), + } + + return app + + +if __name__ == "__main__": + app = create_app() + app.run(host="0.0.0.0", port=5002, debug=False) +``` + +### Test-Datei: `tests/test_app.py` + +3 Tests mit Flask-Test-Client (`app.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 + +**Commit:** `feat: Flask-App-Factory mit Health-Check und QueueHandler` + +--- + +## Aufgabe 9 — WhatsApp-Webhook (Flask-Blueprint) + +### Datei 1: `src/routes/__init__.py` +Leer lassen. + +### Datei 2: `src/routes/whatsapp.py` + +Blueprint `"whatsapp"` mit `/webhook/whatsapp` (GET + POST): +- **GET:** Hub-Challenge — prüft `hub.mode == "subscribe"` und `hub.verify_token == "bsn_verify_token"` → gibt `hub.challenge` zurück (200), sonst 403 +- **POST:** Signatur via `verify_whatsapp_signature()` prüfen → Submission bauen → `current_app.queue_handler.enqueue()` → 200 +- Ungültige Signatur → 401 +- `WHATSAPP_APP_SECRET` aus `os.environ` + +### Test-Datei: `tests/test_whatsapp.py` + +4 Tests mit `app.test_client()` (Blueprint temporär registrieren): +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 + +**Setze** `os.environ["WHATSAPP_APP_SECRET"] = "test"` und `os.environ["WHATSAPP_VERIFY_TOKEN"] = "test-token"` im Test-Setup. + +**Commit:** `feat: WhatsApp-Webhook mit Signatur-Prüfung und Queue-Enqueue` + +--- + +## Aufgabe 10 — Telegram-Webhook (Flask-Blueprint) + +### Datei: `src/routes/telegram.py` + +Blueprint `"telegram"` mit `/webhook/telegram` (NUR POST): +- `request.get_json(silent=True)` mit None-Check → 400 +- `message_id`, `chat_id`, `text` aus `data["message"]` extrahieren +- Fehlende `message_id` oder `text` → 400 +- `Submission` bauen (`plattform="telegram"`) → `current_app.queue_handler.enqueue()` → 200 +- Pydantic `ValidationError` abfangen → 400 + +### Test-Datei: `tests/test_telegram.py` + +4 Tests mit `app.test_client()`: +1. POST mit gültigem Payload → 200 +2. POST ohne JSON → 400 +3. POST ohne `message_id` → 400 +4. POST mit leerem `text` → 400 + +**Commit:** `feat: Telegram-Webhook mit JSON-Validierung und Queue-Enqueue` + +--- + +## ⚠️ WICHTIG + +- **Vor JEDEM Commit:** `ruff check . && ruff format . && mypy --config-file pyproject.toml . && pytest -v` +- **Nicht pushen bevor ALLE 4 Checks grün sind!** +- **Nach jeder Aufgabe:** Commit + Push, dann nächste. +- **Falls ein Test fehlschlägt:** Code korrigieren, nicht aufgeben. +- **Repo:** Arbeite in `/tmp/BSN-Livesystem` (oder wo du geklont hast) +- **Täglich vor Start:** `git pull origin main`