diff --git a/apps/meteor/app/livechat/lib/messageTypes.ts b/apps/meteor/app/livechat/lib/messageTypes.ts index ace0d4e65bfa..8770c3f3deea 100644 --- a/apps/meteor/app/livechat/lib/messageTypes.ts +++ b/apps/meteor/app/livechat/lib/messageTypes.ts @@ -58,11 +58,20 @@ MessageTypes.registerType({ to: message?.transferData?.transferredTo?.name || message?.transferData?.transferredTo?.username || '', duration: comment, }), + autoTransferVerifiedChatsToAgent: (): string => + t(`Livechat_transfer_to_agent_auto_transfer_verified_chat`, { + from, + to: message?.transferData?.transferredTo?.name || message?.transferData?.transferredTo?.username || '', + }), autoTransferUnansweredChatsToQueue: (): string => t(`Livechat_transfer_return_to_the_queue_auto_transfer_unanswered_chat`, { from, duration: comment, }), + autoTransferVerifiedChatsToQueue: (): string => + t(`Livechat_transfer_return_to_the_queue_auto_transfer_verified_chat`, { + from, + }), }; return { transfer: transferTypes[message.transferData.scope](), diff --git a/apps/meteor/app/livechat/server/hooks/automateVerificationProcess.tsx b/apps/meteor/app/livechat/server/hooks/automateVerificationProcess.tsx new file mode 100644 index 000000000000..2db83da622ed --- /dev/null +++ b/apps/meteor/app/livechat/server/hooks/automateVerificationProcess.tsx @@ -0,0 +1,29 @@ +import { OmnichannelVerification } from '@rocket.chat/core-services'; +import { RoomVerificationState, isOmnichannelRoom } from '@rocket.chat/core-typings'; + +import { callbacks } from '../../../../lib/callbacks'; +import { settings } from '../../../settings/server'; + +callbacks.add( + 'afterSaveMessage', + async (message, room) => { + if (!(isOmnichannelRoom(room) && room.v.token)) { + return message; + } + if (message.t) { + return message; + } + if (!message.token) { + return message; + } + if ( + room.verificationStatus === RoomVerificationState.unVerified && + settings.get('Livechat_automate_verification_process') && + settings.get('Livechat_verificaion_bot_assign') === room.servedBy?.username + ) { + await OmnichannelVerification.initiateVerificationProcess(room._id); + } + }, + callbacks.priority.LOW, + 'automate-verification-process', +); diff --git a/apps/meteor/app/livechat/server/hooks/verificationCheck.ts b/apps/meteor/app/livechat/server/hooks/verificationCheck.ts index 7e8ffb71d357..89b5dd9d79ba 100644 --- a/apps/meteor/app/livechat/server/hooks/verificationCheck.ts +++ b/apps/meteor/app/livechat/server/hooks/verificationCheck.ts @@ -6,6 +6,7 @@ import { LivechatRooms, Users } from '@rocket.chat/models'; import { callbacks } from '../../../../lib/callbacks'; import { i18n } from '../../../../server/lib/i18n'; import { sendMessage } from '../../../lib/server/functions/sendMessage'; +import { settings } from '../../../settings/server'; callbacks.add( 'afterSaveMessage', @@ -25,7 +26,11 @@ callbacks.add( break; } case RoomVerificationState.isListeningToOTP: { - const bot = await Users.findOneById('rocket.cat'); + const agent = settings.get('Livechat_verificaion_bot_assign') || 'rocket.cat'; + const bot = + room?.servedBy?.username === agent + ? await Users.findOneByUsername(settings.get('Livechat_verificaion_bot_assign')) + : await Users.findOneById('rocket.cat'); if (message.msg === 'Resend OTP') { if (room.source.type === 'widget') { const wrongOtpInstructionsMessage = { @@ -51,6 +56,12 @@ callbacks.add( groupable: false, }; await sendMessage(bot, completionMessage, room); + if ( + settings.get('Livechat_automate_verification_process') && + room.servedBy?.username === settings.get('Livechat_verificaion_bot_assign') + ) { + await OmnichannelVerification.transferChatAfterVerificationProcess(room._id); + } break; } } diff --git a/apps/meteor/app/livechat/server/index.ts b/apps/meteor/app/livechat/server/index.ts index 2fb83fc9a099..a850ce44c6b7 100644 --- a/apps/meteor/app/livechat/server/index.ts +++ b/apps/meteor/app/livechat/server/index.ts @@ -10,6 +10,7 @@ import './hooks/saveAnalyticsData'; import './hooks/sendToCRM'; import './hooks/processRoomAbandonment'; import './hooks/saveLastVisitorMessageTs'; +import './hooks/automateVerificationProcess'; import './hooks/markRoomNotResponded'; import './hooks/sendEmailTranscriptOnClose'; import './hooks/saveContactLastChat'; diff --git a/apps/meteor/app/livechat/server/lib/Helper.ts b/apps/meteor/app/livechat/server/lib/Helper.ts index 2c43db0ba87a..8d0b7f4e4f01 100644 --- a/apps/meteor/app/livechat/server/lib/Helper.ts +++ b/apps/meteor/app/livechat/server/lib/Helper.ts @@ -401,7 +401,10 @@ export const dispatchInquiryQueued = async (inquiry: ILivechatInquiryRecord, age } }; -export const forwardRoomToAgent = async (room: IOmnichannelRoom, transferData: TransferData) => { +export const forwardRoomToAgent = async ( + room: Pick, + transferData: TransferData, +) => { if (!room?.open) { return false; } diff --git a/apps/meteor/ee/app/livechat-enterprise/server/hooks/handleNextAgentPreferredEvents.ts b/apps/meteor/ee/app/livechat-enterprise/server/hooks/handleNextAgentPreferredEvents.ts index b7026e3b2da4..43aaa088bbd8 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/hooks/handleNextAgentPreferredEvents.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/hooks/handleNextAgentPreferredEvents.ts @@ -85,6 +85,13 @@ settings.watch('Omnichannel_contact_manager_routing', (value) => { callbacks.add( 'livechat.checkDefaultAgentOnNewRoom', async (defaultAgent, defaultGuest) => { + if (settings.get('Livechat_automate_verification_process')) { + const bot = await Users.findOneByUsername(settings.get('Livechat_verificaion_bot_assign')); + const agentId = bot?._id; + const username = bot?.username || 'rocket.cat'; + const agent = { agentId, username }; + return agent; + } if (defaultAgent || !defaultGuest) { return defaultAgent; } diff --git a/apps/meteor/ee/app/livechat-enterprise/server/lib/AutoTransferChatScheduler.ts b/apps/meteor/ee/app/livechat-enterprise/server/lib/AutoTransferChatScheduler.ts index 7e52cc266c04..fc128357a4cc 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/lib/AutoTransferChatScheduler.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/lib/AutoTransferChatScheduler.ts @@ -79,7 +79,7 @@ class AutoTransferChatSchedulerClass { departmentId: 1, }); if (!room?.open || !room?.servedBy?._id) { - throw new Error('Room is not open or is not being served by an agent'); + throw new Error('error-room-not-opened-or-serviced'); } const { diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json index 301a59d96177..85cec84dfbc7 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json @@ -1854,6 +1854,7 @@ "Enable_two-factor_authentication": "Enable two-factor authentication via TOTP", "Enable_two-factor_authentication_email": "Enable two-factor authentication via Email", "Enable_unlimited_apps": "Enable unlimited apps", + "Enable_verification_process_before_routing_to_agent":"Enable verification process before routing to agent", "Enabled": "Enabled", "Encrypted": "Encrypted", "Encrypted_channel_Description": "End to end encrypted channel. Search will not work with encrypted channels and notifications may not show the messages content.", @@ -2058,6 +2059,7 @@ "error-room-onHold": "Error! Room is On Hold", "error-room-is-already-on-hold": "Error! Room is already On Hold", "error-room-not-on-hold": "Error! Room is not On Hold", + "error-room-not-opened-or-serviced": "Error! Room is not open or is not being served by an agent", "error-selected-agent-room-agent-are-same": "The selected agent and the room agent are the same", "error-starring-message": "Message could not be stared", "error-tags-must-be-assigned-before-closing-chat": "Tag(s) must be assigned before closing the chat", @@ -3099,9 +3101,11 @@ "Livechat_transfer_return_to_the_queue": "{{from}} returned the chat to the queue", "Livechat_transfer_return_to_the_queue_with_a_comment": "{{from}} returned the chat to the queue with a comment: {{comment}}", "Livechat_transfer_return_to_the_queue_auto_transfer_unanswered_chat": "{{from}} returned the chat to the queue since it was unanswered for {{duration}} seconds", + "Livechat_transfer_return_to_the_queue_auto_transfer_verified_chat":"{{from}} returned the chat to the queue since it is verified", "Livechat_transfer_to_agent": "{{from}} transferred the chat to {{to}}", "Livechat_transfer_to_agent_with_a_comment": "{{from}} transferred the chat to {{to}} with a comment: {{comment}}", "Livechat_transfer_to_agent_auto_transfer_unanswered_chat": "{{from}} transferred the chat to {{to}} since it was unanswered for {{duration}} seconds", + "Livechat_transfer_to_agent_auto_transfer_verified_chat":"{{from}} transferred the chat to {{to}} since it is verfied now", "Livechat_transfer_to_department": "{{from}} transferred the chat to the department {{to}}", "Livechat_transfer_to_department_with_a_comment": "{{from}} transferred the chat to the department {{to}} with a comment: {{comment}}", "Livechat_transfer_failed_fallback": "The original department ( {{from}} ) doesn't have online agents. Chat succesfully transferred to {{to}}", @@ -5408,6 +5412,7 @@ "Value_messages": "{{value}} messages", "Value_users": "{{value}} users", "Verification": "Verification", + "Verification_Bot":"Enter the username for assigning the verification bot.", "Verification_Description": "You may use the following placeholders: \n - `[Verification_Url]` for the verification URL. \n - `[name]`, `[fname]`, `[lname]` for the user's full name, first name or last name, respectively. \n - `[email]` for the user's email. \n - `[Site_Name]` and `[Site_URL]` for the Application Name and URL respectively. ", "Verification_Email": "Click here to verify your email address.", "Verification_email_body": "Please, click on the button below to confirm your email address.", diff --git a/apps/meteor/server/services/omnichannel-verification/service.ts b/apps/meteor/server/services/omnichannel-verification/service.ts index c3fcf92147ee..96ea78d14054 100644 --- a/apps/meteor/server/services/omnichannel-verification/service.ts +++ b/apps/meteor/server/services/omnichannel-verification/service.ts @@ -1,6 +1,6 @@ import type { IOmnichannelVerification, ISetVisitorEmailResult } from '@rocket.chat/core-services'; import { ServiceClassInternal } from '@rocket.chat/core-services'; -import type { IRoom, IMessage, IOmnichannelGenericRoom, IOmnichannelRoom } from '@rocket.chat/core-typings'; +import type { IRoom, IMessage, IOmnichannelGenericRoom, IOmnichannelRoom, IUser } from '@rocket.chat/core-typings'; import { RoomVerificationState } from '@rocket.chat/core-typings'; import { Logger } from '@rocket.chat/logger'; import { LivechatVisitors, LivechatRooms, Users } from '@rocket.chat/models'; @@ -13,7 +13,10 @@ import { check } from 'meteor/check'; import { checkEmailAvailability } from '../../../app/lib/server/functions/checkEmailAvailability'; import { sendMessage } from '../../../app/lib/server/functions/sendMessage'; import { validateEmailDomain } from '../../../app/lib/server/lib'; +import { Livechat } from '../../../app/livechat/server'; +import { forwardRoomToAgent } from '../../../app/livechat/server/lib/Helper'; import { Livechat as LivechatTyped } from '../../../app/livechat/server/lib/LivechatTyped'; +import { RoutingManager } from '../../../app/livechat/server/lib/RoutingManager'; import * as Mailer from '../../../app/mailer/server/api'; import { settings } from '../../../app/settings/server'; import { i18n } from '../../lib/i18n'; @@ -24,7 +27,7 @@ interface IRandomOTP { expire: Date; } -const bot = await Users.findOneById('rocket.cat'); +let bot: IUser | null; export class OmnichannelVerification extends ServiceClassInternal implements IOmnichannelVerification { protected name = 'omnichannel-verification'; @@ -219,6 +222,11 @@ export class OmnichannelVerification extends ServiceClassInternal implements IOm try { check(rid, String); const room = await LivechatRooms.findOneById(rid); + const agent = settings.get('Livechat_verificaion_bot_assign') || 'rocket.cat'; + bot = + room?.servedBy?.username === agent + ? await Users.findOneByUsername(settings.get('Livechat_verificaion_bot_assign')) + : await Users.findOneById('rocket.cat'); if (room?.verificationStatus !== 'unVerified') { return; } @@ -314,4 +322,60 @@ export class OmnichannelVerification extends ServiceClassInternal implements IOm return { success: false, error: error as Error }; } } + + async transferChatAfterVerificationProcess(roomId: IRoom['_id']): Promise { + try { + const room = await LivechatRooms.findOneById>(roomId, { + projection: { + _id: 1, + v: 1, + servedBy: 1, + open: 1, + departmentId: 1, + }, + }); + if (!room?.open || !room?.servedBy?._id) { + throw new Error('error-room-not-opened-or-serviced'); + } + const { + departmentId, + servedBy: { _id: ignoreAgentId }, + } = room; + + if (!RoutingManager.getConfig()?.autoAssignAgent) { + this.logger.debug(`Auto-assign agent is disabled, returning room ${roomId} as inquiry`); + + await Livechat.returnRoomAsInquiry(room._id, departmentId, { + scope: 'autoTransferVerifiedChatsToQueue', + transferredBy: bot, + }); + return; + } + + const agent = await RoutingManager.getNextAgent(departmentId, ignoreAgentId); + if (!agent) { + await Livechat.returnRoomAsInquiry(room._id, departmentId, { + scope: 'autoTransferVerifiedChatsToQueue', + transferredBy: bot, + }); + this.logger.error(`No agent found to transfer room ${room._id}`); + return; + } + const transferredBy = bot; + + if (!transferredBy) { + this.logger.error(`Error while transferring room ${room._id}: user not found`); + return; + } + + await forwardRoomToAgent(room, { + userId: agent?.agentId, + transferredBy, + transferredTo: agent, + scope: 'autoTransferVerifiedChatsToAgent', + }); + } catch (error) { + this.logger.error(`Error while transferring room`); + } + } } diff --git a/apps/meteor/server/settings/omnichannel.ts b/apps/meteor/server/settings/omnichannel.ts index 00db13fee0e6..019aeab361ad 100644 --- a/apps/meteor/server/settings/omnichannel.ts +++ b/apps/meteor/server/settings/omnichannel.ts @@ -229,6 +229,22 @@ export const createOmniSettings = () => enableQuery: omnichannelEnabledQuery, }); + await this.add('Livechat_automate_verification_process', true, { + type: 'boolean', + group: 'Omnichannel', + public: true, + i18nLabel: 'Enable_verification_process_before_routing_to_agent', + enableQuery: omnichannelEnabledQuery, + }); + + await this.add('Livechat_verificaion_bot_assign', '', { + type: 'string', + group: 'Omnichannel', + public: true, + i18nLabel: 'Verification_Bot', + enableQuery: [{ _id: 'Livechat_automate_verification_process', value: true }, omnichannelEnabledQuery], + }); + await this.add('Livechat_webhookUrl', '', { type: 'string', group: 'Omnichannel', diff --git a/packages/core-services/src/types/IOmnichannelVerification.ts b/packages/core-services/src/types/IOmnichannelVerification.ts index 690e9e3ac3c8..321f2d21381a 100644 --- a/packages/core-services/src/types/IOmnichannelVerification.ts +++ b/packages/core-services/src/types/IOmnichannelVerification.ts @@ -12,4 +12,5 @@ export interface IOmnichannelVerification extends IServiceClass { setVisitorEmail(room: IOmnichannelRoom, email: string): Promise; sendVerificationCodeToVisitor(visitorId: string, room: IOmnichannelGenericRoom): Promise; createLivechatMessage(room: IOmnichannelRoom, text: string): Promise; + transferChatAfterVerificationProcess(roomId: IRoom['_id']): Promise; } diff --git a/packages/core-typings/src/omnichannel/routing.ts b/packages/core-typings/src/omnichannel/routing.ts index eed6dd6f1a19..35a939b18fbf 100644 --- a/packages/core-typings/src/omnichannel/routing.ts +++ b/packages/core-typings/src/omnichannel/routing.ts @@ -36,7 +36,14 @@ export type TransferData = { name?: string; }; clientAction?: boolean; - scope: 'agent' | 'department' | 'queue' | 'autoTransferUnansweredChatsToAgent' | 'autoTransferUnansweredChatsToQueue'; + scope: + | 'agent' + | 'department' + | 'queue' + | 'autoTransferUnansweredChatsToAgent' + | 'autoTransferUnansweredChatsToQueue' + | 'autoTransferVerifiedChatsToQueue' + | 'autoTransferVerifiedChatsToAgent'; comment?: string; }; diff --git a/packages/livechat/src/helpers/normalizeTransferHistoryMessage.ts b/packages/livechat/src/helpers/normalizeTransferHistoryMessage.ts index 83712bdd01ea..f42a83cc57ba 100644 --- a/packages/livechat/src/helpers/normalizeTransferHistoryMessage.ts +++ b/packages/livechat/src/helpers/normalizeTransferHistoryMessage.ts @@ -38,6 +38,8 @@ export const normalizeTransferHistoryMessage = ( }, autoTransferUnansweredChatsToAgent: () => t('the_chat_was_transferred_to_another_agent_due_to_unanswered', { duration: comment }), autoTransferUnansweredChatsToQueue: () => t('the_chat_was_moved_back_to_queue_due_to_unanswered', { duration: comment }), + autoTransferVerifiedChatsToAgent: () => t('the_chat_was_transferred_to_another_agent'), + autoTransferVerifiedChatsToQueue: () => t('from_returned_the_chat_to_the_queue', { from }), }; return transferTypes[scope](); diff --git a/packages/livechat/src/i18n/en.json b/packages/livechat/src/i18n/en.json index 791e2262351f..76afa9a4d0eb 100644 --- a/packages/livechat/src/i18n/en.json +++ b/packages/livechat/src/i18n/en.json @@ -54,6 +54,7 @@ "leave_a_message": "Leave a message", "livechat_connected": "Livechat connected.", "livechat_is_not_connected": "Livechat is not connected.", + "Livechat_transfer_to_agent_auto_transfer_verified_chat":"{{from}} transferred the chat to {{to}} since it is verfied now", "media_types_not_accepted": "Media Types Not Accepted.", "message": "Message", "messages": "Messages",