Skip to content

Conversation

@ricardogarim
Copy link
Contributor

@ricardogarim ricardogarim commented Sep 1, 2025

Works along with #36842.

Summary by CodeRabbit

  • New Features

    • Federation media support: send/receive images, video, audio, files and attachments; threaded and quote-aware federated messages.
    • Media download and thumbnail endpoints plus routing.
    • Media management service for federated file download, storage and MXC handling.
    • New helpers to improve federated thread and quote resolution.
  • Security

    • Media endpoints enforce authorization checks.
  • Bug Fixes

    • End-to-end decryption now runs only for recognized encrypted messages, reducing decryption errors.

@ricardogarim ricardogarim requested review from a team as code owners September 1, 2025 11:23
@dionisio-bot
Copy link
Contributor

dionisio-bot bot commented Sep 1, 2025

Looks like this PR is not ready to merge, because of the following issues:

  • This PR is missing the 'stat: QA assured' label
  • This PR is missing the required milestone or project

Please fix the issues and try again

If you have any trouble, please check the PR guidelines

@changeset-bot
Copy link

changeset-bot bot commented Sep 1, 2025

⚠️ No Changeset found

Latest commit: 32e15a3

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@codecov
Copy link

codecov bot commented Sep 1, 2025

Codecov Report

❌ Patch coverage is 75.00000% with 1 line in your changes missing coverage. Please review.
⚠️ Please upload report for BASE (chore/federation-backup@76470a2). Learn more about missing BASE report.

Additional details and impacted files

Impacted file tree graph

@@                    Coverage Diff                     @@
##             chore/federation-backup   #36842   +/-   ##
==========================================================
  Coverage                           ?   69.86%           
==========================================================
  Files                              ?     3033           
  Lines                              ?   103298           
  Branches                           ?    18363           
==========================================================
  Hits                               ?    72166           
  Misses                             ?    29261           
  Partials                           ?     1871           
Flag Coverage Δ
e2e 56.92% <75.00%> (?)
unit 71.45% <ø> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@sampaiodiego sampaiodiego requested a review from a team as a code owner September 2, 2025 18:08
@ricardogarim ricardogarim force-pushed the feat/federation-files-2 branch from b7f0b7f to 5ef5ebb Compare September 8, 2025 12:15
@ricardogarim ricardogarim marked this pull request as draft September 8, 2025 14:23
@ricardogarim ricardogarim force-pushed the feat/federation-files-2 branch 2 times, most recently from b7f0b7f to d46106d Compare September 8, 2025 15:05
@ricardogarim ricardogarim marked this pull request as ready for review September 8, 2025 16:12
@ricardogarim ricardogarim marked this pull request as draft September 11, 2025 15:52
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 11, 2025

Walkthrough

Adds end-to-end Matrix media support and federated file handling: new MatrixMediaService, media routes and middleware, message flow refactors to handle media/threads/quotes, model/type updates for federation-aware uploads/messages, and E2EE type/guard adjustments.

Changes

Cohort / File(s) Summary
Federation matrix core & routing
ee/packages/federation-matrix/src/FederationMatrix.ts, ee/packages/federation-matrix/src/api/_matrix/media.ts, ee/packages/federation-matrix/src/api/middlewares.ts
Adds Matrix media routes (/v1/media/download, /v1/media/thumbnail), canAccessMedia middleware, wiring into Matrix router, and exports fileTypes. Implements multipart download responses and security headers; mounts media routes after federation version routes.
Matrix media service
ee/packages/federation-matrix/src/services/MatrixMediaService.ts
New MatrixMediaService with MXC URI helpers, local lookup, remote download+local upload, buffer retrieval, federation metadata persistence, and exported IRemoteFileReference.
Federation message handling
ee/packages/federation-matrix/src/events/message.ts
Refactors federated message ingestion: federated user/room resolution, thread/quote resolution, detection of media msgtypes via fileTypes, media download/store via MatrixMediaService, and builds payloads including file/files/attachments for persistence.
Server API / service signature
apps/meteor/server/services/messages/service.ts, packages/core-services/src/types/IMessageService.ts
Extends saveMessageFromFederation signature to accept optional file, files, and attachments (forwarded into message persistence).
Models: uploads & messages
packages/models/src/models/Uploads.ts, packages/model-typings/src/models/IUploadsModel.ts, packages/models/src/models/Messages.ts, packages/model-typings/src/models/IMessagesModel.ts
Adds sparse unique compound index on federation.mediaId+federation.serverName; adds findByFederationMediaIdAndServerName and setFederationInfo to uploads model; adds findLatestFederationThreadMessageByTmid to messages model.
Core typings
packages/core-typings/src/IUpload.ts, packages/core-typings/src/IMessage/IMessage.ts
Adds optional federation block to IUpload (mxcUri, serverName, mediaId); adds EncryptedMessageContent type and isEncryptedMessageContent runtime guard.
Client E2EE adjustments
apps/meteor/client/lib/e2ee/rocketchat.e2e.room.ts, apps/meteor/client/lib/e2ee/rocketchat.e2e.ts
Narrows decryptContent generic to EncryptedMessageContent and uses isEncryptedMessageContent to guard decryption calls.
Service API types
packages/core-services/src/types/IMessageService.ts
Updated saveMessageFromFederation type to include optional file, files, attachments.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant RC as Rocket.Chat (send)
  participant FM as FederationMatrix
  participant MMS as MatrixMediaService
  participant MX as Remote Matrix HS

  RC->>FM: sendMessage({ msg, file?/files?/attachments? })
  alt contains media
    FM->>MMS: downloadAndStoreRemoteFile(mxcUri, metadata) / prepareLocalFileForMatrix(...)
    MMS-->>FM: local fileId / mxc URI
    FM->>MX: send m.room.message with mxc URI and msgtype (mapped)
  else text / thread / quote
    FM->>MX: send text/thread/quote message
  end
  MX-->>FM: event ack
  FM->>RC: persist via saveMessageFromFederation(..., file?/files?/attachments?)
Loading
sequenceDiagram
  autonumber
  participant Client as Remote Matrix HS
  participant API as Media Route (/v1/media/download/:mediaId)
  participant FM as FederationMatrix
  participant MMS as MatrixMediaService
  participant DB as Uploads Model

  Client->>API: GET /v1/media/download/:mediaId (Authorization)
  API->>FM: canAccessMedia(auth, mediaId, path)
  alt authorized
    API->>MMS: getLocalFileForMatrixNode(mediaId, serverName)
    MMS->>DB: findByFederationMediaIdAndServerName(...)
    MMS-->>API: file + buffer
    API-->>Client: 200 multipart/mixed (metadata + file)
  else not authorized / not found
    API-->>Client: 4xx/404/500 Matrix error
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

stat: ready to merge, stat: QA assured

Suggested reviewers

  • sampaiodiego

Poem

A rabbit pads through MXC night,
I tuck bytes close and keep them tight.
Threads and thumbnails hop along the lane,
I hum a tiny federation refrain.
🥕✨

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Out of Scope Changes Check ⚠️ Warning The PR contains changes that appear outside the explicit file-upload objectives: client-side E2E decryption modifications (apps/meteor/client/lib/e2ee/rocketchat.e2e.room.ts and apps/meteor/client/lib/e2ee/rocketchat.e2e.ts) and the added EncryptedMessageContent runtime type/guard (packages/core-typings/src/IMessage/IMessage.ts) are E2E-focused and not required to satisfy FDR-101/FDR-96. These E2E-related edits broaden the PR scope and should be reviewed or justified separately from the federation file-upload work. Split the E2E/decryption changes into a separate PR or add a clear justification in this PR describing why they must be included with the federation file work, and ensure they have dedicated tests and review attention.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title "feat: adds federation files support" is concise, focused on the primary feature, and accurately summarizes the main change (adding federation file/media handling across Matrix integration, media routes, services, and message handling) without listing files or implementation details. It is short, clear, and appropriate for a feature PR.
Linked Issues Check ✅ Passed The changes implement both inbound (homeserver → Rocket.Chat) and outbound (Rocket.Chat → homeserver) file flows referenced by FDR-101 and FDR-96: inbound handling adds MatrixMediaService.downloadAndStoreRemoteFile plus event/message processing and saveMessageFromFederation support for file/files/attachments, while outbound handling adds MatrixMediaService.prepareLocalFileForMatrix, MXC mapping persistence, and send paths that produce Matrix file events; Uploads model methods/indexes and media routes/middleware support the required persistence and access control. Based on the provided summaries, the core coding requirements for file transfer in both directions are implemented.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/federation-files-2

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 8

♻️ Duplicate comments (1)
packages/core-typings/src/IUpload.ts (1)

62-66: Clarify contract: if federation exists, all fields should exist.

Current typing makes subfields optional, but all usages set all three and queries rely on them. Consider making them required for consistency (or document partial states).

Proposed:

-	federation?: {
-		mxcUri?: string;
-		serverName?: string;
-		mediaId?: string;
-	};
+	federation?: {
+		mxcUri: string;
+		serverName: string;
+		mediaId: string;
+	};

If partials are expected, add a brief JSDoc describing which combinations are valid.

🧹 Nitpick comments (15)
apps/meteor/server/services/messages/service.ts (1)

98-106: Allow media-only federated messages (make msg optional).

Federated events may carry attachments without text. executeSendMessage already tolerates missing msg; loosen the typing here to avoid forcing empty strings upstream.

-		msg: string;
+		msg?: string;
packages/core-services/src/types/IMessageService.ts (1)

18-21: Match service typing to accept media-only messages.

Mirror the service implementation suggestion: make msg optional to support attachment-only federation events.

