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

feat!: Restrict livechat visitors to their source type scope #33760

Closed
wants to merge 23 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
9774957
feat: add channelName property to ILivechatVisitor
matheusbsilva137 Oct 14, 2024
6060945
feat: check only for visitors within the app context in the livechat …
matheusbsilva137 Oct 14, 2024
4bd9dbf
feat: restrict visitors by channelName in the SMS messages flow
matheusbsilva137 Oct 14, 2024
39e9576
feat: restrict visitors by channelName in the Email inbox messages flow
matheusbsilva137 Oct 14, 2024
7c621d3
feat: restrict visitors by channelName in the API and Widget messages…
matheusbsilva137 Oct 14, 2024
be799bd
feat: restrict visitors by channelName in the UI kit messages flow
matheusbsilva137 Oct 14, 2024
92a6cfc
replace channelName by source
matheusbsilva137 Oct 14, 2024
d7f5f85
Remove channelName
matheusbsilva137 Oct 14, 2024
386adbe
do not use target phone (destination) to differ visitors
matheusbsilva137 Oct 14, 2024
afc473f
tests: add end-to-end tests
matheusbsilva137 Oct 14, 2024
0c29c2a
fix typecheck
matheusbsilva137 Oct 14, 2024
9d03eaa
fix lint and typecheck
matheusbsilva137 Oct 14, 2024
2c6e1dc
fix nested queries
matheusbsilva137 Oct 15, 2024
0161ec9
Create changeset
matheusbsilva137 Oct 15, 2024
0961db1
improve methods names
matheusbsilva137 Oct 15, 2024
65b2147
apply changes requested to tests
matheusbsilva137 Oct 15, 2024
a3dd975
Fix typecheck
matheusbsilva137 Oct 15, 2024
26b7464
Update message.ts
matheusbsilva137 Oct 15, 2024
c98750d
Apply more changes requested
matheusbsilva137 Oct 15, 2024
45604bf
Revert changes to findGuest function
matheusbsilva137 Oct 15, 2024
e069568
apply changes requested
matheusbsilva137 Oct 15, 2024
a5696b2
Undo typing improvements
matheusbsilva137 Oct 15, 2024
bff9207
do not restrict visitors on uikit block
pierre-lehnen-rc Oct 24, 2024
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
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
Comment on lines +2 to +5
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"@rocket.chat/meteor": minor
"@rocket.chat/apps": minor
"@rocket.chat/core-typings": minor
"@rocket.chat/model-typings": minor
"@rocket.chat/meteor": major
"@rocket.chat/apps": major
"@rocket.chat/core-typings": minor
"@rocket.chat/model-typings": minor

Some changes should be tagged as major now, shouldn't they? Not sure about core and model typings

---

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
Loading