From 7a57f3452fd26a603948b70af8f728953afee53f Mon Sep 17 00:00:00 2001 From: Ricardo Garim Date: Thu, 27 Jun 2024 18:46:01 -0300 Subject: [PATCH] refactor: return created record from registerGuest (#32620) Co-authored-by: Guilherme Gazzo Co-authored-by: Diego Sampaio --- .../app/apps/server/bridges/livechat.ts | 28 ++++- .../app/livechat/imports/server/rest/sms.ts | 9 +- .../app/livechat/server/api/v1/message.ts | 8 +- .../app/livechat/server/api/v1/visitor.ts | 48 ++++---- .../app/livechat/server/lib/LivechatTyped.ts | 109 ++++++++---------- .../livechat/server/methods/registerGuest.ts | 32 ++--- apps/meteor/package.json | 2 +- .../EmailInbox/EmailInbox_Incoming.ts | 11 +- .../server/models/raw/LivechatVisitors.ts | 19 +++ .../src/models/ILivechatVisitorsModel.ts | 14 ++- yarn.lock | 25 +++- 11 files changed, 192 insertions(+), 113 deletions(-) diff --git a/apps/meteor/app/apps/server/bridges/livechat.ts b/apps/meteor/app/apps/server/bridges/livechat.ts index c2a987148864..22ee93ac8041 100644 --- a/apps/meteor/app/apps/server/bridges/livechat.ts +++ b/apps/meteor/app/apps/server/bridges/livechat.ts @@ -201,7 +201,33 @@ export class AppLivechatBridge extends LivechatBridge { ...(visitor.visitorEmails?.length && { email: visitor.visitorEmails[0].address }), }; - return LivechatTyped.registerGuest(registerData); + const livechatVisitor = await LivechatTyped.registerGuest(registerData); + + if (!livechatVisitor) { + throw new Error('Invalid visitor, cannot create'); + } + + return livechatVisitor._id; + } + + protected async createAndReturnVisitor(visitor: IVisitor, appId: string): Promise { + this.orch.debugLog(`The App ${appId} is creating a livechat visitor.`); + + const registerData = { + department: visitor.department, + username: visitor.username, + name: visitor.name, + token: visitor.token, + email: '', + connectionData: undefined, + id: visitor.id, + ...(visitor.phone?.length && { phone: { number: visitor.phone[0].phoneNumber } }), + ...(visitor.visitorEmails?.length && { email: visitor.visitorEmails[0].address }), + }; + + const livechatVisitor = await LivechatTyped.registerGuest(registerData); + + return this.orch.getConverters()?.get('visitors').convertVisitor(livechatVisitor); } protected async transferVisitor(visitor: IVisitor, transferData: ILivechatTransferData, appId: string): Promise { diff --git a/apps/meteor/app/livechat/imports/server/rest/sms.ts b/apps/meteor/app/livechat/imports/server/rest/sms.ts index 2551b79cc425..27f11a6c9808 100644 --- a/apps/meteor/app/livechat/imports/server/rest/sms.ts +++ b/apps/meteor/app/livechat/imports/server/rest/sms.ts @@ -67,8 +67,13 @@ const defineVisitor = async (smsNumber: string, targetDepartment?: string) => { data.department = targetDepartment; } - const id = await LivechatTyped.registerGuest(data); - return LivechatVisitors.findOneEnabledById(id); + const livechatVisitor = await LivechatTyped.registerGuest(data); + + if (!livechatVisitor) { + throw new Meteor.Error('error-invalid-visitor', 'Invalid visitor'); + } + + return livechatVisitor; }; const normalizeLocationSharing = (payload: ServiceData) => { diff --git a/apps/meteor/app/livechat/server/api/v1/message.ts b/apps/meteor/app/livechat/server/api/v1/message.ts index 1dcf54e403a6..921d48ba0461 100644 --- a/apps/meteor/app/livechat/server/api/v1/message.ts +++ b/apps/meteor/app/livechat/server/api/v1/message.ts @@ -251,7 +251,7 @@ API.v1.addRoute( async post() { const visitorToken = this.bodyParams.visitor.token; - let visitor = await LivechatVisitors.getVisitorByToken(visitorToken, {}); + const visitor = await LivechatVisitors.getVisitorByToken(visitorToken, {}); let rid: string; if (visitor) { const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {}); @@ -267,8 +267,10 @@ API.v1.addRoute( const guest: typeof this.bodyParams.visitor & { connectionData?: unknown } = this.bodyParams.visitor; guest.connectionData = normalizeHttpHeaderData(this.request.headers); - const visitorId = await LivechatTyped.registerGuest(guest); - visitor = await LivechatVisitors.findOneEnabledById(visitorId); + const visitor = await LivechatTyped.registerGuest(guest); + if (!visitor) { + throw new Error('error-livechat-visitor-registration'); + } } const guest = visitor; diff --git a/apps/meteor/app/livechat/server/api/v1/visitor.ts b/apps/meteor/app/livechat/server/api/v1/visitor.ts index 9c19f5bbdec8..a5b3f2de35b1 100644 --- a/apps/meteor/app/livechat/server/api/v1/visitor.ts +++ b/apps/meteor/app/livechat/server/api/v1/visitor.ts @@ -1,4 +1,4 @@ -import type { ILivechatCustomField, ILivechatVisitor, IRoom } from '@rocket.chat/core-typings'; +import type { ILivechatCustomField, IRoom } from '@rocket.chat/core-typings'; import { LivechatVisitors as VisitorsRaw, LivechatCustomField, LivechatRooms } from '@rocket.chat/models'; import { Match, check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; @@ -47,27 +47,29 @@ API.v1.addRoute('livechat/visitor', { connectionData: normalizeHttpHeaderData(this.request.headers), }; - const visitorId = await LivechatTyped.registerGuest(guest); - - let visitor: ILivechatVisitor | null = await VisitorsRaw.findOneEnabledById(visitorId, {}); - if (visitor) { - const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {}); - // If it's updating an existing visitor, it must also update the roomInfo - const rooms = await LivechatRooms.findOpenByVisitorToken(visitor?.token, {}, extraQuery).toArray(); - await Promise.all( - rooms.map( - (room: IRoom) => - visitor && - LivechatTyped.saveRoomInfo(room, { - _id: visitor._id, - name: visitor.name, - phone: visitor.phone?.[0]?.phoneNumber, - livechatData: visitor.livechatData as { [k: string]: string }, - }), - ), - ); + const visitor = await LivechatTyped.registerGuest(guest); + if (!visitor) { + throw new Meteor.Error('error-livechat-visitor-registration', 'Error registering visitor', { + method: 'livechat/visitor', + }); } + const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {}); + // If it's updating an existing visitor, it must also update the roomInfo + const rooms = await LivechatRooms.findOpenByVisitorToken(visitor?.token, {}, extraQuery).toArray(); + await Promise.all( + rooms.map( + (room: IRoom) => + visitor && + LivechatTyped.saveRoomInfo(room, { + _id: visitor._id, + name: visitor.name, + phone: visitor.phone?.[0]?.phoneNumber, + livechatData: visitor.livechatData as { [k: string]: string }, + }), + ), + ); + if (customFields && Array.isArray(customFields) && customFields.length > 0) { const keys = customFields.map((field) => field.key); const errors: string[] = []; @@ -96,7 +98,7 @@ API.v1.addRoute('livechat/visitor', { if (processedKeys.length !== keys.length) { LivechatTyped.logger.warn({ msg: 'Some custom fields were not processed', - visitorId, + visitorId: visitor._id, missingKeys: keys.filter((key) => !processedKeys.includes(key)), }); } @@ -104,13 +106,13 @@ API.v1.addRoute('livechat/visitor', { if (errors.length > 0) { LivechatTyped.logger.error({ msg: 'Error updating custom fields', - visitorId, + visitorId: visitor._id, errors, }); throw new Error('error-updating-custom-fields'); } - visitor = await VisitorsRaw.findOneEnabledById(visitorId, {}); + return API.v1.success({ visitor: await VisitorsRaw.findOneEnabledById(visitor._id) }); } if (!visitor) { diff --git a/apps/meteor/app/livechat/server/lib/LivechatTyped.ts b/apps/meteor/app/livechat/server/lib/LivechatTyped.ts index c22b984ffe3a..2aa923acf75f 100644 --- a/apps/meteor/app/livechat/server/lib/LivechatTyped.ts +++ b/apps/meteor/app/livechat/server/lib/LivechatTyped.ts @@ -43,7 +43,7 @@ import { serverFetch as fetch } from '@rocket.chat/server-fetch'; import { Match, check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; import moment from 'moment-timezone'; -import type { Filter, FindCursor, UpdateFilter } from 'mongodb'; +import type { Filter, FindCursor } from 'mongodb'; import UAParser from 'ua-parser-js'; import { callbacks } from '../../../../lib/callbacks'; @@ -76,6 +76,13 @@ import { QueueManager } from './QueueManager'; import { RoutingManager } from './RoutingManager'; import { isDepartmentCreationAvailable } from './isDepartmentCreationAvailable'; +type RegisterGuestType = Partial> & { + id?: string; + connectionData?: any; + email?: string; + phone?: { number: string }; +}; + type GenericCloseRoomParams = { room: IOmnichannelRoom; comment?: string; @@ -426,6 +433,7 @@ class LivechatClass { if (room == null) { const defaultAgent = await callbacks.run('livechat.checkDefaultAgentOnNewRoom', agent, guest); + // if no department selected verify if there is at least one active and pick the first if (!defaultAgent && !guest.department) { const department = await this.getRequiredDepartment(); @@ -439,6 +447,7 @@ class LivechatClass { // delegate room creation to QueueManager Livechat.logger.debug(`Calling QueueManager to request a room for visitor ${guest._id}`); + room = await QueueManager.requestRoom({ guest, message, @@ -666,105 +675,89 @@ class LivechatClass { id, token, name, + phone, email, department, - phone, username, connectionData, status = UserStatus.ONLINE, - }: { - id?: string; - token: string; - name?: string; - email?: string; - department?: string; - phone?: { number: string }; - username?: string; - connectionData?: any; - status?: ILivechatVisitor['status']; - }) { + }: RegisterGuestType): Promise { check(token, String); check(id, Match.Maybe(String)); Livechat.logger.debug(`New incoming conversation: id: ${id} | token: ${token}`); - let userId; - type Mutable = { - -readonly [Key in keyof Type]: Type[Key]; - }; - - type UpdateUserType = Required, '$set'>>; - const updateUser: Required, '$set'>> = { - $set: { - token, - status, - ...(phone?.number ? { phone: [{ phoneNumber: phone.number }] } : {}), - ...(name ? { name } : {}), - }, + const visitorDataToUpdate: Partial & { userAgent?: string; ip?: string; host?: string } = { + token, + status, + ...(phone?.number ? { phone: [{ phoneNumber: phone.number }] } : {}), + ...(name ? { name } : {}), }; if (email) { - email = email.trim().toLowerCase(); - validateEmail(email); - (updateUser.$set as Mutable).visitorEmails = [{ address: email }]; + const visitorEmail = email.trim().toLowerCase(); + validateEmail(visitorEmail); + visitorDataToUpdate.visitorEmails = [{ address: visitorEmail }]; } if (department) { Livechat.logger.debug(`Attempt to find a department with id/name ${department}`); const dep = await LivechatDepartment.findOneByIdOrName(department, { projection: { _id: 1 } }); if (!dep) { - Livechat.logger.debug('Invalid department provided'); + Livechat.logger.debug(`Invalid department provided: ${department}`); throw new Meteor.Error('error-invalid-department', 'The provided department is invalid'); } Livechat.logger.debug(`Assigning visitor ${token} to department ${dep._id}`); - (updateUser.$set as Mutable).department = dep._id; + visitorDataToUpdate.department = dep._id; } - const user = await LivechatVisitors.getVisitorByToken(token, { projection: { _id: 1 } }); + const livechatVisitor = await LivechatVisitors.getVisitorByToken(token, { projection: { _id: 1 } }); + + visitorDataToUpdate.token = livechatVisitor?.token || token; + let existingUser = null; - if (user) { + if (livechatVisitor) { Livechat.logger.debug('Found matching user by token'); - userId = user._id; + visitorDataToUpdate._id = livechatVisitor._id; } else if (phone?.number && (existingUser = await LivechatVisitors.findOneVisitorByPhone(phone.number))) { Livechat.logger.debug('Found matching user by phone number'); - userId = existingUser._id; + visitorDataToUpdate._id = existingUser._id; // Don't change token when matching by phone number, use current visitor token - (updateUser.$set as Mutable).token = existingUser.token; + visitorDataToUpdate.token = existingUser.token; } else if (email && (existingUser = await LivechatVisitors.findOneGuestByEmailAddress(email))) { Livechat.logger.debug('Found matching user by email'); - userId = existingUser._id; - } else { + visitorDataToUpdate._id = existingUser._id; + } else if (!livechatVisitor) { Livechat.logger.debug(`No matches found. Attempting to create new user with token ${token}`); - if (!username) { - username = await LivechatVisitors.getNextVisitorUsername(); - } - const userData = { - username, - status, - ts: new Date(), - token, - ...(id && { _id: id }), - }; + visitorDataToUpdate._id = id || undefined; + visitorDataToUpdate.username = username || (await LivechatVisitors.getNextVisitorUsername()); + visitorDataToUpdate.status = status; + visitorDataToUpdate.ts = new Date(); if (settings.get('Livechat_Allow_collect_and_store_HTTP_header_informations')) { Livechat.logger.debug(`Saving connection data for visitor ${token}`); - const connection = connectionData; - if (connection?.httpHeaders) { - (updateUser.$set as Mutable).userAgent = connection.httpHeaders['user-agent']; - (updateUser.$set as Mutable).ip = - connection.httpHeaders['x-real-ip'] || connection.httpHeaders['x-forwarded-for'] || connection.clientAddress; - (updateUser.$set as Mutable).host = connection.httpHeaders.host; + const { httpHeaders, clientAddress } = connectionData; + if (httpHeaders) { + visitorDataToUpdate.userAgent = httpHeaders['user-agent']; + visitorDataToUpdate.ip = httpHeaders['x-real-ip'] || httpHeaders['x-forwarded-for'] || clientAddress; + visitorDataToUpdate.host = httpHeaders?.host; } } - - userId = (await LivechatVisitors.insertOne(userData)).insertedId; } - await LivechatVisitors.updateById(userId, updateUser); + const upsertedLivechatVisitor = await LivechatVisitors.updateOneByIdOrToken(visitorDataToUpdate, { + upsert: true, + returnDocument: 'after', + }); + + if (!upsertedLivechatVisitor.value) { + Livechat.logger.debug(`No visitor found after upsert`); + return null; + } - return userId; + return upsertedLivechatVisitor.value; } private async getBotAgents(department?: string) { diff --git a/apps/meteor/app/livechat/server/methods/registerGuest.ts b/apps/meteor/app/livechat/server/methods/registerGuest.ts index 01f720b85a4d..4a531d0c89e5 100644 --- a/apps/meteor/app/livechat/server/methods/registerGuest.ts +++ b/apps/meteor/app/livechat/server/methods/registerGuest.ts @@ -23,21 +23,24 @@ declare module '@rocket.chat/ui-contexts' { department?: string; customFields?: Array<{ key: string; value: string; overwrite: boolean; scope?: unknown }>; }): { - userId: string; - visitor: ILivechatVisitor | null; + userId: ILivechatVisitor['_id']; + visitor: Pick; }; } } Meteor.methods({ - async 'livechat:registerGuest'({ token, name, email, department, customFields } = {}) { + async 'livechat:registerGuest'({ token, name, email, department, customFields } = {}): Promise<{ + userId: ILivechatVisitor['_id']; + visitor: Pick; + }> { methodDeprecationLogger.method('livechat:registerGuest', '7.0.0'); if (!token) { throw new Meteor.Error('error-invalid-token', 'Invalid token', { method: 'livechat:registerGuest' }); } - const userId = await LivechatTyped.registerGuest.call(this, { + const visitor = await LivechatTyped.registerGuest.call(this, { token, name, email, @@ -47,16 +50,6 @@ Meteor.methods({ // update visited page history to not expire await Messages.keepHistoryForToken(token); - const visitor = await LivechatVisitors.getVisitorByToken(token, { - projection: { - token: 1, - name: 1, - username: 1, - visitorEmails: 1, - department: 1, - }, - }); - if (!visitor) { throw new Meteor.Error('error-invalid-visitor', 'Invalid visitor', { method: 'livechat:registerGuest' }); } @@ -89,8 +82,15 @@ Meteor.methods({ } return { - userId, - visitor, + userId: visitor._id, + visitor: { + _id: visitor._id, + token: visitor.token, + name: visitor.name, + username: visitor.username, + visitorEmails: visitor.visitorEmails, + department: visitor.department, + }, }; }, }); diff --git a/apps/meteor/package.json b/apps/meteor/package.json index 816379df08a9..c78ce6e0a7ab 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -231,7 +231,7 @@ "@rocket.chat/agenda": "workspace:^", "@rocket.chat/api-client": "workspace:^", "@rocket.chat/apps": "workspace:^", - "@rocket.chat/apps-engine": "alpha", + "@rocket.chat/apps-engine": "^1.43.0-alpha.773", "@rocket.chat/base64": "workspace:^", "@rocket.chat/cas-validate": "workspace:^", "@rocket.chat/core-services": "workspace:^", diff --git a/apps/meteor/server/features/EmailInbox/EmailInbox_Incoming.ts b/apps/meteor/server/features/EmailInbox/EmailInbox_Incoming.ts index d974aa9c91be..642d989f85b6 100644 --- a/apps/meteor/server/features/EmailInbox/EmailInbox_Incoming.ts +++ b/apps/meteor/server/features/EmailInbox/EmailInbox_Incoming.ts @@ -40,21 +40,18 @@ async function getGuestByEmail(email: string, name: string, department = ''): Pr return guest; } - const userId = await LivechatTyped.registerGuest({ + const livechatVisitor = await LivechatTyped.registerGuest({ token: Random.id(), name: name || email, email, department, }); - const newGuest = await LivechatVisitors.findOneEnabledById(userId); - logger.debug(`Guest ${userId} for visitor ${email} created`); - - if (newGuest) { - return newGuest; + if (!livechatVisitor) { + throw new Error('Error getting guest'); } - throw new Error('Error getting guest'); + return livechatVisitor; } async function uploadAttachment(attachmentParam: Attachment, rid: string, visitorToken: string): Promise> { diff --git a/apps/meteor/server/models/raw/LivechatVisitors.ts b/apps/meteor/server/models/raw/LivechatVisitors.ts index e5234a8d5712..502992ea75f8 100644 --- a/apps/meteor/server/models/raw/LivechatVisitors.ts +++ b/apps/meteor/server/models/raw/LivechatVisitors.ts @@ -14,7 +14,10 @@ import type { IndexDescription, DeleteResult, UpdateFilter, + ModifyResult, + FindOneAndUpdateOptions, } from 'mongodb'; +import { ObjectId } from 'mongodb'; import { notifyOnSettingChanged } from '../../../app/lib/server/lib/notifyListener'; import { BaseRaw } from './BaseRaw'; @@ -282,6 +285,22 @@ export class LivechatVisitorsRaw extends BaseRaw implements IL return this.updateOne({ _id }, update); } + async updateOneByIdOrToken( + update: Partial, + options?: FindOneAndUpdateOptions, + ): Promise> { + let query: Filter = {}; + + if (update._id) { + query = { _id: update._id }; + } else if (update.token) { + query = { token: update.token }; + update._id = new ObjectId().toHexString(); + } + + return this.findOneAndUpdate(query, { $set: update }, options); + } + saveGuestById( _id: string, data: { name?: string; username?: string; email?: string; phone?: string; livechatData: { [k: string]: any } }, diff --git a/packages/model-typings/src/models/ILivechatVisitorsModel.ts b/packages/model-typings/src/models/ILivechatVisitorsModel.ts index b899d42a015f..2e4441226661 100644 --- a/packages/model-typings/src/models/ILivechatVisitorsModel.ts +++ b/packages/model-typings/src/models/ILivechatVisitorsModel.ts @@ -1,5 +1,15 @@ import type { ILivechatVisitor } from '@rocket.chat/core-typings'; -import type { AggregationCursor, FindCursor, Filter, FindOptions, UpdateResult, Document, UpdateFilter } from 'mongodb'; +import type { + AggregationCursor, + FindCursor, + Filter, + FindOptions, + UpdateResult, + Document, + UpdateFilter, + FindOneAndUpdateOptions, + ModifyResult, +} from 'mongodb'; import type { FindPaginated, IBaseModel } from './IBaseModel'; @@ -47,6 +57,8 @@ export interface ILivechatVisitorsModel extends IBaseModel { updateById(_id: string, update: UpdateFilter): Promise; + updateOneByIdOrToken(update: UpdateFilter, options?: FindOneAndUpdateOptions): Promise>; + saveGuestEmailPhoneById(_id: string, emails: string[], phones: string[]): Promise; isVisitorActiveOnPeriod(visitorId: string, period: string): Promise; diff --git a/yarn.lock b/yarn.lock index 116c715d3c60..5d7943ccd130 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8508,6 +8508,29 @@ __metadata: languageName: unknown linkType: soft +"@rocket.chat/apps-engine@npm:^1.43.0-alpha.773": + version: 1.43.0-deno.761 + resolution: "@rocket.chat/apps-engine@npm:1.43.0-deno.761" + dependencies: + "@msgpack/msgpack": 3.0.0-beta2 + adm-zip: ^0.5.9 + cryptiles: ^4.1.3 + debug: ^4.3.4 + deno-bin: 1.37.1 + esbuild: ^0.20.2 + jose: ^4.11.1 + jsonrpc-lite: ^2.2.0 + lodash.clonedeep: ^4.5.0 + semver: ^5.7.1 + stack-trace: 0.0.10 + uuid: ~8.3.2 + vm2: ^3.9.19 + peerDependencies: + "@rocket.chat/ui-kit": "*" + checksum: cbcc1cd818504a0d5a300635192053592d54234d640696d14e40f67acf1129bca8a952d34365e08e888bd22ef4ebcae0f9f45888f9b724a627a3b7e48380ab3d + languageName: node + linkType: hard + "@rocket.chat/apps-engine@npm:alpha": version: 1.43.0-alpha.765 resolution: "@rocket.chat/apps-engine@npm:1.43.0-alpha.765" @@ -9356,7 +9379,7 @@ __metadata: "@rocket.chat/agenda": "workspace:^" "@rocket.chat/api-client": "workspace:^" "@rocket.chat/apps": "workspace:^" - "@rocket.chat/apps-engine": alpha + "@rocket.chat/apps-engine": ^1.43.0-alpha.773 "@rocket.chat/base64": "workspace:^" "@rocket.chat/cas-validate": "workspace:^" "@rocket.chat/core-services": "workspace:^"