mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
Plugins: scope SDK imports and harden Matrix routing
This commit is contained in:
@@ -29,4 +29,5 @@ openclaw pairing approve --channel telegram --account work <code> --notify
|
||||
- Channel input: pass it positionally (`pairing list telegram`) or with `--channel <channel>`.
|
||||
- `pairing list` supports `--account <accountId>` for multi-account channels.
|
||||
- `pairing approve` supports `--account <accountId>` and `--notify`.
|
||||
- When you combine `pairing approve --account <accountId> --notify`, OpenClaw sends the approval notice from that same account.
|
||||
- If only one pairing-capable channel is configured, `pairing approve <code>` is allowed.
|
||||
|
||||
@@ -14,6 +14,7 @@ async function main() {
|
||||
password: auth.password,
|
||||
deviceId: auth.deviceId,
|
||||
encryption: false,
|
||||
accountId: auth.accountId,
|
||||
});
|
||||
|
||||
const targetUserId = process.argv[2]?.trim() || "@user:example.org";
|
||||
|
||||
@@ -25,6 +25,7 @@ async function main() {
|
||||
password: auth.password,
|
||||
deviceId: auth.deviceId,
|
||||
encryption: true,
|
||||
accountId: auth.accountId,
|
||||
});
|
||||
const initCrypto = (client as unknown as { initializeCryptoIfNeeded?: () => Promise<void> })
|
||||
.initializeCryptoIfNeeded;
|
||||
|
||||
@@ -23,6 +23,7 @@ async function main() {
|
||||
password: auth.password,
|
||||
deviceId: auth.deviceId,
|
||||
encryption: false,
|
||||
accountId: auth.accountId,
|
||||
});
|
||||
|
||||
try {
|
||||
|
||||
@@ -31,6 +31,7 @@ async function main() {
|
||||
password: auth.password,
|
||||
deviceId: auth.deviceId,
|
||||
encryption: true,
|
||||
accountId: auth.accountId,
|
||||
});
|
||||
|
||||
const stamp = new Date().toISOString();
|
||||
|
||||
@@ -26,6 +26,7 @@ async function main() {
|
||||
password: auth.password,
|
||||
deviceId: auth.deviceId,
|
||||
encryption: true,
|
||||
accountId: auth.accountId,
|
||||
});
|
||||
|
||||
const stamp = new Date().toISOString();
|
||||
|
||||
@@ -40,6 +40,7 @@ async function main() {
|
||||
password: auth.password,
|
||||
deviceId: auth.deviceId,
|
||||
encryption: true,
|
||||
accountId: auth.accountId,
|
||||
});
|
||||
|
||||
try {
|
||||
|
||||
@@ -21,6 +21,7 @@ async function main() {
|
||||
password: auth.password,
|
||||
deviceId: auth.deviceId,
|
||||
encryption: false,
|
||||
accountId: auth.accountId,
|
||||
});
|
||||
|
||||
try {
|
||||
|
||||
90
extensions/matrix/src/channel.account-paths.test.ts
Normal file
90
extensions/matrix/src/channel.account-paths.test.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const sendMessageMatrixMock = vi.hoisted(() => vi.fn());
|
||||
const probeMatrixMock = vi.hoisted(() => vi.fn());
|
||||
const resolveMatrixAuthMock = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock("./matrix/send.js", async () => {
|
||||
const actual = await vi.importActual<typeof import("./matrix/send.js")>("./matrix/send.js");
|
||||
return {
|
||||
...actual,
|
||||
sendMessageMatrix: (...args: unknown[]) => sendMessageMatrixMock(...args),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("./matrix/probe.js", async () => {
|
||||
const actual = await vi.importActual<typeof import("./matrix/probe.js")>("./matrix/probe.js");
|
||||
return {
|
||||
...actual,
|
||||
probeMatrix: (...args: unknown[]) => probeMatrixMock(...args),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("./matrix/client.js", async () => {
|
||||
const actual = await vi.importActual<typeof import("./matrix/client.js")>("./matrix/client.js");
|
||||
return {
|
||||
...actual,
|
||||
resolveMatrixAuth: (...args: unknown[]) => resolveMatrixAuthMock(...args),
|
||||
};
|
||||
});
|
||||
|
||||
const { matrixPlugin } = await import("./channel.js");
|
||||
|
||||
describe("matrix account path propagation", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
sendMessageMatrixMock.mockResolvedValue({
|
||||
messageId: "$sent",
|
||||
roomId: "!room:example.org",
|
||||
});
|
||||
probeMatrixMock.mockResolvedValue({
|
||||
ok: true,
|
||||
error: null,
|
||||
status: null,
|
||||
elapsedMs: 5,
|
||||
userId: "@poe:example.org",
|
||||
});
|
||||
resolveMatrixAuthMock.mockResolvedValue({
|
||||
accountId: "poe",
|
||||
homeserver: "https://matrix.example.org",
|
||||
userId: "@poe:example.org",
|
||||
accessToken: "poe-token",
|
||||
});
|
||||
});
|
||||
|
||||
it("forwards accountId when notifying pairing approval", async () => {
|
||||
await matrixPlugin.pairing!.notifyApproval?.({
|
||||
cfg: {},
|
||||
id: "@user:example.org",
|
||||
accountId: "poe",
|
||||
});
|
||||
|
||||
expect(sendMessageMatrixMock).toHaveBeenCalledWith(
|
||||
"user:@user:example.org",
|
||||
expect.any(String),
|
||||
{ accountId: "poe" },
|
||||
);
|
||||
});
|
||||
|
||||
it("forwards accountId to matrix probes", async () => {
|
||||
await matrixPlugin.status!.probeAccount?.({
|
||||
cfg: {} as never,
|
||||
timeoutMs: 500,
|
||||
account: {
|
||||
accountId: "poe",
|
||||
} as never,
|
||||
});
|
||||
|
||||
expect(resolveMatrixAuthMock).toHaveBeenCalledWith({
|
||||
cfg: {},
|
||||
accountId: "poe",
|
||||
});
|
||||
expect(probeMatrixMock).toHaveBeenCalledWith({
|
||||
homeserver: "https://matrix.example.org",
|
||||
accessToken: "poe-token",
|
||||
userId: "@poe:example.org",
|
||||
timeoutMs: 500,
|
||||
accountId: "poe",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -12,6 +12,7 @@ describe("matrix directory live", () => {
|
||||
beforeEach(() => {
|
||||
vi.mocked(resolveMatrixAuth).mockReset();
|
||||
vi.mocked(resolveMatrixAuth).mockResolvedValue({
|
||||
accountId: "assistant",
|
||||
homeserver: "https://matrix.example.org",
|
||||
userId: "@bot:example.org",
|
||||
accessToken: "test-token",
|
||||
|
||||
@@ -42,6 +42,7 @@ describe("resolveActionClient", () => {
|
||||
getActiveMatrixClientMock.mockReturnValue(null);
|
||||
isBunRuntimeMock.mockReturnValue(false);
|
||||
resolveMatrixAuthMock.mockResolvedValue({
|
||||
accountId: "default",
|
||||
homeserver: "https://matrix.example.org",
|
||||
userId: "@bot:example.org",
|
||||
accessToken: "token",
|
||||
@@ -53,7 +54,7 @@ describe("resolveActionClient", () => {
|
||||
({ cfg, accountId }: { cfg: unknown; accountId?: string | null }) => ({
|
||||
cfg,
|
||||
env: process.env,
|
||||
accountId: accountId ?? undefined,
|
||||
accountId: accountId ?? "default",
|
||||
resolved: {
|
||||
homeserver: "https://matrix.example.org",
|
||||
userId: "@bot:example.org",
|
||||
@@ -129,6 +130,7 @@ describe("resolveActionClient", () => {
|
||||
},
|
||||
});
|
||||
resolveMatrixAuthMock.mockResolvedValue({
|
||||
accountId: "ops",
|
||||
homeserver: "https://ops.example.org",
|
||||
userId: "@ops:example.org",
|
||||
accessToken: "ops-token",
|
||||
|
||||
@@ -43,7 +43,7 @@ export async function resolveActionClient(
|
||||
deviceId: auth.deviceId,
|
||||
encryption: auth.encryption,
|
||||
localTimeoutMs: opts.timeoutMs,
|
||||
accountId: authContext.accountId,
|
||||
accountId: auth.accountId,
|
||||
autoBootstrapCrypto: false,
|
||||
});
|
||||
await client.prepareForOneOff();
|
||||
|
||||
228
extensions/matrix/src/matrix/actions/messages.test.ts
Normal file
228
extensions/matrix/src/matrix/actions/messages.test.ts
Normal file
@@ -0,0 +1,228 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import type { MatrixClient } from "../sdk.js";
|
||||
import { readMatrixMessages } from "./messages.js";
|
||||
|
||||
function createMessagesClient(params: {
|
||||
chunk: Array<Record<string, unknown>>;
|
||||
hydratedChunk?: Array<Record<string, unknown>>;
|
||||
pollRoot?: Record<string, unknown>;
|
||||
pollRelations?: Array<Record<string, unknown>>;
|
||||
}) {
|
||||
const doRequest = vi.fn(async () => ({
|
||||
chunk: params.chunk,
|
||||
start: "start-token",
|
||||
end: "end-token",
|
||||
}));
|
||||
const hydrateEvents = vi.fn(
|
||||
async (_roomId: string, _events: Array<Record<string, unknown>>) =>
|
||||
(params.hydratedChunk ?? params.chunk) as any,
|
||||
);
|
||||
const getEvent = vi.fn(async () => params.pollRoot ?? null);
|
||||
const getRelations = vi.fn(async () => ({
|
||||
events: params.pollRelations ?? [],
|
||||
nextBatch: null,
|
||||
prevBatch: null,
|
||||
}));
|
||||
|
||||
return {
|
||||
client: {
|
||||
doRequest,
|
||||
hydrateEvents,
|
||||
getEvent,
|
||||
getRelations,
|
||||
stop: vi.fn(),
|
||||
} as unknown as MatrixClient,
|
||||
doRequest,
|
||||
hydrateEvents,
|
||||
getEvent,
|
||||
getRelations,
|
||||
};
|
||||
}
|
||||
|
||||
describe("matrix message actions", () => {
|
||||
it("includes poll snapshots when reading message history", async () => {
|
||||
const { client, doRequest, getEvent, getRelations } = createMessagesClient({
|
||||
chunk: [
|
||||
{
|
||||
event_id: "$vote",
|
||||
sender: "@bob:example.org",
|
||||
type: "m.poll.response",
|
||||
origin_server_ts: 20,
|
||||
content: {
|
||||
"m.poll.response": { answers: ["a1"] },
|
||||
"m.relates_to": { rel_type: "m.reference", event_id: "$poll" },
|
||||
},
|
||||
},
|
||||
{
|
||||
event_id: "$msg",
|
||||
sender: "@alice:example.org",
|
||||
type: "m.room.message",
|
||||
origin_server_ts: 10,
|
||||
content: {
|
||||
msgtype: "m.text",
|
||||
body: "hello",
|
||||
},
|
||||
},
|
||||
],
|
||||
pollRoot: {
|
||||
event_id: "$poll",
|
||||
sender: "@alice:example.org",
|
||||
type: "m.poll.start",
|
||||
origin_server_ts: 1,
|
||||
content: {
|
||||
"m.poll.start": {
|
||||
question: { "m.text": "Favorite fruit?" },
|
||||
kind: "m.poll.disclosed",
|
||||
max_selections: 1,
|
||||
answers: [
|
||||
{ id: "a1", "m.text": "Apple" },
|
||||
{ id: "a2", "m.text": "Strawberry" },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
pollRelations: [
|
||||
{
|
||||
event_id: "$vote",
|
||||
sender: "@bob:example.org",
|
||||
type: "m.poll.response",
|
||||
origin_server_ts: 20,
|
||||
content: {
|
||||
"m.poll.response": { answers: ["a1"] },
|
||||
"m.relates_to": { rel_type: "m.reference", event_id: "$poll" },
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const result = await readMatrixMessages("room:!room:example.org", { client, limit: 2.9 });
|
||||
|
||||
expect(doRequest).toHaveBeenCalledWith(
|
||||
"GET",
|
||||
expect.stringContaining("/rooms/!room%3Aexample.org/messages"),
|
||||
expect.objectContaining({ limit: 2 }),
|
||||
);
|
||||
expect(getEvent).toHaveBeenCalledWith("!room:example.org", "$poll");
|
||||
expect(getRelations).toHaveBeenCalledWith(
|
||||
"!room:example.org",
|
||||
"$poll",
|
||||
"m.reference",
|
||||
undefined,
|
||||
{
|
||||
from: undefined,
|
||||
},
|
||||
);
|
||||
expect(result.messages).toEqual([
|
||||
expect.objectContaining({
|
||||
eventId: "$poll",
|
||||
body: expect.stringContaining("1. Apple (1 vote)"),
|
||||
msgtype: "m.text",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
eventId: "$msg",
|
||||
body: "hello",
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("dedupes multiple poll events for the same poll within one read page", async () => {
|
||||
const { client, getEvent } = createMessagesClient({
|
||||
chunk: [
|
||||
{
|
||||
event_id: "$vote",
|
||||
sender: "@bob:example.org",
|
||||
type: "m.poll.response",
|
||||
origin_server_ts: 20,
|
||||
content: {
|
||||
"m.poll.response": { answers: ["a1"] },
|
||||
"m.relates_to": { rel_type: "m.reference", event_id: "$poll" },
|
||||
},
|
||||
},
|
||||
{
|
||||
event_id: "$poll",
|
||||
sender: "@alice:example.org",
|
||||
type: "m.poll.start",
|
||||
origin_server_ts: 1,
|
||||
content: {
|
||||
"m.poll.start": {
|
||||
question: { "m.text": "Favorite fruit?" },
|
||||
answers: [{ id: "a1", "m.text": "Apple" }],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
pollRoot: {
|
||||
event_id: "$poll",
|
||||
sender: "@alice:example.org",
|
||||
type: "m.poll.start",
|
||||
origin_server_ts: 1,
|
||||
content: {
|
||||
"m.poll.start": {
|
||||
question: { "m.text": "Favorite fruit?" },
|
||||
answers: [{ id: "a1", "m.text": "Apple" }],
|
||||
},
|
||||
},
|
||||
},
|
||||
pollRelations: [],
|
||||
});
|
||||
|
||||
const result = await readMatrixMessages("room:!room:example.org", { client });
|
||||
|
||||
expect(result.messages).toHaveLength(1);
|
||||
expect(result.messages[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
eventId: "$poll",
|
||||
body: expect.stringContaining("[Poll]"),
|
||||
}),
|
||||
);
|
||||
expect(getEvent).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("uses hydrated history events so encrypted poll entries can be read", async () => {
|
||||
const { client, hydrateEvents } = createMessagesClient({
|
||||
chunk: [
|
||||
{
|
||||
event_id: "$enc",
|
||||
sender: "@bob:example.org",
|
||||
type: "m.room.encrypted",
|
||||
origin_server_ts: 20,
|
||||
content: {},
|
||||
},
|
||||
],
|
||||
hydratedChunk: [
|
||||
{
|
||||
event_id: "$vote",
|
||||
sender: "@bob:example.org",
|
||||
type: "m.poll.response",
|
||||
origin_server_ts: 20,
|
||||
content: {
|
||||
"m.poll.response": { answers: ["a1"] },
|
||||
"m.relates_to": { rel_type: "m.reference", event_id: "$poll" },
|
||||
},
|
||||
},
|
||||
],
|
||||
pollRoot: {
|
||||
event_id: "$poll",
|
||||
sender: "@alice:example.org",
|
||||
type: "m.poll.start",
|
||||
origin_server_ts: 1,
|
||||
content: {
|
||||
"m.poll.start": {
|
||||
question: { "m.text": "Favorite fruit?" },
|
||||
answers: [{ id: "a1", "m.text": "Apple" }],
|
||||
},
|
||||
},
|
||||
},
|
||||
pollRelations: [],
|
||||
});
|
||||
|
||||
const result = await readMatrixMessages("room:!room:example.org", { client });
|
||||
|
||||
expect(hydrateEvents).toHaveBeenCalledWith(
|
||||
"!room:example.org",
|
||||
expect.arrayContaining([expect.objectContaining({ event_id: "$enc" })]),
|
||||
);
|
||||
expect(result.messages).toHaveLength(1);
|
||||
expect(result.messages[0]?.eventId).toBe("$poll");
|
||||
});
|
||||
});
|
||||
@@ -1,3 +1,5 @@
|
||||
import { fetchMatrixPollMessageSummary, resolveMatrixPollRootEventId } from "../poll-summary.js";
|
||||
import { isPollEventType } from "../poll-types.js";
|
||||
import { resolveMatrixRoomId, sendMessageMatrix } from "../send.js";
|
||||
import { withResolvedActionClient } from "./client.js";
|
||||
import { resolveMatrixActionLimit } from "./limits.js";
|
||||
@@ -99,10 +101,30 @@ export async function readMatrixMessages(
|
||||
from: token,
|
||||
},
|
||||
)) as { chunk: MatrixRawEvent[]; start?: string; end?: string };
|
||||
const messages = res.chunk
|
||||
.filter((event) => event.type === EventType.RoomMessage)
|
||||
.filter((event) => !event.unsigned?.redacted_because)
|
||||
.map(summarizeMatrixRawEvent);
|
||||
const hydratedChunk = await client.hydrateEvents(resolvedRoom, res.chunk);
|
||||
const seenPollRoots = new Set<string>();
|
||||
const messages: MatrixMessageSummary[] = [];
|
||||
for (const event of hydratedChunk) {
|
||||
if (event.unsigned?.redacted_because) {
|
||||
continue;
|
||||
}
|
||||
if (event.type === EventType.RoomMessage) {
|
||||
messages.push(summarizeMatrixRawEvent(event));
|
||||
continue;
|
||||
}
|
||||
if (!isPollEventType(event.type)) {
|
||||
continue;
|
||||
}
|
||||
const pollRootId = resolveMatrixPollRootEventId(event);
|
||||
if (!pollRootId || seenPollRoots.has(pollRootId)) {
|
||||
continue;
|
||||
}
|
||||
seenPollRoots.add(pollRootId);
|
||||
const pollSummary = await fetchMatrixPollMessageSummary(client, resolvedRoom, event);
|
||||
if (pollSummary) {
|
||||
messages.push(pollSummary);
|
||||
}
|
||||
}
|
||||
return {
|
||||
messages,
|
||||
nextBatch: res.end ?? null,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { fetchMatrixPollMessageSummary } from "../poll-summary.js";
|
||||
import type { MatrixClient } from "../sdk.js";
|
||||
import {
|
||||
EventType,
|
||||
@@ -67,6 +68,10 @@ export async function fetchEventSummary(
|
||||
if (raw.unsigned?.redacted_because) {
|
||||
return null;
|
||||
}
|
||||
const pollSummary = await fetchMatrixPollMessageSummary(client, roomId, raw);
|
||||
if (pollSummary) {
|
||||
return pollSummary;
|
||||
}
|
||||
return summarizeMatrixRawEvent(raw);
|
||||
} catch {
|
||||
// Event not found, redacted, or inaccessible - return null
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
import { createMatrixClient } from "./client.js";
|
||||
|
||||
type MatrixClientBootstrapAuth = {
|
||||
homeserver: string;
|
||||
userId: string;
|
||||
accessToken: string;
|
||||
encryption?: boolean;
|
||||
};
|
||||
import type { MatrixAuth } from "./client/types.js";
|
||||
|
||||
type MatrixCryptoPrepare = {
|
||||
prepare: (rooms?: string[]) => Promise<void>;
|
||||
@@ -14,9 +8,8 @@ type MatrixCryptoPrepare = {
|
||||
type MatrixBootstrapClient = Awaited<ReturnType<typeof createMatrixClient>>;
|
||||
|
||||
export async function createPreparedMatrixClient(opts: {
|
||||
auth: MatrixClientBootstrapAuth;
|
||||
auth: Pick<MatrixAuth, "accountId" | "homeserver" | "userId" | "accessToken" | "encryption">;
|
||||
timeoutMs?: number;
|
||||
accountId?: string;
|
||||
}): Promise<MatrixBootstrapClient> {
|
||||
const client = await createMatrixClient({
|
||||
homeserver: opts.auth.homeserver,
|
||||
@@ -24,7 +17,7 @@ export async function createPreparedMatrixClient(opts: {
|
||||
accessToken: opts.auth.accessToken,
|
||||
encryption: opts.auth.encryption,
|
||||
localTimeoutMs: opts.timeoutMs,
|
||||
accountId: opts.accountId,
|
||||
accountId: opts.auth.accountId,
|
||||
});
|
||||
if (opts.auth.encryption && client.crypto) {
|
||||
try {
|
||||
|
||||
@@ -130,6 +130,7 @@ describe("resolveMatrixAuth", () => {
|
||||
}),
|
||||
);
|
||||
expect(auth).toMatchObject({
|
||||
accountId: "default",
|
||||
homeserver: "https://matrix.example.org",
|
||||
userId: "@bot:example.org",
|
||||
accessToken: "tok-123",
|
||||
@@ -144,7 +145,7 @@ describe("resolveMatrixAuth", () => {
|
||||
deviceId: "DEVICE123",
|
||||
}),
|
||||
expect.any(Object),
|
||||
undefined,
|
||||
"default",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -206,6 +207,7 @@ describe("resolveMatrixAuth", () => {
|
||||
});
|
||||
|
||||
expect(auth).toMatchObject({
|
||||
accountId: "default",
|
||||
homeserver: "https://matrix.example.org",
|
||||
userId: "@bot:example.org",
|
||||
accessToken: "cached-token",
|
||||
@@ -238,6 +240,7 @@ describe("resolveMatrixAuth", () => {
|
||||
const auth = await resolveMatrixAuth({ cfg, env: {} as NodeJS.ProcessEnv });
|
||||
|
||||
expect(auth.deviceId).toBe("DEVICE123");
|
||||
expect(auth.accountId).toBe("default");
|
||||
expect(saveMatrixCredentialsMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
homeserver: "https://matrix.example.org",
|
||||
@@ -246,7 +249,7 @@ describe("resolveMatrixAuth", () => {
|
||||
deviceId: "DEVICE123",
|
||||
}),
|
||||
expect.any(Object),
|
||||
undefined,
|
||||
"default",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -273,6 +276,7 @@ describe("resolveMatrixAuth", () => {
|
||||
|
||||
expect(doRequestSpy).toHaveBeenCalledWith("GET", "/_matrix/client/v3/account/whoami");
|
||||
expect(auth).toMatchObject({
|
||||
accountId: "default",
|
||||
homeserver: "https://matrix.example.org",
|
||||
userId: "@bot:example.org",
|
||||
accessToken: "tok-123",
|
||||
@@ -304,6 +308,7 @@ describe("resolveMatrixAuth", () => {
|
||||
const auth = await resolveMatrixAuth({ cfg, env: {} as NodeJS.ProcessEnv });
|
||||
|
||||
expect(auth).toMatchObject({
|
||||
accountId: "default",
|
||||
homeserver: "https://matrix.example.org",
|
||||
userId: "@bot:example.org",
|
||||
accessToken: "tok-123",
|
||||
@@ -332,6 +337,7 @@ describe("resolveMatrixAuth", () => {
|
||||
const auth = await resolveMatrixAuth({ cfg, env: {} as NodeJS.ProcessEnv });
|
||||
|
||||
expect(auth).toMatchObject({
|
||||
accountId: "ops",
|
||||
homeserver: "https://ops.example.org",
|
||||
userId: "@ops:example.org",
|
||||
accessToken: "ops-token",
|
||||
|
||||
@@ -291,7 +291,7 @@ export function resolveMatrixAuthContext(params?: {
|
||||
}): {
|
||||
cfg: CoreConfig;
|
||||
env: NodeJS.ProcessEnv;
|
||||
accountId?: string;
|
||||
accountId: string;
|
||||
resolved: MatrixResolvedConfig;
|
||||
} {
|
||||
const cfg = params?.cfg ?? (getMatrixRuntime().config.loadConfig() as CoreConfig);
|
||||
@@ -301,11 +301,12 @@ export function resolveMatrixAuthContext(params?: {
|
||||
const effectiveAccountId =
|
||||
explicitAccountId ??
|
||||
(defaultResolved.homeserver
|
||||
? undefined
|
||||
: (resolveImplicitMatrixAccountId(cfg, env) ?? undefined));
|
||||
const resolved = effectiveAccountId
|
||||
? resolveMatrixConfigForAccount(cfg, effectiveAccountId, env)
|
||||
: defaultResolved;
|
||||
? DEFAULT_ACCOUNT_ID
|
||||
: (resolveImplicitMatrixAccountId(cfg, env) ?? DEFAULT_ACCOUNT_ID));
|
||||
const resolved =
|
||||
effectiveAccountId === DEFAULT_ACCOUNT_ID && defaultResolved.homeserver
|
||||
? defaultResolved
|
||||
: resolveMatrixConfigForAccount(cfg, effectiveAccountId, env);
|
||||
|
||||
return {
|
||||
cfg,
|
||||
@@ -390,6 +391,7 @@ export async function resolveMatrixAuth(params?: {
|
||||
await touchMatrixCredentials(env, accountId);
|
||||
}
|
||||
return {
|
||||
accountId,
|
||||
homeserver: resolved.homeserver,
|
||||
userId,
|
||||
accessToken: resolved.accessToken,
|
||||
@@ -404,6 +406,7 @@ export async function resolveMatrixAuth(params?: {
|
||||
if (cachedCredentials) {
|
||||
await touchMatrixCredentials(env, accountId);
|
||||
return {
|
||||
accountId,
|
||||
homeserver: cachedCredentials.homeserver,
|
||||
userId: cachedCredentials.userId,
|
||||
accessToken: cachedCredentials.accessToken,
|
||||
@@ -446,6 +449,7 @@ export async function resolveMatrixAuth(params?: {
|
||||
}
|
||||
|
||||
const auth: MatrixAuth = {
|
||||
accountId,
|
||||
homeserver: resolved.homeserver,
|
||||
userId: login.user_id ?? resolved.userId,
|
||||
accessToken,
|
||||
|
||||
@@ -2,10 +2,12 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { MatrixAuth } from "./types.js";
|
||||
|
||||
const resolveMatrixAuthMock = vi.hoisted(() => vi.fn());
|
||||
const resolveMatrixAuthContextMock = vi.hoisted(() => vi.fn());
|
||||
const createMatrixClientMock = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock("./config.js", () => ({
|
||||
resolveMatrixAuth: resolveMatrixAuthMock,
|
||||
resolveMatrixAuthContext: resolveMatrixAuthContextMock,
|
||||
}));
|
||||
|
||||
vi.mock("./create-client.js", () => ({
|
||||
@@ -20,6 +22,7 @@ import {
|
||||
|
||||
function authFor(accountId: string): MatrixAuth {
|
||||
return {
|
||||
accountId,
|
||||
homeserver: "https://matrix.example.org",
|
||||
userId: `@${accountId}:example.org`,
|
||||
accessToken: `token-${accountId}`,
|
||||
@@ -45,7 +48,16 @@ function createMockClient(name: string) {
|
||||
describe("resolveSharedMatrixClient", () => {
|
||||
beforeEach(() => {
|
||||
resolveMatrixAuthMock.mockReset();
|
||||
resolveMatrixAuthContextMock.mockReset();
|
||||
createMatrixClientMock.mockReset();
|
||||
resolveMatrixAuthContextMock.mockImplementation(
|
||||
({ accountId }: { accountId?: string | null } = {}) => ({
|
||||
cfg: undefined,
|
||||
env: undefined,
|
||||
accountId: accountId ?? "default",
|
||||
resolved: {},
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -100,7 +112,7 @@ describe("resolveSharedMatrixClient", () => {
|
||||
await resolveSharedMatrixClient({ accountId: "main", startClient: false });
|
||||
await resolveSharedMatrixClient({ accountId: "ops", startClient: false });
|
||||
|
||||
stopSharedClientForAccount(mainAuth, "main");
|
||||
stopSharedClientForAccount(mainAuth);
|
||||
|
||||
expect(mainClient.stop).toHaveBeenCalledTimes(1);
|
||||
expect(poeClient.stop).toHaveBeenCalledTimes(0);
|
||||
@@ -109,4 +121,45 @@ describe("resolveSharedMatrixClient", () => {
|
||||
|
||||
expect(poeClient.stop).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("reuses the effective implicit account instead of keying it as default", async () => {
|
||||
const poeAuth = authFor("ops");
|
||||
const poeClient = createMockClient("ops");
|
||||
|
||||
resolveMatrixAuthContextMock.mockReturnValue({
|
||||
cfg: undefined,
|
||||
env: undefined,
|
||||
accountId: "ops",
|
||||
resolved: {},
|
||||
});
|
||||
resolveMatrixAuthMock.mockResolvedValue(poeAuth);
|
||||
createMatrixClientMock.mockResolvedValue(poeClient);
|
||||
|
||||
const first = await resolveSharedMatrixClient({ startClient: false });
|
||||
const second = await resolveSharedMatrixClient({ startClient: false });
|
||||
|
||||
expect(first).toBe(poeClient);
|
||||
expect(second).toBe(poeClient);
|
||||
expect(resolveMatrixAuthMock).toHaveBeenCalledWith({
|
||||
cfg: undefined,
|
||||
env: undefined,
|
||||
accountId: "ops",
|
||||
});
|
||||
expect(createMatrixClientMock).toHaveBeenCalledTimes(1);
|
||||
expect(createMatrixClientMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
accountId: "ops",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects mismatched explicit account ids when auth is already resolved", async () => {
|
||||
await expect(
|
||||
resolveSharedMatrixClient({
|
||||
auth: authFor("ops"),
|
||||
accountId: "main",
|
||||
startClient: false,
|
||||
}),
|
||||
).rejects.toThrow("Matrix shared client account mismatch");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { normalizeOptionalAccountId } from "openclaw/plugin-sdk/account-id";
|
||||
import type { CoreConfig } from "../../types.js";
|
||||
import type { MatrixClient } from "../sdk.js";
|
||||
import { LogService } from "../sdk/logger.js";
|
||||
import { resolveMatrixAuth } from "./config.js";
|
||||
import { resolveMatrixAuth, resolveMatrixAuthContext } from "./config.js";
|
||||
import { createMatrixClient } from "./create-client.js";
|
||||
import { DEFAULT_ACCOUNT_KEY } from "./storage.js";
|
||||
import type { MatrixAuth } from "./types.js";
|
||||
|
||||
type SharedMatrixClientState = {
|
||||
@@ -17,20 +17,19 @@ type SharedMatrixClientState = {
|
||||
const sharedClientStates = new Map<string, SharedMatrixClientState>();
|
||||
const sharedClientPromises = new Map<string, Promise<SharedMatrixClientState>>();
|
||||
|
||||
function buildSharedClientKey(auth: MatrixAuth, accountId?: string | null): string {
|
||||
function buildSharedClientKey(auth: MatrixAuth): string {
|
||||
return [
|
||||
auth.homeserver,
|
||||
auth.userId,
|
||||
auth.accessToken,
|
||||
auth.encryption ? "e2ee" : "plain",
|
||||
accountId ?? DEFAULT_ACCOUNT_KEY,
|
||||
auth.accountId,
|
||||
].join("|");
|
||||
}
|
||||
|
||||
async function createSharedMatrixClient(params: {
|
||||
auth: MatrixAuth;
|
||||
timeoutMs?: number;
|
||||
accountId?: string | null;
|
||||
}): Promise<SharedMatrixClientState> {
|
||||
const client = await createMatrixClient({
|
||||
homeserver: params.auth.homeserver,
|
||||
@@ -41,11 +40,11 @@ async function createSharedMatrixClient(params: {
|
||||
encryption: params.auth.encryption,
|
||||
localTimeoutMs: params.timeoutMs,
|
||||
initialSyncLimit: params.auth.initialSyncLimit,
|
||||
accountId: params.accountId,
|
||||
accountId: params.auth.accountId,
|
||||
});
|
||||
return {
|
||||
client,
|
||||
key: buildSharedClientKey(params.auth, params.accountId),
|
||||
key: buildSharedClientKey(params.auth),
|
||||
started: false,
|
||||
cryptoReady: false,
|
||||
startPromise: null,
|
||||
@@ -103,14 +102,27 @@ export async function resolveSharedMatrixClient(
|
||||
accountId?: string | null;
|
||||
} = {},
|
||||
): Promise<MatrixClient> {
|
||||
const requestedAccountId = normalizeOptionalAccountId(params.accountId);
|
||||
if (params.auth && requestedAccountId && requestedAccountId !== params.auth.accountId) {
|
||||
throw new Error(
|
||||
`Matrix shared client account mismatch: requested ${requestedAccountId}, auth resolved ${params.auth.accountId}`,
|
||||
);
|
||||
}
|
||||
const authContext = params.auth
|
||||
? null
|
||||
: resolveMatrixAuthContext({
|
||||
cfg: params.cfg,
|
||||
env: params.env,
|
||||
accountId: params.accountId,
|
||||
});
|
||||
const auth =
|
||||
params.auth ??
|
||||
(await resolveMatrixAuth({
|
||||
cfg: params.cfg,
|
||||
env: params.env,
|
||||
accountId: params.accountId,
|
||||
cfg: authContext?.cfg ?? params.cfg,
|
||||
env: authContext?.env ?? params.env,
|
||||
accountId: authContext?.accountId,
|
||||
}));
|
||||
const key = buildSharedClientKey(auth, params.accountId);
|
||||
const key = buildSharedClientKey(auth);
|
||||
const shouldStart = params.startClient !== false;
|
||||
|
||||
const existingState = sharedClientStates.get(key);
|
||||
@@ -143,7 +155,6 @@ export async function resolveSharedMatrixClient(
|
||||
const creationPromise = createSharedMatrixClient({
|
||||
auth,
|
||||
timeoutMs: params.timeoutMs,
|
||||
accountId: params.accountId,
|
||||
});
|
||||
sharedClientPromises.set(key, creationPromise);
|
||||
|
||||
@@ -181,8 +192,8 @@ export function stopSharedClient(): void {
|
||||
sharedClientPromises.clear();
|
||||
}
|
||||
|
||||
export function stopSharedClientForAccount(auth: MatrixAuth, accountId?: string | null): void {
|
||||
const key = buildSharedClientKey(auth, accountId);
|
||||
export function stopSharedClientForAccount(auth: MatrixAuth): void {
|
||||
const key = buildSharedClientKey(auth);
|
||||
const state = sharedClientStates.get(key);
|
||||
if (!state) {
|
||||
return;
|
||||
|
||||
@@ -17,6 +17,7 @@ export type MatrixResolvedConfig = {
|
||||
* both will need to be recreated together.
|
||||
*/
|
||||
export type MatrixAuth = {
|
||||
accountId: string;
|
||||
homeserver: string;
|
||||
userId: string;
|
||||
accessToken: string;
|
||||
|
||||
@@ -45,7 +45,7 @@ function createHarness(params?: {
|
||||
|
||||
registerMatrixMonitorEvents({
|
||||
client,
|
||||
auth: { encryption: true } as MatrixAuth,
|
||||
auth: { accountId: "default", encryption: true } as MatrixAuth,
|
||||
logVerboseMessage: vi.fn(),
|
||||
warnedEncryptedRooms: new Set<string>(),
|
||||
warnedCryptoMissingRooms: new Set<string>(),
|
||||
|
||||
@@ -99,6 +99,7 @@ describe("createMatrixRoomMessageHandler inbound body formatting", () => {
|
||||
logger: {
|
||||
info: () => {},
|
||||
warn: () => {},
|
||||
error: () => {},
|
||||
} as RuntimeLogger,
|
||||
logVerboseMessage: () => {},
|
||||
allowFrom: [],
|
||||
@@ -247,6 +248,7 @@ describe("createMatrixRoomMessageHandler inbound body formatting", () => {
|
||||
logger: {
|
||||
info: () => {},
|
||||
warn: () => {},
|
||||
error: () => {},
|
||||
} as RuntimeLogger,
|
||||
logVerboseMessage: () => {},
|
||||
allowFrom: [],
|
||||
|
||||
@@ -15,16 +15,8 @@ import {
|
||||
type RuntimeLogger,
|
||||
} from "openclaw/plugin-sdk/matrix";
|
||||
import type { CoreConfig, MatrixRoomConfig, ReplyToMode } from "../../types.js";
|
||||
import {
|
||||
formatPollAsText,
|
||||
formatPollResultsAsText,
|
||||
isPollEventType,
|
||||
isPollStartType,
|
||||
parsePollStartContent,
|
||||
resolvePollReferenceEventId,
|
||||
buildPollResultsSummary,
|
||||
type PollStartContent,
|
||||
} from "../poll-types.js";
|
||||
import { fetchMatrixPollSnapshot } from "../poll-summary.js";
|
||||
import { isPollEventType } from "../poll-types.js";
|
||||
import type { LocationMessageEventContent, MatrixClient } from "../sdk.js";
|
||||
import {
|
||||
reactMatrixMessage,
|
||||
@@ -217,60 +209,18 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
|
||||
|
||||
let content = event.content as RoomMessageEventContent;
|
||||
if (isPollEvent) {
|
||||
const pollEventId = isPollStartType(eventType)
|
||||
? (event.event_id ?? "")
|
||||
: resolvePollReferenceEventId(event.content);
|
||||
if (!pollEventId) {
|
||||
return;
|
||||
}
|
||||
const pollEvent = isPollStartType(eventType)
|
||||
? event
|
||||
: await client.getEvent(roomId, pollEventId).catch((err) => {
|
||||
logVerboseMessage(
|
||||
`matrix: failed resolving poll root room=${roomId} id=${pollEventId}: ${String(err)}`,
|
||||
);
|
||||
return null;
|
||||
});
|
||||
if (
|
||||
!pollEvent ||
|
||||
!isPollStartType(typeof pollEvent.type === "string" ? pollEvent.type : "")
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const pollStartContent = pollEvent.content as PollStartContent;
|
||||
const pollSummary = parsePollStartContent(pollStartContent);
|
||||
if (!pollSummary) {
|
||||
return;
|
||||
}
|
||||
pollSummary.eventId = pollEventId;
|
||||
pollSummary.roomId = roomId;
|
||||
pollSummary.sender = typeof pollEvent.sender === "string" ? pollEvent.sender : senderId;
|
||||
pollSummary.senderName = await getMemberDisplayName(roomId, pollSummary.sender);
|
||||
|
||||
const relationEvents: MatrixRawEvent[] = [];
|
||||
let nextBatch: string | undefined;
|
||||
do {
|
||||
const page = await client.getRelations(roomId, pollEventId, "m.reference", undefined, {
|
||||
from: nextBatch,
|
||||
});
|
||||
relationEvents.push(...page.events);
|
||||
nextBatch = page.nextBatch ?? undefined;
|
||||
} while (nextBatch);
|
||||
|
||||
const pollResults = buildPollResultsSummary({
|
||||
pollEventId,
|
||||
roomId,
|
||||
sender: pollSummary.sender,
|
||||
senderName: pollSummary.senderName,
|
||||
content: pollStartContent,
|
||||
relationEvents,
|
||||
const pollSnapshot = await fetchMatrixPollSnapshot(client, roomId, event).catch((err) => {
|
||||
logVerboseMessage(
|
||||
`matrix: failed resolving poll snapshot room=${roomId} id=${event.event_id ?? "unknown"}: ${String(err)}`,
|
||||
);
|
||||
return null;
|
||||
});
|
||||
const pollText = pollResults
|
||||
? formatPollResultsAsText(pollResults)
|
||||
: formatPollAsText(pollSummary);
|
||||
if (!pollSnapshot) {
|
||||
return;
|
||||
}
|
||||
content = {
|
||||
msgtype: "m.text",
|
||||
body: pollText,
|
||||
body: pollSnapshot.text,
|
||||
} as unknown as RoomMessageEventContent;
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ import { setActiveMatrixClient } from "../active-client.js";
|
||||
import {
|
||||
isBunRuntime,
|
||||
resolveMatrixAuth,
|
||||
resolveMatrixAuthContext,
|
||||
resolveSharedMatrixClient,
|
||||
stopSharedClientForAccount,
|
||||
} from "../client.js";
|
||||
@@ -137,8 +138,14 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi
|
||||
return allowList.map(String);
|
||||
};
|
||||
|
||||
const authContext = resolveMatrixAuthContext({
|
||||
cfg,
|
||||
accountId: opts.accountId,
|
||||
});
|
||||
const effectiveAccountId = authContext.accountId;
|
||||
|
||||
// Resolve account-specific config for multi-account support
|
||||
const account = resolveMatrixAccount({ cfg, accountId: opts.accountId });
|
||||
const account = resolveMatrixAccount({ cfg, accountId: effectiveAccountId });
|
||||
const accountConfig = account.config;
|
||||
|
||||
const allowlistOnly = accountConfig.allowlistOnly === true;
|
||||
@@ -239,7 +246,7 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi
|
||||
},
|
||||
};
|
||||
|
||||
const auth = await resolveMatrixAuth({ cfg, accountId: opts.accountId });
|
||||
const auth = await resolveMatrixAuth({ cfg, accountId: effectiveAccountId });
|
||||
const resolvedInitialSyncLimit =
|
||||
typeof opts.initialSyncLimit === "number"
|
||||
? Math.max(0, Math.floor(opts.initialSyncLimit))
|
||||
@@ -252,9 +259,9 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi
|
||||
cfg,
|
||||
auth: authWithLimit,
|
||||
startClient: false,
|
||||
accountId: opts.accountId ?? undefined,
|
||||
accountId: auth.accountId,
|
||||
});
|
||||
setActiveMatrixClient(client, opts.accountId);
|
||||
setActiveMatrixClient(client, auth.accountId);
|
||||
|
||||
const mentionRegexes = core.channel.mentions.buildMentionRegexes(cfg);
|
||||
const defaultGroupPolicy = resolveDefaultGroupPolicy(cfg);
|
||||
@@ -339,7 +346,7 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi
|
||||
await resolveSharedMatrixClient({
|
||||
cfg,
|
||||
auth: authWithLimit,
|
||||
accountId: opts.accountId,
|
||||
accountId: auth.accountId,
|
||||
});
|
||||
logVerboseMessage("matrix: client started");
|
||||
const threadBindingManager = await createMatrixThreadBindingManager({
|
||||
@@ -398,7 +405,6 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi
|
||||
client,
|
||||
auth,
|
||||
accountConfig,
|
||||
accountId: account.accountId,
|
||||
env: process.env,
|
||||
});
|
||||
if (startupVerification.kind === "verified") {
|
||||
@@ -440,7 +446,6 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi
|
||||
const legacyCryptoRestore = await maybeRestoreLegacyMatrixBackup({
|
||||
client,
|
||||
auth,
|
||||
accountId: account.accountId,
|
||||
env: process.env,
|
||||
});
|
||||
if (legacyCryptoRestore.kind === "restored") {
|
||||
@@ -474,9 +479,9 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi
|
||||
try {
|
||||
threadBindingManager.stop();
|
||||
logVerboseMessage("matrix: stopping client");
|
||||
stopSharedClientForAccount(auth, opts.accountId);
|
||||
stopSharedClientForAccount(auth);
|
||||
} finally {
|
||||
setActiveMatrixClient(null, opts.accountId);
|
||||
setActiveMatrixClient(null, auth.accountId);
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -27,6 +27,7 @@ describe("maybeRestoreLegacyMatrixBackup", () => {
|
||||
await withTempHome(async (home) => {
|
||||
const stateDir = path.join(home, ".openclaw");
|
||||
const auth = {
|
||||
accountId: "default",
|
||||
homeserver: "https://matrix.example.org",
|
||||
userId: "@bot:example.org",
|
||||
accessToken: "tok-123",
|
||||
@@ -89,6 +90,7 @@ describe("maybeRestoreLegacyMatrixBackup", () => {
|
||||
await withTempHome(async (home) => {
|
||||
const stateDir = path.join(home, ".openclaw");
|
||||
const auth = {
|
||||
accountId: "default",
|
||||
homeserver: "https://matrix.example.org",
|
||||
userId: "@bot:example.org",
|
||||
accessToken: "tok-123",
|
||||
|
||||
@@ -45,8 +45,7 @@ function isMigrationState(value: unknown): value is MatrixLegacyCryptoMigrationS
|
||||
|
||||
export async function maybeRestoreLegacyMatrixBackup(params: {
|
||||
client: Pick<MatrixClient, "restoreRoomKeyBackup">;
|
||||
auth: Pick<MatrixAuth, "homeserver" | "userId" | "accessToken">;
|
||||
accountId?: string | null;
|
||||
auth: Pick<MatrixAuth, "homeserver" | "userId" | "accessToken" | "accountId">;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
stateDir?: string;
|
||||
}): Promise<MatrixLegacyCryptoRestoreResult> {
|
||||
@@ -57,7 +56,7 @@ export async function maybeRestoreLegacyMatrixBackup(params: {
|
||||
homeserver: params.auth.homeserver,
|
||||
userId: params.auth.userId,
|
||||
accessToken: params.auth.accessToken,
|
||||
accountId: params.accountId,
|
||||
accountId: params.auth.accountId,
|
||||
});
|
||||
const statePath = path.join(rootDir, "legacy-crypto-migration.json");
|
||||
const { value } = await readJsonFileWithFallback<MatrixLegacyCryptoMigrationState | null>(
|
||||
|
||||
@@ -12,6 +12,16 @@ function createStateFilePath(rootDir: string): string {
|
||||
return path.join(rootDir, "startup-verification.json");
|
||||
}
|
||||
|
||||
function createAuth(accountId = "default") {
|
||||
return {
|
||||
accountId,
|
||||
homeserver: "https://matrix.example.org",
|
||||
userId: "@bot:example.org",
|
||||
accessToken: "token",
|
||||
encryption: true,
|
||||
};
|
||||
}
|
||||
|
||||
type VerificationSummaryLike = {
|
||||
id: string;
|
||||
transactionId?: string;
|
||||
@@ -80,12 +90,7 @@ describe("ensureMatrixStartupVerification", () => {
|
||||
|
||||
const result = await ensureMatrixStartupVerification({
|
||||
client: harness.client as never,
|
||||
auth: {
|
||||
homeserver: "https://matrix.example.org",
|
||||
userId: "@bot:example.org",
|
||||
accessToken: "token",
|
||||
encryption: true,
|
||||
},
|
||||
auth: createAuth(),
|
||||
accountConfig: {},
|
||||
stateFilePath: createStateFilePath(tempHome),
|
||||
});
|
||||
@@ -105,12 +110,7 @@ describe("ensureMatrixStartupVerification", () => {
|
||||
|
||||
const result = await ensureMatrixStartupVerification({
|
||||
client: harness.client as never,
|
||||
auth: {
|
||||
homeserver: "https://matrix.example.org",
|
||||
userId: "@bot:example.org",
|
||||
accessToken: "token",
|
||||
encryption: true,
|
||||
},
|
||||
auth: createAuth(),
|
||||
accountConfig: {},
|
||||
stateFilePath: createStateFilePath(tempHome),
|
||||
});
|
||||
@@ -135,12 +135,7 @@ describe("ensureMatrixStartupVerification", () => {
|
||||
|
||||
const result = await ensureMatrixStartupVerification({
|
||||
client: harness.client as never,
|
||||
auth: {
|
||||
homeserver: "https://matrix.example.org",
|
||||
userId: "@bot:example.org",
|
||||
accessToken: "token",
|
||||
encryption: true,
|
||||
},
|
||||
auth: createAuth(),
|
||||
accountConfig: {},
|
||||
stateFilePath: createStateFilePath(tempHome),
|
||||
});
|
||||
@@ -156,12 +151,7 @@ describe("ensureMatrixStartupVerification", () => {
|
||||
const harness = createHarness();
|
||||
await ensureMatrixStartupVerification({
|
||||
client: harness.client as never,
|
||||
auth: {
|
||||
homeserver: "https://matrix.example.org",
|
||||
userId: "@bot:example.org",
|
||||
accessToken: "token",
|
||||
encryption: true,
|
||||
},
|
||||
auth: createAuth(),
|
||||
accountConfig: {},
|
||||
stateFilePath: createStateFilePath(tempHome),
|
||||
nowMs: Date.now(),
|
||||
@@ -170,12 +160,7 @@ describe("ensureMatrixStartupVerification", () => {
|
||||
|
||||
const second = await ensureMatrixStartupVerification({
|
||||
client: harness.client as never,
|
||||
auth: {
|
||||
homeserver: "https://matrix.example.org",
|
||||
userId: "@bot:example.org",
|
||||
accessToken: "token",
|
||||
encryption: true,
|
||||
},
|
||||
auth: createAuth(),
|
||||
accountConfig: {},
|
||||
stateFilePath: createStateFilePath(tempHome),
|
||||
nowMs: Date.now() + 60_000,
|
||||
@@ -193,12 +178,7 @@ describe("ensureMatrixStartupVerification", () => {
|
||||
|
||||
const result = await ensureMatrixStartupVerification({
|
||||
client: harness.client as never,
|
||||
auth: {
|
||||
homeserver: "https://matrix.example.org",
|
||||
userId: "@bot:example.org",
|
||||
accessToken: "token",
|
||||
encryption: true,
|
||||
},
|
||||
auth: createAuth(),
|
||||
accountConfig: {
|
||||
startupVerification: "off",
|
||||
},
|
||||
@@ -216,12 +196,7 @@ describe("ensureMatrixStartupVerification", () => {
|
||||
|
||||
const result = await ensureMatrixStartupVerification({
|
||||
client: harness.client as never,
|
||||
auth: {
|
||||
homeserver: "https://matrix.example.org",
|
||||
userId: "@bot:example.org",
|
||||
accessToken: "token",
|
||||
encryption: true,
|
||||
},
|
||||
auth: createAuth(),
|
||||
accountConfig: {},
|
||||
stateFilePath: createStateFilePath(tempHome),
|
||||
nowMs: Date.parse("2026-03-08T12:00:00.000Z"),
|
||||
@@ -243,12 +218,7 @@ describe("ensureMatrixStartupVerification", () => {
|
||||
|
||||
const result = await ensureMatrixStartupVerification({
|
||||
client: harness.client as never,
|
||||
auth: {
|
||||
homeserver: "https://matrix.example.org",
|
||||
userId: "@bot:example.org",
|
||||
accessToken: "token",
|
||||
encryption: true,
|
||||
},
|
||||
auth: createAuth(),
|
||||
accountConfig: {},
|
||||
stateFilePath: createStateFilePath(tempHome),
|
||||
});
|
||||
@@ -261,12 +231,7 @@ describe("ensureMatrixStartupVerification", () => {
|
||||
|
||||
const cooledDown = await ensureMatrixStartupVerification({
|
||||
client: harness.client as never,
|
||||
auth: {
|
||||
homeserver: "https://matrix.example.org",
|
||||
userId: "@bot:example.org",
|
||||
accessToken: "token",
|
||||
encryption: true,
|
||||
},
|
||||
auth: createAuth(),
|
||||
accountConfig: {},
|
||||
stateFilePath: createStateFilePath(tempHome),
|
||||
nowMs: Date.now() + 60_000,
|
||||
@@ -286,12 +251,7 @@ describe("ensureMatrixStartupVerification", () => {
|
||||
|
||||
await ensureMatrixStartupVerification({
|
||||
client: failingHarness.client as never,
|
||||
auth: {
|
||||
homeserver: "https://matrix.example.org",
|
||||
userId: "@bot:example.org",
|
||||
accessToken: "token",
|
||||
encryption: true,
|
||||
},
|
||||
auth: createAuth(),
|
||||
accountConfig: {},
|
||||
stateFilePath,
|
||||
nowMs: Date.parse("2026-03-08T12:00:00.000Z"),
|
||||
@@ -300,12 +260,7 @@ describe("ensureMatrixStartupVerification", () => {
|
||||
const retryingHarness = createHarness();
|
||||
const result = await ensureMatrixStartupVerification({
|
||||
client: retryingHarness.client as never,
|
||||
auth: {
|
||||
homeserver: "https://matrix.example.org",
|
||||
userId: "@bot:example.org",
|
||||
accessToken: "token",
|
||||
encryption: true,
|
||||
},
|
||||
auth: createAuth(),
|
||||
accountConfig: {},
|
||||
stateFilePath,
|
||||
nowMs: Date.parse("2026-03-08T13:30:00.000Z"),
|
||||
@@ -322,12 +277,7 @@ describe("ensureMatrixStartupVerification", () => {
|
||||
|
||||
await ensureMatrixStartupVerification({
|
||||
client: unverified.client as never,
|
||||
auth: {
|
||||
homeserver: "https://matrix.example.org",
|
||||
userId: "@bot:example.org",
|
||||
accessToken: "token",
|
||||
encryption: true,
|
||||
},
|
||||
auth: createAuth(),
|
||||
accountConfig: {},
|
||||
stateFilePath,
|
||||
nowMs: Date.parse("2026-03-08T12:00:00.000Z"),
|
||||
@@ -338,12 +288,7 @@ describe("ensureMatrixStartupVerification", () => {
|
||||
const verified = createHarness({ verified: true });
|
||||
const result = await ensureMatrixStartupVerification({
|
||||
client: verified.client as never,
|
||||
auth: {
|
||||
homeserver: "https://matrix.example.org",
|
||||
userId: "@bot:example.org",
|
||||
accessToken: "token",
|
||||
encryption: true,
|
||||
},
|
||||
auth: createAuth(),
|
||||
accountConfig: {},
|
||||
stateFilePath,
|
||||
});
|
||||
|
||||
@@ -44,14 +44,13 @@ function normalizeCooldownHours(value: number | undefined): number {
|
||||
|
||||
function resolveStartupVerificationStatePath(params: {
|
||||
auth: MatrixAuth;
|
||||
accountId?: string | null;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}): string {
|
||||
const storagePaths = resolveMatrixStoragePaths({
|
||||
homeserver: params.auth.homeserver,
|
||||
userId: params.auth.userId,
|
||||
accessToken: params.auth.accessToken,
|
||||
accountId: params.accountId,
|
||||
accountId: params.auth.accountId,
|
||||
env: params.env,
|
||||
});
|
||||
return path.join(storagePaths.rootDir, STARTUP_VERIFICATION_STATE_FILENAME);
|
||||
@@ -143,7 +142,6 @@ export async function ensureMatrixStartupVerification(params: {
|
||||
client: Pick<MatrixClient, "crypto" | "getOwnDeviceVerificationStatus">;
|
||||
auth: MatrixAuth;
|
||||
accountConfig: Pick<MatrixConfig, "startupVerification" | "startupVerificationCooldownHours">;
|
||||
accountId?: string | null;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
nowMs?: number;
|
||||
stateFilePath?: string;
|
||||
@@ -157,7 +155,6 @@ export async function ensureMatrixStartupVerification(params: {
|
||||
params.stateFilePath ??
|
||||
resolveStartupVerificationStatePath({
|
||||
auth: params.auth,
|
||||
accountId: params.accountId,
|
||||
env: params.env,
|
||||
});
|
||||
|
||||
|
||||
110
extensions/matrix/src/matrix/poll-summary.ts
Normal file
110
extensions/matrix/src/matrix/poll-summary.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import type { MatrixMessageSummary } from "./actions/types.js";
|
||||
import {
|
||||
buildPollResultsSummary,
|
||||
formatPollAsText,
|
||||
formatPollResultsAsText,
|
||||
isPollEventType,
|
||||
isPollStartType,
|
||||
parsePollStartContent,
|
||||
resolvePollReferenceEventId,
|
||||
type PollStartContent,
|
||||
} from "./poll-types.js";
|
||||
import type { MatrixClient, MatrixRawEvent } from "./sdk.js";
|
||||
|
||||
export type MatrixPollSnapshot = {
|
||||
pollEventId: string;
|
||||
triggerEvent: MatrixRawEvent;
|
||||
rootEvent: MatrixRawEvent;
|
||||
text: string;
|
||||
};
|
||||
|
||||
export function resolveMatrixPollRootEventId(
|
||||
event: Pick<MatrixRawEvent, "event_id" | "type" | "content">,
|
||||
): string | null {
|
||||
if (isPollStartType(event.type)) {
|
||||
const eventId = event.event_id?.trim();
|
||||
return eventId ? eventId : null;
|
||||
}
|
||||
return resolvePollReferenceEventId(event.content);
|
||||
}
|
||||
|
||||
async function readAllPollRelations(
|
||||
client: MatrixClient,
|
||||
roomId: string,
|
||||
pollEventId: string,
|
||||
): Promise<MatrixRawEvent[]> {
|
||||
const relationEvents: MatrixRawEvent[] = [];
|
||||
let nextBatch: string | undefined;
|
||||
do {
|
||||
const page = await client.getRelations(roomId, pollEventId, "m.reference", undefined, {
|
||||
from: nextBatch,
|
||||
});
|
||||
relationEvents.push(...page.events);
|
||||
nextBatch = page.nextBatch ?? undefined;
|
||||
} while (nextBatch);
|
||||
return relationEvents;
|
||||
}
|
||||
|
||||
export async function fetchMatrixPollSnapshot(
|
||||
client: MatrixClient,
|
||||
roomId: string,
|
||||
event: MatrixRawEvent,
|
||||
): Promise<MatrixPollSnapshot | null> {
|
||||
if (!isPollEventType(event.type)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const pollEventId = resolveMatrixPollRootEventId(event);
|
||||
if (!pollEventId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const rootEvent = isPollStartType(event.type)
|
||||
? event
|
||||
: ((await client.getEvent(roomId, pollEventId)) as MatrixRawEvent);
|
||||
if (!isPollStartType(rootEvent.type)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const pollStartContent = rootEvent.content as PollStartContent;
|
||||
const pollSummary = parsePollStartContent(pollStartContent);
|
||||
if (!pollSummary) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const relationEvents = await readAllPollRelations(client, roomId, pollEventId);
|
||||
const pollResults = buildPollResultsSummary({
|
||||
pollEventId,
|
||||
roomId,
|
||||
sender: rootEvent.sender,
|
||||
senderName: rootEvent.sender,
|
||||
content: pollStartContent,
|
||||
relationEvents,
|
||||
});
|
||||
|
||||
return {
|
||||
pollEventId,
|
||||
triggerEvent: event,
|
||||
rootEvent,
|
||||
text: pollResults ? formatPollResultsAsText(pollResults) : formatPollAsText(pollSummary),
|
||||
};
|
||||
}
|
||||
|
||||
export async function fetchMatrixPollMessageSummary(
|
||||
client: MatrixClient,
|
||||
roomId: string,
|
||||
event: MatrixRawEvent,
|
||||
): Promise<MatrixMessageSummary | null> {
|
||||
const snapshot = await fetchMatrixPollSnapshot(client, roomId, event);
|
||||
if (!snapshot) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
eventId: snapshot.pollEventId,
|
||||
sender: snapshot.rootEvent.sender,
|
||||
body: snapshot.text,
|
||||
msgtype: "m.text",
|
||||
timestamp: snapshot.triggerEvent.origin_server_ts || snapshot.rootEvent.origin_server_ts,
|
||||
};
|
||||
}
|
||||
@@ -322,7 +322,9 @@ export function buildPollResultsSummary(params: {
|
||||
});
|
||||
}
|
||||
|
||||
const voteCounts = new Map(parsed.answers.map((answer) => [answer.id, 0] as const));
|
||||
const voteCounts = new Map<string, number>(
|
||||
parsed.answers.map((answer): [string, number] => [answer.id, 0]),
|
||||
);
|
||||
let totalVotes = 0;
|
||||
for (const latestVote of latestVoteBySender.values()) {
|
||||
if (latestVote.answerIds.length === 0) {
|
||||
|
||||
@@ -50,4 +50,22 @@ describe("probeMatrix", () => {
|
||||
localTimeoutMs: 500,
|
||||
});
|
||||
});
|
||||
|
||||
it("passes accountId through to client creation", async () => {
|
||||
await probeMatrix({
|
||||
homeserver: "https://matrix.example.org",
|
||||
accessToken: "tok",
|
||||
userId: "@bot:example.org",
|
||||
timeoutMs: 500,
|
||||
accountId: "ops",
|
||||
});
|
||||
|
||||
expect(createMatrixClientMock).toHaveBeenCalledWith({
|
||||
homeserver: "https://matrix.example.org",
|
||||
userId: "@bot:example.org",
|
||||
accessToken: "tok",
|
||||
localTimeoutMs: 500,
|
||||
accountId: "ops",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,6 +12,7 @@ export async function probeMatrix(params: {
|
||||
accessToken: string;
|
||||
userId?: string;
|
||||
timeoutMs: number;
|
||||
accountId?: string | null;
|
||||
}): Promise<MatrixProbe> {
|
||||
const started = Date.now();
|
||||
const result: MatrixProbe = {
|
||||
@@ -48,6 +49,7 @@ export async function probeMatrix(params: {
|
||||
userId: inputUserId,
|
||||
accessToken: params.accessToken,
|
||||
localTimeoutMs: params.timeoutMs,
|
||||
accountId: params.accountId,
|
||||
});
|
||||
// The client wrapper resolves user ID via whoami when needed.
|
||||
const userId = await client.getUserId();
|
||||
|
||||
@@ -579,6 +579,25 @@ export class MatrixClient {
|
||||
};
|
||||
}
|
||||
|
||||
async hydrateEvents(
|
||||
roomId: string,
|
||||
events: Array<Record<string, unknown>>,
|
||||
): Promise<MatrixRawEvent[]> {
|
||||
if (events.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const mapper = this.client.getEventMapper();
|
||||
const mappedEvents = events.map((event) =>
|
||||
mapper({
|
||||
room_id: roomId,
|
||||
...event,
|
||||
}),
|
||||
);
|
||||
await Promise.all(mappedEvents.map((event) => this.client.decryptEventIfNeeded(event)));
|
||||
return mappedEvents.map((event) => matrixEventToRaw(event));
|
||||
}
|
||||
|
||||
async setTyping(roomId: string, typing: boolean, timeoutMs: number): Promise<void> {
|
||||
await this.client.sendTyping(roomId, typing, timeoutMs);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ const getActiveMatrixClientMock = vi.fn();
|
||||
const createMatrixClientMock = vi.fn();
|
||||
const isBunRuntimeMock = vi.fn(() => false);
|
||||
const resolveMatrixAuthMock = vi.fn();
|
||||
const resolveMatrixAuthContextMock = vi.fn();
|
||||
const getMatrixRuntimeMock = vi.fn();
|
||||
|
||||
vi.mock("../active-client.js", () => ({
|
||||
getActiveMatrixClient: (...args: unknown[]) => getActiveMatrixClientMock(...args),
|
||||
@@ -14,6 +16,11 @@ vi.mock("../client.js", () => ({
|
||||
createMatrixClient: (...args: unknown[]) => createMatrixClientMock(...args),
|
||||
isBunRuntime: () => isBunRuntimeMock(),
|
||||
resolveMatrixAuth: (...args: unknown[]) => resolveMatrixAuthMock(...args),
|
||||
resolveMatrixAuthContext: (...args: unknown[]) => resolveMatrixAuthContextMock(...args),
|
||||
}));
|
||||
|
||||
vi.mock("../../runtime.js", () => ({
|
||||
getMatrixRuntime: () => getMatrixRuntimeMock(),
|
||||
}));
|
||||
|
||||
let resolveMatrixClient: typeof import("./client.js").resolveMatrixClient;
|
||||
@@ -30,7 +37,19 @@ describe("resolveMatrixClient", () => {
|
||||
vi.clearAllMocks();
|
||||
getActiveMatrixClientMock.mockReturnValue(null);
|
||||
isBunRuntimeMock.mockReturnValue(false);
|
||||
getMatrixRuntimeMock.mockReturnValue({
|
||||
config: {
|
||||
loadConfig: () => ({}),
|
||||
},
|
||||
});
|
||||
resolveMatrixAuthContextMock.mockReturnValue({
|
||||
cfg: {},
|
||||
env: process.env,
|
||||
accountId: "default",
|
||||
resolved: {},
|
||||
});
|
||||
resolveMatrixAuthMock.mockResolvedValue({
|
||||
accountId: "default",
|
||||
homeserver: "https://matrix.example.org",
|
||||
userId: "@bot:example.org",
|
||||
accessToken: "token",
|
||||
@@ -75,4 +94,35 @@ describe("resolveMatrixClient", () => {
|
||||
expect(resolveMatrixAuthMock).not.toHaveBeenCalled();
|
||||
expect(createMatrixClientMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("uses the effective account id when auth resolution is implicit", async () => {
|
||||
resolveMatrixAuthContextMock.mockReturnValue({
|
||||
cfg: {},
|
||||
env: process.env,
|
||||
accountId: "ops",
|
||||
resolved: {},
|
||||
});
|
||||
resolveMatrixAuthMock.mockResolvedValue({
|
||||
accountId: "ops",
|
||||
homeserver: "https://matrix.example.org",
|
||||
userId: "@bot:example.org",
|
||||
accessToken: "token",
|
||||
password: undefined,
|
||||
deviceId: "DEVICE123",
|
||||
encryption: false,
|
||||
});
|
||||
|
||||
await resolveMatrixClient({});
|
||||
|
||||
expect(getActiveMatrixClientMock).toHaveBeenCalledWith("ops");
|
||||
expect(resolveMatrixAuthMock).toHaveBeenCalledWith({
|
||||
cfg: {},
|
||||
accountId: "ops",
|
||||
});
|
||||
expect(createMatrixClientMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
accountId: "ops",
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,7 +2,12 @@ import { getMatrixRuntime } from "../../runtime.js";
|
||||
import type { CoreConfig } from "../../types.js";
|
||||
import { resolveMatrixAccountConfig } from "../accounts.js";
|
||||
import { getActiveMatrixClient } from "../active-client.js";
|
||||
import { createMatrixClient, isBunRuntime, resolveMatrixAuth } from "../client.js";
|
||||
import {
|
||||
createMatrixClient,
|
||||
isBunRuntime,
|
||||
resolveMatrixAuth,
|
||||
resolveMatrixAuthContext,
|
||||
} from "../client.js";
|
||||
import type { MatrixClient } from "../sdk.js";
|
||||
|
||||
const getCore = () => getMatrixRuntime();
|
||||
@@ -32,11 +37,19 @@ export async function resolveMatrixClient(opts: {
|
||||
if (opts.client) {
|
||||
return { client: opts.client, stopOnDone: false };
|
||||
}
|
||||
const active = getActiveMatrixClient(opts.accountId);
|
||||
const cfg = getCore().config.loadConfig() as CoreConfig;
|
||||
const authContext = resolveMatrixAuthContext({
|
||||
cfg,
|
||||
accountId: opts.accountId,
|
||||
});
|
||||
const active = getActiveMatrixClient(authContext.accountId);
|
||||
if (active) {
|
||||
return { client: active, stopOnDone: false };
|
||||
}
|
||||
const auth = await resolveMatrixAuth({ accountId: opts.accountId });
|
||||
const auth = await resolveMatrixAuth({
|
||||
cfg,
|
||||
accountId: authContext.accountId,
|
||||
});
|
||||
const client = await createMatrixClient({
|
||||
homeserver: auth.homeserver,
|
||||
userId: auth.userId,
|
||||
@@ -45,7 +58,7 @@ export async function resolveMatrixClient(opts: {
|
||||
deviceId: auth.deviceId,
|
||||
encryption: auth.encryption,
|
||||
localTimeoutMs: opts.timeoutMs,
|
||||
accountId: opts.accountId,
|
||||
accountId: auth.accountId,
|
||||
autoBootstrapCrypto: false,
|
||||
});
|
||||
await client.prepareForOneOff();
|
||||
|
||||
@@ -47,6 +47,7 @@ describe("matrix thread bindings", () => {
|
||||
await createMatrixThreadBindingManager({
|
||||
accountId: "ops",
|
||||
auth: {
|
||||
accountId: "ops",
|
||||
homeserver: "https://matrix.example.org",
|
||||
userId: "@bot:example.org",
|
||||
accessToken: "token",
|
||||
@@ -87,6 +88,7 @@ describe("matrix thread bindings", () => {
|
||||
await createMatrixThreadBindingManager({
|
||||
accountId: "ops",
|
||||
auth: {
|
||||
accountId: "ops",
|
||||
homeserver: "https://matrix.example.org",
|
||||
userId: "@bot:example.org",
|
||||
accessToken: "token",
|
||||
@@ -137,6 +139,7 @@ describe("matrix thread bindings", () => {
|
||||
await createMatrixThreadBindingManager({
|
||||
accountId: "ops",
|
||||
auth: {
|
||||
accountId: "ops",
|
||||
homeserver: "https://matrix.example.org",
|
||||
userId: "@bot:example.org",
|
||||
accessToken: "token",
|
||||
@@ -182,6 +185,7 @@ describe("matrix thread bindings", () => {
|
||||
await createMatrixThreadBindingManager({
|
||||
accountId: "ops",
|
||||
auth: {
|
||||
accountId: "ops",
|
||||
homeserver: "https://matrix.example.org",
|
||||
userId: "@bot:example.org",
|
||||
accessToken: "token",
|
||||
|
||||
@@ -339,6 +339,11 @@ export async function createMatrixThreadBindingManager(params: {
|
||||
enableSweeper?: boolean;
|
||||
logVerboseMessage?: (message: string) => void;
|
||||
}): Promise<MatrixThreadBindingManager> {
|
||||
if (params.auth.accountId !== params.accountId) {
|
||||
throw new Error(
|
||||
`Matrix thread binding account mismatch: requested ${params.accountId}, auth resolved ${params.auth.accountId}`,
|
||||
);
|
||||
}
|
||||
const existing = MANAGERS_BY_ACCOUNT_ID.get(params.accountId);
|
||||
if (existing) {
|
||||
return existing;
|
||||
|
||||
@@ -52,6 +52,7 @@ export async function notifyPairingApproved(params: {
|
||||
channelId: ChannelId;
|
||||
id: string;
|
||||
cfg: OpenClawConfig;
|
||||
accountId?: string;
|
||||
runtime?: RuntimeEnv;
|
||||
/** Extension channels can pass their adapter directly to bypass registry lookup. */
|
||||
pairingAdapter?: ChannelPairingAdapter;
|
||||
@@ -64,6 +65,7 @@ export async function notifyPairingApproved(params: {
|
||||
await adapter.notifyApproval({
|
||||
cfg: params.cfg,
|
||||
id: params.id,
|
||||
accountId: params.accountId,
|
||||
runtime: params.runtime,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -268,6 +268,7 @@ export type ChannelPairingAdapter = {
|
||||
notifyApproval?: (params: {
|
||||
cfg: OpenClawConfig;
|
||||
id: string;
|
||||
accountId?: string;
|
||||
runtime?: RuntimeEnv;
|
||||
}) => Promise<void>;
|
||||
};
|
||||
|
||||
@@ -220,6 +220,28 @@ describe("pairing cli", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("forwards --account to approval notifications", async () => {
|
||||
mockApprovedPairing();
|
||||
|
||||
await runPairing([
|
||||
"pairing",
|
||||
"approve",
|
||||
"--channel",
|
||||
"telegram",
|
||||
"--account",
|
||||
"yy",
|
||||
"--notify",
|
||||
"ABCDEFGH",
|
||||
]);
|
||||
|
||||
expect(notifyPairingApproved).toHaveBeenCalledWith({
|
||||
channelId: "telegram",
|
||||
id: "123",
|
||||
cfg: {},
|
||||
accountId: "yy",
|
||||
});
|
||||
});
|
||||
|
||||
it("defaults approve to the sole available channel when only code is provided", async () => {
|
||||
listPairingChannels.mockReturnValueOnce(["slack"]);
|
||||
mockApprovedPairing();
|
||||
|
||||
@@ -44,9 +44,9 @@ function parseChannel(raw: unknown, channels: PairingChannel[]): PairingChannel
|
||||
throw new Error(`Invalid channel: ${value}`);
|
||||
}
|
||||
|
||||
async function notifyApproved(channel: PairingChannel, id: string) {
|
||||
async function notifyApproved(channel: PairingChannel, id: string, accountId?: string) {
|
||||
const cfg = loadConfig();
|
||||
await notifyPairingApproved({ channelId: channel, id, cfg });
|
||||
await notifyPairingApproved({ channelId: channel, id, cfg, accountId });
|
||||
}
|
||||
|
||||
export function registerPairingCli(program: Command) {
|
||||
@@ -166,7 +166,7 @@ export function registerPairingCli(program: Command) {
|
||||
if (!opts.notify) {
|
||||
return;
|
||||
}
|
||||
await notifyApproved(channel, approved.id).catch((err) => {
|
||||
await notifyApproved(channel, approved.id, accountId || undefined).catch((err) => {
|
||||
defaultRuntime.log(theme.warn(`Failed to notify requester: ${String(err)}`));
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user