Skip to content

Commit

Permalink
Command builder, quoted args, and multi-prefixes
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
fwrs authored and zeyla committed Dec 9, 2016
1 parent 7914274 commit 8f24aa3
Show file tree
Hide file tree
Showing 7 changed files with 346 additions and 68 deletions.
52 changes: 16 additions & 36 deletions examples/06_command_framework/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ fn main() {
data.insert::<CommandCounter>(HashMap::default());
}

client.on_ready(|_context, ready| {
client.on_ready(|_, ready| {
println!("{} is connected!", ready.user.name);
});

Expand Down Expand Up @@ -81,22 +81,20 @@ fn main() {
})
// Very similar to `before`, except this will be called directly _after_
// command execution.
.after(|_context, _message, command_name| {
.after(|_, _, command_name| {
println!("Processed command '{}'", command_name)
})
.on("commands", commands)
.set_check("commands", owner_check)
.on("ping", ping_command)
.set_check("ping", owner_check) // Ensure only the owner can run this
.on("emoji cat", cat_command)
.on("emoji dog", dog_command)
.on("multiply", multiply)
.on("some long command", some_long_command)
// Commands can be in closure-form as well.
//
// This is not recommended though, as any closure larger than a couple
// lines will look ugly.
.on("about", |context, _message, _args| drop(context.say("A test bot"))));
.command("about", |c| c.exec_str("A test bot"))
.command("commands", |c| c
.check(owner_check)
.exec(commands))
.command("emoji cat", |c| c.exec_str(":cat:"))
.command("emoji dog", |c| c.exec_str(":dog:"))
.command("multiply", |c| c.exec(multiply))
.command("ping", |c| c
.check(owner_check)
.exec_str("Pong!"))
.command("some long command", |c| c.exec(some_long_command)));

if let Err(why) = client.start() {
println!("Client error: {:?}", why);
Expand All @@ -109,12 +107,6 @@ fn main() {
// This may bring more features available for commands in the future. See the
// "multiply" command below for some of the power that the `command!` macro can
// bring.
command!(cat_command(context, _msg, _args) {
if let Err(why) = context.say(":cat:") {
println!("Eror sending message: {:?}", why);
}
});

command!(commands(context, _msg, _args) {
let mut contents = "Commands used:\n".to_owned();

Expand All @@ -130,29 +122,17 @@ command!(commands(context, _msg, _args) {
}
});

fn dog_command(context: &Context, _msg: &Message, _args: Vec<String>) {
if let Err(why) = context.say(":dog:") {
println!("Error sending message: {:?}", why);
}
}

fn ping_command(_context: &Context, message: &Message, _args: Vec<String>) {
if let Err(why) = message.reply("Pong!") {
println!("Error sending reply: {:?}", why);
}
}

// A function which acts as a "check", to determine whether to call a command.
//
// In this case, this command checks to ensure you are the owner of the message
// in order for the command to be executed. If the check fails, the command is
// not called.
fn owner_check(_context: &Context, message: &Message) -> bool {
fn owner_check(_: &Context, message: &Message) -> bool {
// Replace 7 with your ID
message.author.id == 7
}

fn some_long_command(context: &Context, _msg: &Message, args: Vec<String>) {
fn some_long_command(context: &Context, _: &Message, args: Vec<String>) {
if let Err(why) = context.say(&format!("Arguments: {:?}", args)) {
println!("Error sending message: {:?}", why);
}
Expand All @@ -178,7 +158,7 @@ fn some_long_command(context: &Context, _msg: &Message, args: Vec<String>) {
// will be ignored.
//
// Argument type overloading is currently not supported.
command!(multiply(context, _message, args, first: f64, second: f64) {
command!(multiply(context, _msg, args, first: f64, second: f64) {
let res = first * second;

if let Err(why) = context.say(&res.to_string()) {
Expand Down
62 changes: 50 additions & 12 deletions src/ext/framework/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
27 changes: 24 additions & 3 deletions src/ext/framework/configuration.rs
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}
Expand All @@ -137,7 +157,8 @@ impl Default for Configuration {
depth: 5,
on_mention: None,
allow_whitespace: false,
prefix: None,
prefixes: vec![],
dynamic_prefix: None
}
}
}
131 changes: 131 additions & 0 deletions src/ext/framework/create_command.rs
Original file line number Diff line number Diff line change
@@ -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,
}
}
}
Loading

0 comments on commit 8f24aa3

Please sign in to comment.