From 4ada08818359762adbf7cc38393f0bc201758c92 Mon Sep 17 00:00:00 2001 From: HBx Date: Wed, 1 Jul 2026 22:11:07 +0300 Subject: [PATCH] feat: add filename extension fallback for text file detection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Support .svelte, .vue, .tsx, .py, .go, and 40+ other code file extensions when Telegram sends an unrecognized or generic MIME type (e.g. application/octet-stream). This fixes #170 — .svelte files (and similar code files) are now correctly identified as text content instead of being rejected as unsupported. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/app/services/file-download-service.ts | 56 ++++++++++++++++++- src/bot/handlers/document-handler.ts | 2 +- src/bot/handlers/media-group-handler.ts | 2 +- .../services/file-download-service.test.ts | 30 ++++++++++ 4 files changed, 86 insertions(+), 4 deletions(-) diff --git a/src/app/services/file-download-service.ts b/src/app/services/file-download-service.ts index 61a21ab8..f2eeecc3 100644 --- a/src/app/services/file-download-service.ts +++ b/src/app/services/file-download-service.ts @@ -108,7 +108,48 @@ const APPLICATION_TEXT_MIME_TYPES = new Set([ "application/sql", ]); -export function isTextMimeType(mimeType: string | undefined): boolean { +const TEXT_FILE_EXTENSIONS = new Set([ + "svelte", + "vue", + "ts", + "tsx", + "jsx", + "mjs", + "cjs", + "go", + "rs", + "rb", + "py", + "java", + "c", + "cpp", + "h", + "hpp", + "cs", + "swift", + "kt", + "kts", + "sh", + "bash", + "yaml", + "yml", + "toml", + "ini", + "cfg", + "md", + "mdx", + "css", + "scss", + "less", + "html", + "htm", + "graphql", + "gql", + "proto", + "gradle", +]); + +export function isTextMimeType(mimeType: string | undefined, filename?: string): boolean { if (!mimeType) { return false; } @@ -117,5 +158,16 @@ export function isTextMimeType(mimeType: string | undefined): boolean { return true; } - return APPLICATION_TEXT_MIME_TYPES.has(mimeType); + if (APPLICATION_TEXT_MIME_TYPES.has(mimeType)) { + return true; + } + + if (filename) { + const ext = filename.split(".").pop()?.toLowerCase(); + if (ext && TEXT_FILE_EXTENSIONS.has(ext)) { + return true; + } + } + + return false; } diff --git a/src/bot/handlers/document-handler.ts b/src/bot/handlers/document-handler.ts index 2a6b2390..e53882df 100644 --- a/src/bot/handlers/document-handler.ts +++ b/src/bot/handlers/document-handler.ts @@ -50,7 +50,7 @@ export async function handleDocumentMessage( const filename = doc.file_name || "document"; try { - if (isTextMimeType(mimeType)) { + if (isTextMimeType(mimeType, filename)) { if (!isFileSizeAllowed(doc.file_size, config.files.maxFileSizeKb)) { logger.warn( `[Document] Text file too large: ${filename} (${doc.file_size} bytes > ${config.files.maxFileSizeKb}KB)`, diff --git a/src/bot/handlers/media-group-handler.ts b/src/bot/handlers/media-group-handler.ts index 32d680d1..44a678fa 100644 --- a/src/bot/handlers/media-group-handler.ts +++ b/src/bot/handlers/media-group-handler.ts @@ -254,7 +254,7 @@ export class MediaGroupAttachmentHandler { const mimeType = document.mime_type || ""; const filename = document.file_name || "document"; - if (isTextMimeType(mimeType)) { + if (isTextMimeType(mimeType, filename)) { if (!isFileSizeAllowed(document.file_size, config.files.maxFileSizeKb)) { return { reason: "text_file_too_large" }; } diff --git a/tests/app/services/file-download-service.test.ts b/tests/app/services/file-download-service.test.ts index 208bab18..40e3832b 100644 --- a/tests/app/services/file-download-service.test.ts +++ b/tests/app/services/file-download-service.test.ts @@ -114,6 +114,36 @@ describe("app/services/file-download-service", () => { it("returns false for empty string", () => { expect(isTextMimeType("")).toBe(false); }); + + it("returns true for unknown MIME with known code file extension", () => { + expect(isTextMimeType("application/octet-stream", "component.svelte")).toBe(true); + expect(isTextMimeType("application/octet-stream", "App.vue")).toBe(true); + expect(isTextMimeType("application/octet-stream", "main.tsx")).toBe(true); + expect(isTextMimeType("application/octet-stream", "server.go")).toBe(true); + expect(isTextMimeType("application/octet-stream", "script.py")).toBe(true); + }); + + it("returns false for unknown MIME with unknown extension", () => { + expect(isTextMimeType("application/octet-stream", "file.xyz")).toBe(false); + expect(isTextMimeType("application/octet-stream", "archive.7z")).toBe(false); + }); + + it("returns false for unknown MIME without filename", () => { + expect(isTextMimeType("application/octet-stream")).toBe(false); + }); + + it("returns false for undefined MIME even with known extension", () => { + expect(isTextMimeType(undefined, "file.svelte")).toBe(false); + }); + + it("handles files with multiple dots correctly", () => { + expect(isTextMimeType("application/octet-stream", "Component.test.svelte")).toBe(true); + expect(isTextMimeType("application/octet-stream", "some.file.with.dots.py")).toBe(true); + }); + + it("handles files with no extension", () => { + expect(isTextMimeType("application/octet-stream", "Dockerfile")).toBe(false); + }); }); });