Merge pull request #550 from teloxide/bot_command_better_descriptions

Refactor `BotCommand` trait & related stuff
This commit is contained in:
Waffle Maybe 2022-03-27 22:30:11 +04:00 committed by GitHub
commit 30059b3455
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 294 additions and 127 deletions

View file

@ -6,6 +6,9 @@ on:
name: Continuous integration name: Continuous integration
env:
RUSTFLAGS: "--cfg CI_REDIS"
jobs: jobs:
style: style:
runs-on: ubuntu-latest runs-on: ubuntu-latest

View file

@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed ### Changed
- `UpdateListener::StopToken` is now always `Send` - `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 ## 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`. - Log `UpdateKind::Error` in `teloxide::dispatching2::Dispatcher`.
- Don't warn about unhandled updates in `repls2` ([issue 557](https://github.com/teloxide/teloxide/issues/557)). - 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 ## 0.7.1 - 2022-03-09

View file

@ -59,7 +59,8 @@ full = [
[dependencies] [dependencies]
teloxide-core = { version = "0.4", default-features = false } 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_json = "1.0"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }

View file

@ -125,11 +125,11 @@ Commands are strongly typed and defined declaratively, similar to how we define
([Full](examples/simple_commands.rs)) ([Full](examples/simple_commands.rs))
```rust,no_run ```rust,no_run
use teloxide::{prelude2::*, utils::command::BotCommand}; use teloxide::{prelude2::*, utils::command::BotCommands};
use std::error::Error; use std::error::Error;
#[derive(BotCommand, Clone)] #[derive(BotCommands, Clone)]
#[command(rename = "lowercase", description = "These commands are supported:")] #[command(rename = "lowercase", description = "These commands are supported:")]
enum Command { enum Command {
#[command(description = "display this text.")] #[command(description = "display this text.")]
@ -146,7 +146,7 @@ async fn answer(
command: Command, command: Command,
) -> Result<(), Box<dyn Error + Send + Sync>> { ) -> Result<(), Box<dyn Error + Send + Sync>> {
match command { 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) => { Command::Username(username) => {
bot.send_message(message.chat.id, format!("Your username is @{}.", username)).await? bot.send_message(message.chat.id, format!("Your username is @{}.", username)).await?
} }

View file

@ -1,9 +1,9 @@
use std::{error::Error, str::FromStr}; use std::{error::Error, str::FromStr};
use chrono::Duration; 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. // Derive BotCommands to parse text with a command into this enumeration.
// //
// 1. rename = "lowercase" turns all the commands into lowercase letters. // 1. rename = "lowercase" turns all the commands into lowercase letters.
// 2. `description = "..."` specifies a text before all the commands. // 2. `description = "..."` specifies a text before all the commands.
@ -12,7 +12,7 @@ use teloxide::{prelude2::*, types::ChatPermissions, utils::command::BotCommand};
// your commands in this format: // your commands in this format:
// %GENERAL-DESCRIPTION% // %GENERAL-DESCRIPTION%
// %PREFIX%%COMMAND% - %DESCRIPTION% // %PREFIX%%COMMAND% - %DESCRIPTION%
#[derive(BotCommand, Clone)] #[derive(BotCommands, Clone)]
#[command( #[command(
rename = "lowercase", rename = "lowercase",
description = "Use commands in format /%command% %num% %unit%", description = "Use commands in format /%command% %num% %unit%",
@ -132,7 +132,7 @@ async fn action(
) -> Result<(), Box<dyn Error + Send + Sync>> { ) -> Result<(), Box<dyn Error + Send + Sync>> {
match command { match command {
Command::Help => { 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::Kick => kick_user(bot, msg).await?,
Command::Ban { time, unit } => ban_user(bot, msg, calc_restrict_time(time, unit)).await?, Command::Ban { time, unit } => ban_user(bot, msg, calc_restrict_time(time, unit)).await?,

View file

@ -6,10 +6,10 @@ use teloxide::{
InlineKeyboardButton, InlineKeyboardMarkup, InlineQueryResultArticle, InputMessageContent, InlineKeyboardButton, InlineKeyboardMarkup, InlineQueryResultArticle, InputMessageContent,
InputMessageContentText, InputMessageContentText,
}, },
utils::command::BotCommand, utils::command::BotCommands,
}; };
#[derive(BotCommand)] #[derive(BotCommands)]
#[command(rename = "lowercase", description = "These commands are supported:")] #[command(rename = "lowercase", description = "These commands are supported:")]
enum Command { enum Command {
#[command(description = "Display this text")] #[command(description = "Display this text")]
@ -47,10 +47,10 @@ async fn message_handler(
bot: AutoSend<Bot>, bot: AutoSend<Bot>,
) -> Result<(), Box<dyn Error + Send + Sync>> { ) -> Result<(), Box<dyn Error + Send + Sync>> {
if let Some(text) = m.text() { if let Some(text) = m.text() {
match BotCommand::parse(text, "buttons") { match BotCommands::parse(text, "buttons") {
Ok(Command::Help) => { Ok(Command::Help) => {
// Just send the description of all commands. // 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) => { Ok(Command::Start) => {
// Create a list of buttons and send them. // Create a list of buttons and send them.

View file

@ -9,7 +9,7 @@ use teloxide::{
macros::DialogueState, macros::DialogueState,
prelude2::*, prelude2::*,
types::Me, types::Me,
utils::command::BotCommand, utils::command::BotCommands,
}; };
type MyDialogue = Dialogue<State, ErasedStorage<State>>; type MyDialogue = Dialogue<State, ErasedStorage<State>>;
@ -32,7 +32,7 @@ impl Default for State {
} }
} }
#[derive(BotCommand)] #[derive(BotCommands)]
#[command(rename = "lowercase", description = "These commands are supported:")] #[command(rename = "lowercase", description = "These commands are supported:")]
pub enum Command { pub enum Command {
#[command(description = "get your number.")] #[command(description = "get your number.")]

View file

@ -7,8 +7,8 @@ use rand::Rng;
// dispatching system, which will be deprecated in the future. // dispatching system, which will be deprecated in the future.
use teloxide::{ use teloxide::{
prelude2::*, prelude2::*,
types::{Dice, Update}, types::{Dice, Me, Update},
utils::command::BotCommand, utils::command::BotCommands,
}; };
#[tokio::main] #[tokio::main]
@ -26,28 +26,6 @@ async fn main() {
let handler = Update::filter_message() let handler = Update::filter_message()
// You can use branching to define multiple ways in which an update will be handled. If the // 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. // 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<Bot>| 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<Bot>| async move {
bot.send_message(msg.chat.id, format!("Dice value: {}", dice.value))
.reply_to_message_id(msg.id)
.await?;
Ok(())
},
),
)
.branch( .branch(
dptree::entry() dptree::entry()
// Filter commands: the next handlers will receive a parsed `SimpleCommand`. // 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<Bot>| 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<Bot>| 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) Dispatcher::builder(bot, handler)
@ -101,7 +101,7 @@ struct ConfigParameters {
maintainer_username: Option<String>, maintainer_username: Option<String>,
} }
#[derive(BotCommand, Clone)] #[derive(BotCommands, Clone)]
#[command(rename = "lowercase", description = "Simple commands")] #[command(rename = "lowercase", description = "Simple commands")]
enum SimpleCommand { enum SimpleCommand {
#[command(description = "shows this message.")] #[command(description = "shows this message.")]
@ -112,7 +112,7 @@ enum SimpleCommand {
MyId, MyId,
} }
#[derive(BotCommand, Clone)] #[derive(BotCommands, Clone)]
#[command(rename = "lowercase", description = "Maintainer commands")] #[command(rename = "lowercase", description = "Maintainer commands")]
enum MaintainerCommands { enum MaintainerCommands {
#[command(parse_with = "split", description = "generate a number within range")] #[command(parse_with = "split", description = "generate a number within range")]
@ -124,13 +124,20 @@ async fn simple_commands_handler(
bot: AutoSend<Bot>, bot: AutoSend<Bot>,
cmd: SimpleCommand, cmd: SimpleCommand,
cfg: ConfigParameters, cfg: ConfigParameters,
me: Me,
) -> Result<(), teloxide::RequestError> { ) -> Result<(), teloxide::RequestError> {
let text = match cmd { let text = match cmd {
SimpleCommand::Help => { SimpleCommand::Help => {
if msg.from().unwrap().id == cfg.bot_maintainer { 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_from_me(&me).to_string()
} else { } else {
SimpleCommand::descriptions() SimpleCommand::descriptions().to_string()
} }
} }
SimpleCommand::Maintainer => { SimpleCommand::Maintainer => {

View file

@ -1,8 +1,8 @@
use teloxide::{prelude2::*, utils::command::BotCommand}; use teloxide::{prelude2::*, utils::command::BotCommands};
use std::error::Error; use std::error::Error;
#[derive(BotCommand, Clone)] #[derive(BotCommands, Clone)]
#[command(rename = "lowercase", description = "These commands are supported:")] #[command(rename = "lowercase", description = "These commands are supported:")]
enum Command { enum Command {
#[command(description = "display this text.")] #[command(description = "display this text.")]
@ -19,7 +19,9 @@ async fn answer(
command: Command, command: Command,
) -> Result<(), Box<dyn Error + Send + Sync>> { ) -> Result<(), Box<dyn Error + Send + Sync>> {
match command { 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) => { Command::Username(username) => {
bot.send_message(message.chat.id, format!("Your username is @{}.", username)).await? bot.send_message(message.chat.id, format!("Your username is @{}.", username)).await?
} }

View file

@ -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 futures::{stream::BoxStream, Stream, StreamExt};
use teloxide_core::types::Message; use teloxide_core::types::Message;
@ -21,7 +21,7 @@ pub trait DispatcherHandlerRxExt<R> {
fn commands<C, N>(self, bot_name: N) -> BoxStream<'static, (UpdateWithCx<R, Message>, C)> fn commands<C, N>(self, bot_name: N) -> BoxStream<'static, (UpdateWithCx<R, Message>, C)>
where where
Self: Stream<Item = UpdateWithCx<R, Message>>, Self: Stream<Item = UpdateWithCx<R, Message>>,
C: BotCommand, C: BotCommands,
N: Into<String> + Send, N: Into<String> + Send,
R: Send + 'static; R: Send + 'static;
} }
@ -45,7 +45,7 @@ where
fn commands<C, N>(self, bot_name: N) -> BoxStream<'static, (UpdateWithCx<R, Message>, C)> fn commands<C, N>(self, bot_name: N) -> BoxStream<'static, (UpdateWithCx<R, Message>, C)>
where where
Self: Stream<Item = UpdateWithCx<R, Message>>, Self: Stream<Item = UpdateWithCx<R, Message>>,
C: BotCommand, C: BotCommands,
N: Into<String> + Send, N: Into<String> + Send,
R: Send + 'static, R: Send + 'static,
{ {

View file

@ -4,7 +4,7 @@ use crate::{
DispatcherHandlerRxExt, UpdateWithCx, DispatcherHandlerRxExt, UpdateWithCx,
}, },
error_handlers::{LoggingErrorHandler, OnError}, error_handlers::{LoggingErrorHandler, OnError},
utils::command::BotCommand, utils::command::BotCommands,
}; };
use futures::StreamExt; use futures::StreamExt;
use std::{fmt::Debug, future::Future, sync::Arc}; use std::{fmt::Debug, future::Future, sync::Arc};
@ -25,7 +25,7 @@ use tokio_stream::wrappers::UnboundedReceiverStream;
#[cfg(feature = "ctrlc_handler")] #[cfg(feature = "ctrlc_handler")]
pub async fn commands_repl<R, Cmd, H, Fut, HandlerE, N>(requester: R, bot_name: N, handler: H) pub async fn commands_repl<R, Cmd, H, Fut, HandlerE, N>(requester: R, bot_name: N, handler: H)
where where
Cmd: BotCommand + Send + 'static, Cmd: BotCommands + Send + 'static,
H: Fn(UpdateWithCx<R, Message>, Cmd) -> Fut + Send + Sync + 'static, H: Fn(UpdateWithCx<R, Message>, Cmd) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Result<(), HandlerE>> + Send + 'static, Fut: Future<Output = Result<(), HandlerE>> + Send + 'static,
Result<(), HandlerE>: OnError<HandlerE>, Result<(), HandlerE>: OnError<HandlerE>,
@ -64,7 +64,7 @@ pub async fn commands_repl_with_listener<'a, R, Cmd, H, Fut, L, ListenerE, Handl
handler: H, handler: H,
listener: L, listener: L,
) where ) where
Cmd: BotCommand + Send + 'static, Cmd: BotCommands + Send + 'static,
H: Fn(UpdateWithCx<R, Message>, Cmd) -> Fut + Send + Sync + 'static, H: Fn(UpdateWithCx<R, Message>, Cmd) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Result<(), HandlerE>> + Send + 'static, Fut: Future<Output = Result<(), HandlerE>> + Send + 'static,
L: UpdateListener<ListenerE> + Send + 'a, L: UpdateListener<ListenerE> + Send + 'a,

View file

@ -6,7 +6,7 @@ use crate::{
HandlerFactory, HandlerFactory,
}, },
types::{Me, Message}, types::{Me, Message},
utils::command::BotCommand, utils::command::BotCommands,
}; };
use dptree::{di::DependencyMap, Handler}; use dptree::{di::DependencyMap, Handler};
@ -23,7 +23,7 @@ pub trait HandlerExt<Output> {
#[must_use] #[must_use]
fn filter_command<C>(self) -> Self fn filter_command<C>(self) -> Self
where where
C: BotCommand + Send + Sync + 'static; C: BotCommands + Send + Sync + 'static;
/// Passes [`Dialogue<D, S>`] and `D` as handler dependencies. /// Passes [`Dialogue<D, S>`] and `D` as handler dependencies.
/// ///
@ -62,7 +62,7 @@ where
{ {
fn filter_command<C>(self) -> Self fn filter_command<C>(self) -> Self
where where
C: BotCommand + Send + Sync + 'static, C: BotCommands + Send + Sync + 'static,
{ {
self.chain(dptree::filter_map(move |message: Message, me: Me| { self.chain(dptree::filter_map(move |message: Message, me: Me| {
let bot_name = me.user.username.expect("Bots must have a username"); let bot_name = me.user.username.expect("Bots must have a username");

View file

@ -3,7 +3,7 @@ use crate::{
dispatching2::{HandlerExt, UpdateFilterExt}, dispatching2::{HandlerExt, UpdateFilterExt},
error_handlers::LoggingErrorHandler, error_handlers::LoggingErrorHandler,
types::Update, types::Update,
utils::command::BotCommand, utils::command::BotCommands,
}; };
use dptree::di::{DependencyMap, Injectable}; use dptree::di::{DependencyMap, Injectable};
use std::{fmt::Debug, marker::PhantomData}; use std::{fmt::Debug, marker::PhantomData};
@ -27,7 +27,7 @@ use teloxide_core::requests::Requester;
#[cfg(feature = "ctrlc_handler")] #[cfg(feature = "ctrlc_handler")]
pub async fn commands_repl<'a, R, Cmd, H, E, Args>(bot: R, handler: H, cmd: PhantomData<Cmd>) pub async fn commands_repl<'a, R, Cmd, H, E, Args>(bot: R, handler: H, cmd: PhantomData<Cmd>)
where where
Cmd: BotCommand + Send + Sync + 'static, Cmd: BotCommands + Send + Sync + 'static,
H: Injectable<DependencyMap, Result<(), E>, Args> + Send + Sync + 'static, H: Injectable<DependencyMap, Result<(), E>, Args> + Send + Sync + 'static,
R: Requester + Clone + Send + Sync + 'static, R: Requester + Clone + Send + Sync + 'static,
<R as Requester>::GetUpdates: Send, <R as Requester>::GetUpdates: Send,
@ -67,7 +67,7 @@ pub async fn commands_repl_with_listener<'a, R, Cmd, H, L, ListenerE, E, Args>(
listener: L, listener: L,
_cmd: PhantomData<Cmd>, _cmd: PhantomData<Cmd>,
) where ) where
Cmd: BotCommand + Send + Sync + 'static, Cmd: BotCommands + Send + Sync + 'static,
H: Injectable<DependencyMap, Result<(), E>, Args> + Send + Sync + 'static, H: Injectable<DependencyMap, Result<(), E>, Args> + Send + Sync + 'static,
L: UpdateListener<ListenerE> + Send + 'a, L: UpdateListener<ListenerE> + Send + 'a,
ListenerE: Debug + Send + 'a, ListenerE: Debug + Send + 'a,

View file

@ -1,17 +1,18 @@
//! Command parsers. //! 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 //! commands of your bot, or use functions, which split input text into a string
//! command with its arguments. //! command with its arguments.
//! //!
//! # Using BotCommand //! # Using BotCommands
//!
//! ``` //! ```
//! # #[cfg(feature = "macros")] { //! # #[cfg(feature = "macros")] {
//! use teloxide::utils::command::BotCommand; //! use teloxide::utils::command::BotCommands;
//! //!
//! type UnitOfTime = u8; //! type UnitOfTime = u8;
//! //!
//! #[derive(BotCommand, PartialEq, Debug)] //! #[derive(BotCommands, PartialEq, Debug)]
//! #[command(rename = "lowercase", parse_with = "split")] //! #[command(rename = "lowercase", parse_with = "split")]
//! enum AdminCommand { //! enum AdminCommand {
//! Mute(UnitOfTime, char), //! Mute(UnitOfTime, char),
@ -24,6 +25,7 @@
//! ``` //! ```
//! //!
//! # Using parse_command //! # Using parse_command
//!
//! ``` //! ```
//! use teloxide::utils::command::parse_command; //! use teloxide::utils::command::parse_command;
//! //!
@ -33,6 +35,7 @@
//! ``` //! ```
//! //!
//! # Using parse_command_with_prefix //! # Using parse_command_with_prefix
//!
//! ``` //! ```
//! use teloxide::utils::command::parse_command_with_prefix; //! use teloxide::utils::command::parse_command_with_prefix;
//! //!
@ -46,25 +49,27 @@
//! //!
//! [examples/admin_bot]: https://github.com/teloxide/teloxide/blob/master/examples/admin_bot/ //! [examples/admin_bot]: https://github.com/teloxide/teloxide/blob/master/examples/admin_bot/
use core::fmt;
use std::{ use std::{
error::Error, error::Error,
fmt::{Display, Formatter}, fmt::{Display, Formatter, Write},
}; };
use std::marker::PhantomData; use std::marker::PhantomData;
use teloxide_core::types::{BotCommand, Me};
#[cfg(feature = "macros")] #[cfg(feature = "macros")]
pub use teloxide_macros::BotCommand; pub use teloxide_macros::BotCommands;
/// An enumeration of bot's commands. /// An enumeration of bot's commands.
/// ///
/// # Example /// # Example
/// ``` /// ```
/// # #[cfg(feature = "macros")] { /// # #[cfg(feature = "macros")] {
/// use teloxide::utils::command::BotCommand; /// use teloxide::utils::command::BotCommands;
/// ///
/// type UnitOfTime = u8; /// type UnitOfTime = u8;
/// ///
/// #[derive(BotCommand, PartialEq, Debug)] /// #[derive(BotCommands, PartialEq, Debug)]
/// #[command(rename = "lowercase", parse_with = "split")] /// #[command(rename = "lowercase", parse_with = "split")]
/// enum AdminCommand { /// enum AdminCommand {
/// Mute(UnitOfTime, char), /// Mute(UnitOfTime, char),
@ -98,9 +103,9 @@ pub use teloxide_macros::BotCommand;
/// ## Example /// ## Example
/// ``` /// ```
/// # #[cfg(feature = "macros")] { /// # #[cfg(feature = "macros")] {
/// use teloxide::utils::command::BotCommand; /// use teloxide::utils::command::BotCommands;
/// ///
/// #[derive(BotCommand, PartialEq, Debug)] /// #[derive(BotCommands, PartialEq, Debug)]
/// #[command(rename = "lowercase")] /// #[command(rename = "lowercase")]
/// enum Command { /// enum Command {
/// Text(String), /// Text(String),
@ -118,9 +123,9 @@ pub use teloxide_macros::BotCommand;
/// ## Example /// ## Example
/// ``` /// ```
/// # #[cfg(feature = "macros")] { /// # #[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")] /// #[command(rename = "lowercase", parse_with = "split")]
/// enum Command { /// enum Command {
/// Nums(u8, u16, i32), /// Nums(u8, u16, i32),
@ -138,9 +143,9 @@ pub use teloxide_macros::BotCommand;
/// ## Example /// ## Example
/// ``` /// ```
/// # #[cfg(feature = "macros")] { /// # #[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 = "|")] /// #[command(rename = "lowercase", parse_with = "split", separator = "|")]
/// enum Command { /// enum Command {
/// Nums(u8, u16, i32), /// Nums(u8, u16, i32),
@ -171,7 +176,7 @@ pub use teloxide_macros::BotCommand;
/// ## Example /// ## Example
/// ``` /// ```
/// # #[cfg(feature = "macros")] { /// # #[cfg(feature = "macros")] {
/// use teloxide::utils::command::{BotCommand, ParseError}; /// use teloxide::utils::command::{BotCommands, ParseError};
/// ///
/// fn accept_two_digits(input: String) -> Result<(u8,), ParseError> { /// fn accept_two_digits(input: String) -> Result<(u8,), ParseError> {
/// match input.len() { /// match input.len() {
@ -183,7 +188,7 @@ pub use teloxide_macros::BotCommand;
/// } /// }
/// } /// }
/// ///
/// #[derive(BotCommand, PartialEq, Debug)] /// #[derive(BotCommands, PartialEq, Debug)]
/// #[command(rename = "lowercase")] /// #[command(rename = "lowercase")]
/// enum Command { /// enum Command {
/// #[command(parse_with = "accept_two_digits")] /// #[command(parse_with = "accept_two_digits")]
@ -204,24 +209,41 @@ pub use teloxide_macros::BotCommand;
/// specific variant. /// specific variant.
/// ///
/// [`FromStr`]: https://doc.rust-lang.org/std/str/trait.FromStr.html /// [`FromStr`]: https://doc.rust-lang.org/std/str/trait.FromStr.html
/// [`BotCommand`]: crate::utils::command::BotCommand /// [`BotCommands`]: crate::utils::command::BotCommands
pub trait BotCommand: Sized { pub trait BotCommands: Sized {
fn descriptions() -> String; /// Parses a command.
fn parse<N>(s: &str, bot_name: N) -> Result<Self, ParseError> ///
/// `bot_username` is required to parse commands like
/// `/cmd@username_of_the_bot`.
fn parse<N>(s: &str, bot_username: N) -> Result<Self, ParseError>
where where
N: Into<String>; N: Into<String>;
/// Returns descriptions of the commands suitable to be shown to the user
/// (for example when `/help` command is used).
fn descriptions() -> CommandDescriptions<'static>;
/// Returns a vector of [`BotCommand`] that can be used with
/// [`set_my_commands`].
///
/// [`BotCommand`]: crate::types::BotCommand
/// [`set_my_commands`]: crate::requests::Requester::set_my_commands
fn bot_commands() -> Vec<BotCommand>;
/// Returns `PhantomData<Self>` that is used as a param of [`commands_repl`]
///
/// [`commands_repl`]: (crate::repls2::commands_repl)
fn ty() -> PhantomData<Self> { fn ty() -> PhantomData<Self> {
PhantomData PhantomData
} }
fn bot_commands() -> Vec<crate::types::BotCommand>;
} }
pub type PrefixedBotCommand = String; pub type PrefixedBotCommand = String;
pub type BotName = 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)] #[derive(Debug)]
pub enum ParseError { pub enum ParseError {
TooFewArguments { TooFewArguments {
@ -247,28 +269,77 @@ pub enum ParseError {
Custom(Box<dyn Error + Send + Sync + 'static>), Custom(Box<dyn Error + Send + Sync + 'static>),
} }
impl Display for ParseError { /// Command descriptions that can be shown to the user (e.g. as a part of
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { /// `/help` message)
match self { ///
ParseError::TooFewArguments { expected, found, message } => write!( /// Most of the time you don't need to create this struct yourself as it's
f, /// returned from [`BotCommands::descriptions`].
"Too few arguments (expected {}, found {}, message = '{}')", #[derive(Debug, Clone)]
expected, found, message pub struct CommandDescriptions<'a> {
), global_description: Option<&'a str>,
ParseError::TooManyArguments { expected, found, message } => write!( descriptions: &'a [CommandDescription<'a>],
f, bot_username: Option<&'a str>,
"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 {} /// 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
///
/// ```
/// 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" },
/// ]);
///
/// 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(bot_username), ..self }
}
/// Sets the username of the bot.
///
/// This is the same as [`username`], but uses value returned from `get_me`
/// method to get the username.
///
/// [`username`]: self::CommandDescriptions::username
pub fn username_from_me(self, me: &'a Me) -> CommandDescriptions<'a> {
self.username(me.user.username.as_deref().expect("Bots must have usernames"))
}
}
/// Parses a string into a command with args. /// Parses a string into a command with args.
/// ///
@ -346,6 +417,68 @@ where
Some((command, words.collect())) 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 {
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 // The rest of tests are integrational due to problems with macro expansion in
// unit tests. // unit tests.
#[cfg(test)] #[cfg(test)]

View file

@ -13,6 +13,8 @@ use tokio::sync::Notify;
use crate::dispatching::update_listeners::UpdateListener; use crate::dispatching::update_listeners::UpdateListener;
/// A token which used to shutdown [`Dispatcher`]. /// A token which used to shutdown [`Dispatcher`].
///
/// [`Dispatcher`]: crate::dispatching::Dispatcher
#[derive(Clone)] #[derive(Clone)]
pub struct ShutdownToken { pub struct ShutdownToken {
dispatcher_state: Arc<DispatcherState>, dispatcher_state: Arc<DispatcherState>,
@ -21,6 +23,8 @@ pub struct ShutdownToken {
/// This error is returned from [`ShutdownToken::shutdown`] when trying to /// This error is returned from [`ShutdownToken::shutdown`] when trying to
/// shutdown an idle [`Dispatcher`]. /// shutdown an idle [`Dispatcher`].
///
/// [`Dispatcher`]: crate::dispatching::Dispatcher
#[derive(Debug)] #[derive(Debug)]
pub struct IdleShutdownError; pub struct IdleShutdownError;

View file

@ -2,7 +2,7 @@
#![allow(clippy::nonstandard_macro_braces)] #![allow(clippy::nonstandard_macro_braces)]
#[cfg(feature = "macros")] #[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 // We put tests here because macro expand in unit tests in module
// teloxide::utils::command was a failure // teloxide::utils::command was a failure
@ -10,7 +10,7 @@ use teloxide::utils::command::{BotCommand, ParseError};
#[test] #[test]
#[cfg(feature = "macros")] #[cfg(feature = "macros")]
fn parse_command_with_args() { fn parse_command_with_args() {
#[derive(BotCommand, Debug, PartialEq)] #[derive(BotCommands, Debug, PartialEq)]
#[command(rename = "lowercase")] #[command(rename = "lowercase")]
enum DefaultCommands { enum DefaultCommands {
Start(String), Start(String),
@ -26,7 +26,7 @@ fn parse_command_with_args() {
#[test] #[test]
#[cfg(feature = "macros")] #[cfg(feature = "macros")]
fn parse_command_with_non_string_arg() { fn parse_command_with_non_string_arg() {
#[derive(BotCommand, Debug, PartialEq)] #[derive(BotCommands, Debug, PartialEq)]
#[command(rename = "lowercase")] #[command(rename = "lowercase")]
enum DefaultCommands { enum DefaultCommands {
Start(i32), Start(i32),
@ -42,7 +42,7 @@ fn parse_command_with_non_string_arg() {
#[test] #[test]
#[cfg(feature = "macros")] #[cfg(feature = "macros")]
fn attribute_prefix() { fn attribute_prefix() {
#[derive(BotCommand, Debug, PartialEq)] #[derive(BotCommands, Debug, PartialEq)]
#[command(rename = "lowercase")] #[command(rename = "lowercase")]
enum DefaultCommands { enum DefaultCommands {
#[command(prefix = "!")] #[command(prefix = "!")]
@ -59,7 +59,7 @@ fn attribute_prefix() {
#[test] #[test]
#[cfg(feature = "macros")] #[cfg(feature = "macros")]
fn many_attributes() { fn many_attributes() {
#[derive(BotCommand, Debug, PartialEq)] #[derive(BotCommands, Debug, PartialEq)]
#[command(rename = "lowercase")] #[command(rename = "lowercase")]
enum DefaultCommands { enum DefaultCommands {
#[command(prefix = "!", description = "desc")] #[command(prefix = "!", description = "desc")]
@ -68,13 +68,13 @@ fn many_attributes() {
} }
assert_eq!(DefaultCommands::Start, DefaultCommands::parse("!start", "").unwrap()); 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] #[test]
#[cfg(feature = "macros")] #[cfg(feature = "macros")]
fn global_attributes() { fn global_attributes() {
#[derive(BotCommand, Debug, PartialEq)] #[derive(BotCommands, Debug, PartialEq)]
#[command(prefix = "!", rename = "lowercase", description = "Bot commands")] #[command(prefix = "!", rename = "lowercase", description = "Bot commands")]
enum DefaultCommands { enum DefaultCommands {
#[command(prefix = "/")] #[command(prefix = "/")]
@ -84,13 +84,13 @@ fn global_attributes() {
assert_eq!(DefaultCommands::Start, DefaultCommands::parse("/start", "MyNameBot").unwrap()); assert_eq!(DefaultCommands::Start, DefaultCommands::parse("/start", "MyNameBot").unwrap());
assert_eq!(DefaultCommands::Help, DefaultCommands::parse("!help", "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] #[test]
#[cfg(feature = "macros")] #[cfg(feature = "macros")]
fn parse_command_with_bot_name() { fn parse_command_with_bot_name() {
#[derive(BotCommand, Debug, PartialEq)] #[derive(BotCommands, Debug, PartialEq)]
#[command(rename = "lowercase")] #[command(rename = "lowercase")]
enum DefaultCommands { enum DefaultCommands {
#[command(prefix = "/")] #[command(prefix = "/")]
@ -107,7 +107,7 @@ fn parse_command_with_bot_name() {
#[test] #[test]
#[cfg(feature = "macros")] #[cfg(feature = "macros")]
fn parse_with_split() { fn parse_with_split() {
#[derive(BotCommand, Debug, PartialEq)] #[derive(BotCommands, Debug, PartialEq)]
#[command(rename = "lowercase")] #[command(rename = "lowercase")]
#[command(parse_with = "split")] #[command(parse_with = "split")]
enum DefaultCommands { enum DefaultCommands {
@ -124,7 +124,7 @@ fn parse_with_split() {
#[test] #[test]
#[cfg(feature = "macros")] #[cfg(feature = "macros")]
fn parse_with_split2() { fn parse_with_split2() {
#[derive(BotCommand, Debug, PartialEq)] #[derive(BotCommands, Debug, PartialEq)]
#[command(rename = "lowercase")] #[command(rename = "lowercase")]
#[command(parse_with = "split", separator = "|")] #[command(parse_with = "split", separator = "|")]
enum DefaultCommands { enum DefaultCommands {
@ -152,7 +152,7 @@ fn parse_custom_parser() {
.map_err(|_| ParseError::Custom("First argument must be a integer!".to_owned().into())) .map_err(|_| ParseError::Custom("First argument must be a integer!".to_owned().into()))
} }
#[derive(BotCommand, Debug, PartialEq)] #[derive(BotCommands, Debug, PartialEq)]
#[command(rename = "lowercase")] #[command(rename = "lowercase")]
enum DefaultCommands { enum DefaultCommands {
#[command(parse_with = "custom_parse_function")] #[command(parse_with = "custom_parse_function")]
@ -169,7 +169,7 @@ fn parse_custom_parser() {
#[test] #[test]
#[cfg(feature = "macros")] #[cfg(feature = "macros")]
fn parse_named_fields() { fn parse_named_fields() {
#[derive(BotCommand, Debug, PartialEq)] #[derive(BotCommands, Debug, PartialEq)]
#[command(rename = "lowercase")] #[command(rename = "lowercase")]
#[command(parse_with = "split")] #[command(parse_with = "split")]
enum DefaultCommands { enum DefaultCommands {
@ -186,7 +186,7 @@ fn parse_named_fields() {
#[test] #[test]
#[cfg(feature = "macros")] #[cfg(feature = "macros")]
fn descriptions_off() { fn descriptions_off() {
#[derive(BotCommand, Debug, PartialEq)] #[derive(BotCommands, Debug, PartialEq)]
#[command(rename = "lowercase")] #[command(rename = "lowercase")]
enum DefaultCommands { enum DefaultCommands {
#[command(description = "off")] #[command(description = "off")]
@ -194,13 +194,13 @@ fn descriptions_off() {
Help, Help,
} }
assert_eq!(DefaultCommands::descriptions(), "/help\n".to_owned()); assert_eq!(DefaultCommands::descriptions().to_string(), "/help".to_owned());
} }
#[test] #[test]
#[cfg(feature = "macros")] #[cfg(feature = "macros")]
fn rename_rules() { fn rename_rules() {
#[derive(BotCommand, Debug, PartialEq)] #[derive(BotCommands, Debug, PartialEq)]
enum DefaultCommands { enum DefaultCommands {
#[command(rename = "lowercase")] #[command(rename = "lowercase")]
AaaAaa, AaaAaa,
@ -230,7 +230,7 @@ fn rename_rules() {
assert_eq!(DefaultCommands::HhhHhh, DefaultCommands::parse("/HHH-HHH", "").unwrap()); assert_eq!(DefaultCommands::HhhHhh, DefaultCommands::parse("/HHH-HHH", "").unwrap());
assert_eq!( assert_eq!(
"/aaaaaa\n/BBBBBB\n/CccCcc\n/dddDdd\n/eee_eee\n/FFF_FFF\n/ggg-ggg\n/HHH-HHH\n", "/aaaaaa\n/BBBBBB\n/CccCcc\n/dddDdd\n/eee_eee\n/FFF_FFF\n/ggg-ggg\n/HHH-HHH",
DefaultCommands::descriptions() DefaultCommands::descriptions().to_string()
); );
} }

View file

@ -5,6 +5,7 @@ use std::{
use teloxide::dispatching::dialogue::{RedisStorage, RedisStorageError, Serializer, Storage}; use teloxide::dispatching::dialogue::{RedisStorage, RedisStorageError, Serializer, Storage};
#[tokio::test] #[tokio::test]
#[cfg_attr(not(CI_REDIS), ignore)]
async fn test_redis_json() { async fn test_redis_json() {
let storage = RedisStorage::open( let storage = RedisStorage::open(
"redis://127.0.0.1:7777", "redis://127.0.0.1:7777",
@ -16,6 +17,7 @@ async fn test_redis_json() {
} }
#[tokio::test] #[tokio::test]
#[cfg_attr(not(CI_REDIS), ignore)]
async fn test_redis_bincode() { async fn test_redis_bincode() {
let storage = RedisStorage::open( let storage = RedisStorage::open(
"redis://127.0.0.1:7778", "redis://127.0.0.1:7778",
@ -27,6 +29,7 @@ async fn test_redis_bincode() {
} }
#[tokio::test] #[tokio::test]
#[cfg_attr(not(CI_REDIS), ignore)]
async fn test_redis_cbor() { async fn test_redis_cbor() {
let storage = RedisStorage::open( let storage = RedisStorage::open(
"redis://127.0.0.1:7779", "redis://127.0.0.1:7779",

View file

@ -1,36 +1,47 @@
use std::{ use std::{
fmt::{Debug, Display}, fmt::{Debug, Display},
fs,
sync::Arc, sync::Arc,
}; };
use teloxide::dispatching::dialogue::{Serializer, SqliteStorage, SqliteStorageError, Storage}; use teloxide::dispatching::dialogue::{Serializer, SqliteStorage, SqliteStorageError, Storage};
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
async fn test_sqlite_json() { async fn test_sqlite_json() {
let storage = fs::create_dir("./test_db1").unwrap();
SqliteStorage::open("./test_db1.sqlite", teloxide::dispatching::dialogue::serializer::Json) let storage = SqliteStorage::open(
.await "./test_db1/test_db1.sqlite",
.unwrap(); teloxide::dispatching::dialogue::serializer::Json,
)
.await
.unwrap();
test_sqlite(storage).await; test_sqlite(storage).await;
fs::remove_dir_all("./test_db1").unwrap();
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
async fn test_sqlite_bincode() { async fn test_sqlite_bincode() {
fs::create_dir("./test_db2").unwrap();
let storage = SqliteStorage::open( let storage = SqliteStorage::open(
"./test_db2.sqlite", "./test_db2/test_db2.sqlite",
teloxide::dispatching::dialogue::serializer::Bincode, teloxide::dispatching::dialogue::serializer::Bincode,
) )
.await .await
.unwrap(); .unwrap();
test_sqlite(storage).await; test_sqlite(storage).await;
fs::remove_dir_all("./test_db2").unwrap();
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
async fn test_sqlite_cbor() { async fn test_sqlite_cbor() {
let storage = fs::create_dir("./test_db3").unwrap();
SqliteStorage::open("./test_db3.sqlite", teloxide::dispatching::dialogue::serializer::Cbor) let storage = SqliteStorage::open(
.await "./test_db3/test_db3.sqlite",
.unwrap(); teloxide::dispatching::dialogue::serializer::Cbor,
)
.await
.unwrap();
test_sqlite(storage).await; test_sqlite(storage).await;
fs::remove_dir_all("./test_db3").unwrap();
} }
type Dialogue = String; type Dialogue = String;