From 68062216e484751ae004e2616c179afacecfd41f Mon Sep 17 00:00:00 2001 From: Muhsin Keloth Date: Fri, 5 Jan 2024 15:13:22 +0530 Subject: [PATCH 1/6] feat: Add a job to reopen snoozed notifications (#8545) --- .../reopen_snoozed_notifications_job.rb | 10 +++++++++ app/jobs/trigger_scheduled_items_job.rb | 3 +++ .../reopen_snoozed_notifications_job_spec.rb | 22 +++++++++++++++++++ spec/jobs/trigger_scheduled_items_job_spec.rb | 5 +++++ 4 files changed, 40 insertions(+) create mode 100644 app/jobs/notification/reopen_snoozed_notifications_job.rb create mode 100644 spec/jobs/notification/reopen_snoozed_notifications_job_spec.rb diff --git a/app/jobs/notification/reopen_snoozed_notifications_job.rb b/app/jobs/notification/reopen_snoozed_notifications_job.rb new file mode 100644 index 000000000..89b245187 --- /dev/null +++ b/app/jobs/notification/reopen_snoozed_notifications_job.rb @@ -0,0 +1,10 @@ +class Notification::ReopenSnoozedNotificationsJob < ApplicationJob + queue_as :low + + def perform + # rubocop:disable Rails/SkipsModelValidations + Notification.where(snoozed_until: 3.days.ago..Time.current) + .update_all(snoozed_until: nil, updated_at: Time.current, last_activity_at: Time.current) + # rubocop:enable Rails/SkipsModelValidations + end +end diff --git a/app/jobs/trigger_scheduled_items_job.rb b/app/jobs/trigger_scheduled_items_job.rb index f51d9df9a..9fc7a6491 100644 --- a/app/jobs/trigger_scheduled_items_job.rb +++ b/app/jobs/trigger_scheduled_items_job.rb @@ -11,6 +11,9 @@ class TriggerScheduledItemsJob < ApplicationJob # Job to reopen snoozed conversations Conversations::ReopenSnoozedConversationsJob.perform_later + # Job to reopen snoozed notifications + Notification::ReopenSnoozedNotificationsJob.perform_later + # Job to auto-resolve conversations Account::ConversationsResolutionSchedulerJob.perform_later diff --git a/spec/jobs/notification/reopen_snoozed_notifications_job_spec.rb b/spec/jobs/notification/reopen_snoozed_notifications_job_spec.rb new file mode 100644 index 000000000..2b210ef2b --- /dev/null +++ b/spec/jobs/notification/reopen_snoozed_notifications_job_spec.rb @@ -0,0 +1,22 @@ +require 'rails_helper' + +RSpec.describe Notification::ReopenSnoozedNotificationsJob do + let!(:snoozed_till_5_minutes_ago) { create(:notification, snoozed_until: 5.minutes.ago) } + let!(:snoozed_till_tomorrow) { create(:notification, snoozed_until: 1.day.from_now) } + let!(:snoozed_indefinitely) { create(:notification) } + + it 'enqueues the job' do + expect { described_class.perform_later }.to have_enqueued_job(described_class) + .on_queue('low') + end + + context 'when called' do + it 'reopens snoozed notifications whose snooze until has passed' do + described_class.perform_now + + expect(snoozed_till_5_minutes_ago.reload.snoozed_until).to be_nil + expect(snoozed_till_tomorrow.reload.snoozed_until.to_date).to eq 1.day.from_now.to_date + expect(snoozed_indefinitely.reload.snoozed_until).to be_nil + end + end +end diff --git a/spec/jobs/trigger_scheduled_items_job_spec.rb b/spec/jobs/trigger_scheduled_items_job_spec.rb index 270bd9076..9fd846c2d 100644 --- a/spec/jobs/trigger_scheduled_items_job_spec.rb +++ b/spec/jobs/trigger_scheduled_items_job_spec.rb @@ -26,6 +26,11 @@ RSpec.describe TriggerScheduledItemsJob do described_class.perform_now end + it 'triggers Notification::ReopenSnoozedNotificationsJob' do + expect(Notification::ReopenSnoozedNotificationsJob).to receive(:perform_later).once + described_class.perform_now + end + it 'triggers Account::ConversationsResolutionSchedulerJob' do expect(Account::ConversationsResolutionSchedulerJob).to receive(:perform_later).once described_class.perform_now From 6e300644218667f1bfaa4a10aec690c862de63bd Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Sat, 6 Jan 2024 02:35:00 +0530 Subject: [PATCH 2/6] feat: handle unsupported media on the backend (#8650) This PR logs additional information in content_attributes of a message in case it is unsupported. This info can be used by the client to render a fresh UI --- .../messages/instagram/message_builder.rb | 9 ++++++- app/models/message.rb | 2 +- .../instagram_message_create_event.rb | 27 +++++++++++++++++++ .../webhooks/instagram_events_job_spec.rb | 18 +++++++++++++ 4 files changed, 54 insertions(+), 2 deletions(-) diff --git a/app/builders/messages/instagram/message_builder.rb b/app/builders/messages/instagram/message_builder.rb index 5610e0671..e9debb767 100644 --- a/app/builders/messages/instagram/message_builder.rb +++ b/app/builders/messages/instagram/message_builder.rb @@ -48,6 +48,10 @@ class Messages::Instagram::MessageBuilder < Messages::Messenger::MessageBuilder @outgoing_echo ? recipient_id : sender_id end + def message_is_unsupported? + message[:is_unsupported].present? && @messaging[:message][:is_unsupported] == true + end + def sender_id @messaging[:sender][:id] end @@ -118,7 +122,7 @@ class Messages::Instagram::MessageBuilder < Messages::Messenger::MessageBuilder end def message_params - { + params = { account_id: conversation.account_id, inbox_id: conversation.inbox_id, message_type: message_type, @@ -129,6 +133,9 @@ class Messages::Instagram::MessageBuilder < Messages::Messenger::MessageBuilder in_reply_to_external_id: message_reply_attributes } } + + params[:content_attributes][:is_unsupported] = true if message_is_unsupported? + params end def already_sent_from_chatwoot? diff --git a/app/models/message.rb b/app/models/message.rb index 27d5b6046..751890acf 100644 --- a/app/models/message.rb +++ b/app/models/message.rb @@ -101,7 +101,7 @@ class Message < ApplicationRecord # [:external_error : Can specify if the message creation failed due to an error at external API store :content_attributes, accessors: [:submitted_email, :items, :submitted_values, :email, :in_reply_to, :deleted, :external_created_at, :story_sender, :story_id, :external_error, - :translations, :in_reply_to_external_id], coder: JSON + :translations, :in_reply_to_external_id, :is_unsupported], coder: JSON store :external_source_ids, accessors: [:slack], coder: JSON, prefix: :external_source_id diff --git a/spec/factories/instagram/instagram_message_create_event.rb b/spec/factories/instagram/instagram_message_create_event.rb index 7729c1d7a..00ca91e23 100644 --- a/spec/factories/instagram/instagram_message_create_event.rb +++ b/spec/factories/instagram/instagram_message_create_event.rb @@ -273,6 +273,33 @@ FactoryBot.define do initialize_with { attributes } end + factory :instagram_message_unsupported_event, class: Hash do + entry do + [ + { + 'id': 'instagram-message-unsupported-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', + 'message': { + 'mid': 'unsupported-message-id-1', + 'is_unsupported': true + } + } + ] + } + ] + end + initialize_with { attributes } + end + factory :messaging_seen_event, class: Hash do entry do [ diff --git a/spec/jobs/webhooks/instagram_events_job_spec.rb b/spec/jobs/webhooks/instagram_events_job_spec.rb index 905810512..e77d654cc 100644 --- a/spec/jobs/webhooks/instagram_events_job_spec.rb +++ b/spec/jobs/webhooks/instagram_events_job_spec.rb @@ -28,6 +28,7 @@ describe Webhooks::InstagramEventsJob do 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!(:unsupported_message_event) { build(:instagram_message_unsupported_event).with_indifferent_access } let(:fb_object) { double } describe '#perform' do @@ -45,6 +46,7 @@ describe Webhooks::InstagramEventsJob do expect(instagram_inbox.contacts.last.additional_attributes['social_profiles']['instagram']).to eq 'some_user_name' expect(instagram_inbox.conversations.count).to be 1 expect(instagram_inbox.messages.count).to be 1 + expect(instagram_inbox.messages.last.content_attributes['is_unsupported']).to be_nil end it 'creates standby message in the instagram inbox' do @@ -157,6 +159,22 @@ describe Webhooks::InstagramEventsJob 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 + + it 'handles unsupported message' do + allow(Koala::Facebook::API).to receive(:new).and_return(fb_object) + allow(fb_object).to receive(:get_object).and_return( + return_object.with_indifferent_access + ) + + instagram_webhook.perform_now(unsupported_message_event[:entry]) + instagram_inbox.reload + + expect(instagram_inbox.contacts.count).to be 1 + expect(instagram_inbox.contacts.last.additional_attributes['social_profiles']['instagram']).to eq 'some_user_name' + expect(instagram_inbox.conversations.count).to be 1 + expect(instagram_inbox.messages.count).to be 1 + expect(instagram_inbox.messages.last.content_attributes['is_unsupported']).to be true + end end end end From 2c7f93978e4522819b649edd3a0ac438e613ad1d Mon Sep 17 00:00:00 2001 From: Pranav Raj S Date: Fri, 5 Jan 2024 13:09:09 -0800 Subject: [PATCH 3/6] fix: Update broken specs (#8651) - Use fakeTimer for time.spec.js - Use default sort as last_activity_at_desc - Update specs for getAllConversations getter --- .../dashboard/mixins/specs/time.spec.js | 17 +- .../store/modules/conversations/helpers.js | 3 +- .../conversations/conversations.fixtures.js | 34 ++ .../specs/conversations/getters.spec.js | 438 ++++-------------- 4 files changed, 137 insertions(+), 355 deletions(-) create mode 100644 app/javascript/dashboard/store/modules/specs/conversations/conversations.fixtures.js diff --git a/app/javascript/dashboard/mixins/specs/time.spec.js b/app/javascript/dashboard/mixins/specs/time.spec.js index 160cc9ff8..af120eb50 100644 --- a/app/javascript/dashboard/mixins/specs/time.spec.js +++ b/app/javascript/dashboard/mixins/specs/time.spec.js @@ -1,5 +1,4 @@ import TimeMixin from '../time'; -import { format } from 'date-fns'; describe('#messageStamp', () => { it('returns correct value', () => { @@ -11,10 +10,20 @@ describe('#messageStamp', () => { }); describe('#messageTimestamp', () => { + beforeEach(() => { + jest.useFakeTimers('modern'); + + const mockDate = new Date(2023, 4, 5); + jest.setSystemTime(mockDate); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + it('should return the message date in the specified format if the message was sent in the current year', () => { - const currentEpochTime = Math.floor(new Date().getTime() / 1000); - expect(TimeMixin.methods.messageTimestamp(currentEpochTime)).toEqual( - format(new Date(currentEpochTime * 1000), 'MMM d, yyyy') + expect(TimeMixin.methods.messageTimestamp(1680777464)).toEqual( + 'Apr 6, 2023' ); }); it('should return the message date and time in a different format if the message was sent in a different year', () => { diff --git a/app/javascript/dashboard/store/modules/conversations/helpers.js b/app/javascript/dashboard/store/modules/conversations/helpers.js index 8f01de272..0063c8cfc 100644 --- a/app/javascript/dashboard/store/modules/conversations/helpers.js +++ b/app/javascript/dashboard/store/modules/conversations/helpers.js @@ -108,6 +108,7 @@ const sortConfig = { }; export const sortComparator = (a, b, sortKey) => { - const [sortMethod, sortDirection] = SORT_OPTIONS[sortKey] || []; + const [sortMethod, sortDirection] = + SORT_OPTIONS[sortKey] || SORT_OPTIONS.last_activity_at_desc; return sortConfig[sortMethod](a, b, sortDirection); }; diff --git a/app/javascript/dashboard/store/modules/specs/conversations/conversations.fixtures.js b/app/javascript/dashboard/store/modules/specs/conversations/conversations.fixtures.js new file mode 100644 index 000000000..96655b9ec --- /dev/null +++ b/app/javascript/dashboard/store/modules/specs/conversations/conversations.fixtures.js @@ -0,0 +1,34 @@ +export default [ + { + created_at: 1702411932, // Dec 12, 2023 12:12:12 + id: 1, + last_activity_at: 1704408443, // Jan 04, 2024 14:47:23 + messages: [{ content: 'test1' }], + priority: 'medium', + waiting_since: 0, // not waiting + }, + { + created_at: 1699819932, // Nov 12, 2023 12:12:12 + id: 2, + last_activity_at: 1704485532, // Jan 05, 2024 12:12:12 + messages: [{ content: 'test2' }], + priority: 'low', + waiting_since: 1683645800, // May 09 2023 15:23:20 + }, + { + created_at: 1641413532, // Jan 05, 2022 12:12:12 + id: 3, + last_activity_at: 1704408567, // Jan 04, 2024 14:49:27 + messages: [{ content: 'test3' }], + priority: 'low', + waiting_since: 0, // not waiting + }, + { + created_at: 1641413531, // Jan 05, 2022 12:12:11 + id: 4, + last_activity_at: 1704408566, // Jan 04, 2024 14:49:26 + messages: [{ content: 'test4' }], + priority: 'high', + waiting_since: 1683645801, // May 09 2023 15:23:21 + }, +]; diff --git a/app/javascript/dashboard/store/modules/specs/conversations/getters.spec.js b/app/javascript/dashboard/store/modules/specs/conversations/getters.spec.js index a32cc853e..1a2225da6 100644 --- a/app/javascript/dashboard/store/modules/specs/conversations/getters.spec.js +++ b/app/javascript/dashboard/store/modules/specs/conversations/getters.spec.js @@ -1,392 +1,130 @@ import commonHelpers from '../../../../helper/commons'; import getters from '../../conversations/getters'; +/* + Order of conversations in the fixture is as follows: + - lastActivity: c0 < c3 < c2 < c1 + - createdAt: c3 < c2 < c1 < c0 + - priority: c1 < c2 < c0 < c3 + - waitingSince: c1 > c3 > c0 < c2 +*/ +import conversations from './conversations.fixtures'; // loads .last() helper commonHelpers(); describe('#getters', () => { describe('#getAllConversations', () => { - it('order conversations based on last activity', () => { - const state = { - allConversations: [ - { - id: 1, - messages: [ - { - content: 'test1', - }, - ], - created_at: 2466424490, - last_activity_at: 2466424490, - }, - { - id: 2, - messages: [{ content: 'test2' }], - created_at: 1466424480, - last_activity_at: 1466424480, - }, - ], - }; - + it('returns conversations ordered by lastActivityAt in descending order if no sort order is available', () => { + const state = { allConversations: [...conversations] }; expect(getters.getAllConversations(state)).toEqual([ - { - id: 1, - messages: [ - { - content: 'test1', - }, - ], - created_at: 2466424490, - last_activity_at: 2466424490, - }, - { - id: 2, - messages: [{ content: 'test2' }], - created_at: 1466424480, - last_activity_at: 1466424480, - }, - ]); - }); - it('order conversations based on last activity with ascending order', () => { - const state = { - allConversations: [ - { - id: 1, - messages: [ - { - content: 'test1', - }, - ], - created_at: 2466424490, - last_activity_at: 2466424490, - }, - { - id: 2, - messages: [{ content: 'test2' }], - created_at: 1466424480, - last_activity_at: 1466424480, - }, - ], - chatSortFilter: 'latest_last', - }; - - expect(getters.getAllConversations(state)).toEqual([ - { - id: 2, - messages: [{ content: 'test2' }], - created_at: 1466424480, - last_activity_at: 1466424480, - }, - { - id: 1, - messages: [ - { - content: 'test1', - }, - ], - created_at: 2466424490, - last_activity_at: 2466424490, - }, + conversations[1], + conversations[2], + conversations[3], + conversations[0], ]); }); - it('order conversations based on created at', () => { + it('returns conversations ordered by lastActivityAt in descending order if invalid sort order is available', () => { const state = { - allConversations: [ - { - id: 1, - messages: [ - { - content: 'test1', - }, - ], - created_at: 1683645801, // Tuesday, 9 May 2023 - last_activity_at: 2466424490, - }, - { - id: 2, - messages: [{ content: 'test2' }], - created_at: 1652109801, // Monday, 9 May 2022 - last_activity_at: 1466424480, - }, - ], - chatSortFilter: 'created_at_last', + allConversations: [...conversations], + chatSortFilter: 'latest', }; - expect(getters.getAllConversations(state)).toEqual([ - { - id: 2, - messages: [{ content: 'test2' }], - created_at: 1652109801, - last_activity_at: 1466424480, - }, - { - id: 1, - messages: [ - { - content: 'test1', - }, - ], - created_at: 1683645801, - last_activity_at: 2466424490, - }, + conversations[1], + conversations[2], + conversations[3], + conversations[0], ]); }); - it('order conversations based on created at with descending order', () => { + it('returns conversations ordered by lastActivityAt in descending order if chatStatusFilter = last_activity_at_desc', () => { const state = { - allConversations: [ - { - id: 1, - messages: [ - { - content: 'test1', - }, - ], - created_at: 1683645801, // Tuesday, 9 May 2023 - last_activity_at: 2466424490, - }, - { - id: 2, - messages: [{ content: 'test2' }], - created_at: 1652109801, // Monday, 9 May 2022 - last_activity_at: 1466424480, - }, - ], - chatSortFilter: 'created_at_first', + allConversations: [...conversations], + chatSortFilter: 'last_activity_at_desc', }; - expect(getters.getAllConversations(state)).toEqual([ - { - id: 1, - messages: [ - { - content: 'test1', - }, - ], - created_at: 1683645801, - last_activity_at: 2466424490, - }, - { - id: 2, - messages: [{ content: 'test2' }], - created_at: 1652109801, - last_activity_at: 1466424480, - }, + conversations[1], + conversations[2], + conversations[3], + conversations[0], ]); }); - it('order conversations based on default order', () => { + it('returns conversations ordered by lastActivityAt in ascending order if chatStatusFilter = last_activity_at_asc', () => { const state = { - allConversations: [ - { - id: 1, - messages: [ - { - content: 'test1', - }, - ], - created_at: 2466424490, - last_activity_at: 2466424490, - }, - { - id: 2, - messages: [{ content: 'test2' }], - created_at: 1466424480, - last_activity_at: 1466424480, - }, - ], + allConversations: [...conversations], + chatSortFilter: 'last_activity_at_asc', }; - expect(getters.getAllConversations(state)).toEqual([ - { - id: 1, - messages: [ - { - content: 'test1', - }, - ], - created_at: 2466424490, - last_activity_at: 2466424490, - }, - { - id: 2, - messages: [{ content: 'test2' }], - created_at: 1466424480, - last_activity_at: 1466424480, - }, - ]); - }); - it('order conversations based on priority', () => { - const state = { - allConversations: [ - { - id: 1, - messages: [ - { - content: 'test1', - }, - ], - priority: 'low', - created_at: 1683645801, - last_activity_at: 2466424490, - }, - { - id: 2, - messages: [{ content: 'test2' }], - priority: 'urgent', - created_at: 1652109801, - last_activity_at: 1466424480, - }, - { - id: 3, - messages: [{ content: 'test3' }], - priority: 'medium', - created_at: 1652109801, - last_activity_at: 1466421280, - }, - ], - chatSortFilter: 'priority_first', - }; - - expect(getters.getAllConversations(state)).toEqual([ - { - id: 2, - messages: [{ content: 'test2' }], - priority: 'urgent', - created_at: 1652109801, - last_activity_at: 1466424480, - }, - { - id: 3, - messages: [{ content: 'test3' }], - priority: 'medium', - created_at: 1652109801, - last_activity_at: 1466421280, - }, - { - id: 1, - messages: [ - { - content: 'test1', - }, - ], - priority: 'low', - created_at: 1683645801, - last_activity_at: 2466424490, - }, + conversations[0], + conversations[3], + conversations[2], + conversations[1], ]); }); - it('order conversations based on with descending order', () => { + it('returns conversations ordered by createdAt in descending order if chatStatusFilter = created_at_desc', () => { const state = { - allConversations: [ - { - id: 1, - messages: [ - { - content: 'test1', - }, - ], - priority: 'low', - created_at: 1683645801, - last_activity_at: 2466424490, - }, - { - id: 2, - messages: [{ content: 'test2' }], - priority: 'urgent', - created_at: 1652109801, - last_activity_at: 1466424480, - }, - { - id: 3, - messages: [{ content: 'test3' }], - priority: 'medium', - created_at: 1652109801, - last_activity_at: 1466421280, - }, - ], - chatSortFilter: 'priority_last', + allConversations: [...conversations], + chatSortFilter: 'created_at_desc', }; - expect(getters.getAllConversations(state)).toEqual([ - { - id: 1, - messages: [ - { - content: 'test1', - }, - ], - priority: 'low', - created_at: 1683645801, - last_activity_at: 2466424490, - }, - { - id: 3, - messages: [{ content: 'test3' }], - priority: 'medium', - created_at: 1652109801, - last_activity_at: 1466421280, - }, - { - id: 2, - messages: [{ content: 'test2' }], - priority: 'urgent', - created_at: 1652109801, - last_activity_at: 1466424480, - }, + conversations[0], + conversations[1], + conversations[2], + conversations[3], ]); }); - it('order conversations based on waiting_since', () => { + it('returns conversations ordered by createdAt in ascending order if chatStatusFilter = created_at_asc', () => { const state = { - allConversations: [ - { - id: 3, - created_at: 1683645800, - waiting_since: 0, - }, - { - id: 4, - created_at: 1683645799, - waiting_since: 0, - }, - { - id: 1, - created_at: 1683645801, - waiting_since: 1683645802, - }, - { - id: 2, - created_at: 1683645803, - waiting_since: 1683645800, - }, - ], - chatSortFilter: 'waiting_since_last', + allConversations: [...conversations], + chatSortFilter: 'created_at_asc', }; - expect(getters.getAllConversations(state)).toEqual([ - { - id: 2, - created_at: 1683645803, - waiting_since: 1683645800, - }, - { - id: 1, - created_at: 1683645801, - waiting_since: 1683645802, - }, - { - id: 4, - created_at: 1683645799, - waiting_since: 0, - }, - { - id: 3, - created_at: 1683645800, - waiting_since: 0, - }, + conversations[3], + conversations[2], + conversations[1], + conversations[0], + ]); + }); + + it('returns conversations ordered by priority in descending order if chatStatusFilter = priority_desc', () => { + const state = { + allConversations: [...conversations], + chatSortFilter: 'priority_desc', + }; + expect(getters.getAllConversations(state)).toEqual([ + conversations[3], + conversations[0], + conversations[1], + conversations[2], + ]); + }); + + it('returns conversations ordered by priority in ascending order if chatStatusFilter = priority_asc', () => { + const state = { + allConversations: [...conversations], + chatSortFilter: 'priority_asc', + }; + expect(getters.getAllConversations(state)).toEqual([ + conversations[1], + conversations[2], + conversations[0], + conversations[3], + ]); + }); + + it('returns conversations ordered by longest waiting if chatStatusFilter = waiting_since_asc', () => { + const state = { + allConversations: [...conversations], + chatSortFilter: 'waiting_since_asc', + }; + expect(getters.getAllConversations(state)).toEqual([ + conversations[1], + conversations[3], + conversations[2], + conversations[0], ]); }); }); From dc4e13b3008edf76eee63b3419f9ab2281ccf7b8 Mon Sep 17 00:00:00 2001 From: Sojan Jose Date: Fri, 5 Jan 2024 13:10:26 -0800 Subject: [PATCH 4/6] chore: Fix for empty update case for messages (#8641) We observed an issue in production where the external webhook for an API inbox was failing. This, in turn, calls message update to update message status to failed. This causes a loop because rails trigger after_update callbacks even for empty commits. Ref: rails/rails#44500 --- app/models/message.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/models/message.rb b/app/models/message.rb index 751890acf..48507adb7 100644 --- a/app/models/message.rb +++ b/app/models/message.rb @@ -299,6 +299,10 @@ class Message < ApplicationRecord end def dispatch_update_event + # ref: https://github.com/rails/rails/issues/44500 + # we want to skip the update event if the message is not updated + return if previous_changes.blank? + Rails.configuration.dispatcher.dispatch(MESSAGE_UPDATED, Time.zone.now, message: self, performed_by: Current.executed_by, previous_changes: previous_changes) end From 56fbbe92b49bb3b166476347898ba978dab8a267 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Sat, 6 Jan 2024 04:56:52 +0530 Subject: [PATCH 5/6] feat: trigger handoff when agent bot is the actor (#8639) - This PR adds a feature to auto-trigger handoff events when an Agent bot toggles a conversation status from Pending to Open Co-authored-by: Sojan --- .../v1/accounts/conversations_controller.rb | 17 +++++++- .../concerns/access_token_auth_helper.rb | 2 +- app/policies/inbox_policy.rb | 2 +- .../accounts/conversations_controller_spec.rb | 39 +++++++++++++++++++ 4 files changed, 56 insertions(+), 4 deletions(-) diff --git a/app/controllers/api/v1/accounts/conversations_controller.rb b/app/controllers/api/v1/accounts/conversations_controller.rb index a3d3ad645..281ff95de 100644 --- a/app/controllers/api/v1/accounts/conversations_controller.rb +++ b/app/controllers/api/v1/accounts/conversations_controller.rb @@ -60,13 +60,26 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro end def toggle_status - if params[:status].present? + # FIXME: move this logic into a service object + if pending_to_open_by_bot? + @conversation.bot_handoff! + elsif params[:status].present? set_conversation_status @status = @conversation.save! else @status = @conversation.toggle_status end - assign_conversation if @conversation.status == 'open' && Current.user.is_a?(User) && Current.user&.agent? + assign_conversation if should_assign_conversation? + end + + def pending_to_open_by_bot? + return false unless Current.user.is_a?(AgentBot) + + @conversation.status == 'pending' && params[:status] == 'open' + end + + def should_assign_conversation? + @conversation.status == 'open' && Current.user.is_a?(User) && Current.user&.agent? end def toggle_priority diff --git a/app/controllers/concerns/access_token_auth_helper.rb b/app/controllers/concerns/access_token_auth_helper.rb index 2f4dc4337..c35a28d7d 100644 --- a/app/controllers/concerns/access_token_auth_helper.rb +++ b/app/controllers/concerns/access_token_auth_helper.rb @@ -14,7 +14,7 @@ module AccessTokenAuthHelper render_unauthorized('Invalid Access Token') && return if @access_token.blank? @resource = @access_token.owner - Current.user = @resource if current_user.is_a?(User) + Current.user = @resource if [User, AgentBot].include?(@resource.class) end def validate_bot_access_token! diff --git a/app/policies/inbox_policy.rb b/app/policies/inbox_policy.rb index 891b3414a..0f8fe2307 100644 --- a/app/policies/inbox_policy.rb +++ b/app/policies/inbox_policy.rb @@ -21,7 +21,7 @@ class InboxPolicy < ApplicationPolicy def show? # FIXME: for agent bots, lets bring this validation to policies as well in future - return true if @user.blank? + return true if @user.is_a?(AgentBot) Current.user.assigned_inboxes.include? record end diff --git a/spec/controllers/api/v1/accounts/conversations_controller_spec.rb b/spec/controllers/api/v1/accounts/conversations_controller_spec.rb index 90cd018f3..4c4bb9dc9 100644 --- a/spec/controllers/api/v1/accounts/conversations_controller_spec.rb +++ b/spec/controllers/api/v1/accounts/conversations_controller_spec.rb @@ -323,6 +323,9 @@ RSpec.describe 'Conversations API', type: :request do describe 'POST /api/v1/accounts/{account.id}/conversations/:id/toggle_status' do let(:conversation) { create(:conversation, account: account) } + let(:inbox) { create(:inbox, account: account) } + let(:pending_conversation) { create(:conversation, inbox: inbox, account: account, status: 'pending') } + let(:agent_bot) { create(:agent_bot, account: account) } context 'when it is an unauthenticated user' do it 'returns unauthorized' do @@ -424,6 +427,42 @@ RSpec.describe 'Conversations API', type: :request do # expect(conversation.reload.status).to eq('pending') # end end + + context 'when it is an authenticated bot' do + # this test will basically ensure that the status actually changes + # regardless of the value to be done + it 'returns authorized for arbritrary status' do + create(:agent_bot_inbox, inbox: inbox, agent_bot: agent_bot) + + conversation.update!(status: 'open') + expect(conversation.reload.status).to eq('open') + snoozed_until = (DateTime.now.utc + 2.days).to_i + + post "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/toggle_status", + headers: { api_access_token: agent_bot.access_token.token }, + params: { status: 'snoozed', snoozed_until: snoozed_until }, + as: :json + + expect(response).to have_http_status(:success) + expect(conversation.reload.status).to eq('snoozed') + end + + it 'triggers handoff event when moving from pending to open' do + create(:agent_bot_inbox, inbox: inbox, agent_bot: agent_bot) + allow(Rails.configuration.dispatcher).to receive(:dispatch) + + post "/api/v1/accounts/#{account.id}/conversations/#{pending_conversation.display_id}/toggle_status", + headers: { api_access_token: agent_bot.access_token.token }, + params: { status: 'open' }, + as: :json + + expect(response).to have_http_status(:success) + expect(pending_conversation.reload.status).to eq('open') + expect(Rails.configuration.dispatcher).to have_received(:dispatch) + .with(Events::Types::CONVERSATION_BOT_HANDOFF, kind_of(Time), conversation: pending_conversation, notifiable_assignee_change: false, + changed_attributes: anything, performed_by: anything) + end + end end describe 'POST /api/v1/accounts/{account.id}/conversations/:id/toggle_priority' do From 75a54928408fb07b16f40f2ea1924a1cf762aecc Mon Sep 17 00:00:00 2001 From: Pranav Raj S Date: Fri, 5 Jan 2024 15:28:20 -0800 Subject: [PATCH 6/6] fix: Remove the usage of DragWrapper to fix the Dyte integration (#8655) --- app/javascript/widget/components/AgentMessage.vue | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/app/javascript/widget/components/AgentMessage.vue b/app/javascript/widget/components/AgentMessage.vue index 6020af3dc..8526281ef 100755 --- a/app/javascript/widget/components/AgentMessage.vue +++ b/app/javascript/widget/components/AgentMessage.vue @@ -19,11 +19,7 @@
- +
- +