271 lines
7.8 KiB
Markdown
271 lines
7.8 KiB
Markdown
# 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`
|