Merge branch 'chatwoot:develop' into feat/paste-image

This commit is contained in:
David Kubeš 2022-10-14 01:37:25 +02:00 committed by GitHub
commit 696444bf57
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 365 additions and 239 deletions

View file

@ -16,6 +16,7 @@ Metrics/ClassLength:
- 'app/models/message.rb' - 'app/models/message.rb'
- 'app/builders/messages/facebook/message_builder.rb' - 'app/builders/messages/facebook/message_builder.rb'
- 'app/controllers/api/v1/accounts/contacts_controller.rb' - 'app/controllers/api/v1/accounts/contacts_controller.rb'
- 'app/controllers/api/v1/accounts/conversations_controller.rb'
- 'app/listeners/action_cable_listener.rb' - 'app/listeners/action_cable_listener.rb'
- 'app/models/conversation.rb' - 'app/models/conversation.rb'
RSpec/ExampleLength: RSpec/ExampleLength:

View file

@ -1,13 +1,12 @@
# This Builder will create a contact inbox with specified attributes. If the contact inbox already exists, it will be returned.
# For Specific Channels like whatsapp, email etc . it smartly generated appropriate the source id when none is provided.
class ContactInboxBuilder class ContactInboxBuilder
pattr_initialize [:contact_id!, :inbox_id!, :source_id] pattr_initialize [:contact, :inbox, :source_id, { hmac_verified: false }]
def perform def perform
@contact = Contact.find(contact_id) @source_id ||= generate_source_id
@inbox = @contact.account.inboxes.find(inbox_id) create_contact_inbox if source_id.present?
return unless ['Channel::TwilioSms', 'Channel::Sms', 'Channel::Email', 'Channel::Api', 'Channel::Whatsapp'].include? @inbox.channel_type
source_id = @source_id || generate_source_id
create_contact_inbox(source_id) if source_id.present?
end end
private private
@ -19,23 +18,37 @@ class ContactInboxBuilder
when 'Channel::Whatsapp' when 'Channel::Whatsapp'
wa_source_id wa_source_id
when 'Channel::Email' when 'Channel::Email'
@contact.email email_source_id
when 'Channel::Sms' when 'Channel::Sms'
@contact.phone_number phone_source_id
when 'Channel::Api' when 'Channel::Api', 'Channel::WebWidget'
SecureRandom.uuid SecureRandom.uuid
else
raise "Unsupported operation for this channel: #{@inbox.channel_type}"
end end
end end
def email_source_id
raise ActionController::ParameterMissing, 'contact email' unless @contact.email
@contact.email
end
def phone_source_id
raise ActionController::ParameterMissing, 'contact phone number' unless @contact.phone_number
@contact.phone_number
end
def wa_source_id def wa_source_id
return unless @contact.phone_number raise ActionController::ParameterMissing, 'contact phone number' unless @contact.phone_number
# whatsapp doesn't want the + in e164 format # whatsapp doesn't want the + in e164 format
@contact.phone_number.delete('+').to_s @contact.phone_number.delete('+').to_s
end end
def twilio_source_id def twilio_source_id
return unless @contact.phone_number raise ActionController::ParameterMissing, 'contact phone number' unless @contact.phone_number
case @inbox.channel.medium case @inbox.channel.medium
when 'sms' when 'sms'
@ -45,11 +58,11 @@ class ContactInboxBuilder
end end
end end
def create_contact_inbox(source_id) def create_contact_inbox
::ContactInbox.find_or_create_by!( ::ContactInbox.create_with(hmac_verified: hmac_verified || false).find_or_create_by!(
contact_id: @contact.id, contact_id: @contact.id,
inbox_id: @inbox.id, inbox_id: @inbox.id,
source_id: source_id source_id: @source_id
) )
end end
end end

View file

@ -1,25 +1,47 @@
class ContactBuilder # This Builder will create a contact and contact inbox with specified attributes.
pattr_initialize [:source_id!, :inbox!, :contact_attributes!, :hmac_verified] # If an existing identified contact exisits, it will be returned.
# for contact inbox logic it uses the contact inbox builder
class ContactInboxWithContactBuilder
pattr_initialize [:inbox!, :contact_attributes!, :source_id, :hmac_verified]
def perform def perform
contact_inbox = inbox.contact_inboxes.find_by(source_id: source_id) find_or_create_contact_and_contact_inbox
return contact_inbox if contact_inbox # in case of race conditions where contact is created by another thread
# we will try to find the contact and create a contact inbox
rescue ActiveRecord::RecordNotUnique
find_or_create_contact_and_contact_inbox
end
build_contact_inbox def find_or_create_contact_and_contact_inbox
@contact_inbox = inbox.contact_inboxes.find_by(source_id: source_id) if source_id.present?
return @contact_inbox if @contact_inbox
ActiveRecord::Base.transaction(requires_new: true) do
build_contact_with_contact_inbox
update_contact_avatar(@contact) unless @contact.avatar.attached?
@contact_inbox
end
end end
private private
def build_contact_with_contact_inbox
@contact = find_contact || create_contact
@contact_inbox = create_contact_inbox
end
def account def account
@account ||= inbox.account @account ||= inbox.account
end end
def create_contact_inbox(contact) def create_contact_inbox
::ContactInbox.create_with(hmac_verified: hmac_verified || false).find_or_create_by!( ContactInboxBuilder.new(
contact_id: contact.id, contact: @contact,
inbox_id: inbox.id, inbox: @inbox,
source_id: source_id source_id: @source_id,
) hmac_verified: hmac_verified
).perform
end end
def update_contact_avatar(contact) def update_contact_avatar(contact)
@ -61,16 +83,4 @@ class ContactBuilder
account.contacts.find_by(phone_number: phone_number) account.contacts.find_by(phone_number: phone_number)
end end
def build_contact_inbox
ActiveRecord::Base.transaction do
contact = find_contact || create_contact
contact_inbox = create_contact_inbox(contact)
update_contact_avatar(contact)
contact_inbox
rescue StandardError => e
Rails.logger.error e
raise e
end
end
end end

View file

@ -22,10 +22,9 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder
return if @inbox.channel.reauthorization_required? return if @inbox.channel.reauthorization_required?
ActiveRecord::Base.transaction do ActiveRecord::Base.transaction do
build_contact build_contact_inbox
build_message build_message
end end
ensure_contact_avatar
rescue Koala::Facebook::AuthenticationError rescue Koala::Facebook::AuthenticationError
@inbox.channel.authorization_error! @inbox.channel.authorization_error!
rescue StandardError => e rescue StandardError => e
@ -35,15 +34,12 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder
private private
def contact def build_contact_inbox
@contact ||= @inbox.contact_inboxes.find_by(source_id: @sender_id)&.contact @contact_inbox = ::ContactInboxWithContactBuilder.new(
end source_id: @sender_id,
inbox: @inbox,
def build_contact contact_attributes: contact_params
return if contact.present? ).perform
@contact = Contact.create!(contact_params.except(:remote_avatar_url))
@contact_inbox = ContactInbox.find_or_create_by!(contact: contact, inbox: @inbox, source_id: @sender_id)
end end
def build_message def build_message
@ -54,19 +50,11 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder
end end
end end
def ensure_contact_avatar
return if contact_params[:remote_avatar_url].blank?
return if @contact.avatar.attached?
Avatar::AvatarFromUrlJob.perform_later(@contact, contact_params[:remote_avatar_url])
end
def conversation def conversation
@conversation ||= Conversation.find_by(conversation_params) || build_conversation @conversation ||= Conversation.find_by(conversation_params) || build_conversation
end end
def build_conversation def build_conversation
@contact_inbox ||= contact.contact_inboxes.find_by!(source_id: @sender_id)
Conversation.create!(conversation_params.merge( Conversation.create!(conversation_params.merge(
contact_inbox_id: @contact_inbox.id contact_inbox_id: @contact_inbox.id
)) ))
@ -94,7 +82,7 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder
{ {
account_id: @inbox.account_id, account_id: @inbox.account_id,
inbox_id: @inbox.id, inbox_id: @inbox.id,
contact_id: contact.id contact_id: @contact_inbox.contact_id
} }
end end
@ -105,7 +93,7 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder
message_type: @message_type, message_type: @message_type,
content: response.content, content: response.content,
source_id: response.identifier, source_id: response.identifier,
sender: @outgoing_echo ? nil : contact sender: @outgoing_echo ? nil : @contact_inbox.contact
} }
end end
@ -113,7 +101,7 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder
{ {
name: "#{result['first_name'] || 'John'} #{result['last_name'] || 'Doe'}", name: "#{result['first_name'] || 'John'} #{result['last_name'] || 'Doe'}",
account_id: @inbox.account_id, account_id: @inbox.account_id,
remote_avatar_url: result['profile_pic'] || '' avatar_url: result['profile_pic']
} }
end end

View file

@ -2,8 +2,11 @@ class Api::V1::Accounts::Contacts::ContactInboxesController < Api::V1::Accounts:
before_action :ensure_inbox, only: [:create] before_action :ensure_inbox, only: [:create]
def create def create
source_id = params[:source_id] || SecureRandom.uuid @contact_inbox = ContactInboxBuilder.new(
@contact_inbox = ContactInbox.create!(contact: @contact, inbox: @inbox, source_id: source_id) contact: @contact,
inbox: @inbox,
source_id: params[:source_id]
).perform
end end
private private

View file

@ -134,8 +134,11 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
return if params[:inbox_id].blank? return if params[:inbox_id].blank?
inbox = Current.account.inboxes.find(params[:inbox_id]) inbox = Current.account.inboxes.find(params[:inbox_id])
source_id = params[:source_id] || SecureRandom.uuid ContactInboxBuilder.new(
ContactInbox.create!(contact: @contact, inbox: inbox, source_id: source_id) contact: @contact,
inbox: inbox,
source_id: params[:source_id]
).perform
end end
def permitted_params def permitted_params

View file

@ -3,7 +3,7 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
include DateRangeHelper include DateRangeHelper
before_action :conversation, except: [:index, :meta, :search, :create, :filter] before_action :conversation, except: [:index, :meta, :search, :create, :filter]
before_action :contact_inbox, only: [:create] before_action :inbox, :contact, :contact_inbox, only: [:create]
def index def index
result = conversation_finder.perform result = conversation_finder.perform
@ -109,22 +109,35 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
authorize @conversation.inbox, :show? authorize @conversation.inbox, :show?
end end
def inbox
return if params[:inbox_id].blank?
@inbox = Current.account.inboxes.find(params[:inbox_id])
authorize @inbox, :show?
end
def contact
return if params[:contact_id].blank?
@contact = Current.account.contacts.find(params[:contact_id])
end
def contact_inbox def contact_inbox
@contact_inbox = build_contact_inbox @contact_inbox = build_contact_inbox
# fallback for the old case where we do look up only using source id
# In future we need to change this and make sure we do look up on combination of inbox_id and source_id
# and deprecate the support of passing only source_id as the param
@contact_inbox ||= ::ContactInbox.find_by!(source_id: params[:source_id]) @contact_inbox ||= ::ContactInbox.find_by!(source_id: params[:source_id])
authorize @contact_inbox.inbox, :show? authorize @contact_inbox.inbox, :show?
end end
def build_contact_inbox def build_contact_inbox
return if params[:contact_id].blank? || params[:inbox_id].blank? return if @inbox.blank? || @contact.blank?
inbox = Current.account.inboxes.find(params[:inbox_id])
authorize inbox, :show?
ContactInboxBuilder.new( ContactInboxBuilder.new(
contact_id: params[:contact_id], contact: @contact,
inbox_id: inbox.id, inbox: @inbox,
source_id: params[:source_id] source_id: params[:source_id]
).perform ).perform
end end

View file

@ -13,6 +13,8 @@ module RequestExceptionHandler
render_not_found_error('Resource could not be found') render_not_found_error('Resource could not be found')
rescue Pundit::NotAuthorizedError rescue Pundit::NotAuthorizedError
render_unauthorized('You are not authorized to do this action') render_unauthorized('You are not authorized to do this action')
rescue ActionController::ParameterMissing => e
render_could_not_create_error(e.message)
ensure ensure
# to address the thread variable leak issues in Puma/Thin webserver # to address the thread variable leak issues in Puma/Thin webserver
Current.reset Current.reset

View file

@ -4,7 +4,7 @@ class Public::Api::V1::Inboxes::ContactsController < Public::Api::V1::InboxesCon
def create def create
source_id = params[:source_id] || SecureRandom.uuid source_id = params[:source_id] || SecureRandom.uuid
@contact_inbox = ::ContactBuilder.new( @contact_inbox = ::ContactInboxWithContactBuilder.new(
source_id: source_id, source_id: source_id,
inbox: @inbox_channel.inbox, inbox: @inbox_channel.inbox,
contact_attributes: permitted_params.except(:identifier, :identifier_hash) contact_attributes: permitted_params.except(:identifier, :identifier_hash)

View file

@ -113,9 +113,22 @@ $default-button-height: 4.0rem;
} }
&.clear { &.clear {
color: var(--w-700);
&.secondary {
color: var(--s-700)
}
&.success {
color: var(--g-700)
}
&.alert {
color: var(--r-700)
}
&.warning { &.warning {
color: var(--y-600); color: var(--y-700)
} }
&:hover { &:hover {
@ -146,6 +159,8 @@ $default-button-height: 4.0rem;
&.small { &.small {
height: var(--space-large); height: var(--space-large);
padding-bottom: var(--space-smaller);
padding-top: var(--space-smaller);
} }
&.large { &.large {

View file

@ -14,15 +14,9 @@
} }
.modal--close { .modal--close {
border-radius: 50%;
color: $color-heading;
cursor: pointer;
font-size: $font-size-big;
line-height: $space-normal;
padding: $space-normal;
position: absolute; position: absolute;
right: $space-micro; right: $space-small;
top: $space-micro; top: $space-small;
&:hover { &:hover {
background: $color-background; background: $color-background;

View file

@ -7,9 +7,13 @@
@click="onBackDropClick" @click="onBackDropClick"
> >
<div :class="modalContainerClassName" @click.stop> <div :class="modalContainerClassName" @click.stop>
<button class="modal--close" @click="close"> <woot-button
<fluent-icon icon="dismiss" /> color-scheme="secondary"
</button> icon="dismiss"
variant="clear"
class="modal--close"
@click="close"
/>
<slot /> <slot />
</div> </div>
</div> </div>

View file

@ -16,7 +16,12 @@
</div> </div>
</td> </td>
<td> <td>
<span class="fs-small">{{ category.name }}</span> <router-link
class="fs-small button clear link secondary"
:to="getCategoryRoute(category.slug)"
>
{{ category.name }}
</router-link>
</td> </td>
<td> <td>
<span class="fs-small"> <span class="fs-small">
@ -43,6 +48,8 @@
<script> <script>
import timeMixin from 'dashboard/mixins/time'; import timeMixin from 'dashboard/mixins/time';
import portalMixin from '../mixins/portalMixin'; import portalMixin from '../mixins/portalMixin';
import { frontendURL } from 'dashboard/helper/URLHelper';
export default { export default {
mixins: [timeMixin, portalMixin], mixins: [timeMixin, portalMixin],
@ -97,6 +104,14 @@ export default {
} }
}, },
}, },
methods: {
getCategoryRoute(categorySlug) {
const { portalSlug, locale } = this.$route.params;
return frontendURL(
`accounts/${this.accountId}/portals/${portalSlug}/${locale}/categories/${categorySlug}`
);
},
},
}; };
</script> </script>

View file

@ -41,9 +41,18 @@
:class="{ 'dropdown-pane--open': showSearchDropdown }" :class="{ 'dropdown-pane--open': showSearchDropdown }"
class="dropdown-pane" class="dropdown-pane"
> >
<div class="dropdown__header">
<h4 class="text-block-title text-truncate"> <h4 class="text-block-title text-truncate">
{{ multiselectorTitle }} {{ multiselectorTitle }}
</h4> </h4>
<woot-button
icon="dismiss"
size="tiny"
color-scheme="secondary"
variant="clear"
@click="onCloseDropdown"
/>
</div>
<multiselect-dropdown-items <multiselect-dropdown-items
v-if="showSearchDropdown" v-if="showSearchDropdown"
:options="options" :options="options"
@ -172,4 +181,15 @@ export default {
width: 100%; width: 100%;
} }
} }
.dropdown__header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--space-smaller);
.text-block-title {
margin: 0;
}
}
</style> </style>

View file

