Merge pull request #169 from teloxide/fix_parse_command

Fixed command parsing
This commit is contained in:
Temirkhan Myrzamadi 2020-02-23 20:35:07 +06:00 committed by GitHub
commit 196528af2b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 120 additions and 49 deletions

View file

@ -46,7 +46,7 @@ pin-project = "0.4.6"
serde_with_macros = "1.0.1"
either = "1.5.3"
teloxide-macros = "0.1.0"
teloxide-macros = { path = "teloxide-macros" }
[dev-dependencies]
smart-default = "0.6.0"

View file

@ -136,7 +136,7 @@ async fn answer(
async fn handle_commands(rx: DispatcherHandlerRx<Message>) {
// Only iterate through commands in a proper format:
rx.commands::<Command>()
rx.commands::<Command, &str>(panic!("Insert here your bot's name"))
// Execute all incoming commands concurrently:
.for_each_concurrent(None, |(cx, command, _)| async move {
answer(cx, command).await.log_on_error().await;

View file

@ -179,7 +179,7 @@ async fn handle_commands(rx: DispatcherHandlerRx<Message>) {
// Only iterate through messages from groups:
rx.filter(|cx| future::ready(cx.update.chat.is_group()))
// Only iterate through commands in a proper format:
.commands::<Command>()
.commands::<Command, &str>(panic!("Insert here your bot's name"))
// Execute all incoming commands concurrently:
.for_each_concurrent(None, |(cx, command, args)| async move {
action(cx, command, &args).await.log_on_error().await;

View file

@ -32,7 +32,7 @@ async fn answer(
async fn handle_commands(rx: DispatcherHandlerRx<Message>) {
// Only iterate through commands in a proper format:
rx.commands::<Command>()
rx.commands::<Command, &str>(panic!("Insert here your bot's name"))
// Execute all incoming commands concurrently:
.for_each_concurrent(None, |(cx, command, _)| async move {
answer(cx, command).await.log_on_error().await;

View file

@ -16,12 +16,14 @@ pub trait DispatcherHandlerRxExt {
/// Extracts only commands with their arguments from this stream of
/// arbitrary messages.
fn commands<C>(
fn commands<C, N>(
self,
bot_name: N,
) -> BoxStream<'static, (DispatcherHandlerCx<Message>, C, Vec<String>)>
where
Self: Stream<Item = DispatcherHandlerCx<Message>>,
C: BotCommand;
C: BotCommand,
N: Into<String> + Send;
}
impl<T> DispatcherHandlerRxExt for T
@ -39,23 +41,31 @@ where
}))
}
fn commands<C>(
fn commands<C, N>(
self,
bot_name: N,
) -> BoxStream<'static, (DispatcherHandlerCx<Message>, C, Vec<String>)>
where
Self: Stream<Item = DispatcherHandlerCx<Message>>,
C: BotCommand,
N: Into<String> + Send,
{
Box::pin(self.text_messages().filter_map(|(cx, text)| async move {
C::parse(&text).map(|(command, args)| {
(
cx,
command,
args.into_iter()
.map(ToOwned::to_owned)
.collect::<Vec<String>>(),
)
})
let bot_name = bot_name.into();
Box::pin(self.text_messages().filter_map(move |(cx, text)| {
let bot_name = bot_name.clone();
async move {
C::parse(&text, &bot_name).map(|(command, args)| {
(
cx,
command,
args.into_iter()
.map(ToOwned::to_owned)
.collect::<Vec<String>>(),
)
})
}
}))
}
}

View file

@ -72,6 +72,7 @@
//! ([Full](https://github.com/teloxide/teloxide/blob/master/examples/simple_commands_bot/src/main.rs))
//! ```no_run
//! // Imports are omitted...
//! # #[allow(unreachable_code)]
//! # use teloxide::{prelude::*, utils::command::BotCommand};
//! # use rand::{thread_rng, Rng};
//!
@ -108,7 +109,7 @@
//!
//! async fn handle_commands(rx: DispatcherHandlerRx<Message>) {
//! // Only iterate through commands in a proper format:
//! rx.commands::<Command>()
//! rx.commands::<Command, &str>(panic!("Insert here your bot's name"))
//! // Execute all incoming commands concurrently:
//! .for_each_concurrent(None, |(cx, command, _)| async move {
//! answer(cx, command).await.log_on_error().await;

View file

@ -15,7 +15,8 @@
//! Ban,
//! }
//!
//! let (command, args) = AdminCommand::parse("/ban 3 hours").unwrap();
//! let (command, args) =
//! AdminCommand::parse("/ban 3 hours", "bot_name").unwrap();
//! assert_eq!(command, AdminCommand::Ban);
//! assert_eq!(args, vec!["3", "hours"]);
//! ```
@ -24,7 +25,7 @@
//! ```
//! use teloxide::utils::command::parse_command;
//!
//! let (command, args) = parse_command("/ban 3 hours").unwrap();
//! let (command, args) = parse_command("/ban 3 hours", "").unwrap();
//! assert_eq!(command, "/ban");
//! assert_eq!(args, vec!["3", "hours"]);
//! ```
@ -34,11 +35,19 @@
//! use teloxide::utils::command::parse_command_with_prefix;
//!
//! let text = "!ban 3 hours";
//! let (command, args) = parse_command_with_prefix("!", text).unwrap();
//! let (command, args) = parse_command_with_prefix("!", text, "").unwrap();
//! assert_eq!(command, "ban");
//! assert_eq!(args, vec!["3", "hours"]);
//! ```
//!
//! If the name of a bot does not match, it will return `None`:
//! ```
//! use teloxide::utils::command::parse_command;
//!
//! let result = parse_command("/ban@MyNameBot1 3 hours", "MyNameBot2");
//! assert!(result.is_none());
//! ```
//!
//! See [examples/admin_bot] as a more complicated examples.
//!
//! [`parse_command`]: crate::utils::command::parse_command
@ -61,7 +70,7 @@ pub use teloxide_macros::BotCommand;
/// Ban,
/// }
///
/// let (command, args) = AdminCommand::parse("/ban 5 h").unwrap();
/// let (command, args) = AdminCommand::parse("/ban 5 h", "bot_name").unwrap();
/// assert_eq!(command, AdminCommand::Ban);
/// assert_eq!(args, vec!["5", "h"]);
/// ```
@ -92,7 +101,9 @@ pub use teloxide_macros::BotCommand;
pub trait BotCommand: Sized {
fn try_from(s: &str) -> Option<Self>;
fn descriptions() -> String;
fn parse(s: &str) -> Option<(Self, Vec<&str>)>;
fn parse<N>(s: &str, bot_name: N) -> Option<(Self, Vec<&str>)>
where
N: Into<String>;
}
/// Parses a string into a command with args.
@ -103,14 +114,24 @@ pub trait BotCommand: Sized {
/// ```
/// use teloxide::utils::command::parse_command;
///
/// let text = "/mute 5 hours";
/// let (command, args) = parse_command(text).unwrap();
/// let text = "/mute@my_admin_bot 5 hours";
/// let (command, args) = parse_command(text, "my_admin_bot").unwrap();
/// assert_eq!(command, "/mute");
/// assert_eq!(args, vec!["5", "hours"]);
/// ```
pub fn parse_command(text: &str) -> Option<(&str, Vec<&str>)> {
pub fn parse_command<N>(text: &str, bot_name: N) -> Option<(&str, Vec<&str>)>
where
N: AsRef<str>,
{
let mut words = text.split_whitespace();
let command = words.next()?;
let mut splited = words.next()?.split('@');
let command = splited.next()?;
let bot = splited.next();
match bot {
Some(name) if name == bot_name.as_ref() => {}
None => {}
_ => return None,
}
Some((command, words.collect()))
}
@ -123,19 +144,30 @@ pub fn parse_command(text: &str) -> Option<(&str, Vec<&str>)> {
/// use teloxide::utils::command::parse_command_with_prefix;
///
/// let text = "!mute 5 hours";
/// let (command, args) = parse_command_with_prefix("!", text).unwrap();
/// let (command, args) = parse_command_with_prefix("!", text, "").unwrap();
/// assert_eq!(command, "mute");
/// assert_eq!(args, vec!["5", "hours"]);
/// ```
pub fn parse_command_with_prefix<'a>(
pub fn parse_command_with_prefix<'a, N>(
prefix: &str,
text: &'a str,
) -> Option<(&'a str, Vec<&'a str>)> {
bot_name: N,
) -> Option<(&'a str, Vec<&'a str>)>
where
N: AsRef<str>,
{
if !text.starts_with(prefix) {
return None;
}
let mut words = text.split_whitespace();
let command = &words.next()?[prefix.len()..];
let mut splited = words.next()?[prefix.len()..].split('@');
let command = splited.next()?;
let bot = splited.next();
match bot {
Some(name) if name == bot_name.as_ref() => {}
None => {}
_ => return None,
}
Some((command, words.collect()))
}
@ -147,7 +179,7 @@ mod tests {
fn parse_command_with_args_() {
let data = "/command arg1 arg2";
let expected = Some(("/command", vec!["arg1", "arg2"]));
let actual = parse_command(data);
let actual = parse_command(data, "");
assert_eq!(actual, expected)
}
@ -155,7 +187,7 @@ mod tests {
fn parse_command_with_args_without_args() {
let data = "/command";
let expected = Some(("/command", vec![]));
let actual = parse_command(data);
let actual = parse_command(data, "");
assert_eq!(actual, expected)
}
@ -170,7 +202,7 @@ mod tests {
let data = "/start arg1 arg2";
let expected = Some((DefaultCommands::Start, vec!["arg1", "arg2"]));
let actual = DefaultCommands::parse(data);
let actual = DefaultCommands::parse(data, "");
assert_eq!(actual, expected)
}
@ -186,7 +218,7 @@ mod tests {
let data = "!start arg1 arg2";
let expected = Some((DefaultCommands::Start, vec!["arg1", "arg2"]));
let actual = DefaultCommands::parse(data);
let actual = DefaultCommands::parse(data, "");
assert_eq!(actual, expected)
}
@ -202,12 +234,9 @@ mod tests {
assert_eq!(
DefaultCommands::Start,
DefaultCommands::parse("!start").unwrap().0
);
assert_eq!(
DefaultCommands::descriptions(),
"!start - desc\n/help - \n"
DefaultCommands::parse("!start", "").unwrap().0
);
assert_eq!(DefaultCommands::descriptions(), "!start - desc\n/help\n");
}
#[test]
@ -226,15 +255,31 @@ mod tests {
assert_eq!(
DefaultCommands::Start,
DefaultCommands::parse("/start").unwrap().0
DefaultCommands::parse("/start", "MyNameBot").unwrap().0
);
assert_eq!(
DefaultCommands::Help,
DefaultCommands::parse("!help").unwrap().0
DefaultCommands::parse("!help", "MyNameBot").unwrap().0
);
assert_eq!(
DefaultCommands::descriptions(),
"Bot commands\n/start - \n!help - \n"
"Bot commands\n/start\n!help\n"
);
}
#[test]
fn parse_command_with_bot_name() {
#[command(rename = "lowercase")]
#[derive(BotCommand, Debug, PartialEq)]
enum DefaultCommands {
#[command(prefix = "/")]
Start,
Help,
}
assert_eq!(
DefaultCommands::Start,
DefaultCommands::parse("/start@MyNameBot", "MyNameBot").unwrap().0
);
}
}

View file

@ -82,9 +82,12 @@ pub fn derive_telegram_command_enum(tokens: TokenStream) -> TokenStream {
.zip(variant_name)
.map(|(prefix, command)| prefix.to_string() + command.as_str());
let variant_str2 = variant_str1.clone();
let variant_description = variant_infos
.iter()
.map(|info| info.description.as_deref().unwrap_or(""));
let variant_description = variant_infos.iter().map(|info| {
info.description
.as_deref()
.map(|e| format!(" - {}", e))
.unwrap_or(String::new())
});
let ident = &input.ident;
@ -105,11 +108,23 @@ pub fn derive_telegram_command_enum(tokens: TokenStream) -> TokenStream {
}
}
fn descriptions() -> String {
std::concat!(#global_description #(#variant_str2, " - ", #variant_description, '\n'),*).to_string()
std::concat!(#global_description #(#variant_str2, #variant_description, '\n'),*).to_string()
}
fn parse(s: &str) -> Option<(Self, Vec<&str>)> {
fn parse<N>(s: &str, bot_name: N) -> Option<(Self, Vec<&str>)>
where
N: Into<String>
{
let mut words = s.split_whitespace();
let command = Self::try_from(words.next()?)?;
let mut splited = words.next()?.split('@');
let command_raw = splited.next()?;
let bot = splited.next();
let bot_name = bot_name.into();
match bot {
Some(name) if name == bot_name => {}
None => {}
_ => return None,
}
let command = Self::try_from(command_raw)?;
Some((command, words.collect()))
}
}