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
7 changes: 7 additions & 0 deletions .changeset/tough-points-know.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@rocket.chat/meteor": patch
"@rocket.chat/model-typings": patch
"@rocket.chat/models": patch
---

Fixes GDPR contact information removal for Omnichannel.
2 changes: 1 addition & 1 deletion apps/meteor/app/apps/server/converters/contacts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { transformMappedData } from './transformMappedData';

export class AppContactsConverter implements IAppContactsConverter {
async convertById(contactId: ILivechatContact['_id']): Promise<IAppsLivechatContact | undefined> {
const contact = await LivechatContacts.findOneById(contactId);
const contact = await LivechatContacts.findOneEnabledById(contactId);
if (!contact) {
return;
}
Expand Down
2 changes: 1 addition & 1 deletion apps/meteor/app/apps/server/converters/rooms.js
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ export class AppRoomsConverter {
if (!contact?._id) {
return;
}
const contactFromDb = await LivechatContacts.findOneById(contact._id, { projection: { _id: 1 } });
const contactFromDb = await LivechatContacts.findOneEnabledById(contact._id, { projection: { _id: 1 } });
return contactFromDb?._id;
}

Expand Down
2 changes: 1 addition & 1 deletion apps/meteor/app/livechat/server/api/v1/contact.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ API.v1.addRoute(
return API.v1.notFound();
}

const contact = await LivechatContacts.findOneById(contactId);
const contact = await LivechatContacts.findOneEnabledById(contactId);

if (!contact) {
return API.v1.notFound();
Expand Down
24 changes: 13 additions & 11 deletions apps/meteor/app/livechat/server/api/v1/visitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import { callbacks } from '../../../../../lib/callbacks';
import { API } from '../../../../api/server';
import { settings } from '../../../../settings/server';
import { setMultipleVisitorCustomFields } from '../../lib/custom-fields';
import { registerGuest, removeGuest, notifyGuestStatusChanged } from '../../lib/guests';
import { registerGuest, notifyGuestStatusChanged, removeContactsByVisitorId } from '../../lib/guests';
import { livechatLogger } from '../../lib/logger';
import { saveRoomInfo } from '../../lib/rooms';
import { updateCallStatus } from '../../lib/utils';
import { findGuest, normalizeHttpHeaderData } from '../lib/livechat';
Expand Down Expand Up @@ -140,18 +141,19 @@ API.v1.addRoute('livechat/visitor/:token', {
throw new Meteor.Error('visitor-has-open-rooms', 'Cannot remove visitors with opened rooms');
}

const { _id, token } = visitor;
const result = await removeGuest({ _id, token });
if (!result.modifiedCount) {
const { _id } = visitor;
try {
await removeContactsByVisitorId({ _id });
return API.v1.success({
visitor: {
_id,
ts: new Date().toISOString(),
},
});
} catch (e) {
livechatLogger.error(e);
throw new Meteor.Error('error-removing-visitor', 'An error ocurred while deleting visitor');
}

return API.v1.success({
visitor: {
_id,
ts: new Date().toISOString(),
},
});
},
});

Expand Down
2 changes: 1 addition & 1 deletion apps/meteor/app/livechat/server/lib/Helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ export const prepareLivechatRoom = async (
const contactId = await migrateVisitorIfMissingContact(_id, source);
const contact =
contactId &&
(await LivechatContacts.findOneById<Pick<ILivechatContact, '_id' | 'name' | 'channels' | 'activity'>>(contactId, {
(await LivechatContacts.findOneEnabledById<Pick<ILivechatContact, '_id' | 'name' | 'channels' | 'activity'>>(contactId, {
projection: { name: 1, channels: 1, activity: 1 },
}));
if (!contact) {
Expand Down
2 changes: 1 addition & 1 deletion apps/meteor/app/livechat/server/lib/QueueManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ export class QueueManager {
return false;
}

const contact = await LivechatContacts.findOneById(room.contactId, { projection: { channels: 1 } });
const contact = await LivechatContacts.findOneEnabledById(room.contactId, { projection: { channels: 1 } });
if (!contact) {
return false;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { ILivechatContact, ILivechatContactChannel } from '@rocket.chat/cor
import { LivechatContacts } from '@rocket.chat/models';

export async function getContactChannelsGrouped(contactId: string): Promise<ILivechatContactChannel[]> {
const contact = await LivechatContacts.findOneById<Pick<ILivechatContact, 'channels'>>(contactId, { projection: { channels: 1 } });
const contact = await LivechatContacts.findOneEnabledById<Pick<ILivechatContact, 'channels'>>(contactId, { projection: { channels: 1 } });

if (!contact?.channels) {
return [];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export const getContactHistory = makeFunction(
async (params: GetContactHistoryParams): Promise<PaginatedResult<{ history: VisitorSearchChatsResult[] }>> => {
const { contactId, count, offset, sort } = params;

const contact = await LivechatContacts.findOneById<Pick<ILivechatContact, '_id'>>(contactId, { projection: { _id: 1 } });
const contact = await LivechatContacts.findOneEnabledById<Pick<ILivechatContact, '_id'>>(contactId, { projection: { _id: 1 } });

if (!contact) {
throw new Error('error-contact-not-found');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import sinon from 'sinon';

const modelsMock = {
LivechatContacts: {
findOneById: sinon.stub(),
findOneEnabledById: sinon.stub(),
updateContact: sinon.stub(),
},
Settings: {
Expand All @@ -24,13 +24,13 @@ const { resolveContactConflicts } = proxyquire.noCallThru().load('./resolveConta

describe('resolveContactConflicts', () => {
beforeEach(() => {
modelsMock.LivechatContacts.findOneById.reset();
modelsMock.LivechatContacts.findOneEnabledById.reset();
modelsMock.Settings.incrementValueById.reset();
modelsMock.LivechatContacts.updateContact.reset();
});

it('should update the contact with the resolved custom field', async () => {
modelsMock.LivechatContacts.findOneById.resolves({
modelsMock.LivechatContacts.findOneEnabledById.resolves({
_id: 'contactId',
customFields: { customField: 'newValue' },
conflictingFields: [{ field: 'customFields.customField', value: 'oldValue' }],
Expand All @@ -44,7 +44,7 @@ describe('resolveContactConflicts', () => {

const result = await resolveContactConflicts({ contactId: 'contactId', customField: { customField: 'newValue' } });

expect(modelsMock.LivechatContacts.findOneById.getCall(0).args[0]).to.be.equal('contactId');
expect(modelsMock.LivechatContacts.findOneEnabledById.getCall(0).args[0]).to.be.equal('contactId');

expect(modelsMock.Settings.incrementValueById.getCall(0).args[0]).to.be.equal('Livechat_conflicting_fields_counter');
expect(modelsMock.Settings.incrementValueById.getCall(0).args[1]).to.be.equal(1);
Expand All @@ -59,7 +59,7 @@ describe('resolveContactConflicts', () => {
});

it('should update the contact with the resolved name', async () => {
modelsMock.LivechatContacts.findOneById.resolves({
modelsMock.LivechatContacts.findOneEnabledById.resolves({
_id: 'contactId',
name: 'Old Name',
customFields: { customField: 'newValue' },
Expand All @@ -75,7 +75,7 @@ describe('resolveContactConflicts', () => {

const result = await resolveContactConflicts({ contactId: 'contactId', name: 'New Name' });

expect(modelsMock.LivechatContacts.findOneById.getCall(0).args[0]).to.be.equal('contactId');
expect(modelsMock.LivechatContacts.findOneEnabledById.getCall(0).args[0]).to.be.equal('contactId');

expect(modelsMock.Settings.incrementValueById.getCall(0).args[0]).to.be.equal('Livechat_conflicting_fields_counter');
expect(modelsMock.Settings.incrementValueById.getCall(0).args[1]).to.be.equal(1);
Expand All @@ -91,7 +91,7 @@ describe('resolveContactConflicts', () => {
});

it('should update the contact with the resolved contact manager', async () => {
modelsMock.LivechatContacts.findOneById.resolves({
modelsMock.LivechatContacts.findOneEnabledById.resolves({
_id: 'contactId',
name: 'Name',
contactManager: 'contactManagerId',
Expand All @@ -109,7 +109,7 @@ describe('resolveContactConflicts', () => {

const result = await resolveContactConflicts({ contactId: 'contactId', name: 'New Name' });

expect(modelsMock.LivechatContacts.findOneById.getCall(0).args[0]).to.be.equal('contactId');
expect(modelsMock.LivechatContacts.findOneEnabledById.getCall(0).args[0]).to.be.equal('contactId');

expect(modelsMock.Settings.incrementValueById.getCall(0).args[0]).to.be.equal('Livechat_conflicting_fields_counter');
expect(modelsMock.Settings.incrementValueById.getCall(0).args[1]).to.be.equal(1);
Expand All @@ -126,7 +126,7 @@ describe('resolveContactConflicts', () => {

it('should wipe conflicts if wipeConflicts = true', async () => {
it('should update the contact with the resolved name', async () => {
modelsMock.LivechatContacts.findOneById.resolves({
modelsMock.LivechatContacts.findOneEnabledById.resolves({
_id: 'contactId',
name: 'Name',
customFields: { customField: 'newValue' },
Expand All @@ -145,7 +145,7 @@ describe('resolveContactConflicts', () => {

const result = await resolveContactConflicts({ contactId: 'contactId', name: 'New Name', wipeConflicts: true });

expect(modelsMock.LivechatContacts.findOneById.getCall(0).args[0]).to.be.equal('contactId');
expect(modelsMock.LivechatContacts.findOneEnabledById.getCall(0).args[0]).to.be.equal('contactId');

expect(modelsMock.Settings.incrementValueById.getCall(0).args[0]).to.be.equal('Livechat_conflicting_fields_counter');
expect(modelsMock.Settings.incrementValueById.getCall(0).args[1]).to.be.equal(2);
Expand All @@ -163,7 +163,7 @@ describe('resolveContactConflicts', () => {

it('should wipe conflicts if wipeConflicts = true', async () => {
it('should update the contact with the resolved name', async () => {
modelsMock.LivechatContacts.findOneById.resolves({
modelsMock.LivechatContacts.findOneEnabledById.resolves({
_id: 'contactId',
name: 'Name',
customFields: { customField: 'newValue' },
Expand All @@ -182,7 +182,7 @@ describe('resolveContactConflicts', () => {

const result = await resolveContactConflicts({ contactId: 'contactId', name: 'New Name', wipeConflicts: false });

expect(modelsMock.LivechatContacts.findOneById.getCall(0).args[0]).to.be.equal('contactId');
expect(modelsMock.LivechatContacts.findOneEnabledById.getCall(0).args[0]).to.be.equal('contactId');

expect(modelsMock.Settings.incrementValueById.getCall(0).args[0]).to.be.equal('Livechat_conflicting_fields_counter');
expect(modelsMock.Settings.incrementValueById.getCall(0).args[1]).to.be.equal(1);
Expand All @@ -199,15 +199,15 @@ describe('resolveContactConflicts', () => {
});

it('should throw an error if the contact does not exist', async () => {
modelsMock.LivechatContacts.findOneById.resolves(undefined);
modelsMock.LivechatContacts.findOneEnabledById.resolves(undefined);
await expect(resolveContactConflicts({ contactId: 'id', customField: { customField: 'newValue' } })).to.be.rejectedWith(
'error-contact-not-found',
);
expect(modelsMock.LivechatContacts.updateContact.getCall(0)).to.be.null;
});

it('should throw an error if the contact has no conflicting fields', async () => {
modelsMock.LivechatContacts.findOneById.resolves({
modelsMock.LivechatContacts.findOneEnabledById.resolves({
_id: 'contactId',
name: 'Name',
contactManager: 'contactManagerId',
Expand All @@ -221,7 +221,7 @@ describe('resolveContactConflicts', () => {
});

it('should throw an error if the contact manager is invalid', async () => {
modelsMock.LivechatContacts.findOneById.resolves({
modelsMock.LivechatContacts.findOneEnabledById.resolves({
_id: 'contactId',
name: 'Name',
contactManager: 'contactManagerId',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,12 @@ export type ResolveContactConflictsParams = {
export async function resolveContactConflicts(params: ResolveContactConflictsParams): Promise<ILivechatContact> {
const { contactId, name, customFields, contactManager, wipeConflicts } = params;

const contact = await LivechatContacts.findOneById<Pick<ILivechatContact, '_id' | 'customFields' | 'conflictingFields'>>(contactId, {
projection: { _id: 1, customFields: 1, conflictingFields: 1 },
});
const contact = await LivechatContacts.findOneEnabledById<Pick<ILivechatContact, '_id' | 'customFields' | 'conflictingFields'>>(
contactId,
{
projection: { _id: 1, customFields: 1, conflictingFields: 1 },
},
);

if (!contact) {
throw new Error('error-contact-not-found');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import sinon from 'sinon';

const modelsMock = {
LivechatContacts: {
findOneById: sinon.stub(),
findOneEnabledById: sinon.stub(),
updateContact: sinon.stub(),
},
LivechatRooms: {
Expand All @@ -28,19 +28,19 @@ const { updateContact } = proxyquire.noCallThru().load('./updateContact', {

describe('updateContact', () => {
beforeEach(() => {
modelsMock.LivechatContacts.findOneById.reset();
modelsMock.LivechatContacts.findOneEnabledById.reset();
modelsMock.LivechatContacts.updateContact.reset();
modelsMock.LivechatRooms.updateContactDataByContactId.reset();
});

it('should throw an error if the contact does not exist', async () => {
modelsMock.LivechatContacts.findOneById.resolves(undefined);
modelsMock.LivechatContacts.findOneEnabledById.resolves(undefined);
await expect(updateContact('any_id')).to.be.rejectedWith('error-contact-not-found');
expect(modelsMock.LivechatContacts.updateContact.getCall(0)).to.be.null;
});

it('should update the contact with correct params', async () => {
modelsMock.LivechatContacts.findOneById.resolves({ _id: 'contactId' });
modelsMock.LivechatContacts.findOneEnabledById.resolves({ _id: 'contactId' });
modelsMock.LivechatContacts.updateContact.resolves({ _id: 'contactId', name: 'John Doe' } as any);

const updatedContact = await updateContact({ contactId: 'contactId', name: 'John Doe' });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export type UpdateContactParams = {
export async function updateContact(params: UpdateContactParams): Promise<ILivechatContact> {
const { contactId, name, emails, phones, customFields: receivedCustomFields, contactManager, channels, wipeConflicts } = params;

const contact = await LivechatContacts.findOneById<Pick<ILivechatContact, '_id' | 'name' | 'customFields' | 'conflictingFields'>>(
const contact = await LivechatContacts.findOneEnabledById<Pick<ILivechatContact, '_id' | 'name' | 'customFields' | 'conflictingFields'>>(
contactId,
{
projection: { _id: 1, name: 1, customFields: 1, conflictingFields: 1 },
Expand Down
33 changes: 26 additions & 7 deletions apps/meteor/app/livechat/server/lib/guests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,11 +78,30 @@ export async function saveGuest(
return ret;
}

export async function removeGuest({ _id, token }: { _id: string; token: string }) {
await cleanGuestHistory(token);
async function removeGuest({ _id }: { _id: string }) {
await cleanGuestHistory(_id);
return LivechatVisitors.disableById(_id);
}

export async function removeContactsByVisitorId({ _id }: { _id: string }) {
// A visitor shouldn't have many contacts associated, so we can remove them like this
const contacts = await LivechatContacts.findAllByVisitorId(_id).toArray();
if (!contacts.length) {
livechatLogger.debug({ msg: 'No contacts found for visitor', visitorId: _id });
await removeGuest({ _id });
}

// And a contact shouldn't have many channels associated, so we can do this
livechatLogger.debug({ msg: 'Removing channels for contacts', visitorId: _id, contacts: contacts.map(({ _id }) => _id) });
for await (const contact of contacts) {
for await (const { visitor } of contact.channels) {
await removeGuest({ _id: visitor.visitorId });
}

await LivechatContacts.disableByVisitorId(_id);
}
}

export async function registerGuest(newData: RegisterGuestType): Promise<ILivechatVisitor | null> {
const visitor = await Visitors.registerGuest(newData);
if (!visitor) {
Expand Down Expand Up @@ -123,14 +142,14 @@ export async function registerGuest(newData: RegisterGuestType): Promise<ILivech
return visitor;
}

async function cleanGuestHistory(token: string) {
async function cleanGuestHistory(_id: string) {
// This shouldn't be possible, but just in case
if (!token) {
if (!_id) {
throw new Error('error-invalid-guest');
}

// TODO: optimize function => instead of removing one by one, fetch the _ids of the rooms and then remove them in bulk
const cursor = LivechatRooms.findByVisitorToken(token, { projection: { _id: 1 } });
const cursor = LivechatRooms.findByVisitorId(_id, { projection: { _id: 1 } });
for await (const room of cursor) {
await Promise.all([
Subscriptions.removeByRoomId(room._id, {
Expand All @@ -144,9 +163,9 @@ async function cleanGuestHistory(token: string) {
]);
}

await LivechatRooms.removeByVisitorToken(token);
await LivechatRooms.removeByVisitorId(_id);

const livechatInquiries = await LivechatInquiry.findIdsByVisitorToken(token).toArray();
const livechatInquiries = await LivechatInquiry.findIdsByVisitorId(_id).toArray();
await LivechatInquiry.removeByIds(livechatInquiries.map(({ _id }) => _id));
void notifyOnLivechatInquiryChanged(livechatInquiries, 'removed');
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ checkDefaultAgentOnNewRoom.patch(async (_next, defaultAgent, { visitorId, source
}

const contactId = await migrateVisitorIfMissingContact(visitorId, source);
const contact = contactId ? await LivechatContacts.findOneById(contactId, { projection: { contactManager: 1 } }) : undefined;
const contact = contactId ? await LivechatContacts.findOneEnabledById(contactId, { projection: { contactManager: 1 } }) : undefined;

const contactManagerPreferred = settings.get<boolean>('Omnichannel_contact_manager_routing');
const guestManager = contactManagerPreferred && (await getDefaultAgent({ id: contact?.contactManager }));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export const runIsAgentAvailableToTakeContactInquiry = async (
source: IOmnichannelSource,
contactId: ILivechatContact['_id'],
): Promise<{ error: string; value: false } | { value: true }> => {
const contact = await LivechatContacts.findOneById<Pick<ILivechatContact, '_id' | 'unknown' | 'channels'>>(contactId, {
const contact = await LivechatContacts.findOneEnabledById<Pick<ILivechatContact, '_id' | 'unknown' | 'channels'>>(contactId, {
projection: {
unknown: 1,
channels: 1,
Expand Down
4 changes: 2 additions & 2 deletions apps/meteor/ee/server/patches/mergeContacts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export const runMergeContacts = async (
visitor: ILivechatContactVisitorAssociation,
session?: ClientSession,
): Promise<ILivechatContact | null> => {
const originalContact = await LivechatContacts.findOneById(contactId, { session });
const originalContact = await LivechatContacts.findOneEnabledById(contactId, { session });
if (!originalContact) {
throw new Error('error-invalid-contact');
}
Expand Down Expand Up @@ -57,7 +57,7 @@ export const runMergeContacts = async (
logger.debug({ msg: 'Updating rooms with new contact id', contactId });
await LivechatRooms.updateMergedContactIds(similarContactIds, contactId, { session });

return LivechatContacts.findOneById(contactId, { session });
return LivechatContacts.findOneEnabledById(contactId, { session });
};

mergeContacts.patch(runMergeContacts, () => License.hasModule('contact-id-verification'));
Loading
Loading