telega

Types

pub opaque type Telega(session, error, dependencies)
pub opaque type TelegaBuilder(session, error, dependencies)

Values

pub fn get_api_config(
  telega: Telega(session, error, dependencies),
) -> client.TelegramClient

Helper to get the config for API requests.

pub fn get_dependencies(
  ctx: bot.Context(session, error, dependencies),
) -> dependencies

Get the injected dependencies (services) for the current context.

dependencies is set once at bot init via with_dependencies and is never persisted. See the session vs dependencies distinction in with_dependencies.

pub fn get_me(
  telega: Telega(session, error, dependencies),
) -> types.User

Get the bot’s information.

pub fn get_session(
  ctx: bot.Context(session, error, dependencies),
) -> session

Get session for the current context.

pub fn get_supervisor_pid(
  telega: Telega(session, error, dependencies),
) -> process.Pid

Get the supervisor PID for the running bot instance.

pub fn handle_update(
  telega: Telega(session, error, dependencies),
  raw_update: types.Update,
) -> Bool

Handle an update.

This function is useful when you want to handle updates in your own way.

pub fn init(
  builder: TelegaBuilder(session, error, dependencies),
) -> Result(
  Telega(session, error, dependencies),
  error.TelegaError,
)

Initialize the bot for webhook mode with a supervision tree.

pub fn init_for_polling(
  builder: TelegaBuilder(session, error, dependencies),
) -> Result(
  Telega(session, error, dependencies),
  error.TelegaError,
)

Initialize the bot for long polling with a supervision tree. Includes a supervised polling worker that auto-starts.

pub fn init_for_polling_nil_session(
  builder: TelegaBuilder(Nil, error, dependencies),
) -> Result(Telega(Nil, error, dependencies), error.TelegaError)

Initialize the bot for long polling with nil session.

pub fn is_draining(
  telega: Telega(session, error, dependencies),
) -> Bool

Whether the bot is currently draining and no longer accepting updates.

Webhook adapters should answer 503 when this is True so Telegram retries the update after the deploy instead of it being dropped.

pub fn is_secret_token_valid(
  telega: Telega(session, error, dependencies),
  token: String,
) -> Bool

Check if a secret token is valid.

Useful if you plan to implement own adapter.

pub fn is_webhook_path(
  telega: Telega(session, error, dependencies),
  path: String,
) -> Bool

Check if a path is the webhook path for the bot.

Useful if you plan to implement own adapter.

pub fn log_context(
  ctx: bot.Context(session, error, dependencies),
  prefix: String,
  fun: fn(bot.Context(session, error, dependencies)) -> Result(
    bot.Context(session, error, dependencies),
    error,
  ),
) -> Result(bot.Context(session, error, dependencies), error)

Add logging context to the current context.

pub fn log_error(
  ctx: bot.Context(session, error, dependencies),
  message: String,
) -> Nil
pub fn log_info(
  ctx: bot.Context(session, error, dependencies),
  message: String,
) -> Nil

Context helpers for logging

pub fn new(
  api_client api_client: client.TelegramClient,
  url server_url: String,
  webhook_path webhook_path: String,
  secret_token secret_token: option.Option(String),
) -> TelegaBuilder(session, error, Nil)

Create a new Telega instance with no injected dependencies (dependencies is Nil).

Requires an api_client created by an adapter package like telega_httpc or telega_hackney. To inject services, prefer new_with_dependencies — it fixes the dependencies type up front and avoids the field-reset footgun of with_dependencies (see with_dependencies).

pub fn new_for_polling(
  api_client api_client: client.TelegramClient,
) -> TelegaBuilder(session, error, Nil)

Create a new Telega instance optimized for long polling, with no injected dependencies (dependencies is Nil).

Requires an api_client created by an adapter package like telega_httpc or telega_hackney. This is a convenience function for polling bots that don’t need webhook configuration. To inject services, prefer new_for_polling_with_dependencies.

pub fn new_for_polling_with_dependencies(
  api_client api_client: client.TelegramClient,
  dependencies dependencies: dependencies,
) -> TelegaBuilder(session, error, dependencies)

Like new_for_polling, but injects dependencies (services) at construction.

Preferred over new_for_polling + with_dependencies: the dependencies type is fixed up front, so the builder steps that follow are never reset (see with_dependencies).

