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,57 +1,44 @@
 | 
				
			|||||||
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(
 | 
				
			||||||
 | 
					                        ctx.http.clone(),
 | 
				
			||||||
 | 
					                        CreateGenericResponse::new().embed(|e| {
 | 
				
			||||||
                            e.title("Timezone Set")
 | 
					                            e.title("Timezone Set")
 | 
				
			||||||
                                .description(format!(
 | 
					                                .description(format!(
 | 
				
			||||||
                                    "Timezone has been set to **{}**. Your current time should be `{}`",
 | 
					                                    "Timezone has been set to **{}**. Your current time should be `{}`",
 | 
				
			||||||
@@ -59,9 +46,9 @@ pub async fn timezone(
 | 
				
			|||||||
                                    now.format("%H:%M").to_string()
 | 
					                                    now.format("%H:%M").to_string()
 | 
				
			||||||
                                ))
 | 
					                                ))
 | 
				
			||||||
                                .color(*THEME_COLOR)
 | 
					                                .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(
 | 
				
			||||||
 | 
					                        ctx.http.clone(),
 | 
				
			||||||
 | 
					                        CreateGenericResponse::new().embed(|e| {
 | 
				
			||||||
                            e.title("Timezone Not Recognized")
 | 
					                            e.title("Timezone Not Recognized")
 | 
				
			||||||
                                .description("Possibly you meant one of the following timezones, otherwise click [here](https://gist.github.com/JellyWX/913dfc8b63d45192ad6cb54c829324ee):")
 | 
					                                .description("Possibly you meant one of the following timezones, otherwise click [here](https://gist.github.com/JellyWX/913dfc8b63d45192ad6cb54c829324ee):")
 | 
				
			||||||
                                .color(*THEME_COLOR)
 | 
					                                .color(*THEME_COLOR)
 | 
				
			||||||
                                .fields(fields)
 | 
					                                .fields(fields)
 | 
				
			||||||
                                .footer(|f| f.text(footer_text))
 | 
					                                .footer(|f| f.text(footer_text))
 | 
				
			||||||
                                .url("https://gist.github.com/JellyWX/913dfc8b63d45192ad6cb54c829324ee")
 | 
					                                .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,8 +100,10 @@ pub async fn timezone(
 | 
				
			|||||||
            )
 | 
					            )
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        ctx.send(|m| {
 | 
					        let _ = invoke
 | 
				
			||||||
            m.embed(|e| {
 | 
					            .respond(
 | 
				
			||||||
 | 
					                ctx.http.clone(),
 | 
				
			||||||
 | 
					                CreateGenericResponse::new().embed(|e| {
 | 
				
			||||||
                    e.title("Timezone Usage")
 | 
					                    e.title("Timezone Usage")
 | 
				
			||||||
                        .description(
 | 
					                        .description(
 | 
				
			||||||
                            "**Usage:**
 | 
					                            "**Usage:**
 | 
				
			||||||
@@ -125,137 +118,137 @@ You may want to use one of the popular timezones below, otherwise click [here](h
 | 
				
			|||||||
                        .fields(popular_timezones_iter)
 | 
					                        .fields(popular_timezones_iter)
 | 
				
			||||||
                        .footer(|f| f.text(footer_text))
 | 
					                        .footer(|f| f.text(footer_text))
 | 
				
			||||||
                        .url("https://gist.github.com/JellyWX/913dfc8b63d45192ad6cb54c829324ee")
 | 
					                        .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;
 | 
				
			||||||
    .await
 | 
					    }
 | 
				
			||||||
    .unwrap_or(vec![])
 | 
					 | 
				
			||||||
    .iter()
 | 
					 | 
				
			||||||
    .map(|s| s.name.clone())
 | 
					 | 
				
			||||||
    .collect()
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Record and replay command sequences
 | 
					#[command("macro")]
 | 
				
			||||||
#[poise::command(slash_command, rename = "macro", check = "guild_only")]
 | 
					#[description("Record and replay command sequences")]
 | 
				
			||||||
pub async fn macro_base(_ctx: Context<'_>) -> Result<(), Error> {
 | 
					#[subcommand("record")]
 | 
				
			||||||
    Ok(())
 | 
					#[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();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Start recording up to 5 commands to replay
 | 
					    match args.subcommand.clone().unwrap().as_str() {
 | 
				
			||||||
#[poise::command(slash_command, rename = "record", check = "guild_only")]
 | 
					        "record" => {
 | 
				
			||||||
pub async fn record_macro(
 | 
					            let guild_id = invoke.guild_id().unwrap();
 | 
				
			||||||
    ctx: Context<'_>,
 | 
					
 | 
				
			||||||
    #[description = "Name for the new macro"] name: String,
 | 
					            let name = args.get("name").unwrap().to_string();
 | 
				
			||||||
    #[description = "Description for the new macro"] description: Option<String>,
 | 
					 | 
				
			||||||
) -> Result<(), Error> {
 | 
					 | 
				
			||||||
    let guild_id = ctx.guild_id().unwrap();
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
            let row = sqlx::query!(
 | 
					            let row = sqlx::query!(
 | 
				
			||||||
        "
 | 
					                "SELECT 1 as _e FROM macro WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?) AND name = ?",
 | 
				
			||||||
SELECT 1 as _e FROM macro WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?) AND name = ?",
 | 
					 | 
				
			||||||
                guild_id.0,
 | 
					                guild_id.0,
 | 
				
			||||||
                name
 | 
					                name
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
    .fetch_one(&ctx.data().database)
 | 
					            .fetch_one(&pool)
 | 
				
			||||||
            .await;
 | 
					            .await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if row.is_ok() {
 | 
					            if row.is_ok() {
 | 
				
			||||||
        ctx.send(|m| {
 | 
					                let _ = invoke
 | 
				
			||||||
            m.ephemeral(true).embed(|e| {
 | 
					                    .respond(
 | 
				
			||||||
                e.title("Unique Name Required")
 | 
					                        &ctx,
 | 
				
			||||||
                    .description(
 | 
					                        CreateGenericResponse::new().ephemeral().embed(|e| {
 | 
				
			||||||
                        "A macro already exists under this name.
 | 
					                            e
 | 
				
			||||||
Please select a unique name for your macro.",
 | 
					                            .title("Unique Name Required")
 | 
				
			||||||
                    )
 | 
					                            .description("A macro already exists under this name. Please select a unique name for your macro.")
 | 
				
			||||||
                            .color(*THEME_COLOR)
 | 
					                            .color(*THEME_COLOR)
 | 
				
			||||||
            })
 | 
					                        }),
 | 
				
			||||||
        })
 | 
					                    )
 | 
				
			||||||
        .await?;
 | 
					                    .await;
 | 
				
			||||||
            } else {
 | 
					            } else {
 | 
				
			||||||
        let okay = {
 | 
					                let macro_buffer = ctx.data.read().await.get::<RecordingMacros>().cloned().unwrap();
 | 
				
			||||||
            let mut lock = ctx.data().recording_macros.write().await;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if lock.contains_key(&(guild_id, ctx.author().id)) {
 | 
					                let okay = {
 | 
				
			||||||
 | 
					                    let mut lock = macro_buffer.write().await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    if lock.contains_key(&(guild_id, invoke.author_id())) {
 | 
				
			||||||
                        false
 | 
					                        false
 | 
				
			||||||
                    } else {
 | 
					                    } else {
 | 
				
			||||||
                        lock.insert(
 | 
					                        lock.insert(
 | 
				
			||||||
                    (guild_id, ctx.author().id),
 | 
					                            (guild_id, invoke.author_id()),
 | 
				
			||||||
                    CommandMacro { guild_id, name, description, commands: vec![] },
 | 
					                            CommandMacro {
 | 
				
			||||||
 | 
					                                guild_id,
 | 
				
			||||||
 | 
					                                name,
 | 
				
			||||||
 | 
					                                description: args.get("description").map(|d| d.to_string()),
 | 
				
			||||||
 | 
					                                commands: vec![],
 | 
				
			||||||
 | 
					                            },
 | 
				
			||||||
                        );
 | 
					                        );
 | 
				
			||||||
                        true
 | 
					                        true
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                };
 | 
					                };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                if okay {
 | 
					                if okay {
 | 
				
			||||||
            ctx.send(|m| {
 | 
					                    let _ = invoke
 | 
				
			||||||
                m.ephemeral(true).embed(|e| {
 | 
					                        .respond(
 | 
				
			||||||
                    e.title("Macro Recording Started")
 | 
					                            &ctx,
 | 
				
			||||||
 | 
					                            CreateGenericResponse::new().ephemeral().embed(|e| {
 | 
				
			||||||
 | 
					                                e
 | 
				
			||||||
 | 
					                                .title("Macro Recording Started")
 | 
				
			||||||
                                .description(
 | 
					                                .description(
 | 
				
			||||||
"Run up to 5 commands, or type `/macro finish` to stop at any point.
 | 
					"Run up to 5 commands, or type `/macro finish` to stop at any point.
 | 
				
			||||||
Any commands ran as part of recording will be inconsequential",
 | 
					Any commands ran as part of recording will be inconsequential")
 | 
				
			||||||
                        )
 | 
					 | 
				
			||||||
                                .color(*THEME_COLOR)
 | 
					                                .color(*THEME_COLOR)
 | 
				
			||||||
                })
 | 
					                            }),
 | 
				
			||||||
            })
 | 
					                        )
 | 
				
			||||||
            .await?;
 | 
					                        .await;
 | 
				
			||||||
                } else {
 | 
					                } else {
 | 
				
			||||||
            ctx.send(|m| {
 | 
					                    let _ = invoke
 | 
				
			||||||
                m.ephemeral(true).embed(|e| {
 | 
					                        .respond(
 | 
				
			||||||
 | 
					                            &ctx,
 | 
				
			||||||
 | 
					                            CreateGenericResponse::new().ephemeral().embed(|e| {
 | 
				
			||||||
                                e.title("Macro Already Recording")
 | 
					                                e.title("Macro Already Recording")
 | 
				
			||||||
                                    .description(
 | 
					                                    .description(
 | 
				
			||||||
                                        "You are already recording a macro in this server.
 | 
					                                        "You are already recording a macro in this server.
 | 
				
			||||||
Please use `/macro finish` to end this recording before starting another.",
 | 
					Please use `/macro finish` to end this recording before starting another.",
 | 
				
			||||||
                                    )
 | 
					                                    )
 | 
				
			||||||
                                    .color(*THEME_COLOR)
 | 
					                                    .color(*THEME_COLOR)
 | 
				
			||||||
                })
 | 
					                            }),
 | 
				
			||||||
            })
 | 
					                        )
 | 
				
			||||||
            .await?;
 | 
					                        .await;
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					 | 
				
			||||||
    Ok(())
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					        "finish" => {
 | 
				
			||||||
/// Finish current macro recording
 | 
					            let key = (invoke.guild_id().unwrap(), invoke.author_id());
 | 
				
			||||||
#[poise::command(
 | 
					            let macro_buffer = ctx.data.read().await.get::<RecordingMacros>().cloned().unwrap();
 | 
				
			||||||
    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 lock = macro_buffer.read().await;
 | 
				
			||||||
                let contained = lock.get(&key);
 | 
					                let contained = lock.get(&key);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                if contained.map_or(true, |cmacro| cmacro.commands.is_empty()) {
 | 
					                if contained.map_or(true, |cmacro| cmacro.commands.is_empty()) {
 | 
				
			||||||
            ctx.send(|m| {
 | 
					                    let _ = invoke
 | 
				
			||||||
                m.embed(|e| {
 | 
					                        .respond(
 | 
				
			||||||
 | 
					                            &ctx,
 | 
				
			||||||
 | 
					                            CreateGenericResponse::new().embed(|e| {
 | 
				
			||||||
                                e.title("No Macro Recorded")
 | 
					                                e.title("No Macro Recorded")
 | 
				
			||||||
                                    .description("Use `/macro record` to start recording a macro")
 | 
					                                    .description("Use `/macro record` to start recording a macro")
 | 
				
			||||||
                                    .color(*THEME_COLOR)
 | 
					                                    .color(*THEME_COLOR)
 | 
				
			||||||
                })
 | 
					                            }),
 | 
				
			||||||
            })
 | 
					                        )
 | 
				
			||||||
            .await?;
 | 
					                        .await;
 | 
				
			||||||
                } else {
 | 
					                } else {
 | 
				
			||||||
                    let command_macro = contained.unwrap();
 | 
					                    let command_macro = contained.unwrap();
 | 
				
			||||||
                    let json = serde_json::to_string(&command_macro.commands).unwrap();
 | 
					                    let json = serde_json::to_string(&command_macro.commands).unwrap();
 | 
				
			||||||
@@ -267,153 +260,116 @@ pub async fn finish_macro(ctx: Context<'_>) -> Result<(), Error> {
 | 
				
			|||||||
                        command_macro.description,
 | 
					                        command_macro.description,
 | 
				
			||||||
                        json
 | 
					                        json
 | 
				
			||||||
                    )
 | 
					                    )
 | 
				
			||||||
                .execute(&ctx.data().database)
 | 
					                        .execute(&pool)
 | 
				
			||||||
                        .await
 | 
					                        .await
 | 
				
			||||||
                        .unwrap();
 | 
					                        .unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            ctx.send(|m| {
 | 
					                    let _ = invoke
 | 
				
			||||||
                m.embed(|e| {
 | 
					                        .respond(
 | 
				
			||||||
 | 
					                            &ctx,
 | 
				
			||||||
 | 
					                            CreateGenericResponse::new().embed(|e| {
 | 
				
			||||||
                                e.title("Macro Recorded")
 | 
					                                e.title("Macro Recorded")
 | 
				
			||||||
                                    .description("Use `/macro run` to execute the macro")
 | 
					                                    .description("Use `/macro run` to execute the macro")
 | 
				
			||||||
                                    .color(*THEME_COLOR)
 | 
					                                    .color(*THEME_COLOR)
 | 
				
			||||||
                })
 | 
					                            }),
 | 
				
			||||||
            })
 | 
					                        )
 | 
				
			||||||
            .await?;
 | 
					                        .await;
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
        let mut lock = ctx.data().recording_macros.write().await;
 | 
					                let mut lock = macro_buffer.write().await;
 | 
				
			||||||
                lock.remove(&key);
 | 
					                lock.remove(&key);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					 | 
				
			||||||
    Ok(())
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					        "list" => {
 | 
				
			||||||
/// List recorded macros
 | 
					            let macros = CommandMacro::from_guild(ctx, invoke.guild_id().unwrap()).await;
 | 
				
			||||||
#[poise::command(slash_command, rename = "list", check = "guild_only")]
 | 
					 | 
				
			||||||
pub async fn list_macro(ctx: Context<'_>) -> Result<(), Error> {
 | 
					 | 
				
			||||||
    let macros = CommandMacro::from_guild(&ctx.data().database, ctx.guild_id().unwrap()).await;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
            let resp = show_macro_page(¯os, 0);
 | 
					            let resp = show_macro_page(¯os, 0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    ctx.send(|m| {
 | 
					            invoke.respond(&ctx, resp).await.unwrap();
 | 
				
			||||||
        *m = resp;
 | 
					        }
 | 
				
			||||||
        m
 | 
					        "run" => {
 | 
				
			||||||
    })
 | 
					            let macro_name = args.get("name").unwrap().to_string();
 | 
				
			||||||
    .await?;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Ok(())
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
fn find_command<'a>(
 | 
					 | 
				
			||||||
    commands: &'a [poise::Command<Data, Error>],
 | 
					 | 
				
			||||||
    searching_name: &str,
 | 
					 | 
				
			||||||
    command_options: &CommandOptions,
 | 
					 | 
				
			||||||
) -> Option<&'a poise::Command<Data, Error>> {
 | 
					 | 
				
			||||||
    commands.iter().find_map(|cmd| {
 | 
					 | 
				
			||||||
        if searching_name != cmd.name {
 | 
					 | 
				
			||||||
            None
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
            if let Some(subgroup) = &command_options.subcommand_group {
 | 
					 | 
				
			||||||
                find_command(&cmd.subcommands, &subgroup, &command_options)
 | 
					 | 
				
			||||||
            } else if let Some(subcommand) = &command_options.subcommand {
 | 
					 | 
				
			||||||
                find_command(&cmd.subcommands, &subcommand, &command_options)
 | 
					 | 
				
			||||||
            } else {
 | 
					 | 
				
			||||||
                Some(cmd)
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    })
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/// Run a recorded macro
 | 
					 | 
				
			||||||
#[poise::command(slash_command, rename = "run", check = "guild_only")]
 | 
					 | 
				
			||||||
pub async fn run_macro(
 | 
					 | 
				
			||||||
    ctx: Context<'_>,
 | 
					 | 
				
			||||||
    #[description = "Name of macro to run"]
 | 
					 | 
				
			||||||
    #[autocomplete = "macro_name_autocomplete"]
 | 
					 | 
				
			||||||
    name: String,
 | 
					 | 
				
			||||||
) -> Result<(), Error> {
 | 
					 | 
				
			||||||
            match sqlx::query!(
 | 
					            match sqlx::query!(
 | 
				
			||||||
        "
 | 
					                "SELECT commands FROM macro WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?) AND name = ?",
 | 
				
			||||||
SELECT commands FROM macro WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?) AND name = ?",
 | 
					                invoke.guild_id().unwrap().0,
 | 
				
			||||||
        ctx.guild_id().unwrap().0,
 | 
					                macro_name
 | 
				
			||||||
        name
 | 
					 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
    .fetch_one(&ctx.data().database)
 | 
					            .fetch_one(&pool)
 | 
				
			||||||
            .await
 | 
					            .await
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                Ok(row) => {
 | 
					                Ok(row) => {
 | 
				
			||||||
            ctx.defer().await?;
 | 
					                    invoke.defer(&ctx).await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            let commands: Vec<CommandOptions> = serde_json::from_str(&row.commands)?;
 | 
					                    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 {
 | 
					                    for command in commands {
 | 
				
			||||||
                let cmd =
 | 
					                        framework.run_command_from_options(ctx, invoke, command).await;
 | 
				
			||||||
                    find_command(&ctx.framework().options().commands, &command.command, &command);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                if let Some(cmd) = cmd {
 | 
					 | 
				
			||||||
                    let mut executing_ctx = ctx.clone();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    executing_ctx.command = cmd;
 | 
					 | 
				
			||||||
                } else {
 | 
					 | 
				
			||||||
                    ctx.send(|m| {
 | 
					 | 
				
			||||||
                        m.ephemeral(true)
 | 
					 | 
				
			||||||
                            .content(format!("Command `{}` not found", command.command))
 | 
					 | 
				
			||||||
                    })
 | 
					 | 
				
			||||||
                    .await?;
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                Err(sqlx::Error::RowNotFound) => {
 | 
					                Err(sqlx::Error::RowNotFound) => {
 | 
				
			||||||
            ctx.say(format!("Macro \"{}\" not found", name)).await?;
 | 
					                    let _ = invoke
 | 
				
			||||||
 | 
					                        .respond(
 | 
				
			||||||
 | 
					                            &ctx,
 | 
				
			||||||
 | 
					                            CreateGenericResponse::new()
 | 
				
			||||||
 | 
					                                .content(format!("Macro \"{}\" not found", macro_name)),
 | 
				
			||||||
 | 
					                        )
 | 
				
			||||||
 | 
					                        .await;
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                Err(e) => {
 | 
					                Err(e) => {
 | 
				
			||||||
                    panic!("{}", e);
 | 
					                    panic!("{}", e);
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					 | 
				
			||||||
    Ok(())
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					        "delete" => {
 | 
				
			||||||
 | 
					            let macro_name = args.get("name").unwrap().to_string();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// 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!(
 | 
					            match sqlx::query!(
 | 
				
			||||||
        "
 | 
					                "SELECT id FROM macro WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?) AND name = ?",
 | 
				
			||||||
SELECT id FROM macro WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?) AND name = ?",
 | 
					                invoke.guild_id().unwrap().0,
 | 
				
			||||||
        ctx.guild_id().unwrap().0,
 | 
					                macro_name
 | 
				
			||||||
        name
 | 
					 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
    .fetch_one(&ctx.data().database)
 | 
					            .fetch_one(&pool)
 | 
				
			||||||
            .await
 | 
					            .await
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                Ok(row) => {
 | 
					                Ok(row) => {
 | 
				
			||||||
                    sqlx::query!("DELETE FROM macro WHERE id = ?", row.id)
 | 
					                    sqlx::query!("DELETE FROM macro WHERE id = ?", row.id)
 | 
				
			||||||
                .execute(&ctx.data().database)
 | 
					                        .execute(&pool)
 | 
				
			||||||
                        .await
 | 
					                        .await
 | 
				
			||||||
                        .unwrap();
 | 
					                        .unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            ctx.say(format!("Macro \"{}\" deleted", name)).await?;
 | 
					                    let _ = invoke
 | 
				
			||||||
 | 
					                        .respond(
 | 
				
			||||||
 | 
					                            &ctx,
 | 
				
			||||||
 | 
					                            CreateGenericResponse::new()
 | 
				
			||||||
 | 
					                                .content(format!("Macro \"{}\" deleted", macro_name)),
 | 
				
			||||||
 | 
					                        )
 | 
				
			||||||
 | 
					                        .await;
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                Err(sqlx::Error::RowNotFound) => {
 | 
					                Err(sqlx::Error::RowNotFound) => {
 | 
				
			||||||
            ctx.say(format!("Macro \"{}\" not found", name)).await?;
 | 
					                    let _ = invoke
 | 
				
			||||||
 | 
					                        .respond(
 | 
				
			||||||
 | 
					                            &ctx,
 | 
				
			||||||
 | 
					                            CreateGenericResponse::new()
 | 
				
			||||||
 | 
					                                .content(format!("Macro \"{}\" not found", macro_name)),
 | 
				
			||||||
 | 
					                        )
 | 
				
			||||||
 | 
					                        .await;
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                Err(e) => {
 | 
					                Err(e) => {
 | 
				
			||||||
                    panic!("{}", 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(
 | 
				
			||||||
        Ok(false)
 | 
					                &ctx,
 | 
				
			||||||
    }
 | 
					                CreateGenericResponse::new().content("This command can only be used in servers"),
 | 
				
			||||||
}
 | 
					            )
 | 
				
			||||||
 | 
					            .await;
 | 
				
			||||||
async fn macro_check(ctx: Context<'_>) -> bool {
 | 
					
 | 
				
			||||||
    if let Context::Application(app_ctx) = ctx {
 | 
					        HookResult::Halt
 | 
				
			||||||
        if let ApplicationCommandOrAutocompleteInteraction::ApplicationCommand(interaction) =
 | 
					    }
 | 
				
			||||||
            app_ctx.interaction
 | 
					}
 | 
				
			||||||
        {
 | 
					
 | 
				
			||||||
            if let Some(guild_id) = ctx.guild_id() {
 | 
					#[check]
 | 
				
			||||||
                if ctx.command().identifying_name != "macro_finish" {
 | 
					pub async fn macro_check(
 | 
				
			||||||
                    let mut lock = ctx.data().recording_macros.write().await;
 | 
					    ctx: &Context,
 | 
				
			||||||
 | 
					    invoke: &mut CommandInvoke,
 | 
				
			||||||
                    if let Some(command_macro) = lock.get_mut(&(guild_id, ctx.author().id)) {
 | 
					    args: &CommandOptions,
 | 
				
			||||||
                        if command_macro.commands.len() >= MACRO_MAX_COMMANDS {
 | 
					) -> HookResult {
 | 
				
			||||||
                            let _ = ctx.send(|m| {
 | 
					    if let Some(guild_id) = invoke.guild_id() {
 | 
				
			||||||
                                m.ephemeral(true).content(
 | 
					        if args.command != moderation_cmds::MACRO_CMD_COMMAND.names[0] {
 | 
				
			||||||
                                    "5 commands already recorded. Please use `/macro finish` to end recording.",
 | 
					            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, invoke.author_id())) {
 | 
				
			||||||
 | 
					                if command_macro.commands.len() >= 5 {
 | 
				
			||||||
 | 
					                    let _ = invoke
 | 
				
			||||||
 | 
					                        .respond(
 | 
				
			||||||
 | 
					                            &ctx,
 | 
				
			||||||
 | 
					                            CreateGenericResponse::new().content("5 commands already recorded. Please use `/macro finish` to end recording."),
 | 
				
			||||||
                        )
 | 
					                        )
 | 
				
			||||||
                            })
 | 
					 | 
				
			||||||
                        .await;
 | 
					                        .await;
 | 
				
			||||||
                } else {
 | 
					                } else {
 | 
				
			||||||
                            let mut command_options = CommandOptions::new(&ctx.command().name);
 | 
					                    command_macro.commands.push(args.clone());
 | 
				
			||||||
                            command_options.populate(&interaction);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
                            command_macro.commands.push(command_options);
 | 
					                    let _ = invoke
 | 
				
			||||||
 | 
					                        .respond(
 | 
				
			||||||
                            let _ = ctx
 | 
					                            &ctx,
 | 
				
			||||||
                                .send(|m| m.ephemeral(true).content("Command recorded to macro"))
 | 
					                            CreateGenericResponse::new().content("Command recorded to macro"),
 | 
				
			||||||
 | 
					                        )
 | 
				
			||||||
                        .await;
 | 
					                        .await;
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                        false
 | 
					                HookResult::Halt
 | 
				
			||||||
            } else {
 | 
					            } else {
 | 
				
			||||||
                        true
 | 
					                HookResult::Continue
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
                    true
 | 
					            HookResult::Continue
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
                true
 | 
					        HookResult::Continue
 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
            true
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
        true
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
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
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										395
									
								
								src/main.rs
									
									
									
									
									
								
							
							
						
						
									
										395
									
								
								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::{
 | 
				
			||||||
 | 
					    async_trait,
 | 
				
			||||||
 | 
					    client::{bridge::gateway::GatewayIntents, Client},
 | 
				
			||||||
 | 
					    http::{client::Http, CacheHttp},
 | 
				
			||||||
 | 
					    model::{
 | 
				
			||||||
 | 
					        channel::GuildChannel,
 | 
				
			||||||
 | 
					        gateway::{Activity, Ready},
 | 
				
			||||||
 | 
					        guild::{Guild, GuildUnavailable},
 | 
				
			||||||
        id::{GuildId, UserId},
 | 
					        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 framework = RegexFramework::new()
 | 
				
			||||||
 | 
					        .ignore_bots(env::var("IGNORE_BOTS").map_or(true, |var| var == "1"))
 | 
				
			||||||
 | 
					        .debug_guild(env::var("DEBUG_GUILD").map_or(None, |g| {
 | 
				
			||||||
 | 
					            Some(GuildId(g.parse::<u64>().expect("DEBUG_GUILD must be a guild ID")))
 | 
				
			||||||
 | 
					        }))
 | 
				
			||||||
 | 
					        .dm_enabled(dm_enabled)
 | 
				
			||||||
 | 
					        // info commands
 | 
				
			||||||
 | 
					        .add_command(&info_cmds::HELP_COMMAND)
 | 
				
			||||||
 | 
					        .add_command(&info_cmds::INFO_COMMAND)
 | 
				
			||||||
 | 
					        .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);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let framework_arc = Arc::new(framework);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let mut client = Client::builder(&token)
 | 
				
			||||||
 | 
					        .intents(GatewayIntents::GUILDS)
 | 
				
			||||||
 | 
					        .application_id(application_id.0)
 | 
				
			||||||
 | 
					        .event_handler(Handler { is_loop_running: AtomicBool::from(false) })
 | 
				
			||||||
 | 
					        .await
 | 
				
			||||||
 | 
					        .expect("Error occurred creating client");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        let pool = MySqlPool::connect(
 | 
				
			||||||
 | 
					            &env::var("DATABASE_URL").expect("Missing DATABASE_URL from environment"),
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        .await
 | 
				
			||||||
 | 
					        .unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        let popular_timezones = sqlx::query!(
 | 
					        let popular_timezones = sqlx::query!(
 | 
				
			||||||
        "
 | 
					            "SELECT timezone FROM users GROUP BY timezone ORDER BY COUNT(timezone) DESC LIMIT 21"
 | 
				
			||||||
SELECT timezone FROM users GROUP BY timezone ORDER BY COUNT(timezone) DESC LIMIT 21"
 | 
					 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
    .fetch_all(&database)
 | 
					        .fetch_all(&pool)
 | 
				
			||||||
        .await
 | 
					        .await
 | 
				
			||||||
        .unwrap()
 | 
					        .unwrap()
 | 
				
			||||||
        .iter()
 | 
					        .iter()
 | 
				
			||||||
        .map(|t| t.timezone.parse::<Tz>().unwrap())
 | 
					        .map(|t| t.timezone.parse::<Tz>().unwrap())
 | 
				
			||||||
        .collect::<Vec<Tz>>();
 | 
					        .collect::<Vec<Tz>>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    poise::Framework::build()
 | 
					        let mut data = client.data.write().await;
 | 
				
			||||||
        .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(
 | 
					        data.insert::<SQLPool>(pool);
 | 
				
			||||||
                    ctx,
 | 
					        data.insert::<PopularTimezones>(Arc::new(popular_timezones));
 | 
				
			||||||
                    framework,
 | 
					        data.insert::<ReqwestClient>(Arc::new(reqwest::Client::new()));
 | 
				
			||||||
                    env::var("DEBUG_GUILD")
 | 
					        data.insert::<RegexFramework>(framework_arc.clone());
 | 
				
			||||||
                        .map(|inner| GuildId(inner.parse().expect("DEBUG_GUILD not valid")))
 | 
					        data.insert::<RecordingMacros>(Arc::new(RwLock::new(HashMap::new())));
 | 
				
			||||||
                        .ok(),
 | 
					    }
 | 
				
			||||||
                )
 | 
					 | 
				
			||||||
                .await
 | 
					 | 
				
			||||||
                .unwrap();
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
                Ok(Data {
 | 
					    framework_arc.build_slash(&client.cache_and_http.http).await;
 | 
				
			||||||
                    http: reqwest::Client::new(),
 | 
					
 | 
				
			||||||
                    database,
 | 
					    if let Ok((Some(lower), Some(upper))) = env::var("SHARD_RANGE").map(|sr| {
 | 
				
			||||||
                    popular_timezones,
 | 
					        let mut split =
 | 
				
			||||||
                    recording_macros: Default::default(),
 | 
					            sr.split(',').map(|val| val.parse::<u64>().expect("SHARD_RANGE not an integer"));
 | 
				
			||||||
                })
 | 
					
 | 
				
			||||||
            })
 | 
					        (split.next(), split.next())
 | 
				
			||||||
        })
 | 
					    }) {
 | 
				
			||||||
        .options(options)
 | 
					        let total_shards = env::var("SHARD_COUNT")
 | 
				
			||||||
        .client_settings(move |client_builder| client_builder.intents(GatewayIntents::GUILDS))
 | 
					            .map(|shard_count| shard_count.parse::<u64>().ok())
 | 
				
			||||||
        .run_autosharded()
 | 
					            .ok()
 | 
				
			||||||
        .await?;
 | 
					            .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