mirror of
https://github.com/openclaw/openclaw.git
synced 2026-02-19 18:39:20 -05:00
fix(matrix): detect mentions in formatted_body matrix.to links (#16941)
* fix(matrix): detect mentions in formatted_body matrix.to links
Many Matrix clients (including Element) send mentions using HTML links
in formatted_body instead of or in addition to the m.mentions field:
```json
{
"formatted_body": "<a href=\"https://matrix.to/#/@bot:matrix.org\">Bot</a>: hello",
"m.mentions": null
}
```
This change adds detection for matrix.to links in formatted_body,
supporting both plain and URL-encoded user IDs.
Changes:
- Add checkFormattedBodyMention() helper function
- Check formatted_body in resolveMentions()
- Add comprehensive test coverage
Fixes #6982
* Update extensions/matrix/src/matrix/monitor/mentions.ts
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
---------
Co-authored-by: zerone0x <zerone0x@users.noreply.github.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
This commit is contained in:
154
extensions/matrix/src/matrix/monitor/mentions.test.ts
Normal file
154
extensions/matrix/src/matrix/monitor/mentions.test.ts
Normal file
@@ -0,0 +1,154 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
// Mock the runtime before importing resolveMentions
|
||||
vi.mock("../../runtime.js", () => ({
|
||||
getMatrixRuntime: () => ({
|
||||
channel: {
|
||||
mentions: {
|
||||
matchesMentionPatterns: (text: string, patterns: RegExp[]) =>
|
||||
patterns.some((p) => p.test(text)),
|
||||
},
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
import { resolveMentions } from "./mentions.js";
|
||||
|
||||
describe("resolveMentions", () => {
|
||||
const userId = "@bot:matrix.org";
|
||||
const mentionRegexes = [/@bot/i];
|
||||
|
||||
describe("m.mentions field", () => {
|
||||
it("detects mention via m.mentions.user_ids", () => {
|
||||
const result = resolveMentions({
|
||||
content: {
|
||||
msgtype: "m.text",
|
||||
body: "hello",
|
||||
"m.mentions": { user_ids: ["@bot:matrix.org"] },
|
||||
},
|
||||
userId,
|
||||
text: "hello",
|
||||
mentionRegexes,
|
||||
});
|
||||
expect(result.wasMentioned).toBe(true);
|
||||
expect(result.hasExplicitMention).toBe(true);
|
||||
});
|
||||
|
||||
it("detects room mention via m.mentions.room", () => {
|
||||
const result = resolveMentions({
|
||||
content: {
|
||||
msgtype: "m.text",
|
||||
body: "hello everyone",
|
||||
"m.mentions": { room: true },
|
||||
},
|
||||
userId,
|
||||
text: "hello everyone",
|
||||
mentionRegexes,
|
||||
});
|
||||
expect(result.wasMentioned).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatted_body matrix.to links", () => {
|
||||
it("detects mention in formatted_body with plain user ID", () => {
|
||||
const result = resolveMentions({
|
||||
content: {
|
||||
msgtype: "m.text",
|
||||
body: "Bot: hello",
|
||||
formatted_body: '<a href="https://matrix.to/#/@bot:matrix.org">Bot</a>: hello',
|
||||
},
|
||||
userId,
|
||||
text: "Bot: hello",
|
||||
mentionRegexes: [],
|
||||
});
|
||||
expect(result.wasMentioned).toBe(true);
|
||||
});
|
||||
|
||||
it("detects mention in formatted_body with URL-encoded user ID", () => {
|
||||
const result = resolveMentions({
|
||||
content: {
|
||||
msgtype: "m.text",
|
||||
body: "Bot: hello",
|
||||
formatted_body: '<a href="https://matrix.to/#/%40bot%3Amatrix.org">Bot</a>: hello',
|
||||
},
|
||||
userId,
|
||||
text: "Bot: hello",
|
||||
mentionRegexes: [],
|
||||
});
|
||||
expect(result.wasMentioned).toBe(true);
|
||||
});
|
||||
|
||||
it("detects mention with single quotes in href", () => {
|
||||
const result = resolveMentions({
|
||||
content: {
|
||||
msgtype: "m.text",
|
||||
body: "Bot: hello",
|
||||
formatted_body: "<a href='https://matrix.to/#/@bot:matrix.org'>Bot</a>: hello",
|
||||
},
|
||||
userId,
|
||||
text: "Bot: hello",
|
||||
mentionRegexes: [],
|
||||
});
|
||||
expect(result.wasMentioned).toBe(true);
|
||||
});
|
||||
|
||||
it("does not detect mention for different user ID", () => {
|
||||
const result = resolveMentions({
|
||||
content: {
|
||||
msgtype: "m.text",
|
||||
body: "Other: hello",
|
||||
formatted_body: '<a href="https://matrix.to/#/@other:matrix.org">Other</a>: hello',
|
||||
},
|
||||
userId,
|
||||
text: "Other: hello",
|
||||
mentionRegexes: [],
|
||||
});
|
||||
expect(result.wasMentioned).toBe(false);
|
||||
});
|
||||
|
||||
it("does not false-positive on partial user ID match", () => {
|
||||
const result = resolveMentions({
|
||||
content: {
|
||||
msgtype: "m.text",
|
||||
body: "Bot2: hello",
|
||||
formatted_body: '<a href="https://matrix.to/#/@bot2:matrix.org">Bot2</a>: hello',
|
||||
},
|
||||
userId: "@bot:matrix.org",
|
||||
text: "Bot2: hello",
|
||||
mentionRegexes: [],
|
||||
});
|
||||
expect(result.wasMentioned).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("regex patterns", () => {
|
||||
it("detects mention via regex pattern in body text", () => {
|
||||
const result = resolveMentions({
|
||||
content: {
|
||||
msgtype: "m.text",
|
||||
body: "hey @bot can you help?",
|
||||
},
|
||||
userId,
|
||||
text: "hey @bot can you help?",
|
||||
mentionRegexes,
|
||||
});
|
||||
expect(result.wasMentioned).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("no mention", () => {
|
||||
it("returns false when no mention is present", () => {
|
||||
const result = resolveMentions({
|
||||
content: {
|
||||
msgtype: "m.text",
|
||||
body: "hello world",
|
||||
},
|
||||
userId,
|
||||
text: "hello world",
|
||||
mentionRegexes,
|
||||
});
|
||||
expect(result.wasMentioned).toBe(false);
|
||||
expect(result.hasExplicitMention).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -4,12 +4,36 @@ import { getMatrixRuntime } from "../../runtime.js";
|
||||
type MessageContentWithMentions = {
|
||||
msgtype: string;
|
||||
body: string;
|
||||
formatted_body?: string;
|
||||
"m.mentions"?: {
|
||||
user_ids?: string[];
|
||||
room?: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if the formatted_body contains a matrix.to mention link for the given user ID.
|
||||
* Many Matrix clients (including Element) use HTML links in formatted_body instead of
|
||||
* or in addition to the m.mentions field.
|
||||
*/
|
||||
function checkFormattedBodyMention(formattedBody: string | undefined, userId: string): boolean {
|
||||
if (!formattedBody || !userId) {
|
||||
return false;
|
||||
}
|
||||
// Escape special regex characters in the user ID (e.g., @user:matrix.org)
|
||||
const escapedUserId = userId.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
// Match matrix.to links with the user ID, handling both URL-encoded and plain formats
|
||||
// Example: href="https://matrix.to/#/@user:matrix.org" or href="https://matrix.to/#/%40user%3Amatrix.org"
|
||||
const plainPattern = new RegExp(`href=["']https://matrix\\.to/#/${escapedUserId}["']`, "i");
|
||||
if (plainPattern.test(formattedBody)) {
|
||||
return true;
|
||||
}
|
||||
// Also check URL-encoded version (@ -> %40, : -> %3A)
|
||||
const encodedUserId = encodeURIComponent(userId).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
const encodedPattern = new RegExp(`href=["']https://matrix\\.to/#/${encodedUserId}["']`, "i");
|
||||
return encodedPattern.test(formattedBody);
|
||||
}
|
||||
|
||||
export function resolveMentions(params: {
|
||||
content: MessageContentWithMentions;
|
||||
userId?: string | null;
|
||||
@@ -20,9 +44,16 @@ export function resolveMentions(params: {
|
||||
const mentionedUsers = Array.isArray(mentions?.user_ids)
|
||||
? new Set(mentions.user_ids)
|
||||
: new Set<string>();
|
||||
|
||||
// Check formatted_body for matrix.to mention links (legacy/alternative mention format)
|
||||
const mentionedInFormattedBody = params.userId
|
||||
? checkFormattedBodyMention(params.content.formatted_body, params.userId)
|
||||
: false;
|
||||
|
||||
const wasMentioned =
|
||||
Boolean(mentions?.room) ||
|
||||
(params.userId ? mentionedUsers.has(params.userId) : false) ||
|
||||
mentionedInFormattedBody ||
|
||||
getMatrixRuntime().channel.mentions.matchesMentionPatterns(
|
||||
params.text ?? "",
|
||||
params.mentionRegexes,
|
||||
|
||||
Reference in New Issue
Block a user