Compare commits
2 Commits
poise
...
postman-in
Author | SHA1 | Date | |
---|---|---|---|
e5ab99f67b | |||
e47715917e |
991
Cargo.lock
generated
991
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
24
Cargo.toml
24
Cargo.toml
@ -1,12 +1,10 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "reminder_rs"
|
name = "reminder_rs"
|
||||||
version = "1.6.0-beta2"
|
version = "1.6.0-beta3"
|
||||||
authors = ["jellywx <judesouthworth@pm.me>"]
|
authors = ["jellywx <judesouthworth@pm.me>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
songbird = { git = "https://github.com/serenity-rs/songbird", branch = "next" }
|
|
||||||
poise = { git = "https://github.com/kangalioo/poise", branch = "master" }
|
|
||||||
dotenv = "0.15"
|
dotenv = "0.15"
|
||||||
humantime = "2.1"
|
humantime = "2.1"
|
||||||
tokio = { version = "1", features = ["process", "full"] }
|
tokio = { version = "1", features = ["process", "full"] }
|
||||||
@ -26,3 +24,23 @@ rand = "0.7"
|
|||||||
levenshtein = "1.0"
|
levenshtein = "1.0"
|
||||||
sqlx = { version = "0.5", features = ["runtime-tokio-rustls", "macros", "mysql", "bigdecimal", "chrono"]}
|
sqlx = { version = "0.5", features = ["runtime-tokio-rustls", "macros", "mysql", "bigdecimal", "chrono"]}
|
||||||
base64 = "0.13.0"
|
base64 = "0.13.0"
|
||||||
|
|
||||||
|
[dependencies.regex_command_attr]
|
||||||
|
path = "command_attributes"
|
||||||
|
|
||||||
|
[dependencies.serenity]
|
||||||
|
git = "https://github.com/serenity-rs/serenity"
|
||||||
|
branch = "next"
|
||||||
|
default-features = false
|
||||||
|
features = [
|
||||||
|
"builder",
|
||||||
|
"client",
|
||||||
|
"cache",
|
||||||
|
"gateway",
|
||||||
|
"http",
|
||||||
|
"model",
|
||||||
|
"utils",
|
||||||
|
"rustls_backend",
|
||||||
|
"collector",
|
||||||
|
"unstable_discord_api"
|
||||||
|
]
|
||||||
|
@ -41,4 +41,3 @@ __Other Variables__
|
|||||||
|
|
||||||
* Convert aliases to macros
|
* Convert aliases to macros
|
||||||
* Help command
|
* Help command
|
||||||
* Test everything
|
|
||||||
|
16
command_attributes/Cargo.toml
Normal file
16
command_attributes/Cargo.toml
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
[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"] }
|
351
command_attributes/src/attributes.rs
Normal file
351
command_attributes/src/attributes.rs
Normal file
@ -0,0 +1,351 @@
|
|||||||
|
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);
|
10
command_attributes/src/consts.rs
Normal file
10
command_attributes/src/consts.rs
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
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::*;
|
321
command_attributes/src/lib.rs
Normal file
321
command_attributes/src/lib.rs
Normal file
@ -0,0 +1,321 @@
|
|||||||
|
#![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()
|
||||||
|
}
|
331
command_attributes/src/structures.rs
Normal file
331
command_attributes/src/structures.rs
Normal file
@ -0,0 +1,331 @@
|
|||||||
|
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() }
|
||||||
|
}
|
||||||
|
}
|
176
command_attributes/src/util.rs
Normal file
176
command_attributes/src/util.rs
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
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,11 +1,16 @@
|
|||||||
use chrono::offset::Utc;
|
use chrono::offset::Utc;
|
||||||
use poise::serenity::builder::CreateEmbedFooter;
|
use regex_command_attr::command;
|
||||||
|
use serenity::{builder::CreateEmbedFooter, client::Context};
|
||||||
|
|
||||||
use crate::{models::CtxData, Context, Error, THEME_COLOR};
|
use crate::{
|
||||||
|
framework::{CommandInvoke, CreateGenericResponse},
|
||||||
|
models::CtxData,
|
||||||
|
THEME_COLOR,
|
||||||
|
};
|
||||||
|
|
||||||
fn footer(ctx: Context<'_>) -> impl FnOnce(&mut CreateEmbedFooter) -> &mut CreateEmbedFooter {
|
fn footer(ctx: &Context) -> impl FnOnce(&mut CreateEmbedFooter) -> &mut CreateEmbedFooter {
|
||||||
let shard_count = ctx.discord().cache.shard_count();
|
let shard_count = ctx.cache.shard_count();
|
||||||
let shard = ctx.discord().shard_id;
|
let shard = ctx.shard_id;
|
||||||
|
|
||||||
move |f| {
|
move |f| {
|
||||||
f.text(format!(
|
f.text(format!(
|
||||||
@ -17,14 +22,15 @@ fn footer(ctx: Context<'_>) -> impl FnOnce(&mut CreateEmbedFooter) -> &mut Creat
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get an overview of bot commands
|
#[command]
|
||||||
#[poise::command(slash_command)]
|
#[description("Get an overview of the bot commands")]
|
||||||
pub async fn help(ctx: Context<'_>) -> Result<(), Error> {
|
async fn help(ctx: &Context, invoke: &mut CommandInvoke) {
|
||||||
let footer = footer(ctx);
|
let footer = footer(ctx);
|
||||||
|
|
||||||
let _ = ctx
|
let _ = invoke
|
||||||
.send(|m| {
|
.respond(
|
||||||
m.embed(|e| {
|
&ctx,
|
||||||
|
CreateGenericResponse::new().embed(|e| {
|
||||||
e.title("Help")
|
e.title("Help")
|
||||||
.color(*THEME_COLOR)
|
.color(*THEME_COLOR)
|
||||||
.description(
|
.description(
|
||||||
@ -54,21 +60,21 @@ __Advanced Commands__
|
|||||||
",
|
",
|
||||||
)
|
)
|
||||||
.footer(footer)
|
.footer(footer)
|
||||||
})
|
}),
|
||||||
})
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get information about the bot
|
#[command]
|
||||||
#[poise::command(slash_command)]
|
#[aliases("invite")]
|
||||||
pub async fn info(ctx: Context<'_>) -> Result<(), Error> {
|
#[description("Get information about the bot")]
|
||||||
|
async fn info(ctx: &Context, invoke: &mut CommandInvoke) {
|
||||||
let footer = footer(ctx);
|
let footer = footer(ctx);
|
||||||
|
|
||||||
let _ = ctx
|
let _ = invoke
|
||||||
.send(|m| {
|
.respond(
|
||||||
m.embed(|e| {
|
ctx.http.clone(),
|
||||||
|
CreateGenericResponse::new().embed(|e| {
|
||||||
e.title("Info")
|
e.title("Info")
|
||||||
.description(format!(
|
.description(format!(
|
||||||
"Help: `/help`
|
"Help: `/help`
|
||||||
@ -83,19 +89,21 @@ Use our dashboard: https://reminder-bot.com/",
|
|||||||
))
|
))
|
||||||
.footer(footer)
|
.footer(footer)
|
||||||
.color(*THEME_COLOR)
|
.color(*THEME_COLOR)
|
||||||
})
|
}),
|
||||||
})
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Details on supporting the bot and Patreon benefits
|
#[command]
|
||||||
#[poise::command(slash_command)]
|
#[description("Details on supporting the bot and Patreon benefits")]
|
||||||
pub async fn donate(ctx: Context<'_>) -> Result<(), Error> {
|
#[group("Info")]
|
||||||
|
async fn donate(ctx: &Context, invoke: &mut CommandInvoke) {
|
||||||
let footer = footer(ctx);
|
let footer = footer(ctx);
|
||||||
|
|
||||||
let _ = ctx.send(|m| m.embed(|e| {
|
let _ = invoke
|
||||||
|
.respond(
|
||||||
|
ctx.http.clone(),
|
||||||
|
CreateGenericResponse::new().embed(|e| {
|
||||||
e.title("Donate")
|
e.title("Donate")
|
||||||
.description("Thinking of adding a monthly contribution? Click below for my Patreon and official bot server :)
|
.description("Thinking of adding a monthly contribution? Click below for my Patreon and official bot server :)
|
||||||
|
|
||||||
@ -117,41 +125,38 @@ Just $2 USD/month!
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the link to the online dashboard
|
#[command]
|
||||||
#[poise::command(slash_command)]
|
#[description("Get the link to the online dashboard")]
|
||||||
pub async fn dashboard(ctx: Context<'_>) -> Result<(), Error> {
|
#[group("Info")]
|
||||||
|
async fn dashboard(ctx: &Context, invoke: &mut CommandInvoke) {
|
||||||
let footer = footer(ctx);
|
let footer = footer(ctx);
|
||||||
|
|
||||||
let _ = ctx
|
let _ = invoke
|
||||||
.send(|m| {
|
.respond(
|
||||||
m.embed(|e| {
|
ctx.http.clone(),
|
||||||
|
CreateGenericResponse::new().embed(|e| {
|
||||||
e.title("Dashboard")
|
e.title("Dashboard")
|
||||||
.description("**https://reminder-bot.com/dashboard**")
|
.description("**https://reminder-bot.com/dashboard**")
|
||||||
.footer(footer)
|
.footer(footer)
|
||||||
.color(*THEME_COLOR)
|
.color(*THEME_COLOR)
|
||||||
})
|
}),
|
||||||
})
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// View the current time in a user's selected timezone
|
#[command]
|
||||||
#[poise::command(slash_command)]
|
#[description("View the current time in your selected timezone")]
|
||||||
pub async fn clock(ctx: Context<'_>) -> Result<(), Error> {
|
#[group("Info")]
|
||||||
ctx.defer_ephemeral().await?;
|
async fn clock(ctx: &Context, invoke: &mut CommandInvoke) {
|
||||||
|
let ud = ctx.user_data(&invoke.author_id()).await.unwrap();
|
||||||
|
let now = Utc::now().with_timezone(&ud.timezone());
|
||||||
|
|
||||||
let tz = ctx.timezone().await;
|
let _ = invoke
|
||||||
let now = Utc::now().with_timezone(&tz);
|
.respond(
|
||||||
|
ctx.http.clone(),
|
||||||
ctx.send(|m| {
|
CreateGenericResponse::new().content(format!("Current time: {}", now.format("%H:%M"))),
|
||||||
m.ephemeral(true).content(format!("Time in **{}**: `{}`", tz, now.format("%H:%M")))
|
)
|
||||||
})
|
.await;
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
pub mod info_cmds;
|
pub mod info_cmds;
|
||||||
pub mod moderation_cmds;
|
pub mod moderation_cmds;
|
||||||
// pub mod reminder_cmds;
|
pub mod reminder_cmds;
|
||||||
// pub mod todo_cmds;
|
pub mod todo_cmds;
|
||||||
|
@ -1,67 +1,54 @@
|
|||||||
use chrono::offset::Utc;
|
use chrono::offset::Utc;
|
||||||
use chrono_tz::{Tz, TZ_VARIANTS};
|
use chrono_tz::{Tz, TZ_VARIANTS};
|
||||||
use levenshtein::levenshtein;
|
use levenshtein::levenshtein;
|
||||||
use poise::CreateReply;
|
use regex_command_attr::command;
|
||||||
|
use serenity::client::Context;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
component_models::pager::{MacroPager, Pager},
|
||||||
consts::{EMBED_DESCRIPTION_MAX_LENGTH, THEME_COLOR},
|
consts::{EMBED_DESCRIPTION_MAX_LENGTH, THEME_COLOR},
|
||||||
hooks::guild_only,
|
framework::{CommandInvoke, CommandOptions, CreateGenericResponse, OptionValue},
|
||||||
models::{
|
hooks::{CHECK_GUILD_PERMISSIONS_HOOK, GUILD_ONLY_HOOK},
|
||||||
command_macro::{CommandMacro, CommandOptions},
|
models::{command_macro::CommandMacro, CtxData},
|
||||||
CtxData,
|
PopularTimezones, RecordingMacros, RegexFramework, SQLPool,
|
||||||
},
|
|
||||||
Context, Data, Error,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
async fn timezone_autocomplete(ctx: Context<'_>, partial: String) -> Vec<String> {
|
#[command("timezone")]
|
||||||
if partial.is_empty() {
|
#[description("Select your timezone")]
|
||||||
ctx.data().popular_timezones.iter().map(|t| t.to_string()).collect::<Vec<String>>()
|
#[arg(
|
||||||
} else {
|
name = "timezone",
|
||||||
TZ_VARIANTS
|
description = "Timezone to use from this list: https://gist.github.com/JellyWX/913dfc8b63d45192ad6cb54c829324ee",
|
||||||
.iter()
|
kind = "String",
|
||||||
.filter(|tz| {
|
required = false
|
||||||
partial.contains(&tz.to_string())
|
)]
|
||||||
|| tz.to_string().contains(&partial)
|
async fn timezone(ctx: &Context, invoke: &mut CommandInvoke, args: CommandOptions) {
|
||||||
|| levenshtein(&tz.to_string(), &partial) < 4
|
let pool = ctx.data.read().await.get::<SQLPool>().cloned().unwrap();
|
||||||
})
|
let mut user_data = ctx.user_data(invoke.author_id()).await.unwrap();
|
||||||
.take(25)
|
|
||||||
.map(|t| t.to_string())
|
|
||||||
.collect::<Vec<String>>()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Select your timezone
|
|
||||||
#[poise::command(slash_command)]
|
|
||||||
pub async fn timezone(
|
|
||||||
ctx: Context<'_>,
|
|
||||||
#[description = "Timezone to use from this list: https://gist.github.com/JellyWX/913dfc8b63d45192ad6cb54c829324ee"]
|
|
||||||
#[autocomplete = "timezone_autocomplete"]
|
|
||||||
timezone: Option<String>,
|
|
||||||
) -> Result<(), Error> {
|
|
||||||
let mut user_data = ctx.author_data().await.unwrap();
|
|
||||||
|
|
||||||
let footer_text = format!("Current timezone: {}", user_data.timezone);
|
let footer_text = format!("Current timezone: {}", user_data.timezone);
|
||||||
|
|
||||||
if let Some(timezone) = timezone {
|
if let Some(OptionValue::String(timezone)) = args.get("timezone") {
|
||||||
match timezone.parse::<Tz>() {
|
match timezone.parse::<Tz>() {
|
||||||
Ok(tz) => {
|
Ok(tz) => {
|
||||||
user_data.timezone = timezone.clone();
|
user_data.timezone = timezone.clone();
|
||||||
user_data.commit_changes(&ctx.data().database).await;
|
user_data.commit_changes(&pool).await;
|
||||||
|
|
||||||
let now = Utc::now().with_timezone(&tz);
|
let now = Utc::now().with_timezone(&tz);
|
||||||
|
|
||||||
ctx.send(|m| {
|
let _ = invoke
|
||||||
m.embed(|e| {
|
.respond(
|
||||||
e.title("Timezone Set")
|
ctx.http.clone(),
|
||||||
.description(format!(
|
CreateGenericResponse::new().embed(|e| {
|
||||||
"Timezone has been set to **{}**. Your current time should be `{}`",
|
e.title("Timezone Set")
|
||||||
timezone,
|
.description(format!(
|
||||||
now.format("%H:%M").to_string()
|
"Timezone has been set to **{}**. Your current time should be `{}`",
|
||||||
))
|
timezone,
|
||||||
.color(*THEME_COLOR)
|
now.format("%H:%M").to_string()
|
||||||
})
|
))
|
||||||
})
|
.color(*THEME_COLOR)
|
||||||
.await?;
|
}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
@ -69,8 +56,8 @@ pub async fn timezone(
|
|||||||
.iter()
|
.iter()
|
||||||
.filter(|tz| {
|
.filter(|tz| {
|
||||||
timezone.contains(&tz.to_string())
|
timezone.contains(&tz.to_string())
|
||||||
|| tz.to_string().contains(&timezone)
|
|| tz.to_string().contains(timezone)
|
||||||
|| levenshtein(&tz.to_string(), &timezone) < 4
|
|| levenshtein(&tz.to_string(), timezone) < 4
|
||||||
})
|
})
|
||||||
.take(25)
|
.take(25)
|
||||||
.map(|t| t.to_owned())
|
.map(|t| t.to_owned())
|
||||||
@ -87,21 +74,25 @@ pub async fn timezone(
|
|||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
ctx.send(|m| {
|
let _ = invoke
|
||||||
m.embed(|e| {
|
.respond(
|
||||||
e.title("Timezone Not Recognized")
|
ctx.http.clone(),
|
||||||
.description("Possibly you meant one of the following timezones, otherwise click [here](https://gist.github.com/JellyWX/913dfc8b63d45192ad6cb54c829324ee):")
|
CreateGenericResponse::new().embed(|e| {
|
||||||
.color(*THEME_COLOR)
|
e.title("Timezone Not Recognized")
|
||||||
.fields(fields)
|
.description("Possibly you meant one of the following timezones, otherwise click [here](https://gist.github.com/JellyWX/913dfc8b63d45192ad6cb54c829324ee):")
|
||||||
.footer(|f| f.text(footer_text))
|
.color(*THEME_COLOR)
|
||||||
.url("https://gist.github.com/JellyWX/913dfc8b63d45192ad6cb54c829324ee")
|
.fields(fields)
|
||||||
})
|
.footer(|f| f.text(footer_text))
|
||||||
})
|
.url("https://gist.github.com/JellyWX/913dfc8b63d45192ad6cb54c829324ee")
|
||||||
.await?;
|
}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let popular_timezones_iter = ctx.data().popular_timezones.iter().map(|t| {
|
let popular_timezones = ctx.data.read().await.get::<PopularTimezones>().cloned().unwrap();
|
||||||
|
|
||||||
|
let popular_timezones_iter = popular_timezones.iter().map(|t| {
|
||||||
(
|
(
|
||||||
t.to_string(),
|
t.to_string(),
|
||||||
format!("🕗 `{}`", Utc::now().with_timezone(t).format("%H:%M").to_string()),
|
format!("🕗 `{}`", Utc::now().with_timezone(t).format("%H:%M").to_string()),
|
||||||
@ -109,311 +100,276 @@ pub async fn timezone(
|
|||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
ctx.send(|m| {
|
let _ = invoke
|
||||||
m.embed(|e| {
|
.respond(
|
||||||
e.title("Timezone Usage")
|
ctx.http.clone(),
|
||||||
.description(
|
CreateGenericResponse::new().embed(|e| {
|
||||||
"**Usage:**
|
e.title("Timezone Usage")
|
||||||
|
.description(
|
||||||
|
"**Usage:**
|
||||||
`/timezone Name`
|
`/timezone Name`
|
||||||
|
|
||||||
**Example:**
|
**Example:**
|
||||||
`/timezone Europe/London`
|
`/timezone Europe/London`
|
||||||
|
|
||||||
You may want to use one of the popular timezones below, otherwise click [here](https://gist.github.com/JellyWX/913dfc8b63d45192ad6cb54c829324ee):",
|
You may want to use one of the popular timezones below, otherwise click [here](https://gist.github.com/JellyWX/913dfc8b63d45192ad6cb54c829324ee):",
|
||||||
)
|
|
||||||
.color(*THEME_COLOR)
|
|
||||||
.fields(popular_timezones_iter)
|
|
||||||
.footer(|f| f.text(footer_text))
|
|
||||||
.url("https://gist.github.com/JellyWX/913dfc8b63d45192ad6cb54c829324ee")
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn macro_name_autocomplete(ctx: Context<'_>, partial: String) -> Vec<String> {
|
|
||||||
sqlx::query!(
|
|
||||||
"
|
|
||||||
SELECT name
|
|
||||||
FROM macro
|
|
||||||
WHERE
|
|
||||||
guild_id = (SELECT id FROM guilds WHERE guild = ?)
|
|
||||||
AND name LIKE CONCAT(?, '%')",
|
|
||||||
ctx.guild_id().unwrap().0,
|
|
||||||
partial,
|
|
||||||
)
|
|
||||||
.fetch_all(&ctx.data().database)
|
|
||||||
.await
|
|
||||||
.unwrap_or(vec![])
|
|
||||||
.iter()
|
|
||||||
.map(|s| s.name.clone())
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Record and replay command sequences
|
|
||||||
#[poise::command(slash_command, rename = "macro", check = "guild_only")]
|
|
||||||
pub async fn macro_base(_ctx: Context<'_>) -> Result<(), Error> {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Start recording up to 5 commands to replay
|
|
||||||
#[poise::command(slash_command, rename = "record", check = "guild_only")]
|
|
||||||
pub async fn record_macro(
|
|
||||||
ctx: Context<'_>,
|
|
||||||
#[description = "Name for the new macro"] name: String,
|
|
||||||
#[description = "Description for the new macro"] description: Option<String>,
|
|
||||||
) -> Result<(), Error> {
|
|
||||||
let guild_id = ctx.guild_id().unwrap();
|
|
||||||
|
|
||||||
let row = sqlx::query!(
|
|
||||||
"
|
|
||||||
SELECT 1 as _e FROM macro WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?) AND name = ?",
|
|
||||||
guild_id.0,
|
|
||||||
name
|
|
||||||
)
|
|
||||||
.fetch_one(&ctx.data().database)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
if row.is_ok() {
|
|
||||||
ctx.send(|m| {
|
|
||||||
m.ephemeral(true).embed(|e| {
|
|
||||||
e.title("Unique Name Required")
|
|
||||||
.description(
|
|
||||||
"A macro already exists under this name.
|
|
||||||
Please select a unique name for your macro.",
|
|
||||||
)
|
|
||||||
.color(*THEME_COLOR)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
} else {
|
|
||||||
let okay = {
|
|
||||||
let mut lock = ctx.data().recording_macros.write().await;
|
|
||||||
|
|
||||||
if lock.contains_key(&(guild_id, ctx.author().id)) {
|
|
||||||
false
|
|
||||||
} else {
|
|
||||||
lock.insert(
|
|
||||||
(guild_id, ctx.author().id),
|
|
||||||
CommandMacro { guild_id, name, description, commands: vec![] },
|
|
||||||
);
|
|
||||||
true
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if okay {
|
|
||||||
ctx.send(|m| {
|
|
||||||
m.ephemeral(true).embed(|e| {
|
|
||||||
e.title("Macro Recording Started")
|
|
||||||
.description(
|
|
||||||
"Run up to 5 commands, or type `/macro finish` to stop at any point.
|
|
||||||
Any commands ran as part of recording will be inconsequential",
|
|
||||||
)
|
)
|
||||||
.color(*THEME_COLOR)
|
.color(*THEME_COLOR)
|
||||||
})
|
.fields(popular_timezones_iter)
|
||||||
})
|
.footer(|f| f.text(footer_text))
|
||||||
.await?;
|
.url("https://gist.github.com/JellyWX/913dfc8b63d45192ad6cb54c829324ee")
|
||||||
} else {
|
}),
|
||||||
ctx.send(|m| {
|
|
||||||
m.ephemeral(true).embed(|e| {
|
|
||||||
e.title("Macro Already Recording")
|
|
||||||
.description(
|
|
||||||
"You are already recording a macro in this server.
|
|
||||||
Please use `/macro finish` to end this recording before starting another.",
|
|
||||||
)
|
|
||||||
.color(*THEME_COLOR)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Finish current macro recording
|
|
||||||
#[poise::command(
|
|
||||||
slash_command,
|
|
||||||
rename = "finish",
|
|
||||||
check = "guild_only",
|
|
||||||
identifying_name = "macro_finish"
|
|
||||||
)]
|
|
||||||
pub async fn finish_macro(ctx: Context<'_>) -> Result<(), Error> {
|
|
||||||
let key = (ctx.guild_id().unwrap(), ctx.author().id);
|
|
||||||
|
|
||||||
{
|
|
||||||
let lock = ctx.data().recording_macros.read().await;
|
|
||||||
let contained = lock.get(&key);
|
|
||||||
|
|
||||||
if contained.map_or(true, |cmacro| cmacro.commands.is_empty()) {
|
|
||||||
ctx.send(|m| {
|
|
||||||
m.embed(|e| {
|
|
||||||
e.title("No Macro Recorded")
|
|
||||||
.description("Use `/macro record` to start recording a macro")
|
|
||||||
.color(*THEME_COLOR)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
} else {
|
|
||||||
let command_macro = contained.unwrap();
|
|
||||||
let json = serde_json::to_string(&command_macro.commands).unwrap();
|
|
||||||
|
|
||||||
sqlx::query!(
|
|
||||||
"INSERT INTO macro (guild_id, name, description, commands) VALUES ((SELECT id FROM guilds WHERE guild = ?), ?, ?, ?)",
|
|
||||||
command_macro.guild_id.0,
|
|
||||||
command_macro.name,
|
|
||||||
command_macro.description,
|
|
||||||
json
|
|
||||||
)
|
)
|
||||||
.execute(&ctx.data().database)
|
.await;
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
ctx.send(|m| {
|
|
||||||
m.embed(|e| {
|
|
||||||
e.title("Macro Recorded")
|
|
||||||
.description("Use `/macro run` to execute the macro")
|
|
||||||
.color(*THEME_COLOR)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
|
||||||
let mut lock = ctx.data().recording_macros.write().await;
|
|
||||||
lock.remove(&key);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// List recorded macros
|
#[command("macro")]
|
||||||
#[poise::command(slash_command, rename = "list", check = "guild_only")]
|
#[description("Record and replay command sequences")]
|
||||||
pub async fn list_macro(ctx: Context<'_>) -> Result<(), Error> {
|
#[subcommand("record")]
|
||||||
let macros = CommandMacro::from_guild(&ctx.data().database, ctx.guild_id().unwrap()).await;
|
#[description("Start recording up to 5 commands to replay")]
|
||||||
|
#[arg(name = "name", description = "Name for the new macro", kind = "String", required = true)]
|
||||||
|
#[arg(
|
||||||
|
name = "description",
|
||||||
|
description = "Description for the new macro",
|
||||||
|
kind = "String",
|
||||||
|
required = false
|
||||||
|
)]
|
||||||
|
#[subcommand("finish")]
|
||||||
|
#[description("Finish current recording")]
|
||||||
|
#[subcommand("list")]
|
||||||
|
#[description("List recorded macros")]
|
||||||
|
#[subcommand("run")]
|
||||||
|
#[description("Run a recorded macro")]
|
||||||
|
#[arg(name = "name", description = "Name of the macro to run", kind = "String", required = true)]
|
||||||
|
#[subcommand("delete")]
|
||||||
|
#[description("Delete a recorded macro")]
|
||||||
|
#[arg(name = "name", description = "Name of the macro to delete", kind = "String", required = true)]
|
||||||
|
#[supports_dm(false)]
|
||||||
|
#[hook(GUILD_ONLY_HOOK)]
|
||||||
|
#[hook(CHECK_GUILD_PERMISSIONS_HOOK)]
|
||||||
|
async fn macro_cmd(ctx: &Context, invoke: &mut CommandInvoke, args: CommandOptions) {
|
||||||
|
let pool = ctx.data.read().await.get::<SQLPool>().cloned().unwrap();
|
||||||
|
|
||||||
let resp = show_macro_page(¯os, 0);
|
match args.subcommand.clone().unwrap().as_str() {
|
||||||
|
"record" => {
|
||||||
|
let guild_id = invoke.guild_id().unwrap();
|
||||||
|
|
||||||
ctx.send(|m| {
|
let name = args.get("name").unwrap().to_string();
|
||||||
*m = resp;
|
|
||||||
m
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(())
|
let row = sqlx::query!(
|
||||||
}
|
"SELECT 1 as _e FROM macro WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?) AND name = ?",
|
||||||
|
guild_id.0,
|
||||||
|
name
|
||||||
|
)
|
||||||
|
.fetch_one(&pool)
|
||||||
|
.await;
|
||||||
|
|
||||||
fn find_command<'a>(
|
if row.is_ok() {
|
||||||
commands: &'a [poise::Command<Data, Error>],
|
let _ = invoke
|
||||||
searching_name: &str,
|
.respond(
|
||||||
command_options: &CommandOptions,
|
&ctx,
|
||||||
) -> Option<&'a poise::Command<Data, Error>> {
|
CreateGenericResponse::new().ephemeral().embed(|e| {
|
||||||
commands.iter().find_map(|cmd| {
|
e
|
||||||
if searching_name != cmd.name {
|
.title("Unique Name Required")
|
||||||
None
|
.description("A macro already exists under this name. Please select a unique name for your macro.")
|
||||||
} else {
|
.color(*THEME_COLOR)
|
||||||
if let Some(subgroup) = &command_options.subcommand_group {
|
}),
|
||||||
find_command(&cmd.subcommands, &subgroup, &command_options)
|
)
|
||||||
} else if let Some(subcommand) = &command_options.subcommand {
|
.await;
|
||||||
find_command(&cmd.subcommands, &subcommand, &command_options)
|
|
||||||
} else {
|
} else {
|
||||||
Some(cmd)
|
let macro_buffer = ctx.data.read().await.get::<RecordingMacros>().cloned().unwrap();
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Run a recorded macro
|
let okay = {
|
||||||
#[poise::command(slash_command, rename = "run", check = "guild_only")]
|
let mut lock = macro_buffer.write().await;
|
||||||
pub async fn run_macro(
|
|
||||||
ctx: Context<'_>,
|
|
||||||
#[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?;
|
|
||||||
|
|
||||||
let commands: Vec<CommandOptions> = serde_json::from_str(&row.commands)?;
|
if lock.contains_key(&(guild_id, invoke.author_id())) {
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
lock.insert(
|
||||||
|
(guild_id, invoke.author_id()),
|
||||||
|
CommandMacro {
|
||||||
|
guild_id,
|
||||||
|
name,
|
||||||
|
description: args.get("description").map(|d| d.to_string()),
|
||||||
|
commands: vec![],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
for command in commands {
|
if okay {
|
||||||
let cmd =
|
let _ = invoke
|
||||||
find_command(&ctx.framework().options().commands, &command.command, &command);
|
.respond(
|
||||||
|
&ctx,
|
||||||
if let Some(cmd) = cmd {
|
CreateGenericResponse::new().ephemeral().embed(|e| {
|
||||||
let mut executing_ctx = ctx.clone();
|
e
|
||||||
|
.title("Macro Recording Started")
|
||||||
executing_ctx.command = cmd;
|
.description(
|
||||||
|
"Run up to 5 commands, or type `/macro finish` to stop at any point.
|
||||||
|
Any commands ran as part of recording will be inconsequential")
|
||||||
|
.color(*THEME_COLOR)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
} else {
|
} else {
|
||||||
ctx.send(|m| {
|
let _ = invoke
|
||||||
m.ephemeral(true)
|
.respond(
|
||||||
.content(format!("Command `{}` not found", command.command))
|
&ctx,
|
||||||
})
|
CreateGenericResponse::new().ephemeral().embed(|e| {
|
||||||
.await?;
|
e.title("Macro Already Recording")
|
||||||
|
.description(
|
||||||
|
"You are already recording a macro in this server.
|
||||||
|
Please use `/macro finish` to end this recording before starting another.",
|
||||||
|
)
|
||||||
|
.color(*THEME_COLOR)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
"finish" => {
|
||||||
|
let key = (invoke.guild_id().unwrap(), invoke.author_id());
|
||||||
|
let macro_buffer = ctx.data.read().await.get::<RecordingMacros>().cloned().unwrap();
|
||||||
|
|
||||||
Err(sqlx::Error::RowNotFound) => {
|
{
|
||||||
ctx.say(format!("Macro \"{}\" not found", name)).await?;
|
let lock = macro_buffer.read().await;
|
||||||
}
|
let contained = lock.get(&key);
|
||||||
|
|
||||||
Err(e) => {
|
if contained.map_or(true, |cmacro| cmacro.commands.is_empty()) {
|
||||||
panic!("{}", e);
|
let _ = invoke
|
||||||
|
.respond(
|
||||||
|
&ctx,
|
||||||
|
CreateGenericResponse::new().embed(|e| {
|
||||||
|
e.title("No Macro Recorded")
|
||||||
|
.description("Use `/macro record` to start recording a macro")
|
||||||
|
.color(*THEME_COLOR)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
} else {
|
||||||
|
let command_macro = contained.unwrap();
|
||||||
|
let json = serde_json::to_string(&command_macro.commands).unwrap();
|
||||||
|
|
||||||
|
sqlx::query!(
|
||||||
|
"INSERT INTO macro (guild_id, name, description, commands) VALUES ((SELECT id FROM guilds WHERE guild = ?), ?, ?, ?)",
|
||||||
|
command_macro.guild_id.0,
|
||||||
|
command_macro.name,
|
||||||
|
command_macro.description,
|
||||||
|
json
|
||||||
|
)
|
||||||
|
.execute(&pool)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let _ = invoke
|
||||||
|
.respond(
|
||||||
|
&ctx,
|
||||||
|
CreateGenericResponse::new().embed(|e| {
|
||||||
|
e.title("Macro Recorded")
|
||||||
|
.description("Use `/macro run` to execute the macro")
|
||||||
|
.color(*THEME_COLOR)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut lock = macro_buffer.write().await;
|
||||||
|
lock.remove(&key);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
"list" => {
|
||||||
|
let macros = CommandMacro::from_guild(ctx, invoke.guild_id().unwrap()).await;
|
||||||
|
|
||||||
|
let resp = show_macro_page(¯os, 0);
|
||||||
|
|
||||||
|
invoke.respond(&ctx, resp).await.unwrap();
|
||||||
|
}
|
||||||
|
"run" => {
|
||||||
|
let macro_name = args.get("name").unwrap().to_string();
|
||||||
|
|
||||||
|
match sqlx::query!(
|
||||||
|
"SELECT commands FROM macro WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?) AND name = ?",
|
||||||
|
invoke.guild_id().unwrap().0,
|
||||||
|
macro_name
|
||||||
|
)
|
||||||
|
.fetch_one(&pool)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(row) => {
|
||||||
|
invoke.defer(&ctx).await;
|
||||||
|
|
||||||
|
let commands: Vec<CommandOptions> =
|
||||||
|
serde_json::from_str(&row.commands).unwrap();
|
||||||
|
let framework = ctx.data.read().await.get::<RegexFramework>().cloned().unwrap();
|
||||||
|
|
||||||
|
for command in commands {
|
||||||
|
framework.run_command_from_options(ctx, invoke, command).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(sqlx::Error::RowNotFound) => {
|
||||||
|
let _ = invoke
|
||||||
|
.respond(
|
||||||
|
&ctx,
|
||||||
|
CreateGenericResponse::new()
|
||||||
|
.content(format!("Macro \"{}\" not found", macro_name)),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(e) => {
|
||||||
|
panic!("{}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"delete" => {
|
||||||
|
let macro_name = args.get("name").unwrap().to_string();
|
||||||
|
|
||||||
|
match sqlx::query!(
|
||||||
|
"SELECT id FROM macro WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?) AND name = ?",
|
||||||
|
invoke.guild_id().unwrap().0,
|
||||||
|
macro_name
|
||||||
|
)
|
||||||
|
.fetch_one(&pool)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(row) => {
|
||||||
|
sqlx::query!("DELETE FROM macro WHERE id = ?", row.id)
|
||||||
|
.execute(&pool)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let _ = invoke
|
||||||
|
.respond(
|
||||||
|
&ctx,
|
||||||
|
CreateGenericResponse::new()
|
||||||
|
.content(format!("Macro \"{}\" deleted", macro_name)),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(sqlx::Error::RowNotFound) => {
|
||||||
|
let _ = invoke
|
||||||
|
.respond(
|
||||||
|
&ctx,
|
||||||
|
CreateGenericResponse::new()
|
||||||
|
.content(format!("Macro \"{}\" not found", macro_name)),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(e) => {
|
||||||
|
panic!("{}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Delete a recorded macro
|
|
||||||
#[poise::command(slash_command, rename = "delete", check = "guild_only")]
|
|
||||||
pub async fn delete_macro(
|
|
||||||
ctx: Context<'_>,
|
|
||||||
#[description = "Name of macro to delete"]
|
|
||||||
#[autocomplete = "macro_name_autocomplete"]
|
|
||||||
name: String,
|
|
||||||
) -> Result<(), Error> {
|
|
||||||
match sqlx::query!(
|
|
||||||
"
|
|
||||||
SELECT id 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) => {
|
|
||||||
sqlx::query!("DELETE FROM macro WHERE id = ?", row.id)
|
|
||||||
.execute(&ctx.data().database)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
ctx.say(format!("Macro \"{}\" deleted", name)).await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Err(sqlx::Error::RowNotFound) => {
|
|
||||||
ctx.say(format!("Macro \"{}\" not found", name)).await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Err(e) => {
|
|
||||||
panic!("{}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn max_macro_page(macros: &[CommandMacro]) -> usize {
|
pub fn max_macro_page(macros: &[CommandMacro]) -> usize {
|
||||||
@ -440,30 +396,15 @@ pub fn max_macro_page(macros: &[CommandMacro]) -> usize {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn show_macro_page(macros: &[CommandMacro], page: usize) -> CreateReply {
|
pub fn show_macro_page(macros: &[CommandMacro], page: usize) -> CreateGenericResponse {
|
||||||
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);
|
let pager = MacroPager::new(page);
|
||||||
|
|
||||||
if macros.is_empty() {
|
if macros.is_empty() {
|
||||||
let mut reply = CreateReply::default();
|
return CreateGenericResponse::new().embed(|e| {
|
||||||
|
|
||||||
reply.embed(|e| {
|
|
||||||
e.title("Macros")
|
e.title("Macros")
|
||||||
.description("No Macros Set Up. Use `/macro record` to get started.")
|
.description("No Macros Set Up. Use `/macro record` to get started.")
|
||||||
.color(*THEME_COLOR)
|
.color(*THEME_COLOR)
|
||||||
});
|
});
|
||||||
|
|
||||||
return reply;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let pages = max_macro_page(macros);
|
let pages = max_macro_page(macros);
|
||||||
@ -506,9 +447,7 @@ pub fn show_macro_page(macros: &[CommandMacro], page: usize) -> CreateReply {
|
|||||||
|
|
||||||
let display = display_vec.join("\n");
|
let display = display_vec.join("\n");
|
||||||
|
|
||||||
let mut reply = CreateReply::default();
|
CreateGenericResponse::new()
|
||||||
|
|
||||||
reply
|
|
||||||
.embed(|e| {
|
.embed(|e| {
|
||||||
e.title("Macros")
|
e.title("Macros")
|
||||||
.description(display)
|
.description(display)
|
||||||
@ -519,8 +458,5 @@ pub fn show_macro_page(macros: &[CommandMacro], page: usize) -> CreateReply {
|
|||||||
pager.create_button_row(pages, comp);
|
pager.create_button_row(pages, comp);
|
||||||
|
|
||||||
comp
|
comp
|
||||||
});
|
})
|
||||||
|
|
||||||
reply
|
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
|
@ -322,7 +322,7 @@ async fn look(ctx: &Context, invoke: &mut CommandInvoke, args: CommandOptions) {
|
|||||||
.iter()
|
.iter()
|
||||||
.map(|reminder| reminder.display(&flags, &timezone))
|
.map(|reminder| reminder.display(&flags, &timezone))
|
||||||
.fold(0, |t, r| t + r.len())
|
.fold(0, |t, r| t + r.len())
|
||||||
.div_ceil(EMBED_DESCRIPTION_MAX_LENGTH);
|
.div_ceil(&EMBED_DESCRIPTION_MAX_LENGTH);
|
||||||
|
|
||||||
let pager = LookPager::new(flags, timezone);
|
let pager = LookPager::new(flags, timezone);
|
||||||
|
|
||||||
|
@ -3,7 +3,10 @@ pub(crate) mod pager;
|
|||||||
use std::io::Cursor;
|
use std::io::Cursor;
|
||||||
|
|
||||||
use chrono_tz::Tz;
|
use chrono_tz::Tz;
|
||||||
use poise::serenity::{
|
use num_integer::Integer;
|
||||||
|
use rmp_serde::Serializer;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serenity::{
|
||||||
builder::CreateEmbed,
|
builder::CreateEmbed,
|
||||||
client::Context,
|
client::Context,
|
||||||
model::{
|
model::{
|
||||||
@ -12,14 +15,18 @@ use poise::serenity::{
|
|||||||
prelude::InteractionApplicationCommandCallbackDataFlags,
|
prelude::InteractionApplicationCommandCallbackDataFlags,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use rmp_serde::Serializer;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
self,
|
commands::{
|
||||||
|
moderation_cmds::{max_macro_page, show_macro_page},
|
||||||
|
reminder_cmds::{max_delete_page, show_delete_page},
|
||||||
|
todo_cmds::{max_todo_page, show_todo_page},
|
||||||
|
},
|
||||||
component_models::pager::{DelPager, LookPager, MacroPager, Pager, TodoPager},
|
component_models::pager::{DelPager, LookPager, MacroPager, Pager, TodoPager},
|
||||||
consts::{EMBED_DESCRIPTION_MAX_LENGTH, THEME_COLOR},
|
consts::{EMBED_DESCRIPTION_MAX_LENGTH, THEME_COLOR},
|
||||||
|
framework::CommandInvoke,
|
||||||
models::{command_macro::CommandMacro, reminder::Reminder},
|
models::{command_macro::CommandMacro, reminder::Reminder},
|
||||||
|
SQLPool,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize)]
|
#[derive(Deserialize, Serialize)]
|
||||||
@ -72,7 +79,7 @@ impl ComponentDataModel {
|
|||||||
.iter()
|
.iter()
|
||||||
.map(|reminder| reminder.display(&flags, &pager.timezone))
|
.map(|reminder| reminder.display(&flags, &pager.timezone))
|
||||||
.fold(0, |t, r| t + r.len())
|
.fold(0, |t, r| t + r.len())
|
||||||
.div_ceil(EMBED_DESCRIPTION_MAX_LENGTH);
|
.div_ceil(&EMBED_DESCRIPTION_MAX_LENGTH);
|
||||||
|
|
||||||
let channel_name =
|
let channel_name =
|
||||||
if let Some(Channel::Guild(channel)) = channel_id.to_channel_cached(&ctx) {
|
if let Some(Channel::Guild(channel)) = channel_id.to_channel_cached(&ctx) {
|
||||||
|
@ -1,10 +1,8 @@
|
|||||||
// todo split pager out into a single struct
|
// todo split pager out into a single struct
|
||||||
use chrono_tz::Tz;
|
use chrono_tz::Tz;
|
||||||
use poise::serenity::{
|
|
||||||
builder::CreateComponents, model::interactions::message_component::ButtonStyle,
|
|
||||||
};
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_repr::*;
|
use serde_repr::*;
|
||||||
|
use serenity::{builder::CreateComponents, model::interactions::message_component::ButtonStyle};
|
||||||
|
|
||||||
use crate::{component_models::ComponentDataModel, models::reminder::look_flags::LookFlags};
|
use crate::{component_models::ComponentDataModel, models::reminder::look_flags::LookFlags};
|
||||||
|
|
||||||
|
@ -4,18 +4,21 @@ pub const MINUTE: u64 = 60;
|
|||||||
pub const EMBED_DESCRIPTION_MAX_LENGTH: usize = 4000;
|
pub const EMBED_DESCRIPTION_MAX_LENGTH: usize = 4000;
|
||||||
pub const SELECT_MAX_ENTRIES: usize = 25;
|
pub const SELECT_MAX_ENTRIES: usize = 25;
|
||||||
|
|
||||||
pub const MACRO_MAX_COMMANDS: usize = 5;
|
|
||||||
|
|
||||||
pub const CHARACTERS: &str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_";
|
pub const CHARACTERS: &str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_";
|
||||||
|
|
||||||
const THEME_COLOR_FALLBACK: u32 = 0x8fb677;
|
const THEME_COLOR_FALLBACK: u32 = 0x8fb677;
|
||||||
|
|
||||||
use std::{collections::HashSet, env, iter::FromIterator};
|
use std::{collections::HashSet, env, iter::FromIterator};
|
||||||
|
|
||||||
use poise::serenity::model::prelude::AttachmentType;
|
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
|
use serenity::http::AttachmentType;
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
|
pub static ref REMIND_INTERVAL: u64 = env::var("REMIND_INTERVAL")
|
||||||
|
.map(|inner| inner.parse::<u64>().ok())
|
||||||
|
.ok()
|
||||||
|
.flatten()
|
||||||
|
.unwrap_or(10);
|
||||||
pub static ref DEFAULT_AVATAR: AttachmentType<'static> = (
|
pub static ref DEFAULT_AVATAR: AttachmentType<'static> = (
|
||||||
include_bytes!(concat!(
|
include_bytes!(concat!(
|
||||||
env!("CARGO_MANIFEST_DIR"),
|
env!("CARGO_MANIFEST_DIR"),
|
||||||
|
@ -1,83 +0,0 @@
|
|||||||
use std::{collections::HashMap, env};
|
|
||||||
|
|
||||||
use poise::serenity::{client::Context, model::interactions::Interaction, utils::shard_id};
|
|
||||||
|
|
||||||
use crate::{Data, Error};
|
|
||||||
|
|
||||||
pub async fn listener(ctx: &Context, event: &poise::Event<'_>, data: &Data) -> Result<(), Error> {
|
|
||||||
match event {
|
|
||||||
poise::Event::ChannelDelete { channel } => {
|
|
||||||
sqlx::query!(
|
|
||||||
"
|
|
||||||
DELETE FROM channels WHERE channel = ?
|
|
||||||
",
|
|
||||||
channel.id.as_u64()
|
|
||||||
)
|
|
||||||
.execute(&data.database)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
poise::Event::GuildCreate { guild, is_new } => {
|
|
||||||
if *is_new {
|
|
||||||
let guild_id = guild.id.as_u64().to_owned();
|
|
||||||
|
|
||||||
sqlx::query!("INSERT INTO guilds (guild) VALUES (?)", guild_id)
|
|
||||||
.execute(&data.database)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
if let Ok(token) = env::var("DISCORDBOTS_TOKEN") {
|
|
||||||
let shard_count = ctx.cache.shard_count();
|
|
||||||
let current_shard_id = shard_id(guild_id, shard_count);
|
|
||||||
|
|
||||||
let guild_count = ctx
|
|
||||||
.cache
|
|
||||||
.guilds()
|
|
||||||
.iter()
|
|
||||||
.filter(|g| {
|
|
||||||
shard_id(g.as_u64().to_owned(), shard_count) == current_shard_id
|
|
||||||
})
|
|
||||||
.count() as u64;
|
|
||||||
|
|
||||||
let mut hm = HashMap::new();
|
|
||||||
hm.insert("server_count", guild_count);
|
|
||||||
hm.insert("shard_id", current_shard_id);
|
|
||||||
hm.insert("shard_count", shard_count);
|
|
||||||
|
|
||||||
let response = data
|
|
||||||
.http
|
|
||||||
.post(
|
|
||||||
format!(
|
|
||||||
"https://top.gg/api/bots/{}/stats",
|
|
||||||
ctx.cache.current_user_id().as_u64()
|
|
||||||
)
|
|
||||||
.as_str(),
|
|
||||||
)
|
|
||||||
.header("Authorization", token)
|
|
||||||
.json(&hm)
|
|
||||||
.send()
|
|
||||||
.await;
|
|
||||||
|
|
||||||
if let Err(res) = response {
|
|
||||||
println!("DiscordBots Response: {:?}", res);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
poise::Event::GuildDelete { incomplete, full } => {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
},
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
692
src/framework.rs
Normal file
692
src/framework.rs
Normal file
@ -0,0 +1,692 @@
|
|||||||
|
// 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
163
src/hooks.rs
163
src/hooks.rs
@ -1,77 +1,91 @@
|
|||||||
use poise::{serenity::model::channel::Channel, ApplicationCommandOrAutocompleteInteraction};
|
use regex_command_attr::check;
|
||||||
|
use serenity::{client::Context, model::channel::Channel};
|
||||||
|
|
||||||
use crate::{consts::MACRO_MAX_COMMANDS, models::command_macro::CommandOptions, Context, Error};
|
use crate::{
|
||||||
|
framework::{CommandInvoke, CommandOptions, CreateGenericResponse, HookResult},
|
||||||
|
moderation_cmds, RecordingMacros,
|
||||||
|
};
|
||||||
|
|
||||||
pub async fn guild_only(ctx: Context<'_>) -> Result<bool, Error> {
|
#[check]
|
||||||
if ctx.guild_id().is_some() {
|
pub async fn guild_only(
|
||||||
Ok(true)
|
ctx: &Context,
|
||||||
|
invoke: &mut CommandInvoke,
|
||||||
|
_args: &CommandOptions,
|
||||||
|
) -> HookResult {
|
||||||
|
if invoke.guild_id().is_some() {
|
||||||
|
HookResult::Continue
|
||||||
} else {
|
} else {
|
||||||
let _ = ctx.say("This command can only be used in servers").await;
|
let _ = invoke
|
||||||
|
.respond(
|
||||||
|
&ctx,
|
||||||
|
CreateGenericResponse::new().content("This command can only be used in servers"),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
Ok(false)
|
HookResult::Halt
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn macro_check(ctx: Context<'_>) -> bool {
|
#[check]
|
||||||
if let Context::Application(app_ctx) = ctx {
|
pub async fn macro_check(
|
||||||
if let ApplicationCommandOrAutocompleteInteraction::ApplicationCommand(interaction) =
|
ctx: &Context,
|
||||||
app_ctx.interaction
|
invoke: &mut CommandInvoke,
|
||||||
{
|
args: &CommandOptions,
|
||||||
if let Some(guild_id) = ctx.guild_id() {
|
) -> HookResult {
|
||||||
if ctx.command().identifying_name != "macro_finish" {
|
if let Some(guild_id) = invoke.guild_id() {
|
||||||
let mut lock = ctx.data().recording_macros.write().await;
|
if args.command != moderation_cmds::MACRO_CMD_COMMAND.names[0] {
|
||||||
|
let active_recordings =
|
||||||
|
ctx.data.read().await.get::<RecordingMacros>().cloned().unwrap();
|
||||||
|
let mut lock = active_recordings.write().await;
|
||||||
|
|
||||||
if let Some(command_macro) = lock.get_mut(&(guild_id, ctx.author().id)) {
|
if let Some(command_macro) = lock.get_mut(&(guild_id, invoke.author_id())) {
|
||||||
if command_macro.commands.len() >= MACRO_MAX_COMMANDS {
|
if command_macro.commands.len() >= 5 {
|
||||||
let _ = ctx.send(|m| {
|
let _ = invoke
|
||||||
m.ephemeral(true).content(
|
.respond(
|
||||||
"5 commands already recorded. Please use `/macro finish` to end recording.",
|
&ctx,
|
||||||
)
|
CreateGenericResponse::new().content("5 commands already recorded. Please use `/macro finish` to end recording."),
|
||||||
})
|
)
|
||||||
.await;
|
.await;
|
||||||
} else {
|
|
||||||
let mut command_options = CommandOptions::new(&ctx.command().name);
|
|
||||||
command_options.populate(&interaction);
|
|
||||||
|
|
||||||
command_macro.commands.push(command_options);
|
|
||||||
|
|
||||||
let _ = ctx
|
|
||||||
.send(|m| m.ephemeral(true).content("Command recorded to macro"))
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
|
|
||||||
false
|
|
||||||
} else {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
true
|
command_macro.commands.push(args.clone());
|
||||||
|
|
||||||
|
let _ = invoke
|
||||||
|
.respond(
|
||||||
|
&ctx,
|
||||||
|
CreateGenericResponse::new().content("Command recorded to macro"),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
HookResult::Halt
|
||||||
} else {
|
} else {
|
||||||
true
|
HookResult::Continue
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
true
|
HookResult::Continue
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
true
|
HookResult::Continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn check_self_permissions(ctx: Context<'_>) -> bool {
|
#[check]
|
||||||
if let Some(guild) = ctx.guild() {
|
pub async fn check_self_permissions(
|
||||||
let user_id = ctx.discord().cache.current_user_id();
|
ctx: &Context,
|
||||||
|
invoke: &mut CommandInvoke,
|
||||||
|
_args: &CommandOptions,
|
||||||
|
) -> HookResult {
|
||||||
|
if let Some(guild) = invoke.guild(&ctx) {
|
||||||
|
let user_id = ctx.cache.current_user_id();
|
||||||
|
|
||||||
let manage_webhooks = guild
|
let manage_webhooks =
|
||||||
.member_permissions(&ctx.discord(), user_id)
|
guild.member_permissions(&ctx, user_id).await.map_or(false, |p| p.manage_webhooks());
|
||||||
.await
|
let (view_channel, send_messages, embed_links) = invoke
|
||||||
.map_or(false, |p| p.manage_webhooks());
|
|
||||||
let (view_channel, send_messages, embed_links) = ctx
|
|
||||||
.channel_id()
|
.channel_id()
|
||||||
.to_channel_cached(&ctx.discord())
|
.to_channel_cached(&ctx)
|
||||||
.map(|c| {
|
.map(|c| {
|
||||||
if let Channel::Guild(channel) = c {
|
if let Channel::Guild(channel) = c {
|
||||||
channel.permissions_for_user(&ctx.discord(), user_id).ok()
|
channel.permissions_for_user(ctx, user_id).ok()
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
@ -82,11 +96,12 @@ async fn check_self_permissions(ctx: Context<'_>) -> bool {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if manage_webhooks && send_messages && embed_links {
|
if manage_webhooks && send_messages && embed_links {
|
||||||
true
|
HookResult::Continue
|
||||||
} else {
|
} else {
|
||||||
let _ = ctx
|
let _ = invoke
|
||||||
.send(|m| {
|
.respond(
|
||||||
m.content(format!(
|
&ctx,
|
||||||
|
CreateGenericResponse::new().content(format!(
|
||||||
"Please ensure the bot has the correct permissions:
|
"Please ensure the bot has the correct permissions:
|
||||||
|
|
||||||
{} **View Channel**
|
{} **View Channel**
|
||||||
@ -97,17 +112,41 @@ async fn check_self_permissions(ctx: Context<'_>) -> bool {
|
|||||||
if send_messages { "✅" } else { "❌" },
|
if send_messages { "✅" } else { "❌" },
|
||||||
if manage_webhooks { "✅" } else { "❌" },
|
if manage_webhooks { "✅" } else { "❌" },
|
||||||
if embed_links { "✅" } else { "❌" },
|
if embed_links { "✅" } else { "❌" },
|
||||||
))
|
)),
|
||||||
})
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
false
|
HookResult::Halt
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
true
|
HookResult::Continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn all_checks(ctx: Context<'_>) -> Result<bool, Error> {
|
#[check]
|
||||||
Ok(macro_check(ctx).await && check_self_permissions(ctx).await)
|
pub async fn check_guild_permissions(
|
||||||
|
ctx: &Context,
|
||||||
|
invoke: &mut CommandInvoke,
|
||||||
|
_args: &CommandOptions,
|
||||||
|
) -> HookResult {
|
||||||
|
if let Some(guild) = invoke.guild(&ctx) {
|
||||||
|
let permissions = guild.member_permissions(&ctx, invoke.author_id()).await.unwrap();
|
||||||
|
|
||||||
|
if !permissions.manage_guild() {
|
||||||
|
let _ = invoke
|
||||||
|
.respond(
|
||||||
|
&ctx,
|
||||||
|
CreateGenericResponse::new().content(
|
||||||
|
"You must have the \"Manage Server\" permission to use this command",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
HookResult::Halt
|
||||||
|
} else {
|
||||||
|
HookResult::Continue
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
HookResult::Continue
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
411
src/main.rs
411
src/main.rs
@ -3,45 +3,222 @@
|
|||||||
extern crate lazy_static;
|
extern crate lazy_static;
|
||||||
|
|
||||||
mod commands;
|
mod commands;
|
||||||
// mod component_models;
|
mod component_models;
|
||||||
mod consts;
|
mod consts;
|
||||||
mod event_handlers;
|
mod framework;
|
||||||
mod hooks;
|
mod hooks;
|
||||||
mod models;
|
mod models;
|
||||||
|
mod sender;
|
||||||
mod time_parser;
|
mod time_parser;
|
||||||
mod utils;
|
|
||||||
|
|
||||||
use std::{collections::HashMap, env};
|
use std::{
|
||||||
|
collections::HashMap,
|
||||||
|
env,
|
||||||
|
sync::{
|
||||||
|
atomic::{AtomicBool, Ordering},
|
||||||
|
Arc,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
use chrono_tz::Tz;
|
use chrono_tz::Tz;
|
||||||
use dotenv::dotenv;
|
use dotenv::dotenv;
|
||||||
use poise::serenity::model::{
|
use log::info;
|
||||||
gateway::{Activity, GatewayIntents},
|
use serenity::{
|
||||||
id::{GuildId, UserId},
|
async_trait,
|
||||||
|
client::{bridge::gateway::GatewayIntents, Client},
|
||||||
|
http::{client::Http, CacheHttp},
|
||||||
|
model::{
|
||||||
|
channel::GuildChannel,
|
||||||
|
gateway::{Activity, Ready},
|
||||||
|
guild::{Guild, GuildUnavailable},
|
||||||
|
id::{GuildId, UserId},
|
||||||
|
interactions::Interaction,
|
||||||
|
},
|
||||||
|
prelude::{Context, EventHandler, TypeMapKey},
|
||||||
|
utils::shard_id,
|
||||||
|
};
|
||||||
|
use sqlx::mysql::MySqlPool;
|
||||||
|
use tokio::{
|
||||||
|
sync::RwLock,
|
||||||
|
time::{Duration, Instant},
|
||||||
};
|
};
|
||||||
use sqlx::{MySql, Pool};
|
|
||||||
use tokio::sync::RwLock;
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
commands::{info_cmds, moderation_cmds},
|
commands::{info_cmds, moderation_cmds, reminder_cmds, todo_cmds},
|
||||||
consts::THEME_COLOR,
|
component_models::ComponentDataModel,
|
||||||
event_handlers::listener,
|
consts::{CNC_GUILD, REMIND_INTERVAL, SUBSCRIPTION_ROLES, THEME_COLOR},
|
||||||
hooks::all_checks,
|
framework::RegexFramework,
|
||||||
models::command_macro::CommandMacro,
|
models::command_macro::CommandMacro,
|
||||||
utils::register_application_commands,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
type Database = MySql;
|
struct SQLPool;
|
||||||
|
|
||||||
pub struct Data {
|
impl TypeMapKey for SQLPool {
|
||||||
database: Pool<Database>,
|
type Value = MySqlPool;
|
||||||
http: reqwest::Client,
|
|
||||||
recording_macros: RwLock<HashMap<(GuildId, UserId), CommandMacro>>,
|
|
||||||
popular_timezones: Vec<Tz>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Error = Box<dyn std::error::Error + Send + Sync>;
|
struct ReqwestClient;
|
||||||
type Context<'a> = poise::Context<'a, Data, Error>;
|
|
||||||
|
impl TypeMapKey for ReqwestClient {
|
||||||
|
type Value = Arc<reqwest::Client>;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct PopularTimezones;
|
||||||
|
|
||||||
|
impl TypeMapKey for PopularTimezones {
|
||||||
|
type Value = Arc<Vec<Tz>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct RecordingMacros;
|
||||||
|
|
||||||
|
impl TypeMapKey for RecordingMacros {
|
||||||
|
type Value = Arc<RwLock<HashMap<(GuildId, UserId), CommandMacro>>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Handler {
|
||||||
|
is_loop_running: AtomicBool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl EventHandler for Handler {
|
||||||
|
async fn cache_ready(&self, ctx_base: Context, _guilds: Vec<GuildId>) {
|
||||||
|
info!("Cache Ready!");
|
||||||
|
info!("Preparing to send reminders");
|
||||||
|
|
||||||
|
if !self.is_loop_running.load(Ordering::Relaxed) {
|
||||||
|
let ctx = ctx_base.clone();
|
||||||
|
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let pool = ctx.data.read().await.get::<SQLPool>().cloned().unwrap();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let sleep_until = Instant::now() + Duration::from_secs(*REMIND_INTERVAL);
|
||||||
|
let reminders = sender::Reminder::fetch_reminders(&pool).await;
|
||||||
|
|
||||||
|
if reminders.len() > 0 {
|
||||||
|
info!("Preparing to send {} reminders.", reminders.len());
|
||||||
|
|
||||||
|
for reminder in reminders {
|
||||||
|
reminder.send(pool.clone(), ctx.clone()).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tokio::time::sleep_until(sleep_until).await;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
self.is_loop_running.swap(true, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn channel_delete(&self, ctx: Context, channel: &GuildChannel) {
|
||||||
|
let pool = ctx
|
||||||
|
.data
|
||||||
|
.read()
|
||||||
|
.await
|
||||||
|
.get::<SQLPool>()
|
||||||
|
.cloned()
|
||||||
|
.expect("Could not get SQLPool from data");
|
||||||
|
|
||||||
|
sqlx::query!(
|
||||||
|
"
|
||||||
|
DELETE FROM channels WHERE channel = ?
|
||||||
|
",
|
||||||
|
channel.id.as_u64()
|
||||||
|
)
|
||||||
|
.execute(&pool)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn guild_create(&self, ctx: Context, guild: Guild, is_new: bool) {
|
||||||
|
if is_new {
|
||||||
|
let guild_id = guild.id.as_u64().to_owned();
|
||||||
|
|
||||||
|
{
|
||||||
|
let pool = ctx.data.read().await.get::<SQLPool>().cloned().unwrap();
|
||||||
|
|
||||||
|
let _ = sqlx::query!("INSERT INTO guilds (guild) VALUES (?)", guild_id)
|
||||||
|
.execute(&pool)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(token) = env::var("DISCORDBOTS_TOKEN") {
|
||||||
|
let shard_count = ctx.cache.shard_count();
|
||||||
|
let current_shard_id = shard_id(guild_id, shard_count);
|
||||||
|
|
||||||
|
let guild_count = ctx
|
||||||
|
.cache
|
||||||
|
.guilds()
|
||||||
|
.iter()
|
||||||
|
.filter(|g| shard_id(g.as_u64().to_owned(), shard_count) == current_shard_id)
|
||||||
|
.count() as u64;
|
||||||
|
|
||||||
|
let mut hm = HashMap::new();
|
||||||
|
hm.insert("server_count", guild_count);
|
||||||
|
hm.insert("shard_id", current_shard_id);
|
||||||
|
hm.insert("shard_count", shard_count);
|
||||||
|
|
||||||
|
let client = ctx
|
||||||
|
.data
|
||||||
|
.read()
|
||||||
|
.await
|
||||||
|
.get::<ReqwestClient>()
|
||||||
|
.cloned()
|
||||||
|
.expect("Could not get ReqwestClient from data");
|
||||||
|
|
||||||
|
let response = client
|
||||||
|
.post(
|
||||||
|
format!(
|
||||||
|
"https://top.gg/api/bots/{}/stats",
|
||||||
|
ctx.cache.current_user_id().as_u64()
|
||||||
|
)
|
||||||
|
.as_str(),
|
||||||
|
)
|
||||||
|
.header("Authorization", token)
|
||||||
|
.json(&hm)
|
||||||
|
.send()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
if let Err(res) = response {
|
||||||
|
println!("DiscordBots Response: {:?}", res);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn guild_delete(&self, ctx: Context, incomplete: GuildUnavailable, _full: Option<Guild>) {
|
||||||
|
let pool = ctx.data.read().await.get::<SQLPool>().cloned().unwrap();
|
||||||
|
let _ = sqlx::query!("DELETE FROM guilds WHERE guild = ?", incomplete.id.0)
|
||||||
|
.execute(&pool)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn ready(&self, ctx: Context, _: Ready) {
|
||||||
|
ctx.set_activity(Activity::watching("for /remind")).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn interaction_create(&self, ctx: Context, interaction: Interaction) {
|
||||||
|
match interaction {
|
||||||
|
Interaction::ApplicationCommand(application_command) => {
|
||||||
|
let framework = ctx
|
||||||
|
.data
|
||||||
|
.read()
|
||||||
|
.await
|
||||||
|
.get::<RegexFramework>()
|
||||||
|
.cloned()
|
||||||
|
.expect("RegexFramework not found in context");
|
||||||
|
|
||||||
|
framework.execute(ctx, application_command).await;
|
||||||
|
}
|
||||||
|
Interaction::MessageComponent(component) => {
|
||||||
|
let component_model = ComponentDataModel::from_custom_id(&component.data.custom_id);
|
||||||
|
component_model.act(&ctx, component).await;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||||
@ -49,75 +226,141 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|||||||
|
|
||||||
dotenv()?;
|
dotenv()?;
|
||||||
|
|
||||||
let discord_token = env::var("DISCORD_TOKEN").expect("Missing DISCORD_TOKEN from environment");
|
let token = env::var("DISCORD_TOKEN").expect("Missing DISCORD_TOKEN from environment");
|
||||||
|
|
||||||
let options = poise::FrameworkOptions {
|
let application_id = {
|
||||||
commands: vec![
|
let http = Http::new_with_token(&token);
|
||||||
info_cmds::help(),
|
|
||||||
info_cmds::info(),
|
http.get_current_application_info().await?.id
|
||||||
info_cmds::donate(),
|
|
||||||
info_cmds::clock(),
|
|
||||||
info_cmds::dashboard(),
|
|
||||||
moderation_cmds::timezone(),
|
|
||||||
poise::Command {
|
|
||||||
subcommands: vec![
|
|
||||||
moderation_cmds::delete_macro(),
|
|
||||||
moderation_cmds::finish_macro(),
|
|
||||||
moderation_cmds::list_macro(),
|
|
||||||
moderation_cmds::record_macro(),
|
|
||||||
moderation_cmds::run_macro(),
|
|
||||||
],
|
|
||||||
..moderation_cmds::macro_base()
|
|
||||||
},
|
|
||||||
],
|
|
||||||
allowed_mentions: None,
|
|
||||||
command_check: Some(|ctx| Box::pin(all_checks(ctx))),
|
|
||||||
listener: |ctx, event, _framework, data| Box::pin(listener(ctx, event, data)),
|
|
||||||
..Default::default()
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let database =
|
let dm_enabled = env::var("DM_ENABLED").map_or(true, |var| var == "1");
|
||||||
Pool::connect(&env::var("DATABASE_URL").expect("No database URL provided")).await.unwrap();
|
|
||||||
|
|
||||||
let popular_timezones = sqlx::query!(
|
let framework = RegexFramework::new()
|
||||||
"
|
.ignore_bots(env::var("IGNORE_BOTS").map_or(true, |var| var == "1"))
|
||||||
SELECT timezone FROM users GROUP BY timezone ORDER BY COUNT(timezone) DESC LIMIT 21"
|
.debug_guild(env::var("DEBUG_GUILD").map_or(None, |g| {
|
||||||
)
|
Some(GuildId(g.parse::<u64>().expect("DEBUG_GUILD must be a guild ID")))
|
||||||
.fetch_all(&database)
|
}))
|
||||||
.await
|
.dm_enabled(dm_enabled)
|
||||||
.unwrap()
|
// info commands
|
||||||
.iter()
|
.add_command(&info_cmds::HELP_COMMAND)
|
||||||
.map(|t| t.timezone.parse::<Tz>().unwrap())
|
.add_command(&info_cmds::INFO_COMMAND)
|
||||||
.collect::<Vec<Tz>>();
|
.add_command(&info_cmds::DONATE_COMMAND)
|
||||||
|
.add_command(&info_cmds::DASHBOARD_COMMAND)
|
||||||
|
.add_command(&info_cmds::CLOCK_COMMAND)
|
||||||
|
// reminder commands
|
||||||
|
.add_command(&reminder_cmds::TIMER_COMMAND)
|
||||||
|
.add_command(&reminder_cmds::REMIND_COMMAND)
|
||||||
|
// management commands
|
||||||
|
.add_command(&reminder_cmds::DELETE_COMMAND)
|
||||||
|
.add_command(&reminder_cmds::LOOK_COMMAND)
|
||||||
|
.add_command(&reminder_cmds::PAUSE_COMMAND)
|
||||||
|
.add_command(&reminder_cmds::OFFSET_COMMAND)
|
||||||
|
.add_command(&reminder_cmds::NUDGE_COMMAND)
|
||||||
|
// to-do commands
|
||||||
|
.add_command(&todo_cmds::TODO_COMMAND)
|
||||||
|
// moderation commands
|
||||||
|
.add_command(&moderation_cmds::TIMEZONE_COMMAND)
|
||||||
|
.add_command(&moderation_cmds::MACRO_CMD_COMMAND)
|
||||||
|
.add_hook(&hooks::CHECK_SELF_PERMISSIONS_HOOK)
|
||||||
|
.add_hook(&hooks::MACRO_CHECK_HOOK);
|
||||||
|
|
||||||
poise::Framework::build()
|
let framework_arc = Arc::new(framework);
|
||||||
.token(discord_token)
|
|
||||||
.user_data_setup(move |ctx, _bot, framework| {
|
|
||||||
Box::pin(async move {
|
|
||||||
ctx.set_activity(Activity::watching("for /remind")).await;
|
|
||||||
|
|
||||||
register_application_commands(
|
let mut client = Client::builder(&token)
|
||||||
ctx,
|
.intents(GatewayIntents::GUILDS)
|
||||||
framework,
|
.application_id(application_id.0)
|
||||||
env::var("DEBUG_GUILD")
|
.event_handler(Handler { is_loop_running: AtomicBool::from(false) })
|
||||||
.map(|inner| GuildId(inner.parse().expect("DEBUG_GUILD not valid")))
|
.await
|
||||||
.ok(),
|
.expect("Error occurred creating client");
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
Ok(Data {
|
{
|
||||||
http: reqwest::Client::new(),
|
let pool = MySqlPool::connect(
|
||||||
database,
|
&env::var("DATABASE_URL").expect("Missing DATABASE_URL from environment"),
|
||||||
popular_timezones,
|
)
|
||||||
recording_macros: Default::default(),
|
.await
|
||||||
})
|
.unwrap();
|
||||||
})
|
|
||||||
})
|
let popular_timezones = sqlx::query!(
|
||||||
.options(options)
|
"SELECT timezone FROM users GROUP BY timezone ORDER BY COUNT(timezone) DESC LIMIT 21"
|
||||||
.client_settings(move |client_builder| client_builder.intents(GatewayIntents::GUILDS))
|
)
|
||||||
.run_autosharded()
|
.fetch_all(&pool)
|
||||||
.await?;
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.iter()
|
||||||
|
.map(|t| t.timezone.parse::<Tz>().unwrap())
|
||||||
|
.collect::<Vec<Tz>>();
|
||||||
|
|
||||||
|
let mut data = client.data.write().await;
|
||||||
|
|
||||||
|
data.insert::<SQLPool>(pool);
|
||||||
|
data.insert::<PopularTimezones>(Arc::new(popular_timezones));
|
||||||
|
data.insert::<ReqwestClient>(Arc::new(reqwest::Client::new()));
|
||||||
|
data.insert::<RegexFramework>(framework_arc.clone());
|
||||||
|
data.insert::<RecordingMacros>(Arc::new(RwLock::new(HashMap::new())));
|
||||||
|
}
|
||||||
|
|
||||||
|
framework_arc.build_slash(&client.cache_and_http.http).await;
|
||||||
|
|
||||||
|
if let Ok((Some(lower), Some(upper))) = env::var("SHARD_RANGE").map(|sr| {
|
||||||
|
let mut split =
|
||||||
|
sr.split(',').map(|val| val.parse::<u64>().expect("SHARD_RANGE not an integer"));
|
||||||
|
|
||||||
|
(split.next(), split.next())
|
||||||
|
}) {
|
||||||
|
let total_shards = env::var("SHARD_COUNT")
|
||||||
|
.map(|shard_count| shard_count.parse::<u64>().ok())
|
||||||
|
.ok()
|
||||||
|
.flatten()
|
||||||
|
.expect("No SHARD_COUNT provided, but SHARD_RANGE was provided");
|
||||||
|
|
||||||
|
assert!(lower < upper, "SHARD_RANGE lower limit is not less than the upper limit");
|
||||||
|
|
||||||
|
info!("Starting client fragment with shards {}-{}/{}", lower, upper, total_shards);
|
||||||
|
|
||||||
|
client.start_shard_range([lower, upper], total_shards).await?;
|
||||||
|
} else if let Ok(total_shards) = env::var("SHARD_COUNT")
|
||||||
|
.map(|shard_count| shard_count.parse::<u64>().expect("SHARD_COUNT not an integer"))
|
||||||
|
{
|
||||||
|
info!("Starting client with {} shards", total_shards);
|
||||||
|
|
||||||
|
client.start_shards(total_shards).await?;
|
||||||
|
} else {
|
||||||
|
info!("Starting client as autosharded");
|
||||||
|
|
||||||
|
client.start_autosharded().await?;
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn check_subscription(cache_http: impl CacheHttp, user_id: impl Into<UserId>) -> bool {
|
||||||
|
if let Some(subscription_guild) = *CNC_GUILD {
|
||||||
|
let guild_member = GuildId(subscription_guild).member(cache_http, user_id).await;
|
||||||
|
|
||||||
|
if let Ok(member) = guild_member {
|
||||||
|
for role in member.roles {
|
||||||
|
if SUBSCRIPTION_ROLES.contains(role.as_u64()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn check_guild_subscription(
|
||||||
|
cache_http: impl CacheHttp,
|
||||||
|
guild_id: impl Into<GuildId>,
|
||||||
|
) -> bool {
|
||||||
|
if let Some(guild) = cache_http.cache().unwrap().guild(guild_id) {
|
||||||
|
let owner = guild.owner_id;
|
||||||
|
|
||||||
|
check_subscription(&cache_http, owner).await
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
use chrono::NaiveDateTime;
|
use chrono::NaiveDateTime;
|
||||||
use poise::serenity::model::channel::Channel;
|
use serenity::model::channel::Channel;
|
||||||
use sqlx::MySqlPool;
|
use sqlx::MySqlPool;
|
||||||
|
|
||||||
pub struct ChannelData {
|
pub struct ChannelData {
|
||||||
|
@ -1,24 +1,6 @@
|
|||||||
use std::collections::HashMap;
|
use serenity::{client::Context, model::id::GuildId};
|
||||||
|
|
||||||
use poise::{
|
use crate::{framework::CommandOptions, SQLPool};
|
||||||
serenity::{
|
|
||||||
json::Value,
|
|
||||||
model::{
|
|
||||||
id::{ChannelId, GuildId, RoleId, UserId},
|
|
||||||
interactions::application_command::{
|
|
||||||
ApplicationCommandInteraction, ApplicationCommandInteractionData,
|
|
||||||
ApplicationCommandInteractionDataOption, ApplicationCommandOptionType,
|
|
||||||
ApplicationCommandType,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
ApplicationCommandOrAutocompleteInteraction,
|
|
||||||
};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use serde_json::Number;
|
|
||||||
use sqlx::Executor;
|
|
||||||
|
|
||||||
use crate::Database;
|
|
||||||
|
|
||||||
pub struct CommandMacro {
|
pub struct CommandMacro {
|
||||||
pub guild_id: GuildId,
|
pub guild_id: GuildId,
|
||||||
@ -28,17 +10,15 @@ pub struct CommandMacro {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl CommandMacro {
|
impl CommandMacro {
|
||||||
pub async fn from_guild(
|
pub async fn from_guild(ctx: &Context, guild_id: impl Into<GuildId>) -> Vec<Self> {
|
||||||
db_pool: impl Executor<'_, Database = Database>,
|
let pool = ctx.data.read().await.get::<SQLPool>().cloned().unwrap();
|
||||||
guild_id: impl Into<GuildId>,
|
|
||||||
) -> Vec<Self> {
|
|
||||||
let guild_id = guild_id.into();
|
let guild_id = guild_id.into();
|
||||||
|
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
"SELECT * FROM macro WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?)",
|
"SELECT * FROM macro WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?)",
|
||||||
guild_id.0
|
guild_id.0
|
||||||
)
|
)
|
||||||
.fetch_all(db_pool)
|
.fetch_all(&pool)
|
||||||
.await
|
.await
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.iter()
|
.iter()
|
||||||
@ -51,217 +31,3 @@ impl CommandMacro {
|
|||||||
.collect::<Vec<Self>>()
|
.collect::<Vec<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(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn as_value(&self) -> Value {
|
|
||||||
match self {
|
|
||||||
OptionValue::String(s) => Value::String(s.to_string()),
|
|
||||||
OptionValue::Integer(i) => Value::Number(i.to_owned().into()),
|
|
||||||
OptionValue::Boolean(b) => Value::Bool(b.to_owned()),
|
|
||||||
OptionValue::User(u) => Value::String(u.to_string()),
|
|
||||||
OptionValue::Channel(c) => Value::String(c.to_string()),
|
|
||||||
OptionValue::Role(r) => Value::String(r.to_string()),
|
|
||||||
OptionValue::Mentionable(m) => Value::String(m.to_string()),
|
|
||||||
OptionValue::Number(n) => Value::Number(Number::from_f64(n.to_owned()).unwrap()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn kind(&self) -> ApplicationCommandOptionType {
|
|
||||||
match self {
|
|
||||||
OptionValue::String(_) => ApplicationCommandOptionType::String,
|
|
||||||
OptionValue::Integer(_) => ApplicationCommandOptionType::Integer,
|
|
||||||
OptionValue::Boolean(_) => ApplicationCommandOptionType::Boolean,
|
|
||||||
OptionValue::User(_) => ApplicationCommandOptionType::User,
|
|
||||||
OptionValue::Channel(_) => ApplicationCommandOptionType::Channel,
|
|
||||||
OptionValue::Role(_) => ApplicationCommandOptionType::Role,
|
|
||||||
OptionValue::Mentionable(_) => ApplicationCommandOptionType::Mentionable,
|
|
||||||
OptionValue::Number(_) => ApplicationCommandOptionType::Number,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[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 Into<ApplicationCommandInteractionData> for CommandOptions {
|
|
||||||
fn into(self) -> ApplicationCommandInteractionData {
|
|
||||||
ApplicationCommandInteractionData {
|
|
||||||
name: self.command,
|
|
||||||
kind: ApplicationCommandType::ChatInput,
|
|
||||||
options: self
|
|
||||||
.options
|
|
||||||
.iter()
|
|
||||||
.map(|(name, value)| ApplicationCommandInteractionDataOption {
|
|
||||||
name: name.to_string(),
|
|
||||||
value: Some(value.as_value()),
|
|
||||||
kind: value.kind(),
|
|
||||||
options: vec![],
|
|
||||||
..Default::default()
|
|
||||||
})
|
|
||||||
.collect(),
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CommandOptions {
|
|
||||||
pub fn new(command: impl ToString) -> Self {
|
|
||||||
Self {
|
|
||||||
command: command.to_string(),
|
|
||||||
subcommand: None,
|
|
||||||
subcommand_group: None,
|
|
||||||
options: Default::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn populate(&mut self, interaction: &ApplicationCommandInteraction) {
|
|
||||||
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(), self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -5,47 +5,62 @@ pub mod timer;
|
|||||||
pub mod user_data;
|
pub mod user_data;
|
||||||
|
|
||||||
use chrono_tz::Tz;
|
use chrono_tz::Tz;
|
||||||
use poise::serenity::{async_trait, model::id::UserId};
|
use serenity::{
|
||||||
|
async_trait,
|
||||||
|
model::id::{ChannelId, UserId},
|
||||||
|
prelude::Context,
|
||||||
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
models::{channel_data::ChannelData, user_data::UserData},
|
models::{channel_data::ChannelData, user_data::UserData},
|
||||||
Context,
|
SQLPool,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait CtxData {
|
pub trait CtxData {
|
||||||
async fn user_data<U: Into<UserId> + Send>(
|
async fn user_data<U: Into<UserId> + Send + Sync>(
|
||||||
&self,
|
&self,
|
||||||
user_id: U,
|
user_id: U,
|
||||||
) -> Result<UserData, Box<dyn std::error::Error + Sync + Send>>;
|
) -> Result<UserData, Box<dyn std::error::Error + Sync + Send>>;
|
||||||
|
|
||||||
async fn author_data(&self) -> Result<UserData, Box<dyn std::error::Error + Sync + Send>>;
|
async fn timezone<U: Into<UserId> + Send + Sync>(&self, user_id: U) -> Tz;
|
||||||
|
|
||||||
async fn timezone(&self) -> Tz;
|
async fn channel_data<C: Into<ChannelId> + Send + Sync>(
|
||||||
|
&self,
|
||||||
async fn channel_data(&self) -> Result<ChannelData, Box<dyn std::error::Error + Sync + Send>>;
|
channel_id: C,
|
||||||
|
) -> Result<ChannelData, Box<dyn std::error::Error + Sync + Send>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl CtxData for Context<'_> {
|
impl CtxData for Context {
|
||||||
async fn user_data<U: Into<UserId> + Send>(
|
async fn user_data<U: Into<UserId> + Send + Sync>(
|
||||||
&self,
|
&self,
|
||||||
user_id: U,
|
user_id: U,
|
||||||
) -> Result<UserData, Box<dyn std::error::Error + Sync + Send>> {
|
) -> Result<UserData, Box<dyn std::error::Error + Sync + Send>> {
|
||||||
UserData::from_user(user_id, &self.discord(), &self.data().database).await
|
let user_id = user_id.into();
|
||||||
|
let pool = self.data.read().await.get::<SQLPool>().cloned().unwrap();
|
||||||
|
|
||||||
|
let user = user_id.to_user(self).await.unwrap();
|
||||||
|
|
||||||
|
UserData::from_user(&user, &self, &pool).await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn author_data(&self) -> Result<UserData, Box<dyn std::error::Error + Sync + Send>> {
|
async fn timezone<U: Into<UserId> + Send + Sync>(&self, user_id: U) -> Tz {
|
||||||
UserData::from_user(&self.author().id, &self.discord(), &self.data().database).await
|
let user_id = user_id.into();
|
||||||
|
let pool = self.data.read().await.get::<SQLPool>().cloned().unwrap();
|
||||||
|
|
||||||
|
UserData::timezone_of(user_id, &pool).await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn timezone(&self) -> Tz {
|
async fn channel_data<C: Into<ChannelId> + Send + Sync>(
|
||||||
UserData::timezone_of(self.author().id, &self.data().database).await
|
&self,
|
||||||
}
|
channel_id: C,
|
||||||
|
) -> Result<ChannelData, Box<dyn std::error::Error + Sync + Send>> {
|
||||||
|
let channel_id = channel_id.into();
|
||||||
|
let pool = self.data.read().await.get::<SQLPool>().cloned().unwrap();
|
||||||
|
|
||||||
async fn channel_data(&self) -> Result<ChannelData, Box<dyn std::error::Error + Sync + Send>> {
|
let channel = channel_id.to_channel_cached(&self).unwrap();
|
||||||
let channel = self.channel_id().to_channel_cached(&self.discord()).unwrap();
|
|
||||||
|
|
||||||
ChannelData::from_channel(&channel, &self.data().database).await
|
ChannelData::from_channel(&channel, &pool).await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,8 @@ use std::{collections::HashSet, fmt::Display};
|
|||||||
|
|
||||||
use chrono::{Duration, NaiveDateTime, Utc};
|
use chrono::{Duration, NaiveDateTime, Utc};
|
||||||
use chrono_tz::Tz;
|
use chrono_tz::Tz;
|
||||||
use poise::serenity::{
|
use serenity::{
|
||||||
|
client::Context,
|
||||||
http::CacheHttp,
|
http::CacheHttp,
|
||||||
model::{
|
model::{
|
||||||
channel::GuildChannel,
|
channel::GuildChannel,
|
||||||
@ -14,13 +15,14 @@ use poise::serenity::{
|
|||||||
use sqlx::MySqlPool;
|
use sqlx::MySqlPool;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
consts::{DEFAULT_AVATAR, MAX_TIME, MIN_INTERVAL},
|
consts,
|
||||||
|
consts::{MAX_TIME, MIN_INTERVAL},
|
||||||
models::{
|
models::{
|
||||||
channel_data::ChannelData,
|
channel_data::ChannelData,
|
||||||
reminder::{content::Content, errors::ReminderError, helper::generate_uid, Reminder},
|
reminder::{content::Content, errors::ReminderError, helper::generate_uid, Reminder},
|
||||||
user_data::UserData,
|
user_data::UserData,
|
||||||
},
|
},
|
||||||
Context,
|
SQLPool,
|
||||||
};
|
};
|
||||||
|
|
||||||
async fn create_webhook(
|
async fn create_webhook(
|
||||||
@ -28,7 +30,7 @@ async fn create_webhook(
|
|||||||
channel: GuildChannel,
|
channel: GuildChannel,
|
||||||
name: impl Display,
|
name: impl Display,
|
||||||
) -> SerenityResult<Webhook> {
|
) -> SerenityResult<Webhook> {
|
||||||
channel.create_webhook_with_avatar(ctx.http(), name, DEFAULT_AVATAR.clone()).await
|
channel.create_webhook_with_avatar(ctx.http(), name, consts::DEFAULT_AVATAR.clone()).await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Hash, PartialEq, Eq)]
|
#[derive(Hash, PartialEq, Eq)]
|
||||||
@ -138,12 +140,12 @@ pub struct MultiReminderBuilder<'a> {
|
|||||||
expires: Option<NaiveDateTime>,
|
expires: Option<NaiveDateTime>,
|
||||||
content: Content,
|
content: Content,
|
||||||
set_by: Option<u32>,
|
set_by: Option<u32>,
|
||||||
ctx: &'a Context<'a>,
|
ctx: &'a Context,
|
||||||
guild_id: Option<GuildId>,
|
guild_id: Option<GuildId>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> MultiReminderBuilder<'a> {
|
impl<'a> MultiReminderBuilder<'a> {
|
||||||
pub fn new(ctx: &'a Context<'a>, guild_id: Option<GuildId>) -> Self {
|
pub fn new(ctx: &'a Context, guild_id: Option<GuildId>) -> Self {
|
||||||
MultiReminderBuilder {
|
MultiReminderBuilder {
|
||||||
scopes: vec![],
|
scopes: vec![],
|
||||||
utc_time: Utc::now().naive_utc(),
|
utc_time: Utc::now().naive_utc(),
|
||||||
@ -197,7 +199,7 @@ impl<'a> MultiReminderBuilder<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn build(self) -> (HashSet<ReminderError>, HashSet<ReminderScope>) {
|
pub async fn build(self) -> (HashSet<ReminderError>, HashSet<ReminderScope>) {
|
||||||
let pool = self.ctx.data().database.clone();
|
let pool = self.ctx.data.read().await.get::<SQLPool>().cloned().unwrap();
|
||||||
|
|
||||||
let mut errors = HashSet::new();
|
let mut errors = HashSet::new();
|
||||||
|
|
||||||
@ -211,13 +213,12 @@ impl<'a> MultiReminderBuilder<'a> {
|
|||||||
for scope in self.scopes {
|
for scope in self.scopes {
|
||||||
let db_channel_id = match scope {
|
let db_channel_id = match scope {
|
||||||
ReminderScope::User(user_id) => {
|
ReminderScope::User(user_id) => {
|
||||||
if let Ok(user) = UserId(user_id).to_user(&self.ctx.discord()).await {
|
if let Ok(user) = UserId(user_id).to_user(&self.ctx).await {
|
||||||
let user_data = UserData::from_user(&user, &self.ctx.discord(), &pool)
|
let user_data =
|
||||||
.await
|
UserData::from_user(&user, &self.ctx, &pool).await.unwrap();
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
if let Some(guild_id) = self.guild_id {
|
if let Some(guild_id) = self.guild_id {
|
||||||
if guild_id.member(&self.ctx.discord(), user).await.is_err() {
|
if guild_id.member(&self.ctx, user).await.is_err() {
|
||||||
Err(ReminderError::InvalidTag)
|
Err(ReminderError::InvalidTag)
|
||||||
} else {
|
} else {
|
||||||
Ok(user_data.dm_channel)
|
Ok(user_data.dm_channel)
|
||||||
@ -230,8 +231,7 @@ impl<'a> MultiReminderBuilder<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
ReminderScope::Channel(channel_id) => {
|
ReminderScope::Channel(channel_id) => {
|
||||||
let channel =
|
let channel = ChannelId(channel_id).to_channel(&self.ctx).await.unwrap();
|
||||||
ChannelId(channel_id).to_channel(&self.ctx.discord()).await.unwrap();
|
|
||||||
|
|
||||||
if let Some(guild_channel) = channel.clone().guild() {
|
if let Some(guild_channel) = channel.clone().guild() {
|
||||||
if Some(guild_channel.guild_id) != self.guild_id {
|
if Some(guild_channel.guild_id) != self.guild_id {
|
||||||
@ -243,12 +243,7 @@ impl<'a> MultiReminderBuilder<'a> {
|
|||||||
if channel_data.webhook_id.is_none()
|
if channel_data.webhook_id.is_none()
|
||||||
|| channel_data.webhook_token.is_none()
|
|| channel_data.webhook_token.is_none()
|
||||||
{
|
{
|
||||||
match create_webhook(
|
match create_webhook(&self.ctx, guild_channel, "Reminder").await
|
||||||
&self.ctx.discord(),
|
|
||||||
guild_channel,
|
|
||||||
"Reminder",
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
{
|
||||||
Ok(webhook) => {
|
Ok(webhook) => {
|
||||||
channel_data.webhook_id =
|
channel_data.webhook_id =
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
use poise::serenity::model::id::ChannelId;
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_repr::*;
|
use serde_repr::*;
|
||||||
|
use serenity::model::id::ChannelId;
|
||||||
|
|
||||||
#[derive(Serialize_repr, Deserialize_repr, Copy, Clone, Debug)]
|
#[derive(Serialize_repr, Deserialize_repr, Copy, Clone, Debug)]
|
||||||
#[repr(u8)]
|
#[repr(u8)]
|
||||||
|
@ -6,15 +6,18 @@ pub mod look_flags;
|
|||||||
|
|
||||||
use chrono::{NaiveDateTime, TimeZone};
|
use chrono::{NaiveDateTime, TimeZone};
|
||||||
use chrono_tz::Tz;
|
use chrono_tz::Tz;
|
||||||
use poise::serenity::model::id::{ChannelId, GuildId, UserId};
|
use serenity::{
|
||||||
use sqlx::{Executor, MySqlPool};
|
client::Context,
|
||||||
|
model::id::{ChannelId, GuildId, UserId},
|
||||||
|
};
|
||||||
|
use sqlx::MySqlPool;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
models::reminder::{
|
models::reminder::{
|
||||||
helper::longhand_displacement,
|
helper::longhand_displacement,
|
||||||
look_flags::{LookFlags, TimeDisplayType},
|
look_flags::{LookFlags, TimeDisplayType},
|
||||||
},
|
},
|
||||||
Context, Database,
|
SQLPool,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@ -68,10 +71,12 @@ WHERE
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn from_channel<C: Into<ChannelId>>(
|
pub async fn from_channel<C: Into<ChannelId>>(
|
||||||
db_pool: impl Executor<'_, Database = Database>,
|
ctx: &Context,
|
||||||
channel_id: C,
|
channel_id: C,
|
||||||
flags: &LookFlags,
|
flags: &LookFlags,
|
||||||
) -> Vec<Self> {
|
) -> Vec<Self> {
|
||||||
|
let pool = ctx.data.read().await.get::<SQLPool>().cloned().unwrap();
|
||||||
|
|
||||||
let enabled = if flags.show_disabled { "0,1" } else { "1" };
|
let enabled = if flags.show_disabled { "0,1" } else { "1" };
|
||||||
let channel_id = channel_id.into();
|
let channel_id = channel_id.into();
|
||||||
|
|
||||||
@ -108,21 +113,16 @@ ORDER BY
|
|||||||
channel_id.as_u64(),
|
channel_id.as_u64(),
|
||||||
enabled,
|
enabled,
|
||||||
)
|
)
|
||||||
.fetch_all(db_pool)
|
.fetch_all(&pool)
|
||||||
.await
|
.await
|
||||||
.unwrap()
|
.unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn from_guild(
|
pub async fn from_guild(ctx: &Context, guild_id: Option<GuildId>, user: UserId) -> Vec<Self> {
|
||||||
ctx: &Context<'_>,
|
let pool = ctx.data.read().await.get::<SQLPool>().cloned().unwrap();
|
||||||
guild_id: Option<GuildId>,
|
|
||||||
user: UserId,
|
|
||||||
) -> Vec<Self> {
|
|
||||||
// todo: see if this can be moved to just extract from the context
|
|
||||||
let pool = ctx.data().database.clone();
|
|
||||||
|
|
||||||
if let Some(guild_id) = guild_id {
|
if let Some(guild_id) = guild_id {
|
||||||
let guild_opt = guild_id.to_guild_cached(&ctx.discord());
|
let guild_opt = guild_id.to_guild_cached(&ctx);
|
||||||
|
|
||||||
if let Some(guild) = guild_opt {
|
if let Some(guild) = guild_opt {
|
||||||
let channels = guild
|
let channels = guild
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
use chrono_tz::Tz;
|
use chrono_tz::Tz;
|
||||||
use log::error;
|
use log::error;
|
||||||
use poise::serenity::{http::CacheHttp, model::id::UserId};
|
use serenity::{
|
||||||
|
http::CacheHttp,
|
||||||
|
model::{id::UserId, user::User},
|
||||||
|
};
|
||||||
use sqlx::MySqlPool;
|
use sqlx::MySqlPool;
|
||||||
|
|
||||||
use crate::consts::LOCAL_TIMEZONE;
|
use crate::consts::LOCAL_TIMEZONE;
|
||||||
@ -8,6 +11,7 @@ use crate::consts::LOCAL_TIMEZONE;
|
|||||||
pub struct UserData {
|
pub struct UserData {
|
||||||
pub id: u32,
|
pub id: u32,
|
||||||
pub user: u64,
|
pub user: u64,
|
||||||
|
pub name: String,
|
||||||
pub dm_channel: u32,
|
pub dm_channel: u32,
|
||||||
pub timezone: String,
|
pub timezone: String,
|
||||||
}
|
}
|
||||||
@ -36,20 +40,20 @@ SELECT timezone FROM users WHERE user = ?
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn from_user<U: Into<UserId>>(
|
pub async fn from_user(
|
||||||
user: U,
|
user: &User,
|
||||||
ctx: impl CacheHttp,
|
ctx: impl CacheHttp,
|
||||||
pool: &MySqlPool,
|
pool: &MySqlPool,
|
||||||
) -> Result<Self, Box<dyn std::error::Error + Sync + Send>> {
|
) -> Result<Self, Box<dyn std::error::Error + Sync + Send>> {
|
||||||
let user_id = user.into();
|
let user_id = user.id.as_u64().to_owned();
|
||||||
|
|
||||||
match sqlx::query_as_unchecked!(
|
match sqlx::query_as_unchecked!(
|
||||||
Self,
|
Self,
|
||||||
"
|
"
|
||||||
SELECT id, user, dm_channel, IF(timezone IS NULL, ?, timezone) AS timezone FROM users WHERE user = ?
|
SELECT id, user, name, dm_channel, IF(timezone IS NULL, ?, timezone) AS timezone FROM users WHERE user = ?
|
||||||
",
|
",
|
||||||
*LOCAL_TIMEZONE,
|
*LOCAL_TIMEZONE,
|
||||||
user_id.0
|
user_id
|
||||||
)
|
)
|
||||||
.fetch_one(pool)
|
.fetch_one(pool)
|
||||||
.await
|
.await
|
||||||
@ -57,24 +61,27 @@ SELECT id, user, dm_channel, IF(timezone IS NULL, ?, timezone) AS timezone FROM
|
|||||||
Ok(c) => Ok(c),
|
Ok(c) => Ok(c),
|
||||||
|
|
||||||
Err(sqlx::Error::RowNotFound) => {
|
Err(sqlx::Error::RowNotFound) => {
|
||||||
let dm_channel = user_id.create_dm_channel(ctx).await?;
|
let dm_channel = user.create_dm_channel(ctx).await?;
|
||||||
|
let dm_id = dm_channel.id.as_u64().to_owned();
|
||||||
|
|
||||||
let pool_c = pool.clone();
|
let pool_c = pool.clone();
|
||||||
|
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
"
|
"
|
||||||
INSERT IGNORE INTO channels (channel) VALUES (?)
|
INSERT IGNORE INTO channels (channel) VALUES (?)
|
||||||
",
|
",
|
||||||
dm_channel.id.0
|
dm_id
|
||||||
)
|
)
|
||||||
.execute(&pool_c)
|
.execute(&pool_c)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
"
|
"
|
||||||
INSERT INTO users (user, dm_channel, timezone) VALUES (?, (SELECT id FROM channels WHERE channel = ?), ?)
|
INSERT INTO users (user, name, dm_channel, timezone) VALUES (?, ?, (SELECT id FROM channels WHERE channel = ?), ?)
|
||||||
",
|
",
|
||||||
user_id.0,
|
user_id,
|
||||||
dm_channel.id.0,
|
user.name,
|
||||||
|
dm_id,
|
||||||
*LOCAL_TIMEZONE
|
*LOCAL_TIMEZONE
|
||||||
)
|
)
|
||||||
.execute(&pool_c)
|
.execute(&pool_c)
|
||||||
@ -83,9 +90,9 @@ INSERT INTO users (user, dm_channel, timezone) VALUES (?, (SELECT id FROM channe
|
|||||||
Ok(sqlx::query_as_unchecked!(
|
Ok(sqlx::query_as_unchecked!(
|
||||||
Self,
|
Self,
|
||||||
"
|
"
|
||||||
SELECT id, user, dm_channel, timezone FROM users WHERE user = ?
|
SELECT id, user, name, dm_channel, timezone FROM users WHERE user = ?
|
||||||
",
|
",
|
||||||
user_id.0
|
user_id
|
||||||
)
|
)
|
||||||
.fetch_one(pool)
|
.fetch_one(pool)
|
||||||
.await?)
|
.await?)
|
||||||
@ -102,8 +109,9 @@ SELECT id, user, dm_channel, timezone FROM users WHERE user = ?
|
|||||||
pub async fn commit_changes(&self, pool: &MySqlPool) {
|
pub async fn commit_changes(&self, pool: &MySqlPool) {
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
"
|
"
|
||||||
UPDATE users SET timezone = ? WHERE id = ?
|
UPDATE users SET name = ?, timezone = ? WHERE id = ?
|
||||||
",
|
",
|
||||||
|
self.name,
|
||||||
self.timezone,
|
self.timezone,
|
||||||
self.id
|
self.id
|
||||||
)
|
)
|
||||||
|
552
src/sender.rs
Normal file
552
src/sender.rs
Normal file
@ -0,0 +1,552 @@
|
|||||||
|
use chrono::Duration;
|
||||||
|
use chrono_tz::Tz;
|
||||||
|
use log::{error, info, warn};
|
||||||
|
use num_integer::Integer;
|
||||||
|
use regex::{Captures, Regex};
|
||||||
|
use serenity::{
|
||||||
|
builder::CreateEmbed,
|
||||||
|
http::{CacheHttp, Http, StatusCode},
|
||||||
|
model::{
|
||||||
|
channel::{Channel, Embed as SerenityEmbed},
|
||||||
|
id::ChannelId,
|
||||||
|
webhook::Webhook,
|
||||||
|
},
|
||||||
|
Error, Result,
|
||||||
|
};
|
||||||
|
use sqlx::{
|
||||||
|
types::chrono::{NaiveDateTime, Utc},
|
||||||
|
MySqlPool,
|
||||||
|
};
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
pub static ref TIMEFROM_REGEX: Regex =
|
||||||
|
Regex::new(r#"<<timefrom:(?P<time>\d+):(?P<format>.+)?>>"#).unwrap();
|
||||||
|
pub static ref TIMENOW_REGEX: Regex =
|
||||||
|
Regex::new(r#"<<timenow:(?P<timezone>(?:\w|/|_)+):(?P<format>.+)?>>"#).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fmt_displacement(format: &str, seconds: u64) -> String {
|
||||||
|
let mut seconds = seconds;
|
||||||
|
let mut days: u64 = 0;
|
||||||
|
let mut hours: u64 = 0;
|
||||||
|
let mut minutes: u64 = 0;
|
||||||
|
|
||||||
|
for (rep, time_type, div) in
|
||||||
|
[("%d", &mut days, 86400), ("%h", &mut hours, 3600), ("%m", &mut minutes, 60)].iter_mut()
|
||||||
|
{
|
||||||
|
if format.contains(*rep) {
|
||||||
|
let (divided, new_seconds) = seconds.div_rem(&div);
|
||||||
|
|
||||||
|
**time_type = divided;
|
||||||
|
seconds = new_seconds;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
format
|
||||||
|
.replace("%s", &seconds.to_string())
|
||||||
|
.replace("%m", &minutes.to_string())
|
||||||
|
.replace("%h", &hours.to_string())
|
||||||
|
.replace("%d", &days.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn substitute(string: &str) -> String {
|
||||||
|
let new = TIMEFROM_REGEX.replace(string, |caps: &Captures| {
|
||||||
|
let final_time = caps.name("time").unwrap().as_str();
|
||||||
|
let format = caps.name("format").unwrap().as_str();
|
||||||
|
|
||||||
|
if let Ok(final_time) = final_time.parse::<i64>() {
|
||||||
|
let dt = NaiveDateTime::from_timestamp(final_time, 0);
|
||||||
|
let now = Utc::now().naive_utc();
|
||||||
|
|
||||||
|
let difference = {
|
||||||
|
if now < dt {
|
||||||
|
dt - Utc::now().naive_utc()
|
||||||
|
} else {
|
||||||
|
Utc::now().naive_utc() - dt
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fmt_displacement(format, difference.num_seconds() as u64)
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
TIMENOW_REGEX
|
||||||
|
.replace(&new, |caps: &Captures| {
|
||||||
|
let timezone = caps.name("timezone").unwrap().as_str();
|
||||||
|
|
||||||
|
println!("{}", timezone);
|
||||||
|
|
||||||
|
if let Ok(tz) = timezone.parse::<Tz>() {
|
||||||
|
let format = caps.name("format").unwrap().as_str();
|
||||||
|
let now = Utc::now().with_timezone(&tz);
|
||||||
|
|
||||||
|
now.format(format).to_string()
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Embed {
|
||||||
|
inner: EmbedInner,
|
||||||
|
fields: Vec<EmbedField>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct EmbedInner {
|
||||||
|
title: String,
|
||||||
|
description: String,
|
||||||
|
image_url: Option<String>,
|
||||||
|
thumbnail_url: Option<String>,
|
||||||
|
footer: String,
|
||||||
|
footer_url: Option<String>,
|
||||||
|
author: String,
|
||||||
|
author_url: Option<String>,
|
||||||
|
color: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct EmbedField {
|
||||||
|
title: String,
|
||||||
|
value: String,
|
||||||
|
inline: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Embed {
|
||||||
|
pub async fn from_id(pool: &MySqlPool, id: u32) -> Option<Self> {
|
||||||
|
let mut inner = sqlx::query_as_unchecked!(
|
||||||
|
EmbedInner,
|
||||||
|
"
|
||||||
|
SELECT
|
||||||
|
`embed_title` AS title,
|
||||||
|
`embed_description` AS description,
|
||||||
|
`embed_image_url` AS image_url,
|
||||||
|
`embed_thumbnail_url` AS thumbnail_url,
|
||||||
|
`embed_footer` AS footer,
|
||||||
|
`embed_footer_url` AS footer_url,
|
||||||
|
`embed_author` AS author,
|
||||||
|
`embed_author_url` AS author_url,
|
||||||
|
`embed_color` AS color
|
||||||
|
FROM
|
||||||
|
reminders
|
||||||
|
WHERE
|
||||||
|
`id` = ?
|
||||||
|
",
|
||||||
|
id
|
||||||
|
)
|
||||||
|
.fetch_one(&pool.clone())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
inner.title = substitute(&inner.title);
|
||||||
|
inner.description = substitute(&inner.description);
|
||||||
|
inner.footer = substitute(&inner.footer);
|
||||||
|
|
||||||
|
let mut fields = sqlx::query_as_unchecked!(
|
||||||
|
EmbedField,
|
||||||
|
"
|
||||||
|
SELECT
|
||||||
|
title,
|
||||||
|
value,
|
||||||
|
inline
|
||||||
|
FROM
|
||||||
|
embed_fields
|
||||||
|
WHERE
|
||||||
|
reminder_id = ?
|
||||||
|
",
|
||||||
|
id
|
||||||
|
)
|
||||||
|
.fetch_all(pool)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
fields.iter_mut().for_each(|mut field| {
|
||||||
|
field.title = substitute(&field.title);
|
||||||
|
field.value = substitute(&field.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
let e = Embed { inner, fields };
|
||||||
|
|
||||||
|
if e.has_content() {
|
||||||
|
Some(e)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn has_content(&self) -> bool {
|
||||||
|
if self.inner.title.is_empty()
|
||||||
|
&& self.inner.description.is_empty()
|
||||||
|
&& self.inner.image_url.is_none()
|
||||||
|
&& self.inner.thumbnail_url.is_none()
|
||||||
|
&& self.inner.footer.is_empty()
|
||||||
|
&& self.inner.footer_url.is_none()
|
||||||
|
&& self.inner.author.is_empty()
|
||||||
|
&& self.inner.author_url.is_none()
|
||||||
|
&& self.fields.is_empty()
|
||||||
|
{
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Into<CreateEmbed> for Embed {
|
||||||
|
fn into(self) -> CreateEmbed {
|
||||||
|
let mut c = CreateEmbed::default();
|
||||||
|
|
||||||
|
c.title(&self.inner.title)
|
||||||
|
.description(&self.inner.description)
|
||||||
|
.color(self.inner.color)
|
||||||
|
.author(|a| {
|
||||||
|
a.name(&self.inner.author);
|
||||||
|
|
||||||
|
if let Some(author_icon) = &self.inner.author_url {
|
||||||
|
a.icon_url(author_icon);
|
||||||
|
}
|
||||||
|
|
||||||
|
a
|
||||||
|
})
|
||||||
|
.footer(|f| {
|
||||||
|
f.text(&self.inner.footer);
|
||||||
|
|
||||||
|
if let Some(footer_icon) = &self.inner.footer_url {
|
||||||
|
f.icon_url(footer_icon);
|
||||||
|
}
|
||||||
|
|
||||||
|
f
|
||||||
|
});
|
||||||
|
|
||||||
|
for field in &self.fields {
|
||||||
|
c.field(&field.title, &field.value, field.inline);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(image_url) = &self.inner.image_url {
|
||||||
|
c.image(image_url);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(thumbnail_url) = &self.inner.thumbnail_url {
|
||||||
|
c.thumbnail(thumbnail_url);
|
||||||
|
}
|
||||||
|
|
||||||
|
c
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Reminder {
|
||||||
|
id: u32,
|
||||||
|
|
||||||
|
channel_id: u64,
|
||||||
|
webhook_id: Option<u64>,
|
||||||
|
webhook_token: Option<String>,
|
||||||
|
|
||||||
|
channel_paused: bool,
|
||||||
|
channel_paused_until: Option<NaiveDateTime>,
|
||||||
|
enabled: bool,
|
||||||
|
|
||||||
|
tts: bool,
|
||||||
|
pin: bool,
|
||||||
|
content: String,
|
||||||
|
attachment: Option<Vec<u8>>,
|
||||||
|
attachment_name: Option<String>,
|
||||||
|
|
||||||
|
utc_time: NaiveDateTime,
|
||||||
|
timezone: String,
|
||||||
|
restartable: bool,
|
||||||
|
expires: Option<NaiveDateTime>,
|
||||||
|
interval: Option<u32>,
|
||||||
|
|
||||||
|
avatar: Option<String>,
|
||||||
|
username: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Reminder {
|
||||||
|
pub async fn fetch_reminders(pool: &MySqlPool) -> Vec<Self> {
|
||||||
|
sqlx::query_as_unchecked!(
|
||||||
|
Reminder,
|
||||||
|
"
|
||||||
|
SELECT
|
||||||
|
reminders.`id` AS id,
|
||||||
|
|
||||||
|
channels.`channel` AS channel_id,
|
||||||
|
channels.`webhook_id` AS webhook_id,
|
||||||
|
channels.`webhook_token` AS webhook_token,
|
||||||
|
|
||||||
|
channels.`paused` AS channel_paused,
|
||||||
|
channels.`paused_until` AS channel_paused_until,
|
||||||
|
reminders.`enabled` AS enabled,
|
||||||
|
|
||||||
|
reminders.`tts` AS tts,
|
||||||
|
reminders.`pin` AS pin,
|
||||||
|
reminders.`content` AS content,
|
||||||
|
reminders.`attachment` AS attachment,
|
||||||
|
reminders.`attachment_name` AS attachment_name,
|
||||||
|
|
||||||
|
reminders.`utc_time` AS 'utc_time',
|
||||||
|
reminders.`timezone` AS timezone,
|
||||||
|
reminders.`restartable` AS restartable,
|
||||||
|
reminders.`expires` AS expires,
|
||||||
|
reminders.`interval` AS 'interval',
|
||||||
|
|
||||||
|
reminders.`avatar` AS avatar,
|
||||||
|
reminders.`username` AS username
|
||||||
|
FROM
|
||||||
|
reminders
|
||||||
|
INNER JOIN
|
||||||
|
channels
|
||||||
|
ON
|
||||||
|
reminders.channel_id = channels.id
|
||||||
|
WHERE
|
||||||
|
reminders.`utc_time` < NOW()
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.fetch_all(pool)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.into_iter()
|
||||||
|
.map(|mut rem| {
|
||||||
|
rem.content = substitute(&rem.content);
|
||||||
|
|
||||||
|
rem
|
||||||
|
})
|
||||||
|
.collect::<Vec<Self>>()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn reset_webhook(&self, pool: &MySqlPool) {
|
||||||
|
let _ = sqlx::query!(
|
||||||
|
"
|
||||||
|
UPDATE channels SET webhook_id = NULL, webhook_token = NULL WHERE channel = ?
|
||||||
|
",
|
||||||
|
self.channel_id
|
||||||
|
)
|
||||||
|
.execute(pool)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn refresh(&self, pool: &MySqlPool) {
|
||||||
|
if let Some(interval) = self.interval {
|
||||||
|
let now = Utc::now().naive_local();
|
||||||
|
let mut updated_reminder_time = self.utc_time;
|
||||||
|
|
||||||
|
while updated_reminder_time < now {
|
||||||
|
updated_reminder_time += Duration::seconds(interval as i64);
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.expires.map_or(false, |expires| {
|
||||||
|
NaiveDateTime::from_timestamp(updated_reminder_time.timestamp(), 0) > expires
|
||||||
|
}) {
|
||||||
|
self.force_delete(pool).await;
|
||||||
|
} else {
|
||||||
|
sqlx::query!(
|
||||||
|
"
|
||||||
|
UPDATE reminders SET `utc_time` = ? WHERE `id` = ?
|
||||||
|
",
|
||||||
|
updated_reminder_time,
|
||||||
|
self.id
|
||||||
|
)
|
||||||
|
.execute(pool)
|
||||||
|
.await
|
||||||
|
.expect(&format!("Could not update time on Reminder {}", self.id));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.force_delete(pool).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn force_delete(&self, pool: &MySqlPool) {
|
||||||
|
sqlx::query!(
|
||||||
|
"
|
||||||
|
DELETE FROM reminders WHERE `id` = ?
|
||||||
|
",
|
||||||
|
self.id
|
||||||
|
)
|
||||||
|
.execute(pool)
|
||||||
|
.await
|
||||||
|
.expect(&format!("Could not delete Reminder {}", self.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn pin_message<M: Into<u64>>(&self, message_id: M, http: impl AsRef<Http>) {
|
||||||
|
let _ = http.as_ref().pin_message(self.channel_id, message_id.into(), None).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn send(&self, pool: MySqlPool, cache_http: impl CacheHttp) {
|
||||||
|
async fn send_to_channel(
|
||||||
|
cache_http: impl CacheHttp,
|
||||||
|
reminder: &Reminder,
|
||||||
|
embed: Option<CreateEmbed>,
|
||||||
|
) -> Result<()> {
|
||||||
|
let channel = ChannelId(reminder.channel_id).to_channel(&cache_http).await;
|
||||||
|
|
||||||
|
match channel {
|
||||||
|
Ok(Channel::Guild(channel)) => {
|
||||||
|
match channel
|
||||||
|
.send_message(&cache_http, |m| {
|
||||||
|
m.content(&reminder.content).tts(reminder.tts);
|
||||||
|
|
||||||
|
if let (Some(attachment), Some(name)) =
|
||||||
|
(&reminder.attachment, &reminder.attachment_name)
|
||||||
|
{
|
||||||
|
m.add_file((attachment as &[u8], name.as_str()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(embed) = embed {
|
||||||
|
m.set_embed(embed);
|
||||||
|
}
|
||||||
|
|
||||||
|
m
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(m) => {
|
||||||
|
if reminder.pin {
|
||||||
|
reminder.pin_message(m.id, cache_http.http()).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
Err(e) => Err(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(Channel::Private(channel)) => {
|
||||||
|
match channel
|
||||||
|
.send_message(&cache_http.http(), |m| {
|
||||||
|
m.content(&reminder.content).tts(reminder.tts);
|
||||||
|
|
||||||
|
if let (Some(attachment), Some(name)) =
|
||||||
|
(&reminder.attachment, &reminder.attachment_name)
|
||||||
|
{
|
||||||
|
m.add_file((attachment as &[u8], name.as_str()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(embed) = embed {
|
||||||
|
m.set_embed(embed);
|
||||||
|
}
|
||||||
|
|
||||||
|
m
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(m) => {
|
||||||
|
if reminder.pin {
|
||||||
|
reminder.pin_message(m.id, cache_http.http()).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
Err(e) => Err(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => Err(e),
|
||||||
|
_ => Err(Error::Other("Channel not of valid type")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn send_to_webhook(
|
||||||
|
cache_http: impl CacheHttp,
|
||||||
|
reminder: &Reminder,
|
||||||
|
webhook: Webhook,
|
||||||
|
embed: Option<CreateEmbed>,
|
||||||
|
) -> Result<()> {
|
||||||
|
match webhook
|
||||||
|
.execute(&cache_http.http(), reminder.pin || reminder.restartable, |w| {
|
||||||
|
w.content(&reminder.content).tts(reminder.tts);
|
||||||
|
|
||||||
|
if let Some(username) = &reminder.username {
|
||||||
|
w.username(username);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(avatar) = &reminder.avatar {
|
||||||
|
w.avatar_url(avatar);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let (Some(attachment), Some(name)) =
|
||||||
|
(&reminder.attachment, &reminder.attachment_name)
|
||||||
|
{
|
||||||
|
w.add_file((attachment as &[u8], name.as_str()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(embed) = embed {
|
||||||
|
w.embeds(vec![SerenityEmbed::fake(|c| {
|
||||||
|
*c = embed;
|
||||||
|
c
|
||||||
|
})]);
|
||||||
|
}
|
||||||
|
|
||||||
|
w
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(m) => {
|
||||||
|
if reminder.pin {
|
||||||
|
if let Some(message) = m {
|
||||||
|
reminder.pin_message(message.id, cache_http.http()).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
Err(e) => Err(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.enabled
|
||||||
|
&& !(self.channel_paused
|
||||||
|
&& self
|
||||||
|
.channel_paused_until
|
||||||
|
.map_or(true, |inner| inner >= Utc::now().naive_local()))
|
||||||
|
{
|
||||||
|
let _ = sqlx::query!(
|
||||||
|
"
|
||||||
|
UPDATE `channels` SET paused = 0, paused_until = NULL WHERE `channel` = ?
|
||||||
|
",
|
||||||
|
self.channel_id
|
||||||
|
)
|
||||||
|
.execute(&pool.clone())
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let embed = Embed::from_id(&pool.clone(), self.id).await.map(|e| e.into());
|
||||||
|
|
||||||
|
let result = if let (Some(webhook_id), Some(webhook_token)) =
|
||||||
|
(self.webhook_id, &self.webhook_token)
|
||||||
|
{
|
||||||
|
let webhook_res =
|
||||||
|
cache_http.http().get_webhook_with_token(webhook_id, webhook_token).await;
|
||||||
|
|
||||||
|
if let Ok(webhook) = webhook_res {
|
||||||
|
send_to_webhook(cache_http, &self, webhook, embed).await
|
||||||
|
} else {
|
||||||
|
warn!("Webhook vanished: {:?}", webhook_res);
|
||||||
|
|
||||||
|
self.reset_webhook(&pool.clone()).await;
|
||||||
|
send_to_channel(cache_http, &self, embed).await
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
send_to_channel(cache_http, &self, embed).await
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(e) = result {
|
||||||
|
error!("Error sending {:?}: {:?}", self, e);
|
||||||
|
|
||||||
|
if let Error::Http(error) = e {
|
||||||
|
if error.status_code() == Some(StatusCode::from_u16(404).unwrap()) {
|
||||||
|
error!("Seeing channel is deleted. Removing reminder");
|
||||||
|
self.force_delete(&pool).await;
|
||||||
|
} else {
|
||||||
|
self.refresh(&pool).await;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.refresh(&pool).await;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.refresh(&pool).await;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
info!("Reminder {} is paused", self.id);
|
||||||
|
|
||||||
|
self.refresh(&pool).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
67
src/utils.rs
67
src/utils.rs
@ -1,67 +0,0 @@
|
|||||||
use poise::serenity::{
|
|
||||||
builder::CreateApplicationCommands,
|
|
||||||
http::CacheHttp,
|
|
||||||
model::id::{GuildId, UserId},
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
consts::{CNC_GUILD, SUBSCRIPTION_ROLES},
|
|
||||||
Data, Error,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub async fn register_application_commands(
|
|
||||||
ctx: &poise::serenity::client::Context,
|
|
||||||
framework: &poise::Framework<Data, Error>,
|
|
||||||
guild_id: Option<GuildId>,
|
|
||||||
) -> Result<(), poise::serenity::Error> {
|
|
||||||
let mut commands_builder = CreateApplicationCommands::default();
|
|
||||||
let commands = &framework.options().commands;
|
|
||||||
for command in commands {
|
|
||||||
if let Some(slash_command) = command.create_as_slash_command() {
|
|
||||||
commands_builder.add_application_command(slash_command);
|
|
||||||
}
|
|
||||||
if let Some(context_menu_command) = command.create_as_context_menu_command() {
|
|
||||||
commands_builder.add_application_command(context_menu_command);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let commands_builder = poise::serenity::json::Value::Array(commands_builder.0);
|
|
||||||
|
|
||||||
if let Some(guild_id) = guild_id {
|
|
||||||
ctx.http.create_guild_application_commands(guild_id.0, &commands_builder).await?;
|
|
||||||
} else {
|
|
||||||
ctx.http.create_global_application_commands(&commands_builder).await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn check_subscription(cache_http: impl CacheHttp, user_id: impl Into<UserId>) -> bool {
|
|
||||||
if let Some(subscription_guild) = *CNC_GUILD {
|
|
||||||
let guild_member = GuildId(subscription_guild).member(cache_http, user_id).await;
|
|
||||||
|
|
||||||
if let Ok(member) = guild_member {
|
|
||||||
for role in member.roles {
|
|
||||||
if SUBSCRIPTION_ROLES.contains(role.as_u64()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
false
|
|
||||||
} else {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn check_guild_subscription(
|
|
||||||
cache_http: impl CacheHttp,
|
|
||||||
guild_id: impl Into<GuildId>,
|
|
||||||
) -> bool {
|
|
||||||
if let Some(guild) = cache_http.cache().unwrap().guild(guild_id) {
|
|
||||||
let owner = guild.owner_id;
|
|
||||||
|
|
||||||
check_subscription(&cache_http, owner).await
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
Reference in New Issue
Block a user