aboutsummaryrefslogtreecommitdiff
path: root/src/framework
diff options
context:
space:
mode:
Diffstat (limited to 'src/framework')
-rw-r--r--src/framework/buckets.rs60
-rw-r--r--src/framework/command.rs164
-rw-r--r--src/framework/configuration.rs262
-rw-r--r--src/framework/create_command.rs226
-rw-r--r--src/framework/create_group.rs68
-rw-r--r--src/framework/help_commands.rs285
-rw-r--r--src/framework/mod.rs665
7 files changed, 1730 insertions, 0 deletions
diff --git a/src/framework/buckets.rs b/src/framework/buckets.rs
new file mode 100644
index 0000000..02cd658
--- /dev/null
+++ b/src/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 tickets: i32,
+ pub last_time: i64,
+ pub set_time: i64,
+}
+
+impl Default for MemberRatelimit {
+ fn default() -> Self {
+ MemberRatelimit {
+ tickets: 0,
+ last_time: 0,
+ set_time: 0,
+ }
+ }
+}
+
+#[doc(hidden)]
+pub struct Bucket {
+ pub ratelimit: Ratelimit,
+ pub users: HashMap<u64, MemberRatelimit>,
+}
+
+impl Bucket {
+ pub fn take(&mut self, user_id: u64) -> i64 {
+ let time = time::get_time().sec;
+ let user = self.users.entry(user_id)
+ .or_insert_with(MemberRatelimit::default);
+
+ if let Some((timespan, limit)) = self.ratelimit.limit {
+ if (user.tickets + 1) > limit {
+ if time < (user.set_time + timespan) {
+ return (user.set_time + timespan) - time;
+ } else {
+ user.tickets = 0;
+ user.set_time = time;
+ }
+ }
+ }
+
+ if time < user.last_time + self.ratelimit.delay {
+ (user.last_time + self.ratelimit.delay) - time
+ } else {
+ user.tickets += 1;
+ user.last_time = time;
+
+ 0
+ }
+ }
+}
diff --git a/src/framework/command.rs b/src/framework/command.rs
new file mode 100644
index 0000000..5f43284
--- /dev/null
+++ b/src/framework/command.rs
@@ -0,0 +1,164 @@
+use std::sync::Arc;
+use super::Configuration;
+use ::client::Context;
+use ::model::Message;
+use ::model::Permissions;
+use std::collections::HashMap;
+
+pub type Check = Fn(&mut Context, &Message) -> bool + Send + Sync + 'static;
+pub type Exec = Fn(&mut Context, &Message, Vec<String>) -> Result<(), String> + Send + Sync + 'static;
+pub type Help = Fn(&mut Context, &Message, HashMap<String, Arc<CommandGroup>>, &[String]) -> Result<(), String> + Send + Sync + 'static;
+pub type BeforeHook = Fn(&mut Context, &Message, &String) -> bool + Send + Sync + 'static;
+pub type AfterHook = Fn(&mut Context, &Message, &String, Result<(), String>) + Send + Sync + 'static;
+#[doc(hidden)]
+pub type InternalCommand = Arc<Command>;
+pub type PrefixCheck = Fn(&mut Context) -> Option<String> + Send + Sync + 'static;
+
+#[doc(hidden)]
+pub enum CommandOrAlias {
+ Alias(String),
+ Command(InternalCommand),
+}
+
+/// Command function type. Allows to access internal framework things inside
+/// your commands.
+pub enum CommandType {
+ StringResponse(String),
+ Basic(Box<Exec>),
+ WithCommands(Box<Help>),
+}
+
+#[derive(Default)]
+pub struct CommandGroup {
+ pub prefix: Option<String>,
+ pub commands: HashMap<String, CommandOrAlias>,
+}
+
+/// 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<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>,
+ /// Example arguments, used by other commands.
+ pub example: Option<String>,
+ /// Command usage schema, used by other commands.
+ pub usage: Option<String>,
+ /// Whether arguments should be parsed using quote parser or not.
+ pub use_quotes: bool,
+ /// Minumum amount of arguments that should be passed.
+ pub min_args: Option<i32>,
+ /// Maximum amount of arguments that can be passed.
+ pub max_args: Option<i32>,
+ /// Permissions required to use this command.
+ pub required_permissions: Permissions,
+ /// Whether command should be displayed in help list or not, used by other commands.
+ pub help_available: bool,
+ /// Whether command can be used only privately or not.
+ pub dm_only: bool,
+ /// Whether command can be used only in guilds or not.
+ pub guild_only: bool,
+ /// Whether command can only be used by owners or not.
+ pub owners_only: bool,
+ #[doc(hidden)]
+ pub aliases: Vec<String>,
+}
+
+impl Command {
+ pub fn new<F>(f: F) -> Self
+ where F: Fn(&mut Context, &Message, Vec<String>) -> Result<(), String> + Send + Sync + 'static {
+ Command {
+ aliases: Vec::new(),
+ checks: Vec::default(),
+ exec: CommandType::Basic(Box::new(f)),
+ desc: None,
+ usage: None,
+ example: None,
+ use_quotes: false,
+ dm_only: false,
+ bucket: None,
+ guild_only: false,
+ help_available: true,
+ min_args: None,
+ max_args: None,
+ owners_only: false,
+ required_permissions: Permissions::empty(),
+ }
+ }
+}
+
+pub fn positions(ctx: &mut 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
+ // was used. If not, return None.
+ let mut positions: Vec<usize> = 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) {
+ if content.starts_with(&x) {
+ positions.push(x.len());
+ }
+ } else {
+ for n in conf.prefixes.clone() {
+ if content.starts_with(&n) {
+ positions.push(n.len());
+ }
+ }
+ }
+ } else {
+ for n in conf.prefixes.clone() {
+ if content.starts_with(&n) {
+ positions.push(n.len());
+ }
+ }
+ };
+
+ if positions.is_empty() {
+ return None;
+ }
+
+ if conf.allow_whitespace {
+ let pos = *unsafe { positions.get_unchecked(0) };
+
+ positions.insert(0, pos + 1);
+ }
+
+ Some(positions)
+ } else if conf.on_mention.is_some() {
+ match find_mention_end(content, conf) {
+ Some(mention_end) => {
+ let mut positions = vec![mention_end];
+
+ if conf.allow_whitespace {
+ positions.insert(0, mention_end + 1);
+ }
+
+ Some(positions)
+ },
+ None => None,
+ }
+ } else {
+ None
+ }
+}
+
+fn find_mention_end(content: &str, conf: &Configuration) -> Option<usize> {
+ if let Some(ref mentions) = conf.on_mention {
+ for mention in mentions {
+ if !content.starts_with(&mention[..]) {
+ continue;
+ }
+
+ return Some(mention.len());
+ }
+ }
+
+ None
+}
diff --git a/src/framework/configuration.rs b/src/framework/configuration.rs
new file mode 100644
index 0000000..c104e1e
--- /dev/null
+++ b/src/framework/configuration.rs
@@ -0,0 +1,262 @@
+use std::collections::HashSet;
+use std::default::Default;
+use super::command::PrefixCheck;
+use ::client::Context;
+use ::http;
+use ::model::{GuildId, UserId};
+
+/// The configuration to use for a [`Framework`] associated with a [`Client`]
+/// instance.
+///
+/// This allows setting configurations like the depth to search for commands,
+/// whether to treat mentions like a command prefix, etc.
+///
+/// # Examples
+///
+/// Responding to mentions and setting a command prefix of `"~"`:
+///
+/// ```rust,no_run
+/// use serenity::Client;
+/// use std::env;
+///
+/// let mut client = Client::login(&env::var("DISCORD_BOT_TOKEN").unwrap());
+///
+/// client.with_framework(|f| f
+/// .configure(|c| c.on_mention(true).prefix("~")));
+/// ```
+///
+/// [`Client`]: ../../client/struct.Client.html
+/// [`Framework`]: struct.Framework.html
+pub struct Configuration {
+ #[doc(hidden)]
+ pub depth: usize,
+ #[doc(hidden)]
+ pub on_mention: Option<Vec<String>>,
+ #[doc(hidden)]
+ pub allow_whitespace: bool,
+ #[doc(hidden)]
+ pub prefixes: Vec<String>,
+ #[doc(hidden)]
+ pub dynamic_prefix: Option<Box<PrefixCheck>>,
+ #[doc(hidden)]
+ pub ignore_bots: bool,
+ #[doc(hidden)]
+ pub blocked_users: HashSet<UserId>,
+ #[doc(hidden)]
+ pub blocked_guilds: HashSet<GuildId>,
+ #[doc(hidden)]
+ pub owners: HashSet<UserId>,
+ #[doc(hidden)]
+ pub disabled_commands: HashSet<String>,
+ #[doc(hidden)]
+ pub allow_dm: bool,
+ #[doc(hidden)]
+ pub ignore_webhooks: bool,
+}
+
+impl Configuration {
+ /// Whether the bot should respond to other bots.
+ ///
+ /// For example, if this is set to false, then the bot will respond to any other bots including itself.
+ pub fn ignore_bots(mut self, ignore_bots: bool) -> Self {
+ self.ignore_bots = ignore_bots;
+
+ self
+ }
+
+ /// Whether to allow whitespace being optional between a mention/prefix and
+ /// a command.
+ ///
+ /// **Note**: Defaults to `false`.
+ ///
+ /// # Examples
+ ///
+ /// Setting this to `false` will _only_ allow this scenario to occur:
+ ///
+ /// ```ignore
+ /// <@245571012924538880> about
+ /// !about
+ ///
+ /// // bot processes and executes the "about" command if it exists
+ /// ```
+ ///
+ /// while setting this to `true` will _also_ allow this scenario to occur:
+ ///
+ /// ```ignore
+ /// <@245571012924538880>about
+ /// ! about
+ ///
+ /// // bot processes and executes the "about" command if it exists
+ /// ```
+ pub fn allow_whitespace(mut self, allow_whitespace: bool) -> Self {
+ self.allow_whitespace = allow_whitespace;
+
+ self
+ }
+
+ /// If set to false, bot will ignore any private messages.
+ pub fn allow_dm(mut self, allow_dm: bool) -> Self {
+ self.allow_dm = allow_dm;
+
+ self
+ }
+
+ /// If set to true, bot will ignore all commands called by webhooks.
+ /// True by default.
+ pub fn ignore_webhooks(mut self, ignore_webhooks: bool) -> Self {
+ self.ignore_webhooks = ignore_webhooks;
+
+ self
+ }
+
+ /// HashSet of user Ids whose commands will be ignored.
+ /// Guilds owned by user Ids will also be ignored.
+ pub fn blocked_users(mut self, users: HashSet<UserId>) -> Self {
+ self.blocked_users = users;
+
+ self
+ }
+
+ /// HashSet of guild Ids where commands will be ignored.
+ pub fn blocked_guilds(mut self, guilds: HashSet<GuildId>) -> Self {
+ self.blocked_guilds = guilds;
+
+ self
+ }
+
+ /// The default depth of the message to check for commands. Defaults to 5.
+ /// This determines how "far" into a message to check for a valid command.
+ ///
+ /// # Examples
+ ///
+ /// If you set a depth of `1`, and make a command of `"music play"`, but
+ /// not a `"music"` command, then the former command will never be
+ /// triggered, as its "depth" is `2`.
+ pub fn depth(mut self, depth: u8) -> Self {
+ self.depth = depth as usize;
+
+ self
+ }
+
+ /// HashSet of command names that won't be run.
+ pub fn disabled_commands(mut self, commands: HashSet<String>) -> Self {
+ self.disabled_commands = commands;
+
+ self
+ }
+
+ /// Sets the prefix to respond to dynamically based on conditions.
+ ///
+ /// Return `None` to not have a special prefix for the dispatch, and to
+ /// instead use the inherited prefix.
+ ///
+ /// # Examples
+ ///
+ /// If the Id of the channel is divisible by 5, return a prefix of `"!"`,
+ /// otherwise return a prefix of `"~"`.
+ ///
+ /// ```rust,no_run
+ /// # use serenity::Client;
+ /// #
+ /// # let mut client = Client::login("token");
+ /// client.with_framework(|f| f
+ /// .command("ping", |c| c.exec_str("Pong!"))
+ /// .configure(|c| c.dynamic_prefix(|ctx| {
+ /// Some(if ctx.channel_id.unwrap().0 % 5 == 0 {
+ /// "!"
+ /// } else {
+ /// "~"
+ /// }.to_owned())
+ /// })));
+ /// ```
+ pub fn dynamic_prefix<F>(mut self, dynamic_prefix: F) -> Self
+ where F: Fn(&mut Context) -> Option<String> + Send + Sync + 'static {
+ self.dynamic_prefix = Some(Box::new(dynamic_prefix));
+
+ self
+ }
+
+ /// Whether or not to respond to commands initiated with a mention. Note
+ /// that this can be used in conjunction with [`prefix`].
+ ///
+ /// By default this is set to `false`.
+ ///
+ /// # Examples
+ ///
+ /// Setting this to `true` will allow the following types of mentions to be
+ /// responded to:
+ ///
+ /// ```ignore
+ /// <@245571012924538880> about
+ /// <@!245571012924538880> about
+ /// ```
+ ///
+ /// The former is a direct mention, while the latter is a nickname mention,
+ /// which aids mobile devices in determining whether to display a user's
+ /// nickname. It has no real meaning for your bot, and the library
+ /// encourages you to ignore differentiating between the two.
+ ///
+ /// [`prefix`]: #method.prefix
+ pub fn on_mention(mut self, on_mention: bool) -> Self {
+ if !on_mention {
+ return self;
+ }
+
+ if let Ok(current_user) = http::get_current_user() {
+ self.on_mention = Some(vec![
+ format!("<@{}>", current_user.id), // Regular mention
+ format!("<@!{}>", current_user.id), // Nickname mention
+ ]);
+ }
+
+ self
+ }
+
+ /// A `HashSet` of user Ids checks won't apply to.
+ pub fn owners(mut self, user_ids: HashSet<UserId>) -> Self {
+ self.owners = user_ids;
+
+ self
+ }
+
+ /// Sets the prefix to respond to. This can either be a single-char or
+ /// multi-char string.
+ pub fn prefix(mut self, prefix: &str) -> Self {
+ self.prefixes = vec![prefix.to_owned()];
+
+ self
+ }
+
+ /// Sets the prefixes to respond to. Those can either be single-chararacter or
+ /// multi-chararacter strings.
+ pub fn prefixes(mut self, prefixes: Vec<&str>) -> Self {
+ self.prefixes = prefixes.iter().map(|x| x.to_string()).collect();
+
+ self
+ }
+}
+
+impl Default for Configuration {
+ /// Builds a default framework configuration, setting the following:
+ ///
+ /// - **allow_whitespace** to `false`
+ /// - **depth** to `5`
+ /// - **on_mention** to `false` (basically)
+ /// - **prefix** to `None`
+ fn default() -> Configuration {
+ Configuration {
+ depth: 5,
+ on_mention: None,
+ dynamic_prefix: None,
+ allow_whitespace: false,
+ prefixes: vec![],
+ ignore_bots: true,
+ owners: HashSet::default(),
+ blocked_users: HashSet::default(),
+ blocked_guilds: HashSet::default(),
+ disabled_commands: HashSet::default(),
+ allow_dm: true,
+ ignore_webhooks: true,
+ }
+ }
+}
diff --git a/src/framework/create_command.rs b/src/framework/create_command.rs
new file mode 100644
index 0000000..512e82c
--- /dev/null
+++ b/src/framework/create_command.rs
@@ -0,0 +1,226 @@
+pub use super::{Command, CommandType, CommandGroup};
+
+use std::collections::HashMap;
+use std::default::Default;
+use std::sync::Arc;
+use ::client::Context;
+use ::model::{Message, Permissions};
+
+pub struct CreateCommand(pub Command);
+
+impl CreateCommand {
+ /// Adds a ratelimit bucket.
+ pub fn bucket(mut self, bucket: &str) -> Self {
+ self.0.bucket = Some(bucket.to_owned());
+
+ self
+ }
+
+ /// Adds an alias, allowing users to use the command under a different name.
+ pub fn known_as(mut self, name: &str) -> Self {
+ self.0.aliases.push(name.to_owned());
+
+ self
+ }
+
+ /// Adds multiple aliases.
+ pub fn batch_known_as(mut self, names: Vec<&str>) -> Self {
+ for n in names {
+ self.0.aliases.push(n.to_owned());
+ }
+
+ self
+ }
+
+ /// 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(&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: &mut Context, message: &Message, _args: Vec<String>) -> Result<(), String> {
+ /// let _ = message.channel_id.say("Pong!");
+ ///
+ /// Ok(())
+ /// }
+ ///
+ /// fn owner_check(_context: &mut Context, message: &Message) -> bool {
+ /// // replace with your user ID
+ /// message.author.id == 7
+ /// }
+ /// ```
+ pub fn check<F>(mut self, check: F) -> Self
+ where F: Fn(&mut 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
+ }
+
+ /// Whether command can be used only privately or not.
+ pub fn dm_only(mut self, dm_only: bool) -> Self {
+ self.0.dm_only = dm_only;
+
+ self
+ }
+
+ /// Example arguments, used by other commands.
+ pub fn example(mut self, example: &str) -> Self {
+ self.0.example = Some(example.to_owned());
+
+ self
+ }
+
+ /// 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(&mut Context, &Message, Vec<String>) -> Result<(), String> + 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 commands, 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(&mut Context, &Message, HashMap<String, Arc<CommandGroup>>, &[String]) -> Result<(), String> + 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
+ }
+
+ /// Whether command can be used only in guilds or not.
+ pub fn guild_only(mut self, guild_only: bool) -> Self {
+ self.0.guild_only = guild_only;
+
+ 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;
+
+ self
+ }
+
+ /// Maximum amount of arguments that can be passed.
+ pub fn max_args(mut self, max_args: i32) -> Self {
+ self.0.max_args = Some(max_args);
+
+ self
+ }
+
+ /// Minumum amount of arguments that should be passed.
+ pub fn min_args(mut self, min_args: i32) -> Self {
+ self.0.min_args = Some(min_args);
+
+ self
+ }
+
+ /// Whether command can be used only privately or not.
+ pub fn owners_only(mut self, owners_only: bool) -> Self {
+ self.0.owners_only = owners_only;
+
+ self
+ }
+
+ /// The permissions that a user must have in the contextual channel in order
+ /// for the command to be processed.
+ pub fn required_permissions(mut self, permissions: Permissions) -> Self {
+ self.0.required_permissions = permissions;
+
+ 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 {
+ aliases: Vec::new(),
+ checks: Vec::default(),
+ exec: CommandType::Basic(Box::new(|_, _, _| Ok(()))),
+ desc: None,
+ usage: None,
+ example: None,
+ use_quotes: false,
+ min_args: None,
+ bucket: None,
+ max_args: None,
+ required_permissions: Permissions::empty(),
+ dm_only: false,
+ guild_only: false,
+ help_available: true,
+ owners_only: false,
+ }
+ }
+}
diff --git a/src/framework/create_group.rs b/src/framework/create_group.rs
new file mode 100644
index 0000000..03fc33e
--- /dev/null
+++ b/src/framework/create_group.rs
@@ -0,0 +1,68 @@
+pub use ext::framework::command::{Command, CommandType, CommandGroup, CommandOrAlias};
+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("Hakase")))
+/// ```
+impl CreateGroup {
+ /// Adds a command to group.
+ pub fn command<F>(mut self, command_name: &str, f: F) -> Self
+ where F: FnOnce(CreateCommand) -> CreateCommand {
+ let cmd = f(CreateCommand(Command::default())).0;
+
+ for n in &cmd.aliases {
+ if let Some(ref prefix) = self.0.prefix {
+ self.0.commands.insert(format!("{} {}", prefix, n.to_owned()), CommandOrAlias::Alias(format!("{} {}", prefix, command_name.to_string())));
+ } else {
+ self.0.commands.insert(n.to_owned(), CommandOrAlias::Alias(command_name.to_string()));
+ }
+ }
+
+ self.0.commands.insert(command_name.to_owned(), CommandOrAlias::Command(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>(mut self, command_name: &str, f: F) -> Self
+ where F: Fn(&mut Context, &Message, Vec<String>) -> Result<(), String> + Send + Sync + 'static {
+ let cmd = Arc::new(Command::new(f));
+
+ self.0.commands.insert(command_name.to_owned(), CommandOrAlias::Command(cmd));
+
+ self
+ }
+
+ /// 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.
+ ///
+ /// **Note**: It's suggested to call this first when making a group.
+ pub fn prefix(mut self, desc: &str) -> Self {
+ self.0.prefix = Some(desc.to_owned());
+
+ self
+ }
+}
diff --git a/src/framework/help_commands.rs b/src/framework/help_commands.rs
new file mode 100644
index 0000000..1f38bd5
--- /dev/null
+++ b/src/framework/help_commands.rs
@@ -0,0 +1,285 @@
+//! A collection of default help commands for the framework.
+//!
+//! # Example
+//!
+//! Using the [`with_embeds`] function to have the framework's help message use
+//! embeds:
+//!
+//! ```rs,no_run
+//! use serenity::ext::framework::help_commands;
+//! use serenity::Client;
+//! use std::env;
+//!
+//! let mut client = Client::login(&env::var("DISCORD_TOKEN").unwrap());
+//! client.with_framework(|f| f
+//! .command("help", |c| c.exec_help(help_commands::with_embeds)));
+//! ```
+//!
+//! The same can be accomplished with no embeds by substituting `with_embeds`
+//! with the [`plain`] function.
+//!
+//! [`plain`]: fn.plain.html
+//! [`with_embeds`]: fn.with_embeds.html
+
+use std::collections::HashMap;
+use std::sync::Arc;
+use std::fmt::Write;
+use super::command::InternalCommand;
+use super::{Command, CommandGroup, CommandOrAlias};
+use ::client::Context;
+use ::model::Message;
+use ::utils::Colour;
+
+fn error_embed(ctx: &mut Context, input: &str) {
+ let _ = ctx.channel_id
+ .unwrap()
+ .send_message(|m| m
+ .embed(|e| e
+ .colour(Colour::dark_red())
+ .description(input)));
+}
+
+fn remove_aliases(cmds: &HashMap<String, CommandOrAlias>) -> HashMap<&String, &InternalCommand> {
+ let mut result = HashMap::new();
+
+ for (n, v) in cmds {
+ if let CommandOrAlias::Command(ref cmd) = *v {
+ result.insert(n, cmd);
+ }
+ }
+
+ result
+}
+
+pub fn with_embeds(ctx: &mut Context,
+ _: &Message,
+ groups: HashMap<String, Arc<CommandGroup>>,
+ args: &[String]) -> Result<(), String> {
+ if !args.is_empty() {
+ let name = args.join(" ");
+
+ for (group_name, group) in groups {
+ let mut found: Option<(&String, &InternalCommand)> = None;
+
+ for (command_name, command) in &group.commands {
+ let with_prefix = if let Some(ref prefix) = group.prefix {
+ format!("{} {}", prefix, command_name)
+ } else {
+ command_name.to_owned()
+ };
+
+ if name == with_prefix || name == *command_name {
+ match *command {
+ CommandOrAlias::Command(ref cmd) => {
+ found = Some((command_name, cmd));
+ },
+ CommandOrAlias::Alias(ref name) => {
+ error_embed(ctx, &format!("Did you mean \"{}\"?", name));
+ return Ok(());
+ }
+ }
+ }
+ }
+
+ if let Some((command_name, command)) = found {
+ if !command.help_available {
+ error_embed(ctx, "**Error**: No help available.");
+
+ return Ok(());
+ }
+
+ let _ = ctx.channel_id.unwrap().send_message(|m| {
+ m.embed(|e| {
+ let mut embed = e.colour(Colour::rosewater())
+ .title(command_name);
+ if let Some(ref desc) = command.desc {
+ embed = embed.description(desc);
+ }
+
+ if let Some(ref usage) = command.usage {
+ embed = embed.field(|f| f
+ .name("Usage")
+ .value(&format!("`{} {}`", command_name, usage)));
+ }
+
+ if let Some(ref example) = command.example {
+ embed = embed.field(|f| f
+ .name("Sample usage")
+ .value(&format!("`{} {}`", command_name, example)));
+ }
+
+ 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, &error_msg);
+
+ return Ok(());
+ }
+
+ let _ = ctx.channel_id.unwrap().send_message(|m| m
+ .embed(|mut e| {
+ e = e.colour(Colour::rosewater())
+ .description("To get help with an 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;
+
+ for (n, cmd) in remove_aliases(&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: &mut Context,
+ _: &Message,
+ groups: HashMap<String, Arc<CommandGroup>>,
+ args: &[String]) -> Result<(), String> {
+ if !args.is_empty() {
+ let name = args.join(" ");
+
+ for (group_name, group) in groups {
+ let mut found: Option<(&String, &Command)> = None;
+
+ for (command_name, command) in &group.commands {
+ let with_prefix = if let Some(ref prefix) = group.prefix {
+ format!("{} {}", prefix, command_name)
+ } else {
+ command_name.to_owned()
+ };
+
+ if name == with_prefix || name == *command_name {
+ match *command {
+ CommandOrAlias::Command(ref cmd) => {
+ found = Some((command_name, cmd));
+ },
+ CommandOrAlias::Alias(ref name) => {
+ let _ = ctx.channel_id.unwrap().say(&format!("Did you mean {:?}?", name));
+ return Ok(());
+ }
+ }
+ }
+ }
+
+ if let Some((command_name, command)) = found {
+ if !command.help_available {
+ let _ = ctx.channel_id.unwrap().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", command_name, usage);
+ }
+
+ if let Some(ref example) = command.example {
+ let _ = write!(result, "**Sample usage:** `{} {}`\n", command_name, example);
+ }
+
+ if group_name != "Ungrouped" {
+ let _ = write!(result, "**Group:** {}\n", group_name);
+ }
+
+ result.push_str("**Available:** ");
+ result.push_str(if command.dm_only {
+ "Only in DM"
+ } else if command.guild_only {
+ "Only in guilds"
+ } else {
+ "In DM and guilds"
+ });
+ result.push_str("\n");
+
+ let _ = ctx.channel_id.unwrap().say(&result);
+
+ return Ok(());
+ }
+ }
+
+ let _ = ctx.channel_id.unwrap().say(&format!("**Error**: Command `{}` not found.", name));
+
+ return Ok(());
+ }
+
+ let mut result = "**Commands**\nTo get help with an individual command, pass its \
+ name as an argument to this command.\n\n"
+ .to_string();
+
+ for (group_name, group) in groups {
+ let _ = write!(result, "**{}:** ", group_name);
+
+ if let Some(ref x) = group.prefix {
+ let _ = write!(result, "(prefix: `{}`): ", x);
+ }
+
+ let mut no_commands = true;
+
+ for (n, cmd) in remove_aliases(&group.commands) {
+ if cmd.help_available {
+ let _ = write!(result, "`{}` ", n);
+
+ no_commands = false;
+ }
+ }
+
+ if no_commands {
+ result.push_str("*[No Commands]*");
+ }
+
+ result.push('\n');
+ }
+
+ let _ = ctx.channel_id.unwrap().say(&result);
+
+ Ok(())
+}
diff --git a/src/framework/mod.rs b/src/framework/mod.rs
new file mode 100644
index 0000000..99e7c44
--- /dev/null
+++ b/src/framework/mod.rs
@@ -0,0 +1,665 @@
+//! The framework is a customizable method of separating commands.
+//!
+//! This is used in combination with [`Client::with_framework`].
+//!
+//! The framework has a number of configurations, and can have any number of
+//! commands bound to it. The primary purpose of it is to offer the utility of
+//! not needing to manually match message content strings to determine if a
+//! message is a command.
+//!
+//! Additionally, "checks" can be added to commands, to ensure that a certain
+//! condition is met prior to calling a command; this could be a check that the
+//! user who posted a message owns the bot, for example.
+//!
+//! Each command has a given named, and an associated function/closure. For
+//! example, you might have two commands: `"ping"` and `"weather"`. These each
+//! have an associated function that are called if the framework determines
+//! that a message is of that command.
+//!
+//! Assuming a command prefix of `"~"`, then the following would occur with the
+//! two previous commands:
+//!
+//! ```ignore
+//! ~ping // calls the ping command's function
+//! ~pin // does not
+//! ~ ping // _does_ call it _if_ the `allow_whitespace` option is enabled
+//! ~~ping // does not
+//! ```
+//!
+//! # Examples
+//!
+//! Configuring a Client with a framework, which has a prefix of `"~"` and a
+//! ping and about command:
+//!
+//! ```rust,ignore
+//! use serenity::client::{Client, Context};
+//! use serenity::model::Message;
+//! use std::env;
+//!
+//! let mut client = Client::login(&env::var("DISCORD_BOT_TOKEN").unwrap());
+//!
+//! client.with_framework(|f| f
+//! .configure(|c| c.prefix("~"))
+//! .command("about", |c| c.exec_str("A simple test bot"))
+//! .command("ping", |c| c.exec(ping)));
+//!
+//! command!(about(_context, message) {
+//! let _ = message.channel_id.say("A simple test bot");
+//! });
+//!
+//! command!(ping(_context, message) {
+//! let _ = message.channel_id.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::buckets::{Bucket, MemberRatelimit, Ratelimit};
+pub use self::command::{Command, CommandType, CommandGroup, CommandOrAlias};
+pub use self::configuration::Configuration;
+pub use self::create_command::CreateCommand;
+pub use self::create_group::CreateGroup;
+
+use self::command::{AfterHook, BeforeHook};
+use std::collections::HashMap;
+use std::default::Default;
+use std::sync::Arc;
+use std::thread;
+use ::client::Context;
+use ::model::{Message, UserId};
+use ::model::permissions::Permissions;
+use ::utils;
+
+#[cfg(feature="cache")]
+use ::client::CACHE;
+#[cfg(feature="cache")]
+use ::model::Channel;
+
+/// A macro to generate "named parameters". This is useful to avoid manually
+/// using the "arguments" parameter and manually parsing types.
+///
+/// This is meant for use with the command [`Framework`].
+///
+/// # Examples
+///
+/// Create a regular `ping` command which takes no arguments:
+///
+/// ```rust,ignore
+/// command!(ping(_context, message, _args) {
+/// if let Err(why) = message.reply("Pong!") {
+/// println!("Error sending pong: {:?}", why);
+/// }
+/// });
+/// ```
+///
+/// Create a command named `multiply` which accepts 2 floats and multiplies
+/// them, sending the product as a reply:
+///
+/// ```rust,ignore
+/// command!(multiply(_context, message, _args, first: f64, second: f64) {
+/// let product = first * second;
+///
+/// if let Err(why) = message.reply(&product.to_string()) {
+/// println!("Error sending product: {:?}", why);
+/// }
+/// });
+/// ```
+///
+/// [`Framework`]: framework/index.html
+#[macro_export]
+macro_rules! command {
+ ($fname:ident($c:ident) $b:block) => {
+ #[allow(unreachable_code, unused_mut)]
+ pub fn $fname(mut $c: &mut $crate::client::Context, _: &$crate::model::Message, _: Vec<String>) -> ::std::result::Result<(), String> {
+ $b
+
+ Ok(())
+ }
+ };
+ ($fname:ident($c:ident, $m:ident) $b:block) => {
+ #[allow(unreachable_code, unused_mut)]
+ pub fn $fname(mut $c: &mut $crate::client::Context, $m: &$crate::model::Message, _: Vec<String>) -> ::std::result::Result<(), String> {
+ $b
+
+ Ok(())
+ }
+ };
+ ($fname:ident($c:ident, $m:ident, $a:ident) $b:block) => {
+ #[allow(unreachable_code, unused_mut)]
+ pub fn $fname(mut $c: &mut $crate::client::Context, $m: &$crate::model::Message, $a: Vec<String>) -> ::std::result::Result<(), String> {
+ $b
+
+ Ok(())
+ }
+ };
+ ($fname:ident($c:ident, $m:ident, $a:ident, $($name:ident: $t:ty),*) $b:block) => {
+ #[allow(unreachable_code, unreachable_patterns, unused_mut)]
+ pub fn $fname(mut $c: &mut $crate::client::Context, $m: &$crate::model::Message, $a: Vec<String>) -> ::std::result::Result<(), String> {
+ let mut i = $a.iter();
+ let mut arg_counter = 0;
+
+ $(
+ arg_counter += 1;
+
+ let $name = match i.next() {
+ Some(v) => match v.parse::<$t>() {
+ Ok(v) => v,
+ Err(_) => return Err(format!("Failed to parse argument #{} of type {:?}",
+ arg_counter,
+ stringify!($t))),
+ },
+ None => return Err(format!("Missing argument #{} of type {:?}",
+ arg_counter,
+ stringify!($t))),
+ };
+ )*
+
+ drop(i);
+
+ $b
+
+ Ok(())
+ }
+ };
+}
+
+/// A enum representing all possible fail conditions under which a command won't
+/// be executed.
+pub enum DispatchError {
+ /// When a custom function check has failed.
+ CheckFailed,
+ /// When the requested command is disabled in bot configuration.
+ CommandDisabled(String),
+ /// When the user is blocked in bot configuration.
+ BlockedUser,
+ /// When the guild or its owner is blocked in bot configuration.
+ BlockedGuild,
+ /// When the command requester lacks specific required permissions.
+ LackOfPermissions(Permissions),
+ /// When the command requester has exceeded a ratelimit bucket. The attached
+ /// value is the time a requester has to wait to run the command again.
+ RateLimited(i64),
+ /// When the requested command can only be used in a direct message or group
+ /// channel.
+ OnlyForDM,
+ /// When the requested command can only be ran in guilds, or the bot doesn't
+ /// support DMs.
+ OnlyForGuilds,
+ /// When the requested command can only be used by bot owners.
+ OnlyForOwners,
+ /// When there are too few arguments.
+ NotEnoughArguments { min: i32, given: usize },
+ /// When there are too many arguments.
+ TooManyArguments { max: i32, given: usize },
+ /// When the command was requested by a bot user when they are set to be
+ /// ignored.
+ IgnoredBot,
+ /// When the bot ignores webhooks and a command was issued by one.
+ WebhookAuthor,
+}
+
+type DispatchErrorHook = Fn(Context, Message, DispatchError) + Send + Sync + 'static;
+
+/// A utility for easily managing dispatches to commands.
+///
+/// Refer to the [module-level documentation] for more information.
+///
+/// [module-level documentation]: index.html
+#[allow(type_complexity)]
+#[derive(Default)]
+pub struct Framework {
+ configuration: Configuration,
+ groups: HashMap<String, Arc<CommandGroup>>,
+ before: Option<Arc<BeforeHook>>,
+ dispatch_error_handler: Option<Arc<DispatchErrorHook>>,
+ buckets: HashMap<String, Bucket>,
+ after: Option<Arc<AfterHook>>,
+ /// Whether the framework has been "initialized".
+ ///
+ /// The framework is initialized once one of the following occurs:
+ ///
+ /// - configuration has been set;
+ /// - a command handler has been set;
+ /// - a command check has been set.
+ ///
+ /// This is used internally to determine whether or not - in addition to
+ /// dispatching to the [`Client::on_message`] handler - to have the
+ /// framework check if a [`Event::MessageCreate`] should be processed by
+ /// itself.
+ ///
+ /// [`Client::on_message`]: ../client/struct.Client.html#method.on_message
+ /// [`Event::MessageCreate`]: ../model/event/enum.Event.html#variant.MessageCreate
+ pub initialized: bool,
+ user_info: (u64, bool),
+}
+
+impl Framework {
+ /// Configures the framework, setting non-default values. All fields are
+ /// optional. Refer to [`Configuration::default`] for more information on
+ /// the default values.
+ ///
+ /// # Examples
+ ///
+ /// Configuring the framework for a [`Client`], setting the [`depth`] to 3,
+ /// [allowing whitespace], and setting the [`prefix`] to `"~"`:
+ ///
+ /// ```rust,no_run
+ /// use serenity::Client;
+ /// use std::env;
+ ///
+ /// let mut client = Client::login(&env::var("DISCORD_TOKEN").unwrap());
+ /// client.with_framework(|f| f
+ /// .configure(|c| c
+ /// .depth(3)
+ /// .allow_whitespace(true)
+ /// .prefix("~")));
+ /// ```
+ ///
+ /// [`Client`]: ../client/struct.Client.html
+ /// [`Configuration::default`]: struct.Configuration.html#method.default
+ /// [`depth`]: struct.Configuration.html#method.depth
+ /// [`prefix`]: struct.Configuration.html#method.prefix
+ /// [allowing whitespace]: struct.Configuration.html#method.allow_whitespace
+ pub fn configure<F>(mut self, f: F) -> Self
+ where F: FnOnce(Configuration) -> Configuration {
+ self.configuration = f(self.configuration);
+
+ 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)),
+ },
+ users: HashMap::new(),
+ });
+
+ self
+ }
+
+ /// Defines a bucket with only a `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,
+ },
+ users: HashMap::new(),
+ });
+
+ self
+ }
+
+ #[cfg(feature="cache")]
+ fn is_blocked_guild(&self, message: &Message) -> bool {
+ if let Some(Channel::Guild(channel)) = CACHE.read().unwrap().channel(message.channel_id) {
+ let guild_id = channel.read().unwrap().guild_id;
+ if self.configuration.blocked_guilds.contains(&guild_id) {
+ return true;
+ }
+
+ if let Some(guild) = guild_id.find() {
+ return self.configuration.blocked_users.contains(&guild.read().unwrap().owner_id);
+ }
+ }
+
+ false
+ }
+
+ #[cfg(feature="cache")]
+ fn has_correct_permissions(&self, command: &Arc<Command>, message: &Message) -> bool {
+ if !command.required_permissions.is_empty() {
+ if let Some(guild) = message.guild() {
+ let perms = guild.read().unwrap().permissions_for(message.channel_id, message.author.id);
+
+ return perms.contains(command.required_permissions);
+ }
+ }
+
+ true
+ }
+
+ fn checks_passed(&self, command: &Arc<Command>, mut context: &mut Context, message: &Message) -> bool {
+ for check in &command.checks {
+ if !(check)(&mut context, message) {
+ return false;
+ }
+ }
+
+ true
+ }
+
+ #[allow(too_many_arguments)]
+ fn should_fail(&mut self,
+ mut context: &mut Context,
+ message: &Message,
+ command: &Arc<Command>,
+ args: usize,
+ to_check: &str,
+ built: &str) -> Option<DispatchError> {
+ if self.configuration.ignore_bots && message.author.bot {
+ Some(DispatchError::IgnoredBot)
+ } else if self.configuration.ignore_webhooks && message.webhook_id.is_some() {
+ Some(DispatchError::WebhookAuthor)
+ } else if self.configuration.owners.contains(&message.author.id) {
+ None
+ } else {
+ if let Some(rate_limit) = command.bucket.clone().map(|x| self.ratelimit_time(x.as_str(), message.author.id.0)) {
+ if rate_limit > 0i64 {
+ return Some(DispatchError::RateLimited(rate_limit));
+ }
+ }
+
+ if let Some(x) = command.min_args {
+ if args < x as usize {
+ return Some(DispatchError::NotEnoughArguments {
+ min: x,
+ given: args
+ });
+ }
+ }
+
+ if let Some(x) = command.max_args {
+ if args > x as usize {
+ return Some(DispatchError::TooManyArguments {
+ max: x,
+ given: args
+ });
+ }
+ }
+
+ #[cfg(feature="cache")]
+ {
+ if self.is_blocked_guild(message) {
+ return Some(DispatchError::BlockedGuild);
+ }
+
+ if !self.has_correct_permissions(command, message) {
+ return Some(DispatchError::LackOfPermissions(command.required_permissions));
+ }
+
+ if (!self.configuration.allow_dm && message.is_private()) ||
+ (command.guild_only && message.is_private()) {
+ return Some(DispatchError::OnlyForGuilds);
+ }
+
+ if command.dm_only && !message.is_private() {
+ return Some(DispatchError::OnlyForDM);
+ }
+ }
+
+ if command.owners_only {
+ Some(DispatchError::OnlyForOwners)
+ } else if !self.checks_passed(command, &mut context, message) {
+ Some(DispatchError::CheckFailed)
+ } else if self.configuration.blocked_users.contains(&message.author.id) {
+ Some(DispatchError::BlockedUser)
+ } else if self.configuration.disabled_commands.contains(to_check) {
+ Some(DispatchError::CommandDisabled(to_check.to_owned()))
+ } else if self.configuration.disabled_commands.contains(built) {
+ Some(DispatchError::CommandDisabled(built.to_owned()))
+ } else {
+ None
+ }
+ }
+ }
+
+ #[allow(cyclomatic_complexity)]
+ #[doc(hidden)]
+ pub fn dispatch(&mut self, mut context: Context, message: Message) {
+ let res = command::positions(&mut context, &message.content, &self.configuration);
+
+ let positions = match res {
+ Some(mut positions) => {
+ // First, take out the prefixes that are as long as _or_ longer
+ // than the message, to avoid character boundary violations.
+ positions.retain(|p| *p < message.content.len());
+
+ // Ensure that there is _at least one_ position remaining. There
+ // is no point in continuing if there is not.
+ if positions.is_empty() {
+ return;
+ }
+
+ positions
+ },
+ None => return,
+ };
+
+ 'outer: for position in positions {
+ let mut built = String::new();
+ let round = message.content.chars()
+ .skip(position)
+ .collect::<String>();
+ let round = round.trim()
+ .split_whitespace()
+ .collect::<Vec<&str>>();
+
+ for i in 0..self.configuration.depth {
+ if i != 0 {
+ built.push(' ');
+ }
+
+ built.push_str(match round.get(i) {
+ Some(piece) => piece,
+ None => continue 'outer,
+ });
+
+ let groups = self.groups.clone();
+
+ for group in groups.values() {
+ let command_length = built.len();
+
+ if let Some(&CommandOrAlias::Alias(ref points_to)) = group.commands.get(&built) {
+ built = points_to.to_owned();
+ }
+
+ let to_check = if let Some(ref prefix) = group.prefix {
+ if built.starts_with(prefix) && command_length > prefix.len() + 1 {
+ built[(prefix.len() + 1)..].to_owned()
+ } else {
+ continue;
+ }
+ } else {
+ built.clone()
+ };
+
+ if let Some(&CommandOrAlias::Command(ref command)) = group.commands.get(&to_check) {
+ let before = self.before.clone();
+ let command = command.clone();
+ let after = self.after.clone();
+ let groups = self.groups.clone();
+
+ let args = {
+ let content = message.content[position..].trim();
+
+ if command.use_quotes {
+ utils::parse_quotes(&content[command_length..])
+ } else {
+ content[command_length..]
+ .split_whitespace()
+ .map(|arg| arg.to_owned())
+ .collect::<Vec<String>>()
+ }
+ };
+
+ if let Some(error) = self.should_fail(&mut context, &message, &command, args.len(), &to_check, &built) {
+ if let Some(ref handler) = self.dispatch_error_handler {
+ handler(context, message, error);
+ }
+ return;
+ }
+
+ thread::spawn(move || {
+ if let Some(before) = before {
+ if !(before)(&mut context, &message, &built) {
+ return;
+ }
+ }
+
+ let result = match command.exec {
+ CommandType::StringResponse(ref x) => {
+ let _ = &mut context.channel_id.unwrap().say(x);
+
+ Ok(())
+ },
+ CommandType::Basic(ref x) => {
+ (x)(&mut context, &message, args)
+ },
+ CommandType::WithCommands(ref x) => {
+ (x)(&mut context, &message, groups, &args)
+ }
+ };
+
+ if let Some(after) = after {
+ (after)(&mut context, &message, &built, result);
+ }
+ });
+
+ return;
+ }
+ }
+ }
+ }
+ }
+
+ /// Adds a function to be associated with a command, which will be called
+ /// when a command is used in a message.
+ ///
+ /// 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<F, S>(mut self, command_name: S, f: F) -> Self
+ where F: Fn(&mut Context, &Message, Vec<String>) -> Result<(), String> + Send + Sync + 'static,
+ S: Into<String> {
+ {
+ let ungrouped = self.groups.entry("Ungrouped".to_owned())
+ .or_insert_with(|| Arc::new(CommandGroup::default()));
+
+ if let Some(ref mut group) = Arc::get_mut(ungrouped) {
+ let name = command_name.into();
+
+ group.commands.insert(name, CommandOrAlias::Command(Arc::new(Command::new(f))));
+ }
+ }
+
+ 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<F, S>(mut self, command_name: S, f: F) -> Self
+ where F: FnOnce(CreateCommand) -> CreateCommand,
+ S: Into<String> {
+ {
+ let ungrouped = self.groups.entry("Ungrouped".to_owned())
+ .or_insert_with(|| Arc::new(CommandGroup::default()));
+
+ if let Some(ref mut group) = Arc::get_mut(ungrouped) {
+ let cmd = f(CreateCommand(Command::default())).0;
+ let name = command_name.into();
+
+ if let Some(ref prefix) = group.prefix {
+ for v in &cmd.aliases {
+ group.commands.insert(format!("{} {}", prefix, v.to_owned()), CommandOrAlias::Alias(format!("{} {}", prefix, name)));
+ }
+ } else {
+ for v in &cmd.aliases {
+ group.commands.insert(v.to_owned(), CommandOrAlias::Alias(name.clone()));
+ }
+ }
+
+ group.commands.insert(name, CommandOrAlias::Command(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
+ }
+
+ /// Specify the function that's called in case a command wasn't executed for one reason or another.
+ ///
+ /// DispatchError represents all possible fail conditions.
+ pub fn on_dispatch_error<F>(mut self, f: F) -> Self
+ where F: Fn(Context, Message, DispatchError) + Send + Sync + 'static {
+ self.dispatch_error_handler = Some(Arc::new(f));
+
+ self
+ }
+
+ /// Specify the function to be called prior to every command's execution.
+ /// If that function returns true, the command will be executed.
+ pub fn before<F>(mut self, f: F) -> Self
+ where F: Fn(&mut Context, &Message, &String) -> bool + Send + Sync + 'static {
+ self.before = Some(Arc::new(f));
+
+ self
+ }
+
+ /// 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(&mut Context, &Message, &String, Result<(), String>) + Send + Sync + 'static {
+ self.after = Some(Arc::new(f));
+
+ self
+ }
+
+ #[doc(hidden)]
+ pub fn update_current_user(&mut self, user_id: UserId, is_bot: bool) {
+ self.user_info = (user_id.0, is_bot);
+ }
+
+ 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)
+ }
+}