Compare commits
	
		
			294 Commits
		
	
	
		
			poise-2
			...
			218be2f0b1
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 218be2f0b1 | ||
|  | d7515f3611 | ||
|  | 6ae1096d79 | ||
|  | 1f0d7adae3 | ||
|  | fc96ae526f | ||
|  | 8881ef0f85 | ||
|  | 5e82a687f9 | ||
|  | de4ecf8dd6 | ||
|  | 064efd4386 | ||
|  | 65b8ba3b47 | ||
| 9d452ed8cb | |||
|  | 441419b92b | ||
|  | aecf2c15be | ||
|  | 79da56c794 | ||
|  | ef10902c1e | ||
|  | c277f85c2a | ||
|  | 035653c7fa | ||
|  | 6358bc3deb | ||
|  | 9f5066f982 | ||
|  | 1d06999e41 | ||
|  | 1cf707140c | ||
|  | e38c63f5ba | ||
|  | d52b8b26f2 | ||
| bb2128a7ed | |||
| 5e99a6f9de | |||
| 5406e6b8ec | |||
|  | 4ee0bc4e37 | ||
|  | b99bb7dcbf | ||
|  | 98f925dc84 | ||
|  | 24e316b12f | ||
|  | 4063334953 | ||
|  | e128b9848f | ||
|  | 9989ab3b35 | ||
|  | b951db3f55 | ||
|  | 884a47bf36 | ||
|  | b0f932445c | ||
|  | 2861cdda0b | ||
|  | 7ba8fcd6b7 | ||
|  | 850f0fad57 | ||
|  | a770a17ee7 | ||
|  | d15a66d9d9 | ||
|  | 30f011fcd5 | ||
|  | 15dbed2f0f | ||
|  | 18cac0345b | ||
|  | 334b1bc084 | ||
|  | ba3c76c25f | ||
|  | 67b6f30c62 | ||
|  | 8ae311190f | ||
|  | 016164affb | ||
|  | 2c0aeef700 | ||
|  | ecd75d6f55 | ||
|  | 4a80d42f86 | ||
|  | 075fde71df | ||
|  | 55136aecdc | ||
|  | 63fc2cdcbc | ||
|  | 3190738fc5 | ||
|  | 8f4810b532 | ||
|  | a5e6c41fa5 | ||
|  | 5f0aa0f834 | ||
|  | dbe8e8e358 | ||
|  | 85a114e55c | ||
|  | 329492b244 | ||
|  | 66135ecd08 | ||
|  | 382c2a5a1e | ||
|  | b91245a3f7 | ||
|  | 6f0bdf9852 | ||
|  | dcee9e0d2a | ||
|  | 8e6e1a18b7 | ||
|  | 72af0532fa | ||
|  | e83b643d86 | ||
|  | 0e0ab053f3 | ||
|  | 8c2296b9c8 | ||
|  | 1c6103142f | ||
|  | 328127c55e | ||
|  | b0e37b56c0 | ||
|  | 45f5b6261a | ||
|  | 5f6326179c | ||
|  | 6254f91841 | ||
|  | 60b90a61d4 | ||
|  | 90f05758d0 | ||
|  | 74b7b5d711 | ||
|  | 90550dc2c7 | ||
|  | 79e6498245 | ||
|  | a8ef3d03f9 | ||
|  | 53e13844f9 | ||
|  | dd7e681285 | ||
|  | 6c20bf2a0f | ||
|  | 15aa9ccffd | ||
|  | 525471bcad | ||
|  | 86d53b63b6 | ||
|  | d8f266852a | ||
|  | 76a286076b | ||
|  | 5e39e16060 | ||
|  | c1305cfb36 | ||
|  | 4823754955 | ||
|  | eb92eacb90 | ||
|  | d0833b7bca | ||
|  | b81c3c80c1 | ||
|  | 2f6d035efe | ||
|  | 96012ce43c | ||
|  | fa7ec8731b | ||
|  | def43bfa78 | ||
|  | e4e9af2bb4 | ||
|  | cce0de7c75 | ||
| e7803b98e8 | |||
|  | 7aae246388 | ||
| a2d442bc54 | |||
| 59982df827 | |||
|  | 7a6372ed02 | ||
|  | 14a54471f7 | ||
|  | 5d3b77f1cd | ||
|  | 1d64c8bb79 | ||
| 8ba0f02b98 | |||
| d36438c6ce | |||
| e0c60e2ce3 | |||
|  | e7160215b0 | ||
|  | 6eaa6f0f28 | ||
|  | 9db0fa2513 | ||
|  | ca13fd4fa7 | ||
|  | 55acc8fd16 | ||
|  | 145711fa5d | ||
|  | 5524215786 | ||
|  | e8bd05893f | ||
|  | e3d3418f99 | ||
|  | 2681280a39 | ||
|  | 00579428a1 | ||
|  | b8ef999710 | ||
|  | e8f84e281a | ||
|  | 8ddff698e5 | ||
|  | 541633270c | ||
|  | 25286da5e0 | ||
|  | 4bad1324b9 | ||
|  | bd1462a00c | ||
|  | 56ffc43616 | ||
|  | 52cf642455 | ||
|  | 0bf578357a | ||
|  | 6e9eccb62e | ||
|  | 6ea28284ce | ||
|  | a6525f3052 | ||
|  | 348639270d | ||
|  | 37177c2431 | ||
|  | 8587bed703 | ||
|  | 6c9af1ae8e | ||
|  | 7695b7a476 | ||
| 651da7b28e | |||
| eb086146bf | |||
| 4ebd705e5e | |||
| 5a85f1d83a | |||
| 68ba25886a | |||
|  | e25bf6b828 | ||
|  | 5a386daa9d | ||
|  | 0d4a02fb1e | ||
|  | e135a74a9b | ||
|  | 77f17c8dc2 | ||
|  | 6a94f990cf | ||
|  | 3aa5bd37aa | ||
|  | fa83fed1af | ||
|  | 666cb7fa2f | ||
|  | a5678e15dc | ||
|  | 9405cfcee9 | ||
|  | cb25d02cdf | ||
|  | bfe651a125 | ||
|  | dc5e52d9ce | ||
|  | 229ada83e1 | ||
|  | 13171d6744 | ||
|  | 2ad941c94c | ||
|  | 924d31e978 | ||
|  | f9a1b23212 | ||
|  | ae5795a7ea | ||
|  | ee36c38eda | ||
|  | eca7df3d9f | ||
|  | 902b7e1b4a | ||
|  | db1a53a797 | ||
|  | 3605d71b73 | ||
|  | ea2cea573e | ||
|  | d5fa8036e8 | ||
|  | b8707bbc9a | ||
|  | 99eea16f62 | ||
|  | 88737302f3 | ||
|  | 213e3a5100 | ||
|  | 8fa1402ecc | ||
|  | e63996bb61 | ||
|  | 9ede879630 | ||
|  | 88e9826a62 | ||
|  | 5d655c7e6d | ||
|  | 51c9d8a7ae | ||
|  | 90df265114 | ||
|  | e65429aa9c | ||
|  | 8d2232f0da | ||
|  | a58b9866ea | ||
|  | b1f25be5d7 | ||
|  | f0f9787326 | ||
|  | 302f5835e6 | ||
|  | 58c778632e | ||
|  | 5671fd462b | ||
|  | 5ac9733f15 | ||
|  | 01dc0334fd | ||
|  | 4a17aac15c | ||
|  | 8ce4fc9c6d | ||
|  | b4f07cfc1c | ||
|  | 8799089b2d | ||
|  | 88c4830209 | ||
|  | 4dd3df5cc2 | ||
|  | 369a325a46 | ||
|  | 1a1a0fdefb | ||
|  | dda8bd3e10 | ||
|  | edbfc92cb9 | ||
|  | 6de11f09db | ||
|  | 284bfcd9ad | ||
|  | 3d627b5bf0 | ||
|  | c3c0dbbbae | ||
|  | 64dd81e941 | ||
|  | 799298ca34 | ||
|  | fa542bb24f | ||
|  | e025d945cf | ||
|  | bb1c61d0b9 | ||
|  | 1519474f93 | ||
|  | 9d8622f418 | ||
|  | a66db37b33 | ||
|  | c8c1a171d4 | ||
|  | 88cfb829e3 | ||
|  | 16be7a328e | ||
|  | 04babf7930 | ||
|  | 96bc09e8b5 | ||
|  | 976fb91ecc | ||
|  | 1305b6e64e | ||
|  | cdfe44d958 | ||
|  | c824a36832 | ||
|  | c4bd2c1d18 | ||
|  | 561555ab7e | ||
|  | 115fbd44cb | ||
|  | aa931328b0 | ||
| 4b42966284 | |||
| 523ab7f03a | |||
| 6e831c8253 | |||
|  | 4416e5d175 | ||
|  | 734a39a001 | ||
|  | 98191d29ee | ||
|  | 1c4c4a8b31 | ||
|  | d496c81003 | ||
|  | 094d210f64 | ||
|  | 314c72e132 | ||
|  | 4e0163f2cb | ||
|  | e5b8c418af | ||
|  | 3ef8584189 | ||
|  | df2ad09c86 | ||
|  | d70fb24eb1 | ||
|  | 3150c7267d | ||
|  | 6e65e4ff3d | ||
|  | 67a4db2e9a | ||
|  | e9bcb1973f | ||
|  | 9b87fd4258 | ||
|  | a49a849917 | ||
|  | aa74a7f9a3 | ||
|  | 08e4c6cb57 | ||
|  | 6e087bd2dd | ||
| e9792e6322 | |||
| 130504b964 | |||
| 2a8117d0c1 | |||
| 94bfd39085 | |||
| 40cd5f8a36 | |||
| 133b00a2ce | |||
| 57336f5c81 | |||
| b62d24c024 | |||
| 8f8235a86e | |||
| c8f646a8fa | |||
| ecaa382a1e | |||
| 8991198fd3 | |||
|  | f20b95a482 | ||
|  | 8dd7dc6409 | ||
|  | c799d10727 | ||
|  | ceb6fb7b12 | ||
|  | 6708abdb0f | ||
|  | a38f6024c1 | ||
|  | 7d8748e3ef | ||
|  | bb3386c4e8 | ||
|  | 25b84880a5 | ||
|  | 7b6e967a5d | ||
|  | 2781f2923e | ||
|  | 03f08f0a18 | ||
|  | 79c86d43f2 | ||
|  | e19af54caf | ||
|  | f4213c6a83 | ||
|  | f56db14720 | ||
|  | 6f7d0f67b3 | ||
|  | bfc2d71ca0 | ||
|  | 8eb46f1f23 | ||
|  | c4087bf569 | ||
|  | f25cfed8d7 | ||
|  | d2a8bd1982 | ||
|  | 437ee6b446 | ||
|  | 7d43aa5918 | ||
|  | 8bad95510d | ||
|  | d7a0b727fb | 
							
								
								
									
										31
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -1,7 +1,30 @@ | |||||||
| /target | target | ||||||
| .env | .env | ||||||
| /venv | /venv | ||||||
| .cargo | .cargo | ||||||
| assets | .idea | ||||||
| out.json | web/static/index.html | ||||||
| /.idea | web/static/assets | ||||||
|  | # Logs | ||||||
|  | logs | ||||||
|  | *.log | ||||||
|  | npm-debug.log* | ||||||
|  | yarn-debug.log* | ||||||
|  | yarn-error.log* | ||||||
|  | pnpm-debug.log* | ||||||
|  | lerna-debug.log* | ||||||
|  |  | ||||||
|  | node_modules | ||||||
|  | dist | ||||||
|  | dist-ssr | ||||||
|  | *.local | ||||||
|  |  | ||||||
|  | # Editor directories and files | ||||||
|  | .vscode/* | ||||||
|  | !.vscode/extensions.json | ||||||
|  | .DS_Store | ||||||
|  | *.suo | ||||||
|  | *.ntvs* | ||||||
|  | *.njsproj | ||||||
|  | *.sln | ||||||
|  | *.sw? | ||||||
|   | |||||||
							
								
								
									
										3411
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
							
								
								
									
										73
									
								
								Cargo.toml
									
									
									
									
									
								
							
							
						
						| @@ -1,32 +1,71 @@ | |||||||
| [package] | [package] | ||||||
| name = "reminder_rs" | name = "reminder-rs" | ||||||
| version = "1.6.0-beta3" | version = "1.7.23" | ||||||
| authors = ["jellywx <judesouthworth@pm.me>"] | authors = ["Jude Southworth <judesouthworth@pm.me>"] | ||||||
| edition = "2018" | edition = "2021" | ||||||
|  | license = "AGPL-3.0 only" | ||||||
|  | description = "Reminder Bot for Discord, now in Rust" | ||||||
|  |  | ||||||
| [dependencies] | [dependencies] | ||||||
| poise = "0.2" | poise = "0.6.1" | ||||||
| dotenv = "0.15" | dotenv = "0.15" | ||||||
| tokio = { version = "1", features = ["process", "full"] } | tokio = { version = "1", features = ["process", "full"] } | ||||||
| reqwest = "0.11" | reqwest = { version = "0.12", features = ["json"] } | ||||||
| regex = "1.4" | regex = "1.10" | ||||||
| log = "0.4" | log = "0.4" | ||||||
| env_logger = "0.8" | env_logger = "0.11" | ||||||
| chrono = "0.4" | chrono = "0.4" | ||||||
| chrono-tz = { version = "0.5", features = ["serde"] } | chrono-tz = { version = "0.9", features = ["serde"] } | ||||||
| lazy_static = "1.4" | lazy_static = "1.4" | ||||||
| num-integer = "0.1" | num-integer = "0.1" | ||||||
| serde = "1.0" | serde = "1.0" | ||||||
| serde_json = "1.0" | serde_json = "1.0" | ||||||
| serde_repr = "0.1" | serde_repr = "0.1" | ||||||
| rmp-serde = "0.15" | rmp-serde = "1.1" | ||||||
| rand = "0.7" | rand = "0.8" | ||||||
| levenshtein = "1.0" | levenshtein = "1.0" | ||||||
| sqlx = { version = "0.5", features = ["runtime-tokio-rustls", "macros", "mysql", "bigdecimal", "chrono"]} | 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.postman] | [dependencies.extract_derive] | ||||||
| path = "postman" | path = "extract_derive" | ||||||
|  |  | ||||||
| [dependencies.reminder_web] | [dependencies.recordable_derive] | ||||||
| path = "web" | path = "recordable_derive" | ||||||
|  |  | ||||||
|  | [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 | ||||||
|   | |||||||
							
								
								
									
										9
									
								
								Containerfile
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,9 @@ | |||||||
|  | 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 | ||||||
							
								
								
									
										43
									
								
								README.md
									
									
									
									
									
								
							
							
						
						| @@ -2,23 +2,41 @@ | |||||||
| Reminder Bot for Discord. | Reminder Bot for Discord. | ||||||
|  |  | ||||||
| ## How do I use it? | ## How do I use it? | ||||||
| We offer a hosted version of the bot. You can invite it with: **https://invite.reminder-bot.com**. The catch is that repeating  | I offer a hosted version of the bot. You can invite it with: **https://invite.reminder-bot.com**. The catch is that repeating  | ||||||
| reminders are paid on the hosted version of the bot. Keep reading if you want to host it yourself. | reminders are paid on the hosted version of the bot. Keep reading if you want to host it yourself. | ||||||
|  |  | ||||||
| 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) | ||||||
|  |  | ||||||
| ### Compiling | ### Build APT package | ||||||
| Reminder Bot can 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. |  | ||||||
|  |  | ||||||
| #### Compilation environment variables | Recommended method. | ||||||
| These environment variables must be provided when compiling the bot |  | ||||||
|  | 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. | ||||||
|  |  | ||||||
|  | 1. Install container software: `sudo apt install podman`. | ||||||
|  | 2. Install database server: `sudo apt install mysql-server-8.0`. Create a database called `reminders` | ||||||
|  | 3. Install SQLx CLI: `cargo install sqlx-cli` | ||||||
|  | 4. From the source code directory, execute `sqlx migrate run` | ||||||
|  | 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`  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ### Compiling for other target | ||||||
|  |  | ||||||
|  | 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`) |    * `DATABASE_URL` - the URL of your MySQL database (`mysql://user[:password]@domain/database`) | ||||||
| * `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** | 8. Build: `cargo build --release` | ||||||
|  |  | ||||||
| ### 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 |  | ||||||
|  |  | ||||||
| ### Environment Variables | ### 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__ | ||||||
| @@ -30,10 +48,5 @@ __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 `venv/bin/python3`. Can be changed if your Python executable is located somewhere else | * `PYTHON_LOCATION` - default `/usr/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 = 5000 | port = 18920 | ||||||
| template_dir = "web/templates" | template_dir = "templates" | ||||||
| limits = { json = "10MiB" } | limits = { json = "10MiB" } | ||||||
|  |  | ||||||
| [debug] | [debug] | ||||||
| secret_key = "tR8krio5FXTnnyIZNiJDXPondz0kI1v6X6BXZcBGIRY=" | secret_key = "tR8krio5FXTnnyIZNiJDXPondz0kI1v6X6BXZcBGIRY=" | ||||||
|  |  | ||||||
| [debug.tls] | [debug.tls] | ||||||
| certs = "web/private/rsa_sha256_cert.pem" | certs = "private/rsa_sha256_cert.pem" | ||||||
| key = "web/private/rsa_sha256_key.pem" | key = "private/rsa_sha256_key.pem" | ||||||
|  |  | ||||||
| [rsa_sha256.tls] | [debug.rsa_sha256.tls] | ||||||
| certs = "web/private/rsa_sha256_cert.pem" | certs = "private/rsa_sha256_cert.pem" | ||||||
| key = "web/private/rsa_sha256_key.pem" | key = "private/rsa_sha256_key.pem" | ||||||
|  |  | ||||||
| [ecdsa_nistp256_sha256.tls] | [debug.ecdsa_nistp256_sha256.tls] | ||||||
| certs = "web/private/ecdsa_nistp256_sha256_cert.pem" | certs = "private/ecdsa_nistp256_sha256_cert.pem" | ||||||
| key = "web/private/ecdsa_nistp256_sha256_key_pkcs8.pem" | key = "private/ecdsa_nistp256_sha256_key_pkcs8.pem" | ||||||
|  |  | ||||||
| [ecdsa_nistp384_sha384.tls] | [debug.ecdsa_nistp384_sha384.tls] | ||||||
| certs = "web/private/ecdsa_nistp384_sha384_cert.pem" | certs = "private/ecdsa_nistp384_sha384_cert.pem" | ||||||
| key = "web/private/ecdsa_nistp384_sha384_key_pkcs8.pem" | key = "private/ecdsa_nistp384_sha384_key_pkcs8.pem" | ||||||
|  |  | ||||||
| [ed25519.tls] | [debug.ed25519.tls] | ||||||
| certs = "web/private/ed25519_cert.pem" | certs = "private/ed25519_cert.pem" | ||||||
| key = "eb/private/ed25519_key.pem" | key = "private/ed25519_key.pem" | ||||||
|   | |||||||
							
								
								
									
										
											BIN
										
									
								
								assets/webhook.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 21 KiB | 
							
								
								
									
										13
									
								
								build.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,13 @@ | |||||||
|  | use std::{path::Path, process::Command}; | ||||||
|  |  | ||||||
|  | fn main() { | ||||||
|  |     println!("cargo:rerun-if-changed=migrations"); | ||||||
|  |     println!("cargo:rerun-if-changed=reminder-dashboard"); | ||||||
|  |  | ||||||
|  |     Command::new("npm") | ||||||
|  |         .arg("run") | ||||||
|  |         .arg("build") | ||||||
|  |         .current_dir(Path::new("reminder-dashboard")) | ||||||
|  |         .spawn() | ||||||
|  |         .expect("Failed to build NPM"); | ||||||
|  | } | ||||||
							
								
								
									
										8
									
								
								conf/Rocket.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,8 @@ | |||||||
|  | [default] | ||||||
|  | address = "127.0.0.1" | ||||||
|  | port = 18920 | ||||||
|  | template_dir = "/lib/reminder-rs/templates" | ||||||
|  | limits = { json = "10MiB" } | ||||||
|  |  | ||||||
|  | [release] | ||||||
|  | # secret_key = "" | ||||||
							
								
								
									
										19
									
								
								conf/default.env
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,19 @@ | |||||||
|  | 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
									
								
								cron.d/reminder_health
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1 @@ | |||||||
|  | */10 * * * * reminder /lib/reminder-rs/healthcheck | ||||||
							
								
								
									
										9
									
								
								debian/postinst
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,9 @@ | |||||||
|  | #!/bin/bash | ||||||
|  |  | ||||||
|  | set -e | ||||||
|  |  | ||||||
|  | id -u reminder &>/dev/null || useradd -r -M reminder | ||||||
|  |  | ||||||
|  | chown -R reminder /etc/reminder-rs | ||||||
|  |  | ||||||
|  | #DEBHELPER# | ||||||
							
								
								
									
										7
									
								
								debian/postrm
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,7 @@ | |||||||
|  | #!/bin/bash | ||||||
|  |  | ||||||
|  | set -e | ||||||
|  |  | ||||||
|  | id -u reminder &>/dev/null || userdel reminder | ||||||
|  |  | ||||||
|  | #DEBHELPER# | ||||||
							
								
								
									
										46
									
								
								extract_derive/Cargo.lock
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,46 @@ | |||||||
|  | # This file is automatically @generated by Cargo. | ||||||
|  | # It is not intended for manual editing. | ||||||
|  | version = 3 | ||||||
|  |  | ||||||
|  | [[package]] | ||||||
|  | name = "extract_macro" | ||||||
|  | version = "0.1.0" | ||||||
|  | dependencies = [ | ||||||
|  |  "quote", | ||||||
|  |  "syn", | ||||||
|  | ] | ||||||
|  |  | ||||||
|  | [[package]] | ||||||
|  | name = "proc-macro2" | ||||||
|  | version = "1.0.78" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" | ||||||
|  | dependencies = [ | ||||||
|  |  "unicode-ident", | ||||||
|  | ] | ||||||
|  |  | ||||||
|  | [[package]] | ||||||
|  | name = "quote" | ||||||
|  | version = "1.0.35" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" | ||||||
|  | dependencies = [ | ||||||
|  |  "proc-macro2", | ||||||
|  | ] | ||||||
|  |  | ||||||
|  | [[package]] | ||||||
|  | name = "syn" | ||||||
|  | version = "2.0.49" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "915aea9e586f80826ee59f8453c1101f9d1c4b3964cd2460185ee8e299ada496" | ||||||
|  | dependencies = [ | ||||||
|  |  "proc-macro2", | ||||||
|  |  "quote", | ||||||
|  |  "unicode-ident", | ||||||
|  | ] | ||||||
|  |  | ||||||
|  | [[package]] | ||||||
|  | name = "unicode-ident" | ||||||
|  | version = "1.0.12" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" | ||||||
							
								
								
									
										11
									
								
								extract_derive/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,11 @@ | |||||||
|  | [package] | ||||||
|  | name = "extract_derive" | ||||||
|  | version = "0.1.0" | ||||||
|  | edition = "2021" | ||||||
|  |  | ||||||
|  | [lib] | ||||||
|  | proc-macro = true | ||||||
|  |  | ||||||
|  | [dependencies] | ||||||
|  | quote = "1.0.35" | ||||||
|  | syn = { version = "2.0.49", features = ["full"] } | ||||||
							
								
								
									
										53
									
								
								extract_derive/src/lib.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,53 @@ | |||||||
|  | use proc_macro::TokenStream; | ||||||
|  | use syn::{spanned::Spanned, Data, Fields}; | ||||||
|  |  | ||||||
|  | #[proc_macro_derive(Extract)] | ||||||
|  | pub fn extract(input: TokenStream) -> TokenStream { | ||||||
|  |     let ast = syn::parse_macro_input!(input); | ||||||
|  |  | ||||||
|  |     impl_extract(&ast) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fn impl_extract(ast: &syn::DeriveInput) -> TokenStream { | ||||||
|  |     let name = &ast.ident; | ||||||
|  |  | ||||||
|  |     match &ast.data { | ||||||
|  |         // Dispatch over struct: extract args directly from context | ||||||
|  |         Data::Struct(st) => match &st.fields { | ||||||
|  |             Fields::Named(fields) => { | ||||||
|  |                 let extracted = fields.named.iter().map(|field| { | ||||||
|  |                     let ident = &field.ident; | ||||||
|  |                     let ty = &field.ty; | ||||||
|  |  | ||||||
|  |                     quote::quote_spanned! {field.span()=> | ||||||
|  |                         #ident : crate::utils::extract_arg!(ctx, #ident, #ty) | ||||||
|  |                     } | ||||||
|  |                 }); | ||||||
|  |  | ||||||
|  |                 TokenStream::from(quote::quote! { | ||||||
|  |                     impl Extract for #name { | ||||||
|  |                         fn extract(ctx: crate::ApplicationContext) -> Self { | ||||||
|  |                             Self { | ||||||
|  |                                 #(#extracted,)* | ||||||
|  |                             } | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 }) | ||||||
|  |             } | ||||||
|  |             Fields::Unit => TokenStream::from(quote::quote! { | ||||||
|  |                 impl Extract for #name { | ||||||
|  |                     fn extract(ctx: crate::ApplicationContext) -> Self { | ||||||
|  |                         Self {} | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             }), | ||||||
|  |             _ => { | ||||||
|  |                 panic!("Only named/unit structs can derive Extract"); | ||||||
|  |             } | ||||||
|  |         }, | ||||||
|  |  | ||||||
|  |         _ => { | ||||||
|  |             panic!("Only structs can derive Extract"); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -1,4 +0,0 @@ | |||||||
| USE reminders; |  | ||||||
|  |  | ||||||
| ALTER TABLE reminders.reminders RENAME COLUMN `interval` TO `interval_seconds`; |  | ||||||
| ALTER TABLE reminders.reminders ADD COLUMN `interval_months` INT UNSIGNED DEFAULT NULL; |  | ||||||
| @@ -1,10 +1,6 @@ | |||||||
| CREATE DATABASE IF NOT EXISTS reminders; |  | ||||||
| 
 |  | ||||||
| SET FOREIGN_KEY_CHECKS=0; | SET FOREIGN_KEY_CHECKS=0; | ||||||
| 
 | 
 | ||||||
| USE reminders; | CREATE TABLE guilds ( | ||||||
| 
 |  | ||||||
| CREATE TABLE reminders.guilds ( |  | ||||||
|     id INT UNSIGNED UNIQUE NOT NULL AUTO_INCREMENT, |     id INT UNSIGNED UNIQUE NOT NULL AUTO_INCREMENT, | ||||||
|     guild BIGINT UNSIGNED UNIQUE NOT NULL, |     guild BIGINT UNSIGNED UNIQUE NOT NULL, | ||||||
| 
 | 
 | ||||||
| @@ -18,10 +14,10 @@ CREATE TABLE reminders.guilds ( | |||||||
|     default_avatar VARCHAR(512) DEFAULT 'https://raw.githubusercontent.com/reminder-bot/logos/master/Remind_Me_Bot_Logo_PPic.jpg' 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), |     PRIMARY KEY (id), | ||||||
|     FOREIGN KEY (default_channel_id) REFERENCES reminders.channels(id) ON DELETE SET NULL |     FOREIGN KEY (default_channel_id) REFERENCES channels(id) ON DELETE SET NULL | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
| CREATE TABLE reminders.channels ( | CREATE TABLE channels ( | ||||||
|     id INT UNSIGNED UNIQUE NOT NULL AUTO_INCREMENT, |     id INT UNSIGNED UNIQUE NOT NULL AUTO_INCREMENT, | ||||||
|     channel BIGINT UNSIGNED UNIQUE NOT NULL, |     channel BIGINT UNSIGNED UNIQUE NOT NULL, | ||||||
| 
 | 
 | ||||||
| @@ -39,10 +35,10 @@ CREATE TABLE reminders.channels ( | |||||||
|     guild_id INT UNSIGNED, |     guild_id INT UNSIGNED, | ||||||
| 
 | 
 | ||||||
|     PRIMARY KEY (id), |     PRIMARY KEY (id), | ||||||
|     FOREIGN KEY (guild_id) REFERENCES reminders.guilds(id) ON DELETE CASCADE |     FOREIGN KEY (guild_id) REFERENCES guilds(id) ON DELETE CASCADE | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
| CREATE TABLE reminders.users ( | CREATE TABLE users ( | ||||||
|     id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL, |     id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL, | ||||||
|     user BIGINT UNSIGNED UNIQUE NOT NULL, |     user BIGINT UNSIGNED UNIQUE NOT NULL, | ||||||
| 
 | 
 | ||||||
| @@ -59,10 +55,10 @@ CREATE TABLE reminders.users ( | |||||||
|     patreon BOOLEAN NOT NULL DEFAULT 0, |     patreon BOOLEAN NOT NULL DEFAULT 0, | ||||||
| 
 | 
 | ||||||
|     PRIMARY KEY (id), |     PRIMARY KEY (id), | ||||||
|     FOREIGN KEY (dm_channel) REFERENCES reminders.channels(id) ON DELETE RESTRICT |     FOREIGN KEY (dm_channel) REFERENCES channels(id) ON DELETE RESTRICT | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
| CREATE TABLE reminders.roles ( | CREATE TABLE roles ( | ||||||
|     id INT UNSIGNED UNIQUE NOT NULL AUTO_INCREMENT, |     id INT UNSIGNED UNIQUE NOT NULL AUTO_INCREMENT, | ||||||
|     role BIGINT UNSIGNED UNIQUE NOT NULL, |     role BIGINT UNSIGNED UNIQUE NOT NULL, | ||||||
| 
 | 
 | ||||||
| @@ -71,10 +67,10 @@ CREATE TABLE reminders.roles ( | |||||||
|     guild_id INT UNSIGNED NOT NULL, |     guild_id INT UNSIGNED NOT NULL, | ||||||
| 
 | 
 | ||||||
|     PRIMARY KEY (id), |     PRIMARY KEY (id), | ||||||
|     FOREIGN KEY (guild_id) REFERENCES reminders.guilds(id) ON DELETE CASCADE |     FOREIGN KEY (guild_id) REFERENCES guilds(id) ON DELETE CASCADE | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
| CREATE TABLE reminders.embeds ( | CREATE TABLE embeds ( | ||||||
|     id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL, |     id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL, | ||||||
| 
 | 
 | ||||||
|     title VARCHAR(256) NOT NULL DEFAULT '', |     title VARCHAR(256) NOT NULL DEFAULT '', | ||||||
| @@ -91,7 +87,7 @@ CREATE TABLE reminders.embeds ( | |||||||
|     PRIMARY KEY (id) |     PRIMARY KEY (id) | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
| CREATE TABLE reminders.embed_fields ( | CREATE TABLE embed_fields ( | ||||||
|     id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL, |     id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL, | ||||||
| 
 | 
 | ||||||
|     title VARCHAR(256) NOT NULL DEFAULT '', |     title VARCHAR(256) NOT NULL DEFAULT '', | ||||||
| @@ -100,10 +96,10 @@ CREATE TABLE reminders.embed_fields ( | |||||||
|     embed_id INT UNSIGNED NOT NULL, |     embed_id INT UNSIGNED NOT NULL, | ||||||
| 
 | 
 | ||||||
|     PRIMARY KEY (id), |     PRIMARY KEY (id), | ||||||
|     FOREIGN KEY (embed_id) REFERENCES reminders.embeds(id) ON DELETE CASCADE |     FOREIGN KEY (embed_id) REFERENCES embeds(id) ON DELETE CASCADE | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
| CREATE TABLE reminders.messages ( | CREATE TABLE messages ( | ||||||
|     id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL, |     id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL, | ||||||
| 
 | 
 | ||||||
|     content VARCHAR(2048) NOT NULL DEFAULT '', |     content VARCHAR(2048) NOT NULL DEFAULT '', | ||||||
| @@ -114,10 +110,10 @@ CREATE TABLE reminders.messages ( | |||||||
|     attachment_name VARCHAR(260), |     attachment_name VARCHAR(260), | ||||||
| 
 | 
 | ||||||
|     PRIMARY KEY (id), |     PRIMARY KEY (id), | ||||||
|     FOREIGN KEY (embed_id) REFERENCES reminders.embeds(id) ON DELETE SET NULL |     FOREIGN KEY (embed_id) REFERENCES embeds(id) ON DELETE SET NULL | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
| CREATE TABLE reminders.reminders ( | CREATE TABLE reminders ( | ||||||
|     id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL, |     id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL, | ||||||
|     uid VARCHAR(64) UNIQUE NOT NULL, |     uid VARCHAR(64) UNIQUE NOT NULL, | ||||||
| 
 | 
 | ||||||
| @@ -140,20 +136,20 @@ CREATE TABLE reminders.reminders ( | |||||||
|     set_by INT UNSIGNED, |     set_by INT UNSIGNED, | ||||||
| 
 | 
 | ||||||
|     PRIMARY KEY (id), |     PRIMARY KEY (id), | ||||||
|     FOREIGN KEY (message_id) REFERENCES reminders.messages(id) ON DELETE RESTRICT, |     FOREIGN KEY (message_id) REFERENCES messages(id) ON DELETE RESTRICT, | ||||||
|     FOREIGN KEY (channel_id) REFERENCES reminders.channels(id) ON DELETE CASCADE, |     FOREIGN KEY (channel_id) REFERENCES channels(id) ON DELETE CASCADE, | ||||||
|     FOREIGN KEY (set_by) REFERENCES reminders.users(id) ON DELETE SET NULL |     FOREIGN KEY (set_by) REFERENCES users(id) ON DELETE SET NULL | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
| CREATE TRIGGER message_cleanup AFTER DELETE ON reminders.reminders | CREATE TRIGGER message_cleanup AFTER DELETE ON reminders | ||||||
| FOR EACH ROW | FOR EACH ROW | ||||||
|     DELETE FROM reminders.messages WHERE id = OLD.message_id; |     DELETE FROM messages WHERE id = OLD.message_id; | ||||||
| 
 | 
 | ||||||
| CREATE TRIGGER embed_cleanup AFTER DELETE ON reminders.messages | CREATE TRIGGER embed_cleanup AFTER DELETE ON messages | ||||||
| FOR EACH ROW | FOR EACH ROW | ||||||
|     DELETE FROM reminders.embeds WHERE id = OLD.embed_id; |     DELETE FROM embeds WHERE id = OLD.embed_id; | ||||||
| 
 | 
 | ||||||
| CREATE TABLE reminders.todos ( | CREATE TABLE todos ( | ||||||
|     id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL, |     id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL, | ||||||
|     user_id INT UNSIGNED, |     user_id INT UNSIGNED, | ||||||
|     guild_id INT UNSIGNED, |     guild_id INT UNSIGNED, | ||||||
| @@ -161,23 +157,23 @@ CREATE TABLE reminders.todos ( | |||||||
|     value VARCHAR(2000) NOT NULL, |     value VARCHAR(2000) NOT NULL, | ||||||
| 
 | 
 | ||||||
|     PRIMARY KEY (id), |     PRIMARY KEY (id), | ||||||
|     FOREIGN KEY (user_id) REFERENCES reminders.users(id) ON DELETE SET NULL, |     FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL, | ||||||
|     FOREIGN KEY (guild_id) REFERENCES reminders.guilds(id) ON DELETE CASCADE, |     FOREIGN KEY (guild_id) REFERENCES guilds(id) ON DELETE CASCADE, | ||||||
|     FOREIGN KEY (channel_id) REFERENCES reminders.channels(id) ON DELETE SET NULL |     FOREIGN KEY (channel_id) REFERENCES channels(id) ON DELETE SET NULL | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
| CREATE TABLE reminders.command_restrictions ( | CREATE TABLE command_restrictions ( | ||||||
|     id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL, |     id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL, | ||||||
| 
 | 
 | ||||||
|     role_id INT UNSIGNED NOT NULL, |     role_id INT UNSIGNED NOT NULL, | ||||||
|     command ENUM('todos', 'natural', 'remind', 'interval', 'timer', 'del', 'look', 'alias', 'countdown') NOT NULL, |     command ENUM('todos', 'natural', 'remind', 'interval', 'timer', 'del', 'look', 'alias', 'countdown') NOT NULL, | ||||||
| 
 | 
 | ||||||
|     PRIMARY KEY (id), |     PRIMARY KEY (id), | ||||||
|     FOREIGN KEY (role_id) REFERENCES reminders.roles(id) ON DELETE CASCADE, |     FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE, | ||||||
|     UNIQUE KEY (`role_id`, `command`) |     UNIQUE KEY (`role_id`, `command`) | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
| CREATE TABLE reminders.timers ( | CREATE TABLE timers ( | ||||||
|     id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL, |     id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL, | ||||||
|     start_time TIMESTAMP NOT NULL DEFAULT NOW(), |     start_time TIMESTAMP NOT NULL DEFAULT NOW(), | ||||||
|     name VARCHAR(32) NOT NULL, |     name VARCHAR(32) NOT NULL, | ||||||
| @@ -186,7 +182,7 @@ CREATE TABLE reminders.timers ( | |||||||
|     PRIMARY KEY (id) |     PRIMARY KEY (id) | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
| CREATE TABLE reminders.events ( | CREATE TABLE events ( | ||||||
|     id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL, |     id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL, | ||||||
|     `time` TIMESTAMP NOT NULL DEFAULT NOW(), |     `time` TIMESTAMP NOT NULL DEFAULT NOW(), | ||||||
| 
 | 
 | ||||||
| @@ -198,12 +194,12 @@ CREATE TABLE reminders.events ( | |||||||
|     reminder_id INT UNSIGNED, |     reminder_id INT UNSIGNED, | ||||||
| 
 | 
 | ||||||
|     PRIMARY KEY (id), |     PRIMARY KEY (id), | ||||||
|     FOREIGN KEY (guild_id) REFERENCES reminders.guilds(id) ON DELETE CASCADE, |     FOREIGN KEY (guild_id) REFERENCES guilds(id) ON DELETE CASCADE, | ||||||
|     FOREIGN KEY (user_id) REFERENCES reminders.users(id) ON DELETE SET NULL, |     FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL, | ||||||
|     FOREIGN KEY (reminder_id) REFERENCES reminders.reminders(id) ON DELETE SET NULL |     FOREIGN KEY (reminder_id) REFERENCES reminders(id) ON DELETE SET NULL | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
| CREATE TABLE reminders.command_aliases ( | CREATE TABLE command_aliases ( | ||||||
|     id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL, |     id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL, | ||||||
| 
 | 
 | ||||||
|     guild_id INT UNSIGNED NOT NULL, |     guild_id INT UNSIGNED NOT NULL, | ||||||
| @@ -212,22 +208,22 @@ CREATE TABLE reminders.command_aliases ( | |||||||
|     command VARCHAR(2048) NOT NULL, |     command VARCHAR(2048) NOT NULL, | ||||||
| 
 | 
 | ||||||
|     PRIMARY KEY (id), |     PRIMARY KEY (id), | ||||||
|     FOREIGN KEY (guild_id) REFERENCES reminders.guilds(id) ON DELETE CASCADE, |     FOREIGN KEY (guild_id) REFERENCES guilds(id) ON DELETE CASCADE, | ||||||
|     UNIQUE KEY (`guild_id`, `name`) |     UNIQUE KEY (`guild_id`, `name`) | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
| CREATE TABLE reminders.guild_users ( | CREATE TABLE guild_users ( | ||||||
|     guild INT UNSIGNED NOT NULL, |     guild INT UNSIGNED NOT NULL, | ||||||
|     user INT UNSIGNED NOT NULL, |     user INT UNSIGNED NOT NULL, | ||||||
| 
 | 
 | ||||||
|     can_access BOOL NOT NULL DEFAULT 0, |     can_access BOOL NOT NULL DEFAULT 0, | ||||||
| 
 | 
 | ||||||
|     FOREIGN KEY (guild) REFERENCES reminders.guilds(id) ON DELETE CASCADE, |     FOREIGN KEY (guild) REFERENCES guilds(id) ON DELETE CASCADE, | ||||||
|     FOREIGN KEY (user) REFERENCES reminders.users(id) ON DELETE CASCADE, |     FOREIGN KEY (user) REFERENCES users(id) ON DELETE CASCADE, | ||||||
|     UNIQUE KEY (guild, user) |     UNIQUE KEY (guild, user) | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
| CREATE EVENT reminders.event_cleanup | CREATE EVENT event_cleanup | ||||||
| ON SCHEDULE AT CURRENT_TIMESTAMP + INTERVAL 1 DAY | ON SCHEDULE AT CURRENT_TIMESTAMP + INTERVAL 1 DAY | ||||||
| ON COMPLETION PRESERVE | ON COMPLETION PRESERVE | ||||||
| DO DELETE FROM reminders.events WHERE `time` < DATE_SUB(NOW(), INTERVAL 5 DAY); | DO DELETE FROM events WHERE `time` < DATE_SUB(NOW(), INTERVAL 5 DAY); | ||||||
| @@ -1,5 +1,3 @@ | |||||||
| USE reminders; |  | ||||||
| 
 |  | ||||||
| SET FOREIGN_KEY_CHECKS = 0; | SET FOREIGN_KEY_CHECKS = 0; | ||||||
| 
 | 
 | ||||||
| DROP TABLE IF EXISTS reminders_new; | DROP TABLE IF EXISTS reminders_new; | ||||||
| @@ -157,4 +155,9 @@ CREATE TABLE events ( | |||||||
|     FOREIGN KEY (reminder_id) REFERENCES reminders_new(id) ON DELETE SET NULL |     FOREIGN KEY (reminder_id) REFERENCES reminders_new(id) ON DELETE SET NULL | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
|  | DROP TABLE reminders; | ||||||
|  | DROP TABLE embed_fields; | ||||||
|  | RENAME TABLE reminders_new TO reminders; | ||||||
|  | RENAME TABLE embed_fields_new TO embed_fields; | ||||||
|  | 
 | ||||||
| SET FOREIGN_KEY_CHECKS = 1; | SET FOREIGN_KEY_CHECKS = 1; | ||||||
| @@ -1,5 +1,3 @@ | |||||||
| USE reminders; |  | ||||||
| 
 |  | ||||||
| CREATE TABLE macro ( | CREATE TABLE macro ( | ||||||
|     id INT UNSIGNED AUTO_INCREMENT, |     id INT UNSIGNED AUTO_INCREMENT, | ||||||
|     guild_id INT UNSIGNED NOT NULL, |     guild_id INT UNSIGNED NOT NULL, | ||||||
| @@ -0,0 +1,2 @@ | |||||||
|  | ALTER TABLE reminders RENAME COLUMN `interval` TO `interval_seconds`; | ||||||
|  | ALTER TABLE reminders ADD COLUMN `interval_months` INT UNSIGNED DEFAULT NULL; | ||||||
| @@ -1,5 +1,3 @@ | |||||||
| USE reminders; |  | ||||||
| 
 |  | ||||||
| CREATE TABLE reminder_template ( | CREATE TABLE reminder_template ( | ||||||
|     `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, |     `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, | ||||||
| 
 | 
 | ||||||
| @@ -32,3 +30,20 @@ CREATE TABLE reminder_template ( | |||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
| ALTER TABLE reminders ADD COLUMN embed_fields JSON; | 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
									
								
								migrations/20221210000000_reminder_daily_intervals.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1 @@ | |||||||
|  | ALTER TABLE reminders ADD COLUMN `interval_days` INT UNSIGNED DEFAULT NULL; | ||||||
							
								
								
									
										1
									
								
								migrations/20230511125236_reminder_threads.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1 @@ | |||||||
|  | ALTER TABLE reminders ADD COLUMN `thread_id` BIGINT DEFAULT NULL; | ||||||
							
								
								
									
										1
									
								
								migrations/20230511180231_ephemeral_confirmations.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1 @@ | |||||||
|  | ALTER TABLE guilds ADD COLUMN ephemeral_confirmations BOOL NOT NULL DEFAULT 0; | ||||||
							
								
								
									
										2
									
								
								migrations/20230722130906_increase_reminder_name.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,2 @@ | |||||||
|  | ALTER TABLE reminders MODIFY `name` VARCHAR(100) NOT NULL DEFAULT 'Reminder'; | ||||||
|  | ALTER TABLE reminder_template MODIFY `name` VARCHAR(100) NOT NULL DEFAULT 'Reminder'; | ||||||
							
								
								
									
										9
									
								
								migrations/20230730134827_stats.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,9 @@ | |||||||
|  | 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`) | ||||||
|  | ); | ||||||
							
								
								
									
										2
									
								
								migrations/20230731170452_reminder_archive.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,2 @@ | |||||||
|  | ALTER TABLE reminders ADD COLUMN `status` ENUM ('pending', 'sent', 'failed', 'deleted') NOT NULL DEFAULT 'pending'; | ||||||
|  | ALTER TABLE reminders ADD COLUMN `status_message` TEXT; | ||||||
| @@ -0,0 +1,3 @@ | |||||||
|  | ALTER TABLE `reminder_template` ADD COLUMN `interval_seconds` INT UNSIGNED; | ||||||
|  | ALTER TABLE `reminder_template` ADD COLUMN `interval_days` INT UNSIGNED; | ||||||
|  | ALTER TABLE `reminder_template` ADD COLUMN `interval_months` INT UNSIGNED; | ||||||
							
								
								
									
										50
									
								
								migrations/20240210133900_macro_restructure.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,50 @@ | |||||||
|  | CREATE TABLE command_macro ( | ||||||
|  |     id INT UNSIGNED AUTO_INCREMENT, | ||||||
|  |     guild_id INT UNSIGNED NOT NULL, | ||||||
|  |  | ||||||
|  |     name VARCHAR(100) NOT NULL, | ||||||
|  |     description VARCHAR(100), | ||||||
|  |     commands JSON NOT NULL, | ||||||
|  |  | ||||||
|  |     FOREIGN KEY (guild_id) REFERENCES guilds(id) ON DELETE CASCADE, | ||||||
|  |     PRIMARY KEY (id) | ||||||
|  | ); | ||||||
|  |  | ||||||
|  | # New JSON structure is {command_name: "Remind", "<option name>": "<option value>", ...} | ||||||
|  | INSERT INTO command_macro (guild_id, description, name, commands) | ||||||
|  | SELECT | ||||||
|  |     guild_id, | ||||||
|  |     description, | ||||||
|  |     name, | ||||||
|  |     ( | ||||||
|  |         SELECT JSON_ARRAYAGG( | ||||||
|  |             ( | ||||||
|  |                 SELECT JSON_OBJECTAGG(t2.name, t2.value) | ||||||
|  |                 FROM JSON_TABLE( | ||||||
|  |                     JSON_ARRAY_APPEND(t1.options, '$', JSON_OBJECT('name', 'command_name', 'value', t1.command_name)), | ||||||
|  |                     '$[*]' COLUMNS ( | ||||||
|  |                         name VARCHAR(64) PATH '$.name' ERROR ON ERROR, | ||||||
|  |                         value TEXT PATH '$.value' ERROR ON ERROR | ||||||
|  |                     )) AS t2 | ||||||
|  |             ) | ||||||
|  |         ) | ||||||
|  |         FROM macro m2 | ||||||
|  |         JOIN JSON_TABLE( | ||||||
|  |             commands, | ||||||
|  |             '$[*]' COLUMNS ( | ||||||
|  |                 command_name VARCHAR(64) PATH '$.command_name' ERROR ON ERROR, | ||||||
|  |                 options JSON PATH '$.options' ERROR ON ERROR | ||||||
|  |             )) AS t1 | ||||||
|  |         WHERE m1.id = m2.id | ||||||
|  |     ) | ||||||
|  | FROM macro m1; | ||||||
|  |  | ||||||
|  | # # Check which commands are used in macros | ||||||
|  | # SELECT DISTINCT command_name | ||||||
|  | # FROM macro m2 | ||||||
|  | # JOIN JSON_TABLE( | ||||||
|  | #     commands, | ||||||
|  | #     '$[*]' COLUMNS ( | ||||||
|  | #         command_name VARCHAR(64) PATH '$.command_name' ERROR ON ERROR, | ||||||
|  | #         options JSON PATH '$.options' ERROR ON ERROR | ||||||
|  | #     )) AS t1 | ||||||
							
								
								
									
										5
									
								
								migrations/20240303125837_add_indexes.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,5 @@ | |||||||
|  | -- Add migration script here | ||||||
|  | ALTER TABLE reminders | ||||||
|  |     ADD INDEX `utc_time_index` (`utc_time`); | ||||||
|  | ALTER TABLE reminders | ||||||
|  |     ADD INDEX `status_index` (`status`); | ||||||
							
								
								
									
										53
									
								
								nginx/reminder-bot
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,53 @@ | |||||||
|  | server { | ||||||
|  |     server_name www.reminder-bot.com; | ||||||
|  |  | ||||||
|  |     return 301 https://reminder-bot.com$request_uri; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | server { | ||||||
|  |     listen 80; | ||||||
|  |     server_name beta.reminder-bot.com; | ||||||
|  |  | ||||||
|  |     return 301 https://reminder-bot.com$request_uri; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | server { | ||||||
|  |     listen 443 ssl; | ||||||
|  |     server_name beta.reminder-bot.com; | ||||||
|  |  | ||||||
|  |     ssl_certificate /etc/letsencrypt/live/beta.reminder-bot.com/fullchain.pem; | ||||||
|  |     ssl_certificate_key /etc/letsencrypt/live/beta.reminder-bot.com/privkey.pem; | ||||||
|  |  | ||||||
|  |     return 301 https://reminder-bot.com$request_uri; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | server { | ||||||
|  |     listen 443 ssl; | ||||||
|  |     server_name reminder-bot.com; | ||||||
|  |  | ||||||
|  |     ssl_certificate /etc/letsencrypt/live/reminder-bot.com/fullchain.pem; | ||||||
|  |     ssl_certificate_key /etc/letsencrypt/live/reminder-bot.com/privkey.pem; | ||||||
|  |  | ||||||
|  |     access_log /var/log/nginx/access.log; | ||||||
|  |     error_log /var/log/nginx/error.log; | ||||||
|  |  | ||||||
|  |     proxy_buffer_size 128k; | ||||||
|  |     proxy_buffers 4 256k; | ||||||
|  |     proxy_busy_buffers_size 256k; | ||||||
|  |  | ||||||
|  |     client_max_body_size 10M; | ||||||
|  |  | ||||||
|  |     location / { | ||||||
|  |         proxy_pass http://localhost:18920; | ||||||
|  |         proxy_redirect off; | ||||||
|  |         proxy_set_header Host $host; | ||||||
|  |         proxy_set_header X-Real-IP $remote_addr; | ||||||
|  |         proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; | ||||||
|  |         proxy_set_header X-Forwarded-Proto $scheme; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     location /static { | ||||||
|  |         alias /var/www/reminder-rs/static; | ||||||
|  |         expires 30d; | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -1,18 +0,0 @@ | |||||||
| [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"] } |  | ||||||
| @@ -1,559 +0,0 @@ | |||||||
| 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, 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").unwrap().as_str(); |  | ||||||
|         let format = caps.name("format").unwrap().as_str(); |  | ||||||
|  |  | ||||||
|         if let Ok(final_time) = final_time.parse::<i64>() { |  | ||||||
|             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").unwrap().as_str(); |  | ||||||
|  |  | ||||||
|             println!("{}", timezone); |  | ||||||
|  |  | ||||||
|             if let Ok(tz) = timezone.parse::<Tz>() { |  | ||||||
|                 let format = caps.name("format").unwrap().as_str(); |  | ||||||
|                 let now = Utc::now().with_timezone(&tz); |  | ||||||
|  |  | ||||||
|                 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> { |  | ||||||
|         let mut embed = 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 |  | ||||||
|         .unwrap(); |  | ||||||
|  |  | ||||||
|         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 |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     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> { |  | ||||||
|         sqlx::query_as_unchecked!( |  | ||||||
|             Reminder, |  | ||||||
|             " |  | ||||||
| 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() |  | ||||||
|             ", |  | ||||||
|         ) |  | ||||||
|         .fetch_all(pool) |  | ||||||
|         .await |  | ||||||
|         .unwrap() |  | ||||||
|         .into_iter() |  | ||||||
|         .map(|mut rem| { |  | ||||||
|             rem.content = substitute(&rem.content); |  | ||||||
|  |  | ||||||
|             rem |  | ||||||
|         }) |  | ||||||
|         .collect::<Vec<Self>>() |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     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 { |  | ||||||
|                 let row = 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 |  | ||||||
|                 .unwrap(); |  | ||||||
|  |  | ||||||
|                 updated_reminder_time = row.new_time.unwrap(); |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             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::from_u16(404).unwrap()) { |  | ||||||
|                         error!("Seeing channel is deleted. Removing reminder"); |  | ||||||
|                         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; |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
							
								
								
									
										11
									
								
								recordable_derive/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,11 @@ | |||||||
|  | [package] | ||||||
|  | name = "recordable_derive" | ||||||
|  | version = "0.1.0" | ||||||
|  | edition = "2021" | ||||||
|  |  | ||||||
|  | [lib] | ||||||
|  | proc-macro = true | ||||||
|  |  | ||||||
|  | [dependencies] | ||||||
|  | quote = "1.0.35" | ||||||
|  | syn = { version = "2.0.49", features = ["full"] } | ||||||
							
								
								
									
										42
									
								
								recordable_derive/src/lib.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,42 @@ | |||||||
|  | use proc_macro::TokenStream; | ||||||
|  | use syn::{spanned::Spanned, Data}; | ||||||
|  |  | ||||||
|  | /// Macro to allow Recordable to be implemented on an enum by dispatching over each variant. | ||||||
|  | #[proc_macro_derive(Recordable)] | ||||||
|  | pub fn extract(input: TokenStream) -> TokenStream { | ||||||
|  |     let ast = syn::parse_macro_input!(input); | ||||||
|  |  | ||||||
|  |     impl_recordable(&ast) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fn impl_recordable(ast: &syn::DeriveInput) -> TokenStream { | ||||||
|  |     let name = &ast.ident; | ||||||
|  |  | ||||||
|  |     match &ast.data { | ||||||
|  |         Data::Enum(en) => { | ||||||
|  |             let extracted = en.variants.iter().map(|var| { | ||||||
|  |                 let ident = &var.ident; | ||||||
|  |  | ||||||
|  |                 quote::quote_spanned! {var.span()=> | ||||||
|  |                     Self::#ident (opt) => opt.run(ctx).await? | ||||||
|  |                 } | ||||||
|  |             }); | ||||||
|  |  | ||||||
|  |             TokenStream::from(quote::quote! { | ||||||
|  |                 impl Recordable for #name { | ||||||
|  |                     async fn run(self, ctx: crate::Context<'_>) -> Result<(), crate::Error> { | ||||||
|  |                         match self { | ||||||
|  |                             #(#extracted,)* | ||||||
|  |                         } | ||||||
|  |  | ||||||
|  |                         Ok(()) | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             }) | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         _ => { | ||||||
|  |             panic!("Only enums can derive Recordable"); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										2
									
								
								reminder-dashboard/.prettierrc.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,2 @@ | |||||||
|  | printWidth = 100 | ||||||
|  | tabWidth = 4 | ||||||
							
								
								
									
										19
									
								
								reminder-dashboard/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,19 @@ | |||||||
|  | # reminder-dashboard | ||||||
|  |  | ||||||
|  | The re-re-rewrite of the dashboard. | ||||||
|  |  | ||||||
|  | ## Why | ||||||
|  |  | ||||||
|  | The existing beta variant of the dashboard is written using vanilla JavaScript. This is fine, | ||||||
|  | but annoying to update. This would've been okay if I was more dedicated to updating the vanilla | ||||||
|  | JavaScript too, but I want to experiment with "new" things. | ||||||
|  |  | ||||||
|  | This also allows me to expand my frontend skills, which is relevant to part of my job. | ||||||
|  |  | ||||||
|  | ## Developing | ||||||
|  |  | ||||||
|  | 1. Run both `npm run dev` and `cargo run` | ||||||
|  | 2. Symlink assets: assuming cloned | ||||||
|  |    into `$HOME`, `ln -s $HOME/reminder-bot/reminder-dashboard/dist/index.html $HOME/reminder-bot/web/static/index.html` | ||||||
|  |    and | ||||||
|  |    `ln -s $HOME/reminder-bot/reminder-dashboard/dist/static/assets $HOME/reminder-bot/web/static/assets` | ||||||
							
								
								
									
										34
									
								
								reminder-dashboard/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,34 @@ | |||||||
|  | <!DOCTYPE html> | ||||||
|  | <html lang="EN"> | ||||||
|  | <head> | ||||||
|  | 	<meta name="description" content="The most powerful Discord Reminders Bot"> | ||||||
|  | 	<meta name="viewport" content="width=device-width, initial-scale=1.0"> | ||||||
|  | 	<meta charset="UTF-8"> | ||||||
|  | 	<meta name="yandex-verification" content="bb77b8681eb64a90"/> | ||||||
|  | 	<meta name="google-site-verification" content="7h7UVTeEe0AOzHiH3cFtsqMULYGN-zCZdMT_YCkW1Ho"/> | ||||||
|  | 	<!-- <meta http-equiv="Content-Security-Policy" content="default-src 'self'; img-src *; font-src fonts.gstatic.com 'self'"> --> | ||||||
|  |  | ||||||
|  | 	<!-- favicon --> | ||||||
|  | 	<link rel="apple-touch-icon" sizes="180x180" | ||||||
|  | 		  href="/static/favicon/apple-touch-icon.png"> | ||||||
|  | 	<link rel="icon" type="image/png" sizes="32x32" | ||||||
|  | 		  href="/static/favicon/favicon-32x32.png"> | ||||||
|  | 	<link rel="icon" type="image/png" sizes="16x16" | ||||||
|  | 		  href="/static/favicon/favicon-16x16.png"> | ||||||
|  | 	<link rel="manifest" href="/static/site.webmanifest"> | ||||||
|  | 	<meta name="msapplication-TileColor" content="#da532c"> | ||||||
|  | 	<meta name="theme-color" content="#ffffff"> | ||||||
|  |  | ||||||
|  | 	<title>Reminder Bot | Dashboard</title> | ||||||
|  |  | ||||||
|  | 	<!-- styles --> | ||||||
|  | 	<link rel="stylesheet" href="/static/css/bulma.min.css"> | ||||||
|  | 	<link rel="stylesheet" href="/static/css/fa.css"> | ||||||
|  | 	<link rel="stylesheet" href="/static/css/font.css"> | ||||||
|  | 	<link rel="stylesheet" href="/static/css/style.css"> | ||||||
|  | </head> | ||||||
|  | <body> | ||||||
|  | 	<div id="app"></div> | ||||||
|  | 	<script type="module" src="/src/index.tsx"></script> | ||||||
|  | </body> | ||||||
|  | </html> | ||||||
							
								
								
									
										5626
									
								
								reminder-dashboard/package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										32
									
								
								reminder-dashboard/package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,32 @@ | |||||||
|  | { | ||||||
|  | 	"name": "example", | ||||||
|  | 	"private": true, | ||||||
|  | 	"type": "module", | ||||||
|  | 	"scripts": { | ||||||
|  | 		"dev": "vite build --watch --mode development", | ||||||
|  | 		"build": "vite build", | ||||||
|  | 		"preview": "vite preview" | ||||||
|  | 	}, | ||||||
|  | 	"dependencies": { | ||||||
|  | 		"axios": "^1.5.1", | ||||||
|  | 		"bulma": "^0.9.4", | ||||||
|  | 		"luxon": "^3.4.3", | ||||||
|  | 		"preact": "^10.13.1", | ||||||
|  | 		"react-colorful": "^5.6.1", | ||||||
|  | 		"react-query": "^3.39.3", | ||||||
|  | 		"tributejs": "^5.1.3", | ||||||
|  | 		"use-debounce": "^10.0.0", | ||||||
|  | 		"wouter": "^3.0" | ||||||
|  | 	}, | ||||||
|  | 	"devDependencies": { | ||||||
|  | 		"@preact/preset-vite": "^2.5.0", | ||||||
|  | 		"@types/luxon": "^3.3.2", | ||||||
|  | 		"eslint": "^8.50.0", | ||||||
|  | 		"eslint-config-preact": "^1.3.0", | ||||||
|  | 		"prettier": "^3.0.3", | ||||||
|  | 		"react-datepicker": "^4.21.0", | ||||||
|  | 		"sass": "^1.71.1", | ||||||
|  | 		"typescript": "^5.2.2", | ||||||
|  | 		"vite": "^5.1" | ||||||
|  | 	} | ||||||
|  | } | ||||||
| @@ -11,10 +11,26 @@ div.reminderContent.is-collapsed .column.discord-frame { | |||||||
|     display: none; |     display: none; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| div.reminderContent.is-collapsed .collapses { | div.reminderContent.is-collapsed .column.settings { | ||||||
|     display: none; |     display: none; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | div.reminderContent.is-collapsed .reminder-settings { | ||||||
|  |     margin-bottom: 0; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | div.reminderContent.is-collapsed .button-row { | ||||||
|  |     display: none; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | div.reminderContent.is-collapsed .button-row-edit { | ||||||
|  |     display: none; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | div.reminderContent.is-collapsed .reminder-topbar { | ||||||
|  |     padding-bottom: 0; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| div.reminderContent.is-collapsed .invert-collapses { | div.reminderContent.is-collapsed .invert-collapses { | ||||||
|     display: inline-flex; |     display: inline-flex; | ||||||
| } | } | ||||||
| @@ -23,42 +39,42 @@ div.reminderContent .invert-collapses { | |||||||
|     display: none; |     display: none; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| div.reminderContent.is-collapsed .settings { |  | ||||||
|     display: flex; |  | ||||||
|     flex-direction: row; |  | ||||||
|     padding-bottom: 0; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| div.reminderContent.is-collapsed .channel-field { |  | ||||||
|     display: inline-flex; |  | ||||||
|     order: 1; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| div.reminderContent.is-collapsed .reminder-topbar { |  | ||||||
|     display: inline-flex; |  | ||||||
|     margin-bottom: 0px; |  | ||||||
|     flex-grow: 1; |  | ||||||
|     order: 2; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| div.reminderContent.is-collapsed input[name="name"] { | div.reminderContent.is-collapsed input[name="name"] { | ||||||
|     display: inline-flex; |     display: inline-flex; | ||||||
|     flex-grow: 1; |     flex-grow: 1; | ||||||
|     border: none; |     border: none; | ||||||
|     font-weight: 700; |  | ||||||
|     background: none; |     background: none; | ||||||
|  |     box-shadow: none; | ||||||
|  |     opacity: 1; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| div.reminderContent.is-collapsed button.hide-box { | div.reminderContent.is-collapsed .hide-box { | ||||||
|     display: inline-flex; |     display: inline-flex; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| div.reminderContent.is-collapsed button.hide-box i { | div.reminderContent.is-collapsed .hide-box i { | ||||||
|     transform: rotate(90deg); |     transform: rotate(90deg); | ||||||
| } | } | ||||||
| /* END */ | /* END */ | ||||||
| 
 | 
 | ||||||
| /* dashboard styles */ | /* dashboard styles */ | ||||||
|  | .hide-box { | ||||||
|  |     border: none; | ||||||
|  |     background: none; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .hide-box:focus { | ||||||
|  |     outline: none; | ||||||
|  |     box-shadow: none !important; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .channel-bar { | ||||||
|  |     display: flex; | ||||||
|  |     justify-content: center; | ||||||
|  |     flex-direction: column; | ||||||
|  |     font-weight: bold; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| button.inline-btn { | button.inline-btn { | ||||||
|     height: 100%; |     height: 100%; | ||||||
|     padding: 5px; |     padding: 5px; | ||||||
| @@ -85,18 +101,86 @@ div.discord-embed { | |||||||
|     position: relative; |     position: relative; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| div.reminderContent { | div.split-controls { | ||||||
|     padding: 2px; |     display: flex; | ||||||
|     background-color: #f5f5f5; |     flex-direction: column; | ||||||
|     border-radius: 8px; |     justify-content: space-between; | ||||||
|     margin: 8px; |     flex-grow: 2; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| div.interval-group > button { | .reminder-topbar > div { | ||||||
|     margin-left: auto; |     padding-left: 6px; | ||||||
|  |     padding-right: 6px; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .settings { | ||||||
|  |     display: flex; | ||||||
|  |     flex-direction: column; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .name-bar { | ||||||
|  |     flex-grow: 1; | ||||||
|  |     flex-shrink: 1; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .hide-button-bar { | ||||||
|  |     flex-grow: 0; | ||||||
|  |     flex-shrink: 0; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .patreon-only { | ||||||
|  |     padding-bottom: 16px; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .tts-row { | ||||||
|  |     padding-bottom: 10px; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .reminder-topbar { | ||||||
|  |     display: flex; | ||||||
|  |     margin-bottom: 0 !important; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .reminder-settings { | ||||||
|  |     margin-top: 0 !important; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .reminder-settings > .column { | ||||||
|  |     flex-grow: 0; | ||||||
|  |     flex-shrink: 0; | ||||||
|  |     flex-basis: 50%; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | div.reminderContent { | ||||||
|  |     margin-top: 10px; | ||||||
|  |     margin-bottom: 10px; | ||||||
|  |     padding: 14px; | ||||||
|  |     background-color: #f5f5f5; | ||||||
|  |     border-radius: 8px; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /* Interval inputs */ | /* Interval inputs */ | ||||||
|  | div.interval-group { | ||||||
|  |     height: unset !important; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | div.interval-group .clear:focus { | ||||||
|  |     outline: none; | ||||||
|  |     box-shadow: none !important; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | div.interval-group .no-break { | ||||||
|  |     text-wrap: avoid; | ||||||
|  |     white-space: nowrap; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | div.interval-group .clear { | ||||||
|  |     border: none; | ||||||
|  |     background: none; | ||||||
|  |     padding: 1px; | ||||||
|  |     margin-right: -3px; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| div.interval-group > .interval-group-left input { | div.interval-group > .interval-group-left input { | ||||||
|     -webkit-appearance: none; |     -webkit-appearance: none; | ||||||
|     border-style: none; |     border-style: none; | ||||||
| @@ -110,12 +194,13 @@ div.interval-group > .interval-group-left input.w2 { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| div.interval-group > .interval-group-left input.w3 { | div.interval-group > .interval-group-left input.w3 { | ||||||
|     width: 6ch; |     width: 3ch; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| div.interval-group { | div.interval-group { | ||||||
|     display: flex; |     display: flex; | ||||||
|     flex-direction: row; |     flex-direction: row; | ||||||
|  |     justify-content: space-between; | ||||||
| } | } | ||||||
| /* !Interval inputs */ | /* !Interval inputs */ | ||||||
| 
 | 
 | ||||||
| @@ -180,6 +265,23 @@ div#pageNavbar a { | |||||||
|     text-align: center; |     text-align: center; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | .navbar-burger { | ||||||
|  |     flex-shrink: 0; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .navbar-item.pageTitle { | ||||||
|  |     flex-shrink: 1; | ||||||
|  |     white-space: nowrap; | ||||||
|  |     overflow: hidden; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .dashboard-burger, .dashboard-burger:active, .dashboard-burger.is-active { | ||||||
|  |     background-color: #adc99c !important; | ||||||
|  |     border-radius: 14px; | ||||||
|  |     padding: 6px; | ||||||
|  |     background-clip: content-box; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| div#pageNavbar a:hover { | div#pageNavbar a:hover { | ||||||
|     background-color: #4a4a4a; |     background-color: #4a4a4a; | ||||||
| } | } | ||||||
| @@ -206,17 +308,24 @@ div.dashboard-sidebar { | |||||||
|     padding-right: 0; |     padding-right: 0; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| div.dashboard-sidebar:not(.mobile-sidebar) { | ul.guildList { | ||||||
|     display: flex; |     flex-grow: 1; | ||||||
|     flex-direction: column; |     flex-shrink: 1; | ||||||
|  |     overflow: auto; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| div.dashboard-sidebar:not(.mobile-sidebar) .aside-footer { | div.dashboard-sidebar:not(.mobile-sidebar) .aside-footer { | ||||||
|  |     flex-shrink: 0; | ||||||
|  |     flex-grow: 0; | ||||||
|     position: fixed; |     position: fixed; | ||||||
|     bottom: 0; |     bottom: 0; | ||||||
|     width: 226px; |     width: 226px; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | div.dashboard-sidebar svg { | ||||||
|  |     flex-shrink: 0; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| div.mobile-sidebar { | div.mobile-sidebar { | ||||||
|     z-index: 100; |     z-index: 100; | ||||||
|     min-height: 100vh; |     min-height: 100vh; | ||||||
| @@ -288,11 +397,12 @@ textarea, input { | |||||||
|     width: 100%; |     width: 100%; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | input.default-width { | ||||||
|  |     width: initial; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| .message-input:placeholder-shown { | .message-input:placeholder-shown { | ||||||
|     border-top: none; |     font-style: italic; | ||||||
|     border-left: none; |  | ||||||
|     border-right: none; |  | ||||||
|     border-bottom-style: dashed; |  | ||||||
|     background-color: #40444b; |     background-color: #40444b; | ||||||
|     color: #fff; |     color: #fff; | ||||||
| } | } | ||||||
| @@ -363,8 +473,7 @@ textarea, input { | |||||||
| .customizable.is-400x300 img { | .customizable.is-400x300 img { | ||||||
|     margin-top: 10px; |     margin-top: 10px; | ||||||
|     width: 100%; |     width: 100%; | ||||||
|     min-height: 100px; |     height: 100px; | ||||||
|     max-height: 400px; |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .customizable.is-32x32 img { | .customizable.is-32x32 img { | ||||||
| @@ -458,6 +567,7 @@ textarea, input { | |||||||
|     flex-grow: 1; |     flex-grow: 1; | ||||||
|     flex-shrink: 1; |     flex-shrink: 1; | ||||||
|     flex-basis: auto; |     flex-basis: auto; | ||||||
|  |     margin-right: 4px; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .embed-body input, .embed-body textarea { | .embed-body input, .embed-body textarea { | ||||||
| @@ -507,21 +617,84 @@ textarea, input { | |||||||
|     border-bottom: 1px solid #fff; |     border-bottom: 1px solid #fff; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @media only screen and (max-width: 768px) { | .channel-selector { | ||||||
|  |     width: 100%; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .select { | ||||||
|  |     width: 100%; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | li.highlight { | ||||||
|  |     margin-bottom: 0 !important; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .button-row { | ||||||
|  |     display: flex; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .button-row .button-row-reminder { | ||||||
|  |     flex-grow: 0; | ||||||
|  |     padding: 2px; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .button-row-template { | ||||||
|  |     display: flex; | ||||||
|  |     flex-grow: 1; | ||||||
|  |     justify-content: space-between; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .button-row .button-row-template > div { | ||||||
|  |     padding: 2px; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | @media only screen and (max-width: 1023px) { | ||||||
|  |     p.title.pageTitle { | ||||||
|  |         display: none; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     .dashboard-frame { | ||||||
|  |         margin-top: 4rem !important; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     .customizable.thumbnail img { |     .customizable.thumbnail img { | ||||||
|         width: 60px; |         width: 60px; | ||||||
|         height: 60px; |         height: 60px; | ||||||
|     } |     } | ||||||
|  | } | ||||||
| 
 | 
 | ||||||
|     .customizable.is-24x24 img { | @media only screen and (max-width: 768px) { | ||||||
|         width: 16px; |     .button-row { | ||||||
|         height: 16px; |         display: flex; | ||||||
|  |         flex-direction: column; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     .button-row .button-row-reminder { | ||||||
|  |         width: 100%; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     .button-row .button-row-template > div { | ||||||
|  |         flex-basis: 0; | ||||||
|  |         flex-grow: 1; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     .button-row button { | ||||||
|  |         width: 100%; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     .reminder-settings { | ||||||
|  |         margin-bottom: 0 !important; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     .tts-row { | ||||||
|  |         padding-bottom: 0; | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /* loader */ | /* loader */ | ||||||
| #loader { | #loader { | ||||||
|     position: fixed; |     position: fixed; | ||||||
|  |     top: 0; | ||||||
|     background-color: rgba(255, 255, 255, 0.8); |     background-color: rgba(255, 255, 255, 0.8); | ||||||
|     width: 100vw; |     width: 100vw; | ||||||
|     z-index: 999; |     z-index: 999; | ||||||
| @@ -533,6 +706,86 @@ textarea, input { | |||||||
| 
 | 
 | ||||||
| /* END */ | /* END */ | ||||||
| 
 | 
 | ||||||
|  | div.reminderError { | ||||||
|  |     margin: 10px; | ||||||
|  |     padding: 14px; | ||||||
|  |     background-color: #f5f5f5; | ||||||
|  |     border-radius: 8px; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | div.reminderError .errorHead { | ||||||
|  |     display: flex; | ||||||
|  |     flex-direction: row; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | div.reminderError .errorIcon { | ||||||
|  |     padding: 8px; | ||||||
|  |     border-radius: 4px; | ||||||
|  |     margin-right: 12px; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | div.reminderError .errorIcon .fas { | ||||||
|  |     display: none | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | div.reminderError[data-case="deleted"] .errorIcon { | ||||||
|  |     background-color: #e7e5e4; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | div.reminderError[data-case="failed"] .errorIcon { | ||||||
|  |     background-color: #fecaca; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | div.reminderError[data-case="sent"] .errorIcon { | ||||||
|  |     background-color: #d9f99d; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | div.reminderError[data-case="deleted"] .errorIcon .fas.fa-trash { | ||||||
|  |     display: block; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | div.reminderError[data-case="failed"] .errorIcon .fas.fa-exclamation-triangle { | ||||||
|  |     display: block; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | div.reminderError[data-case="sent"] .errorIcon .fas.fa-check { | ||||||
|  |     display: block; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | div.reminderError .errorHead .reminderName { | ||||||
|  |     font-size: 1rem; | ||||||
|  |     display: flex; | ||||||
|  |     flex-direction: column; | ||||||
|  |     justify-content: center; | ||||||
|  |     color: rgb(54, 54, 54); | ||||||
|  |     flex-grow: 1; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | div.reminderError .errorHead .reminderTime { | ||||||
|  |     font-size: 1rem; | ||||||
|  |     display: flex; | ||||||
|  |     flex-direction: column; | ||||||
|  |     flex-shrink: 1; | ||||||
|  |     justify-content: center; | ||||||
|  |     color: rgb(54, 54, 54); | ||||||
|  |     background-color: #ffffff; | ||||||
|  |     padding: 8px; | ||||||
|  |     border-radius: 4px; | ||||||
|  |     border-color: #e5e5e5; | ||||||
|  |     border-width: 1px; | ||||||
|  |     border-style: solid; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | div.reminderError .reminderMessage { | ||||||
|  |     font-size: 1rem; | ||||||
|  |     display: flex; | ||||||
|  |     flex-direction: column; | ||||||
|  |     justify-content: center; | ||||||
|  |     color: rgb(54, 54, 54); | ||||||
|  |     flex-grow: 1; | ||||||
|  |     font-style: italic; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| /* other stuff */ | /* other stuff */ | ||||||
| 
 | 
 | ||||||
| .half-rem { | .half-rem { | ||||||
| @@ -564,11 +817,44 @@ textarea, input { | |||||||
|     background-color: white; |     background-color: white; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | a.switch-pane { | ||||||
|  |     white-space: nowrap; | ||||||
|  |     overflow: hidden; | ||||||
|  |     text-overflow: ellipsis; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .guild-submenu { | ||||||
|  |     display: none; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .guild-submenu li { | ||||||
|  |     font-size: 0.8rem; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | a.switch-pane.is-active ~ .guild-submenu { | ||||||
|  |     display: block; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .feedback { | ||||||
|  |     background-color: #5865F2; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| .is-locked { | .is-locked { | ||||||
|     pointer-events: none; |     pointer-events: none; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .is-locked > :not(.patreon-invert) { | ||||||
|     opacity: 0.4; |     opacity: 0.4; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | .is-locked .patreon-invert { | ||||||
|  |     display: block; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .patreon-invert { | ||||||
|  |     display: none; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| .is-locked .foreground { | .is-locked .foreground { | ||||||
|     pointer-events: auto; |     pointer-events: auto; | ||||||
| } | } | ||||||
| @@ -576,3 +862,27 @@ textarea, input { | |||||||
| .is-locked .field:last-of-type { | .is-locked .field:last-of-type { | ||||||
|     display: none; |     display: none; | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | .stat-row { | ||||||
|  |     display: flex; | ||||||
|  |     flex-direction: row; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .stat-box { | ||||||
|  |     flex-grow: 1; | ||||||
|  |     border-radius: 6px; | ||||||
|  |     background-color: #fcfcfc; | ||||||
|  |     border-color: #efefef; | ||||||
|  |     border-style: solid; | ||||||
|  |     border-width: 1px; | ||||||
|  |     margin: 4px; | ||||||
|  |     padding: 4px; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .figure { | ||||||
|  |     text-align: center; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .figure-num { | ||||||
|  |     font-size: 2rem; | ||||||
|  | } | ||||||
| Before Width: | Height: | Size: 7.2 KiB After Width: | Height: | Size: 7.2 KiB | 
| Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB | 
| Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 6.8 KiB | 
| Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB | 
| Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB | 
| Before Width: | Height: | Size: 7.2 KiB After Width: | Height: | Size: 7.2 KiB | 
| Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 4.9 KiB | 
| Before Width: | Height: | Size: 762 B After Width: | Height: | Size: 762 B | 
| Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB | 
| Before Width: | Height: | Size: 323 KiB After Width: | Height: | Size: 323 KiB | 
| Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 61 KiB | 
							
								
								
									
										
											BIN
										
									
								
								reminder-dashboard/public/static/img/logo_nobg.webp
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 81 KiB | 
| Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB | 
| Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB | 
| Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 55 KiB | 
| Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.6 KiB | 
| After Width: | Height: | Size: 17 KiB | 
| After Width: | Height: | Size: 14 KiB | 
| Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB | 
| Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB | 
| After Width: | Height: | Size: 44 KiB | 
| After Width: | Height: | Size: 40 KiB | 
| After Width: | Height: | Size: 44 KiB | 
| After Width: | Height: | Size: 18 KiB | 
| After Width: | Height: | Size: 20 KiB | 
| Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 65 KiB | 
							
								
								
									
										131
									
								
								reminder-dashboard/public/static/js/admin.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,131 @@ | |||||||
|  | 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, | ||||||
|  |                         }, | ||||||
|  |                     }, | ||||||
|  |                 }, | ||||||
|  |             }); | ||||||
|  |         }); | ||||||
|  | }); | ||||||
							
								
								
									
										20
									
								
								reminder-dashboard/public/static/js/chart.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,7 @@ | |||||||
|  | /*! | ||||||
|  |  * 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()}})})); | ||||||
| @@ -7,8 +7,8 @@ function get_interval(element) { | |||||||
| 
 | 
 | ||||||
|     return { |     return { | ||||||
|         months: parseInt(months) || null, |         months: parseInt(months) || null, | ||||||
|  |         days: parseInt(days) || null, | ||||||
|         seconds: |         seconds: | ||||||
|             (parseInt(days) || 0) * 86400 + |  | ||||||
|             (parseInt(hours) || 0) * 3600 + |             (parseInt(hours) || 0) * 3600 + | ||||||
|                 (parseInt(minutes) || 0) * 60 + |                 (parseInt(minutes) || 0) * 60 + | ||||||
|                 (parseInt(seconds) || 0) || null, |                 (parseInt(seconds) || 0) || null, | ||||||
| @@ -22,6 +22,15 @@ function update_interval(element) { | |||||||
|     let minutes = element.querySelector('input[name="interval_minutes"]'); |     let minutes = element.querySelector('input[name="interval_minutes"]'); | ||||||
|     let seconds = element.querySelector('input[name="interval_seconds"]'); |     let seconds = element.querySelector('input[name="interval_seconds"]'); | ||||||
| 
 | 
 | ||||||
|  |     let interval = get_interval(element); | ||||||
|  | 
 | ||||||
|  |     if (interval.months === null && interval.days === null && interval.seconds === null) { | ||||||
|  |         months.value = ""; | ||||||
|  |         days.value = ""; | ||||||
|  |         hours.value = ""; | ||||||
|  |         minutes.value = ""; | ||||||
|  |         seconds.value = ""; | ||||||
|  |     } else { | ||||||
|         months.value = months.value.padStart(1, "0"); |         months.value = months.value.padStart(1, "0"); | ||||||
|         days.value = days.value.padStart(1, "0"); |         days.value = days.value.padStart(1, "0"); | ||||||
|         hours.value = hours.value.padStart(2, "0"); |         hours.value = hours.value.padStart(2, "0"); | ||||||
| @@ -33,7 +42,10 @@ function update_interval(element) { | |||||||
|             let remainder = seconds.value % 60; |             let remainder = seconds.value % 60; | ||||||
| 
 | 
 | ||||||
|             seconds.value = String(remainder).padStart(2, "0"); |             seconds.value = String(remainder).padStart(2, "0"); | ||||||
|         minutes.value = String(Number(minutes.value) + Number(quotient)).padStart(2, "0"); |             minutes.value = String(Number(minutes.value) + Number(quotient)).padStart( | ||||||
|  |                 2, | ||||||
|  |                 "0" | ||||||
|  |             ); | ||||||
|         } |         } | ||||||
|         if (minutes.value >= 60) { |         if (minutes.value >= 60) { | ||||||
|             let quotient = Math.floor(minutes.value / 60); |             let quotient = Math.floor(minutes.value / 60); | ||||||
| @@ -42,12 +54,6 @@ function update_interval(element) { | |||||||
|             minutes.value = String(remainder).padStart(2, "0"); |             minutes.value = String(remainder).padStart(2, "0"); | ||||||
|             hours.value = String(Number(hours.value) + Number(quotient)).padStart(2, "0"); |             hours.value = String(Number(hours.value) + Number(quotient)).padStart(2, "0"); | ||||||
|         } |         } | ||||||
|     if (hours.value >= 24) { |  | ||||||
|         let quotient = Math.floor(hours.value / 24); |  | ||||||
|         let remainder = hours.value % 24; |  | ||||||
| 
 |  | ||||||
|         hours.value = String(remainder).padStart(2, "0"); |  | ||||||
|         days.value = Number(days.value) + Number(quotient); |  | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @@ -12,12 +12,25 @@ const $createTemplateBtn = $createReminder.querySelector("button#createTemplate" | |||||||
| const $loadTemplateBtn = document.querySelector("button#load-template"); | const $loadTemplateBtn = document.querySelector("button#load-template"); | ||||||
| const $deleteTemplateBtn = document.querySelector("button#delete-template"); | const $deleteTemplateBtn = document.querySelector("button#delete-template"); | ||||||
| const $templateSelect = document.querySelector("select#templateSelect"); | const $templateSelect = document.querySelector("select#templateSelect"); | ||||||
|  | const $exportBtn = document.querySelector("button#export-data"); | ||||||
|  | const $importBtn = document.querySelector("button#import-data"); | ||||||
|  | const $downloader = document.querySelector("a#downloader"); | ||||||
|  | const $uploader = document.querySelector("input#uploader"); | ||||||
| 
 | 
 | ||||||
| let channels = []; | let channels = []; | ||||||
|  | let guildNames = {}; | ||||||
| let roles = []; | let roles = []; | ||||||
| let templates = {}; | let templates = {}; | ||||||
|  | let mentions = new Tribute({ | ||||||
|  |     values: [], | ||||||
|  |     allowSpaces: true, | ||||||
|  |     selectTemplate: (item) => { | ||||||
|  |         return `<@&${item.original.value}>`; | ||||||
|  |     }, | ||||||
|  | }); | ||||||
| 
 | 
 | ||||||
| let globalPatreon = false; | let globalPatreon = false; | ||||||
|  | let guildPatreon = false; | ||||||
| 
 | 
 | ||||||
| function guildId() { | function guildId() { | ||||||
|     return document.querySelector(".guildList a.is-active").dataset["guild"]; |     return document.querySelector(".guildList a.is-active").dataset["guild"]; | ||||||
| @@ -31,18 +44,6 @@ function intToColor(i) { | |||||||
|     return `#${i.toString(16).padStart(6, "0")}`; |     return `#${i.toString(16).padStart(6, "0")}`; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| function resize_textareas() { |  | ||||||
|     document.querySelectorAll("textarea.autoresize").forEach((element) => { |  | ||||||
|         element.style.height = ""; |  | ||||||
|         element.style.height = element.scrollHeight + 3 + "px"; |  | ||||||
| 
 |  | ||||||
|         element.addEventListener("input", () => { |  | ||||||
|             element.style.height = ""; |  | ||||||
|             element.style.height = element.scrollHeight + 3 + "px"; |  | ||||||
|         }); |  | ||||||
|     }); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| function switch_pane(selector) { | function switch_pane(selector) { | ||||||
|     document.querySelectorAll("aside a").forEach((el) => { |     document.querySelectorAll("aside a").forEach((el) => { | ||||||
|         el.classList.remove("is-active"); |         el.classList.remove("is-active"); | ||||||
| @@ -52,23 +53,39 @@ function switch_pane(selector) { | |||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     document.getElementById(selector).classList.remove("is-hidden"); |     document.getElementById(selector).classList.remove("is-hidden"); | ||||||
| 
 |  | ||||||
|     resize_textareas(); |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| function update_select(sel) { | function update_select(sel) { | ||||||
|     if (sel.selectedOptions[0].dataset["webhookAvatar"]) { |     let channelDisplay = sel.closest("div.reminderContent").querySelector(".channel-bar"); | ||||||
|         sel.closest("div.reminderContent").querySelector("img.discord-avatar").src = | 
 | ||||||
|             sel.selectedOptions[0].dataset["webhookAvatar"]; |     if (channelDisplay !== null) { | ||||||
|     } else { |         channelDisplay.textContent = `#${sel.selectedOptions[0].textContent}`; | ||||||
|         sel.closest("div.reminderContent").querySelector("img.discord-avatar").src = ""; |  | ||||||
|     } |     } | ||||||
|     if (sel.selectedOptions[0].dataset["webhookName"]) { | 
 | ||||||
|         sel.closest("div.reminderContent").querySelector("input.discord-username").value = |     if (sel.selectedOptions[0] === undefined) { | ||||||
|             sel.selectedOptions[0].dataset["webhookName"]; |         return; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     const avatarInput = sel.closest("div.reminderContent").querySelector("img.avatar"); | ||||||
|  | 
 | ||||||
|  |     if (!avatarInput.dataset["set"]) { | ||||||
|  |         if (sel.selectedOptions[0].dataset["webhookAvatar"]) { | ||||||
|  |             avatarInput.src = sel.selectedOptions[0].dataset["webhookAvatar"]; | ||||||
|         } else { |         } else { | ||||||
|         sel.closest("div.reminderContent").querySelector("input.discord-username").value = |             avatarInput.src = "/static/img/icon.png"; | ||||||
|             ""; |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     const usernameInput = sel | ||||||
|  |         .closest("div.reminderContent") | ||||||
|  |         .querySelector("input.discord-username"); | ||||||
|  | 
 | ||||||
|  |     if (usernameInput.value.length === 0) { | ||||||
|  |         if (sel.selectedOptions[0].dataset["webhookName"]) { | ||||||
|  |             usernameInput.value = sel.selectedOptions[0].dataset["webhookName"]; | ||||||
|  |         } else { | ||||||
|  |             usernameInput.value = "Reminder"; | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @@ -78,6 +95,18 @@ function reset_guild_pane() { | |||||||
|         .forEach((opt) => opt.remove()); |         .forEach((opt) => opt.remove()); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | async function fetch_patreon(guild_id) { | ||||||
|  |     fetch(`/dashboard/api/guild/${guild_id}/patreon`) | ||||||
|  |         .then((response) => response.json()) | ||||||
|  |         .then((data) => { | ||||||
|  |             if (data.error) { | ||||||
|  |                 show_error(data.error); | ||||||
|  |             } else { | ||||||
|  |                 return data.patreon; | ||||||
|  |             } | ||||||
|  |         }); | ||||||
|  | } | ||||||
|  | 
 | ||||||
| function fetch_roles(guild_id) { | function fetch_roles(guild_id) { | ||||||
|     fetch(`/dashboard/api/guild/${guild_id}/roles`) |     fetch(`/dashboard/api/guild/${guild_id}/roles`) | ||||||
|         .then((response) => response.json()) |         .then((response) => response.json()) | ||||||
| @@ -85,7 +114,16 @@ function fetch_roles(guild_id) { | |||||||
|             if (data.error) { |             if (data.error) { | ||||||
|                 show_error(data.error); |                 show_error(data.error); | ||||||
|             } else { |             } else { | ||||||
|                 roles = data; |                 let values = Array.from( | ||||||
|  |                     data.map((role) => { | ||||||
|  |                         return { | ||||||
|  |                             key: role.name, | ||||||
|  |                             value: role.id, | ||||||
|  |                         }; | ||||||
|  |                     }) | ||||||
|  |                 ); | ||||||
|  | 
 | ||||||
|  |                 mentions.collection[0].values = values; | ||||||
|             } |             } | ||||||
|         }); |         }); | ||||||
| } | } | ||||||
| @@ -118,12 +156,18 @@ async function fetch_channels(guild_id) { | |||||||
|     const event = new Event("channelsLoading"); |     const event = new Event("channelsLoading"); | ||||||
|     document.dispatchEvent(event); |     document.dispatchEvent(event); | ||||||
| 
 | 
 | ||||||
|  |     let hasError = false; | ||||||
|  | 
 | ||||||
|     await fetch(`/dashboard/api/guild/${guild_id}/channels`) |     await fetch(`/dashboard/api/guild/${guild_id}/channels`) | ||||||
|         .then((response) => response.json()) |         .then((response) => response.json()) | ||||||
|         .then((data) => { |         .then((data) => { | ||||||
|             if (data.error) { |             if (data.error) { | ||||||
|                 if (data.error === "Bot not in guild") { |                 if (data.error === "Bot not in guild") { | ||||||
|                     switch_pane("guild-error"); |                     switch_pane("guild-error"); | ||||||
|  |                     hasError = true; | ||||||
|  |                 } else if (data.error === "Incorrect permissions") { | ||||||
|  |                     switch_pane("user-error"); | ||||||
|  |                     hasError = true; | ||||||
|                 } else { |                 } else { | ||||||
|                     show_error(data.error); |                     show_error(data.error); | ||||||
|                 } |                 } | ||||||
| @@ -135,6 +179,8 @@ async function fetch_channels(guild_id) { | |||||||
|             const event = new Event("channelsLoaded"); |             const event = new Event("channelsLoaded"); | ||||||
|             document.dispatchEvent(event); |             document.dispatchEvent(event); | ||||||
|         }); |         }); | ||||||
|  | 
 | ||||||
|  |     return hasError; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| async function fetch_reminders(guild_id) { | async function fetch_reminders(guild_id) { | ||||||
| @@ -158,6 +204,8 @@ async function fetch_reminders(guild_id) { | |||||||
|                     newFrame.querySelector(".reminderContent").dataset["uid"] = |                     newFrame.querySelector(".reminderContent").dataset["uid"] = | ||||||
|                         reminder["uid"]; |                         reminder["uid"]; | ||||||
| 
 | 
 | ||||||
|  |                     mentions.attach(newFrame.querySelector("textarea")); | ||||||
|  | 
 | ||||||
|                     deserialize_reminder(reminder, newFrame, "load"); |                     deserialize_reminder(reminder, newFrame, "load"); | ||||||
| 
 | 
 | ||||||
|                     $reminderBox.appendChild(newFrame); |                     $reminderBox.appendChild(newFrame); | ||||||
| @@ -175,22 +223,25 @@ async function fetch_reminders(guild_id) { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| async function serialize_reminder(node, mode) { | async function serialize_reminder(node, mode) { | ||||||
|     let interval, utc_time, expiration_time; |     let utc_time, expiration_time; | ||||||
|  |     let interval = get_interval(node); | ||||||
| 
 | 
 | ||||||
|     if (mode !== "template") { |     if (mode !== "template") { | ||||||
|         interval = get_interval(node); |  | ||||||
| 
 |  | ||||||
|         utc_time = luxon.DateTime.fromISO( |         utc_time = luxon.DateTime.fromISO( | ||||||
|             node.querySelector('input[name="time"]').value |             node.querySelector('input[name="time"]').value | ||||||
|         ).setZone("UTC"); |         ).setZone("UTC"); | ||||||
|  | 
 | ||||||
|         if (utc_time.invalid) { |         if (utc_time.invalid) { | ||||||
|             return { error: "Time provided invalid." }; |             return { error: "Time provided invalid." }; | ||||||
|         } else { |         } else { | ||||||
|             utc_time = utc_time.toFormat("yyyy-LL-dd'T'HH:mm:ss"); |             utc_time = utc_time.toFormat("yyyy-LL-dd'T'HH:mm:ss"); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  |         let expiration = node.querySelector('input[name="expiration"]').value; | ||||||
|  | 
 | ||||||
|  |         if (expiration) { | ||||||
|             expiration_time = luxon.DateTime.fromISO( |             expiration_time = luxon.DateTime.fromISO( | ||||||
|             node.querySelector('input[name="time"]').value |                 node.querySelector('input[name="expiration"]').value | ||||||
|             ).setZone("UTC"); |             ).setZone("UTC"); | ||||||
|             if (expiration_time.invalid) { |             if (expiration_time.invalid) { | ||||||
|                 return { error: "Expiration provided invalid." }; |                 return { error: "Expiration provided invalid." }; | ||||||
| @@ -198,6 +249,12 @@ async function serialize_reminder(node, mode) { | |||||||
|                 expiration_time = expiration_time.toFormat("yyyy-LL-dd'T'HH:mm:ss"); |                 expiration_time = expiration_time.toFormat("yyyy-LL-dd'T'HH:mm:ss"); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     let name = node.querySelector('input[name="name"]').value; | ||||||
|  |     if (name.length > 100) { | ||||||
|  |         return { error: "Name exceeds maximum length (100)." }; | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
|     let rgb_color = window.getComputedStyle( |     let rgb_color = window.getComputedStyle( | ||||||
|         node.querySelector("div.discord-embed") |         node.querySelector("div.discord-embed") | ||||||
| @@ -261,15 +318,17 @@ async function serialize_reminder(node, mode) { | |||||||
|     const embed_title = node.querySelector('textarea[name="embed_title"]').value; |     const embed_title = node.querySelector('textarea[name="embed_title"]').value; | ||||||
| 
 | 
 | ||||||
|     if ( |     if ( | ||||||
|         attachment === null && |         content.length === 0 && | ||||||
|         content.length == 0 && |         embed_author.length === 0 && | ||||||
|  |         embed_title.length === 0 && | ||||||
|  |         embed_description.length === 0 && | ||||||
|  |         embed_footer.length === 0 && | ||||||
|         embed_author_url === null && |         embed_author_url === null && | ||||||
|         embed_author.length == 0 && |  | ||||||
|         embed_description.length == 0 && |  | ||||||
|         embed_footer.length == 0 && |  | ||||||
|         embed_footer_url === null && |         embed_footer_url === null && | ||||||
|         embed_image_url === null && |         embed_image_url === null && | ||||||
|         embed_thumbnail_url === null |         embed_thumbnail_url === null && | ||||||
|  |         fields.length === 0 && | ||||||
|  |         attachment === null | ||||||
|     ) { |     ) { | ||||||
|         return { error: "Reminder needs content." }; |         return { error: "Reminder needs content." }; | ||||||
|     } |     } | ||||||
| @@ -282,7 +341,7 @@ async function serialize_reminder(node, mode) { | |||||||
|         restartable: false, |         restartable: false, | ||||||
|         attachment: attachment, |         attachment: attachment, | ||||||
|         attachment_name: attachment_name, |         attachment_name: attachment_name, | ||||||
|         avatar: has_source(node.querySelector("img.discord-avatar").src), |         avatar: has_source(node.querySelector("img.avatar").src), | ||||||
|         channel: node.querySelector("select.channel-selector").value, |         channel: node.querySelector("select.channel-selector").value, | ||||||
|         content: content, |         content: content, | ||||||
|         embed_author_url: embed_author_url, |         embed_author_url: embed_author_url, | ||||||
| @@ -296,10 +355,10 @@ async function serialize_reminder(node, mode) { | |||||||
|         embed_title: embed_title, |         embed_title: embed_title, | ||||||
|         embed_fields: fields, |         embed_fields: fields, | ||||||
|         expires: expiration_time, |         expires: expiration_time, | ||||||
|         interval_seconds: mode !== "template" ? interval.seconds : null, |         interval_seconds: interval.seconds, | ||||||
|         interval_months: mode !== "template" ? interval.months : null, |         interval_days: interval.days, | ||||||
|  |         interval_months: interval.months, | ||||||
|         name: node.querySelector('input[name="name"]').value, |         name: node.querySelector('input[name="name"]').value, | ||||||
|         pin: node.querySelector('input[name="pin"]').checked, |  | ||||||
|         tts: node.querySelector('input[name="tts"]').checked, |         tts: node.querySelector('input[name="tts"]').checked, | ||||||
|         username: node.querySelector('input[name="username"]').value, |         username: node.querySelector('input[name="username"]').value, | ||||||
|         utc_time: utc_time, |         utc_time: utc_time, | ||||||
| @@ -310,6 +369,9 @@ function deserialize_reminder(reminder, frame, mode) { | |||||||
|     // populate channels
 |     // populate channels
 | ||||||
|     set_channels(frame.querySelector("select.channel-selector")); |     set_channels(frame.querySelector("select.channel-selector")); | ||||||
| 
 | 
 | ||||||
|  |     frame.querySelector(`*[name="interval_hours"]`).value = 0; | ||||||
|  |     frame.querySelector(`*[name="interval_minutes"]`).value = 0; | ||||||
|  | 
 | ||||||
|     // populate majority of items
 |     // populate majority of items
 | ||||||
|     for (let prop in reminder) { |     for (let prop in reminder) { | ||||||
|         if (reminder.hasOwnProperty(prop) && reminder[prop] !== null) { |         if (reminder.hasOwnProperty(prop) && reminder[prop] !== null) { | ||||||
| @@ -324,15 +386,27 @@ function deserialize_reminder(reminder, frame, mode) { | |||||||
|                 if ($input !== null) { |                 if ($input !== null) { | ||||||
|                     $input.value = reminder[prop]; |                     $input.value = reminder[prop]; | ||||||
|                 } else if ($image !== null) { |                 } else if ($image !== null) { | ||||||
|  |                     console.log(`loading img ${prop}`); | ||||||
|                     $image.src = reminder[prop]; |                     $image.src = reminder[prop]; | ||||||
|  |                     $image.dataset["set"] = "1"; | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     const lastChild = frame.querySelector("div.embed-multifield-box .embed-field-box"); |     update_interval(frame); | ||||||
|  |     update_select(frame.querySelector(".channel-selector")); | ||||||
| 
 | 
 | ||||||
|     for (let field of reminder["embed_fields"]) { |     const lastChild = frame.querySelector( | ||||||
|  |         "div.embed-multifield-box .embed-field-box:last-child" | ||||||
|  |     ); | ||||||
|  | 
 | ||||||
|  |     // Drop existing fields
 | ||||||
|  |     frame | ||||||
|  |         .querySelectorAll(".embed-field-box:not(:last-child)") | ||||||
|  |         .forEach((el) => el.remove()); | ||||||
|  | 
 | ||||||
|  |     for (let field of reminder["embed_fields"] || []) { | ||||||
|         let embed_field = $embedFieldTemplate.content.cloneNode(true); |         let embed_field = $embedFieldTemplate.content.cloneNode(true); | ||||||
|         embed_field.querySelector("textarea.discord-field-title").value = field["title"]; |         embed_field.querySelector("textarea.discord-field-title").value = field["title"]; | ||||||
|         embed_field.querySelector("textarea.discord-field-value").value = field["value"]; |         embed_field.querySelector("textarea.discord-field-value").value = field["value"]; | ||||||
| @@ -345,9 +419,9 @@ function deserialize_reminder(reminder, frame, mode) { | |||||||
|             .insertBefore(embed_field, lastChild); |             .insertBefore(embed_field, lastChild); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     if (mode !== "template") { |  | ||||||
|     if (reminder["interval_seconds"]) update_interval(frame); |     if (reminder["interval_seconds"]) update_interval(frame); | ||||||
| 
 | 
 | ||||||
|  |     if (mode !== "template") { | ||||||
|         let $enableBtn = frame.querySelector(".disable-enable"); |         let $enableBtn = frame.querySelector(".disable-enable"); | ||||||
|         $enableBtn.dataset["action"] = reminder["enabled"] ? "disable" : "enable"; |         $enableBtn.dataset["action"] = reminder["enabled"] ? "disable" : "enable"; | ||||||
| 
 | 
 | ||||||
| @@ -358,7 +432,7 @@ function deserialize_reminder(reminder, frame, mode) { | |||||||
|         timeInput.value = localTime.toFormat("yyyy-LL-dd'T'HH:mm:ss"); |         timeInput.value = localTime.toFormat("yyyy-LL-dd'T'HH:mm:ss"); | ||||||
| 
 | 
 | ||||||
|         if (reminder["expires"]) { |         if (reminder["expires"]) { | ||||||
|             let expiresInput = frame.querySelector('input[name="time"]'); |             let expiresInput = frame.querySelector('input[name="expiration"]'); | ||||||
|             let expiresTime = luxon.DateTime.fromISO(reminder["expires"], { |             let expiresTime = luxon.DateTime.fromISO(reminder["expires"], { | ||||||
|                 zone: "UTC", |                 zone: "UTC", | ||||||
|             }).setZone(timezone); |             }).setZone(timezone); | ||||||
| @@ -370,17 +444,36 @@ function deserialize_reminder(reminder, frame, mode) { | |||||||
| document.addEventListener("guildSwitched", async (e) => { | document.addEventListener("guildSwitched", async (e) => { | ||||||
|     $loader.classList.remove("is-hidden"); |     $loader.classList.remove("is-hidden"); | ||||||
| 
 | 
 | ||||||
|  |     document | ||||||
|  |         .querySelectorAll(".patreon-only") | ||||||
|  |         .forEach((el) => el.classList.add("is-locked")); | ||||||
|  | 
 | ||||||
|     let $anchor = document.querySelector( |     let $anchor = document.querySelector( | ||||||
|         `.switch-pane[data-guild="${e.detail.guild_id}"]` |         `.switch-pane[data-guild="${e.detail.guild_id}"]` | ||||||
|     ); |     ); | ||||||
| 
 | 
 | ||||||
|  |     let hasError = false; | ||||||
|  | 
 | ||||||
|  |     if ($anchor === null) { | ||||||
|  |         switch_pane("user-error"); | ||||||
|  |         hasError = true; | ||||||
|  |         return; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     switch_pane($anchor.dataset["pane"]); |     switch_pane($anchor.dataset["pane"]); | ||||||
|     reset_guild_pane(); |     reset_guild_pane(); | ||||||
|     $anchor.classList.add("is-active"); |     $anchor.classList.add("is-active"); | ||||||
| 
 | 
 | ||||||
|  |     if (globalPatreon || (await fetch_patreon(e.detail.guild_id))) { | ||||||
|  |         document | ||||||
|  |             .querySelectorAll(".patreon-only") | ||||||
|  |             .forEach((el) => el.classList.remove("is-locked")); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     hasError = await fetch_channels(e.detail.guild_id); | ||||||
|  |     if (!hasError) { | ||||||
|         fetch_roles(e.detail.guild_id); |         fetch_roles(e.detail.guild_id); | ||||||
|         fetch_templates(e.detail.guild_id); |         fetch_templates(e.detail.guild_id); | ||||||
|     await fetch_channels(e.detail.guild_id); |  | ||||||
|         fetch_reminders(e.detail.guild_id); |         fetch_reminders(e.detail.guild_id); | ||||||
| 
 | 
 | ||||||
|         document.querySelectorAll("p.pageTitle").forEach((el) => { |         document.querySelectorAll("p.pageTitle").forEach((el) => { | ||||||
| @@ -391,8 +484,7 @@ document.addEventListener("guildSwitched", async (e) => { | |||||||
|                 update_select(e.target); |                 update_select(e.target); | ||||||
|             }); |             }); | ||||||
|         }); |         }); | ||||||
| 
 |     } | ||||||
|     resize_textareas(); |  | ||||||
| 
 | 
 | ||||||
|     $loader.classList.add("is-hidden"); |     $loader.classList.add("is-hidden"); | ||||||
| }); | }); | ||||||
| @@ -404,6 +496,12 @@ document.addEventListener("channelsLoaded", () => { | |||||||
| document.addEventListener("remindersLoaded", (event) => { | document.addEventListener("remindersLoaded", (event) => { | ||||||
|     const guild = guildId(); |     const guild = guildId(); | ||||||
| 
 | 
 | ||||||
|  |     document.querySelectorAll("select.channel-selector").forEach((el) => { | ||||||
|  |         el.addEventListener("change", (e) => { | ||||||
|  |             update_select(e.target); | ||||||
|  |         }); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|     for (let reminder of event.detail) { |     for (let reminder of event.detail) { | ||||||
|         let node = reminder.node; |         let node = reminder.node; | ||||||
| 
 | 
 | ||||||
| @@ -431,9 +529,9 @@ document.addEventListener("remindersLoaded", (event) => { | |||||||
|                     if (data.error) { |                     if (data.error) { | ||||||
|                         show_error(data.error); |                         show_error(data.error); | ||||||
|                     } else { |                     } else { | ||||||
|                         enableBtn.dataset["action"] = data["enabled"] |                         enableBtn.dataset["action"] = data.reminder["enabled"] | ||||||
|                             ? "enable" |                             ? "disable" | ||||||
|                             : "disable"; |                             : "enable"; | ||||||
|                     } |                     } | ||||||
|                 }); |                 }); | ||||||
|         }); |         }); | ||||||
| @@ -468,6 +566,8 @@ document.addEventListener("remindersLoaded", (event) => { | |||||||
|                 .then((response) => response.json()) |                 .then((response) => response.json()) | ||||||
|                 .then((data) => { |                 .then((data) => { | ||||||
|                     for (let error of data.errors) show_error(error); |                     for (let error of data.errors) show_error(error); | ||||||
|  | 
 | ||||||
|  |                     deserialize_reminder(data.reminder, node, "reload"); | ||||||
|                 }); |                 }); | ||||||
| 
 | 
 | ||||||
|             $saveBtn.querySelector("span.icon > i").classList = ["fas fa-check"]; |             $saveBtn.querySelector("span.icon > i").classList = ["fas fa-check"]; | ||||||
| @@ -503,6 +603,16 @@ function show_error(error) { | |||||||
|     }, 5000); |     }, 5000); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | function show_success(error) { | ||||||
|  |     document.getElementById("success").querySelector("span.success-message").textContent = | ||||||
|  |         error; | ||||||
|  |     document.getElementById("success").classList.add("is-active"); | ||||||
|  | 
 | ||||||
|  |     window.setTimeout(() => { | ||||||
|  |         document.getElementById("success").classList.remove("is-active"); | ||||||
|  |     }, 5000); | ||||||
|  | } | ||||||
|  | 
 | ||||||
| $colorPickerInput.value = colorPicker.color.hexString; | $colorPickerInput.value = colorPicker.color.hexString; | ||||||
| 
 | 
 | ||||||
| $colorPickerInput.addEventListener("input", () => { | $colorPickerInput.addEventListener("input", () => { | ||||||
| @@ -528,9 +638,11 @@ document.querySelectorAll(".show-modal").forEach((element) => { | |||||||
|     }); |     }); | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| document.addEventListener("DOMContentLoaded", () => { | document.addEventListener("DOMContentLoaded", async () => { | ||||||
|     $loader.classList.remove("is-hidden"); |     $loader.classList.remove("is-hidden"); | ||||||
| 
 | 
 | ||||||
|  |     mentions.attach(document.querySelectorAll("textarea")); | ||||||
|  | 
 | ||||||
|     document.querySelectorAll(".navbar-burger").forEach((el) => { |     document.querySelectorAll(".navbar-burger").forEach((el) => { | ||||||
|         el.addEventListener("click", () => { |         el.addEventListener("click", () => { | ||||||
|             const target = el.dataset["target"]; |             const target = el.dataset["target"]; | ||||||
| @@ -546,7 +658,7 @@ document.addEventListener("DOMContentLoaded", () => { | |||||||
|         hideBox.closest(".reminderContent").classList.toggle("is-collapsed"); |         hideBox.closest(".reminderContent").classList.toggle("is-collapsed"); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     fetch("/dashboard/api/user") |     await fetch("/dashboard/api/user") | ||||||
|         .then((response) => response.json()) |         .then((response) => response.json()) | ||||||
|         .then((data) => { |         .then((data) => { | ||||||
|             if (data.error) { |             if (data.error) { | ||||||
| @@ -560,7 +672,7 @@ document.addEventListener("DOMContentLoaded", () => { | |||||||
|             } |             } | ||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|     fetch("/dashboard/api/user/guilds") |     await fetch("/dashboard/api/user/guilds") | ||||||
|         .then((response) => response.json()) |         .then((response) => response.json()) | ||||||
|         .then((data) => { |         .then((data) => { | ||||||
|             if (data.error) { |             if (data.error) { | ||||||
| @@ -569,6 +681,8 @@ document.addEventListener("DOMContentLoaded", () => { | |||||||
|                 const $template = document.getElementById("guildListEntry"); |                 const $template = document.getElementById("guildListEntry"); | ||||||
| 
 | 
 | ||||||
|                 for (let guild of data) { |                 for (let guild of data) { | ||||||
|  |                     guildNames[guild.id] = guild.name; | ||||||
|  | 
 | ||||||
|                     document.querySelectorAll(".guildList").forEach((element) => { |                     document.querySelectorAll(".guildList").forEach((element) => { | ||||||
|                         const $clone = $template.content.cloneNode(true); |                         const $clone = $template.content.cloneNode(true); | ||||||
|                         const $anchor = $clone.querySelector("a"); |                         const $anchor = $clone.querySelector("a"); | ||||||
| @@ -585,11 +699,7 @@ document.addEventListener("DOMContentLoaded", () => { | |||||||
| 
 | 
 | ||||||
|                         $anchor.addEventListener("click", async (e) => { |                         $anchor.addEventListener("click", async (e) => { | ||||||
|                             e.preventDefault(); |                             e.preventDefault(); | ||||||
|                             window.history.pushState( |                             window.history.pushState({}, "", `/dashboard/${guild.id}`); | ||||||
|                                 {}, |  | ||||||
|                                 "", |  | ||||||
|                                 `/dashboard/${guild.id}?name=${guild.name}` |  | ||||||
|                             ); |  | ||||||
|                             const event = new CustomEvent("guildSwitched", { |                             const event = new CustomEvent("guildSwitched", { | ||||||
|                                 detail: { |                                 detail: { | ||||||
|                                     guild_name: guild.name, |                                     guild_name: guild.name, | ||||||
| @@ -607,8 +717,8 @@ document.addEventListener("DOMContentLoaded", () => { | |||||||
|                 const matches = window.location.href.match(/dashboard\/(\d+)/); |                 const matches = window.location.href.match(/dashboard\/(\d+)/); | ||||||
|                 if (matches) { |                 if (matches) { | ||||||
|                     let id = matches[1]; |                     let id = matches[1]; | ||||||
|                     let name = |                     let name = guildNames[id]; | ||||||
|                         new URLSearchParams(window.location.search).get("name") || id; | 
 | ||||||
|                     const event = new CustomEvent("guildSwitched", { |                     const event = new CustomEvent("guildSwitched", { | ||||||
|                         detail: { |                         detail: { | ||||||
|                             guild_name: name, |                             guild_name: name, | ||||||
| @@ -645,6 +755,53 @@ function has_source(string) { | |||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | $uploader.addEventListener("change", (ev) => { | ||||||
|  |     const urlTail = document.querySelector('input[name="exportSelect"]:checked').value; | ||||||
|  | 
 | ||||||
|  |     new Promise((resolve) => { | ||||||
|  |         let fileReader = new FileReader(); | ||||||
|  |         fileReader.onload = (e) => resolve(fileReader.result); | ||||||
|  |         fileReader.readAsDataURL($uploader.files[0]); | ||||||
|  |     }).then((dataUrl) => { | ||||||
|  |         $importBtn.setAttribute("disabled", true); | ||||||
|  | 
 | ||||||
|  |         fetch(`/dashboard/api/guild/${guildId()}/export/${urlTail}`, { | ||||||
|  |             method: "PUT", | ||||||
|  |             body: JSON.stringify({ body: dataUrl.split(",")[1] }), | ||||||
|  |         }) | ||||||
|  |             .then((response) => response.json()) | ||||||
|  |             .then((data) => { | ||||||
|  |                 $importBtn.removeAttribute("disabled"); | ||||||
|  | 
 | ||||||
|  |                 if (data.error) { | ||||||
|  |                     show_error(data.error); | ||||||
|  |                 } else { | ||||||
|  |                     show_success(data.message); | ||||||
|  |                 } | ||||||
|  |             }) | ||||||
|  |             .then(() => { | ||||||
|  |                 delete $uploader.files[0]; | ||||||
|  |                 fetch_reminders(guild); | ||||||
|  |             }); | ||||||
|  |     }); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | $importBtn.addEventListener("click", () => { | ||||||
|  |     $uploader.click(); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | $exportBtn.addEventListener("click", () => { | ||||||
|  |     const urlTail = document.querySelector('input[name="exportSelect"]:checked').value; | ||||||
|  | 
 | ||||||
|  |     fetch(`/dashboard/api/guild/${guildId()}/export/${urlTail}`) | ||||||
|  |         .then((response) => response.json()) | ||||||
|  |         .then((data) => { | ||||||
|  |             $downloader.href = | ||||||
|  |                 "data:text/plain;charset=utf-8," + encodeURIComponent(data.body); | ||||||
|  |             $downloader.click(); | ||||||
|  |         }); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
| $createReminderBtn.addEventListener("click", async () => { | $createReminderBtn.addEventListener("click", async () => { | ||||||
|     $createReminderBtn.querySelector("span.icon > i").classList = [ |     $createReminderBtn.querySelector("span.icon > i").classList = [ | ||||||
|         "fas fa-spinner fa-spin", |         "fas fa-spinner fa-spin", | ||||||
| @@ -653,6 +810,7 @@ $createReminderBtn.addEventListener("click", async () => { | |||||||
|     let reminder = await serialize_reminder($createReminder, "create"); |     let reminder = await serialize_reminder($createReminder, "create"); | ||||||
|     if (reminder.error) { |     if (reminder.error) { | ||||||
|         show_error(reminder.error); |         show_error(reminder.error); | ||||||
|  |         $createReminderBtn.querySelector("span.icon > i").classList = ["fas fa-sparkles"]; | ||||||
|         return; |         return; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| @@ -710,6 +868,14 @@ $createTemplateBtn.addEventListener("click", async () => { | |||||||
|     ]; |     ]; | ||||||
| 
 | 
 | ||||||
|     let reminder = await serialize_reminder($createReminder, "template"); |     let reminder = await serialize_reminder($createReminder, "template"); | ||||||
|  |     if (reminder.error) { | ||||||
|  |         show_error(reminder.error); | ||||||
|  |         $createTemplateBtn.querySelector("span.icon > i").classList = [ | ||||||
|  |             "fas fa-file-spreadsheet", | ||||||
|  |         ]; | ||||||
|  |         return; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     let guild = guildId(); |     let guild = guildId(); | ||||||
| 
 | 
 | ||||||
|     fetch(`/dashboard/api/guild/${guild}/templates`, { |     fetch(`/dashboard/api/guild/${guild}/templates`, { | ||||||
| @@ -751,6 +917,7 @@ $loadTemplateBtn.addEventListener("click", (ev) => { | |||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| $deleteTemplateBtn.addEventListener("click", (ev) => { | $deleteTemplateBtn.addEventListener("click", (ev) => { | ||||||
|  |     if (parseInt($templateSelect.value) !== null) { | ||||||
|         fetch(`/dashboard/api/guild/${guildId()}/templates`, { |         fetch(`/dashboard/api/guild/${guildId()}/templates`, { | ||||||
|             method: "DELETE", |             method: "DELETE", | ||||||
|             headers: { |             headers: { | ||||||
| @@ -768,13 +935,7 @@ $deleteTemplateBtn.addEventListener("click", (ev) => { | |||||||
|                         .remove(); |                         .remove(); | ||||||
|                 } |                 } | ||||||
|             }); |             }); | ||||||
| }); |     } | ||||||
| 
 |  | ||||||
| document.querySelectorAll("textarea.autoresize").forEach((element) => { |  | ||||||
|     element.addEventListener("input", () => { |  | ||||||
|         element.style.height = ""; |  | ||||||
|         element.style.height = element.scrollHeight + 3 + "px"; |  | ||||||
|     }); |  | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| let $img; | let $img; | ||||||
| @@ -809,7 +970,7 @@ document.addEventListener("remindersLoaded", () => { | |||||||
|         }); |         }); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     const fileInput = document.querySelectorAll("input[type=file]"); |     const fileInput = document.querySelectorAll("input.file-input[type=file]"); | ||||||
| 
 | 
 | ||||||
|     fileInput.forEach((element) => { |     fileInput.forEach((element) => { | ||||||
|         element.addEventListener("change", () => { |         element.addEventListener("change", () => { | ||||||
| @@ -832,6 +993,13 @@ document.addEventListener("remindersLoaded", () => { | |||||||
|                 window.getComputedStyle($discordFrame).borderLeftColor; |                 window.getComputedStyle($discordFrame).borderLeftColor; | ||||||
|         }); |         }); | ||||||
|     }); |     }); | ||||||
|  | 
 | ||||||
|  |     document.querySelectorAll("textarea.autoresize").forEach((element) => { | ||||||
|  |         element.addEventListener("input", () => { | ||||||
|  |             element.style.height = ""; | ||||||
|  |             element.style.height = element.scrollHeight + 3 + "px"; | ||||||
|  |         }); | ||||||
|  |     }); | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| function check_embed_fields() { | function check_embed_fields() { | ||||||
| @@ -901,13 +1069,19 @@ document.addEventListener("DOMNodeInserted", () => { | |||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     check_embed_fields(); |     check_embed_fields(); | ||||||
|     resize_textareas(); |  | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| document.addEventListener("click", (ev) => { | document.addEventListener("click", (ev) => { | ||||||
|     if (ev.target.closest("button.inline-btn") !== null) { |     if (ev.target.closest("button.inline-btn") !== null) { | ||||||
|         let inlined = ev.target.closest(".embed-field-box").dataset["inlined"]; |         let inlined = ev.target.closest(".embed-field-box").dataset["inlined"]; | ||||||
|         ev.target.closest(".embed-field-box").dataset["inlined"] = |         ev.target.closest(".embed-field-box").dataset["inlined"] = | ||||||
|             inlined == "1" ? "0" : "1"; |             inlined === "1" ? "0" : "1"; | ||||||
|     } |     } | ||||||
| }); | }); | ||||||
|  | 
 | ||||||
|  | document.addEventListener("DOMContentLoaded", () => { | ||||||
|  |     let now = luxon.DateTime.now().setZone(timezone); | ||||||
|  |     document.querySelectorAll(".prefill-now").forEach((el) => { | ||||||
|  |         el.value = now.toFormat("yyyy-LL-dd'T'HH:mm:ss"); | ||||||
|  |     }); | ||||||
|  | }); | ||||||
							
								
								
									
										16
									
								
								reminder-dashboard/public/static/js/reporter.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,16 @@ | |||||||
|  | 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,14 +1,15 @@ | |||||||
| { | { | ||||||
|     "name": "", |     "name": "Reminder Bot Dashboard", | ||||||
|     "short_name": "", |     "short_name": "Reminders", | ||||||
|  |     "start_url": "/dashboard", | ||||||
|     "icons": [ |     "icons": [ | ||||||
|         { |         { | ||||||
|             "src": "/android-chrome-192x192.png", |             "src": "/static/favicon/android-chrome-192x192.png", | ||||||
|             "sizes": "192x192", |             "sizes": "192x192", | ||||||
|             "type": "image/png" |             "type": "image/png" | ||||||
|         }, |         }, | ||||||
|         { |         { | ||||||
|             "src": "/android-chrome-512x512.png", |             "src": "/static/favicon/android-chrome-512x512.png", | ||||||
|             "sizes": "512x512", |             "sizes": "512x512", | ||||||
|             "type": "image/png" |             "type": "image/png" | ||||||
|         } |         } | ||||||
| Before Width: | Height: | Size: 712 KiB After Width: | Height: | Size: 712 KiB |