Current section
Files
Jump to
Current section
Files
src/telega@deep_link.erl
-module(telega@deep_link).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/telega/deep_link.gleam").
-export([validate_payload/1, encode_payload/1, decode_payload/1, start_link/2, start_group_link/2, start_app_link/3, encoded_start_link/2, encoded_start_group_link/2, start_link_for_bot/2, start_group_link_for_bot/2, start_app_link_for_bot/3, encoded_start_link_for_bot/2, encoded_start_group_link_for_bot/2, payload_from_command/1, decoded_payload_from_command/1]).
-export_type([deep_link_error/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(
" Deep-linking helpers: build `t.me` links that open the bot with a\n"
" `/start` payload and read that payload back in the command handler.\n"
"\n"
" Telegram restricts start payloads to `A-Za-z0-9_-`, at most 64\n"
" characters. To carry arbitrary data (user ids, referral codes with\n"
" separators, unicode), pair `encode_payload`/`decode_payload` — they wrap\n"
" the data in URL-safe base64, which fits the allowed alphabet.\n"
"\n"
" ```gleam\n"
" // Somewhere a link is generated:\n"
" let assert Ok(link) = deep_link.encoded_start_link_for_bot(ctx.bot_info, \"ref:42\")\n"
" // -> \"https://t.me/my_bot?start=cmVmOjQy\"\n"
"\n"
" // In the /start handler:\n"
" fn start_handler(ctx, command: update.Command) {\n"
" case deep_link.decoded_payload_from_command(command) {\n"
" Ok(Some(data)) -> reply.with_text(ctx, \"Referred by: \" <> data)\n"
" _ -> reply.with_text(ctx, \"Hello!\")\n"
" }\n"
" }\n"
" ```\n"
).
-type deep_link_error() :: {payload_too_long, integer()} |
{invalid_payload_characters, binary()} |
missing_bot_username.
-file("src/telega/deep_link.gleam", 245).
-spec is_allowed_codepoint(integer()) -> boolean().
is_allowed_codepoint(Codepoint) ->
Code = gleam_stdlib:identity(Codepoint),
(((((Code >= 16#41) andalso (Code =< 16#5A)) orelse ((Code >= 16#61) andalso (Code
=< 16#7A)))
orelse ((Code >= 16#30) andalso (Code =< 16#39)))
orelse (Code =:= 16#5F))
orelse (Code =:= 16#2D).
-file("src/telega/deep_link.gleam", 52).
?DOC(
" Checks that a payload matches the Telegram deep-linking rules:\n"
" 1 to 64 characters from `A-Za-z0-9_-`. Returns the payload unchanged.\n"
).
-spec validate_payload(binary()) -> {ok, binary()} | {error, deep_link_error()}.
validate_payload(Payload) ->
Length = string:length(Payload),
case Length of
0 ->
{error, {invalid_payload_characters, Payload}};
_ when Length > 64 ->
{error, {payload_too_long, Length}};
_ ->
case begin
_pipe = Payload,
_pipe@1 = gleam@string:to_utf_codepoints(_pipe),
gleam@list:all(_pipe@1, fun is_allowed_codepoint/1)
end of
true ->
{ok, Payload};
false ->
{error, {invalid_payload_characters, Payload}}
end
end.
-file("src/telega/deep_link.gleam", 74).
?DOC(
" Encodes arbitrary data into a payload that fits the Telegram alphabet\n"
" (URL-safe base64 without padding). Data must be at most 48 bytes,\n"
" otherwise the encoded payload would not fit into 64 characters.\n"
).
-spec encode_payload(binary()) -> {ok, binary()} | {error, deep_link_error()}.
encode_payload(Data) ->
Bytes = gleam_stdlib:identity(Data),
Size = erlang:byte_size(Bytes),
case Size of
0 ->
{error, {invalid_payload_characters, Data}};
_ when Size > 48 ->
{error, {payload_too_long, Size}};
_ ->
{ok, gleam@bit_array:base64_url_encode(Bytes, false)}
end.
-file("src/telega/deep_link.gleam", 85).
?DOC(" Decodes a payload produced by `encode_payload` back to the original data.\n").
-spec decode_payload(binary()) -> {ok, binary()} | {error, deep_link_error()}.
decode_payload(Payload) ->
_pipe = Payload,
_pipe@1 = gleam@bit_array:base64_url_decode(_pipe),
_pipe@2 = gleam@result:'try'(_pipe@1, fun gleam@bit_array:to_string/1),
gleam@result:replace_error(_pipe@2, {invalid_payload_characters, Payload}).
-file("src/telega/deep_link.gleam", 234).
-spec normalize_username(binary()) -> {ok, binary()} |
{error, deep_link_error()}.
normalize_username(Username) ->
Username@1 = case Username of
<<"@"/utf8, Rest/binary>> ->
Rest;
_ ->
Username
end,
case Username@1 of
<<""/utf8>> ->
{error, missing_bot_username};
_ ->
{ok, Username@1}
end.
-file("src/telega/deep_link.gleam", 97).
?DOC(
" Builds `https://t.me/<username>?start=<payload>` — opens a private chat\n"
" with the bot and suggests pressing Start; the payload arrives in the\n"
" `/start` command. A leading `@` in the username is ignored.\n"
).
-spec start_link(binary(), binary()) -> {ok, binary()} |
{error, deep_link_error()}.
start_link(Username, Payload) ->
gleam@result:'try'(
normalize_username(Username),
fun(Username@1) ->
gleam@result:'try'(
validate_payload(Payload),
fun(Payload@1) ->
{ok,
<<<<<<"https://t.me/"/utf8, Username@1/binary>>/binary,
"?start="/utf8>>/binary,
Payload@1/binary>>}
end
)
end
).
-file("src/telega/deep_link.gleam", 108).
?DOC(
" Builds `https://t.me/<username>?startgroup=<payload>` — prompts the user\n"
" to pick a group to add the bot to; the payload arrives in `/start`.\n"
).
-spec start_group_link(binary(), binary()) -> {ok, binary()} |
{error, deep_link_error()}.
start_group_link(Username, Payload) ->
gleam@result:'try'(
normalize_username(Username),
fun(Username@1) ->
gleam@result:'try'(
validate_payload(Payload),
fun(Payload@1) ->
{ok,
<<<<<<"https://t.me/"/utf8, Username@1/binary>>/binary,
"?startgroup="/utf8>>/binary,
Payload@1/binary>>}
end
)
end
).
-file("src/telega/deep_link.gleam", 119).
?DOC(
" Builds `https://t.me/<username>/<app_name>?startapp=<payload>` — opens a\n"
" Mini App directly. Without a payload the `startapp` parameter is omitted.\n"
).
-spec start_app_link(binary(), binary(), gleam@option:option(binary())) -> {ok,
binary()} |
{error, deep_link_error()}.
start_app_link(Username, App_name, Payload) ->
gleam@result:'try'(
normalize_username(Username),
fun(Username@1) ->
Base = <<<<<<"https://t.me/"/utf8, Username@1/binary>>/binary,
"/"/utf8>>/binary,
App_name/binary>>,
case Payload of
none ->
{ok, Base};
{some, Payload@1} ->
gleam@result:'try'(
validate_payload(Payload@1),
fun(Payload@2) ->
{ok,
<<<<Base/binary, "?startapp="/utf8>>/binary,
Payload@2/binary>>}
end
)
end
end
).
-file("src/telega/deep_link.gleam", 137).
?DOC(
" Like `start_link`, but encodes arbitrary data with `encode_payload`\n"
" first — one call instead of encode + link.\n"
).
-spec encoded_start_link(binary(), binary()) -> {ok, binary()} |
{error, deep_link_error()}.
encoded_start_link(Username, Data) ->
gleam@result:'try'(
encode_payload(Data),
fun(Payload) -> start_link(Username, Payload) end
).
-file("src/telega/deep_link.gleam", 147).
?DOC(
" Like `start_group_link`, but encodes arbitrary data with\n"
" `encode_payload` first.\n"
).
-spec encoded_start_group_link(binary(), binary()) -> {ok, binary()} |
{error, deep_link_error()}.
encoded_start_group_link(Username, Data) ->
gleam@result:'try'(
encode_payload(Data),
fun(Payload) -> start_group_link(Username, Payload) end
).
-file("src/telega/deep_link.gleam", 227).
-spec bot_username(telega@model@types:user()) -> {ok, binary()} |
{error, deep_link_error()}.
bot_username(Bot_info) ->
case erlang:element(6, Bot_info) of
{some, Username} ->
{ok, Username};
none ->
{error, missing_bot_username}
end.
-file("src/telega/deep_link.gleam", 157).
?DOC(
" Like `start_link`, but takes the bot's `User` (e.g. `ctx.bot_info`) and\n"
" fails with `MissingBotUsername` when the bot has no username.\n"
).
-spec start_link_for_bot(telega@model@types:user(), binary()) -> {ok, binary()} |
{error, deep_link_error()}.
start_link_for_bot(Bot_info, Payload) ->
gleam@result:'try'(
bot_username(Bot_info),
fun(Username) -> start_link(Username, Payload) end
).
-file("src/telega/deep_link.gleam", 166).
?DOC(" Like `start_group_link`, but takes the bot's `User` (e.g. `ctx.bot_info`).\n").
-spec start_group_link_for_bot(telega@model@types:user(), binary()) -> {ok,
binary()} |
{error, deep_link_error()}.
start_group_link_for_bot(Bot_info, Payload) ->
gleam@result:'try'(
bot_username(Bot_info),
fun(Username) -> start_group_link(Username, Payload) end
).
-file("src/telega/deep_link.gleam", 175).
?DOC(" Like `start_app_link`, but takes the bot's `User` (e.g. `ctx.bot_info`).\n").
-spec start_app_link_for_bot(
telega@model@types:user(),
binary(),
gleam@option:option(binary())
) -> {ok, binary()} | {error, deep_link_error()}.
start_app_link_for_bot(Bot_info, App_name, Payload) ->
gleam@result:'try'(
bot_username(Bot_info),
fun(Username) -> start_app_link(Username, App_name, Payload) end
).
-file("src/telega/deep_link.gleam", 190).
?DOC(
" Encodes arbitrary data and builds a start link for the bot — the closest\n"
" analog of aiogram's `create_start_link(bot, data, encode=True)`:\n"
"\n"
" ```gleam\n"
" let assert Ok(link) = deep_link.encoded_start_link_for_bot(ctx.bot_info, \"ref:42\")\n"
" ```\n"
).
-spec encoded_start_link_for_bot(telega@model@types:user(), binary()) -> {ok,
binary()} |
{error, deep_link_error()}.
encoded_start_link_for_bot(Bot_info, Data) ->
gleam@result:'try'(
encode_payload(Data),
fun(Payload) -> start_link_for_bot(Bot_info, Payload) end
).
-file("src/telega/deep_link.gleam", 199).
?DOC(" Encodes arbitrary data and builds a group start link for the bot.\n").
-spec encoded_start_group_link_for_bot(telega@model@types:user(), binary()) -> {ok,
binary()} |
{error, deep_link_error()}.
encoded_start_group_link_for_bot(Bot_info, Data) ->
gleam@result:'try'(
encode_payload(Data),
fun(Payload) -> start_group_link_for_bot(Bot_info, Payload) end
).
-file("src/telega/deep_link.gleam", 209).
?DOC(
" Extracts the raw `/start` payload from a command, normalizing the empty\n"
" payload of a bare `/start` to `None`.\n"
).
-spec payload_from_command(telega@update:command()) -> gleam@option:option(binary()).
payload_from_command(Command) ->
case erlang:element(4, Command) of
{some, <<""/utf8>>} ->
none;
none ->
none;
{some, Payload} ->
{some, Payload}
end.
-file("src/telega/deep_link.gleam", 218).
?DOC(
" Extracts and base64-decodes the payload of a command produced by an\n"
" `encode_payload`-built link. `Ok(None)` means there was no payload.\n"
).
-spec decoded_payload_from_command(telega@update:command()) -> {ok,
gleam@option:option(binary())} |
{error, deep_link_error()}.
decoded_payload_from_command(Command) ->
case payload_from_command(Command) of
none ->
{ok, none};
{some, Payload} ->
_pipe = decode_payload(Payload),
gleam@result:map(_pipe, fun(Field@0) -> {some, Field@0} end)
end.