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(sci): Restrict livechat visitors to their source type scope #33569

Merged
merged 25 commits into from
Oct 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
85be068
feat: add channelName property to ILivechatVisitor
matheusbsilva137 Oct 14, 2024
10f253c
feat: check only for visitors within the app context in the livechat …
matheusbsilva137 Oct 14, 2024
a60c467
feat: restrict visitors by channelName in the SMS messages flow
matheusbsilva137 Oct 14, 2024
cf42b87
feat: restrict visitors by channelName in the Email inbox messages flow
matheusbsilva137 Oct 14, 2024
a5ff735
feat: restrict visitors by channelName in the API and Widget messages…
matheusbsilva137 Oct 14, 2024
14102e2
feat: restrict visitors by channelName in the UI kit messages flow
matheusbsilva137 Oct 14, 2024
1777d03
replace channelName by source
matheusbsilva137 Oct 14, 2024
2992755
Remove channelName
matheusbsilva137 Oct 14, 2024
9bdf273
do not use target phone (destination) to differ visitors
matheusbsilva137 Oct 14, 2024
b18c4ea
tests: add end-to-end tests
matheusbsilva137 Oct 14, 2024
1bfe4f7
fix typecheck
matheusbsilva137 Oct 14, 2024
7c0c6d1
fix lint and typecheck
matheusbsilva137 Oct 14, 2024
5e37406
fix nested queries
matheusbsilva137 Oct 15, 2024
6ad6544
Create changeset
matheusbsilva137 Oct 15, 2024
ffb140a
improve methods names
matheusbsilva137 Oct 15, 2024
42b29f5
apply changes requested to tests
matheusbsilva137 Oct 15, 2024
36ca708
Fix typecheck
matheusbsilva137 Oct 15, 2024
68dc1f9
Update message.ts
matheusbsilva137 Oct 15, 2024
bd27ff4
Apply more changes requested
matheusbsilva137 Oct 15, 2024
88602d6
Revert changes to findGuest function
matheusbsilva137 Oct 15, 2024
ebbcaca
apply changes requested
matheusbsilva137 Oct 15, 2024
72b5476
Undo typing improvements
matheusbsilva137 Oct 15, 2024
2616ef1
Merge branch 'develop' into feat/channel-name-visitors
matheusbsilva137 Oct 17, 2024
0dbd244
Merge branch 'develop' into feat/channel-name-visitors
Oct 17, 2024
158b633
Merge branch 'develop' into feat/channel-name-visitors
kodiakhq[bot] Oct 17, 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
---

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