import json import logging from fastapi import APIRouter, Depends, Header, HTTPException, Query, Request, Response, status from pydantic import BaseModel, Field from sqlalchemy import select from sqlalchemy.orm import Session from .auth import current_user from .config import settings from .database import get_db from .models import UserAccount, WhatsAppWebhookEvent from .whatsapp_graph_client import WhatsAppGraphClient, WhatsAppGraphError from .whatsapp_security import verify_meta_signature from .whatsapp_webhook import persist_webhook_payload logger = logging.getLogger(__name__) router = APIRouter(prefix="/api/v1/whatsapp", tags=["whatsapp"]) RECOMMENDED_WEBHOOK_FIELDS = [ "messages", "message_template_status_update", "message_template_quality_update", "account_update", "phone_number_name_update", "phone_number_quality_update", "business_capability_update", ] def _serialize_event(event: WhatsAppWebhookEvent) -> dict: return { "id": str(event.id), "waba_id": event.waba_id, "phone_number_id": event.phone_number_id, "field": event.field, "event_type": event.event_type, "from_wa_id": event.from_wa_id, "message_id": event.message_id, "status": event.status, "created_at": event.created_at, } @router.get("/webhook") def verify_webhook( hub_mode: str | None = Query(default=None, alias="hub.mode"), hub_verify_token: str | None = Query(default=None, alias="hub.verify_token"), hub_challenge: str | None = Query(default=None, alias="hub.challenge"), ) -> Response: """ Meta webhook verification (Step 2 — Configure Webhooks). Meta sends hub.mode=subscribe, hub.verify_token, and hub.challenge. """ if not settings.whatsapp_verify_token: raise HTTPException( status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="WHATSAPP_VERIFY_TOKEN is not configured", ) if hub_mode == "subscribe" and hub_verify_token == settings.whatsapp_verify_token: logger.info("WhatsApp webhook verified by Meta") return Response(content=hub_challenge or "", media_type="text/plain") logger.warning("WhatsApp webhook verification failed mode=%s", hub_mode) raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Verification failed") @router.post("/webhook") async def receive_webhook( request: Request, db: Session = Depends(get_db), x_hub_signature_256: str | None = Header(default=None, alias="X-Hub-Signature-256"), ) -> dict[str, str]: """Receive inbound WhatsApp Business Platform webhook payloads.""" raw_body = await request.body() if settings.whatsapp_app_secret: if not verify_meta_signature(settings.whatsapp_app_secret, raw_body, x_hub_signature_256): logger.warning("WhatsApp webhook signature validation failed") raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Invalid signature") elif settings.environment == "production": raise HTTPException( status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="WHATSAPP_APP_SECRET is required in production", ) try: payload = json.loads(raw_body.decode("utf-8") or "{}") except (UnicodeDecodeError, json.JSONDecodeError): payload = {} persist_webhook_payload(db, payload) return {"status": "ok"} @router.get("/setup") def whatsapp_setup( _: UserAccount = Depends(current_user), ) -> dict: """Console setup info — paste these values into Meta Developer Console.""" return { "webhook_url": settings.whatsapp_webhook_url, "verify_token_configured": bool(settings.whatsapp_verify_token), "app_secret_configured": bool(settings.whatsapp_app_secret), "access_token_configured": bool(settings.whatsapp_access_token), "phone_number_id": settings.whatsapp_phone_number_id or None, "business_account_id": settings.whatsapp_business_account_id or None, "meta_app_id": settings.meta_app_id or None, "api_version": settings.whatsapp_api_version, "recommended_fields": RECOMMENDED_WEBHOOK_FIELDS, "docs_url": "https://developers.facebook.com/documentation/business-messaging/whatsapp/webhooks/overview/", } @router.get("/events") def list_webhook_events( limit: int = Query(default=25, ge=1, le=100), db: Session = Depends(get_db), _: UserAccount = Depends(current_user), ) -> list[dict]: events = db.scalars( select(WhatsAppWebhookEvent) .order_by(WhatsAppWebhookEvent.created_at.desc()) .limit(limit) ).all() return [_serialize_event(event) for event in events] class SendWhatsAppTextRequest(BaseModel): to: str = Field(min_length=8, max_length=20) body: str = Field(min_length=1, max_length=4096) @router.post("/messages/send") async def send_whatsapp_text( payload: SendWhatsAppTextRequest, _: UserAccount = Depends(current_user), ) -> dict: """Send a test text message via Graph API (requires WHATSAPP_ACCESS_TOKEN).""" client = WhatsAppGraphClient() try: return await client.send_text(payload.to.strip(), payload.body.strip()) except WhatsAppGraphError as exc: raise HTTPException(status_code=exc.status_code, detail=exc.detail) from exc