From 8f24aa391f6b8a9103a9c105138c7610288acb05 Mon Sep 17 00:00:00 2001 From: Illia Date: Fri, 9 Dec 2016 23:30:30 +0200 Subject: [PATCH] Command builder, quoted args, and multi-prefixes Add a command builder, which can take arguments such as multiple checks, quoted arguments, and multiple prefix support, as well as dynamic prefixes per context. --- examples/06_command_framework/src/main.rs | 52 +++------ src/ext/framework/command.rs | 62 ++++++++-- src/ext/framework/configuration.rs | 27 ++++- src/ext/framework/create_command.rs | 131 ++++++++++++++++++++++ src/ext/framework/mod.rs | 90 ++++++++++++--- src/utils/mod.rs | 46 ++++++++ tests/test_parsers.rs | 6 + 7 files changed, 346 insertions(+), 68 deletions(-) create mode 100644 src/ext/framework/create_command.rs diff --git a/examples/06_command_framework/src/main.rs b/examples/06_command_framework/src/main.rs index 0943d8106e6..408fb9bc15c 100644 --- a/examples/06_command_framework/src/main.rs +++ b/examples/06_command_framework/src/main.rs @@ -37,7 +37,7 @@ fn main() { data.insert::(HashMap::default()); } - client.on_ready(|_context, ready| { + client.on_ready(|_, ready| { println!("{} is connected!", ready.user.name); }); @@ -81,22 +81,20 @@ fn main() { }) // Very similar to `before`, except this will be called directly _after_ // command execution. - .after(|_context, _message, command_name| { + .after(|_, _, command_name| { println!("Processed command '{}'", command_name) }) - .on("commands", commands) - .set_check("commands", owner_check) - .on("ping", ping_command) - .set_check("ping", owner_check) // Ensure only the owner can run this - .on("emoji cat", cat_command) - .on("emoji dog", dog_command) - .on("multiply", multiply) - .on("some long command", some_long_command) - // Commands can be in closure-form as well. - // - // This is not recommended though, as any closure larger than a couple - // lines will look ugly. - .on("about", |context, _message, _args| drop(context.say("A test bot")))); + .command("about", |c| c.exec_str("A test bot")) + .command("commands", |c| c + .check(owner_check) + .exec(commands)) + .command("emoji cat", |c| c.exec_str(":cat:")) + .command("emoji dog", |c| c.exec_str(":dog:")) + .command("multiply", |c| c.exec(multiply)) + .command("ping", |c| c + .check(owner_check) + .exec_str("Pong!")) + .command("some long command", |c| c.exec(some_long_command))); if let Err(why) = client.start() { println!("Client error: {:?}", why); @@ -109,12 +107,6 @@ fn main() { // This may bring more features available for commands in the future. See the // "multiply" command below for some of the power that the `command!` macro can // bring. -command!(cat_command(context, _msg, _args) { - if let Err(why) = context.say(":cat:") { - println!("Eror sending message: {:?}", why); - } -}); - command!(commands(context, _msg, _args) { let mut contents = "Commands used:\n".to_owned(); @@ -130,29 +122,17 @@ command!(commands(context, _msg, _args) { } }); -fn dog_command(context: &Context, _msg: &Message, _args: Vec) { - if let Err(why) = context.say(":dog:") { - println!("Error sending message: {:?}", why); - } -} - -fn ping_command(_context: &Context, message: &Message, _args: Vec) { - if let Err(why) = message.reply("Pong!") { - println!("Error sending reply: {:?}", why); - } -} - // A function which acts as a "check", to determine whether to call a command. // // In this case, this command checks to ensure you are the owner of the message // in order for the command to be executed. If the check fails, the command is // not called. -fn owner_check(_context: &Context, message: &Message) -> bool { +fn owner_check(_: &Context, message: &Message) -> bool { // Replace 7 with your ID message.author.id == 7 } -fn some_long_command(context: &Context, _msg: &Message, args: Vec) { +fn some_long_command(context: &Context, _: &Message, args: Vec) { if let Err(why) = context.say(&format!("Arguments: {:?}", args)) { println!("Error sending message: {:?}", why); } @@ -178,7 +158,7 @@ fn some_long_command(context: &Context, _msg: &Message, args: Vec) { // will be ignored. // // Argument type overloading is currently not supported. -command!(multiply(context, _message, args, first: f64, second: f64) { +command!(multiply(context, _msg, args, first: f64, second: f64) { let res = first * second; if let Err(why) = context.say(&res.to_string()) { diff --git a/src/ext/framework/command.rs b/src/ext/framework/command.rs index 46338a1cc01..225f50a3ef4 100644 --- a/src/ext/framework/command.rs +++ b/src/ext/framework/command.rs @@ -2,28 +2,66 @@ use std::sync::Arc; use super::Configuration; use ::client::Context; use ::model::Message; +use std::collections::HashMap; + +/// Command function type. Allows to access internal framework things inside +/// your commands. +pub enum CommandType { + StringResponse(String), + Basic(Box) + Send + Sync + 'static>), + WithCommands(Box>, Vec) + Send + Sync + 'static>) +} + +/// Command struct used to store commands internally. +pub struct Command { + /// A set of checks to be called prior to executing the command. The checks + /// will short-circuit on the first check that returns `false`. + pub checks: Vec bool + Send + Sync + 'static>>, + /// Function called when the command is called. + pub exec: CommandType, + /// Command description, used by other commands. + pub desc: Option, + /// Command usage schema, used by other commands. + pub usage: Option, + /// Whether arguments should be parsed using quote parser or not. + pub use_quotes: bool, +} -#[doc(hidden)] -pub type Command = Fn(&Context, &Message, Vec) + Send + Sync; #[doc(hidden)] pub type InternalCommand = Arc; -pub fn positions(content: &str, conf: &Configuration) -> Option> { - if let Some(ref prefix) = conf.prefix { +pub fn positions(ctx: &Context, content: &str, conf: &Configuration) -> Option> { + if conf.prefixes.len() > 0 || conf.dynamic_prefix.is_some() { // Find out if they were mentioned. If not, determine if the prefix // was used. If not, return None. - let mut positions = if let Some(mention_end) = find_mention_end(content, conf) { - vec![mention_end] - } else if content.starts_with(prefix) { - vec![prefix.len()] + let mut positions: Vec = vec![]; + + if let Some(mention_end) = find_mention_end(&content, conf) { + positions.push(mention_end); + } else if let Some(ref func) = conf.dynamic_prefix { + if let Some(x) = func(&ctx) { + positions.push(x.len()); + } else { + for n in conf.prefixes.clone() { + if content.starts_with(&n) { + positions.push(n.len()); + } + } + } } else { - return None; + for n in conf.prefixes.clone() { + if content.starts_with(&n) { + positions.push(n.len()); + } + } }; + if positions.len() == 0 { + return None; + } + if conf.allow_whitespace { - let pos = *unsafe { - positions.get_unchecked(0) - }; + let pos = *unsafe { positions.get_unchecked(0) }; positions.insert(0, pos + 1); } diff --git a/src/ext/framework/configuration.rs b/src/ext/framework/configuration.rs index 6a6c5e5a198..7af38eda56d 100644 --- a/src/ext/framework/configuration.rs +++ b/src/ext/framework/configuration.rs @@ -1,5 +1,6 @@ use std::default::Default; use ::client::rest; +use ::client::Context; /// The configuration to use for a [`Framework`] associated with a [`Client`] /// instance. @@ -31,7 +32,9 @@ pub struct Configuration { #[doc(hidden)] pub allow_whitespace: bool, #[doc(hidden)] - pub prefix: Option, + pub prefixes: Vec, + #[doc(hidden)] + pub dynamic_prefix: Option Option + Send + Sync + 'static>> } impl Configuration { @@ -119,7 +122,24 @@ impl Configuration { /// Sets the prefix to respond to. This can either be a single-char or /// multi-char string. pub fn prefix>(mut self, prefix: S) -> Self { - self.prefix = Some(prefix.into()); + self.prefixes = vec![prefix.into()]; + + self + } + + /// Sets the prefix to respond to. This can either be a single-char or + /// multi-char string. + pub fn prefixes(mut self, prefixes: Vec<&str>) -> Self { + self.prefixes = prefixes.iter().map(|x| x.to_string()).collect(); + + self + } + + /// Sets the prefix to respond to. This can either be a single-char or + /// multi-char string. + pub fn dynamic_prefix(mut self, dynamic_prefix: F) -> Self + where F: Fn(&Context) -> Option + Send + Sync + 'static { + self.dynamic_prefix = Some(Box::new(dynamic_prefix)); self } @@ -137,7 +157,8 @@ impl Default for Configuration { depth: 5, on_mention: None, allow_whitespace: false, - prefix: None, + prefixes: vec![], + dynamic_prefix: None } } } diff --git a/src/ext/framework/create_command.rs b/src/ext/framework/create_command.rs new file mode 100644 index 00000000000..824c3ddf04b --- /dev/null +++ b/src/ext/framework/create_command.rs @@ -0,0 +1,131 @@ +pub use ext::framework::command::{Command, CommandType}; + +use std::collections::HashMap; +use std::default::Default; +use std::sync::Arc; +use ::client::Context; +use ::model::Message; + +pub struct CreateCommand(pub Command); + +impl CreateCommand { + /// Adds a "check" to a command, which checks whether or not the command's + /// function should be called. + /// + /// # Examples + /// + /// Ensure that the user who created a message, calling a "ping" command, + /// is the owner. + /// + /// ```rust,no_run + /// use serenity::client::{Client, Context}; + /// use serenity::model::Message; + /// use std::env; + /// + /// let mut client = Client::login_bot(&env::var("DISCORD_TOKEN").unwrap()); + /// + /// client.with_framework(|f| f + /// .configure(|c| c.prefix("~")) + /// .command("ping", |c| c + /// .check(owner_check) + /// .desc("Replies to a ping with a pong") + /// .exec(ping))); + /// + /// fn ping(context: &Context, _message: &Message, _args: Vec) { + /// context.say("Pong!"); + /// } + /// + /// fn owner_check(_context: &Context, message: &Message) -> bool { + /// // replace with your user ID + /// message.author.id == 7 + /// } + /// ``` + pub fn check(mut self, check: F) -> Self + where F: Fn(&Context, &Message) -> bool + Send + Sync + 'static { + self.0.checks.push(Box::new(check)); + + self + } + + /// Description, used by other commands. + pub fn desc(mut self, desc: &str) -> Self { + self.0.desc = Some(desc.to_owned()); + + self + } + + /// A function that can be called when a command is received. + /// + /// See [`exec_str`] if you _only_ need to return a string on command use. + /// + /// [`exec_str`]: #method.exec_str + pub fn exec(mut self, func: F) -> Self + where F: Fn(&Context, &Message, Vec) + Send + Sync + 'static { + self.0.exec = CommandType::Basic(Box::new(func)); + + self + } + + /// Sets a function that's called when a command is called that can access + /// the internal HashMap of usages, used specifically for creating a help + /// command. + pub fn exec_help(mut self, f: F) -> Self + where F: Fn(&Context, &Message, HashMap>, Vec) + Send + Sync + 'static { + self.0.exec = CommandType::WithCommands(Box::new(f)); + + self + } + + /// Sets a string to be sent in the channel of context on command. This can + /// be useful for an `about`, `invite`, `ping`, etc. command. + /// + /// # Examples + /// + /// Create a command named "ping" that returns "Pong!": + /// + /// ```rust,ignore + /// client.with_framework(|f| f + /// .command("ping", |c| c.exec_str("Pong!"))); + /// ``` + pub fn exec_str(mut self, content: &str) -> Self { + self.0.exec = CommandType::StringResponse(content.to_owned()); + + self + } + + /// Command usage schema, used by other commands. + pub fn usage(mut self, usage: &str) -> Self { + self.0.usage = Some(usage.to_owned()); + + self + } + + /// Whether or not arguments should be parsed using the quotation parser. + /// + /// Enabling this will parse `~command "this is arg 1" "this is arg 2"` as + /// two arguments: `this is arg 1` and `this is arg 2`. + /// + /// Disabling this will parse `~command "this is arg 1" "this is arg 2"` as + /// eight arguments: `"this`, `is`, `arg`, `1"`, `"this`, `is`, `arg`, `2"`. + /// + /// Refer to [`utils::parse_quotes`] for information on the parser. + /// + /// [`utils::parse_quotes`]: ../../utils/fn.parse_quotes.html + pub fn use_quotes(mut self, use_quotes: bool) -> Self { + self.0.use_quotes = use_quotes; + + self + } +} + +impl Default for Command { + fn default() -> Command { + Command { + checks: Vec::default(), + exec: CommandType::Basic(Box::new(|_, _, _| {})), + desc: None, + usage: None, + use_quotes: true, + } + } +} diff --git a/src/ext/framework/mod.rs b/src/ext/framework/mod.rs index 46b80e22eb9..7458223bbeb 100644 --- a/src/ext/framework/mod.rs +++ b/src/ext/framework/mod.rs @@ -39,8 +39,8 @@ //! //! client.with_framework(|f| f //! .configure(|c| c.prefix("~")) -//! .on("about", about) -//! .on("ping", ping)); +//! .command("about", |c| c.exec_str("A simple test bot")) +//! .command("ping", |c| c.exec(ping))); //! //! fn about(context: &Context, _message: &Message, _args: Vec) { //! let _ = context.say("A simple test bot"); @@ -55,9 +55,11 @@ mod command; mod configuration; +mod create_command; -pub use self::command::Command; +pub use self::command::{Command, CommandType}; pub use self::configuration::Configuration; +pub use self::create_command::CreateCommand; use self::command::InternalCommand; use std::collections::HashMap; @@ -65,6 +67,7 @@ use std::sync::Arc; use std::thread; use ::client::Context; use ::model::Message; +use ::utils; /// A macro to generate "named parameters". This is useful to avoid manually /// using the "arguments" parameter and manually parsing types. @@ -137,7 +140,6 @@ pub struct Framework { commands: HashMap, before: Option>, after: Option>, - checks: HashMap bool + Send + Sync + 'static>>, /// Whether the framework has been "initialized". /// /// The framework is initialized once one of the following occurs: @@ -192,7 +194,7 @@ impl Framework { #[doc(hidden)] pub fn dispatch(&mut self, context: Context, message: Message) { - let res = command::positions(&message.content, &self.configuration); + let res = command::positions(&context, &message.content, &self.configuration); let positions = match res { Some(positions) => positions, @@ -206,7 +208,7 @@ impl Framework { return; } - for position in positions { + 'outer: for position in positions { let mut built = String::new(); for i in 0..self.configuration.depth { @@ -228,27 +230,42 @@ impl Framework { }); if let Some(command) = self.commands.get(&built) { - if let Some(check) = self.checks.get(&built) { + for check in &command.checks { if !(check)(&context, &message) { - continue; + continue 'outer; } } let before = self.before.clone(); let command = command.clone(); let after = self.after.clone(); + let commands = self.commands.clone(); thread::spawn(move || { if let Some(before) = before { (before)(&context, &message, &built); } - let args = message.content[position + built.len()..] - .split_whitespace() - .map(|arg| arg.to_owned()) - .collect::>(); + let args = if command.use_quotes { + utils::parse_quotes(&message.content[position + built.len()..]) + } else { + message.content[position + built.len()..] + .split_whitespace() + .map(|arg| arg.to_owned()) + .collect::>() + }; - (command)(&context, &message, args); + match command.exec { + CommandType::StringResponse(ref x) => { + let _ = &context.say(x); + }, + CommandType::Basic(ref x) => { + (x)(&context, &message, args); + }, + CommandType::WithCommands(ref x) => { + (x)(&context, &message, commands, args); + } + } if let Some(after) = after { (after)(&context, &message, &built); @@ -267,20 +284,54 @@ impl Framework { /// This requires that a check - if one exists - passes, prior to being /// called. /// + /// Note that once v0.2.0 lands, you will need to use the command builder + /// via the [`command`] method to set checks. This command will otherwise + /// only be for simple commands. + /// /// Refer to the [module-level documentation] for more information and /// usage. /// + /// [`command`]: #method.command /// [module-level documentation]: index.html pub fn on(mut self, command_name: S, f: F) -> Self where F: Fn(&Context, &Message, Vec) + Send + Sync + 'static, S: Into { - self.commands.insert(command_name.into(), Arc::new(f)); + self.commands.insert(command_name.into(), Arc::new(Command { + checks: Vec::default(), + exec: CommandType::Basic(Box::new(f)), + desc: None, + usage: None, + use_quotes: false, + })); + + self.initialized = true; + + self + } + + /// Adds a command using command builder. + /// + /// # Examples + /// + /// ```rust,ignore + /// framework.command("ping", |c| c + /// .description("Responds with 'pong'.") + /// .exec(|ctx, _, _| { + /// let _ = ctx.say("pong"); + /// })); + /// ``` + pub fn command(mut self, command_name: S, f: F) -> Self + where F: FnOnce(CreateCommand) -> CreateCommand, + S: Into { + let cmd = f(CreateCommand(Command::default())).0; + self.commands.insert(command_name.into(), Arc::new(cmd)); + self.initialized = true; self } - /// This will call given closure before every command's execution + /// Specify the function to be called prior to every command's execution. pub fn before(mut self, f: F) -> Self where F: Fn(&Context, &Message, &String) + Send + Sync + 'static { self.before = Some(Arc::new(f)); @@ -288,7 +339,7 @@ impl Framework { self } - /// This will call given closure after every command's execution + /// Specify the function to be called after every command's execution. pub fn after(mut self, f: F) -> Self where F: Fn(&Context, &Message, &String) + Send + Sync + 'static { self.after = Some(Arc::new(f)); @@ -325,10 +376,15 @@ impl Framework { /// message.author.id == 7 /// } /// ``` + #[deprecated(since="0.1.2", note="Use the `CreateCommand` builder's `check` instead.")] pub fn set_check(mut self, command: S, check: F) -> Self where F: Fn(&Context, &Message) -> bool + Send + Sync + 'static, S: Into { - self.checks.insert(command.into(), Arc::new(check)); + if let Some(command) = self.commands.get_mut(&command.into()) { + if let Some(c) = Arc::get_mut(command) { + c.checks.push(Box::new(check)); + } + } self } diff --git a/src/utils/mod.rs b/src/utils/mod.rs index a7a14a3f2e9..fbc642fda75 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -198,3 +198,49 @@ pub fn read_image>(path: P) -> Result { Ok(format!("data:image/{};base64,{}", ext, b64)) } + +/// Turns a string into a vector of string arguments, splitting by spaces, but +/// parsing content within quotes as one individual argument. +pub fn parse_quotes(s: &str) -> Vec { + let mut args = vec![]; + let mut in_string = false; + let mut current_str = String::default(); + + for x in s.chars() { + if in_string { + if x == '"' { + if !current_str.is_empty() { + args.push(current_str); + } + + current_str = String::default(); + in_string = false; + } else { + current_str.push(x); + } + } else { + if x == ' ' { + if !current_str.is_empty() { + args.push(current_str.clone()); + } + + current_str = String::default(); + } else if x == '"' { + if !current_str.is_empty() { + args.push(current_str.clone()); + } + + in_string = true; + current_str = String::default(); + } else { + current_str.push(x); + } + } + } + + if !current_str.is_empty() { + args.push(current_str); + } + + args +} diff --git a/tests/test_parsers.rs b/tests/test_parsers.rs index 479ac93df6c..768c7c41756 100644 --- a/tests/test_parsers.rs +++ b/tests/test_parsers.rs @@ -31,3 +31,9 @@ fn emoji_parser() { assert_eq!(emoji.name, "name"); assert_eq!(emoji.id, 12345); } + +#[test] +fn quote_parser() { + let parsed = parse_quotes("a \"b c\" d\"e f\" g"); + assert_eq!(parsed, ["a", "b c", "d", "e f", "g"]); +}