pub mod help_commands;
mod command;
mod configuration;
mod create_command;
mod create_help_command;
mod create_group;
mod buckets;
mod args;
pub use self::args::{
Args,
Iter,
Error as ArgError
};
pub(crate) use self::buckets::{Bucket, Ratelimit};
pub(crate) use self::command::Help;
pub use self::command::{
Check,
HelpFunction,
HelpOptions,
Command,
CommandGroup,
CommandOptions,
Error as CommandError
};
pub use self::command::CommandOrAlias;
pub use self::configuration::Configuration;
pub use self::create_help_command::CreateHelpCommand;
pub use self::create_command::{CreateCommand, FnOrCommand};
pub use self::create_group::CreateGroup;
use client::Context;
use internal::RwLockExt;
use model::{
channel::Message,
guild::{Guild, Member},
id::{ChannelId, GuildId, UserId},
Permissions
};
use self::command::{AfterHook, BeforeHook, UnrecognisedCommandHook};
use std::{
collections::HashMap,
default::Default,
sync::Arc
};
use super::Framework;
use threadpool::ThreadPool;
#[cfg(feature = "cache")]
use client::CACHE;
#[cfg(feature = "cache")]
use model::channel::Channel;
#[macro_export]
macro_rules! command {
($fname:ident($c:ident) $b:block) => {
#[allow(non_camel_case_types)]
pub struct $fname;
impl $crate::framework::standard::Command for $fname {
#[allow(unreachable_code, unused_mut)]
fn execute(&self, mut $c: &mut $crate::client::Context,
_: &$crate::model::channel::Message,
_: $crate::framework::standard::Args)
-> ::std::result::Result<(), $crate::framework::standard::CommandError> {
$b
Ok(())
}
}
};
($fname:ident($c:ident, $m:ident) $b:block) => {
#[allow(non_camel_case_types)]
pub struct $fname;
impl $crate::framework::standard::Command for $fname {
#[allow(unreachable_code, unused_mut)]
fn execute(&self, mut $c: &mut $crate::client::Context,
$m: &$crate::model::channel::Message,
_: $crate::framework::standard::Args)
-> ::std::result::Result<(), $crate::framework::standard::CommandError> {
$b
Ok(())
}
}
};
($fname:ident($c:ident, $m:ident, $a:ident) $b:block) => {
#[allow(non_camel_case_types)]
pub struct $fname;
impl $crate::framework::standard::Command for $fname {
#[allow(unreachable_code, unused_mut)]
fn execute(&self, mut $c: &mut $crate::client::Context,
$m: &$crate::model::channel::Message,
mut $a: $crate::framework::standard::Args)
-> ::std::result::Result<(), $crate::framework::standard::CommandError> {
$b
Ok(())
}
}
};
}
macro_rules! command_and_help_args {
($message_content:expr, $position:expr, $command_length:expr, $delimiters:expr) => {
{
let content = $message_content.chars().skip($position).skip_while(|x| x.is_whitespace())
.skip($command_length).collect::<String>();
Args::new(&content.trim(), $delimiters)
}
};
}
#[derive(Debug)]
pub enum DispatchError {
CheckFailed,
CommandDisabled(String),
BlockedUser,
BlockedGuild,
BlockedChannel,
LackOfPermissions(Permissions),
RateLimited(i64),
OnlyForDM,
OnlyForGuilds,
OnlyForOwners,
LackingRole,
NotEnoughArguments { min: i32, given: usize },
TooManyArguments { max: i32, given: usize },
IgnoredBot,
WebhookAuthor,
}
type DispatchErrorHook = Fn(Context, Message, DispatchError) + Send + Sync + 'static;
#[derive(Default)]
pub struct StandardFramework {
configuration: Configuration,
groups: HashMap<String, Arc<CommandGroup>>,
help: Option<Arc<Help>>,
before: Option<Arc<BeforeHook>>,
dispatch_error_handler: Option<Arc<DispatchErrorHook>>,
buckets: HashMap<String, Bucket>,
after: Option<Arc<AfterHook>>,
unrecognised_command: Option<Arc<UnrecognisedCommandHook>>,
pub initialized: bool,
user_id: u64,
}
impl StandardFramework {
pub fn new() -> Self { StandardFramework::default() }
pub fn configure<F>(mut self, f: F) -> Self
where F: FnOnce(Configuration) -> Configuration {
self.configuration = f(self.configuration);
self
}
pub fn bucket(mut self, s: &str, delay: i64, time_span: i64, limit: i32) -> Self {
self.buckets.insert(
s.to_string(),
Bucket {
ratelimit: Ratelimit {
delay,
limit: Some((time_span, limit)),
},
users: HashMap::new(),
check: None,
},
);
self
}
#[cfg(feature = "cache")]
pub fn complex_bucket<Check>(mut self,
s: &str,
delay: i64,
time_span: i64,
limit: i32,
check: Check)
-> Self
where Check: Fn(&mut Context, Option<GuildId>, ChannelId, UserId) -> bool
+ Send
+ Sync
+ 'static {
self.buckets.insert(
s.to_string(),
Bucket {
ratelimit: Ratelimit {
delay,
limit: Some((time_span, limit)),
},
users: HashMap::new(),
check: Some(Box::new(check)),
},
);
self
}
#[cfg(not(feature = "cache"))]
pub fn complex_bucket<Check>(mut self,
s: &str,
delay: i64,
time_span: i64,
limit: i32,
check: Check)
-> Self
where Check: Fn(&mut Context, ChannelId, UserId) -> bool + Send + Sync + 'static {
self.buckets.insert(
s.to_string(),
Bucket {
ratelimit: Ratelimit {
delay,
limit: Some((time_span, limit)),
},
users: HashMap::new(),
check: Some(Box::new(check)),
},
);
self
}
pub fn simple_bucket(mut self, s: &str, delay: i64) -> Self {
self.buckets.insert(
s.to_string(),
Bucket {
ratelimit: Ratelimit {
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().channel(message.channel_id) {
let guild_id = channel.with(|g| g.guild_id);
if self.configuration.blocked_guilds.contains(&guild_id) {
return true;
}
if let Some(guild) = guild_id.to_guild_cached() {
return self.configuration
.blocked_users
.contains(&guild.with(|g| g.owner_id));
}
}
false
}
#[cfg(feature = "cache")]
fn is_blocked_channel(&self, message: &Message) -> bool {
!self.configuration.allowed_channels.is_empty()
&& !self.configuration
.allowed_channels
.contains(&message.channel_id)
}
#[allow(too_many_arguments)]
#[cfg_attr(feature = "cargo-clippy", allow(cyclomatic_complexity))]
fn should_fail(&mut self,
mut context: &mut Context,
message: &Message,
command: &Arc<CommandOptions>,
group: &Arc<CommandGroup>,
args: &mut Args,
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 {
let len = args.len();
if let Some(x) = command.min_args {
if len < x as usize {
return Some(DispatchError::NotEnoughArguments {
min: x,
given: len,
});
}
}
if let Some(x) = command.max_args {
if len > x as usize {
return Some(DispatchError::TooManyArguments {
max: x,
given: len,
});
}
}
if self.configuration.owners.contains(&message.author.id) {
return None;
}
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);
let apply = bucket.check.as_ref().map_or(true, |check| {
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));
}
}
}
#[cfg(feature = "cache")]
{
if self.is_blocked_guild(message) {
return Some(DispatchError::BlockedGuild);
}
if self.is_blocked_channel(message) {
return Some(DispatchError::BlockedChannel);
}
if !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_string()))
} else if self.configuration.disabled_commands.contains(built) {
Some(DispatchError::CommandDisabled(built.to_string()))
} else {
#[cfg(feature = "cache")] {
if !command.allowed_roles.is_empty() {
if let Some(guild) = message.guild() {
let guild = guild.read();
if let Some(member) = guild.members.get(&message.author.id) {
if let Ok(permissions) = member.permissions() {
if !permissions.administrator()
&& !has_correct_roles(command, &guild, member) {
return Some(DispatchError::LackingRole);
}
}
}
}
}
}
let all_group_checks_passed = group
.checks
.iter()
.all(|check| (check.0)(&mut context, message, args, command));
if !all_group_checks_passed {
return Some(DispatchError::CheckFailed);
}
let all_command_checks_passed = command
.checks
.iter()
.all(|check| (check.0)(&mut context, message, args, command));
if all_command_checks_passed {
None
} else {
Some(DispatchError::CheckFailed)
}
}
}
}
pub fn on(self, name: &str,
f: fn(&mut Context, &Message, Args)
-> Result<(), CommandError>) -> Self {
self.cmd(name, f)
}
pub fn cmd<C: Command + 'static>(mut self, name: &str, c: C) -> Self {
{
let ungrouped = self.groups
.entry("Ungrouped".to_string())
.or_insert_with(|| Arc::new(CommandGroup::default()));
if let Some(ref mut group) = Arc::get_mut(ungrouped) {
let cmd: Arc<Command> = Arc::new(c);
group
.commands
.insert(name.to_string(), CommandOrAlias::Command(Arc::clone(&cmd)));
cmd.init();
}
}
self.initialized = true;
self
}
pub fn command<F>(mut self, command_name: &str, f: F) -> Self
where F: FnOnce(CreateCommand) -> CreateCommand {
{
let ungrouped = self.groups
.entry("Ungrouped".to_string())
.or_insert_with(|| Arc::new(CommandGroup::default()));
if let Some(ref mut group) = Arc::get_mut(ungrouped) {
let cmd = f(CreateCommand::default()).finish();
let name = command_name.to_string();
if let Some(ref prefixes) = group.prefixes {
for v in &cmd.options().aliases {
for prefix in prefixes {
group.commands.insert(
format!("{} {}", prefix, v),
CommandOrAlias::Alias(format!("{} {}", prefix, name)),
);
}
}
} else {
for v in &cmd.options().aliases {
group
.commands
.insert(v.to_string(), CommandOrAlias::Alias(name.clone()));
}
}
group
.commands
.insert(name, CommandOrAlias::Command(Arc::clone(&cmd)));
cmd.init();
}
}
self.initialized = true;
self
}
pub fn group<F>(mut self, group_name: &str, f: F) -> Self
where F: FnOnce(CreateGroup) -> CreateGroup {
let group = f(CreateGroup(CommandGroup::default())).0;
self.groups.insert(group_name.into(), Arc::new(group));
self.initialized = true;
self
}
pub fn on_dispatch_error<F>(mut self, f: F) -> Self
where F: Fn(Context, Message, DispatchError) + Send + Sync + 'static {
self.dispatch_error_handler = Some(Arc::new(f));
self
}
pub fn before<F>(mut self, f: F) -> Self
where F: Fn(&mut Context, &Message, &str) -> bool + Send + Sync + 'static {
self.before = Some(Arc::new(f));
self
}
pub fn after<F>(mut self, f: F) -> Self
where F: Fn(&mut Context, &Message, &str, Result<(), CommandError>) + Send + Sync + 'static {
self.after = Some(Arc::new(f));
self
}
pub fn unrecognised_command<F>(mut self, f: F) -> Self
where F: Fn(&mut Context, &Message, &str) + Send + Sync + 'static {
self.unrecognised_command = Some(Arc::new(f));
self
}
pub fn help(mut self, f: HelpFunction) -> Self {
let a = CreateHelpCommand(HelpOptions::default(), f).finish();
self.help = Some(a);
self
}
pub fn customised_help<F>(mut self, f: HelpFunction, c: F) -> Self
where F: FnOnce(CreateHelpCommand) -> CreateHelpCommand {
let res = c(CreateHelpCommand(HelpOptions::default(), f));
self.help = Some(res.finish());
self
}
}
fn skip_chars_and_trim_to_new_string(str_to_transform_to_chars: &str, chars_to_skip: usize) -> String {
let mut chars = str_to_transform_to_chars.chars();
if chars_to_skip > 0 {
chars.nth(chars_to_skip - 1);
}
chars.as_str().trim().to_string()
}
impl Framework for StandardFramework {
fn dispatch(
&mut self,
mut context: Context,
message: Message,
threadpool: &ThreadPool,
) {
let res = command::positions(&mut context, &message, &self.configuration);
let mut unrecognised_command_name = String::from("");
let positions = match res {
Some(mut positions) => {
positions.retain(|p| *p < message.content.len());
if positions.is_empty() {
return;
}
positions
},
None => return,
};
'outer: for position in positions {
let mut built = String::new();
let orginal_round = skip_chars_and_trim_to_new_string(&message.content, position);
let mut round = orginal_round.split_whitespace();
for i in 0..self.configuration.depth {
if i != 0 {
built.push(' ');
}
built.push_str(match round.next() {
Some(piece) => piece,
None => continue 'outer,
});
let groups = self.groups.clone();
for group in groups.values() {
let command_length = built.len();
built = if self.configuration.case_insensitive {
built.to_lowercase()
} else {
built
};
unrecognised_command_name = built.clone();
let cmd = group.commands.get(&built);
if let Some(&CommandOrAlias::Alias(ref points_to)) = cmd {
built = points_to.to_string();
}
let mut check_contains_group_prefix = false;
let mut longest_matching_prefix_len = 0;
let to_check = if let Some(ref prefixes) = group.prefixes {
longest_matching_prefix_len = prefixes.iter().fold(0, |longest_prefix_len, prefix|
if prefix.len() > longest_prefix_len
&& built.starts_with(prefix)
&& (orginal_round.len() == prefix.len() || built.get(prefix.len()..prefix.len() + 1) == Some(" ")) {
prefix.len()
} else {
longest_prefix_len
}
);
if longest_matching_prefix_len == built.len() {
check_contains_group_prefix = true;
String::new()
} else if longest_matching_prefix_len > 0 {
check_contains_group_prefix = true;
built[longest_matching_prefix_len + 1..].to_string()
} else {
continue;
}
} else {
built.clone()
};
let before = self.before.clone();
let after = self.after.clone();
if to_check == "help" {
let help = self.help.clone();
if let Some(help) = help {
let groups = self.groups.clone();
let mut args = command_and_help_args!(&message.content, position, command_length, &self.configuration.delimiters);
threadpool.execute(move || {
if let Some(before) = before {
if !(before)(&mut context, &message, &built) {
return;
}
}
let result = (help.0)(&mut context, &message, &help.1, groups, &args);
if let Some(after) = after {
(after)(&mut context, &message, &built, result);
}
});
return;
}
}
if !to_check.is_empty() {
if let Some(&CommandOrAlias::Command(ref command)) =
group.commands.get(&to_check) {
let command = Arc::clone(command);
let mut args = command_and_help_args!(&message.content, position, command_length, &self.configuration.delimiters);
if let Some(error) = self.should_fail(
&mut context,
&message,
&command.options(),
&group,
&mut args,
&to_check,
&built,
) {
if let Some(ref handler) = self.dispatch_error_handler {
handler(context, message, error);
}
return;
}
threadpool.execute(move || {
if let Some(before) = before {
if !(before)(&mut context, &message, &built) {
return;
}
}
if !command.before(&mut context, &message) {
return;
}
let result = command.execute(&mut context, &message, args);
command.after(&mut context, &message, &result);
if let Some(after) = after {
(after)(&mut context, &message, &built, result);
}
});
return;
}
}
if check_contains_group_prefix {
if let &Some(CommandOrAlias::Command(ref command)) = &group.default_command {
let command = Arc::clone(command);
let mut args = {
Args::new(&orginal_round[longest_matching_prefix_len..], &self.configuration.delimiters)
};
threadpool.execute(move || {
if let Some(before) = before {
if !(before)(&mut context, &message, &args.full()) {
return;
}
}
if !command.before(&mut context, &message) {
return;
}
let result = command.execute(&mut context, &message, args);
command.after(&mut context, &message, &result);
if let Some(after) = after {
(after)(&mut context, &message, &built, result);
}
});
return;
}
}
}
}
}
if let &Some(ref unrecognised_command) = &self.unrecognised_command {
let unrecognised_command = unrecognised_command.clone();
threadpool.execute(move || {
(unrecognised_command)(&mut context, &message, &unrecognised_command_name);
});
}
}
fn update_current_user(&mut self, user_id: UserId) {
self.user_id = user_id.0;
}
}
#[cfg(feature = "cache")]
pub fn has_correct_permissions(command: &Arc<CommandOptions>, message: &Message) -> bool {
if !command.required_permissions.is_empty() {
if let Some(guild) = message.guild() {
let perms = guild
.with(|g| g.permissions_in(message.channel_id, message.author.id));
return perms.contains(command.required_permissions);
}
}
true
}
pub fn has_correct_roles(cmd: &Arc<CommandOptions>, guild: &Guild, member: &Member) -> bool {
if cmd.allowed_roles.is_empty() {
true
} else {
cmd.allowed_roles
.iter()
.flat_map(|r| guild.role_by_name(r))
.any(|g| member.roles.contains(&g.id))
}
}
#[derive(PartialEq, Debug)]
pub enum HelpBehaviour {
Strike,
Hide,
Nothing
}
use std::fmt;
impl fmt::Display for HelpBehaviour {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
fmt::Debug::fmt(self, f)
}
}