fix(telegram): improve gif handling

This commit is contained in:
Peter Steinberger
2026-01-06 02:22:09 +00:00
parent 45c67a48af
commit cc0ef4d012
8 changed files with 209 additions and 15 deletions

View File

@@ -76,6 +76,34 @@ describe("web media loading", () => {
fetchMock.mockRestore();
});
it("uses content-disposition filename when available", async () => {
const fetchMock = vi.spyOn(globalThis, "fetch").mockResolvedValueOnce({
ok: true,
body: true,
arrayBuffer: async () => Buffer.from("%PDF-1.4").buffer,
headers: {
get: (name: string) => {
if (name === "content-disposition") {
return 'attachment; filename="report.pdf"';
}
if (name === "content-type") return "application/pdf";
return null;
},
},
status: 200,
} as Response);
const result = await loadWebMedia(
"https://example.com/download?id=1",
1024 * 1024,
);
expect(result.kind).toBe("document");
expect(result.fileName).toBe("report.pdf");
fetchMock.mockRestore();
});
it("preserves GIF animation by skipping JPEG optimization", async () => {
// Create a minimal valid GIF (1x1 pixel)
// GIF89a header + minimal image data

View File

@@ -22,6 +22,29 @@ type WebMediaOptions = {
optimizeImages?: boolean;
};
function stripQuotes(value: string): string {
return value.replace(/^["']|["']$/g, "");
}
function parseContentDispositionFileName(
header?: string | null,
): string | undefined {
if (!header) return undefined;
const starMatch = /filename\*\s*=\s*([^;]+)/i.exec(header);
if (starMatch?.[1]) {
const cleaned = stripQuotes(starMatch[1].trim());
const encoded = cleaned.split("''").slice(1).join("''") || cleaned;
try {
return path.basename(decodeURIComponent(encoded));
} catch {
return path.basename(encoded);
}
}
const match = /filename\s*=\s*([^;]+)/i.exec(header);
if (match?.[1]) return path.basename(stripQuotes(match[1].trim()));
return undefined;
}
async function loadWebMediaInternal(
mediaUrl: string,
options: WebMediaOptions = {},
@@ -54,11 +77,11 @@ async function loadWebMediaInternal(
};
if (/^https?:\/\//i.test(mediaUrl)) {
let fileName: string | undefined;
let fileNameFromUrl: string | undefined;
try {
const url = new URL(mediaUrl);
const base = path.basename(url.pathname);
fileName = base || undefined;
fileNameFromUrl = base || undefined;
} catch {
// ignore parse errors; leave undefined
}
@@ -67,10 +90,18 @@ async function loadWebMediaInternal(
throw new Error(`Failed to fetch media: HTTP ${res.status}`);
}
const array = Buffer.from(await res.arrayBuffer());
const headerFileName = parseContentDispositionFileName(
res.headers.get("content-disposition"),
);
let fileName = headerFileName || fileNameFromUrl || undefined;
const filePathForMime =
headerFileName && path.extname(headerFileName)
? headerFileName
: mediaUrl;
const contentType = await detectMime({
buffer: array,
headerMime: res.headers.get("content-type"),
filePath: mediaUrl,
filePath: filePathForMime,
});
if (fileName && !path.extname(fileName) && contentType) {
const ext = extensionForMime(contentType);