Compare commits
	
		
			114 Commits
		
	
	
		
			jude/react
			...
			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 | 
							
								
								
									
										29
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -1,5 +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?
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2035
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
							
								
								
									
										49
									
								
								Cargo.toml
									
									
									
									
									
								
							
							
						
						@@ -1,22 +1,21 @@
 | 
			
		||||
[package]
 | 
			
		||||
name = "reminder-rs"
 | 
			
		||||
version = "1.6.50"
 | 
			
		||||
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,13 +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"],
 | 
			
		||||
    ["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"
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
		 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  |