mirror of
https://github.com/openclaw/openclaw.git
synced 2026-02-19 18:39:20 -05:00
feat(matrix): Add multi-account support to Matrix channel
The Matrix channel previously hardcoded `listMatrixAccountIds` to always return only `DEFAULT_ACCOUNT_ID`, ignoring any accounts configured in `channels.matrix.accounts`. This prevented running multiple Matrix bot accounts simultaneously. Changes: - Update `listMatrixAccountIds` to read from `channels.matrix.accounts` config, falling back to `DEFAULT_ACCOUNT_ID` for legacy single-account configurations - Add `resolveMatrixConfigForAccount` to resolve config for a specific account ID, merging account-specific values with top-level defaults - Update `resolveMatrixAccount` to use account-specific config when available - The multi-account config structure (channels.matrix.accounts) was not defined in the MatrixConfig type, causing TypeScript to not recognize the field. Added the accounts field to properly type the multi-account configuration. - Add stopSharedClientForAccount() to stop only the specific account's client instead of all clients when an account shuts down - Wrap dynamic import in try/finally to prevent startup mutex deadlock if the import fails - Pass accountId to resolveSharedMatrixClient(), resolveMatrixAuth(), and createMatrixClient() to ensure the correct account's credentials are used for outbound messages - Add accountId parameter to resolveMediaMaxBytes to check account-specific config before falling back to top-level config - Maintain backward compatibility with existing single-account setups This follows the same pattern already used by the WhatsApp channel for multi-account support. Fixes #3165 Fixes #3085 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
committed by
Peter Steinberger
parent
607b625aab
commit
caf5d2dd7c
@@ -236,6 +236,10 @@ Docs: https://docs.openclaw.ai
|
||||
- Memory/QMD: add `memory.qmd.searchMode` to choose `query`, `search`, or `vsearch` recall mode. (#9967, #10084)
|
||||
- Media understanding: recognize `.caf` audio attachments for transcription. (#10982) Thanks @succ985.
|
||||
- State dir: honor `OPENCLAW_STATE_DIR` for default device identity and canvas storage paths. (#4824) Thanks @kossoy.
|
||||
- Doctor/State dir: suppress repeated legacy migration warnings only for valid symlink mirrors, while keeping warnings for empty or invalid legacy trees. (#11709) Thanks @gumadeiras.
|
||||
- Tests: harden flaky hotspots by removing timer sleeps, consolidating onboarding provider-auth coverage, and improving memory test realism. (#11598) Thanks @gumadeiras.
|
||||
- macOS: honor Nix-managed defaults suite (`ai.openclaw.mac`) for nixMode to prevent onboarding from reappearing after bundle-id churn. (#12205) Thanks @joshp123.
|
||||
- Matrix: add multi-account support via `channels.matrix.accounts`; use per-account config for dm policy, allowFrom, groups, and other settings; serialize account startup to avoid race condition. (#3165, #3085) Thanks @emonty.
|
||||
|
||||
## 2026.2.6
|
||||
|
||||
@@ -332,6 +336,7 @@ Docs: https://docs.openclaw.ai
|
||||
- macOS: fix cron payload summary rendering and ISO 8601 formatter concurrency safety.
|
||||
- Discord: enforce DM allowlists for agent components (buttons/select menus), honoring pairing store approvals and tag matches. (#11254) Thanks @thedudeabidesai.
|
||||
|
||||
|
||||
## 2026.2.2-3
|
||||
|
||||
### Fixes
|
||||
|
||||
@@ -136,6 +136,47 @@ When E2EE is enabled, the bot will request verification from your other sessions
|
||||
Open Element (or another client) and approve the verification request to establish trust.
|
||||
Once verified, the bot can decrypt messages in encrypted rooms.
|
||||
|
||||
## Multi-account
|
||||
|
||||
Multi-account support: use `channels.matrix.accounts` with per-account credentials and optional `name`. See [`gateway/configuration`](/gateway/configuration#telegramaccounts--discordaccounts--slackaccounts--signalaccounts--imessageaccounts) for the shared pattern.
|
||||
|
||||
Each account runs as a separate Matrix user on any homeserver. Per-account config
|
||||
inherits from the top-level `channels.matrix` settings and can override any option
|
||||
(DM policy, groups, encryption, etc.).
|
||||
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
matrix: {
|
||||
enabled: true,
|
||||
dm: { policy: "pairing" },
|
||||
accounts: {
|
||||
assistant: {
|
||||
name: "Main assistant",
|
||||
homeserver: "https://matrix.example.org",
|
||||
accessToken: "syt_assistant_***",
|
||||
encryption: true,
|
||||
},
|
||||
alerts: {
|
||||
name: "Alerts bot",
|
||||
homeserver: "https://matrix.example.org",
|
||||
accessToken: "syt_alerts_***",
|
||||
dm: { policy: "allowlist", allowFrom: ["@admin:example.org"] },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- Account startup is serialized to avoid race conditions with concurrent module imports.
|
||||
- Env variables (`MATRIX_HOMESERVER`, `MATRIX_ACCESS_TOKEN`, etc.) only apply to the **default** account.
|
||||
- Base channel settings (DM policy, group policy, mention gating, etc.) apply to all accounts unless overridden per account.
|
||||
- Use `bindings[].match.accountId` to route each account to a different agent.
|
||||
- Crypto state is stored per account + access token (separate key stores per account).
|
||||
|
||||
## Routing model
|
||||
|
||||
- Replies always go back to Matrix.
|
||||
@@ -256,4 +297,5 @@ Provider options:
|
||||
- `channels.matrix.mediaMaxMb`: inbound/outbound media cap (MB).
|
||||
- `channels.matrix.autoJoin`: invite handling (`always | allowlist | off`, default: always).
|
||||
- `channels.matrix.autoJoinAllowlist`: allowed room IDs/aliases for auto-join.
|
||||
- `channels.matrix.accounts`: multi-account configuration keyed by account ID (each account inherits top-level settings).
|
||||
- `channels.matrix.actions`: per-action tool gating (reactions/messages/pins/memberInfo/channelInfo).
|
||||
|
||||
@@ -31,6 +31,9 @@ import { matrixOnboardingAdapter } from "./onboarding.js";
|
||||
import { matrixOutbound } from "./outbound.js";
|
||||
import { resolveMatrixTargets } from "./resolve-targets.js";
|
||||
|
||||
// Mutex for serializing account startup (workaround for concurrent dynamic import race condition)
|
||||
let matrixStartupLock: Promise<void> = Promise.resolve();
|
||||
|
||||
const meta = {
|
||||
id: "matrix",
|
||||
label: "Matrix",
|
||||
@@ -383,9 +386,12 @@ export const matrixPlugin: ChannelPlugin<ResolvedMatrixAccount> = {
|
||||
probe: snapshot.probe,
|
||||
lastProbeAt: snapshot.lastProbeAt ?? null,
|
||||
}),
|
||||
probeAccount: async ({ timeoutMs, cfg }) => {
|
||||
probeAccount: async ({ account, timeoutMs, cfg }) => {
|
||||
try {
|
||||
const auth = await resolveMatrixAuth({ cfg: cfg as CoreConfig });
|
||||
const auth = await resolveMatrixAuth({
|
||||
cfg: cfg as CoreConfig,
|
||||
accountId: account.accountId,
|
||||
});
|
||||
return await probeMatrix({
|
||||
homeserver: auth.homeserver,
|
||||
accessToken: auth.accessToken,
|
||||
@@ -424,8 +430,32 @@ export const matrixPlugin: ChannelPlugin<ResolvedMatrixAccount> = {
|
||||
baseUrl: account.homeserver,
|
||||
});
|
||||
ctx.log?.info(`[${account.accountId}] starting provider (${account.homeserver ?? "matrix"})`);
|
||||
|
||||
// Serialize startup: wait for any previous startup to complete import phase.
|
||||
// This works around a race condition with concurrent dynamic imports.
|
||||
//
|
||||
// INVARIANT: The import() below cannot hang because:
|
||||
// 1. It only loads local ESM modules with no circular awaits
|
||||
// 2. Module initialization is synchronous (no top-level await in ./matrix/index.js)
|
||||
// 3. The lock only serializes the import phase, not the provider startup
|
||||
const previousLock = matrixStartupLock;
|
||||
let releaseLock: () => void = () => {};
|
||||
matrixStartupLock = new Promise<void>((resolve) => {
|
||||
releaseLock = resolve;
|
||||
});
|
||||
await previousLock;
|
||||
|
||||
// Lazy import: the monitor pulls the reply pipeline; avoid ESM init cycles.
|
||||
const { monitorMatrixProvider } = await import("./matrix/index.js");
|
||||
// Wrap in try/finally to ensure lock is released even if import fails.
|
||||
let monitorMatrixProvider: typeof import("./matrix/index.js").monitorMatrixProvider;
|
||||
try {
|
||||
const module = await import("./matrix/index.js");
|
||||
monitorMatrixProvider = module.monitorMatrixProvider;
|
||||
} finally {
|
||||
// Release lock after import completes or fails
|
||||
releaseLock();
|
||||
}
|
||||
|
||||
return monitorMatrixProvider({
|
||||
runtime: ctx.runtime,
|
||||
abortSignal: ctx.abortSignal,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk";
|
||||
import type { CoreConfig, MatrixConfig } from "../types.js";
|
||||
import { resolveMatrixConfig } from "./client.js";
|
||||
import { resolveMatrixConfigForAccount } from "./client.js";
|
||||
import { credentialsMatchConfig, loadMatrixCredentials } from "./credentials.js";
|
||||
|
||||
export type ResolvedMatrixAccount = {
|
||||
@@ -13,8 +13,21 @@ export type ResolvedMatrixAccount = {
|
||||
config: MatrixConfig;
|
||||
};
|
||||
|
||||
export function listMatrixAccountIds(_cfg: CoreConfig): string[] {
|
||||
return [DEFAULT_ACCOUNT_ID];
|
||||
function listConfiguredAccountIds(cfg: CoreConfig): string[] {
|
||||
const accounts = cfg.channels?.matrix?.accounts;
|
||||
if (!accounts || typeof accounts !== "object") {
|
||||
return [];
|
||||
}
|
||||
return Object.keys(accounts).filter(Boolean);
|
||||
}
|
||||
|
||||
export function listMatrixAccountIds(cfg: CoreConfig): string[] {
|
||||
const ids = listConfiguredAccountIds(cfg);
|
||||
if (ids.length === 0) {
|
||||
// Fall back to default if no accounts configured (legacy top-level config)
|
||||
return [DEFAULT_ACCOUNT_ID];
|
||||
}
|
||||
return ids.toSorted((a, b) => a.localeCompare(b));
|
||||
}
|
||||
|
||||
export function resolveDefaultMatrixAccountId(cfg: CoreConfig): string {
|
||||
@@ -25,20 +38,35 @@ export function resolveDefaultMatrixAccountId(cfg: CoreConfig): string {
|
||||
return ids[0] ?? DEFAULT_ACCOUNT_ID;
|
||||
}
|
||||
|
||||
function resolveAccountConfig(cfg: CoreConfig, accountId: string): MatrixConfig | undefined {
|
||||
const accounts = cfg.channels?.matrix?.accounts;
|
||||
if (!accounts || typeof accounts !== "object") {
|
||||
return undefined;
|
||||
}
|
||||
return accounts[accountId] as MatrixConfig | undefined;
|
||||
}
|
||||
|
||||
export function resolveMatrixAccount(params: {
|
||||
cfg: CoreConfig;
|
||||
accountId?: string | null;
|
||||
}): ResolvedMatrixAccount {
|
||||
const accountId = normalizeAccountId(params.accountId);
|
||||
const base = params.cfg.channels?.matrix ?? {};
|
||||
const enabled = base.enabled !== false;
|
||||
const resolved = resolveMatrixConfig(params.cfg, process.env);
|
||||
const matrixBase = params.cfg.channels?.matrix ?? {};
|
||||
|
||||
// Check if this account exists in accounts structure
|
||||
const accountConfig = resolveAccountConfig(params.cfg, accountId);
|
||||
|
||||
// Use account-specific config if available, otherwise fall back to top-level
|
||||
const base: MatrixConfig = accountConfig ?? matrixBase;
|
||||
const enabled = base.enabled !== false && matrixBase.enabled !== false;
|
||||
|
||||
const resolved = resolveMatrixConfigForAccount(params.cfg, accountId, process.env);
|
||||
const hasHomeserver = Boolean(resolved.homeserver);
|
||||
const hasUserId = Boolean(resolved.userId);
|
||||
const hasAccessToken = Boolean(resolved.accessToken);
|
||||
const hasPassword = Boolean(resolved.password);
|
||||
const hasPasswordAuth = hasUserId && hasPassword;
|
||||
const stored = loadMatrixCredentials(process.env);
|
||||
const stored = loadMatrixCredentials(process.env, accountId);
|
||||
const hasStored =
|
||||
stored && resolved.homeserver
|
||||
? credentialsMatchConfig(stored, {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { normalizeAccountId } from "openclaw/plugin-sdk";
|
||||
import type { CoreConfig } from "../../types.js";
|
||||
import type { MatrixActionClient, MatrixActionClientOpts } from "./types.js";
|
||||
import { getMatrixRuntime } from "../../runtime.js";
|
||||
@@ -22,7 +23,9 @@ export async function resolveActionClient(
|
||||
if (opts.client) {
|
||||
return { client: opts.client, stopOnDone: false };
|
||||
}
|
||||
const active = getActiveMatrixClient();
|
||||
// Normalize accountId early to ensure consistent keying across all lookups
|
||||
const accountId = normalizeAccountId(opts.accountId);
|
||||
const active = getActiveMatrixClient(accountId);
|
||||
if (active) {
|
||||
return { client: active, stopOnDone: false };
|
||||
}
|
||||
@@ -31,11 +34,13 @@ export async function resolveActionClient(
|
||||
const client = await resolveSharedMatrixClient({
|
||||
cfg: getMatrixRuntime().config.loadConfig() as CoreConfig,
|
||||
timeoutMs: opts.timeoutMs,
|
||||
accountId,
|
||||
});
|
||||
return { client, stopOnDone: false };
|
||||
}
|
||||
const auth = await resolveMatrixAuth({
|
||||
cfg: getMatrixRuntime().config.loadConfig() as CoreConfig,
|
||||
accountId,
|
||||
});
|
||||
const client = await createMatrixClient({
|
||||
homeserver: auth.homeserver,
|
||||
@@ -43,6 +48,7 @@ export async function resolveActionClient(
|
||||
accessToken: auth.accessToken,
|
||||
encryption: auth.encryption,
|
||||
localTimeoutMs: opts.timeoutMs,
|
||||
accountId,
|
||||
});
|
||||
if (auth.encryption && client.crypto) {
|
||||
try {
|
||||
|
||||
@@ -57,6 +57,7 @@ export type MatrixRawEvent = {
|
||||
export type MatrixActionClientOpts = {
|
||||
client?: MatrixClient;
|
||||
timeoutMs?: number;
|
||||
accountId?: string | null;
|
||||
};
|
||||
|
||||
export type MatrixMessageSummary = {
|
||||
|
||||
@@ -1,11 +1,32 @@
|
||||
import type { MatrixClient } from "@vector-im/matrix-bot-sdk";
|
||||
import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk";
|
||||
|
||||
let activeClient: MatrixClient | null = null;
|
||||
// Support multiple active clients for multi-account
|
||||
const activeClients = new Map<string, MatrixClient>();
|
||||
|
||||
export function setActiveMatrixClient(client: MatrixClient | null): void {
|
||||
activeClient = client;
|
||||
export function setActiveMatrixClient(
|
||||
client: MatrixClient | null,
|
||||
accountId?: string | null,
|
||||
): void {
|
||||
const key = accountId ?? DEFAULT_ACCOUNT_ID;
|
||||
if (client) {
|
||||
activeClients.set(key, client);
|
||||
} else {
|
||||
activeClients.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
export function getActiveMatrixClient(): MatrixClient | null {
|
||||
return activeClient;
|
||||
export function getActiveMatrixClient(accountId?: string | null): MatrixClient | null {
|
||||
const key = accountId ?? DEFAULT_ACCOUNT_ID;
|
||||
return activeClients.get(key) ?? null;
|
||||
}
|
||||
|
||||
export function getAnyActiveMatrixClient(): MatrixClient | null {
|
||||
// Return any available client (for backward compatibility)
|
||||
const first = activeClients.values().next();
|
||||
return first.done ? null : first.value;
|
||||
}
|
||||
|
||||
export function clearAllActiveMatrixClients(): void {
|
||||
activeClients.clear();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
export type { MatrixAuth, MatrixResolvedConfig } from "./client/types.js";
|
||||
export { isBunRuntime } from "./client/runtime.js";
|
||||
export { resolveMatrixConfig, resolveMatrixAuth } from "./client/config.js";
|
||||
export {
|
||||
resolveMatrixConfig,
|
||||
resolveMatrixConfigForAccount,
|
||||
resolveMatrixAuth,
|
||||
} from "./client/config.js";
|
||||
export { createMatrixClient } from "./client/create-client.js";
|
||||
export { resolveSharedMatrixClient, waitForMatrixSync, stopSharedClient } from "./client/shared.js";
|
||||
export {
|
||||
resolveSharedMatrixClient,
|
||||
waitForMatrixSync,
|
||||
stopSharedClient,
|
||||
stopSharedClientForAccount,
|
||||
} from "./client/shared.js";
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { MatrixClient } from "@vector-im/matrix-bot-sdk";
|
||||
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk";
|
||||
import type { CoreConfig } from "../../types.js";
|
||||
import type { MatrixAuth, MatrixResolvedConfig } from "./types.js";
|
||||
import { getMatrixRuntime } from "../../runtime.js";
|
||||
@@ -8,11 +9,27 @@ function clean(value?: string): string {
|
||||
return value?.trim() ?? "";
|
||||
}
|
||||
|
||||
export function resolveMatrixConfig(
|
||||
/**
|
||||
* Resolve Matrix config for a specific account, with fallback to top-level config.
|
||||
* This supports both multi-account (channels.matrix.accounts.*) and
|
||||
* single-account (channels.matrix.*) configurations.
|
||||
*/
|
||||
export function resolveMatrixConfigForAccount(
|
||||
cfg: CoreConfig = getMatrixRuntime().config.loadConfig() as CoreConfig,
|
||||
accountId?: string | null,
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
): MatrixResolvedConfig {
|
||||
const matrix = cfg.channels?.matrix ?? {};
|
||||
const normalizedAccountId = normalizeAccountId(accountId);
|
||||
const matrixBase = cfg.channels?.matrix ?? {};
|
||||
|
||||
// Try to get account-specific config first
|
||||
const accountConfig = matrixBase.accounts?.[normalizedAccountId];
|
||||
|
||||
// Merge: account-specific values override top-level values
|
||||
// For DEFAULT_ACCOUNT_ID with no accounts, use top-level directly
|
||||
const useAccountConfig = accountConfig !== undefined;
|
||||
const matrix = useAccountConfig ? { ...matrixBase, ...accountConfig } : matrixBase;
|
||||
|
||||
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;
|
||||
@@ -34,13 +51,24 @@ export function resolveMatrixConfig(
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Single-account function for backward compatibility - resolves default account config.
|
||||
*/
|
||||
export function resolveMatrixConfig(
|
||||
cfg: CoreConfig = getMatrixRuntime().config.loadConfig() as CoreConfig,
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
): MatrixResolvedConfig {
|
||||
return resolveMatrixConfigForAccount(cfg, DEFAULT_ACCOUNT_ID, env);
|
||||
}
|
||||
|
||||
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 resolved = resolveMatrixConfigForAccount(cfg, params?.accountId, env);
|
||||
if (!resolved.homeserver) {
|
||||
throw new Error("Matrix homeserver is required (matrix.homeserver)");
|
||||
}
|
||||
@@ -52,7 +80,8 @@ export async function resolveMatrixAuth(params?: {
|
||||
touchMatrixCredentials,
|
||||
} = await import("../credentials.js");
|
||||
|
||||
const cached = loadMatrixCredentials(env);
|
||||
const accountId = params?.accountId;
|
||||
const cached = loadMatrixCredentials(env, accountId);
|
||||
const cachedCredentials =
|
||||
cached &&
|
||||
credentialsMatchConfig(cached, {
|
||||
@@ -72,13 +101,17 @@ export async function resolveMatrixAuth(params?: {
|
||||
const whoami = await tempClient.getUserId();
|
||||
userId = whoami;
|
||||
// Save the credentials with the fetched userId
|
||||
saveMatrixCredentials({
|
||||
homeserver: resolved.homeserver,
|
||||
userId,
|
||||
accessToken: resolved.accessToken,
|
||||
});
|
||||
saveMatrixCredentials(
|
||||
{
|
||||
homeserver: resolved.homeserver,
|
||||
userId,
|
||||
accessToken: resolved.accessToken,
|
||||
},
|
||||
env,
|
||||
accountId,
|
||||
);
|
||||
} else if (cachedCredentials && cachedCredentials.accessToken === resolved.accessToken) {
|
||||
touchMatrixCredentials(env);
|
||||
touchMatrixCredentials(env, accountId);
|
||||
}
|
||||
return {
|
||||
homeserver: resolved.homeserver,
|
||||
@@ -91,7 +124,7 @@ export async function resolveMatrixAuth(params?: {
|
||||
}
|
||||
|
||||
if (cachedCredentials) {
|
||||
touchMatrixCredentials(env);
|
||||
touchMatrixCredentials(env, accountId);
|
||||
return {
|
||||
homeserver: cachedCredentials.homeserver,
|
||||
userId: cachedCredentials.userId,
|
||||
@@ -149,12 +182,16 @@ export async function resolveMatrixAuth(params?: {
|
||||
encryption: resolved.encryption,
|
||||
};
|
||||
|
||||
saveMatrixCredentials({
|
||||
homeserver: auth.homeserver,
|
||||
userId: auth.userId,
|
||||
accessToken: auth.accessToken,
|
||||
deviceId: login.device_id,
|
||||
});
|
||||
saveMatrixCredentials(
|
||||
{
|
||||
homeserver: auth.homeserver,
|
||||
userId: auth.userId,
|
||||
accessToken: auth.accessToken,
|
||||
deviceId: login.device_id,
|
||||
},
|
||||
env,
|
||||
accountId,
|
||||
);
|
||||
|
||||
return auth;
|
||||
}
|
||||
|
||||
@@ -13,9 +13,10 @@ type SharedMatrixClientState = {
|
||||
cryptoReady: boolean;
|
||||
};
|
||||
|
||||
let sharedClientState: SharedMatrixClientState | null = null;
|
||||
let sharedClientPromise: Promise<SharedMatrixClientState> | null = null;
|
||||
let sharedClientStartPromise: Promise<void> | null = null;
|
||||
// Support multiple accounts with separate clients
|
||||
const sharedClientStates = new Map<string, SharedMatrixClientState>();
|
||||
const sharedClientPromises = new Map<string, Promise<SharedMatrixClientState>>();
|
||||
const sharedClientStartPromises = new Map<string, Promise<void>>();
|
||||
|
||||
function buildSharedClientKey(auth: MatrixAuth, accountId?: string | null): string {
|
||||
return [
|
||||
@@ -57,11 +58,13 @@ async function ensureSharedClientStarted(params: {
|
||||
if (params.state.started) {
|
||||
return;
|
||||
}
|
||||
if (sharedClientStartPromise) {
|
||||
await sharedClientStartPromise;
|
||||
const key = params.state.key;
|
||||
const existingStartPromise = sharedClientStartPromises.get(key);
|
||||
if (existingStartPromise) {
|
||||
await existingStartPromise;
|
||||
return;
|
||||
}
|
||||
sharedClientStartPromise = (async () => {
|
||||
const startPromise = (async () => {
|
||||
const client = params.state.client;
|
||||
|
||||
// Initialize crypto if enabled
|
||||
@@ -82,10 +85,11 @@ async function ensureSharedClientStarted(params: {
|
||||
await client.start();
|
||||
params.state.started = true;
|
||||
})();
|
||||
sharedClientStartPromises.set(key, startPromise);
|
||||
try {
|
||||
await sharedClientStartPromise;
|
||||
await startPromise;
|
||||
} finally {
|
||||
sharedClientStartPromise = null;
|
||||
sharedClientStartPromises.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,48 +103,51 @@ 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;
|
||||
|
||||
if (sharedClientState?.key === key) {
|
||||
// Check if we already have a client for this key
|
||||
const existingState = sharedClientStates.get(key);
|
||||
if (existingState) {
|
||||
if (shouldStart) {
|
||||
await ensureSharedClientStarted({
|
||||
state: sharedClientState,
|
||||
state: existingState,
|
||||
timeoutMs: params.timeoutMs,
|
||||
initialSyncLimit: auth.initialSyncLimit,
|
||||
encryption: auth.encryption,
|
||||
});
|
||||
}
|
||||
return sharedClientState.client;
|
||||
return existingState.client;
|
||||
}
|
||||
|
||||
if (sharedClientPromise) {
|
||||
const pending = await sharedClientPromise;
|
||||
if (pending.key === key) {
|
||||
if (shouldStart) {
|
||||
await ensureSharedClientStarted({
|
||||
state: pending,
|
||||
timeoutMs: params.timeoutMs,
|
||||
initialSyncLimit: auth.initialSyncLimit,
|
||||
encryption: auth.encryption,
|
||||
});
|
||||
}
|
||||
return pending.client;
|
||||
// Check if there's a pending creation for this key
|
||||
const existingPromise = sharedClientPromises.get(key);
|
||||
if (existingPromise) {
|
||||
const pending = await existingPromise;
|
||||
if (shouldStart) {
|
||||
await ensureSharedClientStarted({
|
||||
state: pending,
|
||||
timeoutMs: params.timeoutMs,
|
||||
initialSyncLimit: auth.initialSyncLimit,
|
||||
encryption: auth.encryption,
|
||||
});
|
||||
}
|
||||
pending.client.stop();
|
||||
sharedClientState = null;
|
||||
sharedClientPromise = null;
|
||||
return pending.client;
|
||||
}
|
||||
|
||||
sharedClientPromise = createSharedMatrixClient({
|
||||
// Create a new client for this account
|
||||
const createPromise = createSharedMatrixClient({
|
||||
auth,
|
||||
timeoutMs: params.timeoutMs,
|
||||
accountId: params.accountId,
|
||||
});
|
||||
sharedClientPromises.set(key, createPromise);
|
||||
try {
|
||||
const created = await sharedClientPromise;
|
||||
sharedClientState = created;
|
||||
const created = await createPromise;
|
||||
sharedClientStates.set(key, created);
|
||||
if (shouldStart) {
|
||||
await ensureSharedClientStarted({
|
||||
state: created,
|
||||
@@ -151,7 +158,7 @@ export async function resolveSharedMatrixClient(
|
||||
}
|
||||
return created.client;
|
||||
} finally {
|
||||
sharedClientPromise = null;
|
||||
sharedClientPromises.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,9 +171,29 @@ export async function waitForMatrixSync(_params: {
|
||||
// This is kept for API compatibility but is essentially a no-op now
|
||||
}
|
||||
|
||||
export function stopSharedClient(): void {
|
||||
if (sharedClientState) {
|
||||
sharedClientState.client.stop();
|
||||
sharedClientState = null;
|
||||
export function stopSharedClient(key?: string): void {
|
||||
if (key) {
|
||||
// Stop a specific client
|
||||
const state = sharedClientStates.get(key);
|
||||
if (state) {
|
||||
state.client.stop();
|
||||
sharedClientStates.delete(key);
|
||||
}
|
||||
} else {
|
||||
// Stop all clients (backward compatible behavior)
|
||||
for (const state of sharedClientStates.values()) {
|
||||
state.client.stop();
|
||||
}
|
||||
sharedClientStates.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the shared client for a specific account.
|
||||
* Use this instead of stopSharedClient() when shutting down a single account
|
||||
* to avoid stopping all accounts.
|
||||
*/
|
||||
export function stopSharedClientForAccount(auth: MatrixAuth, accountId?: string | null): void {
|
||||
const key = buildSharedClientKey(auth, accountId);
|
||||
stopSharedClient(key);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk";
|
||||
import { getMatrixRuntime } from "../runtime.js";
|
||||
|
||||
export type MatrixStoredCredentials = {
|
||||
@@ -12,7 +13,15 @@ export type MatrixStoredCredentials = {
|
||||
lastUsedAt?: string;
|
||||
};
|
||||
|
||||
const CREDENTIALS_FILENAME = "credentials.json";
|
||||
function credentialsFilename(accountId?: string | null): string {
|
||||
const normalized = normalizeAccountId(accountId);
|
||||
if (normalized === DEFAULT_ACCOUNT_ID) {
|
||||
return "credentials.json";
|
||||
}
|
||||
// Sanitize accountId for use in filename
|
||||
const safe = normalized.replace(/[^a-zA-Z0-9_-]/g, "_");
|
||||
return `credentials-${safe}.json`;
|
||||
}
|
||||
|
||||
export function resolveMatrixCredentialsDir(
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
@@ -22,15 +31,19 @@ export function resolveMatrixCredentialsDir(
|
||||
return path.join(resolvedStateDir, "credentials", "matrix");
|
||||
}
|
||||
|
||||
export function resolveMatrixCredentialsPath(env: NodeJS.ProcessEnv = process.env): string {
|
||||
export function resolveMatrixCredentialsPath(
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
accountId?: string | null,
|
||||
): string {
|
||||
const dir = resolveMatrixCredentialsDir(env);
|
||||
return path.join(dir, CREDENTIALS_FILENAME);
|
||||
return path.join(dir, credentialsFilename(accountId));
|
||||
}
|
||||
|
||||
export function loadMatrixCredentials(
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
accountId?: string | null,
|
||||
): MatrixStoredCredentials | null {
|
||||
const credPath = resolveMatrixCredentialsPath(env);
|
||||
const credPath = resolveMatrixCredentialsPath(env, accountId);
|
||||
try {
|
||||
if (!fs.existsSync(credPath)) {
|
||||
return null;
|
||||
@@ -53,13 +66,14 @@ export function loadMatrixCredentials(
|
||||
export function saveMatrixCredentials(
|
||||
credentials: Omit<MatrixStoredCredentials, "createdAt" | "lastUsedAt">,
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
accountId?: string | null,
|
||||
): void {
|
||||
const dir = resolveMatrixCredentialsDir(env);
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
|
||||
const credPath = resolveMatrixCredentialsPath(env);
|
||||
const credPath = resolveMatrixCredentialsPath(env, accountId);
|
||||
|
||||
const existing = loadMatrixCredentials(env);
|
||||
const existing = loadMatrixCredentials(env, accountId);
|
||||
const now = new Date().toISOString();
|
||||
|
||||
const toSave: MatrixStoredCredentials = {
|
||||
@@ -71,19 +85,25 @@ export function saveMatrixCredentials(
|
||||
fs.writeFileSync(credPath, JSON.stringify(toSave, null, 2), "utf-8");
|
||||
}
|
||||
|
||||
export function touchMatrixCredentials(env: NodeJS.ProcessEnv = process.env): void {
|
||||
const existing = loadMatrixCredentials(env);
|
||||
export function touchMatrixCredentials(
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
accountId?: string | null,
|
||||
): void {
|
||||
const existing = loadMatrixCredentials(env, accountId);
|
||||
if (!existing) {
|
||||
return;
|
||||
}
|
||||
|
||||
existing.lastUsedAt = new Date().toISOString();
|
||||
const credPath = resolveMatrixCredentialsPath(env);
|
||||
const credPath = resolveMatrixCredentialsPath(env, accountId);
|
||||
fs.writeFileSync(credPath, JSON.stringify(existing, null, 2), "utf-8");
|
||||
}
|
||||
|
||||
export function clearMatrixCredentials(env: NodeJS.ProcessEnv = process.env): void {
|
||||
const credPath = resolveMatrixCredentialsPath(env);
|
||||
export function clearMatrixCredentials(
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
accountId?: string | null,
|
||||
): void {
|
||||
const credPath = resolveMatrixCredentialsPath(env, accountId);
|
||||
try {
|
||||
if (fs.existsSync(credPath)) {
|
||||
fs.unlinkSync(credPath);
|
||||
|
||||
@@ -68,6 +68,7 @@ export type MatrixMonitorHandlerParams = {
|
||||
roomId: string,
|
||||
) => Promise<{ name?: string; canonicalAlias?: string; altAliases: string[] }>;
|
||||
getMemberDisplayName: (roomId: string, userId: string) => Promise<string>;
|
||||
accountId?: string | null;
|
||||
};
|
||||
|
||||
export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParams) {
|
||||
@@ -93,6 +94,7 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
|
||||
directTracker,
|
||||
getRoomInfo,
|
||||
getMemberDisplayName,
|
||||
accountId,
|
||||
} = params;
|
||||
|
||||
return async (roomId: string, event: MatrixRawEvent) => {
|
||||
@@ -435,6 +437,7 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
|
||||
const baseRoute = core.channel.routing.resolveAgentRoute({
|
||||
cfg,
|
||||
channel: "matrix",
|
||||
accountId,
|
||||
peer: {
|
||||
kind: isDirectMessage ? "direct" : "channel",
|
||||
id: isDirectMessage ? senderId : roomId,
|
||||
|
||||
@@ -3,12 +3,13 @@ import { mergeAllowlist, summarizeMapping, type RuntimeEnv } from "openclaw/plug
|
||||
import type { CoreConfig, ReplyToMode } from "../../types.js";
|
||||
import { resolveMatrixTargets } from "../../resolve-targets.js";
|
||||
import { getMatrixRuntime } from "../../runtime.js";
|
||||
import { resolveMatrixAccount } from "../accounts.js";
|
||||
import { setActiveMatrixClient } from "../active-client.js";
|
||||
import {
|
||||
isBunRuntime,
|
||||
resolveMatrixAuth,
|
||||
resolveSharedMatrixClient,
|
||||
stopSharedClient,
|
||||
stopSharedClientForAccount,
|
||||
} from "../client.js";
|
||||
import { normalizeMatrixUserId } from "./allowlist.js";
|
||||
import { registerMatrixAutoJoin } from "./auto-join.js";
|
||||
@@ -121,10 +122,14 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi
|
||||
return allowList.map(String);
|
||||
};
|
||||
|
||||
const allowlistOnly = cfg.channels?.matrix?.allowlistOnly === true;
|
||||
let allowFrom: string[] = (cfg.channels?.matrix?.dm?.allowFrom ?? []).map(String);
|
||||
let groupAllowFrom: string[] = (cfg.channels?.matrix?.groupAllowFrom ?? []).map(String);
|
||||
let roomsConfig = cfg.channels?.matrix?.groups ?? cfg.channels?.matrix?.rooms;
|
||||
// Resolve account-specific config for multi-account support
|
||||
const account = resolveMatrixAccount({ cfg, accountId: opts.accountId });
|
||||
const accountConfig = account.config;
|
||||
|
||||
const allowlistOnly = accountConfig.allowlistOnly === true;
|
||||
let allowFrom: string[] = (accountConfig.dm?.allowFrom ?? []).map(String);
|
||||
let groupAllowFrom: string[] = (accountConfig.groupAllowFrom ?? []).map(String);
|
||||
let roomsConfig = accountConfig.groups ?? accountConfig.rooms;
|
||||
|
||||
allowFrom = await resolveUserAllowlist("matrix dm allowlist", allowFrom);
|
||||
groupAllowFrom = await resolveUserAllowlist("matrix group allowlist", groupAllowFrom);
|
||||
@@ -219,7 +224,7 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi
|
||||
},
|
||||
};
|
||||
|
||||
const auth = await resolveMatrixAuth({ cfg });
|
||||
const auth = await resolveMatrixAuth({ cfg, accountId: opts.accountId });
|
||||
const resolvedInitialSyncLimit =
|
||||
typeof opts.initialSyncLimit === "number"
|
||||
? Math.max(0, Math.floor(opts.initialSyncLimit))
|
||||
@@ -234,20 +239,20 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi
|
||||
startClient: false,
|
||||
accountId: opts.accountId,
|
||||
});
|
||||
setActiveMatrixClient(client);
|
||||
setActiveMatrixClient(client, opts.accountId);
|
||||
|
||||
const mentionRegexes = core.channel.mentions.buildMentionRegexes(cfg);
|
||||
const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy;
|
||||
const groupPolicyRaw = cfg.channels?.matrix?.groupPolicy ?? defaultGroupPolicy ?? "allowlist";
|
||||
const groupPolicyRaw = accountConfig.groupPolicy ?? defaultGroupPolicy ?? "allowlist";
|
||||
const groupPolicy = allowlistOnly && groupPolicyRaw === "open" ? "allowlist" : groupPolicyRaw;
|
||||
const replyToMode = opts.replyToMode ?? cfg.channels?.matrix?.replyToMode ?? "off";
|
||||
const threadReplies = cfg.channels?.matrix?.threadReplies ?? "inbound";
|
||||
const dmConfig = cfg.channels?.matrix?.dm;
|
||||
const replyToMode = opts.replyToMode ?? accountConfig.replyToMode ?? "off";
|
||||
const threadReplies = accountConfig.threadReplies ?? "inbound";
|
||||
const dmConfig = accountConfig.dm;
|
||||
const dmEnabled = dmConfig?.enabled ?? true;
|
||||
const dmPolicyRaw = dmConfig?.policy ?? "pairing";
|
||||
const dmPolicy = allowlistOnly && dmPolicyRaw !== "disabled" ? "allowlist" : dmPolicyRaw;
|
||||
const textLimit = core.channel.text.resolveTextChunkLimit(cfg, "matrix");
|
||||
const mediaMaxMb = opts.mediaMaxMb ?? cfg.channels?.matrix?.mediaMaxMb ?? DEFAULT_MEDIA_MAX_MB;
|
||||
const mediaMaxMb = opts.mediaMaxMb ?? accountConfig.mediaMaxMb ?? DEFAULT_MEDIA_MAX_MB;
|
||||
const mediaMaxBytes = Math.max(1, mediaMaxMb) * 1024 * 1024;
|
||||
const startupMs = Date.now();
|
||||
const startupGraceMs = 0;
|
||||
@@ -279,6 +284,7 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi
|
||||
directTracker,
|
||||
getRoomInfo,
|
||||
getMemberDisplayName,
|
||||
accountId: opts.accountId,
|
||||
});
|
||||
|
||||
registerMatrixMonitorEvents({
|
||||
@@ -324,9 +330,9 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi
|
||||
const onAbort = () => {
|
||||
try {
|
||||
logVerboseMessage("matrix: stopping client");
|
||||
stopSharedClient();
|
||||
stopSharedClientForAccount(auth, opts.accountId);
|
||||
} finally {
|
||||
setActiveMatrixClient(null);
|
||||
setActiveMatrixClient(null, opts.accountId);
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -45,6 +45,7 @@ export async function sendMessageMatrix(
|
||||
const { client, stopOnDone } = await resolveMatrixClient({
|
||||
client: opts.client,
|
||||
timeoutMs: opts.timeoutMs,
|
||||
accountId: opts.accountId,
|
||||
});
|
||||
try {
|
||||
const roomId = await resolveMatrixRoomId(client, to);
|
||||
@@ -78,7 +79,7 @@ export async function sendMessageMatrix(
|
||||
|
||||
let lastMessageId = "";
|
||||
if (opts.mediaUrl) {
|
||||
const maxBytes = resolveMediaMaxBytes();
|
||||
const maxBytes = resolveMediaMaxBytes(opts.accountId);
|
||||
const media = await getCore().media.loadWebMedia(opts.mediaUrl, maxBytes);
|
||||
const uploaded = await uploadMediaMaybeEncrypted(client, roomId, media.buffer, {
|
||||
contentType: media.contentType,
|
||||
@@ -166,6 +167,7 @@ export async function sendPollMatrix(
|
||||
const { client, stopOnDone } = await resolveMatrixClient({
|
||||
client: opts.client,
|
||||
timeoutMs: opts.timeoutMs,
|
||||
accountId: opts.accountId,
|
||||
});
|
||||
|
||||
try {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { MatrixClient } from "@vector-im/matrix-bot-sdk";
|
||||
import type { CoreConfig } from "../../types.js";
|
||||
import { getMatrixRuntime } from "../../runtime.js";
|
||||
import { getActiveMatrixClient } from "../active-client.js";
|
||||
import { getActiveMatrixClient, getAnyActiveMatrixClient } from "../active-client.js";
|
||||
import {
|
||||
createMatrixClient,
|
||||
isBunRuntime,
|
||||
@@ -17,8 +17,16 @@ export function ensureNodeRuntime() {
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveMediaMaxBytes(): number | undefined {
|
||||
export function resolveMediaMaxBytes(accountId?: string): number | undefined {
|
||||
const cfg = getCore().config.loadConfig() as CoreConfig;
|
||||
// Check account-specific config first
|
||||
if (accountId) {
|
||||
const accountConfig = cfg.channels?.matrix?.accounts?.[accountId];
|
||||
if (typeof accountConfig?.mediaMaxMb === "number") {
|
||||
return accountConfig.mediaMaxMb * 1024 * 1024;
|
||||
}
|
||||
}
|
||||
// Fall back to top-level config
|
||||
if (typeof cfg.channels?.matrix?.mediaMaxMb === "number") {
|
||||
return cfg.channels.matrix.mediaMaxMb * 1024 * 1024;
|
||||
}
|
||||
@@ -28,29 +36,40 @@ export function resolveMediaMaxBytes(): number | undefined {
|
||||
export async function resolveMatrixClient(opts: {
|
||||
client?: MatrixClient;
|
||||
timeoutMs?: number;
|
||||
accountId?: string;
|
||||
}): Promise<{ client: MatrixClient; stopOnDone: boolean }> {
|
||||
ensureNodeRuntime();
|
||||
if (opts.client) {
|
||||
return { client: opts.client, stopOnDone: false };
|
||||
}
|
||||
const active = getActiveMatrixClient();
|
||||
// Try to get the client for the specific account
|
||||
const active = getActiveMatrixClient(opts.accountId);
|
||||
if (active) {
|
||||
return { client: active, stopOnDone: false };
|
||||
}
|
||||
// Only fall back to any active client when no specific account is requested
|
||||
if (!opts.accountId) {
|
||||
const anyActive = getAnyActiveMatrixClient();
|
||||
if (anyActive) {
|
||||
return { client: anyActive, stopOnDone: false };
|
||||
}
|
||||
}
|
||||
const shouldShareClient = Boolean(process.env.OPENCLAW_GATEWAY_PORT);
|
||||
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,
|
||||
accessToken: auth.accessToken,
|
||||
encryption: auth.encryption,
|
||||
localTimeoutMs: opts.timeoutMs,
|
||||
accountId: opts.accountId,
|
||||
});
|
||||
if (auth.encryption && client.crypto) {
|
||||
try {
|
||||
|
||||
@@ -7,13 +7,14 @@ export const matrixOutbound: ChannelOutboundAdapter = {
|
||||
chunker: (text, limit) => getMatrixRuntime().channel.text.chunkMarkdownText(text, limit),
|
||||
chunkerMode: "markdown",
|
||||
textChunkLimit: 4000,
|
||||
sendText: async ({ to, text, deps, replyToId, threadId }) => {
|
||||
sendText: async ({ to, text, deps, replyToId, threadId, accountId }) => {
|
||||
const send = deps?.sendMatrix ?? sendMessageMatrix;
|
||||
const resolvedThreadId =
|
||||
threadId !== undefined && threadId !== null ? String(threadId) : undefined;
|
||||
const result = await send(to, text, {
|
||||
replyToId: replyToId ?? undefined,
|
||||
threadId: resolvedThreadId,
|
||||
accountId: accountId ?? undefined,
|
||||
});
|
||||
return {
|
||||
channel: "matrix",
|
||||
@@ -21,7 +22,7 @@ export const matrixOutbound: ChannelOutboundAdapter = {
|
||||
roomId: result.roomId,
|
||||
};
|
||||
},
|
||||
sendMedia: async ({ to, text, mediaUrl, deps, replyToId, threadId }) => {
|
||||
sendMedia: async ({ to, text, mediaUrl, deps, replyToId, threadId, accountId }) => {
|
||||
const send = deps?.sendMatrix ?? sendMessageMatrix;
|
||||
const resolvedThreadId =
|
||||
threadId !== undefined && threadId !== null ? String(threadId) : undefined;
|
||||
@@ -29,6 +30,7 @@ export const matrixOutbound: ChannelOutboundAdapter = {
|
||||
mediaUrl,
|
||||
replyToId: replyToId ?? undefined,
|
||||
threadId: resolvedThreadId,
|
||||
accountId: accountId ?? undefined,
|
||||
});
|
||||
return {
|
||||
channel: "matrix",
|
||||
@@ -36,11 +38,12 @@ export const matrixOutbound: ChannelOutboundAdapter = {
|
||||
roomId: result.roomId,
|
||||
};
|
||||
},
|
||||
sendPoll: async ({ to, poll, threadId }) => {
|
||||
sendPoll: async ({ to, poll, threadId, accountId }) => {
|
||||
const resolvedThreadId =
|
||||
threadId !== undefined && threadId !== null ? String(threadId) : undefined;
|
||||
const result = await sendPollMatrix(to, poll, {
|
||||
threadId: resolvedThreadId,
|
||||
accountId: accountId ?? undefined,
|
||||
});
|
||||
return {
|
||||
channel: "matrix",
|
||||
|
||||
@@ -39,11 +39,16 @@ export type MatrixActionConfig = {
|
||||
channelInfo?: boolean;
|
||||
};
|
||||
|
||||
/** Per-account Matrix config (excludes the accounts field to prevent recursion). */
|
||||
export type MatrixAccountConfig = Omit<MatrixConfig, "accounts">;
|
||||
|
||||
export type MatrixConfig = {
|
||||
/** Optional display name for this account (used in CLI/UI lists). */
|
||||
name?: string;
|
||||
/** If false, do not start Matrix. Default: true. */
|
||||
enabled?: boolean;
|
||||
/** Multi-account configuration keyed by account ID. */
|
||||
accounts?: Record<string, MatrixAccountConfig>;
|
||||
/** Matrix homeserver URL (https://matrix.example.org). */
|
||||
homeserver?: string;
|
||||
/** Matrix user id (@user:server). */
|
||||
|
||||
Reference in New Issue
Block a user