telega.new_for_polling_with_dependencies(api_client:, dependencies: Dependencies(db:, catalog:))
|> telega.with_router(router)
|> telega.init_for_polling()
pub fn new_with_dependencies(
  api_client api_client: client.TelegramClient,
  url server_url: String,
  webhook_path webhook_path: String,
  secret_token secret_token: option.Option(String),
  dependencies dependencies: dependencies,
) -> TelegaBuilder(session, error, dependencies)

Like new, but injects dependencies (services) at construction.

This is the safest way to use dependency injection: the builder’s dependencies type is fixed from the start, so with_router/with_catch_handler/on_start can be called in any order without being reset. See with_dependencies for the session vs dependencies distinction.

telega.new_with_dependencies(api_client:, url:, webhook_path:, secret_token:, dependencies: Dependencies(db:, catalog:))
|> telega.with_router(router)
|> telega.init()
pub fn set_allowed_updates(
  builder: TelegaBuilder(session, error, dependencies),
  updates: List(String),
) -> TelegaBuilder(session, error, dependencies)

Set allowed updates for webhook.

pub fn set_api_client(
  builder: TelegaBuilder(session, error, dependencies),
  client: client.TelegramClient,
) -> TelegaBuilder(session, error, dependencies)

Set a custom API client.

pub fn set_certificate(
  builder: TelegaBuilder(session, error, dependencies),
  cert: types.File,
) -> TelegaBuilder(session, error, dependencies)

Set certificate for webhook.

pub fn set_drop_pending_updates(
  builder: TelegaBuilder(session, error, dependencies),
  drop: Bool,
) -> TelegaBuilder(session, error, dependencies)

Set whether to drop pending updates.

pub fn set_ip_address(
  builder: TelegaBuilder(session, error, dependencies),
  ip: String,
) -> TelegaBuilder(session, error, dependencies)

Set IP address for webhook.

pub fn set_max_connections(
  builder: TelegaBuilder(session, error, dependencies),
  max: Int,
) -> TelegaBuilder(session, error, dependencies)

Set max connections for webhook.

pub fn shutdown(
  telega: Telega(session, error, dependencies),
) -> Nil

Graceful shutdown with in-flight draining.

  1. Emits [telega, shutdown, start].
  2. Stops intake — for polling, tells the worker to stop fetching updates (Telegram re-delivers unconfirmed updates on the next start); for webhook, the bot starts rejecting updates and is_draining reports True so adapters can answer 503.
  3. Waits up to drain_timeout for in-flight updates to finish.
  4. Runs the on_shutdown hook.
  5. Emits [telega, shutdown, stop] with the number of drained updates.
  6. Stops the supervisor, cascading to all children (polling → bot → chat_factory).
pub fn start_polling_default(
  telega: Telega(session, error, dependencies),
) -> Result(polling.Poller, error.TelegaError)

Start polling with default configuration for a Telega instance. This is useful when you want to manually start polling outside the supervision tree.

pub fn use_pre_handler(
  builder: TelegaBuilder(session, error, dependencies),
  pre_handler: fn(bot.PreContext(dependencies)) -> bot.PreRouterResult,
) -> TelegaBuilder(session, error, dependencies)

Register a global pre-router middleware (bot.PreHandler).

Pre-router middleware runs once per update inside the bot actor, before routing and before any chat instance is spawned or session loaded. Use it for cross-cutting concerns that apply to every update: anti-spam, analytics, and update deduplication. Returning bot.Stop drops the update before routing; bot.Continue lets it through to the next pre-handler and the router. Handlers run in the order they are registered, and the first Stop short-circuits the rest. Because they all run sequentially in the single bot actor, read-then-write logic (like dedup) is race-free across updates.

// Drop updates from a banned chat before they reach any handler.
telega.new_for_polling(api_client:)
|> telega.use_pre_handler(fn(pre) {
  case pre.update.chat_id == banned_chat {
    True -> bot.Stop
    False -> bot.Continue
  }
})
|> telega.with_router(router)

