diff options
| author | Zeyla Hellyer <[email protected]> | 2017-05-22 17:02:00 -0700 |
|---|---|---|
| committer | Zeyla Hellyer <[email protected]> | 2017-05-22 17:02:00 -0700 |
| commit | 9969be60cf320797c37b317da24d9a08fd5eafa5 (patch) | |
| tree | f27bf7a57af95bbc11990b1edcea9cca99276964 /src/framework | |
| parent | Reasonably derive Debug on items (diff) | |
| download | serenity-9969be60cf320797c37b317da24d9a08fd5eafa5.tar.xz serenity-9969be60cf320797c37b317da24d9a08fd5eafa5.zip | |
Restructure modules
Modules are now separated into a fashion where the library can be used
for most use cases, without needing to compile the rest.
The core of serenity, with no features enabled, contains only the
struct (model) definitions, constants, and prelude. Models do not have
most functions compiled in, as that is separated into the `model`
feature.
The `client` module has been split into 3 modules: `client`, `gateway`,
and `http`.
`http` contains functions to interact with the REST API. `gateway`
contains the Shard to interact with the gateway, requiring `http` for
retrieving the gateway URL. `client` requires both of the other features
and acts as an abstracted interface over both the gateway and REST APIs,
handling the event loop.
The `builder` module has been separated from `utils`, and can now be
optionally compiled in. It and the `http` feature are required by the
`model` feature due to a large number of methods requiring access to
them.
`utils` now contains a number of utilities, such as the Colour struct, the
`MessageBuilder`, and mention parsing functions.
Each of the original `ext` modules are still featured, with `cache` not
requiring any feature to be enabled, `framework` requiring the `client`,
`model`, and `utils`, and `voice` requiring `gateway`.
In total the features and their requirements are:
- `builder`: none
- `cache`: none
- `client`: `gateway`, `http`
- `framework`: `client`, `model`, `utils`
- `gateway`: `http`
- `http`: none
- `model`: `builder`, `http`
- `utils`: none
- `voice`: `gateway`
The default features are `builder`, `cache`, `client`, `framework`,
`gateway`, `model`, `http`, and `utils`.
To help with forwards compatibility, modules have been re-exported from
their original locations.
Diffstat (limited to 'src/framework')
| -rw-r--r-- | src/framework/buckets.rs | 60 | ||||
| -rw-r--r-- | src/framework/command.rs | 164 | ||||
| -rw-r--r-- | src/framework/configuration.rs | 262 | ||||
| -rw-r--r-- | src/framework/create_command.rs | 226 | ||||
| -rw-r--r-- | src/framework/create_group.rs | 68 | ||||
| -rw-r--r-- | src/framework/help_commands.rs | 285 | ||||
| -rw-r--r-- | src/framework/mod.rs | 665 |
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) + } +} |