Matrix: gate expensive inbound work on mentions

This commit is contained in:
Gustavo Madeira Santana
2026-03-12 08:28:41 +00:00
parent 87cdb31412
commit de2464d50f
2 changed files with 176 additions and 52 deletions

View File

@@ -10,6 +10,7 @@ import {
createMatrixTextMessageEvent,
} from "./handler.test-helpers.js";
import type { MatrixRawEvent } from "./types.js";
import { EventType } from "./types.js";
const sendMessageMatrixMock = vi.hoisted(() =>
vi.fn(async (..._args: unknown[]) => ({ messageId: "evt", roomId: "!room" })),
@@ -252,6 +253,88 @@ describe("matrix monitor handler pairing account scope", () => {
expect(recordInboundSession).not.toHaveBeenCalled();
});
it("skips media downloads for unmentioned group media messages", async () => {
const downloadContent = vi.fn(async () => Buffer.from("image"));
const { handler, resolveAgentRoute } = createMatrixHandlerTestHarness({
client: {
downloadContent,
},
isDirectMessage: false,
mentionRegexes: [/@bot/i],
getMemberDisplayName: async () => "sender",
});
await handler("!room:example.org", {
type: EventType.RoomMessage,
sender: "@user:example.org",
event_id: "$media1",
origin_server_ts: Date.now(),
content: {
msgtype: "m.image",
body: "",
url: "mxc://example.org/media",
info: {
mimetype: "image/png",
size: 5,
},
},
} as MatrixRawEvent);
expect(downloadContent).not.toHaveBeenCalled();
expect(resolveAgentRoute).not.toHaveBeenCalled();
});
it("skips poll snapshot fetches for unmentioned group poll responses", async () => {
const getEvent = vi.fn(async () => ({
event_id: "$poll",
sender: "@user:example.org",
type: "m.poll.start",
origin_server_ts: Date.now(),
content: {
"m.poll.start": {
question: { "m.text": "Lunch?" },
kind: "m.poll.disclosed",
max_selections: 1,
answers: [{ id: "a1", "m.text": "Pizza" }],
},
},
}));
const getRelations = vi.fn(async () => ({
events: [],
nextBatch: null,
prevBatch: null,
}));
const { handler, resolveAgentRoute } = createMatrixHandlerTestHarness({
client: {
getEvent,
getRelations,
},
isDirectMessage: false,
mentionRegexes: [/@bot/i],
getMemberDisplayName: async () => "sender",
});
await handler("!room:example.org", {
type: "m.poll.response",
sender: "@user:example.org",
event_id: "$poll-response-1",
origin_server_ts: Date.now(),
content: {
"m.poll.response": {
answers: ["a1"],
},
"m.relates_to": {
rel_type: "m.reference",
event_id: "$poll",
},
},
} as MatrixRawEvent);
expect(getEvent).not.toHaveBeenCalled();
expect(getRelations).not.toHaveBeenCalled();
expect(resolveAgentRoute).not.toHaveBeenCalled();
});
it("records thread starter context for inbound thread replies", async () => {
const { handler, finalizeInboundContext, recordInboundSession } =
createMatrixHandlerTestHarness({

View File

@@ -14,7 +14,12 @@ import {
} from "openclaw/plugin-sdk/matrix";
import type { CoreConfig, MatrixRoomConfig, ReplyToMode } from "../../types.js";
import { fetchMatrixPollSnapshot } from "../poll-summary.js";
import { isPollEventType } from "../poll-types.js";
import {
formatPollAsText,
isPollEventType,
isPollStartType,
parsePollStartContent,
} from "../poll-types.js";
import type { LocationMessageEventContent, MatrixClient } from "../sdk.js";
import {
reactMatrixMessage,
@@ -75,6 +80,26 @@ export type MatrixMonitorHandlerParams = {
getMemberDisplayName: (roomId: string, userId: string) => Promise<string>;
};
function resolveMatrixMentionPrecheckText(params: {
eventType: string;
content: RoomMessageEventContent;
locationText?: string | null;
}): string {
if (params.locationText?.trim()) {
return params.locationText.trim();
}
if (typeof params.content.body === "string" && params.content.body.trim()) {
return params.content.body.trim();
}
if (isPollStartType(params.eventType)) {
const parsed = parsePollStartContent(params.content as never);
if (parsed) {
return formatPollAsText(parsed);
}
}
return "";
}
export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParams) {
const {
client,
@@ -205,21 +230,6 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
const roomAliases = [roomInfo.canonicalAlias ?? "", ...roomInfo.altAliases].filter(Boolean);
let content = event.content as RoomMessageEventContent;
if (isPollEvent) {
const pollSnapshot = await fetchMatrixPollSnapshot(client, roomId, event).catch((err) => {
logVerboseMessage(
`matrix: failed resolving poll snapshot room=${roomId} id=${event.event_id ?? "unknown"}: ${String(err)}`,
);
return null;
});
if (!pollSnapshot) {
return;
}
content = {
msgtype: "m.text",
body: pollSnapshot.text,
} as unknown as RoomMessageEventContent;
}
if (
eventType === EventType.RoomMessage &&
@@ -406,13 +416,11 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
return;
}
const rawBody =
locationPayload?.text ?? (typeof content.body === "string" ? content.body.trim() : "");
let media: {
path: string;
contentType?: string;
placeholder: string;
} | null = null;
const mentionPrecheckText = resolveMatrixMentionPrecheckText({
eventType,
content,
locationText: locationPayload?.text,
});
const contentUrl =
"url" in content && typeof content.url === "string" ? content.url : undefined;
const contentFile =
@@ -420,40 +428,14 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
? content.file
: undefined;
const mediaUrl = contentUrl ?? contentFile?.url;
if (!rawBody && !mediaUrl) {
return;
}
const contentInfo =
"info" in content && content.info && typeof content.info === "object"
? (content.info as { mimetype?: string; size?: number })
: undefined;
const contentType = contentInfo?.mimetype;
const contentSize = typeof contentInfo?.size === "number" ? contentInfo.size : undefined;
if (mediaUrl?.startsWith("mxc://")) {
try {
media = await downloadMatrixMedia({
client,
mxcUrl: mediaUrl,
contentType,
sizeBytes: contentSize,
maxBytes: mediaMaxBytes,
file: contentFile,
});
} catch (err) {
logVerboseMessage(`matrix: media download failed: ${String(err)}`);
}
}
const bodyText = rawBody || media?.placeholder || "";
if (!bodyText) {
if (!mentionPrecheckText && !mediaUrl && !isPollEvent) {
return;
}
const { wasMentioned, hasExplicitMention } = resolveMentions({
content,
userId: selfUserId,
text: bodyText,
text: mentionPrecheckText,
mentionRegexes,
});
const allowTextCommands = core.channel.commands.shouldHandleTextCommands({
@@ -461,7 +443,10 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
surface: "matrix",
});
const useAccessGroups = cfg.commands?.useAccessGroups !== false;
const hasControlCommandInMessage = core.channel.text.hasControlCommand(bodyText, cfg);
const hasControlCommandInMessage = core.channel.text.hasControlCommand(
mentionPrecheckText,
cfg,
);
const commandGate = resolveControlCommandGate({
useAccessGroups,
authorizers: commandAuthorizers,
@@ -501,6 +486,62 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
return;
}
if (isPollEvent) {
const pollSnapshot = await fetchMatrixPollSnapshot(client, roomId, event).catch((err) => {
logVerboseMessage(
`matrix: failed resolving poll snapshot room=${roomId} id=${event.event_id ?? "unknown"}: ${String(err)}`,
);
return null;
});
if (!pollSnapshot) {
return;
}
content = {
msgtype: "m.text",
body: pollSnapshot.text,
} as unknown as RoomMessageEventContent;
}
let media: {
path: string;
contentType?: string;
placeholder: string;
} | null = null;
const finalContentUrl =
"url" in content && typeof content.url === "string" ? content.url : undefined;
const finalContentFile =
"file" in content && content.file && typeof content.file === "object"
? content.file
: undefined;
const finalMediaUrl = finalContentUrl ?? finalContentFile?.url;
const contentInfo =
"info" in content && content.info && typeof content.info === "object"
? (content.info as { mimetype?: string; size?: number })
: undefined;
const contentType = contentInfo?.mimetype;
const contentSize = typeof contentInfo?.size === "number" ? contentInfo.size : undefined;
if (finalMediaUrl?.startsWith("mxc://")) {
try {
media = await downloadMatrixMedia({
client,
mxcUrl: finalMediaUrl,
contentType,
sizeBytes: contentSize,
maxBytes: mediaMaxBytes,
file: finalContentFile,
});
} catch (err) {
logVerboseMessage(`matrix: media download failed: ${String(err)}`);
}
}
const rawBody =
locationPayload?.text ?? (typeof content.body === "string" ? content.body.trim() : "");
const bodyText = rawBody || media?.placeholder || "";
if (!bodyText) {
return;
}
const messageId = event.event_id ?? "";
const replyToEventId = content["m.relates_to"]?.["m.in_reply_to"]?.event_id;
const threadRootId = resolveMatrixThreadRootId({ event, content });