Current section

Files

Jump to
telega src telega@client.erl
Raw

src/telega@client.erl

-module(telega@client).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/telega/client.gleam").
-export([new/2, use_transformer/2, set_default_parse_mode/2, default_parse_mode_string/1, default_request_queue_config/0, set_request_queue/2, new_with_queue/2, fetch/2, set_fetch_client/2, set_fetch_bits_client/2, get_fetch_bits_client/1, set_max_retry_attempts/2, set_tg_api_url/2, get_token/1, shutdown/1, get_queue_length/1, is_queue_overheated/1, fetch_with_rule/3, get_api_url/1, request_method/1, request_body/1, map_request_body/2, new_post_request/3, new_get_request/3]).
-export_type([telegram_client/0, request_queue_config/0, request_queue_rule/0, telegram_api_request/0]).
-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.
?MODULEDOC(
" Module provides a simple interface to the Telegram Bot API.\n"
" If you want to use `telega` as a Telegram client, you can use only this module.\n"
"\n"
" Use an adapter package like `telega_httpc` or `telega_hackney` to create a client,\n"
" or provide your own `FetchClient` function.\n"
"\n"
" ```gleam\n"
" import telega/client\n"
" import telega/api\n"
"\n"
" fn main() {\n"
" ...\n"
" let response = client.new(token, my_fetch_adapter) |> api.send_message(client, send_message_parameters)\n"
" ...\n"
" }\n"
" ```\n"
).
-opaque telegram_client() :: {telegram_client,
binary(),
integer(),
binary(),
fun((gleam@http@request:request(binary())) -> {ok,
gleam@http@response:response(binary())} |
{error, telega@error:telega_error()}),
gleam@option:option(fun((gleam@http@request:request(bitstring())) -> {ok,
gleam@http@response:response(bitstring())} |
{error, telega@error:telega_error()})),
gleam@option:option(telega@internal@request_queue:request_queue()),
list(fun((telegram_api_request(), fun((telegram_api_request()) -> {ok,
gleam@http@response:response(binary())} |
{error, telega@error:telega_error()})) -> {ok,
gleam@http@response:response(binary())} |
{error, telega@error:telega_error()})),
gleam@option:option(telega@format:parse_mode())}.
-type request_queue_config() :: {request_queue_config,
list(request_queue_rule()),
gleam@option:option(integer()),
gleam@option:option(integer()),
integer(),
integer()}.
-type request_queue_rule() :: {request_queue_rule,
binary(),
integer(),
integer(),
integer()}.
-opaque telegram_api_request() :: {telegram_api_post_request,
binary(),
binary(),
binary()} |
{telegram_api_get_request,
binary(),
gleam@option:option(list({binary(), binary()})),
binary()}.
-file("src/telega/client.gleam", 91).
?DOC(" Create a new Telegram client with the given fetch client adapter.\n").
-spec new(
binary(),
fun((gleam@http@request:request(binary())) -> {ok,
gleam@http@response:response(binary())} |
{error, telega@error:telega_error()})
) -> telegram_client().
new(Token, Fetch_client) ->
{telegram_client,
Token,
3,
<<"https://api.telegram.org/bot"/utf8>>,
Fetch_client,
none,
none,
[],
none}.
-file("src/telega/client.gleam", 110).
?DOC(
" Add a transformer to the client's middleware chain.\n"
" Transformers run in the order they were added: the first added\n"
" is the outermost (sees the request first, the result last).\n"
).
-spec use_transformer(
telegram_client(),
fun((telegram_api_request(), fun((telegram_api_request()) -> {ok,
gleam@http@response:response(binary())} |
{error, telega@error:telega_error()})) -> {ok,
gleam@http@response:response(binary())} |
{error, telega@error:telega_error()})
) -> telegram_client().
use_transformer(Client, Transformer) ->
{telegram_client,
erlang:element(2, Client),
erlang:element(3, Client),
erlang:element(4, Client),
erlang:element(5, Client),
erlang:element(6, Client),
erlang:element(7, Client),
lists:append(erlang:element(8, Client), [Transformer]),
erlang:element(9, Client)}.
-file("src/telega/client.gleam", 123).
?DOC(
" Set the default parse mode for `telega/reply` text helpers\n"
" (`with_text`, `with_markup`, `edit_text`, ...). Explicit helpers like\n"
" `with_html` and parameters with a parse mode already set are not affected.\n"
).
-spec set_default_parse_mode(telegram_client(), telega@format:parse_mode()) -> telegram_client().
set_default_parse_mode(Client, Parse_mode) ->
{telegram_client,
erlang:element(2, Client),
erlang:element(3, Client),
erlang:element(4, Client),
erlang:element(5, Client),
erlang:element(6, Client),
erlang:element(7, Client),
erlang:element(8, Client),
{some, Parse_mode}}.
-file("src/telega/client.gleam", 132).
?DOC(
" Get the default parse mode as an API string (e.g. `Some(\"HTML\")`),\n"
" or `None` if no default is configured.\n"
).
-spec default_parse_mode_string(telegram_client()) -> gleam@option:option(binary()).
default_parse_mode_string(Client) ->
gleam@option:map(
erlang:element(9, Client),
fun telega@format:parse_mode_to_string/1
).
-file("src/telega/client.gleam", 257).
-spec default_request_queue_config() -> request_queue_config().
default_request_queue_config() ->
Default_config = telega@internal@request_queue:default_config(),
Rules = gleam@list:map(
erlang:element(2, Default_config),
fun(Rule) ->
{request_queue_rule,
erlang:element(2, Rule),
erlang:element(3, Rule),
erlang:element(4, Rule),
erlang:element(5, Rule)}
end
),
{request_queue_config,
Rules,
erlang:element(3, Default_config),
erlang:element(4, Default_config),
erlang:element(5, Default_config),
erlang:element(6, Default_config)}.
-file("src/telega/client.gleam", 332).
?DOC(
" Enable request queue with custom configuration for rate limiting\n"
"\n"
" The request queue helps prevent hitting Telegram's rate limits by:\n"
" - Queuing requests when limits are reached\n"
" - Automatically retrying failed requests with exponential backoff\n"
" - Supporting different rate limits for different types of requests\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" import telega/client\n"
"\n"
" let config = client.RequestQueueConfig(\n"
" rules: [\n"
" // Default rule for most requests\n"
" client.RequestQueueRule(\n"
" id: \"default\",\n"
" rate: 30, // 30 requests\n"
" limit: 1000, // per 1 second\n"
" priority: 5,\n"
" ),\n"
" // Slower rate for sending messages\n"
" client.RequestQueueRule(\n"
" id: \"send_message\",\n"
" rate: 1, // 1 request\n"
" limit: 1000, // per 1 second\n"
" priority: 10,\n"
" ),\n"
" // Higher priority for important requests\n"
" client.RequestQueueRule(\n"
" id: \"important\",\n"
" rate: 5,\n"
" limit: 1000,\n"
" priority: 1, // Lower number = higher priority\n"
" ),\n"
" ],\n"
" overall_rate: Some(30), // Global limit across all rules\n"
" overall_limit: Some(100), // Max concurrent requests\n"
" retry_delay: 1000, // Retry after 1 second\n"
" max_retries: 3,\n"
" )\n"
"\n"
" let assert Ok(client) =\n"
" client.new(token)\n"
" |> client.set_request_queue(config)\n"
"\n"
" // Use specific rule for rate-limited operations\n"
" client.fetch_with_rule(request, client, \"send_message\")\n"
"\n"
" // Check queue status\n"
" let queue_length = client.get_queue_length(client)\n"
" let is_busy = client.is_queue_overheated(client)\n"
" ```\n"
).
-spec set_request_queue(telegram_client(), request_queue_config()) -> {ok,
telegram_client()} |
{error, telega@error:telega_error()}.
set_request_queue(Client, Config) ->
case erlang:element(7, Client) of
{some, Queue} ->
telega@internal@request_queue:shutdown(Queue);
none ->
nil
end,
gleam@result:'try'(
begin
_pipe = telega@internal@request_queue:start(
{queue_config,
gleam@list:map(
erlang:element(2, Config),
fun(Rule) ->
{rule,
erlang:element(2, Rule),
erlang:element(3, Rule),
erlang:element(4, Rule),
erlang:element(5, Rule)}
end
),
erlang:element(3, Config),
erlang:element(4, Config),
erlang:element(5, Config),
erlang:element(6, Config)}
),
gleam@result:map_error(
_pipe,
fun(_) ->
{fetch_error, <<"Failed to start request queue"/utf8>>}
end
)
end,
fun(Queue@1) ->
{ok,
{telegram_client,
erlang:element(2, Client),
erlang:element(3, Client),
erlang:element(4, Client),
erlang:element(5, Client),
erlang:element(6, Client),
{some, Queue@1},
erlang:element(8, Client),
erlang:element(9, Client)}}
end
).
-file("src/telega/client.gleam", 142).
?DOC(
" Create a new Telegram client with default request queue configuration.\n"
"\n"
" This is a convenience function that creates a client with sensible\n"
" default rate limiting settings for the Telegram Bot API.\n"
).
-spec new_with_queue(
binary(),
fun((gleam@http@request:request(binary())) -> {ok,
gleam@http@response:response(binary())} |
{error, telega@error:telega_error()})
) -> {ok, telegram_client()} | {error, telega@error:telega_error()}.
new_with_queue(Token, Fetch_client) ->
_pipe = new(Token, Fetch_client),
set_request_queue(_pipe, default_request_queue_config()).
-file("src/telega/client.gleam", 458).
-spec emit_api_retry(binary(), integer(), integer()) -> nil.
emit_api_retry(Method, Attempt, Retry_delay) ->
telega@telemetry:execute(
[<<"telega"/utf8>>, <<"api_call"/utf8>>, <<"retry"/utf8>>],
[{<<"retry_after"/utf8>>, Retry_delay}],
[{<<"method"/utf8>>, {string_value, Method}},
{<<"attempt"/utf8>>, {int_value, Attempt}}]
).
-file("src/telega/client.gleam", 471).
?DOC(
" Extract the delay in milliseconds from a 429 response's\n"
" `parameters.retry_after` (seconds), falling back to the default delay.\n"
).
-spec retry_delay_from_response(gleam@http@response:response(binary())) -> integer().
retry_delay_from_response(Response) ->
_pipe = gleam@json:parse(
erlang:element(4, Response),
gleam@dynamic@decode:at(
[<<"parameters"/utf8>>, <<"retry_after"/utf8>>],
{decoder, fun gleam@dynamic@decode:decode_int/1}
)
),
_pipe@1 = gleam@result:map(
_pipe,
fun(Retry_after) -> Retry_after * 1000 end
),
gleam@result:unwrap(_pipe@1, 1000).
-file("src/telega/client.gleam", 480).
-spec send_with_retry(
telegram_client(),
binary(),
gleam@http@request:request(binary()),
integer()
) -> {ok, gleam@http@response:response(binary())} |
{error, telega@error:telega_error()}.
send_with_retry(Client, Method, Api_request, Retries) ->
Response = (erlang:element(5, Client))(Api_request),
case Retries of
0 ->
Response;
_ ->
case Response of
{ok, Response@1} ->
case erlang:element(2, Response@1) of
429 ->
Retry_delay = retry_delay_from_response(Response@1),
emit_api_retry(
Method,
(erlang:element(3, Client) - Retries) + 1,
Retry_delay
),
gleam_erlang_ffi:sleep(Retry_delay),
send_with_retry(
Client,
Method,
Api_request,
Retries - 1
);
_ ->
{ok, Response@1}
end;
{error, _} ->
emit_api_retry(
Method,
(erlang:element(3, Client) - Retries) + 1,
1000
),
gleam_erlang_ffi:sleep(1000),
send_with_retry(Client, Method, Api_request, Retries - 1)
end
end.
-file("src/telega/client.gleam", 417).
-spec send_request(telegram_client(), gleam@http@request:request(binary())) -> {ok,
gleam@http@response:response(binary())} |
{error, telega@error:telega_error()}.
send_request(Client, Api_request) ->
(erlang:element(5, Client))(Api_request).
-file("src/telega/client.gleam", 545).
-spec set_query(
gleam@http@request:request(KSW),
gleam@option:option(list({binary(), binary()}))
) -> gleam@http@request:request(KSW).
set_query(Api_request, Query) ->
case Query of
none ->
Api_request;
{some, Query@1} ->
gleam@http@request:set_query(Api_request, Query@1)
end.
-file("src/telega/client.gleam", 522).
-spec api_to_request(telegram_api_request()) -> {ok,
gleam@http@request:request(binary())} |
{error, telega@error:telega_error()}.
api_to_request(Api_request) ->
_pipe@7 = case Api_request of
{telegram_api_get_request, Url, Query, _} ->
_pipe = gleam@http@request:to(Url),
gleam@result:map(_pipe, fun(Req) -> _pipe@1 = Req,
_pipe@2 = gleam@http@request:set_method(_pipe@1, get),
set_query(_pipe@2, Query) end);
{telegram_api_post_request, Url@1, Body, _} ->
_pipe@3 = gleam@http@request:to(Url@1),
gleam@result:map(_pipe@3, fun(Req@1) -> _pipe@4 = Req@1,
_pipe@5 = gleam@http@request:set_body(_pipe@4, Body),
_pipe@6 = gleam@http@request:set_method(_pipe@5, post),
gleam@http@request:set_header(
_pipe@6,
<<"Content-Type"/utf8>>,
<<"application/json"/utf8>>
) end)
end,
gleam@result:map_error(_pipe@7, fun(_) -> api_to_request_convert_error end).
-file("src/telega/client.gleam", 171).
-spec apply_transformers(
list(fun((telegram_api_request(), fun((telegram_api_request()) -> {ok,
gleam@http@response:response(binary())} |
{error, telega@error:telega_error()})) -> {ok,
gleam@http@response:response(binary())} |
{error, telega@error:telega_error()})),
telegram_api_request(),
fun((telegram_api_request()) -> {ok, gleam@http@response:response(binary())} |
{error, telega@error:telega_error()})
) -> {ok, gleam@http@response:response(binary())} |
{error, telega@error:telega_error()}.
apply_transformers(Transformers, Request, Terminal) ->
case Transformers of
[] ->
Terminal(Request);
[Transformer | Rest] ->
Transformer(
Request,
fun(Request@1) ->
apply_transformers(Rest, Request@1, Terminal)
end
)
end.
-file("src/telega/client.gleam", 425).
?DOC(" Wraps a request execution in `telega.api_call` start/stop/exception events.\n").
-spec fetch_with_telemetry(
binary(),
fun(() -> {ok, gleam@http@response:response(binary())} |
{error, telega@error:telega_error()})
) -> {ok, gleam@http@response:response(binary())} |
{error, telega@error:telega_error()}.
fetch_with_telemetry(Method, Run) ->
Metadata = [{<<"method"/utf8>>, {string_value, Method}}],
Started_at = erlang:monotonic_time(),
telega@telemetry:execute(
[<<"telega"/utf8>>, <<"api_call"/utf8>>, <<"start"/utf8>>],
[{<<"system_time"/utf8>>, erlang:system_time()}],
Metadata
),
Result = Run(),
Duration = erlang:monotonic_time() - Started_at,
case Result of
{ok, Response} ->
telega@telemetry:execute(
[<<"telega"/utf8>>, <<"api_call"/utf8>>, <<"stop"/utf8>>],
[{<<"duration"/utf8>>, Duration}],
[{<<"status"/utf8>>, {int_value, erlang:element(2, Response)}} |
Metadata]
);
{error, Error} ->
telega@telemetry:execute(
[<<"telega"/utf8>>, <<"api_call"/utf8>>, <<"exception"/utf8>>],
[{<<"duration"/utf8>>, Duration}],
[{<<"error"/utf8>>, {string_value, gleam@string:inspect(Error)}} |
Metadata]
)
end,
Result.
-file("src/telega/client.gleam", 153).
?DOC(
" Send a request to the Telegram Bot API.\n"
"\n"
" It uses `default` rule for rate limiting (if request [queue](#set_request_queue) is enabled).\n"
).
-spec fetch(telegram_api_request(), telegram_client()) -> {ok,
gleam@http@response:response(binary())} |
{error, telega@error:telega_error()}.
fetch(Api_request, Client) ->
fetch_with_telemetry(
erlang:element(4, Api_request),
fun() ->
apply_transformers(
erlang:element(8, Client),
Api_request,
fun(Api_request@1) ->
Method = erlang:element(4, Api_request@1),
gleam@result:'try'(
api_to_request(Api_request@1),
fun(Api_request@2) -> case erlang:element(7, Client) of
{some, Queue} ->
telega@internal@request_queue:execute(
Queue,
fun() ->
send_request(Client, Api_request@2)
end
);
none ->
send_with_retry(
Client,
Method,
Api_request@2,
erlang:element(3, Client)
)
end end
)
end
)
end
).
-file("src/telega/client.gleam", 186).
?DOC(" Set the HTTP client to use.\n").
-spec set_fetch_client(
telegram_client(),
fun((gleam@http@request:request(binary())) -> {ok,
gleam@http@response:response(binary())} |
{error, telega@error:telega_error()})
) -> telegram_client().
set_fetch_client(Client, Fetch_client) ->
{telegram_client,
erlang:element(2, Client),
erlang:element(3, Client),
erlang:element(4, Client),
Fetch_client,
erlang:element(6, Client),
erlang:element(7, Client),
erlang:element(8, Client),
erlang:element(9, Client)}.
-file("src/telega/client.gleam", 195).
?DOC(" Set the binary HTTP client for file downloads.\n").
-spec set_fetch_bits_client(
telegram_client(),
fun((gleam@http@request:request(bitstring())) -> {ok,
gleam@http@response:response(bitstring())} |
{error, telega@error:telega_error()})
) -> telegram_client().
set_fetch_bits_client(Client, Fetch_bits_client) ->
{telegram_client,
erlang:element(2, Client),
erlang:element(3, Client),
erlang:element(4, Client),
erlang:element(5, Client),
{some, Fetch_bits_client},
erlang:element(7, Client),
erlang:element(8, Client),
erlang:element(9, Client)}.
-file("src/telega/client.gleam", 203).
?DOC(" Get the binary HTTP client, if configured.\n").
-spec get_fetch_bits_client(telegram_client()) -> gleam@option:option(fun((gleam@http@request:request(bitstring())) -> {ok,
gleam@http@response:response(bitstring())} |
{error, telega@error:telega_error()})).
get_fetch_bits_client(Client) ->
erlang:element(6, Client).
-file("src/telega/client.gleam", 210).
?DOC(" Set the maximum number of times to retry sending a API message.\n").
-spec set_max_retry_attempts(telegram_client(), integer()) -> telegram_client().
set_max_retry_attempts(Client, Max_retry_attempts) ->
{telegram_client,
erlang:element(2, Client),
Max_retry_attempts,
erlang:element(4, Client),
erlang:element(5, Client),
erlang:element(6, Client),
erlang:element(7, Client),
erlang:element(8, Client),
erlang:element(9, Client)}.
-file("src/telega/client.gleam", 218).
?DOC(" Set the Telegram Bot API URL.\n").
-spec set_tg_api_url(telegram_client(), binary()) -> telegram_client().
set_tg_api_url(Client, Tg_api_url) ->
{telegram_client,
erlang:element(2, Client),
erlang:element(3, Client),
Tg_api_url,
erlang:element(5, Client),
erlang:element(6, Client),
erlang:element(7, Client),
erlang:element(8, Client),
erlang:element(9, Client)}.
-file("src/telega/client.gleam", 226).
?DOC(" Get the bot token from the client\n").
-spec get_token(telegram_client()) -> binary().
get_token(Client) ->
erlang:element(2, Client).
-file("src/telega/client.gleam", 367).
?DOC(
" Shutdown the client and its request queue\n"
"\n"
" Only recommended if request queue is enabled.\n"
).
-spec shutdown(telegram_client()) -> nil.
shutdown(Client) ->
case erlang:element(7, Client) of
{some, Queue} ->
telega@internal@request_queue:shutdown(Queue);
none ->
nil
end.
-file("src/telega/client.gleam", 377).
?DOC(
" Get the total number of requests waiting in the queue\n"
"\n"
" Returns 0 if no queue is configured\n"
).
-spec get_queue_length(telegram_client()) -> integer().
get_queue_length(Client) ->
case erlang:element(7, Client) of
{some, Queue} ->
telega@internal@request_queue:total_length(Queue);
none ->
0
end.
-file("src/telega/client.gleam", 387).
?DOC(
" Check if the queue is overheated (any rule is at its rate limit)\n"
"\n"
" Returns False if no queue is configured\n"
).
-spec is_queue_overheated(telegram_client()) -> boolean().
is_queue_overheated(Client) ->
case erlang:element(7, Client) of
{some, Queue} ->
telega@internal@request_queue:is_overheated(Queue);
none ->
false
end.
-file("src/telega/client.gleam", 394).
-spec fetch_with_rule(telegram_api_request(), telegram_client(), binary()) -> {ok,
gleam@http@response:response(binary())} |
{error, telega@error:telega_error()}.
fetch_with_rule(Api_request, Client, Rule_id) ->
fetch_with_telemetry(
erlang:element(4, Api_request),
fun() ->
apply_transformers(
erlang:element(8, Client),
Api_request,
fun(Api_request@1) ->
Method = erlang:element(4, Api_request@1),
gleam@result:'try'(
api_to_request(Api_request@1),
fun(Api_request@2) ->
Request_id = telega@internal@utils:random_string(32),
case erlang:element(7, Client) of
{some, Queue} ->
telega@internal@request_queue:execute_with_rule(
Queue,
Request_id,
Rule_id,
fun() ->
send_request(Client, Api_request@2)
end
);
none ->
send_with_retry(
Client,
Method,
Api_request@2,
erlang:element(3, Client)
)
end
end
)
end
)
end
).
-file("src/telega/client.gleam", 552).
-spec get_api_url(telegram_client()) -> binary().
get_api_url(Client) ->
erlang:element(4, Client).
-file("src/telega/client.gleam", 566).
?DOC(" Get the Telegram API method name of a request (e.g. \"sendMessage\").\n").
-spec request_method(telegram_api_request()) -> binary().
request_method(Request) ->
erlang:element(4, Request).
-file("src/telega/client.gleam", 571).
?DOC(" Get the JSON body of a request. Returns `None` for GET requests.\n").
-spec request_body(telegram_api_request()) -> gleam@option:option(binary()).
request_body(Request) ->
case Request of
{telegram_api_post_request, _, Body, _} ->
{some, Body};
{telegram_api_get_request, _, _, _} ->
none
end.
-file("src/telega/client.gleam", 579).
?DOC(" Transform the JSON body of a POST request. GET requests are returned unchanged.\n").
-spec map_request_body(telegram_api_request(), fun((binary()) -> binary())) -> telegram_api_request().
map_request_body(Request, Mapper) ->
case Request of
{telegram_api_post_request, Url, Body, Method} ->
{telegram_api_post_request, Url, Mapper(Body), Method};
{telegram_api_get_request, _, _, _} ->
Request
end.
-file("src/telega/client.gleam", 606).
-spec build_url(telegram_client(), binary()) -> binary().
build_url(Client, Path) ->
<<<<<<(erlang:element(4, Client))/binary,
(erlang:element(2, Client))/binary>>/binary,
"/"/utf8>>/binary,
Path/binary>>.
-file("src/telega/client.gleam", 590).
-spec new_post_request(telegram_client(), binary(), binary()) -> telegram_api_request().
new_post_request(Client, Path, Body) ->
{telegram_api_post_request, build_url(Client, Path), Body, Path}.
-file("src/telega/client.gleam", 598).
-spec new_get_request(
telegram_client(),
binary(),
gleam@option:option(list({binary(), binary()}))
) -> telegram_api_request().
new_get_request(Client, Path, Query) ->
{telegram_api_get_request, build_url(Client, Path), Query, Path}.