@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.
Files changed (70) hide show
  1. package/CHANGELOG.md +104 -0
  2. package/SETUP.md +131 -0
  3. package/index.ts +23 -0
  4. package/openclaw.plugin.json +1 -0
  5. package/package.json +32 -0
  6. package/src/actions.ts +195 -0
  7. package/src/channel.ts +461 -0
  8. package/src/config-schema.ts +62 -0
  9. package/src/connect.ts +17 -0
  10. package/src/directory-live.ts +209 -0
  11. package/src/group-mentions.ts +52 -0
  12. package/src/matrix/accounts.ts +114 -0
  13. package/src/matrix/actions/client.ts +47 -0
  14. package/src/matrix/actions/limits.ts +6 -0
  15. package/src/matrix/actions/messages.ts +126 -0
  16. package/src/matrix/actions/pins.ts +84 -0
  17. package/src/matrix/actions/reactions.ts +102 -0
  18. package/src/matrix/actions/room.ts +85 -0
  19. package/src/matrix/actions/summary.ts +75 -0
  20. package/src/matrix/actions/types.ts +85 -0
  21. package/src/matrix/actions.ts +15 -0
  22. package/src/matrix/active-client.ts +32 -0
  23. package/src/matrix/client/config.ts +245 -0
  24. package/src/matrix/client/create-client.ts +125 -0
  25. package/src/matrix/client/logging.ts +46 -0
  26. package/src/matrix/client/runtime.ts +4 -0
  27. package/src/matrix/client/shared.ts +210 -0
  28. package/src/matrix/client/startup.ts +29 -0
  29. package/src/matrix/client/storage.ts +131 -0
  30. package/src/matrix/client/types.ts +34 -0
  31. package/src/matrix/client-bootstrap.ts +47 -0
  32. package/src/matrix/client.ts +14 -0
  33. package/src/matrix/credentials.ts +125 -0
  34. package/src/matrix/deps.ts +126 -0
  35. package/src/matrix/format.ts +22 -0
  36. package/src/matrix/index.ts +11 -0
  37. package/src/matrix/monitor/access-policy.ts +126 -0
  38. package/src/matrix/monitor/allowlist.ts +94 -0
  39. package/src/matrix/monitor/auto-join.ts +72 -0
  40. package/src/matrix/monitor/direct.ts +152 -0
  41. package/src/matrix/monitor/events.ts +168 -0
  42. package/src/matrix/monitor/handler.ts +768 -0
  43. package/src/matrix/monitor/inbound-body.ts +28 -0
  44. package/src/matrix/monitor/index.ts +414 -0
  45. package/src/matrix/monitor/location.ts +100 -0
  46. package/src/matrix/monitor/media.ts +118 -0
  47. package/src/matrix/monitor/mentions.ts +62 -0
  48. package/src/matrix/monitor/replies.ts +124 -0
  49. package/src/matrix/monitor/room-info.ts +55 -0
  50. package/src/matrix/monitor/rooms.ts +47 -0
  51. package/src/matrix/monitor/threads.ts +68 -0
  52. package/src/matrix/monitor/types.ts +39 -0
  53. package/src/matrix/poll-types.ts +167 -0
  54. package/src/matrix/probe.ts +69 -0
  55. package/src/matrix/sdk-runtime.ts +18 -0
  56. package/src/matrix/send/client.ts +99 -0
  57. package/src/matrix/send/formatting.ts +93 -0
  58. package/src/matrix/send/media.ts +230 -0
  59. package/src/matrix/send/targets.ts +150 -0
  60. package/src/matrix/send/types.ts +110 -0
  61. package/src/matrix/send-queue.ts +28 -0
  62. package/src/matrix/send.ts +267 -0
  63. package/src/onboarding.ts +331 -0
  64. package/src/outbound.ts +58 -0
  65. package/src/resolve-targets.ts +125 -0
  66. package/src/runtime.ts +6 -0
  67. package/src/secret-input.ts +13 -0
  68. package/src/test-mocks.ts +53 -0
  69. package/src/tool-actions.ts +164 -0
  70. package/src/types.ts +118 -0
