# Low-Level API The low-level API provides direct access to all Telegram Bot API methods without the DSL framework. This is useful for: - **Complex applications** with background tasks or scheduled jobs - **Fine-grained control** over requests and responses - **Library usage** without the bot framework - **Direct integration** with other systems ## How It Works ExGram's low-level API is **automatically generated** from an up-to-date JSON description of the Telegram Bot API using the [telegram_api_json](https://github.com/rockneurotiko/telegram_api_json) project. ### The Generation Process 1. The `telegram_api_json` project scrapes the [Telegram Bot API documentation](https://core.telegram.org/bots/api) 2. It produces a standardized JSON file with all methods, parameters, and models 3. A Python script ([`extractor.py`](../extractor.py)) reads this JSON and generates Elixir code in [`lib/ex_gram.ex`](../lib/ex_gram.ex) 4. As a result, `ExGram` have all the available methods and `ExGram.Model.*` all the models, both with proper typespecs and documentation **Result:** Every method has correct type specs and documentation, making the API a pleasure to use! ## Models All models are in the `ExGram.Model` module and match the [Telegram Bot API documentation](https://core.telegram.org/bots/api) one-to-one. ### Using Models ```elixir alias ExGram.Model.{User, Message, Chat, Update} # Models have correct typespecs @spec get_user_name(User.t()) :: String.t() def get_user_name(%User{first_name: first, last_name: last}) do "#{first} #{last || ""}" end ``` ### Inspecting Model Types In IEx, you can view model types: ```elixir iex> t ExGram.Model.User @type t() :: %ExGram.Model.User{ first_name: String.t(), id: integer(), is_bot: boolean(), language_code: String.t(), last_name: String.t(), username: String.t() } ``` ## Methods All methods are in the `ExGram` module and follow these conventions: ### Naming Convention Telegram's `camelCase` to Elixir's `snake_case` - `sendMessage` to `ExGram.send_message/3` - `getUpdates` to `ExGram.get_updates/1` - `answerCallbackQuery` to `ExGram.answer_callback_query/2` ### Method Signatures **Mandatory parameters** → Function arguments (in order from docs) **Optional parameters** → Keyword list in last argument ```elixir # sendMessage has 2 mandatory params: chat_id, text ExGram.send_message(chat_id, text) ExGram.send_message(chat_id, text, parse_mode: "Markdown") # getUpdates has 0 mandatory params, 4 optional ExGram.get_updates() ExGram.get_updates(offset: 123, limit: 100) ``` ### Return Values Methods return `{:ok, result} | {:error, ExGram.Error.t()}`: ```elixir case ExGram.send_message(chat_id, "Hello!") do {:ok, %ExGram.Model.Message{} = message} -> IO.puts("Message sent! ID: #{message.message_id}") {:error, %ExGram.Error{reason: reason}} -> Logger.error("Failed to send message: #{inspect(reason)}") end ``` ### Bang Methods (!) Every method has a `!` variant that returns the result directly or raises: ```elixir # Safe version {:ok, message} = ExGram.send_message(chat_id, "Hello") # Bang version - returns result or raises message = ExGram.send_message!(chat_id, "Hello") ``` Use bang methods when you're confident the operation will succeed or when you want to let it fail. ## Method Documentation View method documentation in IEx with `h`: ```elixir iex> h ExGram.send_message def send_message(chat_id, text, ops \\ []) @spec send_message( chat_id :: integer() | String.t(), text :: String.t(), ops :: [ parse_mode: String.t(), entities: [ExGram.Model.MessageEntity.t()], disable_web_page_preview: boolean(), disable_notification: boolean(), protect_content: boolean(), reply_to_message_id: integer(), allow_sending_without_reply: boolean(), reply_markup: ExGram.Model.InlineKeyboardMarkup.t() | ExGram.Model.ReplyKeyboardMarkup.t() | ExGram.Model.ReplyKeyboardRemove.t() | ExGram.Model.ForceReply.t() ] ) :: {:ok, ExGram.Model.Message.t()} | {:error, ExGram.Error.t()} ``` ## Extra Options All methods support three extra options: ### `bot` - Use named bot ```elixir ExGram.send_message(chat_id, "Hello", bot: :my_bot) ``` The bot name has to match to a defined AND started bot, the name is the one you write in `use ExGram.Bot, name: :my_bot`, and you can always get the name of a bot from the module with `MyBot.name/0` **Note:** Only use **one** of `token` or `bot`, not both. ### `token` - Use specific token ```elixir ExGram.send_message("@channel", "Update!", token: "BOT_TOKEN") ``` ### `debug` - Print HTTP response ```elixir ExGram.get_me(debug: true) # 16:37:49.397 [info] Path: "/bot/getMe" body: %{} ``` > #### Warning {: .warning } > Do not use this in production, it will log your bot's tokens ## Common Use Cases ### Sending Messages from Background Tasks ```elixir defmodule MyApp.Scheduler do def send_daily_report do users = MyApp.Users.get_subscribed_users() Enum.each(users, fn user -> message = generate_report(user) ExGram.send_message(user.telegram_id, message, bot: :my_bot) end) end end ``` ### Sending to Channels ```elixir # No bot token in config ExGram.send_message("@my_channel", "Update!", token: System.get_env("BOT_TOKEN")) ``` ### Working with not supported on the DSL ```elixir # By file ID {:ok, message} = ExGram.send_photo(chat_id, "BQACAgIAAxkBAAI...") # By local path {:ok, message} = ExGram.send_photo(chat_id, {:file, "priv/image.png"}) # By content image_stream = generate_image_stream() {:ok, message} = ExGram.send_photo(chat_id, {:file_content, image_stream, "image.png"}) ``` ### Pinning Messages ```elixir {:ok, %{message_id: msg_id}} = ExGram.send_message(chat_id, "Important!") ExGram.pin_chat_message(chat_id, msg_id) ``` ### Getting Bot Info ```elixir {:ok, %ExGram.Model.User{username: username}} = ExGram.get_me(bot: :my_bot) IO.puts("Bot username: @#{username}") ``` ## Using Without the Framework You can use ExGram purely as a library without the bot framework: ```elixir # config/config.exs config :ex_gram, token: "YOUR_BOT_TOKEN", adapter: ExGram.Adapter.Req # lib/my_app/application.ex # No need to add ExGram to supervision tree # lib/my_app.ex defmodule MyApp do def notify_users(message) do users = get_users() Enum.each(users, fn user -> ExGram.send_message(user.telegram_id, message) end) end end ``` Or without any config: ```elixir ExGram.send_message("@channel", "Update!", token: "BOT_TOKEN") ``` ## Complete Example: Notification System ```elixir defmodule MyApp.Notifications do # You can still use the ExGram.Dsl.* if you want, they are independent import ExGram.Dsl.Keyboard alias ExGram.Dsl.MessageEntityBuilder, as: B alias ExGram.Model.{InlineKeyboardMarkup, InlineKeyboardButton} def send_notification(user_id, type, data) do {message, entities} = format_message(type, data) keyboard = build_keyboard(type, data) ExGram.send_message(user_id, message, reply_markup: keyboard, entities: entities, bot: :my_bot) end defp format_message(:order_shipped, %{order_id: id, tracking: tracking}) do header = B.join(["📦", B.bold("Order Shipped!")]) order = B.join([ B.join([B.bold("Order"), B.code("##{id}"), "has been shipped"]), B.join([B.bold("Tracking:"), B.url(tracking)]) ], "\n") B.join([header, order], "\n\n") end defp build_keyboard(:order_shipped, %{tracking_url: url}) do keyboard :inline do row do button "Track Package", url: url end end end end ``` ## Next Steps - [Multiple Bots](multiple-bots.md) - Using `bot:` option effectively - [Testing](testing.md) - Test adapter for low-level API calls