telega/client

Module provides a simple interface to the Telegram Bot API. If you want to use telega as a Telegram client, you can use only this module.

Use an adapter package like telega_httpc or telega_hackney to create a client, or provide your own FetchClient function.

import telega/client
import telega/api

fn main() {
  ...
  let response = client.new(token, my_fetch_adapter) |> api.send_message(client, send_message_parameters)
  ...
}

Types

Middleware around a single API call: it receives the outgoing request and a next continuation. It can modify the request before calling next, short-circuit the chain by returning a result without calling next, or inspect/transform the result after next returns.

Transformers run inside the telega.api_call telemetry span, so their latency is included in the span duration.

let log_calls = fn(request, next) {
  io.println("calling " <> client.request_method(request))
  next(request)
}
let client = client.new(token:, fetch_client:) |> client.use_transformer(log_calls)
pub type ApiRequestTransformer =
  fn(
    TelegramApiRequest,
    fn(TelegramApiRequest) -> Result(
      response.Response(String),
      error.TelegaError,
    ),
  ) -> Result(response.Response(String), error.TelegaError)
pub type FetchBitsClient =
  fn(request.Request(BitArray)) -> Result(
    response.Response(BitArray),
    error.TelegaError,
  )
pub type FetchClient =
  fn(request.Request(String)) -> Result(
    response.Response(String),
    error.TelegaError,
  )

Telegram’s per-chat limits, which the global rate does not cover: a bot may send 30 messages a second overall but only about one a second to the same private chat, and about 20 a minute to the same group.

The queue tells the two apart by the sign of the chat_id — Telegram gives groups, supergroups and channels negative ids.

pub type PerChatLimits {
  PerChatLimits(
    private_rate: Int,
    private_window_ms: Int,
    group_rate: Int,
    group_window_ms: Int,
  )
}

Constructors

  • PerChatLimits(
      private_rate: Int,
      private_window_ms: Int,
      group_rate: Int,
      group_window_ms: Int,
    )

    Arguments

    private_rate

    Requests allowed per private_window_ms to one private chat.

    group_rate

    Requests allowed per group_window_ms to one group/supergroup/channel.

pub type RequestQueueConfig {
  RequestQueueConfig(
    rules: List(RequestQueueRule),
    overall_rate: option.Option(Int),
    overall_limit: option.Option(Int),
    retry_delay: Int,
    max_retries: Int,
    per_chat: option.Option(PerChatLimits),
  )
}

Constructors

  • RequestQueueConfig(
      rules: List(RequestQueueRule),
      overall_rate: option.Option(Int),
      overall_limit: option.Option(Int),
      retry_delay: Int,
      max_retries: Int,
      per_chat: option.Option(PerChatLimits),
    )

    Arguments

    overall_rate

    Overall rate limit (requests per second)

    overall_limit

    Overall concurrent request limit

    retry_delay

    Default retry delay in milliseconds

    max_retries

    Maximum retries

    per_chat

    Per-chat pacing on top of the global rules. None paces by the global rules only, which is what a bot busy in one chat will notice first.

pub type RequestQueueRule {
  RequestQueueRule(
    id: String,
    rate: Int,
    limit: Int,
    priority: Int,
  )
}

Constructors

  • RequestQueueRule(
      id: String,
      rate: Int,
      limit: Int,
      priority: Int,
    )

    Arguments

    id

    Rule identifier

    rate

    Maximum requests per time window

    limit

    Time window in milliseconds

    priority

    Priority (lower number = higher priority)

When a failed attempt may be repeated.

pub type RetryOn {
  Never
  OnlyIdempotent
  Always
}

Constructors

  • Never

    Never repeat — hand the failure back after the first attempt.

  • OnlyIdempotent

    Repeat only methods that create nothing, so a replay cannot duplicate a message, an invite link or a payment. The list comes from telega/internal/method_info, generated from the Bot API spec.

  • Always

    Repeat every method, duplicates included. Pick this only when the caller deduplicates on its own.

How the client repeats a call that did not go through.

A 429 is special: Telegram answers it instead of doing the work and says exactly how long to wait, so it is always retried (up to max_attempts) whatever retry_on_* says — but only if the wait fits in max_retry_after_ms, because sleeping it off blocks the calling process.

pub type RetryPolicy {
  RetryPolicy(
    max_attempts: Int,
    base_delay_ms: Int,
    max_delay_ms: Int,
    jitter: Bool,
    retry_on_server_errors: RetryOn,
    retry_on_transport_errors: RetryOn,
    max_retry_after_ms: Int,
  )
}

Constructors

  • RetryPolicy(
      max_attempts: Int,
      base_delay_ms: Int,
      max_delay_ms: Int,
      jitter: Bool,
      retry_on_server_errors: RetryOn,
      retry_on_transport_errors: RetryOn,
      max_retry_after_ms: Int,
    )

    Arguments

    max_attempts

    Total attempts, the first one included. 1 disables retrying.

    base_delay_ms

    Delay before the first retry; each further one doubles it.

    max_delay_ms

    Cap on the doubling.

    jitter

    Spread the delay over [delay / 2, delay] so a fleet of bots that hit the same outage does not come back in lockstep.

    retry_on_server_errors

    Whether a 5xx may be repeated.

    retry_on_transport_errors

    Whether a transport failure (no response at all) may be repeated.

    max_retry_after_ms

    Longest 429 retry_after worth sleeping off inside the calling process. Beyond it the 429 response is returned to the caller.

pub opaque type TelegramApiRequest
pub opaque type TelegramClient

Values

pub fn cache_get_me(
  client client: TelegramClient,
) -> TelegramClient

Answer getMe from a cache instead of the network after the first call.

getMe is asked once at startup by telega itself and then again by anything that wants bot_info; the answer only changes when you rename the bot. The cache lives in an ETS table of its own, so it survives whichever process happened to make the first call — and it is never invalidated, so a bot renamed through setMyName keeps reporting the old name until the node restarts.

Only a successful response is cached; a failure is retried the next time.

pub fn default_parse_mode_string(
  client client: TelegramClient,
) -> option.Option(String)

Get the default parse mode as an API string (e.g. Some("HTML")), or None if no default is configured.

pub fn default_per_chat_limits() -> PerChatLimits

1 request per second per private chat, 20 per minute per group.

pub fn default_request_queue_config() -> RequestQueueConfig
pub fn default_retry_policy() -> RetryPolicy

Four attempts, one second apart and doubling, and only methods that create nothing are repeated on a 5xx or a transport failure.

pub fn fetch(
  request api_request: TelegramApiRequest,
  client client: TelegramClient,
) -> Result(response.Response(String), error.TelegaError)

Send a request to the Telegram Bot API.

With a request queue configured, the call is paced by the rule for the chat it addresses when the queue has per-chat limits, and by the default rule otherwise. getUpdates never goes through the queue.

pub fn fetch_multipart(
  client client: TelegramClient,
  method method: String,
  content_type content_type: String,
  body body: BitArray,
) -> Result(response.Response(String), error.TelegaError)

Send a multipart/form-data POST (a BitArray body) to method, routed through the SAME transformer chain, request queue and 429-retry path as JSON calls, using the configured FetchBitsClient. This is how raw file uploads (e.g. sending a photo by bytes) honor the one-queue rate-limit invariant — there is no second HTTP client. Errors if no FetchBitsClient is configured.

pub fn fetch_with_rule(
  request api_request: TelegramApiRequest,
  client client: TelegramClient,
  rule_id rule_id: String,
) -> Result(response.Response(String), error.TelegaError)
pub fn get_api_url(client client: TelegramClient) -> String
pub fn get_fetch_bits_client(
  client client: TelegramClient,
) -> option.Option(
  fn(request.Request(BitArray)) -> Result(
    response.Response(BitArray),
    error.TelegaError,
  ),
)

Get the binary HTTP client, if configured.

pub fn get_queue_length(client client: TelegramClient) -> Int

