Matrix: share started action client helper

This commit is contained in:
Gustavo Madeira Santana
2026-03-09 05:20:08 -04:00
parent d6b12a3835
commit 57787eec97
5 changed files with 131 additions and 191 deletions

View File

@@ -58,3 +58,10 @@ export async function withResolvedActionClient<T>(
await stopActionClient(resolved, mode); await stopActionClient(resolved, mode);
} }
} }
export async function withStartedActionClient<T>(
opts: MatrixActionClientOpts,
run: (client: MatrixActionClient["client"]) => Promise<T>,
): Promise<T> {
return await withResolvedActionClient({ ...opts, readiness: "started" }, run, "persist");
}

View File

@@ -1,9 +1,9 @@
import { beforeEach, describe, expect, it, vi } from "vitest"; import { beforeEach, describe, expect, it, vi } from "vitest";
const withResolvedActionClientMock = vi.fn(); const withStartedActionClientMock = vi.fn();
vi.mock("./client.js", () => ({ vi.mock("./client.js", () => ({
withResolvedActionClient: (...args: unknown[]) => withResolvedActionClientMock(...args), withStartedActionClient: (...args: unknown[]) => withStartedActionClientMock(...args),
})); }));
let listMatrixOwnDevices: typeof import("./devices.js").listMatrixOwnDevices; let listMatrixOwnDevices: typeof import("./devices.js").listMatrixOwnDevices;
@@ -17,7 +17,7 @@ describe("matrix device actions", () => {
}); });
it("lists own devices on a started client", async () => { it("lists own devices on a started client", async () => {
withResolvedActionClientMock.mockImplementation(async (_opts, run) => { withStartedActionClientMock.mockImplementation(async (_opts, run) => {
return await run({ return await run({
listOwnDevices: vi.fn(async () => [ listOwnDevices: vi.fn(async () => [
{ {
@@ -33,10 +33,9 @@ describe("matrix device actions", () => {
const result = await listMatrixOwnDevices({ accountId: "poe" }); const result = await listMatrixOwnDevices({ accountId: "poe" });
expect(withResolvedActionClientMock).toHaveBeenCalledWith( expect(withStartedActionClientMock).toHaveBeenCalledWith(
{ accountId: "poe", readiness: "started" }, { accountId: "poe" },
expect.any(Function), expect.any(Function),
"persist",
); );
expect(result).toEqual([ expect(result).toEqual([
expect.objectContaining({ expect.objectContaining({
@@ -60,7 +59,7 @@ describe("matrix device actions", () => {
}, },
], ],
})); }));
withResolvedActionClientMock.mockImplementation(async (_opts, run) => { withStartedActionClientMock.mockImplementation(async (_opts, run) => {
return await run({ return await run({
listOwnDevices: vi.fn(async () => [ listOwnDevices: vi.fn(async () => [
{ {

View File

@@ -1,44 +1,34 @@
import { summarizeMatrixDeviceHealth } from "../device-health.js"; import { summarizeMatrixDeviceHealth } from "../device-health.js";
import { withResolvedActionClient } from "./client.js"; import { withStartedActionClient } from "./client.js";
import type { MatrixActionClientOpts } from "./types.js"; import type { MatrixActionClientOpts } from "./types.js";
export async function listMatrixOwnDevices(opts: MatrixActionClientOpts = {}) { export async function listMatrixOwnDevices(opts: MatrixActionClientOpts = {}) {
return await withResolvedActionClient( return await withStartedActionClient(opts, async (client) => await client.listOwnDevices());
{ ...opts, readiness: "started" },
async (client) => await client.listOwnDevices(),
"persist",
);
} }
export async function pruneMatrixStaleGatewayDevices(opts: MatrixActionClientOpts = {}) { export async function pruneMatrixStaleGatewayDevices(opts: MatrixActionClientOpts = {}) {
return await withResolvedActionClient( return await withStartedActionClient(opts, async (client) => {
{ ...opts, readiness: "started" }, const devices = await client.listOwnDevices();
async (client) => { const health = summarizeMatrixDeviceHealth(devices);
const devices = await client.listOwnDevices(); const staleGatewayDeviceIds = health.staleOpenClawDevices.map((device) => device.deviceId);
const health = summarizeMatrixDeviceHealth(devices); const deleted =
const staleGatewayDeviceIds = health.staleOpenClawDevices.map((device) => device.deviceId); staleGatewayDeviceIds.length > 0
const deleted = ? await client.deleteOwnDevices(staleGatewayDeviceIds)
staleGatewayDeviceIds.length > 0 : {
? await client.deleteOwnDevices(staleGatewayDeviceIds) currentDeviceId: devices.find((device) => device.current)?.deviceId ?? null,
: { deletedDeviceIds: [] as string[],
currentDeviceId: devices.find((device) => device.current)?.deviceId ?? null, remainingDevices: devices,
deletedDeviceIds: [] as string[], };
remainingDevices: devices, return {
}; before: devices,
return { staleGatewayDeviceIds,
before: devices, ...deleted,
staleGatewayDeviceIds, };
...deleted, });
};
},
"persist",
);
} }
export async function getMatrixDeviceHealth(opts: MatrixActionClientOpts = {}) { export async function getMatrixDeviceHealth(opts: MatrixActionClientOpts = {}) {
return await withResolvedActionClient( return await withStartedActionClient(opts, async (client) =>
{ ...opts, readiness: "started" }, summarizeMatrixDeviceHealth(await client.listOwnDevices()),
async (client) => summarizeMatrixDeviceHealth(await client.listOwnDevices()),
"persist",
); );
} }

View File

@@ -1,6 +1,6 @@
import { beforeEach, describe, expect, it, vi } from "vitest"; import { beforeEach, describe, expect, it, vi } from "vitest";
const withResolvedActionClientMock = vi.fn(); const withStartedActionClientMock = vi.fn();
const loadConfigMock = vi.fn(() => ({ const loadConfigMock = vi.fn(() => ({
channels: { channels: {
matrix: {}, matrix: {},
@@ -16,7 +16,7 @@ vi.mock("../../runtime.js", () => ({
})); }));
vi.mock("./client.js", () => ({ vi.mock("./client.js", () => ({
withResolvedActionClient: (...args: unknown[]) => withResolvedActionClientMock(...args), withStartedActionClient: (...args: unknown[]) => withStartedActionClientMock(...args),
})); }));
let listMatrixVerifications: typeof import("./verification.js").listMatrixVerifications; let listMatrixVerifications: typeof import("./verification.js").listMatrixVerifications;
@@ -45,7 +45,7 @@ describe("matrix verification actions", () => {
}, },
}, },
}); });
withResolvedActionClientMock.mockImplementation(async (_opts, run) => { withStartedActionClientMock.mockImplementation(async (_opts, run) => {
return await run({ crypto: null }); return await run({ crypto: null });
}); });
@@ -67,7 +67,7 @@ describe("matrix verification actions", () => {
}, },
}, },
}); });
withResolvedActionClientMock.mockImplementation(async (_opts, run) => { withStartedActionClientMock.mockImplementation(async (_opts, run) => {
return await run({ crypto: null }); return await run({ crypto: null });
}); });

View File

@@ -1,7 +1,7 @@
import { getMatrixRuntime } from "../../runtime.js"; import { getMatrixRuntime } from "../../runtime.js";
import type { CoreConfig } from "../../types.js"; import type { CoreConfig } from "../../types.js";
import { formatMatrixEncryptionUnavailableError } from "../encryption-guidance.js"; import { formatMatrixEncryptionUnavailableError } from "../encryption-guidance.js";
import { withResolvedActionClient } from "./client.js"; import { withStartedActionClient } from "./client.js";
import type { MatrixActionClientOpts } from "./types.js"; import type { MatrixActionClientOpts } from "./types.js";
function requireCrypto( function requireCrypto(
@@ -24,14 +24,10 @@ function resolveVerificationId(input: string): string {
} }
export async function listMatrixVerifications(opts: MatrixActionClientOpts = {}) { export async function listMatrixVerifications(opts: MatrixActionClientOpts = {}) {
return await withResolvedActionClient( return await withStartedActionClient(opts, async (client) => {
{ ...opts, readiness: "started" }, const crypto = requireCrypto(client, opts);
async (client) => { return await crypto.listVerifications();
const crypto = requireCrypto(client, opts); });
return await crypto.listVerifications();
},
"persist",
);
} }
export async function requestMatrixVerification( export async function requestMatrixVerification(
@@ -42,79 +38,59 @@ export async function requestMatrixVerification(
roomId?: string; roomId?: string;
} = {}, } = {},
) { ) {
return await withResolvedActionClient( return await withStartedActionClient(params, async (client) => {
{ ...params, readiness: "started" }, const crypto = requireCrypto(client, params);
async (client) => { const ownUser = params.ownUser ?? (!params.userId && !params.deviceId && !params.roomId);
const crypto = requireCrypto(client, params); return await crypto.requestVerification({
const ownUser = params.ownUser ?? (!params.userId && !params.deviceId && !params.roomId); ownUser,
return await crypto.requestVerification({ userId: params.userId?.trim() || undefined,
ownUser, deviceId: params.deviceId?.trim() || undefined,
userId: params.userId?.trim() || undefined, roomId: params.roomId?.trim() || undefined,
deviceId: params.deviceId?.trim() || undefined, });
roomId: params.roomId?.trim() || undefined, });
});
},
"persist",
);
} }
export async function acceptMatrixVerification( export async function acceptMatrixVerification(
requestId: string, requestId: string,
opts: MatrixActionClientOpts = {}, opts: MatrixActionClientOpts = {},
) { ) {
return await withResolvedActionClient( return await withStartedActionClient(opts, async (client) => {
{ ...opts, readiness: "started" }, const crypto = requireCrypto(client, opts);
async (client) => { return await crypto.acceptVerification(resolveVerificationId(requestId));
const crypto = requireCrypto(client, opts); });
return await crypto.acceptVerification(resolveVerificationId(requestId));
},
"persist",
);
} }
export async function cancelMatrixVerification( export async function cancelMatrixVerification(
requestId: string, requestId: string,
opts: MatrixActionClientOpts & { reason?: string; code?: string } = {}, opts: MatrixActionClientOpts & { reason?: string; code?: string } = {},
) { ) {
return await withResolvedActionClient( return await withStartedActionClient(opts, async (client) => {
{ ...opts, readiness: "started" }, const crypto = requireCrypto(client, opts);
async (client) => { return await crypto.cancelVerification(resolveVerificationId(requestId), {
const crypto = requireCrypto(client, opts); reason: opts.reason?.trim() || undefined,
return await crypto.cancelVerification(resolveVerificationId(requestId), { code: opts.code?.trim() || undefined,
reason: opts.reason?.trim() || undefined, });
code: opts.code?.trim() || undefined, });
});
},
"persist",
);
} }
export async function startMatrixVerification( export async function startMatrixVerification(
requestId: string, requestId: string,
opts: MatrixActionClientOpts & { method?: "sas" } = {}, opts: MatrixActionClientOpts & { method?: "sas" } = {},
) { ) {
return await withResolvedActionClient( return await withStartedActionClient(opts, async (client) => {
{ ...opts, readiness: "started" }, const crypto = requireCrypto(client, opts);
async (client) => { return await crypto.startVerification(resolveVerificationId(requestId), opts.method ?? "sas");
const crypto = requireCrypto(client, opts); });
return await crypto.startVerification(resolveVerificationId(requestId), opts.method ?? "sas");
},
"persist",
);
} }
export async function generateMatrixVerificationQr( export async function generateMatrixVerificationQr(
requestId: string, requestId: string,
opts: MatrixActionClientOpts = {}, opts: MatrixActionClientOpts = {},
) { ) {
return await withResolvedActionClient( return await withStartedActionClient(opts, async (client) => {
{ ...opts, readiness: "started" }, const crypto = requireCrypto(client, opts);
async (client) => { return await crypto.generateVerificationQr(resolveVerificationId(requestId));
const crypto = requireCrypto(client, opts); });
return await crypto.generateVerificationQr(resolveVerificationId(requestId));
},
"persist",
);
} }
export async function scanMatrixVerificationQr( export async function scanMatrixVerificationQr(
@@ -122,125 +98,96 @@ export async function scanMatrixVerificationQr(
qrDataBase64: string, qrDataBase64: string,
opts: MatrixActionClientOpts = {}, opts: MatrixActionClientOpts = {},
) { ) {
return await withResolvedActionClient( return await withStartedActionClient(opts, async (client) => {
{ ...opts, readiness: "started" }, const crypto = requireCrypto(client, opts);
async (client) => { const payload = qrDataBase64.trim();
const crypto = requireCrypto(client, opts); if (!payload) {
const payload = qrDataBase64.trim(); throw new Error("Matrix QR data is required");
if (!payload) { }
throw new Error("Matrix QR data is required"); return await crypto.scanVerificationQr(resolveVerificationId(requestId), payload);
} });
return await crypto.scanVerificationQr(resolveVerificationId(requestId), payload);
},
"persist",
);
} }
export async function getMatrixVerificationSas( export async function getMatrixVerificationSas(
requestId: string, requestId: string,
opts: MatrixActionClientOpts = {}, opts: MatrixActionClientOpts = {},
) { ) {
return await withResolvedActionClient( return await withStartedActionClient(opts, async (client) => {
{ ...opts, readiness: "started" }, const crypto = requireCrypto(client, opts);
async (client) => { return await crypto.getVerificationSas(resolveVerificationId(requestId));
const crypto = requireCrypto(client, opts); });
return await crypto.getVerificationSas(resolveVerificationId(requestId));
},
"persist",
);
} }
export async function confirmMatrixVerificationSas( export async function confirmMatrixVerificationSas(
requestId: string, requestId: string,
opts: MatrixActionClientOpts = {}, opts: MatrixActionClientOpts = {},
) { ) {
return await withResolvedActionClient( return await withStartedActionClient(opts, async (client) => {
{ ...opts, readiness: "started" }, const crypto = requireCrypto(client, opts);
async (client) => { return await crypto.confirmVerificationSas(resolveVerificationId(requestId));
const crypto = requireCrypto(client, opts); });
return await crypto.confirmVerificationSas(resolveVerificationId(requestId));
},
"persist",
);
} }
export async function mismatchMatrixVerificationSas( export async function mismatchMatrixVerificationSas(
requestId: string, requestId: string,
opts: MatrixActionClientOpts = {}, opts: MatrixActionClientOpts = {},
) { ) {
return await withResolvedActionClient( return await withStartedActionClient(opts, async (client) => {
{ ...opts, readiness: "started" }, const crypto = requireCrypto(client, opts);
async (client) => { return await crypto.mismatchVerificationSas(resolveVerificationId(requestId));
const crypto = requireCrypto(client, opts); });
return await crypto.mismatchVerificationSas(resolveVerificationId(requestId));
},
"persist",
);
} }
export async function confirmMatrixVerificationReciprocateQr( export async function confirmMatrixVerificationReciprocateQr(
requestId: string, requestId: string,
opts: MatrixActionClientOpts = {}, opts: MatrixActionClientOpts = {},
) { ) {
return await withResolvedActionClient( return await withStartedActionClient(opts, async (client) => {
{ ...opts, readiness: "started" }, const crypto = requireCrypto(client, opts);
async (client) => { return await crypto.confirmVerificationReciprocateQr(resolveVerificationId(requestId));
const crypto = requireCrypto(client, opts); });
return await crypto.confirmVerificationReciprocateQr(resolveVerificationId(requestId));
},
"persist",
);
} }
export async function getMatrixEncryptionStatus( export async function getMatrixEncryptionStatus(
opts: MatrixActionClientOpts & { includeRecoveryKey?: boolean } = {}, opts: MatrixActionClientOpts & { includeRecoveryKey?: boolean } = {},
) { ) {
return await withResolvedActionClient( return await withStartedActionClient(opts, async (client) => {
{ ...opts, readiness: "started" }, const crypto = requireCrypto(client, opts);
async (client) => { const recoveryKey = await crypto.getRecoveryKey();
const crypto = requireCrypto(client, opts); return {
const recoveryKey = await crypto.getRecoveryKey(); encryptionEnabled: true,
return { recoveryKeyStored: Boolean(recoveryKey),
encryptionEnabled: true, recoveryKeyCreatedAt: recoveryKey?.createdAt ?? null,
recoveryKeyStored: Boolean(recoveryKey), ...(opts.includeRecoveryKey ? { recoveryKey: recoveryKey?.encodedPrivateKey ?? null } : {}),
recoveryKeyCreatedAt: recoveryKey?.createdAt ?? null, pendingVerifications: (await crypto.listVerifications()).length,
...(opts.includeRecoveryKey ? { recoveryKey: recoveryKey?.encodedPrivateKey ?? null } : {}), };
pendingVerifications: (await crypto.listVerifications()).length, });
};
},
"persist",
);
} }
export async function getMatrixVerificationStatus( export async function getMatrixVerificationStatus(
opts: MatrixActionClientOpts & { includeRecoveryKey?: boolean } = {}, opts: MatrixActionClientOpts & { includeRecoveryKey?: boolean } = {},
) { ) {
return await withResolvedActionClient( return await withStartedActionClient(opts, async (client) => {
{ ...opts, readiness: "started" }, const status = await client.getOwnDeviceVerificationStatus();
async (client) => { const payload = {
const status = await client.getOwnDeviceVerificationStatus(); ...status,
const payload = { pendingVerifications: client.crypto ? (await client.crypto.listVerifications()).length : 0,
...status, };
pendingVerifications: client.crypto ? (await client.crypto.listVerifications()).length : 0, if (!opts.includeRecoveryKey) {
}; return payload;
if (!opts.includeRecoveryKey) { }
return payload; const recoveryKey = client.crypto ? await client.crypto.getRecoveryKey() : null;
} return {
const recoveryKey = client.crypto ? await client.crypto.getRecoveryKey() : null; ...payload,
return { recoveryKey: recoveryKey?.encodedPrivateKey ?? null,
...payload, };
recoveryKey: recoveryKey?.encodedPrivateKey ?? null, });
};
},
"persist",
);
} }
export async function getMatrixRoomKeyBackupStatus(opts: MatrixActionClientOpts = {}) { export async function getMatrixRoomKeyBackupStatus(opts: MatrixActionClientOpts = {}) {
return await withResolvedActionClient( return await withStartedActionClient(
{ ...opts, readiness: "started" }, opts,
async (client) => await client.getRoomKeyBackupStatus(), async (client) => await client.getRoomKeyBackupStatus(),
"persist",
); );
} }
@@ -248,10 +195,9 @@ export async function verifyMatrixRecoveryKey(
recoveryKey: string, recoveryKey: string,
opts: MatrixActionClientOpts = {}, opts: MatrixActionClientOpts = {},
) { ) {
return await withResolvedActionClient( return await withStartedActionClient(
{ ...opts, readiness: "started" }, opts,
async (client) => await client.verifyWithRecoveryKey(recoveryKey), async (client) => await client.verifyWithRecoveryKey(recoveryKey),
"persist",
); );
} }
@@ -260,13 +206,12 @@ export async function restoreMatrixRoomKeyBackup(
recoveryKey?: string; recoveryKey?: string;
} = {}, } = {},
) { ) {
return await withResolvedActionClient( return await withStartedActionClient(
{ ...opts, readiness: "started" }, opts,
async (client) => async (client) =>
await client.restoreRoomKeyBackup({ await client.restoreRoomKeyBackup({
recoveryKey: opts.recoveryKey?.trim() || undefined, recoveryKey: opts.recoveryKey?.trim() || undefined,
}), }),
"persist",
); );
} }
@@ -276,13 +221,12 @@ export async function bootstrapMatrixVerification(
forceResetCrossSigning?: boolean; forceResetCrossSigning?: boolean;
} = {}, } = {},
) { ) {
return await withResolvedActionClient( return await withStartedActionClient(
{ ...opts, readiness: "started" }, opts,
async (client) => async (client) =>
await client.bootstrapOwnDeviceVerification({ await client.bootstrapOwnDeviceVerification({
recoveryKey: opts.recoveryKey?.trim() || undefined, recoveryKey: opts.recoveryKey?.trim() || undefined,
forceResetCrossSigning: opts.forceResetCrossSigning === true, forceResetCrossSigning: opts.forceResetCrossSigning === true,
}), }),
"persist",
); );
} }