diff --git a/apps/meteor/app/apps/server/bridges/livechat.ts b/apps/meteor/app/apps/server/bridges/livechat.ts index 361ba2098bb..896b7d0c540 100644 --- a/apps/meteor/app/apps/server/bridges/livechat.ts +++ b/apps/meteor/app/apps/server/bridges/livechat.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); } diff --git a/apps/meteor/app/livechat/imports/server/rest/sms.ts b/apps/meteor/app/livechat/imports/server/rest/sms.ts index 8b476c58886..b579f490a4d 100644 --- a/apps/meteor/app/livechat/imports/server/rest/sms.ts +++ b/apps/meteor/app/livechat/imports/server/rest/sms.ts @@ -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'); diff --git a/apps/meteor/app/livechat/server/api/v1/message.ts b/apps/meteor/app/livechat/server/api/v1/message.ts index 5fbeb138e67..75efff45094 100644 --- a/apps/meteor/app/livechat/server/api/v1/message.ts +++ b/apps/meteor/app/livechat/server/api/v1/message.ts @@ -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'); } diff --git a/apps/meteor/app/livechat/server/api/v1/room.ts b/apps/meteor/app/livechat/server/api/v1/room.ts index 19e9a716c46..9c07a1656d2 100644 --- a/apps/meteor/app/livechat/server/api/v1/room.ts +++ b/apps/meteor/app/livechat/server/api/v1/room.ts @@ -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) { diff --git a/apps/meteor/app/livechat/server/api/v1/visitor.ts b/apps/meteor/app/livechat/server/api/v1/visitor.ts index def6ef84edc..cddcd8a5ca0 100644 --- a/apps/meteor/app/livechat/server/api/v1/visitor.ts +++ b/apps/meteor/app/livechat/server/api/v1/visitor.ts @@ -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'); } diff --git a/apps/meteor/app/livechat/server/lib/LivechatTyped.ts b/apps/meteor/app/livechat/server/lib/LivechatTyped.ts index 38d5375b038..62a2163af1b 100644 --- a/apps/meteor/app/livechat/server/lib/LivechatTyped.ts +++ b/apps/meteor/app/livechat/server/lib/LivechatTyped.ts @@ -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 = { [K in keyof T]?: T[K]; @@ -168,16 +162,6 @@ class LivechatClass { } } - async registerGuest(newData: RegisterGuestType): Promise { - 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, fields?: AKeyOf) { 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 & { 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 = {}; - - 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}`); diff --git a/apps/meteor/app/livechat/server/lib/contacts/registerGuestData.ts b/apps/meteor/app/livechat/server/lib/contacts/registerGuestData.ts deleted file mode 100644 index 471104aecae..00000000000 --- a/apps/meteor/app/livechat/server/lib/contacts/registerGuestData.ts +++ /dev/null @@ -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, - visitor: AtLeast, -): Promise { - 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', - }); - } -} diff --git a/apps/meteor/app/livechat/server/lib/guests.ts b/apps/meteor/app/livechat/server/lib/guests.ts new file mode 100644 index 00000000000..e32c6c9c493 --- /dev/null +++ b/apps/meteor/app/livechat/server/lib/guests.ts @@ -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 & { 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 = {}; + + 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 { + 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'); +} diff --git a/apps/meteor/app/livechat/server/lib/tracking.ts b/apps/meteor/app/livechat/server/lib/tracking.ts index 5e21bb4c38e..bfbcf991221 100644 --- a/apps/meteor/app/livechat/server/lib/tracking.ts +++ b/apps/meteor/app/livechat/server/lib/tracking.ts @@ -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'); -} diff --git a/apps/meteor/server/features/EmailInbox/EmailInbox_Incoming.ts b/apps/meteor/server/features/EmailInbox/EmailInbox_Incoming.ts index 1825442be8e..2fc4f164f9e 100644 --- a/apps/meteor/server/features/EmailInbox/EmailInbox_Incoming.ts +++ b/apps/meteor/server/features/EmailInbox/EmailInbox_Incoming.ts @@ -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,