Skip to content

Conversation

@ricardogarim
Copy link
Contributor

@ricardogarim ricardogarim commented Oct 6, 2025

Proposed changes (including videos or screenshots)

Issue(s)

Steps to test or reproduce

Further comments

Summary by CodeRabbit

  • Bug Fixes

    • Improved sanitization of quoted messages to block malicious content and ensure safe, plain-text rendering when needed.
    • More reliable mention and quote handling in Matrix-formatted messages.
  • Refactor

    • Reworked message parsing to a unified, batched pipeline for mentions and replies, improving consistency across formats.
  • Tests

    • Added tests validating sanitization of risky HTML in quoted messages.
  • Chores

    • Removed unused Matrix-related dependencies.
    • Simplified Docker build and CI by dropping Alpine-specific crypto bindings and related caching steps.

@dionisio-bot
Copy link
Contributor

dionisio-bot bot commented Oct 6, 2025

Looks like this PR is ready to merge! 🎉
If you have any trouble, please check the PR guidelines

@changeset-bot
Copy link

changeset-bot bot commented Oct 6, 2025

⚠️ No Changeset found

Latest commit: 193fd19

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

@ricardogarim ricardogarim force-pushed the refactor/internal-mentionpill branch from a6fae0b to c6cf36f Compare October 6, 2025 12:42
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 6, 2025

Walkthrough

Refactors Matrix federation message parsing to a batched mention-extraction/replacement pipeline with standardized Matrix-style HTML pills and updated quote handling. Adds sanitization-focused tests. Removes Matrix bot SDK and appservice dependencies. Cleans Docker and CI of Alpine-specific Matrix crypto build/caching and related steps.

Changes

Cohort / File(s) Summary
Matrix message parsing refactor
ee/packages/federation-matrix/src/helpers/message.parsers.ts, ee/packages/federation-matrix/src/helpers/message.parsers.spec.ts
Implement batched mention extraction/replacement, Matrix-style HTML pill creation, updated quote formatting, and HTML sanitization; add tests validating sanitization in quoted messages.
Dependency updates/removals
ee/packages/federation-matrix/package.json, apps/meteor/package.json
Remove @vector-im/matrix-bot-sdk, matrix-appservice, and matrix-appservice-bridge; update @types/sanitize-html devDependency.
Docker/CI cleanup for Matrix crypto
apps/meteor/.docker/Dockerfile.alpine, .github/actions/build-docker/action.yml, .github/workflows/ci.yml
Drop Alpine-specific matrix-sdk-crypto build/copy/cache steps and the CI job building Matrix Rust bindings; remove dependent needs/verification step.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Client
  participant Parser as MessageParsers
  participant Utils as extractAnchors/extractMentions
  participant Replacer as replaceMentions
  participant HTML as createMentionHtml

  Client->>Parser: toExternalMessageFormat(raw, formatted)
  Parser->>Utils: parse anchors, find mention targets
  Utils-->>Parser: mention mappings (userId→pill)
  Parser->>HTML: build Matrix-style <a> pills
  HTML-->>Parser: pill HTML
  Parser->>Replacer: apply batched replacements
  Replacer-->>Client: formatted_body (Matrix HTML) + body
Loading
sequenceDiagram
  autonumber
  actor Client
  participant Parser as toExternalQuoteMessageFormat
  participant Sanitize as strip/escape HTML
  participant Reply as createReplyContent

  Client->>Parser: format quoted message
  Parser->>Sanitize: remove quote tags, sanitize content
  Sanitize-->>Parser: safe text/HTML
  Parser->>Reply: construct in-reply-to + formatted_body
  Reply-->>Client: Matrix reply content
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested labels

stat: ready to merge, stat: QA assured

Suggested reviewers

  • ricardogarim
  • ggazzo

Poem

I nudge the mentions, hop by hop,
From text to pill—no scripts to drop.
Quotes are tidy, sanitized bright,
Federation pings take flight.
CI sheds its bulky load—
Lighter paws on cleaner code. 🥕🐇

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Linked Issues Check ⚠️ Warning While the refactored pipeline and internal MentionPill structures improve mention formatting for federated users, the PR does not include any implementation for recording mention state or dispatching notifications to federated users as required by FDR-138. Add logic to update mention state for federated users and dispatch notification events, and include tests verifying that federated mentions result in proper notifications.
Out of Scope Changes Check ⚠️ Warning The removal of matrix-bot-sdk installation steps in Dockerfiles and CI workflow changes are unrelated to mention handling and fall outside the objective of fixing federated user mentions. Separate dependency and CI modifications into their own PR or justify their inclusion by linking them to mention handling requirements.
✅ 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 succinctly describes the primary refactor of creating a MentionPill structure within the federation messaging logic and clearly identifies the scope without unnecessary details.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch refactor/internal-mentionpill

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@ricardogarim ricardogarim force-pushed the refactor/internal-mentionpill branch from c6cf36f to 9e4968b Compare October 6, 2025 12:45
@ricardogarim ricardogarim changed the base branch from release-7.11.0 to chore/fed-branded October 6, 2025 13:14
@ricardogarim ricardogarim force-pushed the refactor/internal-mentionpill branch from 3c8aeb9 to 130e2c8 Compare October 6, 2025 13:15
@ricardogarim ricardogarim marked this pull request as ready for review October 6, 2025 13:16
@ricardogarim ricardogarim requested review from a team as code owners October 6, 2025 13:16
@codecov
Copy link

codecov bot commented Oct 6, 2025

Codecov Report

❌ Patch coverage is 96.96970% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 66.35%. Comparing base (0c14232) to head (193fd19).
⚠️ Report is 2 commits behind head on release-7.11.0.

Additional details and impacted files

Impacted file tree graph

@@               Coverage Diff               @@
##           release-7.11.0   #37148   +/-   ##
===============================================
  Coverage           66.34%   66.35%           
