mirror of
				https://github.com/lingble/chatwoot.git
				synced 2025-10-30 18:47:51 +00:00 
			
		
		
		
	feat: Add native support for CSML in agent_bot API (#4913)
This commit is contained in:
		| @@ -30,6 +30,6 @@ class Api::V1::Accounts::AgentBotsController < Api::V1::Accounts::BaseController | |||||||
|   end |   end | ||||||
|  |  | ||||||
|   def permitted_params |   def permitted_params | ||||||
|     params.permit(:name, :description, :outgoing_url) |     params.permit(:name, :description, :outgoing_url, :bot_type, bot_config: [:csml_content]) | ||||||
|   end |   end | ||||||
| end | end | ||||||
|   | |||||||
| @@ -30,7 +30,7 @@ | |||||||
|             <image-bubble |             <image-bubble | ||||||
|               v-if="attachment.file_type === 'image' && !hasImageError" |               v-if="attachment.file_type === 'image' && !hasImageError" | ||||||
|               :url="attachment.data_url" |               :url="attachment.data_url" | ||||||
|               :thumb="attachment.thumb_url" |               :thumb="attachment.data_url" | ||||||
|               :readable-time="readableTime" |               :readable-time="readableTime" | ||||||
|               @error="onImageLoadError" |               @error="onImageLoadError" | ||||||
|             /> |             /> | ||||||
|   | |||||||
| @@ -20,7 +20,7 @@ | |||||||
|             <image-bubble |             <image-bubble | ||||||
|               v-if="attachment.file_type === 'image' && !hasImageError" |               v-if="attachment.file_type === 'image' && !hasImageError" | ||||||
|               :url="attachment.data_url" |               :url="attachment.data_url" | ||||||
|               :thumb="attachment.thumb_url" |               :thumb="attachment.data_url" | ||||||
|               :readable-time="readableTime" |               :readable-time="readableTime" | ||||||
|               @error="onImageLoadError" |               @error="onImageLoadError" | ||||||
|             /> |             /> | ||||||
|   | |||||||
| @@ -1,3 +0,0 @@ | |||||||
| class AgentBotJob < WebhookJob |  | ||||||
|   queue_as :bots |  | ||||||
| end |  | ||||||
							
								
								
									
										10
									
								
								app/jobs/agent_bots/csml_job.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								app/jobs/agent_bots/csml_job.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | |||||||
|  | class AgentBots::CsmlJob < ApplicationJob | ||||||
|  |   queue_as :bots | ||||||
|  |  | ||||||
|  |   def perform(event, agent_bot, message) | ||||||
|  |     event_data = { message: message } | ||||||
|  |     Integrations::Csml::ProcessorService.new( | ||||||
|  |       event_name: event, agent_bot: agent_bot, event_data: event_data | ||||||
|  |     ).perform | ||||||
|  |   end | ||||||
|  | end | ||||||
							
								
								
									
										3
									
								
								app/jobs/agent_bots/webhook_job.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								app/jobs/agent_bots/webhook_job.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | |||||||
|  | class AgentBots::WebhookJob < WebhookJob | ||||||
|  |   queue_as :bots | ||||||
|  | end | ||||||
| @@ -2,61 +2,80 @@ class AgentBotListener < BaseListener | |||||||
|   def conversation_resolved(event) |   def conversation_resolved(event) | ||||||
|     conversation = extract_conversation_and_account(event)[0] |     conversation = extract_conversation_and_account(event)[0] | ||||||
|     inbox = conversation.inbox |     inbox = conversation.inbox | ||||||
|     return if inbox.agent_bot_inbox.blank? |     return unless connected_agent_bot_exist?(inbox) | ||||||
|     return unless inbox.agent_bot_inbox.active? |  | ||||||
|  |  | ||||||
|     agent_bot = inbox.agent_bot_inbox.agent_bot |     event_name = __method__.to_s | ||||||
|  |     payload = conversation.webhook_data.merge(event: event_name) | ||||||
|     payload = conversation.webhook_data.merge(event: __method__.to_s) |     process_webhook_bot_event(inbox.agent_bot, payload) | ||||||
|     AgentBotJob.perform_later(agent_bot.outgoing_url, payload) |  | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   def conversation_opened(event) |   def conversation_opened(event) | ||||||
|     conversation = extract_conversation_and_account(event)[0] |     conversation = extract_conversation_and_account(event)[0] | ||||||
|     inbox = conversation.inbox |     inbox = conversation.inbox | ||||||
|     return if inbox.agent_bot_inbox.blank? |     return unless connected_agent_bot_exist?(inbox) | ||||||
|     return unless inbox.agent_bot_inbox.active? |  | ||||||
|  |  | ||||||
|     agent_bot = inbox.agent_bot_inbox.agent_bot |     event_name = __method__.to_s | ||||||
|  |     payload = conversation.webhook_data.merge(event: event_name) | ||||||
|     payload = conversation.webhook_data.merge(event: __method__.to_s) |     process_webhook_bot_event(inbox.agent_bot, payload) | ||||||
|     AgentBotJob.perform_later(agent_bot.outgoing_url, payload) |  | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   def message_created(event) |   def message_created(event) | ||||||
|     message = extract_message_and_account(event)[0] |     message = extract_message_and_account(event)[0] | ||||||
|     inbox = message.inbox |     inbox = message.inbox | ||||||
|     return unless message.webhook_sendable? && inbox.agent_bot_inbox.present? |     return unless connected_agent_bot_exist?(inbox) | ||||||
|     return unless inbox.agent_bot_inbox.active? |     return unless message.webhook_sendable? | ||||||
|  |  | ||||||
|     agent_bot = inbox.agent_bot_inbox.agent_bot |     method_name = __method__.to_s | ||||||
|  |     process_message_event(method_name, inbox.agent_bot, message, event) | ||||||
|     payload = message.webhook_data.merge(event: __method__.to_s) |  | ||||||
|     AgentBotJob.perform_later(agent_bot.outgoing_url, payload) |  | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   def message_updated(event) |   def message_updated(event) | ||||||
|     message = extract_message_and_account(event)[0] |     message = extract_message_and_account(event)[0] | ||||||
|     inbox = message.inbox |     inbox = message.inbox | ||||||
|     return unless message.webhook_sendable? && inbox.agent_bot_inbox.present? |     return unless connected_agent_bot_exist?(inbox) | ||||||
|     return unless inbox.agent_bot_inbox.active? |     return unless message.webhook_sendable? | ||||||
|  |  | ||||||
|     agent_bot = inbox.agent_bot_inbox.agent_bot |     method_name = __method__.to_s | ||||||
|  |     process_message_event(method_name, inbox.agent_bot, message, event) | ||||||
|     payload = message.webhook_data.merge(event: __method__.to_s) |  | ||||||
|     AgentBotJob.perform_later(agent_bot.outgoing_url, payload) |  | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   def webwidget_triggered(event) |   def webwidget_triggered(event) | ||||||
|     contact_inbox = event.data[:contact_inbox] |     contact_inbox = event.data[:contact_inbox] | ||||||
|     inbox = contact_inbox.inbox |     inbox = contact_inbox.inbox | ||||||
|  |     return unless connected_agent_bot_exist?(inbox) | ||||||
|  |  | ||||||
|  |     event_name = __method__.to_s | ||||||
|  |     payload = contact_inbox.webhook_data.merge(event: event_name) | ||||||
|  |     payload[:event_info] = event.data[:event_info] | ||||||
|  |     process_webhook_bot_event(inbox.agent_bot, payload) | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   private | ||||||
|  |  | ||||||
|  |   def connected_agent_bot_exist?(inbox) | ||||||
|     return if inbox.agent_bot_inbox.blank? |     return if inbox.agent_bot_inbox.blank? | ||||||
|     return unless inbox.agent_bot_inbox.active? |     return unless inbox.agent_bot_inbox.active? | ||||||
|  |  | ||||||
|     agent_bot = inbox.agent_bot_inbox.agent_bot |     true | ||||||
|  |   end | ||||||
|  |  | ||||||
|     payload = contact_inbox.webhook_data.merge(event: __method__.to_s) |   def process_message_event(method_name, agent_bot, message, event) | ||||||
|     payload[:event_info] = event.data[:event_info] |     case agent_bot.bot_type | ||||||
|     AgentBotJob.perform_later(agent_bot.outgoing_url, payload) |     when 'webhook' | ||||||
|  |       payload = message.webhook_data.merge(event: method_name) | ||||||
|  |       process_webhook_bot_event(agent_bot, payload) | ||||||
|  |     when 'csml' | ||||||
|  |       process_csml_bot_event(event.name, agent_bot, message) | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def process_webhook_bot_event(agent_bot, payload) | ||||||
|  |     return if agent_bot.outgoing_url.blank? | ||||||
|  |  | ||||||
|  |     AgentBots::WebhookJob.perform_later(agent_bot.outgoing_url, payload) | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def process_csml_bot_event(event, agent_bot, message) | ||||||
|  |     AgentBots::CsmlJob.perform_later(event, agent_bot, message) | ||||||
|   end |   end | ||||||
| end | end | ||||||
|   | |||||||
| @@ -3,6 +3,8 @@ | |||||||
| # Table name: agent_bots | # Table name: agent_bots | ||||||
| # | # | ||||||
| #  id           :bigint           not null, primary key | #  id           :bigint           not null, primary key | ||||||
|  | #  bot_config   :jsonb | ||||||
|  | #  bot_type     :integer          default(0) | ||||||
| #  description  :string | #  description  :string | ||||||
| #  name         :string | #  name         :string | ||||||
| #  outgoing_url :string | #  outgoing_url :string | ||||||
| @@ -27,6 +29,9 @@ class AgentBot < ApplicationRecord | |||||||
|   has_many :inboxes, through: :agent_bot_inboxes |   has_many :inboxes, through: :agent_bot_inboxes | ||||||
|   has_many :messages, as: :sender, dependent: :restrict_with_exception |   has_many :messages, as: :sender, dependent: :restrict_with_exception | ||||||
|   belongs_to :account, optional: true |   belongs_to :account, optional: true | ||||||
|  |   enum bot_type: { webhook: 0, csml: 1 } | ||||||
|  |  | ||||||
|  |   validate :validate_agent_bot_config | ||||||
|  |  | ||||||
|   def available_name |   def available_name | ||||||
|     name |     name | ||||||
| @@ -48,4 +53,10 @@ class AgentBot < ApplicationRecord | |||||||
|       type: 'agent_bot' |       type: 'agent_bot' | ||||||
|     } |     } | ||||||
|   end |   end | ||||||
|  |  | ||||||
|  |   private | ||||||
|  |  | ||||||
|  |   def validate_agent_bot_config | ||||||
|  |     errors.add(:bot_config, 'Invalid Bot Configuration') unless AgentBots::ValidateBotService.new(agent_bot: self).perform | ||||||
|  |   end | ||||||
| end | end | ||||||
|   | |||||||
							
								
								
									
										38
									
								
								app/services/agent_bots/validate_bot_service.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								app/services/agent_bots/validate_bot_service.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,38 @@ | |||||||
