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 000000000..d9919fc21 Binary files /dev/null and b/app/javascript/dashboard/assets/images/channels/api.png differ 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 000000000..304400e09 Binary files /dev/null and b/app/javascript/dashboard/assets/images/channels/email.png differ 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