aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorIllia <[email protected]>2016-12-09 23:30:30 +0200
committerzeyla <[email protected]>2016-12-09 13:30:30 -0800
commit8f24aa391f6b8a9103a9c105138c7610288acb05 (patch)
tree5af9a958502a49d64868c50ef976157c6b71adca /src
parentImplement From<Embed> for CreateEmbed (diff)
downloadserenity-8f24aa391f6b8a9103a9c105138c7610288acb05.tar.xz
serenity-8f24aa391f6b8a9103a9c105138c7610288acb05.zip
Command builder, quoted args, and multi-prefixes
Add a command builder, which can take arguments such as multiple checks, quoted arguments, and multiple prefix support, as well as dynamic prefixes per context.
Diffstat (limited to 'src')
-rw-r--r--src/ext/framework/command.rs62
-rw-r--r--src/ext/framework/configuration.rs27
-rw-r--r--src/ext/framework/create_command.rs131
-rw-r--r--src/ext/framework/mod.rs90
-rw-r--r--src/utils/mod.rs46
5 files changed, 324 insertions, 32 deletions
diff --git a/src/ext/framework/command.rs b/src/ext/framework/command.rs
index 46338a1..225f50a 100644
--- a/src/ext/framework/command.rs
+++ b/src/ext/framework/command.rs
@@ -2,28 +2,66 @@ use std::sync::Arc;
use super::Configuration;
use ::client::Context;
use ::model::Message;
+use std::collections::HashMap;
+
+/// Command function type. Allows to access internal framework things inside
+/// your commands.
+pub enum CommandType {
+ StringResponse(String),
+ Basic(Box<Fn(&Context, &Message, Vec<String>) + Send + Sync + 'static>),
+ WithCommands(Box<Fn(&Context, &Message, HashMap<String, Arc<Command>>, Vec<String>) + Send + Sync + 'static>)
+}
+
+/// 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<Fn(&Context, &Message) -> bool + Send + Sync + 'static>>,
+ /// Function called when the command is called.
+ pub exec: CommandType,
+ /// Command description, used by other commands.
+ pub desc: 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,
+}
-#[doc(hidden)]
-pub type Command = Fn(&Context, &Message, Vec<String>) + Send + Sync;
#[doc(hidden)]
pub type InternalCommand = Arc<Command>;
-pub fn positions(content: &str, conf: &Configuration) -> Option<Vec<usize>> {
- if let Some(ref prefix) = conf.prefix {
+pub fn positions(ctx: &Context, content: &str, conf: &Configuration) -> Option<Vec<usize>> {
+ if conf.prefixes.len() > 0 || 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 = if let Some(mention_end) = find_mention_end(content, conf) {
- vec![mention_end]
- } else if content.starts_with(prefix) {
- vec![prefix.len()]
+ 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) {
+ positions.push(x.len());
+ } else {
+ for n in conf.prefixes.clone() {
+ if content.starts_with(&n) {
+ positions.push(n.len());
+ }
+ }
+ }
} else {
- return None;
+ for n in conf.prefixes.clone() {
+ if content.starts_with(&n) {
+ positions.push(n.len());
+ }
+ }
};
+ if positions.len() == 0 {
+ return None;
+ }
+
if conf.allow_whitespace {
- let pos = *unsafe {
- positions.get_unchecked(0)
- };
+ let pos = *unsafe { positions.get_unchecked(0) };
positions.insert(0, pos + 1);
}
diff --git a/src/ext/framework/configuration.rs b/src/ext/framework/configuration.rs
index 6a6c5e5..7af38ed 100644
--- a/src/ext/framework/configuration.rs
+++ b/src/ext/framework/configuration.rs
@@ -1,5 +1,6 @@
use std::default::Default;
use ::client::rest;
+use ::client::Context;
/// The configuration to use for a [`Framework`] associated with a [`Client`]
/// instance.
@@ -31,7 +32,9 @@ pub struct Configuration {
#[doc(hidden)]
pub allow_whitespace: bool,
#[doc(hidden)]
- pub prefix: Option<String>,
+ pub prefixes: Vec<String>,
+ #[doc(hidden)]
+ pub dynamic_prefix: Option<Box<Fn(&Context) -> Option<String> + Send + Sync + 'static>>
}
impl Configuration {
@@ -119,7 +122,24 @@ impl Configuration {
/// Sets the prefix to respond to. This can either be a single-char or
/// multi-char string.
pub fn prefix<S: Into<String>>(mut self, prefix: S) -> Self {
- self.prefix = Some(prefix.into());
+ self.prefixes = vec![prefix.into()];
+
+ self
+ }
+
+ /// Sets the prefix to respond to. This can either be a single-char or
+ /// multi-char string.
+ pub fn prefixes(mut self, prefixes: Vec<&str>) -> Self {
+ self.prefixes = prefixes.iter().map(|x| x.to_string()).collect();
+
+ self
+ }
+
+ /// Sets the prefix to respond to. This can either be a single-char or
+ /// multi-char string.
+ pub fn dynamic_prefix<F>(mut self, dynamic_prefix: F) -> Self
+ where F: Fn(&Context) -> Option<String> + Send + Sync + 'static {
+ self.dynamic_prefix = Some(Box::new(dynamic_prefix));
self
}
@@ -137,7 +157,8 @@ impl Default for Configuration {
depth: 5,
on_mention: None,
allow_whitespace: false,
- prefix: None,
+ prefixes: vec![],
+ dynamic_prefix: None
}
}
}
diff --git a/src/ext/framework/create_command.rs b/src/ext/framework/create_command.rs
new file mode 100644
index 0000000..824c3dd
--- /dev/null
+++ b/src/ext/framework/create_command.rs
@@ -0,0 +1,131 @@
+pub use ext::framework::command::{Command, CommandType};
+
+use std::collections::HashMap;
+use std::default::Default;
+use std::sync::Arc;
+use ::client::Context;
+use ::model::Message;
+
+pub struct CreateCommand(pub Command);
+
+impl CreateCommand {
+ /// 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_bot(&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: &Context, _message: &Message, _args: Vec<String>) {
+ /// context.say("Pong!");
+ /// }
+ ///
+ /// fn owner_check(_context: &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(&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
+ }
+
+ /// A function that can be called when a command is received.
+ ///
+ /// See [`exec_str`] if you _only_ need to return a string on command use.
+ ///
+ /// [`exec_str`]: #method.exec_str
+ pub fn exec<F>(mut self, func: F) -> Self
+ where F: Fn(&Context, &Message, Vec<String>) + Send + Sync + 'static {
+ 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 usages, used specifically for creating a help
+ /// command.
+ pub fn exec_help<F>(mut self, f: F) -> Self
+ where F: Fn(&Context, &Message, HashMap<String, Arc<Command>>, Vec<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
+ }
+
+ /// 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 {
+ checks: Vec::default(),
+ exec: CommandType::Basic(Box::new(|_, _, _| {})),
+ desc: None,
+ usage: None,
+ use_quotes: true,
+ }
+ }
+}
diff --git a/src/ext/framework/mod.rs b/src/ext/framework/mod.rs
index 46b80e2..7458223 100644
--- a/src/ext/framework/mod.rs
+++ b/src/ext/framework/mod.rs
@@ -39,8 +39,8 @@
//!
//! client.with_framework(|f| f
//! .configure(|c| c.prefix("~"))
-//! .on("about", about)
-//! .on("ping", ping));
+//! .command("about", |c| c.exec_str("A simple test bot"))
+//! .command("ping", |c| c.exec(ping)));
//!
//! fn about(context: &Context, _message: &Message, _args: Vec<String>) {
//! let _ = context.say("A simple test bot");
@@ -55,9 +55,11 @@
mod command;
mod configuration;
+mod create_command;
-pub use self::command::Command;
+pub use self::command::{Command, CommandType};
pub use self::configuration::Configuration;
+pub use self::create_command::CreateCommand;
use self::command::InternalCommand;
use std::collections::HashMap;
@@ -65,6 +67,7 @@ use std::sync::Arc;
use std::thread;
use ::client::Context;
use ::model::Message;
+use ::utils;
/// A macro to generate "named parameters". This is useful to avoid manually
/// using the "arguments" parameter and manually parsing types.
@@ -137,7 +140,6 @@ pub struct Framework {
commands: HashMap<String, InternalCommand>,
before: Option<Arc<Fn(&Context, &Message, &String) + Send + Sync + 'static>>,
after: Option<Arc<Fn(&Context, &Message, &String) + Send + Sync + 'static>>,
- checks: HashMap<String, Arc<Fn(&Context, &Message) -> bool + Send + Sync + 'static>>,
/// Whether the framework has been "initialized".
///
/// The framework is initialized once one of the following occurs:
@@ -192,7 +194,7 @@ impl Framework {
#[doc(hidden)]
pub fn dispatch(&mut self, context: Context, message: Message) {
- let res = command::positions(&message.content, &self.configuration);
+ let res = command::positions(&context, &message.content, &self.configuration);
let positions = match res {
Some(positions) => positions,
@@ -206,7 +208,7 @@ impl Framework {
return;
}
- for position in positions {
+ 'outer: for position in positions {
let mut built = String::new();
for i in 0..self.configuration.depth {
@@ -228,27 +230,42 @@ impl Framework {
});
if let Some(command) = self.commands.get(&built) {
- if let Some(check) = self.checks.get(&built) {
+ for check in &command.checks {
if !(check)(&context, &message) {
- continue;
+ continue 'outer;
}
}
let before = self.before.clone();
let command = command.clone();
let after = self.after.clone();
+ let commands = self.commands.clone();
thread::spawn(move || {
if let Some(before) = before {
(before)(&context, &message, &built);
}
- let args = message.content[position + built.len()..]
- .split_whitespace()
- .map(|arg| arg.to_owned())
- .collect::<Vec<String>>();
+ let args = if command.use_quotes {
+ utils::parse_quotes(&message.content[position + built.len()..])
+ } else {
+ message.content[position + built.len()..]
+ .split_whitespace()
+ .map(|arg| arg.to_owned())
+ .collect::<Vec<String>>()
+ };
- (command)(&context, &message, args);
+ match command.exec {
+ CommandType::StringResponse(ref x) => {
+ let _ = &context.say(x);
+ },
+ CommandType::Basic(ref x) => {
+ (x)(&context, &message, args);
+ },
+ CommandType::WithCommands(ref x) => {
+ (x)(&context, &message, commands, args);
+ }
+ }
if let Some(after) = after {
(after)(&context, &message, &built);
@@ -267,20 +284,54 @@ impl Framework {
/// 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(&Context, &Message, Vec<String>) + Send + Sync + 'static,
S: Into<String> {
- self.commands.insert(command_name.into(), Arc::new(f));
+ self.commands.insert(command_name.into(), Arc::new(Command {
+ checks: Vec::default(),
+ exec: CommandType::Basic(Box::new(f)),
+ desc: None,
+ usage: None,
+ use_quotes: false,
+ }));
+
+ 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 cmd = f(CreateCommand(Command::default())).0;
+ self.commands.insert(command_name.into(), Arc::new(cmd));
+
self.initialized = true;
self
}
- /// This will call given closure before every command's execution
+ /// Specify the function to be called prior to every command's execution.
pub fn before<F>(mut self, f: F) -> Self
where F: Fn(&Context, &Message, &String) + Send + Sync + 'static {
self.before = Some(Arc::new(f));
@@ -288,7 +339,7 @@ impl Framework {
self
}
- /// This will call given closure after every command's execution
+ /// Specify the function to be called after every command's execution.
pub fn after<F>(mut self, f: F) -> Self
where F: Fn(&Context, &Message, &String) + Send + Sync + 'static {
self.after = Some(Arc::new(f));
@@ -325,10 +376,15 @@ impl Framework {
/// message.author.id == 7
/// }
/// ```
+ #[deprecated(since="0.1.2", note="Use the `CreateCommand` builder's `check` instead.")]
pub fn set_check<F, S>(mut self, command: S, check: F) -> Self
where F: Fn(&Context, &Message) -> bool + Send + Sync + 'static,
S: Into<String> {
- self.checks.insert(command.into(), Arc::new(check));
+ if let Some(command) = self.commands.get_mut(&command.into()) {
+ if let Some(c) = Arc::get_mut(command) {
+ c.checks.push(Box::new(check));
+ }
+ }
self
}
diff --git a/src/utils/mod.rs b/src/utils/mod.rs
index a7a14a3..fbc642f 100644
--- a/src/utils/mod.rs
+++ b/src/utils/mod.rs
@@ -198,3 +198,49 @@ pub fn read_image<P: AsRef<Path>>(path: P) -> Result<String> {
Ok(format!("data:image/{};base64,{}", ext, b64))
}
+
+/// Turns a string into a vector of string arguments, splitting by spaces, but
+/// parsing content within quotes as one individual argument.
+pub fn parse_quotes(s: &str) -> Vec<String> {
+ let mut args = vec![];
+ let mut in_string = false;
+ let mut current_str = String::default();
+
+ for x in s.chars() {
+ if in_string {
+ if x == '"' {
+ if !current_str.is_empty() {
+ args.push(current_str);
+ }
+
+ current_str = String::default();
+ in_string = false;
+ } else {
+ current_str.push(x);
+ }
+ } else {
+ if x == ' ' {
+ if !current_str.is_empty() {
+ args.push(current_str.clone());
+ }
+
+ current_str = String::default();
+ } else if x == '"' {
+ if !current_str.is_empty() {
+ args.push(current_str.clone());
+ }
+
+ in_string = true;
+ current_str = String::default();
+ } else {
+ current_str.push(x);
+ }
+ }
+ }
+
+ if !current_str.is_empty() {
+ args.push(current_str);
+ }
+
+ args
+}