Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Automate omnichannel verification process #30368

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions apps/meteor/app/livechat/lib/messageTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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](),
Expand Down
Original file line number Diff line number Diff line change
@@ -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',
);
13 changes: 12 additions & 1 deletion apps/meteor/app/livechat/server/hooks/verificationCheck.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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 = {
Expand All @@ -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;
}
}
Expand Down
1 change: 1 addition & 0 deletions apps/meteor/app/livechat/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
5 changes: 4 additions & 1 deletion apps/meteor/app/livechat/server/lib/Helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<IOmnichannelRoom, '_id' | 'v' | 'servedBy' | 'open' | 'departmentId'>,
transferData: TransferData,
) => {
if (!room?.open) {
return false;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,13 @@ settings.watch<boolean>('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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
5 changes: 5 additions & 0 deletions apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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}}",
Expand Down Expand Up @@ -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 <a href=\"[Verification_Url]\">here</a> to verify your email address.",
"Verification_email_body": "Please, click on the button below to confirm your email address.",
Expand Down
68 changes: 66 additions & 2 deletions apps/meteor/server/services/omnichannel-verification/service.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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';
Expand All @@ -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';

Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -314,4 +322,60 @@ export class OmnichannelVerification extends ServiceClassInternal implements IOm
return { success: false, error: error as Error };
}
}

async transferChatAfterVerificationProcess(roomId: IRoom['_id']): Promise<void> {
try {
const room = await LivechatRooms.findOneById<Pick<IOmnichannelRoom, '_id' | 'v' | 'servedBy' | 'open' | 'departmentId'>>(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`);
}
}
}
16 changes: 16 additions & 0 deletions apps/meteor/server/settings/omnichannel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ export interface IOmnichannelVerification extends IServiceClass {
setVisitorEmail(room: IOmnichannelRoom, email: string): Promise<ISetVisitorEmailResult>;
sendVerificationCodeToVisitor(visitorId: string, room: IOmnichannelGenericRoom): Promise<void>;
createLivechatMessage(room: IOmnichannelRoom, text: string): Promise<IMessage['_id']>;
transferChatAfterVerificationProcess(roomId: IRoom['_id']): Promise<void>;
}
9 changes: 8 additions & 1 deletion packages/core-typings/src/omnichannel/routing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]();
Expand Down
1 change: 1 addition & 0 deletions packages/livechat/src/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading