# 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! 🌙