Compare commits
17 Commits
codex/fix-
...
fix/whatsa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3223cb68f4 | ||
|
|
b1a5870083 | ||
|
|
826d79395f | ||
|
|
db1e62492f | ||
|
|
478b5f8e81 | ||
|
|
38c4d0ff66 | ||
|
|
18792f9051 | ||
|
|
9b84b520f0 | ||
|
|
466d01aec4 | ||
|
|
64ec41cc86 | ||
|
|
df1658a4f0 | ||
|
|
cb2d8bd37a | ||
|
|
75eebb877c | ||
|
|
7e0f1cf29e | ||
|
|
513d9de7fb | ||
|
|
22dea1959f | ||
|
|
fe4ba71cdf |
21
app/services/contacts/placeholder_name_updater.rb
Normal file
21
app/services/contacts/placeholder_name_updater.rb
Normal file
@@ -0,0 +1,21 @@
|
||||
# A contact created from a phone-only payload is named after its own number. Replace that
|
||||
# placeholder the first time a provider tells us who the person actually is, and leave a name
|
||||
# a human would recognise alone.
|
||||
class Contacts::PlaceholderNameUpdater
|
||||
pattr_initialize [:contact!, :name!, :phone_numbers!]
|
||||
|
||||
def perform
|
||||
return if name.blank? || contact.name == name
|
||||
return unless placeholder_name?
|
||||
|
||||
contact.update!(name: name)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def placeholder_name?
|
||||
Array(phone_numbers).compact_blank.uniq.any? do |number|
|
||||
contact.name == number || contact.name == TelephoneNumber.parse(number).international_number
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -188,28 +188,12 @@ class Twilio::IncomingMessageService
|
||||
end
|
||||
|
||||
def update_contact_name_if_needed
|
||||
return if params[:ProfileName].blank?
|
||||
return if @contact.name == params[:ProfileName]
|
||||
return if phone_number.blank?
|
||||
|
||||
# Only update if current name exactly matches a phone number candidate
|
||||
return unless contact_name_matches_phone_number?
|
||||
|
||||
@contact.update!(name: params[:ProfileName])
|
||||
end
|
||||
|
||||
def contact_name_matches_phone_number?
|
||||
return false if phone_number.blank?
|
||||
|
||||
phone_name_candidates.include?(@contact.name)
|
||||
end
|
||||
|
||||
def phone_name_candidates
|
||||
[phone_number, formatted_phone_number, *formatted_twilio_whatsapp_phone_number_candidates].compact_blank.uniq
|
||||
end
|
||||
|
||||
def formatted_twilio_whatsapp_phone_number_candidates
|
||||
Array(twilio_whatsapp_phone_number_candidates).flat_map do |candidate|
|
||||
[candidate, TelephoneNumber.parse(candidate).international_number]
|
||||
end
|
||||
Contacts::PlaceholderNameUpdater.new(
|
||||
contact: @contact,
|
||||
name: params[:ProfileName],
|
||||
phone_numbers: [phone_number, *twilio_whatsapp_phone_number_candidates]
|
||||
).perform
|
||||
end
|
||||
end
|
||||
|
||||
@@ -223,24 +223,14 @@ class Whatsapp::IncomingMessageBaseService
|
||||
end
|
||||
|
||||
def update_contact_with_profile_name(contact_params)
|
||||
profile_name = contact_params.dig(:profile, :name)
|
||||
return if profile_name.blank?
|
||||
return if @contact.name == profile_name
|
||||
message_phone_number = whatsapp_phone_number(messages_data.first[:from])
|
||||
return if message_phone_number.blank?
|
||||
|
||||
# Only update if current name exactly matches a phone number candidate
|
||||
return unless contact_name_matches_phone_number?
|
||||
|
||||
@contact.update!(name: profile_name)
|
||||
end
|
||||
|
||||
def contact_name_matches_phone_number?
|
||||
return false if (message_phone_number = whatsapp_phone_number(messages_data.first[:from])).blank?
|
||||
|
||||
phone_number_candidates(message_phone_number).any? do |number|
|
||||
phone_number = "+#{number}"
|
||||
formatted_phone_number = TelephoneNumber.parse(phone_number).international_number
|
||||
@contact.name == phone_number || @contact.name == formatted_phone_number
|
||||
end
|
||||
Contacts::PlaceholderNameUpdater.new(
|
||||
contact: @contact,
|
||||
name: contact_params.dig(:profile, :name),
|
||||
phone_numbers: phone_number_candidates(message_phone_number).map { |number| "+#{number}" }
|
||||
).perform
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -15,7 +15,10 @@ class Whatsapp::InboundCallIdentityBuilder
|
||||
]
|
||||
source_ids = Whatsapp::IdentitySourceIdOrderer.new(inbox: inbox, phone_source_id: phone_source_id(phone), source_ids: identifiers).perform
|
||||
|
||||
{ source_ids: source_ids, contact_attributes: contact_attributes(contact, phone, source_ids.first) }
|
||||
# `profile_name` stays raw: the display name below falls back to the formatted phone, which
|
||||
# would rewrite one phone placeholder into another whenever Meta sends no name.
|
||||
{ source_ids: source_ids, contact_attributes: contact_attributes(contact, phone, source_ids.first),
|
||||
profile_name: contact.dig(:profile, :name).presence }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -99,7 +99,7 @@ class Whatsapp::IncomingCallService
|
||||
extra_meta = { 'sdp_offer' => sdp_offer, 'ice_servers' => Call.default_ice_servers }
|
||||
call = Voice::InboundCallBuilder.perform!(inbox: inbox, call_sid: payload[:id],
|
||||
provider: :whatsapp, extra_meta: extra_meta, caller: identity)
|
||||
sync_caller_identifiers(call, identity)
|
||||
sync_caller_identity(call, identity)
|
||||
tombstone = consume_terminate_tombstone(payload[:id])
|
||||
finalize_terminate(call, tombstone['duration'], tombstone['terminate_reason']) if tombstone
|
||||
call
|
||||
@@ -107,9 +107,15 @@ class Whatsapp::IncomingCallService
|
||||
end
|
||||
|
||||
# Backfill every caller alias (the builder only stores the first) so a later event keyed on any one lands on this thread.
|
||||
def sync_caller_identifiers(call, identity)
|
||||
def sync_caller_identity(call, identity)
|
||||
attributes = identity[:contact_attributes]
|
||||
Whatsapp::IdentifierSyncService.new(contact_inbox: call.conversation.contact_inbox, contact: call.contact)
|
||||
.perform(source_ids: identity[:source_ids], phone_number: identity.dig(:contact_attributes, :phone_number))
|
||||
.perform(source_ids: identity[:source_ids], phone_number: attributes[:phone_number])
|
||||
# A reused contact keeps its old name, so one created phone-only stays named after its number.
|
||||
Contacts::PlaceholderNameUpdater.new(
|
||||
contact: call.contact, name: identity[:profile_name],
|
||||
phone_numbers: [attributes[:phone_number], *attributes[:phone_number_candidates]]
|
||||
).perform
|
||||
end
|
||||
|
||||
# `connect` is the WebRTC tunnel-ready signal, not the pickup signal. Apply
|
||||
|
||||
@@ -462,4 +462,59 @@ describe Whatsapp::IncomingCallService do
|
||||
expect(Call.where(provider_call_id: %w[wacid_a wacid_b]).pluck(:provider_call_id)).to contain_exactly('wacid_a', 'wacid_b')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'caller profile name on an existing contact' do
|
||||
let(:offer) { { session: { sdp_type: 'offer', sdp: 'v=0' } } }
|
||||
|
||||
def ring(from, name: 'John Doe', extra: {})
|
||||
params = { contacts: [{ profile: { name: name }, wa_id: from }.merge(extra)],
|
||||
calls: [{ id: 'wacid_name', from: from, event: 'connect', **offer }] }.with_indifferent_access
|
||||
described_class.new(inbox: inbox, params: params).perform
|
||||
end
|
||||
|
||||
it 'replaces a phone-number placeholder name on the contact the call reused' do
|
||||
contact = create(:contact, name: "+#{from_number}", phone_number: "+#{from_number}", account: account)
|
||||
create(:contact_inbox, contact: contact, inbox: inbox, source_id: from_number)
|
||||
|
||||
ring(from_number)
|
||||
|
||||
expect(contact.reload.name).to eq('John Doe')
|
||||
end
|
||||
|
||||
it 'replaces an internationally formatted placeholder name' do
|
||||
contact = create(:contact, name: TelephoneNumber.parse("+#{from_number}").international_number,
|
||||
phone_number: "+#{from_number}", account: account)
|
||||
create(:contact_inbox, contact: contact, inbox: inbox, source_id: from_number)
|
||||
|
||||
ring(from_number)
|
||||
|
||||
expect(contact.reload.name).to eq('John Doe')
|
||||
end
|
||||
|
||||
it 'replaces a placeholder matching a normalized phone candidate' do
|
||||
contact = create(:contact, name: '+5541988887777', phone_number: '+5541988887777', account: account)
|
||||
|
||||
ring('554188887777', name: 'Maria Silva')
|
||||
|
||||
expect(contact.reload.name).to eq('Maria Silva')
|
||||
end
|
||||
|
||||
it 'leaves a real name untouched' do
|
||||
contact = create(:contact, name: 'Existing Human', phone_number: "+#{from_number}", account: account)
|
||||
create(:contact_inbox, contact: contact, inbox: inbox, source_id: from_number)
|
||||
|
||||
ring(from_number, name: 'Impostor')
|
||||
|
||||
expect(contact.reload.name).to eq('Existing Human')
|
||||
end
|
||||
|
||||
it 'leaves the placeholder alone when Meta sends no profile name' do
|
||||
contact = create(:contact, name: "+#{from_number}", phone_number: "+#{from_number}", account: account)
|
||||
create(:contact_inbox, contact: contact, inbox: inbox, source_id: from_number)
|
||||
|
||||
ring(from_number, name: nil)
|
||||
|
||||
expect(contact.reload.name).to eq("+#{from_number}")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user