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)
|
def initialize(user, conversation, params)
|
||||||
@content = params[:message]
|
@content = params[:message]
|
||||||
@private = ['1', 'true', 1, true].include? params[:private]
|
@private = params[:private] || false
|
||||||
@conversation = conversation
|
@conversation = conversation
|
||||||
@user = user
|
@user = user
|
||||||
@fb_id = params[:fb_id]
|
@fb_id = params[:fb_id]
|
||||||
|
|
|
@ -5,6 +5,7 @@ class Api::V1::WebhooksController < ApplicationController
|
||||||
|
|
||||||
before_action :login_from_basic_auth, only: [:chargebee]
|
before_action :login_from_basic_auth, only: [:chargebee]
|
||||||
before_action :check_billing_enabled, only: [:chargebee]
|
before_action :check_billing_enabled, only: [:chargebee]
|
||||||
|
|
||||||
def chargebee
|
def chargebee
|
||||||
chargebee_consumer.consume
|
chargebee_consumer.consume
|
||||||
head :ok
|
head :ok
|
||||||
|
|
|
@ -5,7 +5,7 @@ class AsyncDispatcher < BaseDispatcher
|
||||||
end
|
end
|
||||||
|
|
||||||
def listeners
|
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 << SubscriptionListener.instance if ENV['BILLING_ENABLED']
|
||||||
listeners
|
listeners
|
||||||
end
|
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
|
class EmailNotificationListener < BaseListener
|
||||||
def conversation_created(event)
|
def conversation_created(event)
|
||||||
conversation, _account, _timestamp = extract_conversation_and_account(event)
|
conversation, _account, _timestamp = extract_conversation_and_account(event)
|
||||||
|
return if conversation.bot?
|
||||||
|
|
||||||
conversation.inbox.members.each do |agent|
|
conversation.inbox.members.each do |agent|
|
||||||
next unless agent.notification_settings.find_by(account_id: conversation.account_id).conversation_creation?
|
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 :account_id, presence: true
|
||||||
validates :inbox_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 :latest, -> { order(created_at: :desc) }
|
||||||
scope :unassigned, -> { where(assignee_id: nil) }
|
scope :unassigned, -> { where(assignee_id: nil) }
|
||||||
|
@ -50,6 +50,8 @@ class Conversation < ApplicationRecord
|
||||||
|
|
||||||
before_create :set_display_id, unless: :display_id?
|
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_update :notify_status_change, :create_activity, :send_email_notification_to_assignee
|
||||||
|
|
||||||
after_create :send_events, :run_round_robin
|
after_create :send_events, :run_round_robin
|
||||||
|
@ -102,6 +104,10 @@ class Conversation < ApplicationRecord
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def set_bot_conversation
|
||||||
|
self.status = :bot if inbox.agent_bot_inbox&.active?
|
||||||
|
end
|
||||||
|
|
||||||
def dispatch_events
|
def dispatch_events
|
||||||
dispatcher_dispatch(CONVERSATION_RESOLVED)
|
dispatcher_dispatch(CONVERSATION_RESOLVED)
|
||||||
end
|
end
|
||||||
|
@ -110,11 +116,18 @@ class Conversation < ApplicationRecord
|
||||||
dispatcher_dispatch(CONVERSATION_CREATED)
|
dispatcher_dispatch(CONVERSATION_CREATED)
|
||||||
end
|
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
|
def send_email_notification_to_assignee
|
||||||
return if self_assign?(assignee_id)
|
return unless notifiable_assignee_change?
|
||||||
return unless saved_change_to_assignee_id?
|
|
||||||
return if assignee_id.blank?
|
|
||||||
return if assignee.notification_settings.find_by(account_id: account_id).not_conversation_assignment?
|
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
|
AgentNotifications::ConversationNotificationsMailer.conversation_assigned(self, assignee).deliver_later
|
||||||
end
|
end
|
||||||
|
@ -161,6 +174,7 @@ class Conversation < ApplicationRecord
|
||||||
def run_round_robin
|
def run_round_robin
|
||||||
return unless inbox.enable_auto_assignment
|
return unless inbox.enable_auto_assignment
|
||||||
return if assignee
|
return if assignee
|
||||||
|
return if bot?
|
||||||
|
|
||||||
inbox.next_available_agent.then { |new_assignee| update_assignee(new_assignee) }
|
inbox.next_available_agent.then { |new_assignee| update_assignee(new_assignee) }
|
||||||
end
|
end
|
||||||
|
|
|
@ -33,7 +33,10 @@ class Inbox < ApplicationRecord
|
||||||
has_many :members, through: :inbox_members, source: :user
|
has_many :members, through: :inbox_members, source: :user
|
||||||
has_many :conversations, dependent: :destroy
|
has_many :conversations, dependent: :destroy
|
||||||
has_many :messages, through: :conversations
|
has_many :messages, through: :conversations
|
||||||
|
|
||||||
|
has_one :agent_bot_inbox, dependent: :destroy
|
||||||
has_many :webhooks, dependent: :destroy
|
has_many :webhooks, dependent: :destroy
|
||||||
|
|
||||||
after_create :subscribe_webhook, if: :facebook?
|
after_create :subscribe_webhook, if: :facebook?
|
||||||
after_destroy :delete_round_robin_agents
|
after_destroy :delete_round_robin_agents
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,8 @@ class MessageTemplates::HookExecutionService
|
||||||
pattr_initialize [:message!]
|
pattr_initialize [:message!]
|
||||||
|
|
||||||
def perform
|
def perform
|
||||||
|
return if inbox.agent_bot_inbox&.active?
|
||||||
|
|
||||||
::MessageTemplates::Template::EmailCollect.new(conversation: conversation).perform if should_send_email_collect?
|
::MessageTemplates::Template::EmailCollect.new(conversation: conversation).perform if should_send_email_collect?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
- [low, 1]
|
- [low, 1]
|
||||||
- [mailers, 2]
|
- [mailers, 2]
|
||||||
- [webhooks, 1]
|
- [webhooks, 1]
|
||||||
|
- [bots, 1]
|
||||||
|
|
||||||
# you can override concurrency based on environment
|
# you can override concurrency based on environment
|
||||||
production:
|
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
|
t.index ['key'], name: 'index_active_storage_blobs_on_key', unique: true
|
||||||
end
|
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|
|
create_table 'attachments', id: :serial, force: :cascade do |t|
|
||||||
t.integer 'file_type', default: 0
|
t.integer 'file_type', default: 0
|
||||||
t.string 'external_url'
|
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)
|
expect(lock_event_data).to eq(id: 505, locked: false)
|
||||||
end
|
end
|
||||||
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
|
end
|
||||||
|
|
|
@ -23,6 +23,9 @@ RSpec.describe Inbox do
|
||||||
it { is_expected.to have_many(:conversations).dependent(:destroy) }
|
it { is_expected.to have_many(:conversations).dependent(:destroy) }
|
||||||
|
|
||||||
it { is_expected.to have_many(:messages).through(:conversations) }
|
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) }
|
it { is_expected.to have_many(:webhooks).dependent(:destroy) }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue