@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,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,210 @@
|
|
|
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 { resolveMatrixAuth } from "./config.js";
|
|
6
|
+
import { createMatrixClient } from "./create-client.js";
|
|
7
|
+
import { startMatrixClientWithGrace } from "./startup.js";
|
|
8
|
+
import { DEFAULT_ACCOUNT_KEY } from "./storage.js";
|
|
9
|
+
import type { MatrixAuth } from "./types.js";
|
|
10
|
+
|
|
11
|
+
type SharedMatrixClientState = {
|
|
12
|
+
client: MatrixClient;
|
|
13
|
+
key: string;
|
|
14
|
+
started: boolean;
|
|
15
|
+
cryptoReady: boolean;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// Support multiple accounts with separate clients
|
|
19
|
+
const sharedClientStates = new Map<string, SharedMatrixClientState>();
|
|
20
|
+
const sharedClientPromises = new Map<string, Promise<SharedMatrixClientState>>();
|
|
21
|
+
const sharedClientStartPromises = new Map<string, Promise<void>>();
|
|
22
|
+
|
|
23
|
+
function buildSharedClientKey(auth: MatrixAuth, accountId?: string | null): string {
|
|
24
|
+
const normalizedAccountId = normalizeAccountId(accountId);
|
|
25
|
+
return [
|
|
26
|
+
auth.homeserver,
|
|
27
|
+
auth.userId,
|
|
28
|
+
auth.accessToken,
|
|
29
|
+
auth.encryption ? "e2ee" : "plain",
|
|
30
|
+
normalizedAccountId || DEFAULT_ACCOUNT_KEY,
|
|
31
|
+
].join("|");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function createSharedMatrixClient(params: {
|
|
35
|
+
auth: MatrixAuth;
|
|
36
|
+
timeoutMs?: number;
|
|
37
|
+
accountId?: string | null;
|
|
38
|
+
}): Promise<SharedMatrixClientState> {
|
|
39
|
+
const client = await createMatrixClient({
|
|
40
|
+
homeserver: params.auth.homeserver,
|
|
41
|
+
userId: params.auth.userId,
|
|
42
|
+
accessToken: params.auth.accessToken,
|
|
43
|
+
encryption: params.auth.encryption,
|
|
44
|
+
localTimeoutMs: params.timeoutMs,
|
|
45
|
+
accountId: params.accountId,
|
|
46
|
+
});
|
|
47
|
+
return {
|
|
48
|
+
client,
|
|
49
|
+
key: buildSharedClientKey(params.auth, params.accountId),
|
|
50
|
+
started: false,
|
|
51
|
+
cryptoReady: false,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function ensureSharedClientStarted(params: {
|
|
56
|
+
state: SharedMatrixClientState;
|
|
57
|
+
timeoutMs?: number;
|
|
58
|
+
initialSyncLimit?: number;
|
|
59
|
+
encryption?: boolean;
|
|
60
|
+
}): Promise<void> {
|
|
61
|
+
if (params.state.started) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const key = params.state.key;
|
|
65
|
+
const existingStartPromise = sharedClientStartPromises.get(key);
|
|
66
|
+
if (existingStartPromise) {
|
|
67
|
+
await existingStartPromise;
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const startPromise = (async () => {
|
|
71
|
+
const client = params.state.client;
|
|
72
|
+
|
|
73
|
+
// Initialize crypto if enabled
|
|
74
|
+
if (params.encryption && !params.state.cryptoReady) {
|
|
75
|
+
try {
|
|
76
|
+
const joinedRooms = await client.getJoinedRooms();
|
|
77
|
+
if (client.crypto) {
|
|
78
|
+
await (client.crypto as { prepare: (rooms?: string[]) => Promise<void> }).prepare(
|
|
79
|
+
joinedRooms,
|
|
80
|
+
);
|
|
81
|
+
params.state.cryptoReady = true;
|
|
82
|
+
}
|
|
83
|
+
} catch (err) {
|
|
84
|
+
const LogService = getMatrixLogService();
|
|
85
|
+
LogService.warn("MatrixClientLite", "Failed to prepare crypto:", err);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
await startMatrixClientWithGrace({
|
|
90
|
+
client,
|
|
91
|
+
onError: (err: unknown) => {
|
|
92
|
+
params.state.started = false;
|
|
93
|
+
const LogService = getMatrixLogService();
|
|
94
|
+
LogService.error("MatrixClientLite", "client.start() error:", err);
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
params.state.started = true;
|
|
98
|
+
})();
|
|
99
|
+
sharedClientStartPromises.set(key, startPromise);
|
|
100
|
+
try {
|
|
101
|
+
await startPromise;
|
|
102
|
+
} finally {
|
|
103
|
+
sharedClientStartPromises.delete(key);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export async function resolveSharedMatrixClient(
|
|
108
|
+
params: {
|
|
109
|
+
cfg?: CoreConfig;
|
|
110
|
+
env?: NodeJS.ProcessEnv;
|
|
111
|
+
timeoutMs?: number;
|
|
112
|
+
auth?: MatrixAuth;
|
|
113
|
+
startClient?: boolean;
|
|
114
|
+
accountId?: string | null;
|
|
115
|
+
} = {},
|
|
116
|
+
): Promise<MatrixClient> {
|
|
117
|
+
const accountId = normalizeAccountId(params.accountId);
|
|
118
|
+
const auth =
|
|
119
|
+
params.auth ?? (await resolveMatrixAuth({ cfg: params.cfg, env: params.env, accountId }));
|
|
120
|
+
const key = buildSharedClientKey(auth, accountId);
|
|
121
|
+
const shouldStart = params.startClient !== false;
|
|
122
|
+
|
|
123
|
+
// Check if we already have a client for this key
|
|
124
|
+
const existingState = sharedClientStates.get(key);
|
|
125
|
+
if (existingState) {
|
|
126
|
+
if (shouldStart) {
|
|
127
|
+
await ensureSharedClientStarted({
|
|
128
|
+
state: existingState,
|
|
129
|
+
timeoutMs: params.timeoutMs,
|
|
130
|
+
initialSyncLimit: auth.initialSyncLimit,
|
|
131
|
+
encryption: auth.encryption,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
return existingState.client;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Check if there's a pending creation for this key
|
|
138
|
+
const existingPromise = sharedClientPromises.get(key);
|
|
139
|
+
if (existingPromise) {
|
|
140
|
+
const pending = await existingPromise;
|
|
141
|
+
if (shouldStart) {
|
|
142
|
+
await ensureSharedClientStarted({
|
|
143
|
+
state: pending,
|
|
144
|
+
timeoutMs: params.timeoutMs,
|
|
145
|
+
initialSyncLimit: auth.initialSyncLimit,
|
|
146
|
+
encryption: auth.encryption,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
return pending.client;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Create a new client for this account
|
|
153
|
+
const createPromise = createSharedMatrixClient({
|
|
154
|
+
auth,
|
|
155
|
+
timeoutMs: params.timeoutMs,
|
|
156
|
+
accountId,
|
|
157
|
+
});
|
|
158
|
+
sharedClientPromises.set(key, createPromise);
|
|
159
|
+
try {
|
|
160
|
+
const created = await createPromise;
|
|
161
|
+
sharedClientStates.set(key, created);
|
|
162
|
+
if (shouldStart) {
|
|
163
|
+
await ensureSharedClientStarted({
|
|
164
|
+
state: created,
|
|
165
|
+
timeoutMs: params.timeoutMs,
|
|
166
|
+
initialSyncLimit: auth.initialSyncLimit,
|
|
167
|
+
encryption: auth.encryption,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
return created.client;
|
|
171
|
+
} finally {
|
|
172
|
+
sharedClientPromises.delete(key);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export async function waitForMatrixSync(_params: {
|
|
177
|
+
client: MatrixClient;
|
|
178
|
+
timeoutMs?: number;
|
|
179
|
+
abortSignal?: AbortSignal;
|
|
180
|
+
}): Promise<void> {
|
|
181
|
+
// @vector-im/matrix-bot-sdk handles sync internally in start()
|
|
182
|
+
// This is kept for API compatibility but is essentially a no-op now
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function stopSharedClient(key?: string): void {
|
|
186
|
+
if (key) {
|
|
187
|
+
// Stop a specific client
|
|
188
|
+
const state = sharedClientStates.get(key);
|
|
189
|
+
if (state) {
|
|
190
|
+
state.client.stop();
|
|
191
|
+
sharedClientStates.delete(key);
|
|
192
|
+
}
|
|
193
|
+
} else {
|
|
194
|
+
// Stop all clients (backward compatible behavior)
|
|
195
|
+
for (const state of sharedClientStates.values()) {
|
|
196
|
+
state.client.stop();
|
|
197
|
+
}
|
|
198
|
+
sharedClientStates.clear();
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Stop the shared client for a specific account.
|
|
204
|
+
* Use this instead of stopSharedClient() when shutting down a single account
|
|
205
|
+
* to avoid stopping all accounts.
|
|
206
|
+
*/
|
|
207
|
+
export function stopSharedClientForAccount(auth: MatrixAuth, accountId?: string | null): void {
|
|
208
|
+
const key = buildSharedClientKey(auth, normalizeAccountId(accountId));
|
|
209
|
+
stopSharedClient(key);
|
|
210
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { getMatrixRuntime } from "../../runtime.js";
|
|
6
|
+
import type { MatrixStoragePaths } from "./types.js";
|
|
7
|
+
|
|
8
|
+
export const DEFAULT_ACCOUNT_KEY = "default";
|
|
9
|
+
const STORAGE_META_FILENAME = "storage-meta.json";
|
|
10
|
+
|
|
11
|
+
function sanitizePathSegment(value: string): string {
|
|
12
|
+
const cleaned = value
|
|
13
|
+
.trim()
|
|
14
|
+
.toLowerCase()
|
|
15
|
+
.replace(/[^a-z0-9._-]+/g, "_")
|
|
16
|
+
.replace(/^_+|_+$/g, "");
|
|
17
|
+
return cleaned || "unknown";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function resolveHomeserverKey(homeserver: string): string {
|
|
21
|
+
try {
|
|
22
|
+
const url = new URL(homeserver);
|
|
23
|
+
if (url.host) {
|
|
24
|
+
return sanitizePathSegment(url.host);
|
|
25
|
+
}
|
|
26
|
+
} catch {
|
|
27
|
+
// fall through
|
|
28
|
+
}
|
|
29
|
+
return sanitizePathSegment(homeserver);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function hashAccessToken(accessToken: string): string {
|
|
33
|
+
return crypto.createHash("sha256").update(accessToken).digest("hex").slice(0, 16);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function resolveLegacyStoragePaths(env: NodeJS.ProcessEnv = process.env): {
|
|
37
|
+
storagePath: string;
|
|
38
|
+
cryptoPath: string;
|
|
39
|
+
} {
|
|
40
|
+
const stateDir = getMatrixRuntime().state.resolveStateDir(env, os.homedir);
|
|
41
|
+
return {
|
|
42
|
+
storagePath: path.join(stateDir, "badgerclaw", "bot-storage.json"),
|
|
43
|
+
cryptoPath: path.join(stateDir, "badgerclaw", "crypto"),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function resolveMatrixStoragePaths(params: {
|
|
48
|
+
homeserver: string;
|
|
49
|
+
userId: string;
|
|
50
|
+
accessToken: string;
|
|
51
|
+
accountId?: string | null;
|
|
52
|
+
env?: NodeJS.ProcessEnv;
|
|
53
|
+
}): MatrixStoragePaths {
|
|
54
|
+
const env = params.env ?? process.env;
|
|
55
|
+
const stateDir = getMatrixRuntime().state.resolveStateDir(env, os.homedir);
|
|
56
|
+
const accountKey = sanitizePathSegment(params.accountId ?? DEFAULT_ACCOUNT_KEY);
|
|
57
|
+
const userKey = sanitizePathSegment(params.userId);
|
|
58
|
+
const serverKey = resolveHomeserverKey(params.homeserver);
|
|
59
|
+
const tokenHash = hashAccessToken(params.accessToken);
|
|
60
|
+
const rootDir = path.join(
|
|
61
|
+
stateDir,
|
|
62
|
+
"badgerclaw",
|
|
63
|
+
"accounts",
|
|
64
|
+
accountKey,
|
|
65
|
+
`${serverKey}__${userKey}`,
|
|
66
|
+
tokenHash,
|
|
67
|
+
);
|
|
68
|
+
return {
|
|
69
|
+
rootDir,
|
|
70
|
+
storagePath: path.join(rootDir, "bot-storage.json"),
|
|
71
|
+
cryptoPath: path.join(rootDir, "crypto"),
|
|
72
|
+
metaPath: path.join(rootDir, STORAGE_META_FILENAME),
|
|
73
|
+
accountKey,
|
|
74
|
+
tokenHash,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function maybeMigrateLegacyStorage(params: {
|
|
79
|
+
storagePaths: MatrixStoragePaths;
|
|
80
|
+
env?: NodeJS.ProcessEnv;
|
|
81
|
+
}): void {
|
|
82
|
+
const legacy = resolveLegacyStoragePaths(params.env);
|
|
83
|
+
const hasLegacyStorage = fs.existsSync(legacy.storagePath);
|
|
84
|
+
const hasLegacyCrypto = fs.existsSync(legacy.cryptoPath);
|
|
85
|
+
const hasNewStorage =
|
|
86
|
+
fs.existsSync(params.storagePaths.storagePath) || fs.existsSync(params.storagePaths.cryptoPath);
|
|
87
|
+
|
|
88
|
+
if (!hasLegacyStorage && !hasLegacyCrypto) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (hasNewStorage) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
fs.mkdirSync(params.storagePaths.rootDir, { recursive: true });
|
|
96
|
+
if (hasLegacyStorage) {
|
|
97
|
+
try {
|
|
98
|
+
fs.renameSync(legacy.storagePath, params.storagePaths.storagePath);
|
|
99
|
+
} catch {
|
|
100
|
+
// Ignore migration failures; new store will be created.
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (hasLegacyCrypto) {
|
|
104
|
+
try {
|
|
105
|
+
fs.renameSync(legacy.cryptoPath, params.storagePaths.cryptoPath);
|
|
106
|
+
} catch {
|
|
107
|
+
// Ignore migration failures; new store will be created.
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function writeStorageMeta(params: {
|
|
113
|
+
storagePaths: MatrixStoragePaths;
|
|
114
|
+
homeserver: string;
|
|
115
|
+
userId: string;
|
|
116
|
+
accountId?: string | null;
|
|
117
|
+
}): void {
|
|
118
|
+
try {
|
|
119
|
+
const payload = {
|
|
120
|
+
homeserver: params.homeserver,
|
|
121
|
+
userId: params.userId,
|
|
122
|
+
accountId: params.accountId ?? DEFAULT_ACCOUNT_KEY,
|
|
123
|
+
accessTokenHash: params.storagePaths.tokenHash,
|
|
124
|
+
createdAt: new Date().toISOString(),
|
|
125
|
+
};
|
|
126
|
+
fs.mkdirSync(params.storagePaths.rootDir, { recursive: true });
|
|
127
|
+
fs.writeFileSync(params.storagePaths.metaPath, JSON.stringify(payload, null, 2), "utf-8");
|
|
128
|
+
} catch {
|
|
129
|
+
// ignore meta write failures
|
|
130
|
+
}
|
|
131
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export type MatrixResolvedConfig = {
|
|
2
|
+
homeserver: string;
|
|
3
|
+
userId: string;
|
|
4
|
+
accessToken?: string;
|
|
5
|
+
password?: string;
|
|
6
|
+
deviceName?: string;
|
|
7
|
+
initialSyncLimit?: number;
|
|
8
|
+
encryption?: boolean;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Authenticated Matrix configuration.
|
|
13
|
+
* Note: deviceId is NOT included here because it's implicit in the accessToken.
|
|
14
|
+
* The crypto storage assumes the device ID (and thus access token) does not change
|
|
15
|
+
* between restarts. If the access token becomes invalid or crypto storage is lost,
|
|
16
|
+
* both will need to be recreated together.
|
|
17
|
+
*/
|
|
18
|
+
export type MatrixAuth = {
|
|
19
|
+
homeserver: string;
|
|
20
|
+
userId: string;
|
|
21
|
+
accessToken: string;
|
|
22
|
+
deviceName?: string;
|
|
23
|
+
initialSyncLimit?: number;
|
|
24
|
+
encryption?: boolean;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type MatrixStoragePaths = {
|
|
28
|
+
rootDir: string;
|
|
29
|
+
storagePath: string;
|
|
30
|
+
cryptoPath: string;
|
|
31
|
+
metaPath: string;
|
|
32
|
+
accountKey: string;
|
|
33
|
+
tokenHash: string;
|
|
34
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { createMatrixClient } from "./client/create-client.js";
|
|
2
|
+
import { startMatrixClientWithGrace } from "./client/startup.js";
|
|
3
|
+
import { getMatrixLogService } from "./sdk-runtime.js";
|
|
4
|
+
|
|
5
|
+
type MatrixClientBootstrapAuth = {
|
|
6
|
+
homeserver: string;
|
|
7
|
+
userId: string;
|
|
8
|
+
accessToken: string;
|
|
9
|
+
encryption?: boolean;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
type MatrixCryptoPrepare = {
|
|
13
|
+
prepare: (rooms?: string[]) => Promise<void>;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
type MatrixBootstrapClient = Awaited<ReturnType<typeof createMatrixClient>>;
|
|
17
|
+
|
|
18
|
+
export async function createPreparedMatrixClient(opts: {
|
|
19
|
+
auth: MatrixClientBootstrapAuth;
|
|
20
|
+
timeoutMs?: number;
|
|
21
|
+
accountId?: string;
|
|
22
|
+
}): Promise<MatrixBootstrapClient> {
|
|
23
|
+
const client = await createMatrixClient({
|
|
24
|
+
homeserver: opts.auth.homeserver,
|
|
25
|
+
userId: opts.auth.userId,
|
|
26
|
+
accessToken: opts.auth.accessToken,
|
|
27
|
+
encryption: opts.auth.encryption,
|
|
28
|
+
localTimeoutMs: opts.timeoutMs,
|
|
29
|
+
accountId: opts.accountId,
|
|
30
|
+
});
|
|
31
|
+
if (opts.auth.encryption && client.crypto) {
|
|
32
|
+
try {
|
|
33
|
+
const joinedRooms = await client.getJoinedRooms();
|
|
34
|
+
await (client.crypto as MatrixCryptoPrepare).prepare(joinedRooms);
|
|
35
|
+
} catch {
|
|
36
|
+
// Ignore crypto prep failures for one-off requests.
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
await startMatrixClientWithGrace({
|
|
40
|
+
client,
|
|
41
|
+
onError: (err: unknown) => {
|
|
42
|
+
const LogService = getMatrixLogService();
|
|
43
|
+
LogService.error("MatrixClientBootstrap", "client.start() error:", err);
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
return client;
|
|
47
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type { MatrixAuth, MatrixResolvedConfig } from "./client/types.js";
|
|
2
|
+
export { isBunRuntime } from "./client/runtime.js";
|
|
3
|
+
export {
|
|
4
|
+
resolveMatrixConfig,
|
|
5
|
+
resolveMatrixConfigForAccount,
|
|
6
|
+
resolveMatrixAuth,
|
|
7
|
+
} from "./client/config.js";
|
|
8
|
+
export { createMatrixClient } from "./client/create-client.js";
|
|
9
|
+
export {
|
|
10
|
+
resolveSharedMatrixClient,
|
|
11
|
+
waitForMatrixSync,
|
|
12
|
+
stopSharedClient,
|
|
13
|
+
stopSharedClientForAccount,
|
|
14
|
+
} from "./client/shared.js";
|