Current section

Files

Jump to
telega src telega flow handler.gleam
Raw

src/telega/flow/handler.gleam

//// Built-in step handlers and resume handler factories.
import gleam/dict
import gleam/option.{type Option, None, Some}
import gleam/string
import telega/bot.{type Context}
import telega/flow/engine
import telega/flow/instance
import telega/flow/types.{
type Flow, type FlowInstance, type StepHandler, Cancel, Complete, Next,
Pending, TextInput, Wait,
}
import telega/keyboard
import telega/reply
import telega/update
/// Create a text input step
pub fn text_step(
prompt: String,
data_key: String,
next_step: step_type,
) -> StepHandler(step_type, session, error, dependencies) {
fn(ctx: Context(session, error, dependencies), instance_val: FlowInstance) {
case instance.get_wait_result(instance_val) {
TextInput(value:) -> {
let instance_val = instance.store_data(instance_val, data_key, value)
Ok(#(ctx, Next(next_step), instance_val))
}
Pending -> {
case reply.with_text(ctx, prompt) {
Ok(_) -> Ok(#(ctx, Wait, instance_val))
Error(_) -> Ok(#(ctx, Cancel, instance_val))
}
}
_ -> {
case reply.with_text(ctx, prompt) {
Ok(_) -> Ok(#(ctx, Wait, instance_val))
Error(_) -> Ok(#(ctx, Cancel, instance_val))
}
}
}
}
}
/// Like `text_step`, but the prompt is computed per update from the `Context`
/// and the flow instance instead of being fixed when the flow is built.
///
/// Use this when the prompt depends on something only known at update time —
/// most commonly the active locale for internationalization. `text_step` bakes
/// its prompt in at flow-construction time (startup), which is too early to know
/// the user's language; `text_step_with` resolves it on every prompt instead.
///
/// ```gleam
/// builder.add_step(
/// Date,
/// handler.text_step_with(
/// fn(ctx, _instance) { i18n.t(ctx, "book.ask_date", []) },
/// "booking_date",
/// Time,
/// ),
/// )
/// ```
pub fn text_step_with(
prompt prompt: fn(Context(session, error, dependencies), FlowInstance) ->
String,
data_key data_key: String,
next_step next_step: step_type,
) -> StepHandler(step_type, session, error, dependencies) {
fn(ctx: Context(session, error, dependencies), instance_val: FlowInstance) {
case instance.get_wait_result(instance_val) {
TextInput(value:) -> {
let instance_val = instance.store_data(instance_val, data_key, value)
Ok(#(ctx, Next(next_step), instance_val))
}
_ -> {
case reply.with_text(ctx, prompt(ctx, instance_val)) {
Ok(_) -> Ok(#(ctx, Wait, instance_val))
Error(_) -> Ok(#(ctx, Cancel, instance_val))
}
}
}
}
}
/// Create a message display step
pub fn message_step(
message_fn: fn(FlowInstance) -> String,
next_step: Option(step_type),
) -> StepHandler(step_type, session, error, dependencies) {
fn(ctx: Context(session, error, dependencies), instance_val: FlowInstance) {
let message = message_fn(instance_val)
case reply.with_text(ctx, message) {
Ok(_) -> {
case next_step {
Some(step) -> Ok(#(ctx, Next(step), instance_val))
None -> Ok(#(ctx, Complete(instance_val.state.data), instance_val))
}
}
Error(_) -> Ok(#(ctx, Cancel, instance_val))
}
}
}
/// Like `message_step`, but the message is computed from the `Context` (in
/// addition to the flow instance), enabling localization.
///
/// ```gleam
/// builder.add_step(
/// Welcome,
/// handler.message_step_with(
/// fn(ctx, _instance) { i18n.t(ctx, "book.welcome", []) },
/// option.Some(Date),
/// ),
/// )
/// ```
pub fn message_step_with(
message_fn message_fn: fn(Context(session, error, dependencies), FlowInstance) ->
String,
next_step next_step: Option(step_type),
) -> StepHandler(step_type, session, error, dependencies) {
fn(ctx: Context(session, error, dependencies), instance_val: FlowInstance) {
let message = message_fn(ctx, instance_val)
case reply.with_text(ctx, message) {
Ok(_) -> {
case next_step {
Some(step) -> Ok(#(ctx, Next(step), instance_val))
None -> Ok(#(ctx, Complete(instance_val.state.data), instance_val))
}
}
Error(_) -> Ok(#(ctx, Cancel, instance_val))
}
}
}
/// Create a router handler for resuming flows from callback queries
pub fn create_resume_handler(
flow: Flow(step_type, session, error, dependencies),
) -> fn(Context(session, error, dependencies), update.Update) ->
Result(Context(session, error, dependencies), error) {
resume_handler(flow)
}
/// Create a router handler for resuming flows from callback queries with keyboard parsing
pub fn create_resume_handler_with_keyboard(
flow: Flow(step_type, session, error, dependencies),
callback_data: keyboard.KeyboardCallbackData(String),
) -> fn(Context(session, error, dependencies), update.Update) ->
Result(Context(session, error, dependencies), error) {
resume_handler_with_keyboard(flow, callback_data)
}
/// Create a text handler for resuming flows
pub fn create_text_handler(
flow: Flow(step_type, session, error, dependencies),
) -> fn(Context(session, error, dependencies), update.Update) ->
Result(Context(session, error, dependencies), error) {
fn(ctx, upd) {
case upd {
update.TextUpdate(text:, from_id:, chat_id:, ..) -> {
case flow.storage.list_by_user(from_id, chat_id) {
Ok([inst, ..]) if inst.wait_token != None -> {
let data = dict.from_list([#("user_input", text)])
engine.resume_with_token(
flow,
ctx,
option.unwrap(inst.wait_token, ""),
Some(data),
)
}
_ -> Ok(ctx)
}
}
_ -> Ok(ctx)
}
}
}
fn resume_handler(
flow flow: Flow(step_type, session, error, dependencies),
) -> fn(Context(session, error, dependencies), update.Update) ->
Result(Context(session, error, dependencies), error) {
fn(ctx, upd) {
case upd {
update.CallbackQueryUpdate(query:, ..) -> {
let data = option.unwrap(query.data, "")
let token = case string.split(data, ":") {
[_prefix, token, ..] -> token
_ -> data
}
let resume_data =
dict.from_list([
#("callback_data", data),
#(
instance.wait_result_key,
instance.encode_callback_wait_result(data),
),
])
engine.resume_with_token(flow, ctx, token, Some(resume_data))
}
_ -> Ok(ctx)
}
}
}
fn resume_handler_with_keyboard(
flow: Flow(step_type, session, error, dependencies),
callback_data: keyboard.KeyboardCallbackData(String),
) -> fn(Context(session, error, dependencies), update.Update) ->
Result(Context(session, error, dependencies), error) {
fn(ctx, upd) {
case upd {
update.CallbackQueryUpdate(query:, ..) -> {
let data = option.unwrap(query.data, "")
let wait_result_value = instance.encode_callback_wait_result(data)
let resume_data =
dict.from_list([
#("callback_data", data),
#(instance.wait_result_key, wait_result_value),
])
case keyboard.unpack_callback(data, callback_data) {
Ok(callback) ->
engine.resume_with_token(
flow,
ctx,
callback.data,
Some(resume_data),
)
Error(_) -> {
let token = case string.split(data, ":") {
[_prefix, token, ..] -> token
_ -> data
}
engine.resume_with_token(flow, ctx, token, Some(resume_data))
}
}
}
_ -> Ok(ctx)
}
}
}