mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-10 13:44:58 +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
da05395c2a
@@ -31,8 +31,9 @@ const MAX_MEDIA_BYTES = 10_000_000;
|
|||||||
const BOT_TOKEN = "tok123";
|
const BOT_TOKEN = "tok123";
|
||||||
|
|
||||||
function makeCtx(
|
function makeCtx(
|
||||||
mediaField: "voice" | "audio" | "photo" | "video",
|
mediaField: "voice" | "audio" | "photo" | "video" | "document" | "animation",
|
||||||
getFile: TelegramContext["getFile"],
|
getFile: TelegramContext["getFile"],
|
||||||
|
opts?: { file_name?: string },
|
||||||
): TelegramContext {
|
): TelegramContext {
|
||||||
const msg: Record<string, unknown> = {
|
const msg: Record<string, unknown> = {
|
||||||
message_id: 1,
|
message_id: 1,
|
||||||
@@ -43,13 +44,40 @@ function makeCtx(
|
|||||||
msg.voice = { file_id: "v1", duration: 5, file_unique_id: "u1" };
|
msg.voice = { file_id: "v1", duration: 5, file_unique_id: "u1" };
|
||||||
}
|
}
|
||||||
if (mediaField === "audio") {
|
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") {
|
if (mediaField === "photo") {
|
||||||
msg.photo = [{ file_id: "p1", width: 100, height: 100 }];
|
msg.photo = [{ file_id: "p1", width: 100, height: 100 }];
|
||||||
}
|
}
|
||||||
if (mediaField === "video") {
|
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 {
|
return {
|
||||||
message: msg as unknown as Message,
|
message: msg as unknown as Message,
|
||||||
@@ -204,3 +232,140 @@ describe("resolveMedia getFile retry", () => {
|
|||||||
expect(result).not.toBeNull();
|
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;
|
stickerMetadata?: StickerMetadata;
|
||||||
} | null> {
|
} | null> {
|
||||||
const msg = ctx.message;
|
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 url = `https://api.telegram.org/file/bot${token}/${filePath}`;
|
||||||
const fetched = await fetchRemoteMedia({
|
const fetched = await fetchRemoteMedia({
|
||||||
url,
|
url,
|
||||||
@@ -62,7 +66,7 @@ export async function resolveMedia(
|
|||||||
maxBytes,
|
maxBytes,
|
||||||
ssrfPolicy: TELEGRAM_MEDIA_SSRF_POLICY,
|
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);
|
return saveMediaBuffer(fetched.buffer, fetched.contentType, "inbound", maxBytes, originalName);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -184,7 +188,12 @@ export async function resolveMedia(
|
|||||||
if (!fetchImpl) {
|
if (!fetchImpl) {
|
||||||
throw new Error("fetch is not available; set channels.telegram.proxy in config");
|
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>";
|
const placeholder = resolveTelegramMediaPlaceholder(msg) ?? "<media:document>";
|
||||||
return { path: saved.path, contentType: saved.contentType, placeholder };
|
return { path: saved.path, contentType: saved.contentType, placeholder };
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user