aboutsummaryrefslogtreecommitdiff
path: root/src/ext/framework
diff options
context:
space:
mode:
authorIllia <[email protected]>2016-12-13 21:26:29 +0200
committerzeyla <[email protected]>2016-12-13 11:26:29 -0800
commitdaf92eda815b8f539f6d759ab48cf7a70513915f (patch)
tree36145f5095e7af6fb725635dd104e9d9d3f0ea62 /src/ext/framework
parentFix readme typo (diff)
downloadserenity-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.rs60
-rw-r--r--src/ext/framework/command.rs33
-rw-r--r--src/ext/framework/configuration.rs90
-rw-r--r--src/ext/framework/create_command.rs25
-rw-r--r--src/ext/framework/create_group.rs58
-rw-r--r--src/ext/framework/help_commands.rs229
-rw-r--r--src/ext/framework/mod.rs289
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));
+ }
+ }
}
}