Telega
A Gleam library for the Telegram Bot API on BEAM.
It provides
- an interface to the Telegram Bot HTTP-based APIs
telega/api - a client for the Telegram Bot API
telega/client - OTP supervision tree for all bot processes (bot actor, chat instances, polling)
- adapter to use with wisp
- long polling with automatic retry and exponential backoff
- session bot implementation
- conversation implementation (multi-message flows)
- declarative single-message dialogs with widgets and sub-dialogs (
telega/dialog, guide) - per-user flood control middleware (
router.with_rate_limit) - inline mode result builders with pagination (
telega/inline_mode) - payments helpers — Telegram Stars first-class (
telega/payments) - paid media —
sendPaidMediaviaapi.send_paid_media/reply.with_paid_media - production observability via telemetry events (
telega/telemetry) - graceful shutdown via
telega.shutdown()
Quick start
If you are new to Telegram bots, read the official Introduction for Developers written by the Telegram team.
First, visit @BotFather to create a new bot. Copy the token and save it for later.
Initiate a gleam project and add telega as a dependency:
$ gleam new first_tg_bot
$ cd first_tg_bot
$ gleam add telega gleam_erlang telega_httpc
Replace the first_tg_bot.gleam file content with the following code:
import gleam/erlang/process
import telega
import telega/reply
import telega/router
import telega/update
import telega_httpc
fn handle_text(ctx, text) {
use ctx <- telega.log_context(ctx, "echo_text")
let assert Ok(_) = reply.with_text(ctx, text)
Ok(ctx)
}
fn handle_command(ctx, command: update.Command) {
use ctx <- telega.log_context(ctx, "echo_command")
let assert Ok(_) = reply.with_text(ctx, "Command: " <> command.text)
Ok(ctx)
}
pub fn main() {
let router = router.new("echo_bot")
|> router.on_any_text(handle_text)
|> router.on_commands(["start", "help"], handle_command)
let client =
telega_httpc.new("BOT_TOKEN")
let assert Ok(_bot) =
telega.new_for_polling(api_client: client)
|> telega.with_router(router)
|> telega.init_for_polling_nil_session()
process.sleep_forever()
}
Replace "BOT_TOKEN" with the token you received from the BotFather. Then run the bot:
$ gleam run
And it will echo all received text messages.
Congratulations! You just wrote a Telegram bot :)
Architecture
Calling telega.init_for_polling() (or telega.init() for webhooks) starts an OTP supervision tree:
TelegaRootSupervisor (OneForOne)
├── ChatInstances (factory_supervisor, Transient children)
│ ├── ChatInstance {chat1:user1}
│ ├── ChatInstance {chat2:user2}
│ └── ...
├── Bot actor (Permanent)
└── Polling worker (Permanent) — only in polling mode
- Bot actor — dispatches incoming updates to the correct
ChatInstanceby{chat_id}:{from_id}key. - ChatInstance — one per user-chat combination; holds session state and conversation continuations. Transient restart strategy means it restarts only on abnormal exit and re-registers itself in the ETS registry automatically.
- Polling worker — long-polls the Telegram API with exponential backoff on errors.
Each telega.init* call creates an independent tree with its own ETS registry, so multiple bot instances don't conflict.
Running under your own supervision tree
telega.supervised (webhook) and telega.supervised_for_polling (polling) wrap the corresponding init into a ChildSpecification, so the bot's tree becomes a child of your application's supervisor — a crashed bot is re-initialized by your tree, and ordering against the resources it needs (a database pool, caches) is expressed as child order:
import gleam/otp/static_supervisor as supervisor
let assert Ok(_) =
supervisor.new(supervisor.RestForOne)
|> supervisor.add(db_pool_child)
|> supervisor.add(
telega.new_for_polling(api_client:)
|> telega.with_router(router)
|> telega.with_nil_session()
|> telega.supervised_for_polling(),
)
|> supervisor.start
Supervisors don't hand child data back; when you need the Telega instance outside the tree (webhook adapters, manual shutdown), capture it from the with_on_start hook — see the telega.supervised documentation for the pattern.
Graceful shutdown
telega.shutdown(bot)
Sends an OTP shutdown signal to the root supervisor, which stops children in reverse start order (polling → bot → chat factory).
Dependency injection
Handlers reach shared services — a database pool, an HTTP client, an i18n catalog — through the typed, non-persisted dependencies slot on Context. It is set once at init and is never serialized, unlike session (which holds per-user state):
pub type Dependencies {
Dependencies(db: Connection, catalog: Catalog)
}
telega.new_for_polling_with_dependencies(api_client: client, dependencies: Dependencies(db:, catalog:))
|> telega.with_router(router)
|> telega.init_for_polling()
// in any handler / flow step / middleware:
fn my_bookings(ctx: Context(Nil, String, Dependencies), _cmd) {
let bookings = db.list_bookings(ctx.dependencies.db, ctx.update.from_id)
reply.with_text(ctx, format_bookings(bookings))
}
Bots that need no services pay nothing: dependencies defaults to Nil. See the Dependency injection guide.
Testing
Telega ships with a testing toolkit under telega/testing/ — mock clients, data factories, and a declarative conversation DSL. No real Telegram API calls needed.
import telega/testing/conversation
pub fn greeting_flow_test() {
conversation.conversation_test()
|> conversation.send("/start")
|> conversation.expect_reply_containing("Hello")
|> conversation.send("Alice")
|> conversation.expect_reply_containing("Alice")
|> conversation.run(build_router(), fn() { MySession(name: "") })
}
See the full Testing guide for handler isolation, mock clients, media assertions, and more.
Ecosystem
Telega is a monorepo. The core telega package is HTTP-client- and storage-agnostic; pick the adapters you need:
| Package | Purpose |
|---|---|
telega_wisp | Wisp webhook adapter (endpoint handling, secret-token validation) |
telega_mist | Minimal webhook adapter directly over mist, without wisp |
telega_httpc | HTTP client adapter over Erlang httpc |
telega_hackney | HTTP client adapter over hackney |
telega_storage_postgres | PostgreSQL session/flow storage adapter |
telega_storage_sqlite | SQLite session/flow storage adapter |
telega_storage_redis | Redis/Valkey session/flow storage adapter |
telega_webapp | Telegram Mini Apps (Web App) initData validation and helpers |
telega_i18n | Internationalization: TOML/JSON catalogs, locale middleware, interpolation, CLDR pluralization |
Examples
Progressive examples in the examples directory:
00-echo-bot— Basic echo with long polling01-commands-bot— Command handling02-session-bot— Stateful sessions03-conversation-bot— Multi-message conversations04-keyboard-bot— Inline keyboards and callbacks05-media-group-bot— Media group handling06-restaurant-booking— Full-featured application with flows and database
Development
gleam build # Build the project
gleam test # Run the tests
gleam format # Format code
gleam shell # Run an Erlang shell