This commit is contained in:
debu-2 2026-10-14 05:46:18 +05:30
parent dcae960928
commit a7e6ed2e4b
3 changed files with 107 additions and 14 deletions

View file

@ -169,24 +169,31 @@ async def _debug_token(db: Session, token: str) -> dict[str, Any]:
return payload if isinstance(payload, dict) else {} return payload if isinstance(payload, dict) else {}
def _waba_ids_from_debug(debug: dict[str, Any]) -> list[str]: def _target_ids(debug: dict[str, Any], scope: str) -> list[str]:
found: list[str] = [] found: list[str] = []
scopes = debug.get("granular_scopes") scopes = debug.get("granular_scopes")
if not isinstance(scopes, list): if not isinstance(scopes, list):
return found return found
preferred = {"whatsapp_business_management", "whatsapp_business_messaging"}
for item in scopes: for item in scopes:
if not isinstance(item, dict): if not isinstance(item, dict) or item.get("scope") != scope:
continue
if item.get("scope") not in preferred:
continue continue
for target in item.get("target_ids") or []: for target in item.get("target_ids") or []:
value = str(target) value = str(target).strip()
if value and value not in found: if value and value not in found:
found.append(value) found.append(value)
return found return found
def _waba_ids_from_debug(debug: dict[str, Any]) -> list[str]:
"""whatsapp_business_management targets are WABA IDs."""
return _target_ids(debug, "whatsapp_business_management")
def _phone_ids_from_debug(debug: dict[str, Any]) -> list[str]:
"""whatsapp_business_messaging targets are phone number IDs, not WABA IDs."""
return _target_ids(debug, "whatsapp_business_messaging")
def _as_uuid(account_id: Any) -> uuid.UUID: def _as_uuid(account_id: Any) -> uuid.UUID:
try: try:
return account_id if isinstance(account_id, uuid.UUID) else uuid.UUID(str(account_id)) return account_id if isinstance(account_id, uuid.UUID) else uuid.UUID(str(account_id))
@ -599,7 +606,15 @@ async def complete_signup(db: Session, user: UserAccount, payload: dict[str, Any
} }
session_waba = str(payload.get("waba_id") or "").strip() session_waba = str(payload.get("waba_id") or "").strip()
waba_ids = [session_waba] if session_waba else _waba_ids_from_debug(debug) session_phone = str(payload.get("phone_number_id") or "").strip() or None
management_ids = _waba_ids_from_debug(debug)
phone_ids = _phone_ids_from_debug(debug)
if session_waba and session_waba in phone_ids and session_waba not in management_ids:
session_phone = session_phone or session_waba
session_waba = ""
waba_ids = [session_waba] if session_waba else management_ids
if not session_phone and len(phone_ids) == 1:
session_phone = phone_ids[0]
if not waba_ids: if not waba_ids:
_audit(db, user, "provision_failed", "Embedded Signup did not return a WhatsApp Business Account") _audit(db, user, "provision_failed", "Embedded Signup did not return a WhatsApp Business Account")
db.commit() db.commit()
@ -622,7 +637,6 @@ async def complete_signup(db: Session, user: UserAccount, payload: dict[str, Any
db.flush() db.flush()
_audit(db, user, "authorization_received", "Embedded Signup returned an authorization code", connection_id=connection.id) _audit(db, user, "authorization_received", "Embedded Signup returned an authorization code", connection_id=connection.id)
session_phone = str(payload.get("phone_number_id") or "").strip() or None
accounts: list[WhatsAppBusinessAccount] = [] accounts: list[WhatsAppBusinessAccount] = []
for waba_id in waba_ids: for waba_id in waba_ids:
accounts.append(await _provision_waba(db, user, connection, token, waba_id, session_phone)) accounts.append(await _provision_waba(db, user, connection, token, waba_id, session_phone))

View file

@ -12,7 +12,19 @@ from sqlalchemy.orm import Session, selectinload
from .config import settings from .config import settings
from .models import UserAccount, WhatsAppBusinessAccount, WhatsAppMessage, WhatsAppMessageTemplate, WhatsAppMetaConnection from .models import UserAccount, WhatsAppBusinessAccount, WhatsAppMessage, WhatsAppMessageTemplate, WhatsAppMetaConnection
from .whatsapp_onboarding import _graph, _graph_error_message, _owned_account, _quality_label, _token_app_id from .whatsapp_onboarding import (
_apply_phones,
_debug_token,
_graph,
_graph_error_message,
_load_phones,
_load_waba,
_owned_account,
_phone_ids_from_debug,
_quality_label,
_token_app_id,
_waba_ids_from_debug,
)
_NAME = re.compile(r"^[a-z0-9_]{1,512}$") _NAME = re.compile(r"^[a-z0-9_]{1,512}$")
_LANG = re.compile(r"^[a-z]{2}(_[A-Z]{2})?$") _LANG = re.compile(r"^[a-z]{2}(_[A-Z]{2})?$")
@ -510,6 +522,62 @@ async def _pull_templates(db: Session, account: WhatsAppBusinessAccount, token:
return seen return seen
def _wrong_template_node(exc: HTTPException) -> bool:
detail = str(exc.detail).lower()
return "message_templates" in detail and "nonexisting field" in detail
async def _correct_waba_id(db: Session, account: WhatsAppBusinessAccount, token: str) -> bool:
"""Replace a stored phone-number ID with the WABA ID from the embedded-signup token."""
try:
debug = await _debug_token(db, token)
except HTTPException:
return False
wabas = _waba_ids_from_debug(debug)
phone_ids = set(_phone_ids_from_debug(debug))
stored = str(account.waba_id)
if not wabas or stored in wabas:
return False
real = wabas[0] if len(wabas) == 1 else None
if real is None:
for waba in wabas:
try:
rows = await _load_phones(token, waba)
except HTTPException:
continue
ids = {str(row.get("id")) for row in rows if row.get("id")}
if stored in ids or phone_ids.intersection(ids):
real = waba
break
if not real or real == stored:
return False
taken = db.scalar(
select(WhatsAppBusinessAccount.id).where(
WhatsAppBusinessAccount.waba_id == real,
WhatsAppBusinessAccount.id != account.id,
)
)
if taken is not None:
return False
account.waba_id = real
try:
info = await _load_waba(token, real)
name = str(info.get("name") or "").strip()
if name:
account.name = name[:200]
except HTTPException:
pass
user = db.get(UserAccount, account.user_id)
if user is not None:
try:
rows = await _load_phones(token, real)
_apply_phones(db, user, account, rows, None)
except HTTPException:
pass
db.flush()
return True
async def sync_account_templates(db: Session, account: WhatsAppBusinessAccount) -> str | None: async def sync_account_templates(db: Session, account: WhatsAppBusinessAccount) -> str | None:
tokens = _candidate_tokens(db, account) tokens = _candidate_tokens(db, account)
if not tokens: if not tokens:
@ -519,6 +587,12 @@ async def sync_account_templates(db: Session, account: WhatsAppBusinessAccount)
last_error: str | None = None last_error: str | None = None
for index, token in enumerate(tokens): for index, token in enumerate(tokens):
try: try:
try:
seen = await _pull_templates(db, account, token)
except HTTPException as exc:
db.rollback()
if not _wrong_template_node(exc) or not await _correct_waba_id(db, account, token):
raise
seen = await _pull_templates(db, account, token) seen = await _pull_templates(db, account, token)
except HTTPException as exc: except HTTPException as exc:
db.rollback() db.rollback()

View file

@ -112,11 +112,16 @@ function parseSignupMessage(raw: unknown): SignupSession | null {
const message = data as { type?: string; event?: string; data?: Record<string, unknown> } const message = data as { type?: string; event?: string; data?: Record<string, unknown> }
if (message.type !== 'WA_EMBEDDED_SIGNUP' || !message.event) return null if (message.type !== 'WA_EMBEDDED_SIGNUP' || !message.event) return null
const body = message.data || {} const body = message.data || {}
const metaId = (value: unknown) => {
if (typeof value === 'string' && value.trim()) return value.trim()
if (typeof value === 'number' && Number.isFinite(value)) return String(Math.trunc(value))
return undefined
}
return { return {
event: message.event, event: message.event,
wabaId: typeof body.waba_id === 'string' ? body.waba_id : undefined, wabaId: metaId(body.waba_id),
phoneNumberId: typeof body.phone_number_id === 'string' ? body.phone_number_id : undefined, phoneNumberId: metaId(body.phone_number_id),
businessId: typeof body.business_id === 'string' ? body.business_id : undefined, businessId: metaId(body.business_id),
errorMessage: typeof body.error_message === 'string' ? body.error_message : undefined, errorMessage: typeof body.error_message === 'string' ? body.error_message : undefined,
currentStep: typeof body.current_step === 'string' ? body.current_step : undefined, currentStep: typeof body.current_step === 'string' ? body.current_step : undefined,
} }
@ -311,7 +316,7 @@ export function WhatsAppSetupPage() {
config_id: config.config_id, config_id: config.config_id,
response_type: 'code', response_type: 'code',
override_default_response_type: true, override_default_response_type: true,
extras: {}, extras: { sessionInfoVersion: 3 },
}, },
) )
} catch (reason) { } catch (reason) {