-		msg,
+		msg,
 		federation_event_id,
 		tmid,
 		file,
 		files,
 		attachments,
 	}: {
 		fromId: string;
 		rid: string;
-		msg: string;
+		msg?: string;
 		federation_event_id: string;
 		tmid?: string;
 		file?: IMessage['file'];
 		files?: IMessage['files'];
 		attachments?: IMessage['attachments'];

Also applies to: 27-30

packages/model-typings/src/models/IUploadsModel.ts (1)

18-23: Return type consistency for setFederationInfo.

UploadsRaw.setFederationInfo uses updateOne, which returns UpdateResult. Consider narrowing the interface return type to UpdateResult for clarity.

-	setFederationInfo(fileId: string, info: { mxcUri: string; serverName: string; mediaId: string }): Promise<Document | UpdateResult>;
+	setFederationInfo(fileId: string, info: { mxcUri: string; serverName: string; mediaId: string }): Promise<UpdateResult>;
packages/models/src/models/Uploads.ts (1)

58-63: Optional: validate info completeness at runtime.

Type enforces strings, but adding a quick guard prevents partial writes from callers compiled with looser JS.

-	setFederationInfo(fileId: string, info: { mxcUri: string; serverName: string; mediaId: string }): Promise<Document | UpdateResult> {
+	setFederationInfo(fileId: string, info: { mxcUri: string; serverName: string; mediaId: string }): Promise<Document | UpdateResult> {
+		if (!info?.mxcUri || !info?.serverName || !info?.mediaId) {
+			throw new Error('Invalid federation info');
+		}
 		return this.updateOne(
 			{ _id: fileId },
 			{ $set: { 'federation.mxcUri': info.mxcUri, 'federation.serverName': info.serverName, 'federation.mediaId': info.mediaId } },
 		);
 	}
ee/packages/federation-matrix/src/api/_matrix/media.ts (5)

47-66: Include minimal metadata in multipart header.

You already support a JSON part; include filename/mimeType to help clients.

-const multipartResponse = createMultipartResponse(buffer, mimeType, fileName);
+const multipartResponse = createMultipartResponse(buffer, mimeType, fileName, { fileName, mimeType });

82-132: Add security headers to error responses too.

404/500 branches return JSON without SECURITY_HEADERS; align responses.

- return { statusCode: 404, body: { errcode: 'M_NOT_FOUND', error: 'Media not found' } };
+ return { statusCode: 404, headers: SECURITY_HEADERS, body: { errcode: 'M_NOT_FOUND', error: 'Media not found' } };
...
- return { statusCode: 500, body: { errcode: 'M_UNKNOWN', error: 'Internal server error' } };
+ return { statusCode: 500, headers: SECURITY_HEADERS, body: { errcode: 'M_UNKNOWN', error: 'Internal server error' } };

135-183: Thumbnail route serves full media; confirm intent.

Current implementation returns the original file, not a resized thumbnail. If this is a placeholder, add a TODO or wire it to a real thumbnailer; otherwise, rename route to avoid confusion.


145-145: Type the handler context.

Avoid any; use the Router’s context type for consistency.

- async (context: any) => {
+ async (c) => {
+   // c has the same typed shape as in the first route

68-76: Consider streaming large files instead of buffering.

Reading full buffers can be memory-heavy for big media. Prefer a readable stream piped to the response with chunked transfer.

I can draft a streaming version using Upload.getReadStream if desired.

Also applies to: 113-121, 163-171

ee/packages/federation-matrix/src/services/MatrixMediaService.ts (1)

41-66: Server name source of truth.

You pass serverName in; consider reading it from homeserverServices.config.serverName to avoid mismatches and simplify call sites.

ee/packages/federation-matrix/src/FederationMatrix.ts (2)

242-250: Msgtype mapping is clear; consider a tiny map for readability.

Current checks are fine. Optionally use a map to avoid repeated startsWith.

const typeMap: [prefix: string, t: 'm.image'|'m.video'|'m.audio'][] = [
  ['image/', 'm.image'], ['video/', 'm.video'], ['audio/', 'm.audio'],
];
return typeMap.find(([p]) => fileType?.startsWith(p))?.[1] ?? 'm.file';

252-285: Only the first attached file is sent. Support multi-file messages or document the constraint.

If Matrix supports multiple file events per RC message, iterate all files and send each; return the last event id.

-    const file = message.files[0];
-    const mxcUri = await MatrixMediaService.prepareLocalFileForMatrix(file._id, matrixDomain);
-    const msgtype = this.determineFileMessageType(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);
+    let result: { eventId: string } | null = null;
+    for (const file of message.files) {
+      const mxcUri = await MatrixMediaService.prepareLocalFileForMatrix(file._id, matrixDomain);
+      const msgtype = this.determineFileMessageType(file.type);
+      const fileContent = {
+        body: file.name,
+        msgtype,
+        url: mxcUri,
+        info: { mimetype: file.type, size: file.size },
+      };
+      result = await this.homeserverServices.message.sendFileMessage(matrixRoomId, fileContent, matrixUserId);
+    }
+    return result;

Do we want to send all attachments or just the first one for MVP?

ee/packages/federation-matrix/src/events/message.ts (3)

149-157: Extension derivation: handle empty/unknown MIME robustly.

You already map jpegjpg. Also guard when neither filename nor MIME yields an extension.

-} else if (mimeType && mimeType.includes('/')) {
+} else if (mimeType && mimeType.includes('/')) {
   fileExtension = mimeType.split('/')[1] || '';
   if (fileExtension === 'jpeg') {
     fileExtension = 'jpg';
   }
+} else {
+  fileExtension = '';
}

229-242: Deduplicate relates_to extraction.

Both replyToRelation and threadRelation read the same path. Extract once to reduce mistakes.

- const replyToRelation = content?.['m.relates_to'];
- const threadRelation = content?.['m.relates_to'];
+ const relatesTo = content?.['m.relates_to'];
+ const replyToRelation = relatesTo;
+ const threadRelation = relatesTo;

325-342: Media branch relies on messageBody; safe after fallback fix.

Once the fallback filename is in place, this path is solid. Also consider logging content.url if download fails for easier triage.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6dc2bd9 and c04b7b3.

⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (11)
  • apps/meteor/server/services/messages/service.ts (1 hunks)
  • ee/packages/federation-matrix/src/FederationMatrix.ts (6 hunks)
  • ee/packages/federation-matrix/src/api/_matrix/media.ts (1 hunks)
  • ee/packages/federation-matrix/src/events/message.ts (3 hunks)
  • ee/packages/federation-matrix/src/services/MatrixMediaService.ts (1 hunks)
  • packages/core-services/src/types/IMessageService.ts (1 hunks)
  • packages/core-typings/src/IUpload.ts (1 hunks)
  • packages/model-typings/src/models/IMessagesModel.ts (1 hunks)
  • packages/model-typings/src/models/IUploadsModel.ts (2 hunks)
  • packages/models/src/models/Messages.ts (1 hunks)
  • packages/models/src/models/Uploads.ts (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (8)
ee/packages/federation-matrix/src/services/MatrixMediaService.ts (2)
packages/core-typings/src/IUpload.ts (1)
  • IUpload (3-67)
packages/core-services/src/index.ts (1)
  • Upload (175-175)
ee/packages/federation-matrix/src/api/_matrix/media.ts (2)
packages/core-typings/src/IUpload.ts (1)
  • IUpload (3-67)
ee/packages/federation-matrix/src/services/MatrixMediaService.ts (1)
  • MatrixMediaService (18-144)
packages/model-typings/src/models/IMessagesModel.ts (1)
packages/core-typings/src/IRoom.ts (1)
  • IRoom (21-95)
ee/packages/federation-matrix/src/FederationMatrix.ts (4)
ee/packages/federation-matrix/src/services/MatrixMediaService.ts (1)
  • MatrixMediaService (18-144)
ee/packages/federation-matrix/src/api/_matrix/media.ts (1)
  • getMatrixMediaRoutes (78-185)
packages/core-typings/src/IMessage/IMessage.ts (1)
  • IMessage (138-238)
ee/packages/federation-matrix/src/helpers/message.parsers.ts (1)
  • toExternalMessageFormat (201-212)
apps/meteor/server/services/messages/service.ts (2)
packages/apps-engine/src/definition/messages/IMessage.ts (1)
  • IMessage (11-36)
apps/meteor/app/lib/server/methods/sendMessage.ts (1)
  • executeSendMessage (22-115)
packages/models/src/models/Uploads.ts (1)
packages/core-typings/src/IUpload.ts (1)
  • IUpload (3-67)
packages/model-typings/src/models/IUploadsModel.ts (1)
packages/core-typings/src/IUpload.ts (1)
  • IUpload (3-67)
ee/packages/federation-matrix/src/events/message.ts (4)
packages/core-typings/src/IUser.ts (1)
  • IUser (186-252)
packages/core-typings/src/IRoom.ts (1)
  • IRoom (21-95)
ee/packages/federation-matrix/src/services/MatrixMediaService.ts (1)
  • MatrixMediaService (18-144)
ee/packages/federation-matrix/src/helpers/message.parsers.ts (2)
  • toInternalQuoteMessageFormat (125-151)
  • toInternalMessageFormat (59-73)
🪛 GitHub Actions: CI
ee/packages/federation-matrix/src/services/MatrixMediaService.ts

[error] 114-114: Step '@rocket.chat/federation-matrix:build' failed: TypeScript error TS2339: Property 'downloadFromRemoteServer' does not exist on type 'MediaService'.

🔇 Additional comments (4)
apps/meteor/server/services/messages/service.ts (1)

113-116: LGTM: safely forwarding media fields.

Conditional spreads for file/files/attachments are clean and preserve existing validations in executeSendMessage.

packages/model-typings/src/models/IUploadsModel.ts (1)

2-2: Import looks good.

mongodb UpdateResult import is needed for the new setter’s return type.

ee/packages/federation-matrix/src/services/MatrixMediaService.ts (1)

116-126: ```shell
#!/bin/bash
set -euo pipefail

echo "=== Searching for literal 'federation' ==="
rg -n --hidden --no-ignore -S '\bfederation\b' || true

echo "=== Searching for Upload.uploadFile usages ==="
rg -n --hidden --no-ignore -S 'Upload.uploadFile' || true

echo "=== Searching for uploadFile occurrences/definitions ==="
rg -n --hidden --no-ignore -S '\buploadFile\b' || true
rg -n --hidden --no-ignore -S 'function\s+uploadFile|export\s+function\s+uploadFile|module.exports.uploadFile|exports.uploadFile' || true

echo "=== Searching for creation of a 'federation' user or system user ==="
rg -n --hidden --no-ignore -S 'createUser(|Meteor.users.insert|Users.insert|RocketChat.models.Users|username.*federation|username.*system|username.*rocket.cat' || true

echo "=== Searching for constants like DEFAULT_FEDERATION_USER or SYSTEM_USER ==="
rg -n --hidden --no-ignore -S 'DEFAULT_.*USER|SYSTEM_USER|FEDERATION_USER|federationUser' || true


</blockquote></details>
<details>
<summary>ee/packages/federation-matrix/src/FederationMatrix.ts (1)</summary><blockquote>

`171-173`: **Media routes wiring looks correct.**

Mounting versions and media routes under `/_matrix` is consistent with the new service.

</blockquote></details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (6)
ee/packages/federation-matrix/src/api/middlewares.ts (6)

29-29: Remove now-unused variable after URI construction change

query is no longer used once uriForSignature is built from c.req.url.

Apply this diff:

-        const query = c.req.query();

38-38: Avoid consuming the request body in middleware; clone and read only when needed

c.req.json() consumes the stream and will break downstream handlers for non-GET routes; it also fails for non-JSON bodies and HEAD requests.

Apply this diff:

-            const body = method === 'GET' ? undefined : await c.req.json();
+            let body: string | undefined;
+            if (!['GET', 'HEAD'].includes(method)) {
+                const raw = c.req.raw;
+                if (raw.body) {
+                    // Read a clone so downstream can still consume the original body.
+                    body = await raw.clone().text();
+                }
+            }

If EventAuthorizationService expects a parsed object instead of the raw string, confirm and adjust accordingly.


23-24: Type the middleware and await next()

Return MiddlewareHandler for better typing and await next() for consistency with Hono patterns.

Apply this diff (plus import):

-import type { Context, Next } from 'hono';
+import type { Context, Next, MiddlewareHandler } from 'hono';

-export const canAccessMedia = (federationAuth: EventAuthorizationService) => {
-    return async (c: Context, next: Next) => {
+export const canAccessMedia = (federationAuth: EventAuthorizationService): MiddlewareHandler => {
+    return async (c: Context, next: Next) => {

And later:

-            return next();
+            await next();
+            return;

25-26: Validate mediaId early

Defensive check helps avoid confusing downstream errors if the route is misconfigured or the param is missing.

Apply this diff:

         const mediaId = c.req.param('mediaId');
+        if (!mediaId) {
+            return c.json({ errcode: 'M_INVALID_PARAM', error: 'Missing mediaId' }, 400);
+        }

5-21: Optional: strengthen errCodes typing and extendable mapping

Lock keys/types and make it easy to add codes like M_INVALID_PARAM without losing type safety.

Apply this diff:

-const errCodes: Record<string, { errcode: string; error: string; status: ContentfulStatusCode }> = {
+const errCodes = {
     M_UNAUTHORIZED: {
         errcode: 'M_UNAUTHORIZED',
         error: 'Invalid or missing signature',
         status: 401,
     },
     M_FORBIDDEN: {
         errcode: 'M_FORBIDDEN',
         error: 'Access denied',
         status: 403,
     },
+    M_INVALID_PARAM: {
+        errcode: 'M_INVALID_PARAM',
+        error: 'Missing or invalid parameter',
+        status: 400,
+    },
     M_UNKNOWN: {
         errcode: 'M_UNKNOWN',
         error: 'Internal server error while processing request',
         status: 500,
     },
-};
+} as const satisfies Record<string, { errcode: string; error: string; status: ContentfulStatusCode }>;

60-62: Log failures for observability

Surface authorization failures and unexpected errors to your federation/media logger with request id/media id. Helps ops triage auth/signature issues.

If you have a logger on Context (e.g., c.get('logger')), add a log here before returning the error.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c04b7b3 and 620670b.

📒 Files selected for processing (3)
  • ee/packages/federation-matrix/src/api/_matrix/media.ts (1 hunks)
  • ee/packages/federation-matrix/src/api/middlewares.ts (1 hunks)
  • ee/packages/federation-matrix/src/services/MatrixMediaService.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • ee/packages/federation-matrix/src/services/MatrixMediaService.ts
  • ee/packages/federation-matrix/src/api/_matrix/media.ts
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: 📦 Build Packages
  • GitHub Check: CodeQL-Build
  • GitHub Check: CodeQL-Build

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (3)
ee/packages/federation-matrix/src/api/_matrix/media.ts (3)

101-101: Verify: File retrieval is limited to the configured server name.

The function only retrieves files for the configured serverName, which means it won't serve files originated from other federated servers. This may be intentional for security, but please verify if this aligns with federation requirements.

#!/bin/bash
# Description: Check if there are other media retrieval patterns in the codebase that handle cross-server federation

# Search for other media file retrieval patterns in federation code
rg -n -A3 -B3 "getLocalFileForMatrixNode|downloadFromRemoteServer" --type ts

# Look for federation media handling patterns
ast-grep --pattern 'downloadAndStoreRemoteFile($$$)'

134-150: Return appropriate error code for unimplemented thumbnail endpoint.

The current implementation returns 404 with M_UNRECOGNIZED. Based on the Matrix spec and previous review discussions, consider using 413 status code with an appropriate error message for content that cannot be thumbnailed.

-		async () => ({
-			statusCode: 404,
-			body: {
-				errcode: 'M_UNRECOGNIZED',
-				error: 'This endpoint is not implemented on the homeserver side',
-			},
-		}),
+		async () => ({
+			statusCode: 413,
+			body: {
+				errcode: 'M_TOO_LARGE',
+				error: 'The local content is too large for the server to thumbnail',
+			},
+		}),

100-109: Address TODO: Implement file streaming to avoid memory issues.

Loading entire files into memory can cause OOM errors with large media files. This is a critical issue that needs to be addressed before production deployment.

Would you like me to generate a streaming implementation that efficiently handles large files without loading them entirely into memory?

🧹 Nitpick comments (2)
ee/packages/federation-matrix/src/api/_matrix/media.ts (2)

111-112: Security: Validate MIME type to prevent content-type attacks.

The MIME type from the database is used directly in the response without validation. A malicious or incorrect MIME type could lead to security issues.

-				const mimeType = file.type || 'application/octet-stream';
+				// Validate and sanitize MIME type
+				const rawMimeType = file.type || 'application/octet-stream';
+				// Basic MIME type validation: type/subtype format
+				const mimeType = /^[a-zA-Z0-9][a-zA-Z0-9\-]*\/[a-zA-Z0-9][a-zA-Z0-9\-+.]*$/.test(rawMimeType) 
+					? rawMimeType 
+					: 'application/octet-stream';

126-130: Improve error handling with proper logging.

Generic error handling without logging makes debugging difficult. Consider logging the actual error for monitoring purposes.

 			} catch (error) {
+				console.error('Error handling media download request:', error);
 				return {
 					statusCode: 500,
 					body: { errcode: 'M_UNKNOWN', error: 'Internal server error' },
 				};
 			}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 07577d8 and 7fef87d.

📒 Files selected for processing (2)
  • ee/packages/federation-matrix/src/FederationMatrix.ts (6 hunks)
  • ee/packages/federation-matrix/src/api/_matrix/media.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • ee/packages/federation-matrix/src/FederationMatrix.ts
🧰 Additional context used
🧬 Code graph analysis (1)
ee/packages/federation-matrix/src/api/_matrix/media.ts (3)
packages/core-typings/src/IUpload.ts (1)
  • IUpload (3-67)
ee/packages/federation-matrix/src/services/MatrixMediaService.ts (1)
  • MatrixMediaService (18-147)
ee/packages/federation-matrix/src/api/middlewares.ts (1)
  • canAccessMedia (5-32)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: CodeQL-Build
  • GitHub Check: CodeQL-Build
🔇 Additional comments (1)
ee/packages/federation-matrix/src/api/_matrix/media.ts (1)

38-43: LGTM! Good security headers implementation.

The security headers are well-configured and follow best practices for serving media content securely.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (2)
packages/models/src/models/Uploads.ts (1)

14-16: Add sparse indexes for federation lookups (and consider uniqueness).

Current queries on federation.serverName/mediaId will scan. Add indexes; consider unique: true if one remote media maps to exactly one local upload.

 protected modelIndexes(): IndexDescription[] {
-  return [...super.modelIndexes(), { key: { uploadedAt: -1 } }, { key: { rid: 1, _hidden: 1, typeGroup: 1 } }];
+  return [
+    ...super.modelIndexes(),
+    { key: { uploadedAt: -1 } },
+    { key: { rid: 1, _hidden: 1, typeGroup: 1 } },
+    // For Matrix federation media lookups
+    { key: { 'federation.serverName': 1, 'federation.mediaId': 1 }, sparse: true /* , unique: true (confirm before enabling) */ },
+    // Optional: enable only if you introduce queries by MXC
+    // { key: { 'federation.mxcUri': 1 }, sparse: true },
+  ];
 }

If you confirm 1:1 mapping, enable unique: true and plan a small migration to dedupe before creating the index.

ee/packages/federation-matrix/src/services/MatrixMediaService.ts (1)

114-117: Build error: downloadFromRemoteServer doesn’t exist on MediaService. Add a compatibility shim and replace the call.

This causes TS2339 in CI. Probe for known method names.

-      const buffer = await this.homeserverServices.media.downloadFromRemoteServer(parts.serverName, parts.mediaId);
+      const buffer = await this.downloadRemoteMedia(parts.serverName, parts.mediaId);

Add this helper inside the class (e.g., after parseMXCUri):

private static async downloadRemoteMedia(serverName: string, mediaId: string): Promise<Buffer> {
  if (!this.homeserverServices?.media) {
    throw new Error('Homeserver media service not initialized. Call setHomeserverServices first.');
  }
  const media: any = this.homeserverServices.media;
  if (typeof media.downloadFromRemoteServer === 'function') {
    return media.downloadFromRemoteServer(serverName, mediaId);
  }
  if (typeof media.downloadRemote === 'function') {
    return media.downloadRemote(serverName, mediaId);
  }
  if (typeof media.download === 'function') {
    return media.download(serverName, mediaId);
  }
  throw new Error('Homeserver media service does not expose a download method (expected one of: downloadFromRemoteServer | downloadRemote | download)');
}
🧹 Nitpick comments (6)
packages/models/src/models/Uploads.ts (1)

54-59: Normalize and set federation as a single object (optional).

Minor: set the whole federation object and normalize serverName to lowercase for consistent indexing.

 return this.updateOne(
   { _id: fileId },
-  { $set: { 'federation.mxcUri': info.mxcUri, 'federation.serverName': info.serverName, 'federation.mediaId': info.mediaId } },
+  {
+    $set: {
+      federation: {
+        mxcUri: info.mxcUri,
+        serverName: info.serverName.toLowerCase(),
+        mediaId: info.mediaId,
+      },
+    },
+  },
 );

If you adopt normalization, also lower-case in all finders.

ee/packages/federation-matrix/src/services/MatrixMediaService.ts (5)

29-39: Normalize serverName in MXC parsing (minor).

Lower-case serverName to avoid index misses due to case variance.

- return {
-   serverName: match[1],
-   mediaId: match[2],
- };
+ return {
+   serverName: match[1].toLowerCase(),
+   mediaId: match[2],
+ };

119-129: Add size/MIME guardrails before persisting remote media.

Enforce configured limits to prevent oversized/disallowed uploads.

       const uploadedFile = await Upload.uploadFile({
         userId: metadata.userId || 'federation',
-        buffer,
+        buffer,
         details: {
           name: metadata.name || 'unnamed',
-          size: buffer.length,
-          type: metadata.type || 'application/octet-stream',
+          size: buffer.length,
+          type: metadata.type || 'application/octet-stream',
           rid: metadata.roomId,
           userId: metadata.userId || 'federation',
         },
       });

Insert just before Upload.uploadFile:

const limits = (this.homeserverServices as any)?.config?.media;
if (limits?.maxFileSize && buffer.length > limits.maxFileSize) {
  throw new Error(`Remote media exceeds max file size (${buffer.length} > ${limits.maxFileSize})`);
}
if (limits?.allowedMimeTypes && metadata.type && !limits.allowedMimeTypes.includes(metadata.type)) {
  throw new Error(`MIME type "${metadata.type}" not allowed by configuration`);
}

119-129: Pass message linkage if supported by the upload service.

IUpload uses message_id; wire it if available.

         details: {
           name: metadata.name || 'unnamed',
           size: buffer.length,
           type: metadata.type || 'application/octet-stream',
           rid: metadata.roomId,
           userId: metadata.userId || 'federation',
+          // If supported by Upload.uploadFile details:
+          // message_id: metadata.messageId,
         },

Confirm the expected key is message_id (underscore) vs messageId.


105-137: Handle duplicate races with a unique index fallback.

Two workers can download the same remote media concurrently. After adding a unique sparse index on (federation.serverName, federation.mediaId), catch E11000 and return the existing upload id.

try {
  await Uploads.setFederationInfo(uploadedFile._id, { mxcUri, serverName: parts.serverName, mediaId: parts.mediaId });
  return uploadedFile._id;
} catch (e: any) {
  if (e?.code === 11000) {
    const existing = await Uploads.findByFederationMediaIdAndServerName(parts.mediaId, parts.serverName);
    if (existing?._id && existing._id !== uploadedFile._id) {
      // Optional: decide whether to keep or clean up the duplicate file content
      return existing._id;
    }
  }
  throw e;
}

144-146: Provide a streaming variant for file reads.

Add getLocalFileStream to avoid buffering whole files when responding over HTTP.

static async getLocalFileStream(file: IUpload): Promise<NodeJS.ReadableStream> {
  return Upload.getReadStream({ file });
}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7fef87d and 3289203.

📒 Files selected for processing (3)
  • ee/packages/federation-matrix/src/services/MatrixMediaService.ts (1 hunks)
  • packages/model-typings/src/models/IUploadsModel.ts (2 hunks)
  • packages/models/src/models/Uploads.ts (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/model-typings/src/models/IUploadsModel.ts
🧰 Additional context used
🧬 Code graph analysis (2)
packages/models/src/models/Uploads.ts (1)
packages/core-typings/src/IUpload.ts (1)
  • IUpload (3-67)
ee/packages/federation-matrix/src/services/MatrixMediaService.ts (2)
packages/core-typings/src/IUpload.ts (1)
  • IUpload (3-67)
packages/core-services/src/index.ts (1)
  • Upload (175-175)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: 📦 Build Packages
  • GitHub Check: Builds matrix rust bindings against alpine
  • GitHub Check: CodeQL-Build
  • GitHub Check: CodeQL-Build
🔇 Additional comments (3)
packages/models/src/models/Uploads.ts (1)

50-52: LGTM: targeted finder for (serverName, mediaId).

ee/packages/federation-matrix/src/services/MatrixMediaService.ts (2)

19-23: Static DI setup looks fine.


114-114: No other downloadFromRemoteServer call sites found. ripgrep returned a single match at ee/packages/federation-matrix/src/services/MatrixMediaService.ts:114 — no other matches under ee/ or packages/.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (3)
ee/packages/federation-matrix/src/services/MatrixMediaService.ts (3)

87-143: Consider streaming approach for large files.

The current implementation buffers entire files in memory, which can cause memory issues for large media. Consider implementing streaming to handle large files more efficiently.

As noted in the TODO comment and previous discussions, this should be refactored to use streaming:

// Future implementation sketch:
// const stream = await this.downloadRemoteMediaStream(parts.serverName, parts.mediaId);
// await Upload.uploadFileStream({ userId, stream, details: { ... } });

Consider creating a follow-up task to implement streaming support in both the download and upload pipelines.


114-114: Fix build: replace non-existent downloadFromRemoteServer with a compatibility shim.

The build fails with TS2339 because MediaService doesn't expose downloadFromRemoteServer. Add a helper method that probes for available download methods.

Apply this diff to fix the method call:

-			const buffer = await this.homeserverServices.media.downloadFromRemoteServer(parts.serverName, parts.mediaId);
+			const buffer = await this.downloadRemoteMedia(parts.serverName, parts.mediaId);

Add this helper method after parseMXCUri (around line 40):

	private static async downloadRemoteMedia(serverName: string, mediaId: string): Promise<Buffer> {
		if (!this.homeserverServices?.media) {
			throw new Error('Homeserver media service not initialized. Call setHomeserverServices first.');
		}
		const media: any = this.homeserverServices.media;
		if (typeof media.downloadFromRemoteServer === 'function') {
			return await media.downloadFromRemoteServer(serverName, mediaId);
		}
		if (typeof media.downloadRemote === 'function') {
			return await media.downloadRemote(serverName, mediaId);
		}
		if (typeof media.download === 'function') {
			return await media.download(serverName, mediaId);
		}
		throw new Error('Homeserver media service does not expose a download method (expected one of: downloadFromRemoteServer | downloadRemote | download)');
	}

114-118: Add media size and MIME type validation before storing.

Validate downloaded media against configured limits to prevent storing oversized or disallowed file types.

Apply this diff after obtaining the buffer:

 			const buffer = await this.downloadRemoteMedia(parts.serverName, parts.mediaId);
 			if (!buffer) {
 				throw new Error('Download from remote server returned null content.');
 			}
+
+			const limits = this.homeserverServices?.config?.media;
+			if (limits?.maxFileSize && buffer.length > limits.maxFileSize) {
+				throw new Error(`Remote media exceeds max file size (${buffer.length} > ${limits.maxFileSize})`);
+			}
+			if (limits?.allowedMimeTypes && metadata.type && !limits.allowedMimeTypes.includes(metadata.type)) {
+				throw new Error(`MIME type "${metadata.type}" not allowed by configuration`);
+			}
🧹 Nitpick comments (3)
ee/packages/federation-matrix/src/services/MatrixMediaService.ts (3)

25-27: Validate serverName parameter in MXC URI generation.

The generateMXCUri method should validate the serverName to prevent malformed URIs.

Add validation to ensure serverName doesn't contain invalid characters:

 	static generateMXCUri(fileId: string, serverName: string): string {
+		if (!serverName || serverName.includes('/')) {
+			throw new Error('Invalid serverName for MXC URI generation');
+		}
 		return `mxc://${serverName}/${fileId}`;
 	}

121-129: Remove duplicate userId field in upload details.

The userId field is specified twice in the details object - once on line 122 and again on line 128.

Apply this diff to remove the duplicate:

 			const uploadedFile = await Upload.uploadFile({
 				userId: metadata.userId || 'federation',
 				buffer,
 				details: {
 					name: metadata.name || 'unnamed',
 					size: buffer.length,
 					type: metadata.type || 'application/octet-stream',
 					rid: metadata.roomId,
-					userId: metadata.userId || 'federation',
 				},
 			});

41-66: Add federation domain validation in prepareLocalFileForMatrix.

When creating new federation metadata, validate that the serverName matches expected federation domains to prevent potential security issues.

Consider adding domain validation before creating the MXC URI:

 			const mxcUri = this.generateMXCUri(fileId, serverName);
+
+			// Validate serverName against allowed federation domains if configured
+			const allowedDomains = this.homeserverServices?.config?.federation?.allowedDomains;
+			if (allowedDomains && !allowedDomains.includes(serverName)) {
+				throw new Error(`Server ${serverName} not in allowed federation domains`);
+			}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c3e7d7d and 1bc9b97.

📒 Files selected for processing (1)
  • ee/packages/federation-matrix/src/services/MatrixMediaService.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
ee/packages/federation-matrix/src/services/MatrixMediaService.ts (2)
packages/core-typings/src/IUpload.ts (1)
  • IUpload (3-67)
packages/core-services/src/index.ts (1)
  • Upload (175-175)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: 📦 Build Packages
  • GitHub Check: CodeQL-Build
  • GitHub Check: CodeQL-Build
🔇 Additional comments (1)
ee/packages/federation-matrix/src/services/MatrixMediaService.ts (1)

18-24: LGTM! Clean dependency injection pattern.

The static class design with explicit dependency injection via setHomeserverServices provides good separation of concerns and testability.

@sampaiodiego sampaiodiego force-pushed the feat/federation-files-2 branch from f5c636e to b83f6cb Compare September 19, 2025 21:22
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
ee/packages/federation-matrix/src/FederationMatrix.ts (1)

100-105: Bug: enableThumbnails always true.

process.env.MEDIA_ENABLE_THUMBNAILS === 'true' || true forces true.

-				enableThumbnails: process.env.MEDIA_ENABLE_THUMBNAILS === 'true' || true,
+				enableThumbnails: process.env.MEDIA_ENABLE_THUMBNAILS === 'true',

Or default-on: process.env.MEDIA_ENABLE_THUMBNAILS !== 'false'.

ee/packages/federation-matrix/src/events/message.ts (1)

372-388: Resolve sender via bridge mapping, not raw username, for redactions.

data.sender is a Matrix ID; Users.findOneByUsername(sender) will fail once usernames are sanitized.

-			const internalUsername = data.sender;
-			const user = await Users.findOneByUsername(internalUsername);
+			const user = await getOrCreateFederatedUser(data.sender);
 			if (!user) {
-				logger.debug(`User not found: ${internalUsername}`);
+				logger.debug(`User not found for Matrix sender: ${data.sender}`);
 				return;
 			}
♻️ Duplicate comments (8)
ee/packages/federation-matrix/src/api/_matrix/media.ts (3)

100-116: Avoid loading entire files into memory; add streaming now or track.

Reading full Buffer and building multipart in memory won’t scale for large media.

  • Short term: gate by size and return 413 for oversized payloads.
  • Next step: switch to streamed responses (Readable/pipe) instead of Buffer.concat; add a Jira task and link here.

76-79: Guard required services upfront (fail fast, clearer errors).

Validate homeserverServices.config.serverName and federationAuth before use.

 export const getMatrixMediaRoutes = (homeserverServices: HomeserverServices) => {
-	const { config, federationAuth } = homeserverServices;
+	if (!homeserverServices?.config?.serverName) {
+		throw new TypeError('getMatrixMediaRoutes: homeserverServices.config.serverName is required');
+	}
+	if (!homeserverServices?.federationAuth) {
+		throw new TypeError('getMatrixMediaRoutes: homeserverServices.federationAuth is required');
+	}
+	const { config, federationAuth } = homeserverServices;
 	const router = new Router('/federation');

45-65: Sanitize multipart headers to prevent CRLF/header injection.

fileName and metadata are written into MIME headers without sanitization.

Apply:

 function createMultipartResponse(
  	buffer: Buffer,
  	mimeType: string,
  	fileName: string,
  	metadata: Record<string, any> = {},
 ): { body: Buffer; contentType: string } {
 	const boundary = crypto.randomBytes(16).toString('hex');
 	const parts: string[] = [];
 
-	parts.push(`--${boundary}`, 'Content-Type: application/json', '', JSON.stringify(metadata));
-	parts.push(`--${boundary}`, `Content-Type: ${mimeType}`, `Content-Disposition: attachment; filename="${fileName}"`, '');
+	// harden inputs for header context
+	const safeName = String(fileName).replace(/[\r\n"]/g, '');
+	let safeMetadata: unknown = {};
+	try {
+		// drop non-serializable fields
+		safeMetadata = JSON.parse(JSON.stringify(metadata ?? {}));
+	} catch {
+		safeMetadata = {};
+	}
+	parts.push(`--${boundary}`, 'Content-Type: application/json', '', JSON.stringify(safeMetadata));
+	parts.push(`--${boundary}`, `Content-Type: ${mimeType}`, `Content-Disposition: attachment; filename="${safeName}"`, '');
ee/packages/federation-matrix/src/FederationMatrix.ts (1)

569-574: Do not drop user-entered text when files are present.

Send files and then, if message.msg has text, send a separate text event.

-			if (message.files && message.files.length > 0) {
-				result = await this.handleFileMessage(message, matrixRoomId, actualMatrixUserId, this.serverName);
-			} else {
-				result = await this.handleTextMessage(message, matrixRoomId, actualMatrixUserId, this.serverName);
-			}
+			if (message.files && message.files.length > 0) {
+				const fileResult = await this.handleFileMessage(message, matrixRoomId, actualMatrixUserId, this.serverName);
+				if (message.msg && message.msg.trim().length > 0) {
+					// fire-and-forget text message; result maps to file event
+					void this.handleTextMessage(message, matrixRoomId, actualMatrixUserId, this.serverName);
+				}
+				result = fileResult;
+			} else {
+				result = await this.handleTextMessage(message, matrixRoomId, actualMatrixUserId, this.serverName);
+			}
ee/packages/federation-matrix/src/events/message.ts (3)

229-232: Don’t drop edit events with no body — compute isEdited before early return.

m.replace with only m.new_content gets ignored today.

-			const msgtype = content?.msgtype;
-			const messageBody = content?.body?.toString();
-
-			if (!messageBody && !msgtype) {
+			const msgtype = content?.msgtype;
+			const messageBody = content?.body?.toString();
+			const isEditedMessage = content?.['m.relates_to']?.rel_type === 'm.replace';
+
+			if (!messageBody && !msgtype && !isEditedMessage) {
 				logger.debug('No message content found in event');
 				return;
 			}
@@
-			const isEditedMessage = data.content['m.relates_to']?.rel_type === 'm.replace';
+			// already computed above

Also applies to: 253-253


15-21: Parse Matrix IDs with first colon only (ports safe).

split(':') drops port (e.g., @U:example.com:8448). Use first colon after '@'.

-	const [userPart, domain] = matrixUserId.split(':');
-	if (!userPart || !domain) {
+	const idx = matrixUserId.indexOf(':', 1); // skip leading '@'
+	const userPart = idx > 0 ? matrixUserId.slice(0, idx) : matrixUserId;
+	const domain = idx > 0 ? matrixUserId.slice(idx + 1) : '';
+	if (!userPart || !domain) {

144-168: Null-safety for media fields; safe filename; avoid encodeURIComponent on undefined.

Guard content.info/body and use deterministic fallback name.

-	const fileInfo = content.info;
-	const mimeType = fileInfo.mimetype;
-	const fileName = messageBody;
+	const fileInfo = content?.info ?? {};
+	const mimeType = fileInfo.mimetype ?? 'application/octet-stream';
+	const fileName = (messageBody && String(messageBody).trim().length > 0) ? String(messageBody) : `${msgtype}-${eventId}`;
@@
-	const fileRefId = await MatrixMediaService.downloadAndStoreRemoteFile(content.url, {
-		name: messageBody,
-		size: fileInfo.size,
-		type: mimeType,
+	const fileRefId = await MatrixMediaService.downloadAndStoreRemoteFile(content.url, {
+		name: fileName,
+		size: typeof fileInfo.size === 'number' ? fileInfo.size : 0,
+		type: mimeType,
packages/models/src/models/Messages.ts (1)

607-618: Add index to support sort by ts on tmid with federation filter.

Current query can in-memory sort. Add a compound partial index.

Apply (outside this hunk) in modelIndexes():

 protected modelIndexes(): IndexDescription[] {
   return [
     ...
+    // threads: latest federated event by tmid
+    { key: { tmid: 1, ts: -1 }, partialFilterExpression: { 'federation.eventId': { $exists: true } } },
   ];
 }
🧹 Nitpick comments (4)
ee/packages/federation-matrix/src/FederationMatrix.ts (2)

251-253: Use logger, not console.log, for errors.

-			console.log(error);
+			this.logger.error(error);

431-455: Multi-file TODO — consider batching.

Matrix supports multiple file sends; iterate files with ordered awaits and aggregate last eventId or store per-file mapping.

ee/packages/federation-matrix/src/events/message.ts (2)

166-173: encodeURIComponent on defined string only.

-	const fileUrl = `/file-upload/${fileRefId}/${encodeURIComponent(fileName)}`;
+	const fileUrl = `/file-upload/${fileRefId}/${encodeURIComponent(String(fileName))}`;

9-10: Avoid cyclic import with FederationMatrix.

Importing fileTypes from ../FederationMatrix creates a module cycle. Move fileTypes/MatrixFileTypes to a small shared constants module and import from there.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between f5c636e and b83f6cb.

📒 Files selected for processing (15)
  • apps/meteor/client/lib/e2ee/rocketchat.e2e.room.ts (3 hunks)
  • apps/meteor/client/lib/e2ee/rocketchat.e2e.ts (2 hunks)
  • apps/meteor/server/services/messages/service.ts (1 hunks)
  • ee/packages/federation-matrix/src/FederationMatrix.ts (6 hunks)
  • ee/packages/federation-matrix/src/api/_matrix/media.ts (1 hunks)
  • ee/packages/federation-matrix/src/api/middlewares.ts (1 hunks)
  • ee/packages/federation-matrix/src/events/message.ts (3 hunks)
  • ee/packages/federation-matrix/src/services/MatrixMediaService.ts (1 hunks)
  • packages/core-services/src/types/IMessageService.ts (1 hunks)
  • packages/core-typings/src/IMessage/IMessage.ts (1 hunks)
  • packages/core-typings/src/IUpload.ts (1 hunks)
  • packages/model-typings/src/models/IMessagesModel.ts (1 hunks)
  • packages/model-typings/src/models/IUploadsModel.ts (2 hunks)
  • packages/models/src/models/Messages.ts (1 hunks)
  • packages/models/src/models/Uploads.ts (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (10)
  • packages/core-typings/src/IMessage/IMessage.ts
  • apps/meteor/client/lib/e2ee/rocketchat.e2e.ts
  • packages/core-services/src/types/IMessageService.ts
  • ee/packages/federation-matrix/src/api/middlewares.ts
  • apps/meteor/server/services/messages/service.ts
  • packages/model-typings/src/models/IUploadsModel.ts
  • ee/packages/federation-matrix/src/services/MatrixMediaService.ts
  • packages/models/src/models/Uploads.ts
  • apps/meteor/client/lib/e2ee/rocketchat.e2e.room.ts
  • packages/core-typings/src/IUpload.ts
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-09-16T22:09:18.041Z
Learnt from: CR
PR: RocketChat/Rocket.Chat#0
File: .cursor/rules/test-cases.mdc:0-0
Timestamp: 2025-09-16T22:09:18.041Z
Learning: Use Rocket.Chat primary documentation and provided reference files for guidance

Applied to files:

  • ee/packages/federation-matrix/src/events/message.ts
🧬 Code graph analysis (5)
packages/models/src/models/Messages.ts (1)
packages/core-typings/src/IMessage/IMessage.ts (1)
  • IMessage (138-238)
packages/model-typings/src/models/IMessagesModel.ts (1)
packages/core-typings/src/IMessage/IMessage.ts (1)
  • IMessage (138-238)
ee/packages/federation-matrix/src/events/message.ts (6)
packages/core-typings/src/IUser.ts (1)
  • IUser (186-253)
packages/core-typings/src/IRoom.ts (1)
  • IRoom (21-95)
ee/packages/federation-matrix/src/services/MatrixMediaService.ts (1)
  • MatrixMediaService (18-148)
ee/packages/federation-matrix/src/FederationMatrix.ts (1)
  • fileTypes (34-39)
apps/meteor/server/services/meteor/service.ts (1)
  • MeteorService (130-300)
ee/packages/federation-matrix/src/helpers/message.parsers.ts (2)
  • toInternalQuoteMessageFormat (125-151)
  • toInternalMessageFormat (59-73)
ee/packages/federation-matrix/src/api/_matrix/media.ts (3)
packages/core-typings/src/IUpload.ts (1)
  • IUpload (3-67)
ee/packages/federation-matrix/src/services/MatrixMediaService.ts (1)
  • MatrixMediaService (18-148)
ee/packages/federation-matrix/src/api/middlewares.ts (1)
  • canAccessMedia (6-33)
ee/packages/federation-matrix/src/FederationMatrix.ts (5)
ee/packages/federation-matrix/src/services/MatrixMediaService.ts (1)
  • MatrixMediaService (18-148)
ee/packages/federation-matrix/src/api/_matrix/media.ts (1)
  • getMatrixMediaRoutes (76-153)
ee/packages/federation-matrix/src/events/message.ts (1)
  • message (221-389)
packages/core-typings/src/IMessage/IMessage.ts (1)
  • IMessage (138-238)
ee/packages/federation-matrix/src/helpers/message.parsers.ts (1)
  • toExternalMessageFormat (201-212)
🔇 Additional comments (5)
ee/packages/federation-matrix/src/api/_matrix/media.ts (2)

116-124: Verify Matrix S2S response shape (multipart vs raw).

Spec for GET /_matrix/federation/v1/media/download/{mediaId} typically serves the raw file with its content-type. Confirm multipart/mixed is acceptable for S2S downloads; otherwise serve the binary directly and move metadata to headers.


134-150: Thumbnail endpoint: status code and errcode OK as stub.

Returning 404 with M_UNRECOGNIZED is acceptable while unimplemented.

Consider 413 (too large to thumbnail) once thumbnailing is added.

packages/model-typings/src/models/IMessagesModel.ts (1)

106-107: Signature aligns with current callers.

tmid + messageId exclusion makes sense for latest thread event lookup.

Ensure all usages pass the current message _id (as in FederationMatrix.ts).

ee/packages/federation-matrix/src/FederationMatrix.ts (2)

114-115: Wiring homeserverServices into MatrixMediaService — LGTM.


412-419: Graceful fallback for unknown MIME types — LGTM.

Comment on lines +23 to +27
const user = await Users.findOneByUsername(matrixUserId);
if (user) {
await MatrixBridgedUser.createOrUpdateByLocalId(user._id, matrixUserId, false, domain);
return user;
}
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

Lookup by sanitized local username or bridge mapping, not raw Matrix ID.

Users.findOneByUsername(matrixUserId) mismatches RC username rules.

  • Use MatrixBridgedUser to resolve/create the local user by external ID.
  • If keeping local usernames, derive a validated local username (no '@' or ':') and use that for lookups; keep the Matrix ID only in the bridge mapping.
🤖 Prompt for AI Agents
In ee/packages/federation-matrix/src/events/message.ts around lines 23-27, don't
call Users.findOneByUsername(matrixUserId) with the raw Matrix ID; instead
resolve/create the local user via the bridge mapping or a sanitized local
username. Replace the direct Users lookup with a MatrixBridgedUser call that
finds or creates the local account from the external Matrix ID (e.g.,
findOrCreateByExternalId(matrixUserId, domain)) and return the associated local
User; if you must look up by username, first derive and validate a sanitized
local username by stripping leading '@' and any ':domain' portion and validating
against Rocket.Chat username rules before calling Users.findOneByUsername, and
ensure the Matrix ID is stored only in the bridge mapping.

Comment on lines +31 to 48
const userData = {
username: matrixUserId,
name: username, // TODO: Fetch display name from Matrix profile
type: 'user',
status: UserStatus.ONLINE,
active: true,
roles: ['user'],
requirePasswordChange: false,
federated: true,
federation: {
version: 1,
},
createdAt: new Date(),
_updatedAt: new Date(),
};

if (!user) {
logger.info('Creating new federated user:', { username: internalUsername, externalId: data.sender });

const userData: Partial<IUser> = {
username: internalUsername,
name: username, // TODO: Fetch display name from Matrix profile
type: 'user',
status: UserStatus.ONLINE,
active: true,
roles: ['user'],
requirePasswordChange: false,
federated: true, // Mark as federated user
federation: {
version: 1,
},
createdAt: new Date(),
_updatedAt: new Date(),
};
const { insertedId } = await Users.insertOne(userData);

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

Do not insert users directly — use vetted creation flow.

Users.insertOne bypasses validations/hooks/uniqueness.

Replace with the established user-creation API for federation (service/adapter) that enforces validateUsername, uniqueness fallback, roles, and denormalizations. I can draft the exact call once you confirm the preferred factory.

@ggazzo ggazzo force-pushed the chore/federation-backup branch from 5d3431f to 76470a2 Compare September 19, 2025 21:36
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
ee/packages/federation-matrix/src/events/message.ts (1)

355-385: Redaction: resolve the user via bridge mapping (or getOrCreateFederatedUser), not raw username.

Users.findOneByUsername(data.sender) repeats the Matrix‑ID‑as‑username issue and may fail.

-			const internalUsername = data.sender;
-			const user = await Users.findOneByUsername(internalUsername);
+			const user = await getOrCreateFederatedUser(data.sender);
 			if (!user) {
-				logger.debug(`User not found: ${internalUsername}`);
+				logger.debug(`User not found for redaction sender: ${data.sender}`);
 				return;
 			}
♻️ Duplicate comments (4)
ee/packages/federation-matrix/src/events/message.ts (4)

23-27: Do not use raw Matrix IDs as RC usernames; derive a validated, unique local username.

@/: are invalid and cross‑homeserver collisions are likely. Use a sanitized username and keep the Matrix ID in the bridge mapping.

-	const user = await Users.findOneByUsername(matrixUserId);
+	// Derive a local username that avoids collisions across homeservers.
+	const safeDomain = domain.replace(/[^0-9a-zA-Z]/g, '_');
+	const localUsername = `${baseLocalName}_${safeDomain}`; // e.g., "alice_example_com"
+	const user = await Users.findOneByUsername(localUsername);
@@
-	logger.info('Creating new federated user:', { username: matrixUserId, externalId: matrixUserId });
+	logger.info('Creating new federated user:', { username: localUsername, externalId: matrixUserId });
@@
-		username: matrixUserId,
-		name: username, // TODO: Fetch display name from Matrix profile
+		username: localUsername,
+		name: baseLocalName, // TODO: Fetch display name from Matrix profile
@@
-		federation: {
-			version: 1,
-		},
+		// Avoid using deprecated "federation" field

Also applies to: 31-45


47-54: Don’t call Users.insertOne — use the vetted user‑creation flow with validation and uniqueness fallback.

Direct inserts bypass hooks/validation (username regex, denormalizations, roles). Replace with the established federation‑safe creation API.

Run to locate the canonical creation helper:

#!/bin/bash
rg -nP -C2 '(createFederatedUser|createFederatedUserInternallyOnly|Accounts\.createUserAsync|Users\.create\()' --type=ts --type=js

221-229: Don’t drop edit events — compute isEditedMessage before early return.

Edits may have no content.body but carry m.new_content. Current guard discards them.

-			const msgtype = content?.msgtype;
-			const messageBody = content?.body?.toString();
-
-			if (!messageBody && !msgtype) {
+			const msgtype = content?.msgtype;
+			const messageBody = content?.body?.toString();
+			const isEditedMessage = content?.['m.relates_to']?.rel_type === 'm.replace';
+
+			if (!messageBody && !msgtype && !isEditedMessage) {
 				logger.debug('No message content found in event');
 				return;
 			}
@@
-			const isEditedMessage = data.content['m.relates_to']?.rel_type === 'm.replace';
+			// already computed above

Also applies to: 250-251


16-21: Parse Matrix IDs using the first colon (ports supported) and validate @ prefix.

Current split drops ports like :8448 and may accept malformed IDs.

-	const [userPart, domain] = matrixUserId.split(':');
-	if (!userPart || !domain) {
+	const idx = matrixUserId.indexOf(':', 1); // skip leading '@'
+	const userPart = idx > 0 ? matrixUserId.slice(0, idx) : matrixUserId;
+	const domain = idx > 0 ? matrixUserId.slice(idx + 1) : '';
+	if (!userPart || !domain || !userPart.startsWith('@')) {
 		logger.error('Invalid Matrix sender ID format:', matrixUserId);
 		return null;
 	}
-	const username = userPart.substring(1);
+	const baseLocalName = userPart.slice(1);
🧹 Nitpick comments (2)
ee/packages/federation-matrix/src/events/message.ts (2)

25-26: Mark bridged Matrix users as remote consistently.

isRemote should be true for external Matrix users, even when already existing.

-		await MatrixBridgedUser.createOrUpdateByLocalId(user._id, matrixUserId, false, domain);
+		await MatrixBridgedUser.createOrUpdateByLocalId(user._id, matrixUserId, true, domain);

81-85: Avoid relying on deprecated room.federated; use bridge mapping as the source of truth.

IRoom.federated is deprecated. Prefer MatrixBridgedRoom mapping (presence implies federated) or a new flag owned by federation.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between b83f6cb and d77bdcf.

📒 Files selected for processing (3)
  • apps/meteor/server/services/messages/service.ts (1 hunks)
  • ee/packages/federation-matrix/src/events/message.ts (3 hunks)
  • packages/core-services/src/types/IMessageService.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • apps/meteor/server/services/messages/service.ts
  • packages/core-services/src/types/IMessageService.ts
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-09-16T22:09:18.041Z
Learnt from: CR
PR: RocketChat/Rocket.Chat#0
File: .cursor/rules/test-cases.mdc:0-0
Timestamp: 2025-09-16T22:09:18.041Z
Learning: Use Rocket.Chat primary documentation and provided reference files for guidance

Applied to files:

  • ee/packages/federation-matrix/src/events/message.ts
📚 Learning: 2025-09-19T15:15:04.613Z
Learnt from: rodrigok
PR: RocketChat/Rocket.Chat#36991
File: apps/meteor/server/services/federation/infrastructure/rocket-chat/adapters/Settings.ts:219-221
Timestamp: 2025-09-19T15:15:04.613Z
Learning: The Federation_Matrix_homeserver_domain setting in apps/meteor/server/services/federation/infrastructure/rocket-chat/adapters/Settings.ts is part of the old federation system and is being deprecated/removed, so configuration issues with this setting should not be flagged for improvement.

Applied to files:

  • ee/packages/federation-matrix/src/events/message.ts
🧬 Code graph analysis (1)
ee/packages/federation-matrix/src/events/message.ts (6)
packages/core-typings/src/IUser.ts (1)
  • IUser (186-253)
packages/models/src/index.ts (6)
  • Users (209-209)
  • MatrixBridgedUser (217-217)
  • MatrixBridgedRoom (216-216)
  • Rooms (200-200)
  • Subscriptions (206-206)
  • Messages (184-184)
packages/core-typings/src/IRoom.ts (1)
  • IRoom (21-95)
ee/packages/federation-matrix/src/services/MatrixMediaService.ts (1)
  • MatrixMediaService (18-148)
ee/packages/federation-matrix/src/FederationMatrix.ts (1)
  • fileTypes (34-39)
ee/packages/federation-matrix/src/helpers/message.parsers.ts (2)
  • toInternalQuoteMessageFormat (125-151)
  • toInternalMessageFormat (59-73)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: CodeQL-Build
🔇 Additional comments (1)
ee/packages/federation-matrix/src/events/message.ts (1)

111-120: Thread lookup helper looks good.

Clear behavior and minimal DB touches.

Comment on lines +141 to +151
const fileInfo = content.info;
const mimeType = fileInfo.mimetype;
const fileName = messageBody;

const fileRefId = await MatrixMediaService.downloadAndStoreRemoteFile(content.url, {
name: messageBody,
size: fileInfo.size,
type: mimeType,
roomId: room._id,
userId: user._id,
});
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

Null‑safety for media fields and safe filename; avoid encodeURIComponent on undefined.

Guard content.info/body, set sensible defaults, and always pass a defined filename.

-	const fileInfo = content.info;
-	const mimeType = fileInfo.mimetype;
-	const fileName = messageBody;
+	const fileInfo = content?.info || {};
+	const mimeType = fileInfo.mimetype || 'application/octet-stream';
+	const fileName = (messageBody && String(messageBody).trim().length > 0)
+		? String(messageBody).trim()
+		: `${msgtype || 'm.file'}-${eventId}`;
@@
-	const fileRefId = await MatrixMediaService.downloadAndStoreRemoteFile(content.url, {
-		name: messageBody,
-		size: fileInfo.size,
-		type: mimeType,
+	const fileRefId = await MatrixMediaService.downloadAndStoreRemoteFile(content.url, {
+		name: fileName,
+		size: typeof fileInfo.size === 'number' ? fileInfo.size : 0,
+		type: mimeType,
@@
-	const fileUrl = `/file-upload/${fileRefId}/${encodeURIComponent(fileName)}`;
+	const fileUrl = `/file-upload/${fileRefId}/${encodeURIComponent(fileName)}`;

Also applies to: 153-163

🤖 Prompt for AI Agents
In ee/packages/federation-matrix/src/events/message.ts around lines 141-151 (and
likewise update 153-163), guard access to content.info and message body: use
content?.info and provide a fallback object, derive mimeType with a default like
'application/octet-stream', and compute a safe filename by preferring
messageBody or content.info.name and falling back to a deterministic default
(e.g., room id + timestamp). Only call encodeURIComponent when the filename is
defined; ensure the name passed to MatrixMediaService.downloadAndStoreRemoteFile
is always a defined string.

Comment on lines 274 to 279
messageToReplyToUrl,
formattedMessage: data.content.formatted_body || '',
rawMessage: message,
rawMessage: messageBody,
homeServerDomain: serverName,
senderExternalId: data.sender,
});
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

Edited quote: use m.new_content.body as the raw text.

Using messageBody here is wrong for edits; it may be undefined or stale.

-						rawMessage: messageBody,
+						rawMessage: data.content?.['m.new_content']?.body || '',
📝 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
messageToReplyToUrl,
formattedMessage: data.content.formatted_body || '',
rawMessage: message,
rawMessage: messageBody,
homeServerDomain: serverName,
senderExternalId: data.sender,
});
messageToReplyToUrl,
formattedMessage: data.content.formatted_body || '',
rawMessage: data.content?.['m.new_content']?.body || '',
homeServerDomain: serverName,
senderExternalId: data.sender,
🤖 Prompt for AI Agents
In ee/packages/federation-matrix/src/events/message.ts around lines 274-279, the
code sets rawMessage to messageBody which is incorrect for edited messages;
change the logic to detect edits (presence of data.content["m.new_content"]) and
use the edited plain-text payload (m.new_content.body) as rawMessage (and use
m.new_content.formatted_body for formattedMessage when present); fall back to
the existing messageBody/Content fields if m.new_content is absent and add
null/undefined guards so rawMessage and formattedMessage are never left
undefined.

Comment on lines 332 to 335
if (isMediaMessage && content?.url) {
const result = await handleMediaMessage(content, msgtype, messageBody, user, room, data.event_id, tmid);
await Message.saveMessageFromFederation(result);
} else {
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

tmid is undefined; compile/runtime error. Pass the computed thread (and keep tshow).

Also unifies media vs text thread handling.

-			if (isMediaMessage && content?.url) {
-				const result = await handleMediaMessage(content, msgtype, messageBody, user, room, data.event_id, tmid);
+			if (isMediaMessage && content?.url) {
+				const tmid = thread?.tmid;
+				const result = await handleMediaMessage(content, msgtype, messageBody, user, room, data.event_id, tmid);
 				await Message.saveMessageFromFederation(result);

Recommended stronger refactor (unify on thread end‑to‑end):

-async function handleMediaMessage( /* ... */, tmid?: string, ): Promise<{ /* ... */ tmid?: string; /* ... */ }> {
+async function handleMediaMessage( /* ... */, thread?: { tmid: string; tshow: boolean }, ): Promise<{ /* ... */ thread?: { tmid: string; tshow: boolean }; /* ... */ }> {
@@
-		federation_event_id: eventId,
-		tmid,
+		federation_event_id: eventId,
+		thread,
@@
-	const result = await handleMediaMessage(content, msgtype, messageBody, user, room, data.event_id, thread?.tmid);
+	const result = await handleMediaMessage(content, msgtype, messageBody, user, room, data.event_id, thread);
📝 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
if (isMediaMessage && content?.url) {
const result = await handleMediaMessage(content, msgtype, messageBody, user, room, data.event_id, tmid);
await Message.saveMessageFromFederation(result);
} else {
if (isMediaMessage && content?.url) {
const tmid = thread?.tmid;
const result = await handleMediaMessage(content, msgtype, messageBody, user, room, data.event_id, tmid);
await Message.saveMessageFromFederation(result);
} else {
🤖 Prompt for AI Agents
In ee/packages/federation-matrix/src/events/message.ts around lines 332 to 335,
the code passes an undefined variable tmid into handleMediaMessage which causes
compile/runtime errors; replace tmid with the computed thread object (and retain
tshow) so media message handling uses the same thread data as text messages, and
refactor the call sites so both media and text paths pass the unified thread
variable end-to-end (update handleMediaMessage signature/use if needed).

@ggazzo ggazzo force-pushed the feat/federation-files-2 branch from d77bdcf to 32e15a3 Compare September 19, 2025 22:47
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (6)
ee/packages/federation-matrix/src/events/message.ts (6)

206-215: Media messages should preserve thread metadata consistently with text messages.

Media path returns only tmid while text path uses the full thread object with tshow. This creates inconsistent message structures.

 	return {
 		fromId: user._id,
 		rid: room._id,
 		msg: '',
 		federation_event_id: eventId,
-		tmid,
+		thread: tmid ? { tmid, tshow: false } : undefined,
 		file: fileData,
 		files: [fileData],
 		attachments: [attachment],
 	};

16-21: Parse Matrix IDs using the first colon only to preserve port numbers.

The current matrixUserId.split(':') incorrectly drops port components (e.g., @u:example.com:8448 becomes ["@u", "example.com", "8448"]). Use indexOf to split on the first colon only.

-	const [userPart, domain] = matrixUserId.split(':');
-	if (!userPart || !domain) {
-		logger.error('Invalid Matrix sender ID format:', matrixUserId);
-		return null;
-	}
-	const username = userPart.substring(1);
+	const idx = matrixUserId.indexOf(':');
+	if (idx <= 1 || !matrixUserId.startsWith('@')) {
+		logger.error('Invalid Matrix sender ID format:', matrixUserId);
+		return null;
+	}
+	const userPart = matrixUserId.substring(0, idx);
+	const domain = matrixUserId.substring(idx + 1);
+	const username = userPart.substring(1);

23-27: Matrix IDs cannot be used directly as Rocket.Chat usernames — they violate username validation rules.

Users.findOneByUsername(matrixUserId) won't work because Matrix IDs contain @ and : which are not allowed in Rocket.Chat usernames per validateUsername regex /^[0-9a-zA-Z-_.]+$/.

Use the bridge mapping to resolve users or derive a validated local username:

-	const user = await Users.findOneByUsername(matrixUserId);
-	if (user) {
-		await MatrixBridgedUser.createOrUpdateByLocalId(user._id, matrixUserId, false, domain);
-		return user;
-	}
+	// First check if user exists via bridge mapping
+	const bridgedUser = await MatrixBridgedUser.findOneByExternalId(matrixUserId);
+	if (bridgedUser) {
+		const user = await Users.findOneById(bridgedUser.localId);
+		if (user) {
+			return user;
+		}
+	}

31-48: Don't use Users.insertOne — it bypasses validation and triggers.

Direct database inserts skip username validation, uniqueness checks, hooks, and denormalizations. Use the established user creation flow instead.

Replace with the proper creation API:

-	const userData = {
-		username: matrixUserId,
-		name: username, // TODO: Fetch display name from Matrix profile
-		type: 'user',
-		status: UserStatus.ONLINE,
-		active: true,
-		roles: ['user'],
-		requirePasswordChange: false,
-		federated: true,
-		federation: {
-			version: 1,
-		},
-		createdAt: new Date(),
-		_updatedAt: new Date(),
-	};
-
-	const { insertedId } = await Users.insertOne(userData);
+	// Derive a valid local username and ensure uniqueness
+	let localUsername = username.replace(/[^0-9a-zA-Z-_.]/g, '_');
+	let suffix = 0;
+	while (await Users.findOneByUsername(localUsername)) {
+		suffix++;
+		localUsername = `${username.replace(/[^0-9a-zA-Z-_.]/g, '_')}_${suffix}`;
+	}
+
+	// Use the proper creation flow
+	const userId = await Users.create({
+		username: localUsername,
+		name: username,
+		type: 'user',
+		status: UserStatus.ONLINE,
+		active: true,
+		roles: ['user'],
+		requirePasswordChange: false,
+		federated: true,
+		federation: {
+			version: 1,
+		},
+	});
+
+	const insertedId = userId;

141-151: Add null safety for media fields and ensure valid filename.

content.info could be undefined, and messageBody might be empty/missing, causing runtime errors.

-	const fileInfo = content.info;
-	const mimeType = fileInfo.mimetype;
-	const fileName = messageBody;
+	const fileInfo = content?.info || {};
+	const mimeType = fileInfo.mimetype || 'application/octet-stream';
+	const fileName = (messageBody && String(messageBody).trim()) 
+		? String(messageBody).trim() 
+		: `${msgtype || 'file'}-${eventId}`;
 
 	const fileRefId = await MatrixMediaService.downloadAndStoreRemoteFile(content.url, {
-		name: messageBody,
-		size: fileInfo.size,
+		name: fileName,
+		size: typeof fileInfo.size === 'number' ? fileInfo.size : 0,
 		type: mimeType,

274-279: Use m.new_content.body for edited quotes, not the old messageBody.

For edited messages, messageBody contains the old content. The raw text should come from m.new_content.

 				const formatted = await toInternalQuoteMessageFormat({
 					messageToReplyToUrl,
 					formattedMessage: data.content.formatted_body || '',
-					rawMessage: messageBody,
+					rawMessage: data.content['m.new_content']?.body || '',
 					homeServerDomain: serverName,
 					senderExternalId: data.sender,
 				});
🧹 Nitpick comments (2)
ee/packages/federation-matrix/src/events/message.ts (2)

111-120: Avoid database queries for disabled feature when threads not in use.

When threadRootEventId is undefined, this function is called but immediately returns. Consider checking before calling.

-	const thread = await getThreadMessageId(threadRootEventId);
+	const thread = threadRootEventId ? await getThreadMessageId(threadRootEventId) : undefined;

332-335: Pass the full thread object to handleMediaMessage for consistency.

Currently passing thread?.tmid but the function signature expects just tmid. This loses the tshow property.

Update the function to accept the full thread object:

 async function handleMediaMessage(
 	// TODO improve typing
 	content: any,
 	msgtype: string,
 	messageBody: string,
 	user: IUser,
 	room: IRoom,
 	eventId: string,
-	tmid?: string,
+	thread?: { tmid: string; tshow: boolean },
 ): Promise<{
@@
-		tmid?: string;
+		thread?: { tmid: string; tshow: boolean };
@@
-		tmid,
+		thread,
@@
 			if (isMediaMessage && content?.url) {
-				const result = await handleMediaMessage(content, msgtype, messageBody, user, room, data.event_id, thread?.tmid);
+				const result = await handleMediaMessage(content, msgtype, messageBody, user, room, data.event_id, thread);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between d77bdcf and 32e15a3.

📒 Files selected for processing (3)
  • apps/meteor/server/services/messages/service.ts (1 hunks)
  • ee/packages/federation-matrix/src/events/message.ts (3 hunks)
  • packages/core-services/src/types/IMessageService.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/core-services/src/types/IMessageService.ts
  • apps/meteor/server/services/messages/service.ts
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-09-16T22:09:18.041Z
Learnt from: CR
PR: RocketChat/Rocket.Chat#0
File: .cursor/rules/test-cases.mdc:0-0
Timestamp: 2025-09-16T22:09:18.041Z
Learning: Use Rocket.Chat primary documentation and provided reference files for guidance

Applied to files:

  • ee/packages/federation-matrix/src/events/message.ts
📚 Learning: 2025-09-19T15:15:04.613Z
Learnt from: rodrigok
PR: RocketChat/Rocket.Chat#36991
File: apps/meteor/server/services/federation/infrastructure/rocket-chat/adapters/Settings.ts:219-221
Timestamp: 2025-09-19T15:15:04.613Z
Learning: The Federation_Matrix_homeserver_domain setting in apps/meteor/server/services/federation/infrastructure/rocket-chat/adapters/Settings.ts is part of the old federation system and is being deprecated/removed, so configuration issues with this setting should not be flagged for improvement.

Applied to files:

  • ee/packages/federation-matrix/src/events/message.ts
🧬 Code graph analysis (1)
ee/packages/federation-matrix/src/events/message.ts (4)
packages/core-typings/src/IUser.ts (1)
  • IUser (186-253)
packages/core-typings/src/IRoom.ts (1)
  • IRoom (21-95)
ee/packages/federation-matrix/src/FederationMatrix.ts (1)
  • fileTypes (34-39)
ee/packages/federation-matrix/src/helpers/message.parsers.ts (2)
  • toInternalQuoteMessageFormat (125-151)
  • toInternalMessageFormat (59-73)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: 📦 Build Packages
  • GitHub Check: CodeQL-Build
  • GitHub Check: CodeQL-Build
🔇 Additional comments (1)
ee/packages/federation-matrix/src/events/message.ts (1)

94-102: Good federation subscription handling.

The logic properly creates subscriptions for federated users with minimal defaults, avoiding unwanted notifications while ensuring message visibility.

@ggazzo ggazzo merged commit 6e28339 into chore/federation-backup Sep 20, 2025
51 checks passed
@ggazzo ggazzo deleted the feat/federation-files-2 branch September 20, 2025 01:17
ggazzo added a commit that referenced this pull request Sep 23, 2025
Co-authored-by: Diego Sampaio <chinello@gmail.com>
Co-authored-by: Guilherme Gazzo <guilherme@gazzo.xyz>
ggazzo added a commit that referenced this pull request Sep 25, 2025
Co-authored-by: Diego Sampaio <chinello@gmail.com>
Co-authored-by: Guilherme Gazzo <guilherme@gazzo.xyz>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants