meta embed flow

This commit is contained in:
debu-2 2026-10-07 23:25:19 +05:30
parent ae72b59878
commit 99ed8f2477
10 changed files with 2256 additions and 151 deletions

View file

@ -3,15 +3,29 @@ import logging
from fastapi import APIRouter, Depends, Header, HTTPException, Query, Request, Response, status
from pydantic import BaseModel, Field
from sqlalchemy import select
from sqlalchemy import or_, 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 .models import UserAccount, WhatsAppBusinessAccount, WhatsAppPhoneAsset, WhatsAppWebhookEvent
from .whatsapp_graph_client import WhatsAppGraphClient, WhatsAppGraphError
from .whatsapp_config import resolved_whatsapp_app_secret
from .whatsapp_onboarding import (
RECOMMENDED_WEBHOOK_FIELDS,
account_detail,
account_logs,
complete_signup,
customer_token_for_phone,
developer_config,
disconnect_account,
list_accounts,
mask_identifier,
list_templates,
refresh_account,
signup_public_config,
)
from .whatsapp_security import verify_meta_signature
from .whatsapp_webhook import persist_webhook_payload
from .whatsapp_messages import record_inbound_messages
@ -20,22 +34,12 @@ 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,
"waba_id": mask_identifier(event.waba_id),
"phone_number_id": mask_identifier(event.phone_number_id),
"field": event.field,
"event_type": event.event_type,
"from_wa_id": event.from_wa_id,
@ -101,19 +105,14 @@ async def receive_webhook(
def whatsapp_setup(
_: UserAccount = Depends(current_user),
) -> dict:
"""Console setup info — paste these values into Meta Developer Console."""
"""Customer-safe webhook address. Secrets stay on the server."""
return {
"webhook_url": settings.whatsapp_webhook_url,
"verify_token": settings.whatsapp_verify_token or None,
"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,
"verify_token_visible": False,
"api_version": settings.whatsapp_api_version,
"recommended_fields": RECOMMENDED_WEBHOOK_FIELDS,
"docs_url": "https://developers.facebook.com/documentation/business-messaging/whatsapp/webhooks/overview/",
"docs_url": "https://developers.facebook.com/documentation/business-messaging/whatsapp/embedded-signup",
}
@ -121,10 +120,24 @@ def whatsapp_setup(
def list_webhook_events(
limit: int = Query(default=25, ge=1, le=100),
db: Session = Depends(get_db),
_: UserAccount = Depends(current_user),
user: UserAccount = Depends(current_user),
) -> list[dict]:
waba_ids = list(
db.scalars(select(WhatsAppBusinessAccount.waba_id).where(WhatsAppBusinessAccount.user_id == user.id)).all()
)
phone_ids = list(
db.scalars(select(WhatsAppPhoneAsset.phone_number_id).where(WhatsAppPhoneAsset.user_id == user.id)).all()
)
if not waba_ids and not phone_ids:
return []
filters = []
if waba_ids:
filters.append(WhatsAppWebhookEvent.waba_id.in_(waba_ids))
if phone_ids:
filters.append(WhatsAppWebhookEvent.phone_number_id.in_(phone_ids))
events = db.scalars(
select(WhatsAppWebhookEvent)
.where(or_(*filters))
.order_by(WhatsAppWebhookEvent.created_at.desc())
.limit(limit)
).all()
@ -134,15 +147,106 @@ def list_webhook_events(
class SendWhatsAppTextRequest(BaseModel):
to: str = Field(min_length=8, max_length=20)
body: str = Field(min_length=1, max_length=4096)
phone_number_id: str = Field(min_length=4, max_length=64)
class SignupCompleteRequest(BaseModel):
event: str = Field(min_length=1, max_length=80)
code: str | None = Field(default=None, max_length=2048)
waba_id: str | None = Field(default=None, max_length=64)
phone_number_id: str | None = Field(default=None, max_length=64)
business_id: str | None = Field(default=None, max_length=64)
error_message: str | None = Field(default=None, max_length=500)
@router.get("/signup/config")
def whatsapp_signup_config(
db: Session = Depends(get_db),
_: UserAccount = Depends(current_user),
) -> dict:
return signup_public_config(db)
@router.get("/accounts")
def whatsapp_accounts(
db: Session = Depends(get_db),
user: UserAccount = Depends(current_user),
) -> list[dict]:
return list_accounts(db, user)
@router.post("/signup/complete")
async def whatsapp_signup_complete(
payload: SignupCompleteRequest,
db: Session = Depends(get_db),
user: UserAccount = Depends(current_user),
) -> dict:
return await complete_signup(db, user, payload.model_dump())
@router.get("/accounts/{account_id}")
def whatsapp_account_detail(
account_id: str,
db: Session = Depends(get_db),
user: UserAccount = Depends(current_user),
) -> dict:
return account_detail(db, user, account_id)
@router.post("/accounts/{account_id}/refresh")
async def whatsapp_account_refresh(
account_id: str,
db: Session = Depends(get_db),
user: UserAccount = Depends(current_user),
) -> dict:
return await refresh_account(db, user, account_id)
@router.post("/accounts/{account_id}/disconnect")
async def whatsapp_account_disconnect(
account_id: str,
db: Session = Depends(get_db),
user: UserAccount = Depends(current_user),
) -> dict:
return await disconnect_account(db, user, account_id)
@router.get("/accounts/{account_id}/templates")
async def whatsapp_account_templates(
account_id: str,
db: Session = Depends(get_db),
user: UserAccount = Depends(current_user),
) -> dict:
return await list_templates(db, user, account_id)
@router.get("/accounts/{account_id}/logs")
def whatsapp_account_logs(
account_id: str,
db: Session = Depends(get_db),
user: UserAccount = Depends(current_user),
) -> dict:
return account_logs(db, user, account_id)
@router.get("/accounts/{account_id}/developer")
def whatsapp_account_developer(
account_id: str,
db: Session = Depends(get_db),
user: UserAccount = Depends(current_user),
) -> dict:
return developer_config(db, user, account_id)
@router.post("/messages/send")
async def send_whatsapp_text(
payload: SendWhatsAppTextRequest,
_: UserAccount = Depends(current_user),
db: Session = Depends(get_db),
user: UserAccount = Depends(current_user),
) -> dict:
"""Send a test text message via Graph API (requires WHATSAPP_ACCESS_TOKEN)."""
client = WhatsAppGraphClient()
"""Send from a WhatsApp number this workspace has actually connected."""
token, phone_number_id = customer_token_for_phone(db, user, payload.phone_number_id.strip())
client = WhatsAppGraphClient(access_token=token, phone_number_id=phone_number_id)
try:
return await client.send_text(payload.to.strip(), payload.body.strip())
except WhatsAppGraphError as exc: