Feature: Introduce bots (#545)
Co-authored-by: Pranav Raj S <pranavrajs@gmail.com> Co-authored-by: Sojan Jose <sojan@pepalo.com>
This commit is contained in:
parent
fabc6c87af
commit
b2d5cc7b05
23 changed files with 218 additions and 6 deletions
|
@ -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]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
3
app/jobs/agent_bot_job.rb
Normal file
3
app/jobs/agent_bot_job.rb
Normal file
|
@ -0,0 +1,3 @@
|
|||
class AgentBotJob < WebhookJob
|
||||
queue_as :bots
|
||||
end
|
13
app/listeners/agent_bot_listener.rb
Normal file
13
app/listeners/agent_bot_listener.rb
Normal file
|
@ -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
|
|
@ -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?
|
||||
|
||||
|
|
19
app/models/agent_bot.rb
Normal file
19
app/models/agent_bot.rb
Normal file
|
@ -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
|
20
app/models/agent_bot_inbox.rb
Normal file
20
app/models/agent_bot_inbox.rb
Normal file
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
- [low, 1]
|
||||
- [mailers, 2]
|
||||
- [webhooks, 1]
|
||||
- [bots, 1]
|
||||
|
||||
# you can override concurrency based on environment
|
||||
production:
|
||||
|
|
12
db/migrate/20200222143100_create_agent_bots.rb
Normal file
12
db/migrate/20200222143100_create_agent_bots.rb
Normal file
|
@ -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
|
11
db/migrate/20200222143259_create_agent_bot_inboxes.rb
Normal file
11
db/migrate/20200222143259_create_agent_bot_inboxes.rb
Normal file
|
@ -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
|
17
db/schema.rb
17
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'
|
||||
|
|
7
spec/factories/agent_bot_inboxes.rb
Normal file
7
spec/factories/agent_bot_inboxes.rb
Normal file
|
@ -0,0 +1,7 @@
|
|||
FactoryBot.define do
|
||||
factory :agent_bot_inbox do
|
||||
inbox
|
||||
agent_bot
|
||||
status { 'active' }
|
||||
end
|
||||
end
|
7
spec/factories/agent_bots.rb
Normal file
7
spec/factories/agent_bots.rb
Normal file
|
@ -0,0 +1,7 @@
|
|||
FactoryBot.define do
|
||||
factory :agent_bot do
|
||||
name { 'MyString' }
|
||||
description { 'MyString' }
|
||||
outgoing_url { 'MyString' }
|
||||
end
|
||||
end
|
14
spec/jobs/agent_bot_job_spec.rb
Normal file
14
spec/jobs/agent_bot_job_spec.rb
Normal file
|
@ -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
|
33
spec/listeners/agent_bot_listener_spec.rb
Normal file
33
spec/listeners/agent_bot_listener_spec.rb
Normal file
|
@ -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
|
13
spec/models/agent_bot_inbox_spec.rb
Normal file
13
spec/models/agent_bot_inbox_spec.rb
Normal file
|
@ -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
|
8
spec/models/agent_bot_spec.rb
Normal file
8
spec/models/agent_bot_spec.rb
Normal file
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
Loading…
Reference in a new issue