Compare commits
	
		
			118 Commits
		
	
	
		
			06e1474396
			...
			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 | 
							
								
								
									
										27
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -1,7 +1,30 @@ | ||||
| /target | ||||
| target | ||||
| .env | ||||
| /venv | ||||
| .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? | ||||
|   | ||||
							
								
								
									
										3
									
								
								.gitmodules
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -1,3 +0,0 @@ | ||||
| [submodule "reminder-dashboard"] | ||||
| 	path = reminder-dashboard | ||||
| 	url = gitea@gitea.jellypro.xyz:jude/reminder-dashboard | ||||
							
								
								
									
										2140
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
							
								
								
									
										50
									
								
								Cargo.toml
									
									
									
									
									
								
							
							
						
						| @@ -1,22 +1,21 @@ | ||||
| [package] | ||||
| name = "reminder-rs" | ||||
| version = "1.6.48" | ||||
| version = "1.7.24" | ||||
| authors = ["Jude Southworth <judesouthworth@pm.me>"] | ||||
| edition = "2021" | ||||
| license = "AGPL-3.0 only" | ||||
| description = "Reminder Bot for Discord, now in Rust" | ||||
|  | ||||
| [dependencies] | ||||
| poise = "0.5" | ||||
| poise = "0.6.1" | ||||
| dotenv = "0.15" | ||||
| tokio = { version = "1", features = ["process", "full"] } | ||||
| reqwest = "0.11" | ||||
| lazy-regex = "3.0.2" | ||||
| regex = "1.9" | ||||
| reqwest = { version = "0.12", features = ["json"] } | ||||
| regex = "1.10" | ||||
| log = "0.4" | ||||
| env_logger = "0.10" | ||||
| env_logger = "0.11" | ||||
| chrono = "0.4" | ||||
| chrono-tz = { version = "0.8", features = ["serde"] } | ||||
| chrono-tz = { version = "0.9", features = ["serde"] } | ||||
| lazy_static = "1.4" | ||||
| num-integer = "0.1" | ||||
| serde = "1.0" | ||||
| @@ -25,14 +24,23 @@ serde_repr = "0.1" | ||||
| rmp-serde = "1.1" | ||||
| rand = "0.8" | ||||
| levenshtein = "1.0" | ||||
| sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "macros", "mysql", "bigdecimal", "chrono", "migrate"]} | ||||
| base64 = "0.21.0" | ||||
| sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "macros", "mysql", "bigdecimal", "chrono", "migrate"] } | ||||
| 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] | ||||
| path = "postman" | ||||
| [dependencies.extract_derive] | ||||
| path = "extract_derive" | ||||
|  | ||||
| [dependencies.reminder_web] | ||||
| path = "web" | ||||
| [dependencies.recordable_derive] | ||||
| path = "recordable_derive" | ||||
|  | ||||
| [package.metadata.deb] | ||||
| depends = "$auto, python3-dateparser (>= 1.0.0)" | ||||
| @@ -40,14 +48,18 @@ suggests = "mysql-server-8.0, nginx" | ||||
| maintainer-scripts = "debian" | ||||
| assets = [ | ||||
|     ["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/Rocket.toml", "etc/reminder-rs/Rocket.toml", "600"], | ||||
|     ["web/static/**/*", "lib/reminder-rs/static", "644"], | ||||
|     ["reminder-dashboard/dist/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 = [ | ||||
|     "/etc/reminder-rs/config.env", | ||||
|   | ||||
| @@ -4,6 +4,6 @@ ENV RUSTUP_HOME=/usr/local/rustup \ | ||||
|     CARGO_HOME=/usr/local/cargo \ | ||||
|     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 cargo install cargo-deb | ||||
|   | ||||
							
								
								
									
										22
									
								
								Rocket.toml
									
									
									
									
									
								
							
							
						
						| @@ -1,28 +1,28 @@ | ||||
| [default] | ||||
| address = "0.0.0.0" | ||||
| port = 18920 | ||||
| template_dir = "web/templates" | ||||
| template_dir = "templates" | ||||
| limits = { json = "10MiB" } | ||||
|  | ||||
| [debug] | ||||
| secret_key = "tR8krio5FXTnnyIZNiJDXPondz0kI1v6X6BXZcBGIRY=" | ||||
|  | ||||
| [debug.tls] | ||||
| certs = "web/private/rsa_sha256_cert.pem" | ||||
| key = "web/private/rsa_sha256_key.pem" | ||||
| certs = "private/rsa_sha256_cert.pem" | ||||
| key = "private/rsa_sha256_key.pem" | ||||
|  | ||||
| [debug.rsa_sha256.tls] | ||||
| certs = "web/private/rsa_sha256_cert.pem" | ||||
| key = "web/private/rsa_sha256_key.pem" | ||||
| certs = "private/rsa_sha256_cert.pem" | ||||
| key = "private/rsa_sha256_key.pem" | ||||
|  | ||||
| [debug.ecdsa_nistp256_sha256.tls] | ||||
| certs = "web/private/ecdsa_nistp256_sha256_cert.pem" | ||||
| key = "web/private/ecdsa_nistp256_sha256_key_pkcs8.pem" | ||||
| certs = "private/ecdsa_nistp256_sha256_cert.pem" | ||||
| key = "private/ecdsa_nistp256_sha256_key_pkcs8.pem" | ||||
|  | ||||
| [debug.ecdsa_nistp384_sha384.tls] | ||||
| certs = "web/private/ecdsa_nistp384_sha384_cert.pem" | ||||
| key = "web/private/ecdsa_nistp384_sha384_key_pkcs8.pem" | ||||
| certs = "private/ecdsa_nistp384_sha384_cert.pem" | ||||
| key = "private/ecdsa_nistp384_sha384_key_pkcs8.pem" | ||||
|  | ||||
| [debug.ed25519.tls] | ||||
| certs = "web/private/ed25519_cert.pem" | ||||
| key = "eb/private/ed25519_key.pem" | ||||
| certs = "private/ed25519_cert.pem" | ||||
| key = "private/ed25519_key.pem" | ||||
|   | ||||
							
								
								
									
										10
									
								
								build.rs
									
									
									
									
									
								
							
							
						
						| @@ -1,3 +1,13 @@ | ||||
| use std::{path::Path, process::Command}; | ||||
|  | ||||
| fn main() { | ||||
|     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" | ||||
| 	} | ||||
| } | ||||
| @@ -218,16 +218,17 @@ div.inset-content { | ||||
|     margin-right: 10%; | ||||
| } | ||||
| 
 | ||||
| div.flash-container { | ||||
|     position: fixed; | ||||
|     width: 100%; | ||||
|     bottom: 0; | ||||
| } | ||||
| 
 | ||||
| div.flash-message { | ||||
|     position: fixed; | ||||
|     width: calc(100% - 32px); | ||||
|     margin: 16px !important; | ||||
|     z-index: 99; | ||||
|     bottom: 0; | ||||
|     display: none; | ||||
| } | ||||
| 
 | ||||
| div.flash-message.is-active { | ||||
|     display: block; | ||||
| } | ||||
| 
 | ||||
| body { | ||||
| @@ -632,10 +633,6 @@ li.highlight { | ||||
|     display: flex; | ||||
| } | ||||
| 
 | ||||
| .button-row-edit > button { | ||||
|     margin-right: 4px; | ||||
| } | ||||
| 
 | ||||
| .button-row .button-row-reminder { | ||||
|     flex-grow: 0; | ||||
|     padding: 2px; | ||||
| 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; | ||||
| 
 | ||||
| function guildId() { | ||||
|     return window.location.pathname.match(/dashboard\/(\d+)/)[1]; | ||||
| } | ||||
| 
 | ||||
| function pane() { | ||||
|     const match = window.location.pathname.match(/dashboard\/\d+\/(.+)/); | ||||
|     if (match === null) { | ||||
|         return null; | ||||
|     } else { | ||||
|         return match[1]; | ||||
|     } | ||||
|     return document.querySelector(".guildList a.is-active").dataset["guild"]; | ||||
| } | ||||
| 
 | ||||
| function colorToInt(r, g, b) { | ||||
| @@ -105,7 +96,7 @@ function reset_guild_pane() { | ||||
| } | ||||
| 
 | ||||
| async function fetch_patreon(guild_id) { | ||||
|     fetch(`/dashboard/api/guild/${guild_id}`) | ||||
|     fetch(`/dashboard/api/guild/${guild_id}/patreon`) | ||||
|         .then((response) => response.json()) | ||||
|         .then((data) => { | ||||
|             if (data.error) { | ||||
| @@ -463,17 +454,15 @@ document.addEventListener("guildSwitched", async (e) => { | ||||
| 
 | ||||
|     let hasError = false; | ||||
| 
 | ||||
|     if (pane() === null) { | ||||
|         window.history.replaceState({}, "", `/dashboard/${guildId()}/reminders`); | ||||
|     } | ||||
| 
 | ||||
|     switch_pane(pane()); | ||||
| 
 | ||||
|     if ($anchor !== null) { | ||||
|         $anchor.classList.add("is-active"); | ||||
|     if ($anchor === null) { | ||||
|         switch_pane("user-error"); | ||||
|         hasError = true; | ||||
|         return; | ||||
|     } | ||||
| 
 | ||||
|     switch_pane($anchor.dataset["pane"]); | ||||
|     reset_guild_pane(); | ||||
|     $anchor.classList.add("is-active"); | ||||
| 
 | ||||
|     if (globalPatreon || (await fetch_patreon(e.detail.guild_id))) { | ||||
|         document | ||||
| @@ -706,15 +695,11 @@ document.addEventListener("DOMContentLoaded", async () => { | ||||
|                         ); | ||||
|                         $anchor.dataset["guild"] = guild.id; | ||||
|                         $anchor.dataset["name"] = guild.name; | ||||
|                         $anchor.href = `/dashboard/${guild.id}/reminders`; | ||||
|                         $anchor.href = `/dashboard/${guild.id}?name=${guild.name}`; | ||||
| 
 | ||||
|                         $anchor.addEventListener("click", async (e) => { | ||||
|                             e.preventDefault(); | ||||
|                             window.history.pushState( | ||||
|                                 {}, | ||||
|                                 "", | ||||
|                                 `/dashboard/${guild.id}/reminders` | ||||
|                             ); | ||||
|                             window.history.pushState({}, "", `/dashboard/${guild.id}`); | ||||
|                             const event = new CustomEvent("guildSwitched", { | ||||
|                                 detail: { | ||||
|                                     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 |