teloxide_ng/utils/command.rs
1//! Command parsers.
2//!
3//! You can either create an `enum` with derived [`BotCommands`], containing
4//! commands of your bot, or use functions, which split input text into a string
5//! command with its arguments.
6//!
7//! # Using BotCommands
8//!
9//! ```
10//! # #[cfg(feature = "macros")] {
11//! use teloxide_ng::utils::command::BotCommands;
12//!
13//! type UnitOfTime = u8;
14//!
15//! #[derive(BotCommands, PartialEq, Debug)]
16//! #[command(rename_rule = "lowercase", parse_with = "split")]
17//! enum AdminCommand {
18//! Mute(UnitOfTime, char),
19//! Ban(UnitOfTime, char),
20//! }
21//!
22//! let command = AdminCommand::parse("/ban 5 h", "bot_name").unwrap();
23//! assert_eq!(command, AdminCommand::Ban(5, 'h'));
24//! # }
25//! ```
26//!
27//! # Using parse_command
28//!
29//! ```
30//! use teloxide_ng::utils::command::parse_command;
31//!
32//! let (command, args) = parse_command("/ban@MyBotName 3 hours", "MyBotName").unwrap();
33//! assert_eq!(command, "ban");
34//! assert_eq!(args, vec!["3", "hours"]);
35//! ```
36//!
37//! # Using parse_command_with_prefix
38//!
39//! ```
40//! use teloxide_ng::utils::command::parse_command_with_prefix;
41//!
42//! let text = "!ban 3 hours";
43//! let (command, args) = parse_command_with_prefix("!", text, "").unwrap();
44//! assert_eq!(command, "ban");
45//! assert_eq!(args, vec!["3", "hours"]);
46//! ```
47//!
48//! See [examples/admin] as a more complicated examples.
49//!
50//! [examples/admin]: https://github.com/teloxide/teloxide/blob/master/crates/teloxide-ng/examples/admin.rs
51
52use core::fmt;
53use std::{
54 error::Error,
55 fmt::{Display, Formatter, Write},
56};
57
58use teloxide_core_ng::types::{BotCommand, Me};
59#[cfg(feature = "macros")]
60pub use teloxide_macros_ng::BotCommands;
61
62/// An enumeration of bot's commands.
63///
64/// # Example
65/// ```
66/// # #[cfg(feature = "macros")] {
67/// use teloxide_ng::utils::command::BotCommands;
68///
69/// type UnitOfTime = u8;
70///
71/// #[derive(BotCommands, PartialEq, Debug)]
72/// #[command(rename_rule = "lowercase", parse_with = "split")]
73/// enum AdminCommand {
74/// Mute(UnitOfTime, char),
75/// Ban(UnitOfTime, char),
76/// }
77///
78/// let command = AdminCommand::parse("/ban 5 h", "bot_name").unwrap();
79/// assert_eq!(command, AdminCommand::Ban(5, 'h'));
80/// # }
81/// ```
82///
83/// # Enum attributes
84/// 1. `#[command(rename_rule = "rule")]` Rename all commands by `rule`.
85/// Allowed rules are `lowercase`, `UPPERCASE`, `PascalCase`, `camelCase`,
86/// `snake_case`, `SCREAMING_SNAKE_CASE`, `kebab-case`, and
87/// `SCREAMING-KEBAB-CASE`.
88///
89/// 2. `#[command(prefix = "prefix")]` Change a prefix for all commands (the
90/// default is `/`).
91///
92/// 3. `#[command(description = "description")]` and `/// description` Add a
93/// summary description of commands before all commands.
94///
95/// 4. `#[command(parse_with = "parser")]` Change the parser of arguments.
96/// Possible values:
97/// - `default` - the same as the unspecified parser. It only puts all
98/// text after the first space into the first argument, which must
99/// implement [`FromStr`].
100///
101/// ## Example
102/// ```
103/// # #[cfg(feature = "macros")] {
104/// use teloxide_ng::utils::command::BotCommands;
105///
106/// #[derive(BotCommands, PartialEq, Debug)]
107/// #[command(rename_rule = "lowercase")]
108/// enum Command {
109/// Text(String),
110/// }
111///
112/// let command = Command::parse("/text hello my dear friend!", "").unwrap();
113/// assert_eq!(command, Command::Text("hello my dear friend!".to_string()));
114/// # }
115/// ```
116///
117/// - `split` - separates a message by a given separator (the default is the
118/// space character) and parses each part into the corresponding arguments,
119/// which must implement [`FromStr`].
120///
121/// ## Example
122/// ```
123/// # #[cfg(feature = "macros")] {
124/// use teloxide_ng::utils::command::BotCommands;
125///
126/// #[derive(BotCommands, PartialEq, Debug)]
127/// #[command(rename_rule = "lowercase", parse_with = "split")]
128/// enum Command {
129/// Nums(u8, u16, i32),
130/// }
131///
132/// let command = Command::parse("/nums 1 32 -5", "").unwrap();
133/// assert_eq!(command, Command::Nums(1, 32, -5));
134/// # }
135/// ```
136///
137/// 5. `#[command(separator = "sep")]` Specify separator used by the `split`
138/// parser. It will be ignored when accompanied by another type of parsers.
139///
140/// ## Example
141/// ```
142/// # #[cfg(feature = "macros")] {
143/// use teloxide_ng::utils::command::BotCommands;
144///
145/// #[derive(BotCommands, PartialEq, Debug)]
146/// #[command(rename_rule = "lowercase", parse_with = "split", separator = "|")]
147/// enum Command {
148/// Nums(u8, u16, i32),
149/// }
150///
151/// let command = Command::parse("/nums 1|32|5", "").unwrap();
152/// assert_eq!(command, Command::Nums(1, 32, 5));
153/// # }
154/// ```
155///
156/// 6. `#[command(command_separator = "sep")]` Specify separator between command
157/// and args. Default is a space character.
158///
159/// ## Example
160/// ```
161/// # #[cfg(feature = "macros")] {
162/// use teloxide_ng::utils::command::BotCommands;
163///
164/// #[derive(BotCommands, PartialEq, Debug)]
165/// #[command(
166/// rename_rule = "lowercase",
167/// parse_with = "split",
168/// separator = "_",
169/// command_separator = "_"
170/// )]
171/// enum Command {
172/// Nums(u8, u16, i32),
173/// }
174///
175/// let command = Command::parse("/nums_1_32_5", "").unwrap();
176/// assert_eq!(command, Command::Nums(1, 32, 5));
177/// # }
178/// ```
179///
180/// # Variant attributes
181/// All variant attributes override the corresponding `enum` attributes.
182///
183/// 1. `#[command(rename_rule = "rule")]` Rename one command by a rule. Allowed
184/// rules are `lowercase`, `UPPERCASE`, `PascalCase`, `camelCase`,
185/// `snake_case`, `SCREAMING_SNAKE_CASE`, `kebab-case`,
186/// `SCREAMING-KEBAB-CASE`.
187///
188/// 2. `#[command(rename = "name")]` Rename one command to `name` (literal
189/// renaming; do not confuse with `rename_rule`).
190///
191/// 3. `#[command(description = "description")]` and `/// description` Give
192/// your command a description. It will be shown in the help message.
193///
194/// 4. `#[command(parse_with = "parser")]` Parse arguments of one command with
195/// a given parser. `parser` must be a function of the signature `fn(String)
196/// -> Result<Tuple, ParseError>`, where `Tuple` corresponds to the
197/// variant's arguments.
198///
199/// 5. `#[command(hide)]` Hide a command from the help message. It will still
200/// be parsed.
201///
202/// 6. `#[command(alias = "alias")]` Add an alias to a command. It will be shown
203/// in the help message.
204///
205/// 7. `#[command(aliases = ["alias1", "alias2"])]` Add multiple aliases to a
206/// command. They will be shown in the help message.
207///
208/// 8. `#[command(hide_aliases)]` Hide all aliases of a command from the help
209/// message.
210///
211/// ## Example
212/// ```
213/// # #[cfg(feature = "macros")] {
214/// use teloxide_ng::utils::command::{BotCommands, ParseError};
215///
216/// fn accept_two_digits(input: String) -> Result<(u8,), ParseError> {
217/// match input.len() {
218/// 2 => {
219/// let num = input.parse::<u8>().map_err(|e| ParseError::IncorrectFormat(e.into()))?;
220/// Ok((num,))
221/// }
222/// len => Err(ParseError::Custom(format!("Only 2 digits allowed, not {}", len).into())),
223/// }
224/// }
225///
226/// #[derive(BotCommands, PartialEq, Debug)]
227/// #[command(rename_rule = "lowercase")]
228/// enum Command {
229/// #[command(parse_with = accept_two_digits)]
230/// Num(u8),
231/// }
232///
233/// let command = Command::parse("/num 12", "").unwrap();
234/// assert_eq!(command, Command::Num(12));
235/// let command = Command::parse("/num 333", "");
236/// assert!(command.is_err());
237/// # }
238/// ```
239///
240/// 5. `#[command(prefix = "prefix")]`
241/// 6. `#[command(separator = "sep")]`
242///
243/// These attributes just override the corresponding `enum` attributes for a
244/// specific variant.
245///
246/// [`FromStr`]: https://doc.rust-lang.org/std/str/trait.FromStr.html
247/// [`BotCommands`]: crate::utils::command::BotCommands
248pub trait BotCommands: Sized {
249 /// Parses a command.
250 ///
251 /// `bot_username` is required to parse commands like
252 /// `/cmd@username_of_the_bot`.
253 fn parse(s: &str, bot_username: &str) -> Result<Self, ParseError>;
254
255 /// Returns descriptions of the commands suitable to be shown to the user
256 /// (for example when `/help` command is used).
257 fn descriptions() -> CommandDescriptions<'static>;
258
259 /// Returns a vector of [`BotCommand`] that can be used with
260 /// [`set_my_commands`].
261 ///
262 /// [`BotCommand`]: crate::types::BotCommand
263 /// [`set_my_commands`]: crate::requests::Requester::set_my_commands
264 fn bot_commands() -> Vec<BotCommand>;
265}
266
267pub type PrefixedBotCommand = String;
268pub type BotName = String;
269
270/// Errors returned from [`BotCommands::parse`].
271///
272/// [`BotCommands::parse`]: BotCommands::parse
273#[derive(Debug)]
274pub enum ParseError {
275 TooFewArguments {
276 expected: usize,
277 found: usize,
278 message: String,
279 },
280 TooManyArguments {
281 expected: usize,
282 found: usize,
283 message: String,
284 },
285
286 /// Redirected from [`FromStr::from_str`].
287 ///
288 /// [`FromStr::from_str`]: https://doc.rust-lang.org/std/str/trait.FromStr.html#tymethod.from_str
289 IncorrectFormat(Box<dyn Error + Send + Sync + 'static>),
290
291 UnknownCommand(PrefixedBotCommand),
292 WrongBotName(BotName),
293
294 /// A custom error which you can return from your custom parser.
295 Custom(Box<dyn Error + Send + Sync + 'static>),
296}
297
298/// Command descriptions that can be shown to the user (e.g. as a part of
299/// `/help` message)
300///
301/// Most of the time you don't need to create this struct yourself as it's
302/// returned from [`BotCommands::descriptions`].
303#[derive(Debug, Clone)]
304pub struct CommandDescriptions<'a> {
305 global_description: Option<&'a str>,
306 descriptions: &'a [CommandDescription<'a>],
307 bot_username: Option<&'a str>,
308}
309
310/// Description of a particular command, used in [`CommandDescriptions`].
311#[derive(Debug, Clone)]
312pub struct CommandDescription<'a> {
313 /// Prefix of the command, usually `/`.
314 pub prefix: &'a str,
315 /// The command itself, e.g. `start`.
316 pub command: &'a str,
317 /// The command aliases, e.g. `["help", "h"]`.
318 pub aliases: &'a [&'a str],
319 /// Human-readable description of the command.
320 pub description: &'a str,
321}
322
323impl<'a> CommandDescriptions<'a> {
324 /// Creates new [`CommandDescriptions`] from a list of command descriptions.
325 #[must_use]
326 pub const fn new(descriptions: &'a [CommandDescription<'a>]) -> Self {
327 Self { global_description: None, descriptions, bot_username: None }
328 }
329
330 /// Sets the global description of these commands.
331 #[must_use]
332 pub fn global_description(self, global_description: &'a str) -> Self {
333 Self { global_description: Some(global_description), ..self }
334 }
335
336 /// Sets the username of the bot.
337 ///
338 /// After this method is called, returned instance of
339 /// [`CommandDescriptions`] will append `@bot_username` to all commands.
340 /// This is useful in groups, to disambiguate commands for different bots.
341 ///
342 /// ## Examples
343 ///
344 /// ```
345 /// use teloxide_ng::utils::command::{CommandDescription, CommandDescriptions};
346 ///
347 /// let descriptions = CommandDescriptions::new(&[
348 /// CommandDescription {
349 /// prefix: "/",
350 /// command: "start",
351 /// description: "start this bot",
352 /// aliases: &[],
353 /// },
354 /// CommandDescription {
355 /// prefix: "/",
356 /// command: "help",
357 /// description: "show this message",
358 /// aliases: &[],
359 /// },
360 /// ]);
361 ///
362 /// assert_eq!(descriptions.to_string(), "/start — start this bot\n/help — show this message");
363 /// assert_eq!(
364 /// descriptions.username("username_of_the_bot").to_string(),
365 /// "/start@username_of_the_bot — start this bot\n/help@username_of_the_bot — show this \
366 /// message"
367 /// );
368 /// ```
369 #[must_use]
370 pub fn username(self, bot_username: &'a str) -> Self {
371 Self { bot_username: Some(bot_username), ..self }
372 }
373
374 /// Sets the username of the bot.
375 ///
376 /// This is the same as [`username`], but uses value returned from `get_me`
377 /// method to get the username.
378 ///
379 /// [`username`]: self::CommandDescriptions::username
380 #[must_use]
381 pub fn username_from_me(self, me: &'a Me) -> CommandDescriptions<'a> {
382 self.username(me.user.username.as_deref().expect("Bots must have usernames"))
383 }
384}
385
386/// Parses a string into a command with args.
387///
388/// This function is just a shortcut for calling [`parse_command_with_prefix`]
389/// with the default prefix `/`.
390///
391/// ## Example
392/// ```
393/// use teloxide_ng::utils::command::parse_command;
394///
395/// let text = "/mute@my_admin_bot 5 hours";
396/// let (command, args) = parse_command(text, "my_admin_bot").unwrap();
397/// assert_eq!(command, "mute");
398/// assert_eq!(args, vec!["5", "hours"]);
399/// ```
400///
401/// If the name of a bot does not match, it will return `None`:
402/// ```
403/// use teloxide_ng::utils::command::parse_command;
404///
405/// let result = parse_command("/ban@MyNameBot1 3 hours", "MyNameBot2");
406/// assert!(result.is_none());
407/// ```
408///
409/// [`parse_command_with_prefix`]:
410/// crate::utils::command::parse_command_with_prefix
411pub fn parse_command<N>(text: &str, bot_name: N) -> Option<(&str, Vec<&str>)>
412where
413 N: AsRef<str>,
414{
415 parse_command_with_prefix("/", text, bot_name)
416}
417
418/// Parses a string into a command with args (custom prefix).
419///
420/// `prefix`: symbols, which denote start of a command.
421///
422/// ## Example
423/// ```
424/// use teloxide_ng::utils::command::parse_command_with_prefix;
425///
426/// let text = "!mute 5 hours";
427/// let (command, args) = parse_command_with_prefix("!", text, "").unwrap();
428/// assert_eq!(command, "mute");
429/// assert_eq!(args, vec!["5", "hours"]);
430/// ```
431///
432/// If the name of a bot does not match, it will return `None`:
433/// ```
434/// use teloxide_ng::utils::command::parse_command_with_prefix;
435///
436/// let result = parse_command_with_prefix("!", "!ban@MyNameBot1 3 hours", "MyNameBot2");
437/// assert!(result.is_none());
438/// ```
439pub fn parse_command_with_prefix<'a, N>(
440 prefix: &str,
441 text: &'a str,
442 bot_name: N,
443) -> Option<(&'a str, Vec<&'a str>)>
444where
445 N: AsRef<str>,
446{
447 if !text.starts_with(prefix) {
448 return None;
449 }
450 let mut words = text.split_whitespace();
451 let mut split = words.next()?[prefix.len()..].split('@');
452 let command = split.next()?;
453 let bot = split.next();
454 match bot {
455 Some(name) if name.eq_ignore_ascii_case(bot_name.as_ref()) => {}
456 None => {}
457 _ => return None,
458 }
459 Some((command, words.collect()))
460}
461
462impl Display for ParseError {
463 fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
464 match self {
465 ParseError::TooFewArguments { expected, found, message } => write!(
466 f,
467 "Too few arguments (expected {expected}, found {found}, message = '{message}')"
468 ),
469 ParseError::TooManyArguments { expected, found, message } => write!(
470 f,
471 "Too many arguments (expected {expected}, found {found}, message = '{message}')"
472 ),
473 ParseError::IncorrectFormat(e) => write!(f, "Incorrect format of command args: {e}"),
474 ParseError::UnknownCommand(e) => write!(f, "Unknown command: {e}"),
475 ParseError::WrongBotName(n) => write!(f, "Wrong bot name: {n}"),
476 ParseError::Custom(e) => write!(f, "{e}"),
477 }
478 }
479}
480
481impl std::error::Error for ParseError {}
482
483impl Display for CommandDescriptions<'_> {
484 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
485 if let Some(global_description) = self.global_description {
486 f.write_str(global_description)?;
487 f.write_str("\n\n")?;
488 }
489
490 let format_command = |command: &str, prefix: &str, formater: &mut fmt::Formatter<'_>| {
491 formater.write_str(prefix)?;
492 formater.write_str(command)?;
493 if let Some(username) = self.bot_username {
494 formater.write_char('@')?;
495 formater.write_str(username)?;
496 }
497 fmt::Result::Ok(())
498 };
499
500 let mut write = |&CommandDescription { prefix, command, aliases, description }, nls| {
501 if nls {
502 f.write_char('\n')?;
503 }
504
505 format_command(command, prefix, f)?;
506 for alias in aliases {
507 f.write_str(", ")?;
508 format_command(alias, prefix, f)?;
509 }
510
511 if !description.is_empty() {
512 f.write_str(" — ")?;
513 f.write_str(description)?;
514 }
515
516 fmt::Result::Ok(())
517 };
518
519 if let Some(descr) = self.descriptions.first() {
520 write(descr, false)?;
521 for descr in &self.descriptions[1..] {
522 write(descr, true)?;
523 }
524 }
525
526 Ok(())
527 }
528}
529
530// The rest of tests are integration due to problems with macro expansion in
531// unit tests.
532#[cfg(test)]
533mod tests {
534 use super::*;
535
536 #[test]
537 fn parse_command_with_args_() {
538 let data = "/command arg1 arg2";
539 let expected = Some(("command", vec!["arg1", "arg2"]));
540 let actual = parse_command(data, "");
541 assert_eq!(actual, expected)
542 }
543
544 #[test]
545 fn parse_command_with_args_without_args() {
546 let data = "/command";
547 let expected = Some(("command", vec![]));
548 let actual = parse_command(data, "");
549 assert_eq!(actual, expected)
550 }
551}