@ -20,6 +20,7 @@
<woot-button <woot-button
class="multiselect-dropdown--item" class="multiselect-dropdown--item"
variant="clear" variant="clear"
color-scheme="secondary"
:class="{ :class="{
active: option.id === (selectedItem && selectedItem.id), active: option.id === (selectedItem && selectedItem.id),
}" }"
@ -129,7 +130,7 @@ export default {
width: 100%; width: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
max-height: 16rem; max-height: 20rem;
} }
.search-wrap { .search-wrap {
@ -162,7 +163,7 @@ export default {
.multiselect-dropdown--list { .multiselect-dropdown--list {
width: 100%; width: 100%;
max-height: 12rem; max-height: 16rem;
} }
.multiselect-dropdown--item { .multiselect-dropdown--item {
@ -170,13 +171,13 @@ export default {
width: 100%; width: 100%;
&.active { &.active {
background-color: var(--w-50); background-color: var(--w-25);
color: var(--w-900); color: var(--w-800);
font-weight: var(--font-weight-bold); font-weight: var(--font-weight-medium);
} }
&:focus { &:focus {
background-color: var(--color-background); background-color: var(--color-background-light);
} }
} }

View file

@ -29,19 +29,15 @@ export default {
<style lang="scss" scoped> <style lang="scss" scoped>
.dropdown-menu__item { .dropdown-menu__item {
list-style: none; list-style: none;
margin-bottom: var(--space-micro);
::v-deep { ::v-deep {
a, a,
.button { .button {
display: inline-flex;
width: 100%; width: 100%;
text-align: left; text-align: left;
color: var(--s-700); color: var(--s-700);
white-space: nowrap;
display: inline-flex;
padding: var(--space-small);
padding-top: var(--space-small);
padding-bottom: var(--space-small);
border-radius: var(--border-radius-normal);
&:hover { &:hover {
background: var(--color-background); background: var(--color-background);

View file

@ -34,7 +34,7 @@ module MailboxHelper
end end
def create_contact def create_contact
@contact_inbox = ::ContactBuilder.new( @contact_inbox = ::ContactInboxWithContactBuilder.new(
source_id: processed_mail.original_sender, source_id: processed_mail.original_sender,
inbox: @inbox, inbox: @inbox,
contact_attributes: { contact_attributes: {

View file

@ -37,16 +37,11 @@ class Channel::FacebookPage < ApplicationRecord
end end
def create_contact_inbox(instagram_id, name) def create_contact_inbox(instagram_id, name)
ActiveRecord::Base.transaction do @contact_inbox = ::ContactInboxWithContactBuilder.new({
contact = inbox.account.contacts.create!(name: name) source_id: instagram_id,
::ContactInbox.create!( inbox: inbox,
contact_id: contact.id, contact_attributes: { name: name }
inbox_id: inbox.id, }).perform
source_id: instagram_id
)
rescue StandardError => e
Rails.logger.error e
end
end end
def subscribe def subscribe

View file

@ -32,16 +32,11 @@ class Channel::TwitterProfile < ApplicationRecord
end end
def create_contact_inbox(profile_id, name, additional_attributes) def create_contact_inbox(profile_id, name, additional_attributes)
ActiveRecord::Base.transaction do ::ContactInboxWithContactBuilder.new({
contact = inbox.account.contacts.create!(additional_attributes: additional_attributes, name: name) source_id: profile_id,
::ContactInbox.create!( inbox: inbox,
contact_id: contact.id, contact_attributes: { name: name, additional_attributes: additional_attributes }
inbox_id: inbox.id, }).perform
source_id: profile_id
)
rescue StandardError => e
Rails.logger.error e
end
end end
def twitter_client def twitter_client

View file

@ -98,19 +98,9 @@ class Channel::WebWidget < ApplicationRecord
end end
def create_contact_inbox(additional_attributes = {}) def create_contact_inbox(additional_attributes = {})
ActiveRecord::Base.transaction do ::ContactInboxWithContactBuilder.new({
contact = inbox.account.contacts.create!( inbox: inbox,
name: ::Haikunator.haikunate(1000), contact_attributes: { additional_attributes: additional_attributes }
additional_attributes: additional_attributes }).perform
)
contact_inbox = ::ContactInbox.create!(
contact_id: contact.id,
inbox_id: inbox.id,
source_id: SecureRandom.uuid
)
contact_inbox
rescue StandardError => e
Rails.logger.error e
end
end end
end end

View file

@ -81,7 +81,7 @@ class Line::IncomingMessageService
end end
def set_contact def set_contact
contact_inbox = ::ContactBuilder.new( contact_inbox = ::ContactInboxWithContactBuilder.new(
source_id: line_contact_info['userId'], source_id: line_contact_info['userId'],
inbox: inbox, inbox: inbox,
contact_attributes: contact_attributes contact_attributes: contact_attributes

View file

@ -37,7 +37,7 @@ class Sms::IncomingMessageService
end end
def set_contact def set_contact
contact_inbox = ::ContactBuilder.new( contact_inbox = ::ContactInboxWithContactBuilder.new(
source_id: params[:from], source_id: params[:from],
inbox: @inbox, inbox: @inbox,
contact_attributes: contact_attributes contact_attributes: contact_attributes

View file

@ -31,7 +31,7 @@ class Telegram::IncomingMessageService
end end
def set_contact def set_contact
contact_inbox = ::ContactBuilder.new( contact_inbox = ::ContactInboxWithContactBuilder.new(
source_id: params[:message][:from][:id], source_id: params[:message][:from][:id],
inbox: inbox, inbox: inbox,
contact_attributes: contact_attributes contact_attributes: contact_attributes

View file

@ -47,7 +47,7 @@ class Twilio::IncomingMessageService
end end
def set_contact def set_contact
contact_inbox = ::ContactBuilder.new( contact_inbox = ::ContactInboxWithContactBuilder.new(
source_id: params[:From], source_id: params[:From],
inbox: inbox, inbox: inbox,
contact_attributes: contact_attributes contact_attributes: contact_attributes

View file

@ -12,7 +12,7 @@ class Whatsapp::IncomingMessageBaseService
set_conversation set_conversation
return if @processed_params[:messages].blank? return if @processed_params[:messages].blank? || unprocessable_message_type?
@message = @conversation.messages.build( @message = @conversation.messages.build(
content: message_content(@processed_params[:messages].first), content: message_content(@processed_params[:messages].first),
@ -48,7 +48,7 @@ class Whatsapp::IncomingMessageBaseService
contact_params = @processed_params[:contacts]&.first contact_params = @processed_params[:contacts]&.first
return if contact_params.blank? return if contact_params.blank?
contact_inbox = ::ContactBuilder.new( contact_inbox = ::ContactInboxWithContactBuilder.new(
source_id: contact_params[:wa_id], source_id: contact_params[:wa_id],
inbox: inbox, inbox: inbox,
contact_attributes: { name: contact_params.dig(:profile, :name), phone_number: "+#{@processed_params[:messages].first[:from]}" } contact_attributes: { name: contact_params.dig(:profile, :name), phone_number: "+#{@processed_params[:messages].first[:from]}" }
@ -86,6 +86,10 @@ class Whatsapp::IncomingMessageBaseService
@processed_params[:messages].first[:type] @processed_params[:messages].first[:type]
end end
def unprocessable_message_type?
%w[reaction contacts].include?(message_type)
end
def attach_files def attach_files
return if %w[text button interactive].include?(message_type) return if %w[text button interactive].include?(message_type)

View file

@ -46,8 +46,13 @@ unless Rails.env.production?
inbox = Inbox.create!(channel: web_widget, account: account, name: 'Acme Support') inbox = Inbox.create!(channel: web_widget, account: account, name: 'Acme Support')
InboxMember.create!(user: user, inbox: inbox) InboxMember.create!(user: user, inbox: inbox)
contact = Contact.create!(name: 'jane', email: 'jane@example.com', phone_number: '+2320000', account: account) contact = ::ContactInboxWithContactBuilder.new(
contact_inbox = ContactInbox.create!(inbox: inbox, contact: contact, source_id: user.id, hmac_verified: true) source_id: user.id,
inbox: inbox,
hmac_verified: true,
contact_attributes: { name: 'jane', email: 'jane@example.com', phone_number: '+2320000' }
).perform&.contact
conversation = Conversation.create!( conversation = Conversation.create!(
account: account, account: account,
inbox: inbox, inbox: inbox,

View file

@ -12,8 +12,8 @@ describe ::ContactInboxBuilder do
it 'does not create contact inbox when contact inbox already exists with the source id provided' do it 'does not create contact inbox when contact inbox already exists with the source id provided' do
existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: twilio_inbox, source_id: contact.phone_number) existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: twilio_inbox, source_id: contact.phone_number)
contact_inbox = described_class.new( contact_inbox = described_class.new(
contact_id: contact.id, contact: contact,
inbox_id: twilio_inbox.id, inbox: twilio_inbox,
source_id: contact.phone_number source_id: contact.phone_number
).perform ).perform
@ -23,8 +23,8 @@ describe ::ContactInboxBuilder do
it 'does not create contact inbox when contact inbox already exists with phone number and source id is not provided' do it 'does not create contact inbox when contact inbox already exists with phone number and source id is not provided' do
existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: twilio_inbox, source_id: contact.phone_number) existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: twilio_inbox, source_id: contact.phone_number)
contact_inbox = described_class.new( contact_inbox = described_class.new(
contact_id: contact.id, contact: contact,
inbox_id: twilio_inbox.id inbox: twilio_inbox
).perform ).perform
expect(contact_inbox.id).to eq(existing_contact_inbox.id) expect(contact_inbox.id).to eq(existing_contact_inbox.id)
@ -33,8 +33,8 @@ describe ::ContactInboxBuilder do
it 'creates a new contact inbox when different source id is provided' do it 'creates a new contact inbox when different source id is provided' do
existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: twilio_inbox, source_id: contact.phone_number) existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: twilio_inbox, source_id: contact.phone_number)
contact_inbox = described_class.new( contact_inbox = described_class.new(
contact_id: contact.id, contact: contact,
inbox_id: twilio_inbox.id, inbox: twilio_inbox,
source_id: '+224213223422' source_id: '+224213223422'
).perform ).perform
@ -44,12 +44,23 @@ describe ::ContactInboxBuilder do
it 'creates a contact inbox with contact phone number when source id not provided and no contact inbox exists' do it 'creates a contact inbox with contact phone number when source id not provided and no contact inbox exists' do
contact_inbox = described_class.new( contact_inbox = described_class.new(
contact_id: contact.id, contact: contact,
inbox_id: twilio_inbox.id inbox: twilio_inbox
).perform ).perform
expect(contact_inbox.source_id).to eq(contact.phone_number) expect(contact_inbox.source_id).to eq(contact.phone_number)
end end
it 'raises error when contact phone number is not present and no source id is provided' do
contact.update!(phone_number: nil)
expect do
described_class.new(
contact: contact,
inbox: twilio_inbox
).perform
end.to raise_error(ActionController::ParameterMissing, 'param is missing or the value is empty: contact phone number')
end
end end
describe 'twilio whatsapp inbox' do describe 'twilio whatsapp inbox' do
@ -59,8 +70,8 @@ describe ::ContactInboxBuilder do
it 'does not create contact inbox when contact inbox already exists with the source id provided' do it 'does not create contact inbox when contact inbox already exists with the source id provided' do
existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: twilio_inbox, source_id: "whatsapp:#{contact.phone_number}") existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: twilio_inbox, source_id: "whatsapp:#{contact.phone_number}")
contact_inbox = described_class.new( contact_inbox = described_class.new(
contact_id: contact.id, contact: contact,
inbox_id: twilio_inbox.id, inbox: twilio_inbox,
source_id: "whatsapp:#{contact.phone_number}" source_id: "whatsapp:#{contact.phone_number}"
).perform ).perform
@ -70,8 +81,8 @@ describe ::ContactInboxBuilder do
it 'does not create contact inbox when contact inbox already exists with phone number and source id is not provided' do it 'does not create contact inbox when contact inbox already exists with phone number and source id is not provided' do
existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: twilio_inbox, source_id: "whatsapp:#{contact.phone_number}") existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: twilio_inbox, source_id: "whatsapp:#{contact.phone_number}")
contact_inbox = described_class.new( contact_inbox = described_class.new(
contact_id: contact.id, contact: contact,
inbox_id: twilio_inbox.id inbox: twilio_inbox
).perform ).perform
expect(contact_inbox.id).to eq(existing_contact_inbox.id) expect(contact_inbox.id).to eq(existing_contact_inbox.id)
@ -80,8 +91,8 @@ describe ::ContactInboxBuilder do
it 'creates a new contact inbox when different source id is provided' do it 'creates a new contact inbox when different source id is provided' do
existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: twilio_inbox, source_id: "whatsapp:#{contact.phone_number}") existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: twilio_inbox, source_id: "whatsapp:#{contact.phone_number}")
contact_inbox = described_class.new( contact_inbox = described_class.new(
contact_id: contact.id, contact: contact,
inbox_id: twilio_inbox.id, inbox: twilio_inbox,
source_id: 'whatsapp:+555555' source_id: 'whatsapp:+555555'
).perform ).perform
@ -91,12 +102,23 @@ describe ::ContactInboxBuilder do
it 'creates a contact inbox with contact phone number when source id not provided and no contact inbox exists' do it 'creates a contact inbox with contact phone number when source id not provided and no contact inbox exists' do
contact_inbox = described_class.new( contact_inbox = described_class.new(
contact_id: contact.id, contact: contact,
inbox_id: twilio_inbox.id inbox: twilio_inbox
).perform ).perform
expect(contact_inbox.source_id).to eq("whatsapp:#{contact.phone_number}") expect(contact_inbox.source_id).to eq("whatsapp:#{contact.phone_number}")
end end
it 'raises error when contact phone number is not present and no source id is provided' do
contact.update!(phone_number: nil)
expect do
described_class.new(
contact: contact,
inbox: twilio_inbox
).perform
end.to raise_error(ActionController::ParameterMissing, 'param is missing or the value is empty: contact phone number')
end
end end
describe 'whatsapp inbox' do describe 'whatsapp inbox' do
@ -105,8 +127,8 @@ describe ::ContactInboxBuilder do
it 'does not create contact inbox when contact inbox already exists with the source id provided' do it 'does not create contact inbox when contact inbox already exists with the source id provided' do
existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: whatsapp_inbox, source_id: contact.phone_number&.delete('+')) existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: whatsapp_inbox, source_id: contact.phone_number&.delete('+'))
contact_inbox = described_class.new( contact_inbox = described_class.new(
contact_id: contact.id, contact: contact,
inbox_id: whatsapp_inbox.id, inbox: whatsapp_inbox,
source_id: contact.phone_number&.delete('+') source_id: contact.phone_number&.delete('+')
).perform ).perform
@ -116,8 +138,8 @@ describe ::ContactInboxBuilder do
it 'does not create contact inbox when contact inbox already exists with phone number and source id is not provided' do it 'does not create contact inbox when contact inbox already exists with phone number and source id is not provided' do
existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: whatsapp_inbox, source_id: contact.phone_number&.delete('+')) existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: whatsapp_inbox, source_id: contact.phone_number&.delete('+'))
contact_inbox = described_class.new( contact_inbox = described_class.new(
contact_id: contact.id, contact: contact,
inbox_id: whatsapp_inbox.id inbox: whatsapp_inbox
).perform ).perform
expect(contact_inbox.id).to be(existing_contact_inbox.id) expect(contact_inbox.id).to be(existing_contact_inbox.id)
@ -126,8 +148,8 @@ describe ::ContactInboxBuilder do
it 'creates a new contact inbox when different source id is provided' do it 'creates a new contact inbox when different source id is provided' do
existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: whatsapp_inbox, source_id: contact.phone_number&.delete('+')) existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: whatsapp_inbox, source_id: contact.phone_number&.delete('+'))
contact_inbox = described_class.new( contact_inbox = described_class.new(
contact_id: contact.id, contact: contact,
inbox_id: whatsapp_inbox.id, inbox: whatsapp_inbox,
source_id: '555555' source_id: '555555'
).perform ).perform
@ -137,12 +159,23 @@ describe ::ContactInboxBuilder do
it 'creates a contact inbox with contact phone number when source id not provided and no contact inbox exists' do it 'creates a contact inbox with contact phone number when source id not provided and no contact inbox exists' do
contact_inbox = described_class.new( contact_inbox = described_class.new(
contact_id: contact.id, contact: contact,
inbox_id: whatsapp_inbox.id inbox: whatsapp_inbox
).perform ).perform
expect(contact_inbox.source_id).to eq(contact.phone_number&.delete('+')) expect(contact_inbox.source_id).to eq(contact.phone_number&.delete('+'))
end end
it 'raises error when contact phone number is not present and no source id is provided' do
contact.update!(phone_number: nil)
expect do
described_class.new(
contact: contact,
inbox: whatsapp_inbox
).perform
end.to raise_error(ActionController::ParameterMissing, 'param is missing or the value is empty: contact phone number')
end
end end
describe 'sms inbox' do describe 'sms inbox' do
@ -152,8 +185,8 @@ describe ::ContactInboxBuilder do
it 'does not create contact inbox when contact inbox already exists with the source id provided' do it 'does not create contact inbox when contact inbox already exists with the source id provided' do
existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: sms_inbox, source_id: contact.phone_number) existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: sms_inbox, source_id: contact.phone_number)
contact_inbox = described_class.new( contact_inbox = described_class.new(
contact_id: contact.id, contact: contact,
inbox_id: sms_inbox.id, inbox: sms_inbox,
source_id: contact.phone_number source_id: contact.phone_number
).perform ).perform
@ -163,8 +196,8 @@ describe ::ContactInboxBuilder do
it 'does not create contact inbox when contact inbox already exists with phone number and source id is not provided' do it 'does not create contact inbox when contact inbox already exists with phone number and source id is not provided' do
existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: sms_inbox, source_id: contact.phone_number) existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: sms_inbox, source_id: contact.phone_number)
contact_inbox = described_class.new( contact_inbox = described_class.new(
contact_id: contact.id, contact: contact,
inbox_id: sms_inbox.id inbox: sms_inbox
).perform ).perform
expect(contact_inbox.id).to eq(existing_contact_inbox.id) expect(contact_inbox.id).to eq(existing_contact_inbox.id)
@ -173,8 +206,8 @@ describe ::ContactInboxBuilder do
it 'creates a new contact inbox when different source id is provided' do it 'creates a new contact inbox when different source id is provided' do
existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: sms_inbox, source_id: contact.phone_number) existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: sms_inbox, source_id: contact.phone_number)
contact_inbox = described_class.new( contact_inbox = described_class.new(
contact_id: contact.id, contact: contact,
inbox_id: sms_inbox.id, inbox: sms_inbox,
source_id: '+224213223422' source_id: '+224213223422'
).perform ).perform
@ -184,12 +217,23 @@ describe ::ContactInboxBuilder do
it 'creates a contact inbox with contact phone number when source id not provided and no contact inbox exists' do it 'creates a contact inbox with contact phone number when source id not provided and no contact inbox exists' do
contact_inbox = described_class.new( contact_inbox = described_class.new(
contact_id: contact.id, contact: contact,
inbox_id: sms_inbox.id inbox: sms_inbox
).perform ).perform
expect(contact_inbox.source_id).to eq(contact.phone_number) expect(contact_inbox.source_id).to eq(contact.phone_number)
end end
it 'raises error when contact phone number is not present and no source id is provided' do
contact.update!(phone_number: nil)
expect do
described_class.new(
contact: contact,
inbox: sms_inbox
).perform
end.to raise_error(ActionController::ParameterMissing, 'param is missing or the value is empty: contact phone number')
end
end end
describe 'email inbox' do describe 'email inbox' do
@ -199,8 +243,8 @@ describe ::ContactInboxBuilder do
it 'does not create contact inbox when contact inbox already exists with the source id provided' do it 'does not create contact inbox when contact inbox already exists with the source id provided' do
existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: email_inbox, source_id: contact.email) existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: email_inbox, source_id: contact.email)
contact_inbox = described_class.new( contact_inbox = described_class.new(
contact_id: contact.id, contact: contact,
inbox_id: email_inbox.id, inbox: email_inbox,
source_id: contact.email source_id: contact.email
).perform ).perform
@ -210,8 +254,8 @@ describe ::ContactInboxBuilder do
it 'does not create contact inbox when contact inbox already exists with email and source id is not provided' do it 'does not create contact inbox when contact inbox already exists with email and source id is not provided' do
existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: email_inbox, source_id: contact.email) existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: email_inbox, source_id: contact.email)
contact_inbox = described_class.new( contact_inbox = described_class.new(
contact_id: contact.id, contact: contact,
inbox_id: email_inbox.id inbox: email_inbox
).perform ).perform
expect(contact_inbox.id).to eq(existing_contact_inbox.id) expect(contact_inbox.id).to eq(existing_contact_inbox.id)
@ -220,8 +264,8 @@ describe ::ContactInboxBuilder do
it 'creates a new contact inbox when different source id is provided' do it 'creates a new contact inbox when different source id is provided' do
existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: email_inbox, source_id: contact.email) existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: email_inbox, source_id: contact.email)
contact_inbox = described_class.new( contact_inbox = described_class.new(
contact_id: contact.id, contact: contact,
inbox_id: email_inbox.id, inbox: email_inbox,
source_id: 'xyc@xyc.com' source_id: 'xyc@xyc.com'
).perform ).perform
@ -231,12 +275,23 @@ describe ::ContactInboxBuilder do
it 'creates a contact inbox with contact email when source id not provided and no contact inbox exists' do it 'creates a contact inbox with contact email when source id not provided and no contact inbox exists' do
contact_inbox = described_class.new( contact_inbox = described_class.new(
contact_id: contact.id, contact: contact,
inbox_id: email_inbox.id inbox: email_inbox
).perform ).perform
expect(contact_inbox.source_id).to eq(contact.email) expect(contact_inbox.source_id).to eq(contact.email)
end end
it 'raises error when contact email is not present and no source id is provided' do
contact.update!(email: nil)
expect do
described_class.new(
contact: contact,
inbox: email_inbox
).perform
end.to raise_error(ActionController::ParameterMissing, 'param is missing or the value is empty: contact email')
end
end end
describe 'api inbox' do describe 'api inbox' do
@ -246,8 +301,8 @@ describe ::ContactInboxBuilder do
it 'does not create contact inbox when contact inbox already exists with the source id provided' do it 'does not create contact inbox when contact inbox already exists with the source id provided' do
existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: api_inbox, source_id: 'test') existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: api_inbox, source_id: 'test')
contact_inbox = described_class.new( contact_inbox = described_class.new(
contact_id: contact.id, contact: contact,
inbox_id: api_inbox.id, inbox: api_inbox,
source_id: 'test' source_id: 'test'
).perform ).perform
@ -257,8 +312,8 @@ describe ::ContactInboxBuilder do
it 'creates a new contact inbox when different source id is provided' do it 'creates a new contact inbox when different source id is provided' do
existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: api_inbox, source_id: SecureRandom.uuid) existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: api_inbox, source_id: SecureRandom.uuid)
contact_inbox = described_class.new( contact_inbox = described_class.new(
contact_id: contact.id, contact: contact,
inbox_id: api_inbox.id, inbox: api_inbox,
source_id: 'test' source_id: 'test'
).perform ).perform
@ -268,61 +323,12 @@ describe ::ContactInboxBuilder do
it 'creates a contact inbox with SecureRandom.uuid when source id not provided and no contact inbox exists' do it 'creates a contact inbox with SecureRandom.uuid when source id not provided and no contact inbox exists' do
contact_inbox = described_class.new( contact_inbox = described_class.new(
contact_id: contact.id, contact: contact,
inbox_id: api_inbox.id inbox: api_inbox
).perform ).perform
expect(contact_inbox.source_id).not_to be_nil expect(contact_inbox.source_id).not_to be_nil
end end
end end
describe 'web widget' do
let!(:website_channel) { create(:channel_widget, account: account) }
let!(:website_inbox) { create(:inbox, channel: website_channel, account: account) }
it 'does not create contact inbox' do
contact_inbox = described_class.new(
contact_id: contact.id,
inbox_id: website_inbox.id,
source_id: 'test'
).perform
expect(contact_inbox).to be_nil
end
end
describe 'facebook inbox' do
before do
stub_request(:post, /graph.facebook.com/)
end
let!(:facebook_channel) { create(:channel_facebook_page, account: account) }
let!(:facebook_inbox) { create(:inbox, channel: facebook_channel, account: account) }
it 'does not create contact inbox' do
contact_inbox = described_class.new(
contact_id: contact.id,
inbox_id: facebook_inbox.id,
source_id: 'test'
).perform
expect(contact_inbox).to be_nil
end
end
describe 'twitter inbox' do
let!(:twitter_channel) { create(:channel_twitter_profile, account: account) }
let!(:twitter_inbox) { create(:inbox, channel: twitter_channel, account: account) }
it 'does not create contact inbox' do
contact_inbox = described_class.new(
contact_id: contact.id,
inbox_id: twitter_inbox.id,
source_id: 'test'
).perform
expect(contact_inbox).to be_nil
end
end
end end
end end

