fix: Do not allow sending messages if merged contact has a duplicate session (#11152)

In this PR https://github.com/chatwoot/chatwoot/pull/11139, if there is
an attempt to create a duplication session for the contact in the same
inbox, we will anonymize the old session.

This PR would prevent sending messages to the older sessions. The
support agents will have to create a new conversation to continue
messages with customer.
This commit is contained in:
Pranav
2025-03-21 18:04:46 -07:00
committed by GitHub
parent 950d9f50a5
commit d355801555
12 changed files with 260 additions and 72 deletions

View File

@@ -12,50 +12,11 @@ class ContactInboxBuilder
private
def generate_source_id
case @inbox.channel_type
when 'Channel::TwilioSms'
twilio_source_id
when 'Channel::Whatsapp'
wa_source_id
when 'Channel::Email'
email_source_id
when 'Channel::Sms'
phone_source_id
when 'Channel::Api', 'Channel::WebWidget'
SecureRandom.uuid
else
raise "Unsupported operation for this channel: #{@inbox.channel_type}"
end
end
def email_source_id
raise ActionController::ParameterMissing, 'contact email' unless @contact.email
@contact.email
end
def phone_source_id
raise ActionController::ParameterMissing, 'contact phone number' unless @contact.phone_number
@contact.phone_number
end
def wa_source_id
raise ActionController::ParameterMissing, 'contact phone number' unless @contact.phone_number
# whatsapp doesn't want the + in e164 format
@contact.phone_number.delete('+').to_s
end
def twilio_source_id
raise ActionController::ParameterMissing, 'contact phone number' unless @contact.phone_number
case @inbox.channel.medium
when 'sms'
@contact.phone_number
when 'whatsapp'
"whatsapp:#{@contact.phone_number}"
end
ContactInbox::SourceIdService.new(
contact: @contact,
channel_type: @inbox.channel_type,
medium: @inbox.channel.try(:medium)
).generate
end
def create_contact_inbox
@@ -91,7 +52,7 @@ class ContactInboxBuilder
def new_source_id
if @inbox.whatsapp? || @inbox.sms? || @inbox.twilio?
"whatsapp:#{@source_id}#{rand(100)}"
"#{@source_id}#{rand(100)}"
else
"#{rand(10)}#{@source_id}"
end

View File

@@ -9,6 +9,8 @@ class Api::V1::Accounts::Contacts::ContactInboxesController < Api::V1::Accounts:
source_id: params[:source_id],
hmac_verified: hmac_verified?
).perform
rescue ArgumentError => e
render json: { error: e.message }, status: :unprocessable_entity
end
private

View File

@@ -25,6 +25,11 @@ export const generateLabelForContactableInboxesList = ({
channelType === INBOX_TYPES.TWILIO ||
channelType === INBOX_TYPES.WHATSAPP
) {
// Handled separately for Twilio Inbox where phone number is not mandatory.
// You can send message to a contact with Messaging Service Id.
if (!phoneNumber) {
return name;
}
return `${name} (${phoneNumber})`;
}
return name;

View File

@@ -8,8 +8,8 @@ vi.mock('dashboard/api/contacts');
describe('composeConversationHelper', () => {
describe('generateLabelForContactableInboxesList', () => {
const contact = {
name: 'John Doe',
email: 'john@example.com',
name: 'Priority Inbox',
email: 'hello@example.com',
phoneNumber: '+1234567890',
};
@@ -19,7 +19,7 @@ describe('composeConversationHelper', () => {
...contact,
channelType: INBOX_TYPES.EMAIL,
})
).toBe('John Doe (john@example.com)');
).toBe('Priority Inbox (hello@example.com)');
});
it('generates label for twilio inbox', () => {
@@ -28,7 +28,14 @@ describe('composeConversationHelper', () => {
...contact,
channelType: INBOX_TYPES.TWILIO,
})
).toBe('John Doe (+1234567890)');
).toBe('Priority Inbox (+1234567890)');
expect(
helpers.generateLabelForContactableInboxesList({
name: 'Priority Inbox',
channelType: INBOX_TYPES.TWILIO,
})
).toBe('Priority Inbox');
});
it('generates label for whatsapp inbox', () => {
@@ -37,7 +44,7 @@ describe('composeConversationHelper', () => {
...contact,
channelType: INBOX_TYPES.WHATSAPP,
})
).toBe('John Doe (+1234567890)');
).toBe('Priority Inbox (+1234567890)');
});
it('generates label for other inbox types', () => {
@@ -46,7 +53,7 @@ describe('composeConversationHelper', () => {
...contact,
channelType: 'Channel::Api',
})
).toBe('John Doe');
).toBe('Priority Inbox');
});
});

View File

@@ -12,8 +12,10 @@ class Base::SendOnChannelService
def perform
validate_target_channel
return unless outgoing_message?
return if invalid_message?
return if invalid_source_id?
perform_reply
end
@@ -49,6 +51,29 @@ class Base::SendOnChannelService
message.private? || outgoing_message_originated_from_channel?
end
def invalid_source_id?
return false unless channels_to_validate?
return false if contact_inbox.source_id == expected_source_id
message.update!(status: :failed, external_error: I18n.t('errors.channel_service.invalid_source_id'))
true
end
def expected_source_id
ContactInbox::SourceIdService.new(
contact: contact,
channel_type: inbox.channel_type,
medium: inbox.channel.try(:medium)
).generate
rescue ArgumentError
nil
end
def channels_to_validate?
inbox.sms? || inbox.whatsapp? || inbox.email? || inbox.twilio?
end
def validate_target_channel
raise 'Invalid channel service was called' if inbox.channel.class != channel_class
end

View File

@@ -0,0 +1,55 @@
class ContactInbox::SourceIdService
pattr_initialize [:contact!, :channel_type!, { medium: nil }]
def generate
case channel_type
when 'Channel::TwilioSms'
twilio_source_id
when 'Channel::Whatsapp'
wa_source_id
when 'Channel::Email'
email_source_id
when 'Channel::Sms'
phone_source_id
when 'Channel::Api', 'Channel::WebWidget'
SecureRandom.uuid
else
raise ArgumentError, "Unsupported operation for this channel: #{channel_type}"
end
end
private
def email_source_id
raise ArgumentError, 'contact email required' unless contact.email
contact.email
end
def phone_source_id
raise ArgumentError, 'contact phone number required' unless contact.phone_number
contact.phone_number
end
def wa_source_id
raise ArgumentError, 'contact phone number required' unless contact.phone_number
# whatsapp doesn't want the + in e164 format
contact.phone_number.delete('+').to_s
end
def twilio_source_id
raise ArgumentError, 'contact phone number required' unless contact.phone_number
raise ArgumentError, 'medium required for Twilio channel' if medium.blank?
case medium
when 'sms'
contact.phone_number
when 'whatsapp'
"whatsapp:#{contact.phone_number}"
else
raise ArgumentError, "Unsupported Twilio medium: #{medium}"
end
end
end