Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion ee/apps/federation-service/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
"@rocket.chat/core-typings": "workspace:*",
"@rocket.chat/emitter": "^0.31.25",
"@rocket.chat/federation-matrix": "workspace:^",
"@rocket.chat/federation-sdk": "0.1.9",
"@rocket.chat/federation-sdk": "0.1.10",
"@rocket.chat/http-router": "workspace:*",
"@rocket.chat/instance-status": "workspace:^",
"@rocket.chat/license": "workspace:^",
Expand Down
2 changes: 1 addition & 1 deletion ee/packages/federation-matrix/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
"@rocket.chat/core-services": "workspace:^",
"@rocket.chat/core-typings": "workspace:^",
"@rocket.chat/emitter": "^0.31.25",
"@rocket.chat/federation-sdk": "0.1.9",
"@rocket.chat/federation-sdk": "0.1.10",
"@rocket.chat/http-router": "workspace:^",
"@rocket.chat/license": "workspace:^",
"@rocket.chat/models": "workspace:^",
Expand Down
18 changes: 13 additions & 5 deletions ee/packages/federation-matrix/src/events/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,15 +136,23 @@ export function message(emitter: Emitter<HomeserverEventSignatures>, serverName:

const relation = content['m.relates_to'];

const isThreadMessage = relation && relation.rel_type === 'm.thread';
// SPEC: For example, an m.thread relationship type denotes that the event is part of a “thread” of messages and should be rendered as such.
const hasRelation = relation && 'rel_type' in relation;

const isThreadMessage = hasRelation && relation.rel_type === 'm.thread';

const threadRootEventId = isThreadMessage && relation.event_id;

const quoteMessageEventId = relation && 'm.in_reply_to' in relation && relation['m.in_reply_to']?.event_id;
// SPEC: Though rich replies form a relationship to another event, they do not use rel_type to create this relationship.
// Instead, a subkey named m.in_reply_to is used to describe the reply’s relationship,
const isRichReply = relation && !('rel_type' in relation) && 'm.in_reply_to' in relation;

const quoteMessageEventId = isRichReply && relation['m.in_reply_to']?.event_id;

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

const isEditedMessage = relation?.rel_type === 'm.replace';
if (isEditedMessage && relation?.event_id && data.content['m.new_content']) {
const isEditedMessage = hasRelation && relation.rel_type === 'm.replace';
if (isEditedMessage && relation.event_id && data.content['m.new_content']) {
logger.debug('Received edited message from Matrix, updating existing message');
Comment on lines +140 to 156
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 | 🟠 Major

Restore quote detection when rel_type is present on replies.

Element and other Matrix homeservers routinely send rich replies with m.relates_to.rel_type = "m.reference" (per MSC2677). By requiring the property to be absent (!('rel_type' in relation)), we now skip quote handling for those events, so legitimate replies regress to plain messages. Please loosen the guard to treat replies as quotes whenever m.in_reply_to exists, except for the explicit thread/edit types.

-	const hasRelation = relation && 'rel_type' in relation;
-
-	const isThreadMessage = hasRelation && relation.rel_type === 'm.thread';
+	const hasRelation = relation && 'rel_type' in relation;
+	const relationRelType = hasRelation ? relation.rel_type : undefined;
+
+	const isThreadMessage = relationRelType === 'm.thread';
@@
-	const isRichReply = relation && !('rel_type' in relation) && 'm.in_reply_to' in relation;
+	const isRichReply =
+		!!relation &&
+		'm.in_reply_to' in relation &&
+		relationRelType !== 'm.thread' &&
+		relationRelType !== 'm.replace';
@@
-	const isEditedMessage = hasRelation && relation.rel_type === 'm.replace';
+	const isEditedMessage = relationRelType === 'm.replace';
📝 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
const hasRelation = relation && 'rel_type' in relation;
const isThreadMessage = hasRelation && relation.rel_type === 'm.thread';
const threadRootEventId = isThreadMessage && relation.event_id;
const quoteMessageEventId = relation && 'm.in_reply_to' in relation && relation['m.in_reply_to']?.event_id;
// SPEC: Though rich replies form a relationship to another event, they do not use rel_type to create this relationship.
// Instead, a subkey named m.in_reply_to is used to describe the reply’s relationship,
const isRichReply = relation && !('rel_type' in relation) && 'm.in_reply_to' in relation;
const quoteMessageEventId = isRichReply && relation['m.in_reply_to']?.event_id;
const thread = threadRootEventId ? await getThreadMessageId(threadRootEventId) : undefined;
const isEditedMessage = relation?.rel_type === 'm.replace';
if (isEditedMessage && relation?.event_id && data.content['m.new_content']) {
const isEditedMessage = hasRelation && relation.rel_type === 'm.replace';
if (isEditedMessage && relation.event_id && data.content['m.new_content']) {
logger.debug('Received edited message from Matrix, updating existing message');
const hasRelation = relation && 'rel_type' in relation;
const relationRelType = hasRelation ? relation.rel_type : undefined;
const isThreadMessage = relationRelType === 'm.thread';
const threadRootEventId = isThreadMessage && relation.event_id;
// SPEC: Though rich replies form a relationship to another event, they do not use rel_type to create this relationship.
// Instead, a subkey named m.in_reply_to is used to describe the reply’s relationship,
const isRichReply =
!!relation &&
'm.in_reply_to' in relation &&
relationRelType !== 'm.thread' &&
relationRelType !== 'm.replace';
const quoteMessageEventId = isRichReply && relation['m.in_reply_to']?.event_id;
const thread = threadRootEventId ? await getThreadMessageId(threadRootEventId) : undefined;
const isEditedMessage = relationRelType === 'm.replace';
if (isEditedMessage && relation.event_id && data.content['m.new_content']) {
logger.debug('Received edited message from Matrix, updating existing message');
🤖 Prompt for AI Agents
In ee/packages/federation-matrix/src/events/message.ts around lines 140 to 156,
the current check treats a reply as a rich reply only when rel_type is absent,
which skips quote handling for replies that include rel_type (e.g.
"m.reference"); update the logic so that a reply is considered a rich reply
whenever relation['m.in_reply_to'] exists (and relation is defined), but still
exclude events that are explicit threads (rel_type === 'm.thread') or edits
(rel_type === 'm.replace'). Concretely, compute hasRelation safely, keep
isThreadMessage and isEditedMessage as-is, then set isRichReply = relation &&
relation['m.in_reply_to'] && !isThreadMessage && !isEditedMessage so
quoteMessageEventId is derived when m.in_reply_to.event_id exists; ensure
threadRootEventId and edits remain unaffected.

const originalMessage = await Messages.findOneByFederationId(relation.event_id);
if (!originalMessage) {
Expand Down Expand Up @@ -255,7 +263,7 @@ export function message(emitter: Emitter<HomeserverEventSignatures>, serverName:
});
}
} catch (error) {
logger.error('Error processing Matrix message:', error);
logger.error(error, 'Error processing Matrix message:');
}
});

Expand Down
12 changes: 6 additions & 6 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -7543,7 +7543,7 @@ __metadata:
"@rocket.chat/core-typings": "workspace:^"
"@rocket.chat/emitter": "npm:^0.31.25"
"@rocket.chat/eslint-config": "workspace:^"
"@rocket.chat/federation-sdk": "npm:0.1.9"
"@rocket.chat/federation-sdk": "npm:0.1.10"
"@rocket.chat/http-router": "workspace:^"
"@rocket.chat/license": "workspace:^"
"@rocket.chat/models": "workspace:^"
Expand All @@ -7569,9 +7569,9 @@ __metadata:
languageName: unknown
linkType: soft

"@rocket.chat/federation-sdk@npm:0.1.9":
version: 0.1.9
resolution: "@rocket.chat/federation-sdk@npm:0.1.9"
"@rocket.chat/federation-sdk@npm:0.1.10":
version: 0.1.10
resolution: "@rocket.chat/federation-sdk@npm:0.1.10"
dependencies:
"@datastructures-js/priority-queue": "npm:^6.3.3"
"@noble/ed25519": "npm:^3.0.0"
Expand All @@ -7584,7 +7584,7 @@ __metadata:
zod: "npm:^3.22.4"
peerDependencies:
typescript: ~5.9.2
checksum: 10/ecc12614b673868d7572ffe865fe3d4b3786bb2a20e5c210ef1e1a307b35588bf14349eab3a5852d64621dda4e208f7094e8e4ff5c44803ad9c3207015f8769b
checksum: 10/39ec551128f6717b561c122cebab2354635f4d2dd6bb77be7d3581f4cbfe4f6e63953acf4764eba0e9e36239a5019d52d5f0426bc2968bca2674ed4a3ce60284
languageName: node
linkType: hard

Expand All @@ -7597,7 +7597,7 @@ __metadata:
"@rocket.chat/core-typings": "workspace:*"
"@rocket.chat/emitter": "npm:^0.31.25"
"@rocket.chat/federation-matrix": "workspace:^"
"@rocket.chat/federation-sdk": "npm:0.1.9"
"@rocket.chat/federation-sdk": "npm:0.1.10"
"@rocket.chat/http-router": "workspace:*"
"@rocket.chat/instance-status": "workspace:^"
"@rocket.chat/license": "workspace:^"
Expand Down
Loading