mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-18 10:07:27 +00:00
Matrix: gate expensive inbound work on mentions
This commit is contained in:
@@ -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({
|
||||
|
||||
@@ -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 });
|
||||
|
||||
Reference in New Issue
Block a user