@@ -0,0 +1,267 @@
1
+ import type { MatrixClient } from "@vector-im/matrix-bot-sdk";
2
+ import type { PollInput } from "openclaw/plugin-sdk/matrix";
3
+ import { getMatrixRuntime } from "../runtime.js";
4
+ import { buildPollStartContent, M_POLL_START } from "./poll-types.js";
5
+ import { enqueueSend } from "./send-queue.js";
6
+ import { resolveMatrixClient, resolveMediaMaxBytes } from "./send/client.js";
7
+ import {
8
+ buildReplyRelation,
9
+ buildTextContent,
10
+ buildThreadRelation,
11
+ resolveMatrixMsgType,
12
+ resolveMatrixVoiceDecision,
13
+ } from "./send/formatting.js";
14
+ import {
15
+ buildMediaContent,
16
+ prepareImageInfo,
17
+ resolveMediaDurationMs,
18
+ uploadMediaMaybeEncrypted,
19
+ } from "./send/media.js";
20
+ import { normalizeThreadId, resolveMatrixRoomId } from "./send/targets.js";
21
+ import {
22
+ EventType,
23
+ MsgType,
24
+ RelationType,
25
+ type MatrixOutboundContent,
26
+ type MatrixSendOpts,
27
+ type MatrixSendResult,
28
+ type ReactionEventContent,
29
+ } from "./send/types.js";
30
+
31
+ const MATRIX_TEXT_LIMIT = 4000;
32
+ const getCore = () => getMatrixRuntime();
33
+
34
+ export type { MatrixSendOpts, MatrixSendResult } from "./send/types.js";
35
+ export { resolveMatrixRoomId } from "./send/targets.js";
36
+
37
+ export async function sendMessageMatrix(
38
+ to: string,
39
+ message: string,
40
+ opts: MatrixSendOpts = {},
41
+ ): Promise<MatrixSendResult> {
42
+ const trimmedMessage = message?.trim() ?? "";
43
+ if (!trimmedMessage && !opts.mediaUrl) {
44
+ throw new Error("BadgerClaw send requires text or media");
45
+ }
46
+ const { client, stopOnDone } = await resolveMatrixClient({
47
+ client: opts.client,
48
+ timeoutMs: opts.timeoutMs,
49
+ accountId: opts.accountId,
50
+ cfg: opts.cfg,
51
+ });
52
+ const cfg = opts.cfg ?? getCore().config.loadConfig();
53
+ try {
54
+ const roomId = await resolveMatrixRoomId(client, to);
55
+ return await enqueueSend(roomId, async () => {
56
+ const tableMode = getCore().channel.text.resolveMarkdownTableMode({
57
+ cfg,
58
+ channel: "badgerclaw",
59
+ accountId: opts.accountId,
60
+ });
61
+ const convertedMessage = getCore().channel.text.convertMarkdownTables(
62
+ trimmedMessage,
63
+ tableMode,
64
+ );
65
+ const textLimit = getCore().channel.text.resolveTextChunkLimit(cfg, "badgerclaw");
66
+ const chunkLimit = Math.min(textLimit, MATRIX_TEXT_LIMIT);
67
+ const chunkMode = getCore().channel.text.resolveChunkMode(cfg, "badgerclaw", opts.accountId);
68
+ const chunks = getCore().channel.text.chunkMarkdownTextWithMode(
69
+ convertedMessage,
70
+ chunkLimit,
71
+ chunkMode,
72
+ );
73
+ const threadId = normalizeThreadId(opts.threadId);
74
+ const relation = threadId
75
+ ? buildThreadRelation(threadId, opts.replyToId)
76
+ : buildReplyRelation(opts.replyToId);
77
+ const sendContent = async (content: MatrixOutboundContent) => {
78
+ // @vector-im/matrix-bot-sdk uses sendMessage differently
79
+ const eventId = await client.sendMessage(roomId, content);
80
+ return eventId;
81
+ };
82
+
83
+ let lastMessageId = "";
84
+ if (opts.mediaUrl) {
85
+ const maxBytes = resolveMediaMaxBytes(opts.accountId, cfg);
86
+ const media = await getCore().media.loadWebMedia(opts.mediaUrl, maxBytes);
87
+ const uploaded = await uploadMediaMaybeEncrypted(client, roomId, media.buffer, {
88
+ contentType: media.contentType,
89
+ filename: media.fileName,
90
+ });
91
+ const durationMs = await resolveMediaDurationMs({
92
+ buffer: media.buffer,
93
+ contentType: media.contentType,
94
+ fileName: media.fileName,
95
+ kind: media.kind ?? "unknown",
96
+ });
97
+ const baseMsgType = resolveMatrixMsgType(media.contentType, media.fileName);
98
+ const { useVoice } = resolveMatrixVoiceDecision({
99
+ wantsVoice: opts.audioAsVoice === true,
100
+ contentType: media.contentType,
101
+ fileName: media.fileName,
102
+ });
103
+ const msgtype = useVoice ? MsgType.Audio : baseMsgType;
104
+ const isImage = msgtype === MsgType.Image;
105
+ const imageInfo = isImage
106
+ ? await prepareImageInfo({ buffer: media.buffer, client })
107
+ : undefined;
108
+ const [firstChunk, ...rest] = chunks;
109
+ const body = useVoice ? "Voice message" : (firstChunk ?? media.fileName ?? "(file)");
110
+ const content = buildMediaContent({
111
+ msgtype,
112
+ body,
113
+ url: uploaded.url,
114
+ file: uploaded.file,
115
+ filename: media.fileName,
116
+ mimetype: media.contentType,
117
+ size: media.buffer.byteLength,
118
+ durationMs,
119
+ relation,
120
+ isVoice: useVoice,
121
+ imageInfo,
122
+ });
123
+ const eventId = await sendContent(content);
124
+ lastMessageId = eventId ?? lastMessageId;
125
+ const textChunks = useVoice ? chunks : rest;
126
+ const followupRelation = threadId ? relation : undefined;
127
+ for (const chunk of textChunks) {
128
+ const text = chunk.trim();
129
+ if (!text) {
130
+ continue;
131
+ }
132
+ const followup = buildTextContent(text, followupRelation);
133
+ const followupEventId = await sendContent(followup);
134
+ lastMessageId = followupEventId ?? lastMessageId;
135
+ }
136
+ } else {
137
+ for (const chunk of chunks.length ? chunks : [""]) {
138
+ const text = chunk.trim();
139
+ if (!text) {
140
+ continue;
141
+ }
142
+ const content = buildTextContent(text, relation);
143
+ const eventId = await sendContent(content);
144
+ lastMessageId = eventId ?? lastMessageId;
145
+ }
146
+ }
147
+
148
+ return {
149
+ messageId: lastMessageId || "unknown",
150
+ roomId,
151
+ };
152
+ });
153
+ } finally {
154
+ if (stopOnDone) {
155
+ client.stop();
156
+ }
157
+ }
158
+ }
159
+
160
+ export async function sendPollMatrix(
161
+ to: string,
162
+ poll: PollInput,
163
+ opts: MatrixSendOpts = {},
164
+ ): Promise<{ eventId: string; roomId: string }> {
165
+ if (!poll.question?.trim()) {
166
+ throw new Error("BadgerClaw poll requires a question");
167
+ }
168
+ if (!poll.options?.length) {
169
+ throw new Error("BadgerClaw poll requires options");
170
+ }
171
+ const { client, stopOnDone } = await resolveMatrixClient({
172
+ client: opts.client,
173
+ timeoutMs: opts.timeoutMs,
174
+ accountId: opts.accountId,
175
+ cfg: opts.cfg,
176
+ });
177
+
178
+ try {
179
+ const roomId = await resolveMatrixRoomId(client, to);
180
+ const pollContent = buildPollStartContent(poll);
181
+ const threadId = normalizeThreadId(opts.threadId);
182
+ const pollPayload = threadId
183
+ ? { ...pollContent, "m.relates_to": buildThreadRelation(threadId) }
184
+ : pollContent;
185
+ // @vector-im/matrix-bot-sdk sendEvent returns eventId string directly
186
+ const eventId = await client.sendEvent(roomId, M_POLL_START, pollPayload);
187
+
188
+ return {
189
+ eventId: eventId ?? "unknown",
190
+ roomId,
191
+ };
192
+ } finally {
193
+ if (stopOnDone) {
194
+ client.stop();
195
+ }
196
+ }
197
+ }
198
+
199
+ export async function sendTypingMatrix(
200
+ roomId: string,
201
+ typing: boolean,
202
+ timeoutMs?: number,
203
+ client?: MatrixClient,
204
+ ): Promise<void> {
205
+ const { client: resolved, stopOnDone } = await resolveMatrixClient({
206
+ client,
207
+ timeoutMs,
208
+ });
209
+ try {
210
+ const resolvedTimeoutMs = typeof timeoutMs === "number" ? timeoutMs : 30_000;
211
+ await resolved.setTyping(roomId, typing, resolvedTimeoutMs);
212
+ } finally {
213
+ if (stopOnDone) {
214
+ resolved.stop();
215
+ }
216
+ }
217
+ }
218
+
219
+ export async function sendReadReceiptMatrix(
220
+ roomId: string,
221
+ eventId: string,
222
+ client?: MatrixClient,
223
+ ): Promise<void> {
224
+ if (!eventId?.trim()) {
225
+ return;
226
+ }
227
+ const { client: resolved, stopOnDone } = await resolveMatrixClient({
228
+ client,
229
+ });
230
+ try {
231
+ const resolvedRoom = await resolveMatrixRoomId(resolved, roomId);
232
+ await resolved.sendReadReceipt(resolvedRoom, eventId.trim());
233
+ } finally {
234
+ if (stopOnDone) {
235
+ resolved.stop();
236
+ }
237
+ }
238
+ }
239
+
240
+ export async function reactMatrixMessage(
241
+ roomId: string,
242
+ messageId: string,
243
+ emoji: string,
244
+ client?: MatrixClient,
245
+ ): Promise<void> {
246
+ if (!emoji.trim()) {
247
+ throw new Error("BadgerClaw reaction requires an emoji");
248
+ }
249
+ const { client: resolved, stopOnDone } = await resolveMatrixClient({
250
+ client,
251
+ });
252
+ try {
253
+ const resolvedRoom = await resolveMatrixRoomId(resolved, roomId);
254
+ const reaction: ReactionEventContent = {
255
+ "m.relates_to": {
256
+ rel_type: RelationType.Annotation,
257
+ event_id: messageId,
258
+ key: emoji,
259
+ },
260
+ };
261
+ await resolved.sendEvent(resolvedRoom, EventType.Reaction, reaction);
262
+ } finally {
263
+ if (stopOnDone) {
264
+ resolved.stop();
265
+ }
266
+ }
267
+ }
@@ -0,0 +1,331 @@
1
+ import type { DmPolicy } from "openclaw/plugin-sdk/matrix";
2
+ import {
3
+ addWildcardAllowFrom,
4
+ formatResolvedUnresolvedNote,
5
+ mergeAllowFromEntries,
6
+ promptChannelAccessConfig,
7
+ setTopLevelChannelGroupPolicy,
8
+ type ChannelOnboardingAdapter,
9
+ type ChannelOnboardingDmPolicy,
10
+ type WizardPrompter,
11
+ } from "openclaw/plugin-sdk/matrix";
12
+ import { redeemPairingCode } from "./connect.js";
13
+ import { listMatrixDirectoryGroupsLive } from "./directory-live.js";
14
+ import { resolveMatrixAccount } from "./matrix/accounts.js";
15
+ import { ensureMatrixSdkInstalled, isMatrixSdkAvailable } from "./matrix/deps.js";
16
+ import { resolveMatrixTargets } from "./resolve-targets.js";
17
+ import type { CoreConfig } from "./types.js";
18
+
19
+ const channel = "badgerclaw" as const;
20
+
21
+ function setMatrixDmPolicy(cfg: CoreConfig, policy: DmPolicy) {
22
+ const allowFrom =
23
+ policy === "open" ? addWildcardAllowFrom(cfg.channels?.badgerclaw?.dm?.allowFrom) : undefined;
24
+ return {
25
+ ...cfg,
26
+ channels: {
27
+ ...cfg.channels,
28
+ badgerclaw: {
29
+ ...cfg.channels?.badgerclaw,
30
+ dm: {
31
+ ...cfg.channels?.badgerclaw?.dm,
32
+ policy,
33
+ ...(allowFrom ? { allowFrom } : {}),
34
+ },
35
+ },
36
+ },
37
+ };
38
+ }
39
+
40
+ async function promptPairingCode(prompter: WizardPrompter): Promise<{
41
+ homeserver: string;
42
+ accessToken: string;
43
+ userId: string;
44
+ botName: string;
45
+ } | null> {
46
+ await prompter.note(
47
+ "BadgerClaw connects your OpenClaw agent to encrypted chat rooms.\n\nTo get started:\n1. Create a bot in the BadgerClaw app (/bot create <name>)\n2. You will receive a pairing code (BCK-XXXX-XXXX)\n3. Enter it below",
48
+ "BadgerClaw setup",
49
+ );
50
+
51
+ while (true) {
52
+ const code = String(
53
+ await prompter.text({
54
+ message: "Pairing code",
55
+ validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
56
+ }),
57
+ ).trim();
58
+
59
+ try {
60
+ const result = await redeemPairingCode(code);
61
+ return {
62
+ homeserver: result.homeserver,
63
+ accessToken: result.access_token,
64
+ userId: result.user_id,
65
+ botName: result.bot_name,
66
+ };
67
+ } catch (err) {
68
+ const message = err instanceof Error ? err.message : String(err);
69
+ await prompter.note(`Pairing failed: ${message}\n\nPlease try again.`, "BadgerClaw setup");
70
+ }
71
+ }
72
+ }
73
+
74
+ async function promptMatrixAllowFrom(params: {
75
+ cfg: CoreConfig;
76
+ prompter: WizardPrompter;
77
+ }): Promise<CoreConfig> {
78
+ const { cfg, prompter } = params;
79
+ const existingAllowFrom = cfg.channels?.badgerclaw?.dm?.allowFrom ?? [];
80
+ const account = resolveMatrixAccount({ cfg });
81
+ const canResolve = Boolean(account.configured);
82
+
83
+ const parseInput = (raw: string) =>
84
+ raw
85
+ .split(/[\n,;]+/g)
86
+ .map((entry) => entry.trim())
87
+ .filter(Boolean);
88
+
89
+ const isFullUserId = (value: string) => value.startsWith("@") && value.includes(":");
90
+
91
+ while (true) {
92
+ const entry = await prompter.text({
93
+ message: "BadgerClaw allowFrom (full @user:server; display name only if unique)",
94
+ placeholder: "@user:server",
95
+ initialValue: existingAllowFrom[0] ? String(existingAllowFrom[0]) : undefined,
96
+ validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
97
+ });
98
+ const parts = parseInput(String(entry));
99
+ const resolvedIds: string[] = [];
100
+ const pending: string[] = [];
101
+ const unresolved: string[] = [];
102
+ const unresolvedNotes: string[] = [];
103
+
104
+ for (const part of parts) {
105
+ if (isFullUserId(part)) {
106
+ resolvedIds.push(part);
107
+ continue;
108
+ }
109
+ if (!canResolve) {
110
+ unresolved.push(part);
111
+ continue;
112
+ }
113
+ pending.push(part);
114
+ }
115
+
116
+ if (pending.length > 0) {
117
+ const results = await resolveMatrixTargets({
118
+ cfg,
119
+ inputs: pending,
120
+ kind: "user",
121
+ }).catch(() => []);
122
+ for (const result of results) {
123
+ if (result?.resolved && result.id) {
124
+ resolvedIds.push(result.id);
125
+ continue;
126
+ }
127
+ if (result?.input) {
128
+ unresolved.push(result.input);
129
+ if (result.note) {
130
+ unresolvedNotes.push(`${result.input}: ${result.note}`);
131
+ }
132
+ }
133
+ }
134
+ }
135
+
136
+ if (unresolved.length > 0) {
137
+ const details = unresolvedNotes.length > 0 ? unresolvedNotes : unresolved;
138
+ await prompter.note(
139
+ `Could not resolve:\n${details.join("\n")}\nUse full @user:server IDs.`,
140
+ "BadgerClaw allowlist",
141
+ );
142
+ continue;
143
+ }
144
+
145
+ const unique = mergeAllowFromEntries(existingAllowFrom, resolvedIds);
146
+ return {
147
+ ...cfg,
148
+ channels: {
149
+ ...cfg.channels,
150
+ badgerclaw: {
151
+ ...cfg.channels?.badgerclaw,
152
+ enabled: true,
153
+ dm: {
154
+ ...cfg.channels?.badgerclaw?.dm,
155
+ policy: "allowlist",
156
+ allowFrom: unique,
157
+ },
158
+ },
159
+ },
160
+ };
161
+ }
162
+ }
163
+
164
+ function setMatrixGroupPolicy(cfg: CoreConfig, groupPolicy: "open" | "allowlist" | "disabled") {
165
+ return setTopLevelChannelGroupPolicy({
166
+ cfg,
167
+ channel: "badgerclaw",
168
+ groupPolicy,
169
+ enabled: true,
170
+ }) as CoreConfig;
171
+ }
172
+
173
+ function setMatrixGroupRooms(cfg: CoreConfig, roomKeys: string[]) {
174
+ const groups = Object.fromEntries(roomKeys.map((key) => [key, { allow: true }]));
175
+ return {
176
+ ...cfg,
177
+ channels: {
178
+ ...cfg.channels,
179
+ badgerclaw: {
180
+ ...cfg.channels?.badgerclaw,
181
+ enabled: true,
182
+ groups,
183
+ },
184
+ },
185
+ };
186
+ }
187
+
188
+ const dmPolicy: ChannelOnboardingDmPolicy = {
189
+ label: "BadgerClaw",
190
+ channel,
191
+ policyKey: "channels.badgerclaw.dm.policy",
192
+ allowFromKey: "channels.badgerclaw.dm.allowFrom",
193
+ getCurrent: (cfg) => (cfg as CoreConfig).channels?.badgerclaw?.dm?.policy ?? "pairing",
194
+ setPolicy: (cfg, policy) => setMatrixDmPolicy(cfg as CoreConfig, policy),
195
+ promptAllowFrom: promptMatrixAllowFrom,
196
+ };
197
+
198
+ export const badgerclawOnboardingAdapter: ChannelOnboardingAdapter = {
199
+ channel,
200
+ getStatus: async ({ cfg }) => {
201
+ const account = resolveMatrixAccount({ cfg: cfg as CoreConfig });
202
+ const configured = account.configured;
203
+ const sdkReady = isMatrixSdkAvailable();
204
+ return {
205
+ channel,
206
+ configured,
207
+ statusLines: [
208
+ `BadgerClaw: ${configured ? "configured" : "needs homeserver + access token or password"}`,
209
+ ],
210
+ selectionHint: !sdkReady
211
+ ? "install @vector-im/matrix-bot-sdk"
212
+ : configured
213
+ ? "configured"
214
+ : "needs auth",
215
+ };
216
+ },
217
+ configure: async ({ cfg, runtime, prompter, forceAllowFrom }) => {
218
+ let next = cfg as CoreConfig;
219
+ await ensureMatrixSdkInstalled({
220
+ runtime,
221
+ confirm: async (message) =>
222
+ await prompter.confirm({
223
+ message,
224
+ initialValue: true,
225
+ }),
226
+ });
227
+
228
+ // Pairing code flow — the primary onboarding path
229
+ const pairing = await promptPairingCode(prompter);
230
+ if (pairing) {
231
+ next = {
232
+ ...next,
233
+ channels: {
234
+ ...next.channels,
235
+ badgerclaw: {
236
+ ...next.channels?.badgerclaw,
237
+ enabled: true,
238
+ homeserver: pairing.homeserver,
239
+ accessToken: pairing.accessToken,
240
+ userId: pairing.userId,
241
+ deviceName: pairing.botName || "OpenClaw Gateway",
242
+ encryption: true,
243
+ autoJoin: "always",
244
+ dm: {
245
+ ...next.channels?.badgerclaw?.dm,
246
+ policy: "open",
247
+ allowFrom: addWildcardAllowFrom(next.channels?.badgerclaw?.dm?.allowFrom),
248
+ },
249
+ },
250
+ },
251
+ };
252
+ }
253
+
254
+ if (forceAllowFrom) {
255
+ next = await promptMatrixAllowFrom({ cfg: next, prompter });
256
+ }
257
+
258
+ const existingGroups = next.channels?.badgerclaw?.groups ?? next.channels?.badgerclaw?.rooms;
259
+ const accessConfig = await promptChannelAccessConfig({
260
+ prompter,
261
+ label: "BadgerClaw rooms",
262
+ currentPolicy: next.channels?.badgerclaw?.groupPolicy ?? "allowlist",
263
+ currentEntries: Object.keys(existingGroups ?? {}),
264
+ placeholder: "!roomId:server, #alias:server, Project Room",
265
+ updatePrompt: Boolean(existingGroups),
266
+ });
267
+ if (accessConfig) {
268
+ if (accessConfig.policy !== "allowlist") {
269
+ next = setMatrixGroupPolicy(next, accessConfig.policy);
270
+ } else {
271
+ let roomKeys = accessConfig.entries;
272
+ if (accessConfig.entries.length > 0) {
273
+ try {
274
+ const resolvedIds: string[] = [];
275
+ const unresolved: string[] = [];
276
+ for (const entry of accessConfig.entries) {
277
+ const trimmed = entry.trim();
278
+ if (!trimmed) {
279
+ continue;
280
+ }
281
+ const cleaned = trimmed.replace(/^(room|channel):/i, "").trim();
282
+ if (cleaned.startsWith("!") && cleaned.includes(":")) {
283
+ resolvedIds.push(cleaned);
284
+ continue;
285
+ }
286
+ const matches = await listMatrixDirectoryGroupsLive({
287
+ cfg: next,
288
+ query: trimmed,
289
+ limit: 10,
290
+ });
291
+ const exact = matches.find(
292
+ (match) => (match.name ?? "").toLowerCase() === trimmed.toLowerCase(),
293
+ );
294
+ const best = exact ?? matches[0];
295
+ if (best?.id) {
296
+ resolvedIds.push(best.id);
297
+ } else {
298
+ unresolved.push(entry);
299
+ }
300
+ }
301
+ roomKeys = [...resolvedIds, ...unresolved.map((entry) => entry.trim()).filter(Boolean)];
302
+ const resolution = formatResolvedUnresolvedNote({
303
+ resolved: resolvedIds,
304
+ unresolved,
305
+ });
306
+ if (resolution) {
307
+ await prompter.note(resolution, "BadgerClaw rooms");
308
+ }
309
+ } catch (err) {
310
+ await prompter.note(
311
+ `Room lookup failed; keeping entries as typed. ${String(err)}`,
312
+ "BadgerClaw rooms",
313
+ );
314
+ }
315
+ }
316
+ next = setMatrixGroupPolicy(next, "allowlist");
317
+ next = setMatrixGroupRooms(next, roomKeys);
318
+ }
319
+ }
320
+
321
+ return { cfg: next };
322
+ },
323
+ dmPolicy,
324
+ disable: (cfg) => ({
325
+ ...(cfg as CoreConfig),
326
+ channels: {
327
+ ...(cfg as CoreConfig).channels,
328
+ badgerclaw: { ...(cfg as CoreConfig).channels?.badgerclaw, enabled: false },
329
+ },
330
+ }),
331
+ };
@@ -0,0 +1,58 @@
1
+ import type { ChannelOutboundAdapter } from "openclaw/plugin-sdk/matrix";
2
+ import { sendMessageMatrix, sendPollMatrix } from "./matrix/send.js";
3
+ import { getMatrixRuntime } from "./runtime.js";
4
+
5
+ export const matrixOutbound: ChannelOutboundAdapter = {
6
+ deliveryMode: "direct",
7
+ chunker: (text, limit) => getMatrixRuntime().channel.text.chunkMarkdownText(text, limit),
8
+ chunkerMode: "markdown",
9
+ textChunkLimit: 4000,
10
+ sendText: async ({ cfg, to, text, deps, replyToId, threadId, accountId }) => {
11
+ const send = deps?.sendMatrix ?? sendMessageMatrix;
12
+ const resolvedThreadId =
13
+ threadId !== undefined && threadId !== null ? String(threadId) : undefined;
14
+ const result = await send(to, text, {
15
+ cfg,
16
+ replyToId: replyToId ?? undefined,
17
+ threadId: resolvedThreadId,
18
+ accountId: accountId ?? undefined,
19
+ });
20
+ return {
21
+ channel: "badgerclaw",
22
+ messageId: result.messageId,
23
+ roomId: result.roomId,
24
+ };
25
+ },
26
+ sendMedia: async ({ cfg, to, text, mediaUrl, deps, replyToId, threadId, accountId }) => {
27
+ const send = deps?.sendMatrix ?? sendMessageMatrix;
28
+ const resolvedThreadId =
29
+ threadId !== undefined && threadId !== null ? String(threadId) : undefined;
30
+ const result = await send(to, text, {
31
+ cfg,
32
+ mediaUrl,
33
+ replyToId: replyToId ?? undefined,
34
+ threadId: resolvedThreadId,
35
+ accountId: accountId ?? undefined,
36
+ });
37
+ return {
38
+ channel: "badgerclaw",
39
+ messageId: result.messageId,
40
+ roomId: result.roomId,
41
+ };
42
+ },
43
+ sendPoll: async ({ cfg, to, poll, threadId, accountId }) => {
44
+ const resolvedThreadId =
45
+ threadId !== undefined && threadId !== null ? String(threadId) : undefined;
46
+ const result = await sendPollMatrix(to, poll, {
47
+ cfg,
48
+ threadId: resolvedThreadId,
49
+ accountId: accountId ?? undefined,
50
+ });
51
+ return {
52
+ channel: "badgerclaw",
53
+ messageId: result.eventId,
54
+ roomId: result.roomId,
55
+ pollId: result.eventId,
56
+ };
57
+ },
58
+ };