-module(telega@dialog@engine). -compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). -define(FILEPATH, "src/telega/dialog/engine.gleam"). -export([parse_widget_event/1, parse_callback_data/1, compile/1]). -export_type([compiled_dialog/3, compiled_sub/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( " Compiles a `Dialog` into a `Flow(String, ...)` and dispatches events.\n" "\n" " Each window becomes a flow step (step type = the window id). The step\n" " handler runs the render → wait → dispatch cycle:\n" "\n" " - `Pending` — render the window (edit-or-send) and park on `Wait`\n" " - `DataCallback` — parse the `dlg:` scheme, guard against stale/foreign\n" " messages, run `window.on_action`, auto-answer the callback query\n" " - `TextInput` — run `window.on_text` if set, otherwise re-render\n" "\n" " The engine is **type-erased**: `telega/dialog.build()` wraps every\n" " user-typed window into a `Window(String, ...)` whose closures carry the\n" " dialog's own state codec, so the engine only ever moves the encoded state\n" " string around. This is what lets a sub-dialog with a different state type\n" " live inside its parent's flow.\n" "\n" " ## Sub-dialogs\n" "\n" " Sub-dialogs deliberately do NOT use the flow engine's\n" " `EnterSubflow`/`FlowStackFrame` machinery: that path never re-executes\n" " the parent step after the sub-flow returns (and sets no wait token, so\n" " auto-resume cannot wake it), resumes sub-flow steps against the parent's\n" " step registry, and loses the parent's history. Instead the sub-dialog's\n" " windows are compiled into the parent flow as `.` steps\n" " and the engine keeps its own return bookkeeping in instance data:\n" "\n" " | key | content |\n" " |---|---|\n" " | `__dialog_sub` | active sub-dialog id (absent at top level) |\n" " | `__dialog_return_window` | parent window that emitted `StartSub` |\n" " | `__dialog_sub_saved` | parent's encoded state while the sub runs |\n" "\n" " The live message, its kind, waits, TTL and persistence are shared with\n" " the parent for free (same instance). A `Back` that would cross the sub\n" " boundary cancels the sub and returns to the parent window without calling\n" " `on_sub_result`.\n" "\n" " Navigation deliberately never uses the flow `GoTo` action — it erases\n" " history, which would break `Back`. Only `Next`/`NextString`/`Back` are\n" " emitted, plus the history-preserving `Jump` for sub-dialog enter/return\n" " (which must not push sub steps onto the parent history).\n" ). -type compiled_dialog(AXON, AXOO, AXOP) :: {compiled_dialog, binary(), gleam@dict:dict(binary(), telega@dialog@types:window(binary(), AXON, AXOO, AXOP)), binary(), fun(() -> binary()), gleam@option:option(fun((binary(), telega@bot:context(AXON, AXOO, AXOP)) -> {ok, telega@bot:context(AXON, AXOO, AXOP)} | {error, AXOO})), gleam@dict:dict(binary(), compiled_sub()), telega@flow@types:flow_storage(AXOO), gleam@option:option(integer()), fun((telega@bot:context(AXON, AXOO, AXOP)) -> telega@dialog@types:labels())}. -type compiled_sub() :: {compiled_sub, binary(), binary(), fun((binary(), gleam@dict:dict(binary(), binary())) -> binary()), fun((binary()) -> gleam@dict:dict(binary(), binary()))}. -file("src/telega/dialog/engine.gleam", 839). ?DOC( " The encoded state string. Decoding (and the fallback to the initial\n" " state on codec mismatch) happens inside the erased window closures built\n" " by `dialog.build()`.\n" ). -spec load_state( compiled_dialog(any(), any(), any()), telega@flow@types:flow_instance() ) -> binary(). load_state(Dialog, Inst) -> case telega@flow@instance:get_data(Inst, <<"__dialog_state"/utf8>>) of {some, Raw} -> Raw; none -> (erlang:element(5, Dialog))() end. -file("src/telega/dialog/engine.gleam", 877). -spec kind_to_string(telega@dialog@render:message_kind()) -> binary(). kind_to_string(Kind) -> case Kind of text_message -> <<"text"/utf8>>; media_message -> <<"media"/utf8>> end. -file("src/telega/dialog/engine.gleam", 992). -spec emit_dialog_event( compiled_dialog(any(), any(), any()), binary(), binary(), list({binary(), integer()}) ) -> nil. emit_dialog_event(Dialog, Event, Window_id, Measurements) -> telega@telemetry:execute( [<<"telega"/utf8>>, <<"dialog"/utf8>>, Event], Measurements, [{<<"dialog_id"/utf8>>, {string_value, erlang:element(2, Dialog)}}, {<<"window_id"/utf8>>, {string_value, Window_id}}] ). -file("src/telega/dialog/engine.gleam", 856). -spec message_id(telega@flow@types:flow_instance()) -> gleam@option:option(integer()). message_id(Inst) -> case telega@flow@instance:get_data(Inst, <<"__dialog_message_id"/utf8>>) of {some, Raw} -> gleam@option:from_result(gleam_stdlib:parse_int(Raw)); none -> none end. -file("src/telega/dialog/engine.gleam", 866). ?DOC( " The live message with its kind — what the edit-or-send matrix in\n" " `dialog/render` branches on. A missing/unknown kind means `text`: dialogs\n" " persisted before media support only ever sent text messages.\n" ). -spec live_message(telega@flow@types:flow_instance()) -> gleam@option:option({integer(), telega@dialog@render:message_kind()}). live_message(Inst) -> gleam@option:map( message_id(Inst), fun(Message_id) -> Kind = case telega@flow@instance:get_data( Inst, <<"__dialog_message_kind"/utf8>> ) of {some, <<"media"/utf8>>} -> media_message; _ -> text_message end, {Message_id, Kind} end ). -file("src/telega/dialog/engine.gleam", 764). -spec load_widget_store(telega@flow@types:flow_instance(), binary(), binary()) -> telega@dialog@types:widget_store(). load_widget_store(Inst, Window_id, Widget_id) -> _pipe = telega@flow@instance:get_data( Inst, telega@dialog@widget:store_data_key(Window_id, Widget_id) ), _pipe@1 = gleam@option:map(_pipe, fun telega@dialog@types:decode_store/1), _pipe@2 = gleam@option:map(_pipe@1, fun gleam@option:from_result/1), _pipe@3 = gleam@option:flatten(_pipe@2), gleam@option:unwrap(_pipe@3, telega@dialog@types:new_store()). -file("src/telega/dialog/engine.gleam", 739). ?DOC( " Widget rows come after the window's own buttons, widgets in declaration\n" " order.\n" ). -spec append_widget_rows( compiled_dialog(AXYS, AXYT, AXYU), telega@dialog@types:window(binary(), AXYS, AXYT, AXYU), telega@bot:context(AXYS, AXYT, AXYU), telega@flow@types:flow_instance(), binary(), telega@dialog@types:rendered_window() ) -> telega@dialog@types:rendered_window(). append_widget_rows(Dialog, Window, Ctx, Inst, State, Rendered) -> case erlang:element(6, Window) of [] -> Rendered; Widgets -> Labels = (erlang:element(10, Dialog))(Ctx), Rows = gleam@list:flat_map( Widgets, fun(Item) -> Store = load_widget_store( Inst, erlang:element(2, Window), erlang:element(2, Item) ), (erlang:element(3, Item))( {widget_ctx, State, Store, Labels, Ctx} ) end ), {rendered_window, erlang:element(2, Rendered), lists:append(erlang:element(3, Rendered), Rows), erlang:element(4, Rendered)} end. -file("src/telega/dialog/engine.gleam", 702). -spec render_window( compiled_dialog(AXYD, AXYE, AXYF), telega@dialog@types:window(binary(), AXYD, AXYE, AXYF), telega@bot:context(AXYD, AXYE, AXYF), telega@flow@types:flow_instance(), binary() ) -> {ok, telega@flow@types:flow_instance()} | {error, telega@dialog@render:render_error()}. render_window(Dialog, Window, Ctx, Inst, State) -> telega@dialog@widget:stash_stores( erlang:element(3, erlang:element(6, Inst)) ), Rendered = (erlang:element(3, Window))(State, Ctx), Rendered@1 = append_widget_rows(Dialog, Window, Ctx, Inst, State, Rendered), Started_at = erlang:monotonic_time(), case telega@dialog@render:render_window( Ctx, erlang:element(3, erlang:element(3, Ctx)), live_message(Inst), erlang:element(2, Dialog), erlang:element(2, Window), Rendered@1 ) of {ok, {Message_id, Kind}} -> emit_dialog_event( Dialog, <<"render"/utf8>>, erlang:element(2, Window), [{<<"duration"/utf8>>, erlang:monotonic_time() - Started_at}] ), _pipe = Inst, _pipe@1 = telega@flow@instance:store_data( _pipe, <<"__dialog_message_id"/utf8>>, erlang:integer_to_binary(Message_id) ), _pipe@2 = telega@flow@instance:store_data( _pipe@1, <<"__dialog_message_kind"/utf8>>, kind_to_string(Kind) ), {ok, _pipe@2}; {error, Render_error} -> {error, Render_error} end. -file("src/telega/dialog/engine.gleam", 827). -spec current_window( compiled_dialog(AXZQ, AXZR, AXZS), telega@flow@types:flow_instance() ) -> {ok, telega@dialog@types:window(binary(), AXZQ, AXZR, AXZS)} | {error, nil}. current_window(Dialog, Inst) -> gleam_stdlib:map_get( erlang:element(3, Dialog), erlang:element(2, erlang:element(6, Inst)) ). -file("src/telega/dialog/engine.gleam", 796). ?DOC( " Best-effort re-render for the `on_error` hook. Skipped entirely when the\n" " instance is no longer stored (e.g. the error came from `on_done` after\n" " completion): a finished dialog must not be re-rendered or resurrected.\n" " A successful render is persisted — the edit may have fallen back to a\n" " fresh send, and losing its message id would strand the old message with a\n" " live keyboard. The stored wait token is kept as-is: the in-memory\n" " instance's token was already consumed by this update.\n" ). -spec try_render_current( compiled_dialog(AXZF, AXZG, AXZH), telega@bot:context(AXZF, AXZG, AXZH), telega@flow@types:flow_instance() ) -> {ok, nil} | {error, nil}. try_render_current(Dialog, Ctx, Inst) -> case (erlang:element(3, erlang:element(8, Dialog)))(erlang:element(2, Inst)) of {ok, {some, Stored}} -> case current_window(Dialog, Inst) of {ok, Window} -> case render_window( Dialog, Window, Ctx, Inst, load_state(Dialog, Inst) ) of {ok, Updated} -> _ = (erlang:element(2, erlang:element(8, Dialog)))( {flow_instance, erlang:element(2, Updated), erlang:element(3, Updated), erlang:element(4, Updated), erlang:element(5, Updated), erlang:element(6, Updated), erlang:element(7, Updated), erlang:element(8, Stored), erlang:element(9, Stored), erlang:element(10, Updated), erlang:element(11, Updated)} ), {ok, nil}; {error, _} -> {error, nil} end; {error, nil} -> {error, nil} end; _ -> {error, nil} end. -file("src/telega/dialog/engine.gleam", 1038). -spec log_dialog_error( compiled_dialog(any(), AYCC, any()), telega@flow@types:flow_instance(), gleam@option:option(AYCC) ) -> nil. log_dialog_error(Dialog, Inst, Maybe_error) -> emit_dialog_event( Dialog, <<"error"/utf8>>, erlang:element(2, erlang:element(6, Inst)), [{<<"count"/utf8>>, 1}] ), Details = case Maybe_error of {some, Error} -> gleam@string:inspect(Error); none -> <<<<"unknown step '"/utf8, (erlang:element(2, erlang:element(6, Inst)))/binary>>/binary, "'"/utf8>> end, logging:log( error, <<<<<<<<<<"[dialog:"/utf8, (erlang:element(2, Dialog))/binary>>/binary, "] handler error in window '"/utf8>>/binary, (erlang:element(2, erlang:element(6, Inst)))/binary>>/binary, "': "/utf8>>/binary, Details/binary>> ). -file("src/telega/dialog/engine.gleam", 931). ?DOC( " Park the step waiting for the next event, clearing the consumed wait\n" " result so a later direct `execute_step` (e.g. a repeated start command)\n" " sees `Pending` and re-renders instead of re-processing a stale event.\n" ). -spec wait( telega@bot:context(AYAN, AYAO, AYAP), telega@flow@types:flow_instance() ) -> {ok, {telega@bot:context(AYAN, AYAO, AYAP), telega@flow@types:flow_action(binary()), telega@flow@types:flow_instance()}} | {error, AYAO}. wait(Ctx, Inst) -> Inst@1 = telega@flow@instance:clear_wait_result(Inst), {ok, {Ctx, wait, Inst@1}}. -file("src/telega/dialog/engine.gleam", 1021). -spec log_render_error( compiled_dialog(any(), any(), any()), binary(), telega@dialog@render:render_error() ) -> nil. log_render_error(Dialog, Window_id, Render_error) -> emit_dialog_event( Dialog, <<"render_error"/utf8>>, Window_id, [{<<"count"/utf8>>, 1}] ), logging:log( error, <<<<<<<<<<"[dialog:"/utf8, (erlang:element(2, Dialog))/binary>>/binary, "] failed to render window '"/utf8>>/binary, Window_id/binary>>/binary, "': "/utf8>>/binary, (telega@dialog@render:describe_error(Render_error))/binary>> ). -file("src/telega/dialog/engine.gleam", 686). ?DOC( " Render a window and park on `Wait`, keeping the dialog alive if the\n" " render fails.\n" ). -spec render_and_park( compiled_dialog(AXXM, AXXN, AXXO), telega@dialog@types:window(binary(), AXXM, AXXN, AXXO), telega@bot:context(AXXM, AXXN, AXXO), telega@flow@types:flow_instance(), binary() ) -> {ok, {telega@bot:context(AXXM, AXXN, AXXO), telega@flow@types:flow_action(binary()), telega@flow@types:flow_instance()}} | {error, AXXN}. render_and_park(Dialog, Window, Ctx, Inst, State) -> case render_window(Dialog, Window, Ctx, Inst, State) of {ok, Inst@1} -> wait(Ctx, Inst@1); {error, Render_error} -> log_render_error(Dialog, erlang:element(2, Window), Render_error), wait(Ctx, Inst) end. -file("src/telega/dialog/engine.gleam", 197). -spec render_and_wait( compiled_dialog(AXPO, AXPP, AXPQ), telega@dialog@types:window(binary(), AXPO, AXPP, AXPQ), telega@bot:context(AXPO, AXPP, AXPQ), telega@flow@types:flow_instance() ) -> {ok, {telega@bot:context(AXPO, AXPP, AXPQ), telega@flow@types:flow_action(binary()), telega@flow@types:flow_instance()}} | {error, AXPP}. render_and_wait(Dialog, Window, Ctx, Inst) -> render_and_park(Dialog, Window, Ctx, Inst, load_state(Dialog, Inst)). -file("src/telega/dialog/engine.gleam", 1004). -spec log_unknown_window( compiled_dialog(any(), any(), any()), binary(), binary() ) -> nil. log_unknown_window(Dialog, From, To) -> logging:log( error, <<<<<<<<<<<<"[dialog:"/utf8, (erlang:element(2, Dialog))/binary>>/binary, "] Goto to unknown window '"/utf8>>/binary, To/binary>>/binary, "' from '"/utf8>>/binary, From/binary>>/binary, "'"/utf8>> ). -file("src/telega/dialog/engine.gleam", 669). ?DOC( " Switch to `target` without touching the history — the navigation\n" " primitive for sub-dialog enter/return. Emits the flow `Jump` action, so\n" " the transition goes through the flow engine (persistence, telemetry) and\n" " the target step renders via its own `Pending` handling.\n" ). -spec jump_and_render( compiled_dialog(AXWZ, AXXA, AXXB), telega@bot:context(AXWZ, AXXA, AXXB), telega@flow@types:flow_instance(), binary() ) -> {ok, {telega@bot:context(AXWZ, AXXA, AXXB), telega@flow@types:flow_action(binary()), telega@flow@types:flow_instance()}} | {error, AXXA}. jump_and_render(Dialog, Ctx, Inst, Target) -> case gleam@dict:has_key(erlang:element(3, Dialog), Target) of true -> {ok, {Ctx, {jump, Target}, Inst}}; false -> log_unknown_window( Dialog, erlang:element(2, erlang:element(6, Inst)), Target ), wait(Ctx, Inst) end. -file("src/telega/dialog/engine.gleam", 849). -spec save_state(telega@flow@types:flow_instance(), binary()) -> telega@flow@types:flow_instance(). save_state(Inst, State) -> telega@flow@instance:store_data(Inst, <<"__dialog_state"/utf8>>, State). -file("src/telega/dialog/engine.gleam", 911). -spec set_data( telega@flow@types:flow_instance(), gleam@dict:dict(binary(), binary()) ) -> telega@flow@types:flow_instance(). set_data(Inst, Data) -> {flow_instance, erlang:element(2, Inst), erlang:element(3, Inst), erlang:element(4, Inst), erlang:element(5, Inst), begin _record = erlang:element(6, Inst), {flow_state, erlang:element(2, _record), Data, erlang:element(4, _record), erlang:element(5, _record), erlang:element(6, _record)} end, erlang:element(7, Inst), erlang:element(8, Inst), erlang:element(9, Inst), erlang:element(10, Inst), erlang:element(11, Inst)}. -file("src/telega/dialog/engine.gleam", 653). ?DOC( " A re-entered sub-dialog must not see widget selections from a previous\n" " run: drop all of its windows' widget stores.\n" ). -spec reset_sub_widget_stores(telega@flow@types:flow_instance(), binary()) -> telega@flow@types:flow_instance(). reset_sub_widget_stores(Inst, Sub_id) -> Prefix = <<<<"__dialog_widget:"/utf8, Sub_id/binary>>/binary, "."/utf8>>, Data = gleam@dict:filter( erlang:element(3, erlang:element(6, Inst)), fun(Key, _) -> not gleam_stdlib:string_starts_with(Key, Prefix) end ), set_data(Inst, Data). -file("src/telega/dialog/engine.gleam", 473). ?DOC( " Enter a sub-dialog: park the parent state, swap in the sub's initial\n" " state, reset the sub's widget stores (a re-entered sub starts clean), and\n" " jump-render its initial window. The jump must not push the sub step onto\n" " the parent history — `Back` boundary detection relies on it (see the\n" " module doc).\n" ). -spec start_sub( compiled_dialog(AXUG, AXUH, AXUI), telega@bot:context(AXUG, AXUH, AXUI), telega@flow@types:flow_instance(), binary(), gleam@dict:dict(binary(), binary()), binary(), fun(() -> {ok, {telega@bot:context(AXUG, AXUH, AXUI), telega@flow@types:flow_action(binary()), telega@flow@types:flow_instance()}} | {error, AXUH}) ) -> {ok, {telega@bot:context(AXUG, AXUH, AXUI), telega@flow@types:flow_action(binary()), telega@flow@types:flow_instance()}} | {error, AXUH}. start_sub(Dialog, Ctx, Inst, Sub_id, Args, Parent_state, When_unknown) -> case gleam_stdlib:map_get(erlang:element(7, Dialog), Sub_id) of {error, nil} -> logging:log( error, <<<<<<<<<<<<"[dialog:"/utf8, (erlang:element(2, Dialog))/binary>>/binary, "] StartSub to unknown sub-dialog '"/utf8>>/binary, Sub_id/binary>>/binary, "' from '"/utf8>>/binary, (erlang:element(2, erlang:element(6, Inst)))/binary>>/binary, "'"/utf8>> ), When_unknown(); {ok, Sub} -> emit_dialog_event( Dialog, <<"sub_start"/utf8>>, erlang:element(2, erlang:element(6, Inst)), [{<<"count"/utf8>>, 1}] ), Inst@1 = begin _pipe = Inst, _pipe@1 = reset_sub_widget_stores(_pipe, Sub_id), _pipe@2 = telega@flow@instance:store_data( _pipe@1, <<"__dialog_sub"/utf8>>, Sub_id ), _pipe@3 = telega@flow@instance:store_data( _pipe@2, <<"__dialog_return_window"/utf8>>, erlang:element(2, erlang:element(6, Inst)) ), _pipe@4 = telega@flow@instance:store_data( _pipe@3, <<"__dialog_sub_saved"/utf8>>, Parent_state ), save_state( _pipe@4, (erlang:element(4, Sub))(Parent_state, Args) ) end, jump_and_render(Dialog, Ctx, Inst@1, erlang:element(3, Sub)) end. -file("src/telega/dialog/engine.gleam", 448). -spec finish_dialog( compiled_dialog(AXTT, AXTU, AXTV), telega@bot:context(AXTT, AXTU, AXTV), telega@flow@types:flow_instance(), binary() ) -> {ok, {telega@bot:context(AXTT, AXTU, AXTV), telega@flow@types:flow_action(binary()), telega@flow@types:flow_instance()}} | {error, AXTU}. finish_dialog(_, Ctx, Inst, State) -> Inst@1 = save_state(Inst, State), case message_id(Inst@1) of {some, Message_id} -> _ = telega@dialog@render:remove_keyboard( Ctx, erlang:element(3, erlang:element(3, Ctx)), Message_id ), nil; none -> nil end, {ok, {Ctx, {complete, erlang:element(3, erlang:element(6, Inst@1))}, Inst@1}}. -file("src/telega/dialog/engine.gleam", 884). -spec set_current_step(telega@flow@types:flow_instance(), binary()) -> telega@flow@types:flow_instance(). set_current_step(Inst, Step) -> {flow_instance, erlang:element(2, Inst), erlang:element(3, Inst), erlang:element(4, Inst), erlang:element(5, Inst), begin _record = erlang:element(6, Inst), {flow_state, Step, erlang:element(3, _record), erlang:element(4, _record), erlang:element(5, _record), erlang:element(6, _record)} end, erlang:element(7, Inst), erlang:element(8, Inst), erlang:element(9, Inst), erlang:element(10, Inst), erlang:element(11, Inst)}. -file("src/telega/dialog/engine.gleam", 894). -spec set_history(telega@flow@types:flow_instance(), list(binary())) -> telega@flow@types:flow_instance(). set_history(Inst, History) -> {flow_instance, erlang:element(2, Inst), erlang:element(3, Inst), erlang:element(4, Inst), erlang:element(5, Inst), begin _record = erlang:element(6, Inst), {flow_state, erlang:element(2, _record), erlang:element(3, _record), History, erlang:element(5, _record), erlang:element(6, _record)} end, erlang:element(7, Inst), erlang:element(8, Inst), erlang:element(9, Inst), erlang:element(10, Inst), erlang:element(11, Inst)}. -file("src/telega/dialog/engine.gleam", 904). -spec push_history(telega@flow@types:flow_instance(), binary()) -> telega@flow@types:flow_instance(). push_history(Inst, Step) -> set_history(Inst, [Step | erlang:element(4, erlang:element(6, Inst))]). -file("src/telega/dialog/engine.gleam", 557). ?DOC( " Map the action returned by `on_sub_result`. The current step is still the\n" " sub's last window, so plain `apply_action` semantics (\"re-render the\n" " current window\") would render the wrong window — every branch navigates\n" " relative to the return window instead.\n" ). -spec apply_sub_return_action( compiled_dialog(AXVM, AXVN, AXVO), telega@bot:context(AXVM, AXVN, AXVO), telega@flow@types:flow_instance(), binary(), telega@dialog@types:dialog_action(binary()) ) -> {ok, {telega@bot:context(AXVM, AXVN, AXVO), telega@flow@types:flow_action(binary()), telega@flow@types:flow_instance()}} | {error, AXVN}. apply_sub_return_action(Dialog, Ctx, Inst, Return_window, Action) -> case Action of {stay, State} -> jump_and_render(Dialog, Ctx, save_state(Inst, State), Return_window); {goto, Window_id, State@1} -> Inst@1 = save_state(Inst, State@1), case gleam@dict:has_key(erlang:element(3, Dialog), Window_id) of true -> Inst@2 = push_history(Inst@1, Return_window), jump_and_render(Dialog, Ctx, Inst@2, Window_id); false -> log_unknown_window(Dialog, Return_window, Window_id), jump_and_render(Dialog, Ctx, Inst@1, Return_window) end; {back, State@2} -> Inst@3 = save_state(Inst, State@2), case erlang:element(4, erlang:element(6, Inst@3)) of [Previous | Rest] -> jump_and_render( Dialog, Ctx, set_history(Inst@3, Rest), Previous ); [] -> jump_and_render(Dialog, Ctx, Inst@3, Return_window) end; {done, State@3} -> finish_dialog(Dialog, Ctx, Inst, State@3); {start_sub, Sub_id, Args, State@4} -> Inst@4 = set_current_step(Inst, Return_window), start_sub( Dialog, Ctx, Inst@4, Sub_id, Args, State@4, fun() -> jump_and_render( Dialog, Ctx, save_state(Inst@4, State@4), Return_window ) end ) end. -file("src/telega/dialog/engine.gleam", 921). -spec remove_data(telega@flow@types:flow_instance(), binary()) -> telega@flow@types:flow_instance(). remove_data(Inst, Key) -> set_data( Inst, gleam@dict:delete(erlang:element(3, erlang:element(6, Inst)), Key) ). -file("src/telega/dialog/engine.gleam", 624). ?DOC( " Restore the parent context: parent state back into `__dialog_state`, sub\n" " bookkeeping keys cleared, sub steps stripped from the history top.\n" ). -spec leave_sub( compiled_dialog(any(), any(), any()), telega@flow@types:flow_instance(), binary() ) -> telega@flow@types:flow_instance(). leave_sub(Dialog, Inst, Sub_id) -> Parent_state = begin _pipe = telega@flow@instance:get_data( Inst, <<"__dialog_sub_saved"/utf8>> ), gleam@option:unwrap(_pipe, (erlang:element(5, Dialog))()) end, Prefix = <>, History = gleam@list:drop_while( erlang:element(4, erlang:element(6, Inst)), fun(_capture) -> gleam_stdlib:string_starts_with(_capture, Prefix) end ), _pipe@1 = Inst, _pipe@2 = save_state(_pipe@1, Parent_state), _pipe@3 = remove_data(_pipe@2, <<"__dialog_sub"/utf8>>), _pipe@4 = remove_data(_pipe@3, <<"__dialog_return_window"/utf8>>), _pipe@5 = remove_data(_pipe@4, <<"__dialog_sub_saved"/utf8>>), set_history(_pipe@5, History). -file("src/telega/dialog/engine.gleam", 643). -spec return_window_of( compiled_dialog(any(), any(), any()), telega@flow@types:flow_instance() ) -> binary(). return_window_of(Dialog, Inst) -> _pipe = telega@flow@instance:get_data( Inst, <<"__dialog_return_window"/utf8>> ), gleam@option:unwrap(_pipe, erlang:element(4, Dialog)). -file("src/telega/dialog/engine.gleam", 516). ?DOC( " The sub-dialog finished with `Done`: export its result, restore the\n" " parent state and history, and hand the result to the return window's\n" " `on_sub_result` (default: just re-render the return window).\n" ). -spec finish_sub( compiled_dialog(AXUZ, AXVA, AXVB), telega@bot:context(AXUZ, AXVA, AXVB), telega@flow@types:flow_instance(), binary(), binary() ) -> {ok, {telega@bot:context(AXUZ, AXVA, AXVB), telega@flow@types:flow_action(binary()), telega@flow@types:flow_instance()}} | {error, AXVA}. finish_sub(Dialog, Ctx, Inst, Sub_id, Sub_state) -> emit_dialog_event( Dialog, <<"sub_done"/utf8>>, erlang:element(2, erlang:element(6, Inst)), [{<<"count"/utf8>>, 1}] ), Result = case gleam_stdlib:map_get(erlang:element(7, Dialog), Sub_id) of {ok, Sub} -> (erlang:element(5, Sub))(Sub_state); {error, nil} -> maps:new() end, Return_window = return_window_of(Dialog, Inst), Inst@1 = leave_sub(Dialog, Inst, Sub_id), Parent_state = load_state(Dialog, Inst@1), On_sub_result = begin _pipe = gleam_stdlib:map_get(erlang:element(3, Dialog), Return_window), _pipe@1 = gleam@option:from_result(_pipe), gleam@option:then(_pipe@1, fun(Window) -> erlang:element(7, Window) end) end, case On_sub_result of none -> jump_and_render(Dialog, Ctx, Inst@1, Return_window); {some, Handler} -> telega@dialog@widget:stash_stores( erlang:element(3, erlang:element(6, Inst@1)) ), case Handler(Parent_state, Result, Ctx) of {error, Error} -> {error, Error}; {ok, Action} -> apply_sub_return_action( Dialog, Ctx, Inst@1, Return_window, Action ) end end. -file("src/telega/dialog/engine.gleam", 608). ?DOC(" Back crossed the sub boundary: drop the sub without a result.\n"). -spec cancel_sub( compiled_dialog(AXWA, AXWB, AXWC), telega@bot:context(AXWA, AXWB, AXWC), telega@flow@types:flow_instance(), binary() ) -> {ok, {telega@bot:context(AXWA, AXWB, AXWC), telega@flow@types:flow_action(binary()), telega@flow@types:flow_instance()}} | {error, AXWB}. cancel_sub(Dialog, Ctx, Inst, Sub_id) -> emit_dialog_event( Dialog, <<"sub_cancel"/utf8>>, erlang:element(2, erlang:element(6, Inst)), [{<<"count"/utf8>>, 1}] ), Return_window = return_window_of(Dialog, Inst), Inst@1 = leave_sub(Dialog, Inst, Sub_id), jump_and_render(Dialog, Ctx, Inst@1, Return_window). -file("src/telega/dialog/engine.gleam", 363). -spec apply_action( compiled_dialog(AXTF, AXTG, AXTH), telega@bot:context(AXTF, AXTG, AXTH), telega@flow@types:flow_instance(), telega@dialog@types:dialog_action(binary()) ) -> {ok, {telega@bot:context(AXTF, AXTG, AXTH), telega@flow@types:flow_action(binary()), telega@flow@types:flow_instance()}} | {error, AXTG}. apply_action(Dialog, Ctx, Inst, Action) -> case Action of {stay, State} -> Inst@1 = save_state(Inst, State), case current_window(Dialog, Inst@1) of {ok, Window} -> render_and_park(Dialog, Window, Ctx, Inst@1, State); {error, nil} -> wait(Ctx, Inst@1) end; {goto, Window_id, State@1} -> Inst@2 = save_state(Inst, State@1), case gleam@dict:has_key(erlang:element(3, Dialog), Window_id) of true -> {ok, {Ctx, {next_string, Window_id}, Inst@2}}; false -> log_unknown_window( Dialog, erlang:element(2, erlang:element(6, Inst@2)), Window_id ), wait(Ctx, Inst@2) end; {back, State@2} -> Inst@3 = save_state(Inst, State@2), case telega@flow@instance:get_data(Inst@3, <<"__dialog_sub"/utf8>>) of none -> case erlang:element(4, erlang:element(6, Inst@3)) of [] -> apply_action(Dialog, Ctx, Inst@3, {stay, State@2}); _ -> {ok, {Ctx, back, Inst@3}} end; {some, Sub_id} -> Prefix = <>, case erlang:element(4, erlang:element(6, Inst@3)) of [Head | _] -> case gleam_stdlib:string_starts_with(Head, Prefix) of true -> {ok, {Ctx, back, Inst@3}}; false -> cancel_sub(Dialog, Ctx, Inst@3, Sub_id) end; [] -> cancel_sub(Dialog, Ctx, Inst@3, Sub_id) end end; {done, State@3} -> case telega@flow@instance:get_data(Inst, <<"__dialog_sub"/utf8>>) of {some, Sub_id@1} -> finish_sub(Dialog, Ctx, Inst, Sub_id@1, State@3); none -> finish_dialog(Dialog, Ctx, Inst, State@3) end; {start_sub, Sub_id@2, Args, State@4} -> case telega@flow@instance:get_data(Inst, <<"__dialog_sub"/utf8>>) of {some, Active} -> logging:log( error, <<<<<<<<<<<<"[dialog:"/utf8, (erlang:element(2, Dialog))/binary>>/binary, "] StartSub('"/utf8>>/binary, Sub_id@2/binary>>/binary, "') rejected: sub-dialog '"/utf8>>/binary, Active/binary>>/binary, "' is already active (nesting is one level deep)"/utf8>> ), apply_action(Dialog, Ctx, Inst, {stay, State@4}); none -> start_sub( Dialog, Ctx, Inst, Sub_id@2, Args, State@4, fun() -> apply_action(Dialog, Ctx, Inst, {stay, State@4}) end ) end end. -file("src/telega/dialog/engine.gleam", 340). -spec handle_text( compiled_dialog(AXSO, AXSP, AXSQ), telega@dialog@types:window(binary(), AXSO, AXSP, AXSQ), telega@bot:context(AXSO, AXSP, AXSQ), telega@flow@types:flow_instance(), binary() ) -> {ok, {telega@bot:context(AXSO, AXSP, AXSQ), telega@flow@types:flow_action(binary()), telega@flow@types:flow_instance()}} | {error, AXSP}. handle_text(Dialog, Window, Ctx, Inst, Text) -> case erlang:element(5, Window) of {some, On_text} -> State = load_state(Dialog, Inst), case On_text(State, Text, Ctx) of {ok, Action} -> apply_action(Dialog, Ctx, Inst, Action); {error, Error} -> {error, Error} end; none -> render_and_wait(Dialog, Window, Ctx, Inst) end. -file("src/telega/dialog/engine.gleam", 776). -spec save_widget_store( telega@flow@types:flow_instance(), binary(), binary(), telega@dialog@types:widget_store() ) -> telega@flow@types:flow_instance(). save_widget_store(Inst, Window_id, Widget_id, Store) -> telega@flow@instance:store_data( Inst, telega@dialog@widget:store_data_key(Window_id, Widget_id), telega@dialog@types:encode_store(Store) ). -file("src/telega/dialog/engine.gleam", 305). -spec handle_widget_event( compiled_dialog(AXRW, AXRX, AXRY), telega@dialog@types:window(binary(), AXRW, AXRX, AXRY), telega@bot:context(AXRW, AXRX, AXRY), telega@flow@types:flow_instance(), binary(), binary(), binary(), gleam@option:option(binary()) ) -> {ok, {telega@bot:context(AXRW, AXRX, AXRY), telega@flow@types:flow_action(binary()), telega@flow@types:flow_instance()}} | {error, AXRX}. handle_widget_event(Dialog, Window, Ctx, Inst, State, Widget_id, Cmd, Arg) -> case gleam@list:find( erlang:element(6, Window), fun(Candidate) -> erlang:element(2, Candidate) =:= Widget_id end ) of {error, nil} -> telega@dialog@render:answer_quietly(Ctx, none), wait(Ctx, Inst); {ok, Found} -> Store = load_widget_store( Inst, erlang:element(2, Window), Widget_id ), Widget_ctx = {widget_ctx, State, Store, (erlang:element(10, Dialog))(Ctx), Ctx}, Handled = (erlang:element(4, Found))(Widget_ctx, Cmd, Arg), telega@dialog@render:auto_answer(Ctx), case Handled of {ok, {store_updated, Store@1}} -> Inst@1 = save_widget_store( Inst, erlang:element(2, Window), Widget_id, Store@1 ), apply_action(Dialog, Ctx, Inst@1, {stay, State}); {ok, {emit, Action}} -> apply_action(Dialog, Ctx, Inst, Action); {error, Error} -> {error, Error} end end. -file("src/telega/dialog/engine.gleam", 963). ?DOC(false). -spec parse_widget_event(telega@dialog@types:action_event()) -> {ok, {binary(), binary(), gleam@option:option(binary())}} | {error, nil}. parse_widget_event(Event) -> case {erlang:element(2, Event), erlang:element(3, Event)} of {<<"w"/utf8>>, {some, Rest}} -> case gleam@string:split(Rest, <<":"/utf8>>) of [Widget_id, Cmd] -> {ok, {Widget_id, Cmd, none}}; [Widget_id@1, Cmd@1 | Arg_parts] -> {ok, {Widget_id@1, Cmd@1, {some, gleam@string:join(Arg_parts, <<":"/utf8>>)}}}; _ -> {error, nil} end; {_, _} -> {error, nil} end. -file("src/telega/dialog/engine.gleam", 980). -spec emit_action_event( compiled_dialog(any(), any(), any()), binary(), telega@dialog@types:action_event() ) -> nil. emit_action_event(Dialog, Window_id, Event) -> telega@telemetry:execute( [<<"telega"/utf8>>, <<"dialog"/utf8>>, <<"action"/utf8>>], [{<<"count"/utf8>>, 1}], [{<<"dialog_id"/utf8>>, {string_value, erlang:element(2, Dialog)}}, {<<"window_id"/utf8>>, {string_value, Window_id}}, {<<"action_id"/utf8>>, {string_value, erlang:element(2, Event)}}] ). -file("src/telega/dialog/engine.gleam", 266). -spec answer_stale( compiled_dialog(AXQW, AXQX, AXQY), telega@bot:context(AXQW, AXQX, AXQY), telega@flow@types:flow_instance() ) -> {ok, {telega@bot:context(AXQW, AXQX, AXQY), telega@flow@types:flow_action(binary()), telega@flow@types:flow_instance()}} | {error, AXQX}. answer_stale(Dialog, Ctx, Inst) -> Labels = (erlang:element(10, Dialog))(Ctx), telega@dialog@render:answer_quietly(Ctx, {some, erlang:element(10, Labels)}), wait(Ctx, Inst). -file("src/telega/dialog/engine.gleam", 289). -spec pressed_message_id(telega@bot:context(any(), any(), any())) -> gleam@option:option(integer()). pressed_message_id(Ctx) -> case erlang:element(3, Ctx) of {callback_query_update, _, _, Query, _} -> case erlang:element(4, Query) of {some, {message_maybe_inaccessible_message, Message}} -> {some, erlang:element(2, Message)}; {some, {inaccessible_message_maybe_inaccessible_message, Message@1}} -> {some, erlang:element(3, Message@1)}; none -> none end; _ -> none end. -file("src/telega/dialog/engine.gleam", 279). ?DOC( " Whether the pressed message is not the tracked live dialog message.\n" " `False` when either id is unknown — an unverifiable press is processed\n" " normally.\n" ). -spec pressed_outdated_message( telega@bot:context(any(), any(), any()), telega@flow@types:flow_instance() ) -> boolean(). pressed_outdated_message(Ctx, Inst) -> case {pressed_message_id(Ctx), message_id(Inst)} of {{some, Pressed}, {some, Live}} -> Pressed /= Live; {_, _} -> false end. -file("src/telega/dialog/engine.gleam", 943). ?DOC( " Parse `dlg:::[:]`. Extra segments\n" " are joined back into the arg, so args may contain `:`.\n" ). -spec parse_callback_data(binary()) -> {ok, {binary(), binary(), telega@dialog@types:action_event()}} | {error, nil}. parse_callback_data(Data) -> case gleam@string:split(Data, <<":"/utf8>>) of [<<"dlg"/utf8>>, Dialog_id, Window_id, Action_id] -> {ok, {Dialog_id, Window_id, {action_event, Action_id, none}}}; [<<"dlg"/utf8>>, Dialog_id@1, Window_id@1, Action_id@1 | Arg_parts] -> {ok, {Dialog_id@1, Window_id@1, {action_event, Action_id@1, {some, gleam@string:join(Arg_parts, <<":"/utf8>>)}}}}; _ -> {error, nil} end. -file("src/telega/dialog/engine.gleam", 206). -spec handle_callback( compiled_dialog(AXQF, AXQG, AXQH), telega@dialog@types:window(binary(), AXQF, AXQG, AXQH), telega@bot:context(AXQF, AXQG, AXQH), telega@flow@types:flow_instance(), binary() ) -> {ok, {telega@bot:context(AXQF, AXQG, AXQH), telega@flow@types:flow_action(binary()), telega@flow@types:flow_instance()}} | {error, AXQG}. handle_callback(Dialog, Window, Ctx, Inst, Data) -> case parse_callback_data(Data) of {error, nil} -> telega@dialog@render:answer_quietly(Ctx, none), wait(Ctx, Inst); {ok, {Dialog_id, Window_id, Event}} -> case {Dialog_id =:= erlang:element(2, Dialog), Window_id =:= erlang:element(2, Window)} of {false, _} -> telega@dialog@render:answer_quietly(Ctx, none), wait(Ctx, Inst); {true, false} -> answer_stale(Dialog, Ctx, Inst); {true, true} -> case pressed_outdated_message(Ctx, Inst) of true -> answer_stale(Dialog, Ctx, Inst); false -> State = load_state(Dialog, Inst), emit_action_event( Dialog, erlang:element(2, Window), Event ), case parse_widget_event(Event) of {ok, {Widget_id, Cmd, Arg}} -> handle_widget_event( Dialog, Window, Ctx, Inst, State, Widget_id, Cmd, Arg ); {error, nil} -> Handled = (erlang:element(4, Window))( State, Event, Ctx ), telega@dialog@render:auto_answer(Ctx), case Handled of {ok, Action} -> apply_action( Dialog, Ctx, Inst, Action ); {error, Error} -> {error, Error} end end end end end. -file("src/telega/dialog/engine.gleam", 169). -spec window_step( compiled_dialog(AXPA, AXPB, AXPC), telega@dialog@types:window(binary(), AXPA, AXPB, AXPC) ) -> fun((telega@bot:context(AXPA, AXPB, AXPC), telega@flow@types:flow_instance()) -> {ok, {telega@bot:context(AXPA, AXPB, AXPC), telega@flow@types:flow_action(binary()), telega@flow@types:flow_instance()}} | {error, AXPB}). window_step(Dialog, Window) -> fun(Ctx, Inst) -> telega@dialog@widget:stash_stores( erlang:element(3, erlang:element(6, Inst)) ), case telega@flow@instance:get_wait_result(Inst) of pending -> render_and_wait(Dialog, Window, Ctx, Inst); {data_callback, Value} -> handle_callback(Dialog, Window, Ctx, Inst, Value); {text_input, Value@1} -> handle_text(Dialog, Window, Ctx, Inst, Value@1); {bool_callback, _} -> telega@dialog@render:answer_quietly(Ctx, none), wait(Ctx, Inst); _ -> wait(Ctx, Inst) end end. -file("src/telega/dialog/engine.gleam", 124). ?DOC( " Compile a dialog into a flow. Window id = step name (identity\n" " converters), `on_error` is always set (the flow engine silently swallows\n" " errors otherwise): it logs, emits telemetry, and re-renders the current\n" " window best-effort.\n" ). -spec compile(compiled_dialog(AXOQ, AXOR, AXOS)) -> telega@flow@types:flow(binary(), AXOQ, AXOR, AXOS). compile(Dialog) -> Flow_name = <<"__dialog:"/utf8, (erlang:element(2, Dialog))/binary>>, Builder = telega@flow@builder:new( Flow_name, erlang:element(8, Dialog), fun(Step) -> Step end, fun(Step@1) -> {ok, Step@1} end ), Builder@2 = gleam@dict:fold( erlang:element(3, Dialog), Builder, fun(Builder@1, Window_id, Window) -> telega@flow@builder:add_step( Builder@1, Window_id, window_step(Dialog, Window) ) end ), Builder@3 = telega@flow@builder:on_complete( Builder@2, fun(Ctx, Inst) -> telega@dialog@widget:stash_stores( erlang:element(3, erlang:element(6, Inst)) ), Result = case erlang:element(6, Dialog) of {some, On_done} -> On_done(load_state(Dialog, Inst), Ctx); none -> {ok, Ctx} end, telega@dialog@widget:clear_stash(), Result end ), Builder@4 = telega@flow@builder:on_error( Builder@3, fun(Ctx@1, Inst@1, Maybe_error) -> log_dialog_error(Dialog, Inst@1, Maybe_error), _ = try_render_current(Dialog, Ctx@1, Inst@1), {ok, Ctx@1} end ), Builder@5 = case erlang:element(9, Dialog) of {some, Ms} -> telega@flow@builder:with_ttl(Builder@4, Ms); none -> Builder@4 end, telega@flow@builder:build(Builder@5, erlang:element(4, Dialog)).