victor34593993 commited on
Commit
a26d7d3
·
verified ·
1 Parent(s): fde4572

whatsapp: align API version + register number on Cloud API after embedded-signup connect

Browse files
Files changed (2) hide show
  1. app/routes/admin.py +6 -0
  2. app/wa.py +49 -5
app/routes/admin.py CHANGED
@@ -260,6 +260,9 @@ async def connect_whatsapp(
260
  "status": "connected",
261
  "display_phone_number": result["display_phone_number"],
262
  "verified_name": result["verified_name"],
 
 
 
263
  }
264
 
265
 
@@ -316,6 +319,9 @@ async def whatsapp_embedded_signup(
316
  "status": "connected",
317
  "display_phone_number": result["display_phone_number"],
318
  "verified_name": result["verified_name"],
 
 
 
319
  }
320
 
321
 
 
260
  "status": "connected",
261
  "display_phone_number": result["display_phone_number"],
262
  "verified_name": result["verified_name"],
263
+ # "true" once the number is registered on Cloud API (can SEND replies);
264
+ # "false" = receive-only, registration needs attention (e.g. a 2FA PIN).
265
+ "registered": "true" if result.get("registered") else "false",
266
  }
267
 
268
 
 
319
  "status": "connected",
320
  "display_phone_number": result["display_phone_number"],
321
  "verified_name": result["verified_name"],
322
+ # "true" once the number is registered on Cloud API (can SEND replies);
323
+ # "false" = receive-only, registration needs attention (e.g. a 2FA PIN).
324
+ "registered": "true" if result.get("registered") else "false",
325
  }
326
 
327
 
app/wa.py CHANGED
@@ -13,6 +13,7 @@ import hashlib
13
  import hmac
14
  import logging
15
  import re
 
16
  from collections import OrderedDict
17
  from dataclasses import dataclass
18
  from typing import Any
@@ -229,7 +230,7 @@ async def exchange_code(
229
  app_id: str,
230
  app_secret: str,
231
  graph_base: str = "https://graph.facebook.com",
232
- api_version: str = "v25.0",
233
  ) -> dict[str, Any]:
234
  """Exchange an Embedded Signup auth code for the client's business token.
235
 
@@ -255,19 +256,53 @@ async def exchange_code(
255
  return {"ok": True, "token": token}
256
 
257
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
258
  async def connect_number(
259
  *,
260
  waba_id: str,
261
  phone_number_id: str,
262
  token: str,
263
  graph_base: str = "https://graph.facebook.com",
264
- api_version: str = "v25.0",
 
265
  ) -> dict[str, Any]:
266
- """Subscribe a client's WABA to our app and validate the phone number.
 
267
 
268
  The app-level webhook is configured once globally, so after this call the
269
  number's incoming messages reach /whatsapp/webhook with no further setup.
270
- Returns {"ok": True, "display_phone_number": ...} or {"ok": False, "error": ...}.
 
 
271
  """
272
  base = graph_base.rstrip("/")
273
  headers = {"Authorization": f"Bearer {token}"}
@@ -286,10 +321,19 @@ async def connect_number(
286
  if info.status_code >= 400:
287
  return {"ok": False, "error": f"phone_check_failed: {info.text[:300]}"}
288
  data = info.json()
 
 
 
 
 
289
  except httpx.HTTPError as exc:
290
  return {"ok": False, "error": f"network: {exc}"}
291
- return {
292
  "ok": True,
293
  "display_phone_number": data.get("display_phone_number", ""),
294
  "verified_name": data.get("verified_name", ""),
 
295
  }
 
 
 
 
13
  import hmac
14
  import logging
15
  import re
16
+ import secrets
17
  from collections import OrderedDict
18
  from dataclasses import dataclass
19
  from typing import Any
 
230
  app_id: str,
231
  app_secret: str,
232
  graph_base: str = "https://graph.facebook.com",
233
+ api_version: str = "v21.0",
234
  ) -> dict[str, Any]:
235
  """Exchange an Embedded Signup auth code for the client's business token.
236
 
 
256
  return {"ok": True, "token": token}
257
 
258
 
259
+ def _gen_pin() -> str:
260
+ """A random 6-digit Cloud API two-step PIN for number registration."""
261
+ return f"{secrets.randbelow(900000) + 100000}"
262
+
263
+
264
+ async def _register_number(
265
+ client: httpx.AsyncClient, *, base: str, api_version: str,
266
+ phone_number_id: str, headers: dict[str, str], pin: str,
267
+ ) -> dict[str, Any]:
268
+ """Register the number on Cloud API. Embedded Signup subscribes the WABA but
269
+ does NOT register the phone — without this, SENDING replies can fail (errors
270
+ 133010 'already registered' / 131045 'not registered'). Idempotent: an
271
+ already-registered number counts as success."""
272
+ try:
273
+ resp = await client.post(
274
+ f"{base}/{api_version}/{phone_number_id}/register",
275
+ headers={**headers, "Content-Type": "application/json"},
276
+ json={"messaging_product": "whatsapp", "pin": pin},
277
+ )
278
+ except httpx.HTTPError as exc:
279
+ return {"registered": False, "error": f"network: {exc}"}
280
+ if resp.status_code < 400:
281
+ return {"registered": True}
282
+ body = resp.text
283
+ # already registered -> idempotent success (133010, or the textual hint)
284
+ if "133010" in body or "already" in body.lower():
285
+ return {"registered": True, "note": "already_registered"}
286
+ return {"registered": False, "error": f"register_failed: {body[:300]}"}
287
+
288
+
289
  async def connect_number(
290
  *,
291
  waba_id: str,
292
  phone_number_id: str,
293
  token: str,
294
  graph_base: str = "https://graph.facebook.com",
295
+ api_version: str = "v21.0",
296
+ register_pin: str = "",
297
  ) -> dict[str, Any]:
298
+ """Subscribe a client's WABA to our app, validate the phone, and REGISTER it
299
+ on Cloud API (so the number can both receive AND send).
300
 
301
  The app-level webhook is configured once globally, so after this call the
302
  number's incoming messages reach /whatsapp/webhook with no further setup.
303
+ Returns {"ok": True, "display_phone_number": ..., "registered": bool} or
304
+ {"ok": False, "error": ...}. ``registered`` is surfaced (not fatal) so the
305
+ flow can still save a receive-only connection and flag send-readiness.
306
  """
307
  base = graph_base.rstrip("/")
308
  headers = {"Authorization": f"Bearer {token}"}
 
321
  if info.status_code >= 400:
322
  return {"ok": False, "error": f"phone_check_failed: {info.text[:300]}"}
323
  data = info.json()
324
+ reg = await _register_number(
325
+ client, base=base, api_version=api_version,
326
+ phone_number_id=phone_number_id, headers=headers,
327
+ pin=register_pin or _gen_pin(),
328
+ )
329
  except httpx.HTTPError as exc:
330
  return {"ok": False, "error": f"network: {exc}"}
331
+ out = {
332
  "ok": True,
333
  "display_phone_number": data.get("display_phone_number", ""),
334
  "verified_name": data.get("verified_name", ""),
335
+ "registered": reg["registered"],
336
  }
337
+ if not reg["registered"]:
338
+ out["register_error"] = reg.get("error", "")
339
+ return out