From afc376c44fd0c1b6d7c8967200330c4de1abb8d7 Mon Sep 17 00:00:00 2001 From: jude Date: Sat, 19 Feb 2022 18:21:11 +0000 Subject: [PATCH] everything except component model actions --- Cargo.lock | 20 - Cargo.toml | 3 - command_attributes/Cargo.toml | 16 - command_attributes/src/attributes.rs | 351 ------------- command_attributes/src/consts.rs | 10 - command_attributes/src/lib.rs | 321 ------------ command_attributes/src/structures.rs | 331 ------------ command_attributes/src/util.rs | 176 ------- src/commands/mod.rs | 4 +- src/commands/moderation_cmds.rs | 58 +-- src/commands/reminder_cmds.rs | 751 +++++++++++---------------- src/commands/todo_cmds.rs | 300 ++++++----- src/component_models/mod.rs | 113 ++-- src/component_models/pager.rs | 4 +- src/consts.rs | 2 + src/event_handlers.rs | 31 +- src/framework.rs | 692 ------------------------ src/hooks.rs | 12 +- src/main.rs | 41 +- src/models/command_macro.rs | 74 ++- src/models/mod.rs | 31 +- 21 files changed, 718 insertions(+), 2623 deletions(-) delete mode 100644 command_attributes/Cargo.toml delete mode 100644 command_attributes/src/attributes.rs delete mode 100644 command_attributes/src/consts.rs delete mode 100644 command_attributes/src/lib.rs delete mode 100644 command_attributes/src/structures.rs delete mode 100644 command_attributes/src/util.rs delete mode 100644 src/framework.rs diff --git a/Cargo.lock b/Cargo.lock index 62d8d9a..aa5f7e1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2100,16 +2100,6 @@ version = "0.6.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" -[[package]] -name = "regex_command_attr" -version = "0.3.6" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "uuid", -] - [[package]] name = "reminder_rs" version = "1.6.0-beta3" @@ -2127,7 +2117,6 @@ dependencies = [ "postman", "rand 0.7.3", "regex", - "regex_command_attr", "reminder_web", "reqwest", "rmp-serde", @@ -3340,15 +3329,6 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" -[[package]] -name = "uuid" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" -dependencies = [ - "getrandom 0.2.4", -] - [[package]] name = "valuable" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 23f13b0..15969dc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,9 +25,6 @@ levenshtein = "1.0" sqlx = { version = "0.5", features = ["runtime-tokio-rustls", "macros", "mysql", "bigdecimal", "chrono"]} base64 = "0.13.0" -[dependencies.regex_command_attr] -path = "command_attributes" - [dependencies.postman] path = "postman" diff --git a/command_attributes/Cargo.toml b/command_attributes/Cargo.toml deleted file mode 100644 index a4ce1d3..0000000 --- a/command_attributes/Cargo.toml +++ /dev/null @@ -1,16 +0,0 @@ -[package] -name = "regex_command_attr" -version = "0.3.6" -authors = ["acdenisSK ", "jellywx "] -edition = "2018" -description = "Procedural macros for command creation for the Serenity library." -license = "ISC" - -[lib] -proc-macro = true - -[dependencies] -quote = "^1.0" -syn = { version = "^1.0", features = ["full", "derive", "extra-traits"] } -proc-macro2 = "1.0" -uuid = { version = "0.8", features = ["v4"] } diff --git a/command_attributes/src/attributes.rs b/command_attributes/src/attributes.rs deleted file mode 100644 index 1293186..0000000 --- a/command_attributes/src/attributes.rs +++ /dev/null @@ -1,351 +0,0 @@ -use std::fmt::{self, Write}; - -use proc_macro2::Span; -use syn::{ - parse::{Error, Result}, - spanned::Spanned, - Attribute, Ident, Lit, LitStr, Meta, NestedMeta, Path, -}; - -use crate::{ - structures::{ApplicationCommandOptionType, Arg}, - util::{AsOption, LitExt}, -}; - -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum ValueKind { - // #[] - Name, - - // #[ = ] - Equals, - - // #[([, , , ...])] - List, - - // #[([ = , = , ...])] - EqualsList, - - // #[()] - SingleList, -} - -impl fmt::Display for ValueKind { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - ValueKind::Name => f.pad("`#[]`"), - ValueKind::Equals => f.pad("`#[ = ]`"), - ValueKind::List => f.pad("`#[([, , , ...])]`"), - ValueKind::EqualsList => { - f.pad("`#[([ = , = , ...])]`") - } - ValueKind::SingleList => f.pad("`#[()]`"), - } - } -} - -fn to_ident(p: Path) -> Result { - if p.segments.is_empty() { - return Err(Error::new(p.span(), "cannot convert an empty path to an identifier")); - } - - if p.segments.len() > 1 { - return Err(Error::new(p.span(), "the path must not have more than one segment")); - } - - if !p.segments[0].arguments.is_empty() { - return Err(Error::new(p.span(), "the singular path segment must not have any arguments")); - } - - Ok(p.segments[0].ident.clone()) -} - -#[derive(Debug)] -pub struct Values { - pub name: Ident, - pub literals: Vec<(Option, Lit)>, - pub kind: ValueKind, - pub span: Span, -} - -impl Values { - #[inline] - pub fn new( - name: Ident, - kind: ValueKind, - literals: Vec<(Option, Lit)>, - span: Span, - ) -> Self { - Values { name, literals, kind, span } - } -} - -pub fn parse_values(attr: &Attribute) -> Result { - fn is_list_or_named_list(meta: &NestedMeta) -> ValueKind { - match meta { - // catch if the nested value is a literal value - NestedMeta::Lit(_) => ValueKind::List, - // catch if the nested value is a meta value - NestedMeta::Meta(m) => match m { - // path => some quoted value - Meta::Path(_) => ValueKind::List, - Meta::List(_) | Meta::NameValue(_) => ValueKind::EqualsList, - }, - } - } - - let meta = attr.parse_meta()?; - - match meta { - Meta::Path(path) => { - let name = to_ident(path)?; - - Ok(Values::new(name, ValueKind::Name, Vec::new(), attr.span())) - } - Meta::List(meta) => { - let name = to_ident(meta.path)?; - let nested = meta.nested; - - if nested.is_empty() { - return Err(Error::new(attr.span(), "list cannot be empty")); - } - - if is_list_or_named_list(nested.first().unwrap()) == ValueKind::List { - let mut lits = Vec::with_capacity(nested.len()); - - for meta in nested { - match meta { - // catch if the nested value is a literal value - NestedMeta::Lit(l) => lits.push((None, l)), - // catch if the nested value is a meta value - NestedMeta::Meta(m) => match m { - // path => some quoted value - Meta::Path(path) => { - let i = to_ident(path)?; - lits.push((None, Lit::Str(LitStr::new(&i.to_string(), i.span())))) - } - Meta::List(_) | Meta::NameValue(_) => { - return Err(Error::new(attr.span(), "cannot nest a list; only accept literals and identifiers at this level")) - } - }, - } - } - - let kind = if lits.len() == 1 { ValueKind::SingleList } else { ValueKind::List }; - - Ok(Values::new(name, kind, lits, attr.span())) - } else { - let mut lits = Vec::with_capacity(nested.len()); - - for meta in nested { - match meta { - // catch if the nested value is a literal value - NestedMeta::Lit(_) => { - return Err(Error::new(attr.span(), "key-value pairs expected")) - } - // catch if the nested value is a meta value - NestedMeta::Meta(m) => match m { - Meta::NameValue(n) => { - let name = to_ident(n.path)?.to_string(); - let value = n.lit; - - lits.push((Some(name), value)); - } - Meta::List(_) | Meta::Path(_) => { - return Err(Error::new(attr.span(), "key-value pairs expected")) - } - }, - } - } - - Ok(Values::new(name, ValueKind::EqualsList, lits, attr.span())) - } - } - Meta::NameValue(meta) => { - let name = to_ident(meta.path)?; - let lit = meta.lit; - - Ok(Values::new(name, ValueKind::Equals, vec![(None, lit)], attr.span())) - } - } -} - -#[derive(Debug, Clone)] -struct DisplaySlice<'a, T>(&'a [T]); - -impl<'a, T: fmt::Display> fmt::Display for DisplaySlice<'a, T> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let mut iter = self.0.iter().enumerate(); - - match iter.next() { - None => f.write_str("nothing")?, - Some((idx, elem)) => { - write!(f, "{}: {}", idx, elem)?; - - for (idx, elem) in iter { - f.write_char('\n')?; - write!(f, "{}: {}", idx, elem)?; - } - } - } - - Ok(()) - } -} - -#[inline] -fn is_form_acceptable(expect: &[ValueKind], kind: ValueKind) -> bool { - if expect.contains(&ValueKind::List) && kind == ValueKind::SingleList { - true - } else { - expect.contains(&kind) - } -} - -#[inline] -fn validate(values: &Values, forms: &[ValueKind]) -> Result<()> { - if !is_form_acceptable(forms, values.kind) { - return Err(Error::new( - values.span, - // Using the `_args` version here to avoid an allocation. - format_args!("the attribute must be in of these forms:\n{}", DisplaySlice(forms)), - )); - } - - Ok(()) -} - -#[inline] -pub fn parse(values: Values) -> Result { - T::parse(values) -} - -pub trait AttributeOption: Sized { - fn parse(values: Values) -> Result; -} - -impl AttributeOption for Vec { - fn parse(values: Values) -> Result { - validate(&values, &[ValueKind::List])?; - - Ok(values.literals.into_iter().map(|(_, l)| l.to_str()).collect()) - } -} - -impl AttributeOption for String { - #[inline] - fn parse(values: Values) -> Result { - validate(&values, &[ValueKind::Equals, ValueKind::SingleList])?; - - Ok(values.literals[0].1.to_str()) - } -} - -impl AttributeOption for bool { - #[inline] - fn parse(values: Values) -> Result { - validate(&values, &[ValueKind::Name, ValueKind::SingleList])?; - - Ok(values.literals.get(0).map_or(true, |(_, l)| l.to_bool())) - } -} - -impl AttributeOption for Ident { - #[inline] - fn parse(values: Values) -> Result { - validate(&values, &[ValueKind::SingleList])?; - - Ok(values.literals[0].1.to_ident()) - } -} - -impl AttributeOption for Vec { - #[inline] - fn parse(values: Values) -> Result { - validate(&values, &[ValueKind::List])?; - - Ok(values.literals.into_iter().map(|(_, l)| l.to_ident()).collect()) - } -} - -impl AttributeOption for Option { - fn parse(values: Values) -> Result { - validate(&values, &[ValueKind::Name, ValueKind::Equals, ValueKind::SingleList])?; - - Ok(values.literals.get(0).map(|(_, l)| l.to_str())) - } -} - -impl AttributeOption for Arg { - fn parse(values: Values) -> Result { - validate(&values, &[ValueKind::EqualsList])?; - - let mut arg: Arg = Default::default(); - - for (key, value) in &values.literals { - match key { - Some(s) => match s.as_str() { - "name" => { - arg.name = value.to_str(); - } - "description" => { - arg.description = value.to_str(); - } - "required" => { - arg.required = value.to_bool(); - } - "kind" => arg.kind = ApplicationCommandOptionType::from_str(value.to_str()), - _ => { - return Err(Error::new(key.span(), "unexpected attribute")); - } - }, - _ => { - return Err(Error::new(key.span(), "unnamed attribute")); - } - } - } - - Ok(arg) - } -} - -impl AttributeOption for AsOption { - #[inline] - fn parse(values: Values) -> Result { - Ok(AsOption(Some(T::parse(values)?))) - } -} - -macro_rules! attr_option_num { - ($($n:ty),*) => { - $( - impl AttributeOption for $n { - fn parse(values: Values) -> Result { - validate(&values, &[ValueKind::SingleList])?; - - Ok(match &values.literals[0].1 { - Lit::Int(l) => l.base10_parse::<$n>()?, - l => { - let s = l.to_str(); - // Use `as_str` to guide the compiler to use `&str`'s parse method. - // We don't want to use our `parse` method here (`impl AttributeOption for String`). - match s.as_str().parse::<$n>() { - Ok(n) => n, - Err(_) => return Err(Error::new(l.span(), "invalid integer")), - } - } - }) - } - } - - impl AttributeOption for Option<$n> { - #[inline] - fn parse(values: Values) -> Result { - <$n as AttributeOption>::parse(values).map(Some) - } - } - )* - } -} - -attr_option_num!(u16, u32, usize); diff --git a/command_attributes/src/consts.rs b/command_attributes/src/consts.rs deleted file mode 100644 index 8c334b4..0000000 --- a/command_attributes/src/consts.rs +++ /dev/null @@ -1,10 +0,0 @@ -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"; -} - -pub use self::suffixes::*; diff --git a/command_attributes/src/lib.rs b/command_attributes/src/lib.rs deleted file mode 100644 index cce792e..0000000 --- a/command_attributes/src/lib.rs +++ /dev/null @@ -1,321 +0,0 @@ -#![deny(rust_2018_idioms)] -#![deny(broken_intra_doc_links)] - -use proc_macro::TokenStream; -use proc_macro2::Ident; -use quote::quote; -use syn::{parse::Error, parse_macro_input, parse_quote, spanned::Spanned, Lit, Type}; -use uuid::Uuid; - -pub(crate) mod attributes; -pub(crate) mod consts; -pub(crate) mod structures; - -#[macro_use] -pub(crate) mod util; - -use attributes::*; -use consts::*; -use structures::*; -use util::*; - -macro_rules! match_options { - ($v:expr, $values:ident, $options:ident, $span:expr => [$($name:ident);*]) => { - match $v { - $( - stringify!($name) => $options.$name = propagate_err!($crate::attributes::parse($values)), - )* - _ => { - return Error::new($span, format_args!("invalid attribute: {:?}", $v)) - .to_compile_error() - .into(); - }, - } - }; -} - -#[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() { - parse_macro_input!(attr as Lit).to_str() - } else { - fun.name.to_string() - }; - - let mut hooks: Vec = Vec::new(); - let mut options = Options::new(); - let mut last_desc = LastItem::Fun; - - for attribute in &fun.attributes { - let span = attribute.span(); - let values = propagate_err!(parse_values(attribute)); - - let name = values.name.to_string(); - let name = &name[..]; - - match name { - "subcommand" => { - 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" => { - let arg = propagate_err!(attributes::parse(values)); - - match last_desc { - LastItem::Fun => { - options.cmd_args.push(arg); - } - LastItem::SubFun => { - options.subcommands.last_mut().unwrap().cmd_args.push(arg); - } - LastItem::SubGroup => { - panic!("Argument not expected under subcommand group"); - } - LastItem::SubGroupFun => { - options - .subcommand_groups - .last_mut() - .unwrap() - .subcommands - .last_mut() - .unwrap() - .cmd_args - .push(arg); - } - } - } - "example" => { - options.examples.push(propagate_err!(attributes::parse(values))); - } - "description" => { - let line: String = propagate_err!(attributes::parse(values)); - - 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" => { - hooks.push(propagate_err!(attributes::parse(values))); - } - _ => { - match_options!(name, values, options, span => [ - aliases; - group; - can_blacklist; - supports_dm - ]); - } - } - } - - let Options { - aliases, - description, - group, - examples, - can_blacklist, - 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 root_ident = name.with_suffix(COMMAND); - - let command_path = quote!(crate::framework::Command); - - populate_fut_lifetimes_on_refs(&mut 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::>(); - - let mut subcommand_idents = subcommands - .iter() - .map(|subcommand| { - root_ident - .with_suffix(subcommand.name.replace("-", "_").as_str()) - .with_suffix(SUBCOMMAND) - }) - .collect::>(); - - let mut arg_idents = cmd_args - .iter() - .map(|arg| root_ident.with_suffix(arg.name.replace("-", "_").as_str()).with_suffix(ARG)) - .collect::>(); - - let mut tokens = quote! {}; - - 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( - 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 { - let string: Type = parse_quote!(String); - - let final_arg = args.get(2).unwrap(); - - if final_arg.kind == string { - quote!(crate::framework::CommandFnType::Text) - } else { - quote!(crate::framework::CommandFnType::Slash) - } - }; - - tokens.extend(quote! { - #[allow(missing_docs)] - pub static #root_ident: #command_path = #command_path { - fun: #variant(#name), - names: &[#_name, #(#aliases),*], - desc: #description, - group: #group, - examples: &[#(#examples),*], - can_blacklist: #can_blacklist, - supports_dm: #supports_dm, - args: &[#(&#arg_idents),*], - hooks: &[#(&#hooks),*], - }; - - #[allow(missing_docs)] - #visibility fn #name<'fut> (#(#args),*) -> ::serenity::futures::future::BoxFuture<'fut, ()> { - use ::serenity::futures::future::FutureExt; - - async move { - #(#body)*; - }.boxed() - } - }); - - tokens.into() -} - -#[proc_macro_attribute] -pub fn check(_attr: TokenStream, input: TokenStream) -> TokenStream { - let mut fun = parse_macro_input!(input as CommandFun); - - let n = fun.name.clone(); - let name = n.with_suffix(HOOK); - let fn_name = n.with_suffix(CHECK); - let visibility = fun.visibility; - - let body = fun.body; - let ret = fun.ret; - populate_fut_lifetimes_on_refs(&mut fun.args); - let args = fun.args; - - let hook_path = quote!(crate::framework::Hook); - let uuid = Uuid::new_v4().as_u128(); - - (quote! { - #[allow(missing_docs)] - #visibility fn #fn_name<'fut>(#(#args),*) -> ::serenity::futures::future::BoxFuture<'fut, #ret> { - use ::serenity::futures::future::FutureExt; - - async move { - let _output: #ret = { #(#body)* }; - #[allow(unreachable_code)] - _output - }.boxed() - } - - #[allow(missing_docs)] - pub static #name: #hook_path = #hook_path { - fun: #fn_name, - uuid: #uuid, - }; - }) - .into() -} diff --git a/command_attributes/src/structures.rs b/command_attributes/src/structures.rs deleted file mode 100644 index f77ce46..0000000 --- a/command_attributes/src/structures.rs +++ /dev/null @@ -1,331 +0,0 @@ -use proc_macro2::TokenStream as TokenStream2; -use quote::{quote, ToTokens}; -use syn::{ - braced, - parse::{Error, Parse, ParseStream, Result}, - spanned::Spanned, - Attribute, Block, FnArg, Ident, Pat, ReturnType, Stmt, Token, Type, Visibility, -}; - -use crate::{ - consts::{ARG, SUBCOMMAND}, - util::{Argument, IdentExt2, Parenthesised}, -}; - -fn parse_argument(arg: FnArg) -> Result { - match arg { - FnArg::Typed(typed) => { - let pat = typed.pat; - let kind = typed.ty; - - match *pat { - Pat::Ident(id) => { - let name = id.ident; - let mutable = id.mutability; - - Ok(Argument { mutable, name, kind: *kind }) - } - Pat::Wild(wild) => { - let token = wild.underscore_token; - - let name = Ident::new("_", token.spans[0]); - - Ok(Argument { mutable: None, name, kind: *kind }) - } - _ => Err(Error::new(pat.span(), format_args!("unsupported pattern: {:?}", pat))), - } - } - FnArg::Receiver(_) => { - Err(Error::new(arg.span(), format_args!("`self` arguments are prohibited: {:?}", arg))) - } - } -} - -#[derive(Debug)] -pub struct CommandFun { - /// `#[...]`-style attributes. - pub attributes: Vec, - /// Populated cooked attributes. These are attributes outside of the realm of this crate's procedural macros - /// and will appear in generated output. - pub visibility: Visibility, - pub name: Ident, - pub args: Vec, - pub ret: Type, - pub body: Vec, -} - -impl Parse for CommandFun { - fn parse(input: ParseStream<'_>) -> Result { - let attributes = input.call(Attribute::parse_outer)?; - - let visibility = input.parse::()?; - - input.parse::()?; - - input.parse::()?; - let name = input.parse()?; - - // (...) - let Parenthesised(args) = input.parse::>()?; - - let ret = match input.parse::()? { - ReturnType::Type(_, t) => (*t).clone(), - ReturnType::Default => Type::Verbatim(quote!(())), - }; - - // { ... } - let bcont; - braced!(bcont in input); - let body = bcont.call(Block::parse_within)?; - - let args = args.into_iter().map(parse_argument).collect::>>()?; - - Ok(Self { attributes, visibility, name, args, ret, body }) - } -} - -impl ToTokens for CommandFun { - fn to_tokens(&self, stream: &mut TokenStream2) { - let Self { attributes: _, visibility, name, args, ret, body } = self; - - stream.extend(quote! { - #visibility async fn #name (#(#args),*) -> #ret { - #(#body)* - } - }); - } -} - -#[derive(Debug)] -pub(crate) enum ApplicationCommandOptionType { - SubCommand, - SubCommandGroup, - String, - Integer, - Boolean, - User, - Channel, - Role, - Mentionable, - Number, - Unknown, -} - -impl ApplicationCommandOptionType { - pub fn from_str(s: String) -> Self { - match s.as_str() { - "SubCommand" => Self::SubCommand, - "SubCommandGroup" => Self::SubCommandGroup, - "String" => Self::String, - "Integer" => Self::Integer, - "Boolean" => Self::Boolean, - "User" => Self::User, - "Channel" => Self::Channel, - "Role" => Self::Role, - "Mentionable" => Self::Mentionable, - "Number" => Self::Number, - _ => Self::Unknown, - } - } -} - -impl ToTokens for ApplicationCommandOptionType { - fn to_tokens(&self, stream: &mut TokenStream2) { - let path = quote!( - serenity::model::interactions::application_command::ApplicationCommandOptionType - ); - let variant = match self { - ApplicationCommandOptionType::SubCommand => quote!(SubCommand), - ApplicationCommandOptionType::SubCommandGroup => quote!(SubCommandGroup), - ApplicationCommandOptionType::String => quote!(String), - ApplicationCommandOptionType::Integer => quote!(Integer), - ApplicationCommandOptionType::Boolean => quote!(Boolean), - ApplicationCommandOptionType::User => quote!(User), - ApplicationCommandOptionType::Channel => quote!(Channel), - ApplicationCommandOptionType::Role => quote!(Role), - ApplicationCommandOptionType::Mentionable => quote!(Mentionable), - ApplicationCommandOptionType::Number => quote!(Number), - ApplicationCommandOptionType::Unknown => quote!(Unknown), - }; - - stream.extend(quote! { - #path::#variant - }); - } -} - -#[derive(Debug)] -pub(crate) struct Arg { - pub name: String, - pub description: String, - pub kind: ApplicationCommandOptionType, - 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 { - name: String::new(), - description: String::new(), - kind: ApplicationCommandOptionType::String, - required: false, - } - } -} - -#[derive(Debug)] -pub(crate) struct Subcommand { - pub name: String, - pub description: String, - pub cmd_args: Vec, -} - -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::>(); - - 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![] } - } -} - -impl Subcommand { - pub(crate) fn new(name: String) -> Self { - Self { name, ..Default::default() } - } -} - -#[derive(Debug)] -pub(crate) struct SubcommandGroup { - pub name: String, - pub description: String, - pub subcommands: Vec, -} - -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::>(); - - 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, - pub description: String, - pub group: String, - pub examples: Vec, - pub can_blacklist: bool, - pub supports_dm: bool, - pub cmd_args: Vec, - pub subcommands: Vec, - pub subcommand_groups: Vec, -} - -impl Options { - #[inline] - pub fn new() -> Self { - Self { group: "None".to_string(), ..Default::default() } - } -} diff --git a/command_attributes/src/util.rs b/command_attributes/src/util.rs deleted file mode 100644 index 0c01e73..0000000 --- a/command_attributes/src/util.rs +++ /dev/null @@ -1,176 +0,0 @@ -use proc_macro::TokenStream; -use proc_macro2::{Span, TokenStream as TokenStream2}; -use quote::{format_ident, quote, ToTokens}; -use syn::{ - braced, bracketed, parenthesized, - parse::{Error, Parse, ParseStream, Result as SynResult}, - punctuated::Punctuated, - token::{Comma, Mut}, - Ident, Lifetime, Lit, Type, -}; - -pub trait LitExt { - fn to_str(&self) -> String; - fn to_bool(&self) -> bool; - fn to_ident(&self) -> Ident; -} - -impl LitExt for Lit { - fn to_str(&self) -> String { - match self { - Lit::Str(s) => s.value(), - Lit::ByteStr(s) => unsafe { String::from_utf8_unchecked(s.value()) }, - Lit::Char(c) => c.value().to_string(), - Lit::Byte(b) => (b.value() as char).to_string(), - _ => panic!("values must be a (byte)string or a char"), - } - } - - fn to_bool(&self) -> bool { - if let Lit::Bool(b) = self { - b.value - } else { - self.to_str() - .parse() - .unwrap_or_else(|_| panic!("expected bool from {:?}", self)) - } - } - - #[inline] - fn to_ident(&self) -> Ident { - Ident::new(&self.to_str(), self.span()) - } -} - -pub trait IdentExt2: Sized { - fn to_uppercase(&self) -> Self; - fn with_suffix(&self, suf: &str) -> Ident; -} - -impl IdentExt2 for Ident { - #[inline] - fn to_uppercase(&self) -> Self { - format_ident!("{}", self.to_string().to_uppercase()) - } - - #[inline] - fn with_suffix(&self, suffix: &str) -> Ident { - format_ident!("{}_{}", self.to_string().to_uppercase(), suffix) - } -} - -#[inline] -pub fn into_stream(e: Error) -> TokenStream { - e.to_compile_error().into() -} - -macro_rules! propagate_err { - ($res:expr) => {{ - match $res { - Ok(v) => v, - Err(e) => return $crate::util::into_stream(e), - } - }}; -} - -#[derive(Debug)] -pub struct Bracketed(pub Punctuated); - -impl Parse for Bracketed { - fn parse(input: ParseStream<'_>) -> SynResult { - let content; - bracketed!(content in input); - - Ok(Bracketed(content.parse_terminated(T::parse)?)) - } -} - -#[derive(Debug)] -pub struct Braced(pub Punctuated); - -impl Parse for Braced { - fn parse(input: ParseStream<'_>) -> SynResult { - let content; - braced!(content in input); - - Ok(Braced(content.parse_terminated(T::parse)?)) - } -} - -#[derive(Debug)] -pub struct Parenthesised(pub Punctuated); - -impl Parse for Parenthesised { - fn parse(input: ParseStream<'_>) -> SynResult { - let content; - parenthesized!(content in input); - - Ok(Parenthesised(content.parse_terminated(T::parse)?)) - } -} - -#[derive(Debug)] -pub struct AsOption(pub Option); - -impl ToTokens for AsOption { - fn to_tokens(&self, stream: &mut TokenStream2) { - match &self.0 { - Some(o) => stream.extend(quote!(Some(#o))), - None => stream.extend(quote!(None)), - } - } -} - -impl Default for AsOption { - #[inline] - fn default() -> Self { - AsOption(None) - } -} - -#[derive(Debug)] -pub struct Argument { - pub mutable: Option, - pub name: Ident, - pub kind: Type, -} - -impl ToTokens for Argument { - fn to_tokens(&self, stream: &mut TokenStream2) { - let Argument { - mutable, - name, - kind, - } = self; - - stream.extend(quote! { - #mutable #name: #kind - }); - } -} - -#[inline] -pub fn populate_fut_lifetimes_on_refs(args: &mut Vec) { - for arg in args { - if let Type::Reference(reference) = &mut arg.kind { - reference.lifetime = Some(Lifetime::new("'fut", Span::call_site())); - } - } -} - -pub fn append_line(desc: &mut String, mut line: String) { - if line.starts_with(' ') { - line.remove(0); - } - - match line.rfind("\\$") { - Some(i) => { - desc.push_str(line[..i].trim_end()); - desc.push(' '); - } - None => { - desc.push_str(&line); - desc.push('\n'); - } - } -} diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 8ad997e..e53c6f3 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -1,4 +1,4 @@ pub mod info_cmds; pub mod moderation_cmds; -//pub mod reminder_cmds; -//pub mod todo_cmds; +pub mod reminder_cmds; +pub mod todo_cmds; diff --git a/src/commands/moderation_cmds.rs b/src/commands/moderation_cmds.rs index b554cc4..e1b6939 100644 --- a/src/commands/moderation_cmds.rs +++ b/src/commands/moderation_cmds.rs @@ -4,9 +4,13 @@ use levenshtein::levenshtein; use poise::CreateReply; use crate::{ + component_models::pager::{MacroPager, Pager}, consts::{EMBED_DESCRIPTION_MAX_LENGTH, THEME_COLOR}, hooks::guild_only, - models::{command_macro::CommandMacro, CtxData}, + models::{ + command_macro::{guild_command_macro, CommandMacro}, + CtxData, + }, Context, Data, Error, }; @@ -286,8 +290,7 @@ pub async fn finish_macro(ctx: Context<'_>) -> Result<(), Error> { /// List recorded macros #[poise::command(slash_command, rename = "list", check = "guild_only")] pub async fn list_macro(ctx: Context<'_>) -> Result<(), Error> { - // let macros = CommandMacro::from_guild(&ctx.data().database, ctx.guild_id().unwrap()).await; - let macros: Vec> = vec![]; + let macros = ctx.command_macros().await?; let resp = show_macro_page(¯os, 0); @@ -303,32 +306,31 @@ pub async fn list_macro(ctx: Context<'_>) -> Result<(), Error> { /// Run a recorded macro #[poise::command(slash_command, rename = "run", check = "guild_only")] pub async fn run_macro( - ctx: Context<'_>, + ctx: poise::ApplicationContext<'_, Data, Error>, #[description = "Name of macro to run"] #[autocomplete = "macro_name_autocomplete"] name: String, ) -> Result<(), Error> { - match sqlx::query!( - " -SELECT commands FROM macro WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?) AND name = ?", - ctx.guild_id().unwrap().0, - name - ) - .fetch_one(&ctx.data().database) - .await - { - Ok(row) => { - ctx.defer().await?; + match guild_command_macro(&Context::Application(ctx), &name).await { + Some(command_macro) => { + ctx.defer_response(false).await?; - // TODO TODO TODO!!!!!!!! RUN COMMAND FROM MACRO + for command in command_macro.commands { + if let Some(action) = command.action { + (action)(poise::ApplicationContext { args: &command.options, ..ctx }) + .await + .ok() + .unwrap(); + } else { + Context::Application(ctx) + .say(format!("Command \"{}\" failed to execute", command.command_name)) + .await?; + } + } } - Err(sqlx::Error::RowNotFound) => { - ctx.say(format!("Macro \"{}\" not found", name)).await?; - } - - Err(e) => { - panic!("{}", e); + None => { + Context::Application(ctx).say(format!("Macro \"{}\" not found", name)).await?; } } @@ -398,17 +400,6 @@ pub fn max_macro_page(macros: &[CommandMacro]) -> usize { } pub fn show_macro_page(macros: &[CommandMacro], page: usize) -> CreateReply { - let mut reply = CreateReply::default(); - - reply.embed(|e| { - e.title("Macros") - .description("No Macros Set Up. Use `/macro record` to get started.") - .color(*THEME_COLOR) - }); - - reply - - /* let pager = MacroPager::new(page); if macros.is_empty() { @@ -479,5 +470,4 @@ pub fn show_macro_page(macros: &[CommandMacro], page: usize) -> Crea }); reply - */ } diff --git a/src/commands/reminder_cmds.rs b/src/commands/reminder_cmds.rs index 8696c41..aca114d 100644 --- a/src/commands/reminder_cmds.rs +++ b/src/commands/reminder_cmds.rs @@ -7,17 +7,21 @@ use std::{ use chrono::NaiveDateTime; use chrono_tz::Tz; use num_integer::Integer; -use regex_command_attr::command; -use serenity::{builder::CreateEmbed, client::Context, model::channel::Channel}; +use poise::{ + serenity::{builder::CreateEmbed, model::channel::Channel}, + serenity_prelude::ActionRole::Create, + CreateReply, +}; use crate::{ component_models::{ pager::{DelPager, LookPager, Pager}, ComponentDataModel, DelSelector, }, - consts::{EMBED_DESCRIPTION_MAX_LENGTH, REGEX_CHANNEL_USER, SELECT_MAX_ENTRIES, THEME_COLOR}, - framework::{CommandInvoke, CommandOptions, CreateGenericResponse, OptionValue}, - hooks::CHECK_GUILD_PERMISSIONS_HOOK, + consts::{ + EMBED_DESCRIPTION_MAX_LENGTH, HOUR, MINUTE, REGEX_CHANNEL_USER, SELECT_MAX_ENTRIES, + THEME_COLOR, + }, interval_parser::parse_duration, models::{ reminder::{ @@ -33,29 +37,22 @@ use crate::{ }, time_parser::natural_parser, utils::{check_guild_subscription, check_subscription}, - SQLPool, + Context, Error, }; -#[command("pause")] -#[description("Pause all reminders on the current channel until a certain time or indefinitely")] -#[arg( - name = "until", - description = "When to pause until (hint: try 'next Wednesday', or '10 minutes')", - kind = "String", - required = false -)] -#[supports_dm(false)] -#[hook(CHECK_GUILD_PERMISSIONS_HOOK)] -async fn pause(ctx: &Context, invoke: &mut CommandInvoke, args: CommandOptions) { - let pool = ctx.data.read().await.get::().cloned().unwrap(); +/// Pause all reminders on the current channel until a certain time or indefinitely +#[poise::command(slash_command)] +pub async fn pause( + ctx: Context<'_>, + #[description = "When to pause until"] until: Option, +) -> Result<(), Error> { + let timezone = ctx.timezone().await; - let timezone = UserData::timezone_of(&invoke.author_id(), &pool).await; + let mut channel = ctx.channel_data().await.unwrap(); - let mut channel = ctx.channel_data(invoke.channel_id()).await.unwrap(); - - match args.get("until") { - Some(OptionValue::String(until)) => { - let parsed = natural_parser(until, &timezone.to_string()).await; + match until { + Some(until) => { + let parsed = natural_parser(&until, &timezone.to_string()).await; if let Some(timestamp) = parsed { let dt = NaiveDateTime::from_timestamp(timestamp, 0); @@ -63,92 +60,53 @@ async fn pause(ctx: &Context, invoke: &mut CommandInvoke, args: CommandOptions) channel.paused = true; channel.paused_until = Some(dt); - channel.commit_changes(&pool).await; + channel.commit_changes(&ctx.data().database).await; - let _ = invoke - .respond( - ctx.http.clone(), - CreateGenericResponse::new().content(format!( - "Reminders in this channel have been silenced until ****", - timestamp - )), - ) - .await; + ctx.say(format!( + "Reminders in this channel have been silenced until ****", + timestamp + )) + .await?; } else { - let _ = invoke - .respond( - ctx.http.clone(), - CreateGenericResponse::new() - .content("Time could not be processed. Please write the time as clearly as possible"), - ) - .await; + ctx.say( + "Time could not be processed. Please write the time as clearly as possible", + ) + .await?; } } _ => { channel.paused = !channel.paused; channel.paused_until = None; - channel.commit_changes(&pool).await; + channel.commit_changes(&ctx.data().database).await; if channel.paused { - let _ = invoke - .respond( - ctx.http.clone(), - CreateGenericResponse::new() - .content("Reminders in this channel have been silenced indefinitely"), - ) - .await; + ctx.say("Reminders in this channel have been silenced indefinitely").await?; } else { - let _ = invoke - .respond( - ctx.http.clone(), - CreateGenericResponse::new() - .content("Reminders in this channel have been unsilenced"), - ) - .await; + ctx.say("Reminders in this channel have been unsilenced").await?; } } } + + Ok(()) } -#[command("offset")] -#[description("Move all reminders in the current server by a certain amount of time. Times get added together")] -#[arg( - name = "hours", - description = "Number of hours to offset by", - kind = "Integer", - required = false -)] -#[arg( - name = "minutes", - description = "Number of minutes to offset by", - kind = "Integer", - required = false -)] -#[arg( - name = "seconds", - description = "Number of seconds to offset by", - kind = "Integer", - required = false -)] -#[hook(CHECK_GUILD_PERMISSIONS_HOOK)] -async fn offset(ctx: &Context, invoke: &mut CommandInvoke, args: CommandOptions) { - let pool = ctx.data.read().await.get::().cloned().unwrap(); - - let combined_time = args.get("hours").map_or(0, |h| h.as_i64().unwrap() * 3600) - + args.get("minutes").map_or(0, |m| m.as_i64().unwrap() * 60) - + args.get("seconds").map_or(0, |s| s.as_i64().unwrap()); +/// Move all reminders in the current server by a certain amount of time. Times get added together +#[poise::command(slash_command)] +pub async fn offset( + ctx: Context<'_>, + #[description = "Number of hours to offset by"] hours: Option, + #[description = "Number of minutes to offset by"] minutes: Option, + #[description = "Number of seconds to offset by"] seconds: Option, +) -> Result<(), Error> { + let combined_time = hours.map_or(0, |h| h * HOUR as isize) + + minutes.map_or(0, |m| m * MINUTE as isize) + + seconds.map_or(0, |s| s); if combined_time == 0 { - let _ = invoke - .respond( - ctx.http.clone(), - CreateGenericResponse::new() - .content("Please specify one of `hours`, `minutes` or `seconds`"), - ) - .await; + ctx.say("Please specify one of `hours`, `minutes` or `seconds`").await?; } else { - if let Some(guild) = invoke.guild(ctx.cache.clone()) { + if let Some(guild) = ctx.guild() { let channels = guild .channels .iter() @@ -167,110 +125,67 @@ INNER JOIN `channels` ON `channels`.id = reminders.channel_id SET reminders.`utc_time` = DATE_ADD(reminders.`utc_time`, INTERVAL ? SECOND) WHERE FIND_IN_SET(channels.`channel`, ?)", - combined_time, + combined_time as i64, channels ) - .execute(&pool) + .execute(&ctx.data().database) .await .unwrap(); } else { sqlx::query!( "UPDATE reminders INNER JOIN `channels` ON `channels`.id = reminders.channel_id SET reminders.`utc_time` = reminders.`utc_time` + ? WHERE channels.`channel` = ?", - combined_time, - invoke.channel_id().0 + combined_time as i64, + ctx.channel_id().0 ) - .execute(&pool) + .execute(&ctx.data().database) .await .unwrap(); } - let _ = invoke - .respond( - ctx.http.clone(), - CreateGenericResponse::new() - .content(format!("All reminders offset by {} seconds", combined_time)), - ) - .await; + ctx.say(format!("All reminders offset by {} seconds", combined_time)).await?; } + + Ok(()) } -#[command("nudge")] -#[description("Nudge all future reminders on this channel by a certain amount (don't use for DST! See `/offset`)")] -#[arg( - name = "minutes", - description = "Number of minutes to nudge new reminders by", - kind = "Integer", - required = false -)] -#[arg( - name = "seconds", - description = "Number of seconds to nudge new reminders by", - kind = "Integer", - required = false -)] -#[hook(CHECK_GUILD_PERMISSIONS_HOOK)] -async fn nudge(ctx: &Context, invoke: &mut CommandInvoke, args: CommandOptions) { - let pool = ctx.data.read().await.get::().cloned().unwrap(); +/// Nudge all future reminders on this channel by a certain amount (don't use for DST! See `/offset`) +#[poise::command(slash_command)] +pub async fn nudge( + ctx: Context<'_>, + #[description = "Number of minutes to nudge new reminders by"] minutes: Option, + #[description = "Number of seconds to nudge new reminders by"] seconds: Option, +) -> Result<(), Error> { + let combined_time = minutes.map_or(0, |m| m * MINUTE as isize) + seconds.map_or(0, |s| s); - let combined_time = args.get("minutes").map_or(0, |m| m.as_i64().unwrap() * 60) - + args.get("seconds").map_or(0, |s| s.as_i64().unwrap()); - - if combined_time < i16::MIN as i64 || combined_time > i16::MAX as i64 { - let _ = invoke - .respond( - ctx.http.clone(), - CreateGenericResponse::new().content("Nudge times must be less than 500 minutes"), - ) - .await; + if combined_time < i16::MIN as isize || combined_time > i16::MAX as isize { + ctx.say("Nudge times must be less than 500 minutes").await?; } else { - let mut channel_data = ctx.channel_data(invoke.channel_id()).await.unwrap(); + let mut channel_data = ctx.channel_data().await.unwrap(); channel_data.nudge = combined_time as i16; - channel_data.commit_changes(&pool).await; + channel_data.commit_changes(&ctx.data().database).await; - let _ = invoke - .respond( - ctx.http.clone(), - CreateGenericResponse::new().content(format!( - "Future reminders will be nudged by {} seconds", - combined_time - )), - ) - .await; + ctx.say(format!("Future reminders will be nudged by {} seconds", combined_time)).await?; } + + Ok(()) } -#[command("look")] -#[description("View reminders on a specific channel")] -#[arg( - name = "channel", - description = "The channel to view reminders on", - kind = "Channel", - required = false -)] -#[arg( - name = "disabled", - description = "Whether to show disabled reminders or not", - kind = "Boolean", - required = false -)] -#[arg( - name = "relative", - description = "Whether to display times as relative or exact times", - kind = "Boolean", - required = false -)] -#[hook(CHECK_GUILD_PERMISSIONS_HOOK)] -async fn look(ctx: &Context, invoke: &mut CommandInvoke, args: CommandOptions) { - let pool = ctx.data.read().await.get::().cloned().unwrap(); - - let timezone = UserData::timezone_of(&invoke.author_id(), &pool).await; +/// View reminders on a specific channel +#[poise::command(slash_command)] +pub async fn look( + ctx: Context<'_>, + #[description = "Channel to view reminders on"] channel: Option, + #[description = "Whether to show disabled reminders or not"] disabled: Option, + #[description = "Whether to display times as relative or exact times"] relative: Option, +) -> Result<(), Error> { + let timezone = ctx.timezone().await; let flags = LookFlags { - show_disabled: args.get("disabled").map(|i| i.as_bool()).flatten().unwrap_or(true), - channel_id: args.get("channel").map(|i| i.as_channel_id()).flatten(), - time_display: args.get("relative").map_or(TimeDisplayType::Relative, |b| { - if b.as_bool() == Some(true) { + show_disabled: disabled.unwrap_or(true), + channel_id: channel.map(|c| c.id()), + time_display: relative.map_or(TimeDisplayType::Relative, |b| { + if b { TimeDisplayType::Relative } else { TimeDisplayType::Absolute @@ -278,33 +193,29 @@ async fn look(ctx: &Context, invoke: &mut CommandInvoke, args: CommandOptions) { }), }; - let channel_opt = invoke.channel_id().to_channel_cached(&ctx); + let channel_opt = ctx.channel_id().to_channel_cached(&ctx.discord()); let channel_id = if let Some(Channel::Guild(channel)) = channel_opt { - if Some(channel.guild_id) == invoke.guild_id() { - flags.channel_id.unwrap_or_else(|| invoke.channel_id()) + if Some(channel.guild_id) == ctx.guild_id() { + flags.channel_id.unwrap_or_else(|| ctx.channel_id()) } else { - invoke.channel_id() + ctx.channel_id() } } else { - invoke.channel_id() + ctx.channel_id() }; - let channel_name = if let Some(Channel::Guild(channel)) = channel_id.to_channel_cached(&ctx) { - Some(channel.name) - } else { - None - }; + let channel_name = + if let Some(Channel::Guild(channel)) = channel_id.to_channel_cached(&ctx.discord()) { + Some(channel.name) + } else { + None + }; - let reminders = Reminder::from_channel(ctx, channel_id, &flags).await; + let reminders = Reminder::from_channel(&ctx, channel_id, &flags).await; if reminders.is_empty() { - let _ = invoke - .respond( - ctx.http.clone(), - CreateGenericResponse::new().content("No reminders on specified channel"), - ) - .await; + let _ = ctx.say("No reminders on specified channel").await; } else { let mut char_count = 0; @@ -327,41 +238,45 @@ async fn look(ctx: &Context, invoke: &mut CommandInvoke, args: CommandOptions) { let pager = LookPager::new(flags, timezone); - invoke - .respond( - ctx.http.clone(), - CreateGenericResponse::new() - .embed(|e| { - e.title(format!( - "Reminders{}", - channel_name.map_or(String::new(), |n| format!(" on #{}", n)) - )) - .description(display) - .footer(|f| f.text(format!("Page {} of {}", 1, pages))) - .color(*THEME_COLOR) - }) - .components(|comp| { - pager.create_button_row(pages, comp); + ctx.send(|r| { + r.ephemeral(true) + .embed(|e| { + e.title(format!( + "Reminders{}", + channel_name.map_or(String::new(), |n| format!(" on #{}", n)) + )) + .description(display) + .footer(|f| f.text(format!("Page {} of {}", 1, pages))) + .color(*THEME_COLOR) + }) + .components(|comp| { + pager.create_button_row(pages, comp); - comp - }), - ) - .await - .unwrap(); + comp + }) + }) + .await?; } + + Ok(()) } -#[command("del")] -#[description("Delete reminders")] -#[hook(CHECK_GUILD_PERMISSIONS_HOOK)] -async fn delete(ctx: &Context, invoke: &mut CommandInvoke, _args: CommandOptions) { - let timezone = ctx.timezone(invoke.author_id()).await; +/// Delete reminders +#[poise::command(slash_command, rename = "del")] +pub async fn delete(ctx: Context<'_>) -> Result<(), Error> { + let timezone = ctx.timezone().await; - let reminders = Reminder::from_guild(ctx, invoke.guild_id(), invoke.author_id()).await; + let reminders = Reminder::from_guild(&ctx, ctx.guild_id(), ctx.author().id).await; let resp = show_delete_page(&reminders, 0, timezone); - let _ = invoke.respond(&ctx, resp).await; + ctx.send(|r| { + *r = resp; + r + }) + .await?; + + Ok(()) } pub fn max_delete_page(reminders: &[Reminder], timezone: &Tz) -> usize { @@ -386,20 +301,20 @@ pub fn max_delete_page(reminders: &[Reminder], timezone: &Tz) -> usize { }) } -pub fn show_delete_page( - reminders: &[Reminder], - page: usize, - timezone: Tz, -) -> CreateGenericResponse { +pub fn show_delete_page(reminders: &[Reminder], page: usize, timezone: Tz) -> CreateReply { let pager = DelPager::new(page, timezone); if reminders.is_empty() { - return CreateGenericResponse::new() + let mut reply = CreateReply::default(); + + reply .embed(|e| e.title("Delete Reminders").description("No Reminders").color(*THEME_COLOR)) .components(|comp| { pager.create_button_row(0, comp); comp }); + + return reply; } let pages = max_delete_page(reminders, &timezone); @@ -448,7 +363,9 @@ pub fn show_delete_page( let del_selector = ComponentDataModel::DelSelector(DelSelector { page, timezone }); - CreateGenericResponse::new() + let mut reply = CreateReply::default(); + + reply .embed(|e| { e.title("Delete Reminders") .description(display) @@ -486,290 +403,206 @@ pub fn show_delete_page( }) }) }) + }); + + reply +} + +fn time_difference(start_time: NaiveDateTime) -> String { + let unix_time = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() as i64; + let now = NaiveDateTime::from_timestamp(unix_time, 0); + + let delta = (now - start_time).num_seconds(); + + let (minutes, seconds) = delta.div_rem(&60); + let (hours, minutes) = minutes.div_rem(&60); + let (days, hours) = hours.div_rem(&24); + + format!("{} days, {:02}:{:02}:{:02}", days, hours, minutes, seconds) +} + +/// Manage timers +#[poise::command(slash_command, rename = "timer")] +pub async fn timer_base(_ctx: Context<'_>) -> Result<(), Error> { + Ok(()) +} + +/// List the timers in this server or DM channel +#[poise::command(slash_command, rename = "list")] +pub async fn list_timer(ctx: Context<'_>) -> Result<(), Error> { + let owner = ctx.guild_id().map(|g| g.0).unwrap_or_else(|| ctx.author().id.0); + + let timers = Timer::from_owner(owner, &ctx.data().database).await; + + if !timers.is_empty() { + ctx.send(|m| { + m.embed(|e| { + e.fields(timers.iter().map(|timer| { + (&timer.name, format!("⌚ `{}`", time_difference(timer.start_time)), false) + })) + .color(*THEME_COLOR) + }) }) + .await?; + } else { + ctx.say("No timers currently. Use `/timer start` to create a new timer").await?; + } + + Ok(()) } -#[command("timer")] -#[description("Manage timers")] -#[subcommand("list")] -#[description("List the timers in this server or DM channel")] -#[subcommand("start")] -#[description("Start a new timer from now")] -#[arg(name = "name", description = "Name for the new timer", kind = "String", required = true)] -#[subcommand("delete")] -#[description("Delete a timer")] -#[arg(name = "name", description = "Name of the timer to delete", kind = "String", required = true)] -#[hook(CHECK_GUILD_PERMISSIONS_HOOK)] -async fn timer(ctx: &Context, invoke: &mut CommandInvoke, args: CommandOptions) { - fn time_difference(start_time: NaiveDateTime) -> String { - let unix_time = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() as i64; - let now = NaiveDateTime::from_timestamp(unix_time, 0); +/// Start a new timer from now +#[poise::command(slash_command, rename = "start")] +pub async fn start_timer( + ctx: Context<'_>, + #[description = "Name for the new timer"] name: String, +) -> Result<(), Error> { + let owner = ctx.guild_id().map(|g| g.0).unwrap_or_else(|| ctx.author().id.0); - let delta = (now - start_time).num_seconds(); + let count = Timer::count_from_owner(owner, &ctx.data().database).await; - let (minutes, seconds) = delta.div_rem(&60); - let (hours, minutes) = minutes.div_rem(&60); - let (days, hours) = hours.div_rem(&24); + if count >= 25 { + ctx.say("You already have 25 timers. Please delete some timers before creating a new one") + .await?; + } else { + if name.len() <= 32 { + Timer::create(&name, owner, &ctx.data().database).await; - format!("{} days, {:02}:{:02}:{:02}", days, hours, minutes, seconds) + ctx.say("Created a new timer").await?; + } else { + ctx.say(format!( + "Please name your timer something shorted (max. 32 characters, you used {})", + name.len() + )) + .await?; + } } - let pool = ctx.data.read().await.get::().cloned().unwrap(); - - let owner = invoke.guild_id().map(|g| g.0).unwrap_or_else(|| invoke.author_id().0); - - match args.subcommand.clone().unwrap().as_str() { - "start" => { - let count = Timer::count_from_owner(owner, &pool).await; - - if count >= 25 { - let _ = invoke - .respond( - ctx.http.clone(), - CreateGenericResponse::new() - .content("You already have 25 timers. Please delete some timers before creating a new one"), - ) - .await; - } else { - let name = args.get("name").unwrap().to_string(); - - if name.len() <= 32 { - Timer::create(&name, owner, &pool).await; - - let _ = invoke - .respond( - ctx.http.clone(), - CreateGenericResponse::new().content("Created a new timer"), - ) - .await; - } else { - let _ = invoke - .respond( - ctx.http.clone(), - CreateGenericResponse::new() - .content(format!("Please name your timer something shorted (max. 32 characters, you used {})", name.len())), - ) - .await; - } - } - } - "delete" => { - let name = args.get("name").unwrap().to_string(); - - let exists = sqlx::query!( - " -SELECT 1 as _r FROM timers WHERE owner = ? AND name = ? - ", - owner, - name - ) - .fetch_one(&pool) - .await; - - if exists.is_ok() { - sqlx::query!( - " -DELETE FROM timers WHERE owner = ? AND name = ? - ", - owner, - name - ) - .execute(&pool) - .await - .unwrap(); - - let _ = invoke - .respond( - ctx.http.clone(), - CreateGenericResponse::new().content("Deleted a timer"), - ) - .await; - } else { - let _ = invoke - .respond( - ctx.http.clone(), - CreateGenericResponse::new().content("Could not find a timer by that name"), - ) - .await; - } - } - "list" => { - let timers = Timer::from_owner(owner, &pool).await; - - if !timers.is_empty() { - let _ = invoke - .respond( - ctx.http.clone(), - CreateGenericResponse::new().embed(|e| { - e.fields(timers.iter().map(|timer| { - ( - &timer.name, - format!("⌚ `{}`", time_difference(timer.start_time)), - false, - ) - })) - .color(*THEME_COLOR) - }), - ) - .await; - } else { - let _ = invoke - .respond( - ctx.http.clone(), - CreateGenericResponse::new().content( - "No timers currently. Use `/timer start` to create a new timer", - ), - ) - .await; - } - } - _ => {} - } + Ok(()) } -#[command("remind")] -#[description("Create a new reminder")] -#[arg( - name = "time", - description = "A description of the time to set the reminder for", - kind = "String", - required = true -)] -#[arg( - name = "content", - description = "The message content to send", - kind = "String", - required = true -)] -#[arg( - name = "channels", - description = "Channel or user mentions to set the reminder for", - kind = "String", - required = false -)] -#[arg( - name = "interval", - description = "(Patreon only) Time to wait before repeating the reminder. Leave blank for one-shot reminder", - kind = "String", - required = false -)] -#[arg( - name = "expires", - description = "(Patreon only) For repeating reminders, the time at which the reminder will stop sending", - kind = "String", - required = false -)] -#[arg( - name = "tts", - description = "Set the TTS flag on the reminder message (like the /tts command)", - kind = "Boolean", - required = false -)] -#[hook(CHECK_GUILD_PERMISSIONS_HOOK)] -async fn remind(ctx: &Context, invoke: &mut CommandInvoke, args: CommandOptions) { - if args.get("interval").is_none() && args.get("expires").is_some() { - let _ = invoke - .respond( - &ctx, - CreateGenericResponse::new().content("`expires` can only be used with `interval`"), - ) +/// Delete a timer +#[poise::command(slash_command, rename = "delete")] +pub async fn delete_timer( + ctx: Context<'_>, + #[description = "Name of timer to delete"] name: String, +) -> Result<(), Error> { + let owner = ctx.guild_id().map(|g| g.0).unwrap_or_else(|| ctx.author().id.0); + + let exists = + sqlx::query!("SELECT 1 as _r FROM timers WHERE owner = ? AND name = ?", owner, name) + .fetch_one(&ctx.data().database) .await; - return; + if exists.is_ok() { + sqlx::query!("DELETE FROM timers WHERE owner = ? AND name = ?", owner, name) + .execute(&ctx.data().database) + .await + .unwrap(); + + ctx.say("Deleted a timer").await?; + } else { + ctx.say("Could not find a timer by that name").await?; } - invoke.defer(&ctx).await; + Ok(()) +} - let user_data = ctx.user_data(invoke.author_id()).await.unwrap(); - let timezone = user_data.timezone(); +/// Create a new reminder +#[poise::command(slash_command)] +pub(crate) async fn remind( + ctx: Context<'_>, + #[description = "A description of the time to set the reminder for"] time: String, + #[description = "The message content to send"] content: String, + #[description = "Channel or user mentions to set the reminder for"] channels: Option, + #[description = "(Patreon only) Time to wait before repeating the reminder. Leave blank for one-shot reminder"] + interval: Option, + #[description = "(Patreon only) For repeating reminders, the time at which the reminder will stop sending"] + expires: Option, + #[description = "Set the TTS flag on the reminder message, similar to the /tts command"] + tts: Option, +) -> Result<(), Error> { + if interval.is_none() && expires.is_some() { + ctx.say("`expires` can only be used with `interval`").await?; - let time = { - let time_str = args.get("time").unwrap().to_string(); + return Ok(()); + } - natural_parser(&time_str, &timezone.to_string()).await - }; + ctx.defer().await?; + + let user_data = ctx.author_data().await.unwrap(); + let timezone = ctx.timezone().await; + + let time = natural_parser(&time, &timezone.to_string()).await; match time { Some(time) => { let content = { - let content = args.get("content").unwrap().to_string(); - let tts = args.get("tts").map_or(false, |arg| arg.as_bool().unwrap_or(false)); + let tts = tts.unwrap_or(false); Content { content, tts, attachment: None, attachment_name: None } }; let scopes = { - let list = args - .get("channels") - .map(|arg| parse_mention_list(&arg.to_string())) - .unwrap_or_default(); + let list = + channels.map(|arg| parse_mention_list(&arg.to_string())).unwrap_or_default(); if list.is_empty() { - if invoke.guild_id().is_some() { - vec![ReminderScope::Channel(invoke.channel_id().0)] + if ctx.guild_id().is_some() { + vec![ReminderScope::Channel(ctx.channel_id().0)] } else { - vec![ReminderScope::User(invoke.author_id().0)] + vec![ReminderScope::User(ctx.author().id.0)] } } else { list } }; - let (interval, expires) = if let Some(repeat) = args.get("interval") { - if check_subscription(&ctx, invoke.author_id()).await - || (invoke.guild_id().is_some() - && check_guild_subscription(&ctx, invoke.guild_id().unwrap()).await) + let (processed_interval, processed_expires) = if let Some(repeat) = &interval { + if check_subscription(&ctx.discord(), ctx.author().id).await + || (ctx.guild_id().is_some() + && check_guild_subscription(&ctx.discord(), ctx.guild_id().unwrap()).await) { ( - parse_duration(&repeat.to_string()) + parse_duration(repeat) .or_else(|_| parse_duration(&format!("1 {}", repeat.to_string()))) .ok(), { - if let Some(arg) = args.get("expires") { - natural_parser(&arg.to_string(), &timezone.to_string()).await + if let Some(arg) = &expires { + natural_parser(arg, &timezone.to_string()).await } else { None } }, ) } else { - let _ = invoke - .respond(&ctx, CreateGenericResponse::new() - .content("`repeat` is only available to Patreon subscribers or self-hosted users") - ).await; + ctx.say( + "`repeat` is only available to Patreon subscribers or self-hosted users", + ) + .await?; - return; + return Ok(()); } } else { (None, None) }; - if interval.is_none() && args.get("interval").is_some() { - let _ = invoke - .respond( - &ctx, - CreateGenericResponse::new().content( - "Repeat interval could not be processed. Try and format the repetition similar to `1 hour` or `4 days`", - ), - ) - .await; - } else if expires.is_none() && args.get("expires").is_some() { - let _ = invoke - .respond( - &ctx, - CreateGenericResponse::new().content( - "Expiry time failed to process. Please make it as clear as possible", - ), - ) - .await; + if processed_interval.is_none() && interval.is_some() { + ctx.say( + "Repeat interval could not be processed. Try similar to `1 hour` or `4 days`", + ) + .await?; + } else if processed_expires.is_none() && expires.is_some() { + ctx.say("Expiry time failed to process. Please make it as clear as possible") + .await?; } else { - let mut builder = MultiReminderBuilder::new(ctx, invoke.guild_id()) + let mut builder = MultiReminderBuilder::new(&ctx, ctx.guild_id()) .author(user_data) .content(content) .time(time) .timezone(timezone) - .expires(expires) - .interval(interval); + .expires(processed_expires) + .interval(processed_interval); builder.set_scopes(scopes); @@ -777,23 +610,21 @@ async fn remind(ctx: &Context, invoke: &mut CommandInvoke, args: CommandOptions) let embed = create_response(successes, errors, time); - let _ = invoke - .respond( - &ctx, - CreateGenericResponse::new().embed(|c| { - *c = embed; - c - }), - ) - .await; + ctx.send(|m| { + m.embed(|c| { + *c = embed; + c + }) + }) + .await?; } } None => { - let _ = invoke - .respond(&ctx, CreateGenericResponse::new().content("Time could not be processed")) - .await; + ctx.say("Time could not be processed").await?; } } + + Ok(()) } fn create_response( diff --git a/src/commands/todo_cmds.rs b/src/commands/todo_cmds.rs index 9f004e1..e79113c 100644 --- a/src/commands/todo_cmds.rs +++ b/src/commands/todo_cmds.rs @@ -1,5 +1,4 @@ -use regex_command_attr::command; -use serenity::client::Context; +use poise::CreateReply; use crate::{ component_models::{ @@ -7,134 +6,177 @@ use crate::{ ComponentDataModel, TodoSelector, }, consts::{EMBED_DESCRIPTION_MAX_LENGTH, SELECT_MAX_ENTRIES, THEME_COLOR}, - framework::{CommandInvoke, CommandOptions, CreateGenericResponse}, - hooks::CHECK_GUILD_PERMISSIONS_HOOK, - SQLPool, + Context, Error, }; -#[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")] -#[hook(CHECK_GUILD_PERMISSIONS_HOOK)] -async fn todo(ctx: &Context, invoke: &mut CommandInvoke, args: CommandOptions) { - if invoke.guild_id().is_none() && args.subcommand_group != Some("user".to_string()) { - let _ = invoke - .respond( - &ctx, - CreateGenericResponse::new().content("Please use `/todo user` in direct messages"), - ) - .await; - } else { - let pool = ctx.data.read().await.get::().cloned().unwrap(); +/// Manage todo lists +#[poise::command(slash_command, rename = "todo")] +pub async fn todo_base(_ctx: Context<'_>) -> Result<(), Error> { + Ok(()) +} - let keys = match args.subcommand_group.as_ref().unwrap().as_str() { - "server" => (None, None, invoke.guild_id().map(|g| g.0)), - "channel" => (None, Some(invoke.channel_id().0), invoke.guild_id().map(|g| g.0)), - _ => (Some(invoke.author_id().0), None, None), - }; +/// Manage the server todo list +#[poise::command(slash_command, rename = "server")] +pub async fn todo_guild_base(_ctx: Context<'_>) -> Result<(), Error> { + Ok(()) +} - match args.get("task") { - Some(task) => { - let task = task.to_string(); +/// Add an item to the server todo list +#[poise::command(slash_command, rename = "add")] +pub async fn todo_guild_add( + ctx: Context<'_>, + #[description = "The task to add to the todo list"] task: String, +) -> Result<(), Error> { + sqlx::query!( + "INSERT INTO todos (guild_id, value) +VALUES ((SELECT id FROM guilds WHERE guild = ?), ?)", + ctx.guild_id().unwrap().0, + task + ) + .execute(&ctx.data().database) + .await + .unwrap(); - sqlx::query!( - "INSERT INTO todos (user_id, channel_id, guild_id, value) VALUES ((SELECT id FROM users WHERE user = ?), (SELECT id FROM channels WHERE channel = ?), (SELECT id FROM guilds WHERE guild = ?), ?)", - keys.0, - keys.1, - keys.2, - task - ) - .execute(&pool) - .await - .unwrap(); + ctx.say("Item added to todo list").await?; - let _ = invoke - .respond(&ctx, CreateGenericResponse::new().content("Item added to todo list")) - .await; - } - None => { - let values = if let Some(uid) = keys.0 { - sqlx::query!( - "SELECT todos.id, value FROM todos -INNER JOIN users ON todos.user_id = users.id -WHERE users.user = ?", - uid, - ) - .fetch_all(&pool) - .await - .unwrap() - .iter() - .map(|row| (row.id as usize, row.value.clone())) - .collect::>() - } else if let Some(cid) = keys.1 { - sqlx::query!( - "SELECT todos.id, value FROM todos -INNER JOIN channels ON todos.channel_id = channels.id -WHERE channels.channel = ?", - cid, - ) - .fetch_all(&pool) - .await - .unwrap() - .iter() - .map(|row| (row.id as usize, row.value.clone())) - .collect::>() - } else { - sqlx::query!( - "SELECT todos.id, value FROM todos + Ok(()) +} + +/// View and remove from the server todo list +#[poise::command(slash_command, rename = "view")] +pub async fn todo_guild_view(ctx: Context<'_>) -> Result<(), Error> { + let values = sqlx::query!( + "SELECT todos.id, value FROM todos INNER JOIN guilds ON todos.guild_id = guilds.id WHERE guilds.guild = ?", - keys.2, - ) - .fetch_all(&pool) - .await - .unwrap() - .iter() - .map(|row| (row.id as usize, row.value.clone())) - .collect::>() - }; + ctx.guild_id().unwrap().0, + ) + .fetch_all(&ctx.data().database) + .await + .unwrap() + .iter() + .map(|row| (row.id as usize, row.value.clone())) + .collect::>(); - let resp = show_todo_page(&values, 0, keys.0, keys.1, keys.2); + let resp = show_todo_page(&values, 0, None, None, ctx.guild_id().map(|g| g.0)); - invoke.respond(&ctx, resp).await.unwrap(); - } - } - } + ctx.send(|r| { + *r = resp; + r + }) + .await?; + + Ok(()) +} + +/// Manage the channel todo list +#[poise::command(slash_command, rename = "channel")] +pub async fn todo_channel_base(_ctx: Context<'_>) -> Result<(), Error> { + Ok(()) +} + +/// Add an item to the channel todo list +#[poise::command(slash_command, rename = "add")] +pub async fn todo_channel_add( + ctx: Context<'_>, + #[description = "The task to add to the todo list"] task: String, +) -> Result<(), Error> { + sqlx::query!( + "INSERT INTO todos (guild_id, channel_id, value) +VALUES ((SELECT id FROM guilds WHERE guild = ?), (SELECT id FROM channels WHERE channel = ?), ?)", + ctx.guild_id().unwrap().0, + ctx.channel_id().0, + task + ) + .execute(&ctx.data().database) + .await + .unwrap(); + + ctx.say("Item added to todo list").await?; + + Ok(()) +} + +/// View and remove from the channel todo list +#[poise::command(slash_command, rename = "view")] +pub async fn todo_channel_view(ctx: Context<'_>) -> Result<(), Error> { + let values = sqlx::query!( + "SELECT todos.id, value FROM todos +INNER JOIN channels ON todos.channel_id = channels.id +WHERE channels.channel = ?", + ctx.channel_id().0, + ) + .fetch_all(&ctx.data().database) + .await + .unwrap() + .iter() + .map(|row| (row.id as usize, row.value.clone())) + .collect::>(); + + let resp = + show_todo_page(&values, 0, None, Some(ctx.channel_id().0), ctx.guild_id().map(|g| g.0)); + + ctx.send(|r| { + *r = resp; + r + }) + .await?; + + Ok(()) +} + +/// Manage your personal todo list +#[poise::command(slash_command, rename = "user")] +pub async fn todo_user_base(_ctx: Context<'_>) -> Result<(), Error> { + Ok(()) +} + +/// Add an item to your personal todo list +#[poise::command(slash_command, rename = "add")] +pub async fn todo_user_add( + ctx: Context<'_>, + #[description = "The task to add to the todo list"] task: String, +) -> Result<(), Error> { + sqlx::query!( + "INSERT INTO todos (user_id, value) +VALUES ((SELECT id FROM users WHERE user = ?), ?)", + ctx.author().id.0, + task + ) + .execute(&ctx.data().database) + .await + .unwrap(); + + ctx.say("Item added to todo list").await?; + + Ok(()) +} + +/// View and remove from your personal todo list +#[poise::command(slash_command, rename = "view")] +pub async fn todo_user_view(ctx: Context<'_>) -> Result<(), Error> { + let values = sqlx::query!( + "SELECT todos.id, value FROM todos +INNER JOIN users ON todos.user_id = users.id +WHERE users.user = ?", + ctx.author().id.0, + ) + .fetch_all(&ctx.data().database) + .await + .unwrap() + .iter() + .map(|row| (row.id as usize, row.value.clone())) + .collect::>(); + + let resp = show_todo_page(&values, 0, Some(ctx.author().id.0), None, None); + + ctx.send(|r| { + *r = resp; + r + }) + .await?; + + Ok(()) } pub fn max_todo_page(todo_values: &[(usize, String)]) -> usize { @@ -164,7 +206,7 @@ pub fn show_todo_page( user_id: Option, channel_id: Option, guild_id: Option, -) -> CreateGenericResponse { +) -> CreateReply { let pager = TodoPager::new(page, user_id, channel_id, guild_id); let pages = max_todo_page(todo_values); @@ -219,17 +261,23 @@ pub fn show_todo_page( }; if todo_ids.is_empty() { - CreateGenericResponse::new().embed(|e| { + let mut reply = CreateReply::default(); + + reply.embed(|e| { e.title(format!("{} Todo List", title)) .description("Todo List Empty!") .footer(|f| f.text(format!("Page {} of {}", page + 1, pages))) .color(*THEME_COLOR) - }) + }); + + reply } else { let todo_selector = ComponentDataModel::TodoSelector(TodoSelector { page, user_id, channel_id, guild_id }); - CreateGenericResponse::new() + let mut reply = CreateReply::default(); + + reply .embed(|e| { e.title(format!("{} Todo List", title)) .description(display) @@ -255,6 +303,8 @@ pub fn show_todo_page( }) }) }) - }) + }); + + reply } } diff --git a/src/component_models/mod.rs b/src/component_models/mod.rs index bf16b2b..82fc879 100644 --- a/src/component_models/mod.rs +++ b/src/component_models/mod.rs @@ -3,17 +3,16 @@ pub(crate) mod pager; use std::io::Cursor; use chrono_tz::Tz; -use rmp_serde::Serializer; -use serde::{Deserialize, Serialize}; -use serenity::{ +use poise::serenity::{ builder::CreateEmbed, - client::Context, model::{ channel::Channel, interactions::{message_component::MessageComponentInteraction, InteractionResponseType}, prelude::InteractionApplicationCommandCallbackDataFlags, }, }; +use rmp_serde::Serializer; +use serde::{Deserialize, Serialize}; use crate::{ commands::{ @@ -23,9 +22,8 @@ use crate::{ }, component_models::pager::{DelPager, LookPager, MacroPager, Pager, TodoPager}, consts::{EMBED_DESCRIPTION_MAX_LENGTH, THEME_COLOR}, - framework::CommandInvoke, - models::{command_macro::CommandMacro, reminder::Reminder}, - SQLPool, + models::{reminder::Reminder, CtxData}, + Context, Data, }; #[derive(Deserialize, Serialize)] @@ -55,12 +53,12 @@ impl ComponentDataModel { rmp_serde::from_read(cur).unwrap() } - pub async fn act(&self, ctx: &Context, component: MessageComponentInteraction) { + pub async fn act(&self, ctx: Context<'_>, component: &MessageComponentInteraction) { match self { ComponentDataModel::LookPager(pager) => { let flags = pager.flags; - let channel_opt = component.channel_id.to_channel_cached(&ctx); + let channel_opt = component.channel_id.to_channel_cached(&ctx.discord()); let channel_id = if let Some(Channel::Guild(channel)) = channel_opt { if Some(channel.guild_id) == component.guild_id { @@ -72,7 +70,7 @@ impl ComponentDataModel { component.channel_id }; - let reminders = Reminder::from_channel(ctx, channel_id, &flags).await; + let reminders = Reminder::from_channel(&ctx, channel_id, &flags).await; let pages = reminders .iter() @@ -80,12 +78,13 @@ impl ComponentDataModel { .fold(0, |t, r| t + r.len()) .div_ceil(EMBED_DESCRIPTION_MAX_LENGTH); - let channel_name = - if let Some(Channel::Guild(channel)) = channel_id.to_channel_cached(&ctx) { - Some(channel.name) - } else { - None - }; + let channel_name = if let Some(Channel::Guild(channel)) = + channel_id.to_channel_cached(&ctx.discord()) + { + Some(channel.name) + } else { + None + }; let next_page = pager.next_page(pages); @@ -119,7 +118,7 @@ impl ComponentDataModel { .color(*THEME_COLOR); let _ = component - .create_interaction_response(&ctx, |r| { + .create_interaction_response(&ctx.discord(), |r| { r.kind(InteractionResponseType::UpdateMessage).interaction_response_data( |response| { response.embeds(vec![embed]).components(|comp| { @@ -134,44 +133,49 @@ impl ComponentDataModel { } ComponentDataModel::DelPager(pager) => { let reminders = - Reminder::from_guild(ctx, component.guild_id, component.user.id).await; + Reminder::from_guild(&ctx, component.guild_id, component.user.id).await; let max_pages = max_delete_page(&reminders, &pager.timezone); let resp = show_delete_page(&reminders, pager.next_page(max_pages), pager.timezone); - let mut invoke = CommandInvoke::component(component); - let _ = invoke.respond(&ctx, resp).await; + let _ = ctx + .send(|r| { + *r = resp; + r + }) + .await; } ComponentDataModel::DelSelector(selector) => { - let pool = ctx.data.read().await.get::().cloned().unwrap(); let selected_id = component.data.values.join(","); sqlx::query!("DELETE FROM reminders WHERE FIND_IN_SET(id, ?)", selected_id) - .execute(&pool) + .execute(&ctx.data().database) .await .unwrap(); let reminders = - Reminder::from_guild(ctx, component.guild_id, component.user.id).await; + Reminder::from_guild(&ctx, component.guild_id, component.user.id).await; let resp = show_delete_page(&reminders, selector.page, selector.timezone); - let mut invoke = CommandInvoke::component(component); - let _ = invoke.respond(&ctx, resp).await; + let _ = ctx + .send(|r| { + *r = resp; + r + }) + .await; } ComponentDataModel::TodoPager(pager) => { if Some(component.user.id.0) == pager.user_id || pager.user_id.is_none() { - let pool = ctx.data.read().await.get::().cloned().unwrap(); - let values = if let Some(uid) = pager.user_id { sqlx::query!( "SELECT todos.id, value FROM todos - INNER JOIN users ON todos.user_id = users.id - WHERE users.user = ?", +INNER JOIN users ON todos.user_id = users.id +WHERE users.user = ?", uid, ) - .fetch_all(&pool) + .fetch_all(&ctx.data().database) .await .unwrap() .iter() @@ -180,11 +184,11 @@ impl ComponentDataModel { } else if let Some(cid) = pager.channel_id { sqlx::query!( "SELECT todos.id, value FROM todos - INNER JOIN channels ON todos.channel_id = channels.id - WHERE channels.channel = ?", +INNER JOIN channels ON todos.channel_id = channels.id +WHERE channels.channel = ?", cid, ) - .fetch_all(&pool) + .fetch_all(&ctx.data().database) .await .unwrap() .iter() @@ -193,11 +197,11 @@ impl ComponentDataModel { } else { sqlx::query!( "SELECT todos.id, value FROM todos - INNER JOIN guilds ON todos.guild_id = guilds.id - WHERE guilds.guild = ?", +INNER JOIN guilds ON todos.guild_id = guilds.id +WHERE guilds.guild = ?", pager.guild_id, ) - .fetch_all(&pool) + .fetch_all(&ctx.data().database) .await .unwrap() .iter() @@ -215,11 +219,15 @@ impl ComponentDataModel { pager.guild_id, ); - let mut invoke = CommandInvoke::component(component); - let _ = invoke.respond(&ctx, resp).await; + let _ = ctx + .send(|r| { + *r = resp; + r + }) + .await; } else { let _ = component - .create_interaction_response(&ctx, |r| { + .create_interaction_response(&ctx.discord(), |r| { r.kind(InteractionResponseType::ChannelMessageWithSource) .interaction_response_data(|d| { d.flags( @@ -233,11 +241,10 @@ impl ComponentDataModel { } ComponentDataModel::TodoSelector(selector) => { if Some(component.user.id.0) == selector.user_id || selector.user_id.is_none() { - let pool = ctx.data.read().await.get::().cloned().unwrap(); let selected_id = component.data.values.join(","); sqlx::query!("DELETE FROM todos WHERE FIND_IN_SET(id, ?)", selected_id) - .execute(&pool) + .execute(&ctx.data().database) .await .unwrap(); @@ -248,7 +255,7 @@ impl ComponentDataModel { selector.channel_id, selector.guild_id, ) - .fetch_all(&pool) + .fetch_all(&ctx.data().database) .await .unwrap() .iter() @@ -263,11 +270,15 @@ impl ComponentDataModel { selector.guild_id, ); - let mut invoke = CommandInvoke::component(component); - let _ = invoke.respond(&ctx, resp).await; + let _ = ctx + .send(|r| { + *r = resp; + r + }) + .await; } else { let _ = component - .create_interaction_response(&ctx, |r| { + .create_interaction_response(&ctx.discord(), |r| { r.kind(InteractionResponseType::ChannelMessageWithSource) .interaction_response_data(|d| { d.flags( @@ -280,15 +291,19 @@ impl ComponentDataModel { } } ComponentDataModel::MacroPager(pager) => { - let mut invoke = CommandInvoke::component(component); - - let macros = CommandMacro::from_guild(ctx, invoke.guild_id().unwrap()).await; + let macros = ctx.command_macros().await.unwrap(); let max_page = max_macro_page(¯os); let page = pager.next_page(max_page); let resp = show_macro_page(¯os, page); - let _ = invoke.respond(&ctx, resp).await; + + let _ = ctx + .send(|r| { + *r = resp; + r + }) + .await; } } } diff --git a/src/component_models/pager.rs b/src/component_models/pager.rs index 8ab83e2..e82ff58 100644 --- a/src/component_models/pager.rs +++ b/src/component_models/pager.rs @@ -1,8 +1,10 @@ // todo split pager out into a single struct use chrono_tz::Tz; +use poise::serenity::{ + builder::CreateComponents, model::interactions::message_component::ButtonStyle, +}; use serde::{Deserialize, Serialize}; use serde_repr::*; -use serenity::{builder::CreateComponents, model::interactions::message_component::ButtonStyle}; use crate::{component_models::ComponentDataModel, models::reminder::look_flags::LookFlags}; diff --git a/src/consts.rs b/src/consts.rs index f1c47ba..8df74b9 100644 --- a/src/consts.rs +++ b/src/consts.rs @@ -1,4 +1,6 @@ pub const DAY: u64 = 86_400; +pub const HOUR: u64 = 3_600; +pub const MINUTE: u64 = 60; pub const EMBED_DESCRIPTION_MAX_LENGTH: usize = 4000; pub const SELECT_MAX_ENTRIES: usize = 25; diff --git a/src/event_handlers.rs b/src/event_handlers.rs index dfcf1c2..bc7e014 100644 --- a/src/event_handlers.rs +++ b/src/event_handlers.rs @@ -1,11 +1,27 @@ -use std::{collections::HashMap, env, sync::atomic::Ordering}; +use std::{ + collections::HashMap, + env, + sync::atomic::{AtomicBool, Ordering}, +}; use log::{info, warn}; -use poise::serenity::{client::Context, model::interactions::Interaction, utils::shard_id}; +use poise::{ + serenity::{model::interactions::Interaction, utils::shard_id}, + serenity_prelude as serenity, + serenity_prelude::{ + ApplicationCommandInteraction, ApplicationCommandInteractionData, ApplicationCommandType, + InteractionType, + }, + ApplicationCommandOrAutocompleteInteraction, ApplicationContext, Command, +}; -use crate::{Data, Error}; +use crate::{component_models::ComponentDataModel, Context, Data, Error}; -pub async fn listener(ctx: &Context, event: &poise::Event<'_>, data: &Data) -> Result<(), Error> { +pub async fn listener( + ctx: &serenity::Context, + event: &poise::Event<'_>, + data: &Data, +) -> Result<(), Error> { match event { poise::Event::CacheReady { .. } => { info!("Cache Ready!"); @@ -97,15 +113,16 @@ DELETE FROM channels WHERE channel = ? } } } - poise::Event::GuildDelete { incomplete, full } => { + poise::Event::GuildDelete { incomplete, .. } => { let _ = sqlx::query!("DELETE FROM guilds WHERE guild = ?", incomplete.id.0) .execute(&data.database) .await; } poise::Event::InteractionCreate { interaction } => match interaction { Interaction::MessageComponent(component) => { - //let component_model = ComponentDataModel::from_custom_id(&component.data.custom_id); - //component_model.act(&ctx, component).await; + let component_model = ComponentDataModel::from_custom_id(&component.data.custom_id); + + // component_model.act(ctx, component).await; } _ => {} }, diff --git a/src/framework.rs b/src/framework.rs deleted file mode 100644 index aaf9a28..0000000 --- a/src/framework.rs +++ /dev/null @@ -1,692 +0,0 @@ -// todo move framework to its own module, split out permission checks - -use std::{ - collections::{HashMap, HashSet}, - hash::{Hash, Hasher}, - sync::Arc, -}; - -use log::info; -use serde::{Deserialize, Serialize}; -use serenity::{ - builder::{CreateApplicationCommands, CreateComponents, CreateEmbed}, - cache::Cache, - client::Context, - futures::prelude::future::BoxFuture, - http::Http, - model::{ - guild::Guild, - id::{ChannelId, GuildId, RoleId, UserId}, - interactions::{ - application_command::{ - ApplicationCommand, ApplicationCommandInteraction, ApplicationCommandOptionType, - }, - message_component::MessageComponentInteraction, - InteractionApplicationCommandCallbackDataFlags, InteractionResponseType, - }, - prelude::application_command::ApplicationCommandInteractionDataOption, - }, - prelude::TypeMapKey, - Result as SerenityResult, -}; - -use crate::SQLPool; - -pub struct CreateGenericResponse { - content: String, - embed: Option, - components: Option, - flags: InteractionApplicationCommandCallbackDataFlags, -} - -impl CreateGenericResponse { - pub fn new() -> Self { - Self { - content: "".to_string(), - embed: None, - components: None, - flags: InteractionApplicationCommandCallbackDataFlags::empty(), - } - } - - pub fn ephemeral(mut self) -> Self { - self.flags.insert(InteractionApplicationCommandCallbackDataFlags::EPHEMERAL); - - self - } - - pub fn content(mut self, content: D) -> Self { - self.content = content.to_string(); - - self - } - - pub fn embed &mut CreateEmbed>(mut self, f: F) -> Self { - let mut embed = CreateEmbed::default(); - f(&mut embed); - - self.embed = Some(embed); - self - } - - pub fn components &mut CreateComponents>( - mut self, - f: F, - ) -> Self { - let mut components = CreateComponents::default(); - f(&mut components); - - self.components = Some(components); - self - } -} - -#[derive(Clone)] -enum InvokeModel { - Slash(ApplicationCommandInteraction), - Component(MessageComponentInteraction), -} - -#[derive(Clone)] -pub struct CommandInvoke { - model: InvokeModel, - already_responded: bool, - deferred: bool, -} - -impl CommandInvoke { - pub fn component(component: MessageComponentInteraction) -> Self { - Self { model: InvokeModel::Component(component), already_responded: false, deferred: false } - } - - fn slash(interaction: ApplicationCommandInteraction) -> Self { - Self { model: InvokeModel::Slash(interaction), already_responded: false, deferred: false } - } - - pub async fn defer(&mut self, http: impl AsRef) { - if !self.deferred { - match &self.model { - InvokeModel::Slash(i) => { - i.create_interaction_response(http, |r| { - r.kind(InteractionResponseType::DeferredChannelMessageWithSource) - }) - .await - .unwrap(); - - self.deferred = true; - } - InvokeModel::Component(i) => { - i.create_interaction_response(http, |r| { - r.kind(InteractionResponseType::DeferredChannelMessageWithSource) - }) - .await - .unwrap(); - - self.deferred = true; - } - } - } - } - - pub fn channel_id(&self) -> ChannelId { - match &self.model { - InvokeModel::Slash(i) => i.channel_id, - InvokeModel::Component(i) => i.channel_id, - } - } - - pub fn guild_id(&self) -> Option { - match &self.model { - InvokeModel::Slash(i) => i.guild_id, - InvokeModel::Component(i) => i.guild_id, - } - } - - pub fn guild(&self, cache: impl AsRef) -> Option { - self.guild_id().map(|id| id.to_guild_cached(cache)).flatten() - } - - pub fn author_id(&self) -> UserId { - match &self.model { - InvokeModel::Slash(i) => i.user.id, - InvokeModel::Component(i) => i.user.id, - } - } - - pub async fn respond( - &mut self, - http: impl AsRef, - generic_response: CreateGenericResponse, - ) -> SerenityResult<()> { - match &self.model { - InvokeModel::Slash(i) => { - if self.already_responded { - i.create_followup_message(http, |d| { - d.allowed_mentions(|m| m.empty_parse()); - d.content(generic_response.content); - - if let Some(embed) = generic_response.embed { - d.add_embed(embed); - } - - if let Some(components) = generic_response.components { - d.components(|c| { - *c = components; - c - }); - } - - d - }) - .await - .map(|_| ()) - } else if self.deferred { - i.edit_original_interaction_response(http, |d| { - d.allowed_mentions(|m| m.empty_parse()); - d.content(generic_response.content); - - if let Some(embed) = generic_response.embed { - d.add_embed(embed); - } - - if let Some(components) = generic_response.components { - d.components(|c| { - *c = components; - c - }); - } - - d - }) - .await - .map(|_| ()) - } else { - i.create_interaction_response(http, |r| { - r.kind(InteractionResponseType::ChannelMessageWithSource) - .interaction_response_data(|d| { - d.allowed_mentions(|m| m.empty_parse()); - d.content(generic_response.content); - - if let Some(embed) = generic_response.embed { - d.add_embed(embed); - } - - if let Some(components) = generic_response.components { - d.components(|c| { - *c = components; - c - }); - } - - d - }) - }) - .await - .map(|_| ()) - } - } - InvokeModel::Component(i) => i - .create_interaction_response(http, |r| { - r.kind(InteractionResponseType::UpdateMessage).interaction_response_data(|d| { - d.allowed_mentions(|m| m.empty_parse()); - d.content(generic_response.content); - - if let Some(embed) = generic_response.embed { - d.add_embed(embed); - } - - if let Some(components) = generic_response.components { - d.components(|c| { - *c = components; - c - }); - } - - d - }) - }) - .await - .map(|_| ()), - }?; - - self.already_responded = true; - - Ok(()) - } -} - -#[derive(Debug)] -pub struct Arg { - pub name: &'static str, - pub description: &'static str, - pub kind: ApplicationCommandOptionType, - pub required: bool, - pub options: &'static [&'static Self], -} - -#[derive(Serialize, Deserialize, Clone)] -pub enum OptionValue { - String(String), - Integer(i64), - Boolean(bool), - User(UserId), - Channel(ChannelId), - Role(RoleId), - Mentionable(u64), - Number(f64), -} - -impl OptionValue { - pub fn as_i64(&self) -> Option { - match self { - OptionValue::Integer(i) => Some(*i), - _ => None, - } - } - - pub fn as_bool(&self) -> Option { - match self { - OptionValue::Boolean(b) => Some(*b), - _ => None, - } - } - - pub fn as_channel_id(&self) -> Option { - match self { - OptionValue::Channel(c) => Some(*c), - _ => None, - } - } - - pub fn to_string(&self) -> String { - match self { - OptionValue::String(s) => s.to_string(), - OptionValue::Integer(i) => i.to_string(), - OptionValue::Boolean(b) => b.to_string(), - OptionValue::User(u) => u.to_string(), - OptionValue::Channel(c) => c.to_string(), - OptionValue::Role(r) => r.to_string(), - OptionValue::Mentionable(m) => m.to_string(), - OptionValue::Number(n) => n.to_string(), - } - } -} - -#[derive(Serialize, Deserialize, Clone)] -pub struct CommandOptions { - pub command: String, - pub subcommand: Option, - pub subcommand_group: Option, - pub options: HashMap, -} - -impl CommandOptions { - pub fn get(&self, key: &str) -> Option<&OptionValue> { - self.options.get(key) - } -} - -impl CommandOptions { - fn new(command: &'static Command) -> Self { - Self { - command: command.names[0].to_string(), - subcommand: None, - subcommand_group: None, - options: Default::default(), - } - } - - fn populate(mut self, interaction: &ApplicationCommandInteraction) -> Self { - fn match_option( - option: ApplicationCommandInteractionDataOption, - cmd_opts: &mut CommandOptions, - ) { - match option.kind { - ApplicationCommandOptionType::SubCommand => { - cmd_opts.subcommand = Some(option.name); - - for opt in option.options { - match_option(opt, cmd_opts); - } - } - ApplicationCommandOptionType::SubCommandGroup => { - cmd_opts.subcommand_group = Some(option.name); - - for opt in option.options { - match_option(opt, cmd_opts); - } - } - ApplicationCommandOptionType::String => { - cmd_opts.options.insert( - option.name, - OptionValue::String(option.value.unwrap().as_str().unwrap().to_string()), - ); - } - ApplicationCommandOptionType::Integer => { - cmd_opts.options.insert( - option.name, - OptionValue::Integer(option.value.map(|m| m.as_i64()).flatten().unwrap()), - ); - } - ApplicationCommandOptionType::Boolean => { - cmd_opts.options.insert( - option.name, - OptionValue::Boolean(option.value.map(|m| m.as_bool()).flatten().unwrap()), - ); - } - ApplicationCommandOptionType::User => { - cmd_opts.options.insert( - option.name, - OptionValue::User(UserId( - option - .value - .map(|m| m.as_str().map(|s| s.parse::().ok())) - .flatten() - .flatten() - .unwrap(), - )), - ); - } - ApplicationCommandOptionType::Channel => { - cmd_opts.options.insert( - option.name, - OptionValue::Channel(ChannelId( - option - .value - .map(|m| m.as_str().map(|s| s.parse::().ok())) - .flatten() - .flatten() - .unwrap(), - )), - ); - } - ApplicationCommandOptionType::Role => { - cmd_opts.options.insert( - option.name, - OptionValue::Role(RoleId( - option - .value - .map(|m| m.as_str().map(|s| s.parse::().ok())) - .flatten() - .flatten() - .unwrap(), - )), - ); - } - ApplicationCommandOptionType::Mentionable => { - cmd_opts.options.insert( - option.name, - OptionValue::Mentionable( - option.value.map(|m| m.as_u64()).flatten().unwrap(), - ), - ); - } - ApplicationCommandOptionType::Number => { - cmd_opts.options.insert( - option.name, - OptionValue::Number(option.value.map(|m| m.as_f64()).flatten().unwrap()), - ); - } - _ => {} - } - } - - for option in &interaction.data.options { - match_option(option.clone(), &mut self) - } - - self - } -} - -pub enum HookResult { - Continue, - Halt, -} - -type SlashCommandFn = - for<'fut> fn(&'fut Context, &'fut mut CommandInvoke, CommandOptions) -> BoxFuture<'fut, ()>; - -type MultiCommandFn = for<'fut> fn(&'fut Context, &'fut mut CommandInvoke) -> BoxFuture<'fut, ()>; - -pub type HookFn = for<'fut> fn( - &'fut Context, - &'fut mut CommandInvoke, - &'fut CommandOptions, -) -> BoxFuture<'fut, HookResult>; - -pub enum CommandFnType { - Slash(SlashCommandFn), - Multi(MultiCommandFn), -} - -pub struct Hook { - pub fun: HookFn, - pub uuid: u128, -} - -impl PartialEq for Hook { - fn eq(&self, other: &Self) -> bool { - self.uuid == other.uuid - } -} - -pub struct Command { - pub fun: CommandFnType, - - pub names: &'static [&'static str], - - pub desc: &'static str, - pub examples: &'static [&'static str], - pub group: &'static str, - - pub args: &'static [&'static Arg], - - pub can_blacklist: bool, - pub supports_dm: bool, - - pub hooks: &'static [&'static Hook], -} - -impl Hash for Command { - fn hash(&self, state: &mut H) { - self.names[0].hash(state) - } -} - -impl PartialEq for Command { - fn eq(&self, other: &Self) -> bool { - self.names[0] == other.names[0] - } -} - -impl Eq for Command {} - -pub struct RegexFramework { - pub commands_map: HashMap, - pub commands: HashSet<&'static Command>, - ignore_bots: bool, - dm_enabled: bool, - debug_guild: Option, - hooks: Vec<&'static Hook>, -} - -impl TypeMapKey for RegexFramework { - type Value = Arc; -} - -impl RegexFramework { - pub fn new() -> Self { - Self { - commands_map: HashMap::new(), - commands: HashSet::new(), - ignore_bots: true, - dm_enabled: true, - debug_guild: None, - hooks: vec![], - } - } - - pub fn ignore_bots(mut self, ignore_bots: bool) -> Self { - self.ignore_bots = ignore_bots; - - self - } - - pub fn dm_enabled(mut self, dm_enabled: bool) -> Self { - self.dm_enabled = dm_enabled; - - self - } - - pub fn add_hook(mut self, fun: &'static Hook) -> Self { - self.hooks.push(fun); - - self - } - - pub fn add_command(mut self, command: &'static Command) -> Self { - self.commands.insert(command); - - for name in command.names { - self.commands_map.insert(name.to_string(), command); - } - - self - } - - pub fn debug_guild(mut self, guild_id: Option) -> Self { - self.debug_guild = guild_id; - - self - } - - fn _populate_commands<'a>( - &self, - commands: &'a mut CreateApplicationCommands, - ) -> &'a mut CreateApplicationCommands { - for command in &self.commands { - commands.create_application_command(|c| { - c.name(command.names[0]).description(command.desc); - - for arg in command.args { - c.create_option(|o| { - o.name(arg.name) - .description(arg.description) - .kind(arg.kind) - .required(arg.required); - - for option in arg.options { - o.create_sub_option(|s| { - s.name(option.name) - .description(option.description) - .kind(option.kind) - .required(option.required); - - for sub_option in option.options { - s.create_sub_option(|ss| { - ss.name(sub_option.name) - .description(sub_option.description) - .kind(sub_option.kind) - .required(sub_option.required) - }); - } - - s - }); - } - - o - }); - } - - c - }); - } - - commands - } - - pub async fn build_slash(&self, http: impl AsRef) { - info!("Building slash commands..."); - - match self.debug_guild { - None => { - ApplicationCommand::set_global_application_commands(&http, |c| { - self._populate_commands(c) - }) - .await - .unwrap(); - } - Some(debug_guild) => { - debug_guild - .set_application_commands(&http, |c| self._populate_commands(c)) - .await - .unwrap(); - } - } - - info!("Slash commands built!"); - } - - pub async fn execute(&self, ctx: Context, interaction: ApplicationCommandInteraction) { - { - if let Some(guild_id) = interaction.guild_id { - let pool = ctx.data.read().await.get::().cloned().unwrap(); - let _ = sqlx::query!("INSERT IGNORE INTO guilds (guild) VALUES (?)", guild_id.0) - .execute(&pool) - .await; - } - } - - let command = { - self.commands_map - .get(&interaction.data.name) - .expect(&format!("Received invalid command: {}", interaction.data.name)) - }; - - let args = CommandOptions::new(command).populate(&interaction); - let mut command_invoke = CommandInvoke::slash(interaction); - - for hook in command.hooks { - match (hook.fun)(&ctx, &mut command_invoke, &args).await { - HookResult::Continue => {} - HookResult::Halt => { - return; - } - } - } - - for hook in &self.hooks { - match (hook.fun)(&ctx, &mut command_invoke, &args).await { - HookResult::Continue => {} - HookResult::Halt => { - return; - } - } - } - - match command.fun { - CommandFnType::Slash(t) => t(&ctx, &mut command_invoke, args).await, - CommandFnType::Multi(m) => m(&ctx, &mut command_invoke).await, - } - } - - pub async fn run_command_from_options( - &self, - ctx: &Context, - command_invoke: &mut CommandInvoke, - command_options: CommandOptions, - ) { - let command = { - self.commands_map - .get(&command_options.command) - .expect(&format!("Received invalid command: {}", command_options.command)) - }; - - match command.fun { - CommandFnType::Slash(t) => t(&ctx, command_invoke, command_options).await, - CommandFnType::Multi(m) => m(&ctx, command_invoke).await, - } - } -} diff --git a/src/hooks.rs b/src/hooks.rs index 2b5ccd7..35cd4df 100644 --- a/src/hooks.rs +++ b/src/hooks.rs @@ -1,6 +1,6 @@ use poise::{serenity::model::channel::Channel, ApplicationCommandOrAutocompleteInteraction}; -use crate::{consts::MACRO_MAX_COMMANDS, Context, Error}; +use crate::{consts::MACRO_MAX_COMMANDS, models::command_macro::RecordedCommand, Context, Error}; pub async fn guild_only(ctx: Context<'_>) -> Result { if ctx.guild_id().is_some() { @@ -25,12 +25,18 @@ async fn macro_check(ctx: Context<'_>) -> bool { if command_macro.commands.len() >= MACRO_MAX_COMMANDS { let _ = ctx.send(|m| { m.ephemeral(true).content( - "5 commands already recorded. Please use `/macro finish` to end recording.", + format!("{} commands already recorded. Please use `/macro finish` to end recording.", MACRO_MAX_COMMANDS), ) }) .await; } else { - // TODO TODO TODO write command to macro + let recorded = RecordedCommand { + action: None, + command_name: ctx.command().identifying_name.clone(), + options: Vec::from(app_ctx.args), + }; + + command_macro.commands.push(recorded); let _ = ctx .send(|m| m.ephemeral(true).content("Command recorded to macro")) diff --git a/src/main.rs b/src/main.rs index 89dc26b..44f3fe4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,7 +3,7 @@ extern crate lazy_static; mod commands; -// mod component_models; +mod component_models; mod consts; mod event_handlers; mod hooks; @@ -24,7 +24,7 @@ use sqlx::{MySql, Pool}; use tokio::sync::RwLock; use crate::{ - commands::{info_cmds, moderation_cmds}, + commands::{info_cmds, moderation_cmds, reminder_cmds, todo_cmds}, consts::THEME_COLOR, event_handlers::listener, hooks::all_checks, @@ -71,6 +71,43 @@ async fn main() -> Result<(), Box> { ], ..moderation_cmds::macro_base() }, + reminder_cmds::pause(), + reminder_cmds::offset(), + reminder_cmds::nudge(), + reminder_cmds::look(), + reminder_cmds::delete(), + poise::Command { + subcommands: vec![ + reminder_cmds::list_timer(), + reminder_cmds::start_timer(), + reminder_cmds::delete_timer(), + ], + ..reminder_cmds::timer_base() + }, + reminder_cmds::remind(), + poise::Command { + subcommands: vec![ + poise::Command { + subcommands: vec![ + todo_cmds::todo_guild_add(), + todo_cmds::todo_guild_view(), + ], + ..todo_cmds::todo_guild_base() + }, + poise::Command { + subcommands: vec![ + todo_cmds::todo_channel_add(), + todo_cmds::todo_channel_view(), + ], + ..todo_cmds::todo_channel_base() + }, + poise::Command { + subcommands: vec![todo_cmds::todo_user_add(), todo_cmds::todo_user_view()], + ..todo_cmds::todo_user_base() + }, + ], + ..todo_cmds::todo_base() + }, ], allowed_mentions: None, command_check: Some(|ctx| Box::pin(all_checks(ctx))), diff --git a/src/models/command_macro.rs b/src/models/command_macro.rs index 6ad655e..5f7bfb1 100644 --- a/src/models/command_macro.rs +++ b/src/models/command_macro.rs @@ -1,20 +1,29 @@ -use poise::serenity::{ - client::Context, - model::{ - id::GuildId, interactions::application_command::ApplicationCommandInteractionDataOption, - }, +use poise::serenity::model::{ + id::GuildId, interactions::application_command::ApplicationCommandInteractionDataOption, }; -use serde::Serialize; +use serde::{Deserialize, Serialize}; -#[derive(Serialize)] +use crate::{Context, Data, Error}; + +fn default_none() -> Option< + for<'a> fn( + poise::ApplicationContext<'a, U, E>, + ) -> poise::BoxFuture<'a, Result<(), poise::FrameworkError<'a, U, E>>>, +> { + None +} + +#[derive(Serialize, Deserialize)] pub struct RecordedCommand { #[serde(skip)] - action: for<'a> fn( - poise::ApplicationContext<'a, U, E>, - &'a [ApplicationCommandInteractionDataOption], - ) -> poise::BoxFuture<'a, Result<(), poise::FrameworkError<'a, U, E>>>, - command_name: String, - options: Vec, + #[serde(default = "default_none::")] + pub action: Option< + for<'a> fn( + poise::ApplicationContext<'a, U, E>, + ) -> poise::BoxFuture<'a, Result<(), poise::FrameworkError<'a, U, E>>>, + >, + pub command_name: String, + pub options: Vec, } pub struct CommandMacro { @@ -23,3 +32,42 @@ pub struct CommandMacro { pub description: Option, pub commands: Vec>, } + +pub async fn guild_command_macro( + ctx: &Context<'_>, + name: &str, +) -> Option> { + let row = sqlx::query!( + " +SELECT * FROM macro WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?) AND name = ? + ", + ctx.guild_id().unwrap().0, + name + ) + .fetch_one(&ctx.data().database) + .await + .ok()?; + + let mut commands: Vec> = + serde_json::from_str(&row.commands).unwrap(); + + for recorded_command in &mut commands { + let command = &ctx + .framework() + .options() + .commands + .iter() + .find(|c| c.identifying_name == recorded_command.command_name); + + recorded_command.action = command.map(|c| c.slash_action).flatten().clone(); + } + + let command_macro = CommandMacro { + guild_id: ctx.guild_id().unwrap(), + name: row.name, + description: row.description, + commands, + }; + + Some(command_macro) +} diff --git a/src/models/mod.rs b/src/models/mod.rs index cd33af0..63c19a1 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -9,21 +9,20 @@ use poise::serenity::{async_trait, model::id::UserId}; use crate::{ models::{channel_data::ChannelData, user_data::UserData}, - Context, + CommandMacro, Context, Data, Error, }; #[async_trait] pub trait CtxData { - async fn user_data + Send>( - &self, - user_id: U, - ) -> Result>; + async fn user_data + Send>(&self, user_id: U) -> Result; - async fn author_data(&self) -> Result>; + async fn author_data(&self) -> Result; async fn timezone(&self) -> Tz; - async fn channel_data(&self) -> Result>; + async fn channel_data(&self) -> Result; + + async fn command_macros(&self) -> Result>, Error>; } #[async_trait] @@ -48,4 +47,22 @@ impl CtxData for Context<'_> { ChannelData::from_channel(&channel, &self.data().database).await } + + async fn command_macros(&self) -> Result>, Error> { + let guild_id = self.guild_id().unwrap(); + + let rows = sqlx::query!( + "SELECT name, description FROM macro WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?)", + guild_id.0 + ) + .fetch_all(&self.data().database) + .await?.iter().map(|row| CommandMacro { + guild_id, + name: row.name.clone(), + description: row.description.clone(), + commands: vec![] + }).collect(); + + Ok(rows) + } }