everything except component model actions
This commit is contained in:
parent
84ee7e77c5
commit
afc376c44f
20
Cargo.lock
generated
20
Cargo.lock
generated
@ -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"
|
||||
|
@ -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"
|
||||
|
||||
|
@ -1,16 +0,0 @@
|
||||
[package]
|
||||
name = "regex_command_attr"
|
||||
version = "0.3.6"
|
||||
authors = ["acdenisSK <acdenissk69@gmail.com>", "jellywx <judesouthworth@pm.me>"]
|
||||
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"] }
|
@ -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>]
|
||||
Name,
|
||||
|
||||
// #[<name> = <value>]
|
||||
Equals,
|
||||
|
||||
// #[<name>([<value>, <value>, <value>, ...])]
|
||||
List,
|
||||
|
||||
// #[<name>([<prop> = <value>, <prop> = <value>, ...])]
|
||||
EqualsList,
|
||||
|
||||
// #[<name>(<value>)]
|
||||
SingleList,
|
||||
}
|
||||
|
||||
impl fmt::Display for ValueKind {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
ValueKind::Name => f.pad("`#[<name>]`"),
|
||||
ValueKind::Equals => f.pad("`#[<name> = <value>]`"),
|
||||
ValueKind::List => f.pad("`#[<name>([<value>, <value>, <value>, ...])]`"),
|
||||
ValueKind::EqualsList => {
|
||||
f.pad("`#[<name>([<prop> = <value>, <prop> = <value>, ...])]`")
|
||||
}
|
||||
ValueKind::SingleList => f.pad("`#[<name>(<value>)]`"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn to_ident(p: Path) -> Result<Ident> {
|
||||
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<String>, Lit)>,
|
||||
pub kind: ValueKind,
|
||||
pub span: Span,
|
||||
}
|
||||
|
||||
impl Values {
|
||||
#[inline]
|
||||
pub fn new(
|
||||
name: Ident,
|
||||
kind: ValueKind,
|
||||
literals: Vec<(Option<String>, Lit)>,
|
||||
span: Span,
|
||||
) -> Self {
|
||||
Values { name, literals, kind, span }
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_values(attr: &Attribute) -> Result<Values> {
|
||||
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<T: AttributeOption>(values: Values) -> Result<T> {
|
||||
T::parse(values)
|
||||
}
|
||||
|
||||
pub trait AttributeOption: Sized {
|
||||
fn parse(values: Values) -> Result<Self>;
|
||||
}
|
||||
|
||||
impl AttributeOption for Vec<String> {
|
||||
fn parse(values: Values) -> Result<Self> {
|
||||
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<Self> {
|
||||
validate(&values, &[ValueKind::Equals, ValueKind::SingleList])?;
|
||||
|
||||
Ok(values.literals[0].1.to_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl AttributeOption for bool {
|
||||
#[inline]
|
||||
fn parse(values: Values) -> Result<Self> {
|
||||
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<Self> {
|
||||
validate(&values, &[ValueKind::SingleList])?;
|
||||
|
||||
Ok(values.literals[0].1.to_ident())
|
||||
}
|
||||
}
|
||||
|
||||
impl AttributeOption for Vec<Ident> {
|
||||
#[inline]
|
||||
fn parse(values: Values) -> Result<Self> {
|
||||
validate(&values, &[ValueKind::List])?;
|
||||
|
||||
Ok(values.literals.into_iter().map(|(_, l)| l.to_ident()).collect())
|
||||
}
|
||||
}
|
||||
|
||||
impl AttributeOption for Option<String> {
|
||||
fn parse(values: Values) -> Result<Self> {
|
||||
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<Self> {
|
||||
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<T: AttributeOption> AttributeOption for AsOption<T> {
|
||||
#[inline]
|
||||
fn parse(values: Values) -> Result<Self> {
|
||||
Ok(AsOption(Some(T::parse(values)?)))
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! attr_option_num {
|
||||
($($n:ty),*) => {
|
||||
$(
|
||||
impl AttributeOption for $n {
|
||||
fn parse(values: Values) -> Result<Self> {
|
||||
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<Self> {
|
||||
<$n as AttributeOption>::parse(values).map(Some)
|
||||
}
|
||||
}
|
||||
)*
|
||||
}
|
||||
}
|
||||
|
||||
attr_option_num!(u16, u32, usize);
|
@ -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::*;
|
@ -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<Ident> = 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::<Vec<Ident>>();
|
||||
|
||||
let mut subcommand_idents = subcommands
|
||||
.iter()
|
||||
.map(|subcommand| {
|
||||
root_ident
|
||||
.with_suffix(subcommand.name.replace("-", "_").as_str())
|
||||
.with_suffix(SUBCOMMAND)
|
||||
})
|
||||
.collect::<Vec<Ident>>();
|
||||
|
||||
let mut arg_idents = cmd_args
|
||||
.iter()
|
||||
.map(|arg| root_ident.with_suffix(arg.name.replace("-", "_").as_str()).with_suffix(ARG))
|
||||
.collect::<Vec<Ident>>();
|
||||
|
||||
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()
|
||||
}
|
@ -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<Argument> {
|
||||
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<Attribute>,
|
||||
/// 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<Argument>,
|
||||
pub ret: Type,
|
||||
pub body: Vec<Stmt>,
|
||||
}
|
||||
|
||||
impl Parse for CommandFun {
|
||||
fn parse(input: ParseStream<'_>) -> Result<Self> {
|
||||
let attributes = input.call(Attribute::parse_outer)?;
|
||||
|
||||
let visibility = input.parse::<Visibility>()?;
|
||||
|
||||
input.parse::<Token![async]>()?;
|
||||
|
||||
input.parse::<Token![fn]>()?;
|
||||
let name = input.parse()?;
|
||||
|
||||
// (...)
|
||||
let Parenthesised(args) = input.parse::<Parenthesised<FnArg>>()?;
|
||||
|
||||
let ret = match input.parse::<ReturnType>()? {
|
||||
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::<Result<Vec<_>>>()?;
|
||||
|
||||
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<Arg>,
|
||||
}
|
||||
|
||||
impl Subcommand {
|
||||
pub fn as_tokens(&mut self, ident: &Ident) -> TokenStream2 {
|
||||
let arg_path = quote!(crate::framework::Arg);
|
||||
let subcommand_path = ApplicationCommandOptionType::SubCommand;
|
||||
|
||||
let arg_idents = self
|
||||
.cmd_args
|
||||
.iter()
|
||||
.map(|arg| ident.with_suffix(arg.name.as_str()).with_suffix(ARG))
|
||||
.collect::<Vec<Ident>>();
|
||||
|
||||
let mut tokens = self
|
||||
.cmd_args
|
||||
.iter_mut()
|
||||
.zip(arg_idents.iter())
|
||||
.map(|(arg, ident)| arg.as_tokens(ident))
|
||||
.fold(quote! {}, |mut a, b| {
|
||||
a.extend(b);
|
||||
a
|
||||
});
|
||||
|
||||
let Subcommand { name, description, .. } = self;
|
||||
|
||||
tokens.extend(quote! {
|
||||
#[allow(missing_docs)]
|
||||
pub static #ident: #arg_path = #arg_path {
|
||||
name: #name,
|
||||
description: #description,
|
||||
kind: #subcommand_path,
|
||||
required: false,
|
||||
options: &[#(&#arg_idents),*],
|
||||
};
|
||||
});
|
||||
|
||||
tokens
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Subcommand {
|
||||
fn default() -> Self {
|
||||
Self { name: String::new(), description: String::new(), cmd_args: vec![] }
|
||||
}
|
||||
}
|
||||
|
||||
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<Subcommand>,
|
||||
}
|
||||
|
||||
impl SubcommandGroup {
|
||||
pub fn as_tokens(&mut self, ident: &Ident) -> TokenStream2 {
|
||||
let arg_path = quote!(crate::framework::Arg);
|
||||
let subcommand_group_path = ApplicationCommandOptionType::SubCommandGroup;
|
||||
|
||||
let arg_idents = self
|
||||
.subcommands
|
||||
.iter()
|
||||
.map(|arg| {
|
||||
ident
|
||||
.with_suffix(self.name.as_str())
|
||||
.with_suffix(arg.name.as_str())
|
||||
.with_suffix(SUBCOMMAND)
|
||||
})
|
||||
.collect::<Vec<Ident>>();
|
||||
|
||||
let mut tokens = self
|
||||
.subcommands
|
||||
.iter_mut()
|
||||
.zip(arg_idents.iter())
|
||||
.map(|(subcommand, ident)| subcommand.as_tokens(ident))
|
||||
.fold(quote! {}, |mut a, b| {
|
||||
a.extend(b);
|
||||
a
|
||||
});
|
||||
|
||||
let SubcommandGroup { name, description, .. } = self;
|
||||
|
||||
tokens.extend(quote! {
|
||||
#[allow(missing_docs)]
|
||||
pub static #ident: #arg_path = #arg_path {
|
||||
name: #name,
|
||||
description: #description,
|
||||
kind: #subcommand_group_path,
|
||||
required: false,
|
||||
options: &[#(&#arg_idents),*],
|
||||
};
|
||||
});
|
||||
|
||||
tokens
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for SubcommandGroup {
|
||||
fn default() -> Self {
|
||||
Self { name: String::new(), description: String::new(), subcommands: vec![] }
|
||||
}
|
||||
}
|
||||
|
||||
impl SubcommandGroup {
|
||||
pub(crate) fn new(name: String) -> Self {
|
||||
Self { name, ..Default::default() }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub(crate) struct Options {
|
||||
pub aliases: Vec<String>,
|
||||
pub description: String,
|
||||
pub group: String,
|
||||
pub examples: Vec<String>,
|
||||
pub can_blacklist: bool,
|
||||
pub supports_dm: bool,
|
||||
pub cmd_args: Vec<Arg>,
|
||||
pub subcommands: Vec<Subcommand>,
|
||||
pub subcommand_groups: Vec<SubcommandGroup>,
|
||||
}
|
||||
|
||||
impl Options {
|
||||
#[inline]
|
||||
pub fn new() -> Self {
|
||||
Self { group: "None".to_string(), ..Default::default() }
|
||||
}
|
||||
}
|
@ -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<T>(pub Punctuated<T, Comma>);
|
||||
|
||||
impl<T: Parse> Parse for Bracketed<T> {
|
||||
fn parse(input: ParseStream<'_>) -> SynResult<Self> {
|
||||
let content;
|
||||
bracketed!(content in input);
|
||||
|
||||
Ok(Bracketed(content.parse_terminated(T::parse)?))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Braced<T>(pub Punctuated<T, Comma>);
|
||||
|
||||
impl<T: Parse> Parse for Braced<T> {
|
||||
fn parse(input: ParseStream<'_>) -> SynResult<Self> {
|
||||
let content;
|
||||
braced!(content in input);
|
||||
|
||||
Ok(Braced(content.parse_terminated(T::parse)?))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Parenthesised<T>(pub Punctuated<T, Comma>);
|
||||
|
||||
impl<T: Parse> Parse for Parenthesised<T> {
|
||||
fn parse(input: ParseStream<'_>) -> SynResult<Self> {
|
||||
let content;
|
||||
parenthesized!(content in input);
|
||||
|
||||
Ok(Parenthesised(content.parse_terminated(T::parse)?))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct AsOption<T>(pub Option<T>);
|
||||
|
||||
impl<T: ToTokens> ToTokens for AsOption<T> {
|
||||
fn to_tokens(&self, stream: &mut TokenStream2) {
|
||||
match &self.0 {
|
||||
Some(o) => stream.extend(quote!(Some(#o))),
|
||||
None => stream.extend(quote!(None)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Default for AsOption<T> {
|
||||
#[inline]
|
||||
fn default() -> Self {
|
||||
AsOption(None)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Argument {
|
||||
pub mutable: Option<Mut>,
|
||||
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<Argument>) {
|
||||
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');
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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<CommandMacro<Data, Error>> = 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<U, E>(macros: &[CommandMacro<U, E>]) -> usize {
|
||||
}
|
||||
|
||||
pub fn show_macro_page<U, E>(macros: &[CommandMacro<U, E>], 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<U, E>(macros: &[CommandMacro<U, E>], page: usize) -> Crea
|
||||
});
|
||||
|
||||
reply
|
||||
*/
|
||||
}
|
||||
|
@ -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::<SQLPool>().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<String>,
|
||||
) -> 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 **<t:{}:D>**",
|
||||
timestamp
|
||||
)),
|
||||
)
|
||||
.await;
|
||||
ctx.say(format!(
|
||||
"Reminders in this channel have been silenced until **<t:{}:D>**",
|
||||
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::<SQLPool>().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<isize>,
|
||||
#[description = "Number of minutes to offset by"] minutes: Option<isize>,
|
||||
#[description = "Number of seconds to offset by"] seconds: Option<isize>,
|
||||
) -> 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::<SQLPool>().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<isize>,
|
||||
#[description = "Number of seconds to nudge new reminders by"] seconds: Option<isize>,
|
||||
) -> 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::<SQLPool>().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<Channel>,
|
||||
#[description = "Whether to show disabled reminders or not"] disabled: Option<bool>,
|
||||
#[description = "Whether to display times as relative or exact times"] relative: Option<bool>,
|
||||
) -> 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::<SQLPool>().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<String>,
|
||||
#[description = "(Patreon only) Time to wait before repeating the reminder. Leave blank for one-shot reminder"]
|
||||
interval: Option<String>,
|
||||
#[description = "(Patreon only) For repeating reminders, the time at which the reminder will stop sending"]
|
||||
expires: Option<String>,
|
||||
#[description = "Set the TTS flag on the reminder message, similar to the /tts command"]
|
||||
tts: Option<bool>,
|
||||
) -> 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(
|
||||
|
@ -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::<SQLPool>().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::<Vec<(usize, String)>>()
|
||||
} 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::<Vec<(usize, String)>>()
|
||||
} 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::<Vec<(usize, String)>>()
|
||||
};
|
||||
ctx.guild_id().unwrap().0,
|
||||
)
|
||||
.fetch_all(&ctx.data().database)
|
||||
.await
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|row| (row.id as usize, row.value.clone()))
|
||||
.collect::<Vec<(usize, String)>>();
|
||||
|
||||
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::<Vec<(usize, String)>>();
|
||||
|
||||
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::<Vec<(usize, String)>>();
|
||||
|
||||
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<u64>,
|
||||
channel_id: Option<u64>,
|
||||
guild_id: Option<u64>,
|
||||
) -> 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
|
||||
}
|
||||
}
|
||||
|
@ -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::<SQLPool>().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::<SQLPool>().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::<SQLPool>().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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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};
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
|
692
src/framework.rs
692
src/framework.rs
@ -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<CreateEmbed>,
|
||||
components: Option<CreateComponents>,
|
||||
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<D: ToString>(mut self, content: D) -> Self {
|
||||
self.content = content.to_string();
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
pub fn embed<F: FnOnce(&mut CreateEmbed) -> &mut CreateEmbed>(mut self, f: F) -> Self {
|
||||
let mut embed = CreateEmbed::default();
|
||||
f(&mut embed);
|
||||
|
||||
self.embed = Some(embed);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn components<F: FnOnce(&mut CreateComponents) -> &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<Http>) {
|
||||
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<GuildId> {
|
||||
match &self.model {
|
||||
InvokeModel::Slash(i) => i.guild_id,
|
||||
InvokeModel::Component(i) => i.guild_id,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn guild(&self, cache: impl AsRef<Cache>) -> Option<Guild> {
|
||||
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<Http>,
|
||||
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<i64> {
|
||||
match self {
|
||||
OptionValue::Integer(i) => Some(*i),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_bool(&self) -> Option<bool> {
|
||||
match self {
|
||||
OptionValue::Boolean(b) => Some(*b),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_channel_id(&self) -> Option<ChannelId> {
|
||||
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<String>,
|
||||
pub subcommand_group: Option<String>,
|
||||
pub options: HashMap<String, OptionValue>,
|
||||
}
|
||||
|
||||
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::<u64>().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::<u64>().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::<u64>().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<H: Hasher>(&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<String, &'static Command>,
|
||||
pub commands: HashSet<&'static Command>,
|
||||
ignore_bots: bool,
|
||||
dm_enabled: bool,
|
||||
debug_guild: Option<GuildId>,
|
||||
hooks: Vec<&'static Hook>,
|
||||
}
|
||||
|
||||
impl TypeMapKey for RegexFramework {
|
||||
type Value = Arc<RegexFramework>;
|
||||
}
|
||||
|
||||
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<GuildId>) -> 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<Http>) {
|
||||
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::<SQLPool>().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,
|
||||
}
|
||||
}
|
||||
}
|
12
src/hooks.rs
12
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<bool, Error> {
|
||||
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"))
|
||||
|
41
src/main.rs
41
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<dyn std::error::Error + Send + Sync>> {
|
||||
],
|
||||
..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))),
|
||||
|
@ -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<U, E>() -> 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<U, E> {
|
||||
#[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<ApplicationCommandInteractionDataOption>,
|
||||
#[serde(default = "default_none::<U, E>")]
|
||||
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<ApplicationCommandInteractionDataOption>,
|
||||
}
|
||||
|
||||
pub struct CommandMacro<U, E> {
|
||||
@ -23,3 +32,42 @@ pub struct CommandMacro<U, E> {
|
||||
pub description: Option<String>,
|
||||
pub commands: Vec<RecordedCommand<U, E>>,
|
||||
}
|
||||
|
||||
pub async fn guild_command_macro(
|
||||
ctx: &Context<'_>,
|
||||
name: &str,
|
||||
) -> Option<CommandMacro<Data, Error>> {
|
||||
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<RecordedCommand<Data, Error>> =
|
||||
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)
|
||||
}
|
||||
|
@ -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<U: Into<UserId> + Send>(
|
||||
&self,
|
||||
user_id: U,
|
||||
) -> Result<UserData, Box<dyn std::error::Error + Sync + Send>>;
|
||||
async fn user_data<U: Into<UserId> + Send>(&self, user_id: U) -> Result<UserData, Error>;
|
||||
|
||||
async fn author_data(&self) -> Result<UserData, Box<dyn std::error::Error + Sync + Send>>;
|
||||
async fn author_data(&self) -> Result<UserData, Error>;
|
||||
|
||||
async fn timezone(&self) -> Tz;
|
||||
|
||||
async fn channel_data(&self) -> Result<ChannelData, Box<dyn std::error::Error + Sync + Send>>;
|
||||
async fn channel_data(&self) -> Result<ChannelData, Error>;
|
||||
|
||||
async fn command_macros(&self) -> Result<Vec<CommandMacro<Data, Error>>, 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<Vec<CommandMacro<Data, Error>>, 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)
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user