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
2 changes: 0 additions & 2 deletions ee/packages/federation-matrix/src/events/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import type { Emitter } from '@rocket.chat/emitter';
import type { HomeserverEventSignatures } from '@rocket.chat/federation-sdk';

import { edus } from './edu';
import { invite } from './invite';
import { member } from './member';
import { message } from './message';
import { ping } from './ping';
Expand All @@ -16,7 +15,6 @@ export function registerEvents(
) {
ping(emitter);
message(emitter, serverName);
invite(emitter);
reaction(emitter);
member(emitter);
edus(emitter, eduProcessTypes);
Expand Down
52 changes: 0 additions & 52 deletions ee/packages/federation-matrix/src/events/invite.ts

This file was deleted.

113 changes: 83 additions & 30 deletions ee/packages/federation-matrix/src/events/member.ts
Original file line number Diff line number Diff line change
@@ -1,49 +1,102 @@
import { Room } from '@rocket.chat/core-services';
import { UserStatus } from '@rocket.chat/core-typings';
import type { Emitter } from '@rocket.chat/emitter';
import type { HomeserverEventSignatures } from '@rocket.chat/federation-sdk';
import { Logger } from '@rocket.chat/logger';
import { Rooms, Users } from '@rocket.chat/models';

const logger = new Logger('federation-matrix:member');

async function membershipLeaveAction(data: HomeserverEventSignatures['homeserver.matrix.membership']) {
const room = await Rooms.findOne({ 'federation.mrid': data.room_id }, { projection: { _id: 1 } });
if (!room) {
logger.warn(`No bridged room found for Matrix room_id: ${data.room_id}`);
return;
}

// state_key is the user affected by the membership change
const affectedUser = await Users.findOne({ 'federation.mui': data.state_key });
if (!affectedUser) {
logger.error(`No Rocket.Chat user found for bridged user: ${data.state_key}`);
return;
}

// Check if this is a kick (sender != state_key) or voluntary leave (sender == state_key)
if (data.sender === data.state_key) {
// Voluntary leave
await Room.removeUserFromRoom(room._id, affectedUser);
logger.info(`User ${affectedUser.username} left room ${room._id} via Matrix federation`);
} else {
// Kick - find who kicked
const kickerUser = await Users.findOne({ 'federation.mui': data.sender });

await Room.removeUserFromRoom(room._id, affectedUser, {
byUser: kickerUser || { _id: 'matrix.federation', username: 'Matrix User' },
});

const reasonText = data.content.reason ? ` Reason: ${data.content.reason}` : '';
logger.info(`User ${affectedUser.username} was kicked from room ${room._id} by ${data.sender} via Matrix federation.${reasonText}`);
}
}

async function membershipJoinAction(data: HomeserverEventSignatures['homeserver.matrix.membership']) {
const room = await Rooms.findOne({ 'federation.mrid': data.room_id });
if (!room) {
logger.warn(`No bridged room found for room_id: ${data.room_id}`);
return;
}

const internalUsername = data.sender;
const localUser = await Users.findOneByUsername(internalUsername);
if (localUser) {
await Room.addUserToRoom(room._id, localUser);
return;
}

const [, serverName] = data.sender.split(':');
if (!serverName) {
throw new Error('Invalid sender format, missing server name');
}
Comment on lines +56 to +59
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Handle Matrix server name with ports correctly

Splitting on : drops everything after the first separator, so IDs like @alice:server.example:8448 lose the port and we persist the wrong origin. That breaks reconciliations and future joins from homeservers that expose non-default ports or IPv6 literals. Please slice everything after the first colon instead of relying on array destructuring.

-	const [, serverName] = data.sender.split(':');
-	if (!serverName) {
-		throw new Error('Invalid sender format, missing server name');
-	}
+	const separatorIndex = data.sender.indexOf(':');
+	if (separatorIndex === -1 || separatorIndex === data.sender.length - 1) {
+		throw new Error('Invalid sender format, missing server name');
+	}
+	const serverName = data.sender.slice(separatorIndex + 1);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const [, serverName] = data.sender.split(':');
if (!serverName) {
throw new Error('Invalid sender format, missing server name');
}
const separatorIndex = data.sender.indexOf(':');
if (separatorIndex === -1 || separatorIndex === data.sender.length - 1) {
throw new Error('Invalid sender format, missing server name');
}
const serverName = data.sender.slice(separatorIndex + 1);


const { insertedId } = await Users.insertOne({
username: internalUsername,
type: 'user',
status: UserStatus.OFFLINE,
active: true,
roles: ['user'],
name: data.content.displayname || internalUsername,
requirePasswordChange: false,
createdAt: new Date(),
_updatedAt: new Date(),
federated: true,
federation: {
version: 1,
mui: data.sender,
origin: serverName,
},
});

const user = await Users.findOneById(insertedId);
if (!user) {
console.warn(`User with ID ${insertedId} not found after insertion`);
return;
}
await Room.addUserToRoom(room._id, user);
}

export function member(emitter: Emitter<HomeserverEventSignatures>) {
emitter.on('homeserver.matrix.membership', async (data) => {
try {
// Only handle leave events (including kicks)
if (data.content.membership !== 'leave') {
logger.debug(`Ignoring membership event with membership: ${data.content.membership}`);
return;
if (data.content.membership === 'leave') {
return membershipLeaveAction(data);
}

const room = await Rooms.findOne({ 'federation.mrid': data.room_id }, { projection: { _id: 1 } });
if (!room) {
logger.warn(`No bridged room found for Matrix room_id: ${data.room_id}`);
return;
if (data.content.membership === 'join') {
return membershipJoinAction(data);
}
Comment on lines +90 to 96
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Await membership handlers so errors surface

Returning the promise without awaiting bypasses this try/catch, so any rejection from the join/leave handlers bubbles out as an unhandled rejection with no log. Await the calls and return afterward so failures are captured and logged.

-			if (data.content.membership === 'leave') {
-				return membershipLeaveAction(data);
-			}
+			if (data.content.membership === 'leave') {
+				await membershipLeaveAction(data);
+				return;
+			}-			if (data.content.membership === 'join') {
-				return membershipJoinAction(data);
-			}
+			if (data.content.membership === 'join') {
+				await membershipJoinAction(data);
+				return;
+			}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (data.content.membership === 'leave') {
return membershipLeaveAction(data);
}
const room = await Rooms.findOne({ 'federation.mrid': data.room_id }, { projection: { _id: 1 } });
if (!room) {
logger.warn(`No bridged room found for Matrix room_id: ${data.room_id}`);
return;
if (data.content.membership === 'join') {
return membershipJoinAction(data);
}
if (data.content.membership === 'leave') {
await membershipLeaveAction(data);
return;
}
if (data.content.membership === 'join') {
await membershipJoinAction(data);
return;
}
🤖 Prompt for AI Agents
In ee/packages/federation-matrix/src/events/member.ts around lines 90 to 96, the
code returns the promise from membershipLeaveAction/membershipJoinAction without
awaiting, which bypasses the surrounding try/catch; change those lines to await
the call (await membershipLeaveAction(data); return; and await
membershipJoinAction(data); return;) so any thrown errors are caught and logged
by the existing try/catch (ensure the enclosing function is async if not
already).


// state_key is the user affected by the membership change
const affectedUser = await Users.findOne({ 'federation.mui': data.state_key });
if (!affectedUser) {
logger.error(`No Rocket.Chat user found for bridged user: ${data.state_key}`);
return;
}
logger.debug(`Ignoring membership event with membership: ${data.content.membership}`);

// Check if this is a kick (sender != state_key) or voluntary leave (sender == state_key)
if (data.sender === data.state_key) {
// Voluntary leave
await Room.removeUserFromRoom(room._id, affectedUser);
logger.info(`User ${affectedUser.username} left room ${room._id} via Matrix federation`);
} else {
// Kick - find who kicked
const kickerUser = await Users.findOne({ 'federation.mui': data.sender });

await Room.removeUserFromRoom(room._id, affectedUser, {
byUser: kickerUser || { _id: 'matrix.federation', username: 'Matrix User' },
});

const reasonText = data.content.reason ? ` Reason: ${data.content.reason}` : '';
logger.info(`User ${affectedUser.username} was kicked from room ${room._id} by ${data.sender} via Matrix federation.${reasonText}`);
}
} catch (error) {
logger.error('Failed to process Matrix membership event:', error);
}
Expand Down
Loading