refactor: Break big `livechatTyped` into smaller files (#35324)

pull/35312/head^2
Kevin Aleman 11 months ago committed by GitHub
parent 424f61325d
commit a2460efa82
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 3
      apps/meteor/app/apps/server/bridges/livechat.ts
  2. 6
      apps/meteor/app/lib/server/functions/closeLivechatRoom.ts
  3. 8
      apps/meteor/app/lib/server/functions/closeOmnichannelConversations.ts
  4. 3
      apps/meteor/app/livechat/server/api/v1/room.ts
  5. 5
      apps/meteor/app/livechat/server/api/v1/transcript.ts
  6. 325
      apps/meteor/app/livechat/server/lib/LivechatTyped.ts
  7. 289
      apps/meteor/app/livechat/server/lib/closeRoom.ts
  8. 45
      apps/meteor/app/livechat/server/lib/sendTranscript.ts
  9. 3
      apps/meteor/app/livechat/server/lib/stream/agentStatus.ts
  10. 4
      apps/meteor/app/livechat/server/methods/closeRoom.ts
  11. 4
      apps/meteor/ee/app/livechat-enterprise/server/api/lib/contacts.ts
  12. 4
      apps/meteor/ee/app/livechat-enterprise/server/lib/AutoCloseOnHoldScheduler.ts
  13. 4
      apps/meteor/ee/app/livechat-enterprise/server/lib/QueueInactivityMonitor.ts
  14. 4
      apps/meteor/ee/app/livechat-enterprise/server/lib/VisitorInactivityMonitor.ts
  15. 2
      apps/meteor/ee/tests/unit/apps/livechat-enterprise/lib/AutoCloseOnHold.tests.ts
  16. 12
      apps/meteor/ee/tests/unit/apps/livechat-enterprise/server/lib/QueueInactivityMonitor.spec.ts
  17. 4
      apps/meteor/tests/unit/app/lib/server/functions/closeLivechatRoom.tests.ts

@ -11,6 +11,7 @@ import { LivechatVisitors, LivechatRooms, LivechatDepartment, Users } from '@roc
import { callbacks } from '../../../../lib/callbacks';
import { deasyncPromise } from '../../../../server/deasync/deasync';
import { Livechat as LivechatTyped } from '../../../livechat/server/lib/LivechatTyped';
import { closeRoom } from '../../../livechat/server/lib/closeRoom';
import { getRoomMessages } from '../../../livechat/server/lib/getRoomMessages';
import type { ILivechatMessage } from '../../../livechat/server/lib/localTypes';
import { settings } from '../../../settings/server';
@ -145,7 +146,7 @@ export class AppLivechatBridge extends LivechatBridge {
...(visitor && { visitor }),
};
await LivechatTyped.closeRoom(closeData);
await closeRoom(closeData);
return true;
}

