From 8079bf50a0f656c5e3120919e34df3eb5ee25140 Mon Sep 17 00:00:00 2001 From: Sojan Jose Date: Tue, 21 Jul 2020 12:15:24 +0530 Subject: [PATCH] Feature: API Channel (#1052) --- .../messages/facebook/message_builder.rb | 139 ++++++++++++++++ .../messages/incoming_message_builder.rb | 2 - app/builders/messages/message_builder.rb | 154 ++++-------------- .../messages/outgoing/echo_builder.rb | 2 - .../messages/outgoing/normal_builder.rb | 46 ------ .../api/v1/accounts/contacts_controller.rb | 16 +- .../conversations/messages_controller.rb | 4 +- .../api/v1/accounts/inboxes_controller.rb | 23 ++- .../dashboard/assets/images/channels/api.png | Bin 0 -> 42199 bytes .../assets/images/channels/email.png | Bin 0 -> 5659 bytes .../components/layout/SidebarItem.vue | 8 + .../components/widgets/ChannelItem.vue | 13 +- .../dashboard/i18n/locale/en/inboxMgmt.json | 37 +++++ .../dashboard/settings/inbox/ChannelList.vue | 2 + .../dashboard/settings/inbox/FinishSetup.vue | 15 ++ .../routes/dashboard/settings/inbox/Index.vue | 6 + .../settings/inbox/channel-factory.js | 4 + .../dashboard/settings/inbox/channels/Api.vue | 110 +++++++++++++ .../settings/inbox/channels/Email.vue | 113 +++++++++++++ .../dashboard/store/modules/inboxes.js | 12 ++ app/listeners/webhook_listener.rb | 6 +- app/models/account.rb | 2 + app/models/channel/api.rb | 19 +++ app/models/channel/email.rb | 35 ++++ app/models/channel/facebook_page.rb | 3 - .../v1/accounts/contacts/create.json.jbuilder | 9 + .../v1/accounts/inboxes/create.json.jbuilder | 15 +- .../v1/accounts/inboxes/index.json.jbuilder | 16 +- .../v1/accounts/inboxes/update.json.jbuilder | 15 +- app/views/api/v1/models/_inbox.json.jbuilder | 16 ++ .../20200627115105_create_api_channel.rb | 9 + .../20200715124113_create_email_channel.rb | 10 ++ db/schema.rb | 17 ++ lib/integrations/facebook/message_creator.rb | 20 +-- lib/webhooks/trigger.rb | 2 +- .../message_builder_spec.rb} | 2 +- .../builders/messages/message_builder_spec.rb | 54 ++++++ .../v1/accounts/contacts_controller_spec.rb | 10 ++ spec/factories/channel/channel_api.rb | 9 + spec/lib/webhooks/trigger_spec.rb | 6 +- 40 files changed, 735 insertions(+), 246 deletions(-) create mode 100644 app/builders/messages/facebook/message_builder.rb delete mode 100644 app/builders/messages/incoming_message_builder.rb delete mode 100644 app/builders/messages/outgoing/echo_builder.rb delete mode 100644 app/builders/messages/outgoing/normal_builder.rb create mode 100644 app/javascript/dashboard/assets/images/channels/api.png create mode 100644 app/javascript/dashboard/assets/images/channels/email.png create mode 100644 app/javascript/dashboard/routes/dashboard/settings/inbox/channels/Api.vue create mode 100644 app/javascript/dashboard/routes/dashboard/settings/inbox/channels/Email.vue create mode 100644 app/models/channel/api.rb create mode 100644 app/models/channel/email.rb create mode 100644 app/views/api/v1/accounts/contacts/create.json.jbuilder create mode 100644 app/views/api/v1/models/_inbox.json.jbuilder create mode 100644 db/migrate/20200627115105_create_api_channel.rb create mode 100644 db/migrate/20200715124113_create_email_channel.rb rename spec/builders/messages/{incoming_message_builder_spec.rb => facebook/message_builder_spec.rb} (95%) create mode 100644 spec/builders/messages/message_builder_spec.rb create mode 100644 spec/factories/channel/channel_api.rb diff --git a/app/builders/messages/facebook/message_builder.rb b/app/builders/messages/facebook/message_builder.rb new file mode 100644 index 000000000..0095b9e1b --- /dev/null +++ b/app/builders/messages/facebook/message_builder.rb @@ -0,0 +1,139 @@ +# This class creates both outgoing messages from chatwoot and echo outgoing messages based on the flag `outgoing_echo` +# Assumptions +# 1. Incase of an outgoing message which is echo, source_id will NOT be nil, +# based on this we are showing "not sent from chatwoot" message in frontend +# Hence there is no need to set user_id in message for outgoing echo messages. + +class Messages::Facebook::MessageBuilder + attr_reader :response + + def initialize(response, inbox, outgoing_echo = false) + @response = response + @inbox = inbox + @sender_id = (outgoing_echo ? @response.recipient_id : @response.sender_id) + @message_type = (outgoing_echo ? :outgoing : :incoming) + end + + def perform + ActiveRecord::Base.transaction do + build_contact + build_message + end + rescue StandardError => e + Raven.capture_exception(e) + true + end + + private + + def contact + @contact ||= @inbox.contact_inboxes.find_by(source_id: @sender_id)&.contact + end + + def build_contact + return if contact.present? + + @contact = Contact.create!(contact_params.except(:remote_avatar_url)) + ContactAvatarJob.perform_later(@contact, contact_params[:remote_avatar_url]) if contact_params[:remote_avatar_url] + @contact_inbox = ContactInbox.create(contact: contact, inbox: @inbox, source_id: @sender_id) + end + + def build_message + @message = conversation.messages.create!(message_params) + (response.attachments || []).each do |attachment| + attachment_obj = @message.attachments.new(attachment_params(attachment).except(:remote_file_url)) + attachment_obj.save! + attach_file(attachment_obj, attachment_params(attachment)[:remote_file_url]) if attachment_params(attachment)[:remote_file_url] + end + end + + def attach_file(attachment, file_url) + file_resource = LocalResource.new(file_url) + attachment.file.attach(io: file_resource.file, filename: file_resource.tmp_filename, content_type: file_resource.encoding) + end + + def conversation + @conversation ||= Conversation.find_by(conversation_params) || build_conversation + end + + def build_conversation + @contact_inbox ||= contact.contact_inboxes.find_by!(source_id: @sender_id) + Conversation.create!(conversation_params.merge( + contact_inbox_id: @contact_inbox.id + )) + end + + def attachment_params(attachment) + file_type = attachment['type'].to_sym + params = { file_type: file_type, account_id: @message.account_id } + + if [:image, :file, :audio, :video].include? file_type + params.merge!(file_type_params(attachment)) + elsif file_type == :location + params.merge!(location_params(attachment)) + elsif file_type == :fallback + params.merge!(fallback_params(attachment)) + end + + params + end + + def file_type_params(attachment) + { + external_url: attachment['payload']['url'], + remote_file_url: attachment['payload']['url'] + } + end + + def location_params(attachment) + lat = attachment['payload']['coordinates']['lat'] + long = attachment['payload']['coordinates']['long'] + { + external_url: attachment['url'], + coordinates_lat: lat, + coordinates_long: long, + fallback_title: attachment['title'] + } + end + + def fallback_params(attachment) + { + fallback_title: attachment['title'], + external_url: attachment['url'] + } + end + + def conversation_params + { + account_id: @inbox.account_id, + inbox_id: @inbox.id, + contact_id: contact.id + } + end + + def message_params + { + account_id: conversation.account_id, + inbox_id: conversation.inbox_id, + message_type: @message_type, + content: response.content, + source_id: response.identifier, + sender: contact + } + end + + def contact_params + begin + k = Koala::Facebook::API.new(@inbox.channel.page_access_token) if @inbox.facebook? + result = k.get_object(@sender_id) || {} + rescue StandardError => e + result = {} + Raven.capture_exception(e) + end + { + name: "#{result['first_name'] || 'John'} #{result['last_name'] || 'Doe'}", + account_id: @inbox.account_id, + remote_avatar_url: result['profile_pic'] || '' + } + end +end diff --git a/app/builders/messages/incoming_message_builder.rb b/app/builders/messages/incoming_message_builder.rb deleted file mode 100644 index 3a01c703a..000000000 --- a/app/builders/messages/incoming_message_builder.rb +++ /dev/null @@ -1,2 +0,0 @@ -class Messages::IncomingMessageBuilder < Messages::MessageBuilder -end diff --git a/app/builders/messages/message_builder.rb b/app/builders/messages/message_builder.rb index a8251edb4..e53bcf377 100644 --- a/app/builders/messages/message_builder.rb +++ b/app/builders/messages/message_builder.rb @@ -1,139 +1,57 @@ -# This class creates both outgoing messages from chatwoot and echo outgoing messages based on the flag `outgoing_echo` -# Assumptions -# 1. Incase of an outgoing message which is echo, source_id will NOT be nil, -# based on this we are showing "not sent from chatwoot" message in frontend -# Hence there is no need to set user_id in message for outgoing echo messages. - class Messages::MessageBuilder - attr_reader :response + include ::FileTypeHelper + attr_reader :message - def initialize(response, inbox, outgoing_echo = false) - @response = response - @inbox = inbox - @sender_id = (outgoing_echo ? @response.recipient_id : @response.sender_id) - @message_type = (outgoing_echo ? :outgoing : :incoming) + def initialize(user, conversation, params) + @content = params[:content] + @private = params[:private] || false + @conversation = conversation + @user = user + @message_type = params[:message_type] || 'outgoing' + @content_type = params[:content_type] + @items = params.to_unsafe_h&.dig(:content_attributes, :items) + @attachments = params[:attachments] end def perform - ActiveRecord::Base.transaction do - build_contact - build_message + @message = @conversation.messages.build(message_params) + if @attachments.present? + @attachments.each do |uploaded_attachment| + attachment = @message.attachments.new( + account_id: @message.account_id, + file_type: file_type(uploaded_attachment&.content_type) + ) + attachment.file.attach(uploaded_attachment) + end end - rescue StandardError => e - Raven.capture_exception(e) - true + @message.save + @message end private - def contact - @contact ||= @inbox.contact_inboxes.find_by(source_id: @sender_id)&.contact - end - - def build_contact - return if contact.present? - - @contact = Contact.create!(contact_params.except(:remote_avatar_url)) - ContactAvatarJob.perform_later(@contact, contact_params[:remote_avatar_url]) if contact_params[:remote_avatar_url] - @contact_inbox = ContactInbox.create(contact: contact, inbox: @inbox, source_id: @sender_id) - end - - def build_message - @message = conversation.messages.create!(message_params) - (response.attachments || []).each do |attachment| - attachment_obj = @message.attachments.new(attachment_params(attachment).except(:remote_file_url)) - attachment_obj.save! - attach_file(attachment_obj, attachment_params(attachment)[:remote_file_url]) if attachment_params(attachment)[:remote_file_url] - end - end - - def attach_file(attachment, file_url) - file_resource = LocalResource.new(file_url) - attachment.file.attach(io: file_resource.file, filename: file_resource.tmp_filename, content_type: file_resource.encoding) - end - - def conversation - @conversation ||= Conversation.find_by(conversation_params) || build_conversation - end - - def build_conversation - @contact_inbox ||= contact.contact_inboxes.find_by!(source_id: @sender_id) - Conversation.create!(conversation_params.merge( - contact_inbox_id: @contact_inbox.id - )) - end - - def attachment_params(attachment) - file_type = attachment['type'].to_sym - params = { file_type: file_type, account_id: @message.account_id } - - if [:image, :file, :audio, :video].include? file_type - params.merge!(file_type_params(attachment)) - elsif file_type == :location - params.merge!(location_params(attachment)) - elsif file_type == :fallback - params.merge!(fallback_params(attachment)) + def message_type + if @conversation.inbox.channel.class != Channel::Api && @message_type == 'incoming' + raise StandardError, 'Incoming messages are only allowed in Api inboxes' end - params + @message_type end - def file_type_params(attachment) - { - external_url: attachment['payload']['url'], - remote_file_url: attachment['payload']['url'] - } - end - - def location_params(attachment) - lat = attachment['payload']['coordinates']['lat'] - long = attachment['payload']['coordinates']['long'] - { - external_url: attachment['url'], - coordinates_lat: lat, - coordinates_long: long, - fallback_title: attachment['title'] - } - end - - def fallback_params(attachment) - { - fallback_title: attachment['title'], - external_url: attachment['url'] - } - end - - def conversation_params - { - account_id: @inbox.account_id, - inbox_id: @inbox.id, - contact_id: contact.id - } + def sender + message_type == 'outgoing' ? @user : @conversation.contact end def message_params { - account_id: conversation.account_id, - inbox_id: conversation.inbox_id, - message_type: @message_type, - content: response.content, - source_id: response.identifier, - sender: contact - } - end - - def contact_params - begin - k = Koala::Facebook::API.new(@inbox.channel.page_access_token) if @inbox.facebook? - result = k.get_object(@sender_id) || {} - rescue Exception => e - result = {} - Raven.capture_exception(e) - end - { - name: "#{result['first_name'] || 'John'} #{result['last_name'] || 'Doe'}", - account_id: @inbox.account_id, - remote_avatar_url: result['profile_pic'] || '' + account_id: @conversation.account_id, + inbox_id: @conversation.inbox_id, + message_type: message_type, + content: @content, + private: @private, + sender: sender, + content_type: @content_type, + items: @items } end end diff --git a/app/builders/messages/outgoing/echo_builder.rb b/app/builders/messages/outgoing/echo_builder.rb deleted file mode 100644 index 7d5c3fa79..000000000 --- a/app/builders/messages/outgoing/echo_builder.rb +++ /dev/null @@ -1,2 +0,0 @@ -class Messages::Outgoing::EchoBuilder < ::Messages::MessageBuilder -end diff --git a/app/builders/messages/outgoing/normal_builder.rb b/app/builders/messages/outgoing/normal_builder.rb deleted file mode 100644 index 78572b0ee..000000000 --- a/app/builders/messages/outgoing/normal_builder.rb +++ /dev/null @@ -1,46 +0,0 @@ -class Messages::Outgoing::NormalBuilder - include ::FileTypeHelper - attr_reader :message - - def initialize(user, conversation, params) - @content = params[:content] - @private = params[:private] || false - @conversation = conversation - @user = user - @fb_id = params[:fb_id] - @content_type = params[:content_type] - @items = params.to_unsafe_h&.dig(:content_attributes, :items) - @attachments = params[:attachments] - end - - def perform - @message = @conversation.messages.build(message_params) - if @attachments.present? - @attachments.each do |uploaded_attachment| - attachment = @message.attachments.new( - account_id: @message.account_id, - file_type: file_type(uploaded_attachment&.content_type) - ) - attachment.file.attach(uploaded_attachment) - end - end - @message.save - @message - end - - private - - def message_params - { - account_id: @conversation.account_id, - inbox_id: @conversation.inbox_id, - message_type: :outgoing, - content: @content, - private: @private, - sender: @user, - source_id: @fb_id, - content_type: @content_type, - items: @items - } - end -end diff --git a/app/controllers/api/v1/accounts/contacts_controller.rb b/app/controllers/api/v1/accounts/contacts_controller.rb index 2208a2cf6..7f7e7afdb 100644 --- a/app/controllers/api/v1/accounts/contacts_controller.rb +++ b/app/controllers/api/v1/accounts/contacts_controller.rb @@ -11,9 +11,11 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController def show; end def create - @contact = Current.account.contacts.new(contact_create_params) - @contact.save! - render json: @contact + ActiveRecord::Base.transaction do + @contact = Current.account.contacts.new(contact_create_params) + @contact.save! + @contact_inbox = build_contact_inbox + end end def update @@ -26,6 +28,14 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController authorize(Contact) end + def build_contact_inbox + return if params[:inbox_id].blank? + + inbox = Inbox.find(params[:inbox_id]) + source_id = params[:source_id] || SecureRandom.uuid + ContactInbox.create(contact: @contact, inbox: inbox, source_id: source_id) + end + def contact_params params.require(:contact).permit(:name, :email, :phone_number) end diff --git a/app/controllers/api/v1/accounts/conversations/messages_controller.rb b/app/controllers/api/v1/accounts/conversations/messages_controller.rb index c4d595771..54cf7c595 100644 --- a/app/controllers/api/v1/accounts/conversations/messages_controller.rb +++ b/app/controllers/api/v1/accounts/conversations/messages_controller.rb @@ -5,8 +5,10 @@ class Api::V1::Accounts::Conversations::MessagesController < Api::V1::Accounts:: def create user = current_user || @resource - mb = Messages::Outgoing::NormalBuilder.new(user, @conversation, params) + mb = Messages::MessageBuilder.new(user, @conversation, params) @message = mb.perform + rescue StandardError => e + render_could_not_create_error(e.message) end private diff --git a/app/controllers/api/v1/accounts/inboxes_controller.rb b/app/controllers/api/v1/accounts/inboxes_controller.rb index 45a190bfb..bb2024d50 100644 --- a/app/controllers/api/v1/accounts/inboxes_controller.rb +++ b/app/controllers/api/v1/accounts/inboxes_controller.rb @@ -4,12 +4,12 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController before_action :check_authorization def index - @inboxes = policy_scope(Current.account.inboxes) + @inboxes = policy_scope(Current.account.inboxes.includes(:channel, :avatar_attachment)) end def create ActiveRecord::Base.transaction do - channel = web_widgets.create!(permitted_params[:channel].except(:type)) if permitted_params[:channel][:type] == 'web_widget' + channel = create_channel @inbox = Current.account.inboxes.build( name: permitted_params[:name], greeting_message: permitted_params[:greeting_message], @@ -52,21 +52,28 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController @agent_bot = AgentBot.find(params[:agent_bot]) if params[:agent_bot] end - def web_widgets - Current.account.web_widgets - end - def check_authorization authorize(Inbox) end + def create_channel + case permitted_params[:channel][:type] + when 'web_widget' + Current.account.web_widgets.create!(permitted_params[:channel].except(:type)) + when 'api' + Current.account.api_channels.create!(permitted_params[:channel].except(:type)) + when 'email' + Current.account.email_channels.create!(permitted_params[:channel].except(:type)) + end + end + def permitted_params params.permit(:id, :avatar, :name, :greeting_message, :greeting_enabled, channel: - [:type, :website_url, :widget_color, :welcome_title, :welcome_tagline]) + [:type, :website_url, :widget_color, :welcome_title, :welcome_tagline, :webhook_url, :email]) end def inbox_update_params params.permit(:enable_auto_assignment, :name, :avatar, :greeting_message, :greeting_enabled, - channel: [:website_url, :widget_color, :welcome_title, :welcome_tagline]) + channel: [:website_url, :widget_color, :welcome_title, :welcome_tagline, :webhook_url, :email]) end end diff --git a/app/javascript/dashboard/assets/images/channels/api.png b/app/javascript/dashboard/assets/images/channels/api.png new file mode 100644 index 0000000000000000000000000000000000000000..d9919fc21a8ffeaa1ba288b0dd3a8de21afdfd0b GIT binary patch literal 42199 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≀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 DhCW+@ literal 0 HcmV?d00001 diff --git a/app/javascript/dashboard/assets/images/channels/email.png b/app/javascript/dashboard/assets/images/channels/email.png new file mode 100644 index 0000000000000000000000000000000000000000..304400e097348497dad785c753fb24174f5c30fa GIT binary patch literal 5659 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 W_YLstwSEquM^#Bvu~@<4>Hh#zml%Tp literal 0 HcmV?d00001 diff --git a/app/javascript/dashboard/components/layout/SidebarItem.vue b/app/javascript/dashboard/components/layout/SidebarItem.vue index 7ef2dfa88..7a305fbd3 100644 --- a/app/javascript/dashboard/components/layout/SidebarItem.vue +++ b/app/javascript/dashboard/components/layout/SidebarItem.vue @@ -63,6 +63,8 @@ const INBOX_TYPES = { FB: 'Channel::FacebookPage', TWITTER: 'Channel::TwitterProfile', TWILIO: 'Channel::TwilioSms', + API: 'Channel::Api', + EMAIL: 'Channel::Email', }; const getInboxClassByType = type => { switch (type) { @@ -78,6 +80,12 @@ const getInboxClassByType = type => { case INBOX_TYPES.TWILIO: return 'ion-android-textsms'; + case INBOX_TYPES.API: + return 'ion-cloud'; + + case INBOX_TYPES.EMAIL: + return 'ion-email'; + default: return ''; } diff --git a/app/javascript/dashboard/components/widgets/ChannelItem.vue b/app/javascript/dashboard/components/widgets/ChannelItem.vue index 52f4aff52..21c0aaf91 100644 --- a/app/javascript/dashboard/components/widgets/ChannelItem.vue +++ b/app/javascript/dashboard/components/widgets/ChannelItem.vue @@ -16,6 +16,14 @@ v-if="channel === 'telegram'" src="~dashboard/assets/images/channels/telegram.png" /> + + +
+ + +
Twilio SMS + + Email + + + Api + diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/channel-factory.js b/app/javascript/dashboard/routes/dashboard/settings/inbox/channel-factory.js index 9d15b3664..47c4e4e6e 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/inbox/channel-factory.js +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/channel-factory.js @@ -2,12 +2,16 @@ import Facebook from './channels/Facebook'; import Website from './channels/Website'; import Twitter from './channels/Twitter'; import Twilio from './channels/Twilio'; +import Api from './channels/Api'; +import Email from './channels/Email'; const channelViewList = { facebook: Facebook, website: Website, twitter: Twitter, twilio: Twilio, + api: Api, + email: Email, }; export default { diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/Api.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/Api.vue new file mode 100644 index 000000000..8c19892d6 --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/Api.vue @@ -0,0 +1,110 @@ + + + diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/Email.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/Email.vue new file mode 100644 index 000000000..b17273d44 --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/Email.vue @@ -0,0 +1,113 @@ + + + diff --git a/app/javascript/dashboard/store/modules/inboxes.js b/app/javascript/dashboard/store/modules/inboxes.js index 38c51195a..15e9eb66e 100644 --- a/app/javascript/dashboard/store/modules/inboxes.js +++ b/app/javascript/dashboard/store/modules/inboxes.js @@ -55,6 +55,18 @@ export const actions = { commit(types.default.SET_INBOXES_UI_FLAG, { isFetching: false }); } }, + createChannel: async ({ commit }, params) => { + try { + commit(types.default.SET_INBOXES_UI_FLAG, { isCreating: true }); + const response = await WebChannel.create(params); + commit(types.default.ADD_INBOXES, response.data); + commit(types.default.SET_INBOXES_UI_FLAG, { isCreating: false }); + return response.data; + } catch (error) { + commit(types.default.SET_INBOXES_UI_FLAG, { isCreating: false }); + throw new Error(error); + } + }, createWebsiteChannel: async ({ commit }, params) => { try { commit(types.default.SET_INBOXES_UI_FLAG, { isCreating: true }); diff --git a/app/listeners/webhook_listener.rb b/app/listeners/webhook_listener.rb index 14ad91709..a463fea00 100644 --- a/app/listeners/webhook_listener.rb +++ b/app/listeners/webhook_listener.rb @@ -50,9 +50,7 @@ class WebhookListener < BaseListener WebhookJob.perform_later(webhook.url, payload) end - # Inbox webhooks - inbox.webhooks.inbox.each do |webhook| - WebhookJob.perform_later(webhook.url, payload) - end + # Deliver for API Inbox + WebhookJob.perform_later(inbox.channel.webhook_url, payload) if inbox.channel_type == 'Channel::Api' end end diff --git a/app/models/account.rb b/app/models/account.rb index 12fb04921..f5d6b29f9 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -43,6 +43,8 @@ class Account < ApplicationRecord has_many :twilio_sms, dependent: :destroy, class_name: '::Channel::TwilioSms' has_many :twitter_profiles, dependent: :destroy, class_name: '::Channel::TwitterProfile' has_many :web_widgets, dependent: :destroy, class_name: '::Channel::WebWidget' + has_many :email_channels, dependent: :destroy, class_name: '::Channel::Email' + has_many :api_channels, dependent: :destroy, class_name: '::Channel::Api' has_many :canned_responses, dependent: :destroy has_many :webhooks, dependent: :destroy has_many :labels, dependent: :destroy diff --git a/app/models/channel/api.rb b/app/models/channel/api.rb new file mode 100644 index 000000000..5f080f232 --- /dev/null +++ b/app/models/channel/api.rb @@ -0,0 +1,19 @@ +# == Schema Information +# +# Table name: channel_api +# +# id :bigint not null, primary key +# webhook_url :string not null +# created_at :datetime not null +# updated_at :datetime not null +# account_id :integer not null +# + +class Channel::Api < ApplicationRecord + self.table_name = 'channel_api' + + validates :account_id, presence: true + belongs_to :account + + has_one :inbox, as: :channel, dependent: :destroy +end diff --git a/app/models/channel/email.rb b/app/models/channel/email.rb new file mode 100644 index 000000000..303d59619 --- /dev/null +++ b/app/models/channel/email.rb @@ -0,0 +1,35 @@ +# == Schema Information +# +# Table name: channel_email +# +# id :bigint not null, primary key +# email :string not null +# forward_to_address :string not null +# created_at :datetime not null +# updated_at :datetime not null +# account_id :integer not null +# +# Indexes +# +# index_channel_email_on_email (email) UNIQUE +# index_channel_email_on_forward_to_address (forward_to_address) UNIQUE +# + +class Channel::Email < ApplicationRecord + self.table_name = 'channel_email' + + validates :account_id, presence: true + belongs_to :account + validates :email, uniqueness: true + validates :forward_to_address, uniqueness: true + + has_one :inbox, as: :channel, dependent: :destroy + before_validation :ensure_forward_to_address, on: :create + + private + + def ensure_forward_to_address + # TODO : implement better logic here + self.forward_to_address ||= "#{SecureRandom.hex}@xyc.com" + end +end diff --git a/app/models/channel/facebook_page.rb b/app/models/channel/facebook_page.rb index 7c087bda9..bcad26387 100644 --- a/app/models/channel/facebook_page.rb +++ b/app/models/channel/facebook_page.rb @@ -17,9 +17,6 @@ # class Channel::FacebookPage < ApplicationRecord - # FIXME: this should be removed post 1.4 release. we moved avatars to inbox - include Avatarable - self.table_name = 'channel_facebook_pages' validates :account_id, presence: true diff --git a/app/views/api/v1/accounts/contacts/create.json.jbuilder b/app/views/api/v1/accounts/contacts/create.json.jbuilder new file mode 100644 index 000000000..3fd338c4a --- /dev/null +++ b/app/views/api/v1/accounts/contacts/create.json.jbuilder @@ -0,0 +1,9 @@ +json.payload do + json.contact do + json.partial! 'api/v1/models/contact.json.jbuilder', resource: @contact + end + json.contact_inbox do + json.inbox @contact_inbox&.inbox + json.source_id @contact_inbox&.source_id + end +end diff --git a/app/views/api/v1/accounts/inboxes/create.json.jbuilder b/app/views/api/v1/accounts/inboxes/create.json.jbuilder index c046402bc..981c1dec0 100644 --- a/app/views/api/v1/accounts/inboxes/create.json.jbuilder +++ b/app/views/api/v1/accounts/inboxes/create.json.jbuilder @@ -1,14 +1 @@ -json.id @inbox.id -json.channel_id @inbox.channel_id -json.name @inbox.name -json.channel_type @inbox.channel_type -json.greeting_enabled @inbox.greeting_enabled -json.greeting_message @inbox.greeting_message -json.avatar_url @inbox.try(:avatar_url) -json.website_token @inbox.channel.try(:website_token) -json.widget_color @inbox.channel.try(:widget_color) -json.website_url @inbox.channel.try(:website_url) -json.welcome_title @inbox.channel.try(:welcome_title) -json.welcome_tagline @inbox.channel.try(:welcome_tagline) -json.web_widget_script @inbox.channel.try(:web_widget_script) -json.enable_auto_assignment @inbox.enable_auto_assignment +json.partial! 'api/v1/models/inbox.json.jbuilder', resource: @inbox diff --git a/app/views/api/v1/accounts/inboxes/index.json.jbuilder b/app/views/api/v1/accounts/inboxes/index.json.jbuilder index da52d6632..c01d66d06 100644 --- a/app/views/api/v1/accounts/inboxes/index.json.jbuilder +++ b/app/views/api/v1/accounts/inboxes/index.json.jbuilder @@ -1,19 +1,5 @@ json.payload do json.array! @inboxes do |inbox| - json.id inbox.id - json.channel_id inbox.channel_id - json.name inbox.name - json.channel_type inbox.channel_type - json.greeting_enabled inbox.greeting_enabled - json.greeting_message inbox.greeting_message - json.avatar_url inbox.try(:avatar_url) - json.page_id inbox.channel.try(:page_id) - json.widget_color inbox.channel.try(:widget_color) - json.website_url inbox.channel.try(:website_url) - json.welcome_title inbox.channel.try(:welcome_title) - json.welcome_tagline inbox.channel.try(:welcome_tagline) - json.enable_auto_assignment inbox.enable_auto_assignment - json.web_widget_script inbox.channel.try(:web_widget_script) - json.phone_number inbox.channel.try(:phone_number) + json.partial! 'api/v1/models/inbox.json.jbuilder', resource: inbox end end diff --git a/app/views/api/v1/accounts/inboxes/update.json.jbuilder b/app/views/api/v1/accounts/inboxes/update.json.jbuilder index c046402bc..981c1dec0 100644 --- a/app/views/api/v1/accounts/inboxes/update.json.jbuilder +++ b/app/views/api/v1/accounts/inboxes/update.json.jbuilder @@ -1,14 +1 @@ -json.id @inbox.id -json.channel_id @inbox.channel_id -json.name @inbox.name -json.channel_type @inbox.channel_type -json.greeting_enabled @inbox.greeting_enabled -json.greeting_message @inbox.greeting_message -json.avatar_url @inbox.try(:avatar_url) -json.website_token @inbox.channel.try(:website_token) -json.widget_color @inbox.channel.try(:widget_color) -json.website_url @inbox.channel.try(:website_url) -json.welcome_title @inbox.channel.try(:welcome_title) -json.welcome_tagline @inbox.channel.try(:welcome_tagline) -json.web_widget_script @inbox.channel.try(:web_widget_script) -json.enable_auto_assignment @inbox.enable_auto_assignment +json.partial! 'api/v1/models/inbox.json.jbuilder', resource: @inbox diff --git a/app/views/api/v1/models/_inbox.json.jbuilder b/app/views/api/v1/models/_inbox.json.jbuilder new file mode 100644 index 000000000..260b5ad52 --- /dev/null +++ b/app/views/api/v1/models/_inbox.json.jbuilder @@ -0,0 +1,16 @@ +json.id resource.id +json.channel_id resource.channel_id +json.name resource.name +json.channel_type resource.channel_type +json.greeting_enabled resource.greeting_enabled +json.greeting_message resource.greeting_message +json.avatar_url resource.try(:avatar_url) +json.page_id resource.channel.try(:page_id) +json.widget_color resource.channel.try(:widget_color) +json.website_url resource.channel.try(:website_url) +json.welcome_title resource.channel.try(:welcome_title) +json.welcome_tagline resource.channel.try(:welcome_tagline) +json.enable_auto_assignment resource.enable_auto_assignment +json.web_widget_script resource.channel.try(:web_widget_script) +json.forward_to_address resource.channel.try(:forward_to_address) +json.phone_number resource.channel.try(:phone_number) diff --git a/db/migrate/20200627115105_create_api_channel.rb b/db/migrate/20200627115105_create_api_channel.rb new file mode 100644 index 000000000..3c9e92553 --- /dev/null +++ b/db/migrate/20200627115105_create_api_channel.rb @@ -0,0 +1,9 @@ +class CreateApiChannel < ActiveRecord::Migration[6.0] + def change + create_table :channel_api do |t| + t.integer :account_id, null: false + t.string :webhook_url, null: false + t.timestamps + end + end +end diff --git a/db/migrate/20200715124113_create_email_channel.rb b/db/migrate/20200715124113_create_email_channel.rb new file mode 100644 index 000000000..ca066aacb --- /dev/null +++ b/db/migrate/20200715124113_create_email_channel.rb @@ -0,0 +1,10 @@ +class CreateEmailChannel < ActiveRecord::Migration[6.0] + def change + create_table :channel_email do |t| + t.integer :account_id, null: false + t.string :email, null: false, index: { unique: true } + t.string :forward_to_address, null: false, index: { unique: true } + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index c28dc946c..0addc1f8e 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -120,6 +120,23 @@ ActiveRecord::Schema.define(version: 2020_07_19_171437) do t.datetime "updated_at", null: false end + create_table "channel_api", force: :cascade do |t| + t.integer "account_id", null: false + t.string "webhook_url", null: false + t.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false + end + + create_table "channel_email", force: :cascade do |t| + t.integer "account_id", null: false + t.string "email", null: false + t.string "forward_to_address", null: false + t.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false + t.index ["email"], name: "index_channel_email_on_email", unique: true + t.index ["forward_to_address"], name: "index_channel_email_on_forward_to_address", unique: true + end + create_table "channel_facebook_pages", id: :serial, force: :cascade do |t| t.string "page_id", null: false t.string "user_access_token", null: false diff --git a/lib/integrations/facebook/message_creator.rb b/lib/integrations/facebook/message_creator.rb index abf6c93e2..2669b840a 100644 --- a/lib/integrations/facebook/message_creator.rb +++ b/lib/integrations/facebook/message_creator.rb @@ -9,10 +9,10 @@ class Integrations::Facebook::MessageCreator def perform # begin - if outgoing_message_via_echo? - create_outgoing_message + if agent_message_via_echo? + create_agent_message else - create_incoming_message + create_contact_message end # rescue => e # Raven.capture_exception(e) @@ -21,22 +21,22 @@ class Integrations::Facebook::MessageCreator private - def outgoing_message_via_echo? + def agent_message_via_echo? response.echo? && !response.sent_from_chatwoot_app? - # this means that it is an outgoing message from page, but not sent from chatwoot. - # User can send from fb page directly on mobile messenger, so this case should be handled as outgoing message + # this means that it is an agent message from page, but not sent from chatwoot. + # User can send from fb page directly on mobile / web messenger, so this case should be handled as agent message end - def create_outgoing_message + def create_agent_message Channel::FacebookPage.where(page_id: response.sender_id).each do |page| - mb = Messages::Outgoing::EchoBuilder.new(response, page.inbox, true) + mb = Messages::Facebook::MessageBuilder.new(response, page.inbox, true) mb.perform end end - def create_incoming_message + def create_contact_message Channel::FacebookPage.where(page_id: response.recipient_id).each do |page| - mb = Messages::IncomingMessageBuilder.new(response, page.inbox) + mb = Messages::Facebook::MessageBuilder.new(response, page.inbox) mb.perform end end diff --git a/lib/webhooks/trigger.rb b/lib/webhooks/trigger.rb index 57020e299..efa555845 100644 --- a/lib/webhooks/trigger.rb +++ b/lib/webhooks/trigger.rb @@ -1,6 +1,6 @@ class Webhooks::Trigger def self.execute(url, payload) - RestClient.post(url, payload) + RestClient.post(url, payload.to_json, { content_type: :json, accept: :json }) rescue StandardError => e Raven.capture_exception(e) end diff --git a/spec/builders/messages/incoming_message_builder_spec.rb b/spec/builders/messages/facebook/message_builder_spec.rb similarity index 95% rename from spec/builders/messages/incoming_message_builder_spec.rb rename to spec/builders/messages/facebook/message_builder_spec.rb index 0e91d3ae6..52edd0413 100644 --- a/spec/builders/messages/incoming_message_builder_spec.rb +++ b/spec/builders/messages/facebook/message_builder_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -describe ::Messages::IncomingMessageBuilder do +describe ::Messages::Facebook::MessageBuilder do subject(:message_builder) { described_class.new(incoming_fb_text_message, facebook_channel.inbox).perform } let!(:facebook_channel) { create(:channel_facebook_page) } diff --git a/spec/builders/messages/message_builder_spec.rb b/spec/builders/messages/message_builder_spec.rb new file mode 100644 index 000000000..6227f2a48 --- /dev/null +++ b/spec/builders/messages/message_builder_spec.rb @@ -0,0 +1,54 @@ +require 'rails_helper' + +describe ::Messages::MessageBuilder do + subject(:message_builder) { described_class.new(user, conversation, params).perform } + + let(:account) { create(:account) } + let(:user) { create(:user, account: account) } + let(:inbox) { create(:inbox, account: account) } + let(:inbox_member) { create(:inbox_member, inbox: inbox, account: account) } + let(:conversation) { create(:conversation, inbox: inbox, account: account) } + let(:params) do + ActionController::Parameters.new({ + content: 'test' + }) + end + + describe '#perform' do + it 'creates a message' do + message = message_builder + expect(message.content).to eq params[:content] + end + end + + describe '#perform when message_type is incoming' do + context 'when channel is not api' do + let(:params) do + ActionController::Parameters.new({ + content: 'test', + message_type: 'incoming' + }) + end + + it 'creates throws error when channel is not api' do + expect { message_builder }.to raise_error 'Incoming messages are only allowed in Api inboxes' + end + end + + context 'when channel is api' do + let(:channel_api) { create(:channel_api, account: account) } + let(:conversation) { create(:conversation, inbox: channel_api.inbox, account: account) } + let(:params) do + ActionController::Parameters.new({ + content: 'test', + message_type: 'incoming' + }) + end + + it 'creates message when channel is api' do + message = message_builder + expect(message.message_type).to eq params[:message_type] + end + end + end +end diff --git a/spec/controllers/api/v1/accounts/contacts_controller_spec.rb b/spec/controllers/api/v1/accounts/contacts_controller_spec.rb index 1a1b82442..16cd13593 100644 --- a/spec/controllers/api/v1/accounts/contacts_controller_spec.rb +++ b/spec/controllers/api/v1/accounts/contacts_controller_spec.rb @@ -65,6 +65,7 @@ RSpec.describe 'Contacts API', type: :request do context 'when it is an authenticated user' do let(:admin) { create(:user, account: account, role: :administrator) } + let(:inbox) { create(:inbox, account: account) } it 'creates the contact' do expect do @@ -74,6 +75,15 @@ RSpec.describe 'Contacts API', type: :request do expect(response).to have_http_status(:success) end + + it 'creates the contact identifier when inbox id is passed' do + expect do + post "/api/v1/accounts/#{account.id}/contacts", headers: admin.create_new_auth_token, + params: valid_params.merge({ inbox_id: inbox.id }) + end.to change(ContactInbox, :count).by(1) + + expect(response).to have_http_status(:success) + end end end diff --git a/spec/factories/channel/channel_api.rb b/spec/factories/channel/channel_api.rb new file mode 100644 index 000000000..7a01b5355 --- /dev/null +++ b/spec/factories/channel/channel_api.rb @@ -0,0 +1,9 @@ +FactoryBot.define do + factory :channel_api, class: 'Channel::Api' do + webhook_url { 'http://example.com' } + account + after(:create) do |channel_api| + create(:inbox, channel: channel_api, account: channel_api.account) + end + end +end diff --git a/spec/lib/webhooks/trigger_spec.rb b/spec/lib/webhooks/trigger_spec.rb index 17564510e..7608634ac 100644 --- a/spec/lib/webhooks/trigger_spec.rb +++ b/spec/lib/webhooks/trigger_spec.rb @@ -5,10 +5,10 @@ describe Webhooks::Trigger do describe '#execute' do it 'triggers webhook' do - params = { hello: 'hello' } - url = 'htpps://test.com' + params = { hello: :hello } + url = 'https://test.com' - expect(RestClient).to receive(:post).with(url, params).once + expect(RestClient).to receive(:post).with(url, params.to_json, { accept: :json, content_type: :json }).once trigger.execute(url, params) end end