Merge branch 'develop' into fix/cw_2914
This commit is contained in:
+1
-1
@@ -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)
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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!
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
<template>
|
||||
<mention-box :items="items" @mention-select="handleMentionClick">
|
||||
<mention-box
|
||||
v-if="items.length"
|
||||
:items="items"
|
||||
@mention-select="handleMentionClick"
|
||||
>
|
||||
<template slot-scope="{ item }">
|
||||
<strong>{{ item.label }}</strong> - {{ item.description }}
|
||||
</template>
|
||||
|
||||
@@ -29,8 +29,19 @@
|
||||
:message-type="data.message_type"
|
||||
:parent-has-attachments="hasAttachments"
|
||||
/>
|
||||
<div v-if="isUnsupported">
|
||||
<template v-if="isAFacebookInbox && isInstagram">
|
||||
{{ $t('CONVERSATION.UNSUPPORTED_MESSAGE_INSTAGRAM') }}
|
||||
</template>
|
||||
<template v-else-if="isAFacebookInbox">
|
||||
{{ $t('CONVERSATION.UNSUPPORTED_MESSAGE_FACEBOOK') }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ $t('CONVERSATION.UNSUPPORTED_MESSAGE') }}
|
||||
</template>
|
||||
</div>
|
||||
<bubble-text
|
||||
v-if="data.content"
|
||||
v-else-if="data.content"
|
||||
:message="message"
|
||||
:is-email="isEmailContentType"
|
||||
:display-quoted-button="displayQuotedButton"
|
||||
@@ -176,6 +187,14 @@ export default {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isAFacebookInbox: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isInstagram: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isAWhatsAppChannel: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
@@ -219,6 +238,7 @@ export default {
|
||||
this.hasAttachments ||
|
||||
this.data.content ||
|
||||
this.isEmailContentType ||
|
||||
this.isUnsupported ||
|
||||
this.isAnIntegrationMessage
|
||||
);
|
||||
},
|
||||
@@ -407,6 +427,7 @@ export default {
|
||||
return {
|
||||
bubble: this.isBubble,
|
||||
'is-private': this.data.private,
|
||||
'is-unsupported': this.isUnsupported,
|
||||
'is-image': this.hasMediaAttachment('image'),
|
||||
'is-video': this.hasMediaAttachment('video'),
|
||||
'is-text': this.hasText,
|
||||
@@ -415,6 +436,9 @@ export default {
|
||||
'is-email': this.isEmailContentType,
|
||||
};
|
||||
},
|
||||
isUnsupported() {
|
||||
return this.contentAttributes.is_unsupported ?? false;
|
||||
},
|
||||
isPending() {
|
||||
return this.data.status === MESSAGE_STATUS.PROGRESS;
|
||||
},
|
||||
@@ -426,11 +450,7 @@ export default {
|
||||
return !this.sender.type || this.sender.type === 'agent_bot';
|
||||
},
|
||||
shouldShowContextMenu() {
|
||||
return !(this.isFailed || this.isPending);
|
||||
},
|
||||
errorMessage() {
|
||||
const { meta } = this.data;
|
||||
return meta ? meta.error : '';
|
||||
return !(this.isFailed || this.isPending || this.isUnsupported);
|
||||
},
|
||||
showAvatar() {
|
||||
if (this.isOutgoing || this.isTemplate) {
|
||||
@@ -532,6 +552,14 @@ export default {
|
||||
> .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;
|
||||
}
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="items.length"
|
||||
ref="mentionsListContainer"
|
||||
class="bg-white dark:bg-slate-800 rounded-md overflow-auto absolute w-full z-20 pb-0 shadow-md left-0 bottom-full max-h-[9.75rem] border border-solid border-slate-100 dark:border-slate-700 mention--box"
|
||||
>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
+1
@@ -50,6 +50,7 @@
|
||||
:is-loading="uiFlags.isFetching"
|
||||
:on-click-notification="openConversation"
|
||||
:in-last-page="inLastPage"
|
||||
@close="closeNotificationPanel"
|
||||
/>
|
||||
<div
|
||||
v-if="records.length !== 0"
|
||||
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
<template>
|
||||
<div>
|
||||
<woot-button
|
||||
size="expanded"
|
||||
color-scheme="secondary"
|
||||
variant="link"
|
||||
@click="onClickOpenNotification()"
|
||||
>
|
||||
<div
|
||||
class="flex-row items-center p-2.5 leading-[1.4] border-b border-solid border-slate-50 dark:border-slate-700 flex w-full hover:bg-slate-75 dark:hover:bg-slate-900 hover:rounded-md"
|
||||
>
|
||||
<div
|
||||
v-if="!notificationItem.read_at"
|
||||
class="w-2 h-2 rounded-full bg-woot-500"
|
||||
/>
|
||||
<div v-else class="w-2 flex" />
|
||||
<div
|
||||
class="flex-col ml-2.5 overflow-hidden w-full flex justify-between"
|
||||
>
|
||||
<div class="flex justify-between">
|
||||
<div class="items-center flex">
|
||||
<span class="font-bold text-slate-800 dark:text-slate-100">
|
||||
{{
|
||||
`#${
|
||||
notificationItem.primary_actor
|
||||
? notificationItem.primary_actor.id
|
||||
: $t(`NOTIFICATIONS_PAGE.DELETE_TITLE`)
|
||||
}`
|
||||
}}
|
||||
</span>
|
||||
<span
|
||||
class="text-xxs p-0.5 px-1 my-0 mx-2 bg-slate-50 dark:bg-slate-700 rounded-md"
|
||||
>
|
||||
{{
|
||||
$t(
|
||||
`NOTIFICATIONS_PAGE.TYPE_LABEL.${notificationItem.notification_type}`
|
||||
)
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="hasNotificationAssignee">
|
||||
<thumbnail
|
||||
:src="notificationAssigneeThumbnail"
|
||||
size="16px"
|
||||
:username="notificationAssigneeName"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full flex">
|
||||
<span
|
||||
class="text-slate-700 dark:text-slate-200 font-normal overflow-hidden whitespace-nowrap text-ellipsis"
|
||||
>
|
||||
{{ notificationItem.push_message_title }}
|
||||
</span>
|
||||
</div>
|
||||
<span
|
||||
class="mt-1 text-slate-500 dark:text-slate-400 text-xxs font-semibold flex"
|
||||
>
|
||||
{{ dynamicTime(notificationItem.created_at) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</woot-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
||||
import timeMixin from 'dashboard/mixins/time';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Thumbnail,
|
||||
},
|
||||
mixins: [timeMixin],
|
||||
props: {
|
||||
notificationItem: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
notificationAssignee() {
|
||||
const { primary_actor: primaryActor } = this.notificationItem;
|
||||
return primaryActor?.meta?.assignee;
|
||||
},
|
||||
hasNotificationAssignee() {
|
||||
return !!this.notificationAssignee;
|
||||
},
|
||||
notificationAssigneeName() {
|
||||
return this.notificationAssignee?.name || '';
|
||||
},
|
||||
notificationAssigneeThumbnail() {
|
||||
return this.notificationAssignee?.thumbnail || '';
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onClickOpenNotification() {
|
||||
this.$emit('open-notification', this.notificationItem);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
+7
-69
@@ -1,70 +1,12 @@
|
||||
<template>
|
||||
<div class="flex-col py-2 px-2.5 overflow-auto h-full flex">
|
||||
<woot-button
|
||||
<notification-panel-item
|
||||
v-for="notificationItem in notifications"
|
||||
v-show="!isLoading"
|
||||
:key="notificationItem.id"
|
||||
size="expanded"
|
||||
color-scheme="secondary"
|
||||
variant="link"
|
||||
@click="() => onClickNotification(notificationItem)"
|
||||
>
|
||||
<div
|
||||
class="flex-row items-center p-2.5 leading-[1.4] border-b border-solid border-slate-50 dark:border-slate-700 flex w-full hover:bg-slate-75 dark:hover:bg-slate-900 hover:rounded-md"
|
||||
>
|
||||
<div
|
||||
v-if="!notificationItem.read_at"
|
||||
class="w-2 h-2 rounded-full bg-woot-500"
|
||||
/>
|
||||
<div v-else class="w-2 flex" />
|
||||
<div
|
||||
class="flex-col ml-2.5 overflow-hidden w-full flex justify-between"
|
||||
>
|
||||
<div class="flex justify-between">
|
||||
<div class="items-center flex">
|
||||
<span class="font-bold text-slate-800 dark:text-slate-100">
|
||||
{{
|
||||
`#${
|
||||
notificationItem.primary_actor
|
||||
? notificationItem.primary_actor.id
|
||||
: $t(`NOTIFICATIONS_PAGE.DELETE_TITLE`)
|
||||
}`
|
||||
}}
|
||||
</span>
|
||||
<span
|
||||
class="text-xxs p-0.5 px-1 my-0 mx-2 bg-slate-50 dark:bg-slate-700 rounded-md"
|
||||
>
|
||||
{{
|
||||
$t(
|
||||
`NOTIFICATIONS_PAGE.TYPE_LABEL.${notificationItem.notification_type}`
|
||||
)
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<thumbnail
|
||||
v-if="hasAssignee(notificationItem)"
|
||||
:src="notificationItem.primary_actor.meta.assignee.thumbnail"
|
||||
size="16px"
|
||||
:username="notificationItem.primary_actor.meta.assignee.name"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full flex">
|
||||
<span
|
||||
class="text-slate-700 dark:text-slate-200 font-normal overflow-hidden whitespace-nowrap text-ellipsis"
|
||||
>
|
||||
{{ notificationItem.push_message_title }}
|
||||
</span>
|
||||
</div>
|
||||
<span
|
||||
class="mt-1 text-slate-500 dark:text-slate-400 text-xxs font-semibold flex"
|
||||
>
|
||||
{{ dynamicTime(notificationItem.created_at) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</woot-button>
|
||||
:notification-item="notificationItem"
|
||||
@open-notification="onClickNotification"
|
||||
/>
|
||||
<empty-state
|
||||
v-if="showEmptyResult"
|
||||
:title="$t('NOTIFICATIONS_PAGE.UNREAD_NOTIFICATION.EMPTY_MESSAGE')"
|
||||
@@ -94,18 +36,16 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
||||
import Spinner from 'shared/components/Spinner.vue';
|
||||
import EmptyState from 'dashboard/components/widgets/EmptyState.vue';
|
||||
import timeMixin from '../../../../mixins/time';
|
||||
import NotificationPanelItem from './NotificationPanelItem.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Thumbnail,
|
||||
NotificationPanelItem,
|
||||
Spinner,
|
||||
EmptyState,
|
||||
},
|
||||
mixins: [timeMixin],
|
||||
props: {
|
||||
notifications: {
|
||||
type: Array,
|
||||
@@ -139,9 +79,7 @@ export default {
|
||||
name: 'notifications_index',
|
||||
});
|
||||
}
|
||||
},
|
||||
hasAssignee(notification) {
|
||||
return notification.primary_actor.meta?.assignee;
|
||||
this.$emit('close');
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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
|
||||
},
|
||||
];
|
||||
@@ -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],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -51,6 +51,9 @@ if (window.errorLoggingConfig) {
|
||||
/safari-extension:/i,
|
||||
],
|
||||
integrations: [new Integrations.BrowserTracing()],
|
||||
ignoreErrors: [
|
||||
'ResizeObserver loop completed with undelivered notifications',
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -35,6 +35,9 @@ if (window.errorLoggingConfig) {
|
||||
/safari-extension:/i,
|
||||
],
|
||||
integrations: [new Integrations.BrowserTracing()],
|
||||
ignoreErrors: [
|
||||
'ResizeObserver loop completed with undelivered notifications',
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -19,11 +19,7 @@
|
||||
<reply-to-chip :reply-to="replyTo" />
|
||||
</div>
|
||||
<div class="flex gap-1">
|
||||
<drag-wrapper
|
||||
class="space-y-2"
|
||||
direction="right"
|
||||
@dragged="toggleReply"
|
||||
>
|
||||
<div class="space-y-2">
|
||||
<AgentMessageBubble
|
||||
v-if="shouldDisplayAgentMessage"
|
||||
:content-type="contentType"
|
||||
@@ -54,7 +50,7 @@
|
||||
<file-bubble v-else :url="attachment.data_url" />
|
||||
</div>
|
||||
</div>
|
||||
</drag-wrapper>
|
||||
</div>
|
||||
<div class="flex flex-col justify-end">
|
||||
<message-reply-button
|
||||
class="transition-opacity delay-75 opacity-0 group-hover:opacity-100 sm:opacity-0"
|
||||
@@ -96,7 +92,6 @@ import messageMixin from '../mixins/messageMixin';
|
||||
import { isASubmittedFormMessage } from 'shared/helpers/MessageTypeHelper';
|
||||
import darkModeMixin from 'widget/mixins/darkModeMixin.js';
|
||||
import ReplyToChip from 'widget/components/ReplyToChip.vue';
|
||||
import DragWrapper from 'widget/components/DragWrapper.vue';
|
||||
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||
|
||||
export default {
|
||||
@@ -109,7 +104,6 @@ export default {
|
||||
FileBubble,
|
||||
MessageReplyButton,
|
||||
ReplyToChip,
|
||||
DragWrapper,
|
||||
},
|
||||
mixins: [timeMixin, configMixin, messageMixin, darkModeMixin],
|
||||
props: {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
class SendOnSlackJob < MutexApplicationJob
|
||||
queue_as :medium
|
||||
retry_on LockAcquisitionError, wait: 1.second, attempts: 6
|
||||
retry_on LockAcquisitionError, wait: 1.second, attempts: 8
|
||||
|
||||
def perform(message, hook)
|
||||
with_lock(::Redis::Alfred::SLACK_MESSAGE_MUTEX, conversation_id: message.conversation_id, reference_id: hook.reference_id) do
|
||||
|
||||
@@ -8,6 +8,8 @@ class ActionCableListener < BaseListener
|
||||
end
|
||||
|
||||
def notification_deleted(event)
|
||||
return if event.data[:notification].user.blank?
|
||||
|
||||
notification, account, unread_count, count = extract_notification_and_account(event)
|
||||
tokens = [event.data[:notification].user.pubsub_token]
|
||||
broadcast(account, tokens, NOTIFICATION_DELETED, { notification: notification.push_event_data, unread_count: unread_count, count: count })
|
||||
|
||||
@@ -83,7 +83,7 @@ class ReplyMailbox < ApplicationMailbox
|
||||
end
|
||||
|
||||
def validate_resource(resource)
|
||||
raise "#{resource.class.name} not found" if resource.nil?
|
||||
raise "Email conversation with uuid: #{conversation_uuid} not found" if resource.nil?
|
||||
|
||||
resource
|
||||
end
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -100,6 +100,12 @@ class Telegram::IncomingMessageService
|
||||
def attach_files
|
||||
return unless file
|
||||
|
||||
file_download_path = inbox.channel.get_telegram_file_path(file[:file_id])
|
||||
if file_download_path.blank?
|
||||
Rails.logger.info "Telegram file download path is blank for #{file[:file_id]} : inbox_id: #{inbox.id}"
|
||||
return
|
||||
end
|
||||
|
||||
attachment_file = Down.download(
|
||||
inbox.channel.get_telegram_file_path(file[:file_id])
|
||||
)
|
||||
|
||||
@@ -107,11 +107,7 @@ class Twilio::IncomingMessageService
|
||||
def attach_files
|
||||
return if params[:MediaUrl0].blank?
|
||||
|
||||
attachment_file = Down.download(
|
||||
params[:MediaUrl0],
|
||||
# https://support.twilio.com/hc/en-us/articles/223183748-Protect-Media-Access-with-HTTP-Basic-Authentication-for-Programmable-Messaging
|
||||
http_basic_authentication: [twilio_channel.account_sid, twilio_channel.auth_token || twilio_channel.api_key_sid]
|
||||
)
|
||||
attachment_file = download_attachment_file
|
||||
|
||||
attachment = @message.attachments.new(
|
||||
account_id: @message.account_id,
|
||||
@@ -126,4 +122,28 @@ class Twilio::IncomingMessageService
|
||||
|
||||
@message.save!
|
||||
end
|
||||
|
||||
def download_attachment_file
|
||||
download_with_auth
|
||||
rescue Down::Error => e
|
||||
handle_download_attachment_error(e)
|
||||
end
|
||||
|
||||
def download_with_auth
|
||||
Down.download(
|
||||
params[:MediaUrl0],
|
||||
# https://support.twilio.com/hc/en-us/articles/223183748-Protect-Media-Access-with-HTTP-Basic-Authentication-for-Programmable-Messaging
|
||||
http_basic_authentication: [twilio_channel.account_sid, twilio_channel.auth_token || twilio_channel.api_key_sid]
|
||||
)
|
||||
end
|
||||
|
||||
# This is just a temporary workaround since some users have not yet enabled media protection. We will remove this in the future.
|
||||
def handle_download_attachment_error(error)
|
||||
logger.info "Error downloading attachment from Twilio: #{error.message}"
|
||||
if error.message.include?('401 Unauthorized')
|
||||
Down.download(params[:MediaUrl0])
|
||||
else
|
||||
ChatwootExceptionTracker.new(error, account: @inbox.account).capture_exception
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
+2
-2
@@ -29,8 +29,8 @@ common: &default_settings
|
||||
# independently. If `false`, all logging-related features are disabled.
|
||||
enabled: <%= ENV.fetch('NEW_RELIC_APPLICATION_LOGGING_ENABLED', false) %>
|
||||
forwarding:
|
||||
# If `true`, the agent captures log records emitted by this application.
|
||||
enabled: <%= ENV.fetch('NEW_RELIC_APPLICATION_LOGGING_FORWARDING_ENABLED', true) %>
|
||||
# If `true`, the agent captures log records emitted by this application
|
||||
enabled: <%= ENV.fetch('NEW_RELIC_APPLICATION_LOGGING_FORWARDING_ENABLED', true) == "false" ? false : true %>
|
||||
# Defines the maximum number of log records to buffer in memory at a time.
|
||||
max_samples_stored: 30000
|
||||
metrics:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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?
|
||||
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -12,10 +12,10 @@
|
||||
# end
|
||||
#
|
||||
class Redis::LockManager
|
||||
# Default lock timeout set to 2 seconds. This means that if the lock isn't released
|
||||
# within 2 seconds, it will automatically expire.
|
||||
# Default lock timeout set to 1 second. This means that if the lock isn't released
|
||||
# within 1 second, it will automatically expire.
|
||||
# This helps to avoid deadlocks in case the process holding the lock crashes or fails to release it.
|
||||
LOCK_TIMEOUT = 2.seconds
|
||||
LOCK_TIMEOUT = 1.second
|
||||
|
||||
# Attempts to acquire a lock for the given key.
|
||||
#
|
||||
|
||||
+1
-1
@@ -31,7 +31,7 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"@braid/vue-formulate": "^2.5.2",
|
||||
"@chatwoot/prosemirror-schema": "1.0.3",
|
||||
"@chatwoot/prosemirror-schema": "1.0.5",
|
||||
"@chatwoot/utils": "^0.0.21",
|
||||
"@hcaptcha/vue-hcaptcha": "^0.3.2",
|
||||
"@june-so/analytics-next": "^1.36.5",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
[
|
||||
|
||||
+102
@@ -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 <unique-id@reply.example.com>; 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 <support@example.com>; 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 <support@example.com>; 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 <support@example.com>; 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
|
||||
|
||||
<noreply@example-service.com>: host
|
||||
mx.example-service.com.cust.a.hostedemail.com[198.51.100.4] said: 554 5.7.1
|
||||
<noreply@example-service.com>: 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 <noreply@example-service.com>: 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: <support@example.com>
|
||||
X-Milter-Dummy:
|
||||
DKIM-Signature: v=1; a=rsa-sha256; c
|
||||
@@ -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
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -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) }
|
||||
|
||||
@@ -215,6 +215,26 @@ describe Telegram::IncomingMessageService do
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the API call to get the download path returns an error' do
|
||||
it 'does not process the attachment' do
|
||||
allow(telegram_channel.inbox.channel).to receive(:get_telegram_file_path).and_return(nil)
|
||||
params = {
|
||||
'update_id' => 2_342_342_343_242,
|
||||
'message' => {
|
||||
'document' => {
|
||||
'file_id' => 'AwADBAADbXXXXXXXXXXXGBdhD2l6_XX',
|
||||
'file_name' => 'Screenshot 2021-09-27 at 2.01.14 PM.png',
|
||||
'mime_type' => 'application/png',
|
||||
'file_size' => 536_392
|
||||
}
|
||||
}.merge(message_params)
|
||||
}.with_indifferent_access
|
||||
|
||||
described_class.new(inbox: telegram_channel.inbox, params: params).perform
|
||||
expect(telegram_channel.inbox.messages.first.attachments.count).to eq(0)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when valid location message params' do
|
||||
it 'creates appropriate conversations, message and contacts' do
|
||||
params = {
|
||||
|
||||
@@ -169,5 +169,65 @@ describe Twilio::IncomingMessageService do
|
||||
expect(twilio_sms_channel.inbox.conversations.count).to eq(1)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a message with an attachment is received' do
|
||||
before do
|
||||
stub_request(:get, 'https://chatwoot-assets.local/sample.png')
|
||||
.to_return(status: 200, body: 'image data', headers: {})
|
||||
end
|
||||
|
||||
let(:params_with_attachment) do
|
||||
{
|
||||
SmsSid: 'SMxx',
|
||||
From: '+12345',
|
||||
AccountSid: 'ACxxx',
|
||||
MessagingServiceSid: twilio_channel.messaging_service_sid,
|
||||
Body: 'testing3',
|
||||
NumMedia: '1',
|
||||
MediaContentType0: 'image/jpeg',
|
||||
MediaUrl0: 'https://chatwoot-assets.local/sample.png'
|
||||
}
|
||||
end
|
||||
|
||||
it 'creates a new message with media in existing conversation' do
|
||||
described_class.new(params: params_with_attachment).perform
|
||||
expect(conversation.reload.messages.last.content).to eq('testing3')
|
||||
expect(conversation.reload.messages.last.attachments.count).to eq(1)
|
||||
expect(conversation.reload.messages.last.attachments.first.file_type).to eq('image')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when there is an error downloading the attachment' do
|
||||
before do
|
||||
stub_request(:get, 'https://chatwoot-assets.local/sample.png')
|
||||
.to_raise(Down::Error.new('Download error'))
|
||||
|
||||
stub_request(:get, 'https://chatwoot-assets.local/sample.png')
|
||||
.to_return(status: 200, body: 'image data', headers: {})
|
||||
end
|
||||
|
||||
let(:params_with_attachment_error) do
|
||||
{
|
||||
SmsSid: 'SMxx',
|
||||
From: '+12345',
|
||||
AccountSid: 'ACxxx',
|
||||
MessagingServiceSid: twilio_channel.messaging_service_sid,
|
||||
Body: 'testing3',
|
||||
NumMedia: '1',
|
||||
MediaContentType0: 'image/jpeg',
|
||||
MediaUrl0: 'https://chatwoot-assets.local/sample.png'
|
||||
}
|
||||
end
|
||||
|
||||
it 'retries downloading the attachment without a token after an error' do
|
||||
expect do
|
||||
described_class.new(params: params_with_attachment_error).perform
|
||||
end.not_to raise_error
|
||||
|
||||
expect(conversation.reload.messages.last.content).to eq('testing3')
|
||||
expect(conversation.reload.messages.last.attachments.count).to eq(1)
|
||||
expect(conversation.reload.messages.last.attachments.first.file_type).to eq('image')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -3156,10 +3156,10 @@
|
||||
"@braid/vue-formulate-i18n" "^1.16.0"
|
||||
is-plain-object "^3.0.1"
|
||||
|
||||
"@chatwoot/prosemirror-schema@1.0.3":
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@chatwoot/prosemirror-schema/-/prosemirror-schema-1.0.3.tgz#8599e517d42cb31fabf977554ade666eb6316ef0"
|
||||
integrity sha512-BIVxV7c8x0vbWtWxGPk/VnBrtC1CV0TzZd+GPZC49irVcQQ2vAwgOYaZ/1qcFe9M3jv0kWAWOPqQAfbB5RBB7g==
|
||||
"@chatwoot/prosemirror-schema@1.0.5":
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/@chatwoot/prosemirror-schema/-/prosemirror-schema-1.0.5.tgz#d6053692beae59d466ac0b04128fa157f59eb176"
|
||||
integrity sha512-dOzkZ2K53PPbE9AQB0RHlVs+GIEyHHdXeeW44dNSEuULwH99PmTpzA2r45QX3uaVa2j7Mip76AQbJZGKbM2fxg==
|
||||
dependencies:
|
||||
markdown-it-sup "^1.0.0"
|
||||
prosemirror-commands "^1.1.4"
|
||||
|
||||
Reference in New Issue
Block a user