mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 15:30:39 +00:00
fix(telegram): preserve original filename from Telegram document/audio/video uploads
The downloadAndSaveTelegramFile inner function only used the server-side file path (e.g. "documents/file_42.pdf") or the Content-Disposition header (which Telegram doesn't send) to derive the saved filename. The original filename provided by Telegram via msg.document.file_name, msg.audio.file_name, msg.video.file_name, and msg.animation.file_name was never passed through, causing all inbound files to lose their user-provided names. Now downloadAndSaveTelegramFile accepts an optional telegramFileName parameter that takes priority over the fetched/server-side name. The resolveMedia call site extracts the original name from the message and passes it through. Closes #31768 Made-with: Cursor
This commit is contained in:
committed by
Peter Steinberger
parent
e45d26b9ed
commit
88783417f3
@@ -31,8 +31,9 @@ const MAX_MEDIA_BYTES = 10_000_000;
|
||||
const BOT_TOKEN = "tok123";
|
||||
|
||||
function makeCtx(
|
||||
mediaField: "voice" | "audio" | "photo" | "video",
|
||||
mediaField: "voice" | "audio" | "photo" | "video" | "document" | "animation",
|
||||
getFile: TelegramContext["getFile"],
|
||||
opts?: { file_name?: string },
|
||||
): TelegramContext {
|
||||
const msg: Record<string, unknown> = {
|
||||
message_id: 1,
|
||||
@@ -43,13 +44,40 @@ function makeCtx(
|
||||
msg.voice = { file_id: "v1", duration: 5, file_unique_id: "u1" };
|
||||
}
|
||||
if (mediaField === "audio") {
|
||||
msg.audio = { file_id: "a1", duration: 5, file_unique_id: "u2" };
|
||||
msg.audio = {
|
||||
file_id: "a1",
|
||||
duration: 5,
|
||||
file_unique_id: "u2",
|
||||
...(opts?.file_name && { file_name: opts.file_name }),
|
||||
};
|
||||
}
|
||||
if (mediaField === "photo") {
|
||||
msg.photo = [{ file_id: "p1", width: 100, height: 100 }];
|
||||
}
|
||||
if (mediaField === "video") {
|
||||
msg.video = { file_id: "vid1", duration: 10, file_unique_id: "u3" };
|
||||
msg.video = {
|
||||
file_id: "vid1",
|
||||
duration: 10,
|
||||
file_unique_id: "u3",
|
||||
...(opts?.file_name && { file_name: opts.file_name }),
|
||||
};
|
||||
}
|
||||
if (mediaField === "document") {
|
||||
msg.document = {
|
||||
file_id: "d1",
|
||||
file_unique_id: "u4",
|
||||
...(opts?.file_name && { file_name: opts.file_name }),
|
||||
};
|
||||
}
|
||||
if (mediaField === "animation") {
|
||||
msg.animation = {
|
||||
file_id: "an1",
|
||||
duration: 3,
|
||||
file_unique_id: "u5",
|
||||
width: 200,
|
||||
height: 200,
|
||||
...(opts?.file_name && { file_name: opts.file_name }),
|
||||
};
|
||||
}
|
||||
return {
|
||||
message: msg as unknown as Message,
|
||||
@@ -204,3 +232,140 @@ describe("resolveMedia getFile retry", () => {
|
||||
expect(result).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveMedia original filename preservation", () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
fetchRemoteMedia.mockClear();
|
||||
saveMediaBuffer.mockClear();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("passes document.file_name to saveMediaBuffer instead of server-side path", async () => {
|
||||
const getFile = vi.fn().mockResolvedValue({ file_path: "documents/file_42.pdf" });
|
||||
fetchRemoteMedia.mockResolvedValueOnce({
|
||||
buffer: Buffer.from("pdf-data"),
|
||||
contentType: "application/pdf",
|
||||
fileName: "file_42.pdf",
|
||||
});
|
||||
saveMediaBuffer.mockResolvedValueOnce({
|
||||
path: "/tmp/business-plan---uuid.pdf",
|
||||
contentType: "application/pdf",
|
||||
});
|
||||
|
||||
const ctx = makeCtx("document", getFile, { file_name: "business-plan.pdf" });
|
||||
const result = await resolveMedia(ctx, MAX_MEDIA_BYTES, BOT_TOKEN);
|
||||
|
||||
expect(saveMediaBuffer).toHaveBeenCalledWith(
|
||||
expect.any(Buffer),
|
||||
"application/pdf",
|
||||
"inbound",
|
||||
MAX_MEDIA_BYTES,
|
||||
"business-plan.pdf",
|
||||
);
|
||||
expect(result).toEqual(expect.objectContaining({ path: "/tmp/business-plan---uuid.pdf" }));
|
||||
});
|
||||
|
||||
it("passes audio.file_name to saveMediaBuffer", async () => {
|
||||
const getFile = vi.fn().mockResolvedValue({ file_path: "music/file_99.mp3" });
|
||||
fetchRemoteMedia.mockResolvedValueOnce({
|
||||
buffer: Buffer.from("audio-data"),
|
||||
contentType: "audio/mpeg",
|
||||
fileName: "file_99.mp3",
|
||||
});
|
||||
saveMediaBuffer.mockResolvedValueOnce({
|
||||
path: "/tmp/my-song---uuid.mp3",
|
||||
contentType: "audio/mpeg",
|
||||
});
|
||||
|
||||
const ctx = makeCtx("audio", getFile, { file_name: "my-song.mp3" });
|
||||
const result = await resolveMedia(ctx, MAX_MEDIA_BYTES, BOT_TOKEN);
|
||||
|
||||
expect(saveMediaBuffer).toHaveBeenCalledWith(
|
||||
expect.any(Buffer),
|
||||
"audio/mpeg",
|
||||
"inbound",
|
||||
MAX_MEDIA_BYTES,
|
||||
"my-song.mp3",
|
||||
);
|
||||
expect(result).not.toBeNull();
|
||||
});
|
||||
|
||||
it("passes video.file_name to saveMediaBuffer", async () => {
|
||||
const getFile = vi.fn().mockResolvedValue({ file_path: "videos/file_55.mp4" });
|
||||
fetchRemoteMedia.mockResolvedValueOnce({
|
||||
buffer: Buffer.from("video-data"),
|
||||
contentType: "video/mp4",
|
||||
fileName: "file_55.mp4",
|
||||
});
|
||||
saveMediaBuffer.mockResolvedValueOnce({
|
||||
path: "/tmp/presentation---uuid.mp4",
|
||||
contentType: "video/mp4",
|
||||
});
|
||||
|
||||
const ctx = makeCtx("video", getFile, { file_name: "presentation.mp4" });
|
||||
const result = await resolveMedia(ctx, MAX_MEDIA_BYTES, BOT_TOKEN);
|
||||
|
||||
expect(saveMediaBuffer).toHaveBeenCalledWith(
|
||||
expect.any(Buffer),
|
||||
"video/mp4",
|
||||
"inbound",
|
||||
MAX_MEDIA_BYTES,
|
||||
"presentation.mp4",
|
||||
);
|
||||
expect(result).not.toBeNull();
|
||||
});
|
||||
|
||||
it("falls back to fetched.fileName when telegram file_name is absent", async () => {
|
||||
const getFile = vi.fn().mockResolvedValue({ file_path: "documents/file_42.pdf" });
|
||||
fetchRemoteMedia.mockResolvedValueOnce({
|
||||
buffer: Buffer.from("pdf-data"),
|
||||
contentType: "application/pdf",
|
||||
fileName: "file_42.pdf",
|
||||
});
|
||||
saveMediaBuffer.mockResolvedValueOnce({
|
||||
path: "/tmp/file_42---uuid.pdf",
|
||||
contentType: "application/pdf",
|
||||
});
|
||||
|
||||
const ctx = makeCtx("document", getFile);
|
||||
const result = await resolveMedia(ctx, MAX_MEDIA_BYTES, BOT_TOKEN);
|
||||
|
||||
expect(saveMediaBuffer).toHaveBeenCalledWith(
|
||||
expect.any(Buffer),
|
||||
"application/pdf",
|
||||
"inbound",
|
||||
MAX_MEDIA_BYTES,
|
||||
"file_42.pdf",
|
||||
);
|
||||
expect(result).not.toBeNull();
|
||||
});
|
||||
|
||||
it("falls back to filePath when neither telegram nor fetched fileName is available", async () => {
|
||||
const getFile = vi.fn().mockResolvedValue({ file_path: "documents/file_42.pdf" });
|
||||
fetchRemoteMedia.mockResolvedValueOnce({
|
||||
buffer: Buffer.from("pdf-data"),
|
||||
contentType: "application/pdf",
|
||||
fileName: undefined,
|
||||
});
|
||||
saveMediaBuffer.mockResolvedValueOnce({
|
||||
path: "/tmp/file_42---uuid.pdf",
|
||||
contentType: "application/pdf",
|
||||
});
|
||||
|
||||
const ctx = makeCtx("document", getFile);
|
||||
const result = await resolveMedia(ctx, MAX_MEDIA_BYTES, BOT_TOKEN);
|
||||
|
||||
expect(saveMediaBuffer).toHaveBeenCalledWith(
|
||||
expect.any(Buffer),
|
||||
"application/pdf",
|
||||
"inbound",
|
||||
MAX_MEDIA_BYTES,
|
||||
"documents/file_42.pdf",
|
||||
);
|
||||
expect(result).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -53,7 +53,11 @@ export async function resolveMedia(
|
||||
stickerMetadata?: StickerMetadata;
|
||||
} | null> {
|
||||
const msg = ctx.message;
|
||||
const downloadAndSaveTelegramFile = async (filePath: string, fetchImpl: typeof fetch) => {
|
||||
const downloadAndSaveTelegramFile = async (
|
||||
filePath: string,
|
||||
fetchImpl: typeof fetch,
|
||||
telegramFileName?: string,
|
||||
) => {
|
||||
const url = `https://api.telegram.org/file/bot${token}/${filePath}`;
|
||||
const fetched = await fetchRemoteMedia({
|
||||
url,
|
||||
@@ -62,7 +66,7 @@ export async function resolveMedia(
|
||||
maxBytes,
|
||||
ssrfPolicy: TELEGRAM_MEDIA_SSRF_POLICY,
|
||||
});
|
||||
const originalName = fetched.fileName ?? filePath;
|
||||
const originalName = telegramFileName ?? fetched.fileName ?? filePath;
|
||||
return saveMediaBuffer(fetched.buffer, fetched.contentType, "inbound", maxBytes, originalName);
|
||||
};
|
||||
|
||||
@@ -184,7 +188,12 @@ export async function resolveMedia(
|
||||
if (!fetchImpl) {
|
||||
throw new Error("fetch is not available; set channels.telegram.proxy in config");
|
||||
}
|
||||
const saved = await downloadAndSaveTelegramFile(file.file_path, fetchImpl);
|
||||
const telegramFileName =
|
||||
msg.document?.file_name ??
|
||||
msg.audio?.file_name ??
|
||||
msg.video?.file_name ??
|
||||
msg.animation?.file_name;
|
||||
const saved = await downloadAndSaveTelegramFile(file.file_path, fetchImpl, telegramFileName);
|
||||
const placeholder = resolveTelegramMediaPlaceholder(msg) ?? "<media:document>";
|
||||
return { path: saved.path, contentType: saved.contentType, placeholder };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user