diff --git a/app/javascript/dashboard/helper/automationHelper.js b/app/javascript/dashboard/helper/automationHelper.js index 302a202f5..581d539bc 100644 --- a/app/javascript/dashboard/helper/automationHelper.js +++ b/app/javascript/dashboard/helper/automationHelper.js @@ -136,6 +136,7 @@ export const getConditionOptions = ({ team_id: teams, campaigns: generateConditionOptions(campaigns), browser_language: languages, + conversation_language: languages, country_code: countries, message_type: MESSAGE_CONDITION_VALUES, }; diff --git a/app/javascript/dashboard/mixins/automations/methodsMixin.js b/app/javascript/dashboard/mixins/automations/methodsMixin.js index bbdb3dfd6..611d5ad38 100644 --- a/app/javascript/dashboard/mixins/automations/methodsMixin.js +++ b/app/javascript/dashboard/mixins/automations/methodsMixin.js @@ -251,7 +251,7 @@ export default { }, getActionDropdownValues(type) { const { agents, labels, teams } = this; - return getActionOptions({ agents, labels, teams, type }); + return getActionOptions({ agents, labels, teams, languages, type }); }, manifestCustomAttributes() { const conversationCustomAttributesRaw = this.$store.getters[ diff --git a/app/javascript/dashboard/routes/dashboard/settings/automation/constants.js b/app/javascript/dashboard/routes/dashboard/settings/automation/constants.js index ea5d7238b..f052be66d 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/automation/constants.js +++ b/app/javascript/dashboard/routes/dashboard/settings/automation/constants.js @@ -36,6 +36,13 @@ export const AUTOMATIONS = { inputType: 'multi_select', filterOperators: OPERATOR_TYPES_1, }, + { + key: 'conversation_language', + name: 'Conversation Language', + attributeI18nKey: 'CONVERSATION_LANGUAGE', + inputType: 'multi_select', + filterOperators: OPERATOR_TYPES_1, + }, { key: 'phone_number', name: 'Phone Number', @@ -161,6 +168,13 @@ export const AUTOMATIONS = { inputType: 'multi_select', filterOperators: OPERATOR_TYPES_1, }, + { + key: 'conversation_language', + name: 'Conversation Language', + attributeI18nKey: 'CONVERSATION_LANGUAGE', + inputType: 'multi_select', + filterOperators: OPERATOR_TYPES_1, + }, ], actions: [ { @@ -292,6 +306,13 @@ export const AUTOMATIONS = { inputType: 'multi_select', filterOperators: OPERATOR_TYPES_1, }, + { + key: 'conversation_language', + name: 'Conversation Language', + attributeI18nKey: 'CONVERSATION_LANGUAGE', + inputType: 'multi_select', + filterOperators: OPERATOR_TYPES_1, + }, ], actions: [ { @@ -416,6 +437,13 @@ export const AUTOMATIONS = { inputType: 'multi_select', filterOperators: OPERATOR_TYPES_1, }, + { + key: 'conversation_language', + name: 'Conversation Language', + attributeI18nKey: 'CONVERSATION_LANGUAGE', + inputType: 'multi_select', + filterOperators: OPERATOR_TYPES_1, + }, ], actions: [ { diff --git a/app/jobs/hook_job.rb b/app/jobs/hook_job.rb index ee32d0c15..bf0c7a1f9 100644 --- a/app/jobs/hook_job.rb +++ b/app/jobs/hook_job.rb @@ -7,6 +7,8 @@ class HookJob < ApplicationJob process_slack_integration(hook, event_name, event_data) when 'dialogflow' process_dialogflow_integration(hook, event_name, event_data) + when 'google_translate' + google_translate_integration(hook, event_name, event_data) end rescue StandardError => e Rails.logger.error e @@ -27,4 +29,11 @@ class HookJob < ApplicationJob Integrations::Dialogflow::ProcessorService.new(event_name: event_name, hook: hook, event_data: event_data).perform end + + def google_translate_integration(hook, event_name, event_data) + return unless ['message.created'].include?(event_name) + + message = event_data[:message] + Integrations::GoogleTranslate::DetectLanguageService.new(hook: hook, message: message).perform + end end diff --git a/app/models/automation_rule.rb b/app/models/automation_rule.rb index d46aaa583..9342bc883 100644 --- a/app/models/automation_rule.rb +++ b/app/models/automation_rule.rb @@ -31,7 +31,7 @@ class AutomationRule < ApplicationRecord scope :active, -> { where(active: true) } CONDITIONS_ATTRS = %w[content email country_code status message_type browser_language assignee_id team_id referer city company inbox_id - mail_subject phone_number].freeze + mail_subject phone_number conversation_language].freeze ACTIONS_ATTRS = %w[send_message add_label send_email_to_team assign_team assign_agent send_webhook_event mute_conversation send_attachment change_status resolve_conversation snooze_conversation send_email_transcript].freeze diff --git a/app/models/conversation.rb b/app/models/conversation.rb index 823215476..ddefe7da9 100644 --- a/app/models/conversation.rb +++ b/app/models/conversation.rb @@ -212,12 +212,18 @@ class Conversation < ApplicationRecord end def notify_conversation_updation - return unless previous_changes.keys.present? && (previous_changes.keys & %w[team_id assignee_id status snoozed_until - custom_attributes label_list first_reply_created_at]).present? + return unless previous_changes.keys.present? && whitelisted_keys? dispatcher_dispatch(CONVERSATION_UPDATED, previous_changes) end + def whitelisted_keys? + ( + (previous_changes.keys & %w[team_id assignee_id status snoozed_until custom_attributes label_list first_reply_created_at]).present? || + (previous_changes['additional_attributes'].present? && (previous_changes['additional_attributes'][1].keys & %w[conversation_language]).present?) + ) + end + def self_assign?(assignee_id) assignee_id.present? && Current.user&.id == assignee_id end diff --git a/lib/automation_rules/conditions.json b/lib/automation_rules/conditions.json index c07b78448..ba4bd4b5b 100644 --- a/lib/automation_rules/conditions.json +++ b/lib/automation_rules/conditions.json @@ -63,6 +63,13 @@ "filter_operators": [ "equal_to", "not_equal_to", "contains", "does_not_contain" ], "attribute_type": "additional_attributes" }, + "conversation_language": { + "attribute_name": "Conversation Language", + "input_type": "textbox", + "data_type": "text", + "filter_operators": [ "equal_to", "not_equal_to" ], + "attribute_type": "additional_attributes" + }, "mail_subject": { "attribute_name": "Email Subject", "input_type": "textbox", diff --git a/lib/filters/filter_keys.json b/lib/filters/filter_keys.json index 43d961c50..1853b08e6 100644 --- a/lib/filters/filter_keys.json +++ b/lib/filters/filter_keys.json @@ -63,6 +63,13 @@ "filter_operators": [ "equal_to", "not_equal_to", "contains", "does_not_contain" ], "attribute_type": "additional_attributes" }, + "conversation_language": { + "attribute_name": "Conversation Language", + "input_type": "textbox", + "data_type": "text", + "filter_operators": [ "equal_to", "not_equal_to" ], + "attribute_type": "additional_attributes" + }, "country_code": { "attribute_name": "Country Name", "input_type": "textbox", diff --git a/lib/integrations/google_translate/detect_language_service.rb b/lib/integrations/google_translate/detect_language_service.rb new file mode 100644 index 000000000..265c1d5be --- /dev/null +++ b/lib/integrations/google_translate/detect_language_service.rb @@ -0,0 +1,40 @@ +class Integrations::GoogleTranslate::DetectLanguageService + pattr_initialize [:hook!, :message!] + + def perform + return unless valid_message? + return if conversation.additional_attributes['conversation_language'].present? + + text = message.content[0...1500] + response = client.detect_language( + content: text, + parent: "projects/#{hook.settings['project_id']}" + ) + + update_conversation(response) + end + + private + + def valid_message? + message.incoming? && message.content.present? + end + + def conversation + @conversation ||= message.conversation + end + + def update_conversation(response) + return if response&.languages.blank? + + conversation_language = response.languages.first.language_code + additional_attributes = conversation.additional_attributes.merge({ conversation_language: conversation_language }) + conversation.update!(additional_attributes: additional_attributes) + end + + def client + @client ||= Google::Cloud::Translate.translation_service do |config| + config.credentials = hook.settings['credentials'] + end + end +end diff --git a/spec/factories/integrations/hooks.rb b/spec/factories/integrations/hooks.rb index 21d8a84be..0c41cad13 100644 --- a/spec/factories/integrations/hooks.rb +++ b/spec/factories/integrations/hooks.rb @@ -16,5 +16,10 @@ FactoryBot.define do app_id { 'dyte' } settings { { api_key: 'api_key', organization_id: 'org_id' } } end + + trait :google_translate do + app_id { 'google_translate' } + settings { { project_id: 'test', credentials: {} } } + end end end diff --git a/spec/jobs/hook_job_spec.rb b/spec/jobs/hook_job_spec.rb index e6dbbf968..85517d51c 100644 --- a/spec/jobs/hook_job_spec.rb +++ b/spec/jobs/hook_job_spec.rb @@ -7,7 +7,7 @@ RSpec.describe HookJob, type: :job do let(:hook) { create(:integrations_hook, account: account) } let(:inbox) { create(:inbox, account: account) } let(:event_name) { 'message.created' } - let(:event_data) { { message: create(:message, account: account) } } + let(:event_data) { { message: create(:message, account: account, content: 'muchas muchas gracias', message_type: :incoming) } } it 'enqueues the job' do expect { job }.to have_enqueued_job(described_class) @@ -25,7 +25,7 @@ RSpec.describe HookJob, type: :job do it 'calls Integrations::Slack::SendOnSlackService when its a slack hook' do hook = create(:integrations_hook, app_id: 'slack', account: account) allow(Integrations::Slack::SendOnSlackService).to receive(:new).and_return(process_service) - expect(Integrations::Slack::SendOnSlackService).to receive(:new) + expect(Integrations::Slack::SendOnSlackService).to receive(:new).with(message: event_data[:message], hook: hook) described_class.perform_now(hook, event_name, event_data) end @@ -40,7 +40,14 @@ RSpec.describe HookJob, type: :job do it 'calls Integrations::Dialogflow::ProcessorService when its a dialogflow intergation' do hook = create(:integrations_hook, :dialogflow, inbox: inbox, account: account) allow(Integrations::Dialogflow::ProcessorService).to receive(:new).and_return(process_service) - expect(Integrations::Dialogflow::ProcessorService).to receive(:new) + expect(Integrations::Dialogflow::ProcessorService).to receive(:new).with(event_name: event_name, hook: hook, event_data: event_data) + described_class.perform_now(hook, event_name, event_data) + end + + it 'calls Conversations::DetectLanguageJob when its a google_translate intergation' do + hook = create(:integrations_hook, :google_translate, account: account) + allow(Integrations::GoogleTranslate::DetectLanguageService).to receive(:new).and_return(process_service) + expect(Integrations::GoogleTranslate::DetectLanguageService).to receive(:new).with(hook: hook, message: event_data[:message]) described_class.perform_now(hook, event_name, event_data) end end diff --git a/spec/lib/integrations/google_translate/detect_language_service_spec.rb b/spec/lib/integrations/google_translate/detect_language_service_spec.rb new file mode 100644 index 000000000..6768e9a33 --- /dev/null +++ b/spec/lib/integrations/google_translate/detect_language_service_spec.rb @@ -0,0 +1,44 @@ +require 'rails_helper' +require 'google/cloud/translate/v3' + +describe Integrations::GoogleTranslate::DetectLanguageService do + let(:account) { create(:account) } + let(:message) { create(:message, account: account, content: 'muchas muchas gracias') } + let(:hook) { create(:integrations_hook, :google_translate, account: account) } + let(:translate_client) { double } + + before do + allow(::Google::Cloud::Translate).to receive(:translation_service).and_return(translate_client) + allow(translate_client).to receive(:detect_language).and_return(::Google::Cloud::Translate::V3::DetectLanguageResponse + .new({ languages: [{ language_code: 'es', confidence: 0.71875 }] })) + end + + describe '#perform' do + it 'detects and updates the conversation language' do + described_class.new(hook: hook, message: message).perform + expect(translate_client).to have_received(:detect_language) + expect(message.conversation.reload.additional_attributes['conversation_language']).to eq('es') + end + + it 'will not update the conversation language if it is already present' do + message.conversation.update!(additional_attributes: { conversation_language: 'en' }) + described_class.new(hook: hook, message: message).perform + expect(translate_client).not_to have_received(:detect_language) + expect(message.conversation.reload.additional_attributes['conversation_language']).to eq('en') + end + + it 'will not update the conversation language if the message is not incoming' do + message.update!(message_type: :outgoing) + described_class.new(hook: hook, message: message).perform + expect(translate_client).not_to have_received(:detect_language) + expect(message.conversation.reload.additional_attributes['conversation_language']).to be_nil + end + + it 'will not execute if the message content is blank' do + message.update!(content: nil) + described_class.new(hook: hook, message: message).perform + expect(translate_client).not_to have_received(:detect_language) + expect(message.conversation.reload.additional_attributes['conversation_language']).to be_nil + end + end +end diff --git a/spec/listeners/automation_rule_listener_spec.rb b/spec/listeners/automation_rule_listener_spec.rb index 46caed67d..6f87b8342 100644 --- a/spec/listeners/automation_rule_listener_spec.rb +++ b/spec/listeners/automation_rule_listener_spec.rb @@ -1,4 +1,5 @@ require 'rails_helper' + describe AutomationRuleListener do let(:listener) { described_class.instance } let!(:account) { create(:account) } diff --git a/spec/models/conversation_spec.rb b/spec/models/conversation_spec.rb index aca21e843..2da0af0ca 100644 --- a/spec/models/conversation_spec.rb +++ b/spec/models/conversation_spec.rb @@ -160,6 +160,22 @@ RSpec.describe Conversation, type: :model do .with(described_class::CONVERSATION_UPDATED, kind_of(Time), conversation: conversation, notifiable_assignee_change: true) end + it 'will run conversation_updated event for conversation_language in additional_attributes' do + conversation.additional_attributes[:conversation_language] = 'es' + conversation.save! + changed_attributes = conversation.previous_changes + expect(Rails.configuration.dispatcher).to have_received(:dispatch) + .with(described_class::CONVERSATION_UPDATED, kind_of(Time), conversation: conversation, notifiable_assignee_change: false, + changed_attributes: changed_attributes, performed_by: nil) + end + + it 'will not run conversation_updated event for bowser_language in additional_attributes' do + conversation.additional_attributes[:browser_language] = 'es' + conversation.save! + expect(Rails.configuration.dispatcher).not_to have_received(:dispatch) + .with(described_class::CONVERSATION_UPDATED, kind_of(Time), conversation: conversation, notifiable_assignee_change: true) + end + it 'creates conversation activities' do conversation.update( status: :resolved,