|  | class AgentBots::ValidateBotService | ||||||
|  |   pattr_initialize [:agent_bot] | ||||||
|  |   def perform | ||||||
|  |     return true unless agent_bot.bot_type == 'csml' | ||||||
|  |  | ||||||
|  |     validate_csml_bot | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   private | ||||||
|  |  | ||||||
|  |   def csml_client | ||||||
|  |     @csml_client ||= CsmlEngine.new | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def csml_bot_payload | ||||||
|  |     { | ||||||
|  |       id: agent_bot[:name], | ||||||
|  |       name: agent_bot[:name], | ||||||
|  |       default_flow: 'Default', | ||||||
|  |       flows: [ | ||||||
|  |         { | ||||||
|  |           id: SecureRandom.uuid, | ||||||
|  |           name: 'Default', | ||||||
|  |           content: agent_bot.bot_config['csml_content'], | ||||||
|  |           commands: [] | ||||||
|  |         } | ||||||
|  |       ] | ||||||
|  |     } | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def validate_csml_bot | ||||||
|  |     response = csml_client.validate(csml_bot_payload) | ||||||
|  |     response.blank? || response['valid'] | ||||||
|  |   rescue StandardError => e | ||||||
|  |     ChatwootExceptionTracker.new(e, account: agent_bot).capture_exception | ||||||
|  |     false | ||||||
|  |   end | ||||||
|  | end | ||||||
| @@ -2,5 +2,7 @@ json.id resource.id | |||||||
| json.name resource.name | json.name resource.name | ||||||
| json.description resource.description | json.description resource.description | ||||||
| json.outgoing_url resource.outgoing_url | json.outgoing_url resource.outgoing_url | ||||||
|  | json.bot_type resource.bot_type | ||||||
|  | json.bot_config resource.bot_config | ||||||
| json.account_id resource.account_id | json.account_id resource.account_id | ||||||
| json.access_token resource.access_token if resource.access_token.present? | json.access_token resource.access_token if resource.access_token.present? | ||||||
|   | |||||||
| @@ -62,3 +62,9 @@ | |||||||
| - name: ENABLE_MESSENGER_CHANNEL_HUMAN_AGENT | - name: ENABLE_MESSENGER_CHANNEL_HUMAN_AGENT | ||||||
|   value: false |   value: false | ||||||
|   locked: false |   locked: false | ||||||
|  | - name: CSML_BOT_HOST | ||||||
|  |   value: | ||||||
|  |   locked: false | ||||||
|  | - name: CSML_BOT_API_KEY | ||||||
|  |   value: | ||||||
|  |   locked: false | ||||||
|   | |||||||
							
								
								
									
										15
									
								
								db/migrate/20220622090344_add_type_to_agent_bots.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								db/migrate/20220622090344_add_type_to_agent_bots.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | |||||||