===============================================
  Files                3386     3386           
  Lines              115663   115619   -44     
  Branches            21358    21355    -3     
===============================================
- Hits                76740    76720   -20     
+ Misses              36319    36291   -28     
- Partials             2604     2608    +4     
Flag Coverage Δ
e2e 57.28% <ø> (+<0.01%) ⬆️
unit 71.20% <96.96%> (+<0.01%) ⬆️

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.

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 (1)
ee/packages/federation-matrix/src/helpers/message.parsers.ts (1)

29-31: HTML stripping is not a sanitizer; prior alert acknowledged

stripHtml(html, ['a']) preserves anchor tags entirely (including attributes). While you later only ingest anchors whose href contains matrix.to, the function itself isn’t a sanitizer. Ensure this isn’t reused as a sanitizer elsewhere.

🧹 Nitpick comments (8)
packages/media-signaling/src/lib/services/webrtc/Processor.ts (1)

329-329: Consider using the serializeError utility for consistency.

The PR introduces a new serializeError utility (packages/media-signaling/src/lib/utils/serializeError.ts) designed to normalize error representations. However, onIceCandidateError directly uses JSON.stringify(event), which may not capture all relevant properties of RTCPeerConnectionIceErrorEvent due to non-enumerable properties.

Apply this diff to use the serializeError utility:

+import { serializeError } from '../../utils/serializeError';
+
 	private onIceCandidateError(event: RTCPeerConnectionIceErrorEvent) {
 		if (this.stopped) {
 			return;
 		}
 		this.config.logger?.debug('MediaCallWebRTCProcessor.onIceCandidateError');
 		this.config.logger?.error(event);
 
-		this.emitter.emit('internalError', { critical: false, error: 'ice-candidate-error', errorDetails: JSON.stringify(event) });
+		this.emitter.emit('internalError', { critical: false, error: 'ice-candidate-error', errorDetails: serializeError(event) });
 	}
packages/media-signaling/src/lib/utils/serializeError.ts (2)

12-18: Error spread may not capture all properties.

The spread operator { ...error } on Error instances won't capture non-enumerable properties. While you explicitly add name and message, other properties like stack are also non-enumerable and won't be included.

