diff --git a/apps/meteor/app/lib/server/functions/createDirectRoom.ts b/apps/meteor/app/lib/server/functions/createDirectRoom.ts index 1fcac1c46ed2d..6ed0c96143c0a 100644 --- a/apps/meteor/app/lib/server/functions/createDirectRoom.ts +++ b/apps/meteor/app/lib/server/functions/createDirectRoom.ts @@ -9,6 +9,7 @@ import type { MatchKeysAndValues } from 'mongodb'; import { isTruthy } from '../../../../lib/isTruthy'; import { callbacks } from '../../../../server/lib/callbacks'; +import { getNameForDMs } from '../../../../server/services/room/getNameForDMs'; import { settings } from '../../../settings/server'; import { getDefaultSubscriptionPref } from '../../../utils/lib/getDefaultSubscriptionPref'; import { notifyOnRoomChangedById, notifyOnSubscriptionChangedByRoomIdAndUserId } from '../lib/notifyListener'; @@ -37,9 +38,6 @@ const generateSubscription = ( }, }); -const getFname = (members: IUser[]): string => members.map(({ name, username }) => name || username).join(', '); -const getName = (members: IUser[]): string => members.map(({ username }) => username).join(', '); - export async function createDirectRoom( members: IUser[] | string[], roomExtraData: Partial = {}, @@ -71,6 +69,7 @@ export async function createDirectRoom( await callbacks.run('beforeCreateDirectRoom', membersUsernames, roomExtraData); const roomMembers = await Users.findUsersByUsernames(membersUsernames).toArray(); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const sortedMembers = roomMembers.sort((u1, u2) => (u1.name! || u1.username!).localeCompare(u2.name! || u2.username!)); @@ -159,9 +158,9 @@ export async function createDirectRoom( throw new Meteor.Error('error-creator-not-in-room', 'The creator user must be part of the direct room'); } - for await (const member of membersWithPreferences) { - const otherMembers = sortedMembers.filter(({ _id }) => _id !== member._id); + const roomNames = getNameForDMs(roomMembers); + for await (const member of membersWithPreferences) { const subscriptionStatus: Partial = roomExtraData.federated && options.creator !== member._id && creatorUser ? { @@ -177,11 +176,13 @@ export async function createDirectRoom( } : {}; + const { fname, name } = roomNames[member._id]; + const { modifiedCount, upsertedCount } = await Subscriptions.updateOne( { rid, 'u._id': member._id }, { ...(options?.creator === member._id && { $set: { open: true } }), - $setOnInsert: generateSubscription(getFname(otherMembers), getName(otherMembers), member, { + $setOnInsert: generateSubscription(fname, name, member, { ...options?.subscriptionExtra, ...(options?.creator !== member._id && { open: members.length > 2 }), ...subscriptionStatus, diff --git a/apps/meteor/app/lib/server/functions/removeUserFromRoom.ts b/apps/meteor/app/lib/server/functions/removeUserFromRoom.ts index 0a899a88690e7..b58710aaeb5e1 100644 --- a/apps/meteor/app/lib/server/functions/removeUserFromRoom.ts +++ b/apps/meteor/app/lib/server/functions/removeUserFromRoom.ts @@ -27,11 +27,15 @@ export const performUserRemoval = async function ( return; } + // make TS happy, this should never happen + if (!user.username) { + throw new Error('User must have a username to be removed from the room'); + } + // TODO: move before callbacks to service await beforeLeaveRoomCallback.run(user, room); if (subscription) { - const removedUser = user; if (options?.customSystemMessage) { await Message.saveSystemMessage(options?.customSystemMessage, room._id, user.username || '', user); } else if (options?.byUser) { @@ -40,16 +44,16 @@ export const performUserRemoval = async function ( }; if (room.teamMain) { - await Message.saveSystemMessage('removed-user-from-team', room._id, user.username || '', user, extraData); + await Message.saveSystemMessage('removed-user-from-team', room._id, user.username, user, extraData); } else { - await Message.saveSystemMessage('ru', room._id, user.username || '', user, extraData); + await Message.saveSystemMessage('ru', room._id, user.username, user, extraData); } } else if (subscription.status === 'INVITED') { - await Message.saveSystemMessage('uir', room._id, removedUser.username || '', removedUser); + await Message.saveSystemMessage('uir', room._id, user.username, user); } else if (room.teamMain) { - await Message.saveSystemMessage('ult', room._id, removedUser.username || '', removedUser); + await Message.saveSystemMessage('ult', room._id, user.username, user); } else { - await Message.saveSystemMessage('ul', room._id, removedUser.username || '', removedUser); + await Message.saveSystemMessage('ul', room._id, user.username, user); } } @@ -70,6 +74,11 @@ export const performUserRemoval = async function ( await Rooms.removeUsersFromE2EEQueueByRoomId(room._id, [user._id]); } + // remove references to the user in direct message rooms + if (room.t === 'd') { + await Rooms.removeUserReferenceFromDMsById(room._id, user.username, user._id); + } + void notifyOnRoomChangedById(room._id); }; diff --git a/apps/meteor/server/services/room/getNameForDMs.ts b/apps/meteor/server/services/room/getNameForDMs.ts new file mode 100644 index 0000000000000..a8cc4b3ce6afe --- /dev/null +++ b/apps/meteor/server/services/room/getNameForDMs.ts @@ -0,0 +1,33 @@ +import type { AtLeast, IUser } from '@rocket.chat/core-typings'; + +const getFname = (members: AtLeast[]): string | undefined => { + if (members.length === 0) { + return; + } + return members.map(({ name, username }) => name || username).join(', '); +}; +const getName = (members: AtLeast[]): string | undefined => { + if (members.length === 0) { + return; + } + return members.map(({ username }) => username).join(', '); +}; + +type NameMap = { [userId: string]: { fname: string; name: string } }; + +export function getNameForDMs(members: AtLeast[]): NameMap { + const nameMap: NameMap = {}; + + const sortedMembers = members.sort((u1, u2) => (u1.name! || u1.username!).localeCompare(u2.name! || u2.username!)); + + for (const member of sortedMembers) { + const otherMembers = sortedMembers.filter((m) => m._id !== member._id); + + nameMap[member._id] = { + fname: getFname(otherMembers) || member.name || member.username || '', + name: getName(otherMembers) || member.username || '', + }; + } + + return nameMap; +} diff --git a/apps/meteor/server/services/room/service.ts b/apps/meteor/server/services/room/service.ts index 481de2a38f22e..49b1336c5c99c 100644 --- a/apps/meteor/server/services/room/service.ts +++ b/apps/meteor/server/services/room/service.ts @@ -11,6 +11,7 @@ import { } from '@rocket.chat/core-typings'; import { Rooms, Subscriptions, Users } from '@rocket.chat/models'; +import { getNameForDMs } from './getNameForDMs'; import { FederationActions } from './hooks/BeforeFederationActions'; import { saveRoomName } from '../../../app/channel-settings/server'; import { saveRoomTopic } from '../../../app/channel-settings/server/functions/saveRoomTopic'; @@ -18,7 +19,7 @@ import { performAcceptRoomInvite } from '../../../app/lib/server/functions/accep import { addUserToRoom } from '../../../app/lib/server/functions/addUserToRoom'; import { createRoom } from '../../../app/lib/server/functions/createRoom'; // TODO remove this import import { removeUserFromRoom, performUserRemoval } from '../../../app/lib/server/functions/removeUserFromRoom'; -import { notifyOnSubscriptionChangedById } from '../../../app/lib/server/lib/notifyListener'; +import { notifyOnSubscriptionChangedById, notifyOnSubscriptionChangedByRoomIdAndUserId } from '../../../app/lib/server/lib/notifyListener'; import { getDefaultSubscriptionPref } from '../../../app/utils/lib/getDefaultSubscriptionPref'; import { getValidRoomName } from '../../../app/utils/server/lib/getValidRoomName'; import { RoomMemberActions } from '../../../definition/IRoomTypeConfig'; @@ -35,6 +36,24 @@ import { removeRoomOwner } from '../../methods/removeRoomOwner'; export class RoomService extends ServiceClassInternal implements IRoomService { protected name = 'room'; + async updateDirectMessageRoomName(room: IRoom): Promise { + const subs = await Subscriptions.findByRoomId(room._id, { projection: { u: 1 } }).toArray(); + + const uids = subs.map((sub) => sub.u._id); + + const roomMembers = await Users.findUsersByIds(uids, { projection: { name: 1, username: 1 } }).toArray(); + + const roomNames = getNameForDMs(roomMembers); + + for await (const sub of subs) { + await Subscriptions.updateOne({ _id: sub._id }, { $set: roomNames[sub.u._id] }); + + void notifyOnSubscriptionChangedByRoomIdAndUserId(room._id, sub.u._id, 'updated'); + } + + return true; + } + async create(uid: string, params: ICreateRoomParams): Promise { const { type, name, members = [], readOnly, extraData, options } = params; diff --git a/apps/meteor/tests/data/rooms.helper.ts b/apps/meteor/tests/data/rooms.helper.ts index 0f46f6ad6929a..d7a52ebbe3e3e 100644 --- a/apps/meteor/tests/data/rooms.helper.ts +++ b/apps/meteor/tests/data/rooms.helper.ts @@ -112,13 +112,20 @@ export function actionRoom({ action, type, roomId, overrideCredentials = credent export const deleteRoom = ({ type, roomId }: { type: ActionRoomParams['type']; roomId: IRoom['_id'] }) => actionRoom({ action: 'delete', type, roomId, overrideCredentials: credentials }); -export const getSubscriptionByRoomId = (roomId: IRoom['_id'], userCredentials = credentials): Promise => - new Promise((resolve) => { - void request +export const getSubscriptionByRoomId = (roomId: IRoom['_id'], userCredentials = credentials, req = request): Promise => + new Promise((resolve, reject) => { + void req .get(api('subscriptions.getOne')) .set(userCredentials) .query({ roomId }) - .end((_err, res) => { + .end((err, res) => { + if (err) { + return reject(err); + } + if (!res.body?.subscription) { + return reject(new Error('Subscription not found')); + } + resolve(res.body.subscription); }); }); diff --git a/apps/meteor/tests/unit/server/services/room/getNameForDMs.spec.ts b/apps/meteor/tests/unit/server/services/room/getNameForDMs.spec.ts new file mode 100644 index 0000000000000..e8efb080c9f5b --- /dev/null +++ b/apps/meteor/tests/unit/server/services/room/getNameForDMs.spec.ts @@ -0,0 +1,128 @@ +import { expect } from 'chai'; + +import { getNameForDMs } from '../../../../../server/services/room/getNameForDMs'; + +describe('getNameForDMs', () => { + it('should return empty object when members array is empty', () => { + const result = getNameForDMs([]); + expect(result).to.deep.equal({}); + }); + + it('should return own name for single member', () => { + const members = [{ _id: 'user1', name: 'John Doe', username: 'john' }]; + + const result = getNameForDMs(members); + + expect(result).to.deep.equal({ + user1: { + fname: 'John Doe', + name: 'john', + }, + }); + }); + + it('should return name map for two members using name field', () => { + const members = [ + { _id: 'user1', name: 'John Doe', username: 'john' }, + { _id: 'user2', name: 'Jane Smith', username: 'jane' }, + ]; + + const result = getNameForDMs(members); + + expect(result).to.deep.equal({ + user1: { + fname: 'Jane Smith', + name: 'jane', + }, + user2: { + fname: 'John Doe', + name: 'john', + }, + }); + }); + + it('should fallback to username when name is not available', () => { + const members = [ + { _id: 'user1', name: '', username: 'john' }, + { _id: 'user2', name: '', username: 'jane' }, + ]; + + const result = getNameForDMs(members); + + expect(result).to.deep.equal({ + user1: { + fname: 'jane', + name: 'jane', + }, + user2: { + fname: 'john', + name: 'john', + }, + }); + }); + + it('should handle multiple members and sort them alphabetically', () => { + const members = [ + { _id: 'user3', name: 'Charlie Brown', username: 'charlie' }, + { _id: 'user1', name: 'Alice Wonder', username: 'alice' }, + { _id: 'user2', name: 'Bob Builder', username: 'bob' }, + ]; + + const result = getNameForDMs(members); + + expect(result).to.deep.equal({ + user1: { + fname: 'Bob Builder, Charlie Brown', + name: 'bob, charlie', + }, + user2: { + fname: 'Alice Wonder, Charlie Brown', + name: 'alice, charlie', + }, + user3: { + fname: 'Alice Wonder, Bob Builder', + name: 'alice, bob', + }, + }); + }); + + it('should handle mix of members with and without names', () => { + const members = [ + { _id: 'user1', name: 'Alice Wonder', username: 'alice' }, + { _id: 'user2', name: '', username: 'bob' }, + ]; + + const result = getNameForDMs(members); + + expect(result).to.deep.equal({ + user1: { + fname: 'bob', + name: 'bob', + }, + user2: { + fname: 'Alice Wonder', + name: 'alice', + }, + }); + }); + + it('should sort by username when names are empty', () => { + const members = [ + { _id: 'user1', name: '', username: 'zebra' }, + { _id: 'user2', name: '', username: 'alpha' }, + ]; + + const result = getNameForDMs(members); + + expect(result).to.deep.equal({ + user1: { + fname: 'alpha', + name: 'alpha', + }, + user2: { + fname: 'zebra', + name: 'zebra', + }, + }); + }); +}); diff --git a/ee/packages/federation-matrix/src/events/member.ts b/ee/packages/federation-matrix/src/events/member.ts index 3ba2d107d8ade..ac7a7050748ea 100644 --- a/ee/packages/federation-matrix/src/events/member.ts +++ b/ee/packages/federation-matrix/src/events/member.ts @@ -77,7 +77,7 @@ async function getOrCreateFederatedRoom({ mrid: matrixRoomId, origin, }, - fname: roomFName, + ...(roomType !== 'd' && { fname: roomFName }), // DMs do not have a fname }, }); } catch (error) { @@ -232,6 +232,11 @@ async function handleLeave({ await Room.performUserRemoval(room, leavingUser); + // update room name for DMs + if (room.t === 'd') { + await Room.updateDirectMessageRoomName(room); + } + // TODO check if there are no pending invites to the room, and if so, delete the room } diff --git a/ee/packages/federation-matrix/tests/end-to-end/dms.spec.ts b/ee/packages/federation-matrix/tests/end-to-end/dms.spec.ts index 8b699bff90ab1..a899c0feb4165 100644 --- a/ee/packages/federation-matrix/tests/end-to-end/dms.spec.ts +++ b/ee/packages/federation-matrix/tests/end-to-end/dms.spec.ts @@ -1,12 +1,18 @@ -import type { ISubscription, IUser } from '@rocket.chat/core-typings'; +import type { IRoom, IRoomNativeFederated, ISubscription, IUser } from '@rocket.chat/core-typings'; import type { MatrixEvent, Room, RoomEmittedEvents } from 'matrix-js-sdk'; import { RoomStateEvent } from 'matrix-js-sdk'; import { api } from '../../../../../apps/meteor/tests/data/api-data'; -import { acceptRoomInvite, getSubscriptions } from '../../../../../apps/meteor/tests/data/rooms.helper'; +import { + acceptRoomInvite, + getRoomInfo, + getSubscriptionByRoomId, + getSubscriptions, +} from '../../../../../apps/meteor/tests/data/rooms.helper'; import { getRequestConfig, createUser, deleteUser } from '../../../../../apps/meteor/tests/data/users.helper'; import type { TestUser, IRequestConfig } from '../../../../../apps/meteor/tests/data/users.helper'; import { IS_EE } from '../../../../../apps/meteor/tests/e2e/config/constants'; +import { retry } from '../../../../../apps/meteor/tests/end-to-end/api/helpers/retry'; import { federationConfig } from '../helper/config'; import { SynapseClient } from '../helper/synapse-client'; @@ -83,52 +89,65 @@ const waitForRoomEvent = async ( }); describe('1:1 Direct Messages', () => { - let rcUser: TestUser; - let rcUserConfig: IRequestConfig; - let hs1Room: Room | null; - let invitedRoomId: string; - - const userDm = `dm-federation-user-${Date.now()}`; - const userDmId = `@${userDm}:${federationConfig.rc1.domain}`; - - beforeAll(async () => { - // create both RC and Synapse users - rcUser = await createUser( - { - username: userDm, - password: 'random', - email: `${userDm}}@rocket.chat`, - name: `DM Federation User ${Date.now()}`, - }, - rc1AdminRequestConfig, - ); - - rcUserConfig = await getRequestConfig(federationConfig.rc1.url, rcUser.username, 'random'); - }); - - afterAll(async () => { - // delete both RC and Synapse users - await deleteUser(rcUser, {}, rc1AdminRequestConfig); - }); - describe('Synapse as the resident server', () => { describe('Room list name validations', () => { + let rcUser: TestUser; + let rcUserConfig: IRequestConfig; + let hs1Room: Room; + let subscriptionInvite: ISubscription; + let rcRoom: IRoom; + + const userDm = `dm-federation-user-${Date.now()}`; + const userDmId = `@${userDm}:${federationConfig.rc1.domain}`; + const userDmName = `DM Federation User ${Date.now()}`; + + beforeAll(async () => { + // create both RC and Synapse users + rcUser = await createUser( + { + username: userDm, + password: 'random', + email: `${userDm}}@rocket.chat`, + name: userDmName, + }, + rc1AdminRequestConfig, + ); + + rcUserConfig = await getRequestConfig(federationConfig.rc1.url, rcUser.username, 'random'); + }); + + afterAll(async () => { + // delete both RC and Synapse users + await deleteUser(rcUser, {}, rc1AdminRequestConfig); + }); + it('should create a DM and invite user from rc', async () => { - hs1Room = await hs1AdminApp.createDM([userDmId]); + hs1Room = (await hs1AdminApp.createDM([userDmId])) as Room; expect(hs1Room).toHaveProperty('roomId'); - const subs = await getSubscriptions(rcUserConfig); + await retry('this is an async operation, so we need to wait for the room to be created in RC', async () => { + const roomsResponse = await rcUserConfig.request.get(api('rooms.get')).set(rcUserConfig.credentials).expect(200); - const pendingInvitation = subs.update.find( - (subscription) => - subscription.status === 'INVITED' && - subscription.fname?.includes(`@${federationConfig.hs1.adminUser}:${federationConfig.hs1.domain}`), - ); + expect(roomsResponse.body).toHaveProperty('success', true); + expect(roomsResponse.body).toHaveProperty('update'); - expect(pendingInvitation).toHaveProperty('rid'); + rcRoom = roomsResponse.body.update.find((room: IRoomNativeFederated) => room.federation.mrid === hs1Room.roomId); - const membersBefore = await hs1Room!.getMembers(); + expect(rcRoom).toHaveProperty('_id'); + expect(rcRoom).toHaveProperty('t', 'd'); + expect(rcRoom).toHaveProperty('uids'); + expect(rcRoom).not.toHaveProperty('fname'); + + subscriptionInvite = await getSubscriptionByRoomId(rcRoom._id, rcUserConfig.credentials, rcUserConfig.request); + + expect(subscriptionInvite).toHaveProperty('status', 'INVITED'); + expect(subscriptionInvite).toHaveProperty('fname', `@${federationConfig.hs1.adminUser}:${federationConfig.hs1.domain}`); + }); + }); + + it('should accept the DM invitation from rc', async () => { + const membersBefore = await hs1Room.getMembers(); expect(membersBefore.length).toBe(2); @@ -136,47 +155,151 @@ const waitForRoomEvent = async ( expect(invitedMember).toHaveProperty('membership', 'invite'); - invitedRoomId = pendingInvitation!.rid; - - const waitForRoomEventPromise = waitForRoomEvent(hs1Room!, RoomStateEvent.Members, ({ event }) => { + const waitForRoomEventPromise = waitForRoomEvent(hs1Room, RoomStateEvent.Members, ({ event }) => { expect(event).toHaveProperty('content.membership', 'join'); expect(event).toHaveProperty('state_key', userDmId); }); - const response = await acceptRoomInvite(invitedRoomId, rcUserConfig); + const response = await acceptRoomInvite(rcRoom._id, rcUserConfig); expect(response.success).toBe(true); await waitForRoomEventPromise; }); - it.todo('should display the fname properly'); - it.todo('should display the fname properly after the user from Synapse leaves the DM'); + + it('should display the fname properly', async () => { + const sub = await getSubscriptionByRoomId(rcRoom._id, rcUserConfig.credentials, rcUserConfig.request); + + expect(sub).toHaveProperty('fname', federationConfig.hs1.adminMatrixUserId); + }); + + it('should return own user name as the room name when user is alone in the DM', async () => { + await hs1AdminApp.matrixClient.leave(hs1Room.roomId); + + await retry( + 'this is an async operation, so we need to wait for the event to be processed', + async () => { + const sub = await getSubscriptionByRoomId(rcRoom._id, rcUserConfig.credentials, rcUserConfig.request); + + expect(sub).toHaveProperty('name', userDm); + expect(sub).toHaveProperty('fname', userDmName); + + const roomInfo = await getRoomInfo(rcRoom._id, rcUserConfig); + + expect(roomInfo).toHaveProperty('room'); + + expect(roomInfo.room).toHaveProperty('usersCount', 1); + expect(roomInfo.room).not.toHaveProperty('fname'); + expect(roomInfo.room).toHaveProperty('uids'); + expect(roomInfo.room?.uids).toHaveLength(1); + expect(roomInfo.room?.uids).toEqual([rcUser._id]); + + expect(roomInfo.room).toHaveProperty('usernames'); + expect(roomInfo.room?.usernames).toHaveLength(1); + expect(roomInfo.room?.usernames).toEqual([rcUser.username]); + }, + { delayMs: 100 }, + ); + }); }); describe('Permission validations', () => { - it('should leave the DM from Rocket.Chat', async () => { - const subs = await getSubscriptions(rcUserConfig); - - const dmSubscription = subs.update.find( - (subscription) => - subscription.t === 'd' && subscription.fname?.includes(`@${federationConfig.hs1.adminUser}:${federationConfig.hs1.domain}`), + let rcUser: TestUser; + let rcUserConfig: IRequestConfig; + let hs1Room: Room; + let subscriptionInvite: ISubscription; + let rcRoom: IRoom; + + const userDm = `dm-federation-user-${Date.now()}`; + const userDmId = `@${userDm}:${federationConfig.rc1.domain}`; + + beforeAll(async () => { + // create both RC and Synapse users + rcUser = await createUser( + { + username: userDm, + password: 'random', + email: `${userDm}}@rocket.chat`, + name: `DM Federation User ${Date.now()}`, + }, + rc1AdminRequestConfig, ); - expect(dmSubscription).toHaveProperty('rid'); + rcUserConfig = await getRequestConfig(federationConfig.rc1.url, rcUser.username, 'random'); + }); + + afterAll(async () => { + // delete both RC and Synapse users + await deleteUser(rcUser, {}, rc1AdminRequestConfig); + }); + + it('should create a DM and invite user from rc', async () => { + hs1Room = (await hs1AdminApp.createDM([userDmId])) as Room; + + expect(hs1Room).toHaveProperty('roomId'); + + await retry('this is an async operation, so we need to wait for the room to be created in RC', async () => { + const roomsResponse = await rcUserConfig.request.get(api('rooms.get')).set(rcUserConfig.credentials).expect(200); + + expect(roomsResponse.body).toHaveProperty('success', true); + expect(roomsResponse.body).toHaveProperty('update'); + + rcRoom = roomsResponse.body.update.find((room: IRoomNativeFederated) => room.federation.mrid === hs1Room.roomId); + + expect(rcRoom).toHaveProperty('_id'); + expect(rcRoom).toHaveProperty('t', 'd'); + expect(rcRoom).toHaveProperty('uids'); + expect(rcRoom).not.toHaveProperty('fname'); + + subscriptionInvite = await getSubscriptionByRoomId(rcRoom._id, rcUserConfig.credentials, rcUserConfig.request); + + expect(subscriptionInvite).toHaveProperty('status', 'INVITED'); + expect(subscriptionInvite).toHaveProperty('fname', `@${federationConfig.hs1.adminUser}:${federationConfig.hs1.domain}`); + }); + }); + + it('should accept the DM invitation from rc', async () => { + const membersBefore = await hs1Room.getMembers(); + + expect(membersBefore.length).toBe(2); + + const invitedMember = membersBefore.find((member) => member.userId === userDmId); + + expect(invitedMember).toHaveProperty('membership', 'invite'); + + const waitForRoomEventPromise = waitForRoomEvent(hs1Room, RoomStateEvent.Members, ({ event }) => { + expect(event).toHaveProperty('content.membership', 'join'); + expect(event).toHaveProperty('state_key', userDmId); + }); + + const response = await acceptRoomInvite(rcRoom._id, rcUserConfig); + expect(response.success).toBe(true); + + await waitForRoomEventPromise; + }); + + it('should display the fname properly', async () => { + const sub = await getSubscriptionByRoomId(rcRoom._id, rcUserConfig.credentials, rcUserConfig.request); + + expect(sub).toHaveProperty('fname', federationConfig.hs1.adminMatrixUserId); + }); + + it('should leave the DM from Rocket.Chat', async () => { + const leaveEventPromise = waitForRoomEvent(hs1Room, RoomStateEvent.Members, ({ event }) => { + expect(event).toHaveProperty('content.membership', 'leave'); + expect(event).toHaveProperty('state_key', userDmId); + }); const response = await rcUserConfig.request .post(api('rooms.leave')) .set(rcUserConfig.credentials) .send({ - roomId: invitedRoomId, + roomId: subscriptionInvite.rid, }) .expect(200); expect(response.body).toHaveProperty('success', true); - await waitForRoomEvent(hs1Room!, RoomStateEvent.Members, ({ event }) => { - expect(event).toHaveProperty('content.membership', 'leave'); - expect(event).toHaveProperty('state_key', userDmId); - }); + await leaveEventPromise; }); }); diff --git a/packages/core-services/src/types/IRoomService.ts b/packages/core-services/src/types/IRoomService.ts index 9e74a6d022599..d38b400a4b3b7 100644 --- a/packages/core-services/src/types/IRoomService.ts +++ b/packages/core-services/src/types/IRoomService.ts @@ -67,4 +67,5 @@ export interface IRoomService { skipSystemMessage?: boolean; status?: 'INVITED'; }): Promise; + updateDirectMessageRoomName(room: IRoom): Promise; } diff --git a/packages/model-typings/src/models/IRoomsModel.ts b/packages/model-typings/src/models/IRoomsModel.ts index ace72ff4d006d..e0d2f411c0847 100644 --- a/packages/model-typings/src/models/IRoomsModel.ts +++ b/packages/model-typings/src/models/IRoomsModel.ts @@ -329,4 +329,5 @@ export interface IRoomsModel extends IBaseModel { insertAbacAttributeIfNotExistsById(rid: IRoom['_id'], key: string, values: string[]): Promise; updateAbacAttributeValuesArrayFilteredById(rid: IRoom['_id'], key: string, values: string[]): Promise; removeAbacAttributeByRoomIdAndKey(rid: IRoom['_id'], key: string): Promise; + removeUserReferenceFromDMsById(roomId: string, username: string, userId: string): Promise; } diff --git a/packages/models/src/models/Rooms.ts b/packages/models/src/models/Rooms.ts index d0bc2cc13f3f2..34a7d6f7edc53 100644 --- a/packages/models/src/models/Rooms.ts +++ b/packages/models/src/models/Rooms.ts @@ -2345,4 +2345,20 @@ export class RoomsRaw extends BaseRaw implements IRoomsModel { countAbacEnabled(): Promise { return this.countDocuments({ abacAttributes: { $exists: true }, archived: { $ne: true } }); } + + removeUserReferenceFromDMsById(roomId: string, username: string, userId: string): Promise { + const query: Filter = { + _id: roomId, + t: 'd', + }; + + const update: UpdateFilter = { + $pull: { + usernames: username, + uids: userId, + }, + }; + + return this.updateOne(query, update); + } }