Current section
Files
Jump to
Current section
Files
src/telega@dialog@engine.erl
-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 `<sub_id>.<window_id>` 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(BAQB, BAQC, BAQD) :: {compiled_dialog,
binary(),
gleam@dict:dict(binary(), telega@dialog@types:window(binary(), BAQB, BAQC, BAQD)),
binary(),
fun(() -> binary()),
gleam@option:option(fun((binary(), telega@bot:context(BAQB, BAQC, BAQD)) -> {ok,
telega@bot:context(BAQB, BAQC, BAQD)} |
{error, BAQC})),
gleam@dict:dict(binary(), compiled_sub()),
telega@flow@types:flow_storage(BAQC),
gleam@option:option(integer()),
fun((telega@bot:context(BAQB, BAQC, BAQD)) -> 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(BBAG, BBAH, BBAI),
telega@dialog@types:window(binary(), BBAG, BBAH, BBAI),
telega@bot:context(BBAG, BBAH, BBAI),
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(BAZR, BAZS, BAZT),
telega@dialog@types:window(binary(), BAZR, BAZS, BAZT),
telega@bot:context(BAZR, BAZS, BAZT),
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(BBBE, BBBF, BBBG),
telega@flow@types:flow_instance()
) -> {ok, telega@dialog@types:window(binary(), BBBE, BBBF, BBBG)} | {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(BBAT, BBAU, BBAV),
telega@bot:context(BBAT, BBAU, BBAV),
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(), BBDQ, any()),
telega@flow@types:flow_instance(),
gleam@option:option(BBDQ)
) -> 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(BBCB, BBCC, BBCD),
telega@flow@types:flow_instance()
) -> {ok,
{telega@bot:context(BBCB, BBCC, BBCD),
telega@flow@types:flow_action(binary()),
telega@flow@types:flow_instance()}} |
{error, BBCC}.
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(BAZA, BAZB, BAZC),
telega@dialog@types:window(binary(), BAZA, BAZB, BAZC),
telega@bot:context(BAZA, BAZB, BAZC),
telega@flow@types:flow_instance(),
binary()
) -> {ok,
{telega@bot:context(BAZA, BAZB, BAZC),
telega@flow@types:flow_action(binary()),
telega@flow@types:flow_instance()}} |
{error, BAZB}.
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(BARC, BARD, BARE),
telega@dialog@types:window(binary(), BARC, BARD, BARE),
telega@bot:context(BARC, BARD, BARE),
telega@flow@types:flow_instance()
) -> {ok,
{telega@bot:context(BARC, BARD, BARE),
telega@flow@types:flow_action(binary()),
telega@flow@types:flow_instance()}} |
{error, BARD}.
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(BAYN, BAYO, BAYP),
telega@bot:context(BAYN, BAYO, BAYP),
telega@flow@types:flow_instance(),
binary()
) -> {ok,
{telega@bot:context(BAYN, BAYO, BAYP),
telega@flow@types:flow_action(binary()),
telega@flow@types:flow_instance()}} |
{error, BAYO}.
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(BAVU, BAVV, BAVW),
telega@bot:context(BAVU, BAVV, BAVW),
telega@flow@types:flow_instance(),
binary(),
gleam@dict:dict(binary(), binary()),
binary(),
fun(() -> {ok,
{telega@bot:context(BAVU, BAVV, BAVW),
telega@flow@types:flow_action(binary()),
telega@flow@types:flow_instance()}} |
{error, BAVV})
) -> {ok,
{telega@bot:context(BAVU, BAVV, BAVW),
telega@flow@types:flow_action(binary()),
telega@flow@types:flow_instance()}} |
{error, BAVV}.
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(BAVH, BAVI, BAVJ),
telega@bot:context(BAVH, BAVI, BAVJ),
telega@flow@types:flow_instance(),
binary()
) -> {ok,
{telega@bot:context(BAVH, BAVI, BAVJ),
telega@flow@types:flow_action(binary()),
telega@flow@types:flow_instance()}} |
{error, BAVI}.
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(BAXA, BAXB, BAXC),
telega@bot:context(BAXA, BAXB, BAXC),
telega@flow@types:flow_instance(),
binary(),
telega@dialog@types:dialog_action(binary())
) -> {ok,
{telega@bot:context(BAXA, BAXB, BAXC),
telega@flow@types:flow_action(binary()),
telega@flow@types:flow_instance()}} |
{error, BAXB}.
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 = <<Sub_id/binary, "."/utf8>>,
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(BAWN, BAWO, BAWP),
telega@bot:context(BAWN, BAWO, BAWP),
telega@flow@types:flow_instance(),
binary(),
binary()
) -> {ok,
{telega@bot:context(BAWN, BAWO, BAWP),
telega@flow@types:flow_action(binary()),
telega@flow@types:flow_instance()}} |
{error, BAWO}.
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(BAXO, BAXP, BAXQ),
telega@bot:context(BAXO, BAXP, BAXQ),
telega@flow@types:flow_instance(),
binary()
) -> {ok,
{telega@bot:context(BAXO, BAXP, BAXQ),
telega@flow@types:flow_action(binary()),
telega@flow@types:flow_instance()}} |
{error, BAXP}.
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(BAUT, BAUU, BAUV),
telega@bot:context(BAUT, BAUU, BAUV),
telega@flow@types:flow_instance(),
telega@dialog@types:dialog_action(binary())
) -> {ok,
{telega@bot:context(BAUT, BAUU, BAUV),
telega@flow@types:flow_action(binary()),
telega@flow@types:flow_instance()}} |
{error, BAUU}.
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 = <<Sub_id/binary, "."/utf8>>,
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(BAUC, BAUD, BAUE),
telega@dialog@types:window(binary(), BAUC, BAUD, BAUE),
telega@bot:context(BAUC, BAUD, BAUE),
telega@flow@types:flow_instance(),
binary()
) -> {ok,
{telega@bot:context(BAUC, BAUD, BAUE),
telega@flow@types:flow_action(binary()),
telega@flow@types:flow_instance()}} |
{error, BAUD}.
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(BATK, BATL, BATM),
telega@dialog@types:window(binary(), BATK, BATL, BATM),
telega@bot:context(BATK, BATL, BATM),
telega@flow@types:flow_instance(),
binary(),
binary(),
binary(),
gleam@option:option(binary())
) -> {ok,
{telega@bot:context(BATK, BATL, BATM),
telega@flow@types:flow_action(binary()),
telega@flow@types:flow_instance()}} |
{error, BATL}.
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(BASK, BASL, BASM),
telega@bot:context(BASK, BASL, BASM),
telega@flow@types:flow_instance()
) -> {ok,
{telega@bot:context(BASK, BASL, BASM),
telega@flow@types:flow_action(binary()),
telega@flow@types:flow_instance()}} |
{error, BASL}.
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:<dialog_id>:<window_id>:<action_id>[:<arg>]`. 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(BART, BARU, BARV),
telega@dialog@types:window(binary(), BART, BARU, BARV),
telega@bot:context(BART, BARU, BARV),
telega@flow@types:flow_instance(),
binary()
) -> {ok,
{telega@bot:context(BART, BARU, BARV),
telega@flow@types:flow_action(binary()),
telega@flow@types:flow_instance()}} |
{error, BARU}.
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(BAQO, BAQP, BAQQ),
telega@dialog@types:window(binary(), BAQO, BAQP, BAQQ)
) -> fun((telega@bot:context(BAQO, BAQP, BAQQ), telega@flow@types:flow_instance()) -> {ok,
{telega@bot:context(BAQO, BAQP, BAQQ),
telega@flow@types:flow_action(binary()),
telega@flow@types:flow_instance()}} |
{error, BAQP}).
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(BAQE, BAQF, BAQG)) -> telega@flow@types:flow(binary(), BAQE, BAQF, BAQG).
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)).