Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 2s
315 lines
9.3 KiB
TypeScript
315 lines
9.3 KiB
TypeScript
import { AppEvents, Apps } from '@rocket.chat/apps';
|
|
import { Omnichannel } from '@rocket.chat/core-services';
|
|
import type {
|
|
ILivechatVisitor,
|
|
IMessage,
|
|
IOmnichannelRoomInfo,
|
|
SelectedAgent,
|
|
IOmnichannelRoomExtraData,
|
|
IOmnichannelRoom,
|
|
TransferData,
|
|
} from '@rocket.chat/core-typings';
|
|
import { isOmnichannelRoom } from '@rocket.chat/core-typings';
|
|
import {
|
|
LivechatRooms,
|
|
LivechatContacts,
|
|
Messages,
|
|
LivechatCustomField,
|
|
LivechatInquiry,
|
|
Rooms,
|
|
Subscriptions,
|
|
Users,
|
|
ReadReceipts,
|
|
ReadReceiptsArchive,
|
|
} from '@rocket.chat/models';
|
|
import { check } from 'meteor/check';
|
|
import { Meteor } from 'meteor/meteor';
|
|
|
|
import { normalizeTransferredByData } from './Helper';
|
|
import { QueueManager } from './QueueManager';
|
|
import { RoutingManager } from './RoutingManager';
|
|
import { Visitors } from './Visitors';
|
|
import { getRequiredDepartment } from './departmentsLib';
|
|
import { checkDefaultAgentOnNewRoom } from './hooks';
|
|
import { livechatLogger } from './logger';
|
|
import { saveTransferHistory } from './transfer';
|
|
import { i18n } from '../../../app/utils/lib/i18n';
|
|
import { trim } from '../../../lib/utils/stringUtils';
|
|
import { settings } from '../../settings';
|
|
import { hasPermissionAsync } from '../authorization/hasPermission';
|
|
import { callbacks } from '../callbacks';
|
|
import {
|
|
notifyOnLivechatInquiryChangedByRoom,
|
|
notifyOnSubscriptionChangedByRoomId,
|
|
notifyOnRoomChangedById,
|
|
notifyOnLivechatInquiryChanged,
|
|
notifyOnSubscriptionChanged,
|
|
notifyOnRoomChanged,
|
|
} from '../notifyListener';
|
|
|
|
export async function getRoom(
|
|
guest: ILivechatVisitor,
|
|
message: Pick<IMessage, 'rid' | 'msg' | 'token'>,
|
|
roomInfo: IOmnichannelRoomInfo,
|
|
agent?: SelectedAgent,
|
|
extraData?: IOmnichannelRoomExtraData,
|
|
) {
|
|
if (!settings.get('Livechat_enabled')) {
|
|
throw new Meteor.Error('error-omnichannel-is-disabled');
|
|
}
|
|
livechatLogger.debug({ msg: 'Attempting to find or create a room for visitor', visitorId: guest._id });
|
|
const room = await LivechatRooms.findOneById(message.rid);
|
|
|
|
if (room?.v._id && (await LivechatContacts.isChannelBlocked(Visitors.makeVisitorAssociation(room.v._id, room.source)))) {
|
|
throw new Error('error-contact-channel-blocked');
|
|
}
|
|
|
|
if (!room?.open) {
|
|
livechatLogger.debug({ msg: 'Last room for visitor closed. Creating new one', visitorId: guest._id });
|
|
}
|
|
|
|
if (!room?.open) {
|
|
return {
|
|
room: await createRoom({ visitor: guest, message: message.msg, roomInfo, agent, extraData }),
|
|
newRoom: true,
|
|
};
|
|
}
|
|
|
|
if (room.v.token !== guest.token) {
|
|
livechatLogger.debug({ msg: 'Visitor trying to access another visitor room', visitorId: guest._id });
|
|
throw new Meteor.Error('cannot-access-room');
|
|
}
|
|
|
|
return { room, newRoom: false };
|
|
}
|
|
|
|
export async function createRoom({
|
|
visitor,
|
|
message,
|
|
rid,
|
|
roomInfo,
|
|
agent,
|
|
extraData,
|
|
}: {
|
|
visitor: ILivechatVisitor;
|
|
message?: string;
|
|
rid?: string;
|
|
roomInfo: IOmnichannelRoomInfo;
|
|
agent?: SelectedAgent;
|
|
extraData?: IOmnichannelRoomExtraData;
|
|
}) {
|
|
if (!settings.get('Livechat_enabled')) {
|
|
throw new Meteor.Error('error-omnichannel-is-disabled');
|
|
}
|
|
|
|
if (await LivechatContacts.isChannelBlocked(Visitors.makeVisitorAssociation(visitor._id, roomInfo.source))) {
|
|
throw new Error('error-contact-channel-blocked');
|
|
}
|
|
|
|
const defaultAgent = await checkDefaultAgentOnNewRoom(agent, {
|
|
visitorId: visitor._id,
|
|
source: roomInfo.source,
|
|
});
|
|
|
|
// if no department selected verify if there is at least one active and pick the first
|
|
if (!defaultAgent && !visitor.department) {
|
|
const department = await getRequiredDepartment();
|
|
livechatLogger.debug({ msg: 'No department or default agent selected for visitor', visitorId: visitor._id });
|
|
|
|
if (department) {
|
|
livechatLogger.debug({ msg: 'Assigning visitor to department', visitorId: visitor._id, departmentId: department._id });
|
|
visitor.department = department._id;
|
|
}
|
|
}
|
|
|
|
// delegate room creation to QueueManager
|
|
livechatLogger.debug({ msg: 'Calling QueueManager to request a room for visitor', visitorId: visitor._id });
|
|
|
|
const room = await QueueManager.requestRoom({
|
|
guest: visitor,
|
|
message,
|
|
rid,
|
|
roomInfo,
|
|
agent: defaultAgent,
|
|
extraData,
|
|
});
|
|
|
|
livechatLogger.debug({ msg: 'Room obtained for visitor', visitorId: visitor._id, roomId: room._id });
|
|
|
|
await Messages.setRoomIdByToken(visitor.token, room._id);
|
|
|
|
return room;
|
|
}
|
|
|
|
export async function saveRoomInfo(
|
|
roomData: {
|
|
_id: string;
|
|
topic?: string;
|
|
tags?: string[];
|
|
livechatData?: { [k: string]: string };
|
|
// For priority and SLA, if the value is blank (ie ""), then system will remove the priority or SLA from the room
|
|
priorityId?: string;
|
|
slaId?: string;
|
|
},
|
|
guestData?: {
|
|
_id: string;
|
|
name?: string;
|
|
email?: string;
|
|
phone?: string;
|
|
livechatData?: { [k: string]: string };
|
|
},
|
|
userId?: string,
|
|
) {
|
|
livechatLogger.debug({ msg: 'Saving room information', roomId: roomData._id });
|
|
const { livechatData = {} } = roomData;
|
|
const customFields: Record<string, string> = {};
|
|
|
|
if ((!userId || (await hasPermissionAsync(userId, 'edit-livechat-room-customfields'))) && Object.keys(livechatData).length) {
|
|
const fields = LivechatCustomField.findByScope('room');
|
|
for await (const field of fields) {
|
|
if (!livechatData.hasOwnProperty(field._id)) {
|
|
continue;
|
|
}
|
|
const value = trim(livechatData[field._id]);
|
|
if (value !== '' && field.regexp !== undefined && field.regexp !== '') {
|
|
const regexp = new RegExp(field.regexp);
|
|
if (!regexp.test(value)) {
|
|
throw new Meteor.Error(i18n.t('error-invalid-custom-field-value', { field: field.label }));
|
|
}
|
|
}
|
|
customFields[field._id] = value;
|
|
}
|
|
roomData.livechatData = customFields;
|
|
livechatLogger.debug({
|
|
msg: 'About to update custom fields on room',
|
|
roomId: roomData._id,
|
|
customFieldCount: Object.keys(customFields).length,
|
|
});
|
|
}
|
|
|
|
await LivechatRooms.saveRoomById(roomData);
|
|
|
|
setImmediate(() => {
|
|
void Apps.self?.triggerEvent(AppEvents.IPostLivechatRoomSaved, roomData._id);
|
|
});
|
|
|
|
if (guestData?.name?.trim().length) {
|
|
const { _id: rid } = roomData;
|
|
const { name } = guestData;
|
|
|
|
const responses = await Promise.all([
|
|
Rooms.setFnameById(rid, name),
|
|
LivechatInquiry.setNameByRoomId(rid, name),
|
|
Subscriptions.updateDisplayNameByRoomId(rid, name),
|
|
]);
|
|
|
|
if (responses[1]?.modifiedCount) {
|
|
void notifyOnLivechatInquiryChangedByRoom(rid, 'updated', { name });
|
|
}
|
|
|
|
if (responses[2]?.modifiedCount) {
|
|
await notifyOnSubscriptionChangedByRoomId(rid);
|
|
}
|
|
}
|
|
|
|
void notifyOnRoomChangedById(roomData._id);
|
|
|
|
return true;
|
|
}
|
|
|
|
export async function returnRoomAsInquiry(room: IOmnichannelRoom, departmentId?: string, overrideTransferData: Partial<TransferData> = {}) {
|
|
livechatLogger.debug({ msg: 'Transferring room to queue', scope: departmentId ? 'department' : undefined, room });
|
|
if (!room.open) {
|
|
throw new Meteor.Error('room-closed', 'Room closed');
|
|
}
|
|
|
|
if (room.onHold) {
|
|
throw new Meteor.Error('error-room-onHold');
|
|
}
|
|
|
|
if (!(await Omnichannel.isWithinMACLimit(room))) {
|
|
throw new Meteor.Error('error-mac-limit-reached');
|
|
}
|
|
|
|
if (!room.servedBy) {
|
|
return false;
|
|
}
|
|
|
|
const user = await Users.findOneById(room.servedBy._id);
|
|
if (!user?._id) {
|
|
throw new Meteor.Error('error-invalid-user');
|
|
}
|
|
|
|
const inquiry = await LivechatInquiry.findOne({ rid: room._id });
|
|
if (!inquiry) {
|
|
return false;
|
|
}
|
|
|
|
// update inquiry's last message with room's last message to correctly display in the queue
|
|
// because we stop updating the inquiry when it's been taken
|
|
if (room.lastMessage) {
|
|
await LivechatInquiry.setLastMessageById(inquiry._id, room.lastMessage);
|
|
}
|
|
|
|
const transferredBy = normalizeTransferredByData(user, room);
|
|
livechatLogger.debug({ msg: 'Transferring room by user', roomId: room._id, transferredBy: transferredBy._id });
|
|
const transferData = { scope: 'queue' as const, departmentId, transferredBy, ...overrideTransferData };
|
|
try {
|
|
await saveTransferHistory(room, transferData);
|
|
await RoutingManager.unassignAgent(inquiry, departmentId);
|
|
} catch (err) {
|
|
livechatLogger.error({ err });
|
|
throw new Meteor.Error('error-returning-inquiry');
|
|
}
|
|
|
|
callbacks.runAsync('livechat:afterReturnRoomAsInquiry', { room });
|
|
|
|
return true;
|
|
}
|
|
|
|
export async function removeOmnichannelRoom(rid: string) {
|
|
livechatLogger.debug({ msg: 'Deleting room', roomId: rid });
|
|
check(rid, String);
|
|
const room = await LivechatRooms.findOneById(rid);
|
|
if (!room) {
|
|
throw new Meteor.Error('error-invalid-room', 'Invalid room');
|
|
}
|
|
|
|
if (!isOmnichannelRoom(room)) {
|
|
throw new Meteor.Error('error-this-is-not-a-livechat-room');
|
|
}
|
|
|
|
if (room.open) {
|
|
throw new Meteor.Error('error-room-is-not-closed');
|
|
}
|
|
|
|
const inquiry = await LivechatInquiry.findOneByRoomId(rid);
|
|
|
|
const result = await Promise.allSettled([
|
|
Messages.removeByRoomId(rid),
|
|
ReadReceipts.removeByRoomId(rid),
|
|
Subscriptions.removeByRoomId(rid, {
|
|
async onTrash(doc) {
|
|
void notifyOnSubscriptionChanged(doc, 'removed');
|
|
},
|
|
}),
|
|
LivechatInquiry.removeByRoomId(rid),
|
|
LivechatRooms.removeById(rid),
|
|
ReadReceiptsArchive.removeByRoomId(rid),
|
|
]);
|
|
|
|
if (result[3]?.status === 'fulfilled' && result[3].value?.deletedCount && inquiry) {
|
|
void notifyOnLivechatInquiryChanged(inquiry, 'removed');
|
|
}
|
|
if (result[4]?.status === 'fulfilled' && result[4].value?.deletedCount) {
|
|
void notifyOnRoomChanged(room, 'removed');
|
|
}
|
|
|
|
for (const r of result) {
|
|
if (r.status === 'rejected') {
|
|
livechatLogger.error({ msg: 'Error removing room', roomId: rid, err: r.reason });
|
|
throw new Meteor.Error('error-removing-room', 'Error removing room');
|
|
}
|
|
}
|
|
}
|