Files
BSN-Livesystem/docs/alicia-aufgaben-nacht1.md
T

17 KiB

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:

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:

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:

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:

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:

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:

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:

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:

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:

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