@badgerclaw/connect 1.0.0
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 +104 -0
- package/SETUP.md +131 -0
- package/index.ts +23 -0
- package/openclaw.plugin.json +1 -0
- package/package.json +32 -0
- package/src/actions.ts +195 -0
- package/src/channel.ts +461 -0
- package/src/config-schema.ts +62 -0
- package/src/connect.ts +17 -0
- package/src/directory-live.ts +209 -0
- package/src/group-mentions.ts +52 -0
- package/src/matrix/accounts.ts +114 -0
- package/src/matrix/actions/client.ts +47 -0
- package/src/matrix/actions/limits.ts +6 -0
- package/src/matrix/actions/messages.ts +126 -0
- package/src/matrix/actions/pins.ts +84 -0
- package/src/matrix/actions/reactions.ts +102 -0
- package/src/matrix/actions/room.ts +85 -0
- package/src/matrix/actions/summary.ts +75 -0
- package/src/matrix/actions/types.ts +85 -0
- package/src/matrix/actions.ts +15 -0
- package/src/matrix/active-client.ts +32 -0
- package/src/matrix/client/config.ts +245 -0
- package/src/matrix/client/create-client.ts +125 -0
- package/src/matrix/client/logging.ts +46 -0
- package/src/matrix/client/runtime.ts +4 -0
- package/src/matrix/client/shared.ts +210 -0
- package/src/matrix/client/startup.ts +29 -0
- package/src/matrix/client/storage.ts +131 -0
- package/src/matrix/client/types.ts +34 -0
- package/src/matrix/client-bootstrap.ts +47 -0
- package/src/matrix/client.ts +14 -0
- package/src/matrix/credentials.ts +125 -0
- package/src/matrix/deps.ts +126 -0
- package/src/matrix/format.ts +22 -0
- package/src/matrix/index.ts +11 -0
- package/src/matrix/monitor/access-policy.ts +126 -0
- package/src/matrix/monitor/allowlist.ts +94 -0
- package/src/matrix/monitor/auto-join.ts +72 -0
- package/src/matrix/monitor/direct.ts +152 -0
- package/src/matrix/monitor/events.ts +168 -0
- package/src/matrix/monitor/handler.ts +768 -0
- package/src/matrix/monitor/inbound-body.ts +28 -0
- package/src/matrix/monitor/index.ts +414 -0
- package/src/matrix/monitor/location.ts +100 -0
- package/src/matrix/monitor/media.ts +118 -0
- package/src/matrix/monitor/mentions.ts +62 -0
- package/src/matrix/monitor/replies.ts +124 -0
- package/src/matrix/monitor/room-info.ts +55 -0
- package/src/matrix/monitor/rooms.ts +47 -0
- package/src/matrix/monitor/threads.ts +68 -0
- package/src/matrix/monitor/types.ts +39 -0
- package/src/matrix/poll-types.ts +167 -0
- package/src/matrix/probe.ts +69 -0
- package/src/matrix/sdk-runtime.ts +18 -0
- package/src/matrix/send/client.ts +99 -0
- package/src/matrix/send/formatting.ts +93 -0
- package/src/matrix/send/media.ts +230 -0
- package/src/matrix/send/targets.ts +150 -0
- package/src/matrix/send/types.ts +110 -0
- package/src/matrix/send-queue.ts +28 -0
- package/src/matrix/send.ts +267 -0
- package/src/onboarding.ts +331 -0
- package/src/outbound.ts +58 -0
- package/src/resolve-targets.ts +125 -0
- package/src/runtime.ts +6 -0
- package/src/secret-input.ts +13 -0
- package/src/test-mocks.ts +53 -0
- package/src/tool-actions.ts +164 -0
- package/src/types.ts +118 -0
|
@@ -0,0 +1,768 @@
|
|
|
1
|
+
import type { LocationMessageEventContent, MatrixClient } from "@vector-im/matrix-bot-sdk";
|
|
2
|
+
import {
|
|
3
|
+
DEFAULT_ACCOUNT_ID,
|
|
4
|
+
createScopedPairingAccess,
|
|
5
|
+
createReplyPrefixOptions,
|
|
6
|
+
createTypingCallbacks,
|
|
7
|
+
dispatchReplyFromConfigWithSettledDispatcher,
|
|
8
|
+
evaluateGroupRouteAccessForPolicy,
|
|
9
|
+
formatAllowlistMatchMeta,
|
|
10
|
+
logInboundDrop,
|
|
11
|
+
logTypingFailure,
|
|
12
|
+
resolveInboundSessionEnvelopeContext,
|
|
13
|
+
resolveControlCommandGate,
|
|
14
|
+
type PluginRuntime,
|
|
15
|
+
type RuntimeEnv,
|
|
16
|
+
type RuntimeLogger,
|
|
17
|
+
} from "openclaw/plugin-sdk/matrix";
|
|
18
|
+
import type { CoreConfig, MatrixRoomConfig, ReplyToMode } from "../../types.js";
|
|
19
|
+
import { fetchEventSummary } from "../actions/summary.js";
|
|
20
|
+
import {
|
|
21
|
+
formatPollAsText,
|
|
22
|
+
isPollStartType,
|
|
23
|
+
parsePollStartContent,
|
|
24
|
+
type PollStartContent,
|
|
25
|
+
} from "../poll-types.js";
|
|
26
|
+
import { reactMatrixMessage, sendMessageMatrix, sendTypingMatrix } from "../send.js";
|
|
27
|
+
import { enforceMatrixDirectMessageAccess, resolveMatrixAccessState } from "./access-policy.js";
|
|
28
|
+
import {
|
|
29
|
+
normalizeMatrixAllowList,
|
|
30
|
+
resolveMatrixAllowListMatch,
|
|
31
|
+
resolveMatrixAllowListMatches,
|
|
32
|
+
} from "./allowlist.js";
|
|
33
|
+
import {
|
|
34
|
+
resolveMatrixBodyForAgent,
|
|
35
|
+
resolveMatrixInboundSenderLabel,
|
|
36
|
+
resolveMatrixSenderUsername,
|
|
37
|
+
} from "./inbound-body.js";
|
|
38
|
+
import { resolveMatrixLocation, type MatrixLocationPayload } from "./location.js";
|
|
39
|
+
import { downloadMatrixMedia } from "./media.js";
|
|
40
|
+
import { resolveMentions } from "./mentions.js";
|
|
41
|
+
import { deliverMatrixReplies } from "./replies.js";
|
|
42
|
+
import { resolveMatrixRoomConfig } from "./rooms.js";
|
|
43
|
+
import { resolveMatrixThreadRootId, resolveMatrixThreadTarget } from "./threads.js";
|
|
44
|
+
import type { MatrixRawEvent, RoomMessageEventContent } from "./types.js";
|
|
45
|
+
import { EventType, RelationType } from "./types.js";
|
|
46
|
+
|
|
47
|
+
export type MatrixMonitorHandlerParams = {
|
|
48
|
+
client: MatrixClient;
|
|
49
|
+
core: PluginRuntime;
|
|
50
|
+
cfg: CoreConfig;
|
|
51
|
+
runtime: RuntimeEnv;
|
|
52
|
+
logger: RuntimeLogger;
|
|
53
|
+
logVerboseMessage: (message: string) => void;
|
|
54
|
+
allowFrom: string[];
|
|
55
|
+
roomsConfig: Record<string, MatrixRoomConfig> | undefined;
|
|
56
|
+
mentionRegexes: ReturnType<PluginRuntime["channel"]["mentions"]["buildMentionRegexes"]>;
|
|
57
|
+
groupPolicy: "open" | "allowlist" | "disabled";
|
|
58
|
+
replyToMode: ReplyToMode;
|
|
59
|
+
threadReplies: "off" | "inbound" | "always";
|
|
60
|
+
dmEnabled: boolean;
|
|
61
|
+
dmPolicy: "open" | "pairing" | "allowlist" | "disabled";
|
|
62
|
+
textLimit: number;
|
|
63
|
+
mediaMaxBytes: number;
|
|
64
|
+
startupMs: number;
|
|
65
|
+
startupGraceMs: number;
|
|
66
|
+
directTracker: {
|
|
67
|
+
isDirectMessage: (params: {
|
|
68
|
+
roomId: string;
|
|
69
|
+
senderId: string;
|
|
70
|
+
selfUserId: string;
|
|
71
|
+
}) => Promise<boolean>;
|
|
72
|
+
};
|
|
73
|
+
getRoomInfo: (
|
|
74
|
+
roomId: string,
|
|
75
|
+
) => Promise<{ name?: string; canonicalAlias?: string; altAliases: string[] }>;
|
|
76
|
+
getMemberDisplayName: (roomId: string, userId: string) => Promise<string>;
|
|
77
|
+
accountId?: string | null;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export function resolveMatrixBaseRouteSession(params: {
|
|
81
|
+
buildAgentSessionKey: (params: {
|
|
82
|
+
agentId: string;
|
|
83
|
+
channel: string;
|
|
84
|
+
accountId?: string | null;
|
|
85
|
+
peer?: { kind: "direct" | "channel"; id: string } | null;
|
|
86
|
+
}) => string;
|
|
87
|
+
baseRoute: {
|
|
88
|
+
agentId: string;
|
|
89
|
+
sessionKey: string;
|
|
90
|
+
mainSessionKey: string;
|
|
91
|
+
matchedBy?: string;
|
|
92
|
+
};
|
|
93
|
+
isDirectMessage: boolean;
|
|
94
|
+
roomId: string;
|
|
95
|
+
accountId?: string | null;
|
|
96
|
+
}): { sessionKey: string; lastRoutePolicy: "main" | "session" } {
|
|
97
|
+
const sessionKey =
|
|
98
|
+
params.isDirectMessage && params.baseRoute.matchedBy === "binding.peer.parent"
|
|
99
|
+
? params.buildAgentSessionKey({
|
|
100
|
+
agentId: params.baseRoute.agentId,
|
|
101
|
+
channel: "badgerclaw",
|
|
102
|
+
accountId: params.accountId,
|
|
103
|
+
peer: { kind: "channel", id: params.roomId },
|
|
104
|
+
})
|
|
105
|
+
: params.baseRoute.sessionKey;
|
|
106
|
+
return {
|
|
107
|
+
sessionKey,
|
|
108
|
+
lastRoutePolicy: sessionKey === params.baseRoute.mainSessionKey ? "main" : "session",
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function shouldOverrideMatrixDmToGroup(params: {
|
|
113
|
+
isDirectMessage: boolean;
|
|
114
|
+
roomConfigInfo?:
|
|
115
|
+
| {
|
|
116
|
+
config?: MatrixRoomConfig;
|
|
117
|
+
allowed: boolean;
|
|
118
|
+
matchSource?: string;
|
|
119
|
+
}
|
|
120
|
+
| undefined;
|
|
121
|
+
}): boolean {
|
|
122
|
+
return (
|
|
123
|
+
params.isDirectMessage === true &&
|
|
124
|
+
params.roomConfigInfo?.config !== undefined &&
|
|
125
|
+
params.roomConfigInfo.allowed === true &&
|
|
126
|
+
params.roomConfigInfo.matchSource === "direct"
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParams) {
|
|
131
|
+
const {
|
|
132
|
+
client,
|
|
133
|
+
core,
|
|
134
|
+
cfg,
|
|
135
|
+
runtime,
|
|
136
|
+
logger,
|
|
137
|
+
logVerboseMessage,
|
|
138
|
+
allowFrom,
|
|
139
|
+
roomsConfig,
|
|
140
|
+
mentionRegexes,
|
|
141
|
+
groupPolicy,
|
|
142
|
+
replyToMode,
|
|
143
|
+
threadReplies,
|
|
144
|
+
dmEnabled,
|
|
145
|
+
dmPolicy,
|
|
146
|
+
textLimit,
|
|
147
|
+
mediaMaxBytes,
|
|
148
|
+
startupMs,
|
|
149
|
+
startupGraceMs,
|
|
150
|
+
directTracker,
|
|
151
|
+
getRoomInfo,
|
|
152
|
+
getMemberDisplayName,
|
|
153
|
+
accountId,
|
|
154
|
+
} = params;
|
|
155
|
+
const resolvedAccountId = accountId?.trim() || DEFAULT_ACCOUNT_ID;
|
|
156
|
+
const pairing = createScopedPairingAccess({
|
|
157
|
+
core,
|
|
158
|
+
channel: "badgerclaw",
|
|
159
|
+
accountId: resolvedAccountId,
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
return async (roomId: string, event: MatrixRawEvent) => {
|
|
163
|
+
try {
|
|
164
|
+
const eventType = event.type;
|
|
165
|
+
if (eventType === EventType.RoomMessageEncrypted) {
|
|
166
|
+
// Encrypted messages are decrypted automatically by @vector-im/matrix-bot-sdk with crypto enabled
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const isPollEvent = isPollStartType(eventType);
|
|
171
|
+
const locationContent = event.content as unknown as LocationMessageEventContent;
|
|
172
|
+
const isLocationEvent =
|
|
173
|
+
eventType === EventType.Location ||
|
|
174
|
+
(eventType === EventType.RoomMessage && locationContent.msgtype === EventType.Location);
|
|
175
|
+
if (eventType !== EventType.RoomMessage && !isPollEvent && !isLocationEvent) {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
logVerboseMessage(
|
|
179
|
+
`badgerclaw: room.message recv room=${roomId} type=${eventType} id=${event.event_id ?? "unknown"}`,
|
|
180
|
+
);
|
|
181
|
+
if (event.unsigned?.redacted_because) {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
const senderId = event.sender;
|
|
185
|
+
if (!senderId) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
const selfUserId = await client.getUserId();
|
|
189
|
+
if (senderId === selfUserId) {
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
const eventTs = event.origin_server_ts;
|
|
193
|
+
const eventAge = event.unsigned?.age;
|
|
194
|
+
if (typeof eventTs === "number" && eventTs < startupMs - startupGraceMs) {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
if (
|
|
198
|
+
typeof eventTs !== "number" &&
|
|
199
|
+
typeof eventAge === "number" &&
|
|
200
|
+
eventAge > startupGraceMs
|
|
201
|
+
) {
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const roomInfo = await getRoomInfo(roomId);
|
|
206
|
+
const roomName = roomInfo.name;
|
|
207
|
+
const roomAliases = [roomInfo.canonicalAlias ?? "", ...roomInfo.altAliases].filter(Boolean);
|
|
208
|
+
|
|
209
|
+
let content = event.content as unknown as RoomMessageEventContent;
|
|
210
|
+
if (isPollEvent) {
|
|
211
|
+
const pollStartContent = event.content as unknown as PollStartContent;
|
|
212
|
+
const pollSummary = parsePollStartContent(pollStartContent);
|
|
213
|
+
if (pollSummary) {
|
|
214
|
+
pollSummary.eventId = event.event_id ?? "";
|
|
215
|
+
pollSummary.roomId = roomId;
|
|
216
|
+
pollSummary.sender = senderId;
|
|
217
|
+
const senderDisplayName = await getMemberDisplayName(roomId, senderId);
|
|
218
|
+
pollSummary.senderName = senderDisplayName;
|
|
219
|
+
const pollText = formatPollAsText(pollSummary);
|
|
220
|
+
content = {
|
|
221
|
+
msgtype: "m.text",
|
|
222
|
+
body: pollText,
|
|
223
|
+
} as unknown as RoomMessageEventContent;
|
|
224
|
+
} else {
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const locationPayload: MatrixLocationPayload | null = resolveMatrixLocation({
|
|
230
|
+
eventType,
|
|
231
|
+
content: content as LocationMessageEventContent,
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
const relates = content["m.relates_to"];
|
|
235
|
+
if (relates && "rel_type" in relates) {
|
|
236
|
+
if (relates.rel_type === RelationType.Replace) {
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
let isDirectMessage = await directTracker.isDirectMessage({
|
|
242
|
+
roomId,
|
|
243
|
+
senderId,
|
|
244
|
+
selfUserId,
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
// Resolve room config early so explicitly configured rooms can override DM classification.
|
|
248
|
+
// This ensures rooms in the groups config are always treated as groups regardless of
|
|
249
|
+
// member count or protocol-level DM flags. Only explicit matches (not wildcards) trigger
|
|
250
|
+
// the override to avoid breaking DM routing when a wildcard entry exists. (See #9106)
|
|
251
|
+
const roomConfigInfo = resolveMatrixRoomConfig({
|
|
252
|
+
rooms: roomsConfig,
|
|
253
|
+
roomId,
|
|
254
|
+
aliases: roomAliases,
|
|
255
|
+
name: roomName,
|
|
256
|
+
});
|
|
257
|
+
if (shouldOverrideMatrixDmToGroup({ isDirectMessage, roomConfigInfo })) {
|
|
258
|
+
logVerboseMessage(
|
|
259
|
+
`badgerclaw: overriding DM to group for configured room=${roomId} (${roomConfigInfo.matchKey})`,
|
|
260
|
+
);
|
|
261
|
+
isDirectMessage = false;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const isRoom = !isDirectMessage;
|
|
265
|
+
|
|
266
|
+
if (isRoom && groupPolicy === "disabled") {
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
// Only expose room config for confirmed group rooms. DMs should never inherit
|
|
270
|
+
// group settings (skills, systemPrompt, autoReply) even when a wildcard entry exists.
|
|
271
|
+
const roomConfig = isRoom ? roomConfigInfo?.config : undefined;
|
|
272
|
+
const roomMatchMeta = roomConfigInfo
|
|
273
|
+
? `matchKey=${roomConfigInfo.matchKey ?? "none"} matchSource=${
|
|
274
|
+
roomConfigInfo.matchSource ?? "none"
|
|
275
|
+
}`
|
|
276
|
+
: "matchKey=none matchSource=none";
|
|
277
|
+
|
|
278
|
+
if (isRoom) {
|
|
279
|
+
const routeAccess = evaluateGroupRouteAccessForPolicy({
|
|
280
|
+
groupPolicy,
|
|
281
|
+
routeAllowlistConfigured: Boolean(roomConfigInfo?.allowlistConfigured),
|
|
282
|
+
routeMatched: Boolean(roomConfig),
|
|
283
|
+
routeEnabled: roomConfigInfo?.allowed ?? true,
|
|
284
|
+
});
|
|
285
|
+
if (!routeAccess.allowed) {
|
|
286
|
+
if (routeAccess.reason === "route_disabled") {
|
|
287
|
+
logVerboseMessage(`badgerclaw: room disabled room=${roomId} (${roomMatchMeta})`);
|
|
288
|
+
} else if (routeAccess.reason === "empty_allowlist") {
|
|
289
|
+
logVerboseMessage(`badgerclaw: drop room message (no allowlist, ${roomMatchMeta})`);
|
|
290
|
+
} else if (routeAccess.reason === "route_not_allowlisted") {
|
|
291
|
+
logVerboseMessage(`badgerclaw: drop room message (not in allowlist, ${roomMatchMeta})`);
|
|
292
|
+
}
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const senderName = await getMemberDisplayName(roomId, senderId);
|
|
298
|
+
const senderUsername = resolveMatrixSenderUsername(senderId);
|
|
299
|
+
const senderLabel = resolveMatrixInboundSenderLabel({
|
|
300
|
+
senderName,
|
|
301
|
+
senderId,
|
|
302
|
+
senderUsername,
|
|
303
|
+
});
|
|
304
|
+
const groupAllowFrom = cfg.channels?.badgerclaw?.groupAllowFrom ?? [];
|
|
305
|
+
const { access, effectiveAllowFrom, effectiveGroupAllowFrom, groupAllowConfigured } =
|
|
306
|
+
await resolveMatrixAccessState({
|
|
307
|
+
isDirectMessage,
|
|
308
|
+
resolvedAccountId,
|
|
309
|
+
dmPolicy,
|
|
310
|
+
groupPolicy,
|
|
311
|
+
allowFrom,
|
|
312
|
+
groupAllowFrom,
|
|
313
|
+
senderId,
|
|
314
|
+
readStoreForDmPolicy: pairing.readStoreForDmPolicy,
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
if (isDirectMessage) {
|
|
318
|
+
const allowedDirectMessage = await enforceMatrixDirectMessageAccess({
|
|
319
|
+
dmEnabled,
|
|
320
|
+
dmPolicy,
|
|
321
|
+
accessDecision: access.decision,
|
|
322
|
+
senderId,
|
|
323
|
+
senderName,
|
|
324
|
+
effectiveAllowFrom,
|
|
325
|
+
upsertPairingRequest: pairing.upsertPairingRequest,
|
|
326
|
+
sendPairingReply: async (text) => {
|
|
327
|
+
await sendMessageMatrix(`room:${roomId}`, text, { client });
|
|
328
|
+
},
|
|
329
|
+
logVerboseMessage,
|
|
330
|
+
});
|
|
331
|
+
if (!allowedDirectMessage) {
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const roomUsers = roomConfig?.users ?? [];
|
|
337
|
+
if (isRoom && roomUsers.length > 0) {
|
|
338
|
+
const userMatch = resolveMatrixAllowListMatch({
|
|
339
|
+
allowList: normalizeMatrixAllowList(roomUsers),
|
|
340
|
+
userId: senderId,
|
|
341
|
+
});
|
|
342
|
+
if (!userMatch.allowed) {
|
|
343
|
+
logVerboseMessage(
|
|
344
|
+
`badgerclaw: blocked sender ${senderId} (room users allowlist, ${roomMatchMeta}, ${formatAllowlistMatchMeta(
|
|
345
|
+
userMatch,
|
|
346
|
+
)})`,
|
|
347
|
+
);
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
if (isRoom && roomUsers.length === 0 && groupAllowConfigured && access.decision !== "allow") {
|
|
352
|
+
const groupAllowMatch = resolveMatrixAllowListMatch({
|
|
353
|
+
allowList: effectiveGroupAllowFrom,
|
|
354
|
+
userId: senderId,
|
|
355
|
+
});
|
|
356
|
+
if (!groupAllowMatch.allowed) {
|
|
357
|
+
logVerboseMessage(
|
|
358
|
+
`badgerclaw: blocked sender ${senderId} (groupAllowFrom, ${roomMatchMeta}, ${formatAllowlistMatchMeta(
|
|
359
|
+
groupAllowMatch,
|
|
360
|
+
)})`,
|
|
361
|
+
);
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
if (isRoom) {
|
|
366
|
+
logVerboseMessage(`badgerclaw: allow room ${roomId} (${roomMatchMeta})`);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const rawBody =
|
|
370
|
+
locationPayload?.text ?? (typeof content.body === "string" ? content.body.trim() : "");
|
|
371
|
+
let media: {
|
|
372
|
+
path: string;
|
|
373
|
+
contentType?: string;
|
|
374
|
+
placeholder: string;
|
|
375
|
+
} | null = null;
|
|
376
|
+
const contentUrl =
|
|
377
|
+
"url" in content && typeof content.url === "string" ? content.url : undefined;
|
|
378
|
+
const contentFile =
|
|
379
|
+
"file" in content && content.file && typeof content.file === "object"
|
|
380
|
+
? content.file
|
|
381
|
+
: undefined;
|
|
382
|
+
const mediaUrl = contentUrl ?? contentFile?.url;
|
|
383
|
+
if (!rawBody && !mediaUrl) {
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const contentInfo =
|
|
388
|
+
"info" in content && content.info && typeof content.info === "object"
|
|
389
|
+
? (content.info as { mimetype?: string; size?: number })
|
|
390
|
+
: undefined;
|
|
391
|
+
const contentType = contentInfo?.mimetype;
|
|
392
|
+
const contentSize = typeof contentInfo?.size === "number" ? contentInfo.size : undefined;
|
|
393
|
+
if (mediaUrl?.startsWith("mxc://")) {
|
|
394
|
+
try {
|
|
395
|
+
media = await downloadMatrixMedia({
|
|
396
|
+
client,
|
|
397
|
+
mxcUrl: mediaUrl,
|
|
398
|
+
contentType,
|
|
399
|
+
sizeBytes: contentSize,
|
|
400
|
+
maxBytes: mediaMaxBytes,
|
|
401
|
+
file: contentFile,
|
|
402
|
+
});
|
|
403
|
+
} catch (err) {
|
|
404
|
+
logVerboseMessage(`badgerclaw: media download failed: ${String(err)}`);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const bodyText = rawBody || media?.placeholder || "";
|
|
409
|
+
if (!bodyText) {
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const { wasMentioned, hasExplicitMention } = resolveMentions({
|
|
414
|
+
content,
|
|
415
|
+
userId: selfUserId,
|
|
416
|
+
text: bodyText,
|
|
417
|
+
mentionRegexes,
|
|
418
|
+
});
|
|
419
|
+
const allowTextCommands = core.channel.commands.shouldHandleTextCommands({
|
|
420
|
+
cfg,
|
|
421
|
+
surface: "badgerclaw",
|
|
422
|
+
});
|
|
423
|
+
const useAccessGroups = cfg.commands?.useAccessGroups !== false;
|
|
424
|
+
const senderAllowedForCommands = resolveMatrixAllowListMatches({
|
|
425
|
+
allowList: effectiveAllowFrom,
|
|
426
|
+
userId: senderId,
|
|
427
|
+
});
|
|
428
|
+
const senderAllowedForGroup = groupAllowConfigured
|
|
429
|
+
? resolveMatrixAllowListMatches({
|
|
430
|
+
allowList: effectiveGroupAllowFrom,
|
|
431
|
+
userId: senderId,
|
|
432
|
+
})
|
|
433
|
+
: false;
|
|
434
|
+
const senderAllowedForRoomUsers =
|
|
435
|
+
isRoom && roomUsers.length > 0
|
|
436
|
+
? resolveMatrixAllowListMatches({
|
|
437
|
+
allowList: normalizeMatrixAllowList(roomUsers),
|
|
438
|
+
userId: senderId,
|
|
439
|
+
})
|
|
440
|
+
: false;
|
|
441
|
+
const hasControlCommandInMessage = core.channel.text.hasControlCommand(bodyText, cfg);
|
|
442
|
+
const commandGate = resolveControlCommandGate({
|
|
443
|
+
useAccessGroups,
|
|
444
|
+
authorizers: [
|
|
445
|
+
{ configured: effectiveAllowFrom.length > 0, allowed: senderAllowedForCommands },
|
|
446
|
+
{ configured: roomUsers.length > 0, allowed: senderAllowedForRoomUsers },
|
|
447
|
+
{ configured: groupAllowConfigured, allowed: senderAllowedForGroup },
|
|
448
|
+
],
|
|
449
|
+
allowTextCommands,
|
|
450
|
+
hasControlCommand: hasControlCommandInMessage,
|
|
451
|
+
});
|
|
452
|
+
const commandAuthorized = commandGate.commandAuthorized;
|
|
453
|
+
if (isRoom && commandGate.shouldBlock) {
|
|
454
|
+
logInboundDrop({
|
|
455
|
+
log: logVerboseMessage,
|
|
456
|
+
channel: "badgerclaw",
|
|
457
|
+
reason: "control command (unauthorized)",
|
|
458
|
+
target: senderId,
|
|
459
|
+
});
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
const shouldRequireMention = isRoom
|
|
463
|
+
? roomConfig?.autoReply === true
|
|
464
|
+
? false
|
|
465
|
+
: roomConfig?.autoReply === false
|
|
466
|
+
? true
|
|
467
|
+
: typeof roomConfig?.requireMention === "boolean"
|
|
468
|
+
? roomConfig?.requireMention
|
|
469
|
+
: true
|
|
470
|
+
: false;
|
|
471
|
+
const shouldBypassMention =
|
|
472
|
+
allowTextCommands &&
|
|
473
|
+
isRoom &&
|
|
474
|
+
shouldRequireMention &&
|
|
475
|
+
!wasMentioned &&
|
|
476
|
+
!hasExplicitMention &&
|
|
477
|
+
commandAuthorized &&
|
|
478
|
+
hasControlCommandInMessage;
|
|
479
|
+
const canDetectMention = mentionRegexes.length > 0 || hasExplicitMention;
|
|
480
|
+
if (isRoom && shouldRequireMention && !wasMentioned && !shouldBypassMention) {
|
|
481
|
+
logger.info("skipping room message", { roomId, reason: "no-mention" });
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
const messageId = event.event_id ?? "";
|
|
486
|
+
const replyToEventId = content["m.relates_to"]?.["m.in_reply_to"]?.event_id;
|
|
487
|
+
const threadRootId = resolveMatrixThreadRootId({ event, content });
|
|
488
|
+
const threadTarget = resolveMatrixThreadTarget({
|
|
489
|
+
threadReplies,
|
|
490
|
+
messageId,
|
|
491
|
+
threadRootId,
|
|
492
|
+
isThreadRoot: false, // @vector-im/matrix-bot-sdk doesn't have this info readily available
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
const baseRoute = core.channel.routing.resolveAgentRoute({
|
|
496
|
+
cfg,
|
|
497
|
+
channel: "badgerclaw",
|
|
498
|
+
accountId,
|
|
499
|
+
peer: {
|
|
500
|
+
kind: isDirectMessage ? "direct" : "channel",
|
|
501
|
+
id: isDirectMessage ? senderId : roomId,
|
|
502
|
+
},
|
|
503
|
+
// For DMs, pass roomId as parentPeer so the conversation is bindable by room ID
|
|
504
|
+
// while preserving DM trust semantics (secure 1:1, no group restrictions).
|
|
505
|
+
parentPeer: isDirectMessage ? { kind: "channel", id: roomId } : undefined,
|
|
506
|
+
});
|
|
507
|
+
const baseRouteSession = resolveMatrixBaseRouteSession({
|
|
508
|
+
buildAgentSessionKey: core.channel.routing.buildAgentSessionKey,
|
|
509
|
+
baseRoute,
|
|
510
|
+
isDirectMessage,
|
|
511
|
+
roomId,
|
|
512
|
+
accountId,
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
const route = {
|
|
516
|
+
...baseRoute,
|
|
517
|
+
lastRoutePolicy: baseRouteSession.lastRoutePolicy,
|
|
518
|
+
sessionKey: threadRootId
|
|
519
|
+
? `${baseRouteSession.sessionKey}:thread:${threadRootId}`
|
|
520
|
+
: baseRouteSession.sessionKey,
|
|
521
|
+
};
|
|
522
|
+
|
|
523
|
+
let threadStarterBody: string | undefined;
|
|
524
|
+
let threadLabel: string | undefined;
|
|
525
|
+
let parentSessionKey: string | undefined;
|
|
526
|
+
|
|
527
|
+
if (threadRootId) {
|
|
528
|
+
const existingSession = core.channel.session.readSessionUpdatedAt({
|
|
529
|
+
storePath: core.channel.session.resolveStorePath(cfg.session?.store, {
|
|
530
|
+
agentId: baseRoute.agentId,
|
|
531
|
+
}),
|
|
532
|
+
sessionKey: route.sessionKey,
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
if (existingSession === undefined) {
|
|
536
|
+
try {
|
|
537
|
+
const rootEvent = await fetchEventSummary(client, roomId, threadRootId);
|
|
538
|
+
if (rootEvent?.body) {
|
|
539
|
+
const rootSenderName = rootEvent.sender
|
|
540
|
+
? await getMemberDisplayName(roomId, rootEvent.sender)
|
|
541
|
+
: undefined;
|
|
542
|
+
|
|
543
|
+
threadStarterBody = core.channel.reply.formatAgentEnvelope({
|
|
544
|
+
channel: "BadgerClaw",
|
|
545
|
+
from: rootSenderName ?? rootEvent.sender ?? "Unknown",
|
|
546
|
+
timestamp: rootEvent.timestamp,
|
|
547
|
+
envelope: core.channel.reply.resolveEnvelopeFormatOptions(cfg),
|
|
548
|
+
body: rootEvent.body,
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
threadLabel = `BadgerClaw thread in ${roomName ?? roomId}`;
|
|
552
|
+
parentSessionKey = baseRoute.sessionKey;
|
|
553
|
+
}
|
|
554
|
+
} catch (err) {
|
|
555
|
+
logVerboseMessage(
|
|
556
|
+
`badgerclaw: failed to fetch thread root ${threadRootId}: ${String(err)}`,
|
|
557
|
+
);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
const envelopeFrom = isDirectMessage ? senderName : (roomName ?? roomId);
|
|
563
|
+
const textWithId = threadRootId
|
|
564
|
+
? `${bodyText}\n[badgerclaw event id: ${messageId} room: ${roomId} thread: ${threadRootId}]`
|
|
565
|
+
: `${bodyText}\n[badgerclaw event id: ${messageId} room: ${roomId}]`;
|
|
566
|
+
const { storePath, envelopeOptions, previousTimestamp } =
|
|
567
|
+
resolveInboundSessionEnvelopeContext({
|
|
568
|
+
cfg,
|
|
569
|
+
agentId: route.agentId,
|
|
570
|
+
sessionKey: route.sessionKey,
|
|
571
|
+
});
|
|
572
|
+
const body = core.channel.reply.formatInboundEnvelope({
|
|
573
|
+
channel: "BadgerClaw",
|
|
574
|
+
from: envelopeFrom,
|
|
575
|
+
timestamp: eventTs ?? undefined,
|
|
576
|
+
previousTimestamp,
|
|
577
|
+
envelope: envelopeOptions,
|
|
578
|
+
body: textWithId,
|
|
579
|
+
chatType: isDirectMessage ? "direct" : "channel",
|
|
580
|
+
senderLabel,
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
const groupSystemPrompt = roomConfig?.systemPrompt?.trim() || undefined;
|
|
584
|
+
const ctxPayload = core.channel.reply.finalizeInboundContext({
|
|
585
|
+
Body: body,
|
|
586
|
+
BodyForAgent: resolveMatrixBodyForAgent({
|
|
587
|
+
isDirectMessage,
|
|
588
|
+
bodyText,
|
|
589
|
+
senderLabel,
|
|
590
|
+
}),
|
|
591
|
+
RawBody: bodyText,
|
|
592
|
+
CommandBody: bodyText,
|
|
593
|
+
From: isDirectMessage ? `badgerclaw:${senderId}` : `badgerclaw:channel:${roomId}`,
|
|
594
|
+
To: `room:${roomId}`,
|
|
595
|
+
SessionKey: route.sessionKey,
|
|
596
|
+
AccountId: route.accountId,
|
|
597
|
+
ChatType: threadRootId ? "thread" : isDirectMessage ? "direct" : "channel",
|
|
598
|
+
ConversationLabel: envelopeFrom,
|
|
599
|
+
SenderName: senderName,
|
|
600
|
+
SenderId: senderId,
|
|
601
|
+
SenderUsername: senderUsername,
|
|
602
|
+
GroupSubject: isRoom ? (roomName ?? roomId) : undefined,
|
|
603
|
+
GroupChannel: isRoom ? (roomInfo.canonicalAlias ?? roomId) : undefined,
|
|
604
|
+
GroupSystemPrompt: isRoom ? groupSystemPrompt : undefined,
|
|
605
|
+
Provider: "badgerclaw" as const,
|
|
606
|
+
Surface: "badgerclaw" as const,
|
|
607
|
+
WasMentioned: isRoom ? wasMentioned : undefined,
|
|
608
|
+
MessageSid: messageId,
|
|
609
|
+
ReplyToId: threadTarget ? undefined : (replyToEventId ?? undefined),
|
|
610
|
+
MessageThreadId: threadTarget,
|
|
611
|
+
Timestamp: eventTs ?? undefined,
|
|
612
|
+
MediaPath: media?.path,
|
|
613
|
+
MediaType: media?.contentType,
|
|
614
|
+
MediaUrl: media?.path,
|
|
615
|
+
...locationPayload?.context,
|
|
616
|
+
CommandAuthorized: commandAuthorized,
|
|
617
|
+
CommandSource: "text" as const,
|
|
618
|
+
OriginatingChannel: "badgerclaw" as const,
|
|
619
|
+
OriginatingTo: `room:${roomId}`,
|
|
620
|
+
ThreadStarterBody: threadStarterBody,
|
|
621
|
+
ThreadLabel: threadLabel,
|
|
622
|
+
ParentSessionKey: parentSessionKey,
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
await core.channel.session.recordInboundSession({
|
|
626
|
+
storePath,
|
|
627
|
+
sessionKey: ctxPayload.SessionKey ?? route.sessionKey,
|
|
628
|
+
ctx: ctxPayload,
|
|
629
|
+
updateLastRoute: isDirectMessage
|
|
630
|
+
? {
|
|
631
|
+
sessionKey: route.mainSessionKey,
|
|
632
|
+
channel: "badgerclaw",
|
|
633
|
+
to: `room:${roomId}`,
|
|
634
|
+
accountId: route.accountId,
|
|
635
|
+
}
|
|
636
|
+
: undefined,
|
|
637
|
+
onRecordError: (err) => {
|
|
638
|
+
logger.warn("failed updating session meta", {
|
|
639
|
+
error: String(err),
|
|
640
|
+
storePath,
|
|
641
|
+
sessionKey: ctxPayload.SessionKey ?? route.sessionKey,
|
|
642
|
+
});
|
|
643
|
+
},
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
const preview = bodyText.slice(0, 200).replace(/\n/g, "\\n");
|
|
647
|
+
logVerboseMessage(`badgerclaw inbound: room=${roomId} from=${senderId} preview="${preview}"`);
|
|
648
|
+
|
|
649
|
+
const ackReaction = (cfg.messages?.ackReaction ?? "").trim();
|
|
650
|
+
const ackScope = cfg.messages?.ackReactionScope ?? "group-mentions";
|
|
651
|
+
const shouldAckReaction = () =>
|
|
652
|
+
Boolean(
|
|
653
|
+
ackReaction &&
|
|
654
|
+
core.channel.reactions.shouldAckReaction({
|
|
655
|
+
scope: ackScope,
|
|
656
|
+
isDirect: isDirectMessage,
|
|
657
|
+
isGroup: isRoom,
|
|
658
|
+
isMentionableGroup: isRoom,
|
|
659
|
+
requireMention: Boolean(shouldRequireMention),
|
|
660
|
+
canDetectMention,
|
|
661
|
+
effectiveWasMentioned: wasMentioned || shouldBypassMention,
|
|
662
|
+
shouldBypassMention,
|
|
663
|
+
}),
|
|
664
|
+
);
|
|
665
|
+
if (shouldAckReaction() && messageId) {
|
|
666
|
+
reactMatrixMessage(roomId, messageId, ackReaction, client).catch((err) => {
|
|
667
|
+
logVerboseMessage(`badgerclaw react failed for room ${roomId}: ${String(err)}`);
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
const replyTarget = ctxPayload.To;
|
|
672
|
+
if (!replyTarget) {
|
|
673
|
+
runtime.error?.("badgerclaw: missing reply target");
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
let didSendReply = false;
|
|
678
|
+
const tableMode = core.channel.text.resolveMarkdownTableMode({
|
|
679
|
+
cfg,
|
|
680
|
+
channel: "badgerclaw",
|
|
681
|
+
accountId: route.accountId,
|
|
682
|
+
});
|
|
683
|
+
const { onModelSelected, ...prefixOptions } = createReplyPrefixOptions({
|
|
684
|
+
cfg,
|
|
685
|
+
agentId: route.agentId,
|
|
686
|
+
channel: "badgerclaw",
|
|
687
|
+
accountId: route.accountId,
|
|
688
|
+
});
|
|
689
|
+
const humanDelay = core.channel.reply.resolveHumanDelayConfig(cfg, route.agentId);
|
|
690
|
+
const typingCallbacks = createTypingCallbacks({
|
|
691
|
+
start: () => sendTypingMatrix(roomId, true, undefined, client),
|
|
692
|
+
stop: () => sendTypingMatrix(roomId, false, undefined, client),
|
|
693
|
+
onStartError: (err) => {
|
|
694
|
+
logTypingFailure({
|
|
695
|
+
log: logVerboseMessage,
|
|
696
|
+
channel: "badgerclaw",
|
|
697
|
+
action: "start",
|
|
698
|
+
target: roomId,
|
|
699
|
+
error: err,
|
|
700
|
+
});
|
|
701
|
+
},
|
|
702
|
+
onStopError: (err) => {
|
|
703
|
+
logTypingFailure({
|
|
704
|
+
log: logVerboseMessage,
|
|
705
|
+
channel: "badgerclaw",
|
|
706
|
+
action: "stop",
|
|
707
|
+
target: roomId,
|
|
708
|
+
error: err,
|
|
709
|
+
});
|
|
710
|
+
},
|
|
711
|
+
});
|
|
712
|
+
const { dispatcher, replyOptions, markDispatchIdle } =
|
|
713
|
+
core.channel.reply.createReplyDispatcherWithTyping({
|
|
714
|
+
...prefixOptions,
|
|
715
|
+
humanDelay,
|
|
716
|
+
typingCallbacks,
|
|
717
|
+
deliver: async (payload) => {
|
|
718
|
+
await deliverMatrixReplies({
|
|
719
|
+
replies: [payload],
|
|
720
|
+
roomId,
|
|
721
|
+
client,
|
|
722
|
+
runtime,
|
|
723
|
+
textLimit,
|
|
724
|
+
replyToMode,
|
|
725
|
+
threadId: threadTarget,
|
|
726
|
+
accountId: route.accountId,
|
|
727
|
+
tableMode,
|
|
728
|
+
});
|
|
729
|
+
didSendReply = true;
|
|
730
|
+
},
|
|
731
|
+
onError: (err, info) => {
|
|
732
|
+
runtime.error?.(`badgerclaw ${info.kind} reply failed: ${String(err)}`);
|
|
733
|
+
},
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
const { queuedFinal, counts } = await dispatchReplyFromConfigWithSettledDispatcher({
|
|
737
|
+
cfg,
|
|
738
|
+
ctxPayload,
|
|
739
|
+
dispatcher,
|
|
740
|
+
onSettled: () => {
|
|
741
|
+
markDispatchIdle();
|
|
742
|
+
},
|
|
743
|
+
replyOptions: {
|
|
744
|
+
...replyOptions,
|
|
745
|
+
skillFilter: roomConfig?.skills,
|
|
746
|
+
onModelSelected,
|
|
747
|
+
},
|
|
748
|
+
});
|
|
749
|
+
if (!queuedFinal) {
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
didSendReply = true;
|
|
753
|
+
const finalCount = counts.final;
|
|
754
|
+
logVerboseMessage(
|
|
755
|
+
`badgerclaw: delivered ${finalCount} reply${finalCount === 1 ? "" : "ies"} to ${replyTarget}`,
|
|
756
|
+
);
|
|
757
|
+
if (didSendReply) {
|
|
758
|
+
const previewText = bodyText.replace(/\s+/g, " ").slice(0, 160);
|
|
759
|
+
core.system.enqueueSystemEvent(`BadgerClaw message from ${senderName}: ${previewText}`, {
|
|
760
|
+
sessionKey: route.sessionKey,
|
|
761
|
+
contextKey: `badgerclaw:message:${roomId}:${messageId || "unknown"}`,
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
} catch (err) {
|
|
765
|
+
runtime.error?.(`badgerclaw handler failed: ${String(err)}`);
|
|
766
|
+
}
|
|
767
|
+
};
|
|
768
|
+
}
|