diff --git a/apps/meteor/ee/server/hooks/federation/index.ts b/apps/meteor/ee/server/hooks/federation/index.ts index aed78ee8dbca8..e6b261079206d 100644 --- a/apps/meteor/ee/server/hooks/federation/index.ts +++ b/apps/meteor/ee/server/hooks/federation/index.ts @@ -1,10 +1,27 @@ import { FederationMatrix } from '@rocket.chat/core-services'; import type { IMessage, IRoom, IUser } from '@rocket.chat/core-typings'; +import { Messages } from '@rocket.chat/models'; import { callbacks } from '../../../../lib/callbacks'; import { afterLeaveRoomCallback } from '../../../../lib/callbacks/afterLeaveRoomCallback'; import { afterRemoveFromRoomCallback } from '../../../../lib/callbacks/afterRemoveFromRoomCallback'; +callbacks.add( + 'afterDeleteMessage', + async (message: IMessage) => { + if (!message.federation?.eventId) { + return; + } + const isEchoMessage = !(await Messages.findOneByFederationId(message.federation?.eventId)); + if (isEchoMessage) { + return; + } + await FederationMatrix.deleteMessage(message); + }, + callbacks.priority.MEDIUM, + 'native-federation-after-delete-message', +); + callbacks.add( 'native-federation.onAddUsersToRoom', async ({ invitees, inviter }, room) => FederationMatrix.inviteUsersToRoom(room, invitees, inviter), diff --git a/apps/meteor/server/services/messages/hooks/BeforeFederationActions.ts b/apps/meteor/server/services/messages/hooks/BeforeFederationActions.ts index b284858895397..bedd3db82adf4 100644 --- a/apps/meteor/server/services/messages/hooks/BeforeFederationActions.ts +++ b/apps/meteor/server/services/messages/hooks/BeforeFederationActions.ts @@ -1,12 +1,12 @@ import { isMessageFromMatrixFederation, isRoomFederated } from '@rocket.chat/core-typings'; import type { AtLeast, IMessage, IRoom } from '@rocket.chat/core-typings'; -import { isFederationEnabled, isFederationReady } from '../../federation/utils'; +import { getFederationVersion, isFederationEnabled, isFederationReady } from '../../federation/utils'; export class FederationActions { public static shouldPerformAction(message: IMessage, room: AtLeast): boolean { if (isMessageFromMatrixFederation(message) || isRoomFederated(room)) { - return isFederationEnabled() && isFederationReady(); + return getFederationVersion() === 'native' || (isFederationEnabled() && isFederationReady()); } return true; diff --git a/ee/packages/federation-matrix/src/FederationMatrix.ts b/ee/packages/federation-matrix/src/FederationMatrix.ts index 4cda0fa4acc0f..74581ebcc661a 100644 --- a/ee/packages/federation-matrix/src/FederationMatrix.ts +++ b/ee/packages/federation-matrix/src/FederationMatrix.ts @@ -3,7 +3,7 @@ import 'reflect-metadata'; import { ConfigService, createFederationContainer, getAllServices } from '@hs/federation-sdk'; import type { HomeserverEventSignatures, HomeserverServices, FederationContainerOptions } from '@hs/federation-sdk'; import { type IFederationMatrixService, Room, ServiceClass, Settings } from '@rocket.chat/core-services'; -import type { IMessage, IRoom, IUser } from '@rocket.chat/core-typings'; +import { isDeletedMessage, isMessageFromMatrixFederation, type IMessage, type IRoom, type 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'; @@ -195,6 +195,39 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS } } + async deleteMessage(message: IMessage): Promise { + try { + if (!isMessageFromMatrixFederation(message) || isDeletedMessage(message)) { + return; + } + const matrixRoomId = await MatrixBridgedRoom.getExternalRoomId(message.rid); + if (!matrixRoomId) { + throw new Error(`No Matrix room mapping found for room ${message.rid}`); + } + const matrixDomain = await this.getMatrixDomain(); + const matrixUserId = `@${message.u.username}:${matrixDomain}`; + const existingMatrixUserId = await MatrixBridgedUser.getExternalUserIdByLocalUserId(message.u._id); + if (!existingMatrixUserId) { + await MatrixBridgedUser.createOrUpdateByLocalId(message.u._id, matrixUserId, true, matrixDomain); + } + + if (!this.homeserverServices) { + this.logger.warn('Homeserver services not available, skipping message redaction'); + return; + } + const matrixEventId = message.federation?.eventId; + if (!matrixEventId) { + throw new Error(`No Matrix event ID mapping found for message ${message._id}`); + } + const eventId = await this.homeserverServices.message.redactMessage(matrixRoomId, matrixEventId, matrixUserId); + + this.logger.debug('Message Redaction sent to Matrix successfully:', eventId); + } catch (error) { + this.logger.error('Failed to send redaction to Matrix:', error); + throw error; + } + } + async inviteUsersToRoom(room: IRoom, usersUserName: string[], inviter: IUser): Promise { try { const matrixRoomId = await MatrixBridgedRoom.getExternalRoomId(room._id); diff --git a/ee/packages/federation-matrix/src/events/message.ts b/ee/packages/federation-matrix/src/events/message.ts index 4d90d77ebd838..14ec990143de2 100644 --- a/ee/packages/federation-matrix/src/events/message.ts +++ b/ee/packages/federation-matrix/src/events/message.ts @@ -1,10 +1,10 @@ import type { HomeserverEventSignatures } from '@hs/federation-sdk'; -import { Message } from '@rocket.chat/core-services'; +import { FederationMatrix, Message } from '@rocket.chat/core-services'; import { UserStatus } from '@rocket.chat/core-typings'; import type { IUser } from '@rocket.chat/core-typings'; import type { Emitter } from '@rocket.chat/emitter'; import { Logger } from '@rocket.chat/logger'; -import { Users, MatrixBridgedUser, MatrixBridgedRoom, Rooms, Subscriptions } from '@rocket.chat/models'; +import { Users, MatrixBridgedUser, MatrixBridgedRoom, Rooms, Subscriptions, Messages } from '@rocket.chat/models'; const logger = new Logger('federation-matrix:message'); @@ -126,4 +126,36 @@ export function message(emitter: Emitter) { logger.error('Error processing Matrix message:', error); } }); + + emitter.on('homeserver.matrix.redaction', async (data) => { + try { + const redactedEventId = data.redacts; + if (!redactedEventId) { + logger.debug('No redacts field in redaction event'); + return; + } + + const messageEvent = await FederationMatrix.getEventById(redactedEventId); + if (!messageEvent || messageEvent.type !== 'm.room.message') { + logger.debug(`Event ${redactedEventId} is not a message event`); + return; + } + + const rcMessage = await Messages.findOneByFederationId(data.redacts); + if (!rcMessage) { + logger.debug(`No RC message found for event ${data.redacts}`); + return; + } + + const user = await Users.findOneByUsername(data.sender); + if (!user) { + logger.debug(`User not found: ${data.sender}`); + return; + } + + await Message.deleteMessage(user, rcMessage); + } catch (error) { + logger.error('Failed to process Matrix removal redaction:', error); + } + }); } diff --git a/packages/core-services/src/types/IFederationMatrixService.ts b/packages/core-services/src/types/IFederationMatrixService.ts index 89df53ca9fb6e..9ed0fb8258a2d 100644 --- a/packages/core-services/src/types/IFederationMatrixService.ts +++ b/packages/core-services/src/types/IFederationMatrixService.ts @@ -17,6 +17,7 @@ export interface IFederationMatrixService { }; createRoom(room: IRoom, owner: IUser, members: string[]): Promise; sendMessage(message: IMessage, room: IRoom, user: IUser): Promise; + deleteMessage(message: IMessage): Promise; sendReaction(messageId: string, reaction: string, user: IUser): Promise; removeReaction(messageId: string, reaction: string, user: IUser, oldMessage: IMessage): Promise; getEventById(eventId: string): Promise; diff --git a/packages/core-typings/src/IMessage/IMessage.ts b/packages/core-typings/src/IMessage/IMessage.ts index d5a2ee518d324..d7f995de3e260 100644 --- a/packages/core-typings/src/IMessage/IMessage.ts +++ b/packages/core-typings/src/IMessage/IMessage.ts @@ -259,7 +259,14 @@ export const isSystemMessage = (message: IMessage): message is ISystemMessage => message.t !== undefined && MessageTypes.includes(message.t); export const isDeletedMessage = (message: IMessage): message is IEditedMessage => isEditedMessage(message) && message.t === 'rm'; -export const isMessageFromMatrixFederation = (message: IMessage): boolean => + +export interface IFederatedMessage extends IMessage { + federation: { + eventId: string; + }; +} + +export const isMessageFromMatrixFederation = (message: IMessage): message is IFederatedMessage => 'federation' in message && Boolean(message.federation?.eventId); export interface ITranslatedMessage extends IMessage {