Compare commits

..
Author SHA1 Message Date
Muhsin Keloth d8c4b10511 feat: Show subscribed notifications 2024-01-08 13:28:58 +05:30
Muhsin Keloth 48cc651a0d fix: review comments 2024-01-08 11:29:23 +05:30
Sojan JoseandGitHub 85284fdcc4 Merge branch 'develop' into CW-2773 2024-01-05 15:39:03 -08:00
Muhsin KelothandGitHub 9a151673c2 Merge branch 'develop' into CW-2773 2024-01-05 12:36:34 +05:30
Muhsin KelothandGitHub 430a4343c2 Merge branch 'develop' into CW-2773 2024-01-05 12:29:31 +05:30
Muhsin Keloth 4285fb20f3 Update notification_finder_spec.rb 2024-01-04 14:27:31 +05:30
Muhsin KelothandGitHub 3474473e38 Merge branch 'develop' into CW-2773 2024-01-03 13:04:30 +05:30
Muhsin Keloth 8275cdd7d1 chore: fix remaining specs 2023-12-20 13:25:01 +05:30
Muhsin Keloth 7605d6c855 chore: Sort notifications based on last_activity_at. 2023-12-20 13:21:33 +05:30
Muhsin KelothandGitHub 7b32bd7c14 Merge branch 'develop' into CW-2773 2023-12-20 13:07:30 +05:30
Muhsin KelothandGitHub c1fae81a2a Merge branch 'develop' into CW-2773 2023-12-20 11:07:26 +05:30
Muhsin Keloth 839d679961 feat: Get notifications based on subscribed notification types. 2023-12-19 18:20:53 +05:30
Muhsin KelothandGitHub 3803923ed1 Merge branch 'develop' into CW-2773 2023-12-15 09:13:31 +05:30
Muhsin KelothandGitHub 523e1defff Merge branch 'develop' into CW-2773 2023-12-14 19:45:28 +05:30
Muhsin Keloth 340ba9bc53 Update notification_finder_spec.rb 2023-12-14 19:44:10 +05:30
Muhsin Keloth ce339d0350 Update notifications_controller.rb 2023-12-14 19:30:16 +05:30
Muhsin Keloth 1906c0ff63 feat: add NotificationFinder 2023-12-14 12:53:01 +05:30
50 changed files with 279 additions and 627 deletions
+1 -1
View File
@@ -553,7 +553,7 @@ GEM
pry-rails (0.3.9)
pry (>= 0.10.4)
public_suffix (5.0.1)
puma (6.4.2)
puma (6.3.1)
nio4r (~> 2.0)
pundit (2.3.0)
activesupport (>= 3.0.0)
@@ -8,8 +8,8 @@ class Api::V1::Accounts::NotificationsController < Api::V1::Accounts::BaseContro
def index
@unread_count = current_user.notifications.where(account_id: current_account.id, read_at: nil).count
@count = notifications.count
@notifications = notifications.page(@current_page).per(RESULTS_PER_PAGE)
@notifications = notification_finder.perform
@count = @notifications.count
end
def read_all
@@ -61,7 +61,7 @@ class Api::V1::Accounts::NotificationsController < Api::V1::Accounts::BaseContro
@current_page = params[:page] || 1
end
def notifications
@notifications ||= current_user.notifications.where(account_id: current_account.id)
def notification_finder
@notification_finder ||= NotificationFinder.new(Current.user, Current.account, params)
end
end
@@ -9,9 +9,6 @@ class SuperAdmin::AppConfigsController < SuperAdmin::ApplicationController
.map { |name, serialized_value| [name, serialized_value['value']] }
.to_h
# rubocop:enable Style/HashTransformValues
@installation_configs = ConfigLoader.new.general_configs.each_with_object({}) do |config_hash, result|
result[config_hash['name']] = config_hash.except('name')
end
end
def create
+65
View File
@@ -0,0 +1,65 @@
class NotificationFinder
attr_reader :current_user, :current_account, :params
RESULTS_PER_PAGE = 15
def initialize(current_user, current_account, params)
@current_user = current_user
@current_account = current_account
@params = params
end
def perform
set_up
notifications
end
private
def set_up
find_all_notifications
filter_by_notification_settings
filter_by_status
end
def find_all_notifications
@notifications = current_user.notifications.where(account_id: @current_account.id)
end
def filter_by_notification_settings
notification_settings = current_user.notification_settings.find_by(account_id: @current_account.id)
subscribed_notification_types = get_subscribed_notification_types(notification_settings)
@notifications = filter_notifications_by_types(@notifications, subscribed_notification_types)
end
def get_subscribed_notification_types(notification_settings)
subscribed_notification_types = []
::Notification::NOTIFICATION_TYPES.each_key do |notification_type|
email_flag = "email_#{notification_type}".to_sym
push_flag = "push_#{notification_type}".to_sym
if notification_settings.public_send(email_flag) || notification_settings.public_send(push_flag)
subscribed_notification_types << notification_type
end
end
subscribed_notification_types
end
def filter_notifications_by_types(notifications, types)
notifications.where(notification_type: types)
end
def filter_by_status
@notifications = @notifications.where('snoozed_until > ?', DateTime.now.utc) if params[:status] == 'snoozed'
end
def current_page
params[:page] || 1
end
def notifications
@notifications.page(current_page).per(RESULTS_PER_PAGE).order(last_activity_at: :desc)
end
end
@@ -76,11 +76,7 @@
&.left {
.bubble {
@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
}
@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;
&.is-image {
@apply rounded-lg;
@@ -158,6 +158,7 @@ export default {
this.$store.dispatch('notifications/unReadCount');
this.$store.dispatch('teams/get');
this.$store.dispatch('attributes/get');
this.$store.dispatch('userNotificationSettings/get');
this.fetchCustomViews();
},
@@ -1,9 +1,5 @@
<template>
<mention-box
v-if="items.length"
:items="items"
@mention-select="handleMentionClick"
>
<mention-box :items="items" @mention-select="handleMentionClick">
<template slot-scope="{ item }">
<strong>{{ item.label }}</strong> - {{ item.description }}
</template>
@@ -29,19 +29,8 @@
: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-else-if="data.content"
v-if="data.content"
:message="message"
:is-email="isEmailContentType"
:display-quoted-button="displayQuotedButton"
@@ -187,14 +176,6 @@ export default {
type: Boolean,
default: false,
},
isAFacebookInbox: {
type: Boolean,
default: false,
},
isInstagram: {
type: Boolean,
default: false,
},
isAWhatsAppChannel: {
type: Boolean,
default: false,
@@ -238,7 +219,6 @@ export default {
this.hasAttachments ||
this.data.content ||
this.isEmailContentType ||
this.isUnsupported ||
this.isAnIntegrationMessage
);
},
@@ -427,7 +407,6 @@ 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,
@@ -436,9 +415,6 @@ export default {
'is-email': this.isEmailContentType,
};
},
isUnsupported() {
return this.contentAttributes.is_unsupported ?? false;
},
isPending() {
return this.data.status === MESSAGE_STATUS.PROGRESS;
},
@@ -450,7 +426,11 @@ export default {
return !this.sender.type || this.sender.type === 'agent_bot';
},
shouldShowContextMenu() {
return !(this.isFailed || this.isPending || this.isUnsupported);
return !(this.isFailed || this.isPending);
},
errorMessage() {
const { meta } = this.data;
return meta ? meta.error : '';
},
showAvatar() {
if (this.isOutgoing || this.isTemplate) {
@@ -552,14 +532,6 @@ 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;
@@ -572,12 +544,10 @@ export default {
> video {
@apply rounded-lg;
}
> video {
@apply h-full w-full object-cover;
}
}
.video {
@apply h-[11.25rem];
}
@@ -592,11 +562,9 @@ 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;
}
@@ -605,7 +573,6 @@ 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;
}
@@ -616,7 +583,6 @@ 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,8 +32,6 @@
: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)"
/>
@@ -56,8 +54,6 @@
: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)"
/>
@@ -287,9 +283,6 @@ 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>
<mention-box
v-if="items.length"
type="variable"
:items="items"
@mention-select="handleVariableClick"
@@ -1,5 +1,6 @@
<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"
>
@@ -10,7 +10,6 @@ export class DataManager {
async initDb() {
if (this.db) return this.db;
const dbName = `cw-store-${this.accountId}`;
this.db = await openDB(`cw-store-${this.accountId}`, DATA_VERSION, {
upgrade(db) {
db.createObjectStore('cache-keys');
@@ -20,13 +19,6 @@ export class DataManager {
},
});
// Store the database name in LocalStorage
const dbNames = JSON.parse(localStorage.getItem('cw-idb-names') || '[]');
if (!dbNames.includes(dbName)) {
dbNames.push(dbName);
localStorage.setItem('cw-idb-names', JSON.stringify(dbNames));
}
return this.db;
}
@@ -41,9 +41,6 @@
"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",
@@ -50,7 +50,6 @@
:is-loading="uiFlags.isFetching"
:on-click-notification="openConversation"
:in-last-page="inLastPage"
@close="closeNotificationPanel"
/>
<div
v-if="records.length !== 0"
@@ -1,103 +0,0 @@
<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>
@@ -1,12 +1,70 @@
<template>
<div class="flex-col py-2 px-2.5 overflow-auto h-full flex">
<notification-panel-item
<woot-button
v-for="notificationItem in notifications"
v-show="!isLoading"
:key="notificationItem.id"
:notification-item="notificationItem"
@open-notification="onClickNotification"
/>
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>
<empty-state
v-if="showEmptyResult"
:title="$t('NOTIFICATIONS_PAGE.UNREAD_NOTIFICATION.EMPTY_MESSAGE')"
@@ -36,16 +94,18 @@
<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 NotificationPanelItem from './NotificationPanelItem.vue';
import timeMixin from '../../../../mixins/time';
export default {
components: {
NotificationPanelItem,
Thumbnail,
Spinner,
EmptyState,
},
mixins: [timeMixin],
props: {
notifications: {
type: Array,
@@ -79,7 +139,9 @@ export default {
name: 'notifications_index',
});
}
this.$emit('close');
},
hasAssignee(notification) {
return notification.primary_actor.meta?.assignee;
},
},
};
@@ -1,14 +1,46 @@
const NOTIFICATION_TYPES = {
conversation_creation: 1,
conversation_assignment: 2,
assigned_conversation_new_message: 3,
conversation_mention: 4,
participating_conversation_new_message: 5,
};
function getSubscribedNotificationTypes(pushFlags, emailFlags) {
return Object.keys(NOTIFICATION_TYPES).filter(notificationType => {
const emailFlag = `email_${notificationType}`;
const pushFlag = `push_${notificationType}`;
return pushFlags.includes(pushFlag) || emailFlags.includes(emailFlag);
});
}
export const getters = {
getNotifications($state) {
return Object.values($state.records).sort((n1, n2) => n2.id - n1.id);
getNotifications($state, __, rootState) {
const notificationSettings =
rootState.userNotificationSettings.record || {};
const pushFlags = notificationSettings.selected_push_flags || {};
const emailFlags = notificationSettings.selected_email_flags || {};
const subscribedNotificationTypes = getSubscribedNotificationTypes(
pushFlags,
emailFlags
);
return Object.values($state.records)
.filter(notification =>
subscribedNotificationTypes.includes(notification.notification_type)
)
.sort((n1, n2) => n2.id - n1.id);
},
getUIFlags($state) {
return $state.uiFlags;
},
getNotification: $state => id => {
const notification = $state.records[id];
return notification || {};
return $state.records[id] || {};
},
getMeta: $state => {
return $state.meta;
},
+3 -22
View File
@@ -44,29 +44,10 @@ export const clearLocalStorageOnLogout = () => {
};
export const deleteIndexedDBOnLogout = async () => {
let dbs = [];
try {
dbs = await window.indexedDB.databases();
dbs = dbs.map(db => db.name);
} catch (e) {
dbs = JSON.parse(localStorage.getItem('cw-idb-names') || '[]');
}
dbs.forEach(dbName => {
const deleteRequest = window.indexedDB.deleteDatabase(dbName);
deleteRequest.onerror = event => {
// eslint-disable-next-line no-console
console.error(`Error deleting database ${dbName}.`, event);
};
deleteRequest.onsuccess = () => {
// eslint-disable-next-line no-console
console.log(`Database ${dbName} deleted successfully.`);
};
const dbs = await window.indexedDB.databases();
dbs.forEach(db => {
window.indexedDB.deleteDatabase(db.name);
});
localStorage.removeItem('cw-idb-names');
};
export const clearCookiesOnLogout = () => {
-3
View File
@@ -51,9 +51,6 @@ if (window.errorLoggingConfig) {
/safari-extension:/i,
],
integrations: [new Integrations.BrowserTracing()],
ignoreErrors: [
'ResizeObserver loop completed with undelivered notifications',
],
});
}
-3
View File
@@ -35,9 +35,6 @@ if (window.errorLoggingConfig) {
/safari-extension:/i,
],
integrations: [new Integrations.BrowserTracing()],
ignoreErrors: [
'ResizeObserver loop completed with undelivered notifications',
],
});
}
+1 -1
View File
@@ -1,6 +1,6 @@
class SendOnSlackJob < MutexApplicationJob
queue_as :medium
retry_on LockAcquisitionError, wait: 1.second, attempts: 8
retry_on LockAcquisitionError, wait: 1.second, attempts: 6
def perform(message, hook)
with_lock(::Redis::Alfred::SLACK_MESSAGE_MUTEX, conversation_id: message.conversation_id, reference_id: hook.reference_id) do
-2
View File
@@ -8,8 +8,6 @@ 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 })
+1 -1
View File
@@ -83,7 +83,7 @@ class ReplyMailbox < ApplicationMailbox
end
def validate_resource(resource)
raise "Email conversation with uuid: #{conversation_uuid} not found" if resource.nil?
raise "#{resource.class.name} not found" if resource.nil?
resource
end
+1 -13
View File
@@ -7,19 +7,11 @@ 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
@@ -64,10 +56,6 @@ 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,
@@ -86,7 +74,7 @@ class SupportMailbox < ApplicationMailbox
end
def find_or_create_contact
@contact = @inbox.contacts.find_by(email: original_sender_email)
@contact = @inbox.contacts.find_by(email: @processed_mail.original_sender&.downcase)
if @contact.present?
@contact_inbox = ContactInbox.find_by(inbox: @inbox, contact: @contact)
else
-4
View File
@@ -47,10 +47,6 @@ class Notification < ApplicationRecord
after_create_commit :process_notification_delivery, :dispatch_create_event
after_destroy_commit :dispatch_destroy_event
# TODO: Get rid of default scope
# https://stackoverflow.com/a/1834250/939299
default_scope { order(id: :desc) }
PRIMARY_ACTORS = ['Conversation'].freeze
def push_event_data
+2 -4
View File
@@ -49,10 +49,8 @@ class ActionService
end
def assign_team(team_ids = [])
return unassign_team if team_ids[0]&.zero?
# check if team belongs to account only if team_id is present
# if team_id is nil, then it means that the team is being unassigned
return unless !team_ids[0].nil? && team_belongs_to_account?(team_ids)
return unassign_team if team_ids[0].zero?
return unless team_belongs_to_account?(team_ids)
@conversation.update!(team_id: team_ids[0])
end
@@ -100,12 +100,6 @@ 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,9 +107,11 @@ class Twilio::IncomingMessageService
def attach_files
return if params[:MediaUrl0].blank?
attachment_file = download_attachment_file
return if attachment_file.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 = @message.attachments.new(
account_id: @message.account_id,
@@ -124,29 +126,4 @@ 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)
Rails.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
nil
end
end
end
@@ -8,26 +8,13 @@
</header>
<section class="main-content__body">
<%= form_with url: super_admin_app_config_url(config: @config) , method: :post do |form| %>
<% @allowed_configs.each do |key| %>
<div class="flex mb-8">
<% @allowed_configs.each do |c| %>
<div class="field-unit">
<div class="field-unit__label">
<%= form.label "app_config[#{key}]", @installation_configs[key]&.dig('display_title') || key %>
<%= form.label "app_config[#{c}]", c %>
</div>
<div class="field-unit__field -mt-2 ">
<% if @installation_configs[key]&.dig('type') == 'boolean' %>
<%= form.select "app_config[#{key}]",
[["True", true], ["False", false]],
{ selected: ActiveModel::Type::Boolean.new.cast(@app_config[key]) },
class: "mt-2 border border-slate-100 p-1 rounded-md"
%>
<% else %>
<%= form.text_field "app_config[#{key}]", value: @app_config[key] %>
<% end %>
<%if @installation_configs[key]&.dig('description').present? %>
<p class="text-slate-400 text-xs italic pt-2">
<%= @installation_configs[key]&.dig('description') %>
</p>
<% end %>
<div class="field-unit__field">
<%= form.text_field "app_config[#{c}]", value: @app_config[c] %>
</div>
</div>
<% end %>
+1 -1
View File
@@ -1,5 +1,5 @@
shared: &shared
version: '3.5.0'
version: '3.4.0'
development:
<<: *shared
+3 -24
View File
@@ -1,46 +1,23 @@
# if you don't specify locked attribute, the default value will be true
# if you dont specify locked attribute, the default value will be true
# which means the particular config will be locked
- name: INSTALLATION_NAME
value: 'Chatwoot'
display_title: 'Installation Name'
description: 'The installation wide name that would be used in the dashboard, title etc.'
- name: LOGO_THUMBNAIL
value: '/brand-assets/logo_thumbnail.svg'
display_title: 'Logo Thumbnail'
description: 'The thumbnail that would be used for favicon (512px X 512px)'
- name: LOGO
value: '/brand-assets/logo.svg'
display_title: 'Logo'
description: 'The logo that would be used on the dashboard, login page etc.'
- name: LOGO_DARK
value: '/brand-assets/logo_dark.svg'
display_title: 'Logo Dark Mode'
description: 'The logo that would be used on the dashboard, login page etc. for dark mode'
- name: BRAND_URL
value: 'https://www.chatwoot.com'
display_title: 'Brand URL'
description: 'The URL that would be used in emails under the section “Powered By”'
- name: WIDGET_BRAND_URL
value: 'https://www.chatwoot.com'
display_title: 'Widget Brand URL'
description: 'The URL that would be used in the widget under the section “Powered By”'
- name: BRAND_NAME
value: 'Chatwoot'
display_title: 'Brand Name'
description: 'The name that would be used in emails and the widget'
- name: TERMS_URL
value: 'https://www.chatwoot.com/terms-of-service'
display_title: 'Terms URL'
description: 'The terms of service URL displayed in Signup Page'
- name: PRIVACY_URL
value: 'https://www.chatwoot.com/privacy-policy'
display_title: 'Privacy URL'
description: 'The privacy policy URL displayed in the app'
- name: DISPLAY_MANIFEST
value: true
display_title: 'Chatwoot Metadata'
description: 'Display default Chatwoot metadata like favicons and upgrade warnings'
type: boolean
- name: MAILER_INBOUND_EMAIL_DOMAIN
value:
locked: false
@@ -95,6 +72,8 @@
value: self-hosted
- name: CSML_EDITOR_HOST
value:
- name: LOGO_DARK
value: '/brand-assets/logo_dark.svg'
- name: INSTALLATION_PRICING_PLAN
value: 'community'
- name: INSTALLATION_PRICING_PLAN_QUANTITY
+2 -2
View File
@@ -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) == "false" ? false : true %>
# If `true`, the agent captures log records emitted by this application.
enabled: <%= ENV.fetch('NEW_RELIC_APPLICATION_LOGGING_FORWARDING_ENABLED', true) %>
# Defines the maximum number of log records to buffer in memory at a time.
max_samples_stored: 30000
metrics:
@@ -16,7 +16,6 @@ module Enterprise::SuperAdmin::AppConfigsController
WIDGET_BRAND_URL
TERMS_URL
PRIVACY_URL
DISPLAY_MANIFEST
]
else
super
@@ -21,9 +21,6 @@ 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,
@@ -33,13 +30,5 @@ 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,8 +14,7 @@ class Enterprise::Billing::HandleStripeEventService
private
def process_subscription_updated
plan = find_plan(subscription['plan']['product']) if subscription['plan'].present?
plan = find_plan(subscription['plan']['product'])
# skipping self hosted plan events
return if plan.blank? || account.blank?
+2 -2
View File
@@ -4,7 +4,7 @@ class ChatGpt
end
def initialize(context_sections = '')
@model = 'gpt-4-1106-preview'
@model = 'gpt-4'
@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, response_format: { type: 'json_object' } }.to_json
body = { model: @model, messages: @messages }.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}"
+2 -3
View File
@@ -26,13 +26,12 @@ class ConfigLoader
reconcile_feature_config
end
private
def general_configs
@config_path ||= Rails.root.join('config')
@general_configs ||= YAML.safe_load(File.read("#{@config_path}/installation_config.yml")).freeze
end
private
def account_features
@account_features ||= YAML.safe_load(File.read("#{@config_path}/features.yml")).freeze
end
-9
View File
@@ -12,7 +12,6 @@ class GlobalConfig
config[config_key] = load_from_cache(config_key)
end
typecast_config(config)
config.with_indifferent_access
end
@@ -29,14 +28,6 @@ class GlobalConfig
private
def typecast_config(config)
general_configs = ConfigLoader.new.general_configs
config.each do |config_key, config_value|
config_type = general_configs.find { |c| c['name'] == config_key }&.dig('type')
config[config_key] = ActiveRecord::Type::Boolean.new.cast(config_value) if config_type == 'boolean'
end
end
def load_from_cache(config_key)
cache_key = "#{VERSION}:#{KEY_PREFIX}:#{config_key}"
cached_value = $alfred.with { |conn| conn.get(cache_key) }
@@ -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, Slack::Web::Api::Errors::NotInChannel => e
Slack::Web::Api::Errors::ChannelNotFound => e
Rails.logger.error e
hook.prompt_reauthorization!
hook.disable
+3 -3
View File
@@ -12,10 +12,10 @@
# end
#
class Redis::LockManager
# Default lock timeout set to 1 second. This means that if the lock isn't released
# within 1 second, it will automatically expire.
# Default lock timeout set to 2 seconds. This means that if the lock isn't released
# within 2 seconds, it will automatically expire.
# This helps to avoid deadlocks in case the process holding the lock crashes or fails to release it.
LOCK_TIMEOUT = 1.second
LOCK_TIMEOUT = 2.seconds
# Attempts to acquire a lock for the given key.
#
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@chatwoot/chatwoot",
"version": "3.5.0",
"version": "3.4.0",
"license": "MIT",
"scripts": {
"eslint": "eslint app/**/*.{js,vue}",
@@ -31,7 +31,7 @@
],
"dependencies": {
"@braid/vue-formulate": "^2.5.2",
"@chatwoot/prosemirror-schema": "1.0.5",
"@chatwoot/prosemirror-schema": "1.0.3",
"@chatwoot/utils": "^0.0.21",
"@hcaptcha/vue-hcaptcha": "^0.3.2",
"@june-so/analytics-next": "^1.36.5",
-52
View File
@@ -97,56 +97,4 @@ 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
+55
View File
@@ -0,0 +1,55 @@
require 'rails_helper'
describe NotificationFinder do
subject(:notification_finder) { described_class.new(user, account, params) }
let!(:account) { create(:account) }
let!(:user) { create(:user, account: account) }
before do
create(:notification, account: account, user: user, updated_at: DateTime.now.utc + 1.day, last_activity_at: DateTime.now.utc + 1.day)
create(:notification, account: account, user: user, snoozed_until: DateTime.now.utc + 3.days, updated_at: DateTime.now.utc,
last_activity_at: DateTime.now.utc)
create(:notification, account: account, user: user, updated_at: DateTime.now.utc + 2.days, last_activity_at: DateTime.now.utc + 2.days)
create(:notification, account: account, user: user, updated_at: DateTime.now.utc + 4.days, last_activity_at: DateTime.now.utc + 4.days,
notification_type: :conversation_creation)
create(:notification, account: account, user: user, updated_at: DateTime.now.utc + 5.days, last_activity_at: DateTime.now.utc + 5.days,
notification_type: :conversation_mention)
create(:notification, account: account, user: user, updated_at: DateTime.now.utc + 6.days, last_activity_at: DateTime.now.utc + 6.days,
notification_type: :participating_conversation_new_message)
end
describe '#perform' do
context 'when params are empty' do
let(:params) { {} }
it 'returns notifications which aren not snoozed' do
result = notification_finder.perform
expect(result.length).to be 3
end
it 'orders notifications by last activity at' do
result = notification_finder.perform
expect(result.first.last_activity_at).to be > result.last.last_activity_at
end
it 'filter notifications by user preferences' do
notification_setting = user.notification_settings.find_by(account_id: account.id)
notification_setting.selected_email_flags = [:email_conversation_creation]
notification_setting.save!
result = notification_finder.perform
expect(result.length).to be 4
end
end
context 'when snoozed param is passed' do
let(:params) { { status: 'snoozed' } }
it 'returns only snoozed notifications' do
result = notification_finder.perform
expect(result.length).to be 1
end
end
end
end
-102
View File
@@ -1,102 +0,0 @@
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
-15
View File
@@ -42,11 +42,6 @@ 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}")
@@ -64,11 +59,6 @@ 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}")
@@ -87,11 +77,6 @@ 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}")
-11
View File
@@ -16,17 +16,6 @@ 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) }
-5
View File
@@ -39,10 +39,5 @@ describe ActionService do
action_service.assign_agent(['nil'])
expect(conversation.reload.assignee).to be_nil
end
it 'unassigns the team if team_id is nil' do
action_service.assign_team([nil])
expect(conversation.reload.team).to be_nil
end
end
end
@@ -215,26 +215,6 @@ 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,65 +169,5 @@ 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
+4 -4
View File
@@ -3156,10 +3156,10 @@
"@braid/vue-formulate-i18n" "^1.16.0"
is-plain-object "^3.0.1"
"@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==
"@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==
dependencies:
markdown-it-sup "^1.0.0"
prosemirror-commands "^1.1.4"