From 6e300644218667f1bfaa4a10aec690c862de63bd Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Sat, 6 Jan 2024 02:35:00 +0530 Subject: [PATCH 01/23] 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 02/23] 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 03/23] 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 04/23] 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 05/23] 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 @@
- +
- +
Date: Mon, 8 Jan 2024 16:51:30 -0800 Subject: [PATCH 06/23] chore(deps): bump puma from 6.3.1 to 6.4.2 (#8663) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index ebadf20ef..8d8714fd8 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -553,7 +553,7 @@ GEM pry-rails (0.3.9) pry (>= 0.10.4) public_suffix (5.0.1) - puma (6.3.1) + puma (6.4.2) nio4r (~> 2.0) pundit (2.3.0) activesupport (>= 3.0.0) From 046ce68a45063436a3a4c7e82aeb91e0fa612eaf Mon Sep 17 00:00:00 2001 From: Sojan Jose Date: Mon, 8 Jan 2024 17:02:25 -0800 Subject: [PATCH 07/23] chore: Improve Openai json rendering (#8666) We have been observing JSON parsing errors for responses from GPT. Switching to the gpt-4-1106-preview model along with using response_format has significantly improved the responses from OpenAI, hence making the switch in code. ref: https://openai.com/blog/new-models-and-developer-products-announced-at-devday fixes: #CW-2931 --- enterprise/lib/chat_gpt.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/enterprise/lib/chat_gpt.rb b/enterprise/lib/chat_gpt.rb index 4065d3686..9b09569d0 100644 --- a/enterprise/lib/chat_gpt.rb +++ b/enterprise/lib/chat_gpt.rb @@ -4,7 +4,7 @@ class ChatGpt end def initialize(context_sections = '') - @model = 'gpt-4' + @model = 'gpt-4-1106-preview' @messages = [system_message(context_sections)] end @@ -53,7 +53,7 @@ class ChatGpt def request_gpt headers = { 'Content-Type' => 'application/json', 'Authorization' => "Bearer #{ENV.fetch('OPENAI_API_KEY')}" } - body = { model: @model, messages: @messages }.to_json + body = { model: @model, messages: @messages, response_format: { type: 'json_object' } }.to_json Rails.logger.info "Requesting Chat GPT with body: #{body}" response = HTTParty.post("#{self.class.base_uri}/v1/chat/completions", headers: headers, body: body) Rails.logger.info "Chat GPT response: #{response.body}" From 64138ef2205049b037a5aadc084bf6773b719a6e Mon Sep 17 00:00:00 2001 From: Muhsin Keloth Date: Wed, 10 Jan 2024 04:00:17 +0530 Subject: [PATCH 08/23] chore: Rescue `Slack::Web::Api::Errors::NotInChannel` error (#8670) The primary cause of this issue is when Chatwoot sends a message to a channel that has either been deleted or is unauthorized. So, we will prompt reauthorization when this error occurs. Fixes https://linear.app/chatwoot/issue/CW-2930/slackwebapierrorsnotinchannel-not-in-channel --- lib/integrations/slack/send_on_slack_service.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/integrations/slack/send_on_slack_service.rb b/lib/integrations/slack/send_on_slack_service.rb index c840dd1d9..50a69e02b 100644 --- a/lib/integrations/slack/send_on_slack_service.rb +++ b/lib/integrations/slack/send_on_slack_service.rb @@ -97,7 +97,7 @@ class Integrations::Slack::SendOnSlackService < Base::SendOnChannelService 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, - Slack::Web::Api::Errors::ChannelNotFound => e + Slack::Web::Api::Errors::ChannelNotFound, Slack::Web::Api::Errors::NotInChannel => e Rails.logger.error e hook.prompt_reauthorization! hook.disable From 5845881b08bfa08fb25d951bbb050354d5183938 Mon Sep 17 00:00:00 2001 From: Sojan Jose Date: Tue, 9 Jan 2024 14:48:07 -0800 Subject: [PATCH 09/23] chore: Handle stripe events without plan data (#8668) `plan` isn't a guaranteed object in stripe `customer.subscription.updated` events. It can be null for cases like `send_invoice` for `past_due` event as seen in the payload shown in sentry error. fixes: https://linear.app/chatwoot/issue/CW-2925/nomethoderror-undefined-method-[]-for-nilnilclass-nomethoderror --- .../services/enterprise/billing/handle_stripe_event_service.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/enterprise/app/services/enterprise/billing/handle_stripe_event_service.rb b/enterprise/app/services/enterprise/billing/handle_stripe_event_service.rb index fecc52e03..1ddf00d7a 100644 --- a/enterprise/app/services/enterprise/billing/handle_stripe_event_service.rb +++ b/enterprise/app/services/enterprise/billing/handle_stripe_event_service.rb @@ -14,7 +14,8 @@ class Enterprise::Billing::HandleStripeEventService private def process_subscription_updated - plan = find_plan(subscription['plan']['product']) + plan = find_plan(subscription['plan']['product']) if subscription['plan'].present? + # skipping self hosted plan events return if plan.blank? || account.blank? From d731c972ad5620f425f2c92be9ca7c6b3201a61b Mon Sep 17 00:00:00 2001 From: Vishnu Narayanan Date: Wed, 10 Jan 2024 11:21:48 +0530 Subject: [PATCH 10/23] fix: skip auditlogs for whatsapp template sync (#8579) Skips audit logs for whatsapp_template sync Fixes: https://linear.app/chatwoot/issue/CW-2641/skip-whatsapp-template-updates-from-audit-logs --- .../app/models/enterprise/channelable.rb | 11 ++++ spec/enterprise/models/inbox_spec.rb | 52 +++++++++++++++++++ 2 files changed, 63 insertions(+) diff --git a/enterprise/app/models/enterprise/channelable.rb b/enterprise/app/models/enterprise/channelable.rb index e46fdb2be..6fcae73d8 100644 --- a/enterprise/app/models/enterprise/channelable.rb +++ b/enterprise/app/models/enterprise/channelable.rb @@ -21,6 +21,9 @@ module Enterprise::Channelable return if audited_changes.blank? + # skip audit log creation if the only change is whatsapp channel template update + return if messaging_template_updates?(audited_changes) + Enterprise::AuditLog.create( auditable_id: auditable_id, auditable_type: auditable_type, @@ -30,5 +33,13 @@ module Enterprise::Channelable audited_changes: audited_changes ) end + + def messaging_template_updates?(changes) + # if there is more than one key, return false + return false unless changes.keys.length == 1 + + # if the only key is message_templates_last_updated, return true + changes.key?('message_templates_last_updated') + end end end diff --git a/spec/enterprise/models/inbox_spec.rb b/spec/enterprise/models/inbox_spec.rb index 2cc7eeea9..3e3e060d8 100644 --- a/spec/enterprise/models/inbox_spec.rb +++ b/spec/enterprise/models/inbox_spec.rb @@ -97,4 +97,56 @@ RSpec.describe Inbox do end end end + + describe 'audit log with whatsapp channel' do + let(:channel) { create(:channel_whatsapp, provider: 'whatsapp_cloud', sync_templates: false, validate_provider_config: false) } + let(:inbox) { channel.inbox } + + before do + stub_request(:get, 'https://graph.facebook.com/v14.0//message_templates?access_token=test_key') + .with( + headers: { + 'Accept' => '*/*', + 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', + 'User-Agent' => 'Ruby' + } + ) + .to_return(status: 200, body: '', headers: {}) + end + + context 'when inbox is created' do + it 'has associated audit log created' do + expect(Audited::Audit.where(auditable_type: 'Inbox', action: 'create').count).to eq(1) + end + end + + context 'when inbox is updated' do + it 'has associated audit log created' do + inbox.update(name: 'Updated Inbox') + expect(Audited::Audit.where(auditable_type: 'Inbox', action: 'update').count).to eq(1) + end + end + + context 'when channel is updated' do + it 'has associated audit log created' do + previous_phone_number = inbox.channel.phone_number + new_phone_number = '1234567890' + inbox.channel.update(phone_number: new_phone_number) + + # check if channel update creates an audit log against inbox + expect(Audited::Audit.where(auditable_type: 'Inbox', action: 'update').count).to eq(1) + # Check for the specific phone_number update in the audit log + expect(Audited::Audit.where(auditable_type: 'Inbox', action: 'update', + audited_changes: { 'phone_number' => [previous_phone_number, new_phone_number] }).count).to eq(1) + end + end + + context 'when template sync runs' do + it 'has no associated audit log created' do + channel.sync_templates + # check if template sync does not create an audit log + expect(Audited::Audit.where(auditable_type: 'Inbox', action: 'update').count).to eq(0) + end + end + end end From 50b2ca014e215d941dc6e4ee498549f13d4a7ad9 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Wed, 10 Jan 2024 11:23:58 +0530 Subject: [PATCH 11/23] feat: UI for unsupported message (#8660) Display an unsupported message in UI when handling unsupported messages from channels like facebook, Instagram etc. Co-authored-by: Pranav Raj S Co-authored-by: Sojan Jose --- .../scss/widgets/_conversation-view.scss | 6 ++- .../widgets/conversation/Message.vue | 46 ++++++++++++++++--- .../widgets/conversation/MessagesView.vue | 7 +++ .../i18n/locale/en/conversation.json | 3 ++ 4 files changed, 55 insertions(+), 7 deletions(-) diff --git a/app/javascript/dashboard/assets/scss/widgets/_conversation-view.scss b/app/javascript/dashboard/assets/scss/widgets/_conversation-view.scss index e538299aa..5cc3caa27 100644 --- a/app/javascript/dashboard/assets/scss/widgets/_conversation-view.scss +++ b/app/javascript/dashboard/assets/scss/widgets/_conversation-view.scss @@ -76,7 +76,11 @@ &.left { .bubble { - @apply border border-slate-50 dark:border-slate-700 bg-white dark:bg-slate-700 text-black-900 dark:text-slate-50 rounded-r-lg rounded-l mr-auto break-words; + @apply rounded-r-lg rounded-l mr-auto break-words; + + &:not(.is-unsupported) { + @apply border border-slate-50 dark:border-slate-700 bg-white dark:bg-slate-700 text-black-900 dark:text-slate-50 + } &.is-image { @apply rounded-lg; diff --git a/app/javascript/dashboard/components/widgets/conversation/Message.vue b/app/javascript/dashboard/components/widgets/conversation/Message.vue index 34ad5f59f..8eb03137b 100644 --- a/app/javascript/dashboard/components/widgets/conversation/Message.vue +++ b/app/javascript/dashboard/components/widgets/conversation/Message.vue @@ -29,8 +29,19 @@ :message-type="data.message_type" :parent-has-attachments="hasAttachments" /> +
+ + + +
.bubble { @apply min-w-[128px]; + &.is-unsupported { + @apply text-xs max-w-[300px] border-dashed border border-slate-200 text-slate-600 dark:text-slate-200 bg-slate-50 dark:bg-slate-700 dark:border-slate-500; + + .message-text--metadata .time { + @apply text-slate-400 dark:text-slate-300; + } + } + &.is-image, &.is-video { @apply p-0 overflow-hidden; @@ -544,10 +572,12 @@ export default { > video { @apply rounded-lg; } + > video { @apply h-full w-full object-cover; } } + .video { @apply h-[11.25rem]; } @@ -562,9 +592,11 @@ export default { .file--icon { @apply text-woot-400 dark:text-woot-400; } + .text-block-title { @apply text-slate-700 dark:text-slate-700; } + .download.button { @apply text-woot-400 dark:text-woot-400; } @@ -573,6 +605,7 @@ export default { &.is-private.is-text > .message-text__wrap .link { @apply text-woot-600 dark:text-woot-200; } + &.is-private.is-text > .message-text__wrap .prosemirror-mention-node { @apply font-bold bg-none rounded-sm p-0 bg-yellow-100 dark:bg-yellow-700 text-slate-700 dark:text-slate-25 underline; } @@ -583,6 +616,7 @@ export default { .message-text--metadata .time { @apply text-violet-50 dark:text-violet-50; } + &.is-private .message-text--metadata .time { @apply text-slate-400 dark:text-slate-400; } diff --git a/app/javascript/dashboard/components/widgets/conversation/MessagesView.vue b/app/javascript/dashboard/components/widgets/conversation/MessagesView.vue index d9e255c6d..62eb6d739 100644 --- a/app/javascript/dashboard/components/widgets/conversation/MessagesView.vue +++ b/app/javascript/dashboard/components/widgets/conversation/MessagesView.vue @@ -32,6 +32,8 @@ :is-a-tweet="isATweet" :is-a-whatsapp-channel="isAWhatsAppChannel" :is-web-widget-inbox="isAWebWidgetInbox" + :is-a-facebook-inbox="isAFacebookInbox" + :is-instagram="isInstagramDM" :inbox-supports-reply-to="inboxSupportsReplyTo" :in-reply-to="getInReplyToMessage(message)" /> @@ -54,6 +56,8 @@ :is-a-tweet="isATweet" :is-a-whatsapp-channel="isAWhatsAppChannel" :is-web-widget-inbox="isAWebWidgetInbox" + :is-a-facebook-inbox="isAFacebookInbox" + :is-instagram-dm="isInstagramDM" :inbox-supports-reply-to="inboxSupportsReplyTo" :in-reply-to="getInReplyToMessage(message)" /> @@ -283,6 +287,9 @@ export default { unreadMessageCount() { return this.currentChat.unread_count || 0; }, + isInstagramDM() { + return this.conversationType === 'instagram_direct_message'; + }, inboxSupportsReplyTo() { const incoming = this.inboxHasFeature(INBOX_FEATURES.REPLY_TO); const outgoing = diff --git a/app/javascript/dashboard/i18n/locale/en/conversation.json b/app/javascript/dashboard/i18n/locale/en/conversation.json index 19012abed..227c802d6 100644 --- a/app/javascript/dashboard/i18n/locale/en/conversation.json +++ b/app/javascript/dashboard/i18n/locale/en/conversation.json @@ -41,6 +41,9 @@ "SAVE_CONTACT": "Save", "UPLOADING_ATTACHMENTS": "Uploading attachments...", "REPLIED_TO_STORY": "Replied to your story", + "UNSUPPORTED_MESSAGE": "This message is unsupported.", + "UNSUPPORTED_MESSAGE_FACEBOOK": "This message is unsupported. You can view this message on the Facebook Messenger app.", + "UNSUPPORTED_MESSAGE_INSTAGRAM": "This message is unsupported. You can view this message on the Instagram app.", "SUCCESS_DELETE_MESSAGE": "Message deleted successfully", "FAIL_DELETE_MESSSAGE": "Couldn't delete message! Try again", "NO_RESPONSE": "No response", From 3c21f624859239fb9a8e57ed5c6467cbf8ca8a8d Mon Sep 17 00:00:00 2001 From: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Date: Thu, 11 Jan 2024 01:14:13 +0530 Subject: [PATCH 12/23] fix: Error ResizeObserver loop completed with undelivered notifications. (#8680) --- app/javascript/packs/application.js | 3 +++ app/javascript/packs/v3app.js | 3 +++ 2 files changed, 6 insertions(+) diff --git a/app/javascript/packs/application.js b/app/javascript/packs/application.js index e15071303..18354bc62 100644 --- a/app/javascript/packs/application.js +++ b/app/javascript/packs/application.js @@ -51,6 +51,9 @@ if (window.errorLoggingConfig) { /safari-extension:/i, ], integrations: [new Integrations.BrowserTracing()], + ignoreErrors: [ + 'ResizeObserver loop completed with undelivered notifications', + ], }); } diff --git a/app/javascript/packs/v3app.js b/app/javascript/packs/v3app.js index b510a587c..37318d738 100644 --- a/app/javascript/packs/v3app.js +++ b/app/javascript/packs/v3app.js @@ -35,6 +35,9 @@ if (window.errorLoggingConfig) { /safari-extension:/i, ], integrations: [new Integrations.BrowserTracing()], + ignoreErrors: [ + 'ResizeObserver loop completed with undelivered notifications', + ], }); } From aaf4fee9a666e2c85a970e5d1dc1bfe709f6e8aa Mon Sep 17 00:00:00 2001 From: Sojan Jose Date: Wed, 10 Jan 2024 14:30:23 -0800 Subject: [PATCH 13/23] chore: Fix sentry errors in email processing for bounce notifications (#8677) This case occurs for bounce notification emails where the from address is From: "" (Mail Delivery System) . We will be discarding these emails for now. Fixes: https://linear.app/chatwoot/issue/CW-2793/activerecordrecordinvalid-validation-failed-email-invalid-email --- app/mailboxes/support_mailbox.rb | 14 ++- spec/fixtures/files/bounced_with_no_from.eml | 102 +++++++++++++++++++ spec/mailboxes/support_mailbox_spec.rb | 11 ++ 3 files changed, 126 insertions(+), 1 deletion(-) create mode 100644 spec/fixtures/files/bounced_with_no_from.eml diff --git a/app/mailboxes/support_mailbox.rb b/app/mailboxes/support_mailbox.rb index ca7f39a9a..5a1f5ecf5 100644 --- a/app/mailboxes/support_mailbox.rb +++ b/app/mailboxes/support_mailbox.rb @@ -7,11 +7,19 @@ class SupportMailbox < ApplicationMailbox :decorate_mail def process + Rails.logger.info "Processing email #{mail.message_id} from #{original_sender_email} to #{mail.to} with subject #{mail.subject}" + # to turn off spam conversation creation return unless @account.active? # prevent loop from chatwoot notification emails return if notification_email_from_chatwoot? + # return if email doesn't have a valid sender + # This can happen in cases like bounce emails for invalid contact email address + # TODO: Handle the bounce seperately and mark the contact as invalid + # we are checking for @ since the returned value could be "\"\"" for some email clients + return unless original_sender_email.include?('@') + ActiveRecord::Base.transaction do find_or_create_contact find_or_create_conversation @@ -56,6 +64,10 @@ class SupportMailbox < ApplicationMailbox mail['In-Reply-To'].try(:value) end + def original_sender_email + @processed_mail.original_sender&.downcase + end + def find_or_create_conversation @conversation = find_conversation_by_in_reply_to || ::Conversation.create!({ account_id: @account.id, @@ -74,7 +86,7 @@ class SupportMailbox < ApplicationMailbox end def find_or_create_contact - @contact = @inbox.contacts.find_by(email: @processed_mail.original_sender&.downcase) + @contact = @inbox.contacts.find_by(email: original_sender_email) if @contact.present? @contact_inbox = ContactInbox.find_by(inbox: @inbox, contact: @contact) else diff --git a/spec/fixtures/files/bounced_with_no_from.eml b/spec/fixtures/files/bounced_with_no_from.eml new file mode 100644 index 000000000..9057a6823 --- /dev/null +++ b/spec/fixtures/files/bounced_with_no_from.eml @@ -0,0 +1,102 @@ +X-Original-To: unique-id@reply.example.com +Received: from gate.forward.smtp.example.com (mxd [192.0.2.1]) by mx.example.net with ESMTP id JANE3UihQWCm3SPLwYMiwA for ; Mon, 01 Jan 2024 08:27:12.905 +0000 (UTC) +Return-Path: <> +X-Virus-Scanned: OK +Authentication-Results: smtp6.gate.example.com; iprev=pass policy.iprev="192.0.2.2"; spf=neutral smtp.mailfrom="" smtp.helo="backend.example.com"; dkim=none (message not signed) header.d=none +X-Suspicious-Flag: NO +X-Classification-ID: 9026a23e-a87f-11ee-b226-52540050e3e0-1-1 +Received: from [192.0.2.2] ([192.0.2.2:52052] helo=backend.example.com) + by smtp6.gate.example.com (envelope-from <>) + (ecelerity 4.2.38.62370 r(:)) with ESMTPS (cipher=DHE-RSA-AES256-GCM-SHA384) + id 69/60-03303-06772956; Mon, 01 Jan 2024 03:27:12 -0500 +Received: by backend.example.com (Postfix, from userid 5000) + id BE58147CF5; Mon, 1 Jan 2024 03:27:12 -0500 (EST) +X-Sieve: Pigeonhole Sieve 0.5.12 (f22f7ab3) +X-Sieve-Redirected-From: support@example.com +Delivered-To: support@example.com +Delivered-To: support@example.com +Received: from director.example.com ([192.0.2.3]) + by backend.example.com with LMTP + id AB5kLWB3kmV0bgAAStNUoA + (envelope-from <>) + for ; Mon, 01 Jan 2024 03:27:12 -0500 +Received: from proxy.example.com ([192.0.2.3]) + by director.example.com with LMTP + id 0BPqLGB3kmWXKQAAfY0hYg + (envelope-from <>) + for ; Mon, 01 Jan 2024 03:27:12 -0500 +Received: from smtp.example.com ([192.0.2.3]) + (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) + by proxy.example.com with LMTPS + id yGPPLGB3kmXQNwAAyH2SIw + (envelope-from <>) + for ; Mon, 01 Jan 2024 03:27:12 -0500 +X-Spam-Threshold: 95 +X-Spam-Score: 0 +X-Spam-Flag: NO +X-Virus-Scanned: OK +X-Orig-To: support@example.com +X-Originating-Ip: [192.0.2.4] +Received: from [192.0.2.4] ([192.0.2.4:47194] helo=smtp.example.com) + by smtp36.gate.example.com (envelope-from <>) + (ecelerity 4.2.38.62370 r(:)) with ESMTPS (cipher=DHE-RSA-AES256-GCM-SHA384) + id 57/13-02844-06772956; Mon, 01 Jan 2024 03:27:12 -0500 +Received: by smtp5.relay.example.com (SMTP Server) + id 8D329A008A; Mon, 1 Jan 2024 03:27:12 -0500 (EST) +Date: Mon, 1 Jan 2024 03:27:12 -0500 (EST) +From: "" (Mail Delivery System) +Subject: Undelivered Mail Returned to Sender +To: support@example.com +Auto-Submitted: auto-replied +MIME-Version: 1.0 +Content-Type: multipart/report; report-type=delivery-status; + boundary="AE732A0081.1704097632/smtp5.relay.example.com" +Message-Id: <20240101082712.8D329A008A@smtp5.relay.example.com> + +This is a MIME-encapsulated message. + +--AE732A0081.1704097632/smtp5.relay.example.com +Content-Description: Notification +Content-Type: text/plain; charset=us-ascii + +This is the mail system at host smtp5.relay.example.com. + +I'm sorry to have to inform you that your message could not +be delivered to one or more recipients. It's attached below. + +For further assistance, please send mail to postmaster. + +If you do so, please include this problem report. You can +delete your own text from the attached returned message. + + The mail system + +: host + mx.example-service.com.cust.a.hostedemail.com[198.51.100.4] said: 554 5.7.1 + : Recipient address rejected: user + noreply@example-service.com does not exist (in reply to RCPT TO command) + +--AE732A0081.1704097632/smtp5.relay.example.com +Content-Description: Delivery report +Content-Type: message/delivery-status + +Reporting-MTA: dns; smtp5.relay.example.com +X-SMTP-Server-Queue-ID: AE732A0081 +X-SMTP-Server-Sender: rfc822; support@example.com +Arrival-Date: Mon, 1 Jan 2024 03:27:11 -0500 (EST) + +Final-Recipient: rfc822; noreply@example-service.com +Original-Recipient: rfc822;noreply@example-service.com +Action: failed +Status: 5.7.1 +Remote-MTA: dns; mx.example-service.com.cust.a.hostedemail.com +Diagnostic-Code: smtp; 554 5.7.1 : Recipient + address rejected: user noreply@example-service.com does not exist + +--AE732A0081.1704097632/smtp5.relay.example.com +Content-Description: Undelivered Message +Content-Type: message/rfc822 + +Return-Path: +X-Milter-Dummy: +DKIM-Signature: v=1; a=rsa-sha256; c diff --git a/spec/mailboxes/support_mailbox_spec.rb b/spec/mailboxes/support_mailbox_spec.rb index d2e2c7946..f9e9aa2ff 100644 --- a/spec/mailboxes/support_mailbox_spec.rb +++ b/spec/mailboxes/support_mailbox_spec.rb @@ -16,6 +16,17 @@ RSpec.describe SupportMailbox do end end + describe 'when bounced email with out a sender is recieved' do + let(:account) { create(:account) } + let(:bounced_email) { create_inbound_email_from_fixture('bounced_with_no_from.eml') } + let(:described_subject) { described_class.receive bounced_email } + + it 'shouldnt throw an error' do + create(:channel_email, email: 'support@example.com', account: account) + expect { described_subject }.not_to raise_error + end + end + describe 'when an account is suspended' do let(:account) { create(:account, status: :suspended) } let(:agent) { create(:user, email: 'agent1@example.com', account: account) } From 35c26367da3fc85f195a693132155a919c11732d Mon Sep 17 00:00:00 2001 From: Sojan Jose Date: Wed, 10 Jan 2024 15:32:48 -0800 Subject: [PATCH 14/23] chore: Unset Redis config after spec (#8685) The sentinel configuration set in this specification seems to be affecting other specifications. So, let's ensure that the memoised config variable gets unset after execution of the spec. --- spec/lib/redis/config_spec.rb | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/spec/lib/redis/config_spec.rb b/spec/lib/redis/config_spec.rb index 79dc8b03d..296ede54b 100644 --- a/spec/lib/redis/config_spec.rb +++ b/spec/lib/redis/config_spec.rb @@ -42,6 +42,11 @@ describe Redis::Config do end end + after do + # ensuring the redis config is unset and won't affect other tests + described_class.instance_variable_set(:@config, nil) + end + it 'checks for app redis config' do expect(described_class.app.keys).to contain_exactly(:url, :password, :sentinels, :timeout, :reconnect_attempts, :ssl_params) expect(described_class.app[:url]).to eq("redis://#{redis_master_name}") @@ -59,6 +64,11 @@ describe Redis::Config do end end + after do + # ensuring the redis config is unset and won't affect other tests + described_class.instance_variable_set(:@config, nil) + end + it 'checks for app redis config and sentinel passwords will be empty' do expect(described_class.app.keys).to contain_exactly(:url, :password, :sentinels, :timeout, :reconnect_attempts, :ssl_params) expect(described_class.app[:url]).to eq("redis://#{redis_master_name}") @@ -77,6 +87,11 @@ describe Redis::Config do end end + after do + # ensuring the redis config is unset and won't affect other tests + described_class.instance_variable_set(:@config, nil) + end + it 'checks for app redis config and redis password is replaced in sentinel config' do expect(described_class.app.keys).to contain_exactly(:url, :password, :sentinels, :timeout, :reconnect_attempts, :ssl_params) expect(described_class.app[:url]).to eq("redis://#{redis_master_name}") From 22c2235d908926ae3505d9125f299844ed46ba44 Mon Sep 17 00:00:00 2001 From: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Date: Thu, 11 Jan 2024 11:31:02 +0530 Subject: [PATCH 15/23] fix: TypeError: Cannot read properties of null (reading 'assignee') (#8647) Co-authored-by: Muhsin Keloth --- .../components/NotificationPanel.vue | 1 + .../components/NotificationPanelItem.vue | 103 ++++++++++++++++++ .../components/NotificationPanelList.vue | 76 ++----------- 3 files changed, 111 insertions(+), 69 deletions(-) create mode 100644 app/javascript/dashboard/routes/dashboard/notifications/components/NotificationPanelItem.vue diff --git a/app/javascript/dashboard/routes/dashboard/notifications/components/NotificationPanel.vue b/app/javascript/dashboard/routes/dashboard/notifications/components/NotificationPanel.vue index 256ad3758..e5b008d8b 100644 --- a/app/javascript/dashboard/routes/dashboard/notifications/components/NotificationPanel.vue +++ b/app/javascript/dashboard/routes/dashboard/notifications/components/NotificationPanel.vue @@ -50,6 +50,7 @@ :is-loading="uiFlags.isFetching" :on-click-notification="openConversation" :in-last-page="inLastPage" + @close="closeNotificationPanel" />
+
+ +
+
+
+
+
+
+ + {{ + `#${ + notificationItem.primary_actor + ? notificationItem.primary_actor.id + : $t(`NOTIFICATIONS_PAGE.DELETE_TITLE`) + }` + }} + + + {{ + $t( + `NOTIFICATIONS_PAGE.TYPE_LABEL.${notificationItem.notification_type}` + ) + }} + +
+
+ +
+
+
+ + {{ notificationItem.push_message_title }} + +
+ + {{ dynamicTime(notificationItem.created_at) }} + +
+
+ +
+ + + diff --git a/app/javascript/dashboard/routes/dashboard/notifications/components/NotificationPanelList.vue b/app/javascript/dashboard/routes/dashboard/notifications/components/NotificationPanelList.vue index bf2aed625..9ae08b144 100644 --- a/app/javascript/dashboard/routes/dashboard/notifications/components/NotificationPanelList.vue +++ b/app/javascript/dashboard/routes/dashboard/notifications/components/NotificationPanelList.vue @@ -1,70 +1,12 @@