Consider using Object.getOwnPropertyNames() to capture all properties:

 		if (typeof error === 'object') {
 			if (error instanceof Error) {
-				return JSON.stringify({
-					...error,
-					name: error.name,
-					message: error.message,
-				});
+				const errorData: Record<string, any> = {
+					name: error.name,
+					message: error.message,
+					stack: error.stack,
+				};
+				for (const key of Object.getOwnPropertyNames(error)) {
+					if (key !== 'name' && key !== 'message' && key !== 'stack') {
+						errorData[key] = (error as any)[key];
+					}
+				}
+				return JSON.stringify(errorData);
 			}

1-37: Consider handling circular references.

JSON.stringify will throw when encountering circular references. While the outer try-catch handles this, you could provide better error resilience by detecting or handling circular structures explicitly.

Consider using a circular reference replacer:

export function serializeError(error: unknown): string | undefined {
	try {
		if (!error) {
			return undefined;
		}

		if (typeof error === 'string') {
			return error;
		}

		if (typeof error === 'object') {
			const seen = new WeakSet();
			const replacer = (_key: string, value: any) => {
				if (typeof value === 'object' && value !== null) {
					if (seen.has(value)) {
						return '[Circular]';
					}
					seen.add(value);
				}
				return value;
			};

			if (error instanceof Error) {
				return JSON.stringify({
					...error,
					name: error.name,
					message: error.message,
				}, replacer);
			}

			const errorData: Record<string, any> = { ...error };
			if ('name' in error) {
				errorData.name = error.name;
			}
			if ('message' in error) {
				errorData.message = error.message;
			}

			if (Object.keys(errorData).length > 0) {
				return JSON.stringify(errorData, replacer);
			}
		}
	} catch {
		//
	}

	return undefined;
}
ee/packages/federation-matrix/src/helpers/message.parsers.ts (3)

87-91: Avoid unconditional leading space when replacing with mention pills

Prepending a space can introduce double or leading spaces. Use the replace callback’s offset to insert a space only when needed.

-const replaceWithMentionPills = async (message: string, regex: RegExp, createPill: (match: string) => string): Promise<string> => {
-  const matches = Array.from(message.matchAll(regex), ([match]) => createPill(match.trimStart()));
-  let i = 0;
-  return message.replace(regex, () => ` ${matches[i++]}`);
-};
+const replaceWithMentionPills = async (message: string, regex: RegExp, createPill: (match: string) => string): Promise<string> => {
+  const matches = Array.from(message.matchAll(regex), ([match]) => createPill(match.trimStart()));
+  let i = 0;
+  return message.replace(regex, (m, ...args) => {
+    const offset = args[args.length - 2] as number;
+    const needsSpace = offset > 0 && !/\s/.test(message[offset - 1]);
+    return `${needsSpace ? ' ' : ''}${matches[i++]}`;
+  });
+};

147-151: Dynamic RegExp is safe here but consider precompiling

tag comes from a fixed allowlist (['mx-reply', 'blockquote']), so input isn’t attacker‑controlled; risk of ReDoS is minimal. You can silence tooling and improve readability by precompiling two static regexes.

Example:

- MATRIX_QUOTE_TAGS.forEach((tag) => {
-   cleaned = cleaned.replace(new RegExp(`<${tag}[^>]*>.*?</${tag}>`, 'gis'), '');
- });
+ cleaned = cleaned.replace(/<mx-reply[^>]*>.*?<\/mx-reply>/gis, '').replace(/<blockquote[^>]*>.*?<\/blockquote>/gis, '');

172-196: Quote builder uses two variants; simplify to avoid mixing bodies

reply1.body is not used while reply2.body is returned. Keep a single construction to avoid confusion, and always pass plain text to body and HTML to formatted_body.

- const markdownHtml = await marked.parse(message);
- const withMentions = await toExternalMessageFormat({ message, externalRoomId, homeServerDomain });
- const withMentionsHtml = await marked.parse(withMentions);
-
- const reply1 = createReplyContent(externalRoomId, event, markdownHtml, withMentionsHtml);
- const reply2 = createReplyContent(externalRoomId, event, message, withMentionsHtml);
-
- return {
-   message: reply2.body,
-   formattedMessage: reply1.formatted_body ?? '',
- };
+ const withMentionsHtml = await marked.parse(await toExternalMessageFormat({ message, externalRoomId, homeServerDomain }));
+ const reply = createReplyContent(externalRoomId, event, message, withMentionsHtml);
+ return { message: reply.body, formattedMessage: reply.formatted_body ?? '' };
apps/meteor/client/components/GazzodownText.tsx (1)

69-76: Normalize team mention comparison as well (consistency)

You normalize user mentions by stripping a leading '@'. Consider doing the same for teams to avoid mismatches like '@team' vs 'team'.

-const filterTeam = ({ name, type }: UserMention) => type === 'team' && name === mention;
+const filterTeam = ({ name, type }: UserMention) =>
+  type === 'team' && (name === mention || name === normalizedMention);
apps/meteor/client/views/room/providers/ComposerPopupProvider.tsx (1)

156-156: Null-safe username normalization in getValue
Replace

getValue: (item) => (item.username.startsWith('@') ? item.username.substring(1) : item.username),

with

getValue: (item) => item.username?.replace(/^@/, '') ?? '',

(or fallback to item._id or item.name if an empty string isn’t acceptable)

📜 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 ab8b431 and 130e2c8.

📒 Files selected for processing (16)
  • apps/meteor/app/mentions/server/Mentions.ts (2 hunks)
  • apps/meteor/client/components/GazzodownText.tsx (1 hunks)
  • apps/meteor/client/views/room/providers/ComposerPopupProvider.tsx (1 hunks)
  • ee/packages/federation-matrix/package.json (0 hunks)
  • ee/packages/federation-matrix/src/helpers/message.parsers.ts (5 hunks)
  • ee/packages/media-calls/src/internal/agents/CallSignalProcessor.ts (2 hunks)
  • packages/core-typings/src/mediaCalls/IMediaCall.ts (1 hunks)
  • packages/media-signaling/src/definition/call/IClientMediaCall.ts (1 hunks)
  • packages/media-signaling/src/definition/services/IServiceProcessor.ts (1 hunks)
  • packages/media-signaling/src/definition/signals/client/error.ts (2 hunks)
  • packages/media-signaling/src/lib/Call.ts (9 hunks)
  • packages/media-signaling/src/lib/Session.ts (1 hunks)
  • packages/media-signaling/src/lib/TransportWrapper.ts (1 hunks)
  • packages/media-signaling/src/lib/services/webrtc/Processor.ts (1 hunks)
  • packages/media-signaling/src/lib/utils/serializeError.ts (1 hunks)
  • packages/models/src/models/MediaCalls.ts (2 hunks)
💤 Files with no reviewable changes (1)
  • ee/packages/federation-matrix/package.json
🧰 Additional context used
🧬 Code graph analysis (3)
packages/media-signaling/src/lib/TransportWrapper.ts (1)
packages/media-signaling/src/definition/signals/client/error.ts (1)
  • ClientMediaSignalError (4-14)
ee/packages/media-calls/src/internal/agents/CallSignalProcessor.ts (3)
packages/media-signaling/src/definition/signals/client/error.ts (1)
  • ClientMediaSignalError (4-14)
packages/media-signaling/src/definition/call/IClientMediaCall.ts (1)
  • CallHangupReason (29-41)
ee/packages/media-calls/src/server/CallDirector.ts (1)
  • mediaCallDirector (416-416)
packages/media-signaling/src/lib/Call.ts (1)
packages/media-signaling/src/lib/utils/serializeError.ts (1)
  • serializeError (1-37)
🪛 ast-grep (0.39.5)
ee/packages/federation-matrix/src/helpers/message.parsers.ts

[warning] 147-147: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(<${tag}[^>]*>.*?</${tag}>, 'gis')
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html

(regexp-from-variable)

⏰ 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). (7)
  • GitHub Check: 🔨 Test Storybook / Test Storybook
  • GitHub Check: 🔨 Test Unit / Unit Tests
  • GitHub Check: 🔎 Code Check / TypeScript
  • GitHub Check: 🔎 Code Check / Code Lint
  • GitHub Check: 📦 Meteor Build - coverage
  • GitHub Check: CodeQL-Build
  • GitHub Check: CodeQL-Build
🔇 Additional comments (8)
packages/media-signaling/src/definition/call/IClientMediaCall.ts (1)

39-39: LGTM!

The new input-error hangup reason is well-documented and aligns with its usage in Session.ts for handling audio input track failures.

packages/media-signaling/src/definition/services/IServiceProcessor.ts (1)

16-16: LGTM!

Adding the optional errorDetails field enhances error observability without breaking existing code. The field is properly typed and aligns with its usage across the signaling layer.

packages/media-signaling/src/lib/Session.ts (1)

429-429: LGTM!

Changing the hangup reason from 'service-error' to 'input-error' provides more specific error classification for audio input failures, improving error diagnostics.

packages/models/src/models/MediaCalls.ts (2)

85-85: LGTM!

Recording the acceptance timestamp provides valuable call lifecycle tracking and aligns with the updated IMediaCall type definition.


101-101: LGTM!

Recording the activation timestamp complements the acceptance timestamp and provides a complete picture of call state transitions.

packages/core-typings/src/mediaCalls/IMediaCall.ts (1)

54-57: LGTM!

The new optional timestamp fields are well-documented and provide valuable insights into call lifecycle progression. They align with the implementation in MediaCalls.ts.

