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: block contact channel #33590

Merged
merged 32 commits into from
Oct 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
9670438
feat: add new block contact channel endpoint
tapiarafael Oct 15, 2024
2aba05e
chore: eslint fix
tapiarafael Oct 15, 2024
bc8bac4
fix: module name
tapiarafael Oct 15, 2024
d988260
fix: remove module from license package
tapiarafael Oct 15, 2024
e991bfe
feat: close room for blocked contacts
tapiarafael Oct 15, 2024
8b183a6
feat: throw an error if contact is blocked
tapiarafael Oct 15, 2024
21240e5
Merge branch 'develop' into feat/contact-blocking
tapiarafael Oct 15, 2024
621902f
refactor: move typings to ee
tapiarafael Oct 15, 2024
f7cebb7
Merge branch 'develop' into feat/contact-blocking
tapiarafael Oct 15, 2024
97f2e3f
fix: closes the room as the agent
tapiarafael Oct 15, 2024
46df4ce
chore: register the api endpoints
tapiarafael Oct 15, 2024
836536b
feat: add permissions
tapiarafael Oct 15, 2024
db389cd
test: ensure that blocking feature is working as intende
tapiarafael Oct 15, 2024
c78e37f
Merge branch 'develop' into feat/contact-blocking
tapiarafael Oct 15, 2024
a68651a
Merge branch 'develop' into feat/contact-blocking
tapiarafael Oct 16, 2024
27d23ca
fix: forbid creation of rooms if contact is blocked
tapiarafael Oct 16, 2024
731c117
test: increase coverage
tapiarafael Oct 16, 2024
638e211
fix: remove Meteor usage
tapiarafael Oct 16, 2024
219440e
refactor: make update easier to read
tapiarafael Oct 16, 2024
b5575ee
Merge branch 'develop' into feat/contact-blocking
tapiarafael Oct 16, 2024
0b4d7a9
Merge branch 'develop' into feat/contact-blocking
tapiarafael Oct 16, 2024
c6c886b
fix: check if license has the right module
tapiarafael Oct 16, 2024
ca31e98
fix: remove empty line
tapiarafael Oct 16, 2024
6e3d9e6
fix: remove _id from projection
tapiarafael Oct 16, 2024
634605d
Merge branch 'develop' into feat/contact-blocking
tapiarafael Oct 16, 2024
dd501e2
refactor: return a promise to avoid waiting twice
tapiarafael Oct 16, 2024
088b34b
Merge branch 'develop' into feat/contact-blocking
matheusbsilva137 Oct 17, 2024
0e262a8
Merge branch 'feat/single-contact-id' into feat/contact-blocking
pierre-lehnen-rc Oct 17, 2024
a0afcb5
updated code to new data format
pierre-lehnen-rc Oct 17, 2024
2c6fb62
Merge remote-tracking branch 'origin/feat/single-contact-id' into fea…
pierre-lehnen-rc Oct 17, 2024
d39957c
lints
pierre-lehnen-rc Oct 18, 2024
666dd02
Merge branch 'feat/single-contact-id' into feat/contact-blocking
tapiarafael Oct 18, 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
10 changes: 4 additions & 6 deletions apps/meteor/app/livechat/server/lib/Helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,12 +96,10 @@ export const createLivechatRoom = async <
visitor: { _id, username, departmentId, status, activity },
});

const contactId = await (async () => {
return migrateVisitorIfMissingContact(
_id,
(extraRoomInfo.source || roomInfo.source || { type: OmnichannelSourceType.OTHER }) as IOmnichannelSource,
);
})();
const contactId = await migrateVisitorIfMissingContact(
_id,
(extraRoomInfo.source || roomInfo.source || { type: OmnichannelSourceType.OTHER }) as IOmnichannelSource,
);

// TODO: Solve `u` missing issue
const room: InsertionModel<IOmnichannelRoom> = {
Expand Down
9 changes: 9 additions & 0 deletions apps/meteor/app/livechat/server/lib/LivechatTyped.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import {
ReadReceipts,
Rooms,
LivechatCustomField,
LivechatContacts,
} from '@rocket.chat/models';
import { serverFetch as fetch } from '@rocket.chat/server-fetch';
import { Match, check } from 'meteor/check';
Expand Down Expand Up @@ -446,6 +447,10 @@ class LivechatClass {
}
}

