Skip to content

Commit

Permalink
regression: Update and notify subscriptions on contact name update (#…
Browse files Browse the repository at this point in the history
…33939)

Co-authored-by: Douglas Fabris <[email protected]>
  • Loading branch information
2 people authored and ggazzo committed Dec 5, 2024
1 parent 2f7b59f commit 745f5f5
Show file tree
Hide file tree
Showing 10 changed files with 173 additions and 7 deletions.
39 changes: 39 additions & 0 deletions apps/meteor/app/lib/server/lib/notifyListener.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,11 @@ import type {
IMessage,
SettingValue,
MessageTypesValues,
ILivechatContact,
} from '@rocket.chat/core-typings';
import {
Rooms,
LivechatRooms,
Permissions,
Settings,
PbxEvents,
Expand Down Expand Up @@ -87,6 +89,16 @@ export const notifyOnRoomChangedByUsernamesOrUids = withDbWatcherCheck(
},
);

export const notifyOnRoomChangedByContactId = withDbWatcherCheck(
async <T extends ILivechatContact>(contactId: T['_id'], clientAction: ClientAction = 'updated'): Promise<void> => {
const cursor = LivechatRooms.findOpenByContactId(contactId);

void cursor.forEach((room) => {
void api.broadcast('watch.rooms', { clientAction, room });
});
},
);

export const notifyOnRoomChangedByUserDM = withDbWatcherCheck(
async <T extends IRoom>(userId: T['u']['_id'], clientAction: ClientAction = 'updated'): Promise<void> => {
const items = Rooms.findDMsByUids([userId]);
Expand Down Expand Up @@ -251,6 +263,20 @@ export const notifyOnLivechatInquiryChangedById = withDbWatcherCheck(
},
);

export const notifyOnLivechatInquiryChangedByVisitorIds = withDbWatcherCheck(
async (
visitorIds: ILivechatInquiryRecord['v']['_id'][],
clientAction: Exclude<ClientAction, 'removed'> = 'updated',
diff?: Partial<Record<keyof ILivechatInquiryRecord, unknown> & { queuedAt: Date; takenAt: Date }>,
): Promise<void> => {
const cursor = LivechatInquiry.findByVisitorIds(visitorIds);

void cursor.forEach((inquiry) => {
void api.broadcast('watch.inquiries', { clientAction, inquiry, diff });
});
},
);

export const notifyOnLivechatInquiryChangedByRoom = withDbWatcherCheck(
async (
rid: ILivechatInquiryRecord['rid'],
Expand Down Expand Up @@ -553,6 +579,19 @@ export const notifyOnSubscriptionChangedByUserIdAndRoomType = withDbWatcherCheck
},
);

export const notifyOnSubscriptionChangedByVisitorIds = withDbWatcherCheck(
async (
visitorIds: Exclude<ISubscription['v'], undefined>['_id'][],
clientAction: Exclude<ClientAction, 'removed'> = 'updated',
): Promise<void> => {
const cursor = Subscriptions.findOpenByVisitorIds(visitorIds, { projection: subscriptionFields });

void cursor.forEach((subscription) => {
void api.broadcast('watch.subscriptions', { clientAction, subscription });
});
},
);

export const notifyOnSubscriptionChangedByNameAndRoomType = withDbWatcherCheck(
async (filter: Partial<Pick<ISubscription, 'name' | 't'>>, clientAction: Exclude<ClientAction, 'removed'> = 'updated'): Promise<void> => {
const cursor = Subscriptions.findByNameAndRoomType(filter, { projection: subscriptionFields });
Expand Down
19 changes: 17 additions & 2 deletions apps/meteor/app/livechat/server/lib/contacts/updateContact.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import type { ILivechatContact, ILivechatContactChannel } from '@rocket.chat/core-typings';
import { LivechatContacts, LivechatRooms } from '@rocket.chat/models';
import { LivechatContacts, LivechatInquiry, LivechatRooms, Subscriptions } from '@rocket.chat/models';

import { getAllowedCustomFields } from './getAllowedCustomFields';
import { validateContactManager } from './validateContactManager';
import { validateCustomFields } from './validateCustomFields';
import {
notifyOnSubscriptionChangedByVisitorIds,
notifyOnRoomChangedByContactId,
notifyOnLivechatInquiryChangedByVisitorIds,
} from '../../../../lib/server/lib/notifyListener';

export type UpdateContactParams = {
contactId: string;
Expand Down Expand Up @@ -43,9 +48,19 @@ export async function updateContact(params: UpdateContactParams): Promise<ILivec
...(wipeConflicts && { conflictingFields: [] }),
});

// If the contact name changed, update the name of its existing rooms
// If the contact name changed, update the name of its existing rooms and subscriptions
if (name !== undefined && name !== contact.name) {
await LivechatRooms.updateContactDataByContactId(contactId, { name });
void notifyOnRoomChangedByContactId(contactId);

const visitorIds = updatedContact.channels?.map((channel) => channel.visitor.visitorId);
if (visitorIds?.length) {
await Subscriptions.updateNameAndFnameByVisitorIds(visitorIds, name);
void notifyOnSubscriptionChangedByVisitorIds(visitorIds);

await LivechatInquiry.updateNameByVisitorIds(visitorIds, name);
void notifyOnLivechatInquiryChangedByVisitorIds(visitorIds, 'updated', { name });
}
}

return updatedContact;
Expand Down
15 changes: 15 additions & 0 deletions apps/meteor/server/models/raw/LivechatInquiry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ export class LivechatInquiryRaw extends BaseRaw<ILivechatInquiryRecord> implemen
},
sparse: true,
},
{ key: { 'v._id': 1 } },
];
}

