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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
55
app/services/contact_inbox/source_id_service.rb
Normal file
55
app/services/contact_inbox/source_id_service.rb
Normal 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
|
||||
Reference in New Issue
Block a user