|  | class AddTypeToAgentBots < ActiveRecord::Migration[6.1] | ||||||
|  |   def up | ||||||
|  |     change_table :agent_bots, bulk: true do |t| | ||||||
|  |       t.column :bot_type, :integer, default: 0 | ||||||
|  |       t.column :bot_config, :jsonb, default: {} | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def down | ||||||
|  |     change_table :agent_bots, bulk: true do |t| | ||||||
|  |       t.remove :bot_type | ||||||
|  |       t.remove :bot_config | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | end | ||||||
| @@ -10,7 +10,7 @@ | |||||||
| # | # | ||||||
| # It's strongly recommended that you check this file into your version control system. | # It's strongly recommended that you check this file into your version control system. | ||||||
|  |  | ||||||
| ActiveRecord::Schema.define(version: 2022_06_16_154502) do | ActiveRecord::Schema.define(version: 2022_06_22_090344) do | ||||||
|  |  | ||||||
|   # These are extensions that must be enabled in order to support this database |   # These are extensions that must be enabled in order to support this database | ||||||
|   enable_extension "pg_stat_statements" |   enable_extension "pg_stat_statements" | ||||||
| @@ -108,6 +108,8 @@ ActiveRecord::Schema.define(version: 2022_06_16_154502) do | |||||||
|     t.datetime "created_at", precision: 6, null: false |     t.datetime "created_at", precision: 6, null: false | ||||||
|     t.datetime "updated_at", precision: 6, null: false |     t.datetime "updated_at", precision: 6, null: false | ||||||
|     t.bigint "account_id" |     t.bigint "account_id" | ||||||
|  |     t.integer "bot_type", default: 0 | ||||||
|  |     t.jsonb "bot_config", default: {} | ||||||
|     t.index ["account_id"], name: "index_agent_bots_on_account_id" |     t.index ["account_id"], name: "index_agent_bots_on_account_id" | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										52
									
								
								lib/csml_engine.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								lib/csml_engine.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,52 @@ | |||||||
