"""Local webhook demo. Run: python -m uvicorn webhook:app --port 3000. Set UNPOLL_WEBHOOK_SECRET to the signing secret from your agent's settings. This demo prints events and acknowledges them without durable storage or deduplication. Before production, store event IDs atomically with queued work and deduplicate messages by agent and message ID before processing them. """ import hashlib import hmac import json import os import re import time from fastapi import FastAPI, HTTPException, Request, Response app = FastAPI() secret = os.environ["UNPOLL_WEBHOOK_SECRET"].encode("utf-8") if not secret: raise ValueError("UNPOLL_WEBHOOK_SECRET must not be empty") @app.post("/webhook") async def receive(request: Request) -> Response: body = bytearray() async for chunk in request.stream(): body.extend(chunk) if len(body) > 1024 * 1024: raise HTTPException(413, "Payload too large") timestamps = request.headers.getlist("webhook-timestamp") signatures = request.headers.getlist("webhook-signature") if len(timestamps) != 1 or len(signatures) != 1: raise HTTPException(401, "Missing or duplicate signature headers") timestamp, signature = timestamps[0], signatures[0] if ( not re.fullmatch(r"[0-9]{1,20}", timestamp) or not re.fullmatch(r"v1=[0-9a-f]{64}", signature) or abs(int(time.time()) - int(timestamp)) > 300 ): raise HTTPException(401, "Invalid signature or timestamp") signed = timestamp.encode("ascii") + b"." + bytes(body) expected = "v1=" + hmac.new(secret, signed, hashlib.sha256).hexdigest() if not hmac.compare_digest(expected, signature): raise HTTPException(401, "Invalid signature") try: event = json.loads(body) if event["schema_version"] != 1 or event["type"] != "whatsapp.updates": raise ValueError("Unsupported event") print(f"Event {event['event_id']} for agent {event['agent_id']}", flush=True) for entry in event["data"]["entry"]: for change in entry["changes"]: value = change["value"] for message in value.get("messages", []): if message["type"] == "text": print("Message:", message["text"]["body"], flush=True) for status in value.get("statuses", []): print("Receipt:", status["id"], status["status"], flush=True) except (ValueError, KeyError, TypeError, AttributeError): raise HTTPException(400, "Invalid event") from None return Response(status_code=204)