diff --git a/README.md b/README.md index 9b4f686..feb2738 100644 --- a/README.md +++ b/README.md @@ -143,22 +143,35 @@ end ### Sending Media ```elixir -# Photo -ctx -|> Photo.path("/path/to/image.jpg") -|> Photo.caption("Check this out!") -|> Photo.send(chat_id) - -# Document -ctx -|> Document.url("https://example.com/file.pdf") -|> Document.send(chat_id) - -# Video -ctx -|> Video.path("/path/to/video.mp4") -|> Video.duration(120) -|> Video.send(chat_id) +# Photo with error handling +case Photo.path(ctx, "/path/to/image.jpg") do + {:ok, ctx} -> + ctx + |> Photo.caption("Check this out!") + |> Photo.send(chat_id) + + {:error, :enoent} -> + ctx + |> Message.text("Photo not found") + |> Message.send(chat_id) + + {:error, _} -> + ctx + |> Message.text("Failed to load photo") + |> Message.send(chat_id) +end + +# Document with error handling +case Document.path(ctx, "/path/to/file.pdf") do + {:ok, ctx} -> ctx |> Document.send(chat_id) + {:error, _} -> Message.text(ctx, "Failed to load document") |> Message.send(chat_id) +end + +# Video with error handling +case Video.path(ctx, "/path/to/video.mp4") do + {:ok, ctx} -> ctx |> Video.duration(120) |> Video.send(chat_id) + {:error, _} -> Message.text(ctx, "Failed to load video") |> Message.send(chat_id) +end ``` ### Routers for Code Organization @@ -186,6 +199,18 @@ defmodule MyBot do end ``` +## Error Handling + +File operations (`path/2`, `cover_path/2`) return `{:ok, ctx}` on success +or `{:error, reason}` on error. Always handle both cases: + +```elixir +case Photo.path(ctx, "missing.jpg") do + {:ok, ctx} -> ctx |> Photo.send(chat_id) + {:error, :enoent} -> Message.text(ctx, "File not found") |> Message.send(chat_id) + {:error, :eacces} -> Message.text(ctx, "Permission denied") |> Message.send(chat_id) +end + ## Documentation For detailed documentation, see [HexDocs](https://hexdocs.pm/telegram_ex). diff --git a/example/README.md b/example/README.md index 9306c6b..ea837b0 100644 --- a/example/README.md +++ b/example/README.md @@ -31,3 +31,5 @@ A demo bot that showcases every feature of the [TelegramEx](https://github.com/l ```bash TOKEN=your_bot_token mix run --no-halt ``` + +You can use proxy through the environment variables HTTP_PROXY, HTTPS_PROXY, ALL_PROXY. diff --git a/example/lib/bot.ex b/example/lib/bot.ex index 9cdfc95..b55c2f4 100644 --- a/example/lib/bot.ex +++ b/example/lib/bot.ex @@ -71,12 +71,9 @@ defmodule Example.Bot do # ── /markdown ────────────────────────────────────────────────────── def handle_message(%{text: "/markdown", chat: chat}, ctx) do md = """ - *Bold text* - _Italic text_ - `Inline code` - ``` - Code block - ``` + *Bold text* + _Italic text_ + `Inline code` [TelegramEx on GitHub](https://github.com/lsdrfrx/telegram_ex) """ @@ -151,28 +148,69 @@ defmodule Example.Bot do end # ── /document ────────────────────────────────────────────────────── - # Document from local file + # Document from local file with error handling def handle_message(%{text: "/document", chat: chat}, ctx) do - ctx - |> Document.path("mix.exs") - |> Document.caption("This bot's `mix.exs` sent as a document", "Markdown") - |> Document.send(chat["id"]) + case Document.path(ctx, "mix.exs") do + {:ok, ctx} -> + ctx + |> Document.caption("This bot's `mix.exs` sent as a document", "Markdown") + |> Document.send(chat["id"]) + + {:error, reason} -> + error_text = + case reason do + :enoent -> "File not found: mix.exs" + :eacces -> "Permission denied" + _ -> "Failed to load document" + end + + ctx + |> Message.text(error_text) + |> Message.send(chat["id"]) + end end # ── /sticker ─────────────────────────────────────────────────────── - # Sticker from local file + # Sticker from local file with error handling def handle_message(%{text: "/sticker", chat: chat}, ctx) do - ctx - |> Sticker.path("assets/sticker.webp") - |> Sticker.send(chat["id"]) + case Sticker.path(ctx, "assets/sticker.webp") do + {:ok, ctx} -> + ctx + |> Sticker.send(chat["id"]) + + {:error, reason} -> + error_text = + case reason do + :enoent -> "Sticker not found. Place a sticker.webp in assets/" + :eacces -> "Permission denied" + _ -> "Failed to load sticker" + end + + ctx + |> Message.text(error_text) + |> Message.send(chat["id"]) + end end # ── /video ───────────────────────────────────────────────────────── - # Video from local file def handle_message(%{text: "/video", chat: chat}, ctx) do - ctx - |> Video.path("assets/video.mp4") - |> Video.send(chat["id"]) + case Video.path(ctx, "assets/video.mp4") do + {:ok, ctx} -> + ctx + |> Video.send(chat["id"]) + + {:error, reason} -> + error_text = + case reason do + :enoent -> "Video not found. Place a video.mp4 in assets/" + :eacces -> "Permission denied" + _ -> "Failed to load video" + end + + ctx + |> Message.text(error_text) + |> Message.send(chat["id"]) + end end # ── /location ────────────────────────────────────────────────────── @@ -220,46 +258,6 @@ defmodule Example.Bot do {:transition, :survey_name, %{}} end - # ── Callback queries ─────────────────────────────────────────────── - - def handle_callback(%{data: "vote_like", message: %{chat: chat}} = cb, ctx) do - ctx - |> Message.text("👍 You liked it!") - |> Message.answer_callback_query(cb) - |> Message.send(chat["id"]) - end - - def handle_callback(%{data: "vote_dislike", message: %{chat: chat}} = cb, ctx) do - ctx - |> Message.text("👎 You disliked it!") - |> Message.answer_callback_query(cb) - |> Message.send(chat["id"]) - end - - def handle_callback(%{data: "info", message: %{chat: chat}} = cb, ctx) do - ctx - |> Message.text( - "ℹ️ This bot demonstrates all TelegramEx features:\nBuilders, keyboards, FSM, routers, callbacks." - ) - |> Message.answer_callback_query(cb) - |> Message.send(chat["id"]) - end - - def handle_callback(%{data: "cancel", message: %{chat: chat}} = cb, ctx) do - ctx - |> Message.text("❌ Action cancelled.") - |> Message.answer_callback_query(cb) - |> Message.send(chat["id"]) - end - - # ── Reply keyboard echo ─────────────────────────────────────────── - def handle_message(%{text: "Option " <> letter, chat: chat}, ctx) - when letter in ["A", "B", "C"] do - ctx - |> Message.text("You selected: *Option #{letter}*", "Markdown") - |> Message.send(chat["id"]) - end - # ── /poll ────────────────────────────────────────────────────────── # Regular poll with multiple answers def handle_message(%{text: "/poll", chat: chat}, ctx) do @@ -291,4 +289,44 @@ defmodule Example.Bot do ) |> Poll.send(chat["id"]) end + + # ── Reply keyboard echo ─────────────────────────────────────────── + def handle_message(%{text: "Option " <> letter, chat: chat}, ctx) + when letter in ["A", "B", "C"] do + ctx + |> Message.text("You selected: *Option #{letter}*", "Markdown") + |> Message.send(chat["id"]) + end + + # ── Callback queries ─────────────────────────────────────────────── + + def handle_callback(%{data: "vote_like", message: %{chat: chat}} = cb, ctx) do + ctx + |> Message.text("👍 You liked it!") + |> Message.answer_callback_query(cb) + |> Message.send(chat["id"]) + end + + def handle_callback(%{data: "vote_dislike", message: %{chat: chat}} = cb, ctx) do + ctx + |> Message.text("👎 You disliked it!") + |> Message.answer_callback_query(cb) + |> Message.send(chat["id"]) + end + + def handle_callback(%{data: "info", message: %{chat: chat}} = cb, ctx) do + ctx + |> Message.text( + "ℹ️ This bot demonstrates all TelegramEx features:\nBuilders, keyboards, FSM, routers, callbacks." + ) + |> Message.answer_callback_query(cb) + |> Message.send(chat["id"]) + end + + def handle_callback(%{data: "cancel", message: %{chat: chat}} = cb, ctx) do + ctx + |> Message.text("❌ Action cancelled.") + |> Message.answer_callback_query(cb) + |> Message.send(chat["id"]) + end end diff --git a/lib/builders/document.ex b/lib/builders/document.ex index 0cd2db0..df33e9f 100644 --- a/lib/builders/document.ex +++ b/lib/builders/document.ex @@ -58,16 +58,35 @@ defmodule TelegramEx.Builder.Document do ## Returns - Updated context map with document file content set. + - `{:ok, updated_ctx}` - Document loaded successfully + - `{:error, reason}` - Failed to read file + + ## Examples + + case Document.path(ctx, "/path/to/file.pdf") do + {:ok, ctx} -> ctx |> Document.send(chat_id) + {:error, :enoent} -> Message.text(ctx, "File not found") |> Message.send(chat_id) + {:error, _} -> Message.text(ctx, "Failed to load document") |> Message.send(chat_id) + end """ - @spec path(map(), String.t()) :: map() + @spec path(map(), String.t()) :: {:ok, map()} | {:error, atom()} def path(ctx, path) do - filename = Path.basename(path) - content = File.read!(path) - - Map.get(ctx, :payload, %{}) - |> Map.put(:document, {content, filename: filename, content_type: MimeType.from_path(path)}) - |> then(&Map.put(ctx, :payload, &1)) + case(File.read(path)) do + {:ok, content} -> + filename = Path.basename(path) + + updated_payload = + Map.get(ctx, :payload, %{}) + |> Map.put( + :document, + {content, filename: filename, content_type: MimeType.from_path(path)} + ) + + {:ok, Map.put(ctx, :payload, updated_payload)} + + {:error, reason} -> + {:error, reason} + end end @doc """ diff --git a/lib/builders/photo.ex b/lib/builders/photo.ex index e8a6798..4573f75 100644 --- a/lib/builders/photo.ex +++ b/lib/builders/photo.ex @@ -64,22 +64,35 @@ defmodule TelegramEx.Builder.Photo do ## Returns - Updated context map with photo file content set. + - `{:ok, updated_ctx}` - Photo loaded successfully + - `{:error, reason}` - Failed to read file (e.g., `:enoent`, `:eacces`) ## Examples - ctx - |> Photo.path("/tmp/photo.jpg") - |> Photo.send(chat_id) + case Photo.path(ctx, "/tmp/photo.jpg") do + {:ok, ctx} -> ctx |> Photo.send(chat_id) + {:error, :enoent} -> Message.text(ctx, "File not found") |> Message.send(chat_id) + {:error, _} -> Message.text(ctx, "Failed to load photo") |> Message.send(chat_id) + end """ - @spec path(map(), String.t()) :: map() + @spec path(map(), String.t()) :: {:ok, map()} | {:error, atom()} def path(ctx, path) do - filename = Path.basename(path) - content = File.read!(path) - - Map.get(ctx, :payload, %{}) - |> Map.put(:photo, {content, filename: filename, content_type: MimeType.from_path(path)}) - |> then(&Map.put(ctx, :payload, &1)) + case File.read(path) do + {:ok, content} -> + filename = Path.basename(path) + + updated_payload = + Map.get(ctx, :payload, %{}) + |> Map.put( + :photo, + {content, filename: filename, content_type: MimeType.from_path(path)} + ) + + {:ok, Map.put(ctx, :payload, updated_payload)} + + {:error, reason} -> + {:error, reason} + end end @doc """ diff --git a/lib/builders/sticker.ex b/lib/builders/sticker.ex index f7595c3..4501f8a 100644 --- a/lib/builders/sticker.ex +++ b/lib/builders/sticker.ex @@ -74,16 +74,34 @@ defmodule TelegramEx.Builder.Sticker do ## Returns - Updated context map with sticker file content set. + - `{:ok, updated_ctx}` - Sticker loaded successfully + - `{:error, reason}` - Failed to read file + + ## Examples + + case Sticker.path(ctx, "/tmp/sticker.webp") do + {:ok, ctx} -> ctx |> Sticker.send(chat_id) + {:error, _} -> Message.text(ctx, "Failed to load sticker") |> Message.send(chat_id) + end """ - @spec path(map(), String.t()) :: map() + @spec path(map(), String.t()) :: {:ok, map()} | {:error, atom()} def path(ctx, path) do - filename = Path.basename(path) - content = File.read!(path) - - Map.get(ctx, :payload, %{}) - |> Map.put(:sticker, {content, filename: filename, content_type: MimeType.from_path(path)}) - |> then(&Map.put(ctx, :payload, &1)) + case File.read(path) do + {:ok, content} -> + filename = Path.basename(path) + + updated_payload = + Map.get(ctx, :payload, %{}) + |> Map.put( + :sticker, + {content, filename: filename, content_type: MimeType.from_path(path)} + ) + + {:ok, Map.put(ctx, :payload, updated_payload)} + + {:error, reason} -> + {:error, reason} + end end @doc """ diff --git a/lib/builders/video.ex b/lib/builders/video.ex index 2f0b3cb..32b73f8 100644 --- a/lib/builders/video.ex +++ b/lib/builders/video.ex @@ -76,16 +76,34 @@ defmodule TelegramEx.Builder.Video do ## Returns - Updated context map with video file content set. + - `{:ok, updated_ctx}` - Video loaded successfully + - `{:error, reason}` - Failed to read file + + ## Examples + + case Video.path(ctx, "/tmp/video.mp4") do + {:ok, ctx} -> ctx |> Video.send(chat_id) + {:error, _} -> Message.text(ctx, "Failed to load video") |> Message.send(chat_id) + end """ - @spec path(map(), String.t()) :: map() + @spec path(map(), String.t()) :: {:ok, map()} | {:error, atom()} def path(ctx, path) do - filename = Path.basename(path) - content = File.read!(path) - - Map.get(ctx, :payload, %{}) - |> Map.put(:video, {content, filename: filename, content_type: MimeType.from_path(path)}) - |> then(&Map.put(ctx, :payload, &1)) + case File.read(path) do + {:ok, content} -> + filename = Path.basename(path) + + updated_payload = + Map.get(ctx, :payload, %{}) + |> Map.put( + :video, + {content, filename: filename, content_type: MimeType.from_path(path)} + ) + + {:ok, Map.put(ctx, :payload, updated_payload)} + + {:error, reason} -> + {:error, reason} + end end @doc """ @@ -117,16 +135,34 @@ defmodule TelegramEx.Builder.Video do ## Returns - Updated context map with cover image content set. + - `{:ok, updated_ctx}` - Cover image loaded successfully + - `{:error, reason}` - Failed to read file + + ## Examples + + case Video.cover_path(ctx, "/tmp/cover.jpg") do + {:ok, ctx} -> ctx |> Video.send(chat_id) + {:error, _} -> Message.text(ctx, "Failed to load cover") |> Message.send(chat_id) + end """ - @spec cover_path(map(), String.t()) :: map() + @spec cover_path(map(), String.t()) :: {:ok, map()} | {:error, atom()} def cover_path(ctx, path) do - filename = Path.basename(path) - content = File.read!(path) - - Map.get(ctx, :payload, %{}) - |> Map.put(:cover, {content, filename: filename, content_type: MimeType.from_path(path)}) - |> then(&Map.put(ctx, :payload, &1)) + case File.read(path) do + {:ok, content} -> + filename = Path.basename(path) + + updated_payload = + Map.get(ctx, :payload, %{}) + |> Map.put( + :cover, + {content, filename: filename, content_type: MimeType.from_path(path)} + ) + + {:ok, Map.put(ctx, :payload, updated_payload)} + + {:error, reason} -> + {:error, reason} + end end @doc """ diff --git a/lib/server.ex b/lib/server.ex index 9fe95dd..014bd59 100644 --- a/lib/server.ex +++ b/lib/server.ex @@ -158,12 +158,14 @@ defmodule TelegramEx.Server do defp handle_result({:stay, data}, bot_name, chat_id, state), do: FSM.set_state(bot_name, chat_id, state, data) - defp handle_result(:ok, _bot_name, _chat_id, _state), do: :ok - defp handle_result(:pass, _bot_name, _chat_id, _state), do: :ok + defp handle_result({:ok, _ctx}, _bot_name, _chat_id, _state), do: :ok defp handle_result({:error, reason}, _bot_name, _chat_id, _state), do: Logger.error("Handler error: #{inspect(reason)}") + defp handle_result(:ok, _bot_name, _chat_id, _state), do: :ok + defp handle_result(:pass, _bot_name, _chat_id, _state), do: :ok + defp handle_result(error, _bot_name, _chat_id, _state), do: Logger.error("Unknown handler response: #{inspect(error)}")