Repository navigation
fix(whatsapp): support business-scoped user ids and inbound context variants (#236) - #255
Conversation
…ariants (#236) Port upstream 3e6e866a (vercel/chat#818, chat@4.39.0) and the type-only context variants of 16879fdc (vercel/chat#723, chat@4.37.0). BSUID / username-only webhooks carry from_user_id and no from; the adapter indexed inbound["from"] and the per-message try/except silently dropped them. Identity now resolves phone > BSUID > parent BSUID, aliases and routes persist in chat state under the TS SDK's keys, user_id_update and system messages re-link identity, and text/interactive/reaction sends emit to and/or recipient from the stored route. Part of #184
|
Warning Review limit reachedNext included review available in 34 minutes. View limit detailsLimit details: You’ve used the included review currently available. You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Review configuration: ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (5)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Match a message's contact by parent_user_id too, and fall back to contacts[0] only when the webhook has a single contact. Documented as a divergence; the remaining review findings (concurrent first contact, out-of-order route writes, undocumented system shapes) are upstream parity limitations recorded in docs/UPSTREAM_SYNC.md.
An unmatched message falls back to the only contact just when it carries no sender identifier of its own, so a single-contact batch can no longer merge two senders' identities or overwrite a route.
Also document phone-number recycling (aliases never expire) as an upstream-parity limitation.
…webhook; pin identity invariants (#236)
|
Merge gate: CI green (test 3.12, test 3.13, Lint & Type Check, CodeQL, Analyze actions/python) on bac10ce (6911245 + clean merge of origin/main ff384c9: #252 github, #193 shared, #223 gchat; no conflicts, no overlap with the WhatsApp modules; local full validation 5892 passed, strict fidelity 730/730 real tests, pyrefly 0 errors, fidelity target delta +0). Local Codex review (gpt-6-astra, xhigh, --base origin/main) on 6911245: "No actionable regressions were found relative to the supplied merge base."; 5 astra rounds (4 fix rounds on #236 findings, then a clean final). CodeRabbit rate-limited, no human reviews. Merging with --admin (Protect Main requires a code-owner approval). |
Summary
WhatsApp can now deliver business-scoped user ID (BSUID) and username-only webhooks. Their messages carry
from_user_id/from_parent_user_idand nofrom. The Python adapter indexedinbound["from"], and the per-messagetry/exceptswallowed theKeyError, so these messages were silently dropped. I confirmed this onmainbefore changing anything: the signed webhook returned 200,process_messagewas called 0 times, and the error log read"Failed to handle inbound message" {'error': "'from'"}.This PR ports upstream identity handling. Existing phone-keyed thread ids are unchanged.
Upstream commits mapped
3e6e866afix(whatsapp): support business-scoped user ids (vercel/chat#818)_fields,_resolve,_link,_handle_user_id_update,_recipient,_identity_key,_author,_BSUID_PATTERN;handle_webhookroutesuser_id_update, matches the contact per message, resolves identity, and skipssystemmessages after linking;_build_messagestoresraw["user_id"];parse_messageuses storeduser_id, then_fields, otherwise raisesValidationError;post_messageresolves the recipient once and passes it through text (every chunk) and interactive sends;add_reaction/remove_reactioncall_recipient16879fdcfix(whatsapp): model forwarded and product-inquiry context variants (vercel/chat#723)WhatsAppInboundContext(total=False:from,id,forwarded,frequently_forwarded,referred_product). Type-only.The types now cover:
from, plusfrom_user_id,from_parent_user_idandsystemuser_idandparent_user_id, optionalwa_id, andprofile.usernameWhatsAppUserIdUpdateandWhatsAppWebhookValue.user_id_updaterecipient_user_idandrecipient_parent_user_idWhatsAppRawMessage.user_idwa_id/user_idon send-response contactsState keys and the route shape match the TS SDK byte for byte:
whatsapp:identity:alias:{phone_number_id}:{id}andwhatsapp:identity:route:{phone_number_id}:{user_id}→{"bsuid"?, "parent"?, "phone"?}, with absent keys omitted rather than written asNone.Tests ported (
tests/test_whatsapp_webhook.py, memory state +AsyncMockGraph request)preserves canonical identity when reparsing raw messages,parses BSUID-only raw messages without stored identityhandles $name webhooks, parametrized asphone and BSUID,username without phone,username with phoneandparent BSUIDsends both known identifiers from a preserved phone thread, which also asserts the exact state keys and route JSONpreserves identity across BSUID rotation system messages,preserves identity across number change system messagesclears a stale phone route after a user_id_update rotation,preserves a BSUID-keyed thread across a user_id_update rotationkeeps the pre-change thread when a number change arrives without prior stateskips redundant identity writes for repeat messages,falls back to the user ID when the profile name is empty,ignores messages without a phone number or BSUIDsends %s threads through recipient without to(US.13491208655302741918,US.ENT.11815799212886844830),sends interactive messages to BSUID recipientsgetstill dispatches the message and sends to the pattern-based recipient, with warnings logged.user_id_updateis swallowed.recipient. Reactions on a phone thread with a stored route send bothtoandrecipient.contextwithoutidparses.parse_messagewith no identifier raises._BSUID_PATTERN: lowercase, trailing newline, 129 characters,US.ENT..sends templates to BSUID recipients([4.41/W2] WhatsApp: send_template and typed API errors #237),sends media to BSUID recipients([4.41/W3] WhatsApp: outbound files/media, CTA URL LinkButton, no duplicate card title #238).Fidelity
index.test.tsfor the WhatsApp adapter is not fidelity-mapped, so the target report is unchanged andscripts/fidelity_target.jsonneeded no update:Validation
Full validation is green: ruff check and format,
audit_test_quality(0 hard failures),--check-docs,--strictat chat@4.31.0 (all TS tests have Python equivalents), and pytest (5680 passed, 24 skipped, after merging current main).pyrefly checkreports 0 errors.Divergences (1 of the 2-per-PR budget)
WhatsApp contact matching. This PR adds a row to the non-parity table, a breadcrumb in
_match_contact, three regression tests and a CHANGELOG sub-bullet.user_id/wa_idand otherwise falls back tocontacts[0].fields()fills a message's missing phone or BSUID from that contact, so in a batched webhook it can combine two senders' identifiers.link()then merges both users into one thread and route.parent_user_id. An unmatched message getscontacts[0]only when the webhook has exactly one contact, the message carries nofrom*identifier of its own, and it is not asystemmessage.test_does_not_borrow_identity_from_an_unrelated_contact,test_does_not_pair_a_single_unmatched_contact_with_another_sender,test_system_message_does_not_take_an_unmatched_contact, plustest_uses_the_only_contact_when_the_message_has_no_sender_idsfor the fallback that remains.Known limitations kept at parity. Codex flagged these during review; upstream behaves the same at chat@4.41.1. They are documented in
docs/UPSTREAM_SYNC.mdrather than fixed, because each needs an identity-model change:sample-messages.md.Codex (gpt-6-astra) review: 4 rounds, and round 4 is a PASS ("No actionable regressions found"). Round 1: the contact-fallback P1 was fixed; four P2s were declined as parity limitations and documented. Round 2: the single-contact fallback P1 was fixed. Round 3: the system-message fallback P1 was fixed; the recycled-number P1 was documented as a parity limitation.
docs/UPSTREAM_SYNC.mdgains a "WhatsApp business-scoped user IDs" section covering:fullmatchfor the BSUID pattern, equivalent to JS/^...$/userId→user_idraw-key casingrecipient()call sites that do not exist here yet ([4.41/W2] WhatsApp: send_template and typed API errors #237/[4.41/W3] WhatsApp: outbound files/media, CTA URL LinkButton, no duplicate card title #238/[4.41/W4] WhatsApp & Messenger: mark_as_read, native replies, code fences, guarded downloads #239)A stored alias or route of the wrong type is read as absent. Upstream reads the missing properties as
undefined, which has the same effect.Consumer impact
whatsapp:identity:alias:*andwhatsapp:identity:route:*keys to the chat state adapter, using the same keys and JSON as the TS SDK. State errors are logged and fall back to un-linked behavior.Author.user_id, and the user segment of the thread id, may now be a BSUID for users who don't share a phone number.Author.user_nameprefersprofile.username.Author.is_meis nowuser_id == bot_user_id(it was hard-codedFalse). This matchesparse_messageand upstream.type: "system"messages are no longer dispatched. Before this change they were dropped anyway, because they had no text content.Merge gate
Final HEAD
6911245(fixes plus a merge of currentorigin/main; the only conflict wasdocs/UPSTREAM_SYNC.md, where both sides added independent sections and both were kept.fidelity_target.jsonwas regenerated with no change).Independent review findings (5): all fixed, none declined
handle_webhookreadmetadata.phone_number_idoutside the per-message try, so"metadata": nullraised out of the webhook (500, and Meta retried the whole batch). This was a regression from main. Fixed: the id is now read defensively. A change with no stringphone_number_idfails each of its messages with the per-message error log, as upstream does, and later changes in the same POST are still dispatched. A non-dict changevalueis skipped. Test:TestBusinessScopedUserIdsMalformedPayloads::test_bad_metadata_fails_per_message_and_later_changes_still_dispatch, parametrized over null, string,{}and a null id, plustest_null_change_value_is_skipped.The same issue was reported by the robustness lens, and it is covered by the same fix and tests.
_handle_user_id_updateraised whenuser_id/parent_user_idwas not a dict. Both are now read as absent, like upstream?.. Whenmetadatais missing, the update is skipped with a warning and noalias::/route::keys are written under an empty business number. Tests:test_user_id_update_ignores_non_dict_nested_identifiersandtest_user_id_update_without_business_number_writes_nothing.The same nested-field crash was reported by the robustness lens, and it is covered by the same fix and tests.
Seven untested behaviors are now pinned in
TestBusinessScopedUserIdsIdentityInvariants:is_meuser_id_updateprevious-id fallbackuser_id_updatestored-parent mergeI re-ran the reviewer's mutation list (8 single-line mutants). All 8 are now killed; before this change all 8 survived.
All malformed-payload tests failed on the previous head and pass now. The hardening is documented in
docs/UPSTREAM_SYNC.md, with a CHANGELOG sub-bullet.gpt-6-astra: 1 round on
6911245. Verdict: PASS ("No actionable regressions were found relative to the supplied merge base"). Earlier rounds on the pre-fix head are summarized above.Bots: CodeRabbit skipped the draft PR, then hit its rate limit after the PR was marked ready. There are no inline or review comments. Gemini posted nothing.
CI: all green on
6911245: Lint & Type Check, test (3.12), test (3.13), CodeQL, and Analyze (python and actions).Local: full validation is green. That covers ruff, format, audit (0 hard failures),
--check-docs,--strictat 4.31.0, 5680 passed, and pyrefly with 0 errors.Closes #236
Part of #184