Get the total number of requests waiting in the queue

Returns 0 if no queue is configured

pub fn get_retry_policy(
  client client: TelegramClient,
) -> RetryPolicy

The retry policy this client uses.

pub fn get_token(client: TelegramClient) -> String

Get the bot token from the client

pub fn is_queue_overheated(client client: TelegramClient) -> Bool

Check if the queue is overheated (any rule is at its rate limit)

Returns False if no queue is configured

pub fn map_request_body(
  request request: TelegramApiRequest,
  mapper mapper: fn(String) -> String,
) -> TelegramApiRequest

Transform the JSON body of a POST request. GET requests are returned unchanged.

pub fn new(
  token token: String,
  fetch_client fetch_client: fn(request.Request(String)) -> Result(
    response.Response(String),
    error.TelegaError,
  ),
) -> TelegramClient

Create a new Telegram client with the given fetch client adapter.

pub fn new_get_request(
  client client: TelegramClient,
  path path: String,
  query query: option.Option(List(#(String, String))),
) -> TelegramApiRequest
pub fn new_post_request(
  client client: TelegramClient,
  path path: String,
  body body: String,
) -> TelegramApiRequest
pub fn new_with_default_limits(
  token token: String,
  fetch_client fetch_client: fn(request.Request(String)) -> Result(
    response.Response(String),
    error.TelegaError,
  ),
) -> Result(TelegramClient, error.TelegaError)

Create a client that paces itself by Telegram’s documented limits.

That is 30 requests per second overall, 1 per second to any one private chat and 20 per minute to any one group, supergroup or channel — see default_request_queue_config. Requests over the limit wait in the queue instead of coming back as a 429.

let assert Ok(api_client) =
  client.new_with_default_limits(token:, fetch_client:)
pub fn new_with_queue(
  token token: String,
  fetch_client fetch_client: fn(request.Request(String)) -> Result(
    response.Response(String),
    error.TelegaError,
  ),
) -> Result(TelegramClient, error.TelegaError)

Create a new Telegram client with default request queue configuration.

The older name for new_with_default_limits; the two are the same call.

pub fn redact_token(
  client client: TelegramClient,
  text text: String,
) -> String

Replaces every occurrence of the bot token in text with <token>.

Telegram puts the token in the URL of every request, so any message built from a URL — an error, a log line, a link shown to a user — leaks it unless it goes through this function first.

pub fn request_body(
  request request: TelegramApiRequest,
) -> option.Option(String)

Get the JSON body of a request. Returns None for GET requests.

pub fn request_method(
  request request: TelegramApiRequest,
) -> String

Get the Telegram API method name of a request (e.g. “sendMessage”).

pub fn set_default_parse_mode(
  client client: TelegramClient,
  parse_mode parse_mode: format.ParseMode,
) -> TelegramClient

Set the default parse mode for telega/reply text helpers (with_text, with_markup, edit_text, …). Explicit helpers like with_html and parameters with a parse mode already set are not affected.

pub fn set_fetch_bits_client(
  client client: TelegramClient,
  fetch_bits_client fetch_bits_client: fn(
    request.Request(BitArray),
  ) -> Result(response.Response(BitArray), error.TelegaError),
) -> TelegramClient

Set the binary HTTP client for file downloads.

pub fn set_fetch_client(
  client client: TelegramClient,
  fetch_client fetch_client: fn(request.Request(String)) -> Result(
    response.Response(String),
    error.TelegaError,
  ),
) -> TelegramClient

Set the HTTP client to use.

pub fn set_max_retry_attempts(
  client client: TelegramClient,
  max_retry_attempts max_retry_attempts: Int,
) -> TelegramClient

Set the maximum number of retries after the first attempt.

A shorthand for RetryPolicy.max_attempts, which counts the first attempt too: set_max_retry_attempts(0) means one attempt and no retry.

pub fn set_max_retry_delay(
  client client: TelegramClient,
  max_retry_delay max_retry_delay: Int,
) -> TelegramClient

Set the longest 429 retry_after the client will wait out.

Telegram can ask for minutes; sleeping that off blocks the process that made the call (a chat instance, the broadcast actor). Beyond this the 429 response is returned to the caller instead. Default is 60_000 ms.

A shorthand for RetryPolicy.max_retry_after_ms.

pub fn set_request_queue(
  client client: TelegramClient,
  config config: RequestQueueConfig,
) -> Result(TelegramClient, error.TelegaError)

Enable request queue with custom configuration for rate limiting

The request queue helps prevent hitting Telegram’s rate limits by:

  • Queuing requests when limits are reached
  • Automatically retrying failed requests with exponential backoff
  • Supporting different rate limits for different types of requests

Example

import telega/client

// Start from the defaults and change what you need, so a new field in a
// later version does not turn into a compile error here.
let config = client.RequestQueueConfig(
  ..client.default_request_queue_config(),
  rules: [
    // Default rule for most requests
    client.RequestQueueRule(
      id: "default",
      rate: 30,        // 30 requests
      limit: 1000,     // per 1 second
      priority: 5,
    ),
    // Slower rate for sending messages
    client.RequestQueueRule(
      id: "send_message",
      rate: 1,         // 1 request
      limit: 1000,     // per 1 second
      priority: 10,
    ),
    // Higher priority for important requests
    client.RequestQueueRule(
      id: "important",
      rate: 5,
      limit: 1000,
      priority: 1,     // Lower number = higher priority
    ),
  ],
  overall_rate: Some(30),    // Global limit across all rules
  overall_limit: Some(100),  // Max concurrent requests
  // 1/s per private chat, 20/min per group; `None` to pace by the global
  // rules only.
  per_chat: Some(client.default_per_chat_limits()),
)

let assert Ok(client) =
  client.new(token)
  |> client.set_request_queue(config)

// Use specific rule for rate-limited operations
client.fetch_with_rule(request, client, "send_message")

// Check queue status
let queue_length = client.get_queue_length(client)
let is_busy = client.is_queue_overheated(client)
pub fn set_retry_policy(
  client client: TelegramClient,
  retry_policy retry_policy: RetryPolicy,
) -> TelegramClient

Replace the whole retry policy.

client.new(token:, fetch_client:)
|> client.set_retry_policy(
  client.RetryPolicy(
    ..client.default_retry_policy(),
    max_attempts: 6,
    base_delay_ms: 250,
    retry_on_transport_errors: client.Always,
  ),
)
pub fn set_tg_api_url(
  client client: TelegramClient,
  tg_api_url tg_api_url: String,
) -> TelegramClient

Set the Telegram Bot API URL.

pub fn shutdown(client client: TelegramClient) -> Nil

Shutdown the client and its request queue

Only recommended if request queue is enabled.

pub fn trace_transformer(
  level level: logging.LogLevel,
) -> fn(
  TelegramApiRequest,
  fn(TelegramApiRequest) -> Result(
    response.Response(String),
    error.TelegaError,
  ),
) -> Result(response.Response(String), error.TelegaError)

A transformer that logs every API call at level: the method and request body on the way out, the status and elapsed time on the way back, the description on a failure.

let client =
  client.new(token:, fetch_client:)
  |> client.use_transformer(client.trace_transformer(logging.Debug))

Add it first if you want it to time the whole chain, last to see the request as it actually goes out. Bodies are truncated and anything shaped like a bot token is replaced with <token> — a fetch error carries the request URL, and the URL carries the token. Everything else in a body is logged verbatim, so this is a debugging tool, not something to leave on in production with user data flowing through it.

pub fn use_transformer(
  client client: TelegramClient,
  transformer transformer: fn(
    TelegramApiRequest,
    fn(TelegramApiRequest) -> Result(
      response.Response(String),
      error.TelegaError,
    ),
  ) -> Result(response.Response(String), error.TelegaError),
) -> TelegramClient

Add a transformer to the client’s middleware chain. Transformers run in the order they were added: the first added is the outermost (sees the request first, the result last).

Search Document