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
4 changes: 2 additions & 2 deletions apps/meteor/server/services/room/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import { removeRoomOwner } from '../../methods/removeRoomOwner';
export class RoomService extends ServiceClassInternal implements IRoomService {
protected name = 'room';

async updateDirectMessageRoomName(room: IRoom): Promise<boolean> {
async updateDirectMessageRoomName(room: IRoom, ignoreStatusFromSubs?: string[]): Promise<boolean> {
const subs = await Subscriptions.findByRoomId(room._id, { projection: { u: 1, status: 1 } }).toArray();

const uids = subs.map((sub) => sub.u._id);
Expand All @@ -47,7 +47,7 @@ export class RoomService extends ServiceClassInternal implements IRoomService {

for await (const sub of subs) {
// don't update the name if the user is invited but hasn't accepted yet
if (sub.status === 'INVITED') {
if (!ignoreStatusFromSubs?.includes(sub._id) && sub.status === 'INVITED') {
continue;
}
await Subscriptions.updateOne({ _id: sub._id }, { $set: roomNames[sub.u._id] });
Expand Down
8 changes: 1 addition & 7 deletions ee/packages/federation-matrix/src/FederationMatrix.ts
Original file line number Diff line number Diff line change
Expand Up @@ -291,13 +291,9 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS
continue;
}

if (!isUserNativeFederated(member)) {
continue;
}

try {
await federationSDK.inviteUserToRoom(
userIdSchema.parse(member.username),
isUserNativeFederated(member) ? userIdSchema.parse(member.username) : `@${member.username}:${this.serverName}`,
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Dec 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Inconsistent user ID validation: constructed user ID for non-native federated users should be validated with userIdSchema.parse() for consistency with inviteUsersToRoom (line 553) and to ensure proper type safety.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At ee/packages/federation-matrix/src/FederationMatrix.ts, line 296:

<comment>Inconsistent user ID validation: constructed user ID for non-native federated users should be validated with `userIdSchema.parse()` for consistency with `inviteUsersToRoom` (line 553) and to ensure proper type safety.</comment>

<file context>
@@ -291,13 +291,9 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS
 					try {
 						await federationSDK.inviteUserToRoom(
-							userIdSchema.parse(member.username),
+							isUserNativeFederated(member) ? userIdSchema.parse(member.username) : `@${member.username}:${this.serverName}`,
 							roomIdSchema.parse(matrixRoomResult.room_id),
 							userIdSchema.parse(actualMatrixUserId),
</file context>
Suggested change
isUserNativeFederated(member) ? userIdSchema.parse(member.username) : `@${member.username}:${this.serverName}`,
userIdSchema.parse(isUserNativeFederated(member) ? member.username : `@${member.username}:${this.serverName}`),
Fix with Cubic

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Dec 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've fixed the inconsistent user ID validation at line 296 in ee/packages/federation-matrix/src/FederationMatrix.ts.

The issue: When inviting non-native federated users to a group DM room, the constructed user ID (@${member.username}:${this.serverName}) was passed directly to federationSDK.inviteUserToRoom() without validation through userIdSchema.parse().

The fix: Now both branches of the conditional are validated through userIdSchema.parse():

userIdSchema.parse(isUserNativeFederated(member) ? member.username : `@${member.username}:${this.serverName}`)

This makes the code consistent with the pattern used in inviteUsersToRoom (line 553) and ensures proper type safety for the UserID branded type.

PR: #37937

roomIdSchema.parse(matrixRoomResult.room_id),
userIdSchema.parse(actualMatrixUserId),
);
Expand Down Expand Up @@ -919,8 +915,6 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS

if (action === 'accept') {
await federationSDK.acceptInvite(room.federation.mrid, matrixUserId);

await Room.performAcceptRoomInvite(room, subscription, user);
}

if (action === 'reject') {
Expand Down
2 changes: 1 addition & 1 deletion ee/packages/federation-matrix/src/events/member.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ async function handleJoin({

// update room name for DMs
if (room.t === 'd') {
await Room.updateDirectMessageRoomName(room);
await Room.updateDirectMessageRoomName(room, [subscription._id]);
}

if (!subscription.status) {
Expand Down
80 changes: 43 additions & 37 deletions ee/packages/federation-matrix/tests/end-to-end/dms.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -413,7 +413,7 @@
const room = await hs1User.matrixClient.getRoom(rcRoom.federation.mrid);

expect(room).toBeDefined();
expect(room!.getMyMembership()).toBe('invite');

Check warning on line 416 in ee/packages/federation-matrix/tests/end-to-end/dms.spec.ts

View workflow job for this annotation

GitHub Actions / 🔎 Code Check / Code Lint

Forbidden non-null assertion
});

// Accept the invitation
Expand Down Expand Up @@ -1097,7 +1097,7 @@
});
});

it.failing('should accept the invitation by the Rocket.Chat user', async () => {
it('should accept the invitation by the Rocket.Chat user', async () => {
const response = await acceptRoomInvite(rcRoom._id, rcUserConfig2);
expect(response.success).toBe(true);
});
Expand Down Expand Up @@ -1204,22 +1204,7 @@
});

beforeAll(async () => {
// Create non-federated DM between rcUser1 and rcUser2
const nonFedDmResponse = await rcUser1.config.request
.post(api('dm.create'))
.set(rcUser1.config.credentials)
.send({
username: rcUser2.username,
})
.expect(200);

expect(nonFedDmResponse.body).toHaveProperty('success', true);

const roomInfoNonFed = await getRoomInfo(nonFedDmResponse.body.room._id, rcUser1.config);

rcRoomNonFed = roomInfoNonFed.room as IRoomNativeFederated;

// Create federated DM between rcUser1 and rcUser2 and the Synapse user
// First create a federated DM between rcUser1 and rcUser2 and the Synapse user, so this is the oldest room
const fedDmResponse = await rcUser1.config.request
.post(api('dm.create'))
.set(rcUser1.config.credentials)
Expand Down Expand Up @@ -1260,6 +1245,21 @@
// After acceptance, should display the Synapse user's ID
expect(sub).toHaveProperty('fname', `${federationConfig.hs1.adminMatrixUserId}, ${rcUser1.fullName}`);
});

// Then create non-federated DM between rcUser1 and rcUser2 which should be returned on duplication
const nonFedDmResponse = await rcUser1.config.request
.post(api('dm.create'))
.set(rcUser1.config.credentials)
.send({
username: rcUser2.username,
})
.expect(200);

expect(nonFedDmResponse.body).toHaveProperty('success', true);

const roomInfoNonFed = await getRoomInfo(nonFedDmResponse.body.room._id, rcUser1.config);

rcRoomNonFed = roomInfoNonFed.room as IRoomNativeFederated;
});

it('should have two DMs with same users', async () => {
Expand Down Expand Up @@ -1322,9 +1322,6 @@

expect(response.body).toHaveProperty('success', true);

// expect(response.body).toHaveProperty('room._id', rcRoom._id);

// TODO this is currently wrong, the correct value should be rcRoom._id
expect(response.body).toHaveProperty('room._id', rcRoomNonFed._id);
});
});
Expand Down Expand Up @@ -1435,7 +1432,7 @@
});
});

