Compare commits
	
		
			113 Commits
		
	
	
		
			1d64c8bb79
			...
			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 | 
							
								
								
									
										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? | ||||||
|   | |||||||
							
								
								
									
										2035
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
							
								
								
									
										45
									
								
								Cargo.toml
									
									
									
									
									
								
							
							
						
						| @@ -1,22 +1,21 @@ | |||||||
| [package] | [package] | ||||||
| name = "reminder-rs" | name = "reminder-rs" | ||||||
| version = "1.6.50" | 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.2" | 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" | ||||||
| @@ -26,13 +25,22 @@ 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,12 +48,17 @@ 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"], |  | ||||||
|     ["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"] |     # ["nginx/reminder-rs", "etc/nginx/sites-available/reminder-rs", "755"] | ||||||
| ] | ] | ||||||
| conf-files = [ | conf-files = [ | ||||||
|   | |||||||
| @@ -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 |  | ||||||
							
								
								
									
										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"] } |  | ||||||
							
								
								
									
										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" | ||||||
|  | 	} | ||||||
|  | } | ||||||
| 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 | 
| @@ -33,16 +33,7 @@ let globalPatreon = false; | |||||||
| let guildPatreon = false; | let guildPatreon = false; | ||||||
| 
 | 
 | ||||||
| function guildId() { | function guildId() { | ||||||
|     return window.location.pathname.match(/dashboard\/(\d+)/)[1]; |     return document.querySelector(".guildList a.is-active").dataset["guild"]; | ||||||
| } |  | ||||||
| 
 |  | ||||||
| function pane() { |  | ||||||
|     const match = window.location.pathname.match(/dashboard\/\d+\/(.+)/); |  | ||||||
|     if (match === null) { |  | ||||||
|         return null; |  | ||||||
|     } else { |  | ||||||
|         return match[1]; |  | ||||||
|     } |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| function colorToInt(r, g, b) { | function colorToInt(r, g, b) { | ||||||
| @@ -105,7 +96,7 @@ function reset_guild_pane() { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| async function fetch_patreon(guild_id) { | async function fetch_patreon(guild_id) { | ||||||
|     fetch(`/dashboard/api/guild/${guild_id}`) |     fetch(`/dashboard/api/guild/${guild_id}/patreon`) | ||||||
|         .then((response) => response.json()) |         .then((response) => response.json()) | ||||||
|         .then((data) => { |         .then((data) => { | ||||||
|             if (data.error) { |             if (data.error) { | ||||||
| @@ -463,17 +454,15 @@ document.addEventListener("guildSwitched", async (e) => { | |||||||
| 
 | 
 | ||||||
|     let hasError = false; |     let hasError = false; | ||||||
| 
 | 
 | ||||||
|     if (pane() === null) { |     if ($anchor === null) { | ||||||
|         window.history.replaceState({}, "", `/dashboard/${guildId()}/reminders`); |         switch_pane("user-error"); | ||||||
|     } |         hasError = true; | ||||||
| 
 |         return; | ||||||
|     switch_pane(pane()); |  | ||||||
| 
 |  | ||||||
|     if ($anchor !== null) { |  | ||||||
|         $anchor.classList.add("is-active"); |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     switch_pane($anchor.dataset["pane"]); | ||||||
|     reset_guild_pane(); |     reset_guild_pane(); | ||||||
|  |     $anchor.classList.add("is-active"); | ||||||
| 
 | 
 | ||||||
|     if (globalPatreon || (await fetch_patreon(e.detail.guild_id))) { |     if (globalPatreon || (await fetch_patreon(e.detail.guild_id))) { | ||||||
|         document |         document | ||||||
| @@ -706,15 +695,11 @@ document.addEventListener("DOMContentLoaded", async () => { | |||||||
|                         ); |                         ); | ||||||
|                         $anchor.dataset["guild"] = guild.id; |                         $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}`; | ||||||
| 
 | 
 | ||||||
|                         $anchor.addEventListener("click", async (e) => { |                         $anchor.addEventListener("click", async (e) => { | ||||||
|                             e.preventDefault(); |                             e.preventDefault(); | ||||||
|                             window.history.pushState( |                             window.history.pushState({}, "", `/dashboard/${guild.id}`); | ||||||
|                                 {}, |  | ||||||
|                                 "", |  | ||||||
|                                 `/dashboard/${guild.id}/reminders` |  | ||||||
|                             ); |  | ||||||
|                             const event = new CustomEvent("guildSwitched", { |                             const event = new CustomEvent("guildSwitched", { | ||||||
|                                 detail: { |                                 detail: { | ||||||
|                                     guild_name: guild.name, |                                     guild_name: guild.name, | ||||||
| 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 |