mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-19 05:50:47 +00:00
Matrix: restore live scripts after secrets cleanup
This commit is contained in:
105
extensions/matrix/scripts/live-basic-send.ts
Normal file
105
extensions/matrix/scripts/live-basic-send.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import { sendMatrixMessage } from "../src/matrix/actions.js";
|
||||
import { createMatrixClient, resolveMatrixAuth } from "../src/matrix/client.js";
|
||||
import { installLiveHarnessRuntime, resolveLiveHarnessConfig } from "./live-common.js";
|
||||
|
||||
async function main() {
|
||||
const base = resolveLiveHarnessConfig();
|
||||
const pluginCfg = installLiveHarnessRuntime(base);
|
||||
|
||||
const auth = await resolveMatrixAuth({ cfg: pluginCfg as never });
|
||||
const client = await createMatrixClient({
|
||||
homeserver: auth.homeserver,
|
||||
userId: auth.userId,
|
||||
accessToken: auth.accessToken,
|
||||
password: auth.password,
|
||||
deviceId: auth.deviceId,
|
||||
encryption: false,
|
||||
accountId: auth.accountId,
|
||||
});
|
||||
|
||||
const targetUserId = process.argv[2]?.trim() || "@user:example.org";
|
||||
const stamp = new Date().toISOString();
|
||||
|
||||
try {
|
||||
const dmRoomCreate = (await client.doRequest(
|
||||
"POST",
|
||||
"/_matrix/client/v3/createRoom",
|
||||
undefined,
|
||||
{
|
||||
is_direct: true,
|
||||
invite: [targetUserId],
|
||||
preset: "trusted_private_chat",
|
||||
name: `OpenClaw DM Test ${stamp}`,
|
||||
topic: "matrix basic DM messaging test",
|
||||
},
|
||||
)) as { room_id?: string };
|
||||
|
||||
const dmRoomId = dmRoomCreate.room_id?.trim() ?? "";
|
||||
if (!dmRoomId) {
|
||||
throw new Error("Failed to create DM room");
|
||||
}
|
||||
|
||||
const currentDirect = ((await client.getAccountData("m.direct").catch(() => ({}))) ??
|
||||
{}) as Record<string, string[]>;
|
||||
const existing = Array.isArray(currentDirect[targetUserId]) ? currentDirect[targetUserId] : [];
|
||||
await client.setAccountData("m.direct", {
|
||||
...currentDirect,
|
||||
[targetUserId]: [dmRoomId, ...existing.filter((id) => id !== dmRoomId)],
|
||||
});
|
||||
|
||||
const dmByUserTarget = await sendMatrixMessage(
|
||||
targetUserId,
|
||||
`Matrix basic DM test (user target) ${stamp}`,
|
||||
{ client },
|
||||
);
|
||||
const dmByRoomTarget = await sendMatrixMessage(
|
||||
dmRoomId,
|
||||
`Matrix basic DM test (room target) ${stamp}`,
|
||||
{ client },
|
||||
);
|
||||
|
||||
const roomCreate = (await client.doRequest("POST", "/_matrix/client/v3/createRoom", undefined, {
|
||||
invite: [targetUserId],
|
||||
preset: "private_chat",
|
||||
name: `OpenClaw Room Test ${stamp}`,
|
||||
topic: "matrix basic room messaging test",
|
||||
})) as { room_id?: string };
|
||||
|
||||
const roomId = roomCreate.room_id?.trim() ?? "";
|
||||
if (!roomId) {
|
||||
throw new Error("Failed to create room chat room");
|
||||
}
|
||||
|
||||
const roomSend = await sendMatrixMessage(roomId, `Matrix basic room test ${stamp}`, {
|
||||
client,
|
||||
});
|
||||
|
||||
process.stdout.write(
|
||||
`${JSON.stringify(
|
||||
{
|
||||
homeserver: base.homeserver,
|
||||
senderUserId: base.userId,
|
||||
targetUserId,
|
||||
dm: {
|
||||
roomId: dmRoomId,
|
||||
userTargetMessageId: dmByUserTarget.messageId,
|
||||
roomTargetMessageId: dmByRoomTarget.messageId,
|
||||
},
|
||||
room: {
|
||||
roomId,
|
||||
messageId: roomSend.messageId,
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
);
|
||||
} finally {
|
||||
client.stop();
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
process.stderr.write(`BASIC_SEND_ERROR: ${err instanceof Error ? err.message : String(err)}\n`);
|
||||
process.exit(1);
|
||||
});
|
||||
145
extensions/matrix/scripts/live-common.ts
Normal file
145
extensions/matrix/scripts/live-common.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { setMatrixRuntime } from "../src/runtime.js";
|
||||
|
||||
type EnvMap = Record<string, string>;
|
||||
|
||||
function loadEnvFile(filePath: string): EnvMap {
|
||||
const out: EnvMap = {};
|
||||
if (!fs.existsSync(filePath)) {
|
||||
return out;
|
||||
}
|
||||
const raw = fs.readFileSync(filePath, "utf8");
|
||||
for (const lineRaw of raw.split(/\r?\n/)) {
|
||||
const line = lineRaw.trim();
|
||||
if (!line || line.startsWith("#")) {
|
||||
continue;
|
||||
}
|
||||
const idx = line.indexOf("=");
|
||||
if (idx <= 0) {
|
||||
continue;
|
||||
}
|
||||
const key = line.slice(0, idx).trim();
|
||||
let value = line.slice(idx + 1).trim();
|
||||
if (
|
||||
(value.startsWith('"') && value.endsWith('"')) ||
|
||||
(value.startsWith("'") && value.endsWith("'"))
|
||||
) {
|
||||
value = value.slice(1, -1);
|
||||
}
|
||||
out[key] = value;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function normalizeHomeserver(raw: string): string {
|
||||
const trimmed = raw.trim();
|
||||
if (!trimmed) {
|
||||
return "";
|
||||
}
|
||||
return /^https?:\/\//i.test(trimmed) ? trimmed : `https://${trimmed}`;
|
||||
}
|
||||
|
||||
function chunkText(text: string, limit: number): string[] {
|
||||
if (!text) {
|
||||
return [];
|
||||
}
|
||||
if (text.length <= limit) {
|
||||
return [text];
|
||||
}
|
||||
const out: string[] = [];
|
||||
for (let i = 0; i < text.length; i += limit) {
|
||||
out.push(text.slice(i, i + limit));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export type LiveHarnessConfig = {
|
||||
homeserver: string;
|
||||
userId: string;
|
||||
password: string;
|
||||
};
|
||||
|
||||
export function resolveLiveHarnessConfig(): LiveHarnessConfig {
|
||||
const envFromFile = loadEnvFile(path.join(os.homedir(), ".openclaw", ".env"));
|
||||
const homeserver = normalizeHomeserver(
|
||||
process.env.MATRIX_HOMESERVER ?? envFromFile.MATRIX_HOMESERVER ?? "",
|
||||
);
|
||||
const userId = process.env.MATRIX_USER_ID ?? envFromFile.MATRIX_USER_ID ?? "";
|
||||
const password = process.env.MATRIX_PASSWORD ?? envFromFile.MATRIX_PASSWORD ?? "";
|
||||
|
||||
if (!homeserver || !userId || !password) {
|
||||
throw new Error("Missing MATRIX_HOMESERVER / MATRIX_USER_ID / MATRIX_PASSWORD");
|
||||
}
|
||||
|
||||
return {
|
||||
homeserver,
|
||||
userId,
|
||||
password,
|
||||
};
|
||||
}
|
||||
|
||||
export function installLiveHarnessRuntime(cfg: LiveHarnessConfig): {
|
||||
channels: {
|
||||
matrix: {
|
||||
homeserver: string;
|
||||
userId: string;
|
||||
password: string;
|
||||
encryption: false;
|
||||
};
|
||||
};
|
||||
} {
|
||||
const pluginCfg = {
|
||||
channels: {
|
||||
matrix: {
|
||||
homeserver: cfg.homeserver,
|
||||
userId: cfg.userId,
|
||||
password: cfg.password,
|
||||
encryption: false as const,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
setMatrixRuntime({
|
||||
config: {
|
||||
loadConfig: () => pluginCfg,
|
||||
},
|
||||
state: {
|
||||
resolveStateDir: () => path.join(os.homedir(), ".openclaw", "matrix-live-harness-state"),
|
||||
},
|
||||
channel: {
|
||||
text: {
|
||||
resolveMarkdownTableMode: () => "off",
|
||||
convertMarkdownTables: (text: string) => text,
|
||||
resolveTextChunkLimit: () => 4000,
|
||||
resolveChunkMode: () => "off",
|
||||
chunkMarkdownTextWithMode: (text: string, limit: number) => chunkText(text, limit),
|
||||
},
|
||||
},
|
||||
media: {
|
||||
mediaKindFromMime: (mime: string) => {
|
||||
const value = (mime || "").toLowerCase();
|
||||
if (value.startsWith("image/")) {
|
||||
return "image";
|
||||
}
|
||||
if (value.startsWith("audio/")) {
|
||||
return "audio";
|
||||
}
|
||||
if (value.startsWith("video/")) {
|
||||
return "video";
|
||||
}
|
||||
return "document";
|
||||
},
|
||||
isVoiceCompatibleAudio: () => false,
|
||||
loadWebMedia: async () => ({
|
||||
buffer: Buffer.from("matrix harness media payload\n", "utf8"),
|
||||
contentType: "text/plain",
|
||||
fileName: "matrix-harness.txt",
|
||||
kind: "document" as const,
|
||||
}),
|
||||
},
|
||||
} as never);
|
||||
|
||||
return pluginCfg;
|
||||
}
|
||||
127
extensions/matrix/scripts/live-cross-signing-probe.ts
Normal file
127
extensions/matrix/scripts/live-cross-signing-probe.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
import { createMatrixClient, resolveMatrixAuth } from "../src/matrix/client.js";
|
||||
import { installLiveHarnessRuntime, resolveLiveHarnessConfig } from "./live-common.js";
|
||||
|
||||
type MatrixCryptoProbe = {
|
||||
isCrossSigningReady?: () => Promise<boolean>;
|
||||
userHasCrossSigningKeys?: (userId?: string, downloadUncached?: boolean) => Promise<boolean>;
|
||||
bootstrapCrossSigning?: (opts: {
|
||||
setupNewCrossSigning?: boolean;
|
||||
authUploadDeviceSigningKeys?: <T>(
|
||||
makeRequest: (authData: Record<string, unknown> | null) => Promise<T>,
|
||||
) => Promise<T>;
|
||||
}) => Promise<void>;
|
||||
};
|
||||
|
||||
async function main() {
|
||||
const base = resolveLiveHarnessConfig();
|
||||
const cfg = installLiveHarnessRuntime(base);
|
||||
(cfg.channels["matrix"] as { encryption: boolean }).encryption = true;
|
||||
|
||||
const auth = await resolveMatrixAuth({ cfg: cfg as never });
|
||||
const client = await createMatrixClient({
|
||||
homeserver: auth.homeserver,
|
||||
userId: auth.userId,
|
||||
accessToken: auth.accessToken,
|
||||
password: auth.password,
|
||||
deviceId: auth.deviceId,
|
||||
encryption: true,
|
||||
accountId: auth.accountId,
|
||||
});
|
||||
const initCrypto = (client as unknown as { initializeCryptoIfNeeded?: () => Promise<void> })
|
||||
.initializeCryptoIfNeeded;
|
||||
if (typeof initCrypto === "function") {
|
||||
await initCrypto.call(client);
|
||||
}
|
||||
|
||||
const inner = (client as unknown as { client?: { getCrypto?: () => unknown } }).client;
|
||||
const crypto = (inner?.getCrypto?.() ?? null) as MatrixCryptoProbe | null;
|
||||
const userId = auth.userId;
|
||||
const password = auth.password;
|
||||
|
||||
const out: Record<string, unknown> = {
|
||||
userId,
|
||||
hasCrypto: Boolean(crypto),
|
||||
readyBefore: null,
|
||||
hasKeysBefore: null,
|
||||
bootstrap: "skipped",
|
||||
readyAfter: null,
|
||||
hasKeysAfter: null,
|
||||
queryHasMaster: null,
|
||||
queryHasSelfSigning: null,
|
||||
queryHasUserSigning: null,
|
||||
};
|
||||
|
||||
if (!crypto || !crypto.bootstrapCrossSigning) {
|
||||
process.stdout.write(`${JSON.stringify(out, null, 2)}\n`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof crypto.isCrossSigningReady === "function") {
|
||||
out.readyBefore = await crypto.isCrossSigningReady().catch((err) => `error:${String(err)}`);
|
||||
}
|
||||
if (typeof crypto.userHasCrossSigningKeys === "function") {
|
||||
out.hasKeysBefore = await crypto
|
||||
.userHasCrossSigningKeys(userId, true)
|
||||
.catch((err) => `error:${String(err)}`);
|
||||
}
|
||||
|
||||
const authUploadDeviceSigningKeys = async <T>(
|
||||
makeRequest: (authData: Record<string, unknown> | null) => Promise<T>,
|
||||
): Promise<T> => {
|
||||
try {
|
||||
return await makeRequest(null);
|
||||
} catch {
|
||||
try {
|
||||
return await makeRequest({ type: "m.login.dummy" });
|
||||
} catch {
|
||||
if (!password?.trim()) {
|
||||
throw new Error("Missing password for m.login.password fallback");
|
||||
}
|
||||
return await makeRequest({
|
||||
type: "m.login.password",
|
||||
identifier: { type: "m.id.user", user: userId },
|
||||
password,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
await crypto.bootstrapCrossSigning({ authUploadDeviceSigningKeys });
|
||||
out.bootstrap = "ok";
|
||||
} catch (err) {
|
||||
out.bootstrap = "error";
|
||||
out.bootstrapError = err instanceof Error ? err.message : String(err);
|
||||
}
|
||||
|
||||
if (typeof crypto.isCrossSigningReady === "function") {
|
||||
out.readyAfter = await crypto.isCrossSigningReady().catch((err) => `error:${String(err)}`);
|
||||
}
|
||||
if (typeof crypto.userHasCrossSigningKeys === "function") {
|
||||
out.hasKeysAfter = await crypto
|
||||
.userHasCrossSigningKeys(userId, true)
|
||||
.catch((err) => `error:${String(err)}`);
|
||||
}
|
||||
|
||||
const query = (await client.doRequest("POST", "/_matrix/client/v3/keys/query", undefined, {
|
||||
device_keys: { [userId]: [] },
|
||||
})) as {
|
||||
master_keys?: Record<string, unknown>;
|
||||
self_signing_keys?: Record<string, unknown>;
|
||||
user_signing_keys?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
out.queryHasMaster = Boolean(query.master_keys?.[userId]);
|
||||
out.queryHasSelfSigning = Boolean(query.self_signing_keys?.[userId]);
|
||||
out.queryHasUserSigning = Boolean(query.user_signing_keys?.[userId]);
|
||||
|
||||
process.stdout.write(`${JSON.stringify(out, null, 2)}\n`);
|
||||
client.stop();
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
process.stderr.write(
|
||||
`CROSS_SIGNING_PROBE_ERROR: ${err instanceof Error ? err.message : String(err)}\n`,
|
||||
);
|
||||
process.exit(1);
|
||||
});
|
||||
28
extensions/matrix/scripts/live-e2ee-bootstrap.ts
Normal file
28
extensions/matrix/scripts/live-e2ee-bootstrap.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { bootstrapMatrixVerification } from "../src/matrix/actions/verification.js";
|
||||
import { installLiveHarnessRuntime, resolveLiveHarnessConfig } from "./live-common.js";
|
||||
|
||||
async function main() {
|
||||
const recoveryKeyArg = process.argv[2];
|
||||
const forceResetCrossSigning = process.argv.includes("--force-reset-cross-signing");
|
||||
|
||||
const base = resolveLiveHarnessConfig();
|
||||
const pluginCfg = installLiveHarnessRuntime(base);
|
||||
(pluginCfg.channels["matrix"] as { encryption: boolean }).encryption = true;
|
||||
|
||||
const result = await bootstrapMatrixVerification({
|
||||
recoveryKey: recoveryKeyArg?.trim() || undefined,
|
||||
forceResetCrossSigning,
|
||||
});
|
||||
|
||||
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
||||
if (!result.success) {
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
process.stderr.write(
|
||||
`E2EE_BOOTSTRAP_ERROR: ${err instanceof Error ? err.message : String(err)}\n`,
|
||||
);
|
||||
process.exit(1);
|
||||
});
|
||||
66
extensions/matrix/scripts/live-e2ee-room-state.ts
Normal file
66
extensions/matrix/scripts/live-e2ee-room-state.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { createMatrixClient, resolveMatrixAuth } from "../src/matrix/client.js";
|
||||
import { installLiveHarnessRuntime, resolveLiveHarnessConfig } from "./live-common.js";
|
||||
|
||||
async function main() {
|
||||
const roomId = process.argv[2]?.trim();
|
||||
const eventId = process.argv[3]?.trim();
|
||||
|
||||
if (!roomId) {
|
||||
throw new Error(
|
||||
"Usage: node --import tsx extensions/matrix/scripts/live-e2ee-room-state.ts <roomId> [eventId]",
|
||||
);
|
||||
}
|
||||
|
||||
const base = resolveLiveHarnessConfig();
|
||||
const pluginCfg = installLiveHarnessRuntime(base);
|
||||
(pluginCfg.channels["matrix"] as { encryption: boolean }).encryption = true;
|
||||
|
||||
const auth = await resolveMatrixAuth({ cfg: pluginCfg as never });
|
||||
const client = await createMatrixClient({
|
||||
homeserver: auth.homeserver,
|
||||
userId: auth.userId,
|
||||
accessToken: auth.accessToken,
|
||||
password: auth.password,
|
||||
deviceId: auth.deviceId,
|
||||
encryption: false,
|
||||
accountId: auth.accountId,
|
||||
});
|
||||
|
||||
try {
|
||||
const encryptionState = (await client.doRequest(
|
||||
"GET",
|
||||
`/_matrix/client/v3/rooms/${encodeURIComponent(roomId)}/state/m.room.encryption/`,
|
||||
)) as { algorithm?: string; rotation_period_ms?: number; rotation_period_msgs?: number };
|
||||
|
||||
let eventType: string | null = null;
|
||||
if (eventId) {
|
||||
const event = (await client.doRequest(
|
||||
"GET",
|
||||
`/_matrix/client/v3/rooms/${encodeURIComponent(roomId)}/event/${encodeURIComponent(eventId)}`,
|
||||
)) as { type?: string };
|
||||
eventType = event.type ?? null;
|
||||
}
|
||||
|
||||
process.stdout.write(
|
||||
`${JSON.stringify(
|
||||
{
|
||||
roomId,
|
||||
encryptionState,
|
||||
eventId: eventId ?? null,
|
||||
eventType,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
);
|
||||
} finally {
|
||||
client.stop();
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
process.stderr.write(
|
||||
`E2EE_ROOM_STATE_ERROR: ${err instanceof Error ? err.message : String(err)}\n`,
|
||||
);
|
||||
process.exit(1);
|
||||
});
|
||||
100
extensions/matrix/scripts/live-e2ee-send-room.ts
Normal file
100
extensions/matrix/scripts/live-e2ee-send-room.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import { sendMatrixMessage } from "../src/matrix/actions.js";
|
||||
import { createMatrixClient, resolveMatrixAuth } from "../src/matrix/client.js";
|
||||
import { installLiveHarnessRuntime, resolveLiveHarnessConfig } from "./live-common.js";
|
||||
|
||||
async function delay(ms: number): Promise<void> {
|
||||
await new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const roomId = process.argv[2]?.trim();
|
||||
const useFullBootstrap = process.argv.includes("--full-bootstrap");
|
||||
const startupTimeoutMs = 45_000;
|
||||
const settleMsRaw = Number.parseInt(process.argv[3] ?? "4000", 10);
|
||||
const settleMs = Number.isFinite(settleMsRaw) && settleMsRaw >= 0 ? settleMsRaw : 4000;
|
||||
|
||||
if (!roomId) {
|
||||
throw new Error(
|
||||
"Usage: node --import tsx extensions/matrix/scripts/live-e2ee-send-room.ts <roomId> [settleMs] [--full-bootstrap]",
|
||||
);
|
||||
}
|
||||
|
||||
const base = resolveLiveHarnessConfig();
|
||||
const pluginCfg = installLiveHarnessRuntime(base);
|
||||
(pluginCfg.channels["matrix"] as { encryption: boolean }).encryption = true;
|
||||
|
||||
const auth = await resolveMatrixAuth({ cfg: pluginCfg as never });
|
||||
const client = await createMatrixClient({
|
||||
homeserver: auth.homeserver,
|
||||
userId: auth.userId,
|
||||
accessToken: auth.accessToken,
|
||||
password: auth.password,
|
||||
deviceId: auth.deviceId,
|
||||
encryption: true,
|
||||
accountId: auth.accountId,
|
||||
});
|
||||
|
||||
const stamp = new Date().toISOString();
|
||||
|
||||
try {
|
||||
if (!useFullBootstrap) {
|
||||
const bootstrapper = (
|
||||
client as unknown as { cryptoBootstrapper?: { bootstrap?: () => Promise<void> } }
|
||||
).cryptoBootstrapper;
|
||||
if (bootstrapper?.bootstrap) {
|
||||
bootstrapper.bootstrap = async () => {};
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.race([
|
||||
client.start(),
|
||||
new Promise<never>((_, reject) => {
|
||||
setTimeout(() => {
|
||||
reject(
|
||||
new Error(
|
||||
`Matrix client start timed out after ${startupTimeoutMs}ms (fullBootstrap=${useFullBootstrap})`,
|
||||
),
|
||||
);
|
||||
}, startupTimeoutMs);
|
||||
}),
|
||||
]);
|
||||
|
||||
if (settleMs > 0) {
|
||||
await delay(settleMs);
|
||||
}
|
||||
|
||||
const sent = await sendMatrixMessage(
|
||||
roomId,
|
||||
`Matrix E2EE existing-room test ${stamp} (settleMs=${settleMs})`,
|
||||
{ client },
|
||||
);
|
||||
|
||||
const event = (await client.doRequest(
|
||||
"GET",
|
||||
`/_matrix/client/v3/rooms/${encodeURIComponent(roomId)}/event/${encodeURIComponent(sent.messageId)}`,
|
||||
)) as { type?: string };
|
||||
|
||||
process.stdout.write(
|
||||
`${JSON.stringify(
|
||||
{
|
||||
roomId,
|
||||
messageId: sent.messageId,
|
||||
storedEventType: event.type ?? null,
|
||||
fullBootstrap: useFullBootstrap,
|
||||
settleMs,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
);
|
||||
} finally {
|
||||
client.stop();
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
process.stderr.write(
|
||||
`E2EE_SEND_ROOM_ERROR: ${err instanceof Error ? err.message : String(err)}\n`,
|
||||
);
|
||||
process.exit(1);
|
||||
});
|
||||
170
extensions/matrix/scripts/live-e2ee-send.ts
Normal file
170
extensions/matrix/scripts/live-e2ee-send.ts
Normal file
@@ -0,0 +1,170 @@
|
||||
import { sendMatrixMessage } from "../src/matrix/actions.js";
|
||||
import { createMatrixClient, resolveMatrixAuth } from "../src/matrix/client.js";
|
||||
import { installLiveHarnessRuntime, resolveLiveHarnessConfig } from "./live-common.js";
|
||||
|
||||
const MEGOLM_ALG = "m.megolm.v1.aes-sha2";
|
||||
|
||||
type MatrixEventLike = {
|
||||
type?: string;
|
||||
};
|
||||
|
||||
async function main() {
|
||||
const targetUserId = process.argv[2]?.trim() || "@user:example.org";
|
||||
const useFullBootstrap = process.argv.includes("--full-bootstrap");
|
||||
const startupTimeoutMs = 45_000;
|
||||
const base = resolveLiveHarnessConfig();
|
||||
const pluginCfg = installLiveHarnessRuntime(base);
|
||||
|
||||
// Enable encryption for this run only.
|
||||
(pluginCfg.channels["matrix"] as { encryption: boolean }).encryption = true;
|
||||
|
||||
const auth = await resolveMatrixAuth({ cfg: pluginCfg as never });
|
||||
const client = await createMatrixClient({
|
||||
homeserver: auth.homeserver,
|
||||
userId: auth.userId,
|
||||
accessToken: auth.accessToken,
|
||||
password: auth.password,
|
||||
deviceId: auth.deviceId,
|
||||
encryption: true,
|
||||
accountId: auth.accountId,
|
||||
});
|
||||
|
||||
const stamp = new Date().toISOString();
|
||||
|
||||
try {
|
||||
if (!useFullBootstrap) {
|
||||
const bootstrapper = (
|
||||
client as unknown as { cryptoBootstrapper?: { bootstrap?: () => Promise<void> } }
|
||||
).cryptoBootstrapper;
|
||||
if (bootstrapper?.bootstrap) {
|
||||
bootstrapper.bootstrap = async () => {};
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.race([
|
||||
client.start(),
|
||||
new Promise<never>((_, reject) => {
|
||||
setTimeout(() => {
|
||||
reject(
|
||||
new Error(
|
||||
`Matrix client start timed out after ${startupTimeoutMs}ms (fullBootstrap=${useFullBootstrap})`,
|
||||
),
|
||||
);
|
||||
}, startupTimeoutMs);
|
||||
}),
|
||||
]);
|
||||
|
||||
const dmRoomCreate = (await client.doRequest(
|
||||
"POST",
|
||||
"/_matrix/client/v3/createRoom",
|
||||
undefined,
|
||||
{
|
||||
is_direct: true,
|
||||
invite: [targetUserId],
|
||||
preset: "trusted_private_chat",
|
||||
name: `OpenClaw E2EE DM ${stamp}`,
|
||||
topic: "matrix E2EE DM test",
|
||||
initial_state: [
|
||||
{
|
||||
type: "m.room.encryption",
|
||||
state_key: "",
|
||||
content: {
|
||||
algorithm: MEGOLM_ALG,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
)) as { room_id?: string };
|
||||
|
||||
const dmRoomId = dmRoomCreate.room_id?.trim() ?? "";
|
||||
if (!dmRoomId) {
|
||||
throw new Error("Failed to create encrypted DM room");
|
||||
}
|
||||
|
||||
const currentDirect = ((await client.getAccountData("m.direct").catch(() => ({}))) ??
|
||||
{}) as Record<string, string[]>;
|
||||
const existing = Array.isArray(currentDirect[targetUserId]) ? currentDirect[targetUserId] : [];
|
||||
await client.setAccountData("m.direct", {
|
||||
...currentDirect,
|
||||
[targetUserId]: [dmRoomId, ...existing.filter((id) => id !== dmRoomId)],
|
||||
});
|
||||
|
||||
const dmSend = await sendMatrixMessage(
|
||||
dmRoomId,
|
||||
`Matrix E2EE DM test ${stamp}\nPlease reply here so I can validate decrypt/read.`,
|
||||
{
|
||||
client,
|
||||
},
|
||||
);
|
||||
|
||||
const roomCreate = (await client.doRequest("POST", "/_matrix/client/v3/createRoom", undefined, {
|
||||
invite: [targetUserId],
|
||||
preset: "private_chat",
|
||||
name: `OpenClaw E2EE Room ${stamp}`,
|
||||
topic: "matrix E2EE room test",
|
||||
initial_state: [
|
||||
{
|
||||
type: "m.room.encryption",
|
||||
state_key: "",
|
||||
content: {
|
||||
algorithm: MEGOLM_ALG,
|
||||
},
|
||||
},
|
||||
],
|
||||
})) as { room_id?: string };
|
||||
|
||||
const roomId = roomCreate.room_id?.trim() ?? "";
|
||||
if (!roomId) {
|
||||
throw new Error("Failed to create encrypted room chat");
|
||||
}
|
||||
|
||||
const roomSend = await sendMatrixMessage(
|
||||
roomId,
|
||||
`Matrix E2EE room test ${stamp}\nPlease reply here too.`,
|
||||
{
|
||||
client,
|
||||
},
|
||||
);
|
||||
|
||||
const dmRaw = (await client.doRequest(
|
||||
"GET",
|
||||
`/_matrix/client/v3/rooms/${encodeURIComponent(dmRoomId)}/event/${encodeURIComponent(dmSend.messageId)}`,
|
||||
)) as MatrixEventLike;
|
||||
|
||||
const roomRaw = (await client.doRequest(
|
||||
"GET",
|
||||
`/_matrix/client/v3/rooms/${encodeURIComponent(roomId)}/event/${encodeURIComponent(roomSend.messageId)}`,
|
||||
)) as MatrixEventLike;
|
||||
|
||||
process.stdout.write(
|
||||
`${JSON.stringify(
|
||||
{
|
||||
homeserver: base.homeserver,
|
||||
senderUserId: base.userId,
|
||||
targetUserId,
|
||||
encryptionAlgorithm: MEGOLM_ALG,
|
||||
fullBootstrap: useFullBootstrap,
|
||||
dm: {
|
||||
roomId: dmRoomId,
|
||||
messageId: dmSend.messageId,
|
||||
storedEventType: dmRaw.type ?? null,
|
||||
},
|
||||
room: {
|
||||
roomId,
|
||||
messageId: roomSend.messageId,
|
||||
storedEventType: roomRaw.type ?? null,
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
);
|
||||
} finally {
|
||||
client.stop();
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
process.stderr.write(`E2EE_SEND_ERROR: ${err instanceof Error ? err.message : String(err)}\n`);
|
||||
process.exit(1);
|
||||
});
|
||||
57
extensions/matrix/scripts/live-e2ee-status.ts
Normal file
57
extensions/matrix/scripts/live-e2ee-status.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import {
|
||||
getMatrixEncryptionStatus,
|
||||
getMatrixVerificationStatus,
|
||||
verifyMatrixRecoveryKey,
|
||||
} from "../src/matrix/actions.js";
|
||||
import { installLiveHarnessRuntime, resolveLiveHarnessConfig } from "./live-common.js";
|
||||
|
||||
async function main() {
|
||||
const includeRecoveryKey = process.argv.includes("--include-recovery-key");
|
||||
const verifyStoredRecoveryKey = process.argv.includes("--verify-stored-recovery-key");
|
||||
|
||||
const base = resolveLiveHarnessConfig();
|
||||
const pluginCfg = installLiveHarnessRuntime(base);
|
||||
(pluginCfg.channels["matrix"] as { encryption: boolean }).encryption = true;
|
||||
|
||||
const verification = await getMatrixVerificationStatus({
|
||||
includeRecoveryKey,
|
||||
});
|
||||
const encryption = await getMatrixEncryptionStatus({
|
||||
includeRecoveryKey,
|
||||
});
|
||||
|
||||
let recoveryVerificationResult: unknown = null;
|
||||
if (verifyStoredRecoveryKey) {
|
||||
const key =
|
||||
verification && typeof verification === "object" && "recoveryKey" in verification
|
||||
? (verification as { recoveryKey?: string | null }).recoveryKey
|
||||
: null;
|
||||
if (key?.trim()) {
|
||||
recoveryVerificationResult = await verifyMatrixRecoveryKey(key);
|
||||
} else {
|
||||
recoveryVerificationResult = {
|
||||
success: false,
|
||||
error: "No stored recovery key returned (use --include-recovery-key)",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
process.stdout.write(
|
||||
`${JSON.stringify(
|
||||
{
|
||||
homeserver: base.homeserver,
|
||||
userId: base.userId,
|
||||
verification,
|
||||
encryption,
|
||||
recoveryVerificationResult,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
);
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
process.stderr.write(`E2EE_STATUS_ERROR: ${err instanceof Error ? err.message : String(err)}\n`);
|
||||
process.exit(1);
|
||||
});
|
||||
123
extensions/matrix/scripts/live-e2ee-wait-reply.ts
Normal file
123
extensions/matrix/scripts/live-e2ee-wait-reply.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
import { createMatrixClient, resolveMatrixAuth } from "../src/matrix/client.js";
|
||||
import { installLiveHarnessRuntime, resolveLiveHarnessConfig } from "./live-common.js";
|
||||
|
||||
type MatrixRawEvent = {
|
||||
event_id?: string;
|
||||
type?: string;
|
||||
sender?: string;
|
||||
room_id?: string;
|
||||
origin_server_ts?: number;
|
||||
content?: {
|
||||
body?: string;
|
||||
msgtype?: string;
|
||||
};
|
||||
};
|
||||
|
||||
async function main() {
|
||||
const roomId = process.argv[2]?.trim();
|
||||
const targetUserId = process.argv[3]?.trim() || "@user:example.org";
|
||||
const timeoutSecRaw = Number.parseInt(process.argv[4] ?? "120", 10);
|
||||
const timeoutMs =
|
||||
(Number.isFinite(timeoutSecRaw) && timeoutSecRaw > 0 ? timeoutSecRaw : 120) * 1000;
|
||||
const useFullBootstrap = process.argv.includes("--full-bootstrap");
|
||||
const startupTimeoutMs = 45_000;
|
||||
|
||||
if (!roomId) {
|
||||
throw new Error(
|
||||
"Usage: node --import tsx extensions/matrix/scripts/live-e2ee-wait-reply.ts <roomId> [targetUserId] [timeoutSec] [--full-bootstrap]",
|
||||
);
|
||||
}
|
||||
|
||||
const base = resolveLiveHarnessConfig();
|
||||
const pluginCfg = installLiveHarnessRuntime(base);
|
||||
(pluginCfg.channels["matrix"] as { encryption: boolean }).encryption = true;
|
||||
|
||||
const auth = await resolveMatrixAuth({ cfg: pluginCfg as never });
|
||||
const client = await createMatrixClient({
|
||||
homeserver: auth.homeserver,
|
||||
userId: auth.userId,
|
||||
accessToken: auth.accessToken,
|
||||
password: auth.password,
|
||||
deviceId: auth.deviceId,
|
||||
encryption: true,
|
||||
accountId: auth.accountId,
|
||||
});
|
||||
|
||||
try {
|
||||
if (!useFullBootstrap) {
|
||||
const bootstrapper = (
|
||||
client as unknown as { cryptoBootstrapper?: { bootstrap?: () => Promise<void> } }
|
||||
).cryptoBootstrapper;
|
||||
if (bootstrapper?.bootstrap) {
|
||||
bootstrapper.bootstrap = async () => {};
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.race([
|
||||
client.start(),
|
||||
new Promise<never>((_, reject) => {
|
||||
setTimeout(() => {
|
||||
reject(
|
||||
new Error(
|
||||
`Matrix client start timed out after ${startupTimeoutMs}ms (fullBootstrap=${useFullBootstrap})`,
|
||||
),
|
||||
);
|
||||
}, startupTimeoutMs);
|
||||
}),
|
||||
]);
|
||||
|
||||
const found = await new Promise<MatrixRawEvent | null>((resolve) => {
|
||||
const timer = setTimeout(() => {
|
||||
resolve(null);
|
||||
}, timeoutMs);
|
||||
|
||||
client.on("room.message", (eventRoomId, event) => {
|
||||
const rid = String(eventRoomId || "");
|
||||
const raw = event as MatrixRawEvent;
|
||||
if (rid !== roomId) {
|
||||
return;
|
||||
}
|
||||
if ((raw.sender ?? "").trim() !== targetUserId) {
|
||||
return;
|
||||
}
|
||||
if ((raw.type ?? "").trim() !== "m.room.message") {
|
||||
return;
|
||||
}
|
||||
clearTimeout(timer);
|
||||
resolve(raw);
|
||||
});
|
||||
});
|
||||
|
||||
process.stdout.write(
|
||||
`${JSON.stringify(
|
||||
{
|
||||
roomId,
|
||||
targetUserId,
|
||||
timeoutMs,
|
||||
found: Boolean(found),
|
||||
message: found
|
||||
? {
|
||||
eventId: found.event_id ?? null,
|
||||
type: found.type ?? null,
|
||||
sender: found.sender ?? null,
|
||||
timestamp: found.origin_server_ts ?? null,
|
||||
text: found.content?.body ?? null,
|
||||
msgtype: found.content?.msgtype ?? null,
|
||||
}
|
||||
: null,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
);
|
||||
} finally {
|
||||
client.stop();
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
process.stderr.write(
|
||||
`E2EE_WAIT_REPLY_ERROR: ${err instanceof Error ? err.message : String(err)}\n`,
|
||||
);
|
||||
process.exit(1);
|
||||
});
|
||||
57
extensions/matrix/scripts/live-read-room.ts
Normal file
57
extensions/matrix/scripts/live-read-room.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { readMatrixMessages } from "../src/matrix/actions.js";
|
||||
import { createMatrixClient, resolveMatrixAuth } from "../src/matrix/client.js";
|
||||
import { installLiveHarnessRuntime, resolveLiveHarnessConfig } from "./live-common.js";
|
||||
|
||||
async function main() {
|
||||
const roomId = process.argv[2]?.trim();
|
||||
if (!roomId) {
|
||||
throw new Error("Usage: bun extensions/matrix/scripts/live-read-room.ts <roomId> [limit]");
|
||||
}
|
||||
|
||||
const requestedLimit = Number.parseInt(process.argv[3] ?? "30", 10);
|
||||
const limit = Number.isFinite(requestedLimit) && requestedLimit > 0 ? requestedLimit : 30;
|
||||
|
||||
const base = resolveLiveHarnessConfig();
|
||||
const pluginCfg = installLiveHarnessRuntime(base);
|
||||
const auth = await resolveMatrixAuth({ cfg: pluginCfg as never });
|
||||
const client = await createMatrixClient({
|
||||
homeserver: auth.homeserver,
|
||||
userId: auth.userId,
|
||||
accessToken: auth.accessToken,
|
||||
password: auth.password,
|
||||
deviceId: auth.deviceId,
|
||||
encryption: false,
|
||||
accountId: auth.accountId,
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await readMatrixMessages(roomId, { client, limit });
|
||||
const compact = result.messages.map((msg) => ({
|
||||
id: msg.eventId,
|
||||
sender: msg.sender,
|
||||
ts: msg.timestamp,
|
||||
text: msg.body ?? "",
|
||||
}));
|
||||
|
||||
process.stdout.write(
|
||||
`${JSON.stringify(
|
||||
{
|
||||
roomId,
|
||||
count: compact.length,
|
||||
messages: compact,
|
||||
nextBatch: result.nextBatch ?? null,
|
||||
prevBatch: result.prevBatch ?? null,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
);
|
||||
} finally {
|
||||
client.stop();
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
process.stderr.write(`READ_ROOM_ERROR: ${err instanceof Error ? err.message : String(err)}\n`);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user