diff --git a/apps/meteor/ee/server/hooks/federation/index.ts b/apps/meteor/ee/server/hooks/federation/index.ts index d1ac84396e251..307fadc22768b 100644 --- a/apps/meteor/ee/server/hooks/federation/index.ts +++ b/apps/meteor/ee/server/hooks/federation/index.ts @@ -1,5 +1,12 @@ +import { + isEditedMessage, + isMessageFromMatrixFederation, + isRoomFederated, + type IMessage, + type IRoom, + type IUser, +} from '@rocket.chat/core-typings'; import { api, FederationMatrix } from '@rocket.chat/core-services'; -import type { IMessage, IRoom, IUser } from '@rocket.chat/core-typings'; import { Rooms } from '@rocket.chat/models'; import notifications from '../../../../app/notifications/server/lib/Notifications'; @@ -89,6 +96,24 @@ afterRemoveFromRoomCallback.add( 'federation-matrix-after-remove-from-room', ); +callbacks.add( + 'afterSaveMessage', + async (message: IMessage, { room }): Promise => { + if (!room || !isRoomFederated(room) || !message || !isMessageFromMatrixFederation(message)) { + return message; + } + + if (!isEditedMessage(message)) { + return message; + } + + await FederationMatrix.updateMessage(message._id, message.msg, message.u); + return message; + }, + callbacks.priority.HIGH, + 'federation-matrix-after-room-message-updated', +); + export const setupTypingEventListenerForRoom = (roomId: string): void => { notifications.streamRoom.on(`${roomId}/user-activity`, (username, activity) => { if (Array.isArray(activity) && (!activity.length || activity.includes('user-typing'))) { diff --git a/ee/packages/federation-matrix/src/FederationMatrix.ts b/ee/packages/federation-matrix/src/FederationMatrix.ts index 8339c448c4d7e..f487d5ac9e669 100644 --- a/ee/packages/federation-matrix/src/FederationMatrix.ts +++ b/ee/packages/federation-matrix/src/FederationMatrix.ts @@ -555,4 +555,36 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS throw error; } } + + async updateMessage(messageId: string, newContent: string, sender: IUser): Promise { + try { + const message = await Messages.findOneById(messageId); + if (!message) { + throw new Error(`Message ${messageId} not found`); + } + + const matrixRoomId = await MatrixBridgedRoom.getExternalRoomId(message.rid); + if (!matrixRoomId) { + throw new Error(`No Matrix room mapping found for room ${message.rid}`); + } + + const matrixEventId = message.federation?.eventId; + if (!matrixEventId) { + throw new Error(`No Matrix event ID mapping found for message ${messageId}`); + } + + const existingMatrixUserId = await MatrixBridgedUser.getExternalUserIdByLocalUserId(sender._id); + if (!existingMatrixUserId) { + this.logger.error(`No Matrix user ID mapping found for user ${sender._id}`); + return; + } + + const eventId = await this.homeserverServices.message.updateMessage(matrixRoomId, newContent, existingMatrixUserId, matrixEventId); + + this.logger.debug('Message updated in Matrix successfully:', eventId); + } catch (error) { + this.logger.error('Failed to update message in Matrix:', error); + throw error; + } + } } diff --git a/ee/packages/federation-matrix/src/events/message.ts b/ee/packages/federation-matrix/src/events/message.ts index dfde54bfa158c..e73350f87ab6c 100644 --- a/ee/packages/federation-matrix/src/events/message.ts +++ b/ee/packages/federation-matrix/src/events/message.ts @@ -118,6 +118,33 @@ export function message(emitter: Emitter) { } } + const isEditedMessage = data.content['m.relates_to']?.rel_type === 'm.replace'; + if (isEditedMessage && data.content['m.relates_to']?.event_id && data.content['m.new_content']) { + logger.debug('Received edited message from Matrix, updating existing message'); + const originalMessage = await Messages.findOneByFederationId(data.content['m.relates_to'].event_id); + if (!originalMessage) { + logger.error('Original message not found for edit:', data.content['m.relates_to'].event_id); + return; + } + if (originalMessage.federation?.eventId !== data.content['m.relates_to'].event_id) { + return; + } + if (originalMessage.msg === data.content['m.new_content']?.body) { + logger.debug('No changes in message content, skipping update'); + return; + } + + await Message.updateMessage( + { + ...originalMessage, + msg: data.content['m.new_content']?.body, + }, + user, + originalMessage, + ); + return; + } + await Message.saveMessageFromFederation({ fromId: user._id, rid: internalRoomId, diff --git a/packages/core-services/src/types/IFederationMatrixService.ts b/packages/core-services/src/types/IFederationMatrixService.ts index 9ed0fb8258a2d..4bf09c4d2b2bb 100644 --- a/packages/core-services/src/types/IFederationMatrixService.ts +++ b/packages/core-services/src/types/IFederationMatrixService.ts @@ -1,4 +1,4 @@ -import type { IMessage, IRoom, IUser } from '@rocket.chat/core-typings'; +import type { AtLeast, IMessage, IRoom, IUser } from '@rocket.chat/core-typings'; import type { Router } from '@rocket.chat/http-router'; export interface IRouteContext { @@ -23,4 +23,5 @@ export interface IFederationMatrixService { getEventById(eventId: string): Promise; leaveRoom(roomId: string, user: IUser): Promise; kickUser(roomId: string, removedUser: IUser, userWhoRemoved: IUser): Promise; + updateMessage(messageId: string, newContent: string, sender: AtLeast): Promise; }