chore: Projekt-Initialisierung — Plan, Aufgaben, README
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
@@ -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! 🌙
|
||||
Reference in New Issue
Block a user