Current section
Files
Jump to
Current section
Files
src/telega@dialog.erl
-module(telega@dialog).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/telega/dialog.gleam").
-export([new/5, string_codec/0, json_codec/2, window/4, window_with_input/5, window_with_widgets/5, subdialog/4, on_sub_result/3, initial/2, on_done/2, with_ttl/2, with_labels/2, build/1, compiled/1, attach/2, attach_on_command/3, start/3, restart/3, widget_store/3, alert/2, toast/2]).
-export_type([dialog/4, sub_attachment/3, dialog_builder/4]).
-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(
" # Declarative dialogs\n"
"\n"
" A dialog is a set of windows; a window is a pure render function plus\n"
" event handlers. The engine renders everything into **one live message**\n"
" (edit-or-send, including text ↔ media transitions — see\n"
" `telega/dialog/render`), parses button callback data itself, keeps a\n"
" navigation stack with `Back`, and persists all state in a `FlowStorage` —\n"
" the dialog survives restarts with any persistent backend\n"
" (Postgres/SQLite/Redis).\n"
"\n"
" Dialogs compile to `telega/flow` state machines: a window is a step,\n"
" navigation is a flow action, delivery of callbacks/text into the active\n"
" window is the flow registry's wait-token auto-resume. Nothing here needs\n"
" a dedicated router route.\n"
"\n"
" Full guide (positioning vs conversations/flows/menu builder, widgets,\n"
" sub-dialogs, i18n, testing):\n"
" [docs/dialogs.md](https://hexdocs.pm/telega/docs/dialogs.html).\n"
"\n"
" ## Quick start\n"
"\n"
" ```gleam\n"
" import telega/dialog\n"
" import telega/dialog/types.{ActionButton, RenderedWindow}\n"
" import telega/flow/registry as flow_registry\n"
" import telega/flow/storage as flow_storage\n"
" import telega/format\n"
"\n"
" fn render_menu(state: MyState, _ctx) -> RenderedWindow {\n"
" RenderedWindow(\n"
" text: format.build() |> format.bold_text(\"Settings\") |> format.to_formatted(),\n"
" buttons: [[ActionButton(\"Name\", \"name\")], [ActionButton(\"Done\", \"done\")]],\n"
" media: None,\n"
" )\n"
" }\n"
"\n"
" fn handle_menu(state, event: types.ActionEvent, _ctx) {\n"
" case event.action_id {\n"
" \"name\" -> Ok(types.Goto(\"name\", state))\n"
" \"done\" -> Ok(types.Done(state))\n"
" _ -> Ok(types.Stay(state))\n"
" }\n"
" }\n"
"\n"
" let assert Ok(settings) =\n"
" dialog.new(\n"
" id: \"settings\",\n"
" storage: flow_storage,\n"
" initial_state: fn() { MyState(name: \"\") },\n"
" encode_state: encode_my_state,\n"
" decode_state: decode_my_state,\n"
" )\n"
" |> dialog.window(id: \"menu\", render: render_menu, on_action: handle_menu)\n"
" |> dialog.window_with_input(id: \"name\", render:, on_action:, on_text:)\n"
" |> dialog.initial(\"menu\")\n"
" |> dialog.on_done(save_settings)\n"
" |> dialog.build()\n"
"\n"
" let registry =\n"
" flow_registry.new_registry()\n"
" |> dialog.attach_on_command(\"settings\", settings)\n"
" |> flow_registry.register_cancel_command(\"cancel\")\n"
"\n"
" let router = flow_registry.apply_to_router(router, registry)\n"
" ```\n"
"\n"
" ## Behavior notes\n"
"\n"
" - **One live instance** per `(dialog, chat, user)`: a repeated start\n"
" command resumes (re-renders) the current dialog instead of opening a\n"
" second one. Use `restart` for a hard reset.\n"
" - **The dialog is modal**: while it waits for a callback, plain text\n"
" messages are swallowed (the window is re-rendered). Commands still\n"
" reach the router — register a `/cancel` via\n"
" `flow_registry.register_cancel_command`.\n"
" - **Widgets**: `window_with_widgets` attaches managed keyboards from\n"
" `telega/dialog/widget` (pager, select, radio, multiselect,\n"
" paged_select) — the engine renders their rows, handles their callbacks\n"
" and persists their state; read selections with `widget_store`.\n"
" - **Sub-dialogs**: `subdialog` attaches another built dialog (its state\n"
" type may differ); any window starts it with `StartSub(sub_id, args,\n"
" state)` and receives its exported result in `on_sub_result`. The sub\n"
" shares the live message and the parent's storage/TTL/labels; `Back` on\n"
" its first window cancels it; nesting is one level deep.\n"
" - **Stale buttons**: a press on an outdated dialog message (an old\n"
" window, or an old copy of the live message) answers with `labels.stale`\n"
" and does nothing. Presses on messages of an already **finished** dialog\n"
" are answered the same way by a fallback that `attach` registers\n"
" automatically.\n"
" - **Errors**: user handler errors are logged, emit\n"
" `[\"telega\", \"dialog\", \"error\"]` telemetry and re-render the current\n"
" window; failed renders (API errors, over-64-byte callback data) are\n"
" logged loudly and keep the dialog alive.\n"
).
-opaque dialog(BHDF, BHDG, BHDH, BHDI) :: {dialog,
telega@dialog@engine:compiled_dialog(BHDG, BHDH, BHDI),
fun((BHDF) -> binary()),
fun((binary()) -> BHDF)}.
-type sub_attachment(BHDJ, BHDK, BHDL) :: {sub_attachment,
binary(),
list(telega@dialog@types:window(binary(), BHDJ, BHDK, BHDL)),
binary(),
fun((binary(), gleam@dict:dict(binary(), binary())) -> binary()),
fun((binary()) -> gleam@dict:dict(binary(), binary())),
boolean()}.
-opaque dialog_builder(BHDM, BHDN, BHDO, BHDP) :: {dialog_builder,
binary(),
list(telega@dialog@types:window(BHDM, BHDN, BHDO, BHDP)),
gleam@option:option(binary()),
fun(() -> BHDM),
fun((BHDM) -> binary()),
fun((binary()) -> {ok, BHDM} | {error, nil}),
gleam@option:option(fun((BHDM, telega@bot:context(BHDN, BHDO, BHDP)) -> {ok,
telega@bot:context(BHDN, BHDO, BHDP)} |
{error, BHDO})),
list(sub_attachment(BHDN, BHDO, BHDP)),
list({binary(),
fun((BHDM, gleam@dict:dict(binary(), binary()), telega@bot:context(BHDN, BHDO, BHDP)) -> {ok,
telega@dialog@types:dialog_action(BHDM)} |
{error, BHDO})}),
telega@flow@types:flow_storage(BHDO),
gleam@option:option(integer()),
fun((telega@bot:context(BHDN, BHDO, BHDP)) -> telega@dialog@types:labels())}.
-file("src/telega/dialog.gleam", 178).
?DOC(
" Start building a dialog. `encode_state`/`decode_state` serialize the\n"
" user state for persistence (precedent: session serialization); for simple\n"
" states see `string_codec` and `json_codec`.\n"
).
-spec new(
binary(),
telega@flow@types:flow_storage(BHDQ),
fun(() -> BHDS),
fun((BHDS) -> binary()),
fun((binary()) -> {ok, BHDS} | {error, nil})
) -> dialog_builder(BHDS, any(), BHDQ, any()).
new(Id, Storage, Initial_state, Encode_state, Decode_state) ->
{dialog_builder,
Id,
[],
none,
Initial_state,
Encode_state,
Decode_state,
none,
[],
[],
Storage,
none,
fun(_) -> telega@dialog@types:default_labels() end}.
-file("src/telega/dialog.gleam", 203).
?DOC(
" Codec pair for a plain `String` state:\n"
" `let #(encode, decode) = dialog.string_codec()`.\n"
).
-spec string_codec() -> {fun((binary()) -> binary()),
fun((binary()) -> {ok, binary()} | {error, nil})}.
string_codec() ->
{fun(State) -> State end, fun(Raw) -> {ok, Raw} end}.
-file("src/telega/dialog.gleam", 212).
?DOC(
" Codec pair from a JSON encoder + decoder:\n"
" `let #(encode, decode) = dialog.json_codec(encode_settings, settings_decoder())`.\n"
).
-spec json_codec(
fun((BHED) -> gleam@json:json()),
gleam@dynamic@decode:decoder(BHED)
) -> {fun((BHED) -> binary()), fun((binary()) -> {ok, BHED} | {error, nil})}.
json_codec(Encoder, Decoder) ->
{fun(State) -> gleam@json:to_string(Encoder(State)) end,
fun(Raw) -> _pipe = gleam@json:parse(Raw, Decoder),
gleam@result:replace_error(_pipe, nil) end}.
-file("src/telega/dialog.gleam", 311).
-spec add_window(
dialog_builder(BHHD, BHHE, BHHF, BHHG),
telega@dialog@types:window(BHHD, BHHE, BHHF, BHHG)
) -> dialog_builder(BHHD, BHHE, BHHF, BHHG).
add_window(Builder, Window) ->
{dialog_builder,
erlang:element(2, Builder),
[Window | erlang:element(3, Builder)],
erlang:element(4, Builder),
erlang:element(5, Builder),
erlang:element(6, Builder),
erlang:element(7, Builder),
erlang:element(8, Builder),
erlang:element(9, Builder),
erlang:element(10, Builder),
erlang:element(11, Builder),
erlang:element(12, Builder),
erlang:element(13, Builder)}.
-file("src/telega/dialog.gleam", 223).
?DOC(
" Add a window that only reacts to button presses. Text sent to it is\n"
" swallowed with a re-render.\n"
).
-spec window(
dialog_builder(BHEH, BHEI, BHEJ, BHEK),
binary(),
fun((BHEH, telega@bot:context(BHEI, BHEJ, BHEK)) -> telega@dialog@types:rendered_window()),
fun((BHEH, telega@dialog@types:action_event(), telega@bot:context(BHEI, BHEJ, BHEK)) -> {ok,
telega@dialog@types:dialog_action(BHEH)} |
{error, BHEJ})
) -> dialog_builder(BHEH, BHEI, BHEJ, BHEK).
window(Builder, Id, Render, On_action) ->
add_window(Builder, {window, Id, Render, On_action, none, [], none}).
-file("src/telega/dialog.gleam", 248).
?DOC(" Add a window that also accepts text input (e.g. \"enter your name\").\n").
-spec window_with_input(
dialog_builder(BHFC, BHFD, BHFE, BHFF),
binary(),
fun((BHFC, telega@bot:context(BHFD, BHFE, BHFF)) -> telega@dialog@types:rendered_window()),
fun((BHFC, telega@dialog@types:action_event(), telega@bot:context(BHFD, BHFE, BHFF)) -> {ok,
telega@dialog@types:dialog_action(BHFC)} |
{error, BHFE}),
fun((BHFC, binary(), telega@bot:context(BHFD, BHFE, BHFF)) -> {ok,
telega@dialog@types:dialog_action(BHFC)} |
{error, BHFE})
) -> dialog_builder(BHFC, BHFD, BHFE, BHFF).
window_with_input(Builder, Id, Render, On_action, On_text) ->
add_window(
Builder,
{window, Id, Render, On_action, {some, On_text}, [], none}
).
-file("src/telega/dialog.gleam", 284).
?DOC(
" Add a window with managed keyboard widgets (see `telega/dialog/widget`).\n"
" Widget button rows are appended after the window's own buttons and their\n"
" events are handled by the widgets themselves, bypassing `on_action`.\n"
"\n"
" ```gleam\n"
" |> dialog.window_with_widgets(id: \"fruits\", render:, on_action:, widgets: [\n"
" widget.multiselect(id: \"f\", items: fruit_items, min: 1, max: 3,\n"
" done: \"confirm\"),\n"
" ])\n"
" ```\n"
).
-spec window_with_widgets(
dialog_builder(BHGD, BHGE, BHGF, BHGG),
binary(),
fun((BHGD, telega@bot:context(BHGE, BHGF, BHGG)) -> telega@dialog@types:rendered_window()),
fun((BHGD, telega@dialog@types:action_event(), telega@bot:context(BHGE, BHGF, BHGG)) -> {ok,
telega@dialog@types:dialog_action(BHGD)} |
{error, BHGF}),
list(telega@dialog@types:keyboard_widget(BHGD, BHGE, BHGF, BHGG))
) -> dialog_builder(BHGD, BHGE, BHGF, BHGG).
window_with_widgets(Builder, Id, Render, On_action, Widgets) ->
add_window(Builder, {window, Id, Render, On_action, none, Widgets, none}).
-file("src/telega/dialog.gleam", 494).
?DOC(
" Decode with a logged fallback to the initial state — a codec mismatch\n"
" (e.g. a state shape change after a deploy) must not kill the dialog.\n"
).
-spec decode_or_initial(dialog_builder(BHMD, any(), any(), any())) -> fun((binary()) -> BHMD).
decode_or_initial(Builder) ->
{dialog_builder, Id, _, _, Initial_state, _, Decode_state, _, _, _, _, _, _} = Builder,
fun(Raw) -> case Decode_state(Raw) of
{ok, State} ->
State;
{error, nil} ->
logging:log(
warning,
<<<<"[dialog:"/utf8, Id/binary>>/binary,
"] failed to decode state, falling back to initial"/utf8>>
),
Initial_state()
end end.
-file("src/telega/dialog.gleam", 334).
?DOC(
" Attach a built dialog as a **sub-dialog**, startable from any window via\n"
" `StartSub(sub_id, args, state)` (the sub id is the attached dialog's id).\n"
" The sub takes over the live dialog message; its `Done` hands control back\n"
" to the window that started it (see `on_sub_result`). Nesting is one level\n"
" deep: a dialog with sub-dialogs of its own cannot be attached\n"
" (`NestedSubDialog`).\n"
"\n"
" - `init` builds the sub's starting state from the parent state and the\n"
" `StartSub` args.\n"
" - `result` exports the sub's final state as the dict handed to the parent\n"
" window's `on_sub_result`; prefix the keys with the sub id by convention\n"
" (`\"address.city\"`) to keep them collision-free.\n"
"\n"
" The attached dialog's own `storage`, `ttl`, `labels` and `on_done` are\n"
" ignored while it runs as a sub — the parent's apply. A `Back` on the\n"
" sub's first window cancels the sub (returns without a result).\n"
).
-spec subdialog(
dialog_builder(BHHT, BHHU, BHHV, BHHW),
dialog(BHIB, BHHU, BHHV, BHHW),
fun((BHHT, gleam@dict:dict(binary(), binary())) -> BHIB),
fun((BHIB) -> gleam@dict:dict(binary(), binary()))
) -> dialog_builder(BHHT, BHHU, BHHV, BHHW).
subdialog(Builder, Sub, Init, Result) ->
Parent_decode = decode_or_initial(Builder),
Attachment = {sub_attachment,
erlang:element(2, erlang:element(2, Sub)),
maps:values(erlang:element(3, erlang:element(2, Sub))),
erlang:element(4, erlang:element(2, Sub)),
fun(Parent_raw, Args) ->
(erlang:element(3, Sub))(Init(Parent_decode(Parent_raw), Args))
end,
fun(Sub_raw) -> Result((erlang:element(4, Sub))(Sub_raw)) end,
maps:size(erlang:element(7, erlang:element(2, Sub))) > 0},
{dialog_builder,
erlang:element(2, Builder),
erlang:element(3, Builder),
erlang:element(4, Builder),
erlang:element(5, Builder),
erlang:element(6, Builder),
erlang:element(7, Builder),
erlang:element(8, Builder),
[Attachment | erlang:element(9, Builder)],
erlang:element(10, Builder),
erlang:element(11, Builder),
erlang:element(12, Builder),
erlang:element(13, Builder)}.
-file("src/telega/dialog.gleam", 360).
?DOC(
" Handle the result of a sub-dialog started from `window`: the handler\n"
" receives the window's state and the result dict exported by the\n"
" `subdialog` attachment, and returns the next action (`Stay` re-renders\n"
" the window with the updated state). Without a handler the window is\n"
" simply re-rendered.\n"
).
-spec on_sub_result(
dialog_builder(BHIO, BHIP, BHIQ, BHIR),
binary(),
fun((BHIO, gleam@dict:dict(binary(), binary()), telega@bot:context(BHIP, BHIQ, BHIR)) -> {ok,
telega@dialog@types:dialog_action(BHIO)} |
{error, BHIQ})
) -> dialog_builder(BHIO, BHIP, BHIQ, BHIR).
on_sub_result(Builder, Window, Handler) ->
{dialog_builder,
erlang:element(2, Builder),
erlang:element(3, Builder),
erlang:element(4, Builder),
erlang:element(5, Builder),
erlang:element(6, Builder),
erlang:element(7, Builder),
erlang:element(8, Builder),
erlang:element(9, Builder),
[{Window, Handler} | erlang:element(10, Builder)],
erlang:element(11, Builder),
erlang:element(12, Builder),
erlang:element(13, Builder)}.
-file("src/telega/dialog.gleam", 376).
?DOC(" Set the window the dialog opens with.\n").
-spec initial(dialog_builder(BHJI, BHJJ, BHJK, BHJL), binary()) -> dialog_builder(BHJI, BHJJ, BHJK, BHJL).
initial(Builder, Window_id) ->
{dialog_builder,
erlang:element(2, Builder),
erlang:element(3, Builder),
{some, Window_id},
erlang:element(5, Builder),
erlang:element(6, Builder),
erlang:element(7, Builder),
erlang:element(8, Builder),
erlang:element(9, Builder),
erlang:element(10, Builder),
erlang:element(11, Builder),
erlang:element(12, Builder),
erlang:element(13, Builder)}.
-file("src/telega/dialog.gleam", 385).
?DOC(
" Called when a window returns `Done`: receives the final state. The live\n"
" message keeps its text but loses the keyboard.\n"
).
-spec on_done(
dialog_builder(BHJU, BHJV, BHJW, BHJX),
fun((BHJU, telega@bot:context(BHJV, BHJW, BHJX)) -> {ok,
telega@bot:context(BHJV, BHJW, BHJX)} |
{error, BHJW})
) -> dialog_builder(BHJU, BHJV, BHJW, BHJX).
on_done(Builder, Handler) ->
{dialog_builder,
erlang:element(2, Builder),
erlang:element(3, Builder),
erlang:element(4, Builder),
erlang:element(5, Builder),
erlang:element(6, Builder),
erlang:element(7, Builder),
{some, Handler},
erlang:element(9, Builder),
erlang:element(10, Builder),
erlang:element(11, Builder),
erlang:element(12, Builder),
erlang:element(13, Builder)}.
-file("src/telega/dialog.gleam", 394).
?DOC(" Expire the dialog after `ms` milliseconds (lazy check on next event).\n").
-spec with_ttl(dialog_builder(BHKO, BHKP, BHKQ, BHKR), integer()) -> dialog_builder(BHKO, BHKP, BHKQ, BHKR).
with_ttl(Builder, Ms) ->
{dialog_builder,
erlang:element(2, Builder),
erlang:element(3, Builder),
erlang:element(4, Builder),
erlang:element(5, Builder),
erlang:element(6, Builder),
erlang:element(7, Builder),
erlang:element(8, Builder),
erlang:element(9, Builder),
erlang:element(10, Builder),
erlang:element(11, Builder),
{some, Ms},
erlang:element(13, Builder)}.
-file("src/telega/dialog.gleam", 404).
?DOC(
" Localize engine-generated texts (stale-button notice, widget labels).\n"
" The factory receives the update's `Context`, so `telega_i18n.t` works:\n"
" `dialog.with_labels(builder, fn(ctx) { labels_from_i18n(ctx) })`.\n"
).
-spec with_labels(
dialog_builder(BHLA, BHLB, BHLC, BHLD),
fun((telega@bot:context(BHLB, BHLC, BHLD)) -> telega@dialog@types:labels())
) -> dialog_builder(BHLA, BHLB, BHLC, BHLD).
with_labels(Builder, Labels) ->
{dialog_builder,
erlang:element(2, Builder),
erlang:element(3, Builder),
erlang:element(4, Builder),
erlang:element(5, Builder),
erlang:element(6, Builder),
erlang:element(7, Builder),
erlang:element(8, Builder),
erlang:element(9, Builder),
erlang:element(10, Builder),
erlang:element(11, Builder),
erlang:element(12, Builder),
Labels}.
-file("src/telega/dialog.gleam", 765).
-spec namespaced_id(binary(), binary()) -> binary().
namespaced_id(Sub_id, Window_id) ->
<<<<Sub_id/binary, (<<"."/utf8>>)/binary>>/binary, Window_id/binary>>.
-file("src/telega/dialog.gleam", 771).
?DOC(
" Re-key an (already erased) sub-dialog window under the sub namespace and\n"
" map its navigation targets into it.\n"
).
-spec namespace_window(
binary(),
telega@dialog@types:window(binary(), BHQT, BHQU, BHQV)
) -> telega@dialog@types:window(binary(), BHQT, BHQU, BHQV).
namespace_window(Sub_id, Window) ->
Map_action = fun(Action) -> case Action of
{goto, Window_id, State} ->
{goto, namespaced_id(Sub_id, Window_id), State};
Other ->
Other
end end,
{window,
namespaced_id(Sub_id, erlang:element(2, Window)),
erlang:element(3, Window),
fun(Raw, Event, Ctx) ->
_pipe = (erlang:element(4, Window))(Raw, Event, Ctx),
gleam@result:map(_pipe, Map_action)
end,
gleam@option:map(
erlang:element(5, Window),
fun(On_text) ->
fun(Raw@1, Text, Ctx@1) ->
_pipe@1 = On_text(Raw@1, Text, Ctx@1),
gleam@result:map(_pipe@1, Map_action)
end
end
),
gleam@list:map(
erlang:element(6, Window),
fun(Widget_item) ->
{keyboard_widget,
erlang:element(2, Widget_item),
erlang:element(3, Widget_item),
fun(Wctx, Cmd, Arg) ->
_pipe@2 = (erlang:element(4, Widget_item))(
Wctx,
Cmd,
Arg
),
gleam@result:map(
_pipe@2,
fun(Widget_result) -> case Widget_result of
{emit, Action@1} ->
{emit, Map_action(Action@1)};
Other@1 ->
Other@1
end end
)
end,
gleam@list:map(
erlang:element(5, Widget_item),
fun(_capture) -> namespaced_id(Sub_id, _capture) end
),
erlang:element(6, Widget_item)}
end
),
gleam@option:map(
erlang:element(7, Window),
fun(Handler) ->
fun(Raw@2, Sub_result, Ctx@2) ->
_pipe@3 = Handler(Raw@2, Sub_result, Ctx@2),
gleam@result:map(_pipe@3, Map_action)
end
end
)}.
-file("src/telega/dialog.gleam", 714).
-spec erase_action(
telega@dialog@types:dialog_action(BHPS),
fun((BHPS) -> binary())
) -> telega@dialog@types:dialog_action(binary()).
erase_action(Action, Encode) ->
case Action of
{stay, State} ->
{stay, Encode(State)};
{goto, Window_id, State@1} ->
{goto, Window_id, Encode(State@1)};
{back, State@2} ->
{back, Encode(State@2)};
{done, State@3} ->
{done, Encode(State@3)};
{start_sub, Sub_id, Args, State@4} ->
{start_sub, Sub_id, Args, Encode(State@4)}
end.
-file("src/telega/dialog.gleam", 753).
-spec typed_widget_ctx(
telega@dialog@types:widget_ctx(binary(), BHQH, BHQI, BHQJ),
fun((binary()) -> BHQO)
) -> telega@dialog@types:widget_ctx(BHQO, BHQH, BHQI, BHQJ).
typed_widget_ctx(Wctx, Decode) ->
{widget_ctx,
Decode(erlang:element(2, Wctx)),
erlang:element(3, Wctx),
erlang:element(4, Wctx),
erlang:element(5, Wctx)}.
-file("src/telega/dialog.gleam", 729).
-spec erase_widget(
telega@dialog@types:keyboard_widget(BHPV, BHPW, BHPX, BHPY),
fun((BHPV) -> binary()),
fun((binary()) -> BHPV)
) -> telega@dialog@types:keyboard_widget(binary(), BHPW, BHPX, BHPY).
erase_widget(Widget_item, Encode, Decode) ->
{keyboard_widget,
erlang:element(2, Widget_item),
fun(Wctx) ->
(erlang:element(3, Widget_item))(typed_widget_ctx(Wctx, Decode))
end,
fun(Wctx@1, Cmd, Arg) ->
_pipe = (erlang:element(4, Widget_item))(
typed_widget_ctx(Wctx@1, Decode),
Cmd,
Arg
),
gleam@result:map(_pipe, fun(Widget_result) -> case Widget_result of
{store_updated, Store} ->
{store_updated, Store};
{emit, Action} ->
{emit, erase_action(Action, Encode)}
end end)
end,
erlang:element(5, Widget_item),
erlang:element(6, Widget_item)}.
-file("src/telega/dialog.gleam", 687).
-spec erase_window(
telega@dialog@types:window(BHPG, BHPH, BHPI, BHPJ),
fun((BHPG) -> binary()),
fun((binary()) -> BHPG)
) -> telega@dialog@types:window(binary(), BHPH, BHPI, BHPJ).
erase_window(Window, Encode, Decode) ->
{window,
erlang:element(2, Window),
fun(Raw, Ctx) -> (erlang:element(3, Window))(Decode(Raw), Ctx) end,
fun(Raw@1, Event, Ctx@1) ->
_pipe = (erlang:element(4, Window))(Decode(Raw@1), Event, Ctx@1),
gleam@result:map(
_pipe,
fun(_capture) -> erase_action(_capture, Encode) end
)
end,
gleam@option:map(
erlang:element(5, Window),
fun(On_text) ->
fun(Raw@2, Text, Ctx@2) ->
_pipe@1 = On_text(Decode(Raw@2), Text, Ctx@2),
gleam@result:map(
_pipe@1,
fun(_capture@1) -> erase_action(_capture@1, Encode) end
)
end
end
),
gleam@list:map(
erlang:element(6, Window),
fun(_capture@2) -> erase_widget(_capture@2, Encode, Decode) end
),
gleam@option:map(
erlang:element(7, Window),
fun(Handler) ->
fun(Raw@3, Sub_result, Ctx@3) ->
_pipe@2 = Handler(Decode(Raw@3), Sub_result, Ctx@3),
gleam@result:map(
_pipe@2,
fun(_capture@3) -> erase_action(_capture@3, Encode) end
)
end
end
)}.
-file("src/telega/dialog.gleam", 671).
-spec require(
boolean(),
telega@dialog@types:dialog_build_error(),
fun(() -> {ok, BHPB} | {error, telega@dialog@types:dialog_build_error()})
) -> {ok, BHPB} | {error, telega@dialog@types:dialog_build_error()}.
require(Condition, Build_error, Continue) ->
gleam@bool:guard(not Condition, {error, Build_error}, Continue).
-file("src/telega/dialog.gleam", 547).
?DOC(
" Measure the callback-data budget through the very function that packs it\n"
" at render time (`render.callback_data`), so build-time validation and\n"
" runtime packing can never diverge.\n"
).
-spec validate_budget(binary(), binary(), binary()) -> {ok, nil} |
{error, telega@dialog@types:dialog_build_error()}.
validate_budget(Dialog_id, Window_id, Action_id) ->
Bytes = erlang:byte_size(
telega@dialog@render:callback_data(
Dialog_id,
Window_id,
Action_id,
none
)
),
require(
Bytes =< 64,
{callback_data_too_long, Window_id, Action_id, Bytes},
fun() -> {ok, nil} end
).
-file("src/telega/dialog.gleam", 641).
?DOC(
" Sub-dialog attachments: unique ids, one nesting level, and the re-checked\n"
" 64-byte budget — the sub was validated against its own dialog id, but at\n"
" runtime its buttons carry the parent id plus the `<sub_id>.` prefix.\n"
).
-spec validate_subs(binary(), list(sub_attachment(any(), any(), any()))) -> {ok,
nil} |
{error, telega@dialog@types:dialog_build_error()}.
validate_subs(Dialog_id, Subs) ->
gleam@result:'try'(
gleam@list:try_fold(
Subs,
gleam@set:new(),
fun(Seen, Sub) ->
require(
not gleam@set:contains(Seen, erlang:element(2, Sub)),
{duplicate_sub_dialog_id, erlang:element(2, Sub)},
fun() ->
require(
not erlang:element(7, Sub),
{nested_sub_dialog, erlang:element(2, Sub)},
fun() ->
gleam@result:'try'(
gleam@list:try_each(
erlang:element(3, Sub),
fun(Window) ->
Namespaced = namespaced_id(
erlang:element(2, Sub),
erlang:element(2, Window)
),
gleam@result:'try'(
validate_budget(
Dialog_id,
Namespaced,
<<"x"/utf8>>
),
fun(_use0) ->
nil = _use0,
gleam@list:try_each(
erlang:element(
6,
Window
),
fun(Widget) ->
gleam@list:try_each(
erlang:element(
6,
Widget
),
fun(_capture) ->
validate_budget(
Dialog_id,
Namespaced,
_capture
)
end
)
end
)
end
)
end
),
fun(_use0@1) ->
nil = _use0@1,
{ok,
gleam@set:insert(
Seen,
erlang:element(2, Sub)
)}
end
)
end
)
end
)
end
),
fun(_) -> {ok, nil} end
).
-file("src/telega/dialog.gleam", 621).
?DOC(
" Widget `Goto` targets (e.g. a multiselect's `done` window) must reference\n"
" existing windows; checked once all windows are known.\n"
).
-spec validate_widget_targets(
list(telega@dialog@types:window(any(), any(), any(), any()))
) -> {ok, nil} | {error, telega@dialog@types:dialog_build_error()}.
validate_widget_targets(Windows) ->
Window_ids = gleam@set:from_list(
gleam@list:map(Windows, fun(Window) -> erlang:element(2, Window) end)
),
gleam@list:try_each(
Windows,
fun(Window@1) ->
gleam@list:try_each(
erlang:element(6, Window@1),
fun(Widget) ->
gleam@list:try_each(
erlang:element(5, Widget),
fun(Target) ->
require(
gleam@set:contains(Window_ids, Target),
{unknown_window_reference,
erlang:element(2, Window@1),
Target},
fun() -> {ok, nil} end
)
end
)
end
)
end
).
-file("src/telega/dialog.gleam", 591).
-spec validate_widgets(
binary(),
binary(),
telega@dialog@types:window(any(), any(), any(), any())
) -> {ok, nil} | {error, telega@dialog@types:dialog_build_error()}.
validate_widgets(Dialog_id, Window_id, Window) ->
gleam@result:'try'(
gleam@list:try_fold(
erlang:element(6, Window),
gleam@set:new(),
fun(Seen, Widget) ->
require(
not gleam_stdlib:contains_string(
erlang:element(2, Widget),
<<":"/utf8>>
),
{reserved_id_character,
<<"widget"/utf8>>,
erlang:element(2, Widget)},
fun() ->
require(
not gleam@set:contains(
Seen,
erlang:element(2, Widget)
),
{duplicate_widget_id,
erlang:element(2, Window),
erlang:element(2, Widget)},
fun() ->
gleam@result:'try'(
gleam@list:try_each(
erlang:element(6, Widget),
fun(_capture) ->
validate_budget(
Dialog_id,
Window_id,
_capture
)
end
),
fun(_use0) ->
nil = _use0,
{ok,
gleam@set:insert(
Seen,
erlang:element(2, Widget)
)}
end
)
end
)
end
)
end
),
fun(_) -> {ok, nil} end
).
-file("src/telega/dialog.gleam", 566).
-spec validate_windows(
binary(),
list(telega@dialog@types:window(any(), any(), any(), any()))
) -> {ok, nil} | {error, telega@dialog@types:dialog_build_error()}.
validate_windows(Dialog_id, Windows) ->
gleam@result:'try'(
gleam@list:try_fold(
Windows,
gleam@set:new(),
fun(Seen, Window) ->
require(
not gleam_stdlib:contains_string(
erlang:element(2, Window),
<<":"/utf8>>
)
andalso not gleam_stdlib:contains_string(
erlang:element(2, Window),
<<"."/utf8>>
),
{reserved_id_character,
<<"window"/utf8>>,
erlang:element(2, Window)},
fun() ->
require(
not gleam@set:contains(
Seen,
erlang:element(2, Window)
),
{duplicate_window_id, erlang:element(2, Window)},
fun() ->
gleam@result:'try'(
validate_budget(
Dialog_id,
erlang:element(2, Window),
<<"x"/utf8>>
),
fun(_use0) ->
nil = _use0,
gleam@result:'try'(
validate_widgets(
Dialog_id,
erlang:element(2, Window),
Window
),
fun(_use0@1) ->
nil = _use0@1,
{ok,
gleam@set:insert(
Seen,
erlang:element(
2,
Window
)
)}
end
)
end
)
end
)
end
)
end
),
fun(_) -> {ok, nil} end
).
-file("src/telega/dialog.gleam", 514).
-spec attach_sub_result_hooks(
list(telega@dialog@types:window(BHML, BHMM, BHMN, BHMO)),
list({binary(),
fun((BHML, gleam@dict:dict(binary(), binary()), telega@bot:context(BHMM, BHMN, BHMO)) -> {ok,
telega@dialog@types:dialog_action(BHML)} |
{error, BHMN})})
) -> {ok, list(telega@dialog@types:window(BHML, BHMM, BHMN, BHMO))} |
{error, telega@dialog@types:dialog_build_error()}.
attach_sub_result_hooks(Windows, Hooks) ->
gleam@list:try_fold(
Hooks,
Windows,
fun(Windows@1, Hook) ->
{Window_id, Handler} = Hook,
require(
gleam@list:any(
Windows@1,
fun(Window) -> erlang:element(2, Window) =:= Window_id end
),
{unknown_window_reference, <<"on_sub_result"/utf8>>, Window_id},
fun() ->
{ok,
gleam@list:map(
Windows@1,
fun(Window@1) ->
case erlang:element(2, Window@1) =:= Window_id of
true ->
{window,
erlang:element(2, Window@1),
erlang:element(3, Window@1),
erlang:element(4, Window@1),
erlang:element(5, Window@1),
erlang:element(6, Window@1),
{some, Handler}};
false ->
Window@1
end
end
)}
end
)
end
).
-file("src/telega/dialog.gleam", 420).
?DOC(
" Validate and build the dialog. Checks window ids for duplicates and the\n"
" reserved `:`/`.` characters, the initial window, `on_sub_result` and\n"
" widget window references, sub-dialog attachments, and that all\n"
" callback-data prefixes (including the `<sub_id>.` namespace of sub\n"
" windows) leave room within Telegram's 64-byte limit.\n"
"\n"
" Building also **erases** the state type: every window is wrapped so its\n"
" closures decode/encode the state with this dialog's codec, which is what\n"
" lets sub-dialogs with different state types share one flow.\n"
).
-spec build(dialog_builder(BHLP, BHLQ, BHLR, BHLS)) -> {ok,
dialog(BHLP, BHLQ, BHLR, BHLS)} |
{error, telega@dialog@types:dialog_build_error()}.
build(Builder) ->
Windows = lists:reverse(erlang:element(3, Builder)),
Subs = lists:reverse(erlang:element(9, Builder)),
require(
Windows /= [],
no_windows,
fun() ->
require(
not gleam_stdlib:contains_string(
erlang:element(2, Builder),
<<":"/utf8>>
)
andalso not gleam_stdlib:contains_string(
erlang:element(2, Builder),
<<"."/utf8>>
),
{reserved_id_character,
<<"dialog"/utf8>>,
erlang:element(2, Builder)},
fun() ->
gleam@result:'try'(
attach_sub_result_hooks(
Windows,
lists:reverse(erlang:element(10, Builder))
),
fun(Windows@1) ->
gleam@result:'try'(
validate_windows(
erlang:element(2, Builder),
Windows@1
),
fun(_use0) ->
nil = _use0,
gleam@result:'try'(
validate_widget_targets(Windows@1),
fun(_use0@1) ->
nil = _use0@1,
gleam@result:'try'(
validate_subs(
erlang:element(2, Builder),
Subs
),
fun(_use0@2) ->
nil = _use0@2,
gleam@result:'try'(
gleam@option:to_result(
erlang:element(
4,
Builder
),
{unknown_initial_window,
<<""/utf8>>}
),
fun(Initial) ->
require(
gleam@list:any(
Windows@1,
fun(Window) ->
erlang:element(
2,
Window
)
=:= Initial
end
),
{unknown_initial_window,
Initial},
fun() ->
Encode = erlang:element(
6,
Builder
),
Decode = decode_or_initial(
Builder
),
Erased_own = gleam@list:map(
Windows@1,
fun(
Window@1
) ->
{erlang:element(
2,
Window@1
),
erase_window(
Window@1,
Encode,
Decode
)}
end
),
Erased_subs = gleam@list:flat_map(
Subs,
fun(Sub) ->
gleam@list:map(
erlang:element(
3,
Sub
),
fun(
Window@2
) ->
Namespaced = namespace_window(
erlang:element(
2,
Sub
),
Window@2
),
{erlang:element(
2,
Namespaced
),
Namespaced}
end
)
end
),
{ok,
{dialog,
{compiled_dialog,
erlang:element(
2,
Builder
),
maps:from_list(
lists:append(
Erased_own,
Erased_subs
)
),
Initial,
fun(
) ->
Encode(
(erlang:element(
5,
Builder
))(
)
)
end,
gleam@option:map(
erlang:element(
8,
Builder
),
fun(
On_done
) ->
fun(
Raw,
Ctx
) ->
On_done(
Decode(
Raw
),
Ctx
)
end
end
),
maps:from_list(
gleam@list:map(
Subs,
fun(
Sub@1
) ->
{erlang:element(
2,
Sub@1
),
{compiled_sub,
erlang:element(
2,
Sub@1
),
namespaced_id(
erlang:element(
2,
Sub@1
),
erlang:element(
4,
Sub@1
)
),
erlang:element(
5,
Sub@1
),
erlang:element(
6,
Sub@1
)}}
end
)
),
erlang:element(
11,
Builder
),
erlang:element(
12,
Builder
),
erlang:element(
13,
Builder
)},
Encode,
Decode}}
end
)
end
)
end
)
end
)
end
)
end
)
end
)
end
).
-file("src/telega/dialog.gleam", 819).
?DOC(false).
-spec compiled(dialog(any(), BHRF, BHRG, BHRH)) -> telega@dialog@engine:compiled_dialog(BHRF, BHRG, BHRH).
compiled(Dialog) ->
erlang:element(2, Dialog).
-file("src/telega/dialog.gleam", 860).
-spec with_dialog_routing(
telega@flow@registry:flow_registry(BHSR, BHSS, BHST),
dialog(any(), BHSR, BHSS, BHST)
) -> telega@flow@registry:flow_registry(BHSR, BHSS, BHST).
with_dialog_routing(Registry, Dialog) ->
Flow_name = <<(<<"__dialog:"/utf8>>)/binary,
(erlang:element(2, erlang:element(2, Dialog)))/binary>>,
Prefix = <<<<"dlg:"/utf8,
(erlang:element(2, erlang:element(2, Dialog)))/binary>>/binary,
":"/utf8>>,
Labels = erlang:element(10, erlang:element(2, Dialog)),
_pipe = Registry,
_pipe@1 = telega@flow@registry:with_callback_filter(
_pipe,
Flow_name,
fun(_capture) -> gleam_stdlib:string_starts_with(_capture, Prefix) end
),
telega@flow@registry:with_orphan_callback_handler(
_pipe@1,
fun(_capture@1) ->
gleam_stdlib:string_starts_with(_capture@1, Prefix)
end,
fun(Ctx, _) ->
telega@dialog@render:answer_quietly(
Ctx,
{some, erlang:element(10, Labels(Ctx))}
),
{ok, Ctx}
end
).
-file("src/telega/dialog.gleam", 836).
?DOC(
" Register the dialog in a flow registry without a trigger — start it\n"
" programmatically with `start`. Event delivery into the active window is\n"
" the registry's standard auto-resume, so remember to finish with\n"
" `flow_registry.apply_to_router`.\n"
"\n"
" Attaching also wires two routing guards for free: the dialog only\n"
" auto-resumes on its own `dlg:<id>:` callbacks (so several waiting\n"
" flows/dialogs can coexist), and presses on messages of an already\n"
" finished dialog are answered with `labels.stale` instead of hanging.\n"
).
-spec attach(
telega@flow@registry:flow_registry(BHRP, BHRQ, BHRR),
dialog(any(), BHRP, BHRQ, BHRR)
) -> telega@flow@registry:flow_registry(BHRP, BHRQ, BHRR).
attach(Registry, Dialog) ->
_pipe = telega@flow@registry:register_callable(
Registry,
telega@dialog@engine:compile(erlang:element(2, Dialog))
),
with_dialog_routing(_pipe, Dialog).
-file("src/telega/dialog.gleam", 847).
?DOC(
" Register the dialog and start it on a command (e.g. `\"settings\"` for\n"
" `/settings`). A repeated command while the dialog is active resumes it.\n"
" Wires the same routing guards as `attach`.\n"
).
-spec attach_on_command(
telega@flow@registry:flow_registry(BHSD, BHSE, BHSF),
binary(),
dialog(any(), BHSD, BHSE, BHSF)
) -> telega@flow@registry:flow_registry(BHSD, BHSE, BHSF).
attach_on_command(Registry, Command, Dialog) ->
_pipe = telega@flow@registry:register(
Registry,
{on_command, Command},
telega@dialog@engine:compile(erlang:element(2, Dialog))
),
with_dialog_routing(_pipe, Dialog).
-file("src/telega/dialog.gleam", 882).
?DOC(" Start (or resume) an attached dialog from any handler.\n").
-spec start(
telega@bot:context(BHTF, BHTG, BHTH),
telega@flow@registry:flow_registry(BHTF, BHTG, BHTH),
binary()
) -> {ok, telega@bot:context(BHTF, BHTG, BHTH)} | {error, BHTG}.
start(Ctx, Registry, Dialog_id) ->
telega@flow@registry:call_flow(
Ctx,
Registry,
<<(<<"__dialog:"/utf8>>)/binary, Dialog_id/binary>>,
maps:new()
).
-file("src/telega/dialog.gleam", 897).
?DOC(
" Delete the current instance and start the dialog from scratch (a\n"
" repeated start command only resumes — this is the hard reset).\n"
).
-spec restart(
telega@bot:context(BHTT, BHTU, BHTV),
telega@flow@registry:flow_registry(BHTT, BHTU, BHTV),
binary()
) -> {ok, telega@bot:context(BHTT, BHTU, BHTV)} | {error, BHTU}.
restart(Ctx, Registry, Dialog_id) ->
{User_id, Chat_id} = telega@flow@engine:extract_ids_from_context(Ctx),
Instance_id = telega@flow@storage:generate_id(
User_id,
Chat_id,
<<(<<"__dialog:"/utf8>>)/binary, Dialog_id/binary>>
),
_ = telega@flow@registry:cancel_flow_instance(Registry, Instance_id),
start(Ctx, Registry, Dialog_id).
-file("src/telega/dialog.gleam", 926).
?DOC(
" Read a widget's persistent store from inside a window render or handler\n"
" (`on_action`, `on_text`, `on_done`). Combine with the typed readers from\n"
" `telega/dialog/widget`:\n"
"\n"
" ```gleam\n"
" let zone =\n"
" dialog.widget_store(ctx, window_id: \"prefs\", widget_id: \"zone\")\n"
" |> widget.radio_value\n"
" |> option.unwrap(\"hall\")\n"
" ```\n"
"\n"
" Returns an empty store when the widget has no state yet. In pure render\n"
" tests seed the store first with `widget.seed_store`.\n"
).
-spec widget_store(telega@bot:context(any(), any(), any()), binary(), binary()) -> telega@dialog@types:widget_store().
widget_store(Ctx, Window_id, Widget_id) ->
telega@dialog@widget:widget_store(Ctx, Window_id, Widget_id).
-file("src/telega/dialog.gleam", 939).
?DOC(
" Show a modal alert to the user who pressed the button. Call inside\n"
" `on_action` before returning an action; the engine will skip its\n"
" automatic spinner-removing answer for this event.\n"
).
-spec alert(telega@bot:context(any(), any(), any()), binary()) -> {ok, nil} |
{error, telega@error:telega_error()}.
alert(Ctx, Text) ->
telega@dialog@render:alert(Ctx, Text).
-file("src/telega/dialog.gleam", 948).
?DOC(
" Show a toast notification at the top of the chat. Same contract as\n"
" `alert`.\n"
).
-spec toast(telega@bot:context(any(), any(), any()), binary()) -> {ok, nil} |
{error, telega@error:telega_error()}.
toast(Ctx, Text) ->
telega@dialog@render:toast(Ctx, Text).