Skip to content

Commit

Permalink
feat(sci): Restrict livechat visitors to their source type scope (#33569
Browse files Browse the repository at this point in the history
)
  • Loading branch information
matheusbsilva137 authored Oct 18, 2024
1 parent 8670b57 commit 7726d68
Show file tree
Hide file tree
Showing 19 changed files with 440 additions and 54 deletions.
9 changes: 9 additions & 0 deletions .changeset/fuzzy-pans-share.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@rocket.chat/meteor": minor
"@rocket.chat/apps": minor
"@rocket.chat/core-typings": minor
"@rocket.chat/model-typings": minor
---

Adds a `source` field to livechat visitors, which stores the channel (eg API, widget, SMS, email-inbox, app) that's been used by the visitor to send messages.
Uses the new `source` field to assure each visitor is linked to a single source, so that each connection through a distinct channel creates a new visitor.
37 changes: 32 additions & 5 deletions apps/meteor/app/apps/server/bridges/livechat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,21 @@ export class AppLivechatBridge extends LivechatBridge {
const appMessage = (await this.orch.getConverters().get('messages').convertAppMessage(message)) as IMessage | undefined;
const livechatMessage = appMessage as ILivechatMessage | undefined;

if (guest) {
const visitorSource = {
type: OmnichannelSourceType.APP,
id: appId,
alias: this.orch.getManager()?.getOneById(appId)?.getNameSlug(),
};
const fullVisitor = await LivechatVisitors.findOneEnabledByIdAndSource({
_id: guest._id,
sourceFilter: { 'source.type': visitorSource.type, 'source.id': visitorSource.id, 'source.alias': visitorSource.alias },
});
if (!fullVisitor?.source) {
await LivechatVisitors.setSourceById(guest._id, visitorSource);
}
}

const msg = await LivechatTyped.sendMessage({
guest: guest as ILivechatVisitor,
message: livechatMessage as ILivechatMessage,
Expand Down Expand Up @@ -286,7 +301,7 @@ export class AppLivechatBridge extends LivechatBridge {
}

return Promise.all(
(await LivechatVisitors.findEnabled(query).toArray()).map(
(await LivechatVisitors.findEnabledBySource({ 'source.type': OmnichannelSourceType.APP, 'source.id': appId }, query).toArray()).map(
async (visitor) => visitor && this.orch.getConverters()?.get('visitors').convertVisitor(visitor),
),
);
Expand All @@ -295,7 +310,7 @@ export class AppLivechatBridge extends LivechatBridge {
protected async findVisitorById(id: string, appId: string): Promise<IVisitor | undefined> {
this.orch.debugLog(`The App ${appId} is looking for livechat visitors.`);

return this.orch.getConverters()?.get('visitors').convertById(id);
return this.orch.getConverters()?.get('visitors').convertByIdAndSource(id, appId);
}

protected async findVisitorByEmail(email: string, appId: string): Promise<IVisitor | undefined> {
Expand All @@ -304,7 +319,9 @@ export class AppLivechatBridge extends LivechatBridge {
return this.orch
.getConverters()
?.get('visitors')
.convertVisitor(await LivechatVisitors.findOneGuestByEmailAddress(email));
.convertVisitor(
await LivechatVisitors.findOneGuestByEmailAddressAndSource(email, { 'source.type': OmnichannelSourceType.APP, 'source.id': appId }),
);
}

protected async findVisitorByToken(token: string, appId: string): Promise<IVisitor | undefined> {
Expand All @@ -313,7 +330,12 @@ export class AppLivechatBridge extends LivechatBridge {
return this.orch
.getConverters()
?.get('visitors')
.convertVisitor(await LivechatVisitors.getVisitorByToken(token, {}));
.convertVisitor(
await LivechatVisitors.getVisitorByTokenAndSource({
token,
sourceFilter: { 'source.type': OmnichannelSourceType.APP, 'source.id': appId },
}),
);
}

protected async findVisitorByPhoneNumber(phoneNumber: string, appId: string): Promise<IVisitor | undefined> {
Expand All @@ -322,7 +344,12 @@ export class AppLivechatBridge extends LivechatBridge {
return this.orch
.getConverters()
?.get('visitors')
.convertVisitor(await LivechatVisitors.findOneVisitorByPhone(phoneNumber));
.convertVisitor(
await LivechatVisitors.findOneVisitorByPhoneAndSource(phoneNumber, {
'source.type': OmnichannelSourceType.APP,
'source.id': appId,
}),
);
}

protected async findDepartmentByIdOrName(value: string, appId: string): Promise<IDepartment | undefined> {
Expand Down
21 changes: 21 additions & 0 deletions apps/meteor/app/apps/server/converters/visitors.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { OmnichannelSourceType } from '@rocket.chat/core-typings';
import { LivechatVisitors } from '@rocket.chat/models';

import { transformMappedData } from './transformMappedData';
Expand All @@ -14,12 +15,30 @@ export class AppVisitorsConverter {
return this.convertVisitor(visitor);
}

async convertByIdAndSource(id, appId) {
const visitor = await LivechatVisitors.findOneEnabledByIdAndSource({
_id: id,
sourceFilter: { 'source.type': OmnichannelSourceType.APP, 'source.id': appId },
});

return this.convertVisitor(visitor);
}

async convertByToken(token) {
const visitor = await LivechatVisitors.getVisitorByToken(token);

return this.convertVisitor(visitor);
}

async convertByTokenAndSource(token, appId) {
const visitor = await LivechatVisitors.getVisitorByTokenAndSource({
token,
sourceFilter: { 'source.type': OmnichannelSourceType.APP, 'source.id': appId },
});

return this.convertVisitor(visitor);
}

async convertVisitor(visitor) {
if (!visitor) {
return undefined;
Expand All @@ -37,6 +56,7 @@ export class AppVisitorsConverter {
livechatData: 'livechatData',
status: 'status',
contactId: 'contactId',
source: 'source',
};

return transformMappedData(visitor, map);
Expand All @@ -56,6 +76,7 @@ export class AppVisitorsConverter {
livechatData: visitor.livechatData,
status: visitor.status || 'online',
contactId: visitor.contactId,
source: visitor.source,
...(visitor.visitorEmails && { visitorEmails: visitor.visitorEmails }),
...(visitor.department && { department: visitor.department }),
};
Expand Down
23 changes: 19 additions & 4 deletions apps/meteor/app/livechat/imports/server/rest/sms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type {
MessageAttachment,
ServiceData,
FileAttachmentProps,
IOmnichannelSource,
} from '@rocket.chat/core-typings';
import { OmnichannelSourceType } from '@rocket.chat/core-typings';
import { Logger } from '@rocket.chat/logger';
Expand Down Expand Up @@ -55,10 +56,24 @@ const defineDepartment = async (idOrName?: string) => {
return department?._id;
};

const defineVisitor = async (smsNumber: string, targetDepartment?: string) => {
const visitor = await LivechatVisitors.findOneVisitorByPhone(smsNumber);
let data: { token: string; department?: string } = {
const defineVisitor = async (smsNumber: string, serviceName: string, destination: string, targetDepartment?: string) => {
const visitorSource: IOmnichannelSource = {
type: OmnichannelSourceType.SMS,
alias: serviceName,
};

const visitor = await LivechatVisitors.findOneVisitorByPhoneAndSource(
smsNumber,
{
'source.type': visitorSource.type,
'source.alias': visitorSource.alias,
},
{ projection: { token: 1 } },
);
visitorSource.destination = destination;
let data: { token: string; source: IOmnichannelSource; department?: string } = {
token: visitor?.token || Random.id(),
source: visitorSource,
};

if (!visitor) {
Expand Down Expand Up @@ -117,7 +132,7 @@ API.v1.addRoute('livechat/sms-incoming/:service', {
targetDepartment = await defineDepartment(smsDepartment);
}

const visitor = await defineVisitor(sms.from, targetDepartment);
const visitor = await defineVisitor(sms.from, service, sms.to, targetDepartment);
if (!visitor) {
return API.v1.success(SMSService.error(new Error('Invalid visitor')));
}
Expand Down
12 changes: 11 additions & 1 deletion apps/meteor/app/livechat/imports/server/rest/upload.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { OmnichannelSourceType } from '@rocket.chat/core-typings';
import { LivechatVisitors, LivechatRooms } from '@rocket.chat/models';
import filesize from 'filesize';

import { API } from '../../../../api/server';
import { isWidget } from '../../../../api/server/helpers/isWidget';
import { getUploadFormData } from '../../../../api/server/lib/getUploadFormData';
import { FileUpload } from '../../../../file-upload/server';
import { settings } from '../../../../settings/server';
Expand All @@ -13,6 +15,7 @@ API.v1.addRoute('livechat/upload/:rid', {
if (!this.request.headers['x-visitor-token']) {
return API.v1.unauthorized();
}
const sourceType = isWidget(this.request.headers) ? OmnichannelSourceType.WIDGET : OmnichannelSourceType.API;

const canUpload = settings.get<boolean>('Livechat_fileupload_enabled') && settings.get<boolean>('FileUpload_Enabled');

Expand All @@ -23,7 +26,10 @@ API.v1.addRoute('livechat/upload/:rid', {
}

const visitorToken = this.request.headers['x-visitor-token'];
const visitor = await LivechatVisitors.getVisitorByToken(visitorToken as string, {});
const visitor = await LivechatVisitors.getVisitorByTokenAndSource({
token: visitorToken as string,
sourceFilter: { 'source.type': sourceType },
});

if (!visitor) {
return API.v1.unauthorized();
Expand Down Expand Up @@ -76,6 +82,10 @@ API.v1.addRoute('livechat/upload/:rid', {
return API.v1.failure('Invalid file');
}

if (!visitor.source) {
await LivechatVisitors.setSourceById(visitor._id, { type: sourceType });
}

uploadedFile.description = fields.description;

delete fields.description;
Expand Down
24 changes: 23 additions & 1 deletion apps/meteor/app/livechat/server/api/lib/livechat.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import type { ILivechatAgent, ILivechatDepartment, ILivechatTrigger, ILivechatVisitor, IOmnichannelRoom } from '@rocket.chat/core-typings';
import type {
ILivechatAgent,
ILivechatDepartment,
ILivechatTrigger,
ILivechatVisitor,
IOmnichannelRoom,
OmnichannelSourceType,
} from '@rocket.chat/core-typings';
import { License } from '@rocket.chat/license';
import { EmojiCustom, LivechatTrigger, LivechatVisitors, LivechatRooms, LivechatDepartment } from '@rocket.chat/models';
import { Meteor } from 'meteor/meteor';
Expand Down Expand Up @@ -62,6 +69,21 @@ export function findGuest(token: string): Promise<ILivechatVisitor | null> {
});
}

export function findGuestBySource(token: string, sourceType: OmnichannelSourceType): Promise<ILivechatVisitor | null> {
const projection = {
name: 1,
username: 1,
token: 1,
visitorEmails: 1,
department: 1,
activity: 1,
contactId: 1,
source: 1,
};

return LivechatVisitors.getVisitorByTokenAndSource({ token, sourceFilter: { 'source.type': sourceType } }, { projection });
}

export function findGuestWithoutActivity(token: string): Promise<ILivechatVisitor | null> {
return LivechatVisitors.getVisitorByToken(token, { projection: { name: 1, username: 1, token: 1, visitorEmails: 1, department: 1 } });
}
Expand Down
27 changes: 21 additions & 6 deletions apps/meteor/app/livechat/server/api/v1/message.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { IOmnichannelSource } from '@rocket.chat/core-typings';
import { OmnichannelSourceType } from '@rocket.chat/core-typings';
import { LivechatVisitors, LivechatRooms, Messages } from '@rocket.chat/models';
import { Random } from '@rocket.chat/random';
Expand All @@ -18,16 +19,17 @@ import { loadMessageHistory } from '../../../../lib/server/functions/loadMessage
import { settings } from '../../../../settings/server';
import { normalizeMessageFileUpload } from '../../../../utils/server/functions/normalizeMessageFileUpload';
import { Livechat as LivechatTyped } from '../../lib/LivechatTyped';
import { findGuest, findRoom, normalizeHttpHeaderData } from '../lib/livechat';
import { findGuest, findGuestBySource, findRoom, normalizeHttpHeaderData } from '../lib/livechat';

API.v1.addRoute(
'livechat/message',
{ validateParams: isPOSTLivechatMessageParams },
{
async post() {
const { token, rid, agent, msg } = this.bodyParams;
const sourceType = isWidget(this.request.headers) ? OmnichannelSourceType.WIDGET : OmnichannelSourceType.API;

const guest = await findGuest(token);
const guest = await findGuestBySource(token, sourceType);
if (!guest) {
throw new Error('invalid-token');
}
Expand All @@ -48,6 +50,10 @@ API.v1.addRoute(
throw new Error('message-length-exceeds-character-limit');
}

if (!guest.source) {
await LivechatVisitors.setSourceById(guest._id, { type: sourceType });
}

const _id = this.bodyParams._id || Random.id();

const sendMessage = {
Expand All @@ -61,7 +67,7 @@ API.v1.addRoute(
agent,
roomInfo: {
source: {
type: isWidget(this.request.headers) ? OmnichannelSourceType.WIDGET : OmnichannelSourceType.API,
type: sourceType,
},
},
};
Expand Down Expand Up @@ -250,8 +256,12 @@ API.v1.addRoute(
{
async post() {
const visitorToken = this.bodyParams.visitor.token;
const sourceType = isWidget(this.request.headers) ? OmnichannelSourceType.WIDGET : OmnichannelSourceType.API;

const visitor = await LivechatVisitors.getVisitorByToken(visitorToken, {});
const visitor = await LivechatVisitors.getVisitorByTokenAndSource(
{ token: visitorToken, sourceFilter: { 'source.type': sourceType } },
{},
);
let rid: string;
if (visitor) {
const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {});
Expand All @@ -261,11 +271,16 @@ API.v1.addRoute(
} else {
rid = Random.id();
}

if (!visitor.source) {
await LivechatVisitors.setSourceById(visitor._id, { type: sourceType });
}
} else {
rid = Random.id();

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

const visitor = await LivechatTyped.registerGuest(guest);
if (!visitor) {
Expand All @@ -290,7 +305,7 @@ API.v1.addRoute(
},
roomInfo: {
source: {
type: isWidget(this.request.headers) ? OmnichannelSourceType.WIDGET : OmnichannelSourceType.API,
type: sourceType,
},
},
};
Expand Down
5 changes: 4 additions & 1 deletion apps/meteor/app/livechat/server/lib/LivechatTyped.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ import { isDepartmentCreationAvailable } from './isDepartmentCreationAvailable';
import type { CloseRoomParams, CloseRoomParamsByUser, CloseRoomParamsByVisitor } from './localTypes';
import { parseTranscriptRequest } from './parseTranscriptRequest';

type RegisterGuestType = Partial<Pick<ILivechatVisitor, 'token' | 'name' | 'department' | 'status' | 'username'>> & {
type RegisterGuestType = Partial<Pick<ILivechatVisitor, 'token' | 'name' | 'department' | 'status' | 'username' | 'source'>> & {
id?: string;
connectionData?: any;
email?: string;
Expand Down Expand Up @@ -654,6 +654,7 @@ class LivechatClass {
username,
connectionData,
status = UserStatus.ONLINE,
source,
}: RegisterGuestType): Promise<ILivechatVisitor | null> {
check(token, String);
check(id, Match.Maybe(String));
Expand All @@ -663,6 +664,7 @@ class LivechatClass {
const visitorDataToUpdate: Partial<ILivechatVisitor> & { userAgent?: string; ip?: string; host?: string } = {
token,
status,
source,
...(phone?.number ? { phone: [{ phoneNumber: phone.number }] } : {}),
...(name ? { name } : {}),
};
Expand Down Expand Up @@ -708,6 +710,7 @@ class LivechatClass {
visitorDataToUpdate.username = username || (await LivechatVisitors.getNextVisitorUsername());
visitorDataToUpdate.status = status;
visitorDataToUpdate.ts = new Date();
visitorDataToUpdate.source = source;

if (settings.get('Livechat_Allow_collect_and_store_HTTP_header_informations') && Livechat.isValidObject(connectionData)) {
Livechat.logger.debug(`Saving connection data for visitor ${token}`);
Expand Down
Loading

0 comments on commit 7726d68

Please sign in to comment.