docs: Alicia Aufgaben 6-10 — Webhook, Worker, App-Factory, Routen
This commit is contained in:
@@ -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`
|
||||
Reference in New Issue
Block a user