-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/?start=` — 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/?startgroup=` — 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//?startapp=` — 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, <<< {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.