Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 41 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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).
Expand Down
2 changes: 2 additions & 0 deletions example/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
156 changes: 97 additions & 59 deletions example/lib/bot.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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)
"""

Expand Down Expand Up @@ -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 ──────────────────────────────────────────────────────
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
35 changes: 27 additions & 8 deletions lib/builders/document.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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 """
Expand Down
35 changes: 24 additions & 11 deletions lib/builders/photo.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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 """
Expand Down
Loading