// Webhook idempotency: drop updates Telegram re-delivers on retry.
|> telega.use_pre_handler(idempotency.deduplicate(storage:, ttl_ms: 3600_000))
pub fn wait_any(
  ctx ctx: bot.Context(session, error, dependencies),
  or handle_else: option.Option(
    bot.Handler(session, error, dependencies),
  ),
  timeout timeout: option.Option(Int),
  continue handler: fn(
    bot.Context(session, error, dependencies),
    update.Update,
  ) -> Result(bot.Context(session, error, dependencies), error),
) -> Result(bot.Context(session, error, dependencies), error)

Pauses the current chat actor’s handler and waits for any update. Other chats and users continue to be handled concurrently.

See conversation

pub fn wait_audio(
  ctx ctx: bot.Context(session, error, dependencies),
  or handle_else: option.Option(
    bot.Handler(session, error, dependencies),
  ),
  timeout timeout: option.Option(Int),
  continue continue: fn(
    bot.Context(session, error, dependencies),
    types.Audio,
  ) -> Result(bot.Context(session, error, dependencies), error),
) -> Result(bot.Context(session, error, dependencies), error)

Pauses the current chat actor’s handler and waits for an audio message. Other chats and users continue to be handled concurrently.

See conversation

pub fn wait_callback_query(
  ctx ctx: bot.Context(session, error, dependencies),
  filter filter: option.Option(bot.CallbackQueryFilter),
  or handle_else: option.Option(
    bot.Handler(session, error, dependencies),
  ),
  timeout timeout: option.Option(Int),
  continue continue: fn(
    bot.Context(session, error, dependencies),
    String,
    String,
  ) -> Result(bot.Context(session, error, dependencies), error),
) -> Result(bot.Context(session, error, dependencies), error)

Pauses the current chat actor’s handler and waits for a callback query. Other chats and users continue to be handled concurrently.

See conversation