|  | class CsmlEngine | ||||||
|  |   API_KEY_HEADER = 'X-Api-Key'.freeze | ||||||
|  |  | ||||||
|  |   def initialize | ||||||
|  |     @host_url = GlobalConfigService.load('CSML_BOT_HOST', '') | ||||||
|  |     @api_key = GlobalConfigService.load('CSML_BOT_API_KEY', '') | ||||||
|  |  | ||||||
|  |     raise ArgumentError, 'Missing Credentials' if @host_url.blank? || @api_key.blank? | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def status | ||||||
|  |     response = HTTParty.get("#{@host_url}/status") | ||||||
|  |     process_response(response) | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def run(bot, params) | ||||||
|  |     payload = { | ||||||
|  |       bot: bot, | ||||||
|  |       event: { | ||||||
|  |         request_id: SecureRandom.uuid, | ||||||
|  |         client: params[:client], | ||||||
|  |         payload: params[:payload], | ||||||
|  |         metadata: params[:metadata], | ||||||
|  |         ttl_duration: 4000 | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     response = post('run', payload) | ||||||
|  |     process_response(response) | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def validate(bot) | ||||||
|  |     response = post('validate', bot) | ||||||
|  |     process_response(response) | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   private | ||||||
|  |  | ||||||
|  |   def process_response(response) | ||||||
|  |     return response.parsed_response if response.success? | ||||||
|  |  | ||||||
|  |     { error: response.parsed_response, status: response.code } | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def post(path, payload) | ||||||
|  |     HTTParty.post( | ||||||
|  |       "#{@host_url}/#{path}", { | ||||||
|  |         headers: { API_KEY_HEADER => @api_key, 'Content-Type' => 'application/json' }, | ||||||
|  |         body: payload.to_json | ||||||
|  |       } | ||||||
|  |     ) | ||||||
|  |   end | ||||||
|  | end | ||||||
							
								
								
									
										63
									
								
								lib/integrations/bot_processor_service.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								lib/integrations/bot_processor_service.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,63 @@ | |||||||
|  | class Integrations::BotProcessorService | ||||||
|  |   pattr_initialize [:event_name!, :hook!, :event_data!] | ||||||
|  |  | ||||||
|  |   def perform | ||||||
|  |     message = event_data[:message] | ||||||
|  |     return unless should_run_processor?(message) | ||||||
|  |  | ||||||
|  |     process_content(message) | ||||||
|  |   rescue StandardError => e | ||||||
|  |     ChatwootExceptionTracker.new(e, account: agent_bot).capture_exception | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   private | ||||||
|  |  | ||||||
|  |   def should_run_processor?(message) | ||||||
|  |     return if message.private? | ||||||
|  |     return unless processable_message?(message) | ||||||
|  |     return unless conversation.pending? | ||||||
|  |  | ||||||
|  |     true | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def conversation | ||||||
|  |     message = event_data[:message] | ||||||
|  |     @conversation ||= message.conversation | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def process_content(message) | ||||||
|  |     content = message_content(message) | ||||||
|  |     response = get_response(conversation.contact_inbox.source_id, content) if content.present? | ||||||
|  |     process_response(message, response) if response.present? | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def message_content(message) | ||||||
|  |     # TODO: might needs to change this to a way that we fetch the updated value from event data instead | ||||||
|  |     # cause the message.updated event could be that that the message was deleted | ||||||
|  |  | ||||||
|  |     return message.content_attributes['submitted_values']&.first&.dig('value') if event_name == 'message.updated' | ||||||
|  |  | ||||||
|  |     message.content | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def processable_message?(message) | ||||||
|  |     # TODO: change from reportable and create a dedicated method for this? | ||||||
|  |     return unless message.reportable? | ||||||
|  |     return if message.outgoing? && !processable_outgoing_message?(message) | ||||||
|  |  | ||||||
|  |     true | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def processable_outgoing_message?(message) | ||||||
|  |     event_name == 'message.updated' && ['input_select'].include?(message.content_type) | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def process_action(message, action) | ||||||
|  |     case action | ||||||
|  |     when 'handoff' | ||||||
|  |       message.conversation.open! | ||||||
|  |     when 'resolve' | ||||||
|  |       message.conversation.resolved! | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | end | ||||||
							
								
								
									
										142
									
								
								lib/integrations/csml/processor_service.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										142
									
								
								lib/integrations/csml/processor_service.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,142 @@ | |||||||
|  | class Integrations::Csml::ProcessorService < Integrations::BotProcessorService | ||||||
|  |   pattr_initialize [:event_name!, :event_data!, :agent_bot!] | ||||||
|  |  | ||||||
|  |   private | ||||||
|  |  | ||||||
|  |   def csml_client | ||||||
|  |     @csml_client ||= CsmlEngine.new | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def get_response(session_id, content) | ||||||
|  |     csml_client.run( | ||||||
|  |       bot_payload, | ||||||
|  |       { | ||||||
|  |         client: client_params(session_id), | ||||||
|  |         payload: message_payload(content), | ||||||
|  |         metadata: metadata_params | ||||||
|  |       } | ||||||
|  |     ) | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def client_params(session_id) | ||||||
|  |     { | ||||||
|  |       bot_id: "chatwoot-bot-#{conversation.inbox.id}", | ||||||
|  |       channel_id: "chatwoot-bot-inbox-#{conversation.inbox.id}", | ||||||
|  |       user_id: session_id | ||||||
|  |     } | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def message_payload(content) | ||||||
|  |     { | ||||||
|  |       content_type: 'text', | ||||||
|  |       content: { text: content } | ||||||
|  |     } | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def metadata_params | ||||||
|  |     { | ||||||
|  |       conversation: conversation, | ||||||
|  |       contact: conversation.contact | ||||||
|  |     } | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def bot_payload | ||||||
|  |     { | ||||||
|  |       id: "chatwoot-csml-bot-#{agent_bot.id}", | ||||||
|  |       name: "chatwoot-csml-bot-#{agent_bot.id}", | ||||||
|  |       default_flow: 'chatwoot_bot_flow', | ||||||
|  |       flows: [ | ||||||
|  |         { | ||||||
|  |           id: "chatwoot-csml-bot-flow-#{agent_bot.id}-inbox-#{conversation.inbox.id}", | ||||||
|  |           name: 'chatwoot_bot_flow', | ||||||
|  |           content: agent_bot.bot_config['csml_content'], | ||||||
|  |           commands: [] | ||||||
|  |         } | ||||||
|  |       ] | ||||||
|  |     } | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def process_response(message, response) | ||||||
|  |     csml_messages = response['messages'] | ||||||
|  |     has_conversation_ended = response['conversation_end'] | ||||||
|  |  | ||||||
|  |     process_action(message, 'handoff') if has_conversation_ended.present? | ||||||
|  |  | ||||||
|  |     return if csml_messages.blank? | ||||||
|  |  | ||||||
|  |     # We do not support wait, typing now. | ||||||
|  |     csml_messages.each do |csml_message| | ||||||
|  |       create_messages(csml_message, conversation) | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def create_messages(message, conversation) | ||||||
|  |     message_payload = message['payload'] | ||||||
|  |  | ||||||
|  |     case message_payload['content_type'] | ||||||
|  |     when 'text' | ||||||
|  |       process_text_messages(message_payload, conversation) | ||||||
|  |     when 'question' | ||||||
|  |       process_question_messages(message_payload, conversation) | ||||||
|  |     when 'image' | ||||||
|  |       process_image_messages(message_payload, conversation) | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def process_text_messages(message_payload, conversation) | ||||||
|  |     conversation.messages.create( | ||||||
|  |       { | ||||||
|  |         message_type: :outgoing, | ||||||
|  |         account_id: conversation.account_id, | ||||||
|  |         inbox_id: conversation.inbox_id, | ||||||
|  |         content: message_payload['content']['text'], | ||||||
|  |         sender: agent_bot | ||||||
|  |       } | ||||||
|  |     ) | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def process_question_messages(message_payload, conversation) | ||||||
|  |     buttons = message_payload['content']['buttons'].map do |button| | ||||||
|  |       { title: button['content']['title'], value: button['content']['payload'] } | ||||||
|  |     end | ||||||
|  |     conversation.messages.create( | ||||||
|  |       { | ||||||
|  |         message_type: :outgoing, | ||||||
|  |         account_id: conversation.account_id, | ||||||
|  |         inbox_id: conversation.inbox_id, | ||||||
|  |         content: message_payload['content']['title'], | ||||||
|  |         content_type: 'input_select', | ||||||
|  |         content_attributes: { items: buttons }, | ||||||
|  |         sender: agent_bot | ||||||
|  |       } | ||||||
|  |     ) | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def prepare_attachment(message_payload, message, account_id) | ||||||
|  |     attachment_params = { file_type: :image, account_id: account_id } | ||||||
|  |     attachment_url = message_payload['content']['url'] | ||||||
|  |     attachment = message.attachments.new(attachment_params) | ||||||
|  |     attachment_file = Down.download(attachment_url) | ||||||
|  |     attachment.file.attach( | ||||||
|  |       io: attachment_file, | ||||||
|  |       filename: attachment_file.original_filename, | ||||||
|  |       content_type: attachment_file.content_type | ||||||
|  |     ) | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def process_image_messages(message_payload, conversation) | ||||||
|  |     message = conversation.messages.new( | ||||||
|  |       { | ||||||
|  |         message_type: :outgoing, | ||||||
|  |         account_id: conversation.account_id, | ||||||
|  |         inbox_id: conversation.inbox_id, | ||||||
|  |         content: '', | ||||||
|  |         content_type: 'text', | ||||||
|  |         sender: agent_bot | ||||||
|  |       } | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     prepare_attachment(message_payload, message, conversation.account_id) | ||||||
|  |     message.save! | ||||||
|  |   end | ||||||
|  | end | ||||||
| @@ -1,17 +1,6 @@ | |||||||
| class Integrations::Dialogflow::ProcessorService | class Integrations::Dialogflow::ProcessorService < Integrations::BotProcessorService | ||||||
|   pattr_initialize [:event_name!, :hook!, :event_data!] |   pattr_initialize [:event_name!, :hook!, :event_data!] | ||||||
|  |  | ||||||
|   def perform |  | ||||||
|     message = event_data[:message] |  | ||||||
|     return if message.private? |  | ||||||
|     return unless processable_message?(message) |  | ||||||
|     return unless message.conversation.pending? |  | ||||||
|  |  | ||||||
|     content = message_content(message) |  | ||||||
|     response = get_dialogflow_response(message.conversation.contact_inbox.source_id, content) if content.present? |  | ||||||
|     process_response(message, response) if response.present? |  | ||||||
|   end |  | ||||||
|  |  | ||||||
|   private |   private | ||||||
|  |  | ||||||
|   def message_content(message) |   def message_content(message) | ||||||
| @@ -23,19 +12,7 @@ class Integrations::Dialogflow::ProcessorService | |||||||
|     message.content |     message.content | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   def processable_message?(message) |   def get_response(session_id, message) | ||||||
|     # TODO: change from reportable and create a dedicated method for this? |  | ||||||
|     return unless message.reportable? |  | ||||||
|     return if message.outgoing? && !processable_outgoing_message?(message) |  | ||||||
|  |  | ||||||
|     true |  | ||||||
|   end |  | ||||||
|  |  | ||||||
|   def processable_outgoing_message?(message) |  | ||||||
|     event_name == 'message.updated' && ['input_select'].include?(message.content_type) |  | ||||||
|   end |  | ||||||
|  |  | ||||||
|   def get_dialogflow_response(session_id, message) |  | ||||||
|     Google::Cloud::Dialogflow.configure { |config| config.credentials = hook.settings['credentials'] } |     Google::Cloud::Dialogflow.configure { |config| config.credentials = hook.settings['credentials'] } | ||||||
|     session_client = Google::Cloud::Dialogflow.sessions |     session_client = Google::Cloud::Dialogflow.sessions | ||||||
|     session = session_client.session_path project: hook.settings['project_id'], session: session_id |     session = session_client.session_path project: hook.settings['project_id'], session: session_id | ||||||
| @@ -72,13 +49,4 @@ class Integrations::Dialogflow::ProcessorService | |||||||
|                                                         inbox_id: conversation.inbox_id |                                                         inbox_id: conversation.inbox_id | ||||||
|                                                       })) |                                                       })) | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   def process_action(message, action) |  | ||||||
|     case action |  | ||||||
|     when 'handoff' |  | ||||||
|       message.conversation.open! |  | ||||||
|     when 'resolve' |  | ||||||
|       message.conversation.resolved! |  | ||||||
|     end |  | ||||||
|   end |  | ||||||
| end | end | ||||||
|   | |||||||
| @@ -3,5 +3,11 @@ FactoryBot.define do | |||||||
|     name { 'MyString' } |     name { 'MyString' } | ||||||
|     description { 'MyString' } |     description { 'MyString' } | ||||||
|     outgoing_url { 'MyString' } |     outgoing_url { 'MyString' } | ||||||
|  |     bot_config { {} } | ||||||
|  |     bot_type { 'webhook' } | ||||||
|  |  | ||||||
|  |     trait :skip_validate do | ||||||
|  |       to_create { |instance| instance.save(validate: false) } | ||||||
|  |     end | ||||||
|   end |   end | ||||||
| end | end | ||||||
|   | |||||||
							
								
								
									
										19
									
								
								spec/jobs/agent_bots/csml_job_spec.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								spec/jobs/agent_bots/csml_job_spec.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | |||||||