packages/media-signaling/src/definition/signals/client/error.ts (1)

12-13: LGTM!

The new optional fields enhance error reporting capabilities while maintaining backward compatibility. The type definition and JSON schema are properly synchronized.

Also applies to: 46-53

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

155-170: Sanitization of markdown→HTML

marked.parse outputs HTML. Confirm upstream sanitization guarantees for message. If not guaranteed, consider sanitizing the output with your standard sanitizer before sending to Matrix.

@ricardogarim ricardogarim force-pushed the refactor/internal-mentionpill branch from 130e2c8 to 079f418 Compare October 6, 2025 17: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: 1

♻️ Duplicate comments (3)
ee/packages/federation-matrix/src/helpers/message.parsers.ts (3)

27-27: @here/@ALL distinction lost: pill text should preserve the original label.

The current implementation always uses the ID as both href and text, which causes general mentions (@all/@here) to render as room IDs in the pill text. This prevents round-tripping the original label.

Past review flagged this critical issue with a suggested fix: add a label parameter to createMentionHtml and pass the original match text when creating general mention pills.

Apply this diff to add label support:

-const createMentionHtml = (id: string): string => `<a href="${MATRIX_TO_URL}${id}">${id}</a>`;
+const createMentionHtml = (id: string, label = id): string => `<a href="${MATRIX_TO_URL}${id}">${label}</a>`;

Then update line 140 to preserve the match text:

-	result = await replaceWithMentionPills(result, REGEX.general, () => createMentionHtml(externalRoomId));
+	result = await replaceWithMentionPills(result, REGEX.general, (match) => createMentionHtml(externalRoomId, match));

31-45: @here collapsed to @ALL: preserve the pill text label.

Line 44 always returns @all for room mentions, losing the distinction between @ALL and @here. The fix requires checking the pill's visible text and preserving it.

Past review suggested:

Apply this diff to preserve @here:

-		return { mention: '@all', realName: text };
+		const m = text === '@here' ? '@here' : '@all';
+		return { mention: m, realName: text };

For backward compatibility with older messages where pill text was the room ID:

 		return {
-			mention: '@all',
+			mention: text === '@here' ? '@here' : '@all',
 			realName: text,
 		};

55-57: Room mention replacement needs to handle both @ALL and @here.

Lines 55-57 only replace @all for room mentions. After fixing extractMentions to preserve @here, this logic must handle both labels.

Past review suggested adding backward-compat fallback:

Apply this diff:

-	} else if (realName.startsWith('!')) {
-		result = result.replace(/(?<!\w)@all(?!\w)/, mention);
+	} else if (realName.startsWith('!') || realName.startsWith('#')) {
+		// backward-compat: older pills used roomId as label; try @here first, then @all
+		const index = result.indexOf('@here') !== -1 ? result.indexOf('@here') : result.indexOf('@all');
+		if (index !== -1) {
+			const target = result.includes('@here') ? '@here' : '@all';
+			result = result.replace(new RegExp(`(?<!\\w)${target}(?!\\w)`), mention);
+		}
 	}
🧹 Nitpick comments (2)
ee/packages/federation-matrix/src/helpers/message.parsers.ts (2)

25-25: LGTM: HTML stripping works correctly.

The function correctly uses sanitizeHtml to strip tags while optionally preserving anchors.

For more flexibility, consider directly mapping the keep array:

-const stripHtml = (html: string, keep: string[] = []): string => sanitizeHtml(html, { allowedTags: keep.includes('a') ? ['a'] : [] });
+const stripHtml = (html: string, keep: string[] = []): string => sanitizeHtml(html, { allowedTags: keep });

121-124: Replace the custom regex loop with a sanitize-html exclusiveFilter
RegExp risk is low (tags are fixed), but you can more efficiently strip <mx-reply> and <blockquote> using sanitize-html:

cleaned = sanitizeHtml(formattedMessage, {
  allowedTags: sanitizeHtml.defaults.allowedTags.filter((t) => t !== 'blockquote'),
  exclusiveFilter: (frame) => frame.tag === 'mx-reply',
});
📜 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 079f418 and 352199d.

