Skip to content
Permalink
Browse files
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 791d90c6b71d94124a93fc85aaa8c5094f9b7613
Showing with 211 additions and 95 deletions.
  1. +2 鈭3 app/actions/contact_merge_action.rb
  2. +1 鈭1 app/channels/room_channel.rb
  3. +5 鈭5 app/controllers/widgets_controller.rb
  4. +1 鈭1 app/finders/conversation_finder.rb
  5. +8 鈭0 app/javascript/dashboard/assets/scss/_utility-helpers.scss
  6. +12 鈭3 app/javascript/dashboard/components/widgets/conversation/ConversationHeader.vue
  7. +1 鈭0 app/javascript/dashboard/i18n/locale/en/conversation.json
  8. +6 鈭6 app/javascript/dashboard/routes/dashboard/conversation/contact/ConversationForm.vue
  9. +1 鈭16 app/javascript/widget/helpers/actionCable.js
  10. +12 鈭6 app/javascript/widget/helpers/campaignHelper.js
  11. +54 鈭0 app/javascript/widget/helpers/specs/campaignHelper.spec.js
  12. +1 鈭6 app/javascript/widget/store/modules/contacts.js
  13. +1 鈭6 app/javascript/widget/store/modules/conversation/actions.js
  14. +1 鈭5 app/javascript/widget/store/modules/message.js
  15. +7 鈭5 app/listeners/action_cable_listener.rb
  16. +6 鈭0 app/models/concerns/pubsubable.rb
  17. +1 鈭1 app/models/contact.rb
  18. +3 鈭0 app/models/contact_inbox.rb
  19. +5 鈭1 app/presenters/conversations/event_data_presenter.rb
  20. +1 鈭0 app/views/api/v1/conversations/partials/_conversation.json.jbuilder
  21. +1 鈭1 app/views/api/v1/widget/configs/create.json.jbuilder
  22. +1 鈭0 app/views/public/api/v1/inboxes/contacts/create.json.jbuilder
  23. +1 鈭0 app/views/public/api/v1/inboxes/contacts/show.json.jbuilder
  24. +1 鈭0 app/views/public/api/v1/inboxes/contacts/update.json.jbuilder
  25. +0 鈭1 app/views/public/api/v1/models/_contact.json.jbuilder
  26. +1 鈭1 app/views/widgets/show.html.erb
  27. +1 鈭1 config/app.yml
  28. +6 鈭0 db/migrate/20211122061012_add_pub_sub_token_to_contact_inbox.rb
  29. +3 鈭1 db/schema.rb
  30. +1 鈭1 package.json
  31. +3 鈭3 spec/channels/room_channel_spec.rb
  32. +1 鈭1 spec/controllers/api/v1/widget/configs_controller_spec.rb
  33. +1 鈭1 spec/controllers/public/api/v1/inbox/contacts_controller_spec.rb
  34. +17 鈭1 spec/listeners/action_cable_listener_spec.rb
  35. +40 鈭0 spec/models/contact_inbox_spec.rb
  36. +0 鈭16 spec/models/contact_spec.rb
  37. +2 鈭1 spec/models/conversation_spec.rb
  38. +2 鈭1 spec/presenters/conversations/event_data_presenter_spec.rb
@@ -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
@@ -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
@@ -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

@@ -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
@@ -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);
}
@@ -42,3 +46,7 @@
.bg-white {
background-color: var(--white);
}

.text-y-800 {
color: var(--y-800);
}
@@ -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" />
@@ -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
@@ -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.",
@@ -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>
@@ -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: {
@@ -120,7 +120,7 @@ export default {
},
validations: {
subject: {
required,
required: requiredIf('isAnEmailInbox'),
},
message: {
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 => {
this.app.$store.dispatch('conversationAttributes/update', data);
};
@@ -57,7 +45,7 @@ class ActionCableConnector extends BaseActionCableConnector {

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

export const refreshActionCableConnector =
ActionCableConnector.refreshConnector;

export default ActionCableConnector;
@@ -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;
});
};
@@ -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/',
@@ -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([]);
});
});
});
@@ -1,5 +1,4 @@
import ContactsAPI from '../../api/contacts';
import { refreshActionCableConnector } from '../../helpers/actionCable';

const state = {
currentUser: {},
@@ -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
}
@@ -6,7 +6,6 @@ import {
toggleTyping,
setUserLastSeenAt,
} from 'widget/api/conversation';
import { refreshActionCableConnector } from '../../../helpers/actionCable';

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

@@ -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
@@ -1,5 +1,4 @@
import MessageAPI from '../../api/message';
import { refreshActionCableConnector } from '../../helpers/actionCable';

const state = {
uiFlags: {
@@ -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,
@@ -37,7 +34,6 @@ export const actions = {
{ root: true }
);
dispatch('contacts/get', {}, { root: true });
refreshActionCableConnector(pubsubToken);
} catch (error) {
// Ignore error
}

0 comments on commit 791d90c

Please sign in to comment.