Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
12 changes: 7 additions & 5 deletions apps/meteor/app/apps/server/bridges/livechat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@ import { LivechatBridge } from '@rocket.chat/apps-engine/server/bridges/Livechat
import type { ILivechatDepartment, IOmnichannelRoom, SelectedAgent, IMessage, ILivechatVisitor } from '@rocket.chat/core-typings';
import { OmnichannelSourceType } from '@rocket.chat/core-typings';
import { LivechatVisitors, LivechatRooms, LivechatDepartment, Users } from '@rocket.chat/models';
import { registerGuest } from '@rocket.chat/omni-core';

import { callbacks } from '../../../../lib/callbacks';
import { deasyncPromise } from '../../../../server/deasync/deasync';
import { closeRoom } from '../../../livechat/server/lib/closeRoom';
import { setCustomFields } from '../../../livechat/server/lib/custom-fields';
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';
Expand Down Expand Up @@ -207,13 +207,14 @@ export class AppLivechatBridge extends LivechatBridge {
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 registerGuest(registerData);
const livechatVisitor = await registerGuest(registerData, {
shouldConsiderIdleAgent: settings.get<boolean>('Livechat_enabled_when_agent_idle'),
});

if (!livechatVisitor) {
throw new Error('Invalid visitor, cannot create');
Expand All @@ -231,13 +232,14 @@ export class AppLivechatBridge extends LivechatBridge {
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 registerGuest(registerData);
const livechatVisitor = await registerGuest(registerData, {
shouldConsiderIdleAgent: settings.get<boolean>('Livechat_enabled_when_agent_idle'),
});

return this.orch.getConverters()?.get('visitors').convertVisitor(livechatVisitor);
}
Expand Down
4 changes: 2 additions & 2 deletions apps/meteor/app/livechat/imports/server/rest/sms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type {
import { OmnichannelSourceType } from '@rocket.chat/core-typings';
import { Logger } from '@rocket.chat/logger';
import { LivechatVisitors, LivechatRooms, LivechatDepartment } from '@rocket.chat/models';
import { registerGuest } from '@rocket.chat/omni-core';
import { Random } from '@rocket.chat/random';
import { serverFetch as fetch } from '@rocket.chat/server-fetch';
import { Meteor } from 'meteor/meteor';
Expand All @@ -20,7 +21,6 @@ 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 { registerGuest } from '../../../server/lib/guests';
import type { ILivechatMessage } from '../../../server/lib/localTypes';
import { sendMessage } from '../../../server/lib/messages';
import { createRoom } from '../../../server/lib/rooms';
Expand Down Expand Up @@ -76,7 +76,7 @@ const defineVisitor = async (smsNumber: string, targetDepartment?: string) => {
data.department = targetDepartment;
}

const livechatVisitor = await registerGuest(data);
const livechatVisitor = await registerGuest(data, { shouldConsiderIdleAgent: settings.get<boolean>('Livechat_enabled_when_agent_idle') });

if (!livechatVisitor) {
throw new Meteor.Error('error-invalid-visitor', 'Invalid visitor');
Expand Down
9 changes: 6 additions & 3 deletions apps/meteor/app/livechat/server/api/v1/message.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { OmnichannelSourceType } from '@rocket.chat/core-typings';
import { LivechatVisitors, LivechatRooms, Messages } from '@rocket.chat/models';
import { registerGuest } from '@rocket.chat/omni-core';
import { Random } from '@rocket.chat/random';
import {
isPOSTLivechatMessageParams,
Expand All @@ -17,7 +18,6 @@ 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 { registerGuest } from '../../lib/guests';
import { updateMessage, deleteMessage, sendMessage } from '../../lib/messages';
import { findGuest, findRoom, normalizeHttpHeaderData } from '../lib/livechat';

Expand Down Expand Up @@ -267,9 +267,12 @@ API.v1.addRoute(
rid = Random.id();

const guest: typeof this.bodyParams.visitor & { connectionData?: unknown } = this.bodyParams.visitor;
guest.connectionData = normalizeHttpHeaderData(this.request.headers);

const visitor = await registerGuest(guest);
if (settings.get('Livechat_Allow_collect_and_store_HTTP_header_informations')) {
guest.connectionData = normalizeHttpHeaderData(this.request.headers);
}

const visitor = await registerGuest(guest, { shouldConsiderIdleAgent: settings.get<boolean>('Livechat_enabled_when_agent_idle') });
if (!visitor) {
throw new Error('error-livechat-visitor-registration');
}
Expand Down
5 changes: 3 additions & 2 deletions apps/meteor/app/livechat/server/api/v1/visitor.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import type { IRoom } from '@rocket.chat/core-typings';
import { LivechatVisitors as VisitorsRaw, LivechatRooms } from '@rocket.chat/models';
import { registerGuest } from '@rocket.chat/omni-core';
import { Match, check } from 'meteor/check';
import { Meteor } from 'meteor/meteor';

import { callbacks } from '../../../../../lib/callbacks';
import { API } from '../../../../api/server';
import { settings } from '../../../../settings/server';
import { setMultipleVisitorCustomFields } from '../../lib/custom-fields';
import { registerGuest, notifyGuestStatusChanged, removeContactsByVisitorId } from '../../lib/guests';
import { notifyGuestStatusChanged, removeContactsByVisitorId } from '../../lib/guests';
import { livechatLogger } from '../../lib/logger';
import { saveRoomInfo } from '../../lib/rooms';
import { updateCallStatus } from '../../lib/utils';
Expand Down Expand Up @@ -59,7 +60,7 @@ API.v1.addRoute(
connectionData: normalizeHttpHeaderData(this.request.headers),
};

const visitor = await registerGuest(guest);
const visitor = await registerGuest(guest, { shouldConsiderIdleAgent: settings.get<boolean>('Livechat_enabled_when_agent_idle') });
if (!visitor) {
throw new Meteor.Error('error-livechat-visitor-registration', 'Error registering visitor', {
method: 'livechat/visitor',
Expand Down
127 changes: 1 addition & 126 deletions apps/meteor/app/livechat/server/lib/Visitors.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,6 @@
import { UserStatus } from '@rocket.chat/core-typings';
import type { ILivechatContactVisitorAssociation, IOmnichannelSource, ILivechatVisitor } from '@rocket.chat/core-typings';
import { Logger } from '@rocket.chat/logger';
import { LivechatContacts, LivechatDepartment, LivechatVisitors, Users } from '@rocket.chat/models';

import { validateEmail } from './Helper';
import { settings } from '../../../settings/server';

const logger = new Logger('Livechat - Visitor');

export type RegisterGuestType = Partial<Pick<ILivechatVisitor, 'token' | 'name' | 'department' | 'status' | 'username'>> & {
id?: string;
connectionData?: any;
email?: string;
phone?: { number: string };
};
import type { ILivechatContactVisitorAssociation, IOmnichannelSource } from '@rocket.chat/core-typings';

export const Visitors = {
isValidObject(obj: unknown): obj is Record<string, any> {
return typeof obj === 'object' && obj !== null;
},

makeVisitorAssociation(visitorId: string, roomInfo: IOmnichannelSource): ILivechatContactVisitorAssociation {
return {
visitorId,
Expand All @@ -29,110 +10,4 @@ export const Visitors = {
},
};
},

async registerGuest({
id,
token,
name,
phone,
email,
department,
username,
connectionData,
status = UserStatus.ONLINE,
}: RegisterGuestType): Promise<ILivechatVisitor | null> {
check(token, String);
check(id, Match.Maybe(String));

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

const visitorDataToUpdate: Partial<ILivechatVisitor> & { userAgent?: string; ip?: string; host?: string } = {
token,
status,
...(phone?.number && { phone: [{ phoneNumber: phone.number }] }),
...(name && { name }),
};

if (email) {
const visitorEmail = email.trim().toLowerCase();
validateEmail(visitorEmail);
visitorDataToUpdate.visitorEmails = [{ address: visitorEmail }];

const contact = await LivechatContacts.findContactByEmailAndContactManager(visitorEmail);
if (contact?.contactManager) {
const shouldConsiderIdleAgent = settings.get<boolean>('Livechat_enabled_when_agent_idle');
const agent = await Users.findOneOnlineAgentById(contact.contactManager, shouldConsiderIdleAgent, {
projection: { _id: 1, username: 1, name: 1, emails: 1 },
});
if (agent?.username && agent.name && agent.emails) {
visitorDataToUpdate.contactManager = {
_id: agent._id,
username: agent.username,
name: agent.name,
emails: agent.emails,
};
logger.debug(`Assigning visitor ${token} to agent ${agent.username}`);
}
}
}

const livechatVisitor = await LivechatVisitors.getVisitorByToken(token, { projection: { _id: 1 } });

if (department && livechatVisitor?.department !== department) {
logger.debug(`Attempt to find a department with id/name ${department}`);
const dep = await LivechatDepartment.findOneByIdOrName(department, { projection: { _id: 1 } });
if (!dep) {
logger.debug(`Invalid department provided: ${department}`);
throw new Meteor.Error('error-invalid-department', 'The provided department is invalid');
}
logger.debug(`Assigning visitor ${token} to department ${dep._id}`);
visitorDataToUpdate.department = dep._id;
}

visitorDataToUpdate.token = livechatVisitor?.token || token;

let existingUser = null;

if (livechatVisitor) {
logger.debug('Found matching user by token');
visitorDataToUpdate._id = livechatVisitor._id;
} else if (phone?.number && (existingUser = await LivechatVisitors.findOneVisitorByPhone(phone.number))) {
logger.debug('Found matching user by phone number');
visitorDataToUpdate._id = existingUser._id;
// Don't change token when matching by phone number, use current visitor token
visitorDataToUpdate.token = existingUser.token;
} else if (email && (existingUser = await LivechatVisitors.findOneGuestByEmailAddress(email))) {
logger.debug('Found matching user by email');
visitorDataToUpdate._id = existingUser._id;
} else if (!livechatVisitor) {
logger.debug(`No matches found. Attempting to create new user with token ${token}`);

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') && this.isValidObject(connectionData)) {
logger.debug(`Saving connection data for visitor ${token}`);
const { httpHeaders, clientAddress } = connectionData;
if (this.isValidObject(httpHeaders)) {
visitorDataToUpdate.userAgent = httpHeaders['user-agent'];
visitorDataToUpdate.ip = httpHeaders['x-real-ip'] || httpHeaders['x-forwarded-for'] || clientAddress;
visitorDataToUpdate.host = httpHeaders?.host;
}
}
}

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

if (!upsertedLivechatVisitor) {
logger.debug(`No visitor found after upsert`);
return null;
}

return upsertedLivechatVisitor;
},
};
46 changes: 1 addition & 45 deletions apps/meteor/app/livechat/server/lib/guests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,9 @@ import {
LivechatContacts,
Users,
} from '@rocket.chat/models';
import { wrapExceptions } from '@rocket.chat/tools';
import UAParser from 'ua-parser-js';

import { parseAgentCustomFields, validateEmail } from './Helper';
import type { RegisterGuestType } from './Visitors';
import { Visitors } from './Visitors';
import { ContactMerger, type FieldAndValue } from './contacts/ContactMerger';
import { parseAgentCustomFields } from './Helper';
import type { ICRMData } from './localTypes';
import { livechatLogger } from './logger';
import { trim } from '../../../../lib/utils/stringUtils';
Expand Down Expand Up @@ -102,46 +98,6 @@ export async function removeContactsByVisitorId({ _id }: { _id: string }) {
}
}

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(_id: string) {
// This shouldn't be possible, but just in case
if (!_id) {
Expand Down
47 changes: 46 additions & 1 deletion apps/meteor/app/livechat/server/startup.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import type { IUser } from '@rocket.chat/core-typings';
import { ILivechatAgentStatus, isOmnichannelRoom } from '@rocket.chat/core-typings';
import { Logger } from '@rocket.chat/logger';
import { LivechatRooms } from '@rocket.chat/models';
import { LivechatContacts, LivechatRooms } from '@rocket.chat/models';
import { registerGuest } from '@rocket.chat/omni-core';
import { validateEmail, wrapExceptions } from '@rocket.chat/tools';
import { Accounts } from 'meteor/accounts-base';
import { Meteor } from 'meteor/meteor';

Expand All @@ -18,9 +20,52 @@ import { hasPermissionAsync } from '../../authorization/server/functions/hasPerm
import { notifyOnUserChange } from '../../lib/server/lib/notifyListener';
import { settings } from '../../settings/server';
import './roomAccessValidator.internalService';
import { ContactMerger, type FieldAndValue } from './lib/contacts/ContactMerger';

const logger = new Logger('LivechatStartup');

// TODO this patch is temporary because `ContactMerger` still a lot of dependencies, so it is not suitable to be moved to omni-core package
// TODO add tests covering the ContactMerger usage
registerGuest.patch(async (originalFn, newData, options) => {
const visitor = await originalFn(newData, options);
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;
});

Meteor.startup(async () => {
roomCoordinator.setRoomFind('l', async (id) => maybeMigrateLivechatRoom(await LivechatRooms.findOneById(id)));

Expand Down
Loading
Loading