@openclaw/matrix 2026.1.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +59 -0
- package/index.ts +18 -0
- package/openclaw.plugin.json +11 -0
- package/package.json +36 -0
- package/src/actions.ts +185 -0
- package/src/channel.directory.test.ts +56 -0
- package/src/channel.ts +417 -0
- package/src/config-schema.ts +62 -0
- package/src/directory-live.ts +175 -0
- package/src/group-mentions.ts +61 -0
- package/src/matrix/accounts.test.ts +83 -0
- package/src/matrix/accounts.ts +63 -0
- package/src/matrix/actions/client.ts +53 -0
- package/src/matrix/actions/messages.ts +120 -0
- package/src/matrix/actions/pins.ts +70 -0
- package/src/matrix/actions/reactions.ts +84 -0
- package/src/matrix/actions/room.ts +88 -0
- package/src/matrix/actions/summary.ts +77 -0
- package/src/matrix/actions/types.ts +84 -0
- package/src/matrix/actions.ts +15 -0
- package/src/matrix/active-client.ts +11 -0
- package/src/matrix/client/config.ts +165 -0
- package/src/matrix/client/create-client.ts +127 -0
- package/src/matrix/client/logging.ts +35 -0
- package/src/matrix/client/runtime.ts +4 -0
- package/src/matrix/client/shared.ts +169 -0
- package/src/matrix/client/storage.ts +131 -0
- package/src/matrix/client/types.ts +34 -0
- package/src/matrix/client.test.ts +57 -0
- package/src/matrix/client.ts +9 -0
- package/src/matrix/credentials.ts +103 -0
- package/src/matrix/deps.ts +57 -0
- package/src/matrix/format.test.ts +34 -0
- package/src/matrix/format.ts +22 -0
- package/src/matrix/index.ts +11 -0
- package/src/matrix/monitor/allowlist.ts +58 -0
- package/src/matrix/monitor/auto-join.ts +68 -0
- package/src/matrix/monitor/direct.ts +105 -0
- package/src/matrix/monitor/events.ts +103 -0
- package/src/matrix/monitor/handler.ts +645 -0
- package/src/matrix/monitor/index.ts +279 -0
- package/src/matrix/monitor/location.ts +83 -0
- package/src/matrix/monitor/media.test.ts +103 -0
- package/src/matrix/monitor/media.ts +113 -0
- package/src/matrix/monitor/mentions.ts +31 -0
- package/src/matrix/monitor/replies.ts +96 -0
- package/src/matrix/monitor/room-info.ts +58 -0
- package/src/matrix/monitor/rooms.ts +43 -0
- package/src/matrix/monitor/threads.ts +64 -0
- package/src/matrix/monitor/types.ts +39 -0
- package/src/matrix/poll-types.test.ts +22 -0
- package/src/matrix/poll-types.ts +157 -0
- package/src/matrix/probe.ts +70 -0
- package/src/matrix/send/client.ts +63 -0
- package/src/matrix/send/formatting.ts +92 -0
- package/src/matrix/send/media.ts +220 -0
- package/src/matrix/send/targets.test.ts +102 -0
- package/src/matrix/send/targets.ts +144 -0
- package/src/matrix/send/types.ts +109 -0
- package/src/matrix/send.test.ts +172 -0
- package/src/matrix/send.ts +255 -0
- package/src/onboarding.ts +432 -0
- package/src/outbound.ts +53 -0
- package/src/resolve-targets.ts +89 -0
- package/src/runtime.ts +14 -0
- package/src/tool-actions.ts +160 -0
- package/src/types.ts +95 -0
|
@@ -0,0 +1,645 @@
|
|
|
1
|
+
import type { LocationMessageEventContent, MatrixClient } from "@vector-im/matrix-bot-sdk";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
createReplyPrefixContext,
|
|
5
|
+
createTypingCallbacks,
|
|
6
|
+
formatAllowlistMatchMeta,
|
|
7
|
+
logInboundDrop,
|
|
8
|
+
logTypingFailure,
|
|
9
|
+
resolveControlCommandGate,
|
|
10
|
+
type RuntimeEnv,
|
|
11
|
+
} from "openclaw/plugin-sdk";
|
|
12
|
+
import type { CoreConfig, ReplyToMode } from "../../types.js";
|
|
13
|
+
import {
|
|
14
|
+
formatPollAsText,
|
|
15
|
+
isPollStartType,
|
|
16
|
+
parsePollStartContent,
|
|
17
|
+
type PollStartContent,
|
|
18
|
+
} from "../poll-types.js";
|
|
19
|
+
import { reactMatrixMessage, sendMessageMatrix, sendReadReceiptMatrix, sendTypingMatrix } from "../send.js";
|
|
20
|
+
import {
|
|
21
|
+
resolveMatrixAllowListMatch,
|
|
22
|
+
resolveMatrixAllowListMatches,
|
|
23
|
+
normalizeAllowListLower,
|
|
24
|
+
} from "./allowlist.js";
|
|
25
|
+
import { downloadMatrixMedia } from "./media.js";
|
|
26
|
+
import { resolveMentions } from "./mentions.js";
|
|
27
|
+
import { deliverMatrixReplies } from "./replies.js";
|
|
28
|
+
import { resolveMatrixRoomConfig } from "./rooms.js";
|
|
29
|
+
import { resolveMatrixThreadRootId, resolveMatrixThreadTarget } from "./threads.js";
|
|
30
|
+
import { resolveMatrixLocation, type MatrixLocationPayload } from "./location.js";
|
|
31
|
+
import type { MatrixRawEvent, RoomMessageEventContent } from "./types.js";
|
|
32
|
+
import { EventType, RelationType } from "./types.js";
|
|
33
|
+
|
|
34
|
+
export type MatrixMonitorHandlerParams = {
|
|
35
|
+
client: MatrixClient;
|
|
36
|
+
core: {
|
|
37
|
+
logging: {
|
|
38
|
+
shouldLogVerbose: () => boolean;
|
|
39
|
+
};
|
|
40
|
+
channel: typeof import("openclaw/plugin-sdk")["channel"];
|
|
41
|
+
system: {
|
|
42
|
+
enqueueSystemEvent: (
|
|
43
|
+
text: string,
|
|
44
|
+
meta: { sessionKey?: string | null; contextKey?: string | null },
|
|
45
|
+
) => void;
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
cfg: CoreConfig;
|
|
49
|
+
runtime: RuntimeEnv;
|
|
50
|
+
logger: {
|
|
51
|
+
info: (message: string | Record<string, unknown>, ...meta: unknown[]) => void;
|
|
52
|
+
warn: (meta: Record<string, unknown>, message: string) => void;
|
|
53
|
+
};
|
|
54
|
+
logVerboseMessage: (message: string) => void;
|
|
55
|
+
allowFrom: string[];
|
|
56
|
+
roomsConfig: CoreConfig["channels"] extends { matrix?: infer MatrixConfig }
|
|
57
|
+
? MatrixConfig extends { groups?: infer Groups }
|
|
58
|
+
? Groups
|
|
59
|
+
: Record<string, unknown> | undefined
|
|
60
|
+
: Record<string, unknown> | undefined;
|
|
61
|
+
mentionRegexes: ReturnType<
|
|
62
|
+
typeof import("openclaw/plugin-sdk")["channel"]["mentions"]["buildMentionRegexes"]
|
|
63
|
+
>;
|
|
64
|
+
groupPolicy: "open" | "allowlist" | "disabled";
|
|
65
|
+
replyToMode: ReplyToMode;
|
|
66
|
+
threadReplies: "off" | "inbound" | "always";
|
|
67
|
+
dmEnabled: boolean;
|
|
68
|
+
dmPolicy: "open" | "pairing" | "allowlist" | "disabled";
|
|
69
|
+
textLimit: number;
|
|
70
|
+
mediaMaxBytes: number;
|
|
71
|
+
startupMs: number;
|
|
72
|
+
startupGraceMs: number;
|
|
73
|
+
directTracker: {
|
|
74
|
+
isDirectMessage: (params: {
|
|
75
|
+
roomId: string;
|
|
76
|
+
senderId: string;
|
|
77
|
+
selfUserId: string;
|
|
78
|
+
}) => Promise<boolean>;
|
|
79
|
+
};
|
|
80
|
+
getRoomInfo: (roomId: string) => Promise<{ name?: string; canonicalAlias?: string; altAliases: string[] }>;
|
|
81
|
+
getMemberDisplayName: (roomId: string, userId: string) => Promise<string>;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParams) {
|
|
85
|
+
const {
|
|
86
|
+
client,
|
|
87
|
+
core,
|
|
88
|
+
cfg,
|
|
89
|
+
runtime,
|
|
90
|
+
logger,
|
|
91
|
+
logVerboseMessage,
|
|
92
|
+
allowFrom,
|
|
93
|
+
roomsConfig,
|
|
94
|
+
mentionRegexes,
|
|
95
|
+
groupPolicy,
|
|
96
|
+
replyToMode,
|
|
97
|
+
threadReplies,
|
|
98
|
+
dmEnabled,
|
|
99
|
+
dmPolicy,
|
|
100
|
+
textLimit,
|
|
101
|
+
mediaMaxBytes,
|
|
102
|
+
startupMs,
|
|
103
|
+
startupGraceMs,
|
|
104
|
+
directTracker,
|
|
105
|
+
getRoomInfo,
|
|
106
|
+
getMemberDisplayName,
|
|
107
|
+
} = params;
|
|
108
|
+
|
|
109
|
+
return async (roomId: string, event: MatrixRawEvent) => {
|
|
110
|
+
try {
|
|
111
|
+
const eventType = event.type;
|
|
112
|
+
if (eventType === EventType.RoomMessageEncrypted) {
|
|
113
|
+
// Encrypted messages are decrypted automatically by @vector-im/matrix-bot-sdk with crypto enabled
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const isPollEvent = isPollStartType(eventType);
|
|
118
|
+
const locationContent = event.content as LocationMessageEventContent;
|
|
119
|
+
const isLocationEvent =
|
|
120
|
+
eventType === EventType.Location ||
|
|
121
|
+
(eventType === EventType.RoomMessage &&
|
|
122
|
+
locationContent.msgtype === EventType.Location);
|
|
123
|
+
if (eventType !== EventType.RoomMessage && !isPollEvent && !isLocationEvent) return;
|
|
124
|
+
logVerboseMessage(
|
|
125
|
+
`matrix: room.message recv room=${roomId} type=${eventType} id=${event.event_id ?? "unknown"}`,
|
|
126
|
+
);
|
|
127
|
+
if (event.unsigned?.redacted_because) return;
|
|
128
|
+
const senderId = event.sender;
|
|
129
|
+
if (!senderId) return;
|
|
130
|
+
const selfUserId = await client.getUserId();
|
|
131
|
+
if (senderId === selfUserId) return;
|
|
132
|
+
const eventTs = event.origin_server_ts;
|
|
133
|
+
const eventAge = event.unsigned?.age;
|
|
134
|
+
if (typeof eventTs === "number" && eventTs < startupMs - startupGraceMs) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (
|
|
138
|
+
typeof eventTs !== "number" &&
|
|
139
|
+
typeof eventAge === "number" &&
|
|
140
|
+
eventAge > startupGraceMs
|
|
141
|
+
) {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const roomInfo = await getRoomInfo(roomId);
|
|
146
|
+
const roomName = roomInfo.name;
|
|
147
|
+
const roomAliases = [
|
|
148
|
+
roomInfo.canonicalAlias ?? "",
|
|
149
|
+
...roomInfo.altAliases,
|
|
150
|
+
].filter(Boolean);
|
|
151
|
+
|
|
152
|
+
let content = event.content as RoomMessageEventContent;
|
|
153
|
+
if (isPollEvent) {
|
|
154
|
+
const pollStartContent = event.content as PollStartContent;
|
|
155
|
+
const pollSummary = parsePollStartContent(pollStartContent);
|
|
156
|
+
if (pollSummary) {
|
|
157
|
+
pollSummary.eventId = event.event_id ?? "";
|
|
158
|
+
pollSummary.roomId = roomId;
|
|
159
|
+
pollSummary.sender = senderId;
|
|
160
|
+
const senderDisplayName = await getMemberDisplayName(roomId, senderId);
|
|
161
|
+
pollSummary.senderName = senderDisplayName;
|
|
162
|
+
const pollText = formatPollAsText(pollSummary);
|
|
163
|
+
content = {
|
|
164
|
+
msgtype: "m.text",
|
|
165
|
+
body: pollText,
|
|
166
|
+
} as unknown as RoomMessageEventContent;
|
|
167
|
+
} else {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const locationPayload: MatrixLocationPayload | null = resolveMatrixLocation({
|
|
173
|
+
eventType,
|
|
174
|
+
content: content as LocationMessageEventContent,
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
const relates = content["m.relates_to"];
|
|
178
|
+
if (relates && "rel_type" in relates) {
|
|
179
|
+
if (relates.rel_type === RelationType.Replace) return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const isDirectMessage = await directTracker.isDirectMessage({
|
|
183
|
+
roomId,
|
|
184
|
+
senderId,
|
|
185
|
+
selfUserId,
|
|
186
|
+
});
|
|
187
|
+
const isRoom = !isDirectMessage;
|
|
188
|
+
|
|
189
|
+
if (isRoom && groupPolicy === "disabled") return;
|
|
190
|
+
|
|
191
|
+
const roomConfigInfo = isRoom
|
|
192
|
+
? resolveMatrixRoomConfig({
|
|
193
|
+
rooms: roomsConfig,
|
|
194
|
+
roomId,
|
|
195
|
+
aliases: roomAliases,
|
|
196
|
+
name: roomName,
|
|
197
|
+
})
|
|
198
|
+
: undefined;
|
|
199
|
+
const roomConfig = roomConfigInfo?.config;
|
|
200
|
+
const roomMatchMeta = roomConfigInfo
|
|
201
|
+
? `matchKey=${roomConfigInfo.matchKey ?? "none"} matchSource=${
|
|
202
|
+
roomConfigInfo.matchSource ?? "none"
|
|
203
|
+
}`
|
|
204
|
+
: "matchKey=none matchSource=none";
|
|
205
|
+
|
|
206
|
+
if (isRoom && roomConfig && !roomConfigInfo?.allowed) {
|
|
207
|
+
logVerboseMessage(`matrix: room disabled room=${roomId} (${roomMatchMeta})`);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
if (isRoom && groupPolicy === "allowlist") {
|
|
211
|
+
if (!roomConfigInfo?.allowlistConfigured) {
|
|
212
|
+
logVerboseMessage(`matrix: drop room message (no allowlist, ${roomMatchMeta})`);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
if (!roomConfig) {
|
|
216
|
+
logVerboseMessage(`matrix: drop room message (not in allowlist, ${roomMatchMeta})`);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const senderName = await getMemberDisplayName(roomId, senderId);
|
|
222
|
+
const storeAllowFrom = await core.channel.pairing.readAllowFromStore("matrix").catch(() => []);
|
|
223
|
+
const effectiveAllowFrom = normalizeAllowListLower([...allowFrom, ...storeAllowFrom]);
|
|
224
|
+
const groupAllowFrom = cfg.channels?.matrix?.groupAllowFrom ?? [];
|
|
225
|
+
const effectiveGroupAllowFrom = normalizeAllowListLower([
|
|
226
|
+
...groupAllowFrom,
|
|
227
|
+
...storeAllowFrom,
|
|
228
|
+
]);
|
|
229
|
+
const groupAllowConfigured = effectiveGroupAllowFrom.length > 0;
|
|
230
|
+
|
|
231
|
+
if (isDirectMessage) {
|
|
232
|
+
if (!dmEnabled || dmPolicy === "disabled") return;
|
|
233
|
+
if (dmPolicy !== "open") {
|
|
234
|
+
const allowMatch = resolveMatrixAllowListMatch({
|
|
235
|
+
allowList: effectiveAllowFrom,
|
|
236
|
+
userId: senderId,
|
|
237
|
+
userName: senderName,
|
|
238
|
+
});
|
|
239
|
+
const allowMatchMeta = formatAllowlistMatchMeta(allowMatch);
|
|
240
|
+
if (!allowMatch.allowed) {
|
|
241
|
+
if (dmPolicy === "pairing") {
|
|
242
|
+
const { code, created } = await core.channel.pairing.upsertPairingRequest({
|
|
243
|
+
channel: "matrix",
|
|
244
|
+
id: senderId,
|
|
245
|
+
meta: { name: senderName },
|
|
246
|
+
});
|
|
247
|
+
if (created) {
|
|
248
|
+
logVerboseMessage(
|
|
249
|
+
`matrix pairing request sender=${senderId} name=${senderName ?? "unknown"} (${allowMatchMeta})`,
|
|
250
|
+
);
|
|
251
|
+
try {
|
|
252
|
+
await sendMessageMatrix(
|
|
253
|
+
`room:${roomId}`,
|
|
254
|
+
[
|
|
255
|
+
"OpenClaw: access not configured.",
|
|
256
|
+
"",
|
|
257
|
+
`Pairing code: ${code}`,
|
|
258
|
+
"",
|
|
259
|
+
"Ask the bot owner to approve with:",
|
|
260
|
+
"openclaw pairing approve matrix <code>",
|
|
261
|
+
].join("\n"),
|
|
262
|
+
{ client },
|
|
263
|
+
);
|
|
264
|
+
} catch (err) {
|
|
265
|
+
logVerboseMessage(`matrix pairing reply failed for ${senderId}: ${String(err)}`);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
if (dmPolicy !== "pairing") {
|
|
270
|
+
logVerboseMessage(
|
|
271
|
+
`matrix: blocked dm sender ${senderId} (dmPolicy=${dmPolicy}, ${allowMatchMeta})`,
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const roomUsers = roomConfig?.users ?? [];
|
|
280
|
+
if (isRoom && roomUsers.length > 0) {
|
|
281
|
+
const userMatch = resolveMatrixAllowListMatch({
|
|
282
|
+
allowList: normalizeAllowListLower(roomUsers),
|
|
283
|
+
userId: senderId,
|
|
284
|
+
userName: senderName,
|
|
285
|
+
});
|
|
286
|
+
if (!userMatch.allowed) {
|
|
287
|
+
logVerboseMessage(
|
|
288
|
+
`matrix: blocked sender ${senderId} (room users allowlist, ${roomMatchMeta}, ${formatAllowlistMatchMeta(
|
|
289
|
+
userMatch,
|
|
290
|
+
)})`,
|
|
291
|
+
);
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
if (isRoom && groupPolicy === "allowlist" && roomUsers.length === 0 && groupAllowConfigured) {
|
|
296
|
+
const groupAllowMatch = resolveMatrixAllowListMatch({
|
|
297
|
+
allowList: effectiveGroupAllowFrom,
|
|
298
|
+
userId: senderId,
|
|
299
|
+
userName: senderName,
|
|
300
|
+
});
|
|
301
|
+
if (!groupAllowMatch.allowed) {
|
|
302
|
+
logVerboseMessage(
|
|
303
|
+
`matrix: blocked sender ${senderId} (groupAllowFrom, ${roomMatchMeta}, ${formatAllowlistMatchMeta(
|
|
304
|
+
groupAllowMatch,
|
|
305
|
+
)})`,
|
|
306
|
+
);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
if (isRoom) {
|
|
311
|
+
logVerboseMessage(`matrix: allow room ${roomId} (${roomMatchMeta})`);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const rawBody = locationPayload?.text
|
|
315
|
+
?? (typeof content.body === "string" ? content.body.trim() : "");
|
|
316
|
+
let media: {
|
|
317
|
+
path: string;
|
|
318
|
+
contentType?: string;
|
|
319
|
+
placeholder: string;
|
|
320
|
+
} | null = null;
|
|
321
|
+
const contentUrl =
|
|
322
|
+
"url" in content && typeof content.url === "string" ? content.url : undefined;
|
|
323
|
+
const contentFile =
|
|
324
|
+
"file" in content && content.file && typeof content.file === "object"
|
|
325
|
+
? content.file
|
|
326
|
+
: undefined;
|
|
327
|
+
const mediaUrl = contentUrl ?? contentFile?.url;
|
|
328
|
+
if (!rawBody && !mediaUrl) {
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const contentInfo =
|
|
333
|
+
"info" in content && content.info && typeof content.info === "object"
|
|
334
|
+
? (content.info as { mimetype?: string; size?: number })
|
|
335
|
+
: undefined;
|
|
336
|
+
const contentType = contentInfo?.mimetype;
|
|
337
|
+
const contentSize =
|
|
338
|
+
typeof contentInfo?.size === "number" ? contentInfo.size : undefined;
|
|
339
|
+
if (mediaUrl?.startsWith("mxc://")) {
|
|
340
|
+
try {
|
|
341
|
+
media = await downloadMatrixMedia({
|
|
342
|
+
client,
|
|
343
|
+
mxcUrl: mediaUrl,
|
|
344
|
+
contentType,
|
|
345
|
+
sizeBytes: contentSize,
|
|
346
|
+
maxBytes: mediaMaxBytes,
|
|
347
|
+
file: contentFile,
|
|
348
|
+
});
|
|
349
|
+
} catch (err) {
|
|
350
|
+
logVerboseMessage(`matrix: media download failed: ${String(err)}`);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const bodyText = rawBody || media?.placeholder || "";
|
|
355
|
+
if (!bodyText) return;
|
|
356
|
+
|
|
357
|
+
const { wasMentioned, hasExplicitMention } = resolveMentions({
|
|
358
|
+
content,
|
|
359
|
+
userId: selfUserId,
|
|
360
|
+
text: bodyText,
|
|
361
|
+
mentionRegexes,
|
|
362
|
+
});
|
|
363
|
+
const allowTextCommands = core.channel.commands.shouldHandleTextCommands({
|
|
364
|
+
cfg,
|
|
365
|
+
surface: "matrix",
|
|
366
|
+
});
|
|
367
|
+
const useAccessGroups = cfg.commands?.useAccessGroups !== false;
|
|
368
|
+
const senderAllowedForCommands = resolveMatrixAllowListMatches({
|
|
369
|
+
allowList: effectiveAllowFrom,
|
|
370
|
+
userId: senderId,
|
|
371
|
+
userName: senderName,
|
|
372
|
+
});
|
|
373
|
+
const senderAllowedForGroup = groupAllowConfigured
|
|
374
|
+
? resolveMatrixAllowListMatches({
|
|
375
|
+
allowList: effectiveGroupAllowFrom,
|
|
376
|
+
userId: senderId,
|
|
377
|
+
userName: senderName,
|
|
378
|
+
})
|
|
379
|
+
: false;
|
|
380
|
+
const senderAllowedForRoomUsers =
|
|
381
|
+
isRoom && roomUsers.length > 0
|
|
382
|
+
? resolveMatrixAllowListMatches({
|
|
383
|
+
allowList: normalizeAllowListLower(roomUsers),
|
|
384
|
+
userId: senderId,
|
|
385
|
+
userName: senderName,
|
|
386
|
+
})
|
|
387
|
+
: false;
|
|
388
|
+
const hasControlCommandInMessage = core.channel.text.hasControlCommand(bodyText, cfg);
|
|
389
|
+
const commandGate = resolveControlCommandGate({
|
|
390
|
+
useAccessGroups,
|
|
391
|
+
authorizers: [
|
|
392
|
+
{ configured: effectiveAllowFrom.length > 0, allowed: senderAllowedForCommands },
|
|
393
|
+
{ configured: roomUsers.length > 0, allowed: senderAllowedForRoomUsers },
|
|
394
|
+
{ configured: groupAllowConfigured, allowed: senderAllowedForGroup },
|
|
395
|
+
],
|
|
396
|
+
allowTextCommands,
|
|
397
|
+
hasControlCommand: hasControlCommandInMessage,
|
|
398
|
+
});
|
|
399
|
+
const commandAuthorized = commandGate.commandAuthorized;
|
|
400
|
+
if (isRoom && commandGate.shouldBlock) {
|
|
401
|
+
logInboundDrop({
|
|
402
|
+
log: logVerboseMessage,
|
|
403
|
+
channel: "matrix",
|
|
404
|
+
reason: "control command (unauthorized)",
|
|
405
|
+
target: senderId,
|
|
406
|
+
});
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
const shouldRequireMention = isRoom
|
|
410
|
+
? roomConfig?.autoReply === true
|
|
411
|
+
? false
|
|
412
|
+
: roomConfig?.autoReply === false
|
|
413
|
+
? true
|
|
414
|
+
: typeof roomConfig?.requireMention === "boolean"
|
|
415
|
+
? roomConfig?.requireMention
|
|
416
|
+
: true
|
|
417
|
+
: false;
|
|
418
|
+
const shouldBypassMention =
|
|
419
|
+
allowTextCommands &&
|
|
420
|
+
isRoom &&
|
|
421
|
+
shouldRequireMention &&
|
|
422
|
+
!wasMentioned &&
|
|
423
|
+
!hasExplicitMention &&
|
|
424
|
+
commandAuthorized &&
|
|
425
|
+
hasControlCommandInMessage;
|
|
426
|
+
const canDetectMention = mentionRegexes.length > 0 || hasExplicitMention;
|
|
427
|
+
if (isRoom && shouldRequireMention && !wasMentioned && !shouldBypassMention) {
|
|
428
|
+
logger.info({ roomId, reason: "no-mention" }, "skipping room message");
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const messageId = event.event_id ?? "";
|
|
433
|
+
const replyToEventId = content["m.relates_to"]?.["m.in_reply_to"]?.event_id;
|
|
434
|
+
const threadRootId = resolveMatrixThreadRootId({ event, content });
|
|
435
|
+
const threadTarget = resolveMatrixThreadTarget({
|
|
436
|
+
threadReplies,
|
|
437
|
+
messageId,
|
|
438
|
+
threadRootId,
|
|
439
|
+
isThreadRoot: false, // @vector-im/matrix-bot-sdk doesn't have this info readily available
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
const route = core.channel.routing.resolveAgentRoute({
|
|
443
|
+
cfg,
|
|
444
|
+
channel: "matrix",
|
|
445
|
+
peer: {
|
|
446
|
+
kind: isDirectMessage ? "dm" : "channel",
|
|
447
|
+
id: isDirectMessage ? senderId : roomId,
|
|
448
|
+
},
|
|
449
|
+
});
|
|
450
|
+
const envelopeFrom = isDirectMessage ? senderName : (roomName ?? roomId);
|
|
451
|
+
const textWithId = `${bodyText}\n[matrix event id: ${messageId} room: ${roomId}]`;
|
|
452
|
+
const storePath = core.channel.session.resolveStorePath(cfg.session?.store, {
|
|
453
|
+
agentId: route.agentId,
|
|
454
|
+
});
|
|
455
|
+
const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions(cfg);
|
|
456
|
+
const previousTimestamp = core.channel.session.readSessionUpdatedAt({
|
|
457
|
+
storePath,
|
|
458
|
+
sessionKey: route.sessionKey,
|
|
459
|
+
});
|
|
460
|
+
const body = core.channel.reply.formatAgentEnvelope({
|
|
461
|
+
channel: "Matrix",
|
|
462
|
+
from: envelopeFrom,
|
|
463
|
+
timestamp: eventTs ?? undefined,
|
|
464
|
+
previousTimestamp,
|
|
465
|
+
envelope: envelopeOptions,
|
|
466
|
+
body: textWithId,
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
const groupSystemPrompt = roomConfig?.systemPrompt?.trim() || undefined;
|
|
470
|
+
const ctxPayload = core.channel.reply.finalizeInboundContext({
|
|
471
|
+
Body: body,
|
|
472
|
+
RawBody: bodyText,
|
|
473
|
+
CommandBody: bodyText,
|
|
474
|
+
From: isDirectMessage ? `matrix:${senderId}` : `matrix:channel:${roomId}`,
|
|
475
|
+
To: `room:${roomId}`,
|
|
476
|
+
SessionKey: route.sessionKey,
|
|
477
|
+
AccountId: route.accountId,
|
|
478
|
+
ChatType: isDirectMessage ? "direct" : "channel",
|
|
479
|
+
ConversationLabel: envelopeFrom,
|
|
480
|
+
SenderName: senderName,
|
|
481
|
+
SenderId: senderId,
|
|
482
|
+
SenderUsername: senderId.split(":")[0]?.replace(/^@/, ""),
|
|
483
|
+
GroupSubject: isRoom ? (roomName ?? roomId) : undefined,
|
|
484
|
+
GroupChannel: isRoom ? (roomInfo.canonicalAlias ?? roomId) : undefined,
|
|
485
|
+
GroupSystemPrompt: isRoom ? groupSystemPrompt : undefined,
|
|
486
|
+
Provider: "matrix" as const,
|
|
487
|
+
Surface: "matrix" as const,
|
|
488
|
+
WasMentioned: isRoom ? wasMentioned : undefined,
|
|
489
|
+
MessageSid: messageId,
|
|
490
|
+
ReplyToId: threadTarget ? undefined : (replyToEventId ?? undefined),
|
|
491
|
+
MessageThreadId: threadTarget,
|
|
492
|
+
Timestamp: eventTs ?? undefined,
|
|
493
|
+
MediaPath: media?.path,
|
|
494
|
+
MediaType: media?.contentType,
|
|
495
|
+
MediaUrl: media?.path,
|
|
496
|
+
...(locationPayload?.context ?? {}),
|
|
497
|
+
CommandAuthorized: commandAuthorized,
|
|
498
|
+
CommandSource: "text" as const,
|
|
499
|
+
OriginatingChannel: "matrix" as const,
|
|
500
|
+
OriginatingTo: `room:${roomId}`,
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
await core.channel.session.recordInboundSession({
|
|
504
|
+
storePath,
|
|
505
|
+
sessionKey: ctxPayload.SessionKey ?? route.sessionKey,
|
|
506
|
+
ctx: ctxPayload,
|
|
507
|
+
updateLastRoute: isDirectMessage
|
|
508
|
+
? {
|
|
509
|
+
sessionKey: route.mainSessionKey,
|
|
510
|
+
channel: "matrix",
|
|
511
|
+
to: `room:${roomId}`,
|
|
512
|
+
accountId: route.accountId,
|
|
513
|
+
}
|
|
514
|
+
: undefined,
|
|
515
|
+
onRecordError: (err) => {
|
|
516
|
+
logger.warn(
|
|
517
|
+
{ error: String(err), storePath, sessionKey: ctxPayload.SessionKey ?? route.sessionKey },
|
|
518
|
+
"failed updating session meta",
|
|
519
|
+
);
|
|
520
|
+
},
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
const preview = bodyText.slice(0, 200).replace(/\n/g, "\\n");
|
|
524
|
+
logVerboseMessage(`matrix inbound: room=${roomId} from=${senderId} preview="${preview}"`);
|
|
525
|
+
|
|
526
|
+
const ackReaction = (cfg.messages?.ackReaction ?? "").trim();
|
|
527
|
+
const ackScope = cfg.messages?.ackReactionScope ?? "group-mentions";
|
|
528
|
+
const shouldAckReaction = () =>
|
|
529
|
+
Boolean(
|
|
530
|
+
ackReaction &&
|
|
531
|
+
core.channel.reactions.shouldAckReaction({
|
|
532
|
+
scope: ackScope,
|
|
533
|
+
isDirect: isDirectMessage,
|
|
534
|
+
isGroup: isRoom,
|
|
535
|
+
isMentionableGroup: isRoom,
|
|
536
|
+
requireMention: Boolean(shouldRequireMention),
|
|
537
|
+
canDetectMention,
|
|
538
|
+
effectiveWasMentioned: wasMentioned || shouldBypassMention,
|
|
539
|
+
shouldBypassMention,
|
|
540
|
+
}),
|
|
541
|
+
);
|
|
542
|
+
if (shouldAckReaction() && messageId) {
|
|
543
|
+
reactMatrixMessage(roomId, messageId, ackReaction, client).catch((err) => {
|
|
544
|
+
logVerboseMessage(`matrix react failed for room ${roomId}: ${String(err)}`);
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
const replyTarget = ctxPayload.To;
|
|
549
|
+
if (!replyTarget) {
|
|
550
|
+
runtime.error?.("matrix: missing reply target");
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
if (messageId) {
|
|
555
|
+
sendReadReceiptMatrix(roomId, messageId, client).catch((err) => {
|
|
556
|
+
logVerboseMessage(
|
|
557
|
+
`matrix: read receipt failed room=${roomId} id=${messageId}: ${String(err)}`,
|
|
558
|
+
);
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
let didSendReply = false;
|
|
563
|
+
const tableMode = core.channel.text.resolveMarkdownTableMode({
|
|
564
|
+
cfg,
|
|
565
|
+
channel: "matrix",
|
|
566
|
+
accountId: route.accountId,
|
|
567
|
+
});
|
|
568
|
+
const prefixContext = createReplyPrefixContext({ cfg, agentId: route.agentId });
|
|
569
|
+
const typingCallbacks = createTypingCallbacks({
|
|
570
|
+
start: () => sendTypingMatrix(roomId, true, undefined, client),
|
|
571
|
+
stop: () => sendTypingMatrix(roomId, false, undefined, client),
|
|
572
|
+
onStartError: (err) => {
|
|
573
|
+
logTypingFailure({
|
|
574
|
+
log: logVerboseMessage,
|
|
575
|
+
channel: "matrix",
|
|
576
|
+
action: "start",
|
|
577
|
+
target: roomId,
|
|
578
|
+
error: err,
|
|
579
|
+
});
|
|
580
|
+
},
|
|
581
|
+
onStopError: (err) => {
|
|
582
|
+
logTypingFailure({
|
|
583
|
+
log: logVerboseMessage,
|
|
584
|
+
channel: "matrix",
|
|
585
|
+
action: "stop",
|
|
586
|
+
target: roomId,
|
|
587
|
+
error: err,
|
|
588
|
+
});
|
|
589
|
+
},
|
|
590
|
+
});
|
|
591
|
+
const { dispatcher, replyOptions, markDispatchIdle } =
|
|
592
|
+
core.channel.reply.createReplyDispatcherWithTyping({
|
|
593
|
+
responsePrefix: prefixContext.responsePrefix,
|
|
594
|
+
responsePrefixContextProvider: prefixContext.responsePrefixContextProvider,
|
|
595
|
+
humanDelay: core.channel.reply.resolveHumanDelayConfig(cfg, route.agentId),
|
|
596
|
+
deliver: async (payload) => {
|
|
597
|
+
await deliverMatrixReplies({
|
|
598
|
+
replies: [payload],
|
|
599
|
+
roomId,
|
|
600
|
+
client,
|
|
601
|
+
runtime,
|
|
602
|
+
textLimit,
|
|
603
|
+
replyToMode,
|
|
604
|
+
threadId: threadTarget,
|
|
605
|
+
accountId: route.accountId,
|
|
606
|
+
tableMode,
|
|
607
|
+
});
|
|
608
|
+
didSendReply = true;
|
|
609
|
+
},
|
|
610
|
+
onError: (err, info) => {
|
|
611
|
+
runtime.error?.(`matrix ${info.kind} reply failed: ${String(err)}`);
|
|
612
|
+
},
|
|
613
|
+
onReplyStart: typingCallbacks.onReplyStart,
|
|
614
|
+
onIdle: typingCallbacks.onIdle,
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
const { queuedFinal, counts } = await core.channel.reply.dispatchReplyFromConfig({
|
|
618
|
+
ctx: ctxPayload,
|
|
619
|
+
cfg,
|
|
620
|
+
dispatcher,
|
|
621
|
+
replyOptions: {
|
|
622
|
+
...replyOptions,
|
|
623
|
+
skillFilter: roomConfig?.skills,
|
|
624
|
+
onModelSelected: prefixContext.onModelSelected,
|
|
625
|
+
},
|
|
626
|
+
});
|
|
627
|
+
markDispatchIdle();
|
|
628
|
+
if (!queuedFinal) return;
|
|
629
|
+
didSendReply = true;
|
|
630
|
+
const finalCount = counts.final;
|
|
631
|
+
logVerboseMessage(
|
|
632
|
+
`matrix: delivered ${finalCount} reply${finalCount === 1 ? "" : "ies"} to ${replyTarget}`,
|
|
633
|
+
);
|
|
634
|
+
if (didSendReply) {
|
|
635
|
+
const previewText = bodyText.replace(/\s+/g, " ").slice(0, 160);
|
|
636
|
+
core.system.enqueueSystemEvent(`Matrix message from ${senderName}: ${previewText}`, {
|
|
637
|
+
sessionKey: route.sessionKey,
|
|
638
|
+
contextKey: `matrix:message:${roomId}:${messageId || "unknown"}`,
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
} catch (err) {
|
|
642
|
+
runtime.error?.(`matrix handler failed: ${String(err)}`);
|
|
643
|
+
}
|
|
644
|
+
};
|
|
645
|
+
}
|