fix(telegram): improve gif handling
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user