-module(telega@dialog@render). -compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). -define(FILEPATH, "src/telega/dialog/render.gleam"). -export([is_not_modified/1, is_message_not_found/1, is_cant_be_edited/1, callback_data/4, pack_callback_data/4, build_markup/3, render_window/6, remove_keyboard/3, alert/2, toast/2, auto_answer/1, answer_quietly/2, describe_error/1]). -export_type([render_error/0, message_kind/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( " The edit-or-send engine behind dialogs.\n" "\n" " Keeps one live dialog message per chat: sends it the first time, edits it\n" " on every subsequent render. Media windows are supported by the strategy\n" " matrix below — the Bot API cannot turn a text message into a media one\n" " (or back), so a kind change recreates the message:\n" "\n" " | was \\ becomes | text | media |\n" " |-------------------|----------------------------|----------------------------|\n" " | — (no message) | `sendMessage` | `sendPhoto`/`sendVideo`/… |\n" " | text | `editMessageText` | delete (best effort) + send |\n" " | media (any kind) | delete (best effort) + send | `editMessageMedia` |\n" "\n" " `deleteMessage` is best effort (messages older than 48 hours cannot be\n" " deleted): its failure is swallowed and the fresh message is sent anyway.\n" "\n" " Also hosts the `alert`/`toast` helpers and the Bot API error classifiers\n" " (`is_not_modified` & co) — the latter are useful outside dialogs too.\n" ). -type render_error() :: {api_error, telega@error:telega_error()} | {invalid_button, binary(), binary()}. -type message_kind() :: text_message | media_message. -file("src/telega/dialog/render.gleam", 64). ?DOC( " The edit was a no-op: new content equals the current one. Safe to treat\n" " as success.\n" ). -spec is_not_modified(telega@error:telega_error()) -> boolean(). is_not_modified(Error) -> telega@error:is_message_not_modified(Error). -file("src/telega/dialog/render.gleam", 69). ?DOC(" The message to edit no longer exists (deleted by the user or too old).\n"). -spec is_message_not_found(telega@error:telega_error()) -> boolean(). is_message_not_found(Error) -> telega@error:is_message_not_found(Error). -file("src/telega/dialog/render.gleam", 74). ?DOC(" The message exists but cannot be edited (e.g. not sent by the bot).\n"). -spec is_cant_be_edited(telega@error:telega_error()) -> boolean(). is_cant_be_edited(Error) -> telega@error:is_message_cant_be_edited(Error). -file("src/telega/dialog/render.gleam", 172). -spec delete_best_effort( telega@bot:context(any(), any(), any()), integer(), integer() ) -> nil. delete_best_effort(Ctx, Chat_id, Message_id) -> _ = telega@api:delete_message( erlang:element(5, erlang:element(4, Ctx)), {delete_message_parameters, {int, Chat_id}, Message_id} ), nil. -file("src/telega/dialog/render.gleam", 152). ?DOC( " Apply the shared edit-fallback matrix: \"not modified\" is success, a gone\n" " message falls back to a fresh send, anything else is an error.\n" ). -spec or_send_fallback( {ok, integer()} | {error, telega@error:telega_error()}, integer(), message_kind(), fun(() -> {ok, {integer(), message_kind()}} | {error, render_error()}) ) -> {ok, {integer(), message_kind()}} | {error, render_error()}. or_send_fallback(Edited, Current_id, Kind, Send) -> case Edited of {ok, Message_id} -> {ok, {Message_id, Kind}}; {error, Api_error} -> case {is_not_modified(Api_error), is_message_not_found(Api_error) orelse is_cant_be_edited( Api_error )} of {true, _} -> {ok, {Current_id, Kind}}; {_, true} -> Send(); {_, _} -> {error, {api_error, Api_error}} end end. -file("src/telega/dialog/render.gleam", 450). -spec caption_for(binary()) -> gleam@option:option(binary()). caption_for(Text) -> case Text of <<""/utf8>> -> none; _ -> {some, Text} end. -file("src/telega/dialog/render.gleam", 457). -spec spoiler_option(boolean()) -> gleam@option:option(boolean()). spoiler_option(Has_spoiler) -> case Has_spoiler of true -> {some, true}; false -> none end. -file("src/telega/dialog/render.gleam", 389). -spec input_media( telega@dialog@types:dialog_media(), gleam@option:option(binary()), binary() ) -> telega@model@types:input_media(). input_media(Media, Caption, Parse_mode) -> Parse_mode@1 = gleam@option:map(Caption, fun(_) -> Parse_mode end), case Media of {photo_media, Media@1, Has_spoiler} -> {input_media_photo_input_media, {input_media_photo, <<"photo"/utf8>>, Media@1, Caption, Parse_mode@1, none, none, spoiler_option(Has_spoiler)}}; {video_media, Media@2, Has_spoiler@1} -> {input_media_video_input_media, {input_media_video, <<"video"/utf8>>, Media@2, none, none, none, Caption, Parse_mode@1, none, none, none, none, none, none, spoiler_option(Has_spoiler@1)}}; {animation_media, Media@3} -> {input_media_animation_input_media, {input_media_animation, <<"animation"/utf8>>, Media@3, none, Caption, Parse_mode@1, none, none, none, none, none, none}}; {document_media, Media@4} -> {input_media_document_input_media, {input_media_document, <<"document"/utf8>>, Media@4, none, Caption, Parse_mode@1, none, none}} end. -file("src/telega/dialog/render.gleam", 366). -spec edit_media_window( telega@bot:context(any(), any(), any()), integer(), integer(), telega@dialog@types:dialog_media(), binary(), binary(), gleam@option:option(telega@model@types:inline_keyboard_markup()) ) -> {ok, integer()} | {error, telega@error:telega_error()}. edit_media_window(Ctx, Chat_id, Message_id, Media, Text, Parse_mode, Markup) -> _pipe = telega@api:edit_message_media( erlang:element(5, erlang:element(4, Ctx)), {edit_message_media_parameters, none, {some, {int, Chat_id}}, {some, Message_id}, none, input_media(Media, caption_for(Text), Parse_mode), Markup} ), gleam@result:map(_pipe, fun(Message) -> erlang:element(2, Message) end). -file("src/telega/dialog/render.gleam", 223). -spec edit_window( telega@bot:context(any(), any(), any()), integer(), integer(), binary(), binary(), gleam@option:option(telega@model@types:inline_keyboard_markup()) ) -> {ok, integer()} | {error, telega@error:telega_error()}. edit_window(Ctx, Chat_id, Message_id, Text, Parse_mode, Markup) -> _pipe = telega@api:edit_message_text( erlang:element(5, erlang:element(4, Ctx)), {edit_message_text_parameters, {some, {int, Chat_id}}, {some, Message_id}, none, Text, {some, Parse_mode}, none, none, Markup} ), gleam@result:map(_pipe, fun(Message) -> erlang:element(2, Message) end). -file("src/telega/dialog/render.gleam", 249). -spec send_media_window( telega@bot:context(any(), any(), any()), integer(), telega@dialog@types:dialog_media(), binary(), binary(), gleam@option:option(telega@model@types:inline_keyboard_markup()) ) -> {ok, integer()} | {error, render_error()}. send_media_window(Ctx, Chat_id, Media, Text, Parse_mode, Markup) -> telega@webhook_reply:without_claim( fun() -> Chat_id@1 = {int, Chat_id}, Caption = caption_for(Text), Parse_mode@1 = gleam@option:map(Caption, fun(_) -> Parse_mode end), Reply_markup = gleam@option:map( Markup, fun(Field@0) -> {send_message_reply_inline_keyboard_markup_parameters, Field@0} end ), _pipe = case Media of {photo_media, Media@1, Has_spoiler} -> telega@api:send_photo( erlang:element(5, erlang:element(4, Ctx)), {send_photo_parameters, Chat_id@1, none, none, {string_v, Media@1}, Caption, Parse_mode@1, none, none, spoiler_option(Has_spoiler), none, none, none, none, none, Reply_markup} ); {video_media, Media@2, Has_spoiler@1} -> telega@api:send_video( erlang:element(5, erlang:element(4, Ctx)), {send_video_parameters, Chat_id@1, none, none, {string_v, Media@2}, none, none, none, none, none, none, Caption, Parse_mode@1, none, none, spoiler_option(Has_spoiler@1), none, none, none, none, none, none, Reply_markup} ); {animation_media, Media@3} -> telega@api:send_animation( erlang:element(5, erlang:element(4, Ctx)), {send_animation_parameters, Chat_id@1, none, none, {string_v, Media@3}, none, none, none, none, Caption, Parse_mode@1, none, none, none, none, none, none, none, none, Reply_markup} ); {document_media, Media@4} -> telega@api:send_document( erlang:element(5, erlang:element(4, Ctx)), {send_document_parameters, Chat_id@1, none, none, {string_v, Media@4}, none, Caption, Parse_mode@1, none, none, none, none, none, none, none, Reply_markup} ) end, _pipe@1 = gleam@result:map( _pipe, fun(Message) -> erlang:element(2, Message) end ), gleam@result:map_error( _pipe@1, fun(Field@0) -> {api_error, Field@0} end ) end ). -file("src/telega/dialog/render.gleam", 188). -spec send_window( telega@bot:context(any(), any(), any()), integer(), binary(), binary(), gleam@option:option(telega@model@types:inline_keyboard_markup()) ) -> {ok, integer()} | {error, render_error()}. send_window(Ctx, Chat_id, Text, Parse_mode, Markup) -> telega@webhook_reply:without_claim( fun() -> _pipe = telega@api:send_message( erlang:element(5, erlang:element(4, Ctx)), {send_message_parameters, none, {int, Chat_id}, none, Text, {some, Parse_mode}, none, none, none, none, none, none, none, gleam@option:map( Markup, fun(Field@0) -> {send_message_reply_inline_keyboard_markup_parameters, Field@0} end )} ), _pipe@1 = gleam@result:map( _pipe, fun(Message) -> erlang:element(2, Message) end ), gleam@result:map_error( _pipe@1, fun(Field@0) -> {api_error, Field@0} end ) end ). -file("src/telega/dialog/render.gleam", 548). ?DOC( " The exact callback-data string for an action — the single place the\n" " `dlg:` scheme is constructed. Build-time validation measures budgets\n" " through this same function, so packing and validation can never diverge.\n" ). -spec callback_data(binary(), binary(), binary(), gleam@option:option(binary())) -> binary(). callback_data(Dialog_id, Window_id, Action_id, Arg) -> Data = <<<<<<<<<<"dlg:"/utf8, Dialog_id/binary>>/binary, ":"/utf8>>/binary, Window_id/binary>>/binary, ":"/utf8>>/binary, Action_id/binary>>, case Arg of {some, Arg@1} -> <<<>/binary, Arg@1/binary>>; none -> Data end. -file("src/telega/dialog/render.gleam", 567). ?DOC( " Build callback data for an action, checking the reserved `:` in the\n" " action id and Telegram's 64-byte limit. The `w:` action-id prefix is the\n" " widget namespace (`w::`) and is the only place `:` is\n" " allowed — the engine routes such presses to the widget's `on_event`. The\n" " bare id `\"w\"` is rejected too: with a `:`-carrying arg it would be\n" " indistinguishable from a widget event on parse.\n" ). -spec pack_callback_data( binary(), binary(), binary(), gleam@option:option(binary()) ) -> {ok, binary()} | {error, render_error()}. pack_callback_data(Dialog_id, Window_id, Action_id, Arg) -> gleam@bool:guard( not (gleam_stdlib:string_starts_with(Action_id, <<"w:"/utf8>>) orelse not gleam_stdlib:contains_string( Action_id, <<":"/utf8>> )), {error, {invalid_button, Action_id, <<"action id must not contain ':' (reserved for the w: widget namespace)"/utf8>>}}, fun() -> gleam@bool:guard( Action_id =:= <<"w"/utf8>>, {error, {invalid_button, Action_id, <<"action id 'w' is reserved for the widget namespace"/utf8>>}}, fun() -> Data = callback_data(Dialog_id, Window_id, Action_id, Arg), Bytes = erlang:byte_size(Data), gleam@bool:guard( Bytes > 64, {error, {invalid_button, Action_id, <<<<<<"callback data is "/utf8, (erlang:integer_to_binary(Bytes))/binary>>/binary, " bytes, max is "/utf8>>/binary, (erlang:integer_to_binary(64))/binary>>}}, fun() -> {ok, Data} end ) end ) end ). -file("src/telega/dialog/render.gleam", 509). -spec build_button(binary(), binary(), telega@dialog@types:dialog_button()) -> {ok, telega@model@types:inline_keyboard_button()} | {error, render_error()}. build_button(Dialog_id, Window_id, Button) -> case Button of {action_button, Text, Action_id} -> gleam@result:'try'( pack_callback_data(Dialog_id, Window_id, Action_id, none), fun(Data) -> {ok, telega@keyboard:inline_raw_callback_button(Text, Data)} end ); {action_arg_button, Text@1, Action_id@1, Arg} -> gleam@result:'try'( pack_callback_data( Dialog_id, Window_id, Action_id@1, {some, Arg} ), fun(Data@1) -> {ok, telega@keyboard:inline_raw_callback_button( Text@1, Data@1 )} end ); {url_button, Text@2, Url} -> {ok, telega@keyboard:inline_url_button(Text@2, Url)}; {web_app_button, Text@3, Url@1} -> {ok, telega@keyboard:inline_web_app_button(Text@3, Url@1)}; {noop_button, Text@4} -> {ok, telega@keyboard:inline_copy_text_button(Text@4, Text@4)} end. -file("src/telega/dialog/render.gleam", 489). ?DOC( " Build the inline keyboard for a window, generating callback data by the\n" " `dlg:::[:]` scheme and validating the 64-byte\n" " limit and reserved characters per button.\n" ). -spec build_markup( binary(), binary(), list(list(telega@dialog@types:dialog_button())) ) -> {ok, gleam@option:option(telega@model@types:inline_keyboard_markup())} | {error, render_error()}. build_markup(Dialog_id, Window_id, Buttons) -> case Buttons of [] -> {ok, none}; _ -> gleam@result:'try'( gleam@list:try_map( Buttons, fun(Row) -> gleam@list:try_map( Row, fun(Button) -> build_button(Dialog_id, Window_id, Button) end ) end ), fun(Rows) -> {ok, {some, {inline_keyboard_markup, Rows}}} end ) end. -file("src/telega/dialog/render.gleam", 95). ?DOC( " Render a window into the live dialog message following the edit-or-send\n" " strategy matrix (see the module doc): send when there is no message yet,\n" " edit when the kind is unchanged, delete-and-resend when it changed. Edits\n" " fall back to a fresh send when the old message is gone. Returns the id\n" " and kind of the live message.\n" ). -spec render_window( telega@bot:context(any(), any(), any()), integer(), gleam@option:option({integer(), message_kind()}), binary(), binary(), telega@dialog@types:rendered_window() ) -> {ok, {integer(), message_kind()}} | {error, render_error()}. render_window(Ctx, Chat_id, Message, Dialog_id, Window_id, Window) -> gleam@result:'try'( build_markup(Dialog_id, Window_id, erlang:element(3, Window)), fun(Markup) -> {Text, Parse_mode} = telega@format:render(erlang:element(2, Window)), Parse_mode@1 = telega@format:parse_mode_to_string(Parse_mode), Send_text = fun() -> _pipe = send_window(Ctx, Chat_id, Text, Parse_mode@1, Markup), gleam@result:map(_pipe, fun(Id) -> {Id, text_message} end) end, Send_media = fun(Media) -> _pipe@1 = send_media_window( Ctx, Chat_id, Media, Text, Parse_mode@1, Markup ), gleam@result:map( _pipe@1, fun(Id@1) -> {Id@1, media_message} end ) end, case {Message, erlang:element(4, Window)} of {none, none} -> Send_text(); {none, {some, Media@1}} -> Send_media(Media@1); {{some, {Message_id, text_message}}, none} -> _pipe@2 = edit_window( Ctx, Chat_id, Message_id, Text, Parse_mode@1, Markup ), or_send_fallback( _pipe@2, Message_id, text_message, Send_text ); {{some, {Message_id@1, media_message}}, {some, Media@2}} -> _pipe@3 = edit_media_window( Ctx, Chat_id, Message_id@1, Media@2, Text, Parse_mode@1, Markup ), or_send_fallback( _pipe@3, Message_id@1, media_message, fun() -> Send_media(Media@2) end ); {{some, {Message_id@2, text_message}}, {some, Media@3}} -> delete_best_effort(Ctx, Chat_id, Message_id@2), Send_media(Media@3); {{some, {Message_id@3, media_message}}, none} -> delete_best_effort(Ctx, Chat_id, Message_id@3), Send_text() end end ). -file("src/telega/dialog/render.gleam", 466). ?DOC( " Remove the inline keyboard from the live dialog message (used on `Done`).\n" " Best effort: failures are reported but callers usually ignore them.\n" ). -spec remove_keyboard( telega@bot:context(any(), any(), any()), integer(), integer() ) -> {ok, nil} | {error, telega@error:telega_error()}. remove_keyboard(Ctx, Chat_id, Message_id) -> _pipe = telega@api:edit_message_reply_markup( erlang:element(5, erlang:element(4, Ctx)), {edit_message_reply_markup_parameters, none, {some, {int, Chat_id}}, {some, Message_id}, none, none} ), gleam@result:replace(_pipe, nil). -file("src/telega/dialog/render.gleam", 699). -spec mark_answered(binary()) -> nil. mark_answered(Query_id) -> _ = erlang:put(<<"__telega_dialog_answered"/utf8>>, Query_id), nil. -file("src/telega/dialog/render.gleam", 656). -spec do_answer( telega@bot:context(any(), any(), any()), binary(), gleam@option:option(binary()), boolean() ) -> {ok, nil} | {error, telega@error:telega_error()}. do_answer(Ctx, Query_id, Text, Show_alert) -> _pipe = telega@api:answer_callback_query( erlang:element(5, erlang:element(4, Ctx)), {answer_callback_query_parameters, Query_id, Text, {some, Show_alert}, none, none} ), gleam@result:replace(_pipe, nil). -file("src/telega/dialog/render.gleam", 641). -spec answer_with( telega@bot:context(any(), any(), any()), gleam@option:option(binary()), boolean() ) -> {ok, nil} | {error, telega@error:telega_error()}. answer_with(Ctx, Text, Show_alert) -> case erlang:element(3, Ctx) of {callback_query_update, _, _, Query, _} -> gleam@result:'try'( do_answer(Ctx, erlang:element(2, Query), Text, Show_alert), fun(_) -> mark_answered(erlang:element(2, Query)), {ok, nil} end ); _ -> {ok, nil} end. -file("src/telega/dialog/render.gleam", 607). ?DOC( " Show a modal alert to the user who pressed the button. Call from\n" " `on_action` before returning; the engine then skips its automatic\n" " `answer_callback_query` for this event.\n" ). -spec alert(telega@bot:context(any(), any(), any()), binary()) -> {ok, nil} | {error, telega@error:telega_error()}. alert(Ctx, Text) -> answer_with(Ctx, {some, Text}, true). -file("src/telega/dialog/render.gleam", 616). ?DOC( " Show a toast notification at the top of the chat. Same contract as\n" " `alert`, but without the modal dialog.\n" ). -spec toast(telega@bot:context(any(), any(), any()), binary()) -> {ok, nil} | {error, telega@error:telega_error()}. toast(Ctx, Text) -> answer_with(Ctx, {some, Text}, false). -file("src/telega/dialog/render.gleam", 704). -spec take_answered() -> gleam@option:option(binary()). take_answered() -> _pipe = erlang:erase(<<"__telega_dialog_answered"/utf8>>), _pipe@1 = gleam@dynamic@decode:run( _pipe, {decoder, fun gleam@dynamic@decode:decode_string/1} ), gleam@option:from_result(_pipe@1). -file("src/telega/dialog/render.gleam", 627). ?DOC(false). -spec auto_answer(telega@bot:context(any(), any(), any())) -> nil. auto_answer(Ctx) -> case erlang:element(3, Ctx) of {callback_query_update, _, _, Query, _} -> case take_answered() =:= {some, erlang:element(2, Query)} of true -> nil; false -> _ = do_answer(Ctx, erlang:element(2, Query), none, false), nil end; _ -> nil end. -file("src/telega/dialog/render.gleam", 678). ?DOC(false). -spec answer_quietly( telega@bot:context(any(), any(), any()), gleam@option:option(binary()) ) -> nil. answer_quietly(Ctx, Text) -> case erlang:element(3, Ctx) of {callback_query_update, _, _, Query, _} -> _ = do_answer(Ctx, erlang:element(2, Query), Text, false), nil; _ -> nil end. -file("src/telega/dialog/render.gleam", 718). ?DOC(false). -spec describe_error(render_error()) -> binary(). describe_error(Error) -> case Error of {api_error, Api_error} -> telega@error:to_string(Api_error); {invalid_button, Action_id, Reason} -> <<<<<<"invalid button '"/utf8, Action_id/binary>>/binary, "': "/utf8>>/binary, Reason/binary>> end.