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

[NEW][ENTERPRISE] Automatic transfer of unanswered conversations to another agent #20090

Merged
merged 27 commits into from Jan 18, 2021
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
1222137
[Omnichannel] Auto transfer chat based on inactivity
murtaza98 Jan 7, 2021
1996b93
Updated Livechat.transfer() method to support a new param - ignoredUs…
murtaza98 Jan 9, 2021
93f366b
Merge branch 'develop' into livechat/auto-transfer-feature
murtaza98 Jan 9, 2021
4cd4285
Merge branch 'develop' into livechat/auto-transfer-feature
renatobecker Jan 11, 2021
6c38c24
Merge branch 'develop' into livechat/auto-transfer-feature
renatobecker Jan 13, 2021
845cf1c
Apply suggestions from code review
murtaza98 Jan 14, 2021
6e7e970
Apply suggestion from code review
murtaza98 Jan 14, 2021
18c0dfd
Merge branch 'develop' into livechat/auto-transfer-feature
renatobecker Jan 14, 2021
a97b29b
Fix merge conflict
murtaza98 Jan 14, 2021
af44eab
Merge branch 'livechat/auto-transfer-feature' of https://github.com/R…
murtaza98 Jan 14, 2021
f386196
Apply suggestions from code review
murtaza98 Jan 14, 2021
b312e5a
Fix PR review.
renatobecker Jan 15, 2021
8b33c73
cancel previous jobs b4 scheduling new one + minor improvements
murtaza98 Jan 15, 2021
0a539d5
Merge branch 'livechat/auto-transfer-feature' of https://github.com/R…
murtaza98 Jan 15, 2021
8b8bb86
Use a dedicated variable to read setting value.
renatobecker Jan 15, 2021
f52c9ad
[optimize] prevent cancelling job after each message sent
murtaza98 Jan 15, 2021
b7e5d36
Merge branch 'livechat/auto-transfer-feature' of https://github.com/R…
murtaza98 Jan 15, 2021
6a8068e
Improve codebase.
renatobecker Jan 15, 2021
2799708
Remove unnecessary import.
renatobecker Jan 15, 2021
ae0d4e4
Add PT-BR translations.
renatobecker Jan 15, 2021
8119bb1
Fix class methods.
renatobecker Jan 17, 2021
29076a7
Merge branch 'develop' into livechat/auto-transfer-feature
renatobecker Jan 18, 2021
9ae0940
Merge branch 'livechat/auto-transfer-feature' of https://github.com/R…
renatobecker Jan 18, 2021
3fa9453
Improve class code.
renatobecker Jan 18, 2021
1bd7117
Added final improvements to the codebase.
renatobecker Jan 18, 2021
6561b19
remove unnused import files.
renatobecker Jan 18, 2021
54a4907
Move hardcoded variables to const.
renatobecker Jan 18, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
16 changes: 14 additions & 2 deletions app/livechat/server/lib/RoutingManager.js
Expand Up @@ -13,6 +13,7 @@ import {
import { callbacks } from '../../../callbacks/server';
import { LivechatRooms, Rooms, Messages, Users, LivechatInquiry } from '../../../models/server';
import { Apps, AppEvents } from '../../../apps/server';
import { Livechat } from './Livechat';

export const RoutingManager = {
methodName: null,
Expand All @@ -37,11 +38,11 @@ export const RoutingManager = {
return this.getMethod().config || {};
},

async getNextAgent(department) {
async getNextAgent(department, ignoredUserId) {
murtaza98 marked this conversation as resolved.
Show resolved Hide resolved
let agent = callbacks.run('livechat.beforeGetNextAgent', department);

if (!agent) {
agent = await this.getMethod().getNextAgent(department);
agent = await this.getMethod().getNextAgent(department, ignoredUserId);
murtaza98 marked this conversation as resolved.
Show resolved Hide resolved
}

return agent;
Expand Down Expand Up @@ -151,6 +152,17 @@ export const RoutingManager = {
},

async transferRoom(room, guest, transferData) {
if (transferData.ignoredUserId) {
murtaza98 marked this conversation as resolved.
Show resolved Hide resolved
const agent = await RoutingManager.getNextAgent(transferData.departmentId, transferData.ignoredUserId);
murtaza98 marked this conversation as resolved.
Show resolved Hide resolved
if (agent) {
transferData.userId = agent.agentId;
return forwardRoomToAgent(room, transferData);
}
if (!RoutingManager.getConfig().autoAssignAgent) {
return Livechat.returnRoomAsInquiry(room._id, room.departmentId);
}
}

if (transferData.departmentId) {
return forwardRoomToDepartment(room, guest, transferData);
}
Expand Down
6 changes: 3 additions & 3 deletions app/livechat/server/lib/routing/AutoSelection.js
Expand Up @@ -19,12 +19,12 @@ class AutoSelection {
};
}

getNextAgent(department) {
getNextAgent(department, ignoredUserId) {
murtaza98 marked this conversation as resolved.
Show resolved Hide resolved
if (department) {
return LivechatDepartmentAgents.getNextAgentForDepartment(department);
return LivechatDepartmentAgents.getNextAgentForDepartment(department, ignoredUserId);
murtaza98 marked this conversation as resolved.
Show resolved Hide resolved
}

return Users.getNextAgent();
return Users.getNextAgent(ignoredUserId);
murtaza98 marked this conversation as resolved.
Show resolved Hide resolved
}

delegateAgent(agent) {
Expand Down
8 changes: 6 additions & 2 deletions app/livechat/server/lib/routing/External.js
Expand Up @@ -18,10 +18,14 @@ class ExternalQueue {
};
}

getNextAgent(department) {
getNextAgent(department, ignoredUserId) {
murtaza98 marked this conversation as resolved.
Show resolved Hide resolved
for (let i = 0; i < 10; i++) {
try {
const queryString = department ? `?departmentId=${ department }` : '';
let queryString = department ? `?departmentId=${ department }` : '';
if (ignoredUserId) {
const ignoredUserIdParam = `ignoredUserId=${ ignoredUserId }`;
queryString = queryString.startsWith('?') ? `${ queryString }&${ ignoredUserIdParam }` : `?${ ignoredUserIdParam }`;
}
const result = HTTP.call('GET', `${ settings.get('Livechat_External_Queue_URL') }${ queryString }`, {
headers: {
'User-Agent': 'RocketChat Server',
Expand Down
3 changes: 2 additions & 1 deletion app/models/server/models/LivechatDepartmentAgents.js
Expand Up @@ -54,7 +54,7 @@ export class LivechatDepartmentAgents extends Base {
this.remove({ departmentId });
}

getNextAgentForDepartment(departmentId) {
getNextAgentForDepartment(departmentId, ignoredUserId) {
murtaza98 marked this conversation as resolved.
Show resolved Hide resolved
const agents = this.findByDepartmentId(departmentId).fetch();

if (agents.length === 0) {
Expand All @@ -70,6 +70,7 @@ export class LivechatDepartmentAgents extends Base {
username: {
$in: onlineUsernames,
},
...ignoredUserId && { agentId: { $ne: ignoredUserId } },
murtaza98 marked this conversation as resolved.
Show resolved Hide resolved
};

const sort = {
Expand Down
7 changes: 5 additions & 2 deletions app/models/server/models/Users.js
Expand Up @@ -174,8 +174,11 @@ export class Users extends Base {
return this.find(query);
}

getNextAgent() {
const query = queryStatusAgentOnline();
getNextAgent(ignoredUserId) {
murtaza98 marked this conversation as resolved.
Show resolved Hide resolved
const extraFilters = {
...ignoredUserId && { _id: { $ne: { ignoredUserId } } },
murtaza98 marked this conversation as resolved.
Show resolved Hide resolved
};
const query = queryStatusAgentOnline(extraFilters);

const collectionObj = this.model.rawCollection();
const findAndModify = Meteor.wrapAsync(collectionObj.findAndModify, collectionObj);
Expand Down
4 changes: 2 additions & 2 deletions app/models/server/raw/Users.js
Expand Up @@ -133,9 +133,9 @@ export class UsersRaw extends BaseRaw {
return this.col.distinct('federation.origin', { federation: { $exists: true } });
}

async getNextLeastBusyAgent(department) {
async getNextLeastBusyAgent(department, ignoredUserId) {
murtaza98 marked this conversation as resolved.
Show resolved Hide resolved
const aggregate = [
{ $match: { status: { $exists: true, $ne: 'offline' }, statusLivechat: 'available', roles: 'livechat-agent' } },
{ $match: { status: { $exists: true, $ne: 'offline' }, statusLivechat: 'available', roles: 'livechat-agent', ...ignoredUserId && { _id: { $ne: ignoredUserId } } } },
murtaza98 marked this conversation as resolved.
Show resolved Hide resolved
{ $lookup: {
from: 'rocketchat_subscription',
let: { id: '$_id' },
Expand Down
58 changes: 58 additions & 0 deletions ee/app/livechat-enterprise/server/hooks/scheduleAutoTransfer.ts
@@ -0,0 +1,58 @@
import { AutoTransferMonitor } from '../lib/AutoTransferMonitor';
import { callbacks } from '../../../../../app/callbacks/server';
import { settings } from '../../../../../app/settings/server';
import { RoutingManager } from '../../../../../app/livechat/server/lib/RoutingManager';


const scheduleAutoTransferJob = async (roomId: string): Promise<any> => {
if (!roomId || roomId.length <= 0) {
murtaza98 marked this conversation as resolved.
Show resolved Hide resolved
return;
}

const timeout = settings.get('Livechat_auto_transfer_chat_if_no_response_routing');
if (!timeout || timeout <= 0) {
return;
}

await AutoTransferMonitor.Instance.startMonitoring(roomId, timeout as number);
murtaza98 marked this conversation as resolved.
Show resolved Hide resolved
};

const handleLivechatNewRoomCallback = async (room: any = {}): Promise<any> => {
murtaza98 marked this conversation as resolved.
Show resolved Hide resolved
if (room && RoutingManager.getConfig().autoAssignAgent) {
const { _id } = room;
await scheduleAutoTransferJob(_id);
}
return room;
};

const handleAfterTakeInquiryCallback = async (inquiry: any = {}): Promise<any> => {
if (inquiry && !RoutingManager.getConfig().autoAssignAgent) {
murtaza98 marked this conversation as resolved.
Show resolved Hide resolved
const { rid } = inquiry;
await scheduleAutoTransferJob(rid);
murtaza98 marked this conversation as resolved.
Show resolved Hide resolved
}
return inquiry;
};

const cancelAutoTransferJob = async (message: any = {}, room: any = {}): Promise<any> => {
const { _id: rid, t } = room;
const { token } = message;

if (!rid || !message || rid === '' || t !== 'l' || token) {
return;
}

await AutoTransferMonitor.Instance.stopMonitoring(rid);
murtaza98 marked this conversation as resolved.
Show resolved Hide resolved
};

settings.get('Livechat_auto_transfer_chat_if_no_response_routing', function(_, value) {
if (!value || value === 0) {
callbacks.remove('livechat.newRoom', 'livechat-schedule-auto-transfer-job');
murtaza98 marked this conversation as resolved.
Show resolved Hide resolved
callbacks.remove('livechat.afterTakeInquiry', 'livechat-livechat-auto-transfer-job-inquiry');
callbacks.remove('afterSaveMessage', 'livechat-cancel-auto-transfer-job');
return;
}

callbacks.add('livechat.newRoom', handleLivechatNewRoomCallback, callbacks.priority.MEDIUM, 'livechat-schedule-auto-transfer-job');
murtaza98 marked this conversation as resolved.
Show resolved Hide resolved
callbacks.add('livechat.afterTakeInquiry', handleAfterTakeInquiryCallback, callbacks.priority.MEDIUM, 'livechat-livechat-auto-transfer-job-inquiry');
callbacks.add('afterSaveMessage', cancelAutoTransferJob, callbacks.priority.HIGH, 'livechat-cancel-auto-transfer-job');
});
1 change: 1 addition & 0 deletions ee/app/livechat-enterprise/server/index.js
Expand Up @@ -25,6 +25,7 @@ import './hooks/onCheckRoomParamsApi';
import './hooks/onLoadConfigApi';
import './hooks/onCloseLivechat';
import './hooks/onSaveVisitorInfo';
import './hooks/scheduleAutoTransfer';
import './lib/routing/LoadBalancing';
import { onLicense } from '../../license/server';
import './business-hour';
Expand Down
52 changes: 52 additions & 0 deletions ee/app/livechat-enterprise/server/lib/AutoTransferMonitor.ts
@@ -0,0 +1,52 @@
import Agenda from 'agenda';
import { MongoInternals } from 'meteor/mongo';

import { Users } from '../../../../../app/models/server';
import { autoTransferVisitorJob } from './jobs';
murtaza98 marked this conversation as resolved.
Show resolved Hide resolved

export class AutoTransferMonitor {
private static _instance: AutoTransferMonitor;
murtaza98 marked this conversation as resolved.
Show resolved Hide resolved

schedular: Agenda;
murtaza98 marked this conversation as resolved.
Show resolved Hide resolved

userToPerformAutomaticTransfer: any;

private constructor(schedular: Agenda) {
this.schedular = schedular;
}

public static get Instance(): AutoTransferMonitor {
murtaza98 marked this conversation as resolved.
Show resolved Hide resolved
if (!this._instance) {
const schedular = new Agenda({
mongo: (MongoInternals.defaultRemoteCollectionDriver().mongo as any).client.db(),
db: { collection: 'livechat_scheduler' },
// this ensures the same job doesn't get executed multiple times in a cluster
defaultConcurrency: 1,
});
this._instance = new this(schedular);
this._instance.schedular.start();
const user = Users.findOneById('rocket.cat');
this._instance.userToPerformAutomaticTransfer = user;
}
return this._instance;
}

public async startMonitoring(roomId: string, timeout: number): Promise<void> {
const jobName = `livechat-auto-transfer-${ roomId }`;
const when = this.addMinutesToDate(new Date(), timeout);

this.schedular.define(jobName, autoTransferVisitorJob);

await this.schedular.schedule(when, jobName, { roomId, transferredBy: this.userToPerformAutomaticTransfer });
}

public async stopMonitoring(roomId: string): Promise<void> {
murtaza98 marked this conversation as resolved.
Show resolved Hide resolved
const jobName = `livechat-auto-transfer-${ roomId }`;

await this.schedular.cancel({ name: jobName });
}

private addMinutesToDate(date: Date, minutes: number): Date {
return new Date(date.getTime() + minutes * 1000 * 60);
}
}
murtaza98 marked this conversation as resolved.
Show resolved Hide resolved
23 changes: 23 additions & 0 deletions ee/app/livechat-enterprise/server/lib/Helper.js
Expand Up @@ -10,12 +10,14 @@ import {
LivechatRooms,
Messages,
LivechatCustomField,
LivechatVisitors,
} from '../../../../../app/models/server';
import { Rooms as RoomRaw } from '../../../../../app/models/server/raw';
import { settings } from '../../../../../app/settings';
import { RoutingManager } from '../../../../../app/livechat/server/lib/RoutingManager';
import { dispatchAgentDelegated } from '../../../../../app/livechat/server/lib/Helper';
import notifications from '../../../../../app/notifications/server/lib/Notifications';
import { Livechat } from '../../../../../app/livechat/server';

export const getMaxNumberSimultaneousChat = ({ agentId, departmentId }) => {
if (agentId) {
Expand Down Expand Up @@ -244,3 +246,24 @@ export const getLivechatQueueInfo = async (room) => {

return normalizeQueueInfo(inq);
};

export const transferToNewAgent = async (roomId, transferredBy) => {
const room = await LivechatRooms.findOneById(roomId);
const timeout = await settings.get('Livechat_auto_transfer_chat_if_no_response_routing');

const { departmentId, v: { token }, servedBy: { _id: ignoredUserId, username } = {} } = room;
murtaza98 marked this conversation as resolved.
Show resolved Hide resolved

const guest = await LivechatVisitors.getVisitorByToken(token, {});
const transferData = {
ignoredUserId,
murtaza98 marked this conversation as resolved.
Show resolved Hide resolved
departmentId,
transferredBy,
comment: `The chat was transferred because ${ username } had not replied for ${ timeout } minutes`,
};

try {
await Livechat.transfer(room, guest, transferData);
} catch (err) {
console.error(`Error occurred while transferring chat. Details: ${ err.message }`);
}
};
10 changes: 10 additions & 0 deletions ee/app/livechat-enterprise/server/lib/jobs.ts
@@ -0,0 +1,10 @@
import { transferToNewAgent } from './Helper';

export const autoTransferVisitorJob = async ({ attrs: { data } }: any = {}): Promise<void> => {
murtaza98 marked this conversation as resolved.
Show resolved Hide resolved
const { roomId, transferredBy } = data;
try {
await transferToNewAgent(roomId, transferredBy);
} catch (err) {
console.error(err);
}
};
Expand Up @@ -19,8 +19,8 @@ class LoadBalancing {
};
}

async getNextAgent(department) {
const nextAgent = await Users.getNextLeastBusyAgent(department);
async getNextAgent(department, ignoredUserId) {
murtaza98 marked this conversation as resolved.
Show resolved Hide resolved
const nextAgent = await Users.getNextLeastBusyAgent(department, ignoredUserId);
murtaza98 marked this conversation as resolved.
Show resolved Hide resolved
if (!nextAgent) {
return;
}
Expand Down
13 changes: 13 additions & 0 deletions ee/app/livechat-enterprise/server/settings.js
Expand Up @@ -90,6 +90,19 @@ export const createSettings = () => {
],
});

settings.add('Livechat_auto_transfer_chat_if_no_response_routing', 0, {
murtaza98 marked this conversation as resolved.
Show resolved Hide resolved
type: 'int',
group: 'Omnichannel',
section: 'Routing',
murtaza98 marked this conversation as resolved.
Show resolved Hide resolved
i18nLabel: 'Livechat_Auto_transfer_chat_if_no_response',
murtaza98 marked this conversation as resolved.
Show resolved Hide resolved
i18nDescription: 'Livechat_Auto_transfer_chat_if_no_response_description',
enterprise: true,
invalidValue: 0,
modules: [
'livechat-enterprise',
],
});

settings.addGroup('Omnichannel', function() {
this.section('Business_Hours', function() {
this.add('Livechat_business_hour_type', 'Single', {
Expand Down
2 changes: 2 additions & 0 deletions packages/rocketchat-i18n/i18n/en.i18n.json
Expand Up @@ -2338,6 +2338,8 @@
"Livechat_Agents": "Agents",
"Livechat_AllowedDomainsList": "Livechat Allowed Domains",
"Livechat_Appearance": "Livechat Appearance",
"Livechat_Auto_transfer_chat_if_no_response": "Auto-Transfer to another agent, if the current agent hasn't responsed in the specified time (in minutes)",
murtaza98 marked this conversation as resolved.
Show resolved Hide resolved
"Livechat_Auto_transfer_chat_if_no_response_description" : "This applied when the chat has just started. All the following responses can be longer and won't result in transfer",
murtaza98 marked this conversation as resolved.
Show resolved Hide resolved
"Livechat_business_hour_type": "Business Hour Type (Single or Multiple)",
"Livechat_chat_transcript_sent": "Chat transcript sent: __transcript__",
"Livechat_custom_fields_options_placeholder": "Comma-separated list used to select a pre-configured value. Spaces between elements are not accepted.",
Expand Down