fix: accept file/media tokens safely and improve web media send
parent
9bf35d3272
commit
5ce869f86c
|
|
@ -24,11 +24,17 @@ import { sendTypingIndicator } from "../twilio/typing.js";
|
||||||
import type { TwilioRequester } from "../twilio/types.js";
|
import type { TwilioRequester } from "../twilio/types.js";
|
||||||
import { defaultRuntime, type RuntimeEnv } from "../runtime.js";
|
import { defaultRuntime, type RuntimeEnv } from "../runtime.js";
|
||||||
import { logError } from "../logger.js";
|
import { logError } from "../logger.js";
|
||||||
|
import { ensureMediaHosted } from "../media/host.js";
|
||||||
|
|
||||||
type GetReplyOptions = {
|
type GetReplyOptions = {
|
||||||
onReplyStart?: () => Promise<void> | void;
|
onReplyStart?: () => Promise<void> | void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function normalizeMediaSource(src: string) {
|
||||||
|
if (src.startsWith("file://")) return src.replace("file://", "");
|
||||||
|
return src;
|
||||||
|
}
|
||||||
|
|
||||||
function summarizeClaudeMetadata(payload: unknown): string | undefined {
|
function summarizeClaudeMetadata(payload: unknown): string | undefined {
|
||||||
if (!payload || typeof payload !== "object") return undefined;
|
if (!payload || typeof payload !== "object") return undefined;
|
||||||
const obj = payload as Record<string, unknown>;
|
const obj = payload as Record<string, unknown>;
|
||||||
|
|
@ -295,8 +301,8 @@ const mediaNote =
|
||||||
if (mediaLine) {
|
if (mediaLine) {
|
||||||
const after = mediaLine.replace(/^MEDIA:\s*/i, "");
|
const after = mediaLine.replace(/^MEDIA:\s*/i, "");
|
||||||
const parts = after.trim().split(/\s+/);
|
const parts = after.trim().split(/\s+/);
|
||||||
if (parts.length === 1 && parts[0]) {
|
if (parts[0]) {
|
||||||
mediaFromCommand = parts[0];
|
mediaFromCommand = normalizeMediaSource(parts[0]);
|
||||||
}
|
}
|
||||||
trimmed = rawStdout
|
trimmed = rawStdout
|
||||||
.split("\n")
|
.split("\n")
|
||||||
|
|
@ -310,10 +316,14 @@ const mediaNote =
|
||||||
const looksLikeUrl = mediaFromCommand
|
const looksLikeUrl = mediaFromCommand
|
||||||
? /^https?:\/\//i.test(mediaFromCommand)
|
? /^https?:\/\//i.test(mediaFromCommand)
|
||||||
: false;
|
: false;
|
||||||
|
const looksLikePath = mediaFromCommand
|
||||||
|
? mediaFromCommand.startsWith("/") || mediaFromCommand.startsWith("./")
|
||||||
|
: false;
|
||||||
if (
|
if (
|
||||||
!mediaFromCommand ||
|
!mediaFromCommand ||
|
||||||
hasWhitespace ||
|
hasWhitespace ||
|
||||||
(!looksLikeUrl && mediaFromCommand.length > 1024)
|
(!looksLikeUrl && !looksLikePath) ||
|
||||||
|
mediaFromCommand.length > 1024
|
||||||
) {
|
) {
|
||||||
mediaFromCommand = undefined;
|
mediaFromCommand = undefined;
|
||||||
}
|
}
|
||||||
|
|
@ -440,11 +450,16 @@ export async function autoReplyIfConfigured(
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
let mediaUrl = replyResult.mediaUrl;
|
||||||
|
if (mediaUrl && !/^https?:\/\//i.test(mediaUrl)) {
|
||||||
|
const hosted = await ensureMediaHosted(mediaUrl);
|
||||||
|
mediaUrl = hosted.url;
|
||||||
|
}
|
||||||
await client.messages.create({
|
await client.messages.create({
|
||||||
from: replyFrom,
|
from: replyFrom,
|
||||||
to: replyTo,
|
to: replyTo,
|
||||||
body: replyResult.text ?? "",
|
body: replyResult.text ?? "",
|
||||||
...(replyResult.mediaUrl ? { mediaUrl: [replyResult.mediaUrl] } : {}),
|
...(mediaUrl ? { mediaUrl: [mediaUrl] } : {}),
|
||||||
});
|
});
|
||||||
if (isVerbose()) {
|
if (isVerbose()) {
|
||||||
console.log(
|
console.log(
|
||||||
|
|
|
||||||
|
|
@ -146,6 +146,31 @@ describe("config and templating", () => {
|
||||||
expect(result?.mediaUrl).toBe("https://example.com/img.jpg");
|
expect(result?.mediaUrl).toBe("https://example.com/img.jpg");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("extracts first MEDIA token even with trailing text", async () => {
|
||||||
|
const runSpy = vi.spyOn(index, "runCommandWithTimeout").mockResolvedValue({
|
||||||
|
stdout: "hello\nMEDIA:/tmp/pic.png extra words here\n",
|
||||||
|
stderr: "",
|
||||||
|
code: 0,
|
||||||
|
signal: null,
|
||||||
|
killed: false,
|
||||||
|
});
|
||||||
|
const cfg = {
|
||||||
|
inbound: {
|
||||||
|
reply: {
|
||||||
|
mode: "command" as const,
|
||||||
|
command: ["echo", "{{Body}}"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const result = await index.getReplyFromConfig(
|
||||||
|
{ Body: "hi", From: "+1", To: "+2" },
|
||||||
|
undefined,
|
||||||
|
cfg,
|
||||||
|
runSpy,
|
||||||
|
);
|
||||||
|
expect(result?.mediaUrl).toBe("/tmp/pic.png");
|
||||||
|
});
|
||||||
|
|
||||||
it("ignores invalid MEDIA lines with whitespace", async () => {
|
it("ignores invalid MEDIA lines with whitespace", async () => {
|
||||||
const runSpy = vi.spyOn(index, "runCommandWithTimeout").mockResolvedValue({
|
const runSpy = vi.spyOn(index, "runCommandWithTimeout").mockResolvedValue({
|
||||||
stdout: "hello\nMEDIA: not a url with spaces\nrest\n",
|
stdout: "hello\nMEDIA: not a url with spaces\nrest\n",
|
||||||
|
|
|
||||||
|
|
@ -117,12 +117,15 @@ export async function sendMessageWeb(
|
||||||
logVerbose(`Presence update skipped: ${String(err)}`);
|
logVerbose(`Presence update skipped: ${String(err)}`);
|
||||||
}
|
}
|
||||||
let payload: AnyMessageContent = { text: body };
|
let payload: AnyMessageContent = { text: body };
|
||||||
if (options.mediaUrl) {
|
if (options.mediaUrl) {
|
||||||
const media = await loadWebMedia(options.mediaUrl);
|
const normalized = options.mediaUrl.startsWith("file://")
|
||||||
payload = {
|
? options.mediaUrl.replace("file://", "")
|
||||||
image: media.buffer,
|
: options.mediaUrl;
|
||||||
caption: body || undefined,
|
const media = await loadWebMedia(options.mediaUrl);
|
||||||
mimetype: media.contentType,
|
payload = {
|
||||||
|
image: media.buffer,
|
||||||
|
caption: body || undefined,
|
||||||
|
mimetype: media.contentType,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
logInfo(
|
logInfo(
|
||||||
|
|
@ -514,6 +517,9 @@ async function downloadInboundMedia(
|
||||||
async function loadWebMedia(
|
async function loadWebMedia(
|
||||||
mediaUrl: string,
|
mediaUrl: string,
|
||||||
): Promise<{ buffer: Buffer; contentType?: string }> {
|
): Promise<{ buffer: Buffer; contentType?: string }> {
|
||||||
|
if (mediaUrl.startsWith("file://")) {
|
||||||
|
mediaUrl = mediaUrl.replace("file://", "");
|
||||||
|
}
|
||||||
if (/^https?:\/\//i.test(mediaUrl)) {
|
if (/^https?:\/\//i.test(mediaUrl)) {
|
||||||
const res = await fetch(mediaUrl);
|
const res = await fetch(mediaUrl);
|
||||||
if (!res.ok || !res.body) {
|
if (!res.ok || !res.body) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue