Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
chore: Migrate PubSub Token to contact inbox (#3434)
At present, the websocket pubsub tokens are present at the contact objects in chatwoot. A better approach would be to have these tokens at the contact_inbox object instead. This helps chatwoot to deliver the websocket events targetted to the specific widget connection, stop contact events from leaking into other chat sessions from the same contact.

Fixes #1682
Fixes #1664

Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
  • Loading branch information
3 people committed Nov 22, 2021
1 parent 01577ac commit 791d90c
Show file tree
Hide file tree
Showing 38 changed files with 211 additions and 95 deletions.
5 changes: 2 additions & 3 deletions app/actions/contact_merge_action.rb
Expand Up @@ -48,11 +48,10 @@ def merge_and_remove_mergee_contact

# attributes in base contact are given preference
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!
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)
end
end
2 changes: 1 addition & 1 deletion app/channels/room_channel.rb
Expand Up @@ -31,7 +31,7 @@ def update_subscription

def current_user
@current_user ||= if params[:user_id].blank?
Contact.find_by!(pubsub_token: @pubsub_token)
ContactInbox.find_by!(pubsub_token: @pubsub_token).contact
else
User.find_by!(pubsub_token: @pubsub_token, id: params[:user_id])
end
Expand Down
10 changes: 5 additions & 5 deletions app/controllers/widgets_controller.rb
Expand Up @@ -29,21 +29,21 @@ def set_token
def set_contact
return if @auth_token_params[:source_id].nil?

contact_inbox = ::ContactInbox.find_by(
@contact_inbox = ::ContactInbox.find_by(
inbox_id: @web_widget.inbox.id,
source_id: @auth_token_params[:source_id]
)

@contact = contact_inbox ? contact_inbox.contact : nil
@contact = @contact_inbox ? @contact_inbox.contact : nil
end

def build_contact
return if @contact.present?

contact_inbox = @web_widget.create_contact_inbox(additional_attributes)
@contact = contact_inbox.contact
@contact_inbox = @web_widget.create_contact_inbox(additional_attributes)
@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
end

Expand Down
2 changes: 1 addition & 1 deletion app/finders/conversation_finder.rb
Expand Up @@ -121,7 +121,7 @@ def current_page

def conversations
@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)
end
Expand Down
8 changes: 8 additions & 0 deletions app/javascript/dashboard/assets/scss/_utility-helpers.scss
Expand Up @@ -2,6 +2,10 @@
margin-right: var(--space-small);
}

.margin-right-smaller {
margin-right: var(--space-smaller);
}

.fs-small {
font-size: var(--font-size-small);
}
Expand Down Expand Up @@ -42,3 +46,7 @@
.bg-white {
background-color: var(--white);
}

.text-y-800 {
color: var(--y-800);
}
Expand Up @@ -10,7 +10,12 @@
/>
<div class="user--profile__meta">
<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>
<div class="conversation--header--actions">
<inbox-name :inbox="inbox" class="margin-right-small" />
Expand Down Expand Up @@ -73,11 +78,15 @@ export default {
uiFlags: 'inboxAssignableAgents/getUIFlags',
currentChat: 'getSelectedChat',
}),
chatMetadata() {
return this.chat.meta;
},
isHMACVerified() {
if (!this.isAWebWidgetInbox) {
return true;
}
return this.chatMetadata.hmac_verified;
},
currentContact() {
return this.$store.getters['contacts/getContact'](
this.chat.meta.sender.id
Expand Down
1 change: 1 addition & 0 deletions app/javascript/dashboard/i18n/locale/en/conversation.json
@@ -1,6 +1,7 @@
{
"CONVERSATION": {
"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_2": " to send a message to your page!",
"NO_INBOX_1": "Hola! Looks like you haven't added any inboxes yet.",
Expand Down
Expand Up @@ -41,17 +41,17 @@
</label>
</div>
</div>
<div class="row" v-if="isAnEmailInbox">
<div v-if="isAnEmailInbox" class="row">
<div class="columns">
<label :class="{ error: $v.message.$error }">
<label :class="{ error: $v.subject.$error }">
{{ $t('NEW_CONVERSATION.FORM.SUBJECT.LABEL') }}
<input
v-model="subject"
type="text"
: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') }}
</span>
</label>
Expand Down Expand Up @@ -93,7 +93,7 @@ import Thumbnail from 'dashboard/components/widgets/Thumbnail';
import alertMixin from 'shared/mixins/alertMixin';
import { INBOX_TYPES } from 'shared/mixins/inboxMixin';
import { ExceptionWithMessage } from 'shared/helpers/CustomErrors';
import { required } from 'vuelidate/lib/validators';
import { required, requiredIf } from 'vuelidate/lib/validators';
export default {
components: {
Expand All @@ -120,7 +120,7 @@ export default {
},
validations: {
subject: {
required,
required: requiredIf('isAnEmailInbox'),
},
message: {
required,
Expand Down
17 changes: 1 addition & 16 deletions app/javascript/widget/helpers/actionCable.js
Expand Up @@ -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 => {
this.app.$store.dispatch('conversationAttributes/update', data);
};
Expand Down Expand Up @@ -57,7 +45,7 @@ class ActionCableConnector extends BaseActionCableConnector {

onTypingOn = data => {
if (data.is_private) {
return
return;
}
this.clearTimer();
this.app.$store.dispatch('conversation/toggleAgentTyping', {
Expand Down Expand Up @@ -88,7 +76,4 @@ class ActionCableConnector extends BaseActionCableConnector {
};
}

export const refreshActionCableConnector =
ActionCableConnector.refreshConnector;

export default ActionCableConnector;
18 changes: 12 additions & 6 deletions app/javascript/widget/helpers/campaignHelper.js
Expand Up @@ -21,10 +21,16 @@ export const filterCampaigns = ({
currentURL,
isInBusinessHours,
}) => {
return campaigns.filter(item =>
item.triggerOnlyDuringBusinessHours
? isInBusinessHours
: stripTrailingSlash({ URL: item.url }) ===
stripTrailingSlash({ URL: currentURL })
);
return campaigns.filter(campaign => {
const hasMatchingURL =
stripTrailingSlash({ URL: campaign.url }) ===
stripTrailingSlash({ URL: currentURL });
if (!hasMatchingURL) {
return false;
}
if (campaign.triggerOnlyDuringBusinessHours) {
return isInBusinessHours;
}
return true;
});
};
54 changes: 54 additions & 0 deletions app/javascript/widget/helpers/specs/campaignHelper.spec.js
Expand Up @@ -44,11 +44,13 @@ describe('#Campaigns Helper', () => {
id: 1,
timeOnPage: 3,
url: 'https://www.chatwoot.com/pricing',
triggerOnlyDuringBusinessHours: false,
},
{
id: 2,
timeOnPage: 6,
url: 'https://www.chatwoot.com/about',
triggerOnlyDuringBusinessHours: false,
},
],
currentURL: 'https://www.chatwoot.com/about/',
Expand All @@ -58,8 +60,60 @@ describe('#Campaigns Helper', () => {
id: 2,
timeOnPage: 6,
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([]);
});
});
});
7 changes: 1 addition & 6 deletions app/javascript/widget/store/modules/contacts.js
@@ -1,5 +1,4 @@
import ContactsAPI from '../../api/contacts';
import { refreshActionCableConnector } from '../../helpers/actionCable';

const state = {
currentUser: {},
Expand Down Expand Up @@ -31,17 +30,13 @@ export const actions = {
identifier_hash: userObject.identifier_hash,
phone_number: userObject.phone_number,
};
const {
data: { pubsub_token: pubsubToken },
} = await ContactsAPI.update(identifier, user);
await ContactsAPI.update(identifier, user);

dispatch('get');
if (userObject.identifier_hash) {
dispatch('conversation/clearConversations', {}, { root: true });
dispatch('conversation/fetchOldConversations', {}, { root: true });
}

refreshActionCableConnector(pubsubToken);
} catch (error) {
// Ignore error
}
Expand Down
7 changes: 1 addition & 6 deletions app/javascript/widget/store/modules/conversation/actions.js
Expand Up @@ -6,7 +6,6 @@ import {
toggleTyping,
setUserLastSeenAt,
} from 'widget/api/conversation';
import { refreshActionCableConnector } from '../../../helpers/actionCable';

import { createTemporaryMessage, getNonDeletedMessages } from './helpers';

Expand All @@ -15,13 +14,9 @@ export const actions = {
commit('setConversationUIFlag', { isCreating: true });
try {
const { data } = await createConversationAPI(params);
const {
contact: { pubsub_token: pubsubToken },
messages,
} = data;
const { messages } = data;
const [message = {}] = messages;
commit('pushMessageToConversation', message);
refreshActionCableConnector(pubsubToken);
dispatch('conversationAttributes/getAttributes', {}, { root: true });
} catch (error) {
// Ignore error
Expand Down
6 changes: 1 addition & 5 deletions app/javascript/widget/store/modules/message.js
@@ -1,5 +1,4 @@
import MessageAPI from '../../api/message';
import { refreshActionCableConnector } from '../../helpers/actionCable';

const state = {
uiFlags: {
Expand All @@ -18,9 +17,7 @@ export const actions = {
) => {
commit('toggleUpdateStatus', true);
try {
const {
data: { contact: { pubsub_token: pubsubToken } = {} },
} = await MessageAPI.update({
await MessageAPI.update({
email,
messageId,
values: submittedValues,
Expand All @@ -37,7 +34,6 @@ export const actions = {
{ root: true }
);
dispatch('contacts/get', {}, { root: true });
refreshActionCableConnector(pubsubToken);
} catch (error) {
// Ignore error
}
Expand Down

0 comments on commit 791d90c

Please sign in to comment.