Compare commits
	
		
			143 Commits
		
	
	
		
			next
			...
			jude/remov
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					d7e90614c8 | ||
| 
						 | 
					b5dbfe336d | ||
| 
						 | 
					218be2f0b1 | ||
| 
						 | 
					d7515f3611 | ||
| 
						 | 
					6ae1096d79 | ||
| 
						 | 
					1f0d7adae3 | ||
| 
						 | 
					fc96ae526f | ||
| 
						 | 
					8881ef0f85 | ||
| 
						 | 
					5e82a687f9 | ||
| 
						 | 
					de4ecf8dd6 | ||
| 
						 | 
					064efd4386 | ||
| 
						 | 
					65b8ba3b47 | ||
| 9d452ed8cb | |||
| 
						 | 
					441419b92b | ||
| 
						 | 
					aecf2c15be | ||
| 
						 | 
					79da56c794 | ||
| 
						 | 
					ef10902c1e | ||
| 
						 | 
					c277f85c2a | ||
| 
						 | 
					035653c7fa | ||
| 
						 | 
					6358bc3deb | ||
| 
						 | 
					9f5066f982 | ||
| 
						 | 
					1d06999e41 | ||
| 
						 | 
					1cf707140c | ||
| 
						 | 
					e38c63f5ba | ||
| 
						 | 
					d52b8b26f2 | ||
| bb2128a7ed | |||
| 5e99a6f9de | |||
| 5406e6b8ec | |||
| 
						 | 
					4ee0bc4e37 | ||
| 
						 | 
					b99bb7dcbf | ||
| 
						 | 
					98f925dc84 | ||
| 
						 | 
					24e316b12f | ||
| 
						 | 
					4063334953 | ||
| 
						 | 
					e128b9848f | ||
| 
						 | 
					9989ab3b35 | ||
| 
						 | 
					b951db3f55 | ||
| 
						 | 
					884a47bf36 | ||
| 
						 | 
					b0f932445c | ||
| 
						 | 
					2861cdda0b | ||
| 
						 | 
					7ba8fcd6b7 | ||
| 
						 | 
					850f0fad57 | ||
| 
						 | 
					a770a17ee7 | ||
| 
						 | 
					d15a66d9d9 | ||
| 
						 | 
					30f011fcd5 | ||
| 
						 | 
					15dbed2f0f | ||
| 
						 | 
					18cac0345b | ||
| 
						 | 
					334b1bc084 | ||
| 
						 | 
					ba3c76c25f | ||
| 
						 | 
					67b6f30c62 | ||
| 
						 | 
					8ae311190f | ||
| 
						 | 
					016164affb | ||
| 
						 | 
					2c0aeef700 | ||
| 
						 | 
					ecd75d6f55 | ||
| 
						 | 
					4a80d42f86 | ||
| 
						 | 
					075fde71df | ||
| 
						 | 
					55136aecdc | ||
| 
						 | 
					63fc2cdcbc | ||
| 
						 | 
					3190738fc5 | ||
| 
						 | 
					8f4810b532 | ||
| 
						 | 
					a5e6c41fa5 | ||
| 
						 | 
					5f0aa0f834 | ||
| 
						 | 
					dbe8e8e358 | ||
| 
						 | 
					85a114e55c | ||
| 
						 | 
					329492b244 | ||
| 
						 | 
					66135ecd08 | ||
| 
						 | 
					382c2a5a1e | ||
| 
						 | 
					b91245a3f7 | ||
| 
						 | 
					6f0bdf9852 | ||
| 
						 | 
					dcee9e0d2a | ||
| 
						 | 
					8e6e1a18b7 | ||
| 
						 | 
					72af0532fa | ||
| 
						 | 
					e83b643d86 | ||
| 
						 | 
					0e0ab053f3 | ||
| 
						 | 
					8c2296b9c8 | ||
| 
						 | 
					1c6103142f | ||
| 
						 | 
					328127c55e | ||
| 
						 | 
					b0e37b56c0 | ||
| 
						 | 
					45f5b6261a | ||
| 
						 | 
					5f6326179c | ||
| 
						 | 
					6254f91841 | ||
| 
						 | 
					60b90a61d4 | ||
| 
						 | 
					90f05758d0 | ||
| 
						 | 
					74b7b5d711 | ||
| 
						 | 
					90550dc2c7 | ||
| 
						 | 
					79e6498245 | ||
| 
						 | 
					a8ef3d03f9 | ||
| 
						 | 
					53e13844f9 | ||
| 
						 | 
					dd7e681285 | ||
| 
						 | 
					6c20bf2a0f | ||
| 
						 | 
					15aa9ccffd | ||
| 
						 | 
					525471bcad | ||
| 
						 | 
					86d53b63b6 | ||
| 
						 | 
					d8f266852a | ||
| 
						 | 
					76a286076b | ||
| 
						 | 
					5e39e16060 | ||
| 
						 | 
					c1305cfb36 | ||
| 
						 | 
					4823754955 | ||
| 
						 | 
					eb92eacb90 | ||
| 
						 | 
					d0833b7bca | ||
| 
						 | 
					b81c3c80c1 | ||
| 
						 | 
					2f6d035efe | ||
| 
						 | 
					96012ce43c | ||
| 
						 | 
					fa7ec8731b | ||
| 
						 | 
					def43bfa78 | ||
| 
						 | 
					e4e9af2bb4 | ||
| 
						 | 
					cce0de7c75 | ||
| e7803b98e8 | |||
| 
						 | 
					7aae246388 | ||
| a2d442bc54 | |||
| 59982df827 | |||
| 
						 | 
					7a6372ed02 | ||
| 
						 | 
					14a54471f7 | ||
| 
						 | 
					5d3b77f1cd | ||
| 
						 | 
					1d64c8bb79 | ||
| 8ba0f02b98 | |||
| d36438c6ce | |||
| e0c60e2ce3 | |||
| 
						 | 
					e7160215b0 | ||
| 
						 | 
					6eaa6f0f28 | ||
| 
						 | 
					9db0fa2513 | ||
| 
						 | 
					ca13fd4fa7 | ||
| 
						 | 
					55acc8fd16 | ||
| 
						 | 
					145711fa5d | ||
| 
						 | 
					5524215786 | ||
| 
						 | 
					e8bd05893f | ||
| 
						 | 
					e3d3418f99 | ||
| 
						 | 
					2681280a39 | ||
| 
						 | 
					00579428a1 | ||
| 
						 | 
					b8ef999710 | ||
| 
						 | 
					e8f84e281a | ||
| 
						 | 
					8ddff698e5 | ||
| 
						 | 
					541633270c | ||
| 
						 | 
					25286da5e0 | ||
| 
						 | 
					4bad1324b9 | ||
| 
						 | 
					bd1462a00c | ||
| 
						 | 
					56ffc43616 | ||
| 
						 | 
					52cf642455 | ||
| 
						 | 
					0bf578357a | ||
| 
						 | 
					6e9eccb62e | ||
| 
						 | 
					6ea28284ce | ||
| 
						 | 
					a6525f3052 | ||
| 
						 | 
					348639270d | ||
| 
						 | 
					37177c2431 | 
							
								
								
									
										29
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -1,5 +1,30 @@
 | 
				
			|||||||
/target
 | 
					target
 | 
				
			||||||
.env
 | 
					.env
 | 
				
			||||||
/venv
 | 
					/venv
 | 
				
			||||||
.cargo
 | 
					.cargo
 | 
				
			||||||
