The communications platform that puts data protection first.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
Rocket.Chat/apps/meteor/app/livechat/server/lib/Helper.js

606 lines
18 KiB

import { Meteor } from 'meteor/meteor';
import { Match, check } from 'meteor/check';
import { LivechatTransferEventType } from '@rocket.chat/apps-engine/definition/livechat';
import { OmnichannelSourceType, DEFAULT_SLA_CONFIG } from '@rocket.chat/core-typings';
import { LivechatPriorityWeight } from '@rocket.chat/core-typings/src/ILivechatPriority';
import { api, Message } from '@rocket.chat/core-services';
import {
LivechatDepartmentAgents,
LivechatInquiry,
LivechatRooms,
LivechatDepartment,
Subscriptions,
Rooms,
Users,
} from '@rocket.chat/models';
import { hasRoleAsync } from '../../../authorization/server/functions/hasRole';
import { Livechat } from './Livechat';
import { Livechat as LivechatTyped } from './LivechatTyped';
import { RoutingManager } from './RoutingManager';
import { callbacks } from '../../../../lib/callbacks';
import { Logger } from '../../../logger/server';
import { settings } from '../../../settings/server';
import { Apps, AppEvents } from '../../../../ee/server/apps';
import { sendNotification } from '../../../lib/server';
import { sendMessage } from '../../../lib/server/functions/sendMessage';
import { queueInquiry, saveQueueInquiry } from './QueueManager';
import { validateEmail as validatorFunc } from '../../../../lib/emailValidator';
import { i18n } from '../../../../server/lib/i18n';
const logger = new Logger('LivechatHelper');
export const allowAgentSkipQueue = (agent) => {
check(
agent,
Match.ObjectIncluding({
agentId: String,
}),
);
return hasRoleAsync(agent.agentId, 'bot');
};
export const createLivechatRoom = async (rid, name, guest, roomInfo = {}, extraData = {}) => {
check(rid, String);
check(name, String);
check(
guest,
Match.ObjectIncluding({
_id: String,
username: String,
status: Match.Maybe(String),
department: Match.Maybe(String),
}),
);
const extraRoomInfo = await callbacks.run('livechat.beforeRoom', roomInfo, extraData);
const { _id, username, token, department: departmentId, status = 'online' } = guest;
const newRoomAt = new Date();
logger.debug(`Creating livechat room for visitor ${_id}`);
const room = Object.assign(
{
_id: rid,
msgs: 0,
usersCount: 1,
lm: newRoomAt,
fname: name,
t: 'l',
ts: newRoomAt,
departmentId,
v: {
_id,
username,
token,
status,
},
cl: false,
open: true,
waitingResponse: true,
// this should be overriden by extraRoomInfo when provided
// in case it's not provided, we'll use this "default" type
source: {
type: OmnichannelSourceType.OTHER,
alias: 'unknown',
},
queuedAt: newRoomAt,
priorityWeight: LivechatPriorityWeight.NOT_SPECIFIED,
estimatedWaitingTimeQueue: DEFAULT_SLA_CONFIG.ESTIMATED_WAITING_TIME_QUEUE,
},
extraRoomInfo,
);
const roomId = (await Rooms.insertOne(room)).insertedId;
Apps.triggerEvent(AppEvents.IPostLivechatRoomStarted, room);
await callbacks.run('livechat.newRoom', room);
await sendMessage(guest, { t: 'livechat-started', msg: '', groupable: false }, room);
return roomId;
};
export const createLivechatInquiry = async ({ rid, name, guest, message, initialStatus, extraData = {} }) => {
check(rid, String);
check(name, String);
check(
guest,
Match.ObjectIncluding({
_id: String,
username: String,
status: Match.Maybe(String),
department: Match.Maybe(String),
}),
);
check(
message,
Match.ObjectIncluding({
msg: String,
}),
);
const extraInquiryInfo = await callbacks.run('livechat.beforeInquiry', extraData);
const { _id, username, token, department, status = 'online' } = guest;
const { msg } = message;
const ts = new Date();
logger.debug(`Creating livechat inquiry for visitor ${_id}`);
const inquiry = {
rid,
name,
ts,
department,
message: msg,
status: initialStatus || 'ready',
v: {
_id,
username,
token,
status,
},
t: 'l',
priorityWeight: LivechatPriorityWeight.NOT_SPECIFIED,
estimatedWaitingTimeQueue: DEFAULT_SLA_CONFIG.ESTIMATED_WAITING_TIME_QUEUE,
...extraInquiryInfo,
};
const result = (await LivechatInquiry.insertOne(inquiry)).insertedId;
logger.debug(`Inquiry ${result} created for visitor ${_id}`);
return result;
};
export const createLivechatSubscription = async (rid, name, guest, agent, department) => {
check(rid, String);
check(name, String);
check(
guest,
Match.ObjectIncluding({
_id: String,
username: String,
status: Match.Maybe(String),
}),
);
check(
agent,
Match.ObjectIncluding({
agentId: String,
username: String,
}),
);
const existingSubscription = await Subscriptions.findOneByRoomIdAndUserId(rid, agent.agentId);
if (existingSubscription?._id) {
return existingSubscription;
}
const { _id, username, token, status = 'online' } = guest;
const subscriptionData = {
rid,
fname: name,
alert: true,
open: true,
unread: 1,
userMentions: 1,
groupMentions: 0,
u: {
_id: agent.agentId,
username: agent.username,
},
t: 'l',
desktopNotifications: 'all',
mobilePushNotifications: 'all',
emailNotifications: 'all',
v: {
_id,
username,
token,
status,
},
...(department && { department }),
};
return Subscriptions.insertOne(subscriptionData);
};
export const removeAgentFromSubscription = async (rid, { _id, username }) => {
const room = await LivechatRooms.findOneById(rid);
const user = await Users.findOneById(_id);
await Subscriptions.removeByRoomIdAndUserId(rid, _id);
await Message.saveSystemMessage('ul', rid, username, { _id: user._id, username: user.username, name: user.name });
setImmediate(() => {
Apps.triggerEvent(AppEvents.IPostLivechatAgentUnassigned, { room, user });
});
};
export const parseAgentCustomFields = (customFields) => {
if (!customFields) {
return;
}
const externalCustomFields = () => {
const accountCustomFields = settings.get('Accounts_CustomFields');
if (!accountCustomFields || accountCustomFields.trim() === '') {
return [];
}
try {
const parseCustomFields = JSON.parse(accountCustomFields);
return Object.keys(parseCustomFields).filter((customFieldKey) => parseCustomFields[customFieldKey].sendToIntegrations === true);
} catch (error) {
Livechat.logger.error(error);
return [];
}
};
const externalCF = externalCustomFields();
return Object.keys(customFields).reduce(
(newObj, key) => (externalCF.includes(key) ? { ...newObj, [key]: customFields[key] } : newObj),
null,
);
};
export const normalizeAgent = async (agentId) => {
if (!agentId) {
return;
}
if (!settings.get('Livechat_show_agent_info')) {
return { hiddenInfo: true };
}
const agent = await Users.getAgentInfo(agentId, settings.get('Livechat_show_agent_email'));
const { customFields: agentCustomFields, ...extraData } = agent;
const customFields = parseAgentCustomFields(agentCustomFields);
return Object.assign(extraData, { ...(customFields && { customFields }) });
};
export const dispatchAgentDelegated = async (rid, agentId) => {
const agent = await normalizeAgent(agentId);
void api.broadcast('omnichannel.room', rid, {
type: 'agentData',
data: agent,
});
};
export const dispatchInquiryQueued = async (inquiry, agent) => {
if (!inquiry?._id) {
return;
}
logger.debug(`Notifying agents of new inquiry ${inquiry._id} queued`);
const { department, rid, v } = inquiry;
const room = await LivechatRooms.findOneById(rid);
setImmediate(() => callbacks.run('livechat.chatQueued', room));
if (RoutingManager.getConfig().autoAssignAgent) {
return;
}
if (!agent || !(await allowAgentSkipQueue(agent))) {
await saveQueueInquiry(inquiry);
}
// Alert only the online agents of the queued request
const onlineAgents = await LivechatTyped.getOnlineAgents(department, agent);
if (!onlineAgents) {
logger.debug('Cannot notify agents of queued inquiry. No online agents found');
return;
}
logger.debug(`Notifying ${await onlineAgents.count()} agents of new inquiry`);
const notificationUserName = v && (v.name || v.username);
for await (const agent of onlineAgents) {
const { _id, active, emails, language, status, statusConnection, username } = agent;
await sendNotification({
// fake a subscription in order to make use of the function defined above
subscription: {
rid,
t: 'l',
u: {
_id,
},
receiver: [
{
active,
emails,
language,
status,
statusConnection,
username,
},
],
},
sender: v,
hasMentionToAll: true, // consider all agents to be in the room
hasMentionToHere: false,
message: Object.assign({}, { u: v }),
// we should use server's language for this type of messages instead of user's
notificationMessage: i18n.t('User_started_a_new_conversation', { username: notificationUserName }, language),
room: Object.assign(room, { name: i18n.t('New_chat_in_queue', {}, language) }),
mentionIds: [],
});
}
};
export const forwardRoomToAgent = async (room, transferData) => {
if (!room || !room.open) {
return false;
}
logger.debug(`Forwarding room ${room._id} to agent ${transferData.userId}`);
const { userId: agentId, clientAction } = transferData;
const user = await Users.findOneOnlineAgentById(agentId);
if (!user) {
logger.debug(`Agent ${agentId} is offline. Cannot forward`);
throw new Error('error-user-is-offline');
}
const { _id: rid, servedBy: oldServedBy } = room;
const inquiry = await LivechatInquiry.findOneByRoomId(rid);
if (!inquiry) {
logger.debug(`No inquiries found for room ${room._id}. Cannot forward`);
throw new Error('error-invalid-inquiry');
}
if (oldServedBy && agentId === oldServedBy._id) {
throw new Error('error-selected-agent-room-agent-are-same');
}
const { username } = user;
const agent = { agentId, username };
// Remove department from inquiry to make sure the routing algorithm treat this as forwarding to agent and not as forwarding to department
inquiry.department = undefined;
// There are some Enterprise features that may interrupt the forwarding process
// Due to that we need to check whether the agent has been changed or not
logger.debug(`Forwarding inquiry ${inquiry._id} to agent ${agent._id}`);
const roomTaken = await RoutingManager.takeInquiry(inquiry, agent, {
...(clientAction && { clientAction }),
});
if (!roomTaken) {
logger.debug(`Cannot forward inquiry ${inquiry._id}`);
return false;
}
await Livechat.saveTransferHistory(room, transferData);
const { servedBy } = roomTaken;
if (servedBy) {
if (oldServedBy && servedBy._id !== oldServedBy._id) {
await RoutingManager.removeAllRoomSubscriptions(room, servedBy);
}
setImmediate(() => {
Apps.triggerEvent(AppEvents.IPostLivechatRoomTransferred, {
type: LivechatTransferEventType.AGENT,
room: rid,
from: oldServedBy?._id,
to: servedBy._id,
});
});
}
logger.debug(`Inquiry ${inquiry._id} taken by agent ${agent._id}`);
await callbacks.run('livechat.afterForwardChatToAgent', { rid, servedBy, oldServedBy });
return true;
};
export const updateChatDepartment = async ({ rid, newDepartmentId, oldDepartmentId }) => {
await LivechatRooms.changeDepartmentIdByRoomId(rid, newDepartmentId);
await LivechatInquiry.changeDepartmentIdByRoomId(rid, newDepartmentId);
await Subscriptions.changeDepartmentByRoomId(rid, newDepartmentId);
setImmediate(() => {
Apps.triggerEvent(AppEvents.IPostLivechatRoomTransferred, {
type: LivechatTransferEventType.DEPARTMENT,
room: rid,
from: oldDepartmentId,
to: newDepartmentId,
});
});
return callbacks.run('livechat.afterForwardChatToDepartment', {
rid,
newDepartmentId,
oldDepartmentId,
});
};
export const forwardRoomToDepartment = async (room, guest, transferData) => {
if (!room || !room.open) {
return false;
}
logger.debug(`Attempting to forward room ${room._id} to department ${transferData.departmentId}`);
await callbacks.run('livechat.beforeForwardRoomToDepartment', { room, transferData });
const { _id: rid, servedBy: oldServedBy, departmentId: oldDepartmentId } = room;
let agent = null;
const inquiry = await LivechatInquiry.findOneByRoomId(rid);
if (!inquiry) {
logger.debug(`Cannot forward room ${room._id}. No inquiries found`);
throw new Error('error-transferring-inquiry');
}
const { departmentId } = transferData;
if (oldDepartmentId === departmentId) {
throw new Error('error-forwarding-chat-same-department');
}
const { userId: agentId, clientAction } = transferData;
if (agentId) {
logger.debug(`Forwarding room ${room._id} to department ${departmentId} (to user ${agentId})`);
let user = await Users.findOneOnlineAgentById(agentId);
if (!user) {
throw new Error('error-user-is-offline');
}
user = await LivechatDepartmentAgents.findOneByAgentIdAndDepartmentId(agentId, departmentId);
if (!user) {
throw new Error('error-user-not-belong-to-department');
}
const { username } = user;
agent = { agentId, username };
}
if (!RoutingManager.getConfig().autoAssignAgent) {
logger.debug(
`Routing algorithm doesn't support auto assignment (using ${RoutingManager.methodName}). Chat will be on department queue`,
);
await Livechat.saveTransferHistory(room, transferData);
return RoutingManager.unassignAgent(inquiry, departmentId);
}
// Fake the department to forward the inquiry - Case the forward process does not success
// the inquiry will stay in the same original department
inquiry.department = departmentId;
const roomTaken = await RoutingManager.delegateInquiry(inquiry, agent, {
forwardingToDepartment: { oldDepartmentId },
...(clientAction && { clientAction }),
});
if (!roomTaken) {
logger.debug(`Cannot forward room ${room._id}. Unable to delegate inquiry`);
return false;
}
const { servedBy, chatQueued } = roomTaken;
if (!chatQueued && oldServedBy && servedBy && oldServedBy._id === servedBy._id) {
const department = await LivechatDepartment.findOneById(departmentId);
if (!department?.fallbackForwardDepartment?.length) {
logger.debug(`Cannot forward room ${room._id}. Chat assigned to agent ${servedBy._id} (Previous was ${oldServedBy._id})`);
throw new Error('error-no-agents-online-in-department');
}
// if a chat has a fallback department, attempt to redirect chat to there [EE]
return !!callbacks.run('livechat:onTransferFailure', { room, guest, transferData });
}
await Livechat.saveTransferHistory(room, transferData);
if (oldServedBy) {
// if chat is queued then we don't ignore the new servedBy agent bcs at this
// point the chat is not assigned to him/her and it is still in the queue
await RoutingManager.removeAllRoomSubscriptions(room, !chatQueued && servedBy);
}
if (!chatQueued && servedBy) {
await Message.saveSystemMessage('uj', rid, servedBy.username, servedBy);
}
await updateChatDepartment({ rid, newDepartmentId: departmentId, oldDepartmentId });
if (chatQueued) {
logger.debug(`Forwarding succesful. Marking inquiry ${inquiry._id} as ready`);
await LivechatInquiry.readyInquiry(inquiry._id);
await LivechatRooms.removeAgentByRoomId(rid);
await dispatchAgentDelegated(rid, null);
const newInquiry = await LivechatInquiry.findOneById(inquiry._id);
await queueInquiry(newInquiry);
logger.debug(`Inquiry ${inquiry._id} queued succesfully`);
}
const { token } = guest;
await LivechatTyped.setDepartmentForGuest({ token, department: departmentId });
logger.debug(`Department for visitor with token ${token} was updated to ${departmentId}`);
return true;
};
export const normalizeTransferredByData = (transferredBy, room) => {
if (!transferredBy || !room) {
throw new Error('You must provide "transferredBy" and "room" params to "getTransferredByData"');
}
const { servedBy: { _id: agentId } = {} } = room;
const { _id, username, name, userType: transferType } = transferredBy;
const type = transferType || (_id === agentId ? 'agent' : 'user');
return {
_id,
username,
...(name && { name }),
type,
};
};
export const checkServiceStatus = async ({ guest, agent }) => {
if (!agent) {
return LivechatTyped.online(guest.department);
}
const { agentId } = agent;
const users = await Users.countOnlineAgents(agentId);
return users > 0;
};
export const updateDepartmentAgents = async (departmentId, agents, departmentEnabled) => {
check(departmentId, String);
check(
agents,
Match.ObjectIncluding({
upsert: Match.Maybe(Array),
remove: Match.Maybe(Array),
}),
);
const { upsert = [], remove = [] } = agents;
const agentsRemoved = [];
const agentsAdded = [];
for await (const { agentId } of remove) {
await LivechatDepartmentAgents.removeByDepartmentIdAndAgentId(departmentId, agentId);
agentsRemoved.push(agentId);
}
if (agentsRemoved.length > 0) {
callbacks.runAsync('livechat.removeAgentDepartment', { departmentId, agentsId: agentsRemoved });
}
for await (const agent of upsert) {
const agentFromDb = await Users.findOneById(agent.agentId, { projection: { _id: 1, username: 1 } });
if (!agentFromDb) {
continue;
}
await LivechatDepartmentAgents.saveAgent({
agentId: agent.agentId,
departmentId,
username: agentFromDb.username,
count: agent.count ? parseInt(agent.count) : 0,
order: agent.order ? parseInt(agent.order) : 0,
departmentEnabled,
});
agentsAdded.push(agent.agentId);
}
if (agentsAdded.length > 0) {
callbacks.runAsync('livechat.saveAgentDepartment', {
departmentId,
agentsId: agentsAdded,
});
}
if (agentsRemoved.length > 0 || agentsAdded.length > 0) {
const numAgents = await LivechatDepartmentAgents.countByDepartmentId(departmentId);
await LivechatDepartment.updateNumAgentsById(departmentId, numAgents);
}
return true;
};
export const validateEmail = (email) => {
if (!validatorFunc(email)) {
throw new Meteor.Error('error-invalid-email', `Invalid email ${email}`, {
function: 'Livechat.validateEmail',
email,
});
}
return true;
};