mirror of https://github.com/openclaw/openclaw.git
poMatrix: scope onboarding config to selected account
This commit is contained in:
parent
af2ac1d536
commit
3f7cf9d3a4
|
|
@ -1,2 +1,3 @@
|
|||
export * from "./src/setup-core.js";
|
||||
export * from "./src/setup-surface.js";
|
||||
export { matrixOnboardingAdapter as matrixSetupWizard } from "./src/onboarding.js";
|
||||
|
|
|
|||
|
|
@ -47,4 +47,58 @@ describe("updateMatrixAccountConfig", () => {
|
|||
enabled: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("updates nested access config for named accounts without touching top-level defaults", () => {
|
||||
const cfg = {
|
||||
channels: {
|
||||
matrix: {
|
||||
dm: {
|
||||
policy: "pairing",
|
||||
},
|
||||
groups: {
|
||||
"!default:example.org": { allow: true },
|
||||
},
|
||||
accounts: {
|
||||
ops: {
|
||||
homeserver: "https://matrix.ops.example.org",
|
||||
accessToken: "ops-token",
|
||||
dm: {
|
||||
enabled: true,
|
||||
policy: "pairing",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as CoreConfig;
|
||||
|
||||
const updated = updateMatrixAccountConfig(cfg, "ops", {
|
||||
dm: {
|
||||
policy: "allowlist",
|
||||
allowFrom: ["@alice:example.org"],
|
||||
},
|
||||
groupPolicy: "allowlist",
|
||||
groups: {
|
||||
"!ops-room:example.org": { allow: true },
|
||||
},
|
||||
rooms: null,
|
||||
});
|
||||
|
||||
expect(updated.channels?.["matrix"]?.dm?.policy).toBe("pairing");
|
||||
expect(updated.channels?.["matrix"]?.groups).toEqual({
|
||||
"!default:example.org": { allow: true },
|
||||
});
|
||||
expect(updated.channels?.["matrix"]?.accounts?.ops).toMatchObject({
|
||||
dm: {
|
||||
enabled: true,
|
||||
policy: "allowlist",
|
||||
allowFrom: ["@alice:example.org"],
|
||||
},
|
||||
groupPolicy: "allowlist",
|
||||
groups: {
|
||||
"!ops-room:example.org": { allow: true },
|
||||
},
|
||||
});
|
||||
expect(updated.channels?.["matrix"]?.accounts?.ops?.rooms).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -13,6 +13,11 @@ export type MatrixAccountPatch = {
|
|||
avatarUrl?: string | null;
|
||||
encryption?: boolean | null;
|
||||
initialSyncLimit?: number | null;
|
||||
dm?: MatrixConfig["dm"] | null;
|
||||
groupPolicy?: MatrixConfig["groupPolicy"] | null;
|
||||
groupAllowFrom?: MatrixConfig["groupAllowFrom"] | null;
|
||||
groups?: MatrixConfig["groups"] | null;
|
||||
rooms?: MatrixConfig["rooms"] | null;
|
||||
};
|
||||
|
||||
function applyNullableStringField(
|
||||
|
|
@ -35,6 +40,42 @@ function applyNullableStringField(
|
|||
target[key] = trimmed;
|
||||
}
|
||||
|
||||
function cloneMatrixDmConfig(dm: MatrixConfig["dm"]): MatrixConfig["dm"] {
|
||||
if (!dm) {
|
||||
return dm;
|
||||
}
|
||||
return {
|
||||
...dm,
|
||||
...(dm.allowFrom ? { allowFrom: [...dm.allowFrom] } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
function cloneMatrixRoomMap(
|
||||
rooms: MatrixConfig["groups"] | MatrixConfig["rooms"],
|
||||
): MatrixConfig["groups"] | MatrixConfig["rooms"] {
|
||||
if (!rooms) {
|
||||
return rooms;
|
||||
}
|
||||
return Object.fromEntries(
|
||||
Object.entries(rooms).map(([roomId, roomCfg]) => [roomId, roomCfg ? { ...roomCfg } : roomCfg]),
|
||||
);
|
||||
}
|
||||
|
||||
function applyNullableArrayField(
|
||||
target: Record<string, unknown>,
|
||||
key: keyof MatrixAccountPatch,
|
||||
value: Array<string | number> | null | undefined,
|
||||
): void {
|
||||
if (value === undefined) {
|
||||
return;
|
||||
}
|
||||
if (value === null) {
|
||||
delete target[key];
|
||||
return;
|
||||
}
|
||||
target[key] = [...value];
|
||||
}
|
||||
|
||||
export function shouldStoreMatrixAccountAtTopLevel(cfg: CoreConfig, accountId: string): boolean {
|
||||
const normalizedAccountId = normalizeAccountId(accountId);
|
||||
if (normalizedAccountId !== DEFAULT_ACCOUNT_ID) {
|
||||
|
|
@ -103,6 +144,38 @@ export function updateMatrixAccountConfig(
|
|||
nextAccount.encryption = patch.encryption;
|
||||
}
|
||||
}
|
||||
if (patch.dm !== undefined) {
|
||||
if (patch.dm === null) {
|
||||
delete nextAccount.dm;
|
||||
} else {
|
||||
nextAccount.dm = cloneMatrixDmConfig({
|
||||
...((nextAccount.dm as MatrixConfig["dm"] | undefined) ?? {}),
|
||||
...patch.dm,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (patch.groupPolicy !== undefined) {
|
||||
if (patch.groupPolicy === null) {
|
||||
delete nextAccount.groupPolicy;
|
||||
} else {
|
||||
nextAccount.groupPolicy = patch.groupPolicy;
|
||||
}
|
||||
}
|
||||
applyNullableArrayField(nextAccount, "groupAllowFrom", patch.groupAllowFrom);
|
||||
if (patch.groups !== undefined) {
|
||||
if (patch.groups === null) {
|
||||
delete nextAccount.groups;
|
||||
} else {
|
||||
nextAccount.groups = cloneMatrixRoomMap(patch.groups);
|
||||
}
|
||||
}
|
||||
if (patch.rooms !== undefined) {
|
||||
if (patch.rooms === null) {
|
||||
delete nextAccount.rooms;
|
||||
} else {
|
||||
nextAccount.rooms = cloneMatrixRoomMap(patch.rooms);
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldStoreMatrixAccountAtTopLevel(cfg, normalizedAccountId)) {
|
||||
const { accounts: _ignoredAccounts, defaultAccount, ...baseMatrix } = matrix;
|
||||
|
|
|
|||
|
|
@ -160,4 +160,107 @@ describe("matrix onboarding", () => {
|
|||
expect(noteText).toContain("MATRIX_<ACCOUNT_ID>_DEVICE_ID");
|
||||
expect(noteText).toContain("MATRIX_<ACCOUNT_ID>_DEVICE_NAME");
|
||||
});
|
||||
|
||||
it("writes allowlists and room access to the selected Matrix account", async () => {
|
||||
setMatrixRuntime({
|
||||
state: {
|
||||
resolveStateDir: (_env: NodeJS.ProcessEnv, homeDir?: () => string) =>
|
||||
(homeDir ?? (() => "/tmp"))(),
|
||||
},
|
||||
config: {
|
||||
loadConfig: () => ({}),
|
||||
},
|
||||
} as never);
|
||||
|
||||
const prompter = {
|
||||
note: vi.fn(async () => {}),
|
||||
select: vi.fn(async ({ message }: { message: string }) => {
|
||||
if (message === "Matrix already configured. What do you want to do?") {
|
||||
return "add-account";
|
||||
}
|
||||
if (message === "Matrix auth method") {
|
||||
return "token";
|
||||
}
|
||||
if (message === "Matrix rooms access") {
|
||||
return "allowlist";
|
||||
}
|
||||
throw new Error(`unexpected select prompt: ${message}`);
|
||||
}),
|
||||
text: vi.fn(async ({ message }: { message: string }) => {
|
||||
if (message === "Matrix account name") {
|
||||
return "ops";
|
||||
}
|
||||
if (message === "Matrix homeserver URL") {
|
||||
return "https://matrix.ops.example.org";
|
||||
}
|
||||
if (message === "Matrix access token") {
|
||||
return "ops-token";
|
||||
}
|
||||
if (message === "Matrix device name (optional)") {
|
||||
return "Ops Gateway";
|
||||
}
|
||||
if (message === "Matrix allowFrom (full @user:server; display name only if unique)") {
|
||||
return "@alice:example.org";
|
||||
}
|
||||
if (message === "Matrix rooms allowlist (comma-separated)") {
|
||||
return "!ops-room:example.org";
|
||||
}
|
||||
throw new Error(`unexpected text prompt: ${message}`);
|
||||
}),
|
||||
confirm: vi.fn(async ({ message }: { message: string }) => {
|
||||
if (message === "Enable end-to-end encryption (E2EE)?") {
|
||||
return false;
|
||||
}
|
||||
if (message === "Configure Matrix rooms access?") {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}),
|
||||
} as unknown as WizardPrompter;
|
||||
|
||||
const result = await matrixOnboardingAdapter.configureInteractive!({
|
||||
cfg: {
|
||||
channels: {
|
||||
matrix: {
|
||||
accounts: {
|
||||
default: {
|
||||
homeserver: "https://matrix.main.example.org",
|
||||
accessToken: "main-token",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as CoreConfig,
|
||||
runtime: { log: vi.fn(), error: vi.fn(), exit: vi.fn() } as unknown as RuntimeEnv,
|
||||
prompter,
|
||||
options: undefined,
|
||||
accountOverrides: {},
|
||||
shouldPromptAccountIds: true,
|
||||
forceAllowFrom: true,
|
||||
configured: true,
|
||||
label: "Matrix",
|
||||
});
|
||||
|
||||
expect(result).not.toBe("skip");
|
||||
if (result === "skip") {
|
||||
return;
|
||||
}
|
||||
|
||||
expect(result.accountId).toBe("ops");
|
||||
expect(result.cfg.channels?.["matrix"]?.accounts?.ops).toMatchObject({
|
||||
homeserver: "https://matrix.ops.example.org",
|
||||
accessToken: "ops-token",
|
||||
deviceName: "Ops Gateway",
|
||||
dm: {
|
||||
policy: "allowlist",
|
||||
allowFrom: ["@alice:example.org"],
|
||||
},
|
||||
groupPolicy: "allowlist",
|
||||
groups: {
|
||||
"!ops-room:example.org": { allow: true },
|
||||
},
|
||||
});
|
||||
expect(result.cfg.channels?.["matrix"]?.dm).toBeUndefined();
|
||||
expect(result.cfg.channels?.["matrix"]?.groups).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1 +1,561 @@
|
|||
export { matrixOnboardingAdapter } from "./setup-surface.js";
|
||||
import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/account-id";
|
||||
import type { DmPolicy } from "openclaw/plugin-sdk/matrix";
|
||||
import {
|
||||
addWildcardAllowFrom,
|
||||
formatDocsLink,
|
||||
mergeAllowFromEntries,
|
||||
normalizeAccountId,
|
||||
promptAccountId,
|
||||
type RuntimeEnv,
|
||||
type WizardPrompter,
|
||||
} from "openclaw/plugin-sdk/matrix";
|
||||
import {
|
||||
promptChannelAccessConfig,
|
||||
type ChannelSetupDmPolicy,
|
||||
type ChannelSetupWizardAdapter,
|
||||
} from "openclaw/plugin-sdk/setup";
|
||||
import { listMatrixDirectoryGroupsLive } from "./directory-live.js";
|
||||
import {
|
||||
listMatrixAccountIds,
|
||||
resolveDefaultMatrixAccountId,
|
||||
resolveMatrixAccount,
|
||||
resolveMatrixAccountConfig,
|
||||
} from "./matrix/accounts.js";
|
||||
import {
|
||||
getMatrixScopedEnvVarNames,
|
||||
hasReadyMatrixEnvAuth,
|
||||
resolveScopedMatrixEnvConfig,
|
||||
} from "./matrix/client.js";
|
||||
import { updateMatrixAccountConfig } from "./matrix/config-update.js";
|
||||
import { ensureMatrixSdkInstalled, isMatrixSdkAvailable } from "./matrix/deps.js";
|
||||
import { resolveMatrixTargets } from "./resolve-targets.js";
|
||||
import type { CoreConfig } from "./types.js";
|
||||
|
||||
const channel = "matrix" as const;
|
||||
|
||||
function resolveMatrixOnboardingAccountId(cfg: CoreConfig, accountId?: string): string {
|
||||
return normalizeAccountId(
|
||||
accountId?.trim() || resolveDefaultMatrixAccountId(cfg) || DEFAULT_ACCOUNT_ID,
|
||||
);
|
||||
}
|
||||
|
||||
function setMatrixDmPolicy(cfg: CoreConfig, policy: DmPolicy, accountId?: string) {
|
||||
const resolvedAccountId = resolveMatrixOnboardingAccountId(cfg, accountId);
|
||||
const existing = resolveMatrixAccountConfig({
|
||||
cfg,
|
||||
accountId: resolvedAccountId,
|
||||
});
|
||||
const allowFrom = policy === "open" ? addWildcardAllowFrom(existing.dm?.allowFrom) : undefined;
|
||||
return updateMatrixAccountConfig(cfg, resolvedAccountId, {
|
||||
dm: {
|
||||
...existing.dm,
|
||||
policy,
|
||||
...(allowFrom ? { allowFrom } : {}),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function noteMatrixAuthHelp(prompter: WizardPrompter): Promise<void> {
|
||||
await prompter.note(
|
||||
[
|
||||
"Matrix requires a homeserver URL.",
|
||||
"Use an access token (recommended) or password login to an existing account.",
|
||||
"With access token: user ID is fetched automatically.",
|
||||
"Env vars supported: MATRIX_HOMESERVER, MATRIX_USER_ID, MATRIX_ACCESS_TOKEN, MATRIX_PASSWORD, MATRIX_DEVICE_ID, MATRIX_DEVICE_NAME.",
|
||||
"Per-account env vars: MATRIX_<ACCOUNT_ID>_HOMESERVER, MATRIX_<ACCOUNT_ID>_USER_ID, MATRIX_<ACCOUNT_ID>_ACCESS_TOKEN, MATRIX_<ACCOUNT_ID>_PASSWORD, MATRIX_<ACCOUNT_ID>_DEVICE_ID, MATRIX_<ACCOUNT_ID>_DEVICE_NAME.",
|
||||
`Docs: ${formatDocsLink("/channels/matrix", "channels/matrix")}`,
|
||||
].join("\n"),
|
||||
"Matrix setup",
|
||||
);
|
||||
}
|
||||
|
||||
async function promptMatrixAllowFrom(params: {
|
||||
cfg: CoreConfig;
|
||||
prompter: WizardPrompter;
|
||||
accountId?: string;
|
||||
}): Promise<CoreConfig> {
|
||||
const { cfg, prompter } = params;
|
||||
const accountId = resolveMatrixOnboardingAccountId(cfg, params.accountId);
|
||||
const existingConfig = resolveMatrixAccountConfig({ cfg, accountId });
|
||||
const existingAllowFrom = existingConfig.dm?.allowFrom ?? [];
|
||||
const account = resolveMatrixAccount({ cfg, accountId });
|
||||
const canResolve = Boolean(account.configured);
|
||||
|
||||
const parseInput = (raw: string) =>
|
||||
raw
|
||||
.split(/[\n,;]+/g)
|
||||
.map((entry) => entry.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
const isFullUserId = (value: string) => value.startsWith("@") && value.includes(":");
|
||||
|
||||
while (true) {
|
||||
const entry = await prompter.text({
|
||||
message: "Matrix allowFrom (full @user:server; display name only if unique)",
|
||||
placeholder: "@user:server",
|
||||
initialValue: existingAllowFrom[0] ? String(existingAllowFrom[0]) : undefined,
|
||||
validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
|
||||
});
|
||||
const parts = parseInput(String(entry));
|
||||
const resolvedIds: string[] = [];
|
||||
const pending: string[] = [];
|
||||
const unresolved: string[] = [];
|
||||
const unresolvedNotes: string[] = [];
|
||||
|
||||
for (const part of parts) {
|
||||
if (isFullUserId(part)) {
|
||||
resolvedIds.push(part);
|
||||
continue;
|
||||
}
|
||||
if (!canResolve) {
|
||||
unresolved.push(part);
|
||||
continue;
|
||||
}
|
||||
pending.push(part);
|
||||
}
|
||||
|
||||
if (pending.length > 0) {
|
||||
const results = await resolveMatrixTargets({
|
||||
cfg,
|
||||
inputs: pending,
|
||||
kind: "user",
|
||||
}).catch(() => []);
|
||||
for (const result of results) {
|
||||
if (result?.resolved && result.id) {
|
||||
resolvedIds.push(result.id);
|
||||
continue;
|
||||
}
|
||||
if (result?.input) {
|
||||
unresolved.push(result.input);
|
||||
if (result.note) {
|
||||
unresolvedNotes.push(`${result.input}: ${result.note}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (unresolved.length > 0) {
|
||||
const details = unresolvedNotes.length > 0 ? unresolvedNotes : unresolved;
|
||||
await prompter.note(
|
||||
`Could not resolve:\n${details.join("\n")}\nUse full @user:server IDs.`,
|
||||
"Matrix allowlist",
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const unique = mergeAllowFromEntries(existingAllowFrom, resolvedIds);
|
||||
return updateMatrixAccountConfig(cfg, accountId, {
|
||||
dm: {
|
||||
...existingConfig.dm,
|
||||
policy: "allowlist",
|
||||
allowFrom: unique,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function setMatrixGroupPolicy(
|
||||
cfg: CoreConfig,
|
||||
groupPolicy: "open" | "allowlist" | "disabled",
|
||||
accountId?: string,
|
||||
) {
|
||||
return updateMatrixAccountConfig(cfg, resolveMatrixOnboardingAccountId(cfg, accountId), {
|
||||
groupPolicy,
|
||||
});
|
||||
}
|
||||
|
||||
function setMatrixGroupRooms(cfg: CoreConfig, roomKeys: string[], accountId?: string) {
|
||||
const groups = Object.fromEntries(roomKeys.map((key) => [key, { allow: true }]));
|
||||
return updateMatrixAccountConfig(cfg, resolveMatrixOnboardingAccountId(cfg, accountId), {
|
||||
groups,
|
||||
rooms: null,
|
||||
});
|
||||
}
|
||||
|
||||
const dmPolicy: ChannelSetupDmPolicy = {
|
||||
label: "Matrix",
|
||||
channel,
|
||||
policyKey: "channels.matrix.dm.policy",
|
||||
allowFromKey: "channels.matrix.dm.allowFrom",
|
||||
getCurrent: (cfg, accountId) =>
|
||||
resolveMatrixAccountConfig({
|
||||
cfg: cfg as CoreConfig,
|
||||
accountId: resolveMatrixOnboardingAccountId(cfg as CoreConfig, accountId),
|
||||
}).dm?.policy ?? "pairing",
|
||||
setPolicy: (cfg, policy, accountId) => setMatrixDmPolicy(cfg as CoreConfig, policy, accountId),
|
||||
promptAllowFrom: promptMatrixAllowFrom,
|
||||
};
|
||||
|
||||
type MatrixConfigureIntent = "update" | "add-account";
|
||||
|
||||
async function runMatrixConfigure(params: {
|
||||
cfg: CoreConfig;
|
||||
runtime: RuntimeEnv;
|
||||
prompter: WizardPrompter;
|
||||
forceAllowFrom: boolean;
|
||||
accountOverrides?: Partial<Record<string, string>>;
|
||||
shouldPromptAccountIds?: boolean;
|
||||
intent: MatrixConfigureIntent;
|
||||
}): Promise<{ cfg: CoreConfig; accountId: string }> {
|
||||
let next = params.cfg;
|
||||
await ensureMatrixSdkInstalled({
|
||||
runtime: params.runtime,
|
||||
confirm: async (message) =>
|
||||
await params.prompter.confirm({
|
||||
message,
|
||||
initialValue: true,
|
||||
}),
|
||||
});
|
||||
const defaultAccountId = resolveDefaultMatrixAccountId(next);
|
||||
let accountId = defaultAccountId || DEFAULT_ACCOUNT_ID;
|
||||
if (params.intent === "add-account") {
|
||||
const enteredName = String(
|
||||
await params.prompter.text({
|
||||
message: "Matrix account name",
|
||||
validate: (value) => (value?.trim() ? undefined : "Required"),
|
||||
}),
|
||||
).trim();
|
||||
accountId = normalizeAccountId(enteredName);
|
||||
if (enteredName !== accountId) {
|
||||
await params.prompter.note(`Account id will be "${accountId}".`, "Matrix account");
|
||||
}
|
||||
next = updateMatrixAccountConfig(next, accountId, { name: enteredName, enabled: true });
|
||||
} else {
|
||||
const override = params.accountOverrides?.[channel]?.trim();
|
||||
if (override) {
|
||||
accountId = normalizeAccountId(override);
|
||||
} else if (params.shouldPromptAccountIds) {
|
||||
accountId = await promptAccountId({
|
||||
cfg: next,
|
||||
prompter: params.prompter,
|
||||
label: "Matrix",
|
||||
currentId: accountId,
|
||||
listAccountIds: (inputCfg) => listMatrixAccountIds(inputCfg as CoreConfig),
|
||||
defaultAccountId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const existing = resolveMatrixAccountConfig({ cfg: next, accountId });
|
||||
const account = resolveMatrixAccount({ cfg: next, accountId });
|
||||
if (!account.configured) {
|
||||
await noteMatrixAuthHelp(params.prompter);
|
||||
}
|
||||
|
||||
const scopedEnv = resolveScopedMatrixEnvConfig(accountId, process.env);
|
||||
const defaultScopedEnv = resolveScopedMatrixEnvConfig(DEFAULT_ACCOUNT_ID, process.env);
|
||||
const globalEnv = {
|
||||
homeserver: process.env.MATRIX_HOMESERVER?.trim() ?? "",
|
||||
userId: process.env.MATRIX_USER_ID?.trim() ?? "",
|
||||
accessToken: process.env.MATRIX_ACCESS_TOKEN?.trim() || undefined,
|
||||
password: process.env.MATRIX_PASSWORD?.trim() || undefined,
|
||||
};
|
||||
const scopedReady = hasReadyMatrixEnvAuth(scopedEnv);
|
||||
const defaultScopedReady = hasReadyMatrixEnvAuth(defaultScopedEnv);
|
||||
const globalReady = hasReadyMatrixEnvAuth(globalEnv);
|
||||
const envReady =
|
||||
scopedReady || (accountId === DEFAULT_ACCOUNT_ID && (defaultScopedReady || globalReady));
|
||||
const envHomeserver =
|
||||
scopedEnv.homeserver ||
|
||||
(accountId === DEFAULT_ACCOUNT_ID
|
||||
? defaultScopedEnv.homeserver || globalEnv.homeserver
|
||||
: undefined);
|
||||
const envUserId =
|
||||
scopedEnv.userId ||
|
||||
(accountId === DEFAULT_ACCOUNT_ID ? defaultScopedEnv.userId || globalEnv.userId : undefined);
|
||||
|
||||
if (
|
||||
envReady &&
|
||||
!existing.homeserver &&
|
||||
!existing.userId &&
|
||||
!existing.accessToken &&
|
||||
!existing.password
|
||||
) {
|
||||
const scopedEnvNames = getMatrixScopedEnvVarNames(accountId);
|
||||
const envSourceHint =
|
||||
accountId === DEFAULT_ACCOUNT_ID
|
||||
? "MATRIX_* or MATRIX_DEFAULT_*"
|
||||
: `${scopedEnvNames.homeserver} (+ auth vars)`;
|
||||
const useEnv = await params.prompter.confirm({
|
||||
message: `Matrix env vars detected (${envSourceHint}). Use env values?`,
|
||||
initialValue: true,
|
||||
});
|
||||
if (useEnv) {
|
||||
next = updateMatrixAccountConfig(next, accountId, { enabled: true });
|
||||
if (params.forceAllowFrom) {
|
||||
next = await promptMatrixAllowFrom({
|
||||
cfg: next,
|
||||
prompter: params.prompter,
|
||||
accountId,
|
||||
});
|
||||
}
|
||||
return { cfg: next, accountId };
|
||||
}
|
||||
}
|
||||
|
||||
const homeserver = String(
|
||||
await params.prompter.text({
|
||||
message: "Matrix homeserver URL",
|
||||
initialValue: existing.homeserver ?? envHomeserver,
|
||||
validate: (value) => {
|
||||
const raw = String(value ?? "").trim();
|
||||
if (!raw) {
|
||||
return "Required";
|
||||
}
|
||||
if (!/^https?:\/\//i.test(raw)) {
|
||||
return "Use a full URL (https://...)";
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
}),
|
||||
).trim();
|
||||
|
||||
let accessToken = existing.accessToken ?? "";
|
||||
let password = typeof existing.password === "string" ? existing.password : "";
|
||||
let userId = existing.userId ?? "";
|
||||
|
||||
if (accessToken || password) {
|
||||
const keep = await params.prompter.confirm({
|
||||
message: "Matrix credentials already configured. Keep them?",
|
||||
initialValue: true,
|
||||
});
|
||||
if (!keep) {
|
||||
accessToken = "";
|
||||
password = "";
|
||||
userId = "";
|
||||
}
|
||||
}
|
||||
|
||||
if (!accessToken && !password) {
|
||||
const authMode = await params.prompter.select({
|
||||
message: "Matrix auth method",
|
||||
options: [
|
||||
{ value: "token", label: "Access token (user ID fetched automatically)" },
|
||||
{ value: "password", label: "Password (requires user ID)" },
|
||||
],
|
||||
});
|
||||
|
||||
if (authMode === "token") {
|
||||
accessToken = String(
|
||||
await params.prompter.text({
|
||||
message: "Matrix access token",
|
||||
validate: (value) => (value?.trim() ? undefined : "Required"),
|
||||
}),
|
||||
).trim();
|
||||
userId = "";
|
||||
} else {
|
||||
userId = String(
|
||||
await params.prompter.text({
|
||||
message: "Matrix user ID",
|
||||
initialValue: existing.userId ?? envUserId,
|
||||
validate: (value) => {
|
||||
const raw = String(value ?? "").trim();
|
||||
if (!raw) {
|
||||
return "Required";
|
||||
}
|
||||
if (!raw.startsWith("@")) {
|
||||
return "Matrix user IDs should start with @";
|
||||
}
|
||||
if (!raw.includes(":")) {
|
||||
return "Matrix user IDs should include a server (:server)";
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
}),
|
||||
).trim();
|
||||
password = String(
|
||||
await params.prompter.text({
|
||||
message: "Matrix password",
|
||||
validate: (value) => (value?.trim() ? undefined : "Required"),
|
||||
}),
|
||||
).trim();
|
||||
}
|
||||
}
|
||||
|
||||
const deviceName = String(
|
||||
await params.prompter.text({
|
||||
message: "Matrix device name (optional)",
|
||||
initialValue: existing.deviceName ?? "OpenClaw Gateway",
|
||||
}),
|
||||
).trim();
|
||||
|
||||
const enableEncryption = await params.prompter.confirm({
|
||||
message: "Enable end-to-end encryption (E2EE)?",
|
||||
initialValue: existing.encryption ?? false,
|
||||
});
|
||||
|
||||
next = updateMatrixAccountConfig(next, accountId, {
|
||||
enabled: true,
|
||||
homeserver,
|
||||
userId: userId || null,
|
||||
accessToken: accessToken || null,
|
||||
password: password || null,
|
||||
deviceName: deviceName || null,
|
||||
encryption: enableEncryption,
|
||||
});
|
||||
|
||||
if (params.forceAllowFrom) {
|
||||
next = await promptMatrixAllowFrom({
|
||||
cfg: next,
|
||||
prompter: params.prompter,
|
||||
accountId,
|
||||
});
|
||||
}
|
||||
|
||||
const existingAccountConfig = resolveMatrixAccountConfig({ cfg: next, accountId });
|
||||
const existingGroups = existingAccountConfig.groups ?? existingAccountConfig.rooms;
|
||||
const accessConfig = await promptChannelAccessConfig({
|
||||
prompter: params.prompter,
|
||||
label: "Matrix rooms",
|
||||
currentPolicy: existingAccountConfig.groupPolicy ?? "allowlist",
|
||||
currentEntries: Object.keys(existingGroups ?? {}),
|
||||
placeholder: "!roomId:server, #alias:server, Project Room",
|
||||
updatePrompt: Boolean(existingGroups),
|
||||
});
|
||||
if (accessConfig) {
|
||||
if (accessConfig.policy !== "allowlist") {
|
||||
next = setMatrixGroupPolicy(next, accessConfig.policy, accountId);
|
||||
} else {
|
||||
let roomKeys = accessConfig.entries;
|
||||
if (accessConfig.entries.length > 0) {
|
||||
try {
|
||||
const resolvedIds: string[] = [];
|
||||
const unresolved: string[] = [];
|
||||
for (const entry of accessConfig.entries) {
|
||||
const trimmed = entry.trim();
|
||||
if (!trimmed) {
|
||||
continue;
|
||||
}
|
||||
const cleaned = trimmed.replace(/^(room|channel):/i, "").trim();
|
||||
if (cleaned.startsWith("!") && cleaned.includes(":")) {
|
||||
resolvedIds.push(cleaned);
|
||||
continue;
|
||||
}
|
||||
const matches = await listMatrixDirectoryGroupsLive({
|
||||
cfg: next,
|
||||
accountId,
|
||||
query: trimmed,
|
||||
limit: 10,
|
||||
});
|
||||
const exact = matches.find(
|
||||
(match) => (match.name ?? "").toLowerCase() === trimmed.toLowerCase(),
|
||||
);
|
||||
const best = exact ?? matches[0];
|
||||
if (best?.id) {
|
||||
resolvedIds.push(best.id);
|
||||
} else {
|
||||
unresolved.push(entry);
|
||||
}
|
||||
}
|
||||
roomKeys = [...resolvedIds, ...unresolved.map((entry) => entry.trim()).filter(Boolean)];
|
||||
if (resolvedIds.length > 0 || unresolved.length > 0) {
|
||||
await params.prompter.note(
|
||||
[
|
||||
resolvedIds.length > 0 ? `Resolved: ${resolvedIds.join(", ")}` : undefined,
|
||||
unresolved.length > 0
|
||||
? `Unresolved (kept as typed): ${unresolved.join(", ")}`
|
||||
: undefined,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("\n"),
|
||||
"Matrix rooms",
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
await params.prompter.note(
|
||||
`Room lookup failed; keeping entries as typed. ${String(err)}`,
|
||||
"Matrix rooms",
|
||||
);
|
||||
}
|
||||
}
|
||||
next = setMatrixGroupPolicy(next, "allowlist", accountId);
|
||||
next = setMatrixGroupRooms(next, roomKeys, accountId);
|
||||
}
|
||||
}
|
||||
|
||||
return { cfg: next, accountId };
|
||||
}
|
||||
|
||||
export const matrixOnboardingAdapter: ChannelSetupWizardAdapter = {
|
||||
channel,
|
||||
getStatus: async ({ cfg }) => {
|
||||
const account = resolveMatrixAccount({ cfg: cfg as CoreConfig });
|
||||
const configured = account.configured;
|
||||
const sdkReady = isMatrixSdkAvailable();
|
||||
return {
|
||||
channel,
|
||||
configured,
|
||||
statusLines: [
|
||||
`Matrix: ${configured ? "configured" : "needs homeserver + access token or password"}`,
|
||||
],
|
||||
selectionHint: !sdkReady ? "install matrix-js-sdk" : configured ? "configured" : "needs auth",
|
||||
};
|
||||
},
|
||||
configure: async ({
|
||||
cfg,
|
||||
runtime,
|
||||
prompter,
|
||||
forceAllowFrom,
|
||||
accountOverrides,
|
||||
shouldPromptAccountIds,
|
||||
}) =>
|
||||
await runMatrixConfigure({
|
||||
cfg: cfg as CoreConfig,
|
||||
runtime,
|
||||
prompter,
|
||||
forceAllowFrom,
|
||||
accountOverrides,
|
||||
shouldPromptAccountIds,
|
||||
intent: "update",
|
||||
}),
|
||||
configureInteractive: async ({
|
||||
cfg,
|
||||
runtime,
|
||||
prompter,
|
||||
forceAllowFrom,
|
||||
accountOverrides,
|
||||
shouldPromptAccountIds,
|
||||
configured,
|
||||
}) => {
|
||||
if (!configured) {
|
||||
return await runMatrixConfigure({
|
||||
cfg: cfg as CoreConfig,
|
||||
runtime,
|
||||
prompter,
|
||||
forceAllowFrom,
|
||||
accountOverrides,
|
||||
shouldPromptAccountIds,
|
||||
intent: "update",
|
||||
});
|
||||
}
|
||||
const action = await prompter.select({
|
||||
message: "Matrix already configured. What do you want to do?",
|
||||
options: [
|
||||
{ value: "update", label: "Modify settings" },
|
||||
{ value: "add-account", label: "Add account" },
|
||||
{ value: "skip", label: "Skip (leave as-is)" },
|
||||
],
|
||||
initialValue: "update",
|
||||
});
|
||||
if (action === "skip") {
|
||||
return "skip";
|
||||
}
|
||||
return await runMatrixConfigure({
|
||||
cfg: cfg as CoreConfig,
|
||||
runtime,
|
||||
prompter,
|
||||
forceAllowFrom,
|
||||
accountOverrides,
|
||||
shouldPromptAccountIds,
|
||||
intent: action === "add-account" ? "add-account" : "update",
|
||||
});
|
||||
},
|
||||
dmPolicy,
|
||||
disable: (cfg) => ({
|
||||
...(cfg as CoreConfig),
|
||||
channels: {
|
||||
...(cfg as CoreConfig).channels,
|
||||
matrix: { ...(cfg as CoreConfig).channels?.["matrix"], enabled: false },
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,557 +1 @@
|
|||
import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/account-id";
|
||||
import type { DmPolicy } from "openclaw/plugin-sdk/matrix";
|
||||
import {
|
||||
addWildcardAllowFrom,
|
||||
formatDocsLink,
|
||||
mergeAllowFromEntries,
|
||||
normalizeAccountId,
|
||||
promptAccountId,
|
||||
promptChannelAccessConfig,
|
||||
type RuntimeEnv,
|
||||
type WizardPrompter,
|
||||
} from "openclaw/plugin-sdk/matrix";
|
||||
import { listMatrixDirectoryGroupsLive } from "./directory-live.js";
|
||||
import {
|
||||
listMatrixAccountIds,
|
||||
resolveDefaultMatrixAccountId,
|
||||
resolveMatrixAccount,
|
||||
resolveMatrixAccountConfig,
|
||||
} from "./matrix/accounts.js";
|
||||
import {
|
||||
getMatrixScopedEnvVarNames,
|
||||
hasReadyMatrixEnvAuth,
|
||||
resolveScopedMatrixEnvConfig,
|
||||
} from "./matrix/client.js";
|
||||
import { updateMatrixAccountConfig } from "./matrix/config-update.js";
|
||||
import { ensureMatrixSdkInstalled, isMatrixSdkAvailable } from "./matrix/deps.js";
|
||||
import { resolveMatrixTargets } from "./resolve-targets.js";
|
||||
import type { CoreConfig } from "./types.js";
|
||||
|
||||
const channel = "matrix" as const;
|
||||
|
||||
function setMatrixDmPolicy(cfg: CoreConfig, policy: DmPolicy) {
|
||||
const allowFrom =
|
||||
policy === "open" ? addWildcardAllowFrom(cfg.channels?.["matrix"]?.dm?.allowFrom) : undefined;
|
||||
return {
|
||||
...cfg,
|
||||
channels: {
|
||||
...cfg.channels,
|
||||
matrix: {
|
||||
...cfg.channels?.["matrix"],
|
||||
dm: {
|
||||
...cfg.channels?.["matrix"]?.dm,
|
||||
policy,
|
||||
...(allowFrom ? { allowFrom } : {}),
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function noteMatrixAuthHelp(prompter: WizardPrompter): Promise<void> {
|
||||
await prompter.note(
|
||||
[
|
||||
"Matrix requires a homeserver URL.",
|
||||
"Use an access token (recommended) or password login to an existing account.",
|
||||
"With access token: user ID is fetched automatically.",
|
||||
"Env vars supported: MATRIX_HOMESERVER, MATRIX_USER_ID, MATRIX_ACCESS_TOKEN, MATRIX_PASSWORD, MATRIX_DEVICE_ID, MATRIX_DEVICE_NAME.",
|
||||
"Per-account env vars: MATRIX_<ACCOUNT_ID>_HOMESERVER, MATRIX_<ACCOUNT_ID>_USER_ID, MATRIX_<ACCOUNT_ID>_ACCESS_TOKEN, MATRIX_<ACCOUNT_ID>_PASSWORD, MATRIX_<ACCOUNT_ID>_DEVICE_ID, MATRIX_<ACCOUNT_ID>_DEVICE_NAME.",
|
||||
`Docs: ${formatDocsLink("/channels/matrix", "channels/matrix")}`,
|
||||
].join("\n"),
|
||||
"Matrix setup",
|
||||
);
|
||||
}
|
||||
|
||||
async function promptMatrixAllowFrom(params: {
|
||||
cfg: CoreConfig;
|
||||
prompter: WizardPrompter;
|
||||
accountId?: string;
|
||||
}): Promise<CoreConfig> {
|
||||
const { cfg, prompter } = params;
|
||||
const existingAllowFrom = cfg.channels?.["matrix"]?.dm?.allowFrom ?? [];
|
||||
const account = resolveMatrixAccount({ cfg });
|
||||
const canResolve = Boolean(account.configured);
|
||||
|
||||
const parseInput = (raw: string) =>
|
||||
raw
|
||||
.split(/[\n,;]+/g)
|
||||
.map((entry) => entry.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
const isFullUserId = (value: string) => value.startsWith("@") && value.includes(":");
|
||||
|
||||
while (true) {
|
||||
const entry = await prompter.text({
|
||||
message: "Matrix allowFrom (full @user:server; display name only if unique)",
|
||||
placeholder: "@user:server",
|
||||
initialValue: existingAllowFrom[0] ? String(existingAllowFrom[0]) : undefined,
|
||||
validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
|
||||
});
|
||||
const parts = parseInput(String(entry));
|
||||
const resolvedIds: string[] = [];
|
||||
const pending: string[] = [];
|
||||
const unresolved: string[] = [];
|
||||
const unresolvedNotes: string[] = [];
|
||||
|
||||
for (const part of parts) {
|
||||
if (isFullUserId(part)) {
|
||||
resolvedIds.push(part);
|
||||
continue;
|
||||
}
|
||||
if (!canResolve) {
|
||||
unresolved.push(part);
|
||||
continue;
|
||||
}
|
||||
pending.push(part);
|
||||
}
|
||||
|
||||
if (pending.length > 0) {
|
||||
const results = await resolveMatrixTargets({
|
||||
cfg,
|
||||
inputs: pending,
|
||||
kind: "user",
|
||||
}).catch(() => []);
|
||||
for (const result of results) {
|
||||
if (result?.resolved && result.id) {
|
||||
resolvedIds.push(result.id);
|
||||
continue;
|
||||
}
|
||||
if (result?.input) {
|
||||
unresolved.push(result.input);
|
||||
if (result.note) {
|
||||
unresolvedNotes.push(`${result.input}: ${result.note}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (unresolved.length > 0) {
|
||||
const details = unresolvedNotes.length > 0 ? unresolvedNotes : unresolved;
|
||||
await prompter.note(
|
||||
`Could not resolve:\n${details.join("\n")}\nUse full @user:server IDs.`,
|
||||
"Matrix allowlist",
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const unique = mergeAllowFromEntries(existingAllowFrom, resolvedIds);
|
||||
return {
|
||||
...cfg,
|
||||
channels: {
|
||||
...cfg.channels,
|
||||
matrix: {
|
||||
...cfg.channels?.["matrix"],
|
||||
enabled: true,
|
||||
dm: {
|
||||
...cfg.channels?.["matrix"]?.dm,
|
||||
policy: "allowlist",
|
||||
allowFrom: unique,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function setMatrixGroupPolicy(cfg: CoreConfig, groupPolicy: "open" | "allowlist" | "disabled") {
|
||||
return {
|
||||
...cfg,
|
||||
channels: {
|
||||
...cfg.channels,
|
||||
matrix: {
|
||||
...cfg.channels?.["matrix"],
|
||||
enabled: true,
|
||||
groupPolicy,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function setMatrixGroupRooms(cfg: CoreConfig, roomKeys: string[]) {
|
||||
const groups = Object.fromEntries(roomKeys.map((key) => [key, { allow: true }]));
|
||||
return {
|
||||
...cfg,
|
||||
channels: {
|
||||
...cfg.channels,
|
||||
matrix: {
|
||||
...cfg.channels?.["matrix"],
|
||||
enabled: true,
|
||||
groups,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const dmPolicy = {
|
||||
label: "Matrix",
|
||||
channel,
|
||||
policyKey: "channels.matrix.dm.policy",
|
||||
allowFromKey: "channels.matrix.dm.allowFrom",
|
||||
getCurrent: (cfg) => (cfg as CoreConfig).channels?.["matrix"]?.dm?.policy ?? "pairing",
|
||||
setPolicy: (cfg, policy) => setMatrixDmPolicy(cfg as CoreConfig, policy),
|
||||
promptAllowFrom: promptMatrixAllowFrom,
|
||||
};
|
||||
|
||||
type MatrixConfigureIntent = "update" | "add-account";
|
||||
|
||||
async function runMatrixConfigure(params: {
|
||||
cfg: CoreConfig;
|
||||
runtime: RuntimeEnv;
|
||||
prompter: WizardPrompter;
|
||||
forceAllowFrom: boolean;
|
||||
accountOverrides?: Partial<Record<string, string>>;
|
||||
shouldPromptAccountIds?: boolean;
|
||||
intent: MatrixConfigureIntent;
|
||||
}): Promise<{ cfg: CoreConfig; accountId: string }> {
|
||||
let next = params.cfg;
|
||||
await ensureMatrixSdkInstalled({
|
||||
runtime: params.runtime,
|
||||
confirm: async (message) =>
|
||||
await params.prompter.confirm({
|
||||
message,
|
||||
initialValue: true,
|
||||
}),
|
||||
});
|
||||
const defaultAccountId = resolveDefaultMatrixAccountId(next);
|
||||
let accountId = defaultAccountId || DEFAULT_ACCOUNT_ID;
|
||||
if (params.intent === "add-account") {
|
||||
const enteredName = String(
|
||||
await params.prompter.text({
|
||||
message: "Matrix account name",
|
||||
validate: (value) => (value?.trim() ? undefined : "Required"),
|
||||
}),
|
||||
).trim();
|
||||
accountId = normalizeAccountId(enteredName);
|
||||
if (enteredName !== accountId) {
|
||||
await params.prompter.note(`Account id will be "${accountId}".`, "Matrix account");
|
||||
}
|
||||
next = updateMatrixAccountConfig(next, accountId, { name: enteredName, enabled: true });
|
||||
} else {
|
||||
const override = params.accountOverrides?.[channel]?.trim();
|
||||
if (override) {
|
||||
accountId = normalizeAccountId(override);
|
||||
} else if (params.shouldPromptAccountIds) {
|
||||
accountId = await promptAccountId({
|
||||
cfg: next,
|
||||
prompter: params.prompter,
|
||||
label: "Matrix",
|
||||
currentId: accountId,
|
||||
listAccountIds: (inputCfg) => listMatrixAccountIds(inputCfg as CoreConfig),
|
||||
defaultAccountId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const existing = resolveMatrixAccountConfig({ cfg: next, accountId });
|
||||
const account = resolveMatrixAccount({ cfg: next, accountId });
|
||||
if (!account.configured) {
|
||||
await noteMatrixAuthHelp(params.prompter);
|
||||
}
|
||||
|
||||
const scopedEnv = resolveScopedMatrixEnvConfig(accountId, process.env);
|
||||
const defaultScopedEnv = resolveScopedMatrixEnvConfig(DEFAULT_ACCOUNT_ID, process.env);
|
||||
const globalEnv = {
|
||||
homeserver: process.env.MATRIX_HOMESERVER?.trim() ?? "",
|
||||
userId: process.env.MATRIX_USER_ID?.trim() ?? "",
|
||||
accessToken: process.env.MATRIX_ACCESS_TOKEN?.trim() || undefined,
|
||||
password: process.env.MATRIX_PASSWORD?.trim() || undefined,
|
||||
};
|
||||
const scopedReady = hasReadyMatrixEnvAuth(scopedEnv);
|
||||
const defaultScopedReady = hasReadyMatrixEnvAuth(defaultScopedEnv);
|
||||
const globalReady = hasReadyMatrixEnvAuth(globalEnv);
|
||||
const envReady =
|
||||
scopedReady || (accountId === DEFAULT_ACCOUNT_ID && (defaultScopedReady || globalReady));
|
||||
const envHomeserver =
|
||||
scopedEnv.homeserver ||
|
||||
(accountId === DEFAULT_ACCOUNT_ID
|
||||
? defaultScopedEnv.homeserver || globalEnv.homeserver
|
||||
: undefined);
|
||||
const envUserId =
|
||||
scopedEnv.userId ||
|
||||
(accountId === DEFAULT_ACCOUNT_ID ? defaultScopedEnv.userId || globalEnv.userId : undefined);
|
||||
|
||||
if (
|
||||
envReady &&
|
||||
!existing.homeserver &&
|
||||
!existing.userId &&
|
||||
!existing.accessToken &&
|
||||
!existing.password
|
||||
) {
|
||||
const scopedEnvNames = getMatrixScopedEnvVarNames(accountId);
|
||||
const envSourceHint =
|
||||
accountId === DEFAULT_ACCOUNT_ID
|
||||
? "MATRIX_* or MATRIX_DEFAULT_*"
|
||||
: `${scopedEnvNames.homeserver} (+ auth vars)`;
|
||||
const useEnv = await params.prompter.confirm({
|
||||
message: `Matrix env vars detected (${envSourceHint}). Use env values?`,
|
||||
initialValue: true,
|
||||
});
|
||||
if (useEnv) {
|
||||
next = updateMatrixAccountConfig(next, accountId, { enabled: true });
|
||||
if (params.forceAllowFrom) {
|
||||
next = await promptMatrixAllowFrom({ cfg: next, prompter: params.prompter });
|
||||
}
|
||||
return { cfg: next, accountId };
|
||||
}
|
||||
}
|
||||
|
||||
const homeserver = String(
|
||||
await params.prompter.text({
|
||||
message: "Matrix homeserver URL",
|
||||
initialValue: existing.homeserver ?? envHomeserver,
|
||||
validate: (value) => {
|
||||
const raw = String(value ?? "").trim();
|
||||
if (!raw) {
|
||||
return "Required";
|
||||
}
|
||||
if (!/^https?:\/\//i.test(raw)) {
|
||||
return "Use a full URL (https://...)";
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
}),
|
||||
).trim();
|
||||
|
||||
let accessToken = existing.accessToken ?? "";
|
||||
let password = typeof existing.password === "string" ? existing.password : "";
|
||||
let userId = existing.userId ?? "";
|
||||
|
||||
if (accessToken || password) {
|
||||
const keep = await params.prompter.confirm({
|
||||
message: "Matrix credentials already configured. Keep them?",
|
||||
initialValue: true,
|
||||
});
|
||||
if (!keep) {
|
||||
accessToken = "";
|
||||
password = "";
|
||||
userId = "";
|
||||
}
|
||||
}
|
||||
|
||||
if (!accessToken && !password) {
|
||||
const authMode = await params.prompter.select({
|
||||
message: "Matrix auth method",
|
||||
options: [
|
||||
{ value: "token", label: "Access token (user ID fetched automatically)" },
|
||||
{ value: "password", label: "Password (requires user ID)" },
|
||||
],
|
||||
});
|
||||
|
||||
if (authMode === "token") {
|
||||
accessToken = String(
|
||||
await params.prompter.text({
|
||||
message: "Matrix access token",
|
||||
validate: (value) => (value?.trim() ? undefined : "Required"),
|
||||
}),
|
||||
).trim();
|
||||
userId = "";
|
||||
} else {
|
||||
userId = String(
|
||||
await params.prompter.text({
|
||||
message: "Matrix user ID",
|
||||
initialValue: existing.userId ?? envUserId,
|
||||
validate: (value) => {
|
||||
const raw = String(value ?? "").trim();
|
||||
if (!raw) {
|
||||
return "Required";
|
||||
}
|
||||
if (!raw.startsWith("@")) {
|
||||
return "Matrix user IDs should start with @";
|
||||
}
|
||||
if (!raw.includes(":")) {
|
||||
return "Matrix user IDs should include a server (:server)";
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
}),
|
||||
).trim();
|
||||
password = String(
|
||||
await params.prompter.text({
|
||||
message: "Matrix password",
|
||||
validate: (value) => (value?.trim() ? undefined : "Required"),
|
||||
}),
|
||||
).trim();
|
||||
}
|
||||
}
|
||||
|
||||
const deviceName = String(
|
||||
await params.prompter.text({
|
||||
message: "Matrix device name (optional)",
|
||||
initialValue: existing.deviceName ?? "OpenClaw Gateway",
|
||||
}),
|
||||
).trim();
|
||||
|
||||
const enableEncryption = await params.prompter.confirm({
|
||||
message: "Enable end-to-end encryption (E2EE)?",
|
||||
initialValue: existing.encryption ?? false,
|
||||
});
|
||||
|
||||
next = updateMatrixAccountConfig(next, accountId, {
|
||||
enabled: true,
|
||||
homeserver,
|
||||
userId: userId || null,
|
||||
accessToken: accessToken || null,
|
||||
password: password || null,
|
||||
deviceName: deviceName || null,
|
||||
encryption: enableEncryption,
|
||||
});
|
||||
|
||||
if (params.forceAllowFrom) {
|
||||
next = await promptMatrixAllowFrom({ cfg: next, prompter: params.prompter });
|
||||
}
|
||||
|
||||
const existingGroups = next.channels?.["matrix"]?.groups ?? next.channels?.["matrix"]?.rooms;
|
||||
const accessConfig = await promptChannelAccessConfig({
|
||||
prompter: params.prompter,
|
||||
label: "Matrix rooms",
|
||||
currentPolicy: next.channels?.["matrix"]?.groupPolicy ?? "allowlist",
|
||||
currentEntries: Object.keys(existingGroups ?? {}),
|
||||
placeholder: "!roomId:server, #alias:server, Project Room",
|
||||
updatePrompt: Boolean(existingGroups),
|
||||
});
|
||||
if (accessConfig) {
|
||||
if (accessConfig.policy !== "allowlist") {
|
||||
next = setMatrixGroupPolicy(next, accessConfig.policy);
|
||||
} else {
|
||||
let roomKeys = accessConfig.entries;
|
||||
if (accessConfig.entries.length > 0) {
|
||||
try {
|
||||
const resolvedIds: string[] = [];
|
||||
const unresolved: string[] = [];
|
||||
for (const entry of accessConfig.entries) {
|
||||
const trimmed = entry.trim();
|
||||
if (!trimmed) {
|
||||
continue;
|
||||
}
|
||||
const cleaned = trimmed.replace(/^(room|channel):/i, "").trim();
|
||||
if (cleaned.startsWith("!") && cleaned.includes(":")) {
|
||||
resolvedIds.push(cleaned);
|
||||
continue;
|
||||
}
|
||||
const matches = await listMatrixDirectoryGroupsLive({
|
||||
cfg: next,
|
||||
query: trimmed,
|
||||
limit: 10,
|
||||
});
|
||||
const exact = matches.find(
|
||||
(match) => (match.name ?? "").toLowerCase() === trimmed.toLowerCase(),
|
||||
);
|
||||
const best = exact ?? matches[0];
|
||||
if (best?.id) {
|
||||
resolvedIds.push(best.id);
|
||||
} else {
|
||||
unresolved.push(entry);
|
||||
}
|
||||
}
|
||||
roomKeys = [...resolvedIds, ...unresolved.map((entry) => entry.trim()).filter(Boolean)];
|
||||
if (resolvedIds.length > 0 || unresolved.length > 0) {
|
||||
await params.prompter.note(
|
||||
[
|
||||
resolvedIds.length > 0 ? `Resolved: ${resolvedIds.join(", ")}` : undefined,
|
||||
unresolved.length > 0
|
||||
? `Unresolved (kept as typed): ${unresolved.join(", ")}`
|
||||
: undefined,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("\n"),
|
||||
"Matrix rooms",
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
await params.prompter.note(
|
||||
`Room lookup failed; keeping entries as typed. ${String(err)}`,
|
||||
"Matrix rooms",
|
||||
);
|
||||
}
|
||||
}
|
||||
next = setMatrixGroupPolicy(next, "allowlist");
|
||||
next = setMatrixGroupRooms(next, roomKeys);
|
||||
}
|
||||
}
|
||||
|
||||
return { cfg: next, accountId };
|
||||
}
|
||||
|
||||
export const matrixOnboardingAdapter = {
|
||||
channel,
|
||||
getStatus: async ({ cfg }) => {
|
||||
const account = resolveMatrixAccount({ cfg: cfg as CoreConfig });
|
||||
const configured = account.configured;
|
||||
const sdkReady = isMatrixSdkAvailable();
|
||||
return {
|
||||
channel,
|
||||
configured,
|
||||
statusLines: [
|
||||
`Matrix: ${configured ? "configured" : "needs homeserver + access token or password"}`,
|
||||
],
|
||||
selectionHint: !sdkReady ? "install matrix-js-sdk" : configured ? "configured" : "needs auth",
|
||||
};
|
||||
},
|
||||
configure: async ({
|
||||
cfg,
|
||||
runtime,
|
||||
prompter,
|
||||
forceAllowFrom,
|
||||
accountOverrides,
|
||||
shouldPromptAccountIds,
|
||||
}) =>
|
||||
await runMatrixConfigure({
|
||||
cfg: cfg as CoreConfig,
|
||||
runtime,
|
||||
prompter,
|
||||
forceAllowFrom,
|
||||
accountOverrides,
|
||||
shouldPromptAccountIds,
|
||||
intent: "update",
|
||||
}),
|
||||
configureInteractive: async ({
|
||||
cfg,
|
||||
runtime,
|
||||
prompter,
|
||||
forceAllowFrom,
|
||||
accountOverrides,
|
||||
shouldPromptAccountIds,
|
||||
configured,
|
||||
}) => {
|
||||
if (!configured) {
|
||||
return await runMatrixConfigure({
|
||||
cfg: cfg as CoreConfig,
|
||||
runtime,
|
||||
prompter,
|
||||
forceAllowFrom,
|
||||
accountOverrides,
|
||||
shouldPromptAccountIds,
|
||||
intent: "update",
|
||||
});
|
||||
}
|
||||
const action = await prompter.select({
|
||||
message: "Matrix already configured. What do you want to do?",
|
||||
options: [
|
||||
{ value: "update", label: "Modify settings" },
|
||||
{ value: "add-account", label: "Add account" },
|
||||
{ value: "skip", label: "Skip (leave as-is)" },
|
||||
],
|
||||
initialValue: "update",
|
||||
});
|
||||
if (action === "skip") {
|
||||
return "skip";
|
||||
}
|
||||
return await runMatrixConfigure({
|
||||
cfg: cfg as CoreConfig,
|
||||
runtime,
|
||||
prompter,
|
||||
forceAllowFrom,
|
||||
accountOverrides,
|
||||
shouldPromptAccountIds,
|
||||
intent: action === "add-account" ? "add-account" : "update",
|
||||
});
|
||||
},
|
||||
dmPolicy,
|
||||
disable: (cfg) => ({
|
||||
...(cfg as CoreConfig),
|
||||
channels: {
|
||||
...(cfg as CoreConfig).channels,
|
||||
matrix: { ...(cfg as CoreConfig).channels?.["matrix"], enabled: false },
|
||||
},
|
||||
}),
|
||||
};
|
||||
export { matrixOnboardingAdapter } from "./onboarding.js";
|
||||
|
|
|
|||
|
|
@ -81,8 +81,8 @@ export type ChannelSetupDmPolicy = {
|
|||
channel: ChannelId;
|
||||
policyKey: string;
|
||||
allowFromKey: string;
|
||||
getCurrent: (cfg: OpenClawConfig) => DmPolicy;
|
||||
setPolicy: (cfg: OpenClawConfig, policy: DmPolicy) => OpenClawConfig;
|
||||
getCurrent: (cfg: OpenClawConfig, accountId?: string) => DmPolicy;
|
||||
setPolicy: (cfg: OpenClawConfig, policy: DmPolicy, accountId?: string) => OpenClawConfig;
|
||||
promptAllowFrom?: (params: {
|
||||
cfg: OpenClawConfig;
|
||||
prompter: WizardPrompter;
|
||||
|
|
|
|||
|
|
@ -292,6 +292,7 @@ async function maybeConfigureDmPolicies(params: {
|
|||
|
||||
let cfg = params.cfg;
|
||||
const selectPolicy = async (policy: ChannelSetupDmPolicy) => {
|
||||
const accountId = accountIdsByChannel?.get(policy.channel);
|
||||
await prompter.note(
|
||||
[
|
||||
"Default: pairing (unknown DMs get a pairing code).",
|
||||
|
|
@ -305,28 +306,31 @@ async function maybeConfigureDmPolicies(params: {
|
|||
].join("\n"),
|
||||
`${policy.label} DM access`,
|
||||
);
|
||||
return (await prompter.select({
|
||||
message: `${policy.label} DM policy`,
|
||||
options: [
|
||||
{ value: "pairing", label: "Pairing (recommended)" },
|
||||
{ value: "allowlist", label: "Allowlist (specific users only)" },
|
||||
{ value: "open", label: "Open (public inbound DMs)" },
|
||||
{ value: "disabled", label: "Disabled (ignore DMs)" },
|
||||
],
|
||||
})) as DmPolicy;
|
||||
return {
|
||||
accountId,
|
||||
nextPolicy: (await prompter.select({
|
||||
message: `${policy.label} DM policy`,
|
||||
options: [
|
||||
{ value: "pairing", label: "Pairing (recommended)" },
|
||||
{ value: "allowlist", label: "Allowlist (specific users only)" },
|
||||
{ value: "open", label: "Open (public inbound DMs)" },
|
||||
{ value: "disabled", label: "Disabled (ignore DMs)" },
|
||||
],
|
||||
})) as DmPolicy,
|
||||
};
|
||||
};
|
||||
|
||||
for (const policy of dmPolicies) {
|
||||
const current = policy.getCurrent(cfg);
|
||||
const nextPolicy = await selectPolicy(policy);
|
||||
const { accountId, nextPolicy } = await selectPolicy(policy);
|
||||
const current = policy.getCurrent(cfg, accountId);
|
||||
if (nextPolicy !== current) {
|
||||
cfg = policy.setPolicy(cfg, nextPolicy);
|
||||
cfg = policy.setPolicy(cfg, nextPolicy, accountId);
|
||||
}
|
||||
if (nextPolicy === "allowlist" && policy.promptAllowFrom) {
|
||||
cfg = await policy.promptAllowFrom({
|
||||
cfg,
|
||||
prompter,
|
||||
accountId: accountIdsByChannel?.get(policy.channel),
|
||||
accountId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,10 +48,10 @@ export {
|
|||
addWildcardAllowFrom,
|
||||
mergeAllowFromEntries,
|
||||
promptAccountId,
|
||||
promptChannelAccessConfig,
|
||||
promptSingleChannelSecretInput,
|
||||
setTopLevelChannelGroupPolicy,
|
||||
} from "../channels/plugins/setup-wizard-helpers.js";
|
||||
export { promptChannelAccessConfig } from "../channels/plugins/setup-group-access.js";
|
||||
export { PAIRING_APPROVED_MESSAGE } from "../channels/plugins/pairing-message.js";
|
||||
export {
|
||||
applyAccountNameToChannelSection,
|
||||
|
|
|
|||
|
|
@ -6,7 +6,16 @@ export type { SecretInput } from "../config/types.secrets.js";
|
|||
export type { WizardPrompter } from "../wizard/prompts.js";
|
||||
export type { ChannelSetupAdapter } from "../channels/plugins/types.adapters.js";
|
||||
export type { ChannelSetupInput } from "../channels/plugins/types.core.js";
|
||||
export type { ChannelSetupDmPolicy } from "../channels/plugins/setup-wizard-types.js";
|
||||
export type {
|
||||
ChannelSetupConfigureContext,
|
||||
ChannelSetupConfiguredResult,
|
||||
ChannelSetupDmPolicy,
|
||||
ChannelSetupInteractiveContext,
|
||||
ChannelSetupResult,
|
||||
ChannelSetupStatus,
|
||||
ChannelSetupStatusContext,
|
||||
ChannelSetupWizardAdapter,
|
||||
} from "../channels/plugins/setup-wizard-types.js";
|
||||
export type {
|
||||
ChannelSetupWizard,
|
||||
ChannelSetupWizardAllowFromEntry,
|
||||
|
|
@ -59,3 +68,4 @@ export {
|
|||
export { createAllowlistSetupWizardProxy } from "../channels/plugins/setup-wizard-proxy.js";
|
||||
|
||||
export { formatResolvedUnresolvedNote } from "./resolution-notes.js";
|
||||
export { promptChannelAccessConfig } from "../channels/plugins/setup-group-access.js";
|
||||
|
|
|
|||
Loading…
Reference in New Issue