Expand Down Expand Up @@ -456,4 +457,18 @@ export class LivechatInquiryRaw extends BaseRaw<ILivechatInquiryRecord> implemen
async markInquiryActiveForPeriod(rid: ILivechatInquiryRecord['rid'], period: string): Promise<ILivechatInquiryRecord | null> {
return this.findOneAndUpdate({ rid }, { $addToSet: { 'v.activity': period } });
}

updateNameByVisitorIds(visitorIds: string[], name: string): Promise<UpdateResult | Document> {
const query = { 'v._id': { $in: visitorIds } };

const update = {
$set: { name },
};

return this.updateMany(query, update);
}

findByVisitorIds(visitorIds: string[], options?: FindOptions<ILivechatInquiryRecord>): FindCursor<ILivechatInquiryRecord> {
return this.find({ 'v._id': { $in: visitorIds } }, options);
}
}
5 changes: 5 additions & 0 deletions apps/meteor/server/models/raw/LivechatRooms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ export class LivechatRoomsRaw extends BaseRaw<IOmnichannelRoom> implements ILive
{ key: { 'tags.0': 1, 'ts': 1 }, partialFilterExpression: { 'tags.0': { $exists: true }, 't': 'l' } },
{ key: { servedBy: 1, ts: 1 }, partialFilterExpression: { servedBy: { $exists: true }, t: 'l' } },
{ key: { 'v.activity': 1, 'ts': 1 }, partialFilterExpression: { 'v.activity': { $exists: true }, 't': 'l' } },
{ key: { contactId: 1 }, partialFilterExpression: { contactId: { $exists: true }, t: 'l' } },
];
}

Expand Down Expand Up @@ -2816,4 +2817,8 @@ export class LivechatRoomsRaw extends BaseRaw<IOmnichannelRoom> implements ILive
}): FindPaginated<FindCursor<IOmnichannelRoom>> {
throw new Error('Method not implemented.');
}

findOpenByContactId(contactId: ILivechatContact['_id'], options?: FindOptions<IOmnichannelRoom>): FindCursor<IOmnichannelRoom> {
return this.find({ open: true, contactId }, options);
}
}
23 changes: 23 additions & 0 deletions apps/meteor/server/models/raw/Subscriptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ export class SubscriptionsRaw extends BaseRaw<ISubscription> implements ISubscri
{ key: { 'u._id': 1, 'open': 1, 'department': 1 } },
{ key: { rid: 1, ls: 1 } },
{ key: { 'u._id': 1, 'autotranslate': 1 } },
{ key: { 'v._id': 1, 'open': 1 } },
];
}

Expand Down Expand Up @@ -341,6 +342,15 @@ export class SubscriptionsRaw extends BaseRaw<ISubscription> implements ISubscri
return this.find(query, options || {});
}

findOpenByVisitorIds(visitorIds: string[], options?: FindOptions<ISubscription>): FindCursor<ISubscription> {
const query = {
'open': true,
'v._id': { $in: visitorIds },
};

return this.find(query, options || {});
}

