Compare commits
	
		
			73 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					1c1f5662d3 | ||
| ded750aa2d | |||
| 4c4f0927f1 | |||
| 
						 | 
					0f05018cab | ||
| 
						 | 
					85d27c5bba | ||
| 
						 | 
					d946ef1dca | ||
| 
						 | 
					f21d522435 | ||
| 
						 | 
					3add718cdf | ||
| 
						 | 
					f4ef7afea0 | ||
| 
						 | 
					f8547bba70 | ||
| 
						 | 
					08fd88ce54 | ||
| 
						 | 
					abfe492192 | ||
| 
						 | 
					afb2fbe4ff | ||
| 
						 | 
					878ea11502 | ||
| 
						 | 
					93da746bdc | ||
| 
						 | 
					9e6a387f82 | ||
| 
						 | 
					af9d8bea62 | ||
| 
						 | 
					318be1fa5e | ||
| 
						 | 
					3b6e02e16e | ||
| 
						 | 
					a56f84f659 | ||
| 
						 | 
					3e4dd0fa48 | ||
| 
						 | 
					d0d2d50966 | ||
| 
						 | 
					e2e5b022a0 | ||
| 
						 | 
					6ae2353c92 | ||
| 
						 | 
					06c4deeaa9 | ||
| 
						 | 
					afc376c44f | ||
| 
						 | 
					84ee7e77c5 | ||
| 
						 | 
					620f054703 | ||
| 
						 | 
					cb471c52f3 | ||
| 
						 | 
					37420b2b1f | ||
| 
						 | 
					49974b7153 | ||
| 
						 | 
					a3844dde9e | ||
| d62c8c95c2 | |||
| 05606dfec1 | |||
| 68ee42f244 | |||
| fad28faabb | |||
| e5ab99f67b | |||
| e47715917e | |||
| 4f9eb58c16 | |||
| c953bc0cd3 | |||
| 610779a293 | |||
| ebd1efa990 | |||
| 5230101a8d | |||
| d8f42c1b25 | |||
| 23c6b3869e | |||
| a21f518b21 | |||
| f1bfc11160 | |||
| 72228911f2 | |||
| db7cca6296 | |||
| e36e718f28 | |||
| 44debf93c5 | |||
| 9b54fba5e5 | |||
| 6cf660c7ee | |||
| 4490f19c04 | |||
| a362a24cfc | |||
| 903daf65e6 | |||
| b310e99085 | |||
| ebabe0e85a | |||
| 6b5d6ae288 | |||
| 379e488f7a | |||
| d84d7ab62b | |||
| a0974795e1 | |||
| a9c91bee93 | |||
| b2207e308a | |||
| 3c1eeed92f | |||
| 395a8481f1 | |||
| bae0433bd9 | |||
| 3e547861ea | |||
| 9b5333dc87 | |||
| 471948bed3 | |||
| c148cdf556 | |||
| 98aed91d21 | |||
| 40630c0014 | 
							
								
								
									
										2
									
								
								.prettierrc.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,2 @@
 | 
				
			|||||||
 | 
					printWidth = 90
 | 
				
			||||||
 | 
					tabWidth = 4
 | 
				
			||||||
							
								
								
									
										2633
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
							
								
								
									
										20
									
								
								Cargo.toml
									
									
									
									
									
								
							
							
						
						@@ -1,32 +1,32 @@
 | 
				
			|||||||
[package]
 | 
					[package]
 | 
				
			||||||
name = "reminder_rs"
 | 
					name = "reminder_rs"
 | 
				
			||||||
version = "1.5.1"
 | 
					version = "1.6.0-beta3"
 | 
				
			||||||
authors = ["jellywx <judesouthworth@pm.me>"]
 | 
					authors = ["jellywx <judesouthworth@pm.me>"]
 | 
				
			||||||
edition = "2018"
 | 
					edition = "2018"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[dependencies]
 | 
					[dependencies]
 | 
				
			||||||
dashmap = "4.0"
 | 
					poise = "0.2"
 | 
				
			||||||
dotenv = "0.15"
 | 
					dotenv = "0.15"
 | 
				
			||||||
humantime = "2.1"
 | 
					 | 
				
			||||||
tokio = { version = "1", features = ["process", "full"] }
 | 
					tokio = { version = "1", features = ["process", "full"] }
 | 
				
			||||||
reqwest = "0.11"
 | 
					reqwest = "0.11"
 | 
				
			||||||
regex = "1.4"
 | 
					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 = "0.5"
 | 
					chrono-tz = { version = "0.5", features = ["serde"] }
 | 
				
			||||||
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"]}
 | 
				
			||||||
ring = "0.16"
 | 
					 | 
				
			||||||
base64 = "0.13.0"
 | 
					base64 = "0.13.0"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[dependencies.regex_command_attr]
 | 
					[dependencies.postman]
 | 
				
			||||||
path = "./regex_command_attr"
 | 
					path = "postman"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[dependencies.reminder_web]
 | 
				
			||||||
 | 
					path = "web"
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										15
									
								
								README.md
									
									
									
									
									
								
							
							
						
						@@ -1,6 +1,5 @@
 | 
				
			|||||||
# reminder-rs
 | 
					# reminder-rs
 | 
				
			||||||
Reminder Bot for Discord, now in Rust.
 | 
					Reminder Bot for Discord.
 | 
				
			||||||
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 
 | 
				
			||||||
@@ -15,7 +14,6 @@ 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
 | 
				
			||||||
@@ -29,16 +27,13 @@ __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
 | 
					 | 
				
			||||||
* `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_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
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										28
									
								
								Rocket.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,28 @@
 | 
				
			|||||||
 | 
					[default]
 | 
				
			||||||
 | 
					address = "0.0.0.0"
 | 
				
			||||||
 | 
					port = 5000
 | 
				
			||||||
 | 
					template_dir = "web/templates"
 | 
				
			||||||
 | 
					limits = { json = "10MiB" }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[debug]
 | 
				
			||||||
 | 
					secret_key = "tR8krio5FXTnnyIZNiJDXPondz0kI1v6X6BXZcBGIRY="
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[debug.tls]
 | 
				
			||||||
 | 
					certs = "web/private/rsa_sha256_cert.pem"
 | 
				
			||||||
 | 
					key = "web/private/rsa_sha256_key.pem"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[rsa_sha256.tls]
 | 
				
			||||||
 | 
					certs = "web/private/rsa_sha256_cert.pem"
 | 
				
			||||||
 | 
					key = "web/private/rsa_sha256_key.pem"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[ecdsa_nistp256_sha256.tls]
 | 
				
			||||||
 | 
					certs = "web/private/ecdsa_nistp256_sha256_cert.pem"
 | 
				
			||||||
 | 
					key = "web/private/ecdsa_nistp256_sha256_key_pkcs8.pem"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[ecdsa_nistp384_sha384.tls]
 | 
				
			||||||
 | 
					certs = "web/private/ecdsa_nistp384_sha384_cert.pem"
 | 
				
			||||||
 | 
					key = "web/private/ecdsa_nistp384_sha384_key_pkcs8.pem"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[ed25519.tls]
 | 
				
			||||||
 | 
					certs = "web/private/ed25519_cert.pem"
 | 
				
			||||||
 | 
					key = "eb/private/ed25519_key.pem"
 | 
				
			||||||
@@ -56,8 +56,7 @@ 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 (
 | 
				
			||||||
							
								
								
									
										13
									
								
								migration/02-macro.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,13 @@
 | 
				
			|||||||
 | 
					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)
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
							
								
								
									
										4
									
								
								migration/03-reminder_variable_intervals.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,4 @@
 | 
				
			|||||||
 | 
					USE reminders;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ALTER TABLE reminders.reminders RENAME COLUMN `interval` TO `interval_seconds`;
 | 
				
			||||||
 | 
					ALTER TABLE reminders.reminders ADD COLUMN `interval_months` INT UNSIGNED DEFAULT NULL;
 | 
				
			||||||
							
								
								
									
										34
									
								
								migration/04-reminder_templates.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,34 @@
 | 
				
			|||||||
 | 
					USE reminders;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					CREATE TABLE reminder_template (
 | 
				
			||||||
 | 
					    `id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    `name` VARCHAR(24) NOT NULL DEFAULT 'Reminder',
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    `guild_id` INT UNSIGNED NOT NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    `username` VARCHAR(32) DEFAULT NULL,
 | 
				
			||||||
 | 
					    `avatar` VARCHAR(512) DEFAULT NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    `content` VARCHAR(2048) NOT NULL DEFAULT '',
 | 
				
			||||||
 | 
					    `tts` BOOL NOT NULL DEFAULT 0,
 | 
				
			||||||
 | 
					    `attachment` MEDIUMBLOB,
 | 
				
			||||||
 | 
					    `attachment_name` VARCHAR(260),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    `embed_title` VARCHAR(256) NOT NULL DEFAULT '',
 | 
				
			||||||
 | 
					    `embed_description` VARCHAR(2048) NOT NULL DEFAULT '',
 | 
				
			||||||
 | 
					    `embed_image_url` VARCHAR(512),
 | 
				
			||||||
 | 
					    `embed_thumbnail_url` VARCHAR(512),
 | 
				
			||||||
 | 
					    `embed_footer` VARCHAR(2048) NOT NULL DEFAULT '',
 | 
				
			||||||
 | 
					    `embed_footer_url` VARCHAR(512),
 | 
				
			||||||
 | 
					    `embed_author` VARCHAR(256) NOT NULL DEFAULT '',
 | 
				
			||||||
 | 
					    `embed_author_url` VARCHAR(512),
 | 
				
			||||||
 | 
					    `embed_color` INT UNSIGNED NOT NULL DEFAULT 0x0,
 | 
				
			||||||
 | 
					    `embed_fields` JSON,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    PRIMARY KEY (id),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    FOREIGN KEY (`guild_id`) REFERENCES guilds (`id`) ON DELETE CASCADE
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ALTER TABLE reminders ADD COLUMN embed_fields JSON;
 | 
				
			||||||
							
								
								
									
										18
									
								
								postman/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,18 @@
 | 
				
			|||||||
 | 
					[package]
 | 
				
			||||||
 | 
					name = "postman"
 | 
				
			||||||
 | 
					version = "0.1.0"
 | 
				
			||||||
 | 
					edition = "2021"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[dependencies]
 | 
				
			||||||
 | 
					tokio = { version = "1", features = ["process", "full"] }
 | 
				
			||||||
 | 
					regex = "1.4"
 | 
				
			||||||
 | 
					log = "0.4"
 | 
				
			||||||
 | 
					env_logger = "0.8"
 | 
				
			||||||
 | 
					chrono = "0.4"
 | 
				
			||||||
 | 
					chrono-tz = { version = "0.5", features = ["serde"] }
 | 
				
			||||||
 | 
					lazy_static = "1.4"
 | 
				
			||||||
 | 
					num-integer = "0.1"
 | 
				
			||||||
 | 
					serde = "1.0"
 | 
				
			||||||
 | 
					serde_json = "1.0"
 | 
				
			||||||
 | 
					sqlx = { version = "0.5", features = ["runtime-tokio-rustls", "macros", "mysql", "bigdecimal", "chrono", "json"]}
 | 
				
			||||||
 | 
					serenity = { version = "0.11.1", default-features = false, features = ["builder", "cache", "client", "gateway", "http", "model", "utils", "rustls_backend"] }
 | 
				
			||||||
							
								
								
									
										50
									
								
								postman/src/lib.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,50 @@
 | 
				
			|||||||
 | 
					mod sender;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use std::env;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use log::{info, warn};
 | 
				
			||||||
 | 
					use serenity::client::Context;
 | 
				
			||||||
 | 
					use sqlx::{Executor, MySql};
 | 
				
			||||||
 | 
					use tokio::{
 | 
				
			||||||
 | 
					    sync::broadcast::Receiver,
 | 
				
			||||||
 | 
					    time::{sleep_until, Duration, Instant},
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type Database = MySql;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub async fn initialize(
 | 
				
			||||||
 | 
					    mut kill: Receiver<()>,
 | 
				
			||||||
 | 
					    ctx: Context,
 | 
				
			||||||
 | 
					    pool: impl Executor<'_, Database = Database> + Copy,
 | 
				
			||||||
 | 
					) -> Result<(), &'static str> {
 | 
				
			||||||
 | 
					    tokio::select! {
 | 
				
			||||||
 | 
					        output = _initialize(ctx, pool) => Ok(output),
 | 
				
			||||||
 | 
					        _ = kill.recv() => {
 | 
				
			||||||
 | 
					            warn!("Received terminate signal. Goodbye");
 | 
				
			||||||
 | 
					            Err("Received terminate signal. Goodbye")
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async fn _initialize(ctx: Context, pool: impl Executor<'_, Database = Database> + Copy) {
 | 
				
			||||||
 | 
					    let remind_interval = env::var("REMIND_INTERVAL")
 | 
				
			||||||
 | 
					        .map(|inner| inner.parse::<u64>().ok())
 | 
				
			||||||
 | 
					        .ok()
 | 
				
			||||||
 | 
					        .flatten()
 | 
				
			||||||
 | 
					        .unwrap_or(10);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    loop {
 | 
				
			||||||
 | 
					        let sleep_to = Instant::now() + Duration::from_secs(remind_interval);
 | 
				
			||||||
 | 
					        let reminders = sender::Reminder::fetch_reminders(pool).await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if reminders.len() > 0 {
 | 
				
			||||||
 | 
					            info!("Preparing to send {} reminders.", reminders.len());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            for reminder in reminders {
 | 
				
			||||||
 | 
					                reminder.send(pool, ctx.clone()).await;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        sleep_until(sleep_to).await;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										559
									
								
								postman/src/sender.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,559 @@
 | 
				
			|||||||
 | 
					use chrono::Duration;
 | 
				
			||||||
 | 
					use chrono_tz::Tz;
 | 
				
			||||||
 | 
					use lazy_static::lazy_static;
 | 
				
			||||||
 | 
					use log::{error, info, warn};
 | 
				
			||||||
 | 
					use num_integer::Integer;
 | 
				
			||||||
 | 
					use regex::{Captures, Regex};
 | 
				
			||||||
 | 
					use serde::Deserialize;
 | 
				
			||||||
 | 
					use serenity::{
 | 
				
			||||||
 | 
					    builder::CreateEmbed,
 | 
				
			||||||
 | 
					    http::{CacheHttp, Http, StatusCode},
 | 
				
			||||||
 | 
					    model::{
 | 
				
			||||||
 | 
					        channel::{Channel, Embed as SerenityEmbed},
 | 
				
			||||||
 | 
					        id::ChannelId,
 | 
				
			||||||
 | 
					        webhook::Webhook,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    Error, Result,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					use sqlx::{
 | 
				
			||||||
 | 
					    types::{
 | 
				
			||||||
 | 
					        chrono::{NaiveDateTime, Utc},
 | 
				
			||||||
 | 
					        Json,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    Executor,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::Database;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					lazy_static! {
 | 
				
			||||||
 | 
					    pub static ref TIMEFROM_REGEX: Regex =
 | 
				
			||||||
 | 
					        Regex::new(r#"<<timefrom:(?P<time>\d+):(?P<format>.+)?>>"#).unwrap();
 | 
				
			||||||
 | 
					    pub static ref TIMENOW_REGEX: Regex =
 | 
				
			||||||
 | 
					        Regex::new(r#"<<timenow:(?P<timezone>(?:\w|/|_)+):(?P<format>.+)?>>"#).unwrap();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fn fmt_displacement(format: &str, seconds: u64) -> String {
 | 
				
			||||||
 | 
					    let mut seconds = seconds;
 | 
				
			||||||
 | 
					    let mut days: u64 = 0;
 | 
				
			||||||
 | 
					    let mut hours: u64 = 0;
 | 
				
			||||||
 | 
					    let mut minutes: u64 = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for (rep, time_type, div) in
 | 
				
			||||||
 | 
					        [("%d", &mut days, 86400), ("%h", &mut hours, 3600), ("%m", &mut minutes, 60)].iter_mut()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        if format.contains(*rep) {
 | 
				
			||||||
 | 
					            let (divided, new_seconds) = seconds.div_rem(&div);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            **time_type = divided;
 | 
				
			||||||
 | 
					            seconds = new_seconds;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    format
 | 
				
			||||||
 | 
					        .replace("%s", &seconds.to_string())
 | 
				
			||||||
 | 
					        .replace("%m", &minutes.to_string())
 | 
				
			||||||
 | 
					        .replace("%h", &hours.to_string())
 | 
				
			||||||
 | 
					        .replace("%d", &days.to_string())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub fn substitute(string: &str) -> String {
 | 
				
			||||||
 | 
					    let new = TIMEFROM_REGEX.replace(string, |caps: &Captures| {
 | 
				
			||||||
 | 
					        let final_time = caps.name("time").unwrap().as_str();
 | 
				
			||||||
 | 
					        let format = caps.name("format").unwrap().as_str();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if let Ok(final_time) = final_time.parse::<i64>() {
 | 
				
			||||||
 | 
					            let dt = NaiveDateTime::from_timestamp(final_time, 0);
 | 
				
			||||||
 | 
					            let now = Utc::now().naive_utc();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            let difference = {
 | 
				
			||||||
 | 
					                if now < dt {
 | 
				
			||||||
 | 
					                    dt - Utc::now().naive_utc()
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    Utc::now().naive_utc() - dt
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            fmt_displacement(format, difference.num_seconds() as u64)
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            String::new()
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    TIMENOW_REGEX
 | 
				
			||||||
 | 
					        .replace(&new, |caps: &Captures| {
 | 
				
			||||||
 | 
					            let timezone = caps.name("timezone").unwrap().as_str();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            println!("{}", timezone);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if let Ok(tz) = timezone.parse::<Tz>() {
 | 
				
			||||||
 | 
					                let format = caps.name("format").unwrap().as_str();
 | 
				
			||||||
 | 
					                let now = Utc::now().with_timezone(&tz);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                now.format(format).to_string()
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                String::new()
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        .to_string()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					struct Embed {
 | 
				
			||||||
 | 
					    title: String,
 | 
				
			||||||
 | 
					    description: String,
 | 
				
			||||||
 | 
					    image_url: Option<String>,
 | 
				
			||||||
 | 
					    thumbnail_url: Option<String>,
 | 
				
			||||||
 | 
					    footer: String,
 | 
				
			||||||
 | 
					    footer_url: Option<String>,
 | 
				
			||||||
 | 
					    author: String,
 | 
				
			||||||
 | 
					    author_url: Option<String>,
 | 
				
			||||||
 | 
					    color: u32,
 | 
				
			||||||
 | 
					    fields: Json<Vec<EmbedField>>,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Deserialize)]
 | 
				
			||||||
 | 
					struct EmbedField {
 | 
				
			||||||
 | 
					    title: String,
 | 
				
			||||||
 | 
					    value: String,
 | 
				
			||||||
 | 
					    inline: bool,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl Embed {
 | 
				
			||||||
 | 
					    pub async fn from_id(
 | 
				
			||||||
 | 
					        pool: impl Executor<'_, Database = Database> + Copy,
 | 
				
			||||||
 | 
					        id: u32,
 | 
				
			||||||
 | 
					    ) -> Option<Self> {
 | 
				
			||||||
 | 
					        let mut embed = sqlx::query_as!(
 | 
				
			||||||
 | 
					            Self,
 | 
				
			||||||
 | 
					            r#"
 | 
				
			||||||
 | 
					            SELECT
 | 
				
			||||||
 | 
					             `embed_title` AS title,
 | 
				
			||||||
 | 
					             `embed_description` AS description,
 | 
				
			||||||
 | 
					             `embed_image_url` AS image_url,
 | 
				
			||||||
 | 
					             `embed_thumbnail_url` AS thumbnail_url,
 | 
				
			||||||
 | 
					             `embed_footer` AS footer,
 | 
				
			||||||
 | 
					             `embed_footer_url` AS footer_url,
 | 
				
			||||||
 | 
					             `embed_author` AS author,
 | 
				
			||||||
 | 
					             `embed_author_url` AS author_url,
 | 
				
			||||||
 | 
					             `embed_color` AS color,
 | 
				
			||||||
 | 
					             IFNULL(`embed_fields`, '[]') AS "fields:_"
 | 
				
			||||||
 | 
					            FROM reminders
 | 
				
			||||||
 | 
					            WHERE `id` = ?"#,
 | 
				
			||||||
 | 
					            id
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        .fetch_one(pool)
 | 
				
			||||||
 | 
					        .await
 | 
				
			||||||
 | 
					        .unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        embed.title = substitute(&embed.title);
 | 
				
			||||||
 | 
					        embed.description = substitute(&embed.description);
 | 
				
			||||||
 | 
					        embed.footer = substitute(&embed.footer);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        embed.fields.iter_mut().for_each(|mut field| {
 | 
				
			||||||
 | 
					            field.title = substitute(&field.title);
 | 
				
			||||||
 | 
					            field.value = substitute(&field.value);
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if embed.has_content() {
 | 
				
			||||||
 | 
					            Some(embed)
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            None
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn has_content(&self) -> bool {
 | 
				
			||||||
 | 
					        if self.title.is_empty()
 | 
				
			||||||
 | 
					            && self.description.is_empty()
 | 
				
			||||||
 | 
					            && self.image_url.is_none()
 | 
				
			||||||
 | 
					            && self.thumbnail_url.is_none()
 | 
				
			||||||
 | 
					            && self.footer.is_empty()
 | 
				
			||||||
 | 
					            && self.footer_url.is_none()
 | 
				
			||||||
 | 
					            && self.author.is_empty()
 | 
				
			||||||
 | 
					            && self.author_url.is_none()
 | 
				
			||||||
 | 
					            && self.fields.0.is_empty()
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            false
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            true
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl Into<CreateEmbed> for Embed {
 | 
				
			||||||
 | 
					    fn into(self) -> CreateEmbed {
 | 
				
			||||||
 | 
					        let mut c = CreateEmbed::default();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        c.title(&self.title)
 | 
				
			||||||
 | 
					            .description(&self.description)
 | 
				
			||||||
 | 
					            .color(self.color)
 | 
				
			||||||
 | 
					            .author(|a| {
 | 
				
			||||||
 | 
					                a.name(&self.author);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if let Some(author_icon) = &self.author_url {
 | 
				
			||||||
 | 
					                    a.icon_url(author_icon);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                a
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					            .footer(|f| {
 | 
				
			||||||
 | 
					                f.text(&self.footer);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if let Some(footer_icon) = &self.footer_url {
 | 
				
			||||||
 | 
					                    f.icon_url(footer_icon);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                f
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        for field in &self.fields.0 {
 | 
				
			||||||
 | 
					            c.field(&field.title, &field.value, field.inline);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if let Some(image_url) = &self.image_url {
 | 
				
			||||||
 | 
					            c.image(image_url);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if let Some(thumbnail_url) = &self.thumbnail_url {
 | 
				
			||||||
 | 
					            c.thumbnail(thumbnail_url);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        c
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Debug)]
 | 
				
			||||||
 | 
					pub struct Reminder {
 | 
				
			||||||
 | 
					    id: u32,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    channel_id: u64,
 | 
				
			||||||
 | 
					    webhook_id: Option<u64>,
 | 
				
			||||||
 | 
					    webhook_token: Option<String>,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    channel_paused: bool,
 | 
				
			||||||
 | 
					    channel_paused_until: Option<NaiveDateTime>,
 | 
				
			||||||
 | 
					    enabled: bool,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    tts: bool,
 | 
				
			||||||
 | 
					    pin: bool,
 | 
				
			||||||
 | 
					    content: String,
 | 
				
			||||||
 | 
					    attachment: Option<Vec<u8>>,
 | 
				
			||||||
 | 
					    attachment_name: Option<String>,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    utc_time: NaiveDateTime,
 | 
				
			||||||
 | 
					    timezone: String,
 | 
				
			||||||
 | 
					    restartable: bool,
 | 
				
			||||||
 | 
					    expires: Option<NaiveDateTime>,
 | 
				
			||||||
 | 
					    interval_seconds: Option<u32>,
 | 
				
			||||||
 | 
					    interval_months: Option<u32>,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    avatar: Option<String>,
 | 
				
			||||||
 | 
					    username: Option<String>,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl Reminder {
 | 
				
			||||||
 | 
					    pub async fn fetch_reminders(pool: impl Executor<'_, Database = Database> + Copy) -> Vec<Self> {
 | 
				
			||||||
 | 
					        sqlx::query_as_unchecked!(
 | 
				
			||||||
 | 
					            Reminder,
 | 
				
			||||||
 | 
					            "
 | 
				
			||||||
 | 
					SELECT
 | 
				
			||||||
 | 
					    reminders.`id` AS id,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    channels.`channel` AS channel_id,
 | 
				
			||||||
 | 
					    channels.`webhook_id` AS webhook_id,
 | 
				
			||||||
 | 
					    channels.`webhook_token` AS webhook_token,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    channels.`paused` AS channel_paused,
 | 
				
			||||||
 | 
					    channels.`paused_until` AS channel_paused_until,
 | 
				
			||||||
 | 
					    reminders.`enabled` AS enabled,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    reminders.`tts` AS tts,
 | 
				
			||||||
 | 
					    reminders.`pin` AS pin,
 | 
				
			||||||
 | 
					    reminders.`content` AS content,
 | 
				
			||||||
 | 
					    reminders.`attachment` AS attachment,
 | 
				
			||||||
 | 
					    reminders.`attachment_name` AS attachment_name,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    reminders.`utc_time` AS 'utc_time',
 | 
				
			||||||
 | 
					    reminders.`timezone` AS timezone,
 | 
				
			||||||
 | 
					    reminders.`restartable` AS restartable,
 | 
				
			||||||
 | 
					    reminders.`expires` AS expires,
 | 
				
			||||||
 | 
					    reminders.`interval_seconds` AS 'interval_seconds',
 | 
				
			||||||
 | 
					    reminders.`interval_months` AS 'interval_months',
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    reminders.`avatar` AS avatar,
 | 
				
			||||||
 | 
					    reminders.`username` AS username
 | 
				
			||||||
 | 
					FROM
 | 
				
			||||||
 | 
					    reminders
 | 
				
			||||||
 | 
					INNER JOIN
 | 
				
			||||||
 | 
					    channels
 | 
				
			||||||
 | 
					ON
 | 
				
			||||||
 | 
					    reminders.channel_id = channels.id
 | 
				
			||||||
 | 
					WHERE
 | 
				
			||||||
 | 
					    reminders.`utc_time` < NOW()
 | 
				
			||||||
 | 
					            ",
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        .fetch_all(pool)
 | 
				
			||||||
 | 
					        .await
 | 
				
			||||||
 | 
					        .unwrap()
 | 
				
			||||||
 | 
					        .into_iter()
 | 
				
			||||||
 | 
					        .map(|mut rem| {
 | 
				
			||||||
 | 
					            rem.content = substitute(&rem.content);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            rem
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        .collect::<Vec<Self>>()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async fn reset_webhook(&self, pool: impl Executor<'_, Database = Database> + Copy) {
 | 
				
			||||||
 | 
					        let _ = sqlx::query!(
 | 
				
			||||||
 | 
					            "
 | 
				
			||||||
 | 
					UPDATE channels SET webhook_id = NULL, webhook_token = NULL WHERE channel = ?
 | 
				
			||||||
 | 
					            ",
 | 
				
			||||||
 | 
					            self.channel_id
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        .execute(pool)
 | 
				
			||||||
 | 
					        .await;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async fn refresh(&self, pool: impl Executor<'_, Database = Database> + Copy) {
 | 
				
			||||||
 | 
					        if self.interval_seconds.is_some() || self.interval_months.is_some() {
 | 
				
			||||||
 | 
					            let now = Utc::now().naive_local();
 | 
				
			||||||
 | 
					            let mut updated_reminder_time = self.utc_time;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if let Some(interval) = self.interval_months {
 | 
				
			||||||
 | 
					                let row = sqlx::query!(
 | 
				
			||||||
 | 
					                    // use the second date_add to force return value to datetime
 | 
				
			||||||
 | 
					                    "SELECT DATE_ADD(DATE_ADD(?, INTERVAL ? MONTH), INTERVAL 0 SECOND) AS new_time",
 | 
				
			||||||
 | 
					                    updated_reminder_time,
 | 
				
			||||||
 | 
					                    interval
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					                .fetch_one(pool)
 | 
				
			||||||
 | 
					                .await
 | 
				
			||||||
 | 
					                .unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                updated_reminder_time = row.new_time.unwrap();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if let Some(interval) = self.interval_seconds {
 | 
				
			||||||
 | 
					                while updated_reminder_time < now {
 | 
				
			||||||
 | 
					                    updated_reminder_time += Duration::seconds(interval as i64);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if self.expires.map_or(false, |expires| {
 | 
				
			||||||
 | 
					                NaiveDateTime::from_timestamp(updated_reminder_time.timestamp(), 0) > expires
 | 
				
			||||||
 | 
					            }) {
 | 
				
			||||||
 | 
					                self.force_delete(pool).await;
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                sqlx::query!(
 | 
				
			||||||
 | 
					                    "
 | 
				
			||||||
 | 
					UPDATE reminders SET `utc_time` = ? WHERE `id` = ?
 | 
				
			||||||
 | 
					                    ",
 | 
				
			||||||
 | 
					                    updated_reminder_time,
 | 
				
			||||||
 | 
					                    self.id
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					                .execute(pool)
 | 
				
			||||||
 | 
					                .await
 | 
				
			||||||
 | 
					                .expect(&format!("Could not update time on Reminder {}", self.id));
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            self.force_delete(pool).await;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async fn force_delete(&self, pool: impl Executor<'_, Database = Database> + Copy) {
 | 
				
			||||||
 | 
					        sqlx::query!(
 | 
				
			||||||
 | 
					            "
 | 
				
			||||||
 | 
					DELETE FROM reminders WHERE `id` = ?
 | 
				
			||||||
 | 
					            ",
 | 
				
			||||||
 | 
					            self.id
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        .execute(pool)
 | 
				
			||||||
 | 
					        .await
 | 
				
			||||||
 | 
					        .expect(&format!("Could not delete Reminder {}", self.id));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async fn pin_message<M: Into<u64>>(&self, message_id: M, http: impl AsRef<Http>) {
 | 
				
			||||||
 | 
					        let _ = http.as_ref().pin_message(self.channel_id, message_id.into(), None).await;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub async fn send(
 | 
				
			||||||
 | 
					        &self,
 | 
				
			||||||
 | 
					        pool: impl Executor<'_, Database = Database> + Copy,
 | 
				
			||||||
 | 
					        cache_http: impl CacheHttp,
 | 
				
			||||||
 | 
					    ) {
 | 
				
			||||||
 | 
					        async fn send_to_channel(
 | 
				
			||||||
 | 
					            cache_http: impl CacheHttp,
 | 
				
			||||||
 | 
					            reminder: &Reminder,
 | 
				
			||||||
 | 
					            embed: Option<CreateEmbed>,
 | 
				
			||||||
 | 
					        ) -> Result<()> {
 | 
				
			||||||
 | 
					            let channel = ChannelId(reminder.channel_id).to_channel(&cache_http).await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            match channel {
 | 
				
			||||||
 | 
					                Ok(Channel::Guild(channel)) => {
 | 
				
			||||||
 | 
					                    match channel
 | 
				
			||||||
 | 
					                        .send_message(&cache_http, |m| {
 | 
				
			||||||
 | 
					                            m.content(&reminder.content).tts(reminder.tts);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                            if let (Some(attachment), Some(name)) =
 | 
				
			||||||
 | 
					                                (&reminder.attachment, &reminder.attachment_name)
 | 
				
			||||||
 | 
					                            {
 | 
				
			||||||
 | 
					                                m.add_file((attachment as &[u8], name.as_str()));
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                            if let Some(embed) = embed {
 | 
				
			||||||
 | 
					                                m.set_embed(embed);
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                            m
 | 
				
			||||||
 | 
					                        })
 | 
				
			||||||
 | 
					                        .await
 | 
				
			||||||
 | 
					                    {
 | 
				
			||||||
 | 
					                        Ok(m) => {
 | 
				
			||||||
 | 
					                            if reminder.pin {
 | 
				
			||||||
 | 
					                                reminder.pin_message(m.id, cache_http.http()).await;
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                            Ok(())
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                        Err(e) => Err(e),
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                Ok(Channel::Private(channel)) => {
 | 
				
			||||||
 | 
					                    match channel
 | 
				
			||||||
 | 
					                        .send_message(&cache_http.http(), |m| {
 | 
				
			||||||
 | 
					                            m.content(&reminder.content).tts(reminder.tts);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                            if let (Some(attachment), Some(name)) =
 | 
				
			||||||
 | 
					                                (&reminder.attachment, &reminder.attachment_name)
 | 
				
			||||||
 | 
					                            {
 | 
				
			||||||
 | 
					                                m.add_file((attachment as &[u8], name.as_str()));
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                            if let Some(embed) = embed {
 | 
				
			||||||
 | 
					                                m.set_embed(embed);
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                            m
 | 
				
			||||||
 | 
					                        })
 | 
				
			||||||
 | 
					                        .await
 | 
				
			||||||
 | 
					                    {
 | 
				
			||||||
 | 
					                        Ok(m) => {
 | 
				
			||||||
 | 
					                            if reminder.pin {
 | 
				
			||||||
 | 
					                                reminder.pin_message(m.id, cache_http.http()).await;
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                            Ok(())
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                        Err(e) => Err(e),
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                Err(e) => Err(e),
 | 
				
			||||||
 | 
					                _ => Err(Error::Other("Channel not of valid type")),
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        async fn send_to_webhook(
 | 
				
			||||||
 | 
					            cache_http: impl CacheHttp,
 | 
				
			||||||
 | 
					            reminder: &Reminder,
 | 
				
			||||||
 | 
					            webhook: Webhook,
 | 
				
			||||||
 | 
					            embed: Option<CreateEmbed>,
 | 
				
			||||||
 | 
					        ) -> Result<()> {
 | 
				
			||||||
 | 
					            match webhook
 | 
				
			||||||
 | 
					                .execute(&cache_http.http(), reminder.pin || reminder.restartable, |w| {
 | 
				
			||||||
 | 
					                    w.content(&reminder.content).tts(reminder.tts);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    if let Some(username) = &reminder.username {
 | 
				
			||||||
 | 
					                        w.username(username);
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    if let Some(avatar) = &reminder.avatar {
 | 
				
			||||||
 | 
					                        w.avatar_url(avatar);
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    if let (Some(attachment), Some(name)) =
 | 
				
			||||||
 | 
					                        (&reminder.attachment, &reminder.attachment_name)
 | 
				
			||||||
 | 
					                    {
 | 
				
			||||||
 | 
					                        w.add_file((attachment as &[u8], name.as_str()));
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    if let Some(embed) = embed {
 | 
				
			||||||
 | 
					                        w.embeds(vec![SerenityEmbed::fake(|c| {
 | 
				
			||||||
 | 
					                            *c = embed;
 | 
				
			||||||
 | 
					                            c
 | 
				
			||||||
 | 
					                        })]);
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    w
 | 
				
			||||||
 | 
					                })
 | 
				
			||||||
 | 
					                .await
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                Ok(m) => {
 | 
				
			||||||
 | 
					                    if reminder.pin {
 | 
				
			||||||
 | 
					                        if let Some(message) = m {
 | 
				
			||||||
 | 
					                            reminder.pin_message(message.id, cache_http.http()).await;
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    Ok(())
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                Err(e) => Err(e),
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if self.enabled
 | 
				
			||||||
 | 
					            && !(self.channel_paused
 | 
				
			||||||
 | 
					                && self
 | 
				
			||||||
 | 
					                    .channel_paused_until
 | 
				
			||||||
 | 
					                    .map_or(true, |inner| inner >= Utc::now().naive_local()))
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            let _ = sqlx::query!(
 | 
				
			||||||
 | 
					                "
 | 
				
			||||||
 | 
					UPDATE `channels` SET paused = 0, paused_until = NULL WHERE `channel` = ?
 | 
				
			||||||
 | 
					                ",
 | 
				
			||||||
 | 
					                self.channel_id
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            .execute(pool)
 | 
				
			||||||
 | 
					            .await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            let embed = Embed::from_id(pool, self.id).await.map(|e| e.into());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            let result = if let (Some(webhook_id), Some(webhook_token)) =
 | 
				
			||||||
 | 
					                (self.webhook_id, &self.webhook_token)
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                let webhook_res =
 | 
				
			||||||
 | 
					                    cache_http.http().get_webhook_with_token(webhook_id, webhook_token).await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if let Ok(webhook) = webhook_res {
 | 
				
			||||||
 | 
					                    send_to_webhook(cache_http, &self, webhook, embed).await
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    warn!("Webhook vanished: {:?}", webhook_res);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    self.reset_webhook(pool).await;
 | 
				
			||||||
 | 
					                    send_to_channel(cache_http, &self, embed).await
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                send_to_channel(cache_http, &self, embed).await
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if let Err(e) = result {
 | 
				
			||||||
 | 
					                error!("Error sending {:?}: {:?}", self, e);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if let Error::Http(error) = e {
 | 
				
			||||||
 | 
					                    if error.status_code() == Some(StatusCode::from_u16(404).unwrap()) {
 | 
				
			||||||
 | 
					                        error!("Seeing channel is deleted. Removing reminder");
 | 
				
			||||||
 | 
					                        self.force_delete(pool).await;
 | 
				
			||||||
 | 
					                    } else {
 | 
				
			||||||
 | 
					                        self.refresh(pool).await;
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    self.refresh(pool).await;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                self.refresh(pool).await;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            info!("Reminder {} is paused", self.id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            self.refresh(pool).await;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,14 +0,0 @@
 | 
				
			|||||||
[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"
 | 
					 | 
				
			||||||
@@ -1,293 +0,0 @@
 | 
				
			|||||||
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);
 | 
					 | 
				
			||||||
@@ -1,5 +0,0 @@
 | 
				
			|||||||
pub mod suffixes {
 | 
					 | 
				
			||||||
    pub const COMMAND: &str = "COMMAND";
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
pub use self::suffixes::*;
 | 
					 | 
				
			||||||
@@ -1,102 +0,0 @@
 | 
				
			|||||||
#![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()
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,231 +0,0 @@
 | 
				
			|||||||
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
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,160 +0,0 @@
 | 
				
			|||||||
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()));
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
							
								
								
									
										3
									
								
								rustfmt.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,3 @@
 | 
				
			|||||||
 | 
					imports_granularity = "Crate"
 | 
				
			||||||
 | 
					group_imports = "StdExternalCrate"
 | 
				
			||||||
 | 
					use_small_heuristics = "Max"
 | 
				
			||||||
@@ -1,42 +1,13 @@
 | 
				
			|||||||
use regex_command_attr::command;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use serenity::{builder::CreateEmbedFooter, client::Context, model::channel::Message};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use chrono::offset::Utc;
 | 
					use chrono::offset::Utc;
 | 
				
			||||||
 | 
					use poise::{serenity_prelude as serenity, serenity_prelude::Mentionable};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::{
 | 
					use crate::{models::CtxData, Context, Error, THEME_COLOR};
 | 
				
			||||||
    command_help,
 | 
					 | 
				
			||||||
    consts::DEFAULT_PREFIX,
 | 
					 | 
				
			||||||
    get_ctx_data,
 | 
					 | 
				
			||||||
    language_manager::LanguageManager,
 | 
					 | 
				
			||||||
    models::{user_data::UserData, CtxGuildData},
 | 
					 | 
				
			||||||
    FrameworkCtx, THEME_COLOR,
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
use std::{
 | 
					fn footer(
 | 
				
			||||||
    sync::Arc,
 | 
					    ctx: Context<'_>,
 | 
				
			||||||
    time::{SystemTime, UNIX_EPOCH},
 | 
					) -> impl FnOnce(&mut serenity::CreateEmbedFooter) -> &mut serenity::CreateEmbedFooter {
 | 
				
			||||||
};
 | 
					    let shard_count = ctx.discord().cache.shard_count();
 | 
				
			||||||
 | 
					    let shard = ctx.discord().shard_id;
 | 
				
			||||||
#[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!(
 | 
				
			||||||
@@ -48,174 +19,162 @@ async fn footer(ctx: &Context) -> impl FnOnce(&mut CreateEmbedFooter) -> &mut Cr
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[command]
 | 
					/// Get an overview of bot commands
 | 
				
			||||||
#[can_blacklist(false)]
 | 
					#[poise::command(slash_command)]
 | 
				
			||||||
async fn help(ctx: &Context, msg: &Message, args: String) {
 | 
					pub async fn help(ctx: Context<'_>) -> Result<(), Error> {
 | 
				
			||||||
    async fn default_help(
 | 
					    let footer = footer(ctx);
 | 
				
			||||||
        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 _ = msg
 | 
					    ctx.send(|m| {
 | 
				
			||||||
            .channel_id
 | 
					        m.ephemeral(true).embed(|e| {
 | 
				
			||||||
            .send_message(ctx, |m| {
 | 
					            e.title("Help")
 | 
				
			||||||
                m.embed(move |e| {
 | 
					 | 
				
			||||||
                    e.title("Help Menu")
 | 
					 | 
				
			||||||
                        .description(desc)
 | 
					 | 
				
			||||||
                        .field(
 | 
					 | 
				
			||||||
                            lm.get(language, "help/setup_title"),
 | 
					 | 
				
			||||||
                            "`lang` `timezone` `meridian`",
 | 
					 | 
				
			||||||
                            true,
 | 
					 | 
				
			||||||
                        )
 | 
					 | 
				
			||||||
                        .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)
 | 
					 | 
				
			||||||
                .color(*THEME_COLOR)
 | 
					                .color(*THEME_COLOR)
 | 
				
			||||||
 | 
					                .description(
 | 
				
			||||||
 | 
					                    "__Info Commands__
 | 
				
			||||||
 | 
					`/help` `/info` `/donate` `/dashboard` `/clock`
 | 
				
			||||||
 | 
					*run these commands with no options*
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					__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
 | 
				
			||||||
 | 
					                    ",
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					                .footer(footer)
 | 
				
			||||||
        })
 | 
					        })
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
            .await;
 | 
					    .await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Ok(())
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let (pool, lm) = get_ctx_data(&ctx).await;
 | 
					/// Get information about the bot
 | 
				
			||||||
 | 
					#[poise::command(slash_command)]
 | 
				
			||||||
 | 
					pub async fn info(ctx: Context<'_>) -> Result<(), Error> {
 | 
				
			||||||
 | 
					    let footer = footer(ctx);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let language = UserData::language_of(&msg.author, &pool);
 | 
					    let _ = ctx
 | 
				
			||||||
    let prefix = ctx.prefix(msg.guild_id);
 | 
					        .send(|m| {
 | 
				
			||||||
 | 
					            m.ephemeral(true).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(desc)
 | 
					                    .description(format!(
 | 
				
			||||||
 | 
					                        "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(())
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[command]
 | 
					/// Details on supporting the bot and Patreon benefits
 | 
				
			||||||
async fn donate(ctx: &Context, msg: &Message, _args: String) {
 | 
					#[poise::command(slash_command)]
 | 
				
			||||||
    let (pool, lm) = get_ctx_data(&ctx).await;
 | 
					pub async fn donate(ctx: Context<'_>) -> Result<(), Error> {
 | 
				
			||||||
 | 
					    let footer = footer(ctx);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let language = UserData::language_of(&msg.author, &pool).await;
 | 
					    ctx.send(|m| m.embed(|e| {
 | 
				
			||||||
    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(desc)
 | 
					            .description("Thinking of adding a monthly contribution?
 | 
				
			||||||
 | 
					Click below for my Patreon and official bot server :)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					**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 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("%H:%M").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);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    ctx.send(|m| {
 | 
				
			||||||
 | 
					        m.ephemeral(true).embed(|e| {
 | 
				
			||||||
 | 
					            e.title("Dashboard")
 | 
				
			||||||
 | 
					                .description("**https://reminder-bot.com/dashboard**")
 | 
				
			||||||
 | 
					                .footer(footer)
 | 
				
			||||||
 | 
					                .color(*THEME_COLOR)
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					    .await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Ok(())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// View the current time in your 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(())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// View the current time in a user's selected timezone
 | 
				
			||||||
 | 
					#[poise::command(context_menu_command = "View Local Time")]
 | 
				
			||||||
 | 
					pub async fn clock_context_menu(ctx: Context<'_>, user: serenity::User) -> Result<(), Error> {
 | 
				
			||||||
 | 
					    ctx.defer_ephemeral().await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let user_data = ctx.user_data(user.id).await?;
 | 
				
			||||||
 | 
					    let tz = user_data.timezone();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let now = Utc::now().with_timezone(&tz);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    ctx.send(|m| {
 | 
				
			||||||
 | 
					        m.ephemeral(true).content(format!(
 | 
				
			||||||
 | 
					            "Time in {}'s timezone: `{}`",
 | 
				
			||||||
 | 
					            user.mention(),
 | 
				
			||||||
 | 
					            now.format("%H:%M")
 | 
				
			||||||
 | 
					        ))
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					    .await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Ok(())
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,445 +1,351 @@
 | 
				
			|||||||
use regex_command_attr::command;
 | 
					use poise::CreateReply;
 | 
				
			||||||
 | 
					 | 
				
			||||||
use serenity::{
 | 
					 | 
				
			||||||
    async_trait,
 | 
					 | 
				
			||||||
    client::Context,
 | 
					 | 
				
			||||||
    constants::MESSAGE_CODE_LIMIT,
 | 
					 | 
				
			||||||
    model::{
 | 
					 | 
				
			||||||
        channel::Message,
 | 
					 | 
				
			||||||
        id::{ChannelId, GuildId, UserId},
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use std::fmt;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::{
 | 
					use crate::{
 | 
				
			||||||
    command_help, get_ctx_data,
 | 
					    component_models::{
 | 
				
			||||||
    models::{user_data::UserData, CtxGuildData},
 | 
					        pager::{Pager, TodoPager},
 | 
				
			||||||
};
 | 
					        ComponentDataModel, TodoSelector,
 | 
				
			||||||
use sqlx::MySqlPool;
 | 
					    },
 | 
				
			||||||
use std::convert::TryFrom;
 | 
					    consts::{EMBED_DESCRIPTION_MAX_LENGTH, SELECT_MAX_ENTRIES, THEME_COLOR},
 | 
				
			||||||
 | 
					    Context, Error,
 | 
				
			||||||
#[derive(Debug)]
 | 
					 | 
				
			||||||
struct TodoNotFound;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl std::error::Error for TodoNotFound {}
 | 
					 | 
				
			||||||
impl fmt::Display for TodoNotFound {
 | 
					 | 
				
			||||||
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
 | 
					 | 
				
			||||||
        write!(f, "Todo not found")
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
struct Todo {
 | 
					 | 
				
			||||||
    id: u32,
 | 
					 | 
				
			||||||
    value: String,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
struct TodoTarget {
 | 
					 | 
				
			||||||
    user: UserId,
 | 
					 | 
				
			||||||
    guild: Option<GuildId>,
 | 
					 | 
				
			||||||
    channel: Option<ChannelId>,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl TodoTarget {
 | 
					 | 
				
			||||||
    pub fn command(&self, subcommand_opt: Option<SubCommand>) -> String {
 | 
					 | 
				
			||||||
        let context = if self.channel.is_some() {
 | 
					 | 
				
			||||||
            "channel"
 | 
					 | 
				
			||||||
        } else if self.guild.is_some() {
 | 
					 | 
				
			||||||
            "guild"
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
            "user"
 | 
					 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if let Some(subcommand) = subcommand_opt {
 | 
					/// Manage todo lists
 | 
				
			||||||
            format!("todo {} {}", context, subcommand.to_string())
 | 
					#[poise::command(
 | 
				
			||||||
        } else {
 | 
					    slash_command,
 | 
				
			||||||
            format!("todo {}", context)
 | 
					    rename = "todo",
 | 
				
			||||||
        }
 | 
					    identifying_name = "todo_base",
 | 
				
			||||||
 | 
					    default_member_permissions = "MANAGE_GUILD"
 | 
				
			||||||
 | 
					)]
 | 
				
			||||||
 | 
					pub async fn todo_base(_ctx: Context<'_>) -> Result<(), Error> {
 | 
				
			||||||
 | 
					    Ok(())
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    pub fn name(&self) -> String {
 | 
					/// Manage the server todo list
 | 
				
			||||||
        if self.channel.is_some() {
 | 
					#[poise::command(
 | 
				
			||||||
            "Channel"
 | 
					    slash_command,
 | 
				
			||||||
        } else if self.guild.is_some() {
 | 
					    rename = "server",
 | 
				
			||||||
            "Guild"
 | 
					    guild_only = true,
 | 
				
			||||||
        } else {
 | 
					    identifying_name = "todo_guild_base",
 | 
				
			||||||
            "User"
 | 
					    default_member_permissions = "MANAGE_GUILD"
 | 
				
			||||||
        }
 | 
					)]
 | 
				
			||||||
        .to_string()
 | 
					pub async fn todo_guild_base(_ctx: Context<'_>) -> Result<(), Error> {
 | 
				
			||||||
 | 
					    Ok(())
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    pub async fn view(
 | 
					/// Add an item to the server todo list
 | 
				
			||||||
        &self,
 | 
					#[poise::command(
 | 
				
			||||||
        pool: MySqlPool,
 | 
					    slash_command,
 | 
				
			||||||
    ) -> Result<Vec<Todo>, Box<dyn std::error::Error + Send + Sync>> {
 | 
					    rename = "add",
 | 
				
			||||||
        Ok(if let Some(cid) = self.channel {
 | 
					    guild_only = true,
 | 
				
			||||||
            sqlx::query_as!(
 | 
					    identifying_name = "todo_guild_add",
 | 
				
			||||||
                Todo,
 | 
					    default_member_permissions = "MANAGE_GUILD"
 | 
				
			||||||
                "
 | 
					)]
 | 
				
			||||||
SELECT id, value FROM todos WHERE channel_id = (SELECT id FROM channels WHERE channel = ?)
 | 
					pub async fn todo_guild_add(
 | 
				
			||||||
                ",
 | 
					    ctx: Context<'_>,
 | 
				
			||||||
                cid.as_u64()
 | 
					    #[description = "The task to add to the todo list"] task: String,
 | 
				
			||||||
 | 
					) -> Result<(), Error> {
 | 
				
			||||||
 | 
					    sqlx::query!(
 | 
				
			||||||
 | 
					        "INSERT INTO todos (guild_id, value)
 | 
				
			||||||
 | 
					VALUES ((SELECT id FROM guilds WHERE guild = ?), ?)",
 | 
				
			||||||
 | 
					        ctx.guild_id().unwrap().0,
 | 
				
			||||||
 | 
					        task
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
            .fetch_all(&pool)
 | 
					    .execute(&ctx.data().database)
 | 
				
			||||||
            .await?
 | 
					    .await
 | 
				
			||||||
        } else if let Some(gid) = self.guild {
 | 
					    .unwrap();
 | 
				
			||||||
            sqlx::query_as!(
 | 
					
 | 
				
			||||||
                Todo,
 | 
					    ctx.say("Item added to todo list").await?;
 | 
				
			||||||
                "
 | 
					
 | 
				
			||||||
SELECT id, value FROM todos WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?) AND channel_id IS NULL
 | 
					    Ok(())
 | 
				
			||||||
                ",
 | 
					}
 | 
				
			||||||
                gid.as_u64()
 | 
					
 | 
				
			||||||
 | 
					/// View and remove from the server todo list
 | 
				
			||||||
 | 
					#[poise::command(
 | 
				
			||||||
 | 
					    slash_command,
 | 
				
			||||||
 | 
					    rename = "view",
 | 
				
			||||||
 | 
					    guild_only = true,
 | 
				
			||||||
 | 
					    identifying_name = "todo_guild_view",
 | 
				
			||||||
 | 
					    default_member_permissions = "MANAGE_GUILD"
 | 
				
			||||||
 | 
					)]
 | 
				
			||||||
 | 
					pub async fn todo_guild_view(ctx: Context<'_>) -> Result<(), Error> {
 | 
				
			||||||
 | 
					    let values = sqlx::query!(
 | 
				
			||||||
 | 
					        "SELECT todos.id, value FROM todos
 | 
				
			||||||
 | 
					INNER JOIN guilds ON todos.guild_id = guilds.id
 | 
				
			||||||
 | 
					WHERE guilds.guild = ?",
 | 
				
			||||||
 | 
					        ctx.guild_id().unwrap().0,
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
            .fetch_all(&pool)
 | 
					    .fetch_all(&ctx.data().database)
 | 
				
			||||||
            .await?
 | 
					    .await
 | 
				
			||||||
        } else {
 | 
					    .unwrap()
 | 
				
			||||||
            sqlx::query_as!(
 | 
					    .iter()
 | 
				
			||||||
                Todo,
 | 
					    .map(|row| (row.id as usize, row.value.clone()))
 | 
				
			||||||
                "
 | 
					    .collect::<Vec<(usize, String)>>();
 | 
				
			||||||
SELECT id, value FROM todos WHERE user_id = (SELECT id FROM users WHERE user = ?) AND guild_id IS NULL
 | 
					
 | 
				
			||||||
                ",
 | 
					    let resp = show_todo_page(&values, 0, None, None, ctx.guild_id().map(|g| g.0));
 | 
				
			||||||
                self.user.as_u64()
 | 
					
 | 
				
			||||||
            )
 | 
					    ctx.send(|r| {
 | 
				
			||||||
            .fetch_all(&pool)
 | 
					        *r = resp;
 | 
				
			||||||
            .await?
 | 
					        r
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    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!(
 | 
					 | 
				
			||||||
                "
 | 
					 | 
				
			||||||
INSERT INTO todos (user_id, guild_id, channel_id, value) VALUES (
 | 
					 | 
				
			||||||
    (SELECT id FROM users WHERE user = ?),
 | 
					 | 
				
			||||||
    (SELECT id FROM guilds WHERE guild = ?),
 | 
					 | 
				
			||||||
    (SELECT id FROM channels WHERE channel = ?),
 | 
					 | 
				
			||||||
    ?
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
                ",
 | 
					 | 
				
			||||||
                self.user.as_u64(),
 | 
					 | 
				
			||||||
                gid.as_u64(),
 | 
					 | 
				
			||||||
                cid.as_u64(),
 | 
					 | 
				
			||||||
                value
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
            .execute(&pool)
 | 
					 | 
				
			||||||
    .await?;
 | 
					    .await?;
 | 
				
			||||||
        } 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?;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Ok(())
 | 
					    Ok(())
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    pub async fn remove(
 | 
					/// Manage the channel todo list
 | 
				
			||||||
        &self,
 | 
					#[poise::command(
 | 
				
			||||||
        num: usize,
 | 
					    slash_command,
 | 
				
			||||||
        pool: &MySqlPool,
 | 
					    rename = "channel",
 | 
				
			||||||
    ) -> Result<Todo, Box<dyn std::error::Error + Sync + Send>> {
 | 
					    guild_only = true,
 | 
				
			||||||
        let todos = self.view(pool.clone()).await?;
 | 
					    identifying_name = "todo_channel_base",
 | 
				
			||||||
 | 
					    default_member_permissions = "MANAGE_GUILD"
 | 
				
			||||||
        if let Some(removal_todo) = todos.get(num) {
 | 
					)]
 | 
				
			||||||
            let deleting = sqlx::query_as!(
 | 
					pub async fn todo_channel_base(_ctx: Context<'_>) -> Result<(), Error> {
 | 
				
			||||||
                Todo,
 | 
					    Ok(())
 | 
				
			||||||
                "
 | 
					 | 
				
			||||||
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(
 | 
					/// Add an item to the channel todo list
 | 
				
			||||||
        &self,
 | 
					#[poise::command(
 | 
				
			||||||
        pool: &MySqlPool,
 | 
					    slash_command,
 | 
				
			||||||
    ) -> Result<(), Box<dyn std::error::Error + Sync + Send>> {
 | 
					    rename = "add",
 | 
				
			||||||
        if let Some(cid) = self.channel {
 | 
					    guild_only = true,
 | 
				
			||||||
 | 
					    identifying_name = "todo_channel_add",
 | 
				
			||||||
 | 
					    default_member_permissions = "MANAGE_GUILD"
 | 
				
			||||||
 | 
					)]
 | 
				
			||||||
 | 
					pub async fn todo_channel_add(
 | 
				
			||||||
 | 
					    ctx: Context<'_>,
 | 
				
			||||||
 | 
					    #[description = "The task to add to the todo list"] task: String,
 | 
				
			||||||
 | 
					) -> Result<(), Error> {
 | 
				
			||||||
    sqlx::query!(
 | 
					    sqlx::query!(
 | 
				
			||||||
                "
 | 
					        "INSERT INTO todos (guild_id, channel_id, value)
 | 
				
			||||||
DELETE FROM todos WHERE channel_id = (SELECT id FROM channels WHERE channel = ?)
 | 
					VALUES ((SELECT id FROM guilds WHERE guild = ?), (SELECT id FROM channels WHERE channel = ?), ?)",
 | 
				
			||||||
                ",
 | 
					        ctx.guild_id().unwrap().0,
 | 
				
			||||||
                cid.as_u64()
 | 
					        ctx.channel_id().0,
 | 
				
			||||||
 | 
					        task
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
            .execute(pool)
 | 
					    .execute(&ctx.data().database)
 | 
				
			||||||
            .await?;
 | 
					    .await
 | 
				
			||||||
        } else if let Some(gid) = self.guild {
 | 
					    .unwrap();
 | 
				
			||||||
            sqlx::query!(
 | 
					
 | 
				
			||||||
                "
 | 
					    ctx.say("Item added to todo list").await?;
 | 
				
			||||||
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(())
 | 
					    Ok(())
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async fn execute(&self, ctx: &Context, msg: &Message, subcommand: SubCommand, extra: String) {
 | 
					/// View and remove from the channel todo list
 | 
				
			||||||
        let (pool, lm) = get_ctx_data(&ctx).await;
 | 
					#[poise::command(
 | 
				
			||||||
 | 
					    slash_command,
 | 
				
			||||||
 | 
					    rename = "view",
 | 
				
			||||||
 | 
					    guild_only = true,
 | 
				
			||||||
 | 
					    identifying_name = "todo_channel_view",
 | 
				
			||||||
 | 
					    default_member_permissions = "MANAGE_GUILD"
 | 
				
			||||||
 | 
					)]
 | 
				
			||||||
 | 
					pub async fn todo_channel_view(ctx: Context<'_>) -> Result<(), Error> {
 | 
				
			||||||
 | 
					    let values = sqlx::query!(
 | 
				
			||||||
 | 
					        "SELECT todos.id, value FROM todos
 | 
				
			||||||
 | 
					INNER JOIN channels ON todos.channel_id = channels.id
 | 
				
			||||||
 | 
					WHERE channels.channel = ?",
 | 
				
			||||||
 | 
					        ctx.channel_id().0,
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    .fetch_all(&ctx.data().database)
 | 
				
			||||||
 | 
					    .await
 | 
				
			||||||
 | 
					    .unwrap()
 | 
				
			||||||
 | 
					    .iter()
 | 
				
			||||||
 | 
					    .map(|row| (row.id as usize, row.value.clone()))
 | 
				
			||||||
 | 
					    .collect::<Vec<(usize, String)>>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        let user_data = UserData::from_user(&msg.author, &ctx, &pool).await.unwrap();
 | 
					    let resp =
 | 
				
			||||||
        let prefix = ctx.prefix(msg.guild_id).await;
 | 
					        show_todo_page(&values, 0, None, Some(ctx.channel_id().0), ctx.guild_id().map(|g| g.0));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        match subcommand {
 | 
					    ctx.send(|r| {
 | 
				
			||||||
            SubCommand::View => {
 | 
					        *r = resp;
 | 
				
			||||||
                let todo_items = self.view(pool).await.unwrap();
 | 
					        r
 | 
				
			||||||
                let mut todo_groups = vec!["".to_string()];
 | 
					    })
 | 
				
			||||||
 | 
					    .await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Ok(())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Manage your personal todo list
 | 
				
			||||||
 | 
					#[poise::command(slash_command, rename = "user", identifying_name = "todo_user_base")]
 | 
				
			||||||
 | 
					pub async fn todo_user_base(_ctx: Context<'_>) -> Result<(), Error> {
 | 
				
			||||||
 | 
					    Ok(())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Add an item to your personal todo list
 | 
				
			||||||
 | 
					#[poise::command(slash_command, rename = "add", identifying_name = "todo_user_add")]
 | 
				
			||||||
 | 
					pub async fn todo_user_add(
 | 
				
			||||||
 | 
					    ctx: Context<'_>,
 | 
				
			||||||
 | 
					    #[description = "The task to add to the todo list"] task: String,
 | 
				
			||||||
 | 
					) -> Result<(), Error> {
 | 
				
			||||||
 | 
					    sqlx::query!(
 | 
				
			||||||
 | 
					        "INSERT INTO todos (user_id, value)
 | 
				
			||||||
 | 
					VALUES ((SELECT id FROM users WHERE user = ?), ?)",
 | 
				
			||||||
 | 
					        ctx.author().id.0,
 | 
				
			||||||
 | 
					        task
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    .execute(&ctx.data().database)
 | 
				
			||||||
 | 
					    .await
 | 
				
			||||||
 | 
					    .unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    ctx.say("Item added to todo list").await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Ok(())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// View and remove from your personal todo list
 | 
				
			||||||
 | 
					#[poise::command(slash_command, rename = "view", identifying_name = "todo_user_view")]
 | 
				
			||||||
 | 
					pub async fn todo_user_view(ctx: Context<'_>) -> Result<(), Error> {
 | 
				
			||||||
 | 
					    let values = sqlx::query!(
 | 
				
			||||||
 | 
					        "SELECT todos.id, value FROM todos
 | 
				
			||||||
 | 
					INNER JOIN users ON todos.user_id = users.id
 | 
				
			||||||
 | 
					WHERE users.user = ?",
 | 
				
			||||||
 | 
					        ctx.author().id.0,
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    .fetch_all(&ctx.data().database)
 | 
				
			||||||
 | 
					    .await
 | 
				
			||||||
 | 
					    .unwrap()
 | 
				
			||||||
 | 
					    .iter()
 | 
				
			||||||
 | 
					    .map(|row| (row.id as usize, row.value.clone()))
 | 
				
			||||||
 | 
					    .collect::<Vec<(usize, String)>>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let resp = show_todo_page(&values, 0, Some(ctx.author().id.0), None, None);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    ctx.send(|r| {
 | 
				
			||||||
 | 
					        *r = resp;
 | 
				
			||||||
 | 
					        r
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					    .await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Ok(())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub fn max_todo_page(todo_values: &[(usize, String)]) -> usize {
 | 
				
			||||||
 | 
					    let mut rows = 0;
 | 
				
			||||||
    let mut char_count = 0;
 | 
					    let mut char_count = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                todo_items.iter().enumerate().for_each(|(count, todo)| {
 | 
					    todo_values.iter().enumerate().map(|(c, (_, v))| format!("{}: {}", c, v)).fold(
 | 
				
			||||||
                    let display = format!("{}: {}\n", count + 1, todo.value);
 | 
					        1,
 | 
				
			||||||
 | 
					        |mut pages, text| {
 | 
				
			||||||
 | 
					            rows += 1;
 | 
				
			||||||
 | 
					            char_count += text.len();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    if char_count + display.len() > MESSAGE_CODE_LIMIT as usize {
 | 
					            if char_count > EMBED_DESCRIPTION_MAX_LENGTH || rows > SELECT_MAX_ENTRIES {
 | 
				
			||||||
                        char_count = display.len();
 | 
					                rows = 1;
 | 
				
			||||||
 | 
					                char_count = text.len();
 | 
				
			||||||
                        todo_groups.push(display);
 | 
					                pages += 1;
 | 
				
			||||||
                    } else {
 | 
					 | 
				
			||||||
                        char_count += display.len();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                        let last_group = todo_groups.pop().unwrap();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                        todo_groups.push(format!("{}{}", last_group, display));
 | 
					 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            pages
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub fn show_todo_page(
 | 
				
			||||||
 | 
					    todo_values: &[(usize, String)],
 | 
				
			||||||
 | 
					    page: usize,
 | 
				
			||||||
 | 
					    user_id: Option<u64>,
 | 
				
			||||||
 | 
					    channel_id: Option<u64>,
 | 
				
			||||||
 | 
					    guild_id: Option<u64>,
 | 
				
			||||||
 | 
					) -> CreateReply {
 | 
				
			||||||
 | 
					    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 {
 | 
				
			||||||
 | 
					        "Server"
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if todo_ids.is_empty() {
 | 
				
			||||||
 | 
					        let mut reply = CreateReply::default();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        reply.embed(|e| {
 | 
				
			||||||
 | 
					            e.title(format!("{} Todo List", title))
 | 
				
			||||||
 | 
					                .description("Todo List Empty!")
 | 
				
			||||||
 | 
					                .footer(|f| f.text(format!("Page {} of {}", page + 1, pages)))
 | 
				
			||||||
 | 
					                .color(*THEME_COLOR)
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                for group in todo_groups {
 | 
					        reply
 | 
				
			||||||
                    let _ = msg
 | 
					    } else {
 | 
				
			||||||
                        .channel_id
 | 
					        let todo_selector =
 | 
				
			||||||
                        .send_message(&ctx, |m| {
 | 
					            ComponentDataModel::TodoSelector(TodoSelector { page, user_id, channel_id, guild_id });
 | 
				
			||||||
                            m.embed(|e| e.title(format!("{} Todo", self.name())).description(group))
 | 
					
 | 
				
			||||||
 | 
					        let mut reply = CreateReply::default();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        reply
 | 
				
			||||||
 | 
					            .embed(|e| {
 | 
				
			||||||
 | 
					                e.title(format!("{} Todo List", title))
 | 
				
			||||||
 | 
					                    .description(display)
 | 
				
			||||||
 | 
					                    .footer(|f| f.text(format!("Page {} of {}", page + 1, pages)))
 | 
				
			||||||
 | 
					                    .color(*THEME_COLOR)
 | 
				
			||||||
            })
 | 
					            })
 | 
				
			||||||
                        .await;
 | 
					            .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)
 | 
				
			||||||
 | 
					                                });
 | 
				
			||||||
                            }
 | 
					                            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            SubCommand::Add => {
 | 
					                            opt
 | 
				
			||||||
                let content = lm
 | 
					                        })
 | 
				
			||||||
                    .get(&user_data.language, "todo/added")
 | 
					                    })
 | 
				
			||||||
                    .replacen("{name}", &extra, 1);
 | 
					                })
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                self.add(extra, pool).await.unwrap();
 | 
					        reply
 | 
				
			||||||
 | 
					 | 
				
			||||||
                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,
 | 
					 | 
				
			||||||
                        );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                        let _ = msg.channel_id.say(&ctx, content).await;
 | 
					 | 
				
			||||||
                    } else {
 | 
					 | 
				
			||||||
                        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,
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let subcommand_opt = SubCommand::try_from(split.next());
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    subcommand_opt
 | 
					 | 
				
			||||||
        .execute(ctx, msg, split.collect::<Vec<&str>>().join(" "), target)
 | 
					 | 
				
			||||||
        .await;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[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 {
 | 
					 | 
				
			||||||
                "todo"
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    command_help(ctx, msg, lm, &prefix.await, &language.await, command).await;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										353
									
								
								src/component_models/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,353 @@
 | 
				
			|||||||
 | 
					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::{
 | 
				
			||||||
 | 
					    commands::{
 | 
				
			||||||
 | 
					        moderation_cmds::{max_macro_page, show_macro_page},
 | 
				
			||||||
 | 
					        reminder_cmds::{max_delete_page, show_delete_page},
 | 
				
			||||||
 | 
					        todo_cmds::{max_todo_page, show_todo_page},
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    component_models::pager::{DelPager, LookPager, MacroPager, Pager, TodoPager},
 | 
				
			||||||
 | 
					    consts::{EMBED_DESCRIPTION_MAX_LENGTH, THEME_COLOR},
 | 
				
			||||||
 | 
					    models::reminder::Reminder,
 | 
				
			||||||
 | 
					    utils::send_as_initial_response,
 | 
				
			||||||
 | 
					    Data,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[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, data: &Data, 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(&data.database, 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.set_embeds(vec![embed]).components(|comp| {
 | 
				
			||||||
 | 
					                                    pager.create_button_row(pages, comp);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                                    comp
 | 
				
			||||||
 | 
					                                })
 | 
				
			||||||
 | 
					                            },
 | 
				
			||||||
 | 
					                        )
 | 
				
			||||||
 | 
					                    })
 | 
				
			||||||
 | 
					                    .await;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            ComponentDataModel::DelPager(pager) => {
 | 
				
			||||||
 | 
					                let reminders = Reminder::from_guild(
 | 
				
			||||||
 | 
					                    &ctx,
 | 
				
			||||||
 | 
					                    &data.database,
 | 
				
			||||||
 | 
					                    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 _ = component
 | 
				
			||||||
 | 
					                    .create_interaction_response(&ctx, |f| {
 | 
				
			||||||
 | 
					                        f.kind(InteractionResponseType::UpdateMessage).interaction_response_data(
 | 
				
			||||||
 | 
					                            |d| {
 | 
				
			||||||
 | 
					                                send_as_initial_response(resp, d);
 | 
				
			||||||
 | 
					                                d
 | 
				
			||||||
 | 
					                            },
 | 
				
			||||||
 | 
					                        )
 | 
				
			||||||
 | 
					                    })
 | 
				
			||||||
 | 
					                    .await;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            ComponentDataModel::DelSelector(selector) => {
 | 
				
			||||||
 | 
					                let selected_id = component.data.values.join(",");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                sqlx::query!("DELETE FROM reminders WHERE FIND_IN_SET(id, ?)", selected_id)
 | 
				
			||||||
 | 
					                    .execute(&data.database)
 | 
				
			||||||
 | 
					                    .await
 | 
				
			||||||
 | 
					                    .unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                let reminders = Reminder::from_guild(
 | 
				
			||||||
 | 
					                    &ctx,
 | 
				
			||||||
 | 
					                    &data.database,
 | 
				
			||||||
 | 
					                    component.guild_id,
 | 
				
			||||||
 | 
					                    component.user.id,
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					                .await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                let resp = show_delete_page(&reminders, selector.page, selector.timezone);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                let _ = component
 | 
				
			||||||
 | 
					                    .create_interaction_response(&ctx, |f| {
 | 
				
			||||||
 | 
					                        f.kind(InteractionResponseType::UpdateMessage).interaction_response_data(
 | 
				
			||||||
 | 
					                            |d| {
 | 
				
			||||||
 | 
					                                send_as_initial_response(resp, d);
 | 
				
			||||||
 | 
					                                d
 | 
				
			||||||
 | 
					                            },
 | 
				
			||||||
 | 
					                        )
 | 
				
			||||||
 | 
					                    })
 | 
				
			||||||
 | 
					                    .await;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            ComponentDataModel::TodoPager(pager) => {
 | 
				
			||||||
 | 
					                if Some(component.user.id.0) == pager.user_id || pager.user_id.is_none() {
 | 
				
			||||||
 | 
					                    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(&data.database)
 | 
				
			||||||
 | 
					                        .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(&data.database)
 | 
				
			||||||
 | 
					                        .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(&data.database)
 | 
				
			||||||
 | 
					                        .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 _ = component
 | 
				
			||||||
 | 
					                        .create_interaction_response(&ctx, |f| {
 | 
				
			||||||
 | 
					                            f.kind(InteractionResponseType::UpdateMessage)
 | 
				
			||||||
 | 
					                                .interaction_response_data(|d| {
 | 
				
			||||||
 | 
					                                    send_as_initial_response(resp, d);
 | 
				
			||||||
 | 
					                                    d
 | 
				
			||||||
 | 
					                                })
 | 
				
			||||||
 | 
					                        })
 | 
				
			||||||
 | 
					                        .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 selected_id = component.data.values.join(",");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    sqlx::query!("DELETE FROM todos WHERE FIND_IN_SET(id, ?)", selected_id)
 | 
				
			||||||
 | 
					                        .execute(&data.database)
 | 
				
			||||||
 | 
					                        .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(&data.database)
 | 
				
			||||||
 | 
					                .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 _ = component
 | 
				
			||||||
 | 
					                        .create_interaction_response(&ctx, |f| {
 | 
				
			||||||
 | 
					                            f.kind(InteractionResponseType::UpdateMessage)
 | 
				
			||||||
 | 
					                                .interaction_response_data(|d| {
 | 
				
			||||||
 | 
					                                    send_as_initial_response(resp, d);
 | 
				
			||||||
 | 
					                                    d
 | 
				
			||||||
 | 
					                                })
 | 
				
			||||||
 | 
					                        })
 | 
				
			||||||
 | 
					                        .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 macros = data.command_macros(component.guild_id.unwrap()).await.unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                let max_page = max_macro_page(¯os);
 | 
				
			||||||
 | 
					                let page = pager.next_page(max_page);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                let resp = show_macro_page(¯os, page);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                let _ = component
 | 
				
			||||||
 | 
					                    .create_interaction_response(&ctx, |f| {
 | 
				
			||||||
 | 
					                        f.kind(InteractionResponseType::UpdateMessage).interaction_response_data(
 | 
				
			||||||
 | 
					                            |d| {
 | 
				
			||||||
 | 
					                                send_as_initial_response(resp, d);
 | 
				
			||||||
 | 
					                                d
 | 
				
			||||||
 | 
					                            },
 | 
				
			||||||
 | 
					                        )
 | 
				
			||||||
 | 
					                    })
 | 
				
			||||||
 | 
					                    .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>,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										413
									
								
								src/component_models/pager.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,413 @@
 | 
				
			|||||||
 | 
					// 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 }),
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -2,49 +2,30 @@ 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 CHARACTERS: &str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_";
 | 
					pub const CHARACTERS: &str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const THEME_COLOR_FALLBACK: u32 = 0x8fb677;
 | 
					const THEME_COLOR_FALLBACK: u32 = 0x8fb677;
 | 
				
			||||||
 | 
					pub const MACRO_MAX_COMMANDS: usize = 5;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use std::{collections::HashSet, env, iter::FromIterator};
 | 
					use std::{collections::HashSet, env, iter::FromIterator};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use regex::{Regex, RegexBuilder};
 | 
					use poise::serenity::model::prelude::AttachmentType;
 | 
				
			||||||
 | 
					use regex::Regex;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
lazy_static! {
 | 
					lazy_static! {
 | 
				
			||||||
    pub static ref REGEX_CHANNEL: Regex = Regex::new(r#"^\s*<#(\d+)>\s*$"#).unwrap();
 | 
					    pub static ref DEFAULT_AVATAR: AttachmentType<'static> = (
 | 
				
			||||||
 | 
					        include_bytes!(concat!(
 | 
				
			||||||
    pub static ref REGEX_ROLE: Regex = Regex::new(r#"<@&(\d+)>"#).unwrap();
 | 
					            env!("CARGO_MANIFEST_DIR"),
 | 
				
			||||||
 | 
					            "/assets/",
 | 
				
			||||||
    pub static ref REGEX_COMMANDS: Regex = Regex::new(r#"([a-z]+)"#).unwrap();
 | 
					            env!("WEBHOOK_AVATAR", "WEBHOOK_AVATAR not provided for compilation")
 | 
				
			||||||
 | 
					        )) as &[u8],
 | 
				
			||||||
    pub static ref REGEX_ALIAS: Regex =
 | 
					        env!("WEBHOOK_AVATAR"),
 | 
				
			||||||
        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
 | 
				
			||||||
@@ -53,38 +34,23 @@ lazy_static! {
 | 
				
			|||||||
                .collect::<Vec<u64>>())
 | 
					                .collect::<Vec<u64>>())
 | 
				
			||||||
            .unwrap_or_else(|_| Vec::new())
 | 
					            .unwrap_or_else(|_| Vec::new())
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					    pub static ref CNC_GUILD: Option<u64> =
 | 
				
			||||||
    pub static ref CNC_GUILD: Option<u64> = env::var("CNC_GUILD")
 | 
					        env::var("CNC_GUILD").map(|var| var.parse::<u64>().ok()).ok().flatten();
 | 
				
			||||||
        .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")
 | 
				
			||||||
    pub static ref LOCAL_LANGUAGE: String =
 | 
					        .map_or(THEME_COLOR_FALLBACK, |inner| u32::from_str_radix(&inner, 16)
 | 
				
			||||||
        env::var("LOCAL_LANGUAGE").unwrap_or_else(|_| "EN".to_string());
 | 
					            .unwrap_or(THEME_COLOR_FALLBACK));
 | 
				
			||||||
 | 
					 | 
				
			||||||
    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());
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										126
									
								
								src/event_handlers.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,126 @@
 | 
				
			|||||||
 | 
					use std::{collections::HashMap, env, sync::atomic::Ordering};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use log::{error, info, warn};
 | 
				
			||||||
 | 
					use poise::{
 | 
				
			||||||
 | 
					    serenity::{model::interactions::Interaction, utils::shard_id},
 | 
				
			||||||
 | 
					    serenity_prelude as serenity,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::{component_models::ComponentDataModel, Data, Error};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub async fn listener(
 | 
				
			||||||
 | 
					    ctx: &serenity::Context,
 | 
				
			||||||
 | 
					    event: &poise::Event<'_>,
 | 
				
			||||||
 | 
					    data: &Data,
 | 
				
			||||||
 | 
					) -> Result<(), Error> {
 | 
				
			||||||
 | 
					    match event {
 | 
				
			||||||
 | 
					        poise::Event::CacheReady { .. } => {
 | 
				
			||||||
 | 
					            info!("Cache Ready! Preparing extra processes");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if !data.is_loop_running.load(Ordering::Relaxed) {
 | 
				
			||||||
 | 
					                let kill_tx = data.broadcast.clone();
 | 
				
			||||||
 | 
					                let kill_recv = data.broadcast.subscribe();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                let ctx1 = ctx.clone();
 | 
				
			||||||
 | 
					                let ctx2 = ctx.clone();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                let pool1 = data.database.clone();
 | 
				
			||||||
 | 
					                let pool2 = data.database.clone();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                let run_settings = env::var("DONTRUN").unwrap_or_else(|_| "".to_string());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if !run_settings.contains("postman") {
 | 
				
			||||||
 | 
					                    tokio::spawn(async move {
 | 
				
			||||||
 | 
					                        match postman::initialize(kill_recv, ctx1, &pool1).await {
 | 
				
			||||||
 | 
					                            Ok(_) => {}
 | 
				
			||||||
 | 
					                            Err(e) => {
 | 
				
			||||||
 | 
					                                error!("postman exiting: {}", e);
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					                        };
 | 
				
			||||||
 | 
					                    });
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    warn!("Not running postman")
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if !run_settings.contains("web") {
 | 
				
			||||||
 | 
					                    tokio::spawn(async move {
 | 
				
			||||||
 | 
					                        reminder_web::initialize(kill_tx, ctx2, pool2).await.unwrap();
 | 
				
			||||||
 | 
					                    });
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    warn!("Not running web")
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                data.is_loop_running.swap(true, Ordering::Relaxed);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        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, .. } => {
 | 
				
			||||||
 | 
					            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, data, component).await;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            _ => {}
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        _ => {}
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Ok(())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										500
									
								
								src/framework.rs
									
									
									
									
									
								
							
							
						
						@@ -1,500 +0,0 @@
 | 
				
			|||||||
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, MessageId},
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    Result as SerenityResult,
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use log::{error, info, warn};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use regex::{Match, Regex, RegexBuilder};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use std::{collections::HashMap, fmt};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use crate::{
 | 
					 | 
				
			||||||
    language_manager::LanguageManager,
 | 
					 | 
				
			||||||
    models::{channel_data::ChannelData, guild_data::GuildData, user_data::UserData, CtxGuildData},
 | 
					 | 
				
			||||||
    LimitExecutors, SQLPool,
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
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_key(|a| 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_key(|a| 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
 | 
					 | 
				
			||||||
                                                    .unwrap_or_else(|_| {
 | 
					 | 
				
			||||||
                                                        panic!(
 | 
					 | 
				
			||||||
                                                        "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;
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
							
								
								
									
										100
									
								
								src/hooks.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,100 @@
 | 
				
			|||||||
 | 
					use poise::serenity::model::channel::Channel;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::{consts::MACRO_MAX_COMMANDS, models::command_macro::RecordedCommand, Context, Error};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async fn macro_check(ctx: Context<'_>) -> bool {
 | 
				
			||||||
 | 
					    if let Context::Application(app_ctx) = ctx {
 | 
				
			||||||
 | 
					        if let Some(guild_id) = ctx.guild_id() {
 | 
				
			||||||
 | 
					            if ctx.command().identifying_name != "finish_macro" {
 | 
				
			||||||
 | 
					                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(
 | 
				
			||||||
 | 
					                                    format!("{} commands already recorded. Please use `/macro finish` to end recording.", MACRO_MAX_COMMANDS),
 | 
				
			||||||
 | 
					                                )
 | 
				
			||||||
 | 
					                            })
 | 
				
			||||||
 | 
					                            .await;
 | 
				
			||||||
 | 
					                    } else {
 | 
				
			||||||
 | 
					                        let recorded = RecordedCommand {
 | 
				
			||||||
 | 
					                            action: None,
 | 
				
			||||||
 | 
					                            command_name: ctx.command().identifying_name.clone(),
 | 
				
			||||||
 | 
					                            options: Vec::from(app_ctx.args),
 | 
				
			||||||
 | 
					                        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        command_macro.commands.push(recorded);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        let _ = ctx
 | 
				
			||||||
 | 
					                            .send(|m| m.ephemeral(true).content("Command recorded to macro"))
 | 
				
			||||||
 | 
					                            .await;
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    false
 | 
				
			||||||
 | 
					                } 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.view_channel(), 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)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										251
									
								
								src/interval_parser.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,251 @@
 | 
				
			|||||||
 | 
					/*
 | 
				
			||||||
 | 
					With modifications, 2022 Jude Southworth
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Original copyright notice:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Copyright 2021 Paul Colomiets
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Permission is hereby granted, free of charge, to any person obtaining a copy of this software
 | 
				
			||||||
 | 
					and associated documentation files (the "Software"), to deal in the Software without restriction,
 | 
				
			||||||
 | 
					including without limitation the rights to use, copy, modify, merge, publish, distribute,
 | 
				
			||||||
 | 
					sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
 | 
				
			||||||
 | 
					furnished to do so, subject to the following conditions:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					The above copyright notice and this permission notice shall be included in all copies or
 | 
				
			||||||
 | 
					substantial portions of the Software.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
 | 
				
			||||||
 | 
					BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 | 
				
			||||||
 | 
					NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
 | 
				
			||||||
 | 
					DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 | 
				
			||||||
 | 
					OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 | 
				
			||||||
 | 
					*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use std::{error::Error as StdError, fmt, str::Chars};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Error parsing human-friendly duration
 | 
				
			||||||
 | 
					#[derive(Debug, PartialEq, Clone)]
 | 
				
			||||||
 | 
					pub enum Error {
 | 
				
			||||||
 | 
					    /// Invalid character during parsing
 | 
				
			||||||
 | 
					    ///
 | 
				
			||||||
 | 
					    /// More specifically anything that is not alphanumeric is prohibited
 | 
				
			||||||
 | 
					    ///
 | 
				
			||||||
 | 
					    /// The field is an byte offset of the character in the string.
 | 
				
			||||||
 | 
					    InvalidCharacter(usize),
 | 
				
			||||||
 | 
					    /// Non-numeric value where number is expected
 | 
				
			||||||
 | 
					    ///
 | 
				
			||||||
 | 
					    /// This usually means that either time unit is broken into words,
 | 
				
			||||||
 | 
					    /// e.g. `m sec` instead of `msec`, or just number is omitted,
 | 
				
			||||||
 | 
					    /// for example `2 hours min` instead of `2 hours 1 min`
 | 
				
			||||||
 | 
					    ///
 | 
				
			||||||
 | 
					    /// The field is an byte offset of the errorneous character
 | 
				
			||||||
 | 
					    /// in the string.
 | 
				
			||||||
 | 
					    NumberExpected(usize),
 | 
				
			||||||
 | 
					    /// Unit in the number is not one of allowed units
 | 
				
			||||||
 | 
					    ///
 | 
				
			||||||
 | 
					    /// See documentation of `parse_duration` for the list of supported
 | 
				
			||||||
 | 
					    /// time units.
 | 
				
			||||||
 | 
					    ///
 | 
				
			||||||
 | 
					    /// The two fields are start and end (exclusive) of the slice from
 | 
				
			||||||
 | 
					    /// the original string, containing errorneous value
 | 
				
			||||||
 | 
					    UnknownUnit {
 | 
				
			||||||
 | 
					        /// Start of the invalid unit inside the original string
 | 
				
			||||||
 | 
					        start: usize,
 | 
				
			||||||
 | 
					        /// End of the invalid unit inside the original string
 | 
				
			||||||
 | 
					        end: usize,
 | 
				
			||||||
 | 
					        /// The unit verbatim
 | 
				
			||||||
 | 
					        unit: String,
 | 
				
			||||||
 | 
					        /// A number associated with the unit
 | 
				
			||||||
 | 
					        value: u64,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    /// The numeric value is too large
 | 
				
			||||||
 | 
					    ///
 | 
				
			||||||
 | 
					    /// Usually this means value is too large to be useful. If user writes
 | 
				
			||||||
 | 
					    /// data in subsecond units, then the maximum is about 3k years. When
 | 
				
			||||||
 | 
					    /// using seconds, or larger units, the limit is even larger.
 | 
				
			||||||
 | 
					    NumberOverflow,
 | 
				
			||||||
 | 
					    /// The value was an empty string (or consists only whitespace)
 | 
				
			||||||
 | 
					    Empty,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl StdError for Error {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl fmt::Display for Error {
 | 
				
			||||||
 | 
					    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
 | 
				
			||||||
 | 
					        match self {
 | 
				
			||||||
 | 
					            Error::InvalidCharacter(offset) => write!(f, "invalid character at {}", offset),
 | 
				
			||||||
 | 
					            Error::NumberExpected(offset) => write!(f, "expected number at {}", offset),
 | 
				
			||||||
 | 
					            Error::UnknownUnit { unit, value, .. } if &unit == &"" => {
 | 
				
			||||||
 | 
					                write!(f, "time unit needed, for example {0}sec or {0}ms", value,)
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            Error::UnknownUnit { unit, .. } => {
 | 
				
			||||||
 | 
					                write!(
 | 
				
			||||||
 | 
					                    f,
 | 
				
			||||||
 | 
					                    "unknown time unit {:?}, \
 | 
				
			||||||
 | 
					                    supported units: ns, us, ms, sec, min, hours, days, \
 | 
				
			||||||
 | 
					                    weeks, months, years (and few variations)",
 | 
				
			||||||
 | 
					                    unit
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            Error::NumberOverflow => write!(f, "number is too large"),
 | 
				
			||||||
 | 
					            Error::Empty => write!(f, "value was empty"),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					trait OverflowOp: Sized {
 | 
				
			||||||
 | 
					    fn mul(self, other: Self) -> Result<Self, Error>;
 | 
				
			||||||
 | 
					    fn add(self, other: Self) -> Result<Self, Error>;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl OverflowOp for u64 {
 | 
				
			||||||
 | 
					    fn mul(self, other: Self) -> Result<Self, Error> {
 | 
				
			||||||
 | 
					        self.checked_mul(other).ok_or(Error::NumberOverflow)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    fn add(self, other: Self) -> Result<Self, Error> {
 | 
				
			||||||
 | 
					        self.checked_add(other).ok_or(Error::NumberOverflow)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Copy, Clone)]
 | 
				
			||||||
 | 
					pub struct Interval {
 | 
				
			||||||
 | 
					    pub month: u64,
 | 
				
			||||||
 | 
					    pub sec: u64,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					struct Parser<'a> {
 | 
				
			||||||
 | 
					    iter: Chars<'a>,
 | 
				
			||||||
 | 
					    src: &'a str,
 | 
				
			||||||
 | 
					    current: (u64, u64, u64),
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl<'a> Parser<'a> {
 | 
				
			||||||
 | 
					    fn off(&self) -> usize {
 | 
				
			||||||
 | 
					        self.src.len() - self.iter.as_str().len()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn parse_first_char(&mut self) -> Result<Option<u64>, Error> {
 | 
				
			||||||
 | 
					        let off = self.off();
 | 
				
			||||||
 | 
					        for c in self.iter.by_ref() {
 | 
				
			||||||
 | 
					            match c {
 | 
				
			||||||
 | 
					                '0'..='9' => {
 | 
				
			||||||
 | 
					                    return Ok(Some(c as u64 - '0' as u64));
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                c if c.is_whitespace() => continue,
 | 
				
			||||||
 | 
					                _ => {
 | 
				
			||||||
 | 
					                    return Err(Error::NumberExpected(off));
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        Ok(None)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    fn parse_unit(&mut self, n: u64, start: usize, end: usize) -> Result<(), Error> {
 | 
				
			||||||
 | 
					        let (mut month, mut sec, nsec) = match &self.src[start..end] {
 | 
				
			||||||
 | 
					            "nanos" | "nsec" | "ns" => (0u64, 0u64, n),
 | 
				
			||||||
 | 
					            "usec" | "us" => (0, 0u64, n.mul(1000)?),
 | 
				
			||||||
 | 
					            "millis" | "msec" | "ms" => (0, 0u64, n.mul(1_000_000)?),
 | 
				
			||||||
 | 
					            "seconds" | "second" | "secs" | "sec" | "s" => (0, n, 0),
 | 
				
			||||||
 | 
					            "minutes" | "minute" | "min" | "mins" | "m" => (0, n.mul(60)?, 0),
 | 
				
			||||||
 | 
					            "hours" | "hour" | "hr" | "hrs" | "h" => (0, n.mul(3600)?, 0),
 | 
				
			||||||
 | 
					            "days" | "day" | "d" => (0, n.mul(86400)?, 0),
 | 
				
			||||||
 | 
					            "weeks" | "week" | "w" => (0, n.mul(86400 * 7)?, 0),
 | 
				
			||||||
 | 
					            "months" | "month" | "M" => (n, 0, 0),
 | 
				
			||||||
 | 
					            "years" | "year" | "y" => (12, 0, 0),
 | 
				
			||||||
 | 
					            _ => {
 | 
				
			||||||
 | 
					                return Err(Error::UnknownUnit {
 | 
				
			||||||
 | 
					                    start,
 | 
				
			||||||
 | 
					                    end,
 | 
				
			||||||
 | 
					                    unit: self.src[start..end].to_string(),
 | 
				
			||||||
 | 
					                    value: n,
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        let mut nsec = self.current.2 + nsec;
 | 
				
			||||||
 | 
					        if nsec > 1_000_000_000 {
 | 
				
			||||||
 | 
					            sec = sec + nsec / 1_000_000_000;
 | 
				
			||||||
 | 
					            nsec %= 1_000_000_000;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        sec = self.current.1 + sec;
 | 
				
			||||||
 | 
					        month = self.current.0 + month;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.current = (month, sec, nsec);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Ok(())
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn parse(mut self) -> Result<Interval, Error> {
 | 
				
			||||||
 | 
					        let mut n = self.parse_first_char()?.ok_or(Error::Empty)?;
 | 
				
			||||||
 | 
					        'outer: loop {
 | 
				
			||||||
 | 
					            let mut off = self.off();
 | 
				
			||||||
 | 
					            while let Some(c) = self.iter.next() {
 | 
				
			||||||
 | 
					                match c {
 | 
				
			||||||
 | 
					                    '0'..='9' => {
 | 
				
			||||||
 | 
					                        n = n
 | 
				
			||||||
 | 
					                            .checked_mul(10)
 | 
				
			||||||
 | 
					                            .and_then(|x| x.checked_add(c as u64 - '0' as u64))
 | 
				
			||||||
 | 
					                            .ok_or(Error::NumberOverflow)?;
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    c if c.is_whitespace() => {}
 | 
				
			||||||
 | 
					                    'a'..='z' | 'A'..='Z' => {
 | 
				
			||||||
 | 
					                        break;
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    _ => {
 | 
				
			||||||
 | 
					                        return Err(Error::InvalidCharacter(off));
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                off = self.off();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            let start = off;
 | 
				
			||||||
 | 
					            let mut off = self.off();
 | 
				
			||||||
 | 
					            while let Some(c) = self.iter.next() {
 | 
				
			||||||
 | 
					                match c {
 | 
				
			||||||
 | 
					                    '0'..='9' => {
 | 
				
			||||||
 | 
					                        self.parse_unit(n, start, off)?;
 | 
				
			||||||
 | 
					                        n = c as u64 - '0' as u64;
 | 
				
			||||||
 | 
					                        continue 'outer;
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    c if c.is_whitespace() => break,
 | 
				
			||||||
 | 
					                    'a'..='z' | 'A'..='Z' => {}
 | 
				
			||||||
 | 
					                    _ => {
 | 
				
			||||||
 | 
					                        return Err(Error::InvalidCharacter(off));
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                off = self.off();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            self.parse_unit(n, start, off)?;
 | 
				
			||||||
 | 
					            n = match self.parse_first_char()? {
 | 
				
			||||||
 | 
					                Some(n) => n,
 | 
				
			||||||
 | 
					                None => return Ok(Interval { month: self.current.0, sec: self.current.1 }),
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Parse duration object `1hour 12min 5s`
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					/// The duration object is a concatenation of time spans. Where each time
 | 
				
			||||||
 | 
					/// span is an integer number and a suffix. Supported suffixes:
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					/// * `nsec`, `ns` -- nanoseconds
 | 
				
			||||||
 | 
					/// * `usec`, `us` -- microseconds
 | 
				
			||||||
 | 
					/// * `msec`, `ms` -- milliseconds
 | 
				
			||||||
 | 
					/// * `seconds`, `second`, `sec`, `s`
 | 
				
			||||||
 | 
					/// * `minutes`, `minute`, `min`, `m`
 | 
				
			||||||
 | 
					/// * `hours`, `hour`, `hr`, `h`
 | 
				
			||||||
 | 
					/// * `days`, `day`, `d`
 | 
				
			||||||
 | 
					/// * `weeks`, `week`, `w`
 | 
				
			||||||
 | 
					/// * `months`, `month`, `M` -- defined as 30.44 days
 | 
				
			||||||
 | 
					/// * `years`, `year`, `y` -- defined as 365.25 days
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					/// # Examples
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					/// ```
 | 
				
			||||||
 | 
					/// use std::time::Duration;
 | 
				
			||||||
 | 
					/// use humantime::parse_duration;
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					/// assert_eq!(parse_duration("2h 37min"), Ok(Duration::new(9420, 0)));
 | 
				
			||||||
 | 
					/// assert_eq!(parse_duration("32ms"), Ok(Duration::new(0, 32_000_000)));
 | 
				
			||||||
 | 
					/// ```
 | 
				
			||||||
 | 
					pub fn parse_duration(s: &str) -> Result<Interval, Error> {
 | 
				
			||||||
 | 
					    Parser { iter: s.chars(), src: s, current: (0, 0, 0) }.parse()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,65 +0,0 @@
 | 
				
			|||||||
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)?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        Ok(new)
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    pub fn get(&self, language: &str, name: &str) -> &str {
 | 
					 | 
				
			||||||
        self.strings
 | 
					 | 
				
			||||||
            .get(language)
 | 
					 | 
				
			||||||
            .map(|sm| sm.get(name))
 | 
					 | 
				
			||||||
            .unwrap_or_else(|| panic!(r#"Language does not exist: "{}""#, language))
 | 
					 | 
				
			||||||
            .unwrap_or_else(|| {
 | 
					 | 
				
			||||||
                self.strings
 | 
					 | 
				
			||||||
                    .get(&*LOCAL_LANGUAGE)
 | 
					 | 
				
			||||||
                    .map(|sm| {
 | 
					 | 
				
			||||||
                        sm.get(name)
 | 
					 | 
				
			||||||
                            .unwrap_or_else(|| panic!(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>;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
							
								
								
									
										729
									
								
								src/main.rs
									
									
									
									
									
								
							
							
						
						@@ -1,633 +1,202 @@
 | 
				
			|||||||
 | 
					#![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 framework;
 | 
					mod event_handlers;
 | 
				
			||||||
mod language_manager;
 | 
					mod hooks;
 | 
				
			||||||
 | 
					mod interval_parser;
 | 
				
			||||||
mod models;
 | 
					mod models;
 | 
				
			||||||
mod time_parser;
 | 
					mod time_parser;
 | 
				
			||||||
 | 
					mod utils;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use serenity::{
 | 
					use std::{
 | 
				
			||||||
    async_trait,
 | 
					    collections::HashMap,
 | 
				
			||||||
    cache::Cache,
 | 
					    env,
 | 
				
			||||||
    client::{bridge::gateway::GatewayIntents, Client},
 | 
					    error::Error as StdError,
 | 
				
			||||||
    futures::TryFutureExt,
 | 
					    fmt::{Debug, Display, Formatter},
 | 
				
			||||||
    http::{client::Http, CacheHttp},
 | 
					    sync::atomic::AtomicBool,
 | 
				
			||||||
    model::{
 | 
					 | 
				
			||||||
        channel::GuildChannel,
 | 
					 | 
				
			||||||
        channel::Message,
 | 
					 | 
				
			||||||
        guild::{Guild, GuildUnavailable},
 | 
					 | 
				
			||||||
        id::{GuildId, UserId},
 | 
					 | 
				
			||||||
        interactions::{Interaction, InteractionData, InteractionType},
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    prelude::{Context, EventHandler, TypeMapKey},
 | 
					 | 
				
			||||||
    utils::shard_id,
 | 
					 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use sqlx::mysql::MySqlPool;
 | 
					use chrono_tz::Tz;
 | 
				
			||||||
 | 
					 | 
				
			||||||
use dotenv::dotenv;
 | 
					use dotenv::dotenv;
 | 
				
			||||||
 | 
					use poise::serenity::model::{
 | 
				
			||||||
use std::{collections::HashMap, env, sync::Arc, time::Instant};
 | 
					    gateway::{Activity, GatewayIntents},
 | 
				
			||||||
 | 
					    id::{GuildId, UserId},
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					use sqlx::{MySql, Pool};
 | 
				
			||||||
 | 
					use tokio::sync::{broadcast, broadcast::Sender, RwLock};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::{
 | 
					use crate::{
 | 
				
			||||||
    commands::{info_cmds, moderation_cmds, reminder_cmds, todo_cmds},
 | 
					    commands::{info_cmds, moderation_cmds, reminder_cmds, todo_cmds},
 | 
				
			||||||
    consts::{CNC_GUILD, DEFAULT_PREFIX, SUBSCRIPTION_ROLES, THEME_COLOR},
 | 
					    consts::THEME_COLOR,
 | 
				
			||||||
    framework::RegexFramework,
 | 
					    event_handlers::listener,
 | 
				
			||||||
    language_manager::LanguageManager,
 | 
					    hooks::all_checks,
 | 
				
			||||||
    models::{guild_data::GuildData, user_data::UserData},
 | 
					    models::command_macro::CommandMacro,
 | 
				
			||||||
 | 
					    utils::register_application_commands,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use inflector::Inflector;
 | 
					type Database = MySql;
 | 
				
			||||||
use log::info;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
use dashmap::DashMap;
 | 
					type Error = Box<dyn std::error::Error + Send + Sync>;
 | 
				
			||||||
 | 
					type Context<'a> = poise::Context<'a, Data, Error>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use tokio::sync::RwLock;
 | 
					pub struct Data {
 | 
				
			||||||
 | 
					    database: Pool<Database>,
 | 
				
			||||||
use crate::models::reminder::{Reminder, ReminderAction};
 | 
					    http: reqwest::Client,
 | 
				
			||||||
use chrono::Utc;
 | 
					    recording_macros: RwLock<HashMap<(GuildId, UserId), CommandMacro<Data, Error>>>,
 | 
				
			||||||
use chrono_tz::Tz;
 | 
					    popular_timezones: Vec<Tz>,
 | 
				
			||||||
use serenity::model::prelude::{
 | 
					    is_loop_running: AtomicBool,
 | 
				
			||||||
    InteractionApplicationCommandCallbackDataFlags, InteractionResponseType,
 | 
					    broadcast: Sender<()>,
 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
struct GuildDataCache;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl TypeMapKey for GuildDataCache {
 | 
					 | 
				
			||||||
    type Value = Arc<DashMap<GuildId, Arc<RwLock<GuildData>>>>;
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
struct SQLPool;
 | 
					impl std::fmt::Debug for Data {
 | 
				
			||||||
 | 
					    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
 | 
				
			||||||
impl TypeMapKey for SQLPool {
 | 
					        write!(f, "Data {{ .. }}")
 | 
				
			||||||
    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;
 | 
					struct Ended;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[async_trait]
 | 
					impl Debug for Ended {
 | 
				
			||||||
impl EventHandler for Handler {
 | 
					    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
 | 
				
			||||||
    async fn channel_delete(&self, ctx: Context, channel: &GuildChannel) {
 | 
					        f.write_str("Process ended.")
 | 
				
			||||||
        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
 | 
					 | 
				
			||||||
                    .unwrap_or_else(|_| {
 | 
					 | 
				
			||||||
                        panic!("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(
 | 
					impl Display for Ended {
 | 
				
			||||||
        &self,
 | 
					    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
 | 
				
			||||||
        ctx: Context,
 | 
					        f.write_str("Process ended.")
 | 
				
			||||||
        deleted_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(&deleted_guild.id);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        sqlx::query!(
 | 
					 | 
				
			||||||
            "
 | 
					 | 
				
			||||||
DELETE FROM guilds WHERE guild = ?
 | 
					 | 
				
			||||||
            ",
 | 
					 | 
				
			||||||
            deleted_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)
 | 
					 | 
				
			||||||
                {
 | 
					 | 
				
			||||||
                    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("%H:%M").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"),
 | 
					 | 
				
			||||||
                                                )
 | 
					 | 
				
			||||||
                                            })
 | 
					 | 
				
			||||||
                                            .flags(InteractionApplicationCommandCallbackDataFlags::EPHEMERAL)
 | 
					 | 
				
			||||||
                                        })
 | 
					 | 
				
			||||||
                                })
 | 
					 | 
				
			||||||
                                .await;
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
                    } else {
 | 
					 | 
				
			||||||
                        match Reminder::from_interaction(&ctx, member.user.id, data.custom_id).await
 | 
					 | 
				
			||||||
                        {
 | 
					 | 
				
			||||||
                            Ok((reminder, action)) => {
 | 
					 | 
				
			||||||
                                let response = match action {
 | 
					 | 
				
			||||||
                                    ReminderAction::Delete => {
 | 
					 | 
				
			||||||
                                        reminder.delete(&ctx).await;
 | 
					 | 
				
			||||||
                                        "Reminder has been deleted"
 | 
					 | 
				
			||||||
                                    }
 | 
					 | 
				
			||||||
                                };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                                let _ = interaction
 | 
					 | 
				
			||||||
                                    .create_interaction_response(&ctx, |r| {
 | 
					 | 
				
			||||||
                                        r.kind(InteractionResponseType::ChannelMessageWithSource)
 | 
					 | 
				
			||||||
                                            .interaction_response_data(|d| d
 | 
					 | 
				
			||||||
                                                .content(response)
 | 
					 | 
				
			||||||
                                                .flags(InteractionApplicationCommandCallbackDataFlags::EPHEMERAL)
 | 
					 | 
				
			||||||
                                            )
 | 
					 | 
				
			||||||
                                    })
 | 
					 | 
				
			||||||
                                    .await;
 | 
					 | 
				
			||||||
                            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                            Err(ie) => {
 | 
					 | 
				
			||||||
                                let _ = interaction
 | 
					 | 
				
			||||||
                                    .create_interaction_response(&ctx, |r| {
 | 
					 | 
				
			||||||
                                        r.kind(InteractionResponseType::ChannelMessageWithSource)
 | 
					 | 
				
			||||||
                                            .interaction_response_data(|d| d
 | 
					 | 
				
			||||||
                                                .content(ie.to_string())
 | 
					 | 
				
			||||||
                                                .flags(InteractionApplicationCommandCallbackDataFlags::EPHEMERAL)
 | 
					 | 
				
			||||||
                                            )
 | 
					 | 
				
			||||||
                                    })
 | 
					 | 
				
			||||||
                                    .await;
 | 
					 | 
				
			||||||
                            }
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            _ => {}
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl StdError for Ended {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[tokio::main]
 | 
					#[tokio::main]
 | 
				
			||||||
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
 | 
					async fn main() -> Result<(), Box<dyn StdError + Send + Sync>> {
 | 
				
			||||||
 | 
					    let (tx, mut rx) = broadcast::channel(16);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    tokio::select! {
 | 
				
			||||||
 | 
					        output = _main(tx) => output,
 | 
				
			||||||
 | 
					        _ = rx.recv() => Err(Box::new(Ended) as Box<dyn StdError + Send + Sync>)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async fn _main(tx: Sender<()>) -> Result<(), Box<dyn StdError + Send + Sync>> {
 | 
				
			||||||
    env_logger::init();
 | 
					    env_logger::init();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    dotenv()?;
 | 
					    dotenv()?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let token = env::var("DISCORD_TOKEN").expect("Missing DISCORD_TOKEN from environment");
 | 
					    let discord_token = env::var("DISCORD_TOKEN").expect("Missing DISCORD_TOKEN from environment");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let http = Http::new_with_token(&token);
 | 
					    let options = poise::FrameworkOptions {
 | 
				
			||||||
 | 
					        commands: vec![
 | 
				
			||||||
 | 
					            info_cmds::help(),
 | 
				
			||||||
 | 
					            info_cmds::info(),
 | 
				
			||||||
 | 
					            info_cmds::donate(),
 | 
				
			||||||
 | 
					            info_cmds::clock(),
 | 
				
			||||||
 | 
					            info_cmds::clock_context_menu(),
 | 
				
			||||||
 | 
					            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()
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            reminder_cmds::pause(),
 | 
				
			||||||
 | 
					            reminder_cmds::offset(),
 | 
				
			||||||
 | 
					            reminder_cmds::nudge(),
 | 
				
			||||||
 | 
					            reminder_cmds::look(),
 | 
				
			||||||
 | 
					            reminder_cmds::delete(),
 | 
				
			||||||
 | 
					            poise::Command {
 | 
				
			||||||
 | 
					                subcommands: vec![
 | 
				
			||||||
 | 
					                    reminder_cmds::list_timer(),
 | 
				
			||||||
 | 
					                    reminder_cmds::start_timer(),
 | 
				
			||||||
 | 
					                    reminder_cmds::delete_timer(),
 | 
				
			||||||
 | 
					                ],
 | 
				
			||||||
 | 
					                ..reminder_cmds::timer_base()
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            reminder_cmds::remind(),
 | 
				
			||||||
 | 
					            poise::Command {
 | 
				
			||||||
 | 
					                subcommands: vec![
 | 
				
			||||||
 | 
					                    poise::Command {
 | 
				
			||||||
 | 
					                        subcommands: vec![
 | 
				
			||||||
 | 
					                            todo_cmds::todo_guild_add(),
 | 
				
			||||||
 | 
					                            todo_cmds::todo_guild_view(),
 | 
				
			||||||
 | 
					                        ],
 | 
				
			||||||
 | 
					                        ..todo_cmds::todo_guild_base()
 | 
				
			||||||
 | 
					                    },
 | 
				
			||||||
 | 
					                    poise::Command {
 | 
				
			||||||
 | 
					                        subcommands: vec![
 | 
				
			||||||
 | 
					                            todo_cmds::todo_channel_add(),
 | 
				
			||||||
 | 
					                            todo_cmds::todo_channel_view(),
 | 
				
			||||||
 | 
					                        ],
 | 
				
			||||||
 | 
					                        ..todo_cmds::todo_channel_base()
 | 
				
			||||||
 | 
					                    },
 | 
				
			||||||
 | 
					                    poise::Command {
 | 
				
			||||||
 | 
					                        subcommands: vec![todo_cmds::todo_user_add(), todo_cmds::todo_user_view()],
 | 
				
			||||||
 | 
					                        ..todo_cmds::todo_user_base()
 | 
				
			||||||
 | 
					                    },
 | 
				
			||||||
 | 
					                ],
 | 
				
			||||||
 | 
					                ..todo_cmds::todo_base()
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        allowed_mentions: None,
 | 
				
			||||||
 | 
					        command_check: Some(|ctx| Box::pin(all_checks(ctx))),
 | 
				
			||||||
 | 
					        listener: |ctx, event, _framework, data| Box::pin(listener(ctx, event, data)),
 | 
				
			||||||
 | 
					        ..Default::default()
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let logged_in_id = http
 | 
					    let database =
 | 
				
			||||||
        .get_current_user()
 | 
					        Pool::connect(&env::var("DATABASE_URL").expect("No database URL provided")).await.unwrap();
 | 
				
			||||||
        .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("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(&pool)
 | 
					    .fetch_all(&database)
 | 
				
			||||||
    .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>>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        let mut data = client.data.write().await;
 | 
					    poise::Framework::build()
 | 
				
			||||||
 | 
					        .token(discord_token)
 | 
				
			||||||
 | 
					        .user_data_setup(move |ctx, _bot, framework| {
 | 
				
			||||||
 | 
					            Box::pin(async move {
 | 
				
			||||||
 | 
					                ctx.set_activity(Activity::watching("for /remind")).await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        data.insert::<GuildDataCache>(Arc::new(guild_data_cache));
 | 
					                register_application_commands(
 | 
				
			||||||
        data.insert::<CurrentlyExecuting>(Arc::new(RwLock::new(HashMap::new())));
 | 
					                    ctx,
 | 
				
			||||||
        data.insert::<SQLPool>(pool);
 | 
					                    framework,
 | 
				
			||||||
        data.insert::<PopularTimezones>(Arc::new(popular_timezones));
 | 
					                    env::var("DEBUG_GUILD")
 | 
				
			||||||
        data.insert::<ReqwestClient>(Arc::new(reqwest::Client::new()));
 | 
					                        .map(|inner| GuildId(inner.parse().expect("DEBUG_GUILD not valid")))
 | 
				
			||||||
        data.insert::<FrameworkCtx>(framework_arc.clone());
 | 
					                        .ok(),
 | 
				
			||||||
        data.insert::<LanguageManager>(Arc::new(language_manager))
 | 
					                )
 | 
				
			||||||
    }
 | 
					                .await
 | 
				
			||||||
 | 
					                .unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if let Ok((Some(lower), Some(upper))) = env::var("SHARD_RANGE").map(|sr| {
 | 
					                Ok(Data {
 | 
				
			||||||
        let mut split = sr
 | 
					                    http: reqwest::Client::new(),
 | 
				
			||||||
            .split(',')
 | 
					                    database,
 | 
				
			||||||
            .map(|val| val.parse::<u64>().expect("SHARD_RANGE not an integer"));
 | 
					                    popular_timezones,
 | 
				
			||||||
 | 
					                    recording_macros: Default::default(),
 | 
				
			||||||
        (split.next(), split.next())
 | 
					                    is_loop_running: AtomicBool::new(false),
 | 
				
			||||||
    }) {
 | 
					                    broadcast: tx,
 | 
				
			||||||
        let total_shards = env::var("SHARD_COUNT")
 | 
					                })
 | 
				
			||||||
            .map(|shard_count| shard_count.parse::<u64>().ok())
 | 
					            })
 | 
				
			||||||
            .ok()
 | 
					        })
 | 
				
			||||||
            .flatten()
 | 
					        .options(options)
 | 
				
			||||||
            .expect("No SHARD_COUNT provided, but SHARD_RANGE was provided");
 | 
					        .intents(GatewayIntents::GUILDS)
 | 
				
			||||||
 | 
					        .run_autosharded()
 | 
				
			||||||
        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;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,8 +1,6 @@
 | 
				
			|||||||
use serenity::model::channel::Channel;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use sqlx::MySqlPool;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use chrono::NaiveDateTime;
 | 
					use chrono::NaiveDateTime;
 | 
				
			||||||
 | 
					use poise::serenity::model::channel::Channel;
 | 
				
			||||||
 | 
					use sqlx::MySqlPool;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub struct ChannelData {
 | 
					pub struct ChannelData {
 | 
				
			||||||
    pub id: u32,
 | 
					    pub id: u32,
 | 
				
			||||||
@@ -17,40 +15,45 @@ pub struct ChannelData {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
impl ChannelData {
 | 
					impl ChannelData {
 | 
				
			||||||
    pub async fn from_channel(
 | 
					    pub async fn from_channel(
 | 
				
			||||||
        channel: Channel,
 | 
					        channel: &Channel,
 | 
				
			||||||
        pool: &MySqlPool,
 | 
					        pool: &MySqlPool,
 | 
				
			||||||
    ) -> Result<Self, Box<dyn std::error::Error + Sync + Send>> {
 | 
					    ) -> Result<Self, Box<dyn std::error::Error + Sync + Send>> {
 | 
				
			||||||
        let channel_id = channel.id().as_u64().to_owned();
 | 
					        let channel_id = channel.id().as_u64().to_owned();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if let Ok(c) = sqlx::query_as_unchecked!(Self,
 | 
					        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 = ?
 | 
					SELECT id, name, nudge, blacklisted, webhook_id, webhook_token, paused, paused_until FROM channels WHERE channel = ?
 | 
				
			||||||
            ", channel_id)
 | 
					            ",
 | 
				
			||||||
 | 
					            channel_id
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
        .fetch_one(pool)
 | 
					        .fetch_one(pool)
 | 
				
			||||||
            .await {
 | 
					        .await
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
            Ok(c)
 | 
					            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 {
 | 
					        } else {
 | 
				
			||||||
                (None, None)
 | 
					            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!(
 | 
					            sqlx::query!(
 | 
				
			||||||
                "
 | 
					                "
 | 
				
			||||||
INSERT IGNORE INTO channels (channel, name, guild_id) VALUES (?, ?, (SELECT id FROM guilds WHERE guild = ?))
 | 
					INSERT IGNORE INTO channels (channel, name, guild_id) VALUES (?, ?, (SELECT id FROM guilds WHERE guild = ?))
 | 
				
			||||||
                ", channel_id, channel_name, guild_id)
 | 
					                ",
 | 
				
			||||||
 | 
					                channel_id,
 | 
				
			||||||
 | 
					                channel_name,
 | 
				
			||||||
 | 
					                guild_id
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
            .execute(&pool.clone())
 | 
					            .execute(&pool.clone())
 | 
				
			||||||
            .await?;
 | 
					            .await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            Ok(sqlx::query_as_unchecked!(Self,
 | 
					            Ok(sqlx::query_as_unchecked!(
 | 
				
			||||||
 | 
					                Self,
 | 
				
			||||||
                "
 | 
					                "
 | 
				
			||||||
SELECT id, name, nudge, blacklisted, webhook_id, webhook_token, paused, paused_until FROM channels WHERE channel = ?
 | 
					SELECT id, name, nudge, blacklisted, webhook_id, webhook_token, paused, paused_until FROM channels WHERE channel = ?
 | 
				
			||||||
                ", channel_id)
 | 
					                ",
 | 
				
			||||||
 | 
					                channel_id
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
            .fetch_one(pool)
 | 
					            .fetch_one(pool)
 | 
				
			||||||
            .await?)
 | 
					            .await?)
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@@ -59,9 +62,20 @@ SELECT id, name, nudge, blacklisted, webhook_id, webhook_token, paused, paused_u
 | 
				
			|||||||
    pub async fn commit_changes(&self, pool: &MySqlPool) {
 | 
					    pub async fn commit_changes(&self, pool: &MySqlPool) {
 | 
				
			||||||
        sqlx::query!(
 | 
					        sqlx::query!(
 | 
				
			||||||
            "
 | 
					            "
 | 
				
			||||||
UPDATE channels SET name = ?, nudge = ?, blacklisted = ?, webhook_id = ?, webhook_token = ?, paused = ?, paused_until = ? WHERE id = ?
 | 
					UPDATE channels SET name = ?, nudge = ?, blacklisted = ?, webhook_id = ?, webhook_token = ?, paused = ?, paused_until \
 | 
				
			||||||
            ", self.name, self.nudge, self.blacklisted, self.webhook_id, self.webhook_token, self.paused, self.paused_until, self.id)
 | 
					             = ? WHERE id = ?
 | 
				
			||||||
 | 
					            ",
 | 
				
			||||||
 | 
					            self.name,
 | 
				
			||||||
 | 
					            self.nudge,
 | 
				
			||||||
 | 
					            self.blacklisted,
 | 
				
			||||||
 | 
					            self.webhook_id,
 | 
				
			||||||
 | 
					            self.webhook_token,
 | 
				
			||||||
 | 
					            self.paused,
 | 
				
			||||||
 | 
					            self.paused_until,
 | 
				
			||||||
 | 
					            self.id
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
        .execute(pool)
 | 
					        .execute(pool)
 | 
				
			||||||
            .await.unwrap();
 | 
					        .await
 | 
				
			||||||
 | 
					        .unwrap();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										73
									
								
								src/models/command_macro.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,73 @@
 | 
				
			|||||||
 | 
					use poise::serenity::model::{
 | 
				
			||||||
 | 
					    id::GuildId, interactions::application_command::ApplicationCommandInteractionDataOption,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					use serde::{Deserialize, Serialize};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::{Context, Data, Error};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fn default_none<U, E>() -> Option<
 | 
				
			||||||
 | 
					    for<'a> fn(
 | 
				
			||||||
 | 
					        poise::ApplicationContext<'a, U, E>,
 | 
				
			||||||
 | 
					    ) -> poise::BoxFuture<'a, Result<(), poise::FrameworkError<'a, U, E>>>,
 | 
				
			||||||
 | 
					> {
 | 
				
			||||||
 | 
					    None
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Serialize, Deserialize)]
 | 
				
			||||||
 | 
					pub struct RecordedCommand<U, E> {
 | 
				
			||||||
 | 
					    #[serde(skip)]
 | 
				
			||||||
 | 
					    #[serde(default = "default_none::<U, E>")]
 | 
				
			||||||
 | 
					    pub action: Option<
 | 
				
			||||||
 | 
					        for<'a> fn(
 | 
				
			||||||
 | 
					            poise::ApplicationContext<'a, U, E>,
 | 
				
			||||||
 | 
					        ) -> poise::BoxFuture<'a, Result<(), poise::FrameworkError<'a, U, E>>>,
 | 
				
			||||||
 | 
					    >,
 | 
				
			||||||
 | 
					    pub command_name: String,
 | 
				
			||||||
 | 
					    pub options: Vec<ApplicationCommandInteractionDataOption>,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub struct CommandMacro<U, E> {
 | 
				
			||||||
 | 
					    pub guild_id: GuildId,
 | 
				
			||||||
 | 
					    pub name: String,
 | 
				
			||||||
 | 
					    pub description: Option<String>,
 | 
				
			||||||
 | 
					    pub commands: Vec<RecordedCommand<U, E>>,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub async fn guild_command_macro(
 | 
				
			||||||
 | 
					    ctx: &Context<'_>,
 | 
				
			||||||
 | 
					    name: &str,
 | 
				
			||||||
 | 
					) -> Option<CommandMacro<Data, Error>> {
 | 
				
			||||||
 | 
					    let row = sqlx::query!(
 | 
				
			||||||
 | 
					        "
 | 
				
			||||||
 | 
					SELECT * FROM macro WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?) AND name = ?
 | 
				
			||||||
 | 
					        ",
 | 
				
			||||||
 | 
					        ctx.guild_id().unwrap().0,
 | 
				
			||||||
 | 
					        name
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    .fetch_one(&ctx.data().database)
 | 
				
			||||||
 | 
					    .await
 | 
				
			||||||
 | 
					    .ok()?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let mut commands: Vec<RecordedCommand<Data, Error>> =
 | 
				
			||||||
 | 
					        serde_json::from_str(&row.commands).unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for recorded_command in &mut commands {
 | 
				
			||||||
 | 
					        let command = &ctx
 | 
				
			||||||
 | 
					            .framework()
 | 
				
			||||||
 | 
					            .options()
 | 
				
			||||||
 | 
					            .commands
 | 
				
			||||||
 | 
					            .iter()
 | 
				
			||||||
 | 
					            .find(|c| c.identifying_name == recorded_command.command_name);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        recorded_command.action = command.map(|c| c.slash_action).flatten().clone();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let command_macro = CommandMacro {
 | 
				
			||||||
 | 
					        guild_id: ctx.guild_id().unwrap(),
 | 
				
			||||||
 | 
					        name: row.name,
 | 
				
			||||||
 | 
					        description: row.description,
 | 
				
			||||||
 | 
					        commands,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Some(command_macro)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,79 +0,0 @@
 | 
				
			|||||||
use serenity::model::guild::Guild;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use sqlx::MySqlPool;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use log::error;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use crate::consts::DEFAULT_PREFIX;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
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();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,78 +1,75 @@
 | 
				
			|||||||
pub mod channel_data;
 | 
					pub mod channel_data;
 | 
				
			||||||
pub mod guild_data;
 | 
					pub mod command_macro;
 | 
				
			||||||
pub mod reminder;
 | 
					pub mod reminder;
 | 
				
			||||||
pub mod timer;
 | 
					pub mod timer;
 | 
				
			||||||
pub mod user_data;
 | 
					pub mod user_data;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use serenity::{async_trait, model::id::GuildId, prelude::Context};
 | 
					use chrono_tz::Tz;
 | 
				
			||||||
 | 
					use poise::serenity::{async_trait, model::id::UserId};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::{consts::DEFAULT_PREFIX, GuildDataCache, SQLPool};
 | 
					use crate::{
 | 
				
			||||||
 | 
					    models::{channel_data::ChannelData, user_data::UserData},
 | 
				
			||||||
use guild_data::GuildData;
 | 
					    CommandMacro, Context, Data, Error, GuildId,
 | 
				
			||||||
 | 
					 | 
				
			||||||
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_trait]
 | 
				
			||||||
 | 
					pub trait CtxData {
 | 
				
			||||||
 | 
					    async fn user_data<U: Into<UserId> + Send>(&self, user_id: U) -> Result<UserData, Error>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async fn author_data(&self) -> Result<UserData, Error>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async fn timezone(&self) -> Tz;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async fn channel_data(&self) -> Result<ChannelData, Error>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async fn command_macros(&self) -> Result<Vec<CommandMacro<Data, Error>>, Error>;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async fn prefix<G: Into<GuildId> + Send + Sync>(&self, guild_id: Option<G>) -> String {
 | 
					#[async_trait]
 | 
				
			||||||
        if let Some(guild_id) = guild_id {
 | 
					impl CtxData for Context<'_> {
 | 
				
			||||||
            self.guild_data(guild_id)
 | 
					    async fn user_data<U: Into<UserId> + Send>(
 | 
				
			||||||
                .await
 | 
					        &self,
 | 
				
			||||||
                .unwrap()
 | 
					        user_id: U,
 | 
				
			||||||
                .read()
 | 
					    ) -> Result<UserData, Box<dyn std::error::Error + Sync + Send>> {
 | 
				
			||||||
                .await
 | 
					        UserData::from_user(user_id, &self.discord(), &self.data().database).await
 | 
				
			||||||
                .prefix
 | 
					    }
 | 
				
			||||||
                .clone()
 | 
					
 | 
				
			||||||
        } else {
 | 
					    async fn author_data(&self) -> Result<UserData, Box<dyn std::error::Error + Sync + Send>> {
 | 
				
			||||||
            DEFAULT_PREFIX.clone()
 | 
					        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
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async fn command_macros(&self) -> Result<Vec<CommandMacro<Data, Error>>, Error> {
 | 
				
			||||||
 | 
					        self.data().command_macros(self.guild_id().unwrap()).await
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl Data {
 | 
				
			||||||
 | 
					    pub(crate) async fn command_macros(
 | 
				
			||||||
 | 
					        &self,
 | 
				
			||||||
 | 
					        guild_id: GuildId,
 | 
				
			||||||
 | 
					    ) -> Result<Vec<CommandMacro<Data, Error>>, Error> {
 | 
				
			||||||
 | 
					        let rows = sqlx::query!(
 | 
				
			||||||
 | 
					            "SELECT name, description, commands FROM macro WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?)",
 | 
				
			||||||
 | 
					            guild_id.0
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        .fetch_all(&self.database)
 | 
				
			||||||
 | 
					        .await?.iter().map(|row| CommandMacro {
 | 
				
			||||||
 | 
					            guild_id,
 | 
				
			||||||
 | 
					            name: row.name.clone(),
 | 
				
			||||||
 | 
					            description: row.description.clone(),
 | 
				
			||||||
 | 
					            commands: serde_json::from_str(&row.commands).unwrap(),
 | 
				
			||||||
 | 
					        }).collect();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Ok(rows)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,507 +0,0 @@
 | 
				
			|||||||
use serenity::{
 | 
					 | 
				
			||||||
    client::Context,
 | 
					 | 
				
			||||||
    model::id::{ChannelId, GuildId, UserId},
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use chrono::NaiveDateTime;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use crate::{
 | 
					 | 
				
			||||||
    consts::{DAY, HOUR, MINUTE, REGEX_CHANNEL},
 | 
					 | 
				
			||||||
    SQLPool,
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use num_integer::Integer;
 | 
					 | 
				
			||||||
use ring::hmac;
 | 
					 | 
				
			||||||
use std::convert::{TryFrom, TryInto};
 | 
					 | 
				
			||||||
use std::env;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
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(", ")
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Debug)]
 | 
					 | 
				
			||||||
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(ctx: &Context, uid: String) -> Option<Self> {
 | 
					 | 
				
			||||||
        let pool = ctx.data.read().await.get::<SQLPool>().cloned().unwrap();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        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_id(ctx: &Context, id: u32) -> Option<Self> {
 | 
					 | 
				
			||||||
        let pool = ctx.data.read().await.get::<SQLPool>().cloned().unwrap();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        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.id = ?
 | 
					 | 
				
			||||||
            ",
 | 
					 | 
				
			||||||
            id
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        .fetch_one(&pool)
 | 
					 | 
				
			||||||
        .await
 | 
					 | 
				
			||||||
        .ok()
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    pub async fn from_channel<C: Into<ChannelId>>(
 | 
					 | 
				
			||||||
        ctx: &Context,
 | 
					 | 
				
			||||||
        channel_id: C,
 | 
					 | 
				
			||||||
        flags: &LookFlags,
 | 
					 | 
				
			||||||
    ) -> Vec<Self> {
 | 
					 | 
				
			||||||
        let pool = ctx.data.read().await.get::<SQLPool>().cloned().unwrap();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        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
 | 
					 | 
				
			||||||
LIMIT
 | 
					 | 
				
			||||||
    ?
 | 
					 | 
				
			||||||
            ",
 | 
					 | 
				
			||||||
            channel_id.as_u64(),
 | 
					 | 
				
			||||||
            enabled,
 | 
					 | 
				
			||||||
            flags.limit
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        .fetch_all(&pool)
 | 
					 | 
				
			||||||
        .await
 | 
					 | 
				
			||||||
        .unwrap()
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    pub async fn from_guild(ctx: &Context, guild_id: Option<GuildId>, user: UserId) -> Vec<Self> {
 | 
					 | 
				
			||||||
        let pool = ctx.data.read().await.get::<SQLPool>().cloned().unwrap();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if let Some(guild_id) = guild_id {
 | 
					 | 
				
			||||||
            let guild_opt = guild_id.to_guild_cached(&ctx).await;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            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(&self, flags: &LookFlags, inter: &str) -> String {
 | 
					 | 
				
			||||||
        let time_display = match flags.time_display {
 | 
					 | 
				
			||||||
            TimeDisplayType::Absolute => format!("<t:{}>", self.utc_time.timestamp()),
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            TimeDisplayType::Relative => format!("<t:{}:R>", self.utc_time.timestamp()),
 | 
					 | 
				
			||||||
        };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if let Some(interval) = self.interval {
 | 
					 | 
				
			||||||
            format!(
 | 
					 | 
				
			||||||
                "'{}' *{}* **{}**, repeating every **{}** (set by {})",
 | 
					 | 
				
			||||||
                self.display_content(),
 | 
					 | 
				
			||||||
                &inter,
 | 
					 | 
				
			||||||
                time_display,
 | 
					 | 
				
			||||||
                longhand_displacement(interval as u64),
 | 
					 | 
				
			||||||
                self.set_by
 | 
					 | 
				
			||||||
                    .map(|i| format!("<@{}>", i))
 | 
					 | 
				
			||||||
                    .unwrap_or_else(|| "unknown".to_string())
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
            format!(
 | 
					 | 
				
			||||||
                "'{}' *{}* **{}** (set by {})",
 | 
					 | 
				
			||||||
                self.display_content(),
 | 
					 | 
				
			||||||
                &inter,
 | 
					 | 
				
			||||||
                time_display,
 | 
					 | 
				
			||||||
                self.set_by
 | 
					 | 
				
			||||||
                    .map(|i| format!("<@{}>", i))
 | 
					 | 
				
			||||||
                    .unwrap_or_else(|| "unknown".to_string())
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    pub async fn from_interaction<U: Into<u64>>(
 | 
					 | 
				
			||||||
        ctx: &Context,
 | 
					 | 
				
			||||||
        member_id: U,
 | 
					 | 
				
			||||||
        payload: String,
 | 
					 | 
				
			||||||
    ) -> Result<(Self, ReminderAction), InteractionError> {
 | 
					 | 
				
			||||||
        let sections = payload.split(".").collect::<Vec<&str>>();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if sections.len() != 3 {
 | 
					 | 
				
			||||||
            Err(InteractionError::InvalidFormat)
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
            let action = ReminderAction::try_from(sections[0])
 | 
					 | 
				
			||||||
                .map_err(|_| InteractionError::InvalidAction)?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            let reminder_id = u32::from_le_bytes(
 | 
					 | 
				
			||||||
                base64::decode(sections[1])
 | 
					 | 
				
			||||||
                    .map_err(|_| InteractionError::InvalidBase64)?
 | 
					 | 
				
			||||||
                    .try_into()
 | 
					 | 
				
			||||||
                    .map_err(|_| InteractionError::InvalidSize)?,
 | 
					 | 
				
			||||||
            );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if let Some(reminder) = Self::from_id(ctx, reminder_id).await {
 | 
					 | 
				
			||||||
                if reminder.signed_action(member_id, action) == payload {
 | 
					 | 
				
			||||||
                    Ok((reminder, action))
 | 
					 | 
				
			||||||
                } else {
 | 
					 | 
				
			||||||
                    Err(InteractionError::SignatureMismatch)
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            } else {
 | 
					 | 
				
			||||||
                Err(InteractionError::NoReminder)
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    pub fn signed_action<U: Into<u64>>(&self, member_id: U, action: ReminderAction) -> String {
 | 
					 | 
				
			||||||
        let s_key = hmac::Key::new(
 | 
					 | 
				
			||||||
            hmac::HMAC_SHA256,
 | 
					 | 
				
			||||||
            env::var("SECRET_KEY")
 | 
					 | 
				
			||||||
                .expect("No SECRET_KEY provided")
 | 
					 | 
				
			||||||
                .as_bytes(),
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        let mut context = hmac::Context::with_key(&s_key);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        context.update(&self.id.to_le_bytes());
 | 
					 | 
				
			||||||
        context.update(&member_id.into().to_le_bytes());
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        let signature = context.sign();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        format!(
 | 
					 | 
				
			||||||
            "{}.{}.{}",
 | 
					 | 
				
			||||||
            action.to_string(),
 | 
					 | 
				
			||||||
            base64::encode(self.id.to_le_bytes()),
 | 
					 | 
				
			||||||
            base64::encode(&signature)
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    pub async fn delete(&self, ctx: &Context) {
 | 
					 | 
				
			||||||
        let pool = ctx.data.read().await.get::<SQLPool>().cloned().unwrap();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        sqlx::query!(
 | 
					 | 
				
			||||||
            "
 | 
					 | 
				
			||||||
DELETE FROM reminders WHERE id = ?
 | 
					 | 
				
			||||||
            ",
 | 
					 | 
				
			||||||
            self.id
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        .execute(&pool)
 | 
					 | 
				
			||||||
        .await
 | 
					 | 
				
			||||||
        .unwrap();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Debug)]
 | 
					 | 
				
			||||||
pub enum InteractionError {
 | 
					 | 
				
			||||||
    InvalidFormat,
 | 
					 | 
				
			||||||
    InvalidBase64,
 | 
					 | 
				
			||||||
    InvalidSize,
 | 
					 | 
				
			||||||
    NoReminder,
 | 
					 | 
				
			||||||
    SignatureMismatch,
 | 
					 | 
				
			||||||
    InvalidAction,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl ToString for InteractionError {
 | 
					 | 
				
			||||||
    fn to_string(&self) -> String {
 | 
					 | 
				
			||||||
        match self {
 | 
					 | 
				
			||||||
            InteractionError::InvalidFormat => {
 | 
					 | 
				
			||||||
                String::from("The interaction data was improperly formatted")
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            InteractionError::InvalidBase64 => String::from("The interaction data was invalid"),
 | 
					 | 
				
			||||||
            InteractionError::InvalidSize => String::from("The interaction data was invalid"),
 | 
					 | 
				
			||||||
            InteractionError::NoReminder => String::from("Reminder could not be found"),
 | 
					 | 
				
			||||||
            InteractionError::SignatureMismatch => {
 | 
					 | 
				
			||||||
                String::from("Only the user who did the command can use interactions")
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            InteractionError::InvalidAction => String::from("The action was invalid"),
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Clone, Copy)]
 | 
					 | 
				
			||||||
pub enum ReminderAction {
 | 
					 | 
				
			||||||
    Delete,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl ToString for ReminderAction {
 | 
					 | 
				
			||||||
    fn to_string(&self) -> String {
 | 
					 | 
				
			||||||
        match self {
 | 
					 | 
				
			||||||
            Self::Delete => String::from("del"),
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl TryFrom<&str> for ReminderAction {
 | 
					 | 
				
			||||||
    type Error = ();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    fn try_from(value: &str) -> Result<Self, Self::Error> {
 | 
					 | 
				
			||||||
        match value {
 | 
					 | 
				
			||||||
            "del" => Ok(Self::Delete),
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            _ => Err(()),
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
enum TimeDisplayType {
 | 
					 | 
				
			||||||
    Absolute,
 | 
					 | 
				
			||||||
    Relative,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
pub struct LookFlags {
 | 
					 | 
				
			||||||
    pub limit: u16,
 | 
					 | 
				
			||||||
    pub show_disabled: bool,
 | 
					 | 
				
			||||||
    pub channel_id: Option<ChannelId>,
 | 
					 | 
				
			||||||
    time_display: TimeDisplayType,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl Default for LookFlags {
 | 
					 | 
				
			||||||
    fn default() -> Self {
 | 
					 | 
				
			||||||
        Self {
 | 
					 | 
				
			||||||
            limit: u16::MAX,
 | 
					 | 
				
			||||||
            show_disabled: true,
 | 
					 | 
				
			||||||
            channel_id: None,
 | 
					 | 
				
			||||||
            time_display: TimeDisplayType::Relative,
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl LookFlags {
 | 
					 | 
				
			||||||
    pub fn from_string(args: &str) -> Self {
 | 
					 | 
				
			||||||
        let mut new_flags: Self = Default::default();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        for arg in args.split(' ') {
 | 
					 | 
				
			||||||
            match arg {
 | 
					 | 
				
			||||||
                "enabled" => {
 | 
					 | 
				
			||||||
                    new_flags.show_disabled = false;
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                "time" => {
 | 
					 | 
				
			||||||
                    new_flags.time_display = TimeDisplayType::Absolute;
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                param => {
 | 
					 | 
				
			||||||
                    if let Ok(val) = param.parse::<u16>() {
 | 
					 | 
				
			||||||
                        new_flags.limit = val;
 | 
					 | 
				
			||||||
                    } else if let Some(channel) = REGEX_CHANNEL
 | 
					 | 
				
			||||||
                        .captures(&arg)
 | 
					 | 
				
			||||||
                        .map(|cap| cap.get(1))
 | 
					 | 
				
			||||||
                        .flatten()
 | 
					 | 
				
			||||||
                        .map(|c| c.as_str().parse::<u64>().unwrap())
 | 
					 | 
				
			||||||
                    {
 | 
					 | 
				
			||||||
                        new_flags.channel_id = Some(ChannelId(channel));
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        new_flags
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
							
								
								
									
										329
									
								
								src/models/reminder/builder.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,329 @@
 | 
				
			|||||||
 | 
					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::{DAY, DEFAULT_AVATAR, MAX_TIME, MIN_INTERVAL},
 | 
				
			||||||
 | 
					    interval_parser::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_secs: Option<i64>,
 | 
				
			||||||
 | 
					    interval_months: 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_seconds`,
 | 
				
			||||||
 | 
					    `interval_months`,
 | 
				
			||||||
 | 
					    `expires`,
 | 
				
			||||||
 | 
					    `content`,
 | 
				
			||||||
 | 
					    `tts`,
 | 
				
			||||||
 | 
					    `attachment_name`,
 | 
				
			||||||
 | 
					    `attachment`,
 | 
				
			||||||
 | 
					    `set_by`
 | 
				
			||||||
 | 
					) VALUES (
 | 
				
			||||||
 | 
					    ?,
 | 
				
			||||||
 | 
					    ?,
 | 
				
			||||||
 | 
					    ?,
 | 
				
			||||||
 | 
					    ?,
 | 
				
			||||||
 | 
					    ?,
 | 
				
			||||||
 | 
					    ?,
 | 
				
			||||||
 | 
					    ?,
 | 
				
			||||||
 | 
					    ?,
 | 
				
			||||||
 | 
					    ?,
 | 
				
			||||||
 | 
					    ?,
 | 
				
			||||||
 | 
					    ?,
 | 
				
			||||||
 | 
					    ?
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					            ",
 | 
				
			||||||
 | 
					                        self.uid,
 | 
				
			||||||
 | 
					                        self.channel,
 | 
				
			||||||
 | 
					                        utc_time,
 | 
				
			||||||
 | 
					                        self.timezone,
 | 
				
			||||||
 | 
					                        self.interval_secs,
 | 
				
			||||||
 | 
					                        self.interval_months,
 | 
				
			||||||
 | 
					                        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<Interval>,
 | 
				
			||||||
 | 
					    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, 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 timezone(mut self, timezone: Tz) -> Self {
 | 
				
			||||||
 | 
					        self.timezone = timezone;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    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<Interval>) -> 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 mut errors = HashSet::new();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let mut ok_locs = HashSet::new();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if self.interval.map_or(false, |i| ((i.sec + i.month * 30 * DAY) as i64) < *MIN_INTERVAL) {
 | 
				
			||||||
 | 
					            errors.insert(ReminderError::ShortInterval);
 | 
				
			||||||
 | 
					        } else if self.interval.map_or(false, |i| ((i.sec + i.month * 30 * DAY) 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(),
 | 
				
			||||||
 | 
					                                &self.ctx.data().database,
 | 
				
			||||||
 | 
					                            )
 | 
				
			||||||
 | 
					                            .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, &self.ctx.data().database)
 | 
				
			||||||
 | 
					                                        .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(&self.ctx.data().database)
 | 
				
			||||||
 | 
					                                                .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: self.ctx.data().database.clone(),
 | 
				
			||||||
 | 
					                            uid: generate_uid(),
 | 
				
			||||||
 | 
					                            channel: c,
 | 
				
			||||||
 | 
					                            utc_time: self.utc_time,
 | 
				
			||||||
 | 
					                            timezone: self.timezone.to_string(),
 | 
				
			||||||
 | 
					                            interval_secs: self.interval.map(|i| i.sec as i64),
 | 
				
			||||||
 | 
					                            interval_months: self.interval.map(|i| i.month as i64),
 | 
				
			||||||
 | 
					                            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)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										12
									
								
								src/models/reminder/content.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,12 @@
 | 
				
			|||||||
 | 
					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 }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										36
									
								
								src/models/reminder/errors.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,36 @@
 | 
				
			|||||||
 | 
					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),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										12
									
								
								src/models/reminder/helper.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,12 @@
 | 
				
			|||||||
 | 
					use rand::{rngs::OsRng, seq::IteratorRandom};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::consts::CHARACTERS;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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("")
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										23
									
								
								src/models/reminder/look_flags.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,23 @@
 | 
				
			|||||||
 | 
					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 }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										290
									
								
								src/models/reminder/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,290 @@
 | 
				
			|||||||
 | 
					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},
 | 
				
			||||||
 | 
					    serenity_prelude::Cache,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					use sqlx::Executor;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::{
 | 
				
			||||||
 | 
					    models::reminder::look_flags::{LookFlags, TimeDisplayType},
 | 
				
			||||||
 | 
					    Database,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Debug, Clone)]
 | 
				
			||||||
 | 
					pub struct Reminder {
 | 
				
			||||||
 | 
					    pub id: u32,
 | 
				
			||||||
 | 
					    pub uid: String,
 | 
				
			||||||
 | 
					    pub channel: u64,
 | 
				
			||||||
 | 
					    pub utc_time: NaiveDateTime,
 | 
				
			||||||
 | 
					    pub interval_seconds: Option<u32>,
 | 
				
			||||||
 | 
					    pub interval_months: 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: impl Executor<'_, Database = Database>,
 | 
				
			||||||
 | 
					        uid: String,
 | 
				
			||||||
 | 
					    ) -> Option<Self> {
 | 
				
			||||||
 | 
					        sqlx::query_as_unchecked!(
 | 
				
			||||||
 | 
					            Self,
 | 
				
			||||||
 | 
					            "
 | 
				
			||||||
 | 
					SELECT
 | 
				
			||||||
 | 
					    reminders.id,
 | 
				
			||||||
 | 
					    reminders.uid,
 | 
				
			||||||
 | 
					    channels.channel,
 | 
				
			||||||
 | 
					    reminders.utc_time,
 | 
				
			||||||
 | 
					    reminders.interval_seconds,
 | 
				
			||||||
 | 
					    reminders.interval_months,
 | 
				
			||||||
 | 
					    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>>(
 | 
				
			||||||
 | 
					        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_seconds,
 | 
				
			||||||
 | 
					    reminders.interval_months,
 | 
				
			||||||
 | 
					    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(pool)
 | 
				
			||||||
 | 
					        .await
 | 
				
			||||||
 | 
					        .unwrap()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub async fn from_guild(
 | 
				
			||||||
 | 
					        cache: impl AsRef<Cache>,
 | 
				
			||||||
 | 
					        pool: impl Executor<'_, Database = Database>,
 | 
				
			||||||
 | 
					        guild_id: Option<GuildId>,
 | 
				
			||||||
 | 
					        user: UserId,
 | 
				
			||||||
 | 
					    ) -> Vec<Self> {
 | 
				
			||||||
 | 
					        if let Some(guild_id) = guild_id {
 | 
				
			||||||
 | 
					            let guild_opt = guild_id.to_guild_cached(cache);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            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_seconds,
 | 
				
			||||||
 | 
					    reminders.interval_months,
 | 
				
			||||||
 | 
					    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_seconds,
 | 
				
			||||||
 | 
					    reminders.interval_months,
 | 
				
			||||||
 | 
					    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_seconds,
 | 
				
			||||||
 | 
					    reminders.interval_months,
 | 
				
			||||||
 | 
					    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 self.interval_seconds.is_some() || self.interval_months.is_some() {
 | 
				
			||||||
 | 
					            format!(
 | 
				
			||||||
 | 
					                "'{}' *occurs next at* **{}**, repeating (set by {})",
 | 
				
			||||||
 | 
					                self.display_content(),
 | 
				
			||||||
 | 
					                time_display,
 | 
				
			||||||
 | 
					                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,6 +1,5 @@
 | 
				
			|||||||
use sqlx::MySqlPool;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use chrono::NaiveDateTime;
 | 
					use chrono::NaiveDateTime;
 | 
				
			||||||
 | 
					use sqlx::MySqlPool;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub struct Timer {
 | 
					pub struct Timer {
 | 
				
			||||||
    pub name: String,
 | 
					    pub name: String,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,47 +1,18 @@
 | 
				
			|||||||
use serenity::{
 | 
					use chrono_tz::Tz;
 | 
				
			||||||
    http::CacheHttp,
 | 
					use log::error;
 | 
				
			||||||
    model::{id::UserId, user::User},
 | 
					use poise::serenity::{http::CacheHttp, model::id::UserId};
 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use sqlx::MySqlPool;
 | 
					use sqlx::MySqlPool;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use chrono_tz::Tz;
 | 
					use crate::consts::LOCAL_TIMEZONE;
 | 
				
			||||||
 | 
					 | 
				
			||||||
use log::error;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use crate::consts::{LOCAL_LANGUAGE, LOCAL_TIMEZONE};
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub struct UserData {
 | 
					pub struct UserData {
 | 
				
			||||||
    pub id: u32,
 | 
					    pub id: u32,
 | 
				
			||||||
    pub user: u64,
 | 
					    pub user: u64,
 | 
				
			||||||
    pub name: String,
 | 
					 | 
				
			||||||
    pub dm_channel: u32,
 | 
					    pub dm_channel: u32,
 | 
				
			||||||
    pub language: String,
 | 
					 | 
				
			||||||
    pub timezone: String,
 | 
					    pub timezone: String,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
impl UserData {
 | 
					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
 | 
					    pub async fn timezone_of<U>(user: U, pool: &MySqlPool) -> Tz
 | 
				
			||||||
    where
 | 
					    where
 | 
				
			||||||
        U: Into<UserId>,
 | 
					        U: Into<UserId>,
 | 
				
			||||||
@@ -65,19 +36,20 @@ SELECT timezone FROM users WHERE user = ?
 | 
				
			|||||||
        .unwrap()
 | 
					        .unwrap()
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    pub async fn from_user(
 | 
					    pub async fn from_user<U: Into<UserId>>(
 | 
				
			||||||
        user: &User,
 | 
					        user: U,
 | 
				
			||||||
        ctx: impl CacheHttp,
 | 
					        ctx: impl CacheHttp,
 | 
				
			||||||
        pool: &MySqlPool,
 | 
					        pool: &MySqlPool,
 | 
				
			||||||
    ) -> Result<Self, Box<dyn std::error::Error + Sync + Send>> {
 | 
					    ) -> Result<Self, Box<dyn std::error::Error + Sync + Send>> {
 | 
				
			||||||
        let user_id = user.id.as_u64().to_owned();
 | 
					        let user_id = user.into();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        match sqlx::query_as_unchecked!(
 | 
					        match sqlx::query_as_unchecked!(
 | 
				
			||||||
            Self,
 | 
					            Self,
 | 
				
			||||||
            "
 | 
					            "
 | 
				
			||||||
SELECT id, user, name, dm_channel, IF(language IS NULL, ?, language) AS language, IF(timezone IS NULL, ?, timezone) AS timezone FROM users WHERE user = ?
 | 
					SELECT id, user, dm_channel, IF(timezone IS NULL, ?, timezone) AS timezone FROM users WHERE user = ?
 | 
				
			||||||
            ",
 | 
					            ",
 | 
				
			||||||
            *LOCAL_LANGUAGE, *LOCAL_TIMEZONE, user_id
 | 
					            *LOCAL_TIMEZONE,
 | 
				
			||||||
 | 
					            user_id.0
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        .fetch_one(pool)
 | 
					        .fetch_one(pool)
 | 
				
			||||||
        .await
 | 
					        .await
 | 
				
			||||||
@@ -85,33 +57,35 @@ SELECT id, user, name, dm_channel, IF(language IS NULL, ?, language) AS language
 | 
				
			|||||||
            Ok(c) => Ok(c),
 | 
					            Ok(c) => Ok(c),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            Err(sqlx::Error::RowNotFound) => {
 | 
					            Err(sqlx::Error::RowNotFound) => {
 | 
				
			||||||
                let dm_channel = user.create_dm_channel(ctx).await?;
 | 
					                let dm_channel = user_id.create_dm_channel(ctx).await?;
 | 
				
			||||||
                let dm_id = dm_channel.id.as_u64().to_owned();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                let pool_c = pool.clone();
 | 
					                let pool_c = pool.clone();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                sqlx::query!(
 | 
					                sqlx::query!(
 | 
				
			||||||
                    "
 | 
					                    "
 | 
				
			||||||
INSERT IGNORE INTO channels (channel) VALUES (?)
 | 
					INSERT IGNORE INTO channels (channel) VALUES (?)
 | 
				
			||||||
                    ",
 | 
					                    ",
 | 
				
			||||||
                    dm_id
 | 
					                    dm_channel.id.0
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
                .execute(&pool_c)
 | 
					                .execute(&pool_c)
 | 
				
			||||||
                .await?;
 | 
					                .await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                sqlx::query!(
 | 
					                sqlx::query!(
 | 
				
			||||||
                    "
 | 
					                    "
 | 
				
			||||||
INSERT INTO users (user, name, dm_channel, language, timezone) VALUES (?, ?, (SELECT id FROM channels WHERE channel = ?), ?, ?)
 | 
					INSERT INTO users (user, dm_channel, timezone) VALUES (?, (SELECT id FROM channels WHERE channel = ?), ?)
 | 
				
			||||||
                    ", user_id, user.name, dm_id, *LOCAL_LANGUAGE, *LOCAL_TIMEZONE)
 | 
					                    ",
 | 
				
			||||||
 | 
					                    user_id.0,
 | 
				
			||||||
 | 
					                    dm_channel.id.0,
 | 
				
			||||||
 | 
					                    *LOCAL_TIMEZONE
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
                .execute(&pool_c)
 | 
					                .execute(&pool_c)
 | 
				
			||||||
                .await?;
 | 
					                .await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                Ok(sqlx::query_as_unchecked!(
 | 
					                Ok(sqlx::query_as_unchecked!(
 | 
				
			||||||
                    Self,
 | 
					                    Self,
 | 
				
			||||||
                    "
 | 
					                    "
 | 
				
			||||||
SELECT id, user, name, dm_channel, language, timezone FROM users WHERE user = ?
 | 
					SELECT id, user, dm_channel, timezone FROM users WHERE user = ?
 | 
				
			||||||
                    ",
 | 
					                    ",
 | 
				
			||||||
                    user_id
 | 
					                    user_id.0
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
                .fetch_one(pool)
 | 
					                .fetch_one(pool)
 | 
				
			||||||
                .await?)
 | 
					                .await?)
 | 
				
			||||||
@@ -121,17 +95,15 @@ SELECT id, user, name, dm_channel, language, timezone FROM users WHERE user = ?
 | 
				
			|||||||
                error!("Error querying for user: {:?}", e);
 | 
					                error!("Error querying for user: {:?}", e);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                Err(Box::new(e))
 | 
					                Err(Box::new(e))
 | 
				
			||||||
            },
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    pub async fn commit_changes(&self, pool: &MySqlPool) {
 | 
					    pub async fn commit_changes(&self, pool: &MySqlPool) {
 | 
				
			||||||
        sqlx::query!(
 | 
					        sqlx::query!(
 | 
				
			||||||
            "
 | 
					            "
 | 
				
			||||||
UPDATE users SET name = ?, language = ?, timezone = ? WHERE id = ?
 | 
					UPDATE users SET timezone = ? WHERE id = ?
 | 
				
			||||||
            ",
 | 
					            ",
 | 
				
			||||||
            self.name,
 | 
					 | 
				
			||||||
            self.language,
 | 
					 | 
				
			||||||
            self.timezone,
 | 
					            self.timezone,
 | 
				
			||||||
            self.id
 | 
					            self.id
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,15 +1,16 @@
 | 
				
			|||||||
use std::time::{SystemTime, UNIX_EPOCH};
 | 
					use std::{
 | 
				
			||||||
 | 
					    convert::TryFrom,
 | 
				
			||||||
use std::fmt::{Display, Formatter, Result as FmtResult};
 | 
					    fmt::{Display, Formatter, Result as FmtResult},
 | 
				
			||||||
 | 
					    str::from_utf8,
 | 
				
			||||||
use crate::consts::{LOCAL_TIMEZONE, PYTHON_LOCATION};
 | 
					    time::{SystemTime, UNIX_EPOCH},
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
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,
 | 
				
			||||||
@@ -26,11 +27,13 @@ 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,
 | 
				
			||||||
@@ -95,10 +98,7 @@ impl TimeParser {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    fn process_explicit(&self) -> Result<i64, InvalidTime> {
 | 
					    fn process_explicit(&self) -> Result<i64, InvalidTime> {
 | 
				
			||||||
        let mut time = Utc::now()
 | 
					        let mut time = Utc::now().with_timezone(&self.timezone).with_second(0).unwrap();
 | 
				
			||||||
            .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,9 @@ impl TimeParser {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        let h_m_s = hms.split(':');
 | 
					        let h_m_s = hms.split(':');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        for (t, setter) in h_m_s.take(3).zip(&[
 | 
					        for (t, setter) in
 | 
				
			||||||
            DateTime::with_hour,
 | 
					            h_m_s.take(3).zip(&[DateTime::with_hour, DateTime::with_minute, DateTime::with_second])
 | 
				
			||||||
            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), Ok)?;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@@ -122,9 +120,7 @@ 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]
 | 
					            for (t, setter) in [day, month].iter().zip(&[DateTime::with_day, DateTime::with_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)?)
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										107
									
								
								src/utils.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,107 @@
 | 
				
			|||||||
 | 
					use poise::{
 | 
				
			||||||
 | 
					    serenity::{
 | 
				
			||||||
 | 
					        builder::CreateApplicationCommands,
 | 
				
			||||||
 | 
					        http::CacheHttp,
 | 
				
			||||||
 | 
					        model::id::{GuildId, UserId},
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    serenity_prelude as serenity,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Sends the message, specified via [`crate::CreateReply`], to the interaction initial response
 | 
				
			||||||
 | 
					/// endpoint
 | 
				
			||||||
 | 
					pub fn send_as_initial_response(
 | 
				
			||||||
 | 
					    data: poise::CreateReply<'_>,
 | 
				
			||||||
 | 
					    f: &mut serenity::CreateInteractionResponseData,
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					    let poise::CreateReply {
 | 
				
			||||||
 | 
					        content,
 | 
				
			||||||
 | 
					        embeds,
 | 
				
			||||||
 | 
					        attachments: _, // serenity doesn't support attachments in initial response yet
 | 
				
			||||||
 | 
					        components,
 | 
				
			||||||
 | 
					        ephemeral,
 | 
				
			||||||
 | 
					        allowed_mentions,
 | 
				
			||||||
 | 
					        reference_message: _, // can't reply to a message in interactions
 | 
				
			||||||
 | 
					    } = data;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if let Some(content) = content {
 | 
				
			||||||
 | 
					        f.content(content);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    f.set_embeds(embeds);
 | 
				
			||||||
 | 
					    if let Some(allowed_mentions) = allowed_mentions {
 | 
				
			||||||
 | 
					        f.allowed_mentions(|f| {
 | 
				
			||||||
 | 
					            *f = allowed_mentions.clone();
 | 
				
			||||||
 | 
					            f
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if let Some(components) = components {
 | 
				
			||||||
 | 
					        f.components(|f| {
 | 
				
			||||||
 | 
					            f.0 = components.0;
 | 
				
			||||||
 | 
					            f
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if ephemeral {
 | 
				
			||||||
 | 
					        f.flags(serenity::InteractionApplicationCommandCallbackDataFlags::EPHEMERAL);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										21
									
								
								web/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,21 @@
 | 
				
			|||||||
 | 
					[package]
 | 
				
			||||||
 | 
					name = "reminder_web"
 | 
				
			||||||
 | 
					version = "0.1.0"
 | 
				
			||||||
 | 
					authors = ["jellywx <judesouthworth@pm.me>"]
 | 
				
			||||||
 | 
					edition = "2018"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[dependencies]
 | 
				
			||||||
 | 
					rocket = { git = "https://github.com/SergioBenitez/Rocket", branch = "master", features = ["tls", "secrets", "json"] }
 | 
				
			||||||
 | 
					rocket_dyn_templates = { git = "https://github.com/SergioBenitez/Rocket", branch = "master", features = ["tera"] }
 | 
				
			||||||
 | 
					serenity = { version = "0.11.1", default-features = false, features = ["builder", "cache", "client", "gateway", "http", "model", "utils", "rustls_backend"] }
 | 
				
			||||||
 | 
					oauth2 = "4"
 | 
				
			||||||
 | 
					log = "0.4"
 | 
				
			||||||
 | 
					reqwest = "0.11"
 | 
				
			||||||
 | 
					serde = { version = "1.0", features = ["derive"] }
 | 
				
			||||||
 | 
					serde_json = "1.0"
 | 
				
			||||||
 | 
					sqlx = { version = "0.5", features = ["runtime-tokio-rustls", "macros", "mysql", "chrono", "json"] }
 | 
				
			||||||
 | 
					chrono = "0.4"
 | 
				
			||||||
 | 
					chrono-tz = "0.5"
 | 
				
			||||||
 | 
					lazy_static = "1.4.0"
 | 
				
			||||||
 | 
					rand = "0.7"
 | 
				
			||||||
 | 
					base64 = "0.13"
 | 
				
			||||||
							
								
								
									
										32
									
								
								web/private/ca_cert.pem
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,32 @@
 | 
				
			|||||||
 | 
					-----BEGIN CERTIFICATE-----
 | 
				
			||||||
 | 
					MIIFbzCCA1egAwIBAgIUUY2fYP5h41dtqcuNLVUS99N7+jAwDQYJKoZIhvcNAQEL
 | 
				
			||||||
 | 
					BQAwRzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRIwEAYDVQQKDAlSb2NrZXQg
 | 
				
			||||||
 | 
					Q0ExFzAVBgNVBAMMDlJvY2tldCBSb290IENBMB4XDTIxMDUxNzE5MDIyNFoXDTMx
 | 
				
			||||||
 | 
					MDUxNTE5MDIyNFowRzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRIwEAYDVQQK
 | 
				
			||||||
 | 
					DAlSb2NrZXQgQ0ExFzAVBgNVBAMMDlJvY2tldCBSb290IENBMIICIjANBgkqhkiG
 | 
				
			||||||
 | 
					9w0BAQEFAAOCAg8AMIICCgKCAgEA3MmVOUxbZ0rtBBbXjWkkM2n2IoPuqfiENdrM
 | 
				
			||||||
 | 
					NH9IcIbEgFb7AQ/c2vGBLWWw2RFtXxe2ubbgcHyAySQ+ENGEf+W2yox5JpcOOBP+
 | 
				
			||||||
 | 
					/KrUQ4ROqxLBWOD6+fA/q5m9/CXXJosWXly3DFE9nUEUlNXcaSxJm/bxIqnxwEMQ
 | 
				
			||||||
 | 
					NESWdG1qlsGVhHLi5sflgwAcYH+nZzdtINLyvQ5AGIs8s6LpmvUVbC3I+5oNNSwW
 | 
				
			||||||
 | 
					rRsIfCFaYLuUx7jZMaOk8Dfz8MbFL68+7OCGwmhduSHoGW8jVzDXwXv/crAmFtau
 | 
				
			||||||
 | 
					zmu43/vPDwKa4w/kQJLNr1QmiGz9FBt8DXVJ0s5bokOIlodEgAvjsL3xwnrKak0F
 | 
				
			||||||
 | 
					8ak74IBetfDdVvuC9WWmJ9/dj3z91KvCWLdZGm8ijlYZ2OuaiiJ/oC6iebaPFHEY
 | 
				
			||||||
 | 
					IQmF5wzT/nZLIbQsti9wFnOpptgkhDAcKIAk3+pncuMwZ9WRvWO2IFt+ZCqKCCDU
 | 
				
			||||||
 | 
					JliDZmm3ow/zob8DD6Txex5OkJGUF6iu8o7fjI9BxsjSc76bLrxIhvR43UXjzoHl
 | 
				
			||||||
 | 
					t3lvDH8SzSN/kxzQMXVl48h3xydJ3+ZyDOLle1bqMga8Kcin9GcEUqmL1JcyFafe
 | 
				
			||||||
 | 
					CUE/Nk1pHyrlnhpMCYIDVI3pmNLB0/m/IEWLckSr9+hXyUPJQyWiyrixwC86SPxZ
 | 
				
			||||||
 | 
					AonU2aUCAwEAAaNTMFEwHQYDVR0OBBYEFJ5uQa9kD5fioNeS/Ff/mxDcMAzhMB8G
 | 
				
			||||||
 | 
					A1UdIwQYMBaAFJ5uQa9kD5fioNeS/Ff/mxDcMAzhMA8GA1UdEwEB/wQFMAMBAf8w
 | 
				
			||||||
 | 
					DQYJKoZIhvcNAQELBQADggIBABf7adF1J40/8EjfSJ5XdG7OBUrZTas3+nuULh8B
 | 
				
			||||||
 | 
					6h1igAq0BgX9v3awmPCaxqDLqwJCsFZCk0s9Vgd62McKVKasyW1TQchWbdvlqeAB
 | 
				
			||||||
 | 
					QQfZpJtAZK12dcMl6iznC/JBTPvYl9q1FKS8kYtg19in/xlhxiiEJaL5z+slpTgT
 | 
				
			||||||
 | 
					cN12650W2FqjT9sD9jshZI/a8o53d2yUBXBBecCZ49djceBGLbxbteEi/sb9qM4f
 | 
				
			||||||
 | 
					IUxeSrvK6owxKMZ5okUqZWaFseFCkdMKpMg9eecyfSTweac8yLlZmeqX5D/H85Hr
 | 
				
			||||||
 | 
					hnWHjI5NeVZAoDkdmNeaDbAIv4f3prqC8uFP3ub8D0rG/WiPdOAd79KNgqbUUFyp
 | 
				
			||||||
 | 
					NbjSiAesx4Rm1WIgjHljFQKXJdtnxt+v1VkKV9entXJX2tye7+Z1+zurNYyUyM1J
 | 
				
			||||||
 | 
					COdXeV4jpq2mP8cLpAqX7ip1tNx9YMy5ctbAE1WsUw7lPyO91+VN2Q6MN5OyemY3
 | 
				
			||||||
 | 
					4nBzRJU8X1nsDgeNQWjzb/ZC7eoS916Gywk8Hu6GoIwvYMm3zea25n6mAAOX17vE
 | 
				
			||||||
 | 
					1YdvSzUlnVbwnbgd4BgFRGUfBVjO7qvFC7ZWLngWhBzIIYIGUJfO2rippgkxAoHH
 | 
				
			||||||
 | 
					dgWYk1MNnk2yM8WSo0VKoe4yuF9NwPlfPqeCE683JHdwGEJKZWMocszzHrGZ2KX2
 | 
				
			||||||
 | 
					I4/u
 | 
				
			||||||
 | 
					-----END CERTIFICATE-----
 | 
				
			||||||
							
								
								
									
										51
									
								
								web/private/ca_key.pem
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,51 @@
 | 
				
			|||||||
 | 
					-----BEGIN RSA PRIVATE KEY-----
 | 
				
			||||||
 | 
					MIIJKAIBAAKCAgEA3MmVOUxbZ0rtBBbXjWkkM2n2IoPuqfiENdrMNH9IcIbEgFb7
 | 
				
			||||||
 | 
					AQ/c2vGBLWWw2RFtXxe2ubbgcHyAySQ+ENGEf+W2yox5JpcOOBP+/KrUQ4ROqxLB
 | 
				
			||||||
 | 
					WOD6+fA/q5m9/CXXJosWXly3DFE9nUEUlNXcaSxJm/bxIqnxwEMQNESWdG1qlsGV
 | 
				
			||||||
 | 
					hHLi5sflgwAcYH+nZzdtINLyvQ5AGIs8s6LpmvUVbC3I+5oNNSwWrRsIfCFaYLuU
 | 
				
			||||||
 | 
					x7jZMaOk8Dfz8MbFL68+7OCGwmhduSHoGW8jVzDXwXv/crAmFtauzmu43/vPDwKa
 | 
				
			||||||
 | 
					4w/kQJLNr1QmiGz9FBt8DXVJ0s5bokOIlodEgAvjsL3xwnrKak0F8ak74IBetfDd
 | 
				
			||||||
 | 
					VvuC9WWmJ9/dj3z91KvCWLdZGm8ijlYZ2OuaiiJ/oC6iebaPFHEYIQmF5wzT/nZL
 | 
				
			||||||
 | 
					IbQsti9wFnOpptgkhDAcKIAk3+pncuMwZ9WRvWO2IFt+ZCqKCCDUJliDZmm3ow/z
 | 
				
			||||||
 | 
					ob8DD6Txex5OkJGUF6iu8o7fjI9BxsjSc76bLrxIhvR43UXjzoHlt3lvDH8SzSN/
 | 
				
			||||||
 | 
					kxzQMXVl48h3xydJ3+ZyDOLle1bqMga8Kcin9GcEUqmL1JcyFafeCUE/Nk1pHyrl
 | 
				
			||||||
 | 
					nhpMCYIDVI3pmNLB0/m/IEWLckSr9+hXyUPJQyWiyrixwC86SPxZAonU2aUCAwEA
 | 
				
			||||||
 | 
					AQKCAgBVSXlfXOOiDwtnnPs/IPJe+fuecaBsABfyRcbEMLbm4OhfOzpSurHx0YC4
 | 
				
			||||||
 | 
					7KNX9qdtKFfpfX9NdIq7KEjhbk3kqfPmYkUaZxeTCgZhzAua2S0aYHBXyPCqQ+gU
 | 
				
			||||||
 | 
					fZsqH+Pwe6H0aZQ8KdXHPTCaHdK6veThXo7feQ5t2noT9rq31txpx/Xd6BNGWsmJ
 | 
				
			||||||
 | 
					xS0xCZ68/GgnWdVyumKAGKkmKzRaK3pPA5CzwFqBw7ouvFaWvLuQymU6kWk1B6Xb
 | 
				
			||||||
 | 
					NYIB7IaXWPbRwhnMV0x9C2ABEzFvqOpvT1rqDqloAR4dlvcfbsIZZkQ2mhjt6MeT
 | 
				
			||||||
 | 
					hsorwQ4yCjvtZvVRfW1gTP4iR7ZpmHgHIYQDJy7raZqRVNe9WgMxKTS/IYsHvEvH
 | 
				
			||||||
 | 
					MUBOfQEclAQ8FTUOgTg9+Hf9VXJOtjZRxY08gb+OgKxoAQKxRZmLDUZli9SNWzGe
 | 
				
			||||||
 | 
					R3UECh0gImm/CzWnV3fercLX+qHLTcyJFcb2gclAfG8v8cOT2qu8RtsJAQM2ZSn7
 | 
				
			||||||
 | 
					L8FaWXewjAwkZwCl8Zl5ky1RbBXUkcuLIkAeLAl0+ffM9xiwLhiIj8gRJoq0/jSr
 | 
				
			||||||
 | 
					K1pA8CQ/rZC+9RsI8hP4x2Fn1CU8bOyEBPeNFEIDJHBiCrs/Rl6EAzDMJHlhL/HT
 | 
				
			||||||
 | 
					f7bqssuRjnSbRCKaAdtfGTxQUEjefvqJ11xE3BfHGnKPqoasMQKCAQEA8nY+3MAB
 | 
				
			||||||
 | 
					eBPlE/H4JeVpj7/9/q+JWLFFH9E3Re25LbObzRzIQOd5bTCJkOPQXYRbRIZ1pMt9
 | 
				
			||||||
 | 
					+nZzeLKvWn5nuUFgG2S4n+BEJkBDV78LOkUuGIAPdztF2mwVumygEGJFFfZHbYrh
 | 
				
			||||||
 | 
					XtaGUKdNxn1R3Z4B8W5jWpNNkU+dvoq4Zt0LrL28HB4Sa2yrtHeXbhqSb0BA15+N
 | 
				
			||||||
 | 
					vO9mlfX2I6Yr8F+L/OBfxB0gaNLISJJsj5NSm302je/QrfoMAwbePoNPjEX1/qd2
 | 
				
			||||||
 | 
					rgj9JfM+NE2FsVnFhrV+q4DywRUdX4VBFP4+x7fPm8LxvtVUmLVA3/NtGwPdnC6U
 | 
				
			||||||
 | 
					mQh/+kKVhU2AQwKCAQEA6R2FrxZIUdmk45b/tSjlq7r1ycBYeSztjzXw6hx+w/K3
 | 
				
			||||||
 | 
					Lf+5OGyoGoKr5bZHMhHFxqoWzP6kiZ2Uyvu+pLAGLfenJPec89ZncN4FvW9yU8yL
 | 
				
			||||||
 | 
					nE2Sc8Vf2GnOw2KGJcJR11Ew4dDspHfnM3rof2G4gPC/EMZeGojXoc5GHmT4iasD
 | 
				
			||||||
 | 
					Xwje4qqx5WKvRu1Rw2y+XM5h4gDn3QLLojDOvBpt22yaqSTAupY7OliGFO9h2WPL
 | 
				
			||||||
 | 
					r9TL6va87nXNepCdtzuSlxqTVtgMu2wVu3CnQyw9RiD2M31YnUtBDLNy/UNFj/5Z
 | 
				
			||||||
 | 
					6uXYuakh8vSFFvjgCUyQwR1xCTJur/6LdpAnt9Bz9wKCAQAUqoN9KVh2tatm4c72
 | 
				
			||||||
 | 
					2/D9ca3ikW+xgZqUta5yZWrNPGvhNbzT22b8KZDwKprN/cQRuSw52aZpPMNm3EQa
 | 
				
			||||||
 | 
					AIAyyCG69ADQj7r/T6btybjZRKBDMlcfIIw5q9DGTQ/vlZCx6IX6DkZbYQmdwkTc
 | 
				
			||||||
 | 
					0D20GA2uWGxbggawhgq5/PTuv5SJKrrn4qBLS73u6eqcVeN5XA6q0kywd+9UhNxv
 | 
				
			||||||
 | 
					+W/xUxOJgE5pVto2VREBLonWSwZVfnyx6GjvC0sOzv0Ocv7KxAPNqtRwzQ9Wtr7s
 | 
				
			||||||
 | 
					klb84Nv3OW0MjTcjwfr481Cyy2DqgP5PFnSogWJuibR34jXAgbnX4BiGWrUdzaMU
 | 
				
			||||||
 | 
					86AlAoIBABnw9Bh41VFuc9/zxL7nLy++HW33HqFVc5Y1PXr/8sdhcisHQxhZVxek
 | 
				
			||||||
 | 
					JPbqIuAahDTIZsMnLy41QAKaoyt2fymMXqhJecjUuiwgOOlMxp82qu6Y30xM0Y6m
 | 
				
			||||||
 | 
					r6CkjSMUjcD1QwhOFJd01GCxM8BBIqQOpmR6fqxbQAu8hacKO3IuerCPryXwMt3A
 | 
				
			||||||
 | 
					7ppo/GlP55sySEg7K5I3pmuFHOxn0IPTgR6DfYMGBs9GXJ1lyjDD3z3Q42RhUsMC
 | 
				
			||||||
 | 
					jvwtra9fTL/N8EmAv2H39C8oqSRbfvIX5u3x6/ONFU8RhSFT5CDTADSYoVZ/0MxV
 | 
				
			||||||
 | 
					k53r0hqWz6D94r9QQmsJW4G1JwZYhx8CggEBANUybXsJRgDC5ZOlwbaq0FeTOpZ4
 | 
				
			||||||
 | 
					pSKx1RkVV+G93hK5XdXIVu365pSt3T68MgF+4wWlbC4MuKuzJltFnJBapzz5ygcU
 | 
				
			||||||
 | 
					jw+Q+l/jq+mJj8uvUfo5TAy9thuggj1a7SCs/dm5DBwYA7bfmamLbbBpA0qywMsF
 | 
				
			||||||
 | 
					/vL1bpePIFhbGMhBK7uiEzOAOZVuceZWqcUByjUYy925K/an0ANsQAch5PVeAsEv
 | 
				
			||||||
 | 
					wXC3soaCzfzUyv+eAYBy7LOcz+hpdRj1l4c9Zx6hZeBxMc5LSRoTs+HfE6GgTuI2
 | 
				
			||||||
 | 
					cCfCZNFw7DhDy1TBd3XjQ0AFykeaesImZVR6gp0NYH1kionsMtR5rl4C2tw=
 | 
				
			||||||
 | 
					-----END RSA PRIVATE KEY-----
 | 
				
			||||||
							
								
								
									
										21
									
								
								web/private/ecdsa_nistp256_sha256_cert.pem
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,21 @@
 | 
				
			|||||||
 | 
					-----BEGIN CERTIFICATE-----
 | 
				
			||||||
 | 
					MIIDYTCCAUmgAwIBAgIUA/EaCcXKgOxBiuaMNTackeF9DgcwDQYJKoZIhvcNAQEL
 | 
				
			||||||
 | 
					BQAwRzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRIwEAYDVQQKDAlSb2NrZXQg
 | 
				
			||||||
 | 
					Q0ExFzAVBgNVBAMMDlJvY2tldCBSb290IENBMB4XDTIxMDUxNzE5MDIyNFoXDTMx
 | 
				
			||||||
 | 
					MDUxNTE5MDIyNFowPzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMQ8wDQYDVQQK
 | 
				
			||||||
 | 
					DAZSb2NrZXQxEjAQBgNVBAMMCWxvY2FsaG9zdDBZMBMGByqGSM49AgEGCCqGSM49
 | 
				
			||||||
 | 
					AwEHA0IABOEVdmVd218y3Yy+ocjtt5siaFsNR7tknvS21pzKzxsFc05UrF74YqRx
 | 
				
			||||||
 | 
					Ic6/AQq56C48x6gDhUCdf+nMzD0NWTijGDAWMBQGA1UdEQQNMAuCCWxvY2FsaG9z
 | 
				
			||||||
 | 
					dDANBgkqhkiG9w0BAQsFAAOCAgEAW32kadHWEIsN5WXacjtgE9jbFii/rWJjrAO/
 | 
				
			||||||
 | 
					GWuARwLvjWeEFM5xSTny1cTDEz/7Bmfd9GRRpfgxKCFBGaU7zTmOV/AokrjUay+s
 | 
				
			||||||
 | 
					KPj0EV9ZE/84KVfr+MOuhwXhSSv+NvxduFw1sdhTUHs+Wopwjte+bnOUBXmnQH97
 | 
				
			||||||
 | 
					ITSQuUvEamPUS4tJD/kQ0qSGJKNv3CShhbe3XkjUd0UKK7lZzn6hY2fy3CrkkJDT
 | 
				
			||||||
 | 
					GyzFCRf3ZDfjW5/fGXN/TNoEK65pPQVr5taK/bovDesEO7rR4Y4YgtnGlJ7YToWh
 | 
				
			||||||
 | 
					E6I77CbsJCJdmgpjPNwRlwQ8upoWAfxOHqwg32s6uWq20v4TXZTOnEKOPEJG74bh
 | 
				
			||||||
 | 
					JHtwqsy2fIdGz4kZksKGerzJffyQPhX4f3qxxrijT9Hj2EoFIQ4u/jTRpK31IW3R
 | 
				
			||||||
 | 
					gEeNB8O4iyg3crx0oHRwc6zX63O6wBJCZ+0uxLoyjwtjKCxVYbJpccjoQh6zO3UO
 | 
				
			||||||
 | 
					pqAO+NKxQ469QDBV3uRyyQ7DRPu6p67/8gn1E/o8kyU1P7eLFIhBp4T4wCaMQBG6
 | 
				
			||||||
 | 
					IpL5W7eCyuOcqfdm471N07Z4KAqiV8eBJcKaAE+WzNIvrFvCZ/zq7Gj8p07nkjK8
 | 
				
			||||||
 | 
					+ZGDUieiY0mQEpiXLQZt1xNtT/MmlAzFYrK7t6gSygykQKjO1o4GG4g+eXu3h1YK
 | 
				
			||||||
 | 
					avsOwtc=
 | 
				
			||||||
 | 
					-----END CERTIFICATE-----
 | 
				
			||||||
							
								
								
									
										5
									
								
								web/private/ecdsa_nistp256_sha256_key_pkcs8.pem
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,5 @@
 | 
				
			|||||||
 | 
					-----BEGIN PRIVATE KEY-----
 | 
				
			||||||
 | 
					MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgeo/7wBIYVax9bj4m
 | 
				
			||||||
 | 
					1UEe2H+H4YLsbApDCZMslZbubRuhRANCAAThFXZlXdtfMt2MvqHI7bebImhbDUe7
 | 
				
			||||||
 | 
					ZJ70ttacys8bBXNOVKxe+GKkcSHOvwEKueguPMeoA4VAnX/pzMw9DVk4
 | 
				
			||||||
 | 
					-----END PRIVATE KEY-----
 | 
				
			||||||
							
								
								
									
										21
									
								
								web/private/ecdsa_nistp384_sha384_cert.pem
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,21 @@
 | 
				
			|||||||
 | 
					-----BEGIN CERTIFICATE-----
 | 
				
			||||||
 | 
					MIIDfjCCAWagAwIBAgIUfmkM99JpQUsQsh8TVILPQDBGOhowDQYJKoZIhvcNAQEM
 | 
				
			||||||
 | 
					BQAwRzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRIwEAYDVQQKDAlSb2NrZXQg
 | 
				
			||||||
 | 
					Q0ExFzAVBgNVBAMMDlJvY2tldCBSb290IENBMB4XDTIxMDUxNzE5MDIyNFoXDTMx
 | 
				
			||||||
 | 
					MDUxNTE5MDIyNFowPzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMQ8wDQYDVQQK
 | 
				
			||||||
 | 
					DAZSb2NrZXQxEjAQBgNVBAMMCWxvY2FsaG9zdDB2MBAGByqGSM49AgEGBSuBBAAi
 | 
				
			||||||
 | 
					A2IABM5rQQNZEGWeDyrg2dVQS15c+JsX/ginN9/ArHpTgGO6xaLfkbekIj7gewUR
 | 
				
			||||||
 | 
					VQcQrYulwu02HTFDzGVvf1DdCZzIJLJUpl+jagdI05yMPFg2s5lThzGB6HENyw1I
 | 
				
			||||||
 | 
					hU1dMaMYMBYwFAYDVR0RBA0wC4IJbG9jYWxob3N0MA0GCSqGSIb3DQEBDAUAA4IC
 | 
				
			||||||
 | 
					AQAzpXFEeCMvTBG+kzmHN/+esjCK8MO/Grrw3Qoa1stX0yjNWzfhlGHfWk9Mgwnp
 | 
				
			||||||
 | 
					DnIEFT9lB+nT9uTkKL81Opu2bkWXuL0MyjyYmcCfEgJeDblUbD4HYzPO/s2P7QZu
 | 
				
			||||||
 | 
					Oa1pNKBiSWe1xq+F/gkn0+nDfICq3QQOOQMzaAmlFmszN3v7pryz+x21MqTFsfuW
 | 
				
			||||||
 | 
					ymycRcwZ3VyYeceZuBxjOhR2l8I5yZbR+KPj8VS7A3vgqvkS8ixTK0LrxcLwrTIz
 | 
				
			||||||
 | 
					W9Z1skzVpux4K7iwn3qlWIJ7EbERi9Ydz0MzpjD+CHxGlgHTzT552DGZhPO8D7BE
 | 
				
			||||||
 | 
					+4IoS0YeXEGLeC2a9Hmiy3FXbM4nt3aIWMiWyhjslXIbvWOJL1Vi5GNpdQCG+rB7
 | 
				
			||||||
 | 
					lvA0IISp/tAi9duufdONjgRceRDsqf7o6TOBQdB3QxHRt9gCB2QquRh5llMUY8YH
 | 
				
			||||||
 | 
					PxFJAEzF4X5b0q0k4b50HRVNfDY0SC/XIR7S2xnLDGuoUqrOpUligBzfXV4JHrPv
 | 
				
			||||||
 | 
					YKHsWSOiVxU9eY1SYKuKqnOsGvXSSQlYqSK1ol94eOKDjNy8wekaVYAQNsdNL7o5
 | 
				
			||||||
 | 
					QSWZUcbZLkaNMFdD9twWGduAs0eTichVgyvRgPdevjTGDPBHKNSUURZRTYeW4aIJ
 | 
				
			||||||
 | 
					QzSQUQXwOXsQOmfCkaaTISsDwZAJ0wFqqST1AOhlCWD1OQ==
 | 
				
			||||||
 | 
					-----END CERTIFICATE-----
 | 
				
			||||||
							
								
								
									
										6
									
								
								web/private/ecdsa_nistp384_sha384_key_pkcs8.pem
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,6 @@
 | 
				
			|||||||
 | 
					-----BEGIN PRIVATE KEY-----
 | 
				
			||||||
 | 
					MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDCRBt7MeJXyJRq7grGQ
 | 
				
			||||||
 | 
					jEdBHE31hG5LuKQ5iL8yTVGk75Kc8ISll2LewbvQ3NhR6E+hZANiAATOa0EDWRBl
 | 
				
			||||||
 | 
					ng8q4NnVUEteXPibF/4IpzffwKx6U4BjusWi35G3pCI+4HsFEVUHEK2LpcLtNh0x
 | 
				
			||||||
 | 
					Q8xlb39Q3QmcyCSyVKZfo2oHSNOcjDxYNrOZU4cxgehxDcsNSIVNXTE=
 | 
				
			||||||
 | 
					-----END PRIVATE KEY-----
 | 
				
			||||||
							
								
								
									
										20
									
								
								web/private/ed25519_cert.pem
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,20 @@
 | 
				
			|||||||
 | 
					-----BEGIN CERTIFICATE-----
 | 
				
			||||||
 | 
					MIIDMjCCARqgAwIBAgIUSu1CGfaWlNPjjLG7SaEEgxI+AsAwDQYJKoZIhvcNAQEL
 | 
				
			||||||
 | 
					BQAwRzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRIwEAYDVQQKDAlSb2NrZXQg
 | 
				
			||||||
 | 
					Q0ExFzAVBgNVBAMMDlJvY2tldCBSb290IENBMB4XDTIxMDUxNzE5MDIyNFoXDTMx
 | 
				
			||||||
 | 
					MDUxNTE5MDIyNFowPzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMQ8wDQYDVQQK
 | 
				
			||||||
 | 
					DAZSb2NrZXQxEjAQBgNVBAMMCWxvY2FsaG9zdDAqMAUGAytlcAMhAKNv1fkv5rxY
 | 
				
			||||||
 | 
					xGT84C98g2xPpain+jsliHvYrqNkS1zhoxgwFjAUBgNVHREEDTALgglsb2NhbGhv
 | 
				
			||||||
 | 
					c3QwDQYJKoZIhvcNAQELBQADggIBAMgWZhr3whYbFPNYm5RZxjgEonMY7cH/Rza1
 | 
				
			||||||
 | 
					UrBkyoMRL9v00YKJTEvjl5Xl4ku7jqxY2qjValacDroD8hYTLhhkkbHxH6u+kJSC
 | 
				
			||||||
 | 
					cKRQ8QVBZMmoor8qD2Y2BuXZYGWEtgeoXh+DLHWOim7gXDD6GyFPnYFGHnRhNmkE
 | 
				
			||||||
 | 
					6fPcfw1FZmBrMM9rk31EeK9nLVRabL+vuLDEddX1LH4LwK4OuV51wQMZGYiC3M2b
 | 
				
			||||||
 | 
					JpmuPAUAUyiyN53Utuw/ubeih3cEglOwCyDPFt6XISYHO0UaQdw25uhpjxs8KTZB
 | 
				
			||||||
 | 
					qOPJAI4cvGaN3GARXvz2P/QHUiTzsf2XOXXtrO0g+F9P7t6x2xL35c0A8yxKkfsa
 | 
				
			||||||
 | 
					RKdCveRuVlsLXVLVBikqLE/OgPC6SIhIFvTYzXTaZyNfge6dRy2Wg6qWPcGwseeA
 | 
				
			||||||
 | 
					QpFKgWiY3cuy7nJm6uybR6zOEMj5GgNWIbICGguQ5161MLnv0bEmKh2d9/jQ/XN5
 | 
				
			||||||
 | 
					M+nixtGla/G4hL3FQIy9rhHVZzPWwSxl+/eE4qgnInEhmlQ2KrcpgneCt38t0vjJ
 | 
				
			||||||
 | 
					dwHZSzVv8yX7fE0OSq/DWQXJraWUcT7Ds0g7T7jWkWfS0qStVd9VJ+rYQ8dBbE9Y
 | 
				
			||||||
 | 
					gcmkm1DMUd+y9xDRDjxby7gMDWFiN2Au9FQgaIpIctTNCj0+agFBPnfXPVSIH6gX
 | 
				
			||||||
 | 
					10kA2ZVX
 | 
				
			||||||
 | 
					-----END CERTIFICATE-----
 | 
				
			||||||
							
								
								
									
										3
									
								
								web/private/ed25519_key.pem
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,3 @@
 | 
				
			|||||||
 | 
					-----BEGIN PRIVATE KEY-----
 | 
				
			||||||
 | 
					MC4CAQAwBQYDK2VwBCIEIGtLkaYE4D+P5HLkoc3a/tNhPP+gnhPLF3jEtdYcAtpd
 | 
				
			||||||
 | 
					-----END PRIVATE KEY-----
 | 
				
			||||||
							
								
								
									
										114
									
								
								web/private/gen_certs.sh
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,114 @@
 | 
				
			|||||||
 | 
					#! /bin/bash
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Usage:
 | 
				
			||||||
 | 
					#   ./gen_certs.sh [cert-kind]
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					# [cert-kind]:
 | 
				
			||||||
 | 
					#   ed25519
 | 
				
			||||||
 | 
					#   rsa_sha256
 | 
				
			||||||
 | 
					#   ecdsa_nistp256_sha256
 | 
				
			||||||
 | 
					#   ecdsa_nistp384_sha384
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					# Generate a certificate of the [cert-kind] key type, or if no cert-kind is
 | 
				
			||||||
 | 
					# specified, all of the certificates.
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					# Examples:
 | 
				
			||||||
 | 
					#   ./gen_certs.sh ed25519
 | 
				
			||||||
 | 
					#   ./gen_certs.sh rsa_sha256
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# TODO: `rustls` (really, `webpki`) doesn't currently use the CN in the subject
 | 
				
			||||||
 | 
					# to check if a certificate is valid for a server name sent via SNI. It's not
 | 
				
			||||||
 | 
					# clear if this is intended, since certificates _should_ have a `subjectAltName`
 | 
				
			||||||
 | 
					# with a DNS name, or if it simply hasn't been implemented yet. See
 | 
				
			||||||
 | 
					# https://bugzilla.mozilla.org/show_bug.cgi?id=552346 for a bit more info.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					CA_SUBJECT="/C=US/ST=CA/O=Rocket CA/CN=Rocket Root CA"
 | 
				
			||||||
 | 
					SUBJECT="/C=US/ST=CA/O=Rocket/CN=localhost"
 | 
				
			||||||
 | 
					ALT="DNS:localhost"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function gen_ca() {
 | 
				
			||||||
 | 
					  openssl genrsa -out ca_key.pem 4096
 | 
				
			||||||
 | 
					  openssl req -new -x509 -days 3650 -key ca_key.pem \
 | 
				
			||||||
 | 
					    -subj "${CA_SUBJECT}" -out ca_cert.pem
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function gen_ca_if_non_existent() {
 | 
				
			||||||
 | 
					  if ! [ -f ./ca_cert.pem ]; then gen_ca; fi
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function gen_rsa_sha256() {
 | 
				
			||||||
 | 
					  gen_ca_if_non_existent
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  openssl req -newkey rsa:4096 -nodes -sha256 -keyout rsa_sha256_key.pem \
 | 
				
			||||||
 | 
					    -subj "${SUBJECT}" -out server.csr
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  openssl x509 -req -sha256 -extfile <(printf "subjectAltName=${ALT}") -days 3650 \
 | 
				
			||||||
 | 
					    -CA ca_cert.pem -CAkey ca_key.pem -CAcreateserial \
 | 
				
			||||||
 | 
					    -in server.csr -out rsa_sha256_cert.pem
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  rm ca_cert.srl server.csr
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function gen_ed25519() {
 | 
				
			||||||
 | 
					  gen_ca_if_non_existent
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  openssl genpkey -algorithm ED25519 > ed25519_key.pem
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  openssl req -new -key ed25519_key.pem -subj "${SUBJECT}" -out server.csr
 | 
				
			||||||
 | 
					  openssl x509 -req -extfile <(printf "subjectAltName=${ALT}") -days 3650 \
 | 
				
			||||||
 | 
					    -CA ca_cert.pem -CAkey ca_key.pem -CAcreateserial \
 | 
				
			||||||
 | 
					    -in server.csr -out ed25519_cert.pem
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  rm ca_cert.srl server.csr
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function gen_ecdsa_nistp256_sha256() {
 | 
				
			||||||
 | 
					  gen_ca_if_non_existent
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  openssl ecparam -out ecdsa_nistp256_sha256_key.pem -name prime256v1 -genkey
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # Convert to pkcs8 format supported by rustls
 | 
				
			||||||
 | 
					  openssl pkcs8 -topk8 -nocrypt -in ecdsa_nistp256_sha256_key.pem \
 | 
				
			||||||
 | 
					    -out ecdsa_nistp256_sha256_key_pkcs8.pem
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  openssl req -new -nodes -sha256 -key ecdsa_nistp256_sha256_key_pkcs8.pem \
 | 
				
			||||||
 | 
					    -subj "${SUBJECT}" -out server.csr
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  openssl x509 -req -sha256 -extfile <(printf "subjectAltName=${ALT}") -days 3650 \
 | 
				
			||||||
 | 
					    -CA ca_cert.pem -CAkey ca_key.pem -CAcreateserial \
 | 
				
			||||||
 | 
					    -in server.csr -out ecdsa_nistp256_sha256_cert.pem
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  rm ca_cert.srl server.csr ecdsa_nistp256_sha256_key.pem
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function gen_ecdsa_nistp384_sha384() {
 | 
				
			||||||
 | 
					  gen_ca_if_non_existent
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  openssl ecparam -out ecdsa_nistp384_sha384_key.pem -name secp384r1 -genkey
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # Convert to pkcs8 format supported by rustls
 | 
				
			||||||
 | 
					  openssl pkcs8 -topk8 -nocrypt -in ecdsa_nistp384_sha384_key.pem \
 | 
				
			||||||
 | 
					    -out ecdsa_nistp384_sha384_key_pkcs8.pem
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  openssl req -new -nodes -sha384 -key ecdsa_nistp384_sha384_key_pkcs8.pem \
 | 
				
			||||||
 | 
					    -subj "${SUBJECT}" -out server.csr
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  openssl x509 -req -sha384 -extfile <(printf "subjectAltName=${ALT}") -days 3650 \
 | 
				
			||||||
 | 
					    -CA ca_cert.pem -CAkey ca_key.pem -CAcreateserial \
 | 
				
			||||||
 | 
					    -in server.csr -out ecdsa_nistp384_sha384_cert.pem
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  rm ca_cert.srl server.csr ecdsa_nistp384_sha384_key.pem
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					case $1 in
 | 
				
			||||||
 | 
					  ed25519) gen_ed25519 ;;
 | 
				
			||||||
 | 
					  rsa_sha256) gen_rsa_sha256 ;;
 | 
				
			||||||
 | 
					  ecdsa_nistp256_sha256) gen_ecdsa_nistp256_sha256 ;;
 | 
				
			||||||
 | 
					  ecdsa_nistp384_sha384) gen_ecdsa_nistp384_sha384 ;;
 | 
				
			||||||
 | 
					  *)
 | 
				
			||||||
 | 
					    gen_ed25519
 | 
				
			||||||
 | 
					    gen_rsa_sha256
 | 
				
			||||||
 | 
					    gen_ecdsa_nistp256_sha256
 | 
				
			||||||
 | 
					    gen_ecdsa_nistp384_sha384
 | 
				
			||||||
 | 
					    ;;
 | 
				
			||||||
 | 
					esac
 | 
				
			||||||
							
								
								
									
										30
									
								
								web/private/rsa_sha256_cert.pem
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,30 @@
 | 
				
			|||||||
 | 
					-----BEGIN CERTIFICATE-----
 | 
				
			||||||
 | 
					MIIFLDCCAxSgAwIBAgIUZ461KXDgTT25LP1S6PK6i9ZKtOYwDQYJKoZIhvcNAQEL
 | 
				
			||||||
 | 
					BQAwRzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRIwEAYDVQQKDAlSb2NrZXQg
 | 
				
			||||||
 | 
					Q0ExFzAVBgNVBAMMDlJvY2tldCBSb290IENBMB4XDTIxMDUxNzE5MDIyNFoXDTMx
 | 
				
			||||||
 | 
					MDUxNTE5MDIyNFowPzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMQ8wDQYDVQQK
 | 
				
			||||||
 | 
					DAZSb2NrZXQxEjAQBgNVBAMMCWxvY2FsaG9zdDCCAiIwDQYJKoZIhvcNAQEBBQAD
 | 
				
			||||||
 | 
					ggIPADCCAgoCggIBAKl8Re1dneFB2obYPOuZf3d6BR2xGcMhr2vmHpnVgkeg/xGI
 | 
				
			||||||
 | 
					cwHhhB04mEg4TqbZ0Q1wdKN4ruNigdrQAXDqnm4y2IB1q/uTdoXVWNsvAZmDq4N4
 | 
				
			||||||
 | 
					rPUDWagpkilV4HOHDQOI27Lo/+30wJX6H8i3J6Nag1y9spuySkL1F1SgQng9uzvP
 | 
				
			||||||
 | 
					3SwbsO9R/jS7qTZNEtirnJv3qJlxmT4BCvBuWNALShFlSZYnZk/2csYXEaVkWMUE
 | 
				
			||||||
 | 
					rnWGmTmzMZEzDOdQdPC3sJDCPQbbgD4SnQANqhOVjPSdJ6Y6joN6XYtB2EP+6LJ8
 | 
				
			||||||
 | 
					UBTICETnUROWeKqMm215Gsen32cyWkaCwEWtTFn7rJHbPvUY4vPYQZAB2/h07lYq
 | 
				
			||||||
 | 
					v67W3r5lTq1KuoStD8M/7nCIYIuNhmJLMzzWrSfJrQpYexoMII6kjOxv2kiUgb1y
 | 
				
			||||||
 | 
					bYKnFlVf4up3pTpi2/0We4SHRgc7xTcEPNvU5z5hc5JwHZIFjmi5dx50ZoAULrXl
 | 
				
			||||||
 | 
					OUhfdUPut7H38UQg3riJiNAzedUiTubek26po8qH1iS/EMgAi5RboaBovjWhgjwq
 | 
				
			||||||
 | 
					P/RP0vzpln6OdQIRaRlY9vZiik9HbFybafuA2zEkyc+zc4HqJk3vqX/0hgd9qWeL
 | 
				
			||||||
 | 
					zgsHr6A86tNUXsUaoInQbzznjpbFE85klxd6HeqasOyVDCeDUs4lWoDNp0DbAgMB
 | 
				
			||||||
 | 
					AAGjGDAWMBQGA1UdEQQNMAuCCWxvY2FsaG9zdDANBgkqhkiG9w0BAQsFAAOCAgEA
 | 
				
			||||||
 | 
					sS2TUKKYtNYC68TEQb5KAa8/zi6Lhz9lyn9rpBXAcAVeBdxCqrBU0nRN8EjXEff1
 | 
				
			||||||
 | 
					oBeau9Wi9PwdK6J+4xFuer9YrZZWuWLKUBXJL9ZXEfI+USo5WVz7v/puoKZSUSu2
 | 
				
			||||||
 | 
					+Ee4+v1eoAsCtaA8IR0Sh1KTc9kg1QZW1nIdBV0zgAERWTzm1iy2Ff+euowM/lUR
 | 
				
			||||||
 | 
					FczMAJsNq83O2kaH541fRx3gC2iMN/QLwRalBR2y8hTI5wQa0Yr8moC4IsTfRrKQ
 | 
				
			||||||
 | 
					/SZDjFT1XoA9TnrByIvGQNlxK1Rm2hvK1OQn4OL6sdYK6F+MxfUn/atQNyBQ6CF+
 | 
				
			||||||
 | 
					oKuvOnE2jces8GGZIU1jYJ2271SQkzp0CW3N1ZKjClSQFAYED+5ERTGyeEL4o3Vr
 | 
				
			||||||
 | 
					V/BUd0KC7HhbWBlFc2EDmPOoOTp/ID901Kf499M68pe2Fr/63/nCAON6WFk1NPYA
 | 
				
			||||||
 | 
					+sWMfS25hikU5rOHGQgXxXAz90pwpvpoLQvaq0/azsbHvq9B4ubdcLE0xcoK+DGq
 | 
				
			||||||
 | 
					+/aOeWlYkalWp93akPYpWN3UJXzDXzzagRID/1LEGS+Ssbh1WVhAhLKVoxzBvkgm
 | 
				
			||||||
 | 
					ozVAhR3zHXI2QpQ2FEbOmkcRtpxv2CiaTsgHg2MK8cdmPx3G+ufCZjRbQx/VXVdN
 | 
				
			||||||
 | 
					vaFRdmzr/5EQ7DT5Uy8w32h1to4Fm2sAtbAFYaFLXaM=
 | 
				
			||||||
 | 
					-----END CERTIFICATE-----
 | 
				
			||||||
							
								
								
									
										52
									
								
								web/private/rsa_sha256_key.pem
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,52 @@
 | 
				
			|||||||
 | 
					-----BEGIN PRIVATE KEY-----
 | 
				
			||||||
 | 
					MIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQCpfEXtXZ3hQdqG
 | 
				
			||||||
 | 
					2DzrmX93egUdsRnDIa9r5h6Z1YJHoP8RiHMB4YQdOJhIOE6m2dENcHSjeK7jYoHa
 | 
				
			||||||
 | 
					0AFw6p5uMtiAdav7k3aF1VjbLwGZg6uDeKz1A1moKZIpVeBzhw0DiNuy6P/t9MCV
 | 
				
			||||||
 | 
					+h/ItyejWoNcvbKbskpC9RdUoEJ4Pbs7z90sG7DvUf40u6k2TRLYq5yb96iZcZk+
 | 
				
			||||||
 | 
					AQrwbljQC0oRZUmWJ2ZP9nLGFxGlZFjFBK51hpk5szGRMwznUHTwt7CQwj0G24A+
 | 
				
			||||||
 | 
					Ep0ADaoTlYz0nSemOo6Del2LQdhD/uiyfFAUyAhE51ETlniqjJtteRrHp99nMlpG
 | 
				
			||||||
 | 
					gsBFrUxZ+6yR2z71GOLz2EGQAdv4dO5WKr+u1t6+ZU6tSrqErQ/DP+5wiGCLjYZi
 | 
				
			||||||
 | 
					SzM81q0nya0KWHsaDCCOpIzsb9pIlIG9cm2CpxZVX+Lqd6U6Ytv9FnuEh0YHO8U3
 | 
				
			||||||
 | 
					BDzb1Oc+YXOScB2SBY5ouXcedGaAFC615TlIX3VD7rex9/FEIN64iYjQM3nVIk7m
 | 
				
			||||||
 | 
					3pNuqaPKh9YkvxDIAIuUW6GgaL41oYI8Kj/0T9L86ZZ+jnUCEWkZWPb2YopPR2xc
 | 
				
			||||||
 | 
					m2n7gNsxJMnPs3OB6iZN76l/9IYHfalni84LB6+gPOrTVF7FGqCJ0G88546WxRPO
 | 
				
			||||||
 | 
					ZJcXeh3qmrDslQwng1LOJVqAzadA2wIDAQABAoICABT4gHp/Q+K0UEKxDNCl/ISe
 | 
				
			||||||
 | 
					/3UODb78MwVpws2MAoO0YvsbZAeOjNdEwmrlNK4mc1xzVqtHanROIv0dEaCUFyhR
 | 
				
			||||||
 | 
					eEJkzPPi6h5jKIxuQ4doKFerHdNvJ6/L/P7KVmxVAII4c96uP8SErTOhcD9Ykjn/
 | 
				
			||||||
 | 
					IBPgkPH83H1ucAWTksXn9XvQG3CyuHDUN1z0/1ntrXBLw6P0v9LEoI5weJcJQEn1
 | 
				
			||||||
 | 
					q6N9Yd6HX3xzZP4nqpJJWUZ/bsqx7dGa3340z9rrNJz4TYuLzRtFG5gSm4R/LFUi
 | 
				
			||||||
 | 
					Av/dViOWST3xbROnAQhgyRAUm6AGpCdKa9i9nI6VuUGRY4PivJy7OTpSQVIdwD2K
 | 
				
			||||||
 | 
					VFbFauCmsTY7B+Z+DXe9JJV+Tlazvlwop1DApxCgLMNr2pVB/1yPzMrNYDB4Sg1c
 | 
				
			||||||
 | 
					T3stu4A8PtljTykla2C2LPFrdIijvYv9n6PSPWV4vf1ix9uYs99FhZPQJMgm+CDr
 | 
				
			||||||
 | 
					n3cFfuofIWIRJpCuu3hn4woGgW2bXor4pyMNg1yCp1T2c8g6eJUYAAIRpse0HDbT
 | 
				
			||||||
 | 
					ZUifxlNBx819bf9aqv+lhwMU4UFlmWMv3NETcCBqgUn+z4scNJ3kZwO9ZodLRblK
 | 
				
			||||||
 | 
					SpalZ6SNejTnVukg3zbwJG8w5vIrJvfjBkikAL1O5X6Kn3YqfrXAl38bIvA50ZBe
 | 
				
			||||||
 | 
					eOFcgDPClLPGlNKxQXiZAoIBAQDh0aArTjWi8Kxt2Ga3Hu4jQ7IXklL9osGAlFCB
 | 
				
			||||||
 | 
					wZs831/ktLY0c7vY+mbwrY4AtqK21A00MrNSTsp/McHwAUi410DdcTqzkGnm9BBZ
 | 
				
			||||||
 | 
					FKVO1H9KGg2t344tOXcPsFTl6SR5a0aPqagyvgdH+YWC4ccfYZWMcLELn3sOGoEp
 | 
				
			||||||
 | 
					a7VBxjLZZ+/b+/oIzhwxEaBi8N0U3IZ3SsC9Lmojnb9+LMwGs14TFfahD3uM1hnU
 | 
				
			||||||
 | 
					vww1elwPwXafWc+7Ej1bXijWBXbyzkCITzHafmDsAatEZnemHL8zKKtTn2aSTUSj
 | 
				
			||||||
 | 
					Fl/y94WR7/V4nVEQ0R3fjGC0rKh08BtKzxPjYBqjT5aI7+yfAoIBAQDAIzROr00o
 | 
				
			||||||
 | 
					65auD5TB2k/pSVKQpB/U6ESGafDxg4a+R9Ng2ESmDN5hUUZDefm0Uxf9AZqS6eno
 | 
				
			||||||
 | 
					GSAqxNDixWPy2WFzbZ0mUCoyQn+Eh09WBvt0IqDZ2+ngBEXiKA/8dsvXinBHLuHV
 | 
				
			||||||
 | 
					u+rJdLMzPfhWVo1/iXq7SOjBvDuthKROku5BEt6Q3Cs0nk6uneRIqKtaqa7KIInF
 | 
				
			||||||
 | 
					BPtUifZtCP09h6iFFGNv2g2OYRYDg8TXvjt3scKy0DUC3kTeTCPvNvHaOObGbMRU
 | 
				
			||||||
 | 
					Q85HkXburxWfsMto5m/lRsbY3COxddB47WIQ6QNewESfCab/R2lOdlXQeaH8fuxT
 | 
				
			||||||
 | 
					wWC5DMz4F0ZFAoIBAFm+EjZDnaNEnHIHB0MNIryXAabGev7beKUdzCTVCVmWuChO
 | 
				
			||||||
 | 
					/P45ZFTlppVNk9qKun2IJjsxTvyN3YHRB27XQ8xZlyiqABcudDfZlMmiH9QFNRUA
 | 
				
			||||||
 | 
					56DK8Fjetodgn0zDa8BpNqCPXw3TYVdkPX/3NEgvYtxuSJ4C4keHlv8cE+uw1bJ6
 | 
				
			||||||
 | 
					0OMO754iMyf5BlFrwaCxxyqPZauJT5sZ7Ok66lZbYC6bkukNGx+sUpWu2y5Bk2ab
 | 
				
			||||||
 | 
					jwXjDmAc7o9qCzaK82upNhI1zu0zPldsjmDfi/tS/1VYe0X/WicYWAesM7N+VPHb
 | 
				
			||||||
 | 
					eCVX98iEIqgdxKzo1QWsClyfkRrSraNrVLrVBqcCggEAaLIGK6YMNnMBPUGSPnt2
 | 
				
			||||||
 | 
					NdlVWymDiuExjcimmQOhZYf/33KZHZ4/gunljpklfqQUmzHHh6xcX7NpOsTaSedj
 | 
				
			||||||
 | 
					Sg43ss0U566g/5gKoi2VBnxxglvoKC5T51SMu+o2o8wb0QxHmBIszulBy5qClzZ6
 | 
				
			||||||
 | 
					Xpl1Kvy/2tOkuQSXxDpVydb4ao8cpfTCuj5VA4NXxFvcW1/AtbU7PRc02GEA3XMb
 | 
				
			||||||
 | 
					gu6r3jA46tb3shCnDS09Eo4/Gz7Kp+MaL8Dr5/G3Vv8qlE2TOqZD6OK1wXu7Qd43
 | 
				
			||||||
 | 
					uzd772I5sMZ7TenOrUFUYsB/QlWmF3hPLBX3YH0KHc4PfrT4lnyWzCDAUrVt7vXH
 | 
				
			||||||
 | 
					vQKCAQEAz1Q+jW+NG7CAqkOJ19n+KmGn+zddvy8x4txKx8kV0+3XIVNkseS6IT65
 | 
				
			||||||
 | 
					uemD85Gn7MYwHxcKUHghHSKyJsvCb1vSmB3JtCnrsodmQNMb6Lsq89VlM8Ylc/s3
 | 
				
			||||||
 | 
					F4AraiOlylqcEwLkanD3XSfPdQZhExZHEtpAW/Rr6zYT9J94VO1nK3+RkFI4a8kl
 | 
				
			||||||
 | 
					pEbY+tqJj3LGTuaQkshB9rDtcBT5/JsaxlMk783qkKziCPZF47BWqrJb+t7tm3qg
 | 
				
			||||||
 | 
					5gf+QUInEjdW3k3uZBL4316RP/TJlLvo29PkEwoC8X4R2EgDeHQhhRC2Fi2Wpy4O
 | 
				
			||||||
 | 
					ce4G+zZOOYXwvWGJLwNhgsve8C3oqg==
 | 
				
			||||||
 | 
					-----END PRIVATE KEY-----
 | 
				
			||||||
							
								
								
									
										52
									
								
								web/src/consts.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,52 @@
 | 
				
			|||||||
 | 
					pub const DISCORD_OAUTH_TOKEN: &'static str = "https://discord.com/api/oauth2/token";
 | 
				
			||||||
 | 
					pub const DISCORD_OAUTH_AUTHORIZE: &'static str = "https://discord.com/api/oauth2/authorize";
 | 
				
			||||||
 | 
					pub const DISCORD_API: &'static str = "https://discord.com/api";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub const MAX_CONTENT_LENGTH: usize = 2000;
 | 
				
			||||||
 | 
					pub const MAX_EMBED_DESCRIPTION_LENGTH: usize = 4096;
 | 
				
			||||||
 | 
					pub const MAX_EMBED_TITLE_LENGTH: usize = 256;
 | 
				
			||||||
 | 
					pub const MAX_EMBED_AUTHOR_LENGTH: usize = 256;
 | 
				
			||||||
 | 
					pub const MAX_EMBED_FOOTER_LENGTH: usize = 2048;
 | 
				
			||||||
 | 
					pub const MAX_URL_LENGTH: usize = 512;
 | 
				
			||||||
 | 
					pub const MAX_USERNAME_LENGTH: usize = 100;
 | 
				
			||||||
 | 
					pub const MAX_EMBED_FIELDS: usize = 25;
 | 
				
			||||||
 | 
					pub const MAX_EMBED_FIELD_TITLE_LENGTH: usize = 256;
 | 
				
			||||||
 | 
					pub const MAX_EMBED_FIELD_VALUE_LENGTH: usize = 1024;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub const MINUTE: usize = 60;
 | 
				
			||||||
 | 
					pub const HOUR: usize = 60 * MINUTE;
 | 
				
			||||||
 | 
					pub const DAY: usize = 24 * HOUR;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub const CHARACTERS: &str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use std::{collections::HashSet, env, iter::FromIterator};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use lazy_static::lazy_static;
 | 
				
			||||||
 | 
					use serenity::model::prelude::AttachmentType;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					lazy_static! {
 | 
				
			||||||
 | 
					    pub static ref DEFAULT_AVATAR: AttachmentType<'static> = (
 | 
				
			||||||
 | 
					        include_bytes!(concat!(
 | 
				
			||||||
 | 
					            env!("CARGO_MANIFEST_DIR"),
 | 
				
			||||||
 | 
					            "/../assets/",
 | 
				
			||||||
 | 
					            env!("WEBHOOK_AVATAR", "WEBHOOK_AVATAR not provided for compilation")
 | 
				
			||||||
 | 
					        )) as &[u8],
 | 
				
			||||||
 | 
					        env!("WEBHOOK_AVATAR"),
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					        .into();
 | 
				
			||||||
 | 
					    pub static ref SUBSCRIPTION_ROLES: HashSet<u64> = HashSet::from_iter(
 | 
				
			||||||
 | 
					        env::var("SUBSCRIPTION_ROLES")
 | 
				
			||||||
 | 
					            .map(|var| var
 | 
				
			||||||
 | 
					                .split(',')
 | 
				
			||||||
 | 
					                .filter_map(|item| { item.parse::<u64>().ok() })
 | 
				
			||||||
 | 
					                .collect::<Vec<u64>>())
 | 
				
			||||||
 | 
					            .unwrap_or_else(|_| Vec::new())
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    pub static ref CNC_GUILD: Option<u64> =
 | 
				
			||||||
 | 
					        env::var("CNC_GUILD").map(|var| var.parse::<u64>().ok()).ok().flatten();
 | 
				
			||||||
 | 
					    pub static ref MIN_INTERVAL: u32 = env::var("MIN_INTERVAL")
 | 
				
			||||||
 | 
					        .ok()
 | 
				
			||||||
 | 
					        .map(|inner| inner.parse::<u32>().ok())
 | 
				
			||||||
 | 
					        .flatten()
 | 
				
			||||||
 | 
					        .unwrap_or(600);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										196
									
								
								web/src/lib.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,196 @@
 | 
				
			|||||||
 | 
					#[macro_use]
 | 
				
			||||||
 | 
					extern crate rocket;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					mod consts;
 | 
				
			||||||
 | 
					#[macro_use]
 | 
				
			||||||
 | 
					mod macros;
 | 
				
			||||||
 | 
					mod routes;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use std::{collections::HashMap, env};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use oauth2::{basic::BasicClient, AuthUrl, ClientId, ClientSecret, RedirectUrl, TokenUrl};
 | 
				
			||||||
 | 
					use rocket::{
 | 
				
			||||||
 | 
					    fs::FileServer,
 | 
				
			||||||
 | 
					    serde::json::{json, Value as JsonValue},
 | 
				
			||||||
 | 
					    tokio::sync::broadcast::Sender,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					use rocket_dyn_templates::Template;
 | 
				
			||||||
 | 
					use serenity::{
 | 
				
			||||||
 | 
					    client::Context,
 | 
				
			||||||
 | 
					    http::CacheHttp,
 | 
				
			||||||
 | 
					    model::id::{GuildId, UserId},
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					use sqlx::{MySql, Pool};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::consts::{CNC_GUILD, DISCORD_OAUTH_AUTHORIZE, DISCORD_OAUTH_TOKEN, SUBSCRIPTION_ROLES};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type Database = MySql;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Debug)]
 | 
				
			||||||
 | 
					enum Error {
 | 
				
			||||||
 | 
					    SQLx(sqlx::Error),
 | 
				
			||||||
 | 
					    Serenity(serenity::Error),
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[catch(401)]
 | 
				
			||||||
 | 
					async fn not_authorized() -> Template {
 | 
				
			||||||
 | 
					    let map: HashMap<String, String> = HashMap::new();
 | 
				
			||||||
 | 
					    Template::render("errors/401", &map)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[catch(403)]
 | 
				
			||||||
 | 
					async fn forbidden() -> Template {
 | 
				
			||||||
 | 
					    let map: HashMap<String, String> = HashMap::new();
 | 
				
			||||||
 | 
					    Template::render("errors/403", &map)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[catch(404)]
 | 
				
			||||||
 | 
					async fn not_found() -> Template {
 | 
				
			||||||
 | 
					    let map: HashMap<String, String> = HashMap::new();
 | 
				
			||||||
 | 
					    Template::render("errors/404", &map)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[catch(413)]
 | 
				
			||||||
 | 
					async fn payload_too_large() -> JsonValue {
 | 
				
			||||||
 | 
					    json!({"error": "Data too large.", "errors": ["Data too large."]})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[catch(422)]
 | 
				
			||||||
 | 
					async fn unprocessable_entity() -> JsonValue {
 | 
				
			||||||
 | 
					    json!({"error": "Invalid request.", "errors": ["Invalid request."]})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[catch(500)]
 | 
				
			||||||
 | 
					async fn internal_server_error() -> Template {
 | 
				
			||||||
 | 
					    let map: HashMap<String, String> = HashMap::new();
 | 
				
			||||||
 | 
					    Template::render("errors/500", &map)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub async fn initialize(
 | 
				
			||||||
 | 
					    kill_channel: Sender<()>,
 | 
				
			||||||
 | 
					    serenity_context: Context,
 | 
				
			||||||
 | 
					    db_pool: Pool<Database>,
 | 
				
			||||||
 | 
					) -> Result<(), Box<dyn std::error::Error>> {
 | 
				
			||||||
 | 
					    info!("Checking environment variables...");
 | 
				
			||||||
 | 
					    env::var("OAUTH2_CLIENT_ID").expect("`OAUTH2_CLIENT_ID' not supplied");
 | 
				
			||||||
 | 
					    env::var("OAUTH2_CLIENT_SECRET").expect("`OAUTH2_CLIENT_SECRET' not supplied");
 | 
				
			||||||
 | 
					    env::var("OAUTH2_DISCORD_CALLBACK").expect("`OAUTH2_DISCORD_CALLBACK' not supplied");
 | 
				
			||||||
 | 
					    env::var("PATREON_GUILD_ID").expect("`PATREON_GUILD' not supplied");
 | 
				
			||||||
 | 
					    info!("Done!");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let oauth2_client = BasicClient::new(
 | 
				
			||||||
 | 
					        ClientId::new(env::var("OAUTH2_CLIENT_ID")?),
 | 
				
			||||||
 | 
					        Some(ClientSecret::new(env::var("OAUTH2_CLIENT_SECRET")?)),
 | 
				
			||||||
 | 
					        AuthUrl::new(DISCORD_OAUTH_AUTHORIZE.to_string())?,
 | 
				
			||||||
 | 
					        Some(TokenUrl::new(DISCORD_OAUTH_TOKEN.to_string())?),
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    .set_redirect_uri(RedirectUrl::new(env::var("OAUTH2_DISCORD_CALLBACK")?)?);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let reqwest_client = reqwest::Client::new();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    rocket::build()
 | 
				
			||||||
 | 
					        .attach(Template::fairing())
 | 
				
			||||||
 | 
					        .register(
 | 
				
			||||||
 | 
					            "/",
 | 
				
			||||||
 | 
					            catchers![
 | 
				
			||||||
 | 
					                not_authorized,
 | 
				
			||||||
 | 
					                forbidden,
 | 
				
			||||||
 | 
					                not_found,
 | 
				
			||||||
 | 
					                internal_server_error,
 | 
				
			||||||
 | 
					                unprocessable_entity,
 | 
				
			||||||
 | 
					                payload_too_large,
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        .manage(oauth2_client)
 | 
				
			||||||
 | 
					        .manage(reqwest_client)
 | 
				
			||||||
 | 
					        .manage(serenity_context)
 | 
				
			||||||
 | 
					        .manage(db_pool)
 | 
				
			||||||
 | 
					        .mount("/static", FileServer::from(concat!(env!("CARGO_MANIFEST_DIR"), "/static")))
 | 
				
			||||||
 | 
					        .mount(
 | 
				
			||||||
 | 
					            "/",
 | 
				
			||||||
 | 
					            routes![
 | 
				
			||||||
 | 
					                routes::index,
 | 
				
			||||||
 | 
					                routes::cookies,
 | 
				
			||||||
 | 
					                routes::privacy,
 | 
				
			||||||
 | 
					                routes::terms,
 | 
				
			||||||
 | 
					                routes::return_to_same_site
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        .mount(
 | 
				
			||||||
 | 
					            "/help",
 | 
				
			||||||
 | 
					            routes![
 | 
				
			||||||
 | 
					                routes::help,
 | 
				
			||||||
 | 
					                routes::help_timezone,
 | 
				
			||||||
 | 
					                routes::help_create_reminder,
 | 
				
			||||||
 | 
					                routes::help_delete_reminder,
 | 
				
			||||||
 | 
					                routes::help_timers,
 | 
				
			||||||
 | 
					                routes::help_todo_lists,
 | 
				
			||||||
 | 
					                routes::help_macros,
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        .mount("/login", routes![routes::login::discord_login, routes::login::discord_callback])
 | 
				
			||||||
 | 
					        .mount(
 | 
				
			||||||
 | 
					            "/dashboard",
 | 
				
			||||||
 | 
					            routes![
 | 
				
			||||||
 | 
					                routes::dashboard::dashboard,
 | 
				
			||||||
 | 
					                routes::dashboard::dashboard_home,
 | 
				
			||||||
 | 
					                routes::dashboard::user::get_user_info,
 | 
				
			||||||
 | 
					                routes::dashboard::user::update_user_info,
 | 
				
			||||||
 | 
					                routes::dashboard::user::get_user_guilds,
 | 
				
			||||||
 | 
					                routes::dashboard::guild::get_guild_patreon,
 | 
				
			||||||
 | 
					                routes::dashboard::guild::get_guild_channels,
 | 
				
			||||||
 | 
					                routes::dashboard::guild::get_guild_roles,
 | 
				
			||||||
 | 
					                routes::dashboard::guild::get_reminder_templates,
 | 
				
			||||||
 | 
					                routes::dashboard::guild::create_reminder_template,
 | 
				
			||||||
 | 
					                routes::dashboard::guild::delete_reminder_template,
 | 
				
			||||||
 | 
					                routes::dashboard::guild::create_reminder,
 | 
				
			||||||
 | 
					                routes::dashboard::guild::get_reminders,
 | 
				
			||||||
 | 
					                routes::dashboard::guild::edit_reminder,
 | 
				
			||||||
 | 
					                routes::dashboard::guild::delete_reminder,
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        .launch()
 | 
				
			||||||
 | 
					        .await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    warn!("Exiting rocket runtime");
 | 
				
			||||||
 | 
					    // distribute kill signal
 | 
				
			||||||
 | 
					    match kill_channel.send(()) {
 | 
				
			||||||
 | 
					        Ok(_) => {}
 | 
				
			||||||
 | 
					        Err(e) => {
 | 
				
			||||||
 | 
					            error!("Failed to issue kill signal: {:?}", e);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    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
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										119
									
								
								web/src/macros.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,119 @@
 | 
				
			|||||||
 | 
					macro_rules! check_length {
 | 
				
			||||||
 | 
					    ($max:ident, $field:expr) => {
 | 
				
			||||||
 | 
					        if $field.len() > $max {
 | 
				
			||||||
 | 
					            return json!({ "error": format!("{} exceeded", stringify!($max)) });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    ($max:ident, $field:expr, $($fields:expr),+) => {
 | 
				
			||||||
 | 
					        check_length!($max, $field);
 | 
				
			||||||
 | 
					        check_length!($max, $($fields),+);
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					macro_rules! check_length_opt {
 | 
				
			||||||
 | 
					    ($max:ident, $field:expr) => {
 | 
				
			||||||
 | 
					        if let Some(field) = &$field {
 | 
				
			||||||
 | 
					            check_length!($max, field);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    ($max:ident, $field:expr, $($fields:expr),+) => {
 | 
				
			||||||
 | 
					        check_length_opt!($max, $field);
 | 
				
			||||||
 | 
					        check_length_opt!($max, $($fields),+);
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					macro_rules! check_url {
 | 
				
			||||||
 | 
					    ($field:expr) => {
 | 
				
			||||||
 | 
					        if !($field.starts_with("http://") || $field.starts_with("https://")) {
 | 
				
			||||||
 | 
					            return json!({ "error": "URL invalid" });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    ($field:expr, $($fields:expr),+) => {
 | 
				
			||||||
 | 
					        check_url!($max, $field);
 | 
				
			||||||
 | 
					        check_url!($max, $($fields),+);
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					macro_rules! check_url_opt {
 | 
				
			||||||
 | 
					    ($field:expr) => {
 | 
				
			||||||
 | 
					        if let Some(field) = &$field {
 | 
				
			||||||
 | 
					            check_url!(field);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    ($field:expr, $($fields:expr),+) => {
 | 
				
			||||||
 | 
					        check_url_opt!($field);
 | 
				
			||||||
 | 
					        check_url_opt!($($fields),+);
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					macro_rules! check_authorization {
 | 
				
			||||||
 | 
					    ($cookies:expr, $ctx:expr, $guild:expr) => {
 | 
				
			||||||
 | 
					        use serenity::model::id::UserId;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let user_id = $cookies.get_private("userid").map(|c| c.value().parse::<u64>().ok()).flatten();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        match user_id {
 | 
				
			||||||
 | 
					            Some(user_id) => {
 | 
				
			||||||
 | 
					                match GuildId($guild).to_guild_cached($ctx) {
 | 
				
			||||||
 | 
					                    Some(guild) => {
 | 
				
			||||||
 | 
					                        let member = guild.member($ctx, UserId(user_id)).await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        match member {
 | 
				
			||||||
 | 
					                            Err(_) => {
 | 
				
			||||||
 | 
					                                return json!({"error": "User not in guild"})
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                            Ok(_) => {}
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    None => {
 | 
				
			||||||
 | 
					                        return json!({"error": "Bot not in guild"})
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            None => {
 | 
				
			||||||
 | 
					                return json!({"error": "User not authorized"});
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					macro_rules! update_field {
 | 
				
			||||||
 | 
					    ($pool:expr, $error:ident, $reminder:ident.[$field:ident]) => {
 | 
				
			||||||
 | 
					        if let Some(value) = &$reminder.$field {
 | 
				
			||||||
 | 
					            match sqlx::query(concat!(
 | 
				
			||||||
 | 
					                "UPDATE reminders SET `",
 | 
				
			||||||
 | 
					                stringify!($field),
 | 
				
			||||||
 | 
					                "` = ? WHERE uid = ?"
 | 
				
			||||||
 | 
					            ))
 | 
				
			||||||
 | 
					            .bind(value)
 | 
				
			||||||
 | 
					            .bind(&$reminder.uid)
 | 
				
			||||||
 | 
					            .execute($pool)
 | 
				
			||||||
 | 
					            .await
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                Ok(_) => {}
 | 
				
			||||||
 | 
					                Err(e) => {
 | 
				
			||||||
 | 
					                    warn!(
 | 
				
			||||||
 | 
					                        concat!(
 | 
				
			||||||
 | 
					                            "Error in `update_field!(",
 | 
				
			||||||
 | 
					                            stringify!($pool),
 | 
				
			||||||
 | 
					                            stringify!($reminder),
 | 
				
			||||||
 | 
					                            stringify!($field),
 | 
				
			||||||
 | 
					                            ")': {:?}"
 | 
				
			||||||
 | 
					                        ),
 | 
				
			||||||
 | 
					                        e
 | 
				
			||||||
 | 
					                    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    $error.push(format!("Error setting field {}", stringify!($field)));
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    ($pool:expr, $error:ident, $reminder:ident.[$field:ident, $($fields:ident),+]) => {
 | 
				
			||||||
 | 
					        update_field!($pool, $error, $reminder.[$field]);
 | 
				
			||||||
 | 
					        update_field!($pool, $error, $reminder.[$($fields),+]);
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										733
									
								
								web/src/routes/dashboard/guild.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,733 @@
 | 
				
			|||||||
 | 
					use std::env;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use base64;
 | 
				
			||||||
 | 
					use chrono::Utc;
 | 
				
			||||||
 | 
					use rocket::{
 | 
				
			||||||
 | 
					    http::CookieJar,
 | 
				
			||||||
 | 
					    serde::json::{json, Json, Value as JsonValue},
 | 
				
			||||||
 | 
					    State,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					use serde::Serialize;
 | 
				
			||||||
 | 
					use serenity::{
 | 
				
			||||||
 | 
					    client::Context,
 | 
				
			||||||
 | 
					    model::{
 | 
				
			||||||
 | 
					        channel::GuildChannel,
 | 
				
			||||||
 | 
					        id::{ChannelId, GuildId, RoleId},
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					use sqlx::{MySql, Pool};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::{
 | 
				
			||||||
 | 
					    check_guild_subscription, check_subscription,
 | 
				
			||||||
 | 
					    consts::{
 | 
				
			||||||
 | 
					        DAY, MAX_CONTENT_LENGTH, MAX_EMBED_AUTHOR_LENGTH, MAX_EMBED_DESCRIPTION_LENGTH,
 | 
				
			||||||
 | 
					        MAX_EMBED_FIELDS, MAX_EMBED_FIELD_TITLE_LENGTH, MAX_EMBED_FIELD_VALUE_LENGTH,
 | 
				
			||||||
 | 
					        MAX_EMBED_FOOTER_LENGTH, MAX_EMBED_TITLE_LENGTH, MAX_URL_LENGTH, MAX_USERNAME_LENGTH,
 | 
				
			||||||
 | 
					        MIN_INTERVAL,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    routes::dashboard::{
 | 
				
			||||||
 | 
					        create_database_channel, generate_uid, name_default, template_name_default, DeleteReminder,
 | 
				
			||||||
 | 
					        DeleteReminderTemplate, PatchReminder, Reminder, ReminderTemplate,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Serialize)]
 | 
				
			||||||
 | 
					struct ChannelInfo {
 | 
				
			||||||
 | 
					    id: String,
 | 
				
			||||||
 | 
					    name: String,
 | 
				
			||||||
 | 
					    webhook_avatar: Option<String>,
 | 
				
			||||||
 | 
					    webhook_name: Option<String>,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[get("/api/guild/<id>/patreon")]
 | 
				
			||||||
 | 
					pub async fn get_guild_patreon(
 | 
				
			||||||
 | 
					    id: u64,
 | 
				
			||||||
 | 
					    cookies: &CookieJar<'_>,
 | 
				
			||||||
 | 
					    ctx: &State<Context>,
 | 
				
			||||||
 | 
					) -> JsonValue {
 | 
				
			||||||
 | 
					    check_authorization!(cookies, ctx.inner(), id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    match GuildId(id).to_guild_cached(ctx.inner()) {
 | 
				
			||||||
 | 
					        Some(guild) => {
 | 
				
			||||||
 | 
					            let member_res = GuildId(env::var("PATREON_GUILD_ID").unwrap().parse().unwrap())
 | 
				
			||||||
 | 
					                .member(&ctx.inner(), guild.owner_id)
 | 
				
			||||||
 | 
					                .await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            let patreon = member_res.map_or(false, |member| {
 | 
				
			||||||
 | 
					                member
 | 
				
			||||||
 | 
					                    .roles
 | 
				
			||||||
 | 
					                    .contains(&RoleId(env::var("PATREON_ROLE_ID").unwrap().parse().unwrap()))
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            json!({ "patreon": patreon })
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        None => {
 | 
				
			||||||
 | 
					            json!({"error": "Bot not in guild"})
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[get("/api/guild/<id>/channels")]
 | 
				
			||||||
 | 
					pub async fn get_guild_channels(
 | 
				
			||||||
 | 
					    id: u64,
 | 
				
			||||||
 | 
					    cookies: &CookieJar<'_>,
 | 
				
			||||||
 | 
					    ctx: &State<Context>,
 | 
				
			||||||
 | 
					) -> JsonValue {
 | 
				
			||||||
 | 
					    check_authorization!(cookies, ctx.inner(), id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    match GuildId(id).to_guild_cached(ctx.inner()) {
 | 
				
			||||||
 | 
					        Some(guild) => {
 | 
				
			||||||
 | 
					            let mut channels = guild
 | 
				
			||||||
 | 
					                .channels
 | 
				
			||||||
 | 
					                .iter()
 | 
				
			||||||
 | 
					                .filter_map(|(id, channel)| channel.to_owned().guild().map(|c| (id.to_owned(), c)))
 | 
				
			||||||
 | 
					                .filter(|(_, channel)| channel.is_text_based())
 | 
				
			||||||
 | 
					                .collect::<Vec<(ChannelId, GuildChannel)>>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            channels.sort_by(|(_, c1), (_, c2)| c1.position.cmp(&c2.position));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            let channel_info = channels
 | 
				
			||||||
 | 
					                .iter()
 | 
				
			||||||
 | 
					                .map(|(channel_id, channel)| ChannelInfo {
 | 
				
			||||||
 | 
					                    name: channel.name.to_string(),
 | 
				
			||||||
 | 
					                    id: channel_id.to_string(),
 | 
				
			||||||
 | 
					                    webhook_avatar: None,
 | 
				
			||||||
 | 
					                    webhook_name: None,
 | 
				
			||||||
 | 
					                })
 | 
				
			||||||
 | 
					                .collect::<Vec<ChannelInfo>>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            json!(channel_info)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        None => {
 | 
				
			||||||
 | 
					            json!({"error": "Bot not in guild"})
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Serialize)]
 | 
				
			||||||
 | 
					struct RoleInfo {
 | 
				
			||||||
 | 
					    id: String,
 | 
				
			||||||
 | 
					    name: String,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[get("/api/guild/<id>/roles")]
 | 
				
			||||||
 | 
					pub async fn get_guild_roles(id: u64, cookies: &CookieJar<'_>, ctx: &State<Context>) -> JsonValue {
 | 
				
			||||||
 | 
					    check_authorization!(cookies, ctx.inner(), id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let roles_res = ctx.cache.guild_roles(id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    match roles_res {
 | 
				
			||||||
 | 
					        Some(roles) => {
 | 
				
			||||||
 | 
					            let roles = roles
 | 
				
			||||||
 | 
					                .iter()
 | 
				
			||||||
 | 
					                .map(|(_, r)| RoleInfo { id: r.id.to_string(), name: r.name.to_string() })
 | 
				
			||||||
 | 
					                .collect::<Vec<RoleInfo>>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            json!(roles)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        None => {
 | 
				
			||||||
 | 
					            warn!("Could not fetch roles from {}", id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            json!({"error": "Could not get roles"})
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[get("/api/guild/<id>/templates")]
 | 
				
			||||||
 | 
					pub async fn get_reminder_templates(
 | 
				
			||||||
 | 
					    id: u64,
 | 
				
			||||||
 | 
					    cookies: &CookieJar<'_>,
 | 
				
			||||||
 | 
					    ctx: &State<Context>,
 | 
				
			||||||
 | 
					    pool: &State<Pool<MySql>>,
 | 
				
			||||||
 | 
					) -> JsonValue {
 | 
				
			||||||
 | 
					    check_authorization!(cookies, ctx.inner(), id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    match sqlx::query_as_unchecked!(
 | 
				
			||||||
 | 
					        ReminderTemplate,
 | 
				
			||||||
 | 
					        "SELECT * FROM reminder_template WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?)",
 | 
				
			||||||
 | 
					        id
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    .fetch_all(pool.inner())
 | 
				
			||||||
 | 
					    .await
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        Ok(templates) => {
 | 
				
			||||||
 | 
					            json!(templates)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        Err(e) => {
 | 
				
			||||||
 | 
					            warn!("Could not fetch templates from {}: {:?}", id, e);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            json!({"error": "Could not get templates"})
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[post("/api/guild/<id>/templates", data = "<reminder_template>")]
 | 
				
			||||||
 | 
					pub async fn create_reminder_template(
 | 
				
			||||||
 | 
					    id: u64,
 | 
				
			||||||
 | 
					    reminder_template: Json<ReminderTemplate>,
 | 
				
			||||||
 | 
					    cookies: &CookieJar<'_>,
 | 
				
			||||||
 | 
					    ctx: &State<Context>,
 | 
				
			||||||
 | 
					    pool: &State<Pool<MySql>>,
 | 
				
			||||||
 | 
					) -> JsonValue {
 | 
				
			||||||
 | 
					    check_authorization!(cookies, ctx.inner(), id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // validate lengths
 | 
				
			||||||
 | 
					    check_length!(MAX_CONTENT_LENGTH, reminder_template.content);
 | 
				
			||||||
 | 
					    check_length!(MAX_EMBED_DESCRIPTION_LENGTH, reminder_template.embed_description);
 | 
				
			||||||
 | 
					    check_length!(MAX_EMBED_TITLE_LENGTH, reminder_template.embed_title);
 | 
				
			||||||
 | 
					    check_length!(MAX_EMBED_AUTHOR_LENGTH, reminder_template.embed_author);
 | 
				
			||||||
 | 
					    check_length!(MAX_EMBED_FOOTER_LENGTH, reminder_template.embed_footer);
 | 
				
			||||||
 | 
					    check_length_opt!(MAX_EMBED_FIELDS, reminder_template.embed_fields);
 | 
				
			||||||
 | 
					    if let Some(fields) = &reminder_template.embed_fields {
 | 
				
			||||||
 | 
					        for field in &fields.0 {
 | 
				
			||||||
 | 
					            check_length!(MAX_EMBED_FIELD_VALUE_LENGTH, field.value);
 | 
				
			||||||
 | 
					            check_length!(MAX_EMBED_FIELD_TITLE_LENGTH, field.title);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    check_length_opt!(MAX_USERNAME_LENGTH, reminder_template.username);
 | 
				
			||||||
 | 
					    check_length_opt!(
 | 
				
			||||||
 | 
					        MAX_URL_LENGTH,
 | 
				
			||||||
 | 
					        reminder_template.embed_footer_url,
 | 
				
			||||||
 | 
					        reminder_template.embed_thumbnail_url,
 | 
				
			||||||
 | 
					        reminder_template.embed_author_url,
 | 
				
			||||||
 | 
					        reminder_template.embed_image_url,
 | 
				
			||||||
 | 
					        reminder_template.avatar
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // validate urls
 | 
				
			||||||
 | 
					    check_url_opt!(
 | 
				
			||||||
 | 
					        reminder_template.embed_footer_url,
 | 
				
			||||||
 | 
					        reminder_template.embed_thumbnail_url,
 | 
				
			||||||
 | 
					        reminder_template.embed_author_url,
 | 
				
			||||||
 | 
					        reminder_template.embed_image_url,
 | 
				
			||||||
 | 
					        reminder_template.avatar
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let name = if reminder_template.name.is_empty() {
 | 
				
			||||||
 | 
					        template_name_default()
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        reminder_template.name.clone()
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    match sqlx::query!(
 | 
				
			||||||
 | 
					        "INSERT INTO reminder_template
 | 
				
			||||||
 | 
					        (guild_id,
 | 
				
			||||||
 | 
					         name,
 | 
				
			||||||
 | 
					         attachment,
 | 
				
			||||||
 | 
					         attachment_name,
 | 
				
			||||||
 | 
					         avatar,
 | 
				
			||||||
 | 
					         content,
 | 
				
			||||||
 | 
					         embed_author,
 | 
				
			||||||
 | 
					         embed_author_url,
 | 
				
			||||||
 | 
					         embed_color,
 | 
				
			||||||
 | 
					         embed_description,
 | 
				
			||||||
 | 
					         embed_footer,
 | 
				
			||||||
 | 
					         embed_footer_url,
 | 
				
			||||||
 | 
					         embed_image_url,
 | 
				
			||||||
 | 
					         embed_thumbnail_url,
 | 
				
			||||||
 | 
					         embed_title,
 | 
				
			||||||
 | 
					         embed_fields,
 | 
				
			||||||
 | 
					         tts,
 | 
				
			||||||
 | 
					         username
 | 
				
			||||||
 | 
					        ) VALUES ((SELECT id FROM guilds WHERE guild = ?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
 | 
				
			||||||
 | 
					        id, name,
 | 
				
			||||||
 | 
					        reminder_template.attachment,
 | 
				
			||||||
 | 
					        reminder_template.attachment_name,
 | 
				
			||||||
 | 
					        reminder_template.avatar,
 | 
				
			||||||
 | 
					        reminder_template.content,
 | 
				
			||||||
 | 
					        reminder_template.embed_author,
 | 
				
			||||||
 | 
					        reminder_template.embed_author_url,
 | 
				
			||||||
 | 
					        reminder_template.embed_color,
 | 
				
			||||||
 | 
					        reminder_template.embed_description,
 | 
				
			||||||
 | 
					        reminder_template.embed_footer,
 | 
				
			||||||
 | 
					        reminder_template.embed_footer_url,
 | 
				
			||||||
 | 
					        reminder_template.embed_image_url,
 | 
				
			||||||
 | 
					        reminder_template.embed_thumbnail_url,
 | 
				
			||||||
 | 
					        reminder_template.embed_title,
 | 
				
			||||||
 | 
					        reminder_template.embed_fields,
 | 
				
			||||||
 | 
					        reminder_template.tts,
 | 
				
			||||||
 | 
					        reminder_template.username,
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    .fetch_all(pool.inner())
 | 
				
			||||||
 | 
					    .await
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        Ok(_) => {
 | 
				
			||||||
 | 
					            json!({})
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        Err(e) => {
 | 
				
			||||||
 | 
					            warn!("Could not fetch templates from {}: {:?}", id, e);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            json!({"error": "Could not get templates"})
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[delete("/api/guild/<id>/templates", data = "<delete_reminder_template>")]
 | 
				
			||||||
 | 
					pub async fn delete_reminder_template(
 | 
				
			||||||
 | 
					    id: u64,
 | 
				
			||||||
 | 
					    delete_reminder_template: Json<DeleteReminderTemplate>,
 | 
				
			||||||
 | 
					    cookies: &CookieJar<'_>,
 | 
				
			||||||
 | 
					    ctx: &State<Context>,
 | 
				
			||||||
 | 
					    pool: &State<Pool<MySql>>,
 | 
				
			||||||
 | 
					) -> JsonValue {
 | 
				
			||||||
 | 
					    check_authorization!(cookies, ctx.inner(), id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    match sqlx::query!(
 | 
				
			||||||
 | 
					        "DELETE FROM reminder_template WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?) AND id = ?",
 | 
				
			||||||
 | 
					        id, delete_reminder_template.id
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    .fetch_all(pool.inner())
 | 
				
			||||||
 | 
					    .await
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        Ok(_) => {
 | 
				
			||||||
 | 
					            json!({})
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        Err(e) => {
 | 
				
			||||||
 | 
					            warn!("Could not delete template from {}: {:?}", id, e);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            json!({"error": "Could not delete template"})
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[post("/api/guild/<id>/reminders", data = "<reminder>")]
 | 
				
			||||||
 | 
					pub async fn create_reminder(
 | 
				
			||||||
 | 
					    id: u64,
 | 
				
			||||||
 | 
					    reminder: Json<Reminder>,
 | 
				
			||||||
 | 
					    cookies: &CookieJar<'_>,
 | 
				
			||||||
 | 
					    serenity_context: &State<Context>,
 | 
				
			||||||
 | 
					    pool: &State<Pool<MySql>>,
 | 
				
			||||||
 | 
					) -> JsonValue {
 | 
				
			||||||
 | 
					    check_authorization!(cookies, serenity_context.inner(), id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let user_id =
 | 
				
			||||||
 | 
					        cookies.get_private("userid").map(|c| c.value().parse::<u64>().ok()).flatten().unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // validate channel
 | 
				
			||||||
 | 
					    let channel = ChannelId(reminder.channel).to_channel_cached(&serenity_context.inner());
 | 
				
			||||||
 | 
					    let channel_exists = channel.is_some();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let channel_matches_guild =
 | 
				
			||||||
 | 
					        channel.map_or(false, |c| c.guild().map_or(false, |c| c.guild_id.0 == id));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if !channel_matches_guild || !channel_exists {
 | 
				
			||||||
 | 
					        warn!(
 | 
				
			||||||
 | 
					            "Error in `create_reminder`: channel {} not found for guild {} (channel exists: {})",
 | 
				
			||||||
 | 
					            reminder.channel, id, channel_exists
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return json!({"error": "Channel not found"});
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let channel = create_database_channel(
 | 
				
			||||||
 | 
					        serenity_context.inner(),
 | 
				
			||||||
 | 
					        ChannelId(reminder.channel),
 | 
				
			||||||
 | 
					        pool.inner(),
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    .await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if let Err(e) = channel {
 | 
				
			||||||
 | 
					        warn!("`create_database_channel` returned an error code: {:?}", e);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return json!({"error": "Failed to configure channel for reminders. Please check the bot permissions"});
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let channel = channel.unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // validate lengths
 | 
				
			||||||
 | 
					    check_length!(MAX_CONTENT_LENGTH, reminder.content);
 | 
				
			||||||
 | 
					    check_length!(MAX_EMBED_DESCRIPTION_LENGTH, reminder.embed_description);
 | 
				
			||||||
 | 
					    check_length!(MAX_EMBED_TITLE_LENGTH, reminder.embed_title);
 | 
				
			||||||
 | 
					    check_length!(MAX_EMBED_AUTHOR_LENGTH, reminder.embed_author);
 | 
				
			||||||
 | 
					    check_length!(MAX_EMBED_FOOTER_LENGTH, reminder.embed_footer);
 | 
				
			||||||
 | 
					    check_length_opt!(MAX_EMBED_FIELDS, reminder.embed_fields);
 | 
				
			||||||
 | 
					    if let Some(fields) = &reminder.embed_fields {
 | 
				
			||||||
 | 
					        for field in &fields.0 {
 | 
				
			||||||
 | 
					            check_length!(MAX_EMBED_FIELD_VALUE_LENGTH, field.value);
 | 
				
			||||||
 | 
					            check_length!(MAX_EMBED_FIELD_TITLE_LENGTH, field.title);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    check_length_opt!(MAX_USERNAME_LENGTH, reminder.username);
 | 
				
			||||||
 | 
					    check_length_opt!(
 | 
				
			||||||
 | 
					        MAX_URL_LENGTH,
 | 
				
			||||||
 | 
					        reminder.embed_footer_url,
 | 
				
			||||||
 | 
					        reminder.embed_thumbnail_url,
 | 
				
			||||||
 | 
					        reminder.embed_author_url,
 | 
				
			||||||
 | 
					        reminder.embed_image_url,
 | 
				
			||||||
 | 
					        reminder.avatar
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // validate urls
 | 
				
			||||||
 | 
					    check_url_opt!(
 | 
				
			||||||
 | 
					        reminder.embed_footer_url,
 | 
				
			||||||
 | 
					        reminder.embed_thumbnail_url,
 | 
				
			||||||
 | 
					        reminder.embed_author_url,
 | 
				
			||||||
 | 
					        reminder.embed_image_url,
 | 
				
			||||||
 | 
					        reminder.avatar
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // validate time and interval
 | 
				
			||||||
 | 
					    if reminder.utc_time < Utc::now().naive_utc() {
 | 
				
			||||||
 | 
					        return json!({"error": "Time must be in the future"});
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if reminder.interval_seconds.is_some() || reminder.interval_months.is_some() {
 | 
				
			||||||
 | 
					        if reminder.interval_months.unwrap_or(0) * 30 * DAY as u32
 | 
				
			||||||
 | 
					            + reminder.interval_seconds.unwrap_or(0)
 | 
				
			||||||
 | 
					            < *MIN_INTERVAL
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            return json!({"error": "Interval too short"});
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // check patreon if necessary
 | 
				
			||||||
 | 
					    if reminder.interval_seconds.is_some() || reminder.interval_months.is_some() {
 | 
				
			||||||
 | 
					        if !check_guild_subscription(serenity_context.inner(), GuildId(id)).await
 | 
				
			||||||
 | 
					            && !check_subscription(serenity_context.inner(), user_id).await
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            return json!({"error": "Patreon is required to set intervals"});
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // base64 decode error dropped here
 | 
				
			||||||
 | 
					    let attachment_data = reminder.attachment.as_ref().map(|s| base64::decode(s).ok()).flatten();
 | 
				
			||||||
 | 
					    let name = if reminder.name.is_empty() { name_default() } else { reminder.name.clone() };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let new_uid = generate_uid();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // write to db
 | 
				
			||||||
 | 
					    match sqlx::query!(
 | 
				
			||||||
 | 
					        "INSERT INTO reminders (
 | 
				
			||||||
 | 
					         uid,
 | 
				
			||||||
 | 
					         attachment,
 | 
				
			||||||
 | 
					         attachment_name,
 | 
				
			||||||
 | 
					         channel_id,
 | 
				
			||||||
 | 
					         avatar,
 | 
				
			||||||
 | 
					         content,
 | 
				
			||||||
 | 
					         embed_author,
 | 
				
			||||||
 | 
					         embed_author_url,
 | 
				
			||||||
 | 
					         embed_color,
 | 
				
			||||||
 | 
					         embed_description,
 | 
				
			||||||
 | 
					         embed_footer,
 | 
				
			||||||
 | 
					         embed_footer_url,
 | 
				
			||||||
 | 
					         embed_image_url,
 | 
				
			||||||
 | 
					         embed_thumbnail_url,
 | 
				
			||||||
 | 
					         embed_title,
 | 
				
			||||||
 | 
					         embed_fields,
 | 
				
			||||||
 | 
					         enabled,
 | 
				
			||||||
 | 
					         expires,
 | 
				
			||||||
 | 
					         interval_seconds,
 | 
				
			||||||
 | 
					         interval_months,
 | 
				
			||||||
 | 
					         name,
 | 
				
			||||||
 | 
					         pin,
 | 
				
			||||||
 | 
					         restartable,
 | 
				
			||||||
 | 
					         tts,
 | 
				
			||||||
 | 
					         username,
 | 
				
			||||||
 | 
					         `utc_time`
 | 
				
			||||||
 | 
					        ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
 | 
				
			||||||
 | 
					        new_uid,
 | 
				
			||||||
 | 
					        attachment_data,
 | 
				
			||||||
 | 
					        reminder.attachment_name,
 | 
				
			||||||
 | 
					        channel,
 | 
				
			||||||
 | 
					        reminder.avatar,
 | 
				
			||||||
 | 
					        reminder.content,
 | 
				
			||||||
 | 
					        reminder.embed_author,
 | 
				
			||||||
 | 
					        reminder.embed_author_url,
 | 
				
			||||||
 | 
					        reminder.embed_color,
 | 
				
			||||||
 | 
					        reminder.embed_description,
 | 
				
			||||||
 | 
					        reminder.embed_footer,
 | 
				
			||||||
 | 
					        reminder.embed_footer_url,
 | 
				
			||||||
 | 
					        reminder.embed_image_url,
 | 
				
			||||||
 | 
					        reminder.embed_thumbnail_url,
 | 
				
			||||||
 | 
					        reminder.embed_title,
 | 
				
			||||||
 | 
					        reminder.embed_fields,
 | 
				
			||||||
 | 
					        reminder.enabled,
 | 
				
			||||||
 | 
					        reminder.expires,
 | 
				
			||||||
 | 
					        reminder.interval_seconds,
 | 
				
			||||||
 | 
					        reminder.interval_months,
 | 
				
			||||||
 | 
					        name,
 | 
				
			||||||
 | 
					        reminder.pin,
 | 
				
			||||||
 | 
					        reminder.restartable,
 | 
				
			||||||
 | 
					        reminder.tts,
 | 
				
			||||||
 | 
					        reminder.username,
 | 
				
			||||||
 | 
					        reminder.utc_time,
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    .execute(pool.inner())
 | 
				
			||||||
 | 
					    .await
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        Ok(_) => sqlx::query_as_unchecked!(
 | 
				
			||||||
 | 
					            Reminder,
 | 
				
			||||||
 | 
					            "SELECT
 | 
				
			||||||
 | 
					             reminders.attachment,
 | 
				
			||||||
 | 
					             reminders.attachment_name,
 | 
				
			||||||
 | 
					             reminders.avatar,
 | 
				
			||||||
 | 
					             channels.channel,
 | 
				
			||||||
 | 
					             reminders.content,
 | 
				
			||||||
 | 
					             reminders.embed_author,
 | 
				
			||||||
 | 
					             reminders.embed_author_url,
 | 
				
			||||||
 | 
					             reminders.embed_color,
 | 
				
			||||||
 | 
					             reminders.embed_description,
 | 
				
			||||||
 | 
					             reminders.embed_footer,
 | 
				
			||||||
 | 
					             reminders.embed_footer_url,
 | 
				
			||||||
 | 
					             reminders.embed_image_url,
 | 
				
			||||||
 | 
					             reminders.embed_thumbnail_url,
 | 
				
			||||||
 | 
					             reminders.embed_title,
 | 
				
			||||||
 | 
					             reminders.embed_fields,
 | 
				
			||||||
 | 
					             reminders.enabled,
 | 
				
			||||||
 | 
					             reminders.expires,
 | 
				
			||||||
 | 
					             reminders.interval_seconds,
 | 
				
			||||||
 | 
					             reminders.interval_months,
 | 
				
			||||||
 | 
					             reminders.name,
 | 
				
			||||||
 | 
					             reminders.pin,
 | 
				
			||||||
 | 
					             reminders.restartable,
 | 
				
			||||||
 | 
					             reminders.tts,
 | 
				
			||||||
 | 
					             reminders.uid,
 | 
				
			||||||
 | 
					             reminders.username,
 | 
				
			||||||
 | 
					             reminders.utc_time
 | 
				
			||||||
 | 
					            FROM reminders
 | 
				
			||||||
 | 
					            LEFT JOIN channels ON channels.id = reminders.channel_id
 | 
				
			||||||
 | 
					            WHERE uid = ?",
 | 
				
			||||||
 | 
					            new_uid
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        .fetch_one(pool.inner())
 | 
				
			||||||
 | 
					        .await
 | 
				
			||||||
 | 
					        .map(|r| json!(r))
 | 
				
			||||||
 | 
					        .unwrap_or_else(|e| {
 | 
				
			||||||
 | 
					            warn!("Failed to complete SQL query: {:?}", e);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            json!({"error": "Could not load reminder"})
 | 
				
			||||||
 | 
					        }),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Err(e) => {
 | 
				
			||||||
 | 
					            warn!("Error in `create_reminder`: Could not execute query: {:?}", e);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            json!({"error": "Unknown error"})
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[get("/api/guild/<id>/reminders")]
 | 
				
			||||||
 | 
					pub async fn get_reminders(id: u64, ctx: &State<Context>, pool: &State<Pool<MySql>>) -> JsonValue {
 | 
				
			||||||
 | 
					    let channels_res = GuildId(id).channels(&ctx.inner()).await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    match channels_res {
 | 
				
			||||||
 | 
					        Ok(channels) => {
 | 
				
			||||||
 | 
					            let channels = channels
 | 
				
			||||||
 | 
					                .keys()
 | 
				
			||||||
 | 
					                .into_iter()
 | 
				
			||||||
 | 
					                .map(|k| k.as_u64().to_string())
 | 
				
			||||||
 | 
					                .collect::<Vec<String>>()
 | 
				
			||||||
 | 
					                .join(",");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            sqlx::query_as_unchecked!(
 | 
				
			||||||
 | 
					                Reminder,
 | 
				
			||||||
 | 
					                "SELECT
 | 
				
			||||||
 | 
					                 reminders.attachment,
 | 
				
			||||||
 | 
					                 reminders.attachment_name,
 | 
				
			||||||
 | 
					                 reminders.avatar,
 | 
				
			||||||
 | 
					                 channels.channel,
 | 
				
			||||||
 | 
					                 reminders.content,
 | 
				
			||||||
 | 
					                 reminders.embed_author,
 | 
				
			||||||
 | 
					                 reminders.embed_author_url,
 | 
				
			||||||
 | 
					                 reminders.embed_color,
 | 
				
			||||||
 | 
					                 reminders.embed_description,
 | 
				
			||||||
 | 
					                 reminders.embed_footer,
 | 
				
			||||||
 | 
					                 reminders.embed_footer_url,
 | 
				
			||||||
 | 
					                 reminders.embed_image_url,
 | 
				
			||||||
 | 
					                 reminders.embed_thumbnail_url,
 | 
				
			||||||
 | 
					                 reminders.embed_title,
 | 
				
			||||||
 | 
					                 reminders.embed_fields,
 | 
				
			||||||
 | 
					                 reminders.enabled,
 | 
				
			||||||
 | 
					                 reminders.expires,
 | 
				
			||||||
 | 
					                 reminders.interval_seconds,
 | 
				
			||||||
 | 
					                 reminders.interval_months,
 | 
				
			||||||
 | 
					                 reminders.name,
 | 
				
			||||||
 | 
					                 reminders.pin,
 | 
				
			||||||
 | 
					                 reminders.restartable,
 | 
				
			||||||
 | 
					                 reminders.tts,
 | 
				
			||||||
 | 
					                 reminders.uid,
 | 
				
			||||||
 | 
					                 reminders.username,
 | 
				
			||||||
 | 
					                 reminders.utc_time
 | 
				
			||||||
 | 
					                FROM reminders
 | 
				
			||||||
 | 
					                LEFT JOIN channels ON channels.id = reminders.channel_id
 | 
				
			||||||
 | 
					                WHERE FIND_IN_SET(channels.channel, ?)",
 | 
				
			||||||
 | 
					                channels
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            .fetch_all(pool.inner())
 | 
				
			||||||
 | 
					            .await
 | 
				
			||||||
 | 
					            .map(|r| json!(r))
 | 
				
			||||||
 | 
					            .unwrap_or_else(|e| {
 | 
				
			||||||
 | 
					                warn!("Failed to complete SQL query: {:?}", e);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                json!({"error": "Could not load reminders"})
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        Err(e) => {
 | 
				
			||||||
 | 
					            warn!("Could not fetch channels from {}: {:?}", id, e);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            json!([])
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[patch("/api/guild/<id>/reminders", data = "<reminder>")]
 | 
				
			||||||
 | 
					pub async fn edit_reminder(
 | 
				
			||||||
 | 
					    id: u64,
 | 
				
			||||||
 | 
					    reminder: Json<PatchReminder>,
 | 
				
			||||||
 | 
					    serenity_context: &State<Context>,
 | 
				
			||||||
 | 
					    pool: &State<Pool<MySql>>,
 | 
				
			||||||
 | 
					) -> JsonValue {
 | 
				
			||||||
 | 
					    let mut error = vec![];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    update_field!(pool.inner(), error, reminder.[
 | 
				
			||||||
 | 
					        attachment,
 | 
				
			||||||
 | 
					        attachment_name,
 | 
				
			||||||
 | 
					        avatar,
 | 
				
			||||||
 | 
					        content,
 | 
				
			||||||
 | 
					        embed_author,
 | 
				
			||||||
 | 
					        embed_author_url,
 | 
				
			||||||
 | 
					        embed_color,
 | 
				
			||||||
 | 
					        embed_description,
 | 
				
			||||||
 | 
					        embed_footer,
 | 
				
			||||||
 | 
					        embed_footer_url,
 | 
				
			||||||
 | 
					        embed_image_url,
 | 
				
			||||||
 | 
					        embed_thumbnail_url,
 | 
				
			||||||
 | 
					        embed_title,
 | 
				
			||||||
 | 
					        embed_fields,
 | 
				
			||||||
 | 
					        enabled,
 | 
				
			||||||
 | 
					        expires,
 | 
				
			||||||
 | 
					        interval_seconds,
 | 
				
			||||||
 | 
					        interval_months,
 | 
				
			||||||
 | 
					        name,
 | 
				
			||||||
 | 
					        pin,
 | 
				
			||||||
 | 
					        restartable,
 | 
				
			||||||
 | 
					        tts,
 | 
				
			||||||
 | 
					        username,
 | 
				
			||||||
 | 
					        utc_time
 | 
				
			||||||
 | 
					    ]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if reminder.channel > 0 {
 | 
				
			||||||
 | 
					        let channel = ChannelId(reminder.channel).to_channel_cached(&serenity_context.inner());
 | 
				
			||||||
 | 
					        match channel {
 | 
				
			||||||
 | 
					            Some(channel) => {
 | 
				
			||||||
 | 
					                let channel_matches_guild = channel.guild().map_or(false, |c| c.guild_id.0 == id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if !channel_matches_guild {
 | 
				
			||||||
 | 
					                    warn!(
 | 
				
			||||||
 | 
					                        "Error in `edit_reminder`: channel {:?} not found for guild {}",
 | 
				
			||||||
 | 
					                        reminder.channel, id
 | 
				
			||||||
 | 
					                    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    return json!({"error": "Channel not found"});
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                let channel = create_database_channel(
 | 
				
			||||||
 | 
					                    serenity_context.inner(),
 | 
				
			||||||
 | 
					                    ChannelId(reminder.channel),
 | 
				
			||||||
 | 
					                    pool.inner(),
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					                .await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if let Err(e) = channel {
 | 
				
			||||||
 | 
					                    warn!("`create_database_channel` returned an error code: {:?}", e);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    return json!({"error": "Failed to configure channel for reminders. Please check the bot permissions"});
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                let channel = channel.unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                match sqlx::query!(
 | 
				
			||||||
 | 
					                    "UPDATE reminders SET channel_id = ? WHERE uid = ?",
 | 
				
			||||||
 | 
					                    channel,
 | 
				
			||||||
 | 
					                    reminder.uid
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					                .execute(pool.inner())
 | 
				
			||||||
 | 
					                .await
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    Ok(_) => {}
 | 
				
			||||||
 | 
					                    Err(e) => {
 | 
				
			||||||
 | 
					                        warn!("Error setting channel: {:?}", e);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        error.push("Couldn't set channel".to_string())
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            None => {
 | 
				
			||||||
 | 
					                warn!(
 | 
				
			||||||
 | 
					                    "Error in `edit_reminder`: channel {:?} not found for guild {}",
 | 
				
			||||||
 | 
					                    reminder.channel, id
 | 
				
			||||||
 | 
					                );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                return json!({"error": "Channel not found"});
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    match sqlx::query_as_unchecked!(
 | 
				
			||||||
 | 
					        Reminder,
 | 
				
			||||||
 | 
					        "SELECT reminders.attachment,
 | 
				
			||||||
 | 
					         reminders.attachment_name,
 | 
				
			||||||
 | 
					         reminders.avatar,
 | 
				
			||||||
 | 
					         channels.channel,
 | 
				
			||||||
 | 
					         reminders.content,
 | 
				
			||||||
 | 
					         reminders.embed_author,
 | 
				
			||||||
 | 
					         reminders.embed_author_url,
 | 
				
			||||||
 | 
					         reminders.embed_color,
 | 
				
			||||||
 | 
					         reminders.embed_description,
 | 
				
			||||||
 | 
					         reminders.embed_footer,
 | 
				
			||||||
 | 
					         reminders.embed_footer_url,
 | 
				
			||||||
 | 
					         reminders.embed_image_url,
 | 
				
			||||||
 | 
					         reminders.embed_thumbnail_url,
 | 
				
			||||||
 | 
					         reminders.embed_title,
 | 
				
			||||||
 | 
					         reminders.embed_fields,
 | 
				
			||||||
 | 
					         reminders.enabled,
 | 
				
			||||||
 | 
					         reminders.expires,
 | 
				
			||||||
 | 
					         reminders.interval_seconds,
 | 
				
			||||||
 | 
					         reminders.interval_months,
 | 
				
			||||||
 | 
					         reminders.name,
 | 
				
			||||||
 | 
					         reminders.pin,
 | 
				
			||||||
 | 
					         reminders.restartable,
 | 
				
			||||||
 | 
					         reminders.tts,
 | 
				
			||||||
 | 
					         reminders.uid,
 | 
				
			||||||
 | 
					         reminders.username,
 | 
				
			||||||
 | 
					         reminders.utc_time
 | 
				
			||||||
 | 
					        FROM reminders
 | 
				
			||||||
 | 
					        LEFT JOIN channels ON channels.id = reminders.channel_id
 | 
				
			||||||
 | 
					        WHERE uid = ?",
 | 
				
			||||||
 | 
					        reminder.uid
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    .fetch_one(pool.inner())
 | 
				
			||||||
 | 
					    .await
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        Ok(reminder) => json!({"reminder": reminder, "errors": error}),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Err(e) => {
 | 
				
			||||||
 | 
					            warn!("Error exiting `edit_reminder': {:?}", e);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            json!({"reminder": Option::<Reminder>::None, "errors": vec!["Unknown error"]})
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[delete("/api/guild/<_>/reminders", data = "<reminder>")]
 | 
				
			||||||
 | 
					pub async fn delete_reminder(
 | 
				
			||||||
 | 
					    reminder: Json<DeleteReminder>,
 | 
				
			||||||
 | 
					    pool: &State<Pool<MySql>>,
 | 
				
			||||||
 | 
					) -> JsonValue {
 | 
				
			||||||
 | 
					    match sqlx::query!("DELETE FROM reminders WHERE uid = ?", reminder.uid)
 | 
				
			||||||
 | 
					        .execute(pool.inner())
 | 
				
			||||||
 | 
					        .await
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        Ok(_) => {
 | 
				
			||||||
 | 
					            json!({})
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Err(e) => {
 | 
				
			||||||
 | 
					            warn!("Error in `delete_reminder`: {:?}", e);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            json!({"error": "Could not delete reminder"})
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										314
									
								
								web/src/routes/dashboard/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,314 @@
 | 
				
			|||||||
 | 
					use std::collections::HashMap;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use chrono::naive::NaiveDateTime;
 | 
				
			||||||
 | 
					use rand::{rngs::OsRng, seq::IteratorRandom};
 | 
				
			||||||
 | 
					use rocket::{http::CookieJar, response::Redirect};
 | 
				
			||||||
 | 
					use rocket_dyn_templates::Template;
 | 
				
			||||||
 | 
					use serde::{Deserialize, Serialize};
 | 
				
			||||||
 | 
					use serenity::{http::Http, model::id::ChannelId};
 | 
				
			||||||
 | 
					use sqlx::{types::Json, Executor};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::{
 | 
				
			||||||
 | 
					    consts::{CHARACTERS, DEFAULT_AVATAR},
 | 
				
			||||||
 | 
					    Database, Error,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub mod guild;
 | 
				
			||||||
 | 
					pub mod user;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type Unset<T> = Option<T>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fn name_default() -> String {
 | 
				
			||||||
 | 
					    "Reminder".to_string()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fn template_name_default() -> String {
 | 
				
			||||||
 | 
					    "Template".to_string()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fn channel_default() -> u64 {
 | 
				
			||||||
 | 
					    0
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fn id_default() -> u32 {
 | 
				
			||||||
 | 
					    0
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Serialize, Deserialize)]
 | 
				
			||||||
 | 
					pub struct ReminderTemplate {
 | 
				
			||||||
 | 
					    #[serde(default = "id_default")]
 | 
				
			||||||
 | 
					    id: u32,
 | 
				
			||||||
 | 
					    #[serde(default = "id_default")]
 | 
				
			||||||
 | 
					    guild_id: u32,
 | 
				
			||||||
 | 
					    #[serde(default = "template_name_default")]
 | 
				
			||||||
 | 
					    name: String,
 | 
				
			||||||
 | 
					    attachment: Option<Vec<u8>>,
 | 
				
			||||||
 | 
					    attachment_name: Option<String>,
 | 
				
			||||||
 | 
					    avatar: Option<String>,
 | 
				
			||||||
 | 
					    content: String,
 | 
				
			||||||
 | 
					    embed_author: String,
 | 
				
			||||||
 | 
					    embed_author_url: Option<String>,
 | 
				
			||||||
 | 
					    embed_color: u32,
 | 
				
			||||||
 | 
					    embed_description: String,
 | 
				
			||||||
 | 
					    embed_footer: String,
 | 
				
			||||||
 | 
					    embed_footer_url: Option<String>,
 | 
				
			||||||
 | 
					    embed_image_url: Option<String>,
 | 
				
			||||||
 | 
					    embed_thumbnail_url: Option<String>,
 | 
				
			||||||
 | 
					    embed_title: String,
 | 
				
			||||||
 | 
					    embed_fields: Option<Json<Vec<EmbedField>>>,
 | 
				
			||||||
 | 
					    tts: bool,
 | 
				
			||||||
 | 
					    username: Option<String>,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Deserialize)]
 | 
				
			||||||
 | 
					pub struct DeleteReminderTemplate {
 | 
				
			||||||
 | 
					    id: u32,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Serialize, Deserialize)]
 | 
				
			||||||
 | 
					pub struct EmbedField {
 | 
				
			||||||
 | 
					    title: String,
 | 
				
			||||||
 | 
					    value: String,
 | 
				
			||||||
 | 
					    inline: bool,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Serialize, Deserialize)]
 | 
				
			||||||
 | 
					pub struct Reminder {
 | 
				
			||||||
 | 
					    #[serde(with = "base64s")]
 | 
				
			||||||
 | 
					    attachment: Option<Vec<u8>>,
 | 
				
			||||||
 | 
					    attachment_name: Option<String>,
 | 
				
			||||||
 | 
					    avatar: Option<String>,
 | 
				
			||||||
 | 
					    #[serde(with = "string")]
 | 
				
			||||||
 | 
					    channel: u64,
 | 
				
			||||||
 | 
					    content: String,
 | 
				
			||||||
 | 
					    embed_author: String,
 | 
				
			||||||
 | 
					    embed_author_url: Option<String>,
 | 
				
			||||||
 | 
					    embed_color: u32,
 | 
				
			||||||
 | 
					    embed_description: String,
 | 
				
			||||||
 | 
					    embed_footer: String,
 | 
				
			||||||
 | 
					    embed_footer_url: Option<String>,
 | 
				
			||||||
 | 
					    embed_image_url: Option<String>,
 | 
				
			||||||
 | 
					    embed_thumbnail_url: Option<String>,
 | 
				
			||||||
 | 
					    embed_title: String,
 | 
				
			||||||
 | 
					    embed_fields: Option<Json<Vec<EmbedField>>>,
 | 
				
			||||||
 | 
					    enabled: bool,
 | 
				
			||||||
 | 
					    expires: Option<NaiveDateTime>,
 | 
				
			||||||
 | 
					    interval_seconds: Option<u32>,
 | 
				
			||||||
 | 
					    interval_months: Option<u32>,
 | 
				
			||||||
 | 
					    #[serde(default = "name_default")]
 | 
				
			||||||
 | 
					    name: String,
 | 
				
			||||||
 | 
					    pin: bool,
 | 
				
			||||||
 | 
					    restartable: bool,
 | 
				
			||||||
 | 
					    tts: bool,
 | 
				
			||||||
 | 
					    #[serde(default)]
 | 
				
			||||||
 | 
					    uid: String,
 | 
				
			||||||
 | 
					    username: Option<String>,
 | 
				
			||||||
 | 
					    utc_time: NaiveDateTime,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Deserialize)]
 | 
				
			||||||
 | 
					pub struct PatchReminder {
 | 
				
			||||||
 | 
					    uid: String,
 | 
				
			||||||
 | 
					    #[serde(default)]
 | 
				
			||||||
 | 
					    attachment: Unset<Option<String>>,
 | 
				
			||||||
 | 
					    #[serde(default)]
 | 
				
			||||||
 | 
					    attachment_name: Unset<Option<String>>,
 | 
				
			||||||
 | 
					    #[serde(default)]
 | 
				
			||||||
 | 
					    avatar: Unset<Option<String>>,
 | 
				
			||||||
 | 
					    #[serde(default = "channel_default")]
 | 
				
			||||||
 | 
					    #[serde(with = "string")]
 | 
				
			||||||
 | 
					    channel: u64,
 | 
				
			||||||
 | 
					    #[serde(default)]
 | 
				
			||||||
 | 
					    content: Unset<String>,
 | 
				
			||||||
 | 
					    #[serde(default)]
 | 
				
			||||||
 | 
					    embed_author: Unset<String>,
 | 
				
			||||||
 | 
					    #[serde(default)]
 | 
				
			||||||
 | 
					    embed_author_url: Unset<Option<String>>,
 | 
				
			||||||
 | 
					    #[serde(default)]
 | 
				
			||||||
 | 
					    embed_color: Unset<u32>,
 | 
				
			||||||
 | 
					    #[serde(default)]
 | 
				
			||||||
 | 
					    embed_description: Unset<String>,
 | 
				
			||||||
 | 
					    #[serde(default)]
 | 
				
			||||||
 | 
					    embed_footer: Unset<String>,
 | 
				
			||||||
 | 
					    #[serde(default)]
 | 
				
			||||||
 | 
					    embed_footer_url: Unset<Option<String>>,
 | 
				
			||||||
 | 
					    #[serde(default)]
 | 
				
			||||||
 | 
					    embed_image_url: Unset<Option<String>>,
 | 
				
			||||||
 | 
					    #[serde(default)]
 | 
				
			||||||
 | 
					    embed_thumbnail_url: Unset<Option<String>>,
 | 
				
			||||||
 | 
					    #[serde(default)]
 | 
				
			||||||
 | 
					    embed_title: Unset<String>,
 | 
				
			||||||
 | 
					    #[serde(default)]
 | 
				
			||||||
 | 
					    embed_fields: Unset<Json<Vec<EmbedField>>>,
 | 
				
			||||||
 | 
					    #[serde(default)]
 | 
				
			||||||
 | 
					    enabled: Unset<bool>,
 | 
				
			||||||
 | 
					    #[serde(default)]
 | 
				
			||||||
 | 
					    expires: Unset<Option<NaiveDateTime>>,
 | 
				
			||||||
 | 
					    #[serde(default)]
 | 
				
			||||||
 | 
					    interval_seconds: Unset<Option<u32>>,
 | 
				
			||||||
 | 
					    #[serde(default)]
 | 
				
			||||||
 | 
					    interval_months: Unset<Option<u32>>,
 | 
				
			||||||
 | 
					    #[serde(default)]
 | 
				
			||||||
 | 
					    name: Unset<String>,
 | 
				
			||||||
 | 
					    #[serde(default)]
 | 
				
			||||||
 | 
					    pin: Unset<bool>,
 | 
				
			||||||
 | 
					    #[serde(default)]
 | 
				
			||||||
 | 
					    restartable: Unset<bool>,
 | 
				
			||||||
 | 
					    #[serde(default)]
 | 
				
			||||||
 | 
					    tts: Unset<bool>,
 | 
				
			||||||
 | 
					    #[serde(default)]
 | 
				
			||||||
 | 
					    username: Unset<Option<String>>,
 | 
				
			||||||
 | 
					    #[serde(default)]
 | 
				
			||||||
 | 
					    utc_time: Unset<NaiveDateTime>,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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("")
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// https://github.com/serde-rs/json/issues/329#issuecomment-305608405
 | 
				
			||||||
 | 
					mod string {
 | 
				
			||||||
 | 
					    use std::{fmt::Display, str::FromStr};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    use serde::{de, Deserialize, Deserializer, Serializer};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn serialize<T, S>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
 | 
				
			||||||
 | 
					    where
 | 
				
			||||||
 | 
					        T: Display,
 | 
				
			||||||
 | 
					        S: Serializer,
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        serializer.collect_str(value)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn deserialize<'de, T, D>(deserializer: D) -> Result<T, D::Error>
 | 
				
			||||||
 | 
					    where
 | 
				
			||||||
 | 
					        T: FromStr,
 | 
				
			||||||
 | 
					        T::Err: Display,
 | 
				
			||||||
 | 
					        D: Deserializer<'de>,
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        String::deserialize(deserializer)?.parse().map_err(de::Error::custom)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					mod base64s {
 | 
				
			||||||
 | 
					    use serde::{de, Deserialize, Deserializer, Serializer};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn serialize<S>(value: &Option<Vec<u8>>, serializer: S) -> Result<S::Ok, S::Error>
 | 
				
			||||||
 | 
					    where
 | 
				
			||||||
 | 
					        S: Serializer,
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        if let Some(opt) = value {
 | 
				
			||||||
 | 
					            serializer.collect_str(&base64::encode(opt))
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            serializer.serialize_none()
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<Vec<u8>>, D::Error>
 | 
				
			||||||
 | 
					    where
 | 
				
			||||||
 | 
					        D: Deserializer<'de>,
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        let string = String::deserialize(deserializer)?;
 | 
				
			||||||
 | 
					        Some(base64::decode(string).map_err(de::Error::custom)).transpose()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Deserialize)]
 | 
				
			||||||
 | 
					pub struct DeleteReminder {
 | 
				
			||||||
 | 
					    uid: String,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async fn create_database_channel(
 | 
				
			||||||
 | 
					    ctx: impl AsRef<Http>,
 | 
				
			||||||
 | 
					    channel: ChannelId,
 | 
				
			||||||
 | 
					    pool: impl Executor<'_, Database = Database> + Copy,
 | 
				
			||||||
 | 
					) -> Result<u32, crate::Error> {
 | 
				
			||||||
 | 
					    println!("{:?}", channel);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let row =
 | 
				
			||||||
 | 
					        sqlx::query!("SELECT webhook_token, webhook_id FROM channels WHERE channel = ?", channel.0)
 | 
				
			||||||
 | 
					            .fetch_one(pool)
 | 
				
			||||||
 | 
					            .await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    match row {
 | 
				
			||||||
 | 
					        Ok(row) => {
 | 
				
			||||||
 | 
					            if row.webhook_token.is_none() || row.webhook_id.is_none() {
 | 
				
			||||||
 | 
					                let webhook = channel
 | 
				
			||||||
 | 
					                    .create_webhook_with_avatar(&ctx, "Reminder", DEFAULT_AVATAR.clone())
 | 
				
			||||||
 | 
					                    .await
 | 
				
			||||||
 | 
					                    .map_err(|e| Error::Serenity(e))?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                sqlx::query!(
 | 
				
			||||||
 | 
					                    "UPDATE channels SET webhook_id = ?, webhook_token = ? WHERE channel = ?",
 | 
				
			||||||
 | 
					                    webhook.id.0,
 | 
				
			||||||
 | 
					                    webhook.token,
 | 
				
			||||||
 | 
					                    channel.0
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					                .execute(pool)
 | 
				
			||||||
 | 
					                .await
 | 
				
			||||||
 | 
					                .map_err(|e| Error::SQLx(e))?;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            Ok(())
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Err(sqlx::Error::RowNotFound) => {
 | 
				
			||||||
 | 
					            // create webhook
 | 
				
			||||||
 | 
					            let webhook = channel
 | 
				
			||||||
 | 
					                .create_webhook_with_avatar(&ctx, "Reminder", DEFAULT_AVATAR.clone())
 | 
				
			||||||
 | 
					                .await
 | 
				
			||||||
 | 
					                .map_err(|e| Error::Serenity(e))?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // create database entry
 | 
				
			||||||
 | 
					            sqlx::query!(
 | 
				
			||||||
 | 
					                "INSERT INTO channels (
 | 
				
			||||||
 | 
					                 webhook_id,
 | 
				
			||||||
 | 
					                 webhook_token,
 | 
				
			||||||
 | 
					                 channel
 | 
				
			||||||
 | 
					                ) VALUES (?, ?, ?)",
 | 
				
			||||||
 | 
					                webhook.id.0,
 | 
				
			||||||
 | 
					                webhook.token,
 | 
				
			||||||
 | 
					                channel.0
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            .execute(pool)
 | 
				
			||||||
 | 
					            .await
 | 
				
			||||||
 | 
					            .map_err(|e| Error::SQLx(e))?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            Ok(())
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Err(e) => Err(Error::SQLx(e)),
 | 
				
			||||||
 | 
					    }?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let row = sqlx::query!("SELECT id FROM channels WHERE channel = ?", channel.0)
 | 
				
			||||||
 | 
					        .fetch_one(pool)
 | 
				
			||||||
 | 
					        .await
 | 
				
			||||||
 | 
					        .map_err(|e| Error::SQLx(e))?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Ok(row.id)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[get("/")]
 | 
				
			||||||
 | 
					pub async fn dashboard_home(cookies: &CookieJar<'_>) -> Result<Template, Redirect> {
 | 
				
			||||||
 | 
					    if cookies.get_private("userid").is_some() {
 | 
				
			||||||
 | 
					        let map: HashMap<&str, String> = HashMap::new();
 | 
				
			||||||
 | 
					        Ok(Template::render("dashboard", &map))
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        Err(Redirect::to("/login/discord"))
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[get("/<_>")]
 | 
				
			||||||
 | 
					pub async fn dashboard(cookies: &CookieJar<'_>) -> Result<Template, Redirect> {
 | 
				
			||||||
 | 
					    if cookies.get_private("userid").is_some() {
 | 
				
			||||||
 | 
					        let map: HashMap<&str, String> = HashMap::new();
 | 
				
			||||||
 | 
					        Ok(Template::render("dashboard", &map))
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        Err(Redirect::to("/login/discord"))
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										165
									
								
								web/src/routes/dashboard/user.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,165 @@
 | 
				
			|||||||
 | 
					use std::env;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use chrono_tz::Tz;
 | 
				
			||||||
 | 
					use reqwest::Client;
 | 
				
			||||||
 | 
					use rocket::{
 | 
				
			||||||
 | 
					    http::CookieJar,
 | 
				
			||||||
 | 
					    serde::json::{json, Json, Value as JsonValue},
 | 
				
			||||||
 | 
					    State,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					use serde::{Deserialize, Serialize};
 | 
				
			||||||
 | 
					use serenity::{
 | 
				
			||||||
 | 
					    client::Context,
 | 
				
			||||||
 | 
					    model::{
 | 
				
			||||||
 | 
					        id::{GuildId, RoleId},
 | 
				
			||||||
 | 
					        permissions::Permissions,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					use sqlx::{MySql, Pool};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::consts::DISCORD_API;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Serialize)]
 | 
				
			||||||
 | 
					struct UserInfo {
 | 
				
			||||||
 | 
					    name: String,
 | 
				
			||||||
 | 
					    patreon: bool,
 | 
				
			||||||
 | 
					    timezone: Option<String>,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Deserialize)]
 | 
				
			||||||
 | 
					pub struct UpdateUser {
 | 
				
			||||||
 | 
					    timezone: String,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Serialize)]
 | 
				
			||||||
 | 
					struct GuildInfo {
 | 
				
			||||||
 | 
					    id: String,
 | 
				
			||||||
 | 
					    name: String,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Deserialize)]
 | 
				
			||||||
 | 
					pub struct PartialGuild {
 | 
				
			||||||
 | 
					    pub id: GuildId,
 | 
				
			||||||
 | 
					    pub icon: Option<String>,
 | 
				
			||||||
 | 
					    pub name: String,
 | 
				
			||||||
 | 
					    #[serde(default)]
 | 
				
			||||||
 | 
					    pub owner: bool,
 | 
				
			||||||
 | 
					    #[serde(rename = "permissions_new")]
 | 
				
			||||||
 | 
					    pub permissions: Option<String>,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[get("/api/user")]
 | 
				
			||||||
 | 
					pub async fn get_user_info(
 | 
				
			||||||
 | 
					    cookies: &CookieJar<'_>,
 | 
				
			||||||
 | 
					    ctx: &State<Context>,
 | 
				
			||||||
 | 
					    pool: &State<Pool<MySql>>,
 | 
				
			||||||
 | 
					) -> JsonValue {
 | 
				
			||||||
 | 
					    if let Some(user_id) =
 | 
				
			||||||
 | 
					        cookies.get_private("userid").map(|u| u.value().parse::<u64>().ok()).flatten()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        let member_res = GuildId(env::var("PATREON_GUILD_ID").unwrap().parse().unwrap())
 | 
				
			||||||
 | 
					            .member(&ctx.inner(), user_id)
 | 
				
			||||||
 | 
					            .await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let timezone = sqlx::query!("SELECT timezone FROM users WHERE user = ?", user_id)
 | 
				
			||||||
 | 
					            .fetch_one(pool.inner())
 | 
				
			||||||
 | 
					            .await
 | 
				
			||||||
 | 
					            .map_or(None, |q| Some(q.timezone));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let user_info = UserInfo {
 | 
				
			||||||
 | 
					            name: cookies
 | 
				
			||||||
 | 
					                .get_private("username")
 | 
				
			||||||
 | 
					                .map_or("DiscordUser#0000".to_string(), |c| c.value().to_string()),
 | 
				
			||||||
 | 
					            patreon: member_res.map_or(false, |member| {
 | 
				
			||||||
 | 
					                member
 | 
				
			||||||
 | 
					                    .roles
 | 
				
			||||||
 | 
					                    .contains(&RoleId(env::var("PATREON_ROLE_ID").unwrap().parse().unwrap()))
 | 
				
			||||||
 | 
					            }),
 | 
				
			||||||
 | 
					            timezone,
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        json!(user_info)
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        json!({"error": "Not authorized"})
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[patch("/api/user", data = "<user>")]
 | 
				
			||||||
 | 
					pub async fn update_user_info(
 | 
				
			||||||
 | 
					    cookies: &CookieJar<'_>,
 | 
				
			||||||
 | 
					    user: Json<UpdateUser>,
 | 
				
			||||||
 | 
					    pool: &State<Pool<MySql>>,
 | 
				
			||||||
 | 
					) -> JsonValue {
 | 
				
			||||||
 | 
					    if let Some(user_id) =
 | 
				
			||||||
 | 
					        cookies.get_private("userid").map(|u| u.value().parse::<u64>().ok()).flatten()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        if user.timezone.parse::<Tz>().is_ok() {
 | 
				
			||||||
 | 
					            let _ = sqlx::query!(
 | 
				
			||||||
 | 
					                "UPDATE users SET timezone = ? WHERE user = ?",
 | 
				
			||||||
 | 
					                user.timezone,
 | 
				
			||||||
 | 
					                user_id,
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            .execute(pool.inner())
 | 
				
			||||||
 | 
					            .await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            json!({})
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            json!({"error": "Timezone not recognized"})
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        json!({"error": "Not authorized"})
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[get("/api/user/guilds")]
 | 
				
			||||||
 | 
					pub async fn get_user_guilds(cookies: &CookieJar<'_>, reqwest_client: &State<Client>) -> JsonValue {
 | 
				
			||||||
 | 
					    if let Some(access_token) = cookies.get_private("access_token") {
 | 
				
			||||||
 | 
					        let request_res = reqwest_client
 | 
				
			||||||
 | 
					            .get(format!("{}/users/@me/guilds", DISCORD_API))
 | 
				
			||||||
 | 
					            .bearer_auth(access_token.value())
 | 
				
			||||||
 | 
					            .send()
 | 
				
			||||||
 | 
					            .await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        match request_res {
 | 
				
			||||||
 | 
					            Ok(response) => {
 | 
				
			||||||
 | 
					                let guilds_res = response.json::<Vec<PartialGuild>>().await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                match guilds_res {
 | 
				
			||||||
 | 
					                    Ok(guilds) => {
 | 
				
			||||||
 | 
					                        let reduced_guilds = guilds
 | 
				
			||||||
 | 
					                            .iter()
 | 
				
			||||||
 | 
					                            .filter(|g| {
 | 
				
			||||||
 | 
					                                g.owner
 | 
				
			||||||
 | 
					                                    || g.permissions.as_ref().map_or(false, |p| {
 | 
				
			||||||
 | 
					                                        let permissions =
 | 
				
			||||||
 | 
					                                            Permissions::from_bits_truncate(p.parse().unwrap());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                                        permissions.manage_messages()
 | 
				
			||||||
 | 
					                                            || permissions.manage_guild()
 | 
				
			||||||
 | 
					                                            || permissions.administrator()
 | 
				
			||||||
 | 
					                                    })
 | 
				
			||||||
 | 
					                            })
 | 
				
			||||||
 | 
					                            .map(|g| GuildInfo { id: g.id.to_string(), name: g.name.to_string() })
 | 
				
			||||||
 | 
					                            .collect::<Vec<GuildInfo>>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        json!(reduced_guilds)
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    Err(e) => {
 | 
				
			||||||
 | 
					                        warn!("Error constructing user from request: {:?}", e);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        json!({"error": "Could not get user details"})
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            Err(e) => {
 | 
				
			||||||
 | 
					                warn!("Error getting user guilds: {:?}", e);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                json!({"error": "Could not reach Discord"})
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        json!({"error": "Not authorized"})
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										149
									
								
								web/src/routes/login.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,149 @@
 | 
				
			|||||||
 | 
					use log::warn;
 | 
				
			||||||
 | 
					use oauth2::{
 | 
				
			||||||
 | 
					    basic::BasicClient, reqwest::async_http_client, AuthorizationCode, CsrfToken,
 | 
				
			||||||
 | 
					    PkceCodeChallenge, PkceCodeVerifier, Scope, TokenResponse,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					use reqwest::Client;
 | 
				
			||||||
 | 
					use rocket::{
 | 
				
			||||||
 | 
					    http::{private::cookie::Expiration, Cookie, CookieJar, SameSite},
 | 
				
			||||||
 | 
					    response::{Flash, Redirect},
 | 
				
			||||||
 | 
					    uri, State,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					use serenity::model::user::User;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::consts::DISCORD_API;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[get("/discord")]
 | 
				
			||||||
 | 
					pub async fn discord_login(
 | 
				
			||||||
 | 
					    oauth2_client: &State<BasicClient>,
 | 
				
			||||||
 | 
					    cookies: &CookieJar<'_>,
 | 
				
			||||||
 | 
					) -> Redirect {
 | 
				
			||||||
 | 
					    let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let (auth_url, csrf_token) = oauth2_client
 | 
				
			||||||
 | 
					        .authorize_url(CsrfToken::new_random)
 | 
				
			||||||
 | 
					        // Set the desired scopes.
 | 
				
			||||||
 | 
					        .add_scope(Scope::new("identify".to_string()))
 | 
				
			||||||
 | 
					        .add_scope(Scope::new("guilds".to_string()))
 | 
				
			||||||
 | 
					        .add_scope(Scope::new("email".to_string()))
 | 
				
			||||||
 | 
					        // Set the PKCE code challenge.
 | 
				
			||||||
 | 
					        .set_pkce_challenge(pkce_challenge)
 | 
				
			||||||
 | 
					        .url();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // store the pkce secret to verify the authorization later
 | 
				
			||||||
 | 
					    cookies.add_private(
 | 
				
			||||||
 | 
					        Cookie::build("verify", pkce_verifier.secret().to_string())
 | 
				
			||||||
 | 
					            .http_only(true)
 | 
				
			||||||
 | 
					            .path("/login")
 | 
				
			||||||
 | 
					            .same_site(SameSite::Lax)
 | 
				
			||||||
 | 
					            .expires(Expiration::Session)
 | 
				
			||||||
 | 
					            .finish(),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // store the csrf token to verify no interference
 | 
				
			||||||
 | 
					    cookies.add_private(
 | 
				
			||||||
 | 
					        Cookie::build("csrf", csrf_token.secret().to_string())
 | 
				
			||||||
 | 
					            .http_only(true)
 | 
				
			||||||
 | 
					            .path("/login")
 | 
				
			||||||
 | 
					            .same_site(SameSite::Lax)
 | 
				
			||||||
 | 
					            .expires(Expiration::Session)
 | 
				
			||||||
 | 
					            .finish(),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Redirect::to(auth_url.to_string())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[get("/discord/authorized?<code>&<state>")]
 | 
				
			||||||
 | 
					pub async fn discord_callback(
 | 
				
			||||||
 | 
					    code: &str,
 | 
				
			||||||
 | 
					    state: &str,
 | 
				
			||||||
 | 
					    cookies: &CookieJar<'_>,
 | 
				
			||||||
 | 
					    oauth2_client: &State<BasicClient>,
 | 
				
			||||||
 | 
					    reqwest_client: &State<Client>,
 | 
				
			||||||
 | 
					) -> Result<Redirect, Flash<Redirect>> {
 | 
				
			||||||
 | 
					    if let (Some(pkce_secret), Some(csrf_token)) =
 | 
				
			||||||
 | 
					        (cookies.get_private("verify"), cookies.get_private("csrf"))
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        if state == csrf_token.value() {
 | 
				
			||||||
 | 
					            let token_result = oauth2_client
 | 
				
			||||||
 | 
					                .exchange_code(AuthorizationCode::new(code.to_string()))
 | 
				
			||||||
 | 
					                // Set the PKCE code verifier.
 | 
				
			||||||
 | 
					                .set_pkce_verifier(PkceCodeVerifier::new(pkce_secret.value().to_string()))
 | 
				
			||||||
 | 
					                .request_async(async_http_client)
 | 
				
			||||||
 | 
					                .await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            cookies.remove_private(Cookie::named("verify"));
 | 
				
			||||||
 | 
					            cookies.remove_private(Cookie::named("csrf"));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            match token_result {
 | 
				
			||||||
 | 
					                Ok(token) => {
 | 
				
			||||||
 | 
					                    cookies.add_private(
 | 
				
			||||||
 | 
					                        Cookie::build("access_token", token.access_token().secret().to_string())
 | 
				
			||||||
 | 
					                            .secure(true)
 | 
				
			||||||
 | 
					                            .http_only(true)
 | 
				
			||||||
 | 
					                            .path("/dashboard")
 | 
				
			||||||
 | 
					                            .finish(),
 | 
				
			||||||
 | 
					                    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    let request_res = reqwest_client
 | 
				
			||||||
 | 
					                        .get(format!("{}/users/@me", DISCORD_API))
 | 
				
			||||||
 | 
					                        .bearer_auth(token.access_token().secret())
 | 
				
			||||||
 | 
					                        .send()
 | 
				
			||||||
 | 
					                        .await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    match request_res {
 | 
				
			||||||
 | 
					                        Ok(response) => {
 | 
				
			||||||
 | 
					                            let user_res = response.json::<User>().await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                            match user_res {
 | 
				
			||||||
 | 
					                                Ok(user) => {
 | 
				
			||||||
 | 
					                                    let user_name = format!("{}#{}", user.name, user.discriminator);
 | 
				
			||||||
 | 
					                                    let user_id = user.id.as_u64().to_string();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                                    cookies.add_private(Cookie::new("username", user_name));
 | 
				
			||||||
 | 
					                                    cookies.add_private(Cookie::new("userid", user_id));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                                    Ok(Redirect::to(uri!(super::return_to_same_site("dashboard"))))
 | 
				
			||||||
 | 
					                                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                                Err(e) => {
 | 
				
			||||||
 | 
					                                    warn!("Error constructing user from request: {:?}", e);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                                    Err(Flash::new(
 | 
				
			||||||
 | 
					                                        Redirect::to(uri!(super::return_to_same_site(""))),
 | 
				
			||||||
 | 
					                                        "danger",
 | 
				
			||||||
 | 
					                                        "Failed to contact Discord",
 | 
				
			||||||
 | 
					                                    ))
 | 
				
			||||||
 | 
					                                }
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        Err(e) => {
 | 
				
			||||||
 | 
					                            warn!("Error getting user info: {:?}", e);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                            Err(Flash::new(
 | 
				
			||||||
 | 
					                                Redirect::to(uri!(super::return_to_same_site(""))),
 | 
				
			||||||
 | 
					                                "danger",
 | 
				
			||||||
 | 
					                                "Failed to contact Discord",
 | 
				
			||||||
 | 
					                            ))
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                Err(e) => {
 | 
				
			||||||
 | 
					                    warn!("Error in discord callback: {:?}", e);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    Err(Flash::new(
 | 
				
			||||||
 | 
					                        Redirect::to(uri!(super::return_to_same_site(""))),
 | 
				
			||||||
 | 
					                        "warning",
 | 
				
			||||||
 | 
					                        "Your login request was rejected",
 | 
				
			||||||
 | 
					                    ))
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            Err(Flash::new(Redirect::to(uri!(super::return_to_same_site(""))), "danger", "Your request failed to validate, and so has been rejected (error: CSRF Validation Failure)"))
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        Err(Flash::new(Redirect::to(uri!(super::return_to_same_site(""))), "warning", "Your request was missing information, and so has been rejected (error: CSRF Validation Tokens Missing)"))
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										88
									
								
								web/src/routes/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,88 @@
 | 
				
			|||||||
 | 
					pub mod dashboard;
 | 
				
			||||||
 | 
					pub mod login;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use std::collections::HashMap;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use rocket::request::FlashMessage;
 | 
				
			||||||
 | 
					use rocket_dyn_templates::Template;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[get("/")]
 | 
				
			||||||
 | 
					pub async fn index(flash: Option<FlashMessage<'_>>) -> Template {
 | 
				
			||||||
 | 
					    let mut map: HashMap<&str, String> = HashMap::new();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if let Some(message) = flash {
 | 
				
			||||||
 | 
					        map.insert("flashed_message", message.message().to_string());
 | 
				
			||||||
 | 
					        map.insert("flashed_grade", message.kind().to_string());
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Template::render("index", &map)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[get("/ret?<to>")]
 | 
				
			||||||
 | 
					pub async fn return_to_same_site(to: &str) -> Template {
 | 
				
			||||||
 | 
					    let mut map: HashMap<&str, String> = HashMap::new();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    map.insert("to", to.to_string());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Template::render("return", &map)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[get("/cookies")]
 | 
				
			||||||
 | 
					pub async fn cookies() -> Template {
 | 
				
			||||||
 | 
					    let map: HashMap<&str, String> = HashMap::new();
 | 
				
			||||||
 | 
					    Template::render("cookies", &map)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[get("/privacy")]
 | 
				
			||||||
 | 
					pub async fn privacy() -> Template {
 | 
				
			||||||
 | 
					    let map: HashMap<&str, String> = HashMap::new();
 | 
				
			||||||
 | 
					    Template::render("privacy", &map)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[get("/terms")]
 | 
				
			||||||
 | 
					pub async fn terms() -> Template {
 | 
				
			||||||
 | 
					    let map: HashMap<&str, String> = HashMap::new();
 | 
				
			||||||
 | 
					    Template::render("terms", &map)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[get("/")]
 | 
				
			||||||
 | 
					pub async fn help() -> Template {
 | 
				
			||||||
 | 
					    let map: HashMap<&str, String> = HashMap::new();
 | 
				
			||||||
 | 
					    Template::render("help", &map)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[get("/timezone")]
 | 
				
			||||||
 | 
					pub async fn help_timezone() -> Template {
 | 
				
			||||||
 | 
					    let map: HashMap<&str, String> = HashMap::new();
 | 
				
			||||||
 | 
					    Template::render("support/timezone", &map)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[get("/create_reminder")]
 | 
				
			||||||
 | 
					pub async fn help_create_reminder() -> Template {
 | 
				
			||||||
 | 
					    let map: HashMap<&str, String> = HashMap::new();
 | 
				
			||||||
 | 
					    Template::render("support/create_reminder", &map)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[get("/delete_reminder")]
 | 
				
			||||||
 | 
					pub async fn help_delete_reminder() -> Template {
 | 
				
			||||||
 | 
					    let map: HashMap<&str, String> = HashMap::new();
 | 
				
			||||||
 | 
					    Template::render("support/delete_reminder", &map)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[get("/timers")]
 | 
				
			||||||
 | 
					pub async fn help_timers() -> Template {
 | 
				
			||||||
 | 
					    let map: HashMap<&str, String> = HashMap::new();
 | 
				
			||||||
 | 
					    Template::render("support/timers", &map)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[get("/todo_lists")]
 | 
				
			||||||
 | 
					pub async fn help_todo_lists() -> Template {
 | 
				
			||||||
 | 
					    let map: HashMap<&str, String> = HashMap::new();
 | 
				
			||||||
 | 
					    Template::render("support/todo_lists", &map)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[get("/macros")]
 | 
				
			||||||
 | 
					pub async fn help_macros() -> Template {
 | 
				
			||||||
 | 
					    let map: HashMap<&str, String> = HashMap::new();
 | 
				
			||||||
 | 
					    Template::render("support/macros", &map)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										1
									
								
								web/static/css/bulma.min.css
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										91
									
								
								web/static/css/dtsel.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,91 @@
 | 
				
			|||||||
 | 
					.date-selector-wrapper {
 | 
				
			||||||
 | 
					    width: 200px;
 | 
				
			||||||
 | 
					    padding: 3px;
 | 
				
			||||||
 | 
					    background-color: #fff;
 | 
				
			||||||
 | 
					    box-shadow: 1px 1px 10px 1px #5c5c5c;
 | 
				
			||||||
 | 
					    position: absolute;
 | 
				
			||||||
 | 
					    font-size: 12px;
 | 
				
			||||||
 | 
					    -webkit-user-select: none;
 | 
				
			||||||
 | 
					    -khtml-user-select: none;
 | 
				
			||||||
 | 
					    -moz-user-select: none;
 | 
				
			||||||
 | 
					    -ms-user-select: none;
 | 
				
			||||||
 | 
					    -o-user-select: none;
 | 
				
			||||||
 | 
					    /* user-select: none; */
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.cal-header, .cal-row {
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    width: 100%;
 | 
				
			||||||
 | 
					    height: 30px;
 | 
				
			||||||
 | 
					    line-height: 30px;
 | 
				
			||||||
 | 
					    text-align: center;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.cal-cell, .cal-nav {
 | 
				
			||||||
 | 
					    cursor: pointer;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.cal-day-names {
 | 
				
			||||||
 | 
					    height: 25px;
 | 
				
			||||||
 | 
					    line-height: 25px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.cal-day-names .cal-cell {
 | 
				
			||||||
 | 
					    cursor: default;
 | 
				
			||||||
 | 
					    font-weight: bold;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.cal-cell-prev, .cal-cell-next {
 | 
				
			||||||
 | 
					    color: #777;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.cal-months .cal-row, .cal-years .cal-row {
 | 
				
			||||||
 | 
					    height: 60px;
 | 
				
			||||||
 | 
					    line-height: 60px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.cal-nav-prev, .cal-nav-next {
 | 
				
			||||||
 | 
					    flex: 0.15;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.cal-nav-current {
 | 
				
			||||||
 | 
					    flex: 0.75;
 | 
				
			||||||
 | 
					    font-weight: bold;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.cal-months .cal-cell, .cal-years .cal-cell {
 | 
				
			||||||
 | 
					    flex: 0.25;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.cal-days .cal-cell {
 | 
				
			||||||
 | 
					    flex: 0.143;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.cal-value {
 | 
				
			||||||
 | 
					    color: #fff;
 | 
				
			||||||
 | 
					    background-color: #286090;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.cal-cell:hover, .cal-nav:hover {
 | 
				
			||||||
 | 
					    background-color: #eee;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.cal-value:hover {
 | 
				
			||||||
 | 
					    background-color: #204d74;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* time footer */
 | 
				
			||||||
 | 
					.cal-time {
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    justify-content: flex-start;
 | 
				
			||||||
 | 
					    height: 27px;
 | 
				
			||||||
 | 
					    line-height: 27px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.cal-time-label, .cal-time-value {
 | 
				
			||||||
 | 
					    flex: 0.12;
 | 
				
			||||||
 | 
					    text-align: center;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.cal-time-slider {
 | 
				
			||||||
 | 
					    flex: 0.77;
 | 
				
			||||||
 | 
					    background-image: linear-gradient(to right, #d1d8dd, #d1d8dd);
 | 
				
			||||||
 | 
					    background-repeat: no-repeat;
 | 
				
			||||||
 | 
					    background-size: 100% 1px;
 | 
				
			||||||
 | 
					    background-position: left 50%;
 | 
				
			||||||
 | 
					    height: 100%;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.cal-time-slider input {
 | 
				
			||||||
 | 
					    width: 100%;
 | 
				
			||||||
 | 
					    -webkit-appearance: none;
 | 
				
			||||||
 | 
					    background: 0 0;
 | 
				
			||||||
 | 
					    cursor: pointer;
 | 
				
			||||||
 | 
					    height: 100%;
 | 
				
			||||||
 | 
					    outline: 0;
 | 
				
			||||||
 | 
					    user-select: auto;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										12749
									
								
								web/static/css/fa.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										63
									
								
								web/static/css/font.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,63 @@
 | 
				
			|||||||
 | 
					@font-face {
 | 
				
			||||||
 | 
					  font-family: 'Source Sans Pro';
 | 
				
			||||||
 | 
					  font-style: italic;
 | 
				
			||||||
 | 
					  font-weight: 300;
 | 
				
			||||||
 | 
					  src: local('Source Sans Pro Light Italic'), local('SourceSansPro-LightItalic'), url(https://fonts.gstatic.com/s/sourcesanspro/v13/6xKwdSBYKcSV-LCoeQqfX1RYOo3qPZZMkids18E.ttf) format('truetype');
 | 
				
			||||||
 | 
					  font-display: swap;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					@font-face {
 | 
				
			||||||
 | 
					  font-family: 'Source Sans Pro';
 | 
				
			||||||
 | 
					  font-style: italic;
 | 
				
			||||||
 | 
					  font-weight: 400;
 | 
				
			||||||
 | 
					  src: local('Source Sans Pro Italic'), local('SourceSansPro-Italic'), url(https://fonts.gstatic.com/s/sourcesanspro/v13/6xK1dSBYKcSV-LCoeQqfX1RYOo3qPZ7nsDc.ttf) format('truetype');
 | 
				
			||||||
 | 
					  font-display: swap;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					@font-face {
 | 
				
			||||||
 | 
					  font-family: 'Source Sans Pro';
 | 
				
			||||||
 | 
					  font-style: italic;
 | 
				
			||||||
 | 
					  font-weight: 600;
 | 
				
			||||||
 | 
					  src: local('Source Sans Pro SemiBold Italic'), local('SourceSansPro-SemiBoldItalic'), url(https://fonts.gstatic.com/s/sourcesanspro/v13/6xKwdSBYKcSV-LCoeQqfX1RYOo3qPZY4lCds18E.ttf) format('truetype');
 | 
				
			||||||
 | 
					  font-display: swap;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					@font-face {
 | 
				
			||||||
 | 
					  font-family: 'Source Sans Pro';
 | 
				
			||||||
 | 
					  font-style: normal;
 | 
				
			||||||
 | 
					  font-weight: 300;
 | 
				
			||||||
 | 
					  src: local('Source Sans Pro Light'), local('SourceSansPro-Light'), url(https://fonts.gstatic.com/s/sourcesanspro/v13/6xKydSBYKcSV-LCoeQqfX1RYOo3ik4zwlxdr.ttf) format('truetype');
 | 
				
			||||||
 | 
					  font-display: swap;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					@font-face {
 | 
				
			||||||
 | 
					  font-family: 'Source Sans Pro';
 | 
				
			||||||
 | 
					  font-style: normal;
 | 
				
			||||||
 | 
					  font-weight: 400;
 | 
				
			||||||
 | 
					  src: local('Source Sans Pro Regular'), local('SourceSansPro-Regular'), url(https://fonts.gstatic.com/s/sourcesanspro/v13/6xK3dSBYKcSV-LCoeQqfX1RYOo3qOK7g.ttf) format('truetype');
 | 
				
			||||||
 | 
					  font-display: swap;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					@font-face {
 | 
				
			||||||
 | 
					  font-family: 'Source Sans Pro';
 | 
				
			||||||
 | 
					  font-style: normal;
 | 
				
			||||||
 | 
					  font-weight: 600;
 | 
				
			||||||
 | 
					  src: local('Source Sans Pro SemiBold'), local('SourceSansPro-SemiBold'), url(https://fonts.gstatic.com/s/sourcesanspro/v13/6xKydSBYKcSV-LCoeQqfX1RYOo3i54rwlxdr.ttf) format('truetype');
 | 
				
			||||||
 | 
					  font-display: swap;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					@font-face {
 | 
				
			||||||
 | 
					  font-family: 'Source Sans Pro';
 | 
				
			||||||
 | 
					  font-style: normal;
 | 
				
			||||||
 | 
					  font-weight: 700;
 | 
				
			||||||
 | 
					  src: local('Source Sans Pro Bold'), local('SourceSansPro-Bold'), url(https://fonts.gstatic.com/s/sourcesanspro/v13/6xKydSBYKcSV-LCoeQqfX1RYOo3ig4vwlxdr.ttf) format('truetype');
 | 
				
			||||||
 | 
					  font-display: swap;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					@font-face {
 | 
				
			||||||
 | 
					  font-family: 'Ubuntu';
 | 
				
			||||||
 | 
					  font-style: normal;
 | 
				
			||||||
 | 
					  font-weight: 400;
 | 
				
			||||||
 | 
					  src: url(https://fonts.gstatic.com/s/ubuntu/v15/4iCs6KVjbNBYlgo6eA.ttf) format('truetype');
 | 
				
			||||||
 | 
					  font-display: swap;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					@font-face {
 | 
				
			||||||
 | 
					  font-family: 'Ubuntu';
 | 
				
			||||||
 | 
					  font-style: normal;
 | 
				
			||||||
 | 
					  font-weight: 700;
 | 
				
			||||||
 | 
					  src: url(https://fonts.gstatic.com/s/ubuntu/v15/4iCv6KVjbNBYlgoCxCvTtw.ttf) format('truetype');
 | 
				
			||||||
 | 
					  font-display: swap;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										578
									
								
								web/static/css/style.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,578 @@
 | 
				
			|||||||
 | 
					* {
 | 
				
			||||||
 | 
					    font-family: "Ubuntu Bold", "Ubuntu", sans-serif;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					button {
 | 
				
			||||||
 | 
					    font-weight: 700;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* override styles for when the div is collapsed */
 | 
				
			||||||
 | 
					div.reminderContent.is-collapsed .column.discord-frame {
 | 
				
			||||||
 | 
					    display: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					div.reminderContent.is-collapsed .collapses {
 | 
				
			||||||
 | 
					    display: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					div.reminderContent.is-collapsed .invert-collapses {
 | 
				
			||||||
 | 
					    display: inline-flex;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					div.reminderContent .invert-collapses {
 | 
				
			||||||
 | 
					    display: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					div.reminderContent.is-collapsed .settings {
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    flex-direction: row;
 | 
				
			||||||
 | 
					    padding-bottom: 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					div.reminderContent.is-collapsed .channel-field {
 | 
				
			||||||
 | 
					    display: inline-flex;
 | 
				
			||||||
 | 
					    order: 1;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					div.reminderContent.is-collapsed .reminder-topbar {
 | 
				
			||||||
 | 
					    display: inline-flex;
 | 
				
			||||||
 | 
					    margin-bottom: 0px;
 | 
				
			||||||
 | 
					    flex-grow: 1;
 | 
				
			||||||
 | 
					    order: 2;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					div.reminderContent.is-collapsed input[name="name"] {
 | 
				
			||||||
 | 
					    display: inline-flex;
 | 
				
			||||||
 | 
					    flex-grow: 1;
 | 
				
			||||||
 | 
					    border: none;
 | 
				
			||||||
 | 
					    font-weight: 700;
 | 
				
			||||||
 | 
					    background: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					div.reminderContent.is-collapsed button.hide-box {
 | 
				
			||||||
 | 
					    display: inline-flex;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					div.reminderContent.is-collapsed button.hide-box i {
 | 
				
			||||||
 | 
					    transform: rotate(90deg);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					/* END */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* dashboard styles */
 | 
				
			||||||
 | 
					button.inline-btn {
 | 
				
			||||||
 | 
					    height: 100%;
 | 
				
			||||||
 | 
					    padding: 5px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					button.change-color {
 | 
				
			||||||
 | 
					    position: absolute;
 | 
				
			||||||
 | 
					    left: calc(-1rem - 40px);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					button.disable-enable[data-action="enable"]:after {
 | 
				
			||||||
 | 
					    content: "Enable";
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					button.disable-enable[data-action="disable"]:after {
 | 
				
			||||||
 | 
					    content: "Disable";
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.media-content {
 | 
				
			||||||
 | 
					    overflow-x: visible;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					div.discord-embed {
 | 
				
			||||||
 | 
					    position: relative;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					div.reminderContent {
 | 
				
			||||||
 | 
					    padding: 2px;
 | 
				
			||||||
 | 
					    background-color: #f5f5f5;
 | 
				
			||||||
 | 
					    border-radius: 8px;
 | 
				
			||||||
 | 
					    margin: 8px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					div.interval-group > button {
 | 
				
			||||||
 | 
					    margin-left: auto;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* Interval inputs */
 | 
				
			||||||
 | 
					div.interval-group > .interval-group-left input {
 | 
				
			||||||
 | 
					    -webkit-appearance: none;
 | 
				
			||||||
 | 
					    border-style: none;
 | 
				
			||||||
 | 
					    background-color: #eee;
 | 
				
			||||||
 | 
					    font-size: 1rem;
 | 
				
			||||||
 | 
					    font-family: monospace;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					div.interval-group > .interval-group-left input.w2 {
 | 
				
			||||||
 | 
					    width: 3ch;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					div.interval-group > .interval-group-left input.w3 {
 | 
				
			||||||
 | 
					    width: 6ch;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					div.interval-group {
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    flex-direction: row;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					/* !Interval inputs */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.left-pad {
 | 
				
			||||||
 | 
					    padding-left: 1rem;
 | 
				
			||||||
 | 
					    padding-right: 0.2rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.notification {
 | 
				
			||||||
 | 
					    padding-right: 1.5rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					div.inset-content {
 | 
				
			||||||
 | 
					    margin-left: 10%;
 | 
				
			||||||
 | 
					    margin-right: 10%;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					div.flash-message {
 | 
				
			||||||
 | 
					    position: fixed;
 | 
				
			||||||
 | 
					    width: calc(100% - 32px);
 | 
				
			||||||
 | 
					    margin: 16px !important;
 | 
				
			||||||
 | 
					    z-index: 99;
 | 
				
			||||||
 | 
					    bottom: 0;
 | 
				
			||||||
 | 
					    display: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					div.flash-message.is-active {
 | 
				
			||||||
 | 
					    display: block;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					body {
 | 
				
			||||||
 | 
					    min-height: 100vh;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					span.spacer {
 | 
				
			||||||
 | 
					    width: 10px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					nav .dashboard-button {
 | 
				
			||||||
 | 
					    background: white ;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					span.patreon-color {
 | 
				
			||||||
 | 
					    color: #f96854;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					p.pageTitle {
 | 
				
			||||||
 | 
					    margin-left: 12px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#welcome > div {
 | 
				
			||||||
 | 
					    height: 100%;
 | 
				
			||||||
 | 
					    padding-top: 30vh;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					div#pageNavbar {
 | 
				
			||||||
 | 
					    background-color: #363636;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					div#pageNavbar a {
 | 
				
			||||||
 | 
					    color: #fff;
 | 
				
			||||||
 | 
					    text-align: center;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					div#pageNavbar a:hover {
 | 
				
			||||||
 | 
					    background-color: #4a4a4a;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					img.rounded-corners {
 | 
				
			||||||
 | 
					    border-radius: 12px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					div.brand {
 | 
				
			||||||
 | 
					    text-align: center;
 | 
				
			||||||
 | 
					    height: 52px;
 | 
				
			||||||
 | 
					    background-color: #8fb677;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					img.dashboard-brand {
 | 
				
			||||||
 | 
					    text-align: center;
 | 
				
			||||||
 | 
					    height: 100%;
 | 
				
			||||||
 | 
					    width: auto;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					div.dashboard-sidebar {
 | 
				
			||||||
 | 
					    background-color: #363636;
 | 
				
			||||||
 | 
					    width: 230px !important;
 | 
				
			||||||
 | 
					    padding-right: 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					div.dashboard-sidebar:not(.mobile-sidebar) {
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    flex-direction: column;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					div.dashboard-sidebar:not(.mobile-sidebar) .aside-footer {
 | 
				
			||||||
 | 
					    position: fixed;
 | 
				
			||||||
 | 
					    bottom: 0;
 | 
				
			||||||
 | 
					    width: 226px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					div.mobile-sidebar {
 | 
				
			||||||
 | 
					    z-index: 100;
 | 
				
			||||||
 | 
					    min-height: 100vh;
 | 
				
			||||||
 | 
					    position: absolute;
 | 
				
			||||||
 | 
					    top: 0;
 | 
				
			||||||
 | 
					    display: none;
 | 
				
			||||||
 | 
					    flex-direction: column;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#expandAll {
 | 
				
			||||||
 | 
					    width: 60px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					div.mobile-sidebar .aside-footer {
 | 
				
			||||||
 | 
					    margin-top: auto;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					div.mobile-sidebar.is-active {
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					aside.menu {
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    flex-direction: column;
 | 
				
			||||||
 | 
					    flex-grow: 1;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					div.dashboard-frame {
 | 
				
			||||||
 | 
					    min-height: 100vh;
 | 
				
			||||||
 | 
					    margin-bottom: 0 !important;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.embed-field-box[data-inlined="0"] .inline-btn > i {
 | 
				
			||||||
 | 
					    transform: rotate(90deg);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.embed-field-box[data-inlined="0"] {
 | 
				
			||||||
 | 
					    min-width: 100%;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.embed-field-box[data-inlined="1"] {
 | 
				
			||||||
 | 
					    min-width: auto;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.menu a {
 | 
				
			||||||
 | 
					    color: #fff;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.menu .menu-label {
 | 
				
			||||||
 | 
					    color: #bbb;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.menu {
 | 
				
			||||||
 | 
					    padding-left: 4px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.dashboard-navbar {
 | 
				
			||||||
 | 
					    background-color: #8fb677 !important;
 | 
				
			||||||
 | 
					    position: absolute;
 | 
				
			||||||
 | 
					    top: 0;
 | 
				
			||||||
 | 
					    width: 100%;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					textarea.autoresize {
 | 
				
			||||||
 | 
					    resize: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					textarea, input {
 | 
				
			||||||
 | 
					    width: 100%;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.message-input:placeholder-shown {
 | 
				
			||||||
 | 
					    border-top: none;
 | 
				
			||||||
 | 
					    border-left: none;
 | 
				
			||||||
 | 
					    border-right: none;
 | 
				
			||||||
 | 
					    border-bottom-style: dashed;
 | 
				
			||||||
 | 
					    background-color: #40444b;
 | 
				
			||||||
 | 
					    color: #fff;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.message-input {
 | 
				
			||||||
 | 
					    border: none;
 | 
				
			||||||
 | 
					    background-color: rgba(0, 0, 0, 0);
 | 
				
			||||||
 | 
					    color: #fff;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.time-input {
 | 
				
			||||||
 | 
					    border-top: none;
 | 
				
			||||||
 | 
					    border-left: none;
 | 
				
			||||||
 | 
					    border-right: none;
 | 
				
			||||||
 | 
					    border-bottom-style: solid;
 | 
				
			||||||
 | 
					    background-color: #40444b;
 | 
				
			||||||
 | 
					    color: #fff;
 | 
				
			||||||
 | 
					    width: 120px;
 | 
				
			||||||
 | 
					    font-size: 0.875rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.message-input::placeholder {
 | 
				
			||||||
 | 
					    color: #72767b;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.discord-title {
 | 
				
			||||||
 | 
					    font-weight: bold;
 | 
				
			||||||
 | 
					    font-size: 1rem;
 | 
				
			||||||
 | 
					    margin: 4px 0 4px 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.discord-description {
 | 
				
			||||||
 | 
					    font-size: 0.875rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.discord-username {
 | 
				
			||||||
 | 
					    font-size: 1rem;
 | 
				
			||||||
 | 
					    font-weight: bold;
 | 
				
			||||||
 | 
					    margin-bottom: 4px;
 | 
				
			||||||
 | 
					    width: initial;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.discord-message-header {
 | 
				
			||||||
 | 
					    white-space: nowrap;
 | 
				
			||||||
 | 
					    margin-bottom: 8px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.discord-content {
 | 
				
			||||||
 | 
					    margin-bottom: 8px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.customizable img {
 | 
				
			||||||
 | 
					    background-color: #72767b;
 | 
				
			||||||
 | 
					    border-radius: 8px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.customizable.is-20x20 img {
 | 
				
			||||||
 | 
					    width: 20px;
 | 
				
			||||||
 | 
					    height: 20px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.customizable.is-24x24 img {
 | 
				
			||||||
 | 
					    width: 24px;
 | 
				
			||||||
 | 
					    height: 24px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.customizable.is-400x300 img {
 | 
				
			||||||
 | 
					    margin-top: 10px;
 | 
				
			||||||
 | 
					    width: 100%;
 | 
				
			||||||
 | 
					    min-height: 100px;
 | 
				
			||||||
 | 
					    max-height: 400px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.customizable.is-32x32 img {
 | 
				
			||||||
 | 
					    width: 32px;
 | 
				
			||||||
 | 
					    height: 32px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.customizable.thumbnail img {
 | 
				
			||||||
 | 
					    width: 100px;
 | 
				
			||||||
 | 
					    height: 100px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.customizable input.imageInput {
 | 
				
			||||||
 | 
					    display: none;
 | 
				
			||||||
 | 
					    position: absolute;
 | 
				
			||||||
 | 
					    top: 0;
 | 
				
			||||||
 | 
					    left: 36px;
 | 
				
			||||||
 | 
					    width: 400px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.customizable.thumbnail input.imageInput {
 | 
				
			||||||
 | 
					    display: none;
 | 
				
			||||||
 | 
					    position: absolute;
 | 
				
			||||||
 | 
					    top: 0;
 | 
				
			||||||
 | 
					    left: -400px;
 | 
				
			||||||
 | 
					    width: 400px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.customizable input.is-active {
 | 
				
			||||||
 | 
					    display: block !important;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.discord-frame {
 | 
				
			||||||
 | 
					    color: #fff;
 | 
				
			||||||
 | 
					    padding: 10px;
 | 
				
			||||||
 | 
					    border-radius: 8px;
 | 
				
			||||||
 | 
					    background-color: #36393f;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.discord-embed {
 | 
				
			||||||
 | 
					    padding: 8px 16px 16px 12px;
 | 
				
			||||||
 | 
					    margin: 0 20px 4px 0;
 | 
				
			||||||
 | 
					    border-radius: 4px;
 | 
				
			||||||
 | 
					    border-left: 4px solid #fff;
 | 
				
			||||||
 | 
					    background-color: #2f3136;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.embed-author-box {
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    align-items: center;
 | 
				
			||||||
 | 
					    margin-bottom: 8px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.embed-author-box > .a {
 | 
				
			||||||
 | 
					    flex: initial;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.embed-author-box > .b {
 | 
				
			||||||
 | 
					    flex: auto;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.embed-footer-box {
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    align-items: center;
 | 
				
			||||||
 | 
					    margin-bottom: 8px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.embed-author-box .image {
 | 
				
			||||||
 | 
					    margin: 0 8px 0 0 !important;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.embed-footer-box .image {
 | 
				
			||||||
 | 
					    margin: 0 8px 0 0 !important;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.discord-embed-author {
 | 
				
			||||||
 | 
					    display: inline-block;
 | 
				
			||||||
 | 
					    font-size: 0.875rem;
 | 
				
			||||||
 | 
					    font-weight: bold;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.discord-embed-footer {
 | 
				
			||||||
 | 
					    font-size: 0.75rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.embed-body {
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.embed-body > .a {
 | 
				
			||||||
 | 
					    flex-grow: 1;
 | 
				
			||||||
 | 
					    flex-shrink: 1;
 | 
				
			||||||
 | 
					    flex-basis: auto;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.embed-body input, .embed-body textarea {
 | 
				
			||||||
 | 
					    min-width: 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.embed-body > .b {
 | 
				
			||||||
 | 
					    flex-grow: 0;
 | 
				
			||||||
 | 
					    flex-shrink: 0;
 | 
				
			||||||
 | 
					    flex-basis: auto;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.discord-field-title, .discord-field-value {
 | 
				
			||||||
 | 
					    max-width: 120px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.discord-field-title {
 | 
				
			||||||
 | 
					    font-weight: bold;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.embed-field-box {
 | 
				
			||||||
 | 
					    margin: 12px 8px 0 0;
 | 
				
			||||||
 | 
					    max-width: 120px;
 | 
				
			||||||
 | 
					    flex: initial;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.field-input {
 | 
				
			||||||
 | 
					    font-size: 0.875rem;
 | 
				
			||||||
 | 
					    width: 120px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.embed-multifield-box {
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    max-width: 100%;
 | 
				
			||||||
 | 
					    flex-wrap: wrap;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.channel-select {
 | 
				
			||||||
 | 
					    font-size: 1.125rem;
 | 
				
			||||||
 | 
					    margin-bottom: 4px;
 | 
				
			||||||
 | 
					    margin-left: 48px;
 | 
				
			||||||
 | 
					    display: inline-flex;
 | 
				
			||||||
 | 
					    font-weight: bold;
 | 
				
			||||||
 | 
					    color: #6e89da;
 | 
				
			||||||
 | 
					    width: auto;
 | 
				
			||||||
 | 
					    border-radius: 2px;
 | 
				
			||||||
 | 
					    border-bottom: 1px solid #fff;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@media only screen and (max-width: 768px) {
 | 
				
			||||||
 | 
					    .customizable.thumbnail img {
 | 
				
			||||||
 | 
					        width: 60px;
 | 
				
			||||||
 | 
					        height: 60px;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .customizable.is-24x24 img {
 | 
				
			||||||
 | 
					        width: 16px;
 | 
				
			||||||
 | 
					        height: 16px;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* loader */
 | 
				
			||||||
 | 
					#loader {
 | 
				
			||||||
 | 
					    position: fixed;
 | 
				
			||||||
 | 
					    background-color: rgba(255, 255, 255, 0.8);
 | 
				
			||||||
 | 
					    width: 100vw;
 | 
				
			||||||
 | 
					    z-index: 999;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#loader .title {
 | 
				
			||||||
 | 
					    font-size: 6rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* END */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* other stuff */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.half-rem {
 | 
				
			||||||
 | 
					    width: 0.5rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.pad-left {
 | 
				
			||||||
 | 
					    width: 12px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#dead {
 | 
				
			||||||
 | 
					    display: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.colorpicker-container {
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    justify-content: center;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.create-reminder {
 | 
				
			||||||
 | 
					    margin: 0 12px 12px 12px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.button.is-success:not(.is-outlined) {
 | 
				
			||||||
 | 
					    color: white;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.button.is-outlined.is-success {
 | 
				
			||||||
 | 
					    background-color: white;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.is-locked {
 | 
				
			||||||
 | 
					    pointer-events: none;
 | 
				
			||||||
 | 
					    opacity: 0.4;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.is-locked .foreground {
 | 
				
			||||||
 | 
					    pointer-events: auto;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.is-locked .field:last-of-type {
 | 
				
			||||||
 | 
					    display: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										
											BIN
										
									
								
								web/static/favicon/android-chrome-192x192.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 7.2 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								web/static/favicon/android-chrome-512x512.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 20 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								web/static/favicon/apple-touch-icon.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 6.8 KiB  | 
							
								
								
									
										9
									
								
								web/static/favicon/browserconfig.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,9 @@
 | 
				
			|||||||
 | 
					<?xml version="1.0" encoding="utf-8"?>
 | 
				
			||||||
 | 
					<browserconfig>
 | 
				
			||||||
 | 
					    <msapplication>
 | 
				
			||||||
 | 
					        <tile>
 | 
				
			||||||
 | 
					            <square150x150logo src="/mstile-150x150.png"/>
 | 
				
			||||||
 | 
					            <TileColor>#da532c</TileColor>
 | 
				
			||||||
 | 
					        </tile>
 | 
				
			||||||
 | 
					    </msapplication>
 | 
				
			||||||
 | 
					</browserconfig>
 | 
				
			||||||
							
								
								
									
										
											BIN
										
									
								
								web/static/favicon/favicon-16x16.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 1.8 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								web/static/favicon/favicon-32x32.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 2.3 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								web/static/favicon/favicon.ico
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 7.2 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								web/static/favicon/mstile-150x150.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 4.9 KiB  | 
							
								
								
									
										19
									
								
								web/static/favicon/site.webmanifest
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,19 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
					    "name": "",
 | 
				
			||||||
 | 
					    "short_name": "",
 | 
				
			||||||
 | 
					    "icons": [
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            "src": "/android-chrome-192x192.png",
 | 
				
			||||||
 | 
					            "sizes": "192x192",
 | 
				
			||||||
 | 
					            "type": "image/png"
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            "src": "/android-chrome-512x512.png",
 | 
				
			||||||
 | 
					            "sizes": "512x512",
 | 
				
			||||||
 | 
					            "type": "image/png"
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					    "theme_color": "#ffffff",
 | 
				
			||||||
 | 
					    "background_color": "#ffffff",
 | 
				
			||||||
 | 
					    "display": "standalone"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										
											BIN
										
									
								
								web/static/img/bg.webp
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 762 B  | 
							
								
								
									
										
											BIN
										
									
								
								web/static/img/icon.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 11 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								web/static/img/logo_flat.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 323 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								web/static/img/logo_flat.webp
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 61 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								web/static/img/slash-commands.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 23 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								web/static/img/support/delete_reminder/1.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 35 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								web/static/img/support/delete_reminder/2.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 55 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								web/static/img/support/delete_reminder/3.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 3.6 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								web/static/img/support/delete_reminder/cmd-1.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 24 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								web/static/img/support/delete_reminder/cmd-2.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 17 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								web/static/img/tournament-demo.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 65 KiB  | 
							
								
								
									
										931
									
								
								web/static/js/dtsel.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,931 @@
 | 
				
			|||||||
 | 
					(function () {
 | 
				
			||||||
 | 
					    "use strict";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    var BODYTYPES = ["DAYS", "MONTHS", "YEARS"];
 | 
				
			||||||
 | 
					    var MONTHS = [
 | 
				
			||||||
 | 
					        "January", "February", "March", "April", "May", "June",
 | 
				
			||||||
 | 
					        "July", "August", "September", "October", "November", "December"
 | 
				
			||||||
 | 
					    ];
 | 
				
			||||||
 | 
					    var WEEKDAYS = [
 | 
				
			||||||
 | 
					        "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"
 | 
				
			||||||
 | 
					    ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /** @typedef {Object.<string, Function[]>} Handlers */
 | 
				
			||||||
 | 
					    /** @typedef {function(String, Function): null} AddHandler */
 | 
				
			||||||
 | 
					    /** @typedef {("DAYS"|"MONTHS"|"YEARS")} BodyType */
 | 
				
			||||||
 | 
					    /** @typedef {string|number} StringNum */
 | 
				
			||||||
 | 
					    /** @typedef {Object.<string, StringNum>} StringNumObj */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The local state
 | 
				
			||||||
 | 
					     * @typedef {Object} InstanceState
 | 
				
			||||||
 | 
					     * @property {Date} value
 | 
				
			||||||
 | 
					     * @property {Number} year
 | 
				
			||||||
 | 
					     * @property {Number} month
 | 
				
			||||||
 | 
					     * @property {Number} day
 | 
				
			||||||
 | 
					     * @property {Number} time
 | 
				
			||||||
 | 
					     * @property {Number} hours
 | 
				
			||||||
 | 
					     * @property {Number} minutes
 | 
				
			||||||
 | 
					     * @property {Number} seconds
 | 
				
			||||||
 | 
					     * @property {BodyType} bodyType
 | 
				
			||||||
 | 
					     * @property {Boolean} visible
 | 
				
			||||||
 | 
					     * @property {Number} cancelBlur
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /** 
 | 
				
			||||||
 | 
					     * @typedef {Object} Config
 | 
				
			||||||
 | 
					     * @property {String} dateFormat
 | 
				
			||||||
 | 
					     * @property {String} timeFormat
 | 
				
			||||||
 | 
					     * @property {Boolean} showDate
 | 
				
			||||||
 | 
					     * @property {Boolean} showTime
 | 
				
			||||||
 | 
					     * @property {Number} paddingX
 | 
				
			||||||
 | 
					     * @property {Number} paddingY
 | 
				
			||||||
 | 
					     * @property {BodyType} defaultView
 | 
				
			||||||
 | 
					     * @property {"TOP"|"BOTTOM"} direction
 | 
				
			||||||
 | 
					    */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @class
 | 
				
			||||||
 | 
					     * @param {HTMLElement} elem 
 | 
				
			||||||
 | 
					     * @param {Config} config 
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    function DTS(elem, config) {
 | 
				
			||||||
 | 
					        var config = config || {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /** @type {Config} */
 | 
				
			||||||
 | 
					        var defaultConfig = {
 | 
				
			||||||
 | 
					            defaultView: BODYTYPES[0],
 | 
				
			||||||
 | 
					            dateFormat: "yyyy-mm-dd",
 | 
				
			||||||
 | 
					            timeFormat: "HH:MM:SS",
 | 
				
			||||||
 | 
					            showDate: true,
 | 
				
			||||||
 | 
					            showTime: false,
 | 
				
			||||||
 | 
					            paddingX: 5,
 | 
				
			||||||
 | 
					            paddingY: 5,
 | 
				
			||||||
 | 
					            direction: 'TOP'
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (!elem) {
 | 
				
			||||||
 | 
					            throw TypeError("input element or selector required for contructor");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if (Object.getPrototypeOf(elem) === String.prototype) {
 | 
				
			||||||
 | 
					            var _elem = document.querySelectorAll(elem);
 | 
				
			||||||
 | 
					            if (!_elem[0]){
 | 
				
			||||||
 | 
					                throw Error('"' + elem + '" not found.');
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            elem = _elem[0];
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        this.config = setDefaults(config, defaultConfig);
 | 
				
			||||||
 | 
					        this.dateFormat = this.config.dateFormat;
 | 
				
			||||||
 | 
					        this.timeFormat = this.config.timeFormat;
 | 
				
			||||||
 | 
					        this.dateFormatRegEx = new RegExp("yyyy|yy|mm|dd", "gi");
 | 
				
			||||||
 | 
					        this.timeFormatRegEx = new RegExp("hh|mm|ss|a", "gi");
 | 
				
			||||||
 | 
					        this.inputElem = elem;
 | 
				
			||||||
 | 
					        this.dtbox = null;
 | 
				
			||||||
 | 
					        this.setup();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    DTS.prototype.setup = function () {
 | 
				
			||||||
 | 
					        var handler = this.inputElemHandler.bind(this);
 | 
				
			||||||
 | 
					        this.inputElem.addEventListener("focus", handler, false)
 | 
				
			||||||
 | 
					        this.inputElem.addEventListener("blur", handler, false);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    DTS.prototype.inputElemHandler = function (e) {
 | 
				
			||||||
 | 
					        if (e.type == "focus") {
 | 
				
			||||||
 | 
					            if (!this.dtbox) {
 | 
				
			||||||
 | 
					                this.dtbox = new DTBox(e.target, this);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            this.dtbox.visible = true;
 | 
				
			||||||
 | 
					        } else if (e.type == "blur" && this.dtbox && this.dtbox.visible) {
 | 
				
			||||||
 | 
					            var self = this;
 | 
				
			||||||
 | 
					            setTimeout(function () {
 | 
				
			||||||
 | 
					                if (self.dtbox.cancelBlur > 0) {
 | 
				
			||||||
 | 
					                    self.dtbox.cancelBlur -= 1;
 | 
				
			||||||
 | 
					                 } else {
 | 
				
			||||||
 | 
					                    self.dtbox.visible = false;
 | 
				
			||||||
 | 
					                    self.inputElem.blur();
 | 
				
			||||||
 | 
					                 }
 | 
				
			||||||
 | 
					            }, 100);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @class
 | 
				
			||||||
 | 
					     * @param {HTMLElement} elem 
 | 
				
			||||||
 | 
					     * @param {DTS} settings 
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    function DTBox(elem, settings) {
 | 
				
			||||||
 | 
					        /** @type {DTBox} */
 | 
				
			||||||
 | 
					        var self = this;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /** @type {Handlers} */
 | 
				
			||||||
 | 
					        var handlers = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /** @type {InstanceState} */
 | 
				
			||||||
 | 
					        var localState = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /**
 | 
				
			||||||
 | 
					         * @param {String} key 
 | 
				
			||||||
 | 
					         * @param {*} default_val 
 | 
				
			||||||
 | 
					         */
 | 
				
			||||||
 | 
					        function getterSetter(key, default_val) {
 | 
				
			||||||
 | 
					            return {
 | 
				
			||||||
 | 
					                get: function () {
 | 
				
			||||||
 | 
					                    var val = localState[key];
 | 
				
			||||||
 | 
					                    return val === undefined ? default_val : val;
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					                set: function (val) {
 | 
				
			||||||
 | 
					                    var prevState = self.state;
 | 
				
			||||||
 | 
					                    var _handlers = handlers[key] || [];
 | 
				
			||||||
 | 
					                    localState[key] = val;
 | 
				
			||||||
 | 
					                    for (var i = 0; i < _handlers.length; i++) {
 | 
				
			||||||
 | 
					                        _handlers[i].bind(self)(localState, prevState);
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /** @type {AddHandler} */
 | 
				
			||||||
 | 
					        function addHandler(key, handlerFn) {
 | 
				
			||||||
 | 
					            if (!key || !handlerFn) {
 | 
				
			||||||
 | 
					                return false;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            if (!handlers[key]) {
 | 
				
			||||||
 | 
					                handlers[key] = [];
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            handlers[key].push(handlerFn);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Object.defineProperties(this, {
 | 
				
			||||||
 | 
					            visible: getterSetter("visible", false),
 | 
				
			||||||
 | 
					            bodyType: getterSetter("bodyType", settings.config.defaultView),
 | 
				
			||||||
 | 
					            value: getterSetter("value"),
 | 
				
			||||||
 | 
					            year: getterSetter("year", 0),
 | 
				
			||||||
 | 
					            month: getterSetter("month", 0),
 | 
				
			||||||
 | 
					            day: getterSetter("day", 0),
 | 
				
			||||||
 | 
					            hours: getterSetter("hours", 0),
 | 
				
			||||||
 | 
					            minutes: getterSetter("minutes", 0),
 | 
				
			||||||
 | 
					            seconds: getterSetter("seconds", 0),
 | 
				
			||||||
 | 
					            cancelBlur: getterSetter("cancelBlur", 0),
 | 
				
			||||||
 | 
					            addHandler: {value: addHandler},
 | 
				
			||||||
 | 
					            month_long: {
 | 
				
			||||||
 | 
					                get: function () {
 | 
				
			||||||
 | 
					                    return MONTHS[self.month];
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            month_short: {
 | 
				
			||||||
 | 
					                get: function () {
 | 
				
			||||||
 | 
					                    return self.month_long.slice(0, 3);
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            state: {
 | 
				
			||||||
 | 
					                get: function () {
 | 
				
			||||||
 | 
					                    return Object.assign({}, localState);
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            time: {
 | 
				
			||||||
 | 
					                get: function() {
 | 
				
			||||||
 | 
					                    var hours = self.hours * 60 * 60 * 1000;
 | 
				
			||||||
 | 
					                    var minutes = self.minutes * 60 * 1000;
 | 
				
			||||||
 | 
					                    var seconds = self.seconds * 1000;
 | 
				
			||||||
 | 
					                    return  hours + minutes + seconds;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        this.el = {};
 | 
				
			||||||
 | 
					        this.settings = settings;
 | 
				
			||||||
 | 
					        this.elem = elem;
 | 
				
			||||||
 | 
					        this.setup();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    DTBox.prototype.setup = function () {
 | 
				
			||||||
 | 
					        Object.defineProperties(this.el, {
 | 
				
			||||||
 | 
					            wrapper: { value: null, configurable: true },
 | 
				
			||||||
 | 
					            header: { value: null, configurable: true },
 | 
				
			||||||
 | 
					            body: { value: null, configurable: true },
 | 
				
			||||||
 | 
					            footer: { value: null, configurable: true }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        this.setupWrapper();
 | 
				
			||||||
 | 
					        if (this.settings.config.showDate) {
 | 
				
			||||||
 | 
					            this.setupHeader();
 | 
				
			||||||
 | 
					            this.setupBody();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if (this.settings.config.showTime) {
 | 
				
			||||||
 | 
					            this.setupFooter();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        var self = this;
 | 
				
			||||||
 | 
					        this.addHandler("visible", function (state, prevState) {
 | 
				
			||||||
 | 
					            if (state.visible && !prevState.visible){
 | 
				
			||||||
 | 
					                document.body.appendChild(this.el.wrapper);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                var parts = self.elem.value.split(/\s*,\s*/);
 | 
				
			||||||
 | 
					                var startDate = undefined;
 | 
				
			||||||
 | 
					                var startTime = 0;
 | 
				
			||||||
 | 
					                if (self.settings.config.showDate) {
 | 
				
			||||||
 | 
					                    startDate = parseDate(parts[0], self.settings);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                if (self.settings.config.showTime) {
 | 
				
			||||||
 | 
					                    startTime = parseTime(parts[parts.length-1], self.settings);
 | 
				
			||||||
 | 
					                    startTime = startTime || 0;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                if (!(startDate && startDate.getTime())) {
 | 
				
			||||||
 | 
					                    startDate = new Date();
 | 
				
			||||||
 | 
					                    startDate = new Date(
 | 
				
			||||||
 | 
					                        startDate.getFullYear(),
 | 
				
			||||||
 | 
					                        startDate.getMonth(),
 | 
				
			||||||
 | 
					                        startDate.getDate()
 | 
				
			||||||
 | 
					                    );
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                var value = new Date(startDate.getTime() + startTime);
 | 
				
			||||||
 | 
					                self.value = value;
 | 
				
			||||||
 | 
					                self.year = value.getFullYear();
 | 
				
			||||||
 | 
					                self.month = value.getMonth();
 | 
				
			||||||
 | 
					                self.day = value.getDate();
 | 
				
			||||||
 | 
					                self.hours = value.getHours();
 | 
				
			||||||
 | 
					                self.minutes = value.getMinutes();
 | 
				
			||||||
 | 
					                self.seconds = value.getSeconds();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if (self.settings.config.showDate) {
 | 
				
			||||||
 | 
					                    self.setHeaderContent();
 | 
				
			||||||
 | 
					                    self.setBodyContent();
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                if (self.settings.config.showTime) {
 | 
				
			||||||
 | 
					                    self.setFooterContent();
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            } else if (!state.visible && prevState.visible) {
 | 
				
			||||||
 | 
					                document.body.removeChild(this.el.wrapper);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    DTBox.prototype.setupWrapper = function () {
 | 
				
			||||||
 | 
					        if (!this.el.wrapper) {
 | 
				
			||||||
 | 
					            var el = document.createElement("div");
 | 
				
			||||||
 | 
					            el.classList.add("date-selector-wrapper");
 | 
				
			||||||
 | 
					            Object.defineProperty(this.el, "wrapper", { value: el });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        var self = this;
 | 
				
			||||||
 | 
					        var htmlRoot = document.getElementsByTagName('html')[0];
 | 
				
			||||||
 | 
					        function setPosition(e){
 | 
				
			||||||
 | 
					            var minTopSpace = 300;
 | 
				
			||||||
 | 
					            var box = getOffset(self.elem);
 | 
				
			||||||
 | 
					            var config = self.settings.config;
 | 
				
			||||||
 | 
					            var paddingY = config.paddingY || 5;
 | 
				
			||||||
 | 
					            var paddingX = config.paddingX || 5;
 | 
				
			||||||
 | 
					            var top = box.top + self.elem.offsetHeight + paddingY;
 | 
				
			||||||
 | 
					            var left = box.left + paddingX;
 | 
				
			||||||
 | 
					            var bottom = htmlRoot.clientHeight - box.top + paddingY;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            self.el.wrapper.style.left = `${left}px`;
 | 
				
			||||||
 | 
					            if (box.top > minTopSpace && config.direction != 'BOTTOM') {
 | 
				
			||||||
 | 
					                self.el.wrapper.style.bottom = `${bottom}px`;
 | 
				
			||||||
 | 
					                self.el.wrapper.style.top = '';
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                self.el.wrapper.style.top = `${top}px`;
 | 
				
			||||||
 | 
					                self.el.wrapper.style.bottom = ''; 
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        function handler(e) {
 | 
				
			||||||
 | 
					            self.cancelBlur += 1;
 | 
				
			||||||
 | 
					            setTimeout(function(){
 | 
				
			||||||
 | 
					                self.elem.focus();
 | 
				
			||||||
 | 
					            }, 50);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        setPosition();
 | 
				
			||||||
 | 
					        this.setPosition = setPosition;
 | 
				
			||||||
 | 
					        this.el.wrapper.addEventListener("mousedown", handler, false);
 | 
				
			||||||
 | 
					        this.el.wrapper.addEventListener("touchstart", handler, false);
 | 
				
			||||||
 | 
					        window.addEventListener('resize', this.setPosition);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    DTBox.prototype.setupHeader = function () {
 | 
				
			||||||
 | 
					        if (!this.el.header) {
 | 
				
			||||||
 | 
					            var row = document.createElement("div");
 | 
				
			||||||
 | 
					            var classes = ["cal-nav-prev", "cal-nav-current", "cal-nav-next"];
 | 
				
			||||||
 | 
					            row.classList.add("cal-header");
 | 
				
			||||||
 | 
					            for (var i = 0; i < 3; i++) {
 | 
				
			||||||
 | 
					                var cell = document.createElement("div");
 | 
				
			||||||
 | 
					                cell.classList.add("cal-nav", classes[i]);
 | 
				
			||||||
 | 
					                cell.onclick = this.onHeaderChange.bind(this);
 | 
				
			||||||
 | 
					                row.appendChild(cell);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            row.children[0].innerHTML = "<";
 | 
				
			||||||
 | 
					            row.children[2].innerHTML = ">";
 | 
				
			||||||
 | 
					            Object.defineProperty(this.el, "header", { value: row });
 | 
				
			||||||
 | 
					            tryAppendChild(row, this.el.wrapper);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        this.setHeaderContent();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    DTBox.prototype.setHeaderContent = function () {
 | 
				
			||||||
 | 
					        var content = this.year;
 | 
				
			||||||
 | 
					        if ("DAYS" == this.bodyType) {
 | 
				
			||||||
 | 
					            content = this.month_long + " " + content;
 | 
				
			||||||
 | 
					        } else if ("YEARS" == this.bodyType) {
 | 
				
			||||||
 | 
					            var start = this.year + 10 - (this.year % 10);
 | 
				
			||||||
 | 
					            content = start - 10 + "-" + (start - 1);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        this.el.header.children[1].innerText = content;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    DTBox.prototype.setupBody = function () {
 | 
				
			||||||
 | 
					        if (!this.el.body) {
 | 
				
			||||||
 | 
					            var el = document.createElement("div");
 | 
				
			||||||
 | 
					            el.classList.add("cal-body");
 | 
				
			||||||
 | 
					            Object.defineProperty(this.el, "body", { value: el });
 | 
				
			||||||
 | 
					            tryAppendChild(el, this.el.wrapper);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        var toAppend = null;
 | 
				
			||||||
 | 
					        function makeGrid(rows, cols, className, firstRowClass, clickHandler) {
 | 
				
			||||||
 | 
					            var grid = document.createElement("div");
 | 
				
			||||||
 | 
					            grid.classList.add(className);
 | 
				
			||||||
 | 
					            for (var i = 1; i < rows + 1; i++) {
 | 
				
			||||||
 | 
					                var row = document.createElement("div");
 | 
				
			||||||
 | 
					                row.classList.add("cal-row", "cal-row-" + i);
 | 
				
			||||||
 | 
					                if (i == 1 && firstRowClass) {
 | 
				
			||||||
 | 
					                    row.classList.add(firstRowClass);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                for (var j = 1; j < cols + 1; j++) {
 | 
				
			||||||
 | 
					                    var col = document.createElement("div");
 | 
				
			||||||
 | 
					                    col.classList.add("cal-cell", "cal-col-" + j);
 | 
				
			||||||
 | 
					                    col.onclick = clickHandler;
 | 
				
			||||||
 | 
					                    row.appendChild(col);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                grid.appendChild(row);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            return grid;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if ("DAYS" == this.bodyType) {
 | 
				
			||||||
 | 
					            toAppend = this.el.body.calDays;
 | 
				
			||||||
 | 
					            if (!toAppend) {
 | 
				
			||||||
 | 
					                toAppend = makeGrid(7, 7, "cal-days", "cal-day-names", this.onDateSelected.bind(this));
 | 
				
			||||||
 | 
					                for (var i = 0; i < 7; i++) {
 | 
				
			||||||
 | 
					                    var cell = toAppend.children[0].children[i];
 | 
				
			||||||
 | 
					                    cell.innerText = WEEKDAYS[i].slice(0, 2);
 | 
				
			||||||
 | 
					                    cell.onclick = null;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                this.el.body.calDays = toAppend;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        } else if ("MONTHS" == this.bodyType) {
 | 
				
			||||||
 | 
					            toAppend = this.el.body.calMonths;
 | 
				
			||||||
 | 
					            if (!toAppend) {
 | 
				
			||||||
 | 
					                toAppend = makeGrid(3, 4, "cal-months", null, this.onMonthSelected.bind(this));
 | 
				
			||||||
 | 
					                for (var i = 0; i < 3; i++) {
 | 
				
			||||||
 | 
					                    for (var j = 0; j < 4; j++) {
 | 
				
			||||||
 | 
					                        var monthShort = MONTHS[4 * i + j].slice(0, 3);
 | 
				
			||||||
 | 
					                        toAppend.children[i].children[j].innerText = monthShort;
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                this.el.body.calMonths = toAppend;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        } else if ("YEARS" == this.bodyType) {
 | 
				
			||||||
 | 
					            toAppend = this.el.body.calYears;
 | 
				
			||||||
 | 
					            if (!toAppend) {
 | 
				
			||||||
 | 
					                toAppend = makeGrid(3, 4, "cal-years", null, this.onYearSelected.bind(this));
 | 
				
			||||||
 | 
					                this.el.body.calYears = toAppend;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        empty(this.el.body);
 | 
				
			||||||
 | 
					        tryAppendChild(toAppend, this.el.body);
 | 
				
			||||||
 | 
					        this.setBodyContent();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    DTBox.prototype.setBodyContent = function () {
 | 
				
			||||||
 | 
					        var grid = this.el.body.children[0];
 | 
				
			||||||
 | 
					        var classes = ["cal-cell-prev", "cal-cell-next", "cal-value"];
 | 
				
			||||||
 | 
					        if ("DAYS" == this.bodyType) {
 | 
				
			||||||
 | 
					            var oneDayMilliSecs = 24 * 60 * 60 * 1000;
 | 
				
			||||||
 | 
					            var start = new Date(this.year, this.month, 1);
 | 
				
			||||||
 | 
					            var adjusted = new Date(start.getTime() - oneDayMilliSecs * start.getDay());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            grid.children[6].style.display = "";
 | 
				
			||||||
 | 
					            for (var i = 1; i < 7; i++) {
 | 
				
			||||||
 | 
					                for (var j = 0; j < 7; j++) {
 | 
				
			||||||
 | 
					                    var cell = grid.children[i].children[j];
 | 
				
			||||||
 | 
					                    var month = adjusted.getMonth();
 | 
				
			||||||
 | 
					                    var date = adjusted.getDate();
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                    cell.innerText = date;
 | 
				
			||||||
 | 
					                    cell.classList.remove(classes[0], classes[1], classes[2]);
 | 
				
			||||||
 | 
					                    if (month != this.month) {
 | 
				
			||||||
 | 
					                        if (i == 6 && j == 0) {
 | 
				
			||||||
 | 
					                            grid.children[6].style.display = "none";
 | 
				
			||||||
 | 
					                            break;
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                        cell.classList.add(month < this.month ? classes[0] : classes[1]);
 | 
				
			||||||
 | 
					                    } else if (isEqualDate(adjusted, this.value)){
 | 
				
			||||||
 | 
					                        cell.classList.add(classes[2]);
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    adjusted = new Date(adjusted.getTime() + oneDayMilliSecs);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        } else if ("YEARS" == this.bodyType) {
 | 
				
			||||||
 | 
					            var year = this.year - (this.year % 10) - 1;
 | 
				
			||||||
 | 
					            for (i = 0; i < 3; i++) {
 | 
				
			||||||
 | 
					                for (j = 0; j < 4; j++) {
 | 
				
			||||||
 | 
					                    grid.children[i].children[j].innerText = year;
 | 
				
			||||||
 | 
					                    year += 1;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            grid.children[0].children[0].classList.add(classes[0]);
 | 
				
			||||||
 | 
					            grid.children[2].children[3].classList.add(classes[1]);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /** @param {Event} e */
 | 
				
			||||||
 | 
					    DTBox.prototype.onTimeChange = function(e) {
 | 
				
			||||||
 | 
					        e.stopPropagation();
 | 
				
			||||||
 | 
					        if (e.type == 'mousedown') {
 | 
				
			||||||
 | 
					            this.cancelBlur += 1;
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        var el = e.target;
 | 
				
			||||||
 | 
					        this[el.name] = parseInt(el.value) || 0;
 | 
				
			||||||
 | 
					        this.setupFooter();
 | 
				
			||||||
 | 
					        if (e.type == 'change') {
 | 
				
			||||||
 | 
					            var self = this;
 | 
				
			||||||
 | 
					            setTimeout(function(){
 | 
				
			||||||
 | 
					                self.elem.focus();
 | 
				
			||||||
 | 
					            }, 50);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        this.setInputValue();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    DTBox.prototype.setupFooter = function() {
 | 
				
			||||||
 | 
					        if (!this.el.footer) {
 | 
				
			||||||
 | 
					            var footer = document.createElement("div");
 | 
				
			||||||
 | 
					            var handler = this.onTimeChange.bind(this);
 | 
				
			||||||
 | 
					            var self = this;
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            function makeRow(label, name, range, changeHandler) {
 | 
				
			||||||
 | 
					                var row = document.createElement("div");
 | 
				
			||||||
 | 
					                row.classList.add('cal-time');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                var labelCol = row.appendChild(document.createElement("div"));
 | 
				
			||||||
 | 
					                labelCol.classList.add('cal-time-label');
 | 
				
			||||||
 | 
					                labelCol.innerText = label;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                var valueCol = row.appendChild(document.createElement("div"));
 | 
				
			||||||
 | 
					                valueCol.classList.add('cal-time-value');
 | 
				
			||||||
 | 
					                valueCol.innerText = '00';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                var inputCol = row.appendChild(document.createElement("div"));
 | 
				
			||||||
 | 
					                var slider = inputCol.appendChild(document.createElement("input"));
 | 
				
			||||||
 | 
					                Object.assign(slider, {step:1, min:0, max:range, name:name, type:'range'});
 | 
				
			||||||
 | 
					                Object.defineProperty(footer, name, {value: slider});
 | 
				
			||||||
 | 
					                inputCol.classList.add('cal-time-slider');
 | 
				
			||||||
 | 
					                slider.onchange = changeHandler;
 | 
				
			||||||
 | 
					                slider.oninput = changeHandler;
 | 
				
			||||||
 | 
					                slider.onmousedown = changeHandler;
 | 
				
			||||||
 | 
					                self[name] = self[name] || parseInt(slider.value) || 0;
 | 
				
			||||||
 | 
					                footer.appendChild(row)
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            makeRow('HH:', 'hours', 23, handler);
 | 
				
			||||||
 | 
					            makeRow('MM:', 'minutes', 59, handler);
 | 
				
			||||||
 | 
					            makeRow('SS:', 'seconds', 59, handler);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            footer.classList.add("cal-footer");
 | 
				
			||||||
 | 
					            Object.defineProperty(this.el, "footer", { value: footer });
 | 
				
			||||||
 | 
					            tryAppendChild(footer, this.el.wrapper);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        this.setFooterContent();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    DTBox.prototype.setFooterContent = function() {
 | 
				
			||||||
 | 
					        if (this.el.footer) {
 | 
				
			||||||
 | 
					            var footer = this.el.footer;
 | 
				
			||||||
 | 
					            footer.hours.value = this.hours;
 | 
				
			||||||
 | 
					            footer.children[0].children[1].innerText = padded(this.hours, 2);
 | 
				
			||||||
 | 
					            footer.minutes.value = this.minutes;
 | 
				
			||||||
 | 
					            footer.children[1].children[1].innerText = padded(this.minutes, 2);
 | 
				
			||||||
 | 
					            footer.seconds.value = this.seconds;
 | 
				
			||||||
 | 
					            footer.children[2].children[1].innerText = padded(this.seconds, 2);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    DTBox.prototype.setInputValue = function() {
 | 
				
			||||||
 | 
					        var date = new Date(this.year, this.month, this.day);
 | 
				
			||||||
 | 
					        var strings = [];
 | 
				
			||||||
 | 
					        if (this.settings.config.showDate) {
 | 
				
			||||||
 | 
					            strings.push(renderDate(date, this.settings));
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if (this.settings.config.showTime) {
 | 
				
			||||||
 | 
					            var joined = new Date(date.getTime() + this.time);
 | 
				
			||||||
 | 
					            strings.push(renderTime(joined, this.settings));
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        this.elem.value = strings.join(', ');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    DTBox.prototype.onDateSelected = function (e) {
 | 
				
			||||||
 | 
					        var row = e.target.parentNode;
 | 
				
			||||||
 | 
					        var date = parseInt(e.target.innerText);
 | 
				
			||||||
 | 
					        if (!(row.nextSibling && row.nextSibling.nextSibling) && date < 8) {
 | 
				
			||||||
 | 
					            this.month += 1;
 | 
				
			||||||
 | 
					        } else if (!(row.previousSibling && row.previousSibling.previousSibling) && date > 7) {
 | 
				
			||||||
 | 
					            this.month -= 1;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        this.day = parseInt(e.target.innerText);
 | 
				
			||||||
 | 
					        this.value = new Date(this.year, this.month, this.day);
 | 
				
			||||||
 | 
					        this.setInputValue();
 | 
				
			||||||
 | 
					        this.setHeaderContent();
 | 
				
			||||||
 | 
					        this.setBodyContent();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /** @param {Event} e */
 | 
				
			||||||
 | 
					    DTBox.prototype.onMonthSelected = function (e) {
 | 
				
			||||||
 | 
					        var col = 0;
 | 
				
			||||||
 | 
					        var row = 2;
 | 
				
			||||||
 | 
					        var cell = e.target;
 | 
				
			||||||
 | 
					        if (cell.parentNode.nextSibling){
 | 
				
			||||||
 | 
					            row = cell.parentNode.previousSibling ? 1: 0;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if (cell.previousSibling) {
 | 
				
			||||||
 | 
					            col = 3;
 | 
				
			||||||
 | 
					            if (cell.nextSibling) {
 | 
				
			||||||
 | 
					                col = cell.previousSibling.previousSibling ? 2 : 1;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        this.month = 4 * row + col;
 | 
				
			||||||
 | 
					        this.bodyType = "DAYS";
 | 
				
			||||||
 | 
					        this.setHeaderContent();
 | 
				
			||||||
 | 
					        this.setupBody();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /** @param {Event} e */
 | 
				
			||||||
 | 
					    DTBox.prototype.onYearSelected = function (e) {
 | 
				
			||||||
 | 
					        this.year = parseInt(e.target.innerText);
 | 
				
			||||||
 | 
					        this.bodyType = "MONTHS";
 | 
				
			||||||
 | 
					        this.setHeaderContent();
 | 
				
			||||||
 | 
					        this.setupBody();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /** @param {Event} e */
 | 
				
			||||||
 | 
					    DTBox.prototype.onHeaderChange = function (e) {
 | 
				
			||||||
 | 
					        var cell = e.target;
 | 
				
			||||||
 | 
					        if (cell.previousSibling && cell.nextSibling) {
 | 
				
			||||||
 | 
					            var idx = BODYTYPES.indexOf(this.bodyType);
 | 
				
			||||||
 | 
					            if (idx < 0 || !BODYTYPES[idx + 1]) {
 | 
				
			||||||
 | 
					                return;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            this.bodyType = BODYTYPES[idx + 1];
 | 
				
			||||||
 | 
					            this.setupBody();
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            var sign = cell.previousSibling ? 1 : -1;
 | 
				
			||||||
 | 
					            switch (this.bodyType) {
 | 
				
			||||||
 | 
					                case "DAYS":
 | 
				
			||||||
 | 
					                    this.month += sign * 1;
 | 
				
			||||||
 | 
					                    break;
 | 
				
			||||||
 | 
					                case "MONTHS":
 | 
				
			||||||
 | 
					                    this.year += sign * 1;
 | 
				
			||||||
 | 
					                    break;
 | 
				
			||||||
 | 
					                case "YEARS":
 | 
				
			||||||
 | 
					                    this.year += sign * 10;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            if (this.month > 11 || this.month < 0) {
 | 
				
			||||||
 | 
					                this.year += Math.floor(this.month / 11);
 | 
				
			||||||
 | 
					                this.month = this.month > 11 ? 0 : 11;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        this.setHeaderContent();
 | 
				
			||||||
 | 
					        this.setBodyContent();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @param {HTMLElement} elem 
 | 
				
			||||||
 | 
					     * @returns {{left:number, top:number}}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    function getOffset(elem) {
 | 
				
			||||||
 | 
					        var box = elem.getBoundingClientRect();
 | 
				
			||||||
 | 
					        var left = window.pageXOffset !== undefined ? window.pageXOffset : 
 | 
				
			||||||
 | 
					            (document.documentElement || document.body.parentNode || document.body).scrollLeft;
 | 
				
			||||||
 | 
					        var top = window.pageYOffset !== undefined ? window.pageYOffset : 
 | 
				
			||||||
 | 
					            (document.documentElement || document.body.parentNode || document.body).scrollTop;
 | 
				
			||||||
 | 
					        return { left: box.left + left, top: box.top + top };
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    function empty(e) {
 | 
				
			||||||
 | 
					        for (; e.children.length; ) e.removeChild(e.children[0]);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    function tryAppendChild(newChild, refNode) {
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            refNode.appendChild(newChild);
 | 
				
			||||||
 | 
					            return newChild;
 | 
				
			||||||
 | 
					        } catch (e) {
 | 
				
			||||||
 | 
					            console.trace(e);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /** @class */
 | 
				
			||||||
 | 
					    function hookFuncs() {
 | 
				
			||||||
 | 
					        /** @type {Handlers} */
 | 
				
			||||||
 | 
					        this._funcs = {};
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @param {string} key 
 | 
				
			||||||
 | 
					     * @param {Function} func 
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    hookFuncs.prototype.add = function(key, func){
 | 
				
			||||||
 | 
					        if (!this._funcs[key]){
 | 
				
			||||||
 | 
					            this._funcs[key] = [];
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        this._funcs[key].push(func)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @param {String} key 
 | 
				
			||||||
 | 
					     * @returns {Function[]} handlers
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    hookFuncs.prototype.get = function(key){
 | 
				
			||||||
 | 
					        return this._funcs[key] ? this._funcs[key] : [];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @param {Array.<string>} arr 
 | 
				
			||||||
 | 
					     * @param {String} string 
 | 
				
			||||||
 | 
					     * @returns {Array.<string>} sorted string
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    function sortByStringIndex(arr, string) {
 | 
				
			||||||
 | 
					        return arr.sort(function(a, b){
 | 
				
			||||||
 | 
					            var h = string.indexOf(a);
 | 
				
			||||||
 | 
					            var l = string.indexOf(b);
 | 
				
			||||||
 | 
					            var rank = 0;
 | 
				
			||||||
 | 
					            if (h < l) {
 | 
				
			||||||
 | 
					                rank = -1;
 | 
				
			||||||
 | 
					            } else if (l < h) {
 | 
				
			||||||
 | 
					                rank = 1;
 | 
				
			||||||
 | 
					            } else if (a.length > b.length) {
 | 
				
			||||||
 | 
					                rank = -1;
 | 
				
			||||||
 | 
					            } else if (b.length > a.length) {
 | 
				
			||||||
 | 
					                rank = 1;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            return rank;
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Remove keys from array that are not in format
 | 
				
			||||||
 | 
					     * @param {string[]} keys 
 | 
				
			||||||
 | 
					     * @param {string} format 
 | 
				
			||||||
 | 
					     * @returns {string[]} new filtered array
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    function filterFormatKeys(keys, format) {
 | 
				
			||||||
 | 
					        var out = [];
 | 
				
			||||||
 | 
					        var formatIdx = 0;
 | 
				
			||||||
 | 
					        for (var i = 0; i<keys.length; i++) {
 | 
				
			||||||
 | 
					            var key = keys[i];
 | 
				
			||||||
 | 
					            if (format.slice(formatIdx).indexOf(key) > -1) {
 | 
				
			||||||
 | 
					                formatIdx += key.length;
 | 
				
			||||||
 | 
					                out.push(key);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return out;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @template {StringNumObj} FormatObj
 | 
				
			||||||
 | 
					     * @param {string} value 
 | 
				
			||||||
 | 
					     * @param {string} format 
 | 
				
			||||||
 | 
					     * @param {FormatObj} formatObj 
 | 
				
			||||||
 | 
					     * @param {function(Object.<string, hookFuncs>): null} setHooks 
 | 
				
			||||||
 | 
					     * @returns {FormatObj} formatObj
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    function parseData(value, format, formatObj, setHooks) {
 | 
				
			||||||
 | 
					        var hooks = {
 | 
				
			||||||
 | 
					            canSkip: new hookFuncs(),
 | 
				
			||||||
 | 
					            updateValue: new hookFuncs(),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        var keys = sortByStringIndex(Object.keys(formatObj), format);
 | 
				
			||||||
 | 
					        var filterdKeys = filterFormatKeys(keys, format);
 | 
				
			||||||
 | 
					        var vstart = 0; // value start
 | 
				
			||||||
 | 
					        if (setHooks) {
 | 
				
			||||||
 | 
					            setHooks(hooks);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        for (var i = 0; i < keys.length; i++) {
 | 
				
			||||||
 | 
					            var key = keys[i];
 | 
				
			||||||
 | 
					            var fstart = format.indexOf(key);
 | 
				
			||||||
 | 
					            var _vstart = vstart; // next value start
 | 
				
			||||||
 | 
					            var val = null;
 | 
				
			||||||
 | 
					            var canSkip = false;
 | 
				
			||||||
 | 
					            var funcs = hooks.canSkip.get(key);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            vstart = vstart || fstart;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            for (var j = 0; j < funcs.length; j++) {
 | 
				
			||||||
 | 
					                if (funcs[j](formatObj)){
 | 
				
			||||||
 | 
					                    canSkip = true;
 | 
				
			||||||
 | 
					                    break;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            if (fstart > -1 && !canSkip) {
 | 
				
			||||||
 | 
					                var sep = null;
 | 
				
			||||||
 | 
					                var stop = vstart + key.length;
 | 
				
			||||||
 | 
					                var fnext = -1;
 | 
				
			||||||
 | 
					                var nextKeyIdx = i + 1;
 | 
				
			||||||
 | 
					                _vstart += key.length; // set next value start if current key is found
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                // get next format token used to determine separator
 | 
				
			||||||
 | 
					                while (fnext == -1 && nextKeyIdx < keys.length){
 | 
				
			||||||
 | 
					                    var nextKey = keys[nextKeyIdx];
 | 
				
			||||||
 | 
					                    nextKeyIdx += 1;
 | 
				
			||||||
 | 
					                    if (filterdKeys.indexOf(nextKey) === -1) {
 | 
				
			||||||
 | 
					                        continue;
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    fnext = nextKey ? format.indexOf(nextKey) : -1; // next format start
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                if (fnext > -1){
 | 
				
			||||||
 | 
					                    sep = format.slice(stop, fnext);
 | 
				
			||||||
 | 
					                    if (sep) {
 | 
				
			||||||
 | 
					                        var _stop = value.slice(vstart).indexOf(sep);
 | 
				
			||||||
 | 
					                        if (_stop && _stop > -1){
 | 
				
			||||||
 | 
					                            stop = _stop + vstart;
 | 
				
			||||||
 | 
					                            _vstart = stop + sep.length;
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                val = parseInt(value.slice(vstart, stop));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                var funcs = hooks.updateValue.get(key);
 | 
				
			||||||
 | 
					                for (var k = 0; k < funcs.length; k++) {
 | 
				
			||||||
 | 
					                    val = funcs[k](val, formatObj, vstart, stop);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            formatObj[key] = { index: vstart, value: val };
 | 
				
			||||||
 | 
					            vstart = _vstart; // set next value start
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return formatObj;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @param {String} value 
 | 
				
			||||||
 | 
					     * @param {DTS} settings 
 | 
				
			||||||
 | 
					     * @returns {Date} date object
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    function parseDate(value, settings) {
 | 
				
			||||||
 | 
					        /** @type {{yyyy:number=, yy:number=, mm:number=, dd:number=}} */
 | 
				
			||||||
 | 
					        var formatObj = {yyyy:null, yy:null, mm:null, dd:null};
 | 
				
			||||||
 | 
					        var format = ((settings.dateFormat) || '').toLowerCase();
 | 
				
			||||||
 | 
					        if (!format) {
 | 
				
			||||||
 | 
					            throw new TypeError('dateFormat not found (' + settings.dateFormat + ')');
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        var formatObj = parseData(value, format, formatObj, function(hooks){
 | 
				
			||||||
 | 
					            hooks.canSkip.add("yy", function(data){
 | 
				
			||||||
 | 
					                return data["yyyy"].value;
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					            hooks.updateValue.add("yy", function(val){
 | 
				
			||||||
 | 
					                return 100 * Math.floor(new Date().getFullYear() / 100) + val;
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        var year = formatObj["yyyy"].value || formatObj["yy"].value;
 | 
				
			||||||
 | 
					        var month = formatObj["mm"].value - 1;
 | 
				
			||||||
 | 
					        var date = formatObj["dd"].value;
 | 
				
			||||||
 | 
					        var result = new Date(year, month, date);
 | 
				
			||||||
 | 
					        return result;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @param {String} value 
 | 
				
			||||||
 | 
					     * @param {DTS} settings 
 | 
				
			||||||
 | 
					     * @returns {Number} time in milliseconds <= (24 * 60 * 60 * 1000) - 1
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    function parseTime(value, settings) {
 | 
				
			||||||
 | 
					        var format = ((settings.timeFormat) || '').toLowerCase();
 | 
				
			||||||
 | 
					        if (!format) {
 | 
				
			||||||
 | 
					            throw new TypeError('timeFormat not found (' + settings.timeFormat + ')');
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /** @type {{hh:number=, mm:number=, ss:number=, a:string=}} */
 | 
				
			||||||
 | 
					        var formatObj = {hh:null, mm:null, ss:null, a:null};
 | 
				
			||||||
 | 
					        var formatObj = parseData(value, format, formatObj, function(hooks){
 | 
				
			||||||
 | 
					            hooks.updateValue.add("a", function(val, data, start, stop){
 | 
				
			||||||
 | 
					                return value.slice(start, start + 2);
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        var hours = formatObj["hh"].value;
 | 
				
			||||||
 | 
					        var minutes = formatObj["mm"].value;
 | 
				
			||||||
 | 
					        var seconds = formatObj["ss"].value;
 | 
				
			||||||
 | 
					        var am_pm = formatObj["a"].value;
 | 
				
			||||||
 | 
					        var am_pm_lower = am_pm ? am_pm.toLowerCase() : am_pm;
 | 
				
			||||||
 | 
					        if (am_pm && ["am", "pm"].indexOf(am_pm_lower) > -1){
 | 
				
			||||||
 | 
					            if (am_pm_lower == 'am' && hours == 12){
 | 
				
			||||||
 | 
					                hours = 0;
 | 
				
			||||||
 | 
					            } else if (am_pm_lower == 'pm') {
 | 
				
			||||||
 | 
					                hours += 12;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        var time = hours * 60 * 60 * 1000 + minutes * 60 * 1000 + seconds * 1000;
 | 
				
			||||||
 | 
					        return time;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @param {Date} value 
 | 
				
			||||||
 | 
					     * @param {DTS} settings 
 | 
				
			||||||
 | 
					     * @returns {String} date string
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    function renderDate(value, settings) {
 | 
				
			||||||
 | 
					        var format = settings.dateFormat.toLowerCase();
 | 
				
			||||||
 | 
					        var date = value.getDate();
 | 
				
			||||||
 | 
					        var month = value.getMonth() + 1;
 | 
				
			||||||
 | 
					        var year = value.getFullYear();
 | 
				
			||||||
 | 
					        var yearShort = year % 100;
 | 
				
			||||||
 | 
					        var formatObj = {
 | 
				
			||||||
 | 
					            dd: date < 10 ? "0" + date : date,
 | 
				
			||||||
 | 
					            mm: month < 10 ? "0" + month : month,
 | 
				
			||||||
 | 
					            yyyy: year,
 | 
				
			||||||
 | 
					            yy: yearShort < 10 ? "0" + yearShort : yearShort
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        var str = format.replace(settings.dateFormatRegEx, function (found) {
 | 
				
			||||||
 | 
					            return formatObj[found];
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        return str;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @param {Date} value 
 | 
				
			||||||
 | 
					     * @param {DTS} settings 
 | 
				
			||||||
 | 
					     * @returns {String} date string
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    function renderTime(value, settings) {
 | 
				
			||||||
 | 
					        var Format = settings.timeFormat;
 | 
				
			||||||
 | 
					        var format = Format.toLowerCase();
 | 
				
			||||||
 | 
					        var hours = value.getHours();
 | 
				
			||||||
 | 
					        var minutes = value.getMinutes();
 | 
				
			||||||
 | 
					        var seconds = value.getSeconds();
 | 
				
			||||||
 | 
					        var am_pm = null;
 | 
				
			||||||
 | 
					        var hh_am_pm = null;
 | 
				
			||||||
 | 
					        if (format.indexOf('a') > -1) {
 | 
				
			||||||
 | 
					            am_pm = hours >= 12 ? 'pm' : 'am';
 | 
				
			||||||
 | 
					            am_pm = Format.indexOf('A') > -1 ? am_pm.toUpperCase() : am_pm;
 | 
				
			||||||
 | 
					            hh_am_pm = hours == 0 ? '12' : (hours > 12 ? hours%12 : hours);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        var formatObj = {
 | 
				
			||||||
 | 
					            hh: am_pm ? hh_am_pm : (hours < 10 ? "0" + hours : hours),
 | 
				
			||||||
 | 
					            mm: minutes < 10 ? "0" + minutes : minutes,
 | 
				
			||||||
 | 
					            ss: seconds < 10 ? "0" + seconds : seconds,
 | 
				
			||||||
 | 
					            a: am_pm,
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        var str = format.replace(settings.timeFormatRegEx, function (found) {
 | 
				
			||||||
 | 
					            return formatObj[found];
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        return str;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * checks if two dates are equal
 | 
				
			||||||
 | 
					     * @param {Date} date1 
 | 
				
			||||||
 | 
					     * @param {Date} date2 
 | 
				
			||||||
 | 
					     * @returns {Boolean} true or false
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    function isEqualDate(date1, date2) {
 | 
				
			||||||
 | 
					        if (!(date1 && date2)) return false;
 | 
				
			||||||
 | 
					        return (date1.getFullYear() == date2.getFullYear() && 
 | 
				
			||||||
 | 
					                date1.getMonth() == date2.getMonth() && 
 | 
				
			||||||
 | 
					                date1.getDate() == date2.getDate());
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @param {Number} val 
 | 
				
			||||||
 | 
					     * @param {Number} pad 
 | 
				
			||||||
 | 
					     * @param {*} default_val 
 | 
				
			||||||
 | 
					     * @returns {String} padded string
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    function padded(val, pad, default_val) {
 | 
				
			||||||
 | 
					        var default_val = default_val || 0;
 | 
				
			||||||
 | 
					        var valStr = '' + (parseInt(val) || default_val);
 | 
				
			||||||
 | 
					        var diff = Math.max(pad, valStr.length) - valStr.length;
 | 
				
			||||||
 | 
					        return ('' + default_val).repeat(diff) + valStr;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @template X
 | 
				
			||||||
 | 
					     * @template Y
 | 
				
			||||||
 | 
					     * @param {X} obj 
 | 
				
			||||||
 | 
					     * @param {Y} objDefaults 
 | 
				
			||||||
 | 
					     * @returns {X|Y} merged object
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    function setDefaults(obj, objDefaults) {
 | 
				
			||||||
 | 
					        var keys = Object.keys(objDefaults);
 | 
				
			||||||
 | 
					        for (var i=0; i<keys.length; i++) {
 | 
				
			||||||
 | 
					            var key = keys[i];
 | 
				
			||||||
 | 
					            if (!Object.prototype.hasOwnProperty.call(obj, key)) {
 | 
				
			||||||
 | 
					                obj[key] = objDefaults[key];
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return obj;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    window.dtsel = Object.create({},{
 | 
				
			||||||
 | 
					        DTS: { value: DTS },
 | 
				
			||||||
 | 
					        DTObj: { value: DTBox },
 | 
				
			||||||
 | 
					        fn: {
 | 
				
			||||||
 | 
					            value: Object.defineProperties({}, {
 | 
				
			||||||
 | 
					                empty: { value: empty },
 | 
				
			||||||
 | 
					                appendAfter: {
 | 
				
			||||||
 | 
					                    value: function (newElem, refNode) {
 | 
				
			||||||
 | 
					                        refNode.parentNode.insertBefore(newElem, refNode.nextSibling);
 | 
				
			||||||
 | 
					                    },
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					                getOffset: { value: getOffset },
 | 
				
			||||||
 | 
					                parseDate: { value: parseDate },
 | 
				
			||||||
 | 
					                renderDate: { value: renderDate },
 | 
				
			||||||
 | 
					                parseTime: {value: parseTime},
 | 
				
			||||||
 | 
					                renderTime: {value: renderTime},
 | 
				
			||||||
 | 
					                setDefaults: {value: setDefaults},
 | 
				
			||||||
 | 
					            }),
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					})();
 | 
				
			||||||
							
								
								
									
										23
									
								
								web/static/js/expand.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,23 @@
 | 
				
			|||||||
 | 
					function collapse_all() {
 | 
				
			||||||
 | 
					    document.querySelectorAll("div.reminderContent:not(.creator)").forEach((el) => {
 | 
				
			||||||
 | 
					        el.classList.add("is-collapsed");
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function expand_all() {
 | 
				
			||||||
 | 
					    document.querySelectorAll("div.reminderContent:not(.creator)").forEach((el) => {
 | 
				
			||||||
 | 
					        el.classList.remove("is-collapsed");
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const expandAll = document.querySelector("#expandAll");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					expandAll.addEventListener("change", (ev) => {
 | 
				
			||||||
 | 
					    if (ev.target.value === "expand") {
 | 
				
			||||||
 | 
					        expand_all();
 | 
				
			||||||
 | 
					    } else if (ev.target.value === "collapse") {
 | 
				
			||||||
 | 
					        collapse_all();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    ev.target.value = "";
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
							
								
								
									
										88
									
								
								web/static/js/interval.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,88 @@
 | 
				
			|||||||
 | 
					function get_interval(element) {
 | 
				
			||||||
 | 
					    let months = element.querySelector('input[name="interval_months"]').value;
 | 
				
			||||||
 | 
					    let days = element.querySelector('input[name="interval_days"]').value;
 | 
				
			||||||
 | 
					    let hours = element.querySelector('input[name="interval_hours"]').value;
 | 
				
			||||||
 | 
					    let minutes = element.querySelector('input[name="interval_minutes"]').value;
 | 
				
			||||||
 | 
					    let seconds = element.querySelector('input[name="interval_seconds"]').value;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					        months: parseInt(months) || null,
 | 
				
			||||||
 | 
					        seconds:
 | 
				
			||||||
 | 
					            (parseInt(days) || 0) * 86400 +
 | 
				
			||||||
 | 
					                (parseInt(hours) || 0) * 3600 +
 | 
				
			||||||
 | 
					                (parseInt(minutes) || 0) * 60 +
 | 
				
			||||||
 | 
					                (parseInt(seconds) || 0) || null,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function update_interval(element) {
 | 
				
			||||||
 | 
					    let months = element.querySelector('input[name="interval_months"]');
 | 
				
			||||||
 | 
					    let days = element.querySelector('input[name="interval_days"]');
 | 
				
			||||||
 | 
					    let hours = element.querySelector('input[name="interval_hours"]');
 | 
				
			||||||
 | 
					    let minutes = element.querySelector('input[name="interval_minutes"]');
 | 
				
			||||||
 | 
					    let seconds = element.querySelector('input[name="interval_seconds"]');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    months.value = months.value.padStart(1, "0");
 | 
				
			||||||
 | 
					    days.value = days.value.padStart(1, "0");
 | 
				
			||||||
 | 
					    hours.value = hours.value.padStart(2, "0");
 | 
				
			||||||
 | 
					    minutes.value = minutes.value.padStart(2, "0");
 | 
				
			||||||
 | 
					    seconds.value = seconds.value.padStart(2, "0");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (seconds.value >= 60) {
 | 
				
			||||||
 | 
					        let quotient = Math.floor(seconds.value / 60);
 | 
				
			||||||
 | 
					        let remainder = seconds.value % 60;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        seconds.value = String(remainder).padStart(2, "0");
 | 
				
			||||||
 | 
					        minutes.value = String(Number(minutes.value) + Number(quotient)).padStart(2, "0");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (minutes.value >= 60) {
 | 
				
			||||||
 | 
					        let quotient = Math.floor(minutes.value / 60);
 | 
				
			||||||
 | 
					        let remainder = minutes.value % 60;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        minutes.value = String(remainder).padStart(2, "0");
 | 
				
			||||||
 | 
					        hours.value = String(Number(hours.value) + Number(quotient)).padStart(2, "0");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (hours.value >= 24) {
 | 
				
			||||||
 | 
					        let quotient = Math.floor(hours.value / 24);
 | 
				
			||||||
 | 
					        let remainder = hours.value % 24;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        hours.value = String(remainder).padStart(2, "0");
 | 
				
			||||||
 | 
					        days.value = Number(days.value) + Number(quotient);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const $intervalGroup = document.querySelector(".interval-group");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					document.querySelector(".interval-group").addEventListener(
 | 
				
			||||||
 | 
					    "blur",
 | 
				
			||||||
 | 
					    (ev) => {
 | 
				
			||||||
 | 
					        if (ev.target.nodeName !== "BUTTON") update_interval($intervalGroup);
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    true
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					$intervalGroup.querySelector("button.clear").addEventListener("click", () => {
 | 
				
			||||||
 | 
					    $intervalGroup.querySelectorAll("input").forEach((el) => {
 | 
				
			||||||
 | 
					        el.value = "";
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					document.addEventListener("remindersLoaded", (event) => {
 | 
				
			||||||
 | 
					    for (reminder of event.detail) {
 | 
				
			||||||
 | 
					        let $intervalGroup = reminder.node.querySelector(".interval-group");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $intervalGroup.addEventListener(
 | 
				
			||||||
 | 
					            "blur",
 | 
				
			||||||
 | 
					            (ev) => {
 | 
				
			||||||
 | 
					                if (ev.target.nodeName !== "BUTTON") update_interval($intervalGroup);
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            true
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $intervalGroup.querySelector("button.clear").addEventListener("click", () => {
 | 
				
			||||||
 | 
					            $intervalGroup.querySelectorAll("input").forEach((el) => {
 | 
				
			||||||
 | 
					                el.value = "";
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
							
								
								
									
										7
									
								
								web/static/js/iro.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										2
									
								
								web/static/js/js.cookie.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,2 @@
 | 
				
			|||||||
 | 
					/*! js-cookie v3.0.0-rc.0 | MIT */
 | 
				
			||||||
 | 
					!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e=e||self,function(){var r=e.Cookies,n=e.Cookies=t();n.noConflict=function(){return e.Cookies=r,n}}())}(this,function(){"use strict";function e(e){for(var t=1;t<arguments.length;t++){var r=arguments[t];for(var n in r)e[n]=r[n]}return e}var t={read:function(e){return e.replace(/%3B/g,";")},write:function(e){return e.replace(/;/g,"%3B")}};return function r(n,i){function o(r,o,u){if("undefined"!=typeof document){"number"==typeof(u=e({},i,u)).expires&&(u.expires=new Date(Date.now()+864e5*u.expires)),u.expires&&(u.expires=u.expires.toUTCString()),r=t.write(r).replace(/=/g,"%3D"),o=n.write(String(o),r);var c="";for(var f in u)u[f]&&(c+="; "+f,!0!==u[f]&&(c+="="+u[f].split(";")[0]));return document.cookie=r+"="+o+c}}return Object.create({set:o,get:function(e){if("undefined"!=typeof document&&(!arguments.length||e)){for(var r=document.cookie?document.cookie.split("; "):[],i={},o=0;o<r.length;o++){var u=r[o].split("="),c=u.slice(1).join("="),f=t.read(u[0]).replace(/%3D/g,"=");if(i[f]=n.read(c,f),e===f)break}return e?i[e]:i}},remove:function(t,r){o(t,"",e({},r,{expires:-1}))},withAttributes:function(t){return r(this.converter,e({},this.attributes,t))},withConverter:function(t){return r(e({},this.converter,t),this.attributes)}},{attributes:{value:Object.freeze(i)},converter:{value:Object.freeze(n)}})}(t,{path:"/"})});
 | 
				
			||||||
							
								
								
									
										1
									
								
								web/static/js/luxon.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										913
									
								
								web/static/js/main.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,913 @@
 | 
				
			|||||||
 | 
					let colorPicker = new iro.ColorPicker("#colorpicker");
 | 
				
			||||||
 | 
					let $discordFrame;
 | 
				
			||||||
 | 
					const $loader = document.querySelector("#loader");
 | 
				
			||||||
 | 
					const $colorPickerModal = document.querySelector("div#pickColorModal");
 | 
				
			||||||
 | 
					const $colorPickerInput = $colorPickerModal.querySelector("input");
 | 
				
			||||||
 | 
					const $deleteReminderBtn = document.querySelector("#delete-reminder-confirm");
 | 
				
			||||||
 | 
					const $reminderTemplate = document.querySelector("template#guildReminder");
 | 
				
			||||||
 | 
					const $embedFieldTemplate = document.querySelector("template#embedFieldTemplate");
 | 
				
			||||||
 | 
					const $createReminder = document.querySelector("#reminderCreator");
 | 
				
			||||||
 | 
					const $createReminderBtn = $createReminder.querySelector("button#createReminder");
 | 
				
			||||||
 | 
					const $createTemplateBtn = $createReminder.querySelector("button#createTemplate");
 | 
				
			||||||
 | 
					const $loadTemplateBtn = document.querySelector("button#load-template");
 | 
				
			||||||
 | 
					const $deleteTemplateBtn = document.querySelector("button#delete-template");
 | 
				
			||||||
 | 
					const $templateSelect = document.querySelector("select#templateSelect");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					let channels = [];
 | 
				
			||||||
 | 
					let roles = [];
 | 
				
			||||||
 | 
					let templates = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					let globalPatreon = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function guildId() {
 | 
				
			||||||
 | 
					    return document.querySelector(".guildList a.is-active").dataset["guild"];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function colorToInt(r, g, b) {
 | 
				
			||||||
 | 
					    return (r << 16) + (g << 8) + b;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function intToColor(i) {
 | 
				
			||||||
 | 
					    return `#${i.toString(16).padStart(6, "0")}`;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function resize_textareas() {
 | 
				
			||||||
 | 
					    document.querySelectorAll("textarea.autoresize").forEach((element) => {
 | 
				
			||||||
 | 
					        element.style.height = "";
 | 
				
			||||||
 | 
					        element.style.height = element.scrollHeight + 3 + "px";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        element.addEventListener("input", () => {
 | 
				
			||||||
 | 
					            element.style.height = "";
 | 
				
			||||||
 | 
					            element.style.height = element.scrollHeight + 3 + "px";
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function switch_pane(selector) {
 | 
				
			||||||
 | 
					    document.querySelectorAll("aside a").forEach((el) => {
 | 
				
			||||||
 | 
					        el.classList.remove("is-active");
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    document.querySelectorAll("div.is-main-content > section").forEach((el) => {
 | 
				
			||||||
 | 
					        el.classList.add("is-hidden");
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    document.getElementById(selector).classList.remove("is-hidden");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    resize_textareas();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function update_select(sel) {
 | 
				
			||||||
 | 
					    if (sel.selectedOptions[0].dataset["webhookAvatar"]) {
 | 
				
			||||||
 | 
					        sel.closest("div.reminderContent").querySelector("img.discord-avatar").src =
 | 
				
			||||||
 | 
					            sel.selectedOptions[0].dataset["webhookAvatar"];
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        sel.closest("div.reminderContent").querySelector("img.discord-avatar").src = "";
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (sel.selectedOptions[0].dataset["webhookName"]) {
 | 
				
			||||||
 | 
					        sel.closest("div.reminderContent").querySelector("input.discord-username").value =
 | 
				
			||||||
 | 
					            sel.selectedOptions[0].dataset["webhookName"];
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        sel.closest("div.reminderContent").querySelector("input.discord-username").value =
 | 
				
			||||||
 | 
					            "";
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function reset_guild_pane() {
 | 
				
			||||||
 | 
					    document
 | 
				
			||||||
 | 
					        .querySelectorAll("select.channel-selector option")
 | 
				
			||||||
 | 
					        .forEach((opt) => opt.remove());
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function fetch_roles(guild_id) {
 | 
				
			||||||
 | 
					    fetch(`/dashboard/api/guild/${guild_id}/roles`)
 | 
				
			||||||
 | 
					        .then((response) => response.json())
 | 
				
			||||||
 | 
					        .then((data) => {
 | 
				
			||||||
 | 
					            if (data.error) {
 | 
				
			||||||
 | 
					                show_error(data.error);
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                roles = data;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function fetch_templates(guild_id) {
 | 
				
			||||||
 | 
					    fetch(`/dashboard/api/guild/${guild_id}/templates`)
 | 
				
			||||||
 | 
					        .then((response) => response.json())
 | 
				
			||||||
 | 
					        .then((data) => {
 | 
				
			||||||
 | 
					            if (data.error) {
 | 
				
			||||||
 | 
					                show_error(data.error);
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                templates = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                const select = document.querySelector("#templateSelect");
 | 
				
			||||||
 | 
					                select.innerHTML = "";
 | 
				
			||||||
 | 
					                for (let template of data) {
 | 
				
			||||||
 | 
					                    templates[template["id"]] = template;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    let option = document.createElement("option");
 | 
				
			||||||
 | 
					                    option.value = template["id"];
 | 
				
			||||||
 | 
					                    option.textContent = template["name"];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    select.appendChild(option);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function fetch_channels(guild_id) {
 | 
				
			||||||
 | 
					    const event = new Event("channelsLoading");
 | 
				
			||||||
 | 
					    document.dispatchEvent(event);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await fetch(`/dashboard/api/guild/${guild_id}/channels`)
 | 
				
			||||||
 | 
					        .then((response) => response.json())
 | 
				
			||||||
 | 
					        .then((data) => {
 | 
				
			||||||
 | 
					            if (data.error) {
 | 
				
			||||||
 | 
					                if (data.error === "Bot not in guild") {
 | 
				
			||||||
 | 
					                    switch_pane("guild-error");
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    show_error(data.error);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                channels = data;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        .then(() => {
 | 
				
			||||||
 | 
					            const event = new Event("channelsLoaded");
 | 
				
			||||||
 | 
					            document.dispatchEvent(event);
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function fetch_reminders(guild_id) {
 | 
				
			||||||
 | 
					    document.dispatchEvent(new Event("remindersLoading"));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const $reminderBox = document.querySelector("div#guildReminders");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // reset div contents
 | 
				
			||||||
 | 
					    $reminderBox.innerHTML = "";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // fetch reminders
 | 
				
			||||||
 | 
					    await fetch(`/dashboard/api/guild/${guild_id}/reminders`)
 | 
				
			||||||
 | 
					        .then((response) => response.json())
 | 
				
			||||||
 | 
					        .then((data) => {
 | 
				
			||||||
 | 
					            if (data.error) {
 | 
				
			||||||
 | 
					                show_error(data.error);
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                for (let reminder of data) {
 | 
				
			||||||
 | 
					                    let newFrame = $reminderTemplate.content.cloneNode(true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    newFrame.querySelector(".reminderContent").dataset["uid"] =
 | 
				
			||||||
 | 
					                        reminder["uid"];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    deserialize_reminder(reminder, newFrame, "load");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    $reminderBox.appendChild(newFrame);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    reminder.node = $reminderBox.lastElementChild;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                const remindersLoadedEvent = new CustomEvent("remindersLoaded", {
 | 
				
			||||||
 | 
					                    detail: data,
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                document.dispatchEvent(remindersLoadedEvent);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function serialize_reminder(node, mode) {
 | 
				
			||||||
 | 
					    let interval, utc_time, expiration_time;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (mode !== "template") {
 | 
				
			||||||
 | 
					        interval = get_interval(node);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        utc_time = luxon.DateTime.fromISO(
 | 
				
			||||||
 | 
					            node.querySelector('input[name="time"]').value
 | 
				
			||||||
 | 
					        ).setZone("UTC");
 | 
				
			||||||
 | 
					        if (utc_time.invalid) {
 | 
				
			||||||
 | 
					            return { error: "Time provided invalid." };
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            utc_time = utc_time.toFormat("yyyy-LL-dd'T'HH:mm:ss");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        expiration_time = luxon.DateTime.fromISO(
 | 
				
			||||||
 | 
					            node.querySelector('input[name="time"]').value
 | 
				
			||||||
 | 
					        ).setZone("UTC");
 | 
				
			||||||
 | 
					        if (expiration_time.invalid) {
 | 
				
			||||||
 | 
					            return { error: "Expiration provided invalid." };
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            expiration_time = expiration_time.toFormat("yyyy-LL-dd'T'HH:mm:ss");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let rgb_color = window.getComputedStyle(
 | 
				
			||||||
 | 
					        node.querySelector("div.discord-embed")
 | 
				
			||||||
 | 
					    ).borderLeftColor;
 | 
				
			||||||
 | 
					    let rgb = rgb_color.match(/\d+/g);
 | 
				
			||||||
 | 
					    let color = colorToInt(parseInt(rgb[0]), parseInt(rgb[1]), parseInt(rgb[2]));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let fields = [
 | 
				
			||||||
 | 
					        ...node.querySelectorAll("div.embed-multifield-box div.embed-field-box"),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					        .map((el) => {
 | 
				
			||||||
 | 
					            return {
 | 
				
			||||||
 | 
					                title: el.querySelector("textarea.discord-field-title").value,
 | 
				
			||||||
 | 
					                value: el.querySelector("textarea.discord-field-value").value,
 | 
				
			||||||
 | 
					                inline: el.dataset["inlined"] === "1",
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        .filter(({ title, value, inline }) => title.length + value.length > 0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let attachment = null;
 | 
				
			||||||
 | 
					    let attachment_name = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (node.querySelector('input[name="attachment"]').files.length > 0) {
 | 
				
			||||||
 | 
					        let file = node.querySelector('input[name="attachment"]').files[0];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (file.size >= 8 * 1024 * 1024) {
 | 
				
			||||||
 | 
					            return { error: "File too large." };
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        attachment = await new Promise((resolve) => {
 | 
				
			||||||
 | 
					            let fileReader = new FileReader();
 | 
				
			||||||
 | 
					            fileReader.onload = (e) => resolve(fileReader.result);
 | 
				
			||||||
 | 
					            fileReader.readAsDataURL(file);
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        attachment = attachment.split(",")[1];
 | 
				
			||||||
 | 
					        attachment_name = file.name;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let uid = "";
 | 
				
			||||||
 | 
					    if (mode === "edit") {
 | 
				
			||||||
 | 
					        uid = node.closest(".reminderContent").dataset["uid"];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let enabled = null;
 | 
				
			||||||
 | 
					    if (mode === "create") {
 | 
				
			||||||
 | 
					        enabled = true;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const content = node.querySelector('textarea[name="content"]').value;
 | 
				
			||||||
 | 
					    const embed_author_url = has_source(node.querySelector("img.embed_author_url").src);
 | 
				
			||||||
 | 
					    const embed_author = node.querySelector('textarea[name="embed_author"]').value;
 | 
				
			||||||
 | 
					    const embed_description = node.querySelector(
 | 
				
			||||||
 | 
					        'textarea[name="embed_description"]'
 | 
				
			||||||
 | 
					    ).value;
 | 
				
			||||||
 | 
					    const embed_footer = node.querySelector('textarea[name="embed_footer"]').value;
 | 
				
			||||||
 | 
					    const embed_footer_url = has_source(node.querySelector("img.embed_footer_url").src);
 | 
				
			||||||
 | 
					    const embed_image_url = has_source(node.querySelector("img.embed_image_url").src);
 | 
				
			||||||
 | 
					    const embed_thumbnail_url = has_source(
 | 
				
			||||||
 | 
					        node.querySelector("img.embed_thumbnail_url").src
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    const embed_title = node.querySelector('textarea[name="embed_title"]').value;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (
 | 
				
			||||||
 | 
					        attachment === null &&
 | 
				
			||||||
 | 
					        content.length == 0 &&
 | 
				
			||||||
 | 
					        embed_author_url === null &&
 | 
				
			||||||
 | 
					        embed_author.length == 0 &&
 | 
				
			||||||
 | 
					        embed_description.length == 0 &&
 | 
				
			||||||
 | 
					        embed_footer.length == 0 &&
 | 
				
			||||||
 | 
					        embed_footer_url === null &&
 | 
				
			||||||
 | 
					        embed_image_url === null &&
 | 
				
			||||||
 | 
					        embed_thumbnail_url === null
 | 
				
			||||||
 | 
					    ) {
 | 
				
			||||||
 | 
					        return { error: "Reminder needs content." };
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					        // if we're creating a reminder, ignore this field
 | 
				
			||||||
 | 
					        uid: uid,
 | 
				
			||||||
 | 
					        // if we're editing a reminder, ignore this field
 | 
				
			||||||
 | 
					        enabled: enabled,
 | 
				
			||||||
 | 
					        restartable: false,
 | 
				
			||||||
 | 
					        attachment: attachment,
 | 
				
			||||||
 | 
					        attachment_name: attachment_name,
 | 
				
			||||||
 | 
					        avatar: has_source(node.querySelector("img.discord-avatar").src),
 | 
				
			||||||
 | 
					        channel: node.querySelector("select.channel-selector").value,
 | 
				
			||||||
 | 
					        content: content,
 | 
				
			||||||
 | 
					        embed_author_url: embed_author_url,
 | 
				
			||||||
 | 
					        embed_author: embed_author,
 | 
				
			||||||
 | 
					        embed_color: color,
 | 
				
			||||||
 | 
					        embed_description: embed_description,
 | 
				
			||||||
 | 
					        embed_footer: embed_footer,
 | 
				
			||||||
 | 
					        embed_footer_url: embed_footer_url,
 | 
				
			||||||
 | 
					        embed_image_url: embed_image_url,
 | 
				
			||||||
 | 
					        embed_thumbnail_url: embed_thumbnail_url,
 | 
				
			||||||
 | 
					        embed_title: embed_title,
 | 
				
			||||||
 | 
					        embed_fields: fields,
 | 
				
			||||||
 | 
					        expires: expiration_time,
 | 
				
			||||||
 | 
					        interval_seconds: mode !== "template" ? interval.seconds : null,
 | 
				
			||||||
 | 
					        interval_months: mode !== "template" ? interval.months : null,
 | 
				
			||||||
 | 
					        name: node.querySelector('input[name="name"]').value,
 | 
				
			||||||
 | 
					        pin: node.querySelector('input[name="pin"]').checked,
 | 
				
			||||||
 | 
					        tts: node.querySelector('input[name="tts"]').checked,
 | 
				
			||||||
 | 
					        username: node.querySelector('input[name="username"]').value,
 | 
				
			||||||
 | 
					        utc_time: utc_time,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function deserialize_reminder(reminder, frame, mode) {
 | 
				
			||||||
 | 
					    // populate channels
 | 
				
			||||||
 | 
					    set_channels(frame.querySelector("select.channel-selector"));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // populate majority of items
 | 
				
			||||||
 | 
					    for (let prop in reminder) {
 | 
				
			||||||
 | 
					        if (reminder.hasOwnProperty(prop) && reminder[prop] !== null) {
 | 
				
			||||||
 | 
					            if (prop === "attachment") {
 | 
				
			||||||
 | 
					            } else if (prop === "attachment_name") {
 | 
				
			||||||
 | 
					                frame.querySelector(".file-cta > .file-label").textContent =
 | 
				
			||||||
 | 
					                    reminder[prop];
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                let $input = frame.querySelector(`*[name="${prop}"]`);
 | 
				
			||||||
 | 
					                let $image = frame.querySelector(`img.${prop}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if ($input !== null) {
 | 
				
			||||||
 | 
					                    $input.value = reminder[prop];
 | 
				
			||||||
 | 
					                } else if ($image !== null) {
 | 
				
			||||||
 | 
					                    $image.src = reminder[prop];
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const lastChild = frame.querySelector("div.embed-multifield-box .embed-field-box");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for (let field of reminder["embed_fields"]) {
 | 
				
			||||||
 | 
					        let embed_field = $embedFieldTemplate.content.cloneNode(true);
 | 
				
			||||||
 | 
					        embed_field.querySelector("textarea.discord-field-title").value = field["title"];
 | 
				
			||||||
 | 
					        embed_field.querySelector("textarea.discord-field-value").value = field["value"];
 | 
				
			||||||
 | 
					        embed_field.querySelector(".embed-field-box").dataset["inlined"] = field["inline"]
 | 
				
			||||||
 | 
					            ? "1"
 | 
				
			||||||
 | 
					            : "0";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        frame
 | 
				
			||||||
 | 
					            .querySelector("div.embed-multifield-box")
 | 
				
			||||||
 | 
					            .insertBefore(embed_field, lastChild);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (mode !== "template") {
 | 
				
			||||||
 | 
					        if (reminder["interval_seconds"]) update_interval(frame);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let $enableBtn = frame.querySelector(".disable-enable");
 | 
				
			||||||
 | 
					        $enableBtn.dataset["action"] = reminder["enabled"] ? "disable" : "enable";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let timeInput = frame.querySelector('input[name="time"]');
 | 
				
			||||||
 | 
					        let localTime = luxon.DateTime.fromISO(reminder["utc_time"], {
 | 
				
			||||||
 | 
					            zone: "UTC",
 | 
				
			||||||
 | 
					        }).setZone(timezone);
 | 
				
			||||||
 | 
					        timeInput.value = localTime.toFormat("yyyy-LL-dd'T'HH:mm:ss");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (reminder["expires"]) {
 | 
				
			||||||
 | 
					            let expiresInput = frame.querySelector('input[name="time"]');
 | 
				
			||||||
 | 
					            let expiresTime = luxon.DateTime.fromISO(reminder["expires"], {
 | 
				
			||||||
 | 
					                zone: "UTC",
 | 
				
			||||||
 | 
					            }).setZone(timezone);
 | 
				
			||||||
 | 
					            expiresInput.value = expiresTime.toFormat("yyyy-LL-dd'T'HH:mm:ss");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					document.addEventListener("guildSwitched", async (e) => {
 | 
				
			||||||
 | 
					    $loader.classList.remove("is-hidden");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let $anchor = document.querySelector(
 | 
				
			||||||
 | 
					        `.switch-pane[data-guild="${e.detail.guild_id}"]`
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    switch_pane($anchor.dataset["pane"]);
 | 
				
			||||||
 | 
					    reset_guild_pane();
 | 
				
			||||||
 | 
					    $anchor.classList.add("is-active");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fetch_roles(e.detail.guild_id);
 | 
				
			||||||
 | 
					    fetch_templates(e.detail.guild_id);
 | 
				
			||||||
 | 
					    await fetch_channels(e.detail.guild_id);
 | 
				
			||||||
 | 
					    fetch_reminders(e.detail.guild_id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    document.querySelectorAll("p.pageTitle").forEach((el) => {
 | 
				
			||||||
 | 
					        el.textContent = `${e.detail.guild_name} Reminders`;
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    document.querySelectorAll("select.channel-selector").forEach((el) => {
 | 
				
			||||||
 | 
					        el.addEventListener("change", (e) => {
 | 
				
			||||||
 | 
					            update_select(e.target);
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    resize_textareas();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    $loader.classList.add("is-hidden");
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					document.addEventListener("channelsLoaded", () => {
 | 
				
			||||||
 | 
					    document.querySelectorAll("select.channel-selector").forEach(set_channels);
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					document.addEventListener("remindersLoaded", (event) => {
 | 
				
			||||||
 | 
					    const guild = guildId();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for (let reminder of event.detail) {
 | 
				
			||||||
 | 
					        let node = reminder.node;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        node.querySelector("button.hide-box").addEventListener("click", () => {
 | 
				
			||||||
 | 
					            node.closest(".reminderContent").classList.toggle("is-collapsed");
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        node.querySelector("div.discord-embed").style.borderLeftColor = intToColor(
 | 
				
			||||||
 | 
					            reminder.embed_color
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const enableBtn = node.querySelector(".disable-enable");
 | 
				
			||||||
 | 
					        enableBtn.addEventListener("click", () => {
 | 
				
			||||||
 | 
					            let enable = enableBtn.dataset["action"] === "enable";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            fetch(`/dashboard/api/guild/${guild}/reminders`, {
 | 
				
			||||||
 | 
					                method: "PATCH",
 | 
				
			||||||
 | 
					                body: JSON.stringify({
 | 
				
			||||||
 | 
					                    uid: reminder["uid"],
 | 
				
			||||||
 | 
					                    enabled: enable,
 | 
				
			||||||
 | 
					                }),
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					                .then((response) => response.json())
 | 
				
			||||||
 | 
					                .then((data) => {
 | 
				
			||||||
 | 
					                    if (data.error) {
 | 
				
			||||||
 | 
					                        show_error(data.error);
 | 
				
			||||||
 | 
					                    } else {
 | 
				
			||||||
 | 
					                        enableBtn.dataset["action"] = data["enabled"]
 | 
				
			||||||
 | 
					                            ? "enable"
 | 
				
			||||||
 | 
					                            : "disable";
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        node.querySelector("button.delete-reminder").addEventListener("click", () => {
 | 
				
			||||||
 | 
					            $deleteReminderBtn.dataset["uid"] = reminder["uid"];
 | 
				
			||||||
 | 
					            $deleteReminderBtn.closest(".modal").classList.toggle("is-active");
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const $saveBtn = node.querySelector("button.save-btn");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $saveBtn.addEventListener("click", async (event) => {
 | 
				
			||||||
 | 
					            $saveBtn.querySelector("span.icon > i").classList = [
 | 
				
			||||||
 | 
					                "fas fa-spinner fa-spin",
 | 
				
			||||||
 | 
					            ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            let reminder = await serialize_reminder(node, "edit");
 | 
				
			||||||
 | 
					            if (reminder.error) {
 | 
				
			||||||
 | 
					                show_error(reminder.error);
 | 
				
			||||||
 | 
					                return;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            let guild = guildId();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            fetch(`/dashboard/api/guild/${guild}/reminders`, {
 | 
				
			||||||
 | 
					                method: "PATCH",
 | 
				
			||||||
 | 
					                headers: {
 | 
				
			||||||
 | 
					                    "Content-Type": "application/json",
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					                body: JSON.stringify(reminder),
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					                .then((response) => response.json())
 | 
				
			||||||
 | 
					                .then((data) => {
 | 
				
			||||||
 | 
					                    for (let error of data.errors) show_error(error);
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            $saveBtn.querySelector("span.icon > i").classList = ["fas fa-check"];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            window.setTimeout(() => {
 | 
				
			||||||
 | 
					                $saveBtn.querySelector("span.icon > i").classList = ["fas fa-save"];
 | 
				
			||||||
 | 
					            }, 1500);
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					$deleteReminderBtn.addEventListener("click", () => {
 | 
				
			||||||
 | 
					    let guild = guildId();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fetch(`/dashboard/api/guild/${guild}/reminders`, {
 | 
				
			||||||
 | 
					        method: "DELETE",
 | 
				
			||||||
 | 
					        body: JSON.stringify({
 | 
				
			||||||
 | 
					            uid: $deleteReminderBtn.dataset["uid"],
 | 
				
			||||||
 | 
					        }),
 | 
				
			||||||
 | 
					    }).then(() => {
 | 
				
			||||||
 | 
					        document.querySelector("#deleteReminderModal").classList.remove("is-active");
 | 
				
			||||||
 | 
					        fetch_reminders(guild);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function show_error(error) {
 | 
				
			||||||
 | 
					    document.getElementById("errors").querySelector("span.error-message").textContent =
 | 
				
			||||||
 | 
					        error;
 | 
				
			||||||
 | 
					    document.getElementById("errors").classList.add("is-active");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    window.setTimeout(() => {
 | 
				
			||||||
 | 
					        document.getElementById("errors").classList.remove("is-active");
 | 
				
			||||||
 | 
					    }, 5000);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					$colorPickerInput.value = colorPicker.color.hexString;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					$colorPickerInput.addEventListener("input", () => {
 | 
				
			||||||
 | 
					    if (/^#[0-9a-fA-F]{6}$/.test($colorPickerInput.value) === true) {
 | 
				
			||||||
 | 
					        colorPicker.color.hexString = $colorPickerInput.value;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					colorPicker.on("color:change", function (color) {
 | 
				
			||||||
 | 
					    $colorPickerInput.value = color.hexString;
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					$colorPickerModal.querySelector("button.is-success").addEventListener("click", () => {
 | 
				
			||||||
 | 
					    $discordFrame.style.borderLeftColor = colorPicker.color.rgbString;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    $colorPickerModal.classList.remove("is-active");
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					document.querySelectorAll(".show-modal").forEach((element) => {
 | 
				
			||||||
 | 
					    element.addEventListener("click", (e) => {
 | 
				
			||||||
 | 
					        e.preventDefault();
 | 
				
			||||||
 | 
					        document.getElementById(element.dataset["modal"]).classList.toggle("is-active");
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					document.addEventListener("DOMContentLoaded", () => {
 | 
				
			||||||
 | 
					    $loader.classList.remove("is-hidden");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    document.querySelectorAll(".navbar-burger").forEach((el) => {
 | 
				
			||||||
 | 
					        el.addEventListener("click", () => {
 | 
				
			||||||
 | 
					            const target = el.dataset["target"];
 | 
				
			||||||
 | 
					            const $target = document.getElementById(target);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            el.classList.toggle("is-active");
 | 
				
			||||||
 | 
					            $target.classList.toggle("is-active");
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let hideBox = document.querySelector("#reminderCreator button.hide-box");
 | 
				
			||||||
 | 
					    hideBox.addEventListener("click", () => {
 | 
				
			||||||
 | 
					        hideBox.closest(".reminderContent").classList.toggle("is-collapsed");
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fetch("/dashboard/api/user")
 | 
				
			||||||
 | 
					        .then((response) => response.json())
 | 
				
			||||||
 | 
					        .then((data) => {
 | 
				
			||||||
 | 
					            if (data.error) {
 | 
				
			||||||
 | 
					                show_error(data.error);
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                if (data.timezone !== null) botTimezone = data.timezone;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                globalPatreon = data.patreon;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                update_times();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fetch("/dashboard/api/user/guilds")
 | 
				
			||||||
 | 
					        .then((response) => response.json())
 | 
				
			||||||
 | 
					        .then((data) => {
 | 
				
			||||||
 | 
					            if (data.error) {
 | 
				
			||||||
 | 
					                show_error(data.error);
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                const $template = document.getElementById("guildListEntry");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                for (let guild of data) {
 | 
				
			||||||
 | 
					                    document.querySelectorAll(".guildList").forEach((element) => {
 | 
				
			||||||
 | 
					                        const $clone = $template.content.cloneNode(true);
 | 
				
			||||||
 | 
					                        const $anchor = $clone.querySelector("a");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        let $span = $clone.querySelector("a > span.guild-name");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        $span.textContent = $span.textContent.replace(
 | 
				
			||||||
 | 
					                            "%guildname%",
 | 
				
			||||||
 | 
					                            guild.name
 | 
				
			||||||
 | 
					                        );
 | 
				
			||||||
 | 
					                        $anchor.dataset["guild"] = guild.id;
 | 
				
			||||||
 | 
					                        $anchor.dataset["name"] = guild.name;
 | 
				
			||||||
 | 
					                        $anchor.href = `/dashboard/${guild.id}?name=${guild.name}`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        $anchor.addEventListener("click", async (e) => {
 | 
				
			||||||
 | 
					                            e.preventDefault();
 | 
				
			||||||
 | 
					                            window.history.pushState(
 | 
				
			||||||
 | 
					                                {},
 | 
				
			||||||
 | 
					                                "",
 | 
				
			||||||
 | 
					                                `/dashboard/${guild.id}?name=${guild.name}`
 | 
				
			||||||
 | 
					                            );
 | 
				
			||||||
 | 
					                            const event = new CustomEvent("guildSwitched", {
 | 
				
			||||||
 | 
					                                detail: {
 | 
				
			||||||
 | 
					                                    guild_name: guild.name,
 | 
				
			||||||
 | 
					                                    guild_id: guild.id,
 | 
				
			||||||
 | 
					                                },
 | 
				
			||||||
 | 
					                            });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                            document.dispatchEvent(event);
 | 
				
			||||||
 | 
					                        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        element.append($clone);
 | 
				
			||||||
 | 
					                    });
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                const matches = window.location.href.match(/dashboard\/(\d+)/);
 | 
				
			||||||
 | 
					                if (matches) {
 | 
				
			||||||
 | 
					                    let id = matches[1];
 | 
				
			||||||
 | 
					                    let name =
 | 
				
			||||||
 | 
					                        new URLSearchParams(window.location.search).get("name") || id;
 | 
				
			||||||
 | 
					                    const event = new CustomEvent("guildSwitched", {
 | 
				
			||||||
 | 
					                        detail: {
 | 
				
			||||||
 | 
					                            guild_name: name,
 | 
				
			||||||
 | 
					                            guild_id: id,
 | 
				
			||||||
 | 
					                        },
 | 
				
			||||||
 | 
					                    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    document.dispatchEvent(event);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    $loader.classList.add("is-hidden");
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function set_channels(element) {
 | 
				
			||||||
 | 
					    for (let channel of channels) {
 | 
				
			||||||
 | 
					        let newOption = document.createElement("option");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        newOption.value = channel.id;
 | 
				
			||||||
 | 
					        newOption.textContent = channel.name;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        element.appendChild(newOption);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    update_select(element);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function has_source(string) {
 | 
				
			||||||
 | 
					    if (string.startsWith(`https://${window.location.hostname}`)) {
 | 
				
			||||||
 | 
					        return null;
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        return string;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					$createReminderBtn.addEventListener("click", async () => {
 | 
				
			||||||
 | 
					    $createReminderBtn.querySelector("span.icon > i").classList = [
 | 
				
			||||||
 | 
					        "fas fa-spinner fa-spin",
 | 
				
			||||||
 | 
					    ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let reminder = await serialize_reminder($createReminder, "create");
 | 
				
			||||||
 | 
					    if (reminder.error) {
 | 
				
			||||||
 | 
					        show_error(reminder.error);
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let guild = guildId();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fetch(`/dashboard/api/guild/${guild}/reminders`, {
 | 
				
			||||||
 | 
					        method: "POST",
 | 
				
			||||||
 | 
					        headers: {
 | 
				
			||||||
 | 
					            "Content-Type": "application/json",
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        body: JSON.stringify(reminder),
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					        .then((response) => response.json())
 | 
				
			||||||
 | 
					        .then((data) => {
 | 
				
			||||||
 | 
					            if (data.error) {
 | 
				
			||||||
 | 
					                show_error(data.error);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                $createReminderBtn.querySelector("span.icon > i").classList = [
 | 
				
			||||||
 | 
					                    "fas fa-sparkles",
 | 
				
			||||||
 | 
					                ];
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                const $reminderBox = document.querySelector("div#guildReminders");
 | 
				
			||||||
 | 
					                let newFrame = $reminderTemplate.content.cloneNode(true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                newFrame.querySelector(".reminderContent").dataset["uid"] = data["uid"];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                deserialize_reminder(data, newFrame, "load");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                $reminderBox.appendChild(newFrame);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                data.node = $reminderBox.lastElementChild;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                document.dispatchEvent(
 | 
				
			||||||
 | 
					                    new CustomEvent("remindersLoaded", {
 | 
				
			||||||
 | 
					                        detail: [data],
 | 
				
			||||||
 | 
					                    })
 | 
				
			||||||
 | 
					                );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                $createReminderBtn.querySelector("span.icon > i").classList = [
 | 
				
			||||||
 | 
					                    "fas fa-check",
 | 
				
			||||||
 | 
					                ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                window.setTimeout(() => {
 | 
				
			||||||
 | 
					                    $createReminderBtn.querySelector("span.icon > i").classList = [
 | 
				
			||||||
 | 
					                        "fas fa-sparkles",
 | 
				
			||||||
 | 
					                    ];
 | 
				
			||||||
 | 
					                }, 1500);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					$createTemplateBtn.addEventListener("click", async () => {
 | 
				
			||||||
 | 
					    $createTemplateBtn.querySelector("span.icon > i").classList = [
 | 
				
			||||||
 | 
					        "fas fa-spinner fa-spin",
 | 
				
			||||||
 | 
					    ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let reminder = await serialize_reminder($createReminder, "template");
 | 
				
			||||||
 | 
					    let guild = guildId();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fetch(`/dashboard/api/guild/${guild}/templates`, {
 | 
				
			||||||
 | 
					        method: "POST",
 | 
				
			||||||
 | 
					        headers: {
 | 
				
			||||||
 | 
					            "Content-Type": "application/json",
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        body: JSON.stringify(reminder),
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					        .then((response) => response.json())
 | 
				
			||||||
 | 
					        .then((data) => {
 | 
				
			||||||
 | 
					            if (data.error) {
 | 
				
			||||||
 | 
					                show_error(data.error);
 | 
				
			||||||
 | 
					                $createTemplateBtn.querySelector("span.icon > i").classList = [
 | 
				
			||||||
 | 
					                    "fas fa-file-spreadsheet",
 | 
				
			||||||
 | 
					                ];
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                fetch_templates(guildId());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                $createTemplateBtn.querySelector("span.icon > i").classList = [
 | 
				
			||||||
 | 
					                    "fas fa-check",
 | 
				
			||||||
 | 
					                ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                window.setTimeout(() => {
 | 
				
			||||||
 | 
					                    $createTemplateBtn.querySelector("span.icon > i").classList = [
 | 
				
			||||||
 | 
					                        "fas fa-file-spreadsheet",
 | 
				
			||||||
 | 
					                    ];
 | 
				
			||||||
 | 
					                }, 1500);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					$loadTemplateBtn.addEventListener("click", (ev) => {
 | 
				
			||||||
 | 
					    deserialize_reminder(
 | 
				
			||||||
 | 
					        templates[parseInt($templateSelect.value)],
 | 
				
			||||||
 | 
					        $createReminder,
 | 
				
			||||||
 | 
					        "template"
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					$deleteTemplateBtn.addEventListener("click", (ev) => {
 | 
				
			||||||
 | 
					    fetch(`/dashboard/api/guild/${guildId()}/templates`, {
 | 
				
			||||||
 | 
					        method: "DELETE",
 | 
				
			||||||
 | 
					        headers: {
 | 
				
			||||||
 | 
					            "Content-Type": "application/json",
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        body: JSON.stringify({ id: parseInt($templateSelect.value) }),
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					        .then((response) => response.json())
 | 
				
			||||||
 | 
					        .then((data) => {
 | 
				
			||||||
 | 
					            if (data.error) {
 | 
				
			||||||
 | 
					                show_error(data.error);
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                $templateSelect
 | 
				
			||||||
 | 
					                    .querySelector(`option[value="${$templateSelect.value}"]`)
 | 
				
			||||||
 | 
					                    .remove();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					document.querySelectorAll("textarea.autoresize").forEach((element) => {
 | 
				
			||||||
 | 
					    element.addEventListener("input", () => {
 | 
				
			||||||
 | 
					        element.style.height = "";
 | 
				
			||||||
 | 
					        element.style.height = element.scrollHeight + 3 + "px";
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					let $img;
 | 
				
			||||||
 | 
					const $urlModal = document.querySelector("div#addImageModal");
 | 
				
			||||||
 | 
					const $urlInput = $urlModal.querySelector("input");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					$urlModal.querySelector("button#setImgUrl").addEventListener("click", () => {
 | 
				
			||||||
 | 
					    $img.src = $urlInput.value;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    $urlInput.value = "";
 | 
				
			||||||
 | 
					    $urlModal.classList.remove("is-active");
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					document.querySelectorAll("button.close-modal").forEach((element) => {
 | 
				
			||||||
 | 
					    element.addEventListener("click", () => {
 | 
				
			||||||
 | 
					        let $modal = element.closest("div.modal");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $urlInput.value = "";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $modal.classList.remove("is-active");
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					document.addEventListener("remindersLoaded", () => {
 | 
				
			||||||
 | 
					    document.querySelectorAll(".customizable").forEach((element) => {
 | 
				
			||||||
 | 
					        element.querySelector("a").addEventListener("click", (e) => {
 | 
				
			||||||
 | 
					            e.preventDefault();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            $img = element.querySelector("img");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            $urlModal.classList.toggle("is-active");
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const fileInput = document.querySelectorAll("input[type=file]");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fileInput.forEach((element) => {
 | 
				
			||||||
 | 
					        element.addEventListener("change", () => {
 | 
				
			||||||
 | 
					            if (element.files.length > 0) {
 | 
				
			||||||
 | 
					                const fileName = element.parentElement.querySelector(".file-label");
 | 
				
			||||||
 | 
					                fileName.textContent = element.files[0].name;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    document.querySelectorAll(".change-color").forEach((element) => {
 | 
				
			||||||
 | 
					        element.addEventListener("click", (e) => {
 | 
				
			||||||
 | 
					            e.preventDefault();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            $discordFrame = element
 | 
				
			||||||
 | 
					                .closest("div.reminderContent")
 | 
				
			||||||
 | 
					                .querySelector("div.discord-embed");
 | 
				
			||||||
 | 
					            $colorPickerModal.classList.toggle("is-active");
 | 
				
			||||||
 | 
					            colorPicker.color.rgbString =
 | 
				
			||||||
 | 
					                window.getComputedStyle($discordFrame).borderLeftColor;
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function check_embed_fields() {
 | 
				
			||||||
 | 
					    document.querySelectorAll(".embed-field-box").forEach((element) => {
 | 
				
			||||||
 | 
					        const $titleInput = element.querySelector(".discord-field-title");
 | 
				
			||||||
 | 
					        const $valueInput = element.querySelector(".discord-field-value");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // when the user clicks out of the field title and if the field title/value are empty, remove the field
 | 
				
			||||||
 | 
					        $titleInput.addEventListener("blur", () => {
 | 
				
			||||||
 | 
					            if (
 | 
				
			||||||
 | 
					                $titleInput.value === "" &&
 | 
				
			||||||
 | 
					                $valueInput.value === "" &&
 | 
				
			||||||
 | 
					                element.nextElementSibling !== null
 | 
				
			||||||
 | 
					            ) {
 | 
				
			||||||
 | 
					                element.remove();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $valueInput.addEventListener("blur", () => {
 | 
				
			||||||
 | 
					            if (
 | 
				
			||||||
 | 
					                $titleInput.value === "" &&
 | 
				
			||||||
 | 
					                $valueInput.value === "" &&
 | 
				
			||||||
 | 
					                element.nextElementSibling !== null
 | 
				
			||||||
 | 
					            ) {
 | 
				
			||||||
 | 
					                element.remove();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // when the user inputs into the end field, create a new field after it
 | 
				
			||||||
 | 
					        $titleInput.addEventListener("input", () => {
 | 
				
			||||||
 | 
					            if (
 | 
				
			||||||
 | 
					                $titleInput.value !== "" &&
 | 
				
			||||||
 | 
					                $valueInput.value !== "" &&
 | 
				
			||||||
 | 
					                element.nextElementSibling === null
 | 
				
			||||||
 | 
					            ) {
 | 
				
			||||||
 | 
					                const $clone = $embedFieldTemplate.content.cloneNode(true);
 | 
				
			||||||
 | 
					                element.parentElement.append($clone);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $valueInput.addEventListener("input", () => {
 | 
				
			||||||
 | 
					            if (
 | 
				
			||||||
 | 
					                $titleInput.value !== "" &&
 | 
				
			||||||
 | 
					                $valueInput.value !== "" &&
 | 
				
			||||||
 | 
					                element.nextElementSibling === null
 | 
				
			||||||
 | 
					            ) {
 | 
				
			||||||
 | 
					                const $clone = $embedFieldTemplate.content.cloneNode(true);
 | 
				
			||||||
 | 
					                element.parentElement.append($clone);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					document.addEventListener("DOMNodeInserted", () => {
 | 
				
			||||||
 | 
					    document.querySelectorAll("div.mobile-sidebar a").forEach((element) => {
 | 
				
			||||||
 | 
					        element.addEventListener("click", (e) => {
 | 
				
			||||||
 | 
					            document.getElementById("mobileSidebar").classList.remove("is-active");
 | 
				
			||||||
 | 
					            document.querySelectorAll(".navbar-burger").forEach((el) => {
 | 
				
			||||||
 | 
					                el.classList.remove("is-active");
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    document.querySelectorAll('input[type="datetime-local"]').forEach((el) => {
 | 
				
			||||||
 | 
					        let now = luxon.DateTime.now().setZone(timezone);
 | 
				
			||||||
 | 
					        el.min = now.toFormat("yyyy-LL-dd'T'HH:mm:ss");
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    check_embed_fields();
 | 
				
			||||||
 | 
					    resize_textareas();
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					document.addEventListener("click", (ev) => {
 | 
				
			||||||
 | 
					    if (ev.target.closest("button.inline-btn") !== null) {
 | 
				
			||||||
 | 
					        let inlined = ev.target.closest(".embed-field-box").dataset["inlined"];
 | 
				
			||||||
 | 
					        ev.target.closest(".embed-field-box").dataset["inlined"] =
 | 
				
			||||||
 | 
					            inlined == "1" ? "0" : "1";
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					});
 | 
				
			||||||