Merge branch 'hotfix/1.22.1' into develop
# Conflicts: # db/schema.rb
This commit is contained in:
commit
0033a35ab8
38 changed files with 211 additions and 95 deletions
|
@ -48,11 +48,10 @@ class ContactMergeAction
|
||||||
|
|
||||||
# attributes in base contact are given preference
|
# attributes in base contact are given preference
|
||||||
merged_attributes = mergee_contact_attributes.deep_merge(base_contact_attributes)
|
merged_attributes = mergee_contact_attributes.deep_merge(base_contact_attributes)
|
||||||
# retaining old pubsub token to notify the contacts that are listening
|
|
||||||
mergee_pubsub_token = mergee_contact.pubsub_token
|
|
||||||
|
|
||||||
@mergee_contact.destroy!
|
@mergee_contact.destroy!
|
||||||
Rails.configuration.dispatcher.dispatch(CONTACT_MERGED, Time.zone.now, contact: @base_contact, tokens: [mergee_pubsub_token])
|
Rails.configuration.dispatcher.dispatch(CONTACT_MERGED, Time.zone.now, contact: @base_contact,
|
||||||
|
tokens: [@base_contact.contact_inboxes.filter_map(&:pubsub_token)])
|
||||||
@base_contact.update!(merged_attributes)
|
@base_contact.update!(merged_attributes)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -31,7 +31,7 @@ class RoomChannel < ApplicationCable::Channel
|
||||||
|
|
||||||
def current_user
|
def current_user
|
||||||
@current_user ||= if params[:user_id].blank?
|
@current_user ||= if params[:user_id].blank?
|
||||||
Contact.find_by!(pubsub_token: @pubsub_token)
|
ContactInbox.find_by!(pubsub_token: @pubsub_token).contact
|
||||||
else
|
else
|
||||||
User.find_by!(pubsub_token: @pubsub_token, id: params[:user_id])
|
User.find_by!(pubsub_token: @pubsub_token, id: params[:user_id])
|
||||||
end
|
end
|
||||||
|
|
|
@ -29,21 +29,21 @@ class WidgetsController < ActionController::Base
|
||||||
def set_contact
|
def set_contact
|
||||||
return if @auth_token_params[:source_id].nil?
|
return if @auth_token_params[:source_id].nil?
|
||||||
|
|
||||||
contact_inbox = ::ContactInbox.find_by(
|
@contact_inbox = ::ContactInbox.find_by(
|
||||||
inbox_id: @web_widget.inbox.id,
|
inbox_id: @web_widget.inbox.id,
|
||||||
source_id: @auth_token_params[:source_id]
|
source_id: @auth_token_params[:source_id]
|
||||||
)
|
)
|
||||||
|
|
||||||
@contact = contact_inbox ? contact_inbox.contact : nil
|
@contact = @contact_inbox ? @contact_inbox.contact : nil
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_contact
|
def build_contact
|
||||||
return if @contact.present?
|
return if @contact.present?
|
||||||
|
|
||||||
contact_inbox = @web_widget.create_contact_inbox(additional_attributes)
|
@contact_inbox = @web_widget.create_contact_inbox(additional_attributes)
|
||||||
@contact = contact_inbox.contact
|
@contact = @contact_inbox.contact
|
||||||
|
|
||||||
payload = { source_id: contact_inbox.source_id, inbox_id: @web_widget.inbox.id }
|
payload = { source_id: @contact_inbox.source_id, inbox_id: @web_widget.inbox.id }
|
||||||
@token = ::Widget::TokenService.new(payload: payload).generate_token
|
@token = ::Widget::TokenService.new(payload: payload).generate_token
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -121,7 +121,7 @@ class ConversationFinder
|
||||||
|
|
||||||
def conversations
|
def conversations
|
||||||
@conversations = @conversations.includes(
|
@conversations = @conversations.includes(
|
||||||
:taggings, :inbox, { assignee: { avatar_attachment: [:blob] } }, { contact: { avatar_attachment: [:blob] } }, :team
|
:taggings, :inbox, { assignee: { avatar_attachment: [:blob] } }, { contact: { avatar_attachment: [:blob] } }, :team, :contact_inbox
|
||||||
)
|
)
|
||||||
@conversations.latest.page(current_page)
|
@conversations.latest.page(current_page)
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,6 +2,10 @@
|
||||||
margin-right: var(--space-small);
|
margin-right: var(--space-small);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.margin-right-smaller {
|
||||||
|
margin-right: var(--space-smaller);
|
||||||
|
}
|
||||||
|
|
||||||
.fs-small {
|
.fs-small {
|
||||||
font-size: var(--font-size-small);
|
font-size: var(--font-size-small);
|
||||||
}
|
}
|
||||||
|
@ -42,3 +46,7 @@
|
||||||
.bg-white {
|
.bg-white {
|
||||||
background-color: var(--white);
|
background-color: var(--white);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.text-y-800 {
|
||||||
|
color: var(--y-800);
|
||||||
|
}
|
||||||
|
|
|
@ -10,7 +10,12 @@
|
||||||
/>
|
/>
|
||||||
<div class="user--profile__meta">
|
<div class="user--profile__meta">
|
||||||
<h3 class="user--name text-truncate">
|
<h3 class="user--name text-truncate">
|
||||||
{{ currentContact.name }}
|
<span class="margin-right-smaller">{{ currentContact.name }}</span>
|
||||||
|
<i
|
||||||
|
v-if="!isHMACVerified"
|
||||||
|
v-tooltip="$t('CONVERSATION.UNVERIFIED_SESSION')"
|
||||||
|
class="ion-android-alert text-y-800 fs-default"
|
||||||
|
/>
|
||||||
</h3>
|
</h3>
|
||||||
<div class="conversation--header--actions">
|
<div class="conversation--header--actions">
|
||||||
<inbox-name :inbox="inbox" class="margin-right-small" />
|
<inbox-name :inbox="inbox" class="margin-right-small" />
|
||||||
|
@ -73,11 +78,15 @@ export default {
|
||||||
uiFlags: 'inboxAssignableAgents/getUIFlags',
|
uiFlags: 'inboxAssignableAgents/getUIFlags',
|
||||||
currentChat: 'getSelectedChat',
|
currentChat: 'getSelectedChat',
|
||||||
}),
|
}),
|
||||||
|
|
||||||
chatMetadata() {
|
chatMetadata() {
|
||||||
return this.chat.meta;
|
return this.chat.meta;
|
||||||
},
|
},
|
||||||
|
isHMACVerified() {
|
||||||
|
if (!this.isAWebWidgetInbox) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return this.chatMetadata.hmac_verified;
|
||||||
|
},
|
||||||
currentContact() {
|
currentContact() {
|
||||||
return this.$store.getters['contacts/getContact'](
|
return this.$store.getters['contacts/getContact'](
|
||||||
this.chat.meta.sender.id
|
this.chat.meta.sender.id
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
{
|
{
|
||||||
"CONVERSATION": {
|
"CONVERSATION": {
|
||||||
"404": "Please select a conversation from left pane",
|
"404": "Please select a conversation from left pane",
|
||||||
|
"UNVERIFIED_SESSION": "The identity of this user is not verified",
|
||||||
"NO_MESSAGE_1": "Uh oh! Looks like there are no messages from customers in your inbox.",
|
"NO_MESSAGE_1": "Uh oh! Looks like there are no messages from customers in your inbox.",
|
||||||
"NO_MESSAGE_2": " to send a message to your page!",
|
"NO_MESSAGE_2": " to send a message to your page!",
|
||||||
"NO_INBOX_1": "Hola! Looks like you haven't added any inboxes yet.",
|
"NO_INBOX_1": "Hola! Looks like you haven't added any inboxes yet.",
|
||||||
|
|
|
@ -41,17 +41,17 @@
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row" v-if="isAnEmailInbox">
|
<div v-if="isAnEmailInbox" class="row">
|
||||||
<div class="columns">
|
<div class="columns">
|
||||||
<label :class="{ error: $v.message.$error }">
|
<label :class="{ error: $v.subject.$error }">
|
||||||
{{ $t('NEW_CONVERSATION.FORM.SUBJECT.LABEL') }}
|
{{ $t('NEW_CONVERSATION.FORM.SUBJECT.LABEL') }}
|
||||||
<input
|
<input
|
||||||
v-model="subject"
|
v-model="subject"
|
||||||
type="text"
|
type="text"
|
||||||
:placeholder="$t('NEW_CONVERSATION.FORM.SUBJECT.PLACEHOLDER')"
|
:placeholder="$t('NEW_CONVERSATION.FORM.SUBJECT.PLACEHOLDER')"
|
||||||
@input="$v.message.$touch"
|
@input="$v.subject.$touch"
|
||||||
/>
|
/>
|
||||||
<span v-if="$v.message.$error" class="message">
|
<span v-if="$v.subject.$error" class="message">
|
||||||
{{ $t('NEW_CONVERSATION.FORM.SUBJECT.ERROR') }}
|
{{ $t('NEW_CONVERSATION.FORM.SUBJECT.ERROR') }}
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
|
@ -93,7 +93,7 @@ import Thumbnail from 'dashboard/components/widgets/Thumbnail';
|
||||||
import alertMixin from 'shared/mixins/alertMixin';
|
import alertMixin from 'shared/mixins/alertMixin';
|
||||||
import { INBOX_TYPES } from 'shared/mixins/inboxMixin';
|
import { INBOX_TYPES } from 'shared/mixins/inboxMixin';
|
||||||
import { ExceptionWithMessage } from 'shared/helpers/CustomErrors';
|
import { ExceptionWithMessage } from 'shared/helpers/CustomErrors';
|
||||||
import { required } from 'vuelidate/lib/validators';
|
import { required, requiredIf } from 'vuelidate/lib/validators';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
@ -120,7 +120,7 @@ export default {
|
||||||
},
|
},
|
||||||
validations: {
|
validations: {
|
||||||
subject: {
|
subject: {
|
||||||
required,
|
required: requiredIf('isAnEmailInbox'),
|
||||||
},
|
},
|
||||||
message: {
|
message: {
|
||||||
required,
|
required,
|
||||||
|
|
|
@ -15,18 +15,6 @@ class ActionCableConnector extends BaseActionCableConnector {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
static refreshConnector = pubsubToken => {
|
|
||||||
if (!pubsubToken || window.chatwootPubsubToken === pubsubToken) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
window.chatwootPubsubToken = pubsubToken;
|
|
||||||
window.actionCable.disconnect();
|
|
||||||
window.actionCable = new ActionCableConnector(
|
|
||||||
window.WOOT_WIDGET,
|
|
||||||
window.chatwootPubsubToken
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
onStatusChange = data => {
|
onStatusChange = data => {
|
||||||
this.app.$store.dispatch('conversationAttributes/update', data);
|
this.app.$store.dispatch('conversationAttributes/update', data);
|
||||||
};
|
};
|
||||||
|
@ -57,7 +45,7 @@ class ActionCableConnector extends BaseActionCableConnector {
|
||||||
|
|
||||||
onTypingOn = data => {
|
onTypingOn = data => {
|
||||||
if (data.is_private) {
|
if (data.is_private) {
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
this.clearTimer();
|
this.clearTimer();
|
||||||
this.app.$store.dispatch('conversation/toggleAgentTyping', {
|
this.app.$store.dispatch('conversation/toggleAgentTyping', {
|
||||||
|
@ -88,7 +76,4 @@ class ActionCableConnector extends BaseActionCableConnector {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const refreshActionCableConnector =
|
|
||||||
ActionCableConnector.refreshConnector;
|
|
||||||
|
|
||||||
export default ActionCableConnector;
|
export default ActionCableConnector;
|
||||||
|
|
|
@ -21,10 +21,16 @@ export const filterCampaigns = ({
|
||||||
currentURL,
|
currentURL,
|
||||||
isInBusinessHours,
|
isInBusinessHours,
|
||||||
}) => {
|
}) => {
|
||||||
return campaigns.filter(item =>
|
return campaigns.filter(campaign => {
|
||||||
item.triggerOnlyDuringBusinessHours
|
const hasMatchingURL =
|
||||||
? isInBusinessHours
|
stripTrailingSlash({ URL: campaign.url }) ===
|
||||||
: stripTrailingSlash({ URL: item.url }) ===
|
stripTrailingSlash({ URL: currentURL });
|
||||||
stripTrailingSlash({ URL: currentURL })
|
if (!hasMatchingURL) {
|
||||||
);
|
return false;
|
||||||
|
}
|
||||||
|
if (campaign.triggerOnlyDuringBusinessHours) {
|
||||||
|
return isInBusinessHours;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -44,11 +44,13 @@ describe('#Campaigns Helper', () => {
|
||||||
id: 1,
|
id: 1,
|
||||||
timeOnPage: 3,
|
timeOnPage: 3,
|
||||||
url: 'https://www.chatwoot.com/pricing',
|
url: 'https://www.chatwoot.com/pricing',
|
||||||
|
triggerOnlyDuringBusinessHours: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
timeOnPage: 6,
|
timeOnPage: 6,
|
||||||
url: 'https://www.chatwoot.com/about',
|
url: 'https://www.chatwoot.com/about',
|
||||||
|
triggerOnlyDuringBusinessHours: false,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
currentURL: 'https://www.chatwoot.com/about/',
|
currentURL: 'https://www.chatwoot.com/about/',
|
||||||
|
@ -58,8 +60,60 @@ describe('#Campaigns Helper', () => {
|
||||||
id: 2,
|
id: 2,
|
||||||
timeOnPage: 6,
|
timeOnPage: 6,
|
||||||
url: 'https://www.chatwoot.com/about',
|
url: 'https://www.chatwoot.com/about',
|
||||||
|
triggerOnlyDuringBusinessHours: false,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
it('should return filtered campaigns if formatted campaigns are passed and business hours enabled', () => {
|
||||||
|
expect(
|
||||||
|
filterCampaigns({
|
||||||
|
campaigns: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
timeOnPage: 3,
|
||||||
|
url: 'https://www.chatwoot.com/pricing',
|
||||||
|
triggerOnlyDuringBusinessHours: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
timeOnPage: 6,
|
||||||
|
url: 'https://www.chatwoot.com/about',
|
||||||
|
triggerOnlyDuringBusinessHours: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
currentURL: 'https://www.chatwoot.com/about/',
|
||||||
|
isInBusinessHours: true,
|
||||||
|
})
|
||||||
|
).toStrictEqual([
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
timeOnPage: 6,
|
||||||
|
url: 'https://www.chatwoot.com/about',
|
||||||
|
triggerOnlyDuringBusinessHours: true,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
it('should return empty campaigns if formatted campaigns are passed and business hours disabled', () => {
|
||||||
|
expect(
|
||||||
|
filterCampaigns({
|
||||||
|
campaigns: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
timeOnPage: 3,
|
||||||
|
url: 'https://www.chatwoot.com/pricing',
|
||||||
|
triggerOnlyDuringBusinessHours: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
timeOnPage: 6,
|
||||||
|
url: 'https://www.chatwoot.com/about',
|
||||||
|
triggerOnlyDuringBusinessHours: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
currentURL: 'https://www.chatwoot.com/about/',
|
||||||
|
isInBusinessHours: false,
|
||||||
|
})
|
||||||
|
).toStrictEqual([]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import ContactsAPI from '../../api/contacts';
|
import ContactsAPI from '../../api/contacts';
|
||||||
import { refreshActionCableConnector } from '../../helpers/actionCable';
|
|
||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
currentUser: {},
|
currentUser: {},
|
||||||
|
@ -31,17 +30,13 @@ export const actions = {
|
||||||
identifier_hash: userObject.identifier_hash,
|
identifier_hash: userObject.identifier_hash,
|
||||||
phone_number: userObject.phone_number,
|
phone_number: userObject.phone_number,
|
||||||
};
|
};
|
||||||
const {
|
await ContactsAPI.update(identifier, user);
|
||||||
data: { pubsub_token: pubsubToken },
|
|
||||||
} = await ContactsAPI.update(identifier, user);
|
|
||||||
|
|
||||||
dispatch('get');
|
dispatch('get');
|
||||||
if (userObject.identifier_hash) {
|
if (userObject.identifier_hash) {
|
||||||
dispatch('conversation/clearConversations', {}, { root: true });
|
dispatch('conversation/clearConversations', {}, { root: true });
|
||||||
dispatch('conversation/fetchOldConversations', {}, { root: true });
|
dispatch('conversation/fetchOldConversations', {}, { root: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
refreshActionCableConnector(pubsubToken);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Ignore error
|
// Ignore error
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,6 @@ import {
|
||||||
toggleTyping,
|
toggleTyping,
|
||||||
setUserLastSeenAt,
|
setUserLastSeenAt,
|
||||||
} from 'widget/api/conversation';
|
} from 'widget/api/conversation';
|
||||||
import { refreshActionCableConnector } from '../../../helpers/actionCable';
|
|
||||||
|
|
||||||
import { createTemporaryMessage, getNonDeletedMessages } from './helpers';
|
import { createTemporaryMessage, getNonDeletedMessages } from './helpers';
|
||||||
|
|
||||||
|
@ -15,13 +14,9 @@ export const actions = {
|
||||||
commit('setConversationUIFlag', { isCreating: true });
|
commit('setConversationUIFlag', { isCreating: true });
|
||||||
try {
|
try {
|
||||||
const { data } = await createConversationAPI(params);
|
const { data } = await createConversationAPI(params);
|
||||||
const {
|
const { messages } = data;
|
||||||
contact: { pubsub_token: pubsubToken },
|
|
||||||
messages,
|
|
||||||
} = data;
|
|
||||||
const [message = {}] = messages;
|
const [message = {}] = messages;
|
||||||
commit('pushMessageToConversation', message);
|
commit('pushMessageToConversation', message);
|
||||||
refreshActionCableConnector(pubsubToken);
|
|
||||||
dispatch('conversationAttributes/getAttributes', {}, { root: true });
|
dispatch('conversationAttributes/getAttributes', {}, { root: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Ignore error
|
// Ignore error
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import MessageAPI from '../../api/message';
|
import MessageAPI from '../../api/message';
|
||||||
import { refreshActionCableConnector } from '../../helpers/actionCable';
|
|
||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
uiFlags: {
|
uiFlags: {
|
||||||
|
@ -18,9 +17,7 @@ export const actions = {
|
||||||
) => {
|
) => {
|
||||||
commit('toggleUpdateStatus', true);
|
commit('toggleUpdateStatus', true);
|
||||||
try {
|
try {
|
||||||
const {
|
await MessageAPI.update({
|
||||||
data: { contact: { pubsub_token: pubsubToken } = {} },
|
|
||||||
} = await MessageAPI.update({
|
|
||||||
email,
|
email,
|
||||||
messageId,
|
messageId,
|
||||||
values: submittedValues,
|
values: submittedValues,
|
||||||
|
@ -37,7 +34,6 @@ export const actions = {
|
||||||
{ root: true }
|
{ root: true }
|
||||||
);
|
);
|
||||||
dispatch('contacts/get', {}, { root: true });
|
dispatch('contacts/get', {}, { root: true });
|
||||||
refreshActionCableConnector(pubsubToken);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Ignore error
|
// Ignore error
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,7 @@ class ActionCableListener < BaseListener
|
||||||
def message_created(event)
|
def message_created(event)
|
||||||
message, account = extract_message_and_account(event)
|
message, account = extract_message_and_account(event)
|
||||||
conversation = message.conversation
|
conversation = message.conversation
|
||||||
tokens = user_tokens(account, conversation.inbox.members) + contact_token(conversation.contact, message)
|
tokens = user_tokens(account, conversation.inbox.members) + contact_tokens(conversation.contact_inbox, message)
|
||||||
|
|
||||||
broadcast(account, tokens, MESSAGE_CREATED, message.push_event_data)
|
broadcast(account, tokens, MESSAGE_CREATED, message.push_event_data)
|
||||||
end
|
end
|
||||||
|
@ -27,7 +27,7 @@ class ActionCableListener < BaseListener
|
||||||
message, account = extract_message_and_account(event)
|
message, account = extract_message_and_account(event)
|
||||||
conversation = message.conversation
|
conversation = message.conversation
|
||||||
contact = conversation.contact
|
contact = conversation.contact
|
||||||
tokens = user_tokens(account, conversation.inbox.members) + contact_token(conversation.contact, message)
|
tokens = user_tokens(account, conversation.inbox.members) + contact_tokens(conversation.contact_inbox, message)
|
||||||
|
|
||||||
broadcast(account, tokens, MESSAGE_UPDATED, message.push_event_data)
|
broadcast(account, tokens, MESSAGE_UPDATED, message.push_event_data)
|
||||||
end
|
end
|
||||||
|
@ -132,12 +132,14 @@ class ActionCableListener < BaseListener
|
||||||
(agent_tokens + admin_tokens).uniq
|
(agent_tokens + admin_tokens).uniq
|
||||||
end
|
end
|
||||||
|
|
||||||
def contact_token(contact, message)
|
def contact_tokens(contact_inbox, message)
|
||||||
return [] if message.private?
|
return [] if message.private?
|
||||||
return [] if message.activity?
|
return [] if message.activity?
|
||||||
return [] if contact.nil?
|
return [] if contact_inbox.nil?
|
||||||
|
|
||||||
[contact.pubsub_token]
|
contact = contact_inbox.contact
|
||||||
|
|
||||||
|
contact_inbox.hmac_verified? ? contact.contact_inboxes.where(hmac_verified: true).filter_map(&:pubsub_token) : [contact_inbox.pubsub_token]
|
||||||
end
|
end
|
||||||
|
|
||||||
def broadcast(account, tokens, event_name, data)
|
def broadcast(account, tokens, event_name, data)
|
||||||
|
|
|
@ -7,4 +7,10 @@ module Pubsubable
|
||||||
# Used by the actionCable/PubSub Service we use for real time communications
|
# Used by the actionCable/PubSub Service we use for real time communications
|
||||||
has_secure_token :pubsub_token
|
has_secure_token :pubsub_token
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def pubsub_token
|
||||||
|
# backfills tokens for existing records
|
||||||
|
regenerate_pubsub_token if self[:pubsub_token].blank?
|
||||||
|
self[:pubsub_token]
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -25,7 +25,7 @@
|
||||||
#
|
#
|
||||||
|
|
||||||
class Contact < ApplicationRecord
|
class Contact < ApplicationRecord
|
||||||
include Pubsubable
|
# TODO: remove the pubsub_token attribute from this model in future.
|
||||||
include Avatarable
|
include Avatarable
|
||||||
include AvailabilityStatusable
|
include AvailabilityStatusable
|
||||||
include Labelable
|
include Labelable
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
#
|
#
|
||||||
# id :bigint not null, primary key
|
# id :bigint not null, primary key
|
||||||
# hmac_verified :boolean default(FALSE)
|
# hmac_verified :boolean default(FALSE)
|
||||||
|
# pubsub_token :string
|
||||||
# created_at :datetime not null
|
# created_at :datetime not null
|
||||||
# updated_at :datetime not null
|
# updated_at :datetime not null
|
||||||
# contact_id :bigint
|
# contact_id :bigint
|
||||||
|
@ -15,6 +16,7 @@
|
||||||
# index_contact_inboxes_on_contact_id (contact_id)
|
# index_contact_inboxes_on_contact_id (contact_id)
|
||||||
# index_contact_inboxes_on_inbox_id (inbox_id)
|
# index_contact_inboxes_on_inbox_id (inbox_id)
|
||||||
# index_contact_inboxes_on_inbox_id_and_source_id (inbox_id,source_id) UNIQUE
|
# index_contact_inboxes_on_inbox_id_and_source_id (inbox_id,source_id) UNIQUE
|
||||||
|
# index_contact_inboxes_on_pubsub_token (pubsub_token) UNIQUE
|
||||||
# index_contact_inboxes_on_source_id (source_id)
|
# index_contact_inboxes_on_source_id (source_id)
|
||||||
#
|
#
|
||||||
# Foreign Keys
|
# Foreign Keys
|
||||||
|
@ -24,6 +26,7 @@
|
||||||
#
|
#
|
||||||
|
|
||||||
class ContactInbox < ApplicationRecord
|
class ContactInbox < ApplicationRecord
|
||||||
|
include Pubsubable
|
||||||
validates :inbox_id, presence: true
|
validates :inbox_id, presence: true
|
||||||
validates :contact_id, presence: true
|
validates :contact_id, presence: true
|
||||||
validates :source_id, presence: true
|
validates :source_id, presence: true
|
||||||
|
|
|
@ -23,7 +23,11 @@ class Conversations::EventDataPresenter < SimpleDelegator
|
||||||
end
|
end
|
||||||
|
|
||||||
def push_meta
|
def push_meta
|
||||||
{ sender: contact.push_event_data, assignee: assignee&.push_event_data }
|
{
|
||||||
|
sender: contact.push_event_data,
|
||||||
|
assignee: assignee&.push_event_data,
|
||||||
|
hmac_verified: contact_inbox&.hmac_verified
|
||||||
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
def push_timestamps
|
def push_timestamps
|
||||||
|
|
|
@ -13,6 +13,7 @@ json.meta do
|
||||||
json.partial! 'api/v1/models/team.json.jbuilder', resource: conversation.team
|
json.partial! 'api/v1/models/team.json.jbuilder', resource: conversation.team
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
json.hmac_verified conversation.contact_inbox&.hmac_verified
|
||||||
end
|
end
|
||||||
|
|
||||||
json.id conversation.display_id
|
json.id conversation.display_id
|
||||||
|
|
|
@ -22,7 +22,7 @@ json.chatwoot_widget_defaults do
|
||||||
json.use_inbox_avatar_for_bot ActiveModel::Type::Boolean.new.cast(ENV.fetch('USE_INBOX_AVATAR_FOR_BOT', false))
|
json.use_inbox_avatar_for_bot ActiveModel::Type::Boolean.new.cast(ENV.fetch('USE_INBOX_AVATAR_FOR_BOT', false))
|
||||||
end
|
end
|
||||||
json.contact do
|
json.contact do
|
||||||
json.pubsub_token @contact.pubsub_token
|
json.pubsub_token @contact_inbox.pubsub_token
|
||||||
end
|
end
|
||||||
json.auth_token @token
|
json.auth_token @token
|
||||||
json.global_config @global_config
|
json.global_config @global_config
|
||||||
|
|
|
@ -1,2 +1,3 @@
|
||||||
json.source_id @contact_inbox.source_id
|
json.source_id @contact_inbox.source_id
|
||||||
|
json.pubsub_token @contact_inbox.pubsub_token
|
||||||
json.partial! 'public/api/v1/models/contact.json.jbuilder', resource: @contact_inbox.contact
|
json.partial! 'public/api/v1/models/contact.json.jbuilder', resource: @contact_inbox.contact
|
||||||
|
|
|
@ -1,2 +1,3 @@
|
||||||
json.source_id @contact_inbox.source_id
|
json.source_id @contact_inbox.source_id
|
||||||
|
json.pubsub_token @contact_inbox.pubsub_token
|
||||||
json.partial! 'public/api/v1/models/contact.json.jbuilder', resource: @contact_inbox.contact
|
json.partial! 'public/api/v1/models/contact.json.jbuilder', resource: @contact_inbox.contact
|
||||||
|
|
|
@ -1,2 +1,3 @@
|
||||||
json.source_id @contact_inbox.source_id
|
json.source_id @contact_inbox.source_id
|
||||||
|
json.pubsub_token @contact_inbox.pubsub_token
|
||||||
json.partial! 'public/api/v1/models/contact.json.jbuilder', resource: @contact_inbox.contact
|
json.partial! 'public/api/v1/models/contact.json.jbuilder', resource: @contact_inbox.contact
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
json.id resource.id
|
json.id resource.id
|
||||||
json.name resource.name
|
json.name resource.name
|
||||||
json.email resource.email
|
json.email resource.email
|
||||||
json.pubsub_token resource.pubsub_token
|
|
||||||
|
|
|
@ -28,7 +28,7 @@
|
||||||
window.chatwootWidgetDefaults = {
|
window.chatwootWidgetDefaults = {
|
||||||
useInboxAvatarForBot: <%= ActiveModel::Type::Boolean.new.cast(ENV.fetch('USE_INBOX_AVATAR_FOR_BOT', false)) %>,
|
useInboxAvatarForBot: <%= ActiveModel::Type::Boolean.new.cast(ENV.fetch('USE_INBOX_AVATAR_FOR_BOT', false)) %>,
|
||||||
}
|
}
|
||||||
window.chatwootPubsubToken = '<%= @contact.pubsub_token %>'
|
window.chatwootPubsubToken = '<%= @contact_inbox.pubsub_token %>'
|
||||||
window.authToken = '<%= @token %>'
|
window.authToken = '<%= @token %>'
|
||||||
window.globalConfig = <%= raw @global_config.to_json %>
|
window.globalConfig = <%= raw @global_config.to_json %>
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
shared: &shared
|
shared: &shared
|
||||||
version: '1.22.0'
|
version: '1.22.1'
|
||||||
|
|
||||||
development:
|
development:
|
||||||
<<: *shared
|
<<: *shared
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
class AddPubSubTokenToContactInbox < ActiveRecord::Migration[6.1]
|
||||||
|
def change
|
||||||
|
add_column :contact_inboxes, :pubsub_token, :string
|
||||||
|
add_index :contact_inboxes, :pubsub_token, unique: true
|
||||||
|
end
|
||||||
|
end
|
|
@ -10,7 +10,7 @@
|
||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema.define(version: 2021_11_18_100301) do
|
ActiveRecord::Schema.define(version: 2021_11_22_061012) do
|
||||||
|
|
||||||
# These are extensions that must be enabled in order to support this database
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "pg_stat_statements"
|
enable_extension "pg_stat_statements"
|
||||||
|
@ -288,9 +288,11 @@ ActiveRecord::Schema.define(version: 2021_11_18_100301) do
|
||||||
t.datetime "created_at", precision: 6, null: false
|
t.datetime "created_at", precision: 6, null: false
|
||||||
t.datetime "updated_at", precision: 6, null: false
|
t.datetime "updated_at", precision: 6, null: false
|
||||||
t.boolean "hmac_verified", default: false
|
t.boolean "hmac_verified", default: false
|
||||||
|
t.string "pubsub_token"
|
||||||
t.index ["contact_id"], name: "index_contact_inboxes_on_contact_id"
|
t.index ["contact_id"], name: "index_contact_inboxes_on_contact_id"
|
||||||
t.index ["inbox_id", "source_id"], name: "index_contact_inboxes_on_inbox_id_and_source_id", unique: true
|
t.index ["inbox_id", "source_id"], name: "index_contact_inboxes_on_inbox_id_and_source_id", unique: true
|
||||||
t.index ["inbox_id"], name: "index_contact_inboxes_on_inbox_id"
|
t.index ["inbox_id"], name: "index_contact_inboxes_on_inbox_id"
|
||||||
|
t.index ["pubsub_token"], name: "index_contact_inboxes_on_pubsub_token", unique: true
|
||||||
t.index ["source_id"], name: "index_contact_inboxes_on_source_id"
|
t.index ["source_id"], name: "index_contact_inboxes_on_source_id"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@chatwoot/chatwoot",
|
"name": "@chatwoot/chatwoot",
|
||||||
"version": "1.22.0",
|
"version": "1.22.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"eslint": "eslint app/javascript --fix",
|
"eslint": "eslint app/javascript --fix",
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
require 'rails_helper'
|
require 'rails_helper'
|
||||||
|
|
||||||
RSpec.describe RoomChannel, type: :channel do
|
RSpec.describe RoomChannel, type: :channel do
|
||||||
let!(:contact) { create(:contact) }
|
let!(:contact_inbox) { create(:contact_inbox) }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
stub_connection
|
stub_connection
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'subscribes to a stream when pubsub_token is provided' do
|
it 'subscribes to a stream when pubsub_token is provided' do
|
||||||
subscribe(pubsub_token: contact.pubsub_token)
|
subscribe(pubsub_token: contact_inbox.pubsub_token)
|
||||||
expect(subscription).to be_confirmed
|
expect(subscription).to be_confirmed
|
||||||
expect(subscription).to have_stream_for(contact.pubsub_token)
|
expect(subscription).to have_stream_for(contact_inbox.pubsub_token)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -45,7 +45,7 @@ RSpec.describe '/api/v1/widget/config', type: :request do
|
||||||
expect(response).to have_http_status(:success)
|
expect(response).to have_http_status(:success)
|
||||||
response_data = JSON.parse(response.body)
|
response_data = JSON.parse(response.body)
|
||||||
expect(response_data.keys).to include(*response_keys)
|
expect(response_data.keys).to include(*response_keys)
|
||||||
expect(response_data['contact']['pubsub_token']).to eq(contact.pubsub_token)
|
expect(response_data['contact']['pubsub_token']).to eq(contact_inbox.pubsub_token)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -23,7 +23,7 @@ RSpec.describe 'Public Inbox Contacts API', type: :request do
|
||||||
expect(response).to have_http_status(:success)
|
expect(response).to have_http_status(:success)
|
||||||
data = JSON.parse(response.body)
|
data = JSON.parse(response.body)
|
||||||
expect(data['source_id']).to eq contact_inbox.source_id
|
expect(data['source_id']).to eq contact_inbox.source_id
|
||||||
expect(data['pubsub_token']).to eq contact.pubsub_token
|
expect(data['pubsub_token']).to eq contact_inbox.pubsub_token
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -24,7 +24,23 @@ describe ActionCableListener do
|
||||||
expect(conversation.inbox.reload.inbox_members.count).to eq(1)
|
expect(conversation.inbox.reload.inbox_members.count).to eq(1)
|
||||||
|
|
||||||
expect(ActionCableBroadcastJob).to receive(:perform_later).with(
|
expect(ActionCableBroadcastJob).to receive(:perform_later).with(
|
||||||
[agent.pubsub_token, admin.pubsub_token, conversation.contact.pubsub_token],
|
[agent.pubsub_token, admin.pubsub_token, conversation.contact_inbox.pubsub_token],
|
||||||
|
'message.created',
|
||||||
|
message.push_event_data.merge(account_id: account.id)
|
||||||
|
)
|
||||||
|
listener.message_created(event)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'sends message to all hmac verified contact inboxes' do
|
||||||
|
# HACK: to reload conversation inbox members
|
||||||
|
expect(conversation.inbox.reload.inbox_members.count).to eq(1)
|
||||||
|
conversation.contact_inbox.update(hmac_verified: true)
|
||||||
|
# creating a non verified contact inbox to ensure the events are not sent to it
|
||||||
|
create(:contact_inbox, contact: conversation.contact, inbox: inbox)
|
||||||
|
verified_contact_inbox = create(:contact_inbox, contact: conversation.contact, inbox: inbox, hmac_verified: true)
|
||||||
|
|
||||||
|
expect(ActionCableBroadcastJob).to receive(:perform_later).with(
|
||||||
|
[agent.pubsub_token, admin.pubsub_token, conversation.contact_inbox.pubsub_token, verified_contact_inbox.pubsub_token],
|
||||||
'message.created',
|
'message.created',
|
||||||
message.push_event_data.merge(account_id: account.id)
|
message.push_event_data.merge(account_id: account.id)
|
||||||
)
|
)
|
||||||
|
|
40
spec/models/contact_inbox_spec.rb
Normal file
40
spec/models/contact_inbox_spec.rb
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe ContactInbox do
|
||||||
|
describe 'pubsub_token' do
|
||||||
|
let(:contact_inbox) { create(:contact_inbox) }
|
||||||
|
|
||||||
|
it 'gets created on object create' do
|
||||||
|
obj = contact_inbox
|
||||||
|
expect(obj.pubsub_token).not_to eq(nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not get updated on object update' do
|
||||||
|
obj = contact_inbox
|
||||||
|
old_token = obj.pubsub_token
|
||||||
|
obj.update(source_id: '234234323')
|
||||||
|
expect(obj.pubsub_token).to eq(old_token)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'backfills pubsub_token on call for older objects' do
|
||||||
|
obj = create(:contact_inbox)
|
||||||
|
# to replicate an object with out pubsub_token
|
||||||
|
# rubocop:disable Rails/SkipsModelValidations
|
||||||
|
obj.update_column(:pubsub_token, nil)
|
||||||
|
# rubocop:enable Rails/SkipsModelValidations
|
||||||
|
|
||||||
|
obj.reload
|
||||||
|
|
||||||
|
# ensure the column is nil in database
|
||||||
|
results = ActiveRecord::Base.connection.execute('Select * from contact_inboxes;')
|
||||||
|
expect(results.first['pubsub_token']).to eq(nil)
|
||||||
|
|
||||||
|
new_token = obj.pubsub_token
|
||||||
|
obj.update(source_id: '234234323')
|
||||||
|
# the generated token shoul be persisted in db
|
||||||
|
expect(obj.pubsub_token).to eq(new_token)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -11,20 +11,4 @@ RSpec.describe Contact do
|
||||||
it { is_expected.to belong_to(:account) }
|
it { is_expected.to belong_to(:account) }
|
||||||
it { is_expected.to have_many(:conversations).dependent(:destroy_async) }
|
it { is_expected.to have_many(:conversations).dependent(:destroy_async) }
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'pubsub_token' do
|
|
||||||
let(:user) { create(:user) }
|
|
||||||
|
|
||||||
it 'gets created on object create' do
|
|
||||||
obj = user
|
|
||||||
expect(obj.pubsub_token).not_to eq(nil)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'does not get updated on object update' do
|
|
||||||
obj = user
|
|
||||||
old_token = obj.pubsub_token
|
|
||||||
obj.update(name: 'test')
|
|
||||||
expect(obj.pubsub_token).to eq(old_token)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -399,7 +399,8 @@ RSpec.describe Conversation, type: :model do
|
||||||
additional_attributes: {},
|
additional_attributes: {},
|
||||||
meta: {
|
meta: {
|
||||||
sender: conversation.contact.push_event_data,
|
sender: conversation.contact.push_event_data,
|
||||||
assignee: conversation.assignee
|
assignee: conversation.assignee,
|
||||||
|
hmac_verified: conversation.contact_inbox.hmac_verified
|
||||||
},
|
},
|
||||||
id: conversation.display_id,
|
id: conversation.display_id,
|
||||||
messages: [],
|
messages: [],
|
||||||
|
|
|
@ -12,7 +12,8 @@ RSpec.describe Conversations::EventDataPresenter do
|
||||||
additional_attributes: {},
|
additional_attributes: {},
|
||||||
meta: {
|
meta: {
|
||||||
sender: conversation.contact.push_event_data,
|
sender: conversation.contact.push_event_data,
|
||||||
assignee: conversation.assignee
|
assignee: conversation.assignee,
|
||||||
|
hmac_verified: conversation.contact_inbox.hmac_verified
|
||||||
},
|
},
|
||||||
id: conversation.display_id,
|
id: conversation.display_id,
|
||||||
messages: [],
|
messages: [],
|
||||||
|
|
Loading…
Reference in a new issue