refactor: route channel runtime via plugin api
This commit is contained in:
@@ -1,10 +1,16 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
|
||||
import type { CoreConfig } from "./types.js";
|
||||
|
||||
import { matrixPlugin } from "./channel.js";
|
||||
import { setMatrixRuntime } from "./runtime.js";
|
||||
import { createPluginRuntime } from "../../../src/plugins/runtime/index.js";
|
||||
|
||||
describe("matrix directory", () => {
|
||||
beforeEach(() => {
|
||||
setMatrixRuntime(createPluginRuntime());
|
||||
});
|
||||
|
||||
it("lists peers and groups from config", async () => {
|
||||
const cfg = {
|
||||
channels: {
|
||||
|
||||
@@ -15,7 +15,7 @@ import type {
|
||||
RoomTopicEventContent,
|
||||
} from "matrix-js-sdk/lib/@types/state_events.js";
|
||||
|
||||
import { loadConfig } from "clawdbot/plugin-sdk";
|
||||
import { getMatrixRuntime } from "../runtime.js";
|
||||
import type { CoreConfig } from "../types.js";
|
||||
import { getActiveMatrixClient } from "./active-client.js";
|
||||
import {
|
||||
@@ -74,12 +74,14 @@ async function resolveActionClient(opts: MatrixActionClientOpts = {}): Promise<M
|
||||
const shouldShareClient = Boolean(process.env.CLAWDBOT_GATEWAY_PORT);
|
||||
if (shouldShareClient) {
|
||||
const client = await resolveSharedMatrixClient({
|
||||
cfg: loadConfig() as CoreConfig,
|
||||
cfg: getMatrixRuntime().config.loadConfig() as CoreConfig,
|
||||
timeoutMs: opts.timeoutMs,
|
||||
});
|
||||
return { client, stopOnDone: false };
|
||||
}
|
||||
const auth = await resolveMatrixAuth({ cfg: loadConfig() as CoreConfig });
|
||||
const auth = await resolveMatrixAuth({
|
||||
cfg: getMatrixRuntime().config.loadConfig() as CoreConfig,
|
||||
});
|
||||
const client = await createMatrixClient({
|
||||
homeserver: auth.homeserver,
|
||||
userId: auth.userId,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ClientEvent, type MatrixClient, SyncState } from "matrix-js-sdk";
|
||||
|
||||
import { loadConfig } from "clawdbot/plugin-sdk";
|
||||
import type { CoreConfig } from "../types.js";
|
||||
import { getMatrixRuntime } from "../runtime.js";
|
||||
|
||||
export type MatrixResolvedConfig = {
|
||||
homeserver: string;
|
||||
@@ -46,7 +46,7 @@ function clean(value?: string): string {
|
||||
}
|
||||
|
||||
export function resolveMatrixConfig(
|
||||
cfg: CoreConfig = loadConfig() as CoreConfig,
|
||||
cfg: CoreConfig = getMatrixRuntime().config.loadConfig() as CoreConfig,
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
): MatrixResolvedConfig {
|
||||
const matrix = cfg.channels?.matrix ?? {};
|
||||
@@ -75,7 +75,7 @@ export async function resolveMatrixAuth(params?: {
|
||||
cfg?: CoreConfig;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}): Promise<MatrixAuth> {
|
||||
const cfg = params?.cfg ?? (loadConfig() as CoreConfig);
|
||||
const cfg = params?.cfg ?? (getMatrixRuntime().config.loadConfig() as CoreConfig);
|
||||
const env = params?.env ?? process.env;
|
||||
const resolved = resolveMatrixConfig(cfg, env);
|
||||
if (!resolved.homeserver) {
|
||||
|
||||
@@ -2,7 +2,7 @@ import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
import { resolveStateDir } from "clawdbot/plugin-sdk";
|
||||
import { getMatrixRuntime } from "../runtime.js";
|
||||
|
||||
export type MatrixStoredCredentials = {
|
||||
homeserver: string;
|
||||
@@ -16,9 +16,11 @@ const CREDENTIALS_FILENAME = "credentials.json";
|
||||
|
||||
export function resolveMatrixCredentialsDir(
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
stateDir: string = resolveStateDir(env, os.homedir),
|
||||
stateDir?: string,
|
||||
): string {
|
||||
return path.join(stateDir, "credentials", "matrix");
|
||||
const resolvedStateDir =
|
||||
stateDir ?? getMatrixRuntime().state.resolveStateDir(env, os.homedir);
|
||||
return path.join(resolvedStateDir, "credentials", "matrix");
|
||||
}
|
||||
|
||||
export function resolveMatrixCredentialsPath(env: NodeJS.ProcessEnv = process.env): string {
|
||||
|
||||
@@ -3,7 +3,8 @@ import path from "node:path";
|
||||
import { createRequire } from "node:module";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import { runCommandWithTimeout, type RuntimeEnv } from "clawdbot/plugin-sdk";
|
||||
import type { RuntimeEnv } from "clawdbot/plugin-sdk";
|
||||
import { getMatrixRuntime } from "../runtime.js";
|
||||
|
||||
const MATRIX_SDK_PACKAGE = "matrix-js-sdk";
|
||||
|
||||
@@ -40,7 +41,7 @@ export async function ensureMatrixSdkInstalled(params: {
|
||||
? ["pnpm", "install"]
|
||||
: ["npm", "install", "--omit=dev", "--silent"];
|
||||
params.runtime.log?.(`matrix: installing dependencies via ${command[0]} (${root})…`);
|
||||
const result = await runCommandWithTimeout(command, {
|
||||
const result = await getMatrixRuntime().system.runCommandWithTimeout(command, {
|
||||
cwd: root,
|
||||
timeoutMs: 300_000,
|
||||
env: { COREPACK_ENABLE_DOWNLOAD_PROMPT: "0" },
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import type { MatrixClient, MatrixEvent, RoomMember } from "matrix-js-sdk";
|
||||
import { RoomMemberEvent } from "matrix-js-sdk";
|
||||
|
||||
import { danger, logVerbose, type RuntimeEnv } from "clawdbot/plugin-sdk";
|
||||
import type { RuntimeEnv } from "clawdbot/plugin-sdk";
|
||||
import type { CoreConfig } from "../../types.js";
|
||||
import { getMatrixRuntime } from "../../runtime.js";
|
||||
|
||||
export function registerMatrixAutoJoin(params: {
|
||||
client: MatrixClient;
|
||||
@@ -10,6 +11,11 @@ export function registerMatrixAutoJoin(params: {
|
||||
runtime: RuntimeEnv;
|
||||
}) {
|
||||
const { client, cfg, runtime } = params;
|
||||
const core = getMatrixRuntime();
|
||||
const logVerbose = (message: string) => {
|
||||
if (!core.logging.shouldLogVerbose()) return;
|
||||
runtime.log?.(message);
|
||||
};
|
||||
const autoJoin = cfg.channels?.matrix?.autoJoin ?? "always";
|
||||
const autoJoinAllowlist = cfg.channels?.matrix?.autoJoinAllowlist ?? [];
|
||||
|
||||
@@ -36,7 +42,7 @@ export function registerMatrixAutoJoin(params: {
|
||||
await client.joinRoom(roomId);
|
||||
logVerbose(`matrix: joined room ${roomId}`);
|
||||
} catch (err) {
|
||||
runtime.error?.(danger(`matrix: failed to join room ${roomId}: ${String(err)}`));
|
||||
runtime.error?.(`matrix: failed to join room ${roomId}: ${String(err)}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,34 +3,9 @@ import { EventType, RelationType, RoomEvent } from "matrix-js-sdk";
|
||||
import type { RoomMessageEventContent } from "matrix-js-sdk/lib/@types/events.js";
|
||||
|
||||
import {
|
||||
buildMentionRegexes,
|
||||
chunkMarkdownText,
|
||||
createReplyDispatcherWithTyping,
|
||||
danger,
|
||||
dispatchReplyFromConfig,
|
||||
enqueueSystemEvent,
|
||||
finalizeInboundContext,
|
||||
formatAgentEnvelope,
|
||||
formatAllowlistMatchMeta,
|
||||
getChildLogger,
|
||||
hasControlCommand,
|
||||
loadConfig,
|
||||
logVerbose,
|
||||
mergeAllowlist,
|
||||
matchesMentionPatterns,
|
||||
readChannelAllowFromStore,
|
||||
recordSessionMetaFromInbound,
|
||||
resolveAgentRoute,
|
||||
resolveCommandAuthorizedFromAuthorizers,
|
||||
resolveEffectiveMessagesConfig,
|
||||
resolveHumanDelayConfig,
|
||||
resolveStorePath,
|
||||
resolveTextChunkLimit,
|
||||
shouldHandleTextCommands,
|
||||
shouldLogVerbose,
|
||||
summarizeMapping,
|
||||
updateLastRoute,
|
||||
upsertChannelPairingRequest,
|
||||
type ReplyPayload,
|
||||
type RuntimeEnv,
|
||||
} from "clawdbot/plugin-sdk";
|
||||
@@ -61,6 +36,7 @@ import { deliverMatrixReplies } from "./replies.js";
|
||||
import { resolveMatrixRoomConfig } from "./rooms.js";
|
||||
import { resolveMatrixThreadRootId, resolveMatrixThreadTarget } from "./threads.js";
|
||||
import { resolveMatrixTargets } from "../../resolve-targets.js";
|
||||
import { getMatrixRuntime } from "../../runtime.js";
|
||||
|
||||
export type MonitorMatrixOpts = {
|
||||
runtime?: RuntimeEnv;
|
||||
@@ -76,7 +52,8 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi
|
||||
if (isBunRuntime()) {
|
||||
throw new Error("Matrix provider requires Node (bun runtime not supported)");
|
||||
}
|
||||
let cfg = loadConfig() as CoreConfig;
|
||||
const core = getMatrixRuntime();
|
||||
let cfg = core.config.loadConfig() as CoreConfig;
|
||||
if (cfg.channels?.matrix?.enabled === false) return;
|
||||
|
||||
const runtime: RuntimeEnv = opts.runtime ?? {
|
||||
@@ -207,8 +184,13 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi
|
||||
});
|
||||
setActiveMatrixClient(client);
|
||||
|
||||
const mentionRegexes = buildMentionRegexes(cfg);
|
||||
const logger = getChildLogger({ module: "matrix-auto-reply" });
|
||||
const mentionRegexes = core.channel.mentions.buildMentionRegexes(cfg);
|
||||
const logger = core.logging.getChildLogger({ module: "matrix-auto-reply" });
|
||||
const logVerboseMessage = (message: string) => {
|
||||
if (core.logging.shouldLogVerbose()) {
|
||||
logger.debug(message);
|
||||
}
|
||||
};
|
||||
const allowlistOnly = cfg.channels?.matrix?.allowlistOnly === true;
|
||||
const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy;
|
||||
const groupPolicyRaw = cfg.channels?.matrix?.groupPolicy ?? defaultGroupPolicy ?? "allowlist";
|
||||
@@ -220,7 +202,7 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi
|
||||
const dmPolicyRaw = dmConfig?.policy ?? "pairing";
|
||||
const dmPolicy = allowlistOnly && dmPolicyRaw !== "disabled" ? "allowlist" : dmPolicyRaw;
|
||||
const allowFrom = dmConfig?.allowFrom ?? [];
|
||||
const textLimit = resolveTextChunkLimit(cfg, "matrix");
|
||||
const textLimit = core.channel.text.resolveTextChunkLimit(cfg, "matrix");
|
||||
const mediaMaxMb = opts.mediaMaxMb ?? cfg.channels?.matrix?.mediaMaxMb ?? DEFAULT_MEDIA_MAX_MB;
|
||||
const mediaMaxBytes = Math.max(1, mediaMaxMb) * 1024 * 1024;
|
||||
const startupMs = Date.now();
|
||||
@@ -306,22 +288,22 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi
|
||||
}`;
|
||||
|
||||
if (roomConfigInfo.config && !roomConfigInfo.allowed) {
|
||||
logVerbose(`matrix: room disabled room=${roomId} (${roomMatchMeta})`);
|
||||
logVerboseMessage(`matrix: room disabled room=${roomId} (${roomMatchMeta})`);
|
||||
return;
|
||||
}
|
||||
if (groupPolicy === "allowlist") {
|
||||
if (!roomConfigInfo.allowlistConfigured) {
|
||||
logVerbose(`matrix: drop room message (no allowlist, ${roomMatchMeta})`);
|
||||
logVerboseMessage(`matrix: drop room message (no allowlist, ${roomMatchMeta})`);
|
||||
return;
|
||||
}
|
||||
if (!roomConfigInfo.config) {
|
||||
logVerbose(`matrix: drop room message (not in allowlist, ${roomMatchMeta})`);
|
||||
logVerboseMessage(`matrix: drop room message (not in allowlist, ${roomMatchMeta})`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const senderName = room.getMember(senderId)?.name ?? senderId;
|
||||
const storeAllowFrom = await readChannelAllowFromStore("matrix").catch(() => []);
|
||||
const storeAllowFrom = await core.channel.pairing.readAllowFromStore("matrix").catch(() => []);
|
||||
const effectiveAllowFrom = normalizeAllowListLower([...allowFrom, ...storeAllowFrom]);
|
||||
|
||||
if (isDirectMessage) {
|
||||
@@ -335,13 +317,13 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi
|
||||
const allowMatchMeta = formatAllowlistMatchMeta(allowMatch);
|
||||
if (!allowMatch.allowed) {
|
||||
if (dmPolicy === "pairing") {
|
||||
const { code, created } = await upsertChannelPairingRequest({
|
||||
const { code, created } = await core.channel.pairing.upsertPairingRequest({
|
||||
channel: "matrix",
|
||||
id: senderId,
|
||||
meta: { name: senderName },
|
||||
});
|
||||
if (created) {
|
||||
logVerbose(
|
||||
logVerboseMessage(
|
||||
`matrix pairing request sender=${senderId} name=${senderName ?? "unknown"} (${allowMatchMeta})`,
|
||||
);
|
||||
try {
|
||||
@@ -358,12 +340,12 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi
|
||||
{ client },
|
||||
);
|
||||
} catch (err) {
|
||||
logVerbose(`matrix pairing reply failed for ${senderId}: ${String(err)}`);
|
||||
logVerboseMessage(`matrix pairing reply failed for ${senderId}: ${String(err)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (dmPolicy !== "pairing") {
|
||||
logVerbose(
|
||||
logVerboseMessage(
|
||||
`matrix: blocked dm sender ${senderId} (dmPolicy=${dmPolicy}, ${allowMatchMeta})`,
|
||||
);
|
||||
}
|
||||
@@ -379,7 +361,7 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi
|
||||
userName: senderName,
|
||||
});
|
||||
if (!userMatch.allowed) {
|
||||
logVerbose(
|
||||
logVerboseMessage(
|
||||
`matrix: blocked sender ${senderId} (room users allowlist, ${roomMatchMeta}, ${formatAllowlistMatchMeta(
|
||||
userMatch,
|
||||
)})`,
|
||||
@@ -388,7 +370,7 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi
|
||||
}
|
||||
}
|
||||
if (isRoom) {
|
||||
logVerbose(`matrix: allow room ${roomId} (${roomMatchMeta})`);
|
||||
logVerboseMessage(`matrix: allow room ${roomId} (${roomMatchMeta})`);
|
||||
}
|
||||
|
||||
const rawBody = content.body.trim();
|
||||
@@ -416,7 +398,7 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi
|
||||
maxBytes: mediaMaxBytes,
|
||||
});
|
||||
} catch (err) {
|
||||
logVerbose(`matrix: media download failed: ${String(err)}`);
|
||||
logVerboseMessage(`matrix: media download failed: ${String(err)}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -429,7 +411,7 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi
|
||||
text: bodyText,
|
||||
mentionRegexes,
|
||||
});
|
||||
const allowTextCommands = shouldHandleTextCommands({
|
||||
const allowTextCommands = core.channel.commands.shouldHandleTextCommands({
|
||||
cfg,
|
||||
surface: "matrix",
|
||||
});
|
||||
@@ -439,14 +421,19 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi
|
||||
userId: senderId,
|
||||
userName: senderName,
|
||||
});
|
||||
const commandAuthorized = resolveCommandAuthorizedFromAuthorizers({
|
||||
const commandAuthorized = core.channel.commands.resolveCommandAuthorizedFromAuthorizers({
|
||||
useAccessGroups,
|
||||
authorizers: [
|
||||
{ configured: effectiveAllowFrom.length > 0, allowed: senderAllowedForCommands },
|
||||
],
|
||||
});
|
||||
if (isRoom && allowTextCommands && hasControlCommand(bodyText, cfg) && !commandAuthorized) {
|
||||
logVerbose(`matrix: drop control command from unauthorized sender ${senderId}`);
|
||||
if (
|
||||
isRoom &&
|
||||
allowTextCommands &&
|
||||
core.channel.text.hasControlCommand(bodyText, cfg) &&
|
||||
!commandAuthorized
|
||||
) {
|
||||
logVerboseMessage(`matrix: drop control command from unauthorized sender ${senderId}`);
|
||||
return;
|
||||
}
|
||||
const shouldRequireMention = isRoom
|
||||
@@ -465,7 +452,7 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi
|
||||
!wasMentioned &&
|
||||
!hasExplicitMention &&
|
||||
commandAuthorized &&
|
||||
hasControlCommand(bodyText);
|
||||
core.channel.text.hasControlCommand(bodyText);
|
||||
if (isRoom && shouldRequireMention && !wasMentioned && !shouldBypassMention) {
|
||||
logger.info({ roomId, reason: "no-mention" }, "skipping room message");
|
||||
return;
|
||||
@@ -482,14 +469,14 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi
|
||||
|
||||
const envelopeFrom = isDirectMessage ? senderName : (roomName ?? roomId);
|
||||
const textWithId = `${bodyText}\n[matrix event id: ${messageId} room: ${roomId}]`;
|
||||
const body = formatAgentEnvelope({
|
||||
const body = core.channel.reply.formatAgentEnvelope({
|
||||
channel: "Matrix",
|
||||
from: envelopeFrom,
|
||||
timestamp: event.getTs() ?? undefined,
|
||||
body: textWithId,
|
||||
});
|
||||
|
||||
const route = resolveAgentRoute({
|
||||
const route = core.channel.routing.resolveAgentRoute({
|
||||
cfg,
|
||||
channel: "matrix",
|
||||
peer: {
|
||||
@@ -499,7 +486,7 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi
|
||||
});
|
||||
|
||||
const groupSystemPrompt = roomConfigInfo.config?.systemPrompt?.trim() || undefined;
|
||||
const ctxPayload = finalizeInboundContext({
|
||||
const ctxPayload = core.channel.reply.finalizeInboundContext({
|
||||
Body: body,
|
||||
RawBody: bodyText,
|
||||
CommandBody: bodyText,
|
||||
@@ -531,10 +518,10 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi
|
||||
OriginatingTo: `room:${roomId}`,
|
||||
});
|
||||
|
||||
const storePath = resolveStorePath(cfg.session?.store, {
|
||||
const storePath = core.channel.session.resolveStorePath(cfg.session?.store, {
|
||||
agentId: route.agentId,
|
||||
});
|
||||
void recordSessionMetaFromInbound({
|
||||
void core.channel.session.recordSessionMetaFromInbound({
|
||||
storePath,
|
||||
sessionKey: ctxPayload.SessionKey ?? route.sessionKey,
|
||||
ctx: ctxPayload,
|
||||
@@ -546,7 +533,7 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi
|
||||
});
|
||||
|
||||
if (isDirectMessage) {
|
||||
await updateLastRoute({
|
||||
await core.channel.session.updateLastRoute({
|
||||
storePath,
|
||||
sessionKey: route.mainSessionKey,
|
||||
channel: "matrix",
|
||||
@@ -556,10 +543,8 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi
|
||||
});
|
||||
}
|
||||
|
||||
if (shouldLogVerbose()) {
|
||||
const preview = bodyText.slice(0, 200).replace(/\n/g, "\\n");
|
||||
logVerbose(`matrix inbound: room=${roomId} from=${senderId} preview="${preview}"`);
|
||||
}
|
||||
const preview = bodyText.slice(0, 200).replace(/\n/g, "\\n");
|
||||
logVerboseMessage(`matrix inbound: room=${roomId} from=${senderId} preview="${preview}"`);
|
||||
|
||||
const ackReaction = (cfg.messages?.ackReaction ?? "").trim();
|
||||
const ackScope = cfg.messages?.ackReactionScope ?? "group-mentions";
|
||||
@@ -577,20 +562,20 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi
|
||||
};
|
||||
if (shouldAckReaction() && messageId) {
|
||||
reactMatrixMessage(roomId, messageId, ackReaction, client).catch((err) => {
|
||||
logVerbose(`matrix react failed for room ${roomId}: ${String(err)}`);
|
||||
logVerboseMessage(`matrix react failed for room ${roomId}: ${String(err)}`);
|
||||
});
|
||||
}
|
||||
|
||||
const replyTarget = ctxPayload.To;
|
||||
if (!replyTarget) {
|
||||
runtime.error?.(danger("matrix: missing reply target"));
|
||||
runtime.error?.("matrix: missing reply target");
|
||||
return;
|
||||
}
|
||||
|
||||
let didSendReply = false;
|
||||
const { dispatcher, replyOptions, markDispatchIdle } = createReplyDispatcherWithTyping({
|
||||
responsePrefix: resolveEffectiveMessagesConfig(cfg, route.agentId).responsePrefix,
|
||||
humanDelay: resolveHumanDelayConfig(cfg, route.agentId),
|
||||
const { dispatcher, replyOptions, markDispatchIdle } = core.channel.reply.createReplyDispatcherWithTyping({
|
||||
responsePrefix: core.channel.reply.resolveEffectiveMessagesConfig(cfg, route.agentId).responsePrefix,
|
||||
humanDelay: core.channel.reply.resolveHumanDelayConfig(cfg, route.agentId),
|
||||
deliver: async (payload) => {
|
||||
await deliverMatrixReplies({
|
||||
replies: [payload],
|
||||
@@ -604,13 +589,13 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi
|
||||
didSendReply = true;
|
||||
},
|
||||
onError: (err, info) => {
|
||||
runtime.error?.(danger(`matrix ${info.kind} reply failed: ${String(err)}`));
|
||||
runtime.error?.(`matrix ${info.kind} reply failed: ${String(err)}`);
|
||||
},
|
||||
onReplyStart: () => sendTypingMatrix(roomId, true, undefined, client).catch(() => {}),
|
||||
onIdle: () => sendTypingMatrix(roomId, false, undefined, client).catch(() => {}),
|
||||
});
|
||||
|
||||
const { queuedFinal, counts } = await dispatchReplyFromConfig({
|
||||
const { queuedFinal, counts } = await core.channel.reply.dispatchReplyFromConfig({
|
||||
ctx: ctxPayload,
|
||||
cfg,
|
||||
dispatcher,
|
||||
@@ -622,19 +607,19 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi
|
||||
markDispatchIdle();
|
||||
if (!queuedFinal) return;
|
||||
didSendReply = true;
|
||||
if (shouldLogVerbose()) {
|
||||
const finalCount = counts.final;
|
||||
logVerbose(`matrix: delivered ${finalCount} reply${finalCount === 1 ? "" : "ies"} to ${replyTarget}`);
|
||||
}
|
||||
const finalCount = counts.final;
|
||||
logVerboseMessage(
|
||||
`matrix: delivered ${finalCount} reply${finalCount === 1 ? "" : "ies"} to ${replyTarget}`,
|
||||
);
|
||||
if (didSendReply) {
|
||||
const preview = bodyText.replace(/\s+/g, " ").slice(0, 160);
|
||||
enqueueSystemEvent(`Matrix message from ${senderName}: ${preview}`, {
|
||||
core.system.enqueueSystemEvent(`Matrix message from ${senderName}: ${preview}`, {
|
||||
sessionKey: route.sessionKey,
|
||||
contextKey: `matrix:message:${roomId}:${messageId || "unknown"}`,
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
runtime.error?.(danger(`matrix handler failed: ${String(err)}`));
|
||||
runtime.error?.(`matrix handler failed: ${String(err)}`);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { MatrixClient } from "matrix-js-sdk";
|
||||
|
||||
import { saveMediaBuffer } from "clawdbot/plugin-sdk";
|
||||
import { getMatrixRuntime } from "../../runtime.js";
|
||||
|
||||
async function fetchMatrixMediaBuffer(params: {
|
||||
client: MatrixClient;
|
||||
@@ -49,7 +49,12 @@ export async function downloadMatrixMedia(params: {
|
||||
});
|
||||
if (!fetched) return null;
|
||||
const headerType = fetched.headerType ?? params.contentType ?? undefined;
|
||||
const saved = await saveMediaBuffer(fetched.buffer, headerType, "inbound", params.maxBytes);
|
||||
const saved = await getMatrixRuntime().channel.media.saveMediaBuffer(
|
||||
fetched.buffer,
|
||||
headerType,
|
||||
"inbound",
|
||||
params.maxBytes,
|
||||
);
|
||||
return {
|
||||
path: saved.path,
|
||||
contentType: saved.contentType,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { RoomMessageEventContent } from "matrix-js-sdk/lib/@types/events.js";
|
||||
|
||||
import { matchesMentionPatterns } from "clawdbot/plugin-sdk";
|
||||
import { getMatrixRuntime } from "../../runtime.js";
|
||||
|
||||
export function resolveMentions(params: {
|
||||
content: RoomMessageEventContent;
|
||||
@@ -17,6 +17,9 @@ export function resolveMentions(params: {
|
||||
const wasMentioned =
|
||||
Boolean(mentions?.room) ||
|
||||
(params.userId ? mentionedUsers.has(params.userId) : false) ||
|
||||
matchesMentionPatterns(params.text ?? "", params.mentionRegexes);
|
||||
getMatrixRuntime().channel.mentions.matchesMentionPatterns(
|
||||
params.text ?? "",
|
||||
params.mentionRegexes,
|
||||
);
|
||||
return { wasMentioned, hasExplicitMention: Boolean(mentions) };
|
||||
}
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
import type { MatrixClient } from "matrix-js-sdk";
|
||||
|
||||
import {
|
||||
chunkMarkdownText,
|
||||
danger,
|
||||
logVerbose,
|
||||
type ReplyPayload,
|
||||
type RuntimeEnv,
|
||||
} from "clawdbot/plugin-sdk";
|
||||
import type { ReplyPayload, RuntimeEnv } from "clawdbot/plugin-sdk";
|
||||
import { sendMessageMatrix } from "../send.js";
|
||||
import { getMatrixRuntime } from "../../runtime.js";
|
||||
|
||||
export async function deliverMatrixReplies(params: {
|
||||
replies: ReplyPayload[];
|
||||
@@ -18,6 +13,12 @@ export async function deliverMatrixReplies(params: {
|
||||
replyToMode: "off" | "first" | "all";
|
||||
threadId?: string;
|
||||
}): Promise<void> {
|
||||
const core = getMatrixRuntime();
|
||||
const logVerbose = (message: string) => {
|
||||
if (core.logging.shouldLogVerbose()) {
|
||||
params.runtime.log?.(message);
|
||||
}
|
||||
};
|
||||
const chunkLimit = Math.min(params.textLimit, 4000);
|
||||
let hasReplied = false;
|
||||
for (const reply of params.replies) {
|
||||
@@ -27,7 +28,7 @@ export async function deliverMatrixReplies(params: {
|
||||
logVerbose("matrix reply has audioAsVoice without media/text; skipping");
|
||||
continue;
|
||||
}
|
||||
params.runtime.error?.(danger("matrix reply missing text/media"));
|
||||
params.runtime.error?.("matrix reply missing text/media");
|
||||
continue;
|
||||
}
|
||||
const replyToIdRaw = reply.replyToId?.trim();
|
||||
@@ -42,7 +43,7 @@ export async function deliverMatrixReplies(params: {
|
||||
Boolean(id) && (params.replyToMode === "all" || !hasReplied);
|
||||
|
||||
if (mediaList.length === 0) {
|
||||
for (const chunk of chunkMarkdownText(reply.text ?? "", chunkLimit)) {
|
||||
for (const chunk of core.channel.text.chunkMarkdownText(reply.text ?? "", chunkLimit)) {
|
||||
const trimmed = chunk.trim();
|
||||
if (!trimmed) continue;
|
||||
await sendMessageMatrix(params.roomId, trimmed, {
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import type { PluginRuntime } from "clawdbot/plugin-sdk";
|
||||
import { setMatrixRuntime } from "../runtime.js";
|
||||
|
||||
vi.mock("matrix-js-sdk", () => ({
|
||||
EventType: {
|
||||
Direct: "m.direct",
|
||||
@@ -18,21 +21,33 @@ vi.mock("matrix-js-sdk", () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("clawdbot/plugin-sdk", () => ({
|
||||
loadConfig: () => ({}),
|
||||
resolveTextChunkLimit: () => 4000,
|
||||
chunkMarkdownText: (text: string) => (text ? [text] : []),
|
||||
loadWebMedia: vi.fn().mockResolvedValue({
|
||||
buffer: Buffer.from("media"),
|
||||
fileName: "photo.png",
|
||||
contentType: "image/png",
|
||||
kind: "image",
|
||||
}),
|
||||
mediaKindFromMime: () => "image",
|
||||
isVoiceCompatibleAudio: () => false,
|
||||
getImageMetadata: vi.fn().mockResolvedValue(null),
|
||||
resizeToJpeg: vi.fn(),
|
||||
}));
|
||||
const loadWebMediaMock = vi.fn().mockResolvedValue({
|
||||
buffer: Buffer.from("media"),
|
||||
fileName: "photo.png",
|
||||
contentType: "image/png",
|
||||
kind: "image",
|
||||
});
|
||||
const getImageMetadataMock = vi.fn().mockResolvedValue(null);
|
||||
const resizeToJpegMock = vi.fn();
|
||||
|
||||
const runtimeStub = {
|
||||
config: {
|
||||
loadConfig: () => ({}),
|
||||
},
|
||||
media: {
|
||||
loadWebMedia: (...args: unknown[]) => loadWebMediaMock(...args),
|
||||
mediaKindFromMime: () => "image",
|
||||
isVoiceCompatibleAudio: () => false,
|
||||
getImageMetadata: (...args: unknown[]) => getImageMetadataMock(...args),
|
||||
resizeToJpeg: (...args: unknown[]) => resizeToJpegMock(...args),
|
||||
},
|
||||
channel: {
|
||||
text: {
|
||||
resolveTextChunkLimit: () => 4000,
|
||||
chunkMarkdownText: (text: string) => (text ? [text] : []),
|
||||
},
|
||||
},
|
||||
} as unknown as PluginRuntime;
|
||||
|
||||
let sendMessageMatrix: typeof import("./send.js").sendMessageMatrix;
|
||||
|
||||
@@ -50,11 +65,13 @@ const makeClient = () => {
|
||||
|
||||
describe("sendMessageMatrix media", () => {
|
||||
beforeAll(async () => {
|
||||
setMatrixRuntime(runtimeStub);
|
||||
({ sendMessageMatrix } = await import("./send.js"));
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
setMatrixRuntime(runtimeStub);
|
||||
});
|
||||
|
||||
it("uploads media with url payloads", async () => {
|
||||
|
||||
@@ -5,17 +5,8 @@ import type {
|
||||
ReactionEventContent,
|
||||
} from "matrix-js-sdk/lib/@types/events.js";
|
||||
|
||||
import {
|
||||
chunkMarkdownText,
|
||||
getImageMetadata,
|
||||
isVoiceCompatibleAudio,
|
||||
loadConfig,
|
||||
loadWebMedia,
|
||||
mediaKindFromMime,
|
||||
type PollInput,
|
||||
resolveTextChunkLimit,
|
||||
resizeToJpeg,
|
||||
} from "clawdbot/plugin-sdk";
|
||||
import type { PollInput } from "clawdbot/plugin-sdk";
|
||||
import { getMatrixRuntime } from "../runtime.js";
|
||||
import { getActiveMatrixClient } from "./active-client.js";
|
||||
import {
|
||||
createMatrixClient,
|
||||
@@ -29,6 +20,7 @@ import { buildPollStartContent, M_POLL_START } from "./poll-types.js";
|
||||
import type { CoreConfig } from "../types.js";
|
||||
|
||||
const MATRIX_TEXT_LIMIT = 4000;
|
||||
const getCore = () => getMatrixRuntime();
|
||||
|
||||
type MatrixDirectAccountData = AccountDataEvents[EventType.Direct];
|
||||
|
||||
@@ -65,7 +57,7 @@ function ensureNodeRuntime() {
|
||||
}
|
||||
|
||||
function resolveMediaMaxBytes(): number | undefined {
|
||||
const cfg = loadConfig() as CoreConfig;
|
||||
const cfg = getCore().config.loadConfig() as CoreConfig;
|
||||
if (typeof cfg.channels?.matrix?.mediaMaxMb === "number") {
|
||||
return cfg.channels.matrix.mediaMaxMb * 1024 * 1024;
|
||||
}
|
||||
@@ -224,7 +216,7 @@ function resolveMatrixMsgType(
|
||||
contentType?: string,
|
||||
fileName?: string,
|
||||
): MsgType.Image | MsgType.Audio | MsgType.Video | MsgType.File {
|
||||
const kind = mediaKindFromMime(contentType ?? "");
|
||||
const kind = getCore().media.mediaKindFromMime(contentType ?? "");
|
||||
switch (kind) {
|
||||
case "image":
|
||||
return MsgType.Image;
|
||||
@@ -243,7 +235,7 @@ function resolveMatrixVoiceDecision(opts: {
|
||||
fileName?: string;
|
||||
}): { useVoice: boolean } {
|
||||
if (!opts.wantsVoice) return { useVoice: false };
|
||||
if (isVoiceCompatibleAudio({ contentType: opts.contentType, fileName: opts.fileName })) {
|
||||
if (getCore().media.isVoiceCompatibleAudio({ contentType: opts.contentType, fileName: opts.fileName })) {
|
||||
return { useVoice: true };
|
||||
}
|
||||
return { useVoice: false };
|
||||
@@ -256,19 +248,19 @@ async function prepareImageInfo(params: {
|
||||
buffer: Buffer;
|
||||
client: MatrixClient;
|
||||
}): Promise<MatrixImageInfo | undefined> {
|
||||
const meta = await getImageMetadata(params.buffer).catch(() => null);
|
||||
const meta = await getCore().media.getImageMetadata(params.buffer).catch(() => null);
|
||||
if (!meta) return undefined;
|
||||
const imageInfo: MatrixImageInfo = { w: meta.width, h: meta.height };
|
||||
const maxDim = Math.max(meta.width, meta.height);
|
||||
if (maxDim > THUMBNAIL_MAX_SIDE) {
|
||||
try {
|
||||
const thumbBuffer = await resizeToJpeg({
|
||||
const thumbBuffer = await getCore().media.resizeToJpeg({
|
||||
buffer: params.buffer,
|
||||
maxSide: THUMBNAIL_MAX_SIDE,
|
||||
quality: THUMBNAIL_QUALITY,
|
||||
withoutEnlargement: true,
|
||||
});
|
||||
const thumbMeta = await getImageMetadata(thumbBuffer).catch(() => null);
|
||||
const thumbMeta = await getCore().media.getImageMetadata(thumbBuffer).catch(() => null);
|
||||
const thumbUri = await params.client.uploadContent(thumbBuffer as MatrixUploadContent, {
|
||||
type: "image/jpeg",
|
||||
name: "thumbnail.jpg",
|
||||
@@ -352,10 +344,10 @@ export async function sendMessageMatrix(
|
||||
});
|
||||
try {
|
||||
const roomId = await resolveMatrixRoomId(client, to);
|
||||
const cfg = loadConfig();
|
||||
const textLimit = resolveTextChunkLimit(cfg, "matrix");
|
||||
const cfg = getCore().config.loadConfig();
|
||||
const textLimit = getCore().channel.text.resolveTextChunkLimit(cfg, "matrix");
|
||||
const chunkLimit = Math.min(textLimit, MATRIX_TEXT_LIMIT);
|
||||
const chunks = chunkMarkdownText(trimmedMessage, chunkLimit);
|
||||
const chunks = getCore().channel.text.chunkMarkdownText(trimmedMessage, chunkLimit);
|
||||
const threadId = normalizeThreadId(opts.threadId);
|
||||
const relation = threadId ? undefined : buildReplyRelation(opts.replyToId);
|
||||
const sendContent = (content: RoomMessageEventContent) =>
|
||||
@@ -364,7 +356,7 @@ export async function sendMessageMatrix(
|
||||
let lastMessageId = "";
|
||||
if (opts.mediaUrl) {
|
||||
const maxBytes = resolveMediaMaxBytes();
|
||||
const media = await loadWebMedia(opts.mediaUrl, maxBytes);
|
||||
const media = await getCore().media.loadWebMedia(opts.mediaUrl, maxBytes);
|
||||
const contentUri = await uploadFile(client, media.buffer, {
|
||||
contentType: media.contentType,
|
||||
filename: media.fileName,
|
||||
|
||||
Reference in New Issue
Block a user