diff --git a/.rubocop.yml b/.rubocop.yml
index bc34a343b..b007be1d5 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -4,13 +4,13 @@ require:
- rubocop-rspec
inherit_from: .rubocop_todo.yml
-Metrics/LineLength:
+Layout/LineLength:
Max: 150
Metrics/ClassLength:
Max: 125
RSpec/ExampleLength:
Max: 15
-Documentation:
+Style/Documentation:
Enabled: false
Style/FrozenStringLiteralComment:
Enabled: false
diff --git a/app/builders/messages/message_builder.rb b/app/builders/messages/message_builder.rb
index 6611cfe5b..8cad3f87b 100644
--- a/app/builders/messages/message_builder.rb
+++ b/app/builders/messages/message_builder.rb
@@ -6,132 +6,137 @@ require 'open-uri'
# 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.
-module Messages
- class MessageBuilder
- attr_reader :response
+class Messages::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)
+ 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
- def perform
- ActiveRecord::Base.transaction do
- build_contact
- build_message
- end
- rescue StandardError => e
- Raven.capture_exception(e)
- true
- end
+ private
- private
+ def contact
+ @contact ||= @inbox.contact_inboxes.find_by(source_id: @sender_id)&.contact
+ end
- def contact
- @contact ||= @inbox.contact_inboxes.find_by(source_id: @sender_id)&.contact
- end
+ def build_contact
+ return if contact.present?
- def build_contact
- return if contact.present?
+ @contact = Contact.create!(contact_params.except(:remote_avatar_url))
+ avatar_resource = LocalResource.new(contact_params[:remote_avatar_url])
+ @contact.avatar.attach(io: avatar_resource.file, filename: avatar_resource.tmp_filename, content_type: avatar_resource.encoding)
- @contact = Contact.create!(contact_params.except(:remote_avatar_url))
- avatar_resource = LocalResource.new(contact_params[:remote_avatar_url])
- @contact.avatar.attach(io: avatar_resource.file, filename: avatar_resource.tmp_filename, content_type: avatar_resource.encoding)
+ @contact_inbox = ContactInbox.create(contact: contact, inbox: @inbox, source_id: @sender_id)
+ end
- 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.build_attachment(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) || Conversation.create!(conversation_params)
- 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,
- fb_id: response.identifier
- }
- 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'] || ''
- }
+ def build_message
+ @message = conversation.messages.create!(message_params)
+ (response.attachments || []).each do |attachment|
+ attachment_obj = @message.build_attachment(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,
+ fb_id: response.identifier
+ }
+ 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'] || ''
+ }
+ end
end
diff --git a/app/controllers/api/v1/inbox_members_controller.rb b/app/controllers/api/v1/inbox_members_controller.rb
index f5e8a6a10..ed47f409a 100644
--- a/app/controllers/api/v1/inbox_members_controller.rb
+++ b/app/controllers/api/v1/inbox_members_controller.rb
@@ -1,55 +1,51 @@
-module Api
- module V1
- class InboxMembersController < Api::BaseController
- before_action :fetch_inbox, only: [:create, :show]
- before_action :current_agents_ids, only: [:create]
+class Api::V1::InboxMembersController < Api::BaseController
+ before_action :fetch_inbox, only: [:create, :show]
+ before_action :current_agents_ids, only: [:create]
- def create
- # update also done via same action
- if @inbox
- begin
- update_agents_list
- head :ok
- rescue StandardError => e
- Rails.logger.debug "Rescued: #{e.inspect}"
- render_could_not_create_error('Could not add agents to inbox')
- end
- else
- render_not_found_error('Agents or inbox not found')
- end
- end
-
- def show
- @agents = current_account.users.where(id: @inbox.members.pluck(:user_id))
- end
-
- private
-
- def update_agents_list
- # get all the user_ids which the inbox currently has as members.
- # get the list of user_ids from params
- # the missing ones are the agents which are to be deleted from the inbox
- # the new ones are the agents which are to be added to the inbox
-
- agents_to_be_added_ids.each { |user_id| @inbox.add_member(user_id) }
- agents_to_be_removed_ids.each { |user_id| @inbox.remove_member(user_id) }
- end
-
- def agents_to_be_added_ids
- params[:user_ids] - @current_agents_ids
- end
-
- def agents_to_be_removed_ids
- @current_agents_ids - params[:user_ids]
- end
-
- def current_agents_ids
- @current_agents_ids = @inbox.members.pluck(:id)
- end
-
- def fetch_inbox
- @inbox = current_account.inboxes.find(params[:inbox_id])
+ def create
+ # update also done via same action
+ if @inbox
+ begin
+ update_agents_list
+ head :ok
+ rescue StandardError => e
+ Rails.logger.debug "Rescued: #{e.inspect}"
+ render_could_not_create_error('Could not add agents to inbox')
end
+ else
+ render_not_found_error('Agents or inbox not found')
end
end
+
+ def show
+ @agents = current_account.users.where(id: @inbox.members.pluck(:user_id))
+ end
+
+ private
+
+ def update_agents_list
+ # get all the user_ids which the inbox currently has as members.
+ # get the list of user_ids from params
+ # the missing ones are the agents which are to be deleted from the inbox
+ # the new ones are the agents which are to be added to the inbox
+
+ agents_to_be_added_ids.each { |user_id| @inbox.add_member(user_id) }
+ agents_to_be_removed_ids.each { |user_id| @inbox.remove_member(user_id) }
+ end
+
+ def agents_to_be_added_ids
+ params[:user_ids] - @current_agents_ids
+ end
+
+ def agents_to_be_removed_ids
+ @current_agents_ids - params[:user_ids]
+ end
+
+ def current_agents_ids
+ @current_agents_ids = @inbox.members.pluck(:id)
+ end
+
+ def fetch_inbox
+ @inbox = current_account.inboxes.find(params[:inbox_id])
+ end
end
diff --git a/app/controllers/api/v1/widget/base_controller.rb b/app/controllers/api/v1/widget/base_controller.rb
new file mode 100644
index 000000000..eb19e2bdd
--- /dev/null
+++ b/app/controllers/api/v1/widget/base_controller.rb
@@ -0,0 +1,29 @@
+class Api::V1::Widget::BaseController < ApplicationController
+ private
+
+ def conversation
+ @conversation ||= @contact_inbox.conversations.find_by(
+ inbox_id: auth_token_params[:inbox_id]
+ )
+ end
+
+ def auth_token_params
+ @auth_token_params ||= ::Widget::TokenService.new(token: request.headers[header_name]).decode_token
+ end
+
+ def header_name
+ 'X-Auth-Token'
+ end
+
+ def set_web_widget
+ @web_widget = ::Channel::WebWidget.find_by!(website_token: permitted_params[:website_token])
+ @account = @web_widget.account
+ end
+
+ def set_contact
+ @contact_inbox = @web_widget.inbox.contact_inboxes.find_by(
+ source_id: auth_token_params[:source_id]
+ )
+ @contact = @contact_inbox.contact
+ end
+end
diff --git a/app/controllers/api/v1/widget/messages_controller.rb b/app/controllers/api/v1/widget/messages_controller.rb
index a3e83663e..903e4b26e 100644
--- a/app/controllers/api/v1/widget/messages_controller.rb
+++ b/app/controllers/api/v1/widget/messages_controller.rb
@@ -1,6 +1,8 @@
-class Api::V1::Widget::MessagesController < ActionController::Base
- skip_before_action :verify_authenticity_token
+class Api::V1::Widget::MessagesController < Api::V1::Widget::BaseController
+ before_action :set_web_widget
+ before_action :set_contact
before_action :set_conversation, only: [:create]
+ before_action :set_message, only: [:update]
def index
@messages = conversation.nil? ? [] : message_finder.perform
@@ -9,17 +11,19 @@ class Api::V1::Widget::MessagesController < ActionController::Base
def create
@message = conversation.messages.new(message_params)
@message.save!
+ render json: @message
+ end
+
+ def update
+ @message.update!(input_submitted_email: permitted_params[:contact][:email])
+ update_contact(permitted_params[:contact][:email])
+ head :no_content
+ rescue StandardError => e
+ render json: { error: @contact.errors, message: e.message }.to_json, status: 500
end
private
- def conversation
- @conversation ||= ::Conversation.find_by(
- contact_id: cookie_params[:contact_id],
- inbox_id: cookie_params[:inbox_id]
- )
- end
-
def set_conversation
@conversation = ::Conversation.create!(conversation_params) if conversation.nil?
end
@@ -37,7 +41,8 @@ class Api::V1::Widget::MessagesController < ActionController::Base
{
account_id: inbox.account_id,
inbox_id: inbox.id,
- contact_id: cookie_params[:contact_id],
+ contact_id: @contact.id,
+ contact_inbox_id: @contact_inbox.id,
additional_attributes: {
browser: browser_params,
referer: permitted_params[:message][:referer_url],
@@ -63,13 +68,7 @@ class Api::V1::Widget::MessagesController < ActionController::Base
end
def inbox
- @inbox ||= ::Inbox.find_by(id: cookie_params[:inbox_id])
- end
-
- def cookie_params
- @cookie_params ||= JWT.decode(
- request.headers[header_name], secret_key, true, algorithm: 'HS256'
- ).first.symbolize_keys
+ @inbox ||= ::Inbox.find_by(id: auth_token_params[:inbox_id])
end
def message_finder_params
@@ -83,15 +82,27 @@ class Api::V1::Widget::MessagesController < ActionController::Base
@message_finder ||= MessageFinder.new(conversation, message_finder_params)
end
- def header_name
- 'X-Auth-Token'
+ def update_contact(email)
+ contact_with_email = @account.contacts.find_by(email: email)
+ if contact_with_email
+ ::ContactMergeAction.new(account: @account, base_contact: contact_with_email, mergee_contact: @contact).perform
+ else
+ @contact.update!(
+ email: permitted_params[:contact][:email],
+ name: contact_name
+ )
+ end
+ end
+
+ def contact_name
+ permitted_params[:contact][:email].split('@')[0]
end
def permitted_params
- params.permit(:before, message: [:content, :referer_url, :timestamp])
+ params.permit(:id, :before, :website_token, contact: [:email], message: [:content, :referer_url, :timestamp])
end
- def secret_key
- Rails.application.secrets.secret_key_base
+ def set_message
+ @message = @web_widget.inbox.messages.find(permitted_params[:id])
end
end
diff --git a/app/controllers/widgets_controller.rb b/app/controllers/widgets_controller.rb
index 711a5cca4..60b522831 100644
--- a/app/controllers/widgets_controller.rb
+++ b/app/controllers/widgets_controller.rb
@@ -4,66 +4,47 @@ class WidgetsController < ActionController::Base
before_action :set_contact
before_action :build_contact
+ def index
+ render
+ end
+
private
- def set_contact
- return if cookie_params[:source_id].nil?
-
- contact_inbox = ::ContactInbox.find_by(
- inbox_id: @web_widget.inbox.id,
- source_id: cookie_params[:source_id]
- )
-
- @contact = contact_inbox ? contact_inbox.contact : nil
- end
-
- def set_token
- @token = conversation_token
- end
-
def set_web_widget
@web_widget = ::Channel::WebWidget.find_by!(website_token: permitted_params[:website_token])
end
+ def set_token
+ @token = permitted_params[:cw_conversation]
+ @auth_token_params = if @token.present?
+ ::Widget::TokenService.new(token: @token).decode_token
+ else
+ {}
+ end
+ end
+
+ def set_contact
+ return if @auth_token_params[:source_id].nil?
+
+ contact_inbox = ::ContactInbox.find_by(
+ inbox_id: @web_widget.inbox.id,
+ source_id: @auth_token_params[:source_id]
+ )
+
+ @contact = contact_inbox ? contact_inbox.contact : nil
+ end
+
def build_contact
return if @contact.present?
contact_inbox = @web_widget.create_contact_inbox
@contact = contact_inbox.contact
- payload = {
- source_id: contact_inbox.source_id,
- contact_id: @contact.id,
- inbox_id: @web_widget.inbox.id
- }
- @token = JWT.encode payload, secret_key, 'HS256'
- end
-
- def cookie_params
- return @cookie_params if @cookie_params.present?
-
- if conversation_token.present?
- begin
- @cookie_params = JWT.decode(
- conversation_token, secret_key, true, algorithm: 'HS256'
- ).first.symbolize_keys
- rescue StandardError
- @cookie_params = {}
- end
- return @cookie_params
- end
- {}
- end
-
- def conversation_token
- permitted_params[:cw_conversation]
+ payload = { source_id: contact_inbox.source_id, inbox_id: @web_widget.inbox.id }
+ @token = ::Widget::TokenService.new(payload: payload).generate_token
end
def permitted_params
params.permit(:website_token, :cw_conversation)
end
-
- def secret_key
- Rails.application.secrets.secret_key_base
- end
end
diff --git a/app/javascript/dashboard/assets/images/chatwoot_bot.png b/app/javascript/dashboard/assets/images/chatwoot_bot.png
new file mode 100644
index 000000000..4b5a2d686
Binary files /dev/null and b/app/javascript/dashboard/assets/images/chatwoot_bot.png differ
diff --git a/app/javascript/dashboard/components/Spinner.vue b/app/javascript/dashboard/components/Spinner.vue
deleted file mode 100644
index b90ad471d..000000000
--- a/app/javascript/dashboard/components/Spinner.vue
+++ /dev/null
@@ -1,3 +0,0 @@
-
-
-
diff --git a/app/javascript/dashboard/components/buttons/FormSubmitButton.vue b/app/javascript/dashboard/components/buttons/FormSubmitButton.vue
index b0cf8cdad..c6ec19321 100644
--- a/app/javascript/dashboard/components/buttons/FormSubmitButton.vue
+++ b/app/javascript/dashboard/components/buttons/FormSubmitButton.vue
@@ -12,7 +12,7 @@
diff --git a/app/javascript/widget/components/AgentMessageBubble.vue b/app/javascript/widget/components/AgentMessageBubble.vue
index 54319761a..8017a8854 100755
--- a/app/javascript/widget/components/AgentMessageBubble.vue
+++ b/app/javascript/widget/components/AgentMessageBubble.vue
@@ -1,20 +1,42 @@
-
+
+
+
+
-
diff --git a/app/javascript/widget/helpers/constants.js b/app/javascript/widget/helpers/constants.js
index 605ba2c58..d5862729b 100755
--- a/app/javascript/widget/helpers/constants.js
+++ b/app/javascript/widget/helpers/constants.js
@@ -9,4 +9,6 @@ export const MESSAGE_STATUS = {
export const MESSAGE_TYPE = {
INCOMING: 0,
OUTGOING: 1,
+ ACTIVITY: 2,
+ TEMPLATE: 3,
};
diff --git a/app/javascript/widget/store/index.js b/app/javascript/widget/store/index.js
index 91f6877ef..b63d25414 100755
--- a/app/javascript/widget/store/index.js
+++ b/app/javascript/widget/store/index.js
@@ -1,13 +1,15 @@
import Vue from 'vue';
import Vuex from 'vuex';
-import conversation from 'widget/store/modules/conversation';
import appConfig from 'widget/store/modules/appConfig';
+import contact from 'widget/store/modules/contact';
+import conversation from 'widget/store/modules/conversation';
Vue.use(Vuex);
export default new Vuex.Store({
modules: {
appConfig,
+ contact,
conversation,
},
});
diff --git a/app/javascript/widget/store/modules/contact.js b/app/javascript/widget/store/modules/contact.js
new file mode 100644
index 000000000..8987df242
--- /dev/null
+++ b/app/javascript/widget/store/modules/contact.js
@@ -0,0 +1,45 @@
+import { updateContact } from 'widget/api/contact';
+
+const state = {
+ uiFlags: {
+ isUpdating: false,
+ },
+};
+
+const getters = {
+ getUIFlags: $state => $state.uiFlags,
+};
+
+const actions = {
+ updateContactAttributes: async ({ commit }, { email, messageId }) => {
+ commit('toggleUpdateStatus', true);
+ try {
+ await updateContact({ email, messageId });
+ commit(
+ 'conversation/updateMessage',
+ {
+ id: messageId,
+ content_attributes: { submitted_email: email },
+ },
+ { root: true }
+ );
+ } catch (error) {
+ // Ignore error
+ }
+ commit('toggleUpdateStatus', false);
+ },
+};
+
+const mutations = {
+ toggleUpdateStatus($state, status) {
+ $state.uiFlags.isUpdating = status;
+ },
+};
+
+export default {
+ namespaced: true,
+ state,
+ getters,
+ actions,
+ mutations,
+};
diff --git a/app/javascript/widget/store/modules/conversation.js b/app/javascript/widget/store/modules/conversation.js
index f0bc6cb39..81a19fd13 100755
--- a/app/javascript/widget/store/modules/conversation.js
+++ b/app/javascript/widget/store/modules/conversation.js
@@ -135,6 +135,13 @@ export const mutations = {
payload.map(message => Vue.set($state.conversations, message.id, message));
},
+
+ updateMessage($state, { id, content_attributes }) {
+ $state.conversations[id] = {
+ ...$state.conversations[id],
+ content_attributes,
+ };
+ },
};
export default {
diff --git a/app/models/channel/facebook_page.rb b/app/models/channel/facebook_page.rb
index 7390e5c47..f1004859a 100644
--- a/app/models/channel/facebook_page.rb
+++ b/app/models/channel/facebook_page.rb
@@ -17,31 +17,29 @@
# index_channel_facebook_pages_on_page_id_and_account_id (page_id,account_id) UNIQUE
#
-module Channel
- class FacebookPage < ApplicationRecord
- include Avatarable
+class Channel::FacebookPage < ApplicationRecord
+ include Avatarable
- self.table_name = 'channel_facebook_pages'
+ self.table_name = 'channel_facebook_pages'
- validates :account_id, presence: true
- validates :page_id, uniqueness: { scope: :account_id }
- has_one_attached :avatar
- belongs_to :account
+ validates :account_id, presence: true
+ validates :page_id, uniqueness: { scope: :account_id }
+ has_one_attached :avatar
+ belongs_to :account
- has_one :inbox, as: :channel, dependent: :destroy
+ has_one :inbox, as: :channel, dependent: :destroy
- before_destroy :unsubscribe
+ before_destroy :unsubscribe
- def name
- 'Facebook'
- end
+ def name
+ 'Facebook'
+ end
- private
+ private
- def unsubscribe
- Facebook::Messenger::Subscriptions.unsubscribe(access_token: page_access_token)
- rescue => e
- true
- end
+ def unsubscribe
+ Facebook::Messenger::Subscriptions.unsubscribe(access_token: page_access_token)
+ rescue => e
+ true
end
end
diff --git a/app/models/channel/web_widget.rb b/app/models/channel/web_widget.rb
index 3d63aee8a..97b16eef2 100644
--- a/app/models/channel/web_widget.rb
+++ b/app/models/channel/web_widget.rb
@@ -16,33 +16,31 @@
# index_channel_web_widgets_on_website_token (website_token) UNIQUE
#
-module Channel
- class WebWidget < ApplicationRecord
- self.table_name = 'channel_web_widgets'
+class Channel::WebWidget < ApplicationRecord
+ self.table_name = 'channel_web_widgets'
- validates :website_name, presence: true
- validates :website_url, presence: true
- validates :widget_color, presence: true
+ validates :website_name, presence: true
+ validates :website_url, presence: true
+ validates :widget_color, presence: true
- belongs_to :account
- has_one :inbox, as: :channel, dependent: :destroy
- has_secure_token :website_token
+ belongs_to :account
+ has_one :inbox, as: :channel, dependent: :destroy
+ has_secure_token :website_token
- def name
- 'Website'
- end
+ def name
+ 'Website'
+ end
- def create_contact_inbox
- ActiveRecord::Base.transaction do
- contact = inbox.account.contacts.create!(name: ::Haikunator.haikunate(1000))
- ::ContactInbox.create!(
- contact_id: contact.id,
- inbox_id: inbox.id,
- source_id: SecureRandom.uuid
- )
- rescue StandardError => e
- Rails.logger e
- end
+ def create_contact_inbox
+ ActiveRecord::Base.transaction do
+ contact = inbox.account.contacts.create!(name: ::Haikunator.haikunate(1000))
+ ::ContactInbox.create!(
+ contact_id: contact.id,
+ inbox_id: inbox.id,
+ source_id: SecureRandom.uuid
+ )
+ rescue StandardError => e
+ Rails.logger e
end
end
end
diff --git a/app/models/contact_inbox.rb b/app/models/contact_inbox.rb
index 42c0584a9..c79786e1e 100644
--- a/app/models/contact_inbox.rb
+++ b/app/models/contact_inbox.rb
@@ -29,4 +29,6 @@ class ContactInbox < ApplicationRecord
belongs_to :contact
belongs_to :inbox
+
+ has_many :conversations, dependent: :destroy
end
diff --git a/app/models/conversation.rb b/app/models/conversation.rb
index e86b0124b..2fa1205a6 100644
--- a/app/models/conversation.rb
+++ b/app/models/conversation.rb
@@ -13,6 +13,7 @@
# account_id :integer not null
# assignee_id :integer
# contact_id :bigint
+# contact_inbox_id :bigint
# display_id :integer not null
# inbox_id :integer not null
#
@@ -20,6 +21,11 @@
#
# index_conversations_on_account_id (account_id)
# index_conversations_on_account_id_and_display_id (account_id,display_id) UNIQUE
+# index_conversations_on_contact_inbox_id (contact_inbox_id)
+#
+# Foreign Keys
+#
+# fk_rails_... (contact_inbox_id => contact_inboxes.id)
#
class Conversation < ApplicationRecord
@@ -38,6 +44,7 @@ class Conversation < ApplicationRecord
belongs_to :inbox
belongs_to :assignee, class_name: 'User', optional: true
belongs_to :contact
+ belongs_to :contact_inbox
has_many :messages, dependent: :destroy, autosave: true
diff --git a/app/models/inbox.rb b/app/models/inbox.rb
index 2d2b61db9..28d6a2d51 100644
--- a/app/models/inbox.rb
+++ b/app/models/inbox.rb
@@ -49,6 +49,10 @@ class Inbox < ApplicationRecord
channel.class.name.to_s == 'Channel::FacebookPage'
end
+ def web_widget?
+ channel.class.name.to_s == 'Channel::WebWidget'
+ end
+
def next_available_agent
user_id = Redis::Alfred.rpoplpush(round_robin_key, round_robin_key)
account.users.find_by(id: user_id)
diff --git a/app/models/message.rb b/app/models/message.rb
index 35a7269c9..a40c76efd 100644
--- a/app/models/message.rb
+++ b/app/models/message.rb
@@ -2,18 +2,20 @@
#
# Table name: messages
#
-# id :integer not null, primary key
-# content :text
-# message_type :integer not null
-# private :boolean default(FALSE)
-# status :integer default("sent")
-# created_at :datetime not null
-# updated_at :datetime not null
-# account_id :integer not null
-# conversation_id :integer not null
-# fb_id :string
-# inbox_id :integer not null
-# user_id :integer
+# id :integer not null, primary key
+# content :text
+# content_attributes :json
+# content_type :integer default("text")
+# message_type :integer not null
+# private :boolean default(FALSE)
+# status :integer default("sent")
+# created_at :datetime not null
+# updated_at :datetime not null
+# account_id :integer not null
+# conversation_id :integer not null
+# fb_id :string
+# inbox_id :integer not null
+# user_id :integer
#
# Indexes
#
@@ -27,8 +29,10 @@ class Message < ApplicationRecord
validates :inbox_id, presence: true
validates :conversation_id, presence: true
- enum message_type: [:incoming, :outgoing, :activity]
- enum status: [:sent, :delivered, :read, :failed]
+ enum message_type: { incoming: 0, outgoing: 1, activity: 2, template: 3 }
+ enum content_type: { text: 0, input: 1, input_textarea: 2, input_email: 3 }
+ enum status: { sent: 0, delivered: 1, read: 2, failed: 3 }
+ store :content_attributes, accessors: [:submitted_email], coder: JSON, prefix: :input
# .succ is a hack to avoid https://makandracards.com/makandra/1057-why-two-ruby-time-objects-are-not-equal-although-they-appear-to-be
scope :unread_since, ->(datetime) { where('EXTRACT(EPOCH FROM created_at) > (?)', datetime.to_i.succ) }
@@ -44,7 +48,8 @@ class Message < ApplicationRecord
after_create :reopen_conversation,
:dispatch_event,
- :send_reply
+ :send_reply,
+ :execute_message_template_hooks
def channel_token
@token ||= inbox.channel.try(:page_access_token)
@@ -81,4 +86,8 @@ class Message < ApplicationRecord
Rails.configuration.dispatcher.dispatch(CONVERSATION_REOPENED, Time.zone.now, conversation: conversation)
end
end
+
+ def execute_message_template_hooks
+ ::MessageTemplates::HookExecutionService.new(message: self).perform
+ end
end
diff --git a/app/presenters/conversations/event_data_presenter.rb b/app/presenters/conversations/event_data_presenter.rb
index 32ecde01f..8c1bc0f1e 100644
--- a/app/presenters/conversations/event_data_presenter.rb
+++ b/app/presenters/conversations/event_data_presenter.rb
@@ -1,37 +1,35 @@
-module Conversations
- class EventDataPresenter < SimpleDelegator
- def lock_data
- { id: display_id, locked: locked? }
- end
+class Conversations::EventDataPresenter < SimpleDelegator
+ def lock_data
+ { id: display_id, locked: locked? }
+ end
- def push_data
- {
- id: display_id,
- inbox_id: inbox_id,
- messages: push_messages,
- meta: push_meta,
- status: status_before_type_cast.to_i,
- unread_count: unread_incoming_messages.count,
- **push_timestamps
- }
- end
+ def push_data
+ {
+ id: display_id,
+ inbox_id: inbox_id,
+ messages: push_messages,
+ meta: push_meta,
+ status: status_before_type_cast.to_i,
+ unread_count: unread_incoming_messages.count,
+ **push_timestamps
+ }
+ end
- private
+ private
- def push_messages
- [messages.chat.last&.push_event_data].compact
- end
+ def push_messages
+ [messages.chat.last&.push_event_data].compact
+ end
- def push_meta
- { sender: contact.push_event_data, assignee: assignee }
- end
+ def push_meta
+ { sender: contact.push_event_data, assignee: assignee }
+ end
- def push_timestamps
- {
- agent_last_seen_at: agent_last_seen_at.to_i,
- user_last_seen_at: user_last_seen_at.to_i,
- timestamp: created_at.to_i
- }
- end
+ def push_timestamps
+ {
+ agent_last_seen_at: agent_last_seen_at.to_i,
+ user_last_seen_at: user_last_seen_at.to_i,
+ timestamp: created_at.to_i
+ }
end
end
diff --git a/app/services/contact/merge_service.rb b/app/services/contact/merge_service.rb
new file mode 100644
index 000000000..e69de29bb
diff --git a/app/services/message_templates/hook_execution_service.rb b/app/services/message_templates/hook_execution_service.rb
new file mode 100644
index 000000000..071e12a64
--- /dev/null
+++ b/app/services/message_templates/hook_execution_service.rb
@@ -0,0 +1,20 @@
+class MessageTemplates::HookExecutionService
+ pattr_initialize [:message!]
+
+ def perform
+ ::MessageTemplates::Template::EmailCollect.new(conversation: conversation).perform if should_send_email_collect?
+ end
+
+ private
+
+ delegate :inbox, :conversation, to: :message
+ delegate :contact, to: :conversation
+
+ def first_message_from_contact?
+ conversation.messages.outgoing.count.zero? && conversation.messages.template.count.zero?
+ end
+
+ def should_send_email_collect?
+ conversation.inbox.web_widget? && first_message_from_contact?
+ end
+end
diff --git a/app/services/message_templates/template/email_collect.rb b/app/services/message_templates/template/email_collect.rb
new file mode 100644
index 000000000..07004081e
--- /dev/null
+++ b/app/services/message_templates/template/email_collect.rb
@@ -0,0 +1,56 @@
+class MessageTemplates::Template::EmailCollect
+ pattr_initialize [:conversation!]
+
+ def perform
+ ActiveRecord::Base.transaction do
+ conversation.messages.create!(typical_reply_message_params)
+ conversation.messages.create!(ways_to_reach_you_message_params)
+ conversation.messages.create!(email_input_box_template_message_params)
+ end
+ rescue StandardError => e
+ Raven.capture_exception(e)
+ true
+ end
+
+ private
+
+ delegate :contact, :account, to: :conversation
+ delegate :inbox, to: :message
+
+ def typical_reply_message_params
+ content = I18n.t('conversations.templates.typical_reply_message_body',
+ account_name: account.name)
+
+ {
+ account_id: @conversation.account_id,
+ inbox_id: @conversation.inbox_id,
+ message_type: :template,
+ content: content
+ }
+ end
+
+ def ways_to_reach_you_message_params
+ content = I18n.t('conversations.templates.ways_to_reach_you_message_body',
+ account_name: account.name)
+
+ {
+ account_id: @conversation.account_id,
+ inbox_id: @conversation.inbox_id,
+ message_type: :template,
+ content: content
+ }
+ end
+
+ def email_input_box_template_message_params
+ content = I18n.t('conversations.templates.email_input_box_message_body',
+ account_name: account.name)
+
+ {
+ account_id: @conversation.account_id,
+ inbox_id: @conversation.inbox_id,
+ message_type: :template,
+ content_type: :input_email,
+ content: content
+ }
+ end
+end
diff --git a/app/services/widget/token_service.rb b/app/services/widget/token_service.rb
new file mode 100644
index 000000000..07247fe2a
--- /dev/null
+++ b/app/services/widget/token_service.rb
@@ -0,0 +1,21 @@
+class Widget::TokenService
+ pattr_initialize [:payload, :token]
+
+ def generate_token
+ JWT.encode payload, secret_key, 'HS256'
+ end
+
+ def decode_token
+ JWT.decode(
+ token, secret_key, true, algorithm: 'HS256'
+ ).first.symbolize_keys
+ rescue StandardError
+ {}
+ end
+
+ private
+
+ def secret_key
+ Rails.application.secrets.secret_key_base
+ end
+end
diff --git a/app/views/api/v1/widget/messages/index.json.jbuilder b/app/views/api/v1/widget/messages/index.json.jbuilder
index 57bab734c..1f423beb1 100644
--- a/app/views/api/v1/widget/messages/index.json.jbuilder
+++ b/app/views/api/v1/widget/messages/index.json.jbuilder
@@ -2,6 +2,8 @@ json.array! @messages do |message|
json.id message.id
json.content message.content
json.message_type message.message_type_before_type_cast
+ json.content_type message.content_type
+ json.content_attributes message.content_attributes
json.created_at message.created_at.to_i
json.conversation_id message. conversation_id
json.attachment message.attachment.push_event_data if message.attachment
diff --git a/app/views/widgets/index.html.erb b/app/views/widgets/show.html.erb
similarity index 100%
rename from app/views/widgets/index.html.erb
rename to app/views/widgets/show.html.erb
diff --git a/config/environments/test.rb b/config/environments/test.rb
index 16bfd6192..29018c894 100644
--- a/config/environments/test.rb
+++ b/config/environments/test.rb
@@ -29,7 +29,7 @@ Rails.application.configure do
config.cache_store = :null_store
# Raise exceptions instead of rendering exception templates.
- config.action_dispatch.show_exceptions = false
+ config.action_dispatch.show_exceptions = true
# Disable request forgery protection in test environment.
config.action_controller.allow_forgery_protection = false
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 3306c76c5..b11a27bda 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -50,3 +50,7 @@ en:
assignee:
assigned: "Assigned to %{assignee_name} by %{user_name}"
removed: "Conversation unassigned by %{user_name}"
+ templates:
+ typical_reply_message_body: "%{account_name} typically replies in a few hours."
+ ways_to_reach_you_message_body: "Give the team a way to reach you."
+ email_input_box_message_body: "Get notified by email"
diff --git a/config/routes.rb b/config/routes.rb
index 4a61500c1..35dd8ebdd 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -11,7 +11,7 @@ Rails.application.routes.draw do
match '/status', to: 'home#status', via: [:get]
- resources :widgets, only: [:index]
+ resource :widget, only: [:show]
namespace :api, defaults: { format: 'json' } do
namespace :v1 do
@@ -25,7 +25,7 @@ Rails.application.routes.draw do
end
namespace :widget do
- resources :messages, only: [:index, :create]
+ resources :messages, only: [:index, :create, :update]
resources :inboxes, only: [:create, :update]
end
diff --git a/db/migrate/20191130164019_add_template_type_to_messages.rb b/db/migrate/20191130164019_add_template_type_to_messages.rb
new file mode 100644
index 000000000..ec4c0e949
--- /dev/null
+++ b/db/migrate/20191130164019_add_template_type_to_messages.rb
@@ -0,0 +1,6 @@
+class AddTemplateTypeToMessages < ActiveRecord::Migration[6.0]
+ def change
+ add_column :messages, :content_type, :integer, default: '0'
+ add_column :messages, :content_attributes, :json, default: {}
+ end
+end
diff --git a/db/migrate/20200107164449_add_contact_inbox_to_conversation.rb b/db/migrate/20200107164449_add_contact_inbox_to_conversation.rb
new file mode 100644
index 000000000..47ab3cf7a
--- /dev/null
+++ b/db/migrate/20200107164449_add_contact_inbox_to_conversation.rb
@@ -0,0 +1,14 @@
+class AddContactInboxToConversation < ActiveRecord::Migration[6.0]
+ def change
+ add_reference(:conversations, :contact_inbox, foreign_key: true, index: true)
+
+ ::Conversation.all.each do |conversation|
+ contact_inbox = ::ContactInbox.find_by(
+ contact_id: conversation.contact_id,
+ inbox_id: conversation.inbox_id
+ )
+
+ conversation.update!(contact_inbox_id: contact_inbox.id) if contact_inbox
+ end
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index c3880cf94..9fb5ea94e 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 2019_12_27_191631) do
+ActiveRecord::Schema.define(version: 2020_01_07_164449) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -123,8 +123,10 @@ ActiveRecord::Schema.define(version: 2019_12_27_191631) do
t.datetime "agent_last_seen_at"
t.boolean "locked", default: false
t.jsonb "additional_attributes"
+ t.bigint "contact_inbox_id"
t.index ["account_id", "display_id"], name: "index_conversations_on_account_id_and_display_id", unique: true
t.index ["account_id"], name: "index_conversations_on_account_id"
+ t.index ["contact_inbox_id"], name: "index_conversations_on_contact_inbox_id"
end
create_table "inbox_members", id: :serial, force: :cascade do |t|
@@ -157,6 +159,8 @@ ActiveRecord::Schema.define(version: 2019_12_27_191631) do
t.integer "user_id"
t.integer "status", default: 0
t.string "fb_id"
+ t.integer "content_type", default: 0
+ t.json "content_attributes", default: {}
t.index ["conversation_id"], name: "index_messages_on_conversation_id"
end
@@ -186,11 +190,9 @@ ActiveRecord::Schema.define(version: 2019_12_27_191631) do
t.index ["taggable_id", "taggable_type", "context"], name: "index_taggings_on_taggable_id_and_taggable_type_and_context"
t.index ["taggable_id", "taggable_type", "tagger_id", "context"], name: "taggings_idy"
t.index ["taggable_id"], name: "index_taggings_on_taggable_id"
- t.index ["taggable_type", "taggable_id"], name: "index_taggings_on_taggable_type_and_taggable_id"
t.index ["taggable_type"], name: "index_taggings_on_taggable_type"
t.index ["tagger_id", "tagger_type"], name: "index_taggings_on_tagger_id_and_tagger_type"
t.index ["tagger_id"], name: "index_taggings_on_tagger_id"
- t.index ["tagger_type", "tagger_id"], name: "index_taggings_on_tagger_type_and_tagger_id"
end
create_table "tags", id: :serial, force: :cascade do |t|
@@ -243,5 +245,6 @@ ActiveRecord::Schema.define(version: 2019_12_27_191631) do
add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id"
add_foreign_key "contact_inboxes", "contacts"
add_foreign_key "contact_inboxes", "inboxes"
+ add_foreign_key "conversations", "contact_inboxes"
add_foreign_key "users", "users", column: "inviter_id", on_delete: :nullify
end
diff --git a/db/seeds.rb b/db/seeds.rb
index 1ba0711e4..aed828286 100644
--- a/db/seeds.rb
+++ b/db/seeds.rb
@@ -10,6 +10,6 @@ inbox = Inbox.create!(channel: web_widget, account: account, name: 'Acme Support
InboxMember.create!(user: user, inbox: inbox)
contact = Contact.create!(name: 'jane', email: 'jane@example.com', phone_number: '0000', account: account)
-ContactInbox.create!(inbox: inbox, contact: contact, source_id: user.id)
-conversation = Conversation.create!(account: account, inbox: inbox, status: :open, assignee: user, contact: contact)
-Message.create!(content: 'Hello', account: account, inbox: inbox, conversation: conversation, message_type: :incoming)
\ No newline at end of file
+contact_inbox = ContactInbox.create!(inbox: inbox, contact: contact, source_id: user.id)
+conversation = Conversation.create!(account: account, inbox: inbox, status: :open, assignee: user, contact: contact, contact_inbox: contact_inbox)
+Message.create!(content: 'Hello', account: account, inbox: inbox, conversation: conversation, message_type: :incoming)
diff --git a/lib/integrations/facebook/delivery_status.rb b/lib/integrations/facebook/delivery_status.rb
index ab0774dad..173d514ab 100644
--- a/lib/integrations/facebook/delivery_status.rb
+++ b/lib/integrations/facebook/delivery_status.rb
@@ -1,34 +1,30 @@
# frozen_string_literal: true
-module Integrations
- module Facebook
- class DeliveryStatus
- def initialize(params)
- @params = params
- end
+class Integrations::Facebook::DeliveryStatus
+ def initialize(params)
+ @params = params
+ end
- def perform
- update_message_status
- end
+ def perform
+ update_message_status
+ end
- private
+ private
- def sender_id
- @params.sender['id']
- end
+ def sender_id
+ @params.sender['id']
+ end
- def contact
- ::ContactInbox.find_by(source_id: sender_id).contact
- end
+ def contact
+ ::ContactInbox.find_by(source_id: sender_id).contact
+ end
- def conversation
- @conversation ||= ::Conversation.find_by(contact_id: contact.id)
- end
+ def conversation
+ @conversation ||= ::Conversation.find_by(contact_id: contact.id)
+ end
- def update_message_status
- conversation.user_last_seen_at = @params.at
- conversation.save!
- end
- end
+ def update_message_status
+ conversation.user_last_seen_at = @params.at
+ conversation.save!
end
end
diff --git a/lib/integrations/facebook/message_creator.rb b/lib/integrations/facebook/message_creator.rb
index c04796f71..abf6c93e2 100644
--- a/lib/integrations/facebook/message_creator.rb
+++ b/lib/integrations/facebook/message_creator.rb
@@ -1,47 +1,43 @@
# frozen_string_literal: true
-module Integrations
- module Facebook
- class MessageCreator
- attr_reader :response
+class Integrations::Facebook::MessageCreator
+ attr_reader :response
- def initialize(response)
- @response = response
- end
+ def initialize(response)
+ @response = response
+ end
- def perform
- # begin
- if outgoing_message_via_echo?
- create_outgoing_message
- else
- create_incoming_message
- end
- # rescue => e
- # Raven.capture_exception(e)
- # end
- end
+ def perform
+ # begin
+ if outgoing_message_via_echo?
+ create_outgoing_message
+ else
+ create_incoming_message
+ end
+ # rescue => e
+ # Raven.capture_exception(e)
+ # end
+ end
- private
+ private
- def outgoing_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
- end
+ def outgoing_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
+ end
- def create_outgoing_message
- Channel::FacebookPage.where(page_id: response.sender_id).each do |page|
- mb = Messages::Outgoing::EchoBuilder.new(response, page.inbox, true)
- mb.perform
- end
- end
+ def create_outgoing_message
+ Channel::FacebookPage.where(page_id: response.sender_id).each do |page|
+ mb = Messages::Outgoing::EchoBuilder.new(response, page.inbox, true)
+ mb.perform
+ end
+ end
- def create_incoming_message
- Channel::FacebookPage.where(page_id: response.recipient_id).each do |page|
- mb = Messages::IncomingMessageBuilder.new(response, page.inbox)
- mb.perform
- end
- end
+ def create_incoming_message
+ Channel::FacebookPage.where(page_id: response.recipient_id).each do |page|
+ mb = Messages::IncomingMessageBuilder.new(response, page.inbox)
+ mb.perform
end
end
end
diff --git a/lib/integrations/facebook/message_parser.rb b/lib/integrations/facebook/message_parser.rb
index 5e8e39286..6ddad68f9 100644
--- a/lib/integrations/facebook/message_parser.rb
+++ b/lib/integrations/facebook/message_parser.rb
@@ -1,52 +1,48 @@
# frozen_string_literal: true
-module Integrations
- module Facebook
- class MessageParser
- def initialize(response_json)
- @response = response_json
- end
+class Integrations::Facebook::MessageParser
+ def initialize(response_json)
+ @response = response_json
+ end
- def sender_id
- @response.sender['id']
- end
+ def sender_id
+ @response.sender['id']
+ end
- def recipient_id
- @response.recipient['id']
- end
+ def recipient_id
+ @response.recipient['id']
+ end
- def time_stamp
- @response.sent_at
- end
+ def time_stamp
+ @response.sent_at
+ end
- def content
- @response.text
- end
+ def content
+ @response.text
+ end
- def sequence
- @response.seq
- end
+ def sequence
+ @response.seq
+ end
- def attachments
- @response.attachments
- end
+ def attachments
+ @response.attachments
+ end
- def identifier
- @response.id
- end
+ def identifier
+ @response.id
+ end
- def echo?
- @response.echo?
- end
+ def echo?
+ @response.echo?
+ end
- def app_id
- @response.app_id
- end
+ def app_id
+ @response.app_id
+ end
- def sent_from_chatwoot_app?
- app_id && app_id == ENV['FB_APP_ID'].to_i
- end
- end
+ def sent_from_chatwoot_app?
+ app_id && app_id == ENV['FB_APP_ID'].to_i
end
end
diff --git a/lib/integrations/widget/incoming_message_builder.rb b/lib/integrations/widget/incoming_message_builder.rb
index fb3f32984..abc14aafa 100644
--- a/lib/integrations/widget/incoming_message_builder.rb
+++ b/lib/integrations/widget/incoming_message_builder.rb
@@ -1,61 +1,57 @@
# frozen_string_literal: true
-module Integrations
- module Widget
- class Integrations::Widget::IncomingMessageBuilder
- # params = {
- # contact_id: 1,
- # inbox_id: 1,
- # content: "Hello world"
- # }
+class Integrations::Widget::IncomingMessageBuilder
+ # params = {
+ # contact_id: 1,
+ # inbox_id: 1,
+ # content: "Hello world"
+ # }
- attr_accessor :options, :message
+ attr_accessor :options, :message
- def initialize(options)
- @options = options
- end
+ def initialize(options)
+ @options = options
+ end
- def perform
- ActiveRecord::Base.transaction do
- build_message
- end
- end
-
- private
-
- def inbox
- @inbox ||= Inbox.find(options[:inbox_id])
- end
-
- def contact
- @contact ||= Contact.find(options[:contact_id])
- end
-
- def conversation
- @conversation ||= Conversation.find_by(conversation_params) || Conversation.create!(conversation_params)
- end
-
- def build_message
- @message = conversation.messages.new(message_params)
- @message.save!
- end
-
- def conversation_params
- {
- account_id: inbox.account_id,
- inbox_id: inbox.id,
- contact_id: options[:contact_id]
- }
- end
-
- def message_params
- {
- account_id: conversation.account_id,
- inbox_id: conversation.inbox_id,
- message_type: 0,
- content: options[:content]
- }
- end
+ def perform
+ ActiveRecord::Base.transaction do
+ build_message
end
end
+
+ private
+
+ def inbox
+ @inbox ||= Inbox.find(options[:inbox_id])
+ end
+
+ def contact
+ @contact ||= Contact.find(options[:contact_id])
+ end
+
+ def conversation
+ @conversation ||= Conversation.find_by(conversation_params) || Conversation.create!(conversation_params)
+ end
+
+ def build_message
+ @message = conversation.messages.new(message_params)
+ @message.save!
+ end
+
+ def conversation_params
+ {
+ account_id: inbox.account_id,
+ inbox_id: inbox.id,
+ contact_id: options[:contact_id]
+ }
+ end
+
+ def message_params
+ {
+ account_id: conversation.account_id,
+ inbox_id: conversation.inbox_id,
+ message_type: 0,
+ content: options[:content]
+ }
+ end
end
diff --git a/spec/actions/contact_merge_action_spec.rb b/spec/actions/contact_merge_action_spec.rb
index 7ffcaf1fa..bf95b7291 100644
--- a/spec/actions/contact_merge_action_spec.rb
+++ b/spec/actions/contact_merge_action_spec.rb
@@ -9,9 +9,7 @@ describe ::ContactMergeAction do
before do
2.times.each { create(:conversation, contact: base_contact) }
- 2.times.each { create(:contact_inbox, contact: base_contact) }
2.times.each { create(:conversation, contact: mergee_contact) }
- 2.times.each { create(:contact_inbox, contact: mergee_contact) }
end
describe '#perform' do
diff --git a/spec/builders/messages/message_builder_spec.rb b/spec/builders/messages/incoming_message_builder_spec.rb
similarity index 96%
rename from spec/builders/messages/message_builder_spec.rb
rename to spec/builders/messages/incoming_message_builder_spec.rb
index f86489066..0e91d3ae6 100644
--- a/spec/builders/messages/message_builder_spec.rb
+++ b/spec/builders/messages/incoming_message_builder_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-describe ::Messages::MessageBuilder do
+describe ::Messages::IncomingMessageBuilder 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/controllers/api/v1/widget/messages_controller_spec.rb b/spec/controllers/api/v1/widget/messages_controller_spec.rb
new file mode 100644
index 000000000..213335eeb
--- /dev/null
+++ b/spec/controllers/api/v1/widget/messages_controller_spec.rb
@@ -0,0 +1,84 @@
+require 'rails_helper'
+
+RSpec.describe '/api/v1/widget/messages', type: :request do
+ let(:account) { create(:account) }
+ let(:web_widget) { create(:channel_widget, account: account) }
+ let(:contact) { create(:contact, account: account) }
+ let(:contact_inbox) { create(:contact_inbox, contact: contact, inbox: web_widget.inbox) }
+ let(:conversation) { create(:conversation, contact: contact, account: account, inbox: web_widget.inbox, contact_inbox: contact_inbox) }
+ let(:payload) { { source_id: contact_inbox.source_id, inbox_id: web_widget.inbox.id } }
+ let(:token) { ::Widget::TokenService.new(payload: payload).generate_token }
+
+ before do
+ 2.times.each { create(:message, account: account, inbox: web_widget.inbox, conversation: conversation) }
+ end
+
+ describe 'GET /api/v1/widget/messages' do
+ context 'when get request is made' do
+ it 'returns messages in conversation' do
+ get api_v1_widget_messages_url,
+ params: { website_token: web_widget.website_token },
+ headers: { 'X-Auth-Token' => token },
+ as: :json
+
+ expect(response).to have_http_status(:success)
+ json_response = JSON.parse(response.body)
+
+ # 2 messages created + 3 messages by the template hook
+ expect(json_response.length).to eq(5)
+ end
+ end
+ end
+
+ describe 'POST /api/v1/widget/messages' do
+ context 'when post request is made' do
+ it 'creates message in conversation' do
+ message_params = { content: 'hello world' }
+ post api_v1_widget_messages_url,
+ params: { website_token: web_widget.website_token, message: message_params },
+ headers: { 'X-Auth-Token' => token },
+ as: :json
+
+ expect(response).to have_http_status(:success)
+ json_response = JSON.parse(response.body)
+ expect(json_response['content']).to eq(message_params[:content])
+ end
+ end
+ end
+
+ describe 'PUT /api/v1/widget/messages' do
+ context 'when put request is made with non existing email' do
+ it 'updates message in conversation and creates a new contact' do
+ message = create(:message, account: account, inbox: web_widget.inbox, conversation: conversation)
+ email = Faker::Internet.email
+ contact_params = { email: email }
+ put api_v1_widget_message_url(message.id),
+ params: { website_token: web_widget.website_token, contact: contact_params },
+ headers: { 'X-Auth-Token' => token },
+ as: :json
+
+ expect(response).to have_http_status(:success)
+ message.reload
+ expect(message.input_submitted_email).to eq(email)
+ expect(message.conversation.contact.email).to eq(email)
+ end
+ end
+
+ context 'when put request is made with existing email' do
+ it 'updates message in conversation and deletes the current contact' do
+ message = create(:message, account: account, inbox: web_widget.inbox, conversation: conversation)
+ email = Faker::Internet.email
+ create(:contact, account: account, email: email)
+ contact_params = { email: email }
+ put api_v1_widget_message_url(message.id),
+ params: { website_token: web_widget.website_token, contact: contact_params },
+ headers: { 'X-Auth-Token' => token },
+ as: :json
+
+ expect(response).to have_http_status(:success)
+ message.reload
+ expect { contact.reload }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+ end
+ end
+end
diff --git a/spec/controllers/widget_tests_controller_spec.rb b/spec/controllers/widget_tests_controller_spec.rb
index 5d0d6ccb8..04c5aa880 100644
--- a/spec/controllers/widget_tests_controller_spec.rb
+++ b/spec/controllers/widget_tests_controller_spec.rb
@@ -1,12 +1,14 @@
require 'rails_helper'
-describe WidgetTestsController, type: :controller do
- let(:channel_widget) { create(:channel_widget) }
+describe '/widget_tests', type: :request do
+ before do
+ create(:channel_widget)
+ end
- describe '#index' do
+ describe 'GET /widget_tests' do
it 'renders the page correctly' do
- get :index
- expect(response.status).to eq 200
+ get widget_tests_url
+ expect(response).to be_successful
end
end
end
diff --git a/spec/controllers/widgets_controller_spec.rb b/spec/controllers/widgets_controller_spec.rb
new file mode 100644
index 000000000..6f6eb9c34
--- /dev/null
+++ b/spec/controllers/widgets_controller_spec.rb
@@ -0,0 +1,17 @@
+require 'rails_helper'
+
+describe '/widget', type: :request do
+ let(:web_widget) { create(:channel_widget) }
+
+ describe 'GET /widget' do
+ it 'renders the page correctly when called with website_token' do
+ get widget_url(website_token: web_widget.website_token)
+ expect(response).to be_successful
+ end
+
+ it 'returns 404 when called with out website_token' do
+ get widget_url
+ expect(response.status).to eq(404)
+ end
+ end
+end
diff --git a/spec/factories/channel/channel_widget.rb b/spec/factories/channel/channel_widget.rb
index 6925ebb52..e079d7db6 100644
--- a/spec/factories/channel/channel_widget.rb
+++ b/spec/factories/channel/channel_widget.rb
@@ -6,5 +6,8 @@ FactoryBot.define do
sequence(:website_url) { |n| "https://example-#{n}.com" }
sequence(:widget_color, &:to_s)
account
+ after(:create) do |channel_widget|
+ create(:inbox, channel: channel_widget, account: channel_widget.account)
+ end
end
end
diff --git a/spec/factories/conversations.rb b/spec/factories/conversations.rb
index f89fc899c..a14845e7f 100644
--- a/spec/factories/conversations.rb
+++ b/spec/factories/conversations.rb
@@ -16,6 +16,7 @@ FactoryBot.define do
channel: create(:channel_widget, account: conversation.account)
)
conversation.contact ||= create(:contact, account: conversation.account)
+ conversation.contact_inbox ||= create(:contact_inbox, contact: conversation.contact, inbox: conversation.inbox)
end
end
end
diff --git a/spec/factories/inboxes.rb b/spec/factories/inboxes.rb
index 5017c3d3d..cdd23f3c3 100644
--- a/spec/factories/inboxes.rb
+++ b/spec/factories/inboxes.rb
@@ -3,7 +3,11 @@
FactoryBot.define do
factory :inbox do
account
- name { 'Inbox' }
channel { FactoryBot.build(:channel_widget, account: account) }
+ name { 'Inbox' }
+
+ after(:create) do |inbox|
+ inbox.channel.save!
+ end
end
end
diff --git a/spec/factories/messages.rb b/spec/factories/messages.rb
index 4c60c1d04..6f9e37a5d 100644
--- a/spec/factories/messages.rb
+++ b/spec/factories/messages.rb
@@ -5,9 +5,13 @@ FactoryBot.define do
content { 'Message' }
status { 'sent' }
message_type { 'incoming' }
- account
- inbox
- conversation
- user
+ content_type { 'text' }
+ account { create(:account) }
+
+ after(:build) do |message|
+ message.user ||= create(:user, account: message.account)
+ message.conversation ||= create(:conversation, account: message.account)
+ message.inbox ||= create(:inbox, account: message.account)
+ end
end
end
diff --git a/spec/finders/message_finder_spec.rb b/spec/finders/message_finder_spec.rb
index ca05a510d..a8da9a56f 100644
--- a/spec/finders/message_finder_spec.rb
+++ b/spec/finders/message_finder_spec.rb
@@ -21,7 +21,7 @@ describe ::MessageFinder do
it 'filter conversations by status' do
result = message_finder.perform
- expect(result.count).to be 4
+ expect(result.count).to be 7
end
end
@@ -30,7 +30,7 @@ describe ::MessageFinder do
it 'filter conversations by status' do
result = message_finder.perform
- expect(result.count).to be 2
+ expect(result.count).to be 5
end
end
@@ -40,7 +40,7 @@ describe ::MessageFinder do
it 'filter conversations by status' do
result = message_finder.perform
- expect(result.count).to be 4
+ expect(result.count).to be 7
end
end
end
diff --git a/spec/models/conversation_spec.rb b/spec/models/conversation_spec.rb
index 376ee6f25..e2d44668d 100644
--- a/spec/models/conversation_spec.rb
+++ b/spec/models/conversation_spec.rb
@@ -57,19 +57,17 @@ RSpec.describe Conversation, type: :model do
expect(Rails.configuration.dispatcher).to have_received(:dispatch)
.with(described_class::ASSIGNEE_CHANGED, kind_of(Time), conversation: conversation)
- # create_activity
- expect(conversation.messages.pluck(:content)).to eq(
- [
- "Conversation was marked resolved by #{old_assignee.name}",
- "Assigned to #{new_assignee.name} by #{old_assignee.name}"
- ]
- )
-
# send_email_notification_to_assignee
expect(AssignmentMailer).to have_received(:conversation_assigned).with(conversation, new_assignee)
expect(assignment_mailer).to have_received(:deliver_later) if ENV.fetch('SMTP_ADDRESS', nil).present?
end
+
+ it 'creates conversation activities' do
+ # create_activity
+ expect(conversation.messages.pluck(:content)).to include("Conversation was marked resolved by #{old_assignee.name}")
+ expect(conversation.messages.pluck(:content)).to include("Assigned to #{new_assignee.name} by #{old_assignee.name}")
+ end
end
describe '.after_create' do
@@ -169,7 +167,7 @@ RSpec.describe Conversation, type: :model do
end
it 'returns unread messages' do
- expect(unread_messages).to contain_exactly(message)
+ expect(unread_messages).to include(message)
end
end
diff --git a/spec/models/message_spec.rb b/spec/models/message_spec.rb
new file mode 100644
index 000000000..6df065b24
--- /dev/null
+++ b/spec/models/message_spec.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe Message, type: :model do
+ context 'with validations' do
+ it { is_expected.to validate_presence_of(:inbox_id) }
+ it { is_expected.to validate_presence_of(:conversation_id) }
+ it { is_expected.to validate_presence_of(:account_id) }
+ end
+
+ context 'when message is created' do
+ let(:message) { build(:message) }
+
+ it 'triggers ::MessageTemplates::HookExecutionService' do
+ hook_execution_service = double
+ allow(::MessageTemplates::HookExecutionService).to receive(:new).and_return(hook_execution_service)
+ allow(hook_execution_service).to receive(:perform).and_return(true)
+
+ message.save!
+
+ expect(::MessageTemplates::HookExecutionService).to have_received(:new).with(message: message)
+ expect(hook_execution_service).to have_received(:perform)
+ end
+ end
+end
diff --git a/spec/services/facebook/send_reply_service_spec.rb b/spec/services/facebook/send_reply_service_spec.rb
index f926fa833..4e8bc9097 100644
--- a/spec/services/facebook/send_reply_service_spec.rb
+++ b/spec/services/facebook/send_reply_service_spec.rb
@@ -14,7 +14,8 @@ describe Facebook::SendReplyService do
let!(:facebook_channel) { create(:facebook_page, account: account) }
let!(:facebook_inbox) { create(:inbox, channel: facebook_channel, account: account) }
let!(:contact) { create(:contact, account: account) }
- let(:conversation) { create(:conversation, contact: contact, inbox: facebook_inbox) }
+ let(:contact_inbox) { create(:contact_inbox, contact: contact, inbox: facebook_inbox) }
+ let(:conversation) { create(:conversation, contact: contact, inbox: facebook_inbox, contact_inbox: contact_inbox) }
describe '#perform' do
context 'without reply' do
@@ -41,7 +42,6 @@ describe Facebook::SendReplyService do
context 'with reply' do
it 'if message is sent from chatwoot and is outgoing' do
- create(:contact_inbox, contact: contact, inbox: facebook_inbox)
create(:message, message_type: :incoming, inbox: facebook_inbox, account: account, conversation: conversation)
create(:message, message_type: 'outgoing', inbox: facebook_inbox, account: account, conversation: conversation)
expect(bot).to have_received(:deliver)
diff --git a/spec/services/message_templates/hook_execution_service_spec.rb b/spec/services/message_templates/hook_execution_service_spec.rb
new file mode 100644
index 000000000..ff137c33b
--- /dev/null
+++ b/spec/services/message_templates/hook_execution_service_spec.rb
@@ -0,0 +1,20 @@
+require 'rails_helper'
+
+describe ::MessageTemplates::HookExecutionService do
+ context 'when it is a first message from web widget' do
+ it 'calls ::MessageTemplates::Template::EmailCollect' do
+ message = create(:message)
+ # this hook will only get executed for conversations with out any template messages
+ message.conversation.messages.template.destroy_all
+
+ email_collect_service = double
+ allow(::MessageTemplates::Template::EmailCollect).to receive(:new).and_return(email_collect_service)
+ allow(email_collect_service).to receive(:perform).and_return(true)
+
+ described_class.new(message: message).perform
+
+ expect(::MessageTemplates::Template::EmailCollect).to have_received(:new).with(conversation: message.conversation)
+ expect(email_collect_service).to have_received(:perform)
+ end
+ end
+end
diff --git a/spec/services/message_templates/template/email_collect_spec.rb b/spec/services/message_templates/template/email_collect_spec.rb
new file mode 100644
index 000000000..1d688e37a
--- /dev/null
+++ b/spec/services/message_templates/template/email_collect_spec.rb
@@ -0,0 +1,12 @@
+require 'rails_helper'
+
+describe ::MessageTemplates::Template::EmailCollect do
+ context 'when this hook is called' do
+ let(:conversation) { create(:conversation) }
+
+ it 'creates the email collect messages' do
+ described_class.new(conversation: conversation).perform
+ expect(conversation.messages.count).to eq(3)
+ end
+ end
+end