Merge branch 'hotfix/1.22.1' into develop

# Conflicts:
#	db/schema.rb
This commit is contained in:
Sojan 2021-11-22 23:35:44 +05:30
commit 0033a35ab8
38 changed files with 211 additions and 95 deletions

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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);
}

View file

@ -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

View file

@ -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.",

View file

@ -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,

View file

@ -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;

View file

@ -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;
});
}; };

View file

@ -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([]);
});
}); });
}); });

View file

@ -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
} }

View file

@ -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

View file

@ -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
} }

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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>

View file

@ -1,5 +1,5 @@
shared: &shared shared: &shared
version: '1.22.0' version: '1.22.1'
development: development:
<<: *shared <<: *shared

View file

@ -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

View file

@ -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

View file

@ -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",

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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)
) )

View 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

View file

@ -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

View file

@ -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: [],

View file

@ -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: [],