Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: return created record from registerGuest #32620

Merged
merged 14 commits into from
Jun 27, 2024
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 27 additions & 1 deletion apps/meteor/app/apps/server/bridges/livechat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,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<IVisitor | undefined> {
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<boolean> {
Expand Down
9 changes: 7 additions & 2 deletions apps/meteor/app/livechat/imports/server/rest/sms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 LivechatVisitors.findOneEnabledById(livechatVisitor._id);
};

const normalizeLocationSharing = (payload: ServiceData) => {
Expand Down
7 changes: 5 additions & 2 deletions apps/meteor/app/livechat/server/api/v1/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -267,8 +267,11 @@ 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 livechatVisitor = await LivechatTyped.registerGuest(guest);
if (!livechatVisitor) {
throw new Error('error-livechat-visitor-registration');
}
visitor = await LivechatVisitors.findOneEnabledById(livechatVisitor?._id);
}

const guest = visitor;
Expand Down
15 changes: 10 additions & 5 deletions apps/meteor/app/livechat/server/api/v1/visitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,14 @@ API.v1.addRoute('livechat/visitor', {
connectionData: normalizeHttpHeaderData(this.request.headers),
};

const visitorId = await LivechatTyped.registerGuest(guest);
const livechatVisitor = await LivechatTyped.registerGuest(guest);
if (!livechatVisitor) {
throw new Meteor.Error('error-livechat-visitor-registration', 'Error registering visitor', {
method: 'livechat/visitor',
});
}

let visitor: ILivechatVisitor | null = await VisitorsRaw.findOneEnabledById(visitorId, {});
let visitor: ILivechatVisitor | null = await VisitorsRaw.findOneEnabledById(livechatVisitor._id, {});
if (visitor) {
const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {});
// If it's updating an existing visitor, it must also update the roomInfo
Expand Down Expand Up @@ -96,21 +101,21 @@ API.v1.addRoute('livechat/visitor', {
if (processedKeys.length !== keys.length) {
LivechatTyped.logger.warn({
msg: 'Some custom fields were not processed',
visitorId,
visitorId: livechatVisitor._id,
missingKeys: keys.filter((key) => !processedKeys.includes(key)),
});
}

if (errors.length > 0) {
LivechatTyped.logger.error({
msg: 'Error updating custom fields',
visitorId,
visitorId: livechatVisitor._id,
errors,
});
throw new Error('error-updating-custom-fields');
}

visitor = await VisitorsRaw.findOneEnabledById(visitorId, {});
visitor = await VisitorsRaw.findOneEnabledById(livechatVisitor._id);
}

if (!visitor) {
Expand Down
111 changes: 54 additions & 57 deletions apps/meteor/app/livechat/server/lib/LivechatTyped.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,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';
Expand All @@ -68,6 +68,13 @@ import { QueueManager } from './QueueManager';
import { RoutingManager } from './RoutingManager';
import { isDepartmentCreationAvailable } from './isDepartmentCreationAvailable';

type RegisterGuestType = Partial<Pick<ILivechatVisitor, 'token' | 'name' | 'department' | 'status' | 'username'>> & {
id?: string;
connectionData?: any;
email?: string;
phone?: { number: string };
};

type GenericCloseRoomParams = {
room: IOmnichannelRoom;
comment?: string;
Expand Down Expand Up @@ -407,6 +414,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();
Expand All @@ -420,6 +428,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,
Expand Down Expand Up @@ -641,105 +650,93 @@ 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<ILivechatVisitor | null> {
check(token, String);
check(id, Match.Maybe(String));

Livechat.logger.debug(`New incoming conversation: id: ${id} | token: ${token}`);

let userId;
type Mutable<Type> = {
-readonly [Key in keyof Type]: Type[Key];
};

type UpdateUserType = Required<Pick<UpdateFilter<ILivechatVisitor>, '$set'>>;
const updateUser: Required<Pick<UpdateFilter<ILivechatVisitor>, '$set'>> = {
$set: {
token,
status,
...(phone?.number ? { phone: [{ phoneNumber: phone.number }] } : {}),
...(name ? { name } : {}),
},
const visitorDataToUpdate: Partial<ILivechatVisitor> & { 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<UpdateUserType['$set']>).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<UpdateUserType['$set']>).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<UpdateUserType['$set']>).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) {
ricardogarim marked this conversation as resolved.
Show resolved Hide resolved
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<UpdateUserType['$set']>).userAgent = connection.httpHeaders['user-agent'];
(updateUser.$set as Mutable<UpdateUserType['$set']>).ip =
connection.httpHeaders['x-real-ip'] || connection.httpHeaders['x-forwarded-for'] || connection.clientAddress;
(updateUser.$set as Mutable<UpdateUserType['$set']>).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;
}
}
}

Livechat.logger.debug(visitorDataToUpdate);

const upsertedLivechatVisitor = await LivechatVisitors.updateOneByIdOrToken(visitorDataToUpdate, {
upsert: true,
returnDocument: 'after',
});

userId = (await LivechatVisitors.insertOne(userData)).insertedId;
if (!upsertedLivechatVisitor.value) {
Livechat.logger.debug(`No visitor found after upsert`);
return null;
}

await LivechatVisitors.updateById(userId, updateUser);
Livechat.logger.debug(`Visitor ${upsertedLivechatVisitor.value._id} created/updated`);

return userId;
return upsertedLivechatVisitor.value;
}

