diff --git a/app/builders/messages/outgoing/normal_builder.rb b/app/builders/messages/outgoing/normal_builder.rb index b75e8f84b..5969a97c5 100644 --- a/app/builders/messages/outgoing/normal_builder.rb +++ b/app/builders/messages/outgoing/normal_builder.rb @@ -3,7 +3,7 @@ class Messages::Outgoing::NormalBuilder def initialize(user, conversation, params) @content = params[:message] - @private = ['1', 'true', 1, true].include? params[:private] + @private = params[:private] || false @conversation = conversation @user = user @fb_id = params[:fb_id] diff --git a/app/controllers/api/v1/webhooks_controller.rb b/app/controllers/api/v1/webhooks_controller.rb index d15b414c1..5db576000 100644 --- a/app/controllers/api/v1/webhooks_controller.rb +++ b/app/controllers/api/v1/webhooks_controller.rb @@ -5,6 +5,7 @@ class Api::V1::WebhooksController < ApplicationController before_action :login_from_basic_auth, only: [:chargebee] before_action :check_billing_enabled, only: [:chargebee] + def chargebee chargebee_consumer.consume head :ok diff --git a/app/dispatchers/async_dispatcher.rb b/app/dispatchers/async_dispatcher.rb index dff0c9d92..75b853cd0 100644 --- a/app/dispatchers/async_dispatcher.rb +++ b/app/dispatchers/async_dispatcher.rb @@ -5,7 +5,7 @@ class AsyncDispatcher < BaseDispatcher end def listeners - listeners = [EmailNotificationListener.instance, ReportingListener.instance, WebhookListener.instance] + listeners = [AgentBotListener.instance, EmailNotificationListener.instance, ReportingListener.instance, WebhookListener.instance] listeners << SubscriptionListener.instance if ENV['BILLING_ENABLED'] listeners end diff --git a/app/jobs/agent_bot_job.rb b/app/jobs/agent_bot_job.rb new file mode 100644 index 000000000..e988701c4 --- /dev/null +++ b/app/jobs/agent_bot_job.rb @@ -0,0 +1,3 @@ +class AgentBotJob < WebhookJob + queue_as :bots +end diff --git a/app/listeners/agent_bot_listener.rb b/app/listeners/agent_bot_listener.rb new file mode 100644 index 000000000..848c09cb7 --- /dev/null +++ b/app/listeners/agent_bot_listener.rb @@ -0,0 +1,13 @@ +class AgentBotListener < BaseListener + def message_created(event) + message = extract_message_and_account(event)[0] + inbox = message.inbox + return unless message.reportable? && inbox.agent_bot_inbox.present? + return unless inbox.agent_bot_inbox.active? + + agent_bot = inbox.agent_bot_inbox.agent_bot + + payload = message.webhook_data.merge(event: __method__.to_s) + AgentBotJob.perform_later(agent_bot.outgoing_url, payload) + end +end diff --git a/app/listeners/email_notification_listener.rb b/app/listeners/email_notification_listener.rb index c9dfcfb09..a928960b2 100644 --- a/app/listeners/email_notification_listener.rb +++ b/app/listeners/email_notification_listener.rb @@ -1,6 +1,8 @@ class EmailNotificationListener < BaseListener def conversation_created(event) conversation, _account, _timestamp = extract_conversation_and_account(event) + return if conversation.bot? + conversation.inbox.members.each do |agent| next unless agent.notification_settings.find_by(account_id: conversation.account_id).conversation_creation? diff --git a/app/models/agent_bot.rb b/app/models/agent_bot.rb new file mode 100644 index 000000000..a1ff79768 --- /dev/null +++ b/app/models/agent_bot.rb @@ -0,0 +1,19 @@ +# == Schema Information +# +# Table name: agent_bots +# +# id :bigint not null, primary key +# auth_token :string +# description :string +# name :string +# outgoing_url :string +# created_at :datetime not null +# updated_at :datetime not null +# + +class AgentBot < ApplicationRecord + include Avatarable + has_many :agent_bot_inboxes, dependent: :destroy + has_many :inboxes, through: :agent_bot_inboxes + has_secure_token :auth_token +end diff --git a/app/models/agent_bot_inbox.rb b/app/models/agent_bot_inbox.rb new file mode 100644 index 000000000..aa845a578 --- /dev/null +++ b/app/models/agent_bot_inbox.rb @@ -0,0 +1,20 @@ +# == Schema Information +# +# Table name: agent_bot_inboxes +# +# id :bigint not null, primary key +# status :integer default("active") +# created_at :datetime not null +# updated_at :datetime not null +# agent_bot_id :integer +# inbox_id :integer +# + +class AgentBotInbox < ApplicationRecord + validates :inbox_id, presence: true + validates :agent_bot_id, presence: true + + belongs_to :inbox + belongs_to :agent_bot + enum status: { active: 0, inactive: 1 } +end diff --git a/app/models/conversation.rb b/app/models/conversation.rb index 51f53039a..b868153c2 100644 --- a/app/models/conversation.rb +++ b/app/models/conversation.rb @@ -34,7 +34,7 @@ class Conversation < ApplicationRecord validates :account_id, presence: true validates :inbox_id, presence: true - enum status: [:open, :resolved] + enum status: { open: 0, resolved: 1, bot: 2 } scope :latest, -> { order(created_at: :desc) } scope :unassigned, -> { where(assignee_id: nil) } @@ -50,6 +50,8 @@ class Conversation < ApplicationRecord before_create :set_display_id, unless: :display_id? + before_create :set_bot_conversation + after_update :notify_status_change, :create_activity, :send_email_notification_to_assignee after_create :send_events, :run_round_robin @@ -102,6 +104,10 @@ class Conversation < ApplicationRecord private + def set_bot_conversation + self.status = :bot if inbox.agent_bot_inbox&.active? + end + def dispatch_events dispatcher_dispatch(CONVERSATION_RESOLVED) end @@ -110,11 +116,18 @@ class Conversation < ApplicationRecord dispatcher_dispatch(CONVERSATION_CREATED) end + def notifiable_assignee_change? + return false if self_assign?(assignee_id) + return false unless saved_change_to_assignee_id? + return false if assignee_id.blank? + + true + end + def send_email_notification_to_assignee - return if self_assign?(assignee_id) - return unless saved_change_to_assignee_id? - return if assignee_id.blank? + return unless notifiable_assignee_change? return if assignee.notification_settings.find_by(account_id: account_id).not_conversation_assignment? + return if bot? AgentNotifications::ConversationNotificationsMailer.conversation_assigned(self, assignee).deliver_later end @@ -161,6 +174,7 @@ class Conversation < ApplicationRecord def run_round_robin return unless inbox.enable_auto_assignment return if assignee + return if bot? inbox.next_available_agent.then { |new_assignee| update_assignee(new_assignee) } end diff --git a/app/models/inbox.rb b/app/models/inbox.rb index 3420cf3f0..bda28b160 100644 --- a/app/models/inbox.rb +++ b/app/models/inbox.rb @@ -33,7 +33,10 @@ class Inbox < ApplicationRecord has_many :members, through: :inbox_members, source: :user has_many :conversations, dependent: :destroy has_many :messages, through: :conversations + + has_one :agent_bot_inbox, dependent: :destroy has_many :webhooks, dependent: :destroy + after_create :subscribe_webhook, if: :facebook? after_destroy :delete_round_robin_agents diff --git a/app/services/message_templates/hook_execution_service.rb b/app/services/message_templates/hook_execution_service.rb index 071e12a64..5117529c0 100644 --- a/app/services/message_templates/hook_execution_service.rb +++ b/app/services/message_templates/hook_execution_service.rb @@ -2,6 +2,8 @@ class MessageTemplates::HookExecutionService pattr_initialize [:message!] def perform + return if inbox.agent_bot_inbox&.active? + ::MessageTemplates::Template::EmailCollect.new(conversation: conversation).perform if should_send_email_collect? end diff --git a/config/sidekiq.yml b/config/sidekiq.yml index 1ffb5fa51..8dc7f5ede 100644 --- a/config/sidekiq.yml +++ b/config/sidekiq.yml @@ -16,6 +16,7 @@ - [low, 1] - [mailers, 2] - [webhooks, 1] + - [bots, 1] # you can override concurrency based on environment production: diff --git a/db/migrate/20200222143100_create_agent_bots.rb b/db/migrate/20200222143100_create_agent_bots.rb new file mode 100644 index 000000000..896ec8a0b --- /dev/null +++ b/db/migrate/20200222143100_create_agent_bots.rb @@ -0,0 +1,12 @@ +class CreateAgentBots < ActiveRecord::Migration[6.0] + def change + create_table :agent_bots do |t| + t.string :name + t.string :description + t.string :outgoing_url + t.string :auth_token, unique: true + + t.timestamps + end + end +end diff --git a/db/migrate/20200222143259_create_agent_bot_inboxes.rb b/db/migrate/20200222143259_create_agent_bot_inboxes.rb new file mode 100644 index 000000000..b2d50165e --- /dev/null +++ b/db/migrate/20200222143259_create_agent_bot_inboxes.rb @@ -0,0 +1,11 @@ +class CreateAgentBotInboxes < ActiveRecord::Migration[6.0] + def change + create_table :agent_bot_inboxes do |t| + t.integer :inbox_id + t.integer :agent_bot_id + t.integer :status, default: 0 + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 563501515..1927983cf 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -41,6 +41,23 @@ ActiveRecord::Schema.define(version: 20_200_226_194_012) do t.index ['key'], name: 'index_active_storage_blobs_on_key', unique: true end + create_table 'agent_bot_inboxes', force: :cascade do |t| + t.integer 'inbox_id' + t.integer 'agent_bot_id' + t.integer 'status', default: 0 + t.datetime 'created_at', precision: 6, null: false + t.datetime 'updated_at', precision: 6, null: false + end + + create_table 'agent_bots', force: :cascade do |t| + t.string 'name' + t.string 'description' + t.string 'outgoing_url' + t.string 'auth_token' + t.datetime 'created_at', precision: 6, null: false + t.datetime 'updated_at', precision: 6, null: false + end + create_table 'attachments', id: :serial, force: :cascade do |t| t.integer 'file_type', default: 0 t.string 'external_url' diff --git a/spec/factories/agent_bot_inboxes.rb b/spec/factories/agent_bot_inboxes.rb new file mode 100644 index 000000000..df687d78d --- /dev/null +++ b/spec/factories/agent_bot_inboxes.rb @@ -0,0 +1,7 @@ +FactoryBot.define do + factory :agent_bot_inbox do + inbox + agent_bot + status { 'active' } + end +end diff --git a/spec/factories/agent_bots.rb b/spec/factories/agent_bots.rb new file mode 100644 index 000000000..227fd86a2 --- /dev/null +++ b/spec/factories/agent_bots.rb @@ -0,0 +1,7 @@ +FactoryBot.define do + factory :agent_bot do + name { 'MyString' } + description { 'MyString' } + outgoing_url { 'MyString' } + end +end diff --git a/spec/jobs/agent_bot_job_spec.rb b/spec/jobs/agent_bot_job_spec.rb new file mode 100644 index 000000000..af73e4933 --- /dev/null +++ b/spec/jobs/agent_bot_job_spec.rb @@ -0,0 +1,14 @@ +require 'rails_helper' + +RSpec.describe AgentBotJob, type: :job do + subject(:job) { described_class.perform_later(url, payload) } + + let(:url) { 'https://test.com' } + let(:payload) { { name: 'test' } } + + it 'queues the job' do + expect { job }.to have_enqueued_job(described_class) + .with(url, payload) + .on_queue('bots') + end +end diff --git a/spec/listeners/agent_bot_listener_spec.rb b/spec/listeners/agent_bot_listener_spec.rb new file mode 100644 index 000000000..3425ef3d3 --- /dev/null +++ b/spec/listeners/agent_bot_listener_spec.rb @@ -0,0 +1,33 @@ +require 'rails_helper' +describe AgentBotListener do + let(:listener) { described_class.instance } + let!(:account) { create(:account) } + let!(:user) { create(:user, account: account) } + let!(:inbox) { create(:inbox, account: account) } + let!(:agent_bot) { create(:agent_bot) } + let!(:conversation) { create(:conversation, account: account, inbox: inbox, assignee: user) } + let!(:message) do + create(:message, message_type: 'outgoing', + account: account, inbox: inbox, conversation: conversation) + end + let!(:event) { Events::Base.new(event_name, Time.zone.now, message: message) } + + describe '#message_created' do + let(:event_name) { :'conversation.created' } + + context 'when agent bot is not configured' do + it 'does not send message to agent bot' do + expect(AgentBotJob).to receive(:perform_later).exactly(0).times + listener.message_created(event) + end + end + + context 'when agent bot is configured' do + it 'sends message to agent bot' do + create(:agent_bot_inbox, inbox: inbox, agent_bot: agent_bot) + expect(AgentBotJob).to receive(:perform_later).with(agent_bot.outgoing_url, message.webhook_data.merge(event: 'message_created')).once + listener.message_created(event) + end + end + end +end diff --git a/spec/models/agent_bot_inbox_spec.rb b/spec/models/agent_bot_inbox_spec.rb new file mode 100644 index 000000000..2c0ef84e7 --- /dev/null +++ b/spec/models/agent_bot_inbox_spec.rb @@ -0,0 +1,13 @@ +require 'rails_helper' + +RSpec.describe AgentBotInbox, type: :model do + describe 'validations' do + it { is_expected.to validate_presence_of(:inbox_id) } + it { is_expected.to validate_presence_of(:agent_bot_id) } + end + + describe 'associations' do + it { is_expected.to belong_to(:agent_bot) } + it { is_expected.to belong_to(:inbox) } + end +end diff --git a/spec/models/agent_bot_spec.rb b/spec/models/agent_bot_spec.rb new file mode 100644 index 000000000..d6f1500c4 --- /dev/null +++ b/spec/models/agent_bot_spec.rb @@ -0,0 +1,8 @@ +require 'rails_helper' + +RSpec.describe AgentBot, type: :model do + describe 'associations' do + it { is_expected.to have_many(:agent_bot_inboxes) } + it { is_expected.to have_many(:inboxes) } + end +end diff --git a/spec/models/conversation_spec.rb b/spec/models/conversation_spec.rb index baeb04748..68bdefc53 100644 --- a/spec/models/conversation_spec.rb +++ b/spec/models/conversation_spec.rb @@ -257,4 +257,13 @@ RSpec.describe Conversation, type: :model do expect(lock_event_data).to eq(id: 505, locked: false) end end + + describe '#botinbox: when conversation created inside inbox with agent bot' do + let!(:bot_inbox) { create(:agent_bot_inbox) } + let(:conversation) { create(:conversation, inbox: bot_inbox.inbox) } + + it 'returns conversation status as bot' do + expect(conversation.status).to eq('bot') + end + end end diff --git a/spec/models/inbox_spec.rb b/spec/models/inbox_spec.rb index abe14211d..db189b1d3 100644 --- a/spec/models/inbox_spec.rb +++ b/spec/models/inbox_spec.rb @@ -23,6 +23,9 @@ RSpec.describe Inbox do it { is_expected.to have_many(:conversations).dependent(:destroy) } it { is_expected.to have_many(:messages).through(:conversations) } + + it { is_expected.to have_one(:agent_bot_inbox) } + it { is_expected.to have_many(:webhooks).dependent(:destroy) } end