Skip to content

Commit 547b758

Browse files
author
Marcos Defendi
committed
Merge branch 'feat/federation' into feat/send-redact-message
2 parents 2b83854 + 5056405 commit 547b758

File tree

8 files changed

+199
-6
lines changed

8 files changed

+199
-6
lines changed

apps/meteor/app/reactions/server/setReaction.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Apps, AppEvents } from '@rocket.chat/apps';
2-
import { api, Message } from '@rocket.chat/core-services';
2+
import { api } from '@rocket.chat/core-services';
33
import type { IMessage, IRoom, IUser } from '@rocket.chat/core-typings';
44
import type { ServerMethods } from '@rocket.chat/ddp-client';
55
import { Messages, EmojiCustom, Rooms, Users } from '@rocket.chat/models';
@@ -39,7 +39,7 @@ export async function setReaction(
3939
reaction: string,
4040
userAlreadyReacted?: boolean,
4141
) {
42-
await Message.beforeReacted(message, room);
42+
// await Message.beforeReacted(message, room);
4343

4444
if (Array.isArray(room.muted) && room.muted.includes(user.username as string)) {
4545
throw new Meteor.Error('error-not-allowed', i18n.t('You_have_been_muted', { lng: user.language }), {

apps/meteor/ee/server/hooks/federation/index.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { FederationMatrix } from '@rocket.chat/core-services';
2-
import type { IMessage, IUser } from '@rocket.chat/core-typings';
2+
import type { IMessage, IRoom, IUser } from '@rocket.chat/core-typings';
33
import { Messages } from '@rocket.chat/models';
44

55
import { callbacks } from '../../../../lib/callbacks';
6+
import { afterLeaveRoomCallback } from '../../../../lib/callbacks/afterLeaveRoomCallback';
7+
import { afterRemoveFromRoomCallback } from '../../../../lib/callbacks/afterRemoveFromRoomCallback';
68

79
callbacks.add(
810
'afterDeleteMessage',
@@ -59,3 +61,27 @@ callbacks.add(
5961
callbacks.priority.HIGH,
6062
'federation-matrix-after-unset-reaction',
6163
);
64+
65+
afterLeaveRoomCallback.add(
66+
async (user: IUser, room: IRoom): Promise<void> => {
67+
if (!room.federated) {
68+
return;
69+
}
70+
71+
await FederationMatrix.leaveRoom(room._id, user);
72+
},
73+
callbacks.priority.HIGH,
74+
'federation-matrix-after-leave-room',
75+
);
76+
77+
afterRemoveFromRoomCallback.add(
78+
async (data: { removedUser: IUser; userWhoRemoved: IUser }, room: IRoom): Promise<void> => {
79+
if (!room.federated) {
80+
return;
81+
}
82+
83+
await FederationMatrix.kickUser(room._id, data.removedUser, data.userWhoRemoved);
84+
},
85+
callbacks.priority.HIGH,
86+
'federation-matrix-after-remove-from-room',
87+
);

apps/meteor/ee/server/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import './configuration/index';
1313
import './local-services/ldap/service';
1414
import './methods/getReadReceipts';
1515
import './patches';
16+
import './hooks/federation';
1617
import { License } from '@rocket.chat/license';
1718

1819
export * from './apps/startup';

apps/meteor/server/services/room/service.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { getValidRoomName } from '../../../app/utils/server/lib/getValidRoomName
1212
import { RoomMemberActions } from '../../../definition/IRoomTypeConfig';
1313
import { roomCoordinator } from '../../lib/rooms/roomCoordinator';
1414
import { createDirectMessage } from '../../methods/createDirectMessage';
15+
import { getFederationVersion } from '../federation/utils';
1516

1617
export class RoomService extends ServiceClassInternal implements IRoomService {
1718
protected name = 'room';
@@ -128,11 +129,23 @@ export class RoomService extends ServiceClassInternal implements IRoomService {
128129
}
129130

130131
async beforeLeave(room: IRoom): Promise<void> {
131-
FederationActions.blockIfRoomFederatedButServiceNotReady(room);
132+
const federationVersion = getFederationVersion();
133+
134+
// If its from the deprecated federation, we need to block if the service is not ready
135+
// If its from the new federation, do nothing at this point cause removals will be handled by callbacks
136+
if (federationVersion === 'matrix' && room.federated === true) {
137+
FederationActions.blockIfRoomFederatedButServiceNotReady(room);
138+
}
132139
}
133140

134141
async beforeUserRemoved(room: IRoom): Promise<void> {
135-
FederationActions.blockIfRoomFederatedButServiceNotReady(room);
142+
const federationVersion = getFederationVersion();
143+
144+
// If its from the deprecated federation, we need to block if the service is not ready
145+
// If its from the new federation, do nothing at this point cause removals will be handled by callbacks
146+
if (federationVersion === 'matrix' && room.federated === true) {
147+
FederationActions.blockIfRoomFederatedButServiceNotReady(room);
148+
}
136149
}
137150

138151
async beforeNameChange(room: IRoom): Promise<void> {

ee/packages/federation-matrix/src/FederationMatrix.ts

Lines changed: 89 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { isDeletedMessage, isMessageFromMatrixFederation, type IMessage, type IR
77
import { Emitter } from '@rocket.chat/emitter';
88
import { Router } from '@rocket.chat/http-router';
99
import { Logger } from '@rocket.chat/logger';
10-
import { MatrixBridgedUser, MatrixBridgedRoom, Users, Subscriptions, Messages } from '@rocket.chat/models';
10+
import { MatrixBridgedUser, MatrixBridgedRoom, Users, Subscriptions, Messages, Rooms } from '@rocket.chat/models';
1111
import emojione from 'emojione';
1212

1313
import { getWellKnownRoutes } from './api/.well-known/server';
@@ -380,4 +380,92 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS
380380
throw error;
381381
}
382382
}
383+
384+
async leaveRoom(roomId: string, user: IUser): Promise<void> {
385+
try {
386+
const room = await Rooms.findOneById(roomId);
387+
if (!room?.federated) {
388+
this.logger.debug(`Room ${roomId} is not federated, skipping leave operation`);
389+
return;
390+
}
391+
392+
const matrixRoomId = await MatrixBridgedRoom.getExternalRoomId(roomId);
393+
if (!matrixRoomId) {
394+
this.logger.warn(`No Matrix room mapping found for federated room ${roomId}, skipping leave`);
395+
return;
396+
}
397+
398+
const matrixDomain = await this.getMatrixDomain();
399+
const matrixUserId = `@${user.username}:${matrixDomain}`;
400+
const existingMatrixUserId = await MatrixBridgedUser.getExternalUserIdByLocalUserId(user._id);
401+
402+
if (!existingMatrixUserId) {
403+
// User might not have been bridged yet if they never sent a message
404+
await MatrixBridgedUser.createOrUpdateByLocalId(user._id, matrixUserId, true, matrixDomain);
405+
}
406+
407+
if (!this.homeserverServices) {
408+
this.logger.warn('Homeserver services not available, skipping room leave');
409+
return;
410+
}
411+
412+
const actualMatrixUserId = existingMatrixUserId || matrixUserId;
413+
414+
await this.homeserverServices.room.leaveRoom(matrixRoomId, actualMatrixUserId);
415+
416+
this.logger.info(`User ${user.username} left Matrix room ${matrixRoomId} successfully`);
417+
} catch (error) {
418+
this.logger.error('Failed to leave room in Matrix:', error);
419+
throw error;
420+
}
421+
}
422+
423+
async kickUser(roomId: string, removedUser: IUser, userWhoRemoved: IUser): Promise<void> {
424+
try {
425+
const room = await Rooms.findOneById(roomId);
426+
if (!room?.federated) {
427+
this.logger.debug(`Room ${roomId} is not federated, skipping kick operation`);
428+
return;
429+
}
430+
431+
const matrixRoomId = await MatrixBridgedRoom.getExternalRoomId(roomId);
432+
if (!matrixRoomId) {
433+
this.logger.warn(`No Matrix room mapping found for federated room ${roomId}, skipping kick`);
434+
return;
435+
}
436+
437+
const matrixDomain = await this.getMatrixDomain();
438+
439+
const kickedMatrixUserId = `@${removedUser.username}:${matrixDomain}`;
440+
const existingKickedMatrixUserId = await MatrixBridgedUser.getExternalUserIdByLocalUserId(removedUser._id);
441+
if (!existingKickedMatrixUserId) {
442+
await MatrixBridgedUser.createOrUpdateByLocalId(removedUser._id, kickedMatrixUserId, true, matrixDomain);
443+
}
444+
const actualKickedMatrixUserId = existingKickedMatrixUserId || kickedMatrixUserId;
445+
446+
const senderMatrixUserId = `@${userWhoRemoved.username}:${matrixDomain}`;
447+
const existingSenderMatrixUserId = await MatrixBridgedUser.getExternalUserIdByLocalUserId(userWhoRemoved._id);
448+
if (!existingSenderMatrixUserId) {
449+
await MatrixBridgedUser.createOrUpdateByLocalId(userWhoRemoved._id, senderMatrixUserId, true, matrixDomain);
450+
}
451+
const actualSenderMatrixUserId = existingSenderMatrixUserId || senderMatrixUserId;
452+
453+
if (!this.homeserverServices) {
454+
this.logger.warn('Homeserver services not available, skipping user kick');
455+
return;
456+
}
457+
458+
await this.homeserverServices.room.kickUser(
459+
matrixRoomId,
460+
actualKickedMatrixUserId,
461+
actualSenderMatrixUserId,
462+
`Kicked by ${userWhoRemoved.username}`,
463+
);
464+
465+
this.logger.info(`User ${removedUser.username} was kicked from Matrix room ${matrixRoomId} by ${userWhoRemoved.username}`);
466+
} catch (error) {
467+
this.logger.error('Failed to kick user from Matrix room:', error);
468+
throw error;
469+
}
470+
}
383471
}

ee/packages/federation-matrix/src/events/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { HomeserverEventSignatures } from '@hs/federation-sdk';
22
import type { Emitter } from '@rocket.chat/emitter';
33

44
import { invite } from './invite';
5+
import { member } from './member';
56
import { message } from './message';
67
import { ping } from './ping';
78
import { reaction } from './reaction';
@@ -11,4 +12,5 @@ export function registerEvents(emitter: Emitter<HomeserverEventSignatures>) {
1112
message(emitter);
1213
invite(emitter);
1314
reaction(emitter);
15+
member(emitter);
1416
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import type { HomeserverEventSignatures } from '@hs/federation-sdk';
2+
import { Room } from '@rocket.chat/core-services';
3+
import type { Emitter } from '@rocket.chat/emitter';
4+
import { Logger } from '@rocket.chat/logger';
5+
import { MatrixBridgedRoom, MatrixBridgedUser, Users } from '@rocket.chat/models';
6+
7+
const logger = new Logger('federation-matrix:member');
8+
9+
export function member(emitter: Emitter<HomeserverEventSignatures>) {
10+
emitter.on('homeserver.matrix.membership', async (data) => {
11+
try {
12+
// Only handle leave events (including kicks)
13+
if (data.content.membership !== 'leave') {
14+
logger.debug(`Ignoring membership event with membership: ${data.content.membership}`);
15+
return;
16+
}
17+
18+
const room = await MatrixBridgedRoom.findOne({ mri: data.room_id });
19+
if (!room) {
20+
logger.warn(`No bridged room found for Matrix room_id: ${data.room_id}`);
21+
return;
22+
}
23+
24+
// state_key is the user affected by the membership change
25+
const affectedMatrixUser = await MatrixBridgedUser.findOne({ mui: data.state_key });
26+
if (!affectedMatrixUser) {
27+
logger.warn(`No bridged user found for Matrix user_id: ${data.state_key}`);
28+
return;
29+
}
30+
31+
const affectedUser = await Users.findOneById(affectedMatrixUser.uid);
32+
if (!affectedUser) {
33+
logger.error(`No Rocket.Chat user found for bridged user: ${affectedMatrixUser.uid}`);
34+
return;
35+
}
36+
37+
// Check if this is a kick (sender != state_key) or voluntary leave (sender == state_key)
38+
if (data.sender === data.state_key) {
39+
// Voluntary leave
40+
await Room.removeUserFromRoom(room.rid, affectedUser);
41+
logger.info(`User ${affectedUser.username} left room ${room.rid} via Matrix federation`);
42+
} else {
43+
// Kick - find who kicked
44+
const kickerMatrixUser = await MatrixBridgedUser.findOne({ mui: data.sender });
45+
let kickerUser = null;
46+
if (kickerMatrixUser) {
47+
kickerUser = await Users.findOneById(kickerMatrixUser.uid);
48+
}
49+
50+
await Room.removeUserFromRoom(room.rid, affectedUser, {
51+
byUser: kickerUser || { _id: 'matrix.federation', username: 'Matrix User' },
52+
});
53+
54+
const reasonText = data.content.reason ? ` Reason: ${data.content.reason}` : '';
55+
logger.info(`User ${affectedUser.username} was kicked from room ${room.rid} by ${data.sender} via Matrix federation.${reasonText}`);
56+
}
57+
} catch (error) {
58+
logger.error('Failed to process Matrix membership event:', error);
59+
}
60+
});
61+
}

packages/core-services/src/types/IFederationMatrixService.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,6 @@ export interface IFederationMatrixService {
2121
sendReaction(messageId: string, reaction: string, user: IUser): Promise<void>;
2222
removeReaction(messageId: string, reaction: string, user: IUser, oldMessage: IMessage): Promise<void>;
2323
getEventById(eventId: string): Promise<any | null>;
24+
leaveRoom(roomId: string, user: IUser): Promise<void>;
25+
kickUser(roomId: string, removedUser: IUser, userWhoRemoved: IUser): Promise<void>;
2426
}

0 commit comments

Comments
 (0)