|  | require 'rails_helper' | ||||||
|  |  | ||||||
|  | RSpec.describe AgentBots::CsmlJob, type: :job do | ||||||
|  |   it 'runs csml processor service' do | ||||||
|  |     event = 'message.created' | ||||||
|  |     message = create(:message) | ||||||
|  |     agent_bot = create(:agent_bot) | ||||||
|  |     processor = double | ||||||
|  |  | ||||||
|  |     allow(Integrations::Csml::ProcessorService).to receive(:new).and_return(processor) | ||||||
|  |     allow(processor).to receive(:perform) | ||||||
|  |  | ||||||
|  |     described_class.perform_now(event, agent_bot, message) | ||||||
|  |  | ||||||
|  |     expect(Integrations::Csml::ProcessorService) | ||||||
|  |       .to have_received(:new) | ||||||
|  |       .with(event_name: event, agent_bot: agent_bot, event_data: { message: message }) | ||||||
|  |   end | ||||||
|  | end | ||||||
| @@ -1,6 +1,8 @@ | |||||||
| require 'rails_helper' | require 'rails_helper' | ||||||
| 
 | 
 | ||||||
| RSpec.describe AgentBotJob, type: :job do | RSpec.describe AgentBots::WebhookJob, type: :job do | ||||||
|  |   include ActiveJob::TestHelper | ||||||
|  | 
 | ||||||
|   subject(:job) { described_class.perform_later(url, payload) } |   subject(:job) { described_class.perform_later(url, payload) } | ||||||
| 
 | 
 | ||||||
|   let(:url) { 'https://test.com' } |   let(:url) { 'https://test.com' } | ||||||
| @@ -11,4 +13,9 @@ RSpec.describe AgentBotJob, type: :job do | |||||||
|       .with(url, payload) |       .with(url, payload) | ||||||
|       .on_queue('bots') |       .on_queue('bots') | ||||||
|   end |   end | ||||||
|  | 
 | ||||||