/.idea
 | 
					.idea
 | 
				
			||||||
 | 
					web/static/index.html
 | 
				
			||||||
 | 
					web/static/assets
 | 
				
			||||||
 | 
					# Logs
 | 
				
			||||||
 | 
					logs
 | 
				
			||||||
 | 
					*.log
 | 
				
			||||||
 | 
					npm-debug.log*
 | 
				
			||||||
 | 
					yarn-debug.log*
 | 
				
			||||||
 | 
					yarn-error.log*
 | 
				
			||||||
 | 
					pnpm-debug.log*
 | 
				
			||||||
 | 
					lerna-debug.log*
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					node_modules
 | 
				
			||||||
 | 
					dist
 | 
				
			||||||
 | 
					dist-ssr
 | 
				
			||||||
 | 
					*.local
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Editor directories and files
 | 
				
			||||||
 | 
					.vscode/*
 | 
				
			||||||
 | 
					!.vscode/extensions.json
 | 
				
			||||||
 | 
					.DS_Store
 | 
				
			||||||
 | 
					*.suo
 | 
				
			||||||
 | 
					*.ntvs*
 | 
				
			||||||
 | 
					*.njsproj
 | 
				
			||||||
 | 
					*.sln
 | 
				
			||||||
 | 
					*.sw?
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										2240
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
							
								
								
									
										49
									
								
								Cargo.toml
									
									
									
									
									
								
							
							
						
						@@ -1,22 +1,21 @@
 | 
				
			|||||||
[package]
 | 
					[package]
 | 
				
			||||||
name = "reminder-rs"
 | 
					name = "reminder-rs"
 | 
				
			||||||
version = "1.6.40"
 | 
					version = "1.7.24"
 | 
				
			||||||
authors = ["Jude Southworth <judesouthworth@pm.me>"]
 | 
					authors = ["Jude Southworth <judesouthworth@pm.me>"]
 | 
				
			||||||
edition = "2021"
 | 
					edition = "2021"
 | 
				
			||||||
license = "AGPL-3.0 only"
 | 
					license = "AGPL-3.0 only"
 | 
				
			||||||
description = "Reminder Bot for Discord, now in Rust"
 | 
					description = "Reminder Bot for Discord, now in Rust"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[dependencies]
 | 
					[dependencies]
 | 
				
			||||||
poise = "0.5"
 | 
					poise = "0.6.1"
 | 
				
			||||||
dotenv = "0.15"
 | 
					dotenv = "0.15"
 | 
				
			||||||
tokio = { version = "1", features = ["process", "full"] }
 | 
					tokio = { version = "1", features = ["process", "full"] }
 | 
				
			||||||
reqwest = "0.11"
 | 
					reqwest = { version = "0.12", features = ["json"] }
 | 
				
			||||||
lazy-regex = "3.0"
 | 
					regex = "1.10"
 | 
				
			||||||
regex = "1.9"
 | 
					 | 
				
			||||||
log = "0.4"
 | 
					log = "0.4"
 | 
				
			||||||
env_logger = "0.10"
 | 
					env_logger = "0.11"
 | 
				
			||||||
chrono = "0.4"
 | 
					chrono = "0.4"
 | 
				
			||||||
chrono-tz = { version = "0.8", features = ["serde"] }
 | 
					chrono-tz = { version = "0.9", features = ["serde"] }
 | 
				
			||||||
lazy_static = "1.4"
 | 
					lazy_static = "1.4"
 | 
				
			||||||
num-integer = "0.1"
 | 
					num-integer = "0.1"
 | 
				
			||||||
serde = "1.0"
 | 
					serde = "1.0"
 | 
				
			||||||
@@ -25,14 +24,23 @@ serde_repr = "0.1"
 | 
				
			|||||||
rmp-serde = "1.1"
 | 
					rmp-serde = "1.1"
 | 
				
			||||||
rand = "0.8"
 | 
					rand = "0.8"
 | 
				
			||||||
levenshtein = "1.0"
 | 
					levenshtein = "1.0"
 | 
				
			||||||
sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "macros", "mysql", "bigdecimal", "chrono", "migrate"]}
 | 
					sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "macros", "mysql", "bigdecimal", "chrono", "migrate"] }
 | 
				
			||||||
base64 = "0.21.0"
 | 
					base64 = "0.22"
 | 
				
			||||||
 | 
					secrecy = "0.8.0"
 | 
				
			||||||
 | 
					futures = "0.3.30"
 | 
				
			||||||
 | 
					prometheus = "0.13.3"
 | 
				
			||||||
 | 
					rocket = { version = "0.5.0", features = ["tls", "secrets", "json"] }
 | 
				
			||||||
 | 
					rocket_dyn_templates = { version = "0.1.0", features = ["tera"] }
 | 
				
			||||||
 | 
					serenity = { version = "0.12", default-features = false, features = ["builder", "cache", "client", "gateway", "http", "model", "utils", "rustls_backend"] }
 | 
				
			||||||
 | 
					oauth2 = "4"
 | 
				
			||||||
 | 
					csv = "1.2"
 | 
				
			||||||
 | 
					sd-notify = "0.4.1"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[dependencies.postman]
 | 
					[dependencies.extract_derive]
 | 
				
			||||||
path = "postman"
 | 
					path = "extract_derive"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[dependencies.reminder_web]
 | 
					[dependencies.recordable_derive]
 | 
				
			||||||
path = "web"
 | 
					path = "recordable_derive"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[package.metadata.deb]
 | 
					[package.metadata.deb]
 | 
				
			||||||
depends = "$auto, python3-dateparser (>= 1.0.0)"
 | 
					depends = "$auto, python3-dateparser (>= 1.0.0)"
 | 
				
			||||||
@@ -40,13 +48,18 @@ suggests = "mysql-server-8.0, nginx"
 | 
				
			|||||||
maintainer-scripts = "debian"
 | 
					maintainer-scripts = "debian"
 | 
				
			||||||
assets = [
 | 
					assets = [
 | 
				
			||||||
    ["target/release/reminder-rs", "usr/bin/reminder-rs", "755"],
 | 
					    ["target/release/reminder-rs", "usr/bin/reminder-rs", "755"],
 | 
				
			||||||
 | 
					    ["static/css/*", "lib/reminder-rs/static/css", "644"],
 | 
				
			||||||
 | 
					    ["static/favicon/*", "lib/reminder-rs/static/favicon", "644"],
 | 
				
			||||||
 | 
					    ["static/img/*", "lib/reminder-rs/static/img", "644"],
 | 
				
			||||||
 | 
					    ["static/js/*", "lib/reminder-rs/static/js", "644"],
 | 
				
			||||||
 | 
					    ["static/webfonts/*", "lib/reminder-rs/static/webfonts", "644"],
 | 
				
			||||||
 | 
					    ["static/site.webmanifest", "lib/reminder-rs/static/site.webmanifest", "644"],
 | 
				
			||||||
 | 
					    ["templates/**/*", "lib/reminder-rs/templates", "644"],
 | 
				
			||||||
 | 
					    ["reminder-dashboard/dist/static/assets/*", "lib/reminder-rs/static/assets", "644"],
 | 
				
			||||||
 | 
					    ["reminder-dashboard/dist/index.html", "lib/reminder-rs/static/index.html", "644"],
 | 
				
			||||||
    ["conf/default.env", "etc/reminder-rs/config.env", "600"],
 | 
					    ["conf/default.env", "etc/reminder-rs/config.env", "600"],
 | 
				
			||||||
    ["conf/Rocket.toml", "etc/reminder-rs/Rocket.toml", "600"],
 | 
					    ["conf/Rocket.toml", "etc/reminder-rs/Rocket.toml", "600"],
 | 
				
			||||||
    ["web/static/**/*", "lib/reminder-rs/static", "644"],
 | 
					    # ["nginx/reminder-rs", "etc/nginx/sites-available/reminder-rs", "755"]
 | 
				
			||||||
    ["web/templates/**/*", "lib/reminder-rs/templates", "644"],
 | 
					 | 
				
			||||||
    ["healthcheck", "lib/reminder-rs/healthcheck", "755"],
 | 
					 | 
				
			||||||
    ["cron.d/reminder_health", "etc/cron.d/reminder_health", "644"],
 | 
					 | 
				
			||||||
#    ["nginx/reminder-rs", "etc/nginx/sites-available/reminder-rs", "755"]
 | 
					 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
