Matrix: honor scoped media roots

This commit is contained in:
Gustavo Madeira Santana
2026-03-12 04:04:53 +00:00
parent 8af9b30ae7
commit 26eea79f96
14 changed files with 105 additions and 5 deletions

View File

@@ -60,6 +60,7 @@ describe("matrixMessageActions account propagation", () => {
accountId: "ops",
}),
expect.any(Object),
{ mediaLocalRoots: undefined },
);
});
@@ -80,6 +81,7 @@ describe("matrixMessageActions account propagation", () => {
accountId: "ops",
}),
expect.any(Object),
{ mediaLocalRoots: undefined },
);
});
@@ -103,6 +105,7 @@ describe("matrixMessageActions account propagation", () => {
avatarUrl: "mxc://example/avatar",
}),
expect.any(Object),
{ mediaLocalRoots: undefined },
);
});
@@ -124,6 +127,32 @@ describe("matrixMessageActions account propagation", () => {
avatarPath: "/tmp/avatar.jpg",
}),
expect.any(Object),
{ mediaLocalRoots: undefined },
);
});
it("forwards mediaLocalRoots for media sends", async () => {
await matrixMessageActions.handleAction?.(
createContext({
action: "send",
accountId: "ops",
mediaLocalRoots: ["/tmp/openclaw-matrix-test"],
params: {
to: "room:!room:example",
message: "hello",
media: "file:///tmp/photo.png",
},
}),
);
expect(mocks.handleMatrixAction).toHaveBeenCalledWith(
expect.objectContaining({
action: "sendMessage",
accountId: "ops",
mediaUrl: "file:///tmp/photo.png",
}),
expect.any(Object),
{ mediaLocalRoots: ["/tmp/openclaw-matrix-test"] },
);
});
});

View File

