From 0bb115c32da0057315457cb0002e7a03332bfef1 Mon Sep 17 00:00:00 2001 From: bondiano Date: Mon, 25 Mar 2024 00:53:38 +0500 Subject: [PATCH] feat: support setMyCommands --- src/telega.gleam | 54 ++++++---- src/telega/api.gleam | 229 +++++++++++++++++++++++++++++++++++-------- 2 files changed, 223 insertions(+), 60 deletions(-) diff --git a/src/telega.gleam b/src/telega.gleam index 21b4453..3783e25 100644 --- a/src/telega.gleam +++ b/src/telega.gleam @@ -3,7 +3,7 @@ import gleam/option.{type Option, None, Some} import gleam/list import gleam/string import telega/message.{type Message, CommandMessage, TextMessage} -import telega/api +import telega/api.{type BotCommands, type BotCommandsOptions} import telega/log const telegram_url = "https://api.telegram.org/bot" @@ -55,7 +55,7 @@ pub type Command { } pub type CommandContext { - CommandContext(message: Message, bot: Bot, command: Command) + CommandContext(ctx: Context, command: Command) } /// Creates a new Bot with the given options. @@ -77,6 +77,19 @@ pub fn new( ) } +/// Check if a path is the webhook path for the bot. +pub fn is_webhook_path(bot: Bot, path: String) -> Bool { + bot.config.webhook_path == path +} + +/// Check if a token is the secret token for the bot. +pub fn is_secret_token_valid(bot: Bot, token: String) -> Bool { + case bot.config.secret_token { + Some(secret) -> secret == token + None -> True + } +} + /// Set the webhook URL using [setWebhook](https://core.telegram.org/bots/api#setwebhook) API. pub fn set_webhook(bot: Bot) -> Result(Bool, String) { let webhook_url = bot.config.server_url <> "/" <> bot.config.webhook_path @@ -93,21 +106,8 @@ pub fn set_webhook(bot: Bot) -> Result(Bool, String) { } } -/// Check if a path is the webhook path for the bot. -pub fn is_webhook_path(bot: Bot, path: String) -> Bool { - bot.config.webhook_path == path -} - -/// Check if a token is the secret token for the bot. -pub fn is_secret_token_valid(bot: Bot, token: String) -> Bool { - case bot.config.secret_token { - Some(secret) -> secret == token - None -> True - } -} - /// Use this method to send text messages. -pub fn reply(ctx: Context, text: String) -> Result(Message, Nil) { +pub fn reply(ctx ctx: Context, text text: String) -> Result(Message, Nil) { let chat_id = ctx.message.raw.chat.id api.send_message( @@ -120,6 +120,22 @@ pub fn reply(ctx: Context, text: String) -> Result(Message, Nil) { |> result.nil_error } +/// Use this method to change the list of the bot's commands. See [commands documentation](https://core.telegram.org/bots/features#commands) for more details about bot commands. Returns True on success. +pub fn set_my_commands( + ctx ctx: Context, + commands commands: BotCommands, + options options: Option(BotCommandsOptions), +) -> Result(Bool, Nil) { + api.set_my_commands( + token: ctx.bot.config.token, + telegram_url: ctx.bot.config.telegram_url, + commands: commands, + options: options, + ) + |> result.map(fn(_) { True }) + |> result.nil_error +} + /// Add a handler to the bot. pub fn add_handler(bot: Bot, handler: Handler) -> Bot { Bot(..bot, handlers: [handler, ..bot.handlers]) @@ -160,8 +176,7 @@ fn do_handle_update(bot: Bot, message: Message, handlers: List(Handler)) -> Nil case message_command.command == command { True -> handle(CommandContext( - bot: bot, - message: message, + ctx: Context(bot: bot, message: message), command: message_command, )) False -> Ok(Nil) @@ -172,8 +187,7 @@ fn do_handle_update(bot: Bot, message: Message, handlers: List(Handler)) -> Nil case list.contains(commands, message_command.command) { True -> handle(CommandContext( - bot: bot, - message: message, + ctx: Context(bot: bot, message: message), command: message_command, )) False -> Ok(Nil) diff --git a/src/telega/api.gleam b/src/telega/api.gleam index 4d92b4f..36d1792 100644 --- a/src/telega/api.gleam +++ b/src/telega/api.gleam @@ -1,5 +1,3 @@ -//// > It's internal module for working with Telegram API, **not** for public use. - import gleam/http/request.{type Request} import gleam/http/response.{type Response} import gleam/http.{Get, Post} @@ -18,6 +16,162 @@ type TelegramApiRequest { TelegramApiGetRequest(url: String, query: Option(List(#(String, String)))) } +pub type BotCommand { + BotCommand( + /// Text of the command; 1-32 characters. Can contain only lowercase English letters, digits and underscores. + command: String, + /// Description of the command; 1-256 characters. + description: String, + ) +} + +pub type BotCommands = + List(BotCommand) + +pub type IntOrString { + Int(Int) + String(String) +} + +pub type BotCommandScope { + /// Represents the default scope of bot commands. Default commands are used if no commands with a narrower scope are specified for the user. + BotCommandDefaultScope + /// Represents the scope of bot commands, covering all private chats. + BotCommandAllPrivateChatsScope + /// Represents the scope of bot commands, covering all group and supergroup chats. + BotCommandScopeAllGroupChats + /// Represents the scope of bot commands, covering all group and supergroup chat administrators. + BotCommandScopeAllChatAdministrators + /// Represents the scope of bot commands, covering a specific chat. + BotCommandScopeChat( + /// Unique identifier for the target chat or username of the target supergroup (in the format @supergroupusername) + chat_id: Int, + ) + /// Represents the scope of bot commands, covering a specific chat. + BotCommandScopeChatString( + /// Unique identifier for the target chat or username of the target supergroup (in the format @supergroupusername) + chat_id: IntOrString, + ) + /// Represents the scope of bot commands, covering all administrators of a specific group or supergroup chat. + BotCommandScopeChatAdministrators( + /// Unique identifier for the target chat or username of the target supergroup (in the format @supergroupusername) + chat_id: IntOrString, + ) + /// Represents the scope of bot commands, covering a specific member of a group or supergroup chat. + BotCommandScopeChatMember( + /// Unique identifier for the target chat or username of the target supergroup (in the format @supergroupusername) + chat_id: IntOrString, + /// Unique identifier of the target user + user_id: Int, + ) +} + +pub type BotCommandsOptions { + BotCommandsOptions( + /// An object, describing scope of users for which the commands are relevant. Defaults to `BotCommandScopeDefault`. + scope: Option(BotCommandScope), + /// A two-letter ISO 639-1 language code. If empty, commands will be applied to all users from the given scope, for whose language there are no dedicated commands + language_code: Option(String), + ) +} + +// TODO: Support all options +/// **Official reference:** https://core.telegram.org/bots/api#setwebhook +pub fn set_webhook( + token token: String, + telegram_url telegram_url: String, + webhook_url webhook_url: String, + secret_token secret_token: Option(String), +) -> Result(Response(String), String) { + let query = [#("url", webhook_url)] + let query = case secret_token { + None -> query + Some(secret_token) -> [#("secret_token", secret_token), ..query] + } + + new_get_request( + token: token, + telegram_url: telegram_url, + path: "setWebhook", + query: Some(query), + ) + |> api_to_request + |> fetch +} + +// TODO: Support all options +/// **Official reference:** https://core.telegram.org/bots/api#sendmessage +pub fn send_message( + token token: String, + telegram_url telegram_url: String, + chat_id chat_id: Int, + text text: String, +) -> Result(Response(String), String) { + new_post_request( + token: token, + telegram_url: telegram_url, + path: "sendMessage", + body: json.object([ + #("chat_id", json.int(chat_id)), + #("text", json.string(text)), + ]) + |> json.to_string, + query: None, + ) + |> api_to_request + |> fetch +} + +/// **Official reference:** https://core.telegram.org/bots/api#setmycommands +pub fn set_my_commands( + token token: String, + telegram_url telegram_url: String, + commands commands: BotCommands, + options options: Option(BotCommandsOptions), +) -> Result(Response(String), String) { + let options = case options { + None -> [] + Some(options) -> { + let scope = case options.scope { + None -> [] + Some(scope) -> [#("scope", bot_command_scope_to_json(scope))] + } + + case options.language_code { + None -> scope + Some(language_code) -> [ + #("language_code", json.string(language_code)), + ..scope + ] + } + } + } + + let body_json = + json.object([ + #( + "commands", + json.array(commands, fn(command: BotCommand) { + json.object([ + #("command", json.string(command.command)), + #("description", json.string(command.description)), + ..options + ]) + }), + ), + ]) + + new_post_request( + token: token, + telegram_url: telegram_url, + path: "setMyCommands", + body: json.to_string(body_json), + query: None, + ) + |> api_to_request + |> fetch +} + fn new_post_request( token token: String, telegram_url telegram_url: String, @@ -84,47 +238,42 @@ fn fetch(api_request: Result(Request(String), String)) { }) } -/// **Official reference:** https://core.telegram.org/bots/api#setwebhook -pub fn set_webhook( - token token: String, - webhook_url webhook_url: String, - telegram_url telegram_url: String, - secret_token secret_token: Option(String), -) -> Result(Response(String), String) { - let query = [#("url", webhook_url)] - let query = case secret_token { - None -> query - Some(secret_token) -> [#("secret_token", secret_token), ..query] +fn string_or_int_to_json(value: IntOrString) { + case value { + Int(value) -> json.int(value) + String(value) -> json.string(value) } - - new_get_request( - token: token, - telegram_url: telegram_url, - path: "setWebhook", - query: Some(query), - ) - |> api_to_request - |> fetch } -/// **Official reference:** https://core.telegram.org/bots/api#sendmessage -pub fn send_message( - chat_id chat_id: Int, - text text: String, - token token: String, - telegram_url telegram_url: String, -) -> Result(Response(String), String) { - new_post_request( - token: token, - telegram_url: telegram_url, - path: "sendMessage", - body: json.object([ +fn bot_command_scope_to_json(scope: BotCommandScope) { + case scope { + BotCommandDefaultScope -> json.object([#("type", json.string("default"))]) + BotCommandAllPrivateChatsScope -> + json.object([#("type", json.string("all_private_chats"))]) + BotCommandScopeAllGroupChats -> + json.object([#("type", json.string("all_group_chats"))]) + BotCommandScopeAllChatAdministrators -> + json.object([#("type", json.string("all_chat_administrators"))]) + BotCommandScopeChat(chat_id: chat_id) -> + json.object([ + #("type", json.string("chat")), #("chat_id", json.int(chat_id)), - #("text", json.string(text)), ]) - |> json.to_string, - query: None, - ) - |> api_to_request - |> fetch + BotCommandScopeChatString(chat_id: chat_id) -> + json.object([ + #("type", json.string("chat")), + #("chat_id", string_or_int_to_json(chat_id)), + ]) + BotCommandScopeChatAdministrators(chat_id: chat_id) -> + json.object([ + #("type", json.string("chat_administrators")), + #("chat_id", string_or_int_to_json(chat_id)), + ]) + BotCommandScopeChatMember(chat_id: chat_id, user_id: user_id) -> + json.object([ + #("type", json.string("chat_member")), + #("chat_id", string_or_int_to_json(chat_id)), + #("user_id", json.int(user_id)), + ]) + } }