refactor: Break big `LivechatTyped` into smaller files - move guest functions (#35465)

pull/35753/head^2
Diego Sampaio 9 months ago committed by GitHub
parent ce8f8b9857
commit 3008945dc3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 5
      apps/meteor/app/apps/server/bridges/livechat.ts
  2. 4
      apps/meteor/app/livechat/imports/server/rest/sms.ts
  3. 4
      apps/meteor/app/livechat/server/api/v1/message.ts
  4. 3
      apps/meteor/app/livechat/server/api/v1/room.ts
  5. 5
      apps/meteor/app/livechat/server/api/v1/visitor.ts
  6. 84
      apps/meteor/app/livechat/server/lib/LivechatTyped.ts
  7. 41
      apps/meteor/app/livechat/server/lib/contacts/registerGuestData.ts
  8. 162
      apps/meteor/app/livechat/server/lib/guests.ts
  9. 34
      apps/meteor/app/livechat/server/lib/tracking.ts
  10. 4
      apps/meteor/server/features/EmailInbox/EmailInbox_Incoming.ts

@ -13,6 +13,7 @@ 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 { registerGuest } from '../../../livechat/server/lib/guests';
import type { ILivechatMessage } from '../../../livechat/server/lib/localTypes';
import { updateMessage, sendMessage } from '../../../livechat/server/lib/messages';
import { createRoom } from '../../../livechat/server/lib/rooms';
@ -210,7 +211,7 @@ export class AppLivechatBridge extends LivechatBridge {
...(visitor.visitorEmails?.length && { email: visitor.visitorEmails[0].address }),
};
const livechatVisitor = await LivechatTyped.registerGuest(registerData);
const livechatVisitor = await registerGuest(registerData);
if (!livechatVisitor) {
throw new Error('Invalid visitor, cannot create');
@ -234,7 +235,7 @@ export class AppLivechatBridge extends LivechatBridge {
...(visitor.visitorEmails?.length && { email: visitor.visitorEmails[0].address }),
};
const livechatVisitor = await LivechatTyped.registerGuest(registerData);
const livechatVisitor = await registerGuest(registerData);
return this.orch.getConverters()?.get('visitors').convertVisitor(livechatVisitor);
}

@ -20,7 +20,7 @@ import { FileUpload } from '../../../../file-upload/server';
import { checkUrlForSsrf } from '../../../../lib/server/functions/checkUrlForSsrf';
import { settings } from '../../../../settings/server';
import { setCustomField } from '../../../server/api/lib/customFields';
import { Livechat as LivechatTyped } from '../../../server/lib/LivechatTyped';
import { registerGuest } from '../../../server/lib/guests';
import type { ILivechatMessage } from '../../../server/lib/localTypes';
import { sendMessage } from '../../../server/lib/messages';
import { createRoom } from '../../../server/lib/rooms';
@ -76,7 +76,7 @@ const defineVisitor = async (smsNumber: string, targetDepartment?: string) => {
data.department = targetDepartment;
}
const livechatVisitor = await LivechatTyped.registerGuest(data);
const livechatVisitor = await registerGuest(data);
if (!livechatVisitor) {
throw new Meteor.Error('error-invalid-visitor', 'Invalid visitor');

@ -17,7 +17,7 @@ import { isWidget } from '../../../../api/server/helpers/isWidget';
import { loadMessageHistory } from '../../../../lib/server/functions/loadMessageHistory';
import { settings } from '../../../../settings/server';
import { normalizeMessageFileUpload } from '../../../../utils/server/functions/normalizeMessageFileUpload';
import { Livechat as LivechatTyped } from '../../lib/LivechatTyped';
import { registerGuest } from '../../lib/guests';
import { updateMessage, deleteMessage, sendMessage } from '../../lib/messages';
import { findGuest, findRoom, normalizeHttpHeaderData } from '../lib/livechat';
@ -269,7 +269,7 @@ API.v1.addRoute(
const guest: typeof this.bodyParams.visitor & { connectionData?: unknown } = this.bodyParams.visitor;
guest.connectionData = normalizeHttpHeaderData(this.request.headers);
const visitor = await LivechatTyped.registerGuest(guest);
const visitor = await registerGuest(guest);
if (!visitor) {
throw new Error('error-livechat-visitor-registration');
}

@ -25,6 +25,7 @@ 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 { saveGuest } from '../../lib/guests';
import type { CloseRoomParams } from '../../lib/localTypes';
import { livechatLogger } from '../../lib/logger';
import { createRoom, saveRoomInfo } from '../../lib/rooms';
@ -410,7 +411,7 @@ API.v1.addRoute(
}
// We want this both operations to be concurrent, so we have to go with Promise.allSettled
const result = await Promise.allSettled([LivechatTyped.saveGuest(guestData, this.userId), saveRoomInfo(roomData)]);
const result = await Promise.allSettled([saveGuest(guestData, this.userId), saveRoomInfo(roomData)]);
const firstError = result.find((item) => item.status === 'rejected');
if (firstError) {

@ -7,6 +7,7 @@ import { callbacks } from '../../../../../lib/callbacks';
import { API } from '../../../../api/server';
import { settings } from '../../../../settings/server';
import { Livechat as LivechatTyped } from '../../lib/LivechatTyped';
import { registerGuest, removeGuest } from '../../lib/guests';
import { saveRoomInfo } from '../../lib/rooms';
import { validateRequiredCustomFields } from '../../lib/validateRequiredCustomFields';
import { findGuest, normalizeHttpHeaderData } from '../lib/livechat';
@ -57,7 +58,7 @@ API.v1.addRoute(
connectionData: normalizeHttpHeaderData(this.request.headers),
};
const visitor = await LivechatTyped.registerGuest(guest);
const visitor = await registerGuest(guest);
if (!visitor) {
throw new Meteor.Error('error-livechat-visitor-registration', 'Error registering visitor', {
method: 'livechat/visitor',
@ -183,7 +184,7 @@ API.v1.addRoute('livechat/visitor/:token', {
}
const { _id } = visitor;
const result = await LivechatTyped.removeGuest(_id);
const result = await removeGuest(_id);
if (!result.modifiedCount) {
throw new Meteor.Error('error-removing-visitor', 'An error ocurred while deleting visitor');
}

@ -1,4 +1,3 @@
import { Apps, AppEvents } from '@rocket.chat/apps';
import { Message, VideoConf, api } from '@rocket.chat/core-services';
import type {
IOmnichannelRoom,
@ -33,12 +32,10 @@ import type { Filter } from 'mongodb';
import UAParser from 'ua-parser-js';
import { callbacks } from '../../../../lib/callbacks';
import { trim } from '../../../../lib/utils/stringUtils';
import { i18n } from '../../../../server/lib/i18n';
import { addUserRolesAsync } from '../../../../server/lib/roles/addUserRoles';
import { removeUserFromRolesAsync } from '../../../../server/lib/roles/removeUserFromRoles';
import { canAccessRoomAsync } from '../../../authorization/server';
import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission';
import { hasRoleAsync } from '../../../authorization/server/functions/hasRole';
import { updateMessage } from '../../../lib/server/functions/updateMessage';
import {
@ -51,9 +48,6 @@ import { settings } from '../../../settings/server';
import { businessHourManager } from '../business-hour';
import { parseAgentCustomFields, updateDepartmentAgents, normalizeTransferredByData } from './Helper';
import { RoutingManager } from './RoutingManager';
import { Visitors, type RegisterGuestType } from './Visitors';
import { registerGuestData } from './contacts/registerGuestData';
import { cleanGuestHistory } from './tracking';
type AKeyOf<T> = {
[K in keyof T]?: T[K];
@ -168,16 +162,6 @@ class LivechatClass {
}
}
async registerGuest(newData: RegisterGuestType): Promise<ILivechatVisitor | null> {
const result = await Visitors.registerGuest(newData);
if (result) {
await registerGuestData(newData, result);
}
return result;
}
private async getBotAgents(department?: string) {
if (department) {
return LivechatDepartmentAgents.getBotsForDepartment(department);
@ -331,16 +315,6 @@ class LivechatClass {
}
}
async removeGuest(_id: string) {
const guest = await LivechatVisitors.findOneEnabledById(_id, { projection: { _id: 1, token: 1 } });
if (!guest) {
throw new Error('error-invalid-guest');
}
await cleanGuestHistory(guest);
return LivechatVisitors.disableById(_id);
}
async setUserStatusLivechatIf(userId: string, status: ILivechatAgentStatus, condition?: Filter<IUser>, fields?: AKeyOf<ILivechatAgent>) {
const result = await Users.setLivechatStatusIf(userId, status, condition, fields);
@ -431,64 +405,6 @@ class LivechatClass {
await Message.saveSystemMessageAndNotifyUser('livechat_transfer_history', room._id, '', { _id, username }, transferMessage);
}
async 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');
}
this.logger.debug({ msg: 'Saving guest', guestData });
const updateData: {
name?: string | undefined;
username?: string | undefined;
email?: string | undefined;
phone?: string | undefined;
livechatData: {
[k: string]: any;
};
} = { livechatData: {} };
if (name) {
updateData.name = name;
}
if (email) {
updateData.email = email;
}
if (phone) {
updateData.phone = phone;
}
const customFields: Record<string, any> = {};
if ((!userId || (await hasPermissionAsync(userId, 'edit-livechat-room-customfields'))) && Object.keys(livechatData).length) {
this.logger.debug({ msg: `Saving custom fields for visitor ${_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;
Livechat.logger.debug(`About to update ${Object.keys(customFields).length} custom fields for visitor ${_id}`);
}
const ret = await LivechatVisitors.saveGuestById(_id, updateData);
setImmediate(() => {
void Apps.self?.triggerEvent(AppEvents.IPostLivechatGuestSaved, _id);
});
return ret;
}
async setCustomFields({ token, key, value, overwrite }: { key: string; value: string; overwrite: boolean; token: string }) {
Livechat.logger.debug(`Setting custom fields data for visitor with token ${token}`);

@ -1,41 +0,0 @@
import type { AtLeast, ILivechatVisitor } from '@rocket.chat/core-typings';
import { LivechatContacts } from '@rocket.chat/models';
import { wrapExceptions } from '@rocket.chat/tools';
import { validateEmail } from '../Helper';
import type { RegisterGuestType } from '../Visitors';
import { ContactMerger, type FieldAndValue } from './ContactMerger';
export async function registerGuestData(
{ name, phone, email, username }: Pick<RegisterGuestType, 'name' | 'phone' | 'email' | 'username'>,
visitor: AtLeast<ILivechatVisitor, '_id'>,
): Promise<void> {
const validatedEmail =
email &&
wrapExceptions(() => {
const trimmedEmail = email.trim().toLowerCase();
validateEmail(trimmedEmail);
return trimmedEmail;
}).suppress();
const fields = [
{ type: 'name', value: name },
{ type: 'phone', value: phone?.number },
{ type: 'email', value: validatedEmail },
{ type: 'username', value: username || visitor.username },
].filter((field) => Boolean(field.value)) as FieldAndValue[];
if (!fields.length) {
return;
}
// If a visitor was updated who already had contacts, load up the contacts and update that information as well
const contacts = await LivechatContacts.findAllByVisitorId(visitor._id).toArray();
for await (const contact of contacts) {
await ContactMerger.mergeFieldsIntoContact({
fields,
contact,
conflictHandlingMode: contact.unknown ? 'overwrite' : 'conflict',
});
}
}

@ -0,0 +1,162 @@
import { Apps, AppEvents } from '@rocket.chat/apps';
import type { ILivechatVisitor } from '@rocket.chat/core-typings';
import {
LivechatVisitors,
LivechatCustomField,
LivechatInquiry,
LivechatRooms,
Messages,
ReadReceipts,
Subscriptions,
LivechatContacts,
} from '@rocket.chat/models';
import { wrapExceptions } from '@rocket.chat/tools';
import { validateEmail } from './Helper';
import type { RegisterGuestType } from './Visitors';
import { Visitors } from './Visitors';
import { ContactMerger, type FieldAndValue } from './contacts/ContactMerger';
import { livechatLogger } from './logger';
import { trim } from '../../../../lib/utils/stringUtils';
import { i18n } from '../../../../server/lib/i18n';
import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission';
import { FileUpload } from '../../../file-upload/server';
import { notifyOnSubscriptionChanged, notifyOnLivechatInquiryChanged } from '../../../lib/server/lib/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?: string | undefined;
username?: string | undefined;
email?: string | undefined;
phone?: string | undefined;
livechatData: {
[k: string]: any;
};
} = { livechatData: {} };
if (name) {
updateData.name = name;
}
if (email) {
updateData.email = email;
}
if (phone) {
updateData.phone = phone;
}
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 ${_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(`About to update ${Object.keys(customFields).length} custom fields for visitor ${_id}`);
}
const ret = await LivechatVisitors.saveGuestById(_id, updateData);
setImmediate(() => {
void Apps.self?.triggerEvent(AppEvents.IPostLivechatGuestSaved, _id);
});
return ret;
}
export async function removeGuest(_id: string) {
const guest = await LivechatVisitors.findOneEnabledById(_id, { projection: { _id: 1, token: 1 } });
if (!guest) {
throw new Error('error-invalid-guest');
}
await cleanGuestHistory(guest.token);
return LivechatVisitors.disableById(_id);
}
export async function registerGuest(newData: RegisterGuestType): Promise<ILivechatVisitor | null> {
const visitor = await Visitors.registerGuest(newData);
if (!visitor) {
return null;
}
const { name, phone, email, username } = newData;
const validatedEmail =
email &&
wrapExceptions(() => {
const trimmedEmail = email.trim().toLowerCase();
validateEmail(trimmedEmail);
return trimmedEmail;
}).suppress();
const fields = [
{ type: 'name', value: name },
{ type: 'phone', value: phone?.number },
{ type: 'email', value: validatedEmail },
{ type: 'username', value: username || visitor.username },
].filter((field) => Boolean(field.value)) as FieldAndValue[];
if (!fields.length) {
return null;
}
// If a visitor was updated who already had contacts, load up the contacts and update that information as well
const contacts = await LivechatContacts.findAllByVisitorId(visitor._id).toArray();
for await (const contact of contacts) {
await ContactMerger.mergeFieldsIntoContact({
fields,
contact,
conflictHandlingMode: contact.unknown ? 'overwrite' : 'conflict',
});
}
return visitor;
}
async function cleanGuestHistory(token: string) {
// This shouldn't be possible, but just in case
if (!token) {
throw new Error('error-invalid-guest');
}
const cursor = LivechatRooms.findByVisitorToken(token);
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),
]);
}
await LivechatRooms.removeByVisitorToken(token);
const livechatInquiries = await LivechatInquiry.findIdsByVisitorToken(token).toArray();
await LivechatInquiry.removeByIds(livechatInquiries.map(({ _id }) => _id));
void notifyOnLivechatInquiryChanged(livechatInquiries, 'removed');
}

@ -1,10 +1,7 @@
import { Message } from '@rocket.chat/core-services';
import type { ILivechatVisitor } from '@rocket.chat/core-typings';
import { LivechatInquiry, LivechatRooms, Messages, ReadReceipts, Subscriptions, Users } from '@rocket.chat/models';
import { Users } from '@rocket.chat/models';
import { livechatLogger } from './logger';
import { FileUpload } from '../../../file-upload/server';
import { notifyOnSubscriptionChanged, notifyOnLivechatInquiryChanged } from '../../../lib/server/lib/notifyListener';
import { settings } from '../../../settings/server';
type PageInfo = { title: string; location: { href: string }; change: string };
@ -55,32 +52,3 @@ export async function savePageHistory(token: string, roomId: string | undefined,
// @ts-expect-error: Investigating on which case we won't receive a roomId and where that history is supposed to be stored
return Message.saveSystemMessage('livechat_navigation_history', roomId, `${pageTitle} - ${pageUrl}`, user, extraData);
}
export async function cleanGuestHistory(guest: ILivechatVisitor) {
const { token } = guest;
// This shouldn't be possible, but just in case
if (!token) {
throw new Error('error-invalid-guest');
}
const cursor = LivechatRooms.findByVisitorToken(token);
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),
]);
}
await LivechatRooms.removeByVisitorToken(token);
const livechatInquiries = await LivechatInquiry.findIdsByVisitorToken(token).toArray();
await LivechatInquiry.removeByIds(livechatInquiries.map(({ _id }) => _id));
void notifyOnLivechatInquiryChanged(livechatInquiries, 'removed');
}

@ -14,9 +14,9 @@ import { stripHtml } from 'string-strip-html';
import { logger } from './logger';
import { FileUpload } from '../../../app/file-upload/server';
import { notifyOnMessageChange } from '../../../app/lib/server/lib/notifyListener';
import { Livechat as LivechatTyped } from '../../../app/livechat/server/lib/LivechatTyped';
import { QueueManager } from '../../../app/livechat/server/lib/QueueManager';
import { setDepartmentForGuest } from '../../../app/livechat/server/lib/departmentsLib';
import { registerGuest } from '../../../app/livechat/server/lib/guests';
import { sendMessage } from '../../../app/livechat/server/lib/messages';
import { settings } from '../../../app/settings/server';
import { i18n } from '../../lib/i18n';
@ -42,7 +42,7 @@ async function getGuestByEmail(email: string, name: string, department = ''): Pr
return guest;
}
const livechatVisitor = await LivechatTyped.registerGuest({
const livechatVisitor = await registerGuest({
token: Random.id(),
name: name || email,
email,

Loading…
Cancel
Save