@ -2,7 +2,7 @@ import type { IUser, IRoom, IOmnichannelRoom } from '@rocket.chat/core-typings';
import { LivechatRooms, Subscriptions } from '@rocket.chat/models';
import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission';
import { Livechat } from '../../../livechat/server/lib/LivechatTyped';
import { closeRoom } from '../../../livechat/server/lib/closeRoom';
import type { CloseRoomParams } from '../../../livechat/server/lib/localTypes';
export const closeLivechatRoom = async (
@ -65,7 +65,7 @@ export const closeLivechatRoom = async (
};
if (forceClose) {
return Livechat.closeRoom({
return closeRoom({
room,
user,
options,
@ -78,7 +78,7 @@ export const closeLivechatRoom = async (
throw new Error('error-room-already-closed');
}
return Livechat.closeRoom({
return closeRoom({
room,
user,
options,

@ -3,7 +3,7 @@ import { LivechatRooms } from '@rocket.chat/models';
import { callbacks } from '../../../../lib/callbacks';
import { i18n } from '../../../../server/lib/i18n';
import { Livechat } from '../../../livechat/server/lib/LivechatTyped';
import { closeRoom } from '../../../livechat/server/lib/closeRoom';
import { settings } from '../../../settings/server';
type SubscribedRooms = {
@ -13,7 +13,7 @@ type SubscribedRooms = {
export const closeOmnichannelConversations = async (user: IUser, subscribedRooms: SubscribedRooms[]): Promise<void> => {
const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {});
const roomsInfo = await LivechatRooms.findByIds(
const roomsInfo = LivechatRooms.findByIds(
subscribedRooms.map(({ rid }) => rid),
{},
extraQuery,
@ -22,8 +22,8 @@ export const closeOmnichannelConversations = async (user: IUser, subscribedRooms
const comment = i18n.t('Agent_deactivated', { lng: language });
const promises: Promise<void>[] = [];
await roomsInfo.forEach((room: any) => {
promises.push(Livechat.closeRoom({ user, room, comment }));
await roomsInfo.forEach((room) => {
promises.push(closeRoom({ user, room, comment }));
});
await Promise.all(promises);

@ -24,6 +24,7 @@ import { closeLivechatRoom } from '../../../../lib/server/functions/closeLivecha
import { settings as rcSettings } from '../../../../settings/server';
import { normalizeTransferredByData } from '../../lib/Helper';
import { Livechat as LivechatTyped } from '../../lib/LivechatTyped';
import { closeRoom } from '../../lib/closeRoom';
import type { CloseRoomParams } from '../../lib/localTypes';
import { livechatLogger } from '../../lib/logger';
import { findGuest, findRoom, settings, findAgent, onCheckRoomParams } from '../lib/livechat';
@ -180,7 +181,7 @@ API.v1.addRoute(
}
}
await LivechatTyped.closeRoom({ visitor, room, comment, options });
await closeRoom({ visitor, room, comment, options });
return API.v1.success({ rid, comment });
},

@ -5,8 +5,7 @@ import { isPOSTLivechatTranscriptParams, isPOSTLivechatTranscriptRequestParams }
import { i18n } from '../../../../../server/lib/i18n';
import { API } from '../../../../api/server';
import { Livechat } from '../../lib/LivechatTyped';
import { sendTranscript } from '../../lib/sendTranscript';
import { sendTranscript, requestTranscript } from '../../lib/sendTranscript';
API.v1.addRoute(
'livechat/transcript',
@ -66,7 +65,7 @@ API.v1.addRoute(
throw new Error('error-invalid-user');
}
await Livechat.requestTranscript({ rid, email, subject, user });
await requestTranscript({ rid, email, subject, user });
return API.v1.success();
},

@ -1,8 +1,7 @@
import { Apps, AppEvents } from '@rocket.chat/apps';
import { Message, VideoConf, api, Omnichannel } from '@rocket.chat/core-services';
import { Message, VideoConf, api } from '@rocket.chat/core-services';
import type {
IOmnichannelRoom,
IOmnichannelRoomClosingInfo,
IUser,
ILivechatVisitor,
SelectedAgent,
@ -12,14 +11,13 @@ import type {
AtLeast,
TransferData,
IOmnichannelAgent,
ILivechatInquiryRecord,
UserStatus,
IOmnichannelRoomInfo,
IOmnichannelRoomExtraData,
IOmnichannelSource,
ILivechatContactVisitorAssociation,
} from '@rocket.chat/core-typings';
import { ILivechatAgentStatus, isOmnichannelRoom } from '@rocket.chat/core-typings';
import { ILivechatAgentStatus } from '@rocket.chat/core-typings';
import { Logger, type MainLogger } from '@rocket.chat/logger';
import {
LivechatDepartment,
@ -38,12 +36,11 @@ import {
import { serverFetch as fetch } from '@rocket.chat/server-fetch';
import { Match, check } from 'meteor/check';
import { Meteor } from 'meteor/meteor';
import type { Filter, ClientSession } from 'mongodb';
import type { Filter } from 'mongodb';
import UAParser from 'ua-parser-js';
import { callbacks } from '../../../../lib/callbacks';
import { trim } from '../../../../lib/utils/stringUtils';
import { client, shouldRetryTransaction } from '../../../../server/database/utils';
import { i18n } from '../../../../server/lib/i18n';
import { addUserRolesAsync } from '../../../../server/lib/roles/addUserRoles';
import { removeUserFromRolesAsync } from '../../../../server/lib/roles/removeUserFromRoles';
@ -72,8 +69,7 @@ import { RoutingManager } from './RoutingManager';
import { Visitors, type RegisterGuestType } from './Visitors';
import { registerGuestData } from './contacts/registerGuestData';
import { getRequiredDepartment } from './departmentsLib';
import type { CloseRoomParams, CloseRoomParamsByUser, CloseRoomParamsByVisitor, ILivechatMessage } from './localTypes';
import { parseTranscriptRequest } from './parseTranscriptRequest';
import type { ILivechatMessage } from './localTypes';
type AKeyOf<T> = {
[K in keyof T]?: T[K];
@ -99,13 +95,6 @@ type ICRMData = {
crmData?: IOmnichannelRoom['crmData'];
};
type ChatCloser = { _id: string; username: string | undefined };
const isRoomClosedByUserParams = (params: CloseRoomParams): params is CloseRoomParamsByUser =>
(params as CloseRoomParamsByUser).user !== undefined;
const isRoomClosedByVisitorParams = (params: CloseRoomParams): params is CloseRoomParamsByVisitor =>
(params as CloseRoomParamsByVisitor).visitor !== undefined;
class LivechatClass {
logger: Logger;
@ -140,192 +129,6 @@ class LivechatClass {
return agentsOnline;
}
async closeRoom(params: CloseRoomParams, attempts = 2): Promise<void> {
let newRoom: IOmnichannelRoom;
let chatCloser: ChatCloser;
let removedInquiryObj: ILivechatInquiryRecord | null;
const session = client.startSession();
try {
session.startTransaction();
const { room, closedBy, removedInquiry } = await this.doCloseRoom(params, session);
await session.commitTransaction();
newRoom = room;
chatCloser = closedBy;
removedInquiryObj = removedInquiry;
} catch (e) {
this.logger.error({ err: e, msg: 'Failed to close room', afterAttempts: attempts });
await session.abortTransaction();
// Dont propagate transaction errors
if (shouldRetryTransaction(e)) {
if (attempts > 0) {
this.logger.debug(`Retrying close room because of transient error. Attempts left: ${attempts}`);
return this.closeRoom(params, attempts - 1);
}
throw new Error('error-room-cannot-be-closed-try-again');
}
throw e;
} finally {
await session.endSession();
}
// Note: when reaching this point, the room has been closed
// Transaction is commited and so these messages can be sent here.
return this.afterRoomClosed(newRoom, chatCloser, removedInquiryObj, params);
}
async afterRoomClosed(
newRoom: IOmnichannelRoom,
chatCloser: ChatCloser,
inquiry: ILivechatInquiryRecord | null,
params: CloseRoomParams,
): Promise<void> {
if (!chatCloser) {
// this should never happen
return;
}
// Note: we are okay with these messages being sent outside of the transaction. The process of sending a message
// is huge and involves multiple db calls. Making it transactionable this way would be really hard.
// And passing just _some_ actions to the transaction creates some deadlocks since messages are updated in the afterSaveMessages callbacks.
const transcriptRequested =
!!params.room.transcriptRequest || (!settings.get('Livechat_enable_transcript') && settings.get('Livechat_transcript_send_always'));
this.logger.debug(`Sending closing message to room ${newRoom._id}`);
await Message.saveSystemMessageAndNotifyUser('livechat-close', newRoom._id, params.comment ?? '', chatCloser, {
groupable: false,
transcriptRequested,
...(isRoomClosedByVisitorParams(params) && { token: params.visitor.token }),
});
if (settings.get('Livechat_enable_transcript') && !settings.get('Livechat_transcript_send_always')) {
await Message.saveSystemMessage('command', newRoom._id, 'promptTranscript', chatCloser);
}
this.logger.debug(`Running callbacks for room ${newRoom._id}`);
process.nextTick(() => {
/**
* @deprecated the `AppEvents.ILivechatRoomClosedHandler` event will be removed
* in the next major version of the Apps-Engine
*/
void Apps.self?.getBridges()?.getListenerBridge().livechatEvent(AppEvents.ILivechatRoomClosedHandler, newRoom);
void Apps.self?.getBridges()?.getListenerBridge().livechatEvent(AppEvents.IPostLivechatRoomClosed, newRoom);
});
const visitor = isRoomClosedByVisitorParams(params) ? params.visitor : undefined;
const opts = await parseTranscriptRequest(params.room, params.options, visitor);
if (process.env.TEST_MODE) {
await callbacks.run('livechat.closeRoom', {
room: newRoom,
options: opts,
});
} else {
callbacks.runAsync('livechat.closeRoom', {
room: newRoom,
options: opts,
});
}
void notifyOnRoomChangedById(newRoom._id);
if (inquiry) {
void notifyOnLivechatInquiryChanged(inquiry, 'removed');
}
this.logger.debug(`Room ${newRoom._id} was closed`);
}
async doCloseRoom(
params: CloseRoomParams,
session: ClientSession,
): Promise<{ room: IOmnichannelRoom; closedBy: ChatCloser; removedInquiry: ILivechatInquiryRecord | null }> {
const { comment } = params;
const { room, forceClose } = params;
this.logger.debug({ msg: `Attempting to close room`, roomId: room._id, forceClose });
if (!room || !isOmnichannelRoom(room) || (!forceClose && !room.open)) {
this.logger.debug(`Room ${room._id} is not open`);
throw new Error('error-room-closed');
}
const commentRequired = settings.get('Livechat_request_comment_when_closing_conversation');
if (commentRequired && !comment?.trim()) {
throw new Error('error-comment-is-required');
}
const { updatedOptions: options } = await this.resolveChatTags(room, params.options);
this.logger.debug(`Resolved chat tags for room ${room._id}`);
const now = new Date();
const { _id: rid, servedBy } = room;
const serviceTimeDuration = servedBy && (now.getTime() - new Date(servedBy.ts).getTime()) / 1000;
const closeData: IOmnichannelRoomClosingInfo = {
closedAt: now,
chatDuration: (now.getTime() - new Date(room.ts).getTime()) / 1000,
...(serviceTimeDuration && { serviceTimeDuration }),
...options,
};
this.logger.debug(`Room ${room._id} was closed at ${closeData.closedAt} (duration ${closeData.chatDuration})`);
if (isRoomClosedByUserParams(params)) {
const { user } = params;
this.logger.debug(`Closing by user ${user?._id}`);
closeData.closer = 'user';
closeData.closedBy = {
_id: user?._id || '',
username: user?.username,
};
} else if (isRoomClosedByVisitorParams(params)) {
const { visitor } = params;
this.logger.debug(`Closing by visitor ${params.visitor._id}`);
closeData.closer = 'visitor';
closeData.closedBy = {
_id: visitor._id,
username: visitor.username,
};
} else {
throw new Error('Error: Please provide details of the user or visitor who closed the room');
}
this.logger.debug(`Updating DB for room ${room._id} with close data`);
const inquiry = await LivechatInquiry.findOneByRoomId(rid, { session });
const removedInquiry = await LivechatInquiry.removeByRoomId(rid, { session });
if (!params.forceClose && removedInquiry && removedInquiry.deletedCount !== 1) {
throw new Error('Error removing inquiry');
}
const updatedRoom = await LivechatRooms.closeRoomById(rid, closeData, { session });
if (!params.forceClose && (!updatedRoom || updatedRoom.modifiedCount !== 1)) {
throw new Error('Error closing room');
}
const subs = await Subscriptions.countByRoomId(rid, { session });
if (subs) {
const removedSubs = await Subscriptions.removeByRoomId(rid, {
async onTrash(doc) {
void notifyOnSubscriptionChanged(doc, 'removed');
},
session,
});
if (!params.forceClose && removedSubs.deletedCount !== subs) {
throw new Error('Error removing subscriptions');
}
}
this.logger.debug(`DB updated for room ${room._id}`);
// Retrieve the closed room
const newRoom = await LivechatRooms.findOneById(rid, { session });
if (!newRoom) {
throw new Error('Error: Room not found');
}
return { room: newRoom, closedBy: closeData.closedBy, removedInquiry: inquiry };
}
private makeVisitorAssociation(visitorId: string, roomInfo: IOmnichannelSource): ILivechatContactVisitorAssociation {
return {
visitorId,
@ -515,70 +318,6 @@ class LivechatClass {
return Users.countBotAgents();
}
private async resolveChatTags(
room: IOmnichannelRoom,
options: CloseRoomParams['options'] = {},
): Promise<{ updatedOptions: CloseRoomParams['options'] }> {
this.logger.debug(`Resolving chat tags for room ${room._id}`);
const concatUnique = (...arrays: (string[] | undefined)[]): string[] => [
...new Set(([] as string[]).concat(...arrays.filter((a): a is string[] => !!a))),
];
const { departmentId, tags: optionsTags } = room;
const { clientAction, tags: oldRoomTags } = options;
const roomTags = concatUnique(oldRoomTags, optionsTags);
if (!departmentId) {
return {
updatedOptions: {
...options,
...(roomTags.length && { tags: roomTags }),
},
};
}
const department = await LivechatDepartment.findOneById<Pick<ILivechatDepartment, 'requestTagBeforeClosingChat' | 'chatClosingTags'>>(
departmentId,
{
projection: { requestTagBeforeClosingChat: 1, chatClosingTags: 1 },
},
);
if (!department) {
return {
updatedOptions: {
...options,
...(roomTags.length && { tags: roomTags }),
},
};
}
const { requestTagBeforeClosingChat, chatClosingTags } = department;
const extraRoomTags = concatUnique(roomTags, chatClosingTags);
if (!requestTagBeforeClosingChat) {
return {
updatedOptions: {
...options,
...(extraRoomTags.length && { tags: extraRoomTags }),
},
};
}
const checkRoomTags = !clientAction || (roomTags && roomTags.length > 0);
const checkDepartmentTags = chatClosingTags && chatClosingTags.length > 0;
if (!checkRoomTags || !checkDepartmentTags) {
throw new Error('error-tags-must-be-assigned-before-closing-chat');
}
return {
updatedOptions: {
...options,
...(extraRoomTags.length && { tags: extraRoomTags }),
},
};
}
async sendRequest(
postData: {
type: string;
@ -739,20 +478,6 @@ class LivechatClass {
return true;
}
async closeOpenChats(userId: string, comment?: string) {
this.logger.debug(`Closing open chats for user ${userId}`);
const user = await Users.findOneById(userId);
const extraQuery = await callbacks.run('livechat.applyDepartmentRestrictions', {}, { userId });
const openChats = LivechatRooms.findOpenByAgent(userId, extraQuery);
const promises: Promise<void>[] = [];
await openChats.forEach((room) => {
promises.push(this.closeRoom({ user, room, comment }));
});
await Promise.all(promises);
}
async transfer(room: IOmnichannelRoom, guest: ILivechatVisitor, transferData: TransferData) {
this.logger.debug(`Transfering room ${room._id} [Transfered by: ${transferData?.transferredBy?._id}]`);
if (room.onHold) {
@ -1103,48 +828,6 @@ class LivechatClass {
return result.modifiedCount;
}
async requestTranscript({
rid,
email,
subject,
user,
}: {
rid: string;
email: string;
subject: string;
user: AtLeast<IUser, '_id' | 'username' | 'utcOffset' | 'name'>;
}) {
const room = await LivechatRooms.findOneById(rid, { projection: { _id: 1, open: 1, transcriptRequest: 1 } });
if (!room?.open) {
throw new Meteor.Error('error-invalid-room', 'Invalid room');
}
if (room.transcriptRequest) {
throw new Meteor.Error('error-transcript-already-requested', 'Transcript already requested');
}
if (!(await Omnichannel.isWithinMACLimit(room))) {
throw new Error('error-mac-limit-reached');
}
const { _id, username, name, utcOffset } = user;
const transcriptRequest = {
requestedAt: new Date(),
requestedBy: {
_id,
username,
name,
utcOffset,
},
email,
subject,
};
await LivechatRooms.setEmailTranscriptRequestedByRoomId(rid, transcriptRequest);
return true;
}
async afterRemoveAgent(user: AtLeast<IUser, '_id' | 'username'>) {
await callbacks.run('livechat.afterAgentRemoved', { agent: user });
return true;

@ -0,0 +1,289 @@
import { Apps, AppEvents } from '@rocket.chat/apps';
import { Message } from '@rocket.chat/core-services';
import type { ILivechatDepartment, ILivechatInquiryRecord, IOmnichannelRoom, IOmnichannelRoomClosingInfo } from '@rocket.chat/core-typings';
import { isOmnichannelRoom } from '@rocket.chat/core-typings';
import { LivechatDepartment, LivechatInquiry, LivechatRooms, Subscriptions, Users } from '@rocket.chat/models';
import type { ClientSession } from 'mongodb';
import type { CloseRoomParams, CloseRoomParamsByUser, CloseRoomParamsByVisitor } from './localTypes';
import { livechatLogger as logger } from './logger';
import { parseTranscriptRequest } from './parseTranscriptRequest';
import { callbacks } from '../../../../lib/callbacks';
import { client, shouldRetryTransaction } from '../../../../server/database/utils';
import {
notifyOnLivechatInquiryChanged,
notifyOnRoomChangedById,
notifyOnSubscriptionChanged,
} from '../../../lib/server/lib/notifyListener';
import { settings } from '../../../settings/server';
type ChatCloser = { _id: string; username: string | undefined };
const isRoomClosedByUserParams = (params: CloseRoomParams): params is CloseRoomParamsByUser =>
(params as CloseRoomParamsByUser).user !== undefined;
const isRoomClosedByVisitorParams = (params: CloseRoomParams): params is CloseRoomParamsByVisitor =>
(params as CloseRoomParamsByVisitor).visitor !== undefined;
export async function closeRoom(params: CloseRoomParams, attempts = 2): Promise<void> {
let newRoom: IOmnichannelRoom;
let chatCloser: ChatCloser;
let removedInquiryObj: ILivechatInquiryRecord | null;
const session = client.startSession();
try {
session.startTransaction();
const { room, closedBy, removedInquiry } = await doCloseRoom(params, session);
await session.commitTransaction();
newRoom = room;
chatCloser = closedBy;
removedInquiryObj = removedInquiry;
} catch (e) {
logger.error({ err: e, msg: 'Failed to close room', afterAttempts: attempts });
await session.abortTransaction();
// Dont propagate transaction errors
if (shouldRetryTransaction(e)) {
if (attempts > 0) {
logger.debug(`Retrying close room because of transient error. Attempts left: ${attempts}`);
return closeRoom(params, attempts - 1);
}
throw new Error('error-room-cannot-be-closed-try-again');
}
throw e;
} finally {
await session.endSession();
}
// Note: when reaching this point, the room has been closed
// Transaction is commited and so these messages can be sent here.
return afterRoomClosed(newRoom, chatCloser, removedInquiryObj, params);
}
async function afterRoomClosed(
newRoom: IOmnichannelRoom,
chatCloser: ChatCloser,
inquiry: ILivechatInquiryRecord | null,
params: CloseRoomParams,
): Promise<void> {
if (!chatCloser) {
// this should never happen
return;
}
// Note: we are okay with these messages being sent outside of the transaction. The process of sending a message
// is huge and involves multiple db calls. Making it transactionable this way would be really hard.
// And passing just _some_ actions to the transaction creates some deadlocks since messages are updated in the afterSaveMessages callbacks.
const transcriptRequested =
!!params.room.transcriptRequest || (!settings.get('Livechat_enable_transcript') && settings.get('Livechat_transcript_send_always'));
logger.debug(`Sending closing message to room ${newRoom._id}`);
await Message.saveSystemMessageAndNotifyUser('livechat-close', newRoom._id, params.comment ?? '', chatCloser, {
groupable: false,
transcriptRequested,
...(isRoomClosedByVisitorParams(params) && { token: params.visitor.token }),
});
if (settings.get('Livechat_enable_transcript') && !settings.get('Livechat_transcript_send_always')) {
await Message.saveSystemMessage('command', newRoom._id, 'promptTranscript', chatCloser);
}
logger.debug(`Running callbacks for room ${newRoom._id}`);
process.nextTick(() => {
/**
* @deprecated the `AppEvents.ILivechatRoomClosedHandler` event will be removed
* in the next major version of the Apps-Engine
*/
void Apps.self?.getBridges()?.getListenerBridge().livechatEvent(AppEvents.ILivechatRoomClosedHandler, newRoom);
void Apps.self?.getBridges()?.getListenerBridge().livechatEvent(AppEvents.IPostLivechatRoomClosed, newRoom);
});
const visitor = isRoomClosedByVisitorParams(params) ? params.visitor : undefined;
const opts = await parseTranscriptRequest(params.room, params.options, visitor);
if (process.env.TEST_MODE) {
await callbacks.run('livechat.closeRoom', {
room: newRoom,
options: opts,
});
} else {
callbacks.runAsync('livechat.closeRoom', {
room: newRoom,
options: opts,
});
}
void notifyOnRoomChangedById(newRoom._id);
if (inquiry) {
void notifyOnLivechatInquiryChanged(inquiry, 'removed');
}
logger.debug(`Room ${newRoom._id} was closed`);
}
async function doCloseRoom(
params: CloseRoomParams,
session: ClientSession,
): Promise<{ room: IOmnichannelRoom; closedBy: ChatCloser; removedInquiry: ILivechatInquiryRecord | null }> {
const { comment } = params;
const { room, forceClose } = params;
logger.debug({ msg: `Attempting to close room`, roomId: room._id, forceClose });
if (!room || !isOmnichannelRoom(room) || (!forceClose && !room.open)) {
logger.debug(`Room ${room._id} is not open`);
throw new Error('error-room-closed');
}
const commentRequired = settings.get('Livechat_request_comment_when_closing_conversation');
if (commentRequired && !comment?.trim()) {
throw new Error('error-comment-is-required');
}
const { updatedOptions: options } = await resolveChatTags(room, params.options);
logger.debug(`Resolved chat tags for room ${room._id}`);
const now = new Date();
const { _id: rid, servedBy } = room;
const serviceTimeDuration = servedBy && (now.getTime() - new Date(servedBy.ts).getTime()) / 1000;
const closeData: IOmnichannelRoomClosingInfo = {
closedAt: now,
chatDuration: (now.getTime() - new Date(room.ts).getTime()) / 1000,
...(serviceTimeDuration && { serviceTimeDuration }),
...options,
};
logger.debug(`Room ${room._id} was closed at ${closeData.closedAt} (duration ${closeData.chatDuration})`);
if (isRoomClosedByUserParams(params)) {
const { user } = params;
logger.debug(`Closing by user ${user?._id}`);
closeData.closer = 'user';
closeData.closedBy = {
_id: user?._id || '',
username: user?.username,
};
} else if (isRoomClosedByVisitorParams(params)) {
const { visitor } = params;
logger.debug(`Closing by visitor ${params.visitor._id}`);
closeData.closer = 'visitor';
closeData.closedBy = {
_id: visitor._id,
username: visitor.username,
};
} else {
throw new Error('Error: Please provide details of the user or visitor who closed the room');
}
logger.debug(`Updating DB for room ${room._id} with close data`);
const inquiry = await LivechatInquiry.findOneByRoomId(rid, { session });
const removedInquiry = await LivechatInquiry.removeByRoomId(rid, { session });
if (!params.forceClose && removedInquiry && removedInquiry.deletedCount !== 1) {
throw new Error('Error removing inquiry');
}
const updatedRoom = await LivechatRooms.closeRoomById(rid, closeData, { session });
if (!params.forceClose && (!updatedRoom || updatedRoom.modifiedCount !== 1)) {
throw new Error('Error closing room');
}
const subs = await Subscriptions.countByRoomId(rid, { session });
if (subs) {
const removedSubs = await Subscriptions.removeByRoomId(rid, {
async onTrash(doc) {
void notifyOnSubscriptionChanged(doc, 'removed');
},
session,
});
if (!params.forceClose && removedSubs.deletedCount !== subs) {
throw new Error('Error removing subscriptions');
}
}
logger.debug(`DB updated for room ${room._id}`);
// Retrieve the closed room
const newRoom = await LivechatRooms.findOneById(rid, { session });
if (!newRoom) {
throw new Error('Error: Room not found');
}
return { room: newRoom, closedBy: closeData.closedBy, removedInquiry: inquiry };
}
async function resolveChatTags(
room: IOmnichannelRoom,
options: CloseRoomParams['options'] = {},
): Promise<{ updatedOptions: CloseRoomParams['options'] }> {
logger.debug(`Resolving chat tags for room ${room._id}`);
const concatUnique = (...arrays: (string[] | undefined)[]): string[] => [
...new Set(([] as string[]).concat(...arrays.filter((a): a is string[] => !!a))),
];
const { departmentId, tags: optionsTags } = room;
const { clientAction, tags: oldRoomTags } = options;
const roomTags = concatUnique(oldRoomTags, optionsTags);
if (!departmentId) {
return {
updatedOptions: {
...options,
...(roomTags.length && { tags: roomTags }),
},
};
}
const department = await LivechatDepartment.findOneById<Pick<ILivechatDepartment, 'requestTagBeforeClosingChat' | 'chatClosingTags'>>(
departmentId,
{
projection: { requestTagBeforeClosingChat: 1, chatClosingTags: 1 },
},
);
if (!department) {
return {
updatedOptions: {
...options,
...(roomTags.length && { tags: roomTags }),
},
};
}
const { requestTagBeforeClosingChat, chatClosingTags } = department;
const extraRoomTags = concatUnique(roomTags, chatClosingTags);
if (!requestTagBeforeClosingChat) {
return {
updatedOptions: {
...options,
...(extraRoomTags.length && { tags: extraRoomTags }),
},
};
}
const checkRoomTags = !clientAction || (roomTags && roomTags.length > 0);
const checkDepartmentTags = chatClosingTags && chatClosingTags.length > 0;
if (!checkRoomTags || !checkDepartmentTags) {
throw new Error('error-tags-must-be-assigned-before-closing-chat');
}
return {
updatedOptions: {
...options,
...(extraRoomTags.length && { tags: extraRoomTags }),
},
};
}
export async function closeOpenChats(userId: string, comment?: string) {
logger.debug(`Closing open chats for user ${userId}`);
const user = await Users.findOneById(userId);
const extraQuery = await callbacks.run('livechat.applyDepartmentRestrictions', {}, { userId });
const openChats = LivechatRooms.findOpenByAgent(userId, extraQuery);
const promises: Promise<void>[] = [];
await openChats.forEach((room) => {
promises.push(closeRoom({ user, room, comment }));
});
await Promise.all(promises);
}

@ -1,4 +1,4 @@
import { Message } from '@rocket.chat/core-services';
import { Message, Omnichannel } from '@rocket.chat/core-services';
import {
type IUser,
type MessageTypesValues,
@ -6,6 +6,7 @@ import {
type ILivechatVisitor,
isFileAttachment,
isFileImageAttachment,
type AtLeast,
} from '@rocket.chat/core-typings';
import colors from '@rocket.chat/fuselage-tokens/colors';
import { Logger } from '@rocket.chat/logger';
@ -222,3 +223,45 @@ export async function sendTranscript({
return true;
}
export async function requestTranscript({
rid,
email,
subject,
user,
}: {
rid: string;
email: string;
subject: string;
user: AtLeast<IUser, '_id' | 'username' | 'utcOffset' | 'name'>;
}) {
const room = await LivechatRooms.findOneById(rid, { projection: { _id: 1, open: 1, transcriptRequest: 1 } });
if (!room?.open) {
throw new Meteor.Error('error-invalid-room', 'Invalid room');
}
if (room.transcriptRequest) {
throw new Meteor.Error('error-transcript-already-requested', 'Transcript already requested');
}
if (!(await Omnichannel.isWithinMACLimit(room))) {
throw new Error('error-mac-limit-reached');
}
const { _id, username, name, utcOffset } = user;
const transcriptRequest = {
requestedAt: new Date(),
requestedBy: {
_id,
username,
name,
utcOffset,
},
email,
subject,
};
await LivechatRooms.setEmailTranscriptRequestedByRoomId(rid, transcriptRequest);
return true;
}

@ -2,6 +2,7 @@ import { Logger } from '@rocket.chat/logger';
import { settings } from '../../../../settings/server';
import { Livechat } from '../LivechatTyped';
import { closeOpenChats } from '../closeRoom';
const logger = new Logger('AgentStatusWatcher');
@ -68,7 +69,7 @@ export const onlineAgents = {
try {
if (action === 'close') {
return await Livechat.closeOpenChats(userId, comment);
return await closeOpenChats(userId, comment);
}
if (action === 'forward') {

@ -5,7 +5,7 @@ import { Meteor } from 'meteor/meteor';
import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission';
import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger';
import { Livechat } from '../lib/LivechatTyped';
import { closeRoom } from '../lib/closeRoom';
type CloseRoomOptions = {
clientAction?: boolean;
@ -87,7 +87,7 @@ Meteor.methods<ServerMethods>({
});
}
await Livechat.closeRoom({
await closeRoom({
user,
room,
comment,

@ -2,7 +2,7 @@ import type { IUser, ILivechatContactVisitorAssociation } from '@rocket.chat/cor
import { License } from '@rocket.chat/license';
import { LivechatContacts, LivechatRooms, LivechatVisitors } from '@rocket.chat/models';
import { Livechat } from '../../../../../../app/livechat/server/lib/LivechatTyped';
import { closeRoom } from '../../../../../../app/livechat/server/lib/closeRoom';
import { i18n } from '../../../../../../server/lib/i18n';
export async function changeContactBlockStatus({ block, visitor }: { visitor: ILivechatContactVisitorAssociation; block: boolean }) {
@ -32,5 +32,5 @@ export async function closeBlockedRoom(association: ILivechatContactVisitorAssoc
return;
}
return Livechat.closeRoom({ room, visitor, comment: i18n.t('close-blocked-room-comment'), user });
return closeRoom({ room, visitor, comment: i18n.t('close-blocked-room-comment'), user });
}

@ -7,7 +7,7 @@ import { MongoInternals } from 'meteor/mongo';
import moment from 'moment';
import { schedulerLogger } from './logger';
import { Livechat } from '../../../../../app/livechat/server/lib/LivechatTyped';
import { closeRoom } from '../../../../../app/livechat/server/lib/closeRoom';
const SCHEDULER_NAME = 'omnichannel_auto_close_on_hold_scheduler';
@ -82,7 +82,7 @@ export class AutoCloseOnHoldSchedulerClass {
comment,
};
await Livechat.closeRoom(payload);
await closeRoom(payload);
}
private async getSchedulerUser(): Promise<IUser> {

@ -7,7 +7,7 @@ import { MongoInternals } from 'meteor/mongo';
import type { Db } from 'mongodb';
import { schedulerLogger } from './logger';
import { Livechat } from '../../../../../app/livechat/server/lib/LivechatTyped';
import { closeRoom } from '../../../../../app/livechat/server/lib/closeRoom';
import { settings } from '../../../../../app/settings/server';
import { i18n } from '../../../../../server/lib/i18n';
@ -101,7 +101,7 @@ export class OmnichannelQueueInactivityMonitorClass {
async closeRoomAction(room: IOmnichannelRoom): Promise<void> {
const comment = this.message;
return Livechat.closeRoom({
return closeRoom({
comment,
room,
user: await this.getRocketCatUser(),

@ -6,7 +6,7 @@ import { LivechatVisitors, LivechatRooms, LivechatDepartment, Users } from '@roc
import { schedulerLogger } from './logger';
import { notifyOnRoomChangedById } from '../../../../../app/lib/server/lib/notifyListener';
import { Livechat } from '../../../../../app/livechat/server/lib/LivechatTyped';
import { closeRoom } from '../../../../../app/livechat/server/lib/closeRoom';
import { settings } from '../../../../../app/settings/server';
import { callbacks } from '../../../../../lib/callbacks';
import { i18n } from '../../../../../server/lib/i18n';
@ -92,7 +92,7 @@ export class VisitorInactivityMonitor {
if (room.departmentId) {
comment = (await this._getDepartmentAbandonedCustomMessage(room.departmentId)) || comment;
}
await Livechat.closeRoom({
await closeRoom({
comment,
room,
user: this.user,

@ -64,7 +64,7 @@ const mocks = {
},
},
},
'../../../../../app/livechat/server/lib/LivechatTyped': { Livechat: { closeRoom: mockLivechatCloseRoom } },
'../../../../../app/livechat/server/lib/closeRoom': { closeRoom: mockLivechatCloseRoom },
'./logger': { schedulerLogger: mockLogger },
'@rocket.chat/models': {
LivechatRooms: mockLivechatRooms,

@ -29,7 +29,7 @@ const mongoMock = {
}),
},
};
const livechatMock = { Livechat: { closeRoom: sinon.stub() } };
const livechatMock = { closeRoom: sinon.stub() };
const settingsMock = { settings: { get: sinon.stub() } };
const { OmnichannelQueueInactivityMonitorClass } = proxyquire
@ -41,7 +41,7 @@ const { OmnichannelQueueInactivityMonitorClass } = proxyquire
'@rocket.chat/models': modelsMock,
'meteor/meteor': meteorMock,
'meteor/mongo': mongoMock,
'../../../../../app/livechat/server/lib/LivechatTyped': livechatMock,
'../../../../../app/livechat/server/lib/closeRoom': livechatMock,
'../../../../../app/settings/server': settingsMock,
'../../../../../server/lib/i18n': { i18n: { t: sinon.stub().returns('Closed automatically') } },
});
@ -154,7 +154,7 @@ describe('OmnichannelQueueInactivityMonitorClass', () => {
beforeEach(() => {
modelsMock.LivechatInquiry.findOneById.reset();
modelsMock.LivechatRooms.findOneById.reset();
livechatMock.Livechat.closeRoom.reset();
livechatMock.closeRoom.reset();
});
it('should ignore the inquiry if its not in queue anymore', async () => {
const qclass = new OmnichannelQueueInactivityMonitorClass();
@ -162,7 +162,7 @@ describe('OmnichannelQueueInactivityMonitorClass', () => {
await qclass.closeRoom({ attrs: { data: { inquiryId: 'inquiryId' } } });
expect(modelsMock.LivechatInquiry.findOneById.calledWith('inquiryId')).to.be.true;
expect(livechatMock.Livechat.closeRoom.notCalled).to.be.true;
expect(livechatMock.closeRoom.notCalled).to.be.true;
});
it('should ignore an inquiry with no room', async () => {
const qclass = new OmnichannelQueueInactivityMonitorClass();
@ -172,7 +172,7 @@ describe('OmnichannelQueueInactivityMonitorClass', () => {
expect(modelsMock.LivechatInquiry.findOneById.calledWith('inquiryId')).to.be.true;
expect(modelsMock.LivechatRooms.findOneById.calledWith('roomId')).to.be.true;
expect(livechatMock.Livechat.closeRoom.notCalled).to.be.true;
expect(livechatMock.closeRoom.notCalled).to.be.true;
});
it('should close a room', async () => {
const qclass = new OmnichannelQueueInactivityMonitorClass();
@ -185,7 +185,7 @@ describe('OmnichannelQueueInactivityMonitorClass', () => {
expect(modelsMock.LivechatInquiry.findOneById.calledWith('inquiryId')).to.be.true;
expect(modelsMock.LivechatRooms.findOneById.calledWith('roomId')).to.be.true;
expect(
livechatMock.Livechat.closeRoom.calledWith(
livechatMock.closeRoom.calledWith(
sinon.match({
comment: 'Closed automatically',
room: { _id: 'roomId' },

@ -22,9 +22,7 @@ const livechatStub = {
const hasPermissionStub = sinon.stub();
const { closeLivechatRoom } = proxyquire.noCallThru().load('../../../../../../app/lib/server/functions/closeLivechatRoom.ts', {
'../../../livechat/server/lib/LivechatTyped': {
Livechat: livechatStub,
},
'../../../livechat/server/lib/closeRoom': livechatStub,
'../../../authorization/server/functions/hasPermission': {
hasPermissionAsync: hasPermissionStub,
},

Loading…
Cancel
Save