|  |   it 'executes perform' do | ||||||
|  |     expect(Webhooks::Trigger).to receive(:execute).with(url, payload) | ||||||
|  |     perform_enqueued_jobs { job } | ||||||
|  |   end | ||||||
| end | end | ||||||
| @@ -1,9 +1,11 @@ | |||||||
| require 'rails_helper' | require 'rails_helper' | ||||||
|  |  | ||||||
| RSpec.describe WebhookJob, type: :job do | RSpec.describe WebhookJob, type: :job do | ||||||
|  |   include ActiveJob::TestHelper | ||||||
|  |  | ||||||
|   subject(:job) { described_class.perform_later(url, payload) } |   subject(:job) { described_class.perform_later(url, payload) } | ||||||
|  |  | ||||||
|   let(:url) { 'https://test.com' } |   let(:url) { 'https://test.chatwoot.com' } | ||||||
|   let(:payload) { { name: 'test' } } |   let(:payload) { { name: 'test' } } | ||||||
|  |  | ||||||
|   it 'queues the job' do |   it 'queues the job' do | ||||||
| @@ -11,4 +13,9 @@ RSpec.describe WebhookJob, type: :job do | |||||||
|       .with(url, payload) |       .with(url, payload) | ||||||
|       .on_queue('webhooks') |       .on_queue('webhooks') | ||||||
|   end |   end | ||||||
|  |  | ||||||
|  |   it 'executes perform' do | ||||||
|  |     expect(Webhooks::Trigger).to receive(:execute).with(url, payload) | ||||||
|  |     perform_enqueued_jobs { job } | ||||||
|  |   end | ||||||
| end | end | ||||||
|   | |||||||
							
								
								
									
										99
									
								
								spec/lib/csml_engine_spec.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										99
									
								
								spec/lib/csml_engine_spec.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,99 @@ | |||||||