View file

@ -1,6 +1,6 @@
require 'rails_helper' require 'rails_helper'
describe ::ContactBuilder do describe ::ContactInboxWithContactBuilder do
let(:account) { create(:account) } let(:account) { create(:account) }
let(:inbox) { create(:inbox, account: account) } let(:inbox) { create(:inbox, account: account) }
let(:contact) { create(:contact, email: 'xyc@example.com', phone_number: '+23423424123', account: account, identifier: '123') } let(:contact) { create(:contact, email: 'xyc@example.com', phone_number: '+23423424123', account: account, identifier: '123') }

View file

@ -62,6 +62,59 @@ RSpec.describe Webhooks::WhatsappEventsJob, type: :job do
job.perform_now(wb_params) job.perform_now(wb_params)
end end
it 'Ignore reaction type message and stop raising error' do
other_channel = create(:channel_whatsapp, phone_number: '+1987654', provider: 'whatsapp_cloud', sync_templates: false,
validate_provider_config: false)
wb_params = {
phone_number: channel.phone_number,
object: 'whatsapp_business_account',
entry: [{
changes: [{
value: {
contacts: [{ profile: { name: 'Test Test' }, wa_id: '1111981136571' }],
messages: [{
from: '1111981136571', reaction: { emoji: '👍' }, timestamp: '1664799904', type: 'reaction'
}],
metadata: {
phone_number_id: other_channel.provider_config['phone_number_id'],
display_phone_number: other_channel.phone_number.delete('+')
}
}
}]
}]
}.with_indifferent_access
expect do
Whatsapp::IncomingMessageWhatsappCloudService.new(inbox: other_channel.inbox, params: wb_params).perform
end.not_to change(Message, :count)
end
it 'Ignore contacts type message and stop raising error' do
other_channel = create(:channel_whatsapp, phone_number: '+1987654', provider: 'whatsapp_cloud', sync_templates: false,
validate_provider_config: false)
wb_params = {
phone_number: channel.phone_number,
object: 'whatsapp_business_account',
entry: [{
changes: [{
value: {
contacts: [{ profile: { name: 'Test Test' }, wa_id: '1111981136571' }],
messages: [{ from: '1111981136571',
contacts: [{ phones: [{ phone: '+1987654' }], name: { first_name: 'contact name' } }],
timestamp: '1664799904',
type: 'contacts' }],
metadata: {
phone_number_id: other_channel.provider_config['phone_number_id'],
display_phone_number: other_channel.phone_number.delete('+')
}
}
}]
}]
}.with_indifferent_access
expect do
Whatsapp::IncomingMessageWhatsappCloudService.new(inbox: other_channel.inbox, params: wb_params).perform
end.not_to change(Message, :count)
end
it 'will not enque Whatsapp::IncomingMessageWhatsappCloudService when invalid phone number id' do it 'will not enque Whatsapp::IncomingMessageWhatsappCloudService when invalid phone number id' do
other_channel = create(:channel_whatsapp, phone_number: '+1987654', provider: 'whatsapp_cloud', sync_templates: false, other_channel = create(:channel_whatsapp, phone_number: '+1987654', provider: 'whatsapp_cloud', sync_templates: false,
validate_provider_config: false) validate_provider_config: false)