Matrix-js: wire account-aware client and config plumbing

This commit is contained in:
Gustavo Madeira Santana
2026-02-23 00:44:44 -05:00
parent f634d4018a
commit 877069783b
21 changed files with 277 additions and 126 deletions

View File

@@ -17,7 +17,7 @@ export const matrixMessageActions: ChannelMessageActionAdapter = {
if (!account.enabled || !account.configured) {
return [];
}
const gate = createActionGate((cfg as CoreConfig).channels?.matrix?.actions);
const gate = createActionGate((cfg as CoreConfig).channels?.["matrix-js"]?.actions);
const actions = new Set<ChannelMessageActionName>(["send", "poll"]);
if (gate("reactions")) {
actions.add("react");
@@ -203,6 +203,9 @@ export const matrixMessageActions: ChannelMessageActionAdapter = {
.toLowerCase();
const operationToAction: Record<string, string> = {
"encryption-status": "encryptionStatus",
"verification-status": "verificationStatus",
"verification-bootstrap": "verificationBootstrap",
"verification-recovery-key": "verificationRecoveryKey",
"verification-list": "verificationList",
"verification-request": "verificationRequest",
"verification-accept": "verificationAccept",
@@ -232,6 +235,6 @@ export const matrixMessageActions: ChannelMessageActionAdapter = {
);
}
throw new Error(`Action ${action} is not supported for provider matrix.`);
throw new Error(`Action ${action} is not supported for provider matrix-js.`);
},
};

View File

@@ -24,7 +24,7 @@ describe("matrix directory", () => {
it("lists peers and groups from config", async () => {
const cfg = {
channels: {
matrix: {
"matrix-js": {
dm: { allowFrom: ["matrix:@alice:example.org", "bob"] },
groupAllowFrom: ["@dana:example.org"],
groups: {
@@ -75,7 +75,7 @@ describe("matrix directory", () => {
it("resolves replyToMode from account config", () => {
const cfg = {
channels: {
matrix: {
"matrix-js": {
replyToMode: "off",
accounts: {
Assistant: {
@@ -106,7 +106,7 @@ describe("matrix directory", () => {
it("resolves group mention policy from account config", () => {
const cfg = {
channels: {
matrix: {
"matrix-js": {
groups: {
"!room:example.org": { requireMention: true },
},

View File

@@ -38,9 +38,9 @@ import type { CoreConfig } from "./types.js";
let matrixStartupLock: Promise<void> = Promise.resolve();
const meta = {
id: "matrix",
label: "Matrix",
selectionLabel: "Matrix (plugin)",
id: "matrix-js",
label: "Matrix-js",
selectionLabel: "Matrix-js (plugin)",
docsPath: "/channels/matrix",
docsLabel: "matrix",
blurb: "open protocol; configure a homeserver + access token.",
@@ -73,12 +73,12 @@ function buildMatrixConfigUpdate(
initialSyncLimit?: number;
},
): CoreConfig {
const existing = cfg.channels?.matrix ?? {};
const existing = cfg.channels?.["matrix-js"] ?? {};
return {
...cfg,
channels: {
...cfg.channels,
matrix: {
"matrix-js": {
...existing,
enabled: true,
...(input.homeserver ? { homeserver: input.homeserver } : {}),
@@ -96,7 +96,7 @@ function buildMatrixConfigUpdate(
}
export const matrixPlugin: ChannelPlugin<ResolvedMatrixAccount> = {
id: "matrix",
id: "matrix-js",
meta,
onboarding: matrixOnboardingAdapter,
pairing: {
@@ -113,7 +113,7 @@ export const matrixPlugin: ChannelPlugin<ResolvedMatrixAccount> = {
threads: true,
media: true,
},
reload: { configPrefixes: ["channels.matrix"] },
reload: { configPrefixes: ["channels.matrix-js"] },
configSchema: buildChannelConfigSchema(MatrixConfigSchema),
config: {
listAccountIds: (cfg) => listMatrixAccountIds(cfg as CoreConfig),
@@ -122,7 +122,7 @@ export const matrixPlugin: ChannelPlugin<ResolvedMatrixAccount> = {
setAccountEnabled: ({ cfg, accountId, enabled }) =>
setAccountEnabledInConfigSection({
cfg: cfg as CoreConfig,
sectionKey: "matrix",
sectionKey: "matrix-js",
accountId,
enabled,
allowTopLevel: true,
@@ -130,7 +130,7 @@ export const matrixPlugin: ChannelPlugin<ResolvedMatrixAccount> = {
deleteAccount: ({ cfg, accountId }) =>
deleteAccountFromConfigSection({
cfg: cfg as CoreConfig,
sectionKey: "matrix",
sectionKey: "matrix-js",
accountId,
clearBaseFields: [
"name",
@@ -162,21 +162,21 @@ export const matrixPlugin: ChannelPlugin<ResolvedMatrixAccount> = {
const accountId = account.accountId;
const prefix =
accountId && accountId !== "default"
? `channels.matrix.accounts.${accountId}.dm`
: "channels.matrix.dm";
? `channels.matrix-js.accounts.${accountId}.dm`
: "channels.matrix-js.dm";
return {
policy: account.config.dm?.policy ?? "pairing",
allowFrom: account.config.dm?.allowFrom ?? [],
policyPath: `${prefix}.policy`,
allowFromPath: `${prefix}.allowFrom`,
approveHint: formatPairingApproveHint("matrix"),
approveHint: formatPairingApproveHint("matrix-js"),
normalizeEntry: (raw) => normalizeMatrixUserId(raw),
};
},
collectWarnings: ({ account, cfg }) => {
const defaultGroupPolicy = resolveDefaultGroupPolicy(cfg as CoreConfig);
const { groupPolicy } = resolveAllowlistProviderRuntimeGroupPolicy({
providerConfigPresent: (cfg as CoreConfig).channels?.matrix !== undefined,
providerConfigPresent: (cfg as CoreConfig).channels?.["matrix-js"] !== undefined,
groupPolicy: account.config.groupPolicy,
defaultGroupPolicy,
});
@@ -184,7 +184,7 @@ export const matrixPlugin: ChannelPlugin<ResolvedMatrixAccount> = {
return [];
}
return [
'- Matrix rooms: groupPolicy="open" allows any room to trigger (mention-gated). Set channels.matrix.groupPolicy="allowlist" + channels.matrix.groups (and optionally channels.matrix.groupAllowFrom) to restrict rooms.',
'- Matrix rooms: groupPolicy="open" allows any room to trigger (mention-gated). Set channels.matrix-js.groupPolicy="allowlist" + channels.matrix-js.groups (and optionally channels.matrix-js.groupAllowFrom) to restrict rooms.',
];
},
},
@@ -316,7 +316,7 @@ export const matrixPlugin: ChannelPlugin<ResolvedMatrixAccount> = {
applyAccountName: ({ cfg, accountId, name }) =>
applyAccountNameToChannelSection({
cfg: cfg as CoreConfig,
channelKey: "matrix",
channelKey: "matrix-js",
accountId,
name,
}),
@@ -346,7 +346,7 @@ export const matrixPlugin: ChannelPlugin<ResolvedMatrixAccount> = {
applyAccountConfig: ({ cfg, input }) => {
const namedConfig = applyAccountNameToChannelSection({
cfg: cfg as CoreConfig,
channelKey: "matrix",
channelKey: "matrix-js",
accountId: DEFAULT_ACCOUNT_ID,
name: input.name,
});
@@ -355,8 +355,8 @@ export const matrixPlugin: ChannelPlugin<ResolvedMatrixAccount> = {
...namedConfig,
channels: {
...namedConfig.channels,
matrix: {
...namedConfig.channels?.matrix,
"matrix-js": {
...namedConfig.channels?.["matrix-js"],
enabled: true,
},
},
@@ -389,7 +389,7 @@ export const matrixPlugin: ChannelPlugin<ResolvedMatrixAccount> = {
}
return [
{
channel: "matrix",
channel: "matrix-js",
accountId: account.accountId,
kind: "runtime",
message: `Channel error: ${lastError}`,

View File

@@ -7,7 +7,7 @@ vi.mock("./matrix/client.js", () => ({
}));
describe("matrix directory live", () => {
const cfg = { channels: { matrix: {} } };
const cfg = { channels: { "matrix-js": {} } };
beforeEach(() => {
vi.mocked(resolveMatrixAuth).mockReset();

View File

@@ -40,7 +40,7 @@ describe("resolveMatrixAccount", () => {
it("treats access-token-only config as configured", () => {
const cfg: CoreConfig = {
channels: {
matrix: {
"matrix-js": {
homeserver: "https://matrix.example.org",
accessToken: "tok-access",
},
@@ -54,7 +54,7 @@ describe("resolveMatrixAccount", () => {
it("requires userId + password when no access token is set", () => {
const cfg: CoreConfig = {
channels: {
matrix: {
"matrix-js": {
homeserver: "https://matrix.example.org",
userId: "@bot:example.org",
},
@@ -68,7 +68,7 @@ describe("resolveMatrixAccount", () => {
it("marks password auth as configured when userId is present", () => {
const cfg: CoreConfig = {
channels: {
matrix: {
"matrix-js": {
homeserver: "https://matrix.example.org",
userId: "@bot:example.org",
password: "secret",

View File

@@ -30,7 +30,7 @@ export type ResolvedMatrixAccount = {
};
function listConfiguredAccountIds(cfg: CoreConfig): string[] {
const accounts = cfg.channels?.matrix?.accounts;
const accounts = cfg.channels?.["matrix-js"]?.accounts;
if (!accounts || typeof accounts !== "object") {
return [];
}
@@ -62,7 +62,7 @@ export function resolveDefaultMatrixAccountId(cfg: CoreConfig): string {
}
function resolveAccountConfig(cfg: CoreConfig, accountId: string): MatrixConfig | undefined {
const accounts = cfg.channels?.matrix?.accounts;
const accounts = cfg.channels?.["matrix-js"]?.accounts;
if (!accounts || typeof accounts !== "object") {
return undefined;
}
@@ -85,7 +85,7 @@ export function resolveMatrixAccount(params: {
accountId?: string | null;
}): ResolvedMatrixAccount {
const accountId = normalizeAccountId(params.accountId);
const matrixBase = params.cfg.channels?.matrix ?? {};
const matrixBase = params.cfg.channels?.["matrix-js"] ?? {};
const base = resolveMatrixAccountConfig({ cfg: params.cfg, accountId });
const enabled = base.enabled !== false && matrixBase.enabled !== false;
@@ -120,7 +120,7 @@ export function resolveMatrixAccountConfig(params: {
accountId?: string | null;
}): MatrixConfig {
const accountId = normalizeAccountId(params.accountId);
const matrixBase = params.cfg.channels?.matrix ?? {};
const matrixBase = params.cfg.channels?.["matrix-js"] ?? {};
const accountConfig = resolveAccountConfig(params.cfg, accountId);
if (!accountConfig) {
return matrixBase;

View File

@@ -22,7 +22,7 @@ export async function resolveActionClient(
if (opts.client) {
return { client: opts.client, stopOnDone: false };
}
const active = getActiveMatrixClient();
const active = getActiveMatrixClient(opts.accountId);
if (active) {
return { client: active, stopOnDone: false };
}
@@ -31,11 +31,13 @@ export async function resolveActionClient(
const client = await resolveSharedMatrixClient({
cfg: getMatrixRuntime().config.loadConfig() as CoreConfig,
timeoutMs: opts.timeoutMs,
accountId: opts.accountId,
});
return { client, stopOnDone: false };
}
const auth = await resolveMatrixAuth({
cfg: getMatrixRuntime().config.loadConfig() as CoreConfig,
accountId: opts.accountId,
});
const client = await createMatrixClient({
homeserver: auth.homeserver,
@@ -45,6 +47,7 @@ export async function resolveActionClient(
deviceId: auth.deviceId,
encryption: auth.encryption,
localTimeoutMs: opts.timeoutMs,
accountId: opts.accountId,
});
if (auth.encryption && client.crypto) {
try {

View File

@@ -1,11 +1,26 @@
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/account-id";
import type { MatrixClient } from "./sdk.js";
let activeClient: MatrixClient | null = null;
const activeClients = new Map<string, MatrixClient>();
export function setActiveMatrixClient(client: MatrixClient | null): void {
activeClient = client;
function resolveAccountKey(accountId?: string | null): string {
const normalized = normalizeAccountId(accountId);
return normalized || DEFAULT_ACCOUNT_ID;
}
export function getActiveMatrixClient(): MatrixClient | null {
return activeClient;
export function setActiveMatrixClient(
client: MatrixClient | null,
accountId?: string | null,
): void {
const key = resolveAccountKey(accountId);
if (!client) {
activeClients.delete(key);
return;
}
activeClients.set(key, client);
}
export function getActiveMatrixClient(accountId?: string | null): MatrixClient | null {
const key = resolveAccountKey(accountId);
return activeClients.get(key) ?? null;
}

View File

@@ -26,7 +26,7 @@ describe("resolveMatrixConfig", () => {
it("prefers config over env", () => {
const cfg = {
channels: {
matrix: {
"matrix-js": {
homeserver: "https://cfg.example.org",
userId: "@cfg:example.org",
accessToken: "cfg-token",
@@ -82,7 +82,7 @@ describe("resolveMatrixConfig", () => {
it("reads register flag from config and env", () => {
const cfg = {
channels: {
matrix: {
"matrix-js": {
register: true,
},
},
@@ -118,7 +118,7 @@ describe("resolveMatrixAuth", () => {
const cfg = {
channels: {
matrix: {
"matrix-js": {
homeserver: "https://matrix.example.org",
userId: "@bot:example.org",
password: "secret",
@@ -154,6 +154,8 @@ describe("resolveMatrixAuth", () => {
accessToken: "tok-123",
deviceId: "DEVICE123",
}),
expect.any(Object),
undefined,
);
});
@@ -169,7 +171,7 @@ describe("resolveMatrixAuth", () => {
const cfg = {
channels: {
matrix: {
"matrix-js": {
homeserver: "https://matrix.example.org",
userId: "@newbot:example.org",
password: "secret",
@@ -224,7 +226,7 @@ describe("resolveMatrixAuth", () => {
});
});
it("ignores cached credentials when matrix.register=true", async () => {
it("ignores cached credentials when matrix-js.register=true", async () => {
vi.mocked(credentialsModule.loadMatrixCredentials).mockReturnValue({
homeserver: "https://matrix.example.org",
userId: "@bot:example.org",
@@ -242,7 +244,7 @@ describe("resolveMatrixAuth", () => {
const cfg = {
channels: {
matrix: {
"matrix-js": {
homeserver: "https://matrix.example.org",
userId: "@bot:example.org",
password: "secret",
@@ -265,10 +267,10 @@ describe("resolveMatrixAuth", () => {
expect(prepareMatrixRegisterModeMock).toHaveBeenCalledTimes(1);
});
it("requires matrix.password when matrix.register=true", async () => {
it("requires matrix-js.password when matrix-js.register=true", async () => {
const cfg = {
channels: {
matrix: {
"matrix-js": {
homeserver: "https://matrix.example.org",
userId: "@bot:example.org",
register: true,
@@ -277,16 +279,16 @@ describe("resolveMatrixAuth", () => {
} as CoreConfig;
await expect(resolveMatrixAuth({ cfg, env: {} as NodeJS.ProcessEnv })).rejects.toThrow(
"Matrix password is required when matrix.register=true",
"Matrix password is required when matrix-js.register=true",
);
expect(prepareMatrixRegisterModeMock).not.toHaveBeenCalled();
expect(finalizeMatrixRegisterConfigAfterSuccessMock).not.toHaveBeenCalled();
});
it("requires matrix.userId when matrix.register=true", async () => {
it("requires matrix-js.userId when matrix-js.register=true", async () => {
const cfg = {
channels: {
matrix: {
"matrix-js": {
homeserver: "https://matrix.example.org",
password: "secret",
register: true,
@@ -295,7 +297,7 @@ describe("resolveMatrixAuth", () => {
} as CoreConfig;
await expect(resolveMatrixAuth({ cfg, env: {} as NodeJS.ProcessEnv })).rejects.toThrow(
"Matrix userId is required when matrix.register=true",
"Matrix userId is required when matrix-js.register=true",
);
expect(prepareMatrixRegisterModeMock).not.toHaveBeenCalled();
expect(finalizeMatrixRegisterConfigAfterSuccessMock).not.toHaveBeenCalled();
@@ -312,7 +314,7 @@ describe("resolveMatrixAuth", () => {
const cfg = {
channels: {
matrix: {
"matrix-js": {
homeserver: "https://matrix.example.org",
userId: "@bot:example.org",
accessToken: "tok-123",
@@ -332,6 +334,8 @@ describe("resolveMatrixAuth", () => {
accessToken: "tok-123",
deviceId: "DEVICE123",
}),
expect.any(Object),
undefined,
);
});
@@ -343,7 +347,7 @@ describe("resolveMatrixAuth", () => {
const cfg = {
channels: {
matrix: {
"matrix-js": {
homeserver: "https://matrix.example.org",
accessToken: "tok-123",
encryption: true,
@@ -377,7 +381,7 @@ describe("resolveMatrixAuth", () => {
const cfg = {
channels: {
matrix: {
"matrix-js": {
homeserver: "https://matrix.example.org",
userId: "@bot:example.org",
deviceId: "DEVICE123",

View File

@@ -1,3 +1,4 @@
import { normalizeAccountId } from "openclaw/plugin-sdk/account-id";
import { getMatrixRuntime } from "../../runtime.js";
import { MatrixClient } from "../sdk.js";
import type { CoreConfig } from "../types.js";
@@ -32,6 +33,27 @@ function parseOptionalBoolean(value: unknown): boolean | undefined {
return undefined;
}
function findAccountConfig(cfg: CoreConfig, accountId: string): Record<string, unknown> {
const accounts = cfg.channels?.["matrix-js"]?.accounts;
if (!accounts || typeof accounts !== "object") {
return {};
}
if (accounts[accountId] && typeof accounts[accountId] === "object") {
return accounts[accountId] as Record<string, unknown>;
}
const normalized = normalizeAccountId(accountId);
for (const key of Object.keys(accounts)) {
if (normalizeAccountId(key) === normalized) {
const candidate = accounts[key];
if (candidate && typeof candidate === "object") {
return candidate as Record<string, unknown>;
}
return {};
}
}
return {};
}
function resolveMatrixLocalpart(userId: string): string {
const trimmed = userId.trim();
const noPrefix = trimmed.startsWith("@") ? trimmed.slice(1) : trimmed;
@@ -100,7 +122,7 @@ export function resolveMatrixConfig(
cfg: CoreConfig = getMatrixRuntime().config.loadConfig() as CoreConfig,
env: NodeJS.ProcessEnv = process.env,
): MatrixResolvedConfig {
const matrix = cfg.channels?.matrix ?? {};
const matrix = cfg.channels?.["matrix-js"] ?? {};
const homeserver = clean(matrix.homeserver) || clean(env.MATRIX_HOMESERVER);
const userId = clean(matrix.userId) || clean(env.MATRIX_USER_ID);
const accessToken = clean(matrix.accessToken) || clean(env.MATRIX_ACCESS_TOKEN) || undefined;
@@ -127,16 +149,86 @@ export function resolveMatrixConfig(
};
}
export function resolveMatrixConfigForAccount(
cfg: CoreConfig,
accountId: string,
env: NodeJS.ProcessEnv = process.env,
): MatrixResolvedConfig {
const matrix = cfg.channels?.["matrix-js"] ?? {};
const account = findAccountConfig(cfg, accountId);
const accountHomeserver = clean(
typeof account.homeserver === "string" ? account.homeserver : undefined,
);
const accountUserId = clean(typeof account.userId === "string" ? account.userId : undefined);
const accountAccessToken = clean(
typeof account.accessToken === "string" ? account.accessToken : undefined,
);
const accountPassword = clean(
typeof account.password === "string" ? account.password : undefined,
);
const accountDeviceId = clean(
typeof account.deviceId === "string" ? account.deviceId : undefined,
);
const accountDeviceName = clean(
typeof account.deviceName === "string" ? account.deviceName : undefined,
);
const homeserver = accountHomeserver || clean(matrix.homeserver) || clean(env.MATRIX_HOMESERVER);
const userId = accountUserId || clean(matrix.userId) || clean(env.MATRIX_USER_ID);
const accessToken =
accountAccessToken || clean(matrix.accessToken) || clean(env.MATRIX_ACCESS_TOKEN) || undefined;
const password =
accountPassword || clean(matrix.password) || clean(env.MATRIX_PASSWORD) || undefined;
const register =
parseOptionalBoolean(account.register) ??
parseOptionalBoolean(matrix.register) ??
parseOptionalBoolean(env.MATRIX_REGISTER) ??
false;
const deviceId =
accountDeviceId || clean(matrix.deviceId) || clean(env.MATRIX_DEVICE_ID) || undefined;
const deviceName =
accountDeviceName || clean(matrix.deviceName) || clean(env.MATRIX_DEVICE_NAME) || undefined;
const accountInitialSyncLimit =
typeof account.initialSyncLimit === "number"
? Math.max(0, Math.floor(account.initialSyncLimit))
: undefined;
const initialSyncLimit =
accountInitialSyncLimit ??
(typeof matrix.initialSyncLimit === "number"
? Math.max(0, Math.floor(matrix.initialSyncLimit))
: undefined);
const encryption =
typeof account.encryption === "boolean" ? account.encryption : (matrix.encryption ?? false);
return {
homeserver,
userId,
accessToken,
password,
register,
deviceId,
deviceName,
initialSyncLimit,
encryption,
};
}
export async function resolveMatrixAuth(params?: {
cfg?: CoreConfig;
env?: NodeJS.ProcessEnv;
accountId?: string | null;
}): Promise<MatrixAuth> {
const cfg = params?.cfg ?? (getMatrixRuntime().config.loadConfig() as CoreConfig);
const env = params?.env ?? process.env;
const resolved = resolveMatrixConfig(cfg, env);
const registerFromConfig = cfg.channels?.matrix?.register === true;
const accountId = params?.accountId;
const resolved = accountId
? resolveMatrixConfigForAccount(cfg, accountId, env)
: resolveMatrixConfig(cfg, env);
const registerFromConfig = resolved.register === true;
if (!resolved.homeserver) {
throw new Error("Matrix homeserver is required (matrix.homeserver)");
throw new Error("Matrix homeserver is required (matrix-js.homeserver)");
}
const {
@@ -146,7 +238,7 @@ export async function resolveMatrixAuth(params?: {
touchMatrixCredentials,
} = await import("../credentials.js");
const cached = loadMatrixCredentials(env);
const cached = loadMatrixCredentials(env, accountId);
const cachedCredentials =
cached &&
credentialsMatchConfig(cached, {
@@ -158,10 +250,10 @@ export async function resolveMatrixAuth(params?: {
if (registerFromConfig) {
if (!resolved.userId) {
throw new Error("Matrix userId is required when matrix.register=true");
throw new Error("Matrix userId is required when matrix-js.register=true");
}
if (!resolved.password) {
throw new Error("Matrix password is required when matrix.register=true");
throw new Error("Matrix password is required when matrix-js.register=true");
}
await prepareMatrixRegisterMode({
cfg,
@@ -205,14 +297,18 @@ export async function resolveMatrixAuth(params?: {
cachedCredentials.userId !== userId ||
(cachedCredentials.deviceId || undefined) !== knownDeviceId;
if (shouldRefreshCachedCredentials) {
saveMatrixCredentials({
homeserver: resolved.homeserver,
userId,
accessToken: resolved.accessToken,
deviceId: knownDeviceId,
});
saveMatrixCredentials(
{
homeserver: resolved.homeserver,
userId,
accessToken: resolved.accessToken,
deviceId: knownDeviceId,
},
env,
accountId,
);
} else if (hasMatchingCachedToken) {
touchMatrixCredentials(env);
touchMatrixCredentials(env, accountId);
}
return {
homeserver: resolved.homeserver,
@@ -227,7 +323,7 @@ export async function resolveMatrixAuth(params?: {
}
if (cachedCredentials && !registerFromConfig) {
touchMatrixCredentials(env);
touchMatrixCredentials(env, accountId);
return {
homeserver: cachedCredentials.homeserver,
userId: cachedCredentials.userId,
@@ -241,12 +337,14 @@ export async function resolveMatrixAuth(params?: {
}
if (!resolved.userId) {
throw new Error("Matrix userId is required when no access token is configured (matrix.userId)");
throw new Error(
"Matrix userId is required when no access token is configured (matrix-js.userId)",
);
}
if (!resolved.password) {
throw new Error(
"Matrix password is required when no access token is configured (matrix.password)",
"Matrix password is required when no access token is configured (matrix-js.password)",
);
}
@@ -308,12 +406,16 @@ export async function resolveMatrixAuth(params?: {
encryption: resolved.encryption,
};
saveMatrixCredentials({
homeserver: auth.homeserver,
userId: auth.userId,
accessToken: auth.accessToken,
deviceId: auth.deviceId,
});
saveMatrixCredentials(
{
homeserver: auth.homeserver,
userId: auth.userId,
accessToken: auth.accessToken,
deviceId: auth.deviceId,
},
env,
accountId,
);
if (registerFromConfig) {
await finalizeMatrixRegisterConfigAfterSuccess({

View File

@@ -40,7 +40,7 @@ export async function createMatrixClient(params: {
accountId: params.accountId,
});
const cryptoDatabasePrefix = `openclaw-matrix-${storagePaths.accountKey}-${storagePaths.tokenHash}`;
const cryptoDatabasePrefix = `openclaw-matrix-js-${storagePaths.accountKey}-${storagePaths.tokenHash}`;
return new MatrixClient(params.homeserver, params.accessToken, undefined, undefined, {
userId: matrixClientUserId,

View File

@@ -24,7 +24,7 @@ describe("matrix register mode helpers", () => {
it("moves existing matrix state into a .bak snapshot before fresh registration", async () => {
const stateDir = fs.mkdtempSync(path.join(os.tmpdir(), "matrix-register-mode-"));
tempDirs.push(stateDir);
const credentialsDir = path.join(stateDir, "credentials", "matrix");
const credentialsDir = path.join(stateDir, "credentials", "matrix-js");
const accountsDir = path.join(credentialsDir, "accounts");
fs.mkdirSync(accountsDir, { recursive: true });
fs.writeFileSync(path.join(credentialsDir, "credentials.json"), '{"accessToken":"old"}\n');
@@ -32,7 +32,7 @@ describe("matrix register mode helpers", () => {
const cfg = {
channels: {
matrix: {
"matrix-js": {
userId: "@pinguini:matrix.gumadeiras.com",
register: true,
encryption: true,
@@ -62,7 +62,7 @@ describe("matrix register mode helpers", () => {
loadConfig: () =>
({
channels: {
matrix: {
"matrix-js": {
register: true,
accessToken: "stale-token",
userId: "@pinguini:matrix.gumadeiras.com",
@@ -82,7 +82,7 @@ describe("matrix register mode helpers", () => {
expect(writeConfigFile).toHaveBeenCalledWith(
expect.objectContaining({
channels: expect.objectContaining({
matrix: expect.objectContaining({
"matrix-js": expect.objectContaining({
register: false,
homeserver: "https://matrix.gumadeiras.com",
userId: "@pinguini:matrix.gumadeiras.com",
@@ -92,6 +92,6 @@ describe("matrix register mode helpers", () => {
}),
);
const written = writeConfigFile.mock.calls[0]?.[0] as CoreConfig;
expect(written.channels?.matrix?.accessToken).toBeUndefined();
expect(written.channels?.["matrix-js"]?.accessToken).toBeUndefined();
});
});

View File

@@ -65,7 +65,7 @@ export async function prepareMatrixRegisterMode(params: {
const backupDir = path.join(backupRoot, buildBackupDirName());
fs.mkdirSync(backupDir, { recursive: true });
const matrixConfig = params.cfg.channels?.matrix ?? {};
const matrixConfig = params.cfg.channels?.["matrix-js"] ?? {};
fs.writeFileSync(
path.join(backupDir, "matrix-config.json"),
JSON.stringify(matrixConfig, null, 2).trimEnd().concat("\n"),
@@ -93,11 +93,11 @@ export async function finalizeMatrixRegisterConfigAfterSuccess(params: {
}
const cfg = runtime.config.loadConfig() as CoreConfig;
if (cfg.channels?.matrix?.register !== true) {
if (cfg.channels?.["matrix-js"]?.register !== true) {
return false;
}
const matrixCfg = cfg.channels?.matrix ?? {};
const matrixCfg = cfg.channels?.["matrix-js"] ?? {};
const nextMatrix: Record<string, unknown> = {
...matrixCfg,
register: false,
@@ -112,7 +112,7 @@ export async function finalizeMatrixRegisterConfigAfterSuccess(params: {
...cfg,
channels: {
...(cfg.channels ?? {}),
matrix: nextMatrix as CoreConfig["channels"]["matrix"],
"matrix-js": nextMatrix as CoreConfig["channels"]["matrix-js"],
},
};

View File

@@ -100,7 +100,13 @@ export async function resolveSharedMatrixClient(
accountId?: string | null;
} = {},
): Promise<MatrixClient> {
const auth = params.auth ?? (await resolveMatrixAuth({ cfg: params.cfg, env: params.env }));
const auth =
params.auth ??
(await resolveMatrixAuth({
cfg: params.cfg,
env: params.env,
accountId: params.accountId,
}));
const key = buildSharedClientKey(auth, params.accountId);
const shouldStart = params.startClient !== false;
@@ -171,3 +177,15 @@ export function stopSharedClient(): void {
sharedClientState = null;
}
}
export function stopSharedClientForAccount(auth: MatrixAuth, accountId?: string | null): void {
if (!sharedClientState) {
return;
}
const key = buildSharedClientKey(auth, accountId);
if (sharedClientState.key !== key) {
return;
}
sharedClientState.client.stop();
sharedClientState = null;
}

View File

@@ -39,8 +39,8 @@ function resolveLegacyStoragePaths(env: NodeJS.ProcessEnv = process.env): {
} {
const stateDir = getMatrixRuntime().state.resolveStateDir(env, os.homedir);
return {
storagePath: path.join(stateDir, "credentials", "matrix", "bot-storage.json"),
cryptoPath: path.join(stateDir, "credentials", "matrix", "crypto"),
storagePath: path.join(stateDir, "credentials", "matrix-js", "bot-storage.json"),
cryptoPath: path.join(stateDir, "credentials", "matrix-js", "crypto"),
};
}
@@ -60,7 +60,7 @@ export function resolveMatrixStoragePaths(params: {
const rootDir = path.join(
stateDir,
"credentials",
"matrix",
"matrix-js",
"accounts",
accountKey,
`${serverKey}__${userKey}`,

View File

@@ -28,7 +28,7 @@ export function resolveMatrixCredentialsDir(
stateDir?: string,
): string {
const resolvedStateDir = stateDir ?? getMatrixRuntime().state.resolveStateDir(env, os.homedir);
return path.join(resolvedStateDir, "credentials", "matrix");
return path.join(resolvedStateDir, "credentials", "matrix-js");
}
export function resolveMatrixCredentialsPath(

View File

@@ -52,16 +52,16 @@ export async function sendMessageMatrix(
const cfg = getCore().config.loadConfig();
const tableMode = getCore().channel.text.resolveMarkdownTableMode({
cfg,
channel: "matrix",
channel: "matrix-js",
accountId: opts.accountId,
});
const convertedMessage = getCore().channel.text.convertMarkdownTables(
trimmedMessage,
tableMode,
);
const textLimit = getCore().channel.text.resolveTextChunkLimit(cfg, "matrix");
const textLimit = getCore().channel.text.resolveTextChunkLimit(cfg, "matrix-js");
const chunkLimit = Math.min(textLimit, MATRIX_TEXT_LIMIT);
const chunkMode = getCore().channel.text.resolveChunkMode(cfg, "matrix", opts.accountId);
const chunkMode = getCore().channel.text.resolveChunkMode(cfg, "matrix-js", opts.accountId);
const chunks = getCore().channel.text.chunkMarkdownTextWithMode(
convertedMessage,
chunkLimit,

View File

@@ -19,8 +19,8 @@ export function ensureNodeRuntime() {
export function resolveMediaMaxBytes(): number | undefined {
const cfg = getCore().config.loadConfig() as CoreConfig;
if (typeof cfg.channels?.matrix?.mediaMaxMb === "number") {
return cfg.channels.matrix.mediaMaxMb * 1024 * 1024;
if (typeof cfg.channels?.["matrix-js"]?.mediaMaxMb === "number") {
return cfg.channels["matrix-js"].mediaMaxMb * 1024 * 1024;
}
return undefined;
}
@@ -28,12 +28,13 @@ export function resolveMediaMaxBytes(): number | undefined {
export async function resolveMatrixClient(opts: {
client?: MatrixClient;
timeoutMs?: number;
accountId?: string | null;
}): Promise<{ client: MatrixClient; stopOnDone: boolean }> {
ensureNodeRuntime();
if (opts.client) {
return { client: opts.client, stopOnDone: false };
}
const active = getActiveMatrixClient();
const active = getActiveMatrixClient(opts.accountId);
if (active) {
return { client: active, stopOnDone: false };
}
@@ -41,10 +42,11 @@ export async function resolveMatrixClient(opts: {
if (shouldShareClient) {
const client = await resolveSharedMatrixClient({
timeoutMs: opts.timeoutMs,
accountId: opts.accountId,
});
return { client, stopOnDone: false };
}
const auth = await resolveMatrixAuth();
const auth = await resolveMatrixAuth({ accountId: opts.accountId });
const client = await createMatrixClient({
homeserver: auth.homeserver,
userId: auth.userId,
@@ -53,6 +55,7 @@ export async function resolveMatrixClient(opts: {
deviceId: auth.deviceId,
encryption: auth.encryption,
localTimeoutMs: opts.timeoutMs,
accountId: opts.accountId,
});
if (auth.encryption && client.crypto) {
try {

View File

@@ -14,19 +14,21 @@ import { ensureMatrixSdkInstalled, isMatrixSdkAvailable } from "./matrix/deps.js
import { resolveMatrixTargets } from "./resolve-targets.js";
import type { CoreConfig } from "./types.js";
const channel = "matrix" as const;
const channel = "matrix-js" as const;
function setMatrixDmPolicy(cfg: CoreConfig, policy: DmPolicy) {
const allowFrom =
policy === "open" ? addWildcardAllowFrom(cfg.channels?.matrix?.dm?.allowFrom) : undefined;
policy === "open"
? addWildcardAllowFrom(cfg.channels?.["matrix-js"]?.dm?.allowFrom)
: undefined;
return {
...cfg,
channels: {
...cfg.channels,
matrix: {
...cfg.channels?.matrix,
"matrix-js": {
...cfg.channels?.["matrix-js"],
dm: {
...cfg.channels?.matrix?.dm,
...cfg.channels?.["matrix-js"]?.dm,
policy,
...(allowFrom ? { allowFrom } : {}),
},
@@ -54,7 +56,7 @@ async function promptMatrixAllowFrom(params: {
prompter: WizardPrompter;
}): Promise<CoreConfig> {
const { cfg, prompter } = params;
const existingAllowFrom = cfg.channels?.matrix?.dm?.allowFrom ?? [];
const existingAllowFrom = cfg.channels?.["matrix-js"]?.dm?.allowFrom ?? [];
const account = resolveMatrixAccount({ cfg });
const canResolve = Boolean(account.configured);
@@ -125,11 +127,11 @@ async function promptMatrixAllowFrom(params: {
...cfg,
channels: {
...cfg.channels,
matrix: {
...cfg.channels?.matrix,
"matrix-js": {
...cfg.channels?.["matrix-js"],
enabled: true,
dm: {
...cfg.channels?.matrix?.dm,
...cfg.channels?.["matrix-js"]?.dm,
policy: "allowlist",
allowFrom: unique,
},
@@ -144,8 +146,8 @@ function setMatrixGroupPolicy(cfg: CoreConfig, groupPolicy: "open" | "allowlist"
...cfg,
channels: {
...cfg.channels,
matrix: {
...cfg.channels?.matrix,
"matrix-js": {
...cfg.channels?.["matrix-js"],
enabled: true,
groupPolicy,
},
@@ -159,8 +161,8 @@ function setMatrixGroupRooms(cfg: CoreConfig, roomKeys: string[]) {
...cfg,
channels: {
...cfg.channels,
matrix: {
...cfg.channels?.matrix,
"matrix-js": {
...cfg.channels?.["matrix-js"],
enabled: true,
groups,
},
@@ -171,9 +173,9 @@ function setMatrixGroupRooms(cfg: CoreConfig, roomKeys: string[]) {
const dmPolicy: ChannelOnboardingDmPolicy = {
label: "Matrix",
channel,
policyKey: "channels.matrix.dm.policy",
allowFromKey: "channels.matrix.dm.allowFrom",
getCurrent: (cfg) => (cfg as CoreConfig).channels?.matrix?.dm?.policy ?? "pairing",
policyKey: "channels.matrix-js.dm.policy",
allowFromKey: "channels.matrix-js.dm.allowFrom",
getCurrent: (cfg) => (cfg as CoreConfig).channels?.["matrix-js"]?.dm?.policy ?? "pairing",
setPolicy: (cfg, policy) => setMatrixDmPolicy(cfg as CoreConfig, policy),
promptAllowFrom: promptMatrixAllowFrom,
};
@@ -203,7 +205,7 @@ export const matrixOnboardingAdapter: ChannelOnboardingAdapter = {
initialValue: true,
}),
});
const existing = next.channels?.matrix ?? {};
const existing = next.channels?.["matrix-js"] ?? {};
const account = resolveMatrixAccount({ cfg: next });
if (!account.configured) {
await noteMatrixAuthHelp(prompter);
@@ -231,8 +233,8 @@ export const matrixOnboardingAdapter: ChannelOnboardingAdapter = {
...next,
channels: {
...next.channels,
matrix: {
...next.channels?.matrix,
"matrix-js": {
...next.channels?.["matrix-js"],
enabled: true,
},
},
@@ -352,8 +354,8 @@ export const matrixOnboardingAdapter: ChannelOnboardingAdapter = {
...next,
channels: {
...next.channels,
matrix: {
...next.channels?.matrix,
"matrix-js": {
...next.channels?.["matrix-js"],
enabled: true,
homeserver,
userId: userId || undefined,
@@ -370,11 +372,12 @@ export const matrixOnboardingAdapter: ChannelOnboardingAdapter = {
next = await promptMatrixAllowFrom({ cfg: next, prompter });
}
const existingGroups = next.channels?.matrix?.groups ?? next.channels?.matrix?.rooms;
const existingGroups =
next.channels?.["matrix-js"]?.groups ?? next.channels?.["matrix-js"]?.rooms;
const accessConfig = await promptChannelAccessConfig({
prompter,
label: "Matrix rooms",
currentPolicy: next.channels?.matrix?.groupPolicy ?? "allowlist",
currentPolicy: next.channels?.["matrix-js"]?.groupPolicy ?? "allowlist",
currentEntries: Object.keys(existingGroups ?? {}),
placeholder: "!roomId:server, #alias:server, Project Room",
updatePrompt: Boolean(existingGroups),
@@ -446,7 +449,7 @@ export const matrixOnboardingAdapter: ChannelOnboardingAdapter = {
...(cfg as CoreConfig),
channels: {
...(cfg as CoreConfig).channels,
matrix: { ...(cfg as CoreConfig).channels?.matrix, enabled: false },
"matrix-js": { ...(cfg as CoreConfig).channels?.["matrix-js"], enabled: false },
},
}),
};

View File

@@ -17,7 +17,7 @@ export const matrixOutbound: ChannelOutboundAdapter = {
accountId: accountId ?? undefined,
});
return {
channel: "matrix",
channel: "matrix-js",
messageId: result.messageId,
roomId: result.roomId,
};
@@ -33,7 +33,7 @@ export const matrixOutbound: ChannelOutboundAdapter = {
accountId: accountId ?? undefined,
});
return {
channel: "matrix",
channel: "matrix-js",
messageId: result.messageId,
roomId: result.roomId,
};
@@ -46,7 +46,7 @@ export const matrixOutbound: ChannelOutboundAdapter = {
accountId: accountId ?? undefined,
});
return {
channel: "matrix",
channel: "matrix-js",
messageId: result.eventId,
roomId: result.roomId,
pollId: result.eventId,

View File

@@ -102,7 +102,7 @@ export type MatrixConfig = {
export type CoreConfig = {
channels?: {
matrix?: MatrixConfig;
"matrix-js"?: MatrixConfig;
defaults?: {
groupPolicy?: "open" | "allowlist" | "disabled";
};