From 8116059ba366ae0f6e6002acbaf63461d2c596e7 Mon Sep 17 00:00:00 2001 From: Maybe Waffle Date: Sun, 13 Mar 2022 17:30:13 +0400 Subject: [PATCH 01/10] Document `BotCommand` methods --- src/utils/command.rs | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/src/utils/command.rs b/src/utils/command.rs index 6fcf06a2..8faf06d8 100644 --- a/src/utils/command.rs +++ b/src/utils/command.rs @@ -206,14 +206,31 @@ pub use teloxide_macros::BotCommand; /// [`FromStr`]: https://doc.rust-lang.org/std/str/trait.FromStr.html /// [`BotCommand`]: crate::utils::command::BotCommand pub trait BotCommand: Sized { - fn descriptions() -> String; - fn parse(s: &str, bot_name: N) -> Result + /// Parses a command. + /// + /// `bot_username` is required to parse commands like + /// `/cmd@username_of_the_bot`. + fn parse(s: &str, bot_username: N) -> Result where N: Into; + + /// Returns descriptions of the commands suitable to be shown to the user + /// (for example when `/help` command is used). + fn descriptions() -> String; + + /// Returns a vector of [`types::BotCommand`] that can be used with + /// [`set_my_commands`]. + /// + /// [`types::BotCommand`]: crate::types::BotCommand + /// [`set_my_commands`]: crate::requests::Requester::set_my_commands + fn bot_commands() -> Vec; + + /// Returns `PhantomData` that is used as a param of [`commands_repl`] + /// + /// [`commands_repl`]: (crate::repls2::commands_repl) fn ty() -> PhantomData { PhantomData } - fn bot_commands() -> Vec; } pub type PrefixedBotCommand = String; From b3b8073a12b2e9a7ab249c3d519b32297b94063a Mon Sep 17 00:00:00 2001 From: Maybe Waffle Date: Sun, 13 Mar 2022 19:37:11 +0400 Subject: [PATCH 02/10] Rename `BotCommand` -> `BotCommands` (the trait) This removes the ambiguity with `types::BotCommand` and also the new name just makes more sense, since this trait is usually implemented for enums that can be many different commands and not a single one. Note: this requires changes in the `teloxide-macro` crate. --- Cargo.toml | 3 ++- examples/admin.rs | 4 ++-- examples/buttons.rs | 6 ++--- examples/db_remember.rs | 4 ++-- examples/dispatching2_features.rs | 6 ++--- examples/simple_commands.rs | 4 ++-- src/dispatching/dispatcher_handler_rx_ext.rs | 6 ++--- src/dispatching/repls/commands_repl.rs | 6 ++--- src/dispatching2/handler_ext.rs | 6 ++--- src/dispatching2/repls/commands_repl.rs | 6 ++--- src/utils/command.rs | 11 +++++---- tests/command.rs | 24 ++++++++++---------- 12 files changed, 44 insertions(+), 42 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 8928132d..7fb07c2a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -59,7 +59,8 @@ full = [ [dependencies] teloxide-core = { version = "0.4", default-features = false } -teloxide-macros = { version = "0.5.1", optional = true } +#teloxide-macros = { version = "0.5.1", optional = true } +teloxide-macros = { git = "https://github.com/teloxide/teloxide-macros", rev = "7e000b9", optional = true } serde_json = "1.0" serde = { version = "1.0", features = ["derive"] } diff --git a/examples/admin.rs b/examples/admin.rs index bf257aa9..76b40f91 100644 --- a/examples/admin.rs +++ b/examples/admin.rs @@ -1,7 +1,7 @@ use std::{error::Error, str::FromStr}; use chrono::Duration; -use teloxide::{prelude2::*, types::ChatPermissions, utils::command::BotCommand}; +use teloxide::{prelude2::*, types::ChatPermissions, utils::command::BotCommands}; // Derive BotCommand to parse text with a command into this enumeration. // @@ -12,7 +12,7 @@ use teloxide::{prelude2::*, types::ChatPermissions, utils::command::BotCommand}; // your commands in this format: // %GENERAL-DESCRIPTION% // %PREFIX%%COMMAND% - %DESCRIPTION% -#[derive(BotCommand, Clone)] +#[derive(BotCommands, Clone)] #[command( rename = "lowercase", description = "Use commands in format /%command% %num% %unit%", diff --git a/examples/buttons.rs b/examples/buttons.rs index cb98eeec..c0dd518b 100644 --- a/examples/buttons.rs +++ b/examples/buttons.rs @@ -6,10 +6,10 @@ use teloxide::{ InlineKeyboardButton, InlineKeyboardMarkup, InlineQueryResultArticle, InputMessageContent, InputMessageContentText, }, - utils::command::BotCommand, + utils::command::BotCommands, }; -#[derive(BotCommand)] +#[derive(BotCommands)] #[command(rename = "lowercase", description = "These commands are supported:")] enum Command { #[command(description = "Display this text")] @@ -47,7 +47,7 @@ async fn message_handler( bot: AutoSend, ) -> Result<(), Box> { if let Some(text) = m.text() { - match BotCommand::parse(text, "buttons") { + match BotCommands::parse(text, "buttons") { Ok(Command::Help) => { // Just send the description of all commands. bot.send_message(m.chat.id, Command::descriptions()).await?; diff --git a/examples/db_remember.rs b/examples/db_remember.rs index 8a52318e..1e1fcbdd 100644 --- a/examples/db_remember.rs +++ b/examples/db_remember.rs @@ -9,7 +9,7 @@ use teloxide::{ macros::DialogueState, prelude2::*, types::Me, - utils::command::BotCommand, + utils::command::BotCommands, }; type MyDialogue = Dialogue>; @@ -32,7 +32,7 @@ impl Default for State { } } -#[derive(BotCommand)] +#[derive(BotCommands)] #[command(rename = "lowercase", description = "These commands are supported:")] pub enum Command { #[command(description = "get your number.")] diff --git a/examples/dispatching2_features.rs b/examples/dispatching2_features.rs index 6f5d1e07..ce894272 100644 --- a/examples/dispatching2_features.rs +++ b/examples/dispatching2_features.rs @@ -8,7 +8,7 @@ use rand::Rng; use teloxide::{ prelude2::*, types::{Dice, Update}, - utils::command::BotCommand, + utils::command::BotCommands, }; #[tokio::main] @@ -101,7 +101,7 @@ struct ConfigParameters { maintainer_username: Option, } -#[derive(BotCommand, Clone)] +#[derive(BotCommands, Clone)] #[command(rename = "lowercase", description = "Simple commands")] enum SimpleCommand { #[command(description = "shows this message.")] @@ -112,7 +112,7 @@ enum SimpleCommand { MyId, } -#[derive(BotCommand, Clone)] +#[derive(BotCommands, Clone)] #[command(rename = "lowercase", description = "Maintainer commands")] enum MaintainerCommands { #[command(parse_with = "split", description = "generate a number within range")] diff --git a/examples/simple_commands.rs b/examples/simple_commands.rs index 47b4337a..4c13102b 100644 --- a/examples/simple_commands.rs +++ b/examples/simple_commands.rs @@ -1,8 +1,8 @@ -use teloxide::{prelude2::*, utils::command::BotCommand}; +use teloxide::{prelude2::*, utils::command::BotCommands}; use std::error::Error; -#[derive(BotCommand, Clone)] +#[derive(BotCommands, Clone)] #[command(rename = "lowercase", description = "These commands are supported:")] enum Command { #[command(description = "display this text.")] diff --git a/src/dispatching/dispatcher_handler_rx_ext.rs b/src/dispatching/dispatcher_handler_rx_ext.rs index 9270826f..c7ad4215 100644 --- a/src/dispatching/dispatcher_handler_rx_ext.rs +++ b/src/dispatching/dispatcher_handler_rx_ext.rs @@ -1,4 +1,4 @@ -use crate::{dispatching::UpdateWithCx, utils::command::BotCommand}; +use crate::{dispatching::UpdateWithCx, utils::command::BotCommands}; use futures::{stream::BoxStream, Stream, StreamExt}; use teloxide_core::types::Message; @@ -21,7 +21,7 @@ pub trait DispatcherHandlerRxExt { fn commands(self, bot_name: N) -> BoxStream<'static, (UpdateWithCx, C)> where Self: Stream>, - C: BotCommand, + C: BotCommands, N: Into + Send, R: Send + 'static; } @@ -45,7 +45,7 @@ where fn commands(self, bot_name: N) -> BoxStream<'static, (UpdateWithCx, C)> where Self: Stream>, - C: BotCommand, + C: BotCommands, N: Into + Send, R: Send + 'static, { diff --git a/src/dispatching/repls/commands_repl.rs b/src/dispatching/repls/commands_repl.rs index d87cb83f..4ac20dfb 100644 --- a/src/dispatching/repls/commands_repl.rs +++ b/src/dispatching/repls/commands_repl.rs @@ -4,7 +4,7 @@ use crate::{ DispatcherHandlerRxExt, UpdateWithCx, }, error_handlers::{LoggingErrorHandler, OnError}, - utils::command::BotCommand, + utils::command::BotCommands, }; use futures::StreamExt; use std::{fmt::Debug, future::Future, sync::Arc}; @@ -25,7 +25,7 @@ use tokio_stream::wrappers::UnboundedReceiverStream; #[cfg(feature = "ctrlc_handler")] pub async fn commands_repl(requester: R, bot_name: N, handler: H) where - Cmd: BotCommand + Send + 'static, + Cmd: BotCommands + Send + 'static, H: Fn(UpdateWithCx, Cmd) -> Fut + Send + Sync + 'static, Fut: Future> + Send + 'static, Result<(), HandlerE>: OnError, @@ -64,7 +64,7 @@ pub async fn commands_repl_with_listener<'a, R, Cmd, H, Fut, L, ListenerE, Handl handler: H, listener: L, ) where - Cmd: BotCommand + Send + 'static, + Cmd: BotCommands + Send + 'static, H: Fn(UpdateWithCx, Cmd) -> Fut + Send + Sync + 'static, Fut: Future> + Send + 'static, L: UpdateListener + Send + 'a, diff --git a/src/dispatching2/handler_ext.rs b/src/dispatching2/handler_ext.rs index 6fcafca1..a3774aa5 100644 --- a/src/dispatching2/handler_ext.rs +++ b/src/dispatching2/handler_ext.rs @@ -6,7 +6,7 @@ use crate::{ HandlerFactory, }, types::{Me, Message}, - utils::command::BotCommand, + utils::command::BotCommands, }; use dptree::{di::DependencyMap, Handler}; @@ -23,7 +23,7 @@ pub trait HandlerExt { #[must_use] fn filter_command(self) -> Self where - C: BotCommand + Send + Sync + 'static; + C: BotCommands + Send + Sync + 'static; /// Passes [`Dialogue`] and `D` as handler dependencies. /// @@ -62,7 +62,7 @@ where { fn filter_command(self) -> Self where - C: BotCommand + Send + Sync + 'static, + C: BotCommands + Send + Sync + 'static, { self.chain(dptree::filter_map(move |message: Message, me: Me| { let bot_name = me.user.username.expect("Bots must have a username"); diff --git a/src/dispatching2/repls/commands_repl.rs b/src/dispatching2/repls/commands_repl.rs index 488d8313..13d5c8de 100644 --- a/src/dispatching2/repls/commands_repl.rs +++ b/src/dispatching2/repls/commands_repl.rs @@ -3,7 +3,7 @@ use crate::{ dispatching2::{HandlerExt, UpdateFilterExt}, error_handlers::LoggingErrorHandler, types::Update, - utils::command::BotCommand, + utils::command::BotCommands, }; use dptree::di::{DependencyMap, Injectable}; use std::{fmt::Debug, marker::PhantomData}; @@ -27,7 +27,7 @@ use teloxide_core::requests::Requester; #[cfg(feature = "ctrlc_handler")] pub async fn commands_repl<'a, R, Cmd, H, E, Args>(bot: R, handler: H, cmd: PhantomData) where - Cmd: BotCommand + Send + Sync + 'static, + Cmd: BotCommands + Send + Sync + 'static, H: Injectable, Args> + Send + Sync + 'static, R: Requester + Clone + Send + Sync + 'static, ::GetUpdates: Send, @@ -67,7 +67,7 @@ pub async fn commands_repl_with_listener<'a, R, Cmd, H, L, ListenerE, E, Args>( listener: L, _cmd: PhantomData, ) where - Cmd: BotCommand + Send + Sync + 'static, + Cmd: BotCommands + Send + Sync + 'static, H: Injectable, Args> + Send + Sync + 'static, L: UpdateListener + Send + 'a, ListenerE: Debug + Send + 'a, diff --git a/src/utils/command.rs b/src/utils/command.rs index 8faf06d8..6a8f337c 100644 --- a/src/utils/command.rs +++ b/src/utils/command.rs @@ -52,8 +52,9 @@ use std::{ }; use std::marker::PhantomData; +use teloxide_core::types::BotCommand; #[cfg(feature = "macros")] -pub use teloxide_macros::BotCommand; +pub use teloxide_macros::BotCommands; /// An enumeration of bot's commands. /// @@ -205,7 +206,7 @@ pub use teloxide_macros::BotCommand; /// /// [`FromStr`]: https://doc.rust-lang.org/std/str/trait.FromStr.html /// [`BotCommand`]: crate::utils::command::BotCommand -pub trait BotCommand: Sized { +pub trait BotCommands: Sized { /// Parses a command. /// /// `bot_username` is required to parse commands like @@ -218,12 +219,12 @@ pub trait BotCommand: Sized { /// (for example when `/help` command is used). fn descriptions() -> String; - /// Returns a vector of [`types::BotCommand`] that can be used with + /// Returns a vector of [`BotCommand`] that can be used with /// [`set_my_commands`]. /// - /// [`types::BotCommand`]: crate::types::BotCommand + /// [`BotCommand`]: crate::types::BotCommand /// [`set_my_commands`]: crate::requests::Requester::set_my_commands - fn bot_commands() -> Vec; + fn bot_commands() -> Vec; /// Returns `PhantomData` that is used as a param of [`commands_repl`] /// diff --git a/tests/command.rs b/tests/command.rs index ebdbab6f..a7c3183b 100644 --- a/tests/command.rs +++ b/tests/command.rs @@ -2,7 +2,7 @@ #![allow(clippy::nonstandard_macro_braces)] #[cfg(feature = "macros")] -use teloxide::utils::command::{BotCommand, ParseError}; +use teloxide::utils::command::{BotCommands, ParseError}; // We put tests here because macro expand in unit tests in module // teloxide::utils::command was a failure @@ -10,7 +10,7 @@ use teloxide::utils::command::{BotCommand, ParseError}; #[test] #[cfg(feature = "macros")] fn parse_command_with_args() { - #[derive(BotCommand, Debug, PartialEq)] + #[derive(BotCommands, Debug, PartialEq)] #[command(rename = "lowercase")] enum DefaultCommands { Start(String), @@ -26,7 +26,7 @@ fn parse_command_with_args() { #[test] #[cfg(feature = "macros")] fn parse_command_with_non_string_arg() { - #[derive(BotCommand, Debug, PartialEq)] + #[derive(BotCommands, Debug, PartialEq)] #[command(rename = "lowercase")] enum DefaultCommands { Start(i32), @@ -42,7 +42,7 @@ fn parse_command_with_non_string_arg() { #[test] #[cfg(feature = "macros")] fn attribute_prefix() { - #[derive(BotCommand, Debug, PartialEq)] + #[derive(BotCommands, Debug, PartialEq)] #[command(rename = "lowercase")] enum DefaultCommands { #[command(prefix = "!")] @@ -59,7 +59,7 @@ fn attribute_prefix() { #[test] #[cfg(feature = "macros")] fn many_attributes() { - #[derive(BotCommand, Debug, PartialEq)] + #[derive(BotCommands, Debug, PartialEq)] #[command(rename = "lowercase")] enum DefaultCommands { #[command(prefix = "!", description = "desc")] @@ -74,7 +74,7 @@ fn many_attributes() { #[test] #[cfg(feature = "macros")] fn global_attributes() { - #[derive(BotCommand, Debug, PartialEq)] + #[derive(BotCommands, Debug, PartialEq)] #[command(prefix = "!", rename = "lowercase", description = "Bot commands")] enum DefaultCommands { #[command(prefix = "/")] @@ -90,7 +90,7 @@ fn global_attributes() { #[test] #[cfg(feature = "macros")] fn parse_command_with_bot_name() { - #[derive(BotCommand, Debug, PartialEq)] + #[derive(BotCommands, Debug, PartialEq)] #[command(rename = "lowercase")] enum DefaultCommands { #[command(prefix = "/")] @@ -107,7 +107,7 @@ fn parse_command_with_bot_name() { #[test] #[cfg(feature = "macros")] fn parse_with_split() { - #[derive(BotCommand, Debug, PartialEq)] + #[derive(BotCommands, Debug, PartialEq)] #[command(rename = "lowercase")] #[command(parse_with = "split")] enum DefaultCommands { @@ -124,7 +124,7 @@ fn parse_with_split() { #[test] #[cfg(feature = "macros")] fn parse_with_split2() { - #[derive(BotCommand, Debug, PartialEq)] + #[derive(BotCommands, Debug, PartialEq)] #[command(rename = "lowercase")] #[command(parse_with = "split", separator = "|")] enum DefaultCommands { @@ -152,7 +152,7 @@ fn parse_custom_parser() { .map_err(|_| ParseError::Custom("First argument must be a integer!".to_owned().into())) } - #[derive(BotCommand, Debug, PartialEq)] + #[derive(BotCommands, Debug, PartialEq)] #[command(rename = "lowercase")] enum DefaultCommands { #[command(parse_with = "custom_parse_function")] @@ -169,7 +169,7 @@ fn parse_custom_parser() { #[test] #[cfg(feature = "macros")] fn parse_named_fields() { - #[derive(BotCommand, Debug, PartialEq)] + #[derive(BotCommands, Debug, PartialEq)] #[command(rename = "lowercase")] #[command(parse_with = "split")] enum DefaultCommands { @@ -186,7 +186,7 @@ fn parse_named_fields() { #[test] #[cfg(feature = "macros")] fn descriptions_off() { - #[derive(BotCommand, Debug, PartialEq)] + #[derive(BotCommands, Debug, PartialEq)] #[command(rename = "lowercase")] enum DefaultCommands { #[command(description = "off")] From 86cc3d782fb5e4dff635d335ed67faedd4f124df Mon Sep 17 00:00:00 2001 From: Maybe Waffle Date: Mon, 14 Mar 2022 18:11:57 +0400 Subject: [PATCH 03/10] Add `CommandDescription[s]` Add two new types - `CommandDescription` and `CommandDescriptions`, make `BotCommands` return the latter. --- examples/admin.rs | 2 +- examples/buttons.rs | 2 +- examples/dispatching2_features.rs | 10 +- examples/simple_commands.rs | 4 +- src/utils/command.rs | 162 ++++++++++++++++++++++++++---- tests/command.rs | 6 +- 6 files changed, 156 insertions(+), 30 deletions(-) diff --git a/examples/admin.rs b/examples/admin.rs index 76b40f91..4295b0a8 100644 --- a/examples/admin.rs +++ b/examples/admin.rs @@ -132,7 +132,7 @@ async fn action( ) -> Result<(), Box> { match command { Command::Help => { - bot.send_message(msg.chat.id, Command::descriptions()).await?; + bot.send_message(msg.chat.id, Command::descriptions().to_string()).await?; } Command::Kick => kick_user(bot, msg).await?, Command::Ban { time, unit } => ban_user(bot, msg, calc_restrict_time(time, unit)).await?, diff --git a/examples/buttons.rs b/examples/buttons.rs index c0dd518b..598c745e 100644 --- a/examples/buttons.rs +++ b/examples/buttons.rs @@ -50,7 +50,7 @@ async fn message_handler( match BotCommands::parse(text, "buttons") { Ok(Command::Help) => { // Just send the description of all commands. - bot.send_message(m.chat.id, Command::descriptions()).await?; + bot.send_message(m.chat.id, Command::descriptions().to_string()).await?; } Ok(Command::Start) => { // Create a list of buttons and send them. diff --git a/examples/dispatching2_features.rs b/examples/dispatching2_features.rs index ce894272..d8e141d9 100644 --- a/examples/dispatching2_features.rs +++ b/examples/dispatching2_features.rs @@ -128,9 +128,15 @@ async fn simple_commands_handler( let text = match cmd { SimpleCommand::Help => { if msg.from().unwrap().id == cfg.bot_maintainer { - format!("{}\n{}", SimpleCommand::descriptions(), MaintainerCommands::descriptions()) + format!( + "{}\n\n{}", + SimpleCommand::descriptions(), + MaintainerCommands::descriptions() + ) + } else if msg.chat.is_group() || msg.chat.is_supergroup() { + SimpleCommand::descriptions().username("USERNAME_BOT").to_string() } else { - SimpleCommand::descriptions() + SimpleCommand::descriptions().to_string() } } SimpleCommand::Maintainer => { diff --git a/examples/simple_commands.rs b/examples/simple_commands.rs index 4c13102b..2f144a8b 100644 --- a/examples/simple_commands.rs +++ b/examples/simple_commands.rs @@ -19,7 +19,9 @@ async fn answer( command: Command, ) -> Result<(), Box> { match command { - Command::Help => bot.send_message(message.chat.id, Command::descriptions()).await?, + Command::Help => { + bot.send_message(message.chat.id, Command::descriptions().to_string()).await? + } Command::Username(username) => { bot.send_message(message.chat.id, format!("Your username is @{}.", username)).await? } diff --git a/src/utils/command.rs b/src/utils/command.rs index 6a8f337c..e509f029 100644 --- a/src/utils/command.rs +++ b/src/utils/command.rs @@ -46,13 +46,18 @@ //! //! [examples/admin_bot]: https://github.com/teloxide/teloxide/blob/master/examples/admin_bot/ +use core::fmt; use std::{ + borrow::Cow, error::Error, - fmt::{Display, Formatter}, + fmt::{Display, Formatter, Write}, }; use std::marker::PhantomData; -use teloxide_core::types::BotCommand; +use teloxide_core::{ + requests::{Request, Requester}, + types::{BotCommand, Me}, +}; #[cfg(feature = "macros")] pub use teloxide_macros::BotCommands; @@ -217,7 +222,7 @@ pub trait BotCommands: Sized { /// Returns descriptions of the commands suitable to be shown to the user /// (for example when `/help` command is used). - fn descriptions() -> String; + fn descriptions() -> CommandDescriptions<'static>; /// Returns a vector of [`BotCommand`] that can be used with /// [`set_my_commands`]. @@ -265,29 +270,80 @@ pub enum ParseError { Custom(Box), } -impl Display for ParseError { - fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { - match self { - ParseError::TooFewArguments { expected, found, message } => write!( - f, - "Too few arguments (expected {}, found {}, message = '{}')", - expected, found, message - ), - ParseError::TooManyArguments { expected, found, message } => write!( - f, - "Too many arguments (expected {}, found {}, message = '{}')", - expected, found, message - ), - ParseError::IncorrectFormat(e) => write!(f, "Incorrect format of command args: {}", e), - ParseError::UnknownCommand(e) => write!(f, "Unknown command: {}", e), - ParseError::WrongBotName(n) => write!(f, "Wrong bot name: {}", n), - ParseError::Custom(e) => write!(f, "{}", e), +/// Command descriptions that can be shown to the user (e.g. as a part of +/// `/help` message) +/// +/// Most of the time you don't need to create this struct yourself as it's +/// returned from [`BotCommands::descriptions`]. +#[derive(Debug, Clone)] +pub struct CommandDescriptions<'a> { + global_description: Option<&'a str>, + descriptions: &'a [CommandDescription<'a>], + bot_username: Option>, +} + +/// Description of a particular command, used in [`CommandDescriptions`]. +#[derive(Debug, Clone)] +pub struct CommandDescription<'a> { + /// Prefix of the command, usually `/`. + pub prefix: &'a str, + /// The command itself, e.g. `start`. + pub command: &'a str, + /// Human-readable description of the command. + pub description: &'a str, +} + +impl<'a> CommandDescriptions<'a> { + /// Creates new [`CommandDescriptions`] from a list of command descriptions. + pub fn new(descriptions: &'a [CommandDescription<'a>]) -> Self { + Self { global_description: None, descriptions, bot_username: None } + } + + /// Sets the global description of these commands. + pub fn global_description(self, global_description: &'a str) -> Self { + Self { global_description: Some(global_description), ..self } + } + + /// Sets the username of the bot. + /// + /// After this method is called, returned instance of + /// [`CommandDescriptions`] will append `@bot_username` to all commands. + /// This is useful in groups, to disambiguate commands for different bots. + /// + /// ## Examples + /// + /// ``` + /// let descriptions = CommandDescriptions::new(&[ + /// CommandDescription { prefix: "/", command: "start", description: "start this bot" }, + /// CommandDescription { prefix: "/", command: "help", description: "show this message" }, + /// ]); + /// + /// assert_eq!(descriptions.to_string(), "/start — start this bot\n/help — show this message"); + /// assert_eq!( + /// descriptions.username("username_of_the_bot").to_string(), + /// "/start@username_of_the_bot — start this bot\n/help@username_of_the_bot — show this \ + /// message" + /// ); + /// ``` + pub fn username(self, bot_username: &'a str) -> Self { + Self { bot_username: Some(Cow::Borrowed(bot_username)), ..self } + } + + /// Sets the username of the bot. + /// + /// This is the same as [`username`], but uses `get_me` method of the bot to + /// get the username. + /// + /// [`username`]: self::CommandDescriptions::username + pub async fn username_from_bot(self, bot: &impl Requester) -> CommandDescriptions<'a> { + let Me { user, .. } = bot.get_me().send().await.expect("get_me failed"); + Self { + bot_username: Some(Cow::Owned(user.username.expect("Bots must have usernames"))), + ..self } } } -impl std::error::Error for ParseError {} - /// Parses a string into a command with args. /// /// This function is just a shortcut for calling [`parse_command_with_prefix`] @@ -364,6 +420,68 @@ where Some((command, words.collect())) } +impl Display for ParseError { + fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { + match self { + ParseError::TooFewArguments { expected, found, message } => write!( + f, + "Too few arguments (expected {}, found {}, message = '{}')", + expected, found, message + ), + ParseError::TooManyArguments { expected, found, message } => write!( + f, + "Too many arguments (expected {}, found {}, message = '{}')", + expected, found, message + ), + ParseError::IncorrectFormat(e) => write!(f, "Incorrect format of command args: {}", e), + ParseError::UnknownCommand(e) => write!(f, "Unknown command: {}", e), + ParseError::WrongBotName(n) => write!(f, "Wrong bot name: {}", n), + ParseError::Custom(e) => write!(f, "{}", e), + } + } +} + +impl std::error::Error for ParseError {} + +impl Display for CommandDescriptions<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if let Some(global_description) = self.global_description { + f.write_str(global_description)?; + f.write_str("\n\n")?; + } + + let mut write = |&CommandDescription { prefix, command, description }, nls| { + if nls { + f.write_char('\n')?; + } + + f.write_str(prefix)?; + f.write_str(command)?; + + if let Some(username) = self.bot_username.as_deref() { + f.write_char('@')?; + f.write_str(username)?; + } + + if !description.is_empty() { + f.write_str(" — ")?; + f.write_str(description)?; + } + + fmt::Result::Ok(()) + }; + + if let Some(descr) = self.descriptions.first() { + write(descr, false)?; + for descr in &self.descriptions[1..] { + write(descr, true)?; + } + } + + Ok(()) + } +} + // The rest of tests are integrational due to problems with macro expansion in // unit tests. #[cfg(test)] diff --git a/tests/command.rs b/tests/command.rs index a7c3183b..0176c749 100644 --- a/tests/command.rs +++ b/tests/command.rs @@ -68,7 +68,7 @@ fn many_attributes() { } assert_eq!(DefaultCommands::Start, DefaultCommands::parse("!start", "").unwrap()); - assert_eq!(DefaultCommands::descriptions(), "!start - desc\n/help\n"); + assert_eq!(DefaultCommands::descriptions().to_string(), "!start — desc\n/help"); } #[test] @@ -84,7 +84,7 @@ fn global_attributes() { assert_eq!(DefaultCommands::Start, DefaultCommands::parse("/start", "MyNameBot").unwrap()); assert_eq!(DefaultCommands::Help, DefaultCommands::parse("!help", "MyNameBot").unwrap()); - assert_eq!(DefaultCommands::descriptions(), "Bot commands\n/start\n!help\n"); + assert_eq!(DefaultCommands::descriptions().to_string(), "Bot commands\n\n/start\n!help"); } #[test] @@ -194,7 +194,7 @@ fn descriptions_off() { Help, } - assert_eq!(DefaultCommands::descriptions(), "/help\n".to_owned()); + assert_eq!(DefaultCommands::descriptions().to_string(), "/help".to_owned()); } #[test] From 484d1ccd83c357419e3c8298f2c538e8521bca5a Mon Sep 17 00:00:00 2001 From: Maybe Waffle Date: Mon, 14 Mar 2022 18:21:57 +0400 Subject: [PATCH 04/10] Fixup documentation --- README.md | 6 +++--- src/utils/command.rs | 37 +++++++++++++++++++++---------------- src/utils/shutdown_token.rs | 4 ++++ 3 files changed, 28 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 7acff473..81d3fc3a 100644 --- a/README.md +++ b/README.md @@ -125,11 +125,11 @@ Commands are strongly typed and defined declaratively, similar to how we define ([Full](examples/simple_commands.rs)) ```rust,no_run -use teloxide::{prelude2::*, utils::command::BotCommand}; +use teloxide::{prelude2::*, utils::command::BotCommands}; use std::error::Error; -#[derive(BotCommand, Clone)] +#[derive(BotCommands, Clone)] #[command(rename = "lowercase", description = "These commands are supported:")] enum Command { #[command(description = "display this text.")] @@ -146,7 +146,7 @@ async fn answer( command: Command, ) -> Result<(), Box> { match command { - Command::Help => bot.send_message(message.chat.id, Command::descriptions()).await?, + Command::Help => bot.send_message(message.chat.id, Command::descriptions().to_string()).await?, Command::Username(username) => { bot.send_message(message.chat.id, format!("Your username is @{}.", username)).await? } diff --git a/src/utils/command.rs b/src/utils/command.rs index e509f029..c2d962fc 100644 --- a/src/utils/command.rs +++ b/src/utils/command.rs @@ -1,17 +1,18 @@ //! Command parsers. //! -//! You can either create an `enum` with derived [`BotCommand`], containing +//! You can either create an `enum` with derived [`BotCommands`], containing //! commands of your bot, or use functions, which split input text into a string //! command with its arguments. //! -//! # Using BotCommand +//! # Using BotCommands +//! //! ``` //! # #[cfg(feature = "macros")] { -//! use teloxide::utils::command::BotCommand; +//! use teloxide::utils::command::BotCommands; //! //! type UnitOfTime = u8; //! -//! #[derive(BotCommand, PartialEq, Debug)] +//! #[derive(BotCommands, PartialEq, Debug)] //! #[command(rename = "lowercase", parse_with = "split")] //! enum AdminCommand { //! Mute(UnitOfTime, char), @@ -24,6 +25,7 @@ //! ``` //! //! # Using parse_command +//! //! ``` //! use teloxide::utils::command::parse_command; //! @@ -33,6 +35,7 @@ //! ``` //! //! # Using parse_command_with_prefix +//! //! ``` //! use teloxide::utils::command::parse_command_with_prefix; //! @@ -66,11 +69,11 @@ pub use teloxide_macros::BotCommands; /// # Example /// ``` /// # #[cfg(feature = "macros")] { -/// use teloxide::utils::command::BotCommand; +/// use teloxide::utils::command::BotCommands; /// /// type UnitOfTime = u8; /// -/// #[derive(BotCommand, PartialEq, Debug)] +/// #[derive(BotCommands, PartialEq, Debug)] /// #[command(rename = "lowercase", parse_with = "split")] /// enum AdminCommand { /// Mute(UnitOfTime, char), @@ -104,9 +107,9 @@ pub use teloxide_macros::BotCommands; /// ## Example /// ``` /// # #[cfg(feature = "macros")] { -/// use teloxide::utils::command::BotCommand; +/// use teloxide::utils::command::BotCommands; /// -/// #[derive(BotCommand, PartialEq, Debug)] +/// #[derive(BotCommands, PartialEq, Debug)] /// #[command(rename = "lowercase")] /// enum Command { /// Text(String), @@ -124,9 +127,9 @@ pub use teloxide_macros::BotCommands; /// ## Example /// ``` /// # #[cfg(feature = "macros")] { -/// use teloxide::utils::command::BotCommand; +/// use teloxide::utils::command::BotCommands; /// -/// #[derive(BotCommand, PartialEq, Debug)] +/// #[derive(BotCommands, PartialEq, Debug)] /// #[command(rename = "lowercase", parse_with = "split")] /// enum Command { /// Nums(u8, u16, i32), @@ -144,9 +147,9 @@ pub use teloxide_macros::BotCommands; /// ## Example /// ``` /// # #[cfg(feature = "macros")] { -/// use teloxide::utils::command::BotCommand; +/// use teloxide::utils::command::BotCommands; /// -/// #[derive(BotCommand, PartialEq, Debug)] +/// #[derive(BotCommands, PartialEq, Debug)] /// #[command(rename = "lowercase", parse_with = "split", separator = "|")] /// enum Command { /// Nums(u8, u16, i32), @@ -177,7 +180,7 @@ pub use teloxide_macros::BotCommands; /// ## Example /// ``` /// # #[cfg(feature = "macros")] { -/// use teloxide::utils::command::{BotCommand, ParseError}; +/// use teloxide::utils::command::{BotCommands, ParseError}; /// /// fn accept_two_digits(input: String) -> Result<(u8,), ParseError> { /// match input.len() { @@ -189,7 +192,7 @@ pub use teloxide_macros::BotCommands; /// } /// } /// -/// #[derive(BotCommand, PartialEq, Debug)] +/// #[derive(BotCommands, PartialEq, Debug)] /// #[command(rename = "lowercase")] /// enum Command { /// #[command(parse_with = "accept_two_digits")] @@ -242,9 +245,9 @@ pub trait BotCommands: Sized { pub type PrefixedBotCommand = String; pub type BotName = String; -/// Errors returned from [`BotCommand::parse`]. +/// Errors returned from [`BotCommands::parse`]. /// -/// [`BotCommand::parse`]: crate::utils::command::BotCommand::parse +/// [`BotCommands::parse`]: BotCommands::parse #[derive(Debug)] pub enum ParseError { TooFewArguments { @@ -313,6 +316,8 @@ impl<'a> CommandDescriptions<'a> { /// ## Examples /// /// ``` + /// use teloxide::utils::command::{CommandDescription, CommandDescriptions}; + /// /// let descriptions = CommandDescriptions::new(&[ /// CommandDescription { prefix: "/", command: "start", description: "start this bot" }, /// CommandDescription { prefix: "/", command: "help", description: "show this message" }, diff --git a/src/utils/shutdown_token.rs b/src/utils/shutdown_token.rs index 3c11c812..2447f6fd 100644 --- a/src/utils/shutdown_token.rs +++ b/src/utils/shutdown_token.rs @@ -13,6 +13,8 @@ use tokio::sync::Notify; use crate::dispatching::update_listeners::UpdateListener; /// A token which used to shutdown [`Dispatcher`]. +/// +/// [`Dispatcher`]: crate::dispatching::Dispatcher #[derive(Clone)] pub struct ShutdownToken { dispatcher_state: Arc, @@ -21,6 +23,8 @@ pub struct ShutdownToken { /// This error is returned from [`ShutdownToken::shutdown`] when trying to /// shutdown an idle [`Dispatcher`]. +/// +/// [`Dispatcher`]: crate::dispatching::Dispatcher #[derive(Debug)] pub struct IdleShutdownError; From 8ae32224095d44df493e2ecc7206c136e9b1db3e Mon Sep 17 00:00:00 2001 From: Maybe Waffle Date: Mon, 14 Mar 2022 19:28:20 +0400 Subject: [PATCH 05/10] Disable redis tests outside of CI ...to not annoy people like me who don't have redis open while testing --- .github/workflows/ci.yml | 3 +++ tests/redis.rs | 3 +++ 2 files changed, 6 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 28110882..387c6070 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,6 +6,9 @@ on: name: Continuous integration +env: + RUSTFLAGS: "--cfg CI_REDIS" + jobs: style: runs-on: ubuntu-latest diff --git a/tests/redis.rs b/tests/redis.rs index 2e011596..dff537ec 100644 --- a/tests/redis.rs +++ b/tests/redis.rs @@ -5,6 +5,7 @@ use std::{ use teloxide::dispatching::dialogue::{RedisStorage, RedisStorageError, Serializer, Storage}; #[tokio::test] +#[cfg_attr(not(CI_REDIS), ignore)] async fn test_redis_json() { let storage = RedisStorage::open( "redis://127.0.0.1:7777", @@ -16,6 +17,7 @@ async fn test_redis_json() { } #[tokio::test] +#[cfg_attr(not(CI_REDIS), ignore)] async fn test_redis_bincode() { let storage = RedisStorage::open( "redis://127.0.0.1:7778", @@ -27,6 +29,7 @@ async fn test_redis_bincode() { } #[tokio::test] +#[cfg_attr(not(CI_REDIS), ignore)] async fn test_redis_cbor() { let storage = RedisStorage::open( "redis://127.0.0.1:7779", From 3c97ed83f4eedffd488c79504b75732f42397c61 Mon Sep 17 00:00:00 2001 From: Maybe Waffle Date: Mon, 14 Mar 2022 22:05:15 +0400 Subject: [PATCH 06/10] Cleanup files when testing sqlite storage --- tests/sqlite.rs | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/tests/sqlite.rs b/tests/sqlite.rs index a66c745f..18dec25b 100644 --- a/tests/sqlite.rs +++ b/tests/sqlite.rs @@ -1,36 +1,47 @@ use std::{ fmt::{Debug, Display}, + fs, sync::Arc, }; use teloxide::dispatching::dialogue::{Serializer, SqliteStorage, SqliteStorageError, Storage}; #[tokio::test(flavor = "multi_thread")] async fn test_sqlite_json() { - let storage = - SqliteStorage::open("./test_db1.sqlite", teloxide::dispatching::dialogue::serializer::Json) - .await - .unwrap(); + fs::create_dir("./test_db1").unwrap(); + let storage = SqliteStorage::open( + "./test_db1/test_db1.sqlite", + teloxide::dispatching::dialogue::serializer::Json, + ) + .await + .unwrap(); test_sqlite(storage).await; + fs::remove_dir_all("./test_db1").unwrap(); } #[tokio::test(flavor = "multi_thread")] async fn test_sqlite_bincode() { + fs::create_dir("./test_db2").unwrap(); let storage = SqliteStorage::open( - "./test_db2.sqlite", + "./test_db2/test_db2.sqlite", teloxide::dispatching::dialogue::serializer::Bincode, ) .await .unwrap(); test_sqlite(storage).await; + fs::remove_dir_all("./test_db2").unwrap(); } #[tokio::test(flavor = "multi_thread")] async fn test_sqlite_cbor() { - let storage = - SqliteStorage::open("./test_db3.sqlite", teloxide::dispatching::dialogue::serializer::Cbor) - .await - .unwrap(); + fs::create_dir("./test_db3").unwrap(); + let storage = SqliteStorage::open( + "./test_db3/test_db3.sqlite", + teloxide::dispatching::dialogue::serializer::Cbor, + ) + .await + .unwrap(); test_sqlite(storage).await; + fs::remove_dir_all("./test_db3").unwrap(); } type Dialogue = String; From a5aada3dc7408ed7b7147e7aa7dce8a732a95a98 Mon Sep 17 00:00:00 2001 From: Maybe Waffle Date: Mon, 14 Mar 2022 22:22:00 +0400 Subject: [PATCH 07/10] Update changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d2c68377..969b5f65 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - `UpdateListener::StopToken` is now always `Send` +- Rename `BotCommand` trait to `BotCommands` +- `BotCommands::descriptions` now returns `CommandDescriptions` instead of `String` ## 0.7.2 - 2022-03-23 @@ -26,6 +28,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Log `UpdateKind::Error` in `teloxide::dispatching2::Dispatcher`. - Don't warn about unhandled updates in `repls2` ([issue 557](https://github.com/teloxide/teloxide/issues/557)). +- `parse_command` and `parse_command_with_prefix` now ignores case of the bot username ## 0.7.1 - 2022-03-09 From 91bea5be5e6550f815b7dd4dae8451a4520aed81 Mon Sep 17 00:00:00 2001 From: Maybe Waffle Date: Sun, 27 Mar 2022 13:11:59 +0400 Subject: [PATCH 08/10] Apply suggestions from the review - remove `username_from_bot` - add `username_from_me` --- examples/dispatching2_features.rs | 49 ++++++++++++++++--------------- src/utils/command.rs | 22 +++++--------- 2 files changed, 32 insertions(+), 39 deletions(-) diff --git a/examples/dispatching2_features.rs b/examples/dispatching2_features.rs index d8e141d9..bc79b956 100644 --- a/examples/dispatching2_features.rs +++ b/examples/dispatching2_features.rs @@ -7,7 +7,7 @@ use rand::Rng; // dispatching system, which will be deprecated in the future. use teloxide::{ prelude2::*, - types::{Dice, Update}, + types::{Dice, Me, Update}, utils::command::BotCommands, }; @@ -26,28 +26,6 @@ async fn main() { let handler = Update::filter_message() // You can use branching to define multiple ways in which an update will be handled. If the // first branch fails, an update will be passed to the second branch, and so on. - .branch( - // Filtering allow you to filter updates by some condition. - dptree::filter(|msg: Message| msg.chat.is_group() || msg.chat.is_supergroup()) - // An endpoint is the last update handler. - .endpoint(|msg: Message, bot: AutoSend| async move { - log::info!("Received a message from a group chat."); - bot.send_message(msg.chat.id, "This is a group chat.").await?; - respond(()) - }), - ) - .branch( - // There are some extension filtering functions on `Message`. The following filter will - // filter only messages with dices. - Message::filter_dice().endpoint( - |msg: Message, dice: Dice, bot: AutoSend| async move { - bot.send_message(msg.chat.id, format!("Dice value: {}", dice.value)) - .reply_to_message_id(msg.id) - .await?; - Ok(()) - }, - ), - ) .branch( dptree::entry() // Filter commands: the next handlers will receive a parsed `SimpleCommand`. @@ -74,6 +52,28 @@ async fn main() { } }, ), + ) + .branch( + // Filtering allow you to filter updates by some condition. + dptree::filter(|msg: Message| msg.chat.is_group() || msg.chat.is_supergroup()) + // An endpoint is the last update handler. + .endpoint(|msg: Message, bot: AutoSend| async move { + log::info!("Received a message from a group chat."); + bot.send_message(msg.chat.id, "This is a group chat.").await?; + respond(()) + }), + ) + .branch( + // There are some extension filtering functions on `Message`. The following filter will + // filter only messages with dices. + Message::filter_dice().endpoint( + |msg: Message, dice: Dice, bot: AutoSend| async move { + bot.send_message(msg.chat.id, format!("Dice value: {}", dice.value)) + .reply_to_message_id(msg.id) + .await?; + Ok(()) + }, + ), ); Dispatcher::builder(bot, handler) @@ -124,6 +124,7 @@ async fn simple_commands_handler( bot: AutoSend, cmd: SimpleCommand, cfg: ConfigParameters, + me: Me, ) -> Result<(), teloxide::RequestError> { let text = match cmd { SimpleCommand::Help => { @@ -134,7 +135,7 @@ async fn simple_commands_handler( MaintainerCommands::descriptions() ) } else if msg.chat.is_group() || msg.chat.is_supergroup() { - SimpleCommand::descriptions().username("USERNAME_BOT").to_string() + SimpleCommand::descriptions().username_from_me(&me).to_string() } else { SimpleCommand::descriptions().to_string() } diff --git a/src/utils/command.rs b/src/utils/command.rs index c2d962fc..ee77b11e 100644 --- a/src/utils/command.rs +++ b/src/utils/command.rs @@ -51,16 +51,12 @@ use core::fmt; use std::{ - borrow::Cow, error::Error, fmt::{Display, Formatter, Write}, }; use std::marker::PhantomData; -use teloxide_core::{ - requests::{Request, Requester}, - types::{BotCommand, Me}, -}; +use teloxide_core::types::{BotCommand, Me}; #[cfg(feature = "macros")] pub use teloxide_macros::BotCommands; @@ -282,7 +278,7 @@ pub enum ParseError { pub struct CommandDescriptions<'a> { global_description: Option<&'a str>, descriptions: &'a [CommandDescription<'a>], - bot_username: Option>, + bot_username: Option<&'a str>, } /// Description of a particular command, used in [`CommandDescriptions`]. @@ -331,21 +327,17 @@ impl<'a> CommandDescriptions<'a> { /// ); /// ``` pub fn username(self, bot_username: &'a str) -> Self { - Self { bot_username: Some(Cow::Borrowed(bot_username)), ..self } + Self { bot_username: Some(bot_username), ..self } } /// Sets the username of the bot. /// - /// This is the same as [`username`], but uses `get_me` method of the bot to - /// get the username. + /// This is the same as [`username`], but uses value returned from `get_me` + /// method to get the username. /// /// [`username`]: self::CommandDescriptions::username - pub async fn username_from_bot(self, bot: &impl Requester) -> CommandDescriptions<'a> { - let Me { user, .. } = bot.get_me().send().await.expect("get_me failed"); - Self { - bot_username: Some(Cow::Owned(user.username.expect("Bots must have usernames"))), - ..self - } + pub fn username_from_me(self, me: &'a Me) -> CommandDescriptions<'a> { + self.username(me.user.username.as_deref().expect("Bots must have usernames")) } } From 6922128cb69ea24a52b876b0d92a47c892d25951 Mon Sep 17 00:00:00 2001 From: Maybe Waffle Date: Sun, 27 Mar 2022 13:47:24 +0400 Subject: [PATCH 09/10] Fix some places with remaining `BotCommand` (w/o `s`) --- examples/admin.rs | 2 +- src/utils/command.rs | 2 +- tests/command.rs | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/examples/admin.rs b/examples/admin.rs index 4295b0a8..90f95255 100644 --- a/examples/admin.rs +++ b/examples/admin.rs @@ -3,7 +3,7 @@ use std::{error::Error, str::FromStr}; use chrono::Duration; use teloxide::{prelude2::*, types::ChatPermissions, utils::command::BotCommands}; -// Derive BotCommand to parse text with a command into this enumeration. +// Derive BotCommands to parse text with a command into this enumeration. // // 1. rename = "lowercase" turns all the commands into lowercase letters. // 2. `description = "..."` specifies a text before all the commands. diff --git a/src/utils/command.rs b/src/utils/command.rs index ee77b11e..943acfb2 100644 --- a/src/utils/command.rs +++ b/src/utils/command.rs @@ -209,7 +209,7 @@ pub use teloxide_macros::BotCommands; /// specific variant. /// /// [`FromStr`]: https://doc.rust-lang.org/std/str/trait.FromStr.html -/// [`BotCommand`]: crate::utils::command::BotCommand +/// [`BotCommands`]: crate::utils::command::BotCommands pub trait BotCommands: Sized { /// Parses a command. /// diff --git a/tests/command.rs b/tests/command.rs index 0176c749..a522b2a3 100644 --- a/tests/command.rs +++ b/tests/command.rs @@ -200,7 +200,7 @@ fn descriptions_off() { #[test] #[cfg(feature = "macros")] fn rename_rules() { - #[derive(BotCommand, Debug, PartialEq)] + #[derive(BotCommands, Debug, PartialEq)] enum DefaultCommands { #[command(rename = "lowercase")] AaaAaa, @@ -230,7 +230,7 @@ fn rename_rules() { assert_eq!(DefaultCommands::HhhHhh, DefaultCommands::parse("/HHH-HHH", "").unwrap()); assert_eq!( - "/aaaaaa\n/BBBBBB\n/CccCcc\n/dddDdd\n/eee_eee\n/FFF_FFF\n/ggg-ggg\n/HHH-HHH\n", - DefaultCommands::descriptions() + "/aaaaaa\n/BBBBBB\n/CccCcc\n/dddDdd\n/eee_eee\n/FFF_FFF\n/ggg-ggg\n/HHH-HHH", + DefaultCommands::descriptions().to_string() ); } From ed40e2ad3bc9f87bd93cf74a5fcb791d066260ed Mon Sep 17 00:00:00 2001 From: Maybe Waffle Date: Sun, 27 Mar 2022 15:01:37 +0400 Subject: [PATCH 10/10] fix clippy --- src/utils/command.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/command.rs b/src/utils/command.rs index 943acfb2..e6491697 100644 --- a/src/utils/command.rs +++ b/src/utils/command.rs @@ -455,7 +455,7 @@ impl Display for CommandDescriptions<'_> { f.write_str(prefix)?; f.write_str(command)?; - if let Some(username) = self.bot_username.as_deref() { + if let Some(username) = self.bot_username { f.write_char('@')?; f.write_str(username)?; }