conf-files = [
 | 
					conf-files = [
 | 
				
			||||||
    "/etc/reminder-rs/config.env",
 | 
					    "/etc/reminder-rs/config.env",
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,6 +4,6 @@ ENV RUSTUP_HOME=/usr/local/rustup \
 | 
				
			|||||||
    CARGO_HOME=/usr/local/cargo \
 | 
					    CARGO_HOME=/usr/local/cargo \
 | 
				
			||||||
    PATH=/usr/local/cargo/bin:$PATH
 | 
					    PATH=/usr/local/cargo/bin:$PATH
 | 
				
			||||||
 | 
					
 | 
				
			||||||
RUN apt update && DEBIAN_FRONTEND=noninteractive TZ=Etc/UTC apt install -y gcc gcc-multilib cmake pkg-config libssl-dev curl mysql-client-8.0
 | 
					RUN apt update && DEBIAN_FRONTEND=noninteractive TZ=Etc/UTC apt install -y gcc gcc-multilib cmake pkg-config libssl-dev curl mysql-client-8.0 npm
 | 
				
			||||||
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --no-modify-path --profile minimal --default-toolchain nightly
 | 
					RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --no-modify-path --profile minimal --default-toolchain nightly
 | 
				
			||||||
RUN cargo install cargo-deb
 | 
					RUN cargo install cargo-deb
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										22
									
								
								Rocket.toml
									
									
									
									
									
								
							
							
						
						@@ -1,28 +1,28 @@
 | 
				
			|||||||
[default]
 | 
					[default]
 | 
				
			||||||
address = "0.0.0.0"
 | 
					address = "0.0.0.0"
 | 
				
			||||||
port = 18920
 | 
					port = 18920
 | 
				
			||||||
template_dir = "web/templates"
 | 
					template_dir = "templates"
 | 
				
			||||||
limits = { json = "10MiB" }
 | 
					limits = { json = "10MiB" }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[debug]
 | 
					[debug]
 | 
				
			||||||
secret_key = "tR8krio5FXTnnyIZNiJDXPondz0kI1v6X6BXZcBGIRY="
 | 
					secret_key = "tR8krio5FXTnnyIZNiJDXPondz0kI1v6X6BXZcBGIRY="
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[debug.tls]
 | 
					[debug.tls]
 | 
				
			||||||
certs = "web/private/rsa_sha256_cert.pem"
 | 
					certs = "private/rsa_sha256_cert.pem"
 | 
				
			||||||
key = "web/private/rsa_sha256_key.pem"
 | 
					key = "private/rsa_sha256_key.pem"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[debug.rsa_sha256.tls]
 | 
					[debug.rsa_sha256.tls]
 | 
				
			||||||
certs = "web/private/rsa_sha256_cert.pem"
 | 
					certs = "private/rsa_sha256_cert.pem"
 | 
				
			||||||
key = "web/private/rsa_sha256_key.pem"
 | 
					key = "private/rsa_sha256_key.pem"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[debug.ecdsa_nistp256_sha256.tls]
 | 
					[debug.ecdsa_nistp256_sha256.tls]
 | 
				
			||||||
certs = "web/private/ecdsa_nistp256_sha256_cert.pem"
 | 
					certs = "private/ecdsa_nistp256_sha256_cert.pem"
 | 
				
			||||||
key = "web/private/ecdsa_nistp256_sha256_key_pkcs8.pem"
 | 
					key = "private/ecdsa_nistp256_sha256_key_pkcs8.pem"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[debug.ecdsa_nistp384_sha384.tls]
 | 
					[debug.ecdsa_nistp384_sha384.tls]
 | 
				
			||||||
certs = "web/private/ecdsa_nistp384_sha384_cert.pem"
 | 
					certs = "private/ecdsa_nistp384_sha384_cert.pem"
 | 
				
			||||||
key = "web/private/ecdsa_nistp384_sha384_key_pkcs8.pem"
 | 
					key = "private/ecdsa_nistp384_sha384_key_pkcs8.pem"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[debug.ed25519.tls]
 | 
					[debug.ed25519.tls]
 | 
				
			||||||
certs = "web/private/ed25519_cert.pem"
 | 
					certs = "private/ed25519_cert.pem"
 | 
				
			||||||
key = "eb/private/ed25519_key.pem"
 | 
					key = "private/ed25519_key.pem"
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										10
									
								
								build.rs
									
									
									
									
									
								
							
							
						
						@@ -1,3 +1,13 @@
 | 
				
			|||||||
 | 
					use std::{path::Path, process::Command};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
fn main() {
 | 
					fn main() {
 | 
				
			||||||
    println!("cargo:rerun-if-changed=migrations");
 | 
					    println!("cargo:rerun-if-changed=migrations");
 | 
				
			||||||
 | 
					    println!("cargo:rerun-if-changed=reminder-dashboard");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Command::new("npm")
 | 
				
			||||||
 | 
					        .arg("run")
 | 
				
			||||||
 | 
					        .arg("build")
 | 
				
			||||||
 | 
					        .current_dir(Path::new("reminder-dashboard"))
 | 
				
			||||||
 | 
					        .spawn()
 | 
				
			||||||
 | 
					        .expect("Failed to build NPM");
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										46
									
								
								extract_derive/Cargo.lock
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,46 @@
 | 
				
			|||||||
 | 
					# This file is automatically @generated by Cargo.
 | 
				
			||||||
 | 
					# It is not intended for manual editing.
 | 
				
			||||||
 | 
					version = 3
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "extract_macro"
 | 
				
			||||||
 | 
					version = "0.1.0"
 | 
				
			||||||
 | 
					dependencies = [
 | 
				
			||||||
 | 
					 "quote",
 | 
				
			||||||
 | 
					 "syn",
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "proc-macro2"
 | 
				
			||||||
 | 
					version = "1.0.78"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae"
 | 
				
			||||||
 | 
					dependencies = [
 | 
				
			||||||
 | 
					 "unicode-ident",
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "quote"
 | 
				
			||||||
 | 
					version = "1.0.35"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef"
 | 
				
			||||||
 | 
					dependencies = [
 | 
				
			||||||
 | 
					 "proc-macro2",
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "syn"
 | 
				
			||||||
 | 
					version = "2.0.49"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "915aea9e586f80826ee59f8453c1101f9d1c4b3964cd2460185ee8e299ada496"
 | 
				
			||||||
 | 
					dependencies = [
 | 
				
			||||||
 | 
					 "proc-macro2",
 | 
				
			||||||
 | 
					 "quote",
 | 
				
			||||||
 | 
					 "unicode-ident",
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "unicode-ident"
 | 
				
			||||||
 | 
					version = "1.0.12"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
 | 
				
			||||||
							
								
								
									
										11
									
								
								extract_derive/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,11 @@
 | 
				
			|||||||
 | 
					[package]
 | 
				
			||||||
 | 
					name = "extract_derive"
 | 
				
			||||||
 | 
					version = "0.1.0"
 | 
				
			||||||
 | 
					edition = "2021"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[lib]
 | 
				
			||||||
 | 
					proc-macro = true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[dependencies]
 | 
				
			||||||
 | 
					quote = "1.0.35"
 | 
				
			||||||
 | 
					syn = { version = "2.0.49", features = ["full"] }
 | 
				
			||||||
							
								
								
									
										53
									
								
								extract_derive/src/lib.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,53 @@
 | 
				
			|||||||
 | 
					use proc_macro::TokenStream;
 | 
				
			||||||
 | 
					use syn::{spanned::Spanned, Data, Fields};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[proc_macro_derive(Extract)]
 | 
				
			||||||
 | 
					pub fn extract(input: TokenStream) -> TokenStream {
 | 
				
			||||||
 | 
					    let ast = syn::parse_macro_input!(input);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    impl_extract(&ast)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fn impl_extract(ast: &syn::DeriveInput) -> TokenStream {
 | 
				
			||||||
 | 
					    let name = &ast.ident;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    match &ast.data {
 | 
				
			||||||
 | 
					        // Dispatch over struct: extract args directly from context
 | 
				
			||||||
 | 
					        Data::Struct(st) => match &st.fields {
 | 
				
			||||||
 | 
					            Fields::Named(fields) => {
 | 
				
			||||||
 | 
					                let extracted = fields.named.iter().map(|field| {
 | 
				
			||||||
 | 
					                    let ident = &field.ident;
 | 
				
			||||||
 | 
					                    let ty = &field.ty;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    quote::quote_spanned! {field.span()=>
 | 
				
			||||||
 | 
					                        #ident : crate::utils::extract_arg!(ctx, #ident, #ty)
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                TokenStream::from(quote::quote! {
 | 
				
			||||||
 | 
					                    impl Extract for #name {
 | 
				
			||||||
 | 
					                        fn extract(ctx: crate::ApplicationContext) -> Self {
 | 
				
			||||||
 | 
					                            Self {
 | 
				
			||||||
 | 
					                                #(#extracted,)*
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                })
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            Fields::Unit => TokenStream::from(quote::quote! {
 | 
				
			||||||
 | 
					                impl Extract for #name {
 | 
				
			||||||
 | 
					                    fn extract(ctx: crate::ApplicationContext) -> Self {
 | 
				
			||||||
 | 
					                        Self {}
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }),
 | 
				
			||||||
 | 
					            _ => {
 | 
				
			||||||
 | 
					                panic!("Only named/unit structs can derive Extract");
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        _ => {
 | 
				
			||||||
 | 
					            panic!("Only structs can derive Extract");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										13
									
								
								healthcheck
									
									
									
									
									
								
							
							
						
						@@ -1,13 +0,0 @@
 | 
				
			|||||||
#!/bin/bash
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export $(grep -v '^#' /etc/reminder-rs/config.env | xargs -d '\n')
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
REGEX='mysql://([A-Za-z]+)@(.+)/(.+)'
 | 
					 | 
				
			||||||
[[ $DATABASE_URL =~ $REGEX ]]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
VAR=$(mysql -u "${BASH_REMATCH[1]}" -h "${BASH_REMATCH[2]}" -N -D "${BASH_REMATCH[3]}" -e "SELECT COUNT(1) FROM reminders WHERE utc_time < NOW() - INTERVAL 10 MINUTE AND enabled = 1 AND status = 'pending'")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
if [ "$VAR" -gt 0 ]
 | 
					 | 
				
			||||||
then
 | 
					 | 
				
			||||||
  echo "This is to inform that there is a reminder backlog which must be resolved." | mail -s "Backlog: $VAR" "$REPORT_EMAIL"
 | 
					 | 
				
			||||||
fi
 | 
					 | 
				
			||||||
@@ -1,19 +0,0 @@
 | 
				
			|||||||
-- Drop existing constraint
 | 
					 | 
				
			||||||
ALTER TABLE `reminders` DROP CONSTRAINT `reminders_ibfk_1`;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
ALTER TABLE `reminders` MODIFY COLUMN `channel_id` INT UNSIGNED;
 | 
					 | 
				
			||||||
ALTER TABLE `reminders` ADD COLUMN `guild_id` INT UNSIGNED;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
ALTER TABLE `reminders`
 | 
					 | 
				
			||||||
    ADD CONSTRAINT `guild_id_fk`
 | 
					 | 
				
			||||||
        FOREIGN KEY (`guild_id`)
 | 
					 | 
				
			||||||
        REFERENCES `guilds`(`id`)
 | 
					 | 
				
			||||||
        ON DELETE CASCADE;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
ALTER TABLE `reminders`
 | 
					 | 
				
			||||||
    ADD CONSTRAINT `channel_id_fk`
 | 
					 | 
				
			||||||
        FOREIGN KEY (`channel_id`)
 | 
					 | 
				
			||||||
        REFERENCES `channels`(`id`)
 | 
					 | 
				
			||||||
        ON DELETE SET NULL;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
UPDATE `reminders` SET `guild_id` = (SELECT guilds.`id` FROM `channels` INNER JOIN `guilds` ON channels.guild_id = guilds.id WHERE reminders.channel_id = channels.id);
 | 
					 | 
				
			||||||
@@ -1,4 +0,0 @@
 | 
				
			|||||||
ALTER TABLE reminders ADD COLUMN `status_change_time` DATETIME;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
-- This is a best-guess as to the status change time.
 | 
					 | 
				
			||||||
UPDATE reminders SET `status_change_time` = `utc_time` WHERE `status` != 'pending';
 | 
					 | 
				
			||||||
@@ -0,0 +1,3 @@
 | 
				
			|||||||
 | 
					ALTER TABLE `reminder_template` ADD COLUMN `interval_seconds` INT UNSIGNED;
 | 
				
			||||||
 | 
					ALTER TABLE `reminder_template` ADD COLUMN `interval_days` INT UNSIGNED;
 | 
				
			||||||
 | 
					ALTER TABLE `reminder_template` ADD COLUMN `interval_months` INT UNSIGNED;
 | 
				
			||||||
							
								
								
									
										50
									
								
								migrations/20240210133900_macro_restructure.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,50 @@
 | 
				
			|||||||
 | 
					CREATE TABLE command_macro (
 | 
				
			||||||
 | 
					    id INT UNSIGNED AUTO_INCREMENT,
 | 
				
			||||||
 | 
					    guild_id INT UNSIGNED NOT NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    name VARCHAR(100) NOT NULL,
 | 
				
			||||||
 | 
					    description VARCHAR(100),
 | 
				
			||||||
 | 
					    commands JSON NOT NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    FOREIGN KEY (guild_id) REFERENCES guilds(id) ON DELETE CASCADE,
 | 
				
			||||||
 | 
					    PRIMARY KEY (id)
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# New JSON structure is {command_name: "Remind", "<option name>": "<option value>", ...}
 | 
				
			||||||
 | 
					INSERT INTO command_macro (guild_id, description, name, commands)
 | 
				
			||||||
 | 
					SELECT
 | 
				
			||||||
 | 
					    guild_id,
 | 
				
			||||||
 | 
					    description,
 | 
				
			||||||
 | 
					    name,
 | 
				
			||||||
 | 
					    (
 | 
				
			||||||
 | 
					        SELECT JSON_ARRAYAGG(
 | 
				
			||||||
 | 
					            (
 | 
				
			||||||
 | 
					                SELECT JSON_OBJECTAGG(t2.name, t2.value)
 | 
				
			||||||
 | 
					                FROM JSON_TABLE(
 | 
				
			||||||
 | 
					                    JSON_ARRAY_APPEND(t1.options, '$', JSON_OBJECT('name', 'command_name', 'value', t1.command_name)),
 | 
				
			||||||
 | 
					                    '$[*]' COLUMNS (
 | 
				
			||||||
 | 
					                        name VARCHAR(64) PATH '$.name' ERROR ON ERROR,
 | 
				
			||||||
 | 
					                        value TEXT PATH '$.value' ERROR ON ERROR
 | 
				
			||||||
 | 
					                    )) AS t2
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        FROM macro m2
 | 
				
			||||||
 | 
					        JOIN JSON_TABLE(
 | 
				
			||||||
 | 
					            commands,
 | 
				
			||||||
 | 
					            '$[*]' COLUMNS (
 | 
				
			||||||
 | 
					                command_name VARCHAR(64) PATH '$.command_name' ERROR ON ERROR,
 | 
				
			||||||
 | 
					                options JSON PATH '$.options' ERROR ON ERROR
 | 
				
			||||||
 | 
					            )) AS t1
 | 
				
			||||||
 | 
					        WHERE m1.id = m2.id
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					FROM macro m1;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# # Check which commands are used in macros
 | 
				
			||||||
 | 
					# SELECT DISTINCT command_name
 | 
				
			||||||
 | 
					# FROM macro m2
 | 
				
			||||||
 | 
					# JOIN JSON_TABLE(
 | 
				
			||||||
 | 
					#     commands,
 | 
				
			||||||
 | 
					#     '$[*]' COLUMNS (
 | 
				
			||||||
 | 
					#         command_name VARCHAR(64) PATH '$.command_name' ERROR ON ERROR,
 | 
				
			||||||
 | 
					#         options JSON PATH '$.options' ERROR ON ERROR
 | 
				
			||||||
 | 
					#     )) AS t1
 | 
				
			||||||
							
								
								
									
										5
									
								
								migrations/20240303125837_add_indexes.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,5 @@
 | 
				
			|||||||
 | 
					-- Add migration script here
 | 
				
			||||||
 | 
					ALTER TABLE reminders
 | 
				
			||||||
 | 
					    ADD INDEX `utc_time_index` (`utc_time`);
 | 
				
			||||||
 | 
					ALTER TABLE reminders
 | 
				
			||||||
 | 
					    ADD INDEX `status_index` (`status`);
 | 
				
			||||||
							
								
								
									
										53
									
								
								nginx/reminder-bot
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,53 @@
 | 
				
			|||||||
 | 
					server {
 | 
				
			||||||
 | 
					    server_name www.reminder-bot.com;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return 301 https://reminder-bot.com$request_uri;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					server {
 | 
				
			||||||
 | 
					    listen 80;
 | 
				
			||||||
 | 
					    server_name beta.reminder-bot.com;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return 301 https://reminder-bot.com$request_uri;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					server {
 | 
				
			||||||
 | 
					    listen 443 ssl;
 | 
				
			||||||
 | 
					    server_name beta.reminder-bot.com;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    ssl_certificate /etc/letsencrypt/live/beta.reminder-bot.com/fullchain.pem;
 | 
				
			||||||
 | 
					    ssl_certificate_key /etc/letsencrypt/live/beta.reminder-bot.com/privkey.pem;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return 301 https://reminder-bot.com$request_uri;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					server {
 | 
				
			||||||
 | 
					    listen 443 ssl;
 | 
				
			||||||
 | 
					    server_name reminder-bot.com;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    ssl_certificate /etc/letsencrypt/live/reminder-bot.com/fullchain.pem;
 | 
				
			||||||
 | 
					    ssl_certificate_key /etc/letsencrypt/live/reminder-bot.com/privkey.pem;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    access_log /var/log/nginx/access.log;
 | 
				
			||||||
 | 
					    error_log /var/log/nginx/error.log;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    proxy_buffer_size 128k;
 | 
				
			||||||
 | 
					    proxy_buffers 4 256k;
 | 
				
			||||||
 | 
					    proxy_busy_buffers_size 256k;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    client_max_body_size 10M;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    location / {
 | 
				
			||||||
 | 
					        proxy_pass http://localhost:18920;
 | 
				
			||||||
 | 
					        proxy_redirect off;
 | 
				
			||||||
 | 
					        proxy_set_header Host $host;
 | 
				
			||||||
 | 
					        proxy_set_header X-Real-IP $remote_addr;
 | 
				
			||||||
 | 
					        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
 | 
				
			||||||
 | 
					        proxy_set_header X-Forwarded-Proto $scheme;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    location /static {
 | 
				
			||||||
 | 
					        alias /var/www/reminder-rs/static;
 | 
				
			||||||
 | 
					        expires 30d;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,41 +0,0 @@
 | 
				
			|||||||
server {
 | 
					 | 
				
			||||||
        server_name www.reminder-bot.com;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return 301 $scheme://reminder-bot.com$request_uri;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
server {
 | 
					 | 
				
			||||||
        listen 80;
 | 
					 | 
				
			||||||
        server_name reminder-bot.com;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	    return 301 https://reminder-bot.com$request_uri;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
server {
 | 
					 | 
				
			||||||
        listen 443 ssl;
 | 
					 | 
				
			||||||
        server_name reminder-bot.com;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        ssl_certificate /etc/letsencrypt/live/reminder-bot.com/fullchain.pem;
 | 
					 | 
				
			||||||
        ssl_certificate_key /etc/letsencrypt/live/reminder-bot.com/privkey.pem;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        access_log /var/log/nginx/access.log;
 | 
					 | 
				
			||||||
        error_log /var/log/nginx/error.log;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        proxy_buffer_size 128k;
 | 
					 | 
				
			||||||
        proxy_buffers 4 256k;
 | 
					 | 
				
			||||||
        proxy_busy_buffers_size 256k;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        location / {
 | 
					 | 
				
			||||||
                proxy_pass http://localhost:18920;
 | 
					 | 
				
			||||||
                proxy_redirect off;
 | 
					 | 
				
			||||||
                proxy_set_header Host $host;
 | 
					 | 
				
			||||||
                proxy_set_header X-Real-IP $remote_addr;
 | 
					 | 
				
			||||||
                proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
 | 
					 | 
				
			||||||
		        proxy_set_header X-Forwarded-Proto $scheme;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        location /static {
 | 
					 | 
				
			||||||
                alias /var/www/reminder-rs/static;
 | 
					 | 
				
			||||||
                expires 30d;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,16 +0,0 @@
 | 
				
			|||||||
[package]
 | 
					 | 
				
			||||||
name = "postman"
 | 
					 | 
				
			||||||
version = "0.1.0"
 | 
					 | 
				
			||||||
edition = "2021"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
[dependencies]
 | 
					 | 
				
			||||||
tokio = { version = "1", features = ["process", "full"] }
 | 
					 | 
				
			||||||
regex = "1.9"
 | 
					 | 
				
			||||||
log = "0.4"
 | 
					 | 
				
			||||||
chrono = "0.4"
 | 
					 | 
				
			||||||
chrono-tz = { version = "0.8", features = ["serde"] }
 | 
					 | 
				
			||||||
lazy_static = "1.4"
 | 
					 | 
				
			||||||
num-integer = "0.1"
 | 
					 | 
				
			||||||
serde = "1.0"
 | 
					 | 
				
			||||||
sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "macros", "mysql", "bigdecimal", "chrono", "json"]}
 | 
					 | 
				
			||||||
serenity = { version = "0.11", default-features = false, features = ["builder", "cache", "client", "gateway", "http", "model", "utils", "rustls_backend"] }
 | 
					 | 
				
			||||||
@@ -1,800 +0,0 @@
 | 
				
			|||||||
use std::env;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use chrono::{DateTime, Days, Duration, Months};
 | 
					 | 
				
			||||||
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, HttpError},
 | 
					 | 
				
			||||||
    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();
 | 
					 | 
				
			||||||
    pub static ref LOG_TO_DATABASE: bool = env::var("LOG_TO_DATABASE").map_or(true, |v| v == "1");
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
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").map(|m| m.as_str().parse::<i64>().ok()).flatten();
 | 
					 | 
				
			||||||
        let format = caps.name("format").map(|m| m.as_str());
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if let (Some(final_time), Some(format)) = (final_time, format) {
 | 
					 | 
				
			||||||
            match NaiveDateTime::from_timestamp_opt(final_time, 0) {
 | 
					 | 
				
			||||||
                Some(dt) => {
 | 
					 | 
				
			||||||
                    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)
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                None => String::new(),
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
            String::new()
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    TIMENOW_REGEX
 | 
					 | 
				
			||||||
        .replace(&new, |caps: &Captures| {
 | 
					 | 
				
			||||||
            let timezone = caps.name("timezone").map(|m| m.as_str().parse::<Tz>().ok()).flatten();
 | 
					 | 
				
			||||||
            let format = caps.name("format").map(|m| m.as_str());
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if let (Some(timezone), Some(format)) = (timezone, format) {
 | 
					 | 
				
			||||||
                let now = Utc::now().with_timezone(&timezone);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                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> {
 | 
					 | 
				
			||||||
        match 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
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            Ok(mut embed) => {
 | 
					 | 
				
			||||||
                embed.title = substitute(&embed.title);
 | 
					 | 
				
			||||||
                embed.description = substitute(&embed.description);
 | 
					 | 
				
			||||||
                embed.footer = substitute(&embed.footer);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                embed.fields.iter_mut().for_each(|field| {
 | 
					 | 
				
			||||||
                    field.title = substitute(&field.title);
 | 
					 | 
				
			||||||
                    field.value = substitute(&field.value);
 | 
					 | 
				
			||||||
                });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                if embed.has_content() {
 | 
					 | 
				
			||||||
                    Some(embed)
 | 
					 | 
				
			||||||
                } else {
 | 
					 | 
				
			||||||
                    None
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            Err(e) => {
 | 
					 | 
				
			||||||
                warn!("Error loading embed from reminder: {:?}", e);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                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
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
pub struct Reminder {
 | 
					 | 
				
			||||||
    id: u32,
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    channel_id: Option<u64>,
 | 
					 | 
				
			||||||
    webhook_id: Option<u64>,
 | 
					 | 
				
			||||||
    webhook_token: Option<String>,
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    channel_paused: Option<bool>,
 | 
					 | 
				
			||||||
    channel_paused_until: Option<NaiveDateTime>,
 | 
					 | 
				
			||||||
    enabled: bool,
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    tts: bool,
 | 
					 | 
				
			||||||
    pin: bool,
 | 
					 | 
				
			||||||
    content: String,
 | 
					 | 
				
			||||||
    attachment: Option<Vec<u8>>,
 | 
					 | 
				
			||||||
    attachment_name: Option<String>,
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    utc_time: DateTime<Utc>,
 | 
					 | 
				
			||||||
    timezone: String,
 | 
					 | 
				
			||||||
    restartable: bool,
 | 
					 | 
				
			||||||
    expires: Option<DateTime<Utc>>,
 | 
					 | 
				
			||||||
    interval_seconds: Option<u32>,
 | 
					 | 
				
			||||||
    interval_days: 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> {
 | 
					 | 
				
			||||||
        match sqlx::query_as_unchecked!(
 | 
					 | 
				
			||||||
            Reminder,
 | 
					 | 
				
			||||||
            r#"
 | 
					 | 
				
			||||||
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_days` AS 'interval_days',
 | 
					 | 
				
			||||||
    reminders.`interval_months` AS 'interval_months',
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    reminders.`avatar` AS avatar,
 | 
					 | 
				
			||||||
    reminders.`username` AS username
 | 
					 | 
				
			||||||
FROM
 | 
					 | 
				
			||||||
    reminders
 | 
					 | 
				
			||||||
LEFT JOIN
 | 
					 | 
				
			||||||
    channels
 | 
					 | 
				
			||||||
ON
 | 
					 | 
				
			||||||
    reminders.channel_id = channels.id
 | 
					 | 
				
			||||||
WHERE
 | 
					 | 
				
			||||||
    reminders.`status` = 'pending' AND
 | 
					 | 
				
			||||||
    reminders.`id` IN (
 | 
					 | 
				
			||||||
        SELECT
 | 
					 | 
				
			||||||
            MIN(id)
 | 
					 | 
				
			||||||
        FROM
 | 
					 | 
				
			||||||
            reminders
 | 
					 | 
				
			||||||
        WHERE
 | 
					 | 
				
			||||||
            reminders.`utc_time` <= NOW() AND
 | 
					 | 
				
			||||||
            `status` = 'pending' AND
 | 
					 | 
				
			||||||
            (
 | 
					 | 
				
			||||||
                reminders.`interval_seconds` IS NOT NULL
 | 
					 | 
				
			||||||
                OR reminders.`interval_months` IS NOT NULL
 | 
					 | 
				
			||||||
                OR reminders.`interval_days` IS NOT NULL
 | 
					 | 
				
			||||||
                OR reminders.enabled
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
        GROUP BY channel_id
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
    "#,
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        .fetch_all(pool)
 | 
					 | 
				
			||||||
        .await
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            Ok(reminders) => reminders
 | 
					 | 
				
			||||||
                .into_iter()
 | 
					 | 
				
			||||||
                .map(|mut rem| {
 | 
					 | 
				
			||||||
                    rem.content = substitute(&rem.content);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    rem
 | 
					 | 
				
			||||||
                })
 | 
					 | 
				
			||||||
                .collect::<Vec<Self>>(),
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            Err(e) => {
 | 
					 | 
				
			||||||
                warn!("Could not fetch reminders: {:?}", e);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                vec![]
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    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()
 | 
					 | 
				
			||||||
            || self.interval_days.is_some()
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            // If all intervals are zero then dont care
 | 
					 | 
				
			||||||
            if self.interval_seconds == Some(0)
 | 
					 | 
				
			||||||
                && self.interval_days == Some(0)
 | 
					 | 
				
			||||||
                && self.interval_months == Some(0)
 | 
					 | 
				
			||||||
            {
 | 
					 | 
				
			||||||
                self.set_sent(pool).await;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            let now = Utc::now();
 | 
					 | 
				
			||||||
            let mut updated_reminder_time =
 | 
					 | 
				
			||||||
                self.utc_time.with_timezone(&self.timezone.parse().unwrap_or(Tz::UTC));
 | 
					 | 
				
			||||||
            let mut fail_count = 0;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            while updated_reminder_time < now && fail_count < 4 {
 | 
					 | 
				
			||||||
                if let Some(interval) = self.interval_months {
 | 
					 | 
				
			||||||
                    if interval != 0 {
 | 
					 | 
				
			||||||
                        updated_reminder_time = updated_reminder_time
 | 
					 | 
				
			||||||
                            .checked_add_months(Months::new(interval))
 | 
					 | 
				
			||||||
                            .unwrap_or_else(|| {
 | 
					 | 
				
			||||||
                                warn!(
 | 
					 | 
				
			||||||
                                    "{}: Could not add {} months to a reminder",
 | 
					 | 
				
			||||||
                                    interval, self.id
 | 
					 | 
				
			||||||
                                );
 | 
					 | 
				
			||||||
                                fail_count += 1;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                                updated_reminder_time
 | 
					 | 
				
			||||||
                            });
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                if let Some(interval) = self.interval_days {
 | 
					 | 
				
			||||||
                    if interval != 0 {
 | 
					 | 
				
			||||||
                        updated_reminder_time = updated_reminder_time
 | 
					 | 
				
			||||||
                            .checked_add_days(Days::new(interval as u64))
 | 
					 | 
				
			||||||
                            .unwrap_or_else(|| {
 | 
					 | 
				
			||||||
                                warn!("{}: Could not add {} days to a reminder", self.id, interval);
 | 
					 | 
				
			||||||
                                fail_count += 1;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                                updated_reminder_time
 | 
					 | 
				
			||||||
                            })
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                if let Some(interval) = self.interval_seconds {
 | 
					 | 
				
			||||||
                    updated_reminder_time += Duration::seconds(interval as i64);
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if fail_count >= 4 {
 | 
					 | 
				
			||||||
                self.log_error(
 | 
					 | 
				
			||||||
                    pool,
 | 
					 | 
				
			||||||
                    "Failed to update 4 times and so is being deleted",
 | 
					 | 
				
			||||||
                    None::<&'static str>,
 | 
					 | 
				
			||||||
                )
 | 
					 | 
				
			||||||
                .await;
 | 
					 | 
				
			||||||
                self.set_failed(pool, "Failed to update 4 times and so is being deleted").await;
 | 
					 | 
				
			||||||
            } else if self.expires.map_or(false, |expires| updated_reminder_time > expires) {
 | 
					 | 
				
			||||||
                self.set_sent(pool).await;
 | 
					 | 
				
			||||||
            } else {
 | 
					 | 
				
			||||||
                sqlx::query!(
 | 
					 | 
				
			||||||
                    "
 | 
					 | 
				
			||||||
                    UPDATE reminders SET `utc_time` = ? WHERE `id` = ?
 | 
					 | 
				
			||||||
                    ",
 | 
					 | 
				
			||||||
                    updated_reminder_time.with_timezone(&Utc),
 | 
					 | 
				
			||||||
                    self.id
 | 
					 | 
				
			||||||
                )
 | 
					 | 
				
			||||||
                .execute(pool)
 | 
					 | 
				
			||||||
                .await
 | 
					 | 
				
			||||||
                .expect(&format!("Could not update time on Reminder {}", self.id));
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
            self.set_sent(pool).await;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    async fn log_error(
 | 
					 | 
				
			||||||
        &self,
 | 
					 | 
				
			||||||
        pool: impl Executor<'_, Database = Database> + Copy,
 | 
					 | 
				
			||||||
        error: &'static str,
 | 
					 | 
				
			||||||
        debug_info: Option<impl std::fmt::Debug>,
 | 
					 | 
				
			||||||
    ) {
 | 
					 | 
				
			||||||
        let message = match debug_info {
 | 
					 | 
				
			||||||
            Some(info) => format!(
 | 
					 | 
				
			||||||
                "{}
 | 
					 | 
				
			||||||
{:?}",
 | 
					 | 
				
			||||||
                error, info
 | 
					 | 
				
			||||||
            ),
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            None => error.to_string(),
 | 
					 | 
				
			||||||
        };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        error!("[Reminder {}] {}", self.id, message);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if *LOG_TO_DATABASE {
 | 
					 | 
				
			||||||
            sqlx::query!(
 | 
					 | 
				
			||||||
                "
 | 
					 | 
				
			||||||
                INSERT INTO stat (type, reminder_id, message)
 | 
					 | 
				
			||||||
                VALUES ('reminder_failed', ?, ?)
 | 
					 | 
				
			||||||
                ",
 | 
					 | 
				
			||||||
                self.id,
 | 
					 | 
				
			||||||
                message,
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
            .execute(pool)
 | 
					 | 
				
			||||||
            .await
 | 
					 | 
				
			||||||
            .expect("Could not log error to database");
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    async fn log_success(&self, pool: impl Executor<'_, Database = Database> + Copy) {
 | 
					 | 
				
			||||||
        if *LOG_TO_DATABASE {
 | 
					 | 
				
			||||||
            sqlx::query!(
 | 
					 | 
				
			||||||
                "
 | 
					 | 
				
			||||||
                INSERT INTO stat (type, reminder_id)
 | 
					 | 
				
			||||||
                VALUES ('reminder_sent', ?)
 | 
					 | 
				
			||||||
                ",
 | 
					 | 
				
			||||||
                self.id,
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
            .execute(pool)
 | 
					 | 
				
			||||||
            .await
 | 
					 | 
				
			||||||
            .expect("Could not log success to database");
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    async fn set_sent(&self, pool: impl Executor<'_, Database = Database> + Copy) {
 | 
					 | 
				
			||||||
        sqlx::query!(
 | 
					 | 
				
			||||||
            "
 | 
					 | 
				
			||||||
            UPDATE reminders
 | 
					 | 
				
			||||||
            SET `status` = 'sent', `status_change_time` = NOW()
 | 
					 | 
				
			||||||
            WHERE `id` = ?
 | 
					 | 
				
			||||||
            ",
 | 
					 | 
				
			||||||
            self.id
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        .execute(pool)
 | 
					 | 
				
			||||||
        .await
 | 
					 | 
				
			||||||
        .expect(&format!("Could not delete Reminder {}", self.id));
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    async fn set_failed(
 | 
					 | 
				
			||||||
        &self,
 | 
					 | 
				
			||||||
        pool: impl Executor<'_, Database = Database> + Copy,
 | 
					 | 
				
			||||||
        message: &'static str,
 | 
					 | 
				
			||||||
    ) {
 | 
					 | 
				
			||||||
        sqlx::query!(
 | 
					 | 
				
			||||||
            "
 | 
					 | 
				
			||||||
            UPDATE reminders
 | 
					 | 
				
			||||||
            SET `status` = 'failed', `status_message` = ?, `status_change_time` = NOW()
 | 
					 | 
				
			||||||
            WHERE `id` = ?
 | 
					 | 
				
			||||||
            ",
 | 
					 | 
				
			||||||
            message,
 | 
					 | 
				
			||||||
            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>) {
 | 
					 | 
				
			||||||
        if let Some(channel_id) = self.channel_id {
 | 
					 | 
				
			||||||
            let _ = http.as_ref().pin_message(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,
 | 
					 | 
				
			||||||
            channel_id: u64,
 | 
					 | 
				
			||||||
            reminder: &Reminder,
 | 
					 | 
				
			||||||
            embed: Option<CreateEmbed>,
 | 
					 | 
				
			||||||
        ) -> Result<()> {
 | 
					 | 
				
			||||||
            let channel = ChannelId(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 {
 | 
					 | 
				
			||||||
                        if !username.is_empty() {
 | 
					 | 
				
			||||||
                            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),
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        match self.channel_id {
 | 
					 | 
				
			||||||
            Some(channel_id) => {
 | 
					 | 
				
			||||||
                if self.enabled
 | 
					 | 
				
			||||||
                    && !(self.channel_paused.unwrap_or(false)
 | 
					 | 
				
			||||||
                        && 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 for reminder {}: {:?}", self.id, webhook_res);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                            self.reset_webhook(pool).await;
 | 
					 | 
				
			||||||
                            send_to_channel(cache_http, channel_id, &self, embed).await
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
                    } else {
 | 
					 | 
				
			||||||
                        send_to_channel(cache_http, channel_id, &self, embed).await
 | 
					 | 
				
			||||||
                    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    if let Err(e) = result {
 | 
					 | 
				
			||||||
                        if let Error::Http(error) = e {
 | 
					 | 
				
			||||||
                            if let HttpError::UnsuccessfulRequest(http_error) = *error {
 | 
					 | 
				
			||||||
                                match http_error.error.code {
 | 
					 | 
				
			||||||
                                    10003 => {
 | 
					 | 
				
			||||||
                                        self.log_error(
 | 
					 | 
				
			||||||
                                            pool,
 | 
					 | 
				
			||||||
                                            "Could not be sent as channel does not exist",
 | 
					 | 
				
			||||||
                                            None::<&'static str>,
 | 
					 | 
				
			||||||
                                        )
 | 
					 | 
				
			||||||
                                        .await;
 | 
					 | 
				
			||||||
                                        self.set_failed(
 | 
					 | 
				
			||||||
                                            pool,
 | 
					 | 
				
			||||||
                                            "Could not be sent as channel does not exist",
 | 
					 | 
				
			||||||
                                        )
 | 
					 | 
				
			||||||
                                        .await;
 | 
					 | 
				
			||||||
                                    }
 | 
					 | 
				
			||||||
                                    10004 => {
 | 
					 | 
				
			||||||
                                        self.log_error(
 | 
					 | 
				
			||||||
                                            pool,
 | 
					 | 
				
			||||||
                                            "Could not be sent as guild does not exist",
 | 
					 | 
				
			||||||
                                            None::<&'static str>,
 | 
					 | 
				
			||||||
                                        )
 | 
					 | 
				
			||||||
                                        .await;
 | 
					 | 
				
			||||||
                                        self.set_failed(
 | 
					 | 
				
			||||||
                                            pool,
 | 
					 | 
				
			||||||
                                            "Could not be sent as guild does not exist",
 | 
					 | 
				
			||||||
                                        )
 | 
					 | 
				
			||||||
                                        .await;
 | 
					 | 
				
			||||||
                                    }
 | 
					 | 
				
			||||||
                                    50001 => {
 | 
					 | 
				
			||||||
                                        self.log_error(
 | 
					 | 
				
			||||||
                                            pool,
 | 
					 | 
				
			||||||
                                            "Could not be sent as missing access",
 | 
					 | 
				
			||||||
                                            None::<&'static str>,
 | 
					 | 
				
			||||||
                                        )
 | 
					 | 
				
			||||||
                                        .await;
 | 
					 | 
				
			||||||
                                        self.set_failed(
 | 
					 | 
				
			||||||
                                            pool,
 | 
					 | 
				
			||||||
                                            "Could not be sent as missing access",
 | 
					 | 
				
			||||||
                                        )
 | 
					 | 
				
			||||||
                                        .await;
 | 
					 | 
				
			||||||
                                    }
 | 
					 | 
				
			||||||
                                    50007 => {
 | 
					 | 
				
			||||||
                                        self.log_error(
 | 
					 | 
				
			||||||
                                            pool,
 | 
					 | 
				
			||||||
                                            "Could not be sent as user has DMs disabled",
 | 
					 | 
				
			||||||
                                            None::<&'static str>,
 | 
					 | 
				
			||||||
                                        )
 | 
					 | 
				
			||||||
                                        .await;
 | 
					 | 
				
			||||||
                                        self.set_failed(
 | 
					 | 
				
			||||||
                                            pool,
 | 
					 | 
				
			||||||
                                            "Could not be sent as user has DMs disabled",
 | 
					 | 
				
			||||||
                                        )
 | 
					 | 
				
			||||||
                                        .await;
 | 
					 | 
				
			||||||
                                    }
 | 
					 | 
				
			||||||
                                    50013 => {
 | 
					 | 
				
			||||||
                                        self.log_error(
 | 
					 | 
				
			||||||
                                            pool,
 | 
					 | 
				
			||||||
                                            "Could not be sent as permissions are invalid",
 | 
					 | 
				
			||||||
                                            None::<&'static str>,
 | 
					 | 
				
			||||||
                                        )
 | 
					 | 
				
			||||||
                                        .await;
 | 
					 | 
				
			||||||
                                        self.set_failed(
 | 
					 | 
				
			||||||
                                            pool,
 | 
					 | 
				
			||||||
                                            "Could not be sent as permissions are invalid",
 | 
					 | 
				
			||||||
                                        )
 | 
					 | 
				
			||||||
                                        .await;
 | 
					 | 
				
			||||||
                                    }
 | 
					 | 
				
			||||||
                                    _ => {
 | 
					 | 
				
			||||||
                                        self.log_error(
 | 
					 | 
				
			||||||
                                            pool,
 | 
					 | 
				
			||||||
                                            "HTTP error sending reminder",
 | 
					 | 
				
			||||||
                                            Some(http_error),
 | 
					 | 
				
			||||||
                                        )
 | 
					 | 
				
			||||||
                                        .await;
 | 
					 | 
				
			||||||
                                        self.refresh(pool).await;
 | 
					 | 
				
			||||||
                                    }
 | 
					 | 
				
			||||||
                                }
 | 
					 | 
				
			||||||
                            } else {
 | 
					 | 
				
			||||||
                                self.log_error(pool, "(Likely) a parsing error", Some(error)).await;
 | 
					 | 
				
			||||||
                                self.refresh(pool).await;
 | 
					 | 
				
			||||||
                            }
 | 
					 | 
				
			||||||
                        } else {
 | 
					 | 
				
			||||||
                            self.log_error(pool, "Non-HTTP error", Some(e)).await;
 | 
					 | 
				
			||||||
                            self.refresh(pool).await;
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
                    } else {
 | 
					 | 
				
			||||||
                        self.log_success(pool).await;
 | 
					 | 
				
			||||||
                        self.refresh(pool).await;
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                } else {
 | 
					 | 
				
			||||||
                    info!("Reminder {} is paused", self.id);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    self.refresh(pool).await;
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            None => {
 | 
					 | 
				
			||||||
                info!("Reminder {} is orphaned", self.id);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                self.log_error(pool, "Orphaned", Option::<u8>::None).await;
 | 
					 | 
				
			||||||
                self.set_failed(pool, "Could not be sent as channel was deleted").await;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
							
								
								
									
										11
									
								
								recordable_derive/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,11 @@
 | 
				
			|||||||
 | 
					[package]
 | 
				
			||||||
 | 
					name = "recordable_derive"
 | 
				
			||||||
 | 
					version = "0.1.0"
 | 
				
			||||||
 | 
					edition = "2021"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[lib]
 | 
				
			||||||
 | 
					proc-macro = true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[dependencies]
 | 
				
			||||||
 | 
					quote = "1.0.35"
 | 
				
			||||||
 | 
					syn = { version = "2.0.49", features = ["full"] }
 | 
				
			||||||
							
								
								
									
										42
									
								
								recordable_derive/src/lib.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,42 @@
 | 
				
			|||||||
 | 
					use proc_macro::TokenStream;
 | 
				
			||||||
 | 
					use syn::{spanned::Spanned, Data};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Macro to allow Recordable to be implemented on an enum by dispatching over each variant.
 | 
				
			||||||
 | 
					#[proc_macro_derive(Recordable)]
 | 
				
			||||||
 | 
					pub fn extract(input: TokenStream) -> TokenStream {
 | 
				
			||||||
 | 
					    let ast = syn::parse_macro_input!(input);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    impl_recordable(&ast)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fn impl_recordable(ast: &syn::DeriveInput) -> TokenStream {
 | 
				
			||||||
 | 
					    let name = &ast.ident;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    match &ast.data {
 | 
				
			||||||
 | 
					        Data::Enum(en) => {
 | 
				
			||||||
 | 
					            let extracted = en.variants.iter().map(|var| {
 | 
				
			||||||
 | 
					                let ident = &var.ident;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                quote::quote_spanned! {var.span()=>
 | 
				
			||||||
 | 
					                    Self::#ident (opt) => opt.run(ctx).await?
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            TokenStream::from(quote::quote! {
 | 
				
			||||||
 | 
					                impl Recordable for #name {
 | 
				
			||||||
 | 
					                    async fn run(self, ctx: crate::Context<'_>) -> Result<(), crate::Error> {
 | 
				
			||||||
 | 
					                        match self {
 | 
				
			||||||
 | 
					                            #(#extracted,)*
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        Ok(())
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        _ => {
 | 
				
			||||||
 | 
					            panic!("Only enums can derive Recordable");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										2
									
								
								reminder-dashboard/.prettierrc.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,2 @@
 | 
				
			|||||||
 | 
					printWidth = 100
 | 
				
			||||||
 | 
					tabWidth = 4
 | 
				
			||||||
							
								
								
									
										19
									
								
								reminder-dashboard/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,19 @@
 | 
				
			|||||||
 | 
					# reminder-dashboard
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					The re-re-rewrite of the dashboard.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Why
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					The existing beta variant of the dashboard is written using vanilla JavaScript. This is fine,
 | 
				
			||||||
 | 
					but annoying to update. This would've been okay if I was more dedicated to updating the vanilla
 | 
				
			||||||
 | 
					JavaScript too, but I want to experiment with "new" things.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					This also allows me to expand my frontend skills, which is relevant to part of my job.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Developing
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					1. Run both `npm run dev` and `cargo run`
 | 
				
			||||||
 | 
					2. Symlink assets: assuming cloned
 | 
				
			||||||
 | 
					   into `$HOME`, `ln -s $HOME/reminder-bot/reminder-dashboard/dist/index.html $HOME/reminder-bot/web/static/index.html`
 | 
				
			||||||
 | 
					   and
 | 
				
			||||||
 | 
					   `ln -s $HOME/reminder-bot/reminder-dashboard/dist/static/assets $HOME/reminder-bot/web/static/assets`
 | 
				
			||||||
							
								
								
									
										34
									
								
								reminder-dashboard/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,34 @@
 | 
				
			|||||||
 | 
					<!DOCTYPE html>
 | 
				
			||||||
 | 
					<html lang="EN">
 | 
				
			||||||
 | 
					<head>
 | 
				
			||||||
 | 
						<meta name="description" content="The most powerful Discord Reminders Bot">
 | 
				
			||||||
 | 
						<meta name="viewport" content="width=device-width, initial-scale=1.0">
 | 
				
			||||||
 | 
						<meta charset="UTF-8">
 | 
				
			||||||
 | 
						<meta name="yandex-verification" content="bb77b8681eb64a90"/>
 | 
				
			||||||
 | 
						<meta name="google-site-verification" content="7h7UVTeEe0AOzHiH3cFtsqMULYGN-zCZdMT_YCkW1Ho"/>
 | 
				
			||||||
 | 
						<!-- <meta http-equiv="Content-Security-Policy" content="default-src 'self'; img-src *; font-src fonts.gstatic.com 'self'"> -->
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						<!-- favicon -->
 | 
				
			||||||
 | 
						<link rel="apple-touch-icon" sizes="180x180"
 | 
				
			||||||
 | 
							  href="/static/favicon/apple-touch-icon.png">
 | 
				
			||||||
 | 
						<link rel="icon" type="image/png" sizes="32x32"
 | 
				
			||||||
 | 
							  href="/static/favicon/favicon-32x32.png">
 | 
				
			||||||
 | 
						<link rel="icon" type="image/png" sizes="16x16"
 | 
				
			||||||
 | 
							  href="/static/favicon/favicon-16x16.png">
 | 
				
			||||||
 | 
						<link rel="manifest" href="/static/site.webmanifest">
 | 
				
			||||||
 | 
						<meta name="msapplication-TileColor" content="#da532c">
 | 
				
			||||||
 | 
						<meta name="theme-color" content="#ffffff">
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						<title>Reminder Bot | Dashboard</title>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						<!-- styles -->
 | 
				
			||||||
 | 
						<link rel="stylesheet" href="/static/css/bulma.min.css">
 | 
				
			||||||
 | 
						<link rel="stylesheet" href="/static/css/fa.css">
 | 
				
			||||||
 | 
						<link rel="stylesheet" href="/static/css/font.css">
 | 
				
			||||||
 | 
						<link rel="stylesheet" href="/static/css/style.css">
 | 
				
			||||||
 | 
					</head>
 | 
				
			||||||
 | 
					<body>
 | 
				
			||||||
 | 
						<div id="app"></div>
 | 
				
			||||||
 | 
						<script type="module" src="/src/index.tsx"></script>
 | 
				
			||||||
 | 
					</body>
 | 
				
			||||||
 | 
					</html>
 | 
				
			||||||
							
								
								
									
										5626
									
								
								reminder-dashboard/package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										32
									
								
								reminder-dashboard/package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,32 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
						"name": "example",
 | 
				
			||||||
 | 
						"private": true,
 | 
				
			||||||
 | 
						"type": "module",
 | 
				
			||||||
 | 
						"scripts": {
 | 
				
			||||||
 | 
							"dev": "vite build --watch --mode development",
 | 
				
			||||||
 | 
							"build": "vite build",
 | 
				
			||||||
 | 
							"preview": "vite preview"
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						"dependencies": {
 | 
				
			||||||
 | 
							"axios": "^1.5.1",
 | 
				
			||||||
 | 
							"bulma": "^0.9.4",
 | 
				
			||||||
 | 
							"luxon": "^3.4.3",
 | 
				
			||||||
 | 
							"preact": "^10.13.1",
 | 
				
			||||||
 | 
							"react-colorful": "^5.6.1",
 | 
				
			||||||
 | 
							"react-query": "^3.39.3",
 | 
				
			||||||
 | 
							"tributejs": "^5.1.3",
 | 
				
			||||||
 | 
							"use-debounce": "^10.0.0",
 | 
				
			||||||
 | 
							"wouter": "^3.0"
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						"devDependencies": {
 | 
				
			||||||
 | 
							"@preact/preset-vite": "^2.5.0",
 | 
				
			||||||
 | 
							"@types/luxon": "^3.3.2",
 | 
				
			||||||
 | 
							"eslint": "^8.50.0",
 | 
				
			||||||
 | 
							"eslint-config-preact": "^1.3.0",
 | 
				
			||||||
 | 
							"prettier": "^3.0.3",
 | 
				
			||||||
 | 
							"react-datepicker": "^4.21.0",
 | 
				
			||||||
 | 
							"sass": "^1.71.1",
 | 
				
			||||||
 | 
							"typescript": "^5.2.2",
 | 
				
			||||||
 | 
							"vite": "^5.1"
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -15,6 +15,10 @@ div.reminderContent.is-collapsed .column.settings {
 | 
				
			|||||||
    display: none;
 | 
					    display: none;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					div.reminderContent.is-collapsed .reminder-settings {
 | 
				
			||||||
 | 
					    margin-bottom: 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
div.reminderContent.is-collapsed .button-row {
 | 
					div.reminderContent.is-collapsed .button-row {
 | 
				
			||||||
    display: none;
 | 
					    display: none;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -304,11 +308,6 @@ div.dashboard-sidebar {
 | 
				
			|||||||
    padding-right: 0;
 | 
					    padding-right: 0;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
div.dashboard-sidebar:not(.mobile-sidebar) {
 | 
					 | 
				
			||||||
    display: flex;
 | 
					 | 
				
			||||||
    flex-direction: column;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
ul.guildList {
 | 
					ul.guildList {
 | 
				
			||||||
    flex-grow: 1;
 | 
					    flex-grow: 1;
 | 
				
			||||||
    flex-shrink: 1;
 | 
					    flex-shrink: 1;
 | 
				
			||||||
@@ -318,6 +317,9 @@ ul.guildList {
 | 
				
			|||||||
div.dashboard-sidebar:not(.mobile-sidebar) .aside-footer {
 | 
					div.dashboard-sidebar:not(.mobile-sidebar) .aside-footer {
 | 
				
			||||||
    flex-shrink: 0;
 | 
					    flex-shrink: 0;
 | 
				
			||||||
    flex-grow: 0;
 | 
					    flex-grow: 0;
 | 
				
			||||||
 | 
					    position: fixed;
 | 
				
			||||||
 | 
					    bottom: 0;
 | 
				
			||||||
 | 
					    width: 226px;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
div.dashboard-sidebar svg {
 | 
					div.dashboard-sidebar svg {
 | 
				
			||||||
| 
		 Before Width: | Height: | Size: 7.2 KiB After Width: | Height: | Size: 7.2 KiB  | 
| 
		 Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB  | 
| 
		 Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 6.8 KiB  | 
| 
		 Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB  | 
| 
		 Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB  | 
| 
		 Before Width: | Height: | Size: 7.2 KiB After Width: | Height: | Size: 7.2 KiB  | 
| 
		 Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 4.9 KiB  | 
| 
		 Before Width: | Height: | Size: 762 B After Width: | Height: | Size: 762 B  | 
| 
		 Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB  | 
| 
		 Before Width: | Height: | Size: 323 KiB After Width: | Height: | Size: 323 KiB  | 
| 
		 Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 61 KiB  | 
| 
		 Before Width: | Height: | Size: 81 KiB After Width: | Height: | Size: 81 KiB  | 
| 
		 Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB  | 
| 
		 Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB  | 
| 
		 Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 55 KiB  | 
| 
		 Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.6 KiB  | 
| 
		 Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB  | 
| 
		 Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB  | 
| 
		 Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB  | 
| 
		 Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB  | 
| 
		 Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB  | 
| 
		 Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB  | 
| 
		 Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB  | 
| 
		 Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB  | 
| 
		 Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB  | 
| 
		 Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 65 KiB  | 
@@ -18,7 +18,6 @@ const $downloader = document.querySelector("a#downloader");
 | 
				
			|||||||
const $uploader = document.querySelector("input#uploader");
 | 
					const $uploader = document.querySelector("input#uploader");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
let channels = [];
 | 
					let channels = [];
 | 
				
			||||||
let reminderErrors = [];
 | 
					 | 
				
			||||||
let guildNames = {};
 | 
					let guildNames = {};
 | 
				
			||||||
let roles = [];
 | 
					let roles = [];
 | 
				
			||||||
let templates = {};
 | 
					let templates = {};
 | 
				
			||||||
@@ -34,11 +33,7 @@ let globalPatreon = false;
 | 
				
			|||||||
let guildPatreon = false;
 | 
					let guildPatreon = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function guildId() {
 | 
					function guildId() {
 | 
				
			||||||
    return document.querySelector("li > a.is-active").parentElement.dataset["guild"];
 | 
					    return document.querySelector(".guildList a.is-active").dataset["guild"];
 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function guildName() {
 | 
					 | 
				
			||||||
    return guildNames[guildId()];
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function colorToInt(r, g, b) {
 | 
					function colorToInt(r, g, b) {
 | 
				
			||||||
@@ -57,7 +52,7 @@ function switch_pane(selector) {
 | 
				
			|||||||
        el.classList.add("is-hidden");
 | 
					        el.classList.add("is-hidden");
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    document.querySelector(`*[data-name=${selector}]`).classList.remove("is-hidden");
 | 
					    document.getElementById(selector).classList.remove("is-hidden");
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function update_select(sel) {
 | 
					function update_select(sel) {
 | 
				
			||||||
@@ -228,11 +223,10 @@ async function fetch_reminders(guild_id) {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function serialize_reminder(node, mode) {
 | 
					async function serialize_reminder(node, mode) {
 | 
				
			||||||
    let interval, utc_time, expiration_time;
 | 
					    let utc_time, expiration_time;
 | 
				
			||||||
 | 
					    let interval = get_interval(node);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (mode !== "template") {
 | 
					    if (mode !== "template") {
 | 
				
			||||||
        interval = get_interval(node);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        utc_time = luxon.DateTime.fromISO(
 | 
					        utc_time = luxon.DateTime.fromISO(
 | 
				
			||||||
            node.querySelector('input[name="time"]').value
 | 
					            node.querySelector('input[name="time"]').value
 | 
				
			||||||
        ).setZone("UTC");
 | 
					        ).setZone("UTC");
 | 
				
			||||||
@@ -361,9 +355,9 @@ async function serialize_reminder(node, mode) {
 | 
				
			|||||||
        embed_title: embed_title,
 | 
					        embed_title: embed_title,
 | 
				
			||||||
        embed_fields: fields,
 | 
					        embed_fields: fields,
 | 
				
			||||||
        expires: expiration_time,
 | 
					        expires: expiration_time,
 | 
				
			||||||
        interval_seconds: mode !== "template" ? interval.seconds : null,
 | 
					        interval_seconds: interval.seconds,
 | 
				
			||||||
        interval_days: mode !== "template" ? interval.days : null,
 | 
					        interval_days: interval.days,
 | 
				
			||||||
        interval_months: mode !== "template" ? interval.months : null,
 | 
					        interval_months: interval.months,
 | 
				
			||||||
        name: node.querySelector('input[name="name"]').value,
 | 
					        name: node.querySelector('input[name="name"]').value,
 | 
				
			||||||
        tts: node.querySelector('input[name="tts"]').checked,
 | 
					        tts: node.querySelector('input[name="tts"]').checked,
 | 
				
			||||||
        username: node.querySelector('input[name="username"]').value,
 | 
					        username: node.querySelector('input[name="username"]').value,
 | 
				
			||||||
@@ -425,9 +419,9 @@ function deserialize_reminder(reminder, frame, mode) {
 | 
				
			|||||||
            .insertBefore(embed_field, lastChild);
 | 
					            .insertBefore(embed_field, lastChild);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (mode !== "template") {
 | 
					    if (reminder["interval_seconds"]) update_interval(frame);
 | 
				
			||||||
        if (reminder["interval_seconds"]) update_interval(frame);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (mode !== "template") {
 | 
				
			||||||
        let $enableBtn = frame.querySelector(".disable-enable");
 | 
					        let $enableBtn = frame.querySelector(".disable-enable");
 | 
				
			||||||
        $enableBtn.dataset["action"] = reminder["enabled"] ? "disable" : "enable";
 | 
					        $enableBtn.dataset["action"] = reminder["enabled"] ? "disable" : "enable";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -454,27 +448,21 @@ document.addEventListener("guildSwitched", async (e) => {
 | 
				
			|||||||
        .querySelectorAll(".patreon-only")
 | 
					        .querySelectorAll(".patreon-only")
 | 
				
			||||||
        .forEach((el) => el.classList.add("is-locked"));
 | 
					        .forEach((el) => el.classList.add("is-locked"));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let $li = document.querySelectorAll(`li[data-guild="${e.detail.guild_id}"]`);
 | 
					    let $anchor = document.querySelector(
 | 
				
			||||||
 | 
					        `.switch-pane[data-guild="${e.detail.guild_id}"]`
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if ($li.length === 0) {
 | 
					    let hasError = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if ($anchor === null) {
 | 
				
			||||||
        switch_pane("user-error");
 | 
					        switch_pane("user-error");
 | 
				
			||||||
 | 
					        hasError = true;
 | 
				
			||||||
        return;
 | 
					        return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    switch_pane(e.detail.pane);
 | 
					    switch_pane($anchor.dataset["pane"]);
 | 
				
			||||||
    reset_guild_pane();
 | 
					    reset_guild_pane();
 | 
				
			||||||
    document
 | 
					    $anchor.classList.add("is-active");
 | 
				
			||||||
        .querySelectorAll(`li[data-guild="${e.detail.guild_id}"] > a`)
 | 
					 | 
				
			||||||
        .forEach((el) => {
 | 
					 | 
				
			||||||
            el.classList.add("is-active");
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
    document
 | 
					 | 
				
			||||||
        .querySelectorAll(
 | 
					 | 
				
			||||||
            `li[data-guild="${e.detail.guild_id}"] *[data-pane="${e.detail.pane}"]`
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        .forEach((el) => {
 | 
					 | 
				
			||||||
            el.classList.add("is-active");
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (globalPatreon || (await fetch_patreon(e.detail.guild_id))) {
 | 
					    if (globalPatreon || (await fetch_patreon(e.detail.guild_id))) {
 | 
				
			||||||
        document
 | 
					        document
 | 
				
			||||||
@@ -482,26 +470,15 @@ document.addEventListener("guildSwitched", async (e) => {
 | 
				
			|||||||
            .forEach((el) => el.classList.remove("is-locked"));
 | 
					            .forEach((el) => el.classList.remove("is-locked"));
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const event = new CustomEvent("paneLoad", {
 | 
					    hasError = await fetch_channels(e.detail.guild_id);
 | 
				
			||||||
        detail: {
 | 
					 | 
				
			||||||
            guild_id: e.detail.guild_id,
 | 
					 | 
				
			||||||
            pane: e.detail.pane,
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
    document.dispatchEvent(event);
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
document.addEventListener("paneLoad", async (ev) => {
 | 
					 | 
				
			||||||
    const hasError = await fetch_channels(ev.detail.guild_id);
 | 
					 | 
				
			||||||
    if (!hasError) {
 | 
					    if (!hasError) {
 | 
				
			||||||
        fetch_roles(ev.detail.guild_id);
 | 
					        fetch_roles(e.detail.guild_id);
 | 
				
			||||||
        fetch_templates(ev.detail.guild_id);
 | 
					        fetch_templates(e.detail.guild_id);
 | 
				
			||||||
        fetch_reminders(ev.detail.guild_id);
 | 
					        fetch_reminders(e.detail.guild_id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        document.querySelectorAll("p.pageTitle").forEach((el) => {
 | 
					        document.querySelectorAll("p.pageTitle").forEach((el) => {
 | 
				
			||||||
            el.textContent = `${guildName()} Reminders`;
 | 
					            el.textContent = `${e.detail.guild_name} Reminders`;
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
 | 
					 | 
				
			||||||
        document.querySelectorAll("select.channel-selector").forEach((el) => {
 | 
					        document.querySelectorAll("select.channel-selector").forEach((el) => {
 | 
				
			||||||
            el.addEventListener("change", (e) => {
 | 
					            el.addEventListener("change", (e) => {
 | 
				
			||||||
                update_select(e.target);
 | 
					                update_select(e.target);
 | 
				
			||||||
@@ -626,6 +603,16 @@ function show_error(error) {
 | 
				
			|||||||
    }, 5000);
 | 
					    }, 5000);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function show_success(error) {
 | 
				
			||||||
 | 
					    document.getElementById("success").querySelector("span.success-message").textContent =
 | 
				
			||||||
 | 
					        error;
 | 
				
			||||||
 | 
					    document.getElementById("success").classList.add("is-active");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    window.setTimeout(() => {
 | 
				
			||||||
 | 
					        document.getElementById("success").classList.remove("is-active");
 | 
				
			||||||
 | 
					    }, 5000);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
$colorPickerInput.value = colorPicker.color.hexString;
 | 
					$colorPickerInput.value = colorPicker.color.hexString;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
$colorPickerInput.addEventListener("input", () => {
 | 
					$colorPickerInput.addEventListener("input", () => {
 | 
				
			||||||
@@ -706,56 +693,36 @@ document.addEventListener("DOMContentLoaded", async () => {
 | 
				
			|||||||
                            "%guildname%",
 | 
					                            "%guildname%",
 | 
				
			||||||
                            guild.name
 | 
					                            guild.name
 | 
				
			||||||
                        );
 | 
					                        );
 | 
				
			||||||
 | 
					                        $anchor.dataset["guild"] = guild.id;
 | 
				
			||||||
                        $anchor.dataset["name"] = guild.name;
 | 
					                        $anchor.dataset["name"] = guild.name;
 | 
				
			||||||
                        $anchor.href = `/dashboard/${guild.id}/reminders`;
 | 
					                        $anchor.href = `/dashboard/${guild.id}?name=${guild.name}`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                        const $li = $anchor.parentElement;
 | 
					                        $anchor.addEventListener("click", async (e) => {
 | 
				
			||||||
                        $li.dataset["guild"] = guild.id;
 | 
					                            e.preventDefault();
 | 
				
			||||||
 | 
					                            window.history.pushState({}, "", `/dashboard/${guild.id}`);
 | 
				
			||||||
                        $li.querySelectorAll("a").forEach((el) => {
 | 
					                            const event = new CustomEvent("guildSwitched", {
 | 
				
			||||||
                            el.addEventListener("click", (e) => {
 | 
					                                detail: {
 | 
				
			||||||
                                const pane = el.dataset["pane"];
 | 
					                                    guild_name: guild.name,
 | 
				
			||||||
                                const slug = el.dataset["slug"];
 | 
					                                    guild_id: guild.id,
 | 
				
			||||||
 | 
					                                },
 | 
				
			||||||
                                if (pane !== undefined && slug !== undefined) {
 | 
					 | 
				
			||||||
                                    e.preventDefault();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                                    switch_pane(pane);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                                    window.history.pushState(
 | 
					 | 
				
			||||||
                                        {},
 | 
					 | 
				
			||||||
                                        "",
 | 
					 | 
				
			||||||
                                        `/dashboard/${guild.id}/${slug}`
 | 
					 | 
				
			||||||
                                    );
 | 
					 | 
				
			||||||
                                    const event = new CustomEvent("guildSwitched", {
 | 
					 | 
				
			||||||
                                        detail: {
 | 
					 | 
				
			||||||
                                            guild_id: guild.id,
 | 
					 | 
				
			||||||
                                            pane,
 | 
					 | 
				
			||||||
                                        },
 | 
					 | 
				
			||||||
                                    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                                    document.dispatchEvent(event);
 | 
					 | 
				
			||||||
                                }
 | 
					 | 
				
			||||||
                            });
 | 
					                            });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                            document.dispatchEvent(event);
 | 
				
			||||||
                        });
 | 
					                        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                        element.append($clone);
 | 
					                        element.append($clone);
 | 
				
			||||||
                    });
 | 
					                    });
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                const matches = window.location.href.match(
 | 
					                const matches = window.location.href.match(/dashboard\/(\d+)/);
 | 
				
			||||||
                    /dashboard\/(\d+)(\/)?([a-zA-Z\-]+)?/
 | 
					 | 
				
			||||||
                );
 | 
					 | 
				
			||||||
                if (matches) {
 | 
					                if (matches) {
 | 
				
			||||||
                    let id = matches[1];
 | 
					                    let id = matches[1];
 | 
				
			||||||
                    let kind = matches[3];
 | 
					 | 
				
			||||||
                    let name = guildNames[id];
 | 
					                    let name = guildNames[id];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    const event = new CustomEvent("guildSwitched", {
 | 
					                    const event = new CustomEvent("guildSwitched", {
 | 
				
			||||||
                        detail: {
 | 
					                        detail: {
 | 
				
			||||||
                            guild_name: name,
 | 
					                            guild_name: name,
 | 
				
			||||||
                            guild_id: id,
 | 
					                            guild_id: id,
 | 
				
			||||||
                            pane: kind,
 | 
					 | 
				
			||||||
                        },
 | 
					                        },
 | 
				
			||||||
                    });
 | 
					                    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -796,12 +763,26 @@ $uploader.addEventListener("change", (ev) => {
 | 
				
			|||||||
        fileReader.onload = (e) => resolve(fileReader.result);
 | 
					        fileReader.onload = (e) => resolve(fileReader.result);
 | 
				
			||||||
        fileReader.readAsDataURL($uploader.files[0]);
 | 
					        fileReader.readAsDataURL($uploader.files[0]);
 | 
				
			||||||
    }).then((dataUrl) => {
 | 
					    }).then((dataUrl) => {
 | 
				
			||||||
 | 
					        $importBtn.setAttribute("disabled", true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        fetch(`/dashboard/api/guild/${guildId()}/export/${urlTail}`, {
 | 
					        fetch(`/dashboard/api/guild/${guildId()}/export/${urlTail}`, {
 | 
				
			||||||
            method: "PUT",
 | 
					            method: "PUT",
 | 
				
			||||||
            body: JSON.stringify({ body: dataUrl.split(",")[1] }),
 | 
					            body: JSON.stringify({ body: dataUrl.split(",")[1] }),
 | 
				
			||||||
        }).then(() => {
 | 
					        })
 | 
				
			||||||
            delete $uploader.files[0];
 | 
					            .then((response) => response.json())
 | 
				
			||||||
        });
 | 
					            .then((data) => {
 | 
				
			||||||
 | 
					                $importBtn.removeAttribute("disabled");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if (data.error) {
 | 
				
			||||||
 | 
					                    show_error(data.error);
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    show_success(data.message);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					            .then(() => {
 | 
				
			||||||
 | 
					                delete $uploader.files[0];
 | 
				
			||||||
 | 
					                fetch_reminders(guild);
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
		 Before Width: | Height: | Size: 712 KiB After Width: | Height: | Size: 712 KiB  | 
| 
		 Before Width: | Height: | Size: 2.5 MiB After Width: | Height: | Size: 2.5 MiB  | 
| 
		 Before Width: | Height: | Size: 2.3 MiB After Width: | Height: | Size: 2.3 MiB  | 
| 
		 Before Width: | Height: | Size: 2.1 MiB After Width: | Height: | Size: 2.1 MiB  |