subcommand group syntax
This commit is contained in:
parent
d84d7ab62b
commit
379e488f7a
@ -2,6 +2,7 @@ pub mod suffixes {
|
||||
pub const COMMAND: &str = "COMMAND";
|
||||
pub const ARG: &str = "ARG";
|
||||
pub const SUBCOMMAND: &str = "SUBCOMMAND";
|
||||
pub const SUBCOMMAND_GROUP: &str = "GROUP";
|
||||
pub const CHECK: &str = "CHECK";
|
||||
pub const HOOK: &str = "HOOK";
|
||||
}
|
||||
|
@ -36,6 +36,13 @@ macro_rules! match_options {
|
||||
|
||||
#[proc_macro_attribute]
|
||||
pub fn command(attr: TokenStream, input: TokenStream) -> TokenStream {
|
||||
enum LastItem {
|
||||
Fun,
|
||||
SubFun,
|
||||
SubGroup,
|
||||
SubGroupFun,
|
||||
}
|
||||
|
||||
let mut fun = parse_macro_input!(input as CommandFun);
|
||||
|
||||
let _name = if !attr.is_empty() {
|
||||
@ -46,6 +53,7 @@ pub fn command(attr: TokenStream, input: TokenStream) -> TokenStream {
|
||||
|
||||
let mut hooks: Vec<Ident> = Vec::new();
|
||||
let mut options = Options::new();
|
||||
let mut last_desc = LastItem::Fun;
|
||||
|
||||
for attribute in &fun.attributes {
|
||||
let span = attribute.span();
|
||||
@ -56,15 +64,39 @@ pub fn command(attr: TokenStream, input: TokenStream) -> TokenStream {
|
||||
|
||||
match name {
|
||||
"subcommand" => {
|
||||
options
|
||||
.subcommands
|
||||
.push(Subcommand::new(propagate_err!(attributes::parse(values))));
|
||||
let new_subcommand = Subcommand::new(propagate_err!(attributes::parse(values)));
|
||||
|
||||
if let Some(subcommand_group) = options.subcommand_groups.last_mut() {
|
||||
last_desc = LastItem::SubGroupFun;
|
||||
subcommand_group.subcommands.push(new_subcommand);
|
||||
} else {
|
||||
last_desc = LastItem::SubFun;
|
||||
options.subcommands.push(new_subcommand);
|
||||
}
|
||||
}
|
||||
"subcommandgroup" => {
|
||||
let new_group = SubcommandGroup::new(propagate_err!(attributes::parse(values)));
|
||||
last_desc = LastItem::SubGroup;
|
||||
|
||||
options.subcommand_groups.push(new_group);
|
||||
}
|
||||
"arg" => {
|
||||
if let Some(subcommand) = options.subcommands.last_mut() {
|
||||
subcommand.cmd_args.push(propagate_err!(attributes::parse(values)));
|
||||
if let Some(subcommand_group) = options.subcommand_groups.last_mut() {
|
||||
if let Some(subcommand) = subcommand_group.subcommands.last_mut() {
|
||||
subcommand.cmd_args.push(propagate_err!(attributes::parse(values)));
|
||||
} else {
|
||||
if let Some(subcommand) = options.subcommands.last_mut() {
|
||||
subcommand.cmd_args.push(propagate_err!(attributes::parse(values)));
|
||||
} else {
|
||||
options.cmd_args.push(propagate_err!(attributes::parse(values)));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
options.cmd_args.push(propagate_err!(attributes::parse(values)));
|
||||
if let Some(subcommand) = options.subcommands.last_mut() {
|
||||
subcommand.cmd_args.push(propagate_err!(attributes::parse(values)));
|
||||
} else {
|
||||
options.cmd_args.push(propagate_err!(attributes::parse(values)));
|
||||
}
|
||||
}
|
||||
}
|
||||
"example" => {
|
||||
@ -72,10 +104,36 @@ pub fn command(attr: TokenStream, input: TokenStream) -> TokenStream {
|
||||
}
|
||||
"description" => {
|
||||
let line: String = propagate_err!(attributes::parse(values));
|
||||
if let Some(subcommand) = options.subcommands.last_mut() {
|
||||
util::append_line(&mut subcommand.description, line);
|
||||
} else {
|
||||
util::append_line(&mut options.description, line);
|
||||
|
||||
match last_desc {
|
||||
LastItem::Fun => {
|
||||
util::append_line(&mut options.description, line);
|
||||
}
|
||||
LastItem::SubFun => {
|
||||
util::append_line(
|
||||
&mut options.subcommands.last_mut().unwrap().description,
|
||||
line,
|
||||
);
|
||||
}
|
||||
LastItem::SubGroup => {
|
||||
util::append_line(
|
||||
&mut options.subcommand_groups.last_mut().unwrap().description,
|
||||
line,
|
||||
);
|
||||
}
|
||||
LastItem::SubGroupFun => {
|
||||
util::append_line(
|
||||
&mut options
|
||||
.subcommand_groups
|
||||
.last_mut()
|
||||
.unwrap()
|
||||
.subcommands
|
||||
.last_mut()
|
||||
.unwrap()
|
||||
.description,
|
||||
line,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
"hook" => {
|
||||
@ -101,120 +159,81 @@ pub fn command(attr: TokenStream, input: TokenStream) -> TokenStream {
|
||||
supports_dm,
|
||||
mut cmd_args,
|
||||
mut subcommands,
|
||||
mut subcommand_groups,
|
||||
} = options;
|
||||
|
||||
let visibility = fun.visibility;
|
||||
let name = fun.name.clone();
|
||||
let body = fun.body;
|
||||
|
||||
let n = name.with_suffix(COMMAND);
|
||||
|
||||
let cooked = fun.cooked.clone();
|
||||
let root_ident = name.with_suffix(COMMAND);
|
||||
|
||||
let command_path = quote!(crate::framework::Command);
|
||||
let arg_path = quote!(crate::framework::Arg);
|
||||
let subcommand_path = ApplicationCommandOptionType::SubCommand;
|
||||
|
||||
populate_fut_lifetimes_on_refs(&mut fun.args);
|
||||
let args = fun.args;
|
||||
|
||||
let mut subcommand_group_idents = subcommand_groups
|
||||
.iter()
|
||||
.map(|subcommand| {
|
||||
root_ident
|
||||
.with_suffix(subcommand.name.replace("-", "_").as_str())
|
||||
.with_suffix(SUBCOMMAND_GROUP)
|
||||
})
|
||||
.collect::<Vec<Ident>>();
|
||||
|
||||
let mut subcommand_idents = subcommands
|
||||
.iter()
|
||||
.map(|subcommand| {
|
||||
n.with_suffix(subcommand.name.replace("-", "_").as_str()).with_suffix(SUBCOMMAND)
|
||||
root_ident
|
||||
.with_suffix(subcommand.name.replace("-", "_").as_str())
|
||||
.with_suffix(SUBCOMMAND)
|
||||
})
|
||||
.collect::<Vec<Ident>>();
|
||||
|
||||
let mut tokens = subcommands
|
||||
.iter_mut()
|
||||
.zip(subcommand_idents.iter())
|
||||
.map(|(subcommand, sc_ident)| {
|
||||
let arg_idents = subcommand
|
||||
.cmd_args
|
||||
.iter()
|
||||
.map(|arg| {
|
||||
n.with_suffix(subcommand.name.as_str())
|
||||
.with_suffix(arg.name.as_str())
|
||||
.with_suffix(ARG)
|
||||
})
|
||||
.collect::<Vec<Ident>>();
|
||||
|
||||
let mut tokens = subcommand
|
||||
.cmd_args
|
||||
.iter_mut()
|
||||
.zip(arg_idents.iter())
|
||||
.map(|(arg, ident)| {
|
||||
let Arg { name, description, kind, required } = arg;
|
||||
|
||||
quote! {
|
||||
#(#cooked)*
|
||||
#[allow(missing_docs)]
|
||||
pub static #ident: #arg_path = #arg_path {
|
||||
name: #name,
|
||||
description: #description,
|
||||
kind: #kind,
|
||||
required: #required,
|
||||
options: &[]
|
||||
};
|
||||
}
|
||||
})
|
||||
.fold(quote! {}, |mut a, b| {
|
||||
a.extend(b);
|
||||
a
|
||||
});
|
||||
|
||||
let Subcommand { name, description, .. } = subcommand;
|
||||
|
||||
tokens.extend(quote! {
|
||||
#(#cooked)*
|
||||
#[allow(missing_docs)]
|
||||
pub static #sc_ident: #arg_path = #arg_path {
|
||||
name: #name,
|
||||
description: #description,
|
||||
kind: #subcommand_path,
|
||||
required: false,
|
||||
options: &[#(&#arg_idents),*],
|
||||
};
|
||||
});
|
||||
|
||||
tokens
|
||||
})
|
||||
.fold(quote! {}, |mut a, b| {
|
||||
a.extend(b);
|
||||
a
|
||||
});
|
||||
|
||||
let mut arg_idents = cmd_args
|
||||
.iter()
|
||||
.map(|arg| n.with_suffix(arg.name.replace("-", "_").as_str()).with_suffix(ARG))
|
||||
.map(|arg| root_ident.with_suffix(arg.name.replace("-", "_").as_str()).with_suffix(ARG))
|
||||
.collect::<Vec<Ident>>();
|
||||
|
||||
let arg_tokens = cmd_args
|
||||
.iter_mut()
|
||||
.zip(arg_idents.iter())
|
||||
.map(|(arg, ident)| {
|
||||
let Arg { name, description, kind, required } = arg;
|
||||
let mut tokens = quote! {};
|
||||
|
||||
quote! {
|
||||
#(#cooked)*
|
||||
#[allow(missing_docs)]
|
||||
pub static #ident: #arg_path = #arg_path {
|
||||
name: #name,
|
||||
description: #description,
|
||||
kind: #kind,
|
||||
required: #required,
|
||||
options: &[],
|
||||
};
|
||||
}
|
||||
})
|
||||
.fold(quote! {}, |mut a, b| {
|
||||
a.extend(b);
|
||||
a
|
||||
});
|
||||
tokens.extend(
|
||||
subcommand_groups
|
||||
.iter_mut()
|
||||
.zip(subcommand_group_idents.iter())
|
||||
.map(|(group, group_ident)| group.as_tokens(group_ident))
|
||||
.fold(quote! {}, |mut a, b| {
|
||||
a.extend(b);
|
||||
a
|
||||
}),
|
||||
);
|
||||
|
||||
tokens.extend(arg_tokens);
|
||||
tokens.extend(
|
||||
subcommands
|
||||
.iter_mut()
|
||||
.zip(subcommand_idents.iter())
|
||||
.map(|(subcommand, sc_ident)| subcommand.as_tokens(sc_ident))
|
||||
.fold(quote! {}, |mut a, b| {
|
||||
a.extend(b);
|
||||
a
|
||||
}),
|
||||
);
|
||||
|
||||
tokens.extend(
|
||||
cmd_args.iter_mut().zip(arg_idents.iter()).map(|(arg, ident)| arg.as_tokens(ident)).fold(
|
||||
quote! {},
|
||||
|mut a, b| {
|
||||
a.extend(b);
|
||||
a
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
arg_idents.append(&mut subcommand_group_idents);
|
||||
arg_idents.append(&mut subcommand_idents);
|
||||
|
||||
let args = fun.args;
|
||||
|
||||
let variant = if args.len() == 2 {
|
||||
quote!(crate::framework::CommandFnType::Multi)
|
||||
} else {
|
||||
@ -230,9 +249,8 @@ pub fn command(attr: TokenStream, input: TokenStream) -> TokenStream {
|
||||
};
|
||||
|
||||
tokens.extend(quote! {
|
||||
#(#cooked)*
|
||||
#[allow(missing_docs)]
|
||||
pub static #n: #command_path = #command_path {
|
||||
pub static #root_ident: #command_path = #command_path {
|
||||
fun: #variant(#name),
|
||||
names: &[#_name, #(#aliases),*],
|
||||
desc: #description,
|
||||
@ -243,10 +261,7 @@ pub fn command(attr: TokenStream, input: TokenStream) -> TokenStream {
|
||||
args: &[#(&#arg_idents),*],
|
||||
hooks: &[#(&#hooks),*],
|
||||
};
|
||||
});
|
||||
|
||||
tokens.extend(quote! {
|
||||
#(#cooked)*
|
||||
#[allow(missing_docs)]
|
||||
#visibility fn #name<'fut> (#(#args),*) -> ::serenity::futures::future::BoxFuture<'fut, ()> {
|
||||
use ::serenity::futures::future::FutureExt;
|
||||
@ -269,7 +284,6 @@ pub fn check(_attr: TokenStream, input: TokenStream) -> TokenStream {
|
||||
let fn_name = n.with_suffix(CHECK);
|
||||
let visibility = fun.visibility;
|
||||
|
||||
let cooked = fun.cooked;
|
||||
let body = fun.body;
|
||||
let ret = fun.ret;
|
||||
populate_fut_lifetimes_on_refs(&mut fun.args);
|
||||
@ -279,7 +293,6 @@ pub fn check(_attr: TokenStream, input: TokenStream) -> TokenStream {
|
||||
let uuid = Uuid::new_v4().as_u128();
|
||||
|
||||
(quote! {
|
||||
#(#cooked)*
|
||||
#[allow(missing_docs)]
|
||||
#visibility fn #fn_name<'fut>(#(#args),*) -> ::serenity::futures::future::BoxFuture<'fut, #ret> {
|
||||
use ::serenity::futures::future::FutureExt;
|
||||
@ -291,7 +304,6 @@ pub fn check(_attr: TokenStream, input: TokenStream) -> TokenStream {
|
||||
}.boxed()
|
||||
}
|
||||
|
||||
#(#cooked)*
|
||||
#[allow(missing_docs)]
|
||||
pub static #name: #hook_path = #hook_path {
|
||||
fun: #fn_name,
|
||||
|
@ -7,7 +7,10 @@ use syn::{
|
||||
Attribute, Block, FnArg, Ident, Pat, ReturnType, Stmt, Token, Type, Visibility,
|
||||
};
|
||||
|
||||
use crate::util::{Argument, Parenthesised};
|
||||
use crate::{
|
||||
consts::{ARG, SUBCOMMAND},
|
||||
util::{Argument, IdentExt2, Parenthesised},
|
||||
};
|
||||
|
||||
fn parse_argument(arg: FnArg) -> Result<Argument> {
|
||||
match arg {
|
||||
@ -38,43 +41,12 @@ fn parse_argument(arg: FnArg) -> Result<Argument> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Test if the attribute is cooked.
|
||||
fn is_cooked(attr: &Attribute) -> bool {
|
||||
const COOKED_ATTRIBUTE_NAMES: &[&str] =
|
||||
&["cfg", "cfg_attr", "derive", "inline", "allow", "warn", "deny", "forbid"];
|
||||
|
||||
COOKED_ATTRIBUTE_NAMES.iter().any(|n| attr.path.is_ident(n))
|
||||
}
|
||||
|
||||
/// Removes cooked attributes from a vector of attributes. Uncooked attributes are left in the vector.
|
||||
///
|
||||
/// # Return
|
||||
///
|
||||
/// Returns a vector of cooked attributes that have been removed from the input vector.
|
||||
fn remove_cooked(attrs: &mut Vec<Attribute>) -> Vec<Attribute> {
|
||||
let mut cooked = Vec::new();
|
||||
|
||||
// FIXME: Replace with `Vec::drain_filter` once it is stable.
|
||||
let mut i = 0;
|
||||
while i < attrs.len() {
|
||||
if !is_cooked(&attrs[i]) {
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
cooked.push(attrs.remove(i));
|
||||
}
|
||||
|
||||
cooked
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct CommandFun {
|
||||
/// `#[...]`-style attributes.
|
||||
pub attributes: Vec<Attribute>,
|
||||
/// Populated cooked attributes. These are attributes outside of the realm of this crate's procedural macros
|
||||
/// and will appear in generated output.
|
||||
pub cooked: Vec<Attribute>,
|
||||
pub visibility: Visibility,
|
||||
pub name: Ident,
|
||||
pub args: Vec<Argument>,
|
||||
@ -84,9 +56,7 @@ pub struct CommandFun {
|
||||
|
||||
impl Parse for CommandFun {
|
||||
fn parse(input: ParseStream<'_>) -> Result<Self> {
|
||||
let mut attributes = input.call(Attribute::parse_outer)?;
|
||||
|
||||
let cooked = remove_cooked(&mut attributes);
|
||||
let attributes = input.call(Attribute::parse_outer)?;
|
||||
|
||||
let visibility = input.parse::<Visibility>()?;
|
||||
|
||||
@ -110,16 +80,15 @@ impl Parse for CommandFun {
|
||||
|
||||
let args = args.into_iter().map(parse_argument).collect::<Result<Vec<_>>>()?;
|
||||
|
||||
Ok(Self { attributes, cooked, visibility, name, args, ret, body })
|
||||
Ok(Self { attributes, visibility, name, args, ret, body })
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for CommandFun {
|
||||
fn to_tokens(&self, stream: &mut TokenStream2) {
|
||||
let Self { attributes: _, cooked, visibility, name, args, ret, body } = self;
|
||||
let Self { attributes: _, visibility, name, args, ret, body } = self;
|
||||
|
||||
stream.extend(quote! {
|
||||
#(#cooked)*
|
||||
#visibility async fn #name (#(#args),*) -> #ret {
|
||||
#(#body)*
|
||||
}
|
||||
@ -193,6 +162,24 @@ pub(crate) struct Arg {
|
||||
pub required: bool,
|
||||
}
|
||||
|
||||
impl Arg {
|
||||
pub fn as_tokens(&self, ident: &Ident) -> TokenStream2 {
|
||||
let arg_path = quote!(crate::framework::Arg);
|
||||
let Arg { name, description, kind, required } = self;
|
||||
|
||||
quote! {
|
||||
#[allow(missing_docs)]
|
||||
pub static #ident: #arg_path = #arg_path {
|
||||
name: #name,
|
||||
description: #description,
|
||||
kind: #kind,
|
||||
required: #required,
|
||||
options: &[]
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Arg {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
@ -211,6 +198,44 @@ pub(crate) struct Subcommand {
|
||||
pub cmd_args: Vec<Arg>,
|
||||
}
|
||||
|
||||
impl Subcommand {
|
||||
pub fn as_tokens(&mut self, ident: &Ident) -> TokenStream2 {
|
||||
let arg_path = quote!(crate::framework::Arg);
|
||||
let subcommand_path = ApplicationCommandOptionType::SubCommand;
|
||||
|
||||
let arg_idents = self
|
||||
.cmd_args
|
||||
.iter()
|
||||
.map(|arg| ident.with_suffix(arg.name.as_str()).with_suffix(ARG))
|
||||
.collect::<Vec<Ident>>();
|
||||
|
||||
let mut tokens = self
|
||||
.cmd_args
|
||||
.iter_mut()
|
||||
.zip(arg_idents.iter())
|
||||
.map(|(arg, ident)| arg.as_tokens(ident))
|
||||
.fold(quote! {}, |mut a, b| {
|
||||
a.extend(b);
|
||||
a
|
||||
});
|
||||
|
||||
let Subcommand { name, description, .. } = self;
|
||||
|
||||
tokens.extend(quote! {
|
||||
#[allow(missing_docs)]
|
||||
pub static #ident: #arg_path = #arg_path {
|
||||
name: #name,
|
||||
description: #description,
|
||||
kind: #subcommand_path,
|
||||
required: false,
|
||||
options: &[#(&#arg_idents),*],
|
||||
};
|
||||
});
|
||||
|
||||
tokens
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Subcommand {
|
||||
fn default() -> Self {
|
||||
Self { name: String::new(), description: String::new(), cmd_args: vec![] }
|
||||
@ -223,6 +248,68 @@ impl Subcommand {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct SubcommandGroup {
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub subcommands: Vec<Subcommand>,
|
||||
}
|
||||
|
||||
impl SubcommandGroup {
|
||||
pub fn as_tokens(&mut self, ident: &Ident) -> TokenStream2 {
|
||||
let arg_path = quote!(crate::framework::Arg);
|
||||
let subcommand_group_path = ApplicationCommandOptionType::SubCommandGroup;
|
||||
|
||||
let arg_idents = self
|
||||
.subcommands
|
||||
.iter()
|
||||
.map(|arg| {
|
||||
ident
|
||||
.with_suffix(self.name.as_str())
|
||||
.with_suffix(arg.name.as_str())
|
||||
.with_suffix(SUBCOMMAND)
|
||||
})
|
||||
.collect::<Vec<Ident>>();
|
||||
|
||||
let mut tokens = self
|
||||
.subcommands
|
||||
.iter_mut()
|
||||
.zip(arg_idents.iter())
|
||||
.map(|(subcommand, ident)| subcommand.as_tokens(ident))
|
||||
.fold(quote! {}, |mut a, b| {
|
||||
a.extend(b);
|
||||
a
|
||||
});
|
||||
|
||||
let SubcommandGroup { name, description, .. } = self;
|
||||
|
||||
tokens.extend(quote! {
|
||||
#[allow(missing_docs)]
|
||||
pub static #ident: #arg_path = #arg_path {
|
||||
name: #name,
|
||||
description: #description,
|
||||
kind: #subcommand_group_path,
|
||||
required: false,
|
||||
options: &[#(&#arg_idents),*],
|
||||
};
|
||||
});
|
||||
|
||||
tokens
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for SubcommandGroup {
|
||||
fn default() -> Self {
|
||||
Self { name: String::new(), description: String::new(), subcommands: vec![] }
|
||||
}
|
||||
}
|
||||
|
||||
impl SubcommandGroup {
|
||||
pub(crate) fn new(name: String) -> Self {
|
||||
Self { name, ..Default::default() }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub(crate) struct Options {
|
||||
pub aliases: Vec<String>,
|
||||
@ -233,6 +320,7 @@ pub(crate) struct Options {
|
||||
pub supports_dm: bool,
|
||||
pub cmd_args: Vec<Arg>,
|
||||
pub subcommands: Vec<Subcommand>,
|
||||
pub subcommand_groups: Vec<SubcommandGroup>,
|
||||
}
|
||||
|
||||
impl Options {
|
||||
|
@ -1,4 +1,4 @@
|
||||
pub mod info_cmds;
|
||||
pub mod moderation_cmds;
|
||||
pub mod reminder_cmds;
|
||||
//pub mod todo_cmds;
|
||||
pub mod todo_cmds;
|
||||
|
@ -1,443 +1,44 @@
|
||||
use std::{convert::TryFrom, fmt};
|
||||
|
||||
use regex_command_attr::command;
|
||||
use serenity::{
|
||||
async_trait,
|
||||
client::Context,
|
||||
constants::MESSAGE_CODE_LIMIT,
|
||||
model::{
|
||||
channel::Message,
|
||||
id::{ChannelId, GuildId, UserId},
|
||||
},
|
||||
};
|
||||
use sqlx::MySqlPool;
|
||||
use serenity::client::Context;
|
||||
|
||||
use crate::{
|
||||
command_help, get_ctx_data,
|
||||
models::{user_data::UserData, CtxData},
|
||||
};
|
||||
use crate::framework::{CommandInvoke, CommandOptions};
|
||||
|
||||
#[derive(Debug)]
|
||||
struct TodoNotFound;
|
||||
|
||||
impl std::error::Error for TodoNotFound {}
|
||||
impl fmt::Display for TodoNotFound {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "Todo not found")
|
||||
}
|
||||
}
|
||||
|
||||
struct Todo {
|
||||
id: u32,
|
||||
value: String,
|
||||
}
|
||||
|
||||
struct TodoTarget {
|
||||
user: UserId,
|
||||
guild: Option<GuildId>,
|
||||
channel: Option<ChannelId>,
|
||||
}
|
||||
|
||||
impl TodoTarget {
|
||||
pub fn command(&self, subcommand_opt: Option<SubCommand>) -> String {
|
||||
let context = if self.channel.is_some() {
|
||||
"channel"
|
||||
} else if self.guild.is_some() {
|
||||
"guild"
|
||||
} else {
|
||||
"user"
|
||||
};
|
||||
|
||||
if let Some(subcommand) = subcommand_opt {
|
||||
format!("todo {} {}", context, subcommand.to_string())
|
||||
} else {
|
||||
format!("todo {}", context)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn name(&self) -> String {
|
||||
if self.channel.is_some() {
|
||||
"Channel"
|
||||
} else if self.guild.is_some() {
|
||||
"Guild"
|
||||
} else {
|
||||
"User"
|
||||
}
|
||||
.to_string()
|
||||
}
|
||||
|
||||
pub async fn view(
|
||||
&self,
|
||||
pool: MySqlPool,
|
||||
) -> Result<Vec<Todo>, Box<dyn std::error::Error + Send + Sync>> {
|
||||
Ok(if let Some(cid) = self.channel {
|
||||
sqlx::query_as!(
|
||||
Todo,
|
||||
"
|
||||
SELECT id, value FROM todos WHERE channel_id = (SELECT id FROM channels WHERE channel = ?)
|
||||
",
|
||||
cid.as_u64()
|
||||
)
|
||||
.fetch_all(&pool)
|
||||
.await?
|
||||
} else if let Some(gid) = self.guild {
|
||||
sqlx::query_as!(
|
||||
Todo,
|
||||
"
|
||||
SELECT id, value FROM todos WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?) AND channel_id IS NULL
|
||||
",
|
||||
gid.as_u64()
|
||||
)
|
||||
.fetch_all(&pool)
|
||||
.await?
|
||||
} else {
|
||||
sqlx::query_as!(
|
||||
Todo,
|
||||
"
|
||||
SELECT id, value FROM todos WHERE user_id = (SELECT id FROM users WHERE user = ?) AND guild_id IS NULL
|
||||
",
|
||||
self.user.as_u64()
|
||||
)
|
||||
.fetch_all(&pool)
|
||||
.await?
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn add(
|
||||
&self,
|
||||
value: String,
|
||||
pool: MySqlPool,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
if let (Some(cid), Some(gid)) = (self.channel, self.guild) {
|
||||
sqlx::query!(
|
||||
"
|
||||
INSERT INTO todos (user_id, guild_id, channel_id, value) VALUES (
|
||||
(SELECT id FROM users WHERE user = ?),
|
||||
(SELECT id FROM guilds WHERE guild = ?),
|
||||
(SELECT id FROM channels WHERE channel = ?),
|
||||
?
|
||||
)
|
||||
",
|
||||
self.user.as_u64(),
|
||||
gid.as_u64(),
|
||||
cid.as_u64(),
|
||||
value
|
||||
)
|
||||
.execute(&pool)
|
||||
.await?;
|
||||
} else if let Some(gid) = self.guild {
|
||||
sqlx::query!(
|
||||
"
|
||||
INSERT INTO todos (user_id, guild_id, value) VALUES (
|
||||
(SELECT id FROM users WHERE user = ?),
|
||||
(SELECT id FROM guilds WHERE guild = ?),
|
||||
?
|
||||
)
|
||||
",
|
||||
self.user.as_u64(),
|
||||
gid.as_u64(),
|
||||
value
|
||||
)
|
||||
.execute(&pool)
|
||||
.await?;
|
||||
} else {
|
||||
sqlx::query!(
|
||||
"
|
||||
INSERT INTO todos (user_id, value) VALUES (
|
||||
(SELECT id FROM users WHERE user = ?),
|
||||
?
|
||||
)
|
||||
",
|
||||
self.user.as_u64(),
|
||||
value
|
||||
)
|
||||
.execute(&pool)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn remove(
|
||||
&self,
|
||||
num: usize,
|
||||
pool: &MySqlPool,
|
||||
) -> Result<Todo, Box<dyn std::error::Error + Sync + Send>> {
|
||||
let todos = self.view(pool.clone()).await?;
|
||||
|
||||
if let Some(removal_todo) = todos.get(num) {
|
||||
let deleting = sqlx::query_as!(
|
||||
Todo,
|
||||
"
|
||||
SELECT id, value FROM todos WHERE id = ?
|
||||
",
|
||||
removal_todo.id
|
||||
)
|
||||
.fetch_one(&pool.clone())
|
||||
.await?;
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
DELETE FROM todos WHERE id = ?
|
||||
",
|
||||
removal_todo.id
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
Ok(deleting)
|
||||
} else {
|
||||
Err(Box::new(TodoNotFound))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn clear(
|
||||
&self,
|
||||
pool: &MySqlPool,
|
||||
) -> Result<(), Box<dyn std::error::Error + Sync + Send>> {
|
||||
if let Some(cid) = self.channel {
|
||||
sqlx::query!(
|
||||
"
|
||||
DELETE FROM todos WHERE channel_id = (SELECT id FROM channels WHERE channel = ?)
|
||||
",
|
||||
cid.as_u64()
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
} else if let Some(gid) = self.guild {
|
||||
sqlx::query!(
|
||||
"
|
||||
DELETE FROM todos WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?) AND channel_id IS NULL
|
||||
",
|
||||
gid.as_u64()
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
} else {
|
||||
sqlx::query!(
|
||||
"
|
||||
DELETE FROM todos WHERE user_id = (SELECT id FROM users WHERE user = ?) AND guild_id IS NULL
|
||||
",
|
||||
self.user.as_u64()
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn execute(&self, ctx: &Context, msg: &Message, subcommand: SubCommand, extra: String) {
|
||||
let (pool, lm) = get_ctx_data(&ctx).await;
|
||||
|
||||
let user_data = UserData::from_user(&msg.author, &ctx, &pool).await.unwrap();
|
||||
let prefix = ctx.prefix(msg.guild_id).await;
|
||||
|
||||
match subcommand {
|
||||
SubCommand::View => {
|
||||
let todo_items = self.view(pool).await.unwrap();
|
||||
let mut todo_groups = vec!["".to_string()];
|
||||
let mut char_count = 0;
|
||||
|
||||
todo_items.iter().enumerate().for_each(|(count, todo)| {
|
||||
let display = format!("{}: {}\n", count + 1, todo.value);
|
||||
|
||||
if char_count + display.len() > MESSAGE_CODE_LIMIT as usize {
|
||||
char_count = display.len();
|
||||
|
||||
todo_groups.push(display);
|
||||
} else {
|
||||
char_count += display.len();
|
||||
|
||||
let last_group = todo_groups.pop().unwrap();
|
||||
|
||||
todo_groups.push(format!("{}{}", last_group, display));
|
||||
}
|
||||
});
|
||||
|
||||
for group in todo_groups {
|
||||
let _ = msg
|
||||
.channel_id
|
||||
.send_message(&ctx, |m| {
|
||||
m.embed(|e| e.title(format!("{} Todo", self.name())).description(group))
|
||||
})
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
SubCommand::Add => {
|
||||
let content = lm
|
||||
.get(&user_data.language, "todo/added")
|
||||
.replacen("{name}", &extra, 1);
|
||||
|
||||
self.add(extra, pool).await.unwrap();
|
||||
|
||||
let _ = msg.channel_id.say(&ctx, content).await;
|
||||
}
|
||||
|
||||
SubCommand::Remove => {
|
||||
if let Ok(num) = extra.parse::<usize>() {
|
||||
if let Ok(todo) = self.remove(num - 1, &pool).await {
|
||||
let content = lm.get(&user_data.language, "todo/removed").replacen(
|
||||
"{}",
|
||||
&todo.value,
|
||||
1,
|
||||
);
|
||||
|
||||
let _ = msg.channel_id.say(&ctx, content).await;
|
||||
} else {
|
||||
let _ = msg
|
||||
.channel_id
|
||||
.say(&ctx, lm.get(&user_data.language, "todo/error_index"))
|
||||
.await;
|
||||
}
|
||||
} else {
|
||||
let content = lm
|
||||
.get(&user_data.language, "todo/error_value")
|
||||
.replacen("{prefix}", &prefix, 1)
|
||||
.replacen("{command}", &self.command(Some(subcommand)), 1);
|
||||
|
||||
let _ = msg.channel_id.say(&ctx, content).await;
|
||||
}
|
||||
}
|
||||
|
||||
SubCommand::Clear => {
|
||||
self.clear(&pool).await.unwrap();
|
||||
|
||||
let content = lm.get(&user_data.language, "todo/cleared");
|
||||
|
||||
let _ = msg.channel_id.say(&ctx, content).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum SubCommand {
|
||||
View,
|
||||
Add,
|
||||
Remove,
|
||||
Clear,
|
||||
}
|
||||
|
||||
impl TryFrom<Option<&str>> for SubCommand {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(value: Option<&str>) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
Some("add") => Ok(SubCommand::Add),
|
||||
|
||||
Some("remove") => Ok(SubCommand::Remove),
|
||||
|
||||
Some("clear") => Ok(SubCommand::Clear),
|
||||
|
||||
None | Some("") => Ok(SubCommand::View),
|
||||
|
||||
Some(_unrecognised) => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToString for SubCommand {
|
||||
fn to_string(&self) -> String {
|
||||
match self {
|
||||
SubCommand::View => "",
|
||||
SubCommand::Add => "add",
|
||||
SubCommand::Remove => "remove",
|
||||
SubCommand::Clear => "clear",
|
||||
}
|
||||
.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
trait Execute {
|
||||
async fn execute(self, ctx: &Context, msg: &Message, extra: String, target: TodoTarget);
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Execute for Result<SubCommand, ()> {
|
||||
async fn execute(self, ctx: &Context, msg: &Message, extra: String, target: TodoTarget) {
|
||||
if let Ok(subcommand) = self {
|
||||
target.execute(ctx, msg, subcommand, extra).await;
|
||||
} else {
|
||||
show_help(ctx, msg, Some(target)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[command("todo")]
|
||||
async fn todo_user(ctx: &Context, msg: &Message, args: String) {
|
||||
let mut split = args.split(' ');
|
||||
|
||||
let target = TodoTarget {
|
||||
user: msg.author.id,
|
||||
guild: None,
|
||||
channel: None,
|
||||
};
|
||||
|
||||
let subcommand_opt = SubCommand::try_from(split.next());
|
||||
|
||||
subcommand_opt
|
||||
.execute(ctx, msg, split.collect::<Vec<&str>>().join(" "), target)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[command("todoc")]
|
||||
#[supports_dm(false)]
|
||||
#[permission_level(Managed)]
|
||||
async fn todo_channel(ctx: &Context, msg: &Message, args: String) {
|
||||
let mut split = args.split(' ');
|
||||
|
||||
let target = TodoTarget {
|
||||
user: msg.author.id,
|
||||
guild: msg.guild_id,
|
||||
channel: Some(msg.channel_id),
|
||||
};
|
||||
|
||||
let subcommand_opt = SubCommand::try_from(split.next());
|
||||
|
||||
subcommand_opt
|
||||
.execute(ctx, msg, split.collect::<Vec<&str>>().join(" "), target)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[command("todos")]
|
||||
#[supports_dm(false)]
|
||||
#[permission_level(Managed)]
|
||||
async fn todo_guild(ctx: &Context, msg: &Message, args: String) {
|
||||
let mut split = args.split(' ');
|
||||
|
||||
let target = TodoTarget {
|
||||
user: msg.author.id,
|
||||
guild: msg.guild_id,
|
||||
channel: None,
|
||||
};
|
||||
|
||||
let subcommand_opt = SubCommand::try_from(split.next());
|
||||
|
||||
subcommand_opt
|
||||
.execute(ctx, msg, split.collect::<Vec<&str>>().join(" "), target)
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn show_help(ctx: &Context, msg: &Message, target: Option<TodoTarget>) {
|
||||
let (pool, lm) = get_ctx_data(&ctx).await;
|
||||
|
||||
let language = UserData::language_of(&msg.author, &pool);
|
||||
let prefix = ctx.prefix(msg.guild_id);
|
||||
|
||||
let command = match target {
|
||||
None => "todo",
|
||||
Some(t) => {
|
||||
if t.channel.is_some() {
|
||||
"todoc"
|
||||
} else if t.guild.is_some() {
|
||||
"todos"
|
||||
} else {
|
||||
"todo"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
command_help(ctx, msg, lm, &prefix.await, &language.await, command).await;
|
||||
}
|
||||
#[command]
|
||||
#[description("Manage todo lists")]
|
||||
#[subcommandgroup("server")]
|
||||
#[description("Manage the server todo list")]
|
||||
#[subcommand("add")]
|
||||
#[description("Add an item to the server todo list")]
|
||||
#[arg(
|
||||
name = "task",
|
||||
description = "The task to add to the todo list",
|
||||
kind = "String",
|
||||
required = true
|
||||
)]
|
||||
#[subcommand("view")]
|
||||
#[description("View and remove from the server todo list")]
|
||||
#[subcommandgroup("channel")]
|
||||
#[description("Manage the channel todo list")]
|
||||
#[subcommand("add")]
|
||||
#[description("Add to the channel todo list")]
|
||||
#[arg(
|
||||
name = "task",
|
||||
description = "The task to add to the todo list",
|
||||
kind = "String",
|
||||
required = true
|
||||
)]
|
||||
#[subcommand("view")]
|
||||
#[description("View and remove from the channel todo list")]
|
||||
#[subcommandgroup("user")]
|
||||
#[description("Manage your personal todo list")]
|
||||
#[subcommand("add")]
|
||||
#[description("Add to your personal todo list")]
|
||||
#[arg(
|
||||
name = "task",
|
||||
description = "The task to add to the todo list",
|
||||
kind = "String",
|
||||
required = true
|
||||
)]
|
||||
#[subcommand("view")]
|
||||
#[description("View and remove from your personal todo list")]
|
||||
async fn todo(ctx: &Context, invoke: CommandInvoke, args: CommandOptions) {}
|
||||
|
12
src/main.rs
12
src/main.rs
@ -36,7 +36,7 @@ use sqlx::mysql::MySqlPool;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use crate::{
|
||||
commands::{info_cmds, moderation_cmds, reminder_cmds},
|
||||
commands::{info_cmds, moderation_cmds, reminder_cmds, todo_cmds},
|
||||
component_models::ComponentDataModel,
|
||||
consts::{CNC_GUILD, DEFAULT_PREFIX, SUBSCRIPTION_ROLES, THEME_COLOR},
|
||||
framework::RegexFramework,
|
||||
@ -318,16 +318,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
.add_command(&reminder_cmds::PAUSE_COMMAND)
|
||||
.add_command(&reminder_cmds::OFFSET_COMMAND)
|
||||
.add_command(&reminder_cmds::NUDGE_COMMAND)
|
||||
/*
|
||||
// to-do commands
|
||||
.add_command("todo", &todo_cmds::TODO_USER_COMMAND)
|
||||
.add_command("todo user", &todo_cmds::TODO_USER_COMMAND)
|
||||
.add_command("todoc", &todo_cmds::TODO_CHANNEL_COMMAND)
|
||||
.add_command("todo channel", &todo_cmds::TODO_CHANNEL_COMMAND)
|
||||
.add_command("todos", &todo_cmds::TODO_GUILD_COMMAND)
|
||||
.add_command("todo server", &todo_cmds::TODO_GUILD_COMMAND)
|
||||
.add_command("todo guild", &todo_cmds::TODO_GUILD_COMMAND)
|
||||
*/
|
||||
.add_command(&todo_cmds::TODO_COMMAND)
|
||||
// moderation commands
|
||||
.add_command(&moderation_cmds::BLACKLIST_COMMAND)
|
||||
.add_command(&moderation_cmds::RESTRICT_COMMAND)
|
||||
|
Loading…
Reference in New Issue
Block a user