access fix
This commit is contained in:
parent
73ce8c5f95
commit
bb9fc9a9cf
8 changed files with 309 additions and 192 deletions
|
|
@ -257,12 +257,7 @@ def list_accounts(db: Session, user: UserAccount) -> list[dict[str, Any]]:
|
|||
|
||||
|
||||
async def _load_waba(token: str, waba_id: str) -> dict[str, Any]:
|
||||
return await _graph(
|
||||
"GET",
|
||||
waba_id,
|
||||
token=token,
|
||||
params={"fields": "id,name,owner_business_info"},
|
||||
)
|
||||
return await _graph_fields(token, waba_id, ["id,name,owner_business_info", "id,name"])
|
||||
|
||||
|
||||
async def _load_phones(token: str, waba_id: str) -> list[dict[str, Any]]:
|
||||
|
|
@ -620,6 +615,148 @@ def _clean_token(value: str) -> str:
|
|||
return token
|
||||
|
||||
|
||||
def _manual_connection(connection: WhatsAppMetaConnection | None) -> bool:
|
||||
return bool(connection and connection.granted_scopes == "manual")
|
||||
|
||||
|
||||
def _field_rejected(exc: HTTPException) -> bool:
|
||||
detail = str(exc.detail).lower()
|
||||
return "nonexisting field" in detail or "(#100)" in detail
|
||||
|
||||
|
||||
async def _graph_fields(token: str, path: str, field_sets: list[str], extra: dict[str, Any] | None = None) -> dict[str, Any]:
|
||||
"""Try the smallest field set that this token's Graph version accepts."""
|
||||
last: HTTPException | None = None
|
||||
for fields in field_sets:
|
||||
params = dict(extra or {})
|
||||
params["fields"] = fields
|
||||
try:
|
||||
return await _graph("GET", path, token=token, params=params)
|
||||
except HTTPException as exc:
|
||||
last = exc
|
||||
if _field_rejected(exc):
|
||||
continue
|
||||
raise
|
||||
if last is not None:
|
||||
raise last
|
||||
return {}
|
||||
|
||||
|
||||
async def _phones_for_token(token: str, waba_id: str, phone_id: str | None) -> list[dict[str, Any]]:
|
||||
"""Read numbers with the token the customer supplied. Never registers a number."""
|
||||
rows: list[dict[str, Any]] = []
|
||||
try:
|
||||
data = await _graph_fields(
|
||||
token,
|
||||
f"{waba_id}/phone_numbers",
|
||||
["id,display_phone_number,verified_name", "id"],
|
||||
{"limit": "50"},
|
||||
)
|
||||
raw = data.get("data")
|
||||
rows = [row for row in raw if isinstance(row, dict)] if isinstance(raw, list) else []
|
||||
except HTTPException:
|
||||
logger.info("Token connection could not list numbers for WhatsApp account %s", waba_id[-4:])
|
||||
for row in rows:
|
||||
if not row.get("status"):
|
||||
row["status"] = "CONNECTED"
|
||||
seen = {str(row.get("id")) for row in rows if row.get("id")}
|
||||
if phone_id and phone_id not in seen:
|
||||
detail: dict[str, Any] = {"id": phone_id, "status": "CONNECTED"}
|
||||
try:
|
||||
node = await _graph_fields(token, phone_id, ["id,display_phone_number,verified_name", "id"])
|
||||
if isinstance(node, dict):
|
||||
detail.update({key: value for key, value in node.items() if value})
|
||||
detail["status"] = "CONNECTED"
|
||||
except HTTPException:
|
||||
logger.info("Token connection stored phone %s without extra fields", phone_id[-4:])
|
||||
rows.append(detail)
|
||||
return rows
|
||||
|
||||
|
||||
async def _save_token_account(
|
||||
db: Session,
|
||||
user: UserAccount,
|
||||
connection: WhatsAppMetaConnection,
|
||||
token: str,
|
||||
waba_id: str,
|
||||
phone_id: str | None,
|
||||
) -> WhatsAppBusinessAccount:
|
||||
"""Store a pasted WhatsApp token without Embedded Signup fields or phone registration."""
|
||||
claimed = _claim_waba(db, user, waba_id)
|
||||
if claimed is None and db.scalar(select(WhatsAppBusinessAccount.id).where(WhatsAppBusinessAccount.waba_id == waba_id)):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail="This WhatsApp Business Account is already connected to another Urmi workspace",
|
||||
)
|
||||
try:
|
||||
waba = await _graph_fields(token, waba_id, ["id,name"])
|
||||
except HTTPException as exc:
|
||||
account = claimed or WhatsAppBusinessAccount(
|
||||
user_id=user.id,
|
||||
connection_id=connection.id,
|
||||
waba_id=waba_id,
|
||||
name="WhatsApp Business",
|
||||
)
|
||||
if claimed is None:
|
||||
db.add(account)
|
||||
db.flush()
|
||||
detail = str(exc.detail)[:500]
|
||||
expired = "expired" in detail.lower() or "validating access token" in detail.lower()
|
||||
account.status = "expired" if expired else "failed"
|
||||
account.last_error = detail
|
||||
account.messaging_status = "unavailable"
|
||||
account.management_status = "unavailable"
|
||||
connection.status = "failed"
|
||||
connection.last_error = account.last_error
|
||||
_audit(db, user, "token_rejected", account.last_error or "", connection_id=connection.id, waba_id=waba_id)
|
||||
return account
|
||||
|
||||
name = str(waba.get("name") or "WhatsApp Business")
|
||||
connection.business_name = name
|
||||
connection.status = "connected"
|
||||
connection.last_error = None
|
||||
connection.access_token = token
|
||||
connection.granted_scopes = "manual"
|
||||
|
||||
account = claimed or WhatsAppBusinessAccount(
|
||||
user_id=user.id,
|
||||
connection_id=connection.id,
|
||||
waba_id=waba_id,
|
||||
)
|
||||
if claimed is None:
|
||||
db.add(account)
|
||||
db.flush()
|
||||
previous_connection_id = account.connection_id if claimed is not None else None
|
||||
account.user_id = user.id
|
||||
account.connection_id = connection.id
|
||||
if previous_connection_id and previous_connection_id != connection.id:
|
||||
previous = db.get(WhatsAppMetaConnection, previous_connection_id)
|
||||
if previous is not None and previous.user_id == user.id:
|
||||
previous.access_token = None
|
||||
account.name = name
|
||||
account.last_error = None
|
||||
account.management_status = "active"
|
||||
|
||||
phones = await _phones_for_token(token, waba_id, phone_id)
|
||||
if phones:
|
||||
_apply_phones(db, user, account, phones, phone_id)
|
||||
subscribed = await _subscribe(token, waba_id)
|
||||
template_count, templates_ok = await _template_count(token, waba_id)
|
||||
account.template_count = template_count
|
||||
account.templates_status = "active" if templates_ok else "unavailable"
|
||||
account.webhooks_status = "active" if subscribed else "unavailable"
|
||||
if account.phones:
|
||||
account.status = "connected"
|
||||
account.messaging_status = "active"
|
||||
_audit(db, user, "token_connected", "WhatsApp token saved for this account", connection_id=connection.id, waba_id=waba_id)
|
||||
else:
|
||||
account.status = "incomplete"
|
||||
account.messaging_status = "unavailable"
|
||||
account.last_error = "The token is saved. Add the phone number ID from Meta so this account has a number to use."
|
||||
_audit(db, user, "token_needs_phone", account.last_error, connection_id=connection.id, waba_id=waba_id)
|
||||
return account
|
||||
|
||||
|
||||
async def connect_with_token(
|
||||
db: Session,
|
||||
user: UserAccount,
|
||||
|
|
@ -627,11 +764,7 @@ async def connect_with_token(
|
|||
waba_id: str,
|
||||
phone_number_id: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Store a customer-supplied WhatsApp token and provision that account.
|
||||
|
||||
This is the non-embedded alternative to Embedded Signup. The token is
|
||||
kept on the connection row and is not returned to the browser.
|
||||
"""
|
||||
"""Store a customer-supplied WhatsApp token. Does not use Embedded Signup."""
|
||||
token = _clean_token(access_token)
|
||||
account_id = "".join(str(waba_id or "").split())
|
||||
phone_id = "".join(str(phone_number_id or "").split()) or None
|
||||
|
|
@ -664,21 +797,21 @@ async def connect_with_token(
|
|||
connection_id=connection.id,
|
||||
waba_id=account_id,
|
||||
)
|
||||
account = await _provision_waba(db, user, connection, token, account_id, phone_id)
|
||||
account = await _save_token_account(db, user, connection, token, account_id, phone_id)
|
||||
db.commit()
|
||||
db.refresh(account)
|
||||
if account.status == "connected":
|
||||
overall = "connected"
|
||||
message = "WhatsApp Business is connected."
|
||||
message = "This WhatsApp account is saved for the workspace."
|
||||
elif account.status == "incomplete":
|
||||
overall = "incomplete"
|
||||
message = account.last_error or "The account was saved, but the phone number still needs verification."
|
||||
message = account.last_error or "The token is saved. Add the phone number ID to finish."
|
||||
elif account.status == "expired":
|
||||
overall = "expired"
|
||||
message = account.last_error or "Meta did not accept this access token."
|
||||
else:
|
||||
overall = "failed"
|
||||
message = account.last_error or "Urmi could not finish WhatsApp setup."
|
||||
message = account.last_error or "Meta did not accept this WhatsApp token."
|
||||
return {"status": overall, "message": message, "accounts": [serialize_account(account)]}
|
||||
|
||||
|
||||
|
|
@ -694,7 +827,10 @@ async def refresh_account(db: Session, user: UserAccount, account_id: Any) -> di
|
|||
db.commit()
|
||||
db.refresh(account)
|
||||
return serialize_account(account)
|
||||
await _provision_waba(db, user, connection, token, account.waba_id, None)
|
||||
if _manual_connection(connection):
|
||||
await _save_token_account(db, user, connection, token, account.waba_id, None)
|
||||
else:
|
||||
await _provision_waba(db, user, connection, token, account.waba_id, None)
|
||||
db.commit()
|
||||
db.refresh(account)
|
||||
return serialize_account(account)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue