feat: sys-2 Edge-Queue — Signatur-Prüfung, Dedup, asynchrone Verarbeitung
Quality Gate / 🛡️ Lint + Type Check + Tests (push) Failing after 11m21s

- webhook_queue.py: WhatsApp X-Hub-Signature-256 + Telegram Webhook-Intake
- queue_handler.py: SQLite-basierte Queue mit Idempotenz (UNIQUE message_id)
- queue_worker.py: Asynchroner Worker für GPU-Verarbeitung
- app.py: Webhooks umgebaut → sofort 200 OK, Worker verarbeitet später
- Redis 8.0 läuft als Infrastruktur (Queue ist SQLite für Persistenz)
This commit is contained in:
Hermes Agent
2026-06-21 23:59:17 +02:00
parent 5bad339558
commit a5eb0e7d27
5 changed files with 834 additions and 497 deletions
+225
View File
@@ -0,0 +1,225 @@
#!/usr/bin/env python3
"""
Webhook-Queue-Integration — Signatur-Prüfung + Enqueue.
=======================================================
Wird in app.py importiert und wickelt die Webhook-Routen ab.
Ersetzt NICHT die bestehende Logik, sondern sitzt DAVOR.
Flow:
1. Webhook empfängt Request
2. Signatur wird geprüft (WhatsApp X-Hub-Signature-256)
3. Message-IDs werden extrahiert
4. Submission wird in Queue eingereiht (SQLite UNIQUE = Dedup)
5. 200 OK wird sofort zurückgegeben
6. Worker (queue_worker.py) verarbeitet asynchron
Usage in app.py:
from webhook_queue import whatsapp_intake, telegram_intake
@app.route("/whatsapp/webhook", methods=["GET", "POST"])
def whatsapp_webhook():
...
return whatsapp_intake(request, db)
@app.route("/telegram/webhook", methods=["GET", "POST"])
def telegram_webhook():
...
return telegram_intake(request, db)
"""
import hashlib
import hmac
import logging
import os
from typing import Optional
logger = logging.getLogger(__name__)
# ── Konfiguration ────────────────────────────────────────────────────
META_APP_SECRET = os.environ.get("META_APP_SECRET", "")
# Falls kein App Secret gesetzt → Signatur-Prüfung überspringen (Test-Modus)
# ── Signatur-Prüfung ─────────────────────────────────────────────────
def verify_whatsapp_signature(
raw_body: bytes,
signature_header: Optional[str],
) -> bool:
"""
Prüft die X-Hub-Signature-256 von Meta.
Args:
raw_body: Der rohe Request-Body als Bytes.
signature_header: Wert des X-Hub-Signature-256-Headers.
Returns:
True wenn gültig oder Test-Modus (kein Secret).
"""
if not META_APP_SECRET:
logger.debug("Kein META_APP_SECRET → Signatur-Prüfung übersprungen")
return True
if not signature_header:
logger.warning("X-Hub-Signature-256 Header fehlt")
return False
if not signature_header.startswith("sha256="):
logger.warning("Unbekanntes Signatur-Format: %s...", signature_header[:30])
return False
expected_hex = signature_header[7:] # Nach "sha256="
computed_hmac = hmac.new(
key=META_APP_SECRET.encode("utf-8"),
msg=raw_body,
digestmod=hashlib.sha256,
)
computed_hex = computed_hmac.hexdigest()
if not hmac.compare_digest(computed_hex, expected_hex):
logger.warning(
"❌ WhatsApp-Signatur UNGÜLTIG — möglicher Replay-Angriff"
)
return False
logger.debug("✅ WhatsApp-Signatur gültig")
return True
# ── Intake-Funktionen ────────────────────────────────────────────────
def whatsapp_intake(request_obj, db) -> tuple:
"""
Verarbeitet EINE WhatsApp-Webhook-Anfrage.
Args:
request_obj: Flask request-Objekt.
db: SQLite-Datenbankverbindung.
Returns:
(body, status_code) — IMMER 200 OK.
"""
from queue_handler import QueueHandler
qh = QueueHandler(db)
# ── 1. Signatur prüfen ──────────────────────────────────────────
raw_body = request_obj.get_data()
signature = request_obj.headers.get("X-Hub-Signature-256", "")
if not verify_whatsapp_signature(raw_body, signature):
logger.warning("WhatsApp-Request mit ungültiger Signatur abgelehnt")
return "Forbidden", 403
# ── 2. Body parsen ──────────────────────────────────────────────
body = request_obj.get_json(force=True, silent=True)
if not body:
return "OK", 200
entries = body.get("entry", [])
enqueued = 0
duplicates = 0
# ── 3. Jede Nachricht in Queue ──────────────────────────────────
for entry in entries:
changes = entry.get("changes", [])
for change in changes:
value = change.get("value", {})
messages = value.get("messages", [])
for msg in messages:
msg_id = msg.get("id", "")
if not msg_id:
continue
# Payload bauen (nur das relevante message-Objekt)
payload = {
"entry": [{
"id": entry.get("id", ""),
"changes": [{
"value": {
"messaging_product": value.get("messaging_product", "whatsapp"),
"metadata": value.get("metadata", {}),
"contacts": value.get("contacts", []),
"messages": [msg],
}
}]
}]
}
success, status = qh.enqueue(
channel="whatsapp",
message_id=msg_id,
payload=payload,
raw_body=raw_body,
)
if success:
enqueued += 1
else:
duplicates += 1
logger.info("WhatsApp-Duplikat: %s%s", msg_id, status)
logger.info(
"WhatsApp-Intake: %d enqueued, %d duplicates",
enqueued,
duplicates,
)
return "OK", 200
def telegram_intake(request_obj, db) -> tuple:
"""
Verarbeitet EINE Telegram-Webhook-Anfrage.
Args:
request_obj: Flask request-Objekt.
db: SQLite-Datenbankverbindung.
Returns:
(body, status_code) — IMMER 200 OK.
"""
from queue_handler import QueueHandler
qh = QueueHandler(db)
body = request_obj.get_json(force=True, silent=True)
if not body:
return "OK", 200
msg = body.get("message", {})
if not msg:
# Kein Message-Objekt (z.B. edited_message, callback_query)
update_id = str(body.get("update_id", "unknown"))
logger.debug("Telegram-Update ohne Message: update_id=%s", update_id)
return "OK", 200
msg_id = str(msg.get("message_id", ""))
if not msg_id:
return "OK", 200
# Payload ist das gesamte Update (enthält update_id + message)
success, status = qh.enqueue(
channel="telegram",
message_id=msg_id,
payload=body,
raw_body=request_obj.get_data(),
)
if success:
logger.info("Telegram-Intake: %s enqueued", msg_id)
else:
logger.info("Telegram-Duplikat: %s%s", msg_id, status)
return "OK", 200
# ── Queue-Status (für Monitoring) ────────────────────────────────────
def get_queue_status(db) -> dict:
"""Gibt Queue-Statistiken zurück (für /admin/queue Endpoint)."""
from queue_handler import QueueHandler
qh = QueueHandler(db)
return qh.get_queue_stats()