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

regression: Update and notify subscriptions on contact name update #33939

Merged
merged 20 commits into from
Nov 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
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
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 @@ -463,4 +464,18 @@ export class LivechatInquiryRaw extends BaseRaw<ILivechatInquiryRecord> implemen
const updated = await this.findOneAndUpdate({ rid }, { $addToSet: { 'v.activity': period } });
return updated?.value;
}

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 @@ -2817,4 +2818,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
Loading