@@ -93,7 +93,7 @@ export const matrixMessageActions: ChannelMessageActionAdapter = {
return { to };
},
handleAction: async (ctx: ChannelMessageActionContext) => {
const { action, params, cfg, accountId } = ctx;
const { action, params, cfg, accountId, mediaLocalRoots } = ctx;
const dispatch = async (actionParams: Record<string, unknown>) =>
await handleMatrixAction(
{
@@ -101,6 +101,7 @@ export const matrixMessageActions: ChannelMessageActionAdapter = {
...(accountId ? { accountId } : {}),
},
cfg as CoreConfig,
{ mediaLocalRoots },
);
const resolveRoomId = () =>
readStringParam(params, "roomId") ??

View File

@@ -26,6 +26,7 @@ export async function sendMatrixMessage(
return await sendMessageMatrix(to, content, {
cfg: opts.cfg,
mediaUrl: opts.mediaUrl,
mediaLocalRoots: opts.mediaLocalRoots,
replyToId: opts.replyToId,
threadId: opts.threadId,
accountId: opts.accountId ?? undefined,

View File

@@ -26,7 +26,10 @@ export async function updateMatrixOwnProfile(
avatarPath: avatarPath || undefined,
loadAvatarFromUrl: async (url, maxBytes) => await runtime.media.loadWebMedia(url, maxBytes),
loadAvatarFromPath: async (path, maxBytes) =>
await runtime.media.loadWebMedia(path, maxBytes),
await runtime.media.loadWebMedia(path, {
maxBytes,
localRoots: opts.mediaLocalRoots,
}),
});
},
"persist",

View File

@@ -48,6 +48,7 @@ export type RoomTopicEventContent = {
export type MatrixActionClientOpts = {
client?: MatrixClient;
cfg?: CoreConfig;
mediaLocalRoots?: readonly string[];
timeoutMs?: number;
accountId?: string | null;
readiness?: "none" | "prepared" | "started";

View File

@@ -232,9 +232,27 @@ describe("sendMessageMatrix media", () => {
});
expect(loadConfigMock).not.toHaveBeenCalled();
expect(loadWebMediaMock).toHaveBeenCalledWith("file:///tmp/photo.png", 1024 * 1024);
expect(loadWebMediaMock).toHaveBeenCalledWith("file:///tmp/photo.png", {
maxBytes: 1024 * 1024,
localRoots: undefined,
});
expect(resolveTextChunkLimitMock).toHaveBeenCalledWith(explicitCfg, "matrix", "ops");
});
it("passes caller mediaLocalRoots to media loading", async () => {
const { client } = makeClient();
await sendMessageMatrix("room:!room:example", "caption", {
client,
mediaUrl: "file:///tmp/photo.png",
mediaLocalRoots: ["/tmp/openclaw-matrix-test"],
});
expect(loadWebMediaMock).toHaveBeenCalledWith("file:///tmp/photo.png", {
maxBytes: undefined,
localRoots: ["/tmp/openclaw-matrix-test"],
});
});
});
describe("sendMessageMatrix threads", () => {

View File

@@ -109,7 +109,10 @@ export async function sendMessageMatrix(
let lastMessageId = "";
if (opts.mediaUrl) {
const maxBytes = resolveMediaMaxBytes(opts.accountId, cfg);
const media = await getCore().media.loadWebMedia(opts.mediaUrl, maxBytes);
const media = await getCore().media.loadWebMedia(opts.mediaUrl, {
maxBytes,
localRoots: opts.mediaLocalRoots,
});
const uploaded = await uploadMediaMaybeEncrypted(client, roomId, media.buffer, {
contentType: media.contentType,
filename: media.fileName,

View File

@@ -88,6 +88,7 @@ export type MatrixSendOpts = {
client?: import("../sdk.js").MatrixClient;
cfg?: CoreConfig;
mediaUrl?: string;
mediaLocalRoots?: readonly string[];
accountId?: string;
replyToId?: string;
threadId?: string | number | null;

View File

@@ -75,6 +75,7 @@ describe("matrixOutbound cfg threading", () => {
to: "room:!room:example",
text: "caption",
mediaUrl: "file:///tmp/cat.png",
mediaLocalRoots: ["/tmp/openclaw"],
accountId: "default",
});
@@ -84,6 +85,7 @@ describe("matrixOutbound cfg threading", () => {
expect.objectContaining({
cfg,
mediaUrl: "file:///tmp/cat.png",
mediaLocalRoots: ["/tmp/openclaw"],
}),
);
});

View File

@@ -23,13 +23,24 @@ export const matrixOutbound: ChannelOutboundAdapter = {
roomId: result.roomId,
};
},
sendMedia: async ({ cfg, to, text, mediaUrl, deps, replyToId, threadId, accountId }) => {
sendMedia: async ({
cfg,
to,
text,
mediaUrl,
mediaLocalRoots,
deps,
replyToId,
threadId,
accountId,
}) => {
const send = deps?.sendMatrix ?? sendMessageMatrix;
const resolvedThreadId =
threadId !== undefined && threadId !== null ? String(threadId) : undefined;
const result = await send(to, text, {
cfg,
mediaUrl,
mediaLocalRoots,
replyToId: replyToId ?? undefined,
threadId: resolvedThreadId,
accountId: accountId ?? undefined,

View File

@@ -24,6 +24,7 @@ export async function applyMatrixProfileUpdate(params: {
displayName?: string;
avatarUrl?: string;
avatarPath?: string;
mediaLocalRoots?: readonly string[];
}): Promise<MatrixProfileUpdateResult> {
const runtime = getMatrixRuntime();
const persistedCfg = runtime.config.loadConfig() as CoreConfig;
@@ -41,6 +42,7 @@ export async function applyMatrixProfileUpdate(params: {
displayName: displayName ?? undefined,
avatarUrl: avatarUrl ?? undefined,
avatarPath: avatarPath ?? undefined,
mediaLocalRoots: params.mediaLocalRoots,
});
const persistedAvatarUrl =
synced.uploadedAvatarSource && synced.resolvedAvatarUrl ? synced.resolvedAvatarUrl : avatarUrl;

View File

@@ -194,17 +194,41 @@ describe("handleMatrixAction pollVote", () => {
threadId: "$thread",
},
cfg,
{ mediaLocalRoots: ["/tmp/openclaw-matrix-test"] },
);
expect(mocks.sendMatrixMessage).toHaveBeenCalledWith("room:!room:example", "hello", {
cfg,
accountId: "ops",
mediaUrl: undefined,
mediaLocalRoots: ["/tmp/openclaw-matrix-test"],
replyToId: undefined,
threadId: "$thread",
});
});
it("passes mediaLocalRoots to profile updates", async () => {
const cfg = { channels: { matrix: { actions: { profile: true } } } } as CoreConfig;
await handleMatrixAction(
{
action: "setProfile",
accountId: "ops",
avatarPath: "/tmp/avatar.jpg",
},
cfg,
{ mediaLocalRoots: ["/tmp/openclaw-matrix-test"] },
);
expect(mocks.applyMatrixProfileUpdate).toHaveBeenCalledWith(
expect.objectContaining({
cfg,
account: "ops",
avatarPath: "/tmp/avatar.jpg",
mediaLocalRoots: ["/tmp/openclaw-matrix-test"],
}),
);
});
it("passes account-scoped opts to pin listing", async () => {
const cfg = { channels: { matrix: { actions: { pins: true } } } } as CoreConfig;
await handleMatrixAction(

View File

@@ -129,6 +129,7 @@ function readNumericArrayParam(
export async function handleMatrixAction(
params: Record<string, unknown>,
cfg: CoreConfig,
opts: { mediaLocalRoots?: readonly string[] } = {},
): Promise<AgentToolResult<unknown>> {
const action = readStringParam(params, "action", { required: true });
const accountId = readStringParam(params, "accountId") ?? undefined;
@@ -204,6 +205,7 @@ export async function handleMatrixAction(
const threadId = readStringParam(params, "threadId");
const result = await sendMatrixMessage(to, content, {
mediaUrl: mediaUrl ?? undefined,
mediaLocalRoots: opts.mediaLocalRoots,
replyToId: replyToId ?? undefined,
threadId: threadId ?? undefined,
...clientOpts,
@@ -278,6 +280,7 @@ export async function handleMatrixAction(
displayName: readStringParam(params, "displayName") ?? readStringParam(params, "name"),
avatarUrl: readStringParam(params, "avatarUrl"),
avatarPath,
mediaLocalRoots: opts.mediaLocalRoots,
});
return jsonResult({ ok: true, ...result });
}

View File

@@ -57,6 +57,7 @@ type SendMatrixMessage = (
cfg?: OpenClawConfig;
accountId?: string;
mediaUrl?: string;
mediaLocalRoots?: readonly string[];
replyToId?: string;
threadId?: string;
timeoutMs?: number;