Compare commits
	
		
			373 Commits
		
	
	
		
			discord-ti
			...
			6ae1096d79
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 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 | ||
|  | 1c1f5662d3 | ||
| ded750aa2d | |||
| 4c4f0927f1 | |||
|  | 0f05018cab | ||
|  | 85d27c5bba | ||
|  | d946ef1dca | ||
|  | f21d522435 | ||
|  | 3add718cdf | ||
|  | f4ef7afea0 | ||
|  | f8547bba70 | ||
|  | 08fd88ce54 | ||
|  | abfe492192 | ||
|  | afb2fbe4ff | ||
|  | 878ea11502 | ||
|  | 93da746bdc | ||
|  | 9e6a387f82 | ||
|  | af9d8bea62 | ||
|  | 318be1fa5e | ||
|  | 3b6e02e16e | ||
|  | a56f84f659 | ||
|  | 3e4dd0fa48 | ||
|  | d0d2d50966 | ||
|  | e2e5b022a0 | ||
|  | 6ae2353c92 | ||
|  | 06c4deeaa9 | ||
|  | afc376c44f | ||
|  | 84ee7e77c5 | ||
|  | 620f054703 | ||
|  | cb471c52f3 | ||
|  | 37420b2b1f | ||
|  | 49974b7153 | ||
|  | a3844dde9e | ||
| d62c8c95c2 | |||
| 05606dfec1 | |||
| 68ee42f244 | |||
| fad28faabb | |||
| e5ab99f67b | |||
| e47715917e | |||
| 4f9eb58c16 | |||
| c953bc0cd3 | |||
| 610779a293 | |||
| ebd1efa990 | |||
| 5230101a8d | |||
| d8f42c1b25 | |||
| 23c6b3869e | |||
| a21f518b21 | |||
| f1bfc11160 | |||
| 72228911f2 | |||
| db7cca6296 | |||
| e36e718f28 | |||
| 44debf93c5 | |||
| 9b54fba5e5 | |||
| 6cf660c7ee | |||
| 4490f19c04 | |||
| a362a24cfc | |||
| 903daf65e6 | |||
| b310e99085 | |||
| ebabe0e85a | |||
| 6b5d6ae288 | |||
| 379e488f7a | |||
| d84d7ab62b | |||
| a0974795e1 | |||
| a9c91bee93 | |||
| b2207e308a | |||
| 3c1eeed92f | |||
| 395a8481f1 | |||
| bae0433bd9 | |||
| 3e547861ea | |||
| 9b5333dc87 | |||
| 471948bed3 | |||
| c148cdf556 | |||
| 98aed91d21 | |||
| 40630c0014 | |||
| 85a8ae625d | |||
| 43bbcb3fe0 | |||
| 1556318d07 | |||
| ea2b0f4b0a | |||
| f02c04b313 | |||
| 320060b1bd | |||
| bef33c6dac | |||
| 7bcb3c4a70 | 
							
								
								
									
										31
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -1,7 +1,30 @@ | ||||
| /target | ||||
| target | ||||
| .env | ||||
| /venv | ||||
| .cargo | ||||
| assets | ||||
| out.json | ||||
| /.idea | ||||
| .idea | ||||
| web/static/index.html | ||||
| web/static/assets | ||||
| # Logs | ||||
| logs | ||||
| *.log | ||||
| npm-debug.log* | ||||
| yarn-debug.log* | ||||
| yarn-error.log* | ||||
| pnpm-debug.log* | ||||
| lerna-debug.log* | ||||
|  | ||||
| node_modules | ||||
| dist | ||||
| dist-ssr | ||||
| *.local | ||||
|  | ||||
| # Editor directories and files | ||||
| .vscode/* | ||||
| !.vscode/extensions.json | ||||
| .DS_Store | ||||
| *.suo | ||||
| *.ntvs* | ||||
| *.njsproj | ||||
| *.sln | ||||
| *.sw? | ||||
|   | ||||
							
								
								
									
										2
									
								
								.prettierrc.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,2 @@ | ||||
| printWidth = 90 | ||||
| tabWidth = 4 | ||||
							
								
								
									
										3818
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
							
								
								
									
										75
									
								
								Cargo.toml
									
									
									
									
									
								
							
							
						
						| @@ -1,30 +1,71 @@ | ||||
| [package] | ||||
| name = "reminder_rs" | ||||
| version = "1.5.0-2" | ||||
| authors = ["jellywx <judesouthworth@pm.me>"] | ||||
| edition = "2018" | ||||
| name = "reminder-rs" | ||||
| version = "1.7.22" | ||||
| authors = ["Jude Southworth <judesouthworth@pm.me>"] | ||||
| edition = "2021" | ||||
| license = "AGPL-3.0 only" | ||||
| description = "Reminder Bot for Discord, now in Rust" | ||||
|  | ||||
| [dependencies] | ||||
| dashmap = "4.0" | ||||
| poise = "0.6.1" | ||||
| dotenv = "0.15" | ||||
| humantime = "2.1" | ||||
| tokio = { version = "1", features = ["process", "full"] } | ||||
| reqwest = "0.11" | ||||
| regex = "1.4" | ||||
| reqwest = { version = "0.12", features = ["json"] } | ||||
| regex = "1.10" | ||||
| log = "0.4" | ||||
| env_logger = "0.8" | ||||
| env_logger = "0.11" | ||||
| chrono = "0.4" | ||||
| chrono-tz = "0.5" | ||||
| chrono-tz = { version = "0.9", features = ["serde"] } | ||||
| lazy_static = "1.4" | ||||
| num-integer = "0.1" | ||||
| serde = "1.0" | ||||
| serde_json = "1.0" | ||||
| rand = "0.7" | ||||
| Inflector = "0.11" | ||||
| serde_repr = "0.1" | ||||
| rmp-serde = "1.1" | ||||
| rand = "0.8" | ||||
| levenshtein = "1.0" | ||||
| # serenity = { version = "0.10", features = ["collector"] } | ||||
| serenity = { path = "/home/jude/serenity", features = ["collector", "unstable_discord_api"] } | ||||
| 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.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.regex_command_attr] | ||||
| path = "./regex_command_attr" | ||||
| [dependencies.extract_derive] | ||||
| path = "extract_derive" | ||||
|  | ||||
| [dependencies.recordable_derive] | ||||
| 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 | ||||
							
								
								
									
										52
									
								
								README.md
									
									
									
									
									
								
							
							
						
						| @@ -1,26 +1,42 @@ | ||||
| # reminder-rs | ||||
| Reminder Bot for Discord, now in Rust. | ||||
| Old Python version: https://github.com/reminder-bot/bot | ||||
| Reminder Bot for Discord. | ||||
|  | ||||
| ## 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. | ||||
|  | ||||
| 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 | ||||
| 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. | ||||
| ### Build APT package | ||||
|  | ||||
| #### Compilation environment variables | ||||
| These environment variables must be provided when compiling the bot | ||||
| * `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** | ||||
| * `STRINGS_FILE` - accepts the name of a compiled strings file located in `$CARGO_MANIFEST_DIR/assets/` to be used for creating messages. Compiled string files can be generated with `compile.py` at https://github.com/reminder-bot/languages | ||||
| Recommended method. | ||||
|  | ||||
| ### 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 | ||||
| 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`) | ||||
| 8. Build: `cargo build --release` | ||||
|  | ||||
|  | ||||
| ### Configuring | ||||
|  | ||||
| ### Environment Variables | ||||
| 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__ | ||||
| @@ -29,16 +45,8 @@ __Required Variables__ | ||||
|  | ||||
| __Other Variables__ | ||||
| * `MIN_INTERVAL` - default `600`, defines the shortest interval the bot should accept | ||||
| * `MAX_TIME` - default `1576800000`, defines the maximum time ahead that reminders can be set for | ||||
| * `LOCAL_TIMEZONE` - default `UTC`, necessary for calculations in the natural language processor | ||||
| * `DEFAULT_PREFIX` - default `$`, used for the default prefix on new guilds | ||||
| * `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 | ||||
| * `IGNORE_BOTS` - default `1`, if `1`, Reminder Bot will ignore all other bots | ||||
| * `PYTHON_LOCATION` - default `venv/bin/python3`. Can be changed if your Python executable is located somewhere else | ||||
| * `LOCAL_LANGUAGE` - default `EN`. Specifies the string set to fall back to if a string cannot be found (and to be used with new users) | ||||
| * `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  | ||||
| * `CASE_INSENSITIVE` - default `1`, if `1`, commands will be treated with case insensitivity (so both `$help` and `$HELP` will work) | ||||
| * `SHARD_COUNT` - default `None`, accepts the number of shards that are being ran | ||||
| * `SHARD_RANGE` - default `None`, if `SHARD_COUNT` is specified, specifies what range of shards to start on this process  | ||||
| * `DM_ENABLED` - default `1`, if `1`, Reminder Bot will respond to direct messages | ||||
|   | ||||
							
								
								
									
										28
									
								
								Rocket.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,28 @@ | ||||
| [default] | ||||
| address = "0.0.0.0" | ||||
| port = 18920 | ||||
| template_dir = "templates" | ||||
| limits = { json = "10MiB" } | ||||
|  | ||||
| [debug] | ||||
| secret_key = "tR8krio5FXTnnyIZNiJDXPondz0kI1v6X6BXZcBGIRY=" | ||||
|  | ||||
| [debug.tls] | ||||
| certs = "private/rsa_sha256_cert.pem" | ||||
| key = "private/rsa_sha256_key.pem" | ||||
|  | ||||
| [debug.rsa_sha256.tls] | ||||
| certs = "private/rsa_sha256_cert.pem" | ||||
| key = "private/rsa_sha256_key.pem" | ||||
|  | ||||
| [debug.ecdsa_nistp256_sha256.tls] | ||||
| certs = "private/ecdsa_nistp256_sha256_cert.pem" | ||||
| key = "private/ecdsa_nistp256_sha256_key_pkcs8.pem" | ||||
|  | ||||
| [debug.ecdsa_nistp384_sha384.tls] | ||||
| certs = "private/ecdsa_nistp384_sha384_cert.pem" | ||||
| key = "private/ecdsa_nistp384_sha384_key_pkcs8.pem" | ||||
|  | ||||
| [debug.ed25519.tls] | ||||
| certs = "private/ed25519_cert.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,10 +1,6 @@ | ||||
| CREATE DATABASE IF NOT EXISTS reminders; | ||||
| 
 | ||||
| SET FOREIGN_KEY_CHECKS=0; | ||||
| 
 | ||||
| USE reminders; | ||||
| 
 | ||||
| CREATE TABLE reminders.guilds ( | ||||
| CREATE TABLE guilds ( | ||||
|     id INT UNSIGNED UNIQUE NOT NULL AUTO_INCREMENT, | ||||
|     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, | ||||
| 
 | ||||
|     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, | ||||
|     channel BIGINT UNSIGNED UNIQUE NOT NULL, | ||||
| 
 | ||||
| @@ -39,10 +35,10 @@ CREATE TABLE reminders.channels ( | ||||
|     guild_id INT UNSIGNED, | ||||
| 
 | ||||
|     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, | ||||
|     user BIGINT UNSIGNED UNIQUE NOT NULL, | ||||
| 
 | ||||
| @@ -59,10 +55,10 @@ CREATE TABLE reminders.users ( | ||||
|     patreon BOOLEAN NOT NULL DEFAULT 0, | ||||
| 
 | ||||
|     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, | ||||
|     role BIGINT UNSIGNED UNIQUE NOT NULL, | ||||
| 
 | ||||
| @@ -71,10 +67,10 @@ CREATE TABLE reminders.roles ( | ||||
|     guild_id INT UNSIGNED NOT NULL, | ||||
| 
 | ||||
|     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, | ||||
| 
 | ||||
|     title VARCHAR(256) NOT NULL DEFAULT '', | ||||
| @@ -91,7 +87,7 @@ CREATE TABLE reminders.embeds ( | ||||
|     PRIMARY KEY (id) | ||||
| ); | ||||
| 
 | ||||
| CREATE TABLE reminders.embed_fields ( | ||||
| CREATE TABLE embed_fields ( | ||||
|     id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL, | ||||
| 
 | ||||
|     title VARCHAR(256) NOT NULL DEFAULT '', | ||||
| @@ -100,10 +96,10 @@ CREATE TABLE reminders.embed_fields ( | ||||
|     embed_id INT UNSIGNED NOT NULL, | ||||
| 
 | ||||
|     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, | ||||
| 
 | ||||
|     content VARCHAR(2048) NOT NULL DEFAULT '', | ||||
| @@ -114,10 +110,10 @@ CREATE TABLE reminders.messages ( | ||||
|     attachment_name VARCHAR(260), | ||||
| 
 | ||||
|     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, | ||||
|     uid VARCHAR(64) UNIQUE NOT NULL, | ||||
| 
 | ||||
| @@ -140,20 +136,20 @@ CREATE TABLE reminders.reminders ( | ||||
|     set_by INT UNSIGNED, | ||||
| 
 | ||||
|     PRIMARY KEY (id), | ||||
|     FOREIGN KEY (message_id) REFERENCES reminders.messages(id) ON DELETE RESTRICT, | ||||
|     FOREIGN KEY (channel_id) REFERENCES reminders.channels(id) ON DELETE CASCADE, | ||||
|     FOREIGN KEY (set_by) REFERENCES reminders.users(id) ON DELETE SET NULL | ||||
|     FOREIGN KEY (message_id) REFERENCES messages(id) ON DELETE RESTRICT, | ||||
|     FOREIGN KEY (channel_id) REFERENCES channels(id) ON DELETE CASCADE, | ||||
|     FOREIGN KEY (set_by) REFERENCES users(id) ON DELETE SET NULL | ||||
| ); | ||||
| 
 | ||||
| CREATE TRIGGER message_cleanup AFTER DELETE ON reminders.reminders | ||||
| CREATE TRIGGER message_cleanup AFTER DELETE ON reminders | ||||
| 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 | ||||
|     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, | ||||
|     user_id INT UNSIGNED, | ||||
|     guild_id INT UNSIGNED, | ||||
| @@ -161,23 +157,23 @@ CREATE TABLE reminders.todos ( | ||||
|     value VARCHAR(2000) NOT NULL, | ||||
| 
 | ||||
|     PRIMARY KEY (id), | ||||
|     FOREIGN KEY (user_id) REFERENCES reminders.users(id) ON DELETE SET NULL, | ||||
|     FOREIGN KEY (guild_id) REFERENCES reminders.guilds(id) ON DELETE CASCADE, | ||||
|     FOREIGN KEY (channel_id) REFERENCES reminders.channels(id) ON DELETE SET NULL | ||||
|     FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL, | ||||
|     FOREIGN KEY (guild_id) REFERENCES guilds(id) ON DELETE CASCADE, | ||||
|     FOREIGN KEY (channel_id) REFERENCES channels(id) ON DELETE SET NULL | ||||
| ); | ||||
| 
 | ||||
| CREATE TABLE reminders.command_restrictions ( | ||||
| CREATE TABLE command_restrictions ( | ||||
|     id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL, | ||||
| 
 | ||||
|     role_id INT UNSIGNED NOT NULL, | ||||
|     command ENUM('todos', 'natural', 'remind', 'interval', 'timer', 'del', 'look', 'alias', 'countdown') NOT NULL, | ||||
| 
 | ||||
|     PRIMARY KEY (id), | ||||
|     FOREIGN KEY (role_id) REFERENCES reminders.roles(id) ON DELETE CASCADE, | ||||
|     FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE, | ||||
|     UNIQUE KEY (`role_id`, `command`) | ||||
| ); | ||||
| 
 | ||||
| CREATE TABLE reminders.timers ( | ||||
| CREATE TABLE timers ( | ||||
|     id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL, | ||||
|     start_time TIMESTAMP NOT NULL DEFAULT NOW(), | ||||
|     name VARCHAR(32) NOT NULL, | ||||
| @@ -186,7 +182,7 @@ CREATE TABLE reminders.timers ( | ||||
|     PRIMARY KEY (id) | ||||
| ); | ||||
| 
 | ||||
| CREATE TABLE reminders.events ( | ||||
| CREATE TABLE events ( | ||||
|     id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL, | ||||
|     `time` TIMESTAMP NOT NULL DEFAULT NOW(), | ||||
| 
 | ||||
| @@ -198,12 +194,12 @@ CREATE TABLE reminders.events ( | ||||
|     reminder_id INT UNSIGNED, | ||||
| 
 | ||||
|     PRIMARY KEY (id), | ||||
|     FOREIGN KEY (guild_id) REFERENCES reminders.guilds(id) ON DELETE CASCADE, | ||||
|     FOREIGN KEY (user_id) REFERENCES reminders.users(id) ON DELETE SET NULL, | ||||
|     FOREIGN KEY (reminder_id) REFERENCES reminders.reminders(id) ON DELETE SET NULL | ||||
|     FOREIGN KEY (guild_id) REFERENCES guilds(id) ON DELETE CASCADE, | ||||
|     FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL, | ||||
|     FOREIGN KEY (reminder_id) REFERENCES reminders(id) ON DELETE SET NULL | ||||
| ); | ||||
| 
 | ||||
| CREATE TABLE reminders.command_aliases ( | ||||
| CREATE TABLE command_aliases ( | ||||
|     id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL, | ||||
| 
 | ||||
|     guild_id INT UNSIGNED NOT NULL, | ||||
| @@ -212,22 +208,22 @@ CREATE TABLE reminders.command_aliases ( | ||||
|     command VARCHAR(2048) NOT NULL, | ||||
| 
 | ||||
|     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`) | ||||
| ); | ||||
| 
 | ||||
| CREATE TABLE reminders.guild_users ( | ||||
| CREATE TABLE guild_users ( | ||||
|     guild INT UNSIGNED NOT NULL, | ||||
|     user INT UNSIGNED NOT NULL, | ||||
| 
 | ||||
|     can_access BOOL NOT NULL DEFAULT 0, | ||||
| 
 | ||||
|     FOREIGN KEY (guild) REFERENCES reminders.guilds(id) ON DELETE CASCADE, | ||||
|     FOREIGN KEY (user) REFERENCES reminders.users(id) ON DELETE CASCADE, | ||||
|     FOREIGN KEY (guild) REFERENCES guilds(id) ON DELETE CASCADE, | ||||
|     FOREIGN KEY (user) REFERENCES users(id) ON DELETE CASCADE, | ||||
|     UNIQUE KEY (guild, user) | ||||
| ); | ||||
| 
 | ||||
| CREATE EVENT reminders.event_cleanup | ||||
| CREATE EVENT event_cleanup | ||||
| ON SCHEDULE AT CURRENT_TIMESTAMP + INTERVAL 1 DAY | ||||
| 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; | ||||
| 
 | ||||
| DROP TABLE IF EXISTS reminders_new; | ||||
| @@ -56,8 +54,7 @@ CREATE TABLE reminders_new ( | ||||
|     -- , CONSTRAINT interval_enabled_mutin CHECK (`enabled` = 1 OR `interval` IS NULL) | ||||
|     # disallow an expiry time if interval is unspecified | ||||
|     -- , CONSTRAINT interval_expires_mutin CHECK (`expires` IS NULL OR `interval` IS NOT NULL) | ||||
| ) | ||||
| COLLATE utf8mb4_unicode_ci; | ||||
| ); | ||||
| 
 | ||||
| # import data from other tables | ||||
| INSERT INTO reminders_new ( | ||||
| @@ -158,4 +155,9 @@ CREATE TABLE events ( | ||||
|     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; | ||||
							
								
								
									
										11
									
								
								migrations/20210922000000_macro.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,11 @@ | ||||
| CREATE TABLE macro ( | ||||
|     id INT UNSIGNED AUTO_INCREMENT, | ||||
|     guild_id INT UNSIGNED NOT NULL, | ||||
|  | ||||
|     name VARCHAR(100) NOT NULL, | ||||
|     description VARCHAR(100), | ||||
|     commands TEXT NOT NULL, | ||||
|  | ||||
|     FOREIGN KEY (guild_id) REFERENCES guilds(id) ON DELETE CASCADE, | ||||
|     PRIMARY KEY (id) | ||||
| ); | ||||
| @@ -0,0 +1,2 @@ | ||||
| ALTER TABLE reminders RENAME COLUMN `interval` TO `interval_seconds`; | ||||
| ALTER TABLE reminders ADD COLUMN `interval_months` INT UNSIGNED DEFAULT NULL; | ||||
							
								
								
									
										49
									
								
								migrations/20220211000000_reminder_templates.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,49 @@ | ||||
| CREATE TABLE reminder_template ( | ||||
|     `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, | ||||
|  | ||||
|     `name` VARCHAR(24) NOT NULL DEFAULT 'Reminder', | ||||
|  | ||||
|     `guild_id` INT UNSIGNED NOT NULL, | ||||
|  | ||||
|     `username` VARCHAR(32) DEFAULT NULL, | ||||
|     `avatar` VARCHAR(512) DEFAULT NULL, | ||||
|  | ||||
|     `content` VARCHAR(2048) NOT NULL DEFAULT '', | ||||
|     `tts` BOOL NOT NULL DEFAULT 0, | ||||
|     `attachment` MEDIUMBLOB, | ||||
|     `attachment_name` VARCHAR(260), | ||||
|  | ||||
|     `embed_title` VARCHAR(256) NOT NULL DEFAULT '', | ||||
|     `embed_description` VARCHAR(2048) NOT NULL DEFAULT '', | ||||
|     `embed_image_url` VARCHAR(512), | ||||
|     `embed_thumbnail_url` VARCHAR(512), | ||||
|     `embed_footer` VARCHAR(2048) NOT NULL DEFAULT '', | ||||
|     `embed_footer_url` VARCHAR(512), | ||||
|     `embed_author` VARCHAR(256) NOT NULL DEFAULT '', | ||||
|     `embed_author_url` VARCHAR(512), | ||||
|     `embed_color` INT UNSIGNED NOT NULL DEFAULT 0x0, | ||||
|     `embed_fields` JSON, | ||||
|  | ||||
|     PRIMARY KEY (id), | ||||
|  | ||||
|     FOREIGN KEY (`guild_id`) REFERENCES guilds (`id`) ON DELETE CASCADE | ||||
| ); | ||||
|  | ||||
| ALTER TABLE reminders ADD COLUMN embed_fields JSON; | ||||
|  | ||||
| update reminders | ||||
|     inner join embed_fields as E | ||||
|     on E.reminder_id = reminders.id | ||||
| set embed_fields = ( | ||||
|     select JSON_ARRAYAGG( | ||||
|         JSON_OBJECT( | ||||
|             'title', E.title, | ||||
|             'value', E.value, | ||||
|             'inline', | ||||
|             if(inline = 1, cast(TRUE as json), cast(FALSE as json)) | ||||
|             ) | ||||
|         ) | ||||
|     from embed_fields | ||||
|     group by reminder_id | ||||
|     having reminder_id = reminders.id | ||||
|     ); | ||||
							
								
								
									
										1
									
								
								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; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										32
									
								
								private/ca_cert.pem
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,32 @@ | ||||
| -----BEGIN CERTIFICATE----- | ||||
| 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 | ||||
| I4/u | ||||
| -----END CERTIFICATE----- | ||||
							
								
								
									
										51
									
								
								private/ca_key.pem
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,51 @@ | ||||
| -----BEGIN RSA PRIVATE KEY----- | ||||
| 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 | ||||
| -----END RSA PRIVATE KEY----- | ||||
							
								
								
									
										21
									
								
								private/ecdsa_nistp256_sha256_cert.pem
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,21 @@ | ||||
| -----BEGIN CERTIFICATE----- | ||||
| MIIDYTCCAUmgAwIBAgIUA/EaCcXKgOxBiuaMNTackeF9DgcwDQYJKoZIhvcNAQEL | ||||
| BQAwRzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRIwEAYDVQQKDAlSb2NrZXQg | ||||
| Q0ExFzAVBgNVBAMMDlJvY2tldCBSb290IENBMB4XDTIxMDUxNzE5MDIyNFoXDTMx | ||||
| MDUxNTE5MDIyNFowPzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMQ8wDQYDVQQK | ||||
| DAZSb2NrZXQxEjAQBgNVBAMMCWxvY2FsaG9zdDBZMBMGByqGSM49AgEGCCqGSM49 | ||||
| AwEHA0IABOEVdmVd218y3Yy+ocjtt5siaFsNR7tknvS21pzKzxsFc05UrF74YqRx | ||||
| Ic6/AQq56C48x6gDhUCdf+nMzD0NWTijGDAWMBQGA1UdEQQNMAuCCWxvY2FsaG9z | ||||
| dDANBgkqhkiG9w0BAQsFAAOCAgEAW32kadHWEIsN5WXacjtgE9jbFii/rWJjrAO/ | ||||
| GWuARwLvjWeEFM5xSTny1cTDEz/7Bmfd9GRRpfgxKCFBGaU7zTmOV/AokrjUay+s | ||||
| KPj0EV9ZE/84KVfr+MOuhwXhSSv+NvxduFw1sdhTUHs+Wopwjte+bnOUBXmnQH97 | ||||
| ITSQuUvEamPUS4tJD/kQ0qSGJKNv3CShhbe3XkjUd0UKK7lZzn6hY2fy3CrkkJDT | ||||
| GyzFCRf3ZDfjW5/fGXN/TNoEK65pPQVr5taK/bovDesEO7rR4Y4YgtnGlJ7YToWh | ||||
| E6I77CbsJCJdmgpjPNwRlwQ8upoWAfxOHqwg32s6uWq20v4TXZTOnEKOPEJG74bh | ||||
| JHtwqsy2fIdGz4kZksKGerzJffyQPhX4f3qxxrijT9Hj2EoFIQ4u/jTRpK31IW3R | ||||
| gEeNB8O4iyg3crx0oHRwc6zX63O6wBJCZ+0uxLoyjwtjKCxVYbJpccjoQh6zO3UO | ||||
| pqAO+NKxQ469QDBV3uRyyQ7DRPu6p67/8gn1E/o8kyU1P7eLFIhBp4T4wCaMQBG6 | ||||
| IpL5W7eCyuOcqfdm471N07Z4KAqiV8eBJcKaAE+WzNIvrFvCZ/zq7Gj8p07nkjK8 | ||||
| +ZGDUieiY0mQEpiXLQZt1xNtT/MmlAzFYrK7t6gSygykQKjO1o4GG4g+eXu3h1YK | ||||
| avsOwtc= | ||||
| -----END CERTIFICATE----- | ||||
							
								
								
									
										5
									
								
								private/ecdsa_nistp256_sha256_key_pkcs8.pem
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,5 @@ | ||||
| -----BEGIN PRIVATE KEY----- | ||||
| MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgeo/7wBIYVax9bj4m | ||||
| 1UEe2H+H4YLsbApDCZMslZbubRuhRANCAAThFXZlXdtfMt2MvqHI7bebImhbDUe7 | ||||
| ZJ70ttacys8bBXNOVKxe+GKkcSHOvwEKueguPMeoA4VAnX/pzMw9DVk4 | ||||
| -----END PRIVATE KEY----- | ||||
							
								
								
									
										21
									
								
								private/ecdsa_nistp384_sha384_cert.pem
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,21 @@ | ||||
| -----BEGIN CERTIFICATE----- | ||||
| MIIDfjCCAWagAwIBAgIUfmkM99JpQUsQsh8TVILPQDBGOhowDQYJKoZIhvcNAQEM | ||||
| BQAwRzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRIwEAYDVQQKDAlSb2NrZXQg | ||||
| Q0ExFzAVBgNVBAMMDlJvY2tldCBSb290IENBMB4XDTIxMDUxNzE5MDIyNFoXDTMx | ||||
| MDUxNTE5MDIyNFowPzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMQ8wDQYDVQQK | ||||
| DAZSb2NrZXQxEjAQBgNVBAMMCWxvY2FsaG9zdDB2MBAGByqGSM49AgEGBSuBBAAi | ||||
| A2IABM5rQQNZEGWeDyrg2dVQS15c+JsX/ginN9/ArHpTgGO6xaLfkbekIj7gewUR | ||||
| VQcQrYulwu02HTFDzGVvf1DdCZzIJLJUpl+jagdI05yMPFg2s5lThzGB6HENyw1I | ||||
| hU1dMaMYMBYwFAYDVR0RBA0wC4IJbG9jYWxob3N0MA0GCSqGSIb3DQEBDAUAA4IC | ||||
| AQAzpXFEeCMvTBG+kzmHN/+esjCK8MO/Grrw3Qoa1stX0yjNWzfhlGHfWk9Mgwnp | ||||
| DnIEFT9lB+nT9uTkKL81Opu2bkWXuL0MyjyYmcCfEgJeDblUbD4HYzPO/s2P7QZu | ||||
| Oa1pNKBiSWe1xq+F/gkn0+nDfICq3QQOOQMzaAmlFmszN3v7pryz+x21MqTFsfuW | ||||
| ymycRcwZ3VyYeceZuBxjOhR2l8I5yZbR+KPj8VS7A3vgqvkS8ixTK0LrxcLwrTIz | ||||
| W9Z1skzVpux4K7iwn3qlWIJ7EbERi9Ydz0MzpjD+CHxGlgHTzT552DGZhPO8D7BE | ||||
| +4IoS0YeXEGLeC2a9Hmiy3FXbM4nt3aIWMiWyhjslXIbvWOJL1Vi5GNpdQCG+rB7 | ||||
| lvA0IISp/tAi9duufdONjgRceRDsqf7o6TOBQdB3QxHRt9gCB2QquRh5llMUY8YH | ||||
| PxFJAEzF4X5b0q0k4b50HRVNfDY0SC/XIR7S2xnLDGuoUqrOpUligBzfXV4JHrPv | ||||
| YKHsWSOiVxU9eY1SYKuKqnOsGvXSSQlYqSK1ol94eOKDjNy8wekaVYAQNsdNL7o5 | ||||
| QSWZUcbZLkaNMFdD9twWGduAs0eTichVgyvRgPdevjTGDPBHKNSUURZRTYeW4aIJ | ||||
| QzSQUQXwOXsQOmfCkaaTISsDwZAJ0wFqqST1AOhlCWD1OQ== | ||||
| -----END CERTIFICATE----- | ||||
							
								
								
									
										6
									
								
								private/ecdsa_nistp384_sha384_key_pkcs8.pem
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,6 @@ | ||||
| -----BEGIN PRIVATE KEY----- | ||||
| MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDCRBt7MeJXyJRq7grGQ | ||||
| jEdBHE31hG5LuKQ5iL8yTVGk75Kc8ISll2LewbvQ3NhR6E+hZANiAATOa0EDWRBl | ||||
| ng8q4NnVUEteXPibF/4IpzffwKx6U4BjusWi35G3pCI+4HsFEVUHEK2LpcLtNh0x | ||||
| Q8xlb39Q3QmcyCSyVKZfo2oHSNOcjDxYNrOZU4cxgehxDcsNSIVNXTE= | ||||
| -----END PRIVATE KEY----- | ||||
							
								
								
									
										20
									
								
								private/ed25519_cert.pem
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,20 @@ | ||||
| -----BEGIN CERTIFICATE----- | ||||
| MIIDMjCCARqgAwIBAgIUSu1CGfaWlNPjjLG7SaEEgxI+AsAwDQYJKoZIhvcNAQEL | ||||
| BQAwRzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRIwEAYDVQQKDAlSb2NrZXQg | ||||
| Q0ExFzAVBgNVBAMMDlJvY2tldCBSb290IENBMB4XDTIxMDUxNzE5MDIyNFoXDTMx | ||||
| MDUxNTE5MDIyNFowPzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMQ8wDQYDVQQK | ||||
| DAZSb2NrZXQxEjAQBgNVBAMMCWxvY2FsaG9zdDAqMAUGAytlcAMhAKNv1fkv5rxY | ||||
| xGT84C98g2xPpain+jsliHvYrqNkS1zhoxgwFjAUBgNVHREEDTALgglsb2NhbGhv | ||||
| c3QwDQYJKoZIhvcNAQELBQADggIBAMgWZhr3whYbFPNYm5RZxjgEonMY7cH/Rza1 | ||||
| UrBkyoMRL9v00YKJTEvjl5Xl4ku7jqxY2qjValacDroD8hYTLhhkkbHxH6u+kJSC | ||||
| cKRQ8QVBZMmoor8qD2Y2BuXZYGWEtgeoXh+DLHWOim7gXDD6GyFPnYFGHnRhNmkE | ||||
| 6fPcfw1FZmBrMM9rk31EeK9nLVRabL+vuLDEddX1LH4LwK4OuV51wQMZGYiC3M2b | ||||
| JpmuPAUAUyiyN53Utuw/ubeih3cEglOwCyDPFt6XISYHO0UaQdw25uhpjxs8KTZB | ||||
| qOPJAI4cvGaN3GARXvz2P/QHUiTzsf2XOXXtrO0g+F9P7t6x2xL35c0A8yxKkfsa | ||||
| RKdCveRuVlsLXVLVBikqLE/OgPC6SIhIFvTYzXTaZyNfge6dRy2Wg6qWPcGwseeA | ||||
| QpFKgWiY3cuy7nJm6uybR6zOEMj5GgNWIbICGguQ5161MLnv0bEmKh2d9/jQ/XN5 | ||||
| M+nixtGla/G4hL3FQIy9rhHVZzPWwSxl+/eE4qgnInEhmlQ2KrcpgneCt38t0vjJ | ||||
| dwHZSzVv8yX7fE0OSq/DWQXJraWUcT7Ds0g7T7jWkWfS0qStVd9VJ+rYQ8dBbE9Y | ||||
| gcmkm1DMUd+y9xDRDjxby7gMDWFiN2Au9FQgaIpIctTNCj0+agFBPnfXPVSIH6gX | ||||
| 10kA2ZVX | ||||
| -----END CERTIFICATE----- | ||||
							
								
								
									
										3
									
								
								private/ed25519_key.pem
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,3 @@ | ||||
| -----BEGIN PRIVATE KEY----- | ||||
| MC4CAQAwBQYDK2VwBCIEIGtLkaYE4D+P5HLkoc3a/tNhPP+gnhPLF3jEtdYcAtpd | ||||
| -----END PRIVATE KEY----- | ||||
							
								
								
									
										114
									
								
								private/gen_certs.sh
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,114 @@ | ||||
| #! /bin/bash | ||||
|  | ||||
| # Usage: | ||||
| #   ./gen_certs.sh [cert-kind] | ||||
| # | ||||
| # [cert-kind]: | ||||
| #   ed25519 | ||||
| #   rsa_sha256 | ||||
| #   ecdsa_nistp256_sha256 | ||||
| #   ecdsa_nistp384_sha384 | ||||
| # | ||||
| # Generate a certificate of the [cert-kind] key type, or if no cert-kind is | ||||
| # specified, all of the certificates. | ||||
| # | ||||
| # Examples: | ||||
| #   ./gen_certs.sh ed25519 | ||||
| #   ./gen_certs.sh rsa_sha256 | ||||
|  | ||||
| # TODO: `rustls` (really, `webpki`) doesn't currently use the CN in the subject | ||||
| # to check if a certificate is valid for a server name sent via SNI. It's not | ||||
| # clear if this is intended, since certificates _should_ have a `subjectAltName` | ||||
| # with a DNS name, or if it simply hasn't been implemented yet. See | ||||
| # https://bugzilla.mozilla.org/show_bug.cgi?id=552346 for a bit more info. | ||||
|  | ||||
| CA_SUBJECT="/C=US/ST=CA/O=Rocket CA/CN=Rocket Root CA" | ||||
| SUBJECT="/C=US/ST=CA/O=Rocket/CN=localhost" | ||||
| ALT="DNS:localhost" | ||||
|  | ||||
| function gen_ca() { | ||||
|   openssl genrsa -out ca_key.pem 4096 | ||||
|   openssl req -new -x509 -days 3650 -key ca_key.pem \ | ||||
|     -subj "${CA_SUBJECT}" -out ca_cert.pem | ||||
| } | ||||
|  | ||||
| function gen_ca_if_non_existent() { | ||||
|   if ! [ -f ./ca_cert.pem ]; then gen_ca; fi | ||||
| } | ||||
|  | ||||
| function gen_rsa_sha256() { | ||||
|   gen_ca_if_non_existent | ||||
|  | ||||
|   openssl req -newkey rsa:4096 -nodes -sha256 -keyout rsa_sha256_key.pem \ | ||||
|     -subj "${SUBJECT}" -out server.csr | ||||
|  | ||||
|   openssl x509 -req -sha256 -extfile <(printf "subjectAltName=${ALT}") -days 3650 \ | ||||
|     -CA ca_cert.pem -CAkey ca_key.pem -CAcreateserial \ | ||||
|     -in server.csr -out rsa_sha256_cert.pem | ||||
|  | ||||
|   rm ca_cert.srl server.csr | ||||
| } | ||||
|  | ||||
| function gen_ed25519() { | ||||
|   gen_ca_if_non_existent | ||||
|  | ||||
|   openssl genpkey -algorithm ED25519 > ed25519_key.pem | ||||
|  | ||||
|   openssl req -new -key ed25519_key.pem -subj "${SUBJECT}" -out server.csr | ||||
|   openssl x509 -req -extfile <(printf "subjectAltName=${ALT}") -days 3650 \ | ||||
|     -CA ca_cert.pem -CAkey ca_key.pem -CAcreateserial \ | ||||
|     -in server.csr -out ed25519_cert.pem | ||||
|  | ||||
|   rm ca_cert.srl server.csr | ||||
| } | ||||
|  | ||||
| function gen_ecdsa_nistp256_sha256() { | ||||
|   gen_ca_if_non_existent | ||||
|  | ||||
|   openssl ecparam -out ecdsa_nistp256_sha256_key.pem -name prime256v1 -genkey | ||||
|  | ||||
|   # Convert to pkcs8 format supported by rustls | ||||
|   openssl pkcs8 -topk8 -nocrypt -in ecdsa_nistp256_sha256_key.pem \ | ||||
|     -out ecdsa_nistp256_sha256_key_pkcs8.pem | ||||
|  | ||||
|   openssl req -new -nodes -sha256 -key ecdsa_nistp256_sha256_key_pkcs8.pem \ | ||||
|     -subj "${SUBJECT}" -out server.csr | ||||
|  | ||||
|   openssl x509 -req -sha256 -extfile <(printf "subjectAltName=${ALT}") -days 3650 \ | ||||
|     -CA ca_cert.pem -CAkey ca_key.pem -CAcreateserial \ | ||||
|     -in server.csr -out ecdsa_nistp256_sha256_cert.pem | ||||
|  | ||||
|   rm ca_cert.srl server.csr ecdsa_nistp256_sha256_key.pem | ||||
| } | ||||
|  | ||||
| function gen_ecdsa_nistp384_sha384() { | ||||
|   gen_ca_if_non_existent | ||||
|  | ||||
|   openssl ecparam -out ecdsa_nistp384_sha384_key.pem -name secp384r1 -genkey | ||||
|  | ||||
|   # Convert to pkcs8 format supported by rustls | ||||
|   openssl pkcs8 -topk8 -nocrypt -in ecdsa_nistp384_sha384_key.pem \ | ||||
|     -out ecdsa_nistp384_sha384_key_pkcs8.pem | ||||
|  | ||||
|   openssl req -new -nodes -sha384 -key ecdsa_nistp384_sha384_key_pkcs8.pem \ | ||||
|     -subj "${SUBJECT}" -out server.csr | ||||
|  | ||||
|   openssl x509 -req -sha384 -extfile <(printf "subjectAltName=${ALT}") -days 3650 \ | ||||
|     -CA ca_cert.pem -CAkey ca_key.pem -CAcreateserial \ | ||||
|     -in server.csr -out ecdsa_nistp384_sha384_cert.pem | ||||
|  | ||||
|   rm ca_cert.srl server.csr ecdsa_nistp384_sha384_key.pem | ||||
| } | ||||
|  | ||||
| case $1 in | ||||
|   ed25519) gen_ed25519 ;; | ||||
|   rsa_sha256) gen_rsa_sha256 ;; | ||||
|   ecdsa_nistp256_sha256) gen_ecdsa_nistp256_sha256 ;; | ||||
|   ecdsa_nistp384_sha384) gen_ecdsa_nistp384_sha384 ;; | ||||
|   *) | ||||
|     gen_ed25519 | ||||
|     gen_rsa_sha256 | ||||
|     gen_ecdsa_nistp256_sha256 | ||||
|     gen_ecdsa_nistp384_sha384 | ||||
|     ;; | ||||
| esac | ||||
							
								
								
									
										30
									
								
								private/rsa_sha256_cert.pem
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,30 @@ | ||||
| -----BEGIN CERTIFICATE----- | ||||
| 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 | ||||
| -----END CERTIFICATE----- | ||||
							
								
								
									
										52
									
								
								private/rsa_sha256_key.pem
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,52 @@ | ||||
| -----BEGIN PRIVATE KEY----- | ||||
| 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 | ||||
| ce4G+zZOOYXwvWGJLwNhgsve8C3oqg== | ||||
| -----END PRIVATE KEY----- | ||||
							
								
								
									
										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"); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,14 +0,0 @@ | ||||
| [package] | ||||
| name = "regex_command_attr" | ||||
| version = "0.2.0" | ||||
| authors = ["acdenisSK <acdenissk69@gmail.com>", "jellywx <judesouthworth@pm.me>"] | ||||
| edition = "2018" | ||||
| description = "Procedural macros for command creation for the RegexFramework for serenity." | ||||
|  | ||||
| [lib] | ||||
| proc-macro = true | ||||
|  | ||||
| [dependencies] | ||||
| quote = "^1.0" | ||||
| syn = { version = "^1.0", features = ["full", "derive", "extra-traits"] } | ||||
| proc-macro2 = "1.0" | ||||
| @@ -1,293 +0,0 @@ | ||||
| use proc_macro2::Span; | ||||
| use syn::parse::{Error, Result}; | ||||
| use syn::spanned::Spanned; | ||||
| use syn::{Attribute, Ident, Lit, LitStr, Meta, NestedMeta, Path}; | ||||
|  | ||||
| use crate::structures::PermissionLevel; | ||||
| use crate::util::{AsOption, LitExt}; | ||||
|  | ||||
| use std::fmt::{self, Write}; | ||||
|  | ||||
| #[derive(Debug, Clone, Copy, PartialEq)] | ||||
| pub enum ValueKind { | ||||
|     // #[<name>] | ||||
|     Name, | ||||
|  | ||||
|     // #[<name> = <value>] | ||||
|     Equals, | ||||
|  | ||||
|     // #[<name>([<value>, <value>, <value>, ...])] | ||||
|     List, | ||||
|  | ||||
|     // #[<name>(<value>)] | ||||
|     SingleList, | ||||
| } | ||||
|  | ||||
| impl fmt::Display for ValueKind { | ||||
|     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||||
|         match self { | ||||
|             ValueKind::Name => f.pad("`#[<name>]`"), | ||||
|             ValueKind::Equals => f.pad("`#[<name> = <value>]`"), | ||||
|             ValueKind::List => f.pad("`#[<name>([<value>, <value>, <value>, ...])]`"), | ||||
|             ValueKind::SingleList => f.pad("`#[<name>(<value>)]`"), | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| fn to_ident(p: Path) -> Result<Ident> { | ||||
|     if p.segments.is_empty() { | ||||
|         return Err(Error::new( | ||||
|             p.span(), | ||||
|             "cannot convert an empty path to an identifier", | ||||
|         )); | ||||
|     } | ||||
|  | ||||
|     if p.segments.len() > 1 { | ||||
|         return Err(Error::new( | ||||
|             p.span(), | ||||
|             "the path must not have more than one segment", | ||||
|         )); | ||||
|     } | ||||
|  | ||||
|     if !p.segments[0].arguments.is_empty() { | ||||
|         return Err(Error::new( | ||||
|             p.span(), | ||||
|             "the singular path segment must not have any arguments", | ||||
|         )); | ||||
|     } | ||||
|  | ||||
|     Ok(p.segments[0].ident.clone()) | ||||
| } | ||||
|  | ||||
| #[derive(Debug)] | ||||
| pub struct Values { | ||||
|     pub name: Ident, | ||||
|     pub literals: Vec<Lit>, | ||||
|     pub kind: ValueKind, | ||||
|     pub span: Span, | ||||
| } | ||||
|  | ||||
| impl Values { | ||||
|     #[inline] | ||||
|     pub fn new(name: Ident, kind: ValueKind, literals: Vec<Lit>, span: Span) -> Self { | ||||
|         Values { | ||||
|             name, | ||||
|             literals, | ||||
|             kind, | ||||
|             span, | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| pub fn parse_values(attr: &Attribute) -> Result<Values> { | ||||
|     let meta = attr.parse_meta()?; | ||||
|  | ||||
|     match meta { | ||||
|         Meta::Path(path) => { | ||||
|             let name = to_ident(path)?; | ||||
|  | ||||
|             Ok(Values::new(name, ValueKind::Name, Vec::new(), attr.span())) | ||||
|         } | ||||
|         Meta::List(meta) => { | ||||
|             let name = to_ident(meta.path)?; | ||||
|             let nested = meta.nested; | ||||
|  | ||||
|             if nested.is_empty() { | ||||
|                 return Err(Error::new(attr.span(), "list cannot be empty")); | ||||
|             } | ||||
|  | ||||
|             let mut lits = Vec::with_capacity(nested.len()); | ||||
|  | ||||
|             for meta in nested { | ||||
|                 match meta { | ||||
|                     NestedMeta::Lit(l) => lits.push(l), | ||||
|                     NestedMeta::Meta(m) => match m { | ||||
|                         Meta::Path(path) => { | ||||
|                             let i = to_ident(path)?; | ||||
|                             lits.push(Lit::Str(LitStr::new(&i.to_string(), i.span()))) | ||||
|                         } | ||||
|                         Meta::List(_) | Meta::NameValue(_) => { | ||||
|                             return Err(Error::new(attr.span(), "cannot nest a list; only accept literals and identifiers at this level")) | ||||
|                         } | ||||
|                     }, | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             let kind = if lits.len() == 1 { | ||||
|                 ValueKind::SingleList | ||||
|             } else { | ||||
|                 ValueKind::List | ||||
|             }; | ||||
|  | ||||
|             Ok(Values::new(name, kind, lits, attr.span())) | ||||
|         } | ||||
|         Meta::NameValue(meta) => { | ||||
|             let name = to_ident(meta.path)?; | ||||
|             let lit = meta.lit; | ||||
|  | ||||
|             Ok(Values::new(name, ValueKind::Equals, vec![lit], attr.span())) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Clone)] | ||||
| struct DisplaySlice<'a, T>(&'a [T]); | ||||
|  | ||||
| impl<'a, T: fmt::Display> fmt::Display for DisplaySlice<'a, T> { | ||||
|     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||||
|         let mut iter = self.0.iter().enumerate(); | ||||
|  | ||||
|         match iter.next() { | ||||
|             None => f.write_str("nothing")?, | ||||
|             Some((idx, elem)) => { | ||||
|                 write!(f, "{}: {}", idx, elem)?; | ||||
|  | ||||
|                 for (idx, elem) in iter { | ||||
|                     f.write_char('\n')?; | ||||
|                     write!(f, "{}: {}", idx, elem)?; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         Ok(()) | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[inline] | ||||
| fn is_form_acceptable(expect: &[ValueKind], kind: ValueKind) -> bool { | ||||
|     if expect.contains(&ValueKind::List) && kind == ValueKind::SingleList { | ||||
|         true | ||||
|     } else { | ||||
|         expect.contains(&kind) | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[inline] | ||||
| fn validate(values: &Values, forms: &[ValueKind]) -> Result<()> { | ||||
|     if !is_form_acceptable(forms, values.kind) { | ||||
|         return Err(Error::new( | ||||
|             values.span, | ||||
|             // Using the `_args` version here to avoid an allocation. | ||||
|             format_args!( | ||||
|                 "the attribute must be in of these forms:\n{}", | ||||
|                 DisplaySlice(forms) | ||||
|             ), | ||||
|         )); | ||||
|     } | ||||
|  | ||||
|     Ok(()) | ||||
| } | ||||
|  | ||||
| #[inline] | ||||
| pub fn parse<T: AttributeOption>(values: Values) -> Result<T> { | ||||
|     T::parse(values) | ||||
| } | ||||
|  | ||||
| pub trait AttributeOption: Sized { | ||||
|     fn parse(values: Values) -> Result<Self>; | ||||
| } | ||||
|  | ||||
| impl AttributeOption for Vec<String> { | ||||
|     fn parse(values: Values) -> Result<Self> { | ||||
|         validate(&values, &[ValueKind::List])?; | ||||
|  | ||||
|         Ok(values | ||||
|             .literals | ||||
|             .into_iter() | ||||
|             .map(|lit| lit.to_str()) | ||||
|             .collect()) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl AttributeOption for String { | ||||
|     #[inline] | ||||
|     fn parse(values: Values) -> Result<Self> { | ||||
|         validate(&values, &[ValueKind::Equals, ValueKind::SingleList])?; | ||||
|  | ||||
|         Ok(values.literals[0].to_str()) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl AttributeOption for bool { | ||||
|     #[inline] | ||||
|     fn parse(values: Values) -> Result<Self> { | ||||
|         validate(&values, &[ValueKind::Name, ValueKind::SingleList])?; | ||||
|  | ||||
|         Ok(values.literals.get(0).map_or(true, |l| l.to_bool())) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl AttributeOption for Ident { | ||||
|     #[inline] | ||||
|     fn parse(values: Values) -> Result<Self> { | ||||
|         validate(&values, &[ValueKind::SingleList])?; | ||||
|  | ||||
|         Ok(values.literals[0].to_ident()) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl AttributeOption for Vec<Ident> { | ||||
|     #[inline] | ||||
|     fn parse(values: Values) -> Result<Self> { | ||||
|         validate(&values, &[ValueKind::List])?; | ||||
|  | ||||
|         Ok(values.literals.into_iter().map(|l| l.to_ident()).collect()) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl AttributeOption for Option<String> { | ||||
|     fn parse(values: Values) -> Result<Self> { | ||||
|         validate(&values, &[ValueKind::Name, ValueKind::Equals, ValueKind::SingleList])?; | ||||
|  | ||||
|         Ok(values.literals.get(0).map(|l| l.to_str())) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl AttributeOption for PermissionLevel { | ||||
|     fn parse(values: Values) -> Result<Self> { | ||||
|         validate(&values, &[ValueKind::SingleList])?; | ||||
|  | ||||
|         Ok(values.literals.get(0).map(|l| PermissionLevel::from_str(&*l.to_str()).unwrap()).unwrap()) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl<T: AttributeOption> AttributeOption for AsOption<T> { | ||||
|     #[inline] | ||||
|     fn parse(values: Values) -> Result<Self> { | ||||
|         Ok(AsOption(Some(T::parse(values)?))) | ||||
|     } | ||||
| } | ||||
|  | ||||
| macro_rules! attr_option_num { | ||||
|     ($($n:ty),*) => { | ||||
|         $( | ||||
|             impl AttributeOption for $n { | ||||
|                 fn parse(values: Values) -> Result<Self> { | ||||
|                     validate(&values, &[ValueKind::SingleList])?; | ||||
|  | ||||
|                     Ok(match &values.literals[0] { | ||||
|                         Lit::Int(l) => l.base10_parse::<$n>()?, | ||||
|                         l => { | ||||
|                             let s = l.to_str(); | ||||
|                             // Use `as_str` to guide the compiler to use `&str`'s parse method. | ||||
|                             // We don't want to use our `parse` method here (`impl AttributeOption for String`). | ||||
|                             match s.as_str().parse::<$n>() { | ||||
|                                 Ok(n) => n, | ||||
|                                 Err(_) => return Err(Error::new(l.span(), "invalid integer")), | ||||
|                             } | ||||
|                         } | ||||
|                     }) | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             impl AttributeOption for Option<$n> { | ||||
|                 #[inline] | ||||
|                 fn parse(values: Values) -> Result<Self> { | ||||
|                     <$n as AttributeOption>::parse(values).map(Some) | ||||
|                 } | ||||
|             } | ||||
|         )* | ||||
|     } | ||||
| } | ||||
|  | ||||
| attr_option_num!(u16, u32, usize); | ||||
| @@ -1,5 +0,0 @@ | ||||
| pub mod suffixes { | ||||
|     pub const COMMAND: &str = "COMMAND"; | ||||
| } | ||||
|  | ||||
| pub use self::suffixes::*; | ||||
| @@ -1,102 +0,0 @@ | ||||
| #![deny(rust_2018_idioms)] | ||||
| // FIXME: Remove this in a foreseeable future. | ||||
| // Currently exists for backwards compatibility to previous Rust versions. | ||||
| #![recursion_limit = "128"] | ||||
|  | ||||
| #[allow(unused_extern_crates)] | ||||
| extern crate proc_macro; | ||||
|  | ||||
| use proc_macro::TokenStream; | ||||
| use quote::quote; | ||||
| use syn::{parse::Error, parse_macro_input, spanned::Spanned, Lit}; | ||||
|  | ||||
| pub(crate) mod attributes; | ||||
| pub(crate) mod consts; | ||||
| pub(crate) mod structures; | ||||
|  | ||||
| #[macro_use] | ||||
| pub(crate) mod util; | ||||
|  | ||||
| use attributes::*; | ||||
| use consts::*; | ||||
| use structures::*; | ||||
| use util::*; | ||||
|  | ||||
| macro_rules! match_options { | ||||
|     ($v:expr, $values:ident, $options:ident, $span:expr => [$($name:ident);*]) => { | ||||
|         match $v { | ||||
|             $( | ||||
|                 stringify!($name) => $options.$name = propagate_err!($crate::attributes::parse($values)), | ||||
|             )* | ||||
|             _ => { | ||||
|                 return Error::new($span, format_args!("invalid attribute: {:?}", $v)) | ||||
|                     .to_compile_error() | ||||
|                     .into(); | ||||
|             }, | ||||
|         } | ||||
|     }; | ||||
| } | ||||
|  | ||||
| #[proc_macro_attribute] | ||||
| pub fn command(attr: TokenStream, input: TokenStream) -> TokenStream { | ||||
|     let mut fun = parse_macro_input!(input as CommandFun); | ||||
|  | ||||
|     let lit_name = if !attr.is_empty() { | ||||
|         parse_macro_input!(attr as Lit).to_str() | ||||
|     } else { | ||||
|         fun.name.to_string() | ||||
|     }; | ||||
|  | ||||
|     let mut options = Options::new(); | ||||
|  | ||||
|     for attribute in &fun.attributes { | ||||
|         let span = attribute.span(); | ||||
|         let values = propagate_err!(parse_values(attribute)); | ||||
|  | ||||
|         let name = values.name.to_string(); | ||||
|         let name = &name[..]; | ||||
|  | ||||
|         match_options!(name, values, options, span => [ | ||||
|             permission_level; | ||||
|             supports_dm; | ||||
|             can_blacklist | ||||
|         ]); | ||||
|     } | ||||
|  | ||||
|     let Options { | ||||
|         permission_level, | ||||
|         supports_dm, | ||||
|         can_blacklist, | ||||
|     } = options; | ||||
|  | ||||
|     let visibility = fun.visibility; | ||||
|     let name = fun.name.clone(); | ||||
|     let body = fun.body; | ||||
|  | ||||
|     let n = name.with_suffix(COMMAND); | ||||
|  | ||||
|     let cooked = fun.cooked.clone(); | ||||
|  | ||||
|     let command_path = quote!(crate::framework::Command); | ||||
|  | ||||
|     populate_fut_lifetimes_on_refs(&mut fun.args); | ||||
|     let args = fun.args; | ||||
|  | ||||
|     (quote! { | ||||
|         #(#cooked)* | ||||
|         pub static #n: #command_path = #command_path { | ||||
|             func: #name, | ||||
|             name: #lit_name, | ||||
|             required_perms: #permission_level, | ||||
|             supports_dm: #supports_dm, | ||||
|             can_blacklist: #can_blacklist, | ||||
|         }; | ||||
|  | ||||
|         #visibility fn #name<'fut> (#(#args),*) -> ::serenity::futures::future::BoxFuture<'fut, ()> { | ||||
|             use ::serenity::futures::future::FutureExt; | ||||
|  | ||||
|             async move { #(#body)* }.boxed() | ||||
|         } | ||||
|     }) | ||||
|     .into() | ||||
| } | ||||
| @@ -1,231 +0,0 @@ | ||||
| use crate::util::{Argument, Parenthesised}; | ||||
| use proc_macro2::Span; | ||||
| use proc_macro2::TokenStream as TokenStream2; | ||||
| use quote::{quote, ToTokens}; | ||||
| use syn::{ | ||||
|     braced, | ||||
|     parse::{Error, Parse, ParseStream, Result}, | ||||
|     spanned::Spanned, | ||||
|     Attribute, Block, FnArg, Ident, Pat, Path, PathSegment, Stmt, Token, Visibility, | ||||
| }; | ||||
|  | ||||
| fn parse_argument(arg: FnArg) -> Result<Argument> { | ||||
|     match arg { | ||||
|         FnArg::Typed(typed) => { | ||||
|             let pat = typed.pat; | ||||
|             let kind = typed.ty; | ||||
|  | ||||
|             match *pat { | ||||
|                 Pat::Ident(id) => { | ||||
|                     let name = id.ident; | ||||
|                     let mutable = id.mutability; | ||||
|  | ||||
|                     Ok(Argument { | ||||
|                         mutable, | ||||
|                         name, | ||||
|                         kind: *kind, | ||||
|                     }) | ||||
|                 } | ||||
|                 Pat::Wild(wild) => { | ||||
|                     let token = wild.underscore_token; | ||||
|  | ||||
|                     let name = Ident::new("_", token.spans[0]); | ||||
|  | ||||
|                     Ok(Argument { | ||||
|                         mutable: None, | ||||
|                         name, | ||||
|                         kind: *kind, | ||||
|                     }) | ||||
|                 } | ||||
|                 _ => Err(Error::new( | ||||
|                     pat.span(), | ||||
|                     format_args!("unsupported pattern: {:?}", pat), | ||||
|                 )), | ||||
|             } | ||||
|         } | ||||
|         FnArg::Receiver(_) => Err(Error::new( | ||||
|             arg.span(), | ||||
|             format_args!("`self` arguments are prohibited: {:?}", arg), | ||||
|         )), | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// Test if the attribute is cooked. | ||||
| fn is_cooked(attr: &Attribute) -> bool { | ||||
|     const COOKED_ATTRIBUTE_NAMES: &[&str] = &[ | ||||
|         "cfg", "cfg_attr", "doc", "derive", "inline", "allow", "warn", "deny", "forbid", | ||||
|     ]; | ||||
|  | ||||
|     COOKED_ATTRIBUTE_NAMES.iter().any(|n| attr.path.is_ident(n)) | ||||
| } | ||||
|  | ||||
| /// Removes cooked attributes from a vector of attributes. Uncooked attributes are left in the vector. | ||||
| /// | ||||
| /// # Return | ||||
| /// | ||||
| /// Returns a vector of cooked attributes that have been removed from the input vector. | ||||
| fn remove_cooked(attrs: &mut Vec<Attribute>) -> Vec<Attribute> { | ||||
|     let mut cooked = Vec::new(); | ||||
|  | ||||
|     // FIXME: Replace with `Vec::drain_filter` once it is stable. | ||||
|     let mut i = 0; | ||||
|     while i < attrs.len() { | ||||
|         if !is_cooked(&attrs[i]) { | ||||
|             i += 1; | ||||
|             continue; | ||||
|         } | ||||
|  | ||||
|         cooked.push(attrs.remove(i)); | ||||
|     } | ||||
|  | ||||
|     cooked | ||||
| } | ||||
|  | ||||
| #[derive(Debug)] | ||||
| pub struct CommandFun { | ||||
|     /// `#[...]`-style attributes. | ||||
|     pub attributes: Vec<Attribute>, | ||||
|     /// Populated cooked attributes. These are attributes outside of the realm of this crate's procedural macros | ||||
|     /// and will appear in generated output. | ||||
|     pub cooked: Vec<Attribute>, | ||||
|     pub visibility: Visibility, | ||||
|     pub name: Ident, | ||||
|     pub args: Vec<Argument>, | ||||
|     pub body: Vec<Stmt>, | ||||
| } | ||||
|  | ||||
| impl Parse for CommandFun { | ||||
|     fn parse(input: ParseStream<'_>) -> Result<Self> { | ||||
|         let mut attributes = input.call(Attribute::parse_outer)?; | ||||
|  | ||||
|         // `#[doc = "..."]` is a cooked attribute but it is special-cased for commands. | ||||
|         for attr in &mut attributes { | ||||
|             // Rename documentation comment attributes (`#[doc = "..."]`) to `#[description = "..."]`. | ||||
|             if attr.path.is_ident("doc") { | ||||
|                 attr.path = Path::from(PathSegment::from(Ident::new( | ||||
|                     "description", | ||||
|                     Span::call_site(), | ||||
|                 ))); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         let cooked = remove_cooked(&mut attributes); | ||||
|  | ||||
|         let visibility = input.parse::<Visibility>()?; | ||||
|  | ||||
|         input.parse::<Token![async]>()?; | ||||
|  | ||||
|         input.parse::<Token![fn]>()?; | ||||
|         let name = input.parse()?; | ||||
|  | ||||
|         // (...) | ||||
|         let Parenthesised(args) = input.parse::<Parenthesised<FnArg>>()?; | ||||
|  | ||||
|         // { ... } | ||||
|         let bcont; | ||||
|         braced!(bcont in input); | ||||
|         let body = bcont.call(Block::parse_within)?; | ||||
|  | ||||
|         let args = args | ||||
|             .into_iter() | ||||
|             .map(parse_argument) | ||||
|             .collect::<Result<Vec<_>>>()?; | ||||
|  | ||||
|         Ok(Self { | ||||
|             attributes, | ||||
|             cooked, | ||||
|             visibility, | ||||
|             name, | ||||
|             args, | ||||
|             body, | ||||
|         }) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl ToTokens for CommandFun { | ||||
|     fn to_tokens(&self, stream: &mut TokenStream2) { | ||||
|         let Self { | ||||
|             attributes: _, | ||||
|             cooked, | ||||
|             visibility, | ||||
|             name, | ||||
|             args, | ||||
|             body, | ||||
|         } = self; | ||||
|  | ||||
|         stream.extend(quote! { | ||||
|             #(#cooked)* | ||||
|             #visibility async fn #name (#(#args),*) -> () { | ||||
|                 #(#body)* | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Debug)] | ||||
| pub enum PermissionLevel { | ||||
|     Unrestricted, | ||||
|     Managed, | ||||
|     Restricted, | ||||
| } | ||||
|  | ||||
| impl Default for PermissionLevel { | ||||
|     fn default() -> Self { | ||||
|         Self::Unrestricted | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl PermissionLevel { | ||||
|     pub fn from_str(s: &str) -> Option<Self> { | ||||
|         Some(match s.to_uppercase().as_str() { | ||||
|             "UNRESTRICTED" => Self::Unrestricted, | ||||
|             "MANAGED" => Self::Managed, | ||||
|             "RESTRICTED" => Self::Restricted, | ||||
|             _ => return None, | ||||
|         }) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl ToTokens for PermissionLevel { | ||||
|     fn to_tokens(&self, stream: &mut TokenStream2) { | ||||
|         let path = quote!(crate::framework::PermissionLevel); | ||||
|         let variant; | ||||
|  | ||||
|         match self { | ||||
|             Self::Unrestricted => { | ||||
|                 variant = quote!(Unrestricted); | ||||
|             } | ||||
|  | ||||
|             Self::Managed => { | ||||
|                 variant = quote!(Managed); | ||||
|             } | ||||
|  | ||||
|             Self::Restricted => { | ||||
|                 variant = quote!(Restricted); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         stream.extend(quote! { | ||||
|             #path::#variant | ||||
|         }); | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Default)] | ||||
| pub struct Options { | ||||
|     pub permission_level: PermissionLevel, | ||||
|     pub supports_dm: bool, | ||||
|     pub can_blacklist: bool, | ||||
| } | ||||
|  | ||||
| impl Options { | ||||
|     #[inline] | ||||
|     pub fn new() -> Self { | ||||
|         let mut options = Self::default(); | ||||
|  | ||||
|         options.can_blacklist = true; | ||||
|         options.supports_dm = true; | ||||
|  | ||||
|         options | ||||
|     } | ||||
| } | ||||
| @@ -1,160 +0,0 @@ | ||||
| use proc_macro::TokenStream; | ||||
| use proc_macro2::Span; | ||||
| use proc_macro2::TokenStream as TokenStream2; | ||||
| use quote::{format_ident, quote, ToTokens}; | ||||
| use syn::{ | ||||
|     braced, bracketed, parenthesized, | ||||
|     parse::{Error, Parse, ParseStream, Result as SynResult}, | ||||
|     punctuated::Punctuated, | ||||
|     token::{Comma, Mut}, | ||||
|     Ident, Lifetime, Lit, Type, | ||||
| }; | ||||
|  | ||||
| pub trait LitExt { | ||||
|     fn to_str(&self) -> String; | ||||
|     fn to_bool(&self) -> bool; | ||||
|     fn to_ident(&self) -> Ident; | ||||
| } | ||||
|  | ||||
| impl LitExt for Lit { | ||||
|     fn to_str(&self) -> String { | ||||
|         match self { | ||||
|             Lit::Str(s) => s.value(), | ||||
|             Lit::ByteStr(s) => unsafe { String::from_utf8_unchecked(s.value()) }, | ||||
|             Lit::Char(c) => c.value().to_string(), | ||||
|             Lit::Byte(b) => (b.value() as char).to_string(), | ||||
|             _ => panic!("values must be a (byte)string or a char"), | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fn to_bool(&self) -> bool { | ||||
|         if let Lit::Bool(b) = self { | ||||
|             b.value | ||||
|         } else { | ||||
|             self.to_str() | ||||
|                 .parse() | ||||
|                 .unwrap_or_else(|_| panic!("expected bool from {:?}", self)) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     #[inline] | ||||
|     fn to_ident(&self) -> Ident { | ||||
|         Ident::new(&self.to_str(), self.span()) | ||||
|     } | ||||
| } | ||||
|  | ||||
| pub trait IdentExt2: Sized { | ||||
|     fn to_uppercase(&self) -> Self; | ||||
|     fn with_suffix(&self, suf: &str) -> Ident; | ||||
| } | ||||
|  | ||||
| impl IdentExt2 for Ident { | ||||
|     #[inline] | ||||
|     fn to_uppercase(&self) -> Self { | ||||
|         format_ident!("{}", self.to_string().to_uppercase()) | ||||
|     } | ||||
|  | ||||
|     #[inline] | ||||
|     fn with_suffix(&self, suffix: &str) -> Ident { | ||||
|         format_ident!("{}_{}", self.to_string().to_uppercase(), suffix) | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[inline] | ||||
| pub fn into_stream(e: Error) -> TokenStream { | ||||
|     e.to_compile_error().into() | ||||
| } | ||||
|  | ||||
| macro_rules! propagate_err { | ||||
|     ($res:expr) => {{ | ||||
|         match $res { | ||||
|             Ok(v) => v, | ||||
|             Err(e) => return $crate::util::into_stream(e), | ||||
|         } | ||||
|     }}; | ||||
| } | ||||
|  | ||||
| #[derive(Debug)] | ||||
| pub struct Bracketed<T>(pub Punctuated<T, Comma>); | ||||
|  | ||||
| impl<T: Parse> Parse for Bracketed<T> { | ||||
|     fn parse(input: ParseStream<'_>) -> SynResult<Self> { | ||||
|         let content; | ||||
|         bracketed!(content in input); | ||||
|  | ||||
|         Ok(Bracketed(content.parse_terminated(T::parse)?)) | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Debug)] | ||||
| pub struct Braced<T>(pub Punctuated<T, Comma>); | ||||
|  | ||||
| impl<T: Parse> Parse for Braced<T> { | ||||
|     fn parse(input: ParseStream<'_>) -> SynResult<Self> { | ||||
|         let content; | ||||
|         braced!(content in input); | ||||
|  | ||||
|         Ok(Braced(content.parse_terminated(T::parse)?)) | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Debug)] | ||||
| pub struct Parenthesised<T>(pub Punctuated<T, Comma>); | ||||
|  | ||||
| impl<T: Parse> Parse for Parenthesised<T> { | ||||
|     fn parse(input: ParseStream<'_>) -> SynResult<Self> { | ||||
|         let content; | ||||
|         parenthesized!(content in input); | ||||
|  | ||||
|         Ok(Parenthesised(content.parse_terminated(T::parse)?)) | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Debug)] | ||||
| pub struct AsOption<T>(pub Option<T>); | ||||
|  | ||||
| impl<T: ToTokens> ToTokens for AsOption<T> { | ||||
|     fn to_tokens(&self, stream: &mut TokenStream2) { | ||||
|         match &self.0 { | ||||
|             Some(o) => stream.extend(quote!(Some(#o))), | ||||
|             None => stream.extend(quote!(None)), | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl<T> Default for AsOption<T> { | ||||
|     #[inline] | ||||
|     fn default() -> Self { | ||||
|         AsOption(None) | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Debug)] | ||||
| pub struct Argument { | ||||
|     pub mutable: Option<Mut>, | ||||
|     pub name: Ident, | ||||
|     pub kind: Type, | ||||
| } | ||||
|  | ||||
| impl ToTokens for Argument { | ||||
|     fn to_tokens(&self, stream: &mut TokenStream2) { | ||||
|         let Argument { | ||||
|             mutable, | ||||
|             name, | ||||
|             kind, | ||||
|         } = self; | ||||
|  | ||||
|         stream.extend(quote! { | ||||
|             #mutable #name: #kind | ||||
|         }); | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[inline] | ||||
| pub fn populate_fut_lifetimes_on_refs(args: &mut Vec<Argument>) { | ||||
|     for arg in args { | ||||
|         if let Type::Reference(reference) = &mut arg.kind { | ||||
|             reference.lifetime = Some(Lifetime::new("'fut", Span::call_site())); | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										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" | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										1
									
								
								reminder-dashboard/public/static/css/bulma.min.css
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										91
									
								
								reminder-dashboard/public/static/css/dtsel.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,91 @@ | ||||
| .date-selector-wrapper { | ||||
|     width: 200px; | ||||
|     padding: 3px; | ||||
|     background-color: #fff; | ||||
|     box-shadow: 1px 1px 10px 1px #5c5c5c; | ||||
|     position: absolute; | ||||
|     font-size: 12px; | ||||
|     -webkit-user-select: none; | ||||
|     -khtml-user-select: none; | ||||
|     -moz-user-select: none; | ||||
|     -ms-user-select: none; | ||||
|     -o-user-select: none; | ||||
|     /* user-select: none; */ | ||||
| } | ||||
| .cal-header, .cal-row { | ||||
|     display: flex; | ||||
|     width: 100%; | ||||
|     height: 30px; | ||||
|     line-height: 30px; | ||||
|     text-align: center; | ||||
| } | ||||
| .cal-cell, .cal-nav { | ||||
|     cursor: pointer; | ||||
| } | ||||
| .cal-day-names { | ||||
|     height: 25px; | ||||
|     line-height: 25px; | ||||
| } | ||||
| .cal-day-names .cal-cell { | ||||
|     cursor: default; | ||||
|     font-weight: bold; | ||||
| } | ||||
| .cal-cell-prev, .cal-cell-next { | ||||
|     color: #777; | ||||
| } | ||||
| .cal-months .cal-row, .cal-years .cal-row { | ||||
|     height: 60px; | ||||
|     line-height: 60px; | ||||
| } | ||||
| .cal-nav-prev, .cal-nav-next { | ||||
|     flex: 0.15; | ||||
| } | ||||
| .cal-nav-current { | ||||
|     flex: 0.75; | ||||
|     font-weight: bold; | ||||
| } | ||||
| .cal-months .cal-cell, .cal-years .cal-cell { | ||||
|     flex: 0.25; | ||||
| } | ||||
| .cal-days .cal-cell { | ||||
|     flex: 0.143; | ||||
| } | ||||
| .cal-value { | ||||
|     color: #fff; | ||||
|     background-color: #286090; | ||||
| } | ||||
| .cal-cell:hover, .cal-nav:hover { | ||||
|     background-color: #eee; | ||||
| } | ||||
| .cal-value:hover { | ||||
|     background-color: #204d74; | ||||
| } | ||||
|  | ||||
| /* time footer */ | ||||
| .cal-time { | ||||
|     display: flex; | ||||
|     justify-content: flex-start; | ||||
|     height: 27px; | ||||
|     line-height: 27px; | ||||
| } | ||||
| .cal-time-label, .cal-time-value { | ||||
|     flex: 0.12; | ||||
|     text-align: center; | ||||
| } | ||||
| .cal-time-slider { | ||||
|     flex: 0.77; | ||||
|     background-image: linear-gradient(to right, #d1d8dd, #d1d8dd); | ||||
|     background-repeat: no-repeat; | ||||
|     background-size: 100% 1px; | ||||
|     background-position: left 50%; | ||||
|     height: 100%; | ||||
| } | ||||
| .cal-time-slider input { | ||||
|     width: 100%; | ||||
|     -webkit-appearance: none; | ||||
|     background: 0 0; | ||||
|     cursor: pointer; | ||||
|     height: 100%; | ||||
|     outline: 0; | ||||
|     user-select: auto; | ||||
| } | ||||
							
								
								
									
										12749
									
								
								reminder-dashboard/public/static/css/fa.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										63
									
								
								reminder-dashboard/public/static/css/font.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,63 @@ | ||||
| @font-face { | ||||
|   font-family: 'Source Sans Pro'; | ||||
|   font-style: italic; | ||||
|   font-weight: 300; | ||||
|   src: local('Source Sans Pro Light Italic'), local('SourceSansPro-LightItalic'), url(https://fonts.gstatic.com/s/sourcesanspro/v13/6xKwdSBYKcSV-LCoeQqfX1RYOo3qPZZMkids18E.ttf) format('truetype'); | ||||
|   font-display: swap; | ||||
| } | ||||
| @font-face { | ||||
|   font-family: 'Source Sans Pro'; | ||||
|   font-style: italic; | ||||
|   font-weight: 400; | ||||
|   src: local('Source Sans Pro Italic'), local('SourceSansPro-Italic'), url(https://fonts.gstatic.com/s/sourcesanspro/v13/6xK1dSBYKcSV-LCoeQqfX1RYOo3qPZ7nsDc.ttf) format('truetype'); | ||||
|   font-display: swap; | ||||
| } | ||||
| @font-face { | ||||
|   font-family: 'Source Sans Pro'; | ||||
|   font-style: italic; | ||||
|   font-weight: 600; | ||||
|   src: local('Source Sans Pro SemiBold Italic'), local('SourceSansPro-SemiBoldItalic'), url(https://fonts.gstatic.com/s/sourcesanspro/v13/6xKwdSBYKcSV-LCoeQqfX1RYOo3qPZY4lCds18E.ttf) format('truetype'); | ||||
|   font-display: swap; | ||||
| } | ||||
| @font-face { | ||||
|   font-family: 'Source Sans Pro'; | ||||
|   font-style: normal; | ||||
|   font-weight: 300; | ||||
|   src: local('Source Sans Pro Light'), local('SourceSansPro-Light'), url(https://fonts.gstatic.com/s/sourcesanspro/v13/6xKydSBYKcSV-LCoeQqfX1RYOo3ik4zwlxdr.ttf) format('truetype'); | ||||
|   font-display: swap; | ||||
| } | ||||
| @font-face { | ||||
|   font-family: 'Source Sans Pro'; | ||||
|   font-style: normal; | ||||
|   font-weight: 400; | ||||
|   src: local('Source Sans Pro Regular'), local('SourceSansPro-Regular'), url(https://fonts.gstatic.com/s/sourcesanspro/v13/6xK3dSBYKcSV-LCoeQqfX1RYOo3qOK7g.ttf) format('truetype'); | ||||
|   font-display: swap; | ||||
| } | ||||
| @font-face { | ||||
|   font-family: 'Source Sans Pro'; | ||||
|   font-style: normal; | ||||
|   font-weight: 600; | ||||
|   src: local('Source Sans Pro SemiBold'), local('SourceSansPro-SemiBold'), url(https://fonts.gstatic.com/s/sourcesanspro/v13/6xKydSBYKcSV-LCoeQqfX1RYOo3i54rwlxdr.ttf) format('truetype'); | ||||
|   font-display: swap; | ||||
| } | ||||
| @font-face { | ||||
|   font-family: 'Source Sans Pro'; | ||||
|   font-style: normal; | ||||
|   font-weight: 700; | ||||
|   src: local('Source Sans Pro Bold'), local('SourceSansPro-Bold'), url(https://fonts.gstatic.com/s/sourcesanspro/v13/6xKydSBYKcSV-LCoeQqfX1RYOo3ig4vwlxdr.ttf) format('truetype'); | ||||
|   font-display: swap; | ||||
| } | ||||
| @font-face { | ||||
|   font-family: 'Ubuntu'; | ||||
|   font-style: normal; | ||||
|   font-weight: 400; | ||||
|   src: url(https://fonts.gstatic.com/s/ubuntu/v15/4iCs6KVjbNBYlgo6eA.ttf) format('truetype'); | ||||
|   font-display: swap; | ||||
| } | ||||
| @font-face { | ||||
|   font-family: 'Ubuntu'; | ||||
|   font-style: normal; | ||||
|   font-weight: 700; | ||||
|   src: url(https://fonts.gstatic.com/s/ubuntu/v15/4iCv6KVjbNBYlgoCxCvTtw.ttf) format('truetype'); | ||||
|   font-display: swap; | ||||
| } | ||||
							
								
								
									
										888
									
								
								reminder-dashboard/public/static/css/style.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,888 @@ | ||||
| * { | ||||
|     font-family: "Ubuntu Bold", "Ubuntu", sans-serif; | ||||
| } | ||||
|  | ||||
| button { | ||||
|     font-weight: 700; | ||||
| } | ||||
|  | ||||
| /* override styles for when the div is collapsed */ | ||||
| div.reminderContent.is-collapsed .column.discord-frame { | ||||
|     display: none; | ||||
| } | ||||
|  | ||||
| div.reminderContent.is-collapsed .column.settings { | ||||
|     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 { | ||||
|     display: inline-flex; | ||||
| } | ||||
|  | ||||
| div.reminderContent .invert-collapses { | ||||
|     display: none; | ||||
| } | ||||
|  | ||||
| div.reminderContent.is-collapsed input[name="name"] { | ||||
|     display: inline-flex; | ||||
|     flex-grow: 1; | ||||
|     border: none; | ||||
|     background: none; | ||||
|     box-shadow: none; | ||||
|     opacity: 1; | ||||
| } | ||||
|  | ||||
| div.reminderContent.is-collapsed .hide-box { | ||||
|     display: inline-flex; | ||||
| } | ||||
|  | ||||
| div.reminderContent.is-collapsed .hide-box i { | ||||
|     transform: rotate(90deg); | ||||
| } | ||||
| /* END */ | ||||
|  | ||||
| /* 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 { | ||||
|     height: 100%; | ||||
|     padding: 5px; | ||||
| } | ||||
|  | ||||
| button.change-color { | ||||
|     position: absolute; | ||||
|     left: calc(-1rem - 40px); | ||||
| } | ||||
|  | ||||
| button.disable-enable[data-action="enable"]:after { | ||||
|     content: "Enable"; | ||||
| } | ||||
|  | ||||
| button.disable-enable[data-action="disable"]:after { | ||||
|     content: "Disable"; | ||||
| } | ||||
|  | ||||
| .media-content { | ||||
|     overflow-x: visible; | ||||
| } | ||||
|  | ||||
| div.discord-embed { | ||||
|     position: relative; | ||||
| } | ||||
|  | ||||
| div.split-controls { | ||||
|     display: flex; | ||||
|     flex-direction: column; | ||||
|     justify-content: space-between; | ||||
|     flex-grow: 2; | ||||
| } | ||||
|  | ||||
| .reminder-topbar > div { | ||||
|     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 */ | ||||
| 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 { | ||||
|     -webkit-appearance: none; | ||||
|     border-style: none; | ||||
|     background-color: #eee; | ||||
|     font-size: 1rem; | ||||
|     font-family: monospace; | ||||
| } | ||||
|  | ||||
| div.interval-group > .interval-group-left input.w2 { | ||||
|     width: 3ch; | ||||
| } | ||||
|  | ||||
| div.interval-group > .interval-group-left input.w3 { | ||||
|     width: 3ch; | ||||
| } | ||||
|  | ||||
| div.interval-group { | ||||
|     display: flex; | ||||
|     flex-direction: row; | ||||
|     justify-content: space-between; | ||||
| } | ||||
| /* !Interval inputs */ | ||||
|  | ||||
| .left-pad { | ||||
|     padding-left: 1rem; | ||||
|     padding-right: 0.2rem; | ||||
| } | ||||
|  | ||||
| .notification { | ||||
|     padding-right: 1.5rem; | ||||
| } | ||||
|  | ||||
| div.inset-content { | ||||
|     margin-left: 10%; | ||||
|     margin-right: 10%; | ||||
| } | ||||
|  | ||||
| div.flash-message { | ||||
|     position: fixed; | ||||
|     width: calc(100% - 32px); | ||||
|     margin: 16px !important; | ||||
|     z-index: 99; | ||||
|     bottom: 0; | ||||
|     display: none; | ||||
| } | ||||
|  | ||||
| div.flash-message.is-active { | ||||
|     display: block; | ||||
| } | ||||
|  | ||||
| body { | ||||
|     min-height: 100vh; | ||||
| } | ||||
|  | ||||
| span.spacer { | ||||
|     width: 10px; | ||||
| } | ||||
|  | ||||
| nav .dashboard-button { | ||||
|     background: white ; | ||||
| } | ||||
|  | ||||
| span.patreon-color { | ||||
|     color: #f96854; | ||||
| } | ||||
|  | ||||
| p.pageTitle { | ||||
|     margin-left: 12px; | ||||
| } | ||||
|  | ||||
| #welcome > div { | ||||
|     height: 100%; | ||||
|     padding-top: 30vh; | ||||
| } | ||||
|  | ||||
| div#pageNavbar { | ||||
|     background-color: #363636; | ||||
| } | ||||
|  | ||||
| div#pageNavbar a { | ||||
|     color: #fff; | ||||
|     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 { | ||||
|     background-color: #4a4a4a; | ||||
| } | ||||
|  | ||||
| img.rounded-corners { | ||||
|     border-radius: 12px; | ||||
| } | ||||
|  | ||||
| div.brand { | ||||
|     text-align: center; | ||||
|     height: 52px; | ||||
|     background-color: #8fb677; | ||||
| } | ||||
|  | ||||
| img.dashboard-brand { | ||||
|     text-align: center; | ||||
|     height: 100%; | ||||
|     width: auto; | ||||
| } | ||||
|  | ||||
| div.dashboard-sidebar { | ||||
|     background-color: #363636; | ||||
|     width: 230px !important; | ||||
|     padding-right: 0; | ||||
| } | ||||
|  | ||||
| ul.guildList { | ||||
|     flex-grow: 1; | ||||
|     flex-shrink: 1; | ||||
|     overflow: auto; | ||||
| } | ||||
|  | ||||
| div.dashboard-sidebar:not(.mobile-sidebar) .aside-footer { | ||||
|     flex-shrink: 0; | ||||
|     flex-grow: 0; | ||||
|     position: fixed; | ||||
|     bottom: 0; | ||||
|     width: 226px; | ||||
| } | ||||
|  | ||||
| div.dashboard-sidebar svg { | ||||
|     flex-shrink: 0; | ||||
| } | ||||
|  | ||||
| div.mobile-sidebar { | ||||
|     z-index: 100; | ||||
|     min-height: 100vh; | ||||
|     position: absolute; | ||||
|     top: 0; | ||||
|     display: none; | ||||
|     flex-direction: column; | ||||
| } | ||||
|  | ||||
| #expandAll { | ||||
|     width: 60px; | ||||
| } | ||||
|  | ||||
| div.mobile-sidebar .aside-footer { | ||||
|     margin-top: auto; | ||||
| } | ||||
|  | ||||
| div.mobile-sidebar.is-active { | ||||
|     display: flex; | ||||
| } | ||||
|  | ||||
| aside.menu { | ||||
|     display: flex; | ||||
|     flex-direction: column; | ||||
|     flex-grow: 1; | ||||
| } | ||||
|  | ||||
| div.dashboard-frame { | ||||
|     min-height: 100vh; | ||||
|     margin-bottom: 0 !important; | ||||
| } | ||||
|  | ||||
| .embed-field-box[data-inlined="0"] .inline-btn > i { | ||||
|     transform: rotate(90deg); | ||||
| } | ||||
|  | ||||
| .embed-field-box[data-inlined="0"] { | ||||
|     min-width: 100%; | ||||
| } | ||||
|  | ||||
| .embed-field-box[data-inlined="1"] { | ||||
|     min-width: auto; | ||||
| } | ||||
|  | ||||
| .menu a { | ||||
|     color: #fff; | ||||
| } | ||||
|  | ||||
| .menu .menu-label { | ||||
|     color: #bbb; | ||||
| } | ||||
|  | ||||
| .menu { | ||||
|     padding-left: 4px; | ||||
| } | ||||
|  | ||||
| .dashboard-navbar { | ||||
|     background-color: #8fb677 !important; | ||||
|     position: absolute; | ||||
|     top: 0; | ||||
|     width: 100%; | ||||
| } | ||||
|  | ||||
| textarea.autoresize { | ||||
|     resize: none; | ||||
| } | ||||
|  | ||||
| textarea, input { | ||||
|     width: 100%; | ||||
| } | ||||
|  | ||||
| input.default-width { | ||||
|     width: initial; | ||||
| } | ||||
|  | ||||
| .message-input:placeholder-shown { | ||||
|     font-style: italic; | ||||
|     background-color: #40444b; | ||||
|     color: #fff; | ||||
| } | ||||
|  | ||||
| .message-input { | ||||
|     border: none; | ||||
|     background-color: rgba(0, 0, 0, 0); | ||||
|     color: #fff; | ||||
| } | ||||
|  | ||||
| .time-input { | ||||
|     border-top: none; | ||||
|     border-left: none; | ||||
|     border-right: none; | ||||
|     border-bottom-style: solid; | ||||
|     background-color: #40444b; | ||||
|     color: #fff; | ||||
|     width: 120px; | ||||
|     font-size: 0.875rem; | ||||
| } | ||||
|  | ||||
|  | ||||
| .message-input::placeholder { | ||||
|     color: #72767b; | ||||
| } | ||||
|  | ||||
| .discord-title { | ||||
|     font-weight: bold; | ||||
|     font-size: 1rem; | ||||
|     margin: 4px 0 4px 0; | ||||
| } | ||||
|  | ||||
| .discord-description { | ||||
|     font-size: 0.875rem; | ||||
| } | ||||
|  | ||||
| .discord-username { | ||||
|     font-size: 1rem; | ||||
|     font-weight: bold; | ||||
|     margin-bottom: 4px; | ||||
|     width: initial; | ||||
| } | ||||
|  | ||||
| .discord-message-header { | ||||
|     white-space: nowrap; | ||||
|     margin-bottom: 8px; | ||||
| } | ||||
|  | ||||
| .discord-content { | ||||
|     margin-bottom: 8px; | ||||
| } | ||||
|  | ||||
| .customizable img { | ||||
|     background-color: #72767b; | ||||
|     border-radius: 8px; | ||||
| } | ||||
|  | ||||
| .customizable.is-20x20 img { | ||||
|     width: 20px; | ||||
|     height: 20px; | ||||
| } | ||||
|  | ||||
| .customizable.is-24x24 img { | ||||
|     width: 24px; | ||||
|     height: 24px; | ||||
| } | ||||
|  | ||||
| .customizable.is-400x300 img { | ||||
|     margin-top: 10px; | ||||
|     width: 100%; | ||||
|     height: 100px; | ||||
| } | ||||
|  | ||||
| .customizable.is-32x32 img { | ||||
|     width: 32px; | ||||
|     height: 32px; | ||||
| } | ||||
|  | ||||
| .customizable.thumbnail img { | ||||
|     width: 100px; | ||||
|     height: 100px; | ||||
| } | ||||
|  | ||||
| .customizable input.imageInput { | ||||
|     display: none; | ||||
|     position: absolute; | ||||
|     top: 0; | ||||
|     left: 36px; | ||||
|     width: 400px; | ||||
| } | ||||
|  | ||||
| .customizable.thumbnail input.imageInput { | ||||
|     display: none; | ||||
|     position: absolute; | ||||
|     top: 0; | ||||
|     left: -400px; | ||||
|     width: 400px; | ||||
| } | ||||
|  | ||||
| .customizable input.is-active { | ||||
|     display: block !important; | ||||
| } | ||||
|  | ||||
| .discord-frame { | ||||
|     color: #fff; | ||||
|     padding: 10px; | ||||
|     border-radius: 8px; | ||||
|     background-color: #36393f; | ||||
| } | ||||
|  | ||||
| .discord-embed { | ||||
|     padding: 8px 16px 16px 12px; | ||||
|     margin: 0 20px 4px 0; | ||||
|     border-radius: 4px; | ||||
|     border-left: 4px solid #fff; | ||||
|     background-color: #2f3136; | ||||
| } | ||||
|  | ||||
| .embed-author-box { | ||||
|     display: flex; | ||||
|     align-items: center; | ||||
|     margin-bottom: 8px; | ||||
| } | ||||
|  | ||||
| .embed-author-box > .a { | ||||
|     flex: initial; | ||||
| } | ||||
|  | ||||
| .embed-author-box > .b { | ||||
|     flex: auto; | ||||
| } | ||||
|  | ||||
| .embed-footer-box { | ||||
|     display: flex; | ||||
|     align-items: center; | ||||
|     margin-bottom: 8px; | ||||
| } | ||||
|  | ||||
| .embed-author-box .image { | ||||
|     margin: 0 8px 0 0 !important; | ||||
| } | ||||
|  | ||||
| .embed-footer-box .image { | ||||
|     margin: 0 8px 0 0 !important; | ||||
| } | ||||
|  | ||||
| .discord-embed-author { | ||||
|     display: inline-block; | ||||
|     font-size: 0.875rem; | ||||
|     font-weight: bold; | ||||
| } | ||||
|  | ||||
| .discord-embed-footer { | ||||
|     font-size: 0.75rem; | ||||
| } | ||||
|  | ||||
| .embed-body { | ||||
|     display: flex; | ||||
| } | ||||
|  | ||||
| .embed-body > .a { | ||||
|     flex-grow: 1; | ||||
|     flex-shrink: 1; | ||||
|     flex-basis: auto; | ||||
|     margin-right: 4px; | ||||
| } | ||||
|  | ||||
| .embed-body input, .embed-body textarea { | ||||
|     min-width: 0; | ||||
| } | ||||
|  | ||||
| .embed-body > .b { | ||||
|     flex-grow: 0; | ||||
|     flex-shrink: 0; | ||||
|     flex-basis: auto; | ||||
| } | ||||
|  | ||||
| .discord-field-title, .discord-field-value { | ||||
|     max-width: 120px; | ||||
| } | ||||
|  | ||||
| .discord-field-title { | ||||
|     font-weight: bold; | ||||
| } | ||||
|  | ||||
| .embed-field-box { | ||||
|     margin: 12px 8px 0 0; | ||||
|     max-width: 120px; | ||||
|     flex: initial; | ||||
| } | ||||
|  | ||||
| .field-input { | ||||
|     font-size: 0.875rem; | ||||
|     width: 120px; | ||||
| } | ||||
|  | ||||
| .embed-multifield-box { | ||||
|     display: flex; | ||||
|     max-width: 100%; | ||||
|     flex-wrap: wrap; | ||||
| } | ||||
|  | ||||
| .channel-select { | ||||
|     font-size: 1.125rem; | ||||
|     margin-bottom: 4px; | ||||
|     margin-left: 48px; | ||||
|     display: inline-flex; | ||||
|     font-weight: bold; | ||||
|     color: #6e89da; | ||||
|     width: auto; | ||||
|     border-radius: 2px; | ||||
|     border-bottom: 1px solid #fff; | ||||
| } | ||||
|  | ||||
| .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 { | ||||
|         width: 60px; | ||||
|         height: 60px; | ||||
|     } | ||||
| } | ||||
|  | ||||
| @media only screen and (max-width: 768px) { | ||||
|     .button-row { | ||||
|         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 { | ||||
|     position: fixed; | ||||
|     top: 0; | ||||
|     background-color: rgba(255, 255, 255, 0.8); | ||||
|     width: 100vw; | ||||
|     z-index: 999; | ||||
| } | ||||
|  | ||||
| #loader .title { | ||||
|     font-size: 6rem; | ||||
| } | ||||
|  | ||||
| /* 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 */ | ||||
|  | ||||
| .half-rem { | ||||
|     width: 0.5rem; | ||||
| } | ||||
|  | ||||
| .pad-left { | ||||
|     width: 12px; | ||||
| } | ||||
|  | ||||
| #dead { | ||||
|     display: none; | ||||
| } | ||||
|  | ||||
| .colorpicker-container { | ||||
|     display: flex; | ||||
|     justify-content: center; | ||||
| } | ||||
|  | ||||
| .create-reminder { | ||||
|     margin: 0 12px 12px 12px; | ||||
| } | ||||
|  | ||||
| .button.is-success:not(.is-outlined) { | ||||
|     color: white; | ||||
| } | ||||
|  | ||||
| .button.is-outlined.is-success { | ||||
|     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 { | ||||
|     pointer-events: none; | ||||
| } | ||||
|  | ||||
| .is-locked > :not(.patreon-invert) { | ||||
|     opacity: 0.4; | ||||
| } | ||||
|  | ||||
| .is-locked .patreon-invert { | ||||
|     display: block; | ||||
| } | ||||
|  | ||||
| .patreon-invert { | ||||
|     display: none; | ||||
| } | ||||
|  | ||||
| .is-locked .foreground { | ||||
|     pointer-events: auto; | ||||
| } | ||||
|  | ||||
| .is-locked .field:last-of-type { | ||||
|     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; | ||||
| } | ||||
| After Width: | Height: | Size: 7.2 KiB | 
| After Width: | Height: | Size: 20 KiB | 
							
								
								
									
										
											BIN
										
									
								
								reminder-dashboard/public/static/favicon/apple-touch-icon.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 6.8 KiB | 
| @@ -0,0 +1,9 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <browserconfig> | ||||
|     <msapplication> | ||||
|         <tile> | ||||
|             <square150x150logo src="/mstile-150x150.png"/> | ||||
|             <TileColor>#da532c</TileColor> | ||||
|         </tile> | ||||
|     </msapplication> | ||||
| </browserconfig> | ||||
							
								
								
									
										
											BIN
										
									
								
								reminder-dashboard/public/static/favicon/favicon-16x16.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 1.8 KiB | 
							
								
								
									
										
											BIN
										
									
								
								reminder-dashboard/public/static/favicon/favicon-32x32.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 2.3 KiB | 
							
								
								
									
										
											BIN
										
									
								
								reminder-dashboard/public/static/favicon/favicon.ico
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 7.2 KiB | 
							
								
								
									
										
											BIN
										
									
								
								reminder-dashboard/public/static/favicon/mstile-150x150.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 4.9 KiB | 
							
								
								
									
										
											BIN
										
									
								
								reminder-dashboard/public/static/img/bg.webp
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 762 B | 
							
								
								
									
										
											BIN
										
									
								
								reminder-dashboard/public/static/img/icon.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 11 KiB | 
							
								
								
									
										
											BIN
										
									
								
								reminder-dashboard/public/static/img/logo_flat.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 323 KiB | 
							
								
								
									
										
											BIN
										
									
								
								reminder-dashboard/public/static/img/logo_flat.webp
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 61 KiB | 
							
								
								
									
										
											BIN
										
									
								
								reminder-dashboard/public/static/img/logo_nobg.webp
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 81 KiB | 
							
								
								
									
										
											BIN
										
									
								
								reminder-dashboard/public/static/img/slash-commands.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 23 KiB | 
| After Width: | Height: | Size: 35 KiB | 
| After Width: | Height: | Size: 55 KiB | 
| After Width: | Height: | Size: 3.6 KiB | 
| After Width: | Height: | Size: 17 KiB | 
| After Width: | Height: | Size: 14 KiB | 
| After Width: | Height: | Size: 24 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 | 
							
								
								
									
										
											BIN
										
									
								
								reminder-dashboard/public/static/img/tournament-demo.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 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()}})})); | ||||
							
								
								
									
										931
									
								
								reminder-dashboard/public/static/js/dtsel.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,931 @@ | ||||
| (function () { | ||||
|     "use strict"; | ||||
|  | ||||
|     var BODYTYPES = ["DAYS", "MONTHS", "YEARS"]; | ||||
|     var MONTHS = [ | ||||
|         "January", "February", "March", "April", "May", "June", | ||||
|         "July", "August", "September", "October", "November", "December" | ||||
|     ]; | ||||
|     var WEEKDAYS = [ | ||||
|         "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" | ||||
|     ]; | ||||
|  | ||||
|     /** @typedef {Object.<string, Function[]>} Handlers */ | ||||
|     /** @typedef {function(String, Function): null} AddHandler */ | ||||
|     /** @typedef {("DAYS"|"MONTHS"|"YEARS")} BodyType */ | ||||
|     /** @typedef {string|number} StringNum */ | ||||
|     /** @typedef {Object.<string, StringNum>} StringNumObj */ | ||||
|  | ||||
|     /** | ||||
|      * The local state | ||||
|      * @typedef {Object} InstanceState | ||||
|      * @property {Date} value | ||||
|      * @property {Number} year | ||||
|      * @property {Number} month | ||||
|      * @property {Number} day | ||||
|      * @property {Number} time | ||||
|      * @property {Number} hours | ||||
|      * @property {Number} minutes | ||||
|      * @property {Number} seconds | ||||
|      * @property {BodyType} bodyType | ||||
|      * @property {Boolean} visible | ||||
|      * @property {Number} cancelBlur | ||||
|      */ | ||||
|  | ||||
|     /**  | ||||
|      * @typedef {Object} Config | ||||
|      * @property {String} dateFormat | ||||
|      * @property {String} timeFormat | ||||
|      * @property {Boolean} showDate | ||||
|      * @property {Boolean} showTime | ||||
|      * @property {Number} paddingX | ||||
|      * @property {Number} paddingY | ||||
|      * @property {BodyType} defaultView | ||||
|      * @property {"TOP"|"BOTTOM"} direction | ||||
|     */ | ||||
|  | ||||
|     /** | ||||
|      * @class | ||||
|      * @param {HTMLElement} elem  | ||||
|      * @param {Config} config  | ||||
|      */ | ||||
|     function DTS(elem, config) { | ||||
|         var config = config || {}; | ||||
|  | ||||
|         /** @type {Config} */ | ||||
|         var defaultConfig = { | ||||
|             defaultView: BODYTYPES[0], | ||||
|             dateFormat: "yyyy-mm-dd", | ||||
|             timeFormat: "HH:MM:SS", | ||||
|             showDate: true, | ||||
|             showTime: false, | ||||
|             paddingX: 5, | ||||
|             paddingY: 5, | ||||
|             direction: 'TOP' | ||||
|         } | ||||
|  | ||||
|         if (!elem) { | ||||
|             throw TypeError("input element or selector required for contructor"); | ||||
|         } | ||||
|         if (Object.getPrototypeOf(elem) === String.prototype) { | ||||
|             var _elem = document.querySelectorAll(elem); | ||||
|             if (!_elem[0]){ | ||||
|                 throw Error('"' + elem + '" not found.'); | ||||
|             } | ||||
|             elem = _elem[0]; | ||||
|         } | ||||
|         this.config = setDefaults(config, defaultConfig); | ||||
|         this.dateFormat = this.config.dateFormat; | ||||
|         this.timeFormat = this.config.timeFormat; | ||||
|         this.dateFormatRegEx = new RegExp("yyyy|yy|mm|dd", "gi"); | ||||
|         this.timeFormatRegEx = new RegExp("hh|mm|ss|a", "gi"); | ||||
|         this.inputElem = elem; | ||||
|         this.dtbox = null; | ||||
|         this.setup(); | ||||
|     } | ||||
|     DTS.prototype.setup = function () { | ||||
|         var handler = this.inputElemHandler.bind(this); | ||||
|         this.inputElem.addEventListener("focus", handler, false) | ||||
|         this.inputElem.addEventListener("blur", handler, false); | ||||
|     } | ||||
|     DTS.prototype.inputElemHandler = function (e) { | ||||
|         if (e.type == "focus") { | ||||
|             if (!this.dtbox) { | ||||
|                 this.dtbox = new DTBox(e.target, this); | ||||
|             } | ||||
|             this.dtbox.visible = true; | ||||
|         } else if (e.type == "blur" && this.dtbox && this.dtbox.visible) { | ||||
|             var self = this; | ||||
|             setTimeout(function () { | ||||
|                 if (self.dtbox.cancelBlur > 0) { | ||||
|                     self.dtbox.cancelBlur -= 1; | ||||
|                  } else { | ||||
|                     self.dtbox.visible = false; | ||||
|                     self.inputElem.blur(); | ||||
|                  } | ||||
|             }, 100); | ||||
|         } | ||||
|     } | ||||
|     /** | ||||
|      * @class | ||||
|      * @param {HTMLElement} elem  | ||||
|      * @param {DTS} settings  | ||||
|      */ | ||||
|     function DTBox(elem, settings) { | ||||
|         /** @type {DTBox} */ | ||||
|         var self = this; | ||||
|  | ||||
|         /** @type {Handlers} */ | ||||
|         var handlers = {}; | ||||
|  | ||||
|         /** @type {InstanceState} */ | ||||
|         var localState = {}; | ||||
|  | ||||
|         /** | ||||
|          * @param {String} key  | ||||
|          * @param {*} default_val  | ||||
|          */ | ||||
|         function getterSetter(key, default_val) { | ||||
|             return { | ||||
|                 get: function () { | ||||
|                     var val = localState[key]; | ||||
|                     return val === undefined ? default_val : val; | ||||
|                 }, | ||||
|                 set: function (val) { | ||||
|                     var prevState = self.state; | ||||
|                     var _handlers = handlers[key] || []; | ||||
|                     localState[key] = val; | ||||
|                     for (var i = 0; i < _handlers.length; i++) { | ||||
|                         _handlers[i].bind(self)(localState, prevState); | ||||
|                     } | ||||
|                 }, | ||||
|             }; | ||||
|         }; | ||||
|  | ||||
|         /** @type {AddHandler} */ | ||||
|         function addHandler(key, handlerFn) { | ||||
|             if (!key || !handlerFn) { | ||||
|                 return false; | ||||
|             } | ||||
|             if (!handlers[key]) { | ||||
|                 handlers[key] = []; | ||||
|             } | ||||
|             handlers[key].push(handlerFn); | ||||
|         } | ||||
|  | ||||
|         Object.defineProperties(this, { | ||||
|             visible: getterSetter("visible", false), | ||||
|             bodyType: getterSetter("bodyType", settings.config.defaultView), | ||||
|             value: getterSetter("value"), | ||||
|             year: getterSetter("year", 0), | ||||
|             month: getterSetter("month", 0), | ||||
|             day: getterSetter("day", 0), | ||||
|             hours: getterSetter("hours", 0), | ||||
|             minutes: getterSetter("minutes", 0), | ||||
|             seconds: getterSetter("seconds", 0), | ||||
|             cancelBlur: getterSetter("cancelBlur", 0), | ||||
|             addHandler: {value: addHandler}, | ||||
|             month_long: { | ||||
|                 get: function () { | ||||
|                     return MONTHS[self.month]; | ||||
|                 }, | ||||
|             }, | ||||
|             month_short: { | ||||
|                 get: function () { | ||||
|                     return self.month_long.slice(0, 3); | ||||
|                 }, | ||||
|             }, | ||||
|             state: { | ||||
|                 get: function () { | ||||
|                     return Object.assign({}, localState); | ||||
|                 }, | ||||
|             }, | ||||
|             time: { | ||||
|                 get: function() { | ||||
|                     var hours = self.hours * 60 * 60 * 1000; | ||||
|                     var minutes = self.minutes * 60 * 1000; | ||||
|                     var seconds = self.seconds * 1000; | ||||
|                     return  hours + minutes + seconds; | ||||
|                 } | ||||
|             }, | ||||
|         }); | ||||
|         this.el = {}; | ||||
|         this.settings = settings; | ||||
|         this.elem = elem; | ||||
|         this.setup(); | ||||
|     } | ||||
|     DTBox.prototype.setup = function () { | ||||
|         Object.defineProperties(this.el, { | ||||
|             wrapper: { value: null, configurable: true }, | ||||
|             header: { value: null, configurable: true }, | ||||
|             body: { value: null, configurable: true }, | ||||
|             footer: { value: null, configurable: true } | ||||
|         }); | ||||
|         this.setupWrapper(); | ||||
|         if (this.settings.config.showDate) { | ||||
|             this.setupHeader(); | ||||
|             this.setupBody(); | ||||
|         } | ||||
|         if (this.settings.config.showTime) { | ||||
|             this.setupFooter(); | ||||
|         } | ||||
|  | ||||
|         var self = this; | ||||
|         this.addHandler("visible", function (state, prevState) { | ||||
|             if (state.visible && !prevState.visible){ | ||||
|                 document.body.appendChild(this.el.wrapper); | ||||
|  | ||||
|                 var parts = self.elem.value.split(/\s*,\s*/); | ||||
|                 var startDate = undefined; | ||||
|                 var startTime = 0; | ||||
|                 if (self.settings.config.showDate) { | ||||
|                     startDate = parseDate(parts[0], self.settings); | ||||
|                 } | ||||
|                 if (self.settings.config.showTime) { | ||||
|                     startTime = parseTime(parts[parts.length-1], self.settings); | ||||
|                     startTime = startTime || 0; | ||||
|                 } | ||||
|                 if (!(startDate && startDate.getTime())) { | ||||
|                     startDate = new Date(); | ||||
|                     startDate = new Date( | ||||
|                         startDate.getFullYear(), | ||||
|                         startDate.getMonth(), | ||||
|                         startDate.getDate() | ||||
|                     ); | ||||
|                 } | ||||
|                 var value = new Date(startDate.getTime() + startTime); | ||||
|                 self.value = value; | ||||
|                 self.year = value.getFullYear(); | ||||
|                 self.month = value.getMonth(); | ||||
|                 self.day = value.getDate(); | ||||
|                 self.hours = value.getHours(); | ||||
|                 self.minutes = value.getMinutes(); | ||||
|                 self.seconds = value.getSeconds(); | ||||
|  | ||||
|                 if (self.settings.config.showDate) { | ||||
|                     self.setHeaderContent(); | ||||
|                     self.setBodyContent(); | ||||
|                 } | ||||
|                 if (self.settings.config.showTime) { | ||||
|                     self.setFooterContent(); | ||||
|                 } | ||||
|             } else if (!state.visible && prevState.visible) { | ||||
|                 document.body.removeChild(this.el.wrapper); | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
|     DTBox.prototype.setupWrapper = function () { | ||||
|         if (!this.el.wrapper) { | ||||
|             var el = document.createElement("div"); | ||||
|             el.classList.add("date-selector-wrapper"); | ||||
|             Object.defineProperty(this.el, "wrapper", { value: el }); | ||||
|         } | ||||
|         var self = this; | ||||
|         var htmlRoot = document.getElementsByTagName('html')[0]; | ||||
|         function setPosition(e){ | ||||
|             var minTopSpace = 300; | ||||
|             var box = getOffset(self.elem); | ||||
|             var config = self.settings.config; | ||||
|             var paddingY = config.paddingY || 5; | ||||
|             var paddingX = config.paddingX || 5; | ||||
|             var top = box.top + self.elem.offsetHeight + paddingY; | ||||
|             var left = box.left + paddingX; | ||||
|             var bottom = htmlRoot.clientHeight - box.top + paddingY; | ||||
|  | ||||
|             self.el.wrapper.style.left = `${left}px`; | ||||
|             if (box.top > minTopSpace && config.direction != 'BOTTOM') { | ||||
|                 self.el.wrapper.style.bottom = `${bottom}px`; | ||||
|                 self.el.wrapper.style.top = ''; | ||||
|             } else { | ||||
|                 self.el.wrapper.style.top = `${top}px`; | ||||
|                 self.el.wrapper.style.bottom = '';  | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         function handler(e) { | ||||
|             self.cancelBlur += 1; | ||||
|             setTimeout(function(){ | ||||
|                 self.elem.focus(); | ||||
|             }, 50); | ||||
|         } | ||||
|         setPosition(); | ||||
|         this.setPosition = setPosition; | ||||
|         this.el.wrapper.addEventListener("mousedown", handler, false); | ||||
|         this.el.wrapper.addEventListener("touchstart", handler, false); | ||||
|         window.addEventListener('resize', this.setPosition); | ||||
|     } | ||||
|     DTBox.prototype.setupHeader = function () { | ||||
|         if (!this.el.header) { | ||||
|             var row = document.createElement("div"); | ||||
|             var classes = ["cal-nav-prev", "cal-nav-current", "cal-nav-next"]; | ||||
|             row.classList.add("cal-header"); | ||||
|             for (var i = 0; i < 3; i++) { | ||||
|                 var cell = document.createElement("div"); | ||||
|                 cell.classList.add("cal-nav", classes[i]); | ||||
|                 cell.onclick = this.onHeaderChange.bind(this); | ||||
|                 row.appendChild(cell); | ||||
|             } | ||||
|             row.children[0].innerHTML = "<"; | ||||
|             row.children[2].innerHTML = ">"; | ||||
|             Object.defineProperty(this.el, "header", { value: row }); | ||||
|             tryAppendChild(row, this.el.wrapper); | ||||
|         } | ||||
|         this.setHeaderContent(); | ||||
|     } | ||||
|     DTBox.prototype.setHeaderContent = function () { | ||||
|         var content = this.year; | ||||
|         if ("DAYS" == this.bodyType) { | ||||
|             content = this.month_long + " " + content; | ||||
|         } else if ("YEARS" == this.bodyType) { | ||||
|             var start = this.year + 10 - (this.year % 10); | ||||
|             content = start - 10 + "-" + (start - 1); | ||||
|         } | ||||
|         this.el.header.children[1].innerText = content; | ||||
|     } | ||||
|     DTBox.prototype.setupBody = function () { | ||||
|         if (!this.el.body) { | ||||
|             var el = document.createElement("div"); | ||||
|             el.classList.add("cal-body"); | ||||
|             Object.defineProperty(this.el, "body", { value: el }); | ||||
|             tryAppendChild(el, this.el.wrapper); | ||||
|         } | ||||
|         var toAppend = null; | ||||
|         function makeGrid(rows, cols, className, firstRowClass, clickHandler) { | ||||
|             var grid = document.createElement("div"); | ||||
|             grid.classList.add(className); | ||||
|             for (var i = 1; i < rows + 1; i++) { | ||||
|                 var row = document.createElement("div"); | ||||
|                 row.classList.add("cal-row", "cal-row-" + i); | ||||
|                 if (i == 1 && firstRowClass) { | ||||
|                     row.classList.add(firstRowClass); | ||||
|                 } | ||||
|                 for (var j = 1; j < cols + 1; j++) { | ||||
|                     var col = document.createElement("div"); | ||||
|                     col.classList.add("cal-cell", "cal-col-" + j); | ||||
|                     col.onclick = clickHandler; | ||||
|                     row.appendChild(col); | ||||
|                 } | ||||
|                 grid.appendChild(row); | ||||
|             } | ||||
|             return grid; | ||||
|         } | ||||
|         if ("DAYS" == this.bodyType) { | ||||
|             toAppend = this.el.body.calDays; | ||||
|             if (!toAppend) { | ||||
|                 toAppend = makeGrid(7, 7, "cal-days", "cal-day-names", this.onDateSelected.bind(this)); | ||||
|                 for (var i = 0; i < 7; i++) { | ||||
|                     var cell = toAppend.children[0].children[i]; | ||||
|                     cell.innerText = WEEKDAYS[i].slice(0, 2); | ||||
|                     cell.onclick = null; | ||||
|                 } | ||||
|                 this.el.body.calDays = toAppend; | ||||
|             } | ||||
|         } else if ("MONTHS" == this.bodyType) { | ||||
|             toAppend = this.el.body.calMonths; | ||||
|             if (!toAppend) { | ||||
|                 toAppend = makeGrid(3, 4, "cal-months", null, this.onMonthSelected.bind(this)); | ||||
|                 for (var i = 0; i < 3; i++) { | ||||
|                     for (var j = 0; j < 4; j++) { | ||||
|                         var monthShort = MONTHS[4 * i + j].slice(0, 3); | ||||
|                         toAppend.children[i].children[j].innerText = monthShort; | ||||
|                     } | ||||
|                 } | ||||
|                 this.el.body.calMonths = toAppend; | ||||
|             } | ||||
|         } else if ("YEARS" == this.bodyType) { | ||||
|             toAppend = this.el.body.calYears; | ||||
|             if (!toAppend) { | ||||
|                 toAppend = makeGrid(3, 4, "cal-years", null, this.onYearSelected.bind(this)); | ||||
|                 this.el.body.calYears = toAppend; | ||||
|             } | ||||
|         } | ||||
|         empty(this.el.body); | ||||
|         tryAppendChild(toAppend, this.el.body); | ||||
|         this.setBodyContent(); | ||||
|     } | ||||
|     DTBox.prototype.setBodyContent = function () { | ||||
|         var grid = this.el.body.children[0]; | ||||
|         var classes = ["cal-cell-prev", "cal-cell-next", "cal-value"]; | ||||
|         if ("DAYS" == this.bodyType) { | ||||
|             var oneDayMilliSecs = 24 * 60 * 60 * 1000; | ||||
|             var start = new Date(this.year, this.month, 1); | ||||
|             var adjusted = new Date(start.getTime() - oneDayMilliSecs * start.getDay()); | ||||
|  | ||||
|             grid.children[6].style.display = ""; | ||||
|             for (var i = 1; i < 7; i++) { | ||||
|                 for (var j = 0; j < 7; j++) { | ||||
|                     var cell = grid.children[i].children[j]; | ||||
|                     var month = adjusted.getMonth(); | ||||
|                     var date = adjusted.getDate(); | ||||
|                      | ||||
|                     cell.innerText = date; | ||||
|                     cell.classList.remove(classes[0], classes[1], classes[2]); | ||||
|                     if (month != this.month) { | ||||
|                         if (i == 6 && j == 0) { | ||||
|                             grid.children[6].style.display = "none"; | ||||
|                             break; | ||||
|                         } | ||||
|                         cell.classList.add(month < this.month ? classes[0] : classes[1]); | ||||
|                     } else if (isEqualDate(adjusted, this.value)){ | ||||
|                         cell.classList.add(classes[2]); | ||||
|                     } | ||||
|                     adjusted = new Date(adjusted.getTime() + oneDayMilliSecs); | ||||
|                 } | ||||
|             } | ||||
|         } else if ("YEARS" == this.bodyType) { | ||||
|             var year = this.year - (this.year % 10) - 1; | ||||
|             for (i = 0; i < 3; i++) { | ||||
|                 for (j = 0; j < 4; j++) { | ||||
|                     grid.children[i].children[j].innerText = year; | ||||
|                     year += 1; | ||||
|                 } | ||||
|             } | ||||
|             grid.children[0].children[0].classList.add(classes[0]); | ||||
|             grid.children[2].children[3].classList.add(classes[1]); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** @param {Event} e */ | ||||
|     DTBox.prototype.onTimeChange = function(e) { | ||||
|         e.stopPropagation(); | ||||
|         if (e.type == 'mousedown') { | ||||
|             this.cancelBlur += 1; | ||||
|             return; | ||||
|         } | ||||
|          | ||||
|         var el = e.target; | ||||
|         this[el.name] = parseInt(el.value) || 0; | ||||
|         this.setupFooter(); | ||||
|         if (e.type == 'change') { | ||||
|             var self = this; | ||||
|             setTimeout(function(){ | ||||
|                 self.elem.focus(); | ||||
|             }, 50); | ||||
|         } | ||||
|         this.setInputValue(); | ||||
|     } | ||||
|  | ||||
|     DTBox.prototype.setupFooter = function() { | ||||
|         if (!this.el.footer) { | ||||
|             var footer = document.createElement("div"); | ||||
|             var handler = this.onTimeChange.bind(this); | ||||
|             var self = this; | ||||
|              | ||||
|             function makeRow(label, name, range, changeHandler) { | ||||
|                 var row = document.createElement("div"); | ||||
|                 row.classList.add('cal-time'); | ||||
|  | ||||
|                 var labelCol = row.appendChild(document.createElement("div")); | ||||
|                 labelCol.classList.add('cal-time-label'); | ||||
|                 labelCol.innerText = label; | ||||
|  | ||||
|                 var valueCol = row.appendChild(document.createElement("div")); | ||||
|                 valueCol.classList.add('cal-time-value'); | ||||
|                 valueCol.innerText = '00'; | ||||
|  | ||||
|                 var inputCol = row.appendChild(document.createElement("div")); | ||||
|                 var slider = inputCol.appendChild(document.createElement("input")); | ||||
|                 Object.assign(slider, {step:1, min:0, max:range, name:name, type:'range'}); | ||||
|                 Object.defineProperty(footer, name, {value: slider}); | ||||
|                 inputCol.classList.add('cal-time-slider'); | ||||
|                 slider.onchange = changeHandler; | ||||
|                 slider.oninput = changeHandler; | ||||
|                 slider.onmousedown = changeHandler; | ||||
|                 self[name] = self[name] || parseInt(slider.value) || 0; | ||||
|                 footer.appendChild(row) | ||||
|             } | ||||
|             makeRow('HH:', 'hours', 23, handler); | ||||
|             makeRow('MM:', 'minutes', 59, handler); | ||||
|             makeRow('SS:', 'seconds', 59, handler); | ||||
|  | ||||
|             footer.classList.add("cal-footer"); | ||||
|             Object.defineProperty(this.el, "footer", { value: footer }); | ||||
|             tryAppendChild(footer, this.el.wrapper); | ||||
|         } | ||||
|         this.setFooterContent(); | ||||
|     } | ||||
|  | ||||
|     DTBox.prototype.setFooterContent = function() { | ||||
|         if (this.el.footer) { | ||||
|             var footer = this.el.footer; | ||||
|             footer.hours.value = this.hours; | ||||
|             footer.children[0].children[1].innerText = padded(this.hours, 2); | ||||
|             footer.minutes.value = this.minutes; | ||||
|             footer.children[1].children[1].innerText = padded(this.minutes, 2); | ||||
|             footer.seconds.value = this.seconds; | ||||
|             footer.children[2].children[1].innerText = padded(this.seconds, 2); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     DTBox.prototype.setInputValue = function() { | ||||
|         var date = new Date(this.year, this.month, this.day); | ||||
|         var strings = []; | ||||
|         if (this.settings.config.showDate) { | ||||
|             strings.push(renderDate(date, this.settings)); | ||||
|         } | ||||
|         if (this.settings.config.showTime) { | ||||
|             var joined = new Date(date.getTime() + this.time); | ||||
|             strings.push(renderTime(joined, this.settings)); | ||||
|         } | ||||
|         this.elem.value = strings.join(', '); | ||||
|     } | ||||
|  | ||||
|     DTBox.prototype.onDateSelected = function (e) { | ||||
|         var row = e.target.parentNode; | ||||
|         var date = parseInt(e.target.innerText); | ||||
|         if (!(row.nextSibling && row.nextSibling.nextSibling) && date < 8) { | ||||
|             this.month += 1; | ||||
|         } else if (!(row.previousSibling && row.previousSibling.previousSibling) && date > 7) { | ||||
|             this.month -= 1; | ||||
|         } | ||||
|         this.day = parseInt(e.target.innerText); | ||||
|         this.value = new Date(this.year, this.month, this.day); | ||||
|         this.setInputValue(); | ||||
|         this.setHeaderContent(); | ||||
|         this.setBodyContent(); | ||||
|     } | ||||
|  | ||||
|     /** @param {Event} e */ | ||||
|     DTBox.prototype.onMonthSelected = function (e) { | ||||
|         var col = 0; | ||||
|         var row = 2; | ||||
|         var cell = e.target; | ||||
|         if (cell.parentNode.nextSibling){ | ||||
|             row = cell.parentNode.previousSibling ? 1: 0; | ||||
|         } | ||||
|         if (cell.previousSibling) { | ||||
|             col = 3; | ||||
|             if (cell.nextSibling) { | ||||
|                 col = cell.previousSibling.previousSibling ? 2 : 1; | ||||
|             } | ||||
|         } | ||||
|         this.month = 4 * row + col; | ||||
|         this.bodyType = "DAYS"; | ||||
|         this.setHeaderContent(); | ||||
|         this.setupBody(); | ||||
|     } | ||||
|  | ||||
|     /** @param {Event} e */ | ||||
|     DTBox.prototype.onYearSelected = function (e) { | ||||
|         this.year = parseInt(e.target.innerText); | ||||
|         this.bodyType = "MONTHS"; | ||||
|         this.setHeaderContent(); | ||||
|         this.setupBody(); | ||||
|     } | ||||
|  | ||||
|     /** @param {Event} e */ | ||||
|     DTBox.prototype.onHeaderChange = function (e) { | ||||
|         var cell = e.target; | ||||
|         if (cell.previousSibling && cell.nextSibling) { | ||||
|             var idx = BODYTYPES.indexOf(this.bodyType); | ||||
|             if (idx < 0 || !BODYTYPES[idx + 1]) { | ||||
|                 return; | ||||
|             } | ||||
|             this.bodyType = BODYTYPES[idx + 1]; | ||||
|             this.setupBody(); | ||||
|         } else { | ||||
|             var sign = cell.previousSibling ? 1 : -1; | ||||
|             switch (this.bodyType) { | ||||
|                 case "DAYS": | ||||
|                     this.month += sign * 1; | ||||
|                     break; | ||||
|                 case "MONTHS": | ||||
|                     this.year += sign * 1; | ||||
|                     break; | ||||
|                 case "YEARS": | ||||
|                     this.year += sign * 10; | ||||
|             } | ||||
|             if (this.month > 11 || this.month < 0) { | ||||
|                 this.year += Math.floor(this.month / 11); | ||||
|                 this.month = this.month > 11 ? 0 : 11; | ||||
|             } | ||||
|         } | ||||
|         this.setHeaderContent(); | ||||
|         this.setBodyContent(); | ||||
|     } | ||||
|  | ||||
|  | ||||
|     /** | ||||
|      * @param {HTMLElement} elem  | ||||
|      * @returns {{left:number, top:number}} | ||||
|      */ | ||||
|     function getOffset(elem) { | ||||
|         var box = elem.getBoundingClientRect(); | ||||
|         var left = window.pageXOffset !== undefined ? window.pageXOffset :  | ||||
|             (document.documentElement || document.body.parentNode || document.body).scrollLeft; | ||||
|         var top = window.pageYOffset !== undefined ? window.pageYOffset :  | ||||
|             (document.documentElement || document.body.parentNode || document.body).scrollTop; | ||||
|         return { left: box.left + left, top: box.top + top }; | ||||
|     } | ||||
|     function empty(e) { | ||||
|         for (; e.children.length; ) e.removeChild(e.children[0]); | ||||
|     } | ||||
|     function tryAppendChild(newChild, refNode) { | ||||
|         try { | ||||
|             refNode.appendChild(newChild); | ||||
|             return newChild; | ||||
|         } catch (e) { | ||||
|             console.trace(e); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** @class */ | ||||
|     function hookFuncs() { | ||||
|         /** @type {Handlers} */ | ||||
|         this._funcs = {}; | ||||
|     } | ||||
|     /** | ||||
|      * @param {string} key  | ||||
|      * @param {Function} func  | ||||
|      */ | ||||
|     hookFuncs.prototype.add = function(key, func){ | ||||
|         if (!this._funcs[key]){ | ||||
|             this._funcs[key] = []; | ||||
|         } | ||||
|         this._funcs[key].push(func) | ||||
|     } | ||||
|     /** | ||||
|      * @param {String} key  | ||||
|      * @returns {Function[]} handlers | ||||
|      */ | ||||
|     hookFuncs.prototype.get = function(key){ | ||||
|         return this._funcs[key] ? this._funcs[key] : []; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @param {Array.<string>} arr  | ||||
|      * @param {String} string  | ||||
|      * @returns {Array.<string>} sorted string | ||||
|      */ | ||||
|     function sortByStringIndex(arr, string) { | ||||
|         return arr.sort(function(a, b){ | ||||
|             var h = string.indexOf(a); | ||||
|             var l = string.indexOf(b); | ||||
|             var rank = 0; | ||||
|             if (h < l) { | ||||
|                 rank = -1; | ||||
|             } else if (l < h) { | ||||
|                 rank = 1; | ||||
|             } else if (a.length > b.length) { | ||||
|                 rank = -1; | ||||
|             } else if (b.length > a.length) { | ||||
|                 rank = 1; | ||||
|             } | ||||
|             return rank; | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Remove keys from array that are not in format | ||||
|      * @param {string[]} keys  | ||||
|      * @param {string} format  | ||||
|      * @returns {string[]} new filtered array | ||||
|      */ | ||||
|     function filterFormatKeys(keys, format) { | ||||
|         var out = []; | ||||
|         var formatIdx = 0; | ||||
|         for (var i = 0; i<keys.length; i++) { | ||||
|             var key = keys[i]; | ||||
|             if (format.slice(formatIdx).indexOf(key) > -1) { | ||||
|                 formatIdx += key.length; | ||||
|                 out.push(key); | ||||
|             } | ||||
|         } | ||||
|         return out; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @template {StringNumObj} FormatObj | ||||
|      * @param {string} value  | ||||
|      * @param {string} format  | ||||
|      * @param {FormatObj} formatObj  | ||||
|      * @param {function(Object.<string, hookFuncs>): null} setHooks  | ||||
|      * @returns {FormatObj} formatObj | ||||
|      */ | ||||
|     function parseData(value, format, formatObj, setHooks) { | ||||
|         var hooks = { | ||||
|             canSkip: new hookFuncs(), | ||||
|             updateValue: new hookFuncs(), | ||||
|         } | ||||
|         var keys = sortByStringIndex(Object.keys(formatObj), format); | ||||
|         var filterdKeys = filterFormatKeys(keys, format); | ||||
|         var vstart = 0; // value start | ||||
|         if (setHooks) { | ||||
|             setHooks(hooks); | ||||
|         } | ||||
|  | ||||
|         for (var i = 0; i < keys.length; i++) { | ||||
|             var key = keys[i]; | ||||
|             var fstart = format.indexOf(key); | ||||
|             var _vstart = vstart; // next value start | ||||
|             var val = null; | ||||
|             var canSkip = false; | ||||
|             var funcs = hooks.canSkip.get(key); | ||||
|  | ||||
|             vstart = vstart || fstart; | ||||
|  | ||||
|             for (var j = 0; j < funcs.length; j++) { | ||||
|                 if (funcs[j](formatObj)){ | ||||
|                     canSkip = true; | ||||
|                     break; | ||||
|                 } | ||||
|             } | ||||
|             if (fstart > -1 && !canSkip) { | ||||
|                 var sep = null; | ||||
|                 var stop = vstart + key.length; | ||||
|                 var fnext = -1; | ||||
|                 var nextKeyIdx = i + 1; | ||||
|                 _vstart += key.length; // set next value start if current key is found | ||||
|  | ||||
|                 // get next format token used to determine separator | ||||
|                 while (fnext == -1 && nextKeyIdx < keys.length){ | ||||
|                     var nextKey = keys[nextKeyIdx]; | ||||
|                     nextKeyIdx += 1; | ||||
|                     if (filterdKeys.indexOf(nextKey) === -1) { | ||||
|                         continue; | ||||
|                     } | ||||
|                     fnext = nextKey ? format.indexOf(nextKey) : -1; // next format start | ||||
|                 } | ||||
|                 if (fnext > -1){ | ||||
|                     sep = format.slice(stop, fnext); | ||||
|                     if (sep) { | ||||
|                         var _stop = value.slice(vstart).indexOf(sep); | ||||
|                         if (_stop && _stop > -1){ | ||||
|                             stop = _stop + vstart; | ||||
|                             _vstart = stop + sep.length; | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|                 val = parseInt(value.slice(vstart, stop)); | ||||
|  | ||||
|                 var funcs = hooks.updateValue.get(key); | ||||
|                 for (var k = 0; k < funcs.length; k++) { | ||||
|                     val = funcs[k](val, formatObj, vstart, stop); | ||||
|                 } | ||||
|             } | ||||
|             formatObj[key] = { index: vstart, value: val }; | ||||
|             vstart = _vstart; // set next value start | ||||
|         } | ||||
|         return formatObj; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @param {String} value  | ||||
|      * @param {DTS} settings  | ||||
|      * @returns {Date} date object | ||||
|      */ | ||||
|     function parseDate(value, settings) { | ||||
|         /** @type {{yyyy:number=, yy:number=, mm:number=, dd:number=}} */ | ||||
|         var formatObj = {yyyy:null, yy:null, mm:null, dd:null}; | ||||
|         var format = ((settings.dateFormat) || '').toLowerCase(); | ||||
|         if (!format) { | ||||
|             throw new TypeError('dateFormat not found (' + settings.dateFormat + ')'); | ||||
|         } | ||||
|         var formatObj = parseData(value, format, formatObj, function(hooks){ | ||||
|             hooks.canSkip.add("yy", function(data){ | ||||
|                 return data["yyyy"].value; | ||||
|             }); | ||||
|             hooks.updateValue.add("yy", function(val){ | ||||
|                 return 100 * Math.floor(new Date().getFullYear() / 100) + val; | ||||
|             }); | ||||
|         }); | ||||
|         var year = formatObj["yyyy"].value || formatObj["yy"].value; | ||||
|         var month = formatObj["mm"].value - 1; | ||||
|         var date = formatObj["dd"].value; | ||||
|         var result = new Date(year, month, date); | ||||
|         return result; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @param {String} value  | ||||
|      * @param {DTS} settings  | ||||
|      * @returns {Number} time in milliseconds <= (24 * 60 * 60 * 1000) - 1 | ||||
|      */ | ||||
|     function parseTime(value, settings) { | ||||
|         var format = ((settings.timeFormat) || '').toLowerCase(); | ||||
|         if (!format) { | ||||
|             throw new TypeError('timeFormat not found (' + settings.timeFormat + ')'); | ||||
|         } | ||||
|  | ||||
|         /** @type {{hh:number=, mm:number=, ss:number=, a:string=}} */ | ||||
|         var formatObj = {hh:null, mm:null, ss:null, a:null}; | ||||
|         var formatObj = parseData(value, format, formatObj, function(hooks){ | ||||
|             hooks.updateValue.add("a", function(val, data, start, stop){ | ||||
|                 return value.slice(start, start + 2); | ||||
|             }); | ||||
|         }); | ||||
|         var hours = formatObj["hh"].value; | ||||
|         var minutes = formatObj["mm"].value; | ||||
|         var seconds = formatObj["ss"].value; | ||||
|         var am_pm = formatObj["a"].value; | ||||
|         var am_pm_lower = am_pm ? am_pm.toLowerCase() : am_pm; | ||||
|         if (am_pm && ["am", "pm"].indexOf(am_pm_lower) > -1){ | ||||
|             if (am_pm_lower == 'am' && hours == 12){ | ||||
|                 hours = 0; | ||||
|             } else if (am_pm_lower == 'pm') { | ||||
|                 hours += 12; | ||||
|             } | ||||
|         } | ||||
|         var time = hours * 60 * 60 * 1000 + minutes * 60 * 1000 + seconds * 1000; | ||||
|         return time; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @param {Date} value  | ||||
|      * @param {DTS} settings  | ||||
|      * @returns {String} date string | ||||
|      */ | ||||
|     function renderDate(value, settings) { | ||||
|         var format = settings.dateFormat.toLowerCase(); | ||||
|         var date = value.getDate(); | ||||
|         var month = value.getMonth() + 1; | ||||
|         var year = value.getFullYear(); | ||||
|         var yearShort = year % 100; | ||||
|         var formatObj = { | ||||
|             dd: date < 10 ? "0" + date : date, | ||||
|             mm: month < 10 ? "0" + month : month, | ||||
|             yyyy: year, | ||||
|             yy: yearShort < 10 ? "0" + yearShort : yearShort | ||||
|         }; | ||||
|         var str = format.replace(settings.dateFormatRegEx, function (found) { | ||||
|             return formatObj[found]; | ||||
|         }); | ||||
|         return str; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @param {Date} value  | ||||
|      * @param {DTS} settings  | ||||
|      * @returns {String} date string | ||||
|      */ | ||||
|     function renderTime(value, settings) { | ||||
|         var Format = settings.timeFormat; | ||||
|         var format = Format.toLowerCase(); | ||||
|         var hours = value.getHours(); | ||||
|         var minutes = value.getMinutes(); | ||||
|         var seconds = value.getSeconds(); | ||||
|         var am_pm = null; | ||||
|         var hh_am_pm = null; | ||||
|         if (format.indexOf('a') > -1) { | ||||
|             am_pm = hours >= 12 ? 'pm' : 'am'; | ||||
|             am_pm = Format.indexOf('A') > -1 ? am_pm.toUpperCase() : am_pm; | ||||
|             hh_am_pm = hours == 0 ? '12' : (hours > 12 ? hours%12 : hours); | ||||
|         } | ||||
|         var formatObj = { | ||||
|             hh: am_pm ? hh_am_pm : (hours < 10 ? "0" + hours : hours), | ||||
|             mm: minutes < 10 ? "0" + minutes : minutes, | ||||
|             ss: seconds < 10 ? "0" + seconds : seconds, | ||||
|             a: am_pm, | ||||
|         }; | ||||
|         var str = format.replace(settings.timeFormatRegEx, function (found) { | ||||
|             return formatObj[found]; | ||||
|         }); | ||||
|         return str; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * checks if two dates are equal | ||||
|      * @param {Date} date1  | ||||
|      * @param {Date} date2  | ||||
|      * @returns {Boolean} true or false | ||||
|      */ | ||||
|     function isEqualDate(date1, date2) { | ||||
|         if (!(date1 && date2)) return false; | ||||
|         return (date1.getFullYear() == date2.getFullYear() &&  | ||||
|                 date1.getMonth() == date2.getMonth() &&  | ||||
|                 date1.getDate() == date2.getDate()); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @param {Number} val  | ||||
|      * @param {Number} pad  | ||||
|      * @param {*} default_val  | ||||
|      * @returns {String} padded string | ||||
|      */ | ||||
|     function padded(val, pad, default_val) { | ||||
|         var default_val = default_val || 0; | ||||
|         var valStr = '' + (parseInt(val) || default_val); | ||||
|         var diff = Math.max(pad, valStr.length) - valStr.length; | ||||
|         return ('' + default_val).repeat(diff) + valStr; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @template X | ||||
|      * @template Y | ||||
|      * @param {X} obj  | ||||
|      * @param {Y} objDefaults  | ||||
|      * @returns {X|Y} merged object | ||||
|      */ | ||||
|     function setDefaults(obj, objDefaults) { | ||||
|         var keys = Object.keys(objDefaults); | ||||
|         for (var i=0; i<keys.length; i++) { | ||||
|             var key = keys[i]; | ||||
|             if (!Object.prototype.hasOwnProperty.call(obj, key)) { | ||||
|                 obj[key] = objDefaults[key]; | ||||
|             } | ||||
|         } | ||||
|         return obj; | ||||
|     } | ||||
|  | ||||
|  | ||||
|     window.dtsel = Object.create({},{ | ||||
|         DTS: { value: DTS }, | ||||
|         DTObj: { value: DTBox }, | ||||
|         fn: { | ||||
|             value: Object.defineProperties({}, { | ||||
|                 empty: { value: empty }, | ||||
|                 appendAfter: { | ||||
|                     value: function (newElem, refNode) { | ||||
|                         refNode.parentNode.insertBefore(newElem, refNode.nextSibling); | ||||
|                     }, | ||||
|                 }, | ||||
|                 getOffset: { value: getOffset }, | ||||
|                 parseDate: { value: parseDate }, | ||||
|                 renderDate: { value: renderDate }, | ||||
|                 parseTime: {value: parseTime}, | ||||
|                 renderTime: {value: renderTime}, | ||||
|                 setDefaults: {value: setDefaults}, | ||||
|             }), | ||||
|         }, | ||||
|     }); | ||||
| })(); | ||||
							
								
								
									
										23
									
								
								reminder-dashboard/public/static/js/expand.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,23 @@ | ||||
| function collapse_all() { | ||||
|     document.querySelectorAll("div.reminderContent:not(.creator)").forEach((el) => { | ||||
|         el.classList.add("is-collapsed"); | ||||
|     }); | ||||
| } | ||||
|  | ||||
| function expand_all() { | ||||
|     document.querySelectorAll("div.reminderContent:not(.creator)").forEach((el) => { | ||||
|         el.classList.remove("is-collapsed"); | ||||
|     }); | ||||
| } | ||||
|  | ||||
| const expandAll = document.querySelector("#expandAll"); | ||||
|  | ||||
| expandAll.addEventListener("change", (ev) => { | ||||
|     if (ev.target.value === "expand") { | ||||
|         expand_all(); | ||||
|     } else if (ev.target.value === "collapse") { | ||||
|         collapse_all(); | ||||
|     } | ||||
|  | ||||
|     ev.target.value = ""; | ||||
| }); | ||||
							
								
								
									
										94
									
								
								reminder-dashboard/public/static/js/interval.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,94 @@ | ||||
| function get_interval(element) { | ||||
|     let months = element.querySelector('input[name="interval_months"]').value; | ||||
|     let days = element.querySelector('input[name="interval_days"]').value; | ||||
|     let hours = element.querySelector('input[name="interval_hours"]').value; | ||||
|     let minutes = element.querySelector('input[name="interval_minutes"]').value; | ||||
|     let seconds = element.querySelector('input[name="interval_seconds"]').value; | ||||
|  | ||||
|     return { | ||||
|         months: parseInt(months) || null, | ||||
|         days: parseInt(days) || null, | ||||
|         seconds: | ||||
|             (parseInt(hours) || 0) * 3600 + | ||||
|                 (parseInt(minutes) || 0) * 60 + | ||||
|                 (parseInt(seconds) || 0) || null, | ||||
|     }; | ||||
| } | ||||
|  | ||||
| function update_interval(element) { | ||||
|     let months = element.querySelector('input[name="interval_months"]'); | ||||
|     let days = element.querySelector('input[name="interval_days"]'); | ||||
|     let hours = element.querySelector('input[name="interval_hours"]'); | ||||
|     let minutes = element.querySelector('input[name="interval_minutes"]'); | ||||
|     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"); | ||||
|         days.value = days.value.padStart(1, "0"); | ||||
|         hours.value = hours.value.padStart(2, "0"); | ||||
|         minutes.value = minutes.value.padStart(2, "0"); | ||||
|         seconds.value = seconds.value.padStart(2, "0"); | ||||
|  | ||||
|         if (seconds.value >= 60) { | ||||
|             let quotient = Math.floor(seconds.value / 60); | ||||
|             let remainder = seconds.value % 60; | ||||
|  | ||||
|             seconds.value = String(remainder).padStart(2, "0"); | ||||
|             minutes.value = String(Number(minutes.value) + Number(quotient)).padStart( | ||||
|                 2, | ||||
|                 "0" | ||||
|             ); | ||||
|         } | ||||
|         if (minutes.value >= 60) { | ||||
|             let quotient = Math.floor(minutes.value / 60); | ||||
|             let remainder = minutes.value % 60; | ||||
|  | ||||
|             minutes.value = String(remainder).padStart(2, "0"); | ||||
|             hours.value = String(Number(hours.value) + Number(quotient)).padStart(2, "0"); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| const $intervalGroup = document.querySelector(".interval-group"); | ||||
|  | ||||
| document.querySelector(".interval-group").addEventListener( | ||||
|     "blur", | ||||
|     (ev) => { | ||||
|         if (ev.target.nodeName !== "BUTTON") update_interval($intervalGroup); | ||||
|     }, | ||||
|     true | ||||
| ); | ||||
|  | ||||
| $intervalGroup.querySelector("button.clear").addEventListener("click", () => { | ||||
|     $intervalGroup.querySelectorAll("input").forEach((el) => { | ||||
|         el.value = ""; | ||||
|     }); | ||||
| }); | ||||
|  | ||||
| document.addEventListener("remindersLoaded", (event) => { | ||||
|     for (reminder of event.detail) { | ||||
|         let $intervalGroup = reminder.node.querySelector(".interval-group"); | ||||
|  | ||||
|         $intervalGroup.addEventListener( | ||||
|             "blur", | ||||
|             (ev) => { | ||||
|                 if (ev.target.nodeName !== "BUTTON") update_interval($intervalGroup); | ||||
|             }, | ||||
|             true | ||||
|         ); | ||||
|  | ||||
|         $intervalGroup.querySelector("button.clear").addEventListener("click", () => { | ||||
|             $intervalGroup.querySelectorAll("input").forEach((el) => { | ||||
|                 el.value = ""; | ||||
|             }); | ||||
|         }); | ||||
|     } | ||||
| }); | ||||
							
								
								
									
										7
									
								
								reminder-dashboard/public/static/js/iro.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										2
									
								
								reminder-dashboard/public/static/js/js.cookie.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,2 @@ | ||||
| /*! js-cookie v3.0.0-rc.0 | MIT */ | ||||
| !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e=e||self,function(){var r=e.Cookies,n=e.Cookies=t();n.noConflict=function(){return e.Cookies=r,n}}())}(this,function(){"use strict";function e(e){for(var t=1;t<arguments.length;t++){var r=arguments[t];for(var n in r)e[n]=r[n]}return e}var t={read:function(e){return e.replace(/%3B/g,";")},write:function(e){return e.replace(/;/g,"%3B")}};return function r(n,i){function o(r,o,u){if("undefined"!=typeof document){"number"==typeof(u=e({},i,u)).expires&&(u.expires=new Date(Date.now()+864e5*u.expires)),u.expires&&(u.expires=u.expires.toUTCString()),r=t.write(r).replace(/=/g,"%3D"),o=n.write(String(o),r);var c="";for(var f in u)u[f]&&(c+="; "+f,!0!==u[f]&&(c+="="+u[f].split(";")[0]));return document.cookie=r+"="+o+c}}return Object.create({set:o,get:function(e){if("undefined"!=typeof document&&(!arguments.length||e)){for(var r=document.cookie?document.cookie.split("; "):[],i={},o=0;o<r.length;o++){var u=r[o].split("="),c=u.slice(1).join("="),f=t.read(u[0]).replace(/%3D/g,"=");if(i[f]=n.read(c,f),e===f)break}return e?i[e]:i}},remove:function(t,r){o(t,"",e({},r,{expires:-1}))},withAttributes:function(t){return r(this.converter,e({},this.attributes,t))},withConverter:function(t){return r(e({},this.converter,t),this.attributes)}},{attributes:{value:Object.freeze(i)},converter:{value:Object.freeze(n)}})}(t,{path:"/"})}); | ||||
							
								
								
									
										1
									
								
								reminder-dashboard/public/static/js/luxon.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										1087
									
								
								reminder-dashboard/public/static/js/main.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										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, | ||||
|         }), | ||||
|     }); | ||||
| }); | ||||
							
								
								
									
										70
									
								
								reminder-dashboard/public/static/js/sort.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,70 @@ | ||||
| let guildReminders = document.querySelector("#guildReminders"); | ||||
|  | ||||
| function sort_by(cond) { | ||||
|     if (cond === "channel") { | ||||
|         [...guildReminders.children] | ||||
|             .sort((a, b) => { | ||||
|                 let channel1 = a.querySelector("select.channel-selector").value; | ||||
|                 let channel2 = b.querySelector("select.channel-selector").value; | ||||
|  | ||||
|                 return channel1 > channel2 ? 1 : -1; | ||||
|             }) | ||||
|             .forEach((node) => guildReminders.appendChild(node)); | ||||
|  | ||||
|         // go through and add channel categories | ||||
|         let currentChannelGroup = null; | ||||
|         for (let child of guildReminders.querySelectorAll("div.reminderContent")) { | ||||
|             let thisChannelGroup = child.querySelector("select.channel-selector").value; | ||||
|  | ||||
|             if (currentChannelGroup !== thisChannelGroup) { | ||||
|                 let newNode = document.createElement("div"); | ||||
|                 newNode.textContent = | ||||
|                     "#" + channels.find((a) => a.id === thisChannelGroup).name; | ||||
|                 newNode.classList.add("channel-tag"); | ||||
|  | ||||
|                 guildReminders.insertBefore(newNode, child); | ||||
|  | ||||
|                 currentChannelGroup = thisChannelGroup; | ||||
|             } | ||||
|         } | ||||
|     } else { | ||||
|         // remove any channel tags if previous ordering was by channel | ||||
|         guildReminders.querySelectorAll("div.channel-tag").forEach((el) => { | ||||
|             el.remove(); | ||||
|         }); | ||||
|  | ||||
|         if (cond === "time") { | ||||
|             [...guildReminders.children] | ||||
|                 .sort((a, b) => { | ||||
|                     let time1 = luxon.DateTime.fromISO( | ||||
|                         a.querySelector('input[name="time"]').value | ||||
|                     ); | ||||
|                     let time2 = luxon.DateTime.fromISO( | ||||
|                         b.querySelector('input[name="time"]').value | ||||
|                     ); | ||||
|  | ||||
|                     return time1 > time2 ? 1 : -1; | ||||
|                 }) | ||||
|                 .forEach((node) => guildReminders.appendChild(node)); | ||||
|         } else { | ||||
|             [...guildReminders.children] | ||||
|                 .sort((a, b) => { | ||||
|                     let name1 = a.querySelector('input[name="name"]').value; | ||||
|                     let name2 = b.querySelector('input[name="name"]').value; | ||||
|  | ||||
|                     return name1 > name2 ? 1 : -1; | ||||
|                 }) | ||||
|                 .forEach((node) => guildReminders.appendChild(node)); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| const selector = document.querySelector("#orderBy"); | ||||
|  | ||||
| selector.addEventListener("change", () => { | ||||
|     sort_by(selector.value); | ||||
| }); | ||||
|  | ||||
| document.addEventListener("remindersLoaded", () => { | ||||
|     sort_by(selector.value); | ||||
| }); | ||||