From 04c874fe35d9959ce60839218194283eef6c5524 Mon Sep 17 00:00:00 2001 From: Muhsin Keloth Date: Wed, 18 Oct 2023 10:11:13 +0530 Subject: [PATCH 01/94] feat: Add delivery reports for API channel (#8116) --- .../public/api/v1/inboxes/conversations_controller.rb | 1 + .../components/widgets/conversation/bubble/Actions.vue | 4 ++-- .../public/api/v1/inbox/conversations_controller_spec.rb | 1 + 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/controllers/public/api/v1/inboxes/conversations_controller.rb b/app/controllers/public/api/v1/inboxes/conversations_controller.rb index c79952372..1e20b2a1d 100644 --- a/app/controllers/public/api/v1/inboxes/conversations_controller.rb +++ b/app/controllers/public/api/v1/inboxes/conversations_controller.rb @@ -23,6 +23,7 @@ class Public::Api::V1::Inboxes::ConversationsController < Public::Api::V1::Inbox def update_last_seen @conversation.contact_last_seen_at = DateTime.now.utc @conversation.save! + ::Conversations::MarkMessagesAsReadJob.perform_later(@conversation) head :ok end diff --git a/app/javascript/dashboard/components/widgets/conversation/bubble/Actions.vue b/app/javascript/dashboard/components/widgets/conversation/bubble/Actions.vue index e03cc74ed..f3c7e4f3c 100644 --- a/app/javascript/dashboard/components/widgets/conversation/bubble/Actions.vue +++ b/app/javascript/dashboard/components/widgets/conversation/bubble/Actions.vue @@ -214,8 +214,8 @@ export default { if (this.isAWhatsAppChannel || this.isATwilioChannel) { return this.sourceId && this.isDelivered; } - // We will consider messages as delivered for web widget inbox if they are sent - if (this.isAWebWidgetInbox) { + // We will consider messages as delivered for web widget inbox and API inbox if they are sent + if (this.isAWebWidgetInbox || this.isAPIInbox) { return this.isSent; } diff --git a/spec/controllers/public/api/v1/inbox/conversations_controller_spec.rb b/spec/controllers/public/api/v1/inbox/conversations_controller_spec.rb index 0bd8cbfd0..3001263bf 100644 --- a/spec/controllers/public/api/v1/inbox/conversations_controller_spec.rb +++ b/spec/controllers/public/api/v1/inbox/conversations_controller_spec.rb @@ -81,6 +81,7 @@ RSpec.describe 'Public Inbox Contact Conversations API', type: :request do it 'updates the last seen of the conversation contact' do contact_last_seen_at = conversation.contact_last_seen_at + expect(Conversations::MarkMessagesAsReadJob).to receive(:perform_later).with(conversation) post update_last_seen_path expect(response).to have_http_status(:success) From 78ce8a4652449b6ca65a8fcda32cf04f35296b8c Mon Sep 17 00:00:00 2001 From: Muhsin Keloth Date: Thu, 19 Oct 2023 12:12:34 +0530 Subject: [PATCH 02/94] feat: Add support for Instagram delivery reports (#8125) --- .../widgets/conversation/bubble/Actions.vue | 6 ++- app/jobs/webhooks/instagram_events_job.rb | 6 ++- app/services/instagram/read_status_service.rb | 28 +++++++++++ .../instagram_message_create_event.rb | 26 ++++++++++ .../webhooks/instagram_events_job_spec.rb | 6 +++ .../instagram/read_status_service_spec.rb | 48 +++++++++++++++++++ 6 files changed, 118 insertions(+), 2 deletions(-) create mode 100644 app/services/instagram/read_status_service.rb create mode 100644 spec/services/instagram/read_status_service_spec.rb diff --git a/app/javascript/dashboard/components/widgets/conversation/bubble/Actions.vue b/app/javascript/dashboard/components/widgets/conversation/bubble/Actions.vue index f3c7e4f3c..27eecdc4b 100644 --- a/app/javascript/dashboard/components/widgets/conversation/bubble/Actions.vue +++ b/app/javascript/dashboard/components/widgets/conversation/bubble/Actions.vue @@ -231,7 +231,11 @@ export default { return contactLastSeenAt >= this.createdAt; } - if (this.isAWhatsAppChannel || this.isATwilioChannel) { + if ( + this.isAWhatsAppChannel || + this.isATwilioChannel || + this.isAFacebookInbox + ) { return this.sourceId && this.isRead; } diff --git a/app/jobs/webhooks/instagram_events_job.rb b/app/jobs/webhooks/instagram_events_job.rb index ad1114036..024ba4a23 100644 --- a/app/jobs/webhooks/instagram_events_job.rb +++ b/app/jobs/webhooks/instagram_events_job.rb @@ -7,7 +7,7 @@ class Webhooks::InstagramEventsJob < MutexApplicationJob base_uri 'https://graph.facebook.com/v11.0/me' # @return [Array] We will support further events like reaction or seen in future - SUPPORTED_EVENTS = [:message].freeze + SUPPORTED_EVENTS = [:message, :read].freeze def perform(entries) @entries = entries @@ -45,6 +45,10 @@ class Webhooks::InstagramEventsJob < MutexApplicationJob ::Instagram::MessageText.new(messaging).perform end + def read(messaging) + ::Instagram::ReadStatusService.new(params: messaging).perform + end + def messages(entry) (entry[:messaging].presence || entry[:standby] || []) end diff --git a/app/services/instagram/read_status_service.rb b/app/services/instagram/read_status_service.rb new file mode 100644 index 000000000..80f67d680 --- /dev/null +++ b/app/services/instagram/read_status_service.rb @@ -0,0 +1,28 @@ +class Instagram::ReadStatusService + pattr_initialize [:params!] + + def perform + return if instagram_channel.blank? + + process_status if message.present? + end + + def process_status + @message.status = 'read' + @message.save! + end + + def instagram_id + params[:recipient][:id] + end + + def instagram_channel + @instagram_channel ||= Channel::FacebookPage.find_by(instagram_id: instagram_id) + end + + def message + return unless params[:read][:mid] + + @message ||= @instagram_channel.inbox.messages.find_by(source_id: params[:read][:mid]) + end +end diff --git a/spec/factories/instagram/instagram_message_create_event.rb b/spec/factories/instagram/instagram_message_create_event.rb index 6b66c29a7..4cc8b7f67 100644 --- a/spec/factories/instagram/instagram_message_create_event.rb +++ b/spec/factories/instagram/instagram_message_create_event.rb @@ -242,4 +242,30 @@ FactoryBot.define do end initialize_with { attributes } end + + factory :messaging_seen_event, class: Hash do + entry do + [ + { + 'id': 'instagram-message-id-123', + 'time': '2021-09-08T06:34:04+0000', + 'messaging': [ + { + 'sender': { + 'id': 'Sender-id-1' + }, + 'recipient': { + 'id': 'chatwoot-app-user-id-1' + }, + 'timestamp': '2021-09-08T06:34:04+0000', + 'read': { + 'mid': 'message-id-1' + } + } + ] + } + ] + end + initialize_with { attributes } + end end diff --git a/spec/jobs/webhooks/instagram_events_job_spec.rb b/spec/jobs/webhooks/instagram_events_job_spec.rb index b5e73e2f5..905810512 100644 --- a/spec/jobs/webhooks/instagram_events_job_spec.rb +++ b/spec/jobs/webhooks/instagram_events_job_spec.rb @@ -27,6 +27,7 @@ describe Webhooks::InstagramEventsJob do let!(:attachment_params) { build(:instagram_message_attachment_event).with_indifferent_access } let!(:story_mention_params) { build(:instagram_story_mention_event).with_indifferent_access } let!(:story_mention_echo_params) { build(:instagram_story_mention_event_with_echo).with_indifferent_access } + let!(:messaging_seen_event) { build(:messaging_seen_event).with_indifferent_access } let(:fb_object) { double } describe '#perform' do @@ -151,6 +152,11 @@ describe Webhooks::InstagramEventsJob do expect(instagram_inbox.contact_inboxes.count).to be 0 expect(instagram_inbox.messages.count).to be 0 end + + it 'handle messaging_seen callback' do + expect(Instagram::ReadStatusService).to receive(:new).with(params: messaging_seen_event[:entry][0][:messaging][0]).and_call_original + instagram_webhook.perform_now(messaging_seen_event[:entry]) + end end end end diff --git a/spec/services/instagram/read_status_service_spec.rb b/spec/services/instagram/read_status_service_spec.rb new file mode 100644 index 000000000..db5dc73b3 --- /dev/null +++ b/spec/services/instagram/read_status_service_spec.rb @@ -0,0 +1,48 @@ +require 'rails_helper' + +describe Instagram::ReadStatusService do + before do + create(:message, message_type: :incoming, inbox: instagram_inbox, account: account, conversation: conversation, + source_id: 'chatwoot-app-user-id-1') + end + + let!(:account) { create(:account) } + let!(:instagram_channel) { create(:channel_instagram_fb_page, account: account, instagram_id: 'chatwoot-app-user-id-1') } + let!(:instagram_inbox) { create(:inbox, channel: instagram_channel, account: account, greeting_enabled: false) } + let!(:contact) { create(:contact, account: account) } + let(:contact_inbox) { create(:contact_inbox, contact: contact, inbox: instagram_inbox) } + let(:conversation) { create(:conversation, contact: contact, inbox: instagram_inbox, contact_inbox: contact_inbox) } + + describe '#perform' do + context 'when messaging_seen callback is fired' do + let(:message) { conversation.messages.last } + + it 'updates the message status to read if the status is delivered' do + params = { + recipient: { + id: 'chatwoot-app-user-id-1' + }, + read: { + mid: message.source_id + } + } + described_class.new(params: params).perform + expect(conversation.reload.messages.last.status).to eq('read') + end + + it 'does not update the status if message is not found' do + params = { + recipient: { + id: 'chatwoot-app-user-id-1' + }, + read: { + mid: 'random-message-id' + } + } + + described_class.new(params: params).perform + expect(conversation.reload.messages.last.status).not_to eq('read') + end + end + end +end From b94c89ebf1be7ae4a97700dc591915d958d060f8 Mon Sep 17 00:00:00 2001 From: Muhsin Keloth Date: Thu, 19 Oct 2023 13:22:19 +0530 Subject: [PATCH 03/94] fix: Show sent status for messenger channel (#8145) --- .../components/widgets/conversation/bubble/Actions.vue | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/javascript/dashboard/components/widgets/conversation/bubble/Actions.vue b/app/javascript/dashboard/components/widgets/conversation/bubble/Actions.vue index 27eecdc4b..2e58bce10 100644 --- a/app/javascript/dashboard/components/widgets/conversation/bubble/Actions.vue +++ b/app/javascript/dashboard/components/widgets/conversation/bubble/Actions.vue @@ -201,7 +201,11 @@ export default { return !!this.sourceId; } - if (this.isAWhatsAppChannel || this.isATwilioChannel) { + if ( + this.isAWhatsAppChannel || + this.isATwilioChannel || + this.isAFacebookInbox + ) { return this.sourceId && this.isSent; } return false; From 7416bbb25eb74bae537888e590321a6507445771 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Fri, 20 Oct 2023 01:54:46 +0530 Subject: [PATCH 04/94] feat: support reply to for outgoing message in WhatsApp (#8107) - This PR enables replies to WhatsApp. --- .../widgets/conversation/ReplyBox.vue | 31 +++++++++++++------ .../widgets/conversation/bubble/ReplyTo.vue | 3 +- app/models/message.rb | 12 +++---- .../messages/in_reply_to_message_builder.rb | 24 ++++++++++++++ .../whatsapp/incoming_message_base_service.rb | 3 +- .../incoming_message_service_helpers.rb | 10 +----- .../providers/whatsapp_cloud_service.rb | 11 +++++++ .../providers/whatsapp_cloud_service_spec.rb | 30 +++++++++++++++++- 8 files changed, 95 insertions(+), 29 deletions(-) create mode 100644 app/services/messages/in_reply_to_message_builder.rb diff --git a/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue b/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue index 5ad5f66a4..23f339f46 100644 --- a/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue +++ b/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue @@ -964,6 +964,19 @@ export default { (item, index) => itemIndex !== index ); }, + setReplyToInPayload(payload) { + if (this.inReplyTo?.id) { + return { + ...payload, + contentAttributes: { + ...payload.contentAttributes, + in_reply_to: this.inReplyTo.id, + }, + }; + } + + return payload; + }, getMessagePayloadForWhatsapp(message) { const multipleMessagePayload = []; @@ -973,41 +986,41 @@ export default { const attachedFile = this.globalConfig.directUploadsEnabled ? attachment.blobSignedId : attachment.resource.file; - const attachmentPayload = { + let attachmentPayload = { conversationId: this.currentChat.id, files: [attachedFile], private: false, message: caption, sender: this.sender, }; + + attachmentPayload = this.setReplyToInPayload(attachmentPayload); multipleMessagePayload.push(attachmentPayload); caption = ''; }); } else { - const messagePayload = { + let messagePayload = { conversationId: this.currentChat.id, message, private: false, sender: this.sender, }; + + messagePayload = this.setReplyToInPayload(messagePayload); + multipleMessagePayload.push(messagePayload); } return multipleMessagePayload; }, getMessagePayload(message) { - const messagePayload = { + let messagePayload = { conversationId: this.currentChat.id, message, private: this.isPrivate, sender: this.sender, }; - - if (this.inReplyTo?.id) { - messagePayload.contentAttributes = { - in_reply_to: this.inReplyTo.id, - }; - } + messagePayload = this.setReplyToInPayload(messagePayload); if (this.attachedFiles && this.attachedFiles.length) { messagePayload.files = []; diff --git a/app/javascript/dashboard/components/widgets/conversation/bubble/ReplyTo.vue b/app/javascript/dashboard/components/widgets/conversation/bubble/ReplyTo.vue index 06b6f0445..fb557293b 100644 --- a/app/javascript/dashboard/components/widgets/conversation/bubble/ReplyTo.vue +++ b/app/javascript/dashboard/components/widgets/conversation/bubble/ReplyTo.vue @@ -1,10 +1,11 @@ + + + + diff --git a/app/javascript/shared/components/FluentIcon/dashboard-icons.json b/app/javascript/shared/components/FluentIcon/dashboard-icons.json index 07d49eb48..5221a5d0e 100644 --- a/app/javascript/shared/components/FluentIcon/dashboard-icons.json +++ b/app/javascript/shared/components/FluentIcon/dashboard-icons.json @@ -92,6 +92,7 @@ "credit-card-person-outline": "M2 7.25A3.25 3.25 0 0 1 5.25 4h13.5A3.25 3.25 0 0 1 22 7.25V10h-.258A3.74 3.74 0 0 0 20.5 7.455V7.25a1.75 1.75 0 0 0-1.75-1.75H5.25A1.75 1.75 0 0 0 3.5 7.25v.25h11.95c-.44.409-.782.922-.987 1.5H3.5v5.75c0 .966.784 1.75 1.75 1.75h6.78c.06.522.217 1.028.458 1.5H5.25A3.25 3.25 0 0 1 2 14.75v-7.5Zm21 8.25a1.5 1.5 0 0 0-1.5-1.5h-7a1.5 1.5 0 0 0-1.5 1.5v.5c0 1.971 1.86 4 5 4 3.14 0 5-2.029 5-4v-.5Zm-2.25-5.25a2.75 2.75 0 1 0-5.5 0 2.75 2.75 0 0 0 5.5 0Z", "delete-outline": "M12 1.75a3.25 3.25 0 0 1 3.245 3.066L15.25 5h5.25a.75.75 0 0 1 .102 1.493L20.5 6.5h-.796l-1.28 13.02a2.75 2.75 0 0 1-2.561 2.474l-.176.006H8.313a2.75 2.75 0 0 1-2.714-2.307l-.023-.174L4.295 6.5H3.5a.75.75 0 0 1-.743-.648L2.75 5.75a.75.75 0 0 1 .648-.743L3.5 5h5.25A3.25 3.25 0 0 1 12 1.75Zm6.197 4.75H5.802l1.267 12.872a1.25 1.25 0 0 0 1.117 1.122l.127.006h7.374c.6 0 1.109-.425 1.225-1.002l.02-.126L18.196 6.5ZM13.75 9.25a.75.75 0 0 1 .743.648L14.5 10v7a.75.75 0 0 1-1.493.102L13 17v-7a.75.75 0 0 1 .75-.75Zm-3.5 0a.75.75 0 0 1 .743.648L11 10v7a.75.75 0 0 1-1.493.102L9.5 17v-7a.75.75 0 0 1 .75-.75Zm1.75-6a1.75 1.75 0 0 0-1.744 1.606L10.25 5h3.5A1.75 1.75 0 0 0 12 3.25Z", "dismiss-circle-outline": "M12 2c5.523 0 10 4.477 10 10s-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2Zm0 1.5a8.5 8.5 0 1 0 0 17 8.5 8.5 0 0 0 0-17Zm3.446 4.897.084.073a.75.75 0 0 1 .073.976l-.073.084L13.061 12l2.47 2.47a.75.75 0 0 1 .072.976l-.073.084a.75.75 0 0 1-.976.073l-.084-.073L12 13.061l-2.47 2.47a.75.75 0 0 1-.976.072l-.084-.073a.75.75 0 0 1-.073-.976l.073-.084L10.939 12l-2.47-2.47a.75.75 0 0 1-.072-.976l.073-.084a.75.75 0 0 1 .976-.073l.084.073L12 10.939l2.47-2.47a.75.75 0 0 1 .976-.072Z", + "dismiss-circle-filled": "M12 2c5.523 0 10 4.477 10 10s-4.477 10-10 10S2 17.523 2 12S6.477 2 12 2Zm3.53 6.47l-.084-.073a.75.75 0 0 0-.882-.007l-.094.08L12 10.939l-2.47-2.47l-.084-.072a.75.75 0 0 0-.882-.007l-.094.08l-.073.084a.75.75 0 0 0-.007.882l.08.094L10.939 12l-2.47 2.47l-.072.084a.75.75 0 0 0-.007.882l.08.094l.084.073a.75.75 0 0 0 .882.007l.094-.08L12 13.061l2.47 2.47l.084.072a.75.75 0 0 0 .882.007l.094-.08l.073-.084a.75.75 0 0 0 .007-.882l-.08-.094L13.061 12l2.47-2.47l.072-.084a.75.75 0 0 0 .007-.882l-.08-.094l-.084-.073l.084.073Z", "dismiss-outline": "m4.397 4.554.073-.084a.75.75 0 0 1 .976-.073l.084.073L12 10.939l6.47-6.47a.75.75 0 1 1 1.06 1.061L13.061 12l6.47 6.47a.75.75 0 0 1 .072.976l-.073.084a.75.75 0 0 1-.976.073l-.084-.073L12 13.061l-6.47 6.47a.75.75 0 0 1-1.06-1.061L10.939 12l-6.47-6.47a.75.75 0 0 1-.072-.976l.073-.084-.073.084Z", "document-outline": "M18.5 20a.5.5 0 0 1-.5.5H6a.5.5 0 0 1-.5-.5V4a.5.5 0 0 1 .5-.5h6V8a2 2 0 0 0 2 2h4.5v10Zm-5-15.379L17.378 8.5H14a.5.5 0 0 1-.5-.5V4.621Zm5.914 3.793-5.829-5.828c-.026-.026-.058-.046-.085-.07a2.072 2.072 0 0 0-.219-.18c-.04-.027-.086-.045-.128-.068-.071-.04-.141-.084-.216-.116a1.977 1.977 0 0 0-.624-.138C12.266 2.011 12.22 2 12.172 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9.828a2 2 0 0 0-.586-1.414Z", "document-error-outline": "M6 2a2 2 0 0 0-2 2v5.207a5.48 5.48 0 0 1 1-.185V4a1 1 0 0 1 1-1h4v3.5A1.5 1.5 0 0 0 11.5 8H15v8a1 1 0 0 1-1 1h-3.6a5.507 5.507 0 0 1-.657 1H14a2 2 0 0 0 2-2V7.414a1.5 1.5 0 0 0-.44-1.06l-3.914-3.915A1.5 1.5 0 0 0 10.586 2H6Zm8.793 5H11.5a.5.5 0 0 1-.5-.5V3.207L14.793 7ZM10 14.5a4.5 4.5 0 1 1-9 0a4.5 4.5 0 0 1 9 0ZM5.5 12a.5.5 0 0 0-.5.5v2a.5.5 0 0 0 1 0v-2a.5.5 0 0 0-.5-.5Zm0 5.125a.625.625 0 1 0 0-1.25a.625.625 0 0 0 0 1.25Z", @@ -100,6 +101,7 @@ "dual-screen-clock-outline": "M10.019 6.002a6.632 6.632 0 0 0 .058 1.5H3.75a.25.25 0 0 0-.25.25v12.494c0 .138.112.25.25.25h7.498l-.001-10.167c.416.57.924 1.07 1.5 1.479v8.69h7.498a.25.25 0 0 0 .25-.25v-8.62a6.535 6.535 0 0 0 1.501-1.656V20.25a1.75 1.75 0 0 1-1.75 1.75h-8.998l-.001-.003H3.75A1.75 1.75 0 0 1 2 20.246V7.751c0-.966.784-1.75 1.75-1.75h6.269Zm6.22 11.498a.75.75 0 0 1 .101 1.493L16.24 19h-1.5a.75.75 0 0 1-.102-1.493l.102-.007h1.5Zm-6.996 0a.75.75 0 0 1 .102 1.493L9.243 19H7.74a.75.75 0 0 1-.102-1.493l.102-.007h1.502ZM16.498 1a5.5 5.5 0 1 1 0 11 5.5 5.5 0 0 1 0-11Zm-1 2a.5.5 0 0 0-.5.5v4a.5.5 0 0 0 .5.5h3.001a.5.5 0 0 0 0-1h-2.501V3.5a.5.5 0 0 0-.5-.5Z", "edit-outline": "M21.03 2.97a3.578 3.578 0 0 1 0 5.06L9.062 20a2.25 2.25 0 0 1-.999.58l-5.116 1.395a.75.75 0 0 1-.92-.921l1.395-5.116a2.25 2.25 0 0 1 .58-.999L15.97 2.97a3.578 3.578 0 0 1 5.06 0ZM15 6.06 5.062 16a.75.75 0 0 0-.193.333l-1.05 3.85 3.85-1.05A.75.75 0 0 0 8 18.938L17.94 9 15 6.06Zm2.03-2.03-.97.97L19 7.94l.97-.97a2.079 2.079 0 0 0-2.94-2.94Z", "emoji-outline": "M12 1.999c5.524 0 10.002 4.478 10.002 10.002 0 5.523-4.478 10.001-10.002 10.001-5.524 0-10.002-4.478-10.002-10.001C1.998 6.477 6.476 1.999 12 1.999Zm0 1.5a8.502 8.502 0 1 0 0 17.003A8.502 8.502 0 0 0 12 3.5ZM8.462 14.784A4.491 4.491 0 0 0 12 16.502a4.492 4.492 0 0 0 3.535-1.714.75.75 0 1 1 1.177.93A5.991 5.991 0 0 1 12 18.002a5.991 5.991 0 0 1-4.716-2.29.75.75 0 0 1 1.178-.928ZM9 8.75a1.25 1.25 0 1 1 0 2.499A1.25 1.25 0 0 1 9 8.75Zm6 0a1.25 1.25 0 1 1 0 2.499 1.25 1.25 0 0 1 0-2.499Z", + "emoji-add-outline": "M17.5 12a5.5 5.5 0 1 1 0 10.999a5.5 5.5 0 0 1 0-11ZM12 1.997c5.524 0 10.002 4.478 10.002 10.002c0 .263-.01.524-.03.782a6.503 6.503 0 0 0-1.475-1.052a8.501 8.501 0 1 0-8.765 8.767c.28.54.635 1.036 1.05 1.474c-.257.02-.518.03-.782.03c-5.524 0-10.002-4.478-10.002-10.001S6.476 1.998 12 1.998Zm5.5 12l-.09.008a.5.5 0 0 0-.402.402l-.008.09v2.5L14.498 17l-.09.008a.5.5 0 0 0-.402.402l-.008.09l.008.09a.5.5 0 0 0 .402.402l.09.008H17v2.504l.008.09a.5.5 0 0 0 .402.401l.09.009l.09-.009a.5.5 0 0 0 .402-.402l.008-.09v-2.504l2.504.001l.09-.008a.5.5 0 0 0 .402-.402l.008-.09l-.008-.09a.5.5 0 0 0-.402-.402l-.09-.008H18v-2.5l-.008-.09a.5.5 0 0 0-.402-.403l-.09-.008Zm-9.038.785a4.494 4.494 0 0 0 2.63 1.626a6.449 6.449 0 0 0-.079 1.51a5.982 5.982 0 0 1-3.73-2.208a.75.75 0 1 1 1.179-.928ZM9 8.75a1.25 1.25 0 1 1 0 2.499A1.25 1.25 0 0 1 9 8.75Zm6 0a1.25 1.25 0 1 1 0 2.499a1.25 1.25 0 0 1 0-2.499Z", "error-circle-outline": "M12 2c5.523 0 10 4.478 10 10s-4.477 10-10 10S2 17.522 2 12 6.477 2 12 2Zm0 1.667c-4.595 0-8.333 3.738-8.333 8.333 0 4.595 3.738 8.333 8.333 8.333 4.595 0 8.333-3.738 8.333-8.333 0-4.595-3.738-8.333-8.333-8.333Zm-.001 10.835a.999.999 0 1 1 0 1.998.999.999 0 0 1 0-1.998ZM11.994 7a.75.75 0 0 1 .744.648l.007.101.004 4.502a.75.75 0 0 1-1.493.103l-.007-.102-.004-4.501a.75.75 0 0 1 .75-.751Z", "file-upload-outline": "M6 2a2 2 0 0 0-2 2v5.207a5.48 5.48 0 0 1 1-.185V4a1 1 0 0 1 1-1h4v3.5A1.5 1.5 0 0 0 11.5 8H15v8a1 1 0 0 1-1 1h-3.6a5.507 5.507 0 0 1-.657 1H14a2 2 0 0 0 2-2V7.414a1.5 1.5 0 0 0-.44-1.06l-3.914-3.915A1.5 1.5 0 0 0 10.586 2H6Zm8.793 5H11.5a.5.5 0 0 1-.5-.5V3.207L14.793 7ZM5.5 19a4.5 4.5 0 1 0 0-9a4.5 4.5 0 0 0 0 9Zm2.354-4.854a.5.5 0 1 1-.708.708L6 13.707V16.5a.5.5 0 0 1-1 0v-2.793l-1.146 1.147a.5.5 0 1 1-.708-.707l2-2A.5.5 0 0 1 5.497 12h.006a.498.498 0 0 1 .348.144l.003.003l2 2Z", "filter-outline": "M13.5 16a.75.75 0 0 1 0 1.5h-3a.75.75 0 0 1 0-1.5h3Zm3-5a.75.75 0 0 1 0 1.5h-9a.75.75 0 0 1 0-1.5h9Zm3-5a.75.75 0 0 1 0 1.5h-15a.75.75 0 0 1 0-1.5h15Z", diff --git a/app/javascript/shared/components/emoji/EmojiInput.vue b/app/javascript/shared/components/emoji/EmojiInput.vue index 2c2e440aa..b225d1035 100644 --- a/app/javascript/shared/components/emoji/EmojiInput.vue +++ b/app/javascript/shared/components/emoji/EmojiInput.vue @@ -4,14 +4,24 @@ class="emoji-dialog bg-white shadow-lg dark:bg-slate-900 rounded-md border border-solid border-slate-75 dark:border-slate-800/50 box-content h-[300px] absolute right-0 -top-[95px] w-80 z-20" >
-
+
+ + {{ $t('EMOJI.REMOVE') }} +
@@ -99,6 +109,10 @@ export default { type: Function, default: () => {}, }, + showRemoveButton: { + type: Boolean, + default: false, + }, }, data() { return { @@ -171,13 +185,13 @@ export default { .box-shadow-blue { box-shadow: 0 0 0 1px #1f93ff, - 0 0 2px 3px #c7e3ff; + 0 0 1px 2px #c7e3ff; } - .box-shadow-blue-dark { + .box-shadow-dark { box-shadow: - 0 0 0 1px #1f93ff, - 0 0 2px 3px #4c5155; + 0 0 0 1px #212222, + 0 0 1px 2px #4c5155; } } @@ -217,7 +231,7 @@ export default { @apply m-2 sticky top-2; .emoji-search--input { - @apply text-sm focus-visible:border-transparent text-slate-800 dark:text-slate-100 h-8 m-0 p-2 w-full rounded-md bg-slate-75 dark:bg-slate-800 border border-solid border-transparent dark:border-slate-800/50; + @apply text-sm focus-visible:border-transparent text-slate-800 dark:text-slate-100 h-8 m-0 p-2 w-full rounded-md bg-slate-50 dark:bg-slate-800 border border-solid border-transparent dark:border-slate-800/50; } } diff --git a/app/models/category.rb b/app/models/category.rb index d0db67b94..a020533a9 100644 --- a/app/models/category.rb +++ b/app/models/category.rb @@ -4,6 +4,7 @@ # # id :bigint not null, primary key # description :text +# icon :string default("") # locale :string default("en") # name :string # position :integer diff --git a/app/views/api/v1/accounts/categories/_category.json.jbuilder b/app/views/api/v1/accounts/categories/_category.json.jbuilder index 825b9c2d7..c3674b7fa 100644 --- a/app/views/api/v1/accounts/categories/_category.json.jbuilder +++ b/app/views/api/v1/accounts/categories/_category.json.jbuilder @@ -5,6 +5,7 @@ json.locale category.locale json.description category.description json.position category.position json.account_id category.account_id +json.icon category.icon json.related_categories do if category.related_categories.any? diff --git a/db/migrate/20231013072802_add_icon_to_categories.rb b/db/migrate/20231013072802_add_icon_to_categories.rb new file mode 100644 index 000000000..195d405e7 --- /dev/null +++ b/db/migrate/20231013072802_add_icon_to_categories.rb @@ -0,0 +1,5 @@ +class AddIconToCategories < ActiveRecord::Migration[7.0] + def change + add_column :categories, :icon, :string, default: '' + end +end diff --git a/db/schema.rb b/db/schema.rb index 15a0b6abd..ce15abf8f 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.0].define(version: 2023_10_11_041615) do +ActiveRecord::Schema[7.0].define(version: 2023_10_13_072802) do # These are extensions that must be enabled in order to support this database enable_extension "pg_stat_statements" enable_extension "pg_trgm" @@ -231,6 +231,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_10_11_041615) do t.string "slug", null: false t.bigint "parent_category_id" t.bigint "associated_category_id" + t.string "icon", default: "" t.index ["associated_category_id"], name: "index_categories_on_associated_category_id" t.index ["locale", "account_id"], name: "index_categories_on_locale_and_account_id" t.index ["locale"], name: "index_categories_on_locale" From 6d9b2a6b3376c927aa1fd1c3f39d80896f2b94ef Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Fri, 20 Oct 2023 14:19:19 +0530 Subject: [PATCH 07/94] refactor: UI flags for Reply To (#8147) --- .../components/widgets/conversation/MessagesView.vue | 2 +- app/javascript/shared/mixins/inboxMixin.js | 10 +++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/app/javascript/dashboard/components/widgets/conversation/MessagesView.vue b/app/javascript/dashboard/components/widgets/conversation/MessagesView.vue index 5370b27f6..10d28d108 100644 --- a/app/javascript/dashboard/components/widgets/conversation/MessagesView.vue +++ b/app/javascript/dashboard/components/widgets/conversation/MessagesView.vue @@ -292,7 +292,7 @@ export default { }, inboxSupportsReplyTo() { return ( - this.inboxHasFeature(INBOX_FEATURES.REPLY_TO) && + this.inboxHasFeature(INBOX_FEATURES.REPLY_TO_OUTGOING) && this.isFeatureEnabledonAccount( this.accountId, FEATURE_FLAGS.MESSAGE_REPLY_TO diff --git a/app/javascript/shared/mixins/inboxMixin.js b/app/javascript/shared/mixins/inboxMixin.js index 35819d7ae..8266a5d37 100644 --- a/app/javascript/shared/mixins/inboxMixin.js +++ b/app/javascript/shared/mixins/inboxMixin.js @@ -13,16 +13,24 @@ export const INBOX_TYPES = { export const INBOX_FEATURES = { REPLY_TO: 'replyTo', + REPLY_TO_OUTGOING: 'replyToOutgoing', }; // This is a single source of truth for inbox features // This is used to check if a feature is available for a particular inbox or not export const INBOX_FEATURE_MAP = { [INBOX_FEATURES.REPLY_TO]: [ + INBOX_TYPES.FB, + INBOX_TYPES.WEB, + INBOX_TYPES.TWITTER, + INBOX_TYPES.WHATSAPP, + INBOX_TYPES.TELEGRAM, + INBOX_TYPES.API, + ], + [INBOX_FEATURES.REPLY_TO_OUTGOING]: [ INBOX_TYPES.WEB, INBOX_TYPES.TWITTER, INBOX_TYPES.WHATSAPP, - INBOX_TYPES.LINE, INBOX_TYPES.TELEGRAM, INBOX_TYPES.API, ], From 2e54f4a67cc491604339d53bfe4eeb127b84d9b9 Mon Sep 17 00:00:00 2001 From: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Date: Tue, 24 Oct 2023 15:34:51 +0530 Subject: [PATCH 08/94] feat: Adds an event for the Google Translate usage (#8175) --- app/javascript/dashboard/helper/AnalyticsHelper/events.js | 1 + .../modules/conversations/components/MessageContextMenu.vue | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/app/javascript/dashboard/helper/AnalyticsHelper/events.js b/app/javascript/dashboard/helper/AnalyticsHelper/events.js index 8ece9f648..53e0a82ca 100644 --- a/app/javascript/dashboard/helper/AnalyticsHelper/events.js +++ b/app/javascript/dashboard/helper/AnalyticsHelper/events.js @@ -3,6 +3,7 @@ export const CONVERSATION_EVENTS = Object.freeze({ SENT_MESSAGE: 'Sent a message', SENT_PRIVATE_NOTE: 'Sent a private note', INSERTED_A_CANNED_RESPONSE: 'Inserted a canned response', + TRANSLATE_A_MESSAGE: 'Translated a message', INSERTED_A_VARIABLE: 'Inserted a variable', USED_MENTIONS: 'Used mentions', SEARCH_CONVERSATION: 'Searched conversations', diff --git a/app/javascript/dashboard/modules/conversations/components/MessageContextMenu.vue b/app/javascript/dashboard/modules/conversations/components/MessageContextMenu.vue index 51e5e6a59..9adb9b2eb 100644 --- a/app/javascript/dashboard/modules/conversations/components/MessageContextMenu.vue +++ b/app/javascript/dashboard/modules/conversations/components/MessageContextMenu.vue @@ -111,7 +111,10 @@ import messageFormatterMixin from 'shared/mixins/messageFormatterMixin'; import AddCannedModal from 'dashboard/routes/dashboard/settings/canned/AddCanned.vue'; import { copyTextToClipboard } from 'shared/helpers/clipboard'; import { conversationUrl, frontendURL } from '../../../helper/URLHelper'; -import { ACCOUNT_EVENTS } from '../../../helper/AnalyticsHelper/events'; +import { + ACCOUNT_EVENTS, + CONVERSATION_EVENTS, +} from '../../../helper/AnalyticsHelper/events'; import TranslateModal from 'dashboard/components/widgets/conversation/bubble/TranslateModal.vue'; import MenuItem from '../../../components/widgets/conversation/contextMenu/menuItem.vue'; @@ -210,6 +213,7 @@ export default { messageId: this.messageId, targetLanguage: locale || 'en', }); + this.$track(CONVERSATION_EVENTS.TRANSLATE_A_MESSAGE); this.handleClose(); this.showTranslateModal = true; }, From 9551dbc25fe4ff292fd2ba75f324ae1ca6e209b4 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Tue, 24 Oct 2023 15:51:21 +0530 Subject: [PATCH 09/94] feat: add gtm to vueapp (#8154) --- .env.example | 3 +++ app/views/layouts/vueapp.html.erb | 9 +++++++++ 2 files changed, 12 insertions(+) diff --git a/.env.example b/.env.example index a97679f7e..f85607fb9 100644 --- a/.env.example +++ b/.env.example @@ -176,6 +176,9 @@ ANDROID_SHA256_CERT_FINGERPRINT=AC:73:8E:DE:EB:56:EA:CC:10:87:02:A7:65:37:7B:38: # MICROSOFT CLARITY # MS_CLARITY_TOKEN=xxxxxxxxx +# GOOGLE_TAG_MANAGER +# GOOGLE_TAG = GTM-XXXXXXX + ## Scout ## https://scoutapm.com/docs/ruby/configuration # SCOUT_KEY=YOURKEY diff --git a/app/views/layouts/vueapp.html.erb b/app/views/layouts/vueapp.html.erb index c7d1120f6..d81387687 100644 --- a/app/views/layouts/vueapp.html.erb +++ b/app/views/layouts/vueapp.html.erb @@ -93,5 +93,14 @@ })(window, document, 'clarity', 'script', '<%= ENV.fetch('MS_CLARITY_TOKEN', '') %>'); <% end %> + <% if ENV.fetch('GOOGLE_TAG', nil).present? %> + + <% end %> From 7438f3b157da834351a756744d6e86ab5f8e5d22 Mon Sep 17 00:00:00 2001 From: Muhsin Keloth Date: Wed, 25 Oct 2023 13:53:30 +0530 Subject: [PATCH 10/94] fix: Rescue slack `ChannelNotFound` error (#8196) --- lib/integrations/slack/send_on_slack_service.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/integrations/slack/send_on_slack_service.rb b/lib/integrations/slack/send_on_slack_service.rb index 7892ea92e..fc7ffe5ab 100644 --- a/lib/integrations/slack/send_on_slack_service.rb +++ b/lib/integrations/slack/send_on_slack_service.rb @@ -92,7 +92,8 @@ class Integrations::Slack::SendOnSlackService < Base::SendOnChannelService def send_message post_message if message_content.present? upload_file if message.attachments.any? - rescue Slack::Web::Api::Errors::AccountInactive, Slack::Web::Api::Errors::MissingScope, Slack::Web::Api::Errors::InvalidAuth => e + rescue Slack::Web::Api::Errors::AccountInactive, Slack::Web::Api::Errors::MissingScope, Slack::Web::Api::Errors::InvalidAuth, + Slack::Web::Api::Errors::ChannelNotFound => e Rails.logger.error e hook.prompt_reauthorization! hook.disable From 4a89bab23a99b87116a9268972f5ac53921476cd Mon Sep 17 00:00:00 2001 From: Muhsin Keloth Date: Wed, 25 Oct 2023 21:06:31 +0530 Subject: [PATCH 11/94] feat: Handle Telegram send message/attachments errors (#8173) Co-authored-by: Pranav Raj S --- app/models/channel/telegram.rb | 31 +++++++++++++++++------- spec/models/channel/telegram_spec.rb | 36 ++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 9 deletions(-) diff --git a/app/models/channel/telegram.rb b/app/models/channel/telegram.rb index ac2f4b9be..94780cce4 100644 --- a/app/models/channel/telegram.rb +++ b/app/models/channel/telegram.rb @@ -87,9 +87,19 @@ class Channel::Telegram < ApplicationRecord def send_message(message) response = message_request(chat_id(message), message.content, reply_markup(message), reply_to_message_id(message)) + process_error(message, response) response.parsed_response['result']['message_id'] if response.success? end + def process_error(message, response) + return unless response.parsed_response['ok'] == false + + # https://github.com/TelegramBotAPI/errors/tree/master/json + message.external_error = "#{response.parsed_response['error_code']}, #{response.parsed_response['description']}" + message.status = :failed + message.save! + end + def reply_markup(message) return unless message.content_type == 'input_select' @@ -110,23 +120,26 @@ class Channel::Telegram < ApplicationRecord telegram_attachments = [] message.attachments.each do |attachment| telegram_attachment = {} - - case attachment[:file_type] - when 'audio' - telegram_attachment[:type] = 'audio' - when 'image' - telegram_attachment[:type] = 'photo' - when 'file' - telegram_attachment[:type] = 'document' - end + telegram_attachment[:type] = attachment_type(attachment[:file_type]) telegram_attachment[:media] = attachment.download_url telegram_attachments << telegram_attachment end response = attachments_request(chat_id(message), telegram_attachments, reply_to_message_id(message)) + process_error(message, response) response.parsed_response['result'].first['message_id'] if response.success? end + def attachment_type(file_type) + file_type_mappings = { + 'audio' => 'audio', + 'image' => 'photo', + 'file' => 'document', + 'video' => 'video' + } + file_type_mappings[file_type] + end + def attachments_request(chat_id, attachments, reply_to_message_id) HTTParty.post("#{telegram_api_url}/sendMediaGroup", body: { diff --git a/spec/models/channel/telegram_spec.rb b/spec/models/channel/telegram_spec.rb index 0fa5515fb..cda0b03d7 100644 --- a/spec/models/channel/telegram_spec.rb +++ b/spec/models/channel/telegram_spec.rb @@ -42,6 +42,28 @@ RSpec.describe Channel::Telegram do expect(telegram_channel.send_message_on_telegram(message)).to eq('telegram_123') end + + it 'send text message failed' do + message = create(:message, message_type: :outgoing, content: 'test', + conversation: create(:conversation, inbox: telegram_channel.inbox, additional_attributes: { 'chat_id' => '123' })) + + stub_request(:post, "https://api.telegram.org/bot#{telegram_channel.bot_token}/sendMessage") + .with( + body: 'chat_id=123&text=test&reply_markup=&reply_to_message_id=' + ) + .to_return( + status: 403, + headers: { 'Content-Type' => 'application/json' }, + body: { + ok: false, + error_code: '403', + description: 'Forbidden: bot was blocked by the user' + }.to_json + ) + telegram_channel.send_message_on_telegram(message) + expect(message.reload.status).to eq('failed') + expect(message.reload.external_error).to eq('403: Forbidden: bot was blocked by the user') + end end context 'when a empty message and valid attachments' do @@ -72,6 +94,20 @@ RSpec.describe Channel::Telegram do allow(telegram_channel).to receive(:attachments_request).and_return(telegram_attachment_response) expect(telegram_channel.send_message_on_telegram(message)).to eq('telegram_456') end + + it 'send image failed' do + telegram_attachment_response = double + attachment = message.attachments.new(account_id: message.account_id, file_type: :image) + attachment.file.attach(io: Rails.root.join('spec/assets/avatar.png').open, filename: 'avatar.png', content_type: 'image/png') + + allow(telegram_attachment_response).to receive(:success?).and_return(false) + allow(telegram_attachment_response).to receive(:parsed_response).and_return({ 'ok' => false, 'error_code' => '400', + 'description' => 'Bad Request: invalid file id' }) + allow(telegram_channel).to receive(:attachments_request).and_return(telegram_attachment_response) + telegram_channel.send_message_on_telegram(message) + expect(message.reload.status).to eq('failed') + expect(message.reload.external_error).to eq('400: Bad Request: invalid file id') + end end context 'when a valid message and valid attachment' do From 4fc62ed9ae91108bab698f8d67d4404dd1c98bf2 Mon Sep 17 00:00:00 2001 From: Muhsin Keloth Date: Wed, 25 Oct 2023 21:26:09 +0530 Subject: [PATCH 12/94] feat: Handle Instagram send message/attachments errors (#8174) Co-authored-by: Pranav Raj S --- .../instagram/send_on_instagram_service.rb | 18 ++++++++++++++++-- .../send_on_instagram_service_spec.rb | 19 +++++++++++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/app/services/instagram/send_on_instagram_service.rb b/app/services/instagram/send_on_instagram_service.rb index e7a061330..df24c1c25 100644 --- a/app/services/instagram/send_on_instagram_service.rb +++ b/app/services/instagram/send_on_instagram_service.rb @@ -66,12 +66,26 @@ class Instagram::SendOnInstagramService < Base::SendOnChannelService query: query ) - Rails.logger.error("Instagram response: #{response['error']} : #{message_content}") if response['error'] - message.update!(source_id: response['message_id']) if response['message_id'].present? + if response[:error].present? + Rails.logger.error("Instagram response: #{response['error']} : #{message_content}") + message.status = :failed + message.external_error = external_error(response) + end + + message.source_id = response['message_id'] if response['message_id'].present? + message.save! response end + def external_error(response) + # https://developers.facebook.com/docs/instagram-api/reference/error-codes/ + error_message = response[:error][:message] + error_code = response[:error][:code] + + "#{error_code} - #{error_message}" + end + def calculate_app_secret_proof(app_secret, access_token) Facebook::Messenger::Configuration::AppSecretProofCalculator.call( app_secret, access_token diff --git a/spec/services/instagram/send_on_instagram_service_spec.rb b/spec/services/instagram/send_on_instagram_service_spec.rb index ee553a7d8..f95d452eb 100644 --- a/spec/services/instagram/send_on_instagram_service_spec.rb +++ b/spec/services/instagram/send_on_instagram_service_spec.rb @@ -62,6 +62,25 @@ describe Instagram::SendOnInstagramService do expect(response).to eq({ message_id: 'anyrandommessageid1234567890' }) end + + it 'if message sent from chatwoot is failed' do + message = create(:message, message_type: 'outgoing', inbox: instagram_inbox, account: account, conversation: conversation) + + allow(HTTParty).to receive(:post).and_return( + { + 'error': { + 'message': 'The Instagram account is restricted.', + 'type': 'OAuthException', + 'code': 400, + 'fbtrace_id': 'anyrandomfbtraceid1234567890' + } + } + ) + described_class.new(message: message).perform + expect(HTTParty).to have_received(:post) + expect(message.reload.status).to eq('failed') + expect(message.reload.external_error).to eq('400 - The Instagram account is restricted.') + end end context 'with message_tag HUMAN_AGENT' do From 58f47eb02c8221d29a6ba2b4ae2510eafe107db5 Mon Sep 17 00:00:00 2001 From: Muhsin Keloth Date: Wed, 25 Oct 2023 21:29:25 +0530 Subject: [PATCH 13/94] feat: Handle Facebook send message/attachments errors (#8197) --- app/services/facebook/send_on_facebook_service.rb | 12 +++++++++++- .../facebook/send_on_facebook_service_spec.rb | 9 +++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/app/services/facebook/send_on_facebook_service.rb b/app/services/facebook/send_on_facebook_service.rb index 621bf57d5..776e8f5e8 100644 --- a/app/services/facebook/send_on_facebook_service.rb +++ b/app/services/facebook/send_on_facebook_service.rb @@ -15,7 +15,9 @@ class Facebook::SendOnFacebookService < Base::SendOnChannelService def send_message_to_facebook(delivery_params) result = Facebook::Messenger::Bot.deliver(delivery_params, page_id: channel.page_id) - message.update!(source_id: JSON.parse(result)['message_id']) + parsed_result = JSON.parse(result) + message.update!(status: :failed, external_error: external_error(parsed_result)) if parsed_result['error'].present? + message.update!(source_id: parsed_result['message_id']) if parsed_result['message_id'].present? end def fb_text_message_params @@ -27,6 +29,14 @@ class Facebook::SendOnFacebookService < Base::SendOnChannelService } end + def external_error(response) + # https://developers.facebook.com/docs/graph-api/guides/error-handling/ + error_message = response['error']['message'] + error_code = response['error']['code'] + + "#{error_code} - #{error_message}" + end + def fb_attachment_message_params attachment = message.attachments.first { diff --git a/spec/services/facebook/send_on_facebook_service_spec.rb b/spec/services/facebook/send_on_facebook_service_spec.rb index 39a194ca4..38d33e6d7 100644 --- a/spec/services/facebook/send_on_facebook_service_spec.rb +++ b/spec/services/facebook/send_on_facebook_service_spec.rb @@ -87,6 +87,15 @@ describe Facebook::SendOnFacebookService do tag: 'ACCOUNT_UPDATE' }, { page_id: facebook_channel.page_id }) end + + it 'if message sent from chatwoot is failed' do + message = create(:message, message_type: 'outgoing', inbox: facebook_inbox, account: account, conversation: conversation) + allow(bot).to receive(:deliver).and_return({ error: { message: 'Invalid OAuth access token.', type: 'OAuthException', code: 190, + fbtrace_id: 'BLBz/WZt8dN' } }.to_json) + described_class.new(message: message).perform + expect(bot).to have_received(:deliver) + expect(message.reload.status).to eq('failed') + end end end end From 54bc4c23dfa7f0012072b56822b6b06cd6c17d95 Mon Sep 17 00:00:00 2001 From: Muhsin Keloth Date: Thu, 26 Oct 2023 00:14:09 +0530 Subject: [PATCH 14/94] fix: Telegram model specs (#8207) --- spec/models/channel/telegram_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/models/channel/telegram_spec.rb b/spec/models/channel/telegram_spec.rb index cda0b03d7..6ed932de7 100644 --- a/spec/models/channel/telegram_spec.rb +++ b/spec/models/channel/telegram_spec.rb @@ -62,7 +62,7 @@ RSpec.describe Channel::Telegram do ) telegram_channel.send_message_on_telegram(message) expect(message.reload.status).to eq('failed') - expect(message.reload.external_error).to eq('403: Forbidden: bot was blocked by the user') + expect(message.reload.external_error).to eq('403, Forbidden: bot was blocked by the user') end end @@ -106,7 +106,7 @@ RSpec.describe Channel::Telegram do allow(telegram_channel).to receive(:attachments_request).and_return(telegram_attachment_response) telegram_channel.send_message_on_telegram(message) expect(message.reload.status).to eq('failed') - expect(message.reload.external_error).to eq('400: Bad Request: invalid file id') + expect(message.reload.external_error).to eq('400, Bad Request: invalid file id') end end From 8c67ea56e7def6ffd18ffc17530258272b36d1e9 Mon Sep 17 00:00:00 2001 From: Sojan Jose Date: Wed, 25 Oct 2023 13:03:08 -0700 Subject: [PATCH 15/94] chore: Campaign conversations should be created in open state (#8209) - Ensure that conversations originating from campaigns are created in the open state. --- app/models/conversation.rb | 4 ++++ spec/models/conversation_spec.rb | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/app/models/conversation.rb b/app/models/conversation.rb index d764f84a0..88a837c26 100644 --- a/app/models/conversation.rb +++ b/app/models/conversation.rb @@ -226,6 +226,10 @@ class Conversation < ApplicationRecord end def mark_conversation_pending_if_bot + # Message template hooks aren't executed for conversations from campaigns + # So making these conversations open for agent visibility + return if campaign.present? + # TODO: make this an inbox config instead of assuming bot conversations should start as pending self.status = :pending if inbox.active_bot? end diff --git a/spec/models/conversation_spec.rb b/spec/models/conversation_spec.rb index 70d33b71c..0a04cd73b 100644 --- a/spec/models/conversation_spec.rb +++ b/spec/models/conversation_spec.rb @@ -556,6 +556,11 @@ RSpec.describe Conversation do it 'returns conversation status as pending' do expect(conversation.status).to eq('pending') end + + it 'returns conversation as open if campaign is present' do + conversation = create(:conversation, inbox: bot_inbox.inbox, campaign: create(:campaign)) + expect(conversation.status).to eq('open') + end end describe '#botintegration: when conversation created in inbox with dialogflow integration' do From b6831d464e6b20ff6bf69e8ceafecd1e8077ed85 Mon Sep 17 00:00:00 2001 From: Sojan Jose Date: Wed, 25 Oct 2023 17:06:58 -0700 Subject: [PATCH 16/94] chore: Update API documentation for whatsapp templates (#8211) - Update API documentation for whatsapp templates --- .../request/conversation/create_message.yml | 21 ++++++ .../paths/application/conversation/index.yml | 33 ++++++++- swagger/swagger.json | 73 +++++++++++++++++++ 3 files changed, 126 insertions(+), 1 deletion(-) diff --git a/swagger/definitions/request/conversation/create_message.yml b/swagger/definitions/request/conversation/create_message.yml index 9082f1318..c8f4efc7f 100644 --- a/swagger/definitions/request/conversation/create_message.yml +++ b/swagger/definitions/request/conversation/create_message.yml @@ -19,3 +19,24 @@ properties: content_attributes: type: object description: attributes based on your content type + template_params: + type: object + description: The template params for the message in case of whatsapp Channel + properties: + name: + type: string + description: Name of the template + example: 'sample_issue_resolution' + category: + type: string + description: Category of the template + example: UTILITY + language: + type: string + description: Language of the template + example: en_US + processed_params: + type: object + description: The processed param values for template variables in template + example: + 1: "Chatwoot" diff --git a/swagger/paths/application/conversation/index.yml b/swagger/paths/application/conversation/index.yml index 4c9241464..c1cb199b0 100644 --- a/swagger/paths/application/conversation/index.yml +++ b/swagger/paths/application/conversation/index.yml @@ -66,6 +66,9 @@ post: required: true schema: type: object + required: + - source_id + - inbox_id properties: source_id: type: string @@ -93,7 +96,35 @@ post: team_id: type: string description: Team Id for assigning a conversation to a team - + message: + type: object + description: The initial message to be sent to the conversation + required: ['content'] + properties: + content: + type: string + description: The content of the message + template_params: + type: object + description: The template params for the message in case of whatsapp Channel + properties: + name: + type: string + description: Name of the template + example: 'sample_issue_resolution' + category: + type: string + description: Category of the template + example: UTILITY + language: + type: string + description: Language of the template + example: en_US + processed_params: + type: object + description: The processed param values for template variables in template + example: + 1: "Chatwoot" responses: 200: description: Success diff --git a/swagger/swagger.json b/swagger/swagger.json index cd8f3e7d3..9c6093fcf 100644 --- a/swagger/swagger.json +++ b/swagger/swagger.json @@ -2840,6 +2840,10 @@ "required": true, "schema": { "type": "object", + "required": [ + "source_id", + "inbox_id" + ], "properties": { "source_id": { "type": "string", @@ -2881,6 +2885,47 @@ "team_id": { "type": "string", "description": "Team Id for assigning a conversation to a team" + }, + "message": { + "type": "object", + "description": "The initial message to be sent to the conversation", + "required": [ + "content" + ], + "properties": { + "content": { + "type": "string", + "description": "The content of the message" + }, + "template_params": { + "type": "object", + "description": "The template params for the message in case of whatsapp Channel", + "properties": { + "name": { + "type": "string", + "description": "Name of the template", + "example": "sample_issue_resolution" + }, + "category": { + "type": "string", + "description": "Category of the template", + "example": "UTILITY" + }, + "language": { + "type": "string", + "description": "Language of the template", + "example": "en_US" + }, + "processed_params": { + "type": "object", + "description": "The processed param values for template variables in template", + "example": { + "1": "Chatwoot" + } + } + } + } + } } } } @@ -6246,6 +6291,34 @@ "content_attributes": { "type": "object", "description": "attributes based on your content type" + }, + "template_params": { + "type": "object", + "description": "The template params for the message in case of whatsapp Channel", + "properties": { + "name": { + "type": "string", + "description": "Name of the template", + "example": "sample_issue_resolution" + }, + "category": { + "type": "string", + "description": "Category of the template", + "example": "UTILITY" + }, + "language": { + "type": "string", + "description": "Language of the template", + "example": "en_US" + }, + "processed_params": { + "type": "object", + "description": "The processed param values for template variables in template", + "example": { + "1": "Chatwoot" + } + } + } } } }, From 3e54d3654b0ddf6531874c2005cde78fdc51d849 Mon Sep 17 00:00:00 2001 From: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Date: Thu, 26 Oct 2023 07:46:49 +0530 Subject: [PATCH 17/94] feat: Updated the search result fly-out menu design (#8203) Co-authored-by: Pranav Raj S --- .../search/components/MessageContent.vue | 17 +- .../conversation/search/PopOverSearch.vue | 80 -------- .../conversation/search/ResultItem.stories.js | 41 ---- .../conversation/search/ResultItem.vue | 185 ------------------ .../search/SearchMessageItem.stories.js | 47 ----- .../conversation/search/SearchMessageItem.vue | 163 --------------- .../conversation/search/SwitchLayout.vue | 36 ---- .../helpcenter/pages/portals/EditPortal.vue | 8 +- app/javascript/packs/portal.js | 5 + .../portal/components/PublicArticleSearch.vue | 5 +- .../portal/components/PublicSearchInput.vue | 2 +- .../portal/components/SearchSuggestions.vue | 55 ++++-- .../shared/mixins/messageFormatterMixin.js | 15 ++ 13 files changed, 67 insertions(+), 592 deletions(-) delete mode 100644 app/javascript/dashboard/routes/dashboard/conversation/search/PopOverSearch.vue delete mode 100644 app/javascript/dashboard/routes/dashboard/conversation/search/ResultItem.stories.js delete mode 100644 app/javascript/dashboard/routes/dashboard/conversation/search/ResultItem.vue delete mode 100644 app/javascript/dashboard/routes/dashboard/conversation/search/SearchMessageItem.stories.js delete mode 100644 app/javascript/dashboard/routes/dashboard/conversation/search/SearchMessageItem.vue delete mode 100644 app/javascript/dashboard/routes/dashboard/conversation/search/SwitchLayout.vue diff --git a/app/javascript/dashboard/modules/search/components/MessageContent.vue b/app/javascript/dashboard/modules/search/components/MessageContent.vue index 74d85ec87..4c86862f5 100644 --- a/app/javascript/dashboard/modules/search/components/MessageContent.vue +++ b/app/javascript/dashboard/modules/search/components/MessageContent.vue @@ -71,18 +71,11 @@ export default { }, prepareContent(content = '') { const escapedText = this.escapeHtml(content); - const plainTextContent = this.getPlainText(escapedText); - const escapedSearchTerm = this.escapeRegExp(this.searchTerm); - return plainTextContent - .replace( - new RegExp(`(${escapedSearchTerm})`, 'ig'), - '$1' - ) - .replace(/\s{2,}|\n|\r/g, ' '); - }, - // from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping - escapeRegExp(string) { - return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + return this.highlightContent( + escapedText, + this.searchTerm, + 'searchkey--highlight' + ); }, }, }; diff --git a/app/javascript/dashboard/routes/dashboard/conversation/search/PopOverSearch.vue b/app/javascript/dashboard/routes/dashboard/conversation/search/PopOverSearch.vue deleted file mode 100644 index 639b694e8..000000000 --- a/app/javascript/dashboard/routes/dashboard/conversation/search/PopOverSearch.vue +++ /dev/null @@ -1,80 +0,0 @@ - - - - diff --git a/app/javascript/dashboard/routes/dashboard/conversation/search/ResultItem.stories.js b/app/javascript/dashboard/routes/dashboard/conversation/search/ResultItem.stories.js deleted file mode 100644 index b0652e538..000000000 --- a/app/javascript/dashboard/routes/dashboard/conversation/search/ResultItem.stories.js +++ /dev/null @@ -1,41 +0,0 @@ -import ResultItem from './ResultItem'; - -export default { - title: 'Components/Search/Result Items', - component: ResultItem, - argTypes: { - conversationId: { - defaultValue: '1', - control: { - type: 'number', - }, - }, - userName: { - defaultValue: 'John davies', - control: { - type: 'text', - }, - }, - inboxName: { - defaultValue: 'Support', - control: { - type: 'text', - }, - }, - timestamp: { - defaultValue: '1618046084', - control: { - type: 'number', - }, - }, - }, -}; - -const Template = (args, { argTypes }) => ({ - props: Object.keys(argTypes), - components: { ResultItem }, - template: '', -}); - -export const ResultItems = Template.bind({}); -ResultItems.args = {}; diff --git a/app/javascript/dashboard/routes/dashboard/conversation/search/ResultItem.vue b/app/javascript/dashboard/routes/dashboard/conversation/search/ResultItem.vue deleted file mode 100644 index 2fece6f33..000000000 --- a/app/javascript/dashboard/routes/dashboard/conversation/search/ResultItem.vue +++ /dev/null @@ -1,185 +0,0 @@ - - - - - diff --git a/app/javascript/dashboard/routes/dashboard/conversation/search/SearchMessageItem.stories.js b/app/javascript/dashboard/routes/dashboard/conversation/search/SearchMessageItem.stories.js deleted file mode 100644 index 78f0d0deb..000000000 --- a/app/javascript/dashboard/routes/dashboard/conversation/search/SearchMessageItem.stories.js +++ /dev/null @@ -1,47 +0,0 @@ -import SearchMessage from './SearchMessageItem'; - -export default { - title: 'Components/Search/Messages', - component: SearchMessage, - argTypes: { - userName: { - defaultValue: 'John davies', - control: { - type: 'text', - }, - }, - timestamp: { - defaultValue: '1618046084', - control: { - type: 'number', - }, - }, - messageType: { - control: { - type: 'number', - }, - }, - content: { - defaultValue: - 'some designers and developers around the web know this and have put together a bunch of text generators', - control: { - type: 'text', - }, - }, - searchTerm: { - defaultValue: 'developers', - control: { - type: 'text', - }, - }, - }, -}; - -const Template = (args, { argTypes }) => ({ - props: Object.keys(argTypes), - components: { SearchMessage }, - template: '', -}); - -export const Messages = Template.bind({}); -Messages.args = {}; diff --git a/app/javascript/dashboard/routes/dashboard/conversation/search/SearchMessageItem.vue b/app/javascript/dashboard/routes/dashboard/conversation/search/SearchMessageItem.vue deleted file mode 100644 index 35f208341..000000000 --- a/app/javascript/dashboard/routes/dashboard/conversation/search/SearchMessageItem.vue +++ /dev/null @@ -1,163 +0,0 @@ - - - - diff --git a/app/javascript/dashboard/routes/dashboard/conversation/search/SwitchLayout.vue b/app/javascript/dashboard/routes/dashboard/conversation/search/SwitchLayout.vue deleted file mode 100644 index 1988e9974..000000000 --- a/app/javascript/dashboard/routes/dashboard/conversation/search/SwitchLayout.vue +++ /dev/null @@ -1,36 +0,0 @@ - - - - - diff --git a/app/javascript/dashboard/routes/dashboard/helpcenter/pages/portals/EditPortal.vue b/app/javascript/dashboard/routes/dashboard/helpcenter/pages/portals/EditPortal.vue index 53cc83c44..53a8bdf40 100644 --- a/app/javascript/dashboard/routes/dashboard/helpcenter/pages/portals/EditPortal.vue +++ b/app/javascript/dashboard/routes/dashboard/helpcenter/pages/portals/EditPortal.vue @@ -9,7 +9,7 @@ " :show-new-button="false" /> -
+
-
-
- +
+ +
diff --git a/app/javascript/packs/portal.js b/app/javascript/packs/portal.js index 42493615d..874d3488d 100644 --- a/app/javascript/packs/portal.js +++ b/app/javascript/packs/portal.js @@ -1,7 +1,12 @@ import Rails from '@rails/ujs'; import Turbolinks from 'turbolinks'; import '../portal/application.scss'; +import Vue from 'vue'; import { InitializationHelpers } from '../portal/portalHelpers'; +import VueDOMPurifyHTML from 'vue-dompurify-html'; +import { domPurifyConfig } from '../shared/helpers/HTMLSanitizer'; + +Vue.use(VueDOMPurifyHTML, domPurifyConfig); Rails.start(); Turbolinks.start(); diff --git a/app/javascript/portal/components/PublicArticleSearch.vue b/app/javascript/portal/components/PublicArticleSearch.vue index b06ce112f..f2afeb4ce 100644 --- a/app/javascript/portal/components/PublicArticleSearch.vue +++ b/app/javascript/portal/components/PublicArticleSearch.vue @@ -1,5 +1,5 @@