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:
Pranav Raj S 2020-03-06 01:43:12 +05:30 committed by GitHub
parent fabc6c87af
commit b2d5cc7b05
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 218 additions and 6 deletions

View file

@ -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]

View file

@ -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

View file

@ -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

View file

@ -0,0 +1,3 @@
class AgentBotJob < WebhookJob
queue_as :bots
end

View 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

View file

@ -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
View 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

View 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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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:

View 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

View 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

View file

@ -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'

View file

@ -0,0 +1,7 @@
FactoryBot.define do
factory :agent_bot_inbox do
inbox
agent_bot
status { 'active' }
end
end

View file

@ -0,0 +1,7 @@
FactoryBot.define do
factory :agent_bot do
name { 'MyString' }
description { 'MyString' }
outgoing_url { 'MyString' }
end
end

View 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

View 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

View 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

View 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

View file

@ -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

View file

@ -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