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
6 changes: 6 additions & 0 deletions .changeset/strong-bags-train.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@rocket.chat/meteor": patch
"@rocket.chat/federation-matrix": patch
---

Fixes an issue where membership updates were not reflected when the user was the first member on their own server.
24 changes: 24 additions & 0 deletions apps/meteor/tests/data/rooms.helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,30 @@ export const acceptRoomInvite = (roomId: IRoom['_id'], config?: IRequestConfig)
});
};

/**
* Retrieves the subscriptions for the authenticated user.
*
* Fetches the complete list of subscriptions for the authenticated user, which is essential
* for verifying federation subscription synchronization and member synchronization.
*
* @param config - Optional request configuration for custom domains
* @returns Promise resolving to the subscriptions response
*/

export const getSubscriptions = (config?: IRequestConfig) => {
const requestInstance = config?.request || request;
const credentialsInstance = config?.credentials || credentials;

return new Promise<ReturnType<Endpoints['/v1/subscriptions.get']['GET']>>((resolve) => {
void requestInstance
.get(api('subscriptions.get'))
.set(credentialsInstance)
.end((_err: any, req: any) => {
resolve(req.body);
});
});
};

/**
* Rejects a room invite for the authenticated user.
*
Expand Down
9 changes: 4 additions & 5 deletions ee/packages/federation-matrix/src/events/edu.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import { api } from '@rocket.chat/core-services';
import { UserStatus } from '@rocket.chat/core-typings';
import type { Emitter } from '@rocket.chat/emitter';
import { federationSDK, type HomeserverEventSignatures } from '@rocket.chat/federation-sdk';
import { federationSDK } from '@rocket.chat/federation-sdk';
import { Logger } from '@rocket.chat/logger';
import { Rooms, Users } from '@rocket.chat/models';

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

export const edus = async (emitter: Emitter<HomeserverEventSignatures>) => {
emitter.on('homeserver.matrix.typing', async (data) => {
export const edus = async () => {
federationSDK.eventEmitterService.on('homeserver.matrix.typing', async (data) => {
const config = federationSDK.getConfig('edu');
if (!config.processTyping) {
return;
Expand All @@ -31,7 +30,7 @@ export const edus = async (emitter: Emitter<HomeserverEventSignatures>) => {
}
});

emitter.on('homeserver.matrix.presence', async (data) => {
federationSDK.eventEmitterService.on('homeserver.matrix.presence', async (data) => {
const config = federationSDK.getConfig('edu');
if (!config.processPresence) {
return;
Expand Down
17 changes: 7 additions & 10 deletions ee/packages/federation-matrix/src/events/index.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,15 @@
import type { Emitter } from '@rocket.chat/emitter';
import { type HomeserverEventSignatures } from '@rocket.chat/federation-sdk';

import { edus } from './edu';
import { member } from './member';
import { message } from './message';
import { ping } from './ping';
import { reaction } from './reaction';
import { room } from './room';

export function registerEvents(emitter: Emitter<HomeserverEventSignatures>) {
ping(emitter);
message(emitter);
reaction(emitter);
member(emitter);
edus(emitter);
room(emitter);
export function registerEvents() {
ping();
message();
reaction();
member();
edus();
room();
}
8 changes: 3 additions & 5 deletions ee/packages/federation-matrix/src/events/member.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { Room } from '@rocket.chat/core-services';
import type { IRoomNativeFederated, IRoom, IUser, RoomType } from '@rocket.chat/core-typings';
import type { Emitter } from '@rocket.chat/emitter';
import type { HomeserverEventSignatures, PduForType } from '@rocket.chat/federation-sdk';
import { federationSDK } from '@rocket.chat/federation-sdk';
import { federationSDK, type HomeserverEventSignatures, type PduForType } from '@rocket.chat/federation-sdk';
import { Logger } from '@rocket.chat/logger';
import { Rooms, Subscriptions, Users } from '@rocket.chat/models';

Expand Down Expand Up @@ -237,8 +235,8 @@ async function handleLeave({
// TODO check if there are no pending invites to the room, and if so, delete the room
}

export function member(emitter: Emitter<HomeserverEventSignatures>) {
emitter.on('homeserver.matrix.membership', async ({ event }) => {
export function member() {
federationSDK.eventEmitterService.on('homeserver.matrix.membership', async ({ event }) => {
try {
switch (event.content.membership) {
case 'invite':
Expand Down
19 changes: 6 additions & 13 deletions ee/packages/federation-matrix/src/events/message.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,6 @@
import { FederationMatrix, Message, MeteorService } from '@rocket.chat/core-services';
import type { IUser, IRoom, FileAttachmentProps } from '@rocket.chat/core-typings';
import type { Emitter } from '@rocket.chat/emitter';
import {
type FileMessageType,
type MessageType,
type FileMessageContent,
type HomeserverEventSignatures,
type EventID,
federationSDK,
} from '@rocket.chat/federation-sdk';
import { type FileMessageType, type MessageType, type FileMessageContent, type EventID, federationSDK } from '@rocket.chat/federation-sdk';
import { Logger } from '@rocket.chat/logger';
import { Users, Rooms, Messages } from '@rocket.chat/models';

Expand Down Expand Up @@ -118,8 +110,8 @@ async function handleMediaMessage(
};
}

export function message(emitter: Emitter<HomeserverEventSignatures>) {
emitter.on('homeserver.matrix.message', async ({ event, event_id: eventId }) => {
export function message() {
federationSDK.eventEmitterService.on('homeserver.matrix.message', async ({ event, event_id: eventId }) => {
try {
const { msgtype, body } = event.content;
const messageBody = body.toString();
Expand Down Expand Up @@ -258,6 +250,7 @@ export function message(emitter: Emitter<HomeserverEventSignatures>) {
homeServerDomain: serverName,
senderExternalId: event.sender,
});

await Message.saveMessageFromFederation({
fromId: user._id,
rid: room._id,
Expand All @@ -271,7 +264,7 @@ export function message(emitter: Emitter<HomeserverEventSignatures>) {
}
});

emitter.on('homeserver.matrix.encrypted', async ({ event, event_id: eventId }) => {
federationSDK.eventEmitterService.on('homeserver.matrix.encrypted', async ({ event, event_id: eventId }) => {
try {
if (!event.content.ciphertext) {
logger.debug('No message content found in event');
Expand Down Expand Up @@ -385,7 +378,7 @@ export function message(emitter: Emitter<HomeserverEventSignatures>) {
}
});

emitter.on('homeserver.matrix.redaction', async ({ event }) => {
federationSDK.eventEmitterService.on('homeserver.matrix.redaction', async ({ event }) => {
try {
const redactedEventId = event.redacts;
if (!redactedEventId) {
Expand Down
7 changes: 3 additions & 4 deletions ee/packages/federation-matrix/src/events/ping.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import type { Emitter } from '@rocket.chat/emitter';
import type { HomeserverEventSignatures } from '@rocket.chat/federation-sdk';
import { federationSDK } from '@rocket.chat/federation-sdk';

export const ping = async (emitter: Emitter<HomeserverEventSignatures>) => {
emitter.on('homeserver.ping', async (data) => {
export const ping = async () => {
federationSDK.eventEmitterService.on('homeserver.ping', async (data) => {
console.log('Message received from homeserver', data);
});
};
9 changes: 4 additions & 5 deletions ee/packages/federation-matrix/src/events/reaction.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import { Message, FederationMatrix } from '@rocket.chat/core-services';
import type { Emitter } from '@rocket.chat/emitter';
import type { HomeserverEventSignatures } from '@rocket.chat/federation-sdk';
import { federationSDK } from '@rocket.chat/federation-sdk';
import { Logger } from '@rocket.chat/logger';
import { Users, Messages } from '@rocket.chat/models'; // Rooms
import emojione from 'emojione';

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

export function reaction(emitter: Emitter<HomeserverEventSignatures>) {
emitter.on('homeserver.matrix.reaction', async ({ event, event_id: eventId }) => {
export function reaction() {
federationSDK.eventEmitterService.on('homeserver.matrix.reaction', async ({ event, event_id: eventId }) => {
try {
const isSetReaction = event.content?.['m.relates_to'];

Expand Down Expand Up @@ -47,7 +46,7 @@ export function reaction(emitter: Emitter<HomeserverEventSignatures>) {
}
});

emitter.on('homeserver.matrix.redaction', async ({ event }) => {
federationSDK.eventEmitterService.on('homeserver.matrix.redaction', async ({ event }) => {
try {
const redactedEventId = event.redacts;
if (!redactedEventId) {
Expand Down
15 changes: 7 additions & 8 deletions ee/packages/federation-matrix/src/events/room.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { Room } from '@rocket.chat/core-services';
import type { Emitter } from '@rocket.chat/emitter';
import { federationSDK, type HomeserverEventSignatures } from '@rocket.chat/federation-sdk';
import { federationSDK } from '@rocket.chat/federation-sdk';
import { Rooms, Users } from '@rocket.chat/models';

import { getUsernameServername } from '../FederationMatrix';

export function room(emitter: Emitter<HomeserverEventSignatures>) {
emitter.on('homeserver.matrix.room.name', async ({ event }) => {
export function room() {
federationSDK.eventEmitterService.on('homeserver.matrix.room.name', async ({ event }) => {
const {
room_id: roomId,
content: { name },
Expand All @@ -15,18 +14,18 @@ export function room(emitter: Emitter<HomeserverEventSignatures>) {

const localRoomId = await Rooms.findOne({ 'federation.mrid': roomId }, { projection: { _id: 1 } });
if (!localRoomId) {
throw new Error('mapped room not found');
throw new Error(`mapped room not found: ${roomId}`);
}

const localUserId = await Users.findOneByUsername(userId, { projection: { _id: 1 } });
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

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

The sender field in Matrix events contains the full Matrix user ID (format: @username:server.domain), but Users.findOneByUsername() expects only the username part. This will cause the user lookup to fail. Use the getUsernameServername utility function to extract the username, similar to how it's done in the room.role event handler (line 63).

Copilot uses AI. Check for mistakes.
if (!localUserId) {
throw new Error('mapped user not found');
throw new Error(`mapped user not found: ${userId}`);
}

await Room.saveRoomName(localRoomId._id, localUserId._id, name);
});

emitter.on('homeserver.matrix.room.topic', async ({ event }) => {
federationSDK.eventEmitterService.on('homeserver.matrix.room.topic', async ({ event }) => {
const {
room_id: roomId,
content: { topic },
Expand All @@ -51,7 +50,7 @@ export function room(emitter: Emitter<HomeserverEventSignatures>) {
});
});

emitter.on('homeserver.matrix.room.role', async (data) => {
federationSDK.eventEmitterService.on('homeserver.matrix.room.role', async (data) => {
const { room_id: roomId, user_id: userId, sender_id: senderId, role } = data;

const localRoomId = await Rooms.findOne({ 'federation.mrid': roomId }, { projection: { _id: 1 } });
Expand Down
7 changes: 1 addition & 6 deletions ee/packages/federation-matrix/src/setup.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { Emitter } from '@rocket.chat/emitter';
import type { HomeserverEventSignatures } from '@rocket.chat/federation-sdk';
import { federationSDK, init } from '@rocket.chat/federation-sdk';
import { Logger } from '@rocket.chat/logger';

Expand Down Expand Up @@ -101,15 +99,12 @@ export function configureFederationMatrixSettings(settings: {
}

export async function setupFederationMatrix() {
const eventHandler = new Emitter<HomeserverEventSignatures>();

await init({
emitter: eventHandler,
dbConfig: {
uri: process.env.MONGO_URL || 'mongodb://localhost:3001/meteor',
poolSize: Number.parseInt(process.env.DATABASE_POOL_SIZE || '10', 10),
},
});

registerEvents(eventHandler);
registerEvents();
}
50 changes: 48 additions & 2 deletions ee/packages/federation-matrix/tests/end-to-end/room.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import {
addUserToRoomSlashCommand,
acceptRoomInvite,
rejectRoomInvite,
getRoomMembers,
getSubscriptions,
} 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';
Expand Down Expand Up @@ -1563,14 +1565,58 @@ import { SynapseClient } from '../helper/synapse-client';
// RC view: Admin tries to accept rc1User1's invitation
const response = await acceptRoomInvite(federatedChannel._id, rc1AdminRequestConfig);
expect(response.success).toBe(false);
expect(response.error).toBe('Failed to handle invite: No subscription found or user does not have permission to accept or reject this invite');
expect(response.error).toBe(
'Failed to handle invite: No subscription found or user does not have permission to accept or reject this invite',
);
});

it('It should not allow admin to reject invitation on behalf of another user', async () => {
// RC view: Admin tries to reject rc1User1's invitation
const response = await rejectRoomInvite(federatedChannel._id, rc1AdminRequestConfig);
expect(response.success).toBe(false);
expect(response.error).toBe('Failed to handle invite: No subscription found or user does not have permission to accept or reject this invite');
expect(response.error).toBe(
'Failed to handle invite: No subscription found or user does not have permission to accept or reject this invite',
);
});
});
});

describe('Inviting a RC user from Synapse', () => {
describe('Room that already contains previous events', () => {
let matrixRoomId: string;
let channelName: string;
let rid: string;
beforeAll(async () => {
channelName = `federated-channel-from-synapse-${Date.now()}`;
matrixRoomId = await hs1AdminApp.createRoom(channelName);

await hs1AdminApp.matrixClient.sendTextMessage(matrixRoomId, 'Message from admin');
await hs1AdminApp.matrixClient.invite(matrixRoomId, federationConfig.hs1.additionalUser1.matrixUserId);
await hs1User1App.matrixClient.joinRoom(matrixRoomId);
await hs1User1App.matrixClient.sendTextMessage(matrixRoomId, 'Message from user1');
await hs1AdminApp.matrixClient.invite(matrixRoomId, federationConfig.rc1.adminMatrixUserId);

const subscriptions = await getSubscriptions(rc1AdminRequestConfig);

const pendingInvitation = subscriptions.update.find((subscription) => subscription.status === 'INVITED');

expect(pendingInvitation).not.toBeUndefined();

rid = pendingInvitation?.rid!;

await acceptRoomInvite(rid, rc1AdminRequestConfig);
}, 15000);

describe('It should reflect all the members and messagens on the rocket.chat side', () => {
Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

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

Typo in the describe block: "messagens" should be "messages".

Suggested change
describe('It should reflect all the members and messagens on the rocket.chat side', () => {
describe('It should reflect all the members and messages on the rocket.chat side', () => {

Copilot uses AI. Check for mistakes.
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),
Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

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

The test is checking for the wrong user. Line 1616 checks for federationConfig.rc1.additionalUser1.username, but this user was never invited to the room. Based on the setup in beforeAll, the three members should be:

  1. The Synapse admin who created the room
  2. The Synapse user1 (federationConfig.hs1.additionalUser1.matrixUserId) who was invited on line 1594
  3. The RC admin (federationConfig.rc1.adminUser) who was invited on line 1597

This assertion should check for federationConfig.hs1.additionalUser1.matrixUserId instead of federationConfig.rc1.additionalUser1.username.

Suggested change
members.members.find((member: IUser) => member.username === federationConfig.rc1.additionalUser1.username),
members.members.find((member: IUser) => member.username === federationConfig.hs1.additionalUser1.matrixUserId),

Copilot uses AI. Check for mistakes.
).not.toBeNull();
expect(members.members.find((member: IUser) => member.username === federationConfig.hs1.adminMatrixUserId)).not.toBeNull();
});
});
});
});
Expand Down
Loading
Loading