whatsapp integration

This commit is contained in:
debu-2 2026-10-02 12:51:46 +05:30
parent d077292bbc
commit 141ca9fda3
15 changed files with 578 additions and 1 deletions

View file

@ -0,0 +1,144 @@
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