diff options
| author | Illia <[email protected]> | 2016-12-13 21:26:29 +0200 |
|---|---|---|
| committer | zeyla <[email protected]> | 2016-12-13 11:26:29 -0800 |
| commit | daf92eda815b8f539f6d759ab48cf7a70513915f (patch) | |
| tree | 36145f5095e7af6fb725635dd104e9d9d3f0ea62 /src/ext/framework | |
| parent | Fix readme typo (diff) | |
| download | serenity-daf92eda815b8f539f6d759ab48cf7a70513915f.tar.xz serenity-daf92eda815b8f539f6d759ab48cf7a70513915f.zip | |
Implement command groups and buckets
* Implement command groups
* change to ref mut
* Implement framework API.
* Remove commands field
* Make it all work
* Make example use command groups
* Requested changes
* Implement adding buckets
* Add ratelimit check function
* Finish everything
* Fix voice example
* Actually fix it
* Fix doc tests
* Switch to result
* Savage examples
* Fix docs
* Fixes
* Accidental push
* 👀
* Fix an example
* fix some example
* Small cleanup
* Abstract ratelimit bucket logic
Diffstat (limited to 'src/ext/framework')
| -rw-r--r-- | src/ext/framework/buckets.rs | 60 | ||||
| -rw-r--r-- | src/ext/framework/command.rs | 33 | ||||
| -rw-r--r-- | src/ext/framework/configuration.rs | 90 | ||||
| -rw-r--r-- | src/ext/framework/create_command.rs | 25 | ||||
| -rw-r--r-- | src/ext/framework/create_group.rs | 58 | ||||
| -rw-r--r-- | src/ext/framework/help_commands.rs | 229 | ||||
| -rw-r--r-- | src/ext/framework/mod.rs | 289 |
7 files changed, 703 insertions, 81 deletions
diff --git a/src/ext/framework/buckets.rs b/src/ext/framework/buckets.rs new file mode 100644 index 0000000..2e42250 --- /dev/null +++ b/src/ext/framework/buckets.rs @@ -0,0 +1,60 @@ +use std::collections::HashMap; +use std::default::Default; +use time; + +#[doc(hidden)] +pub struct Ratelimit { + pub delay: i64, + pub limit: Option<(i64, i32)>, +} + +#[doc(hidden)] +pub struct MemberRatelimit { + pub count: i32, + pub last_time: i64, + pub set_time: i64, +} + +impl Default for MemberRatelimit { + fn default() -> Self { + MemberRatelimit { + count: 0, + last_time: 0, + set_time: 0, + } + } +} + +#[doc(hidden)] +pub struct Bucket { + pub ratelimit: Ratelimit, + pub limits: HashMap<u64, MemberRatelimit>, +} + +impl Bucket { + pub fn take(&mut self, user_id: u64) -> i64 { + let time =- time::get_time().sec; + let member = self.limits.entry(user_id) + .or_insert_with(MemberRatelimit::default); + + if let Some((timespan, limit)) = self.ratelimit.limit { + if (member.count + 1) > limit { + if time < (member.set_time + timespan) { + return (member.set_time + timespan) - time; + } else { + member.count = 0; + member.set_time = time; + } + } + } + + if time < member.last_time + self.ratelimit.delay { + (member.last_time + self.ratelimit.delay) - time + } else { + member.count += 1; + member.last_time = time; + + 0 + } + } +} diff --git a/src/ext/framework/command.rs b/src/ext/framework/command.rs index 204c90c..c1e9197 100644 --- a/src/ext/framework/command.rs +++ b/src/ext/framework/command.rs @@ -6,9 +6,10 @@ use ::model::Permissions; use std::collections::HashMap; pub type Check = Fn(&Context, &Message) -> bool + Send + Sync + 'static; -pub type Exec = Fn(&Context, &Message, Vec<String>) + Send + Sync + 'static; -pub type Help = Fn(&Context, &Message, HashMap<String, Arc<Command>>, Vec<String>) + Send + Sync + 'static; +pub type Exec = Fn(&Context, &Message, Vec<String>) -> Result<(), String> + Send + Sync + 'static; +pub type Help = Fn(&Context, &Message, HashMap<String, Arc<CommandGroup>>, Vec<String>) -> Result<(), String> + Send + Sync + 'static; pub type Hook = Fn(&Context, &Message, &String) + Send + Sync + 'static; +pub type AfterHook = Fn(&Context, &Message, &String, Result<(), String>) + Send + Sync + 'static; #[doc(hidden)] pub type InternalCommand = Arc<Command>; pub type PrefixCheck = Fn(&Context) -> Option<String> + Send + Sync + 'static; @@ -21,6 +22,12 @@ pub enum CommandType { WithCommands(Box<Help>), } +#[derive(Default)] +pub struct CommandGroup { + pub prefix: Option<String>, + pub commands: HashMap<String, InternalCommand>, +} + /// Command struct used to store commands internally. pub struct Command { /// A set of checks to be called prior to executing the command. The checks @@ -28,6 +35,8 @@ pub struct Command { pub checks: Vec<Box<Check>>, /// Function called when the command is called. pub exec: CommandType, + /// Ratelimit bucket. + pub bucket: Option<String>, /// Command description, used by other commands. pub desc: Option<String>, /// Command usage schema, used by other commands. @@ -48,6 +57,26 @@ pub struct Command { pub guild_only: bool, } +impl Command { + pub fn new<F>(f: F) -> Self + where F: Fn(&Context, &Message, Vec<String>) -> Result<(), String> + Send + Sync + 'static { + Command { + checks: Vec::default(), + exec: CommandType::Basic(Box::new(f)), + desc: None, + usage: None, + use_quotes: false, + dm_only: false, + bucket: None, + guild_only: false, + help_available: true, + min_args: None, + max_args: None, + required_permissions: Permissions::empty(), + } + } +} + pub fn positions(ctx: &Context, content: &str, conf: &Configuration) -> Option<Vec<usize>> { if !conf.prefixes.is_empty() || conf.dynamic_prefix.is_some() { // Find out if they were mentioned. If not, determine if the prefix diff --git a/src/ext/framework/configuration.rs b/src/ext/framework/configuration.rs index e95faea..f841a85 100644 --- a/src/ext/framework/configuration.rs +++ b/src/ext/framework/configuration.rs @@ -49,7 +49,21 @@ pub struct Configuration { #[doc(hidden)] pub dynamic_prefix: Option<Box<PrefixCheck>>, #[doc(hidden)] - pub account_type: AccountType + pub rate_limit_message: Option<String>, + #[doc(hidden)] + pub invalid_permission_message: Option<String>, + #[doc(hidden)] + pub invalid_check_message: Option<String>, + #[doc(hidden)] + pub no_dm_message: Option<String>, + #[doc(hidden)] + pub no_guild_message: Option<String>, + #[doc(hidden)] + pub too_many_args_message: Option<String>, + #[doc(hidden)] + pub not_enough_args_message: Option<String>, + #[doc(hidden)] + pub account_type: AccountType, } impl Configuration { @@ -67,6 +81,73 @@ impl Configuration { self } + /// Message that's sent when a command is on cooldown. + /// See framework documentation to see where is this utilized. + /// + /// %time% will be replaced with waiting time in seconds. + pub fn rate_limit_message<S>(mut self, rate_limit_message: S) -> Self + where S: Into<String> { + self.rate_limit_message = Some(rate_limit_message.into()); + + self + } + + /// Message that's sent when a user with wrong permissions calls a command. + pub fn invalid_permission_message<S>(mut self, invalid_permission_message: S) -> Self + where S: Into<String> { + self.invalid_permission_message = Some(invalid_permission_message.into()); + + self + } + + /// Message that's sent when one of a command's checks doesn't succeed. + pub fn invalid_check_message<S>(mut self, invalid_check_message: S) -> Self + where S: Into<String> { + self.invalid_check_message = Some(invalid_check_message.into()); + + self + } + + /// Message that's sent when a command isn't available in DM. + pub fn no_dm_message<S>(mut self, no_dm_message: S) -> Self + where S: Into<String> { + self.no_dm_message = Some(no_dm_message.into()); + + self + } + + /// Message that's sent when a command isn't available in guilds. + pub fn no_guild_message<S>(mut self, no_guild_message: S) -> Self + where S: Into<String> { + self.no_guild_message = Some(no_guild_message.into()); + + self + } + + /// Message that's sent when user sends too many arguments to a command. + /// + /// %max% will be replaced with maximum allowed amount of arguments. + /// + /// %given% will be replced with the given amount of arguments. + pub fn too_many_args_message<S>(mut self, too_many_args_message: S) -> Self + where S: Into<String> { + self.too_many_args_message = Some(too_many_args_message.into()); + + self + } + + /// Message that's sent when user sends too few arguments to a command. + /// + /// %min% will be replaced with minimum allowed amount of arguments. + /// + /// %given% will be replced with the given amount of arguments. + pub fn not_enough_args_message<S>(mut self, not_enough_args_message: S) -> Self + where S: Into<String> { + self.not_enough_args_message = Some(not_enough_args_message.into()); + + self + } + /// Whether or not to respond to commands initiated with a mention. Note /// that this can be used in conjunction with [`prefix`]. /// @@ -181,6 +262,13 @@ impl Default for Configuration { allow_whitespace: false, prefixes: vec![], dynamic_prefix: None, + rate_limit_message: None, + invalid_permission_message: None, + invalid_check_message: None, + no_dm_message: None, + no_guild_message: None, + too_many_args_message: None, + not_enough_args_message: None, account_type: AccountType::Automatic } } diff --git a/src/ext/framework/create_command.rs b/src/ext/framework/create_command.rs index 6d40592..e3faffd 100644 --- a/src/ext/framework/create_command.rs +++ b/src/ext/framework/create_command.rs @@ -1,4 +1,4 @@ -pub use ext::framework::command::{Command, CommandType}; +pub use super::{Command, CommandType, CommandGroup}; use std::collections::HashMap; use std::default::Default; @@ -32,8 +32,10 @@ impl CreateCommand { /// .desc("Replies to a ping with a pong") /// .exec(ping))); /// - /// fn ping(context: &Context, _message: &Message, _args: Vec<String>) { - /// context.say("Pong!"); + /// fn ping(context: &Context, _message: &Message, _args: Vec<String>) -> Result<(), String> { + /// let _ = context.say("Pong!"); + /// + /// Ok(()) /// } /// /// fn owner_check(_context: &Context, message: &Message) -> bool { @@ -55,6 +57,13 @@ impl CreateCommand { self } + /// Adds a ratelimit bucket. + pub fn bucket(mut self, bucket: &str) -> Self { + self.0.bucket = Some(bucket.to_owned()); + + self + } + /// Whether command should be displayed in help list or not, used by other commands. pub fn help_available(mut self, help_available: bool) -> Self { self.0.help_available = help_available; @@ -98,12 +107,13 @@ impl CreateCommand { } /// A function that can be called when a command is received. + /// You can return Err(string) if there's an error. /// /// See [`exec_str`] if you _only_ need to return a string on command use. /// /// [`exec_str`]: #method.exec_str pub fn exec<F>(mut self, func: F) -> Self - where F: Fn(&Context, &Message, Vec<String>) + Send + Sync + 'static { + where F: Fn(&Context, &Message, Vec<String>) -> Result<(), String> + Send + Sync + 'static { self.0.exec = CommandType::Basic(Box::new(func)); self @@ -112,8 +122,10 @@ impl CreateCommand { /// 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. + /// + /// You can return Err(string) if there's an error. pub fn exec_help<F>(mut self, f: F) -> Self - where F: Fn(&Context, &Message, HashMap<String, Arc<Command>>, Vec<String>) + Send + Sync + 'static { + where F: Fn(&Context, &Message, HashMap<String, Arc<CommandGroup>>, Vec<String>) -> Result<(), String> + Send + Sync + 'static { self.0.exec = CommandType::WithCommands(Box::new(f)); self @@ -165,11 +177,12 @@ impl Default for Command { fn default() -> Command { Command { checks: Vec::default(), - exec: CommandType::Basic(Box::new(|_, _, _| {})), + exec: CommandType::Basic(Box::new(|_, _, _| Ok(()))), desc: None, usage: None, use_quotes: true, min_args: None, + bucket: None, max_args: None, required_permissions: Permissions::empty(), dm_only: false, diff --git a/src/ext/framework/create_group.rs b/src/ext/framework/create_group.rs new file mode 100644 index 0000000..db2832b --- /dev/null +++ b/src/ext/framework/create_group.rs @@ -0,0 +1,58 @@ +pub use ext::framework::command::{Command, CommandType, CommandGroup}; +pub use ext::framework::create_command::CreateCommand; + +use std::default::Default; +use std::sync::Arc; +use ::client::Context; +use ::model::Message; + +#[derive(Default)] +pub struct CreateGroup(pub CommandGroup); + +/// Used to create command groups +/// +/// # Examples +/// +/// Create group named Information where all commands are prefixed with info, +/// and add one command named "name". For example, if prefix is "~", we say "~info name" +/// to call the "name" command. +/// +/// ```rust,ignore +/// framework.group("Information", |g| g +/// .prefix("info") +/// .command("name", |c| c +/// .exec_str("meew0"))) +/// ``` +impl CreateGroup { + /// If prefix is set, it will be required before all command names. + /// For example, if bot prefix is "~" and group prefix is "image" + /// we'd call a subcommand named "hibiki" by sending "~image hibiki". + /// + /// **Note**: serenity automatically puts a space after group prefix. + pub fn prefix(mut self, desc: &str) -> Self { + self.0.prefix = Some(desc.to_owned()); + + self + } + + /// Adds a command to group. + pub fn command<F, S>(mut self, command_name: S, f: F) -> Self + where F: FnOnce(CreateCommand) -> CreateCommand, + S: Into<String> { + let cmd = f(CreateCommand(Command::default())).0; + + self.0.commands.insert(command_name.into(), Arc::new(cmd)); + + self + } + + /// Adds a command to group with simplified API. + /// You can return Err(string) if there's an error. + pub fn on<F, S>(mut self, command_name: S, f: F) -> Self + where F: Fn(&Context, &Message, Vec<String>) -> Result<(), String> + Send + Sync + 'static, + S: Into<String> { + self.0.commands.insert(command_name.into(), Arc::new(Command::new(f))); + + self + } +} diff --git a/src/ext/framework/help_commands.rs b/src/ext/framework/help_commands.rs new file mode 100644 index 0000000..375b8dc --- /dev/null +++ b/src/ext/framework/help_commands.rs @@ -0,0 +1,229 @@ +pub use super::{Command, CommandGroup}; + +use std::collections::HashMap; +use std::sync::Arc; +use std::fmt::Write; +use ::client::Context; +use ::model::Message; +use ::utils::Colour; + +fn error_embed(ctx: &Context, message: &Message, input: &str) { + let _ = ctx.send_message(message.channel_id, |m| m + .embed(|e| e + .colour(Colour::dark_red()) + .description(input))); +} + +pub fn with_embeds(ctx: &Context, + message: &Message, + groups: HashMap<String, Arc<CommandGroup>>, + args: Vec<String>) -> Result<(), String> { + if !args.is_empty() { + let name = args.join(" "); + + for (group_name, group) in groups { + let mut found: Option<(&String, &Command)> = None; + + if let Some(ref prefix) = group.prefix { + for (command_name, command) in &group.commands { + if name == format!("{} {}", prefix, command_name) { + found = Some((command_name, command)); + } + } + } else { + for (command_name, command) in &group.commands { + if name == command_name[..] { + found = Some((command_name, command)); + } + } + }; + + if let Some((command_name, command)) = found { + if !command.help_available { + error_embed(ctx, message, "**Error**: No help available."); + return Ok(()); + } + + let _ = ctx.send_message(message.channel_id, |m| { + m.embed(|e| { + let mut embed = e.colour(Colour::rosewater()) + .title(command_name); + if let Some(ref desc) = command.desc { + embed = embed.field(|f| { + f.name("Description") + .value(desc) + .inline(false) + }); + } + + if let Some(ref usage) = command.usage { + embed = embed.field(|f| { + f.name("Usage") + .value(&format!("{} {}", command_name, usage)) + }); + } + + if group_name != "Ungrouped" { + embed = embed.field(|f| { + f.name("Group") + .value(&group_name) + }); + } + + let available = if command.dm_only { + "Only in DM" + } else if command.guild_only { + "Only in guilds" + } else { + "In DM and guilds" + }; + + embed = embed.field(|f| { + f.name("Available") + .value(available) + }); + + embed + }) + }); + + return Ok(()); + } + } + + let error_msg = format!("**Error**: Command `{}` not found.", name); + error_embed(ctx, message, &error_msg); + + return Ok(()); + } + let _ = ctx.send_message(message.channel_id, |m| { + m.embed(|mut e| { + e = e.colour(Colour::rosewater()) + .description("To get help about individual command, pass its \ + name as an argument to this command."); + + for (group_name, group) in groups { + let mut desc = String::new(); + + if let Some(ref x) = group.prefix { + let _ = write!(desc, "Prefix: {}\n", x); + } + + let mut no_commands = true; + let _ = write!(desc, "Commands:\n"); + + for (n, cmd) in &group.commands { + if cmd.help_available { + let _ = write!(desc, "`{}`\n", n); + + no_commands = false; + } + } + + if no_commands { + let _ = write!(desc, "*[No commands]*"); + } + + e = e.field(|f| f.name(&group_name).value(&desc)); + } + + e + }) + }); + + Ok(()) +} + +pub fn plain(ctx: &Context, + _: &Message, + groups: HashMap<String, Arc<CommandGroup>>, + args: Vec<String>) -> Result<(), String> { + if !args.is_empty() { + let name = args.join(" "); + + for (group_name, group) in groups { + let mut found: Option<(&String, &Command)> = None; + if let Some(ref prefix) = group.prefix { + for (command_name, command) in &group.commands { + if name == format!("{} {}", prefix, command_name) { + found = Some((command_name, command)); + } + } + } else { + for (command_name, command) in &group.commands { + if name == command_name[..] { + found = Some((command_name, command)); + } + } + }; + + if let Some((command_name, command)) = found { + if !command.help_available { + let _ = ctx.say("**Error**: No help available."); + return Ok(()); + } + + let mut result = format!("**{}**\n", command_name); + + if let Some(ref desc) = command.desc { + let _ = write!(result, "**Description:** {}\n", desc); + } + + if let Some(ref usage) = command.usage { + let _ = write!(result, "**Usage:** {}\n", usage); + } + + if group_name != "Ungrouped" { + let _ = write!(result, "**Group:** {}\n", group_name); + } + + let available = if command.dm_only { + "Only in DM" + } else if command.guild_only { + "Only in guilds" + } else { + "In DM and guilds" + }; + + let _ = write!(result, "**Available:** {}\n", available); + let _ = ctx.say(&result); + + return Ok(()); + } + } + + let _ = ctx.say(&format!("**Error**: Command `{}` not found.", name)); + + return Ok(()); + } + let mut result = "**Commands**\nTo get help about individual command, pass \ + its name as an argument to this command.\n\n" + .to_string(); + + for (group_name, group) in groups { + let mut desc = String::new(); + + if let Some(ref x) = group.prefix { + let _ = write!(desc, "(prefix: `{}`): ", x); + } + + let mut no_commands = true; + + for (n, cmd) in &group.commands { + if cmd.help_available { + let _ = write!(desc, "`{}` ", n); + no_commands = false; + } + } + + if no_commands { + let _ = write!(desc, "*[No commands]*"); + } + + let _ = write!(result, "**{}:** {}\n", group_name, desc); + } + + let _ = ctx.say(&result); + + Ok(()) +} diff --git a/src/ext/framework/mod.rs b/src/ext/framework/mod.rs index fa7fdac..6cd222f 100644 --- a/src/ext/framework/mod.rs +++ b/src/ext/framework/mod.rs @@ -30,7 +30,7 @@ //! Configuring a Client with a framework, which has a prefix of `"~"` and a //! ping and about command: //! -//! ```rust,no_run +//! ```rust,ignore //! use serenity::client::{Client, Context}; //! use serenity::model::Message; //! use std::env; @@ -42,34 +42,39 @@ //! .command("about", |c| c.exec_str("A simple test bot")) //! .command("ping", |c| c.exec(ping))); //! -//! fn about(context: &Context, _message: &Message, _args: Vec<String>) { +//! command!(about(context) { //! let _ = context.say("A simple test bot"); -//! } +//! }); //! -//! fn ping(context: &Context, _message: &Message, _args: Vec<String>) { +//! command!(ping(context) { //! let _ = context.say("Pong!"); -//! } +//! }); //! ``` //! //! [`Client::with_framework`]: ../../client/struct.Client.html#method.with_framework +pub mod help_commands; + mod command; mod configuration; mod create_command; +mod create_group; +mod buckets; -pub use self::command::{Command, CommandType}; +pub use self::command::{Command, CommandType, CommandGroup}; pub use self::configuration::{AccountType, Configuration}; pub use self::create_command::CreateCommand; +pub use self::create_group::CreateGroup; +pub use self::buckets::{Bucket, MemberRatelimit, Ratelimit}; -use self::command::{Hook, InternalCommand}; +use self::command::{AfterHook, Hook}; use std::collections::HashMap; +use std::default::Default; use std::sync::Arc; use std::thread; -use ::client::Context; +use ::client::{CACHE, Context}; use ::model::Message; use ::utils; -use ::client::CACHE; -use ::model::Permissions; /// A macro to generate "named parameters". This is useful to avoid manually /// using the "arguments" parameter and manually parsing types. @@ -104,28 +109,46 @@ use ::model::Permissions; /// [`Framework`]: ext/framework/index.html #[macro_export] macro_rules! command { + ($fname:ident($c:ident) $b:block) => { + pub fn $fname($c: &Context, _: &Message, _: Vec<String>) -> Result<(), String> { + $b + + Ok(()) + } + }; + ($fname:ident($c:ident, $m:ident) $b:block) => { + pub fn $fname($c: &Context, $m: &Message, _: Vec<String>) -> Result<(), String> { + $b + + Ok(()) + } + }; ($fname:ident($c:ident, $m:ident, $a:ident) $b:block) => { - pub fn $fname($c: &Context, $m: &Message, $a: Vec<String>) { + pub fn $fname($c: &Context, $m: &Message, $a: Vec<String>) -> Result<(), String> { $b + + Ok(()) } }; ($fname:ident($c:ident, $m:ident, $a:ident, $($name:ident: $t:ty),*) $b:block) => { - pub fn $fname($c: &Context, $m: &Message, $a: Vec<String>) { + pub fn $fname($c: &Context, $m: &Message, $a: Vec<String>) -> Result<(), String> { let mut i = $a.iter(); $( let $name = match i.next() { Some(v) => match v.parse::<$t>() { Ok(v) => v, - Err(_why) => return, + Err(_) => return Err(format!("Failed to parse {:?}", stringify!($t))), }, - None => return, + None => return Err(format!("Failed to parse {:?}", stringify!($t))), }; )* drop(i); $b + + Ok(()) } }; } @@ -139,9 +162,10 @@ macro_rules! command { #[derive(Default)] pub struct Framework { configuration: Configuration, - commands: HashMap<String, InternalCommand>, + groups: HashMap<String, Arc<CommandGroup>>, before: Option<Arc<Hook>>, - after: Option<Arc<Hook>>, + buckets: HashMap<String, Bucket>, + after: Option<Arc<AfterHook>>, /// Whether the framework has been "initialized". /// /// The framework is initialized once one of the following occurs: @@ -194,6 +218,44 @@ impl Framework { self } + /// Defines a bucket with `delay` between each command, and the `limit` of uses + /// per `time_span`. + pub fn bucket<S>(mut self, s: S, delay: i64, time_span: i64, limit: i32) -> Self + where S: Into<String> { + self.buckets.insert(s.into(), Bucket { + ratelimit: Ratelimit { + delay: delay, + limit: Some((time_span, limit)) + }, + limits: HashMap::new() + }); + + self + } + + /// Defines a bucket just with `delay` between each command. + pub fn simple_bucket<S>(mut self, s: S, delay: i64) -> Self + where S: Into<String> { + self.buckets.insert(s.into(), Bucket { + ratelimit: Ratelimit { + delay: delay, + limit: None + }, + limits: HashMap::new() + }); + + self + } + + #[allow(map_entry)] + fn ratelimit_time(&mut self, bucket_name: &str, user_id: u64) -> i64 { + self.buckets + .get_mut(bucket_name) + .map(|bucket| bucket.take(user_id)) + .unwrap_or(0) + } + + #[allow(cyclomatic_complexity)] #[doc(hidden)] pub fn dispatch(&mut self, context: Context, message: Message) { match self.configuration.account_type { @@ -209,6 +271,7 @@ impl Framework { }, AccountType::Automatic => { let cache = CACHE.read().unwrap(); + if cache.user.bot { if message.author.bot { return; @@ -254,31 +317,64 @@ impl Framework { None => continue, }); - if let Some(command) = self.commands.get(&built) { - if message.is_private() { - if command.guild_only { - return; + let groups = self.groups.clone(); + + for group in groups.values() { + let to_check = if let Some(ref prefix) = group.prefix { + if built.starts_with(prefix) && built.len() > prefix.len() + 1 { + built[(prefix.len() + 1)..].to_owned() + } else { + continue; } - } else if command.dm_only { - return; - } + } else { + built.clone() + }; + + if let Some(command) = group.commands.get(&to_check) { + if let Some(ref bucket_name) = command.bucket { + let rate_limit = self.ratelimit_time(bucket_name, message.author.id.0); + + if rate_limit > 0 { + if let Some(ref message) = self.configuration.rate_limit_message { + let _ = context.say( + &message.replace("%time%", &rate_limit.to_string())); + } - for check in &command.checks { - if !(check)(&context, &message) { - continue 'outer; + return; + } } - } - let before = self.before.clone(); - let command = command.clone(); - let after = self.after.clone(); - let commands = self.commands.clone(); + if message.is_private() { + if command.guild_only { + if let Some(ref message) = self.configuration.no_guild_message { + let _ = context.say(message); + } + + return; + } + } else if command.dm_only { + if let Some(ref message) = self.configuration.no_dm_message { + let _ = context.say(message); + } - thread::spawn(move || { - if let Some(before) = before { - (before)(&context, &message, &built); + return; } + for check in &command.checks { + if !(check)(&context, &message) { + if let Some(ref message) = self.configuration.invalid_check_message { + let _ = context.say(message); + } + + continue 'outer; + } + } + + let before = self.before.clone(); + let command = command.clone(); + let after = self.after.clone(); + let groups = self.groups.clone(); + let args = if command.use_quotes { utils::parse_quotes(&message.content[position + built.len()..]) } else { @@ -290,12 +386,24 @@ impl Framework { if let Some(x) = command.min_args { if args.len() < x as usize { + if let Some(ref message) = self.configuration.not_enough_args_message { + let _ = context.say( + &message.replace("%min%", &x.to_string()) + .replace("%given%", &args.len().to_string())); + } + return; } } if let Some(x) = command.max_args { if args.len() > x as usize { + if let Some(ref message) = self.configuration.too_many_args_message { + let _ = context.say( + &message.replace("%max%", &x.to_string()) + .replace("%given%", &args.len().to_string())); + } + return; } } @@ -316,28 +424,40 @@ impl Framework { } if !permissions_fulfilled { + if let Some(ref message) = self.configuration.invalid_permission_message { + let _ = context.say(message); + } + return; } } - 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); + thread::spawn(move || { + if let Some(before) = before { + (before)(&context, &message, &built); } - } - if let Some(after) = after { - (after)(&context, &message, &built); - } - }); + let result = match command.exec { + CommandType::StringResponse(ref x) => { + let _ = &context.say(x); + + Ok(()) + }, + CommandType::Basic(ref x) => { + (x)(&context, &message, args) + }, + CommandType::WithCommands(ref x) => { + (x)(&context, &message, groups, args) + } + }; - return; + if let Some(after) = after { + (after)(&context, &message, &built, result); + } + }); + + return; + } } } } @@ -359,21 +479,17 @@ impl Framework { /// [`command`]: #method.command /// [module-level documentation]: index.html pub fn on<F, S>(mut self, command_name: S, f: F) -> Self - where F: Fn(&Context, &Message, Vec<String>) + Send + Sync + 'static, + where F: Fn(&Context, &Message, Vec<String>) -> Result<(), String> + Send + Sync + 'static, S: Into<String> { - 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, - dm_only: false, - guild_only: false, - help_available: true, - min_args: None, - max_args: None, - required_permissions: Permissions::empty() - })); + if !self.groups.contains_key("Ungrouped") { + self.groups.insert("Ungrouped".to_string(), Arc::new(CommandGroup::default())); + } + + if let Some(ref mut x) = self.groups.get_mut("Ungrouped") { + if let Some(ref mut y) = Arc::get_mut(x) { + y.commands.insert(command_name.into(), Arc::new(Command::new(f))); + } + } self.initialized = true; @@ -395,8 +511,28 @@ impl Framework { where F: FnOnce(CreateCommand) -> CreateCommand, S: Into<String> { let cmd = f(CreateCommand(Command::default())).0; - self.commands.insert(command_name.into(), Arc::new(cmd)); + if !self.groups.contains_key("Ungrouped") { + self.groups.insert("Ungrouped".to_string(), Arc::new(CommandGroup::default())); + } + + if let Some(ref mut x) = self.groups.get_mut("Ungrouped") { + if let Some(ref mut y) = Arc::get_mut(x) { + y.commands.insert(command_name.into(), Arc::new(cmd)); + } + } + + self.initialized = true; + + self + } + + pub fn group<F, S>(mut self, group_name: S, f: F) -> Self + where F: FnOnce(CreateGroup) -> CreateGroup, + S: Into<String> { + let group = f(CreateGroup(CommandGroup::default())).0; + + self.groups.insert(group_name.into(), Arc::new(group)); self.initialized = true; self @@ -411,8 +547,9 @@ impl Framework { } /// Specify the function to be called after every command's execution. + /// Fourth argument exists if command returned an error which you can handle. pub fn after<F>(mut self, f: F) -> Self - where F: Fn(&Context, &Message, &String) + Send + Sync + 'static { + where F: Fn(&Context, &Message, &String, Result<(), String>) + Send + Sync + 'static { self.after = Some(Arc::new(f)); self @@ -426,7 +563,7 @@ impl Framework { /// Ensure that the user who created a message, calling a "ping" command, /// is the owner. /// - /// ```rust,no_run + /// ```rust,ignore /// use serenity::client::{Client, Context}; /// use serenity::model::Message; /// use std::env; @@ -438,9 +575,9 @@ impl Framework { /// .on("ping", ping) /// .set_check("ping", owner_check)); /// - /// fn ping(context: &Context, _message: &Message, _args: Vec<String>) { - /// context.say("Pong!"); - /// } + /// command!(ping(context) { + /// let _ = context.say("Pong!"); + /// }); /// /// fn owner_check(_context: &Context, message: &Message) -> bool { /// // replace with your user ID @@ -451,9 +588,17 @@ impl Framework { pub fn set_check<F, S>(mut self, command: S, check: F) -> Self where F: Fn(&Context, &Message) -> bool + Send + Sync + 'static, S: Into<String> { - if let Some(command) = self.commands.get_mut(&command.into()) { - if let Some(c) = Arc::get_mut(command) { - c.checks.push(Box::new(check)); + if !self.groups.contains_key("Ungrouped") { + self.groups.insert("Ungrouped".to_string(), Arc::new(CommandGroup::default())); + } + + if let Some(ref mut group) = self.groups.get_mut("Ungrouped") { + if let Some(group_mut) = Arc::get_mut(group) { + if let Some(ref mut command) = group_mut.commands.get_mut(&command.into()) { + if let Some(c) = Arc::get_mut(command) { + c.checks.push(Box::new(check)); + } + } } } |