Files
upstream-rocketchat/apps/meteor/server/lib/omnichannel/guests.ts
upstream-mirror 30810eecd7
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 1s
snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
2026-09-16 01:21:02 +05:00

206 lines
6.5 KiB
TypeScript

import { Apps, AppEvents } from '@rocket.chat/apps';
import type { ILivechatVisitor, IOmnichannelRoom, IUser, UserStatus } from '@rocket.chat/core-typings';
import {
LivechatVisitors,
LivechatCustomField,
LivechatInquiry,
LivechatRooms,
Messages,
ReadReceipts,
ReadReceiptsArchive,
Subscriptions,
LivechatContacts,
Users,
} from '@rocket.chat/models';
import UAParser from 'ua-parser-js';
import { parseAgentCustomFields } from './Helper';
import type { ICRMData } from './localTypes';
import { livechatLogger } from './logger';
import { trim } from '../../../lib/utils/stringUtils';
import { hasPermissionAsync } from '../authorization/hasPermission';
import { i18n } from '../i18n';
import { FileUpload } from '../media/file-upload';
import { notifyOnSubscriptionChanged, notifyOnLivechatInquiryChanged, notifyOnLivechatInquiryChangedByToken } from '../notifyListener';
export async function saveGuest(
guestData: Pick<ILivechatVisitor, '_id' | 'name' | 'livechatData'> & { email?: string; phone?: string },
userId: string,
) {
const { _id, name, email, phone, livechatData = {} } = guestData;
const visitor = await LivechatVisitors.findOneById(_id, { projection: { _id: 1 } });
if (!visitor) {
throw new Error('error-invalid-visitor');
}
livechatLogger.debug({ msg: 'Saving guest', guestData });
const updateData = {
...(name && { name }),
...(email && { email }),
...(phone && { phone }),
livechatData: {},
};
const customFields: Record<string, any> = {};
if ((!userId || (await hasPermissionAsync(userId, 'edit-livechat-room-customfields'))) && Object.keys(livechatData).length) {
livechatLogger.debug({ msg: 'Saving custom fields for visitor', visitorId: _id, livechatData });
for await (const field of LivechatCustomField.findByScope('visitor')) {
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 Error(i18n.t('error-invalid-custom-field-value'));
}
}
customFields[field._id] = value;
}
updateData.livechatData = customFields;
livechatLogger.debug({
msg: 'About to update custom fields for visitor',
visitorId: _id,
customFieldCount: Object.keys(customFields).length,
});
}
const ret = await LivechatVisitors.saveGuestById(_id, updateData);
setImmediate(() => {
void Apps.self?.triggerEvent(AppEvents.IPostLivechatGuestSaved, _id);
});
return ret;
}
export async function removeGuest({ _id }: { _id: string }) {
await cleanGuestHistory(_id);
return LivechatVisitors.disableById(_id);
}
export async function removeContactsByVisitorId({ _id }: { _id: string }) {
// A visitor shouldn't have many contacts associated, so we can remove them like this
const contacts = await LivechatContacts.findAllByVisitorId(_id).toArray();
if (!contacts.length) {
livechatLogger.debug({ msg: 'No contacts found for visitor', visitorId: _id });
await removeGuest({ _id });
}
// And a contact shouldn't have many channels associated, so we can do this
livechatLogger.debug({ msg: 'Removing channels for contacts', visitorId: _id, contacts: contacts.map(({ _id }) => _id) });
for await (const contact of contacts) {
for await (const { visitor } of contact.channels) {
await removeGuest({ _id: visitor.visitorId });
}
await LivechatContacts.disableByVisitorId(_id);
}
}
async function cleanGuestHistory(_id: string) {
// This shouldn't be possible, but just in case
if (!_id) {
throw new Error('error-invalid-guest');
}
// TODO: optimize function => instead of removing one by one, fetch the _ids of the rooms and then remove them in bulk
const cursor = LivechatRooms.findByVisitorId(_id, { projection: { _id: 1 } });
for await (const room of cursor) {
await Promise.all([
Subscriptions.removeByRoomId(room._id, {
async onTrash(doc) {
void notifyOnSubscriptionChanged(doc, 'removed');
},
}),
FileUpload.removeFilesByRoomId(room._id),
Messages.removeByRoomId(room._id),
ReadReceipts.removeByRoomId(room._id),
ReadReceiptsArchive.removeByRoomId(room._id),
]);
}
await LivechatRooms.removeByVisitorId(_id);
const livechatInquiries = await LivechatInquiry.findByVisitorIds([_id]).toArray();
await LivechatInquiry.removeByIds(livechatInquiries.map(({ _id }) => _id));
void notifyOnLivechatInquiryChanged(livechatInquiries, 'removed');
}
export async function getLivechatRoomGuestInfo(room: IOmnichannelRoom) {
const visitor = await LivechatVisitors.findOneEnabledById(room.v._id);
if (!visitor) {
throw new Error('error-invalid-visitor');
}
const agent = room.servedBy?._id
? await Users.findOneById<Pick<IUser, '_id' | 'customFields' | 'name' | 'username' | 'emails'>>(room.servedBy?._id, {
projection: { _id: 1, customFields: 1, name: 1, username: 1, emails: 1 },
})
: null;
const ua = new UAParser();
ua.setUA(visitor.userAgent || '');
const postData: ICRMData = {
_id: room._id,
label: room.fname || room.label, // using same field for compatibility
topic: room.topic,
createdAt: room.ts,
lastMessageAt: room.lm,
tags: room.tags,
customFields: room.livechatData,
visitor: {
_id: visitor._id,
token: visitor.token,
name: visitor.name,
username: visitor.username,
department: visitor.department,
ip: visitor.ip,
os: ua.getOS().name && `${ua.getOS().name} ${ua.getOS().version}`,
browser: ua.getBrowser().name && `${ua.getBrowser().name} ${ua.getBrowser().version}`,
customFields: visitor.livechatData,
},
};
if (agent) {
const customFields = parseAgentCustomFields(agent.customFields);
postData.agent = {
_id: agent._id,
username: agent.username,
name: agent.name,
...(customFields && { customFields }),
};
if (agent.emails && agent.emails.length > 0) {
postData.agent.email = agent.emails[0].address;
}
}
if (room.crmData) {
postData.crmData = room.crmData;
}
if (visitor.visitorEmails && visitor.visitorEmails.length > 0) {
postData.visitor.email = visitor.visitorEmails;
}
if (visitor.phone && visitor.phone.length > 0) {
postData.visitor.phone = visitor.phone;
}
return postData;
}
export async function notifyGuestStatusChanged(token: string, status: UserStatus) {
const [, inquiryVisitorStatus] = await Promise.all([
LivechatRooms.updateVisitorStatus(token, status),
LivechatInquiry.updateVisitorStatus(token, status),
]);
if (inquiryVisitorStatus.modifiedCount) {
void notifyOnLivechatInquiryChangedByToken(token, 'updated', { v: { status } });
}
}