it.failing('should create a federated DM between rcUser1 and rcUser2 and the Synapse user', async () => {
it('should create a federated DM between rcUser1 and rcUser2 and the Synapse user', async () => {
const fedDmResponse = await rcUser1.config.request
.post(api('dm.create'))
.set(rcUser1.config.credentials)
Expand Down Expand Up @@ -1486,13 +1483,21 @@
{ delayMs: 100 },
);

// TODO accept not working
const response = await acceptRoomInvite(rcRoom._id, rcUser2.config);
expect(response.success).toBe(true);

await retry(
'wait for the join to be processed',
async () => {
const sub = await getSubscriptionByRoomId(rcRoom._id, rcUser2.config.credentials, rcUser2.config.request);

expect(sub).not.toHaveProperty('status');
},
{ delayMs: 100 },
);
});

// TODO this is failing because we cannot accept the invite properly on the line above
it.failing('should have two DMs with same users', async () => {
it('should have a single DM with same users before one leaves', async () => {
const roomsResponse = await rcUser1.config.request.get(api('rooms.get')).set(rcUser1.config.credentials).expect(200);

const dmRooms = roomsResponse.body.update.filter(
Expand All @@ -1506,7 +1511,9 @@

// at this time there should be only one DM with only two users (the non-federated one)
expect(dmRooms.length).toBe(1);
});

it('should leave the federated DM with three users', async () => {
// now the rc user leaves the federated DM
const response = await rcUser2.config.request
.post(api('rooms.leave'))
Expand All @@ -1518,19 +1525,21 @@

expect(response.body).toHaveProperty('success', true);

await retry(
'wait for the leave to be processed',
async () => {
expect(hs1Room1.getMyMembership()).toBe('leave');
// Verify room is no longer accessible to RC users
await retry('waiting for room cleanup', async () => {
const roomsResponse = await rcUser2.config.request.get(api('rooms.get')).set(rcUser2.config.credentials).expect(200);

const sub = await getSubscriptionByRoomId(rcRoom._id, rcUser1.config.credentials, rcUser1.config.request);
expect(roomsResponse.body).toHaveProperty('update');
expect(roomsResponse.body.update).toBeInstanceOf(Array);

// After leave, should display only the RC user's full name
expect(sub).toHaveProperty('fname', rcUser2.fullName);
},
{ delayMs: 100 },
);
const room = roomsResponse.body.update?.find((r: IRoom) => r._id === rcRoom._id);

// Room should not be in active rooms list
expect(room).toBeUndefined();
});
});

it('should have two DMs with same users', async () => {
// now there should be two DMs with the same users
const roomsResponseAfterLeave = await rcUser1.config.request
.get(api('rooms.get'))
Expand Down Expand Up @@ -1560,9 +1569,6 @@

expect(response.body).toHaveProperty('success', true);

// expect(response.body).toHaveProperty('room._id', rcRoom._id);

// TODO this is currently wrong, the correct value should be rcRoom._id
expect(response.body).toHaveProperty('room._id', rcRoom1on1._id);
});
});
Expand Down
22 changes: 15 additions & 7 deletions ee/packages/federation-matrix/tests/end-to-end/room.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
} from '../../../../../apps/meteor/tests/data/rooms.helper';
import { type IRequestConfig, getRequestConfig, createUser, deleteUser } 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 { createDDPListener } from '../helper/ddp-listener';
import { SynapseClient } from '../helper/synapse-client';
Expand Down Expand Up @@ -1612,13 +1613,20 @@ import { SynapseClient } from '../helper/synapse-client';

describe('It should reflect all the members and messagens on the rocket.chat side', () => {
it('It should show all the three users in the members list', async () => {
const members = await getRoomMembers(rid, rc1AdminRequestConfig);
expect(members.members.length).toBe(3);
expect(members.members.find((member: IUser) => member.username === federationConfig.rc1.adminUser)).not.toBeNull();
expect(
members.members.find((member: IUser) => member.username === federationConfig.rc1.additionalUser1.username),
).not.toBeNull();
expect(members.members.find((member: IUser) => member.username === federationConfig.hs1.adminMatrixUserId)).not.toBeNull();
await retry(
'Getting room members until all are present',
async () => {
const members = await getRoomMembers(rid, rc1AdminRequestConfig);

expect(members.members.length).toBe(3);
expect(members.members.find((member: IUser) => member.username === federationConfig.rc1.adminUser)).not.toBeNull();
expect(
members.members.find((member: IUser) => member.username === federationConfig.rc1.additionalUser1.username),
).not.toBeNull();
expect(members.members.find((member: IUser) => member.username === federationConfig.hs1.adminMatrixUserId)).not.toBeNull();
},
{ delayMs: 200 },
);
});
});
});
Expand Down
2 changes: 1 addition & 1 deletion packages/core-services/src/types/IRoomService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,5 +69,5 @@ export interface IRoomService {
status?: 'INVITED';
roles?: ISubscription['roles'];
}): Promise<string | undefined>;
updateDirectMessageRoomName(room: IRoom): Promise<boolean>;
updateDirectMessageRoomName(room: IRoom, ignoreStatusFromSubs?: string[]): Promise<boolean>;
}
8 changes: 7 additions & 1 deletion packages/models/src/models/Rooms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -897,7 +897,13 @@ export class RoomsRaw extends BaseRaw<IRoom> implements IRoomsModel {
uids: { $size: uid.length, $all: uid },
};

return this.findOne<IRoom>(query, options);
return this.findOne<IRoom>(query, {
...options,
sort: {
federated: 1,
ts: 1,
},
});
}

findFederatedRooms(options: FindOptions<IRoom> = {}): FindCursor<IRoomFederated> {
Expand Down
Loading