badgerclaw 0.1.7 → 1.4.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.
Potentially problematic release.
This version of badgerclaw might be problematic. Click here for more details.
- package/CHANGELOG.md +104 -0
- package/SETUP.md +291 -0
- package/index.ts +47 -0
- package/openclaw.plugin.json +1 -0
- package/package.json +32 -34
- package/scripts/postinstall.js +34 -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 +103 -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/backup.ts +91 -0
- package/src/matrix/client/config.ts +274 -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 +223 -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 +126 -0
- package/src/matrix/monitor/bot-commands.ts +431 -0
- package/src/matrix/monitor/chat-history.ts +75 -0
- package/src/matrix/monitor/direct.ts +152 -0
- package/src/matrix/monitor/events.ts +250 -0
- package/src/matrix/monitor/handler.ts +847 -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 +350 -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 +121 -0
- package/README.md +0 -32
- package/dist/commands/autopair.d.ts +0 -3
- package/dist/commands/autopair.js +0 -102
- package/dist/commands/autopair.js.map +0 -1
- package/dist/commands/bot.d.ts +0 -2
- package/dist/commands/bot.js +0 -94
- package/dist/commands/bot.js.map +0 -1
- package/dist/commands/login.d.ts +0 -2
- package/dist/commands/login.js +0 -88
- package/dist/commands/login.js.map +0 -1
- package/dist/commands/logout.d.ts +0 -2
- package/dist/commands/logout.js +0 -36
- package/dist/commands/logout.js.map +0 -1
- package/dist/commands/status.d.ts +0 -2
- package/dist/commands/status.js +0 -23
- package/dist/commands/status.js.map +0 -1
- package/dist/commands/watch.d.ts +0 -2
- package/dist/commands/watch.js +0 -29
- package/dist/commands/watch.js.map +0 -1
- package/dist/index.d.ts +0 -2
- package/dist/index.js +0 -23
- package/dist/index.js.map +0 -1
- package/dist/lib/api.d.ts +0 -4
- package/dist/lib/api.js +0 -37
- package/dist/lib/api.js.map +0 -1
- package/dist/lib/auth.d.ts +0 -11
- package/dist/lib/auth.js +0 -48
- package/dist/lib/auth.js.map +0 -1
- package/dist/lib/pkce.d.ts +0 -2
- package/dist/lib/pkce.js +0 -15
- package/dist/lib/pkce.js.map +0 -1
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/account-id";
|
|
2
|
+
import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/matrix";
|
|
3
|
+
import { getMatrixRuntime } from "../../runtime.js";
|
|
4
|
+
import {
|
|
5
|
+
normalizeResolvedSecretInputString,
|
|
6
|
+
normalizeSecretInputString,
|
|
7
|
+
} from "../../secret-input.js";
|
|
8
|
+
import type { CoreConfig } from "../../types.js";
|
|
9
|
+
import { loadMatrixSdk } from "../sdk-runtime.js";
|
|
10
|
+
import { ensureMatrixSdkLoggingConfigured } from "./logging.js";
|
|
11
|
+
import type { MatrixAuth, MatrixResolvedConfig } from "./types.js";
|
|
12
|
+
|
|
13
|
+
function clean(value: unknown, path: string): string {
|
|
14
|
+
return normalizeResolvedSecretInputString({ value, path }) ?? "";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Shallow-merge known nested config sub-objects so partial overrides inherit base values. */
|
|
18
|
+
function deepMergeConfig<T extends Record<string, unknown>>(base: T, override: Partial<T>): T {
|
|
19
|
+
const merged = { ...base, ...override } as Record<string, unknown>;
|
|
20
|
+
// Merge known nested objects (dm, actions) so partial overrides keep base fields
|
|
21
|
+
for (const key of ["dm", "actions"] as const) {
|
|
22
|
+
const b = base[key];
|
|
23
|
+
const o = override[key];
|
|
24
|
+
if (typeof b === "object" && b !== null && typeof o === "object" && o !== null) {
|
|
25
|
+
merged[key] = { ...(b as Record<string, unknown>), ...(o as Record<string, unknown>) };
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return merged as T;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Resolve Matrix config for a specific account, with fallback to top-level config.
|
|
33
|
+
* This supports both multi-account (channels.badgerclaw.accounts.*) and
|
|
34
|
+
* single-account (channels.badgerclaw.*) configurations.
|
|
35
|
+
*/
|
|
36
|
+
export function resolveMatrixConfigForAccount(
|
|
37
|
+
cfg: CoreConfig = getMatrixRuntime().config.loadConfig() as CoreConfig,
|
|
38
|
+
accountId?: string | null,
|
|
39
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
40
|
+
): MatrixResolvedConfig {
|
|
41
|
+
const normalizedAccountId = normalizeAccountId(accountId);
|
|
42
|
+
const matrixBase = cfg.channels?.badgerclaw ?? {};
|
|
43
|
+
const accounts = cfg.channels?.badgerclaw?.accounts;
|
|
44
|
+
|
|
45
|
+
// Try to get account-specific config first (direct lookup, then case-insensitive fallback)
|
|
46
|
+
let accountConfig = accounts?.[normalizedAccountId];
|
|
47
|
+
if (!accountConfig && accounts) {
|
|
48
|
+
for (const key of Object.keys(accounts)) {
|
|
49
|
+
if (normalizeAccountId(key) === normalizedAccountId) {
|
|
50
|
+
accountConfig = accounts[key];
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Deep merge: account-specific values override top-level values, preserving
|
|
57
|
+
// nested object inheritance (dm, actions, groups) so partial overrides work.
|
|
58
|
+
const matrix = accountConfig ? deepMergeConfig(matrixBase, accountConfig) : matrixBase;
|
|
59
|
+
|
|
60
|
+
const homeserver =
|
|
61
|
+
clean(matrix.homeserver, "channels.badgerclaw.homeserver") ||
|
|
62
|
+
clean(env.MATRIX_HOMESERVER, "MATRIX_HOMESERVER");
|
|
63
|
+
const userId =
|
|
64
|
+
clean(matrix.userId, "channels.badgerclaw.userId") || clean(env.MATRIX_USER_ID, "MATRIX_USER_ID");
|
|
65
|
+
const accessToken =
|
|
66
|
+
clean(matrix.accessToken, "channels.badgerclaw.accessToken") ||
|
|
67
|
+
clean(env.MATRIX_ACCESS_TOKEN, "MATRIX_ACCESS_TOKEN") ||
|
|
68
|
+
undefined;
|
|
69
|
+
const password =
|
|
70
|
+
clean(matrix.password, "channels.badgerclaw.password") ||
|
|
71
|
+
clean(env.MATRIX_PASSWORD, "MATRIX_PASSWORD") ||
|
|
72
|
+
undefined;
|
|
73
|
+
const deviceName =
|
|
74
|
+
clean(matrix.deviceName, "channels.badgerclaw.deviceName") ||
|
|
75
|
+
clean(env.MATRIX_DEVICE_NAME, "MATRIX_DEVICE_NAME") ||
|
|
76
|
+
undefined;
|
|
77
|
+
const initialSyncLimit =
|
|
78
|
+
typeof matrix.initialSyncLimit === "number"
|
|
79
|
+
? Math.max(0, Math.floor(matrix.initialSyncLimit))
|
|
80
|
+
: undefined;
|
|
81
|
+
const encryption = matrix.encryption ?? false;
|
|
82
|
+
return {
|
|
83
|
+
homeserver,
|
|
84
|
+
userId,
|
|
85
|
+
accessToken,
|
|
86
|
+
password,
|
|
87
|
+
deviceName,
|
|
88
|
+
initialSyncLimit,
|
|
89
|
+
encryption,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Single-account function for backward compatibility - resolves default account config.
|
|
95
|
+
*/
|
|
96
|
+
export function resolveMatrixConfig(
|
|
97
|
+
cfg: CoreConfig = getMatrixRuntime().config.loadConfig() as CoreConfig,
|
|
98
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
99
|
+
): MatrixResolvedConfig {
|
|
100
|
+
return resolveMatrixConfigForAccount(cfg, DEFAULT_ACCOUNT_ID, env);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Resolve the actual homeserver base URL via Matrix .well-known discovery.
|
|
105
|
+
* Falls back to the provided URL if discovery fails or returns no result.
|
|
106
|
+
* This ensures the bot always connects to the correct shard even when only
|
|
107
|
+
* the canonical domain (e.g. badger.signout.io) is configured.
|
|
108
|
+
*/
|
|
109
|
+
async function resolveHomeserverViaWellKnown(homeserver: string): Promise<string> {
|
|
110
|
+
try {
|
|
111
|
+
const url = new URL(homeserver);
|
|
112
|
+
const wellKnownUrl = `${url.protocol}//${url.host}/.well-known/matrix/client`;
|
|
113
|
+
const resp = await fetch(wellKnownUrl, { signal: AbortSignal.timeout(5000) });
|
|
114
|
+
if (resp.ok) {
|
|
115
|
+
const data = (await resp.json()) as Record<string, unknown>;
|
|
116
|
+
const baseUrl = (data?.["m.homeserver"] as Record<string, unknown>)?.["base_url"];
|
|
117
|
+
if (typeof baseUrl === "string" && baseUrl.startsWith("http")) {
|
|
118
|
+
// Strip trailing slash for consistency
|
|
119
|
+
return baseUrl.replace(/\/$/, "");
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
} catch {
|
|
123
|
+
// Discovery failed — use configured URL as-is
|
|
124
|
+
}
|
|
125
|
+
return homeserver;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export async function resolveMatrixAuth(params?: {
|
|
129
|
+
cfg?: CoreConfig;
|
|
130
|
+
env?: NodeJS.ProcessEnv;
|
|
131
|
+
accountId?: string | null;
|
|
132
|
+
}): Promise<MatrixAuth> {
|
|
133
|
+
const cfg = params?.cfg ?? (getMatrixRuntime().config.loadConfig() as CoreConfig);
|
|
134
|
+
const env = params?.env ?? process.env;
|
|
135
|
+
const resolved = resolveMatrixConfigForAccount(cfg, params?.accountId, env);
|
|
136
|
+
if (!resolved.homeserver) {
|
|
137
|
+
throw new Error("BadgerClaw homeserver is required (matrix.homeserver)");
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Resolve the actual shard via .well-known so we always hit the right server
|
|
141
|
+
// even when the canonical domain (e.g. badger.signout.io) is configured.
|
|
142
|
+
resolved.homeserver = await resolveHomeserverViaWellKnown(resolved.homeserver);
|
|
143
|
+
|
|
144
|
+
const {
|
|
145
|
+
loadMatrixCredentials,
|
|
146
|
+
saveMatrixCredentials,
|
|
147
|
+
credentialsMatchConfig,
|
|
148
|
+
touchMatrixCredentials,
|
|
149
|
+
} = await import("../credentials.js");
|
|
150
|
+
|
|
151
|
+
const accountId = params?.accountId;
|
|
152
|
+
const cached = loadMatrixCredentials(env, accountId);
|
|
153
|
+
const cachedCredentials =
|
|
154
|
+
cached &&
|
|
155
|
+
credentialsMatchConfig(cached, {
|
|
156
|
+
homeserver: resolved.homeserver,
|
|
157
|
+
userId: resolved.userId || "",
|
|
158
|
+
})
|
|
159
|
+
? cached
|
|
160
|
+
: null;
|
|
161
|
+
|
|
162
|
+
// If we have an access token, we can fetch userId via whoami if not provided
|
|
163
|
+
if (resolved.accessToken) {
|
|
164
|
+
let userId = resolved.userId;
|
|
165
|
+
if (!userId) {
|
|
166
|
+
// Fetch userId from access token via whoami
|
|
167
|
+
ensureMatrixSdkLoggingConfigured();
|
|
168
|
+
const { MatrixClient } = loadMatrixSdk();
|
|
169
|
+
const tempClient = new MatrixClient(resolved.homeserver, resolved.accessToken);
|
|
170
|
+
const whoami = await tempClient.getUserId();
|
|
171
|
+
userId = whoami;
|
|
172
|
+
// Save the credentials with the fetched userId
|
|
173
|
+
saveMatrixCredentials(
|
|
174
|
+
{
|
|
175
|
+
homeserver: resolved.homeserver,
|
|
176
|
+
userId,
|
|
177
|
+
accessToken: resolved.accessToken,
|
|
178
|
+
},
|
|
179
|
+
env,
|
|
180
|
+
accountId,
|
|
181
|
+
);
|
|
182
|
+
} else if (cachedCredentials && cachedCredentials.accessToken === resolved.accessToken) {
|
|
183
|
+
touchMatrixCredentials(env, accountId);
|
|
184
|
+
}
|
|
185
|
+
return {
|
|
186
|
+
homeserver: resolved.homeserver,
|
|
187
|
+
userId,
|
|
188
|
+
accessToken: resolved.accessToken,
|
|
189
|
+
deviceName: resolved.deviceName,
|
|
190
|
+
initialSyncLimit: resolved.initialSyncLimit,
|
|
191
|
+
encryption: resolved.encryption,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (cachedCredentials) {
|
|
196
|
+
touchMatrixCredentials(env, accountId);
|
|
197
|
+
return {
|
|
198
|
+
homeserver: cachedCredentials.homeserver,
|
|
199
|
+
userId: cachedCredentials.userId,
|
|
200
|
+
accessToken: cachedCredentials.accessToken,
|
|
201
|
+
deviceName: resolved.deviceName,
|
|
202
|
+
initialSyncLimit: resolved.initialSyncLimit,
|
|
203
|
+
encryption: resolved.encryption,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (!resolved.userId) {
|
|
208
|
+
throw new Error("BadgerClaw userId is required when no access token is configured (matrix.userId)");
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (!resolved.password) {
|
|
212
|
+
throw new Error(
|
|
213
|
+
"BadgerClaw password is required when no access token is configured (matrix.password)",
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Login with password using HTTP API.
|
|
218
|
+
const { response: loginResponse, release: releaseLoginResponse } = await fetchWithSsrFGuard({
|
|
219
|
+
url: `${resolved.homeserver}/_matrix/client/v3/login`,
|
|
220
|
+
init: {
|
|
221
|
+
method: "POST",
|
|
222
|
+
headers: { "Content-Type": "application/json" },
|
|
223
|
+
body: JSON.stringify({
|
|
224
|
+
type: "m.login.password",
|
|
225
|
+
identifier: { type: "m.id.user", user: resolved.userId },
|
|
226
|
+
password: resolved.password,
|
|
227
|
+
initial_device_display_name: resolved.deviceName ?? "OpenClaw Gateway",
|
|
228
|
+
}),
|
|
229
|
+
},
|
|
230
|
+
auditContext: "matrix.login",
|
|
231
|
+
});
|
|
232
|
+
const login = await (async () => {
|
|
233
|
+
try {
|
|
234
|
+
if (!loginResponse.ok) {
|
|
235
|
+
const errorText = await loginResponse.text();
|
|
236
|
+
throw new Error(`BadgerClaw login failed: ${errorText}`);
|
|
237
|
+
}
|
|
238
|
+
return (await loginResponse.json()) as {
|
|
239
|
+
access_token?: string;
|
|
240
|
+
user_id?: string;
|
|
241
|
+
device_id?: string;
|
|
242
|
+
};
|
|
243
|
+
} finally {
|
|
244
|
+
await releaseLoginResponse();
|
|
245
|
+
}
|
|
246
|
+
})();
|
|
247
|
+
|
|
248
|
+
const accessToken = login.access_token?.trim();
|
|
249
|
+
if (!accessToken) {
|
|
250
|
+
throw new Error("BadgerClaw login did not return an access token");
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const auth: MatrixAuth = {
|
|
254
|
+
homeserver: resolved.homeserver,
|
|
255
|
+
userId: login.user_id ?? resolved.userId,
|
|
256
|
+
accessToken,
|
|
257
|
+
deviceName: resolved.deviceName,
|
|
258
|
+
initialSyncLimit: resolved.initialSyncLimit,
|
|
259
|
+
encryption: resolved.encryption,
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
saveMatrixCredentials(
|
|
263
|
+
{
|
|
264
|
+
homeserver: auth.homeserver,
|
|
265
|
+
userId: auth.userId,
|
|
266
|
+
accessToken: auth.accessToken,
|
|
267
|
+
deviceId: login.device_id,
|
|
268
|
+
},
|
|
269
|
+
env,
|
|
270
|
+
accountId,
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
return auth;
|
|
274
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import type {
|
|
3
|
+
IStorageProvider,
|
|
4
|
+
ICryptoStorageProvider,
|
|
5
|
+
MatrixClient,
|
|
6
|
+
} from "@vector-im/matrix-bot-sdk";
|
|
7
|
+
import { loadMatrixSdk } from "../sdk-runtime.js";
|
|
8
|
+
import { ensureMatrixSdkLoggingConfigured } from "./logging.js";
|
|
9
|
+
import {
|
|
10
|
+
maybeMigrateLegacyStorage,
|
|
11
|
+
resolveMatrixStoragePaths,
|
|
12
|
+
writeStorageMeta,
|
|
13
|
+
} from "./storage.js";
|
|
14
|
+
|
|
15
|
+
function sanitizeUserIdList(input: unknown, label: string): string[] {
|
|
16
|
+
const LogService = loadMatrixSdk().LogService;
|
|
17
|
+
if (input == null) {
|
|
18
|
+
return [];
|
|
19
|
+
}
|
|
20
|
+
if (!Array.isArray(input)) {
|
|
21
|
+
LogService.warn(
|
|
22
|
+
"MatrixClientLite",
|
|
23
|
+
`Expected ${label} list to be an array, got ${typeof input}`,
|
|
24
|
+
);
|
|
25
|
+
return [];
|
|
26
|
+
}
|
|
27
|
+
const filtered = input.filter(
|
|
28
|
+
(entry): entry is string => typeof entry === "string" && entry.trim().length > 0,
|
|
29
|
+
);
|
|
30
|
+
if (filtered.length !== input.length) {
|
|
31
|
+
LogService.warn(
|
|
32
|
+
"MatrixClientLite",
|
|
33
|
+
`Dropping ${input.length - filtered.length} invalid ${label} entries from sync payload`,
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
return filtered;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function createMatrixClient(params: {
|
|
40
|
+
homeserver: string;
|
|
41
|
+
userId: string;
|
|
42
|
+
accessToken: string;
|
|
43
|
+
encryption?: boolean;
|
|
44
|
+
localTimeoutMs?: number;
|
|
45
|
+
accountId?: string | null;
|
|
46
|
+
}): Promise<MatrixClient> {
|
|
47
|
+
const { MatrixClient, SimpleFsStorageProvider, RustSdkCryptoStorageProvider, LogService } =
|
|
48
|
+
loadMatrixSdk();
|
|
49
|
+
ensureMatrixSdkLoggingConfigured();
|
|
50
|
+
const env = process.env;
|
|
51
|
+
|
|
52
|
+
// Create storage provider
|
|
53
|
+
const storagePaths = resolveMatrixStoragePaths({
|
|
54
|
+
homeserver: params.homeserver,
|
|
55
|
+
userId: params.userId,
|
|
56
|
+
accessToken: params.accessToken,
|
|
57
|
+
accountId: params.accountId,
|
|
58
|
+
env,
|
|
59
|
+
});
|
|
60
|
+
maybeMigrateLegacyStorage({ storagePaths, env });
|
|
61
|
+
fs.mkdirSync(storagePaths.rootDir, { recursive: true });
|
|
62
|
+
const storage: IStorageProvider = new SimpleFsStorageProvider(storagePaths.storagePath);
|
|
63
|
+
|
|
64
|
+
// Create crypto storage if encryption is enabled
|
|
65
|
+
let cryptoStorage: ICryptoStorageProvider | undefined;
|
|
66
|
+
if (params.encryption) {
|
|
67
|
+
fs.mkdirSync(storagePaths.cryptoPath, { recursive: true });
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
const { StoreType } = await import("@matrix-org/matrix-sdk-crypto-nodejs");
|
|
71
|
+
cryptoStorage = new RustSdkCryptoStorageProvider(storagePaths.cryptoPath, StoreType.Sqlite);
|
|
72
|
+
} catch (err) {
|
|
73
|
+
LogService.warn(
|
|
74
|
+
"MatrixClientLite",
|
|
75
|
+
"Failed to initialize crypto storage, E2EE disabled:",
|
|
76
|
+
err,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
writeStorageMeta({
|
|
82
|
+
storagePaths,
|
|
83
|
+
homeserver: params.homeserver,
|
|
84
|
+
userId: params.userId,
|
|
85
|
+
accountId: params.accountId,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const client = new MatrixClient(params.homeserver, params.accessToken, storage, cryptoStorage);
|
|
89
|
+
|
|
90
|
+
if (client.crypto) {
|
|
91
|
+
const originalUpdateSyncData = client.crypto.updateSyncData.bind(client.crypto);
|
|
92
|
+
client.crypto.updateSyncData = async (
|
|
93
|
+
toDeviceMessages,
|
|
94
|
+
otkCounts,
|
|
95
|
+
unusedFallbackKeyAlgs,
|
|
96
|
+
changedDeviceLists,
|
|
97
|
+
leftDeviceLists,
|
|
98
|
+
) => {
|
|
99
|
+
const safeChanged = sanitizeUserIdList(changedDeviceLists, "changed device list");
|
|
100
|
+
const safeLeft = sanitizeUserIdList(leftDeviceLists, "left device list");
|
|
101
|
+
try {
|
|
102
|
+
return await originalUpdateSyncData(
|
|
103
|
+
toDeviceMessages,
|
|
104
|
+
otkCounts,
|
|
105
|
+
unusedFallbackKeyAlgs,
|
|
106
|
+
safeChanged,
|
|
107
|
+
safeLeft,
|
|
108
|
+
);
|
|
109
|
+
} catch (err) {
|
|
110
|
+
const message = typeof err === "string" ? err : err instanceof Error ? err.message : "";
|
|
111
|
+
if (message.includes("Expect value to be String")) {
|
|
112
|
+
LogService.warn(
|
|
113
|
+
"MatrixClientLite",
|
|
114
|
+
"Ignoring malformed device list entries during crypto sync",
|
|
115
|
+
message,
|
|
116
|
+
);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
throw err;
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return client;
|
|
125
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { loadMatrixSdk } from "../sdk-runtime.js";
|
|
2
|
+
|
|
3
|
+
let matrixSdkLoggingConfigured = false;
|
|
4
|
+
let matrixSdkBaseLogger:
|
|
5
|
+
| {
|
|
6
|
+
trace: (module: string, ...messageOrObject: unknown[]) => void;
|
|
7
|
+
debug: (module: string, ...messageOrObject: unknown[]) => void;
|
|
8
|
+
info: (module: string, ...messageOrObject: unknown[]) => void;
|
|
9
|
+
warn: (module: string, ...messageOrObject: unknown[]) => void;
|
|
10
|
+
error: (module: string, ...messageOrObject: unknown[]) => void;
|
|
11
|
+
}
|
|
12
|
+
| undefined;
|
|
13
|
+
|
|
14
|
+
function shouldSuppressMatrixHttpNotFound(module: string, messageOrObject: unknown[]): boolean {
|
|
15
|
+
if (module !== "MatrixHttpClient") {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
return messageOrObject.some((entry) => {
|
|
19
|
+
if (!entry || typeof entry !== "object") {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
return (entry as { errcode?: string }).errcode === "M_NOT_FOUND";
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function ensureMatrixSdkLoggingConfigured(): void {
|
|
27
|
+
if (matrixSdkLoggingConfigured) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const { ConsoleLogger, LogService } = loadMatrixSdk();
|
|
31
|
+
matrixSdkBaseLogger = new ConsoleLogger();
|
|
32
|
+
matrixSdkLoggingConfigured = true;
|
|
33
|
+
|
|
34
|
+
LogService.setLogger({
|
|
35
|
+
trace: (module, ...messageOrObject) => matrixSdkBaseLogger?.trace(module, ...messageOrObject),
|
|
36
|
+
debug: (module, ...messageOrObject) => matrixSdkBaseLogger?.debug(module, ...messageOrObject),
|
|
37
|
+
info: (module, ...messageOrObject) => matrixSdkBaseLogger?.info(module, ...messageOrObject),
|
|
38
|
+
warn: (module, ...messageOrObject) => matrixSdkBaseLogger?.warn(module, ...messageOrObject),
|
|
39
|
+
error: (module, ...messageOrObject) => {
|
|
40
|
+
if (shouldSuppressMatrixHttpNotFound(module, messageOrObject)) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
matrixSdkBaseLogger?.error(module, ...messageOrObject);
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import type { MatrixClient } from "@vector-im/matrix-bot-sdk";
|
|
2
|
+
import { normalizeAccountId } from "openclaw/plugin-sdk/account-id";
|
|
3
|
+
import type { CoreConfig } from "../../types.js";
|
|
4
|
+
import { getMatrixLogService } from "../sdk-runtime.js";
|
|
5
|
+
import { setupKeyBackup } from "./backup.js";
|
|
6
|
+
import { resolveMatrixAuth } from "./config.js";
|
|
7
|
+
import { createMatrixClient } from "./create-client.js";
|
|
8
|
+
import { startMatrixClientWithGrace } from "./startup.js";
|
|
9
|
+
import { DEFAULT_ACCOUNT_KEY } from "./storage.js";
|
|
10
|
+
import type { MatrixAuth } from "./types.js";
|
|
11
|
+
|
|
12
|
+
type SharedMatrixClientState = {
|
|
13
|
+
client: MatrixClient;
|
|
14
|
+
key: string;
|
|
15
|
+
started: boolean;
|
|
16
|
+
cryptoReady: boolean;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// Support multiple accounts with separate clients
|
|
20
|
+
const sharedClientStates = new Map<string, SharedMatrixClientState>();
|
|
21
|
+
const sharedClientPromises = new Map<string, Promise<SharedMatrixClientState>>();
|
|
22
|
+
const sharedClientStartPromises = new Map<string, Promise<void>>();
|
|
23
|
+
|
|
24
|
+
function buildSharedClientKey(auth: MatrixAuth, accountId?: string | null): string {
|
|
25
|
+
const normalizedAccountId = normalizeAccountId(accountId);
|
|
26
|
+
return [
|
|
27
|
+
auth.homeserver,
|
|
28
|
+
auth.userId,
|
|
29
|
+
auth.accessToken,
|
|
30
|
+
auth.encryption ? "e2ee" : "plain",
|
|
31
|
+
normalizedAccountId || DEFAULT_ACCOUNT_KEY,
|
|
32
|
+
].join("|");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function createSharedMatrixClient(params: {
|
|
36
|
+
auth: MatrixAuth;
|
|
37
|
+
timeoutMs?: number;
|
|
38
|
+
accountId?: string | null;
|
|
39
|
+
}): Promise<SharedMatrixClientState> {
|
|
40
|
+
const client = await createMatrixClient({
|
|
41
|
+
homeserver: params.auth.homeserver,
|
|
42
|
+
userId: params.auth.userId,
|
|
43
|
+
accessToken: params.auth.accessToken,
|
|
44
|
+
encryption: params.auth.encryption,
|
|
45
|
+
localTimeoutMs: params.timeoutMs,
|
|
46
|
+
accountId: params.accountId,
|
|
47
|
+
});
|
|
48
|
+
return {
|
|
49
|
+
client,
|
|
50
|
+
key: buildSharedClientKey(params.auth, params.accountId),
|
|
51
|
+
started: false,
|
|
52
|
+
cryptoReady: false,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function ensureSharedClientStarted(params: {
|
|
57
|
+
state: SharedMatrixClientState;
|
|
58
|
+
timeoutMs?: number;
|
|
59
|
+
initialSyncLimit?: number;
|
|
60
|
+
encryption?: boolean;
|
|
61
|
+
}): Promise<void> {
|
|
62
|
+
if (params.state.started) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const key = params.state.key;
|
|
66
|
+
const existingStartPromise = sharedClientStartPromises.get(key);
|
|
67
|
+
if (existingStartPromise) {
|
|
68
|
+
await existingStartPromise;
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const startPromise = (async () => {
|
|
72
|
+
const client = params.state.client;
|
|
73
|
+
|
|
74
|
+
// Initialize crypto if enabled
|
|
75
|
+
if (params.encryption && !params.state.cryptoReady) {
|
|
76
|
+
try {
|
|
77
|
+
const joinedRooms = await client.getJoinedRooms();
|
|
78
|
+
if (client.crypto) {
|
|
79
|
+
await (client.crypto as { prepare: (rooms?: string[]) => Promise<void> }).prepare(
|
|
80
|
+
joinedRooms,
|
|
81
|
+
);
|
|
82
|
+
params.state.cryptoReady = true;
|
|
83
|
+
await setupKeyBackup(client);
|
|
84
|
+
}
|
|
85
|
+
} catch (err) {
|
|
86
|
+
const LogService = getMatrixLogService();
|
|
87
|
+
LogService.warn("MatrixClientLite", "Failed to prepare crypto:", err);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
await startMatrixClientWithGrace({
|
|
92
|
+
client,
|
|
93
|
+
onError: (err: unknown) => {
|
|
94
|
+
params.state.started = false;
|
|
95
|
+
const LogService = getMatrixLogService();
|
|
96
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
97
|
+
// OTK conflict means the server has stale keys for this device.
|
|
98
|
+
// Log clearly so the user knows to re-pair.
|
|
99
|
+
if (message.includes("One time key") && message.includes("already exists")) {
|
|
100
|
+
LogService.error(
|
|
101
|
+
"MatrixClientLite",
|
|
102
|
+
"E2EE key conflict detected — the bot's encryption session is stale.",
|
|
103
|
+
"Run: /bot pair <botname> in BadgerClaw and reconnect with 'openclaw badgerclaw connect <code>'",
|
|
104
|
+
);
|
|
105
|
+
} else {
|
|
106
|
+
LogService.error("MatrixClientLite", "client.start() error:", err);
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
params.state.started = true;
|
|
111
|
+
})();
|
|
112
|
+
sharedClientStartPromises.set(key, startPromise);
|
|
113
|
+
try {
|
|
114
|
+
await startPromise;
|
|
115
|
+
} finally {
|
|
116
|
+
sharedClientStartPromises.delete(key);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export async function resolveSharedMatrixClient(
|
|
121
|
+
params: {
|
|
122
|
+
cfg?: CoreConfig;
|
|
123
|
+
env?: NodeJS.ProcessEnv;
|
|
124
|
+
timeoutMs?: number;
|
|
125
|
+
auth?: MatrixAuth;
|
|
126
|
+
startClient?: boolean;
|
|
127
|
+
accountId?: string | null;
|
|
128
|
+
} = {},
|
|
129
|
+
): Promise<MatrixClient> {
|
|
130
|
+
const accountId = normalizeAccountId(params.accountId);
|
|
131
|
+
const auth =
|
|
132
|
+
params.auth ?? (await resolveMatrixAuth({ cfg: params.cfg, env: params.env, accountId }));
|
|
133
|
+
const key = buildSharedClientKey(auth, accountId);
|
|
134
|
+
const shouldStart = params.startClient !== false;
|
|
135
|
+
|
|
136
|
+
// Check if we already have a client for this key
|
|
137
|
+
const existingState = sharedClientStates.get(key);
|
|
138
|
+
if (existingState) {
|
|
139
|
+
if (shouldStart) {
|
|
140
|
+
await ensureSharedClientStarted({
|
|
141
|
+
state: existingState,
|
|
142
|
+
timeoutMs: params.timeoutMs,
|
|
143
|
+
initialSyncLimit: auth.initialSyncLimit,
|
|
144
|
+
encryption: auth.encryption,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
return existingState.client;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Check if there's a pending creation for this key
|
|
151
|
+
const existingPromise = sharedClientPromises.get(key);
|
|
152
|
+
if (existingPromise) {
|
|
153
|
+
const pending = await existingPromise;
|
|
154
|
+
if (shouldStart) {
|
|
155
|
+
await ensureSharedClientStarted({
|
|
156
|
+
state: pending,
|
|
157
|
+
timeoutMs: params.timeoutMs,
|
|
158
|
+
initialSyncLimit: auth.initialSyncLimit,
|
|
159
|
+
encryption: auth.encryption,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
return pending.client;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Create a new client for this account
|
|
166
|
+
const createPromise = createSharedMatrixClient({
|
|
167
|
+
auth,
|
|
168
|
+
timeoutMs: params.timeoutMs,
|
|
169
|
+
accountId,
|
|
170
|
+
});
|
|
171
|
+
sharedClientPromises.set(key, createPromise);
|
|
172
|
+
try {
|
|
173
|
+
const created = await createPromise;
|
|
174
|
+
sharedClientStates.set(key, created);
|
|
175
|
+
if (shouldStart) {
|
|
176
|
+
await ensureSharedClientStarted({
|
|
177
|
+
state: created,
|
|
178
|
+
timeoutMs: params.timeoutMs,
|
|
179
|
+
initialSyncLimit: auth.initialSyncLimit,
|
|
180
|
+
encryption: auth.encryption,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
return created.client;
|
|
184
|
+
} finally {
|
|
185
|
+
sharedClientPromises.delete(key);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export async function waitForMatrixSync(_params: {
|
|
190
|
+
client: MatrixClient;
|
|
191
|
+
timeoutMs?: number;
|
|
192
|
+
abortSignal?: AbortSignal;
|
|
193
|
+
}): Promise<void> {
|
|
194
|
+
// @vector-im/matrix-bot-sdk handles sync internally in start()
|
|
195
|
+
// This is kept for API compatibility but is essentially a no-op now
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function stopSharedClient(key?: string): void {
|
|
199
|
+
if (key) {
|
|
200
|
+
// Stop a specific client
|
|
201
|
+
const state = sharedClientStates.get(key);
|
|
202
|
+
if (state) {
|
|
203
|
+
state.client.stop();
|
|
204
|
+
sharedClientStates.delete(key);
|
|
205
|
+
}
|
|
206
|
+
} else {
|
|
207
|
+
// Stop all clients (backward compatible behavior)
|
|
208
|
+
for (const state of sharedClientStates.values()) {
|
|
209
|
+
state.client.stop();
|
|
210
|
+
}
|
|
211
|
+
sharedClientStates.clear();
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Stop the shared client for a specific account.
|
|
217
|
+
* Use this instead of stopSharedClient() when shutting down a single account
|
|
218
|
+
* to avoid stopping all accounts.
|
|
219
|
+
*/
|
|
220
|
+
export function stopSharedClientForAccount(auth: MatrixAuth, accountId?: string | null): void {
|
|
221
|
+
const key = buildSharedClientKey(auth, normalizeAccountId(accountId));
|
|
222
|
+
stopSharedClient(key);
|
|
223
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { MatrixClient } from "@vector-im/matrix-bot-sdk";
|
|
2
|
+
|
|
3
|
+
export const MATRIX_CLIENT_STARTUP_GRACE_MS = 2000;
|
|
4
|
+
|
|
5
|
+
export async function startMatrixClientWithGrace(params: {
|
|
6
|
+
client: Pick<MatrixClient, "start">;
|
|
7
|
+
graceMs?: number;
|
|
8
|
+
onError?: (err: unknown) => void;
|
|
9
|
+
}): Promise<void> {
|
|
10
|
+
const graceMs = params.graceMs ?? MATRIX_CLIENT_STARTUP_GRACE_MS;
|
|
11
|
+
let startFailed = false;
|
|
12
|
+
let startError: unknown = undefined;
|
|
13
|
+
let startPromise: Promise<unknown>;
|
|
14
|
+
try {
|
|
15
|
+
startPromise = params.client.start();
|
|
16
|
+
} catch (err) {
|
|
17
|
+
params.onError?.(err);
|
|
18
|
+
throw err;
|
|
19
|
+
}
|
|
20
|
+
void startPromise.catch((err: unknown) => {
|
|
21
|
+
startFailed = true;
|
|
22
|
+
startError = err;
|
|
23
|
+
params.onError?.(err);
|
|
24
|
+
});
|
|
25
|
+
await new Promise((resolve) => setTimeout(resolve, graceMs));
|
|
26
|
+
if (startFailed) {
|
|
27
|
+
throw startError;
|
|
28
|
+
}
|
|
29
|
+
}
|