Compare commits
	
		
			1 Commits
		
	
	
		
			poise
			...
			discord-ti
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 51d2ac2b92 | 
							
								
								
									
										1508
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1508
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										16
									
								
								Cargo.toml
									
									
									
									
									
								
							
							
						
						
									
										16
									
								
								Cargo.toml
									
									
									
									
									
								
							@@ -1,12 +1,11 @@
 | 
				
			|||||||
[package]
 | 
					[package]
 | 
				
			||||||
name = "reminder_rs"
 | 
					name = "reminder_rs"
 | 
				
			||||||
version = "1.6.0-beta2"
 | 
					version = "1.5.0-2"
 | 
				
			||||||
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" }
 | 
					dashmap = "4.0"
 | 
				
			||||||
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"] }
 | 
				
			||||||
@@ -15,14 +14,17 @@ regex = "1.4"
 | 
				
			|||||||
log = "0.4"
 | 
					log = "0.4"
 | 
				
			||||||
env_logger = "0.8"
 | 
					env_logger = "0.8"
 | 
				
			||||||
chrono = "0.4"
 | 
					chrono = "0.4"
 | 
				
			||||||
chrono-tz = { version = "0.5", features = ["serde"] }
 | 
					chrono-tz = "0.5"
 | 
				
			||||||
lazy_static = "1.4"
 | 
					lazy_static = "1.4"
 | 
				
			||||||
num-integer = "0.1"
 | 
					num-integer = "0.1"
 | 
				
			||||||
serde = "1.0"
 | 
					serde = "1.0"
 | 
				
			||||||
serde_json = "1.0"
 | 
					serde_json = "1.0"
 | 
				
			||||||
serde_repr = "0.1"
 | 
					 | 
				
			||||||
rmp-serde = "0.15"
 | 
					 | 
				
			||||||
rand = "0.7"
 | 
					rand = "0.7"
 | 
				
			||||||
 | 
					Inflector = "0.11"
 | 
				
			||||||
levenshtein = "1.0"
 | 
					levenshtein = "1.0"
 | 
				
			||||||
 | 
					# serenity = { version = "0.10", features = ["collector"] }
 | 
				
			||||||
 | 
					serenity = { path = "/home/jude/serenity", features = ["collector", "unstable_discord_api"] }
 | 
				
			||||||
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"
 | 
					
 | 
				
			||||||
 | 
					[dependencies.regex_command_attr]
 | 
				
			||||||
 | 
					path = "./regex_command_attr"
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										14
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										14
									
								
								README.md
									
									
									
									
									
								
							@@ -1,5 +1,6 @@
 | 
				
			|||||||
# reminder-rs
 | 
					# reminder-rs
 | 
				
			||||||
Reminder Bot for Discord.
 | 
					Reminder Bot for Discord, now in Rust.
 | 
				
			||||||
 | 
					Old Python version: https://github.com/reminder-bot/bot
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## How do I use it?
 | 
					## How do I use it?
 | 
				
			||||||
We offer a hosted version of the bot. You can invite it with: **https://invite.reminder-bot.com**. The catch is that repeating 
 | 
					We offer a hosted version of the bot. You can invite it with: **https://invite.reminder-bot.com**. The catch is that repeating 
 | 
				
			||||||
@@ -14,6 +15,7 @@ Reminder Bot can be built by running `cargo build --release` in the top level di
 | 
				
			|||||||
These environment variables must be provided when compiling the bot
 | 
					These environment variables must be provided when compiling the bot
 | 
				
			||||||
* `DATABASE_URL` - the URL of your MySQL database (`mysql://user[:password]@domain/database`)
 | 
					* `DATABASE_URL` - the URL of your MySQL database (`mysql://user[:password]@domain/database`)
 | 
				
			||||||
* `WEBHOOK_AVATAR` - accepts the name of an image file located in `$CARGO_MANIFEST_DIR/assets/` to be used as the avatar when creating webhooks. **IMPORTANT: image file must be 128x128 or smaller in size**
 | 
					* `WEBHOOK_AVATAR` - accepts the name of an image file located in `$CARGO_MANIFEST_DIR/assets/` to be used as the avatar when creating webhooks. **IMPORTANT: image file must be 128x128 or smaller in size**
 | 
				
			||||||
 | 
					* `STRINGS_FILE` - accepts the name of a compiled strings file located in `$CARGO_MANIFEST_DIR/assets/` to be used for creating messages. Compiled string files can be generated with `compile.py` at https://github.com/reminder-bot/languages
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### Setting up Python
 | 
					### Setting up Python
 | 
				
			||||||
Reminder Bot by default looks for a venv within it's working directory to run Python out of. To set up a venv, install `python3-venv` and run `python3 -m venv venv`. Then, run `source venv/bin/activate` to activate the venv, and do `pip install dateparser` to install the required library
 | 
					Reminder Bot by default looks for a venv within it's working directory to run Python out of. To set up a venv, install `python3-venv` and run `python3 -m venv venv`. Then, run `source venv/bin/activate` to activate the venv, and do `pip install dateparser` to install the required library
 | 
				
			||||||
@@ -27,18 +29,16 @@ __Required Variables__
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
__Other Variables__
 | 
					__Other Variables__
 | 
				
			||||||
* `MIN_INTERVAL` - default `600`, defines the shortest interval the bot should accept
 | 
					* `MIN_INTERVAL` - default `600`, defines the shortest interval the bot should accept
 | 
				
			||||||
 | 
					* `MAX_TIME` - default `1576800000`, defines the maximum time ahead that reminders can be set for
 | 
				
			||||||
* `LOCAL_TIMEZONE` - default `UTC`, necessary for calculations in the natural language processor
 | 
					* `LOCAL_TIMEZONE` - default `UTC`, necessary for calculations in the natural language processor
 | 
				
			||||||
 | 
					* `DEFAULT_PREFIX` - default `$`, used for the default prefix on new guilds
 | 
				
			||||||
* `SUBSCRIPTION_ROLES` - default `None`, accepts a list of Discord role IDs that are given to subscribed users
 | 
					* `SUBSCRIPTION_ROLES` - default `None`, accepts a list of Discord role IDs that are given to subscribed users
 | 
				
			||||||
* `CNC_GUILD` - default `None`, accepts a single Discord guild ID for the server that the subscription roles belong to
 | 
					* `CNC_GUILD` - default `None`, accepts a single Discord guild ID for the server that the subscription roles belong to
 | 
				
			||||||
* `IGNORE_BOTS` - default `1`, if `1`, Reminder Bot will ignore all other bots
 | 
					* `IGNORE_BOTS` - default `1`, if `1`, Reminder Bot will ignore all other bots
 | 
				
			||||||
* `PYTHON_LOCATION` - default `venv/bin/python3`. Can be changed if your Python executable is located somewhere else
 | 
					* `PYTHON_LOCATION` - default `venv/bin/python3`. Can be changed if your Python executable is located somewhere else
 | 
				
			||||||
 | 
					* `LOCAL_LANGUAGE` - default `EN`. Specifies the string set to fall back to if a string cannot be found (and to be used with new users)
 | 
				
			||||||
* `THEME_COLOR` - default `8fb677`. Specifies the hex value of the color to use on info message embeds 
 | 
					* `THEME_COLOR` - default `8fb677`. Specifies the hex value of the color to use on info message embeds 
 | 
				
			||||||
 | 
					* `CASE_INSENSITIVE` - default `1`, if `1`, commands will be treated with case insensitivity (so both `$help` and `$HELP` will work)
 | 
				
			||||||
* `SHARD_COUNT` - default `None`, accepts the number of shards that are being ran
 | 
					* `SHARD_COUNT` - default `None`, accepts the number of shards that are being ran
 | 
				
			||||||
* `SHARD_RANGE` - default `None`, if `SHARD_COUNT` is specified, specifies what range of shards to start on this process 
 | 
					* `SHARD_RANGE` - default `None`, if `SHARD_COUNT` is specified, specifies what range of shards to start on this process 
 | 
				
			||||||
* `DM_ENABLED` - default `1`, if `1`, Reminder Bot will respond to direct messages
 | 
					* `DM_ENABLED` - default `1`, if `1`, Reminder Bot will respond to direct messages
 | 
				
			||||||
 | 
					 | 
				
			||||||
### Todo List
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
* Convert aliases to macros
 | 
					 | 
				
			||||||
* Help command
 | 
					 | 
				
			||||||
* Test everything
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,13 +0,0 @@
 | 
				
			|||||||
USE reminders;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
CREATE TABLE macro (
 | 
					 | 
				
			||||||
    id INT UNSIGNED AUTO_INCREMENT,
 | 
					 | 
				
			||||||
    guild_id INT UNSIGNED NOT NULL,
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    name VARCHAR(100) NOT NULL,
 | 
					 | 
				
			||||||
    description VARCHAR(100),
 | 
					 | 
				
			||||||
    commands TEXT NOT NULL,
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    FOREIGN KEY (guild_id) REFERENCES guilds(id) ON DELETE CASCADE,
 | 
					 | 
				
			||||||
    PRIMARY KEY (id)
 | 
					 | 
				
			||||||
);
 | 
					 | 
				
			||||||
@@ -56,7 +56,8 @@ CREATE TABLE reminders_new (
 | 
				
			|||||||
    -- , CONSTRAINT interval_enabled_mutin CHECK (`enabled` = 1 OR `interval` IS NULL)
 | 
					    -- , CONSTRAINT interval_enabled_mutin CHECK (`enabled` = 1 OR `interval` IS NULL)
 | 
				
			||||||
    # disallow an expiry time if interval is unspecified
 | 
					    # disallow an expiry time if interval is unspecified
 | 
				
			||||||
    -- , CONSTRAINT interval_expires_mutin CHECK (`expires` IS NULL OR `interval` IS NOT NULL)
 | 
					    -- , CONSTRAINT interval_expires_mutin CHECK (`expires` IS NULL OR `interval` IS NOT NULL)
 | 
				
			||||||
);
 | 
					)
 | 
				
			||||||
 | 
					COLLATE utf8mb4_unicode_ci;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# import data from other tables
 | 
					# import data from other tables
 | 
				
			||||||
