From 4ed48f7f4b992d01df9c00ee41d3eeb2cf5f9198 Mon Sep 17 00:00:00 2001 From: Sai Asish Y Date: Mon, 18 May 2026 17:03:25 -0700 Subject: [PATCH] feat(builders): detect file MIME types from extension instead of hardcoding --- lib/builders/document.ex | 3 +- lib/builders/photo.ex | 3 +- lib/builders/sticker.ex | 3 +- lib/builders/video.ex | 5 +-- lib/mime_type.ex | 77 ++++++++++++++++++++++++++++++++++++++++ test/mime_type_test.exs | 34 ++++++++++++++++++ 6 files changed, 120 insertions(+), 5 deletions(-) create mode 100644 lib/mime_type.ex create mode 100644 test/mime_type_test.exs diff --git a/lib/builders/document.ex b/lib/builders/document.ex index c61dc1b..0cd2db0 100644 --- a/lib/builders/document.ex +++ b/lib/builders/document.ex @@ -27,6 +27,7 @@ defmodule TelegramEx.Builder.Document do """ alias TelegramEx.API + alias TelegramEx.MimeType @doc """ Sets the document from a URL. @@ -65,7 +66,7 @@ defmodule TelegramEx.Builder.Document do content = File.read!(path) Map.get(ctx, :payload, %{}) - |> Map.put(:document, {content, filename: filename, content_type: "application/octet-stream"}) + |> Map.put(:document, {content, filename: filename, content_type: MimeType.from_path(path)}) |> then(&Map.put(ctx, :payload, &1)) end diff --git a/lib/builders/photo.ex b/lib/builders/photo.ex index bceb453..e8a6798 100644 --- a/lib/builders/photo.ex +++ b/lib/builders/photo.ex @@ -27,6 +27,7 @@ defmodule TelegramEx.Builder.Photo do """ alias TelegramEx.API + alias TelegramEx.MimeType @doc """ Sets the photo from a URL. @@ -77,7 +78,7 @@ defmodule TelegramEx.Builder.Photo do content = File.read!(path) Map.get(ctx, :payload, %{}) - |> Map.put(:photo, {content, filename: filename, content_type: "image/jpeg"}) + |> Map.put(:photo, {content, filename: filename, content_type: MimeType.from_path(path)}) |> then(&Map.put(ctx, :payload, &1)) end diff --git a/lib/builders/sticker.ex b/lib/builders/sticker.ex index bb886fd..f7595c3 100644 --- a/lib/builders/sticker.ex +++ b/lib/builders/sticker.ex @@ -24,6 +24,7 @@ defmodule TelegramEx.Builder.Sticker do """ alias TelegramEx.API + alias TelegramEx.MimeType @doc """ Sets the sticker by Telegram file ID. @@ -81,7 +82,7 @@ defmodule TelegramEx.Builder.Sticker do content = File.read!(path) Map.get(ctx, :payload, %{}) - |> Map.put(:sticker, {content, filename: filename, content_type: "image/webp"}) + |> Map.put(:sticker, {content, filename: filename, content_type: MimeType.from_path(path)}) |> then(&Map.put(ctx, :payload, &1)) end diff --git a/lib/builders/video.ex b/lib/builders/video.ex index f1d45f6..2f0b3cb 100644 --- a/lib/builders/video.ex +++ b/lib/builders/video.ex @@ -26,6 +26,7 @@ defmodule TelegramEx.Builder.Video do """ alias TelegramEx.API + alias TelegramEx.MimeType @doc """ Sets the video by Telegram file ID. @@ -83,7 +84,7 @@ defmodule TelegramEx.Builder.Video do content = File.read!(path) Map.get(ctx, :payload, %{}) - |> Map.put(:video, {content, filename: filename, content_type: "video/mp4"}) + |> Map.put(:video, {content, filename: filename, content_type: MimeType.from_path(path)}) |> then(&Map.put(ctx, :payload, &1)) end @@ -124,7 +125,7 @@ defmodule TelegramEx.Builder.Video do content = File.read!(path) Map.get(ctx, :payload, %{}) - |> Map.put(:cover, {content, filename: filename, content_type: "image/jpeg"}) + |> Map.put(:cover, {content, filename: filename, content_type: MimeType.from_path(path)}) |> then(&Map.put(ctx, :payload, &1)) end diff --git a/lib/mime_type.ex b/lib/mime_type.ex new file mode 100644 index 0000000..81a24f9 --- /dev/null +++ b/lib/mime_type.ex @@ -0,0 +1,77 @@ +defmodule TelegramEx.MimeType do + @moduledoc """ + Resolves MIME content types for local files based on their extension. + + This is used by the file-based builders (`Photo`, `Video`, `Sticker`, + `Document`) so that uploaded media carries the correct `Content-Type` + header instead of a hardcoded value. + """ + + @types %{ + ".jpg" => "image/jpeg", + ".jpeg" => "image/jpeg", + ".png" => "image/png", + ".gif" => "image/gif", + ".webp" => "image/webp", + ".bmp" => "image/bmp", + ".tif" => "image/tiff", + ".tiff" => "image/tiff", + ".svg" => "image/svg+xml", + ".mp4" => "video/mp4", + ".webm" => "video/webm", + ".mov" => "video/quicktime", + ".avi" => "video/x-msvideo", + ".mkv" => "video/x-matroska", + ".mpeg" => "video/mpeg", + ".mpg" => "video/mpeg", + ".mp3" => "audio/mpeg", + ".ogg" => "audio/ogg", + ".wav" => "audio/wav", + ".m4a" => "audio/mp4", + ".pdf" => "application/pdf", + ".zip" => "application/zip", + ".json" => "application/json", + ".txt" => "text/plain", + ".csv" => "text/csv", + ".html" => "text/html" + } + + @default "application/octet-stream" + + @doc """ + Returns the MIME content type for the given file path. + + The lookup is based on the lowercased file extension. Unknown extensions + fall back to `#{@default}`. + + ## Parameters + + - `path` - Path or filename of the file + + ## Returns + + A MIME content type string. + + ## Examples + + iex> TelegramEx.MimeType.from_path("photo.PNG") + "image/png" + + iex> TelegramEx.MimeType.from_path("clip.webm") + "video/webm" + + iex> TelegramEx.MimeType.from_path("archive.unknown") + "application/octet-stream" + """ + @spec from_path(String.t()) :: String.t() + def from_path(path) do + extension = path |> Path.extname() |> String.downcase() + Map.get(@types, extension, @default) + end + + @doc """ + Returns the fallback MIME content type used for unknown extensions. + """ + @spec default() :: String.t() + def default, do: @default +end diff --git a/test/mime_type_test.exs b/test/mime_type_test.exs new file mode 100644 index 0000000..1fa36a6 --- /dev/null +++ b/test/mime_type_test.exs @@ -0,0 +1,34 @@ +defmodule TelegramEx.MimeTypeTest do + use ExUnit.Case + + doctest TelegramEx.MimeType + + alias TelegramEx.MimeType + + test "resolves image extensions to their MIME types" do + assert MimeType.from_path("avatar.png") == "image/png" + assert MimeType.from_path("animation.gif") == "image/gif" + assert MimeType.from_path("sticker.webp") == "image/webp" + assert MimeType.from_path("photo.jpg") == "image/jpeg" + end + + test "resolves video extensions to their MIME types" do + assert MimeType.from_path("clip.webm") == "video/webm" + assert MimeType.from_path("movie.mp4") == "video/mp4" + end + + test "lookup is case insensitive" do + assert MimeType.from_path("PHOTO.PNG") == "image/png" + assert MimeType.from_path("Clip.WebM") == "video/webm" + end + + test "falls back to application/octet-stream for unknown extensions" do + assert MimeType.from_path("archive.unknown") == "application/octet-stream" + assert MimeType.from_path("noextension") == "application/octet-stream" + assert MimeType.from_path("report.pdf") == "application/pdf" + end + + test "default/0 returns the safe fallback type" do + assert MimeType.default() == "application/octet-stream" + end +end