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_msto one private chat. - group_rate
-
Requests allowed per
group_window_msto 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.
Nonepaces 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
-
NeverNever repeat — hand the failure back after the first attempt.
-
OnlyIdempotentRepeat 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. -
AlwaysRepeat 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.
1disables 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_afterworth 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 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).