Adds storage support for WhatsApp business-scoped user identifiers received from Meta Cloud API and Twilio WhatsApp webhooks. The change keeps existing phone-based behavior intact, stores BSUID and parent BSUID values as additional `contact_inboxes.source_id` rows for the same contact, and allows BSUID-only inbound messages to create contacts, conversations, and messages without requiring a phone number. Related: https://github.com/chatwoot/chatwoot/issues/13837 **What changed** - Extended WhatsApp source ID validation to accept regular BSUID and parent BSUID formats. - For Meta Cloud API, stores phone, `user_id`, and `parent_user_id` identifiers as contact inbox source IDs when they are present. - For Twilio WhatsApp, stores phone, `ExternalUserId`, and `ParentExternalUserId` identifiers as contact inbox source IDs while preserving the existing `whatsapp:` Twilio source ID shape. - Supports BSUID-only inbound messages by creating a contact, contact inbox, conversation, and message even when the phone number is missing. - Links phone-first and later BSUID-only messages to the same contact when the first payload contains both phone and BSUID. - Stores WhatsApp usernames in contact `additional_attributes`, matching existing social channel patterns. - Keeps existing phone-based outbound and new-conversation behavior unchanged for this milestone. **How to test** 1. Send a Meta Cloud webhook payload with both `wa_id` and `user_id`. 2. Verify Chatwoot creates or finds the phone `contact_inbox` and also creates a BSUID `contact_inbox` for the same contact. 3. Send a later Meta Cloud payload for the same user with only `user_id` / `from_user_id`. 4. Verify Chatwoot finds the BSUID `contact_inbox` and creates the inbound message without requiring a phone number. 5. Send a Twilio WhatsApp webhook with `From: whatsapp:+E164`, `ExternalUserId`, and optionally `ParentExternalUserId`. 6. Verify Chatwoot stores the Twilio phone and BSUID identifiers as `whatsapp:`-prefixed source IDs for the same contact. 7. Send a Twilio WhatsApp webhook where `From` is `whatsapp:<BSUID>` and there is no phone number. 8. Verify Chatwoot creates the contact, contact inbox, conversation, and message without a phone number. --------- Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
70 lines
2.1 KiB
Ruby
70 lines
2.1 KiB
Ruby
module Twilio::WhatsappIdentifierHelper
|
|
TWILIO_WHATSAPP_BSUID_SOURCE_ID_REGEX = Regexp.new("\\Awhatsapp:#{RegexHelper::WHATSAPP_BSUID_PATTERN}\\z")
|
|
|
|
def update_twilio_whatsapp_identifiers
|
|
return unless twilio_channel.whatsapp?
|
|
|
|
Whatsapp::IdentifierSyncService.new(contact_inbox: @contact_inbox, contact: @contact).perform(
|
|
source_ids: twilio_whatsapp_source_ids,
|
|
username: params[:ProfileUsername].presence || params[:Username],
|
|
phone_number: phone_number.presence
|
|
)
|
|
end
|
|
|
|
def twilio_whatsapp_phone_source?
|
|
params[:From].to_s.match?(/\Awhatsapp:\+\d{1,15}\z/)
|
|
end
|
|
|
|
def twilio_whatsapp_bsuid
|
|
params[:ExternalUserId].presence || twilio_whatsapp_bsuid_source_id
|
|
end
|
|
|
|
def twilio_whatsapp_display_identifier
|
|
twilio_whatsapp_bsuid.to_s.delete_prefix('whatsapp:').presence
|
|
end
|
|
|
|
def twilio_whatsapp_source_ids
|
|
[
|
|
twilio_whatsapp_phone_source_id,
|
|
twilio_whatsapp_source_id(params[:ExternalUserId].presence) || twilio_whatsapp_bsuid_source_id,
|
|
twilio_whatsapp_source_id(params[:ParentExternalUserId].presence)
|
|
].compact_blank.uniq
|
|
end
|
|
|
|
def twilio_whatsapp_primary_source_id
|
|
twilio_whatsapp_source_ids.first
|
|
end
|
|
|
|
def twilio_whatsapp_phone_source_id
|
|
return if phone_number.blank?
|
|
|
|
Whatsapp::PhoneNumberNormalizationService.new(inbox).normalize_and_find_contact_by_provider("whatsapp:#{phone_number}", :twilio)
|
|
end
|
|
|
|
def twilio_whatsapp_source_id(identifier)
|
|
identifier = identifier.to_s
|
|
return if identifier.blank?
|
|
|
|
"whatsapp:#{identifier.delete_prefix('whatsapp:')}"
|
|
end
|
|
|
|
def twilio_whatsapp_bsuid_source_id
|
|
from = params[:From].to_s
|
|
return from if from.match?(TWILIO_WHATSAPP_BSUID_SOURCE_ID_REGEX)
|
|
end
|
|
|
|
def twilio_contact_inbox(source_id)
|
|
ContactInboxSourceIdResolver.new(
|
|
inbox: inbox,
|
|
source_ids: twilio_contact_inbox_source_ids(source_id),
|
|
contact_attributes: contact_attributes
|
|
).perform
|
|
end
|
|
|
|
def twilio_contact_inbox_source_ids(source_id)
|
|
return [source_id] unless twilio_channel.whatsapp?
|
|
|
|
twilio_whatsapp_source_ids.presence || [source_id]
|
|
end
|
|
end
|