diff options
| author | Zeyla Hellyer <[email protected]> | 2017-08-19 09:36:15 -0700 |
|---|---|---|
| committer | Zeyla Hellyer <[email protected]> | 2017-08-19 09:39:44 -0700 |
| commit | 948b27ce74e8dce458d427d8159f2a821d4d7cec (patch) | |
| tree | bf82bedd1821ca210e4a9f08644581486738aed6 /src/framework/standard | |
| parent | Add html_root_url (diff) | |
| download | serenity-948b27ce74e8dce458d427d8159f2a821d4d7cec.tar.xz serenity-948b27ce74e8dce458d427d8159f2a821d4d7cec.zip | |
Move builtin framework impl to its own module
The framework is now moved in its entirity to the `framework` module,
with the `Framework` trait currently on its own and the builtin
implementation provided.
The builtin implementation has been renamed to "Standard".
Upgrade path:
Rename the `BuiltinFramework` import to `StandardFramework`. Instead of
importing builtin framework items from `serenity::framework`, import
them from `serenity::framework::standard`.
This is the beginning to #60. The root `framework` module (non-standard
implementation) will be built more by the time it's closed.
Diffstat (limited to 'src/framework/standard')
| -rw-r--r-- | src/framework/standard/buckets.rs | 58 | ||||
| -rw-r--r-- | src/framework/standard/command.rs | 157 | ||||
| -rw-r--r-- | src/framework/standard/configuration.rs | 451 | ||||
| -rw-r--r-- | src/framework/standard/create_command.rs | 245 | ||||
| -rw-r--r-- | src/framework/standard/create_group.rs | 86 | ||||
| -rw-r--r-- | src/framework/standard/help_commands.rs | 346 | ||||
| -rw-r--r-- | src/framework/standard/mod.rs | 1026 |
7 files changed, 2369 insertions, 0 deletions
diff --git a/src/framework/standard/buckets.rs b/src/framework/standard/buckets.rs new file mode 100644 index 0000000..f2c4486 --- /dev/null +++ b/src/framework/standard/buckets.rs @@ -0,0 +1,58 @@ +use chrono::Utc; +use std::collections::HashMap; +use std::default::Default; +use client::Context; +use model::{ChannelId, GuildId, UserId}; + +#[cfg(feature = "cache")] +type Check = Fn(&mut Context, Option<GuildId>, ChannelId, UserId) -> bool + 'static; + +#[cfg(not(feature = "cache"))] +type Check = Fn(&mut Context, ChannelId, UserId) -> bool + 'static; + +pub(crate) struct Ratelimit { + pub delay: i64, + pub limit: Option<(i64, i32)>, +} + +#[derive(Default)] +pub(crate) struct MemberRatelimit { + pub last_time: i64, + pub set_time: i64, + pub tickets: i32, +} + +pub(crate) struct Bucket { + pub ratelimit: Ratelimit, + pub users: HashMap<u64, MemberRatelimit>, + pub check: Option<Box<Check>>, +} + +impl Bucket { + pub fn take(&mut self, user_id: u64) -> i64 { + let time = Utc::now().timestamp(); + 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/standard/command.rs b/src/framework/standard/command.rs new file mode 100644 index 0000000..01c4c5e --- /dev/null +++ b/src/framework/standard/command.rs @@ -0,0 +1,157 @@ +use std::sync::Arc; +use super::Configuration; +use client::Context; +use model::{Message, Permissions}; +use std::collections::HashMap; + +pub type Check = Fn(&mut Context, &Message, &[String], &Arc<Command>) -> bool + 'static; +pub type Exec = Fn(&mut Context, &Message, Vec<String>, String) -> Result<(), String> + 'static; +pub type Help = Fn(&mut Context, + &Message, + HashMap<String, Arc<CommandGroup>>, + &[String]) + -> Result<(), String> + + 'static; +pub type BeforeHook = Fn(&mut Context, &Message, &str) -> bool + 'static; +pub type AfterHook = Fn(&mut Context, &Message, &str, Result<(), String>) + 'static; +pub(crate) type InternalCommand = Arc<Command>; +pub type PrefixCheck = Fn(&mut Context, &Message) -> Option<String> + 'static; + +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, + pub(crate) aliases: Vec<String>, +} + +impl Command { + pub fn new<F>(f: F) -> Self + where F: Fn(&mut Context, &Message, Vec<String>, String) -> Result<(), String> + '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, msg: &Message, 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(&msg.content, conf) { + positions.push(mention_end); + } else if let Some(ref func) = conf.dynamic_prefix { + if let Some(x) = func(ctx, msg) { + if msg.content.starts_with(&x) { + positions.push(x.len()); + } + } else { + for n in &conf.prefixes { + if msg.content.starts_with(n) { + positions.push(n.len()); + } + } + } + } else { + for n in &conf.prefixes { + if msg.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() { + find_mention_end(&msg.content, conf).map(|mention_end| { + let mut positions = vec![mention_end]; + + if conf.allow_whitespace { + positions.insert(0, mention_end + 1); + } + + positions + }) + } else { + None + } +} + +fn find_mention_end(content: &str, conf: &Configuration) -> Option<usize> { + conf.on_mention.as_ref().and_then(|mentions| { + mentions + .iter() + .find(|mention| content.starts_with(&mention[..])) + .map(|m| m.len()) + }) +} diff --git a/src/framework/standard/configuration.rs b/src/framework/standard/configuration.rs new file mode 100644 index 0000000..491bbc4 --- /dev/null +++ b/src/framework/standard/configuration.rs @@ -0,0 +1,451 @@ +use std::collections::HashSet; +use std::default::Default; +use super::command::PrefixCheck; +use client::Context; +use http; +use model::{GuildId, Message, 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::prelude::EventHandler; +/// struct Handler; +/// +/// impl EventHandler for Handler {} +/// use serenity::Client; +/// use std::env; +/// use serenity::framework::StandardFramework; +/// +/// let mut client = Client::new(&env::var("DISCORD_BOT_TOKEN").unwrap(), Handler); +/// +/// client.with_framework(StandardFramework::new() +/// .configure(|c| c.on_mention(true).prefix("~"))); +/// ``` +/// +/// [`Client`]: ../../client/struct.Client.html +/// [`Framework`]: struct.Framework.html +pub struct Configuration { + #[doc(hidden)] + pub allow_dm: bool, + #[doc(hidden)] + pub allow_whitespace: bool, + #[doc(hidden)] + pub blocked_guilds: HashSet<GuildId>, + #[doc(hidden)] + pub blocked_users: HashSet<UserId>, + #[doc(hidden)] + pub depth: usize, + #[doc(hidden)] + pub disabled_commands: HashSet<String>, + #[doc(hidden)] + pub dynamic_prefix: Option<Box<PrefixCheck>>, + #[doc(hidden)] + pub ignore_bots: bool, + #[doc(hidden)] + pub ignore_webhooks: bool, + #[doc(hidden)] + pub on_mention: Option<Vec<String>>, + #[doc(hidden)] + pub owners: HashSet<UserId>, + #[doc(hidden)] + pub prefixes: Vec<String>, + #[doc(hidden)] + pub delimiters: Vec<String>, +} + +impl Configuration { + /// 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 + } + + /// 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 + } + + /// HashSet of guild Ids where commands will be ignored. + /// + /// # Examples + /// + /// Create a HashSet in-place: + /// + /// ```rust + /// # use serenity::prelude::*; + /// # struct Handler; + /// # + /// # impl EventHandler for Handler {} + /// # let mut client = Client::new("token", Handler); + /// use serenity::model::GuildId; + /// use serenity::framework::StandardFramework; + /// + /// client.with_framework(StandardFramework::new().configure(|c| c + /// .blocked_guilds(vec![GuildId(7), GuildId(77)].into_iter().collect()))); + /// ``` + pub fn blocked_guilds(mut self, guilds: HashSet<GuildId>) -> Self { + self.blocked_guilds = guilds; + + self + } + + /// HashSet of user Ids whose commands will be ignored. + /// Guilds owned by user Ids will also be ignored. + /// + /// # Examples + /// + /// Create a HashSet in-place: + /// + /// ```rust + /// # use serenity::prelude::*; + /// # struct Handler; + /// # + /// # impl EventHandler for Handler {} + /// # let mut client = Client::new("token", Handler); + /// use serenity::model::UserId; + /// use serenity::framework::StandardFramework; + /// + /// client.with_framework(StandardFramework::new().configure(|c| c + /// .blocked_users(vec![UserId(7), UserId(77)].into_iter().collect()))); + /// ``` + pub fn blocked_users(mut self, users: HashSet<UserId>) -> Self { + self.blocked_users = users; + + 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. + /// + /// # Examples + /// + /// Ignore a set of commands, assuming they exist: + /// + /// ```rust + /// # use serenity::prelude::*; + /// # struct Handler; + /// # + /// # impl EventHandler for Handler {} + /// # let mut client = Client::new("token", Handler); + /// use serenity::framework::StandardFramework; + /// + /// let disabled = vec!["ping"].into_iter().map(|x| x.to_owned()).collect(); + /// + /// client.with_framework(StandardFramework::new() + /// .command("ping", |c| c.exec_str("pong!")) + /// .configure(|c| c.disabled_commands(disabled))); + /// ``` + 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::prelude::*; + /// # struct Handler; + /// # + /// # impl EventHandler for Handler {} + /// # let mut client = Client::new("token", Handler); + /// use serenity::framework::StandardFramework; + /// + /// client.with_framework(StandardFramework::new() + /// .command("ping", |c| c.exec_str("Pong!")) + /// .configure(|c| c.dynamic_prefix(|_, msg| { + /// Some(if msg.channel_id.0 % 5 == 0 { + /// "!" + /// } else { + /// "~" + /// }.to_owned()) + /// }))); + /// ``` + pub fn dynamic_prefix<F>(mut self, dynamic_prefix: F) -> Self + where F: Fn(&mut Context, &Message) -> Option<String> + Send + Sync + 'static { + self.dynamic_prefix = Some(Box::new(dynamic_prefix)); + + self + } + + /// 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 + } + + /// 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 + } + + /// 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. + /// + /// # Examples + /// + /// Create a HashSet in-place: + /// + /// ```rust + /// # use serenity::prelude::*; + /// # struct Handler; + /// # + /// # impl EventHandler for Handler {} + /// # let mut client = Client::new("token", Handler); + /// use serenity::model::UserId; + /// use serenity::framework::StandardFramework; + /// + /// client.with_framework(StandardFramework::new().configure(|c| c + /// .owners(vec![UserId(7), UserId(77)].into_iter().collect()))); + /// ``` + /// + /// Create a HashSet beforehand: + /// + /// ```rust + /// # use serenity::prelude::*; + /// # struct Handler; + /// # + /// # impl EventHandler for Handler {} + /// # let mut client = Client::new("token", Handler); + /// use serenity::model::UserId; + /// use std::collections::HashSet; + /// use serenity::framework::StandardFramework; + /// + /// let mut set = HashSet::new(); + /// set.insert(UserId(7)); + /// set.insert(UserId(77)); + /// + /// client.with_framework(StandardFramework::new().configure(|c| c.owners(set))); + /// ``` + pub fn owners(mut self, user_ids: HashSet<UserId>) -> Self { + self.owners = user_ids; + + self + } + + /// Sets the prefix to respond to. A prefix can be a string slice of any + /// non-zero length. + /// + /// # Examples + /// + /// Assign a basic prefix: + /// + /// ```rust + /// # use serenity::prelude::*; + /// # struct Handler; + /// # + /// # impl EventHandler for Handler {} + /// # let mut client = Client::new("token", Handler); + /// # + /// use serenity::framework::StandardFramework; + /// + /// client.with_framework(StandardFramework::new().configure(|c| c + /// .prefix("!"))); + /// ``` + pub fn prefix(mut self, prefix: &str) -> Self { + self.prefixes = vec![prefix.to_owned()]; + + self + } + + /// Sets the prefixes to respond to. Each can be a string slice of any + /// non-zero length. + /// + /// # Examples + /// + /// Assign a set of prefixes the bot can respond to: + /// + /// ```rust + /// # use serenity::prelude::*; + /// # struct Handler; + /// # + /// # impl EventHandler for Handler {} + /// # let mut client = Client::new("token", Handler); + /// # + /// use serenity::framework::StandardFramework; + /// + /// client.with_framework(StandardFramework::new().configure(|c| c + /// .prefixes(vec!["!", ">", "+"]))); + /// ``` + pub fn prefixes(mut self, prefixes: Vec<&str>) -> Self { + self.prefixes = prefixes.iter().map(|x| x.to_string()).collect(); + + self + } + + /// Sets a delimiter to be used when splitting the content after a command. + /// + /// # Examples + /// + /// Have the args be seperated by a comma and a space: + /// + /// ```rust + /// # use serenity::prelude::*; + /// # struct Handler; + /// # + /// # impl EventHandler for Handler {} + /// # let mut client = Client::new("token", Handler); + /// # + /// use serenity::framework::StandardFramework; + /// + /// client.with_framework(StandardFramework::new().configure(|c| c + /// .delimiter(", "))); + /// ``` + pub fn delimiter(mut self, delimiter: &str) -> Self { + self.delimiters.push(delimiter.to_string()); + + self + } + + /// Sets multiple delimiters to be used when splitting the content after a command. + /// Additionally cleans the default delimiter from the vector. + /// + /// # Examples + /// + /// Have the args be seperated by a comma and a space; and a regular space: + /// + /// ```rust + /// # use serenity::prelude::*; + /// # struct Handler; + /// # + /// # impl EventHandler for Handler {} + /// # let mut client = Client::new("token", Handler); + /// # + /// use serenity::framework::StandardFramework; + /// + /// client.with_framework(StandardFramework::new().configure(|c| c + /// .delimiters(vec![", ", " "]))); + /// ``` + pub fn delimiters(mut self, delimiters: Vec<&str>) -> Self { + self.delimiters.clear(); + self.delimiters.extend( + delimiters.into_iter().map(|s| s.to_string()), + ); + + 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` + /// - **delimiters** to vec![" "] + 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, + delimiters: vec![" ".to_string()], + } + } +} diff --git a/src/framework/standard/create_command.rs b/src/framework/standard/create_command.rs new file mode 100644 index 0000000..160a508 --- /dev/null +++ b/src/framework/standard/create_command.rs @@ -0,0 +1,245 @@ +pub use super::{Command, CommandGroup, CommandType}; + +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 multiple aliases. + pub fn batch_known_as(mut self, names: Vec<&str>) -> Self { + self.0.aliases.extend( + names.into_iter().map(|n| n.to_owned()), + ); + + self + } + + /// Adds a ratelimit bucket. + pub fn bucket(mut self, bucket: &str) -> Self { + self.0.bucket = Some(bucket.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::prelude::EventHandler; + /// # struct Handler; + /// # impl EventHandler for Handler {} + /// use serenity::client::{Client, Context}; + /// use serenity::framework::standard::{Command, StandardFramework}; + /// use serenity::model::Message; + /// use std::env; + /// use std::sync::Arc; + /// + /// let mut client = Client::new(&env::var("DISCORD_TOKEN").unwrap(), Handler); + /// + /// client.with_framework(StandardFramework::new() + /// .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>, _original_msg: + /// String) -> Result<(), + /// String> { + /// let _ = message.channel_id.say("Pong!"); + /// + /// Ok(()) + /// } + /// + /// fn owner_check(_context: &mut Context, message: &Message, _: &[String], _: + /// &Arc<Command>) -> 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, &[String], &Arc<Command>) -> 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>, 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> + + '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 + } + + /// 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 + } + + /// 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/standard/create_group.rs b/src/framework/standard/create_group.rs new file mode 100644 index 0000000..b45c41e --- /dev/null +++ b/src/framework/standard/create_group.rs @@ -0,0 +1,86 @@ +pub use super::command::{Command, CommandGroup, CommandType}; +pub(crate) use super::command::CommandOrAlias; +pub use super::create_command::CreateCommand; + +use std::default::Default; +use std::sync::Arc; +use client::Context; +use model::Message; + +/// 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"))) +/// ``` +#[derive(Default)] +pub struct CreateGroup(pub CommandGroup); + +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>, 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/standard/help_commands.rs b/src/framework/standard/help_commands.rs new file mode 100644 index 0000000..a7c20fd --- /dev/null +++ b/src/framework/standard/help_commands.rs @@ -0,0 +1,346 @@ +//! 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::framework::standard::help_commands; +//! use serenity::Client; +//! use std::env; +//! +//! let mut client = Client::new(&env::var("DISCORD_TOKEN").unwrap()); +//! use serenity::framework::StandardFramework; +//! +//! client.with_framework(StandardFramework::new() +//! .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::{ChannelId, Message}; +use utils::Colour; + +fn error_embed(channel_id: &ChannelId, input: &str) { + let _ = channel_id.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 +} + +/// Posts an embed showing each individual command group and its commands. +/// +/// # Examples +/// +/// Use the command with `exec_help`: +/// +/// ```rust +/// # use serenity::prelude::*; +/// # struct Handler; +/// # +/// # impl EventHandler for Handler {} +/// # let mut client = Client::new("token", Handler); +/// # +/// use serenity::framework::standard::{StandardFramework, help_commands}; +/// +/// client.with_framework(StandardFramework::new() +/// .command("help", |c| c.exec_help(help_commands::with_embeds))); +/// ``` +pub fn with_embeds(_: &mut Context, + msg: &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(&msg.channel_id, &format!("Did you mean \"{}\"?", name)); + + return Ok(()); + }, + } + } + } + + if let Some((command_name, command)) = found { + if !command.help_available { + error_embed(&msg.channel_id, "**Error**: No help available."); + + return Ok(()); + } + + let _ = msg.channel_id.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(&msg.channel_id, &error_msg); + + return Ok(()); + } + + let _ = msg.channel_id.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.", + ); + + let mut group_names = groups.keys().collect::<Vec<_>>(); + group_names.sort(); + + for group_name in group_names { + let group = &groups[group_name]; + let mut desc = String::new(); + + if let Some(ref x) = group.prefix { + let _ = write!(desc, "Prefix: {}\n", x); + } + + let mut has_commands = false; + + let commands = remove_aliases(&group.commands); + let mut command_names = commands.keys().collect::<Vec<_>>(); + command_names.sort(); + + for name in command_names { + let cmd = &commands[name]; + + if cmd.help_available { + let _ = write!(desc, "`{}`\n", name); + + has_commands = true; + } + } + + if has_commands { + e = e.field(|f| f.name(group_name).value(&desc)); + } + } + + e + }) + }); + + Ok(()) +} + +/// Posts formatted text displaying each individual command group and its commands. +/// +/// # Examples +/// +/// Use the command with `exec_help`: +/// +/// ```rust +/// # use serenity::prelude::*; +/// # struct Handler; +/// # +/// # impl EventHandler for Handler {} +/// # let mut client = Client::new("token", Handler); +/// # +/// use serenity::framework::standard::{StandardFramework, help_commands}; +/// +/// client.with_framework(StandardFramework::new() +/// .command("help", |c| c.exec_help(help_commands::plain))); +/// ``` +pub fn plain(_: &mut Context, + msg: &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 _ = msg.channel_id.say(&format!("Did you mean {:?}?", name)); + return Ok(()); + }, + } + } + } + + if let Some((command_name, command)) = found { + if !command.help_available { + let _ = msg.channel_id.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); + } + + let only = if command.dm_only { + "Only in DM" + } else if command.guild_only { + "Only in guilds" + } else { + "In DM and guilds" + }; + + result.push_str("**Available:** "); + result.push_str(only); + result.push_str("\n"); + + let _ = msg.channel_id.say(&result); + + return Ok(()); + } + } + + let _ = msg.channel_id.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(); + + let mut group_names = groups.keys().collect::<Vec<_>>(); + group_names.sort(); + + for group_name in group_names { + let group = &groups[group_name]; + let mut group_help = String::new(); + + let commands = remove_aliases(&group.commands); + let mut command_names = commands.keys().collect::<Vec<_>>(); + command_names.sort(); + + for name in command_names { + let cmd = &commands[name]; + + if cmd.help_available { + let _ = write!(group_help, "`{}` ", name); + } + } + + if !group_help.is_empty() { + let _ = write!(result, "**{}:** ", group_name); + + if let Some(ref x) = group.prefix { + let _ = write!(result, "(prefix: `{}`): ", x); + } + + result.push_str(&group_help); + result.push('\n'); + } + } + + let _ = msg.channel_id.say(&result); + + Ok(()) +} diff --git a/src/framework/standard/mod.rs b/src/framework/standard/mod.rs new file mode 100644 index 0000000..f9c0e11 --- /dev/null +++ b/src/framework/standard/mod.rs @@ -0,0 +1,1026 @@ + +pub mod help_commands; + +mod command; +mod configuration; +mod create_command; +mod create_group; +mod buckets; + +pub(crate) use self::buckets::{Bucket, Ratelimit}; +pub use self::command::{Command, CommandGroup, CommandType}; +pub use self::command::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 client::Context; +use super::Framework; +use model::{ChannelId, GuildId, Message, UserId}; +use model::permissions::Permissions; +use utils; +use tokio_core::reactor::Handle; +use itertools::Itertools; +use regex::Regex; +use regex::escape; + +#[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>, + _: 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>, + _: 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>, + _: 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, + _: Vec<String>, + $a: String) + -> ::std::result::Result<(), String> { + $b + + Ok(()) + } + }; + ($fname:ident($c:ident, $m:ident, $a:ident, @$f:ident) $b:block) => { + #[allow(unreachable_code, unused_mut)] + pub fn $fname(mut $c: &mut $crate::client::Context, + $m: &$crate::model::Message, + $a: Vec<String>, + $f: 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>, + _: 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(Arc<Command>), + /// 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) + 'static; + +/// A utility for easily managing dispatches to commands. +/// +/// Refer to the [module-level documentation] for more information. +/// +/// [module-level documentation]: index.html +#[derive(Default)] +pub struct StandardFramework { + 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 [`EventHandler::on_message`] handler - to have the + /// framework check if a [`Event::MessageCreate`] should be processed by + /// itself. + /// + /// [`EventHandler::on_message`]: + /// ../client/event_handler/trait.EventHandler.html#method.on_message + /// [`Event::MessageCreate`]: ../model/event/enum.Event.html#variant.MessageCreate + pub initialized: bool, + user_info: (u64, bool), +} + +impl StandardFramework { + pub fn new() -> Self { StandardFramework::default() } + + /// 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::prelude::EventHandler; + /// # struct Handler; + /// # impl EventHandler for Handler {} + /// use serenity::Client; + /// use serenity::framework::StandardFramework; + /// use std::env; + /// + /// let mut client = Client::new(&env::var("DISCORD_TOKEN").unwrap(), Handler); + /// client.with_framework(StandardFramework::new() + /// .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`. + /// + /// # Examples + /// + /// Create and use a bucket that limits a command to 3 uses per 10 seconds with + /// a 2 second delay inbetween invocations: + /// + /// ```rust + /// # use serenity::prelude::*; + /// # struct Handler; + /// # + /// # impl EventHandler for Handler {} + /// # let mut client = Client::new("token", Handler); + /// # + /// use serenity::framework::StandardFramework; + /// + /// client.with_framework(StandardFramework::new() + /// .bucket("basic", 2, 10, 3) + /// .command("ping", |c| c + /// .bucket("basic") + /// .exec_str("pong!"))); + /// ``` + 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(), + check: None, + }, + ); + + self + } + + /// Same as [`bucket`] but with a check added. + /// + /// # Examples + /// + /// ```rust + /// # use serenity::prelude::*; + /// # struct Handler; + /// # + /// # impl EventHandler for Handler {} + /// # let mut client = Client::new("token", Handler); + /// # + /// use serenity::framework::StandardFramework; + /// + /// client.with_framework(StandardFramework::new() + /// .complex_bucket("basic", 2, 10, 3, |_, guild_id, channel_id, user_id| { + /// // check if the guild is `123` and the channel where the command(s) was called: + /// // `456` + /// // and if the user who called the command(s) is `789` + /// // otherwise don't apply the bucket at all. + /// guild_id.is_some() && guild_id.unwrap() == 123 && channel_id == 456 + /// && user_id == 789 + /// }) + /// .command("ping", |c| c + /// .bucket("basic") + /// .exec_str("pong!"))); + /// ``` + /// + /// [`bucket`]: #method.bucket + #[cfg(feature = "cache")] + pub fn complex_bucket<S, Check>(mut self, + s: S, + delay: i64, + time_span: i64, + limit: i32, + check: Check) + -> Self + where Check: Fn(&mut Context, Option<GuildId>, ChannelId, UserId) -> bool + 'static, + S: Into<String> { + self.buckets.insert( + s.into(), + Bucket { + ratelimit: Ratelimit { + delay, + limit: Some((time_span, limit)), + }, + users: HashMap::new(), + check: Some(Box::new(check)), + }, + ); + + self + } + + /// Same as [`bucket`] but with a check added. + /// + /// # Examples + /// + /// ```rust + /// # use serenity::prelude::*; + /// # struct Handler; + /// # + /// # impl EventHandler for Handler {} + /// # let mut client = Client::new("token", Handler); + /// # + /// use serenity::framework::StandardFramework; + /// + /// client.with_framework(StandardFramework::new() + /// .complex_bucket("basic", 2, 10, 3, |_, channel_id, user_id| { + /// // check if the channel's id where the command(s) was called is `456` + /// // and if the user who called the command(s) is `789` + /// // otherwise don't apply the bucket at all. + /// channel_id == 456 && user_id == 789 + /// }) + /// .command("ping", |c| c + /// .bucket("basic") + /// .exec_str("pong!"))); + /// ``` + /// + /// [`bucket`]: #method.bucket + #[cfg(not(feature = "cache"))] + pub fn complex_bucket<S, Check>(mut self, + s: S, + delay: i64, + time_span: i64, + limit: i32, + check: Check) + -> Self + where Check: Fn(&mut Context, ChannelId, UserId) -> bool + 'static, S: Into<String> { + self.buckets.insert( + s.into(), + Bucket { + ratelimit: Ratelimit { + delay, + limit: Some((time_span, limit)), + }, + users: HashMap::new(), + check: Some(Box::new(check)), + }, + ); + + self + } + + /// Defines a bucket with only a `delay` between each command. + /// + /// # Examples + /// + /// Create and use a simple bucket that has a 2 second delay between invocations: + /// + /// ```rust + /// # use serenity::prelude::*; + /// # struct Handler; + /// # + /// # impl EventHandler for Handler {} + /// # let mut client = Client::new("token", Handler); + /// # + /// use serenity::framework::StandardFramework; + /// + /// client.with_framework(StandardFramework::new() + /// .simple_bucket("simple", 2) + /// .command("ping", |c| c + /// .bucket("simple") + /// .exec_str("pong!"))); + /// ``` + 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(), + check: None, + }, + ); + + 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 + } + + #[allow(too_many_arguments)] + fn should_fail(&mut self, + mut context: &mut Context, + message: &Message, + command: &Arc<Command>, + args: &[String], + 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(ref bucket) = command.bucket { + if let Some(ref mut bucket) = self.buckets.get_mut(bucket) { + let rate_limit = bucket.take(message.author.id.0); + match bucket.check { + Some(ref check) => { + let apply = + feature_cache! {{ + let guild_id = message.guild_id(); + (check)(context, guild_id, message.channel_id, message.author.id) + } else { + (check)(context, message.channel_id, message.author.id) + }}; + + if apply && rate_limit > 0i64 { + return Some(DispatchError::RateLimited(rate_limit)); + } + }, + None => { + if rate_limit > 0i64 { + return Some(DispatchError::RateLimited(rate_limit)); + } + }, + } + } + } + + let arg_len = args.len(); + + if let Some(x) = command.min_args { + if arg_len < x as usize { + return Some(DispatchError::NotEnoughArguments { + min: x, + given: arg_len, + }); + } + } + + if let Some(x) = command.max_args { + if arg_len > x as usize { + return Some(DispatchError::TooManyArguments { + max: x, + given: arg_len, + }); + } + } + + #[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.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 { + let all_passed = command.checks.iter().all(|check| { + check(&mut context, message, args, command) + }); + + if all_passed { + None + } else { + Some(DispatchError::CheckFailed(command.to_owned())) + } + } + } + } + + /// 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. + /// + /// Prior to v0.2.0, 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 + /// + /// # Examples + /// + /// Create and use a simple command: + /// + /// ```rust + /// # #[macro_use] extern crate serenity; + /// # + /// # fn main() { + /// # use serenity::prelude::*; + /// # struct Handler; + /// # + /// # impl EventHandler for Handler {} + /// # let mut client = Client::new("token", Handler); + /// # + /// use serenity::framework::StandardFramework; + /// + /// client.with_framework(StandardFramework::new().on("ping", ping)); + /// + /// command!(ping(_ctx, msg) { + /// let _ = msg.channel_id.say("pong!"); + /// }); + /// # } + /// ``` + pub fn on<F, S>(mut self, command_name: S, f: F) -> Self + where F: Fn(&mut Context, &Message, Vec<String>, String) -> Result<(), String> + '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), + 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 + } + + /// Adds a group which can organize several related commands. + /// Groups are taken into account when using + /// `serenity::framework::standard::help_commands`. + /// + /// # Examples + /// + /// Creating a simple group: + /// + /// ```rust + /// # use serenity::prelude::*; + /// # struct Handler; + /// # + /// # impl EventHandler for Handler {} + /// # let mut client = Client::new("token", Handler); + /// # + /// use serenity::framework::StandardFramework; + /// + /// client.with_framework(StandardFramework::new() + /// .group("ping-pong", |g| g + /// .command("ping", |c| c.exec_str("pong!")) + /// .command("pong", |c| c.exec_str("ping!")))); + /// ``` + 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. + /// + /// # Examples + /// + /// Making a simple argument error responder: + /// + /// ```rust + /// # use serenity::prelude::*; + /// # struct Handler; + /// # + /// # impl EventHandler for Handler {} + /// # let mut client = Client::new("token", Handler); + /// use serenity::framework::standard::DispatchError::{NotEnoughArguments, TooManyArguments}; + /// use serenity::framework::StandardFramework; + /// + /// client.with_framework(StandardFramework::new() + /// .on_dispatch_error(|_, msg, error| { + /// match error { + /// NotEnoughArguments { min, given } => { + /// let s = format!("Need {} arguments, but only got {}.", min, given); + /// + /// let _ = msg.channel_id.say(&s); + /// }, + /// TooManyArguments { max, given } => { + /// let s = format!("Max arguments allowed is {}, but got {}.", max, given); + /// + /// let _ = msg.channel_id.say(&s); + /// }, + /// _ => println!("Unhandled dispatch error."), + /// } + /// })); + /// ``` + pub fn on_dispatch_error<F>(mut self, f: F) -> Self + where F: Fn(Context, Message, DispatchError) + '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. + /// + /// # Examples + /// + /// Using `before` to log command usage: + /// + /// ```rust + /// # use serenity::prelude::*; + /// # struct Handler; + /// # + /// # impl EventHandler for Handler {} + /// # let mut client = Client::new("token", Handler); + /// # + /// use serenity::framework::StandardFramework; + /// + /// client.with_framework(StandardFramework::new() + /// .before(|ctx, msg, cmd_name| { + /// println!("Running command {}", cmd_name); + /// true + /// })); + /// ``` + /// + /// Using before to prevent command usage: + /// + /// ```rust + /// # use serenity::prelude::*; + /// # struct Handler; + /// # + /// # impl EventHandler for Handler {} + /// # let mut client = Client::new("token", Handler); + /// # + /// use serenity::framework::StandardFramework; + /// + /// client.with_framework(StandardFramework::new() + /// .before(|_, msg, cmd_name| { + /// if let Ok(channel) = msg.channel_id.get() { + /// // Don't run unless in nsfw channel + /// if !channel.is_nsfw() { + /// return false; + /// } + /// } + /// + /// println!("Running command {}", cmd_name); + /// + /// true + /// })); + /// ``` + /// + pub fn before<F>(mut self, f: F) -> Self + where F: Fn(&mut Context, &Message, &str) -> bool + '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. + /// + /// # Examples + /// + /// Using `after` to log command usage: + /// + /// ```rust + /// # use serenity::prelude::*; + /// # struct Handler; + /// # + /// # impl EventHandler for Handler {} + /// # let mut client = Client::new("token", Handler); + /// # + /// use serenity::framework::StandardFramework; + /// + /// client.with_framework(StandardFramework::new() + /// .after(|ctx, msg, cmd_name, error| { + /// // Print out an error if it happened + /// if let Err(why) = error { + /// println!("Error in {}: {:?}", cmd_name, why); + /// } + /// })); + /// ``` + pub fn after<F>(mut self, f: F) -> Self + where F: Fn(&mut Context, &Message, &str, Result<(), String>) + 'static { + self.after = Some(Arc::new(f)); + + self + } +} + +impl Framework for StandardFramework { + fn dispatch(&mut self, mut context: Context, message: Message, tokio_handle: &Handle) { + let res = command::positions(&mut context, &message, &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, content) = { + let mut content = message.content[position..].trim(); + content = content[command_length..].trim(); + + if command.use_quotes { + (utils::parse_quotes(content), content.to_string()) + } else { + let delimiters = &self.configuration.delimiters; + let regular_expression = delimiters + .iter() + .map(|delimiter| escape(delimiter)) + .join("|"); + + let regex = Regex::new(®ular_expression).unwrap(); + + ( + regex + .split(content) + .filter_map(|p| if p.is_empty() { + None + } else { + Some(p.to_string()) + }) + .collect::<Vec<_>>(), + content.to_string(), + ) + } + }; + + if let Some(error) = self.should_fail( + &mut context, + &message, + &command, + &args, + &to_check, + &built, + ) { + if let Some(ref handler) = self.dispatch_error_handler { + handler(context, message, error); + } + return; + } + + tokio_handle.spawn_fn(move || { + if let Some(before) = before { + if !(before)(&mut context, &message, &built) { + return Ok(()); + } + } + + let result = match command.exec { + CommandType::StringResponse(ref x) => { + let _ = message.channel_id.say(x); + + Ok(()) + }, + CommandType::Basic(ref x) => { + (x)(&mut context, &message, args, content) + }, + CommandType::WithCommands(ref x) => { + (x)(&mut context, &message, groups, &args) + }, + }; + + if let Some(after) = after { + (after)(&mut context, &message, &built, result); + } + + Ok(()) + }); + + return; + } + } + } + } + } + + fn update_current_user(&mut self, user_id: UserId, is_bot: bool) { + self.user_info = (user_id.0, is_bot); + } + + fn initialized(&self) -> bool { self.initialized } +} |