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
206 lines
6.5 KiB
TypeScript
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 } });
|
|
}
|
|
}
|