Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
699b989
feat: adds files support
ricardogarim Sep 1, 2025
f4f98a3
chore: kek
ricardogarim Sep 1, 2025
19e024e
chore: fixing conflicts
ricardogarim Sep 8, 2025
876a36b
chore: fixing conflicts
ricardogarim Sep 8, 2025
ce6ff01
refactor: removes MatrixRemote file store
ricardogarim Sep 11, 2025
d690294
refactor: removes unused code
ricardogarim Sep 11, 2025
5b5822e
refactor: makes files be sent into rc messages
ricardogarim Sep 11, 2025
063ab06
chore: adds homeserver back to .gitignore
ricardogarim Sep 11, 2025
80dff52
chore: improves code style
ricardogarim Sep 11, 2025
2c92d4e
chore: removes duplicated IUpload type
ricardogarim Sep 11, 2025
f93a37b
chore: adds setFederationInfo into Upload model
ricardogarim Sep 11, 2025
e994863
chore: removes unneeded function wrapper
ricardogarim Sep 11, 2025
d193e10
chore: merges related functions
ricardogarim Sep 11, 2025
d9129a0
refactor: moves file download to homeserver
ricardogarim Sep 11, 2025
9241741
feat: adds Federation canAccessMedia middleware (#36926)
ricardogarim Sep 13, 2025
61c7d86
feat: adds canAccessMedia middleware
ricardogarim Sep 12, 2025
7ba60f6
chore: makes federation sub props mandatory when federation prop exists
ricardogarim Sep 13, 2025
815c435
refactor: moves errCodes to homeserver package
ricardogarim Sep 13, 2025
940fa12
chore: changes thumbnail endpoint to return unrecognized
ricardogarim Sep 15, 2025
7d886b4
chore: adds todo to handle multiple files
ricardogarim Sep 15, 2025
6c8b824
chore: removes findByFederationMxcUri from uploads model
ricardogarim Sep 15, 2025
46621c8
chore: adds comment to improve uploadFile typing to avoid db calls
ricardogarim Sep 15, 2025
bbd9669
refactor: makes getMatrixMessageType to use a dict
ricardogarim Sep 15, 2025
ca0e6e8
updates yarn.lock
ricardogarim Sep 17, 2025
6382975
uses shared file types definition
ricardogarim Sep 18, 2025
bc342c8
fixes setFederationInfo typings
ricardogarim Sep 18, 2025
d7bdea4
returns file type when mimeType validation fails
ricardogarim Sep 18, 2025
844a07c
fixes findLatestFederationThreadMessageByTmid signature
ricardogarim Sep 18, 2025
20bb413
fixes broken type with encrypted rooms
ricardogarim Sep 18, 2025
5b8de1e
code style
sampaiodiego Sep 18, 2025
4eaeead
improve upload model to set federation
sampaiodiego Sep 18, 2025
f48c1ac
add TODOs
sampaiodiego Sep 18, 2025
b83f6cb
add index on uploads collection for mxcUri and serverName
ricardogarim Sep 18, 2025
32e15a3
Merge remote-tracking branch 'origin/chore/federation-backup' into fe…
ggazzo Sep 19, 2025
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
13 changes: 6 additions & 7 deletions apps/meteor/client/lib/e2ee/rocketchat.e2e.room.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Base64 } from '@rocket.chat/base64';
import type { IE2EEMessage, IMessage, IRoom, ISubscription, IUser, IUploadWithUser, AtLeast } from '@rocket.chat/core-typings';
import type { IE2EEMessage, IMessage, IRoom, ISubscription, IUser, AtLeast, EncryptedMessageContent } from '@rocket.chat/core-typings';
import { isEncryptedMessageContent } from '@rocket.chat/core-typings';
import { Emitter } from '@rocket.chat/emitter';
import type { Optional } from '@tanstack/react-query';
import EJSON from 'ejson';
Expand Down Expand Up @@ -670,11 +671,9 @@ export class E2ERoom extends Emitter {
return this.encryptText(data);
}

async decryptContent<T extends IUploadWithUser | IE2EEMessage>(data: T) {
if (data.content && data.content.algorithm === 'rc.v1.aes-sha2') {
const content = await this.decrypt(data.content.ciphertext);
Object.assign(data, content);
}
async decryptContent<T extends EncryptedMessageContent>(data: T) {
const content = await this.decrypt(data.content.ciphertext);
Object.assign(data, content);

Comment on lines +674 to 677
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Don’t blindly merge decrypted payload into the message; restrict to allowed keys

Object.assign(data, content) can overwrite reserved fields (e.g., t, rid, _id) if a crafted payload is decrypted. Limit merge to the fields we actually encrypt (msg, attachments, files, file).

Apply this diff:

-  async decryptContent<T extends EncryptedMessageContent>(data: T) {
-    const content = await this.decrypt(data.content.ciphertext);
-    Object.assign(data, content);
-    return data;
-  }
+  async decryptContent<T extends EncryptedMessageContent>(data: T) {
+    const content = await this.decrypt(data.content.ciphertext);
+    const { msg, attachments, files, file } = content as Partial<Pick<IMessage, 'msg' | 'attachments' | 'files' | 'file'>>;
+    // Only merge known, safe fields produced by encryptMessageContent
+    if (msg !== undefined) (data as any).msg = msg;
+    if (attachments !== undefined) (data as any).attachments = attachments;
+    if (files !== undefined) (data as any).files = files;
+    if (file !== undefined) (data as any).file = file;
+    return data;
+  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async decryptContent<T extends EncryptedMessageContent>(data: T) {
const content = await this.decrypt(data.content.ciphertext);
Object.assign(data, content);
async decryptContent<T extends EncryptedMessageContent>(data: T) {
const content = await this.decrypt(data.content.ciphertext);
const { msg, attachments, files, file } = content as Partial<Pick<IMessage, 'msg' | 'attachments' | 'files' | 'file'>>;
// Only merge known, safe fields produced by encryptMessageContent
if (msg !== undefined) (data as any).msg = msg;
if (attachments !== undefined) (data as any).attachments = attachments;
if (files !== undefined) (data as any).files = files;
if (file !== undefined) (data as any).file = file;
return data;
}
🤖 Prompt for AI Agents
In apps/meteor/client/lib/e2ee/rocketchat.e2e.room.ts around lines 674 to 677,
the current Object.assign(data, content) blindly merges decrypted payload into
the message and can overwrite reserved fields like t, rid, _id; instead, only
copy the allowed encrypted fields (msg, attachments, files, file) from the
decrypted content into data. Replace the Object.assign call with code that picks
those specific keys if they exist on the decrypted content and assigns them to
data (e.g., check for each allowed key and set data[key] = content[key] only
when defined) so reserved fields cannot be overwritten.

return data;
}
Expand All @@ -693,7 +692,7 @@ export class E2ERoom extends Emitter {
}
}

message = await this.decryptContent(message);
message = isEncryptedMessageContent(message) ? await this.decryptContent(message) : message;

return {
...message,
Expand Down
4 changes: 2 additions & 2 deletions apps/meteor/client/lib/e2ee/rocketchat.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import QueryString from 'querystring';
import URL from 'url';

import type { IE2EEMessage, IMessage, IRoom, ISubscription, IUser, IUploadWithUser, MessageAttachment } from '@rocket.chat/core-typings';
import { isE2EEMessage } from '@rocket.chat/core-typings';
import { isE2EEMessage, isEncryptedMessageContent } from '@rocket.chat/core-typings';
import { Emitter } from '@rocket.chat/emitter';
import { imperativeModal } from '@rocket.chat/ui-client';
import EJSON from 'ejson';
Expand Down Expand Up @@ -664,7 +664,7 @@ class E2E extends Emitter {
}

async decryptFileContent(file: IUploadWithUser): Promise<IUploadWithUser> {
if (!file.rid) {
if (!file.rid || !isEncryptedMessageContent(file)) {
return file;
}

Expand Down
9 changes: 9 additions & 0 deletions apps/meteor/server/services/messages/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,19 +90,28 @@ export class MessageService extends ServiceClassInternal implements IMessageServ
rid,
msg,
federation_event_id,
file,
files,
attachments,
thread,
}: {
fromId: string;
rid: string;
msg: string;
federation_event_id: string;
file?: IMessage['file'];
files?: IMessage['files'];
attachments?: IMessage['attachments'];
thread?: { tmid: string; tshow: boolean };
}): Promise<IMessage> {
return executeSendMessage(fromId, {
rid,
msg,
...thread,
federation: { eventId: federation_event_id },
...(file && { file }),
...(files && { files }),
...(attachments && { attachments }),
});
}

Expand Down
232 changes: 154 additions & 78 deletions ee/packages/federation-matrix/src/FederationMatrix.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import emojione from 'emojione';
import { getWellKnownRoutes } from './api/.well-known/server';
import { getMatrixInviteRoutes } from './api/_matrix/invite';
import { getKeyServerRoutes } from './api/_matrix/key/server';
import { getMatrixMediaRoutes } from './api/_matrix/media';
import { getMatrixProfilesRoutes } from './api/_matrix/profiles';
import { getMatrixRoomsRoutes } from './api/_matrix/rooms';
import { getMatrixSendJoinRoutes } from './api/_matrix/send-join';
Expand All @@ -26,6 +27,16 @@ import { isLicenseEnabledMiddleware } from './api/middlewares/isLicenseEnabled';
import { registerEvents } from './events';
import { saveExternalUserIdForLocalUser } from './helpers/identifiers';
import { toExternalMessageFormat, toExternalQuoteMessageFormat } from './helpers/message.parsers';
import { MatrixMediaService } from './services/MatrixMediaService';

type MatrixFileTypes = 'm.image' | 'm.video' | 'm.audio' | 'm.file';

export const fileTypes: Record<string, MatrixFileTypes> = {
image: 'm.image',
video: 'm.video',
audio: 'm.audio',
file: 'm.file',
};

export class FederationMatrix extends ServiceClass implements IFederationMatrixService {
protected name = 'federation-matrix';
Expand Down Expand Up @@ -100,6 +111,7 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS

await createFederationContainer(containerOptions, config);
instance.homeserverServices = getAllServices();
MatrixMediaService.setHomeserverServices(instance.homeserverServices);
instance.buildMatrixHTTPRoutes();
instance.onEvent('user.typing', async ({ isTyping, roomId, user: { username } }): Promise<void> => {
if (!roomId || !username) {
Expand Down Expand Up @@ -170,7 +182,8 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS
.use(getMatrixSendJoinRoutes(this.homeserverServices))
.use(getMatrixTransactionsRoutes(this.homeserverServices))
.use(getKeyServerRoutes(this.homeserverServices))
.use(getFederationVersionsRoutes(this.homeserverServices));
.use(getFederationVersionsRoutes(this.homeserverServices))
.use(getMatrixMediaRoutes(this.homeserverServices));

wellKnown.use(isFederationEnabledMiddleware).use(isLicenseEnabledMiddleware).use(getWellKnownRoutes(this.homeserverServices));

Expand Down Expand Up @@ -396,6 +409,143 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS
}
}

private getMatrixMessageType(mimeType?: string): MatrixFileTypes {
const mainType = mimeType?.split('/')[0];
if (!mainType) {
return fileTypes.file;
}

return fileTypes[mainType] ?? fileTypes.file;
}

private async handleFileMessage(
message: IMessage,
matrixRoomId: string,
matrixUserId: string,
matrixDomain: string,
): Promise<{ eventId: string } | null> {
if (!message.files || message.files.length === 0) {
return null;
}

try {
// TODO: Handle multiple files
const file = message.files[0];
const mxcUri = await MatrixMediaService.prepareLocalFileForMatrix(file._id, matrixDomain);

const msgtype = this.getMatrixMessageType(file.type);
const fileContent = {
body: file.name,
msgtype,
url: mxcUri,
info: {
mimetype: file.type,
size: file.size,
},
};

return this.homeserverServices.message.sendFileMessage(matrixRoomId, fileContent, matrixUserId);
} catch (error) {
this.logger.error('Failed to handle file message', {
messageId: message._id,
error,
});
throw error;
}
}

private async handleTextMessage(
message: IMessage,
matrixRoomId: string,
matrixUserId: string,
matrixDomain: string,
): Promise<{ eventId: string } | null> {
const parsedMessage = await toExternalMessageFormat({
message: message.msg,
externalRoomId: matrixRoomId,
homeServerDomain: matrixDomain,
});

if (message.tmid) {
return this.handleThreadedMessage(message, matrixRoomId, matrixUserId, matrixDomain, parsedMessage);
}

if (message.attachments?.some((attachment) => isQuoteAttachment(attachment) && Boolean(attachment.message_link))) {
return this.handleQuoteMessage(message, matrixRoomId, matrixUserId, matrixDomain);
}

return this.homeserverServices.message.sendMessage(matrixRoomId, message.msg, parsedMessage, matrixUserId);
}

private async handleThreadedMessage(
message: IMessage,
matrixRoomId: string,
matrixUserId: string,
matrixDomain: string,
parsedMessage: string,
): Promise<{ eventId: string } | null> {
if (!message.tmid) {
throw new Error('Thread message ID not found');
}

const threadRootMessage = await Messages.findOneById(message.tmid);
const threadRootEventId = threadRootMessage?.federation?.eventId;

if (!threadRootEventId) {
this.logger.warn('Thread root event ID not found, sending as regular message');
if (message.attachments?.some((attachment) => isQuoteAttachment(attachment) && Boolean(attachment.message_link))) {
return this.handleQuoteMessage(message, matrixRoomId, matrixUserId, matrixDomain);
}
return this.homeserverServices.message.sendMessage(matrixRoomId, message.msg, parsedMessage, matrixUserId);
}

const latestThreadMessage = await Messages.findLatestFederationThreadMessageByTmid(message.tmid, message._id);
const latestThreadEventId = latestThreadMessage?.federation?.eventId;

if (message.attachments?.some((attachment) => isQuoteAttachment(attachment) && Boolean(attachment.message_link))) {
const quoteMessage = await this.getQuoteMessage(message, matrixRoomId, matrixUserId, matrixDomain);
if (!quoteMessage) {
throw new Error('Failed to retrieve quote message');
}
return this.homeserverServices.message.sendReplyToInsideThreadMessage(
matrixRoomId,
quoteMessage.rawMessage,
quoteMessage.formattedMessage,
matrixUserId,
threadRootEventId,
quoteMessage.eventToReplyTo,
);
}

return this.homeserverServices.message.sendThreadMessage(
matrixRoomId,
message.msg,
parsedMessage,
matrixUserId,
threadRootEventId,
latestThreadEventId,
);
}

private async handleQuoteMessage(
message: IMessage,
matrixRoomId: string,
matrixUserId: string,
matrixDomain: string,
): Promise<{ eventId: string } | null> {
const quoteMessage = await this.getQuoteMessage(message, matrixRoomId, matrixUserId, matrixDomain);
if (!quoteMessage) {
throw new Error('Failed to retrieve quote message');
}
return this.homeserverServices.message.sendReplyToMessage(
matrixRoomId,
quoteMessage.rawMessage,
quoteMessage.formattedMessage,
quoteMessage.eventToReplyTo,
matrixUserId,
);
}

async sendMessage(message: IMessage, room: IRoom, user: IUser): Promise<void> {
try {
const matrixRoomId = await MatrixBridgedRoom.getExternalRoomId(room._id);
Expand All @@ -417,84 +567,10 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS
const actualMatrixUserId = existingMatrixUserId || matrixUserId;

let result;

const parsedMessage = await toExternalMessageFormat({
message: message.msg,
externalRoomId: matrixRoomId,
homeServerDomain: this.serverName,
});
if (!message.tmid) {
if (message.attachments?.some((attachment) => isQuoteAttachment(attachment) && Boolean(attachment.message_link))) {
const quoteMessage = await this.getQuoteMessage(message, matrixRoomId, actualMatrixUserId, this.serverName);
if (!quoteMessage) {
throw new Error('Failed to retrieve quote message');
}
result = await this.homeserverServices.message.sendReplyToMessage(
matrixRoomId,
quoteMessage.rawMessage,
quoteMessage.formattedMessage,
quoteMessage.eventToReplyTo,
actualMatrixUserId,
);
} else {
result = await this.homeserverServices.message.sendMessage(matrixRoomId, message.msg, parsedMessage, actualMatrixUserId);
}
if (message.files && message.files.length > 0) {
result = await this.handleFileMessage(message, matrixRoomId, actualMatrixUserId, this.serverName);
} else {
const threadRootMessage = await Messages.findOneById(message.tmid);
const threadRootEventId = threadRootMessage?.federation?.eventId;

if (threadRootEventId) {
const latestThreadMessage = await Messages.findOne(
{
'tmid': message.tmid,
'federation.eventId': { $exists: true },
'_id': { $ne: message._id }, // Exclude the current message
},
{ sort: { ts: -1 } },
);
const latestThreadEventId = latestThreadMessage?.federation?.eventId;

if (message.attachments?.some((attachment) => isQuoteAttachment(attachment) && Boolean(attachment.message_link))) {
const quoteMessage = await this.getQuoteMessage(message, matrixRoomId, actualMatrixUserId, this.serverName);
if (!quoteMessage) {
throw new Error('Failed to retrieve quote message');
}
result = await this.homeserverServices.message.sendReplyToInsideThreadMessage(
matrixRoomId,
quoteMessage.rawMessage,
quoteMessage.formattedMessage,
actualMatrixUserId,
threadRootEventId,
quoteMessage.eventToReplyTo,
);
} else {
result = await this.homeserverServices.message.sendThreadMessage(
matrixRoomId,
message.msg,
parsedMessage,
actualMatrixUserId,
threadRootEventId,
latestThreadEventId,
);
}
} else {
this.logger.warn('Thread root event ID not found, sending as regular message');
if (message.attachments?.some((attachment) => isQuoteAttachment(attachment) && Boolean(attachment.message_link))) {
const quoteMessage = await this.getQuoteMessage(message, matrixRoomId, actualMatrixUserId, this.serverName);
if (!quoteMessage) {
throw new Error('Failed to retrieve quote message');
}
result = await this.homeserverServices.message.sendReplyToMessage(
matrixRoomId,
quoteMessage.rawMessage,
quoteMessage.formattedMessage,
quoteMessage.eventToReplyTo,
actualMatrixUserId,
);
} else {
result = await this.homeserverServices.message.sendMessage(matrixRoomId, message.msg, parsedMessage, actualMatrixUserId);
}
}
result = await this.handleTextMessage(message, matrixRoomId, actualMatrixUserId, this.serverName);
}

if (!result) {
Expand Down
Loading
Loading