meta embed flow
This commit is contained in:
parent
ae72b59878
commit
99ed8f2477
10 changed files with 2256 additions and 151 deletions
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue