Compare commits
	
		
			1 Commits
		
	
	
		
			6ae1096d79
			...
			postgres
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 2d1668a63a | 
							
								
								
									
										31
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -1,30 +1,7 @@ | |||||||
| target | /target | ||||||
| .env | .env | ||||||
| /venv | /venv | ||||||
| .cargo | .cargo | ||||||
| .idea | assets | ||||||
| web/static/index.html | out.json | ||||||
| web/static/assets | /.idea | ||||||
| # 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? |  | ||||||
|   | |||||||
							
								
								
									
										3379
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
							
								
								
									
										73
									
								
								Cargo.toml
									
									
									
									
									
								
							
							
						
						| @@ -1,71 +1,32 @@ | |||||||
| [package] | [package] | ||||||
| name = "reminder-rs" | name = "reminder_rs" | ||||||
| version = "1.7.22" | version = "1.6.0" | ||||||
| authors = ["Jude Southworth <judesouthworth@pm.me>"] | authors = ["jellywx <judesouthworth@pm.me>"] | ||||||
| edition = "2021" | edition = "2018" | ||||||
| license = "AGPL-3.0 only" | workspaces = [".", "postman", "web", "entity", "migration"] | ||||||
| description = "Reminder Bot for Discord, now in Rust" |  | ||||||
|  |  | ||||||
| [dependencies] | [dependencies] | ||||||
| poise = "0.6.1" | poise = "0.2" | ||||||
| dotenv = "0.15" | dotenv = "0.15" | ||||||
| tokio = { version = "1", features = ["process", "full"] } | tokio = { version = "1", features = ["process", "full"] } | ||||||
| reqwest = { version = "0.12", features = ["json"] } | reqwest = "0.11" | ||||||
| regex = "1.10" | regex = "1.4" | ||||||
| log = "0.4" | log = "0.4" | ||||||
| env_logger = "0.11" | env_logger = "0.8" | ||||||
| chrono = "0.4" | chrono = "0.4" | ||||||
| chrono-tz = { version = "0.9", features = ["serde"] } | chrono-tz = { version = "0.5", features = ["serde"] } | ||||||
| lazy_static = "1.4" | lazy_static = "1.4" | ||||||
| num-integer = "0.1" | num-integer = "0.1" | ||||||
| serde = "1.0" | serde = "1.0" | ||||||
| serde_json = "1.0" | serde_json = "1.0" | ||||||
| serde_repr = "0.1" | serde_repr = "0.1" | ||||||
| rmp-serde = "1.1" | rmp-serde = "0.15" | ||||||
| rand = "0.8" | rand = "0.7" | ||||||
| levenshtein = "1.0" | levenshtein = "1.0" | ||||||
| sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "macros", "mysql", "bigdecimal", "chrono", "migrate"] } | base64 = "0.13.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.extract_derive] | [dependencies.postman] | ||||||
| path = "extract_derive" | path = "postman" | ||||||
|  |  | ||||||
| [dependencies.recordable_derive] | [dependencies.reminder_web] | ||||||
| path = "recordable_derive" | path = "web" | ||||||
|  |  | ||||||
| [package.metadata.deb] |  | ||||||
| depends = "$auto, python3-dateparser (>= 1.0.0)" |  | ||||||
| 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"], |  | ||||||
|     # ["nginx/reminder-rs", "etc/nginx/sites-available/reminder-rs", "755"] |  | ||||||
| ] |  | ||||||
| conf-files = [ |  | ||||||
|     "/etc/reminder-rs/config.env", |  | ||||||
|     "/etc/reminder-rs/Rocket.toml", |  | ||||||
| ] |  | ||||||
|  |  | ||||||
| [package.metadata.deb.systemd-units] |  | ||||||
| unit-scripts = "systemd" |  | ||||||
| start = false |  | ||||||
|   | |||||||
| @@ -1,9 +0,0 @@ | |||||||
| FROM ubuntu:20.04 |  | ||||||
|  |  | ||||||
| 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 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 |  | ||||||
							
								
								
									
										46
									
								
								README.md
									
									
									
									
									
								
							
							
						
						| @@ -7,36 +7,25 @@ reminders are paid on the hosted version of the bot. Keep reading if you want to | |||||||
|  |  | ||||||
| You'll need rustc and cargo for compilation. To run, you'll need Python 3 still (due to no suitable replacement for dateparser in Rust) | You'll need rustc and cargo for compilation. To run, you'll need Python 3 still (due to no suitable replacement for dateparser in Rust) | ||||||
|  |  | ||||||
| ### Build APT package | ### Compiling | ||||||
|  | Install build requirements:  | ||||||
|  | `sudo apt install gcc gcc-multilib cmake libssl-dev build-essential` | ||||||
|  |  | ||||||
| Recommended method. | Install Rust from https://rustup.rs | ||||||
|  |  | ||||||
| By default, this builds targeting Ubuntu 20.04. Modify the Containerfile if you wish to target a different platform. These instructions are written using `podman`, but `docker` should work too. | Reminder Bot can then be built by running `cargo build --release` in the top level directory. It is necessary to create a  | ||||||
|  | folder called 'assets' containing an image file with its name specified in the environment as `WEBHOOK_AVATAR`, of  | ||||||
|  | dimensions 128x128px to be used as the webhook avatar. | ||||||
|  |  | ||||||
| 1. Install container software: `sudo apt install podman`. | #### Compilation environment variables | ||||||
| 2. Install database server: `sudo apt install mysql-server-8.0`. Create a database called `reminders` | These environment variables must be provided when compiling the bot | ||||||
| 3. Install SQLx CLI: `cargo install sqlx-cli` | * `DATABASE_URL` - the URL of your MySQL database (`mysql://user[:password]@domain/database`) | ||||||
| 4. From the source code directory, execute `sqlx migrate run` | * `WEBHOOK_AVATAR` - accepts the name of an image file located in `$CARGO_MANIFEST_DIR/assets/` to be used as the avatar when creating webhooks. **IMPORTANT: image file must be 128x128 or smaller in size** | ||||||
| 5. Build container image: `podman build -t reminder-rs .` |  | ||||||
| 6. Build with podman: `podman run --rm --network=host -v "$PWD":/mnt -w /mnt -e "DATABASE_URL=mysql://user@localhost/reminders" reminder-rs cargo deb`  |  | ||||||
|  |  | ||||||
|  | ### Setting up Python | ||||||
|  | Reminder Bot by default looks for a venv within it's working directory to run Python out of. To set up a venv, install `python3-venv` and run `python3 -m venv venv`. Then, run `source venv/bin/activate` to activate the venv, and do `pip install dateparser` to install the required library | ||||||
|  |  | ||||||
| ### Compiling for other target | ### Environment Variables | ||||||
|  |  | ||||||
| 1. Install requirements:  |  | ||||||
| `sudo apt install gcc gcc-multilib cmake libssl-dev build-essential python3-dateparser` |  | ||||||
| 2. Install rustup from https://rustup.rs |  | ||||||
| 3. Install the nightly toolchain: `rustup toolchain default nightly` |  | ||||||
| 4. Install database server: `sudo apt install mysql-server-8.0`. Create a database called `reminders`. |  | ||||||
| 5. Install `sqlx-cli`: `cargo install sqlx-cli`. |  | ||||||
| 6. Run migrations: `sqlx migrate run`. |  | ||||||
| 7. Set environment variables: |  | ||||||
|    * `DATABASE_URL` - the URL of your MySQL database (`mysql://user[:password]@domain/database`) |  | ||||||
| 8. Build: `cargo build --release` |  | ||||||
|  |  | ||||||
|  |  | ||||||
| ### Configuring |  | ||||||
|  |  | ||||||
| Reminder Bot reads a number of environment variables. Some are essential, and others have hardcoded fallbacks. Environment variables can be loaded from a .env file in the working directory. | Reminder Bot reads a number of environment variables. Some are essential, and others have hardcoded fallbacks. Environment variables can be loaded from a .env file in the working directory. | ||||||
|  |  | ||||||
| __Required Variables__ | __Required Variables__ | ||||||
| @@ -48,5 +37,10 @@ __Other Variables__ | |||||||
| * `LOCAL_TIMEZONE` - default `UTC`, necessary for calculations in the natural language processor | * `LOCAL_TIMEZONE` - default `UTC`, necessary for calculations in the natural language processor | ||||||
| * `SUBSCRIPTION_ROLES` - default `None`, accepts a list of Discord role IDs that are given to subscribed users | * `SUBSCRIPTION_ROLES` - default `None`, accepts a list of Discord role IDs that are given to subscribed users | ||||||
| * `CNC_GUILD` - default `None`, accepts a single Discord guild ID for the server that the subscription roles belong to | * `CNC_GUILD` - default `None`, accepts a single Discord guild ID for the server that the subscription roles belong to | ||||||
| * `PYTHON_LOCATION` - default `/usr/bin/python3`. Can be changed if your Python executable is located somewhere else | * `PYTHON_LOCATION` - default `venv/bin/python3`. Can be changed if your Python executable is located somewhere else | ||||||
| * `THEME_COLOR` - default `8fb677`. Specifies the hex value of the color to use on info message embeds  | * `THEME_COLOR` - default `8fb677`. Specifies the hex value of the color to use on info message embeds  | ||||||
|  | * `DM_ENABLED` - default `1`, if `1`, Reminder Bot will respond to direct messages | ||||||
|  |  | ||||||
|  | ### Todo List | ||||||
|  |  | ||||||
|  | * Convert aliases to macros | ||||||
|   | |||||||
							
								
								
									
										32
									
								
								Rocket.toml
									
									
									
									
									
								
							
							
						
						| @@ -1,28 +1,28 @@ | |||||||
| [default] | [default] | ||||||
| address = "0.0.0.0" | address = "0.0.0.0" | ||||||
| port = 18920 | port = 5000 | ||||||
| template_dir = "templates" | template_dir = "web/templates" | ||||||
| limits = { json = "10MiB" } | limits = { json = "10MiB" } | ||||||
|  |  | ||||||
| [debug] | [debug] | ||||||
| secret_key = "tR8krio5FXTnnyIZNiJDXPondz0kI1v6X6BXZcBGIRY=" | secret_key = "tR8krio5FXTnnyIZNiJDXPondz0kI1v6X6BXZcBGIRY=" | ||||||
|  |  | ||||||
| [debug.tls] | [debug.tls] | ||||||
| certs = "private/rsa_sha256_cert.pem" | certs = "web/private/rsa_sha256_cert.pem" | ||||||
| key = "private/rsa_sha256_key.pem" | key = "web/private/rsa_sha256_key.pem" | ||||||
|  |  | ||||||
| [debug.rsa_sha256.tls] | [rsa_sha256.tls] | ||||||
| certs = "private/rsa_sha256_cert.pem" | certs = "web/private/rsa_sha256_cert.pem" | ||||||
| key = "private/rsa_sha256_key.pem" | key = "web/private/rsa_sha256_key.pem" | ||||||
|  |  | ||||||
| [debug.ecdsa_nistp256_sha256.tls] | [ecdsa_nistp256_sha256.tls] | ||||||
| certs = "private/ecdsa_nistp256_sha256_cert.pem" | certs = "web/private/ecdsa_nistp256_sha256_cert.pem" | ||||||
| key = "private/ecdsa_nistp256_sha256_key_pkcs8.pem" | key = "web/private/ecdsa_nistp256_sha256_key_pkcs8.pem" | ||||||
|  |  | ||||||
| [debug.ecdsa_nistp384_sha384.tls] | [ecdsa_nistp384_sha384.tls] | ||||||
| certs = "private/ecdsa_nistp384_sha384_cert.pem" | certs = "web/private/ecdsa_nistp384_sha384_cert.pem" | ||||||
| key = "private/ecdsa_nistp384_sha384_key_pkcs8.pem" | key = "web/private/ecdsa_nistp384_sha384_key_pkcs8.pem" | ||||||
|  |  | ||||||
| [debug.ed25519.tls] | [ed25519.tls] | ||||||
| certs = "private/ed25519_cert.pem" | certs = "web/private/ed25519_cert.pem" | ||||||
| key = "private/ed25519_key.pem" | key = "eb/private/ed25519_key.pem" | ||||||
|   | |||||||
| Before Width: | Height: | Size: 21 KiB | 
							
								
								
									
										13
									
								
								build.rs
									
									
									
									
									
								
							
							
						
						| @@ -1,13 +0,0 @@ | |||||||
| 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"); |  | ||||||
| } |  | ||||||
| @@ -1,8 +0,0 @@ | |||||||
| [default] |  | ||||||
| address = "127.0.0.1" |  | ||||||
| port = 18920 |  | ||||||
| template_dir = "/lib/reminder-rs/templates" |  | ||||||
| limits = { json = "10MiB" } |  | ||||||
|  |  | ||||||
| [release] |  | ||||||
| # secret_key = "" |  | ||||||
| @@ -1,19 +0,0 @@ | |||||||
| DATABASE_URL= |  | ||||||
|  |  | ||||||
| DISCORD_TOKEN= |  | ||||||
| PATREON_GUILD_ID= |  | ||||||
| PATREON_ROLE_ID= |  | ||||||
|  |  | ||||||
| LOCAL_TIMEZONE= |  | ||||||
| MIN_INTERVAL= |  | ||||||
| PYTHON_LOCATION=/usr/bin/python3 |  | ||||||
| DONTRUN= |  | ||||||
| SECRET_KEY= |  | ||||||
|  |  | ||||||
| REMIND_INTERVAL= |  | ||||||
| OAUTH2_DISCORD_CALLBACK= |  | ||||||
| OAUTH2_CLIENT_ID= |  | ||||||
| OAUTH2_CLIENT_SECRET= |  | ||||||
|  |  | ||||||
| REPORT_EMAIL= |  | ||||||
| LOG_TO_DATABASE=1 |  | ||||||
| @@ -1 +0,0 @@ | |||||||
| */10 * * * * reminder /lib/reminder-rs/healthcheck |  | ||||||
							
								
								
									
										9
									
								
								debian/postinst
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -1,9 +0,0 @@ | |||||||
| #!/bin/bash |  | ||||||
|  |  | ||||||
| set -e |  | ||||||
|  |  | ||||||
| id -u reminder &>/dev/null || useradd -r -M reminder |  | ||||||
|  |  | ||||||
| chown -R reminder /etc/reminder-rs |  | ||||||
|  |  | ||||||
| #DEBHELPER# |  | ||||||
							
								
								
									
										7
									
								
								debian/postrm
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -1,7 +0,0 @@ | |||||||
| #!/bin/bash |  | ||||||
|  |  | ||||||
| set -e |  | ||||||
|  |  | ||||||
| id -u reminder &>/dev/null || userdel reminder |  | ||||||
|  |  | ||||||
| #DEBHELPER# |  | ||||||
							
								
								
									
										46
									
								
								extract_derive/Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						| @@ -1,46 +0,0 @@ | |||||||
| # 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" |  | ||||||
| @@ -1,11 +0,0 @@ | |||||||
| [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"] } |  | ||||||
| @@ -1,53 +0,0 @@ | |||||||
| 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"); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,229 +0,0 @@ | |||||||
| SET FOREIGN_KEY_CHECKS=0; |  | ||||||
|  |  | ||||||
| CREATE TABLE guilds ( |  | ||||||
|     id INT UNSIGNED UNIQUE NOT NULL AUTO_INCREMENT, |  | ||||||
|     guild BIGINT UNSIGNED UNIQUE NOT NULL, |  | ||||||
|  |  | ||||||
|     name VARCHAR(100), |  | ||||||
|  |  | ||||||
|     prefix VARCHAR(5) DEFAULT '$' NOT NULL, |  | ||||||
|     timezone VARCHAR(32) DEFAULT 'UTC' NOT NULL, |  | ||||||
|  |  | ||||||
|     default_channel_id INT UNSIGNED, |  | ||||||
|     default_username VARCHAR(32) DEFAULT 'Reminder' NOT NULL, |  | ||||||
|     default_avatar VARCHAR(512) DEFAULT 'https://raw.githubusercontent.com/reminder-bot/logos/master/Remind_Me_Bot_Logo_PPic.jpg' NOT NULL, |  | ||||||
|  |  | ||||||
|     PRIMARY KEY (id), |  | ||||||
|     FOREIGN KEY (default_channel_id) REFERENCES channels(id) ON DELETE SET NULL |  | ||||||
| ); |  | ||||||
|  |  | ||||||
| CREATE TABLE channels ( |  | ||||||
|     id INT UNSIGNED UNIQUE NOT NULL AUTO_INCREMENT, |  | ||||||
|     channel BIGINT UNSIGNED UNIQUE NOT NULL, |  | ||||||
|  |  | ||||||
|     name VARCHAR(100), |  | ||||||
|  |  | ||||||
|     nudge SMALLINT NOT NULL DEFAULT 0, |  | ||||||
|     blacklisted BOOL NOT NULL DEFAULT FALSE, |  | ||||||
|  |  | ||||||
|     webhook_id BIGINT UNSIGNED UNIQUE, |  | ||||||
|     webhook_token TEXT, |  | ||||||
|  |  | ||||||
|     paused BOOL NOT NULL DEFAULT 0, |  | ||||||
|     paused_until TIMESTAMP, |  | ||||||
|  |  | ||||||
|     guild_id INT UNSIGNED, |  | ||||||
|  |  | ||||||
|     PRIMARY KEY (id), |  | ||||||
|     FOREIGN KEY (guild_id) REFERENCES guilds(id) ON DELETE CASCADE |  | ||||||
| ); |  | ||||||
|  |  | ||||||
| CREATE TABLE users ( |  | ||||||
|     id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL, |  | ||||||
|     user BIGINT UNSIGNED UNIQUE NOT NULL, |  | ||||||
|  |  | ||||||
|     name VARCHAR(37) NOT NULL, |  | ||||||
|  |  | ||||||
|     dm_channel INT UNSIGNED UNIQUE NOT NULL, |  | ||||||
|  |  | ||||||
|     language VARCHAR(2) DEFAULT 'EN' NOT NULL, |  | ||||||
|     timezone VARCHAR(32) DEFAULT 'UTC' NOT NULL, |  | ||||||
|     meridian_time BOOLEAN DEFAULT 0 NOT NULL, |  | ||||||
|  |  | ||||||
|     allowed_dm BOOLEAN DEFAULT 1 NOT NULL, |  | ||||||
|  |  | ||||||
|     patreon BOOLEAN NOT NULL DEFAULT 0, |  | ||||||
|  |  | ||||||
|     PRIMARY KEY (id), |  | ||||||
|     FOREIGN KEY (dm_channel) REFERENCES channels(id) ON DELETE RESTRICT |  | ||||||
| ); |  | ||||||
|  |  | ||||||
| CREATE TABLE roles ( |  | ||||||
|     id INT UNSIGNED UNIQUE NOT NULL AUTO_INCREMENT, |  | ||||||
|     role BIGINT UNSIGNED UNIQUE NOT NULL, |  | ||||||
|  |  | ||||||
|     name VARCHAR(100), |  | ||||||
|  |  | ||||||
|     guild_id INT UNSIGNED NOT NULL, |  | ||||||
|  |  | ||||||
|     PRIMARY KEY (id), |  | ||||||
|     FOREIGN KEY (guild_id) REFERENCES guilds(id) ON DELETE CASCADE |  | ||||||
| ); |  | ||||||
|  |  | ||||||
| CREATE TABLE embeds ( |  | ||||||
|     id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL, |  | ||||||
|  |  | ||||||
|     title VARCHAR(256) NOT NULL DEFAULT '', |  | ||||||
|     description VARCHAR(2048) NOT NULL DEFAULT '', |  | ||||||
|  |  | ||||||
|     image_url VARCHAR(512), |  | ||||||
|     thumbnail_url VARCHAR(512), |  | ||||||
|  |  | ||||||
|     footer VARCHAR(2048) NOT NULL DEFAULT '', |  | ||||||
|     footer_icon VARCHAR(512), |  | ||||||
|  |  | ||||||
|     color MEDIUMINT UNSIGNED NOT NULL DEFAULT 0x0, |  | ||||||
|  |  | ||||||
|     PRIMARY KEY (id) |  | ||||||
| ); |  | ||||||
|  |  | ||||||
| CREATE TABLE embed_fields ( |  | ||||||
|     id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL, |  | ||||||
|  |  | ||||||
|     title VARCHAR(256) NOT NULL DEFAULT '', |  | ||||||
|     value VARCHAR(1024) NOT NULL DEFAULT '', |  | ||||||
|     inline BOOL NOT NULL DEFAULT 0, |  | ||||||
|     embed_id INT UNSIGNED NOT NULL, |  | ||||||
|  |  | ||||||
|     PRIMARY KEY (id), |  | ||||||
|     FOREIGN KEY (embed_id) REFERENCES embeds(id) ON DELETE CASCADE |  | ||||||
| ); |  | ||||||
|  |  | ||||||
| CREATE TABLE messages ( |  | ||||||
|     id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL, |  | ||||||
|  |  | ||||||
|     content VARCHAR(2048) NOT NULL DEFAULT '', |  | ||||||
|     tts BOOL NOT NULL DEFAULT 0, |  | ||||||
|     embed_id INT UNSIGNED, |  | ||||||
|  |  | ||||||
|     attachment MEDIUMBLOB, |  | ||||||
|     attachment_name VARCHAR(260), |  | ||||||
|  |  | ||||||
|     PRIMARY KEY (id), |  | ||||||
|     FOREIGN KEY (embed_id) REFERENCES embeds(id) ON DELETE SET NULL |  | ||||||
| ); |  | ||||||
|  |  | ||||||
| CREATE TABLE reminders ( |  | ||||||
|     id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL, |  | ||||||
|     uid VARCHAR(64) UNIQUE NOT NULL, |  | ||||||
|  |  | ||||||
|     name VARCHAR(24) NOT NULL DEFAULT 'Reminder', |  | ||||||
|  |  | ||||||
|     message_id INT UNSIGNED NOT NULL, |  | ||||||
|     channel_id INT UNSIGNED NOT NULL, |  | ||||||
|  |  | ||||||
|     `time` INT UNSIGNED DEFAULT 0 NOT NULL, |  | ||||||
|     `interval` INT UNSIGNED DEFAULT NULL, |  | ||||||
|     expires TIMESTAMP DEFAULT NULL, |  | ||||||
|  |  | ||||||
|     enabled BOOLEAN DEFAULT 1 NOT NULL, |  | ||||||
|  |  | ||||||
|     avatar VARCHAR(512), |  | ||||||
|     username VARCHAR(32), |  | ||||||
|  |  | ||||||
|     method ENUM('remind', 'natural', 'dashboard', 'todo', 'countdown'), |  | ||||||
|     set_at TIMESTAMP DEFAULT NOW(), |  | ||||||
|     set_by INT UNSIGNED, |  | ||||||
|  |  | ||||||
|     PRIMARY KEY (id), |  | ||||||
|     FOREIGN KEY (message_id) REFERENCES messages(id) ON DELETE RESTRICT, |  | ||||||
|     FOREIGN KEY (channel_id) REFERENCES channels(id) ON DELETE CASCADE, |  | ||||||
|     FOREIGN KEY (set_by) REFERENCES users(id) ON DELETE SET NULL |  | ||||||
| ); |  | ||||||
|  |  | ||||||
| CREATE TRIGGER message_cleanup AFTER DELETE ON reminders |  | ||||||
| FOR EACH ROW |  | ||||||
|     DELETE FROM messages WHERE id = OLD.message_id; |  | ||||||
|  |  | ||||||
| CREATE TRIGGER embed_cleanup AFTER DELETE ON messages |  | ||||||
| FOR EACH ROW |  | ||||||
|     DELETE FROM embeds WHERE id = OLD.embed_id; |  | ||||||
|  |  | ||||||
| CREATE TABLE todos ( |  | ||||||
|     id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL, |  | ||||||
|     user_id INT UNSIGNED, |  | ||||||
|     guild_id INT UNSIGNED, |  | ||||||
|     channel_id INT UNSIGNED, |  | ||||||
|     value VARCHAR(2000) NOT NULL, |  | ||||||
|  |  | ||||||
|     PRIMARY KEY (id), |  | ||||||
|     FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL, |  | ||||||
|     FOREIGN KEY (guild_id) REFERENCES guilds(id) ON DELETE CASCADE, |  | ||||||
|     FOREIGN KEY (channel_id) REFERENCES channels(id) ON DELETE SET NULL |  | ||||||
| ); |  | ||||||
|  |  | ||||||
| CREATE TABLE command_restrictions ( |  | ||||||
|     id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL, |  | ||||||
|  |  | ||||||
|     role_id INT UNSIGNED NOT NULL, |  | ||||||
|     command ENUM('todos', 'natural', 'remind', 'interval', 'timer', 'del', 'look', 'alias', 'countdown') NOT NULL, |  | ||||||
|  |  | ||||||
|     PRIMARY KEY (id), |  | ||||||
|     FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE, |  | ||||||
|     UNIQUE KEY (`role_id`, `command`) |  | ||||||
| ); |  | ||||||
|  |  | ||||||
| CREATE TABLE timers ( |  | ||||||
|     id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL, |  | ||||||
|     start_time TIMESTAMP NOT NULL DEFAULT NOW(), |  | ||||||
|     name VARCHAR(32) NOT NULL, |  | ||||||
|     owner BIGINT UNSIGNED NOT NULL, |  | ||||||
|  |  | ||||||
|     PRIMARY KEY (id) |  | ||||||
| ); |  | ||||||
|  |  | ||||||
| CREATE TABLE events ( |  | ||||||
|     id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL, |  | ||||||
|     `time` TIMESTAMP NOT NULL DEFAULT NOW(), |  | ||||||
|  |  | ||||||
|     event_name ENUM('edit', 'enable', 'disable', 'delete') NOT NULL, |  | ||||||
|     bulk_count INT UNSIGNED, |  | ||||||
|  |  | ||||||
|     guild_id INT UNSIGNED NOT NULL, |  | ||||||
|     user_id INT UNSIGNED, |  | ||||||
|     reminder_id INT UNSIGNED, |  | ||||||
|  |  | ||||||
|     PRIMARY KEY (id), |  | ||||||
|     FOREIGN KEY (guild_id) REFERENCES guilds(id) ON DELETE CASCADE, |  | ||||||
|     FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL, |  | ||||||
|     FOREIGN KEY (reminder_id) REFERENCES reminders(id) ON DELETE SET NULL |  | ||||||
| ); |  | ||||||
|  |  | ||||||
| CREATE TABLE command_aliases ( |  | ||||||
|     id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL, |  | ||||||
|  |  | ||||||
|     guild_id INT UNSIGNED NOT NULL, |  | ||||||
|     name VARCHAR(12) NOT NULL, |  | ||||||
|  |  | ||||||
|     command VARCHAR(2048) NOT NULL, |  | ||||||
|  |  | ||||||
|     PRIMARY KEY (id), |  | ||||||
|     FOREIGN KEY (guild_id) REFERENCES guilds(id) ON DELETE CASCADE, |  | ||||||
|     UNIQUE KEY (`guild_id`, `name`) |  | ||||||
| ); |  | ||||||
|  |  | ||||||
| CREATE TABLE guild_users ( |  | ||||||
|     guild INT UNSIGNED NOT NULL, |  | ||||||
|     user INT UNSIGNED NOT NULL, |  | ||||||
|  |  | ||||||
|     can_access BOOL NOT NULL DEFAULT 0, |  | ||||||
|  |  | ||||||
|     FOREIGN KEY (guild) REFERENCES guilds(id) ON DELETE CASCADE, |  | ||||||
|     FOREIGN KEY (user) REFERENCES users(id) ON DELETE CASCADE, |  | ||||||
|     UNIQUE KEY (guild, user) |  | ||||||
| ); |  | ||||||
|  |  | ||||||
| CREATE EVENT event_cleanup |  | ||||||
| ON SCHEDULE AT CURRENT_TIMESTAMP + INTERVAL 1 DAY |  | ||||||
| ON COMPLETION PRESERVE |  | ||||||
| DO DELETE FROM events WHERE `time` < DATE_SUB(NOW(), INTERVAL 5 DAY); |  | ||||||
| @@ -1,11 +0,0 @@ | |||||||
| CREATE TABLE macro ( |  | ||||||
|     id INT UNSIGNED AUTO_INCREMENT, |  | ||||||
|     guild_id INT UNSIGNED NOT NULL, |  | ||||||
|  |  | ||||||
|     name VARCHAR(100) NOT NULL, |  | ||||||
|     description VARCHAR(100), |  | ||||||
|     commands TEXT NOT NULL, |  | ||||||
|  |  | ||||||
|     FOREIGN KEY (guild_id) REFERENCES guilds(id) ON DELETE CASCADE, |  | ||||||
|     PRIMARY KEY (id) |  | ||||||
| ); |  | ||||||
| @@ -1,2 +0,0 @@ | |||||||
| ALTER TABLE reminders RENAME COLUMN `interval` TO `interval_seconds`; |  | ||||||
| ALTER TABLE reminders ADD COLUMN `interval_months` INT UNSIGNED DEFAULT NULL; |  | ||||||
| @@ -1,49 +0,0 @@ | |||||||
| CREATE TABLE reminder_template ( |  | ||||||
|     `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, |  | ||||||
|  |  | ||||||
|     `name` VARCHAR(24) NOT NULL DEFAULT 'Reminder', |  | ||||||
|  |  | ||||||
|     `guild_id` INT UNSIGNED NOT NULL, |  | ||||||
|  |  | ||||||
|     `username` VARCHAR(32) DEFAULT NULL, |  | ||||||
|     `avatar` VARCHAR(512) DEFAULT NULL, |  | ||||||
|  |  | ||||||
|     `content` VARCHAR(2048) NOT NULL DEFAULT '', |  | ||||||
|     `tts` BOOL NOT NULL DEFAULT 0, |  | ||||||
|     `attachment` MEDIUMBLOB, |  | ||||||
|     `attachment_name` VARCHAR(260), |  | ||||||
|  |  | ||||||
|     `embed_title` VARCHAR(256) NOT NULL DEFAULT '', |  | ||||||
|     `embed_description` VARCHAR(2048) NOT NULL DEFAULT '', |  | ||||||
|     `embed_image_url` VARCHAR(512), |  | ||||||
|     `embed_thumbnail_url` VARCHAR(512), |  | ||||||
|     `embed_footer` VARCHAR(2048) NOT NULL DEFAULT '', |  | ||||||
|     `embed_footer_url` VARCHAR(512), |  | ||||||
|     `embed_author` VARCHAR(256) NOT NULL DEFAULT '', |  | ||||||
|     `embed_author_url` VARCHAR(512), |  | ||||||
|     `embed_color` INT UNSIGNED NOT NULL DEFAULT 0x0, |  | ||||||
|     `embed_fields` JSON, |  | ||||||
|  |  | ||||||
|     PRIMARY KEY (id), |  | ||||||
|  |  | ||||||
|     FOREIGN KEY (`guild_id`) REFERENCES guilds (`id`) ON DELETE CASCADE |  | ||||||
| ); |  | ||||||
|  |  | ||||||
| ALTER TABLE reminders ADD COLUMN embed_fields JSON; |  | ||||||
|  |  | ||||||
| update reminders |  | ||||||
|     inner join embed_fields as E |  | ||||||
|     on E.reminder_id = reminders.id |  | ||||||
| set embed_fields = ( |  | ||||||
|     select JSON_ARRAYAGG( |  | ||||||
|         JSON_OBJECT( |  | ||||||
|             'title', E.title, |  | ||||||
|             'value', E.value, |  | ||||||
|             'inline', |  | ||||||
|             if(inline = 1, cast(TRUE as json), cast(FALSE as json)) |  | ||||||
|             ) |  | ||||||
|         ) |  | ||||||
|     from embed_fields |  | ||||||
|     group by reminder_id |  | ||||||
|     having reminder_id = reminders.id |  | ||||||
|     ); |  | ||||||
| @@ -1 +0,0 @@ | |||||||
| ALTER TABLE reminders ADD COLUMN `interval_days` INT UNSIGNED DEFAULT NULL; |  | ||||||
| @@ -1 +0,0 @@ | |||||||
| ALTER TABLE reminders ADD COLUMN `thread_id` BIGINT DEFAULT NULL; |  | ||||||
| @@ -1 +0,0 @@ | |||||||
| ALTER TABLE guilds ADD COLUMN ephemeral_confirmations BOOL NOT NULL DEFAULT 0; |  | ||||||
| @@ -1,2 +0,0 @@ | |||||||
| ALTER TABLE reminders MODIFY `name` VARCHAR(100) NOT NULL DEFAULT 'Reminder'; |  | ||||||
| ALTER TABLE reminder_template MODIFY `name` VARCHAR(100) NOT NULL DEFAULT 'Reminder'; |  | ||||||
| @@ -1,9 +0,0 @@ | |||||||
| CREATE TABLE stat ( |  | ||||||
|     `id` BIGINT NOT NULL AUTO_INCREMENT, |  | ||||||
|     `utc_time` DATETIME NOT NULL DEFAULT NOW(), |  | ||||||
|     `type` ENUM('reminder_sent', 'reminder_failed'), |  | ||||||
|     `reminder_id` INT UNSIGNED, |  | ||||||
|     `message` TEXT, |  | ||||||
|  |  | ||||||
|     PRIMARY KEY (`id`) |  | ||||||
| ); |  | ||||||
| @@ -1,2 +0,0 @@ | |||||||
| ALTER TABLE reminders ADD COLUMN `status` ENUM ('pending', 'sent', 'failed', 'deleted') NOT NULL DEFAULT 'pending'; |  | ||||||
| ALTER TABLE reminders ADD COLUMN `status_message` TEXT; |  | ||||||
| @@ -1,3 +0,0 @@ | |||||||
| ALTER TABLE `reminder_template` ADD COLUMN `interval_seconds` INT UNSIGNED; |  | ||||||
| ALTER TABLE `reminder_template` ADD COLUMN `interval_days` INT UNSIGNED; |  | ||||||
| ALTER TABLE `reminder_template` ADD COLUMN `interval_months` INT UNSIGNED; |  | ||||||
| @@ -1,50 +0,0 @@ | |||||||
| 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 |  | ||||||
| @@ -1,5 +0,0 @@ | |||||||
| -- Add migration script here |  | ||||||
| ALTER TABLE reminders |  | ||||||
|     ADD INDEX `utc_time_index` (`utc_time`); |  | ||||||
| ALTER TABLE reminders |  | ||||||
|     ADD INDEX `status_index` (`status`); |  | ||||||
							
								
								
									
										7
									
								
								models/Cargo.lock
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,7 @@ | |||||||
|  | # This file is automatically @generated by Cargo. | ||||||
|  | # It is not intended for manual editing. | ||||||
|  | version = 3 | ||||||
|  |  | ||||||
|  | [[package]] | ||||||
|  | name = "models" | ||||||
|  | version = "0.1.0" | ||||||
							
								
								
									
										8
									
								
								models/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,8 @@ | |||||||
|  | [package] | ||||||
|  | name = "models" | ||||||
|  | version = "0.1.0" | ||||||
|  | edition = "2021" | ||||||
|  |  | ||||||
|  | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html | ||||||
|  |  | ||||||
|  | [dependencies] | ||||||
							
								
								
									
										8
									
								
								models/entity/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,8 @@ | |||||||
|  | [package] | ||||||
|  | name = "entity" | ||||||
|  | version = "0.1.0" | ||||||
|  | edition = "2021" | ||||||
|  |  | ||||||
|  | [dependencies] | ||||||
|  | chrono-tz = "^0.6" | ||||||
|  | sea-orm = { version = "^0.8", features = ["sqlx-postgres", "runtime-tokio-native-tls", "macros"] } | ||||||
							
								
								
									
										60
									
								
								models/entity/src/channel.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,60 @@ | |||||||
|  | //! SeaORM Entity. Generated by sea-orm-codegen 0.8.0 | ||||||
|  |  | ||||||
|  | use sea_orm::entity::prelude::*; | ||||||
|  |  | ||||||
|  | #[derive(Clone, Debug, PartialEq, DeriveEntityModel)] | ||||||
|  | #[sea_orm(table_name = "channel")] | ||||||
|  | pub struct Model { | ||||||
|  |     #[sea_orm(primary_key, auto_increment = false)] | ||||||
|  |     pub id: i64, | ||||||
|  |     pub guild_id: Option<i64>, | ||||||
|  |     pub nudge: i32, | ||||||
|  |     pub webhook_id: Option<i64>, | ||||||
|  |     pub webhook_token: Option<String>, | ||||||
|  |     pub paused: bool, | ||||||
|  |     pub paused_until: Option<DateTimeUtc>, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] | ||||||
|  | pub enum Relation { | ||||||
|  |     #[sea_orm( | ||||||
|  |         belongs_to = "super::guild::Entity", | ||||||
|  |         from = "Column::GuildId", | ||||||
|  |         to = "super::guild::Column::Id", | ||||||
|  |         on_update = "NoAction", | ||||||
|  |         on_delete = "Cascade" | ||||||
|  |     )] | ||||||
|  |     Guild, | ||||||
|  |     #[sea_orm(has_many = "super::user::Entity")] | ||||||
|  |     User, | ||||||
|  |     #[sea_orm(has_many = "super::reminder::Entity")] | ||||||
|  |     Reminder, | ||||||
|  |     #[sea_orm(has_many = "super::todo::Entity")] | ||||||
|  |     Todo, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl Related<super::guild::Entity> for Entity { | ||||||
|  |     fn to() -> RelationDef { | ||||||
|  |         Relation::Guild.def() | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl Related<super::user::Entity> for Entity { | ||||||
|  |     fn to() -> RelationDef { | ||||||
|  |         Relation::User.def() | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl Related<super::reminder::Entity> for Entity { | ||||||
|  |     fn to() -> RelationDef { | ||||||
|  |         Relation::Reminder.def() | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl Related<super::todo::Entity> for Entity { | ||||||
|  |     fn to() -> RelationDef { | ||||||
|  |         Relation::Todo.def() | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl ActiveModelBehavior for ActiveModel {} | ||||||
							
								
								
									
										34
									
								
								models/entity/src/command_macro.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,34 @@ | |||||||
|  | //! SeaORM Entity. Generated by sea-orm-codegen 0.8.0 | ||||||
|  |  | ||||||
|  | use sea_orm::entity::prelude::*; | ||||||
|  |  | ||||||
|  | #[derive(Clone, Debug, PartialEq, DeriveEntityModel)] | ||||||
|  | #[sea_orm(table_name = "command_macro")] | ||||||
|  | pub struct Model { | ||||||
|  |     #[sea_orm(primary_key)] | ||||||
|  |     pub id: i32, | ||||||
|  |     pub guild_id: i64, | ||||||
|  |     pub name: String, | ||||||
|  |     pub description: Option<String>, | ||||||
|  |     pub commands: Option<Json>, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] | ||||||
|  | pub enum Relation { | ||||||
|  |     #[sea_orm( | ||||||
|  |         belongs_to = "super::guild::Entity", | ||||||
|  |         from = "Column::GuildId", | ||||||
|  |         to = "super::guild::Column::Id", | ||||||
|  |         on_update = "NoAction", | ||||||
|  |         on_delete = "Cascade" | ||||||
|  |     )] | ||||||
|  |     Guild, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl Related<super::guild::Entity> for Entity { | ||||||
|  |     fn to() -> RelationDef { | ||||||
|  |         Relation::Guild.def() | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl ActiveModelBehavior for ActiveModel {} | ||||||
							
								
								
									
										48
									
								
								models/entity/src/guild.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,48 @@ | |||||||
|  | //! SeaORM Entity. Generated by sea-orm-codegen 0.8.0 | ||||||
|  |  | ||||||
|  | use sea_orm::entity::prelude::*; | ||||||
|  |  | ||||||
|  | #[derive(Clone, Debug, PartialEq, DeriveEntityModel)] | ||||||
|  | #[sea_orm(table_name = "guild")] | ||||||
|  | pub struct Model { | ||||||
|  |     #[sea_orm(primary_key, auto_increment = false)] | ||||||
|  |     pub id: i64, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] | ||||||
|  | pub enum Relation { | ||||||
|  |     #[sea_orm(has_many = "super::reminder_template::Entity")] | ||||||
|  |     ReminderTemplate, | ||||||
|  |     #[sea_orm(has_many = "super::channel::Entity")] | ||||||
|  |     Channel, | ||||||
|  |     #[sea_orm(has_many = "super::todo::Entity")] | ||||||
|  |     Todo, | ||||||
|  |     #[sea_orm(has_many = "super::command_macro::Entity")] | ||||||
|  |     CommandMacro, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl Related<super::reminder_template::Entity> for Entity { | ||||||
|  |     fn to() -> RelationDef { | ||||||
|  |         Relation::ReminderTemplate.def() | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl Related<super::channel::Entity> for Entity { | ||||||
|  |     fn to() -> RelationDef { | ||||||
|  |         Relation::Channel.def() | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl Related<super::todo::Entity> for Entity { | ||||||
|  |     fn to() -> RelationDef { | ||||||
|  |         Relation::Todo.def() | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl Related<super::command_macro::Entity> for Entity { | ||||||
|  |     fn to() -> RelationDef { | ||||||
|  |         Relation::CommandMacro.def() | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl ActiveModelBehavior for ActiveModel {} | ||||||
							
								
								
									
										1
									
								
								models/entity/src/lib.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1 @@ | |||||||
|  |  | ||||||
							
								
								
									
										14
									
								
								models/entity/src/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,14 @@ | |||||||
|  | //! SeaORM Entity. Generated by sea-orm-codegen 0.8.0 | ||||||
|  |  | ||||||
|  | pub mod prelude; | ||||||
|  |  | ||||||
|  | pub mod channel; | ||||||
|  | pub mod command_macro; | ||||||
|  | pub mod guild; | ||||||
|  | pub mod reminder; | ||||||
|  | pub mod reminder_template; | ||||||
|  | pub mod sea_orm_active_enums; | ||||||
|  | pub mod seaql_migrations; | ||||||
|  | pub mod timer; | ||||||
|  | pub mod todo; | ||||||
|  | pub mod user; | ||||||
							
								
								
									
										8
									
								
								models/entity/src/prelude.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,8 @@ | |||||||
|  | //! SeaORM Entity. Generated by sea-orm-codegen 0.8.0 | ||||||
|  |  | ||||||
|  | pub use super::{ | ||||||
|  |     channel::Entity as Channel, command_macro::Entity as CommandMacro, guild::Entity as Guild, | ||||||
|  |     reminder::Entity as Reminder, reminder_template::Entity as ReminderTemplate, | ||||||
|  |     seaql_migrations::Entity as SeaqlMigrations, timer::Entity as Timer, todo::Entity as Todo, | ||||||
|  |     user::Entity as User, | ||||||
|  | }; | ||||||
							
								
								
									
										73
									
								
								models/entity/src/reminder.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,73 @@ | |||||||
|  | //! SeaORM Entity. Generated by sea-orm-codegen 0.8.0 | ||||||
|  |  | ||||||
|  | use sea_orm::entity::prelude::*; | ||||||
|  |  | ||||||
|  | use super::sea_orm_active_enums::Timezone; | ||||||
|  |  | ||||||
|  | #[derive(Clone, Debug, PartialEq, DeriveEntityModel)] | ||||||
|  | #[sea_orm(table_name = "reminder")] | ||||||
|  | pub struct Model { | ||||||
|  |     #[sea_orm(primary_key)] | ||||||
|  |     pub id: i32, | ||||||
|  |     pub uid: String, | ||||||
|  |     pub name: String, | ||||||
|  |     pub channel_id: i64, | ||||||
|  |     pub utc_time: DateTimeUtc, | ||||||
|  |     pub timezone: Timezone, | ||||||
|  |     pub interval_seconds: Option<i32>, | ||||||
|  |     pub interval_months: Option<i32>, | ||||||
|  |     pub enabled: bool, | ||||||
|  |     pub expires: Option<DateTimeUtc>, | ||||||
|  |     pub username: Option<String>, | ||||||
|  |     pub avatar: Option<String>, | ||||||
|  |     pub content: Option<String>, | ||||||
|  |     pub tts: bool, | ||||||
|  |     pub attachment: Option<Vec<u8>>, | ||||||
|  |     pub attachment_name: Option<String>, | ||||||
|  |     pub embed_title: Option<String>, | ||||||
|  |     pub embed_description: Option<String>, | ||||||
|  |     pub embed_image_url: Option<String>, | ||||||
|  |     pub embed_thumbnail_url: Option<String>, | ||||||
|  |     pub embed_footer: Option<String>, | ||||||
|  |     pub embed_footer_url: Option<String>, | ||||||
|  |     pub embed_author: Option<String>, | ||||||
|  |     pub embed_author_url: Option<String>, | ||||||
|  |     pub embed_color: Option<i32>, | ||||||
|  |     pub embed_fields: Option<Json>, | ||||||
|  |     pub set_at: DateTimeUtc, | ||||||
|  |     pub set_by: i64, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] | ||||||
|  | pub enum Relation { | ||||||
|  |     #[sea_orm( | ||||||
|  |         belongs_to = "super::channel::Entity", | ||||||
|  |         from = "Column::ChannelId", | ||||||
|  |         to = "super::channel::Column::Id", | ||||||
|  |         on_update = "NoAction", | ||||||
|  |         on_delete = "Cascade" | ||||||
|  |     )] | ||||||
|  |     Channel, | ||||||
|  |     #[sea_orm( | ||||||
|  |         belongs_to = "super::user::Entity", | ||||||
|  |         from = "Column::SetBy", | ||||||
|  |         to = "super::user::Column::Id", | ||||||
|  |         on_update = "NoAction", | ||||||
|  |         on_delete = "Cascade" | ||||||
|  |     )] | ||||||
|  |     User, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl Related<super::channel::Entity> for Entity { | ||||||
|  |     fn to() -> RelationDef { | ||||||
|  |         Relation::Channel.def() | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl Related<super::user::Entity> for Entity { | ||||||
|  |     fn to() -> RelationDef { | ||||||
|  |         Relation::User.def() | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl ActiveModelBehavior for ActiveModel {} | ||||||
							
								
								
									
										48
									
								
								models/entity/src/reminder_template.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,48 @@ | |||||||
|  | //! SeaORM Entity. Generated by sea-orm-codegen 0.8.0 | ||||||
|  |  | ||||||
|  | use sea_orm::entity::prelude::*; | ||||||
|  |  | ||||||
|  | #[derive(Clone, Debug, PartialEq, DeriveEntityModel)] | ||||||
|  | #[sea_orm(table_name = "reminder_template")] | ||||||
|  | pub struct Model { | ||||||
|  |     #[sea_orm(primary_key)] | ||||||
|  |     pub id: i32, | ||||||
|  |     pub guild_id: i64, | ||||||
|  |     pub name: String, | ||||||
|  |     pub username: Option<String>, | ||||||
|  |     pub avatar: Option<String>, | ||||||
|  |     pub content: Option<String>, | ||||||
|  |     pub tts: bool, | ||||||
|  |     pub attachment: Option<Vec<u8>>, | ||||||
|  |     pub attachment_name: Option<String>, | ||||||
|  |     pub embed_title: Option<String>, | ||||||
|  |     pub embed_description: Option<String>, | ||||||
|  |     pub embed_image_url: Option<String>, | ||||||
|  |     pub embed_thumbnail_url: Option<String>, | ||||||
|  |     pub embed_footer: Option<String>, | ||||||
|  |     pub embed_footer_url: Option<String>, | ||||||
|  |     pub embed_author: Option<String>, | ||||||
|  |     pub embed_author_url: Option<String>, | ||||||
|  |     pub embed_color: Option<i32>, | ||||||
|  |     pub embed_fields: Option<Json>, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] | ||||||
|  | pub enum Relation { | ||||||
|  |     #[sea_orm( | ||||||
|  |         belongs_to = "super::guild::Entity", | ||||||
|  |         from = "Column::GuildId", | ||||||
|  |         to = "super::guild::Column::Id", | ||||||
|  |         on_update = "NoAction", | ||||||
|  |         on_delete = "Cascade" | ||||||
|  |     )] | ||||||
|  |     Guild, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl Related<super::guild::Entity> for Entity { | ||||||
|  |     fn to() -> RelationDef { | ||||||
|  |         Relation::Guild.def() | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl ActiveModelBehavior for ActiveModel {} | ||||||
							
								
								
									
										1196
									
								
								models/entity/src/sea_orm_active_enums.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										22
									
								
								models/entity/src/seaql_migrations.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,22 @@ | |||||||
|  | //! SeaORM Entity. Generated by sea-orm-codegen 0.8.0 | ||||||
|  |  | ||||||
|  | use sea_orm::entity::prelude::*; | ||||||
|  |  | ||||||
|  | #[derive(Clone, Debug, PartialEq, DeriveEntityModel)] | ||||||
|  | #[sea_orm(table_name = "seaql_migrations")] | ||||||
|  | pub struct Model { | ||||||
|  |     #[sea_orm(primary_key, auto_increment = false)] | ||||||
|  |     pub version: String, | ||||||
|  |     pub applied_at: i64, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[derive(Copy, Clone, Debug, EnumIter)] | ||||||
|  | pub enum Relation {} | ||||||
|  |  | ||||||
|  | impl RelationTrait for Relation { | ||||||
|  |     fn def(&self) -> RelationDef { | ||||||
|  |         panic!("No RelationDef") | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl ActiveModelBehavior for ActiveModel {} | ||||||
							
								
								
									
										36
									
								
								models/entity/src/timer.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,36 @@ | |||||||
|  | //! SeaORM Entity. Generated by sea-orm-codegen 0.8.0 | ||||||
|  |  | ||||||
|  | use sea_orm::entity::prelude::*; | ||||||
|  |  | ||||||
|  | #[derive(Clone, Debug, PartialEq, DeriveEntityModel)] | ||||||
|  | #[sea_orm(table_name = "timer")] | ||||||
|  | pub struct Model { | ||||||
|  |     #[sea_orm(primary_key)] | ||||||
|  |     pub id: i32, | ||||||
|  |     pub start_time: DateTimeUtc, | ||||||
|  |     pub name: String, | ||||||
|  |     pub user_id: Option<i64>, | ||||||
|  |     pub guild_id: Option<i64>, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] | ||||||
|  | pub enum Relation { | ||||||
|  |     #[sea_orm( | ||||||
|  |         belongs_to = "super::guild::Entity", | ||||||
|  |         from = "Column::GuildId", | ||||||
|  |         to = "super::guild::Column::Id", | ||||||
|  |         on_update = "NoAction", | ||||||
|  |         on_delete = "Cascade" | ||||||
|  |     )] | ||||||
|  |     Guild2, | ||||||
|  |     #[sea_orm( | ||||||
|  |         belongs_to = "super::guild::Entity", | ||||||
|  |         from = "Column::UserId", | ||||||
|  |         to = "super::guild::Column::Id", | ||||||
|  |         on_update = "NoAction", | ||||||
|  |         on_delete = "Cascade" | ||||||
|  |     )] | ||||||
|  |     Guild1, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl ActiveModelBehavior for ActiveModel {} | ||||||
							
								
								
									
										62
									
								
								models/entity/src/todo.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,62 @@ | |||||||
|  | //! SeaORM Entity. Generated by sea-orm-codegen 0.8.0 | ||||||
|  |  | ||||||
|  | use sea_orm::entity::prelude::*; | ||||||
|  |  | ||||||
|  | #[derive(Clone, Debug, PartialEq, DeriveEntityModel)] | ||||||
|  | #[sea_orm(table_name = "todo")] | ||||||
|  | pub struct Model { | ||||||
|  |     #[sea_orm(primary_key)] | ||||||
|  |     pub id: i32, | ||||||
|  |     pub user_id: Option<i64>, | ||||||
|  |     pub guild_id: Option<i64>, | ||||||
|  |     pub channel_id: Option<i64>, | ||||||
|  |     pub value: String, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] | ||||||
|  | pub enum Relation { | ||||||
|  |     #[sea_orm( | ||||||
|  |         belongs_to = "super::channel::Entity", | ||||||
|  |         from = "Column::ChannelId", | ||||||
|  |         to = "super::channel::Column::Id", | ||||||
|  |         on_update = "NoAction", | ||||||
|  |         on_delete = "Cascade" | ||||||
|  |     )] | ||||||
|  |     Channel, | ||||||
|  |     #[sea_orm( | ||||||
|  |         belongs_to = "super::guild::Entity", | ||||||
|  |         from = "Column::GuildId", | ||||||
|  |         to = "super::guild::Column::Id", | ||||||
|  |         on_update = "NoAction", | ||||||
|  |         on_delete = "Cascade" | ||||||
|  |     )] | ||||||
|  |     Guild, | ||||||
|  |     #[sea_orm( | ||||||
|  |         belongs_to = "super::user::Entity", | ||||||
|  |         from = "Column::UserId", | ||||||
|  |         to = "super::user::Column::Id", | ||||||
|  |         on_update = "NoAction", | ||||||
|  |         on_delete = "Cascade" | ||||||
|  |     )] | ||||||
|  |     User, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl Related<super::channel::Entity> for Entity { | ||||||
|  |     fn to() -> RelationDef { | ||||||
|  |         Relation::Channel.def() | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl Related<super::guild::Entity> for Entity { | ||||||
|  |     fn to() -> RelationDef { | ||||||
|  |         Relation::Guild.def() | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl Related<super::user::Entity> for Entity { | ||||||
|  |     fn to() -> RelationDef { | ||||||
|  |         Relation::User.def() | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl ActiveModelBehavior for ActiveModel {} | ||||||
							
								
								
									
										50
									
								
								models/entity/src/user.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,50 @@ | |||||||
|  | //! SeaORM Entity. Generated by sea-orm-codegen 0.8.0 | ||||||
|  |  | ||||||
|  | use sea_orm::entity::prelude::*; | ||||||
|  |  | ||||||
|  | use super::sea_orm_active_enums::Timezone; | ||||||
|  |  | ||||||
|  | #[derive(Clone, Debug, PartialEq, DeriveEntityModel)] | ||||||
|  | #[sea_orm(table_name = "user")] | ||||||
|  | pub struct Model { | ||||||
|  |     #[sea_orm(primary_key, auto_increment = false)] | ||||||
|  |     pub id: i64, | ||||||
|  |     pub dm_channel: i64, | ||||||
|  |     pub timezone: Timezone, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] | ||||||
|  | pub enum Relation { | ||||||
|  |     #[sea_orm( | ||||||
|  |         belongs_to = "super::channel::Entity", | ||||||
|  |         from = "Column::DmChannel", | ||||||
|  |         to = "super::channel::Column::Id", | ||||||
|  |         on_update = "NoAction", | ||||||
|  |         on_delete = "Cascade" | ||||||
|  |     )] | ||||||
|  |     Channel, | ||||||
|  |     #[sea_orm(has_many = "super::reminder::Entity")] | ||||||
|  |     Reminder, | ||||||
|  |     #[sea_orm(has_many = "super::todo::Entity")] | ||||||
|  |     Todo, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl Related<super::channel::Entity> for Entity { | ||||||
|  |     fn to() -> RelationDef { | ||||||
|  |         Relation::Channel.def() | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl Related<super::reminder::Entity> for Entity { | ||||||
|  |     fn to() -> RelationDef { | ||||||
|  |         Relation::Reminder.def() | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl Related<super::todo::Entity> for Entity { | ||||||
|  |     fn to() -> RelationDef { | ||||||
|  |         Relation::Todo.def() | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl ActiveModelBehavior for ActiveModel {} | ||||||
							
								
								
									
										2400
									
								
								models/migration/Cargo.lock
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										16
									
								
								models/migration/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,16 @@ | |||||||
|  | [package] | ||||||
|  | name = "migration" | ||||||
|  | version = "0.1.0" | ||||||
|  | edition = "2021" | ||||||
|  | publish = false | ||||||
|  |  | ||||||
|  | [lib] | ||||||
|  | name = "migration" | ||||||
|  | path = "src/lib.rs" | ||||||
|  |  | ||||||
|  | [dependencies] | ||||||
|  | entity = { path = "../entity" } | ||||||
|  | chrono-tz = "^0.6" | ||||||
|  |  | ||||||
|  | [dependencies.sea-orm-migration] | ||||||
|  | version = "^0.8.0" | ||||||
							
								
								
									
										37
									
								
								models/migration/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,37 @@ | |||||||
|  | # Running Migrator CLI | ||||||
|  |  | ||||||
|  | - Apply all pending migrations | ||||||
|  |     ```sh | ||||||
|  |     cargo run | ||||||
|  |     ``` | ||||||
|  |     ```sh | ||||||
|  |     cargo run -- up | ||||||
|  |     ``` | ||||||
|  | - Apply first 10 pending migrations | ||||||
|  |     ```sh | ||||||
|  |     cargo run -- up -n 10 | ||||||
|  |     ``` | ||||||
|  | - Rollback last applied migrations | ||||||
|  |     ```sh | ||||||
|  |     cargo run -- down | ||||||
|  |     ``` | ||||||
|  | - Rollback last 10 applied migrations | ||||||
|  |     ```sh | ||||||
|  |     cargo run -- down -n 10 | ||||||
|  |     ``` | ||||||
|  | - Drop all tables from the database, then reapply all migrations | ||||||
|  |     ```sh | ||||||
|  |     cargo run -- fresh | ||||||
|  |     ``` | ||||||
|  | - Rollback all applied migrations, then reapply all migrations | ||||||
|  |     ```sh | ||||||
|  |     cargo run -- refresh | ||||||
|  |     ``` | ||||||
|  | - Rollback all applied migrations | ||||||
|  |     ```sh | ||||||
|  |     cargo run -- reset | ||||||
|  |     ``` | ||||||
|  | - Check the status of all migrations | ||||||
|  |     ```sh | ||||||
|  |     cargo run -- status | ||||||
|  |     ``` | ||||||
							
								
								
									
										12
									
								
								models/migration/src/lib.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,12 @@ | |||||||
|  | pub use sea_orm_migration::prelude::*; | ||||||
|  |  | ||||||
|  | mod m20220101_000001_create_table; | ||||||
|  |  | ||||||
|  | pub struct Migrator; | ||||||
|  |  | ||||||
|  | #[async_trait::async_trait] | ||||||
|  | impl MigratorTrait for Migrator { | ||||||
|  |     fn migrations() -> Vec<Box<dyn MigrationTrait>> { | ||||||
|  |         vec![Box::new(m20220101_000001_create_table::Migration)] | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										553
									
								
								models/migration/src/m20220101_000001_create_table.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,553 @@ | |||||||
|  | use chrono_tz::{Tz, TZ_VARIANTS}; | ||||||
|  | use sea_orm_migration::prelude::*; | ||||||
|  |  | ||||||
|  | use crate::extension::postgres::Type; | ||||||
|  |  | ||||||
|  | pub struct Migration; | ||||||
|  |  | ||||||
|  | impl MigrationName for Migration { | ||||||
|  |     fn name(&self) -> &str { | ||||||
|  |         "m20220101_000001_create_table" | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[derive(Iden)] | ||||||
|  | pub enum Guild { | ||||||
|  |     Table, | ||||||
|  |     Id, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[derive(Iden)] | ||||||
|  | pub enum Channel { | ||||||
|  |     Table, | ||||||
|  |     Id, | ||||||
|  |     GuildId, | ||||||
|  |     Nudge, | ||||||
|  |     WebhookId, | ||||||
|  |     WebhookToken, | ||||||
|  |     Paused, | ||||||
|  |     PausedUntil, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[derive(Iden)] | ||||||
|  | pub enum User { | ||||||
|  |     Table, | ||||||
|  |     Id, | ||||||
|  |     DmChannel, | ||||||
|  |     Timezone, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[derive(Iden)] | ||||||
|  | pub enum Reminder { | ||||||
|  |     Table, | ||||||
|  |     Id, | ||||||
|  |     Uid, | ||||||
|  |     Name, | ||||||
|  |     ChannelId, | ||||||
|  |     UtcTime, | ||||||
|  |     Timezone, | ||||||
|  |     IntervalSeconds, | ||||||
|  |     IntervalMonths, | ||||||
|  |     Enabled, | ||||||
|  |     Expires, | ||||||
|  |     Username, | ||||||
|  |     Avatar, | ||||||
|  |     Content, | ||||||
|  |     Tts, | ||||||
|  |     Attachment, | ||||||
|  |     AttachmentName, | ||||||
|  |     EmbedTitle, | ||||||
|  |     EmbedDescription, | ||||||
|  |     EmbedImageUrl, | ||||||
|  |     EmbedThumbnailUrl, | ||||||
|  |     EmbedFooter, | ||||||
|  |     EmbedFooterUrl, | ||||||
|  |     EmbedAuthor, | ||||||
|  |     EmbedAuthorUrl, | ||||||
|  |     EmbedColor, | ||||||
|  |     EmbedFields, | ||||||
|  |     SetAt, | ||||||
|  |     SetBy, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[derive(Iden)] | ||||||
|  | pub enum ReminderTemplate { | ||||||
|  |     Table, | ||||||
|  |     Id, | ||||||
|  |     GuildId, | ||||||
|  |     Name, | ||||||
|  |     Username, | ||||||
|  |     Avatar, | ||||||
|  |     Content, | ||||||
|  |     Tts, | ||||||
|  |     Attachment, | ||||||
|  |     AttachmentName, | ||||||
|  |     EmbedTitle, | ||||||
|  |     EmbedDescription, | ||||||
|  |     EmbedImageUrl, | ||||||
|  |     EmbedThumbnailUrl, | ||||||
|  |     EmbedFooter, | ||||||
|  |     EmbedFooterUrl, | ||||||
|  |     EmbedAuthor, | ||||||
|  |     EmbedAuthorUrl, | ||||||
|  |     EmbedColor, | ||||||
|  |     EmbedFields, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[derive(Iden)] | ||||||
|  | pub enum Timer { | ||||||
|  |     Table, | ||||||
|  |     Id, | ||||||
|  |     StartTime, | ||||||
|  |     Name, | ||||||
|  |     UserId, | ||||||
|  |     GuildId, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[derive(Iden)] | ||||||
|  | pub enum Todo { | ||||||
|  |     Table, | ||||||
|  |     Id, | ||||||
|  |     UserId, | ||||||
|  |     GuildId, | ||||||
|  |     ChannelId, | ||||||
|  |     Value, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[derive(Iden)] | ||||||
|  | pub enum CommandMacro { | ||||||
|  |     Table, | ||||||
|  |     Id, | ||||||
|  |     GuildId, | ||||||
|  |     Name, | ||||||
|  |     Description, | ||||||
|  |     Commands, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | pub enum Timezone { | ||||||
|  |     Type, | ||||||
|  |     Tz(Tz), | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl Iden for Timezone { | ||||||
|  |     fn unquoted(&self, s: &mut dyn Write) { | ||||||
|  |         write!( | ||||||
|  |             s, | ||||||
|  |             "{}", | ||||||
|  |             match self { | ||||||
|  |                 Self::Type => "timezone".to_string(), | ||||||
|  |                 Self::Tz(tz) => tz.to_string(), | ||||||
|  |             } | ||||||
|  |         ) | ||||||
|  |         .unwrap(); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[async_trait::async_trait] | ||||||
|  | impl MigrationTrait for Migration { | ||||||
|  |     async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { | ||||||
|  |         manager | ||||||
|  |             .create_type( | ||||||
|  |                 Type::create() | ||||||
|  |                     .as_enum(Timezone::Type) | ||||||
|  |                     .values(TZ_VARIANTS.iter().map(|tz| Timezone::Tz(tz.to_owned()))) | ||||||
|  |                     .to_owned(), | ||||||
|  |             ) | ||||||
|  |             .await?; | ||||||
|  |  | ||||||
|  |         manager | ||||||
|  |             .create_table( | ||||||
|  |                 Table::create() | ||||||
|  |                     .table(Guild::Table) | ||||||
|  |                     .if_not_exists() | ||||||
|  |                     .col(ColumnDef::new(Guild::Id).big_integer().not_null().primary_key()) | ||||||
|  |                     .to_owned(), | ||||||
|  |             ) | ||||||
|  |             .await?; | ||||||
|  |  | ||||||
|  |         manager | ||||||
|  |             .create_table( | ||||||
|  |                 Table::create() | ||||||
|  |                     .table(Channel::Table) | ||||||
|  |                     .if_not_exists() | ||||||
|  |                     .col(ColumnDef::new(Channel::Id).big_integer().not_null().primary_key()) | ||||||
|  |                     .col(ColumnDef::new(Channel::GuildId).big_integer()) | ||||||
|  |                     .col(ColumnDef::new(Channel::Nudge).integer().not_null().default(0)) | ||||||
|  |                     .col(ColumnDef::new(Channel::WebhookId).big_integer()) | ||||||
|  |                     .col(ColumnDef::new(Channel::WebhookToken).string()) | ||||||
|  |                     .col(ColumnDef::new(Channel::Paused).boolean().not_null().default(false)) | ||||||
|  |                     .col(ColumnDef::new(Channel::PausedUntil).date_time()) | ||||||
|  |                     .to_owned(), | ||||||
|  |             ) | ||||||
|  |             .await?; | ||||||
|  |  | ||||||
|  |         manager | ||||||
|  |             .create_foreign_key( | ||||||
|  |                 ForeignKey::create() | ||||||
|  |                     .name("fk_channel_guild") | ||||||
|  |                     .from(Channel::Table, Channel::GuildId) | ||||||
|  |                     .to(Guild::Table, Guild::Id) | ||||||
|  |                     .on_delete(ForeignKeyAction::Cascade) | ||||||
|  |                     .to_owned(), | ||||||
|  |             ) | ||||||
|  |             .await?; | ||||||
|  |  | ||||||
|  |         manager | ||||||
|  |             .create_table( | ||||||
|  |                 Table::create() | ||||||
|  |                     .table(User::Table) | ||||||
|  |                     .if_not_exists() | ||||||
|  |                     .col(ColumnDef::new(User::Id).big_integer().not_null().primary_key()) | ||||||
|  |                     .col(ColumnDef::new(User::DmChannel).big_integer().not_null()) | ||||||
|  |                     .col( | ||||||
|  |                         ColumnDef::new(User::Timezone) | ||||||
|  |                             .custom(Timezone::Type) | ||||||
|  |                             .not_null() | ||||||
|  |                             .default("UTC"), | ||||||
|  |                     ) | ||||||
|  |                     .to_owned(), | ||||||
|  |             ) | ||||||
|  |             .await?; | ||||||
|  |  | ||||||
|  |         manager | ||||||
|  |             .create_foreign_key( | ||||||
|  |                 ForeignKey::create() | ||||||
|  |                     .name("fk_user_channel") | ||||||
|  |                     .from(User::Table, User::DmChannel) | ||||||
|  |                     .to(Channel::Table, Channel::Id) | ||||||
|  |                     .on_delete(ForeignKeyAction::Cascade) | ||||||
|  |                     .to_owned(), | ||||||
|  |             ) | ||||||
|  |             .await?; | ||||||
|  |  | ||||||
|  |         manager | ||||||
|  |             .create_table( | ||||||
|  |                 Table::create() | ||||||
|  |                     .table(Reminder::Table) | ||||||
|  |                     .if_not_exists() | ||||||
|  |                     .col( | ||||||
|  |                         ColumnDef::new(Reminder::Id) | ||||||
|  |                             .integer() | ||||||
|  |                             .not_null() | ||||||
|  |                             .auto_increment() | ||||||
|  |                             .primary_key(), | ||||||
|  |                     ) | ||||||
|  |                     .col(ColumnDef::new(Reminder::Uid).string().char_len(64).not_null()) | ||||||
|  |                     .col( | ||||||
|  |                         ColumnDef::new(Reminder::Name) | ||||||
|  |                             .string() | ||||||
|  |                             .char_len(24) | ||||||
|  |                             .default("Reminder") | ||||||
|  |                             .not_null(), | ||||||
|  |                     ) | ||||||
|  |                     .col(ColumnDef::new(Reminder::ChannelId).big_integer().not_null()) | ||||||
|  |                     .col(ColumnDef::new(Reminder::UtcTime).date_time().not_null()) | ||||||
|  |                     .col( | ||||||
|  |                         ColumnDef::new(Reminder::Timezone) | ||||||
|  |                             .custom(Timezone::Type) | ||||||
|  |                             .not_null() | ||||||
|  |                             .default("UTC"), | ||||||
|  |                     ) | ||||||
|  |                     .col(ColumnDef::new(Reminder::IntervalSeconds).integer()) | ||||||
|  |                     .col(ColumnDef::new(Reminder::IntervalMonths).integer()) | ||||||
|  |                     .col(ColumnDef::new(Reminder::Enabled).boolean().not_null().default(false)) | ||||||
|  |                     .col(ColumnDef::new(Reminder::Expires).date_time()) | ||||||
|  |                     .col(ColumnDef::new(Reminder::Username).string_len(32)) | ||||||
|  |                     .col(ColumnDef::new(Reminder::Avatar).string_len(512)) | ||||||
|  |                     .col(ColumnDef::new(Reminder::Content).string_len(2000)) | ||||||
|  |                     .col(ColumnDef::new(Reminder::Tts).boolean().not_null().default(false)) | ||||||
|  |                     .col(ColumnDef::new(Reminder::Attachment).binary_len(8 * 1024 * 1024)) | ||||||
|  |                     .col(ColumnDef::new(Reminder::AttachmentName).string_len(260)) | ||||||
|  |                     .col(ColumnDef::new(Reminder::EmbedTitle).string_len(256)) | ||||||
|  |                     .col(ColumnDef::new(Reminder::EmbedDescription).string_len(4096)) | ||||||
|  |                     .col(ColumnDef::new(Reminder::EmbedImageUrl).string_len(500)) | ||||||
|  |                     .col(ColumnDef::new(Reminder::EmbedThumbnailUrl).string_len(500)) | ||||||
|  |                     .col(ColumnDef::new(Reminder::EmbedFooter).string_len(2048)) | ||||||
|  |                     .col(ColumnDef::new(Reminder::EmbedFooterUrl).string_len(500)) | ||||||
|  |                     .col(ColumnDef::new(Reminder::EmbedAuthor).string_len(256)) | ||||||
|  |                     .col(ColumnDef::new(Reminder::EmbedAuthorUrl).string_len(500)) | ||||||
|  |                     .col(ColumnDef::new(Reminder::EmbedColor).integer()) | ||||||
|  |                     .col(ColumnDef::new(Reminder::EmbedFields).json()) | ||||||
|  |                     .col(ColumnDef::new(Reminder::SetAt).date_time().not_null().default("NOW()")) | ||||||
|  |                     .col(ColumnDef::new(Reminder::SetBy).big_integer().not_null()) | ||||||
|  |                     .to_owned(), | ||||||
|  |             ) | ||||||
|  |             .await?; | ||||||
|  |  | ||||||
|  |         manager | ||||||
|  |             .create_foreign_key( | ||||||
|  |                 ForeignKey::create() | ||||||
|  |                     .name("fk_reminder_channel") | ||||||
|  |                     .from(Reminder::Table, Reminder::ChannelId) | ||||||
|  |                     .to(Channel::Table, Channel::Id) | ||||||
|  |                     .on_delete(ForeignKeyAction::Cascade) | ||||||
|  |                     .to_owned(), | ||||||
|  |             ) | ||||||
|  |             .await?; | ||||||
|  |  | ||||||
|  |         manager | ||||||
|  |             .create_foreign_key( | ||||||
|  |                 ForeignKey::create() | ||||||
|  |                     .name("fk_reminder_user") | ||||||
|  |                     .from(Reminder::Table, Reminder::SetBy) | ||||||
|  |                     .to(User::Table, User::Id) | ||||||
|  |                     .on_delete(ForeignKeyAction::Cascade) | ||||||
|  |                     .to_owned(), | ||||||
|  |             ) | ||||||
|  |             .await?; | ||||||
|  |  | ||||||
|  |         manager | ||||||
|  |             .create_table( | ||||||
|  |                 Table::create() | ||||||
|  |                     .table(ReminderTemplate::Table) | ||||||
|  |                     .if_not_exists() | ||||||
|  |                     .col( | ||||||
|  |                         ColumnDef::new(ReminderTemplate::Id) | ||||||
|  |                             .integer() | ||||||
|  |                             .not_null() | ||||||
|  |                             .auto_increment() | ||||||
|  |                             .primary_key(), | ||||||
|  |                     ) | ||||||
|  |                     .col(ColumnDef::new(ReminderTemplate::GuildId).big_integer().not_null()) | ||||||
|  |                     .col( | ||||||
|  |                         ColumnDef::new(ReminderTemplate::Name) | ||||||
|  |                             .string() | ||||||
|  |                             .char_len(24) | ||||||
|  |                             .default("Reminder") | ||||||
|  |                             .not_null(), | ||||||
|  |                     ) | ||||||
|  |                     .col(ColumnDef::new(ReminderTemplate::Username).string_len(32)) | ||||||
|  |                     .col(ColumnDef::new(ReminderTemplate::Avatar).string_len(512)) | ||||||
|  |                     .col(ColumnDef::new(ReminderTemplate::Content).string_len(2000)) | ||||||
|  |                     .col(ColumnDef::new(ReminderTemplate::Tts).boolean().not_null().default(false)) | ||||||
|  |                     .col(ColumnDef::new(ReminderTemplate::Attachment).binary_len(8 * 1024 * 1024)) | ||||||
|  |                     .col(ColumnDef::new(ReminderTemplate::AttachmentName).string_len(260)) | ||||||
|  |                     .col(ColumnDef::new(ReminderTemplate::EmbedTitle).string_len(256)) | ||||||
|  |                     .col(ColumnDef::new(ReminderTemplate::EmbedDescription).string_len(4096)) | ||||||
|  |                     .col(ColumnDef::new(ReminderTemplate::EmbedImageUrl).string_len(500)) | ||||||
|  |                     .col(ColumnDef::new(ReminderTemplate::EmbedThumbnailUrl).string_len(500)) | ||||||
|  |                     .col(ColumnDef::new(ReminderTemplate::EmbedFooter).string_len(2048)) | ||||||
|  |                     .col(ColumnDef::new(ReminderTemplate::EmbedFooterUrl).string_len(500)) | ||||||
|  |                     .col(ColumnDef::new(ReminderTemplate::EmbedAuthor).string_len(256)) | ||||||
|  |                     .col(ColumnDef::new(ReminderTemplate::EmbedAuthorUrl).string_len(500)) | ||||||
|  |                     .col(ColumnDef::new(ReminderTemplate::EmbedColor).integer()) | ||||||
|  |                     .col(ColumnDef::new(ReminderTemplate::EmbedFields).json()) | ||||||
|  |                     .to_owned(), | ||||||
|  |             ) | ||||||
|  |             .await?; | ||||||
|  |  | ||||||
|  |         manager | ||||||
|  |             .create_foreign_key( | ||||||
|  |                 ForeignKey::create() | ||||||
|  |                     .name("fk_reminder_template_guild") | ||||||
|  |                     .from(ReminderTemplate::Table, ReminderTemplate::GuildId) | ||||||
|  |                     .to(Guild::Table, Guild::Id) | ||||||
|  |                     .on_delete(ForeignKeyAction::Cascade) | ||||||
|  |                     .to_owned(), | ||||||
|  |             ) | ||||||
|  |             .await?; | ||||||
|  |  | ||||||
|  |         manager | ||||||
|  |             .create_table( | ||||||
|  |                 Table::create() | ||||||
|  |                     .table(Timer::Table) | ||||||
|  |                     .if_not_exists() | ||||||
|  |                     .col( | ||||||
|  |                         ColumnDef::new(Timer::Id) | ||||||
|  |                             .integer() | ||||||
|  |                             .not_null() | ||||||
|  |                             .auto_increment() | ||||||
|  |                             .primary_key(), | ||||||
|  |                     ) | ||||||
|  |                     .col(ColumnDef::new(Timer::StartTime).date_time().not_null().default("NOW()")) | ||||||
|  |                     .col(ColumnDef::new(Timer::Name).string_len(32).not_null().default("Timer")) | ||||||
|  |                     .col(ColumnDef::new(Timer::UserId).big_integer()) | ||||||
|  |                     .col(ColumnDef::new(Timer::GuildId).big_integer()) | ||||||
|  |                     .to_owned(), | ||||||
|  |             ) | ||||||
|  |             .await?; | ||||||
|  |  | ||||||
|  |         manager | ||||||
|  |             .create_foreign_key( | ||||||
|  |                 ForeignKey::create() | ||||||
|  |                     .name("fk_timer_user") | ||||||
|  |                     .from(Timer::Table, Timer::UserId) | ||||||
|  |                     .to(Guild::Table, Guild::Id) | ||||||
|  |                     .on_delete(ForeignKeyAction::Cascade) | ||||||
|  |                     .to_owned(), | ||||||
|  |             ) | ||||||
|  |             .await?; | ||||||
|  |  | ||||||
|  |         manager | ||||||
|  |             .create_foreign_key( | ||||||
|  |                 ForeignKey::create() | ||||||
|  |                     .name("fk_timer_guild") | ||||||
|  |                     .from(Timer::Table, Timer::GuildId) | ||||||
|  |                     .to(Guild::Table, Guild::Id) | ||||||
|  |                     .on_delete(ForeignKeyAction::Cascade) | ||||||
|  |                     .to_owned(), | ||||||
|  |             ) | ||||||
|  |             .await?; | ||||||
|  |  | ||||||
|  |         manager | ||||||
|  |             .create_table( | ||||||
|  |                 Table::create() | ||||||
|  |                     .table(Todo::Table) | ||||||
|  |                     .if_not_exists() | ||||||
|  |                     .col( | ||||||
|  |                         ColumnDef::new(Todo::Id) | ||||||
|  |                             .integer() | ||||||
|  |                             .not_null() | ||||||
|  |                             .auto_increment() | ||||||
|  |                             .primary_key(), | ||||||
|  |                     ) | ||||||
|  |                     .col(ColumnDef::new(Todo::UserId).big_integer()) | ||||||
|  |                     .col(ColumnDef::new(Todo::GuildId).big_integer()) | ||||||
|  |                     .col(ColumnDef::new(Todo::ChannelId).big_integer()) | ||||||
|  |                     .col(ColumnDef::new(Todo::Value).string_len(2000).not_null()) | ||||||
|  |                     .to_owned(), | ||||||
|  |             ) | ||||||
|  |             .await?; | ||||||
|  |  | ||||||
|  |         manager | ||||||
|  |             .create_foreign_key( | ||||||
|  |                 ForeignKey::create() | ||||||
|  |                     .name("fk_todo_user") | ||||||
|  |                     .from(Todo::Table, Todo::UserId) | ||||||
|  |                     .to(User::Table, User::Id) | ||||||
|  |                     .on_delete(ForeignKeyAction::Cascade) | ||||||
|  |                     .to_owned(), | ||||||
|  |             ) | ||||||
|  |             .await?; | ||||||
|  |  | ||||||
|  |         manager | ||||||
|  |             .create_foreign_key( | ||||||
|  |                 ForeignKey::create() | ||||||
|  |                     .name("fk_todo_guild") | ||||||
|  |                     .from(Todo::Table, Todo::GuildId) | ||||||
|  |                     .to(Guild::Table, Guild::Id) | ||||||
|  |                     .on_delete(ForeignKeyAction::Cascade) | ||||||
|  |                     .to_owned(), | ||||||
|  |             ) | ||||||
|  |             .await?; | ||||||
|  |  | ||||||
|  |         manager | ||||||
|  |             .create_foreign_key( | ||||||
|  |                 ForeignKey::create() | ||||||
|  |                     .name("fk_todo_channel") | ||||||
|  |                     .from(Todo::Table, Todo::ChannelId) | ||||||
|  |                     .to(Channel::Table, Channel::Id) | ||||||
|  |                     .on_delete(ForeignKeyAction::Cascade) | ||||||
|  |                     .to_owned(), | ||||||
|  |             ) | ||||||
|  |             .await?; | ||||||
|  |  | ||||||
|  |         manager | ||||||
|  |             .create_table( | ||||||
|  |                 Table::create() | ||||||
|  |                     .table(CommandMacro::Table) | ||||||
|  |                     .if_not_exists() | ||||||
|  |                     .col( | ||||||
|  |                         ColumnDef::new(CommandMacro::Id) | ||||||
|  |                             .integer() | ||||||
|  |                             .not_null() | ||||||
|  |                             .auto_increment() | ||||||
|  |                             .primary_key(), | ||||||
|  |                     ) | ||||||
|  |                     .col(ColumnDef::new(CommandMacro::GuildId).big_integer().not_null()) | ||||||
|  |                     .col(ColumnDef::new(CommandMacro::Name).string_len(100).not_null()) | ||||||
|  |                     .col(ColumnDef::new(CommandMacro::Description).string_len(100)) | ||||||
|  |                     .col(ColumnDef::new(CommandMacro::Commands).json()) | ||||||
|  |                     .to_owned(), | ||||||
|  |             ) | ||||||
|  |             .await?; | ||||||
|  |  | ||||||
|  |         manager | ||||||
|  |             .create_foreign_key( | ||||||
|  |                 ForeignKey::create() | ||||||
|  |                     .name("fk_command_macro_guild") | ||||||
|  |                     .from(CommandMacro::Table, CommandMacro::GuildId) | ||||||
|  |                     .to(Guild::Table, Guild::Id) | ||||||
|  |                     .on_delete(ForeignKeyAction::Cascade) | ||||||
|  |                     .to_owned(), | ||||||
|  |             ) | ||||||
|  |             .await?; | ||||||
|  |  | ||||||
|  |         Ok(()) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { | ||||||
|  |         manager | ||||||
|  |             .drop_foreign_key( | ||||||
|  |                 ForeignKey::drop().table(Channel::Table).name("fk_channel_guild").to_owned(), | ||||||
|  |             ) | ||||||
|  |             .await?; | ||||||
|  |         manager | ||||||
|  |             .drop_foreign_key( | ||||||
|  |                 ForeignKey::drop().table(User::Table).name("fk_user_channel").to_owned(), | ||||||
|  |             ) | ||||||
|  |             .await?; | ||||||
|  |         manager | ||||||
|  |             .drop_foreign_key( | ||||||
|  |                 ForeignKey::drop().table(Reminder::Table).name("fk_reminder_channel").to_owned(), | ||||||
|  |             ) | ||||||
|  |             .await?; | ||||||
|  |         manager | ||||||
|  |             .drop_foreign_key( | ||||||
|  |                 ForeignKey::drop().table(Reminder::Table).name("fk_reminder_user").to_owned(), | ||||||
|  |             ) | ||||||
|  |             .await?; | ||||||
|  |         manager | ||||||
|  |             .drop_foreign_key( | ||||||
|  |                 ForeignKey::drop() | ||||||
|  |                     .table(ReminderTemplate::Table) | ||||||
|  |                     .name("fk_reminder_template_guild") | ||||||
|  |                     .to_owned(), | ||||||
|  |             ) | ||||||
|  |             .await?; | ||||||
|  |         manager | ||||||
|  |             .drop_foreign_key( | ||||||
|  |                 ForeignKey::drop().table(Timer::Table).name("fk_timer_user").to_owned(), | ||||||
|  |             ) | ||||||
|  |             .await?; | ||||||
|  |         manager | ||||||
|  |             .drop_foreign_key( | ||||||
|  |                 ForeignKey::drop().table(Timer::Table).name("fk_timer_guild").to_owned(), | ||||||
|  |             ) | ||||||
|  |             .await?; | ||||||
|  |         manager | ||||||
|  |             .drop_foreign_key(ForeignKey::drop().table(Todo::Table).name("fk_todo_user").to_owned()) | ||||||
|  |             .await?; | ||||||
|  |         manager | ||||||
|  |             .drop_foreign_key( | ||||||
|  |                 ForeignKey::drop().table(Todo::Table).name("fk_todo_guild").to_owned(), | ||||||
|  |             ) | ||||||
|  |             .await?; | ||||||
|  |         manager | ||||||
|  |             .drop_foreign_key( | ||||||
|  |                 ForeignKey::drop().table(Todo::Table).name("fk_todo_channel").to_owned(), | ||||||
|  |             ) | ||||||
|  |             .await?; | ||||||
|  |         manager | ||||||
|  |             .drop_foreign_key( | ||||||
|  |                 ForeignKey::drop() | ||||||
|  |                     .table(CommandMacro::Table) | ||||||
|  |                     .name("fk_command_macro_guild") | ||||||
|  |                     .to_owned(), | ||||||
|  |             ) | ||||||
|  |             .await?; | ||||||
|  |  | ||||||
|  |         manager.drop_table(Table::drop().table(Guild::Table).to_owned()).await?; | ||||||
|  |         manager.drop_table(Table::drop().table(Channel::Table).to_owned()).await?; | ||||||
|  |         manager.drop_table(Table::drop().table(User::Table).to_owned()).await?; | ||||||
|  |         manager.drop_table(Table::drop().table(Reminder::Table).to_owned()).await?; | ||||||
|  |         manager.drop_table(Table::drop().table(ReminderTemplate::Table).to_owned()).await?; | ||||||
|  |         manager.drop_table(Table::drop().table(Timer::Table).to_owned()).await?; | ||||||
|  |         manager.drop_table(Table::drop().table(Todo::Table).to_owned()).await?; | ||||||
|  |         manager.drop_table(Table::drop().table(CommandMacro::Table).to_owned()).await?; | ||||||
|  |  | ||||||
|  |         manager.drop_type(Type::drop().name(Timezone::Type).to_owned()).await?; | ||||||
|  |  | ||||||
|  |         Ok(()) | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										6
									
								
								models/migration/src/main.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,6 @@ | |||||||
|  | use sea_orm_migration::prelude::*; | ||||||
|  |  | ||||||
|  | #[async_std::main] | ||||||
|  | async fn main() { | ||||||
|  |     cli::run_cli(migration::Migrator).await; | ||||||
|  | } | ||||||
							
								
								
									
										1
									
								
								models/src/lib.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1 @@ | |||||||
|  |  | ||||||
| @@ -1,53 +0,0 @@ | |||||||
| 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; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
							
								
								
									
										18
									
								
								postman/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,18 @@ | |||||||
|  | [package] | ||||||
|  | name = "postman" | ||||||
|  | version = "0.1.0" | ||||||
|  | edition = "2021" | ||||||
|  |  | ||||||
|  | [dependencies] | ||||||
|  | tokio = { version = "1", features = ["process", "full"] } | ||||||
|  | regex = "1.4" | ||||||
|  | log = "0.4" | ||||||
|  | env_logger = "0.8" | ||||||
|  | chrono = "0.4" | ||||||
|  | chrono-tz = { version = "0.5", features = ["serde"] } | ||||||
|  | lazy_static = "1.4" | ||||||
|  | num-integer = "0.1" | ||||||
|  | serde = "1.0" | ||||||
|  | serde_json = "1.0" | ||||||
|  | sqlx = { version = "0.5", features = ["runtime-tokio-rustls", "macros", "mysql", "bigdecimal", "chrono", "json"]} | ||||||
|  | serenity = { version = "0.11.1", default-features = false, features = ["builder", "cache", "client", "gateway", "http", "model", "utils", "rustls_backend"] } | ||||||
| @@ -3,8 +3,7 @@ mod sender; | |||||||
| use std::env; | use std::env; | ||||||
| 
 | 
 | ||||||
| use log::{info, warn}; | use log::{info, warn}; | ||||||
| use poise::serenity_prelude::client::Context; | use serenity::client::Context; | ||||||
| use sd_notify::{self, NotifyState}; |  | ||||||
| use sqlx::{Executor, MySql}; | use sqlx::{Executor, MySql}; | ||||||
| use tokio::{ | use tokio::{ | ||||||
|     sync::broadcast::Receiver, |     sync::broadcast::Receiver, | ||||||
| @@ -34,15 +33,6 @@ async fn _initialize(ctx: Context, pool: impl Executor<'_, Database = Database> | |||||||
|         .flatten() |         .flatten() | ||||||
|         .unwrap_or(10); |         .unwrap_or(10); | ||||||
| 
 | 
 | ||||||
|     let mut watchdog_interval = 0; |  | ||||||
|     let watchdog = sd_notify::watchdog_enabled(false, &mut watchdog_interval); |  | ||||||
| 
 |  | ||||||
|     if watchdog { |  | ||||||
|         warn!("Watchdog enabled. Don't die!"); |  | ||||||
|     } else { |  | ||||||
|         warn!("No watchdog running") |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     loop { |     loop { | ||||||
|         let sleep_to = Instant::now() + Duration::from_secs(remind_interval); |         let sleep_to = Instant::now() + Duration::from_secs(remind_interval); | ||||||
|         let reminders = sender::Reminder::fetch_reminders(pool).await; |         let reminders = sender::Reminder::fetch_reminders(pool).await; | ||||||
| @@ -52,11 +42,9 @@ async fn _initialize(ctx: Context, pool: impl Executor<'_, Database = Database> | |||||||
| 
 | 
 | ||||||
|             for reminder in reminders { |             for reminder in reminders { | ||||||
|                 reminder.send(pool, ctx.clone()).await; |                 reminder.send(pool, ctx.clone()).await; | ||||||
|                 let _ = sd_notify::notify(false, &[NotifyState::Watchdog]); |  | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         sleep_until(sleep_to).await; |         sleep_until(sleep_to).await; | ||||||
|         let _ = sd_notify::notify(false, &[NotifyState::Watchdog]); |  | ||||||
|     } |     } | ||||||
| } | } | ||||||
							
								
								
									
										595
									
								
								postman/src/sender.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,595 @@ | |||||||
|  | use chrono::Duration; | ||||||
|  | use chrono_tz::Tz; | ||||||
|  | use lazy_static::lazy_static; | ||||||
|  | use log::{error, info, warn}; | ||||||
|  | use num_integer::Integer; | ||||||
|  | use regex::{Captures, Regex}; | ||||||
|  | use serde::Deserialize; | ||||||
|  | use serenity::{ | ||||||
|  |     builder::CreateEmbed, | ||||||
|  |     http::{CacheHttp, Http, HttpError, StatusCode}, | ||||||
|  |     model::{ | ||||||
|  |         channel::{Channel, Embed as SerenityEmbed}, | ||||||
|  |         id::ChannelId, | ||||||
|  |         webhook::Webhook, | ||||||
|  |     }, | ||||||
|  |     Error, Result, | ||||||
|  | }; | ||||||
|  | use sqlx::{ | ||||||
|  |     types::{ | ||||||
|  |         chrono::{NaiveDateTime, Utc}, | ||||||
|  |         Json, | ||||||
|  |     }, | ||||||
|  |     Executor, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | use crate::Database; | ||||||
|  |  | ||||||
|  | lazy_static! { | ||||||
|  |     pub static ref TIMEFROM_REGEX: Regex = | ||||||
|  |         Regex::new(r#"<<timefrom:(?P<time>\d+):(?P<format>.+)?>>"#).unwrap(); | ||||||
|  |     pub static ref TIMENOW_REGEX: Regex = | ||||||
|  |         Regex::new(r#"<<timenow:(?P<timezone>(?:\w|/|_)+):(?P<format>.+)?>>"#).unwrap(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fn fmt_displacement(format: &str, seconds: u64) -> String { | ||||||
|  |     let mut seconds = seconds; | ||||||
|  |     let mut days: u64 = 0; | ||||||
|  |     let mut hours: u64 = 0; | ||||||
|  |     let mut minutes: u64 = 0; | ||||||
|  |  | ||||||
|  |     for (rep, time_type, div) in | ||||||
|  |         [("%d", &mut days, 86400), ("%h", &mut hours, 3600), ("%m", &mut minutes, 60)].iter_mut() | ||||||
|  |     { | ||||||
|  |         if format.contains(*rep) { | ||||||
|  |             let (divided, new_seconds) = seconds.div_rem(&div); | ||||||
|  |  | ||||||
|  |             **time_type = divided; | ||||||
|  |             seconds = new_seconds; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     format | ||||||
|  |         .replace("%s", &seconds.to_string()) | ||||||
|  |         .replace("%m", &minutes.to_string()) | ||||||
|  |         .replace("%h", &hours.to_string()) | ||||||
|  |         .replace("%d", &days.to_string()) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | pub fn substitute(string: &str) -> String { | ||||||
|  |     let new = TIMEFROM_REGEX.replace(string, |caps: &Captures| { | ||||||
|  |         let final_time = caps.name("time").map(|m| m.as_str().parse::<i64>().ok()).flatten(); | ||||||
|  |         let format = caps.name("format").map(|m| m.as_str()); | ||||||
|  |  | ||||||
|  |         if let (Some(final_time), Some(format)) = (final_time, format) { | ||||||
|  |             let dt = NaiveDateTime::from_timestamp(final_time, 0); | ||||||
|  |             let now = Utc::now().naive_utc(); | ||||||
|  |  | ||||||
|  |             let difference = { | ||||||
|  |                 if now < dt { | ||||||
|  |                     dt - Utc::now().naive_utc() | ||||||
|  |                 } else { | ||||||
|  |                     Utc::now().naive_utc() - dt | ||||||
|  |                 } | ||||||
|  |             }; | ||||||
|  |  | ||||||
|  |             fmt_displacement(format, difference.num_seconds() as u64) | ||||||
|  |         } else { | ||||||
|  |             String::new() | ||||||
|  |         } | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     TIMENOW_REGEX | ||||||
|  |         .replace(&new, |caps: &Captures| { | ||||||
|  |             let timezone = caps.name("timezone").map(|m| m.as_str().parse::<Tz>().ok()).flatten(); | ||||||
|  |             let format = caps.name("format").map(|m| m.as_str()); | ||||||
|  |  | ||||||
|  |             if let (Some(timezone), Some(format)) = (timezone, format) { | ||||||
|  |                 let now = Utc::now().with_timezone(&timezone); | ||||||
|  |  | ||||||
|  |                 now.format(format).to_string() | ||||||
|  |             } else { | ||||||
|  |                 String::new() | ||||||
|  |             } | ||||||
|  |         }) | ||||||
|  |         .to_string() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | struct Embed { | ||||||
|  |     title: String, | ||||||
|  |     description: String, | ||||||
|  |     image_url: Option<String>, | ||||||
|  |     thumbnail_url: Option<String>, | ||||||
|  |     footer: String, | ||||||
|  |     footer_url: Option<String>, | ||||||
|  |     author: String, | ||||||
|  |     author_url: Option<String>, | ||||||
|  |     color: u32, | ||||||
|  |     fields: Json<Vec<EmbedField>>, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[derive(Deserialize)] | ||||||
|  | struct EmbedField { | ||||||
|  |     title: String, | ||||||
|  |     value: String, | ||||||
|  |     inline: bool, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl Embed { | ||||||
|  |     pub async fn from_id( | ||||||
|  |         pool: impl Executor<'_, Database = Database> + Copy, | ||||||
|  |         id: u32, | ||||||
|  |     ) -> Option<Self> { | ||||||
|  |         match sqlx::query_as!( | ||||||
|  |             Self, | ||||||
|  |             r#" | ||||||
|  |             SELECT | ||||||
|  |              `embed_title` AS title, | ||||||
|  |              `embed_description` AS description, | ||||||
|  |              `embed_image_url` AS image_url, | ||||||
|  |              `embed_thumbnail_url` AS thumbnail_url, | ||||||
|  |              `embed_footer` AS footer, | ||||||
|  |              `embed_footer_url` AS footer_url, | ||||||
|  |              `embed_author` AS author, | ||||||
|  |              `embed_author_url` AS author_url, | ||||||
|  |              `embed_color` AS color, | ||||||
|  |              IFNULL(`embed_fields`, '[]') AS "fields:_" | ||||||
|  |             FROM reminders | ||||||
|  |             WHERE `id` = ?"#, | ||||||
|  |             id | ||||||
|  |         ) | ||||||
|  |         .fetch_one(pool) | ||||||
|  |         .await | ||||||
|  |         { | ||||||
|  |             Ok(mut embed) => { | ||||||
|  |                 embed.title = substitute(&embed.title); | ||||||
|  |                 embed.description = substitute(&embed.description); | ||||||
|  |                 embed.footer = substitute(&embed.footer); | ||||||
|  |  | ||||||
|  |                 embed.fields.iter_mut().for_each(|mut field| { | ||||||
|  |                     field.title = substitute(&field.title); | ||||||
|  |                     field.value = substitute(&field.value); | ||||||
|  |                 }); | ||||||
|  |  | ||||||
|  |                 if embed.has_content() { | ||||||
|  |                     Some(embed) | ||||||
|  |                 } else { | ||||||
|  |                     None | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             Err(e) => { | ||||||
|  |                 warn!("Error loading embed from reminder: {:?}", e); | ||||||
|  |  | ||||||
|  |                 None | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     pub fn has_content(&self) -> bool { | ||||||
|  |         if self.title.is_empty() | ||||||
|  |             && self.description.is_empty() | ||||||
|  |             && self.image_url.is_none() | ||||||
|  |             && self.thumbnail_url.is_none() | ||||||
|  |             && self.footer.is_empty() | ||||||
|  |             && self.footer_url.is_none() | ||||||
|  |             && self.author.is_empty() | ||||||
|  |             && self.author_url.is_none() | ||||||
|  |             && self.fields.0.is_empty() | ||||||
|  |         { | ||||||
|  |             false | ||||||
|  |         } else { | ||||||
|  |             true | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl Into<CreateEmbed> for Embed { | ||||||
|  |     fn into(self) -> CreateEmbed { | ||||||
|  |         let mut c = CreateEmbed::default(); | ||||||
|  |  | ||||||
|  |         c.title(&self.title) | ||||||
|  |             .description(&self.description) | ||||||
|  |             .color(self.color) | ||||||
|  |             .author(|a| { | ||||||
|  |                 a.name(&self.author); | ||||||
|  |  | ||||||
|  |                 if let Some(author_icon) = &self.author_url { | ||||||
|  |                     a.icon_url(author_icon); | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 a | ||||||
|  |             }) | ||||||
|  |             .footer(|f| { | ||||||
|  |                 f.text(&self.footer); | ||||||
|  |  | ||||||
|  |                 if let Some(footer_icon) = &self.footer_url { | ||||||
|  |                     f.icon_url(footer_icon); | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 f | ||||||
|  |             }); | ||||||
|  |  | ||||||
|  |         for field in &self.fields.0 { | ||||||
|  |             c.field(&field.title, &field.value, field.inline); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if let Some(image_url) = &self.image_url { | ||||||
|  |             c.image(image_url); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if let Some(thumbnail_url) = &self.thumbnail_url { | ||||||
|  |             c.thumbnail(thumbnail_url); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         c | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[derive(Debug)] | ||||||
|  | pub struct Reminder { | ||||||
|  |     id: u32, | ||||||
|  |  | ||||||
|  |     channel_id: u64, | ||||||
|  |     webhook_id: Option<u64>, | ||||||
|  |     webhook_token: Option<String>, | ||||||
|  |  | ||||||
|  |     channel_paused: bool, | ||||||
|  |     channel_paused_until: Option<NaiveDateTime>, | ||||||
|  |     enabled: bool, | ||||||
|  |  | ||||||
|  |     tts: bool, | ||||||
|  |     pin: bool, | ||||||
|  |     content: String, | ||||||
|  |     attachment: Option<Vec<u8>>, | ||||||
|  |     attachment_name: Option<String>, | ||||||
|  |  | ||||||
|  |     utc_time: NaiveDateTime, | ||||||
|  |     timezone: String, | ||||||
|  |     restartable: bool, | ||||||
|  |     expires: Option<NaiveDateTime>, | ||||||
|  |     interval_seconds: Option<u32>, | ||||||
|  |     interval_months: Option<u32>, | ||||||
|  |  | ||||||
|  |     avatar: Option<String>, | ||||||
|  |     username: Option<String>, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl Reminder { | ||||||
|  |     pub async fn fetch_reminders(pool: impl Executor<'_, Database = Database> + Copy) -> Vec<Self> { | ||||||
|  |         match sqlx::query_as_unchecked!( | ||||||
|  |             Reminder, | ||||||
|  |             r#" | ||||||
|  | SELECT | ||||||
|  |     reminders.`id` AS id, | ||||||
|  |  | ||||||
|  |     channels.`channel` AS channel_id, | ||||||
|  |     channels.`webhook_id` AS webhook_id, | ||||||
|  |     channels.`webhook_token` AS webhook_token, | ||||||
|  |  | ||||||
|  |     channels.`paused` AS 'channel_paused', | ||||||
|  |     channels.`paused_until` AS 'channel_paused_until', | ||||||
|  |     reminders.`enabled` AS 'enabled', | ||||||
|  |  | ||||||
|  |     reminders.`tts` AS tts, | ||||||
|  |     reminders.`pin` AS pin, | ||||||
|  |     reminders.`content` AS content, | ||||||
|  |     reminders.`attachment` AS attachment, | ||||||
|  |     reminders.`attachment_name` AS attachment_name, | ||||||
|  |  | ||||||
|  |     reminders.`utc_time` AS 'utc_time', | ||||||
|  |     reminders.`timezone` AS timezone, | ||||||
|  |     reminders.`restartable` AS restartable, | ||||||
|  |     reminders.`expires` AS 'expires', | ||||||
|  |     reminders.`interval_seconds` AS 'interval_seconds', | ||||||
|  |     reminders.`interval_months` AS 'interval_months', | ||||||
|  |  | ||||||
|  |     reminders.`avatar` AS avatar, | ||||||
|  |     reminders.`username` AS username | ||||||
|  | FROM | ||||||
|  |     reminders | ||||||
|  | INNER JOIN | ||||||
|  |     channels | ||||||
|  | ON | ||||||
|  |     reminders.channel_id = channels.id | ||||||
|  | WHERE | ||||||
|  |     reminders.`utc_time` < NOW() | ||||||
|  | LIMIT 25 | ||||||
|  |             "#, | ||||||
|  |         ) | ||||||
|  |         .fetch_all(pool) | ||||||
|  |         .await | ||||||
|  |         { | ||||||
|  |             Ok(reminders) => reminders | ||||||
|  |                 .into_iter() | ||||||
|  |                 .map(|mut rem| { | ||||||
|  |                     rem.content = substitute(&rem.content); | ||||||
|  |  | ||||||
|  |                     rem | ||||||
|  |                 }) | ||||||
|  |                 .collect::<Vec<Self>>(), | ||||||
|  |  | ||||||
|  |             Err(e) => { | ||||||
|  |                 warn!("Could not fetch reminders: {:?}", e); | ||||||
|  |  | ||||||
|  |                 vec![] | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     async fn reset_webhook(&self, pool: impl Executor<'_, Database = Database> + Copy) { | ||||||
|  |         let _ = sqlx::query!( | ||||||
|  |             " | ||||||
|  | UPDATE channels SET webhook_id = NULL, webhook_token = NULL WHERE channel = ? | ||||||
|  |             ", | ||||||
|  |             self.channel_id | ||||||
|  |         ) | ||||||
|  |         .execute(pool) | ||||||
|  |         .await; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     async fn refresh(&self, pool: impl Executor<'_, Database = Database> + Copy) { | ||||||
|  |         if self.interval_seconds.is_some() || self.interval_months.is_some() { | ||||||
|  |             let now = Utc::now().naive_local(); | ||||||
|  |             let mut updated_reminder_time = self.utc_time; | ||||||
|  |  | ||||||
|  |             if let Some(interval) = self.interval_months { | ||||||
|  |                 match sqlx::query!( | ||||||
|  |                     // use the second date_add to force return value to datetime | ||||||
|  |                     "SELECT DATE_ADD(DATE_ADD(?, INTERVAL ? MONTH), INTERVAL 0 SECOND) AS new_time", | ||||||
|  |                     updated_reminder_time, | ||||||
|  |                     interval | ||||||
|  |                 ) | ||||||
|  |                 .fetch_one(pool) | ||||||
|  |                 .await | ||||||
|  |                 { | ||||||
|  |                     Ok(row) => match row.new_time { | ||||||
|  |                         Some(datetime) => { | ||||||
|  |                             updated_reminder_time = datetime; | ||||||
|  |                         } | ||||||
|  |                         None => { | ||||||
|  |                             warn!("Could not update interval by months: got NULL"); | ||||||
|  |  | ||||||
|  |                             updated_reminder_time += Duration::days(30); | ||||||
|  |                         } | ||||||
|  |                     }, | ||||||
|  |  | ||||||
|  |                     Err(e) => { | ||||||
|  |                         warn!("Could not update interval by months: {:?}", e); | ||||||
|  |  | ||||||
|  |                         // naively fallback to adding 30 days | ||||||
|  |                         updated_reminder_time += Duration::days(30); | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             if let Some(interval) = self.interval_seconds { | ||||||
|  |                 while updated_reminder_time < now { | ||||||
|  |                     updated_reminder_time += Duration::seconds(interval as i64); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             if self.expires.map_or(false, |expires| { | ||||||
|  |                 NaiveDateTime::from_timestamp(updated_reminder_time.timestamp(), 0) > expires | ||||||
|  |             }) { | ||||||
|  |                 self.force_delete(pool).await; | ||||||
|  |             } else { | ||||||
|  |                 sqlx::query!( | ||||||
|  |                     " | ||||||
|  | UPDATE reminders SET `utc_time` = ? WHERE `id` = ? | ||||||
|  |                     ", | ||||||
|  |                     updated_reminder_time, | ||||||
|  |                     self.id | ||||||
|  |                 ) | ||||||
|  |                 .execute(pool) | ||||||
|  |                 .await | ||||||
|  |                 .expect(&format!("Could not update time on Reminder {}", self.id)); | ||||||
|  |             } | ||||||
|  |         } else { | ||||||
|  |             self.force_delete(pool).await; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     async fn force_delete(&self, pool: impl Executor<'_, Database = Database> + Copy) { | ||||||
|  |         sqlx::query!( | ||||||
|  |             " | ||||||
|  | DELETE FROM reminders WHERE `id` = ? | ||||||
|  |             ", | ||||||
|  |             self.id | ||||||
|  |         ) | ||||||
|  |         .execute(pool) | ||||||
|  |         .await | ||||||
|  |         .expect(&format!("Could not delete Reminder {}", self.id)); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     async fn pin_message<M: Into<u64>>(&self, message_id: M, http: impl AsRef<Http>) { | ||||||
|  |         let _ = http.as_ref().pin_message(self.channel_id, message_id.into(), None).await; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     pub async fn send( | ||||||
|  |         &self, | ||||||
|  |         pool: impl Executor<'_, Database = Database> + Copy, | ||||||
|  |         cache_http: impl CacheHttp, | ||||||
|  |     ) { | ||||||
|  |         async fn send_to_channel( | ||||||
|  |             cache_http: impl CacheHttp, | ||||||
|  |             reminder: &Reminder, | ||||||
|  |             embed: Option<CreateEmbed>, | ||||||
|  |         ) -> Result<()> { | ||||||
|  |             let channel = ChannelId(reminder.channel_id).to_channel(&cache_http).await; | ||||||
|  |  | ||||||
|  |             match channel { | ||||||
|  |                 Ok(Channel::Guild(channel)) => { | ||||||
|  |                     match channel | ||||||
|  |                         .send_message(&cache_http, |m| { | ||||||
|  |                             m.content(&reminder.content).tts(reminder.tts); | ||||||
|  |  | ||||||
|  |                             if let (Some(attachment), Some(name)) = | ||||||
|  |                                 (&reminder.attachment, &reminder.attachment_name) | ||||||
|  |                             { | ||||||
|  |                                 m.add_file((attachment as &[u8], name.as_str())); | ||||||
|  |                             } | ||||||
|  |  | ||||||
|  |                             if let Some(embed) = embed { | ||||||
|  |                                 m.set_embed(embed); | ||||||
|  |                             } | ||||||
|  |  | ||||||
|  |                             m | ||||||
|  |                         }) | ||||||
|  |                         .await | ||||||
|  |                     { | ||||||
|  |                         Ok(m) => { | ||||||
|  |                             if reminder.pin { | ||||||
|  |                                 reminder.pin_message(m.id, cache_http.http()).await; | ||||||
|  |                             } | ||||||
|  |  | ||||||
|  |                             Ok(()) | ||||||
|  |                         } | ||||||
|  |                         Err(e) => Err(e), | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |                 Ok(Channel::Private(channel)) => { | ||||||
|  |                     match channel | ||||||
|  |                         .send_message(&cache_http.http(), |m| { | ||||||
|  |                             m.content(&reminder.content).tts(reminder.tts); | ||||||
|  |  | ||||||
|  |                             if let (Some(attachment), Some(name)) = | ||||||
|  |                                 (&reminder.attachment, &reminder.attachment_name) | ||||||
|  |                             { | ||||||
|  |                                 m.add_file((attachment as &[u8], name.as_str())); | ||||||
|  |                             } | ||||||
|  |  | ||||||
|  |                             if let Some(embed) = embed { | ||||||
|  |                                 m.set_embed(embed); | ||||||
|  |                             } | ||||||
|  |  | ||||||
|  |                             m | ||||||
|  |                         }) | ||||||
|  |                         .await | ||||||
|  |                     { | ||||||
|  |                         Ok(m) => { | ||||||
|  |                             if reminder.pin { | ||||||
|  |                                 reminder.pin_message(m.id, cache_http.http()).await; | ||||||
|  |                             } | ||||||
|  |  | ||||||
|  |                             Ok(()) | ||||||
|  |                         } | ||||||
|  |                         Err(e) => Err(e), | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |                 Err(e) => Err(e), | ||||||
|  |                 _ => Err(Error::Other("Channel not of valid type")), | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         async fn send_to_webhook( | ||||||
|  |             cache_http: impl CacheHttp, | ||||||
|  |             reminder: &Reminder, | ||||||
|  |             webhook: Webhook, | ||||||
|  |             embed: Option<CreateEmbed>, | ||||||
|  |         ) -> Result<()> { | ||||||
|  |             match webhook | ||||||
|  |                 .execute(&cache_http.http(), reminder.pin || reminder.restartable, |w| { | ||||||
|  |                     w.content(&reminder.content).tts(reminder.tts); | ||||||
|  |  | ||||||
|  |                     if let Some(username) = &reminder.username { | ||||||
|  |                         w.username(username); | ||||||
|  |                     } | ||||||
|  |  | ||||||
|  |                     if let Some(avatar) = &reminder.avatar { | ||||||
|  |                         w.avatar_url(avatar); | ||||||
|  |                     } | ||||||
|  |  | ||||||
|  |                     if let (Some(attachment), Some(name)) = | ||||||
|  |                         (&reminder.attachment, &reminder.attachment_name) | ||||||
|  |                     { | ||||||
|  |                         w.add_file((attachment as &[u8], name.as_str())); | ||||||
|  |                     } | ||||||
|  |  | ||||||
|  |                     if let Some(embed) = embed { | ||||||
|  |                         w.embeds(vec![SerenityEmbed::fake(|c| { | ||||||
|  |                             *c = embed; | ||||||
|  |                             c | ||||||
|  |                         })]); | ||||||
|  |                     } | ||||||
|  |  | ||||||
|  |                     w | ||||||
|  |                 }) | ||||||
|  |                 .await | ||||||
|  |             { | ||||||
|  |                 Ok(m) => { | ||||||
|  |                     if reminder.pin { | ||||||
|  |                         if let Some(message) = m { | ||||||
|  |                             reminder.pin_message(message.id, cache_http.http()).await; | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |  | ||||||
|  |                     Ok(()) | ||||||
|  |                 } | ||||||
|  |                 Err(e) => Err(e), | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if self.enabled | ||||||
|  |             && !(self.channel_paused | ||||||
|  |                 && self | ||||||
|  |                     .channel_paused_until | ||||||
|  |                     .map_or(true, |inner| inner >= Utc::now().naive_local())) | ||||||
|  |         { | ||||||
|  |             let _ = sqlx::query!( | ||||||
|  |                 " | ||||||
|  | UPDATE `channels` SET paused = 0, paused_until = NULL WHERE `channel` = ? | ||||||
|  |                 ", | ||||||
|  |                 self.channel_id | ||||||
|  |             ) | ||||||
|  |             .execute(pool) | ||||||
|  |             .await; | ||||||
|  |  | ||||||
|  |             let embed = Embed::from_id(pool, self.id).await.map(|e| e.into()); | ||||||
|  |  | ||||||
|  |             let result = if let (Some(webhook_id), Some(webhook_token)) = | ||||||
|  |                 (self.webhook_id, &self.webhook_token) | ||||||
|  |             { | ||||||
|  |                 let webhook_res = | ||||||
|  |                     cache_http.http().get_webhook_with_token(webhook_id, webhook_token).await; | ||||||
|  |  | ||||||
|  |                 if let Ok(webhook) = webhook_res { | ||||||
|  |                     send_to_webhook(cache_http, &self, webhook, embed).await | ||||||
|  |                 } else { | ||||||
|  |                     warn!("Webhook vanished: {:?}", webhook_res); | ||||||
|  |  | ||||||
|  |                     self.reset_webhook(pool).await; | ||||||
|  |                     send_to_channel(cache_http, &self, embed).await | ||||||
|  |                 } | ||||||
|  |             } else { | ||||||
|  |                 send_to_channel(cache_http, &self, embed).await | ||||||
|  |             }; | ||||||
|  |  | ||||||
|  |             if let Err(e) = result { | ||||||
|  |                 error!("Error sending {:?}: {:?}", self, e); | ||||||
|  |  | ||||||
|  |                 if let Error::Http(error) = e { | ||||||
|  |                     if error.status_code() == Some(StatusCode::NOT_FOUND) { | ||||||
|  |                         warn!("Seeing channel is deleted. Removing reminder"); | ||||||
|  |                         self.force_delete(pool).await; | ||||||
|  |                     } else if let HttpError::UnsuccessfulRequest(error) = *error { | ||||||
|  |                         if error.error.code == 50007 { | ||||||
|  |                             warn!("User cannot receive DMs"); | ||||||
|  |                             self.force_delete(pool).await; | ||||||
|  |                         } else { | ||||||
|  |                             self.refresh(pool).await; | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 } else { | ||||||
|  |                     self.refresh(pool).await; | ||||||
|  |                 } | ||||||
|  |             } else { | ||||||
|  |                 self.refresh(pool).await; | ||||||
|  |             } | ||||||
|  |         } else { | ||||||
|  |             info!("Reminder {} is paused", self.id); | ||||||
|  |  | ||||||
|  |             self.refresh(pool).await; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -1,11 +0,0 @@ | |||||||
| [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"] } |  | ||||||
| @@ -1,42 +0,0 @@ | |||||||
| 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"); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,2 +0,0 @@ | |||||||
| printWidth = 100 |  | ||||||
| tabWidth = 4 |  | ||||||
| @@ -1,19 +0,0 @@ | |||||||
| # 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` |  | ||||||
| @@ -1,34 +0,0 @@ | |||||||
| <!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
									
									
									
								
							
							
						
						| @@ -1,32 +0,0 @@ | |||||||
| { |  | ||||||
| 	"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: 81 KiB | 
| Before Width: | Height: | Size: 44 KiB | 
| Before Width: | Height: | Size: 40 KiB | 
| Before Width: | Height: | Size: 44 KiB | 
| Before Width: | Height: | Size: 18 KiB | 
| Before Width: | Height: | Size: 20 KiB | 
| @@ -1,131 +0,0 @@ | |||||||
| document.addEventListener("DOMContentLoaded", () => { |  | ||||||
|     fetch("/admin/data") |  | ||||||
|         .then((resp) => resp.json()) |  | ||||||
|         .then((data) => { |  | ||||||
|             document.querySelector("#backlog").textContent = data.backlog; |  | ||||||
|             document.querySelector("#reminders").textContent = data.count.reminders; |  | ||||||
|             document.querySelector("#intervals").textContent = data.count.intervals; |  | ||||||
|  |  | ||||||
|             let historySent = data.historyLong.sent.reduce( |  | ||||||
|                 (iv, frame) => iv + frame.count, |  | ||||||
|                 0 |  | ||||||
|             ); |  | ||||||
|             let historyFailed = data.historyLong.failed.reduce( |  | ||||||
|                 (iv, frame) => iv + frame.count, |  | ||||||
|                 0 |  | ||||||
|             ); |  | ||||||
|             let rate = historyFailed / (historySent + historyFailed); |  | ||||||
|             let formatted = Math.round(rate * 10000) / 100; |  | ||||||
|  |  | ||||||
|             document.querySelector("#historySent").textContent = historySent; |  | ||||||
|             document.querySelector("#historyFailed").textContent = historyFailed; |  | ||||||
|             document.querySelector("#failRate").textContent = `${formatted}%`; |  | ||||||
|  |  | ||||||
|             new Chart(document.getElementById("schedule"), { |  | ||||||
|                 type: "bar", |  | ||||||
|                 data: { |  | ||||||
|                     labels: [ |  | ||||||
|                         ...data.scheduleShort.once, |  | ||||||
|                         ...data.scheduleShort.interval, |  | ||||||
|                     ].map((row) => luxon.DateTime.fromISO(row.time_key)), |  | ||||||
|                     datasets: [ |  | ||||||
|                         { |  | ||||||
|                             label: "Reminders", |  | ||||||
|                             data: data.scheduleShort.once.map((row) => row.count), |  | ||||||
|                         }, |  | ||||||
|                         { |  | ||||||
|                             label: "Intervals", |  | ||||||
|                             data: data.scheduleShort.interval.map((row) => row.count), |  | ||||||
|                         }, |  | ||||||
|                     ], |  | ||||||
|                 }, |  | ||||||
|                 options: { |  | ||||||
|                     responsive: true, |  | ||||||
|                     maintainAspectRatio: false, |  | ||||||
|                     scales: { |  | ||||||
|                         x: { |  | ||||||
|                             stacked: true, |  | ||||||
|                             type: "time", |  | ||||||
|                             time: { |  | ||||||
|                                 unit: "minute", |  | ||||||
|                             }, |  | ||||||
|                         }, |  | ||||||
|                         y: { |  | ||||||
|                             stacked: true, |  | ||||||
|                         }, |  | ||||||
|                     }, |  | ||||||
|                 }, |  | ||||||
|             }); |  | ||||||
|  |  | ||||||
|             new Chart(document.getElementById("scheduleLong"), { |  | ||||||
|                 type: "bar", |  | ||||||
|                 data: { |  | ||||||
|                     labels: [ |  | ||||||
|                         ...data.scheduleLong.once, |  | ||||||
|                         ...data.scheduleLong.interval, |  | ||||||
|                     ].map((row) => luxon.DateTime.fromISO(row.time_key)), |  | ||||||
|                     datasets: [ |  | ||||||
|                         { |  | ||||||
|                             label: "Reminders", |  | ||||||
|                             data: data.scheduleLong.once.map((row) => row.count), |  | ||||||
|                         }, |  | ||||||
|                         { |  | ||||||
|                             label: "Intervals", |  | ||||||
|                             data: data.scheduleLong.interval.map((row) => row.count), |  | ||||||
|                         }, |  | ||||||
|                     ], |  | ||||||
|                 }, |  | ||||||
|                 options: { |  | ||||||
|                     responsive: true, |  | ||||||
|                     maintainAspectRatio: false, |  | ||||||
|                     scales: { |  | ||||||
|                         x: { |  | ||||||
|                             stacked: true, |  | ||||||
|                             type: "time", |  | ||||||
|                             time: { |  | ||||||
|                                 unit: "day", |  | ||||||
|                             }, |  | ||||||
|                         }, |  | ||||||
|                         y: { |  | ||||||
|                             stacked: true, |  | ||||||
|                         }, |  | ||||||
|                     }, |  | ||||||
|                 }, |  | ||||||
|             }); |  | ||||||
|  |  | ||||||
|             new Chart(document.getElementById("historyLong"), { |  | ||||||
|                 type: "bar", |  | ||||||
|                 data: { |  | ||||||
|                     labels: [...data.historyLong.sent, ...data.historyLong.failed].map( |  | ||||||
|                         (row) => luxon.DateTime.fromISO(row.time_key) |  | ||||||
|                     ), |  | ||||||
|                     datasets: [ |  | ||||||
|                         { |  | ||||||
|                             label: "Success", |  | ||||||
|                             data: data.historyLong.sent.map((row) => row.count), |  | ||||||
|                         }, |  | ||||||
|                         { |  | ||||||
|                             label: "Fail", |  | ||||||
|                             data: data.historyLong.failed.map((row) => row.count), |  | ||||||
|                         }, |  | ||||||
|                     ], |  | ||||||
|                 }, |  | ||||||
|                 options: { |  | ||||||
|                     responsive: true, |  | ||||||
|                     maintainAspectRatio: false, |  | ||||||
|                     scales: { |  | ||||||
|                         x: { |  | ||||||
|                             stacked: true, |  | ||||||
|                             type: "time", |  | ||||||
|                             time: { |  | ||||||
|                                 unit: "day", |  | ||||||
|                             }, |  | ||||||
|                         }, |  | ||||||
|                         y: { |  | ||||||
|                             stacked: true, |  | ||||||
|                         }, |  | ||||||
|                     }, |  | ||||||
|                 }, |  | ||||||
|             }); |  | ||||||
|         }); |  | ||||||
| }); |  | ||||||
| @@ -1,7 +0,0 @@ | |||||||
| /*! |  | ||||||
|  * chartjs-adapter-luxon v1.3.1 |  | ||||||
|  * https://www.chartjs.org |  | ||||||
|  * (c) 2023 chartjs-adapter-luxon Contributors |  | ||||||
|  * Released under the MIT license |  | ||||||
|  */ |  | ||||||
| !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(require("chart.js"),require("luxon")):"function"==typeof define&&define.amd?define(["chart.js","luxon"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).Chart,e.luxon)}(this,(function(e,t){"use strict";const n={datetime:t.DateTime.DATETIME_MED_WITH_SECONDS,millisecond:"h:mm:ss.SSS a",second:t.DateTime.TIME_WITH_SECONDS,minute:t.DateTime.TIME_SIMPLE,hour:{hour:"numeric"},day:{day:"numeric",month:"short"},week:"DD",month:{month:"short",year:"numeric"},quarter:"'Q'q - yyyy",year:{year:"numeric"}};e._adapters._date.override({_id:"luxon",_create:function(e){return t.DateTime.fromMillis(e,this.options)},init(e){this.options.locale||(this.options.locale=e.locale)},formats:function(){return n},parse:function(e,n){const i=this.options,r=typeof e;return null===e||"undefined"===r?null:("number"===r?e=this._create(e):"string"===r?e="string"==typeof n?t.DateTime.fromFormat(e,n,i):t.DateTime.fromISO(e,i):e instanceof Date?e=t.DateTime.fromJSDate(e,i):"object"!==r||e instanceof t.DateTime||(e=t.DateTime.fromObject(e,i)),e.isValid?e.valueOf():null)},format:function(e,t){const n=this._create(e);return"string"==typeof t?n.toFormat(t):n.toLocaleString(t)},add:function(e,t,n){const i={};return i[n]=t,this._create(e).plus(i).valueOf()},diff:function(e,t,n){return this._create(e).diff(this._create(t)).as(n).valueOf()},startOf:function(e,t,n){if("isoWeek"===t){n=Math.trunc(Math.min(Math.max(0,n),6));const t=this._create(e);return t.minus({days:(t.weekday-n+7)%7}).startOf("day").valueOf()}return t?this._create(e).startOf(t).valueOf():e},endOf:function(e,t){return this._create(e).endOf(t).valueOf()}})})); |  | ||||||
| @@ -1,16 +0,0 @@ | |||||||
| const REPORTER_ID = crypto.randomUUID(); |  | ||||||
|  |  | ||||||
| window.addEventListener("error", async (ev) => { |  | ||||||
|     await fetch("/report", { |  | ||||||
|         method: "POST", |  | ||||||
|         body: JSON.stringify({ |  | ||||||
|             reporterId: REPORTER_ID, |  | ||||||
|             url: window.location.href, |  | ||||||
|             relativeTimestamp: ev.timeStamp, |  | ||||||
|             errorMessage: ev.message, |  | ||||||
|             errorLine: ev.lineno, |  | ||||||
|             errorFile: ev.filename, |  | ||||||
|             errorType: ev.type, |  | ||||||
|         }), |  | ||||||
|     }); |  | ||||||
| }); |  | ||||||
| @@ -1,20 +0,0 @@ | |||||||
| { |  | ||||||
|     "name": "Reminder Bot Dashboard", |  | ||||||
|     "short_name": "Reminders", |  | ||||||
|     "start_url": "/dashboard", |  | ||||||
|     "icons": [ |  | ||||||
|         { |  | ||||||
|             "src": "/static/favicon/android-chrome-192x192.png", |  | ||||||
|             "sizes": "192x192", |  | ||||||
|             "type": "image/png" |  | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|             "src": "/static/favicon/android-chrome-512x512.png", |  | ||||||
|             "sizes": "512x512", |  | ||||||
|             "type": "image/png" |  | ||||||
|         } |  | ||||||
|     ], |  | ||||||
|     "theme_color": "#ffffff", |  | ||||||
|     "background_color": "#ffffff", |  | ||||||
|     "display": "standalone" |  | ||||||
| } |  | ||||||
| @@ -1,247 +0,0 @@ | |||||||
| import axios from "axios"; |  | ||||||
|  |  | ||||||
| type UserInfo = { |  | ||||||
|     name: string; |  | ||||||
|     patreon: boolean; |  | ||||||
|     timezone: string | null; |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| export type GuildInfo = { |  | ||||||
|     id: string; |  | ||||||
|     patreon: boolean; |  | ||||||
|     name: string; |  | ||||||
|     error?: string; |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| type EmbedField = { |  | ||||||
|     title: string; |  | ||||||
|     value: string; |  | ||||||
|     inline: boolean; |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| export type Reminder = { |  | ||||||
|     attachment: string | null; |  | ||||||
|     attachment_name: string | null; |  | ||||||
|     avatar: string | null; |  | ||||||
|     channel: string; |  | ||||||
|     content: string; |  | ||||||
|     embed_author: string; |  | ||||||
|     embed_author_url: string | null; |  | ||||||
|     embed_color: number; |  | ||||||
|     embed_description: string; |  | ||||||
|     embed_footer: string; |  | ||||||
|     embed_footer_url: string | null; |  | ||||||
|     embed_image_url: string | null; |  | ||||||
|     embed_thumbnail_url: string | null; |  | ||||||
|     embed_title: string; |  | ||||||
|     embed_fields: EmbedField[] | null; |  | ||||||
|     enabled: boolean; |  | ||||||
|     expires: string | null; |  | ||||||
|     interval_seconds: number | null; |  | ||||||
|     interval_days: number | null; |  | ||||||
|     interval_months: number | null; |  | ||||||
|     name: string; |  | ||||||
|     restartable: boolean; |  | ||||||
|     tts: boolean; |  | ||||||
|     uid: string; |  | ||||||
|     username: string; |  | ||||||
|     utc_time: string; |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| export type Todo = { |  | ||||||
|     id: number; |  | ||||||
|     channel_id: string; |  | ||||||
|     value: string; |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| export type CreateTodo = { |  | ||||||
|     channel_id: string; |  | ||||||
|     value: string; |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| export type UpdateTodo = { |  | ||||||
|     value: string; |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| export type ChannelInfo = { |  | ||||||
|     id: string; |  | ||||||
|     name: string; |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| type RoleInfo = { |  | ||||||
|     id: string; |  | ||||||
|     name: string; |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| type EmojiInfo = { |  | ||||||
|     fmt: string; |  | ||||||
|     name: string; |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| type Template = { |  | ||||||
|     id: number; |  | ||||||
|     name: string; |  | ||||||
|     attachment: string | null; |  | ||||||
|     attachment_name: string | null; |  | ||||||
|     avatar: string | null; |  | ||||||
|     channel: string; |  | ||||||
|     content: string; |  | ||||||
|     embed_author: string; |  | ||||||
|     embed_author_url: string | null; |  | ||||||
|     embed_color: number; |  | ||||||
|     embed_description: string; |  | ||||||
|     embed_footer: string; |  | ||||||
|     embed_footer_url: string | null; |  | ||||||
|     embed_image_url: string | null; |  | ||||||
|     embed_thumbnail_url: string | null; |  | ||||||
|     embed_title: string; |  | ||||||
|     embed_fields: EmbedField[] | null; |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| const USER_INFO_STALE_TIME = 120_000; |  | ||||||
| const GUILD_INFO_STALE_TIME = 300_000; |  | ||||||
| const OTHER_STALE_TIME = 120_000; |  | ||||||
|  |  | ||||||
| export const fetchUserInfo = () => ({ |  | ||||||
|     queryKey: ["USER_INFO"], |  | ||||||
|     queryFn: () => axios.get("/dashboard/api/user").then((resp) => resp.data) as Promise<UserInfo>, |  | ||||||
|     staleTime: USER_INFO_STALE_TIME, |  | ||||||
| }); |  | ||||||
|  |  | ||||||
| export const patchUserInfo = () => ({ |  | ||||||
|     mutationFn: (timezone: string) => axios.patch(`/dashboard/api/user`, { timezone }), |  | ||||||
| }); |  | ||||||
|  |  | ||||||
| export const fetchUserGuilds = () => ({ |  | ||||||
|     queryKey: ["USER_GUILDS"], |  | ||||||
|     queryFn: () => |  | ||||||
|         axios.get("/dashboard/api/user/guilds").then((resp) => resp.data) as Promise<GuildInfo[]>, |  | ||||||
|     staleTime: USER_INFO_STALE_TIME, |  | ||||||
| }); |  | ||||||
|  |  | ||||||
| export const fetchGuildInfo = (guild: string) => ({ |  | ||||||
|     queryKey: ["GUILD_INFO", guild], |  | ||||||
|     queryFn: () => |  | ||||||
|         axios.get(`/dashboard/api/guild/${guild}`).then((resp) => resp.data) as Promise<GuildInfo>, |  | ||||||
|     staleTime: GUILD_INFO_STALE_TIME, |  | ||||||
| }); |  | ||||||
|  |  | ||||||
| export const fetchGuildChannels = (guild: string) => ({ |  | ||||||
|     queryKey: ["GUILD_CHANNELS", guild], |  | ||||||
|     queryFn: () => |  | ||||||
|         axios |  | ||||||
|             .get(`/dashboard/api/guild/${guild}/channels`) |  | ||||||
|             .then((resp) => |  | ||||||
|                 resp.data.sort((a: ChannelInfo, b: ChannelInfo) => |  | ||||||
|                     a.name == b.name ? 0 : a.name > b.name ? 1 : -1, |  | ||||||
|                 ), |  | ||||||
|             ) as Promise<ChannelInfo[]>, |  | ||||||
|     staleTime: GUILD_INFO_STALE_TIME, |  | ||||||
| }); |  | ||||||
|  |  | ||||||
| export const fetchGuildRoles = (guild: string) => ({ |  | ||||||
|     queryKey: ["GUILD_ROLES", guild], |  | ||||||
|     queryFn: () => |  | ||||||
|         axios.get(`/dashboard/api/guild/${guild}/roles`).then((resp) => resp.data) as Promise< |  | ||||||
|             RoleInfo[] |  | ||||||
|         >, |  | ||||||
|     staleTime: GUILD_INFO_STALE_TIME, |  | ||||||
| }); |  | ||||||
|  |  | ||||||
| export const fetchGuildEmojis = (guild: string) => ({ |  | ||||||
|     queryKey: ["GUILD_EMOJIS", guild], |  | ||||||
|     queryFn: () => |  | ||||||
|         axios.get(`/dashboard/api/guild/${guild}/emojis`).then((resp) => resp.data) as Promise< |  | ||||||
|             EmojiInfo[] |  | ||||||
|         >, |  | ||||||
|     staleTime: GUILD_INFO_STALE_TIME, |  | ||||||
| }); |  | ||||||
|  |  | ||||||
| export const fetchGuildReminders = (guild: string) => ({ |  | ||||||
|     queryKey: ["GUILD_REMINDERS", guild], |  | ||||||
|     queryFn: () => |  | ||||||
|         axios.get(`/dashboard/api/guild/${guild}/reminders`).then((resp) => resp.data) as Promise< |  | ||||||
|             Reminder[] |  | ||||||
|         >, |  | ||||||
|     staleTime: OTHER_STALE_TIME, |  | ||||||
| }); |  | ||||||
|  |  | ||||||
| export const patchGuildReminder = (guild: string) => ({ |  | ||||||
|     mutationFn: (reminder: Reminder) => |  | ||||||
|         axios.patch(`/dashboard/api/guild/${guild}/reminders`, reminder), |  | ||||||
| }); |  | ||||||
|  |  | ||||||
| export const postGuildReminder = (guild: string) => ({ |  | ||||||
|     mutationFn: (reminder: Reminder) => |  | ||||||
|         axios.post(`/dashboard/api/guild/${guild}/reminders`, reminder).then((resp) => resp.data), |  | ||||||
| }); |  | ||||||
|  |  | ||||||
| export const deleteReminder = () => ({ |  | ||||||
|     mutationFn: (reminder: Reminder) => |  | ||||||
|         axios.delete(`/dashboard/api/reminders`, { |  | ||||||
|             data: { |  | ||||||
|                 uid: reminder.uid, |  | ||||||
|             }, |  | ||||||
|         }), |  | ||||||
| }); |  | ||||||
|  |  | ||||||
| export const fetchGuildTemplates = (guild: string) => ({ |  | ||||||
|     queryKey: ["GUILD_TEMPLATES", guild], |  | ||||||
|     queryFn: () => |  | ||||||
|         axios.get(`/dashboard/api/guild/${guild}/templates`).then((resp) => resp.data) as Promise< |  | ||||||
|             Template[] |  | ||||||
|         >, |  | ||||||
|     staleTime: OTHER_STALE_TIME, |  | ||||||
| }); |  | ||||||
|  |  | ||||||
| export const postGuildTemplate = (guild: string) => ({ |  | ||||||
|     mutationFn: (reminder: Reminder) => |  | ||||||
|         axios.post(`/dashboard/api/guild/${guild}/templates`, reminder).then((resp) => resp.data), |  | ||||||
| }); |  | ||||||
|  |  | ||||||
| export const deleteGuildTemplate = (guild: string) => ({ |  | ||||||
|     mutationFn: (template: Template) => |  | ||||||
|         axios.delete(`/dashboard/api/guild/${guild}/templates`, { |  | ||||||
|             data: { |  | ||||||
|                 id: template.id, |  | ||||||
|             }, |  | ||||||
|         }), |  | ||||||
| }); |  | ||||||
|  |  | ||||||
| export const fetchGuildTodos = (guild: string) => ({ |  | ||||||
|     queryKey: ["GUILD_TODOS", guild], |  | ||||||
|     queryFn: () => |  | ||||||
|         axios.get(`/dashboard/api/guild/${guild}/todos`).then((resp) => resp.data) as Promise< |  | ||||||
|             Todo[] |  | ||||||
|         >, |  | ||||||
|     staleTime: OTHER_STALE_TIME, |  | ||||||
| }); |  | ||||||
|  |  | ||||||
| export const patchGuildTodo = (guild: string) => ({ |  | ||||||
|     mutationFn: ({ id, todo }) => axios.patch(`/dashboard/api/guild/${guild}/todos/${id}`, todo), |  | ||||||
| }); |  | ||||||
|  |  | ||||||
| export const postGuildTodo = (guild: string) => ({ |  | ||||||
|     mutationFn: (todo: CreateTodo) => |  | ||||||
|         axios.post(`/dashboard/api/guild/${guild}/todos`, todo).then((resp) => resp.data), |  | ||||||
| }); |  | ||||||
|  |  | ||||||
| export const deleteGuildTodo = (guild: string) => ({ |  | ||||||
|     mutationFn: (todoId: number) => axios.delete(`/dashboard/api/guild/${guild}/todos/${todoId}`), |  | ||||||
| }); |  | ||||||
|  |  | ||||||
| export const fetchUserReminders = () => ({ |  | ||||||
|     queryKey: ["USER_REMINDERS"], |  | ||||||
|     queryFn: () => |  | ||||||
|         axios.get(`/dashboard/api/user/reminders`).then((resp) => resp.data) as Promise<Reminder[]>, |  | ||||||
|     staleTime: OTHER_STALE_TIME, |  | ||||||
| }); |  | ||||||
|  |  | ||||||
| export const postUserReminder = () => ({ |  | ||||||
|     mutationFn: (reminder: Reminder) => |  | ||||||
|         axios.post(`/dashboard/api/user/reminders`, reminder).then((resp) => resp.data), |  | ||||||
| }); |  | ||||||
|  |  | ||||||
| export const patchUserReminder = () => ({ |  | ||||||
|     mutationFn: (reminder: Reminder) => axios.patch(`/dashboard/api/user/reminders`, reminder), |  | ||||||
| }); |  | ||||||
| @@ -1,7 +0,0 @@ | |||||||
| import { createContext } from "preact"; |  | ||||||
| import { useContext } from "preact/compat"; |  | ||||||
| import { Message } from "./FlashProvider"; |  | ||||||
|  |  | ||||||
| export const FlashContext = createContext(null as (message: Message) => void); |  | ||||||
|  |  | ||||||
| export const useFlash = () => useContext(FlashContext); |  | ||||||
| @@ -1,43 +0,0 @@ | |||||||
| import { FlashContext } from "./FlashContext"; |  | ||||||
| import { useState } from "preact/hooks"; |  | ||||||
| import { MESSAGE_FLASH_TIME } from "../../consts"; |  | ||||||
|  |  | ||||||
| export type Message = { |  | ||||||
|     message: string; |  | ||||||
|     type: "error" | "success"; |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| export const FlashProvider = ({ children }) => { |  | ||||||
|     const [messages, setMessages] = useState([] as Message[]); |  | ||||||
|  |  | ||||||
|     return ( |  | ||||||
|         <FlashContext.Provider |  | ||||||
|             value={(message: Message) => { |  | ||||||
|                 setMessages((messages: Message[]) => [...messages, message]); |  | ||||||
|                 setTimeout(() => { |  | ||||||
|                     setMessages((messages) => [...messages].splice(1)); |  | ||||||
|                 }, MESSAGE_FLASH_TIME); |  | ||||||
|             }} |  | ||||||
|         > |  | ||||||
|             <> |  | ||||||
|                 {children} |  | ||||||
|                 <div class="flash-container"> |  | ||||||
|                     {messages.map((message) => { |  | ||||||
|                         const className = message.type === "error" ? "is-danger" : "is-success"; |  | ||||||
|                         const icon = |  | ||||||
|                             message.type === "error" ? "fa-exclamation-circle" : "fa-check"; |  | ||||||
|  |  | ||||||
|                         return ( |  | ||||||
|                             <div class={`notification flash-message is-active ${className}`}> |  | ||||||
|                                 <span class="icon"> |  | ||||||
|                                     <i class={`far ${icon}`}></i> |  | ||||||
|                                 </span>{" "} |  | ||||||
|                                 <span class="error-message">{message.message}</span> |  | ||||||
|                             </div> |  | ||||||
|                         ); |  | ||||||
|                     })} |  | ||||||
|                 </div> |  | ||||||
|             </> |  | ||||||
|         </FlashContext.Provider> |  | ||||||
|     ); |  | ||||||
| }; |  | ||||||
| @@ -1,56 +0,0 @@ | |||||||
| import { useEffect, useMemo } from "preact/hooks"; |  | ||||||
| import { useQuery } from "react-query"; |  | ||||||
| import { fetchGuildChannels, fetchGuildRoles, fetchGuildEmojis } from "../../api"; |  | ||||||
| import Tribute from "tributejs"; |  | ||||||
| import { useGuild } from "./useGuild"; |  | ||||||
|  |  | ||||||
| export const Mentions = ({ input }) => { |  | ||||||
|     const guild = useGuild(); |  | ||||||
|  |  | ||||||
|     return <>{guild && <_Mentions guild={guild} input={input} />}</>; |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| const _Mentions = ({ guild, input }) => { |  | ||||||
|     const { data: roles } = useQuery(fetchGuildRoles(guild)); |  | ||||||
|     const { data: channels } = useQuery(fetchGuildChannels(guild)); |  | ||||||
|     const { data: emojis } = useQuery(fetchGuildEmojis(guild)); |  | ||||||
|  |  | ||||||
|     const tribute = useMemo(() => { |  | ||||||
|         return new Tribute({ |  | ||||||
|             collection: [ |  | ||||||
|                 { |  | ||||||
|                     trigger: "@", |  | ||||||
|                     values: (roles || []) |  | ||||||
|                         .filter((role) => role.name !== "@everyone") |  | ||||||
|                         .map(({ id, name }) => ({ key: name, value: id })), |  | ||||||
|                     allowSpaces: true, |  | ||||||
|                     selectTemplate: (item) => `<@&${item.original.value}>`, |  | ||||||
|                     menuItemTemplate: (item) => `@${item.original.key}`, |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                     trigger: "#", |  | ||||||
|                     values: (channels || []).map(({ id, name }) => ({ key: name, value: id })), |  | ||||||
|                     allowSpaces: true, |  | ||||||
|                     selectTemplate: (item) => `<#${item.original.value}>`, |  | ||||||
|                     menuItemTemplate: (item) => `#${item.original.key}`, |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                     trigger: ":", |  | ||||||
|                     values: (emojis || []).map(({ fmt, name }) => ({ key: name, value: fmt })), |  | ||||||
|                     allowSpaces: true, |  | ||||||
|                     selectTemplate: (item) => item.original.value, |  | ||||||
|                     menuItemTemplate: (item) => `:${item.original.key}:`, |  | ||||||
|                 }, |  | ||||||
|             ], |  | ||||||
|         }); |  | ||||||
|     }, [roles, channels, emojis]); |  | ||||||
|  |  | ||||||
|     useEffect(() => { |  | ||||||
|         tribute.detach(input.current); |  | ||||||
|         if (input.current !== null) { |  | ||||||
|             tribute.attach(input.current); |  | ||||||
|         } |  | ||||||
|     }, [tribute]); |  | ||||||
|  |  | ||||||
|     return <></>; |  | ||||||
| }; |  | ||||||
| @@ -1,20 +0,0 @@ | |||||||
| import { createContext } from "preact"; |  | ||||||
| import { useContext } from "preact/compat"; |  | ||||||
| import { useState } from "preact/hooks"; |  | ||||||
| import { DateTime } from "luxon"; |  | ||||||
|  |  | ||||||
| type TTimezoneContext = [string, (tz: string) => void]; |  | ||||||
|  |  | ||||||
| const TimezoneContext = createContext(["UTC", () => {}] as TTimezoneContext); |  | ||||||
|  |  | ||||||
| export const TimezoneProvider = ({ children }) => { |  | ||||||
|     const [timezone, setTimezone] = useState(DateTime.now().zoneName); |  | ||||||
|  |  | ||||||
|     return ( |  | ||||||
|         <TimezoneContext.Provider value={[timezone, setTimezone]}> |  | ||||||
|             {children} |  | ||||||
|         </TimezoneContext.Provider> |  | ||||||
|     ); |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| export const useTimezone = () => useContext(TimezoneContext); |  | ||||||
| @@ -1,54 +0,0 @@ | |||||||
| import { Sidebar } from "../Sidebar"; |  | ||||||
| import { QueryClient, QueryClientProvider } from "react-query"; |  | ||||||
| import { Route, Router, Switch } from "wouter"; |  | ||||||
| import { Welcome } from "../Welcome"; |  | ||||||
| import { Guild } from "../Guild"; |  | ||||||
| import { FlashProvider } from "./FlashProvider"; |  | ||||||
| import { TimezoneProvider } from "./TimezoneProvider"; |  | ||||||
| import { User } from "../User"; |  | ||||||
| import { GuildReminders } from "../Guild/GuildReminders"; |  | ||||||
| import { GuildTodos } from "../Guild/GuildTodos"; |  | ||||||
|  |  | ||||||
| export function App() { |  | ||||||
|     const queryClient = new QueryClient(); |  | ||||||
|  |  | ||||||
|     return ( |  | ||||||
|         <TimezoneProvider> |  | ||||||
|             <FlashProvider> |  | ||||||
|                 <QueryClientProvider client={queryClient}> |  | ||||||
|                     <Router base={"/dashboard"}> |  | ||||||
|                         <div class="columns is-gapless dashboard-frame"> |  | ||||||
|                             <Sidebar /> |  | ||||||
|                             <div class="column is-main-content"> |  | ||||||
|                                 <div style={{ margin: "0 12px 12px 12px" }}> |  | ||||||
|                                     <Switch> |  | ||||||
|                                         <Route path={"/@me/reminders"} component={User}></Route> |  | ||||||
|                                         <Route |  | ||||||
|                                             path={"/:guild/reminders"} |  | ||||||
|                                             component={() => ( |  | ||||||
|                                                 <Guild> |  | ||||||
|                                                     <GuildReminders /> |  | ||||||
|                                                 </Guild> |  | ||||||
|                                             )} |  | ||||||
|                                         ></Route> |  | ||||||
|                                         <Route |  | ||||||
|                                             path={"/:guild/todos"} |  | ||||||
|                                             component={() => ( |  | ||||||
|                                                 <Guild> |  | ||||||
|                                                     <GuildTodos /> |  | ||||||
|                                                 </Guild> |  | ||||||
|                                             )} |  | ||||||
|                                         ></Route> |  | ||||||
|                                         <Route> |  | ||||||
|                                             <Welcome /> |  | ||||||
|                                         </Route> |  | ||||||
|                                     </Switch> |  | ||||||
|                                 </div> |  | ||||||
|                             </div> |  | ||||||
|                         </div> |  | ||||||
|                     </Router> |  | ||||||
|                 </QueryClientProvider> |  | ||||||
|             </FlashProvider> |  | ||||||
|         </TimezoneProvider> |  | ||||||
|     ); |  | ||||||
| } |  | ||||||
| @@ -1,6 +0,0 @@ | |||||||
| import { useParams } from "wouter"; |  | ||||||
|  |  | ||||||
| export const useGuild = () => { |  | ||||||
|     const { guild } = useParams() as { guild?: string }; |  | ||||||
|     return guild || null; |  | ||||||
| }; |  | ||||||
| @@ -1,30 +0,0 @@ | |||||||
| export const GuildError = () => { |  | ||||||
|     return ( |  | ||||||
|         <div class="hero is-fullheight"> |  | ||||||
|             <div class="hero-body"> |  | ||||||
|                 <div class="container has-text-centered"> |  | ||||||
|                     <p class="title">We couldn't get this server's data</p> |  | ||||||
|                     <p class="subtitle"> |  | ||||||
|                         The bot may have just been restarted, in which case please try again in a |  | ||||||
|                         few minutes. |  | ||||||
|                         <br /> |  | ||||||
|                         <br /> |  | ||||||
|                         Otherwise, please check Reminder Bot is in the server, and has correct |  | ||||||
|                         permissions. |  | ||||||
|                     </p> |  | ||||||
|                     <a |  | ||||||
|                         class="button is-size-4 is-rounded is-success" |  | ||||||
|                         href="https://invite.reminder-bot.com" |  | ||||||
|                     > |  | ||||||
|                         <p class="is-size-4"> |  | ||||||
|                             <span>Add to Server</span>{" "} |  | ||||||
|                             <span class="icon"> |  | ||||||
|                                 <i class="fas fa-chevron-right"></i> |  | ||||||
|                             </span> |  | ||||||
|                         </p> |  | ||||||
|                     </a> |  | ||||||
|                 </div> |  | ||||||
|             </div> |  | ||||||
|         </div> |  | ||||||
|     ); |  | ||||||
| }; |  | ||||||
| @@ -1,144 +0,0 @@ | |||||||
| import { useQuery, useQueryClient } from "react-query"; |  | ||||||
| import { fetchGuildChannels, fetchGuildReminders } from "../../api"; |  | ||||||
| import { EditReminder } from "../Reminder/EditReminder"; |  | ||||||
| import { CreateReminder } from "../Reminder/CreateReminder"; |  | ||||||
| import { useCallback, useState } from "preact/hooks"; |  | ||||||
| import { Loader } from "../Loader"; |  | ||||||
| import { useGuild } from "../App/useGuild"; |  | ||||||
|  |  | ||||||
| enum Sort { |  | ||||||
|     Time = "time", |  | ||||||
|     Name = "name", |  | ||||||
|     Channel = "channel", |  | ||||||
| } |  | ||||||
|  |  | ||||||
| export const GuildReminders = () => { |  | ||||||
|     const guild = useGuild(); |  | ||||||
|  |  | ||||||
|     const { |  | ||||||
|         isSuccess, |  | ||||||
|         isFetching, |  | ||||||
|         isFetched, |  | ||||||
|         data: guildReminders, |  | ||||||
|     } = useQuery(fetchGuildReminders(guild)); |  | ||||||
|     const { data: channels } = useQuery(fetchGuildChannels(guild)); |  | ||||||
|  |  | ||||||
|     const [collapsed, setCollapsed] = useState(false); |  | ||||||
|     const [sort, _setSort] = useState(Sort.Time); |  | ||||||
|     const queryClient = useQueryClient(); |  | ||||||
|  |  | ||||||
|     let prevReminder = null; |  | ||||||
|  |  | ||||||
|     const setSort = useCallback((sort) => { |  | ||||||
|         queryClient.invalidateQueries(["GUILD_REMINDERS"]); |  | ||||||
|         _setSort(sort); |  | ||||||
|     }, []); |  | ||||||
|  |  | ||||||
|     return ( |  | ||||||
|         <> |  | ||||||
|             {!isFetched && <Loader />} |  | ||||||
|  |  | ||||||
|             <strong>Create Reminder</strong> |  | ||||||
|             <div id={"reminderCreator"}> |  | ||||||
|                 <CreateReminder /> |  | ||||||
|             </div> |  | ||||||
|             <br /> |  | ||||||
|             <div class={"field"}> |  | ||||||
|                 <div class={"columns is-mobile"}> |  | ||||||
|                     <div class={"column"}> |  | ||||||
|                         <strong>Reminders</strong> |  | ||||||
|                     </div> |  | ||||||
|                     <div class={"column is-narrow"}> |  | ||||||
|                         <div class="control has-icons-left"> |  | ||||||
|                             <div class="select is-small"> |  | ||||||
|                                 <select |  | ||||||
|                                     id="orderBy" |  | ||||||
|                                     onInput={(ev) => { |  | ||||||
|                                         setSort(ev.currentTarget.value as Sort); |  | ||||||
|                                     }} |  | ||||||
|                                 > |  | ||||||
|                                     <option value={Sort.Time} selected={sort == Sort.Time}> |  | ||||||
|                                         Time |  | ||||||
|                                     </option> |  | ||||||
|                                     <option value={Sort.Name} selected={sort == Sort.Name}> |  | ||||||
|                                         Name |  | ||||||
|                                     </option> |  | ||||||
|                                     <option value={Sort.Channel} selected={sort == Sort.Channel}> |  | ||||||
|                                         Channel |  | ||||||
|                                     </option> |  | ||||||
|                                 </select> |  | ||||||
|                             </div> |  | ||||||
|                             <div class="icon is-small is-left"> |  | ||||||
|                                 <i class="fas fa-sort-amount-down"></i> |  | ||||||
|                             </div> |  | ||||||
|                         </div> |  | ||||||
|                     </div> |  | ||||||
|                     <div class={"column is-narrow"}> |  | ||||||
|                         <div class="control has-icons-left"> |  | ||||||
|                             <div class="select is-small"> |  | ||||||
|                                 <select |  | ||||||
|                                     id="expandAll" |  | ||||||
|                                     onInput={(ev) => { |  | ||||||
|                                         if (ev.currentTarget.value === "expand") { |  | ||||||
|                                             setCollapsed(false); |  | ||||||
|                                         } else if (ev.currentTarget.value === "collapse") { |  | ||||||
|                                             setCollapsed(true); |  | ||||||
|                                         } |  | ||||||
|                                     }} |  | ||||||
|                                 > |  | ||||||
|                                     <option value="" selected></option> |  | ||||||
|                                     <option value="expand">Expand All</option> |  | ||||||
|                                     <option value="collapse">Collapse All</option> |  | ||||||
|                                 </select> |  | ||||||
|                             </div> |  | ||||||
|                             <div class="icon is-small is-left"> |  | ||||||
|                                 <i class="fas fa-expand-arrows"></i> |  | ||||||
|                             </div> |  | ||||||
|                         </div> |  | ||||||
|                     </div> |  | ||||||
|                 </div> |  | ||||||
|             </div> |  | ||||||
|  |  | ||||||
|             <div id={"guildReminders"} className={isFetching ? "loading" : ""}> |  | ||||||
|                 {isSuccess && |  | ||||||
|                     guildReminders |  | ||||||
|                         .sort((r1, r2) => { |  | ||||||
|                             if (sort === Sort.Time) { |  | ||||||
|                                 return r1.utc_time > r2.utc_time ? 1 : -1; |  | ||||||
|                             } else if (sort === Sort.Name) { |  | ||||||
|                                 return r1.name > r2.name ? 1 : -1; |  | ||||||
|                             } else { |  | ||||||
|                                 return r1.channel > r2.channel ? 1 : -1; |  | ||||||
|                             } |  | ||||||
|                         }) |  | ||||||
|                         .map((reminder) => { |  | ||||||
|                             let breaker = <></>; |  | ||||||
|                             if (sort === Sort.Channel && channels) { |  | ||||||
|                                 if ( |  | ||||||
|                                     prevReminder === null || |  | ||||||
|                                     prevReminder.channel !== reminder.channel |  | ||||||
|                                 ) { |  | ||||||
|                                     const channel = channels.find( |  | ||||||
|                                         (ch) => ch.id === reminder.channel, |  | ||||||
|                                     ); |  | ||||||
|                                     breaker = <div class={"channel-tag"}>#{channel.name}</div>; |  | ||||||
|                                 } |  | ||||||
|                             } |  | ||||||
|  |  | ||||||
|                             prevReminder = reminder; |  | ||||||
|  |  | ||||||
|                             return ( |  | ||||||
|                                 <> |  | ||||||
|                                     {breaker} |  | ||||||
|                                     <EditReminder |  | ||||||
|                                         key={reminder.uid} |  | ||||||
|                                         reminder={reminder} |  | ||||||
|                                         globalCollapse={collapsed} |  | ||||||
|                                     /> |  | ||||||
|                                 </> |  | ||||||
|                             ); |  | ||||||
|                         })} |  | ||||||
|             </div> |  | ||||||
|         </> |  | ||||||
|     ); |  | ||||||
| }; |  | ||||||
| @@ -1,62 +0,0 @@ | |||||||
| import { useQuery } from "react-query"; |  | ||||||
| import { ChannelInfo, fetchGuildChannels, fetchGuildTodos } from "../../api"; |  | ||||||
| import { useGuild } from "../App/useGuild"; |  | ||||||
| import { Todo } from "../Todo"; |  | ||||||
| import { Todo as TodoT } from "../../api"; |  | ||||||
| import { Loader } from "../Loader"; |  | ||||||
| import { CreateTodo } from "../Todo/CreateTodo"; |  | ||||||
|  |  | ||||||
| export const GuildTodos = () => { |  | ||||||
|     const guild = useGuild(); |  | ||||||
|  |  | ||||||
|     const { isFetched, data: guildTodos } = useQuery(fetchGuildTodos(guild)); |  | ||||||
|     const { data: channels } = useQuery(fetchGuildChannels(guild)); |  | ||||||
|  |  | ||||||
|     if (!isFetched || !channels) { |  | ||||||
|         return <Loader />; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     const sortedTodos = guildTodos.sort((a, b) => (a.id < b.id ? -1 : 1)); |  | ||||||
|     const globalTodos = sortedTodos.filter((todo) => todo.channel_id === null); |  | ||||||
|  |  | ||||||
|     return ( |  | ||||||
|         <> |  | ||||||
|             <strong>Create todo list</strong> |  | ||||||
|             <CreateTodo channel={null} showSelector={true} /> |  | ||||||
|             <strong>Todo lists</strong> |  | ||||||
|             {globalTodos.length > 0 && ( |  | ||||||
|                 <> |  | ||||||
|                     <h2>Server</h2> |  | ||||||
|                     {globalTodos.map((todo) => ( |  | ||||||
|                         <> |  | ||||||
|                             <Todo todo={todo} key={todo.id} /> |  | ||||||
|                         </> |  | ||||||
|                     ))} |  | ||||||
|                     <CreateTodo channel={null} /> |  | ||||||
|                 </> |  | ||||||
|             )} |  | ||||||
|             {channels |  | ||||||
|                 .map( |  | ||||||
|                     (channel) => |  | ||||||
|                         [channel, sortedTodos.filter((todo) => todo.channel_id === channel.id)] as [ |  | ||||||
|                             ChannelInfo, |  | ||||||
|                             TodoT[], |  | ||||||
|                         ], |  | ||||||
|                 ) |  | ||||||
|                 .filter(([_, todos]) => todos.length > 0) |  | ||||||
|                 .map(([channel, todos]) => { |  | ||||||
|                     return ( |  | ||||||
|                         <> |  | ||||||
|                             <h2>#{channel.name}</h2> |  | ||||||
|                             {todos.map((todo) => ( |  | ||||||
|                                 <> |  | ||||||
|                                     <Todo todo={todo} key={todo.id} /> |  | ||||||
|                                 </> |  | ||||||
|                             ))} |  | ||||||
|                             <CreateTodo channel={channel.id} /> |  | ||||||
|                         </> |  | ||||||
|                     ); |  | ||||||
|                 })} |  | ||||||
|         </> |  | ||||||
|     ); |  | ||||||
| }; |  | ||||||
| @@ -1,5 +0,0 @@ | |||||||
| .page-links { |  | ||||||
|     > * { |  | ||||||
|         margin: 2px; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,51 +0,0 @@ | |||||||
| import { useQuery } from "react-query"; |  | ||||||
| import { fetchGuildInfo } from "../../api"; |  | ||||||
| import { GuildError } from "./GuildError"; |  | ||||||
| import { createPortal, PropsWithChildren } from "preact/compat"; |  | ||||||
| import { Import } from "../Import"; |  | ||||||
| import { useGuild } from "../App/useGuild"; |  | ||||||
| import { Link } from "wouter"; |  | ||||||
| import { usePathname } from "wouter/use-browser-location"; |  | ||||||
|  |  | ||||||
| import "./index.scss"; |  | ||||||
|  |  | ||||||
| export const Guild = ({ children }: PropsWithChildren) => { |  | ||||||
|     const guild = useGuild(); |  | ||||||
|     const { isSuccess, data: guildInfo } = useQuery(fetchGuildInfo(guild)); |  | ||||||
|  |  | ||||||
|     if (!isSuccess) { |  | ||||||
|         return <></>; |  | ||||||
|     } else if (guildInfo.error) { |  | ||||||
|         return <GuildError />; |  | ||||||
|     } else { |  | ||||||
|         const importModal = createPortal(<Import />, document.getElementById("bottom-sidebar")); |  | ||||||
|         const path = usePathname(); |  | ||||||
|  |  | ||||||
|         return ( |  | ||||||
|             <> |  | ||||||
|                 {importModal} |  | ||||||
|                 <div class="page-links"> |  | ||||||
|                     <Link |  | ||||||
|                         class={`button is-outlined is-success is-small ${path.endsWith("reminders") && "is-focused"}`} |  | ||||||
|                         href={`/${guild}/reminders`} |  | ||||||
|                     > |  | ||||||
|                         <span>Reminders</span> |  | ||||||
|                         <span class="icon"> |  | ||||||
|                             <i class="fa fa-chevron-right"></i> |  | ||||||
|                         </span> |  | ||||||
|                     </Link> |  | ||||||
|                     <Link |  | ||||||
|                         class={`button is-outlined is-success is-small ${path.endsWith("todos") && "is-focused"}`} |  | ||||||
|                         href={`/${guild}/todos`} |  | ||||||
|                     > |  | ||||||
|                         <span>Todo lists</span> |  | ||||||
|                         <span class="icon"> |  | ||||||
|                             <i class="fa fa-chevron-right"></i> |  | ||||||
|                         </span> |  | ||||||
|                     </Link> |  | ||||||
|                 </div> |  | ||||||
|                 {children} |  | ||||||
|             </> |  | ||||||
|         ); |  | ||||||
|     } |  | ||||||
| }; |  | ||||||
| @@ -1,151 +0,0 @@ | |||||||
| import { Modal } from "../Modal"; |  | ||||||
| import { useRef, useState } from "preact/hooks"; |  | ||||||
| import { useParams } from "wouter"; |  | ||||||
| import axios from "axios"; |  | ||||||
| import { useFlash } from "../App/FlashContext"; |  | ||||||
| import { useGuild } from "../App/useGuild"; |  | ||||||
| import { useQueryClient } from "react-query"; |  | ||||||
|  |  | ||||||
| export const Import = () => { |  | ||||||
|     const [modalOpen, setModalOpen] = useState(false); |  | ||||||
|  |  | ||||||
|     return ( |  | ||||||
|         <> |  | ||||||
|             <a |  | ||||||
|                 class="show-modal" |  | ||||||
|                 data-modal="chooseTimezoneModal" |  | ||||||
|                 onClick={() => { |  | ||||||
|                     setModalOpen(true); |  | ||||||
|                 }} |  | ||||||
|             > |  | ||||||
|                 <span class="icon"> |  | ||||||
|                     <i class="fas fa-exchange"></i> |  | ||||||
|                 </span>{" "} |  | ||||||
|                 Import/Export |  | ||||||
|             </a> |  | ||||||
|             {modalOpen && <ImportModal setModalOpen={setModalOpen} />} |  | ||||||
|         </> |  | ||||||
|     ); |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| const ImportModal = ({ setModalOpen }) => { |  | ||||||
|     const guild = useGuild(); |  | ||||||
|  |  | ||||||
|     const aRef = useRef<HTMLAnchorElement>(); |  | ||||||
|     const inputRef = useRef<HTMLInputElement>(); |  | ||||||
|     const flash = useFlash(); |  | ||||||
|  |  | ||||||
|     const [isImporting, setIsImporting] = useState(false); |  | ||||||
|  |  | ||||||
|     const queryClient = useQueryClient(); |  | ||||||
|  |  | ||||||
|     return ( |  | ||||||
|         <Modal |  | ||||||
|             setModalOpen={setModalOpen} |  | ||||||
|             title={ |  | ||||||
|                 <> |  | ||||||
|                     Import/Export Manager{" "} |  | ||||||
|                     <a href="/help/iemanager"> |  | ||||||
|                         <span> |  | ||||||
|                             <i class="fa fa-question-circle"></i> |  | ||||||
|                         </span> |  | ||||||
|                     </a> |  | ||||||
|                 </> |  | ||||||
|             } |  | ||||||
|         > |  | ||||||
|             <> |  | ||||||
|                 <div class="control"> |  | ||||||
|                     <div class="field"> |  | ||||||
|                         <label> |  | ||||||
|                             <input |  | ||||||
|                                 type="radio" |  | ||||||
|                                 class="default-width" |  | ||||||
|                                 name="exportSelect" |  | ||||||
|                                 value="reminders" |  | ||||||
|                                 checked |  | ||||||
|                             /> |  | ||||||
|                             Reminders |  | ||||||
|                         </label> |  | ||||||
|                     </div> |  | ||||||
|                 </div> |  | ||||||
|                 <br /> |  | ||||||
|                 <div class="has-text-centered"> |  | ||||||
|                     <div style="color: red"> |  | ||||||
|                         Please first read the <a href="/help/iemanager">support page</a> |  | ||||||
|                     </div> |  | ||||||
|                     <button |  | ||||||
|                         class="button is-success is-outlined" |  | ||||||
|                         style={{ margin: "2px" }} |  | ||||||
|                         id="import-data" |  | ||||||
|                         disabled={isImporting} |  | ||||||
|                         onClick={() => { |  | ||||||
|                             inputRef.current.click(); |  | ||||||
|                         }} |  | ||||||
|                     > |  | ||||||
|                         Import Data |  | ||||||
|                     </button> |  | ||||||
|                     <button |  | ||||||
|                         class="button is-success" |  | ||||||
|                         style={{ margin: "2px" }} |  | ||||||
|                         id="export-data" |  | ||||||
|                         onClick={() => |  | ||||||
|                             axios |  | ||||||
|                                 .get(`/dashboard/api/guild/${guild}/export/reminders`) |  | ||||||
|                                 .then(({ data, status }) => { |  | ||||||
|                                     if (status === 200) { |  | ||||||
|                                         aRef.current.href = `data:text/plain;charset=utf-8,${encodeURIComponent( |  | ||||||
|                                             data.body, |  | ||||||
|                                         )}`; |  | ||||||
|                                         aRef.current.click(); |  | ||||||
|                                     } else { |  | ||||||
|                                         flash({ |  | ||||||
|                                             message: `Unexpected status ${status}`, |  | ||||||
|                                             type: "error", |  | ||||||
|                                         }); |  | ||||||
|                                     } |  | ||||||
|                                 }) |  | ||||||
|                         } |  | ||||||
|                     > |  | ||||||
|                         Export Data |  | ||||||
|                     </button> |  | ||||||
|                 </div> |  | ||||||
|                 <a ref={aRef} id="downloader" download="export.csv" class="is-hidden" /> |  | ||||||
|                 <input |  | ||||||
|                     ref={inputRef} |  | ||||||
|                     id="uploader" |  | ||||||
|                     type="file" |  | ||||||
|                     hidden |  | ||||||
|                     onChange={() => { |  | ||||||
|                         new Promise((resolve) => { |  | ||||||
|                             let fileReader = new FileReader(); |  | ||||||
|                             fileReader.onload = (e) => resolve(fileReader.result); |  | ||||||
|                             fileReader.readAsDataURL(inputRef.current.files[0]); |  | ||||||
|                         }).then((dataUrl: string) => { |  | ||||||
|                             setIsImporting(true); |  | ||||||
|  |  | ||||||
|                             axios |  | ||||||
|                                 .put(`/dashboard/api/guild/${guild}/export/reminders`, { |  | ||||||
|                                     body: dataUrl.split(",")[1], |  | ||||||
|                                 }) |  | ||||||
|                                 .then(({ data }) => { |  | ||||||
|                                     setIsImporting(false); |  | ||||||
|  |  | ||||||
|                                     if (data.error) { |  | ||||||
|                                         flash({ message: data.error, type: "error" }); |  | ||||||
|                                     } else { |  | ||||||
|                                         flash({ message: data.message, type: "success" }); |  | ||||||
|                                         queryClient.invalidateQueries({ |  | ||||||
|                                             queryKey: ["GUILD_REMINDERS", guild], |  | ||||||
|                                         }); |  | ||||||
|                                     } |  | ||||||
|                                 }) |  | ||||||
|                                 .then(() => { |  | ||||||
|                                     delete inputRef.current.files[0]; |  | ||||||
|                                 }); |  | ||||||
|                         }); |  | ||||||
|                     }} |  | ||||||
|                 /> |  | ||||||
|             </> |  | ||||||
|         </Modal> |  | ||||||
|     ); |  | ||||||
| }; |  | ||||||
| @@ -1,8 +0,0 @@ | |||||||
| export const Loader = () => ( |  | ||||||
|     <div className={"load-screen"}> |  | ||||||
|         <div> |  | ||||||
|             <p>Loading...</p> |  | ||||||
|             <i className={"fa fa-cog fa-spin"}></i> |  | ||||||
|         </div> |  | ||||||
|     </div> |  | ||||||
| ); |  | ||||||
| @@ -1,61 +0,0 @@ | |||||||
| import {JSX} from "preact"; |  | ||||||
| import {createPortal} from "preact/compat"; |  | ||||||
|  |  | ||||||
| type Props = { |  | ||||||
|     setModalOpen: (open: boolean) => never; |  | ||||||
|     title: string | JSX.Element; |  | ||||||
|     onSubmitText?: string; |  | ||||||
|     onSubmit?: () => void; |  | ||||||
|     children: string | JSX.Element | JSX.Element[] | (() => JSX.Element); |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| export const Modal = ({setModalOpen, title, onSubmit, onSubmitText, children}: Props) => { |  | ||||||
|     const body = document.querySelector("body"); |  | ||||||
|  |  | ||||||
|     return createPortal( |  | ||||||
|         <div class="modal is-active"> |  | ||||||
|             <div |  | ||||||
|                 class="modal-background" |  | ||||||
|                 onClick={() => { |  | ||||||
|                     setModalOpen(false); |  | ||||||
|                 }} |  | ||||||
|             ></div> |  | ||||||
|             <div class="modal-card"> |  | ||||||
|                 <header class="modal-card-head"> |  | ||||||
|                     <label class="modal-card-title">{title}</label> |  | ||||||
|                     <button |  | ||||||
|                         class="delete close-modal" |  | ||||||
|                         aria-label="close" |  | ||||||
|                         onClick={() => { |  | ||||||
|                             setModalOpen(false); |  | ||||||
|                         }} |  | ||||||
|                     ></button> |  | ||||||
|                 </header> |  | ||||||
|                 <section class="modal-card-body">{children}</section> |  | ||||||
|                 {onSubmit && ( |  | ||||||
|                     <footer class="modal-card-foot"> |  | ||||||
|                         <button class="button is-success" onClick={onSubmit}> |  | ||||||
|                             {onSubmitText || "Save"} |  | ||||||
|                         </button> |  | ||||||
|                         <button |  | ||||||
|                             class="button close-modal" |  | ||||||
|                             onClick={() => { |  | ||||||
|                                 setModalOpen(false); |  | ||||||
|                             }} |  | ||||||
|                         > |  | ||||||
|                             Cancel |  | ||||||
|                         </button> |  | ||||||
|                     </footer> |  | ||||||
|                 )} |  | ||||||
|             </div> |  | ||||||
|             <button |  | ||||||
|                 class="modal-close is-large close-modal" |  | ||||||
|                 aria-label="close" |  | ||||||
|                 onClick={() => { |  | ||||||
|                     setModalOpen(false); |  | ||||||
|                 }} |  | ||||||
|             ></button> |  | ||||||
|         </div>, |  | ||||||
|         body, |  | ||||||
|     ); |  | ||||||
| }; |  | ||||||
| @@ -1,80 +0,0 @@ | |||||||
| import { useReminder } from "./ReminderContext"; |  | ||||||
| import { useFlash } from "../App/FlashContext"; |  | ||||||
|  |  | ||||||
| export const Attachment = () => { |  | ||||||
|     const [{ attachment_name }, setReminder] = useReminder(); |  | ||||||
|  |  | ||||||
|     const flash = useFlash(); |  | ||||||
|  |  | ||||||
|     return ( |  | ||||||
|         <div class="file is-small is-boxed"> |  | ||||||
|             <label class="file-label"> |  | ||||||
|                 <input |  | ||||||
|                     class="file-input" |  | ||||||
|                     type="file" |  | ||||||
|                     name="attachment" |  | ||||||
|                     onInput={async (ev) => { |  | ||||||
|                         const input = ev.currentTarget; |  | ||||||
|  |  | ||||||
|                         let file = input.files[0]; |  | ||||||
|  |  | ||||||
|                         if (file.size >= 8 * 1024 * 1024) { |  | ||||||
|                             flash({ message: "File too large (max. 8MB).", type: "error" }); |  | ||||||
|                             return; |  | ||||||
|                         } |  | ||||||
|  |  | ||||||
|                         let attachment: string = await new Promise((resolve) => { |  | ||||||
|                             let fileReader = new FileReader(); |  | ||||||
|                             fileReader.onload = () => resolve(fileReader.result as string); |  | ||||||
|                             fileReader.readAsDataURL(file); |  | ||||||
|                         }); |  | ||||||
|                         attachment = attachment.split(",")[1]; |  | ||||||
|                         const attachment_name = file.name; |  | ||||||
|  |  | ||||||
|                         setReminder((reminder) => ({ |  | ||||||
|                             ...reminder, |  | ||||||
|                             attachment, |  | ||||||
|                             attachment_name, |  | ||||||
|                         })); |  | ||||||
|                     }} |  | ||||||
|                 ></input> |  | ||||||
|                 <span class="file-cta"> |  | ||||||
|                     <span |  | ||||||
|                         class="file-label" |  | ||||||
|                         style={{ |  | ||||||
|                             maxWidth: "200px", |  | ||||||
|                         }} |  | ||||||
|                     > |  | ||||||
|                         {attachment_name || "Add Attachment"} |  | ||||||
|                     </span> |  | ||||||
|                     <span class="file-icon"> |  | ||||||
|                         <i class="fas fa-upload"></i> |  | ||||||
|                     </span> |  | ||||||
|                 </span> |  | ||||||
|             </label> |  | ||||||
|             {attachment_name && ( |  | ||||||
|                 <> |  | ||||||
|                     <button |  | ||||||
|                         onClick={() => { |  | ||||||
|                             setReminder((reminder) => ({ |  | ||||||
|                                 ...reminder, |  | ||||||
|                                 attachment: null, |  | ||||||
|                                 attachment_name: null, |  | ||||||
|                             })); |  | ||||||
|                         }} |  | ||||||
|                         style={{ |  | ||||||
|                             border: "none", |  | ||||||
|                             background: "none", |  | ||||||
|                             cursor: "pointer", |  | ||||||
|                         }} |  | ||||||
|                     > |  | ||||||
|                         <span class="sr-only">Remove attachment</span> |  | ||||||
|                         <span class="icon"> |  | ||||||
|                             <i class="fas fa-trash"></i> |  | ||||||
|                         </span> |  | ||||||
|                     </button> |  | ||||||
|                 </> |  | ||||||
|             )} |  | ||||||
|         </div> |  | ||||||
|     ); |  | ||||||
| }; |  | ||||||
| @@ -1,28 +0,0 @@ | |||||||
| import { ImagePicker } from "./ImagePicker"; |  | ||||||
| import { useReminder } from "./ReminderContext"; |  | ||||||
| import { useGuild } from "../App/useGuild"; |  | ||||||
|  |  | ||||||
| export const Avatar = () => { |  | ||||||
|     const guild = useGuild(); |  | ||||||
|     const [reminder, setReminder] = useReminder(); |  | ||||||
|  |  | ||||||
|     return guild ? ( |  | ||||||
|         <ImagePicker |  | ||||||
|             class="is-rounded avatar" |  | ||||||
|             url={reminder.avatar || "/static/img/icon.png"} |  | ||||||
|             alt="Image for discord avatar" |  | ||||||
|             setImage={(url: string) => { |  | ||||||
|                 setReminder((reminder) => ({ |  | ||||||
|                     ...reminder, |  | ||||||
|                     avatar: url, |  | ||||||
|                 })); |  | ||||||
|             }} |  | ||||||
|         ></ImagePicker> |  | ||||||
|     ) : ( |  | ||||||
|         <img |  | ||||||
|             class="is-rounded avatar" |  | ||||||
|             alt="Image for discord avatar" |  | ||||||
|             src={"/static/img/icon.png"} |  | ||||||
|         ></img> |  | ||||||
|     ); |  | ||||||
| }; |  | ||||||
| @@ -1,136 +0,0 @@ | |||||||
| import { LoadTemplate } from "../LoadTemplate"; |  | ||||||
| import { useReminder } from "../ReminderContext"; |  | ||||||
| import { useMutation, useQueryClient } from "react-query"; |  | ||||||
| import { postGuildReminder, postGuildTemplate, postUserReminder } from "../../../api"; |  | ||||||
| import { useState } from "preact/hooks"; |  | ||||||
| import { ICON_FLASH_TIME } from "../../../consts"; |  | ||||||
| import { useFlash } from "../../App/FlashContext"; |  | ||||||
| import { useGuild } from "../../App/useGuild"; |  | ||||||
|  |  | ||||||
| export const CreateButtonRow = () => { |  | ||||||
|     const guild = useGuild(); |  | ||||||
|     const [reminder] = useReminder(); |  | ||||||
|  |  | ||||||
|     const [recentlyCreated, setRecentlyCreated] = useState(false); |  | ||||||
|     const [templateRecentlyCreated, setTemplateRecentlyCreated] = useState(false); |  | ||||||
|  |  | ||||||
|     const flash = useFlash(); |  | ||||||
|     const queryClient = useQueryClient(); |  | ||||||
|     const mutation = useMutation({ |  | ||||||
|         ...(guild ? postGuildReminder(guild) : postUserReminder()), |  | ||||||
|         onError: (error) => { |  | ||||||
|             flash({ |  | ||||||
|                 message: `An error occurred: ${error}`, |  | ||||||
|                 type: "error", |  | ||||||
|             }); |  | ||||||
|         }, |  | ||||||
|         onSuccess: (data) => { |  | ||||||
|             if (data.error) { |  | ||||||
|                 flash({ |  | ||||||
|                     message: data.error, |  | ||||||
|                     type: "error", |  | ||||||
|                 }); |  | ||||||
|             } else { |  | ||||||
|                 flash({ |  | ||||||
|                     message: "Reminder created", |  | ||||||
|                     type: "success", |  | ||||||
|                 }); |  | ||||||
|                 if (guild) { |  | ||||||
|                     queryClient.invalidateQueries({ |  | ||||||
|                         queryKey: ["GUILD_REMINDERS", guild], |  | ||||||
|                     }); |  | ||||||
|                 } else { |  | ||||||
|                     queryClient.invalidateQueries({ |  | ||||||
|                         queryKey: ["USER_REMINDERS"], |  | ||||||
|                     }); |  | ||||||
|                 } |  | ||||||
|                 setRecentlyCreated(true); |  | ||||||
|                 setTimeout(() => { |  | ||||||
|                     setRecentlyCreated(false); |  | ||||||
|                 }, ICON_FLASH_TIME); |  | ||||||
|             } |  | ||||||
|         }, |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     const templateMutation = useMutation({ |  | ||||||
|         ...postGuildTemplate(guild), |  | ||||||
|         onSuccess: (data) => { |  | ||||||
|             if (data.error) { |  | ||||||
|                 flash({ |  | ||||||
|                     message: data.error, |  | ||||||
|                     type: "error", |  | ||||||
|                 }); |  | ||||||
|             } else { |  | ||||||
|                 flash({ |  | ||||||
|                     message: "Template created", |  | ||||||
|                     type: "success", |  | ||||||
|                 }); |  | ||||||
|                 queryClient.invalidateQueries({ |  | ||||||
|                     queryKey: ["GUILD_TEMPLATES", guild], |  | ||||||
|                 }); |  | ||||||
|                 setTemplateRecentlyCreated(true); |  | ||||||
|                 setTimeout(() => { |  | ||||||
|                     setTemplateRecentlyCreated(false); |  | ||||||
|                 }, ICON_FLASH_TIME); |  | ||||||
|             } |  | ||||||
|         }, |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     return ( |  | ||||||
|         <div class="button-row"> |  | ||||||
|             <div class="button-row-reminder"> |  | ||||||
|                 <button |  | ||||||
|                     class="button is-success" |  | ||||||
|                     onClick={() => { |  | ||||||
|                         mutation.mutate(reminder); |  | ||||||
|                     }} |  | ||||||
|                 > |  | ||||||
|                     <span>Create Reminder</span>{" "} |  | ||||||
|                     {mutation.isLoading ? ( |  | ||||||
|                         <span class="icon"> |  | ||||||
|                             <i class="fas fa-spin fa-cog"></i> |  | ||||||
|                         </span> |  | ||||||
|                     ) : recentlyCreated ? ( |  | ||||||
|                         <span class="icon"> |  | ||||||
|                             <i class="fas fa-check"></i> |  | ||||||
|                         </span> |  | ||||||
|                     ) : ( |  | ||||||
|                         <span class="icon"> |  | ||||||
|                             <i class="fas fa-sparkles"></i> |  | ||||||
|                         </span> |  | ||||||
|                     )} |  | ||||||
|                 </button> |  | ||||||
|             </div> |  | ||||||
|             {guild && ( |  | ||||||
|                 <div class="button-row-template"> |  | ||||||
|                     <div> |  | ||||||
|                         <button |  | ||||||
|                             class="button is-success is-outlined" |  | ||||||
|                             onClick={() => { |  | ||||||
|                                 templateMutation.mutate(reminder); |  | ||||||
|                             }} |  | ||||||
|                         > |  | ||||||
|                             <span>Create Template</span>{" "} |  | ||||||
|                             {templateMutation.isLoading ? ( |  | ||||||
|                                 <span class="icon"> |  | ||||||
|                                     <i class="fas fa-spin fa-cog"></i> |  | ||||||
|                                 </span> |  | ||||||
|                             ) : templateRecentlyCreated ? ( |  | ||||||
|                                 <span class="icon"> |  | ||||||
|                                     <i class="fas fa-check"></i> |  | ||||||
|                                 </span> |  | ||||||
|                             ) : ( |  | ||||||
|                                 <span class="icon"> |  | ||||||
|                                     <i class="fas fa-file-spreadsheet"></i> |  | ||||||
|                                 </span> |  | ||||||
|                             )} |  | ||||||
|                         </button> |  | ||||||
|                     </div> |  | ||||||
|                     <div> |  | ||||||
|                         <LoadTemplate /> |  | ||||||
|                     </div> |  | ||||||
|                 </div> |  | ||||||
|             )} |  | ||||||
|         </div> |  | ||||||
|     ); |  | ||||||
| }; |  | ||||||
| @@ -1,81 +0,0 @@ | |||||||
| import { useState } from "preact/hooks"; |  | ||||||
| import { Modal } from "../../Modal"; |  | ||||||
| import { useMutation, useQueryClient } from "react-query"; |  | ||||||
| import { useReminder } from "../ReminderContext"; |  | ||||||
| import { deleteReminder } from "../../../api"; |  | ||||||
| import { useParams } from "wouter"; |  | ||||||
| import { useFlash } from "../../App/FlashContext"; |  | ||||||
| import { useGuild } from "../../App/useGuild"; |  | ||||||
|  |  | ||||||
| export const DeleteButton = () => { |  | ||||||
|     const [modalOpen, setModalOpen] = useState(false); |  | ||||||
|  |  | ||||||
|     return ( |  | ||||||
|         <> |  | ||||||
|             <button |  | ||||||
|                 class="button is-danger delete-reminder" |  | ||||||
|                 onClick={() => { |  | ||||||
|                     setModalOpen(true); |  | ||||||
|                 }} |  | ||||||
|             > |  | ||||||
|                 Delete |  | ||||||
|             </button> |  | ||||||
|             {modalOpen && <DeleteModal setModalOpen={setModalOpen}></DeleteModal>} |  | ||||||
|         </> |  | ||||||
|     ); |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| const DeleteModal = ({ setModalOpen }) => { |  | ||||||
|     const [reminder] = useReminder(); |  | ||||||
|     const guild = useGuild(); |  | ||||||
|  |  | ||||||
|     const flash = useFlash(); |  | ||||||
|     const queryClient = useQueryClient(); |  | ||||||
|     const mutation = useMutation({ |  | ||||||
|         ...deleteReminder(), |  | ||||||
|         onSuccess: () => { |  | ||||||
|             flash({ |  | ||||||
|                 message: "Reminder deleted", |  | ||||||
|                 type: "success", |  | ||||||
|             }); |  | ||||||
|             if (guild) { |  | ||||||
|                 queryClient.invalidateQueries({ |  | ||||||
|                     queryKey: ["GUILD_REMINDERS", guild], |  | ||||||
|                 }); |  | ||||||
|             } else { |  | ||||||
|                 queryClient.invalidateQueries({ |  | ||||||
|                     queryKey: ["USER_REMINDERS"], |  | ||||||
|                 }); |  | ||||||
|             } |  | ||||||
|             setModalOpen(false); |  | ||||||
|         }, |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     return ( |  | ||||||
|         <Modal setModalOpen={setModalOpen} title={"Delete Reminder"}> |  | ||||||
|             <> |  | ||||||
|                 <p>This reminder will be permanently deleted. Are you sure?</p> |  | ||||||
|                 <br></br> |  | ||||||
|                 <div class="has-text-centered"> |  | ||||||
|                     <button |  | ||||||
|                         class="button is-danger" |  | ||||||
|                         onClick={() => { |  | ||||||
|                             mutation.mutate(reminder); |  | ||||||
|                         }} |  | ||||||
|                         disabled={mutation.isLoading} |  | ||||||
|                     > |  | ||||||
|                         Delete |  | ||||||
|                     </button> |  | ||||||
|                     <button |  | ||||||
|                         class="button is-light close-modal" |  | ||||||
|                         onClick={() => { |  | ||||||
|                             setModalOpen(false); |  | ||||||
|                         }} |  | ||||||
|                     > |  | ||||||
|                         Cancel |  | ||||||
|                     </button> |  | ||||||
|                 </div> |  | ||||||
|             </> |  | ||||||
|         </Modal> |  | ||||||
|     ); |  | ||||||
| }; |  | ||||||
| @@ -1,89 +0,0 @@ | |||||||
| import { useRef, useState } from "preact/hooks"; |  | ||||||
| import { useMutation, useQueryClient } from "react-query"; |  | ||||||
| import { patchGuildReminder, patchUserReminder } from "../../../api"; |  | ||||||
| import { ICON_FLASH_TIME } from "../../../consts"; |  | ||||||
| import { DeleteButton } from "./DeleteButton"; |  | ||||||
| import { useReminder } from "../ReminderContext"; |  | ||||||
| import { useFlash } from "../../App/FlashContext"; |  | ||||||
| import { useGuild } from "../../App/useGuild"; |  | ||||||
|  |  | ||||||
| export const EditButtonRow = () => { |  | ||||||
|     const guild = useGuild(); |  | ||||||
|     const [reminder, setReminder] = useReminder(); |  | ||||||
|  |  | ||||||
|     const [recentlySaved, setRecentlySaved] = useState(false); |  | ||||||
|  |  | ||||||
|     const iconFlashTimeout = useRef(0); |  | ||||||
|  |  | ||||||
|     const flash = useFlash(); |  | ||||||
|     const mutation = useMutation({ |  | ||||||
|         ...(guild ? patchGuildReminder(guild) : patchUserReminder()), |  | ||||||
|         onError: (error) => { |  | ||||||
|             flash({ |  | ||||||
|                 message: `An error occurred: ${error}`, |  | ||||||
|                 type: "error", |  | ||||||
|             }); |  | ||||||
|         }, |  | ||||||
|         onSuccess: (response) => { |  | ||||||
|             if (iconFlashTimeout.current !== null) { |  | ||||||
|                 clearTimeout(iconFlashTimeout.current); |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             if (response.data.errors.length > 0) { |  | ||||||
|                 setRecentlySaved(false); |  | ||||||
|  |  | ||||||
|                 for (const error of response.data.errors) { |  | ||||||
|                     flash({ message: error, type: "error" }); |  | ||||||
|                 } |  | ||||||
|             } else { |  | ||||||
|                 setRecentlySaved(true); |  | ||||||
|  |  | ||||||
|                 iconFlashTimeout.current = setTimeout(() => { |  | ||||||
|                     setRecentlySaved(false); |  | ||||||
|                 }, ICON_FLASH_TIME); |  | ||||||
|  |  | ||||||
|                 setReminder(response.data.reminder); |  | ||||||
|             } |  | ||||||
|         }, |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     return ( |  | ||||||
|         <div class="button-row-edit"> |  | ||||||
|             <button |  | ||||||
|                 class="button is-success save-btn" |  | ||||||
|                 onClick={() => { |  | ||||||
|                     mutation.mutate(reminder); |  | ||||||
|                 }} |  | ||||||
|                 disabled={mutation.isLoading} |  | ||||||
|             > |  | ||||||
|                 <span>Save</span>{" "} |  | ||||||
|                 {mutation.isLoading ? ( |  | ||||||
|                     <span class="icon"> |  | ||||||
|                         <i class="fas fa-spin fa-cog"></i> |  | ||||||
|                     </span> |  | ||||||
|                 ) : recentlySaved ? ( |  | ||||||
|                     <span class="icon"> |  | ||||||
|                         <i class="fas fa-check"></i> |  | ||||||
|                     </span> |  | ||||||
|                 ) : ( |  | ||||||
|                     <span class="icon"> |  | ||||||
|                         <i class="fas fa-save"></i> |  | ||||||
|                     </span> |  | ||||||
|                 )} |  | ||||||
|             </button> |  | ||||||
|             <button |  | ||||||
|                 class="button is-warning" |  | ||||||
|                 onClick={() => { |  | ||||||
|                     mutation.mutate({ |  | ||||||
|                         ...reminder, |  | ||||||
|                         enabled: !reminder.enabled, |  | ||||||
|                     }); |  | ||||||
|                 }} |  | ||||||
|                 disabled={mutation.isLoading} |  | ||||||
|             > |  | ||||||
|                 {reminder.enabled ? "Disable" : "Enable"} |  | ||||||
|             </button> |  | ||||||
|             <DeleteButton /> |  | ||||||
|         </div> |  | ||||||
|     ); |  | ||||||
| }; |  | ||||||
| @@ -1,32 +0,0 @@ | |||||||
| import { useQuery } from "react-query"; |  | ||||||
| import { fetchGuildChannels } from "../../api"; |  | ||||||
| import { useGuild } from "../App/useGuild"; |  | ||||||
|  |  | ||||||
| export const ChannelSelector = ({ channel, setChannel }) => { |  | ||||||
|     const guild = useGuild(); |  | ||||||
|     const { isSuccess, data } = useQuery(fetchGuildChannels(guild)); |  | ||||||
|  |  | ||||||
|     return ( |  | ||||||
|         <div class="control has-icons-left"> |  | ||||||
|             <div class="select"> |  | ||||||
|                 <select |  | ||||||
|                     name="channel" |  | ||||||
|                     class="channel-selector" |  | ||||||
|                     onInput={(ev) => { |  | ||||||
|                         setChannel(ev.currentTarget.value); |  | ||||||
|                     }} |  | ||||||
|                 > |  | ||||||
|                     {isSuccess && |  | ||||||
|                         data.map((c) => ( |  | ||||||
|                             <option value={c.id} selected={c.id === channel}> |  | ||||||
|                                 {c.name} |  | ||||||
|                             </option> |  | ||||||
|                         ))} |  | ||||||
|                 </select> |  | ||||||
|             </div> |  | ||||||
|             <div class="icon is-small is-left"> |  | ||||||
|                 <i class="fas fa-hashtag"></i> |  | ||||||
|             </div> |  | ||||||
|         </div> |  | ||||||
|     ); |  | ||||||
| }; |  | ||||||
| @@ -1,32 +0,0 @@ | |||||||
| import { useReminder } from "./ReminderContext"; |  | ||||||
| import { useRef } from "preact/hooks"; |  | ||||||
| import { Mentions } from "../App/Mentions"; |  | ||||||
| import { useGuild } from "../App/useGuild"; |  | ||||||
|  |  | ||||||
| export const Content = () => { |  | ||||||
|     const guild = useGuild(); |  | ||||||
|     const [reminder, setReminder] = useReminder(); |  | ||||||
|     const input = useRef(null); |  | ||||||
|  |  | ||||||
|     return ( |  | ||||||
|         <> |  | ||||||
|             {guild && <Mentions input={input} />} |  | ||||||
|             <label class="is-sr-only">Content</label> |  | ||||||
|             <textarea |  | ||||||
|                 class="message-input autoresize discord-content" |  | ||||||
|                 placeholder="Content..." |  | ||||||
|                 maxlength={2000} |  | ||||||
|                 name="content" |  | ||||||
|                 rows={1} |  | ||||||
|                 ref={input} |  | ||||||
|                 value={reminder.content} |  | ||||||
|                 onInput={(ev) => { |  | ||||||
|                     setReminder((reminder) => ({ |  | ||||||
|                         ...reminder, |  | ||||||
|                         content: ev.currentTarget.value, |  | ||||||
|                     })); |  | ||||||
|                 }} |  | ||||||
|             ></textarea> |  | ||||||
|         </> |  | ||||||
|     ); |  | ||||||
| }; |  | ||||||
| @@ -1,108 +0,0 @@ | |||||||
| import { useState } from "preact/hooks"; |  | ||||||
| import { fetchGuildChannels, Reminder } from "../../api"; |  | ||||||
| import { DateTime } from "luxon"; |  | ||||||
| import { CreateButtonRow } from "./ButtonRow/CreateButtonRow"; |  | ||||||
| import { TopBar } from "./TopBar"; |  | ||||||
| import { Message } from "./Message"; |  | ||||||
| import { Settings } from "./Settings"; |  | ||||||
| import { ReminderContext } from "./ReminderContext"; |  | ||||||
| import { useQuery } from "react-query"; |  | ||||||
| import "./styles.scss"; |  | ||||||
| import { useGuild } from "../App/useGuild"; |  | ||||||
| import { DEFAULT_COLOR } from "./Embed"; |  | ||||||
|  |  | ||||||
| function defaultReminder(): Reminder { |  | ||||||
|     return { |  | ||||||
|         attachment: null, |  | ||||||
|         attachment_name: null, |  | ||||||
|         avatar: null, |  | ||||||
|         channel: null, |  | ||||||
|         content: "", |  | ||||||
|         embed_author: "", |  | ||||||
|         embed_author_url: null, |  | ||||||
|         embed_color: DEFAULT_COLOR, |  | ||||||
|         embed_description: "", |  | ||||||
|         embed_fields: [], |  | ||||||
|         embed_footer: "", |  | ||||||
|         embed_footer_url: null, |  | ||||||
|         embed_image_url: null, |  | ||||||
|         embed_thumbnail_url: null, |  | ||||||
|         embed_title: "", |  | ||||||
|         enabled: true, |  | ||||||
|         expires: null, |  | ||||||
|         interval_days: null, |  | ||||||
|         interval_months: null, |  | ||||||
|         interval_seconds: null, |  | ||||||
|         name: "", |  | ||||||
|         restartable: false, |  | ||||||
|         tts: false, |  | ||||||
|         uid: "", |  | ||||||
|         username: "", |  | ||||||
|         utc_time: DateTime.now().setZone("UTC").toFormat("yyyy-LL-dd'T'HH:mm:ss"), |  | ||||||
|     }; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| export const CreateReminder = () => { |  | ||||||
|     const guild = useGuild(); |  | ||||||
|  |  | ||||||
|     if (guild) { |  | ||||||
|         return <_Guild guild={guild} />; |  | ||||||
|     } else { |  | ||||||
|         return <_User />; |  | ||||||
|     } |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| const _User = () => { |  | ||||||
|     const [reminder, setReminder] = useState(defaultReminder()); |  | ||||||
|     const [collapsed, setCollapsed] = useState(false); |  | ||||||
|  |  | ||||||
|     return ( |  | ||||||
|         <ReminderContext.Provider value={[reminder, setReminder]}> |  | ||||||
|             <div class={collapsed ? "reminderContent is-collapsed" : "reminderContent"}> |  | ||||||
|                 <TopBar |  | ||||||
|                     isCreating={true} |  | ||||||
|                     toggleCollapsed={() => { |  | ||||||
|                         setCollapsed(!collapsed); |  | ||||||
|                     }} |  | ||||||
|                 /> |  | ||||||
|                 <div class="columns reminder-settings"> |  | ||||||
|                     <Message /> |  | ||||||
|                     <Settings /> |  | ||||||
|                 </div> |  | ||||||
|                 <CreateButtonRow /> |  | ||||||
|             </div> |  | ||||||
|         </ReminderContext.Provider> |  | ||||||
|     ); |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| const _Guild = ({ guild }) => { |  | ||||||
|     const [reminder, setReminder] = useState(defaultReminder()); |  | ||||||
|     const [collapsed, setCollapsed] = useState(false); |  | ||||||
|  |  | ||||||
|     const { isSuccess, data: guildChannels } = useQuery(fetchGuildChannels(guild)); |  | ||||||
|  |  | ||||||
|     if (isSuccess && reminder.channel === null) { |  | ||||||
|         setReminder((reminder) => ({ |  | ||||||
|             ...reminder, |  | ||||||
|             channel: reminder.channel || guildChannels[0].id, |  | ||||||
|         })); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     return ( |  | ||||||
|         <ReminderContext.Provider value={[reminder, setReminder]}> |  | ||||||
|             <div class={collapsed ? "reminderContent is-collapsed" : "reminderContent"}> |  | ||||||
|                 <TopBar |  | ||||||
|                     isCreating={true} |  | ||||||
|                     toggleCollapsed={() => { |  | ||||||
|                         setCollapsed(!collapsed); |  | ||||||
|                     }} |  | ||||||
|                 /> |  | ||||||
|                 <div class="columns reminder-settings"> |  | ||||||
|                     <Message /> |  | ||||||
|                     <Settings /> |  | ||||||
|                 </div> |  | ||||||
|                 <CreateButtonRow /> |  | ||||||
|             </div> |  | ||||||
|         </ReminderContext.Provider> |  | ||||||
|     ); |  | ||||||
| }; |  | ||||||
| @@ -1,50 +0,0 @@ | |||||||
| import { Reminder } from "../../api"; |  | ||||||
| import { useEffect, useState } from "preact/hooks"; |  | ||||||
| import { EditButtonRow } from "./ButtonRow/EditButtonRow"; |  | ||||||
| import { Message } from "./Message"; |  | ||||||
| import { Settings } from "./Settings"; |  | ||||||
| import { ReminderContext } from "./ReminderContext"; |  | ||||||
| import { TopBar } from "./TopBar"; |  | ||||||
| import "./styles.scss"; |  | ||||||
|  |  | ||||||
| type Props = { |  | ||||||
|     reminder: Reminder; |  | ||||||
|     globalCollapse: boolean; |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| export const EditReminder = ({ reminder: initialReminder, globalCollapse }: Props) => { |  | ||||||
|     const [propReminder, setPropReminder] = useState(initialReminder); |  | ||||||
|     const [reminder, setReminder] = useState(initialReminder); |  | ||||||
|     const [collapsed, setCollapsed] = useState(false); |  | ||||||
|  |  | ||||||
|     useEffect(() => { |  | ||||||
|         setCollapsed(globalCollapse); |  | ||||||
|     }, [globalCollapse]); |  | ||||||
|  |  | ||||||
|     // Reminder updated from web response |  | ||||||
|     if (propReminder !== initialReminder) { |  | ||||||
|         setReminder(initialReminder); |  | ||||||
|         setPropReminder(initialReminder); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     return ( |  | ||||||
|         <ReminderContext.Provider value={[reminder, setReminder]} key={reminder.uid}> |  | ||||||
|             <div |  | ||||||
|                 class={collapsed ? "reminderContent is-collapsed" : "reminderContent"} |  | ||||||
|                 id={`reminder-${reminder.uid.slice(0, 12)}`} |  | ||||||
|             > |  | ||||||
|                 <TopBar |  | ||||||
|                     isCreating={false} |  | ||||||
|                     toggleCollapsed={() => { |  | ||||||
|                         setCollapsed(!collapsed); |  | ||||||
|                     }} |  | ||||||
|                 /> |  | ||||||
|                 <div class="columns reminder-settings"> |  | ||||||
|                     <Message /> |  | ||||||
|                     <Settings /> |  | ||||||
|                 </div> |  | ||||||
|                 <EditButtonRow /> |  | ||||||
|             </div> |  | ||||||
|         </ReminderContext.Provider> |  | ||||||
|     ); |  | ||||||
| }; |  | ||||||
| @@ -1,58 +0,0 @@ | |||||||
| import { ImagePicker } from "../ImagePicker"; |  | ||||||
| import { Reminder } from "../../../api"; |  | ||||||
| import { Mentions } from "../../App/Mentions"; |  | ||||||
| import { useRef } from "preact/hooks"; |  | ||||||
| import { useGuild } from "../../App/useGuild"; |  | ||||||
|  |  | ||||||
| type Props = { |  | ||||||
|     name: string; |  | ||||||
|     icon: string; |  | ||||||
|     setReminder: (r: (reminder: Reminder) => Reminder) => void; |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| export const Author = ({ name, icon, setReminder }: Props) => { |  | ||||||
|     const guild = useGuild(); |  | ||||||
|     const input = useRef(null); |  | ||||||
|  |  | ||||||
|     return ( |  | ||||||
|         <div class="embed-author-box"> |  | ||||||
|             <div class="a"> |  | ||||||
|                 <p class="image is-24x24 customizable"> |  | ||||||
|                     <ImagePicker |  | ||||||
|                         class="is-rounded embed_author_url" |  | ||||||
|                         url={icon} |  | ||||||
|                         alt="Image for embed author" |  | ||||||
|                         setImage={(url: string) => { |  | ||||||
|                             setReminder((reminder) => ({ |  | ||||||
|                                 ...reminder, |  | ||||||
|                                 embed_author_url: url, |  | ||||||
|                             })); |  | ||||||
|                         }} |  | ||||||
|                     ></ImagePicker> |  | ||||||
|                 </p> |  | ||||||
|             </div> |  | ||||||
|  |  | ||||||
|             <div class="b"> |  | ||||||
|                 {guild && <Mentions input={input} />} |  | ||||||
|                 <label class="is-sr-only" for="embedAuthor"> |  | ||||||
|                     Embed Author |  | ||||||
|                 </label> |  | ||||||
|                 <textarea |  | ||||||
|                     class="discord-embed-author message-input autoresize" |  | ||||||
|                     placeholder="Embed Author..." |  | ||||||
|                     rows={1} |  | ||||||
|                     ref={input} |  | ||||||
|                     maxlength={256} |  | ||||||
|                     name="embed_author" |  | ||||||
|                     value={name} |  | ||||||
|                     onInput={(ev) => { |  | ||||||
|                         setReminder((reminder) => ({ |  | ||||||
|                             ...reminder, |  | ||||||
|                             embed_author: ev.currentTarget.value, |  | ||||||
|                         })); |  | ||||||
|                     }} |  | ||||||
|                 ></textarea> |  | ||||||
|             </div> |  | ||||||
|         </div> |  | ||||||
|     ); |  | ||||||
| }; |  | ||||||