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

Co-authored-by: Douglas Fabris <27704687+dougfabris@users.noreply.github.com>
pull/34061/head
Matheus Barbosa Silva 1 year ago committed by GitHub
parent e72e9d6316
commit 97edeccdda
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 39
      apps/meteor/app/lib/server/lib/notifyListener.ts
  2. 19
      apps/meteor/app/livechat/server/lib/contacts/updateContact.ts
  3. 15
      apps/meteor/server/models/raw/LivechatInquiry.ts
  4. 5
      apps/meteor/server/models/raw/LivechatRooms.ts
  5. 23
      apps/meteor/server/models/raw/Subscriptions.ts
  6. 16
      apps/meteor/tests/e2e/omnichannel/omnichannel-contact-info.spec.ts
  7. 56
      apps/meteor/tests/end-to-end/api/livechat/contacts.ts
  8. 2
      packages/model-typings/src/models/ILivechatInquiryModel.ts
  9. 1
      packages/model-typings/src/models/ILivechatRoomsModel.ts
  10. 4
      packages/model-typings/src/models/ISubscriptionsModel.ts

@ -21,9 +21,11 @@ import type {
IMessage,
SettingValue,
MessageTypesValues,
ILivechatContact,
} from '@rocket.chat/core-typings';
import {
Rooms,
LivechatRooms,
Permissions,
Settings,
PbxEvents,
@ -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]);
@ -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'],
@ -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 });

@ -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;
@ -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;

@ -102,6 +102,7 @@ export class LivechatInquiryRaw extends BaseRaw<ILivechatInquiryRecord> implemen
},
sparse: true,
},
{ key: { 'v._id': 1 } },
];
}
@ -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);
}
}

@ -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' } },
];
}
@ -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);
}
}

@ -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 } },
];
}
@ -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,
@ -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 }) } };

@ -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();
@ -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);
@ -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();
});
});
});

@ -1,4 +1,5 @@
import { faker } from '@faker-js/faker';
import type { Credentials } from '@rocket.chat/api-client';
import type {
ILivechatAgent,
ILivechatVisitor,
@ -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';
@ -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 () => {
@ -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', () => {

@ -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>;
}

@ -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>;
}

@ -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'];
@ -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>;

Loading…
Cancel
Save