📒 Files selected for processing (2)
  • ee/packages/federation-matrix/package.json (1 hunks)
  • ee/packages/federation-matrix/src/helpers/message.parsers.ts (5 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • ee/packages/federation-matrix/package.json
🧰 Additional context used
🪛 ast-grep (0.39.5)
ee/packages/federation-matrix/src/helpers/message.parsers.ts

[warning] 51-51: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp((?<!\\w)${realName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?!\\w))
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html

(regexp-from-variable)


[warning] 122-122: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(<${tag}[^>]*>.*?</${tag}>, 'gis')
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html

(regexp-from-variable)

🔇 Additional comments (6)
ee/packages/federation-matrix/src/helpers/message.parsers.ts (6)

147-172: LGTM: Quote reply structure is correct.

The dual reply creation (reply1 with markdown HTML, reply2 with raw message) correctly populates both body and formatted_body fields for Matrix reply format.


77-94: LGTM: Matrix reply structure follows spec correctly.

The function correctly implements Matrix's reply format with mx-reply tags, m.in_reply_to metadata, and proper fallbacks for missing formatted_body.


29-29: LGTM: Anchor extraction is correct.

The implementation correctly uses matchAll to extract href and text from HTML anchors, handling both single and double quotes.


22-23: LGTM: HTML escaping is correct.

The function correctly escapes all necessary HTML special characters.


62-66: Verify whitespace handling in mention pill replacement.

Line 63 calls trimStart() on matches, and line 65 prepends a space to the replacement. This assumes:

  1. Matches may have leading whitespace that should be removed
  2. A single space should always precede the pill

However, this could lead to incorrect spacing if:

  • The original match had no leading space (space gets added)
  • The original match had multiple spaces (all collapsed to one)

Please verify this logic handles all cases correctly:

// Test cases
'foo@all bar'      // Should become 'foo <pill> bar'
'foo @all bar'     // Should become 'foo <pill> bar' (original space preserved)
'foo  @all bar'    // Should become 'foo <pill> bar' (extra space removed?)

Consider preserving the original leading whitespace:

-const replaceWithMentionPills = async (message: string, regex: RegExp, createPill: (match: string) => string): Promise<string> => {
-	const matches = Array.from(message.matchAll(regex), ([match]) => createPill(match.trimStart()));
-	let i = 0;
-	return message.replace(regex, () => ` ${matches[i++]}`);
+const replaceWithMentionPills = async (message: string, regex: RegExp, createPill: (match: string) => string): Promise<string> => {
+	const matches = Array.from(message.matchAll(regex), ([match]) => ({
+		pill: createPill(match.trimStart()),
+		leadingSpace: match.match(/^\s*/)?.[0] || ''
+	}));
+	let i = 0;
+	return message.replace(regex, () => `${matches[i].leadingSpace}${matches[i++].pill}`);
 };

68-75: LGTM: Quote prefix stripping is correct.

The function correctly identifies and removes quote prefix lines (starting with '>'), returning the first non-quoted section.

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 (1)
ee/packages/federation-matrix/src/helpers/message.parsers.ts (1)

27-27: Preserve @here vs @ALL label in pills to round‑trip correctly

General mentions are collapsed to @ALL: pills are created with label = room id (Line 153), and extractMentions maps any room pill to '@ALL' (Lines 37-38). Keep the visible label ('@here' or '@ALL') in the pill text and round‑trip it back.

Apply these diffs:

-const createMentionHtml = (id: string): string => `<a href="${MATRIX_TO_URL}${id}">${id}</a>`;
+const createMentionHtml = (id: string, label = id): string => `<a href="${MATRIX_TO_URL}${id}">${label}</a>`;
-		.map(({ href, text }) => {
+		.map(({ href, text }) => {
 			const userMatch = href.match(/@([^:]+):(.+)/);
 			if (!userMatch) {
-				return { mention: '@all', realName: text };
+				const label = (text || '').trim();
+				const m = label === '@here' ? '@here' : '@all';
+				return { mention: m, realName: text };
 			}
-	result = await replaceWithMentionPills(result, REGEX.general, () => createMentionHtml(externalRoomId));
+	result = await replaceWithMentionPills(result, REGEX.general, (match) => createMentionHtml(externalRoomId, match));

Also applies to: 31-46, 153-156

🧹 Nitpick comments (6)
ee/packages/federation-matrix/src/helpers/message.parsers.ts (5)

48-73: Avoid dynamic lookbehind regex; use indexOf with explicit word-boundary checks

Current replaceMentions builds regex from variable input with lookbehinds, risking ReDoS and unnecessary complexity. Switch to a deterministic indexOf-based replacer with explicit boundary checks and handle general mentions via the same helper.

Based on static analysis hints

Apply this diff:

-const replaceMentions = (message: string, mentions: Array<{ mention: string; realName: string }>): string => {
-	if (!mentions.length) return message;
-
-	let parsedMessage = '';
-	let remaining = message;
-
-	for (const { mention, realName } of mentions) {
-		const regex = new RegExp(`(?<!\\w)${realName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?!\\w)`);
-		const position = remaining.search(regex);
-
-		if (position !== -1) {
-			parsedMessage += remaining.slice(0, position) + mention;
-			remaining = remaining.slice(position + realName.length);
-		} else if (realName.startsWith('!')) {
-			const allRegex = /(?<!\w)@all(?!\w)/;
-			const allPosition = remaining.search(allRegex);
-			if (allPosition !== -1) {
-				parsedMessage += remaining.slice(0, allPosition) + mention;
-				remaining = remaining.slice(allPosition + 4); // length of '@all'
-			}
-		}
-	}
-
-	parsedMessage += remaining;
-	return parsedMessage.trim();
-};
+const replaceMentions = (message: string, mentions: Array<{ mention: string; realName: string }>): string => {
+	if (!mentions.length) return message;
+
+	// Replace longer realNames first to avoid partial overlaps
+	const sorted = [...mentions].sort((a, b) => b.realName.length - a.realName.length);
+
+	const isWord = (ch: string) => /\w/.test(ch);
+	const replaceWord = (src: string, token: string, replacement: string): string => {
+		if (!token) return src;
+		let i = 0;
+		let out = '';
+		while (i < src.length) {
+			const at = src.indexOf(token, i);
+			if (at === -1) {
+				out += src.slice(i);
+				break;
+			}
+			const before = at === 0 ? '' : src[at - 1];
+			const after = at + token.length >= src.length ? '' : src[at + token.length];
+			if (!isWord(before) && !isWord(after)) {
+				out += src.slice(i, at) + replacement;
+				i = at + token.length;
+			} else {
+				// Not a word-bounded match; keep it and continue
+				out += src.slice(i, at + token.length);
+				i = at + token.length;
+			}
+		}
+		return out;
+	};
+
+	let result = message;
+	for (const { mention, realName } of sorted) {
+		// Room-level mentions: prefer @here over @all if present
+		if (realName.startsWith('!') || realName.startsWith('#')) {
+			result = replaceWord(result, '@here', mention);
+			result = replaceWord(result, '@all', mention);
+			continue;
+		}
+		result = replaceWord(result, realName, mention);
+	}
+	return result.trim();
+};

25-26: Constrain allowed attributes/schemes for during sanitization

stripHtml allows tags but doesn’t restrict attributes or schemes here. Explicitly allow only href, only http/https, and optionally drop non-Matrix targets to reduce XSS/phishing surface.

-const stripHtml = (html: string, keep: string[] = []): string => sanitizeHtml(html, { allowedTags: keep.includes('a') ? ['a'] : [] });
+const stripHtml = (html: string, keep: string[] = []): string =>
+	sanitizeHtml(html, {
+		allowedTags: keep.includes('a') ? ['a'] : [],
+		allowedAttributes: keep.includes('a') ? { a: ['href'] } : {},
+		allowProtocolRelative: false,
+		allowedSchemes: ['http', 'https'],
+		transformTags: keep.includes('a')
+			? {
+					a: (tagName, attribs) => ({
+						tagName,
+						attribs: attribs.href?.startsWith(MATRIX_TO_URL) ? { href: attribs.href } : {}, // drop non-matrix.to hrefs
+					}),
+			  }
+			: {},
+	});

75-79: Avoid unconditional leading spaces when inserting pills

Prepending a space for every replacement can introduce odd spacing. Compute a leading space only when the previous character isn’t whitespace.

-const replaceWithMentionPills = async (message: string, regex: RegExp, createPill: (match: string) => string): Promise<string> => {
-	const matches = Array.from(message.matchAll(regex), ([match]) => createPill(match.trimStart()));
-	let i = 0;
-	return message.replace(regex, () => ` ${matches[i++]}`);
-};
+const replaceWithMentionPills = async (message: string, regex: RegExp, createPill: (match: string) => string): Promise<string> => {
+	const matches = Array.from(message.matchAll(regex), ([match]) => createPill(match.trimStart()));
+	let i = 0;
+	return message.replace(regex, (_m, ...args) => {
+		const offset = (args[args.length - 2] as number) ?? 0;
+		const needsSpace = offset > 0 && /\S/.test(message[offset - 1]);
+		return `${needsSpace ? ' ' : ''}${matches[i++]}`;
+	});
+};

174-180: Remove double parse and duplicate reply assembly in toExternalQuoteMessageFormat

You parse Markdown twice and build two reply payloads to return one pair. Build once using plain text for body and the already‑formatted HTML (from toExternalMessageFormat) for formatted_body.

-	const markdownHtml = await marked.parse(message);
-	const withMentions = await toExternalMessageFormat({ message, externalRoomId, homeServerDomain });
-	const withMentionsHtml = await marked.parse(withMentions);
-
-	const reply1 = createReplyContent(externalRoomId, event, markdownHtml, withMentionsHtml);
-	const reply2 = createReplyContent(externalRoomId, event, message, withMentionsHtml);
-
-	return {
-		message: reply2.body,
-		formattedMessage: reply1.formatted_body ?? '',
-	};
+	const formattedWithMentions = await toExternalMessageFormat({ message, externalRoomId, homeServerDomain });
+	const reply = createReplyContent(externalRoomId, event, message, formattedWithMentions);
+	return {
+		message: reply.body,
+		formattedMessage: reply.formatted_body ?? '',
+	};

Also applies to: 181-184


135-137: Prefer sanitizer to drop quote tags instead of regex

You build a regex from tag names to strip /

. Since tags are known constants, it’s safe, but using sanitizeHtml to drop them avoids edge cases with nested/irregular markup.

Based on static analysis hints

Example:

let cleaned = sanitizeHtml(formattedMessage, {
  allowedTags: ['a'], // drop mx-reply/blockquote implicitly
  allowedAttributes: { a: ['href'] },
  allowedSchemes: ['http','https'],
});
ee/packages/federation-matrix/src/helpers/message.parsers.spec.ts (1)

78-87: Future: add a round‑trip test to preserve @here vs @ALL (if/when implemented)

If you adopt label‑preserving pills, add cases ensuring '@here' stays '@here' across toExternal → toInternal and in quotes.

Also applies to: 1064-1116, 1412-1444

📜 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 352199d and 4fc9cc7.

📒 Files selected for processing (2)
  • ee/packages/federation-matrix/src/helpers/message.parsers.spec.ts (1 hunks)
  • ee/packages/federation-matrix/src/helpers/message.parsers.ts (5 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
ee/packages/federation-matrix/src/helpers/message.parsers.spec.ts (1)
ee/packages/federation-matrix/src/helpers/message.parsers.ts (1)
  • toInternalQuoteMessageFormat (121-141)
🪛 ast-grep (0.39.5)
ee/packages/federation-matrix/src/helpers/message.parsers.ts

[warning] 54-54: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp((?<!\\w)${realName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?!\\w))
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html

(regexp-from-variable)


[warning] 135-135: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(<${tag}[^>]*>.*?</${tag}>, 'gis')
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html

(regexp-from-variable)

⏰ 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/helpers/message.parsers.spec.ts (1)

1020-1036: Good addition; closes the split‑script/onerror gap

This test meaningfully verifies sanitization against tricky split <script> and onerror/img payloads. LGTM.

@ggazzo ggazzo force-pushed the chore/fed-branded branch 7 times, most recently from 6fc8f40 to 928973e Compare October 8, 2025 02:36
@ggazzo ggazzo requested a review from a team as a code owner October 8, 2025 02:36
@ggazzo ggazzo force-pushed the chore/fed-branded branch 5 times, most recently from 89ebe54 to 94445d8 Compare October 8, 2025 02:47
Base automatically changed from chore/fed-branded to release-7.11.0 October 8, 2025 03:42
@ggazzo ggazzo force-pushed the refactor/internal-mentionpill branch from c252629 to 47fd833 Compare October 8, 2025 03:46
@rodrigok rodrigok force-pushed the refactor/internal-mentionpill branch 2 times, most recently from 344b975 to 89612f8 Compare October 10, 2025 00:18
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 (1)
ee/packages/federation-matrix/src/helpers/message.parsers.ts (1)

31-46: @here is collapsed to @ALL; no distinction in extractMentions.

The current implementation returns @all for any anchor that doesn't match a user pattern (line 37). This means @here and @all mentions are indistinguishable after extraction, which can lead to incorrect behavior if @here is expected to have different semantics (e.g., notifying only online users vs. all users).

This concern was previously raised in past review comments.

🧹 Nitpick comments (1)
ee/packages/federation-matrix/src/helpers/message.parsers.ts (1)

134-137: Dynamic regex from constant array is safe but consider precompilation.

The dynamic regex at line 136 is constructed from the MATRIX_QUOTE_TAGS constant array (not user input), so it's safe from injection. However, reconstructing the regex on every call is inefficient.

Consider precompiling the regex patterns:

+const QUOTE_TAG_REGEXES = MATRIX_QUOTE_TAGS.map(tag => new RegExp(`<${tag}[^>]*>.*?</${tag}>`, 'gis'));
+
 export const toInternalQuoteMessageFormat = async ({
 	homeServerDomain,
 	formattedMessage,
 	rawMessage,
 	messageToReplyToUrl,
 	senderExternalId,
 }: {
 	messageToReplyToUrl: string;
 	formattedMessage: string;
 	rawMessage: string;
 	homeServerDomain: string;
 	senderExternalId: string;
 }): Promise<string> => {
 	let cleaned = formattedMessage;
-	MATRIX_QUOTE_TAGS.forEach((tag) => {
-		cleaned = cleaned.replace(new RegExp(`<${tag}[^>]*>.*?</${tag}>`, 'gis'), '');
+	QUOTE_TAG_REGEXES.forEach((regex) => {
+		cleaned = cleaned.replace(regex, '');
 	});
 	cleaned = stripHtml(cleaned, ['a']);
 
 	return `[ ](${messageToReplyToUrl}) ${replaceMentions(stripQuotePrefix(rawMessage), extractMentions(cleaned, homeServerDomain, senderExternalId))}`;
 };
📜 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 344b975 and 89612f8.

⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (7)
  • .github/actions/build-docker/action.yml (0 hunks)
  • .github/workflows/ci.yml (0 hunks)
  • apps/meteor/.docker/Dockerfile.alpine (0 hunks)
  • apps/meteor/package.json (0 hunks)
  • ee/packages/federation-matrix/package.json (1 hunks)
  • ee/packages/federation-matrix/src/helpers/message.parsers.spec.ts (1 hunks)
  • ee/packages/federation-matrix/src/helpers/message.parsers.ts (5 hunks)
💤 Files with no reviewable changes (4)
  • .github/actions/build-docker/action.yml
  • apps/meteor/package.json
  • apps/meteor/.docker/Dockerfile.alpine
  • .github/workflows/ci.yml
🚧 Files skipped from review as they are similar to previous changes (1)
  • ee/packages/federation-matrix/package.json
🧰 Additional context used
🧬 Code graph analysis (1)
ee/packages/federation-matrix/src/helpers/message.parsers.spec.ts (1)
ee/packages/federation-matrix/src/helpers/message.parsers.ts (1)
  • toInternalQuoteMessageFormat (121-141)
🪛 ast-grep (0.39.6)
ee/packages/federation-matrix/src/helpers/message.parsers.ts

[warning] 54-54: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp((?<!\\w)${realName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?!\\w))
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html

(regexp-from-variable)


[warning] 135-135: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(<${tag}[^>]*>.*?</${tag}>, 'gis')
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html

(regexp-from-variable)

⏰ 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 (3)
ee/packages/federation-matrix/src/helpers/message.parsers.spec.ts (1)

1020-1036: Excellent addition of XSS sanitization test.

This test verifies that malicious HTML (including obfuscated script tags and event handlers) is properly sanitized in toInternalQuoteMessageFormat. The test confirms that the sanitization layer strips dangerous content while preserving the plain text message, which is essential for security in federated environments.

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

75-79: Add tests for replaceWithMentionPills edge cases.

No existing tests cover replaceWithMentionPills. Please add tests for:

  • Multiple consecutive matches
  • Matches at string boundaries
  • Empty or whitespace-only matches

48-73: Verify realName length constraints No upstream limits found on realName prior to its use in the negative lookarounds; confirm or enforce a max length/sanitization to mitigate ReDoS risk.

@rodrigok rodrigok force-pushed the refactor/internal-mentionpill branch 2 times, most recently from 5ce10a1 to 1a2264e Compare October 10, 2025 00:40
@rodrigok rodrigok force-pushed the refactor/internal-mentionpill branch from 1a2264e to 3b3edce Compare October 10, 2025 02:02
rodrigok
rodrigok previously approved these changes Oct 10, 2025
@rodrigok rodrigok force-pushed the refactor/internal-mentionpill branch 2 times, most recently from 8441b4f to daa8f05 Compare October 10, 2025 02:06
@rodrigok rodrigok force-pushed the refactor/internal-mentionpill branch from daa8f05 to 193fd19 Compare October 10, 2025 02:07
@ggazzo ggazzo added this to the 7.11.0 milestone Oct 10, 2025
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 (1)
ee/packages/federation-matrix/src/helpers/message.parsers.ts (1)

31-46: @here is collapsed into @ALL; preserve the original label.

This issue was previously flagged: when href points to a room ID (doesn't match the user pattern at line 35), the function returns @all for the mention regardless of whether the original pill text was @here or @all. This causes @here mentions to be lost.

The suggested fix from the previous review:

  1. Modify createMentionHtml to accept an optional label parameter
  2. Pass the match text when calling replaceWithMentionPills for general mentions
  3. Preserve @here vs @all by checking the text parameter at line 37
-const createMentionHtml = (id: string): string => `<a href="${MATRIX_TO_URL}${id}">${id}</a>`;
+const createMentionHtml = (id: string, label = id): string => `<a href="${MATRIX_TO_URL}${id}">${label}</a>`;

And at line 37:

-		return { mention: '@all', realName: text };
+		const mention = text === '@here' ? '@here' : '@all';
+		return { mention, realName: text };

And at line 153 in toExternalMessageFormat:

-	result = await replaceWithMentionPills(result, REGEX.general, () => createMentionHtml(externalRoomId));
+	result = await replaceWithMentionPills(result, REGEX.general, (match) => createMentionHtml(externalRoomId, match));
🧹 Nitpick comments (2)
ee/packages/federation-matrix/src/helpers/message.parsers.ts (2)

48-73: ReDoS risk still flagged by static analysis.

While the previous review marked the ReDoS concern as addressed, the static analysis tool continues to flag line 55 where a dynamic regex with negative lookbehind/lookahead is constructed from user-controlled realName. Although realName is escaped, the pattern complexity with lookarounds can still cause performance issues on certain inputs.

Consider validating realName length before constructing the regex or switching to a simpler replacement strategy without lookarounds:

const replaceMentions = (message: string, mentions: Array<{ mention: string; realName: string }>): string => {
	if (!mentions.length) return message;

	let result = message;
	// Sort by length descending to avoid partial replacements
	const sorted = [...mentions].sort((a, b) => b.realName.length - a.realName.length);
	
	for (const { mention, realName } of sorted) {
		const escaped = realName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
		const regex = new RegExp(`\\b${escaped}\\b`, 'g');
		result = result.replace(regex, mention);
	}
	
	return result.trim();
};

This approach uses word boundaries (\b) instead of lookarounds, which are more efficient and less prone to ReDoS.


160-185: LGTM! Properly formatted Matrix replies.

The rewritten toExternalQuoteMessageFormat correctly generates Matrix reply content with both plain text and HTML formatted bodies, including proper mention handling and reply metadata.

Optional optimization: Lines 174-176 call marked.parse multiple times. If performance becomes a concern, consider caching or restructuring to reduce redundant parsing:

const markdownHtml = await marked.parse(message);
const withMentionsHtml = await marked.parse(await toExternalMessageFormat({ message, externalRoomId, homeServerDomain }));

const reply1 = createReplyContent(externalRoomId, event, markdownHtml, withMentionsHtml);
const reply2 = createReplyContent(externalRoomId, event, message, withMentionsHtml);

This reduces the calls from 3 to 2 by reusing withMentionsHtml.

📜 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 1a2264e and 193fd19.

⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (7)
  • .github/actions/build-docker/action.yml (0 hunks)
  • .github/workflows/ci.yml (1 hunks)
  • apps/meteor/.docker/Dockerfile.alpine (0 hunks)
  • apps/meteor/package.json (0 hunks)
  • ee/packages/federation-matrix/package.json (1 hunks)
  • ee/packages/federation-matrix/src/helpers/message.parsers.spec.ts (1 hunks)
  • ee/packages/federation-matrix/src/helpers/message.parsers.ts (5 hunks)
💤 Files with no reviewable changes (3)
  • apps/meteor/.docker/Dockerfile.alpine
  • apps/meteor/package.json
  • .github/actions/build-docker/action.yml
🚧 Files skipped from review as they are similar to previous changes (2)
  • ee/packages/federation-matrix/src/helpers/message.parsers.spec.ts
  • .github/workflows/ci.yml
🧰 Additional context used
🪛 ast-grep (0.39.6)
ee/packages/federation-matrix/src/helpers/message.parsers.ts

[warning] 54-54: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp((?<!\\w)${realName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?!\\w))
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html

(regexp-from-variable)


[warning] 135-135: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(<${tag}[^>]*>.*?</${tag}>, 'gis')
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html

(regexp-from-variable)

⏰ 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 (8)
ee/packages/federation-matrix/package.json (2)

13-13: LGTM! Type version is more specific.

The update to @types/sanitize-html from ^2 to ^2.13.0 improves reproducibility and is compatible with the [email protected] dependency at line 52.


37-55: All references to @vector-im/matrix-bot-sdk removed
Search found no remaining imports, requires, or mentions in the codebase.

ee/packages/federation-matrix/src/helpers/message.parsers.ts (6)

1-20: LGTM! Well-structured Matrix types and patterns.

The new type definitions and regex constants provide a solid foundation for the batched mention pipeline. The patterns correctly handle Matrix.to URLs and various mention formats.


22-29: LGTM! Clean HTML utility functions.

The escapeHtml, stripHtml, createMentionHtml, and extractAnchors helpers are well-implemented and provide clear, focused functionality for the mention pipeline.


75-107: LGTM! Helper functions are well-implemented.

replaceWithMentionPills, stripQuotePrefix, and createReplyContent provide clean, focused functionality. The Matrix reply format in createReplyContent follows the standard specification correctly.


109-119: LGTM! Simplified and clean.

The refactored toInternalMessageFormat delegates to the new batched pipeline, making the logic clear and maintainable.


121-141: LGTM! Quote handling is correct.

The refactored toInternalQuoteMessageFormat properly strips Matrix quote tags, preserves anchor tags, and applies mention replacement.

Note on static analysis warning: Line 136 is flagged for ReDoS risk, but this is a false positive since tag comes from the constant array MATRIX_QUOTE_TAGS = ['mx-reply', 'blockquote'], not user input. The pattern is safe.


143-158: LGTM! Batched mention replacement pipeline.

The refactored toExternalMessageFormat correctly applies the batched replacement pipeline for general, external, and internal mentions, then converts to Matrix HTML format.

@ggazzo ggazzo added the stat: QA assured Means it has been tested and approved by a company insider label Oct 10, 2025
@dionisio-bot dionisio-bot bot added the stat: ready to merge PR tested and approved waiting for merge label Oct 10, 2025
@ggazzo ggazzo merged commit 3e91d9e into release-7.11.0 Oct 10, 2025
21 checks passed
@ggazzo ggazzo deleted the refactor/internal-mentionpill branch October 10, 2025 02:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

stat: QA assured Means it has been tested and approved by a company insider stat: ready to merge PR tested and approved waiting for merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants