mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 19:50:43 +00:00
Matrix-js: dedupe config helpers and harden monitoring/auth flows
This commit is contained in:
@@ -59,6 +59,10 @@ function printTimestamp(label: string, value: string | null | undefined): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function printAccountLabel(accountId?: string): void {
|
||||||
|
console.log(`Account: ${normalizeAccountId(accountId)}`);
|
||||||
|
}
|
||||||
|
|
||||||
function configureCliLogMode(verbose: boolean): void {
|
function configureCliLogMode(verbose: boolean): void {
|
||||||
setMatrixSdkLogMode(verbose ? "default" : "quiet");
|
setMatrixSdkLogMode(verbose ? "default" : "quiet");
|
||||||
}
|
}
|
||||||
@@ -521,6 +525,7 @@ export function registerMatrixJsCli(params: { program: Command }): void {
|
|||||||
includeRecoveryKey: options.includeRecoveryKey === true,
|
includeRecoveryKey: options.includeRecoveryKey === true,
|
||||||
}),
|
}),
|
||||||
onText: (status, verbose) => {
|
onText: (status, verbose) => {
|
||||||
|
printAccountLabel(options.account);
|
||||||
printVerificationStatus(status, verbose);
|
printVerificationStatus(status, verbose);
|
||||||
},
|
},
|
||||||
errorPrefix: "Error",
|
errorPrefix: "Error",
|
||||||
@@ -542,6 +547,7 @@ export function registerMatrixJsCli(params: { program: Command }): void {
|
|||||||
json: options.json === true,
|
json: options.json === true,
|
||||||
run: async () => await getMatrixRoomKeyBackupStatus({ accountId: options.account }),
|
run: async () => await getMatrixRoomKeyBackupStatus({ accountId: options.account }),
|
||||||
onText: (status, verbose) => {
|
onText: (status, verbose) => {
|
||||||
|
printAccountLabel(options.account);
|
||||||
printBackupSummary(status);
|
printBackupSummary(status);
|
||||||
if (verbose) {
|
if (verbose) {
|
||||||
printBackupStatus(status);
|
printBackupStatus(status);
|
||||||
@@ -574,6 +580,7 @@ export function registerMatrixJsCli(params: { program: Command }): void {
|
|||||||
recoveryKey: options.recoveryKey,
|
recoveryKey: options.recoveryKey,
|
||||||
}),
|
}),
|
||||||
onText: (result, verbose) => {
|
onText: (result, verbose) => {
|
||||||
|
printAccountLabel(options.account);
|
||||||
console.log(`Restore success: ${result.success ? "yes" : "no"}`);
|
console.log(`Restore success: ${result.success ? "yes" : "no"}`);
|
||||||
if (result.error) {
|
if (result.error) {
|
||||||
console.log(`Error: ${result.error}`);
|
console.log(`Error: ${result.error}`);
|
||||||
@@ -622,6 +629,7 @@ export function registerMatrixJsCli(params: { program: Command }): void {
|
|||||||
forceResetCrossSigning: options.forceResetCrossSigning === true,
|
forceResetCrossSigning: options.forceResetCrossSigning === true,
|
||||||
}),
|
}),
|
||||||
onText: (result, verbose) => {
|
onText: (result, verbose) => {
|
||||||
|
printAccountLabel(options.account);
|
||||||
console.log(`Bootstrap success: ${result.success ? "yes" : "no"}`);
|
console.log(`Bootstrap success: ${result.success ? "yes" : "no"}`);
|
||||||
if (result.error) {
|
if (result.error) {
|
||||||
console.log(`Error: ${result.error}`);
|
console.log(`Error: ${result.error}`);
|
||||||
@@ -666,6 +674,7 @@ export function registerMatrixJsCli(params: { program: Command }): void {
|
|||||||
json: options.json === true,
|
json: options.json === true,
|
||||||
run: async () => await verifyMatrixRecoveryKey(key, { accountId: options.account }),
|
run: async () => await verifyMatrixRecoveryKey(key, { accountId: options.account }),
|
||||||
onText: (result, verbose) => {
|
onText: (result, verbose) => {
|
||||||
|
printAccountLabel(options.account);
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
console.error(`Verification failed: ${result.error ?? "unknown error"}`);
|
console.error(`Verification failed: ${result.error ?? "unknown error"}`);
|
||||||
return;
|
return;
|
||||||
|
|||||||
37
extensions/matrix-js/src/matrix/account-config.ts
Normal file
37
extensions/matrix-js/src/matrix/account-config.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { normalizeAccountId } from "openclaw/plugin-sdk/account-id";
|
||||||
|
import type { CoreConfig, MatrixAccountConfig, MatrixConfig } from "../types.js";
|
||||||
|
|
||||||
|
export function resolveMatrixBaseConfig(cfg: CoreConfig): MatrixConfig {
|
||||||
|
return cfg.channels?.["matrix-js"] ?? {};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveMatrixAccountsMap(
|
||||||
|
cfg: CoreConfig,
|
||||||
|
): Readonly<Record<string, MatrixAccountConfig>> {
|
||||||
|
const accounts = resolveMatrixBaseConfig(cfg).accounts;
|
||||||
|
if (!accounts || typeof accounts !== "object") {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return accounts;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function findMatrixAccountConfig(
|
||||||
|
cfg: CoreConfig,
|
||||||
|
accountId: string,
|
||||||
|
): MatrixAccountConfig | undefined {
|
||||||
|
const accounts = resolveMatrixAccountsMap(cfg);
|
||||||
|
if (accounts[accountId] && typeof accounts[accountId] === "object") {
|
||||||
|
return accounts[accountId];
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
@@ -1,5 +1,10 @@
|
|||||||
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/account-id";
|
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/account-id";
|
||||||
import type { CoreConfig, MatrixConfig } from "../types.js";
|
import type { CoreConfig, MatrixConfig } from "../types.js";
|
||||||
|
import {
|
||||||
|
findMatrixAccountConfig,
|
||||||
|
resolveMatrixAccountsMap,
|
||||||
|
resolveMatrixBaseConfig,
|
||||||
|
} from "./account-config.js";
|
||||||
import { resolveMatrixConfigForAccount } from "./client.js";
|
import { resolveMatrixConfigForAccount } from "./client.js";
|
||||||
import { credentialsMatchConfig, loadMatrixCredentials } from "./credentials.js";
|
import { credentialsMatchConfig, loadMatrixCredentials } from "./credentials.js";
|
||||||
|
|
||||||
@@ -30,8 +35,8 @@ export type ResolvedMatrixAccount = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function listConfiguredAccountIds(cfg: CoreConfig): string[] {
|
function listConfiguredAccountIds(cfg: CoreConfig): string[] {
|
||||||
const accounts = cfg.channels?.["matrix-js"]?.accounts;
|
const accounts = resolveMatrixAccountsMap(cfg);
|
||||||
if (!accounts || typeof accounts !== "object") {
|
if (Object.keys(accounts).length === 0) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
// Normalize and de-duplicate keys so listing and resolution use the same semantics
|
// Normalize and de-duplicate keys so listing and resolution use the same semantics
|
||||||
@@ -62,22 +67,7 @@ export function resolveDefaultMatrixAccountId(cfg: CoreConfig): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function resolveAccountConfig(cfg: CoreConfig, accountId: string): MatrixConfig | undefined {
|
function resolveAccountConfig(cfg: CoreConfig, accountId: string): MatrixConfig | undefined {
|
||||||
const accounts = cfg.channels?.["matrix-js"]?.accounts;
|
return findMatrixAccountConfig(cfg, accountId);
|
||||||
if (!accounts || typeof accounts !== "object") {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
// Direct lookup first (fast path for already-normalized keys)
|
|
||||||
if (accounts[accountId]) {
|
|
||||||
return accounts[accountId] as MatrixConfig;
|
|
||||||
}
|
|
||||||
// Fall back to case-insensitive match (user may have mixed-case keys in config)
|
|
||||||
const normalized = normalizeAccountId(accountId);
|
|
||||||
for (const key of Object.keys(accounts)) {
|
|
||||||
if (normalizeAccountId(key) === normalized) {
|
|
||||||
return accounts[key] as MatrixConfig;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveMatrixAccount(params: {
|
export function resolveMatrixAccount(params: {
|
||||||
@@ -85,7 +75,7 @@ export function resolveMatrixAccount(params: {
|
|||||||
accountId?: string | null;
|
accountId?: string | null;
|
||||||
}): ResolvedMatrixAccount {
|
}): ResolvedMatrixAccount {
|
||||||
const accountId = normalizeAccountId(params.accountId);
|
const accountId = normalizeAccountId(params.accountId);
|
||||||
const matrixBase = params.cfg.channels?.["matrix-js"] ?? {};
|
const matrixBase = resolveMatrixBaseConfig(params.cfg);
|
||||||
const base = resolveMatrixAccountConfig({ cfg: params.cfg, accountId });
|
const base = resolveMatrixAccountConfig({ cfg: params.cfg, accountId });
|
||||||
const enabled = base.enabled !== false && matrixBase.enabled !== false;
|
const enabled = base.enabled !== false && matrixBase.enabled !== false;
|
||||||
|
|
||||||
@@ -120,7 +110,7 @@ export function resolveMatrixAccountConfig(params: {
|
|||||||
accountId?: string | null;
|
accountId?: string | null;
|
||||||
}): MatrixConfig {
|
}): MatrixConfig {
|
||||||
const accountId = normalizeAccountId(params.accountId);
|
const accountId = normalizeAccountId(params.accountId);
|
||||||
const matrixBase = params.cfg.channels?.["matrix-js"] ?? {};
|
const matrixBase = resolveMatrixBaseConfig(params.cfg);
|
||||||
const accountConfig = resolveAccountConfig(params.cfg, accountId);
|
const accountConfig = resolveAccountConfig(params.cfg, accountId);
|
||||||
if (!accountConfig) {
|
if (!accountConfig) {
|
||||||
return matrixBase;
|
return matrixBase;
|
||||||
|
|||||||
@@ -39,3 +39,32 @@ export async function resolveActionClient(
|
|||||||
await client.prepareForOneOff();
|
await client.prepareForOneOff();
|
||||||
return { client, stopOnDone: true };
|
return { client, stopOnDone: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type MatrixActionClientStopMode = "stop" | "persist";
|
||||||
|
|
||||||
|
export async function stopActionClient(
|
||||||
|
resolved: MatrixActionClient,
|
||||||
|
mode: MatrixActionClientStopMode = "stop",
|
||||||
|
): Promise<void> {
|
||||||
|
if (!resolved.stopOnDone) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (mode === "persist") {
|
||||||
|
await resolved.client.stopAndPersist();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resolved.client.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function withResolvedActionClient<T>(
|
||||||
|
opts: MatrixActionClientOpts,
|
||||||
|
run: (client: MatrixActionClient["client"]) => Promise<T>,
|
||||||
|
mode: MatrixActionClientStopMode = "stop",
|
||||||
|
): Promise<T> {
|
||||||
|
const resolved = await resolveActionClient(opts);
|
||||||
|
try {
|
||||||
|
return await run(resolved.client);
|
||||||
|
} finally {
|
||||||
|
await stopActionClient(resolved, mode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { resolveMatrixRoomId, sendMessageMatrix } from "../send.js";
|
import { resolveMatrixRoomId, sendMessageMatrix } from "../send.js";
|
||||||
import { resolveActionClient } from "./client.js";
|
import { withResolvedActionClient } from "./client.js";
|
||||||
import { resolveMatrixActionLimit } from "./limits.js";
|
import { resolveMatrixActionLimit } from "./limits.js";
|
||||||
import { summarizeMatrixRawEvent } from "./summary.js";
|
import { summarizeMatrixRawEvent } from "./summary.js";
|
||||||
import {
|
import {
|
||||||
@@ -40,8 +40,7 @@ export async function editMatrixMessage(
|
|||||||
if (!trimmed) {
|
if (!trimmed) {
|
||||||
throw new Error("Matrix edit requires content");
|
throw new Error("Matrix edit requires content");
|
||||||
}
|
}
|
||||||
const { client, stopOnDone } = await resolveActionClient(opts);
|
return await withResolvedActionClient(opts, async (client) => {
|
||||||
try {
|
|
||||||
const resolvedRoom = await resolveMatrixRoomId(client, roomId);
|
const resolvedRoom = await resolveMatrixRoomId(client, roomId);
|
||||||
const newContent = {
|
const newContent = {
|
||||||
msgtype: MsgType.Text,
|
msgtype: MsgType.Text,
|
||||||
@@ -58,11 +57,7 @@ export async function editMatrixMessage(
|
|||||||
};
|
};
|
||||||
const eventId = await client.sendMessage(resolvedRoom, payload);
|
const eventId = await client.sendMessage(resolvedRoom, payload);
|
||||||
return { eventId: eventId ?? null };
|
return { eventId: eventId ?? null };
|
||||||
} finally {
|
});
|
||||||
if (stopOnDone) {
|
|
||||||
client.stop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteMatrixMessage(
|
export async function deleteMatrixMessage(
|
||||||
@@ -70,15 +65,10 @@ export async function deleteMatrixMessage(
|
|||||||
messageId: string,
|
messageId: string,
|
||||||
opts: MatrixActionClientOpts & { reason?: string } = {},
|
opts: MatrixActionClientOpts & { reason?: string } = {},
|
||||||
) {
|
) {
|
||||||
const { client, stopOnDone } = await resolveActionClient(opts);
|
await withResolvedActionClient(opts, async (client) => {
|
||||||
try {
|
|
||||||
const resolvedRoom = await resolveMatrixRoomId(client, roomId);
|
const resolvedRoom = await resolveMatrixRoomId(client, roomId);
|
||||||
await client.redactEvent(resolvedRoom, messageId, opts.reason);
|
await client.redactEvent(resolvedRoom, messageId, opts.reason);
|
||||||
} finally {
|
});
|
||||||
if (stopOnDone) {
|
|
||||||
client.stop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function readMatrixMessages(
|
export async function readMatrixMessages(
|
||||||
@@ -93,8 +83,7 @@ export async function readMatrixMessages(
|
|||||||
nextBatch?: string | null;
|
nextBatch?: string | null;
|
||||||
prevBatch?: string | null;
|
prevBatch?: string | null;
|
||||||
}> {
|
}> {
|
||||||
const { client, stopOnDone } = await resolveActionClient(opts);
|
return await withResolvedActionClient(opts, async (client) => {
|
||||||
try {
|
|
||||||
const resolvedRoom = await resolveMatrixRoomId(client, roomId);
|
const resolvedRoom = await resolveMatrixRoomId(client, roomId);
|
||||||
const limit = resolveMatrixActionLimit(opts.limit, 20);
|
const limit = resolveMatrixActionLimit(opts.limit, 20);
|
||||||
const token = opts.before?.trim() || opts.after?.trim() || undefined;
|
const token = opts.before?.trim() || opts.after?.trim() || undefined;
|
||||||
@@ -118,9 +107,5 @@ export async function readMatrixMessages(
|
|||||||
nextBatch: res.end ?? null,
|
nextBatch: res.end ?? null,
|
||||||
prevBatch: res.start ?? null,
|
prevBatch: res.start ?? null,
|
||||||
};
|
};
|
||||||
} finally {
|
});
|
||||||
if (stopOnDone) {
|
|
||||||
client.stop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { resolveMatrixRoomId } from "../send.js";
|
import { resolveMatrixRoomId } from "../send.js";
|
||||||
import { resolveActionClient } from "./client.js";
|
import { withResolvedActionClient } from "./client.js";
|
||||||
import { fetchEventSummary, readPinnedEvents } from "./summary.js";
|
import { fetchEventSummary, readPinnedEvents } from "./summary.js";
|
||||||
import {
|
import {
|
||||||
EventType,
|
EventType,
|
||||||
@@ -16,15 +16,10 @@ async function withResolvedPinRoom<T>(
|
|||||||
opts: MatrixActionClientOpts,
|
opts: MatrixActionClientOpts,
|
||||||
run: (client: ActionClient, resolvedRoom: string) => Promise<T>,
|
run: (client: ActionClient, resolvedRoom: string) => Promise<T>,
|
||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
const { client, stopOnDone } = await resolveActionClient(opts);
|
return await withResolvedActionClient(opts, async (client) => {
|
||||||
try {
|
|
||||||
const resolvedRoom = await resolveMatrixRoomId(client, roomId);
|
const resolvedRoom = await resolveMatrixRoomId(client, roomId);
|
||||||
return await run(client, resolvedRoom);
|
return await run(client, resolvedRoom);
|
||||||
} finally {
|
});
|
||||||
if (stopOnDone) {
|
|
||||||
client.stop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateMatrixPins(
|
async function updateMatrixPins(
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { resolveMatrixRoomId } from "../send.js";
|
import { resolveMatrixRoomId } from "../send.js";
|
||||||
import { resolveActionClient } from "./client.js";
|
import { withResolvedActionClient } from "./client.js";
|
||||||
import {
|
import {
|
||||||
EventType,
|
EventType,
|
||||||
RelationType,
|
RelationType,
|
||||||
@@ -14,8 +14,7 @@ export async function listMatrixReactions(
|
|||||||
messageId: string,
|
messageId: string,
|
||||||
opts: MatrixActionClientOpts & { limit?: number } = {},
|
opts: MatrixActionClientOpts & { limit?: number } = {},
|
||||||
): Promise<MatrixReactionSummary[]> {
|
): Promise<MatrixReactionSummary[]> {
|
||||||
const { client, stopOnDone } = await resolveActionClient(opts);
|
return await withResolvedActionClient(opts, async (client) => {
|
||||||
try {
|
|
||||||
const resolvedRoom = await resolveMatrixRoomId(client, roomId);
|
const resolvedRoom = await resolveMatrixRoomId(client, roomId);
|
||||||
const limit =
|
const limit =
|
||||||
typeof opts.limit === "number" && Number.isFinite(opts.limit)
|
typeof opts.limit === "number" && Number.isFinite(opts.limit)
|
||||||
@@ -47,11 +46,7 @@ export async function listMatrixReactions(
|
|||||||
summaries.set(key, entry);
|
summaries.set(key, entry);
|
||||||
}
|
}
|
||||||
return Array.from(summaries.values());
|
return Array.from(summaries.values());
|
||||||
} finally {
|
});
|
||||||
if (stopOnDone) {
|
|
||||||
client.stop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function removeMatrixReactions(
|
export async function removeMatrixReactions(
|
||||||
@@ -59,8 +54,7 @@ export async function removeMatrixReactions(
|
|||||||
messageId: string,
|
messageId: string,
|
||||||
opts: MatrixActionClientOpts & { emoji?: string } = {},
|
opts: MatrixActionClientOpts & { emoji?: string } = {},
|
||||||
): Promise<{ removed: number }> {
|
): Promise<{ removed: number }> {
|
||||||
const { client, stopOnDone } = await resolveActionClient(opts);
|
return await withResolvedActionClient(opts, async (client) => {
|
||||||
try {
|
|
||||||
const resolvedRoom = await resolveMatrixRoomId(client, roomId);
|
const resolvedRoom = await resolveMatrixRoomId(client, roomId);
|
||||||
const res = (await client.doRequest(
|
const res = (await client.doRequest(
|
||||||
"GET",
|
"GET",
|
||||||
@@ -88,9 +82,5 @@ export async function removeMatrixReactions(
|
|||||||
}
|
}
|
||||||
await Promise.all(toRemove.map((id) => client.redactEvent(resolvedRoom, id)));
|
await Promise.all(toRemove.map((id) => client.redactEvent(resolvedRoom, id)));
|
||||||
return { removed: toRemove.length };
|
return { removed: toRemove.length };
|
||||||
} finally {
|
});
|
||||||
if (stopOnDone) {
|
|
||||||
client.stop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
import { resolveMatrixRoomId } from "../send.js";
|
import { resolveMatrixRoomId } from "../send.js";
|
||||||
import { resolveActionClient } from "./client.js";
|
import { withResolvedActionClient } from "./client.js";
|
||||||
import { EventType, type MatrixActionClientOpts } from "./types.js";
|
import { EventType, type MatrixActionClientOpts } from "./types.js";
|
||||||
|
|
||||||
export async function getMatrixMemberInfo(
|
export async function getMatrixMemberInfo(
|
||||||
userId: string,
|
userId: string,
|
||||||
opts: MatrixActionClientOpts & { roomId?: string } = {},
|
opts: MatrixActionClientOpts & { roomId?: string } = {},
|
||||||
) {
|
) {
|
||||||
const { client, stopOnDone } = await resolveActionClient(opts);
|
return await withResolvedActionClient(opts, async (client) => {
|
||||||
try {
|
|
||||||
const roomId = opts.roomId ? await resolveMatrixRoomId(client, opts.roomId) : undefined;
|
const roomId = opts.roomId ? await resolveMatrixRoomId(client, opts.roomId) : undefined;
|
||||||
const profile = await client.getUserProfile(userId);
|
const profile = await client.getUserProfile(userId);
|
||||||
// Membership and power levels are not included in profile calls; fetch state separately if needed.
|
// Membership and power levels are not included in profile calls; fetch state separately if needed.
|
||||||
@@ -22,16 +21,11 @@ export async function getMatrixMemberInfo(
|
|||||||
displayName: profile?.displayname ?? null,
|
displayName: profile?.displayname ?? null,
|
||||||
roomId: roomId ?? null,
|
roomId: roomId ?? null,
|
||||||
};
|
};
|
||||||
} finally {
|
});
|
||||||
if (stopOnDone) {
|
|
||||||
client.stop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getMatrixRoomInfo(roomId: string, opts: MatrixActionClientOpts = {}) {
|
export async function getMatrixRoomInfo(roomId: string, opts: MatrixActionClientOpts = {}) {
|
||||||
const { client, stopOnDone } = await resolveActionClient(opts);
|
return await withResolvedActionClient(opts, async (client) => {
|
||||||
try {
|
|
||||||
const resolvedRoom = await resolveMatrixRoomId(client, roomId);
|
const resolvedRoom = await resolveMatrixRoomId(client, roomId);
|
||||||
let name: string | null = null;
|
let name: string | null = null;
|
||||||
let topic: string | null = null;
|
let topic: string | null = null;
|
||||||
@@ -74,9 +68,5 @@ export async function getMatrixRoomInfo(roomId: string, opts: MatrixActionClient
|
|||||||
altAliases: [], // Would need separate query
|
altAliases: [], // Would need separate query
|
||||||
memberCount,
|
memberCount,
|
||||||
};
|
};
|
||||||
} finally {
|
});
|
||||||
if (stopOnDone) {
|
|
||||||
client.stop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { resolveActionClient } from "./client.js";
|
import { withResolvedActionClient } from "./client.js";
|
||||||
import type { MatrixActionClientOpts } from "./types.js";
|
import type { MatrixActionClientOpts } from "./types.js";
|
||||||
|
|
||||||
function requireCrypto(
|
function requireCrypto(
|
||||||
@@ -12,16 +12,6 @@ function requireCrypto(
|
|||||||
return client.crypto;
|
return client.crypto;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function stopActionClient(params: {
|
|
||||||
client: import("../sdk.js").MatrixClient;
|
|
||||||
stopOnDone: boolean;
|
|
||||||
}): Promise<void> {
|
|
||||||
if (!params.stopOnDone) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await params.client.stopAndPersist();
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveVerificationId(input: string): string {
|
function resolveVerificationId(input: string): string {
|
||||||
const normalized = input.trim();
|
const normalized = input.trim();
|
||||||
if (!normalized) {
|
if (!normalized) {
|
||||||
@@ -31,13 +21,14 @@ function resolveVerificationId(input: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function listMatrixVerifications(opts: MatrixActionClientOpts = {}) {
|
export async function listMatrixVerifications(opts: MatrixActionClientOpts = {}) {
|
||||||
const { client, stopOnDone } = await resolveActionClient(opts);
|
return await withResolvedActionClient(
|
||||||
try {
|
opts,
|
||||||
const crypto = requireCrypto(client);
|
async (client) => {
|
||||||
return await crypto.listVerifications();
|
const crypto = requireCrypto(client);
|
||||||
} finally {
|
return await crypto.listVerifications();
|
||||||
await stopActionClient({ client, stopOnDone });
|
},
|
||||||
}
|
"persist",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function requestMatrixVerification(
|
export async function requestMatrixVerification(
|
||||||
@@ -48,74 +39,79 @@ export async function requestMatrixVerification(
|
|||||||
roomId?: string;
|
roomId?: string;
|
||||||
} = {},
|
} = {},
|
||||||
) {
|
) {
|
||||||
const { client, stopOnDone } = await resolveActionClient(params);
|
return await withResolvedActionClient(
|
||||||
try {
|
params,
|
||||||
const crypto = requireCrypto(client);
|
async (client) => {
|
||||||
const ownUser = params.ownUser ?? (!params.userId && !params.deviceId && !params.roomId);
|
const crypto = requireCrypto(client);
|
||||||
return await crypto.requestVerification({
|
const ownUser = params.ownUser ?? (!params.userId && !params.deviceId && !params.roomId);
|
||||||
ownUser,
|
return await crypto.requestVerification({
|
||||||
userId: params.userId?.trim() || undefined,
|
ownUser,
|
||||||
deviceId: params.deviceId?.trim() || undefined,
|
userId: params.userId?.trim() || undefined,
|
||||||
roomId: params.roomId?.trim() || undefined,
|
deviceId: params.deviceId?.trim() || undefined,
|
||||||
});
|
roomId: params.roomId?.trim() || undefined,
|
||||||
} finally {
|
});
|
||||||
await stopActionClient({ client, stopOnDone });
|
},
|
||||||
}
|
"persist",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function acceptMatrixVerification(
|
export async function acceptMatrixVerification(
|
||||||
requestId: string,
|
requestId: string,
|
||||||
opts: MatrixActionClientOpts = {},
|
opts: MatrixActionClientOpts = {},
|
||||||
) {
|
) {
|
||||||
const { client, stopOnDone } = await resolveActionClient(opts);
|
return await withResolvedActionClient(
|
||||||
try {
|
opts,
|
||||||
const crypto = requireCrypto(client);
|
async (client) => {
|
||||||
return await crypto.acceptVerification(resolveVerificationId(requestId));
|
const crypto = requireCrypto(client);
|
||||||
} finally {
|
return await crypto.acceptVerification(resolveVerificationId(requestId));
|
||||||
await stopActionClient({ client, stopOnDone });
|
},
|
||||||
}
|
"persist",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function cancelMatrixVerification(
|
export async function cancelMatrixVerification(
|
||||||
requestId: string,
|
requestId: string,
|
||||||
opts: MatrixActionClientOpts & { reason?: string; code?: string } = {},
|
opts: MatrixActionClientOpts & { reason?: string; code?: string } = {},
|
||||||
) {
|
) {
|
||||||
const { client, stopOnDone } = await resolveActionClient(opts);
|
return await withResolvedActionClient(
|
||||||
try {
|
opts,
|
||||||
const crypto = requireCrypto(client);
|
async (client) => {
|
||||||
return await crypto.cancelVerification(resolveVerificationId(requestId), {
|
const crypto = requireCrypto(client);
|
||||||
reason: opts.reason?.trim() || undefined,
|
return await crypto.cancelVerification(resolveVerificationId(requestId), {
|
||||||
code: opts.code?.trim() || undefined,
|
reason: opts.reason?.trim() || undefined,
|
||||||
});
|
code: opts.code?.trim() || undefined,
|
||||||
} finally {
|
});
|
||||||
await stopActionClient({ client, stopOnDone });
|
},
|
||||||
}
|
"persist",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function startMatrixVerification(
|
export async function startMatrixVerification(
|
||||||
requestId: string,
|
requestId: string,
|
||||||
opts: MatrixActionClientOpts & { method?: "sas" } = {},
|
opts: MatrixActionClientOpts & { method?: "sas" } = {},
|
||||||
) {
|
) {
|
||||||
const { client, stopOnDone } = await resolveActionClient(opts);
|
return await withResolvedActionClient(
|
||||||
try {
|
opts,
|
||||||
const crypto = requireCrypto(client);
|
async (client) => {
|
||||||
return await crypto.startVerification(resolveVerificationId(requestId), opts.method ?? "sas");
|
const crypto = requireCrypto(client);
|
||||||
} finally {
|
return await crypto.startVerification(resolveVerificationId(requestId), opts.method ?? "sas");
|
||||||
await stopActionClient({ client, stopOnDone });
|
},
|
||||||
}
|
"persist",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function generateMatrixVerificationQr(
|
export async function generateMatrixVerificationQr(
|
||||||
requestId: string,
|
requestId: string,
|
||||||
opts: MatrixActionClientOpts = {},
|
opts: MatrixActionClientOpts = {},
|
||||||
) {
|
) {
|
||||||
const { client, stopOnDone } = await resolveActionClient(opts);
|
return await withResolvedActionClient(
|
||||||
try {
|
opts,
|
||||||
const crypto = requireCrypto(client);
|
async (client) => {
|
||||||
return await crypto.generateVerificationQr(resolveVerificationId(requestId));
|
const crypto = requireCrypto(client);
|
||||||
} finally {
|
return await crypto.generateVerificationQr(resolveVerificationId(requestId));
|
||||||
await stopActionClient({ client, stopOnDone });
|
},
|
||||||
}
|
"persist",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function scanMatrixVerificationQr(
|
export async function scanMatrixVerificationQr(
|
||||||
@@ -123,132 +119,137 @@ export async function scanMatrixVerificationQr(
|
|||||||
qrDataBase64: string,
|
qrDataBase64: string,
|
||||||
opts: MatrixActionClientOpts = {},
|
opts: MatrixActionClientOpts = {},
|
||||||
) {
|
) {
|
||||||
const { client, stopOnDone } = await resolveActionClient(opts);
|
return await withResolvedActionClient(
|
||||||
try {
|
opts,
|
||||||
const crypto = requireCrypto(client);
|
async (client) => {
|
||||||
const payload = qrDataBase64.trim();
|
const crypto = requireCrypto(client);
|
||||||
if (!payload) {
|
const payload = qrDataBase64.trim();
|
||||||
throw new Error("Matrix QR data is required");
|
if (!payload) {
|
||||||
}
|
throw new Error("Matrix QR data is required");
|
||||||
return await crypto.scanVerificationQr(resolveVerificationId(requestId), payload);
|
}
|
||||||
} finally {
|
return await crypto.scanVerificationQr(resolveVerificationId(requestId), payload);
|
||||||
await stopActionClient({ client, stopOnDone });
|
},
|
||||||
}
|
"persist",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getMatrixVerificationSas(
|
export async function getMatrixVerificationSas(
|
||||||
requestId: string,
|
requestId: string,
|
||||||
opts: MatrixActionClientOpts = {},
|
opts: MatrixActionClientOpts = {},
|
||||||
) {
|
) {
|
||||||
const { client, stopOnDone } = await resolveActionClient(opts);
|
return await withResolvedActionClient(
|
||||||
try {
|
opts,
|
||||||
const crypto = requireCrypto(client);
|
async (client) => {
|
||||||
return await crypto.getVerificationSas(resolveVerificationId(requestId));
|
const crypto = requireCrypto(client);
|
||||||
} finally {
|
return await crypto.getVerificationSas(resolveVerificationId(requestId));
|
||||||
await stopActionClient({ client, stopOnDone });
|
},
|
||||||
}
|
"persist",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function confirmMatrixVerificationSas(
|
export async function confirmMatrixVerificationSas(
|
||||||
requestId: string,
|
requestId: string,
|
||||||
opts: MatrixActionClientOpts = {},
|
opts: MatrixActionClientOpts = {},
|
||||||
) {
|
) {
|
||||||
const { client, stopOnDone } = await resolveActionClient(opts);
|
return await withResolvedActionClient(
|
||||||
try {
|
opts,
|
||||||
const crypto = requireCrypto(client);
|
async (client) => {
|
||||||
return await crypto.confirmVerificationSas(resolveVerificationId(requestId));
|
const crypto = requireCrypto(client);
|
||||||
} finally {
|
return await crypto.confirmVerificationSas(resolveVerificationId(requestId));
|
||||||
await stopActionClient({ client, stopOnDone });
|
},
|
||||||
}
|
"persist",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function mismatchMatrixVerificationSas(
|
export async function mismatchMatrixVerificationSas(
|
||||||
requestId: string,
|
requestId: string,
|
||||||
opts: MatrixActionClientOpts = {},
|
opts: MatrixActionClientOpts = {},
|
||||||
) {
|
) {
|
||||||
const { client, stopOnDone } = await resolveActionClient(opts);
|
return await withResolvedActionClient(
|
||||||
try {
|
opts,
|
||||||
const crypto = requireCrypto(client);
|
async (client) => {
|
||||||
return await crypto.mismatchVerificationSas(resolveVerificationId(requestId));
|
const crypto = requireCrypto(client);
|
||||||
} finally {
|
return await crypto.mismatchVerificationSas(resolveVerificationId(requestId));
|
||||||
await stopActionClient({ client, stopOnDone });
|
},
|
||||||
}
|
"persist",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function confirmMatrixVerificationReciprocateQr(
|
export async function confirmMatrixVerificationReciprocateQr(
|
||||||
requestId: string,
|
requestId: string,
|
||||||
opts: MatrixActionClientOpts = {},
|
opts: MatrixActionClientOpts = {},
|
||||||
) {
|
) {
|
||||||
const { client, stopOnDone } = await resolveActionClient(opts);
|
return await withResolvedActionClient(
|
||||||
try {
|
opts,
|
||||||
const crypto = requireCrypto(client);
|
async (client) => {
|
||||||
return await crypto.confirmVerificationReciprocateQr(resolveVerificationId(requestId));
|
const crypto = requireCrypto(client);
|
||||||
} finally {
|
return await crypto.confirmVerificationReciprocateQr(resolveVerificationId(requestId));
|
||||||
await stopActionClient({ client, stopOnDone });
|
},
|
||||||
}
|
"persist",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getMatrixEncryptionStatus(
|
export async function getMatrixEncryptionStatus(
|
||||||
opts: MatrixActionClientOpts & { includeRecoveryKey?: boolean } = {},
|
opts: MatrixActionClientOpts & { includeRecoveryKey?: boolean } = {},
|
||||||
) {
|
) {
|
||||||
const { client, stopOnDone } = await resolveActionClient(opts);
|
return await withResolvedActionClient(
|
||||||
try {
|
opts,
|
||||||
const crypto = requireCrypto(client);
|
async (client) => {
|
||||||
const recoveryKey = await crypto.getRecoveryKey();
|
const crypto = requireCrypto(client);
|
||||||
return {
|
const recoveryKey = await crypto.getRecoveryKey();
|
||||||
encryptionEnabled: true,
|
return {
|
||||||
recoveryKeyStored: Boolean(recoveryKey),
|
encryptionEnabled: true,
|
||||||
recoveryKeyCreatedAt: recoveryKey?.createdAt ?? null,
|
recoveryKeyStored: Boolean(recoveryKey),
|
||||||
...(opts.includeRecoveryKey ? { recoveryKey: recoveryKey?.encodedPrivateKey ?? null } : {}),
|
recoveryKeyCreatedAt: recoveryKey?.createdAt ?? null,
|
||||||
pendingVerifications: (await crypto.listVerifications()).length,
|
...(opts.includeRecoveryKey ? { recoveryKey: recoveryKey?.encodedPrivateKey ?? null } : {}),
|
||||||
};
|
pendingVerifications: (await crypto.listVerifications()).length,
|
||||||
} finally {
|
};
|
||||||
await stopActionClient({ client, stopOnDone });
|
},
|
||||||
}
|
"persist",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getMatrixVerificationStatus(
|
export async function getMatrixVerificationStatus(
|
||||||
opts: MatrixActionClientOpts & { includeRecoveryKey?: boolean } = {},
|
opts: MatrixActionClientOpts & { includeRecoveryKey?: boolean } = {},
|
||||||
) {
|
) {
|
||||||
const { client, stopOnDone } = await resolveActionClient(opts);
|
return await withResolvedActionClient(
|
||||||
try {
|
opts,
|
||||||
const status = await client.getOwnDeviceVerificationStatus();
|
async (client) => {
|
||||||
const payload = {
|
const status = await client.getOwnDeviceVerificationStatus();
|
||||||
...status,
|
const payload = {
|
||||||
pendingVerifications: client.crypto ? (await client.crypto.listVerifications()).length : 0,
|
...status,
|
||||||
};
|
pendingVerifications: client.crypto ? (await client.crypto.listVerifications()).length : 0,
|
||||||
if (!opts.includeRecoveryKey) {
|
};
|
||||||
return payload;
|
if (!opts.includeRecoveryKey) {
|
||||||
}
|
return payload;
|
||||||
const recoveryKey = client.crypto ? await client.crypto.getRecoveryKey() : null;
|
}
|
||||||
return {
|
const recoveryKey = client.crypto ? await client.crypto.getRecoveryKey() : null;
|
||||||
...payload,
|
return {
|
||||||
recoveryKey: recoveryKey?.encodedPrivateKey ?? null,
|
...payload,
|
||||||
};
|
recoveryKey: recoveryKey?.encodedPrivateKey ?? null,
|
||||||
} finally {
|
};
|
||||||
await stopActionClient({ client, stopOnDone });
|
},
|
||||||
}
|
"persist",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getMatrixRoomKeyBackupStatus(opts: MatrixActionClientOpts = {}) {
|
export async function getMatrixRoomKeyBackupStatus(opts: MatrixActionClientOpts = {}) {
|
||||||
const { client, stopOnDone } = await resolveActionClient(opts);
|
return await withResolvedActionClient(
|
||||||
try {
|
opts,
|
||||||
return await client.getRoomKeyBackupStatus();
|
async (client) => await client.getRoomKeyBackupStatus(),
|
||||||
} finally {
|
"persist",
|
||||||
await stopActionClient({ client, stopOnDone });
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function verifyMatrixRecoveryKey(
|
export async function verifyMatrixRecoveryKey(
|
||||||
recoveryKey: string,
|
recoveryKey: string,
|
||||||
opts: MatrixActionClientOpts = {},
|
opts: MatrixActionClientOpts = {},
|
||||||
) {
|
) {
|
||||||
const { client, stopOnDone } = await resolveActionClient(opts);
|
return await withResolvedActionClient(
|
||||||
try {
|
opts,
|
||||||
return await client.verifyWithRecoveryKey(recoveryKey);
|
async (client) => await client.verifyWithRecoveryKey(recoveryKey),
|
||||||
} finally {
|
"persist",
|
||||||
await stopActionClient({ client, stopOnDone });
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function restoreMatrixRoomKeyBackup(
|
export async function restoreMatrixRoomKeyBackup(
|
||||||
@@ -256,14 +257,14 @@ export async function restoreMatrixRoomKeyBackup(
|
|||||||
recoveryKey?: string;
|
recoveryKey?: string;
|
||||||
} = {},
|
} = {},
|
||||||
) {
|
) {
|
||||||
const { client, stopOnDone } = await resolveActionClient(opts);
|
return await withResolvedActionClient(
|
||||||
try {
|
opts,
|
||||||
return await client.restoreRoomKeyBackup({
|
async (client) =>
|
||||||
recoveryKey: opts.recoveryKey?.trim() || undefined,
|
await client.restoreRoomKeyBackup({
|
||||||
});
|
recoveryKey: opts.recoveryKey?.trim() || undefined,
|
||||||
} finally {
|
}),
|
||||||
await stopActionClient({ client, stopOnDone });
|
"persist",
|
||||||
}
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function bootstrapMatrixVerification(
|
export async function bootstrapMatrixVerification(
|
||||||
@@ -272,13 +273,13 @@ export async function bootstrapMatrixVerification(
|
|||||||
forceResetCrossSigning?: boolean;
|
forceResetCrossSigning?: boolean;
|
||||||
} = {},
|
} = {},
|
||||||
) {
|
) {
|
||||||
const { client, stopOnDone } = await resolveActionClient(opts);
|
return await withResolvedActionClient(
|
||||||
try {
|
opts,
|
||||||
return await client.bootstrapOwnDeviceVerification({
|
async (client) =>
|
||||||
recoveryKey: opts.recoveryKey?.trim() || undefined,
|
await client.bootstrapOwnDeviceVerification({
|
||||||
forceResetCrossSigning: opts.forceResetCrossSigning === true,
|
recoveryKey: opts.recoveryKey?.trim() || undefined,
|
||||||
});
|
forceResetCrossSigning: opts.forceResetCrossSigning === true,
|
||||||
} finally {
|
}),
|
||||||
await stopActionClient({ client, stopOnDone });
|
"persist",
|
||||||
}
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/account-id";
|
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/account-id";
|
||||||
import { getMatrixRuntime } from "../../runtime.js";
|
import { getMatrixRuntime } from "../../runtime.js";
|
||||||
import type { CoreConfig } from "../../types.js";
|
import type { CoreConfig } from "../../types.js";
|
||||||
|
import { findMatrixAccountConfig, resolveMatrixBaseConfig } from "../account-config.js";
|
||||||
import { MatrixClient } from "../sdk.js";
|
import { MatrixClient } from "../sdk.js";
|
||||||
import { ensureMatrixSdkLoggingConfigured } from "./logging.js";
|
import { ensureMatrixSdkLoggingConfigured } from "./logging.js";
|
||||||
import type { MatrixAuth, MatrixResolvedConfig } from "./types.js";
|
import type { MatrixAuth, MatrixResolvedConfig } from "./types.js";
|
||||||
@@ -83,32 +84,11 @@ export function hasReadyMatrixEnvAuth(config: {
|
|||||||
return Boolean(homeserver && (accessToken || (userId && password)));
|
return Boolean(homeserver && (accessToken || (userId && password)));
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function resolveMatrixConfig(
|
export function resolveMatrixConfig(
|
||||||
cfg: CoreConfig = getMatrixRuntime().config.loadConfig() as CoreConfig,
|
cfg: CoreConfig = getMatrixRuntime().config.loadConfig() as CoreConfig,
|
||||||
env: NodeJS.ProcessEnv = process.env,
|
env: NodeJS.ProcessEnv = process.env,
|
||||||
): MatrixResolvedConfig {
|
): MatrixResolvedConfig {
|
||||||
const matrix = cfg.channels?.["matrix-js"] ?? {};
|
const matrix = resolveMatrixBaseConfig(cfg);
|
||||||
const defaultScopedEnv = resolveScopedMatrixEnvConfig(DEFAULT_ACCOUNT_ID, env);
|
const defaultScopedEnv = resolveScopedMatrixEnvConfig(DEFAULT_ACCOUNT_ID, env);
|
||||||
const globalEnv = resolveGlobalMatrixEnvConfig(env);
|
const globalEnv = resolveGlobalMatrixEnvConfig(env);
|
||||||
const homeserver =
|
const homeserver =
|
||||||
@@ -144,8 +124,8 @@ export function resolveMatrixConfigForAccount(
|
|||||||
accountId: string,
|
accountId: string,
|
||||||
env: NodeJS.ProcessEnv = process.env,
|
env: NodeJS.ProcessEnv = process.env,
|
||||||
): MatrixResolvedConfig {
|
): MatrixResolvedConfig {
|
||||||
const matrix = cfg.channels?.["matrix-js"] ?? {};
|
const matrix = resolveMatrixBaseConfig(cfg);
|
||||||
const account = findAccountConfig(cfg, accountId);
|
const account = findMatrixAccountConfig(cfg, accountId) ?? {};
|
||||||
const normalizedAccountId = normalizeAccountId(accountId);
|
const normalizedAccountId = normalizeAccountId(accountId);
|
||||||
const scopedEnv = resolveScopedMatrixEnvConfig(normalizedAccountId, env);
|
const scopedEnv = resolveScopedMatrixEnvConfig(normalizedAccountId, env);
|
||||||
const globalEnv = resolveGlobalMatrixEnvConfig(env);
|
const globalEnv = resolveGlobalMatrixEnvConfig(env);
|
||||||
@@ -285,7 +265,7 @@ export async function resolveMatrixAuth(params?: {
|
|||||||
cachedCredentials.userId !== userId ||
|
cachedCredentials.userId !== userId ||
|
||||||
(cachedCredentials.deviceId || undefined) !== knownDeviceId;
|
(cachedCredentials.deviceId || undefined) !== knownDeviceId;
|
||||||
if (shouldRefreshCachedCredentials) {
|
if (shouldRefreshCachedCredentials) {
|
||||||
saveMatrixCredentials(
|
await saveMatrixCredentials(
|
||||||
{
|
{
|
||||||
homeserver: resolved.homeserver,
|
homeserver: resolved.homeserver,
|
||||||
userId,
|
userId,
|
||||||
@@ -296,7 +276,7 @@ export async function resolveMatrixAuth(params?: {
|
|||||||
accountId,
|
accountId,
|
||||||
);
|
);
|
||||||
} else if (hasMatchingCachedToken) {
|
} else if (hasMatchingCachedToken) {
|
||||||
touchMatrixCredentials(env, accountId);
|
await touchMatrixCredentials(env, accountId);
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
homeserver: resolved.homeserver,
|
homeserver: resolved.homeserver,
|
||||||
@@ -311,7 +291,7 @@ export async function resolveMatrixAuth(params?: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (cachedCredentials) {
|
if (cachedCredentials) {
|
||||||
touchMatrixCredentials(env, accountId);
|
await touchMatrixCredentials(env, accountId);
|
||||||
return {
|
return {
|
||||||
homeserver: cachedCredentials.homeserver,
|
homeserver: cachedCredentials.homeserver,
|
||||||
userId: cachedCredentials.userId,
|
userId: cachedCredentials.userId,
|
||||||
@@ -367,7 +347,7 @@ export async function resolveMatrixAuth(params?: {
|
|||||||
encryption: resolved.encryption,
|
encryption: resolved.encryption,
|
||||||
};
|
};
|
||||||
|
|
||||||
saveMatrixCredentials(
|
await saveMatrixCredentials(
|
||||||
{
|
{
|
||||||
homeserver: auth.homeserver,
|
homeserver: auth.homeserver,
|
||||||
userId: auth.userId,
|
userId: auth.userId,
|
||||||
|
|||||||
80
extensions/matrix-js/src/matrix/credentials.test.ts
Normal file
80
extensions/matrix-js/src/matrix/credentials.test.ts
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import fs from "node:fs";
|
||||||
|
import os from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import { setMatrixRuntime } from "../runtime.js";
|
||||||
|
import {
|
||||||
|
loadMatrixCredentials,
|
||||||
|
resolveMatrixCredentialsPath,
|
||||||
|
saveMatrixCredentials,
|
||||||
|
touchMatrixCredentials,
|
||||||
|
} from "./credentials.js";
|
||||||
|
|
||||||
|
describe("matrix credentials storage", () => {
|
||||||
|
const tempDirs: string[] = [];
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
for (const dir of tempDirs.splice(0)) {
|
||||||
|
fs.rmSync(dir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function setupStateDir(): string {
|
||||||
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-matrix-creds-"));
|
||||||
|
tempDirs.push(dir);
|
||||||
|
setMatrixRuntime({
|
||||||
|
state: {
|
||||||
|
resolveStateDir: () => dir,
|
||||||
|
},
|
||||||
|
} as never);
|
||||||
|
return dir;
|
||||||
|
}
|
||||||
|
|
||||||
|
it("writes credentials atomically with secure file permissions", async () => {
|
||||||
|
setupStateDir();
|
||||||
|
await saveMatrixCredentials(
|
||||||
|
{
|
||||||
|
homeserver: "https://matrix.example.org",
|
||||||
|
userId: "@bot:example.org",
|
||||||
|
accessToken: "secret-token",
|
||||||
|
deviceId: "DEVICE123",
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
"ops",
|
||||||
|
);
|
||||||
|
|
||||||
|
const credPath = resolveMatrixCredentialsPath({}, "ops");
|
||||||
|
expect(fs.existsSync(credPath)).toBe(true);
|
||||||
|
const mode = fs.statSync(credPath).mode & 0o777;
|
||||||
|
expect(mode).toBe(0o600);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("touch updates lastUsedAt while preserving createdAt", async () => {
|
||||||
|
setupStateDir();
|
||||||
|
vi.useFakeTimers();
|
||||||
|
try {
|
||||||
|
vi.setSystemTime(new Date("2026-03-01T10:00:00.000Z"));
|
||||||
|
await saveMatrixCredentials(
|
||||||
|
{
|
||||||
|
homeserver: "https://matrix.example.org",
|
||||||
|
userId: "@bot:example.org",
|
||||||
|
accessToken: "secret-token",
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
"default",
|
||||||
|
);
|
||||||
|
const initial = loadMatrixCredentials({}, "default");
|
||||||
|
expect(initial).not.toBeNull();
|
||||||
|
|
||||||
|
vi.setSystemTime(new Date("2026-03-01T10:05:00.000Z"));
|
||||||
|
await touchMatrixCredentials({}, "default");
|
||||||
|
const touched = loadMatrixCredentials({}, "default");
|
||||||
|
expect(touched).not.toBeNull();
|
||||||
|
|
||||||
|
expect(touched?.createdAt).toBe(initial?.createdAt);
|
||||||
|
expect(touched?.lastUsedAt).toBe("2026-03-01T10:05:00.000Z");
|
||||||
|
} finally {
|
||||||
|
vi.useRealTimers();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
import os from "node:os";
|
import os from "node:os";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
|
import { writeJsonFileAtomically } from "openclaw/plugin-sdk";
|
||||||
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/account-id";
|
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/account-id";
|
||||||
import { getMatrixRuntime } from "../runtime.js";
|
import { getMatrixRuntime } from "../runtime.js";
|
||||||
|
|
||||||
@@ -63,14 +64,11 @@ export function loadMatrixCredentials(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function saveMatrixCredentials(
|
export async function saveMatrixCredentials(
|
||||||
credentials: Omit<MatrixStoredCredentials, "createdAt" | "lastUsedAt">,
|
credentials: Omit<MatrixStoredCredentials, "createdAt" | "lastUsedAt">,
|
||||||
env: NodeJS.ProcessEnv = process.env,
|
env: NodeJS.ProcessEnv = process.env,
|
||||||
accountId?: string | null,
|
accountId?: string | null,
|
||||||
): void {
|
): Promise<void> {
|
||||||
const dir = resolveMatrixCredentialsDir(env);
|
|
||||||
fs.mkdirSync(dir, { recursive: true });
|
|
||||||
|
|
||||||
const credPath = resolveMatrixCredentialsPath(env, accountId);
|
const credPath = resolveMatrixCredentialsPath(env, accountId);
|
||||||
|
|
||||||
const existing = loadMatrixCredentials(env, accountId);
|
const existing = loadMatrixCredentials(env, accountId);
|
||||||
@@ -82,13 +80,13 @@ export function saveMatrixCredentials(
|
|||||||
lastUsedAt: now,
|
lastUsedAt: now,
|
||||||
};
|
};
|
||||||
|
|
||||||
fs.writeFileSync(credPath, JSON.stringify(toSave, null, 2), "utf-8");
|
await writeJsonFileAtomically(credPath, toSave);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function touchMatrixCredentials(
|
export async function touchMatrixCredentials(
|
||||||
env: NodeJS.ProcessEnv = process.env,
|
env: NodeJS.ProcessEnv = process.env,
|
||||||
accountId?: string | null,
|
accountId?: string | null,
|
||||||
): void {
|
): Promise<void> {
|
||||||
const existing = loadMatrixCredentials(env, accountId);
|
const existing = loadMatrixCredentials(env, accountId);
|
||||||
if (!existing) {
|
if (!existing) {
|
||||||
return;
|
return;
|
||||||
@@ -96,7 +94,7 @@ export function touchMatrixCredentials(
|
|||||||
|
|
||||||
existing.lastUsedAt = new Date().toISOString();
|
existing.lastUsedAt = new Date().toISOString();
|
||||||
const credPath = resolveMatrixCredentialsPath(env, accountId);
|
const credPath = resolveMatrixCredentialsPath(env, accountId);
|
||||||
fs.writeFileSync(credPath, JSON.stringify(existing, null, 2), "utf-8");
|
await writeJsonFileAtomically(credPath, existing);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function clearMatrixCredentials(
|
export function clearMatrixCredentials(
|
||||||
|
|||||||
@@ -2,7 +2,164 @@ import { describe, expect, it, vi } from "vitest";
|
|||||||
import { createMatrixRoomMessageHandler } from "./handler.js";
|
import { createMatrixRoomMessageHandler } from "./handler.js";
|
||||||
import { EventType, type MatrixRawEvent } from "./types.js";
|
import { EventType, type MatrixRawEvent } from "./types.js";
|
||||||
|
|
||||||
|
const sendMessageMatrixMock = vi.hoisted(() =>
|
||||||
|
vi.fn(async (..._args: unknown[]) => ({ messageId: "evt", roomId: "!room" })),
|
||||||
|
);
|
||||||
|
|
||||||
|
vi.mock("../send.js", () => ({
|
||||||
|
reactMatrixMessage: vi.fn(async () => {}),
|
||||||
|
sendMessageMatrix: sendMessageMatrixMock,
|
||||||
|
sendReadReceiptMatrix: vi.fn(async () => {}),
|
||||||
|
sendTypingMatrix: vi.fn(async () => {}),
|
||||||
|
}));
|
||||||
|
|
||||||
describe("matrix monitor handler pairing account scope", () => {
|
describe("matrix monitor handler pairing account scope", () => {
|
||||||
|
it("caches account-scoped allowFrom store reads on hot path", async () => {
|
||||||
|
const readAllowFromStore = vi.fn(async () => [] as string[]);
|
||||||
|
const upsertPairingRequest = vi.fn(async () => ({ code: "ABCDEFGH", created: false }));
|
||||||
|
sendMessageMatrixMock.mockClear();
|
||||||
|
|
||||||
|
const handler = createMatrixRoomMessageHandler({
|
||||||
|
client: {
|
||||||
|
getUserId: async () => "@bot:example.org",
|
||||||
|
} as never,
|
||||||
|
core: {
|
||||||
|
channel: {
|
||||||
|
pairing: {
|
||||||
|
readAllowFromStore,
|
||||||
|
upsertPairingRequest,
|
||||||
|
buildPairingReply: () => "pairing",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as never,
|
||||||
|
cfg: {} as never,
|
||||||
|
accountId: "poe",
|
||||||
|
runtime: {} as never,
|
||||||
|
logger: {
|
||||||
|
info: () => {},
|
||||||
|
warn: () => {},
|
||||||
|
} as never,
|
||||||
|
logVerboseMessage: () => {},
|
||||||
|
allowFrom: [],
|
||||||
|
mentionRegexes: [],
|
||||||
|
groupPolicy: "open",
|
||||||
|
replyToMode: "off",
|
||||||
|
threadReplies: "inbound",
|
||||||
|
dmEnabled: true,
|
||||||
|
dmPolicy: "pairing",
|
||||||
|
textLimit: 8_000,
|
||||||
|
mediaMaxBytes: 10_000_000,
|
||||||
|
startupMs: 0,
|
||||||
|
startupGraceMs: 0,
|
||||||
|
directTracker: {
|
||||||
|
isDirectMessage: async () => true,
|
||||||
|
},
|
||||||
|
getRoomInfo: async () => ({ altAliases: [] }),
|
||||||
|
getMemberDisplayName: async () => "sender",
|
||||||
|
});
|
||||||
|
|
||||||
|
await handler("!room:example.org", {
|
||||||
|
type: EventType.RoomMessage,
|
||||||
|
sender: "@user:example.org",
|
||||||
|
event_id: "$event1",
|
||||||
|
origin_server_ts: Date.now(),
|
||||||
|
content: {
|
||||||
|
msgtype: "m.text",
|
||||||
|
body: "hello",
|
||||||
|
"m.mentions": { room: true },
|
||||||
|
},
|
||||||
|
} as MatrixRawEvent);
|
||||||
|
|
||||||
|
await handler("!room:example.org", {
|
||||||
|
type: EventType.RoomMessage,
|
||||||
|
sender: "@user:example.org",
|
||||||
|
event_id: "$event2",
|
||||||
|
origin_server_ts: Date.now(),
|
||||||
|
content: {
|
||||||
|
msgtype: "m.text",
|
||||||
|
body: "hello again",
|
||||||
|
"m.mentions": { room: true },
|
||||||
|
},
|
||||||
|
} as MatrixRawEvent);
|
||||||
|
|
||||||
|
expect(readAllowFromStore).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sends pairing reminders for pending requests with cooldown", async () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
vi.setSystemTime(new Date("2026-03-01T10:00:00.000Z"));
|
||||||
|
try {
|
||||||
|
const readAllowFromStore = vi.fn(async () => [] as string[]);
|
||||||
|
const upsertPairingRequest = vi.fn(async () => ({ code: "ABCDEFGH", created: false }));
|
||||||
|
sendMessageMatrixMock.mockClear();
|
||||||
|
|
||||||
|
const handler = createMatrixRoomMessageHandler({
|
||||||
|
client: {
|
||||||
|
getUserId: async () => "@bot:example.org",
|
||||||
|
} as never,
|
||||||
|
core: {
|
||||||
|
channel: {
|
||||||
|
pairing: {
|
||||||
|
readAllowFromStore,
|
||||||
|
upsertPairingRequest,
|
||||||
|
buildPairingReply: () => "Pairing code: ABCDEFGH",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as never,
|
||||||
|
cfg: {} as never,
|
||||||
|
accountId: "poe",
|
||||||
|
runtime: {} as never,
|
||||||
|
logger: {
|
||||||
|
info: () => {},
|
||||||
|
warn: () => {},
|
||||||
|
} as never,
|
||||||
|
logVerboseMessage: () => {},
|
||||||
|
allowFrom: [],
|
||||||
|
mentionRegexes: [],
|
||||||
|
groupPolicy: "open",
|
||||||
|
replyToMode: "off",
|
||||||
|
threadReplies: "inbound",
|
||||||
|
dmEnabled: true,
|
||||||
|
dmPolicy: "pairing",
|
||||||
|
textLimit: 8_000,
|
||||||
|
mediaMaxBytes: 10_000_000,
|
||||||
|
startupMs: 0,
|
||||||
|
startupGraceMs: 0,
|
||||||
|
directTracker: {
|
||||||
|
isDirectMessage: async () => true,
|
||||||
|
},
|
||||||
|
getRoomInfo: async () => ({ altAliases: [] }),
|
||||||
|
getMemberDisplayName: async () => "sender",
|
||||||
|
});
|
||||||
|
|
||||||
|
const makeEvent = (id: string): MatrixRawEvent =>
|
||||||
|
({
|
||||||
|
type: EventType.RoomMessage,
|
||||||
|
sender: "@user:example.org",
|
||||||
|
event_id: id,
|
||||||
|
origin_server_ts: Date.now(),
|
||||||
|
content: {
|
||||||
|
msgtype: "m.text",
|
||||||
|
body: "hello",
|
||||||
|
"m.mentions": { room: true },
|
||||||
|
},
|
||||||
|
}) as MatrixRawEvent;
|
||||||
|
|
||||||
|
await handler("!room:example.org", makeEvent("$event1"));
|
||||||
|
await handler("!room:example.org", makeEvent("$event2"));
|
||||||
|
expect(sendMessageMatrixMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(String(sendMessageMatrixMock.mock.calls[0]?.[1] ?? "")).toContain(
|
||||||
|
"Pairing request is still pending approval.",
|
||||||
|
);
|
||||||
|
|
||||||
|
await vi.advanceTimersByTimeAsync(5 * 60_000 + 1);
|
||||||
|
await handler("!room:example.org", makeEvent("$event3"));
|
||||||
|
expect(sendMessageMatrixMock).toHaveBeenCalledTimes(2);
|
||||||
|
} finally {
|
||||||
|
vi.useRealTimers();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it("uses account-scoped pairing store reads and upserts for dm pairing", async () => {
|
it("uses account-scoped pairing store reads and upserts for dm pairing", async () => {
|
||||||
const readAllowFromStore = vi.fn(async () => [] as string[]);
|
const readAllowFromStore = vi.fn(async () => [] as string[]);
|
||||||
const upsertPairingRequest = vi.fn(async () => ({ code: "ABCDEFGH", created: false }));
|
const upsertPairingRequest = vi.fn(async () => ({ code: "ABCDEFGH", created: false }));
|
||||||
@@ -57,7 +214,11 @@ describe("matrix monitor handler pairing account scope", () => {
|
|||||||
},
|
},
|
||||||
} as MatrixRawEvent);
|
} as MatrixRawEvent);
|
||||||
|
|
||||||
expect(readAllowFromStore).toHaveBeenCalledWith("matrix-js", process.env, "poe");
|
expect(readAllowFromStore).toHaveBeenCalledWith({
|
||||||
|
channel: "matrix-js",
|
||||||
|
env: process.env,
|
||||||
|
accountId: "poe",
|
||||||
|
});
|
||||||
expect(upsertPairingRequest).toHaveBeenCalledWith({
|
expect(upsertPairingRequest).toHaveBeenCalledWith({
|
||||||
channel: "matrix-js",
|
channel: "matrix-js",
|
||||||
id: "@user:example.org",
|
id: "@user:example.org",
|
||||||
|
|||||||
@@ -39,11 +39,15 @@ import type { MatrixRawEvent, RoomMessageEventContent } from "./types.js";
|
|||||||
import { EventType, RelationType } from "./types.js";
|
import { EventType, RelationType } from "./types.js";
|
||||||
import { isMatrixVerificationRoomMessage } from "./verification-utils.js";
|
import { isMatrixVerificationRoomMessage } from "./verification-utils.js";
|
||||||
|
|
||||||
|
const ALLOW_FROM_STORE_CACHE_TTL_MS = 30_000;
|
||||||
|
const PAIRING_REPLY_COOLDOWN_MS = 5 * 60_000;
|
||||||
|
const MAX_TRACKED_PAIRING_REPLY_SENDERS = 512;
|
||||||
|
|
||||||
export type MatrixMonitorHandlerParams = {
|
export type MatrixMonitorHandlerParams = {
|
||||||
client: MatrixClient;
|
client: MatrixClient;
|
||||||
core: PluginRuntime;
|
core: PluginRuntime;
|
||||||
cfg: CoreConfig;
|
cfg: CoreConfig;
|
||||||
accountId?: string;
|
accountId: string;
|
||||||
runtime: RuntimeEnv;
|
runtime: RuntimeEnv;
|
||||||
logger: RuntimeLogger;
|
logger: RuntimeLogger;
|
||||||
logVerboseMessage: (message: string) => void;
|
logVerboseMessage: (message: string) => void;
|
||||||
@@ -97,6 +101,50 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
|
|||||||
getRoomInfo,
|
getRoomInfo,
|
||||||
getMemberDisplayName,
|
getMemberDisplayName,
|
||||||
} = params;
|
} = params;
|
||||||
|
let cachedStoreAllowFrom: {
|
||||||
|
value: string[];
|
||||||
|
expiresAtMs: number;
|
||||||
|
} | null = null;
|
||||||
|
const pairingReplySentAtMsBySender = new Map<string, number>();
|
||||||
|
|
||||||
|
const readStoreAllowFrom = async (): Promise<string[]> => {
|
||||||
|
const now = Date.now();
|
||||||
|
if (cachedStoreAllowFrom && now < cachedStoreAllowFrom.expiresAtMs) {
|
||||||
|
return cachedStoreAllowFrom.value;
|
||||||
|
}
|
||||||
|
const value = await core.channel.pairing
|
||||||
|
.readAllowFromStore({
|
||||||
|
channel: "matrix-js",
|
||||||
|
env: process.env,
|
||||||
|
accountId,
|
||||||
|
})
|
||||||
|
.catch(() => []);
|
||||||
|
cachedStoreAllowFrom = {
|
||||||
|
value,
|
||||||
|
expiresAtMs: now + ALLOW_FROM_STORE_CACHE_TTL_MS,
|
||||||
|
};
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const shouldSendPairingReply = (senderId: string, created: boolean): boolean => {
|
||||||
|
const now = Date.now();
|
||||||
|
if (created) {
|
||||||
|
pairingReplySentAtMsBySender.set(senderId, now);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const lastSentAtMs = pairingReplySentAtMsBySender.get(senderId);
|
||||||
|
if (typeof lastSentAtMs === "number" && now - lastSentAtMs < PAIRING_REPLY_COOLDOWN_MS) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
pairingReplySentAtMsBySender.set(senderId, now);
|
||||||
|
if (pairingReplySentAtMsBySender.size > MAX_TRACKED_PAIRING_REPLY_SENDERS) {
|
||||||
|
const oldestSender = pairingReplySentAtMsBySender.keys().next().value;
|
||||||
|
if (typeof oldestSender === "string") {
|
||||||
|
pairingReplySentAtMsBySender.delete(oldestSender);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
return async (roomId: string, event: MatrixRawEvent) => {
|
return async (roomId: string, event: MatrixRawEvent) => {
|
||||||
try {
|
try {
|
||||||
@@ -230,9 +278,7 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
|
|||||||
}
|
}
|
||||||
|
|
||||||
const senderName = await getMemberDisplayName(roomId, senderId);
|
const senderName = await getMemberDisplayName(roomId, senderId);
|
||||||
const storeAllowFrom = await core.channel.pairing
|
const storeAllowFrom = await readStoreAllowFrom();
|
||||||
.readAllowFromStore("matrix-js", process.env, accountId)
|
|
||||||
.catch(() => []);
|
|
||||||
const effectiveAllowFrom = normalizeMatrixAllowList([...allowFrom, ...storeAllowFrom]);
|
const effectiveAllowFrom = normalizeMatrixAllowList([...allowFrom, ...storeAllowFrom]);
|
||||||
const groupAllowFrom = cfg.channels?.["matrix-js"]?.groupAllowFrom ?? [];
|
const groupAllowFrom = cfg.channels?.["matrix-js"]?.groupAllowFrom ?? [];
|
||||||
const effectiveGroupAllowFrom = normalizeMatrixAllowList(groupAllowFrom);
|
const effectiveGroupAllowFrom = normalizeMatrixAllowList(groupAllowFrom);
|
||||||
@@ -256,23 +302,32 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
|
|||||||
accountId,
|
accountId,
|
||||||
meta: { name: senderName },
|
meta: { name: senderName },
|
||||||
});
|
});
|
||||||
if (created) {
|
if (shouldSendPairingReply(senderId, created)) {
|
||||||
|
const pairingReply = core.channel.pairing.buildPairingReply({
|
||||||
|
channel: "matrix-js",
|
||||||
|
idLine: `Your Matrix user id: ${senderId}`,
|
||||||
|
code,
|
||||||
|
});
|
||||||
logVerboseMessage(
|
logVerboseMessage(
|
||||||
`matrix pairing request sender=${senderId} name=${senderName ?? "unknown"} (${allowMatchMeta})`,
|
created
|
||||||
|
? `matrix pairing request sender=${senderId} name=${senderName ?? "unknown"} (${allowMatchMeta})`
|
||||||
|
: `matrix pairing reminder sender=${senderId} name=${senderName ?? "unknown"} (${allowMatchMeta})`,
|
||||||
);
|
);
|
||||||
try {
|
try {
|
||||||
await sendMessageMatrix(
|
await sendMessageMatrix(
|
||||||
`room:${roomId}`,
|
`room:${roomId}`,
|
||||||
core.channel.pairing.buildPairingReply({
|
created
|
||||||
channel: "matrix-js",
|
? pairingReply
|
||||||
idLine: `Your Matrix user id: ${senderId}`,
|
: `${pairingReply}\n\nPairing request is still pending approval. Reusing existing code.`,
|
||||||
code,
|
|
||||||
}),
|
|
||||||
{ client },
|
{ client },
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logVerboseMessage(`matrix pairing reply failed for ${senderId}: ${String(err)}`);
|
logVerboseMessage(`matrix pairing reply failed for ${senderId}: ${String(err)}`);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
logVerboseMessage(
|
||||||
|
`matrix pairing reminder suppressed sender=${senderId} (cooldown)`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (dmPolicy !== "pairing") {
|
if (dmPolicy !== "pairing") {
|
||||||
|
|||||||
@@ -286,7 +286,7 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi
|
|||||||
client,
|
client,
|
||||||
core,
|
core,
|
||||||
cfg,
|
cfg,
|
||||||
accountId: opts.accountId ?? undefined,
|
accountId: account.accountId,
|
||||||
runtime,
|
runtime,
|
||||||
logger,
|
logger,
|
||||||
logVerboseMessage,
|
logVerboseMessage,
|
||||||
|
|||||||
@@ -289,6 +289,7 @@ describe("MatrixVerificationManager", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("does not auto-confirm SAS for verifications initiated by this device", async () => {
|
it("does not auto-confirm SAS for verifications initiated by this device", async () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
const confirm = vi.fn(async () => {});
|
const confirm = vi.fn(async () => {});
|
||||||
const verifier = new MockVerifier(
|
const verifier = new MockVerifier(
|
||||||
{
|
{
|
||||||
@@ -312,11 +313,15 @@ describe("MatrixVerificationManager", () => {
|
|||||||
initiatedByMe: true,
|
initiatedByMe: true,
|
||||||
verifier,
|
verifier,
|
||||||
});
|
});
|
||||||
const manager = new MatrixVerificationManager();
|
try {
|
||||||
manager.trackVerificationRequest(request);
|
const manager = new MatrixVerificationManager();
|
||||||
|
manager.trackVerificationRequest(request);
|
||||||
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 20));
|
await vi.advanceTimersByTimeAsync(20);
|
||||||
expect(confirm).not.toHaveBeenCalled();
|
expect(confirm).not.toHaveBeenCalled();
|
||||||
|
} finally {
|
||||||
|
vi.useRealTimers();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it("prunes stale terminal sessions during list operations", () => {
|
it("prunes stale terminal sessions during list operations", () => {
|
||||||
|
|||||||
@@ -54,7 +54,14 @@ describe("sendMessageMatrix media", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
loadWebMediaMock.mockReset().mockResolvedValue({
|
||||||
|
buffer: Buffer.from("media"),
|
||||||
|
fileName: "photo.png",
|
||||||
|
contentType: "image/png",
|
||||||
|
kind: "image",
|
||||||
|
});
|
||||||
|
getImageMetadataMock.mockReset().mockResolvedValue(null);
|
||||||
|
resizeToJpegMock.mockReset();
|
||||||
setMatrixRuntime(runtimeStub);
|
setMatrixRuntime(runtimeStub);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -117,6 +124,72 @@ describe("sendMessageMatrix media", () => {
|
|||||||
expect(content.url).toBeUndefined();
|
expect(content.url).toBeUndefined();
|
||||||
expect(content.file?.url).toBe("mxc://example/file");
|
expect(content.file?.url).toBe("mxc://example/file");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("does not upload plaintext thumbnails for encrypted image sends", async () => {
|
||||||
|
const { client, uploadContent } = makeClient();
|
||||||
|
(client as { crypto?: object }).crypto = {
|
||||||
|
isRoomEncrypted: vi.fn().mockResolvedValue(true),
|
||||||
|
encryptMedia: vi.fn().mockResolvedValue({
|
||||||
|
buffer: Buffer.from("encrypted"),
|
||||||
|
file: {
|
||||||
|
key: {
|
||||||
|
kty: "oct",
|
||||||
|
key_ops: ["encrypt", "decrypt"],
|
||||||
|
alg: "A256CTR",
|
||||||
|
k: "secret",
|
||||||
|
ext: true,
|
||||||
|
},
|
||||||
|
iv: "iv",
|
||||||
|
hashes: { sha256: "hash" },
|
||||||
|
v: "v2",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
getImageMetadataMock
|
||||||
|
.mockResolvedValueOnce({ width: 1600, height: 1200 })
|
||||||
|
.mockResolvedValueOnce({ width: 800, height: 600 });
|
||||||
|
resizeToJpegMock.mockResolvedValueOnce(Buffer.from("thumb"));
|
||||||
|
|
||||||
|
await sendMessageMatrix("room:!room:example", "caption", {
|
||||||
|
client,
|
||||||
|
mediaUrl: "file:///tmp/photo.png",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(uploadContent).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uploads thumbnail metadata for unencrypted large images", async () => {
|
||||||
|
const { client, sendMessage, uploadContent } = makeClient();
|
||||||
|
getImageMetadataMock
|
||||||
|
.mockResolvedValueOnce({ width: 1600, height: 1200 })
|
||||||
|
.mockResolvedValueOnce({ width: 800, height: 600 });
|
||||||
|
resizeToJpegMock.mockResolvedValueOnce(Buffer.from("thumb"));
|
||||||
|
|
||||||
|
await sendMessageMatrix("room:!room:example", "caption", {
|
||||||
|
client,
|
||||||
|
mediaUrl: "file:///tmp/photo.png",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(uploadContent).toHaveBeenCalledTimes(2);
|
||||||
|
const content = sendMessage.mock.calls[0]?.[1] as {
|
||||||
|
info?: {
|
||||||
|
thumbnail_url?: string;
|
||||||
|
thumbnail_info?: {
|
||||||
|
w?: number;
|
||||||
|
h?: number;
|
||||||
|
mimetype?: string;
|
||||||
|
size?: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
expect(content.info?.thumbnail_url).toBe("mxc://example/file");
|
||||||
|
expect(content.info?.thumbnail_info).toMatchObject({
|
||||||
|
w: 800,
|
||||||
|
h: 600,
|
||||||
|
mimetype: "image/jpeg",
|
||||||
|
size: Buffer.from("thumb").byteLength,
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("sendMessageMatrix threads", () => {
|
describe("sendMessageMatrix threads", () => {
|
||||||
|
|||||||
@@ -99,7 +99,11 @@ export async function sendMessageMatrix(
|
|||||||
const msgtype = useVoice ? MsgType.Audio : baseMsgType;
|
const msgtype = useVoice ? MsgType.Audio : baseMsgType;
|
||||||
const isImage = msgtype === MsgType.Image;
|
const isImage = msgtype === MsgType.Image;
|
||||||
const imageInfo = isImage
|
const imageInfo = isImage
|
||||||
? await prepareImageInfo({ buffer: media.buffer, client })
|
? await prepareImageInfo({
|
||||||
|
buffer: media.buffer,
|
||||||
|
client,
|
||||||
|
encrypted: Boolean(uploaded.file),
|
||||||
|
})
|
||||||
: undefined;
|
: undefined;
|
||||||
const [firstChunk, ...rest] = chunks;
|
const [firstChunk, ...rest] = chunks;
|
||||||
const body = useVoice ? "Voice message" : (firstChunk ?? media.fileName ?? "(file)");
|
const body = useVoice ? "Voice message" : (firstChunk ?? media.fileName ?? "(file)");
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { normalizeAccountId } from "openclaw/plugin-sdk/account-id";
|
|
||||||
import { getMatrixRuntime } from "../../runtime.js";
|
import { getMatrixRuntime } from "../../runtime.js";
|
||||||
import type { CoreConfig } from "../../types.js";
|
import type { CoreConfig } from "../../types.js";
|
||||||
|
import { resolveMatrixAccountConfig } from "../accounts.js";
|
||||||
import { getActiveMatrixClient } from "../active-client.js";
|
import { getActiveMatrixClient } from "../active-client.js";
|
||||||
import { createMatrixClient, isBunRuntime, resolveMatrixAuth } from "../client.js";
|
import { createMatrixClient, isBunRuntime, resolveMatrixAuth } from "../client.js";
|
||||||
import type { MatrixClient } from "../sdk.js";
|
import type { MatrixClient } from "../sdk.js";
|
||||||
@@ -15,16 +15,8 @@ export function ensureNodeRuntime() {
|
|||||||
|
|
||||||
export function resolveMediaMaxBytes(accountId?: string | null): number | undefined {
|
export function resolveMediaMaxBytes(accountId?: string | null): number | undefined {
|
||||||
const cfg = getCore().config.loadConfig() as CoreConfig;
|
const cfg = getCore().config.loadConfig() as CoreConfig;
|
||||||
const matrixCfg = cfg.channels?.["matrix-js"];
|
const matrixCfg = resolveMatrixAccountConfig({ cfg, accountId });
|
||||||
const accountCfg = accountId
|
const mediaMaxMb = typeof matrixCfg.mediaMaxMb === "number" ? matrixCfg.mediaMaxMb : undefined;
|
||||||
? (matrixCfg?.accounts?.[accountId] ?? matrixCfg?.accounts?.[normalizeAccountId(accountId)])
|
|
||||||
: undefined;
|
|
||||||
const mediaMaxMb =
|
|
||||||
typeof accountCfg?.mediaMaxMb === "number"
|
|
||||||
? accountCfg.mediaMaxMb
|
|
||||||
: typeof matrixCfg?.mediaMaxMb === "number"
|
|
||||||
? matrixCfg.mediaMaxMb
|
|
||||||
: undefined;
|
|
||||||
if (typeof mediaMaxMb === "number") {
|
if (typeof mediaMaxMb === "number") {
|
||||||
return mediaMaxMb * 1024 * 1024;
|
return mediaMaxMb * 1024 * 1024;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -113,6 +113,7 @@ const THUMBNAIL_QUALITY = 80;
|
|||||||
export async function prepareImageInfo(params: {
|
export async function prepareImageInfo(params: {
|
||||||
buffer: Buffer;
|
buffer: Buffer;
|
||||||
client: MatrixClient;
|
client: MatrixClient;
|
||||||
|
encrypted?: boolean;
|
||||||
}): Promise<DimensionalFileInfo | undefined> {
|
}): Promise<DimensionalFileInfo | undefined> {
|
||||||
const meta = await getCore()
|
const meta = await getCore()
|
||||||
.media.getImageMetadata(params.buffer)
|
.media.getImageMetadata(params.buffer)
|
||||||
@@ -121,6 +122,10 @@ export async function prepareImageInfo(params: {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
const imageInfo: DimensionalFileInfo = { w: meta.width, h: meta.height };
|
const imageInfo: DimensionalFileInfo = { w: meta.width, h: meta.height };
|
||||||
|
if (params.encrypted) {
|
||||||
|
// For E2EE media, avoid uploading plaintext thumbnails.
|
||||||
|
return imageInfo;
|
||||||
|
}
|
||||||
const maxDim = Math.max(meta.width, meta.height);
|
const maxDim = Math.max(meta.width, meta.height);
|
||||||
if (maxDim > THUMBNAIL_MAX_SIDE) {
|
if (maxDim > THUMBNAIL_MAX_SIDE) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ describe("matrix onboarding", () => {
|
|||||||
MATRIX_USER_ID: process.env.MATRIX_USER_ID,
|
MATRIX_USER_ID: process.env.MATRIX_USER_ID,
|
||||||
MATRIX_ACCESS_TOKEN: process.env.MATRIX_ACCESS_TOKEN,
|
MATRIX_ACCESS_TOKEN: process.env.MATRIX_ACCESS_TOKEN,
|
||||||
MATRIX_PASSWORD: process.env.MATRIX_PASSWORD,
|
MATRIX_PASSWORD: process.env.MATRIX_PASSWORD,
|
||||||
|
MATRIX_DEVICE_ID: process.env.MATRIX_DEVICE_ID,
|
||||||
|
MATRIX_DEVICE_NAME: process.env.MATRIX_DEVICE_NAME,
|
||||||
MATRIX_OPS_HOMESERVER: process.env.MATRIX_OPS_HOMESERVER,
|
MATRIX_OPS_HOMESERVER: process.env.MATRIX_OPS_HOMESERVER,
|
||||||
MATRIX_OPS_ACCESS_TOKEN: process.env.MATRIX_OPS_ACCESS_TOKEN,
|
MATRIX_OPS_ACCESS_TOKEN: process.env.MATRIX_OPS_ACCESS_TOKEN,
|
||||||
};
|
};
|
||||||
@@ -114,4 +116,48 @@ describe("matrix onboarding", () => {
|
|||||||
),
|
),
|
||||||
).toBe(true);
|
).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("includes device env var names in auth help text", async () => {
|
||||||
|
setMatrixRuntime({
|
||||||
|
state: {
|
||||||
|
resolveStateDir: (_env: NodeJS.ProcessEnv, homeDir?: () => string) =>
|
||||||
|
(homeDir ?? (() => "/tmp"))(),
|
||||||
|
},
|
||||||
|
config: {
|
||||||
|
loadConfig: () => ({}),
|
||||||
|
},
|
||||||
|
} as never);
|
||||||
|
|
||||||
|
const notes: string[] = [];
|
||||||
|
const prompter = {
|
||||||
|
note: vi.fn(async (message: unknown) => {
|
||||||
|
notes.push(String(message));
|
||||||
|
}),
|
||||||
|
text: vi.fn(async () => {
|
||||||
|
throw new Error("stop-after-help");
|
||||||
|
}),
|
||||||
|
confirm: vi.fn(async () => false),
|
||||||
|
select: vi.fn(async () => "token"),
|
||||||
|
} as unknown as WizardPrompter;
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
matrixOnboardingAdapter.configureInteractive!({
|
||||||
|
cfg: { channels: {} } as CoreConfig,
|
||||||
|
runtime: { log: vi.fn(), error: vi.fn(), exit: vi.fn() } as unknown as RuntimeEnv,
|
||||||
|
prompter,
|
||||||
|
options: undefined,
|
||||||
|
accountOverrides: {},
|
||||||
|
shouldPromptAccountIds: false,
|
||||||
|
forceAllowFrom: false,
|
||||||
|
configured: false,
|
||||||
|
label: "Matrix-js",
|
||||||
|
}),
|
||||||
|
).rejects.toThrow("stop-after-help");
|
||||||
|
|
||||||
|
const noteText = notes.join("\n");
|
||||||
|
expect(noteText).toContain("MATRIX_DEVICE_ID");
|
||||||
|
expect(noteText).toContain("MATRIX_DEVICE_NAME");
|
||||||
|
expect(noteText).toContain("MATRIX_<ACCOUNT_ID>_DEVICE_ID");
|
||||||
|
expect(noteText).toContain("MATRIX_<ACCOUNT_ID>_DEVICE_NAME");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -59,8 +59,8 @@ async function noteMatrixAuthHelp(prompter: WizardPrompter): Promise<void> {
|
|||||||
"Matrix requires a homeserver URL.",
|
"Matrix requires a homeserver URL.",
|
||||||
"Use an access token (recommended) or password login to an existing account.",
|
"Use an access token (recommended) or password login to an existing account.",
|
||||||
"With access token: user ID is fetched automatically.",
|
"With access token: user ID is fetched automatically.",
|
||||||
"Env vars supported: MATRIX_HOMESERVER, MATRIX_USER_ID, MATRIX_ACCESS_TOKEN, MATRIX_PASSWORD.",
|
"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.",
|
"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-js", "channels/matrix-js")}`,
|
`Docs: ${formatDocsLink("/channels/matrix-js", "channels/matrix-js")}`,
|
||||||
].join("\n"),
|
].join("\n"),
|
||||||
"Matrix setup",
|
"Matrix setup",
|
||||||
@@ -70,6 +70,7 @@ async function noteMatrixAuthHelp(prompter: WizardPrompter): Promise<void> {
|
|||||||
async function promptMatrixAllowFrom(params: {
|
async function promptMatrixAllowFrom(params: {
|
||||||
cfg: CoreConfig;
|
cfg: CoreConfig;
|
||||||
prompter: WizardPrompter;
|
prompter: WizardPrompter;
|
||||||
|
accountId?: string;
|
||||||
}): Promise<CoreConfig> {
|
}): Promise<CoreConfig> {
|
||||||
const { cfg, prompter } = params;
|
const { cfg, prompter } = params;
|
||||||
const existingAllowFrom = cfg.channels?.["matrix-js"]?.dm?.allowFrom ?? [];
|
const existingAllowFrom = cfg.channels?.["matrix-js"]?.dm?.allowFrom ?? [];
|
||||||
|
|||||||
@@ -64,4 +64,29 @@ describe("resolveMatrixTargets (users)", () => {
|
|||||||
expect(result?.id).toBe("!two:example.org");
|
expect(result?.id).toBe("!two:example.org");
|
||||||
expect(result?.note).toBe("multiple matches; chose first");
|
expect(result?.note).toBe("multiple matches; chose first");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("reuses directory lookups for duplicate inputs", async () => {
|
||||||
|
vi.mocked(listMatrixDirectoryPeersLive).mockResolvedValue([
|
||||||
|
{ kind: "user", id: "@alice:example.org", name: "Alice" },
|
||||||
|
]);
|
||||||
|
vi.mocked(listMatrixDirectoryGroupsLive).mockResolvedValue([
|
||||||
|
{ kind: "group", id: "!team:example.org", name: "Team", handle: "#team" },
|
||||||
|
]);
|
||||||
|
|
||||||
|
const userResults = await resolveMatrixTargets({
|
||||||
|
cfg: {},
|
||||||
|
inputs: ["Alice", "Alice"],
|
||||||
|
kind: "user",
|
||||||
|
});
|
||||||
|
const groupResults = await resolveMatrixTargets({
|
||||||
|
cfg: {},
|
||||||
|
inputs: ["#team", "#team"],
|
||||||
|
kind: "group",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(userResults.every((entry) => entry.resolved)).toBe(true);
|
||||||
|
expect(groupResults.every((entry) => entry.resolved)).toBe(true);
|
||||||
|
expect(listMatrixDirectoryPeersLive).toHaveBeenCalledTimes(1);
|
||||||
|
expect(listMatrixDirectoryGroupsLive).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -72,6 +72,37 @@ export async function resolveMatrixTargets(params: {
|
|||||||
runtime?: RuntimeEnv;
|
runtime?: RuntimeEnv;
|
||||||
}): Promise<ChannelResolveResult[]> {
|
}): Promise<ChannelResolveResult[]> {
|
||||||
const results: ChannelResolveResult[] = [];
|
const results: ChannelResolveResult[] = [];
|
||||||
|
const userLookupCache = new Map<string, ChannelDirectoryEntry[]>();
|
||||||
|
const groupLookupCache = new Map<string, ChannelDirectoryEntry[]>();
|
||||||
|
|
||||||
|
const readUserMatches = async (query: string): Promise<ChannelDirectoryEntry[]> => {
|
||||||
|
const cached = userLookupCache.get(query);
|
||||||
|
if (cached) {
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
const matches = await listMatrixDirectoryPeersLive({
|
||||||
|
cfg: params.cfg,
|
||||||
|
query,
|
||||||
|
limit: 5,
|
||||||
|
});
|
||||||
|
userLookupCache.set(query, matches);
|
||||||
|
return matches;
|
||||||
|
};
|
||||||
|
|
||||||
|
const readGroupMatches = async (query: string): Promise<ChannelDirectoryEntry[]> => {
|
||||||
|
const cached = groupLookupCache.get(query);
|
||||||
|
if (cached) {
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
const matches = await listMatrixDirectoryGroupsLive({
|
||||||
|
cfg: params.cfg,
|
||||||
|
query,
|
||||||
|
limit: 5,
|
||||||
|
});
|
||||||
|
groupLookupCache.set(query, matches);
|
||||||
|
return matches;
|
||||||
|
};
|
||||||
|
|
||||||
for (const input of params.inputs) {
|
for (const input of params.inputs) {
|
||||||
const trimmed = input.trim();
|
const trimmed = input.trim();
|
||||||
if (!trimmed) {
|
if (!trimmed) {
|
||||||
@@ -84,11 +115,7 @@ export async function resolveMatrixTargets(params: {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const matches = await listMatrixDirectoryPeersLive({
|
const matches = await readUserMatches(trimmed);
|
||||||
cfg: params.cfg,
|
|
||||||
query: trimmed,
|
|
||||||
limit: 5,
|
|
||||||
});
|
|
||||||
const best = pickBestUserMatch(matches, trimmed);
|
const best = pickBestUserMatch(matches, trimmed);
|
||||||
results.push({
|
results.push({
|
||||||
input,
|
input,
|
||||||
@@ -104,11 +131,7 @@ export async function resolveMatrixTargets(params: {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const matches = await listMatrixDirectoryGroupsLive({
|
const matches = await readGroupMatches(trimmed);
|
||||||
cfg: params.cfg,
|
|
||||||
query: trimmed,
|
|
||||||
limit: 5,
|
|
||||||
});
|
|
||||||
const best = pickBestGroupMatch(matches, trimmed);
|
const best = pickBestGroupMatch(matches, trimmed);
|
||||||
results.push({
|
results.push({
|
||||||
input,
|
input,
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ export type CoreConfig = {
|
|||||||
};
|
};
|
||||||
messages?: {
|
messages?: {
|
||||||
ackReaction?: string;
|
ackReaction?: string;
|
||||||
ackReactionScope?: "group-mentions" | "group-all" | "direct" | "all";
|
ackReactionScope?: "group-mentions" | "group-all" | "direct" | "all" | "none" | "off";
|
||||||
};
|
};
|
||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user