import gleam/bool import gleam/float import gleam/option.{type Option, None, Some} import gleam/result import gleam/string import gleam/time/duration import gleam/time/timestamp import telega/internal/config.{type Config} import telega/internal/log import telega/internal/registry import telega/internal/utils import telega/api import telega/bot.{ type BotSubject, type CallbackQueryFilter, type CatchHandler, type Context, type Handler, type Hears, type SessionSettings, HandleAll, HandleAudio, HandleCallbackQuery, HandleChatMember, HandleCommand, HandleCommands, HandleHears, HandleMessage, HandlePhotos, HandleText, HandleVideo, HandleVoice, HandleWebAppData, SessionSettings, } import telega/client.{type TelegramClient} import telega/error import telega/model.{type Update, type User} import telega/update.{type Command} pub opaque type Telega(session, error) { Telega(config: Config, bot_info: User, bot_subject: BotSubject) } pub opaque type TelegaBuilder(session, error) { TelegaBuilder( config: Config, handlers: List(Handler(session, error)), session_settings: Option(SessionSettings(session, error)), bot_subject: Option(BotSubject), catch_handler: Option(CatchHandler(session, error)), // SetWebhook parameters drop_pending_updates: Option(Bool), max_connections: Option(Int), ip_address: Option(String), allowed_updates: Option(List(String)), certificate: Option(model.File), ) } /// Check if a path is the webhook path for the bot. /// /// Useful if you plan to implement own adapter. pub fn is_webhook_path(telega: Telega(session, error), path: String) { telega.config.webhook_path == path } /// Check if a secret token is valid. /// /// Useful if you plan to implement own adapter. pub fn is_secret_token_valid(telega: Telega(session, error), token: String) { telega.config.secret_token == token } /// Helper to get the config for API requests. pub fn get_api_config(telega: Telega(session, error)) { telega.config.api_client } /// Create a new Telega instance. pub fn new( token token: String, url server_url: String, webhook_path webhook_path: String, secret_token secret_token: Option(String), ) { let url = utils.normalize_url(server_url) let webhook_path = utils.normalize_webhook_path(webhook_path) TelegaBuilder( handlers: [], config: config.new(token:, webhook_path:, secret_token:, url:), session_settings: None, bot_subject: None, catch_handler: None, drop_pending_updates: None, max_connections: None, ip_address: None, allowed_updates: None, certificate: None, ) } /// Handles all messages. pub fn handle_all( bot builder: TelegaBuilder(session, error), handler handler: fn(Context(session, error), update.Update) -> Result(Context(session, error), error), ) -> TelegaBuilder(session, error) { TelegaBuilder(..builder, handlers: [HandleAll(handler), ..builder.handlers]) } /// Stops bot message handling from current chat and waits for any message. /// /// See [conversation](/docs/conversation) pub fn wait_any( ctx ctx: Context(session, error), or handle_else: Option(Handler(session, error)), timeout timeout: Option(Int), continue handler: fn(Context(session, error), update.Update) -> Result(Context(session, error), error), ) -> Result(Context(session, error), error) { bot.wait_handler(ctx:, handler: HandleAll(handler:), handle_else:, timeout:) } /// Handles a specific command. pub fn handle_command( bot builder: TelegaBuilder(session, error), command command: String, handler handler: fn(Context(session, error), Command) -> Result(Context(session, error), error), ) { TelegaBuilder(..builder, handlers: [ HandleCommand(command, handler), ..builder.handlers ]) } /// Stops bot message handling from current chat and waits for a specific command. /// /// See [conversation](/docs/conversation) pub fn wait_command( ctx ctx: Context(session, error), command command: String, or handle_else: Option(Handler(session, error)), timeout timeout: Option(Int), continue continue: fn(Context(session, error), Command) -> Result(Context(session, error), error), ) -> Result(Context(session, error), error) { bot.wait_handler( ctx:, handler: HandleCommand(command, continue), handle_else:, timeout:, ) } /// Handles multiple commands. pub fn handle_commands( bot builder: TelegaBuilder(session, error), commands commands: List(String), handler handler: fn(Context(session, error), Command) -> Result(Context(session, error), error), ) { TelegaBuilder(..builder, handlers: [ HandleCommands(commands, handler), ..builder.handlers ]) } /// Stops bot message handling from current chat and waits for a specific command. /// /// See [conversation](/docs/conversation) pub fn wait_commands( ctx ctx: Context(session, error), commands commands: List(String), or handle_else: Option(Handler(session, error)), timeout timeout: Option(Int), continue continue: fn(Context(session, error), Command) -> Result(Context(session, error), error), ) -> Result(Context(session, error), error) { bot.wait_handler( ctx:, handler: HandleCommands(commands, continue), handle_else:, timeout:, ) } /// Handles text messages. pub fn handle_text( bot builder: TelegaBuilder(session, error), handler handler: fn(Context(session, error), String) -> Result(Context(session, error), error), ) { TelegaBuilder(..builder, handlers: [HandleText(handler), ..builder.handlers]) } /// Stops bot message handling from current chat and waits for a text message. /// /// See [conversation](/docs/conversation) pub fn wait_text( ctx ctx: Context(session, error), or handle_else: Option(Handler(session, error)), timeout timeout: Option(Int), continue continue: fn(Context(session, error), String) -> Result(Context(session, error), error), ) -> Result(Context(session, error), error) { bot.wait_handler(ctx:, handler: HandleText(continue), handle_else:, timeout:) } /// Handles messages that match the given `Hears`. pub fn handle_hears( bot builder: TelegaBuilder(session, error), hears hears: Hears, handler handler: fn(Context(session, error), String) -> Result(Context(session, error), error), ) { TelegaBuilder(..builder, handlers: [ HandleHears(hears, handler), ..builder.handlers ]) } /// Stops bot message handling from current chat and waits for a message that matches the given `Hears`. /// /// See [conversation](/docs/conversation) pub fn wait_hears( ctx ctx: Context(session, error), hears hears: Hears, or handle_else: Option(Handler(session, error)), timeout timeout: Option(Int), continue continue: fn(Context(session, error), String) -> Result(Context(session, error), error), ) -> Result(Context(session, error), error) { bot.wait_handler( ctx:, handler: HandleHears(hears, continue), handle_else:, timeout:, ) } /// Handles any message. pub fn handle_message( bot builder: TelegaBuilder(session, error), handler handler: fn(Context(session, error), model.Message) -> Result(Context(session, error), error), ) -> TelegaBuilder(session, error) { TelegaBuilder(..builder, handlers: [ HandleMessage(handler:), ..builder.handlers ]) } /// Stops bot message handling from current chat and waits for any message. /// /// See [conversation](/docs/conversation) pub fn wait_message( ctx ctx: Context(session, error), or handle_else: Option(Handler(session, error)), timeout timeout: Option(Int), continue continue: fn(Context(session, error), model.Message) -> Result(Context(session, error), error), ) -> Result(Context(session, error), error) { bot.wait_handler( ctx:, handler: HandleMessage(continue), handle_else:, timeout:, ) } /// Handles messages from inline keyboard callback. /// /// See [conversation](/docs/conversation) pub fn handle_callback_query( bot builder: TelegaBuilder(session, error), filter filter: CallbackQueryFilter, handler handler: fn(Context(session, error), String, String) -> Result(Context(session, error), error), ) -> TelegaBuilder(session, error) { TelegaBuilder(..builder, handlers: [ HandleCallbackQuery(filter, handler), ..builder.handlers ]) } /// Wait for a callback query and continue with the given function. /// /// See [conversation](/docs/conversation) pub fn wait_callback_query( ctx ctx: Context(session, error), filter filter: CallbackQueryFilter, or handle_else: Option(Handler(session, error)), timeout timeout: Option(Int), continue continue: fn(Context(session, error), String, String) -> Result(Context(session, error), error), ) -> Result(Context(session, error), error) { bot.wait_handler( ctx:, handler: HandleCallbackQuery(filter, continue), handle_else:, timeout:, ) } /// Handles voice messages. pub fn handle_voice( bot builder: TelegaBuilder(session, error), handler handler: fn(Context(session, error), model.Voice) -> Result(Context(session, error), error), ) -> TelegaBuilder(session, error) { TelegaBuilder(..builder, handlers: [HandleVoice(handler), ..builder.handlers]) } /// Stops bot message handling from current chat and waits for a voice message. /// /// See [conversation](/docs/conversation) pub fn wait_voice( ctx ctx: Context(session, error), or handle_else: Option(Handler(session, error)), timeout timeout: Option(Int), continue continue: fn(Context(session, error), model.Voice) -> Result(Context(session, error), error), ) -> Result(Context(session, error), error) { bot.wait_handler(ctx:, handler: HandleVoice(continue), handle_else:, timeout:) } /// Handles audio messages. pub fn handle_audio( bot builder: TelegaBuilder(session, error), handler handler: fn(Context(session, error), model.Audio) -> Result(Context(session, error), error), ) { TelegaBuilder(..builder, handlers: [HandleAudio(handler), ..builder.handlers]) } /// Stops bot message handling from current chat and waits for an audio message. /// /// See [conversation](/docs/conversation) pub fn wait_audio( ctx ctx: Context(session, error), or handle_else: Option(Handler(session, error)), timeout timeout: Option(Int), continue continue: fn(Context(session, error), model.Audio) -> Result(Context(session, error), error), ) -> Result(Context(session, error), error) { bot.wait_handler(ctx:, handler: HandleAudio(continue), handle_else:, timeout:) } /// Handles video messages. pub fn handle_video( bot builder: TelegaBuilder(session, error), handler handler: fn(Context(session, error), model.Video) -> Result(Context(session, error), error), ) { TelegaBuilder(..builder, handlers: [HandleVideo(handler), ..builder.handlers]) } /// Stops bot message handling from current chat and waits for a video message. /// /// See [conversation](/docs/conversation) pub fn wait_video( ctx ctx: Context(session, error), or handle_else: Option(Handler(session, error)), timeout timeout: Option(Int), continue continue: fn(Context(session, error), model.Video) -> Result(Context(session, error), error), ) -> Result(Context(session, error), error) { bot.wait_handler(ctx:, handler: HandleVideo(continue), handle_else:, timeout:) } /// Handles photo messages. pub fn handle_photos( bot builder: TelegaBuilder(session, error), handler handler: fn(Context(session, error), List(model.PhotoSize)) -> Result(Context(session, error), error), ) { TelegaBuilder(..builder, handlers: [HandlePhotos(handler), ..builder.handlers]) } /// Stops bot message handling from current chat and waits for a photo message. /// /// See [conversation](/docs/conversation) pub fn wait_photos( ctx ctx: Context(session, error), or handle_else: Option(Handler(session, error)), timeout timeout: Option(Int), continue continue: fn(Context(session, error), List(model.PhotoSize)) -> Result(Context(session, error), error), ) -> Result(Context(session, error), error) { bot.wait_handler( ctx:, handler: HandlePhotos(continue), handle_else:, timeout:, ) } /// Handles web app data messages. pub fn handle_web_app_data( bot builder: TelegaBuilder(session, error), handler handler: fn(Context(session, error), model.WebAppData) -> Result(Context(session, error), error), ) { TelegaBuilder(..builder, handlers: [ HandleWebAppData(handler), ..builder.handlers ]) } /// Stops bot message handling from current chat and waits for a web app data message. /// /// See [conversation](/docs/conversation) pub fn wait_web_app_data( ctx ctx: Context(session, error), or handle_else: Option(Handler(session, error)), timeout timeout: Option(Int), continue continue: fn(Context(session, error), model.WebAppData) -> Result(Context(session, error), error), ) -> Result(Context(session, error), error) { bot.wait_handler( ctx:, handler: HandleWebAppData(continue), handle_else:, timeout:, ) } /// Set a catch handler for all handlers. /// /// If handler returns `Error`, the chat instance will be stopped and the error will be logged /// The default handler is `fn(_) -> Ok(Nil)`, which will do nothing if handler returns an error pub fn with_catch_handler( builder builder: TelegaBuilder(session, error), catch_handler catch_handler: CatchHandler(session, error), ) -> TelegaBuilder(session, error) { TelegaBuilder(..builder, catch_handler: Some(catch_handler)) } /// Handles chat member update (when user joins/leaves a group). The bot must be an administrator in the chat and must explicitly specify "chat_member" in the list of `allowed_updates` to receive these updates. pub fn handle_chat_member( bot builder: TelegaBuilder(session, error), handler handler: fn(Context(session, error), model.ChatMemberUpdated) -> Result(Context(session, error), error), ) -> TelegaBuilder(session, error) { TelegaBuilder(..builder, handlers: [ HandleChatMember(handler), ..builder.handlers ]) } /// Log the message and error message if the handler fails. pub fn log_context( ctx ctx: Context(session, error), prefix prefix: String, handler handler: fn() -> Result(Context(session, error), error), ) -> Result(Context(session, error), error) { let id = utils.random_string(5) let prefix = "[" <> prefix <> ":" <> id <> "] " log.info(prefix <> "received update: " <> update.to_string(ctx.update)) let start_time = timestamp.system_time() let result = handler() |> result.map_error(fn(e) { log.error(prefix <> "handler failed: " <> string.inspect(e)) e }) let end_time = timestamp.system_time() let time = start_time |> timestamp.difference(end_time) |> duration.to_seconds |> float.to_string log.info(prefix <> "handler completed in " <> time <> " seconds") result } /// Construct a session settings. pub fn with_session_settings( builder: TelegaBuilder(session, error), persist_session persist_session: fn(String, session) -> Result(session, error), get_session get_session: fn(String) -> Result(Option(session), error), default_session default_session: fn() -> session, ) -> TelegaBuilder(session, error) { TelegaBuilder( ..builder, session_settings: Some(SessionSettings( persist_session:, get_session:, default_session:, )), ) } /// Initialize a Telega instance with a `Nil` session. /// Useful when you don't need to persist the session. pub fn init_nil_session( builder: TelegaBuilder(Nil, error), ) -> Result(Telega(Nil, error), error.TelegaError) { let persist_session = fn(_, _) { Ok(Nil) } let get_session = fn(_) { Ok(Some(Nil)) } let default_session = fn() { Nil } TelegaBuilder( ..builder, session_settings: Some(SessionSettings( persist_session:, get_session:, default_session:, )), ) |> init } /// Set the drop pending updates flag as set webhook parameter. pub fn set_drop_pending_updates( builder: TelegaBuilder(session, error), drop_pending_updates: Bool, ) -> TelegaBuilder(session, error) { TelegaBuilder(..builder, drop_pending_updates: Some(drop_pending_updates)) } /// Set the max connections as set webhook parameter. pub fn set_max_connections( builder: TelegaBuilder(session, error), max_connections: Int, ) -> TelegaBuilder(session, error) { TelegaBuilder(..builder, max_connections: Some(max_connections)) } /// Set the ip address as set webhook parameter. pub fn set_ip_address( builder: TelegaBuilder(session, error), ip_address: String, ) -> TelegaBuilder(session, error) { TelegaBuilder(..builder, ip_address: Some(ip_address)) } /// Set the allowed updates as set webhook parameter. pub fn set_allowed_updates( builder: TelegaBuilder(session, error), allowed_updates: List(String), ) -> TelegaBuilder(session, error) { TelegaBuilder(..builder, allowed_updates: Some(allowed_updates)) } /// Set the certificate as set webhook parameter. pub fn set_certificate( builder: TelegaBuilder(session, error), certificate: model.File, ) -> TelegaBuilder(session, error) { TelegaBuilder(..builder, certificate: Some(certificate)) } /// Pass custom api client to the builder. /// /// Useful if you want set custom fetch function or pass your api client options pub fn set_api_client( builder: TelegaBuilder(session, error), api_client: TelegramClient, ) -> TelegaBuilder(session, error) { TelegaBuilder(..builder, config: config.Config(..builder.config, api_client:)) } /// Initialize a Telega instance. /// This function should be called **only** after all handlers are added to the builder. /// It will set the webhook and start handling messages. pub fn init( builder: TelegaBuilder(session, error), ) -> Result(Telega(session, error), error.TelegaError) { use is_ok <- result.try(api.set_webhook( builder.config.api_client, model.SetWebhookParameters( url: builder.config.server_url <> "/" <> builder.config.webhook_path, secret_token: Some(builder.config.secret_token), drop_pending_updates: builder.drop_pending_updates, max_connections: builder.max_connections, ip_address: builder.ip_address, allowed_updates: builder.allowed_updates, certificate: builder.certificate, ), )) use <- bool.guard(!is_ok, Error(error.SetWebhookError)) use bot_info <- result.try(api.get_me(builder.config.api_client)) let session_settings = option.to_result(builder.session_settings, error.NoSessionSettingsError) use session_settings <- result.try(session_settings) use registry_subject <- result.try(registry.start()) let catch_handler = option.lazy_unwrap(builder.catch_handler, fn() { nil_catch_handler }) use bot_subject <- result.try(bot.start( bot_info:, catch_handler:, registry_subject:, session_settings:, config: builder.config, handlers: builder.handlers, )) Ok(Telega(bot_info:, bot_subject:, config: builder.config)) } fn nil_catch_handler(_, _) { Ok(Nil) } /// Handle an update from the Telegram API. pub fn handle_update( telega: Telega(session, error), raw_update: Update, ) -> Result(Bool, error.TelegaError) { update.raw_to_update(raw_update) |> bot.handle_update(telega.bot_subject, _) }