private async getBotAgents(department?: string) {
Expand Down
10 changes: 7 additions & 3 deletions apps/meteor/app/livechat/server/methods/registerGuest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,17 @@ declare module '@rocket.chat/ui-contexts' {
}

Meteor.methods<ServerMethods>({
async 'livechat:registerGuest'({ token, name, email, department, customFields } = {}) {
async 'livechat:registerGuest'({ token, name, email, department, customFields } = {}): Promise<{
userId: string;
visitor: ILivechatVisitor | null;
}> {
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 livechatVisitor = await LivechatTyped.registerGuest.call(this, {
token,
name,
email,
Expand All @@ -47,6 +50,7 @@ Meteor.methods<ServerMethods>({
// update visited page history to not expire
await Messages.keepHistoryForToken(token);

// TODO: remove unneeded find - registerGuest returns created record
const visitor = await LivechatVisitors.getVisitorByToken(token, {
projection: {
token: 1,
Expand Down Expand Up @@ -89,7 +93,7 @@ Meteor.methods<ServerMethods>({
}

return {
userId,
userId: livechatVisitor?._id ?? '',
ricardogarim marked this conversation as resolved.
Show resolved Hide resolved
visitor,
};
},
Expand Down
2 changes: 1 addition & 1 deletion apps/meteor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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:^",
Expand Down
10 changes: 7 additions & 3 deletions apps/meteor/server/features/EmailInbox/EmailInbox_Incoming.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,15 +40,19 @@ 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 (!livechatVisitor) {
throw new Error('Error getting guest');
}

const newGuest = await LivechatVisitors.findOneEnabledById(livechatVisitor._id);
logger.debug(`Guest ${livechatVisitor._id} for visitor ${email} created`);

if (newGuest) {
return newGuest;
Expand Down
19 changes: 19 additions & 0 deletions apps/meteor/server/models/raw/LivechatVisitors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@ import type {
IndexDescription,
DeleteResult,
UpdateFilter,
ModifyResult,
FindOneAndUpdateOptions,
} from 'mongodb';
import { ObjectId } from 'mongodb';

import { BaseRaw } from './BaseRaw';

Expand Down Expand Up @@ -290,6 +293,22 @@ export class LivechatVisitorsRaw extends BaseRaw<ILivechatVisitor> implements IL
return this.updateOne({ _id }, update);
}

async updateOneByIdOrToken(
update: Partial<ILivechatVisitor>,
options?: FindOneAndUpdateOptions,
): Promise<ModifyResult<ILivechatVisitor>> {
let query: Filter<ILivechatVisitor> = {};

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 } },
Expand Down
Loading
Loading