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: 1 addition & 1 deletion apps/meteor/app/reactions/server/setReaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export async function setReaction(
reaction: string,
userAlreadyReacted?: boolean,
) {
await Message.beforeReacted(message, room);
// await Message.beforeReacted(message, room);

if (Array.isArray(room.muted) && room.muted.includes(user.username as string)) {
throw new Meteor.Error('error-not-allowed', i18n.t('You_have_been_muted', { lng: user.language }), {
Expand Down
28 changes: 27 additions & 1 deletion apps/meteor/ee/server/hooks/federation/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { FederationMatrix } from '@rocket.chat/core-services';
import type { IMessage, IUser } from '@rocket.chat/core-typings';
import type { IMessage, IRoom, IUser } from '@rocket.chat/core-typings';

import { callbacks } from '../../../../lib/callbacks';
import { afterLeaveRoomCallback } from '../../../../lib/callbacks/afterLeaveRoomCallback';
import { afterRemoveFromRoomCallback } from '../../../../lib/callbacks/afterRemoveFromRoomCallback';

callbacks.add(
'native-federation.onAddUsersToRoom',
Expand Down Expand Up @@ -42,3 +44,27 @@ callbacks.add(
callbacks.priority.HIGH,
'federation-matrix-after-unset-reaction',
);

afterLeaveRoomCallback.add(
async (user: IUser, room: IRoom): Promise<void> => {
if (!room.federated) {
return;
}

await FederationMatrix.leaveRoom(room._id, user);
},
callbacks.priority.HIGH,
'federation-matrix-after-leave-room',
);

afterRemoveFromRoomCallback.add(
async (data: { removedUser: IUser; userWhoRemoved: IUser }, room: IRoom): Promise<void> => {
if (!room.federated) {
return;
}

await FederationMatrix.kickUser(room._id, data.removedUser, data.userWhoRemoved);
},
callbacks.priority.HIGH,
'federation-matrix-after-remove-from-room',
);
1 change: 1 addition & 0 deletions apps/meteor/ee/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import './configuration/index';
import './local-services/ldap/service';
import './methods/getReadReceipts';
import './patches';
import './hooks/federation';
import { License } from '@rocket.chat/license';

export * from './apps/startup';
Expand Down
17 changes: 15 additions & 2 deletions apps/meteor/server/services/room/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { getValidRoomName } from '../../../app/utils/server/lib/getValidRoomName
import { RoomMemberActions } from '../../../definition/IRoomTypeConfig';
import { roomCoordinator } from '../../lib/rooms/roomCoordinator';
import { createDirectMessage } from '../../methods/createDirectMessage';
import { getFederationVersion } from '../federation/utils';

export class RoomService extends ServiceClassInternal implements IRoomService {
protected name = 'room';
Expand Down Expand Up @@ -128,11 +129,23 @@ export class RoomService extends ServiceClassInternal implements IRoomService {
}

async beforeLeave(room: IRoom): Promise<void> {
FederationActions.blockIfRoomFederatedButServiceNotReady(room);
const federationVersion = getFederationVersion();

// If its from the deprecated federation, we need to block if the service is not ready
// If its from the new federation, do nothing at this point cause removals will be handled by callbacks
if (federationVersion === 'matrix' && room.federated === true) {
FederationActions.blockIfRoomFederatedButServiceNotReady(room);
}
}

async beforeUserRemoved(room: IRoom): Promise<void> {
FederationActions.blockIfRoomFederatedButServiceNotReady(room);
const federationVersion = getFederationVersion();

// If its from the deprecated federation, we need to block if the service is not ready
// If its from the new federation, do nothing at this point cause removals will be handled by callbacks
if (federationVersion === 'matrix' && room.federated === true) {
FederationActions.blockIfRoomFederatedButServiceNotReady(room);
}
}

async beforeNameChange(room: IRoom): Promise<void> {
Expand Down
90 changes: 89 additions & 1 deletion ee/packages/federation-matrix/src/FederationMatrix.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import type { IMessage, IRoom, IUser } from '@rocket.chat/core-typings';
import { Emitter } from '@rocket.chat/emitter';
import { Router } from '@rocket.chat/http-router';
import { Logger } from '@rocket.chat/logger';
import { MatrixBridgedUser, MatrixBridgedRoom, Users, Subscriptions, Messages } from '@rocket.chat/models';
import { MatrixBridgedUser, MatrixBridgedRoom, Users, Subscriptions, Messages, Rooms } from '@rocket.chat/models';
import emojione from 'emojione';

import { getWellKnownRoutes } from './api/.well-known/server';
Expand Down Expand Up @@ -347,4 +347,92 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS
throw error;
}
}

async leaveRoom(roomId: string, user: IUser): Promise<void> {
try {
const room = await Rooms.findOneById(roomId);
if (!room?.federated) {
this.logger.debug(`Room ${roomId} is not federated, skipping leave operation`);
return;
}

const matrixRoomId = await MatrixBridgedRoom.getExternalRoomId(roomId);
if (!matrixRoomId) {
this.logger.warn(`No Matrix room mapping found for federated room ${roomId}, skipping leave`);
return;
}

const matrixDomain = await this.getMatrixDomain();
const matrixUserId = `@${user.username}:${matrixDomain}`;
const existingMatrixUserId = await MatrixBridgedUser.getExternalUserIdByLocalUserId(user._id);

if (!existingMatrixUserId) {
// User might not have been bridged yet if they never sent a message
await MatrixBridgedUser.createOrUpdateByLocalId(user._id, matrixUserId, true, matrixDomain);
}

if (!this.homeserverServices) {
this.logger.warn('Homeserver services not available, skipping room leave');
return;
}

const actualMatrixUserId = existingMatrixUserId || matrixUserId;

await this.homeserverServices.room.leaveRoom(matrixRoomId, actualMatrixUserId);

this.logger.info(`User ${user.username} left Matrix room ${matrixRoomId} successfully`);
} catch (error) {
this.logger.error('Failed to leave room in Matrix:', error);
throw error;
}
}

async kickUser(roomId: string, removedUser: IUser, userWhoRemoved: IUser): Promise<void> {
try {
const room = await Rooms.findOneById(roomId);
if (!room?.federated) {
this.logger.debug(`Room ${roomId} is not federated, skipping kick operation`);
return;
}

const matrixRoomId = await MatrixBridgedRoom.getExternalRoomId(roomId);
if (!matrixRoomId) {
this.logger.warn(`No Matrix room mapping found for federated room ${roomId}, skipping kick`);
return;
}

const matrixDomain = await this.getMatrixDomain();

const kickedMatrixUserId = `@${removedUser.username}:${matrixDomain}`;
const existingKickedMatrixUserId = await MatrixBridgedUser.getExternalUserIdByLocalUserId(removedUser._id);
if (!existingKickedMatrixUserId) {
await MatrixBridgedUser.createOrUpdateByLocalId(removedUser._id, kickedMatrixUserId, true, matrixDomain);
}
const actualKickedMatrixUserId = existingKickedMatrixUserId || kickedMatrixUserId;

const senderMatrixUserId = `@${userWhoRemoved.username}:${matrixDomain}`;
const existingSenderMatrixUserId = await MatrixBridgedUser.getExternalUserIdByLocalUserId(userWhoRemoved._id);
if (!existingSenderMatrixUserId) {
await MatrixBridgedUser.createOrUpdateByLocalId(userWhoRemoved._id, senderMatrixUserId, true, matrixDomain);
}
const actualSenderMatrixUserId = existingSenderMatrixUserId || senderMatrixUserId;

if (!this.homeserverServices) {
this.logger.warn('Homeserver services not available, skipping user kick');
return;
}

await this.homeserverServices.room.kickUser(
matrixRoomId,
actualKickedMatrixUserId,
actualSenderMatrixUserId,
`Kicked by ${userWhoRemoved.username}`,
);

this.logger.info(`User ${removedUser.username} was kicked from Matrix room ${matrixRoomId} by ${userWhoRemoved.username}`);
} catch (error) {
this.logger.error('Failed to kick user from Matrix room:', error);
throw error;
}
}
}
2 changes: 2 additions & 0 deletions ee/packages/federation-matrix/src/events/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { HomeserverEventSignatures } from '@hs/federation-sdk';
import type { Emitter } from '@rocket.chat/emitter';

import { invite } from './invite';
import { member } from './member';
import { message } from './message';
import { ping } from './ping';
import { reaction } from './reaction';
Expand All @@ -11,4 +12,5 @@ export function registerEvents(emitter: Emitter<HomeserverEventSignatures>) {
message(emitter);
invite(emitter);
reaction(emitter);
member(emitter);
}
61 changes: 61 additions & 0 deletions ee/packages/federation-matrix/src/events/member.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import type { HomeserverEventSignatures } from '@hs/federation-sdk';
import { Room } from '@rocket.chat/core-services';
import type { Emitter } from '@rocket.chat/emitter';
import { Logger } from '@rocket.chat/logger';
import { MatrixBridgedRoom, MatrixBridgedUser, Users } from '@rocket.chat/models';

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

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

const room = await MatrixBridgedRoom.findOne({ mri: data.room_id });
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 affectedMatrixUser = await MatrixBridgedUser.findOne({ mui: data.state_key });
if (!affectedMatrixUser) {
logger.warn(`No bridged user found for Matrix user_id: ${data.state_key}`);
return;
}

const affectedUser = await Users.findOneById(affectedMatrixUser.uid);
if (!affectedUser) {
logger.error(`No Rocket.Chat user found for bridged user: ${affectedMatrixUser.uid}`);
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.rid, affectedUser);
logger.info(`User ${affectedUser.username} left room ${room.rid} via Matrix federation`);
} else {
// Kick - find who kicked
const kickerMatrixUser = await MatrixBridgedUser.findOne({ mui: data.sender });
let kickerUser = null;
if (kickerMatrixUser) {
kickerUser = await Users.findOneById(kickerMatrixUser.uid);
}

await Room.removeUserFromRoom(room.rid, 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.rid} by ${data.sender} via Matrix federation.${reasonText}`);
}
} catch (error) {
logger.error('Failed to process Matrix membership event:', error);
}
});
}
2 changes: 2 additions & 0 deletions packages/core-services/src/types/IFederationMatrixService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,6 @@ export interface IFederationMatrixService {
sendReaction(messageId: string, reaction: string, user: IUser): Promise<void>;
removeReaction(messageId: string, reaction: string, user: IUser, oldMessage: IMessage): Promise<void>;
getEventById(eventId: string): Promise<any | null>;
leaveRoom(roomId: string, user: IUser): Promise<void>;
kickUser(roomId: string, removedUser: IUser, userWhoRemoved: IUser): Promise<void>;
}
Loading