findByRoomIdAndNotAlertOrOpenExcludingUserIds(
{
roomId,
Expand Down Expand Up @@ -594,6 +604,19 @@ export class SubscriptionsRaw extends BaseRaw<ISubscription> implements ISubscri
return this.updateMany(query, update);
}

updateNameAndFnameByVisitorIds(visitorIds: string[], name: string): Promise<UpdateResult | Document> {
const query = { 'v._id': { $in: visitorIds } };

const update = {
$set: {
name,
fname: name,
},
};

return this.updateMany(query, update);
}

async setGroupE2EKeyAndOldRoomKeys(_id: string, key: string, oldRoomKeys?: ISubscription['oldRoomKeys']): Promise<UpdateResult> {
const query = { _id };
const update = { $set: { E2EKey: key, ...(oldRoomKeys && { oldRoomKeys }) } };
Expand Down
16 changes: 12 additions & 4 deletions apps/meteor/tests/e2e/omnichannel/omnichannel-contact-info.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@ import { createFakeVisitor } from '../../mocks/data';
import { createAuxContext } from '../fixtures/createAuxContext';
import { Users } from '../fixtures/userStates';
import { OmnichannelLiveChat, HomeChannel } from '../page-objects';
import { test } from '../utils/test';
import { OmnichannelContacts } from '../page-objects/omnichannel-contacts-list';
import { expect, test } from '../utils/test';

test.describe('Omnichannel contact info', () => {
let poLiveChat: OmnichannelLiveChat;
let newVisitor: { email: string; name: string };

let agent: { page: Page; poHomeChannel: HomeChannel };
let agent: { page: Page; poHomeChannel: HomeChannel; poContacts: OmnichannelContacts };

test.beforeAll(async ({ api, browser }) => {
newVisitor = createFakeVisitor();
Expand All @@ -20,7 +21,7 @@ test.describe('Omnichannel contact info', () => {
await api.post('/livechat/users/manager', { username: 'user1' });

const { page } = await createAuxContext(browser, Users.user1);
agent = { page, poHomeChannel: new HomeChannel(page) };
agent = { page, poHomeChannel: new HomeChannel(page), poContacts: new OmnichannelContacts(page) };
});
test.beforeEach(async ({ page, api }) => {
poLiveChat = new OmnichannelLiveChat(page, api);
Expand All @@ -45,9 +46,16 @@ test.describe('Omnichannel contact info', () => {
await agent.poHomeChannel.sidenav.openChat(newVisitor.name);
});

await test.step('Expect to be see contact information and edit', async () => {
await test.step('Expect to be able to see contact information and edit', async () => {
await agent.poHomeChannel.content.btnContactInformation.click();
await agent.poHomeChannel.content.btnContactEdit.click();
});

await test.step('Expect to update room name and subscription when updating contact name', async () => {
await agent.poContacts.newContact.inputName.fill('Edited Contact Name');
await agent.poContacts.newContact.btnSave.click();
await expect(agent.poHomeChannel.sidenav.sidebarChannelsList.getByText('Edited Contact Name')).toBeVisible();
await expect(agent.poHomeChannel.content.channelHeader.getByText('Edited Contact Name')).toBeVisible();
});
});
});
56 changes: 55 additions & 1 deletion apps/meteor/tests/end-to-end/api/livechat/contacts.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { faker } from '@faker-js/faker';
import type { Credentials } from '@rocket.chat/api-client';
import type {
ILivechatAgent,
ILivechatVisitor,
Expand All @@ -18,9 +19,11 @@ import {
createLivechatRoomWidget,
createVisitor,
deleteVisitor,
fetchInquiry,
getLivechatRoomInfo,
startANewLivechatRoomAndTakeIt,
} from '../../../data/livechat/rooms';
import { removeAgent } from '../../../data/livechat/users';
import { createAnOnlineAgent, removeAgent } from '../../../data/livechat/users';
import { removePermissionFromAllRoles, restorePermissionToRoles, updatePermission, updateSetting } from '../../../data/permissions.helper';
import { createUser, deleteUser } from '../../../data/users.helper';
import { expectInvalidParams } from '../../../data/validation.helper';
Expand Down Expand Up @@ -595,12 +598,16 @@ describe('LIVECHAT - contacts', () => {
});

describe('Contact Rooms', () => {
let agent: { credentials: Credentials; user: IUser & { username: string } };

before(async () => {
await updatePermission('view-livechat-contact', ['admin']);
agent = await createAnOnlineAgent();
});

after(async () => {
await restorePermissionToRoles('view-livechat-contact');
await deleteUser(agent.user);
});

it('should create a contact and assign it to the room', async () => {
Expand Down Expand Up @@ -651,6 +658,53 @@ describe('LIVECHAT - contacts', () => {
expect(sameRoom._id).to.be.equal(room._id);
expect(sameRoom.fname).to.be.equal('New Contact Name');
});

it('should update room subscriptions when a contact name changes', async () => {
const response = await startANewLivechatRoomAndTakeIt({ agent: agent.credentials });
const { room, visitor } = response;
const newName = faker.person.fullName();

expect(room).to.have.property('contactId').that.is.a('string');
expect(room.fname).to.be.equal(visitor.name);

const res = await request.post(api('omnichannel/contacts.update')).set(credentials).send({
contactId: room.contactId,
name: newName,
});

expect(res.status).to.be.equal(200);

const sameRoom = await createLivechatRoom(visitor.token, { rid: room._id });
expect(sameRoom._id).to.be.equal(room._id);
expect(sameRoom.fname).to.be.equal(newName);

const subscriptionResponse = await request
.get(api('subscriptions.getOne'))
.set(agent.credentials)
.query({ roomId: room._id })
.expect('Content-Type', 'application/json');
const { subscription } = subscriptionResponse.body;
expect(subscription).to.have.property('v').that.is.an('object');
expect(subscription.v).to.have.property('_id', visitor._id);
expect(subscription).to.have.property('name', newName);
expect(subscription).to.have.property('fname', newName);
});

it('should update inquiry when a contact name changes', async () => {
const visitor = await createVisitor();
const room = await createLivechatRoom(visitor.token);
expect(room).to.have.property('contactId').that.is.a('string');
expect(room.fname).to.not.be.equal('New Contact Name');

const res = await request.post(api('omnichannel/contacts.update')).set(credentials).send({
contactId: room.contactId,
name: 'Edited Contact Name Inquiry',
});
expect(res.status).to.be.equal(200);

const roomInquiry = await fetchInquiry(room._id);
expect(roomInquiry).to.have.property('name', 'Edited Contact Name Inquiry');
});
});

describe('[GET] omnichannel/contacts.get', () => {
Expand Down
2 changes: 2 additions & 0 deletions packages/model-typings/src/models/ILivechatInquiryModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,6 @@ export interface ILivechatInquiryModel extends IBaseModel<ILivechatInquiryRecord
markInquiryActiveForPeriod(rid: ILivechatInquiryRecord['rid'], period: string): Promise<ILivechatInquiryRecord | null>;
findIdsByVisitorToken(token: ILivechatInquiryRecord['v']['token']): FindCursor<ILivechatInquiryRecord>;
setStatusById(inquiryId: string, status: LivechatInquiryStatus): Promise<ILivechatInquiryRecord>;
updateNameByVisitorIds(visitorIds: string[], name: string): Promise<UpdateResult | Document>;
findByVisitorIds(visitorIds: string[], options?: FindOptions<ILivechatInquiryRecord>): FindCursor<ILivechatInquiryRecord>;
}
1 change: 1 addition & 0 deletions packages/model-typings/src/models/ILivechatRoomsModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -286,4 +286,5 @@ export interface ILivechatRoomsModel extends IBaseModel<IOmnichannelRoom> {
oldContactId: ILivechatContact['_id'],
contact: Partial<Pick<ILivechatContact, '_id' | 'name'>>,
): Promise<UpdateResult | Document>;
findOpenByContactId(contactId: ILivechatContact['_id'], options?: FindOptions<IOmnichannelRoom>): FindCursor<IOmnichannelRoom>;
}
4 changes: 4 additions & 0 deletions packages/model-typings/src/models/ISubscriptionsModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ export interface ISubscriptionsModel extends IBaseModel<ISubscription> {

findByUserIdAndTypes(userId: string, types: ISubscription['t'][], options?: FindOptions<ISubscription>): FindCursor<ISubscription>;

findOpenByVisitorIds(visitorIds: string[], options?: FindOptions<ISubscription>): FindCursor<ISubscription>;

findByRoomIdAndNotAlertOrOpenExcludingUserIds(
filter: {
roomId: ISubscription['rid'];
Expand Down Expand Up @@ -114,6 +116,8 @@ export interface ISubscriptionsModel extends IBaseModel<ISubscription> {

updateNameAndFnameByRoomId(roomId: string, name: string, fname: string): Promise<UpdateResult | Document>;

updateNameAndFnameByVisitorIds(visitorIds: string[], name: string): Promise<UpdateResult | Document>;

setGroupE2EKey(_id: string, key: string): Promise<UpdateResult>;

setGroupE2EKeyAndOldRoomKeys(_id: string, key: string, oldRoomKeys: ISubscription['oldRoomKeys']): Promise<UpdateResult>;
Expand Down

0 comments on commit 745f5f5

Please sign in to comment.