pub fn wait_choice(
  ctx ctx: bot.Context(session, error, dependencies),
  options options: List(#(String, a)),
  or handle_else: option.Option(
    bot.Handler(session, error, dependencies),
  ),
  timeout timeout: option.Option(Int),
  continue continue: fn(
    bot.Context(session, error, dependencies),
    a,
  ) -> Result(bot.Context(session, error, dependencies), error),
) -> Result(bot.Context(session, error, dependencies), error)

Wait for user choice from inline keyboard.

This function creates an inline keyboard with provided options and waits for user to select one.

Examples

use ctx, color <- wait_choice(
  ctx,
  [
    #("🔴 Red", Red),
    #("🔵 Blue", Blue),
    #("🟢 Green", Green),
  ],
  or: None,
  timeout: None,
)

See conversation

pub fn wait_command(
  ctx ctx: bot.Context(session, error, dependencies),
  command command: String,
  or handle_else: option.Option(
    bot.Handler(session, error, dependencies),
  ),
  timeout timeout: option.Option(Int),
  continue continue: fn(
    bot.Context(session, error, dependencies),
    update.Command,
  ) -> Result(bot.Context(session, error, dependencies), error),
) -> Result(bot.Context(session, error, dependencies), error)

Pauses the current chat actor’s handler and waits for a specific command. Other chats and users continue to be handled concurrently.

See conversation

pub fn wait_commands(
  ctx ctx: bot.Context(session, error, dependencies),
  commands commands: List(String),
  or handle_else: option.Option(
    bot.Handler(session, error, dependencies),
  ),
  timeout timeout: option.Option(Int),
  continue continue: fn(
    bot.Context(session, error, dependencies),
    update.Command,
  ) -> Result(bot.Context(session, error, dependencies), error),
) -> Result(bot.Context(session, error, dependencies), error)

Pauses the current chat actor’s handler and waits for one of the specified commands. Other chats and users continue to be handled concurrently.

See conversation

pub fn wait_email(
  ctx ctx: bot.Context(session, error, dependencies),
  or handle_else: option.Option(
    bot.Handler(session, error, dependencies),
  ),
  timeout timeout: option.Option(Int),
  continue continue: fn(
    bot.Context(session, error, dependencies),
    String,
  ) -> Result(bot.Context(session, error, dependencies), error),
) -> Result(bot.Context(session, error, dependencies), error)

Wait for email with validation.

This function waits for user to send text that matches email pattern.

If validation fails and or handler is provided, it will be called. Otherwise, the function will keep waiting for valid input.

Examples

use ctx, email <- wait_email(
  ctx,
  or: Some(bot.HandleText(fn(ctx, invalid) {
    reply.with_text(ctx, "Invalid email format. Try again.")
  })),
  timeout: None,
)

See conversation

pub fn wait_for(
  ctx ctx: bot.Context(session, error, dependencies),
  filter filter: fn(update.Update) -> Bool,
  or handle_else: option.Option(
    bot.Handler(session, error, dependencies),
  ),
  timeout timeout: option.Option(Int),
  continue continue: fn(
    bot.Context(session, error, dependencies),
    update.Update,
  ) -> Result(bot.Context(session, error, dependencies), error),
) -> Result(bot.Context(session, error, dependencies), error)

Wait for update matching custom filter.

This function waits for any update that passes the provided filter function.

Examples

use ctx, photo_update <- wait_for(
  ctx,
  filter: fn(upd) {
    case upd {
      update.PhotoUpdate(..) -> True
      _ -> False
    }
  },
  or: Some(bot.HandleAll(fn(ctx, wrong_update) {
    reply.with_text(ctx, "Please send a photo")
  })),
  timeout: Some(60_000),
)

See conversation

pub fn wait_hears(
  ctx ctx: bot.Context(session, error, dependencies),
  hears hears: bot.Hears,
  or handle_else: option.Option(
    bot.Handler(session, error, dependencies),
  ),
  timeout timeout: option.Option(Int),
  continue continue: fn(
    bot.Context(session, error, dependencies),
    String,
  ) -> Result(bot.Context(session, error, dependencies), error),
) -> Result(bot.Context(session, error, dependencies), error)

Pauses the current chat actor’s handler and waits for a message that matches the given Hears. Other chats and users continue to be handled concurrently.

See conversation

pub fn wait_message(
  ctx ctx: bot.Context(session, error, dependencies),
  or handle_else: option.Option(
    bot.Handler(session, error, dependencies),
  ),
  timeout timeout: option.Option(Int),
  continue continue: fn(
    bot.Context(session, error, dependencies),
    types.Message,
  ) -> Result(bot.Context(session, error, dependencies), error),
) -> Result(bot.Context(session, error, dependencies), error)

Pauses the current chat actor’s handler and waits for any message. Other chats and users continue to be handled concurrently.

See conversation

pub fn wait_number(
  ctx ctx: bot.Context(session, error, dependencies),
  min min: option.Option(Int),
  max max: option.Option(Int),
  or handle_else: option.Option(
    bot.Handler(session, error, dependencies),
  ),
  timeout timeout: option.Option(Int),
  continue continue: fn(
    bot.Context(session, error, dependencies),
    Int,
  ) -> Result(bot.Context(session, error, dependencies), error),
) -> Result(bot.Context(session, error, dependencies), error)

Wait for a number with validation.

This function waits for user to send text that can be parsed as an integer, with optional min/max validation.

If validation fails and or handler is provided, it will be called. Otherwise, the function will keep waiting for valid input.

Examples

use ctx, age <- wait_number(
  ctx,
  min: Some(0),
  max: Some(120),
  or: Some(bot.HandleText(fn(ctx, invalid) {
    reply.with_text(ctx, "Please enter age between 0 and 120")
  })),
  timeout: None,
)

See conversation

pub fn wait_photos(
  ctx ctx: bot.Context(session, error, dependencies),
  or handle_else: option.Option(
    bot.Handler(session, error, dependencies),
  ),
  timeout timeout: option.Option(Int),
  continue continue: fn(
    bot.Context(session, error, dependencies),
    List(types.PhotoSize),
  ) -> Result(bot.Context(session, error, dependencies), error),
) -> Result(bot.Context(session, error, dependencies), error)

Pauses the current chat actor’s handler and waits for photos. Other chats and users continue to be handled concurrently.

See conversation

pub fn wait_text(
  ctx ctx: bot.Context(session, error, dependencies),
  or handle_else: option.Option(
    bot.Handler(session, error, dependencies),
  ),
  timeout timeout: option.Option(Int),
  continue continue: fn(
    bot.Context(session, error, dependencies),
    String,
  ) -> Result(bot.Context(session, error, dependencies), error),
) -> Result(bot.Context(session, error, dependencies), error)

Pauses the current chat actor’s handler and waits for a text message. Other chats and users continue to be handled concurrently.

See conversation

pub fn wait_video(
  ctx ctx: bot.Context(session, error, dependencies),
  or handle_else: option.Option(
    bot.Handler(session, error, dependencies),
  ),
  timeout timeout: option.Option(Int),
  continue continue: fn(
    bot.Context(session, error, dependencies),
    types.Video,
  ) -> Result(bot.Context(session, error, dependencies), error),
) -> Result(bot.Context(session, error, dependencies), error)

Pauses the current chat actor’s handler and waits for a video message. Other chats and users continue to be handled concurrently.

See conversation

pub fn wait_voice(
  ctx ctx: bot.Context(session, error, dependencies),
  or handle_else: option.Option(
    bot.Handler(session, error, dependencies),
  ),
  timeout timeout: option.Option(Int),
  continue continue: fn(
    bot.Context(session, error, dependencies),
    types.Voice,
  ) -> Result(bot.Context(session, error, dependencies), error),
) -> Result(bot.Context(session, error, dependencies), error)

Pauses the current chat actor’s handler and waits for a voice message. Other chats and users continue to be handled concurrently.

See conversation

pub fn with_auto_allowed_updates(
  builder: TelegaBuilder(session, error, dependencies),
) -> TelegaBuilder(session, error, dependencies)

Derive allowed_updates from the router’s registered routes.

Telegram then sends only the update types the bot actually handles, cutting out traffic for routes you never registered. A manual set_allowed_updates always wins (the escape hatch). If the router has a fallback, custom, or filtered route — which can match anything — derivation can’t narrow safely and falls back to Telegram’s default update set.

pub fn with_auto_commands(
  builder: TelegaBuilder(session, error, dependencies),
) -> TelegaBuilder(session, error, dependencies)

Publish the router’s commands to Telegram on start.

Every command registered with router.on_command_with_description is sent via setMyCommands once the bot is up, so the Telegram client shows them in the command menu without a manual call. Commands added with plain router.on_command (no description) are not published.

For localized descriptions use with_command_translations instead — it turns this on as well.

telega.new_for_polling(api_client:)
|> telega.with_router(router)
|> telega.with_auto_commands()
|> telega.init_for_polling()
pub fn with_catch_handler(
  builder: TelegaBuilder(session, error, dependencies),
  catch_handler: fn(
    bot.Context(session, error, dependencies),
    error,
  ) -> Result(Nil, error),
) -> TelegaBuilder(session, error, dependencies)

Set catch handler for system errors (like session persistence failures) and conversation errors. This is different from router’s catch handler which handles route errors.

pub fn with_chat_config(
  builder: TelegaBuilder(session, error, dependencies),
  restart_tolerance_intensity intensity: Int,
  restart_tolerance_period period: Int,
  init_timeout timeout: Int,
) -> TelegaBuilder(session, error, dependencies)

Configure the chat instance factory supervisor.

  • restart_tolerance_intensity — max restarts within the period (default: 5)
  • restart_tolerance_period — period in seconds (default: 10)
  • init_timeout — chat instance init timeout in ms (default: 10 000)
pub fn with_command_translations(
  builder: TelegaBuilder(session, error, dependencies),
  locales locales: List(String),
  translate translate: fn(String, String) -> option.Option(String),
) -> TelegaBuilder(session, error, dependencies)

Publish localized command descriptions on start.

Implies with_auto_commands: the default-language commands are published first, then for every locale in locales a setMyCommands(language_code:) call is made. translate(command, locale) supplies the per-language text; returning None keeps the router’s default description for that command.

telega_i18n provides a convenience wrapper that builds translate from a translation catalog, so you usually call this through it.

telega.new_for_polling(api_client:)
|> telega.with_router(router)
|> telega.with_command_translations(
  locales: ["en", "ru"],
  translate: fn(command, locale) { lookup_description(command, locale) },
)
|> telega.init_for_polling()
pub fn with_dependencies(
  builder builder: TelegaBuilder(session, error, old_dependencies),
  dependencies dependencies: dependencies,
) -> TelegaBuilder(session, error, dependencies)

Inject typed, non-persisted dependencies (services) available in every handler via ctx.dependencies (or get_dependencies).

Use this for things that are not user state and must not be persisted — a database pool, an http client, an i18n catalog, configuration, etc. The rule of thumb: session is the user’s state (persisted), dependencies is the bot’s services (set once at init, never persisted).

⚠️ Footgun — silently resets fields. with_dependencies changes the builder’s dependencies type, so it cannot keep the previously-set router, catch_handler, or on_start (they are typed against the old dependencies). It resets them to their defaults. If you call it after with_router, your router is silently dropped and the bot runs with no routes — and there is no compile error, only a dead bot at runtime. Either call with_dependencies first, or — better — skip it entirely and inject at construction with new_for_polling_with_dependencies / new_with_dependencies, which fix the dependencies type up front and have no reset behaviour.

// Preferred — no reset, any order:
telega.new_for_polling_with_dependencies(api_client:, dependencies: Dependencies(db:, catalog:))
|> telega.with_router(router)
|> telega.init_for_polling()

// With `with_dependencies` — MUST come before with_router/with_catch_handler/on_start:
telega.new_for_polling(api_client:)
|> telega.with_dependencies(Dependencies(db:, catalog:))
|> telega.with_router(router)
|> telega.init_for_polling()
pub fn with_drain_timeout(
  builder: TelegaBuilder(session, error, dependencies),
  timeout timeout: Int,
) -> TelegaBuilder(session, error, dependencies)

Set the maximum time (in milliseconds) shutdown waits for in-flight updates to finish before forcibly stopping the supervision tree.

Defaults to 5000ms.

pub fn with_nil_session(
  builder: TelegaBuilder(Nil, error, dependencies),
) -> TelegaBuilder(Nil, error, dependencies)

Set nil session for the bot.

pub fn with_on_shutdown(
  builder: TelegaBuilder(session, error, dependencies),
  on_shutdown on_shutdown: fn() -> Nil,
) -> TelegaBuilder(session, error, dependencies)

Set a hook to run during shutdown, after in-flight updates have drained and before the supervision tree is stopped. Use it to release resources (close pools, flush buffers, deregister from a service discovery, …).

pub fn with_on_start(
  builder: TelegaBuilder(session, error, dependencies),
  on_start on_start: fn(Telega(session, error, dependencies)) -> Result(
    Nil,
    error.TelegaError,
  ),
) -> TelegaBuilder(session, error, dependencies)

Set a hook to run once the bot has fully started.

Runs after the supervision tree is up and the Telega instance is built, so you can use it for warming caches, registering commands via the API, etc. Returning Error aborts startup and tears the supervision tree back down.

telega.new_for_polling(api_client:)
|> telega.with_router(router)
|> telega.with_on_start(fn(bot) {
  // register commands, warm caches...
  Ok(Nil)
})
|> telega.init_for_polling()
pub fn with_polling_config(
  builder: TelegaBuilder(session, error, dependencies),
  timeout timeout: Int,
  limit limit: Int,
  poll_interval poll_interval: Int,
) -> TelegaBuilder(session, error, dependencies)

Set polling configuration for the supervised polling worker.

pub fn with_polling_on_stop(
  builder: TelegaBuilder(session, error, dependencies),
  on_stop on_stop: fn(error.TelegaError) -> Nil,
) -> TelegaBuilder(session, error, dependencies)

Set a callback for when polling stops due to errors.

pub fn with_router(
  builder: TelegaBuilder(session, error, dependencies),
  router: router.Router(session, error, dependencies),
) -> TelegaBuilder(session, error, dependencies)

Set the router for handling updates. This is the primary way to handle updates - use router.new() to create a router and configure it with command handlers, text handlers, middleware, etc.

pub fn with_session_settings(
  builder: TelegaBuilder(session, error, dependencies),
  session_settings: bot.SessionSettings(session, error),
) -> TelegaBuilder(session, error, dependencies)

Set session settings for the bot.

pub fn with_signal_handlers(
  builder: TelegaBuilder(session, error, dependencies),
) -> TelegaBuilder(session, error, dependencies)

Install an OS signal handler (SIGTERM) that runs a graceful shutdown and then halts the VM.

This makes the bot survive rolling deploys on platforms like fly.io or Kubernetes: on SIGTERM the bot stops accepting new updates, drains in-flight work (bounded by with_drain_timeout), runs the on_shutdown hook, and stops cleanly. The handler replaces the runtime’s default signal behavior.

Only SIGTERM is handled — BEAM reserves SIGINT for its interactive break handler, so it cannot be intercepted this way.

Search Document