INSERT INTO reminders_new (
 | 
					INSERT INTO reminders_new (
 | 
				
			||||||
							
								
								
									
										14
									
								
								regex_command_attr/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								regex_command_attr/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,14 @@
 | 
				
			|||||||
 | 
					[package]
 | 
				
			||||||
 | 
					name = "regex_command_attr"
 | 
				
			||||||
 | 
					version = "0.2.0"
 | 
				
			||||||
 | 
					authors = ["acdenisSK <acdenissk69@gmail.com>", "jellywx <judesouthworth@pm.me>"]
 | 
				
			||||||
 | 
					edition = "2018"
 | 
				
			||||||
 | 
					description = "Procedural macros for command creation for the RegexFramework for serenity."
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[lib]
 | 
				
			||||||
 | 
					proc-macro = true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[dependencies]
 | 
				
			||||||
 | 
					quote = "^1.0"
 | 
				
			||||||
 | 
					syn = { version = "^1.0", features = ["full", "derive", "extra-traits"] }
 | 
				
			||||||
 | 
					proc-macro2 = "1.0"
 | 
				
			||||||
							
								
								
									
										293
									
								
								regex_command_attr/src/attributes.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										293
									
								
								regex_command_attr/src/attributes.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,293 @@
 | 
				
			|||||||
 | 
					use proc_macro2::Span;
 | 
				
			||||||
 | 
					use syn::parse::{Error, Result};
 | 
				
			||||||
 | 
					use syn::spanned::Spanned;
 | 
				
			||||||
 | 
					use syn::{Attribute, Ident, Lit, LitStr, Meta, NestedMeta, Path};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::structures::PermissionLevel;
 | 
				
			||||||
 | 
					use crate::util::{AsOption, LitExt};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use std::fmt::{self, Write};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Debug, Clone, Copy, PartialEq)]
 | 
				
			||||||
 | 
					pub enum ValueKind {
 | 
				
			||||||
 | 
					    // #[<name>]
 | 
				
			||||||
 | 
					    Name,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // #[<name> = <value>]
 | 
				
			||||||
 | 
					    Equals,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // #[<name>([<value>, <value>, <value>, ...])]
 | 
				
			||||||
 | 
					    List,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // #[<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::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<Lit>,
 | 
				
			||||||
 | 
					    pub kind: ValueKind,
 | 
				
			||||||
 | 
					    pub span: Span,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl Values {
 | 
				
			||||||
 | 
					    #[inline]
 | 
				
			||||||
 | 
					    pub fn new(name: Ident, kind: ValueKind, literals: Vec<Lit>, span: Span) -> Self {
 | 
				
			||||||
 | 
					        Values {
 | 
				
			||||||
 | 
					            name,
 | 
				
			||||||
 | 
					            literals,
 | 
				
			||||||
 | 
					            kind,
 | 
				
			||||||
 | 
					            span,
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub fn parse_values(attr: &Attribute) -> Result<Values> {
 | 
				
			||||||
 | 
					    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"));
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            let mut lits = Vec::with_capacity(nested.len());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            for meta in nested {
 | 
				
			||||||
 | 
					                match meta {
 | 
				
			||||||
 | 
					                    NestedMeta::Lit(l) => lits.push(l),
 | 
				
			||||||
 | 
					                    NestedMeta::Meta(m) => match m {
 | 
				
			||||||
 | 
					                        Meta::Path(path) => {
 | 
				
			||||||
 | 
					                            let i = to_ident(path)?;
 | 
				
			||||||
 | 
					                            lits.push(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()))
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        Meta::NameValue(meta) => {
 | 
				
			||||||
 | 
					            let name = to_ident(meta.path)?;
 | 
				
			||||||
 | 
					            let lit = meta.lit;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            Ok(Values::new(name, ValueKind::Equals, vec![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(|lit| lit.to_str())
 | 
				
			||||||
 | 
					            .collect())
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl AttributeOption for String {
 | 
				
			||||||
 | 
					    #[inline]
 | 
				
			||||||
 | 
					    fn parse(values: Values) -> Result<Self> {
 | 
				
			||||||
 | 
					        validate(&values, &[ValueKind::Equals, ValueKind::SingleList])?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Ok(values.literals[0].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].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 PermissionLevel {
 | 
				
			||||||
 | 
					    fn parse(values: Values) -> Result<Self> {
 | 
				
			||||||
 | 
					        validate(&values, &[ValueKind::SingleList])?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Ok(values.literals.get(0).map(|l| PermissionLevel::from_str(&*l.to_str()).unwrap()).unwrap())
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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] {
 | 
				
			||||||
 | 
					                        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);
 | 
				
			||||||
							
								
								
									
										5
									
								
								regex_command_attr/src/consts.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								regex_command_attr/src/consts.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,5 @@
 | 
				
			|||||||
 | 
					pub mod suffixes {
 | 
				
			||||||
 | 
					    pub const COMMAND: &str = "COMMAND";
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub use self::suffixes::*;
 | 
				
			||||||
							
								
								
									
										102
									
								
								regex_command_attr/src/lib.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										102
									
								
								regex_command_attr/src/lib.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,102 @@
 | 
				
			|||||||
 | 
					#![deny(rust_2018_idioms)]
 | 
				
			||||||
 | 
					// FIXME: Remove this in a foreseeable future.
 | 
				
			||||||
 | 
					// Currently exists for backwards compatibility to previous Rust versions.
 | 
				
			||||||
 | 
					#![recursion_limit = "128"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[allow(unused_extern_crates)]
 | 
				
			||||||
 | 
					extern crate proc_macro;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use proc_macro::TokenStream;
 | 
				
			||||||
 | 
					use quote::quote;
 | 
				
			||||||
 | 
					use syn::{parse::Error, parse_macro_input, spanned::Spanned, Lit};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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 {
 | 
				
			||||||
 | 
					    let mut fun = parse_macro_input!(input as CommandFun);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let lit_name = if !attr.is_empty() {
 | 
				
			||||||
 | 
					        parse_macro_input!(attr as Lit).to_str()
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        fun.name.to_string()
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let mut options = Options::new();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    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_options!(name, values, options, span => [
 | 
				
			||||||
 | 
					            permission_level;
 | 
				
			||||||
 | 
					            supports_dm;
 | 
				
			||||||
 | 
					            can_blacklist
 | 
				
			||||||
 | 
					        ]);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let Options {
 | 
				
			||||||
 | 
					        permission_level,
 | 
				
			||||||
 | 
					        supports_dm,
 | 
				
			||||||
 | 
					        can_blacklist,
 | 
				
			||||||
 | 
					    } = options;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let visibility = fun.visibility;
 | 
				
			||||||
 | 
					    let name = fun.name.clone();
 | 
				
			||||||
 | 
					    let body = fun.body;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let n = name.with_suffix(COMMAND);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let cooked = fun.cooked.clone();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let command_path = quote!(crate::framework::Command);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    populate_fut_lifetimes_on_refs(&mut fun.args);
 | 
				
			||||||
 | 
					    let args = fun.args;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    (quote! {
 | 
				
			||||||
 | 
					        #(#cooked)*
 | 
				
			||||||
 | 
					        pub static #n: #command_path = #command_path {
 | 
				
			||||||
 | 
					            func: #name,
 | 
				
			||||||
 | 
					            name: #lit_name,
 | 
				
			||||||
 | 
					            required_perms: #permission_level,
 | 
				
			||||||
 | 
					            supports_dm: #supports_dm,
 | 
				
			||||||
 | 
					            can_blacklist: #can_blacklist,
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        #visibility fn #name<'fut> (#(#args),*) -> ::serenity::futures::future::BoxFuture<'fut, ()> {
 | 
				
			||||||
 | 
					            use ::serenity::futures::future::FutureExt;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            async move { #(#body)* }.boxed()
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					    .into()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										231
									
								
								regex_command_attr/src/structures.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										231
									
								
								regex_command_attr/src/structures.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,231 @@
 | 
				
			|||||||
 | 
					use crate::util::{Argument, Parenthesised};
 | 
				
			||||||
 | 
					use proc_macro2::Span;
 | 
				
			||||||
 | 
					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, Path, PathSegment, Stmt, Token, Visibility,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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),
 | 
				
			||||||
 | 
					        )),
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Test if the attribute is cooked.
 | 
				
			||||||
 | 
					fn is_cooked(attr: &Attribute) -> bool {
 | 
				
			||||||
 | 
					    const COOKED_ATTRIBUTE_NAMES: &[&str] = &[
 | 
				
			||||||
 | 
					        "cfg", "cfg_attr", "doc", "derive", "inline", "allow", "warn", "deny", "forbid",
 | 
				
			||||||
 | 
					    ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    COOKED_ATTRIBUTE_NAMES.iter().any(|n| attr.path.is_ident(n))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Removes cooked attributes from a vector of attributes. Uncooked attributes are left in the vector.
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					/// # Return
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					/// Returns a vector of cooked attributes that have been removed from the input vector.
 | 
				
			||||||
 | 
					fn remove_cooked(attrs: &mut Vec<Attribute>) -> Vec<Attribute> {
 | 
				
			||||||
 | 
					    let mut cooked = Vec::new();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // FIXME: Replace with `Vec::drain_filter` once it is stable.
 | 
				
			||||||
 | 
					    let mut i = 0;
 | 
				
			||||||
 | 
					    while i < attrs.len() {
 | 
				
			||||||
 | 
					        if !is_cooked(&attrs[i]) {
 | 
				
			||||||
 | 
					            i += 1;
 | 
				
			||||||
 | 
					            continue;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        cooked.push(attrs.remove(i));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    cooked
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Debug)]
 | 
				
			||||||
 | 
					pub struct CommandFun {
 | 
				
			||||||
 | 
					    /// `#[...]`-style attributes.
 | 
				
			||||||
 | 
					    pub attributes: Vec<Attribute>,
 | 
				
			||||||
 | 
					    /// Populated cooked attributes. These are attributes outside of the realm of this crate's procedural macros
 | 
				
			||||||
 | 
					    /// and will appear in generated output.
 | 
				
			||||||
 | 
					    pub cooked: Vec<Attribute>,
 | 
				
			||||||
 | 
					    pub visibility: Visibility,
 | 
				
			||||||
 | 
					    pub name: Ident,
 | 
				
			||||||
 | 
					    pub args: Vec<Argument>,
 | 
				
			||||||
 | 
					    pub body: Vec<Stmt>,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl Parse for CommandFun {
 | 
				
			||||||
 | 
					    fn parse(input: ParseStream<'_>) -> Result<Self> {
 | 
				
			||||||
 | 
					        let mut attributes = input.call(Attribute::parse_outer)?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // `#[doc = "..."]` is a cooked attribute but it is special-cased for commands.
 | 
				
			||||||
 | 
					        for attr in &mut attributes {
 | 
				
			||||||
 | 
					            // Rename documentation comment attributes (`#[doc = "..."]`) to `#[description = "..."]`.
 | 
				
			||||||
 | 
					            if attr.path.is_ident("doc") {
 | 
				
			||||||
 | 
					                attr.path = Path::from(PathSegment::from(Ident::new(
 | 
				
			||||||
 | 
					                    "description",
 | 
				
			||||||
 | 
					                    Span::call_site(),
 | 
				
			||||||
 | 
					                )));
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let cooked = remove_cooked(&mut attributes);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        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 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,
 | 
				
			||||||
 | 
					            cooked,
 | 
				
			||||||
 | 
					            visibility,
 | 
				
			||||||
 | 
					            name,
 | 
				
			||||||
 | 
					            args,
 | 
				
			||||||
 | 
					            body,
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl ToTokens for CommandFun {
 | 
				
			||||||
 | 
					    fn to_tokens(&self, stream: &mut TokenStream2) {
 | 
				
			||||||
 | 
					        let Self {
 | 
				
			||||||
 | 
					            attributes: _,
 | 
				
			||||||
 | 
					            cooked,
 | 
				
			||||||
 | 
					            visibility,
 | 
				
			||||||
 | 
					            name,
 | 
				
			||||||
 | 
					            args,
 | 
				
			||||||
 | 
					            body,
 | 
				
			||||||
 | 
					        } = self;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        stream.extend(quote! {
 | 
				
			||||||
 | 
					            #(#cooked)*
 | 
				
			||||||
 | 
					            #visibility async fn #name (#(#args),*) -> () {
 | 
				
			||||||
 | 
					                #(#body)*
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Debug)]
 | 
				
			||||||
 | 
					pub enum PermissionLevel {
 | 
				
			||||||
 | 
					    Unrestricted,
 | 
				
			||||||
 | 
					    Managed,
 | 
				
			||||||
 | 
					    Restricted,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl Default for PermissionLevel {
 | 
				
			||||||
 | 
					    fn default() -> Self {
 | 
				
			||||||
 | 
					        Self::Unrestricted
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl PermissionLevel {
 | 
				
			||||||
 | 
					    pub fn from_str(s: &str) -> Option<Self> {
 | 
				
			||||||
 | 
					        Some(match s.to_uppercase().as_str() {
 | 
				
			||||||
 | 
					            "UNRESTRICTED" => Self::Unrestricted,
 | 
				
			||||||
 | 
					            "MANAGED" => Self::Managed,
 | 
				
			||||||
 | 
					            "RESTRICTED" => Self::Restricted,
 | 
				
			||||||
 | 
					            _ => return None,
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl ToTokens for PermissionLevel {
 | 
				
			||||||
 | 
					    fn to_tokens(&self, stream: &mut TokenStream2) {
 | 
				
			||||||
 | 
					        let path = quote!(crate::framework::PermissionLevel);
 | 
				
			||||||
 | 
					        let variant;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        match self {
 | 
				
			||||||
 | 
					            Self::Unrestricted => {
 | 
				
			||||||
 | 
					                variant = quote!(Unrestricted);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            Self::Managed => {
 | 
				
			||||||
 | 
					                variant = quote!(Managed);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            Self::Restricted => {
 | 
				
			||||||
 | 
					                variant = quote!(Restricted);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        stream.extend(quote! {
 | 
				
			||||||
 | 
					            #path::#variant
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Debug, Default)]
 | 
				
			||||||
 | 
					pub struct Options {
 | 
				
			||||||
 | 
					    pub permission_level: PermissionLevel,
 | 
				
			||||||
 | 
					    pub supports_dm: bool,
 | 
				
			||||||
 | 
					    pub can_blacklist: bool,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl Options {
 | 
				
			||||||
 | 
					    #[inline]
 | 
				
			||||||
 | 
					    pub fn new() -> Self {
 | 
				
			||||||
 | 
					        let mut options = Self::default();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        options.can_blacklist = true;
 | 
				
			||||||
 | 
					        options.supports_dm = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        options
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										160
									
								
								regex_command_attr/src/util.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										160
									
								
								regex_command_attr/src/util.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,160 @@
 | 
				
			|||||||
 | 
					use proc_macro::TokenStream;
 | 
				
			||||||
 | 
					use proc_macro2::Span;
 | 
				
			||||||
 | 
					use proc_macro2::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()));
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,3 +0,0 @@
 | 
				
			|||||||
imports_granularity = "Crate"
 | 
					 | 
				
			||||||
group_imports = "StdExternalCrate"
 | 
					 | 
				
			||||||
use_small_heuristics = "Max"
 | 
					 | 
				
			||||||
@@ -1,11 +1,38 @@
 | 
				
			|||||||
 | 
					use regex_command_attr::command;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use serenity::{client::Context, model::channel::Message};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use chrono::offset::Utc;
 | 
					use chrono::offset::Utc;
 | 
				
			||||||
use poise::serenity::builder::CreateEmbedFooter;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::{models::CtxData, Context, Error, THEME_COLOR};
 | 
					use crate::{
 | 
				
			||||||
 | 
					    command_help, consts::DEFAULT_PREFIX, get_ctx_data, language_manager::LanguageManager,
 | 
				
			||||||
 | 
					    models::UserData, FrameworkCtx, THEME_COLOR,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
fn footer(ctx: Context<'_>) -> impl FnOnce(&mut CreateEmbedFooter) -> &mut CreateEmbedFooter {
 | 
					use crate::models::CtxGuildData;
 | 
				
			||||||
    let shard_count = ctx.discord().cache.shard_count();
 | 
					use serenity::builder::CreateEmbedFooter;
 | 
				
			||||||
    let shard = ctx.discord().shard_id;
 | 
					use std::sync::Arc;
 | 
				
			||||||
 | 
					use std::time::{SystemTime, UNIX_EPOCH};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[command]
 | 
				
			||||||
 | 
					#[can_blacklist(false)]
 | 
				
			||||||
 | 
					async fn ping(ctx: &Context, msg: &Message, _args: String) {
 | 
				
			||||||
 | 
					    let now = SystemTime::now();
 | 
				
			||||||
 | 
					    let since_epoch = now
 | 
				
			||||||
 | 
					        .duration_since(UNIX_EPOCH)
 | 
				
			||||||
 | 
					        .expect("Time calculated as going backwards. Very bad");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let delta = since_epoch.as_millis() as i64 - msg.timestamp.timestamp_millis();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let _ = msg
 | 
				
			||||||
 | 
					        .channel_id
 | 
				
			||||||
 | 
					        .say(&ctx, format!("Time taken to receive message: {}ms", delta))
 | 
				
			||||||
 | 
					        .await;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async fn footer(ctx: &Context) -> impl FnOnce(&mut CreateEmbedFooter) -> &mut CreateEmbedFooter {
 | 
				
			||||||
 | 
					    let shard_count = ctx.cache.shard_count().await;
 | 
				
			||||||
 | 
					    let shard = ctx.shard_id;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    move |f| {
 | 
					    move |f| {
 | 
				
			||||||
        f.text(format!(
 | 
					        f.text(format!(
 | 
				
			||||||
@@ -17,141 +44,175 @@ fn footer(ctx: Context<'_>) -> impl FnOnce(&mut CreateEmbedFooter) -> &mut Creat
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Get an overview of bot commands
 | 
					#[command]
 | 
				
			||||||
#[poise::command(slash_command)]
 | 
					#[can_blacklist(false)]
 | 
				
			||||||
pub async fn help(ctx: Context<'_>) -> Result<(), Error> {
 | 
					async fn help(ctx: &Context, msg: &Message, args: String) {
 | 
				
			||||||
    let footer = footer(ctx);
 | 
					    async fn default_help(
 | 
				
			||||||
 | 
					        ctx: &Context,
 | 
				
			||||||
 | 
					        msg: &Message,
 | 
				
			||||||
 | 
					        lm: Arc<LanguageManager>,
 | 
				
			||||||
 | 
					        prefix: &str,
 | 
				
			||||||
 | 
					        language: &str,
 | 
				
			||||||
 | 
					    ) {
 | 
				
			||||||
 | 
					        let desc = lm.get(language, "help/desc").replace("{prefix}", prefix);
 | 
				
			||||||
 | 
					        let footer = footer(ctx).await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let _ = ctx
 | 
					        let _ = msg
 | 
				
			||||||
        .send(|m| {
 | 
					            .channel_id
 | 
				
			||||||
            m.embed(|e| {
 | 
					            .send_message(ctx, |m| {
 | 
				
			||||||
                e.title("Help")
 | 
					                m.embed(move |e| {
 | 
				
			||||||
                    .color(*THEME_COLOR)
 | 
					                    e.title("Help Menu")
 | 
				
			||||||
                    .description(
 | 
					                        .description(desc)
 | 
				
			||||||
                        "__Info Commands__
 | 
					                        .field(
 | 
				
			||||||
`/help` `/info` `/donate` `/dashboard` `/clock`
 | 
					                            lm.get(language, "help/setup_title"),
 | 
				
			||||||
*run these commands with no options*
 | 
					                            "`lang` `timezone` `meridian`",
 | 
				
			||||||
 | 
					                            true,
 | 
				
			||||||
__Reminder Commands__
 | 
					 | 
				
			||||||
`/remind` - Create a new reminder that will send a message at a certain time
 | 
					 | 
				
			||||||
`/timer` - Start a timer from now, that will count time passed. Also used to view and remove timers
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
__Reminder Management__
 | 
					 | 
				
			||||||
`/del` - Delete reminders
 | 
					 | 
				
			||||||
`/look` - View reminders
 | 
					 | 
				
			||||||
`/pause` - Pause all reminders on the channel
 | 
					 | 
				
			||||||
`/offset` - Move all reminders by a certain time
 | 
					 | 
				
			||||||
`/nudge` - Move all new reminders on this channel by a certain time
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
__Todo Commands__
 | 
					 | 
				
			||||||
`/todo` - Add, view and manage the server, channel or user todo lists
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
__Setup Commands__
 | 
					 | 
				
			||||||
`/timezone` - Set your timezone (necessary for `/remind` to work properly)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
__Advanced Commands__
 | 
					 | 
				
			||||||
`/macro` - Record and replay command sequences
 | 
					 | 
				
			||||||
                    ",
 | 
					 | 
				
			||||||
                        )
 | 
					                        )
 | 
				
			||||||
 | 
					                        .field(
 | 
				
			||||||
 | 
					                            lm.get(language, "help/mod_title"),
 | 
				
			||||||
 | 
					                            "`prefix` `blacklist` `restrict` `alias`",
 | 
				
			||||||
 | 
					                            true,
 | 
				
			||||||
 | 
					                        )
 | 
				
			||||||
 | 
					                        .field(
 | 
				
			||||||
 | 
					                            lm.get(language, "help/reminder_title"),
 | 
				
			||||||
 | 
					                            "`remind` `interval` `natural` `look` `countdown`",
 | 
				
			||||||
 | 
					                            true,
 | 
				
			||||||
 | 
					                        )
 | 
				
			||||||
 | 
					                        .field(
 | 
				
			||||||
 | 
					                            lm.get(language, "help/reminder_mod_title"),
 | 
				
			||||||
 | 
					                            "`del` `offset` `pause` `nudge`",
 | 
				
			||||||
 | 
					                            true,
 | 
				
			||||||
 | 
					                        )
 | 
				
			||||||
 | 
					                        .field(
 | 
				
			||||||
 | 
					                            lm.get(language, "help/info_title"),
 | 
				
			||||||
 | 
					                            "`help` `info` `donate` `clock`",
 | 
				
			||||||
 | 
					                            true,
 | 
				
			||||||
 | 
					                        )
 | 
				
			||||||
 | 
					                        .field(
 | 
				
			||||||
 | 
					                            lm.get(language, "help/todo_title"),
 | 
				
			||||||
 | 
					                            "`todo` `todos` `todoc`",
 | 
				
			||||||
 | 
					                            true,
 | 
				
			||||||
 | 
					                        )
 | 
				
			||||||
 | 
					                        .field(lm.get(language, "help/other_title"), "`timer`", true)
 | 
				
			||||||
                        .footer(footer)
 | 
					                        .footer(footer)
 | 
				
			||||||
 | 
					                        .color(*THEME_COLOR)
 | 
				
			||||||
                })
 | 
					                })
 | 
				
			||||||
            })
 | 
					            })
 | 
				
			||||||
            .await;
 | 
					            .await;
 | 
				
			||||||
 | 
					 | 
				
			||||||
    Ok(())
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Get information about the bot
 | 
					    let (pool, lm) = get_ctx_data(&ctx).await;
 | 
				
			||||||
#[poise::command(slash_command)]
 | 
					 | 
				
			||||||
pub async fn info(ctx: Context<'_>) -> Result<(), Error> {
 | 
					 | 
				
			||||||
    let footer = footer(ctx);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let _ = ctx
 | 
					    let language = UserData::language_of(&msg.author, &pool);
 | 
				
			||||||
        .send(|m| {
 | 
					    let prefix = ctx.prefix(msg.guild_id);
 | 
				
			||||||
            m.embed(|e| {
 | 
					
 | 
				
			||||||
 | 
					    if !args.is_empty() {
 | 
				
			||||||
 | 
					        let framework = ctx
 | 
				
			||||||
 | 
					            .data
 | 
				
			||||||
 | 
					            .read()
 | 
				
			||||||
 | 
					            .await
 | 
				
			||||||
 | 
					            .get::<FrameworkCtx>()
 | 
				
			||||||
 | 
					            .cloned()
 | 
				
			||||||
 | 
					            .expect("Could not get FrameworkCtx from data");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let matched = framework
 | 
				
			||||||
 | 
					            .commands
 | 
				
			||||||
 | 
					            .get(args.as_str())
 | 
				
			||||||
 | 
					            .map(|inner| inner.name);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if let Some(command_name) = matched {
 | 
				
			||||||
 | 
					            command_help(ctx, msg, lm, &prefix.await, &language.await, command_name).await
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            default_help(ctx, msg, lm, &prefix.await, &language.await).await;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        default_help(ctx, msg, lm, &prefix.await, &language.await).await;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[command]
 | 
				
			||||||
 | 
					async fn info(ctx: &Context, msg: &Message, _args: String) {
 | 
				
			||||||
 | 
					    let (pool, lm) = get_ctx_data(&ctx).await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let language = UserData::language_of(&msg.author, &pool);
 | 
				
			||||||
 | 
					    let prefix = ctx.prefix(msg.guild_id);
 | 
				
			||||||
 | 
					    let current_user = ctx.cache.current_user();
 | 
				
			||||||
 | 
					    let footer = footer(ctx).await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let desc = lm
 | 
				
			||||||
 | 
					        .get(&language.await, "info")
 | 
				
			||||||
 | 
					        .replacen("{user}", ¤t_user.await.name, 1)
 | 
				
			||||||
 | 
					        .replace("{default_prefix}", &*DEFAULT_PREFIX)
 | 
				
			||||||
 | 
					        .replace("{prefix}", &prefix.await);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let _ = msg
 | 
				
			||||||
 | 
					        .channel_id
 | 
				
			||||||
 | 
					        .send_message(ctx, |m| {
 | 
				
			||||||
 | 
					            m.embed(move |e| {
 | 
				
			||||||
                e.title("Info")
 | 
					                e.title("Info")
 | 
				
			||||||
                    .description(format!(
 | 
					                    .description(desc)
 | 
				
			||||||
                        "Help: `/help`
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
**Welcome to Reminder Bot!**
 | 
					 | 
				
			||||||
Developer: <@203532103185465344>
 | 
					 | 
				
			||||||
Icon: <@253202252821430272>
 | 
					 | 
				
			||||||
Find me on https://discord.jellywx.com and on https://github.com/JellyWX :)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Invite the bot: https://invite.reminder-bot.com/
 | 
					 | 
				
			||||||
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)]
 | 
					async fn donate(ctx: &Context, msg: &Message, _args: String) {
 | 
				
			||||||
pub async fn donate(ctx: Context<'_>) -> Result<(), Error> {
 | 
					    let (pool, lm) = get_ctx_data(&ctx).await;
 | 
				
			||||||
    let footer = footer(ctx);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let _ = ctx.send(|m| m.embed(|e| {
 | 
					    let language = UserData::language_of(&msg.author, &pool).await;
 | 
				
			||||||
 | 
					    let desc = lm.get(&language, "donate");
 | 
				
			||||||
 | 
					    let footer = footer(ctx).await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let _ = msg
 | 
				
			||||||
 | 
					        .channel_id
 | 
				
			||||||
 | 
					        .send_message(ctx, |m| {
 | 
				
			||||||
 | 
					            m.embed(move |e| {
 | 
				
			||||||
                e.title("Donate")
 | 
					                e.title("Donate")
 | 
				
			||||||
                    .description("Thinking of adding a monthly contribution? Click below for my Patreon and official bot server :)
 | 
					                    .description(desc)
 | 
				
			||||||
 | 
					 | 
				
			||||||
**https://www.patreon.com/jellywx/**
 | 
					 | 
				
			||||||
**https://discord.jellywx.com/**
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
When you subscribe, Patreon will automatically rank you up on our Discord server (make sure you link your Patreon and Discord accounts!)
 | 
					 | 
				
			||||||
With your new rank, you'll be able to:
 | 
					 | 
				
			||||||
• Set repeating reminders with `interval`, `natural` or the dashboard
 | 
					 | 
				
			||||||
• Use unlimited uploads on SoundFX
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
(Also, members of servers you __own__ will be able to set repeating reminders via commands)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Just $2 USD/month!
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
*Please note, you must be in the JellyWX Discord server to receive Patreon features*")
 | 
					 | 
				
			||||||
                    .footer(footer)
 | 
					                    .footer(footer)
 | 
				
			||||||
                    .color(*THEME_COLOR)
 | 
					                    .color(*THEME_COLOR)
 | 
				
			||||||
            }),
 | 
					            })
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        .await;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[command]
 | 
				
			||||||
 | 
					async fn dashboard(ctx: &Context, msg: &Message, _args: String) {
 | 
				
			||||||
 | 
					    let footer = footer(ctx).await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let _ = msg
 | 
				
			||||||
 | 
					        .channel_id
 | 
				
			||||||
 | 
					        .send_message(ctx, |m| {
 | 
				
			||||||
 | 
					            m.embed(move |e| {
 | 
				
			||||||
 | 
					                e.title("Dashboard")
 | 
				
			||||||
 | 
					                    .description("https://reminder-bot.com/dashboard")
 | 
				
			||||||
 | 
					                    .footer(footer)
 | 
				
			||||||
 | 
					                    .color(*THEME_COLOR)
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        .await;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[command]
 | 
				
			||||||
 | 
					async fn clock(ctx: &Context, msg: &Message, _args: String) {
 | 
				
			||||||
 | 
					    let (pool, lm) = get_ctx_data(&ctx).await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let language = UserData::language_of(&msg.author, &pool).await;
 | 
				
			||||||
 | 
					    let timezone = UserData::timezone_of(&msg.author, &pool).await;
 | 
				
			||||||
 | 
					    let meridian = UserData::meridian_of(&msg.author, &pool).await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let now = Utc::now().with_timezone(&timezone);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let clock_display = lm.get(&language, "clock/time");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let _ = msg
 | 
				
			||||||
 | 
					        .channel_id
 | 
				
			||||||
 | 
					        .say(
 | 
				
			||||||
 | 
					            &ctx,
 | 
				
			||||||
 | 
					            clock_display.replacen("{}", &now.format(meridian.fmt_str()).to_string(), 1),
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        .await;
 | 
					        .await;
 | 
				
			||||||
 | 
					 | 
				
			||||||
    Ok(())
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/// Get the link to the online dashboard
 | 
					 | 
				
			||||||
#[poise::command(slash_command)]
 | 
					 | 
				
			||||||
pub async fn dashboard(ctx: Context<'_>) -> Result<(), Error> {
 | 
					 | 
				
			||||||
    let footer = footer(ctx);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let _ = ctx
 | 
					 | 
				
			||||||
        .send(|m| {
 | 
					 | 
				
			||||||
            m.embed(|e| {
 | 
					 | 
				
			||||||
                e.title("Dashboard")
 | 
					 | 
				
			||||||
                    .description("**https://reminder-bot.com/dashboard**")
 | 
					 | 
				
			||||||
                    .footer(footer)
 | 
					 | 
				
			||||||
                    .color(*THEME_COLOR)
 | 
					 | 
				
			||||||
            })
 | 
					 | 
				
			||||||
        })
 | 
					 | 
				
			||||||
        .await;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    Ok(())
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/// View the current time in a user's selected timezone
 | 
					 | 
				
			||||||
#[poise::command(slash_command)]
 | 
					 | 
				
			||||||
pub async fn clock(ctx: Context<'_>) -> Result<(), Error> {
 | 
					 | 
				
			||||||
    ctx.defer_ephemeral().await?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let tz = ctx.timezone().await;
 | 
					 | 
				
			||||||
    let now = Utc::now().with_timezone(&tz);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    ctx.send(|m| {
 | 
					 | 
				
			||||||
        m.ephemeral(true).content(format!("Time in **{}**: `{}`", tz, now.format("%H:%M")))
 | 
					 | 
				
			||||||
    })
 | 
					 | 
				
			||||||
    .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;
 | 
				
			||||||
 
 | 
				
			|||||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -1,260 +1,443 @@
 | 
				
			|||||||
use regex_command_attr::command;
 | 
					use regex_command_attr::command;
 | 
				
			||||||
use serenity::client::Context;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::{
 | 
					use serenity::{
 | 
				
			||||||
    component_models::{
 | 
					    async_trait,
 | 
				
			||||||
        pager::{Pager, TodoPager},
 | 
					    client::Context,
 | 
				
			||||||
        ComponentDataModel, TodoSelector,
 | 
					    constants::MESSAGE_CODE_LIMIT,
 | 
				
			||||||
 | 
					    model::{
 | 
				
			||||||
 | 
					        channel::Message,
 | 
				
			||||||
 | 
					        id::{ChannelId, GuildId, UserId},
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    consts::{EMBED_DESCRIPTION_MAX_LENGTH, SELECT_MAX_ENTRIES, THEME_COLOR},
 | 
					 | 
				
			||||||
    framework::{CommandInvoke, CommandOptions, CreateGenericResponse},
 | 
					 | 
				
			||||||
    hooks::CHECK_GUILD_PERMISSIONS_HOOK,
 | 
					 | 
				
			||||||
    SQLPool,
 | 
					 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[command]
 | 
					use std::fmt;
 | 
				
			||||||
#[description("Manage todo lists")]
 | 
					
 | 
				
			||||||
#[subcommandgroup("server")]
 | 
					use crate::models::CtxGuildData;
 | 
				
			||||||
#[description("Manage the server todo list")]
 | 
					use crate::{command_help, get_ctx_data, models::UserData};
 | 
				
			||||||
#[subcommand("add")]
 | 
					use sqlx::MySqlPool;
 | 
				
			||||||
#[description("Add an item to the server todo list")]
 | 
					use std::convert::TryFrom;
 | 
				
			||||||
#[arg(
 | 
					
 | 
				
			||||||
    name = "task",
 | 
					#[derive(Debug)]
 | 
				
			||||||
    description = "The task to add to the todo list",
 | 
					struct TodoNotFound;
 | 
				
			||||||
    kind = "String",
 | 
					
 | 
				
			||||||
    required = true
 | 
					impl std::error::Error for TodoNotFound {}
 | 
				
			||||||
)]
 | 
					impl fmt::Display for TodoNotFound {
 | 
				
			||||||
#[subcommand("view")]
 | 
					    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
 | 
				
			||||||
#[description("View and remove from the server todo list")]
 | 
					        write!(f, "Todo not found")
 | 
				
			||||||
#[subcommandgroup("channel")]
 | 
					    }
 | 
				
			||||||
#[description("Manage the channel todo list")]
 | 
					}
 | 
				
			||||||
#[subcommand("add")]
 | 
					
 | 
				
			||||||
#[description("Add to the channel todo list")]
 | 
					struct Todo {
 | 
				
			||||||
#[arg(
 | 
					    id: u32,
 | 
				
			||||||
    name = "task",
 | 
					    value: String,
 | 
				
			||||||
    description = "The task to add to the todo list",
 | 
					}
 | 
				
			||||||
    kind = "String",
 | 
					
 | 
				
			||||||
    required = true
 | 
					struct TodoTarget {
 | 
				
			||||||
)]
 | 
					    user: UserId,
 | 
				
			||||||
#[subcommand("view")]
 | 
					    guild: Option<GuildId>,
 | 
				
			||||||
#[description("View and remove from the channel todo list")]
 | 
					    channel: Option<ChannelId>,
 | 
				
			||||||
#[subcommandgroup("user")]
 | 
					}
 | 
				
			||||||
#[description("Manage your personal todo list")]
 | 
					
 | 
				
			||||||
#[subcommand("add")]
 | 
					impl TodoTarget {
 | 
				
			||||||
#[description("Add to your personal todo list")]
 | 
					    pub fn command(&self, subcommand_opt: Option<SubCommand>) -> String {
 | 
				
			||||||
#[arg(
 | 
					        let context = if self.channel.is_some() {
 | 
				
			||||||
    name = "task",
 | 
					            "channel"
 | 
				
			||||||
    description = "The task to add to the todo list",
 | 
					        } else if self.guild.is_some() {
 | 
				
			||||||
    kind = "String",
 | 
					            "guild"
 | 
				
			||||||
    required = true
 | 
					 | 
				
			||||||
)]
 | 
					 | 
				
			||||||
#[subcommand("view")]
 | 
					 | 
				
			||||||
#[description("View and remove from your personal todo list")]
 | 
					 | 
				
			||||||
#[hook(CHECK_GUILD_PERMISSIONS_HOOK)]
 | 
					 | 
				
			||||||
async fn todo(ctx: &Context, invoke: &mut CommandInvoke, args: CommandOptions) {
 | 
					 | 
				
			||||||
    if invoke.guild_id().is_none() && args.subcommand_group != Some("user".to_string()) {
 | 
					 | 
				
			||||||
        let _ = invoke
 | 
					 | 
				
			||||||
            .respond(
 | 
					 | 
				
			||||||
                &ctx,
 | 
					 | 
				
			||||||
                CreateGenericResponse::new().content("Please use `/todo user` in direct messages"),
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
            .await;
 | 
					 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
        let pool = ctx.data.read().await.get::<SQLPool>().cloned().unwrap();
 | 
					            "user"
 | 
				
			||||||
 | 
					 | 
				
			||||||
        let keys = match args.subcommand_group.as_ref().unwrap().as_str() {
 | 
					 | 
				
			||||||
            "server" => (None, None, invoke.guild_id().map(|g| g.0)),
 | 
					 | 
				
			||||||
            "channel" => (None, Some(invoke.channel_id().0), invoke.guild_id().map(|g| g.0)),
 | 
					 | 
				
			||||||
            _ => (Some(invoke.author_id().0), None, None),
 | 
					 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        match args.get("task") {
 | 
					        if let Some(subcommand) = subcommand_opt {
 | 
				
			||||||
            Some(task) => {
 | 
					            format!("todo {} {}", context, subcommand.to_string())
 | 
				
			||||||
                let task = task.to_string();
 | 
					        } else {
 | 
				
			||||||
 | 
					            format!("todo {}", context)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn name(&self) -> String {
 | 
				
			||||||
 | 
					        if self.channel.is_some() {
 | 
				
			||||||
 | 
					            "Channel"
 | 
				
			||||||
 | 
					        } else if self.guild.is_some() {
 | 
				
			||||||
 | 
					            "Guild"
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            "User"
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        .to_string()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub async fn view(
 | 
				
			||||||
 | 
					        &self,
 | 
				
			||||||
 | 
					        pool: MySqlPool,
 | 
				
			||||||
 | 
					    ) -> Result<Vec<Todo>, Box<dyn std::error::Error + Send + Sync>> {
 | 
				
			||||||
 | 
					        Ok(if let Some(cid) = self.channel {
 | 
				
			||||||
 | 
					            sqlx::query_as!(
 | 
				
			||||||
 | 
					                Todo,
 | 
				
			||||||
 | 
					                "
 | 
				
			||||||
 | 
					SELECT id, value FROM todos WHERE channel_id = (SELECT id FROM channels WHERE channel = ?)
 | 
				
			||||||
 | 
					                ",
 | 
				
			||||||
 | 
					                cid.as_u64()
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            .fetch_all(&pool)
 | 
				
			||||||
 | 
					            .await?
 | 
				
			||||||
 | 
					        } else if let Some(gid) = self.guild {
 | 
				
			||||||
 | 
					            sqlx::query_as!(
 | 
				
			||||||
 | 
					                Todo,
 | 
				
			||||||
 | 
					                "
 | 
				
			||||||
 | 
					SELECT id, value FROM todos WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?) AND channel_id IS NULL
 | 
				
			||||||
 | 
					                ",
 | 
				
			||||||
 | 
					                gid.as_u64()
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            .fetch_all(&pool)
 | 
				
			||||||
 | 
					            .await?
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            sqlx::query_as!(
 | 
				
			||||||
 | 
					                Todo,
 | 
				
			||||||
 | 
					                "
 | 
				
			||||||
 | 
					SELECT id, value FROM todos WHERE user_id = (SELECT id FROM users WHERE user = ?) AND guild_id IS NULL
 | 
				
			||||||
 | 
					                ",
 | 
				
			||||||
 | 
					                self.user.as_u64()
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            .fetch_all(&pool)
 | 
				
			||||||
 | 
					            .await?
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub async fn add(
 | 
				
			||||||
 | 
					        &self,
 | 
				
			||||||
 | 
					        value: String,
 | 
				
			||||||
 | 
					        pool: MySqlPool,
 | 
				
			||||||
 | 
					    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
 | 
				
			||||||
 | 
					        if let (Some(cid), Some(gid)) = (self.channel, self.guild) {
 | 
				
			||||||
            sqlx::query!(
 | 
					            sqlx::query!(
 | 
				
			||||||
                    "INSERT INTO todos (user_id, channel_id, guild_id, value) VALUES ((SELECT id FROM users WHERE user = ?), (SELECT id FROM channels WHERE channel = ?), (SELECT id FROM guilds WHERE guild = ?), ?)",
 | 
					                "
 | 
				
			||||||
                    keys.0,
 | 
					INSERT INTO todos (user_id, guild_id, channel_id, value) VALUES (
 | 
				
			||||||
                    keys.1,
 | 
					    (SELECT id FROM users WHERE user = ?),
 | 
				
			||||||
                    keys.2,
 | 
					    (SELECT id FROM guilds WHERE guild = ?),
 | 
				
			||||||
                    task
 | 
					    (SELECT id FROM channels WHERE channel = ?),
 | 
				
			||||||
 | 
					    ?
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					                ",
 | 
				
			||||||
 | 
					                self.user.as_u64(),
 | 
				
			||||||
 | 
					                gid.as_u64(),
 | 
				
			||||||
 | 
					                cid.as_u64(),
 | 
				
			||||||
 | 
					                value
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
            .execute(&pool)
 | 
					            .execute(&pool)
 | 
				
			||||||
                .await
 | 
					            .await?;
 | 
				
			||||||
                .unwrap();
 | 
					        } else if let Some(gid) = self.guild {
 | 
				
			||||||
 | 
					            sqlx::query!(
 | 
				
			||||||
 | 
					                "
 | 
				
			||||||
 | 
					INSERT INTO todos (user_id, guild_id, value) VALUES (
 | 
				
			||||||
 | 
					    (SELECT id FROM users WHERE user = ?),
 | 
				
			||||||
 | 
					    (SELECT id FROM guilds WHERE guild = ?),
 | 
				
			||||||
 | 
					    ?
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					                ",
 | 
				
			||||||
 | 
					                self.user.as_u64(),
 | 
				
			||||||
 | 
					                gid.as_u64(),
 | 
				
			||||||
 | 
					                value
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            .execute(&pool)
 | 
				
			||||||
 | 
					            .await?;
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            sqlx::query!(
 | 
				
			||||||
 | 
					                "
 | 
				
			||||||
 | 
					INSERT INTO todos (user_id, value) VALUES (
 | 
				
			||||||
 | 
					    (SELECT id FROM users WHERE user = ?),
 | 
				
			||||||
 | 
					    ?
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					                ",
 | 
				
			||||||
 | 
					                self.user.as_u64(),
 | 
				
			||||||
 | 
					                value
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            .execute(&pool)
 | 
				
			||||||
 | 
					            .await?;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                let _ = invoke
 | 
					        Ok(())
 | 
				
			||||||
                    .respond(&ctx, CreateGenericResponse::new().content("Item added to todo list"))
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub async fn remove(
 | 
				
			||||||
 | 
					        &self,
 | 
				
			||||||
 | 
					        num: usize,
 | 
				
			||||||
 | 
					        pool: &MySqlPool,
 | 
				
			||||||
 | 
					    ) -> Result<Todo, Box<dyn std::error::Error + Sync + Send>> {
 | 
				
			||||||
 | 
					        let todos = self.view(pool.clone()).await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if let Some(removal_todo) = todos.get(num) {
 | 
				
			||||||
 | 
					            let deleting = sqlx::query_as!(
 | 
				
			||||||
 | 
					                Todo,
 | 
				
			||||||
 | 
					                "
 | 
				
			||||||
 | 
					SELECT id, value FROM todos WHERE id = ?
 | 
				
			||||||
 | 
					                ",
 | 
				
			||||||
 | 
					                removal_todo.id
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            .fetch_one(&pool.clone())
 | 
				
			||||||
 | 
					            .await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            sqlx::query!(
 | 
				
			||||||
 | 
					                "
 | 
				
			||||||
 | 
					DELETE FROM todos WHERE id = ?
 | 
				
			||||||
 | 
					                ",
 | 
				
			||||||
 | 
					                removal_todo.id
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            .execute(pool)
 | 
				
			||||||
 | 
					            .await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            Ok(deleting)
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            Err(Box::new(TodoNotFound))
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub async fn clear(
 | 
				
			||||||
 | 
					        &self,
 | 
				
			||||||
 | 
					        pool: &MySqlPool,
 | 
				
			||||||
 | 
					    ) -> Result<(), Box<dyn std::error::Error + Sync + Send>> {
 | 
				
			||||||
 | 
					        if let Some(cid) = self.channel {
 | 
				
			||||||
 | 
					            sqlx::query!(
 | 
				
			||||||
 | 
					                "
 | 
				
			||||||
 | 
					DELETE FROM todos WHERE channel_id = (SELECT id FROM channels WHERE channel = ?)
 | 
				
			||||||
 | 
					                ",
 | 
				
			||||||
 | 
					                cid.as_u64()
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            .execute(pool)
 | 
				
			||||||
 | 
					            .await?;
 | 
				
			||||||
 | 
					        } else if let Some(gid) = self.guild {
 | 
				
			||||||
 | 
					            sqlx::query!(
 | 
				
			||||||
 | 
					                "
 | 
				
			||||||
 | 
					DELETE FROM todos WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?) AND channel_id IS NULL
 | 
				
			||||||
 | 
					                ",
 | 
				
			||||||
 | 
					                gid.as_u64()
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            .execute(pool)
 | 
				
			||||||
 | 
					            .await?;
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            sqlx::query!(
 | 
				
			||||||
 | 
					                "
 | 
				
			||||||
 | 
					DELETE FROM todos WHERE user_id = (SELECT id FROM users WHERE user = ?) AND guild_id IS NULL
 | 
				
			||||||
 | 
					                ",
 | 
				
			||||||
 | 
					                self.user.as_u64()
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            .execute(pool)
 | 
				
			||||||
 | 
					            .await?;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Ok(())
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async fn execute(&self, ctx: &Context, msg: &Message, subcommand: SubCommand, extra: String) {
 | 
				
			||||||
 | 
					        let (pool, lm) = get_ctx_data(&ctx).await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let user_data = UserData::from_user(&msg.author, &ctx, &pool).await.unwrap();
 | 
				
			||||||
 | 
					        let prefix = ctx.prefix(msg.guild_id).await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        match subcommand {
 | 
				
			||||||
 | 
					            SubCommand::View => {
 | 
				
			||||||
 | 
					                let todo_items = self.view(pool).await.unwrap();
 | 
				
			||||||
 | 
					                let mut todo_groups = vec!["".to_string()];
 | 
				
			||||||
 | 
					                let mut char_count = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                todo_items.iter().enumerate().for_each(|(count, todo)| {
 | 
				
			||||||
 | 
					                    let display = format!("{}: {}\n", count + 1, todo.value);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    if char_count + display.len() > MESSAGE_CODE_LIMIT as usize {
 | 
				
			||||||
 | 
					                        char_count = display.len();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        todo_groups.push(display);
 | 
				
			||||||
 | 
					                    } else {
 | 
				
			||||||
 | 
					                        char_count += display.len();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        let last_group = todo_groups.pop().unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        todo_groups.push(format!("{}{}", last_group, display));
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                for group in todo_groups {
 | 
				
			||||||
 | 
					                    let _ = msg
 | 
				
			||||||
 | 
					                        .channel_id
 | 
				
			||||||
 | 
					                        .send_message(&ctx, |m| {
 | 
				
			||||||
 | 
					                            m.embed(|e| e.title(format!("{} Todo", self.name())).description(group))
 | 
				
			||||||
 | 
					                        })
 | 
				
			||||||
                        .await;
 | 
					                        .await;
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            None => {
 | 
					 | 
				
			||||||
                let values = if let Some(uid) = keys.0 {
 | 
					 | 
				
			||||||
                    sqlx::query!(
 | 
					 | 
				
			||||||
                        "SELECT todos.id, value FROM todos
 | 
					 | 
				
			||||||
INNER JOIN users ON todos.user_id = users.id
 | 
					 | 
				
			||||||
WHERE users.user = ?",
 | 
					 | 
				
			||||||
                        uid,
 | 
					 | 
				
			||||||
                    )
 | 
					 | 
				
			||||||
                    .fetch_all(&pool)
 | 
					 | 
				
			||||||
                    .await
 | 
					 | 
				
			||||||
                    .unwrap()
 | 
					 | 
				
			||||||
                    .iter()
 | 
					 | 
				
			||||||
                    .map(|row| (row.id as usize, row.value.clone()))
 | 
					 | 
				
			||||||
                    .collect::<Vec<(usize, String)>>()
 | 
					 | 
				
			||||||
                } else if let Some(cid) = keys.1 {
 | 
					 | 
				
			||||||
                    sqlx::query!(
 | 
					 | 
				
			||||||
                        "SELECT todos.id, value FROM todos
 | 
					 | 
				
			||||||
INNER JOIN channels ON todos.channel_id = channels.id
 | 
					 | 
				
			||||||
WHERE channels.channel = ?",
 | 
					 | 
				
			||||||
                        cid,
 | 
					 | 
				
			||||||
                    )
 | 
					 | 
				
			||||||
                    .fetch_all(&pool)
 | 
					 | 
				
			||||||
                    .await
 | 
					 | 
				
			||||||
                    .unwrap()
 | 
					 | 
				
			||||||
                    .iter()
 | 
					 | 
				
			||||||
                    .map(|row| (row.id as usize, row.value.clone()))
 | 
					 | 
				
			||||||
                    .collect::<Vec<(usize, String)>>()
 | 
					 | 
				
			||||||
                } else {
 | 
					 | 
				
			||||||
                    sqlx::query!(
 | 
					 | 
				
			||||||
                        "SELECT todos.id, value FROM todos
 | 
					 | 
				
			||||||
INNER JOIN guilds ON todos.guild_id = guilds.id
 | 
					 | 
				
			||||||
WHERE guilds.guild = ?",
 | 
					 | 
				
			||||||
                        keys.2,
 | 
					 | 
				
			||||||
                    )
 | 
					 | 
				
			||||||
                    .fetch_all(&pool)
 | 
					 | 
				
			||||||
                    .await
 | 
					 | 
				
			||||||
                    .unwrap()
 | 
					 | 
				
			||||||
                    .iter()
 | 
					 | 
				
			||||||
                    .map(|row| (row.id as usize, row.value.clone()))
 | 
					 | 
				
			||||||
                    .collect::<Vec<(usize, String)>>()
 | 
					 | 
				
			||||||
                };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                let resp = show_todo_page(&values, 0, keys.0, keys.1, keys.2);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                invoke.respond(&ctx, resp).await.unwrap();
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub fn max_todo_page(todo_values: &[(usize, String)]) -> usize {
 | 
					            SubCommand::Add => {
 | 
				
			||||||
    let mut rows = 0;
 | 
					                let content = lm
 | 
				
			||||||
    let mut char_count = 0;
 | 
					                    .get(&user_data.language, "todo/added")
 | 
				
			||||||
 | 
					                    .replacen("{name}", &extra, 1);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    todo_values.iter().enumerate().map(|(c, (_, v))| format!("{}: {}", c, v)).fold(
 | 
					                self.add(extra, pool).await.unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                let _ = msg.channel_id.say(&ctx, content).await;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            SubCommand::Remove => {
 | 
				
			||||||
 | 
					                if let Ok(num) = extra.parse::<usize>() {
 | 
				
			||||||
 | 
					                    if let Ok(todo) = self.remove(num - 1, &pool).await {
 | 
				
			||||||
 | 
					                        let content = lm.get(&user_data.language, "todo/removed").replacen(
 | 
				
			||||||
 | 
					                            "{}",
 | 
				
			||||||
 | 
					                            &todo.value,
 | 
				
			||||||
                            1,
 | 
					                            1,
 | 
				
			||||||
        |mut pages, text| {
 | 
					                        );
 | 
				
			||||||
            rows += 1;
 | 
					 | 
				
			||||||
            char_count += text.len();
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if char_count > EMBED_DESCRIPTION_MAX_LENGTH || rows > SELECT_MAX_ENTRIES {
 | 
					                        let _ = msg.channel_id.say(&ctx, content).await;
 | 
				
			||||||
                rows = 1;
 | 
					 | 
				
			||||||
                char_count = text.len();
 | 
					 | 
				
			||||||
                pages += 1;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            pages
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
pub fn show_todo_page(
 | 
					 | 
				
			||||||
    todo_values: &[(usize, String)],
 | 
					 | 
				
			||||||
    page: usize,
 | 
					 | 
				
			||||||
    user_id: Option<u64>,
 | 
					 | 
				
			||||||
    channel_id: Option<u64>,
 | 
					 | 
				
			||||||
    guild_id: Option<u64>,
 | 
					 | 
				
			||||||
) -> CreateGenericResponse {
 | 
					 | 
				
			||||||
    let pager = TodoPager::new(page, user_id, channel_id, guild_id);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let pages = max_todo_page(todo_values);
 | 
					 | 
				
			||||||
    let mut page = page;
 | 
					 | 
				
			||||||
    if page >= pages {
 | 
					 | 
				
			||||||
        page = pages - 1;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let mut char_count = 0;
 | 
					 | 
				
			||||||
    let mut rows = 0;
 | 
					 | 
				
			||||||
    let mut skipped_rows = 0;
 | 
					 | 
				
			||||||
    let mut skipped_char_count = 0;
 | 
					 | 
				
			||||||
    let mut first_num = 0;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let mut skipped_pages = 0;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let (todo_ids, display_vec): (Vec<usize>, Vec<String>) = todo_values
 | 
					 | 
				
			||||||
        .iter()
 | 
					 | 
				
			||||||
        .enumerate()
 | 
					 | 
				
			||||||
        .map(|(c, (i, v))| (i, format!("`{}`: {}", c + 1, v)))
 | 
					 | 
				
			||||||
        .skip_while(|(_, p)| {
 | 
					 | 
				
			||||||
            first_num += 1;
 | 
					 | 
				
			||||||
            skipped_rows += 1;
 | 
					 | 
				
			||||||
            skipped_char_count += p.len();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if skipped_char_count > EMBED_DESCRIPTION_MAX_LENGTH
 | 
					 | 
				
			||||||
                || skipped_rows > SELECT_MAX_ENTRIES
 | 
					 | 
				
			||||||
            {
 | 
					 | 
				
			||||||
                skipped_rows = 1;
 | 
					 | 
				
			||||||
                skipped_char_count = p.len();
 | 
					 | 
				
			||||||
                skipped_pages += 1;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            skipped_pages < page
 | 
					 | 
				
			||||||
        })
 | 
					 | 
				
			||||||
        .take_while(|(_, p)| {
 | 
					 | 
				
			||||||
            rows += 1;
 | 
					 | 
				
			||||||
            char_count += p.len();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            char_count < EMBED_DESCRIPTION_MAX_LENGTH && rows <= SELECT_MAX_ENTRIES
 | 
					 | 
				
			||||||
        })
 | 
					 | 
				
			||||||
        .unzip();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let display = display_vec.join("\n");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let title = if user_id.is_some() {
 | 
					 | 
				
			||||||
        "Your"
 | 
					 | 
				
			||||||
    } else if channel_id.is_some() {
 | 
					 | 
				
			||||||
        "Channel"
 | 
					 | 
				
			||||||
                    } else {
 | 
					                    } else {
 | 
				
			||||||
        "Server"
 | 
					                        let _ = msg
 | 
				
			||||||
 | 
					                            .channel_id
 | 
				
			||||||
 | 
					                            .say(&ctx, lm.get(&user_data.language, "todo/error_index"))
 | 
				
			||||||
 | 
					                            .await;
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    let content = lm
 | 
				
			||||||
 | 
					                        .get(&user_data.language, "todo/error_value")
 | 
				
			||||||
 | 
					                        .replacen("{prefix}", &prefix, 1)
 | 
				
			||||||
 | 
					                        .replacen("{command}", &self.command(Some(subcommand)), 1);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    let _ = msg.channel_id.say(&ctx, content).await;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            SubCommand::Clear => {
 | 
				
			||||||
 | 
					                self.clear(&pool).await.unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                let content = lm.get(&user_data.language, "todo/cleared");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                let _ = msg.channel_id.say(&ctx, content).await;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					enum SubCommand {
 | 
				
			||||||
 | 
					    View,
 | 
				
			||||||
 | 
					    Add,
 | 
				
			||||||
 | 
					    Remove,
 | 
				
			||||||
 | 
					    Clear,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl TryFrom<Option<&str>> for SubCommand {
 | 
				
			||||||
 | 
					    type Error = ();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn try_from(value: Option<&str>) -> Result<Self, Self::Error> {
 | 
				
			||||||
 | 
					        match value {
 | 
				
			||||||
 | 
					            Some("add") => Ok(SubCommand::Add),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            Some("remove") => Ok(SubCommand::Remove),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            Some("clear") => Ok(SubCommand::Clear),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            None | Some("") => Ok(SubCommand::View),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            Some(_unrecognised) => Err(()),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl ToString for SubCommand {
 | 
				
			||||||
 | 
					    fn to_string(&self) -> String {
 | 
				
			||||||
 | 
					        match self {
 | 
				
			||||||
 | 
					            SubCommand::View => "",
 | 
				
			||||||
 | 
					            SubCommand::Add => "add",
 | 
				
			||||||
 | 
					            SubCommand::Remove => "remove",
 | 
				
			||||||
 | 
					            SubCommand::Clear => "clear",
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        .to_string()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[async_trait]
 | 
				
			||||||
 | 
					trait Execute {
 | 
				
			||||||
 | 
					    async fn execute(self, ctx: &Context, msg: &Message, extra: String, target: TodoTarget);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[async_trait]
 | 
				
			||||||
 | 
					impl Execute for Result<SubCommand, ()> {
 | 
				
			||||||
 | 
					    async fn execute(self, ctx: &Context, msg: &Message, extra: String, target: TodoTarget) {
 | 
				
			||||||
 | 
					        if let Ok(subcommand) = self {
 | 
				
			||||||
 | 
					            target.execute(ctx, msg, subcommand, extra).await;
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            show_help(&ctx, msg, Some(target)).await;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[command("todo")]
 | 
				
			||||||
 | 
					async fn todo_user(ctx: &Context, msg: &Message, args: String) {
 | 
				
			||||||
 | 
					    let mut split = args.split(' ');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let target = TodoTarget {
 | 
				
			||||||
 | 
					        user: msg.author.id,
 | 
				
			||||||
 | 
					        guild: None,
 | 
				
			||||||
 | 
					        channel: None,
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if todo_ids.is_empty() {
 | 
					    let subcommand_opt = SubCommand::try_from(split.next());
 | 
				
			||||||
        CreateGenericResponse::new().embed(|e| {
 | 
					
 | 
				
			||||||
            e.title(format!("{} Todo List", title))
 | 
					    subcommand_opt
 | 
				
			||||||
                .description("Todo List Empty!")
 | 
					        .execute(ctx, msg, split.collect::<Vec<&str>>().join(" "), target)
 | 
				
			||||||
                .footer(|f| f.text(format!("Page {} of {}", page + 1, pages)))
 | 
					        .await;
 | 
				
			||||||
                .color(*THEME_COLOR)
 | 
					}
 | 
				
			||||||
        })
 | 
					
 | 
				
			||||||
 | 
					#[command("todoc")]
 | 
				
			||||||
 | 
					#[supports_dm(false)]
 | 
				
			||||||
 | 
					#[permission_level(Managed)]
 | 
				
			||||||
 | 
					async fn todo_channel(ctx: &Context, msg: &Message, args: String) {
 | 
				
			||||||
 | 
					    let mut split = args.split(' ');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let target = TodoTarget {
 | 
				
			||||||
 | 
					        user: msg.author.id,
 | 
				
			||||||
 | 
					        guild: msg.guild_id,
 | 
				
			||||||
 | 
					        channel: Some(msg.channel_id),
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let subcommand_opt = SubCommand::try_from(split.next());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    subcommand_opt
 | 
				
			||||||
 | 
					        .execute(ctx, msg, split.collect::<Vec<&str>>().join(" "), target)
 | 
				
			||||||
 | 
					        .await;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[command("todos")]
 | 
				
			||||||
 | 
					#[supports_dm(false)]
 | 
				
			||||||
 | 
					#[permission_level(Managed)]
 | 
				
			||||||
 | 
					async fn todo_guild(ctx: &Context, msg: &Message, args: String) {
 | 
				
			||||||
 | 
					    let mut split = args.split(' ');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let target = TodoTarget {
 | 
				
			||||||
 | 
					        user: msg.author.id,
 | 
				
			||||||
 | 
					        guild: msg.guild_id,
 | 
				
			||||||
 | 
					        channel: None,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let subcommand_opt = SubCommand::try_from(split.next());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    subcommand_opt
 | 
				
			||||||
 | 
					        .execute(ctx, msg, split.collect::<Vec<&str>>().join(" "), target)
 | 
				
			||||||
 | 
					        .await;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async fn show_help(ctx: &Context, msg: &Message, target: Option<TodoTarget>) {
 | 
				
			||||||
 | 
					    let (pool, lm) = get_ctx_data(&ctx).await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let language = UserData::language_of(&msg.author, &pool);
 | 
				
			||||||
 | 
					    let prefix = ctx.prefix(msg.guild_id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let command = match target {
 | 
				
			||||||
 | 
					        None => "todo",
 | 
				
			||||||
 | 
					        Some(t) => {
 | 
				
			||||||
 | 
					            if t.channel.is_some() {
 | 
				
			||||||
 | 
					                "todoc"
 | 
				
			||||||
 | 
					            } else if t.guild.is_some() {
 | 
				
			||||||
 | 
					                "todos"
 | 
				
			||||||
            } else {
 | 
					            } else {
 | 
				
			||||||
        let todo_selector =
 | 
					                "todo"
 | 
				
			||||||
            ComponentDataModel::TodoSelector(TodoSelector { page, user_id, channel_id, guild_id });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        CreateGenericResponse::new()
 | 
					 | 
				
			||||||
            .embed(|e| {
 | 
					 | 
				
			||||||
                e.title(format!("{} Todo List", title))
 | 
					 | 
				
			||||||
                    .description(display)
 | 
					 | 
				
			||||||
                    .footer(|f| f.text(format!("Page {} of {}", page + 1, pages)))
 | 
					 | 
				
			||||||
                    .color(*THEME_COLOR)
 | 
					 | 
				
			||||||
            })
 | 
					 | 
				
			||||||
            .components(|comp| {
 | 
					 | 
				
			||||||
                pager.create_button_row(pages, comp);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                comp.create_action_row(|row| {
 | 
					 | 
				
			||||||
                    row.create_select_menu(|menu| {
 | 
					 | 
				
			||||||
                        menu.custom_id(todo_selector.to_custom_id()).options(|opt| {
 | 
					 | 
				
			||||||
                            for (count, (id, disp)) in todo_ids.iter().zip(&display_vec).enumerate()
 | 
					 | 
				
			||||||
                            {
 | 
					 | 
				
			||||||
                                opt.create_option(|o| {
 | 
					 | 
				
			||||||
                                    o.label(format!("Mark {} complete", count + first_num))
 | 
					 | 
				
			||||||
                                        .value(id)
 | 
					 | 
				
			||||||
                                        .description(disp.split_once(" ").unwrap_or(("", "")).1)
 | 
					 | 
				
			||||||
                                });
 | 
					 | 
				
			||||||
                            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                            opt
 | 
					 | 
				
			||||||
                        })
 | 
					 | 
				
			||||||
                    })
 | 
					 | 
				
			||||||
                })
 | 
					 | 
				
			||||||
            })
 | 
					 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    command_help(ctx, msg, lm, &prefix.await, &language.await, command).await;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,303 +0,0 @@
 | 
				
			|||||||
pub(crate) mod pager;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use std::io::Cursor;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use chrono_tz::Tz;
 | 
					 | 
				
			||||||
use poise::serenity::{
 | 
					 | 
				
			||||||
    builder::CreateEmbed,
 | 
					 | 
				
			||||||
    client::Context,
 | 
					 | 
				
			||||||
    model::{
 | 
					 | 
				
			||||||
        channel::Channel,
 | 
					 | 
				
			||||||
        interactions::{message_component::MessageComponentInteraction, InteractionResponseType},
 | 
					 | 
				
			||||||
        prelude::InteractionApplicationCommandCallbackDataFlags,
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
use rmp_serde::Serializer;
 | 
					 | 
				
			||||||
use serde::{Deserialize, Serialize};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use crate::{
 | 
					 | 
				
			||||||
    self,
 | 
					 | 
				
			||||||
    component_models::pager::{DelPager, LookPager, MacroPager, Pager, TodoPager},
 | 
					 | 
				
			||||||
    consts::{EMBED_DESCRIPTION_MAX_LENGTH, THEME_COLOR},
 | 
					 | 
				
			||||||
    models::{command_macro::CommandMacro, reminder::Reminder},
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Deserialize, Serialize)]
 | 
					 | 
				
			||||||
#[serde(tag = "type")]
 | 
					 | 
				
			||||||
#[repr(u8)]
 | 
					 | 
				
			||||||
pub enum ComponentDataModel {
 | 
					 | 
				
			||||||
    LookPager(LookPager),
 | 
					 | 
				
			||||||
    DelPager(DelPager),
 | 
					 | 
				
			||||||
    TodoPager(TodoPager),
 | 
					 | 
				
			||||||
    DelSelector(DelSelector),
 | 
					 | 
				
			||||||
    TodoSelector(TodoSelector),
 | 
					 | 
				
			||||||
    MacroPager(MacroPager),
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl ComponentDataModel {
 | 
					 | 
				
			||||||
    pub fn to_custom_id(&self) -> String {
 | 
					 | 
				
			||||||
        let mut buf = Vec::new();
 | 
					 | 
				
			||||||
        self.serialize(&mut Serializer::new(&mut buf)).unwrap();
 | 
					 | 
				
			||||||
        base64::encode(buf)
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    pub fn from_custom_id(data: &String) -> Self {
 | 
					 | 
				
			||||||
        let buf = base64::decode(data)
 | 
					 | 
				
			||||||
            .map_err(|e| format!("Could not decode `custom_id' {}: {:?}", data, e))
 | 
					 | 
				
			||||||
            .unwrap();
 | 
					 | 
				
			||||||
        let cur = Cursor::new(buf);
 | 
					 | 
				
			||||||
        rmp_serde::from_read(cur).unwrap()
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    pub async fn act(&self, ctx: &Context, component: MessageComponentInteraction) {
 | 
					 | 
				
			||||||
        match self {
 | 
					 | 
				
			||||||
            ComponentDataModel::LookPager(pager) => {
 | 
					 | 
				
			||||||
                let flags = pager.flags;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                let channel_opt = component.channel_id.to_channel_cached(&ctx);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                let channel_id = if let Some(Channel::Guild(channel)) = channel_opt {
 | 
					 | 
				
			||||||
                    if Some(channel.guild_id) == component.guild_id {
 | 
					 | 
				
			||||||
                        flags.channel_id.unwrap_or(component.channel_id)
 | 
					 | 
				
			||||||
                    } else {
 | 
					 | 
				
			||||||
                        component.channel_id
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                } else {
 | 
					 | 
				
			||||||
                    component.channel_id
 | 
					 | 
				
			||||||
                };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                let reminders = Reminder::from_channel(ctx, channel_id, &flags).await;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                let pages = reminders
 | 
					 | 
				
			||||||
                    .iter()
 | 
					 | 
				
			||||||
                    .map(|reminder| reminder.display(&flags, &pager.timezone))
 | 
					 | 
				
			||||||
                    .fold(0, |t, r| t + r.len())
 | 
					 | 
				
			||||||
                    .div_ceil(EMBED_DESCRIPTION_MAX_LENGTH);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                let channel_name =
 | 
					 | 
				
			||||||
                    if let Some(Channel::Guild(channel)) = channel_id.to_channel_cached(&ctx) {
 | 
					 | 
				
			||||||
                        Some(channel.name)
 | 
					 | 
				
			||||||
                    } else {
 | 
					 | 
				
			||||||
                        None
 | 
					 | 
				
			||||||
                    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                let next_page = pager.next_page(pages);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                let mut char_count = 0;
 | 
					 | 
				
			||||||
                let mut skip_char_count = 0;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                let display = reminders
 | 
					 | 
				
			||||||
                    .iter()
 | 
					 | 
				
			||||||
                    .map(|reminder| reminder.display(&flags, &pager.timezone))
 | 
					 | 
				
			||||||
                    .skip_while(|p| {
 | 
					 | 
				
			||||||
                        skip_char_count += p.len();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                        skip_char_count < EMBED_DESCRIPTION_MAX_LENGTH * next_page as usize
 | 
					 | 
				
			||||||
                    })
 | 
					 | 
				
			||||||
                    .take_while(|p| {
 | 
					 | 
				
			||||||
                        char_count += p.len();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                        char_count < EMBED_DESCRIPTION_MAX_LENGTH
 | 
					 | 
				
			||||||
                    })
 | 
					 | 
				
			||||||
                    .collect::<Vec<String>>()
 | 
					 | 
				
			||||||
                    .join("\n");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                let mut embed = CreateEmbed::default();
 | 
					 | 
				
			||||||
                embed
 | 
					 | 
				
			||||||
                    .title(format!(
 | 
					 | 
				
			||||||
                        "Reminders{}",
 | 
					 | 
				
			||||||
                        channel_name.map_or(String::new(), |n| format!(" on #{}", n))
 | 
					 | 
				
			||||||
                    ))
 | 
					 | 
				
			||||||
                    .description(display)
 | 
					 | 
				
			||||||
                    .footer(|f| f.text(format!("Page {} of {}", next_page + 1, pages)))
 | 
					 | 
				
			||||||
                    .color(*THEME_COLOR);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                let _ = component
 | 
					 | 
				
			||||||
                    .create_interaction_response(&ctx, |r| {
 | 
					 | 
				
			||||||
                        r.kind(InteractionResponseType::UpdateMessage).interaction_response_data(
 | 
					 | 
				
			||||||
                            |response| {
 | 
					 | 
				
			||||||
                                response.embeds(vec![embed]).components(|comp| {
 | 
					 | 
				
			||||||
                                    pager.create_button_row(pages, comp);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                                    comp
 | 
					 | 
				
			||||||
                                })
 | 
					 | 
				
			||||||
                            },
 | 
					 | 
				
			||||||
                        )
 | 
					 | 
				
			||||||
                    })
 | 
					 | 
				
			||||||
                    .await;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            ComponentDataModel::DelPager(pager) => {
 | 
					 | 
				
			||||||
                let reminders =
 | 
					 | 
				
			||||||
                    Reminder::from_guild(ctx, component.guild_id, component.user.id).await;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                let max_pages = max_delete_page(&reminders, &pager.timezone);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                let resp = show_delete_page(&reminders, pager.next_page(max_pages), pager.timezone);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                let mut invoke = CommandInvoke::component(component);
 | 
					 | 
				
			||||||
                let _ = invoke.respond(&ctx, resp).await;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            ComponentDataModel::DelSelector(selector) => {
 | 
					 | 
				
			||||||
                let pool = ctx.data.read().await.get::<SQLPool>().cloned().unwrap();
 | 
					 | 
				
			||||||
                let selected_id = component.data.values.join(",");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                sqlx::query!("DELETE FROM reminders WHERE FIND_IN_SET(id, ?)", selected_id)
 | 
					 | 
				
			||||||
                    .execute(&pool)
 | 
					 | 
				
			||||||
                    .await
 | 
					 | 
				
			||||||
                    .unwrap();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                let reminders =
 | 
					 | 
				
			||||||
                    Reminder::from_guild(ctx, component.guild_id, component.user.id).await;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                let resp = show_delete_page(&reminders, selector.page, selector.timezone);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                let mut invoke = CommandInvoke::component(component);
 | 
					 | 
				
			||||||
                let _ = invoke.respond(&ctx, resp).await;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            ComponentDataModel::TodoPager(pager) => {
 | 
					 | 
				
			||||||
                if Some(component.user.id.0) == pager.user_id || pager.user_id.is_none() {
 | 
					 | 
				
			||||||
                    let pool = ctx.data.read().await.get::<SQLPool>().cloned().unwrap();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    let values = if let Some(uid) = pager.user_id {
 | 
					 | 
				
			||||||
                        sqlx::query!(
 | 
					 | 
				
			||||||
                            "SELECT todos.id, value FROM todos
 | 
					 | 
				
			||||||
    INNER JOIN users ON todos.user_id = users.id
 | 
					 | 
				
			||||||
    WHERE users.user = ?",
 | 
					 | 
				
			||||||
                            uid,
 | 
					 | 
				
			||||||
                        )
 | 
					 | 
				
			||||||
                        .fetch_all(&pool)
 | 
					 | 
				
			||||||
                        .await
 | 
					 | 
				
			||||||
                        .unwrap()
 | 
					 | 
				
			||||||
                        .iter()
 | 
					 | 
				
			||||||
                        .map(|row| (row.id as usize, row.value.clone()))
 | 
					 | 
				
			||||||
                        .collect::<Vec<(usize, String)>>()
 | 
					 | 
				
			||||||
                    } else if let Some(cid) = pager.channel_id {
 | 
					 | 
				
			||||||
                        sqlx::query!(
 | 
					 | 
				
			||||||
                            "SELECT todos.id, value FROM todos
 | 
					 | 
				
			||||||
    INNER JOIN channels ON todos.channel_id = channels.id
 | 
					 | 
				
			||||||
    WHERE channels.channel = ?",
 | 
					 | 
				
			||||||
                            cid,
 | 
					 | 
				
			||||||
                        )
 | 
					 | 
				
			||||||
                        .fetch_all(&pool)
 | 
					 | 
				
			||||||
                        .await
 | 
					 | 
				
			||||||
                        .unwrap()
 | 
					 | 
				
			||||||
                        .iter()
 | 
					 | 
				
			||||||
                        .map(|row| (row.id as usize, row.value.clone()))
 | 
					 | 
				
			||||||
                        .collect::<Vec<(usize, String)>>()
 | 
					 | 
				
			||||||
                    } else {
 | 
					 | 
				
			||||||
                        sqlx::query!(
 | 
					 | 
				
			||||||
                            "SELECT todos.id, value FROM todos
 | 
					 | 
				
			||||||
    INNER JOIN guilds ON todos.guild_id = guilds.id
 | 
					 | 
				
			||||||
    WHERE guilds.guild = ?",
 | 
					 | 
				
			||||||
                            pager.guild_id,
 | 
					 | 
				
			||||||
                        )
 | 
					 | 
				
			||||||
                        .fetch_all(&pool)
 | 
					 | 
				
			||||||
                        .await
 | 
					 | 
				
			||||||
                        .unwrap()
 | 
					 | 
				
			||||||
                        .iter()
 | 
					 | 
				
			||||||
                        .map(|row| (row.id as usize, row.value.clone()))
 | 
					 | 
				
			||||||
                        .collect::<Vec<(usize, String)>>()
 | 
					 | 
				
			||||||
                    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    let max_pages = max_todo_page(&values);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    let resp = show_todo_page(
 | 
					 | 
				
			||||||
                        &values,
 | 
					 | 
				
			||||||
                        pager.next_page(max_pages),
 | 
					 | 
				
			||||||
                        pager.user_id,
 | 
					 | 
				
			||||||
                        pager.channel_id,
 | 
					 | 
				
			||||||
                        pager.guild_id,
 | 
					 | 
				
			||||||
                    );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    let mut invoke = CommandInvoke::component(component);
 | 
					 | 
				
			||||||
                    let _ = invoke.respond(&ctx, resp).await;
 | 
					 | 
				
			||||||
                } else {
 | 
					 | 
				
			||||||
                    let _ = component
 | 
					 | 
				
			||||||
                        .create_interaction_response(&ctx, |r| {
 | 
					 | 
				
			||||||
                            r.kind(InteractionResponseType::ChannelMessageWithSource)
 | 
					 | 
				
			||||||
                                .interaction_response_data(|d| {
 | 
					 | 
				
			||||||
                                    d.flags(
 | 
					 | 
				
			||||||
                                        InteractionApplicationCommandCallbackDataFlags::EPHEMERAL,
 | 
					 | 
				
			||||||
                                    )
 | 
					 | 
				
			||||||
                                    .content("Only the user who performed the command can use these components")
 | 
					 | 
				
			||||||
                                })
 | 
					 | 
				
			||||||
                        })
 | 
					 | 
				
			||||||
                        .await;
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            ComponentDataModel::TodoSelector(selector) => {
 | 
					 | 
				
			||||||
                if Some(component.user.id.0) == selector.user_id || selector.user_id.is_none() {
 | 
					 | 
				
			||||||
                    let pool = ctx.data.read().await.get::<SQLPool>().cloned().unwrap();
 | 
					 | 
				
			||||||
                    let selected_id = component.data.values.join(",");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    sqlx::query!("DELETE FROM todos WHERE FIND_IN_SET(id, ?)", selected_id)
 | 
					 | 
				
			||||||
                        .execute(&pool)
 | 
					 | 
				
			||||||
                        .await
 | 
					 | 
				
			||||||
                        .unwrap();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    let values = sqlx::query!(
 | 
					 | 
				
			||||||
                    // fucking braindead mysql use <=> instead of = for null comparison
 | 
					 | 
				
			||||||
                    "SELECT id, value FROM todos WHERE user_id <=> ? AND channel_id <=> ? AND guild_id <=> ?",
 | 
					 | 
				
			||||||
                    selector.user_id,
 | 
					 | 
				
			||||||
                    selector.channel_id,
 | 
					 | 
				
			||||||
                    selector.guild_id,
 | 
					 | 
				
			||||||
                )
 | 
					 | 
				
			||||||
                .fetch_all(&pool)
 | 
					 | 
				
			||||||
                .await
 | 
					 | 
				
			||||||
                .unwrap()
 | 
					 | 
				
			||||||
                .iter()
 | 
					 | 
				
			||||||
                .map(|row| (row.id as usize, row.value.clone()))
 | 
					 | 
				
			||||||
                .collect::<Vec<(usize, String)>>();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    let resp = show_todo_page(
 | 
					 | 
				
			||||||
                        &values,
 | 
					 | 
				
			||||||
                        selector.page,
 | 
					 | 
				
			||||||
                        selector.user_id,
 | 
					 | 
				
			||||||
                        selector.channel_id,
 | 
					 | 
				
			||||||
                        selector.guild_id,
 | 
					 | 
				
			||||||
                    );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    let mut invoke = CommandInvoke::component(component);
 | 
					 | 
				
			||||||
                    let _ = invoke.respond(&ctx, resp).await;
 | 
					 | 
				
			||||||
                } else {
 | 
					 | 
				
			||||||
                    let _ = component
 | 
					 | 
				
			||||||
                        .create_interaction_response(&ctx, |r| {
 | 
					 | 
				
			||||||
                            r.kind(InteractionResponseType::ChannelMessageWithSource)
 | 
					 | 
				
			||||||
                                .interaction_response_data(|d| {
 | 
					 | 
				
			||||||
                                    d.flags(
 | 
					 | 
				
			||||||
                                        InteractionApplicationCommandCallbackDataFlags::EPHEMERAL,
 | 
					 | 
				
			||||||
                                    )
 | 
					 | 
				
			||||||
                                    .content("Only the user who performed the command can use these components")
 | 
					 | 
				
			||||||
                                })
 | 
					 | 
				
			||||||
                        })
 | 
					 | 
				
			||||||
                        .await;
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            ComponentDataModel::MacroPager(pager) => {
 | 
					 | 
				
			||||||
                let mut invoke = CommandInvoke::component(component);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                let macros = CommandMacro::from_guild(ctx, invoke.guild_id().unwrap()).await;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                let max_page = max_macro_page(¯os);
 | 
					 | 
				
			||||||
                let page = pager.next_page(max_page);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                let resp = show_macro_page(¯os, page);
 | 
					 | 
				
			||||||
                let _ = invoke.respond(&ctx, resp).await;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Serialize, Deserialize)]
 | 
					 | 
				
			||||||
pub struct DelSelector {
 | 
					 | 
				
			||||||
    pub page: usize,
 | 
					 | 
				
			||||||
    pub timezone: Tz,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Serialize, Deserialize)]
 | 
					 | 
				
			||||||
pub struct TodoSelector {
 | 
					 | 
				
			||||||
    pub page: usize,
 | 
					 | 
				
			||||||
    pub user_id: Option<u64>,
 | 
					 | 
				
			||||||
    pub channel_id: Option<u64>,
 | 
					 | 
				
			||||||
    pub guild_id: Option<u64>,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,413 +0,0 @@
 | 
				
			|||||||
// todo split pager out into a single struct
 | 
					 | 
				
			||||||
use chrono_tz::Tz;
 | 
					 | 
				
			||||||
use poise::serenity::{
 | 
					 | 
				
			||||||
    builder::CreateComponents, model::interactions::message_component::ButtonStyle,
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
use serde::{Deserialize, Serialize};
 | 
					 | 
				
			||||||
use serde_repr::*;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use crate::{component_models::ComponentDataModel, models::reminder::look_flags::LookFlags};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
pub trait Pager {
 | 
					 | 
				
			||||||
    fn next_page(&self, max_pages: usize) -> usize;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    fn create_button_row(&self, max_pages: usize, comp: &mut CreateComponents);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Serialize_repr, Deserialize_repr)]
 | 
					 | 
				
			||||||
#[repr(u8)]
 | 
					 | 
				
			||||||
enum PageAction {
 | 
					 | 
				
			||||||
    First = 0,
 | 
					 | 
				
			||||||
    Previous = 1,
 | 
					 | 
				
			||||||
    Refresh = 2,
 | 
					 | 
				
			||||||
    Next = 3,
 | 
					 | 
				
			||||||
    Last = 4,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Serialize, Deserialize)]
 | 
					 | 
				
			||||||
pub struct LookPager {
 | 
					 | 
				
			||||||
    pub flags: LookFlags,
 | 
					 | 
				
			||||||
    pub page: usize,
 | 
					 | 
				
			||||||
    action: PageAction,
 | 
					 | 
				
			||||||
    pub timezone: Tz,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl Pager for LookPager {
 | 
					 | 
				
			||||||
    fn next_page(&self, max_pages: usize) -> usize {
 | 
					 | 
				
			||||||
        match self.action {
 | 
					 | 
				
			||||||
            PageAction::First => 0,
 | 
					 | 
				
			||||||
            PageAction::Previous => 0.max(self.page - 1),
 | 
					 | 
				
			||||||
            PageAction::Refresh => self.page,
 | 
					 | 
				
			||||||
            PageAction::Next => (max_pages - 1).min(self.page + 1),
 | 
					 | 
				
			||||||
            PageAction::Last => max_pages - 1,
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    fn create_button_row(&self, max_pages: usize, comp: &mut CreateComponents) {
 | 
					 | 
				
			||||||
        let next_page = self.next_page(max_pages);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        let (page_first, page_prev, page_refresh, page_next, page_last) =
 | 
					 | 
				
			||||||
            LookPager::buttons(self.flags, next_page, self.timezone);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        comp.create_action_row(|row| {
 | 
					 | 
				
			||||||
            row.create_button(|b| {
 | 
					 | 
				
			||||||
                b.label("⏮️")
 | 
					 | 
				
			||||||
                    .style(ButtonStyle::Primary)
 | 
					 | 
				
			||||||
                    .custom_id(page_first.to_custom_id())
 | 
					 | 
				
			||||||
                    .disabled(next_page == 0)
 | 
					 | 
				
			||||||
            })
 | 
					 | 
				
			||||||
            .create_button(|b| {
 | 
					 | 
				
			||||||
                b.label("◀️")
 | 
					 | 
				
			||||||
                    .style(ButtonStyle::Secondary)
 | 
					 | 
				
			||||||
                    .custom_id(page_prev.to_custom_id())
 | 
					 | 
				
			||||||
                    .disabled(next_page == 0)
 | 
					 | 
				
			||||||
            })
 | 
					 | 
				
			||||||
            .create_button(|b| {
 | 
					 | 
				
			||||||
                b.label("🔁").style(ButtonStyle::Secondary).custom_id(page_refresh.to_custom_id())
 | 
					 | 
				
			||||||
            })
 | 
					 | 
				
			||||||
            .create_button(|b| {
 | 
					 | 
				
			||||||
                b.label("▶️")
 | 
					 | 
				
			||||||
                    .style(ButtonStyle::Secondary)
 | 
					 | 
				
			||||||
                    .custom_id(page_next.to_custom_id())
 | 
					 | 
				
			||||||
                    .disabled(next_page + 1 == max_pages)
 | 
					 | 
				
			||||||
            })
 | 
					 | 
				
			||||||
            .create_button(|b| {
 | 
					 | 
				
			||||||
                b.label("⏭️")
 | 
					 | 
				
			||||||
                    .style(ButtonStyle::Primary)
 | 
					 | 
				
			||||||
                    .custom_id(page_last.to_custom_id())
 | 
					 | 
				
			||||||
                    .disabled(next_page + 1 == max_pages)
 | 
					 | 
				
			||||||
            })
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl LookPager {
 | 
					 | 
				
			||||||
    pub fn new(flags: LookFlags, timezone: Tz) -> Self {
 | 
					 | 
				
			||||||
        Self { flags, page: 0, action: PageAction::First, timezone }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    pub fn buttons(
 | 
					 | 
				
			||||||
        flags: LookFlags,
 | 
					 | 
				
			||||||
        page: usize,
 | 
					 | 
				
			||||||
        timezone: Tz,
 | 
					 | 
				
			||||||
    ) -> (
 | 
					 | 
				
			||||||
        ComponentDataModel,
 | 
					 | 
				
			||||||
        ComponentDataModel,
 | 
					 | 
				
			||||||
        ComponentDataModel,
 | 
					 | 
				
			||||||
        ComponentDataModel,
 | 
					 | 
				
			||||||
        ComponentDataModel,
 | 
					 | 
				
			||||||
    ) {
 | 
					 | 
				
			||||||
        (
 | 
					 | 
				
			||||||
            ComponentDataModel::LookPager(LookPager {
 | 
					 | 
				
			||||||
                flags,
 | 
					 | 
				
			||||||
                page,
 | 
					 | 
				
			||||||
                action: PageAction::First,
 | 
					 | 
				
			||||||
                timezone,
 | 
					 | 
				
			||||||
            }),
 | 
					 | 
				
			||||||
            ComponentDataModel::LookPager(LookPager {
 | 
					 | 
				
			||||||
                flags,
 | 
					 | 
				
			||||||
                page,
 | 
					 | 
				
			||||||
                action: PageAction::Previous,
 | 
					 | 
				
			||||||
                timezone,
 | 
					 | 
				
			||||||
            }),
 | 
					 | 
				
			||||||
            ComponentDataModel::LookPager(LookPager {
 | 
					 | 
				
			||||||
                flags,
 | 
					 | 
				
			||||||
                page,
 | 
					 | 
				
			||||||
                action: PageAction::Refresh,
 | 
					 | 
				
			||||||
                timezone,
 | 
					 | 
				
			||||||
            }),
 | 
					 | 
				
			||||||
            ComponentDataModel::LookPager(LookPager {
 | 
					 | 
				
			||||||
                flags,
 | 
					 | 
				
			||||||
                page,
 | 
					 | 
				
			||||||
                action: PageAction::Next,
 | 
					 | 
				
			||||||
                timezone,
 | 
					 | 
				
			||||||
            }),
 | 
					 | 
				
			||||||
            ComponentDataModel::LookPager(LookPager {
 | 
					 | 
				
			||||||
                flags,
 | 
					 | 
				
			||||||
                page,
 | 
					 | 
				
			||||||
                action: PageAction::Last,
 | 
					 | 
				
			||||||
                timezone,
 | 
					 | 
				
			||||||
            }),
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Serialize, Deserialize)]
 | 
					 | 
				
			||||||
pub struct DelPager {
 | 
					 | 
				
			||||||
    pub page: usize,
 | 
					 | 
				
			||||||
    action: PageAction,
 | 
					 | 
				
			||||||
    pub timezone: Tz,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl Pager for DelPager {
 | 
					 | 
				
			||||||
    fn next_page(&self, max_pages: usize) -> usize {
 | 
					 | 
				
			||||||
        match self.action {
 | 
					 | 
				
			||||||
            PageAction::First => 0,
 | 
					 | 
				
			||||||
            PageAction::Previous => 0.max(self.page - 1),
 | 
					 | 
				
			||||||
            PageAction::Refresh => self.page,
 | 
					 | 
				
			||||||
            PageAction::Next => (max_pages - 1).min(self.page + 1),
 | 
					 | 
				
			||||||
            PageAction::Last => max_pages - 1,
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    fn create_button_row(&self, max_pages: usize, comp: &mut CreateComponents) {
 | 
					 | 
				
			||||||
        let next_page = self.next_page(max_pages);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        let (page_first, page_prev, page_refresh, page_next, page_last) =
 | 
					 | 
				
			||||||
            DelPager::buttons(next_page, self.timezone);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        comp.create_action_row(|row| {
 | 
					 | 
				
			||||||
            row.create_button(|b| {
 | 
					 | 
				
			||||||
                b.label("⏮️")
 | 
					 | 
				
			||||||
                    .style(ButtonStyle::Primary)
 | 
					 | 
				
			||||||
                    .custom_id(page_first.to_custom_id())
 | 
					 | 
				
			||||||
                    .disabled(next_page == 0)
 | 
					 | 
				
			||||||
            })
 | 
					 | 
				
			||||||
            .create_button(|b| {
 | 
					 | 
				
			||||||
                b.label("◀️")
 | 
					 | 
				
			||||||
                    .style(ButtonStyle::Secondary)
 | 
					 | 
				
			||||||
                    .custom_id(page_prev.to_custom_id())
 | 
					 | 
				
			||||||
                    .disabled(next_page == 0)
 | 
					 | 
				
			||||||
            })
 | 
					 | 
				
			||||||
            .create_button(|b| {
 | 
					 | 
				
			||||||
                b.label("🔁").style(ButtonStyle::Secondary).custom_id(page_refresh.to_custom_id())
 | 
					 | 
				
			||||||
            })
 | 
					 | 
				
			||||||
            .create_button(|b| {
 | 
					 | 
				
			||||||
                b.label("▶️")
 | 
					 | 
				
			||||||
                    .style(ButtonStyle::Secondary)
 | 
					 | 
				
			||||||
                    .custom_id(page_next.to_custom_id())
 | 
					 | 
				
			||||||
                    .disabled(next_page + 1 == max_pages)
 | 
					 | 
				
			||||||
            })
 | 
					 | 
				
			||||||
            .create_button(|b| {
 | 
					 | 
				
			||||||
                b.label("⏭️")
 | 
					 | 
				
			||||||
                    .style(ButtonStyle::Primary)
 | 
					 | 
				
			||||||
                    .custom_id(page_last.to_custom_id())
 | 
					 | 
				
			||||||
                    .disabled(next_page + 1 == max_pages)
 | 
					 | 
				
			||||||
            })
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl DelPager {
 | 
					 | 
				
			||||||
    pub fn new(page: usize, timezone: Tz) -> Self {
 | 
					 | 
				
			||||||
        Self { page, action: PageAction::Refresh, timezone }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    pub fn buttons(
 | 
					 | 
				
			||||||
        page: usize,
 | 
					 | 
				
			||||||
        timezone: Tz,
 | 
					 | 
				
			||||||
    ) -> (
 | 
					 | 
				
			||||||
        ComponentDataModel,
 | 
					 | 
				
			||||||
        ComponentDataModel,
 | 
					 | 
				
			||||||
        ComponentDataModel,
 | 
					 | 
				
			||||||
        ComponentDataModel,
 | 
					 | 
				
			||||||
        ComponentDataModel,
 | 
					 | 
				
			||||||
    ) {
 | 
					 | 
				
			||||||
        (
 | 
					 | 
				
			||||||
            ComponentDataModel::DelPager(DelPager { page, action: PageAction::First, timezone }),
 | 
					 | 
				
			||||||
            ComponentDataModel::DelPager(DelPager { page, action: PageAction::Previous, timezone }),
 | 
					 | 
				
			||||||
            ComponentDataModel::DelPager(DelPager { page, action: PageAction::Refresh, timezone }),
 | 
					 | 
				
			||||||
            ComponentDataModel::DelPager(DelPager { page, action: PageAction::Next, timezone }),
 | 
					 | 
				
			||||||
            ComponentDataModel::DelPager(DelPager { page, action: PageAction::Last, timezone }),
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Deserialize, Serialize)]
 | 
					 | 
				
			||||||
pub struct TodoPager {
 | 
					 | 
				
			||||||
    pub page: usize,
 | 
					 | 
				
			||||||
    action: PageAction,
 | 
					 | 
				
			||||||
    pub user_id: Option<u64>,
 | 
					 | 
				
			||||||
    pub channel_id: Option<u64>,
 | 
					 | 
				
			||||||
    pub guild_id: Option<u64>,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl Pager for TodoPager {
 | 
					 | 
				
			||||||
    fn next_page(&self, max_pages: usize) -> usize {
 | 
					 | 
				
			||||||
        match self.action {
 | 
					 | 
				
			||||||
            PageAction::First => 0,
 | 
					 | 
				
			||||||
            PageAction::Previous => 0.max(self.page - 1),
 | 
					 | 
				
			||||||
            PageAction::Refresh => self.page,
 | 
					 | 
				
			||||||
            PageAction::Next => (max_pages - 1).min(self.page + 1),
 | 
					 | 
				
			||||||
            PageAction::Last => max_pages - 1,
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    fn create_button_row(&self, max_pages: usize, comp: &mut CreateComponents) {
 | 
					 | 
				
			||||||
        let next_page = self.next_page(max_pages);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        let (page_first, page_prev, page_refresh, page_next, page_last) =
 | 
					 | 
				
			||||||
            TodoPager::buttons(next_page, self.user_id, self.channel_id, self.guild_id);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        comp.create_action_row(|row| {
 | 
					 | 
				
			||||||
            row.create_button(|b| {
 | 
					 | 
				
			||||||
                b.label("⏮️")
 | 
					 | 
				
			||||||
                    .style(ButtonStyle::Primary)
 | 
					 | 
				
			||||||
                    .custom_id(page_first.to_custom_id())
 | 
					 | 
				
			||||||
                    .disabled(next_page == 0)
 | 
					 | 
				
			||||||
            })
 | 
					 | 
				
			||||||
            .create_button(|b| {
 | 
					 | 
				
			||||||
                b.label("◀️")
 | 
					 | 
				
			||||||
                    .style(ButtonStyle::Secondary)
 | 
					 | 
				
			||||||
                    .custom_id(page_prev.to_custom_id())
 | 
					 | 
				
			||||||
                    .disabled(next_page == 0)
 | 
					 | 
				
			||||||
            })
 | 
					 | 
				
			||||||
            .create_button(|b| {
 | 
					 | 
				
			||||||
                b.label("🔁").style(ButtonStyle::Secondary).custom_id(page_refresh.to_custom_id())
 | 
					 | 
				
			||||||
            })
 | 
					 | 
				
			||||||
            .create_button(|b| {
 | 
					 | 
				
			||||||
                b.label("▶️")
 | 
					 | 
				
			||||||
                    .style(ButtonStyle::Secondary)
 | 
					 | 
				
			||||||
                    .custom_id(page_next.to_custom_id())
 | 
					 | 
				
			||||||
                    .disabled(next_page + 1 == max_pages)
 | 
					 | 
				
			||||||
            })
 | 
					 | 
				
			||||||
            .create_button(|b| {
 | 
					 | 
				
			||||||
                b.label("⏭️")
 | 
					 | 
				
			||||||
                    .style(ButtonStyle::Primary)
 | 
					 | 
				
			||||||
                    .custom_id(page_last.to_custom_id())
 | 
					 | 
				
			||||||
                    .disabled(next_page + 1 == max_pages)
 | 
					 | 
				
			||||||
            })
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl TodoPager {
 | 
					 | 
				
			||||||
    pub fn new(
 | 
					 | 
				
			||||||
        page: usize,
 | 
					 | 
				
			||||||
        user_id: Option<u64>,
 | 
					 | 
				
			||||||
        channel_id: Option<u64>,
 | 
					 | 
				
			||||||
        guild_id: Option<u64>,
 | 
					 | 
				
			||||||
    ) -> Self {
 | 
					 | 
				
			||||||
        Self { page, action: PageAction::Refresh, user_id, channel_id, guild_id }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    pub fn buttons(
 | 
					 | 
				
			||||||
        page: usize,
 | 
					 | 
				
			||||||
        user_id: Option<u64>,
 | 
					 | 
				
			||||||
        channel_id: Option<u64>,
 | 
					 | 
				
			||||||
        guild_id: Option<u64>,
 | 
					 | 
				
			||||||
    ) -> (
 | 
					 | 
				
			||||||
        ComponentDataModel,
 | 
					 | 
				
			||||||
        ComponentDataModel,
 | 
					 | 
				
			||||||
        ComponentDataModel,
 | 
					 | 
				
			||||||
        ComponentDataModel,
 | 
					 | 
				
			||||||
        ComponentDataModel,
 | 
					 | 
				
			||||||
    ) {
 | 
					 | 
				
			||||||
        (
 | 
					 | 
				
			||||||
            ComponentDataModel::TodoPager(TodoPager {
 | 
					 | 
				
			||||||
                page,
 | 
					 | 
				
			||||||
                action: PageAction::First,
 | 
					 | 
				
			||||||
                user_id,
 | 
					 | 
				
			||||||
                channel_id,
 | 
					 | 
				
			||||||
                guild_id,
 | 
					 | 
				
			||||||
            }),
 | 
					 | 
				
			||||||
            ComponentDataModel::TodoPager(TodoPager {
 | 
					 | 
				
			||||||
                page,
 | 
					 | 
				
			||||||
                action: PageAction::Previous,
 | 
					 | 
				
			||||||
                user_id,
 | 
					 | 
				
			||||||
                channel_id,
 | 
					 | 
				
			||||||
                guild_id,
 | 
					 | 
				
			||||||
            }),
 | 
					 | 
				
			||||||
            ComponentDataModel::TodoPager(TodoPager {
 | 
					 | 
				
			||||||
                page,
 | 
					 | 
				
			||||||
                action: PageAction::Refresh,
 | 
					 | 
				
			||||||
                user_id,
 | 
					 | 
				
			||||||
                channel_id,
 | 
					 | 
				
			||||||
                guild_id,
 | 
					 | 
				
			||||||
            }),
 | 
					 | 
				
			||||||
            ComponentDataModel::TodoPager(TodoPager {
 | 
					 | 
				
			||||||
                page,
 | 
					 | 
				
			||||||
                action: PageAction::Next,
 | 
					 | 
				
			||||||
                user_id,
 | 
					 | 
				
			||||||
                channel_id,
 | 
					 | 
				
			||||||
                guild_id,
 | 
					 | 
				
			||||||
            }),
 | 
					 | 
				
			||||||
            ComponentDataModel::TodoPager(TodoPager {
 | 
					 | 
				
			||||||
                page,
 | 
					 | 
				
			||||||
                action: PageAction::Last,
 | 
					 | 
				
			||||||
                user_id,
 | 
					 | 
				
			||||||
                channel_id,
 | 
					 | 
				
			||||||
                guild_id,
 | 
					 | 
				
			||||||
            }),
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Serialize, Deserialize)]
 | 
					 | 
				
			||||||
pub struct MacroPager {
 | 
					 | 
				
			||||||
    pub page: usize,
 | 
					 | 
				
			||||||
    action: PageAction,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl Pager for MacroPager {
 | 
					 | 
				
			||||||
    fn next_page(&self, max_pages: usize) -> usize {
 | 
					 | 
				
			||||||
        match self.action {
 | 
					 | 
				
			||||||
            PageAction::First => 0,
 | 
					 | 
				
			||||||
            PageAction::Previous => 0.max(self.page - 1),
 | 
					 | 
				
			||||||
            PageAction::Refresh => self.page,
 | 
					 | 
				
			||||||
            PageAction::Next => (max_pages - 1).min(self.page + 1),
 | 
					 | 
				
			||||||
            PageAction::Last => max_pages - 1,
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    fn create_button_row(&self, max_pages: usize, comp: &mut CreateComponents) {
 | 
					 | 
				
			||||||
        let next_page = self.next_page(max_pages);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        let (page_first, page_prev, page_refresh, page_next, page_last) =
 | 
					 | 
				
			||||||
            MacroPager::buttons(next_page);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        comp.create_action_row(|row| {
 | 
					 | 
				
			||||||
            row.create_button(|b| {
 | 
					 | 
				
			||||||
                b.label("⏮️")
 | 
					 | 
				
			||||||
                    .style(ButtonStyle::Primary)
 | 
					 | 
				
			||||||
                    .custom_id(page_first.to_custom_id())
 | 
					 | 
				
			||||||
                    .disabled(next_page == 0)
 | 
					 | 
				
			||||||
            })
 | 
					 | 
				
			||||||
            .create_button(|b| {
 | 
					 | 
				
			||||||
                b.label("◀️")
 | 
					 | 
				
			||||||
                    .style(ButtonStyle::Secondary)
 | 
					 | 
				
			||||||
                    .custom_id(page_prev.to_custom_id())
 | 
					 | 
				
			||||||
                    .disabled(next_page == 0)
 | 
					 | 
				
			||||||
            })
 | 
					 | 
				
			||||||
            .create_button(|b| {
 | 
					 | 
				
			||||||
                b.label("🔁").style(ButtonStyle::Secondary).custom_id(page_refresh.to_custom_id())
 | 
					 | 
				
			||||||
            })
 | 
					 | 
				
			||||||
            .create_button(|b| {
 | 
					 | 
				
			||||||
                b.label("▶️")
 | 
					 | 
				
			||||||
                    .style(ButtonStyle::Secondary)
 | 
					 | 
				
			||||||
                    .custom_id(page_next.to_custom_id())
 | 
					 | 
				
			||||||
                    .disabled(next_page + 1 == max_pages)
 | 
					 | 
				
			||||||
            })
 | 
					 | 
				
			||||||
            .create_button(|b| {
 | 
					 | 
				
			||||||
                b.label("⏭️")
 | 
					 | 
				
			||||||
                    .style(ButtonStyle::Primary)
 | 
					 | 
				
			||||||
                    .custom_id(page_last.to_custom_id())
 | 
					 | 
				
			||||||
                    .disabled(next_page + 1 == max_pages)
 | 
					 | 
				
			||||||
            })
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl MacroPager {
 | 
					 | 
				
			||||||
    pub fn new(page: usize) -> Self {
 | 
					 | 
				
			||||||
        Self { page, action: PageAction::Refresh }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    pub fn buttons(
 | 
					 | 
				
			||||||
        page: usize,
 | 
					 | 
				
			||||||
    ) -> (
 | 
					 | 
				
			||||||
        ComponentDataModel,
 | 
					 | 
				
			||||||
        ComponentDataModel,
 | 
					 | 
				
			||||||
        ComponentDataModel,
 | 
					 | 
				
			||||||
        ComponentDataModel,
 | 
					 | 
				
			||||||
        ComponentDataModel,
 | 
					 | 
				
			||||||
    ) {
 | 
					 | 
				
			||||||
        (
 | 
					 | 
				
			||||||
            ComponentDataModel::MacroPager(MacroPager { page, action: PageAction::First }),
 | 
					 | 
				
			||||||
            ComponentDataModel::MacroPager(MacroPager { page, action: PageAction::Previous }),
 | 
					 | 
				
			||||||
            ComponentDataModel::MacroPager(MacroPager { page, action: PageAction::Refresh }),
 | 
					 | 
				
			||||||
            ComponentDataModel::MacroPager(MacroPager { page, action: PageAction::Next }),
 | 
					 | 
				
			||||||
            ComponentDataModel::MacroPager(MacroPager { page, action: PageAction::Last }),
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,10 +1,6 @@
 | 
				
			|||||||
pub const DAY: u64 = 86_400;
 | 
					pub const DAY: u64 = 86_400;
 | 
				
			||||||
pub const HOUR: u64 = 3_600;
 | 
					pub const HOUR: u64 = 3_600;
 | 
				
			||||||
pub const MINUTE: u64 = 60;
 | 
					pub const MINUTE: u64 = 60;
 | 
				
			||||||
pub const EMBED_DESCRIPTION_MAX_LENGTH: usize = 4000;
 | 
					 | 
				
			||||||
pub const SELECT_MAX_ENTRIES: usize = 25;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
pub const MACRO_MAX_COMMANDS: usize = 5;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub const CHARACTERS: &str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_";
 | 
					pub const CHARACTERS: &str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -12,45 +8,83 @@ 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, RegexBuilder};
 | 
				
			||||||
use regex::Regex;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
lazy_static! {
 | 
					lazy_static! {
 | 
				
			||||||
    pub static ref DEFAULT_AVATAR: AttachmentType<'static> = (
 | 
					    pub static ref REGEX_CHANNEL: Regex = Regex::new(r#"^\s*<#(\d+)>\s*$"#).unwrap();
 | 
				
			||||||
        include_bytes!(concat!(
 | 
					
 | 
				
			||||||
            env!("CARGO_MANIFEST_DIR"),
 | 
					    pub static ref REGEX_ROLE: Regex = Regex::new(r#"<@&(\d+)>"#).unwrap();
 | 
				
			||||||
            "/assets/",
 | 
					
 | 
				
			||||||
            env!("WEBHOOK_AVATAR", "WEBHOOK_AVATAR not provided for compilation")
 | 
					    pub static ref REGEX_COMMANDS: Regex = Regex::new(r#"([a-z]+)"#).unwrap();
 | 
				
			||||||
        )) as &[u8],
 | 
					
 | 
				
			||||||
        env!("WEBHOOK_AVATAR"),
 | 
					    pub static ref REGEX_ALIAS: Regex =
 | 
				
			||||||
    )
 | 
					        Regex::new(r#"(?P<name>[\S]{1,12})(?:(?: (?P<cmd>.*)$)|$)"#).unwrap();
 | 
				
			||||||
        .into();
 | 
					
 | 
				
			||||||
 | 
					    pub static ref REGEX_CONTENT_SUBSTITUTION: Regex = Regex::new(r#"<<((?P<user>\d+)|(?P<role>.{1,100}))>>"#).unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    pub static ref REGEX_CHANNEL_USER: Regex = Regex::new(r#"\s*<(#|@)(?:!)?(\d+)>\s*"#).unwrap();
 | 
					    pub static ref REGEX_CHANNEL_USER: Regex = Regex::new(r#"\s*<(#|@)(?:!)?(\d+)>\s*"#).unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub static ref REGEX_REMIND_COMMAND: Regex = RegexBuilder::new(
 | 
				
			||||||
 | 
					    r#"(?P<mentions>(?:<@\d+>\s+|<@!\d+>\s+|<#\d+>\s+)*)(?P<time>(?:(?:\d+)(?:s|m|h|d|:|/|-|))+)(?:\s+(?P<interval>(?:(?:\d+)(?:s|m|h|d|))+))?(?:\s+(?P<expires>(?:(?:\d+)(?:s|m|h|d|:|/|-|))+))?\s+(?P<content>.*)"#
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					        .dot_matches_new_line(true)
 | 
				
			||||||
 | 
					        .build()
 | 
				
			||||||
 | 
					        .unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub static ref REGEX_NATURAL_COMMAND_1: Regex = RegexBuilder::new(
 | 
				
			||||||
 | 
					    r#"(?P<time>.*?)(?:\s+)(?:send|say)(?:\s+)(?P<msg>.*?)(?:(?:\s+)to(?:\s+)(?P<mentions>((?:<@\d+>)|(?:<@!\d+>)|(?:<#\d+>)|(?:\s+))+))?$"#
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					        .dot_matches_new_line(true)
 | 
				
			||||||
 | 
					        .build()
 | 
				
			||||||
 | 
					        .unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub static ref REGEX_NATURAL_COMMAND_2: Regex = RegexBuilder::new(
 | 
				
			||||||
 | 
					    r#"(?P<msg>.*)(?:\s+)every(?:\s+)(?P<interval>.*?)(?:(?:\s+)(?:until|for)(?:\s+)(?P<expires>.*?))?$"#
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					        .dot_matches_new_line(true)
 | 
				
			||||||
 | 
					        .build()
 | 
				
			||||||
 | 
					        .unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    pub static ref SUBSCRIPTION_ROLES: HashSet<u64> = HashSet::from_iter(
 | 
					    pub static ref SUBSCRIPTION_ROLES: HashSet<u64> = HashSet::from_iter(
 | 
				
			||||||
        env::var("SUBSCRIPTION_ROLES")
 | 
					        env::var("SUBSCRIPTION_ROLES")
 | 
				
			||||||
            .map(|var| var
 | 
					            .map(|var| var
 | 
				
			||||||
                .split(',')
 | 
					                .split(',')
 | 
				
			||||||
                .filter_map(|item| { item.parse::<u64>().ok() })
 | 
					                .filter_map(|item| { item.parse::<u64>().ok() })
 | 
				
			||||||
                .collect::<Vec<u64>>())
 | 
					                .collect::<Vec<u64>>())
 | 
				
			||||||
            .unwrap_or_else(|_| Vec::new())
 | 
					            .unwrap_or_else(|_| vec![])
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
    pub static ref CNC_GUILD: Option<u64> =
 | 
					
 | 
				
			||||||
        env::var("CNC_GUILD").map(|var| var.parse::<u64>().ok()).ok().flatten();
 | 
					    pub static ref CNC_GUILD: Option<u64> = env::var("CNC_GUILD")
 | 
				
			||||||
 | 
					        .map(|var| var.parse::<u64>().ok())
 | 
				
			||||||
 | 
					        .ok()
 | 
				
			||||||
 | 
					        .flatten();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    pub static ref MIN_INTERVAL: i64 = env::var("MIN_INTERVAL")
 | 
					    pub static ref MIN_INTERVAL: i64 = env::var("MIN_INTERVAL")
 | 
				
			||||||
        .ok()
 | 
					        .ok()
 | 
				
			||||||
        .map(|inner| inner.parse::<i64>().ok())
 | 
					        .map(|inner| inner.parse::<i64>().ok())
 | 
				
			||||||
        .flatten()
 | 
					        .flatten()
 | 
				
			||||||
        .unwrap_or(600);
 | 
					        .unwrap_or(600);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    pub static ref MAX_TIME: i64 = env::var("MAX_TIME")
 | 
					    pub static ref MAX_TIME: i64 = env::var("MAX_TIME")
 | 
				
			||||||
        .ok()
 | 
					        .ok()
 | 
				
			||||||
        .map(|inner| inner.parse::<i64>().ok())
 | 
					        .map(|inner| inner.parse::<i64>().ok())
 | 
				
			||||||
        .flatten()
 | 
					        .flatten()
 | 
				
			||||||
        .unwrap_or(60 * 60 * 24 * 365 * 50);
 | 
					        .unwrap_or(60 * 60 * 24 * 365 * 50);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    pub static ref LOCAL_TIMEZONE: String =
 | 
					    pub static ref LOCAL_TIMEZONE: String =
 | 
				
			||||||
        env::var("LOCAL_TIMEZONE").unwrap_or_else(|_| "UTC".to_string());
 | 
					        env::var("LOCAL_TIMEZONE").unwrap_or_else(|_| "UTC".to_string());
 | 
				
			||||||
    pub static ref THEME_COLOR: u32 = env::var("THEME_COLOR")
 | 
					
 | 
				
			||||||
        .map_or(THEME_COLOR_FALLBACK, |inner| u32::from_str_radix(&inner, 16)
 | 
					    pub static ref LOCAL_LANGUAGE: String =
 | 
				
			||||||
            .unwrap_or(THEME_COLOR_FALLBACK));
 | 
					        env::var("LOCAL_LANGUAGE").unwrap_or_else(|_| "EN".to_string());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub static ref DEFAULT_PREFIX: String =
 | 
				
			||||||
 | 
					        env::var("DEFAULT_PREFIX").unwrap_or_else(|_| "$".to_string());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub static ref THEME_COLOR: u32 = env::var("THEME_COLOR").map_or(
 | 
				
			||||||
 | 
					        THEME_COLOR_FALLBACK,
 | 
				
			||||||
 | 
					        |inner| u32::from_str_radix(&inner, 16).unwrap_or(THEME_COLOR_FALLBACK)
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    pub static ref PYTHON_LOCATION: String =
 | 
					    pub static ref PYTHON_LOCATION: String =
 | 
				
			||||||
        env::var("PYTHON_LOCATION").unwrap_or_else(|_| "venv/bin/python3".to_string());
 | 
					        env::var("PYTHON_LOCATION").unwrap_or_else(|_| "venv/bin/python3".to_string());
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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(())
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
							
								
								
									
										497
									
								
								src/framework.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										497
									
								
								src/framework.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,497 @@
 | 
				
			|||||||
 | 
					use serenity::{
 | 
				
			||||||
 | 
					    async_trait,
 | 
				
			||||||
 | 
					    client::Context,
 | 
				
			||||||
 | 
					    constants::MESSAGE_CODE_LIMIT,
 | 
				
			||||||
 | 
					    framework::Framework,
 | 
				
			||||||
 | 
					    futures::prelude::future::BoxFuture,
 | 
				
			||||||
 | 
					    http::Http,
 | 
				
			||||||
 | 
					    model::{
 | 
				
			||||||
 | 
					        channel::{Channel, GuildChannel, Message},
 | 
				
			||||||
 | 
					        guild::{Guild, Member},
 | 
				
			||||||
 | 
					        id::ChannelId,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    Result as SerenityResult,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use log::{error, info, warn};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use regex::{Match, Regex, RegexBuilder};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use std::{collections::HashMap, fmt};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::language_manager::LanguageManager;
 | 
				
			||||||
 | 
					use crate::models::{CtxGuildData, GuildData, UserData};
 | 
				
			||||||
 | 
					use crate::{models::ChannelData, LimitExecutors, SQLPool};
 | 
				
			||||||
 | 
					use serenity::model::id::MessageId;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type CommandFn = for<'fut> fn(&'fut Context, &'fut Message, String) -> BoxFuture<'fut, ()>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Debug, PartialEq)]
 | 
				
			||||||
 | 
					pub enum PermissionLevel {
 | 
				
			||||||
 | 
					    Unrestricted,
 | 
				
			||||||
 | 
					    Managed,
 | 
				
			||||||
 | 
					    Restricted,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub struct Command {
 | 
				
			||||||
 | 
					    pub name: &'static str,
 | 
				
			||||||
 | 
					    pub required_perms: PermissionLevel,
 | 
				
			||||||
 | 
					    pub supports_dm: bool,
 | 
				
			||||||
 | 
					    pub can_blacklist: bool,
 | 
				
			||||||
 | 
					    pub func: CommandFn,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl Command {
 | 
				
			||||||
 | 
					    async fn check_permissions(&self, ctx: &Context, guild: &Guild, member: &Member) -> bool {
 | 
				
			||||||
 | 
					        if self.required_perms == PermissionLevel::Unrestricted {
 | 
				
			||||||
 | 
					            true
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            let permissions = guild.member_permissions(&ctx, &member.user).await.unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if permissions.manage_guild()
 | 
				
			||||||
 | 
					                || (permissions.manage_messages()
 | 
				
			||||||
 | 
					                    && self.required_perms == PermissionLevel::Managed)
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                return true;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if self.required_perms == PermissionLevel::Managed {
 | 
				
			||||||
 | 
					                let pool = ctx
 | 
				
			||||||
 | 
					                    .data
 | 
				
			||||||
 | 
					                    .read()
 | 
				
			||||||
 | 
					                    .await
 | 
				
			||||||
 | 
					                    .get::<SQLPool>()
 | 
				
			||||||
 | 
					                    .cloned()
 | 
				
			||||||
 | 
					                    .expect("Could not get SQLPool from data");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                match sqlx::query!(
 | 
				
			||||||
 | 
					                    "
 | 
				
			||||||
 | 
					SELECT
 | 
				
			||||||
 | 
					    role
 | 
				
			||||||
 | 
					FROM
 | 
				
			||||||
 | 
					    roles
 | 
				
			||||||
 | 
					INNER JOIN
 | 
				
			||||||
 | 
					    command_restrictions ON roles.id = command_restrictions.role_id
 | 
				
			||||||
 | 
					WHERE
 | 
				
			||||||
 | 
					    command_restrictions.command = ? AND
 | 
				
			||||||
 | 
					    roles.guild_id = (
 | 
				
			||||||
 | 
					        SELECT
 | 
				
			||||||
 | 
					            id
 | 
				
			||||||
 | 
					        FROM
 | 
				
			||||||
 | 
					            guilds
 | 
				
			||||||
 | 
					        WHERE
 | 
				
			||||||
 | 
					            guild = ?)
 | 
				
			||||||
 | 
					                    ",
 | 
				
			||||||
 | 
					                    self.name,
 | 
				
			||||||
 | 
					                    guild.id.as_u64()
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					                .fetch_all(&pool)
 | 
				
			||||||
 | 
					                .await
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    Ok(rows) => {
 | 
				
			||||||
 | 
					                        let role_ids = member
 | 
				
			||||||
 | 
					                            .roles
 | 
				
			||||||
 | 
					                            .iter()
 | 
				
			||||||
 | 
					                            .map(|r| *r.as_u64())
 | 
				
			||||||
 | 
					                            .collect::<Vec<u64>>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        for row in rows {
 | 
				
			||||||
 | 
					                            if role_ids.contains(&row.role) {
 | 
				
			||||||
 | 
					                                return true;
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        false
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    Err(sqlx::Error::RowNotFound) => false,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    Err(e) => {
 | 
				
			||||||
 | 
					                        warn!(
 | 
				
			||||||
 | 
					                            "Unexpected error occurred querying command_restrictions: {:?}",
 | 
				
			||||||
 | 
					                            e
 | 
				
			||||||
 | 
					                        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        false
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                false
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl fmt::Debug for Command {
 | 
				
			||||||
 | 
					    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
 | 
				
			||||||
 | 
					        f.debug_struct("Command")
 | 
				
			||||||
 | 
					            .field("name", &self.name)
 | 
				
			||||||
 | 
					            .field("required_perms", &self.required_perms)
 | 
				
			||||||
 | 
					            .field("supports_dm", &self.supports_dm)
 | 
				
			||||||
 | 
					            .field("can_blacklist", &self.can_blacklist)
 | 
				
			||||||
 | 
					            .finish()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[async_trait]
 | 
				
			||||||
 | 
					pub trait SendIterator {
 | 
				
			||||||
 | 
					    async fn say_lines(
 | 
				
			||||||
 | 
					        self,
 | 
				
			||||||
 | 
					        http: impl AsRef<Http> + Send + Sync + 'async_trait,
 | 
				
			||||||
 | 
					        content: impl Iterator<Item = String> + Send + 'async_trait,
 | 
				
			||||||
 | 
					    ) -> SerenityResult<()>;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[async_trait]
 | 
				
			||||||
 | 
					impl SendIterator for ChannelId {
 | 
				
			||||||
 | 
					    async fn say_lines(
 | 
				
			||||||
 | 
					        self,
 | 
				
			||||||
 | 
					        http: impl AsRef<Http> + Send + Sync + 'async_trait,
 | 
				
			||||||
 | 
					        content: impl Iterator<Item = String> + Send + 'async_trait,
 | 
				
			||||||
 | 
					    ) -> SerenityResult<()> {
 | 
				
			||||||
 | 
					        let mut current_content = String::new();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        for line in content {
 | 
				
			||||||
 | 
					            if current_content.len() + line.len() > MESSAGE_CODE_LIMIT as usize {
 | 
				
			||||||
 | 
					                self.send_message(&http, |m| {
 | 
				
			||||||
 | 
					                    m.allowed_mentions(|am| am.empty_parse())
 | 
				
			||||||
 | 
					                        .content(¤t_content)
 | 
				
			||||||
 | 
					                })
 | 
				
			||||||
 | 
					                .await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                current_content = line;
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                current_content = format!("{}\n{}", current_content, line);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if !current_content.is_empty() {
 | 
				
			||||||
 | 
					            self.send_message(&http, |m| {
 | 
				
			||||||
 | 
					                m.allowed_mentions(|am| am.empty_parse())
 | 
				
			||||||
 | 
					                    .content(¤t_content)
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					            .await?;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Ok(())
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub struct RegexFramework {
 | 
				
			||||||
 | 
					    pub commands: HashMap<String, &'static Command>,
 | 
				
			||||||
 | 
					    command_matcher: Regex,
 | 
				
			||||||
 | 
					    dm_regex_matcher: Regex,
 | 
				
			||||||
 | 
					    default_prefix: String,
 | 
				
			||||||
 | 
					    client_id: u64,
 | 
				
			||||||
 | 
					    ignore_bots: bool,
 | 
				
			||||||
 | 
					    case_insensitive: bool,
 | 
				
			||||||
 | 
					    dm_enabled: bool,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl RegexFramework {
 | 
				
			||||||
 | 
					    pub fn new<T: Into<u64>>(client_id: T) -> Self {
 | 
				
			||||||
 | 
					        Self {
 | 
				
			||||||
 | 
					            commands: HashMap::new(),
 | 
				
			||||||
 | 
					            command_matcher: Regex::new(r#"^$"#).unwrap(),
 | 
				
			||||||
 | 
					            dm_regex_matcher: Regex::new(r#"^$"#).unwrap(),
 | 
				
			||||||
 | 
					            default_prefix: "".to_string(),
 | 
				
			||||||
 | 
					            client_id: client_id.into(),
 | 
				
			||||||
 | 
					            ignore_bots: true,
 | 
				
			||||||
 | 
					            case_insensitive: true,
 | 
				
			||||||
 | 
					            dm_enabled: true,
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn case_insensitive(mut self, case_insensitive: bool) -> Self {
 | 
				
			||||||
 | 
					        self.case_insensitive = case_insensitive;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn default_prefix<T: ToString>(mut self, new_prefix: T) -> Self {
 | 
				
			||||||
 | 
					        self.default_prefix = new_prefix.to_string();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    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_command<S: ToString>(mut self, name: S, command: &'static Command) -> Self {
 | 
				
			||||||
 | 
					        self.commands.insert(name.to_string(), command);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn build(mut self) -> Self {
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            let command_names;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                let mut command_names_vec =
 | 
				
			||||||
 | 
					                    self.commands.keys().map(|k| &k[..]).collect::<Vec<&str>>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                command_names_vec.sort_unstable_by(|a, b| b.len().cmp(&a.len()));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                command_names = command_names_vec.join("|");
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            info!("Command names: {}", command_names);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                let match_string = r#"^(?:(?:<@ID>\s*)|(?:<@!ID>\s*)|(?P<prefix>\S{1,5}?))(?P<cmd>COMMANDS)(?:$|\s+(?P<args>.*))$"#
 | 
				
			||||||
 | 
					                    .replace("COMMANDS", command_names.as_str())
 | 
				
			||||||
 | 
					                    .replace("ID", self.client_id.to_string().as_str());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                self.command_matcher = RegexBuilder::new(match_string.as_str())
 | 
				
			||||||
 | 
					                    .case_insensitive(self.case_insensitive)
 | 
				
			||||||
 | 
					                    .dot_matches_new_line(true)
 | 
				
			||||||
 | 
					                    .build()
 | 
				
			||||||
 | 
					                    .unwrap();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            let dm_command_names;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                let mut command_names_vec = self
 | 
				
			||||||
 | 
					                    .commands
 | 
				
			||||||
 | 
					                    .iter()
 | 
				
			||||||
 | 
					                    .filter_map(|(key, command)| {
 | 
				
			||||||
 | 
					                        if command.supports_dm {
 | 
				
			||||||
 | 
					                            Some(&key[..])
 | 
				
			||||||
 | 
					                        } else {
 | 
				
			||||||
 | 
					                            None
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    })
 | 
				
			||||||
 | 
					                    .collect::<Vec<&str>>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                command_names_vec.sort_unstable_by(|a, b| b.len().cmp(&a.len()));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                dm_command_names = command_names_vec.join("|");
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                let match_string = r#"^(?:(?:<@ID>\s+)|(?:<@!ID>\s+)|(\$)|())(?P<cmd>COMMANDS)(?:$|\s+(?P<args>.*))$"#
 | 
				
			||||||
 | 
					                    .replace("COMMANDS", dm_command_names.as_str())
 | 
				
			||||||
 | 
					                    .replace("ID", self.client_id.to_string().as_str());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                self.dm_regex_matcher = RegexBuilder::new(match_string.as_str())
 | 
				
			||||||
 | 
					                    .case_insensitive(self.case_insensitive)
 | 
				
			||||||
 | 
					                    .dot_matches_new_line(true)
 | 
				
			||||||
 | 
					                    .build()
 | 
				
			||||||
 | 
					                    .unwrap();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					enum PermissionCheck {
 | 
				
			||||||
 | 
					    None,              // No permissions
 | 
				
			||||||
 | 
					    Basic(bool, bool), // Send + Embed permissions (sufficient to reply)
 | 
				
			||||||
 | 
					    All,               // Above + Manage Webhooks (sufficient to operate)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[async_trait]
 | 
				
			||||||
 | 
					impl Framework for RegexFramework {
 | 
				
			||||||
 | 
					    async fn dispatch(&self, ctx: Context, msg: Message) {
 | 
				
			||||||
 | 
					        async fn check_self_permissions(
 | 
				
			||||||
 | 
					            ctx: &Context,
 | 
				
			||||||
 | 
					            guild: &Guild,
 | 
				
			||||||
 | 
					            channel: &GuildChannel,
 | 
				
			||||||
 | 
					        ) -> SerenityResult<PermissionCheck> {
 | 
				
			||||||
 | 
					            let user_id = ctx.cache.current_user_id().await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            let guild_perms = guild.member_permissions(&ctx, user_id).await?;
 | 
				
			||||||
 | 
					            let channel_perms = channel.permissions_for_user(ctx, user_id).await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            let basic_perms = channel_perms.send_messages();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            Ok(
 | 
				
			||||||
 | 
					                if basic_perms && guild_perms.manage_webhooks() && channel_perms.embed_links() {
 | 
				
			||||||
 | 
					                    PermissionCheck::All
 | 
				
			||||||
 | 
					                } else if basic_perms {
 | 
				
			||||||
 | 
					                    PermissionCheck::Basic(
 | 
				
			||||||
 | 
					                        guild_perms.manage_webhooks(),
 | 
				
			||||||
 | 
					                        channel_perms.embed_links(),
 | 
				
			||||||
 | 
					                    )
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    PermissionCheck::None
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        async fn check_prefix(ctx: &Context, guild: &Guild, prefix_opt: Option<Match<'_>>) -> bool {
 | 
				
			||||||
 | 
					            if let Some(prefix) = prefix_opt {
 | 
				
			||||||
 | 
					                let guild_prefix = ctx.prefix(Some(guild.id)).await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                guild_prefix.as_str() == prefix.as_str()
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                true
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // gate to prevent analysing messages unnecessarily
 | 
				
			||||||
 | 
					        if (msg.author.bot && self.ignore_bots) || msg.content.is_empty() {
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            // Guild Command
 | 
				
			||||||
 | 
					            if let (Some(guild), Some(Channel::Guild(channel))) =
 | 
				
			||||||
 | 
					                (msg.guild(&ctx).await, msg.channel(&ctx).await)
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                let data = ctx.data.read().await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                let pool = data
 | 
				
			||||||
 | 
					                    .get::<SQLPool>()
 | 
				
			||||||
 | 
					                    .cloned()
 | 
				
			||||||
 | 
					                    .expect("Could not get SQLPool from data");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if let Some(full_match) = self.command_matcher.captures(&msg.content) {
 | 
				
			||||||
 | 
					                    if check_prefix(&ctx, &guild, full_match.name("prefix")).await {
 | 
				
			||||||
 | 
					                        let lm = data.get::<LanguageManager>().unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        let language = UserData::language_of(&msg.author, &pool);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        match check_self_permissions(&ctx, &guild, &channel).await {
 | 
				
			||||||
 | 
					                            Ok(perms) => match perms {
 | 
				
			||||||
 | 
					                                PermissionCheck::All => {
 | 
				
			||||||
 | 
					                                    let command = self
 | 
				
			||||||
 | 
					                                        .commands
 | 
				
			||||||
 | 
					                                        .get(
 | 
				
			||||||
 | 
					                                            &full_match
 | 
				
			||||||
 | 
					                                                .name("cmd")
 | 
				
			||||||
 | 
					                                                .unwrap()
 | 
				
			||||||
 | 
					                                                .as_str()
 | 
				
			||||||
 | 
					                                                .to_lowercase(),
 | 
				
			||||||
 | 
					                                        )
 | 
				
			||||||
 | 
					                                        .unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                                    let channel_data = ChannelData::from_channel(
 | 
				
			||||||
 | 
					                                        msg.channel(&ctx).await.unwrap(),
 | 
				
			||||||
 | 
					                                        &pool,
 | 
				
			||||||
 | 
					                                    )
 | 
				
			||||||
 | 
					                                    .await
 | 
				
			||||||
 | 
					                                    .unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                                    if !command.can_blacklist || !channel_data.blacklisted {
 | 
				
			||||||
 | 
					                                        let args = full_match
 | 
				
			||||||
 | 
					                                            .name("args")
 | 
				
			||||||
 | 
					                                            .map(|m| m.as_str())
 | 
				
			||||||
 | 
					                                            .unwrap_or("")
 | 
				
			||||||
 | 
					                                            .to_string();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                                        let member = guild.member(&ctx, &msg.author).await.unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                                        if command.check_permissions(&ctx, &guild, &member).await {
 | 
				
			||||||
 | 
					                                            dbg!(command.name);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                                            {
 | 
				
			||||||
 | 
					                                                let guild_id = guild.id.as_u64().to_owned();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                                                GuildData::from_guild(guild, &pool).await.expect(
 | 
				
			||||||
 | 
					                                                    &format!(
 | 
				
			||||||
 | 
					                                                        "Failed to create new guild object for {}",
 | 
				
			||||||
 | 
					                                                        guild_id
 | 
				
			||||||
 | 
					                                                    ),
 | 
				
			||||||
 | 
					                                                );
 | 
				
			||||||
 | 
					                                            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                                            if msg.id == MessageId(0)
 | 
				
			||||||
 | 
					                                                || !ctx.check_executing(msg.author.id).await
 | 
				
			||||||
 | 
					                                            {
 | 
				
			||||||
 | 
					                                                ctx.set_executing(msg.author.id).await;
 | 
				
			||||||
 | 
					                                                (command.func)(&ctx, &msg, args).await;
 | 
				
			||||||
 | 
					                                                ctx.drop_executing(msg.author.id).await;
 | 
				
			||||||
 | 
					                                            }
 | 
				
			||||||
 | 
					                                        } else if command.required_perms
 | 
				
			||||||
 | 
					                                            == PermissionLevel::Restricted
 | 
				
			||||||
 | 
					                                        {
 | 
				
			||||||
 | 
					                                            let _ = msg
 | 
				
			||||||
 | 
					                                                .channel_id
 | 
				
			||||||
 | 
					                                                .say(
 | 
				
			||||||
 | 
					                                                    &ctx,
 | 
				
			||||||
 | 
					                                                    lm.get(&language.await, "no_perms_restricted"),
 | 
				
			||||||
 | 
					                                                )
 | 
				
			||||||
 | 
					                                                .await;
 | 
				
			||||||
 | 
					                                        } else if command.required_perms == PermissionLevel::Managed
 | 
				
			||||||
 | 
					                                        {
 | 
				
			||||||
 | 
					                                            let _ = msg
 | 
				
			||||||
 | 
					                                                .channel_id
 | 
				
			||||||
 | 
					                                                .say(
 | 
				
			||||||
 | 
					                                                    &ctx,
 | 
				
			||||||
 | 
					                                                    lm.get(&language.await, "no_perms_managed")
 | 
				
			||||||
 | 
					                                                        .replace(
 | 
				
			||||||
 | 
					                                                            "{prefix}",
 | 
				
			||||||
 | 
					                                                            &ctx.prefix(msg.guild_id).await,
 | 
				
			||||||
 | 
					                                                        ),
 | 
				
			||||||
 | 
					                                                )
 | 
				
			||||||
 | 
					                                                .await;
 | 
				
			||||||
 | 
					                                        }
 | 
				
			||||||
 | 
					                                    }
 | 
				
			||||||
 | 
					                                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                                PermissionCheck::Basic(manage_webhooks, embed_links) => {
 | 
				
			||||||
 | 
					                                    let response = lm
 | 
				
			||||||
 | 
					                                        .get(&language.await, "no_perms_general")
 | 
				
			||||||
 | 
					                                        .replace(
 | 
				
			||||||
 | 
					                                            "{manage_webhooks}",
 | 
				
			||||||
 | 
					                                            if manage_webhooks { "✅" } else { "❌" },
 | 
				
			||||||
 | 
					                                        )
 | 
				
			||||||
 | 
					                                        .replace(
 | 
				
			||||||
 | 
					                                            "{embed_links}",
 | 
				
			||||||
 | 
					                                            if embed_links { "✅" } else { "❌" },
 | 
				
			||||||
 | 
					                                        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                                    let _ = msg.channel_id.say(&ctx, response).await;
 | 
				
			||||||
 | 
					                                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                                PermissionCheck::None => {
 | 
				
			||||||
 | 
					                                    warn!("Missing enough permissions for guild {}", guild.id);
 | 
				
			||||||
 | 
					                                }
 | 
				
			||||||
 | 
					                            },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                            Err(e) => {
 | 
				
			||||||
 | 
					                                error!(
 | 
				
			||||||
 | 
					                                    "Error occurred getting permissions in guild {}: {:?}",
 | 
				
			||||||
 | 
					                                    guild.id, e
 | 
				
			||||||
 | 
					                                );
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            // DM Command
 | 
				
			||||||
 | 
					            else if self.dm_enabled {
 | 
				
			||||||
 | 
					                if let Some(full_match) = self.dm_regex_matcher.captures(&msg.content[..]) {
 | 
				
			||||||
 | 
					                    let command = self
 | 
				
			||||||
 | 
					                        .commands
 | 
				
			||||||
 | 
					                        .get(&full_match.name("cmd").unwrap().as_str().to_lowercase())
 | 
				
			||||||
 | 
					                        .unwrap();
 | 
				
			||||||
 | 
					                    let args = full_match
 | 
				
			||||||
 | 
					                        .name("args")
 | 
				
			||||||
 | 
					                        .map(|m| m.as_str())
 | 
				
			||||||
 | 
					                        .unwrap_or("")
 | 
				
			||||||
 | 
					                        .to_string();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    dbg!(command.name);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    if msg.id == MessageId(0) || !ctx.check_executing(msg.author.id).await {
 | 
				
			||||||
 | 
					                        ctx.set_executing(msg.author.id).await;
 | 
				
			||||||
 | 
					                        (command.func)(&ctx, &msg, args).await;
 | 
				
			||||||
 | 
					                        ctx.drop_executing(msg.author.id).await;
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										113
									
								
								src/hooks.rs
									
									
									
									
									
								
							
							
						
						
									
										113
									
								
								src/hooks.rs
									
									
									
									
									
								
							@@ -1,113 +0,0 @@
 | 
				
			|||||||
use poise::{serenity::model::channel::Channel, ApplicationCommandOrAutocompleteInteraction};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use crate::{consts::MACRO_MAX_COMMANDS, models::command_macro::CommandOptions, Context, Error};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
pub async fn guild_only(ctx: Context<'_>) -> Result<bool, Error> {
 | 
					 | 
				
			||||||
    if ctx.guild_id().is_some() {
 | 
					 | 
				
			||||||
        Ok(true)
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
        let _ = ctx.say("This command can only be used in servers").await;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        Ok(false)
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
async fn macro_check(ctx: Context<'_>) -> bool {
 | 
					 | 
				
			||||||
    if let Context::Application(app_ctx) = ctx {
 | 
					 | 
				
			||||||
        if let ApplicationCommandOrAutocompleteInteraction::ApplicationCommand(interaction) =
 | 
					 | 
				
			||||||
            app_ctx.interaction
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            if let Some(guild_id) = ctx.guild_id() {
 | 
					 | 
				
			||||||
                if ctx.command().identifying_name != "macro_finish" {
 | 
					 | 
				
			||||||
                    let mut lock = ctx.data().recording_macros.write().await;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    if let Some(command_macro) = lock.get_mut(&(guild_id, ctx.author().id)) {
 | 
					 | 
				
			||||||
                        if command_macro.commands.len() >= MACRO_MAX_COMMANDS {
 | 
					 | 
				
			||||||
                            let _ = ctx.send(|m| {
 | 
					 | 
				
			||||||
                                m.ephemeral(true).content(
 | 
					 | 
				
			||||||
                                    "5 commands already recorded. Please use `/macro finish` to end recording.",
 | 
					 | 
				
			||||||
                                )
 | 
					 | 
				
			||||||
                            })
 | 
					 | 
				
			||||||
                            .await;
 | 
					 | 
				
			||||||
                        } else {
 | 
					 | 
				
			||||||
                            let mut command_options = CommandOptions::new(&ctx.command().name);
 | 
					 | 
				
			||||||
                            command_options.populate(&interaction);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                            command_macro.commands.push(command_options);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                            let _ = ctx
 | 
					 | 
				
			||||||
                                .send(|m| m.ephemeral(true).content("Command recorded to macro"))
 | 
					 | 
				
			||||||
                                .await;
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                        false
 | 
					 | 
				
			||||||
                    } else {
 | 
					 | 
				
			||||||
                        true
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                } else {
 | 
					 | 
				
			||||||
                    true
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            } else {
 | 
					 | 
				
			||||||
                true
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
            true
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
        true
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
async fn check_self_permissions(ctx: Context<'_>) -> bool {
 | 
					 | 
				
			||||||
    if let Some(guild) = ctx.guild() {
 | 
					 | 
				
			||||||
        let user_id = ctx.discord().cache.current_user_id();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        let manage_webhooks = guild
 | 
					 | 
				
			||||||
            .member_permissions(&ctx.discord(), user_id)
 | 
					 | 
				
			||||||
            .await
 | 
					 | 
				
			||||||
            .map_or(false, |p| p.manage_webhooks());
 | 
					 | 
				
			||||||
        let (view_channel, send_messages, embed_links) = ctx
 | 
					 | 
				
			||||||
            .channel_id()
 | 
					 | 
				
			||||||
            .to_channel_cached(&ctx.discord())
 | 
					 | 
				
			||||||
            .map(|c| {
 | 
					 | 
				
			||||||
                if let Channel::Guild(channel) = c {
 | 
					 | 
				
			||||||
                    channel.permissions_for_user(&ctx.discord(), user_id).ok()
 | 
					 | 
				
			||||||
                } else {
 | 
					 | 
				
			||||||
                    None
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            })
 | 
					 | 
				
			||||||
            .flatten()
 | 
					 | 
				
			||||||
            .map_or((false, false, false), |p| {
 | 
					 | 
				
			||||||
                (p.read_messages(), p.send_messages(), p.embed_links())
 | 
					 | 
				
			||||||
            });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if manage_webhooks && send_messages && embed_links {
 | 
					 | 
				
			||||||
            true
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
            let _ = ctx
 | 
					 | 
				
			||||||
                .send(|m| {
 | 
					 | 
				
			||||||
                    m.content(format!(
 | 
					 | 
				
			||||||
                        "Please ensure the bot has the correct permissions:
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
{}     **View Channel**
 | 
					 | 
				
			||||||
{}     **Send Message**
 | 
					 | 
				
			||||||
{}     **Embed Links**
 | 
					 | 
				
			||||||
{}     **Manage Webhooks**",
 | 
					 | 
				
			||||||
                        if view_channel { "✅" } else { "❌" },
 | 
					 | 
				
			||||||
                        if send_messages { "✅" } else { "❌" },
 | 
					 | 
				
			||||||
                        if manage_webhooks { "✅" } else { "❌" },
 | 
					 | 
				
			||||||
                        if embed_links { "✅" } else { "❌" },
 | 
					 | 
				
			||||||
                    ))
 | 
					 | 
				
			||||||
                })
 | 
					 | 
				
			||||||
                .await;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            false
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
        true
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
pub async fn all_checks(ctx: Context<'_>) -> Result<bool, Error> {
 | 
					 | 
				
			||||||
    Ok(macro_check(ctx).await && check_self_permissions(ctx).await)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
							
								
								
									
										65
									
								
								src/language_manager.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								src/language_manager.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,65 @@
 | 
				
			|||||||
 | 
					use serde::Deserialize;
 | 
				
			||||||
 | 
					use serde_json::from_str;
 | 
				
			||||||
 | 
					use serenity::prelude::TypeMapKey;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use std::{collections::HashMap, error::Error, sync::Arc};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::consts::LOCAL_LANGUAGE;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Deserialize)]
 | 
				
			||||||
 | 
					pub struct LanguageManager {
 | 
				
			||||||
 | 
					    languages: HashMap<String, String>,
 | 
				
			||||||
 | 
					    strings: HashMap<String, HashMap<String, String>>,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl LanguageManager {
 | 
				
			||||||
 | 
					    pub fn from_compiled(content: &'static str) -> Result<Self, Box<dyn Error + Send + Sync>> {
 | 
				
			||||||
 | 
					        let new: Self = from_str(content.as_ref())?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Ok(new)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn get(&self, language: &str, name: &str) -> &str {
 | 
				
			||||||
 | 
					        self.strings
 | 
				
			||||||
 | 
					            .get(language)
 | 
				
			||||||
 | 
					            .map(|sm| sm.get(name))
 | 
				
			||||||
 | 
					            .expect(&format!(r#"Language does not exist: "{}""#, language))
 | 
				
			||||||
 | 
					            .unwrap_or_else(|| {
 | 
				
			||||||
 | 
					                self.strings
 | 
				
			||||||
 | 
					                    .get(&*LOCAL_LANGUAGE)
 | 
				
			||||||
 | 
					                    .map(|sm| {
 | 
				
			||||||
 | 
					                        sm.get(name)
 | 
				
			||||||
 | 
					                            .expect(&format!(r#"String does not exist: "{}""#, name))
 | 
				
			||||||
 | 
					                    })
 | 
				
			||||||
 | 
					                    .expect("LOCAL_LANGUAGE is not available")
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn get_language(&self, language: &str) -> Option<&str> {
 | 
				
			||||||
 | 
					        let language_normal = language.to_lowercase();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.languages
 | 
				
			||||||
 | 
					            .iter()
 | 
				
			||||||
 | 
					            .filter(|(k, v)| {
 | 
				
			||||||
 | 
					                k.to_lowercase() == language_normal || v.to_lowercase() == language_normal
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					            .map(|(k, _)| k.as_str())
 | 
				
			||||||
 | 
					            .next()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn get_language_by_flag(&self, flag: &str) -> Option<&str> {
 | 
				
			||||||
 | 
					        self.languages
 | 
				
			||||||
 | 
					            .iter()
 | 
				
			||||||
 | 
					            .filter(|(k, _)| self.get(k, "flag") == flag)
 | 
				
			||||||
 | 
					            .map(|(k, _)| k.as_str())
 | 
				
			||||||
 | 
					            .next()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn all_languages(&self) -> impl Iterator<Item = (&str, &str)> {
 | 
				
			||||||
 | 
					        self.languages.iter().map(|(k, v)| (k.as_str(), v.as_str()))
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl TypeMapKey for LanguageManager {
 | 
				
			||||||
 | 
					    type Value = Arc<Self>;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										636
									
								
								src/main.rs
									
									
									
									
									
								
							
							
						
						
									
										636
									
								
								src/main.rs
									
									
									
									
									
								
							@@ -1,47 +1,349 @@
 | 
				
			|||||||
#![feature(int_roundings)]
 | 
					 | 
				
			||||||
#[macro_use]
 | 
					#[macro_use]
 | 
				
			||||||
extern crate lazy_static;
 | 
					extern crate lazy_static;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
mod commands;
 | 
					mod commands;
 | 
				
			||||||
// mod component_models;
 | 
					 | 
				
			||||||
mod consts;
 | 
					mod consts;
 | 
				
			||||||
mod event_handlers;
 | 
					mod framework;
 | 
				
			||||||
mod hooks;
 | 
					mod language_manager;
 | 
				
			||||||
mod models;
 | 
					mod models;
 | 
				
			||||||
mod time_parser;
 | 
					mod time_parser;
 | 
				
			||||||
mod utils;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
use std::{collections::HashMap, env};
 | 
					use serenity::{
 | 
				
			||||||
 | 
					    async_trait,
 | 
				
			||||||
use chrono_tz::Tz;
 | 
					    cache::Cache,
 | 
				
			||||||
use dotenv::dotenv;
 | 
					    client::{bridge::gateway::GatewayIntents, Client},
 | 
				
			||||||
use poise::serenity::model::{
 | 
					    futures::TryFutureExt,
 | 
				
			||||||
    gateway::{Activity, GatewayIntents},
 | 
					    http::{client::Http, CacheHttp},
 | 
				
			||||||
 | 
					    model::{
 | 
				
			||||||
 | 
					        channel::GuildChannel,
 | 
				
			||||||
 | 
					        channel::Message,
 | 
				
			||||||
 | 
					        guild::{Guild, GuildUnavailable},
 | 
				
			||||||
        id::{GuildId, UserId},
 | 
					        id::{GuildId, UserId},
 | 
				
			||||||
 | 
					        interactions::{Interaction, InteractionData, InteractionType},
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    prelude::{Context, EventHandler, TypeMapKey},
 | 
				
			||||||
 | 
					    utils::shard_id,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
use sqlx::{MySql, Pool};
 | 
					
 | 
				
			||||||
use tokio::sync::RwLock;
 | 
					use sqlx::mysql::MySqlPool;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use dotenv::dotenv;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use std::{collections::HashMap, env, sync::Arc, time::Instant};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::{
 | 
					use crate::{
 | 
				
			||||||
    commands::{info_cmds, moderation_cmds},
 | 
					    commands::{info_cmds, moderation_cmds, reminder_cmds, todo_cmds},
 | 
				
			||||||
    consts::THEME_COLOR,
 | 
					    consts::{CNC_GUILD, DEFAULT_PREFIX, SUBSCRIPTION_ROLES, THEME_COLOR},
 | 
				
			||||||
    event_handlers::listener,
 | 
					    framework::RegexFramework,
 | 
				
			||||||
    hooks::all_checks,
 | 
					    language_manager::LanguageManager,
 | 
				
			||||||
    models::command_macro::CommandMacro,
 | 
					    models::GuildData,
 | 
				
			||||||
    utils::register_application_commands,
 | 
					 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type Database = MySql;
 | 
					use inflector::Inflector;
 | 
				
			||||||
 | 
					use log::info;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub struct Data {
 | 
					use dashmap::DashMap;
 | 
				
			||||||
    database: Pool<Database>,
 | 
					
 | 
				
			||||||
    http: reqwest::Client,
 | 
					use tokio::sync::RwLock;
 | 
				
			||||||
    recording_macros: RwLock<HashMap<(GuildId, UserId), CommandMacro>>,
 | 
					
 | 
				
			||||||
    popular_timezones: Vec<Tz>,
 | 
					use crate::models::UserData;
 | 
				
			||||||
 | 
					use chrono::Utc;
 | 
				
			||||||
 | 
					use chrono_tz::Tz;
 | 
				
			||||||
 | 
					use serenity::model::prelude::{
 | 
				
			||||||
 | 
					    InteractionApplicationCommandCallbackDataFlags, InteractionResponseType,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					struct GuildDataCache;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl TypeMapKey for GuildDataCache {
 | 
				
			||||||
 | 
					    type Value = Arc<DashMap<GuildId, Arc<RwLock<GuildData>>>>;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type Error = Box<dyn std::error::Error + Send + Sync>;
 | 
					struct SQLPool;
 | 
				
			||||||
type Context<'a> = poise::Context<'a, Data, Error>;
 | 
					
 | 
				
			||||||
 | 
					impl TypeMapKey for SQLPool {
 | 
				
			||||||
 | 
					    type Value = MySqlPool;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					struct ReqwestClient;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl TypeMapKey for ReqwestClient {
 | 
				
			||||||
 | 
					    type Value = Arc<reqwest::Client>;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					struct FrameworkCtx;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl TypeMapKey for FrameworkCtx {
 | 
				
			||||||
 | 
					    type Value = Arc<RegexFramework>;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					struct PopularTimezones;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl TypeMapKey for PopularTimezones {
 | 
				
			||||||
 | 
					    type Value = Arc<Vec<Tz>>;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					struct CurrentlyExecuting;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl TypeMapKey for CurrentlyExecuting {
 | 
				
			||||||
 | 
					    type Value = Arc<RwLock<HashMap<UserId, Instant>>>;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[async_trait]
 | 
				
			||||||
 | 
					trait LimitExecutors {
 | 
				
			||||||
 | 
					    async fn check_executing(&self, user: UserId) -> bool;
 | 
				
			||||||
 | 
					    async fn set_executing(&self, user: UserId);
 | 
				
			||||||
 | 
					    async fn drop_executing(&self, user: UserId);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[async_trait]
 | 
				
			||||||
 | 
					impl LimitExecutors for Context {
 | 
				
			||||||
 | 
					    async fn check_executing(&self, user: UserId) -> bool {
 | 
				
			||||||
 | 
					        let currently_executing = self
 | 
				
			||||||
 | 
					            .data
 | 
				
			||||||
 | 
					            .read()
 | 
				
			||||||
 | 
					            .await
 | 
				
			||||||
 | 
					            .get::<CurrentlyExecuting>()
 | 
				
			||||||
 | 
					            .cloned()
 | 
				
			||||||
 | 
					            .unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let lock = currently_executing.read().await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        lock.get(&user)
 | 
				
			||||||
 | 
					            .map_or(false, |now| now.elapsed().as_secs() < 4)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async fn set_executing(&self, user: UserId) {
 | 
				
			||||||
 | 
					        let currently_executing = self
 | 
				
			||||||
 | 
					            .data
 | 
				
			||||||
 | 
					            .read()
 | 
				
			||||||
 | 
					            .await
 | 
				
			||||||
 | 
					            .get::<CurrentlyExecuting>()
 | 
				
			||||||
 | 
					            .cloned()
 | 
				
			||||||
 | 
					            .unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let mut lock = currently_executing.write().await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        lock.insert(user, Instant::now());
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async fn drop_executing(&self, user: UserId) {
 | 
				
			||||||
 | 
					        let currently_executing = self
 | 
				
			||||||
 | 
					            .data
 | 
				
			||||||
 | 
					            .read()
 | 
				
			||||||
 | 
					            .await
 | 
				
			||||||
 | 
					            .get::<CurrentlyExecuting>()
 | 
				
			||||||
 | 
					            .cloned()
 | 
				
			||||||
 | 
					            .unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let mut lock = currently_executing.write().await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        lock.remove(&user);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					struct Handler;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[async_trait]
 | 
				
			||||||
 | 
					impl EventHandler for Handler {
 | 
				
			||||||
 | 
					    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()
 | 
				
			||||||
 | 
					                    .expect("Could not get SQLPool from data");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                GuildData::from_guild(guild, &pool).await.expect(&format!(
 | 
				
			||||||
 | 
					                    "Failed to create new guild object for {}",
 | 
				
			||||||
 | 
					                    guild_id
 | 
				
			||||||
 | 
					                ));
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if let Ok(token) = env::var("DISCORDBOTS_TOKEN") {
 | 
				
			||||||
 | 
					                let shard_count = ctx.cache.shard_count().await;
 | 
				
			||||||
 | 
					                let current_shard_id = shard_id(guild_id, shard_count);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                let guild_count = ctx
 | 
				
			||||||
 | 
					                    .cache
 | 
				
			||||||
 | 
					                    .guilds()
 | 
				
			||||||
 | 
					                    .await
 | 
				
			||||||
 | 
					                    .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().await.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, guild: GuildUnavailable, _guild: Option<Guild>) {
 | 
				
			||||||
 | 
					        let pool = ctx
 | 
				
			||||||
 | 
					            .data
 | 
				
			||||||
 | 
					            .read()
 | 
				
			||||||
 | 
					            .await
 | 
				
			||||||
 | 
					            .get::<SQLPool>()
 | 
				
			||||||
 | 
					            .cloned()
 | 
				
			||||||
 | 
					            .expect("Could not get SQLPool from data");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let guild_data_cache = ctx
 | 
				
			||||||
 | 
					            .data
 | 
				
			||||||
 | 
					            .read()
 | 
				
			||||||
 | 
					            .await
 | 
				
			||||||
 | 
					            .get::<GuildDataCache>()
 | 
				
			||||||
 | 
					            .cloned()
 | 
				
			||||||
 | 
					            .unwrap();
 | 
				
			||||||
 | 
					        guild_data_cache.remove(&guild.id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        sqlx::query!(
 | 
				
			||||||
 | 
					            "
 | 
				
			||||||
 | 
					DELETE FROM guilds WHERE guild = ?
 | 
				
			||||||
 | 
					            ",
 | 
				
			||||||
 | 
					            guild.id.as_u64()
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        .execute(&pool)
 | 
				
			||||||
 | 
					        .await
 | 
				
			||||||
 | 
					        .unwrap();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async fn interaction_create(&self, ctx: Context, interaction: Interaction) {
 | 
				
			||||||
 | 
					        let (pool, lm) = get_ctx_data(&&ctx).await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        match interaction.kind {
 | 
				
			||||||
 | 
					            InteractionType::ApplicationCommand => {}
 | 
				
			||||||
 | 
					            InteractionType::MessageComponent => {
 | 
				
			||||||
 | 
					                if let (Some(InteractionData::MessageComponent(data)), Some(member)) =
 | 
				
			||||||
 | 
					                    (interaction.clone().data, interaction.clone().member)
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    println!("{}", data.custom_id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    if data.custom_id.starts_with("timezone:") {
 | 
				
			||||||
 | 
					                        let mut user_data = UserData::from_user(&member.user, &ctx, &pool)
 | 
				
			||||||
 | 
					                            .await
 | 
				
			||||||
 | 
					                            .unwrap();
 | 
				
			||||||
 | 
					                        let new_timezone = data.custom_id.replace("timezone:", "").parse::<Tz>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        if let Ok(timezone) = new_timezone {
 | 
				
			||||||
 | 
					                            user_data.timezone = timezone.to_string();
 | 
				
			||||||
 | 
					                            user_data.commit_changes(&pool).await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                            let _ = interaction.create_interaction_response(&ctx, |r| {
 | 
				
			||||||
 | 
					                                r.kind(InteractionResponseType::ChannelMessageWithSource)
 | 
				
			||||||
 | 
					                                    .interaction_response_data(|d| {
 | 
				
			||||||
 | 
					                                        let footer_text = lm.get(&user_data.language, "timezone/footer").replacen(
 | 
				
			||||||
 | 
					                                            "{timezone}",
 | 
				
			||||||
 | 
					                                            &user_data.timezone,
 | 
				
			||||||
 | 
					                                            1,
 | 
				
			||||||
 | 
					                                        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                                        let now = Utc::now().with_timezone(&user_data.timezone());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                                        let content = lm
 | 
				
			||||||
 | 
					                                            .get(&user_data.language, "timezone/set_p")
 | 
				
			||||||
 | 
					                                            .replacen("{timezone}", &user_data.timezone, 1)
 | 
				
			||||||
 | 
					                                            .replacen(
 | 
				
			||||||
 | 
					                                                "{time}",
 | 
				
			||||||
 | 
					                                                &now.format(user_data.meridian().fmt_str_short()).to_string(),
 | 
				
			||||||
 | 
					                                                1,
 | 
				
			||||||
 | 
					                                            );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                                        d.create_embed(|e| e.title(lm.get(&user_data.language, "timezone/set_p_title"))
 | 
				
			||||||
 | 
					                                            .color(*THEME_COLOR)
 | 
				
			||||||
 | 
					                                            .description(content)
 | 
				
			||||||
 | 
					                                            .footer(|f| f.text(footer_text)))
 | 
				
			||||||
 | 
					                                            .flags(InteractionApplicationCommandCallbackDataFlags::EPHEMERAL);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                                        d
 | 
				
			||||||
 | 
					                                    })
 | 
				
			||||||
 | 
					                            }).await;
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    } else if data.custom_id.starts_with("lang:") {
 | 
				
			||||||
 | 
					                        let mut user_data = UserData::from_user(&member.user, &ctx, &pool)
 | 
				
			||||||
 | 
					                            .await
 | 
				
			||||||
 | 
					                            .unwrap();
 | 
				
			||||||
 | 
					                        let lang_code = data.custom_id.replace("lang:", "");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        if let Some(lang) = lm.get_language(&lang_code) {
 | 
				
			||||||
 | 
					                            user_data.language = lang.to_string();
 | 
				
			||||||
 | 
					                            user_data.commit_changes(&pool).await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                            let _ = interaction
 | 
				
			||||||
 | 
					                                .create_interaction_response(&ctx, |r| {
 | 
				
			||||||
 | 
					                                    r.kind(InteractionResponseType::ChannelMessageWithSource)
 | 
				
			||||||
 | 
					                                        .interaction_response_data(|d| {
 | 
				
			||||||
 | 
					                                            d.create_embed(|e| {
 | 
				
			||||||
 | 
					                                                e.title(
 | 
				
			||||||
 | 
					                                                    lm.get(&user_data.language, "lang/set_p_title"),
 | 
				
			||||||
 | 
					                                                )
 | 
				
			||||||
 | 
					                                                .color(*THEME_COLOR)
 | 
				
			||||||
 | 
					                                                .description(
 | 
				
			||||||
 | 
					                                                    lm.get(&user_data.language, "lang/set_p"),
 | 
				
			||||||
 | 
					                                                )
 | 
				
			||||||
 | 
					                                            })
 | 
				
			||||||
 | 
					                                        })
 | 
				
			||||||
 | 
					                                })
 | 
				
			||||||
 | 
					                                .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 +351,245 @@ 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 http = Http::new_with_token(&token);
 | 
				
			||||||
        commands: vec![
 | 
					 | 
				
			||||||
            info_cmds::help(),
 | 
					 | 
				
			||||||
            info_cmds::info(),
 | 
					 | 
				
			||||||
            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 logged_in_id = http
 | 
				
			||||||
        Pool::connect(&env::var("DATABASE_URL").expect("No database URL provided")).await.unwrap();
 | 
					        .get_current_user()
 | 
				
			||||||
 | 
					        .map_ok(|user| user.id.as_u64().to_owned())
 | 
				
			||||||
 | 
					        .await?;
 | 
				
			||||||
 | 
					    let application_id = http.get_current_application_info().await?.id;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let dm_enabled = env::var("DM_ENABLED").map_or(true, |var| var == "1");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let framework = RegexFramework::new(logged_in_id)
 | 
				
			||||||
 | 
					        .default_prefix(DEFAULT_PREFIX.clone())
 | 
				
			||||||
 | 
					        .case_insensitive(env::var("CASE_INSENSITIVE").map_or(true, |var| var == "1"))
 | 
				
			||||||
 | 
					        .ignore_bots(env::var("IGNORE_BOTS").map_or(true, |var| var == "1"))
 | 
				
			||||||
 | 
					        .dm_enabled(dm_enabled)
 | 
				
			||||||
 | 
					        // info commands
 | 
				
			||||||
 | 
					        .add_command("ping", &info_cmds::PING_COMMAND)
 | 
				
			||||||
 | 
					        .add_command("help", &info_cmds::HELP_COMMAND)
 | 
				
			||||||
 | 
					        .add_command("info", &info_cmds::INFO_COMMAND)
 | 
				
			||||||
 | 
					        .add_command("invite", &info_cmds::INFO_COMMAND)
 | 
				
			||||||
 | 
					        .add_command("donate", &info_cmds::DONATE_COMMAND)
 | 
				
			||||||
 | 
					        .add_command("dashboard", &info_cmds::DASHBOARD_COMMAND)
 | 
				
			||||||
 | 
					        .add_command("clock", &info_cmds::CLOCK_COMMAND)
 | 
				
			||||||
 | 
					        // reminder commands
 | 
				
			||||||
 | 
					        .add_command("timer", &reminder_cmds::TIMER_COMMAND)
 | 
				
			||||||
 | 
					        .add_command("remind", &reminder_cmds::REMIND_COMMAND)
 | 
				
			||||||
 | 
					        .add_command("r", &reminder_cmds::REMIND_COMMAND)
 | 
				
			||||||
 | 
					        .add_command("interval", &reminder_cmds::INTERVAL_COMMAND)
 | 
				
			||||||
 | 
					        .add_command("i", &reminder_cmds::INTERVAL_COMMAND)
 | 
				
			||||||
 | 
					        .add_command("natural", &reminder_cmds::NATURAL_COMMAND)
 | 
				
			||||||
 | 
					        .add_command("n", &reminder_cmds::NATURAL_COMMAND)
 | 
				
			||||||
 | 
					        .add_command("", &reminder_cmds::NATURAL_COMMAND)
 | 
				
			||||||
 | 
					        .add_command("countdown", &reminder_cmds::COUNTDOWN_COMMAND)
 | 
				
			||||||
 | 
					        // management commands
 | 
				
			||||||
 | 
					        .add_command("look", &reminder_cmds::LOOK_COMMAND)
 | 
				
			||||||
 | 
					        .add_command("del", &reminder_cmds::DELETE_COMMAND)
 | 
				
			||||||
 | 
					        // to-do commands
 | 
				
			||||||
 | 
					        .add_command("todo", &todo_cmds::TODO_USER_COMMAND)
 | 
				
			||||||
 | 
					        .add_command("todo user", &todo_cmds::TODO_USER_COMMAND)
 | 
				
			||||||
 | 
					        .add_command("todoc", &todo_cmds::TODO_CHANNEL_COMMAND)
 | 
				
			||||||
 | 
					        .add_command("todo channel", &todo_cmds::TODO_CHANNEL_COMMAND)
 | 
				
			||||||
 | 
					        .add_command("todos", &todo_cmds::TODO_GUILD_COMMAND)
 | 
				
			||||||
 | 
					        .add_command("todo server", &todo_cmds::TODO_GUILD_COMMAND)
 | 
				
			||||||
 | 
					        .add_command("todo guild", &todo_cmds::TODO_GUILD_COMMAND)
 | 
				
			||||||
 | 
					        // moderation commands
 | 
				
			||||||
 | 
					        .add_command("blacklist", &moderation_cmds::BLACKLIST_COMMAND)
 | 
				
			||||||
 | 
					        .add_command("restrict", &moderation_cmds::RESTRICT_COMMAND)
 | 
				
			||||||
 | 
					        .add_command("timezone", &moderation_cmds::TIMEZONE_COMMAND)
 | 
				
			||||||
 | 
					        .add_command("meridian", &moderation_cmds::CHANGE_MERIDIAN_COMMAND)
 | 
				
			||||||
 | 
					        .add_command("prefix", &moderation_cmds::PREFIX_COMMAND)
 | 
				
			||||||
 | 
					        .add_command("lang", &moderation_cmds::LANGUAGE_COMMAND)
 | 
				
			||||||
 | 
					        .add_command("pause", &reminder_cmds::PAUSE_COMMAND)
 | 
				
			||||||
 | 
					        .add_command("offset", &reminder_cmds::OFFSET_COMMAND)
 | 
				
			||||||
 | 
					        .add_command("nudge", &reminder_cmds::NUDGE_COMMAND)
 | 
				
			||||||
 | 
					        .add_command("alias", &moderation_cmds::ALIAS_COMMAND)
 | 
				
			||||||
 | 
					        .add_command("a", &moderation_cmds::ALIAS_COMMAND)
 | 
				
			||||||
 | 
					        .build();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let framework_arc = Arc::new(framework);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let mut client = Client::builder(&token)
 | 
				
			||||||
 | 
					        .intents(if dm_enabled {
 | 
				
			||||||
 | 
					            GatewayIntents::GUILD_MESSAGES
 | 
				
			||||||
 | 
					                | GatewayIntents::GUILDS
 | 
				
			||||||
 | 
					                | GatewayIntents::GUILD_MESSAGE_REACTIONS
 | 
				
			||||||
 | 
					                | GatewayIntents::DIRECT_MESSAGES
 | 
				
			||||||
 | 
					                | GatewayIntents::DIRECT_MESSAGE_REACTIONS
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            GatewayIntents::GUILD_MESSAGES
 | 
				
			||||||
 | 
					                | GatewayIntents::GUILDS
 | 
				
			||||||
 | 
					                | GatewayIntents::GUILD_MESSAGE_REACTIONS
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        .application_id(application_id.0)
 | 
				
			||||||
 | 
					        .event_handler(Handler)
 | 
				
			||||||
 | 
					        .framework_arc(framework_arc.clone())
 | 
				
			||||||
 | 
					        .await
 | 
				
			||||||
 | 
					        .expect("Error occurred creating client");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        let guild_data_cache = dashmap::DashMap::new();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let pool = MySqlPool::connect(
 | 
				
			||||||
 | 
					            &env::var("DATABASE_URL").expect("Missing DATABASE_URL from environment"),
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        .await
 | 
				
			||||||
 | 
					        .unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let language_manager = LanguageManager::from_compiled(include_str!(concat!(
 | 
				
			||||||
 | 
					            env!("CARGO_MANIFEST_DIR"),
 | 
				
			||||||
 | 
					            "/assets/",
 | 
				
			||||||
 | 
					            env!("STRINGS_FILE")
 | 
				
			||||||
 | 
					        )))
 | 
				
			||||||
 | 
					        .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::<GuildDataCache>(Arc::new(guild_data_cache));
 | 
				
			||||||
                    ctx,
 | 
					        data.insert::<CurrentlyExecuting>(Arc::new(RwLock::new(HashMap::new())));
 | 
				
			||||||
                    framework,
 | 
					        data.insert::<SQLPool>(pool);
 | 
				
			||||||
                    env::var("DEBUG_GUILD")
 | 
					        data.insert::<PopularTimezones>(Arc::new(popular_timezones));
 | 
				
			||||||
                        .map(|inner| GuildId(inner.parse().expect("DEBUG_GUILD not valid")))
 | 
					        data.insert::<ReqwestClient>(Arc::new(reqwest::Client::new()));
 | 
				
			||||||
                        .ok(),
 | 
					        data.insert::<FrameworkCtx>(framework_arc.clone());
 | 
				
			||||||
                )
 | 
					        data.insert::<LanguageManager>(Arc::new(language_manager))
 | 
				
			||||||
                .await
 | 
					    }
 | 
				
			||||||
                .unwrap();
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
                Ok(Data {
 | 
					    if let Ok((Some(lower), Some(upper))) = env::var("SHARD_RANGE").map(|sr| {
 | 
				
			||||||
                    http: reqwest::Client::new(),
 | 
					        let mut split = sr
 | 
				
			||||||
                    database,
 | 
					            .split(',')
 | 
				
			||||||
                    popular_timezones,
 | 
					            .map(|val| val.parse::<u64>().expect("SHARD_RANGE not an integer"));
 | 
				
			||||||
                    recording_macros: Default::default(),
 | 
					
 | 
				
			||||||
                })
 | 
					        (split.next(), split.next())
 | 
				
			||||||
            })
 | 
					    }) {
 | 
				
			||||||
        })
 | 
					        let total_shards = env::var("SHARD_COUNT")
 | 
				
			||||||
        .options(options)
 | 
					            .map(|shard_count| shard_count.parse::<u64>().ok())
 | 
				
			||||||
        .client_settings(move |client_builder| client_builder.intents(GatewayIntents::GUILDS))
 | 
					            .ok()
 | 
				
			||||||
        .run_autosharded()
 | 
					            .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?;
 | 
					            .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_subscription_on_message(
 | 
				
			||||||
 | 
					    cache_http: impl CacheHttp + AsRef<Cache>,
 | 
				
			||||||
 | 
					    msg: &Message,
 | 
				
			||||||
 | 
					) -> bool {
 | 
				
			||||||
 | 
					    check_subscription(&cache_http, &msg.author).await
 | 
				
			||||||
 | 
					        || if let Some(guild) = msg.guild(&cache_http).await {
 | 
				
			||||||
 | 
					            check_subscription(&cache_http, guild.owner_id).await
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            false
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub async fn get_ctx_data(ctx: &&Context) -> (MySqlPool, Arc<LanguageManager>) {
 | 
				
			||||||
 | 
					    let pool;
 | 
				
			||||||
 | 
					    let lm;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        let data = ctx.data.read().await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        pool = data
 | 
				
			||||||
 | 
					            .get::<SQLPool>()
 | 
				
			||||||
 | 
					            .cloned()
 | 
				
			||||||
 | 
					            .expect("Could not get SQLPool");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        lm = data
 | 
				
			||||||
 | 
					            .get::<LanguageManager>()
 | 
				
			||||||
 | 
					            .cloned()
 | 
				
			||||||
 | 
					            .expect("Could not get LanguageManager");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    (pool, lm)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async fn command_help(
 | 
				
			||||||
 | 
					    ctx: &Context,
 | 
				
			||||||
 | 
					    msg: &Message,
 | 
				
			||||||
 | 
					    lm: Arc<LanguageManager>,
 | 
				
			||||||
 | 
					    prefix: &str,
 | 
				
			||||||
 | 
					    language: &str,
 | 
				
			||||||
 | 
					    command_name: &str,
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					    let _ = msg
 | 
				
			||||||
 | 
					        .channel_id
 | 
				
			||||||
 | 
					        .send_message(ctx, |m| {
 | 
				
			||||||
 | 
					            m.embed(move |e| {
 | 
				
			||||||
 | 
					                e.title(format!("{} Help", command_name.to_title_case()))
 | 
				
			||||||
 | 
					                    .description(
 | 
				
			||||||
 | 
					                        lm.get(&language, &format!("help/{}", command_name))
 | 
				
			||||||
 | 
					                            .replace("{prefix}", &prefix),
 | 
				
			||||||
 | 
					                    )
 | 
				
			||||||
 | 
					                    .footer(|f| {
 | 
				
			||||||
 | 
					                        f.text(concat!(
 | 
				
			||||||
 | 
					                            env!("CARGO_PKG_NAME"),
 | 
				
			||||||
 | 
					                            " ver ",
 | 
				
			||||||
 | 
					                            env!("CARGO_PKG_VERSION")
 | 
				
			||||||
 | 
					                        ))
 | 
				
			||||||
 | 
					                    })
 | 
				
			||||||
 | 
					                    .color(*THEME_COLOR)
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        .await;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										452
									
								
								src/models.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										452
									
								
								src/models.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,452 @@
 | 
				
			|||||||
 | 
					use serenity::{
 | 
				
			||||||
 | 
					    async_trait,
 | 
				
			||||||
 | 
					    http::CacheHttp,
 | 
				
			||||||
 | 
					    model::{
 | 
				
			||||||
 | 
					        channel::Channel,
 | 
				
			||||||
 | 
					        guild::Guild,
 | 
				
			||||||
 | 
					        id::{GuildId, UserId},
 | 
				
			||||||
 | 
					        user::User,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    prelude::Context,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use sqlx::MySqlPool;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use chrono::NaiveDateTime;
 | 
				
			||||||
 | 
					use chrono_tz::Tz;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use log::error;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::{
 | 
				
			||||||
 | 
					    consts::{DEFAULT_PREFIX, LOCAL_LANGUAGE, LOCAL_TIMEZONE},
 | 
				
			||||||
 | 
					    GuildDataCache, SQLPool,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use std::sync::Arc;
 | 
				
			||||||
 | 
					use tokio::sync::RwLock;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[async_trait]
 | 
				
			||||||
 | 
					pub trait CtxGuildData {
 | 
				
			||||||
 | 
					    async fn guild_data<G: Into<GuildId> + Send + Sync>(
 | 
				
			||||||
 | 
					        &self,
 | 
				
			||||||
 | 
					        guild_id: G,
 | 
				
			||||||
 | 
					    ) -> Result<Arc<RwLock<GuildData>>, sqlx::Error>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async fn prefix<G: Into<GuildId> + Send + Sync>(&self, guild_id: Option<G>) -> String;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[async_trait]
 | 
				
			||||||
 | 
					impl CtxGuildData for Context {
 | 
				
			||||||
 | 
					    async fn guild_data<G: Into<GuildId> + Send + Sync>(
 | 
				
			||||||
 | 
					        &self,
 | 
				
			||||||
 | 
					        guild_id: G,
 | 
				
			||||||
 | 
					    ) -> Result<Arc<RwLock<GuildData>>, sqlx::Error> {
 | 
				
			||||||
 | 
					        let guild_id = guild_id.into();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let guild = guild_id.to_guild_cached(&self.cache).await.unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let guild_cache = self
 | 
				
			||||||
 | 
					            .data
 | 
				
			||||||
 | 
					            .read()
 | 
				
			||||||
 | 
					            .await
 | 
				
			||||||
 | 
					            .get::<GuildDataCache>()
 | 
				
			||||||
 | 
					            .cloned()
 | 
				
			||||||
 | 
					            .unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let x = if let Some(guild_data) = guild_cache.get(&guild_id) {
 | 
				
			||||||
 | 
					            Ok(guild_data.clone())
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            let pool = self.data.read().await.get::<SQLPool>().cloned().unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            match GuildData::from_guild(guild, &pool).await {
 | 
				
			||||||
 | 
					                Ok(d) => {
 | 
				
			||||||
 | 
					                    let lock = Arc::new(RwLock::new(d));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    guild_cache.insert(guild_id, lock.clone());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    Ok(lock)
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                Err(e) => Err(e),
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        x
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async fn prefix<G: Into<GuildId> + Send + Sync>(&self, guild_id: Option<G>) -> String {
 | 
				
			||||||
 | 
					        if let Some(guild_id) = guild_id {
 | 
				
			||||||
 | 
					            self.guild_data(guild_id)
 | 
				
			||||||
 | 
					                .await
 | 
				
			||||||
 | 
					                .unwrap()
 | 
				
			||||||
 | 
					                .read()
 | 
				
			||||||
 | 
					                .await
 | 
				
			||||||
 | 
					                .prefix
 | 
				
			||||||
 | 
					                .clone()
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            DEFAULT_PREFIX.clone()
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub struct GuildData {
 | 
				
			||||||
 | 
					    pub id: u32,
 | 
				
			||||||
 | 
					    pub name: Option<String>,
 | 
				
			||||||
 | 
					    pub prefix: String,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl GuildData {
 | 
				
			||||||
 | 
					    pub async fn from_guild(guild: Guild, pool: &MySqlPool) -> Result<Self, sqlx::Error> {
 | 
				
			||||||
 | 
					        let guild_id = guild.id.as_u64().to_owned();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        match sqlx::query_as!(
 | 
				
			||||||
 | 
					            Self,
 | 
				
			||||||
 | 
					            "
 | 
				
			||||||
 | 
					SELECT id, name, prefix FROM guilds WHERE guild = ?
 | 
				
			||||||
 | 
					            ",
 | 
				
			||||||
 | 
					            guild_id
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        .fetch_one(pool)
 | 
				
			||||||
 | 
					        .await
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            Ok(mut g) => {
 | 
				
			||||||
 | 
					                g.name = Some(guild.name);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                Ok(g)
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            Err(sqlx::Error::RowNotFound) => {
 | 
				
			||||||
 | 
					                sqlx::query!(
 | 
				
			||||||
 | 
					                    "
 | 
				
			||||||
 | 
					INSERT INTO guilds (guild, name, prefix) VALUES (?, ?, ?)
 | 
				
			||||||
 | 
					                    ",
 | 
				
			||||||
 | 
					                    guild_id,
 | 
				
			||||||
 | 
					                    guild.name,
 | 
				
			||||||
 | 
					                    *DEFAULT_PREFIX
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					                .execute(&pool.clone())
 | 
				
			||||||
 | 
					                .await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                Ok(sqlx::query_as!(
 | 
				
			||||||
 | 
					                    Self,
 | 
				
			||||||
 | 
					                    "
 | 
				
			||||||
 | 
					SELECT id, name, prefix FROM guilds WHERE guild = ?
 | 
				
			||||||
 | 
					                    ",
 | 
				
			||||||
 | 
					                    guild_id
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					                .fetch_one(pool)
 | 
				
			||||||
 | 
					                .await?)
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            Err(e) => {
 | 
				
			||||||
 | 
					                error!("Unexpected error in guild query: {:?}", e);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                Err(e)
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub async fn commit_changes(&self, pool: &MySqlPool) {
 | 
				
			||||||
 | 
					        sqlx::query!(
 | 
				
			||||||
 | 
					            "
 | 
				
			||||||
 | 
					UPDATE guilds SET name = ?, prefix = ? WHERE id = ?
 | 
				
			||||||
 | 
					            ",
 | 
				
			||||||
 | 
					            self.name,
 | 
				
			||||||
 | 
					            self.prefix,
 | 
				
			||||||
 | 
					            self.id
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        .execute(pool)
 | 
				
			||||||
 | 
					        .await
 | 
				
			||||||
 | 
					        .unwrap();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub struct ChannelData {
 | 
				
			||||||
 | 
					    pub id: u32,
 | 
				
			||||||
 | 
					    pub name: Option<String>,
 | 
				
			||||||
 | 
					    pub nudge: i16,
 | 
				
			||||||
 | 
					    pub blacklisted: bool,
 | 
				
			||||||
 | 
					    pub webhook_id: Option<u64>,
 | 
				
			||||||
 | 
					    pub webhook_token: Option<String>,
 | 
				
			||||||
 | 
					    pub paused: bool,
 | 
				
			||||||
 | 
					    pub paused_until: Option<NaiveDateTime>,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl ChannelData {
 | 
				
			||||||
 | 
					    pub async fn from_channel(
 | 
				
			||||||
 | 
					        channel: Channel,
 | 
				
			||||||
 | 
					        pool: &MySqlPool,
 | 
				
			||||||
 | 
					    ) -> Result<Self, Box<dyn std::error::Error + Sync + Send>> {
 | 
				
			||||||
 | 
					        let channel_id = channel.id().as_u64().to_owned();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if let Ok(c) = sqlx::query_as_unchecked!(Self,
 | 
				
			||||||
 | 
					            "
 | 
				
			||||||
 | 
					SELECT id, name, nudge, blacklisted, webhook_id, webhook_token, paused, paused_until FROM channels WHERE channel = ?
 | 
				
			||||||
 | 
					            ", channel_id)
 | 
				
			||||||
 | 
					            .fetch_one(pool)
 | 
				
			||||||
 | 
					            .await {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            Ok(c)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        else {
 | 
				
			||||||
 | 
					            let props = channel.guild().map(|g| (g.guild_id.as_u64().to_owned(), g.name));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            let (guild_id, channel_name) = if let Some((a, b)) = props {
 | 
				
			||||||
 | 
					                (Some(a), Some(b))
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                (None, None)
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            sqlx::query!(
 | 
				
			||||||
 | 
					                "
 | 
				
			||||||
 | 
					INSERT IGNORE INTO channels (channel, name, guild_id) VALUES (?, ?, (SELECT id FROM guilds WHERE guild = ?))
 | 
				
			||||||
 | 
					                ", channel_id, channel_name, guild_id)
 | 
				
			||||||
 | 
					                .execute(&pool.clone())
 | 
				
			||||||
 | 
					                .await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            Ok(sqlx::query_as_unchecked!(Self,
 | 
				
			||||||
 | 
					                "
 | 
				
			||||||
 | 
					SELECT id, name, nudge, blacklisted, webhook_id, webhook_token, paused, paused_until FROM channels WHERE channel = ?
 | 
				
			||||||
 | 
					                ", channel_id)
 | 
				
			||||||
 | 
					                .fetch_one(pool)
 | 
				
			||||||
 | 
					                .await?)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub async fn commit_changes(&self, pool: &MySqlPool) {
 | 
				
			||||||
 | 
					        sqlx::query!(
 | 
				
			||||||
 | 
					            "
 | 
				
			||||||
 | 
					UPDATE channels SET name = ?, nudge = ?, blacklisted = ?, webhook_id = ?, webhook_token = ?, paused = ?, paused_until = ? WHERE id = ?
 | 
				
			||||||
 | 
					            ", self.name, self.nudge, self.blacklisted, self.webhook_id, self.webhook_token, self.paused, self.paused_until, self.id)
 | 
				
			||||||
 | 
					            .execute(pool)
 | 
				
			||||||
 | 
					            .await.unwrap();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub struct UserData {
 | 
				
			||||||
 | 
					    pub id: u32,
 | 
				
			||||||
 | 
					    pub user: u64,
 | 
				
			||||||
 | 
					    pub name: String,
 | 
				
			||||||
 | 
					    pub dm_channel: u32,
 | 
				
			||||||
 | 
					    pub language: String,
 | 
				
			||||||
 | 
					    pub timezone: String,
 | 
				
			||||||
 | 
					    pub meridian_time: bool,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub struct MeridianType(bool);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl MeridianType {
 | 
				
			||||||
 | 
					    pub fn fmt_str(&self) -> &str {
 | 
				
			||||||
 | 
					        if self.0 {
 | 
				
			||||||
 | 
					            "%Y-%m-%d %I:%M:%S %p"
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            "%Y-%m-%d %H:%M:%S"
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn fmt_str_short(&self) -> &str {
 | 
				
			||||||
 | 
					        if self.0 {
 | 
				
			||||||
 | 
					            "%I:%M %p"
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            "%H:%M"
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl UserData {
 | 
				
			||||||
 | 
					    pub async fn language_of<U>(user: U, pool: &MySqlPool) -> String
 | 
				
			||||||
 | 
					    where
 | 
				
			||||||
 | 
					        U: Into<UserId>,
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        let user_id = user.into().as_u64().to_owned();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        match sqlx::query!(
 | 
				
			||||||
 | 
					            "
 | 
				
			||||||
 | 
					SELECT language FROM users WHERE user = ?
 | 
				
			||||||
 | 
					            ",
 | 
				
			||||||
 | 
					            user_id
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        .fetch_one(pool)
 | 
				
			||||||
 | 
					        .await
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            Ok(r) => r.language,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            Err(_) => LOCAL_LANGUAGE.clone(),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub async fn timezone_of<U>(user: U, pool: &MySqlPool) -> Tz
 | 
				
			||||||
 | 
					    where
 | 
				
			||||||
 | 
					        U: Into<UserId>,
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        let user_id = user.into().as_u64().to_owned();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        match sqlx::query!(
 | 
				
			||||||
 | 
					            "
 | 
				
			||||||
 | 
					SELECT timezone FROM users WHERE user = ?
 | 
				
			||||||
 | 
					            ",
 | 
				
			||||||
 | 
					            user_id
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        .fetch_one(pool)
 | 
				
			||||||
 | 
					        .await
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            Ok(r) => r.timezone,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            Err(_) => LOCAL_TIMEZONE.clone(),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        .parse()
 | 
				
			||||||
 | 
					        .unwrap()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub async fn meridian_of<U>(user: U, pool: &MySqlPool) -> MeridianType
 | 
				
			||||||
 | 
					    where
 | 
				
			||||||
 | 
					        U: Into<UserId>,
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        let user_id = user.into().as_u64().to_owned();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        match sqlx::query!(
 | 
				
			||||||
 | 
					            "
 | 
				
			||||||
 | 
					SELECT meridian_time FROM users WHERE user = ?
 | 
				
			||||||
 | 
					            ",
 | 
				
			||||||
 | 
					            user_id
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        .fetch_one(pool)
 | 
				
			||||||
 | 
					        .await
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            Ok(r) => MeridianType(r.meridian_time != 0),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            Err(_) => MeridianType(false),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub async fn from_user(
 | 
				
			||||||
 | 
					        user: &User,
 | 
				
			||||||
 | 
					        ctx: impl CacheHttp,
 | 
				
			||||||
 | 
					        pool: &MySqlPool,
 | 
				
			||||||
 | 
					    ) -> Result<Self, Box<dyn std::error::Error + Sync + Send>> {
 | 
				
			||||||
 | 
					        let user_id = user.id.as_u64().to_owned();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        match sqlx::query_as_unchecked!(
 | 
				
			||||||
 | 
					            Self,
 | 
				
			||||||
 | 
					            "
 | 
				
			||||||
 | 
					SELECT id, user, name, dm_channel, IF(language IS NULL, ?, language) AS language, IF(timezone IS NULL, ?, timezone) AS timezone, meridian_time FROM users WHERE user = ?
 | 
				
			||||||
 | 
					            ",
 | 
				
			||||||
 | 
					            *LOCAL_LANGUAGE, *LOCAL_TIMEZONE, user_id
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        .fetch_one(pool)
 | 
				
			||||||
 | 
					        .await
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            Ok(c) => Ok(c),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            Err(sqlx::Error::RowNotFound) => {
 | 
				
			||||||
 | 
					                let dm_channel = user.create_dm_channel(ctx).await?;
 | 
				
			||||||
 | 
					                let dm_id = dm_channel.id.as_u64().to_owned();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                let pool_c = pool.clone();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                sqlx::query!(
 | 
				
			||||||
 | 
					                    "
 | 
				
			||||||
 | 
					INSERT IGNORE INTO channels (channel) VALUES (?)
 | 
				
			||||||
 | 
					                    ",
 | 
				
			||||||
 | 
					                    dm_id
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					                .execute(&pool_c)
 | 
				
			||||||
 | 
					                .await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                sqlx::query!(
 | 
				
			||||||
 | 
					                    "
 | 
				
			||||||
 | 
					INSERT INTO users (user, name, dm_channel, language, timezone) VALUES (?, ?, (SELECT id FROM channels WHERE channel = ?), ?, ?)
 | 
				
			||||||
 | 
					                    ", user_id, user.name, dm_id, *LOCAL_LANGUAGE, *LOCAL_TIMEZONE)
 | 
				
			||||||
 | 
					                    .execute(&pool_c)
 | 
				
			||||||
 | 
					                    .await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                Ok(sqlx::query_as_unchecked!(
 | 
				
			||||||
 | 
					                    Self,
 | 
				
			||||||
 | 
					                    "
 | 
				
			||||||
 | 
					SELECT id, user, name, dm_channel, language, timezone, meridian_time FROM users WHERE user = ?
 | 
				
			||||||
 | 
					                    ",
 | 
				
			||||||
 | 
					                    user_id
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					                .fetch_one(pool)
 | 
				
			||||||
 | 
					                .await?)
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            Err(e) => {
 | 
				
			||||||
 | 
					                error!("Error querying for user: {:?}", e);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                Err(Box::new(e))
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub async fn commit_changes(&self, pool: &MySqlPool) {
 | 
				
			||||||
 | 
					        sqlx::query!(
 | 
				
			||||||
 | 
					            "
 | 
				
			||||||
 | 
					UPDATE users SET name = ?, language = ?, timezone = ?, meridian_time = ? WHERE id = ?
 | 
				
			||||||
 | 
					            ",
 | 
				
			||||||
 | 
					            self.name,
 | 
				
			||||||
 | 
					            self.language,
 | 
				
			||||||
 | 
					            self.timezone,
 | 
				
			||||||
 | 
					            self.meridian_time,
 | 
				
			||||||
 | 
					            self.id
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        .execute(pool)
 | 
				
			||||||
 | 
					        .await
 | 
				
			||||||
 | 
					        .unwrap();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn timezone(&self) -> Tz {
 | 
				
			||||||
 | 
					        self.timezone.parse().unwrap()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn meridian(&self) -> MeridianType {
 | 
				
			||||||
 | 
					        MeridianType(self.meridian_time)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub struct Timer {
 | 
				
			||||||
 | 
					    pub name: String,
 | 
				
			||||||
 | 
					    pub start_time: NaiveDateTime,
 | 
				
			||||||
 | 
					    pub owner: u64,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl Timer {
 | 
				
			||||||
 | 
					    pub async fn from_owner(owner: u64, pool: &MySqlPool) -> Vec<Self> {
 | 
				
			||||||
 | 
					        sqlx::query_as_unchecked!(
 | 
				
			||||||
 | 
					            Timer,
 | 
				
			||||||
 | 
					            "
 | 
				
			||||||
 | 
					SELECT name, start_time, owner FROM timers WHERE owner = ?
 | 
				
			||||||
 | 
					            ",
 | 
				
			||||||
 | 
					            owner
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        .fetch_all(pool)
 | 
				
			||||||
 | 
					        .await
 | 
				
			||||||
 | 
					        .unwrap()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub async fn count_from_owner(owner: u64, pool: &MySqlPool) -> u32 {
 | 
				
			||||||
 | 
					        sqlx::query!(
 | 
				
			||||||
 | 
					            "
 | 
				
			||||||
 | 
					SELECT COUNT(1) as count FROM timers WHERE owner = ?
 | 
				
			||||||
 | 
					            ",
 | 
				
			||||||
 | 
					            owner
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        .fetch_one(pool)
 | 
				
			||||||
 | 
					        .await
 | 
				
			||||||
 | 
					        .unwrap()
 | 
				
			||||||
 | 
					        .count as u32
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub async fn create(name: &str, owner: u64, pool: &MySqlPool) {
 | 
				
			||||||
 | 
					        sqlx::query!(
 | 
				
			||||||
 | 
					            "
 | 
				
			||||||
 | 
					INSERT INTO timers (name, owner) VALUES (?, ?)
 | 
				
			||||||
 | 
					            ",
 | 
				
			||||||
 | 
					            name,
 | 
				
			||||||
 | 
					            owner
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        .execute(pool)
 | 
				
			||||||
 | 
					        .await
 | 
				
			||||||
 | 
					        .unwrap();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,81 +0,0 @@
 | 
				
			|||||||
use chrono::NaiveDateTime;
 | 
					 | 
				
			||||||
use poise::serenity::model::channel::Channel;
 | 
					 | 
				
			||||||
use sqlx::MySqlPool;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
pub struct ChannelData {
 | 
					 | 
				
			||||||
    pub id: u32,
 | 
					 | 
				
			||||||
    pub name: Option<String>,
 | 
					 | 
				
			||||||
    pub nudge: i16,
 | 
					 | 
				
			||||||
    pub blacklisted: bool,
 | 
					 | 
				
			||||||
    pub webhook_id: Option<u64>,
 | 
					 | 
				
			||||||
    pub webhook_token: Option<String>,
 | 
					 | 
				
			||||||
    pub paused: bool,
 | 
					 | 
				
			||||||
    pub paused_until: Option<NaiveDateTime>,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl ChannelData {
 | 
					 | 
				
			||||||
    pub async fn from_channel(
 | 
					 | 
				
			||||||
        channel: &Channel,
 | 
					 | 
				
			||||||
        pool: &MySqlPool,
 | 
					 | 
				
			||||||
    ) -> Result<Self, Box<dyn std::error::Error + Sync + Send>> {
 | 
					 | 
				
			||||||
        let channel_id = channel.id().as_u64().to_owned();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if let Ok(c) = sqlx::query_as_unchecked!(
 | 
					 | 
				
			||||||
            Self,
 | 
					 | 
				
			||||||
            "
 | 
					 | 
				
			||||||
SELECT id, name, nudge, blacklisted, webhook_id, webhook_token, paused, paused_until FROM channels WHERE channel = ?
 | 
					 | 
				
			||||||
            ",
 | 
					 | 
				
			||||||
            channel_id
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        .fetch_one(pool)
 | 
					 | 
				
			||||||
        .await
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            Ok(c)
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
            let props = channel.to_owned().guild().map(|g| (g.guild_id.as_u64().to_owned(), g.name));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            let (guild_id, channel_name) = if let Some((a, b)) = props { (Some(a), Some(b)) } else { (None, None) };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            sqlx::query!(
 | 
					 | 
				
			||||||
                "
 | 
					 | 
				
			||||||
INSERT IGNORE INTO channels (channel, name, guild_id) VALUES (?, ?, (SELECT id FROM guilds WHERE guild = ?))
 | 
					 | 
				
			||||||
                ",
 | 
					 | 
				
			||||||
                channel_id,
 | 
					 | 
				
			||||||
                channel_name,
 | 
					 | 
				
			||||||
                guild_id
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
            .execute(&pool.clone())
 | 
					 | 
				
			||||||
            .await?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            Ok(sqlx::query_as_unchecked!(
 | 
					 | 
				
			||||||
                Self,
 | 
					 | 
				
			||||||
                "
 | 
					 | 
				
			||||||
SELECT id, name, nudge, blacklisted, webhook_id, webhook_token, paused, paused_until FROM channels WHERE channel = ?
 | 
					 | 
				
			||||||
                ",
 | 
					 | 
				
			||||||
                channel_id
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
            .fetch_one(pool)
 | 
					 | 
				
			||||||
            .await?)
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    pub async fn commit_changes(&self, pool: &MySqlPool) {
 | 
					 | 
				
			||||||
        sqlx::query!(
 | 
					 | 
				
			||||||
            "
 | 
					 | 
				
			||||||
UPDATE channels SET name = ?, nudge = ?, blacklisted = ?, webhook_id = ?, webhook_token = ?, paused = ?, paused_until \
 | 
					 | 
				
			||||||
             = ? WHERE id = ?
 | 
					 | 
				
			||||||
            ",
 | 
					 | 
				
			||||||
            self.name,
 | 
					 | 
				
			||||||
            self.nudge,
 | 
					 | 
				
			||||||
            self.blacklisted,
 | 
					 | 
				
			||||||
            self.webhook_id,
 | 
					 | 
				
			||||||
            self.webhook_token,
 | 
					 | 
				
			||||||
            self.paused,
 | 
					 | 
				
			||||||
            self.paused_until,
 | 
					 | 
				
			||||||
            self.id
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        .execute(pool)
 | 
					 | 
				
			||||||
        .await
 | 
					 | 
				
			||||||
        .unwrap();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,267 +0,0 @@
 | 
				
			|||||||
use std::collections::HashMap;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use poise::{
 | 
					 | 
				
			||||||
    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 guild_id: GuildId,
 | 
					 | 
				
			||||||
    pub name: String,
 | 
					 | 
				
			||||||
    pub description: Option<String>,
 | 
					 | 
				
			||||||
    pub commands: Vec<CommandOptions>,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl CommandMacro {
 | 
					 | 
				
			||||||
    pub async fn from_guild(
 | 
					 | 
				
			||||||
        db_pool: impl Executor<'_, Database = Database>,
 | 
					 | 
				
			||||||
        guild_id: impl Into<GuildId>,
 | 
					 | 
				
			||||||
    ) -> Vec<Self> {
 | 
					 | 
				
			||||||
        let guild_id = guild_id.into();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        sqlx::query!(
 | 
					 | 
				
			||||||
            "SELECT * FROM macro WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?)",
 | 
					 | 
				
			||||||
            guild_id.0
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        .fetch_all(db_pool)
 | 
					 | 
				
			||||||
        .await
 | 
					 | 
				
			||||||
        .unwrap()
 | 
					 | 
				
			||||||
        .iter()
 | 
					 | 
				
			||||||
        .map(|row| Self {
 | 
					 | 
				
			||||||
            guild_id,
 | 
					 | 
				
			||||||
            name: row.name.clone(),
 | 
					 | 
				
			||||||
            description: row.description.clone(),
 | 
					 | 
				
			||||||
            commands: serde_json::from_str(&row.commands).unwrap(),
 | 
					 | 
				
			||||||
        })
 | 
					 | 
				
			||||||
        .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)
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,51 +0,0 @@
 | 
				
			|||||||
pub mod channel_data;
 | 
					 | 
				
			||||||
pub mod command_macro;
 | 
					 | 
				
			||||||
pub mod reminder;
 | 
					 | 
				
			||||||
pub mod timer;
 | 
					 | 
				
			||||||
pub mod user_data;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use chrono_tz::Tz;
 | 
					 | 
				
			||||||
use poise::serenity::{async_trait, model::id::UserId};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use crate::{
 | 
					 | 
				
			||||||
    models::{channel_data::ChannelData, user_data::UserData},
 | 
					 | 
				
			||||||
    Context,
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[async_trait]
 | 
					 | 
				
			||||||
pub trait CtxData {
 | 
					 | 
				
			||||||
    async fn user_data<U: Into<UserId> + Send>(
 | 
					 | 
				
			||||||
        &self,
 | 
					 | 
				
			||||||
        user_id: U,
 | 
					 | 
				
			||||||
    ) -> Result<UserData, Box<dyn std::error::Error + Sync + Send>>;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    async fn author_data(&self) -> Result<UserData, Box<dyn std::error::Error + Sync + Send>>;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    async fn timezone(&self) -> Tz;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    async fn channel_data(&self) -> Result<ChannelData, Box<dyn std::error::Error + Sync + Send>>;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[async_trait]
 | 
					 | 
				
			||||||
impl CtxData for Context<'_> {
 | 
					 | 
				
			||||||
    async fn user_data<U: Into<UserId> + Send>(
 | 
					 | 
				
			||||||
        &self,
 | 
					 | 
				
			||||||
        user_id: U,
 | 
					 | 
				
			||||||
    ) -> Result<UserData, Box<dyn std::error::Error + Sync + Send>> {
 | 
					 | 
				
			||||||
        UserData::from_user(user_id, &self.discord(), &self.data().database).await
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    async fn author_data(&self) -> Result<UserData, Box<dyn std::error::Error + Sync + Send>> {
 | 
					 | 
				
			||||||
        UserData::from_user(&self.author().id, &self.discord(), &self.data().database).await
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    async fn timezone(&self) -> Tz {
 | 
					 | 
				
			||||||
        UserData::timezone_of(self.author().id, &self.data().database).await
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    async fn channel_data(&self) -> Result<ChannelData, Box<dyn std::error::Error + Sync + Send>> {
 | 
					 | 
				
			||||||
        let channel = self.channel_id().to_channel_cached(&self.discord()).unwrap();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        ChannelData::from_channel(&channel, &self.data().database).await
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,310 +0,0 @@
 | 
				
			|||||||
use std::{collections::HashSet, fmt::Display};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use chrono::{Duration, NaiveDateTime, Utc};
 | 
					 | 
				
			||||||
use chrono_tz::Tz;
 | 
					 | 
				
			||||||
use poise::serenity::{
 | 
					 | 
				
			||||||
    http::CacheHttp,
 | 
					 | 
				
			||||||
    model::{
 | 
					 | 
				
			||||||
        channel::GuildChannel,
 | 
					 | 
				
			||||||
        id::{ChannelId, GuildId, UserId},
 | 
					 | 
				
			||||||
        webhook::Webhook,
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    Result as SerenityResult,
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
use sqlx::MySqlPool;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use crate::{
 | 
					 | 
				
			||||||
    consts::{DEFAULT_AVATAR, MAX_TIME, MIN_INTERVAL},
 | 
					 | 
				
			||||||
    models::{
 | 
					 | 
				
			||||||
        channel_data::ChannelData,
 | 
					 | 
				
			||||||
        reminder::{content::Content, errors::ReminderError, helper::generate_uid, Reminder},
 | 
					 | 
				
			||||||
        user_data::UserData,
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    Context,
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
async fn create_webhook(
 | 
					 | 
				
			||||||
    ctx: impl CacheHttp,
 | 
					 | 
				
			||||||
    channel: GuildChannel,
 | 
					 | 
				
			||||||
    name: impl Display,
 | 
					 | 
				
			||||||
) -> SerenityResult<Webhook> {
 | 
					 | 
				
			||||||
    channel.create_webhook_with_avatar(ctx.http(), name, DEFAULT_AVATAR.clone()).await
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Hash, PartialEq, Eq)]
 | 
					 | 
				
			||||||
pub enum ReminderScope {
 | 
					 | 
				
			||||||
    User(u64),
 | 
					 | 
				
			||||||
    Channel(u64),
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl ReminderScope {
 | 
					 | 
				
			||||||
    pub fn mention(&self) -> String {
 | 
					 | 
				
			||||||
        match self {
 | 
					 | 
				
			||||||
            Self::User(id) => format!("<@{}>", id),
 | 
					 | 
				
			||||||
            Self::Channel(id) => format!("<#{}>", id),
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
pub struct ReminderBuilder {
 | 
					 | 
				
			||||||
    pool: MySqlPool,
 | 
					 | 
				
			||||||
    uid: String,
 | 
					 | 
				
			||||||
    channel: u32,
 | 
					 | 
				
			||||||
    utc_time: NaiveDateTime,
 | 
					 | 
				
			||||||
    timezone: String,
 | 
					 | 
				
			||||||
    interval: Option<i64>,
 | 
					 | 
				
			||||||
    expires: Option<NaiveDateTime>,
 | 
					 | 
				
			||||||
    content: String,
 | 
					 | 
				
			||||||
    tts: bool,
 | 
					 | 
				
			||||||
    attachment_name: Option<String>,
 | 
					 | 
				
			||||||
    attachment: Option<Vec<u8>>,
 | 
					 | 
				
			||||||
    set_by: Option<u32>,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl ReminderBuilder {
 | 
					 | 
				
			||||||
    pub async fn build(self) -> Result<Reminder, ReminderError> {
 | 
					 | 
				
			||||||
        let queried_time = sqlx::query!(
 | 
					 | 
				
			||||||
            "SELECT DATE_ADD(?, INTERVAL (SELECT nudge FROM channels WHERE id = ?) SECOND) AS `utc_time`",
 | 
					 | 
				
			||||||
            self.utc_time,
 | 
					 | 
				
			||||||
            self.channel,
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        .fetch_one(&self.pool)
 | 
					 | 
				
			||||||
        .await
 | 
					 | 
				
			||||||
        .unwrap();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        match queried_time.utc_time {
 | 
					 | 
				
			||||||
            Some(utc_time) => {
 | 
					 | 
				
			||||||
                if utc_time < (Utc::now() - Duration::seconds(60)).naive_local() {
 | 
					 | 
				
			||||||
                    Err(ReminderError::PastTime)
 | 
					 | 
				
			||||||
                } else {
 | 
					 | 
				
			||||||
                    sqlx::query!(
 | 
					 | 
				
			||||||
                        "
 | 
					 | 
				
			||||||
INSERT INTO reminders (
 | 
					 | 
				
			||||||
    `uid`,
 | 
					 | 
				
			||||||
    `channel_id`,
 | 
					 | 
				
			||||||
    `utc_time`,
 | 
					 | 
				
			||||||
    `timezone`,
 | 
					 | 
				
			||||||
    `interval`,
 | 
					 | 
				
			||||||
    `expires`,
 | 
					 | 
				
			||||||
    `content`,
 | 
					 | 
				
			||||||
    `tts`,
 | 
					 | 
				
			||||||
    `attachment_name`,
 | 
					 | 
				
			||||||
    `attachment`,
 | 
					 | 
				
			||||||
    `set_by`
 | 
					 | 
				
			||||||
) VALUES (
 | 
					 | 
				
			||||||
    ?,
 | 
					 | 
				
			||||||
    ?,
 | 
					 | 
				
			||||||
    ?,
 | 
					 | 
				
			||||||
    ?,
 | 
					 | 
				
			||||||
    ?,
 | 
					 | 
				
			||||||
    ?,
 | 
					 | 
				
			||||||
    ?,
 | 
					 | 
				
			||||||
    ?,
 | 
					 | 
				
			||||||
    ?,
 | 
					 | 
				
			||||||
    ?,
 | 
					 | 
				
			||||||
    ?
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
            ",
 | 
					 | 
				
			||||||
                        self.uid,
 | 
					 | 
				
			||||||
                        self.channel,
 | 
					 | 
				
			||||||
                        utc_time,
 | 
					 | 
				
			||||||
                        self.timezone,
 | 
					 | 
				
			||||||
                        self.interval,
 | 
					 | 
				
			||||||
                        self.expires,
 | 
					 | 
				
			||||||
                        self.content,
 | 
					 | 
				
			||||||
                        self.tts,
 | 
					 | 
				
			||||||
                        self.attachment_name,
 | 
					 | 
				
			||||||
                        self.attachment,
 | 
					 | 
				
			||||||
                        self.set_by
 | 
					 | 
				
			||||||
                    )
 | 
					 | 
				
			||||||
                    .execute(&self.pool)
 | 
					 | 
				
			||||||
                    .await
 | 
					 | 
				
			||||||
                    .unwrap();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    Ok(Reminder::from_uid(&self.pool, self.uid).await.unwrap())
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            None => Err(ReminderError::LongTime),
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
pub struct MultiReminderBuilder<'a> {
 | 
					 | 
				
			||||||
    scopes: Vec<ReminderScope>,
 | 
					 | 
				
			||||||
    utc_time: NaiveDateTime,
 | 
					 | 
				
			||||||
    timezone: Tz,
 | 
					 | 
				
			||||||
    interval: Option<i64>,
 | 
					 | 
				
			||||||
    expires: Option<NaiveDateTime>,
 | 
					 | 
				
			||||||
    content: Content,
 | 
					 | 
				
			||||||
    set_by: Option<u32>,
 | 
					 | 
				
			||||||
    ctx: &'a Context<'a>,
 | 
					 | 
				
			||||||
    guild_id: Option<GuildId>,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl<'a> MultiReminderBuilder<'a> {
 | 
					 | 
				
			||||||
    pub fn new(ctx: &'a Context<'a>, guild_id: Option<GuildId>) -> Self {
 | 
					 | 
				
			||||||
        MultiReminderBuilder {
 | 
					 | 
				
			||||||
            scopes: vec![],
 | 
					 | 
				
			||||||
            utc_time: Utc::now().naive_utc(),
 | 
					 | 
				
			||||||
            timezone: Tz::UTC,
 | 
					 | 
				
			||||||
            interval: None,
 | 
					 | 
				
			||||||
            expires: None,
 | 
					 | 
				
			||||||
            content: Content::new(),
 | 
					 | 
				
			||||||
            set_by: None,
 | 
					 | 
				
			||||||
            ctx,
 | 
					 | 
				
			||||||
            guild_id,
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    pub fn content(mut self, content: Content) -> Self {
 | 
					 | 
				
			||||||
        self.content = content;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        self
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    pub fn time<T: Into<i64>>(mut self, time: T) -> Self {
 | 
					 | 
				
			||||||
        self.utc_time = NaiveDateTime::from_timestamp(time.into(), 0);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        self
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    pub fn expires<T: Into<i64>>(mut self, time: Option<T>) -> Self {
 | 
					 | 
				
			||||||
        if let Some(t) = time {
 | 
					 | 
				
			||||||
            self.expires = Some(NaiveDateTime::from_timestamp(t.into(), 0));
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
            self.expires = None;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        self
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    pub fn author(mut self, user: UserData) -> Self {
 | 
					 | 
				
			||||||
        self.set_by = Some(user.id);
 | 
					 | 
				
			||||||
        self.timezone = user.timezone();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        self
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    pub fn interval(mut self, interval: Option<i64>) -> Self {
 | 
					 | 
				
			||||||
        self.interval = interval;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        self
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    pub fn set_scopes(&mut self, scopes: Vec<ReminderScope>) {
 | 
					 | 
				
			||||||
        self.scopes = scopes;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    pub async fn build(self) -> (HashSet<ReminderError>, HashSet<ReminderScope>) {
 | 
					 | 
				
			||||||
        let pool = self.ctx.data().database.clone();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        let mut errors = HashSet::new();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        let mut ok_locs = HashSet::new();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if self.interval.map_or(false, |i| (i as i64) < *MIN_INTERVAL) {
 | 
					 | 
				
			||||||
            errors.insert(ReminderError::ShortInterval);
 | 
					 | 
				
			||||||
        } else if self.interval.map_or(false, |i| (i as i64) > *MAX_TIME) {
 | 
					 | 
				
			||||||
            errors.insert(ReminderError::LongInterval);
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
            for scope in self.scopes {
 | 
					 | 
				
			||||||
                let db_channel_id = match scope {
 | 
					 | 
				
			||||||
                    ReminderScope::User(user_id) => {
 | 
					 | 
				
			||||||
                        if let Ok(user) = UserId(user_id).to_user(&self.ctx.discord()).await {
 | 
					 | 
				
			||||||
                            let user_data = UserData::from_user(&user, &self.ctx.discord(), &pool)
 | 
					 | 
				
			||||||
                                .await
 | 
					 | 
				
			||||||
                                .unwrap();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                            if let Some(guild_id) = self.guild_id {
 | 
					 | 
				
			||||||
                                if guild_id.member(&self.ctx.discord(), user).await.is_err() {
 | 
					 | 
				
			||||||
                                    Err(ReminderError::InvalidTag)
 | 
					 | 
				
			||||||
                                } else {
 | 
					 | 
				
			||||||
                                    Ok(user_data.dm_channel)
 | 
					 | 
				
			||||||
                                }
 | 
					 | 
				
			||||||
                            } else {
 | 
					 | 
				
			||||||
                                Ok(user_data.dm_channel)
 | 
					 | 
				
			||||||
                            }
 | 
					 | 
				
			||||||
                        } else {
 | 
					 | 
				
			||||||
                            Err(ReminderError::InvalidTag)
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                    ReminderScope::Channel(channel_id) => {
 | 
					 | 
				
			||||||
                        let channel =
 | 
					 | 
				
			||||||
                            ChannelId(channel_id).to_channel(&self.ctx.discord()).await.unwrap();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                        if let Some(guild_channel) = channel.clone().guild() {
 | 
					 | 
				
			||||||
                            if Some(guild_channel.guild_id) != self.guild_id {
 | 
					 | 
				
			||||||
                                Err(ReminderError::InvalidTag)
 | 
					 | 
				
			||||||
                            } else {
 | 
					 | 
				
			||||||
                                let mut channel_data =
 | 
					 | 
				
			||||||
                                    ChannelData::from_channel(&channel, &pool).await.unwrap();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                                if channel_data.webhook_id.is_none()
 | 
					 | 
				
			||||||
                                    || channel_data.webhook_token.is_none()
 | 
					 | 
				
			||||||
                                {
 | 
					 | 
				
			||||||
                                    match create_webhook(
 | 
					 | 
				
			||||||
                                        &self.ctx.discord(),
 | 
					 | 
				
			||||||
                                        guild_channel,
 | 
					 | 
				
			||||||
                                        "Reminder",
 | 
					 | 
				
			||||||
                                    )
 | 
					 | 
				
			||||||
                                    .await
 | 
					 | 
				
			||||||
                                    {
 | 
					 | 
				
			||||||
                                        Ok(webhook) => {
 | 
					 | 
				
			||||||
                                            channel_data.webhook_id =
 | 
					 | 
				
			||||||
                                                Some(webhook.id.as_u64().to_owned());
 | 
					 | 
				
			||||||
                                            channel_data.webhook_token = webhook.token;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                                            channel_data.commit_changes(&pool).await;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                                            Ok(channel_data.id)
 | 
					 | 
				
			||||||
                                        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                                        Err(e) => Err(ReminderError::DiscordError(e.to_string())),
 | 
					 | 
				
			||||||
                                    }
 | 
					 | 
				
			||||||
                                } else {
 | 
					 | 
				
			||||||
                                    Ok(channel_data.id)
 | 
					 | 
				
			||||||
                                }
 | 
					 | 
				
			||||||
                            }
 | 
					 | 
				
			||||||
                        } else {
 | 
					 | 
				
			||||||
                            Err(ReminderError::InvalidTag)
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                match db_channel_id {
 | 
					 | 
				
			||||||
                    Ok(c) => {
 | 
					 | 
				
			||||||
                        let builder = ReminderBuilder {
 | 
					 | 
				
			||||||
                            pool: pool.clone(),
 | 
					 | 
				
			||||||
                            uid: generate_uid(),
 | 
					 | 
				
			||||||
                            channel: c,
 | 
					 | 
				
			||||||
                            utc_time: self.utc_time,
 | 
					 | 
				
			||||||
                            timezone: self.timezone.to_string(),
 | 
					 | 
				
			||||||
                            interval: self.interval,
 | 
					 | 
				
			||||||
                            expires: self.expires,
 | 
					 | 
				
			||||||
                            content: self.content.content.clone(),
 | 
					 | 
				
			||||||
                            tts: self.content.tts,
 | 
					 | 
				
			||||||
                            attachment_name: self.content.attachment_name.clone(),
 | 
					 | 
				
			||||||
                            attachment: self.content.attachment.clone(),
 | 
					 | 
				
			||||||
                            set_by: self.set_by,
 | 
					 | 
				
			||||||
                        };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                        match builder.build().await {
 | 
					 | 
				
			||||||
                            Ok(_) => {
 | 
					 | 
				
			||||||
                                ok_locs.insert(scope);
 | 
					 | 
				
			||||||
                            }
 | 
					 | 
				
			||||||
                            Err(e) => {
 | 
					 | 
				
			||||||
                                errors.insert(e);
 | 
					 | 
				
			||||||
                            }
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                    Err(e) => {
 | 
					 | 
				
			||||||
                        errors.insert(e);
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        (errors, ok_locs)
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,12 +0,0 @@
 | 
				
			|||||||
pub struct Content {
 | 
					 | 
				
			||||||
    pub content: String,
 | 
					 | 
				
			||||||
    pub tts: bool,
 | 
					 | 
				
			||||||
    pub attachment: Option<Vec<u8>>,
 | 
					 | 
				
			||||||
    pub attachment_name: Option<String>,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl Content {
 | 
					 | 
				
			||||||
    pub fn new() -> Self {
 | 
					 | 
				
			||||||
        Self { content: "".to_string(), tts: false, attachment: None, attachment_name: None }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,36 +0,0 @@
 | 
				
			|||||||
use crate::consts::{MAX_TIME, MIN_INTERVAL};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(PartialEq, Eq, Hash, Debug)]
 | 
					 | 
				
			||||||
pub enum ReminderError {
 | 
					 | 
				
			||||||
    LongTime,
 | 
					 | 
				
			||||||
    LongInterval,
 | 
					 | 
				
			||||||
    PastTime,
 | 
					 | 
				
			||||||
    ShortInterval,
 | 
					 | 
				
			||||||
    InvalidTag,
 | 
					 | 
				
			||||||
    DiscordError(String),
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl ToString for ReminderError {
 | 
					 | 
				
			||||||
    fn to_string(&self) -> String {
 | 
					 | 
				
			||||||
        match self {
 | 
					 | 
				
			||||||
            ReminderError::LongTime => {
 | 
					 | 
				
			||||||
                "That time is too far in the future. Please specify a shorter time.".to_string()
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            ReminderError::LongInterval => format!(
 | 
					 | 
				
			||||||
                "Please ensure the interval specified is less than {max_time} days",
 | 
					 | 
				
			||||||
                max_time = *MAX_TIME / 86_400
 | 
					 | 
				
			||||||
            ),
 | 
					 | 
				
			||||||
            ReminderError::PastTime => {
 | 
					 | 
				
			||||||
                "Please ensure the time provided is in the future. If the time should be in the future, please be more specific with the definition.".to_string()
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            ReminderError::ShortInterval => format!(
 | 
					 | 
				
			||||||
                "Please ensure the interval provided is longer than {min_interval} seconds",
 | 
					 | 
				
			||||||
                min_interval = *MIN_INTERVAL
 | 
					 | 
				
			||||||
            ),
 | 
					 | 
				
			||||||
            ReminderError::InvalidTag => {
 | 
					 | 
				
			||||||
                "Couldn't find a location by your tag. Your tag must be either a channel or a user (not a role)".to_string()
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            ReminderError::DiscordError(s) => format!("A Discord error occurred: **{}**", s),
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,31 +0,0 @@
 | 
				
			|||||||
use num_integer::Integer;
 | 
					 | 
				
			||||||
use rand::{rngs::OsRng, seq::IteratorRandom};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use crate::consts::{CHARACTERS, DAY, HOUR, MINUTE};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
pub fn longhand_displacement(seconds: u64) -> String {
 | 
					 | 
				
			||||||
    let (days, seconds) = seconds.div_rem(&DAY);
 | 
					 | 
				
			||||||
    let (hours, seconds) = seconds.div_rem(&HOUR);
 | 
					 | 
				
			||||||
    let (minutes, seconds) = seconds.div_rem(&MINUTE);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let mut sections = vec![];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    for (var, name) in
 | 
					 | 
				
			||||||
        [days, hours, minutes, seconds].iter().zip(["days", "hours", "minutes", "seconds"].iter())
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        if *var > 0 {
 | 
					 | 
				
			||||||
            sections.push(format!("{} {}", var, name));
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    sections.join(", ")
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
pub fn generate_uid() -> String {
 | 
					 | 
				
			||||||
    let mut generator: OsRng = Default::default();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    (0..64)
 | 
					 | 
				
			||||||
        .map(|_| CHARACTERS.chars().choose(&mut generator).unwrap().to_owned().to_string())
 | 
					 | 
				
			||||||
        .collect::<Vec<String>>()
 | 
					 | 
				
			||||||
        .join("")
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,23 +0,0 @@
 | 
				
			|||||||
use poise::serenity::model::id::ChannelId;
 | 
					 | 
				
			||||||
use serde::{Deserialize, Serialize};
 | 
					 | 
				
			||||||
use serde_repr::*;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Serialize_repr, Deserialize_repr, Copy, Clone, Debug)]
 | 
					 | 
				
			||||||
#[repr(u8)]
 | 
					 | 
				
			||||||
pub enum TimeDisplayType {
 | 
					 | 
				
			||||||
    Absolute = 0,
 | 
					 | 
				
			||||||
    Relative = 1,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Serialize, Deserialize, Copy, Clone, Debug)]
 | 
					 | 
				
			||||||
pub struct LookFlags {
 | 
					 | 
				
			||||||
    pub show_disabled: bool,
 | 
					 | 
				
			||||||
    pub channel_id: Option<ChannelId>,
 | 
					 | 
				
			||||||
    pub time_display: TimeDisplayType,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl Default for LookFlags {
 | 
					 | 
				
			||||||
    fn default() -> Self {
 | 
					 | 
				
			||||||
        Self { show_disabled: true, channel_id: None, time_display: TimeDisplayType::Relative }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,284 +0,0 @@
 | 
				
			|||||||
pub mod builder;
 | 
					 | 
				
			||||||
pub mod content;
 | 
					 | 
				
			||||||
pub mod errors;
 | 
					 | 
				
			||||||
mod helper;
 | 
					 | 
				
			||||||
pub mod look_flags;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use chrono::{NaiveDateTime, TimeZone};
 | 
					 | 
				
			||||||
use chrono_tz::Tz;
 | 
					 | 
				
			||||||
use poise::serenity::model::id::{ChannelId, GuildId, UserId};
 | 
					 | 
				
			||||||
use sqlx::{Executor, MySqlPool};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use crate::{
 | 
					 | 
				
			||||||
    models::reminder::{
 | 
					 | 
				
			||||||
        helper::longhand_displacement,
 | 
					 | 
				
			||||||
        look_flags::{LookFlags, TimeDisplayType},
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    Context, Database,
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Debug, Clone)]
 | 
					 | 
				
			||||||
pub struct Reminder {
 | 
					 | 
				
			||||||
    pub id: u32,
 | 
					 | 
				
			||||||
    pub uid: String,
 | 
					 | 
				
			||||||
    pub channel: u64,
 | 
					 | 
				
			||||||
    pub utc_time: NaiveDateTime,
 | 
					 | 
				
			||||||
    pub interval: Option<u32>,
 | 
					 | 
				
			||||||
    pub expires: Option<NaiveDateTime>,
 | 
					 | 
				
			||||||
    pub enabled: bool,
 | 
					 | 
				
			||||||
    pub content: String,
 | 
					 | 
				
			||||||
    pub embed_description: String,
 | 
					 | 
				
			||||||
    pub set_by: Option<u64>,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl Reminder {
 | 
					 | 
				
			||||||
    pub async fn from_uid(pool: &MySqlPool, uid: String) -> Option<Self> {
 | 
					 | 
				
			||||||
        sqlx::query_as_unchecked!(
 | 
					 | 
				
			||||||
            Self,
 | 
					 | 
				
			||||||
            "
 | 
					 | 
				
			||||||
SELECT
 | 
					 | 
				
			||||||
    reminders.id,
 | 
					 | 
				
			||||||
    reminders.uid,
 | 
					 | 
				
			||||||
    channels.channel,
 | 
					 | 
				
			||||||
    reminders.utc_time,
 | 
					 | 
				
			||||||
    reminders.interval,
 | 
					 | 
				
			||||||
    reminders.expires,
 | 
					 | 
				
			||||||
    reminders.enabled,
 | 
					 | 
				
			||||||
    reminders.content,
 | 
					 | 
				
			||||||
    reminders.embed_description,
 | 
					 | 
				
			||||||
    users.user AS set_by
 | 
					 | 
				
			||||||
FROM
 | 
					 | 
				
			||||||
    reminders
 | 
					 | 
				
			||||||
INNER JOIN
 | 
					 | 
				
			||||||
    channels
 | 
					 | 
				
			||||||
ON
 | 
					 | 
				
			||||||
    reminders.channel_id = channels.id
 | 
					 | 
				
			||||||
LEFT JOIN
 | 
					 | 
				
			||||||
    users
 | 
					 | 
				
			||||||
ON
 | 
					 | 
				
			||||||
    reminders.set_by = users.id
 | 
					 | 
				
			||||||
WHERE
 | 
					 | 
				
			||||||
    reminders.uid = ?
 | 
					 | 
				
			||||||
            ",
 | 
					 | 
				
			||||||
            uid
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        .fetch_one(pool)
 | 
					 | 
				
			||||||
        .await
 | 
					 | 
				
			||||||
        .ok()
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    pub async fn from_channel<C: Into<ChannelId>>(
 | 
					 | 
				
			||||||
        db_pool: impl Executor<'_, Database = Database>,
 | 
					 | 
				
			||||||
        channel_id: C,
 | 
					 | 
				
			||||||
        flags: &LookFlags,
 | 
					 | 
				
			||||||
    ) -> Vec<Self> {
 | 
					 | 
				
			||||||
        let enabled = if flags.show_disabled { "0,1" } else { "1" };
 | 
					 | 
				
			||||||
        let channel_id = channel_id.into();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        sqlx::query_as_unchecked!(
 | 
					 | 
				
			||||||
            Self,
 | 
					 | 
				
			||||||
            "
 | 
					 | 
				
			||||||
SELECT
 | 
					 | 
				
			||||||
    reminders.id,
 | 
					 | 
				
			||||||
    reminders.uid,
 | 
					 | 
				
			||||||
    channels.channel,
 | 
					 | 
				
			||||||
    reminders.utc_time,
 | 
					 | 
				
			||||||
    reminders.interval,
 | 
					 | 
				
			||||||
    reminders.expires,
 | 
					 | 
				
			||||||
    reminders.enabled,
 | 
					 | 
				
			||||||
    reminders.content,
 | 
					 | 
				
			||||||
    reminders.embed_description,
 | 
					 | 
				
			||||||
    users.user AS set_by
 | 
					 | 
				
			||||||
FROM
 | 
					 | 
				
			||||||
    reminders
 | 
					 | 
				
			||||||
INNER JOIN
 | 
					 | 
				
			||||||
    channels
 | 
					 | 
				
			||||||
ON
 | 
					 | 
				
			||||||
    reminders.channel_id = channels.id
 | 
					 | 
				
			||||||
LEFT JOIN
 | 
					 | 
				
			||||||
    users
 | 
					 | 
				
			||||||
ON
 | 
					 | 
				
			||||||
    reminders.set_by = users.id
 | 
					 | 
				
			||||||
WHERE
 | 
					 | 
				
			||||||
    channels.channel = ? AND
 | 
					 | 
				
			||||||
    FIND_IN_SET(reminders.enabled, ?)
 | 
					 | 
				
			||||||
ORDER BY
 | 
					 | 
				
			||||||
    reminders.utc_time
 | 
					 | 
				
			||||||
            ",
 | 
					 | 
				
			||||||
            channel_id.as_u64(),
 | 
					 | 
				
			||||||
            enabled,
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        .fetch_all(db_pool)
 | 
					 | 
				
			||||||
        .await
 | 
					 | 
				
			||||||
        .unwrap()
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    pub async fn from_guild(
 | 
					 | 
				
			||||||
        ctx: &Context<'_>,
 | 
					 | 
				
			||||||
        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 {
 | 
					 | 
				
			||||||
            let guild_opt = guild_id.to_guild_cached(&ctx.discord());
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if let Some(guild) = guild_opt {
 | 
					 | 
				
			||||||
                let channels = guild
 | 
					 | 
				
			||||||
                    .channels
 | 
					 | 
				
			||||||
                    .keys()
 | 
					 | 
				
			||||||
                    .into_iter()
 | 
					 | 
				
			||||||
                    .map(|k| k.as_u64().to_string())
 | 
					 | 
				
			||||||
                    .collect::<Vec<String>>()
 | 
					 | 
				
			||||||
                    .join(",");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                sqlx::query_as_unchecked!(
 | 
					 | 
				
			||||||
                    Self,
 | 
					 | 
				
			||||||
                    "
 | 
					 | 
				
			||||||
SELECT
 | 
					 | 
				
			||||||
    reminders.id,
 | 
					 | 
				
			||||||
    reminders.uid,
 | 
					 | 
				
			||||||
    channels.channel,
 | 
					 | 
				
			||||||
    reminders.utc_time,
 | 
					 | 
				
			||||||
    reminders.interval,
 | 
					 | 
				
			||||||
    reminders.expires,
 | 
					 | 
				
			||||||
    reminders.enabled,
 | 
					 | 
				
			||||||
    reminders.content,
 | 
					 | 
				
			||||||
    reminders.embed_description,
 | 
					 | 
				
			||||||
    users.user AS set_by
 | 
					 | 
				
			||||||
FROM
 | 
					 | 
				
			||||||
    reminders
 | 
					 | 
				
			||||||
LEFT JOIN
 | 
					 | 
				
			||||||
    channels
 | 
					 | 
				
			||||||
ON
 | 
					 | 
				
			||||||
    channels.id = reminders.channel_id
 | 
					 | 
				
			||||||
LEFT JOIN
 | 
					 | 
				
			||||||
    users
 | 
					 | 
				
			||||||
ON
 | 
					 | 
				
			||||||
    reminders.set_by = users.id
 | 
					 | 
				
			||||||
WHERE
 | 
					 | 
				
			||||||
    FIND_IN_SET(channels.channel, ?)
 | 
					 | 
				
			||||||
                ",
 | 
					 | 
				
			||||||
                    channels
 | 
					 | 
				
			||||||
                )
 | 
					 | 
				
			||||||
                .fetch_all(&pool)
 | 
					 | 
				
			||||||
                .await
 | 
					 | 
				
			||||||
            } else {
 | 
					 | 
				
			||||||
                sqlx::query_as_unchecked!(
 | 
					 | 
				
			||||||
                    Self,
 | 
					 | 
				
			||||||
                    "
 | 
					 | 
				
			||||||
SELECT
 | 
					 | 
				
			||||||
    reminders.id,
 | 
					 | 
				
			||||||
    reminders.uid,
 | 
					 | 
				
			||||||
    channels.channel,
 | 
					 | 
				
			||||||
    reminders.utc_time,
 | 
					 | 
				
			||||||
    reminders.interval,
 | 
					 | 
				
			||||||
    reminders.expires,
 | 
					 | 
				
			||||||
    reminders.enabled,
 | 
					 | 
				
			||||||
    reminders.content,
 | 
					 | 
				
			||||||
    reminders.embed_description,
 | 
					 | 
				
			||||||
    users.user AS set_by
 | 
					 | 
				
			||||||
FROM
 | 
					 | 
				
			||||||
    reminders
 | 
					 | 
				
			||||||
LEFT JOIN
 | 
					 | 
				
			||||||
    channels
 | 
					 | 
				
			||||||
ON
 | 
					 | 
				
			||||||
    channels.id = reminders.channel_id
 | 
					 | 
				
			||||||
LEFT JOIN
 | 
					 | 
				
			||||||
    users
 | 
					 | 
				
			||||||
ON
 | 
					 | 
				
			||||||
    reminders.set_by = users.id
 | 
					 | 
				
			||||||
WHERE
 | 
					 | 
				
			||||||
    channels.guild_id = (SELECT id FROM guilds WHERE guild = ?)
 | 
					 | 
				
			||||||
                ",
 | 
					 | 
				
			||||||
                    guild_id.as_u64()
 | 
					 | 
				
			||||||
                )
 | 
					 | 
				
			||||||
                .fetch_all(&pool)
 | 
					 | 
				
			||||||
                .await
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
            sqlx::query_as_unchecked!(
 | 
					 | 
				
			||||||
                Self,
 | 
					 | 
				
			||||||
                "
 | 
					 | 
				
			||||||
SELECT
 | 
					 | 
				
			||||||
    reminders.id,
 | 
					 | 
				
			||||||
    reminders.uid,
 | 
					 | 
				
			||||||
    channels.channel,
 | 
					 | 
				
			||||||
    reminders.utc_time,
 | 
					 | 
				
			||||||
    reminders.interval,
 | 
					 | 
				
			||||||
    reminders.expires,
 | 
					 | 
				
			||||||
    reminders.enabled,
 | 
					 | 
				
			||||||
    reminders.content,
 | 
					 | 
				
			||||||
    reminders.embed_description,
 | 
					 | 
				
			||||||
    users.user AS set_by
 | 
					 | 
				
			||||||
FROM
 | 
					 | 
				
			||||||
    reminders
 | 
					 | 
				
			||||||
INNER JOIN
 | 
					 | 
				
			||||||
    channels
 | 
					 | 
				
			||||||
ON
 | 
					 | 
				
			||||||
    channels.id = reminders.channel_id
 | 
					 | 
				
			||||||
LEFT JOIN
 | 
					 | 
				
			||||||
    users
 | 
					 | 
				
			||||||
ON
 | 
					 | 
				
			||||||
    reminders.set_by = users.id
 | 
					 | 
				
			||||||
WHERE
 | 
					 | 
				
			||||||
    channels.id = (SELECT dm_channel FROM users WHERE user = ?)
 | 
					 | 
				
			||||||
            ",
 | 
					 | 
				
			||||||
                user.as_u64()
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
            .fetch_all(&pool)
 | 
					 | 
				
			||||||
            .await
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        .unwrap()
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    pub fn display_content(&self) -> &str {
 | 
					 | 
				
			||||||
        if self.content.is_empty() {
 | 
					 | 
				
			||||||
            &self.embed_description
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
            &self.content
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    pub fn display_del(&self, count: usize, timezone: &Tz) -> String {
 | 
					 | 
				
			||||||
        format!(
 | 
					 | 
				
			||||||
            "**{}**: '{}' *<#{}>* at **{}**",
 | 
					 | 
				
			||||||
            count + 1,
 | 
					 | 
				
			||||||
            self.display_content(),
 | 
					 | 
				
			||||||
            self.channel,
 | 
					 | 
				
			||||||
            timezone
 | 
					 | 
				
			||||||
                .timestamp(self.utc_time.timestamp(), 0)
 | 
					 | 
				
			||||||
                .format("%Y-%m-%d %H:%M:%S")
 | 
					 | 
				
			||||||
                .to_string()
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    pub fn display(&self, flags: &LookFlags, timezone: &Tz) -> String {
 | 
					 | 
				
			||||||
        let time_display = match flags.time_display {
 | 
					 | 
				
			||||||
            TimeDisplayType::Absolute => timezone
 | 
					 | 
				
			||||||
                .timestamp(self.utc_time.timestamp(), 0)
 | 
					 | 
				
			||||||
                .format("%Y-%m-%d %H:%M:%S")
 | 
					 | 
				
			||||||
                .to_string(),
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            TimeDisplayType::Relative => format!("<t:{}:R>", self.utc_time.timestamp()),
 | 
					 | 
				
			||||||
        };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if let Some(interval) = self.interval {
 | 
					 | 
				
			||||||
            format!(
 | 
					 | 
				
			||||||
                "'{}' *occurs next at* **{}**, repeating every **{}** (set by {})",
 | 
					 | 
				
			||||||
                self.display_content(),
 | 
					 | 
				
			||||||
                time_display,
 | 
					 | 
				
			||||||
                longhand_displacement(interval as u64),
 | 
					 | 
				
			||||||
                self.set_by.map(|i| format!("<@{}>", i)).unwrap_or_else(|| "unknown".to_string())
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
            format!(
 | 
					 | 
				
			||||||
                "'{}' *occurs next at* **{}** (set by {})",
 | 
					 | 
				
			||||||
                self.display_content(),
 | 
					 | 
				
			||||||
                time_display,
 | 
					 | 
				
			||||||
                self.set_by.map(|i| format!("<@{}>", i)).unwrap_or_else(|| "unknown".to_string())
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,49 +0,0 @@
 | 
				
			|||||||
use chrono::NaiveDateTime;
 | 
					 | 
				
			||||||
use sqlx::MySqlPool;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
pub struct Timer {
 | 
					 | 
				
			||||||
    pub name: String,
 | 
					 | 
				
			||||||
    pub start_time: NaiveDateTime,
 | 
					 | 
				
			||||||
    pub owner: u64,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl Timer {
 | 
					 | 
				
			||||||
    pub async fn from_owner(owner: u64, pool: &MySqlPool) -> Vec<Self> {
 | 
					 | 
				
			||||||
        sqlx::query_as_unchecked!(
 | 
					 | 
				
			||||||
            Timer,
 | 
					 | 
				
			||||||
            "
 | 
					 | 
				
			||||||
SELECT name, start_time, owner FROM timers WHERE owner = ?
 | 
					 | 
				
			||||||
            ",
 | 
					 | 
				
			||||||
            owner
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        .fetch_all(pool)
 | 
					 | 
				
			||||||
        .await
 | 
					 | 
				
			||||||
        .unwrap()
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    pub async fn count_from_owner(owner: u64, pool: &MySqlPool) -> u32 {
 | 
					 | 
				
			||||||
        sqlx::query!(
 | 
					 | 
				
			||||||
            "
 | 
					 | 
				
			||||||
SELECT COUNT(1) as count FROM timers WHERE owner = ?
 | 
					 | 
				
			||||||
            ",
 | 
					 | 
				
			||||||
            owner
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        .fetch_one(pool)
 | 
					 | 
				
			||||||
        .await
 | 
					 | 
				
			||||||
        .unwrap()
 | 
					 | 
				
			||||||
        .count as u32
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    pub async fn create(name: &str, owner: u64, pool: &MySqlPool) {
 | 
					 | 
				
			||||||
        sqlx::query!(
 | 
					 | 
				
			||||||
            "
 | 
					 | 
				
			||||||
INSERT INTO timers (name, owner) VALUES (?, ?)
 | 
					 | 
				
			||||||
            ",
 | 
					 | 
				
			||||||
            name,
 | 
					 | 
				
			||||||
            owner
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        .execute(pool)
 | 
					 | 
				
			||||||
        .await
 | 
					 | 
				
			||||||
        .unwrap();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,118 +0,0 @@
 | 
				
			|||||||
use chrono_tz::Tz;
 | 
					 | 
				
			||||||
use log::error;
 | 
					 | 
				
			||||||
use poise::serenity::{http::CacheHttp, model::id::UserId};
 | 
					 | 
				
			||||||
use sqlx::MySqlPool;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use crate::consts::LOCAL_TIMEZONE;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
pub struct UserData {
 | 
					 | 
				
			||||||
    pub id: u32,
 | 
					 | 
				
			||||||
    pub user: u64,
 | 
					 | 
				
			||||||
    pub dm_channel: u32,
 | 
					 | 
				
			||||||
    pub timezone: String,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl UserData {
 | 
					 | 
				
			||||||
    pub async fn timezone_of<U>(user: U, pool: &MySqlPool) -> Tz
 | 
					 | 
				
			||||||
    where
 | 
					 | 
				
			||||||
        U: Into<UserId>,
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        let user_id = user.into().as_u64().to_owned();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        match sqlx::query!(
 | 
					 | 
				
			||||||
            "
 | 
					 | 
				
			||||||
SELECT timezone FROM users WHERE user = ?
 | 
					 | 
				
			||||||
            ",
 | 
					 | 
				
			||||||
            user_id
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        .fetch_one(pool)
 | 
					 | 
				
			||||||
        .await
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            Ok(r) => r.timezone,
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            Err(_) => LOCAL_TIMEZONE.clone(),
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        .parse()
 | 
					 | 
				
			||||||
        .unwrap()
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    pub async fn from_user<U: Into<UserId>>(
 | 
					 | 
				
			||||||
        user: U,
 | 
					 | 
				
			||||||
        ctx: impl CacheHttp,
 | 
					 | 
				
			||||||
        pool: &MySqlPool,
 | 
					 | 
				
			||||||
    ) -> Result<Self, Box<dyn std::error::Error + Sync + Send>> {
 | 
					 | 
				
			||||||
        let user_id = user.into();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        match sqlx::query_as_unchecked!(
 | 
					 | 
				
			||||||
            Self,
 | 
					 | 
				
			||||||
            "
 | 
					 | 
				
			||||||
SELECT id, user, dm_channel, IF(timezone IS NULL, ?, timezone) AS timezone FROM users WHERE user = ?
 | 
					 | 
				
			||||||
            ",
 | 
					 | 
				
			||||||
            *LOCAL_TIMEZONE,
 | 
					 | 
				
			||||||
            user_id.0
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        .fetch_one(pool)
 | 
					 | 
				
			||||||
        .await
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            Ok(c) => Ok(c),
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            Err(sqlx::Error::RowNotFound) => {
 | 
					 | 
				
			||||||
                let dm_channel = user_id.create_dm_channel(ctx).await?;
 | 
					 | 
				
			||||||
                let pool_c = pool.clone();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                sqlx::query!(
 | 
					 | 
				
			||||||
                    "
 | 
					 | 
				
			||||||
INSERT IGNORE INTO channels (channel) VALUES (?)
 | 
					 | 
				
			||||||
                    ",
 | 
					 | 
				
			||||||
                    dm_channel.id.0
 | 
					 | 
				
			||||||
                )
 | 
					 | 
				
			||||||
                .execute(&pool_c)
 | 
					 | 
				
			||||||
                .await?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                sqlx::query!(
 | 
					 | 
				
			||||||
                    "
 | 
					 | 
				
			||||||
INSERT INTO users (user, dm_channel, timezone) VALUES (?, (SELECT id FROM channels WHERE channel = ?), ?)
 | 
					 | 
				
			||||||
                    ",
 | 
					 | 
				
			||||||
                    user_id.0,
 | 
					 | 
				
			||||||
                    dm_channel.id.0,
 | 
					 | 
				
			||||||
                    *LOCAL_TIMEZONE
 | 
					 | 
				
			||||||
                )
 | 
					 | 
				
			||||||
                .execute(&pool_c)
 | 
					 | 
				
			||||||
                .await?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                Ok(sqlx::query_as_unchecked!(
 | 
					 | 
				
			||||||
                    Self,
 | 
					 | 
				
			||||||
                    "
 | 
					 | 
				
			||||||
SELECT id, user, dm_channel, timezone FROM users WHERE user = ?
 | 
					 | 
				
			||||||
                    ",
 | 
					 | 
				
			||||||
                    user_id.0
 | 
					 | 
				
			||||||
                )
 | 
					 | 
				
			||||||
                .fetch_one(pool)
 | 
					 | 
				
			||||||
                .await?)
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            Err(e) => {
 | 
					 | 
				
			||||||
                error!("Error querying for user: {:?}", e);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                Err(Box::new(e))
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    pub async fn commit_changes(&self, pool: &MySqlPool) {
 | 
					 | 
				
			||||||
        sqlx::query!(
 | 
					 | 
				
			||||||
            "
 | 
					 | 
				
			||||||
UPDATE users SET timezone = ? WHERE id = ?
 | 
					 | 
				
			||||||
            ",
 | 
					 | 
				
			||||||
            self.timezone,
 | 
					 | 
				
			||||||
            self.id
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        .execute(pool)
 | 
					 | 
				
			||||||
        .await
 | 
					 | 
				
			||||||
        .unwrap();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    pub fn timezone(&self) -> Tz {
 | 
					 | 
				
			||||||
        self.timezone.parse().unwrap()
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,16 +1,15 @@
 | 
				
			|||||||
use std::{
 | 
					use std::time::{SystemTime, UNIX_EPOCH};
 | 
				
			||||||
    convert::TryFrom,
 | 
					
 | 
				
			||||||
    fmt::{Display, Formatter, Result as FmtResult},
 | 
					use std::fmt::{Display, Formatter, Result as FmtResult};
 | 
				
			||||||
    str::from_utf8,
 | 
					
 | 
				
			||||||
    time::{SystemTime, UNIX_EPOCH},
 | 
					use crate::consts::{LOCAL_TIMEZONE, PYTHON_LOCATION};
 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
use chrono::{DateTime, Datelike, Timelike, Utc};
 | 
					use chrono::{DateTime, Datelike, Timelike, Utc};
 | 
				
			||||||
use chrono_tz::Tz;
 | 
					use chrono_tz::Tz;
 | 
				
			||||||
 | 
					use std::convert::TryFrom;
 | 
				
			||||||
 | 
					use std::str::from_utf8;
 | 
				
			||||||
use tokio::process::Command;
 | 
					use tokio::process::Command;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::consts::{LOCAL_TIMEZONE, PYTHON_LOCATION};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Debug)]
 | 
					#[derive(Debug)]
 | 
				
			||||||
pub enum InvalidTime {
 | 
					pub enum InvalidTime {
 | 
				
			||||||
    ParseErrorDMY,
 | 
					    ParseErrorDMY,
 | 
				
			||||||
@@ -27,13 +26,11 @@ impl Display for InvalidTime {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
impl std::error::Error for InvalidTime {}
 | 
					impl std::error::Error for InvalidTime {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[derive(Copy, Clone)]
 | 
					 | 
				
			||||||
enum ParseType {
 | 
					enum ParseType {
 | 
				
			||||||
    Explicit,
 | 
					    Explicit,
 | 
				
			||||||
    Displacement,
 | 
					    Displacement,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[derive(Clone)]
 | 
					 | 
				
			||||||
pub struct TimeParser {
 | 
					pub struct TimeParser {
 | 
				
			||||||
    timezone: Tz,
 | 
					    timezone: Tz,
 | 
				
			||||||
    inverted: bool,
 | 
					    inverted: bool,
 | 
				
			||||||
@@ -98,7 +95,10 @@ impl TimeParser {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    fn process_explicit(&self) -> Result<i64, InvalidTime> {
 | 
					    fn process_explicit(&self) -> Result<i64, InvalidTime> {
 | 
				
			||||||
        let mut time = Utc::now().with_timezone(&self.timezone).with_second(0).unwrap();
 | 
					        let mut time = Utc::now()
 | 
				
			||||||
 | 
					            .with_timezone(&self.timezone)
 | 
				
			||||||
 | 
					            .with_second(0)
 | 
				
			||||||
 | 
					            .unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        let mut segments = self.time_string.rsplit('-');
 | 
					        let mut segments = self.time_string.rsplit('-');
 | 
				
			||||||
        // this segment will always exist even if split fails
 | 
					        // this segment will always exist even if split fails
 | 
				
			||||||
@@ -106,11 +106,13 @@ impl TimeParser {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        let h_m_s = hms.split(':');
 | 
					        let h_m_s = hms.split(':');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        for (t, setter) in
 | 
					        for (t, setter) in h_m_s.take(3).zip(&[
 | 
				
			||||||
            h_m_s.take(3).zip(&[DateTime::with_hour, DateTime::with_minute, DateTime::with_second])
 | 
					            DateTime::with_hour,
 | 
				
			||||||
        {
 | 
					            DateTime::with_minute,
 | 
				
			||||||
 | 
					            DateTime::with_second,
 | 
				
			||||||
 | 
					        ]) {
 | 
				
			||||||
            time = setter(&time, t.parse().map_err(|_| InvalidTime::ParseErrorHMS)?)
 | 
					            time = setter(&time, t.parse().map_err(|_| InvalidTime::ParseErrorHMS)?)
 | 
				
			||||||
                .map_or_else(|| Err(InvalidTime::ParseErrorHMS), Ok)?;
 | 
					                .map_or_else(|| Err(InvalidTime::ParseErrorHMS), |inner| Ok(inner))?;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if let Some(dmy) = segments.next() {
 | 
					        if let Some(dmy) = segments.next() {
 | 
				
			||||||
@@ -120,11 +122,13 @@ impl TimeParser {
 | 
				
			|||||||
            let month = d_m_y.next();
 | 
					            let month = d_m_y.next();
 | 
				
			||||||
            let year = d_m_y.next();
 | 
					            let year = d_m_y.next();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            for (t, setter) in [day, month].iter().zip(&[DateTime::with_day, DateTime::with_month])
 | 
					            for (t, setter) in [day, month]
 | 
				
			||||||
 | 
					                .iter()
 | 
				
			||||||
 | 
					                .zip(&[DateTime::with_day, DateTime::with_month])
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                if let Some(t) = t {
 | 
					                if let Some(t) = t {
 | 
				
			||||||
                    time = setter(&time, t.parse().map_err(|_| InvalidTime::ParseErrorDMY)?)
 | 
					                    time = setter(&time, t.parse().map_err(|_| InvalidTime::ParseErrorDMY)?)
 | 
				
			||||||
                        .map_or_else(|| Err(InvalidTime::ParseErrorDMY), Ok)?;
 | 
					                        .map_or_else(|| Err(InvalidTime::ParseErrorDMY), |inner| Ok(inner))?;
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -132,7 +136,7 @@ impl TimeParser {
 | 
				
			|||||||
                if year.len() == 4 {
 | 
					                if year.len() == 4 {
 | 
				
			||||||
                    time = time
 | 
					                    time = time
 | 
				
			||||||
                        .with_year(year.parse().map_err(|_| InvalidTime::ParseErrorDMY)?)
 | 
					                        .with_year(year.parse().map_err(|_| InvalidTime::ParseErrorDMY)?)
 | 
				
			||||||
                        .map_or_else(|| Err(InvalidTime::ParseErrorDMY), Ok)?;
 | 
					                        .map_or_else(|| Err(InvalidTime::ParseErrorDMY), |inner| Ok(inner))?;
 | 
				
			||||||
                } else if year.len() == 2 {
 | 
					                } else if year.len() == 2 {
 | 
				
			||||||
                    time = time
 | 
					                    time = time
 | 
				
			||||||
                        .with_year(
 | 
					                        .with_year(
 | 
				
			||||||
@@ -140,9 +144,9 @@ impl TimeParser {
 | 
				
			|||||||
                                .parse()
 | 
					                                .parse()
 | 
				
			||||||
                                .map_err(|_| InvalidTime::ParseErrorDMY)?,
 | 
					                                .map_err(|_| InvalidTime::ParseErrorDMY)?,
 | 
				
			||||||
                        )
 | 
					                        )
 | 
				
			||||||
                        .map_or_else(|| Err(InvalidTime::ParseErrorDMY), Ok)?;
 | 
					                        .map_or_else(|| Err(InvalidTime::ParseErrorDMY), |inner| Ok(inner))?;
 | 
				
			||||||
                } else {
 | 
					                } else {
 | 
				
			||||||
                    return Err(InvalidTime::ParseErrorDMY);
 | 
					                    Err(InvalidTime::ParseErrorDMY)?;
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@@ -153,10 +157,10 @@ impl TimeParser {
 | 
				
			|||||||
    fn process_displacement(&self) -> Result<i64, InvalidTime> {
 | 
					    fn process_displacement(&self) -> Result<i64, InvalidTime> {
 | 
				
			||||||
        let mut current_buffer = "0".to_string();
 | 
					        let mut current_buffer = "0".to_string();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        let mut seconds = 0_i64;
 | 
					        let mut seconds = 0 as i64;
 | 
				
			||||||
        let mut minutes = 0_i64;
 | 
					        let mut minutes = 0 as i64;
 | 
				
			||||||
        let mut hours = 0_i64;
 | 
					        let mut hours = 0 as i64;
 | 
				
			||||||
        let mut days = 0_i64;
 | 
					        let mut days = 0 as i64;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        for character in self.time_string.chars() {
 | 
					        for character in self.time_string.chars() {
 | 
				
			||||||
            match character {
 | 
					            match character {
 | 
				
			||||||
@@ -201,7 +205,7 @@ impl TimeParser {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub async fn natural_parser(time: &str, timezone: &str) -> Option<i64> {
 | 
					pub(crate) async fn natural_parser(time: &str, timezone: &str) -> Option<i64> {
 | 
				
			||||||
    Command::new(&*PYTHON_LOCATION)
 | 
					    Command::new(&*PYTHON_LOCATION)
 | 
				
			||||||
        .arg("-c")
 | 
					        .arg("-c")
 | 
				
			||||||
        .arg(include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/dp.py")))
 | 
					        .arg(include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/dp.py")))
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										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