|  | require 'rails_helper' | ||||||
|  |  | ||||||
|  | describe CsmlEngine do | ||||||
|  |   it 'raises an exception if host and api is absent' do | ||||||
|  |     expect { described_class.new }.to raise_error(StandardError) | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   context 'when CSML_BOT_HOST & CSML_BOT_API_KEY is present' do | ||||||
|  |     before do | ||||||
|  |       create(:installation_config, { name: 'CSML_BOT_HOST', value: 'https://csml.chatwoot.dev' }) | ||||||
|  |       create(:installation_config, { name: 'CSML_BOT_API_KEY', value: 'random_api_key' }) | ||||||
|  |     end | ||||||
|  |  | ||||||
|  |     let(:csml_request) { double } | ||||||
|  |  | ||||||
|  |     context 'when status is called' do | ||||||
|  |       it 'returns api response if client response is valid' do | ||||||
|  |         allow(HTTParty).to receive(:get).and_return(csml_request) | ||||||
|  |         allow(csml_request).to receive(:success?).and_return(true) | ||||||
|  |         allow(csml_request).to receive(:parsed_response).and_return({ 'engine_version': '1.11.1' }) | ||||||
|  |  | ||||||
|  |         response = described_class.new.status | ||||||
|  |  | ||||||
|  |         expect(HTTParty).to have_received(:get).with('https://csml.chatwoot.dev/status') | ||||||
|  |         expect(csml_request).to have_received(:success?) | ||||||
|  |         expect(csml_request).to have_received(:parsed_response) | ||||||
|  |         expect(response).to eq({ 'engine_version': '1.11.1' }) | ||||||
|  |       end | ||||||
|  |  | ||||||
|  |       it 'returns error if client response is invalid' do | ||||||
|  |         allow(HTTParty).to receive(:get).and_return(csml_request) | ||||||
|  |         allow(csml_request).to receive(:success?).and_return(false) | ||||||
|  |         allow(csml_request).to receive(:code).and_return(401) | ||||||
|  |         allow(csml_request).to receive(:parsed_response).and_return({ 'error': true }) | ||||||
|  |  | ||||||
|  |         response = described_class.new.status | ||||||
|  |  | ||||||
|  |         expect(HTTParty).to have_received(:get).with('https://csml.chatwoot.dev/status') | ||||||
|  |         expect(csml_request).to have_received(:success?) | ||||||
|  |         expect(response).to eq({ error: { 'error': true }, status: 401 }) | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |  | ||||||
|  |     context 'when run is called' do | ||||||
|  |       it 'returns api response if client response is valid' do | ||||||
|  |         allow(HTTParty).to receive(:post).and_return(csml_request) | ||||||
|  |         allow(SecureRandom).to receive(:uuid).and_return('xxxx-yyyy-wwww-cccc') | ||||||
|  |         allow(csml_request).to receive(:success?).and_return(true) | ||||||
|  |         allow(csml_request).to receive(:parsed_response).and_return({ 'success': true }) | ||||||
|  |  | ||||||
|  |         response = described_class.new.run({ flow: 'default' }, { client: 'client', payload: { id: 1 }, metadata: {} }) | ||||||
|  |  | ||||||
|  |         payload = { | ||||||
|  |           bot: { flow: 'default' }, | ||||||
|  |           event: { | ||||||
|  |             request_id: 'xxxx-yyyy-wwww-cccc', | ||||||
|  |             client: 'client', | ||||||
|  |             payload: { id: 1 }, | ||||||
|  |             metadata: {}, | ||||||
|  |             ttl_duration: 4000 | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |         expect(HTTParty).to have_received(:post) | ||||||
|  |           .with( | ||||||
|  |             'https://csml.chatwoot.dev/run', { | ||||||
|  |               body: payload.to_json, | ||||||
|  |               headers: { 'X-Api-Key' => 'random_api_key', 'Content-Type' => 'application/json' } | ||||||
|  |             } | ||||||
|  |           ) | ||||||
|  |         expect(csml_request).to have_received(:success?) | ||||||
|  |         expect(csml_request).to have_received(:parsed_response) | ||||||
|  |         expect(response).to eq({ 'success': true }) | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |  | ||||||
|  |     context 'when validate is called' do | ||||||
|  |       it 'returns api response if client response is valid' do | ||||||
|  |         allow(HTTParty).to receive(:post).and_return(csml_request) | ||||||
|  |         allow(SecureRandom).to receive(:uuid).and_return('xxxx-yyyy-wwww-cccc') | ||||||
|  |         allow(csml_request).to receive(:success?).and_return(true) | ||||||
|  |         allow(csml_request).to receive(:parsed_response).and_return({ 'success': true }) | ||||||
|  |  | ||||||
|  |         payload = { flow: 'default' } | ||||||
|  |         response = described_class.new.validate(payload) | ||||||
|  |  | ||||||
|  |         expect(HTTParty).to have_received(:post) | ||||||
|  |           .with( | ||||||
|  |             'https://csml.chatwoot.dev/validate', { | ||||||
|  |               body: payload.to_json, | ||||||
|  |               headers: { 'X-Api-Key' => 'random_api_key', 'Content-Type' => 'application/json' } | ||||||
|  |             } | ||||||
|  |           ) | ||||||
|  |         expect(csml_request).to have_received(:success?) | ||||||
|  |         expect(csml_request).to have_received(:parsed_response) | ||||||
|  |         expect(response).to eq({ 'success': true }) | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | end | ||||||
							
								
								
									
										108
									
								
								spec/lib/integrations/csml/processor_service_spec.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										108
									
								
								spec/lib/integrations/csml/processor_service_spec.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,108 @@ | |||||||
|  | require 'rails_helper' | ||||||
|  |  | ||||||
|  | describe Integrations::Csml::ProcessorService do | ||||||
|  |   let(:account) { create(:account) } | ||||||
|  |   let(:inbox) { create(:inbox, account: account) } | ||||||
|  |   let(:agent_bot) { create(:agent_bot, :skip_validate, bot_type: 'csml', account: account) } | ||||||
|  |   let(:agent_bot_inbox) { create(:agent_bot_inbox, agent_bot: agent_bot, inbox: inbox, account: account) } | ||||||
|  |   let(:conversation) { create(:conversation, account: account, status: :pending) } | ||||||
|  |   let(:message) { create(:message, account: account, conversation: conversation) } | ||||||
|  |   let(:event_name) { 'message.created' } | ||||||
|  |   let(:event_data) { { message: message } } | ||||||
|  |  | ||||||
|  |   describe '#perform' do | ||||||
|  |     let(:csml_client) { double } | ||||||
|  |     let(:processor) { described_class.new(event_name: event_name, agent_bot: agent_bot, event_data: event_data) } | ||||||
|  |  | ||||||
|  |     before do | ||||||
|  |       allow(CsmlEngine).to receive(:new).and_return(csml_client) | ||||||
|  |     end | ||||||
|  |  | ||||||
|  |     context 'when a conversation is completed from CSML' do | ||||||
|  |       it 'open the conversation and handsoff it to an agent' do | ||||||
|  |         csml_response = ActiveSupport::HashWithIndifferentAccess.new(conversation_end: true) | ||||||
|  |         allow(csml_client).to receive(:run).and_return(csml_response) | ||||||
|  |  | ||||||
|  |         processor.perform | ||||||
|  |         expect(conversation.reload.status).to eql('open') | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |  | ||||||
|  |     context 'when a new message is returned from CSML' do | ||||||
|  |       it 'creates a text message' do | ||||||
|  |         csml_response = ActiveSupport::HashWithIndifferentAccess.new( | ||||||
|  |           messages: [ | ||||||
|  |             { payload: { content_type: 'text', content: { text: 'hello payload' } } } | ||||||
|  |           ] | ||||||
|  |         ) | ||||||
|  |         allow(csml_client).to receive(:run).and_return(csml_response) | ||||||
|  |         processor.perform | ||||||
|  |         expect(conversation.messages.last.content).to eql('hello payload') | ||||||
|  |       end | ||||||
|  |  | ||||||
|  |       it 'creates a question message' do | ||||||
|  |         csml_response = ActiveSupport::HashWithIndifferentAccess.new( | ||||||
|  |           messages: [{ | ||||||
|  |             payload: { | ||||||
|  |               content_type: 'question', | ||||||
|  |               content: { title: 'Question Payload', buttons: [{ content: { title: 'Q1', payload: 'q1' } }] } | ||||||
|  |             } | ||||||
|  |           }] | ||||||
|  |         ) | ||||||
|  |         allow(csml_client).to receive(:run).and_return(csml_response) | ||||||
|  |         processor.perform | ||||||
|  |         expect(conversation.messages.last.content).to eql('Question Payload') | ||||||
|  |         expect(conversation.messages.last.content_type).to eql('input_select') | ||||||
|  |         expect(conversation.messages.last.content_attributes).to eql({ items: [{ title: 'Q1', value: 'q1' }] }.with_indifferent_access) | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |  | ||||||
|  |     context 'when conversation status is not pending' do | ||||||
|  |       let(:conversation) { create(:conversation, account: account, status: :open) } | ||||||
|  |  | ||||||
|  |       it 'returns nil' do | ||||||
|  |         expect(processor.perform).to be(nil) | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |  | ||||||
|  |     context 'when message is private' do | ||||||
|  |       let(:message) { create(:message, account: account, conversation: conversation, private: true) } | ||||||
|  |  | ||||||
|  |       it 'returns nil' do | ||||||
|  |         expect(processor.perform).to be(nil) | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |  | ||||||
|  |     context 'when message type is template (not outgoing or incoming)' do | ||||||
|  |       let(:message) { create(:message, account: account, conversation: conversation, message_type: :template) } | ||||||
|  |  | ||||||
|  |       it 'returns nil' do | ||||||
|  |         expect(processor.perform).to be(nil) | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |  | ||||||
|  |     context 'when message updated' do | ||||||
|  |       let(:event_name) { 'message.updated' } | ||||||
|  |  | ||||||
|  |       context 'when content_type is input_select' do | ||||||
|  |         let(:message) do | ||||||
|  |           create(:message, account: account, conversation: conversation, private: true, | ||||||
|  |                            submitted_values: [{ 'title' => 'Support', 'value' => 'selected_gas' }]) | ||||||
|  |         end | ||||||
|  |  | ||||||
|  |         it 'returns submitted value for message content' do | ||||||
|  |           expect(processor.send(:message_content, message)).to eql('selected_gas') | ||||||
|  |         end | ||||||
|  |       end | ||||||
|  |  | ||||||
|  |       context 'when content_type is not input_select' do | ||||||
|  |         let(:message) { create(:message, account: account, conversation: conversation, message_type: :outgoing, content_type: :text) } | ||||||
|  |         let(:event_name) { 'message.updated' } | ||||||
|  |  | ||||||
|  |         it 'returns nil' do | ||||||
|  |           expect(processor.perform).to be(nil) | ||||||
|  |         end | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | end | ||||||
| @@ -24,7 +24,7 @@ describe Integrations::Dialogflow::ProcessorService do | |||||||
|  |  | ||||||
|     before do |     before do | ||||||
|       allow(dialogflow_service).to receive(:query_result).and_return(dialogflow_response) |       allow(dialogflow_service).to receive(:query_result).and_return(dialogflow_response) | ||||||
|       allow(processor).to receive(:get_dialogflow_response).and_return(dialogflow_service) |       allow(processor).to receive(:get_response).and_return(dialogflow_service) | ||||||
|       allow(dialogflow_text_double).to receive(:to_h).and_return({ text: ['hello payload'] }) |       allow(dialogflow_text_double).to receive(:to_h).and_return({ text: ['hello payload'] }) | ||||||
|     end |     end | ||||||
|  |  | ||||||
|   | |||||||
| @@ -6,18 +6,18 @@ describe AgentBotListener do | |||||||
|   let!(:inbox) { create(:inbox, account: account) } |   let!(:inbox) { create(:inbox, account: account) } | ||||||
|   let!(:agent_bot) { create(:agent_bot) } |   let!(:agent_bot) { create(:agent_bot) } | ||||||
|   let!(:conversation) { create(:conversation, account: account, inbox: inbox, assignee: user) } |   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 |   describe '#message_created' do | ||||||
|     let(:event_name) { :'conversation.created' } |     let(:event_name) { 'message.created' } | ||||||
|  |     let!(:event) { Events::Base.new(event_name, Time.zone.now, message: message) } | ||||||
|  |     let!(:message) do | ||||||
|  |       create(:message, message_type: 'outgoing', | ||||||
|  |                        account: account, inbox: inbox, conversation: conversation) | ||||||
|  |     end | ||||||
|  |  | ||||||
|     context 'when agent bot is not configured' do |     context 'when agent bot is not configured' do | ||||||
|       it 'does not send message to agent bot' do |       it 'does not send message to agent bot' do | ||||||
|         expect(AgentBotJob).to receive(:perform_later).exactly(0).times |         expect(AgentBots::WebhookJob).to receive(:perform_later).exactly(0).times | ||||||
|         listener.message_created(event) |         listener.message_created(event) | ||||||
|       end |       end | ||||||
|     end |     end | ||||||
| @@ -25,9 +25,52 @@ describe AgentBotListener do | |||||||
|     context 'when agent bot is configured' do |     context 'when agent bot is configured' do | ||||||
|       it 'sends message to agent bot' do |       it 'sends message to agent bot' do | ||||||
|         create(:agent_bot_inbox, inbox: inbox, agent_bot: agent_bot) |         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 |         expect(AgentBots::WebhookJob).to receive(:perform_later).with(agent_bot.outgoing_url, | ||||||
|  |                                                                       message.webhook_data.merge(event: 'message_created')).once | ||||||
|  |         listener.message_created(event) | ||||||
|  |       end | ||||||
|  |  | ||||||
|  |       it 'does not send message to agent bot if url is empty' do | ||||||
|  |         agent_bot = create(:agent_bot, outgoing_url: '') | ||||||
|  |         create(:agent_bot_inbox, inbox: inbox, agent_bot: agent_bot) | ||||||
|  |         expect(AgentBots::WebhookJob).not_to receive(:perform_later) | ||||||
|  |         listener.message_created(event) | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |  | ||||||
|  |     context 'when agent bot csml type is configured' do | ||||||
|  |       it 'sends message to agent bot' do | ||||||
|  |         agent_bot_csml = create(:agent_bot, :skip_validate, bot_type: 'csml') | ||||||
|  |         create(:agent_bot_inbox, inbox: inbox, agent_bot: agent_bot_csml) | ||||||
|  |         expect(AgentBots::CsmlJob).to receive(:perform_later).with('message.created', agent_bot_csml, message).once | ||||||
|         listener.message_created(event) |         listener.message_created(event) | ||||||
|       end |       end | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
|  |  | ||||||
|  |   describe '#webwidget_triggered' do | ||||||
|  |     let(:event_name) { 'webwidget.triggered' } | ||||||
|  |  | ||||||
|  |     context 'when agent bot is configured' do | ||||||
|  |       it 'send message to agent bot URL' do | ||||||
|  |         create(:agent_bot_inbox, inbox: inbox, agent_bot: agent_bot) | ||||||
|  |  | ||||||
|  |         event = double | ||||||
|  |         allow(event).to receive(:data) | ||||||
|  |           .and_return( | ||||||
|  |             { | ||||||
|  |               contact_inbox: conversation.contact_inbox, | ||||||
|  |               event_info: { country: 'US' } | ||||||
|  |             } | ||||||
|  |           ) | ||||||
|  |         expect(AgentBots::WebhookJob).to receive(:perform_later) | ||||||
|  |           .with( | ||||||
|  |             agent_bot.outgoing_url, | ||||||
|  |             conversation.contact_inbox.webhook_data.merge(event: 'webwidget_triggered', event_info: { country: 'US' }) | ||||||
|  |           ).once | ||||||
|  |  | ||||||
|  |         listener.webwidget_triggered(event) | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |   end | ||||||
| end | end | ||||||
|   | |||||||
							
								
								
									
										25
									
								
								spec/services/agent_bots/validate_bot_service_spec.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								spec/services/agent_bots/validate_bot_service_spec.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | |||||||
|  | require 'rails_helper' | ||||||
|  |  | ||||||
|  | describe AgentBots::ValidateBotService do | ||||||
|  |   describe '#perform' do | ||||||
|  |     it 'returns true if bot_type is not csml' do | ||||||
|  |       agent_bot = create(:agent_bot) | ||||||
|  |       valid = described_class.new(agent_bot: agent_bot).perform | ||||||
|  |       expect(valid).to be true | ||||||
|  |     end | ||||||
|  |  | ||||||
|  |     it 'returns true if validate csml returns true' do | ||||||
|  |       agent_bot = create(:agent_bot, :skip_validate, bot_type: 'csml', bot_config: {}) | ||||||
|  |       csml_client = double | ||||||
|  |       csml_response = double | ||||||
|  |       allow(CsmlEngine).to receive(:new).and_return(csml_client) | ||||||
|  |       allow(csml_client).to receive(:validate).and_return(csml_response) | ||||||
|  |       allow(csml_response).to receive(:blank?).and_return(false) | ||||||
|  |       allow(csml_response).to receive(:[]).with('valid').and_return(true) | ||||||
|  |  | ||||||
|  |       valid = described_class.new(agent_bot: agent_bot).perform | ||||||
|  |       expect(valid).to be true | ||||||
|  |       expect(CsmlEngine).to have_received(:new) | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | end | ||||||
		Reference in New Issue
	
	Block a user
	 Pranav Raj S
					Pranav Raj S