if (await LivechatContacts.isChannelBlocked(visitor._id)) {
throw new Error('error-contact-channel-blocked');
}

// delegate room creation to QueueManager
Livechat.logger.debug(`Calling QueueManager to request a room for visitor ${visitor._id}`);

Expand Down Expand Up @@ -487,6 +492,10 @@ class LivechatClass {
Livechat.logger.debug(`Attempting to find or create a room for visitor ${guest._id}`);
const room = await LivechatRooms.findOneById(message.rid);

if (room?.v._id && (await LivechatContacts.isChannelBlocked(room?.v._id))) {
throw new Error('error-contact-channel-blocked');
}

if (room && !room.open) {
Livechat.logger.debug(`Last room for visitor ${guest._id} closed. Creating new one`);
}
Expand Down
87 changes: 87 additions & 0 deletions apps/meteor/ee/app/livechat-enterprise/server/api/contacts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import Ajv from 'ajv';

import { API } from '../../../../../app/api/server';
import { changeContactBlockStatus, closeBlockedRoom, ensureSingleContactLicense } from './lib/contacts';

const ajv = new Ajv({
coerceTypes: true,
});

type blockContactProps = {
visitorId: string;
};

const blockContactSchema = {
type: 'object',
properties: {
contactId: {
type: 'string',
},
visitorId: {
type: 'string',
},
},
required: ['contactId', 'visitorId'],
additionalProperties: false,
};

const isBlockContactProps = ajv.compile<blockContactProps>(blockContactSchema);

declare module '@rocket.chat/rest-typings' {
// eslint-disable-next-line @typescript-eslint/naming-convention
interface Endpoints {
'/v1/omnichannel/contacts.block': {
POST: (params: blockContactProps) => void;
};
'/v1/omnichannel/contacts.unblock': {
POST: (params: blockContactProps) => void;
};
}
}

API.v1.addRoute(
'omnichannel/contacts.block',
tapiarafael marked this conversation as resolved.
Show resolved Hide resolved
{
authRequired: true,
permissionsRequired: ['block-livechat-contact'],
validateParams: isBlockContactProps,
},
{
async post() {
ensureSingleContactLicense();
const { visitorId } = this.bodyParams;
const { user } = this;

await changeContactBlockStatus({
visitorId,
block: true,
});

await closeBlockedRoom(visitorId, user);

return API.v1.success();
},
},
);

API.v1.addRoute(
'omnichannel/contacts.unblock',
{
authRequired: true,
permissionsRequired: ['unblock-livechat-contact'],
validateParams: isBlockContactProps,
},
{
async post() {
ensureSingleContactLicense();
const { visitorId } = this.bodyParams;

await changeContactBlockStatus({
visitorId,
block: false,
});

return API.v1.success();
},
},
);
1 change: 1 addition & 0 deletions apps/meteor/ee/app/livechat-enterprise/server/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ import './rooms';
import './transcript';
import './reports';
import './triggers';
import './contacts';
36 changes: 36 additions & 0 deletions apps/meteor/ee/app/livechat-enterprise/server/api/lib/contacts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import type { IUser } from '@rocket.chat/core-typings';
import { License } from '@rocket.chat/license';
import { LivechatContacts, LivechatRooms, LivechatVisitors } from '@rocket.chat/models';

import { Livechat } from '../../../../../../app/livechat/server/lib/LivechatTyped';
import { i18n } from '../../../../../../server/lib/i18n';

export async function changeContactBlockStatus({ block, visitorId }: { visitorId: string; block: boolean }) {
const result = await LivechatContacts.updateContactChannel(visitorId, { blocked: block });

if (!result.modifiedCount) {
throw new Error('error-contact-not-found');
}
}

export function ensureSingleContactLicense() {
if (!License.hasModule('contact-id-verification')) {
throw new Error('error-action-not-allowed');
}
}

export async function closeBlockedRoom(visitorId: string, user: IUser) {
const visitor = await LivechatVisitors.findOneById(visitorId);

if (!visitor) {
throw new Error('error-visitor-not-found');
}

const room = await LivechatRooms.findOneOpenByVisitorToken(visitor.token);

if (!room) {
return;
}

return Livechat.closeRoom({ room, visitor, comment: i18n.t('close-blocked-room-comment'), user });
}
2 changes: 2 additions & 0 deletions apps/meteor/ee/app/livechat-enterprise/server/permissions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ export const omnichannelEEPermissions = [
{ _id: 'outbound-voip-calls', roles: [adminRole, livechatManagerRole] },
{ _id: 'request-pdf-transcript', roles: [adminRole, livechatManagerRole, livechatMonitorRole, livechatAgentRole] },
{ _id: 'view-livechat-reports', roles: [adminRole, livechatManagerRole, livechatMonitorRole] },
{ _id: 'block-livechat-contact', roles: [adminRole, livechatManagerRole, livechatMonitorRole, livechatAgentRole] },
{ _id: 'unblock-livechat-contact', roles: [adminRole, livechatManagerRole, livechatMonitorRole, livechatAgentRole] },
];

export const createPermissions = async (): Promise<void> => {
Expand Down
19 changes: 12 additions & 7 deletions apps/meteor/ee/server/patches/verifyContactChannel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,18 @@ export const runVerifyContactChannel = async (
): Promise<ILivechatContact | null> => {
const { contactId, field, value, visitorId, roomId } = params;

await LivechatContacts.updateContactChannel(contactId, visitorId, {
'unknown': false,
'channels.$.verified': true,
'channels.$.verifiedAt': new Date(),
'channels.$.field': field,
'channels.$.value': value,
});
await LivechatContacts.updateContactChannel(
visitorId,
{
verified: true,
verifiedAt: new Date(),
field,
value,
},
{
unknown: false,
},
);

await LivechatRooms.update({ _id: roomId }, { $set: { verified: true } });

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,14 @@ describe('verifyContactChannel', () => {

expect(
modelsMock.LivechatContacts.updateContactChannel.calledOnceWith(
'contactId',
'visitorId',
sinon.match({
'unknown': false,
'channels.$.verified': true,
'channels.$.field': 'field',
'channels.$.value': 'value',
verified: true,
field: 'field',
value: 'value',
}),
sinon.match({
unknown: false,
}),
),
).to.be.true;
Expand Down
41 changes: 32 additions & 9 deletions apps/meteor/server/models/raw/LivechatContacts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,15 +79,6 @@ export class LivechatContactsRaw extends BaseRaw<ILivechatContact> implements IL
return updatedValue.value as ILivechatContact;
}

async updateContactChannel(contactId: string, visitorId: string, data: UpdateFilter<ILivechatContact>['$set']): Promise<UpdateResult> {
return this.updateOne(
{ '_id': contactId, 'channels.visitorId': visitorId },
{
$set: data,
},
);
}

findPaginatedContacts(searchText?: string, options?: FindOptions): FindPaginated<FindCursor<ILivechatContact>> {
const searchRegex = escapeRegExp(searchText || '');
const match: Filter<ILivechatContact & RootFilterOperators<ILivechatContact>> = {
Expand Down Expand Up @@ -157,6 +148,38 @@ export class LivechatContactsRaw extends BaseRaw<ILivechatContact> implements IL
return this.updateOne({ '_id': contactId, 'channels.visitorId': visitorId }, { $set: { lastChat, 'channels.$.lastChat': lastChat } });
}

async isChannelBlocked(visitorId: ILivechatVisitor['_id']): Promise<boolean> {
return Boolean(
await this.findOne(
{
'channels.visitorId': visitorId,
'channels.blocked': true,
},
{ projection: { _id: 1 } },
),
);
}

async updateContactChannel(
visitorId: ILivechatVisitor['_id'],
data: Partial<ILivechatContactChannel>,
contactData?: Partial<Omit<ILivechatContact, 'channels'>>,
): Promise<UpdateResult> {
return this.updateOne(
{
'channels.visitorId': visitorId,
},
{
$set: {
...contactData,
...(Object.fromEntries(
Object.keys(data).map((key) => [`channels.$.${key}`, data[key as keyof ILivechatContactChannel]]),
) as UpdateFilter<ILivechatContact>['$set']),
},
},
);
}

async findSimilarVerifiedContacts(
{ field, value }: Pick<ILivechatContactChannel, 'field' | 'value'>,
originalContactId: string,
Expand Down
Loading
Loading