Compare commits
	
		
			215 Commits
		
	
	
		
			postman-in
			...
			jude/fix-o
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					adb9c728f4 | ||
| 
						 | 
					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 | 
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -2,6 +2,4 @@
 | 
				
			|||||||
.env
 | 
					.env
 | 
				
			||||||
/venv
 | 
					/venv
 | 
				
			||||||
.cargo
 | 
					.cargo
 | 
				
			||||||
assets
 | 
					 | 
				
			||||||
out.json
 | 
					 | 
				
			||||||
/.idea
 | 
					/.idea
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										2
									
								
								.prettierrc.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								.prettierrc.toml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,2 @@
 | 
				
			|||||||
 | 
					printWidth = 90
 | 
				
			||||||
 | 
					tabWidth = 4
 | 
				
			||||||
							
								
								
									
										3171
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										3171
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										70
									
								
								Cargo.toml
									
									
									
									
									
								
							
							
						
						
									
										70
									
								
								Cargo.toml
									
									
									
									
									
								
							@@ -1,46 +1,58 @@
 | 
				
			|||||||
[package]
 | 
					[package]
 | 
				
			||||||
name = "reminder_rs"
 | 
					name = "reminder-rs"
 | 
				
			||||||
version = "1.6.0-beta3"
 | 
					version = "1.6.48"
 | 
				
			||||||
authors = ["jellywx <judesouthworth@pm.me>"]
 | 
					authors = ["Jude Southworth <judesouthworth@pm.me>"]
 | 
				
			||||||
edition = "2018"
 | 
					edition = "2021"
 | 
				
			||||||
 | 
					license = "AGPL-3.0 only"
 | 
				
			||||||
 | 
					description = "Reminder Bot for Discord, now in Rust"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[dependencies]
 | 
					[dependencies]
 | 
				
			||||||
 | 
					poise = "0.5"
 | 
				
			||||||
dotenv = "0.15"
 | 
					dotenv = "0.15"
 | 
				
			||||||
humantime = "2.1"
 | 
					 | 
				
			||||||
tokio = { version = "1", features = ["process", "full"] }
 | 
					tokio = { version = "1", features = ["process", "full"] }
 | 
				
			||||||
reqwest = "0.11"
 | 
					reqwest = "0.11"
 | 
				
			||||||
regex = "1.4"
 | 
					lazy-regex = "3.0.2"
 | 
				
			||||||
 | 
					regex = "1.9"
 | 
				
			||||||
log = "0.4"
 | 
					log = "0.4"
 | 
				
			||||||
env_logger = "0.8"
 | 
					env_logger = "0.10"
 | 
				
			||||||
chrono = "0.4"
 | 
					chrono = "0.4"
 | 
				
			||||||
chrono-tz = { version = "0.5", features = ["serde"] }
 | 
					chrono-tz = { version = "0.8", features = ["serde"] }
 | 
				
			||||||
lazy_static = "1.4"
 | 
					lazy_static = "1.4"
 | 
				
			||||||
num-integer = "0.1"
 | 
					num-integer = "0.1"
 | 
				
			||||||
serde = "1.0"
 | 
					serde = "1.0"
 | 
				
			||||||
serde_json = "1.0"
 | 
					serde_json = "1.0"
 | 
				
			||||||
serde_repr = "0.1"
 | 
					serde_repr = "0.1"
 | 
				
			||||||
rmp-serde = "0.15"
 | 
					rmp-serde = "1.1"
 | 
				
			||||||
rand = "0.7"
 | 
					rand = "0.8"
 | 
				
			||||||
levenshtein = "1.0"
 | 
					levenshtein = "1.0"
 | 
				
			||||||
sqlx = { version = "0.5", features = ["runtime-tokio-rustls", "macros", "mysql", "bigdecimal", "chrono"]}
 | 
					sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "macros", "mysql", "bigdecimal", "chrono", "migrate"]}
 | 
				
			||||||
base64 = "0.13.0"
 | 
					base64 = "0.21.0"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[dependencies.regex_command_attr]
 | 
					[dependencies.postman]
 | 
				
			||||||
path = "command_attributes"
 | 
					path = "postman"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[dependencies.serenity]
 | 
					[dependencies.reminder_web]
 | 
				
			||||||
git = "https://github.com/serenity-rs/serenity"
 | 
					path = "web"
 | 
				
			||||||
branch = "next"
 | 
					
 | 
				
			||||||
default-features = false
 | 
					[package.metadata.deb]
 | 
				
			||||||
features = [
 | 
					depends = "$auto, python3-dateparser (>= 1.0.0)"
 | 
				
			||||||
    "builder",
 | 
					suggests = "mysql-server-8.0, nginx"
 | 
				
			||||||
    "client",
 | 
					maintainer-scripts = "debian"
 | 
				
			||||||
    "cache",
 | 
					assets = [
 | 
				
			||||||
    "gateway",
 | 
					    ["target/release/reminder-rs", "usr/bin/reminder-rs", "755"],
 | 
				
			||||||
    "http",
 | 
					    ["conf/default.env", "etc/reminder-rs/config.env", "600"],
 | 
				
			||||||
    "model",
 | 
					    ["conf/Rocket.toml", "etc/reminder-rs/Rocket.toml", "600"],
 | 
				
			||||||
    "utils",
 | 
					    ["web/static/**/*", "lib/reminder-rs/static", "644"],
 | 
				
			||||||
    "rustls_backend",
 | 
					    ["web/templates/**/*", "lib/reminder-rs/templates", "644"],
 | 
				
			||||||
    "collector",
 | 
					    ["healthcheck", "lib/reminder-rs/healthcheck", "755"],
 | 
				
			||||||
    "unstable_discord_api"
 | 
					    ["cron.d/reminder_health", "etc/cron.d/reminder_health", "644"],
 | 
				
			||||||
 | 
					#    ["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
									
								
							
							
						
						
									
										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
 | 
				
			||||||
 | 
					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
 | 
				
			||||||
							
								
								
									
										49
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										49
									
								
								README.md
									
									
									
									
									
								
							@@ -2,23 +2,41 @@
 | 
				
			|||||||
Reminder Bot for Discord.
 | 
					Reminder Bot for Discord.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## How do I use it?
 | 
					## How do I use it?
 | 
				
			||||||
We offer a hosted version of the bot. You can invite it with: **https://invite.reminder-bot.com**. The catch is that repeating 
 | 
					I offer a hosted version of the bot. You can invite it with: **https://invite.reminder-bot.com**. The catch is that repeating 
 | 
				
			||||||
reminders are paid on the hosted version of the bot. Keep reading if you want to host it yourself.
 | 
					reminders are paid on the hosted version of the bot. Keep reading if you want to host it yourself.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
You'll need rustc and cargo for compilation. To run, you'll need Python 3 still (due to no suitable replacement for dateparser in Rust)
 | 
					You'll need rustc and cargo for compilation. To run, you'll need Python 3 still (due to no suitable replacement for dateparser in Rust)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### Compiling
 | 
					### Build APT package
 | 
				
			||||||
Reminder Bot can be built by running `cargo build --release` in the top level directory. It is necessary to create a folder called 'assets' containing an image file with its name specified in the environment as `WEBHOOK_AVATAR`, of dimensions 128x128px to be used as the webhook avatar.
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
#### Compilation environment variables
 | 
					Recommended method.
 | 
				
			||||||
These environment variables must be provided when compiling the bot
 | 
					 | 
				
			||||||
* `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**
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
### Setting up Python
 | 
					By default, this builds targeting Ubuntu 20.04. Modify the Containerfile if you wish to target a different platform. These instructions are written using `podman`, but `docker` should work too.
 | 
				
			||||||
Reminder Bot 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
 | 
					
 | 
				
			||||||
 | 
					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.
 | 
					Reminder Bot reads a number of environment variables. Some are essential, and others have hardcoded fallbacks. Environment variables can be loaded from a .env file in the working directory.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
__Required Variables__
 | 
					__Required Variables__
 | 
				
			||||||
@@ -30,14 +48,5 @@ __Other Variables__
 | 
				
			|||||||
* `LOCAL_TIMEZONE` - default `UTC`, necessary for calculations in the natural language processor
 | 
					* `LOCAL_TIMEZONE` - default `UTC`, necessary for calculations in the natural language processor
 | 
				
			||||||
* `SUBSCRIPTION_ROLES` - default `None`, accepts a list of Discord role IDs that are given to subscribed users
 | 
					* `SUBSCRIPTION_ROLES` - default `None`, accepts a list of Discord role IDs that are given to subscribed users
 | 
				
			||||||
* `CNC_GUILD` - default `None`, accepts a single Discord guild ID for the server that the subscription roles belong to
 | 
					* `CNC_GUILD` - default `None`, accepts a single Discord guild ID for the server that the subscription roles belong to
 | 
				
			||||||
* `IGNORE_BOTS` - default `1`, if `1`, Reminder Bot will ignore all other bots
 | 
					* `PYTHON_LOCATION` - default `/usr/bin/python3`. Can be changed if your Python executable is located somewhere else
 | 
				
			||||||
* `PYTHON_LOCATION` - default `venv/bin/python3`. Can be changed if your Python executable is located somewhere else
 | 
					 | 
				
			||||||
* `THEME_COLOR` - default `8fb677`. Specifies the hex value of the color to use on info message embeds 
 | 
					* `THEME_COLOR` - default `8fb677`. Specifies the hex value of the color to use on info message embeds 
 | 
				
			||||||
* `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
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### Todo List
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
* Convert aliases to macros
 | 
					 | 
				
			||||||
* Help command
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										28
									
								
								Rocket.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								Rocket.toml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,28 @@
 | 
				
			|||||||
 | 
					[default]
 | 
				
			||||||
 | 
					address = "0.0.0.0"
 | 
				
			||||||
 | 
					port = 18920
 | 
				
			||||||
 | 
					template_dir = "web/templates"
 | 
				
			||||||
 | 
					limits = { json = "10MiB" }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[debug]
 | 
				
			||||||
 | 
					secret_key = "tR8krio5FXTnnyIZNiJDXPondz0kI1v6X6BXZcBGIRY="
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[debug.tls]
 | 
				
			||||||
 | 
					certs = "web/private/rsa_sha256_cert.pem"
 | 
				
			||||||
 | 
					key = "web/private/rsa_sha256_key.pem"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[debug.rsa_sha256.tls]
 | 
				
			||||||
 | 
					certs = "web/private/rsa_sha256_cert.pem"
 | 
				
			||||||
 | 
					key = "web/private/rsa_sha256_key.pem"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[debug.ecdsa_nistp256_sha256.tls]
 | 
				
			||||||
 | 
					certs = "web/private/ecdsa_nistp256_sha256_cert.pem"
 | 
				
			||||||
 | 
					key = "web/private/ecdsa_nistp256_sha256_key_pkcs8.pem"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[debug.ecdsa_nistp384_sha384.tls]
 | 
				
			||||||
 | 
					certs = "web/private/ecdsa_nistp384_sha384_cert.pem"
 | 
				
			||||||
 | 
					key = "web/private/ecdsa_nistp384_sha384_key_pkcs8.pem"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[debug.ed25519.tls]
 | 
				
			||||||
 | 
					certs = "web/private/ed25519_cert.pem"
 | 
				
			||||||
 | 
					key = "eb/private/ed25519_key.pem"
 | 
				
			||||||
							
								
								
									
										
											BIN
										
									
								
								assets/webhook.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								assets/webhook.jpg
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 21 KiB  | 
							
								
								
									
										3
									
								
								build.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								build.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,3 @@
 | 
				
			|||||||
 | 
					fn main() {
 | 
				
			||||||
 | 
					    println!("cargo:rerun-if-changed=migrations");
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,16 +0,0 @@
 | 
				
			|||||||
[package]
 | 
					 | 
				
			||||||
name = "regex_command_attr"
 | 
					 | 
				
			||||||
version = "0.3.6"
 | 
					 | 
				
			||||||
authors = ["acdenisSK <acdenissk69@gmail.com>", "jellywx <judesouthworth@pm.me>"]
 | 
					 | 
				
			||||||
edition = "2018"
 | 
					 | 
				
			||||||
description = "Procedural macros for command creation for the Serenity library."
 | 
					 | 
				
			||||||
license = "ISC"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
[lib]
 | 
					 | 
				
			||||||
proc-macro = true
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
[dependencies]
 | 
					 | 
				
			||||||
quote = "^1.0"
 | 
					 | 
				
			||||||
syn = { version = "^1.0", features = ["full", "derive", "extra-traits"] }
 | 
					 | 
				
			||||||
proc-macro2 = "1.0"
 | 
					 | 
				
			||||||
uuid = { version = "0.8", features = ["v4"] }
 | 
					 | 
				
			||||||
@@ -1,351 +0,0 @@
 | 
				
			|||||||
use std::fmt::{self, Write};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use proc_macro2::Span;
 | 
					 | 
				
			||||||
use syn::{
 | 
					 | 
				
			||||||
    parse::{Error, Result},
 | 
					 | 
				
			||||||
    spanned::Spanned,
 | 
					 | 
				
			||||||
    Attribute, Ident, Lit, LitStr, Meta, NestedMeta, Path,
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use crate::{
 | 
					 | 
				
			||||||
    structures::{ApplicationCommandOptionType, Arg},
 | 
					 | 
				
			||||||
    util::{AsOption, LitExt},
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Debug, Clone, Copy, PartialEq)]
 | 
					 | 
				
			||||||
pub enum ValueKind {
 | 
					 | 
				
			||||||
    // #[<name>]
 | 
					 | 
				
			||||||
    Name,
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // #[<name> = <value>]
 | 
					 | 
				
			||||||
    Equals,
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // #[<name>([<value>, <value>, <value>, ...])]
 | 
					 | 
				
			||||||
    List,
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // #[<name>([<prop> = <value>, <prop> = <value>, ...])]
 | 
					 | 
				
			||||||
    EqualsList,
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // #[<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::EqualsList => {
 | 
					 | 
				
			||||||
                f.pad("`#[<name>([<prop> = <value>, <prop> = <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<(Option<String>, Lit)>,
 | 
					 | 
				
			||||||
    pub kind: ValueKind,
 | 
					 | 
				
			||||||
    pub span: Span,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl Values {
 | 
					 | 
				
			||||||
    #[inline]
 | 
					 | 
				
			||||||
    pub fn new(
 | 
					 | 
				
			||||||
        name: Ident,
 | 
					 | 
				
			||||||
        kind: ValueKind,
 | 
					 | 
				
			||||||
        literals: Vec<(Option<String>, Lit)>,
 | 
					 | 
				
			||||||
        span: Span,
 | 
					 | 
				
			||||||
    ) -> Self {
 | 
					 | 
				
			||||||
        Values { name, literals, kind, span }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
pub fn parse_values(attr: &Attribute) -> Result<Values> {
 | 
					 | 
				
			||||||
    fn is_list_or_named_list(meta: &NestedMeta) -> ValueKind {
 | 
					 | 
				
			||||||
        match meta {
 | 
					 | 
				
			||||||
            // catch if the nested value is a literal value
 | 
					 | 
				
			||||||
            NestedMeta::Lit(_) => ValueKind::List,
 | 
					 | 
				
			||||||
            // catch if the nested value is a meta value
 | 
					 | 
				
			||||||
            NestedMeta::Meta(m) => match m {
 | 
					 | 
				
			||||||
                // path => some quoted value
 | 
					 | 
				
			||||||
                Meta::Path(_) => ValueKind::List,
 | 
					 | 
				
			||||||
                Meta::List(_) | Meta::NameValue(_) => ValueKind::EqualsList,
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    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"));
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if is_list_or_named_list(nested.first().unwrap()) == ValueKind::List {
 | 
					 | 
				
			||||||
                let mut lits = Vec::with_capacity(nested.len());
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                for meta in nested {
 | 
					 | 
				
			||||||
                    match meta {
 | 
					 | 
				
			||||||
                        // catch if the nested value is a literal value
 | 
					 | 
				
			||||||
                        NestedMeta::Lit(l) => lits.push((None, l)),
 | 
					 | 
				
			||||||
                        // catch if the nested value is a meta value
 | 
					 | 
				
			||||||
                        NestedMeta::Meta(m) => match m {
 | 
					 | 
				
			||||||
                            // path => some quoted value
 | 
					 | 
				
			||||||
                            Meta::Path(path) => {
 | 
					 | 
				
			||||||
                                let i = to_ident(path)?;
 | 
					 | 
				
			||||||
                                lits.push((None, 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()))
 | 
					 | 
				
			||||||
            } else {
 | 
					 | 
				
			||||||
                let mut lits = Vec::with_capacity(nested.len());
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                for meta in nested {
 | 
					 | 
				
			||||||
                    match meta {
 | 
					 | 
				
			||||||
                        // catch if the nested value is a literal value
 | 
					 | 
				
			||||||
                        NestedMeta::Lit(_) => {
 | 
					 | 
				
			||||||
                            return Err(Error::new(attr.span(), "key-value pairs expected"))
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
                        // catch if the nested value is a meta value
 | 
					 | 
				
			||||||
                        NestedMeta::Meta(m) => match m {
 | 
					 | 
				
			||||||
                            Meta::NameValue(n) => {
 | 
					 | 
				
			||||||
                                let name = to_ident(n.path)?.to_string();
 | 
					 | 
				
			||||||
                                let value = n.lit;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                                lits.push((Some(name), value));
 | 
					 | 
				
			||||||
                            }
 | 
					 | 
				
			||||||
                            Meta::List(_) | Meta::Path(_) => {
 | 
					 | 
				
			||||||
                                return Err(Error::new(attr.span(), "key-value pairs expected"))
 | 
					 | 
				
			||||||
                            }
 | 
					 | 
				
			||||||
                        },
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                Ok(Values::new(name, ValueKind::EqualsList, lits, attr.span()))
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        Meta::NameValue(meta) => {
 | 
					 | 
				
			||||||
            let name = to_ident(meta.path)?;
 | 
					 | 
				
			||||||
            let lit = meta.lit;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            Ok(Values::new(name, ValueKind::Equals, vec![(None, 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(|(_, l)| l.to_str()).collect())
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl AttributeOption for String {
 | 
					 | 
				
			||||||
    #[inline]
 | 
					 | 
				
			||||||
    fn parse(values: Values) -> Result<Self> {
 | 
					 | 
				
			||||||
        validate(&values, &[ValueKind::Equals, ValueKind::SingleList])?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        Ok(values.literals[0].1.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].1.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 Arg {
 | 
					 | 
				
			||||||
    fn parse(values: Values) -> Result<Self> {
 | 
					 | 
				
			||||||
        validate(&values, &[ValueKind::EqualsList])?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        let mut arg: Arg = Default::default();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        for (key, value) in &values.literals {
 | 
					 | 
				
			||||||
            match key {
 | 
					 | 
				
			||||||
                Some(s) => match s.as_str() {
 | 
					 | 
				
			||||||
                    "name" => {
 | 
					 | 
				
			||||||
                        arg.name = value.to_str();
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                    "description" => {
 | 
					 | 
				
			||||||
                        arg.description = value.to_str();
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                    "required" => {
 | 
					 | 
				
			||||||
                        arg.required = value.to_bool();
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                    "kind" => arg.kind = ApplicationCommandOptionType::from_str(value.to_str()),
 | 
					 | 
				
			||||||
                    _ => {
 | 
					 | 
				
			||||||
                        return Err(Error::new(key.span(), "unexpected attribute"));
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                },
 | 
					 | 
				
			||||||
                _ => {
 | 
					 | 
				
			||||||
                    return Err(Error::new(key.span(), "unnamed attribute"));
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        Ok(arg)
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
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].1 {
 | 
					 | 
				
			||||||
                        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,10 +0,0 @@
 | 
				
			|||||||
pub mod suffixes {
 | 
					 | 
				
			||||||
    pub const COMMAND: &str = "COMMAND";
 | 
					 | 
				
			||||||
    pub const ARG: &str = "ARG";
 | 
					 | 
				
			||||||
    pub const SUBCOMMAND: &str = "SUBCOMMAND";
 | 
					 | 
				
			||||||
    pub const SUBCOMMAND_GROUP: &str = "GROUP";
 | 
					 | 
				
			||||||
    pub const CHECK: &str = "CHECK";
 | 
					 | 
				
			||||||
    pub const HOOK: &str = "HOOK";
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
pub use self::suffixes::*;
 | 
					 | 
				
			||||||
@@ -1,321 +0,0 @@
 | 
				
			|||||||
#![deny(rust_2018_idioms)]
 | 
					 | 
				
			||||||
#![deny(broken_intra_doc_links)]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use proc_macro::TokenStream;
 | 
					 | 
				
			||||||
use proc_macro2::Ident;
 | 
					 | 
				
			||||||
use quote::quote;
 | 
					 | 
				
			||||||
use syn::{parse::Error, parse_macro_input, parse_quote, spanned::Spanned, Lit, Type};
 | 
					 | 
				
			||||||
use uuid::Uuid;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
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 {
 | 
					 | 
				
			||||||
    enum LastItem {
 | 
					 | 
				
			||||||
        Fun,
 | 
					 | 
				
			||||||
        SubFun,
 | 
					 | 
				
			||||||
        SubGroup,
 | 
					 | 
				
			||||||
        SubGroupFun,
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let mut fun = parse_macro_input!(input as CommandFun);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let _name = if !attr.is_empty() {
 | 
					 | 
				
			||||||
        parse_macro_input!(attr as Lit).to_str()
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
        fun.name.to_string()
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let mut hooks: Vec<Ident> = Vec::new();
 | 
					 | 
				
			||||||
    let mut options = Options::new();
 | 
					 | 
				
			||||||
    let mut last_desc = LastItem::Fun;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    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 name {
 | 
					 | 
				
			||||||
            "subcommand" => {
 | 
					 | 
				
			||||||
                let new_subcommand = Subcommand::new(propagate_err!(attributes::parse(values)));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                if let Some(subcommand_group) = options.subcommand_groups.last_mut() {
 | 
					 | 
				
			||||||
                    last_desc = LastItem::SubGroupFun;
 | 
					 | 
				
			||||||
                    subcommand_group.subcommands.push(new_subcommand);
 | 
					 | 
				
			||||||
                } else {
 | 
					 | 
				
			||||||
                    last_desc = LastItem::SubFun;
 | 
					 | 
				
			||||||
                    options.subcommands.push(new_subcommand);
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            "subcommandgroup" => {
 | 
					 | 
				
			||||||
                let new_group = SubcommandGroup::new(propagate_err!(attributes::parse(values)));
 | 
					 | 
				
			||||||
                last_desc = LastItem::SubGroup;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                options.subcommand_groups.push(new_group);
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            "arg" => {
 | 
					 | 
				
			||||||
                let arg = propagate_err!(attributes::parse(values));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                match last_desc {
 | 
					 | 
				
			||||||
                    LastItem::Fun => {
 | 
					 | 
				
			||||||
                        options.cmd_args.push(arg);
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                    LastItem::SubFun => {
 | 
					 | 
				
			||||||
                        options.subcommands.last_mut().unwrap().cmd_args.push(arg);
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                    LastItem::SubGroup => {
 | 
					 | 
				
			||||||
                        panic!("Argument not expected under subcommand group");
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                    LastItem::SubGroupFun => {
 | 
					 | 
				
			||||||
                        options
 | 
					 | 
				
			||||||
                            .subcommand_groups
 | 
					 | 
				
			||||||
                            .last_mut()
 | 
					 | 
				
			||||||
                            .unwrap()
 | 
					 | 
				
			||||||
                            .subcommands
 | 
					 | 
				
			||||||
                            .last_mut()
 | 
					 | 
				
			||||||
                            .unwrap()
 | 
					 | 
				
			||||||
                            .cmd_args
 | 
					 | 
				
			||||||
                            .push(arg);
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            "example" => {
 | 
					 | 
				
			||||||
                options.examples.push(propagate_err!(attributes::parse(values)));
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            "description" => {
 | 
					 | 
				
			||||||
                let line: String = propagate_err!(attributes::parse(values));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                match last_desc {
 | 
					 | 
				
			||||||
                    LastItem::Fun => {
 | 
					 | 
				
			||||||
                        util::append_line(&mut options.description, line);
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                    LastItem::SubFun => {
 | 
					 | 
				
			||||||
                        util::append_line(
 | 
					 | 
				
			||||||
                            &mut options.subcommands.last_mut().unwrap().description,
 | 
					 | 
				
			||||||
                            line,
 | 
					 | 
				
			||||||
                        );
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                    LastItem::SubGroup => {
 | 
					 | 
				
			||||||
                        util::append_line(
 | 
					 | 
				
			||||||
                            &mut options.subcommand_groups.last_mut().unwrap().description,
 | 
					 | 
				
			||||||
                            line,
 | 
					 | 
				
			||||||
                        );
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                    LastItem::SubGroupFun => {
 | 
					 | 
				
			||||||
                        util::append_line(
 | 
					 | 
				
			||||||
                            &mut options
 | 
					 | 
				
			||||||
                                .subcommand_groups
 | 
					 | 
				
			||||||
                                .last_mut()
 | 
					 | 
				
			||||||
                                .unwrap()
 | 
					 | 
				
			||||||
                                .subcommands
 | 
					 | 
				
			||||||
                                .last_mut()
 | 
					 | 
				
			||||||
                                .unwrap()
 | 
					 | 
				
			||||||
                                .description,
 | 
					 | 
				
			||||||
                            line,
 | 
					 | 
				
			||||||
                        );
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            "hook" => {
 | 
					 | 
				
			||||||
                hooks.push(propagate_err!(attributes::parse(values)));
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            _ => {
 | 
					 | 
				
			||||||
                match_options!(name, values, options, span => [
 | 
					 | 
				
			||||||
                    aliases;
 | 
					 | 
				
			||||||
                    group;
 | 
					 | 
				
			||||||
                    can_blacklist;
 | 
					 | 
				
			||||||
                    supports_dm
 | 
					 | 
				
			||||||
                ]);
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let Options {
 | 
					 | 
				
			||||||
        aliases,
 | 
					 | 
				
			||||||
        description,
 | 
					 | 
				
			||||||
        group,
 | 
					 | 
				
			||||||
        examples,
 | 
					 | 
				
			||||||
        can_blacklist,
 | 
					 | 
				
			||||||
        supports_dm,
 | 
					 | 
				
			||||||
        mut cmd_args,
 | 
					 | 
				
			||||||
        mut subcommands,
 | 
					 | 
				
			||||||
        mut subcommand_groups,
 | 
					 | 
				
			||||||
    } = options;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let visibility = fun.visibility;
 | 
					 | 
				
			||||||
    let name = fun.name.clone();
 | 
					 | 
				
			||||||
    let body = fun.body;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let root_ident = name.with_suffix(COMMAND);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let command_path = quote!(crate::framework::Command);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    populate_fut_lifetimes_on_refs(&mut fun.args);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let mut subcommand_group_idents = subcommand_groups
 | 
					 | 
				
			||||||
        .iter()
 | 
					 | 
				
			||||||
        .map(|subcommand| {
 | 
					 | 
				
			||||||
            root_ident
 | 
					 | 
				
			||||||
                .with_suffix(subcommand.name.replace("-", "_").as_str())
 | 
					 | 
				
			||||||
                .with_suffix(SUBCOMMAND_GROUP)
 | 
					 | 
				
			||||||
        })
 | 
					 | 
				
			||||||
        .collect::<Vec<Ident>>();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let mut subcommand_idents = subcommands
 | 
					 | 
				
			||||||
        .iter()
 | 
					 | 
				
			||||||
        .map(|subcommand| {
 | 
					 | 
				
			||||||
            root_ident
 | 
					 | 
				
			||||||
                .with_suffix(subcommand.name.replace("-", "_").as_str())
 | 
					 | 
				
			||||||
                .with_suffix(SUBCOMMAND)
 | 
					 | 
				
			||||||
        })
 | 
					 | 
				
			||||||
        .collect::<Vec<Ident>>();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let mut arg_idents = cmd_args
 | 
					 | 
				
			||||||
        .iter()
 | 
					 | 
				
			||||||
        .map(|arg| root_ident.with_suffix(arg.name.replace("-", "_").as_str()).with_suffix(ARG))
 | 
					 | 
				
			||||||
        .collect::<Vec<Ident>>();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let mut tokens = quote! {};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    tokens.extend(
 | 
					 | 
				
			||||||
        subcommand_groups
 | 
					 | 
				
			||||||
            .iter_mut()
 | 
					 | 
				
			||||||
            .zip(subcommand_group_idents.iter())
 | 
					 | 
				
			||||||
            .map(|(group, group_ident)| group.as_tokens(group_ident))
 | 
					 | 
				
			||||||
            .fold(quote! {}, |mut a, b| {
 | 
					 | 
				
			||||||
                a.extend(b);
 | 
					 | 
				
			||||||
                a
 | 
					 | 
				
			||||||
            }),
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    tokens.extend(
 | 
					 | 
				
			||||||
        subcommands
 | 
					 | 
				
			||||||
            .iter_mut()
 | 
					 | 
				
			||||||
            .zip(subcommand_idents.iter())
 | 
					 | 
				
			||||||
            .map(|(subcommand, sc_ident)| subcommand.as_tokens(sc_ident))
 | 
					 | 
				
			||||||
            .fold(quote! {}, |mut a, b| {
 | 
					 | 
				
			||||||
                a.extend(b);
 | 
					 | 
				
			||||||
                a
 | 
					 | 
				
			||||||
            }),
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    tokens.extend(
 | 
					 | 
				
			||||||
        cmd_args.iter_mut().zip(arg_idents.iter()).map(|(arg, ident)| arg.as_tokens(ident)).fold(
 | 
					 | 
				
			||||||
            quote! {},
 | 
					 | 
				
			||||||
            |mut a, b| {
 | 
					 | 
				
			||||||
                a.extend(b);
 | 
					 | 
				
			||||||
                a
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    arg_idents.append(&mut subcommand_group_idents);
 | 
					 | 
				
			||||||
    arg_idents.append(&mut subcommand_idents);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let args = fun.args;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let variant = if args.len() == 2 {
 | 
					 | 
				
			||||||
        quote!(crate::framework::CommandFnType::Multi)
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
        let string: Type = parse_quote!(String);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        let final_arg = args.get(2).unwrap();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if final_arg.kind == string {
 | 
					 | 
				
			||||||
            quote!(crate::framework::CommandFnType::Text)
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
            quote!(crate::framework::CommandFnType::Slash)
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    tokens.extend(quote! {
 | 
					 | 
				
			||||||
        #[allow(missing_docs)]
 | 
					 | 
				
			||||||
        pub static #root_ident: #command_path = #command_path {
 | 
					 | 
				
			||||||
            fun: #variant(#name),
 | 
					 | 
				
			||||||
            names: &[#_name, #(#aliases),*],
 | 
					 | 
				
			||||||
            desc: #description,
 | 
					 | 
				
			||||||
            group: #group,
 | 
					 | 
				
			||||||
            examples: &[#(#examples),*],
 | 
					 | 
				
			||||||
            can_blacklist: #can_blacklist,
 | 
					 | 
				
			||||||
            supports_dm: #supports_dm,
 | 
					 | 
				
			||||||
            args: &[#(&#arg_idents),*],
 | 
					 | 
				
			||||||
            hooks: &[#(&#hooks),*],
 | 
					 | 
				
			||||||
        };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        #[allow(missing_docs)]
 | 
					 | 
				
			||||||
        #visibility fn #name<'fut> (#(#args),*) -> ::serenity::futures::future::BoxFuture<'fut, ()> {
 | 
					 | 
				
			||||||
            use ::serenity::futures::future::FutureExt;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            async move {
 | 
					 | 
				
			||||||
                #(#body)*;
 | 
					 | 
				
			||||||
            }.boxed()
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    tokens.into()
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[proc_macro_attribute]
 | 
					 | 
				
			||||||
pub fn check(_attr: TokenStream, input: TokenStream) -> TokenStream {
 | 
					 | 
				
			||||||
    let mut fun = parse_macro_input!(input as CommandFun);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let n = fun.name.clone();
 | 
					 | 
				
			||||||
    let name = n.with_suffix(HOOK);
 | 
					 | 
				
			||||||
    let fn_name = n.with_suffix(CHECK);
 | 
					 | 
				
			||||||
    let visibility = fun.visibility;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let body = fun.body;
 | 
					 | 
				
			||||||
    let ret = fun.ret;
 | 
					 | 
				
			||||||
    populate_fut_lifetimes_on_refs(&mut fun.args);
 | 
					 | 
				
			||||||
    let args = fun.args;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let hook_path = quote!(crate::framework::Hook);
 | 
					 | 
				
			||||||
    let uuid = Uuid::new_v4().as_u128();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    (quote! {
 | 
					 | 
				
			||||||
        #[allow(missing_docs)]
 | 
					 | 
				
			||||||
        #visibility fn #fn_name<'fut>(#(#args),*) -> ::serenity::futures::future::BoxFuture<'fut, #ret> {
 | 
					 | 
				
			||||||
            use ::serenity::futures::future::FutureExt;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            async move {
 | 
					 | 
				
			||||||
                let _output: #ret = { #(#body)* };
 | 
					 | 
				
			||||||
                #[allow(unreachable_code)]
 | 
					 | 
				
			||||||
                _output
 | 
					 | 
				
			||||||
            }.boxed()
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        #[allow(missing_docs)]
 | 
					 | 
				
			||||||
        pub static #name: #hook_path = #hook_path {
 | 
					 | 
				
			||||||
            fun: #fn_name,
 | 
					 | 
				
			||||||
            uuid: #uuid,
 | 
					 | 
				
			||||||
        };
 | 
					 | 
				
			||||||
    })
 | 
					 | 
				
			||||||
    .into()
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,331 +0,0 @@
 | 
				
			|||||||
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, ReturnType, Stmt, Token, Type, Visibility,
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use crate::{
 | 
					 | 
				
			||||||
    consts::{ARG, SUBCOMMAND},
 | 
					 | 
				
			||||||
    util::{Argument, IdentExt2, Parenthesised},
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
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)))
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[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 visibility: Visibility,
 | 
					 | 
				
			||||||
    pub name: Ident,
 | 
					 | 
				
			||||||
    pub args: Vec<Argument>,
 | 
					 | 
				
			||||||
    pub ret: Type,
 | 
					 | 
				
			||||||
    pub body: Vec<Stmt>,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl Parse for CommandFun {
 | 
					 | 
				
			||||||
    fn parse(input: ParseStream<'_>) -> Result<Self> {
 | 
					 | 
				
			||||||
        let attributes = input.call(Attribute::parse_outer)?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        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 ret = match input.parse::<ReturnType>()? {
 | 
					 | 
				
			||||||
            ReturnType::Type(_, t) => (*t).clone(),
 | 
					 | 
				
			||||||
            ReturnType::Default => Type::Verbatim(quote!(())),
 | 
					 | 
				
			||||||
        };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // { ... }
 | 
					 | 
				
			||||||
        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, visibility, name, args, ret, body })
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl ToTokens for CommandFun {
 | 
					 | 
				
			||||||
    fn to_tokens(&self, stream: &mut TokenStream2) {
 | 
					 | 
				
			||||||
        let Self { attributes: _, visibility, name, args, ret, body } = self;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        stream.extend(quote! {
 | 
					 | 
				
			||||||
            #visibility async fn #name (#(#args),*) -> #ret {
 | 
					 | 
				
			||||||
                #(#body)*
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Debug)]
 | 
					 | 
				
			||||||
pub(crate) enum ApplicationCommandOptionType {
 | 
					 | 
				
			||||||
    SubCommand,
 | 
					 | 
				
			||||||
    SubCommandGroup,
 | 
					 | 
				
			||||||
    String,
 | 
					 | 
				
			||||||
    Integer,
 | 
					 | 
				
			||||||
    Boolean,
 | 
					 | 
				
			||||||
    User,
 | 
					 | 
				
			||||||
    Channel,
 | 
					 | 
				
			||||||
    Role,
 | 
					 | 
				
			||||||
    Mentionable,
 | 
					 | 
				
			||||||
    Number,
 | 
					 | 
				
			||||||
    Unknown,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl ApplicationCommandOptionType {
 | 
					 | 
				
			||||||
    pub fn from_str(s: String) -> Self {
 | 
					 | 
				
			||||||
        match s.as_str() {
 | 
					 | 
				
			||||||
            "SubCommand" => Self::SubCommand,
 | 
					 | 
				
			||||||
            "SubCommandGroup" => Self::SubCommandGroup,
 | 
					 | 
				
			||||||
            "String" => Self::String,
 | 
					 | 
				
			||||||
            "Integer" => Self::Integer,
 | 
					 | 
				
			||||||
            "Boolean" => Self::Boolean,
 | 
					 | 
				
			||||||
            "User" => Self::User,
 | 
					 | 
				
			||||||
            "Channel" => Self::Channel,
 | 
					 | 
				
			||||||
            "Role" => Self::Role,
 | 
					 | 
				
			||||||
            "Mentionable" => Self::Mentionable,
 | 
					 | 
				
			||||||
            "Number" => Self::Number,
 | 
					 | 
				
			||||||
            _ => Self::Unknown,
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl ToTokens for ApplicationCommandOptionType {
 | 
					 | 
				
			||||||
    fn to_tokens(&self, stream: &mut TokenStream2) {
 | 
					 | 
				
			||||||
        let path = quote!(
 | 
					 | 
				
			||||||
            serenity::model::interactions::application_command::ApplicationCommandOptionType
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
        let variant = match self {
 | 
					 | 
				
			||||||
            ApplicationCommandOptionType::SubCommand => quote!(SubCommand),
 | 
					 | 
				
			||||||
            ApplicationCommandOptionType::SubCommandGroup => quote!(SubCommandGroup),
 | 
					 | 
				
			||||||
            ApplicationCommandOptionType::String => quote!(String),
 | 
					 | 
				
			||||||
            ApplicationCommandOptionType::Integer => quote!(Integer),
 | 
					 | 
				
			||||||
            ApplicationCommandOptionType::Boolean => quote!(Boolean),
 | 
					 | 
				
			||||||
            ApplicationCommandOptionType::User => quote!(User),
 | 
					 | 
				
			||||||
            ApplicationCommandOptionType::Channel => quote!(Channel),
 | 
					 | 
				
			||||||
            ApplicationCommandOptionType::Role => quote!(Role),
 | 
					 | 
				
			||||||
            ApplicationCommandOptionType::Mentionable => quote!(Mentionable),
 | 
					 | 
				
			||||||
            ApplicationCommandOptionType::Number => quote!(Number),
 | 
					 | 
				
			||||||
            ApplicationCommandOptionType::Unknown => quote!(Unknown),
 | 
					 | 
				
			||||||
        };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        stream.extend(quote! {
 | 
					 | 
				
			||||||
            #path::#variant
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Debug)]
 | 
					 | 
				
			||||||
pub(crate) struct Arg {
 | 
					 | 
				
			||||||
    pub name: String,
 | 
					 | 
				
			||||||
    pub description: String,
 | 
					 | 
				
			||||||
    pub kind: ApplicationCommandOptionType,
 | 
					 | 
				
			||||||
    pub required: bool,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl Arg {
 | 
					 | 
				
			||||||
    pub fn as_tokens(&self, ident: &Ident) -> TokenStream2 {
 | 
					 | 
				
			||||||
        let arg_path = quote!(crate::framework::Arg);
 | 
					 | 
				
			||||||
        let Arg { name, description, kind, required } = self;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        quote! {
 | 
					 | 
				
			||||||
            #[allow(missing_docs)]
 | 
					 | 
				
			||||||
            pub static #ident: #arg_path = #arg_path {
 | 
					 | 
				
			||||||
                name: #name,
 | 
					 | 
				
			||||||
                description: #description,
 | 
					 | 
				
			||||||
                kind: #kind,
 | 
					 | 
				
			||||||
                required: #required,
 | 
					 | 
				
			||||||
                options: &[]
 | 
					 | 
				
			||||||
            };
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl Default for Arg {
 | 
					 | 
				
			||||||
    fn default() -> Self {
 | 
					 | 
				
			||||||
        Self {
 | 
					 | 
				
			||||||
            name: String::new(),
 | 
					 | 
				
			||||||
            description: String::new(),
 | 
					 | 
				
			||||||
            kind: ApplicationCommandOptionType::String,
 | 
					 | 
				
			||||||
            required: false,
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Debug)]
 | 
					 | 
				
			||||||
pub(crate) struct Subcommand {
 | 
					 | 
				
			||||||
    pub name: String,
 | 
					 | 
				
			||||||
    pub description: String,
 | 
					 | 
				
			||||||
    pub cmd_args: Vec<Arg>,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl Subcommand {
 | 
					 | 
				
			||||||
    pub fn as_tokens(&mut self, ident: &Ident) -> TokenStream2 {
 | 
					 | 
				
			||||||
        let arg_path = quote!(crate::framework::Arg);
 | 
					 | 
				
			||||||
        let subcommand_path = ApplicationCommandOptionType::SubCommand;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        let arg_idents = self
 | 
					 | 
				
			||||||
            .cmd_args
 | 
					 | 
				
			||||||
            .iter()
 | 
					 | 
				
			||||||
            .map(|arg| ident.with_suffix(arg.name.as_str()).with_suffix(ARG))
 | 
					 | 
				
			||||||
            .collect::<Vec<Ident>>();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        let mut tokens = self
 | 
					 | 
				
			||||||
            .cmd_args
 | 
					 | 
				
			||||||
            .iter_mut()
 | 
					 | 
				
			||||||
            .zip(arg_idents.iter())
 | 
					 | 
				
			||||||
            .map(|(arg, ident)| arg.as_tokens(ident))
 | 
					 | 
				
			||||||
            .fold(quote! {}, |mut a, b| {
 | 
					 | 
				
			||||||
                a.extend(b);
 | 
					 | 
				
			||||||
                a
 | 
					 | 
				
			||||||
            });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        let Subcommand { name, description, .. } = self;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        tokens.extend(quote! {
 | 
					 | 
				
			||||||
            #[allow(missing_docs)]
 | 
					 | 
				
			||||||
            pub static #ident: #arg_path = #arg_path {
 | 
					 | 
				
			||||||
                name: #name,
 | 
					 | 
				
			||||||
                description: #description,
 | 
					 | 
				
			||||||
                kind: #subcommand_path,
 | 
					 | 
				
			||||||
                required: false,
 | 
					 | 
				
			||||||
                options: &[#(&#arg_idents),*],
 | 
					 | 
				
			||||||
            };
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        tokens
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl Default for Subcommand {
 | 
					 | 
				
			||||||
    fn default() -> Self {
 | 
					 | 
				
			||||||
        Self { name: String::new(), description: String::new(), cmd_args: vec![] }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl Subcommand {
 | 
					 | 
				
			||||||
    pub(crate) fn new(name: String) -> Self {
 | 
					 | 
				
			||||||
        Self { name, ..Default::default() }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Debug)]
 | 
					 | 
				
			||||||
pub(crate) struct SubcommandGroup {
 | 
					 | 
				
			||||||
    pub name: String,
 | 
					 | 
				
			||||||
    pub description: String,
 | 
					 | 
				
			||||||
    pub subcommands: Vec<Subcommand>,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl SubcommandGroup {
 | 
					 | 
				
			||||||
    pub fn as_tokens(&mut self, ident: &Ident) -> TokenStream2 {
 | 
					 | 
				
			||||||
        let arg_path = quote!(crate::framework::Arg);
 | 
					 | 
				
			||||||
        let subcommand_group_path = ApplicationCommandOptionType::SubCommandGroup;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        let arg_idents = self
 | 
					 | 
				
			||||||
            .subcommands
 | 
					 | 
				
			||||||
            .iter()
 | 
					 | 
				
			||||||
            .map(|arg| {
 | 
					 | 
				
			||||||
                ident
 | 
					 | 
				
			||||||
                    .with_suffix(self.name.as_str())
 | 
					 | 
				
			||||||
                    .with_suffix(arg.name.as_str())
 | 
					 | 
				
			||||||
                    .with_suffix(SUBCOMMAND)
 | 
					 | 
				
			||||||
            })
 | 
					 | 
				
			||||||
            .collect::<Vec<Ident>>();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        let mut tokens = self
 | 
					 | 
				
			||||||
            .subcommands
 | 
					 | 
				
			||||||
            .iter_mut()
 | 
					 | 
				
			||||||
            .zip(arg_idents.iter())
 | 
					 | 
				
			||||||
            .map(|(subcommand, ident)| subcommand.as_tokens(ident))
 | 
					 | 
				
			||||||
            .fold(quote! {}, |mut a, b| {
 | 
					 | 
				
			||||||
                a.extend(b);
 | 
					 | 
				
			||||||
                a
 | 
					 | 
				
			||||||
            });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        let SubcommandGroup { name, description, .. } = self;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        tokens.extend(quote! {
 | 
					 | 
				
			||||||
            #[allow(missing_docs)]
 | 
					 | 
				
			||||||
            pub static #ident: #arg_path = #arg_path {
 | 
					 | 
				
			||||||
                name: #name,
 | 
					 | 
				
			||||||
                description: #description,
 | 
					 | 
				
			||||||
                kind: #subcommand_group_path,
 | 
					 | 
				
			||||||
                required: false,
 | 
					 | 
				
			||||||
                options: &[#(&#arg_idents),*],
 | 
					 | 
				
			||||||
            };
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        tokens
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl Default for SubcommandGroup {
 | 
					 | 
				
			||||||
    fn default() -> Self {
 | 
					 | 
				
			||||||
        Self { name: String::new(), description: String::new(), subcommands: vec![] }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl SubcommandGroup {
 | 
					 | 
				
			||||||
    pub(crate) fn new(name: String) -> Self {
 | 
					 | 
				
			||||||
        Self { name, ..Default::default() }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Debug, Default)]
 | 
					 | 
				
			||||||
pub(crate) struct Options {
 | 
					 | 
				
			||||||
    pub aliases: Vec<String>,
 | 
					 | 
				
			||||||
    pub description: String,
 | 
					 | 
				
			||||||
    pub group: String,
 | 
					 | 
				
			||||||
    pub examples: Vec<String>,
 | 
					 | 
				
			||||||
    pub can_blacklist: bool,
 | 
					 | 
				
			||||||
    pub supports_dm: bool,
 | 
					 | 
				
			||||||
    pub cmd_args: Vec<Arg>,
 | 
					 | 
				
			||||||
    pub subcommands: Vec<Subcommand>,
 | 
					 | 
				
			||||||
    pub subcommand_groups: Vec<SubcommandGroup>,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl Options {
 | 
					 | 
				
			||||||
    #[inline]
 | 
					 | 
				
			||||||
    pub fn new() -> Self {
 | 
					 | 
				
			||||||
        Self { group: "None".to_string(), ..Default::default() }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,176 +0,0 @@
 | 
				
			|||||||
use proc_macro::TokenStream;
 | 
					 | 
				
			||||||
use proc_macro2::{Span, 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()));
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
pub fn append_line(desc: &mut String, mut line: String) {
 | 
					 | 
				
			||||||
    if line.starts_with(' ') {
 | 
					 | 
				
			||||||
        line.remove(0);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    match line.rfind("\\$") {
 | 
					 | 
				
			||||||
        Some(i) => {
 | 
					 | 
				
			||||||
            desc.push_str(line[..i].trim_end());
 | 
					 | 
				
			||||||
            desc.push(' ');
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        None => {
 | 
					 | 
				
			||||||
            desc.push_str(&line);
 | 
					 | 
				
			||||||
            desc.push('\n');
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
							
								
								
									
										8
									
								
								conf/Rocket.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										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
									
								
							
							
						
						
									
										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
									
								
							
							
						
						
									
										1
									
								
								cron.d/reminder_health
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					*/10 * * * * reminder /lib/reminder-rs/healthcheck
 | 
				
			||||||
							
								
								
									
										9
									
								
								debian/postinst
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										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
									
								
							
							
						
						
									
										7
									
								
								debian/postrm
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
				
			|||||||
 | 
					#!/bin/bash
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					set -e
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					id -u reminder &>/dev/null || userdel reminder
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#DEBHELPER#
 | 
				
			||||||
							
								
								
									
										13
									
								
								healthcheck
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										13
									
								
								healthcheck
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,13 @@
 | 
				
			|||||||
 | 
					#!/bin/bash
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export $(grep -v '^#' /etc/reminder-rs/config.env | xargs -d '\n')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					REGEX='mysql://([A-Za-z]+)@(.+)/(.+)'
 | 
				
			||||||
 | 
					[[ $DATABASE_URL =~ $REGEX ]]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					VAR=$(mysql -u "${BASH_REMATCH[1]}" -h "${BASH_REMATCH[2]}" -N -D "${BASH_REMATCH[3]}" -e "SELECT COUNT(1) FROM reminders WHERE utc_time < NOW() - INTERVAL 10 MINUTE AND enabled = 1 AND status = 'pending'")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					if [ "$VAR" -gt 0 ]
 | 
				
			||||||
 | 
					then
 | 
				
			||||||
 | 
					  echo "This is to inform that there is a reminder backlog which must be resolved." | mail -s "Backlog: $VAR" "$REPORT_EMAIL"
 | 
				
			||||||
 | 
					fi
 | 
				
			||||||
@@ -1,10 +1,6 @@
 | 
				
			|||||||
CREATE DATABASE IF NOT EXISTS reminders;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
SET FOREIGN_KEY_CHECKS=0;
 | 
					SET FOREIGN_KEY_CHECKS=0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
USE reminders;
 | 
					CREATE TABLE guilds (
 | 
				
			||||||
 | 
					 | 
				
			||||||
CREATE TABLE reminders.guilds (
 | 
					 | 
				
			||||||
    id INT UNSIGNED UNIQUE NOT NULL AUTO_INCREMENT,
 | 
					    id INT UNSIGNED UNIQUE NOT NULL AUTO_INCREMENT,
 | 
				
			||||||
    guild BIGINT UNSIGNED UNIQUE NOT NULL,
 | 
					    guild BIGINT UNSIGNED UNIQUE NOT NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -18,10 +14,10 @@ CREATE TABLE reminders.guilds (
 | 
				
			|||||||
    default_avatar VARCHAR(512) DEFAULT 'https://raw.githubusercontent.com/reminder-bot/logos/master/Remind_Me_Bot_Logo_PPic.jpg' NOT NULL,
 | 
					    default_avatar VARCHAR(512) DEFAULT 'https://raw.githubusercontent.com/reminder-bot/logos/master/Remind_Me_Bot_Logo_PPic.jpg' NOT NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    PRIMARY KEY (id),
 | 
					    PRIMARY KEY (id),
 | 
				
			||||||
    FOREIGN KEY (default_channel_id) REFERENCES reminders.channels(id) ON DELETE SET NULL
 | 
					    FOREIGN KEY (default_channel_id) REFERENCES channels(id) ON DELETE SET NULL
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
CREATE TABLE reminders.channels (
 | 
					CREATE TABLE channels (
 | 
				
			||||||
    id INT UNSIGNED UNIQUE NOT NULL AUTO_INCREMENT,
 | 
					    id INT UNSIGNED UNIQUE NOT NULL AUTO_INCREMENT,
 | 
				
			||||||
    channel BIGINT UNSIGNED UNIQUE NOT NULL,
 | 
					    channel BIGINT UNSIGNED UNIQUE NOT NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -39,10 +35,10 @@ CREATE TABLE reminders.channels (
 | 
				
			|||||||
    guild_id INT UNSIGNED,
 | 
					    guild_id INT UNSIGNED,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    PRIMARY KEY (id),
 | 
					    PRIMARY KEY (id),
 | 
				
			||||||
    FOREIGN KEY (guild_id) REFERENCES reminders.guilds(id) ON DELETE CASCADE
 | 
					    FOREIGN KEY (guild_id) REFERENCES guilds(id) ON DELETE CASCADE
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
CREATE TABLE reminders.users (
 | 
					CREATE TABLE users (
 | 
				
			||||||
    id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL,
 | 
					    id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL,
 | 
				
			||||||
    user BIGINT UNSIGNED UNIQUE NOT NULL,
 | 
					    user BIGINT UNSIGNED UNIQUE NOT NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -59,10 +55,10 @@ CREATE TABLE reminders.users (
 | 
				
			|||||||
    patreon BOOLEAN NOT NULL DEFAULT 0,
 | 
					    patreon BOOLEAN NOT NULL DEFAULT 0,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    PRIMARY KEY (id),
 | 
					    PRIMARY KEY (id),
 | 
				
			||||||
    FOREIGN KEY (dm_channel) REFERENCES reminders.channels(id) ON DELETE RESTRICT
 | 
					    FOREIGN KEY (dm_channel) REFERENCES channels(id) ON DELETE RESTRICT
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
CREATE TABLE reminders.roles (
 | 
					CREATE TABLE roles (
 | 
				
			||||||
    id INT UNSIGNED UNIQUE NOT NULL AUTO_INCREMENT,
 | 
					    id INT UNSIGNED UNIQUE NOT NULL AUTO_INCREMENT,
 | 
				
			||||||
    role BIGINT UNSIGNED UNIQUE NOT NULL,
 | 
					    role BIGINT UNSIGNED UNIQUE NOT NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -71,10 +67,10 @@ CREATE TABLE reminders.roles (
 | 
				
			|||||||
    guild_id INT UNSIGNED NOT NULL,
 | 
					    guild_id INT UNSIGNED NOT NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    PRIMARY KEY (id),
 | 
					    PRIMARY KEY (id),
 | 
				
			||||||
    FOREIGN KEY (guild_id) REFERENCES reminders.guilds(id) ON DELETE CASCADE
 | 
					    FOREIGN KEY (guild_id) REFERENCES guilds(id) ON DELETE CASCADE
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
CREATE TABLE reminders.embeds (
 | 
					CREATE TABLE embeds (
 | 
				
			||||||
    id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL,
 | 
					    id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    title VARCHAR(256) NOT NULL DEFAULT '',
 | 
					    title VARCHAR(256) NOT NULL DEFAULT '',
 | 
				
			||||||
@@ -91,7 +87,7 @@ CREATE TABLE reminders.embeds (
 | 
				
			|||||||
    PRIMARY KEY (id)
 | 
					    PRIMARY KEY (id)
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
CREATE TABLE reminders.embed_fields (
 | 
					CREATE TABLE embed_fields (
 | 
				
			||||||
    id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL,
 | 
					    id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    title VARCHAR(256) NOT NULL DEFAULT '',
 | 
					    title VARCHAR(256) NOT NULL DEFAULT '',
 | 
				
			||||||
@@ -100,10 +96,10 @@ CREATE TABLE reminders.embed_fields (
 | 
				
			|||||||
    embed_id INT UNSIGNED NOT NULL,
 | 
					    embed_id INT UNSIGNED NOT NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    PRIMARY KEY (id),
 | 
					    PRIMARY KEY (id),
 | 
				
			||||||
    FOREIGN KEY (embed_id) REFERENCES reminders.embeds(id) ON DELETE CASCADE
 | 
					    FOREIGN KEY (embed_id) REFERENCES embeds(id) ON DELETE CASCADE
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
CREATE TABLE reminders.messages (
 | 
					CREATE TABLE messages (
 | 
				
			||||||
    id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL,
 | 
					    id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    content VARCHAR(2048) NOT NULL DEFAULT '',
 | 
					    content VARCHAR(2048) NOT NULL DEFAULT '',
 | 
				
			||||||
@@ -114,10 +110,10 @@ CREATE TABLE reminders.messages (
 | 
				
			|||||||
    attachment_name VARCHAR(260),
 | 
					    attachment_name VARCHAR(260),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    PRIMARY KEY (id),
 | 
					    PRIMARY KEY (id),
 | 
				
			||||||
    FOREIGN KEY (embed_id) REFERENCES reminders.embeds(id) ON DELETE SET NULL
 | 
					    FOREIGN KEY (embed_id) REFERENCES embeds(id) ON DELETE SET NULL
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
CREATE TABLE reminders.reminders (
 | 
					CREATE TABLE reminders (
 | 
				
			||||||
    id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL,
 | 
					    id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL,
 | 
				
			||||||
    uid VARCHAR(64) UNIQUE NOT NULL,
 | 
					    uid VARCHAR(64) UNIQUE NOT NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -140,20 +136,20 @@ CREATE TABLE reminders.reminders (
 | 
				
			|||||||
    set_by INT UNSIGNED,
 | 
					    set_by INT UNSIGNED,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    PRIMARY KEY (id),
 | 
					    PRIMARY KEY (id),
 | 
				
			||||||
    FOREIGN KEY (message_id) REFERENCES reminders.messages(id) ON DELETE RESTRICT,
 | 
					    FOREIGN KEY (message_id) REFERENCES messages(id) ON DELETE RESTRICT,
 | 
				
			||||||
    FOREIGN KEY (channel_id) REFERENCES reminders.channels(id) ON DELETE CASCADE,
 | 
					    FOREIGN KEY (channel_id) REFERENCES channels(id) ON DELETE CASCADE,
 | 
				
			||||||
    FOREIGN KEY (set_by) REFERENCES reminders.users(id) ON DELETE SET NULL
 | 
					    FOREIGN KEY (set_by) REFERENCES users(id) ON DELETE SET NULL
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
CREATE TRIGGER message_cleanup AFTER DELETE ON reminders.reminders
 | 
					CREATE TRIGGER message_cleanup AFTER DELETE ON reminders
 | 
				
			||||||
FOR EACH ROW
 | 
					FOR EACH ROW
 | 
				
			||||||
    DELETE FROM reminders.messages WHERE id = OLD.message_id;
 | 
					    DELETE FROM messages WHERE id = OLD.message_id;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
CREATE TRIGGER embed_cleanup AFTER DELETE ON reminders.messages
 | 
					CREATE TRIGGER embed_cleanup AFTER DELETE ON messages
 | 
				
			||||||
FOR EACH ROW
 | 
					FOR EACH ROW
 | 
				
			||||||
    DELETE FROM reminders.embeds WHERE id = OLD.embed_id;
 | 
					    DELETE FROM embeds WHERE id = OLD.embed_id;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
CREATE TABLE reminders.todos (
 | 
					CREATE TABLE todos (
 | 
				
			||||||
    id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL,
 | 
					    id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL,
 | 
				
			||||||
    user_id INT UNSIGNED,
 | 
					    user_id INT UNSIGNED,
 | 
				
			||||||
    guild_id INT UNSIGNED,
 | 
					    guild_id INT UNSIGNED,
 | 
				
			||||||
@@ -161,23 +157,23 @@ CREATE TABLE reminders.todos (
 | 
				
			|||||||
    value VARCHAR(2000) NOT NULL,
 | 
					    value VARCHAR(2000) NOT NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    PRIMARY KEY (id),
 | 
					    PRIMARY KEY (id),
 | 
				
			||||||
    FOREIGN KEY (user_id) REFERENCES reminders.users(id) ON DELETE SET NULL,
 | 
					    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL,
 | 
				
			||||||
    FOREIGN KEY (guild_id) REFERENCES reminders.guilds(id) ON DELETE CASCADE,
 | 
					    FOREIGN KEY (guild_id) REFERENCES guilds(id) ON DELETE CASCADE,
 | 
				
			||||||
    FOREIGN KEY (channel_id) REFERENCES reminders.channels(id) ON DELETE SET NULL
 | 
					    FOREIGN KEY (channel_id) REFERENCES channels(id) ON DELETE SET NULL
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
CREATE TABLE reminders.command_restrictions (
 | 
					CREATE TABLE command_restrictions (
 | 
				
			||||||
    id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL,
 | 
					    id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    role_id INT UNSIGNED NOT NULL,
 | 
					    role_id INT UNSIGNED NOT NULL,
 | 
				
			||||||
    command ENUM('todos', 'natural', 'remind', 'interval', 'timer', 'del', 'look', 'alias', 'countdown') NOT NULL,
 | 
					    command ENUM('todos', 'natural', 'remind', 'interval', 'timer', 'del', 'look', 'alias', 'countdown') NOT NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    PRIMARY KEY (id),
 | 
					    PRIMARY KEY (id),
 | 
				
			||||||
    FOREIGN KEY (role_id) REFERENCES reminders.roles(id) ON DELETE CASCADE,
 | 
					    FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE,
 | 
				
			||||||
    UNIQUE KEY (`role_id`, `command`)
 | 
					    UNIQUE KEY (`role_id`, `command`)
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
CREATE TABLE reminders.timers (
 | 
					CREATE TABLE timers (
 | 
				
			||||||
    id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL,
 | 
					    id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL,
 | 
				
			||||||
    start_time TIMESTAMP NOT NULL DEFAULT NOW(),
 | 
					    start_time TIMESTAMP NOT NULL DEFAULT NOW(),
 | 
				
			||||||
    name VARCHAR(32) NOT NULL,
 | 
					    name VARCHAR(32) NOT NULL,
 | 
				
			||||||
@@ -186,7 +182,7 @@ CREATE TABLE reminders.timers (
 | 
				
			|||||||
    PRIMARY KEY (id)
 | 
					    PRIMARY KEY (id)
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
CREATE TABLE reminders.events (
 | 
					CREATE TABLE events (
 | 
				
			||||||
    id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL,
 | 
					    id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL,
 | 
				
			||||||
    `time` TIMESTAMP NOT NULL DEFAULT NOW(),
 | 
					    `time` TIMESTAMP NOT NULL DEFAULT NOW(),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -198,12 +194,12 @@ CREATE TABLE reminders.events (
 | 
				
			|||||||
    reminder_id INT UNSIGNED,
 | 
					    reminder_id INT UNSIGNED,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    PRIMARY KEY (id),
 | 
					    PRIMARY KEY (id),
 | 
				
			||||||
    FOREIGN KEY (guild_id) REFERENCES reminders.guilds(id) ON DELETE CASCADE,
 | 
					    FOREIGN KEY (guild_id) REFERENCES guilds(id) ON DELETE CASCADE,
 | 
				
			||||||
    FOREIGN KEY (user_id) REFERENCES reminders.users(id) ON DELETE SET NULL,
 | 
					    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL,
 | 
				
			||||||
    FOREIGN KEY (reminder_id) REFERENCES reminders.reminders(id) ON DELETE SET NULL
 | 
					    FOREIGN KEY (reminder_id) REFERENCES reminders(id) ON DELETE SET NULL
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
CREATE TABLE reminders.command_aliases (
 | 
					CREATE TABLE command_aliases (
 | 
				
			||||||
    id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL,
 | 
					    id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    guild_id INT UNSIGNED NOT NULL,
 | 
					    guild_id INT UNSIGNED NOT NULL,
 | 
				
			||||||
@@ -212,22 +208,22 @@ CREATE TABLE reminders.command_aliases (
 | 
				
			|||||||
    command VARCHAR(2048) NOT NULL,
 | 
					    command VARCHAR(2048) NOT NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    PRIMARY KEY (id),
 | 
					    PRIMARY KEY (id),
 | 
				
			||||||
    FOREIGN KEY (guild_id) REFERENCES reminders.guilds(id) ON DELETE CASCADE,
 | 
					    FOREIGN KEY (guild_id) REFERENCES guilds(id) ON DELETE CASCADE,
 | 
				
			||||||
    UNIQUE KEY (`guild_id`, `name`)
 | 
					    UNIQUE KEY (`guild_id`, `name`)
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
CREATE TABLE reminders.guild_users (
 | 
					CREATE TABLE guild_users (
 | 
				
			||||||
    guild INT UNSIGNED NOT NULL,
 | 
					    guild INT UNSIGNED NOT NULL,
 | 
				
			||||||
    user INT UNSIGNED NOT NULL,
 | 
					    user INT UNSIGNED NOT NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    can_access BOOL NOT NULL DEFAULT 0,
 | 
					    can_access BOOL NOT NULL DEFAULT 0,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    FOREIGN KEY (guild) REFERENCES reminders.guilds(id) ON DELETE CASCADE,
 | 
					    FOREIGN KEY (guild) REFERENCES guilds(id) ON DELETE CASCADE,
 | 
				
			||||||
    FOREIGN KEY (user) REFERENCES reminders.users(id) ON DELETE CASCADE,
 | 
					    FOREIGN KEY (user) REFERENCES users(id) ON DELETE CASCADE,
 | 
				
			||||||
    UNIQUE KEY (guild, user)
 | 
					    UNIQUE KEY (guild, user)
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
CREATE EVENT reminders.event_cleanup
 | 
					CREATE EVENT event_cleanup
 | 
				
			||||||
ON SCHEDULE AT CURRENT_TIMESTAMP + INTERVAL 1 DAY
 | 
					ON SCHEDULE AT CURRENT_TIMESTAMP + INTERVAL 1 DAY
 | 
				
			||||||
ON COMPLETION PRESERVE
 | 
					ON COMPLETION PRESERVE
 | 
				
			||||||
DO DELETE FROM reminders.events WHERE `time` < DATE_SUB(NOW(), INTERVAL 5 DAY);
 | 
					DO DELETE FROM events WHERE `time` < DATE_SUB(NOW(), INTERVAL 5 DAY);
 | 
				
			||||||
@@ -1,5 +1,3 @@
 | 
				
			|||||||
USE reminders;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
SET FOREIGN_KEY_CHECKS = 0;
 | 
					SET FOREIGN_KEY_CHECKS = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
DROP TABLE IF EXISTS reminders_new;
 | 
					DROP TABLE IF EXISTS reminders_new;
 | 
				
			||||||
@@ -157,4 +155,9 @@ CREATE TABLE events (
 | 
				
			|||||||
    FOREIGN KEY (reminder_id) REFERENCES reminders_new(id) ON DELETE SET NULL
 | 
					    FOREIGN KEY (reminder_id) REFERENCES reminders_new(id) ON DELETE SET NULL
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					DROP TABLE reminders;
 | 
				
			||||||
 | 
					DROP TABLE embed_fields;
 | 
				
			||||||
 | 
					RENAME TABLE reminders_new TO reminders;
 | 
				
			||||||
 | 
					RENAME TABLE embed_fields_new TO embed_fields;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
SET FOREIGN_KEY_CHECKS = 1;
 | 
					SET FOREIGN_KEY_CHECKS = 1;
 | 
				
			||||||
@@ -1,5 +1,3 @@
 | 
				
			|||||||
USE reminders;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
CREATE TABLE macro (
 | 
					CREATE TABLE macro (
 | 
				
			||||||
    id INT UNSIGNED AUTO_INCREMENT,
 | 
					    id INT UNSIGNED AUTO_INCREMENT,
 | 
				
			||||||
    guild_id INT UNSIGNED NOT NULL,
 | 
					    guild_id INT UNSIGNED NOT NULL,
 | 
				
			||||||
@@ -0,0 +1,2 @@
 | 
				
			|||||||
 | 
					ALTER TABLE reminders RENAME COLUMN `interval` TO `interval_seconds`;
 | 
				
			||||||
 | 
					ALTER TABLE reminders ADD COLUMN `interval_months` INT UNSIGNED DEFAULT NULL;
 | 
				
			||||||
							
								
								
									
										49
									
								
								migrations/20220211000000_reminder_templates.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										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
									
								
							
							
						
						
									
										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
									
								
							
							
						
						
									
										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
									
								
							
							
						
						
									
										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
									
								
							
							
						
						
									
										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
									
								
							
							
						
						
									
										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
									
								
							
							
						
						
									
										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;
 | 
				
			||||||
							
								
								
									
										41
									
								
								nginx/reminder-rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								nginx/reminder-rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,41 @@
 | 
				
			|||||||
 | 
					server {
 | 
				
			||||||
 | 
					        server_name www.reminder-bot.com;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return 301 $scheme://reminder-bot.com$request_uri;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					server {
 | 
				
			||||||
 | 
					        listen 80;
 | 
				
			||||||
 | 
					        server_name reminder-bot.com;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						    return 301 https://reminder-bot.com$request_uri;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					server {
 | 
				
			||||||
 | 
					        listen 443 ssl;
 | 
				
			||||||
 | 
					        server_name reminder-bot.com;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        ssl_certificate /etc/letsencrypt/live/reminder-bot.com/fullchain.pem;
 | 
				
			||||||
 | 
					        ssl_certificate_key /etc/letsencrypt/live/reminder-bot.com/privkey.pem;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        access_log /var/log/nginx/access.log;
 | 
				
			||||||
 | 
					        error_log /var/log/nginx/error.log;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        proxy_buffer_size 128k;
 | 
				
			||||||
 | 
					        proxy_buffers 4 256k;
 | 
				
			||||||
 | 
					        proxy_busy_buffers_size 256k;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        location / {
 | 
				
			||||||
 | 
					                proxy_pass http://localhost:18920;
 | 
				
			||||||
 | 
					                proxy_redirect off;
 | 
				
			||||||
 | 
					                proxy_set_header Host $host;
 | 
				
			||||||
 | 
					                proxy_set_header X-Real-IP $remote_addr;
 | 
				
			||||||
 | 
					                proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
 | 
				
			||||||
 | 
							        proxy_set_header X-Forwarded-Proto $scheme;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        location /static {
 | 
				
			||||||
 | 
					                alias /var/www/reminder-rs/static;
 | 
				
			||||||
 | 
					                expires 30d;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										16
									
								
								postman/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								postman/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,16 @@
 | 
				
			|||||||
 | 
					[package]
 | 
				
			||||||
 | 
					name = "postman"
 | 
				
			||||||
 | 
					version = "0.1.0"
 | 
				
			||||||
 | 
					edition = "2021"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[dependencies]
 | 
				
			||||||
 | 
					tokio = { version = "1", features = ["process", "full"] }
 | 
				
			||||||
 | 
					regex = "1.9"
 | 
				
			||||||
 | 
					log = "0.4"
 | 
				
			||||||
 | 
					chrono = "0.4"
 | 
				
			||||||
 | 
					chrono-tz = { version = "0.8", features = ["serde"] }
 | 
				
			||||||
 | 
					lazy_static = "1.4"
 | 
				
			||||||
 | 
					num-integer = "0.1"
 | 
				
			||||||
 | 
					serde = "1.0"
 | 
				
			||||||
 | 
					sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "macros", "mysql", "bigdecimal", "chrono", "json"]}
 | 
				
			||||||
 | 
					serenity = { version = "0.11", default-features = false, features = ["builder", "cache", "client", "gateway", "http", "model", "utils", "rustls_backend"] }
 | 
				
			||||||
							
								
								
									
										50
									
								
								postman/src/lib.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								postman/src/lib.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,50 @@
 | 
				
			|||||||
 | 
					mod sender;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use std::env;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use log::{info, warn};
 | 
				
			||||||
 | 
					use serenity::client::Context;
 | 
				
			||||||
 | 
					use sqlx::{Executor, MySql};
 | 
				
			||||||
 | 
					use tokio::{
 | 
				
			||||||
 | 
					    sync::broadcast::Receiver,
 | 
				
			||||||
 | 
					    time::{sleep_until, Duration, Instant},
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type Database = MySql;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub async fn initialize(
 | 
				
			||||||
 | 
					    mut kill: Receiver<()>,
 | 
				
			||||||
 | 
					    ctx: Context,
 | 
				
			||||||
 | 
					    pool: impl Executor<'_, Database = Database> + Copy,
 | 
				
			||||||
 | 
					) -> Result<(), &'static str> {
 | 
				
			||||||
 | 
					    tokio::select! {
 | 
				
			||||||
 | 
					        output = _initialize(ctx, pool) => Ok(output),
 | 
				
			||||||
 | 
					        _ = kill.recv() => {
 | 
				
			||||||
 | 
					            warn!("Received terminate signal. Goodbye");
 | 
				
			||||||
 | 
					            Err("Received terminate signal. Goodbye")
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async fn _initialize(ctx: Context, pool: impl Executor<'_, Database = Database> + Copy) {
 | 
				
			||||||
 | 
					    let remind_interval = env::var("REMIND_INTERVAL")
 | 
				
			||||||
 | 
					        .map(|inner| inner.parse::<u64>().ok())
 | 
				
			||||||
 | 
					        .ok()
 | 
				
			||||||
 | 
					        .flatten()
 | 
				
			||||||
 | 
					        .unwrap_or(10);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    loop {
 | 
				
			||||||
 | 
					        let sleep_to = Instant::now() + Duration::from_secs(remind_interval);
 | 
				
			||||||
 | 
					        let reminders = sender::Reminder::fetch_reminders(pool).await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if reminders.len() > 0 {
 | 
				
			||||||
 | 
					            info!("Preparing to send {} reminders.", reminders.len());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            for reminder in reminders {
 | 
				
			||||||
 | 
					                reminder.send(pool, ctx.clone()).await;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        sleep_until(sleep_to).await;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										745
									
								
								postman/src/sender.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										745
									
								
								postman/src/sender.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,745 @@
 | 
				
			|||||||
 | 
					use std::env;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use chrono::{DateTime, Days, Duration, Months};
 | 
				
			||||||
 | 
					use chrono_tz::Tz;
 | 
				
			||||||
 | 
					use lazy_static::lazy_static;
 | 
				
			||||||
 | 
					use log::{error, info, warn};
 | 
				
			||||||
 | 
					use num_integer::Integer;
 | 
				
			||||||
 | 
					use regex::{Captures, Regex};
 | 
				
			||||||
 | 
					use serde::Deserialize;
 | 
				
			||||||
 | 
					use serenity::{
 | 
				
			||||||
 | 
					    builder::CreateEmbed,
 | 
				
			||||||
 | 
					    http::{CacheHttp, Http, HttpError},
 | 
				
			||||||
 | 
					    model::{
 | 
				
			||||||
 | 
					        channel::{Channel, Embed as SerenityEmbed},
 | 
				
			||||||
 | 
					        id::ChannelId,
 | 
				
			||||||
 | 
					        webhook::Webhook,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    Error, Result,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					use sqlx::{
 | 
				
			||||||
 | 
					    types::{
 | 
				
			||||||
 | 
					        chrono::{NaiveDateTime, Utc},
 | 
				
			||||||
 | 
					        Json,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    Executor,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::Database;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					lazy_static! {
 | 
				
			||||||
 | 
					    pub static ref TIMEFROM_REGEX: Regex =
 | 
				
			||||||
 | 
					        Regex::new(r#"<<timefrom:(?P<time>\d+):(?P<format>.+)?>>"#).unwrap();
 | 
				
			||||||
 | 
					    pub static ref TIMENOW_REGEX: Regex =
 | 
				
			||||||
 | 
					        Regex::new(r#"<<timenow:(?P<timezone>(?:\w|/|_)+):(?P<format>.+)?>>"#).unwrap();
 | 
				
			||||||
 | 
					    pub static ref LOG_TO_DATABASE: bool = env::var("LOG_TO_DATABASE").map_or(true, |v| v == "1");
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fn fmt_displacement(format: &str, seconds: u64) -> String {
 | 
				
			||||||
 | 
					    let mut seconds = seconds;
 | 
				
			||||||
 | 
					    let mut days: u64 = 0;
 | 
				
			||||||
 | 
					    let mut hours: u64 = 0;
 | 
				
			||||||
 | 
					    let mut minutes: u64 = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for (rep, time_type, div) in
 | 
				
			||||||
 | 
					        [("%d", &mut days, 86400), ("%h", &mut hours, 3600), ("%m", &mut minutes, 60)].iter_mut()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        if format.contains(*rep) {
 | 
				
			||||||
 | 
					            let (divided, new_seconds) = seconds.div_rem(&div);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            **time_type = divided;
 | 
				
			||||||
 | 
					            seconds = new_seconds;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    format
 | 
				
			||||||
 | 
					        .replace("%s", &seconds.to_string())
 | 
				
			||||||
 | 
					        .replace("%m", &minutes.to_string())
 | 
				
			||||||
 | 
					        .replace("%h", &hours.to_string())
 | 
				
			||||||
 | 
					        .replace("%d", &days.to_string())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub fn substitute(string: &str) -> String {
 | 
				
			||||||
 | 
					    let new = TIMEFROM_REGEX.replace(string, |caps: &Captures| {
 | 
				
			||||||
 | 
					        let final_time = caps.name("time").map(|m| m.as_str().parse::<i64>().ok()).flatten();
 | 
				
			||||||
 | 
					        let format = caps.name("format").map(|m| m.as_str());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if let (Some(final_time), Some(format)) = (final_time, format) {
 | 
				
			||||||
 | 
					            match NaiveDateTime::from_timestamp_opt(final_time, 0) {
 | 
				
			||||||
 | 
					                Some(dt) => {
 | 
				
			||||||
 | 
					                    let now = Utc::now().naive_utc();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    let difference = {
 | 
				
			||||||
 | 
					                        if now < dt {
 | 
				
			||||||
 | 
					                            dt - Utc::now().naive_utc()
 | 
				
			||||||
 | 
					                        } else {
 | 
				
			||||||
 | 
					                            Utc::now().naive_utc() - dt
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    fmt_displacement(format, difference.num_seconds() as u64)
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                None => String::new(),
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            String::new()
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    TIMENOW_REGEX
 | 
				
			||||||
 | 
					        .replace(&new, |caps: &Captures| {
 | 
				
			||||||
 | 
					            let timezone = caps.name("timezone").map(|m| m.as_str().parse::<Tz>().ok()).flatten();
 | 
				
			||||||
 | 
					            let format = caps.name("format").map(|m| m.as_str());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if let (Some(timezone), Some(format)) = (timezone, format) {
 | 
				
			||||||
 | 
					                let now = Utc::now().with_timezone(&timezone);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                now.format(format).to_string()
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                String::new()
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        .to_string()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					struct Embed {
 | 
				
			||||||
 | 
					    title: String,
 | 
				
			||||||
 | 
					    description: String,
 | 
				
			||||||
 | 
					    image_url: Option<String>,
 | 
				
			||||||
 | 
					    thumbnail_url: Option<String>,
 | 
				
			||||||
 | 
					    footer: String,
 | 
				
			||||||
 | 
					    footer_url: Option<String>,
 | 
				
			||||||
 | 
					    author: String,
 | 
				
			||||||
 | 
					    author_url: Option<String>,
 | 
				
			||||||
 | 
					    color: u32,
 | 
				
			||||||
 | 
					    fields: Json<Vec<EmbedField>>,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Deserialize)]
 | 
				
			||||||
 | 
					struct EmbedField {
 | 
				
			||||||
 | 
					    title: String,
 | 
				
			||||||
 | 
					    value: String,
 | 
				
			||||||
 | 
					    inline: bool,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl Embed {
 | 
				
			||||||
 | 
					    pub async fn from_id(
 | 
				
			||||||
 | 
					        pool: impl Executor<'_, Database = Database> + Copy,
 | 
				
			||||||
 | 
					        id: u32,
 | 
				
			||||||
 | 
					    ) -> Option<Self> {
 | 
				
			||||||
 | 
					        match sqlx::query_as!(
 | 
				
			||||||
 | 
					            Self,
 | 
				
			||||||
 | 
					            r#"
 | 
				
			||||||
 | 
					            SELECT
 | 
				
			||||||
 | 
					             `embed_title` AS title,
 | 
				
			||||||
 | 
					             `embed_description` AS description,
 | 
				
			||||||
 | 
					             `embed_image_url` AS image_url,
 | 
				
			||||||
 | 
					             `embed_thumbnail_url` AS thumbnail_url,
 | 
				
			||||||
 | 
					             `embed_footer` AS footer,
 | 
				
			||||||
 | 
					             `embed_footer_url` AS footer_url,
 | 
				
			||||||
 | 
					             `embed_author` AS author,
 | 
				
			||||||
 | 
					             `embed_author_url` AS author_url,
 | 
				
			||||||
 | 
					             `embed_color` AS color,
 | 
				
			||||||
 | 
					             IFNULL(`embed_fields`, '[]') AS "fields:_"
 | 
				
			||||||
 | 
					            FROM reminders
 | 
				
			||||||
 | 
					            WHERE `id` = ?"#,
 | 
				
			||||||
 | 
					            id
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        .fetch_one(pool)
 | 
				
			||||||
 | 
					        .await
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            Ok(mut embed) => {
 | 
				
			||||||
 | 
					                embed.title = substitute(&embed.title);
 | 
				
			||||||
 | 
					                embed.description = substitute(&embed.description);
 | 
				
			||||||
 | 
					                embed.footer = substitute(&embed.footer);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                embed.fields.iter_mut().for_each(|field| {
 | 
				
			||||||
 | 
					                    field.title = substitute(&field.title);
 | 
				
			||||||
 | 
					                    field.value = substitute(&field.value);
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if embed.has_content() {
 | 
				
			||||||
 | 
					                    Some(embed)
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    None
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            Err(e) => {
 | 
				
			||||||
 | 
					                warn!("Error loading embed from reminder: {:?}", e);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                None
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn has_content(&self) -> bool {
 | 
				
			||||||
 | 
					        if self.title.is_empty()
 | 
				
			||||||
 | 
					            && self.description.is_empty()
 | 
				
			||||||
 | 
					            && self.image_url.is_none()
 | 
				
			||||||
 | 
					            && self.thumbnail_url.is_none()
 | 
				
			||||||
 | 
					            && self.footer.is_empty()
 | 
				
			||||||
 | 
					            && self.footer_url.is_none()
 | 
				
			||||||
 | 
					            && self.author.is_empty()
 | 
				
			||||||
 | 
					            && self.author_url.is_none()
 | 
				
			||||||
 | 
					            && self.fields.0.is_empty()
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            false
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            true
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl Into<CreateEmbed> for Embed {
 | 
				
			||||||
 | 
					    fn into(self) -> CreateEmbed {
 | 
				
			||||||
 | 
					        let mut c = CreateEmbed::default();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        c.title(&self.title)
 | 
				
			||||||
 | 
					            .description(&self.description)
 | 
				
			||||||
 | 
					            .color(self.color)
 | 
				
			||||||
 | 
					            .author(|a| {
 | 
				
			||||||
 | 
					                a.name(&self.author);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if let Some(author_icon) = &self.author_url {
 | 
				
			||||||
 | 
					                    a.icon_url(author_icon);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                a
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					            .footer(|f| {
 | 
				
			||||||
 | 
					                f.text(&self.footer);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if let Some(footer_icon) = &self.footer_url {
 | 
				
			||||||
 | 
					                    f.icon_url(footer_icon);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                f
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        for field in &self.fields.0 {
 | 
				
			||||||
 | 
					            c.field(&field.title, &field.value, field.inline);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if let Some(image_url) = &self.image_url {
 | 
				
			||||||
 | 
					            c.image(image_url);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if let Some(thumbnail_url) = &self.thumbnail_url {
 | 
				
			||||||
 | 
					            c.thumbnail(thumbnail_url);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        c
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub struct Reminder {
 | 
				
			||||||
 | 
					    id: u32,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    channel_id: u64,
 | 
				
			||||||
 | 
					    webhook_id: Option<u64>,
 | 
				
			||||||
 | 
					    webhook_token: Option<String>,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    channel_paused: bool,
 | 
				
			||||||
 | 
					    channel_paused_until: Option<NaiveDateTime>,
 | 
				
			||||||
 | 
					    enabled: bool,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    tts: bool,
 | 
				
			||||||
 | 
					    pin: bool,
 | 
				
			||||||
 | 
					    content: String,
 | 
				
			||||||
 | 
					    attachment: Option<Vec<u8>>,
 | 
				
			||||||
 | 
					    attachment_name: Option<String>,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    utc_time: DateTime<Utc>,
 | 
				
			||||||
 | 
					    timezone: String,
 | 
				
			||||||
 | 
					    restartable: bool,
 | 
				
			||||||
 | 
					    expires: Option<DateTime<Utc>>,
 | 
				
			||||||
 | 
					    interval_seconds: Option<u32>,
 | 
				
			||||||
 | 
					    interval_days: Option<u32>,
 | 
				
			||||||
 | 
					    interval_months: Option<u32>,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    avatar: Option<String>,
 | 
				
			||||||
 | 
					    username: Option<String>,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl Reminder {
 | 
				
			||||||
 | 
					    pub async fn fetch_reminders(pool: impl Executor<'_, Database = Database> + Copy) -> Vec<Self> {
 | 
				
			||||||
 | 
					        match sqlx::query_as_unchecked!(
 | 
				
			||||||
 | 
					            Reminder,
 | 
				
			||||||
 | 
					            r#"
 | 
				
			||||||
 | 
					SELECT
 | 
				
			||||||
 | 
					    reminders.`id` AS id,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    channels.`channel` AS channel_id,
 | 
				
			||||||
 | 
					    channels.`webhook_id` AS webhook_id,
 | 
				
			||||||
 | 
					    channels.`webhook_token` AS webhook_token,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    channels.`paused` AS 'channel_paused',
 | 
				
			||||||
 | 
					    channels.`paused_until` AS 'channel_paused_until',
 | 
				
			||||||
 | 
					    reminders.`enabled` AS 'enabled',
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    reminders.`tts` AS tts,
 | 
				
			||||||
 | 
					    reminders.`pin` AS pin,
 | 
				
			||||||
 | 
					    reminders.`content` AS content,
 | 
				
			||||||
 | 
					    reminders.`attachment` AS attachment,
 | 
				
			||||||
 | 
					    reminders.`attachment_name` AS attachment_name,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    reminders.`utc_time` AS 'utc_time',
 | 
				
			||||||
 | 
					    reminders.`timezone` AS timezone,
 | 
				
			||||||
 | 
					    reminders.`restartable` AS restartable,
 | 
				
			||||||
 | 
					    reminders.`expires` AS 'expires',
 | 
				
			||||||
 | 
					    reminders.`interval_seconds` AS 'interval_seconds',
 | 
				
			||||||
 | 
					    reminders.`interval_days` AS 'interval_days',
 | 
				
			||||||
 | 
					    reminders.`interval_months` AS 'interval_months',
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    reminders.`avatar` AS avatar,
 | 
				
			||||||
 | 
					    reminders.`username` AS username
 | 
				
			||||||
 | 
					FROM
 | 
				
			||||||
 | 
					    reminders
 | 
				
			||||||
 | 
					INNER JOIN
 | 
				
			||||||
 | 
					    channels
 | 
				
			||||||
 | 
					ON
 | 
				
			||||||
 | 
					    reminders.channel_id = channels.id
 | 
				
			||||||
 | 
					WHERE
 | 
				
			||||||
 | 
					    reminders.`status` = 'pending' AND
 | 
				
			||||||
 | 
					    reminders.`id` IN (
 | 
				
			||||||
 | 
					        SELECT
 | 
				
			||||||
 | 
					            MIN(id)
 | 
				
			||||||
 | 
					        FROM
 | 
				
			||||||
 | 
					            reminders
 | 
				
			||||||
 | 
					        WHERE
 | 
				
			||||||
 | 
					            reminders.`utc_time` <= NOW() AND
 | 
				
			||||||
 | 
					            `status` = 'pending' AND
 | 
				
			||||||
 | 
					            (
 | 
				
			||||||
 | 
					                reminders.`interval_seconds` IS NOT NULL
 | 
				
			||||||
 | 
					                OR reminders.`interval_months` IS NOT NULL
 | 
				
			||||||
 | 
					                OR reminders.`interval_days` IS NOT NULL
 | 
				
			||||||
 | 
					                OR reminders.enabled
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        GROUP BY channel_id
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    "#,
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        .fetch_all(pool)
 | 
				
			||||||
 | 
					        .await
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            Ok(reminders) => reminders
 | 
				
			||||||
 | 
					                .into_iter()
 | 
				
			||||||
 | 
					                .map(|mut rem| {
 | 
				
			||||||
 | 
					                    rem.content = substitute(&rem.content);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    rem
 | 
				
			||||||
 | 
					                })
 | 
				
			||||||
 | 
					                .collect::<Vec<Self>>(),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            Err(e) => {
 | 
				
			||||||
 | 
					                warn!("Could not fetch reminders: {:?}", e);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                vec![]
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async fn reset_webhook(&self, pool: impl Executor<'_, Database = Database> + Copy) {
 | 
				
			||||||
 | 
					        let _ = sqlx::query!(
 | 
				
			||||||
 | 
					            "UPDATE channels SET webhook_id = NULL, webhook_token = NULL WHERE channel = ?",
 | 
				
			||||||
 | 
					            self.channel_id
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        .execute(pool)
 | 
				
			||||||
 | 
					        .await;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async fn refresh(&self, pool: impl Executor<'_, Database = Database> + Copy) {
 | 
				
			||||||
 | 
					        if self.interval_seconds.is_some()
 | 
				
			||||||
 | 
					            || self.interval_months.is_some()
 | 
				
			||||||
 | 
					            || self.interval_days.is_some()
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            // If all intervals are zero then dont care
 | 
				
			||||||
 | 
					            if self.interval_seconds == Some(0)
 | 
				
			||||||
 | 
					                && self.interval_days == Some(0)
 | 
				
			||||||
 | 
					                && self.interval_months == Some(0)
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                self.set_sent(pool).await;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            let now = Utc::now();
 | 
				
			||||||
 | 
					            let mut updated_reminder_time =
 | 
				
			||||||
 | 
					                self.utc_time.with_timezone(&self.timezone.parse().unwrap_or(Tz::UTC));
 | 
				
			||||||
 | 
					            let mut fail_count = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            while updated_reminder_time < now && fail_count < 4 {
 | 
				
			||||||
 | 
					                if let Some(interval) = self.interval_months {
 | 
				
			||||||
 | 
					                    if interval != 0 {
 | 
				
			||||||
 | 
					                        updated_reminder_time = updated_reminder_time
 | 
				
			||||||
 | 
					                            .checked_add_months(Months::new(interval))
 | 
				
			||||||
 | 
					                            .unwrap_or_else(|| {
 | 
				
			||||||
 | 
					                                warn!(
 | 
				
			||||||
 | 
					                                    "{}: Could not add {} months to a reminder",
 | 
				
			||||||
 | 
					                                    interval, self.id
 | 
				
			||||||
 | 
					                                );
 | 
				
			||||||
 | 
					                                fail_count += 1;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                                updated_reminder_time
 | 
				
			||||||
 | 
					                            });
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if let Some(interval) = self.interval_days {
 | 
				
			||||||
 | 
					                    if interval != 0 {
 | 
				
			||||||
 | 
					                        updated_reminder_time = updated_reminder_time
 | 
				
			||||||
 | 
					                            .checked_add_days(Days::new(interval as u64))
 | 
				
			||||||
 | 
					                            .unwrap_or_else(|| {
 | 
				
			||||||
 | 
					                                warn!("{}: Could not add {} days to a reminder", self.id, interval);
 | 
				
			||||||
 | 
					                                fail_count += 1;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                                updated_reminder_time
 | 
				
			||||||
 | 
					                            })
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if let Some(interval) = self.interval_seconds {
 | 
				
			||||||
 | 
					                    updated_reminder_time += Duration::seconds(interval as i64);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if fail_count >= 4 {
 | 
				
			||||||
 | 
					                self.log_error(
 | 
				
			||||||
 | 
					                    pool,
 | 
				
			||||||
 | 
					                    "Failed to update 4 times and so is being deleted",
 | 
				
			||||||
 | 
					                    None::<&'static str>,
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					                .await;
 | 
				
			||||||
 | 
					                self.set_failed(pool, "Failed to update 4 times and so is being deleted").await;
 | 
				
			||||||
 | 
					            } else if self.expires.map_or(false, |expires| updated_reminder_time > expires) {
 | 
				
			||||||
 | 
					                self.set_sent(pool).await;
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                sqlx::query!(
 | 
				
			||||||
 | 
					                    "UPDATE reminders SET `utc_time` = ? WHERE `id` = ?",
 | 
				
			||||||
 | 
					                    updated_reminder_time.with_timezone(&Utc),
 | 
				
			||||||
 | 
					                    self.id
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					                .execute(pool)
 | 
				
			||||||
 | 
					                .await
 | 
				
			||||||
 | 
					                .expect(&format!("Could not update time on Reminder {}", self.id));
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            self.set_sent(pool).await;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async fn log_error(
 | 
				
			||||||
 | 
					        &self,
 | 
				
			||||||
 | 
					        pool: impl Executor<'_, Database = Database> + Copy,
 | 
				
			||||||
 | 
					        error: &'static str,
 | 
				
			||||||
 | 
					        debug_info: Option<impl std::fmt::Debug>,
 | 
				
			||||||
 | 
					    ) {
 | 
				
			||||||
 | 
					        let message = match debug_info {
 | 
				
			||||||
 | 
					            Some(info) => format!(
 | 
				
			||||||
 | 
					                "{}
 | 
				
			||||||
 | 
					{:?}",
 | 
				
			||||||
 | 
					                error, info
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            None => error.to_string(),
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        error!("[Reminder {}] {}", self.id, message);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if *LOG_TO_DATABASE {
 | 
				
			||||||
 | 
					            sqlx::query!(
 | 
				
			||||||
 | 
					                "INSERT INTO stat (type, reminder_id, message) VALUES ('reminder_failed', ?, ?)",
 | 
				
			||||||
 | 
					                self.id,
 | 
				
			||||||
 | 
					                message,
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            .execute(pool)
 | 
				
			||||||
 | 
					            .await
 | 
				
			||||||
 | 
					            .expect("Could not log error to database");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async fn log_success(&self, pool: impl Executor<'_, Database = Database> + Copy) {
 | 
				
			||||||
 | 
					        if *LOG_TO_DATABASE {
 | 
				
			||||||
 | 
					            sqlx::query!(
 | 
				
			||||||
 | 
					                "INSERT INTO stat (type, reminder_id) VALUES ('reminder_sent', ?)",
 | 
				
			||||||
 | 
					                self.id,
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            .execute(pool)
 | 
				
			||||||
 | 
					            .await
 | 
				
			||||||
 | 
					            .expect("Could not log success to database");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async fn set_sent(&self, pool: impl Executor<'_, Database = Database> + Copy) {
 | 
				
			||||||
 | 
					        sqlx::query!("UPDATE reminders SET `status` = 'sent' WHERE `id` = ?", self.id)
 | 
				
			||||||
 | 
					            .execute(pool)
 | 
				
			||||||
 | 
					            .await
 | 
				
			||||||
 | 
					            .expect(&format!("Could not delete Reminder {}", self.id));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async fn set_failed(
 | 
				
			||||||
 | 
					        &self,
 | 
				
			||||||
 | 
					        pool: impl Executor<'_, Database = Database> + Copy,
 | 
				
			||||||
 | 
					        message: &'static str,
 | 
				
			||||||
 | 
					    ) {
 | 
				
			||||||
 | 
					        sqlx::query!(
 | 
				
			||||||
 | 
					            "UPDATE reminders SET `status` = 'failed', `status_message` = ? WHERE `id` = ?",
 | 
				
			||||||
 | 
					            message,
 | 
				
			||||||
 | 
					            self.id
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        .execute(pool)
 | 
				
			||||||
 | 
					        .await
 | 
				
			||||||
 | 
					        .expect(&format!("Could not delete Reminder {}", self.id));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async fn pin_message<M: Into<u64>>(&self, message_id: M, http: impl AsRef<Http>) {
 | 
				
			||||||
 | 
					        let _ = http.as_ref().pin_message(self.channel_id, message_id.into(), None).await;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub async fn send(
 | 
				
			||||||
 | 
					        &self,
 | 
				
			||||||
 | 
					        pool: impl Executor<'_, Database = Database> + Copy,
 | 
				
			||||||
 | 
					        cache_http: impl CacheHttp,
 | 
				
			||||||
 | 
					    ) {
 | 
				
			||||||
 | 
					        async fn send_to_channel(
 | 
				
			||||||
 | 
					            cache_http: impl CacheHttp,
 | 
				
			||||||
 | 
					            reminder: &Reminder,
 | 
				
			||||||
 | 
					            embed: Option<CreateEmbed>,
 | 
				
			||||||
 | 
					        ) -> Result<()> {
 | 
				
			||||||
 | 
					            let channel = ChannelId(reminder.channel_id).to_channel(&cache_http).await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            match channel {
 | 
				
			||||||
 | 
					                Ok(Channel::Guild(channel)) => {
 | 
				
			||||||
 | 
					                    match channel
 | 
				
			||||||
 | 
					                        .send_message(&cache_http, |m| {
 | 
				
			||||||
 | 
					                            m.content(&reminder.content).tts(reminder.tts);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                            if let (Some(attachment), Some(name)) =
 | 
				
			||||||
 | 
					                                (&reminder.attachment, &reminder.attachment_name)
 | 
				
			||||||
 | 
					                            {
 | 
				
			||||||
 | 
					                                m.add_file((attachment as &[u8], name.as_str()));
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                            if let Some(embed) = embed {
 | 
				
			||||||
 | 
					                                m.set_embed(embed);
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                            m
 | 
				
			||||||
 | 
					                        })
 | 
				
			||||||
 | 
					                        .await
 | 
				
			||||||
 | 
					                    {
 | 
				
			||||||
 | 
					                        Ok(m) => {
 | 
				
			||||||
 | 
					                            if reminder.pin {
 | 
				
			||||||
 | 
					                                reminder.pin_message(m.id, cache_http.http()).await;
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                            Ok(())
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                        Err(e) => Err(e),
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                Ok(Channel::Private(channel)) => {
 | 
				
			||||||
 | 
					                    match channel
 | 
				
			||||||
 | 
					                        .send_message(&cache_http.http(), |m| {
 | 
				
			||||||
 | 
					                            m.content(&reminder.content).tts(reminder.tts);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                            if let (Some(attachment), Some(name)) =
 | 
				
			||||||
 | 
					                                (&reminder.attachment, &reminder.attachment_name)
 | 
				
			||||||
 | 
					                            {
 | 
				
			||||||
 | 
					                                m.add_file((attachment as &[u8], name.as_str()));
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                            if let Some(embed) = embed {
 | 
				
			||||||
 | 
					                                m.set_embed(embed);
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                            m
 | 
				
			||||||
 | 
					                        })
 | 
				
			||||||
 | 
					                        .await
 | 
				
			||||||
 | 
					                    {
 | 
				
			||||||
 | 
					                        Ok(m) => {
 | 
				
			||||||
 | 
					                            if reminder.pin {
 | 
				
			||||||
 | 
					                                reminder.pin_message(m.id, cache_http.http()).await;
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                            Ok(())
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                        Err(e) => Err(e),
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                Err(e) => Err(e),
 | 
				
			||||||
 | 
					                _ => Err(Error::Other("Channel not of valid type")),
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        async fn send_to_webhook(
 | 
				
			||||||
 | 
					            cache_http: impl CacheHttp,
 | 
				
			||||||
 | 
					            reminder: &Reminder,
 | 
				
			||||||
 | 
					            webhook: Webhook,
 | 
				
			||||||
 | 
					            embed: Option<CreateEmbed>,
 | 
				
			||||||
 | 
					        ) -> Result<()> {
 | 
				
			||||||
 | 
					            match webhook
 | 
				
			||||||
 | 
					                .execute(&cache_http.http(), reminder.pin || reminder.restartable, |w| {
 | 
				
			||||||
 | 
					                    w.content(&reminder.content).tts(reminder.tts);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    if let Some(username) = &reminder.username {
 | 
				
			||||||
 | 
					                        if !username.is_empty() {
 | 
				
			||||||
 | 
					                            w.username(username);
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    if let Some(avatar) = &reminder.avatar {
 | 
				
			||||||
 | 
					                        w.avatar_url(avatar);
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    if let (Some(attachment), Some(name)) =
 | 
				
			||||||
 | 
					                        (&reminder.attachment, &reminder.attachment_name)
 | 
				
			||||||
 | 
					                    {
 | 
				
			||||||
 | 
					                        w.add_file((attachment as &[u8], name.as_str()));
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    if let Some(embed) = embed {
 | 
				
			||||||
 | 
					                        w.embeds(vec![SerenityEmbed::fake(|c| {
 | 
				
			||||||
 | 
					                            *c = embed;
 | 
				
			||||||
 | 
					                            c
 | 
				
			||||||
 | 
					                        })]);
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    w
 | 
				
			||||||
 | 
					                })
 | 
				
			||||||
 | 
					                .await
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                Ok(m) => {
 | 
				
			||||||
 | 
					                    if reminder.pin {
 | 
				
			||||||
 | 
					                        if let Some(message) = m {
 | 
				
			||||||
 | 
					                            reminder.pin_message(message.id, cache_http.http()).await;
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    Ok(())
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                Err(e) => Err(e),
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if self.enabled
 | 
				
			||||||
 | 
					            && !(self.channel_paused
 | 
				
			||||||
 | 
					                && self
 | 
				
			||||||
 | 
					                    .channel_paused_until
 | 
				
			||||||
 | 
					                    .map_or(true, |inner| inner >= Utc::now().naive_local()))
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            let _ = sqlx::query!(
 | 
				
			||||||
 | 
					                "UPDATE `channels` SET paused = 0, paused_until = NULL WHERE `channel` = ?",
 | 
				
			||||||
 | 
					                self.channel_id
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            .execute(pool)
 | 
				
			||||||
 | 
					            .await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            let embed = Embed::from_id(pool, self.id).await.map(|e| e.into());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            let result = if let (Some(webhook_id), Some(webhook_token)) =
 | 
				
			||||||
 | 
					                (self.webhook_id, &self.webhook_token)
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                let webhook_res =
 | 
				
			||||||
 | 
					                    cache_http.http().get_webhook_with_token(webhook_id, webhook_token).await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if let Ok(webhook) = webhook_res {
 | 
				
			||||||
 | 
					                    send_to_webhook(cache_http, &self, webhook, embed).await
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    warn!("Webhook vanished for reminder {}: {:?}", self.id, webhook_res);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    self.reset_webhook(pool).await;
 | 
				
			||||||
 | 
					                    send_to_channel(cache_http, &self, embed).await
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                send_to_channel(cache_http, &self, embed).await
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if let Err(e) = result {
 | 
				
			||||||
 | 
					                if let Error::Http(error) = e {
 | 
				
			||||||
 | 
					                    if let HttpError::UnsuccessfulRequest(http_error) = *error {
 | 
				
			||||||
 | 
					                        match http_error.error.code {
 | 
				
			||||||
 | 
					                            10003 => {
 | 
				
			||||||
 | 
					                                self.log_error(
 | 
				
			||||||
 | 
					                                    pool,
 | 
				
			||||||
 | 
					                                    "Could not be sent as channel does not exist",
 | 
				
			||||||
 | 
					                                    None::<&'static str>,
 | 
				
			||||||
 | 
					                                )
 | 
				
			||||||
 | 
					                                .await;
 | 
				
			||||||
 | 
					                                self.set_failed(
 | 
				
			||||||
 | 
					                                    pool,
 | 
				
			||||||
 | 
					                                    "Could not be sent as channel does not exist",
 | 
				
			||||||
 | 
					                                )
 | 
				
			||||||
 | 
					                                .await;
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					                            10004 => {
 | 
				
			||||||
 | 
					                                self.log_error(
 | 
				
			||||||
 | 
					                                    pool,
 | 
				
			||||||
 | 
					                                    "Could not be sent as guild does not exist",
 | 
				
			||||||
 | 
					                                    None::<&'static str>,
 | 
				
			||||||
 | 
					                                )
 | 
				
			||||||
 | 
					                                .await;
 | 
				
			||||||
 | 
					                                self.set_failed(pool, "Could not be sent as guild does not exist")
 | 
				
			||||||
 | 
					                                    .await;
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					                            50001 => {
 | 
				
			||||||
 | 
					                                self.log_error(
 | 
				
			||||||
 | 
					                                    pool,
 | 
				
			||||||
 | 
					                                    "Could not be sent as missing access",
 | 
				
			||||||
 | 
					                                    None::<&'static str>,
 | 
				
			||||||
 | 
					                                )
 | 
				
			||||||
 | 
					                                .await;
 | 
				
			||||||
 | 
					                                self.set_failed(pool, "Could not be sent as missing access").await;
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					                            50007 => {
 | 
				
			||||||
 | 
					                                self.log_error(
 | 
				
			||||||
 | 
					                                    pool,
 | 
				
			||||||
 | 
					                                    "Could not be sent as user has DMs disabled",
 | 
				
			||||||
 | 
					                                    None::<&'static str>,
 | 
				
			||||||
 | 
					                                )
 | 
				
			||||||
 | 
					                                .await;
 | 
				
			||||||
 | 
					                                self.set_failed(pool, "Could not be sent as user has DMs disabled")
 | 
				
			||||||
 | 
					                                    .await;
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					                            50013 => {
 | 
				
			||||||
 | 
					                                self.log_error(
 | 
				
			||||||
 | 
					                                    pool,
 | 
				
			||||||
 | 
					                                    "Could not be sent as permissions are invalid",
 | 
				
			||||||
 | 
					                                    None::<&'static str>,
 | 
				
			||||||
 | 
					                                )
 | 
				
			||||||
 | 
					                                .await;
 | 
				
			||||||
 | 
					                                self.set_failed(
 | 
				
			||||||
 | 
					                                    pool,
 | 
				
			||||||
 | 
					                                    "Could not be sent as permissions are invalid",
 | 
				
			||||||
 | 
					                                )
 | 
				
			||||||
 | 
					                                .await;
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					                            _ => {
 | 
				
			||||||
 | 
					                                self.log_error(
 | 
				
			||||||
 | 
					                                    pool,
 | 
				
			||||||
 | 
					                                    "HTTP error sending reminder",
 | 
				
			||||||
 | 
					                                    Some(http_error),
 | 
				
			||||||
 | 
					                                )
 | 
				
			||||||
 | 
					                                .await;
 | 
				
			||||||
 | 
					                                self.refresh(pool).await;
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    } else {
 | 
				
			||||||
 | 
					                        self.log_error(pool, "(Likely) a parsing error", Some(error)).await;
 | 
				
			||||||
 | 
					                        self.refresh(pool).await;
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    self.log_error(pool, "Non-HTTP error", Some(e)).await;
 | 
				
			||||||
 | 
					                    self.refresh(pool).await;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                self.log_success(pool).await;
 | 
				
			||||||
 | 
					                self.refresh(pool).await;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            info!("Reminder {} is paused", self.id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            self.refresh(pool).await;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										117
									
								
								src/commands/autocomplete.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										117
									
								
								src/commands/autocomplete.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,117 @@
 | 
				
			|||||||
 | 
					use std::time::{SystemTime, UNIX_EPOCH};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use chrono_tz::TZ_VARIANTS;
 | 
				
			||||||
 | 
					use poise::AutocompleteChoice;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::{models::CtxData, time_parser::natural_parser, Context};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub async fn timezone_autocomplete(ctx: Context<'_>, partial: &str) -> Vec<String> {
 | 
				
			||||||
 | 
					    if partial.is_empty() {
 | 
				
			||||||
 | 
					        ctx.data().popular_timezones.iter().map(|t| t.to_string()).collect::<Vec<String>>()
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        TZ_VARIANTS
 | 
				
			||||||
 | 
					            .iter()
 | 
				
			||||||
 | 
					            .filter(|tz| tz.to_string().contains(&partial))
 | 
				
			||||||
 | 
					            .take(25)
 | 
				
			||||||
 | 
					            .map(|t| t.to_string())
 | 
				
			||||||
 | 
					            .collect::<Vec<String>>()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub async fn macro_name_autocomplete(ctx: Context<'_>, partial: &str) -> Vec<String> {
 | 
				
			||||||
 | 
					    sqlx::query!(
 | 
				
			||||||
 | 
					        "
 | 
				
			||||||
 | 
					SELECT name
 | 
				
			||||||
 | 
					FROM macro
 | 
				
			||||||
 | 
					WHERE
 | 
				
			||||||
 | 
					    guild_id = (SELECT id FROM guilds WHERE guild = ?)
 | 
				
			||||||
 | 
					    AND name LIKE CONCAT(?, '%')",
 | 
				
			||||||
 | 
					        ctx.guild_id().unwrap().0,
 | 
				
			||||||
 | 
					        partial,
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    .fetch_all(&ctx.data().database)
 | 
				
			||||||
 | 
					    .await
 | 
				
			||||||
 | 
					    .unwrap_or_default()
 | 
				
			||||||
 | 
					    .iter()
 | 
				
			||||||
 | 
					    .map(|s| s.name.clone())
 | 
				
			||||||
 | 
					    .collect()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub async fn time_hint_autocomplete(
 | 
				
			||||||
 | 
					    ctx: Context<'_>,
 | 
				
			||||||
 | 
					    partial: &str,
 | 
				
			||||||
 | 
					) -> Vec<AutocompleteChoice<String>> {
 | 
				
			||||||
 | 
					    if partial.is_empty() {
 | 
				
			||||||
 | 
					        vec![AutocompleteChoice {
 | 
				
			||||||
 | 
					            name: "Start typing a time...".to_string(),
 | 
				
			||||||
 | 
					            value: "now".to_string(),
 | 
				
			||||||
 | 
					        }]
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        match natural_parser(partial, &ctx.timezone().await.to_string()).await {
 | 
				
			||||||
 | 
					            Some(timestamp) => match SystemTime::now().duration_since(UNIX_EPOCH) {
 | 
				
			||||||
 | 
					                Ok(now) => {
 | 
				
			||||||
 | 
					                    let diff = timestamp - now.as_secs() as i64;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    if diff < 0 {
 | 
				
			||||||
 | 
					                        vec![AutocompleteChoice {
 | 
				
			||||||
 | 
					                            name: "Time is in the past".to_string(),
 | 
				
			||||||
 | 
					                            value: "1 year ago".to_string(),
 | 
				
			||||||
 | 
					                        }]
 | 
				
			||||||
 | 
					                    } else {
 | 
				
			||||||
 | 
					                        if diff > 86400 {
 | 
				
			||||||
 | 
					                            vec![
 | 
				
			||||||
 | 
					                                AutocompleteChoice {
 | 
				
			||||||
 | 
					                                    name: partial.to_string(),
 | 
				
			||||||
 | 
					                                    value: partial.to_string(),
 | 
				
			||||||
 | 
					                                },
 | 
				
			||||||
 | 
					                                AutocompleteChoice {
 | 
				
			||||||
 | 
					                                    name: format!(
 | 
				
			||||||
 | 
					                                        "In approximately {} days, {} hours",
 | 
				
			||||||
 | 
					                                        diff / 86400,
 | 
				
			||||||
 | 
					                                        (diff % 86400) / 3600
 | 
				
			||||||
 | 
					                                    ),
 | 
				
			||||||
 | 
					                                    value: partial.to_string(),
 | 
				
			||||||
 | 
					                                },
 | 
				
			||||||
 | 
					                            ]
 | 
				
			||||||
 | 
					                        } else if diff > 3600 {
 | 
				
			||||||
 | 
					                            vec![
 | 
				
			||||||
 | 
					                                AutocompleteChoice {
 | 
				
			||||||
 | 
					                                    name: partial.to_string(),
 | 
				
			||||||
 | 
					                                    value: partial.to_string(),
 | 
				
			||||||
 | 
					                                },
 | 
				
			||||||
 | 
					                                AutocompleteChoice {
 | 
				
			||||||
 | 
					                                    name: format!("In approximately {} hours", diff / 3600),
 | 
				
			||||||
 | 
					                                    value: partial.to_string(),
 | 
				
			||||||
 | 
					                                },
 | 
				
			||||||
 | 
					                            ]
 | 
				
			||||||
 | 
					                        } else {
 | 
				
			||||||
 | 
					                            vec![
 | 
				
			||||||
 | 
					                                AutocompleteChoice {
 | 
				
			||||||
 | 
					                                    name: partial.to_string(),
 | 
				
			||||||
 | 
					                                    value: partial.to_string(),
 | 
				
			||||||
 | 
					                                },
 | 
				
			||||||
 | 
					                                AutocompleteChoice {
 | 
				
			||||||
 | 
					                                    name: format!("In approximately {} minutes", diff / 60),
 | 
				
			||||||
 | 
					                                    value: partial.to_string(),
 | 
				
			||||||
 | 
					                                },
 | 
				
			||||||
 | 
					                            ]
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                Err(_) => {
 | 
				
			||||||
 | 
					                    vec![AutocompleteChoice {
 | 
				
			||||||
 | 
					                        name: partial.to_string(),
 | 
				
			||||||
 | 
					                        value: partial.to_string(),
 | 
				
			||||||
 | 
					                    }]
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            None => {
 | 
				
			||||||
 | 
					                vec![AutocompleteChoice {
 | 
				
			||||||
 | 
					                    name: "Time not recognised".to_string(),
 | 
				
			||||||
 | 
					                    value: "now".to_string(),
 | 
				
			||||||
 | 
					                }]
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										46
									
								
								src/commands/command_macro/delete.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								src/commands/command_macro/delete.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,46 @@
 | 
				
			|||||||
 | 
					use super::super::autocomplete::macro_name_autocomplete;
 | 
				
			||||||
 | 
					use crate::{Context, Error};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Delete a recorded macro
 | 
				
			||||||
 | 
					#[poise::command(
 | 
				
			||||||
 | 
					    slash_command,
 | 
				
			||||||
 | 
					    rename = "delete",
 | 
				
			||||||
 | 
					    guild_only = true,
 | 
				
			||||||
 | 
					    default_member_permissions = "MANAGE_GUILD",
 | 
				
			||||||
 | 
					    identifying_name = "delete_macro"
 | 
				
			||||||
 | 
					)]
 | 
				
			||||||
 | 
					pub async fn delete_macro(
 | 
				
			||||||
 | 
					    ctx: Context<'_>,
 | 
				
			||||||
 | 
					    #[description = "Name of macro to delete"]
 | 
				
			||||||
 | 
					    #[autocomplete = "macro_name_autocomplete"]
 | 
				
			||||||
 | 
					    name: String,
 | 
				
			||||||
 | 
					) -> Result<(), Error> {
 | 
				
			||||||
 | 
					    match sqlx::query!(
 | 
				
			||||||
 | 
					        "
 | 
				
			||||||
 | 
					SELECT id FROM macro WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?) AND name = ?",
 | 
				
			||||||
 | 
					        ctx.guild_id().unwrap().0,
 | 
				
			||||||
 | 
					        name
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    .fetch_one(&ctx.data().database)
 | 
				
			||||||
 | 
					    .await
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        Ok(row) => {
 | 
				
			||||||
 | 
					            sqlx::query!("DELETE FROM macro WHERE id = ?", row.id)
 | 
				
			||||||
 | 
					                .execute(&ctx.data().database)
 | 
				
			||||||
 | 
					                .await
 | 
				
			||||||
 | 
					                .unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            ctx.say(format!("Macro \"{}\" deleted", name)).await?;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Err(sqlx::Error::RowNotFound) => {
 | 
				
			||||||
 | 
					            ctx.say(format!("Macro \"{}\" not found", name)).await?;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Err(e) => {
 | 
				
			||||||
 | 
					            panic!("{}", e);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Ok(())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										89
									
								
								src/commands/command_macro/list.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										89
									
								
								src/commands/command_macro/list.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,89 @@
 | 
				
			|||||||
 | 
					use poise::CreateReply;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::{
 | 
				
			||||||
 | 
					    component_models::pager::{MacroPager, Pager},
 | 
				
			||||||
 | 
					    consts::THEME_COLOR,
 | 
				
			||||||
 | 
					    models::{command_macro::CommandMacro, CtxData},
 | 
				
			||||||
 | 
					    Context, Error,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// List recorded macros
 | 
				
			||||||
 | 
					#[poise::command(
 | 
				
			||||||
 | 
					    slash_command,
 | 
				
			||||||
 | 
					    rename = "list",
 | 
				
			||||||
 | 
					    guild_only = true,
 | 
				
			||||||
 | 
					    default_member_permissions = "MANAGE_GUILD",
 | 
				
			||||||
 | 
					    identifying_name = "list_macro"
 | 
				
			||||||
 | 
					)]
 | 
				
			||||||
 | 
					pub async fn list_macro(ctx: Context<'_>) -> Result<(), Error> {
 | 
				
			||||||
 | 
					    let macros = ctx.command_macros().await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let resp = show_macro_page(¯os, 0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    ctx.send(|m| {
 | 
				
			||||||
 | 
					        *m = resp;
 | 
				
			||||||
 | 
					        m
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					    .await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Ok(())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub fn max_macro_page<U, E>(macros: &[CommandMacro<U, E>]) -> usize {
 | 
				
			||||||
 | 
					    ((macros.len() as f64) / 25.0).ceil() as usize
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub fn show_macro_page<U, E>(macros: &[CommandMacro<U, E>], page: usize) -> CreateReply {
 | 
				
			||||||
 | 
					    let pager = MacroPager::new(page);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if macros.is_empty() {
 | 
				
			||||||
 | 
					        let mut reply = CreateReply::default();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        reply.embed(|e| {
 | 
				
			||||||
 | 
					            e.title("Macros")
 | 
				
			||||||
 | 
					                .description("No Macros Set Up. Use `/macro record` to get started.")
 | 
				
			||||||
 | 
					                .color(*THEME_COLOR)
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return reply;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let pages = max_macro_page(macros);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let mut page = page;
 | 
				
			||||||
 | 
					    if page >= pages {
 | 
				
			||||||
 | 
					        page = pages - 1;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let lower = (page * 25).min(macros.len());
 | 
				
			||||||
 | 
					    let upper = ((page + 1) * 25).min(macros.len());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let fields = macros[lower..upper].iter().map(|m| {
 | 
				
			||||||
 | 
					        if let Some(description) = &m.description {
 | 
				
			||||||
 | 
					            (
 | 
				
			||||||
 | 
					                m.name.clone(),
 | 
				
			||||||
 | 
					                format!("*{}*\n- Has {} commands", description, m.commands.len()),
 | 
				
			||||||
 | 
					                true,
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            (m.name.clone(), format!("- Has {} commands", m.commands.len()), true)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let mut reply = CreateReply::default();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    reply
 | 
				
			||||||
 | 
					        .embed(|e| {
 | 
				
			||||||
 | 
					            e.title("Macros")
 | 
				
			||||||
 | 
					                .fields(fields)
 | 
				
			||||||
 | 
					                .footer(|f| f.text(format!("Page {} of {}", page + 1, pages)))
 | 
				
			||||||
 | 
					                .color(*THEME_COLOR)
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        .components(|comp| {
 | 
				
			||||||
 | 
					            pager.create_button_row(pages, comp);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            comp
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    reply
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										229
									
								
								src/commands/command_macro/migrate.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										229
									
								
								src/commands/command_macro/migrate.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,229 @@
 | 
				
			|||||||
 | 
					use lazy_regex::regex;
 | 
				
			||||||
 | 
					use poise::serenity_prelude::command::CommandOptionType;
 | 
				
			||||||
 | 
					use regex::Captures;
 | 
				
			||||||
 | 
					use serde_json::{json, Value};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::{models::command_macro::RawCommandMacro, Context, Error, GuildId};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					struct Alias {
 | 
				
			||||||
 | 
					    name: String,
 | 
				
			||||||
 | 
					    command: String,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Migrate old $alias reminder commands to macros. Only macro names that are not taken will be used.
 | 
				
			||||||
 | 
					#[poise::command(
 | 
				
			||||||
 | 
					    slash_command,
 | 
				
			||||||
 | 
					    rename = "migrate",
 | 
				
			||||||
 | 
					    guild_only = true,
 | 
				
			||||||
 | 
					    default_member_permissions = "MANAGE_GUILD",
 | 
				
			||||||
 | 
					    identifying_name = "migrate_macro"
 | 
				
			||||||
 | 
					)]
 | 
				
			||||||
 | 
					pub async fn migrate_macro(ctx: Context<'_>) -> Result<(), Error> {
 | 
				
			||||||
 | 
					    let guild_id = ctx.guild_id().unwrap();
 | 
				
			||||||
 | 
					    let mut transaction = ctx.data().database.begin().await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let aliases = sqlx::query_as!(
 | 
				
			||||||
 | 
					        Alias,
 | 
				
			||||||
 | 
					        "SELECT name, command FROM command_aliases WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?)",
 | 
				
			||||||
 | 
					        guild_id.0
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    .fetch_all(&mut *transaction)
 | 
				
			||||||
 | 
					    .await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let mut added_aliases = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for alias in aliases {
 | 
				
			||||||
 | 
					        match parse_text_command(guild_id, alias.name, &alias.command) {
 | 
				
			||||||
 | 
					            Some(cmd_macro) => {
 | 
				
			||||||
 | 
					                sqlx::query!(
 | 
				
			||||||
 | 
					                    "INSERT INTO macro (guild_id, name, description, commands) VALUES ((SELECT id FROM guilds WHERE guild = ?), ?, ?, ?)",
 | 
				
			||||||
 | 
					                    cmd_macro.guild_id.0,
 | 
				
			||||||
 | 
					                    cmd_macro.name,
 | 
				
			||||||
 | 
					                    cmd_macro.description,
 | 
				
			||||||
 | 
					                    cmd_macro.commands
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					                .execute(&mut *transaction)
 | 
				
			||||||
 | 
					                .await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                added_aliases += 1;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            None => {}
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    transaction.commit().await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    ctx.send(|b| b.content(format!("Added {} macros.", added_aliases))).await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Ok(())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fn parse_text_command(
 | 
				
			||||||
 | 
					    guild_id: GuildId,
 | 
				
			||||||
 | 
					    alias_name: String,
 | 
				
			||||||
 | 
					    command: &str,
 | 
				
			||||||
 | 
					) -> Option<RawCommandMacro> {
 | 
				
			||||||
 | 
					    match command.split_once(" ") {
 | 
				
			||||||
 | 
					        Some((command_word, args)) => {
 | 
				
			||||||
 | 
					            let command_word = command_word.to_lowercase();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if command_word == "r"
 | 
				
			||||||
 | 
					                || command_word == "i"
 | 
				
			||||||
 | 
					                || command_word == "remind"
 | 
				
			||||||
 | 
					                || command_word == "interval"
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                let matcher = regex!(
 | 
				
			||||||
 | 
					                    r#"(?P<mentions>(?:<@\d+>\s+|<@!\d+>\s+|<#\d+>\s+)*)(?P<time>(?:(?:\d+)(?:s|m|h|d|:|/|-|))+)(?:\s+(?P<interval>(?:(?:\d+)(?:s|m|h|d|))+))?(?:\s+(?P<expires>(?:(?:\d+)(?:s|m|h|d|:|/|-|))+))?\s+(?P<content>.*)"#s
 | 
				
			||||||
 | 
					                );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                match matcher.captures(&args) {
 | 
				
			||||||
 | 
					                    Some(captures) => {
 | 
				
			||||||
 | 
					                        let mut args: Vec<Value> = vec![];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        if let Some(group) = captures.name("time") {
 | 
				
			||||||
 | 
					                            let content = group.as_str();
 | 
				
			||||||
 | 
					                            args.push(json!({
 | 
				
			||||||
 | 
					                                "name": "time",
 | 
				
			||||||
 | 
					                                "value": content,
 | 
				
			||||||
 | 
					                                "type": CommandOptionType::String,
 | 
				
			||||||
 | 
					                            }));
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        if let Some(group) = captures.name("content") {
 | 
				
			||||||
 | 
					                            let content = group.as_str();
 | 
				
			||||||
 | 
					                            args.push(json!({
 | 
				
			||||||
 | 
					                                "name": "content",
 | 
				
			||||||
 | 
					                                "value": content,
 | 
				
			||||||
 | 
					                                "type": CommandOptionType::String,
 | 
				
			||||||
 | 
					                            }));
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        if let Some(group) = captures.name("interval") {
 | 
				
			||||||
 | 
					                            let content = group.as_str();
 | 
				
			||||||
 | 
					                            args.push(json!({
 | 
				
			||||||
 | 
					                                "name": "interval",
 | 
				
			||||||
 | 
					                                "value": content,
 | 
				
			||||||
 | 
					                                "type": CommandOptionType::String,
 | 
				
			||||||
 | 
					                            }));
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        if let Some(group) = captures.name("expires") {
 | 
				
			||||||
 | 
					                            let content = group.as_str();
 | 
				
			||||||
 | 
					                            args.push(json!({
 | 
				
			||||||
 | 
					                                "name": "expires",
 | 
				
			||||||
 | 
					                                "value": content,
 | 
				
			||||||
 | 
					                                "type": CommandOptionType::String,
 | 
				
			||||||
 | 
					                            }));
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        if let Some(group) = captures.name("mentions") {
 | 
				
			||||||
 | 
					                            let content = group.as_str();
 | 
				
			||||||
 | 
					                            args.push(json!({
 | 
				
			||||||
 | 
					                                "name": "channels",
 | 
				
			||||||
 | 
					                                "value": content,
 | 
				
			||||||
 | 
					                                "type": CommandOptionType::String,
 | 
				
			||||||
 | 
					                            }));
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        Some(RawCommandMacro {
 | 
				
			||||||
 | 
					                            guild_id,
 | 
				
			||||||
 | 
					                            name: alias_name,
 | 
				
			||||||
 | 
					                            description: None,
 | 
				
			||||||
 | 
					                            commands: json!([
 | 
				
			||||||
 | 
					                                {
 | 
				
			||||||
 | 
					                                    "command_name": "remind",
 | 
				
			||||||
 | 
					                                    "options": args,
 | 
				
			||||||
 | 
					                                }
 | 
				
			||||||
 | 
					                            ]),
 | 
				
			||||||
 | 
					                        })
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    None => None,
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            } else if command_word == "n" || command_word == "natural" {
 | 
				
			||||||
 | 
					                let matcher_primary = regex!(
 | 
				
			||||||
 | 
					                    r#"(?P<time>.*?)(?:\s+)(?:send|say)(?:\s+)(?P<content>.*?)(?:(?:\s+)to(?:\s+)(?P<mentions>((?:<@\d+>)|(?:<@!\d+>)|(?:<#\d+>)|(?:\s+))+))?$"#s
 | 
				
			||||||
 | 
					                );
 | 
				
			||||||
 | 
					                let matcher_secondary = regex!(
 | 
				
			||||||
 | 
					                    r#"(?P<msg>.*)(?:\s+)every(?:\s+)(?P<interval>.*?)(?:(?:\s+)(?:until|for)(?:\s+)(?P<expires>.*?))?$"#s
 | 
				
			||||||
 | 
					                );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                match matcher_primary.captures(&args) {
 | 
				
			||||||
 | 
					                    Some(captures) => {
 | 
				
			||||||
 | 
					                        let captures_secondary = matcher_secondary.captures(&args);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        let mut args: Vec<Value> = vec![];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        if let Some(group) = captures.name("time") {
 | 
				
			||||||
 | 
					                            let content = group.as_str();
 | 
				
			||||||
 | 
					                            args.push(json!({
 | 
				
			||||||
 | 
					                                "name": "time",
 | 
				
			||||||
 | 
					                                "value": content,
 | 
				
			||||||
 | 
					                                "type": CommandOptionType::String,
 | 
				
			||||||
 | 
					                            }));
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        if let Some(group) = captures.name("content") {
 | 
				
			||||||
 | 
					                            let content = group.as_str();
 | 
				
			||||||
 | 
					                            args.push(json!({
 | 
				
			||||||
 | 
					                                "name": "content",
 | 
				
			||||||
 | 
					                                "value": content,
 | 
				
			||||||
 | 
					                                "type": CommandOptionType::String,
 | 
				
			||||||
 | 
					                            }));
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        if let Some(group) =
 | 
				
			||||||
 | 
					                            captures_secondary.as_ref().and_then(|c: &Captures| c.name("interval"))
 | 
				
			||||||
 | 
					                        {
 | 
				
			||||||
 | 
					                            let content = group.as_str();
 | 
				
			||||||
 | 
					                            args.push(json!({
 | 
				
			||||||
 | 
					                                "name": "interval",
 | 
				
			||||||
 | 
					                                "value": content,
 | 
				
			||||||
 | 
					                                "type": CommandOptionType::String,
 | 
				
			||||||
 | 
					                            }));
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        if let Some(group) =
 | 
				
			||||||
 | 
					                            captures_secondary.and_then(|c: Captures| c.name("expires"))
 | 
				
			||||||
 | 
					                        {
 | 
				
			||||||
 | 
					                            let content = group.as_str();
 | 
				
			||||||
 | 
					                            args.push(json!({
 | 
				
			||||||
 | 
					                                "name": "expires",
 | 
				
			||||||
 | 
					                                "value": content,
 | 
				
			||||||
 | 
					                                "type": CommandOptionType::String,
 | 
				
			||||||
 | 
					                            }));
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        if let Some(group) = captures.name("mentions") {
 | 
				
			||||||
 | 
					                            let content = group.as_str();
 | 
				
			||||||
 | 
					                            args.push(json!({
 | 
				
			||||||
 | 
					                                "name": "channels",
 | 
				
			||||||
 | 
					                                "value": content,
 | 
				
			||||||
 | 
					                                "type": CommandOptionType::String,
 | 
				
			||||||
 | 
					                            }));
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        Some(RawCommandMacro {
 | 
				
			||||||
 | 
					                            guild_id,
 | 
				
			||||||
 | 
					                            name: alias_name,
 | 
				
			||||||
 | 
					                            description: None,
 | 
				
			||||||
 | 
					                            commands: json!([
 | 
				
			||||||
 | 
					                                {
 | 
				
			||||||
 | 
					                                    "command_name": "remind",
 | 
				
			||||||
 | 
					                                    "options": args,
 | 
				
			||||||
 | 
					                                }
 | 
				
			||||||
 | 
					                            ]),
 | 
				
			||||||
 | 
					                        })
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    None => None,
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                None
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        None => None,
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										19
									
								
								src/commands/command_macro/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								src/commands/command_macro/mod.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,19 @@
 | 
				
			|||||||
 | 
					use crate::{Context, Error};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub mod delete;
 | 
				
			||||||
 | 
					pub mod list;
 | 
				
			||||||
 | 
					pub mod migrate;
 | 
				
			||||||
 | 
					pub mod record;
 | 
				
			||||||
 | 
					pub mod run;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Record and replay command sequences
 | 
				
			||||||
 | 
					#[poise::command(
 | 
				
			||||||
 | 
					    slash_command,
 | 
				
			||||||
 | 
					    rename = "macro",
 | 
				
			||||||
 | 
					    guild_only = true,
 | 
				
			||||||
 | 
					    default_member_permissions = "MANAGE_GUILD",
 | 
				
			||||||
 | 
					    identifying_name = "macro_base"
 | 
				
			||||||
 | 
					)]
 | 
				
			||||||
 | 
					pub async fn macro_base(_ctx: Context<'_>) -> Result<(), Error> {
 | 
				
			||||||
 | 
					    Ok(())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										151
									
								
								src/commands/command_macro/record.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										151
									
								
								src/commands/command_macro/record.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,151 @@
 | 
				
			|||||||
 | 
					use std::collections::hash_map::Entry;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::{consts::THEME_COLOR, models::command_macro::CommandMacro, Context, Error};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Start recording up to 5 commands to replay
 | 
				
			||||||
 | 
					#[poise::command(
 | 
				
			||||||
 | 
					    slash_command,
 | 
				
			||||||
 | 
					    rename = "record",
 | 
				
			||||||
 | 
					    guild_only = true,
 | 
				
			||||||
 | 
					    default_member_permissions = "MANAGE_GUILD",
 | 
				
			||||||
 | 
					    identifying_name = "record_macro"
 | 
				
			||||||
 | 
					)]
 | 
				
			||||||
 | 
					pub async fn record_macro(
 | 
				
			||||||
 | 
					    ctx: Context<'_>,
 | 
				
			||||||
 | 
					    #[description = "Name for the new macro"] name: String,
 | 
				
			||||||
 | 
					    #[description = "Description for the new macro"] description: Option<String>,
 | 
				
			||||||
 | 
					) -> Result<(), Error> {
 | 
				
			||||||
 | 
					    if name.len() > 100 {
 | 
				
			||||||
 | 
					        ctx.say("Name must be less than 100 characters").await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return Ok(());
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if description.as_ref().map_or(0, |d| d.len()) > 100 {
 | 
				
			||||||
 | 
					        ctx.say("Description must be less than 100 characters").await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return Ok(());
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let guild_id = ctx.guild_id().unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let row = sqlx::query!(
 | 
				
			||||||
 | 
					        "
 | 
				
			||||||
 | 
					SELECT 1 as _e FROM macro WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?) AND name = ?",
 | 
				
			||||||
 | 
					        guild_id.0,
 | 
				
			||||||
 | 
					        name
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    .fetch_one(&ctx.data().database)
 | 
				
			||||||
 | 
					    .await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if row.is_ok() {
 | 
				
			||||||
 | 
					        ctx.send(|m| {
 | 
				
			||||||
 | 
					            m.ephemeral(true).embed(|e| {
 | 
				
			||||||
 | 
					                e.title("Unique Name Required")
 | 
				
			||||||
 | 
					                    .description(
 | 
				
			||||||
 | 
					                        "A macro already exists under this name.
 | 
				
			||||||
 | 
					Please select a unique name for your macro.",
 | 
				
			||||||
 | 
					                    )
 | 
				
			||||||
 | 
					                    .color(*THEME_COLOR)
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        .await?;
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        let okay = {
 | 
				
			||||||
 | 
					            let mut lock = ctx.data().recording_macros.write().await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if let Entry::Vacant(e) = lock.entry((guild_id, ctx.author().id)) {
 | 
				
			||||||
 | 
					                e.insert(CommandMacro { guild_id, name, description, commands: vec![] });
 | 
				
			||||||
 | 
					                true
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                false
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if okay {
 | 
				
			||||||
 | 
					            ctx.send(|m| {
 | 
				
			||||||
 | 
					                m.ephemeral(true).embed(|e| {
 | 
				
			||||||
 | 
					                    e.title("Macro Recording Started")
 | 
				
			||||||
 | 
					                        .description(
 | 
				
			||||||
 | 
					                            "Run up to 5 commands, or type `/macro finish` to stop at any point.
 | 
				
			||||||
 | 
					Any commands ran as part of recording will be inconsequential",
 | 
				
			||||||
 | 
					                        )
 | 
				
			||||||
 | 
					                        .color(*THEME_COLOR)
 | 
				
			||||||
 | 
					                })
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					            .await?;
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            ctx.send(|m| {
 | 
				
			||||||
 | 
					                m.ephemeral(true).embed(|e| {
 | 
				
			||||||
 | 
					                    e.title("Macro Already Recording")
 | 
				
			||||||
 | 
					                        .description(
 | 
				
			||||||
 | 
					                            "You are already recording a macro in this server.
 | 
				
			||||||
 | 
					Please use `/macro finish` to end this recording before starting another.",
 | 
				
			||||||
 | 
					                        )
 | 
				
			||||||
 | 
					                        .color(*THEME_COLOR)
 | 
				
			||||||
 | 
					                })
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					            .await?;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Ok(())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Finish current macro recording
 | 
				
			||||||
 | 
					#[poise::command(
 | 
				
			||||||
 | 
					    slash_command,
 | 
				
			||||||
 | 
					    rename = "finish",
 | 
				
			||||||
 | 
					    guild_only = true,
 | 
				
			||||||
 | 
					    default_member_permissions = "MANAGE_GUILD",
 | 
				
			||||||
 | 
					    identifying_name = "finish_macro"
 | 
				
			||||||
 | 
					)]
 | 
				
			||||||
 | 
					pub async fn finish_macro(ctx: Context<'_>) -> Result<(), Error> {
 | 
				
			||||||
 | 
					    let key = (ctx.guild_id().unwrap(), ctx.author().id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        let lock = ctx.data().recording_macros.read().await;
 | 
				
			||||||
 | 
					        let contained = lock.get(&key);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if contained.map_or(true, |cmacro| cmacro.commands.is_empty()) {
 | 
				
			||||||
 | 
					            ctx.send(|m| {
 | 
				
			||||||
 | 
					                m.embed(|e| {
 | 
				
			||||||
 | 
					                    e.title("No Macro Recorded")
 | 
				
			||||||
 | 
					                        .description("Use `/macro record` to start recording a macro")
 | 
				
			||||||
 | 
					                        .color(*THEME_COLOR)
 | 
				
			||||||
 | 
					                })
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					            .await?;
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            let command_macro = contained.unwrap();
 | 
				
			||||||
 | 
					            let json = serde_json::to_string(&command_macro.commands).unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            sqlx::query!(
 | 
				
			||||||
 | 
					                "INSERT INTO macro (guild_id, name, description, commands) VALUES ((SELECT id FROM guilds WHERE guild = ?), ?, ?, ?)",
 | 
				
			||||||
 | 
					                command_macro.guild_id.0,
 | 
				
			||||||
 | 
					                command_macro.name,
 | 
				
			||||||
 | 
					                command_macro.description,
 | 
				
			||||||
 | 
					                json
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					                .execute(&ctx.data().database)
 | 
				
			||||||
 | 
					                .await
 | 
				
			||||||
 | 
					                .unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            ctx.send(|m| {
 | 
				
			||||||
 | 
					                m.embed(|e| {
 | 
				
			||||||
 | 
					                    e.title("Macro Recorded")
 | 
				
			||||||
 | 
					                        .description("Use `/macro run` to execute the macro")
 | 
				
			||||||
 | 
					                        .color(*THEME_COLOR)
 | 
				
			||||||
 | 
					                })
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					            .await?;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        let mut lock = ctx.data().recording_macros.write().await;
 | 
				
			||||||
 | 
					        lock.remove(&key);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Ok(())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										56
									
								
								src/commands/command_macro/run.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								src/commands/command_macro/run.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,56 @@
 | 
				
			|||||||
 | 
					use super::super::autocomplete::macro_name_autocomplete;
 | 
				
			||||||
 | 
					use crate::{models::command_macro::guild_command_macro, Context, Data, Error, THEME_COLOR};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Run a recorded macro
 | 
				
			||||||
 | 
					#[poise::command(
 | 
				
			||||||
 | 
					    slash_command,
 | 
				
			||||||
 | 
					    rename = "run",
 | 
				
			||||||
 | 
					    guild_only = true,
 | 
				
			||||||
 | 
					    default_member_permissions = "MANAGE_GUILD",
 | 
				
			||||||
 | 
					    identifying_name = "run_macro"
 | 
				
			||||||
 | 
					)]
 | 
				
			||||||
 | 
					pub async fn run_macro(
 | 
				
			||||||
 | 
					    ctx: poise::ApplicationContext<'_, Data, Error>,
 | 
				
			||||||
 | 
					    #[description = "Name of macro to run"]
 | 
				
			||||||
 | 
					    #[autocomplete = "macro_name_autocomplete"]
 | 
				
			||||||
 | 
					    name: String,
 | 
				
			||||||
 | 
					) -> Result<(), Error> {
 | 
				
			||||||
 | 
					    match guild_command_macro(&Context::Application(ctx), &name).await {
 | 
				
			||||||
 | 
					        Some(command_macro) => {
 | 
				
			||||||
 | 
					            Context::Application(ctx)
 | 
				
			||||||
 | 
					                .send(|b| {
 | 
				
			||||||
 | 
					                    b.embed(|e| {
 | 
				
			||||||
 | 
					                        e.title("Running Macro").color(*THEME_COLOR).description(format!(
 | 
				
			||||||
 | 
					                            "Running macro {} ({} commands)",
 | 
				
			||||||
 | 
					                            command_macro.name,
 | 
				
			||||||
 | 
					                            command_macro.commands.len()
 | 
				
			||||||
 | 
					                        ))
 | 
				
			||||||
 | 
					                    })
 | 
				
			||||||
 | 
					                })
 | 
				
			||||||
 | 
					                .await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            for command in command_macro.commands {
 | 
				
			||||||
 | 
					                if let Some(action) = command.action {
 | 
				
			||||||
 | 
					                    match (action)(poise::ApplicationContext { args: &command.options, ..ctx })
 | 
				
			||||||
 | 
					                        .await
 | 
				
			||||||
 | 
					                    {
 | 
				
			||||||
 | 
					                        Ok(()) => {}
 | 
				
			||||||
 | 
					                        Err(e) => {
 | 
				
			||||||
 | 
					                            println!("{:?}", e);
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    Context::Application(ctx)
 | 
				
			||||||
 | 
					                        .say(format!("Command \"{}\" not found", command.command_name))
 | 
				
			||||||
 | 
					                        .await?;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        None => {
 | 
				
			||||||
 | 
					            Context::Application(ctx).say(format!("Macro \"{}\" not found", name)).await?;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Ok(())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,16 +1,13 @@
 | 
				
			|||||||
use chrono::offset::Utc;
 | 
					use chrono::offset::Utc;
 | 
				
			||||||
use regex_command_attr::command;
 | 
					use poise::{serenity_prelude as serenity, serenity_prelude::Mentionable};
 | 
				
			||||||
use serenity::{builder::CreateEmbedFooter, client::Context};
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::{
 | 
					use crate::{models::CtxData, Context, Error, THEME_COLOR};
 | 
				
			||||||
    framework::{CommandInvoke, CreateGenericResponse},
 | 
					 | 
				
			||||||
    models::CtxData,
 | 
					 | 
				
			||||||
    THEME_COLOR,
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
fn footer(ctx: &Context) -> impl FnOnce(&mut CreateEmbedFooter) -> &mut CreateEmbedFooter {
 | 
					fn footer(
 | 
				
			||||||
    let shard_count = ctx.cache.shard_count();
 | 
					    ctx: Context<'_>,
 | 
				
			||||||
    let shard = ctx.shard_id;
 | 
					) -> impl FnOnce(&mut serenity::CreateEmbedFooter) -> &mut serenity::CreateEmbedFooter {
 | 
				
			||||||
 | 
					    let shard_count = ctx.serenity_context().cache.shard_count();
 | 
				
			||||||
 | 
					    let shard = ctx.serenity_context().shard_id;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    move |f| {
 | 
					    move |f| {
 | 
				
			||||||
        f.text(format!(
 | 
					        f.text(format!(
 | 
				
			||||||
@@ -22,15 +19,13 @@ fn footer(ctx: &Context) -> impl FnOnce(&mut CreateEmbedFooter) -> &mut CreateEm
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[command]
 | 
					/// Get an overview of bot commands
 | 
				
			||||||
#[description("Get an overview of the bot commands")]
 | 
					#[poise::command(slash_command)]
 | 
				
			||||||
async fn help(ctx: &Context, invoke: &mut CommandInvoke) {
 | 
					pub async fn help(ctx: Context<'_>) -> Result<(), Error> {
 | 
				
			||||||
    let footer = footer(ctx);
 | 
					    let footer = footer(ctx);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let _ = invoke
 | 
					    ctx.send(|m| {
 | 
				
			||||||
        .respond(
 | 
					        m.ephemeral(true).embed(|e| {
 | 
				
			||||||
            &ctx,
 | 
					 | 
				
			||||||
            CreateGenericResponse::new().embed(|e| {
 | 
					 | 
				
			||||||
            e.title("Help")
 | 
					            e.title("Help")
 | 
				
			||||||
                .color(*THEME_COLOR)
 | 
					                .color(*THEME_COLOR)
 | 
				
			||||||
                .description(
 | 
					                .description(
 | 
				
			||||||
@@ -54,29 +49,30 @@ __Todo Commands__
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
__Setup Commands__
 | 
					__Setup Commands__
 | 
				
			||||||
`/timezone` - Set your timezone (necessary for `/remind` to work properly)
 | 
					`/timezone` - Set your timezone (necessary for `/remind` to work properly)
 | 
				
			||||||
 | 
					`/dm allow/block` - Change your DM settings for reminders.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
__Advanced Commands__
 | 
					__Advanced Commands__
 | 
				
			||||||
`/macro` - Record and replay command sequences
 | 
					`/macro` - Record and replay command sequences
 | 
				
			||||||
                    ",
 | 
					                    ",
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
                .footer(footer)
 | 
					                .footer(footer)
 | 
				
			||||||
            }),
 | 
					        })
 | 
				
			||||||
        )
 | 
					    })
 | 
				
			||||||
        .await;
 | 
					    .await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Ok(())
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[command]
 | 
					/// Get information about the bot
 | 
				
			||||||
#[aliases("invite")]
 | 
					#[poise::command(slash_command)]
 | 
				
			||||||
#[description("Get information about the bot")]
 | 
					pub async fn info(ctx: Context<'_>) -> Result<(), Error> {
 | 
				
			||||||
async fn info(ctx: &Context, invoke: &mut CommandInvoke) {
 | 
					 | 
				
			||||||
    let footer = footer(ctx);
 | 
					    let footer = footer(ctx);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let _ = invoke
 | 
					    let _ = ctx
 | 
				
			||||||
        .respond(
 | 
					        .send(|m| {
 | 
				
			||||||
            ctx.http.clone(),
 | 
					            m.ephemeral(true).embed(|e| {
 | 
				
			||||||
            CreateGenericResponse::new().embed(|e| {
 | 
					 | 
				
			||||||
                e.title("Info")
 | 
					                e.title("Info")
 | 
				
			||||||
                    .description(format!(
 | 
					                    .description(
 | 
				
			||||||
                        "Help: `/help`
 | 
					                        "Help: `/help`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
**Welcome to Reminder Bot!**
 | 
					**Welcome to Reminder Bot!**
 | 
				
			||||||
@@ -86,26 +82,25 @@ Find me on https://discord.jellywx.com and on https://github.com/JellyWX :)
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
Invite the bot: https://invite.reminder-bot.com/
 | 
					Invite the bot: https://invite.reminder-bot.com/
 | 
				
			||||||
Use our dashboard: https://reminder-bot.com/",
 | 
					Use our dashboard: https://reminder-bot.com/",
 | 
				
			||||||
                    ))
 | 
					                    )
 | 
				
			||||||
                    .footer(footer)
 | 
					                    .footer(footer)
 | 
				
			||||||
                    .color(*THEME_COLOR)
 | 
					                    .color(*THEME_COLOR)
 | 
				
			||||||
            }),
 | 
					            })
 | 
				
			||||||
        )
 | 
					        })
 | 
				
			||||||
        .await;
 | 
					        .await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Ok(())
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[command]
 | 
					/// Details on supporting the bot and Patreon benefits
 | 
				
			||||||
#[description("Details on supporting the bot and Patreon benefits")]
 | 
					#[poise::command(slash_command)]
 | 
				
			||||||
#[group("Info")]
 | 
					pub async fn donate(ctx: Context<'_>) -> Result<(), Error> {
 | 
				
			||||||
async fn donate(ctx: &Context, invoke: &mut CommandInvoke) {
 | 
					 | 
				
			||||||
    let footer = footer(ctx);
 | 
					    let footer = footer(ctx);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let _ = invoke
 | 
					    ctx.send(|m| m.embed(|e| {
 | 
				
			||||||
        .respond(
 | 
					 | 
				
			||||||
            ctx.http.clone(),
 | 
					 | 
				
			||||||
            CreateGenericResponse::new().embed(|e| {
 | 
					 | 
				
			||||||
        e.title("Donate")
 | 
					        e.title("Donate")
 | 
				
			||||||
                    .description("Thinking of adding a monthly contribution? Click below for my Patreon and official bot server :)
 | 
					            .description("Thinking of adding a monthly contribution?
 | 
				
			||||||
 | 
					Click below for my Patreon and official bot server :)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
**https://www.patreon.com/jellywx/**
 | 
					**https://www.patreon.com/jellywx/**
 | 
				
			||||||
**https://discord.jellywx.com/**
 | 
					**https://discord.jellywx.com/**
 | 
				
			||||||
@@ -124,39 +119,63 @@ Just $2 USD/month!
 | 
				
			|||||||
                .color(*THEME_COLOR)
 | 
					                .color(*THEME_COLOR)
 | 
				
			||||||
        }),
 | 
					        }),
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
        .await;
 | 
					    .await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Ok(())
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[command]
 | 
					/// Get the link to the online dashboard
 | 
				
			||||||
#[description("Get the link to the online dashboard")]
 | 
					#[poise::command(slash_command)]
 | 
				
			||||||
#[group("Info")]
 | 
					pub async fn dashboard(ctx: Context<'_>) -> Result<(), Error> {
 | 
				
			||||||
async fn dashboard(ctx: &Context, invoke: &mut CommandInvoke) {
 | 
					 | 
				
			||||||
    let footer = footer(ctx);
 | 
					    let footer = footer(ctx);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let _ = invoke
 | 
					    ctx.send(|m| {
 | 
				
			||||||
        .respond(
 | 
					        m.ephemeral(true).embed(|e| {
 | 
				
			||||||
            ctx.http.clone(),
 | 
					 | 
				
			||||||
            CreateGenericResponse::new().embed(|e| {
 | 
					 | 
				
			||||||
            e.title("Dashboard")
 | 
					            e.title("Dashboard")
 | 
				
			||||||
                .description("**https://reminder-bot.com/dashboard**")
 | 
					                .description("**https://reminder-bot.com/dashboard**")
 | 
				
			||||||
                .footer(footer)
 | 
					                .footer(footer)
 | 
				
			||||||
                .color(*THEME_COLOR)
 | 
					                .color(*THEME_COLOR)
 | 
				
			||||||
            }),
 | 
					        })
 | 
				
			||||||
        )
 | 
					    })
 | 
				
			||||||
        .await;
 | 
					    .await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Ok(())
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[command]
 | 
					/// View the current time in your selected timezone
 | 
				
			||||||
#[description("View the current time in your selected timezone")]
 | 
					#[poise::command(slash_command)]
 | 
				
			||||||
#[group("Info")]
 | 
					pub async fn clock(ctx: Context<'_>) -> Result<(), Error> {
 | 
				
			||||||
async fn clock(ctx: &Context, invoke: &mut CommandInvoke) {
 | 
					    ctx.defer_ephemeral().await?;
 | 
				
			||||||
    let ud = ctx.user_data(&invoke.author_id()).await.unwrap();
 | 
					 | 
				
			||||||
    let now = Utc::now().with_timezone(&ud.timezone());
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let _ = invoke
 | 
					    let tz = ctx.timezone().await;
 | 
				
			||||||
        .respond(
 | 
					    let now = Utc::now().with_timezone(&tz);
 | 
				
			||||||
            ctx.http.clone(),
 | 
					
 | 
				
			||||||
            CreateGenericResponse::new().content(format!("Current time: {}", now.format("%H:%M"))),
 | 
					    ctx.send(|m| {
 | 
				
			||||||
        )
 | 
					        m.ephemeral(true).content(format!("Time in **{}**: `{}`", tz, now.format("%H:%M")))
 | 
				
			||||||
        .await;
 | 
					    })
 | 
				
			||||||
 | 
					    .await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Ok(())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// View the current time in a user's selected timezone
 | 
				
			||||||
 | 
					#[poise::command(context_menu_command = "View Local Time")]
 | 
				
			||||||
 | 
					pub async fn clock_context_menu(ctx: Context<'_>, user: serenity::User) -> Result<(), Error> {
 | 
				
			||||||
 | 
					    ctx.defer_ephemeral().await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let user_data = ctx.user_data(user.id).await?;
 | 
				
			||||||
 | 
					    let tz = user_data.timezone();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let now = Utc::now().with_timezone(&tz);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    ctx.send(|m| {
 | 
				
			||||||
 | 
					        m.ephemeral(true).content(format!(
 | 
				
			||||||
 | 
					            "Time in {}'s timezone: `{}`",
 | 
				
			||||||
 | 
					            user.mention(),
 | 
				
			||||||
 | 
					            now.format("%H:%M")
 | 
				
			||||||
 | 
					        ))
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					    .await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Ok(())
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,3 +1,5 @@
 | 
				
			|||||||
 | 
					mod autocomplete;
 | 
				
			||||||
 | 
					pub mod command_macro;
 | 
				
			||||||
pub mod info_cmds;
 | 
					pub mod info_cmds;
 | 
				
			||||||
pub mod moderation_cmds;
 | 
					pub mod moderation_cmds;
 | 
				
			||||||
pub mod reminder_cmds;
 | 
					pub mod reminder_cmds;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,54 +1,43 @@
 | 
				
			|||||||
use chrono::offset::Utc;
 | 
					use chrono::offset::Utc;
 | 
				
			||||||
use chrono_tz::{Tz, TZ_VARIANTS};
 | 
					use chrono_tz::{Tz, TZ_VARIANTS};
 | 
				
			||||||
use levenshtein::levenshtein;
 | 
					use levenshtein::levenshtein;
 | 
				
			||||||
use regex_command_attr::command;
 | 
					use log::warn;
 | 
				
			||||||
use serenity::client::Context;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::{
 | 
					use super::autocomplete::timezone_autocomplete;
 | 
				
			||||||
    component_models::pager::{MacroPager, Pager},
 | 
					use crate::{consts::THEME_COLOR, models::CtxData, Context, Error};
 | 
				
			||||||
    consts::{EMBED_DESCRIPTION_MAX_LENGTH, THEME_COLOR},
 | 
					 | 
				
			||||||
    framework::{CommandInvoke, CommandOptions, CreateGenericResponse, OptionValue},
 | 
					 | 
				
			||||||
    hooks::{CHECK_GUILD_PERMISSIONS_HOOK, GUILD_ONLY_HOOK},
 | 
					 | 
				
			||||||
    models::{command_macro::CommandMacro, CtxData},
 | 
					 | 
				
			||||||
    PopularTimezones, RecordingMacros, RegexFramework, SQLPool,
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[command("timezone")]
 | 
					/// Select your timezone
 | 
				
			||||||
#[description("Select your timezone")]
 | 
					#[poise::command(slash_command, identifying_name = "timezone")]
 | 
				
			||||||
#[arg(
 | 
					pub async fn timezone(
 | 
				
			||||||
    name = "timezone",
 | 
					    ctx: Context<'_>,
 | 
				
			||||||
    description = "Timezone to use from this list: https://gist.github.com/JellyWX/913dfc8b63d45192ad6cb54c829324ee",
 | 
					    #[description = "Timezone to use from this list: https://gist.github.com/JellyWX/913dfc8b63d45192ad6cb54c829324ee"]
 | 
				
			||||||
    kind = "String",
 | 
					    #[autocomplete = "timezone_autocomplete"]
 | 
				
			||||||
    required = false
 | 
					    timezone: Option<String>,
 | 
				
			||||||
)]
 | 
					) -> Result<(), Error> {
 | 
				
			||||||
async fn timezone(ctx: &Context, invoke: &mut CommandInvoke, args: CommandOptions) {
 | 
					    let mut user_data = ctx.author_data().await.unwrap();
 | 
				
			||||||
    let pool = ctx.data.read().await.get::<SQLPool>().cloned().unwrap();
 | 
					 | 
				
			||||||
    let mut user_data = ctx.user_data(invoke.author_id()).await.unwrap();
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let footer_text = format!("Current timezone: {}", user_data.timezone);
 | 
					    let footer_text = format!("Current timezone: {}", user_data.timezone);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if let Some(OptionValue::String(timezone)) = args.get("timezone") {
 | 
					    if let Some(timezone) = timezone {
 | 
				
			||||||
        match timezone.parse::<Tz>() {
 | 
					        match timezone.parse::<Tz>() {
 | 
				
			||||||
            Ok(tz) => {
 | 
					            Ok(tz) => {
 | 
				
			||||||
                user_data.timezone = timezone.clone();
 | 
					                user_data.timezone = timezone.clone();
 | 
				
			||||||
                user_data.commit_changes(&pool).await;
 | 
					                user_data.commit_changes(&ctx.data().database).await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                let now = Utc::now().with_timezone(&tz);
 | 
					                let now = Utc::now().with_timezone(&tz);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                let _ = invoke
 | 
					                ctx.send(|m| {
 | 
				
			||||||
                    .respond(
 | 
					                    m.embed(|e| {
 | 
				
			||||||
                        ctx.http.clone(),
 | 
					 | 
				
			||||||
                        CreateGenericResponse::new().embed(|e| {
 | 
					 | 
				
			||||||
                        e.title("Timezone Set")
 | 
					                        e.title("Timezone Set")
 | 
				
			||||||
                            .description(format!(
 | 
					                            .description(format!(
 | 
				
			||||||
                                "Timezone has been set to **{}**. Your current time should be `{}`",
 | 
					                                "Timezone has been set to **{}**. Your current time should be `{}`",
 | 
				
			||||||
                                timezone,
 | 
					                                timezone,
 | 
				
			||||||
                                    now.format("%H:%M").to_string()
 | 
					                                now.format("%H:%M")
 | 
				
			||||||
                            ))
 | 
					                            ))
 | 
				
			||||||
                            .color(*THEME_COLOR)
 | 
					                            .color(*THEME_COLOR)
 | 
				
			||||||
                        }),
 | 
					                    })
 | 
				
			||||||
                    )
 | 
					                })
 | 
				
			||||||
                    .await;
 | 
					                .await?;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            Err(_) => {
 | 
					            Err(_) => {
 | 
				
			||||||
@@ -56,8 +45,8 @@ async fn timezone(ctx: &Context, invoke: &mut CommandInvoke, args: CommandOption
 | 
				
			|||||||
                    .iter()
 | 
					                    .iter()
 | 
				
			||||||
                    .filter(|tz| {
 | 
					                    .filter(|tz| {
 | 
				
			||||||
                        timezone.contains(&tz.to_string())
 | 
					                        timezone.contains(&tz.to_string())
 | 
				
			||||||
                            || tz.to_string().contains(timezone)
 | 
					                            || tz.to_string().contains(&timezone)
 | 
				
			||||||
                            || levenshtein(&tz.to_string(), timezone) < 4
 | 
					                            || levenshtein(&tz.to_string(), &timezone) < 4
 | 
				
			||||||
                    })
 | 
					                    })
 | 
				
			||||||
                    .take(25)
 | 
					                    .take(25)
 | 
				
			||||||
                    .map(|t| t.to_owned())
 | 
					                    .map(|t| t.to_owned())
 | 
				
			||||||
@@ -66,44 +55,31 @@ async fn timezone(ctx: &Context, invoke: &mut CommandInvoke, args: CommandOption
 | 
				
			|||||||
                let fields = filtered_tz.iter().map(|tz| {
 | 
					                let fields = filtered_tz.iter().map(|tz| {
 | 
				
			||||||
                    (
 | 
					                    (
 | 
				
			||||||
                        tz.to_string(),
 | 
					                        tz.to_string(),
 | 
				
			||||||
                        format!(
 | 
					                        format!("🕗 `{}`", Utc::now().with_timezone(tz).format("%H:%M")),
 | 
				
			||||||
                            "🕗 `{}`",
 | 
					 | 
				
			||||||
                            Utc::now().with_timezone(tz).format("%H:%M").to_string()
 | 
					 | 
				
			||||||
                        ),
 | 
					 | 
				
			||||||
                        true,
 | 
					                        true,
 | 
				
			||||||
                    )
 | 
					                    )
 | 
				
			||||||
                });
 | 
					                });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                let _ = invoke
 | 
					                ctx.send(|m| {
 | 
				
			||||||
                    .respond(
 | 
					                    m.embed(|e| {
 | 
				
			||||||
                        ctx.http.clone(),
 | 
					 | 
				
			||||||
                        CreateGenericResponse::new().embed(|e| {
 | 
					 | 
				
			||||||
                        e.title("Timezone Not Recognized")
 | 
					                        e.title("Timezone Not Recognized")
 | 
				
			||||||
                            .description("Possibly you meant one of the following timezones, otherwise click [here](https://gist.github.com/JellyWX/913dfc8b63d45192ad6cb54c829324ee):")
 | 
					                            .description("Possibly you meant one of the following timezones, otherwise click [here](https://gist.github.com/JellyWX/913dfc8b63d45192ad6cb54c829324ee):")
 | 
				
			||||||
                            .color(*THEME_COLOR)
 | 
					                            .color(*THEME_COLOR)
 | 
				
			||||||
                            .fields(fields)
 | 
					                            .fields(fields)
 | 
				
			||||||
                            .footer(|f| f.text(footer_text))
 | 
					                            .footer(|f| f.text(footer_text))
 | 
				
			||||||
                            .url("https://gist.github.com/JellyWX/913dfc8b63d45192ad6cb54c829324ee")
 | 
					                            .url("https://gist.github.com/JellyWX/913dfc8b63d45192ad6cb54c829324ee")
 | 
				
			||||||
                        }),
 | 
					                    })
 | 
				
			||||||
                    )
 | 
					                })
 | 
				
			||||||
                    .await;
 | 
					                .await?;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
        let popular_timezones = ctx.data.read().await.get::<PopularTimezones>().cloned().unwrap();
 | 
					        let popular_timezones_iter = ctx.data().popular_timezones.iter().map(|t| {
 | 
				
			||||||
 | 
					            (t.to_string(), format!("🕗 `{}`", Utc::now().with_timezone(t).format("%H:%M")), true)
 | 
				
			||||||
        let popular_timezones_iter = popular_timezones.iter().map(|t| {
 | 
					 | 
				
			||||||
            (
 | 
					 | 
				
			||||||
                t.to_string(),
 | 
					 | 
				
			||||||
                format!("🕗 `{}`", Utc::now().with_timezone(t).format("%H:%M").to_string()),
 | 
					 | 
				
			||||||
                true,
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        let _ = invoke
 | 
					        ctx.send(|m| {
 | 
				
			||||||
            .respond(
 | 
					            m.embed(|e| {
 | 
				
			||||||
                ctx.http.clone(),
 | 
					 | 
				
			||||||
                CreateGenericResponse::new().embed(|e| {
 | 
					 | 
				
			||||||
                e.title("Timezone Usage")
 | 
					                e.title("Timezone Usage")
 | 
				
			||||||
                    .description(
 | 
					                    .description(
 | 
				
			||||||
                        "**Usage:**
 | 
					                        "**Usage:**
 | 
				
			||||||
@@ -118,345 +94,162 @@ You may want to use one of the popular timezones below, otherwise click [here](h
 | 
				
			|||||||
                    .fields(popular_timezones_iter)
 | 
					                    .fields(popular_timezones_iter)
 | 
				
			||||||
                    .footer(|f| f.text(footer_text))
 | 
					                    .footer(|f| f.text(footer_text))
 | 
				
			||||||
                    .url("https://gist.github.com/JellyWX/913dfc8b63d45192ad6cb54c829324ee")
 | 
					                    .url("https://gist.github.com/JellyWX/913dfc8b63d45192ad6cb54c829324ee")
 | 
				
			||||||
                }),
 | 
					            })
 | 
				
			||||||
            )
 | 
					        })
 | 
				
			||||||
            .await;
 | 
					        .await?;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Ok(())
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[command("macro")]
 | 
					/// Configure server settings
 | 
				
			||||||
#[description("Record and replay command sequences")]
 | 
					#[poise::command(
 | 
				
			||||||
#[subcommand("record")]
 | 
					    slash_command,
 | 
				
			||||||
#[description("Start recording up to 5 commands to replay")]
 | 
					    rename = "settings",
 | 
				
			||||||
#[arg(name = "name", description = "Name for the new macro", kind = "String", required = true)]
 | 
					    identifying_name = "settings",
 | 
				
			||||||
#[arg(
 | 
					    guild_only = true
 | 
				
			||||||
    name = "description",
 | 
					 | 
				
			||||||
    description = "Description for the new macro",
 | 
					 | 
				
			||||||
    kind = "String",
 | 
					 | 
				
			||||||
    required = false
 | 
					 | 
				
			||||||
)]
 | 
					)]
 | 
				
			||||||
#[subcommand("finish")]
 | 
					pub async fn settings(_ctx: Context<'_>) -> Result<(), Error> {
 | 
				
			||||||
#[description("Finish current recording")]
 | 
					    Ok(())
 | 
				
			||||||
#[subcommand("list")]
 | 
					}
 | 
				
			||||||
#[description("List recorded macros")]
 | 
					 | 
				
			||||||
#[subcommand("run")]
 | 
					 | 
				
			||||||
#[description("Run a recorded macro")]
 | 
					 | 
				
			||||||
#[arg(name = "name", description = "Name of the macro to run", kind = "String", required = true)]
 | 
					 | 
				
			||||||
#[subcommand("delete")]
 | 
					 | 
				
			||||||
#[description("Delete a recorded macro")]
 | 
					 | 
				
			||||||
#[arg(name = "name", description = "Name of the macro to delete", kind = "String", required = true)]
 | 
					 | 
				
			||||||
#[supports_dm(false)]
 | 
					 | 
				
			||||||
#[hook(GUILD_ONLY_HOOK)]
 | 
					 | 
				
			||||||
#[hook(CHECK_GUILD_PERMISSIONS_HOOK)]
 | 
					 | 
				
			||||||
async fn macro_cmd(ctx: &Context, invoke: &mut CommandInvoke, args: CommandOptions) {
 | 
					 | 
				
			||||||
    let pool = ctx.data.read().await.get::<SQLPool>().cloned().unwrap();
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    match args.subcommand.clone().unwrap().as_str() {
 | 
					/// Configure ephemeral setup
 | 
				
			||||||
        "record" => {
 | 
					#[poise::command(
 | 
				
			||||||
            let guild_id = invoke.guild_id().unwrap();
 | 
					    slash_command,
 | 
				
			||||||
 | 
					    rename = "ephemeral",
 | 
				
			||||||
 | 
					    identifying_name = "ephemeral_confirmations",
 | 
				
			||||||
 | 
					    guild_only = true
 | 
				
			||||||
 | 
					)]
 | 
				
			||||||
 | 
					pub async fn ephemeral_confirmations(_ctx: Context<'_>) -> Result<(), Error> {
 | 
				
			||||||
 | 
					    Ok(())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            let name = args.get("name").unwrap().to_string();
 | 
					/// Set reminder confirmations to be sent "ephemerally" (private and cleared automatically)
 | 
				
			||||||
 | 
					#[poise::command(
 | 
				
			||||||
 | 
					    slash_command,
 | 
				
			||||||
 | 
					    rename = "on",
 | 
				
			||||||
 | 
					    identifying_name = "set_ephemeral_confirmations",
 | 
				
			||||||
 | 
					    guild_only = true
 | 
				
			||||||
 | 
					)]
 | 
				
			||||||
 | 
					pub async fn set_ephemeral_confirmations(ctx: Context<'_>) -> Result<(), Error> {
 | 
				
			||||||
 | 
					    let mut guild_data = ctx.guild_data().await.unwrap()?;
 | 
				
			||||||
 | 
					    guild_data.ephemeral_confirmations = true;
 | 
				
			||||||
 | 
					    guild_data.commit_changes(&ctx.data().database).await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            let row = sqlx::query!(
 | 
					    ctx.send(|r| {
 | 
				
			||||||
                "SELECT 1 as _e FROM macro WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?) AND name = ?",
 | 
					        r.ephemeral(true).embed(|e| {
 | 
				
			||||||
                guild_id.0,
 | 
					            e.title("Confirmations ephemeral")
 | 
				
			||||||
                name
 | 
					                .description("Reminder confirmations will be sent privately, and removed when your client restarts.")
 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
            .fetch_one(&pool)
 | 
					 | 
				
			||||||
            .await;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if row.is_ok() {
 | 
					 | 
				
			||||||
                let _ = invoke
 | 
					 | 
				
			||||||
                    .respond(
 | 
					 | 
				
			||||||
                        &ctx,
 | 
					 | 
				
			||||||
                        CreateGenericResponse::new().ephemeral().embed(|e| {
 | 
					 | 
				
			||||||
                            e
 | 
					 | 
				
			||||||
                            .title("Unique Name Required")
 | 
					 | 
				
			||||||
                            .description("A macro already exists under this name. Please select a unique name for your macro.")
 | 
					 | 
				
			||||||
                .color(*THEME_COLOR)
 | 
					                .color(*THEME_COLOR)
 | 
				
			||||||
                        }),
 | 
					        })
 | 
				
			||||||
                    )
 | 
					    })
 | 
				
			||||||
                    .await;
 | 
					    .await?;
 | 
				
			||||||
            } else {
 | 
					 | 
				
			||||||
                let macro_buffer = ctx.data.read().await.get::<RecordingMacros>().cloned().unwrap();
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
                let okay = {
 | 
					    Ok(())
 | 
				
			||||||
                    let mut lock = macro_buffer.write().await;
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    if lock.contains_key(&(guild_id, invoke.author_id())) {
 | 
					/// Set reminder confirmations to persist indefinitely
 | 
				
			||||||
                        false
 | 
					#[poise::command(
 | 
				
			||||||
                    } else {
 | 
					    slash_command,
 | 
				
			||||||
                        lock.insert(
 | 
					    rename = "off",
 | 
				
			||||||
                            (guild_id, invoke.author_id()),
 | 
					    identifying_name = "unset_ephemeral_confirmations",
 | 
				
			||||||
                            CommandMacro {
 | 
					    guild_only = true
 | 
				
			||||||
                                guild_id,
 | 
					)]
 | 
				
			||||||
                                name,
 | 
					pub async fn unset_ephemeral_confirmations(ctx: Context<'_>) -> Result<(), Error> {
 | 
				
			||||||
                                description: args.get("description").map(|d| d.to_string()),
 | 
					    let mut guild_data = ctx.guild_data().await.unwrap()?;
 | 
				
			||||||
                                commands: vec![],
 | 
					    guild_data.ephemeral_confirmations = false;
 | 
				
			||||||
                            },
 | 
					    guild_data.commit_changes(&ctx.data().database).await;
 | 
				
			||||||
                        );
 | 
					 | 
				
			||||||
                        true
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                };
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
                if okay {
 | 
					    ctx.send(|r| {
 | 
				
			||||||
                    let _ = invoke
 | 
					        r.ephemeral(true).embed(|e| {
 | 
				
			||||||
                        .respond(
 | 
					            e.title("Confirmations public")
 | 
				
			||||||
                            &ctx,
 | 
					 | 
				
			||||||
                            CreateGenericResponse::new().ephemeral().embed(|e| {
 | 
					 | 
				
			||||||
                                e
 | 
					 | 
				
			||||||
                                .title("Macro Recording Started")
 | 
					 | 
				
			||||||
                .description(
 | 
					                .description(
 | 
				
			||||||
"Run up to 5 commands, or type `/macro finish` to stop at any point.
 | 
					                    "Reminder confirmations will be sent as regular messages, and won't be removed automatically.",
 | 
				
			||||||
Any commands ran as part of recording will be inconsequential")
 | 
					 | 
				
			||||||
                                .color(*THEME_COLOR)
 | 
					 | 
				
			||||||
                            }),
 | 
					 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
                        .await;
 | 
					                .color(*THEME_COLOR)
 | 
				
			||||||
                } else {
 | 
					        })
 | 
				
			||||||
                    let _ = invoke
 | 
					    })
 | 
				
			||||||
                        .respond(
 | 
					    .await?;
 | 
				
			||||||
                            &ctx,
 | 
					
 | 
				
			||||||
                            CreateGenericResponse::new().ephemeral().embed(|e| {
 | 
					    Ok(())
 | 
				
			||||||
                                e.title("Macro Already Recording")
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Configure whether other users can set reminders to your direct messages
 | 
				
			||||||
 | 
					#[poise::command(slash_command, rename = "dm", identifying_name = "allowed_dm")]
 | 
				
			||||||
 | 
					pub async fn allowed_dm(_ctx: Context<'_>) -> Result<(), Error> {
 | 
				
			||||||
 | 
					    Ok(())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Allow other users to set reminders in your direct messages
 | 
				
			||||||
 | 
					#[poise::command(slash_command, rename = "allow", identifying_name = "set_allowed_dm")]
 | 
				
			||||||
 | 
					pub async fn set_allowed_dm(ctx: Context<'_>) -> Result<(), Error> {
 | 
				
			||||||
 | 
					    let mut user_data = ctx.author_data().await?;
 | 
				
			||||||
 | 
					    user_data.allowed_dm = true;
 | 
				
			||||||
 | 
					    user_data.commit_changes(&ctx.data().database).await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    ctx.send(|r| {
 | 
				
			||||||
 | 
					        r.ephemeral(true).embed(|e| {
 | 
				
			||||||
 | 
					            e.title("DMs permitted")
 | 
				
			||||||
 | 
					                .description("You will receive a message if a user sets a DM reminder for you.")
 | 
				
			||||||
 | 
					                .color(*THEME_COLOR)
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					    .await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Ok(())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Block other users from setting reminders in your direct messages
 | 
				
			||||||
 | 
					#[poise::command(slash_command, rename = "block", identifying_name = "unset_allowed_dm")]
 | 
				
			||||||
 | 
					pub async fn unset_allowed_dm(ctx: Context<'_>) -> Result<(), Error> {
 | 
				
			||||||
 | 
					    let mut user_data = ctx.author_data().await?;
 | 
				
			||||||
 | 
					    user_data.allowed_dm = false;
 | 
				
			||||||
 | 
					    user_data.commit_changes(&ctx.data().database).await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    ctx.send(|r| {
 | 
				
			||||||
 | 
					        r.ephemeral(true).embed(|e| {
 | 
				
			||||||
 | 
					            e.title("DMs blocked")
 | 
				
			||||||
                .description(
 | 
					                .description(
 | 
				
			||||||
                                        "You are already recording a macro in this server.
 | 
					                    "You can still set DM reminders for yourself or for users with DMs enabled.",
 | 
				
			||||||
Please use `/macro finish` to end this recording before starting another.",
 | 
					 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
                .color(*THEME_COLOR)
 | 
					                .color(*THEME_COLOR)
 | 
				
			||||||
                            }),
 | 
					        })
 | 
				
			||||||
                        )
 | 
					    })
 | 
				
			||||||
                        .await;
 | 
					    .await?;
 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        "finish" => {
 | 
					 | 
				
			||||||
            let key = (invoke.guild_id().unwrap(), invoke.author_id());
 | 
					 | 
				
			||||||
            let macro_buffer = ctx.data.read().await.get::<RecordingMacros>().cloned().unwrap();
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
            {
 | 
					    Ok(())
 | 
				
			||||||
                let lock = macro_buffer.read().await;
 | 
					}
 | 
				
			||||||
                let contained = lock.get(&key);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
                if contained.map_or(true, |cmacro| cmacro.commands.is_empty()) {
 | 
					/// View the webhook being used to send reminders to this channel
 | 
				
			||||||
                    let _ = invoke
 | 
					#[poise::command(
 | 
				
			||||||
                        .respond(
 | 
					    slash_command,
 | 
				
			||||||
                            &ctx,
 | 
					    identifying_name = "webhook_url",
 | 
				
			||||||
                            CreateGenericResponse::new().embed(|e| {
 | 
					    required_permissions = "ADMINISTRATOR"
 | 
				
			||||||
                                e.title("No Macro Recorded")
 | 
					)]
 | 
				
			||||||
                                    .description("Use `/macro record` to start recording a macro")
 | 
					pub async fn webhook(ctx: Context<'_>) -> Result<(), Error> {
 | 
				
			||||||
                                    .color(*THEME_COLOR)
 | 
					    match ctx.channel_data().await {
 | 
				
			||||||
                            }),
 | 
					        Ok(data) => {
 | 
				
			||||||
                        )
 | 
					            if let (Some(id), Some(token)) = (data.webhook_id, data.webhook_token) {
 | 
				
			||||||
                        .await;
 | 
					                ctx.send(|b| {
 | 
				
			||||||
 | 
					                    b.ephemeral(true).content(format!(
 | 
				
			||||||
 | 
					                        "**Warning!**
 | 
				
			||||||
 | 
					This link can be used by users to anonymously send messages, with or without permissions.
 | 
				
			||||||
 | 
					Do not share it!
 | 
				
			||||||
 | 
					|| https://discord.com/api/webhooks/{}/{} ||",
 | 
				
			||||||
 | 
					                        id, token,
 | 
				
			||||||
 | 
					                    ))
 | 
				
			||||||
 | 
					                })
 | 
				
			||||||
 | 
					                .await?;
 | 
				
			||||||
            } else {
 | 
					            } else {
 | 
				
			||||||
                    let command_macro = contained.unwrap();
 | 
					                ctx.say("No webhook configured on this channel.").await?;
 | 
				
			||||||
                    let json = serde_json::to_string(&command_macro.commands).unwrap();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    sqlx::query!(
 | 
					 | 
				
			||||||
                        "INSERT INTO macro (guild_id, name, description, commands) VALUES ((SELECT id FROM guilds WHERE guild = ?), ?, ?, ?)",
 | 
					 | 
				
			||||||
                        command_macro.guild_id.0,
 | 
					 | 
				
			||||||
                        command_macro.name,
 | 
					 | 
				
			||||||
                        command_macro.description,
 | 
					 | 
				
			||||||
                        json
 | 
					 | 
				
			||||||
                    )
 | 
					 | 
				
			||||||
                        .execute(&pool)
 | 
					 | 
				
			||||||
                        .await
 | 
					 | 
				
			||||||
                        .unwrap();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    let _ = invoke
 | 
					 | 
				
			||||||
                        .respond(
 | 
					 | 
				
			||||||
                            &ctx,
 | 
					 | 
				
			||||||
                            CreateGenericResponse::new().embed(|e| {
 | 
					 | 
				
			||||||
                                e.title("Macro Recorded")
 | 
					 | 
				
			||||||
                                    .description("Use `/macro run` to execute the macro")
 | 
					 | 
				
			||||||
                                    .color(*THEME_COLOR)
 | 
					 | 
				
			||||||
                            }),
 | 
					 | 
				
			||||||
                        )
 | 
					 | 
				
			||||||
                        .await;
 | 
					 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					 | 
				
			||||||
            {
 | 
					 | 
				
			||||||
                let mut lock = macro_buffer.write().await;
 | 
					 | 
				
			||||||
                lock.remove(&key);
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        "list" => {
 | 
					 | 
				
			||||||
            let macros = CommandMacro::from_guild(ctx, invoke.guild_id().unwrap()).await;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            let resp = show_macro_page(¯os, 0);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            invoke.respond(&ctx, resp).await.unwrap();
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        "run" => {
 | 
					 | 
				
			||||||
            let macro_name = args.get("name").unwrap().to_string();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            match sqlx::query!(
 | 
					 | 
				
			||||||
                "SELECT commands FROM macro WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?) AND name = ?",
 | 
					 | 
				
			||||||
                invoke.guild_id().unwrap().0,
 | 
					 | 
				
			||||||
                macro_name
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
            .fetch_one(&pool)
 | 
					 | 
				
			||||||
            .await
 | 
					 | 
				
			||||||
            {
 | 
					 | 
				
			||||||
                Ok(row) => {
 | 
					 | 
				
			||||||
                    invoke.defer(&ctx).await;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    let commands: Vec<CommandOptions> =
 | 
					 | 
				
			||||||
                        serde_json::from_str(&row.commands).unwrap();
 | 
					 | 
				
			||||||
                    let framework = ctx.data.read().await.get::<RegexFramework>().cloned().unwrap();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    for command in commands {
 | 
					 | 
				
			||||||
                        framework.run_command_from_options(ctx, invoke, command).await;
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                Err(sqlx::Error::RowNotFound) => {
 | 
					 | 
				
			||||||
                    let _ = invoke
 | 
					 | 
				
			||||||
                        .respond(
 | 
					 | 
				
			||||||
                            &ctx,
 | 
					 | 
				
			||||||
                            CreateGenericResponse::new()
 | 
					 | 
				
			||||||
                                .content(format!("Macro \"{}\" not found", macro_name)),
 | 
					 | 
				
			||||||
                        )
 | 
					 | 
				
			||||||
                        .await;
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        Err(e) => {
 | 
					        Err(e) => {
 | 
				
			||||||
                    panic!("{}", e);
 | 
					            warn!("Error fetching channel data: {:?}", e);
 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        "delete" => {
 | 
					 | 
				
			||||||
            let macro_name = args.get("name").unwrap().to_string();
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
            match sqlx::query!(
 | 
					            ctx.say("No webhook configured on this channel.").await?;
 | 
				
			||||||
                "SELECT id FROM macro WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?) AND name = ?",
 | 
					        }
 | 
				
			||||||
                invoke.guild_id().unwrap().0,
 | 
					 | 
				
			||||||
                macro_name
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
            .fetch_one(&pool)
 | 
					 | 
				
			||||||
            .await
 | 
					 | 
				
			||||||
            {
 | 
					 | 
				
			||||||
                Ok(row) => {
 | 
					 | 
				
			||||||
                    sqlx::query!("DELETE FROM macro WHERE id = ?", row.id)
 | 
					 | 
				
			||||||
                        .execute(&pool)
 | 
					 | 
				
			||||||
                        .await
 | 
					 | 
				
			||||||
                        .unwrap();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    let _ = invoke
 | 
					 | 
				
			||||||
                        .respond(
 | 
					 | 
				
			||||||
                            &ctx,
 | 
					 | 
				
			||||||
                            CreateGenericResponse::new()
 | 
					 | 
				
			||||||
                                .content(format!("Macro \"{}\" deleted", macro_name)),
 | 
					 | 
				
			||||||
                        )
 | 
					 | 
				
			||||||
                        .await;
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                Err(sqlx::Error::RowNotFound) => {
 | 
					    Ok(())
 | 
				
			||||||
                    let _ = invoke
 | 
					 | 
				
			||||||
                        .respond(
 | 
					 | 
				
			||||||
                            &ctx,
 | 
					 | 
				
			||||||
                            CreateGenericResponse::new()
 | 
					 | 
				
			||||||
                                .content(format!("Macro \"{}\" not found", macro_name)),
 | 
					 | 
				
			||||||
                        )
 | 
					 | 
				
			||||||
                        .await;
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                Err(e) => {
 | 
					 | 
				
			||||||
                    panic!("{}", e);
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        _ => {}
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
pub fn max_macro_page(macros: &[CommandMacro]) -> usize {
 | 
					 | 
				
			||||||
    let mut skipped_char_count = 0;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    macros
 | 
					 | 
				
			||||||
        .iter()
 | 
					 | 
				
			||||||
        .map(|m| {
 | 
					 | 
				
			||||||
            if let Some(description) = &m.description {
 | 
					 | 
				
			||||||
                format!("**{}**\n- *{}*\n- Has {} commands", m.name, description, m.commands.len())
 | 
					 | 
				
			||||||
            } else {
 | 
					 | 
				
			||||||
                format!("**{}**\n- Has {} commands", m.name, m.commands.len())
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        })
 | 
					 | 
				
			||||||
        .fold(1, |mut pages, p| {
 | 
					 | 
				
			||||||
            skipped_char_count += p.len();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if skipped_char_count > EMBED_DESCRIPTION_MAX_LENGTH {
 | 
					 | 
				
			||||||
                skipped_char_count = p.len();
 | 
					 | 
				
			||||||
                pages += 1;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            pages
 | 
					 | 
				
			||||||
        })
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
pub fn show_macro_page(macros: &[CommandMacro], page: usize) -> CreateGenericResponse {
 | 
					 | 
				
			||||||
    let pager = MacroPager::new(page);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if macros.is_empty() {
 | 
					 | 
				
			||||||
        return CreateGenericResponse::new().embed(|e| {
 | 
					 | 
				
			||||||
            e.title("Macros")
 | 
					 | 
				
			||||||
                .description("No Macros Set Up. Use `/macro record` to get started.")
 | 
					 | 
				
			||||||
                .color(*THEME_COLOR)
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let pages = max_macro_page(macros);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let mut page = page;
 | 
					 | 
				
			||||||
    if page >= pages {
 | 
					 | 
				
			||||||
        page = pages - 1;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let mut char_count = 0;
 | 
					 | 
				
			||||||
    let mut skipped_char_count = 0;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let mut skipped_pages = 0;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let display_vec: Vec<String> = macros
 | 
					 | 
				
			||||||
        .iter()
 | 
					 | 
				
			||||||
        .map(|m| {
 | 
					 | 
				
			||||||
            if let Some(description) = &m.description {
 | 
					 | 
				
			||||||
                format!("**{}**\n- *{}*\n- Has {} commands", m.name, description, m.commands.len())
 | 
					 | 
				
			||||||
            } else {
 | 
					 | 
				
			||||||
                format!("**{}**\n- Has {} commands", m.name, m.commands.len())
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        })
 | 
					 | 
				
			||||||
        .skip_while(|p| {
 | 
					 | 
				
			||||||
            skipped_char_count += p.len();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if skipped_char_count > EMBED_DESCRIPTION_MAX_LENGTH {
 | 
					 | 
				
			||||||
                skipped_char_count = p.len();
 | 
					 | 
				
			||||||
                skipped_pages += 1;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            skipped_pages < page
 | 
					 | 
				
			||||||
        })
 | 
					 | 
				
			||||||
        .take_while(|p| {
 | 
					 | 
				
			||||||
            char_count += p.len();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            char_count < EMBED_DESCRIPTION_MAX_LENGTH
 | 
					 | 
				
			||||||
        })
 | 
					 | 
				
			||||||
        .collect::<Vec<String>>();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let display = display_vec.join("\n");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    CreateGenericResponse::new()
 | 
					 | 
				
			||||||
        .embed(|e| {
 | 
					 | 
				
			||||||
            e.title("Macros")
 | 
					 | 
				
			||||||
                .description(display)
 | 
					 | 
				
			||||||
                .footer(|f| f.text(format!("Page {} of {}", page + 1, pages)))
 | 
					 | 
				
			||||||
                .color(*THEME_COLOR)
 | 
					 | 
				
			||||||
        })
 | 
					 | 
				
			||||||
        .components(|comp| {
 | 
					 | 
				
			||||||
            pager.create_button_row(pages, comp);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            comp
 | 
					 | 
				
			||||||
        })
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -1,5 +1,4 @@
 | 
				
			|||||||
use regex_command_attr::command;
 | 
					use poise::CreateReply;
 | 
				
			||||||
use serenity::client::Context;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::{
 | 
					use crate::{
 | 
				
			||||||
    component_models::{
 | 
					    component_models::{
 | 
				
			||||||
@@ -7,134 +6,222 @@ use crate::{
 | 
				
			|||||||
        ComponentDataModel, TodoSelector,
 | 
					        ComponentDataModel, TodoSelector,
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    consts::{EMBED_DESCRIPTION_MAX_LENGTH, SELECT_MAX_ENTRIES, THEME_COLOR},
 | 
					    consts::{EMBED_DESCRIPTION_MAX_LENGTH, SELECT_MAX_ENTRIES, THEME_COLOR},
 | 
				
			||||||
    framework::{CommandInvoke, CommandOptions, CreateGenericResponse},
 | 
					    models::CtxData,
 | 
				
			||||||
    hooks::CHECK_GUILD_PERMISSIONS_HOOK,
 | 
					    Context, Error,
 | 
				
			||||||
    SQLPool,
 | 
					 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[command]
 | 
					/// Manage todo lists
 | 
				
			||||||
#[description("Manage todo lists")]
 | 
					#[poise::command(
 | 
				
			||||||
#[subcommandgroup("server")]
 | 
					    slash_command,
 | 
				
			||||||
#[description("Manage the server todo list")]
 | 
					    rename = "todo",
 | 
				
			||||||
#[subcommand("add")]
 | 
					    identifying_name = "todo_base",
 | 
				
			||||||
#[description("Add an item to the server todo list")]
 | 
					    default_member_permissions = "MANAGE_GUILD"
 | 
				
			||||||
#[arg(
 | 
					 | 
				
			||||||
    name = "task",
 | 
					 | 
				
			||||||
    description = "The task to add to the todo list",
 | 
					 | 
				
			||||||
    kind = "String",
 | 
					 | 
				
			||||||
    required = true
 | 
					 | 
				
			||||||
)]
 | 
					)]
 | 
				
			||||||
#[subcommand("view")]
 | 
					pub async fn todo_base(_ctx: Context<'_>) -> Result<(), Error> {
 | 
				
			||||||
#[description("View and remove from the server todo list")]
 | 
					    Ok(())
 | 
				
			||||||
#[subcommandgroup("channel")]
 | 
					}
 | 
				
			||||||
#[description("Manage the channel todo list")]
 | 
					
 | 
				
			||||||
#[subcommand("add")]
 | 
					/// Manage the server todo list
 | 
				
			||||||
#[description("Add to the channel todo list")]
 | 
					#[poise::command(
 | 
				
			||||||
#[arg(
 | 
					    slash_command,
 | 
				
			||||||
    name = "task",
 | 
					    rename = "server",
 | 
				
			||||||
    description = "The task to add to the todo list",
 | 
					    guild_only = true,
 | 
				
			||||||
    kind = "String",
 | 
					    identifying_name = "todo_guild_base",
 | 
				
			||||||
    required = true
 | 
					    default_member_permissions = "MANAGE_GUILD"
 | 
				
			||||||
)]
 | 
					)]
 | 
				
			||||||
#[subcommand("view")]
 | 
					pub async fn todo_guild_base(_ctx: Context<'_>) -> Result<(), Error> {
 | 
				
			||||||
#[description("View and remove from the channel todo list")]
 | 
					    Ok(())
 | 
				
			||||||
#[subcommandgroup("user")]
 | 
					}
 | 
				
			||||||
#[description("Manage your personal todo list")]
 | 
					
 | 
				
			||||||
#[subcommand("add")]
 | 
					/// Add an item to the server todo list
 | 
				
			||||||
#[description("Add to your personal todo list")]
 | 
					#[poise::command(
 | 
				
			||||||
#[arg(
 | 
					    slash_command,
 | 
				
			||||||
    name = "task",
 | 
					    rename = "add",
 | 
				
			||||||
    description = "The task to add to the todo list",
 | 
					    guild_only = true,
 | 
				
			||||||
    kind = "String",
 | 
					    identifying_name = "todo_guild_add",
 | 
				
			||||||
    required = true
 | 
					    default_member_permissions = "MANAGE_GUILD"
 | 
				
			||||||
)]
 | 
					)]
 | 
				
			||||||
#[subcommand("view")]
 | 
					pub async fn todo_guild_add(
 | 
				
			||||||
#[description("View and remove from your personal todo list")]
 | 
					    ctx: Context<'_>,
 | 
				
			||||||
#[hook(CHECK_GUILD_PERMISSIONS_HOOK)]
 | 
					    #[description = "The task to add to the todo list"] task: String,
 | 
				
			||||||
async fn todo(ctx: &Context, invoke: &mut CommandInvoke, args: CommandOptions) {
 | 
					) -> Result<(), Error> {
 | 
				
			||||||
    if invoke.guild_id().is_none() && args.subcommand_group != Some("user".to_string()) {
 | 
					 | 
				
			||||||
        let _ = invoke
 | 
					 | 
				
			||||||
            .respond(
 | 
					 | 
				
			||||||
                &ctx,
 | 
					 | 
				
			||||||
                CreateGenericResponse::new().content("Please use `/todo user` in direct messages"),
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
            .await;
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
        let pool = ctx.data.read().await.get::<SQLPool>().cloned().unwrap();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        let keys = match args.subcommand_group.as_ref().unwrap().as_str() {
 | 
					 | 
				
			||||||
            "server" => (None, None, invoke.guild_id().map(|g| g.0)),
 | 
					 | 
				
			||||||
            "channel" => (None, Some(invoke.channel_id().0), invoke.guild_id().map(|g| g.0)),
 | 
					 | 
				
			||||||
            _ => (Some(invoke.author_id().0), None, None),
 | 
					 | 
				
			||||||
        };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        match args.get("task") {
 | 
					 | 
				
			||||||
            Some(task) => {
 | 
					 | 
				
			||||||
                let task = task.to_string();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    sqlx::query!(
 | 
					    sqlx::query!(
 | 
				
			||||||
                    "INSERT INTO todos (user_id, channel_id, guild_id, value) VALUES ((SELECT id FROM users WHERE user = ?), (SELECT id FROM channels WHERE channel = ?), (SELECT id FROM guilds WHERE guild = ?), ?)",
 | 
					        "INSERT INTO todos (guild_id, value)
 | 
				
			||||||
                    keys.0,
 | 
					VALUES ((SELECT id FROM guilds WHERE guild = ?), ?)",
 | 
				
			||||||
                    keys.1,
 | 
					        ctx.guild_id().unwrap().0,
 | 
				
			||||||
                    keys.2,
 | 
					 | 
				
			||||||
        task
 | 
					        task
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
                .execute(&pool)
 | 
					    .execute(&ctx.data().database)
 | 
				
			||||||
    .await
 | 
					    .await
 | 
				
			||||||
    .unwrap();
 | 
					    .unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                let _ = invoke
 | 
					    ctx.say("Item added to todo list").await?;
 | 
				
			||||||
                    .respond(&ctx, CreateGenericResponse::new().content("Item added to todo list"))
 | 
					
 | 
				
			||||||
                    .await;
 | 
					    Ok(())
 | 
				
			||||||
            }
 | 
					}
 | 
				
			||||||
            None => {
 | 
					
 | 
				
			||||||
                let values = if let Some(uid) = keys.0 {
 | 
					/// View and remove from the server todo list
 | 
				
			||||||
                    sqlx::query!(
 | 
					#[poise::command(
 | 
				
			||||||
                        "SELECT todos.id, value FROM todos
 | 
					    slash_command,
 | 
				
			||||||
INNER JOIN users ON todos.user_id = users.id
 | 
					    rename = "view",
 | 
				
			||||||
WHERE users.user = ?",
 | 
					    guild_only = true,
 | 
				
			||||||
                        uid,
 | 
					    identifying_name = "todo_guild_view",
 | 
				
			||||||
                    )
 | 
					    default_member_permissions = "MANAGE_GUILD"
 | 
				
			||||||
                    .fetch_all(&pool)
 | 
					)]
 | 
				
			||||||
                    .await
 | 
					pub async fn todo_guild_view(ctx: Context<'_>) -> Result<(), Error> {
 | 
				
			||||||
                    .unwrap()
 | 
					    let values = sqlx::query!(
 | 
				
			||||||
                    .iter()
 | 
					 | 
				
			||||||
                    .map(|row| (row.id as usize, row.value.clone()))
 | 
					 | 
				
			||||||
                    .collect::<Vec<(usize, String)>>()
 | 
					 | 
				
			||||||
                } else if let Some(cid) = keys.1 {
 | 
					 | 
				
			||||||
                    sqlx::query!(
 | 
					 | 
				
			||||||
                        "SELECT todos.id, value FROM todos
 | 
					 | 
				
			||||||
INNER JOIN channels ON todos.channel_id = channels.id
 | 
					 | 
				
			||||||
WHERE channels.channel = ?",
 | 
					 | 
				
			||||||
                        cid,
 | 
					 | 
				
			||||||
                    )
 | 
					 | 
				
			||||||
                    .fetch_all(&pool)
 | 
					 | 
				
			||||||
                    .await
 | 
					 | 
				
			||||||
                    .unwrap()
 | 
					 | 
				
			||||||
                    .iter()
 | 
					 | 
				
			||||||
                    .map(|row| (row.id as usize, row.value.clone()))
 | 
					 | 
				
			||||||
                    .collect::<Vec<(usize, String)>>()
 | 
					 | 
				
			||||||
                } else {
 | 
					 | 
				
			||||||
                    sqlx::query!(
 | 
					 | 
				
			||||||
        "SELECT todos.id, value FROM todos
 | 
					        "SELECT todos.id, value FROM todos
 | 
				
			||||||
INNER JOIN guilds ON todos.guild_id = guilds.id
 | 
					INNER JOIN guilds ON todos.guild_id = guilds.id
 | 
				
			||||||
WHERE guilds.guild = ?",
 | 
					WHERE guilds.guild = ?",
 | 
				
			||||||
                        keys.2,
 | 
					        ctx.guild_id().unwrap().0,
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
                    .fetch_all(&pool)
 | 
					    .fetch_all(&ctx.data().database)
 | 
				
			||||||
    .await
 | 
					    .await
 | 
				
			||||||
    .unwrap()
 | 
					    .unwrap()
 | 
				
			||||||
    .iter()
 | 
					    .iter()
 | 
				
			||||||
    .map(|row| (row.id as usize, row.value.clone()))
 | 
					    .map(|row| (row.id as usize, row.value.clone()))
 | 
				
			||||||
                    .collect::<Vec<(usize, String)>>()
 | 
					    .collect::<Vec<(usize, String)>>();
 | 
				
			||||||
                };
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
                let resp = show_todo_page(&values, 0, keys.0, keys.1, keys.2);
 | 
					    let resp = show_todo_page(&values, 0, None, None, ctx.guild_id().map(|g| g.0));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                invoke.respond(&ctx, resp).await.unwrap();
 | 
					    ctx.send(|r| {
 | 
				
			||||||
            }
 | 
					        *r = resp;
 | 
				
			||||||
        }
 | 
					        r
 | 
				
			||||||
    }
 | 
					    })
 | 
				
			||||||
 | 
					    .await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Ok(())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Manage the channel todo list
 | 
				
			||||||
 | 
					#[poise::command(
 | 
				
			||||||
 | 
					    slash_command,
 | 
				
			||||||
 | 
					    rename = "channel",
 | 
				
			||||||
 | 
					    guild_only = true,
 | 
				
			||||||
 | 
					    identifying_name = "todo_channel_base",
 | 
				
			||||||
 | 
					    default_member_permissions = "MANAGE_GUILD"
 | 
				
			||||||
 | 
					)]
 | 
				
			||||||
 | 
					pub async fn todo_channel_base(_ctx: Context<'_>) -> Result<(), Error> {
 | 
				
			||||||
 | 
					    Ok(())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Add an item to the channel todo list
 | 
				
			||||||
 | 
					#[poise::command(
 | 
				
			||||||
 | 
					    slash_command,
 | 
				
			||||||
 | 
					    rename = "add",
 | 
				
			||||||
 | 
					    guild_only = true,
 | 
				
			||||||
 | 
					    identifying_name = "todo_channel_add",
 | 
				
			||||||
 | 
					    default_member_permissions = "MANAGE_GUILD"
 | 
				
			||||||
 | 
					)]
 | 
				
			||||||
 | 
					pub async fn todo_channel_add(
 | 
				
			||||||
 | 
					    ctx: Context<'_>,
 | 
				
			||||||
 | 
					    #[description = "The task to add to the todo list"] task: String,
 | 
				
			||||||
 | 
					) -> Result<(), Error> {
 | 
				
			||||||
 | 
					    // ensure channel is cached
 | 
				
			||||||
 | 
					    let _ = ctx.channel_data().await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    sqlx::query!(
 | 
				
			||||||
 | 
					        "INSERT INTO todos (guild_id, channel_id, value)
 | 
				
			||||||
 | 
					VALUES ((SELECT id FROM guilds WHERE guild = ?), (SELECT id FROM channels WHERE channel = ?), ?)",
 | 
				
			||||||
 | 
					        ctx.guild_id().unwrap().0,
 | 
				
			||||||
 | 
					        ctx.channel_id().0,
 | 
				
			||||||
 | 
					        task
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    .execute(&ctx.data().database)
 | 
				
			||||||
 | 
					    .await
 | 
				
			||||||
 | 
					    .unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    ctx.say("Item added to todo list").await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Ok(())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// View and remove from the channel todo list
 | 
				
			||||||
 | 
					#[poise::command(
 | 
				
			||||||
 | 
					    slash_command,
 | 
				
			||||||
 | 
					    rename = "view",
 | 
				
			||||||
 | 
					    guild_only = true,
 | 
				
			||||||
 | 
					    identifying_name = "todo_channel_view",
 | 
				
			||||||
 | 
					    default_member_permissions = "MANAGE_GUILD"
 | 
				
			||||||
 | 
					)]
 | 
				
			||||||
 | 
					pub async fn todo_channel_view(ctx: Context<'_>) -> Result<(), Error> {
 | 
				
			||||||
 | 
					    let values = sqlx::query!(
 | 
				
			||||||
 | 
					        "SELECT todos.id, value FROM todos
 | 
				
			||||||
 | 
					INNER JOIN channels ON todos.channel_id = channels.id
 | 
				
			||||||
 | 
					WHERE channels.channel = ?",
 | 
				
			||||||
 | 
					        ctx.channel_id().0,
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    .fetch_all(&ctx.data().database)
 | 
				
			||||||
 | 
					    .await
 | 
				
			||||||
 | 
					    .unwrap()
 | 
				
			||||||
 | 
					    .iter()
 | 
				
			||||||
 | 
					    .map(|row| (row.id as usize, row.value.clone()))
 | 
				
			||||||
 | 
					    .collect::<Vec<(usize, String)>>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let resp =
 | 
				
			||||||
 | 
					        show_todo_page(&values, 0, None, Some(ctx.channel_id().0), ctx.guild_id().map(|g| g.0));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    ctx.send(|r| {
 | 
				
			||||||
 | 
					        *r = resp;
 | 
				
			||||||
 | 
					        r
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					    .await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Ok(())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Manage your personal todo list
 | 
				
			||||||
 | 
					#[poise::command(slash_command, rename = "user", identifying_name = "todo_user_base")]
 | 
				
			||||||
 | 
					pub async fn todo_user_base(_ctx: Context<'_>) -> Result<(), Error> {
 | 
				
			||||||
 | 
					    Ok(())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Add an item to your personal todo list
 | 
				
			||||||
 | 
					#[poise::command(slash_command, rename = "add", identifying_name = "todo_user_add")]
 | 
				
			||||||
 | 
					pub async fn todo_user_add(
 | 
				
			||||||
 | 
					    ctx: Context<'_>,
 | 
				
			||||||
 | 
					    #[description = "The task to add to the todo list"] task: String,
 | 
				
			||||||
 | 
					) -> Result<(), Error> {
 | 
				
			||||||
 | 
					    sqlx::query!(
 | 
				
			||||||
 | 
					        "INSERT INTO todos (user_id, value)
 | 
				
			||||||
 | 
					VALUES ((SELECT id FROM users WHERE user = ?), ?)",
 | 
				
			||||||
 | 
					        ctx.author().id.0,
 | 
				
			||||||
 | 
					        task
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    .execute(&ctx.data().database)
 | 
				
			||||||
 | 
					    .await
 | 
				
			||||||
 | 
					    .unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    ctx.say("Item added to todo list").await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Ok(())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// View and remove from your personal todo list
 | 
				
			||||||
 | 
					#[poise::command(slash_command, rename = "view", identifying_name = "todo_user_view")]
 | 
				
			||||||
 | 
					pub async fn todo_user_view(ctx: Context<'_>) -> Result<(), Error> {
 | 
				
			||||||
 | 
					    let values = sqlx::query!(
 | 
				
			||||||
 | 
					        "SELECT todos.id, value FROM todos
 | 
				
			||||||
 | 
					INNER JOIN users ON todos.user_id = users.id
 | 
				
			||||||
 | 
					WHERE users.user = ?",
 | 
				
			||||||
 | 
					        ctx.author().id.0,
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    .fetch_all(&ctx.data().database)
 | 
				
			||||||
 | 
					    .await
 | 
				
			||||||
 | 
					    .unwrap()
 | 
				
			||||||
 | 
					    .iter()
 | 
				
			||||||
 | 
					    .map(|row| (row.id as usize, row.value.clone()))
 | 
				
			||||||
 | 
					    .collect::<Vec<(usize, String)>>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let resp = show_todo_page(&values, 0, Some(ctx.author().id.0), None, None);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    ctx.send(|r| {
 | 
				
			||||||
 | 
					        *r = resp;
 | 
				
			||||||
 | 
					        r
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					    .await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Ok(())
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub fn max_todo_page(todo_values: &[(usize, String)]) -> usize {
 | 
					pub fn max_todo_page(todo_values: &[(usize, String)]) -> usize {
 | 
				
			||||||
@@ -164,7 +251,7 @@ pub fn show_todo_page(
 | 
				
			|||||||
    user_id: Option<u64>,
 | 
					    user_id: Option<u64>,
 | 
				
			||||||
    channel_id: Option<u64>,
 | 
					    channel_id: Option<u64>,
 | 
				
			||||||
    guild_id: Option<u64>,
 | 
					    guild_id: Option<u64>,
 | 
				
			||||||
) -> CreateGenericResponse {
 | 
					) -> CreateReply {
 | 
				
			||||||
    let pager = TodoPager::new(page, user_id, channel_id, guild_id);
 | 
					    let pager = TodoPager::new(page, user_id, channel_id, guild_id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let pages = max_todo_page(todo_values);
 | 
					    let pages = max_todo_page(todo_values);
 | 
				
			||||||
@@ -219,17 +306,23 @@ pub fn show_todo_page(
 | 
				
			|||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if todo_ids.is_empty() {
 | 
					    if todo_ids.is_empty() {
 | 
				
			||||||
        CreateGenericResponse::new().embed(|e| {
 | 
					        let mut reply = CreateReply::default();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        reply.embed(|e| {
 | 
				
			||||||
            e.title(format!("{} Todo List", title))
 | 
					            e.title(format!("{} Todo List", title))
 | 
				
			||||||
                .description("Todo List Empty!")
 | 
					                .description("Todo List Empty!")
 | 
				
			||||||
                .footer(|f| f.text(format!("Page {} of {}", page + 1, pages)))
 | 
					                .footer(|f| f.text(format!("Page {} of {}", page + 1, pages)))
 | 
				
			||||||
                .color(*THEME_COLOR)
 | 
					                .color(*THEME_COLOR)
 | 
				
			||||||
        })
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        reply
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
        let todo_selector =
 | 
					        let todo_selector =
 | 
				
			||||||
            ComponentDataModel::TodoSelector(TodoSelector { page, user_id, channel_id, guild_id });
 | 
					            ComponentDataModel::TodoSelector(TodoSelector { page, user_id, channel_id, guild_id });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        CreateGenericResponse::new()
 | 
					        let mut reply = CreateReply::default();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        reply
 | 
				
			||||||
            .embed(|e| {
 | 
					            .embed(|e| {
 | 
				
			||||||
                e.title(format!("{} Todo List", title))
 | 
					                e.title(format!("{} Todo List", title))
 | 
				
			||||||
                    .description(display)
 | 
					                    .description(display)
 | 
				
			||||||
@@ -247,7 +340,18 @@ pub fn show_todo_page(
 | 
				
			|||||||
                                opt.create_option(|o| {
 | 
					                                opt.create_option(|o| {
 | 
				
			||||||
                                    o.label(format!("Mark {} complete", count + first_num))
 | 
					                                    o.label(format!("Mark {} complete", count + first_num))
 | 
				
			||||||
                                        .value(id)
 | 
					                                        .value(id)
 | 
				
			||||||
                                        .description(disp.split_once(" ").unwrap_or(("", "")).1)
 | 
					                                        .description({
 | 
				
			||||||
 | 
					                                            let c = disp.split_once(' ').unwrap_or(("", "")).1;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                                            if c.len() > 100 {
 | 
				
			||||||
 | 
					                                                format!(
 | 
				
			||||||
 | 
					                                                    "{}...",
 | 
				
			||||||
 | 
					                                                    c.chars().take(97).collect::<String>()
 | 
				
			||||||
 | 
					                                                )
 | 
				
			||||||
 | 
					                                            } else {
 | 
				
			||||||
 | 
					                                                c.to_string()
 | 
				
			||||||
 | 
					                                            }
 | 
				
			||||||
 | 
					                                        })
 | 
				
			||||||
                                });
 | 
					                                });
 | 
				
			||||||
                            }
 | 
					                            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -255,6 +359,8 @@ pub fn show_todo_page(
 | 
				
			|||||||
                        })
 | 
					                        })
 | 
				
			||||||
                    })
 | 
					                    })
 | 
				
			||||||
                })
 | 
					                })
 | 
				
			||||||
            })
 | 
					            });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        reply
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,31 +2,37 @@ pub(crate) mod pager;
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
use std::io::Cursor;
 | 
					use std::io::Cursor;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use base64::{engine::general_purpose, Engine};
 | 
				
			||||||
use chrono_tz::Tz;
 | 
					use chrono_tz::Tz;
 | 
				
			||||||
use num_integer::Integer;
 | 
					use log::warn;
 | 
				
			||||||
use rmp_serde::Serializer;
 | 
					use poise::{
 | 
				
			||||||
use serde::{Deserialize, Serialize};
 | 
					    serenity_prelude as serenity,
 | 
				
			||||||
use serenity::{
 | 
					    serenity_prelude::{
 | 
				
			||||||
        builder::CreateEmbed,
 | 
					        builder::CreateEmbed,
 | 
				
			||||||
    client::Context,
 | 
					 | 
				
			||||||
        model::{
 | 
					        model::{
 | 
				
			||||||
 | 
					            application::interaction::{
 | 
				
			||||||
 | 
					                message_component::MessageComponentInteraction, InteractionResponseType,
 | 
				
			||||||
 | 
					                MessageFlags,
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
            channel::Channel,
 | 
					            channel::Channel,
 | 
				
			||||||
        interactions::{message_component::MessageComponentInteraction, InteractionResponseType},
 | 
					        },
 | 
				
			||||||
        prelude::InteractionApplicationCommandCallbackDataFlags,
 | 
					        Context,
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					use rmp_serde::Serializer;
 | 
				
			||||||
 | 
					use serde::{Deserialize, Serialize};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::{
 | 
					use crate::{
 | 
				
			||||||
    commands::{
 | 
					    commands::{
 | 
				
			||||||
        moderation_cmds::{max_macro_page, show_macro_page},
 | 
					        command_macro::list::{max_macro_page, show_macro_page},
 | 
				
			||||||
        reminder_cmds::{max_delete_page, show_delete_page},
 | 
					        reminder_cmds::{max_delete_page, show_delete_page},
 | 
				
			||||||
        todo_cmds::{max_todo_page, show_todo_page},
 | 
					        todo_cmds::{max_todo_page, show_todo_page},
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    component_models::pager::{DelPager, LookPager, MacroPager, Pager, TodoPager},
 | 
					    component_models::pager::{DelPager, LookPager, MacroPager, Pager, TodoPager},
 | 
				
			||||||
    consts::{EMBED_DESCRIPTION_MAX_LENGTH, THEME_COLOR},
 | 
					    consts::{EMBED_DESCRIPTION_MAX_LENGTH, THEME_COLOR},
 | 
				
			||||||
    framework::CommandInvoke,
 | 
					    models::reminder::Reminder,
 | 
				
			||||||
    models::{command_macro::CommandMacro, reminder::Reminder},
 | 
					    utils::send_as_initial_response,
 | 
				
			||||||
    SQLPool,
 | 
					    Data,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[derive(Deserialize, Serialize)]
 | 
					#[derive(Deserialize, Serialize)]
 | 
				
			||||||
@@ -39,24 +45,26 @@ pub enum ComponentDataModel {
 | 
				
			|||||||
    DelSelector(DelSelector),
 | 
					    DelSelector(DelSelector),
 | 
				
			||||||
    TodoSelector(TodoSelector),
 | 
					    TodoSelector(TodoSelector),
 | 
				
			||||||
    MacroPager(MacroPager),
 | 
					    MacroPager(MacroPager),
 | 
				
			||||||
 | 
					    UndoReminder(UndoReminder),
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
impl ComponentDataModel {
 | 
					impl ComponentDataModel {
 | 
				
			||||||
    pub fn to_custom_id(&self) -> String {
 | 
					    pub fn to_custom_id(&self) -> String {
 | 
				
			||||||
        let mut buf = Vec::new();
 | 
					        let mut buf = Vec::new();
 | 
				
			||||||
        self.serialize(&mut Serializer::new(&mut buf)).unwrap();
 | 
					        self.serialize(&mut Serializer::new(&mut buf)).unwrap();
 | 
				
			||||||
        base64::encode(buf)
 | 
					        general_purpose::STANDARD.encode(buf)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    pub fn from_custom_id(data: &String) -> Self {
 | 
					    pub fn from_custom_id(data: &String) -> Self {
 | 
				
			||||||
        let buf = base64::decode(data)
 | 
					        let buf = general_purpose::STANDARD
 | 
				
			||||||
 | 
					            .decode(data)
 | 
				
			||||||
            .map_err(|e| format!("Could not decode `custom_id' {}: {:?}", data, e))
 | 
					            .map_err(|e| format!("Could not decode `custom_id' {}: {:?}", data, e))
 | 
				
			||||||
            .unwrap();
 | 
					            .unwrap();
 | 
				
			||||||
        let cur = Cursor::new(buf);
 | 
					        let cur = Cursor::new(buf);
 | 
				
			||||||
        rmp_serde::from_read(cur).unwrap()
 | 
					        rmp_serde::from_read(cur).unwrap()
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    pub async fn act(&self, ctx: &Context, component: MessageComponentInteraction) {
 | 
					    pub async fn act(&self, ctx: &Context, data: &Data, component: &MessageComponentInteraction) {
 | 
				
			||||||
        match self {
 | 
					        match self {
 | 
				
			||||||
            ComponentDataModel::LookPager(pager) => {
 | 
					            ComponentDataModel::LookPager(pager) => {
 | 
				
			||||||
                let flags = pager.flags;
 | 
					                let flags = pager.flags;
 | 
				
			||||||
@@ -73,13 +81,13 @@ impl ComponentDataModel {
 | 
				
			|||||||
                    component.channel_id
 | 
					                    component.channel_id
 | 
				
			||||||
                };
 | 
					                };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                let reminders = Reminder::from_channel(ctx, channel_id, &flags).await;
 | 
					                let reminders = Reminder::from_channel(&data.database, channel_id, &flags).await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                let pages = reminders
 | 
					                let pages = reminders
 | 
				
			||||||
                    .iter()
 | 
					                    .iter()
 | 
				
			||||||
                    .map(|reminder| reminder.display(&flags, &pager.timezone))
 | 
					                    .map(|reminder| reminder.display(&flags, &pager.timezone))
 | 
				
			||||||
                    .fold(0, |t, r| t + r.len())
 | 
					                    .fold(0, |t, r| t + r.len())
 | 
				
			||||||
                    .div_ceil(&EMBED_DESCRIPTION_MAX_LENGTH);
 | 
					                    .div_ceil(EMBED_DESCRIPTION_MAX_LENGTH);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                let channel_name =
 | 
					                let channel_name =
 | 
				
			||||||
                    if let Some(Channel::Guild(channel)) = channel_id.to_channel_cached(&ctx) {
 | 
					                    if let Some(Channel::Guild(channel)) = channel_id.to_channel_cached(&ctx) {
 | 
				
			||||||
@@ -107,7 +115,7 @@ impl ComponentDataModel {
 | 
				
			|||||||
                        char_count < EMBED_DESCRIPTION_MAX_LENGTH
 | 
					                        char_count < EMBED_DESCRIPTION_MAX_LENGTH
 | 
				
			||||||
                    })
 | 
					                    })
 | 
				
			||||||
                    .collect::<Vec<String>>()
 | 
					                    .collect::<Vec<String>>()
 | 
				
			||||||
                    .join("\n");
 | 
					                    .join("");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                let mut embed = CreateEmbed::default();
 | 
					                let mut embed = CreateEmbed::default();
 | 
				
			||||||
                embed
 | 
					                embed
 | 
				
			||||||
@@ -123,7 +131,7 @@ impl ComponentDataModel {
 | 
				
			|||||||
                    .create_interaction_response(&ctx, |r| {
 | 
					                    .create_interaction_response(&ctx, |r| {
 | 
				
			||||||
                        r.kind(InteractionResponseType::UpdateMessage).interaction_response_data(
 | 
					                        r.kind(InteractionResponseType::UpdateMessage).interaction_response_data(
 | 
				
			||||||
                            |response| {
 | 
					                            |response| {
 | 
				
			||||||
                                response.embeds(vec![embed]).components(|comp| {
 | 
					                                response.set_embeds(vec![embed]).components(|comp| {
 | 
				
			||||||
                                    pager.create_button_row(pages, comp);
 | 
					                                    pager.create_button_row(pages, comp);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                                    comp
 | 
					                                    comp
 | 
				
			||||||
@@ -134,45 +142,71 @@ impl ComponentDataModel {
 | 
				
			|||||||
                    .await;
 | 
					                    .await;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            ComponentDataModel::DelPager(pager) => {
 | 
					            ComponentDataModel::DelPager(pager) => {
 | 
				
			||||||
                let reminders =
 | 
					                let reminders = Reminder::from_guild(
 | 
				
			||||||
                    Reminder::from_guild(ctx, component.guild_id, component.user.id).await;
 | 
					                    &ctx,
 | 
				
			||||||
 | 
					                    &data.database,
 | 
				
			||||||
 | 
					                    component.guild_id,
 | 
				
			||||||
 | 
					                    component.user.id,
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					                .await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                let max_pages = max_delete_page(&reminders, &pager.timezone);
 | 
					                let max_pages = max_delete_page(&reminders, &pager.timezone);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                let resp = show_delete_page(&reminders, pager.next_page(max_pages), pager.timezone);
 | 
					                let resp = show_delete_page(&reminders, pager.next_page(max_pages), pager.timezone);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                let mut invoke = CommandInvoke::component(component);
 | 
					                let _ = component
 | 
				
			||||||
                let _ = invoke.respond(&ctx, resp).await;
 | 
					                    .create_interaction_response(&ctx, |f| {
 | 
				
			||||||
 | 
					                        f.kind(InteractionResponseType::UpdateMessage).interaction_response_data(
 | 
				
			||||||
 | 
					                            |d| {
 | 
				
			||||||
 | 
					                                send_as_initial_response(resp, d);
 | 
				
			||||||
 | 
					                                d
 | 
				
			||||||
 | 
					                            },
 | 
				
			||||||
 | 
					                        )
 | 
				
			||||||
 | 
					                    })
 | 
				
			||||||
 | 
					                    .await;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            ComponentDataModel::DelSelector(selector) => {
 | 
					            ComponentDataModel::DelSelector(selector) => {
 | 
				
			||||||
                let pool = ctx.data.read().await.get::<SQLPool>().cloned().unwrap();
 | 
					 | 
				
			||||||
                let selected_id = component.data.values.join(",");
 | 
					                let selected_id = component.data.values.join(",");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                sqlx::query!("DELETE FROM reminders WHERE FIND_IN_SET(id, ?)", selected_id)
 | 
					                sqlx::query!(
 | 
				
			||||||
                    .execute(&pool)
 | 
					                    "UPDATE reminders SET `status` = 'deleted' WHERE FIND_IN_SET(id, ?)",
 | 
				
			||||||
 | 
					                    selected_id
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					                .execute(&data.database)
 | 
				
			||||||
                .await
 | 
					                .await
 | 
				
			||||||
                .unwrap();
 | 
					                .unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                let reminders =
 | 
					                let reminders = Reminder::from_guild(
 | 
				
			||||||
                    Reminder::from_guild(ctx, component.guild_id, component.user.id).await;
 | 
					                    &ctx,
 | 
				
			||||||
 | 
					                    &data.database,
 | 
				
			||||||
 | 
					                    component.guild_id,
 | 
				
			||||||
 | 
					                    component.user.id,
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					                .await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                let resp = show_delete_page(&reminders, selector.page, selector.timezone);
 | 
					                let resp = show_delete_page(&reminders, selector.page, selector.timezone);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                let mut invoke = CommandInvoke::component(component);
 | 
					                let _ = component
 | 
				
			||||||
                let _ = invoke.respond(&ctx, resp).await;
 | 
					                    .create_interaction_response(&ctx, |f| {
 | 
				
			||||||
 | 
					                        f.kind(InteractionResponseType::UpdateMessage).interaction_response_data(
 | 
				
			||||||
 | 
					                            |d| {
 | 
				
			||||||
 | 
					                                send_as_initial_response(resp, d);
 | 
				
			||||||
 | 
					                                d
 | 
				
			||||||
 | 
					                            },
 | 
				
			||||||
 | 
					                        )
 | 
				
			||||||
 | 
					                    })
 | 
				
			||||||
 | 
					                    .await;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            ComponentDataModel::TodoPager(pager) => {
 | 
					            ComponentDataModel::TodoPager(pager) => {
 | 
				
			||||||
                if Some(component.user.id.0) == pager.user_id || pager.user_id.is_none() {
 | 
					                if Some(component.user.id.0) == pager.user_id || pager.user_id.is_none() {
 | 
				
			||||||
                    let pool = ctx.data.read().await.get::<SQLPool>().cloned().unwrap();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    let values = if let Some(uid) = pager.user_id {
 | 
					                    let values = if let Some(uid) = pager.user_id {
 | 
				
			||||||
                        sqlx::query!(
 | 
					                        sqlx::query!(
 | 
				
			||||||
                            "SELECT todos.id, value FROM todos
 | 
					                            "SELECT todos.id, value FROM todos
 | 
				
			||||||
    INNER JOIN users ON todos.user_id = users.id
 | 
					INNER JOIN users ON todos.user_id = users.id
 | 
				
			||||||
    WHERE users.user = ?",
 | 
					WHERE users.user = ?",
 | 
				
			||||||
                            uid,
 | 
					                            uid,
 | 
				
			||||||
                        )
 | 
					                        )
 | 
				
			||||||
                        .fetch_all(&pool)
 | 
					                        .fetch_all(&data.database)
 | 
				
			||||||
                        .await
 | 
					                        .await
 | 
				
			||||||
                        .unwrap()
 | 
					                        .unwrap()
 | 
				
			||||||
                        .iter()
 | 
					                        .iter()
 | 
				
			||||||
@@ -181,11 +215,11 @@ impl ComponentDataModel {
 | 
				
			|||||||
                    } else if let Some(cid) = pager.channel_id {
 | 
					                    } else if let Some(cid) = pager.channel_id {
 | 
				
			||||||
                        sqlx::query!(
 | 
					                        sqlx::query!(
 | 
				
			||||||
                            "SELECT todos.id, value FROM todos
 | 
					                            "SELECT todos.id, value FROM todos
 | 
				
			||||||
    INNER JOIN channels ON todos.channel_id = channels.id
 | 
					INNER JOIN channels ON todos.channel_id = channels.id
 | 
				
			||||||
    WHERE channels.channel = ?",
 | 
					WHERE channels.channel = ?",
 | 
				
			||||||
                            cid,
 | 
					                            cid,
 | 
				
			||||||
                        )
 | 
					                        )
 | 
				
			||||||
                        .fetch_all(&pool)
 | 
					                        .fetch_all(&data.database)
 | 
				
			||||||
                        .await
 | 
					                        .await
 | 
				
			||||||
                        .unwrap()
 | 
					                        .unwrap()
 | 
				
			||||||
                        .iter()
 | 
					                        .iter()
 | 
				
			||||||
@@ -194,11 +228,11 @@ impl ComponentDataModel {
 | 
				
			|||||||
                    } else {
 | 
					                    } else {
 | 
				
			||||||
                        sqlx::query!(
 | 
					                        sqlx::query!(
 | 
				
			||||||
                            "SELECT todos.id, value FROM todos
 | 
					                            "SELECT todos.id, value FROM todos
 | 
				
			||||||
    INNER JOIN guilds ON todos.guild_id = guilds.id
 | 
					INNER JOIN guilds ON todos.guild_id = guilds.id
 | 
				
			||||||
    WHERE guilds.guild = ?",
 | 
					WHERE guilds.guild = ?",
 | 
				
			||||||
                            pager.guild_id,
 | 
					                            pager.guild_id,
 | 
				
			||||||
                        )
 | 
					                        )
 | 
				
			||||||
                        .fetch_all(&pool)
 | 
					                        .fetch_all(&data.database)
 | 
				
			||||||
                        .await
 | 
					                        .await
 | 
				
			||||||
                        .unwrap()
 | 
					                        .unwrap()
 | 
				
			||||||
                        .iter()
 | 
					                        .iter()
 | 
				
			||||||
@@ -216,15 +250,22 @@ impl ComponentDataModel {
 | 
				
			|||||||
                        pager.guild_id,
 | 
					                        pager.guild_id,
 | 
				
			||||||
                    );
 | 
					                    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    let mut invoke = CommandInvoke::component(component);
 | 
					                    let _ = component
 | 
				
			||||||
                    let _ = invoke.respond(&ctx, resp).await;
 | 
					                        .create_interaction_response(&ctx, |f| {
 | 
				
			||||||
 | 
					                            f.kind(InteractionResponseType::UpdateMessage)
 | 
				
			||||||
 | 
					                                .interaction_response_data(|d| {
 | 
				
			||||||
 | 
					                                    send_as_initial_response(resp, d);
 | 
				
			||||||
 | 
					                                    d
 | 
				
			||||||
 | 
					                                })
 | 
				
			||||||
 | 
					                        })
 | 
				
			||||||
 | 
					                        .await;
 | 
				
			||||||
                } else {
 | 
					                } else {
 | 
				
			||||||
                    let _ = component
 | 
					                    let _ = component
 | 
				
			||||||
                        .create_interaction_response(&ctx, |r| {
 | 
					                        .create_interaction_response(&ctx, |r| {
 | 
				
			||||||
                            r.kind(InteractionResponseType::ChannelMessageWithSource)
 | 
					                            r.kind(InteractionResponseType::ChannelMessageWithSource)
 | 
				
			||||||
                                .interaction_response_data(|d| {
 | 
					                                .interaction_response_data(|d| {
 | 
				
			||||||
                                    d.flags(
 | 
					                                    d.flags(
 | 
				
			||||||
                                        InteractionApplicationCommandCallbackDataFlags::EPHEMERAL,
 | 
					                                        MessageFlags::EPHEMERAL,
 | 
				
			||||||
                                    )
 | 
					                                    )
 | 
				
			||||||
                                    .content("Only the user who performed the command can use these components")
 | 
					                                    .content("Only the user who performed the command can use these components")
 | 
				
			||||||
                                })
 | 
					                                })
 | 
				
			||||||
@@ -234,11 +275,10 @@ impl ComponentDataModel {
 | 
				
			|||||||
            }
 | 
					            }
 | 
				
			||||||
            ComponentDataModel::TodoSelector(selector) => {
 | 
					            ComponentDataModel::TodoSelector(selector) => {
 | 
				
			||||||
                if Some(component.user.id.0) == selector.user_id || selector.user_id.is_none() {
 | 
					                if Some(component.user.id.0) == selector.user_id || selector.user_id.is_none() {
 | 
				
			||||||
                    let pool = ctx.data.read().await.get::<SQLPool>().cloned().unwrap();
 | 
					 | 
				
			||||||
                    let selected_id = component.data.values.join(",");
 | 
					                    let selected_id = component.data.values.join(",");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    sqlx::query!("DELETE FROM todos WHERE FIND_IN_SET(id, ?)", selected_id)
 | 
					                    sqlx::query!("DELETE FROM todos WHERE FIND_IN_SET(id, ?)", selected_id)
 | 
				
			||||||
                        .execute(&pool)
 | 
					                        .execute(&data.database)
 | 
				
			||||||
                        .await
 | 
					                        .await
 | 
				
			||||||
                        .unwrap();
 | 
					                        .unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -249,7 +289,7 @@ impl ComponentDataModel {
 | 
				
			|||||||
                    selector.channel_id,
 | 
					                    selector.channel_id,
 | 
				
			||||||
                    selector.guild_id,
 | 
					                    selector.guild_id,
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
                .fetch_all(&pool)
 | 
					                .fetch_all(&data.database)
 | 
				
			||||||
                .await
 | 
					                .await
 | 
				
			||||||
                .unwrap()
 | 
					                .unwrap()
 | 
				
			||||||
                .iter()
 | 
					                .iter()
 | 
				
			||||||
@@ -264,15 +304,22 @@ impl ComponentDataModel {
 | 
				
			|||||||
                        selector.guild_id,
 | 
					                        selector.guild_id,
 | 
				
			||||||
                    );
 | 
					                    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    let mut invoke = CommandInvoke::component(component);
 | 
					                    let _ = component
 | 
				
			||||||
                    let _ = invoke.respond(&ctx, resp).await;
 | 
					                        .create_interaction_response(&ctx, |f| {
 | 
				
			||||||
 | 
					                            f.kind(InteractionResponseType::UpdateMessage)
 | 
				
			||||||
 | 
					                                .interaction_response_data(|d| {
 | 
				
			||||||
 | 
					                                    send_as_initial_response(resp, d);
 | 
				
			||||||
 | 
					                                    d
 | 
				
			||||||
 | 
					                                })
 | 
				
			||||||
 | 
					                        })
 | 
				
			||||||
 | 
					                        .await;
 | 
				
			||||||
                } else {
 | 
					                } else {
 | 
				
			||||||
                    let _ = component
 | 
					                    let _ = component
 | 
				
			||||||
                        .create_interaction_response(&ctx, |r| {
 | 
					                        .create_interaction_response(&ctx, |r| {
 | 
				
			||||||
                            r.kind(InteractionResponseType::ChannelMessageWithSource)
 | 
					                            r.kind(InteractionResponseType::ChannelMessageWithSource)
 | 
				
			||||||
                                .interaction_response_data(|d| {
 | 
					                                .interaction_response_data(|d| {
 | 
				
			||||||
                                    d.flags(
 | 
					                                    d.flags(
 | 
				
			||||||
                                        InteractionApplicationCommandCallbackDataFlags::EPHEMERAL,
 | 
					                                        MessageFlags::EPHEMERAL,
 | 
				
			||||||
                                    )
 | 
					                                    )
 | 
				
			||||||
                                    .content("Only the user who performed the command can use these components")
 | 
					                                    .content("Only the user who performed the command can use these components")
 | 
				
			||||||
                                })
 | 
					                                })
 | 
				
			||||||
@@ -281,15 +328,87 @@ impl ComponentDataModel {
 | 
				
			|||||||
                }
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            ComponentDataModel::MacroPager(pager) => {
 | 
					            ComponentDataModel::MacroPager(pager) => {
 | 
				
			||||||
                let mut invoke = CommandInvoke::component(component);
 | 
					                let macros = data.command_macros(component.guild_id.unwrap()).await.unwrap();
 | 
				
			||||||
 | 
					 | 
				
			||||||
                let macros = CommandMacro::from_guild(ctx, invoke.guild_id().unwrap()).await;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
                let max_page = max_macro_page(¯os);
 | 
					                let max_page = max_macro_page(¯os);
 | 
				
			||||||
                let page = pager.next_page(max_page);
 | 
					                let page = pager.next_page(max_page);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                let resp = show_macro_page(¯os, page);
 | 
					                let resp = show_macro_page(¯os, page);
 | 
				
			||||||
                let _ = invoke.respond(&ctx, resp).await;
 | 
					
 | 
				
			||||||
 | 
					                let _ = component
 | 
				
			||||||
 | 
					                    .create_interaction_response(&ctx, |f| {
 | 
				
			||||||
 | 
					                        f.kind(InteractionResponseType::UpdateMessage).interaction_response_data(
 | 
				
			||||||
 | 
					                            |d| {
 | 
				
			||||||
 | 
					                                send_as_initial_response(resp, d);
 | 
				
			||||||
 | 
					                                d
 | 
				
			||||||
 | 
					                            },
 | 
				
			||||||
 | 
					                        )
 | 
				
			||||||
 | 
					                    })
 | 
				
			||||||
 | 
					                    .await;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            ComponentDataModel::UndoReminder(undo_reminder) => {
 | 
				
			||||||
 | 
					                if component.user.id == undo_reminder.user_id {
 | 
				
			||||||
 | 
					                    let reminder =
 | 
				
			||||||
 | 
					                        Reminder::from_id(&data.database, undo_reminder.reminder_id).await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    if let Some(reminder) = reminder {
 | 
				
			||||||
 | 
					                        match reminder.delete(&data.database).await {
 | 
				
			||||||
 | 
					                            Ok(()) => {
 | 
				
			||||||
 | 
					                                let _ = component
 | 
				
			||||||
 | 
					                                    .create_interaction_response(&ctx, |f| {
 | 
				
			||||||
 | 
					                                        f.kind(InteractionResponseType::UpdateMessage)
 | 
				
			||||||
 | 
					                                            .interaction_response_data(|d| {
 | 
				
			||||||
 | 
					                                                d.embed(|e| {
 | 
				
			||||||
 | 
					                                                    e.title("Reminder Canceled")
 | 
				
			||||||
 | 
					                                                        .description(
 | 
				
			||||||
 | 
					                                                            "This reminder has been canceled.",
 | 
				
			||||||
 | 
					                                                        )
 | 
				
			||||||
 | 
					                                                        .color(*THEME_COLOR)
 | 
				
			||||||
 | 
					                                                })
 | 
				
			||||||
 | 
					                                                .components(|c| c)
 | 
				
			||||||
 | 
					                                            })
 | 
				
			||||||
 | 
					                                    })
 | 
				
			||||||
 | 
					                                    .await;
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					                            Err(e) => {
 | 
				
			||||||
 | 
					                                warn!("Error canceling reminder: {:?}", e);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                                let _ = component
 | 
				
			||||||
 | 
					                                    .create_interaction_response(&ctx, |f| {
 | 
				
			||||||
 | 
					                                        f.kind(InteractionResponseType::ChannelMessageWithSource)
 | 
				
			||||||
 | 
					                                            .interaction_response_data(|d| {
 | 
				
			||||||
 | 
					                                                d.content(
 | 
				
			||||||
 | 
					                                                    "The reminder could not be canceled: it may have already been deleted. Check `/del`!")
 | 
				
			||||||
 | 
					                                                    .ephemeral(true)
 | 
				
			||||||
 | 
					                                            })
 | 
				
			||||||
 | 
					                                    })
 | 
				
			||||||
 | 
					                                    .await;
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    } else {
 | 
				
			||||||
 | 
					                        let _ = component
 | 
				
			||||||
 | 
					                            .create_interaction_response(&ctx, |f| {
 | 
				
			||||||
 | 
					                                f.kind(InteractionResponseType::ChannelMessageWithSource)
 | 
				
			||||||
 | 
					                                    .interaction_response_data(|d| {
 | 
				
			||||||
 | 
					                                        d.content(
 | 
				
			||||||
 | 
					                                            "The reminder could not be canceled: it may have already been deleted. Check `/del`!")
 | 
				
			||||||
 | 
					                                            .ephemeral(true)
 | 
				
			||||||
 | 
					                                    })
 | 
				
			||||||
 | 
					                            })
 | 
				
			||||||
 | 
					                            .await;
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    let _ = component
 | 
				
			||||||
 | 
					                        .create_interaction_response(&ctx, |f| {
 | 
				
			||||||
 | 
					                            f.kind(InteractionResponseType::ChannelMessageWithSource)
 | 
				
			||||||
 | 
					                                .interaction_response_data(|d| {
 | 
				
			||||||
 | 
					                                    d.content(
 | 
				
			||||||
 | 
					                                        "Only the user who performed the command can use this button.")
 | 
				
			||||||
 | 
					                                        .ephemeral(true)
 | 
				
			||||||
 | 
					                                })
 | 
				
			||||||
 | 
					                        })
 | 
				
			||||||
 | 
					                        .await;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -308,3 +427,9 @@ pub struct TodoSelector {
 | 
				
			|||||||
    pub channel_id: Option<u64>,
 | 
					    pub channel_id: Option<u64>,
 | 
				
			||||||
    pub guild_id: Option<u64>,
 | 
					    pub guild_id: Option<u64>,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Serialize, Deserialize)]
 | 
				
			||||||
 | 
					pub struct UndoReminder {
 | 
				
			||||||
 | 
					    pub user_id: serenity::UserId,
 | 
				
			||||||
 | 
					    pub reminder_id: u32,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,8 +1,10 @@
 | 
				
			|||||||
// todo split pager out into a single struct
 | 
					// todo split pager out into a single struct
 | 
				
			||||||
use chrono_tz::Tz;
 | 
					use chrono_tz::Tz;
 | 
				
			||||||
 | 
					use poise::serenity_prelude::{
 | 
				
			||||||
 | 
					    builder::CreateComponents, model::application::component::ButtonStyle,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
use serde::{Deserialize, Serialize};
 | 
					use serde::{Deserialize, Serialize};
 | 
				
			||||||
use serde_repr::*;
 | 
					use serde_repr::*;
 | 
				
			||||||
use serenity::{builder::CreateComponents, model::interactions::message_component::ButtonStyle};
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::{component_models::ComponentDataModel, models::reminder::look_flags::LookFlags};
 | 
					use crate::{component_models::ComponentDataModel, models::reminder::look_flags::LookFlags};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,36 +1,29 @@
 | 
				
			|||||||
pub const DAY: u64 = 86_400;
 | 
					pub const DAY: u64 = 86_400;
 | 
				
			||||||
pub const HOUR: u64 = 3_600;
 | 
					pub const HOUR: u64 = 3_600;
 | 
				
			||||||
pub const MINUTE: u64 = 60;
 | 
					pub const MINUTE: u64 = 60;
 | 
				
			||||||
pub const EMBED_DESCRIPTION_MAX_LENGTH: usize = 4000;
 | 
					
 | 
				
			||||||
 | 
					pub const EMBED_DESCRIPTION_MAX_LENGTH: usize = 4096;
 | 
				
			||||||
pub const SELECT_MAX_ENTRIES: usize = 25;
 | 
					pub const SELECT_MAX_ENTRIES: usize = 25;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub const CHARACTERS: &str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_";
 | 
					pub const CHARACTERS: &str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const THEME_COLOR_FALLBACK: u32 = 0x8fb677;
 | 
					const THEME_COLOR_FALLBACK: u32 = 0x8fb677;
 | 
				
			||||||
 | 
					pub const MACRO_MAX_COMMANDS: usize = 5;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use std::{collections::HashSet, env, iter::FromIterator};
 | 
					use std::{collections::HashSet, env, iter::FromIterator};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use poise::serenity_prelude::model::prelude::AttachmentType;
 | 
				
			||||||
use regex::Regex;
 | 
					use regex::Regex;
 | 
				
			||||||
use serenity::http::AttachmentType;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
lazy_static! {
 | 
					lazy_static! {
 | 
				
			||||||
    pub static ref REMIND_INTERVAL: u64 = env::var("REMIND_INTERVAL")
 | 
					 | 
				
			||||||
        .map(|inner| inner.parse::<u64>().ok())
 | 
					 | 
				
			||||||
        .ok()
 | 
					 | 
				
			||||||
        .flatten()
 | 
					 | 
				
			||||||
        .unwrap_or(10);
 | 
					 | 
				
			||||||
    pub static ref DEFAULT_AVATAR: AttachmentType<'static> = (
 | 
					    pub static ref DEFAULT_AVATAR: AttachmentType<'static> = (
 | 
				
			||||||
        include_bytes!(concat!(
 | 
					        include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/assets/webhook.jpg")) as &[u8],
 | 
				
			||||||
            env!("CARGO_MANIFEST_DIR"),
 | 
					        "webhook.jpg",
 | 
				
			||||||
            "/assets/",
 | 
					 | 
				
			||||||
            env!("WEBHOOK_AVATAR", "WEBHOOK_AVATAR not provided for compilation")
 | 
					 | 
				
			||||||
        )) as &[u8],
 | 
					 | 
				
			||||||
        env!("WEBHOOK_AVATAR"),
 | 
					 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
        .into();
 | 
					        .into();
 | 
				
			||||||
    pub static ref REGEX_CHANNEL_USER: Regex = Regex::new(r#"\s*<(#|@)(?:!)?(\d+)>\s*"#).unwrap();
 | 
					    pub static ref REGEX_CHANNEL_USER: Regex = Regex::new(r#"\s*<(#|@)(?:!)?(\d+)>\s*"#).unwrap();
 | 
				
			||||||
    pub static ref SUBSCRIPTION_ROLES: HashSet<u64> = HashSet::from_iter(
 | 
					    pub static ref SUBSCRIPTION_ROLES: HashSet<u64> = HashSet::from_iter(
 | 
				
			||||||
        env::var("SUBSCRIPTION_ROLES")
 | 
					        env::var("PATREON_ROLE_ID")
 | 
				
			||||||
            .map(|var| var
 | 
					            .map(|var| var
 | 
				
			||||||
                .split(',')
 | 
					                .split(',')
 | 
				
			||||||
                .filter_map(|item| { item.parse::<u64>().ok() })
 | 
					                .filter_map(|item| { item.parse::<u64>().ok() })
 | 
				
			||||||
@@ -38,16 +31,12 @@ lazy_static! {
 | 
				
			|||||||
            .unwrap_or_else(|_| Vec::new())
 | 
					            .unwrap_or_else(|_| Vec::new())
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
    pub static ref CNC_GUILD: Option<u64> =
 | 
					    pub static ref CNC_GUILD: Option<u64> =
 | 
				
			||||||
        env::var("CNC_GUILD").map(|var| var.parse::<u64>().ok()).ok().flatten();
 | 
					        env::var("PATREON_GUILD_ID").map(|var| var.parse::<u64>().ok()).ok().flatten();
 | 
				
			||||||
    pub static ref MIN_INTERVAL: i64 = env::var("MIN_INTERVAL")
 | 
					    pub static ref MIN_INTERVAL: i64 =
 | 
				
			||||||
        .ok()
 | 
					        env::var("MIN_INTERVAL").ok().and_then(|inner| inner.parse::<i64>().ok()).unwrap_or(600);
 | 
				
			||||||
        .map(|inner| inner.parse::<i64>().ok())
 | 
					 | 
				
			||||||
        .flatten()
 | 
					 | 
				
			||||||
        .unwrap_or(600);
 | 
					 | 
				
			||||||
    pub static ref MAX_TIME: i64 = env::var("MAX_TIME")
 | 
					    pub static ref MAX_TIME: i64 = env::var("MAX_TIME")
 | 
				
			||||||
        .ok()
 | 
					        .ok()
 | 
				
			||||||
        .map(|inner| inner.parse::<i64>().ok())
 | 
					        .and_then(|inner| inner.parse::<i64>().ok())
 | 
				
			||||||
        .flatten()
 | 
					 | 
				
			||||||
        .unwrap_or(60 * 60 * 24 * 365 * 50);
 | 
					        .unwrap_or(60 * 60 * 24 * 365 * 50);
 | 
				
			||||||
    pub static ref LOCAL_TIMEZONE: String =
 | 
					    pub static ref LOCAL_TIMEZONE: String =
 | 
				
			||||||
        env::var("LOCAL_TIMEZONE").unwrap_or_else(|_| "UTC".to_string());
 | 
					        env::var("LOCAL_TIMEZONE").unwrap_or_else(|_| "UTC".to_string());
 | 
				
			||||||
@@ -55,5 +44,5 @@ lazy_static! {
 | 
				
			|||||||
        .map_or(THEME_COLOR_FALLBACK, |inner| u32::from_str_radix(&inner, 16)
 | 
					        .map_or(THEME_COLOR_FALLBACK, |inner| u32::from_str_radix(&inner, 16)
 | 
				
			||||||
            .unwrap_or(THEME_COLOR_FALLBACK));
 | 
					            .unwrap_or(THEME_COLOR_FALLBACK));
 | 
				
			||||||
    pub static ref PYTHON_LOCATION: String =
 | 
					    pub static ref PYTHON_LOCATION: String =
 | 
				
			||||||
        env::var("PYTHON_LOCATION").unwrap_or_else(|_| "venv/bin/python3".to_string());
 | 
					        env::var("PYTHON_LOCATION").unwrap_or_else(|_| "/usr/bin/python3".to_string());
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										114
									
								
								src/event_handlers.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										114
									
								
								src/event_handlers.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,114 @@
 | 
				
			|||||||
 | 
					use std::{collections::HashMap, env};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use log::error;
 | 
				
			||||||
 | 
					use poise::{
 | 
				
			||||||
 | 
					    serenity_prelude as serenity,
 | 
				
			||||||
 | 
					    serenity_prelude::{model::application::interaction::Interaction, utils::shard_id},
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::{component_models::ComponentDataModel, Data, Error, THEME_COLOR};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub async fn listener(
 | 
				
			||||||
 | 
					    ctx: &serenity::Context,
 | 
				
			||||||
 | 
					    event: &poise::Event<'_>,
 | 
				
			||||||
 | 
					    data: &Data,
 | 
				
			||||||
 | 
					) -> Result<(), Error> {
 | 
				
			||||||
 | 
					    match event {
 | 
				
			||||||
 | 
					        poise::Event::Ready { .. } => {
 | 
				
			||||||
 | 
					            ctx.set_activity(serenity::Activity::watching("for /remind")).await;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        poise::Event::ChannelDelete { channel } => {
 | 
				
			||||||
 | 
					            sqlx::query!("DELETE FROM channels WHERE channel = ?", channel.id.as_u64())
 | 
				
			||||||
 | 
					                .execute(&data.database)
 | 
				
			||||||
 | 
					                .await
 | 
				
			||||||
 | 
					                .unwrap();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        poise::Event::GuildCreate { guild, is_new } => {
 | 
				
			||||||
 | 
					            if *is_new {
 | 
				
			||||||
 | 
					                let guild_id = guild.id.as_u64().to_owned();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                sqlx::query!("INSERT IGNORE INTO guilds (guild) VALUES (?)", guild_id)
 | 
				
			||||||
 | 
					                    .execute(&data.database)
 | 
				
			||||||
 | 
					                    .await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if let Err(e) = post_guild_count(ctx, &data.http, guild_id).await {
 | 
				
			||||||
 | 
					                    error!("DiscordBotList: {:?}", e);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                let default_channel = guild.default_channel_guaranteed();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if let Some(default_channel) = default_channel {
 | 
				
			||||||
 | 
					                    default_channel
 | 
				
			||||||
 | 
					                        .send_message(&ctx, |m| {
 | 
				
			||||||
 | 
					                            m.embed(|e| {
 | 
				
			||||||
 | 
					                                e.title("Thank you for adding Reminder Bot!").description(
 | 
				
			||||||
 | 
					                                    "To get started:
 | 
				
			||||||
 | 
					• Set your timezone with `/timezone`
 | 
				
			||||||
 | 
					• Set up permissions in Server Settings 🠚 Integrations 🠚 Reminder Bot (desktop only)
 | 
				
			||||||
 | 
					• Create your first reminder with `/remind`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					__Support__
 | 
				
			||||||
 | 
					If you need any support, please come and ask us! Join our [Discord](https://discord.jellywx.com).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					__Updates__
 | 
				
			||||||
 | 
					To stay up to date on the latest features and fixes, join our [Discord](https://discord.jellywx.com).
 | 
				
			||||||
 | 
					",
 | 
				
			||||||
 | 
					                                ).color(*THEME_COLOR)
 | 
				
			||||||
 | 
					                            })
 | 
				
			||||||
 | 
					                        })
 | 
				
			||||||
 | 
					                        .await?;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        poise::Event::GuildDelete { incomplete, .. } => {
 | 
				
			||||||
 | 
					            let _ = sqlx::query!("DELETE FROM guilds WHERE guild = ?", incomplete.id.0)
 | 
				
			||||||
 | 
					                .execute(&data.database)
 | 
				
			||||||
 | 
					                .await;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        poise::Event::InteractionCreate { interaction } => {
 | 
				
			||||||
 | 
					            if let Interaction::MessageComponent(component) = interaction {
 | 
				
			||||||
 | 
					                let component_model = ComponentDataModel::from_custom_id(&component.data.custom_id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                component_model.act(ctx, data, component).await;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        _ => {}
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Ok(())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async fn post_guild_count(
 | 
				
			||||||
 | 
					    ctx: &serenity::Context,
 | 
				
			||||||
 | 
					    http: &reqwest::Client,
 | 
				
			||||||
 | 
					    guild_id: u64,
 | 
				
			||||||
 | 
					) -> Result<(), reqwest::Error> {
 | 
				
			||||||
 | 
					    if let Ok(token) = env::var("DISCORDBOTS_TOKEN") {
 | 
				
			||||||
 | 
					        let shard_count = ctx.cache.shard_count();
 | 
				
			||||||
 | 
					        let current_shard_id = shard_id(guild_id, shard_count);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let guild_count = ctx
 | 
				
			||||||
 | 
					            .cache
 | 
				
			||||||
 | 
					            .guilds()
 | 
				
			||||||
 | 
					            .iter()
 | 
				
			||||||
 | 
					            .filter(|g| shard_id(g.as_u64().to_owned(), shard_count) == current_shard_id)
 | 
				
			||||||
 | 
					            .count() as u64;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let mut hm = HashMap::new();
 | 
				
			||||||
 | 
					        hm.insert("server_count", guild_count);
 | 
				
			||||||
 | 
					        hm.insert("shard_id", current_shard_id);
 | 
				
			||||||
 | 
					        hm.insert("shard_count", shard_count);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        http.post(
 | 
				
			||||||
 | 
					            format!("https://top.gg/api/bots/{}/stats", ctx.cache.current_user_id().as_u64())
 | 
				
			||||||
 | 
					                .as_str(),
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        .header("Authorization", token)
 | 
				
			||||||
 | 
					        .json(&hm)
 | 
				
			||||||
 | 
					        .send()
 | 
				
			||||||
 | 
					        .await
 | 
				
			||||||
 | 
					        .map(|_| ())
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        Ok(())
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										692
									
								
								src/framework.rs
									
									
									
									
									
								
							
							
						
						
									
										692
									
								
								src/framework.rs
									
									
									
									
									
								
							@@ -1,692 +0,0 @@
 | 
				
			|||||||
// todo move framework to its own module, split out permission checks
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use std::{
 | 
					 | 
				
			||||||
    collections::{HashMap, HashSet},
 | 
					 | 
				
			||||||
    hash::{Hash, Hasher},
 | 
					 | 
				
			||||||
    sync::Arc,
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use log::info;
 | 
					 | 
				
			||||||
use serde::{Deserialize, Serialize};
 | 
					 | 
				
			||||||
use serenity::{
 | 
					 | 
				
			||||||
    builder::{CreateApplicationCommands, CreateComponents, CreateEmbed},
 | 
					 | 
				
			||||||
    cache::Cache,
 | 
					 | 
				
			||||||
    client::Context,
 | 
					 | 
				
			||||||
    futures::prelude::future::BoxFuture,
 | 
					 | 
				
			||||||
    http::Http,
 | 
					 | 
				
			||||||
    model::{
 | 
					 | 
				
			||||||
        guild::Guild,
 | 
					 | 
				
			||||||
        id::{ChannelId, GuildId, RoleId, UserId},
 | 
					 | 
				
			||||||
        interactions::{
 | 
					 | 
				
			||||||
            application_command::{
 | 
					 | 
				
			||||||
                ApplicationCommand, ApplicationCommandInteraction, ApplicationCommandOptionType,
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
            message_component::MessageComponentInteraction,
 | 
					 | 
				
			||||||
            InteractionApplicationCommandCallbackDataFlags, InteractionResponseType,
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        prelude::application_command::ApplicationCommandInteractionDataOption,
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    prelude::TypeMapKey,
 | 
					 | 
				
			||||||
    Result as SerenityResult,
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use crate::SQLPool;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
pub struct CreateGenericResponse {
 | 
					 | 
				
			||||||
    content: String,
 | 
					 | 
				
			||||||
    embed: Option<CreateEmbed>,
 | 
					 | 
				
			||||||
    components: Option<CreateComponents>,
 | 
					 | 
				
			||||||
    flags: InteractionApplicationCommandCallbackDataFlags,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl CreateGenericResponse {
 | 
					 | 
				
			||||||
    pub fn new() -> Self {
 | 
					 | 
				
			||||||
        Self {
 | 
					 | 
				
			||||||
            content: "".to_string(),
 | 
					 | 
				
			||||||
            embed: None,
 | 
					 | 
				
			||||||
            components: None,
 | 
					 | 
				
			||||||
            flags: InteractionApplicationCommandCallbackDataFlags::empty(),
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    pub fn ephemeral(mut self) -> Self {
 | 
					 | 
				
			||||||
        self.flags.insert(InteractionApplicationCommandCallbackDataFlags::EPHEMERAL);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        self
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    pub fn content<D: ToString>(mut self, content: D) -> Self {
 | 
					 | 
				
			||||||
        self.content = content.to_string();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        self
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    pub fn embed<F: FnOnce(&mut CreateEmbed) -> &mut CreateEmbed>(mut self, f: F) -> Self {
 | 
					 | 
				
			||||||
        let mut embed = CreateEmbed::default();
 | 
					 | 
				
			||||||
        f(&mut embed);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        self.embed = Some(embed);
 | 
					 | 
				
			||||||
        self
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    pub fn components<F: FnOnce(&mut CreateComponents) -> &mut CreateComponents>(
 | 
					 | 
				
			||||||
        mut self,
 | 
					 | 
				
			||||||
        f: F,
 | 
					 | 
				
			||||||
    ) -> Self {
 | 
					 | 
				
			||||||
        let mut components = CreateComponents::default();
 | 
					 | 
				
			||||||
        f(&mut components);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        self.components = Some(components);
 | 
					 | 
				
			||||||
        self
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Clone)]
 | 
					 | 
				
			||||||
enum InvokeModel {
 | 
					 | 
				
			||||||
    Slash(ApplicationCommandInteraction),
 | 
					 | 
				
			||||||
    Component(MessageComponentInteraction),
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Clone)]
 | 
					 | 
				
			||||||
pub struct CommandInvoke {
 | 
					 | 
				
			||||||
    model: InvokeModel,
 | 
					 | 
				
			||||||
    already_responded: bool,
 | 
					 | 
				
			||||||
    deferred: bool,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl CommandInvoke {
 | 
					 | 
				
			||||||
    pub fn component(component: MessageComponentInteraction) -> Self {
 | 
					 | 
				
			||||||
        Self { model: InvokeModel::Component(component), already_responded: false, deferred: false }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    fn slash(interaction: ApplicationCommandInteraction) -> Self {
 | 
					 | 
				
			||||||
        Self { model: InvokeModel::Slash(interaction), already_responded: false, deferred: false }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    pub async fn defer(&mut self, http: impl AsRef<Http>) {
 | 
					 | 
				
			||||||
        if !self.deferred {
 | 
					 | 
				
			||||||
            match &self.model {
 | 
					 | 
				
			||||||
                InvokeModel::Slash(i) => {
 | 
					 | 
				
			||||||
                    i.create_interaction_response(http, |r| {
 | 
					 | 
				
			||||||
                        r.kind(InteractionResponseType::DeferredChannelMessageWithSource)
 | 
					 | 
				
			||||||
                    })
 | 
					 | 
				
			||||||
                    .await
 | 
					 | 
				
			||||||
                    .unwrap();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    self.deferred = true;
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                InvokeModel::Component(i) => {
 | 
					 | 
				
			||||||
                    i.create_interaction_response(http, |r| {
 | 
					 | 
				
			||||||
                        r.kind(InteractionResponseType::DeferredChannelMessageWithSource)
 | 
					 | 
				
			||||||
                    })
 | 
					 | 
				
			||||||
                    .await
 | 
					 | 
				
			||||||
                    .unwrap();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    self.deferred = true;
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    pub fn channel_id(&self) -> ChannelId {
 | 
					 | 
				
			||||||
        match &self.model {
 | 
					 | 
				
			||||||
            InvokeModel::Slash(i) => i.channel_id,
 | 
					 | 
				
			||||||
            InvokeModel::Component(i) => i.channel_id,
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    pub fn guild_id(&self) -> Option<GuildId> {
 | 
					 | 
				
			||||||
        match &self.model {
 | 
					 | 
				
			||||||
            InvokeModel::Slash(i) => i.guild_id,
 | 
					 | 
				
			||||||
            InvokeModel::Component(i) => i.guild_id,
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    pub fn guild(&self, cache: impl AsRef<Cache>) -> Option<Guild> {
 | 
					 | 
				
			||||||
        self.guild_id().map(|id| id.to_guild_cached(cache)).flatten()
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    pub fn author_id(&self) -> UserId {
 | 
					 | 
				
			||||||
        match &self.model {
 | 
					 | 
				
			||||||
            InvokeModel::Slash(i) => i.user.id,
 | 
					 | 
				
			||||||
            InvokeModel::Component(i) => i.user.id,
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    pub async fn respond(
 | 
					 | 
				
			||||||
        &mut self,
 | 
					 | 
				
			||||||
        http: impl AsRef<Http>,
 | 
					 | 
				
			||||||
        generic_response: CreateGenericResponse,
 | 
					 | 
				
			||||||
    ) -> SerenityResult<()> {
 | 
					 | 
				
			||||||
        match &self.model {
 | 
					 | 
				
			||||||
            InvokeModel::Slash(i) => {
 | 
					 | 
				
			||||||
                if self.already_responded {
 | 
					 | 
				
			||||||
                    i.create_followup_message(http, |d| {
 | 
					 | 
				
			||||||
                        d.allowed_mentions(|m| m.empty_parse());
 | 
					 | 
				
			||||||
                        d.content(generic_response.content);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                        if let Some(embed) = generic_response.embed {
 | 
					 | 
				
			||||||
                            d.add_embed(embed);
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                        if let Some(components) = generic_response.components {
 | 
					 | 
				
			||||||
                            d.components(|c| {
 | 
					 | 
				
			||||||
                                *c = components;
 | 
					 | 
				
			||||||
                                c
 | 
					 | 
				
			||||||
                            });
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                        d
 | 
					 | 
				
			||||||
                    })
 | 
					 | 
				
			||||||
                    .await
 | 
					 | 
				
			||||||
                    .map(|_| ())
 | 
					 | 
				
			||||||
                } else if self.deferred {
 | 
					 | 
				
			||||||
                    i.edit_original_interaction_response(http, |d| {
 | 
					 | 
				
			||||||
                        d.allowed_mentions(|m| m.empty_parse());
 | 
					 | 
				
			||||||
                        d.content(generic_response.content);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                        if let Some(embed) = generic_response.embed {
 | 
					 | 
				
			||||||
                            d.add_embed(embed);
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                        if let Some(components) = generic_response.components {
 | 
					 | 
				
			||||||
                            d.components(|c| {
 | 
					 | 
				
			||||||
                                *c = components;
 | 
					 | 
				
			||||||
                                c
 | 
					 | 
				
			||||||
                            });
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                        d
 | 
					 | 
				
			||||||
                    })
 | 
					 | 
				
			||||||
                    .await
 | 
					 | 
				
			||||||
                    .map(|_| ())
 | 
					 | 
				
			||||||
                } else {
 | 
					 | 
				
			||||||
                    i.create_interaction_response(http, |r| {
 | 
					 | 
				
			||||||
                        r.kind(InteractionResponseType::ChannelMessageWithSource)
 | 
					 | 
				
			||||||
                            .interaction_response_data(|d| {
 | 
					 | 
				
			||||||
                                d.allowed_mentions(|m| m.empty_parse());
 | 
					 | 
				
			||||||
                                d.content(generic_response.content);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                                if let Some(embed) = generic_response.embed {
 | 
					 | 
				
			||||||
                                    d.add_embed(embed);
 | 
					 | 
				
			||||||
                                }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                                if let Some(components) = generic_response.components {
 | 
					 | 
				
			||||||
                                    d.components(|c| {
 | 
					 | 
				
			||||||
                                        *c = components;
 | 
					 | 
				
			||||||
                                        c
 | 
					 | 
				
			||||||
                                    });
 | 
					 | 
				
			||||||
                                }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                                d
 | 
					 | 
				
			||||||
                            })
 | 
					 | 
				
			||||||
                    })
 | 
					 | 
				
			||||||
                    .await
 | 
					 | 
				
			||||||
                    .map(|_| ())
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            InvokeModel::Component(i) => i
 | 
					 | 
				
			||||||
                .create_interaction_response(http, |r| {
 | 
					 | 
				
			||||||
                    r.kind(InteractionResponseType::UpdateMessage).interaction_response_data(|d| {
 | 
					 | 
				
			||||||
                        d.allowed_mentions(|m| m.empty_parse());
 | 
					 | 
				
			||||||
                        d.content(generic_response.content);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                        if let Some(embed) = generic_response.embed {
 | 
					 | 
				
			||||||
                            d.add_embed(embed);
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                        if let Some(components) = generic_response.components {
 | 
					 | 
				
			||||||
                            d.components(|c| {
 | 
					 | 
				
			||||||
                                *c = components;
 | 
					 | 
				
			||||||
                                c
 | 
					 | 
				
			||||||
                            });
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                        d
 | 
					 | 
				
			||||||
                    })
 | 
					 | 
				
			||||||
                })
 | 
					 | 
				
			||||||
                .await
 | 
					 | 
				
			||||||
                .map(|_| ()),
 | 
					 | 
				
			||||||
        }?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        self.already_responded = true;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        Ok(())
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Debug)]
 | 
					 | 
				
			||||||
pub struct Arg {
 | 
					 | 
				
			||||||
    pub name: &'static str,
 | 
					 | 
				
			||||||
    pub description: &'static str,
 | 
					 | 
				
			||||||
    pub kind: ApplicationCommandOptionType,
 | 
					 | 
				
			||||||
    pub required: bool,
 | 
					 | 
				
			||||||
    pub options: &'static [&'static Self],
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Serialize, Deserialize, Clone)]
 | 
					 | 
				
			||||||
pub enum OptionValue {
 | 
					 | 
				
			||||||
    String(String),
 | 
					 | 
				
			||||||
    Integer(i64),
 | 
					 | 
				
			||||||
    Boolean(bool),
 | 
					 | 
				
			||||||
    User(UserId),
 | 
					 | 
				
			||||||
    Channel(ChannelId),
 | 
					 | 
				
			||||||
    Role(RoleId),
 | 
					 | 
				
			||||||
    Mentionable(u64),
 | 
					 | 
				
			||||||
    Number(f64),
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl OptionValue {
 | 
					 | 
				
			||||||
    pub fn as_i64(&self) -> Option<i64> {
 | 
					 | 
				
			||||||
        match self {
 | 
					 | 
				
			||||||
            OptionValue::Integer(i) => Some(*i),
 | 
					 | 
				
			||||||
            _ => None,
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    pub fn as_bool(&self) -> Option<bool> {
 | 
					 | 
				
			||||||
        match self {
 | 
					 | 
				
			||||||
            OptionValue::Boolean(b) => Some(*b),
 | 
					 | 
				
			||||||
            _ => None,
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    pub fn as_channel_id(&self) -> Option<ChannelId> {
 | 
					 | 
				
			||||||
        match self {
 | 
					 | 
				
			||||||
            OptionValue::Channel(c) => Some(*c),
 | 
					 | 
				
			||||||
            _ => None,
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    pub fn to_string(&self) -> String {
 | 
					 | 
				
			||||||
        match self {
 | 
					 | 
				
			||||||
            OptionValue::String(s) => s.to_string(),
 | 
					 | 
				
			||||||
            OptionValue::Integer(i) => i.to_string(),
 | 
					 | 
				
			||||||
            OptionValue::Boolean(b) => b.to_string(),
 | 
					 | 
				
			||||||
            OptionValue::User(u) => u.to_string(),
 | 
					 | 
				
			||||||
            OptionValue::Channel(c) => c.to_string(),
 | 
					 | 
				
			||||||
            OptionValue::Role(r) => r.to_string(),
 | 
					 | 
				
			||||||
            OptionValue::Mentionable(m) => m.to_string(),
 | 
					 | 
				
			||||||
            OptionValue::Number(n) => n.to_string(),
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Serialize, Deserialize, Clone)]
 | 
					 | 
				
			||||||
pub struct CommandOptions {
 | 
					 | 
				
			||||||
    pub command: String,
 | 
					 | 
				
			||||||
    pub subcommand: Option<String>,
 | 
					 | 
				
			||||||
    pub subcommand_group: Option<String>,
 | 
					 | 
				
			||||||
    pub options: HashMap<String, OptionValue>,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl CommandOptions {
 | 
					 | 
				
			||||||
    pub fn get(&self, key: &str) -> Option<&OptionValue> {
 | 
					 | 
				
			||||||
        self.options.get(key)
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl CommandOptions {
 | 
					 | 
				
			||||||
    fn new(command: &'static Command) -> Self {
 | 
					 | 
				
			||||||
        Self {
 | 
					 | 
				
			||||||
            command: command.names[0].to_string(),
 | 
					 | 
				
			||||||
            subcommand: None,
 | 
					 | 
				
			||||||
            subcommand_group: None,
 | 
					 | 
				
			||||||
            options: Default::default(),
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    fn populate(mut self, interaction: &ApplicationCommandInteraction) -> Self {
 | 
					 | 
				
			||||||
        fn match_option(
 | 
					 | 
				
			||||||
            option: ApplicationCommandInteractionDataOption,
 | 
					 | 
				
			||||||
            cmd_opts: &mut CommandOptions,
 | 
					 | 
				
			||||||
        ) {
 | 
					 | 
				
			||||||
            match option.kind {
 | 
					 | 
				
			||||||
                ApplicationCommandOptionType::SubCommand => {
 | 
					 | 
				
			||||||
                    cmd_opts.subcommand = Some(option.name);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    for opt in option.options {
 | 
					 | 
				
			||||||
                        match_option(opt, cmd_opts);
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                ApplicationCommandOptionType::SubCommandGroup => {
 | 
					 | 
				
			||||||
                    cmd_opts.subcommand_group = Some(option.name);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    for opt in option.options {
 | 
					 | 
				
			||||||
                        match_option(opt, cmd_opts);
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                ApplicationCommandOptionType::String => {
 | 
					 | 
				
			||||||
                    cmd_opts.options.insert(
 | 
					 | 
				
			||||||
                        option.name,
 | 
					 | 
				
			||||||
                        OptionValue::String(option.value.unwrap().as_str().unwrap().to_string()),
 | 
					 | 
				
			||||||
                    );
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                ApplicationCommandOptionType::Integer => {
 | 
					 | 
				
			||||||
                    cmd_opts.options.insert(
 | 
					 | 
				
			||||||
                        option.name,
 | 
					 | 
				
			||||||
                        OptionValue::Integer(option.value.map(|m| m.as_i64()).flatten().unwrap()),
 | 
					 | 
				
			||||||
                    );
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                ApplicationCommandOptionType::Boolean => {
 | 
					 | 
				
			||||||
                    cmd_opts.options.insert(
 | 
					 | 
				
			||||||
                        option.name,
 | 
					 | 
				
			||||||
                        OptionValue::Boolean(option.value.map(|m| m.as_bool()).flatten().unwrap()),
 | 
					 | 
				
			||||||
                    );
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                ApplicationCommandOptionType::User => {
 | 
					 | 
				
			||||||
                    cmd_opts.options.insert(
 | 
					 | 
				
			||||||
                        option.name,
 | 
					 | 
				
			||||||
                        OptionValue::User(UserId(
 | 
					 | 
				
			||||||
                            option
 | 
					 | 
				
			||||||
                                .value
 | 
					 | 
				
			||||||
                                .map(|m| m.as_str().map(|s| s.parse::<u64>().ok()))
 | 
					 | 
				
			||||||
                                .flatten()
 | 
					 | 
				
			||||||
                                .flatten()
 | 
					 | 
				
			||||||
                                .unwrap(),
 | 
					 | 
				
			||||||
                        )),
 | 
					 | 
				
			||||||
                    );
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                ApplicationCommandOptionType::Channel => {
 | 
					 | 
				
			||||||
                    cmd_opts.options.insert(
 | 
					 | 
				
			||||||
                        option.name,
 | 
					 | 
				
			||||||
                        OptionValue::Channel(ChannelId(
 | 
					 | 
				
			||||||
                            option
 | 
					 | 
				
			||||||
                                .value
 | 
					 | 
				
			||||||
                                .map(|m| m.as_str().map(|s| s.parse::<u64>().ok()))
 | 
					 | 
				
			||||||
                                .flatten()
 | 
					 | 
				
			||||||
                                .flatten()
 | 
					 | 
				
			||||||
                                .unwrap(),
 | 
					 | 
				
			||||||
                        )),
 | 
					 | 
				
			||||||
                    );
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                ApplicationCommandOptionType::Role => {
 | 
					 | 
				
			||||||
                    cmd_opts.options.insert(
 | 
					 | 
				
			||||||
                        option.name,
 | 
					 | 
				
			||||||
                        OptionValue::Role(RoleId(
 | 
					 | 
				
			||||||
                            option
 | 
					 | 
				
			||||||
                                .value
 | 
					 | 
				
			||||||
                                .map(|m| m.as_str().map(|s| s.parse::<u64>().ok()))
 | 
					 | 
				
			||||||
                                .flatten()
 | 
					 | 
				
			||||||
                                .flatten()
 | 
					 | 
				
			||||||
                                .unwrap(),
 | 
					 | 
				
			||||||
                        )),
 | 
					 | 
				
			||||||
                    );
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                ApplicationCommandOptionType::Mentionable => {
 | 
					 | 
				
			||||||
                    cmd_opts.options.insert(
 | 
					 | 
				
			||||||
                        option.name,
 | 
					 | 
				
			||||||
                        OptionValue::Mentionable(
 | 
					 | 
				
			||||||
                            option.value.map(|m| m.as_u64()).flatten().unwrap(),
 | 
					 | 
				
			||||||
                        ),
 | 
					 | 
				
			||||||
                    );
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                ApplicationCommandOptionType::Number => {
 | 
					 | 
				
			||||||
                    cmd_opts.options.insert(
 | 
					 | 
				
			||||||
                        option.name,
 | 
					 | 
				
			||||||
                        OptionValue::Number(option.value.map(|m| m.as_f64()).flatten().unwrap()),
 | 
					 | 
				
			||||||
                    );
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                _ => {}
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        for option in &interaction.data.options {
 | 
					 | 
				
			||||||
            match_option(option.clone(), &mut self)
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        self
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
pub enum HookResult {
 | 
					 | 
				
			||||||
    Continue,
 | 
					 | 
				
			||||||
    Halt,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type SlashCommandFn =
 | 
					 | 
				
			||||||
    for<'fut> fn(&'fut Context, &'fut mut CommandInvoke, CommandOptions) -> BoxFuture<'fut, ()>;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type MultiCommandFn = for<'fut> fn(&'fut Context, &'fut mut CommandInvoke) -> BoxFuture<'fut, ()>;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
pub type HookFn = for<'fut> fn(
 | 
					 | 
				
			||||||
    &'fut Context,
 | 
					 | 
				
			||||||
    &'fut mut CommandInvoke,
 | 
					 | 
				
			||||||
    &'fut CommandOptions,
 | 
					 | 
				
			||||||
) -> BoxFuture<'fut, HookResult>;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
pub enum CommandFnType {
 | 
					 | 
				
			||||||
    Slash(SlashCommandFn),
 | 
					 | 
				
			||||||
    Multi(MultiCommandFn),
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
pub struct Hook {
 | 
					 | 
				
			||||||
    pub fun: HookFn,
 | 
					 | 
				
			||||||
    pub uuid: u128,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl PartialEq for Hook {
 | 
					 | 
				
			||||||
    fn eq(&self, other: &Self) -> bool {
 | 
					 | 
				
			||||||
        self.uuid == other.uuid
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
pub struct Command {
 | 
					 | 
				
			||||||
    pub fun: CommandFnType,
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    pub names: &'static [&'static str],
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    pub desc: &'static str,
 | 
					 | 
				
			||||||
    pub examples: &'static [&'static str],
 | 
					 | 
				
			||||||
    pub group: &'static str,
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    pub args: &'static [&'static Arg],
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    pub can_blacklist: bool,
 | 
					 | 
				
			||||||
    pub supports_dm: bool,
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    pub hooks: &'static [&'static Hook],
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl Hash for Command {
 | 
					 | 
				
			||||||
    fn hash<H: Hasher>(&self, state: &mut H) {
 | 
					 | 
				
			||||||
        self.names[0].hash(state)
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl PartialEq for Command {
 | 
					 | 
				
			||||||
    fn eq(&self, other: &Self) -> bool {
 | 
					 | 
				
			||||||
        self.names[0] == other.names[0]
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl Eq for Command {}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
pub struct RegexFramework {
 | 
					 | 
				
			||||||
    pub commands_map: HashMap<String, &'static Command>,
 | 
					 | 
				
			||||||
    pub commands: HashSet<&'static Command>,
 | 
					 | 
				
			||||||
    ignore_bots: bool,
 | 
					 | 
				
			||||||
    dm_enabled: bool,
 | 
					 | 
				
			||||||
    debug_guild: Option<GuildId>,
 | 
					 | 
				
			||||||
    hooks: Vec<&'static Hook>,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl TypeMapKey for RegexFramework {
 | 
					 | 
				
			||||||
    type Value = Arc<RegexFramework>;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl RegexFramework {
 | 
					 | 
				
			||||||
    pub fn new() -> Self {
 | 
					 | 
				
			||||||
        Self {
 | 
					 | 
				
			||||||
            commands_map: HashMap::new(),
 | 
					 | 
				
			||||||
            commands: HashSet::new(),
 | 
					 | 
				
			||||||
            ignore_bots: true,
 | 
					 | 
				
			||||||
            dm_enabled: true,
 | 
					 | 
				
			||||||
            debug_guild: None,
 | 
					 | 
				
			||||||
            hooks: vec![],
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    pub fn ignore_bots(mut self, ignore_bots: bool) -> Self {
 | 
					 | 
				
			||||||
        self.ignore_bots = ignore_bots;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        self
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    pub fn dm_enabled(mut self, dm_enabled: bool) -> Self {
 | 
					 | 
				
			||||||
        self.dm_enabled = dm_enabled;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        self
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    pub fn add_hook(mut self, fun: &'static Hook) -> Self {
 | 
					 | 
				
			||||||
        self.hooks.push(fun);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        self
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    pub fn add_command(mut self, command: &'static Command) -> Self {
 | 
					 | 
				
			||||||
        self.commands.insert(command);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        for name in command.names {
 | 
					 | 
				
			||||||
            self.commands_map.insert(name.to_string(), command);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        self
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    pub fn debug_guild(mut self, guild_id: Option<GuildId>) -> Self {
 | 
					 | 
				
			||||||
        self.debug_guild = guild_id;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        self
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    fn _populate_commands<'a>(
 | 
					 | 
				
			||||||
        &self,
 | 
					 | 
				
			||||||
        commands: &'a mut CreateApplicationCommands,
 | 
					 | 
				
			||||||
    ) -> &'a mut CreateApplicationCommands {
 | 
					 | 
				
			||||||
        for command in &self.commands {
 | 
					 | 
				
			||||||
            commands.create_application_command(|c| {
 | 
					 | 
				
			||||||
                c.name(command.names[0]).description(command.desc);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                for arg in command.args {
 | 
					 | 
				
			||||||
                    c.create_option(|o| {
 | 
					 | 
				
			||||||
                        o.name(arg.name)
 | 
					 | 
				
			||||||
                            .description(arg.description)
 | 
					 | 
				
			||||||
                            .kind(arg.kind)
 | 
					 | 
				
			||||||
                            .required(arg.required);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                        for option in arg.options {
 | 
					 | 
				
			||||||
                            o.create_sub_option(|s| {
 | 
					 | 
				
			||||||
                                s.name(option.name)
 | 
					 | 
				
			||||||
                                    .description(option.description)
 | 
					 | 
				
			||||||
                                    .kind(option.kind)
 | 
					 | 
				
			||||||
                                    .required(option.required);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                                for sub_option in option.options {
 | 
					 | 
				
			||||||
                                    s.create_sub_option(|ss| {
 | 
					 | 
				
			||||||
                                        ss.name(sub_option.name)
 | 
					 | 
				
			||||||
                                            .description(sub_option.description)
 | 
					 | 
				
			||||||
                                            .kind(sub_option.kind)
 | 
					 | 
				
			||||||
                                            .required(sub_option.required)
 | 
					 | 
				
			||||||
                                    });
 | 
					 | 
				
			||||||
                                }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                                s
 | 
					 | 
				
			||||||
                            });
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                        o
 | 
					 | 
				
			||||||
                    });
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                c
 | 
					 | 
				
			||||||
            });
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        commands
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    pub async fn build_slash(&self, http: impl AsRef<Http>) {
 | 
					 | 
				
			||||||
        info!("Building slash commands...");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        match self.debug_guild {
 | 
					 | 
				
			||||||
            None => {
 | 
					 | 
				
			||||||
                ApplicationCommand::set_global_application_commands(&http, |c| {
 | 
					 | 
				
			||||||
                    self._populate_commands(c)
 | 
					 | 
				
			||||||
                })
 | 
					 | 
				
			||||||
                .await
 | 
					 | 
				
			||||||
                .unwrap();
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            Some(debug_guild) => {
 | 
					 | 
				
			||||||
                debug_guild
 | 
					 | 
				
			||||||
                    .set_application_commands(&http, |c| self._populate_commands(c))
 | 
					 | 
				
			||||||
                    .await
 | 
					 | 
				
			||||||
                    .unwrap();
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        info!("Slash commands built!");
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    pub async fn execute(&self, ctx: Context, interaction: ApplicationCommandInteraction) {
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            if let Some(guild_id) = interaction.guild_id {
 | 
					 | 
				
			||||||
                let pool = ctx.data.read().await.get::<SQLPool>().cloned().unwrap();
 | 
					 | 
				
			||||||
                let _ = sqlx::query!("INSERT IGNORE INTO guilds (guild) VALUES (?)", guild_id.0)
 | 
					 | 
				
			||||||
                    .execute(&pool)
 | 
					 | 
				
			||||||
                    .await;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        let command = {
 | 
					 | 
				
			||||||
            self.commands_map
 | 
					 | 
				
			||||||
                .get(&interaction.data.name)
 | 
					 | 
				
			||||||
                .expect(&format!("Received invalid command: {}", interaction.data.name))
 | 
					 | 
				
			||||||
        };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        let args = CommandOptions::new(command).populate(&interaction);
 | 
					 | 
				
			||||||
        let mut command_invoke = CommandInvoke::slash(interaction);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        for hook in command.hooks {
 | 
					 | 
				
			||||||
            match (hook.fun)(&ctx, &mut command_invoke, &args).await {
 | 
					 | 
				
			||||||
                HookResult::Continue => {}
 | 
					 | 
				
			||||||
                HookResult::Halt => {
 | 
					 | 
				
			||||||
                    return;
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        for hook in &self.hooks {
 | 
					 | 
				
			||||||
            match (hook.fun)(&ctx, &mut command_invoke, &args).await {
 | 
					 | 
				
			||||||
                HookResult::Continue => {}
 | 
					 | 
				
			||||||
                HookResult::Halt => {
 | 
					 | 
				
			||||||
                    return;
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        match command.fun {
 | 
					 | 
				
			||||||
            CommandFnType::Slash(t) => t(&ctx, &mut command_invoke, args).await,
 | 
					 | 
				
			||||||
            CommandFnType::Multi(m) => m(&ctx, &mut command_invoke).await,
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    pub async fn run_command_from_options(
 | 
					 | 
				
			||||||
        &self,
 | 
					 | 
				
			||||||
        ctx: &Context,
 | 
					 | 
				
			||||||
        command_invoke: &mut CommandInvoke,
 | 
					 | 
				
			||||||
        command_options: CommandOptions,
 | 
					 | 
				
			||||||
    ) {
 | 
					 | 
				
			||||||
        let command = {
 | 
					 | 
				
			||||||
            self.commands_map
 | 
					 | 
				
			||||||
                .get(&command_options.command)
 | 
					 | 
				
			||||||
                .expect(&format!("Received invalid command: {}", command_options.command))
 | 
					 | 
				
			||||||
        };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        match command.fun {
 | 
					 | 
				
			||||||
            CommandFnType::Slash(t) => t(&ctx, command_invoke, command_options).await,
 | 
					 | 
				
			||||||
            CommandFnType::Multi(m) => m(&ctx, command_invoke).await,
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
							
								
								
									
										182
									
								
								src/hooks.rs
									
									
									
									
									
								
							
							
						
						
									
										182
									
								
								src/hooks.rs
									
									
									
									
									
								
							@@ -1,107 +1,79 @@
 | 
				
			|||||||
use regex_command_attr::check;
 | 
					use poise::{
 | 
				
			||||||
use serenity::{client::Context, model::channel::Channel};
 | 
					    serenity_prelude::model::channel::Channel, ApplicationCommandOrAutocompleteInteraction,
 | 
				
			||||||
 | 
					 | 
				
			||||||
use crate::{
 | 
					 | 
				
			||||||
    framework::{CommandInvoke, CommandOptions, CreateGenericResponse, HookResult},
 | 
					 | 
				
			||||||
    moderation_cmds, RecordingMacros,
 | 
					 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[check]
 | 
					use crate::{consts::MACRO_MAX_COMMANDS, models::command_macro::RecordedCommand, Context, Error};
 | 
				
			||||||
pub async fn guild_only(
 | 
					 | 
				
			||||||
    ctx: &Context,
 | 
					 | 
				
			||||||
    invoke: &mut CommandInvoke,
 | 
					 | 
				
			||||||
    _args: &CommandOptions,
 | 
					 | 
				
			||||||
) -> HookResult {
 | 
					 | 
				
			||||||
    if invoke.guild_id().is_some() {
 | 
					 | 
				
			||||||
        HookResult::Continue
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
        let _ = invoke
 | 
					 | 
				
			||||||
            .respond(
 | 
					 | 
				
			||||||
                &ctx,
 | 
					 | 
				
			||||||
                CreateGenericResponse::new().content("This command can only be used in servers"),
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
            .await;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        HookResult::Halt
 | 
					async fn macro_check(ctx: Context<'_>) -> bool {
 | 
				
			||||||
 | 
					    if let Context::Application(app_ctx) = ctx {
 | 
				
			||||||
 | 
					        if let ApplicationCommandOrAutocompleteInteraction::ApplicationCommand(_) =
 | 
				
			||||||
 | 
					            app_ctx.interaction
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            if let Some(guild_id) = ctx.guild_id() {
 | 
				
			||||||
 | 
					                if ctx.command().identifying_name != "finish_macro" {
 | 
				
			||||||
 | 
					                    let mut lock = ctx.data().recording_macros.write().await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    if let Some(command_macro) = lock.get_mut(&(guild_id, ctx.author().id)) {
 | 
				
			||||||
 | 
					                        if command_macro.commands.len() >= MACRO_MAX_COMMANDS {
 | 
				
			||||||
 | 
					                            let _ = ctx.send(|m| {
 | 
				
			||||||
 | 
					                            m.ephemeral(true).content(
 | 
				
			||||||
 | 
					                                format!("{} commands already recorded. Please use `/macro finish` to end recording.", MACRO_MAX_COMMANDS),
 | 
				
			||||||
 | 
					                            )
 | 
				
			||||||
 | 
					                        })
 | 
				
			||||||
 | 
					                            .await;
 | 
				
			||||||
 | 
					                        } else {
 | 
				
			||||||
 | 
					                            let recorded = RecordedCommand {
 | 
				
			||||||
 | 
					                                action: None,
 | 
				
			||||||
 | 
					                                command_name: ctx.command().identifying_name.clone(),
 | 
				
			||||||
 | 
					                                options: Vec::from(app_ctx.args),
 | 
				
			||||||
 | 
					                            };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                            command_macro.commands.push(recorded);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                            let _ = ctx
 | 
				
			||||||
 | 
					                                .send(|m| m.ephemeral(true).content("Command recorded to macro"))
 | 
				
			||||||
 | 
					                                .await;
 | 
				
			||||||
                        }
 | 
					                        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        return false;
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    true
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[check]
 | 
					async fn check_self_permissions(ctx: Context<'_>) -> bool {
 | 
				
			||||||
pub async fn macro_check(
 | 
					    if let Some(guild) = ctx.guild() {
 | 
				
			||||||
    ctx: &Context,
 | 
					        let user_id = ctx.serenity_context().cache.current_user_id();
 | 
				
			||||||
    invoke: &mut CommandInvoke,
 | 
					 | 
				
			||||||
    args: &CommandOptions,
 | 
					 | 
				
			||||||
) -> HookResult {
 | 
					 | 
				
			||||||
    if let Some(guild_id) = invoke.guild_id() {
 | 
					 | 
				
			||||||
        if args.command != moderation_cmds::MACRO_CMD_COMMAND.names[0] {
 | 
					 | 
				
			||||||
            let active_recordings =
 | 
					 | 
				
			||||||
                ctx.data.read().await.get::<RecordingMacros>().cloned().unwrap();
 | 
					 | 
				
			||||||
            let mut lock = active_recordings.write().await;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if let Some(command_macro) = lock.get_mut(&(guild_id, invoke.author_id())) {
 | 
					 | 
				
			||||||
                if command_macro.commands.len() >= 5 {
 | 
					 | 
				
			||||||
                    let _ = invoke
 | 
					 | 
				
			||||||
                        .respond(
 | 
					 | 
				
			||||||
                            &ctx,
 | 
					 | 
				
			||||||
                            CreateGenericResponse::new().content("5 commands already recorded. Please use `/macro finish` to end recording."),
 | 
					 | 
				
			||||||
                        )
 | 
					 | 
				
			||||||
                        .await;
 | 
					 | 
				
			||||||
                } else {
 | 
					 | 
				
			||||||
                    command_macro.commands.push(args.clone());
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    let _ = invoke
 | 
					 | 
				
			||||||
                        .respond(
 | 
					 | 
				
			||||||
                            &ctx,
 | 
					 | 
				
			||||||
                            CreateGenericResponse::new().content("Command recorded to macro"),
 | 
					 | 
				
			||||||
                        )
 | 
					 | 
				
			||||||
                        .await;
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                HookResult::Halt
 | 
					 | 
				
			||||||
            } else {
 | 
					 | 
				
			||||||
                HookResult::Continue
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
            HookResult::Continue
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
        HookResult::Continue
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[check]
 | 
					 | 
				
			||||||
pub async fn check_self_permissions(
 | 
					 | 
				
			||||||
    ctx: &Context,
 | 
					 | 
				
			||||||
    invoke: &mut CommandInvoke,
 | 
					 | 
				
			||||||
    _args: &CommandOptions,
 | 
					 | 
				
			||||||
) -> HookResult {
 | 
					 | 
				
			||||||
    if let Some(guild) = invoke.guild(&ctx) {
 | 
					 | 
				
			||||||
        let user_id = ctx.cache.current_user_id();
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        let manage_webhooks =
 | 
					        let manage_webhooks =
 | 
				
			||||||
            guild.member_permissions(&ctx, user_id).await.map_or(false, |p| p.manage_webhooks());
 | 
					            guild.member_permissions(&ctx, user_id).await.map_or(false, |p| p.manage_webhooks());
 | 
				
			||||||
        let (view_channel, send_messages, embed_links) = invoke
 | 
					
 | 
				
			||||||
 | 
					        let (view_channel, send_messages, embed_links) = ctx
 | 
				
			||||||
            .channel_id()
 | 
					            .channel_id()
 | 
				
			||||||
            .to_channel_cached(&ctx)
 | 
					            .to_channel(&ctx)
 | 
				
			||||||
            .map(|c| {
 | 
					            .await
 | 
				
			||||||
 | 
					            .ok()
 | 
				
			||||||
 | 
					            .and_then(|c| {
 | 
				
			||||||
                if let Channel::Guild(channel) = c {
 | 
					                if let Channel::Guild(channel) = c {
 | 
				
			||||||
                    channel.permissions_for_user(ctx, user_id).ok()
 | 
					                    let perms = channel.permissions_for_user(&ctx, user_id).ok()?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    Some((perms.view_channel(), perms.send_messages(), perms.embed_links()))
 | 
				
			||||||
                } else {
 | 
					                } else {
 | 
				
			||||||
                    None
 | 
					                    None
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            })
 | 
					            })
 | 
				
			||||||
            .flatten()
 | 
					            .unwrap_or((false, false, false));
 | 
				
			||||||
            .map_or((false, false, false), |p| {
 | 
					 | 
				
			||||||
                (p.read_messages(), p.send_messages(), p.embed_links())
 | 
					 | 
				
			||||||
            });
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if manage_webhooks && send_messages && embed_links {
 | 
					        if manage_webhooks && send_messages && embed_links {
 | 
				
			||||||
            HookResult::Continue
 | 
					            true
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
            let _ = invoke
 | 
					            let _ = ctx
 | 
				
			||||||
                .respond(
 | 
					                .send(|m| {
 | 
				
			||||||
                    &ctx,
 | 
					                    m.content(format!(
 | 
				
			||||||
                    CreateGenericResponse::new().content(format!(
 | 
					 | 
				
			||||||
                        "Please ensure the bot has the correct permissions:
 | 
					                        "Please ensure the bot has the correct permissions:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{}     **View Channel**
 | 
					{}     **View Channel**
 | 
				
			||||||
@@ -110,43 +82,19 @@ pub async fn check_self_permissions(
 | 
				
			|||||||
{}     **Manage Webhooks**",
 | 
					{}     **Manage Webhooks**",
 | 
				
			||||||
                        if view_channel { "✅" } else { "❌" },
 | 
					                        if view_channel { "✅" } else { "❌" },
 | 
				
			||||||
                        if send_messages { "✅" } else { "❌" },
 | 
					                        if send_messages { "✅" } else { "❌" },
 | 
				
			||||||
                        if manage_webhooks { "✅" } else { "❌" },
 | 
					 | 
				
			||||||
                        if embed_links { "✅" } else { "❌" },
 | 
					                        if embed_links { "✅" } else { "❌" },
 | 
				
			||||||
                    )),
 | 
					                        if manage_webhooks { "✅" } else { "❌" },
 | 
				
			||||||
                )
 | 
					                    ))
 | 
				
			||||||
 | 
					                })
 | 
				
			||||||
                .await;
 | 
					                .await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            HookResult::Halt
 | 
					            false
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
        HookResult::Continue
 | 
					        true
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[check]
 | 
					pub async fn all_checks(ctx: Context<'_>) -> Result<bool, Error> {
 | 
				
			||||||
pub async fn check_guild_permissions(
 | 
					    Ok(macro_check(ctx).await && check_self_permissions(ctx).await)
 | 
				
			||||||
    ctx: &Context,
 | 
					 | 
				
			||||||
    invoke: &mut CommandInvoke,
 | 
					 | 
				
			||||||
    _args: &CommandOptions,
 | 
					 | 
				
			||||||
) -> HookResult {
 | 
					 | 
				
			||||||
    if let Some(guild) = invoke.guild(&ctx) {
 | 
					 | 
				
			||||||
        let permissions = guild.member_permissions(&ctx, invoke.author_id()).await.unwrap();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if !permissions.manage_guild() {
 | 
					 | 
				
			||||||
            let _ = invoke
 | 
					 | 
				
			||||||
                .respond(
 | 
					 | 
				
			||||||
                    &ctx,
 | 
					 | 
				
			||||||
                    CreateGenericResponse::new().content(
 | 
					 | 
				
			||||||
                        "You must have the \"Manage Server\" permission to use this command",
 | 
					 | 
				
			||||||
                    ),
 | 
					 | 
				
			||||||
                )
 | 
					 | 
				
			||||||
                .await;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            HookResult::Halt
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
            HookResult::Continue
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
        HookResult::Continue
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										336
									
								
								src/interval_parser.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										336
									
								
								src/interval_parser.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,336 @@
 | 
				
			|||||||
 | 
					/*
 | 
				
			||||||
 | 
					With modifications, 2022 Jude Southworth
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Original copyright notice:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Copyright 2021 Paul Colomiets
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Permission is hereby granted, free of charge, to any person obtaining a copy of this software
 | 
				
			||||||
 | 
					and associated documentation files (the "Software"), to deal in the Software without restriction,
 | 
				
			||||||
 | 
					including without limitation the rights to use, copy, modify, merge, publish, distribute,
 | 
				
			||||||
 | 
					sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
 | 
				
			||||||
 | 
					furnished to do so, subject to the following conditions:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					The above copyright notice and this permission notice shall be included in all copies or
 | 
				
			||||||
 | 
					substantial portions of the Software.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
 | 
				
			||||||
 | 
					BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 | 
				
			||||||
 | 
					NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
 | 
				
			||||||
 | 
					DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 | 
				
			||||||
 | 
					OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 | 
				
			||||||
 | 
					*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use std::{error::Error as StdError, fmt, str::Chars};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Error parsing human-friendly duration
 | 
				
			||||||
 | 
					#[derive(Debug, PartialEq, Clone)]
 | 
				
			||||||
 | 
					pub enum Error {
 | 
				
			||||||
 | 
					    /// Invalid character during parsing
 | 
				
			||||||
 | 
					    ///
 | 
				
			||||||
 | 
					    /// More specifically anything that is not alphanumeric is prohibited
 | 
				
			||||||
 | 
					    ///
 | 
				
			||||||
 | 
					    /// The field is an byte offset of the character in the string.
 | 
				
			||||||
 | 
					    InvalidCharacter(usize),
 | 
				
			||||||
 | 
					    /// Non-numeric value where number is expected
 | 
				
			||||||
 | 
					    ///
 | 
				
			||||||
 | 
					    /// This usually means that either time unit is broken into words,
 | 
				
			||||||
 | 
					    /// e.g. `m sec` instead of `msec`, or just number is omitted,
 | 
				
			||||||
 | 
					    /// for example `2 hours min` instead of `2 hours 1 min`
 | 
				
			||||||
 | 
					    ///
 | 
				
			||||||
 | 
					    /// The field is an byte offset of the errorneous character
 | 
				
			||||||
 | 
					    /// in the string.
 | 
				
			||||||
 | 
					    NumberExpected(usize),
 | 
				
			||||||
 | 
					    /// Unit in the number is not one of allowed units
 | 
				
			||||||
 | 
					    ///
 | 
				
			||||||
 | 
					    /// See documentation of `parse_duration` for the list of supported
 | 
				
			||||||
 | 
					    /// time units.
 | 
				
			||||||
 | 
					    ///
 | 
				
			||||||
 | 
					    /// The two fields are start and end (exclusive) of the slice from
 | 
				
			||||||
 | 
					    /// the original string, containing errorneous value
 | 
				
			||||||
 | 
					    UnknownUnit {
 | 
				
			||||||
 | 
					        /// Start of the invalid unit inside the original string
 | 
				
			||||||
 | 
					        start: usize,
 | 
				
			||||||
 | 
					        /// End of the invalid unit inside the original string
 | 
				
			||||||
 | 
					        end: usize,
 | 
				
			||||||
 | 
					        /// The unit verbatim
 | 
				
			||||||
 | 
					        unit: String,
 | 
				
			||||||
 | 
					        /// A number associated with the unit
 | 
				
			||||||
 | 
					        value: u64,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    /// The numeric value is too large
 | 
				
			||||||
 | 
					    ///
 | 
				
			||||||
 | 
					    /// Usually this means value is too large to be useful. If user writes
 | 
				
			||||||
 | 
					    /// data in subsecond units, then the maximum is about 3k years. When
 | 
				
			||||||
 | 
					    /// using seconds, or larger units, the limit is even larger.
 | 
				
			||||||
 | 
					    NumberOverflow,
 | 
				
			||||||
 | 
					    /// The value was an empty string (or consists only whitespace)
 | 
				
			||||||
 | 
					    Empty,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl StdError for Error {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl fmt::Display for Error {
 | 
				
			||||||
 | 
					    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
 | 
				
			||||||
 | 
					        match self {
 | 
				
			||||||
 | 
					            Error::InvalidCharacter(offset) => write!(f, "invalid character at {}", offset),
 | 
				
			||||||
 | 
					            Error::NumberExpected(offset) => write!(f, "expected number at {}", offset),
 | 
				
			||||||
 | 
					            Error::UnknownUnit { unit, value, .. } if unit.is_empty() => {
 | 
				
			||||||
 | 
					                write!(f, "time unit needed, for example {0}sec or {0}ms", value,)
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            Error::UnknownUnit { unit, .. } => {
 | 
				
			||||||
 | 
					                write!(
 | 
				
			||||||
 | 
					                    f,
 | 
				
			||||||
 | 
					                    "unknown time unit {:?}, \
 | 
				
			||||||
 | 
					                    supported units: ns, us, ms, sec, min, hours, days, \
 | 
				
			||||||
 | 
					                    weeks, months, years (and few variations)",
 | 
				
			||||||
 | 
					                    unit
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            Error::NumberOverflow => write!(f, "number is too large"),
 | 
				
			||||||
 | 
					            Error::Empty => write!(f, "value was empty"),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					trait OverflowOp: Sized {
 | 
				
			||||||
 | 
					    fn mul(self, other: Self) -> Result<Self, Error>;
 | 
				
			||||||
 | 
					    fn add(self, other: Self) -> Result<Self, Error>;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl OverflowOp for u64 {
 | 
				
			||||||
 | 
					    fn mul(self, other: Self) -> Result<Self, Error> {
 | 
				
			||||||
 | 
					        self.checked_mul(other).ok_or(Error::NumberOverflow)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    fn add(self, other: Self) -> Result<Self, Error> {
 | 
				
			||||||
 | 
					        self.checked_add(other).ok_or(Error::NumberOverflow)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Copy, Clone)]
 | 
				
			||||||
 | 
					pub struct Interval {
 | 
				
			||||||
 | 
					    pub month: u64,
 | 
				
			||||||
 | 
					    pub day: u64,
 | 
				
			||||||
 | 
					    pub sec: u64,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					struct Parser<'a> {
 | 
				
			||||||
 | 
					    iter: Chars<'a>,
 | 
				
			||||||
 | 
					    src: &'a str,
 | 
				
			||||||
 | 
					    current: (u64, u64, u64, u64),
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl<'a> Parser<'a> {
 | 
				
			||||||
 | 
					    fn off(&self) -> usize {
 | 
				
			||||||
 | 
					        self.src.len() - self.iter.as_str().len()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn parse_first_char(&mut self) -> Result<Option<u64>, Error> {
 | 
				
			||||||
 | 
					        let off = self.off();
 | 
				
			||||||
 | 
					        for c in self.iter.by_ref() {
 | 
				
			||||||
 | 
					            match c {
 | 
				
			||||||
 | 
					                '0'..='9' => {
 | 
				
			||||||
 | 
					                    return Ok(Some(c as u64 - '0' as u64));
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                c if c.is_whitespace() => continue,
 | 
				
			||||||
 | 
					                _ => {
 | 
				
			||||||
 | 
					                    return Err(Error::NumberExpected(off));
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        Ok(None)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    fn parse_unit(&mut self, n: u64, start: usize, end: usize) -> Result<(), Error> {
 | 
				
			||||||
 | 
					        let (mut month, mut day, mut sec, nsec) = match &self.src[start..end] {
 | 
				
			||||||
 | 
					            "nanos" | "nsec" | "ns" => (0, 0u64, 0u64, n),
 | 
				
			||||||
 | 
					            "usec" | "us" => (0, 0, 0u64, n.mul(1000)?),
 | 
				
			||||||
 | 
					            "millis" | "msec" | "ms" => (0, 0, 0u64, n.mul(1_000_000)?),
 | 
				
			||||||
 | 
					            "seconds" | "second" | "secs" | "sec" | "s" => (0, 0, n, 0),
 | 
				
			||||||
 | 
					            "minutes" | "minute" | "min" | "mins" | "m" => (0, 0, n.mul(60)?, 0),
 | 
				
			||||||
 | 
					            "hours" | "hour" | "hr" | "hrs" | "h" => (0, 0, n.mul(3600)?, 0),
 | 
				
			||||||
 | 
					            "days" | "day" | "d" => (0, n, 0, 0),
 | 
				
			||||||
 | 
					            "weeks" | "week" | "w" => (0, n.mul(7)?, 0, 0),
 | 
				
			||||||
 | 
					            "months" | "month" => (n, 0, 0, 0),
 | 
				
			||||||
 | 
					            "years" | "year" | "y" => (n.mul(12)?, 0, 0, 0),
 | 
				
			||||||
 | 
					            _ => {
 | 
				
			||||||
 | 
					                return Err(Error::UnknownUnit {
 | 
				
			||||||
 | 
					                    start,
 | 
				
			||||||
 | 
					                    end,
 | 
				
			||||||
 | 
					                    unit: self.src[start..end].to_string(),
 | 
				
			||||||
 | 
					                    value: n,
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        let mut nsec = self.current.3 + nsec;
 | 
				
			||||||
 | 
					        if nsec > 1_000_000_000 {
 | 
				
			||||||
 | 
					            sec += nsec / 1_000_000_000;
 | 
				
			||||||
 | 
					            nsec %= 1_000_000_000;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        sec += self.current.2;
 | 
				
			||||||
 | 
					        day += self.current.1;
 | 
				
			||||||
 | 
					        month += self.current.0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.current = (month, day, sec, nsec);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Ok(())
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn parse(mut self) -> Result<Interval, Error> {
 | 
				
			||||||
 | 
					        let mut n = self.parse_first_char()?.ok_or(Error::Empty)?;
 | 
				
			||||||
 | 
					        'outer: loop {
 | 
				
			||||||
 | 
					            let mut off = self.off();
 | 
				
			||||||
 | 
					            while let Some(c) = self.iter.next() {
 | 
				
			||||||
 | 
					                match c {
 | 
				
			||||||
 | 
					                    '0'..='9' => {
 | 
				
			||||||
 | 
					                        n = n
 | 
				
			||||||
 | 
					                            .checked_mul(10)
 | 
				
			||||||
 | 
					                            .and_then(|x| x.checked_add(c as u64 - '0' as u64))
 | 
				
			||||||
 | 
					                            .ok_or(Error::NumberOverflow)?;
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    c if c.is_whitespace() => {}
 | 
				
			||||||
 | 
					                    'a'..='z' | 'A'..='Z' => {
 | 
				
			||||||
 | 
					                        break;
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    _ => {
 | 
				
			||||||
 | 
					                        return Err(Error::InvalidCharacter(off));
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                off = self.off();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            let start = off;
 | 
				
			||||||
 | 
					            let mut off = self.off();
 | 
				
			||||||
 | 
					            while let Some(c) = self.iter.next() {
 | 
				
			||||||
 | 
					                match c {
 | 
				
			||||||
 | 
					                    '0'..='9' => {
 | 
				
			||||||
 | 
					                        self.parse_unit(n, start, off)?;
 | 
				
			||||||
 | 
					                        n = c as u64 - '0' as u64;
 | 
				
			||||||
 | 
					                        continue 'outer;
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    c if c.is_whitespace() => break,
 | 
				
			||||||
 | 
					                    'a'..='z' | 'A'..='Z' => {}
 | 
				
			||||||
 | 
					                    _ => {
 | 
				
			||||||
 | 
					                        return Err(Error::InvalidCharacter(off));
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                off = self.off();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            self.parse_unit(n, start, off)?;
 | 
				
			||||||
 | 
					            n = match self.parse_first_char()? {
 | 
				
			||||||
 | 
					                Some(n) => n,
 | 
				
			||||||
 | 
					                None => {
 | 
				
			||||||
 | 
					                    return Ok(Interval {
 | 
				
			||||||
 | 
					                        month: self.current.0,
 | 
				
			||||||
 | 
					                        day: self.current.1,
 | 
				
			||||||
 | 
					                        sec: self.current.2,
 | 
				
			||||||
 | 
					                    })
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Parse duration object `1hour 12min 5s`
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					/// The duration object is a concatenation of time spans. Where each time
 | 
				
			||||||
 | 
					/// span is an integer number and a suffix. Supported suffixes:
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					/// * `nsec`, `ns` -- nanoseconds
 | 
				
			||||||
 | 
					/// * `usec`, `us` -- microseconds
 | 
				
			||||||
 | 
					/// * `msec`, `ms` -- milliseconds
 | 
				
			||||||
 | 
					/// * `seconds`, `second`, `sec`, `s`
 | 
				
			||||||
 | 
					/// * `minutes`, `minute`, `min`, `m`
 | 
				
			||||||
 | 
					/// * `hours`, `hour`, `hr`, `h`
 | 
				
			||||||
 | 
					/// * `days`, `day`, `d`
 | 
				
			||||||
 | 
					/// * `weeks`, `week`, `w`
 | 
				
			||||||
 | 
					/// * `months`, `month`, `M` -- defined as 30.44 days
 | 
				
			||||||
 | 
					/// * `years`, `year`, `y` -- defined as 365.25 days
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					/// # Examples
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					/// ```
 | 
				
			||||||
 | 
					/// use std::time::Duration;
 | 
				
			||||||
 | 
					/// use humantime::parse_duration;
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					/// assert_eq!(parse_duration("2h 37min"), Ok(Duration::new(9420, 0)));
 | 
				
			||||||
 | 
					/// assert_eq!(parse_duration("32ms"), Ok(Duration::new(0, 32_000_000)));
 | 
				
			||||||
 | 
					/// ```
 | 
				
			||||||
 | 
					pub fn parse_duration(s: &str) -> Result<Interval, Error> {
 | 
				
			||||||
 | 
					    Parser { iter: s.to_lowercase().chars(), src: &s.to_lowercase(), current: (0, 0, 0, 0) }.parse()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[cfg(test)]
 | 
				
			||||||
 | 
					mod tests {
 | 
				
			||||||
 | 
					    use super::*;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    #[test]
 | 
				
			||||||
 | 
					    fn parse_seconds() {
 | 
				
			||||||
 | 
					        let interval = parse_duration("10 seconds").unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        assert_eq!(interval.sec, 10);
 | 
				
			||||||
 | 
					        assert_eq!(interval.day, 0);
 | 
				
			||||||
 | 
					        assert_eq!(interval.month, 0);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    #[test]
 | 
				
			||||||
 | 
					    fn parse_minutes() {
 | 
				
			||||||
 | 
					        let interval = parse_duration("10 minutes").unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        assert_eq!(interval.sec, 600);
 | 
				
			||||||
 | 
					        assert_eq!(interval.day, 0);
 | 
				
			||||||
 | 
					        assert_eq!(interval.month, 0);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    #[test]
 | 
				
			||||||
 | 
					    fn parse_hours() {
 | 
				
			||||||
 | 
					        let interval = parse_duration("10 hours").unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        assert_eq!(interval.sec, 36_000);
 | 
				
			||||||
 | 
					        assert_eq!(interval.day, 0);
 | 
				
			||||||
 | 
					        assert_eq!(interval.month, 0);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    #[test]
 | 
				
			||||||
 | 
					    fn parse_days() {
 | 
				
			||||||
 | 
					        let interval = parse_duration("10 days").unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        assert_eq!(interval.sec, 0);
 | 
				
			||||||
 | 
					        assert_eq!(interval.day, 10);
 | 
				
			||||||
 | 
					        assert_eq!(interval.month, 0);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    #[test]
 | 
				
			||||||
 | 
					    fn parse_weeks() {
 | 
				
			||||||
 | 
					        let interval = parse_duration("10 weeks").unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        assert_eq!(interval.sec, 0);
 | 
				
			||||||
 | 
					        assert_eq!(interval.day, 70);
 | 
				
			||||||
 | 
					        assert_eq!(interval.month, 0);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    #[test]
 | 
				
			||||||
 | 
					    fn parse_months() {
 | 
				
			||||||
 | 
					        let interval = parse_duration("10 months").unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        assert_eq!(interval.sec, 0);
 | 
				
			||||||
 | 
					        assert_eq!(interval.day, 0);
 | 
				
			||||||
 | 
					        assert_eq!(interval.month, 10);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    #[test]
 | 
				
			||||||
 | 
					    fn parse_years() {
 | 
				
			||||||
 | 
					        let interval = parse_duration("10 years").unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        assert_eq!(interval.sec, 0);
 | 
				
			||||||
 | 
					        assert_eq!(interval.day, 0);
 | 
				
			||||||
 | 
					        assert_eq!(interval.month, 120);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    #[test]
 | 
				
			||||||
 | 
					    fn parse_case() {
 | 
				
			||||||
 | 
					        let interval = parse_duration("200 Seconds").unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        assert_eq!(interval.sec, 200);
 | 
				
			||||||
 | 
					        assert_eq!(interval.day, 0);
 | 
				
			||||||
 | 
					        assert_eq!(interval.month, 0);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										529
									
								
								src/main.rs
									
									
									
									
									
								
							
							
						
						
									
										529
									
								
								src/main.rs
									
									
									
									
									
								
							@@ -1,366 +1,269 @@
 | 
				
			|||||||
#![feature(int_roundings)]
 | 
					#![feature(int_roundings)]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[macro_use]
 | 
					#[macro_use]
 | 
				
			||||||
extern crate lazy_static;
 | 
					extern crate lazy_static;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
mod commands;
 | 
					mod commands;
 | 
				
			||||||
mod component_models;
 | 
					mod component_models;
 | 
				
			||||||
mod consts;
 | 
					mod consts;
 | 
				
			||||||
mod framework;
 | 
					mod event_handlers;
 | 
				
			||||||
mod hooks;
 | 
					mod hooks;
 | 
				
			||||||
 | 
					mod interval_parser;
 | 
				
			||||||
mod models;
 | 
					mod models;
 | 
				
			||||||
mod sender;
 | 
					 | 
				
			||||||
mod time_parser;
 | 
					mod time_parser;
 | 
				
			||||||
 | 
					mod utils;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use std::{
 | 
					use std::{
 | 
				
			||||||
    collections::HashMap,
 | 
					    collections::HashMap,
 | 
				
			||||||
    env,
 | 
					    env,
 | 
				
			||||||
    sync::{
 | 
					    error::Error as StdError,
 | 
				
			||||||
        atomic::{AtomicBool, Ordering},
 | 
					    fmt::{Debug, Display, Formatter},
 | 
				
			||||||
        Arc,
 | 
					    path::Path,
 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use chrono_tz::Tz;
 | 
					use chrono_tz::Tz;
 | 
				
			||||||
use dotenv::dotenv;
 | 
					use log::{error, warn};
 | 
				
			||||||
use log::info;
 | 
					use poise::serenity_prelude::model::{
 | 
				
			||||||
use serenity::{
 | 
					    gateway::GatewayIntents,
 | 
				
			||||||
    async_trait,
 | 
					 | 
				
			||||||
    client::{bridge::gateway::GatewayIntents, Client},
 | 
					 | 
				
			||||||
    http::{client::Http, CacheHttp},
 | 
					 | 
				
			||||||
    model::{
 | 
					 | 
				
			||||||
        channel::GuildChannel,
 | 
					 | 
				
			||||||
        gateway::{Activity, Ready},
 | 
					 | 
				
			||||||
        guild::{Guild, GuildUnavailable},
 | 
					 | 
				
			||||||
    id::{GuildId, UserId},
 | 
					    id::{GuildId, UserId},
 | 
				
			||||||
        interactions::Interaction,
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    prelude::{Context, EventHandler, TypeMapKey},
 | 
					 | 
				
			||||||
    utils::shard_id,
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
use sqlx::mysql::MySqlPool;
 | 
					 | 
				
			||||||
use tokio::{
 | 
					 | 
				
			||||||
    sync::RwLock,
 | 
					 | 
				
			||||||
    time::{Duration, Instant},
 | 
					 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					use sqlx::{MySql, Pool};
 | 
				
			||||||
 | 
					use tokio::sync::{broadcast, broadcast::Sender, RwLock};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::{
 | 
					use crate::{
 | 
				
			||||||
    commands::{info_cmds, moderation_cmds, reminder_cmds, todo_cmds},
 | 
					    commands::{command_macro, info_cmds, moderation_cmds, reminder_cmds, todo_cmds},
 | 
				
			||||||
    component_models::ComponentDataModel,
 | 
					    consts::THEME_COLOR,
 | 
				
			||||||
    consts::{CNC_GUILD, REMIND_INTERVAL, SUBSCRIPTION_ROLES, THEME_COLOR},
 | 
					    event_handlers::listener,
 | 
				
			||||||
    framework::RegexFramework,
 | 
					    hooks::all_checks,
 | 
				
			||||||
    models::command_macro::CommandMacro,
 | 
					    models::command_macro::CommandMacro,
 | 
				
			||||||
 | 
					    utils::register_application_commands,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
struct SQLPool;
 | 
					type Database = MySql;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
impl TypeMapKey for SQLPool {
 | 
					type Error = Box<dyn std::error::Error + Send + Sync>;
 | 
				
			||||||
    type Value = MySqlPool;
 | 
					type Context<'a> = poise::Context<'a, Data, Error>;
 | 
				
			||||||
 | 
					type ApplicationContext<'a> = poise::ApplicationContext<'a, Data, Error>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub struct Data {
 | 
				
			||||||
 | 
					    database: Pool<Database>,
 | 
				
			||||||
 | 
					    http: reqwest::Client,
 | 
				
			||||||
 | 
					    recording_macros: RwLock<HashMap<(GuildId, UserId), CommandMacro<Data, Error>>>,
 | 
				
			||||||
 | 
					    popular_timezones: Vec<Tz>,
 | 
				
			||||||
 | 
					    _broadcast: Sender<()>,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
struct ReqwestClient;
 | 
					impl Debug for Data {
 | 
				
			||||||
 | 
					    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
 | 
				
			||||||
impl TypeMapKey for ReqwestClient {
 | 
					        write!(f, "Data {{ .. }}")
 | 
				
			||||||
    type Value = Arc<reqwest::Client>;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
struct PopularTimezones;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl TypeMapKey for PopularTimezones {
 | 
					 | 
				
			||||||
    type Value = Arc<Vec<Tz>>;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
struct RecordingMacros;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl TypeMapKey for RecordingMacros {
 | 
					 | 
				
			||||||
    type Value = Arc<RwLock<HashMap<(GuildId, UserId), CommandMacro>>>;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
struct Handler {
 | 
					 | 
				
			||||||
    is_loop_running: AtomicBool,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[async_trait]
 | 
					 | 
				
			||||||
impl EventHandler for Handler {
 | 
					 | 
				
			||||||
    async fn cache_ready(&self, ctx_base: Context, _guilds: Vec<GuildId>) {
 | 
					 | 
				
			||||||
        info!("Cache Ready!");
 | 
					 | 
				
			||||||
        info!("Preparing to send reminders");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if !self.is_loop_running.load(Ordering::Relaxed) {
 | 
					 | 
				
			||||||
            let ctx = ctx_base.clone();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            tokio::spawn(async move {
 | 
					 | 
				
			||||||
                let pool = ctx.data.read().await.get::<SQLPool>().cloned().unwrap();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                loop {
 | 
					 | 
				
			||||||
                    let sleep_until = Instant::now() + Duration::from_secs(*REMIND_INTERVAL);
 | 
					 | 
				
			||||||
                    let reminders = sender::Reminder::fetch_reminders(&pool).await;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    if reminders.len() > 0 {
 | 
					 | 
				
			||||||
                        info!("Preparing to send {} reminders.", reminders.len());
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                        for reminder in reminders {
 | 
					 | 
				
			||||||
                            reminder.send(pool.clone(), ctx.clone()).await;
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    tokio::time::sleep_until(sleep_until).await;
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            self.is_loop_running.swap(true, Ordering::Relaxed);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    async fn channel_delete(&self, ctx: Context, channel: &GuildChannel) {
 | 
					 | 
				
			||||||
        let pool = ctx
 | 
					 | 
				
			||||||
            .data
 | 
					 | 
				
			||||||
            .read()
 | 
					 | 
				
			||||||
            .await
 | 
					 | 
				
			||||||
            .get::<SQLPool>()
 | 
					 | 
				
			||||||
            .cloned()
 | 
					 | 
				
			||||||
            .expect("Could not get SQLPool from data");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        sqlx::query!(
 | 
					 | 
				
			||||||
            "
 | 
					 | 
				
			||||||
DELETE FROM channels WHERE channel = ?
 | 
					 | 
				
			||||||
            ",
 | 
					 | 
				
			||||||
            channel.id.as_u64()
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        .execute(&pool)
 | 
					 | 
				
			||||||
        .await
 | 
					 | 
				
			||||||
        .unwrap();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    async fn guild_create(&self, ctx: Context, guild: Guild, is_new: bool) {
 | 
					 | 
				
			||||||
        if is_new {
 | 
					 | 
				
			||||||
            let guild_id = guild.id.as_u64().to_owned();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            {
 | 
					 | 
				
			||||||
                let pool = ctx.data.read().await.get::<SQLPool>().cloned().unwrap();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                let _ = sqlx::query!("INSERT INTO guilds (guild) VALUES (?)", guild_id)
 | 
					 | 
				
			||||||
                    .execute(&pool)
 | 
					 | 
				
			||||||
                    .await;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if let Ok(token) = env::var("DISCORDBOTS_TOKEN") {
 | 
					 | 
				
			||||||
                let shard_count = ctx.cache.shard_count();
 | 
					 | 
				
			||||||
                let current_shard_id = shard_id(guild_id, shard_count);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                let guild_count = ctx
 | 
					 | 
				
			||||||
                    .cache
 | 
					 | 
				
			||||||
                    .guilds()
 | 
					 | 
				
			||||||
                    .iter()
 | 
					 | 
				
			||||||
                    .filter(|g| shard_id(g.as_u64().to_owned(), shard_count) == current_shard_id)
 | 
					 | 
				
			||||||
                    .count() as u64;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                let mut hm = HashMap::new();
 | 
					 | 
				
			||||||
                hm.insert("server_count", guild_count);
 | 
					 | 
				
			||||||
                hm.insert("shard_id", current_shard_id);
 | 
					 | 
				
			||||||
                hm.insert("shard_count", shard_count);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                let client = ctx
 | 
					 | 
				
			||||||
                    .data
 | 
					 | 
				
			||||||
                    .read()
 | 
					 | 
				
			||||||
                    .await
 | 
					 | 
				
			||||||
                    .get::<ReqwestClient>()
 | 
					 | 
				
			||||||
                    .cloned()
 | 
					 | 
				
			||||||
                    .expect("Could not get ReqwestClient from data");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                let response = client
 | 
					 | 
				
			||||||
                    .post(
 | 
					 | 
				
			||||||
                        format!(
 | 
					 | 
				
			||||||
                            "https://top.gg/api/bots/{}/stats",
 | 
					 | 
				
			||||||
                            ctx.cache.current_user_id().as_u64()
 | 
					 | 
				
			||||||
                        )
 | 
					 | 
				
			||||||
                        .as_str(),
 | 
					 | 
				
			||||||
                    )
 | 
					 | 
				
			||||||
                    .header("Authorization", token)
 | 
					 | 
				
			||||||
                    .json(&hm)
 | 
					 | 
				
			||||||
                    .send()
 | 
					 | 
				
			||||||
                    .await;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                if let Err(res) = response {
 | 
					 | 
				
			||||||
                    println!("DiscordBots Response: {:?}", res);
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    async fn guild_delete(&self, ctx: Context, incomplete: GuildUnavailable, _full: Option<Guild>) {
 | 
					 | 
				
			||||||
        let pool = ctx.data.read().await.get::<SQLPool>().cloned().unwrap();
 | 
					 | 
				
			||||||
        let _ = sqlx::query!("DELETE FROM guilds WHERE guild = ?", incomplete.id.0)
 | 
					 | 
				
			||||||
            .execute(&pool)
 | 
					 | 
				
			||||||
            .await;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    async fn ready(&self, ctx: Context, _: Ready) {
 | 
					 | 
				
			||||||
        ctx.set_activity(Activity::watching("for /remind")).await;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    async fn interaction_create(&self, ctx: Context, interaction: Interaction) {
 | 
					 | 
				
			||||||
        match interaction {
 | 
					 | 
				
			||||||
            Interaction::ApplicationCommand(application_command) => {
 | 
					 | 
				
			||||||
                let framework = ctx
 | 
					 | 
				
			||||||
                    .data
 | 
					 | 
				
			||||||
                    .read()
 | 
					 | 
				
			||||||
                    .await
 | 
					 | 
				
			||||||
                    .get::<RegexFramework>()
 | 
					 | 
				
			||||||
                    .cloned()
 | 
					 | 
				
			||||||
                    .expect("RegexFramework not found in context");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                framework.execute(ctx, application_command).await;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            Interaction::MessageComponent(component) => {
 | 
					 | 
				
			||||||
                let component_model = ComponentDataModel::from_custom_id(&component.data.custom_id);
 | 
					 | 
				
			||||||
                component_model.act(&ctx, component).await;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            _ => {}
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[tokio::main]
 | 
					struct Ended;
 | 
				
			||||||
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
 | 
					
 | 
				
			||||||
 | 
					impl Debug for Ended {
 | 
				
			||||||
 | 
					    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
 | 
				
			||||||
 | 
					        f.write_str("Process ended.")
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl Display for Ended {
 | 
				
			||||||
 | 
					    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
 | 
				
			||||||
 | 
					        f.write_str("Process ended.")
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl StdError for Ended {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[tokio::main(flavor = "multi_thread")]
 | 
				
			||||||
 | 
					async fn main() -> Result<(), Box<dyn StdError + Send + Sync>> {
 | 
				
			||||||
 | 
					    let (tx, mut rx) = broadcast::channel(16);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    tokio::select! {
 | 
				
			||||||
 | 
					        output = _main(tx) => output,
 | 
				
			||||||
 | 
					        _ = rx.recv() => Err(Box::new(Ended) as Box<dyn StdError + Send + Sync>)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async fn _main(tx: Sender<()>) -> Result<(), Box<dyn StdError + Send + Sync>> {
 | 
				
			||||||
    env_logger::init();
 | 
					    env_logger::init();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    dotenv()?;
 | 
					    if Path::new("/etc/reminder-rs/config.env").exists() {
 | 
				
			||||||
 | 
					        dotenv::from_path("/etc/reminder-rs/config.env")?;
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        let _ = dotenv::dotenv();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let token = env::var("DISCORD_TOKEN").expect("Missing DISCORD_TOKEN from environment");
 | 
					    let discord_token = env::var("DISCORD_TOKEN").expect("Missing DISCORD_TOKEN from environment");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let application_id = {
 | 
					    let options = poise::FrameworkOptions {
 | 
				
			||||||
        let http = Http::new_with_token(&token);
 | 
					        commands: vec![
 | 
				
			||||||
 | 
					            info_cmds::help(),
 | 
				
			||||||
        http.get_current_application_info().await?.id
 | 
					            info_cmds::info(),
 | 
				
			||||||
 | 
					            info_cmds::donate(),
 | 
				
			||||||
 | 
					            info_cmds::clock(),
 | 
				
			||||||
 | 
					            info_cmds::clock_context_menu(),
 | 
				
			||||||
 | 
					            info_cmds::dashboard(),
 | 
				
			||||||
 | 
					            moderation_cmds::timezone(),
 | 
				
			||||||
 | 
					            poise::Command {
 | 
				
			||||||
 | 
					                subcommands: vec![
 | 
				
			||||||
 | 
					                    moderation_cmds::set_allowed_dm(),
 | 
				
			||||||
 | 
					                    moderation_cmds::unset_allowed_dm(),
 | 
				
			||||||
 | 
					                ],
 | 
				
			||||||
 | 
					                ..moderation_cmds::allowed_dm()
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            poise::Command {
 | 
				
			||||||
 | 
					                subcommands: vec![poise::Command {
 | 
				
			||||||
 | 
					                    subcommands: vec![
 | 
				
			||||||
 | 
					                        moderation_cmds::set_ephemeral_confirmations(),
 | 
				
			||||||
 | 
					                        moderation_cmds::unset_ephemeral_confirmations(),
 | 
				
			||||||
 | 
					                    ],
 | 
				
			||||||
 | 
					                    ..moderation_cmds::ephemeral_confirmations()
 | 
				
			||||||
 | 
					                }],
 | 
				
			||||||
 | 
					                ..moderation_cmds::settings()
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            moderation_cmds::webhook(),
 | 
				
			||||||
 | 
					            poise::Command {
 | 
				
			||||||
 | 
					                subcommands: vec![
 | 
				
			||||||
 | 
					                    command_macro::delete::delete_macro(),
 | 
				
			||||||
 | 
					                    command_macro::record::finish_macro(),
 | 
				
			||||||
 | 
					                    command_macro::list::list_macro(),
 | 
				
			||||||
 | 
					                    command_macro::record::record_macro(),
 | 
				
			||||||
 | 
					                    command_macro::run::run_macro(),
 | 
				
			||||||
 | 
					                    command_macro::migrate::migrate_macro(),
 | 
				
			||||||
 | 
					                ],
 | 
				
			||||||
 | 
					                ..command_macro::macro_base()
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            reminder_cmds::pause(),
 | 
				
			||||||
 | 
					            reminder_cmds::offset(),
 | 
				
			||||||
 | 
					            reminder_cmds::nudge(),
 | 
				
			||||||
 | 
					            reminder_cmds::look(),
 | 
				
			||||||
 | 
					            reminder_cmds::delete(),
 | 
				
			||||||
 | 
					            poise::Command {
 | 
				
			||||||
 | 
					                subcommands: vec![
 | 
				
			||||||
 | 
					                    reminder_cmds::list_timer(),
 | 
				
			||||||
 | 
					                    reminder_cmds::start_timer(),
 | 
				
			||||||
 | 
					                    reminder_cmds::delete_timer(),
 | 
				
			||||||
 | 
					                ],
 | 
				
			||||||
 | 
					                ..reminder_cmds::timer_base()
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            reminder_cmds::multiline(),
 | 
				
			||||||
 | 
					            reminder_cmds::remind(),
 | 
				
			||||||
 | 
					            poise::Command {
 | 
				
			||||||
 | 
					                subcommands: vec![
 | 
				
			||||||
 | 
					                    poise::Command {
 | 
				
			||||||
 | 
					                        subcommands: vec![
 | 
				
			||||||
 | 
					                            todo_cmds::todo_guild_add(),
 | 
				
			||||||
 | 
					                            todo_cmds::todo_guild_view(),
 | 
				
			||||||
 | 
					                        ],
 | 
				
			||||||
 | 
					                        ..todo_cmds::todo_guild_base()
 | 
				
			||||||
 | 
					                    },
 | 
				
			||||||
 | 
					                    poise::Command {
 | 
				
			||||||
 | 
					                        subcommands: vec![
 | 
				
			||||||
 | 
					                            todo_cmds::todo_channel_add(),
 | 
				
			||||||
 | 
					                            todo_cmds::todo_channel_view(),
 | 
				
			||||||
 | 
					                        ],
 | 
				
			||||||
 | 
					                        ..todo_cmds::todo_channel_base()
 | 
				
			||||||
 | 
					                    },
 | 
				
			||||||
 | 
					                    poise::Command {
 | 
				
			||||||
 | 
					                        subcommands: vec![todo_cmds::todo_user_add(), todo_cmds::todo_user_view()],
 | 
				
			||||||
 | 
					                        ..todo_cmds::todo_user_base()
 | 
				
			||||||
 | 
					                    },
 | 
				
			||||||
 | 
					                ],
 | 
				
			||||||
 | 
					                ..todo_cmds::todo_base()
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        allowed_mentions: None,
 | 
				
			||||||
 | 
					        command_check: Some(|ctx| Box::pin(all_checks(ctx))),
 | 
				
			||||||
 | 
					        event_handler: |ctx, event, _framework, data| Box::pin(listener(ctx, event, data)),
 | 
				
			||||||
 | 
					        on_error: |error| {
 | 
				
			||||||
 | 
					            Box::pin(async move {
 | 
				
			||||||
 | 
					                match error {
 | 
				
			||||||
 | 
					                    poise::FrameworkError::CommandCheckFailed { .. } => {
 | 
				
			||||||
 | 
					                        // suppress error
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    error => {
 | 
				
			||||||
 | 
					                        if let Err(e) = poise::builtins::on_error(error).await {
 | 
				
			||||||
 | 
					                            log::error!("Error while handling error: {}", e);
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        ..Default::default()
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let dm_enabled = env::var("DM_ENABLED").map_or(true, |var| var == "1");
 | 
					    let database =
 | 
				
			||||||
 | 
					        Pool::connect(&env::var("DATABASE_URL").expect("No database URL provided")).await.unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let framework = RegexFramework::new()
 | 
					    sqlx::migrate!().run(&database).await?;
 | 
				
			||||||
        .ignore_bots(env::var("IGNORE_BOTS").map_or(true, |var| var == "1"))
 | 
					 | 
				
			||||||
        .debug_guild(env::var("DEBUG_GUILD").map_or(None, |g| {
 | 
					 | 
				
			||||||
            Some(GuildId(g.parse::<u64>().expect("DEBUG_GUILD must be a guild ID")))
 | 
					 | 
				
			||||||
        }))
 | 
					 | 
				
			||||||
        .dm_enabled(dm_enabled)
 | 
					 | 
				
			||||||
        // info commands
 | 
					 | 
				
			||||||
        .add_command(&info_cmds::HELP_COMMAND)
 | 
					 | 
				
			||||||
        .add_command(&info_cmds::INFO_COMMAND)
 | 
					 | 
				
			||||||
        .add_command(&info_cmds::DONATE_COMMAND)
 | 
					 | 
				
			||||||
        .add_command(&info_cmds::DASHBOARD_COMMAND)
 | 
					 | 
				
			||||||
        .add_command(&info_cmds::CLOCK_COMMAND)
 | 
					 | 
				
			||||||
        // reminder commands
 | 
					 | 
				
			||||||
        .add_command(&reminder_cmds::TIMER_COMMAND)
 | 
					 | 
				
			||||||
        .add_command(&reminder_cmds::REMIND_COMMAND)
 | 
					 | 
				
			||||||
        // management commands
 | 
					 | 
				
			||||||
        .add_command(&reminder_cmds::DELETE_COMMAND)
 | 
					 | 
				
			||||||
        .add_command(&reminder_cmds::LOOK_COMMAND)
 | 
					 | 
				
			||||||
        .add_command(&reminder_cmds::PAUSE_COMMAND)
 | 
					 | 
				
			||||||
        .add_command(&reminder_cmds::OFFSET_COMMAND)
 | 
					 | 
				
			||||||
        .add_command(&reminder_cmds::NUDGE_COMMAND)
 | 
					 | 
				
			||||||
        // to-do commands
 | 
					 | 
				
			||||||
        .add_command(&todo_cmds::TODO_COMMAND)
 | 
					 | 
				
			||||||
        // moderation commands
 | 
					 | 
				
			||||||
        .add_command(&moderation_cmds::TIMEZONE_COMMAND)
 | 
					 | 
				
			||||||
        .add_command(&moderation_cmds::MACRO_CMD_COMMAND)
 | 
					 | 
				
			||||||
        .add_hook(&hooks::CHECK_SELF_PERMISSIONS_HOOK)
 | 
					 | 
				
			||||||
        .add_hook(&hooks::MACRO_CHECK_HOOK);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let framework_arc = Arc::new(framework);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let mut client = Client::builder(&token)
 | 
					 | 
				
			||||||
        .intents(GatewayIntents::GUILDS)
 | 
					 | 
				
			||||||
        .application_id(application_id.0)
 | 
					 | 
				
			||||||
        .event_handler(Handler { is_loop_running: AtomicBool::from(false) })
 | 
					 | 
				
			||||||
        .await
 | 
					 | 
				
			||||||
        .expect("Error occurred creating client");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        let pool = MySqlPool::connect(
 | 
					 | 
				
			||||||
            &env::var("DATABASE_URL").expect("Missing DATABASE_URL from environment"),
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        .await
 | 
					 | 
				
			||||||
        .unwrap();
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let popular_timezones = sqlx::query!(
 | 
					    let popular_timezones = sqlx::query!(
 | 
				
			||||||
            "SELECT timezone FROM users GROUP BY timezone ORDER BY COUNT(timezone) DESC LIMIT 21"
 | 
					        "SELECT IFNULL(timezone, 'UTC') AS timezone
 | 
				
			||||||
 | 
					        FROM users
 | 
				
			||||||
 | 
					        WHERE timezone IS NOT NULL
 | 
				
			||||||
 | 
					        GROUP BY timezone
 | 
				
			||||||
 | 
					        ORDER BY COUNT(timezone) DESC
 | 
				
			||||||
 | 
					        LIMIT 21"
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
        .fetch_all(&pool)
 | 
					    .fetch_all(&database)
 | 
				
			||||||
    .await
 | 
					    .await
 | 
				
			||||||
    .unwrap()
 | 
					    .unwrap()
 | 
				
			||||||
    .iter()
 | 
					    .iter()
 | 
				
			||||||
    .map(|t| t.timezone.parse::<Tz>().unwrap())
 | 
					    .map(|t| t.timezone.parse::<Tz>().unwrap())
 | 
				
			||||||
    .collect::<Vec<Tz>>();
 | 
					    .collect::<Vec<Tz>>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        let mut data = client.data.write().await;
 | 
					    poise::Framework::builder()
 | 
				
			||||||
 | 
					        .token(discord_token)
 | 
				
			||||||
 | 
					        .setup(move |ctx, _bot, framework| {
 | 
				
			||||||
 | 
					            Box::pin(async move {
 | 
				
			||||||
 | 
					                register_application_commands(ctx, framework, None).await.unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        data.insert::<SQLPool>(pool);
 | 
					                let kill_tx = tx.clone();
 | 
				
			||||||
        data.insert::<PopularTimezones>(Arc::new(popular_timezones));
 | 
					                let kill_recv = tx.subscribe();
 | 
				
			||||||
        data.insert::<ReqwestClient>(Arc::new(reqwest::Client::new()));
 | 
					
 | 
				
			||||||
        data.insert::<RegexFramework>(framework_arc.clone());
 | 
					                let ctx1 = ctx.clone();
 | 
				
			||||||
        data.insert::<RecordingMacros>(Arc::new(RwLock::new(HashMap::new())));
 | 
					                let ctx2 = ctx.clone();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                let pool1 = database.clone();
 | 
				
			||||||
 | 
					                let pool2 = database.clone();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                let run_settings = env::var("DONTRUN").unwrap_or_else(|_| "".to_string());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if !run_settings.contains("postman") {
 | 
				
			||||||
 | 
					                    tokio::spawn(async move {
 | 
				
			||||||
 | 
					                        match postman::initialize(kill_recv, ctx1, &pool1).await {
 | 
				
			||||||
 | 
					                            Ok(_) => {}
 | 
				
			||||||
 | 
					                            Err(e) => {
 | 
				
			||||||
 | 
					                                error!("postman exiting: {}", e);
 | 
				
			||||||
                            }
 | 
					                            }
 | 
				
			||||||
 | 
					                        };
 | 
				
			||||||
    framework_arc.build_slash(&client.cache_and_http.http).await;
 | 
					                    });
 | 
				
			||||||
 | 
					 | 
				
			||||||
    if let Ok((Some(lower), Some(upper))) = env::var("SHARD_RANGE").map(|sr| {
 | 
					 | 
				
			||||||
        let mut split =
 | 
					 | 
				
			||||||
            sr.split(',').map(|val| val.parse::<u64>().expect("SHARD_RANGE not an integer"));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        (split.next(), split.next())
 | 
					 | 
				
			||||||
    }) {
 | 
					 | 
				
			||||||
        let total_shards = env::var("SHARD_COUNT")
 | 
					 | 
				
			||||||
            .map(|shard_count| shard_count.parse::<u64>().ok())
 | 
					 | 
				
			||||||
            .ok()
 | 
					 | 
				
			||||||
            .flatten()
 | 
					 | 
				
			||||||
            .expect("No SHARD_COUNT provided, but SHARD_RANGE was provided");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        assert!(lower < upper, "SHARD_RANGE lower limit is not less than the upper limit");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        info!("Starting client fragment with shards {}-{}/{}", lower, upper, total_shards);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        client.start_shard_range([lower, upper], total_shards).await?;
 | 
					 | 
				
			||||||
    } else if let Ok(total_shards) = env::var("SHARD_COUNT")
 | 
					 | 
				
			||||||
        .map(|shard_count| shard_count.parse::<u64>().expect("SHARD_COUNT not an integer"))
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        info!("Starting client with {} shards", total_shards);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        client.start_shards(total_shards).await?;
 | 
					 | 
				
			||||||
                } else {
 | 
					                } else {
 | 
				
			||||||
        info!("Starting client as autosharded");
 | 
					                    warn!("Not running postman");
 | 
				
			||||||
 | 
					 | 
				
			||||||
        client.start_autosharded().await?;
 | 
					 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if !run_settings.contains("web") {
 | 
				
			||||||
 | 
					                    tokio::spawn(async move {
 | 
				
			||||||
 | 
					                        reminder_web::initialize(kill_tx, ctx2, pool2).await.unwrap();
 | 
				
			||||||
 | 
					                    });
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    warn!("Not running web");
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                Ok(Data {
 | 
				
			||||||
 | 
					                    http: reqwest::Client::new(),
 | 
				
			||||||
 | 
					                    database,
 | 
				
			||||||
 | 
					                    popular_timezones,
 | 
				
			||||||
 | 
					                    recording_macros: Default::default(),
 | 
				
			||||||
 | 
					                    _broadcast: tx,
 | 
				
			||||||
 | 
					                })
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        .options(options)
 | 
				
			||||||
 | 
					        .intents(GatewayIntents::GUILDS)
 | 
				
			||||||
 | 
					        .run_autosharded()
 | 
				
			||||||
 | 
					        .await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Ok(())
 | 
					    Ok(())
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					 | 
				
			||||||
pub async fn check_subscription(cache_http: impl CacheHttp, user_id: impl Into<UserId>) -> bool {
 | 
					 | 
				
			||||||
    if let Some(subscription_guild) = *CNC_GUILD {
 | 
					 | 
				
			||||||
        let guild_member = GuildId(subscription_guild).member(cache_http, user_id).await;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if let Ok(member) = guild_member {
 | 
					 | 
				
			||||||
            for role in member.roles {
 | 
					 | 
				
			||||||
                if SUBSCRIPTION_ROLES.contains(role.as_u64()) {
 | 
					 | 
				
			||||||
                    return true;
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        false
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
        true
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
pub async fn check_guild_subscription(
 | 
					 | 
				
			||||||
    cache_http: impl CacheHttp,
 | 
					 | 
				
			||||||
    guild_id: impl Into<GuildId>,
 | 
					 | 
				
			||||||
) -> bool {
 | 
					 | 
				
			||||||
    if let Some(guild) = cache_http.cache().unwrap().guild(guild_id) {
 | 
					 | 
				
			||||||
        let owner = guild.owner_id;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        check_subscription(&cache_http, owner).await
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
        false
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,5 @@
 | 
				
			|||||||
use chrono::NaiveDateTime;
 | 
					use chrono::NaiveDateTime;
 | 
				
			||||||
use serenity::model::channel::Channel;
 | 
					use poise::serenity_prelude::model::channel::Channel;
 | 
				
			||||||
use sqlx::MySqlPool;
 | 
					use sqlx::MySqlPool;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub struct ChannelData {
 | 
					pub struct ChannelData {
 | 
				
			||||||
@@ -22,9 +22,7 @@ impl ChannelData {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        if let Ok(c) = sqlx::query_as_unchecked!(
 | 
					        if let Ok(c) = sqlx::query_as_unchecked!(
 | 
				
			||||||
            Self,
 | 
					            Self,
 | 
				
			||||||
            "
 | 
					            "SELECT id, name, nudge, blacklisted, webhook_id, webhook_token, paused, paused_until FROM channels WHERE channel = ?",
 | 
				
			||||||
SELECT id, name, nudge, blacklisted, webhook_id, webhook_token, paused, paused_until FROM channels WHERE channel = ?
 | 
					 | 
				
			||||||
            ",
 | 
					 | 
				
			||||||
            channel_id
 | 
					            channel_id
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        .fetch_one(pool)
 | 
					        .fetch_one(pool)
 | 
				
			||||||
@@ -37,9 +35,7 @@ SELECT id, name, nudge, blacklisted, webhook_id, webhook_token, paused, paused_u
 | 
				
			|||||||
            let (guild_id, channel_name) = if let Some((a, b)) = props { (Some(a), Some(b)) } else { (None, None) };
 | 
					            let (guild_id, channel_name) = if let Some((a, b)) = props { (Some(a), Some(b)) } else { (None, None) };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            sqlx::query!(
 | 
					            sqlx::query!(
 | 
				
			||||||
                "
 | 
					                "INSERT IGNORE INTO channels (channel, name, guild_id) VALUES (?, ?, (SELECT id FROM guilds WHERE guild = ?))",
 | 
				
			||||||
INSERT IGNORE INTO channels (channel, name, guild_id) VALUES (?, ?, (SELECT id FROM guilds WHERE guild = ?))
 | 
					 | 
				
			||||||
                ",
 | 
					 | 
				
			||||||
                channel_id,
 | 
					                channel_id,
 | 
				
			||||||
                channel_name,
 | 
					                channel_name,
 | 
				
			||||||
                guild_id
 | 
					                guild_id
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,33 +1,77 @@
 | 
				
			|||||||
use serenity::{client::Context, model::id::GuildId};
 | 
					use poise::serenity_prelude::model::{
 | 
				
			||||||
 | 
					    application::interaction::application_command::CommandDataOption, id::GuildId,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					use serde::{Deserialize, Serialize};
 | 
				
			||||||
 | 
					use serde_json::Value;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::{framework::CommandOptions, SQLPool};
 | 
					use crate::{Context, Data, Error};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub struct CommandMacro {
 | 
					type Func<U, E> = for<'a> fn(
 | 
				
			||||||
 | 
					    poise::ApplicationContext<'a, U, E>,
 | 
				
			||||||
 | 
					) -> poise::BoxFuture<'a, Result<(), poise::FrameworkError<'a, U, E>>>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fn default_none<U, E>() -> Option<Func<U, E>> {
 | 
				
			||||||
 | 
					    None
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Serialize, Deserialize)]
 | 
				
			||||||
 | 
					pub struct RecordedCommand<U, E> {
 | 
				
			||||||
 | 
					    #[serde(skip)]
 | 
				
			||||||
 | 
					    #[serde(default = "default_none::<U, E>")]
 | 
				
			||||||
 | 
					    pub action: Option<Func<U, E>>,
 | 
				
			||||||
 | 
					    pub command_name: String,
 | 
				
			||||||
 | 
					    pub options: Vec<CommandDataOption>,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub struct CommandMacro<U, E> {
 | 
				
			||||||
    pub guild_id: GuildId,
 | 
					    pub guild_id: GuildId,
 | 
				
			||||||
    pub name: String,
 | 
					    pub name: String,
 | 
				
			||||||
    pub description: Option<String>,
 | 
					    pub description: Option<String>,
 | 
				
			||||||
    pub commands: Vec<CommandOptions>,
 | 
					    pub commands: Vec<RecordedCommand<U, E>>,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
impl CommandMacro {
 | 
					pub struct RawCommandMacro {
 | 
				
			||||||
    pub async fn from_guild(ctx: &Context, guild_id: impl Into<GuildId>) -> Vec<Self> {
 | 
					    pub guild_id: GuildId,
 | 
				
			||||||
        let pool = ctx.data.read().await.get::<SQLPool>().cloned().unwrap();
 | 
					    pub name: String,
 | 
				
			||||||
        let guild_id = guild_id.into();
 | 
					    pub description: Option<String>,
 | 
				
			||||||
 | 
					    pub commands: Value,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        sqlx::query!(
 | 
					pub async fn guild_command_macro(
 | 
				
			||||||
            "SELECT * FROM macro WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?)",
 | 
					    ctx: &Context<'_>,
 | 
				
			||||||
            guild_id.0
 | 
					    name: &str,
 | 
				
			||||||
 | 
					) -> Option<CommandMacro<Data, Error>> {
 | 
				
			||||||
 | 
					    let row = sqlx::query!(
 | 
				
			||||||
 | 
					        "
 | 
				
			||||||
 | 
					SELECT * FROM macro WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?) AND name = ?
 | 
				
			||||||
 | 
					        ",
 | 
				
			||||||
 | 
					        ctx.guild_id().unwrap().0,
 | 
				
			||||||
 | 
					        name
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
        .fetch_all(&pool)
 | 
					    .fetch_one(&ctx.data().database)
 | 
				
			||||||
    .await
 | 
					    .await
 | 
				
			||||||
        .unwrap()
 | 
					    .ok()?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let mut commands: Vec<RecordedCommand<Data, Error>> =
 | 
				
			||||||
 | 
					        serde_json::from_str(&row.commands).unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for recorded_command in &mut commands {
 | 
				
			||||||
 | 
					        let command = &ctx
 | 
				
			||||||
 | 
					            .framework()
 | 
				
			||||||
 | 
					            .options()
 | 
				
			||||||
 | 
					            .commands
 | 
				
			||||||
            .iter()
 | 
					            .iter()
 | 
				
			||||||
        .map(|row| Self {
 | 
					            .find(|c| c.identifying_name == recorded_command.command_name);
 | 
				
			||||||
            guild_id,
 | 
					
 | 
				
			||||||
            name: row.name.clone(),
 | 
					        recorded_command.action = command.map(|c| c.slash_action).flatten();
 | 
				
			||||||
            description: row.description.clone(),
 | 
					 | 
				
			||||||
            commands: serde_json::from_str(&row.commands).unwrap(),
 | 
					 | 
				
			||||||
        })
 | 
					 | 
				
			||||||
        .collect::<Vec<Self>>()
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let command_macro = CommandMacro {
 | 
				
			||||||
 | 
					        guild_id: ctx.guild_id().unwrap(),
 | 
				
			||||||
 | 
					        name: row.name,
 | 
				
			||||||
 | 
					        description: row.description,
 | 
				
			||||||
 | 
					        commands,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Some(command_macro)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										48
									
								
								src/models/guild_data.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								src/models/guild_data.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,48 @@
 | 
				
			|||||||
 | 
					use poise::serenity_prelude::GuildId;
 | 
				
			||||||
 | 
					use sqlx::MySqlPool;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub struct GuildData {
 | 
				
			||||||
 | 
					    pub ephemeral_confirmations: bool,
 | 
				
			||||||
 | 
					    pub id: u32,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl GuildData {
 | 
				
			||||||
 | 
					    pub async fn from_guild(
 | 
				
			||||||
 | 
					        guild_id: GuildId,
 | 
				
			||||||
 | 
					        pool: &MySqlPool,
 | 
				
			||||||
 | 
					    ) -> Result<Self, Box<dyn std::error::Error + Sync + Send>> {
 | 
				
			||||||
 | 
					        if let Ok(c) = sqlx::query_as_unchecked!(
 | 
				
			||||||
 | 
					            Self,
 | 
				
			||||||
 | 
					            "SELECT id, ephemeral_confirmations FROM guilds WHERE guild = ?",
 | 
				
			||||||
 | 
					            guild_id.0
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        .fetch_one(pool)
 | 
				
			||||||
 | 
					        .await
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            Ok(c)
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            sqlx::query!("INSERT IGNORE INTO guilds (guild) VALUES (?)", guild_id.0)
 | 
				
			||||||
 | 
					                .execute(&pool.clone())
 | 
				
			||||||
 | 
					                .await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            Ok(sqlx::query_as_unchecked!(
 | 
				
			||||||
 | 
					                Self,
 | 
				
			||||||
 | 
					                "SELECT id, ephemeral_confirmations FROM guilds WHERE guild = ?",
 | 
				
			||||||
 | 
					                guild_id.0
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            .fetch_one(pool)
 | 
				
			||||||
 | 
					            .await?)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub async fn commit_changes(&self, pool: &MySqlPool) {
 | 
				
			||||||
 | 
					        sqlx::query!(
 | 
				
			||||||
 | 
					            "UPDATE guilds SET ephemeral_confirmations = ? WHERE id = ?",
 | 
				
			||||||
 | 
					            self.ephemeral_confirmations,
 | 
				
			||||||
 | 
					            self.id
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        .execute(pool)
 | 
				
			||||||
 | 
					        .await
 | 
				
			||||||
 | 
					        .unwrap();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,66 +1,97 @@
 | 
				
			|||||||
pub mod channel_data;
 | 
					pub mod channel_data;
 | 
				
			||||||
pub mod command_macro;
 | 
					pub mod command_macro;
 | 
				
			||||||
 | 
					pub mod guild_data;
 | 
				
			||||||
pub mod reminder;
 | 
					pub mod reminder;
 | 
				
			||||||
pub mod timer;
 | 
					pub mod timer;
 | 
				
			||||||
pub mod user_data;
 | 
					pub mod user_data;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use chrono_tz::Tz;
 | 
					use chrono_tz::Tz;
 | 
				
			||||||
use serenity::{
 | 
					use poise::serenity_prelude::{async_trait, model::id::UserId, ChannelType};
 | 
				
			||||||
    async_trait,
 | 
					 | 
				
			||||||
    model::id::{ChannelId, UserId},
 | 
					 | 
				
			||||||
    prelude::Context,
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::{
 | 
					use crate::{
 | 
				
			||||||
    models::{channel_data::ChannelData, user_data::UserData},
 | 
					    models::{channel_data::ChannelData, guild_data::GuildData, user_data::UserData},
 | 
				
			||||||
    SQLPool,
 | 
					    CommandMacro, Context, Data, Error, GuildId,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[async_trait]
 | 
					#[async_trait]
 | 
				
			||||||
pub trait CtxData {
 | 
					pub trait CtxData {
 | 
				
			||||||
    async fn user_data<U: Into<UserId> + Send + Sync>(
 | 
					    async fn user_data<U: Into<UserId> + Send>(&self, user_id: U) -> Result<UserData, Error>;
 | 
				
			||||||
        &self,
 | 
					 | 
				
			||||||
        user_id: U,
 | 
					 | 
				
			||||||
    ) -> Result<UserData, Box<dyn std::error::Error + Sync + Send>>;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async fn timezone<U: Into<UserId> + Send + Sync>(&self, user_id: U) -> Tz;
 | 
					    async fn author_data(&self) -> Result<UserData, Error>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async fn channel_data<C: Into<ChannelId> + Send + Sync>(
 | 
					    async fn guild_data(&self) -> Option<Result<GuildData, Error>>;
 | 
				
			||||||
        &self,
 | 
					
 | 
				
			||||||
        channel_id: C,
 | 
					    async fn timezone(&self) -> Tz;
 | 
				
			||||||
    ) -> Result<ChannelData, Box<dyn std::error::Error + Sync + Send>>;
 | 
					
 | 
				
			||||||
 | 
					    async fn channel_data(&self) -> Result<ChannelData, Error>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async fn command_macros(&self) -> Result<Vec<CommandMacro<Data, Error>>, Error>;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[async_trait]
 | 
					#[async_trait]
 | 
				
			||||||
impl CtxData for Context {
 | 
					impl CtxData for Context<'_> {
 | 
				
			||||||
    async fn user_data<U: Into<UserId> + Send + Sync>(
 | 
					    async fn user_data<U: Into<UserId> + Send>(&self, user_id: U) -> Result<UserData, Error> {
 | 
				
			||||||
        &self,
 | 
					        UserData::from_user(user_id, &self.serenity_context(), &self.data().database).await
 | 
				
			||||||
        user_id: U,
 | 
					 | 
				
			||||||
    ) -> Result<UserData, Box<dyn std::error::Error + Sync + Send>> {
 | 
					 | 
				
			||||||
        let user_id = user_id.into();
 | 
					 | 
				
			||||||
        let pool = self.data.read().await.get::<SQLPool>().cloned().unwrap();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        let user = user_id.to_user(self).await.unwrap();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        UserData::from_user(&user, &self, &pool).await
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async fn timezone<U: Into<UserId> + Send + Sync>(&self, user_id: U) -> Tz {
 | 
					    async fn author_data(&self) -> Result<UserData, Error> {
 | 
				
			||||||
        let user_id = user_id.into();
 | 
					        UserData::from_user(&self.author().id, &self.serenity_context(), &self.data().database)
 | 
				
			||||||
        let pool = self.data.read().await.get::<SQLPool>().cloned().unwrap();
 | 
					            .await
 | 
				
			||||||
 | 
					 | 
				
			||||||
        UserData::timezone_of(user_id, &pool).await
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async fn channel_data<C: Into<ChannelId> + Send + Sync>(
 | 
					    async fn guild_data(&self) -> Option<Result<GuildData, Error>> {
 | 
				
			||||||
        &self,
 | 
					        if let Some(guild_id) = self.guild_id() {
 | 
				
			||||||
        channel_id: C,
 | 
					            Some(GuildData::from_guild(guild_id, &self.data().database).await)
 | 
				
			||||||
    ) -> Result<ChannelData, Box<dyn std::error::Error + Sync + Send>> {
 | 
					        } else {
 | 
				
			||||||
        let channel_id = channel_id.into();
 | 
					            None
 | 
				
			||||||
        let pool = self.data.read().await.get::<SQLPool>().cloned().unwrap();
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        let channel = channel_id.to_channel_cached(&self).unwrap();
 | 
					    async fn timezone(&self) -> Tz {
 | 
				
			||||||
 | 
					        UserData::timezone_of(self.author().id, &self.data().database).await
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        ChannelData::from_channel(&channel, &pool).await
 | 
					    async fn channel_data(&self) -> Result<ChannelData, Box<dyn std::error::Error + Sync + Send>> {
 | 
				
			||||||
 | 
					        // If we're in a thread, get the parent channel.
 | 
				
			||||||
 | 
					        let recv_channel = self.channel_id().to_channel(&self).await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let channel = match recv_channel.guild() {
 | 
				
			||||||
 | 
					            Some(guild_channel) => {
 | 
				
			||||||
 | 
					                if guild_channel.kind == ChannelType::PublicThread {
 | 
				
			||||||
 | 
					                    guild_channel.parent_id.unwrap().to_channel_cached(&self).unwrap()
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    self.channel_id().to_channel_cached(&self).unwrap()
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            None => self.channel_id().to_channel_cached(&self).unwrap(),
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        ChannelData::from_channel(&channel, &self.data().database).await
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async fn command_macros(&self) -> Result<Vec<CommandMacro<Data, Error>>, Error> {
 | 
				
			||||||
 | 
					        self.data().command_macros(self.guild_id().unwrap()).await
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl Data {
 | 
				
			||||||
 | 
					    pub(crate) async fn command_macros(
 | 
				
			||||||
 | 
					        &self,
 | 
				
			||||||
 | 
					        guild_id: GuildId,
 | 
				
			||||||
 | 
					    ) -> Result<Vec<CommandMacro<Data, Error>>, Error> {
 | 
				
			||||||
 | 
					        let rows = sqlx::query!(
 | 
				
			||||||
 | 
					            "SELECT name, description, commands FROM macro WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?)",
 | 
				
			||||||
 | 
					            guild_id.0
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        .fetch_all(&self.database)
 | 
				
			||||||
 | 
					        .await?.iter().map(|row| CommandMacro {
 | 
				
			||||||
 | 
					            guild_id,
 | 
				
			||||||
 | 
					            name: row.name.clone(),
 | 
				
			||||||
 | 
					            description: row.description.clone(),
 | 
				
			||||||
 | 
					            commands: serde_json::from_str(&row.commands).unwrap(),
 | 
				
			||||||
 | 
					        }).collect();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Ok(rows)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,27 +2,26 @@ use std::{collections::HashSet, fmt::Display};
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
use chrono::{Duration, NaiveDateTime, Utc};
 | 
					use chrono::{Duration, NaiveDateTime, Utc};
 | 
				
			||||||
use chrono_tz::Tz;
 | 
					use chrono_tz::Tz;
 | 
				
			||||||
use serenity::{
 | 
					use poise::serenity_prelude::{
 | 
				
			||||||
    client::Context,
 | 
					 | 
				
			||||||
    http::CacheHttp,
 | 
					    http::CacheHttp,
 | 
				
			||||||
    model::{
 | 
					    model::{
 | 
				
			||||||
        channel::GuildChannel,
 | 
					        channel::GuildChannel,
 | 
				
			||||||
        id::{ChannelId, GuildId, UserId},
 | 
					        id::{ChannelId, GuildId, UserId},
 | 
				
			||||||
        webhook::Webhook,
 | 
					        webhook::Webhook,
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    Result as SerenityResult,
 | 
					    ChannelType, Result as SerenityResult,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
use sqlx::MySqlPool;
 | 
					use sqlx::MySqlPool;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::{
 | 
					use crate::{
 | 
				
			||||||
    consts,
 | 
					    consts::{DAY, DEFAULT_AVATAR, MAX_TIME, MIN_INTERVAL},
 | 
				
			||||||
    consts::{MAX_TIME, MIN_INTERVAL},
 | 
					    interval_parser::Interval,
 | 
				
			||||||
    models::{
 | 
					    models::{
 | 
				
			||||||
        channel_data::ChannelData,
 | 
					        channel_data::ChannelData,
 | 
				
			||||||
        reminder::{content::Content, errors::ReminderError, helper::generate_uid, Reminder},
 | 
					        reminder::{content::Content, errors::ReminderError, helper::generate_uid, Reminder},
 | 
				
			||||||
        user_data::UserData,
 | 
					        user_data::UserData,
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    SQLPool,
 | 
					    Context,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async fn create_webhook(
 | 
					async fn create_webhook(
 | 
				
			||||||
@@ -30,7 +29,7 @@ async fn create_webhook(
 | 
				
			|||||||
    channel: GuildChannel,
 | 
					    channel: GuildChannel,
 | 
				
			||||||
    name: impl Display,
 | 
					    name: impl Display,
 | 
				
			||||||
) -> SerenityResult<Webhook> {
 | 
					) -> SerenityResult<Webhook> {
 | 
				
			||||||
    channel.create_webhook_with_avatar(ctx.http(), name, consts::DEFAULT_AVATAR.clone()).await
 | 
					    channel.create_webhook_with_avatar(ctx.http(), name, DEFAULT_AVATAR.clone()).await
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[derive(Hash, PartialEq, Eq)]
 | 
					#[derive(Hash, PartialEq, Eq)]
 | 
				
			||||||
@@ -52,9 +51,12 @@ pub struct ReminderBuilder {
 | 
				
			|||||||
    pool: MySqlPool,
 | 
					    pool: MySqlPool,
 | 
				
			||||||
    uid: String,
 | 
					    uid: String,
 | 
				
			||||||
    channel: u32,
 | 
					    channel: u32,
 | 
				
			||||||
 | 
					    thread_id: Option<u64>,
 | 
				
			||||||
    utc_time: NaiveDateTime,
 | 
					    utc_time: NaiveDateTime,
 | 
				
			||||||
    timezone: String,
 | 
					    timezone: String,
 | 
				
			||||||
    interval: Option<i64>,
 | 
					    interval_seconds: Option<i64>,
 | 
				
			||||||
 | 
					    interval_days: Option<i64>,
 | 
				
			||||||
 | 
					    interval_months: Option<i64>,
 | 
				
			||||||
    expires: Option<NaiveDateTime>,
 | 
					    expires: Option<NaiveDateTime>,
 | 
				
			||||||
    content: String,
 | 
					    content: String,
 | 
				
			||||||
    tts: bool,
 | 
					    tts: bool,
 | 
				
			||||||
@@ -86,7 +88,9 @@ INSERT INTO reminders (
 | 
				
			|||||||
    `channel_id`,
 | 
					    `channel_id`,
 | 
				
			||||||
    `utc_time`,
 | 
					    `utc_time`,
 | 
				
			||||||
    `timezone`,
 | 
					    `timezone`,
 | 
				
			||||||
    `interval`,
 | 
					    `interval_seconds`,
 | 
				
			||||||
 | 
					    `interval_days`,
 | 
				
			||||||
 | 
					    `interval_months`,
 | 
				
			||||||
    `expires`,
 | 
					    `expires`,
 | 
				
			||||||
    `content`,
 | 
					    `content`,
 | 
				
			||||||
    `tts`,
 | 
					    `tts`,
 | 
				
			||||||
@@ -104,6 +108,8 @@ INSERT INTO reminders (
 | 
				
			|||||||
    ?,
 | 
					    ?,
 | 
				
			||||||
    ?,
 | 
					    ?,
 | 
				
			||||||
    ?,
 | 
					    ?,
 | 
				
			||||||
 | 
					    ?,
 | 
				
			||||||
 | 
					    ?,
 | 
				
			||||||
    ?
 | 
					    ?
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
            ",
 | 
					            ",
 | 
				
			||||||
@@ -111,7 +117,9 @@ INSERT INTO reminders (
 | 
				
			|||||||
                        self.channel,
 | 
					                        self.channel,
 | 
				
			||||||
                        utc_time,
 | 
					                        utc_time,
 | 
				
			||||||
                        self.timezone,
 | 
					                        self.timezone,
 | 
				
			||||||
                        self.interval,
 | 
					                        self.interval_seconds,
 | 
				
			||||||
 | 
					                        self.interval_days,
 | 
				
			||||||
 | 
					                        self.interval_months,
 | 
				
			||||||
                        self.expires,
 | 
					                        self.expires,
 | 
				
			||||||
                        self.content,
 | 
					                        self.content,
 | 
				
			||||||
                        self.tts,
 | 
					                        self.tts,
 | 
				
			||||||
@@ -123,7 +131,7 @@ INSERT INTO reminders (
 | 
				
			|||||||
                    .await
 | 
					                    .await
 | 
				
			||||||
                    .unwrap();
 | 
					                    .unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    Ok(Reminder::from_uid(&self.pool, self.uid).await.unwrap())
 | 
					                    Ok(Reminder::from_uid(&self.pool, &self.uid).await.unwrap())
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -136,11 +144,11 @@ pub struct MultiReminderBuilder<'a> {
 | 
				
			|||||||
    scopes: Vec<ReminderScope>,
 | 
					    scopes: Vec<ReminderScope>,
 | 
				
			||||||
    utc_time: NaiveDateTime,
 | 
					    utc_time: NaiveDateTime,
 | 
				
			||||||
    timezone: Tz,
 | 
					    timezone: Tz,
 | 
				
			||||||
    interval: Option<i64>,
 | 
					    interval: Option<Interval>,
 | 
				
			||||||
    expires: Option<NaiveDateTime>,
 | 
					    expires: Option<NaiveDateTime>,
 | 
				
			||||||
    content: Content,
 | 
					    content: Content,
 | 
				
			||||||
    set_by: Option<u32>,
 | 
					    set_by: Option<u32>,
 | 
				
			||||||
    ctx: &'a Context,
 | 
					    ctx: &'a Context<'a>,
 | 
				
			||||||
    guild_id: Option<GuildId>,
 | 
					    guild_id: Option<GuildId>,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -159,6 +167,12 @@ impl<'a> MultiReminderBuilder<'a> {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn timezone(mut self, timezone: Tz) -> Self {
 | 
				
			||||||
 | 
					        self.timezone = timezone;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    pub fn content(mut self, content: Content) -> Self {
 | 
					    pub fn content(mut self, content: Content) -> Self {
 | 
				
			||||||
        self.content = content;
 | 
					        self.content = content;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -166,17 +180,15 @@ impl<'a> MultiReminderBuilder<'a> {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    pub fn time<T: Into<i64>>(mut self, time: T) -> Self {
 | 
					    pub fn time<T: Into<i64>>(mut self, time: T) -> Self {
 | 
				
			||||||
        self.utc_time = NaiveDateTime::from_timestamp(time.into(), 0);
 | 
					        if let Some(utc_time) = NaiveDateTime::from_timestamp_opt(time.into(), 0) {
 | 
				
			||||||
 | 
					            self.utc_time = utc_time;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self
 | 
					        self
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    pub fn expires<T: Into<i64>>(mut self, time: Option<T>) -> Self {
 | 
					    pub fn expires<T: Into<i64>>(mut self, time: Option<T>) -> Self {
 | 
				
			||||||
        if let Some(t) = time {
 | 
					        self.expires = time.map(|t| NaiveDateTime::from_timestamp_opt(t.into(), 0)).flatten();
 | 
				
			||||||
            self.expires = Some(NaiveDateTime::from_timestamp(t.into(), 0));
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
            self.expires = None;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self
 | 
					        self
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -188,7 +200,7 @@ impl<'a> MultiReminderBuilder<'a> {
 | 
				
			|||||||
        self
 | 
					        self
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    pub fn interval(mut self, interval: Option<i64>) -> Self {
 | 
					    pub fn interval(mut self, interval: Option<Interval>) -> Self {
 | 
				
			||||||
        self.interval = interval;
 | 
					        self.interval = interval;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self
 | 
					        self
 | 
				
			||||||
@@ -198,28 +210,42 @@ impl<'a> MultiReminderBuilder<'a> {
 | 
				
			|||||||
        self.scopes = scopes;
 | 
					        self.scopes = scopes;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    pub async fn build(self) -> (HashSet<ReminderError>, HashSet<ReminderScope>) {
 | 
					    pub async fn build(self) -> (HashSet<ReminderError>, HashSet<(Reminder, ReminderScope)>) {
 | 
				
			||||||
        let pool = self.ctx.data.read().await.get::<SQLPool>().cloned().unwrap();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        let mut errors = HashSet::new();
 | 
					        let mut errors = HashSet::new();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        let mut ok_locs = HashSet::new();
 | 
					        let mut ok_locs = HashSet::new();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if self.interval.map_or(false, |i| (i as i64) < *MIN_INTERVAL) {
 | 
					        if self
 | 
				
			||||||
 | 
					            .interval
 | 
				
			||||||
 | 
					            .map_or(false, |i| ((i.sec + i.day * DAY + i.month * 30 * DAY) as i64) < *MIN_INTERVAL)
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
            errors.insert(ReminderError::ShortInterval);
 | 
					            errors.insert(ReminderError::ShortInterval);
 | 
				
			||||||
        } else if self.interval.map_or(false, |i| (i as i64) > *MAX_TIME) {
 | 
					        } else if self
 | 
				
			||||||
 | 
					            .interval
 | 
				
			||||||
 | 
					            .map_or(false, |i| ((i.sec + i.day * DAY + i.month * 30 * DAY) as i64) > *MAX_TIME)
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
            errors.insert(ReminderError::LongInterval);
 | 
					            errors.insert(ReminderError::LongInterval);
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
            for scope in self.scopes {
 | 
					            for scope in self.scopes {
 | 
				
			||||||
 | 
					                let thread_id = None;
 | 
				
			||||||
                let db_channel_id = match scope {
 | 
					                let db_channel_id = match scope {
 | 
				
			||||||
                    ReminderScope::User(user_id) => {
 | 
					                    ReminderScope::User(user_id) => {
 | 
				
			||||||
                        if let Ok(user) = UserId(user_id).to_user(&self.ctx).await {
 | 
					                        if let Ok(user) = UserId(user_id).to_user(&self.ctx).await {
 | 
				
			||||||
                            let user_data =
 | 
					                            let user_data = UserData::from_user(
 | 
				
			||||||
                                UserData::from_user(&user, &self.ctx, &pool).await.unwrap();
 | 
					                                &user,
 | 
				
			||||||
 | 
					                                &self.ctx.serenity_context(),
 | 
				
			||||||
 | 
					                                &self.ctx.data().database,
 | 
				
			||||||
 | 
					                            )
 | 
				
			||||||
 | 
					                            .await
 | 
				
			||||||
 | 
					                            .unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                            if let Some(guild_id) = self.guild_id {
 | 
					                            if let Some(guild_id) = self.guild_id {
 | 
				
			||||||
                                if guild_id.member(&self.ctx, user).await.is_err() {
 | 
					                                if guild_id.member(&self.ctx, user).await.is_err() {
 | 
				
			||||||
                                    Err(ReminderError::InvalidTag)
 | 
					                                    Err(ReminderError::InvalidTag)
 | 
				
			||||||
 | 
					                                } else if self.set_by.map_or(true, |i| i != user_data.id)
 | 
				
			||||||
 | 
					                                    && !user_data.allowed_dm
 | 
				
			||||||
 | 
					                                {
 | 
				
			||||||
 | 
					                                    Err(ReminderError::UserBlockedDm)
 | 
				
			||||||
                                } else {
 | 
					                                } else {
 | 
				
			||||||
                                    Ok(user_data.dm_channel)
 | 
					                                    Ok(user_data.dm_channel)
 | 
				
			||||||
                                }
 | 
					                                }
 | 
				
			||||||
@@ -233,12 +259,29 @@ impl<'a> MultiReminderBuilder<'a> {
 | 
				
			|||||||
                    ReminderScope::Channel(channel_id) => {
 | 
					                    ReminderScope::Channel(channel_id) => {
 | 
				
			||||||
                        let channel = ChannelId(channel_id).to_channel(&self.ctx).await.unwrap();
 | 
					                        let channel = ChannelId(channel_id).to_channel(&self.ctx).await.unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                        if let Some(guild_channel) = channel.clone().guild() {
 | 
					                        if let Some(mut guild_channel) = channel.clone().guild() {
 | 
				
			||||||
                            if Some(guild_channel.guild_id) != self.guild_id {
 | 
					                            if Some(guild_channel.guild_id) != self.guild_id {
 | 
				
			||||||
                                Err(ReminderError::InvalidTag)
 | 
					                                Err(ReminderError::InvalidTag)
 | 
				
			||||||
                            } else {
 | 
					                            } else {
 | 
				
			||||||
                                let mut channel_data =
 | 
					                                let mut channel_data = if guild_channel.kind
 | 
				
			||||||
                                    ChannelData::from_channel(&channel, &pool).await.unwrap();
 | 
					                                    == ChannelType::PublicThread
 | 
				
			||||||
 | 
					                                {
 | 
				
			||||||
 | 
					                                    // fixme jesus christ
 | 
				
			||||||
 | 
					                                    let parent = guild_channel
 | 
				
			||||||
 | 
					                                        .parent_id
 | 
				
			||||||
 | 
					                                        .unwrap()
 | 
				
			||||||
 | 
					                                        .to_channel(&self.ctx)
 | 
				
			||||||
 | 
					                                        .await
 | 
				
			||||||
 | 
					                                        .unwrap();
 | 
				
			||||||
 | 
					                                    guild_channel = parent.clone().guild().unwrap();
 | 
				
			||||||
 | 
					                                    ChannelData::from_channel(&parent, &self.ctx.data().database)
 | 
				
			||||||
 | 
					                                        .await
 | 
				
			||||||
 | 
					                                        .unwrap()
 | 
				
			||||||
 | 
					                                } else {
 | 
				
			||||||
 | 
					                                    ChannelData::from_channel(&channel, &self.ctx.data().database)
 | 
				
			||||||
 | 
					                                        .await
 | 
				
			||||||
 | 
					                                        .unwrap()
 | 
				
			||||||
 | 
					                                };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                                if channel_data.webhook_id.is_none()
 | 
					                                if channel_data.webhook_id.is_none()
 | 
				
			||||||
                                    || channel_data.webhook_token.is_none()
 | 
					                                    || channel_data.webhook_token.is_none()
 | 
				
			||||||
@@ -250,7 +293,9 @@ impl<'a> MultiReminderBuilder<'a> {
 | 
				
			|||||||
                                                Some(webhook.id.as_u64().to_owned());
 | 
					                                                Some(webhook.id.as_u64().to_owned());
 | 
				
			||||||
                                            channel_data.webhook_token = webhook.token;
 | 
					                                            channel_data.webhook_token = webhook.token;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                                            channel_data.commit_changes(&pool).await;
 | 
					                                            channel_data
 | 
				
			||||||
 | 
					                                                .commit_changes(&self.ctx.data().database)
 | 
				
			||||||
 | 
					                                                .await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                                            Ok(channel_data.id)
 | 
					                                            Ok(channel_data.id)
 | 
				
			||||||
                                        }
 | 
					                                        }
 | 
				
			||||||
@@ -270,12 +315,15 @@ impl<'a> MultiReminderBuilder<'a> {
 | 
				
			|||||||
                match db_channel_id {
 | 
					                match db_channel_id {
 | 
				
			||||||
                    Ok(c) => {
 | 
					                    Ok(c) => {
 | 
				
			||||||
                        let builder = ReminderBuilder {
 | 
					                        let builder = ReminderBuilder {
 | 
				
			||||||
                            pool: pool.clone(),
 | 
					                            pool: self.ctx.data().database.clone(),
 | 
				
			||||||
                            uid: generate_uid(),
 | 
					                            uid: generate_uid(),
 | 
				
			||||||
                            channel: c,
 | 
					                            channel: c,
 | 
				
			||||||
 | 
					                            thread_id,
 | 
				
			||||||
                            utc_time: self.utc_time,
 | 
					                            utc_time: self.utc_time,
 | 
				
			||||||
                            timezone: self.timezone.to_string(),
 | 
					                            timezone: self.timezone.to_string(),
 | 
				
			||||||
                            interval: self.interval,
 | 
					                            interval_seconds: self.interval.map(|i| i.sec as i64),
 | 
				
			||||||
 | 
					                            interval_days: self.interval.map(|i| i.day as i64),
 | 
				
			||||||
 | 
					                            interval_months: self.interval.map(|i| i.month as i64),
 | 
				
			||||||
                            expires: self.expires,
 | 
					                            expires: self.expires,
 | 
				
			||||||
                            content: self.content.content.clone(),
 | 
					                            content: self.content.content.clone(),
 | 
				
			||||||
                            tts: self.content.tts,
 | 
					                            tts: self.content.tts,
 | 
				
			||||||
@@ -285,8 +333,8 @@ impl<'a> MultiReminderBuilder<'a> {
 | 
				
			|||||||
                        };
 | 
					                        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                        match builder.build().await {
 | 
					                        match builder.build().await {
 | 
				
			||||||
                            Ok(_) => {
 | 
					                            Ok(r) => {
 | 
				
			||||||
                                ok_locs.insert(scope);
 | 
					                                ok_locs.insert((r, scope));
 | 
				
			||||||
                            }
 | 
					                            }
 | 
				
			||||||
                            Err(e) => {
 | 
					                            Err(e) => {
 | 
				
			||||||
                                errors.insert(e);
 | 
					                                errors.insert(e);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -7,6 +7,7 @@ pub enum ReminderError {
 | 
				
			|||||||
    PastTime,
 | 
					    PastTime,
 | 
				
			||||||
    ShortInterval,
 | 
					    ShortInterval,
 | 
				
			||||||
    InvalidTag,
 | 
					    InvalidTag,
 | 
				
			||||||
 | 
					    UserBlockedDm,
 | 
				
			||||||
    DiscordError(String),
 | 
					    DiscordError(String),
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -30,6 +31,9 @@ impl ToString for ReminderError {
 | 
				
			|||||||
            ReminderError::InvalidTag => {
 | 
					            ReminderError::InvalidTag => {
 | 
				
			||||||
                "Couldn't find a location by your tag. Your tag must be either a channel or a user (not a role)".to_string()
 | 
					                "Couldn't find a location by your tag. Your tag must be either a channel or a user (not a role)".to_string()
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					            ReminderError::UserBlockedDm => {
 | 
				
			||||||
 | 
					                "User has DM reminders disabled".to_string()
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
            ReminderError::DiscordError(s) => format!("A Discord error occurred: **{}**", s),
 | 
					            ReminderError::DiscordError(s) => format!("A Discord error occurred: **{}**", s),
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,25 +1,6 @@
 | 
				
			|||||||
use num_integer::Integer;
 | 
					 | 
				
			||||||
use rand::{rngs::OsRng, seq::IteratorRandom};
 | 
					use rand::{rngs::OsRng, seq::IteratorRandom};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::consts::{CHARACTERS, DAY, HOUR, MINUTE};
 | 
					use crate::consts::CHARACTERS;
 | 
				
			||||||
 | 
					 | 
				
			||||||
pub fn longhand_displacement(seconds: u64) -> String {
 | 
					 | 
				
			||||||
    let (days, seconds) = seconds.div_rem(&DAY);
 | 
					 | 
				
			||||||
    let (hours, seconds) = seconds.div_rem(&HOUR);
 | 
					 | 
				
			||||||
    let (minutes, seconds) = seconds.div_rem(&MINUTE);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let mut sections = vec![];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    for (var, name) in
 | 
					 | 
				
			||||||
        [days, hours, minutes, seconds].iter().zip(["days", "hours", "minutes", "seconds"].iter())
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        if *var > 0 {
 | 
					 | 
				
			||||||
            sections.push(format!("{} {}", var, name));
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    sections.join(", ")
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub fn generate_uid() -> String {
 | 
					pub fn generate_uid() -> String {
 | 
				
			||||||
    let mut generator: OsRng = Default::default();
 | 
					    let mut generator: OsRng = Default::default();
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,6 @@
 | 
				
			|||||||
 | 
					use poise::serenity_prelude::model::id::ChannelId;
 | 
				
			||||||
use serde::{Deserialize, Serialize};
 | 
					use serde::{Deserialize, Serialize};
 | 
				
			||||||
use serde_repr::*;
 | 
					use serde_repr::*;
 | 
				
			||||||
use serenity::model::id::ChannelId;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[derive(Serialize_repr, Deserialize_repr, Copy, Clone, Debug)]
 | 
					#[derive(Serialize_repr, Deserialize_repr, Copy, Clone, Debug)]
 | 
				
			||||||
#[repr(u8)]
 | 
					#[repr(u8)]
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,20 +4,19 @@ pub mod errors;
 | 
				
			|||||||
mod helper;
 | 
					mod helper;
 | 
				
			||||||
pub mod look_flags;
 | 
					pub mod look_flags;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use chrono::{NaiveDateTime, TimeZone};
 | 
					use std::hash::{Hash, Hasher};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use chrono::{DateTime, NaiveDateTime, Utc};
 | 
				
			||||||
use chrono_tz::Tz;
 | 
					use chrono_tz::Tz;
 | 
				
			||||||
use serenity::{
 | 
					use poise::serenity_prelude::{
 | 
				
			||||||
    client::Context,
 | 
					 | 
				
			||||||
    model::id::{ChannelId, GuildId, UserId},
 | 
					    model::id::{ChannelId, GuildId, UserId},
 | 
				
			||||||
 | 
					    Cache,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
use sqlx::MySqlPool;
 | 
					use sqlx::Executor;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::{
 | 
					use crate::{
 | 
				
			||||||
    models::reminder::{
 | 
					    models::reminder::look_flags::{LookFlags, TimeDisplayType},
 | 
				
			||||||
        helper::longhand_displacement,
 | 
					    Database,
 | 
				
			||||||
        look_flags::{LookFlags, TimeDisplayType},
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    SQLPool,
 | 
					 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[derive(Debug, Clone)]
 | 
					#[derive(Debug, Clone)]
 | 
				
			||||||
@@ -25,8 +24,10 @@ pub struct Reminder {
 | 
				
			|||||||
    pub id: u32,
 | 
					    pub id: u32,
 | 
				
			||||||
    pub uid: String,
 | 
					    pub uid: String,
 | 
				
			||||||
    pub channel: u64,
 | 
					    pub channel: u64,
 | 
				
			||||||
    pub utc_time: NaiveDateTime,
 | 
					    pub utc_time: DateTime<Utc>,
 | 
				
			||||||
    pub interval: Option<u32>,
 | 
					    pub interval_seconds: Option<u32>,
 | 
				
			||||||
 | 
					    pub interval_days: Option<u32>,
 | 
				
			||||||
 | 
					    pub interval_months: Option<u32>,
 | 
				
			||||||
    pub expires: Option<NaiveDateTime>,
 | 
					    pub expires: Option<NaiveDateTime>,
 | 
				
			||||||
    pub enabled: bool,
 | 
					    pub enabled: bool,
 | 
				
			||||||
    pub content: String,
 | 
					    pub content: String,
 | 
				
			||||||
@@ -34,8 +35,22 @@ pub struct Reminder {
 | 
				
			|||||||
    pub set_by: Option<u64>,
 | 
					    pub set_by: Option<u64>,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl Hash for Reminder {
 | 
				
			||||||
 | 
					    fn hash<H: Hasher>(&self, state: &mut H) {
 | 
				
			||||||
 | 
					        self.uid.hash(state);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl PartialEq<Self> for Reminder {
 | 
				
			||||||
 | 
					    fn eq(&self, other: &Self) -> bool {
 | 
				
			||||||
 | 
					        self.uid == other.uid
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl Eq for Reminder {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
impl Reminder {
 | 
					impl Reminder {
 | 
				
			||||||
    pub async fn from_uid(pool: &MySqlPool, uid: String) -> Option<Self> {
 | 
					    pub async fn from_uid(pool: impl Executor<'_, Database = Database>, uid: &str) -> Option<Self> {
 | 
				
			||||||
        sqlx::query_as_unchecked!(
 | 
					        sqlx::query_as_unchecked!(
 | 
				
			||||||
            Self,
 | 
					            Self,
 | 
				
			||||||
            "
 | 
					            "
 | 
				
			||||||
@@ -44,7 +59,9 @@ SELECT
 | 
				
			|||||||
    reminders.uid,
 | 
					    reminders.uid,
 | 
				
			||||||
    channels.channel,
 | 
					    channels.channel,
 | 
				
			||||||
    reminders.utc_time,
 | 
					    reminders.utc_time,
 | 
				
			||||||
    reminders.interval,
 | 
					    reminders.interval_seconds,
 | 
				
			||||||
 | 
					    reminders.interval_days,
 | 
				
			||||||
 | 
					    reminders.interval_months,
 | 
				
			||||||
    reminders.expires,
 | 
					    reminders.expires,
 | 
				
			||||||
    reminders.enabled,
 | 
					    reminders.enabled,
 | 
				
			||||||
    reminders.content,
 | 
					    reminders.content,
 | 
				
			||||||
@@ -70,16 +87,7 @@ WHERE
 | 
				
			|||||||
        .ok()
 | 
					        .ok()
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    pub async fn from_channel<C: Into<ChannelId>>(
 | 
					    pub async fn from_id(pool: impl Executor<'_, Database = Database>, id: u32) -> Option<Self> {
 | 
				
			||||||
        ctx: &Context,
 | 
					 | 
				
			||||||
        channel_id: C,
 | 
					 | 
				
			||||||
        flags: &LookFlags,
 | 
					 | 
				
			||||||
    ) -> Vec<Self> {
 | 
					 | 
				
			||||||
        let pool = ctx.data.read().await.get::<SQLPool>().cloned().unwrap();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        let enabled = if flags.show_disabled { "0,1" } else { "1" };
 | 
					 | 
				
			||||||
        let channel_id = channel_id.into();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        sqlx::query_as_unchecked!(
 | 
					        sqlx::query_as_unchecked!(
 | 
				
			||||||
            Self,
 | 
					            Self,
 | 
				
			||||||
            "
 | 
					            "
 | 
				
			||||||
@@ -88,7 +96,9 @@ SELECT
 | 
				
			|||||||
    reminders.uid,
 | 
					    reminders.uid,
 | 
				
			||||||
    channels.channel,
 | 
					    channels.channel,
 | 
				
			||||||
    reminders.utc_time,
 | 
					    reminders.utc_time,
 | 
				
			||||||
    reminders.interval,
 | 
					    reminders.interval_seconds,
 | 
				
			||||||
 | 
					    reminders.interval_days,
 | 
				
			||||||
 | 
					    reminders.interval_months,
 | 
				
			||||||
    reminders.expires,
 | 
					    reminders.expires,
 | 
				
			||||||
    reminders.enabled,
 | 
					    reminders.enabled,
 | 
				
			||||||
    reminders.content,
 | 
					    reminders.content,
 | 
				
			||||||
@@ -105,6 +115,51 @@ LEFT JOIN
 | 
				
			|||||||
ON
 | 
					ON
 | 
				
			||||||
    reminders.set_by = users.id
 | 
					    reminders.set_by = users.id
 | 
				
			||||||
WHERE
 | 
					WHERE
 | 
				
			||||||
 | 
					    reminders.id = ?
 | 
				
			||||||
 | 
					            ",
 | 
				
			||||||
 | 
					            id
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        .fetch_one(pool)
 | 
				
			||||||
 | 
					        .await
 | 
				
			||||||
 | 
					        .ok()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub async fn from_channel<C: Into<ChannelId>>(
 | 
				
			||||||
 | 
					        pool: impl Executor<'_, Database = Database>,
 | 
				
			||||||
 | 
					        channel_id: C,
 | 
				
			||||||
 | 
					        flags: &LookFlags,
 | 
				
			||||||
 | 
					    ) -> Vec<Self> {
 | 
				
			||||||
 | 
					        let enabled = if flags.show_disabled { "0,1" } else { "1" };
 | 
				
			||||||
 | 
					        let channel_id = channel_id.into();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        sqlx::query_as_unchecked!(
 | 
				
			||||||
 | 
					            Self,
 | 
				
			||||||
 | 
					            "
 | 
				
			||||||
 | 
					SELECT
 | 
				
			||||||
 | 
					    reminders.id,
 | 
				
			||||||
 | 
					    reminders.uid,
 | 
				
			||||||
 | 
					    channels.channel,
 | 
				
			||||||
 | 
					    reminders.utc_time,
 | 
				
			||||||
 | 
					    reminders.interval_seconds,
 | 
				
			||||||
 | 
					    reminders.interval_days,
 | 
				
			||||||
 | 
					    reminders.interval_months,
 | 
				
			||||||
 | 
					    reminders.expires,
 | 
				
			||||||
 | 
					    reminders.enabled,
 | 
				
			||||||
 | 
					    reminders.content,
 | 
				
			||||||
 | 
					    reminders.embed_description,
 | 
				
			||||||
 | 
					    users.user AS set_by
 | 
				
			||||||
 | 
					FROM
 | 
				
			||||||
 | 
					    reminders
 | 
				
			||||||
 | 
					INNER JOIN
 | 
				
			||||||
 | 
					    channels
 | 
				
			||||||
 | 
					ON
 | 
				
			||||||
 | 
					    reminders.channel_id = channels.id
 | 
				
			||||||
 | 
					LEFT JOIN
 | 
				
			||||||
 | 
					    users
 | 
				
			||||||
 | 
					ON
 | 
				
			||||||
 | 
					    reminders.set_by = users.id
 | 
				
			||||||
 | 
					WHERE
 | 
				
			||||||
 | 
					    `status` = 'pending' AND
 | 
				
			||||||
    channels.channel = ? AND
 | 
					    channels.channel = ? AND
 | 
				
			||||||
    FIND_IN_SET(reminders.enabled, ?)
 | 
					    FIND_IN_SET(reminders.enabled, ?)
 | 
				
			||||||
ORDER BY
 | 
					ORDER BY
 | 
				
			||||||
@@ -113,16 +168,19 @@ ORDER BY
 | 
				
			|||||||
            channel_id.as_u64(),
 | 
					            channel_id.as_u64(),
 | 
				
			||||||
            enabled,
 | 
					            enabled,
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        .fetch_all(&pool)
 | 
					        .fetch_all(pool)
 | 
				
			||||||
        .await
 | 
					        .await
 | 
				
			||||||
        .unwrap()
 | 
					        .unwrap()
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    pub async fn from_guild(ctx: &Context, guild_id: Option<GuildId>, user: UserId) -> Vec<Self> {
 | 
					    pub async fn from_guild(
 | 
				
			||||||
        let pool = ctx.data.read().await.get::<SQLPool>().cloned().unwrap();
 | 
					        cache: impl AsRef<Cache>,
 | 
				
			||||||
 | 
					        pool: impl Executor<'_, Database = Database>,
 | 
				
			||||||
 | 
					        guild_id: Option<GuildId>,
 | 
				
			||||||
 | 
					        user: UserId,
 | 
				
			||||||
 | 
					    ) -> Vec<Self> {
 | 
				
			||||||
        if let Some(guild_id) = guild_id {
 | 
					        if let Some(guild_id) = guild_id {
 | 
				
			||||||
            let guild_opt = guild_id.to_guild_cached(&ctx);
 | 
					            let guild_opt = guild_id.to_guild_cached(cache);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if let Some(guild) = guild_opt {
 | 
					            if let Some(guild) = guild_opt {
 | 
				
			||||||
                let channels = guild
 | 
					                let channels = guild
 | 
				
			||||||
@@ -141,7 +199,9 @@ SELECT
 | 
				
			|||||||
    reminders.uid,
 | 
					    reminders.uid,
 | 
				
			||||||
    channels.channel,
 | 
					    channels.channel,
 | 
				
			||||||
    reminders.utc_time,
 | 
					    reminders.utc_time,
 | 
				
			||||||
    reminders.interval,
 | 
					    reminders.interval_seconds,
 | 
				
			||||||
 | 
					    reminders.interval_days,
 | 
				
			||||||
 | 
					    reminders.interval_months,
 | 
				
			||||||
    reminders.expires,
 | 
					    reminders.expires,
 | 
				
			||||||
    reminders.enabled,
 | 
					    reminders.enabled,
 | 
				
			||||||
    reminders.content,
 | 
					    reminders.content,
 | 
				
			||||||
@@ -158,11 +218,12 @@ LEFT JOIN
 | 
				
			|||||||
ON
 | 
					ON
 | 
				
			||||||
    reminders.set_by = users.id
 | 
					    reminders.set_by = users.id
 | 
				
			||||||
WHERE
 | 
					WHERE
 | 
				
			||||||
 | 
					    `status` = 'pending' AND
 | 
				
			||||||
    FIND_IN_SET(channels.channel, ?)
 | 
					    FIND_IN_SET(channels.channel, ?)
 | 
				
			||||||
                ",
 | 
					                ",
 | 
				
			||||||
                    channels
 | 
					                    channels
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
                .fetch_all(&pool)
 | 
					                .fetch_all(pool)
 | 
				
			||||||
                .await
 | 
					                .await
 | 
				
			||||||
            } else {
 | 
					            } else {
 | 
				
			||||||
                sqlx::query_as_unchecked!(
 | 
					                sqlx::query_as_unchecked!(
 | 
				
			||||||
@@ -173,7 +234,9 @@ SELECT
 | 
				
			|||||||
    reminders.uid,
 | 
					    reminders.uid,
 | 
				
			||||||
    channels.channel,
 | 
					    channels.channel,
 | 
				
			||||||
    reminders.utc_time,
 | 
					    reminders.utc_time,
 | 
				
			||||||
    reminders.interval,
 | 
					    reminders.interval_seconds,
 | 
				
			||||||
 | 
					    reminders.interval_days,
 | 
				
			||||||
 | 
					    reminders.interval_months,
 | 
				
			||||||
    reminders.expires,
 | 
					    reminders.expires,
 | 
				
			||||||
    reminders.enabled,
 | 
					    reminders.enabled,
 | 
				
			||||||
    reminders.content,
 | 
					    reminders.content,
 | 
				
			||||||
@@ -190,11 +253,12 @@ LEFT JOIN
 | 
				
			|||||||
ON
 | 
					ON
 | 
				
			||||||
    reminders.set_by = users.id
 | 
					    reminders.set_by = users.id
 | 
				
			||||||
WHERE
 | 
					WHERE
 | 
				
			||||||
 | 
					    `status` = 'pending' AND
 | 
				
			||||||
    channels.guild_id = (SELECT id FROM guilds WHERE guild = ?)
 | 
					    channels.guild_id = (SELECT id FROM guilds WHERE guild = ?)
 | 
				
			||||||
                ",
 | 
					                ",
 | 
				
			||||||
                    guild_id.as_u64()
 | 
					                    guild_id.as_u64()
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
                .fetch_all(&pool)
 | 
					                .fetch_all(pool)
 | 
				
			||||||
                .await
 | 
					                .await
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
@@ -206,7 +270,9 @@ SELECT
 | 
				
			|||||||
    reminders.uid,
 | 
					    reminders.uid,
 | 
				
			||||||
    channels.channel,
 | 
					    channels.channel,
 | 
				
			||||||
    reminders.utc_time,
 | 
					    reminders.utc_time,
 | 
				
			||||||
    reminders.interval,
 | 
					    reminders.interval_seconds,
 | 
				
			||||||
 | 
					    reminders.interval_days,
 | 
				
			||||||
 | 
					    reminders.interval_months,
 | 
				
			||||||
    reminders.expires,
 | 
					    reminders.expires,
 | 
				
			||||||
    reminders.enabled,
 | 
					    reminders.enabled,
 | 
				
			||||||
    reminders.content,
 | 
					    reminders.content,
 | 
				
			||||||
@@ -223,16 +289,27 @@ LEFT JOIN
 | 
				
			|||||||
ON
 | 
					ON
 | 
				
			||||||
    reminders.set_by = users.id
 | 
					    reminders.set_by = users.id
 | 
				
			||||||
WHERE
 | 
					WHERE
 | 
				
			||||||
 | 
					    `status` = 'pending' AND
 | 
				
			||||||
    channels.id = (SELECT dm_channel FROM users WHERE user = ?)
 | 
					    channels.id = (SELECT dm_channel FROM users WHERE user = ?)
 | 
				
			||||||
            ",
 | 
					            ",
 | 
				
			||||||
                user.as_u64()
 | 
					                user.as_u64()
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
            .fetch_all(&pool)
 | 
					            .fetch_all(pool)
 | 
				
			||||||
            .await
 | 
					            .await
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        .unwrap()
 | 
					        .unwrap()
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub async fn delete(
 | 
				
			||||||
 | 
					        &self,
 | 
				
			||||||
 | 
					        db: impl Executor<'_, Database = Database>,
 | 
				
			||||||
 | 
					    ) -> Result<(), sqlx::Error> {
 | 
				
			||||||
 | 
					        sqlx::query!("UPDATE reminders SET `status` = 'deleted' WHERE uid = ?", self.uid)
 | 
				
			||||||
 | 
					            .execute(db)
 | 
				
			||||||
 | 
					            .await
 | 
				
			||||||
 | 
					            .map(|_| ())
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    pub fn display_content(&self) -> &str {
 | 
					    pub fn display_content(&self) -> &str {
 | 
				
			||||||
        if self.content.is_empty() {
 | 
					        if self.content.is_empty() {
 | 
				
			||||||
            &self.embed_description
 | 
					            &self.embed_description
 | 
				
			||||||
@@ -247,34 +324,32 @@ WHERE
 | 
				
			|||||||
            count + 1,
 | 
					            count + 1,
 | 
				
			||||||
            self.display_content(),
 | 
					            self.display_content(),
 | 
				
			||||||
            self.channel,
 | 
					            self.channel,
 | 
				
			||||||
            timezone
 | 
					            self.utc_time.with_timezone(timezone).format("%Y-%m-%d %H:%M:%S")
 | 
				
			||||||
                .timestamp(self.utc_time.timestamp(), 0)
 | 
					 | 
				
			||||||
                .format("%Y-%m-%d %H:%M:%S")
 | 
					 | 
				
			||||||
                .to_string()
 | 
					 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    pub fn display(&self, flags: &LookFlags, timezone: &Tz) -> String {
 | 
					    pub fn display(&self, flags: &LookFlags, timezone: &Tz) -> String {
 | 
				
			||||||
        let time_display = match flags.time_display {
 | 
					        let time_display = match flags.time_display {
 | 
				
			||||||
            TimeDisplayType::Absolute => timezone
 | 
					            TimeDisplayType::Absolute => {
 | 
				
			||||||
                .timestamp(self.utc_time.timestamp(), 0)
 | 
					                self.utc_time.with_timezone(timezone).format("%Y-%m-%d %H:%M:%S").to_string()
 | 
				
			||||||
                .format("%Y-%m-%d %H:%M:%S")
 | 
					            }
 | 
				
			||||||
                .to_string(),
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
            TimeDisplayType::Relative => format!("<t:{}:R>", self.utc_time.timestamp()),
 | 
					            TimeDisplayType::Relative => format!("<t:{}:R>", self.utc_time.timestamp()),
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if let Some(interval) = self.interval {
 | 
					        if self.interval_seconds.is_some()
 | 
				
			||||||
 | 
					            || self.interval_days.is_some()
 | 
				
			||||||
 | 
					            || self.interval_months.is_some()
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
            format!(
 | 
					            format!(
 | 
				
			||||||
                "'{}' *occurs next at* **{}**, repeating every **{}** (set by {})",
 | 
					                "'{}' *occurs next at* **{}**, repeating (set by {})\n",
 | 
				
			||||||
                self.display_content(),
 | 
					                self.display_content(),
 | 
				
			||||||
                time_display,
 | 
					                time_display,
 | 
				
			||||||
                longhand_displacement(interval as u64),
 | 
					 | 
				
			||||||
                self.set_by.map(|i| format!("<@{}>", i)).unwrap_or_else(|| "unknown".to_string())
 | 
					                self.set_by.map(|i| format!("<@{}>", i)).unwrap_or_else(|| "unknown".to_string())
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
            format!(
 | 
					            format!(
 | 
				
			||||||
                "'{}' *occurs next at* **{}** (set by {})",
 | 
					                "'{}' *occurs next at* **{}** (set by {})\n",
 | 
				
			||||||
                self.display_content(),
 | 
					                self.display_content(),
 | 
				
			||||||
                time_display,
 | 
					                time_display,
 | 
				
			||||||
                self.set_by.map(|i| format!("<@{}>", i)).unwrap_or_else(|| "unknown".to_string())
 | 
					                self.set_by.map(|i| format!("<@{}>", i)).unwrap_or_else(|| "unknown".to_string())
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,9 +1,9 @@
 | 
				
			|||||||
use chrono::NaiveDateTime;
 | 
					use chrono::{DateTime, Utc};
 | 
				
			||||||
use sqlx::MySqlPool;
 | 
					use sqlx::MySqlPool;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub struct Timer {
 | 
					pub struct Timer {
 | 
				
			||||||
    pub name: String,
 | 
					    pub name: String,
 | 
				
			||||||
    pub start_time: NaiveDateTime,
 | 
					    pub start_time: DateTime<Utc>,
 | 
				
			||||||
    pub owner: u64,
 | 
					    pub owner: u64,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,9 +1,6 @@
 | 
				
			|||||||
use chrono_tz::Tz;
 | 
					use chrono_tz::Tz;
 | 
				
			||||||
use log::error;
 | 
					use log::error;
 | 
				
			||||||
use serenity::{
 | 
					use poise::serenity_prelude::{http::CacheHttp, model::id::UserId};
 | 
				
			||||||
    http::CacheHttp,
 | 
					 | 
				
			||||||
    model::{id::UserId, user::User},
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
use sqlx::MySqlPool;
 | 
					use sqlx::MySqlPool;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::consts::LOCAL_TIMEZONE;
 | 
					use crate::consts::LOCAL_TIMEZONE;
 | 
				
			||||||
@@ -11,9 +8,9 @@ use crate::consts::LOCAL_TIMEZONE;
 | 
				
			|||||||
pub struct UserData {
 | 
					pub struct UserData {
 | 
				
			||||||
    pub id: u32,
 | 
					    pub id: u32,
 | 
				
			||||||
    pub user: u64,
 | 
					    pub user: u64,
 | 
				
			||||||
    pub name: String,
 | 
					 | 
				
			||||||
    pub dm_channel: u32,
 | 
					    pub dm_channel: u32,
 | 
				
			||||||
    pub timezone: String,
 | 
					    pub timezone: String,
 | 
				
			||||||
 | 
					    pub allowed_dm: bool,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
impl UserData {
 | 
					impl UserData {
 | 
				
			||||||
@@ -25,7 +22,7 @@ impl UserData {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        match sqlx::query!(
 | 
					        match sqlx::query!(
 | 
				
			||||||
            "
 | 
					            "
 | 
				
			||||||
SELECT timezone FROM users WHERE user = ?
 | 
					SELECT IFNULL(timezone, 'UTC') AS timezone FROM users WHERE user = ?
 | 
				
			||||||
            ",
 | 
					            ",
 | 
				
			||||||
            user_id
 | 
					            user_id
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
@@ -40,20 +37,20 @@ SELECT timezone FROM users WHERE user = ?
 | 
				
			|||||||
        .unwrap()
 | 
					        .unwrap()
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    pub async fn from_user(
 | 
					    pub async fn from_user<U: Into<UserId>>(
 | 
				
			||||||
        user: &User,
 | 
					        user: U,
 | 
				
			||||||
        ctx: impl CacheHttp,
 | 
					        ctx: impl CacheHttp,
 | 
				
			||||||
        pool: &MySqlPool,
 | 
					        pool: &MySqlPool,
 | 
				
			||||||
    ) -> Result<Self, Box<dyn std::error::Error + Sync + Send>> {
 | 
					    ) -> Result<Self, Box<dyn std::error::Error + Sync + Send>> {
 | 
				
			||||||
        let user_id = user.id.as_u64().to_owned();
 | 
					        let user_id = user.into();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        match sqlx::query_as_unchecked!(
 | 
					        match sqlx::query_as_unchecked!(
 | 
				
			||||||
            Self,
 | 
					            Self,
 | 
				
			||||||
            "
 | 
					            "
 | 
				
			||||||
SELECT id, user, name, dm_channel, IF(timezone IS NULL, ?, timezone) AS timezone FROM users WHERE user = ?
 | 
					SELECT id, user, dm_channel, IF(timezone IS NULL, ?, timezone) AS timezone, allowed_dm FROM users WHERE user = ?
 | 
				
			||||||
            ",
 | 
					            ",
 | 
				
			||||||
            *LOCAL_TIMEZONE,
 | 
					            *LOCAL_TIMEZONE,
 | 
				
			||||||
            user_id
 | 
					            user_id.0
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        .fetch_one(pool)
 | 
					        .fetch_one(pool)
 | 
				
			||||||
        .await
 | 
					        .await
 | 
				
			||||||
@@ -61,27 +58,24 @@ SELECT id, user, name, dm_channel, IF(timezone IS NULL, ?, timezone) AS timezone
 | 
				
			|||||||
            Ok(c) => Ok(c),
 | 
					            Ok(c) => Ok(c),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            Err(sqlx::Error::RowNotFound) => {
 | 
					            Err(sqlx::Error::RowNotFound) => {
 | 
				
			||||||
                let dm_channel = user.create_dm_channel(ctx).await?;
 | 
					                let dm_channel = user_id.create_dm_channel(ctx).await?;
 | 
				
			||||||
                let dm_id = dm_channel.id.as_u64().to_owned();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                let pool_c = pool.clone();
 | 
					                let pool_c = pool.clone();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                sqlx::query!(
 | 
					                sqlx::query!(
 | 
				
			||||||
                    "
 | 
					                    "
 | 
				
			||||||
INSERT IGNORE INTO channels (channel) VALUES (?)
 | 
					INSERT IGNORE INTO channels (channel) VALUES (?)
 | 
				
			||||||
                    ",
 | 
					                    ",
 | 
				
			||||||
                    dm_id
 | 
					                    dm_channel.id.0
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
                .execute(&pool_c)
 | 
					                .execute(&pool_c)
 | 
				
			||||||
                .await?;
 | 
					                .await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                sqlx::query!(
 | 
					                sqlx::query!(
 | 
				
			||||||
                    "
 | 
					                    "
 | 
				
			||||||
INSERT INTO users (user, name, dm_channel, timezone) VALUES (?, ?, (SELECT id FROM channels WHERE channel = ?), ?)
 | 
					INSERT INTO users (name, user, dm_channel, timezone) VALUES ('', ?, (SELECT id FROM channels WHERE channel = ?), ?)
 | 
				
			||||||
                    ",
 | 
					                    ",
 | 
				
			||||||
                    user_id,
 | 
					                    user_id.0,
 | 
				
			||||||
                    user.name,
 | 
					                    dm_channel.id.0,
 | 
				
			||||||
                    dm_id,
 | 
					 | 
				
			||||||
                    *LOCAL_TIMEZONE
 | 
					                    *LOCAL_TIMEZONE
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
                .execute(&pool_c)
 | 
					                .execute(&pool_c)
 | 
				
			||||||
@@ -90,9 +84,9 @@ INSERT INTO users (user, name, dm_channel, timezone) VALUES (?, ?, (SELECT id FR
 | 
				
			|||||||
                Ok(sqlx::query_as_unchecked!(
 | 
					                Ok(sqlx::query_as_unchecked!(
 | 
				
			||||||
                    Self,
 | 
					                    Self,
 | 
				
			||||||
                    "
 | 
					                    "
 | 
				
			||||||
SELECT id, user, name, dm_channel, timezone FROM users WHERE user = ?
 | 
					SELECT id, user, dm_channel, timezone, allowed_dm FROM users WHERE user = ?
 | 
				
			||||||
                    ",
 | 
					                    ",
 | 
				
			||||||
                    user_id
 | 
					                    user_id.0
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
                .fetch_one(pool)
 | 
					                .fetch_one(pool)
 | 
				
			||||||
                .await?)
 | 
					                .await?)
 | 
				
			||||||
@@ -109,10 +103,10 @@ SELECT id, user, name, dm_channel, timezone FROM users WHERE user = ?
 | 
				
			|||||||
    pub async fn commit_changes(&self, pool: &MySqlPool) {
 | 
					    pub async fn commit_changes(&self, pool: &MySqlPool) {
 | 
				
			||||||
        sqlx::query!(
 | 
					        sqlx::query!(
 | 
				
			||||||
            "
 | 
					            "
 | 
				
			||||||
UPDATE users SET name = ?, timezone = ? WHERE id = ?
 | 
					UPDATE users SET timezone = ?, allowed_dm = ? WHERE id = ?
 | 
				
			||||||
            ",
 | 
					            ",
 | 
				
			||||||
            self.name,
 | 
					 | 
				
			||||||
            self.timezone,
 | 
					            self.timezone,
 | 
				
			||||||
 | 
					            self.allowed_dm,
 | 
				
			||||||
            self.id
 | 
					            self.id
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        .execute(pool)
 | 
					        .execute(pool)
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										552
									
								
								src/sender.rs
									
									
									
									
									
								
							
							
						
						
									
										552
									
								
								src/sender.rs
									
									
									
									
									
								
							@@ -1,552 +0,0 @@
 | 
				
			|||||||
use chrono::Duration;
 | 
					 | 
				
			||||||
use chrono_tz::Tz;
 | 
					 | 
				
			||||||
use log::{error, info, warn};
 | 
					 | 
				
			||||||
use num_integer::Integer;
 | 
					 | 
				
			||||||
use regex::{Captures, Regex};
 | 
					 | 
				
			||||||
use serenity::{
 | 
					 | 
				
			||||||
    builder::CreateEmbed,
 | 
					 | 
				
			||||||
    http::{CacheHttp, Http, StatusCode},
 | 
					 | 
				
			||||||
    model::{
 | 
					 | 
				
			||||||
        channel::{Channel, Embed as SerenityEmbed},
 | 
					 | 
				
			||||||
        id::ChannelId,
 | 
					 | 
				
			||||||
        webhook::Webhook,
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    Error, Result,
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
use sqlx::{
 | 
					 | 
				
			||||||
    types::chrono::{NaiveDateTime, Utc},
 | 
					 | 
				
			||||||
    MySqlPool,
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
lazy_static! {
 | 
					 | 
				
			||||||
    pub static ref TIMEFROM_REGEX: Regex =
 | 
					 | 
				
			||||||
        Regex::new(r#"<<timefrom:(?P<time>\d+):(?P<format>.+)?>>"#).unwrap();
 | 
					 | 
				
			||||||
    pub static ref TIMENOW_REGEX: Regex =
 | 
					 | 
				
			||||||
        Regex::new(r#"<<timenow:(?P<timezone>(?:\w|/|_)+):(?P<format>.+)?>>"#).unwrap();
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
fn fmt_displacement(format: &str, seconds: u64) -> String {
 | 
					 | 
				
			||||||
    let mut seconds = seconds;
 | 
					 | 
				
			||||||
    let mut days: u64 = 0;
 | 
					 | 
				
			||||||
    let mut hours: u64 = 0;
 | 
					 | 
				
			||||||
    let mut minutes: u64 = 0;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    for (rep, time_type, div) in
 | 
					 | 
				
			||||||
        [("%d", &mut days, 86400), ("%h", &mut hours, 3600), ("%m", &mut minutes, 60)].iter_mut()
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        if format.contains(*rep) {
 | 
					 | 
				
			||||||
            let (divided, new_seconds) = seconds.div_rem(&div);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            **time_type = divided;
 | 
					 | 
				
			||||||
            seconds = new_seconds;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    format
 | 
					 | 
				
			||||||
        .replace("%s", &seconds.to_string())
 | 
					 | 
				
			||||||
        .replace("%m", &minutes.to_string())
 | 
					 | 
				
			||||||
        .replace("%h", &hours.to_string())
 | 
					 | 
				
			||||||
        .replace("%d", &days.to_string())
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
pub fn substitute(string: &str) -> String {
 | 
					 | 
				
			||||||
    let new = TIMEFROM_REGEX.replace(string, |caps: &Captures| {
 | 
					 | 
				
			||||||
        let final_time = caps.name("time").unwrap().as_str();
 | 
					 | 
				
			||||||
        let format = caps.name("format").unwrap().as_str();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if let Ok(final_time) = final_time.parse::<i64>() {
 | 
					 | 
				
			||||||
            let dt = NaiveDateTime::from_timestamp(final_time, 0);
 | 
					 | 
				
			||||||
            let now = Utc::now().naive_utc();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            let difference = {
 | 
					 | 
				
			||||||
                if now < dt {
 | 
					 | 
				
			||||||
                    dt - Utc::now().naive_utc()
 | 
					 | 
				
			||||||
                } else {
 | 
					 | 
				
			||||||
                    Utc::now().naive_utc() - dt
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            fmt_displacement(format, difference.num_seconds() as u64)
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
            String::new()
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    TIMENOW_REGEX
 | 
					 | 
				
			||||||
        .replace(&new, |caps: &Captures| {
 | 
					 | 
				
			||||||
            let timezone = caps.name("timezone").unwrap().as_str();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            println!("{}", timezone);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if let Ok(tz) = timezone.parse::<Tz>() {
 | 
					 | 
				
			||||||
                let format = caps.name("format").unwrap().as_str();
 | 
					 | 
				
			||||||
                let now = Utc::now().with_timezone(&tz);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                now.format(format).to_string()
 | 
					 | 
				
			||||||
            } else {
 | 
					 | 
				
			||||||
                String::new()
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        })
 | 
					 | 
				
			||||||
        .to_string()
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
struct Embed {
 | 
					 | 
				
			||||||
    inner: EmbedInner,
 | 
					 | 
				
			||||||
    fields: Vec<EmbedField>,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
struct EmbedInner {
 | 
					 | 
				
			||||||
    title: String,
 | 
					 | 
				
			||||||
    description: String,
 | 
					 | 
				
			||||||
    image_url: Option<String>,
 | 
					 | 
				
			||||||
    thumbnail_url: Option<String>,
 | 
					 | 
				
			||||||
    footer: String,
 | 
					 | 
				
			||||||
    footer_url: Option<String>,
 | 
					 | 
				
			||||||
    author: String,
 | 
					 | 
				
			||||||
    author_url: Option<String>,
 | 
					 | 
				
			||||||
    color: u32,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
struct EmbedField {
 | 
					 | 
				
			||||||
    title: String,
 | 
					 | 
				
			||||||
    value: String,
 | 
					 | 
				
			||||||
    inline: bool,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl Embed {
 | 
					 | 
				
			||||||
    pub async fn from_id(pool: &MySqlPool, id: u32) -> Option<Self> {
 | 
					 | 
				
			||||||
        let mut inner = sqlx::query_as_unchecked!(
 | 
					 | 
				
			||||||
            EmbedInner,
 | 
					 | 
				
			||||||
            "
 | 
					 | 
				
			||||||
SELECT
 | 
					 | 
				
			||||||
    `embed_title` AS title,
 | 
					 | 
				
			||||||
    `embed_description` AS description,
 | 
					 | 
				
			||||||
    `embed_image_url` AS image_url,
 | 
					 | 
				
			||||||
    `embed_thumbnail_url` AS thumbnail_url,
 | 
					 | 
				
			||||||
    `embed_footer` AS footer,
 | 
					 | 
				
			||||||
    `embed_footer_url` AS footer_url,
 | 
					 | 
				
			||||||
    `embed_author` AS author,
 | 
					 | 
				
			||||||
    `embed_author_url` AS author_url,
 | 
					 | 
				
			||||||
    `embed_color` AS color
 | 
					 | 
				
			||||||
FROM
 | 
					 | 
				
			||||||
    reminders
 | 
					 | 
				
			||||||
WHERE
 | 
					 | 
				
			||||||
    `id` = ?
 | 
					 | 
				
			||||||
            ",
 | 
					 | 
				
			||||||
            id
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        .fetch_one(&pool.clone())
 | 
					 | 
				
			||||||
        .await
 | 
					 | 
				
			||||||
        .unwrap();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        inner.title = substitute(&inner.title);
 | 
					 | 
				
			||||||
        inner.description = substitute(&inner.description);
 | 
					 | 
				
			||||||
        inner.footer = substitute(&inner.footer);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        let mut fields = sqlx::query_as_unchecked!(
 | 
					 | 
				
			||||||
            EmbedField,
 | 
					 | 
				
			||||||
            "
 | 
					 | 
				
			||||||
SELECT
 | 
					 | 
				
			||||||
    title,
 | 
					 | 
				
			||||||
    value,
 | 
					 | 
				
			||||||
    inline
 | 
					 | 
				
			||||||
FROM
 | 
					 | 
				
			||||||
    embed_fields
 | 
					 | 
				
			||||||
WHERE
 | 
					 | 
				
			||||||
    reminder_id = ?
 | 
					 | 
				
			||||||
            ",
 | 
					 | 
				
			||||||
            id
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        .fetch_all(pool)
 | 
					 | 
				
			||||||
        .await
 | 
					 | 
				
			||||||
        .unwrap();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        fields.iter_mut().for_each(|mut field| {
 | 
					 | 
				
			||||||
            field.title = substitute(&field.title);
 | 
					 | 
				
			||||||
            field.value = substitute(&field.value);
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        let e = Embed { inner, fields };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if e.has_content() {
 | 
					 | 
				
			||||||
            Some(e)
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
            None
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    pub fn has_content(&self) -> bool {
 | 
					 | 
				
			||||||
        if self.inner.title.is_empty()
 | 
					 | 
				
			||||||
            && self.inner.description.is_empty()
 | 
					 | 
				
			||||||
            && self.inner.image_url.is_none()
 | 
					 | 
				
			||||||
            && self.inner.thumbnail_url.is_none()
 | 
					 | 
				
			||||||
            && self.inner.footer.is_empty()
 | 
					 | 
				
			||||||
            && self.inner.footer_url.is_none()
 | 
					 | 
				
			||||||
            && self.inner.author.is_empty()
 | 
					 | 
				
			||||||
            && self.inner.author_url.is_none()
 | 
					 | 
				
			||||||
            && self.fields.is_empty()
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            false
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
            true
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl Into<CreateEmbed> for Embed {
 | 
					 | 
				
			||||||
    fn into(self) -> CreateEmbed {
 | 
					 | 
				
			||||||
        let mut c = CreateEmbed::default();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        c.title(&self.inner.title)
 | 
					 | 
				
			||||||
            .description(&self.inner.description)
 | 
					 | 
				
			||||||
            .color(self.inner.color)
 | 
					 | 
				
			||||||
            .author(|a| {
 | 
					 | 
				
			||||||
                a.name(&self.inner.author);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                if let Some(author_icon) = &self.inner.author_url {
 | 
					 | 
				
			||||||
                    a.icon_url(author_icon);
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                a
 | 
					 | 
				
			||||||
            })
 | 
					 | 
				
			||||||
            .footer(|f| {
 | 
					 | 
				
			||||||
                f.text(&self.inner.footer);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                if let Some(footer_icon) = &self.inner.footer_url {
 | 
					 | 
				
			||||||
                    f.icon_url(footer_icon);
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                f
 | 
					 | 
				
			||||||
            });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        for field in &self.fields {
 | 
					 | 
				
			||||||
            c.field(&field.title, &field.value, field.inline);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if let Some(image_url) = &self.inner.image_url {
 | 
					 | 
				
			||||||
            c.image(image_url);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if let Some(thumbnail_url) = &self.inner.thumbnail_url {
 | 
					 | 
				
			||||||
            c.thumbnail(thumbnail_url);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        c
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Debug)]
 | 
					 | 
				
			||||||
pub struct Reminder {
 | 
					 | 
				
			||||||
    id: u32,
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    channel_id: u64,
 | 
					 | 
				
			||||||
    webhook_id: Option<u64>,
 | 
					 | 
				
			||||||
    webhook_token: Option<String>,
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    channel_paused: bool,
 | 
					 | 
				
			||||||
    channel_paused_until: Option<NaiveDateTime>,
 | 
					 | 
				
			||||||
    enabled: bool,
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    tts: bool,
 | 
					 | 
				
			||||||
    pin: bool,
 | 
					 | 
				
			||||||
    content: String,
 | 
					 | 
				
			||||||
    attachment: Option<Vec<u8>>,
 | 
					 | 
				
			||||||
    attachment_name: Option<String>,
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    utc_time: NaiveDateTime,
 | 
					 | 
				
			||||||
    timezone: String,
 | 
					 | 
				
			||||||
    restartable: bool,
 | 
					 | 
				
			||||||
    expires: Option<NaiveDateTime>,
 | 
					 | 
				
			||||||
    interval: Option<u32>,
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    avatar: Option<String>,
 | 
					 | 
				
			||||||
    username: Option<String>,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl Reminder {
 | 
					 | 
				
			||||||
    pub async fn fetch_reminders(pool: &MySqlPool) -> Vec<Self> {
 | 
					 | 
				
			||||||
        sqlx::query_as_unchecked!(
 | 
					 | 
				
			||||||
            Reminder,
 | 
					 | 
				
			||||||
            "
 | 
					 | 
				
			||||||
SELECT
 | 
					 | 
				
			||||||
    reminders.`id` AS id,
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    channels.`channel` AS channel_id,
 | 
					 | 
				
			||||||
    channels.`webhook_id` AS webhook_id,
 | 
					 | 
				
			||||||
    channels.`webhook_token` AS webhook_token,
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    channels.`paused` AS channel_paused,
 | 
					 | 
				
			||||||
    channels.`paused_until` AS channel_paused_until,
 | 
					 | 
				
			||||||
    reminders.`enabled` AS enabled,
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    reminders.`tts` AS tts,
 | 
					 | 
				
			||||||
    reminders.`pin` AS pin,
 | 
					 | 
				
			||||||
    reminders.`content` AS content,
 | 
					 | 
				
			||||||
    reminders.`attachment` AS attachment,
 | 
					 | 
				
			||||||
    reminders.`attachment_name` AS attachment_name,
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    reminders.`utc_time` AS 'utc_time',
 | 
					 | 
				
			||||||
    reminders.`timezone` AS timezone,
 | 
					 | 
				
			||||||
    reminders.`restartable` AS restartable,
 | 
					 | 
				
			||||||
    reminders.`expires` AS expires,
 | 
					 | 
				
			||||||
    reminders.`interval` AS 'interval',
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    reminders.`avatar` AS avatar,
 | 
					 | 
				
			||||||
    reminders.`username` AS username
 | 
					 | 
				
			||||||
FROM
 | 
					 | 
				
			||||||
    reminders
 | 
					 | 
				
			||||||
INNER JOIN
 | 
					 | 
				
			||||||
    channels
 | 
					 | 
				
			||||||
ON
 | 
					 | 
				
			||||||
    reminders.channel_id = channels.id
 | 
					 | 
				
			||||||
WHERE
 | 
					 | 
				
			||||||
    reminders.`utc_time` < NOW()
 | 
					 | 
				
			||||||
            ",
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        .fetch_all(pool)
 | 
					 | 
				
			||||||
        .await
 | 
					 | 
				
			||||||
        .unwrap()
 | 
					 | 
				
			||||||
        .into_iter()
 | 
					 | 
				
			||||||
        .map(|mut rem| {
 | 
					 | 
				
			||||||
            rem.content = substitute(&rem.content);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            rem
 | 
					 | 
				
			||||||
        })
 | 
					 | 
				
			||||||
        .collect::<Vec<Self>>()
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    async fn reset_webhook(&self, pool: &MySqlPool) {
 | 
					 | 
				
			||||||
        let _ = sqlx::query!(
 | 
					 | 
				
			||||||
            "
 | 
					 | 
				
			||||||
UPDATE channels SET webhook_id = NULL, webhook_token = NULL WHERE channel = ?
 | 
					 | 
				
			||||||
            ",
 | 
					 | 
				
			||||||
            self.channel_id
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        .execute(pool)
 | 
					 | 
				
			||||||
        .await;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    async fn refresh(&self, pool: &MySqlPool) {
 | 
					 | 
				
			||||||
        if let Some(interval) = self.interval {
 | 
					 | 
				
			||||||
            let now = Utc::now().naive_local();
 | 
					 | 
				
			||||||
            let mut updated_reminder_time = self.utc_time;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            while updated_reminder_time < now {
 | 
					 | 
				
			||||||
                updated_reminder_time += Duration::seconds(interval as i64);
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if self.expires.map_or(false, |expires| {
 | 
					 | 
				
			||||||
                NaiveDateTime::from_timestamp(updated_reminder_time.timestamp(), 0) > expires
 | 
					 | 
				
			||||||
            }) {
 | 
					 | 
				
			||||||
                self.force_delete(pool).await;
 | 
					 | 
				
			||||||
            } else {
 | 
					 | 
				
			||||||
                sqlx::query!(
 | 
					 | 
				
			||||||
                    "
 | 
					 | 
				
			||||||
UPDATE reminders SET `utc_time` = ? WHERE `id` = ?
 | 
					 | 
				
			||||||
                    ",
 | 
					 | 
				
			||||||
                    updated_reminder_time,
 | 
					 | 
				
			||||||
                    self.id
 | 
					 | 
				
			||||||
                )
 | 
					 | 
				
			||||||
                .execute(pool)
 | 
					 | 
				
			||||||
                .await
 | 
					 | 
				
			||||||
                .expect(&format!("Could not update time on Reminder {}", self.id));
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
            self.force_delete(pool).await;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    async fn force_delete(&self, pool: &MySqlPool) {
 | 
					 | 
				
			||||||
        sqlx::query!(
 | 
					 | 
				
			||||||
            "
 | 
					 | 
				
			||||||
DELETE FROM reminders WHERE `id` = ?
 | 
					 | 
				
			||||||
            ",
 | 
					 | 
				
			||||||
            self.id
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        .execute(pool)
 | 
					 | 
				
			||||||
        .await
 | 
					 | 
				
			||||||
        .expect(&format!("Could not delete Reminder {}", self.id));
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    async fn pin_message<M: Into<u64>>(&self, message_id: M, http: impl AsRef<Http>) {
 | 
					 | 
				
			||||||
        let _ = http.as_ref().pin_message(self.channel_id, message_id.into(), None).await;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    pub async fn send(&self, pool: MySqlPool, cache_http: impl CacheHttp) {
 | 
					 | 
				
			||||||
        async fn send_to_channel(
 | 
					 | 
				
			||||||
            cache_http: impl CacheHttp,
 | 
					 | 
				
			||||||
            reminder: &Reminder,
 | 
					 | 
				
			||||||
            embed: Option<CreateEmbed>,
 | 
					 | 
				
			||||||
        ) -> Result<()> {
 | 
					 | 
				
			||||||
            let channel = ChannelId(reminder.channel_id).to_channel(&cache_http).await;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            match channel {
 | 
					 | 
				
			||||||
                Ok(Channel::Guild(channel)) => {
 | 
					 | 
				
			||||||
                    match channel
 | 
					 | 
				
			||||||
                        .send_message(&cache_http, |m| {
 | 
					 | 
				
			||||||
                            m.content(&reminder.content).tts(reminder.tts);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                            if let (Some(attachment), Some(name)) =
 | 
					 | 
				
			||||||
                                (&reminder.attachment, &reminder.attachment_name)
 | 
					 | 
				
			||||||
                            {
 | 
					 | 
				
			||||||
                                m.add_file((attachment as &[u8], name.as_str()));
 | 
					 | 
				
			||||||
                            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                            if let Some(embed) = embed {
 | 
					 | 
				
			||||||
                                m.set_embed(embed);
 | 
					 | 
				
			||||||
                            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                            m
 | 
					 | 
				
			||||||
                        })
 | 
					 | 
				
			||||||
                        .await
 | 
					 | 
				
			||||||
                    {
 | 
					 | 
				
			||||||
                        Ok(m) => {
 | 
					 | 
				
			||||||
                            if reminder.pin {
 | 
					 | 
				
			||||||
                                reminder.pin_message(m.id, cache_http.http()).await;
 | 
					 | 
				
			||||||
                            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                            Ok(())
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
                        Err(e) => Err(e),
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                Ok(Channel::Private(channel)) => {
 | 
					 | 
				
			||||||
                    match channel
 | 
					 | 
				
			||||||
                        .send_message(&cache_http.http(), |m| {
 | 
					 | 
				
			||||||
                            m.content(&reminder.content).tts(reminder.tts);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                            if let (Some(attachment), Some(name)) =
 | 
					 | 
				
			||||||
                                (&reminder.attachment, &reminder.attachment_name)
 | 
					 | 
				
			||||||
                            {
 | 
					 | 
				
			||||||
                                m.add_file((attachment as &[u8], name.as_str()));
 | 
					 | 
				
			||||||
                            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                            if let Some(embed) = embed {
 | 
					 | 
				
			||||||
                                m.set_embed(embed);
 | 
					 | 
				
			||||||
                            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                            m
 | 
					 | 
				
			||||||
                        })
 | 
					 | 
				
			||||||
                        .await
 | 
					 | 
				
			||||||
                    {
 | 
					 | 
				
			||||||
                        Ok(m) => {
 | 
					 | 
				
			||||||
                            if reminder.pin {
 | 
					 | 
				
			||||||
                                reminder.pin_message(m.id, cache_http.http()).await;
 | 
					 | 
				
			||||||
                            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                            Ok(())
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
                        Err(e) => Err(e),
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                Err(e) => Err(e),
 | 
					 | 
				
			||||||
                _ => Err(Error::Other("Channel not of valid type")),
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        async fn send_to_webhook(
 | 
					 | 
				
			||||||
            cache_http: impl CacheHttp,
 | 
					 | 
				
			||||||
            reminder: &Reminder,
 | 
					 | 
				
			||||||
            webhook: Webhook,
 | 
					 | 
				
			||||||
            embed: Option<CreateEmbed>,
 | 
					 | 
				
			||||||
        ) -> Result<()> {
 | 
					 | 
				
			||||||
            match webhook
 | 
					 | 
				
			||||||
                .execute(&cache_http.http(), reminder.pin || reminder.restartable, |w| {
 | 
					 | 
				
			||||||
                    w.content(&reminder.content).tts(reminder.tts);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    if let Some(username) = &reminder.username {
 | 
					 | 
				
			||||||
                        w.username(username);
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    if let Some(avatar) = &reminder.avatar {
 | 
					 | 
				
			||||||
                        w.avatar_url(avatar);
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    if let (Some(attachment), Some(name)) =
 | 
					 | 
				
			||||||
                        (&reminder.attachment, &reminder.attachment_name)
 | 
					 | 
				
			||||||
                    {
 | 
					 | 
				
			||||||
                        w.add_file((attachment as &[u8], name.as_str()));
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    if let Some(embed) = embed {
 | 
					 | 
				
			||||||
                        w.embeds(vec![SerenityEmbed::fake(|c| {
 | 
					 | 
				
			||||||
                            *c = embed;
 | 
					 | 
				
			||||||
                            c
 | 
					 | 
				
			||||||
                        })]);
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    w
 | 
					 | 
				
			||||||
                })
 | 
					 | 
				
			||||||
                .await
 | 
					 | 
				
			||||||
            {
 | 
					 | 
				
			||||||
                Ok(m) => {
 | 
					 | 
				
			||||||
                    if reminder.pin {
 | 
					 | 
				
			||||||
                        if let Some(message) = m {
 | 
					 | 
				
			||||||
                            reminder.pin_message(message.id, cache_http.http()).await;
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    Ok(())
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                Err(e) => Err(e),
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if self.enabled
 | 
					 | 
				
			||||||
            && !(self.channel_paused
 | 
					 | 
				
			||||||
                && self
 | 
					 | 
				
			||||||
                    .channel_paused_until
 | 
					 | 
				
			||||||
                    .map_or(true, |inner| inner >= Utc::now().naive_local()))
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            let _ = sqlx::query!(
 | 
					 | 
				
			||||||
                "
 | 
					 | 
				
			||||||
UPDATE `channels` SET paused = 0, paused_until = NULL WHERE `channel` = ?
 | 
					 | 
				
			||||||
                ",
 | 
					 | 
				
			||||||
                self.channel_id
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
            .execute(&pool.clone())
 | 
					 | 
				
			||||||
            .await;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            let embed = Embed::from_id(&pool.clone(), self.id).await.map(|e| e.into());
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            let result = if let (Some(webhook_id), Some(webhook_token)) =
 | 
					 | 
				
			||||||
                (self.webhook_id, &self.webhook_token)
 | 
					 | 
				
			||||||
            {
 | 
					 | 
				
			||||||
                let webhook_res =
 | 
					 | 
				
			||||||
                    cache_http.http().get_webhook_with_token(webhook_id, webhook_token).await;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                if let Ok(webhook) = webhook_res {
 | 
					 | 
				
			||||||
                    send_to_webhook(cache_http, &self, webhook, embed).await
 | 
					 | 
				
			||||||
                } else {
 | 
					 | 
				
			||||||
                    warn!("Webhook vanished: {:?}", webhook_res);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    self.reset_webhook(&pool.clone()).await;
 | 
					 | 
				
			||||||
                    send_to_channel(cache_http, &self, embed).await
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            } else {
 | 
					 | 
				
			||||||
                send_to_channel(cache_http, &self, embed).await
 | 
					 | 
				
			||||||
            };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if let Err(e) = result {
 | 
					 | 
				
			||||||
                error!("Error sending {:?}: {:?}", self, e);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                if let Error::Http(error) = e {
 | 
					 | 
				
			||||||
                    if error.status_code() == Some(StatusCode::from_u16(404).unwrap()) {
 | 
					 | 
				
			||||||
                        error!("Seeing channel is deleted. Removing reminder");
 | 
					 | 
				
			||||||
                        self.force_delete(&pool).await;
 | 
					 | 
				
			||||||
                    } else {
 | 
					 | 
				
			||||||
                        self.refresh(&pool).await;
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                } else {
 | 
					 | 
				
			||||||
                    self.refresh(&pool).await;
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            } else {
 | 
					 | 
				
			||||||
                self.refresh(&pool).await;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
            info!("Reminder {} is paused", self.id);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            self.refresh(&pool).await;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -211,14 +211,12 @@ pub async fn natural_parser(time: &str, timezone: &str) -> Option<i64> {
 | 
				
			|||||||
        .output()
 | 
					        .output()
 | 
				
			||||||
        .await
 | 
					        .await
 | 
				
			||||||
        .ok()
 | 
					        .ok()
 | 
				
			||||||
        .map(|inner| {
 | 
					        .and_then(|inner| {
 | 
				
			||||||
            if inner.status.success() {
 | 
					            if inner.status.success() {
 | 
				
			||||||
                Some(from_utf8(&*inner.stdout).unwrap().parse::<i64>().unwrap())
 | 
					                Some(from_utf8(&*inner.stdout).unwrap().parse::<i64>().unwrap())
 | 
				
			||||||
            } else {
 | 
					            } else {
 | 
				
			||||||
                None
 | 
					                None
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        })
 | 
					        })
 | 
				
			||||||
        .flatten()
 | 
					        .and_then(|inner| if inner < 0 { None } else { Some(inner) })
 | 
				
			||||||
        .map(|inner| if inner < 0 { None } else { Some(inner) })
 | 
					 | 
				
			||||||
        .flatten()
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										108
									
								
								src/utils.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										108
									
								
								src/utils.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,108 @@
 | 
				
			|||||||
 | 
					use poise::{
 | 
				
			||||||
 | 
					    serenity_prelude as serenity,
 | 
				
			||||||
 | 
					    serenity_prelude::{
 | 
				
			||||||
 | 
					        builder::CreateApplicationCommands,
 | 
				
			||||||
 | 
					        http::CacheHttp,
 | 
				
			||||||
 | 
					        interaction::MessageFlags,
 | 
				
			||||||
 | 
					        model::id::{GuildId, UserId},
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::{
 | 
				
			||||||
 | 
					    consts::{CNC_GUILD, SUBSCRIPTION_ROLES},
 | 
				
			||||||
 | 
					    Data, Error,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub async fn register_application_commands(
 | 
				
			||||||
 | 
					    ctx: &serenity::Context,
 | 
				
			||||||
 | 
					    framework: &poise::Framework<Data, Error>,
 | 
				
			||||||
 | 
					    guild_id: Option<GuildId>,
 | 
				
			||||||
 | 
					) -> Result<(), serenity::Error> {
 | 
				
			||||||
 | 
					    let mut commands_builder = CreateApplicationCommands::default();
 | 
				
			||||||
 | 
					    let commands = &framework.options().commands;
 | 
				
			||||||
 | 
					    for command in commands {
 | 
				
			||||||
 | 
					        if let Some(slash_command) = command.create_as_slash_command() {
 | 
				
			||||||
 | 
					            commands_builder.add_application_command(slash_command);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if let Some(context_menu_command) = command.create_as_context_menu_command() {
 | 
				
			||||||
 | 
					            commands_builder.add_application_command(context_menu_command);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    let commands_builder = poise::serenity_prelude::json::Value::Array(commands_builder.0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if let Some(guild_id) = guild_id {
 | 
				
			||||||
 | 
					        ctx.http.create_guild_application_commands(guild_id.0, &commands_builder).await?;
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        ctx.http.create_global_application_commands(&commands_builder).await?;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Ok(())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub async fn check_subscription(cache_http: impl CacheHttp, user_id: impl Into<UserId>) -> bool {
 | 
				
			||||||
 | 
					    if let Some(subscription_guild) = *CNC_GUILD {
 | 
				
			||||||
 | 
					        let guild_member = GuildId(subscription_guild).member(cache_http, user_id).await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if let Ok(member) = guild_member {
 | 
				
			||||||
 | 
					            for role in member.roles {
 | 
				
			||||||
 | 
					                if SUBSCRIPTION_ROLES.contains(role.as_u64()) {
 | 
				
			||||||
 | 
					                    return true;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        false
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        true
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub async fn check_guild_subscription(
 | 
				
			||||||
 | 
					    cache_http: impl CacheHttp,
 | 
				
			||||||
 | 
					    guild_id: impl Into<GuildId>,
 | 
				
			||||||
 | 
					) -> bool {
 | 
				
			||||||
 | 
					    if let Some(guild) = cache_http.cache().unwrap().guild(guild_id) {
 | 
				
			||||||
 | 
					        let owner = guild.owner_id;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        check_subscription(&cache_http, owner).await
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        false
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Sends the message, specified via [`crate::CreateReply`], to the interaction initial response
 | 
				
			||||||
 | 
					/// endpoint
 | 
				
			||||||
 | 
					pub fn send_as_initial_response(
 | 
				
			||||||
 | 
					    data: poise::CreateReply<'_>,
 | 
				
			||||||
 | 
					    f: &mut serenity::CreateInteractionResponseData,
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					    let poise::CreateReply {
 | 
				
			||||||
 | 
					        content,
 | 
				
			||||||
 | 
					        embeds,
 | 
				
			||||||
 | 
					        attachments: _, // serenity doesn't support attachments in initial response yet
 | 
				
			||||||
 | 
					        components,
 | 
				
			||||||
 | 
					        ephemeral,
 | 
				
			||||||
 | 
					        allowed_mentions,
 | 
				
			||||||
 | 
					        reply: _,
 | 
				
			||||||
 | 
					    } = data;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if let Some(content) = content {
 | 
				
			||||||
 | 
					        f.content(content);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    f.set_embeds(embeds);
 | 
				
			||||||
 | 
					    if let Some(allowed_mentions) = allowed_mentions {
 | 
				
			||||||
 | 
					        f.allowed_mentions(|f| {
 | 
				
			||||||
 | 
					            *f = allowed_mentions.clone();
 | 
				
			||||||
 | 
					            f
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if let Some(components) = components {
 | 
				
			||||||
 | 
					        f.components(|f| {
 | 
				
			||||||
 | 
					            f.0 = components.0;
 | 
				
			||||||
 | 
					            f
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if ephemeral {
 | 
				
			||||||
 | 
					        f.flags(MessageFlags::EPHEMERAL);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										14
									
								
								systemd/reminder-rs.service
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								systemd/reminder-rs.service
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,14 @@
 | 
				
			|||||||
 | 
					[Unit]
 | 
				
			||||||
 | 
					Description=Reminder Bot
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[Service]
 | 
				
			||||||
 | 
					User=reminder
 | 
				
			||||||
 | 
					Type=simple
 | 
				
			||||||
 | 
					ExecStart=/usr/bin/reminder-rs
 | 
				
			||||||
 | 
					WorkingDirectory=/etc/reminder-rs
 | 
				
			||||||
 | 
					Restart=always
 | 
				
			||||||
 | 
					RestartSec=4
 | 
				
			||||||
 | 
					Environment="reminder_rs=warn,postman=warn"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[Install]
 | 
				
			||||||
 | 
					WantedBy=multi-user.target
 | 
				
			||||||
							
								
								
									
										21
									
								
								web/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								web/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,21 @@
 | 
				
			|||||||
 | 
					[package]
 | 
				
			||||||
 | 
					name = "reminder_web"
 | 
				
			||||||
 | 
					version = "0.1.2"
 | 
				
			||||||
 | 
					authors = ["jellywx <judesouthworth@pm.me>"]
 | 
				
			||||||
 | 
					edition = "2018"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[dependencies]
 | 
				
			||||||
 | 
					rocket = { git = "https://github.com/SergioBenitez/Rocket", branch = "master", features = ["tls", "secrets", "json"] }
 | 
				
			||||||
 | 
					rocket_dyn_templates = { git = "https://github.com/SergioBenitez/Rocket", branch = "master", features = ["tera"] }
 | 
				
			||||||
 | 
					serenity = { version = "0.11", default-features = false, features = ["builder", "cache", "client", "gateway", "http", "model", "utils", "rustls_backend"] }
 | 
				
			||||||
 | 
					oauth2 = "4"
 | 
				
			||||||
 | 
					log = "0.4"
 | 
				
			||||||
 | 
					reqwest = "0.11"
 | 
				
			||||||
 | 
					serde = { version = "1.0", features = ["derive"] }
 | 
				
			||||||
 | 
					sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "macros", "mysql", "chrono", "json"] }
 | 
				
			||||||
 | 
					chrono = "0.4"
 | 
				
			||||||
 | 
					chrono-tz = "0.8"
 | 
				
			||||||
 | 
					lazy_static = "1.4.0"
 | 
				
			||||||
 | 
					rand = "0.8"
 | 
				
			||||||
 | 
					base64 = "0.13"
 | 
				
			||||||
 | 
					csv = "1.2"
 | 
				
			||||||
							
								
								
									
										32
									
								
								web/private/ca_cert.pem
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								web/private/ca_cert.pem
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,32 @@
 | 
				
			|||||||
 | 
					-----BEGIN CERTIFICATE-----
 | 
				
			||||||
 | 
					MIIFbzCCA1egAwIBAgIUUY2fYP5h41dtqcuNLVUS99N7+jAwDQYJKoZIhvcNAQEL
 | 
				
			||||||
 | 
					BQAwRzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRIwEAYDVQQKDAlSb2NrZXQg
 | 
				
			||||||
 | 
					Q0ExFzAVBgNVBAMMDlJvY2tldCBSb290IENBMB4XDTIxMDUxNzE5MDIyNFoXDTMx
 | 
				
			||||||
 | 
					MDUxNTE5MDIyNFowRzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRIwEAYDVQQK
 | 
				
			||||||
 | 
					DAlSb2NrZXQgQ0ExFzAVBgNVBAMMDlJvY2tldCBSb290IENBMIICIjANBgkqhkiG
 | 
				
			||||||
 | 
					9w0BAQEFAAOCAg8AMIICCgKCAgEA3MmVOUxbZ0rtBBbXjWkkM2n2IoPuqfiENdrM
 | 
				
			||||||
 | 
					NH9IcIbEgFb7AQ/c2vGBLWWw2RFtXxe2ubbgcHyAySQ+ENGEf+W2yox5JpcOOBP+
 | 
				
			||||||
 | 
					/KrUQ4ROqxLBWOD6+fA/q5m9/CXXJosWXly3DFE9nUEUlNXcaSxJm/bxIqnxwEMQ
 | 
				
			||||||
 | 
					NESWdG1qlsGVhHLi5sflgwAcYH+nZzdtINLyvQ5AGIs8s6LpmvUVbC3I+5oNNSwW
 | 
				
			||||||
 | 
					rRsIfCFaYLuUx7jZMaOk8Dfz8MbFL68+7OCGwmhduSHoGW8jVzDXwXv/crAmFtau
 | 
				
			||||||
 | 
					zmu43/vPDwKa4w/kQJLNr1QmiGz9FBt8DXVJ0s5bokOIlodEgAvjsL3xwnrKak0F
 | 
				
			||||||
 | 
					8ak74IBetfDdVvuC9WWmJ9/dj3z91KvCWLdZGm8ijlYZ2OuaiiJ/oC6iebaPFHEY
 | 
				
			||||||
 | 
					IQmF5wzT/nZLIbQsti9wFnOpptgkhDAcKIAk3+pncuMwZ9WRvWO2IFt+ZCqKCCDU
 | 
				
			||||||
 | 
					JliDZmm3ow/zob8DD6Txex5OkJGUF6iu8o7fjI9BxsjSc76bLrxIhvR43UXjzoHl
 | 
				
			||||||
 | 
					t3lvDH8SzSN/kxzQMXVl48h3xydJ3+ZyDOLle1bqMga8Kcin9GcEUqmL1JcyFafe
 | 
				
			||||||
 | 
					CUE/Nk1pHyrlnhpMCYIDVI3pmNLB0/m/IEWLckSr9+hXyUPJQyWiyrixwC86SPxZ
 | 
				
			||||||
 | 
					AonU2aUCAwEAAaNTMFEwHQYDVR0OBBYEFJ5uQa9kD5fioNeS/Ff/mxDcMAzhMB8G
 | 
				
			||||||
 | 
					A1UdIwQYMBaAFJ5uQa9kD5fioNeS/Ff/mxDcMAzhMA8GA1UdEwEB/wQFMAMBAf8w
 | 
				
			||||||
 | 
					DQYJKoZIhvcNAQELBQADggIBABf7adF1J40/8EjfSJ5XdG7OBUrZTas3+nuULh8B
 | 
				
			||||||
 | 
					6h1igAq0BgX9v3awmPCaxqDLqwJCsFZCk0s9Vgd62McKVKasyW1TQchWbdvlqeAB
 | 
				
			||||||
 | 
					QQfZpJtAZK12dcMl6iznC/JBTPvYl9q1FKS8kYtg19in/xlhxiiEJaL5z+slpTgT
 | 
				
			||||||
 | 
					cN12650W2FqjT9sD9jshZI/a8o53d2yUBXBBecCZ49djceBGLbxbteEi/sb9qM4f
 | 
				
			||||||
 | 
					IUxeSrvK6owxKMZ5okUqZWaFseFCkdMKpMg9eecyfSTweac8yLlZmeqX5D/H85Hr
 | 
				
			||||||
 | 
					hnWHjI5NeVZAoDkdmNeaDbAIv4f3prqC8uFP3ub8D0rG/WiPdOAd79KNgqbUUFyp
 | 
				
			||||||
 | 
					NbjSiAesx4Rm1WIgjHljFQKXJdtnxt+v1VkKV9entXJX2tye7+Z1+zurNYyUyM1J
 | 
				
			||||||
 | 
					COdXeV4jpq2mP8cLpAqX7ip1tNx9YMy5ctbAE1WsUw7lPyO91+VN2Q6MN5OyemY3
 | 
				
			||||||
 | 
					4nBzRJU8X1nsDgeNQWjzb/ZC7eoS916Gywk8Hu6GoIwvYMm3zea25n6mAAOX17vE
 | 
				
			||||||
 | 
					1YdvSzUlnVbwnbgd4BgFRGUfBVjO7qvFC7ZWLngWhBzIIYIGUJfO2rippgkxAoHH
 | 
				
			||||||
 | 
					dgWYk1MNnk2yM8WSo0VKoe4yuF9NwPlfPqeCE683JHdwGEJKZWMocszzHrGZ2KX2
 | 
				
			||||||
 | 
					I4/u
 | 
				
			||||||
 | 
					-----END CERTIFICATE-----
 | 
				
			||||||
							
								
								
									
										51
									
								
								web/private/ca_key.pem
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								web/private/ca_key.pem
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,51 @@
 | 
				
			|||||||
 | 
					-----BEGIN RSA PRIVATE KEY-----
 | 
				
			||||||
 | 
					MIIJKAIBAAKCAgEA3MmVOUxbZ0rtBBbXjWkkM2n2IoPuqfiENdrMNH9IcIbEgFb7
 | 
				
			||||||
 | 
					AQ/c2vGBLWWw2RFtXxe2ubbgcHyAySQ+ENGEf+W2yox5JpcOOBP+/KrUQ4ROqxLB
 | 
				
			||||||
 | 
					WOD6+fA/q5m9/CXXJosWXly3DFE9nUEUlNXcaSxJm/bxIqnxwEMQNESWdG1qlsGV
 | 
				
			||||||
 | 
					hHLi5sflgwAcYH+nZzdtINLyvQ5AGIs8s6LpmvUVbC3I+5oNNSwWrRsIfCFaYLuU
 | 
				
			||||||
 | 
					x7jZMaOk8Dfz8MbFL68+7OCGwmhduSHoGW8jVzDXwXv/crAmFtauzmu43/vPDwKa
 | 
				
			||||||
 | 
					4w/kQJLNr1QmiGz9FBt8DXVJ0s5bokOIlodEgAvjsL3xwnrKak0F8ak74IBetfDd
 | 
				
			||||||
 | 
					VvuC9WWmJ9/dj3z91KvCWLdZGm8ijlYZ2OuaiiJ/oC6iebaPFHEYIQmF5wzT/nZL
 | 
				
			||||||
 | 
					IbQsti9wFnOpptgkhDAcKIAk3+pncuMwZ9WRvWO2IFt+ZCqKCCDUJliDZmm3ow/z
 | 
				
			||||||
 | 
					ob8DD6Txex5OkJGUF6iu8o7fjI9BxsjSc76bLrxIhvR43UXjzoHlt3lvDH8SzSN/
 | 
				
			||||||
 | 
					kxzQMXVl48h3xydJ3+ZyDOLle1bqMga8Kcin9GcEUqmL1JcyFafeCUE/Nk1pHyrl
 | 
				
			||||||
 | 
					nhpMCYIDVI3pmNLB0/m/IEWLckSr9+hXyUPJQyWiyrixwC86SPxZAonU2aUCAwEA
 | 
				
			||||||
 | 
					AQKCAgBVSXlfXOOiDwtnnPs/IPJe+fuecaBsABfyRcbEMLbm4OhfOzpSurHx0YC4
 | 
				
			||||||
 | 
					7KNX9qdtKFfpfX9NdIq7KEjhbk3kqfPmYkUaZxeTCgZhzAua2S0aYHBXyPCqQ+gU
 | 
				
			||||||
 | 
					fZsqH+Pwe6H0aZQ8KdXHPTCaHdK6veThXo7feQ5t2noT9rq31txpx/Xd6BNGWsmJ
 | 
				
			||||||
 | 
					xS0xCZ68/GgnWdVyumKAGKkmKzRaK3pPA5CzwFqBw7ouvFaWvLuQymU6kWk1B6Xb
 | 
				
			||||||
 | 
					NYIB7IaXWPbRwhnMV0x9C2ABEzFvqOpvT1rqDqloAR4dlvcfbsIZZkQ2mhjt6MeT
 | 
				
			||||||
 | 
					hsorwQ4yCjvtZvVRfW1gTP4iR7ZpmHgHIYQDJy7raZqRVNe9WgMxKTS/IYsHvEvH
 | 
				
			||||||
 | 
					MUBOfQEclAQ8FTUOgTg9+Hf9VXJOtjZRxY08gb+OgKxoAQKxRZmLDUZli9SNWzGe
 | 
				
			||||||
 | 
					R3UECh0gImm/CzWnV3fercLX+qHLTcyJFcb2gclAfG8v8cOT2qu8RtsJAQM2ZSn7
 | 
				
			||||||
 | 
					L8FaWXewjAwkZwCl8Zl5ky1RbBXUkcuLIkAeLAl0+ffM9xiwLhiIj8gRJoq0/jSr
 | 
				
			||||||
 | 
					K1pA8CQ/rZC+9RsI8hP4x2Fn1CU8bOyEBPeNFEIDJHBiCrs/Rl6EAzDMJHlhL/HT
 | 
				
			||||||
 | 
					f7bqssuRjnSbRCKaAdtfGTxQUEjefvqJ11xE3BfHGnKPqoasMQKCAQEA8nY+3MAB
 | 
				
			||||||
 | 
					eBPlE/H4JeVpj7/9/q+JWLFFH9E3Re25LbObzRzIQOd5bTCJkOPQXYRbRIZ1pMt9
 | 
				
			||||||
 | 
					+nZzeLKvWn5nuUFgG2S4n+BEJkBDV78LOkUuGIAPdztF2mwVumygEGJFFfZHbYrh
 | 
				
			||||||
 | 
					XtaGUKdNxn1R3Z4B8W5jWpNNkU+dvoq4Zt0LrL28HB4Sa2yrtHeXbhqSb0BA15+N
 | 
				
			||||||
 | 
					vO9mlfX2I6Yr8F+L/OBfxB0gaNLISJJsj5NSm302je/QrfoMAwbePoNPjEX1/qd2
 | 
				
			||||||
 | 
					rgj9JfM+NE2FsVnFhrV+q4DywRUdX4VBFP4+x7fPm8LxvtVUmLVA3/NtGwPdnC6U
 | 
				
			||||||
 | 
					mQh/+kKVhU2AQwKCAQEA6R2FrxZIUdmk45b/tSjlq7r1ycBYeSztjzXw6hx+w/K3
 | 
				
			||||||
 | 
					Lf+5OGyoGoKr5bZHMhHFxqoWzP6kiZ2Uyvu+pLAGLfenJPec89ZncN4FvW9yU8yL
 | 
				
			||||||
 | 
					nE2Sc8Vf2GnOw2KGJcJR11Ew4dDspHfnM3rof2G4gPC/EMZeGojXoc5GHmT4iasD
 | 
				
			||||||
 | 
					Xwje4qqx5WKvRu1Rw2y+XM5h4gDn3QLLojDOvBpt22yaqSTAupY7OliGFO9h2WPL
 | 
				
			||||||
 | 
					r9TL6va87nXNepCdtzuSlxqTVtgMu2wVu3CnQyw9RiD2M31YnUtBDLNy/UNFj/5Z
 | 
				
			||||||
 | 
					6uXYuakh8vSFFvjgCUyQwR1xCTJur/6LdpAnt9Bz9wKCAQAUqoN9KVh2tatm4c72
 | 
				
			||||||
 | 
					2/D9ca3ikW+xgZqUta5yZWrNPGvhNbzT22b8KZDwKprN/cQRuSw52aZpPMNm3EQa
 | 
				
			||||||
 | 
					AIAyyCG69ADQj7r/T6btybjZRKBDMlcfIIw5q9DGTQ/vlZCx6IX6DkZbYQmdwkTc
 | 
				
			||||||
 | 
					0D20GA2uWGxbggawhgq5/PTuv5SJKrrn4qBLS73u6eqcVeN5XA6q0kywd+9UhNxv
 | 
				
			||||||
 | 
					+W/xUxOJgE5pVto2VREBLonWSwZVfnyx6GjvC0sOzv0Ocv7KxAPNqtRwzQ9Wtr7s
 | 
				
			||||||
 | 
					klb84Nv3OW0MjTcjwfr481Cyy2DqgP5PFnSogWJuibR34jXAgbnX4BiGWrUdzaMU
 | 
				
			||||||
 | 
					86AlAoIBABnw9Bh41VFuc9/zxL7nLy++HW33HqFVc5Y1PXr/8sdhcisHQxhZVxek
 | 
				
			||||||
 | 
					JPbqIuAahDTIZsMnLy41QAKaoyt2fymMXqhJecjUuiwgOOlMxp82qu6Y30xM0Y6m
 | 
				
			||||||
 | 
					r6CkjSMUjcD1QwhOFJd01GCxM8BBIqQOpmR6fqxbQAu8hacKO3IuerCPryXwMt3A
 | 
				
			||||||
 | 
					7ppo/GlP55sySEg7K5I3pmuFHOxn0IPTgR6DfYMGBs9GXJ1lyjDD3z3Q42RhUsMC
 | 
				
			||||||
 | 
					jvwtra9fTL/N8EmAv2H39C8oqSRbfvIX5u3x6/ONFU8RhSFT5CDTADSYoVZ/0MxV
 | 
				
			||||||
 | 
					k53r0hqWz6D94r9QQmsJW4G1JwZYhx8CggEBANUybXsJRgDC5ZOlwbaq0FeTOpZ4
 | 
				
			||||||
 | 
					pSKx1RkVV+G93hK5XdXIVu365pSt3T68MgF+4wWlbC4MuKuzJltFnJBapzz5ygcU
 | 
				
			||||||
 | 
					jw+Q+l/jq+mJj8uvUfo5TAy9thuggj1a7SCs/dm5DBwYA7bfmamLbbBpA0qywMsF
 | 
				
			||||||
 | 
					/vL1bpePIFhbGMhBK7uiEzOAOZVuceZWqcUByjUYy925K/an0ANsQAch5PVeAsEv
 | 
				
			||||||
 | 
					wXC3soaCzfzUyv+eAYBy7LOcz+hpdRj1l4c9Zx6hZeBxMc5LSRoTs+HfE6GgTuI2
 | 
				
			||||||
 | 
					cCfCZNFw7DhDy1TBd3XjQ0AFykeaesImZVR6gp0NYH1kionsMtR5rl4C2tw=
 | 
				
			||||||
 | 
					-----END RSA PRIVATE KEY-----
 | 
				
			||||||
							
								
								
									
										21
									
								
								web/private/ecdsa_nistp256_sha256_cert.pem
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								web/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
									
								
								web/private/ecdsa_nistp256_sha256_key_pkcs8.pem
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								web/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
									
								
								web/private/ecdsa_nistp384_sha384_cert.pem
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								web/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
									
								
								web/private/ecdsa_nistp384_sha384_key_pkcs8.pem
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								web/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
									
								
								web/private/ed25519_cert.pem
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								web/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
									
								
								web/private/ed25519_key.pem
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								web/private/ed25519_key.pem
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,3 @@
 | 
				
			|||||||
 | 
					-----BEGIN PRIVATE KEY-----
 | 
				
			||||||
 | 
					MC4CAQAwBQYDK2VwBCIEIGtLkaYE4D+P5HLkoc3a/tNhPP+gnhPLF3jEtdYcAtpd
 | 
				
			||||||
 | 
					-----END PRIVATE KEY-----
 | 
				
			||||||
							
								
								
									
										114
									
								
								web/private/gen_certs.sh
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										114
									
								
								web/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
									
								
								web/private/rsa_sha256_cert.pem
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								web/private/rsa_sha256_cert.pem
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,30 @@
 | 
				
			|||||||
 | 
					-----BEGIN CERTIFICATE-----
 | 
				
			||||||
 | 
					MIIFLDCCAxSgAwIBAgIUZ461KXDgTT25LP1S6PK6i9ZKtOYwDQYJKoZIhvcNAQEL
 | 
				
			||||||
 | 
					BQAwRzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRIwEAYDVQQKDAlSb2NrZXQg
 | 
				
			||||||
 | 
					Q0ExFzAVBgNVBAMMDlJvY2tldCBSb290IENBMB4XDTIxMDUxNzE5MDIyNFoXDTMx
 | 
				
			||||||
 | 
					MDUxNTE5MDIyNFowPzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMQ8wDQYDVQQK
 | 
				
			||||||
 | 
					DAZSb2NrZXQxEjAQBgNVBAMMCWxvY2FsaG9zdDCCAiIwDQYJKoZIhvcNAQEBBQAD
 | 
				
			||||||
 | 
					ggIPADCCAgoCggIBAKl8Re1dneFB2obYPOuZf3d6BR2xGcMhr2vmHpnVgkeg/xGI
 | 
				
			||||||
 | 
					cwHhhB04mEg4TqbZ0Q1wdKN4ruNigdrQAXDqnm4y2IB1q/uTdoXVWNsvAZmDq4N4
 | 
				
			||||||
 | 
					rPUDWagpkilV4HOHDQOI27Lo/+30wJX6H8i3J6Nag1y9spuySkL1F1SgQng9uzvP
 | 
				
			||||||
 | 
					3SwbsO9R/jS7qTZNEtirnJv3qJlxmT4BCvBuWNALShFlSZYnZk/2csYXEaVkWMUE
 | 
				
			||||||
 | 
					rnWGmTmzMZEzDOdQdPC3sJDCPQbbgD4SnQANqhOVjPSdJ6Y6joN6XYtB2EP+6LJ8
 | 
				
			||||||
 | 
					UBTICETnUROWeKqMm215Gsen32cyWkaCwEWtTFn7rJHbPvUY4vPYQZAB2/h07lYq
 | 
				
			||||||
 | 
					v67W3r5lTq1KuoStD8M/7nCIYIuNhmJLMzzWrSfJrQpYexoMII6kjOxv2kiUgb1y
 | 
				
			||||||
 | 
					bYKnFlVf4up3pTpi2/0We4SHRgc7xTcEPNvU5z5hc5JwHZIFjmi5dx50ZoAULrXl
 | 
				
			||||||
 | 
					OUhfdUPut7H38UQg3riJiNAzedUiTubek26po8qH1iS/EMgAi5RboaBovjWhgjwq
 | 
				
			||||||
 | 
					P/RP0vzpln6OdQIRaRlY9vZiik9HbFybafuA2zEkyc+zc4HqJk3vqX/0hgd9qWeL
 | 
				
			||||||
 | 
					zgsHr6A86tNUXsUaoInQbzznjpbFE85klxd6HeqasOyVDCeDUs4lWoDNp0DbAgMB
 | 
				
			||||||
 | 
					AAGjGDAWMBQGA1UdEQQNMAuCCWxvY2FsaG9zdDANBgkqhkiG9w0BAQsFAAOCAgEA
 | 
				
			||||||
 | 
					sS2TUKKYtNYC68TEQb5KAa8/zi6Lhz9lyn9rpBXAcAVeBdxCqrBU0nRN8EjXEff1
 | 
				
			||||||
 | 
					oBeau9Wi9PwdK6J+4xFuer9YrZZWuWLKUBXJL9ZXEfI+USo5WVz7v/puoKZSUSu2
 | 
				
			||||||
 | 
					+Ee4+v1eoAsCtaA8IR0Sh1KTc9kg1QZW1nIdBV0zgAERWTzm1iy2Ff+euowM/lUR
 | 
				
			||||||
 | 
					FczMAJsNq83O2kaH541fRx3gC2iMN/QLwRalBR2y8hTI5wQa0Yr8moC4IsTfRrKQ
 | 
				
			||||||
 | 
					/SZDjFT1XoA9TnrByIvGQNlxK1Rm2hvK1OQn4OL6sdYK6F+MxfUn/atQNyBQ6CF+
 | 
				
			||||||
 | 
					oKuvOnE2jces8GGZIU1jYJ2271SQkzp0CW3N1ZKjClSQFAYED+5ERTGyeEL4o3Vr
 | 
				
			||||||
 | 
					V/BUd0KC7HhbWBlFc2EDmPOoOTp/ID901Kf499M68pe2Fr/63/nCAON6WFk1NPYA
 | 
				
			||||||
 | 
					+sWMfS25hikU5rOHGQgXxXAz90pwpvpoLQvaq0/azsbHvq9B4ubdcLE0xcoK+DGq
 | 
				
			||||||
 | 
					+/aOeWlYkalWp93akPYpWN3UJXzDXzzagRID/1LEGS+Ssbh1WVhAhLKVoxzBvkgm
 | 
				
			||||||
 | 
					ozVAhR3zHXI2QpQ2FEbOmkcRtpxv2CiaTsgHg2MK8cdmPx3G+ufCZjRbQx/VXVdN
 | 
				
			||||||
 | 
					vaFRdmzr/5EQ7DT5Uy8w32h1to4Fm2sAtbAFYaFLXaM=
 | 
				
			||||||
 | 
					-----END CERTIFICATE-----
 | 
				
			||||||
							
								
								
									
										52
									
								
								web/private/rsa_sha256_key.pem
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								web/private/rsa_sha256_key.pem
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,52 @@
 | 
				
			|||||||
 | 
					-----BEGIN PRIVATE KEY-----
 | 
				
			||||||
 | 
					MIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQCpfEXtXZ3hQdqG
 | 
				
			||||||
 | 
					2DzrmX93egUdsRnDIa9r5h6Z1YJHoP8RiHMB4YQdOJhIOE6m2dENcHSjeK7jYoHa
 | 
				
			||||||
 | 
					0AFw6p5uMtiAdav7k3aF1VjbLwGZg6uDeKz1A1moKZIpVeBzhw0DiNuy6P/t9MCV
 | 
				
			||||||
 | 
					+h/ItyejWoNcvbKbskpC9RdUoEJ4Pbs7z90sG7DvUf40u6k2TRLYq5yb96iZcZk+
 | 
				
			||||||
 | 
					AQrwbljQC0oRZUmWJ2ZP9nLGFxGlZFjFBK51hpk5szGRMwznUHTwt7CQwj0G24A+
 | 
				
			||||||
 | 
					Ep0ADaoTlYz0nSemOo6Del2LQdhD/uiyfFAUyAhE51ETlniqjJtteRrHp99nMlpG
 | 
				
			||||||
 | 
					gsBFrUxZ+6yR2z71GOLz2EGQAdv4dO5WKr+u1t6+ZU6tSrqErQ/DP+5wiGCLjYZi
 | 
				
			||||||
 | 
					SzM81q0nya0KWHsaDCCOpIzsb9pIlIG9cm2CpxZVX+Lqd6U6Ytv9FnuEh0YHO8U3
 | 
				
			||||||
 | 
					BDzb1Oc+YXOScB2SBY5ouXcedGaAFC615TlIX3VD7rex9/FEIN64iYjQM3nVIk7m
 | 
				
			||||||
 | 
					3pNuqaPKh9YkvxDIAIuUW6GgaL41oYI8Kj/0T9L86ZZ+jnUCEWkZWPb2YopPR2xc
 | 
				
			||||||
 | 
					m2n7gNsxJMnPs3OB6iZN76l/9IYHfalni84LB6+gPOrTVF7FGqCJ0G88546WxRPO
 | 
				
			||||||
 | 
					ZJcXeh3qmrDslQwng1LOJVqAzadA2wIDAQABAoICABT4gHp/Q+K0UEKxDNCl/ISe
 | 
				
			||||||
 | 
					/3UODb78MwVpws2MAoO0YvsbZAeOjNdEwmrlNK4mc1xzVqtHanROIv0dEaCUFyhR
 | 
				
			||||||
 | 
					eEJkzPPi6h5jKIxuQ4doKFerHdNvJ6/L/P7KVmxVAII4c96uP8SErTOhcD9Ykjn/
 | 
				
			||||||
 | 
					IBPgkPH83H1ucAWTksXn9XvQG3CyuHDUN1z0/1ntrXBLw6P0v9LEoI5weJcJQEn1
 | 
				
			||||||
 | 
					q6N9Yd6HX3xzZP4nqpJJWUZ/bsqx7dGa3340z9rrNJz4TYuLzRtFG5gSm4R/LFUi
 | 
				
			||||||
 | 
					Av/dViOWST3xbROnAQhgyRAUm6AGpCdKa9i9nI6VuUGRY4PivJy7OTpSQVIdwD2K
 | 
				
			||||||
 | 
					VFbFauCmsTY7B+Z+DXe9JJV+Tlazvlwop1DApxCgLMNr2pVB/1yPzMrNYDB4Sg1c
 | 
				
			||||||
 | 
					T3stu4A8PtljTykla2C2LPFrdIijvYv9n6PSPWV4vf1ix9uYs99FhZPQJMgm+CDr
 | 
				
			||||||
 | 
					n3cFfuofIWIRJpCuu3hn4woGgW2bXor4pyMNg1yCp1T2c8g6eJUYAAIRpse0HDbT
 | 
				
			||||||
 | 
					ZUifxlNBx819bf9aqv+lhwMU4UFlmWMv3NETcCBqgUn+z4scNJ3kZwO9ZodLRblK
 | 
				
			||||||
 | 
					SpalZ6SNejTnVukg3zbwJG8w5vIrJvfjBkikAL1O5X6Kn3YqfrXAl38bIvA50ZBe
 | 
				
			||||||
 | 
					eOFcgDPClLPGlNKxQXiZAoIBAQDh0aArTjWi8Kxt2Ga3Hu4jQ7IXklL9osGAlFCB
 | 
				
			||||||
 | 
					wZs831/ktLY0c7vY+mbwrY4AtqK21A00MrNSTsp/McHwAUi410DdcTqzkGnm9BBZ
 | 
				
			||||||
 | 
					FKVO1H9KGg2t344tOXcPsFTl6SR5a0aPqagyvgdH+YWC4ccfYZWMcLELn3sOGoEp
 | 
				
			||||||
 | 
					a7VBxjLZZ+/b+/oIzhwxEaBi8N0U3IZ3SsC9Lmojnb9+LMwGs14TFfahD3uM1hnU
 | 
				
			||||||
 | 
					vww1elwPwXafWc+7Ej1bXijWBXbyzkCITzHafmDsAatEZnemHL8zKKtTn2aSTUSj
 | 
				
			||||||
 | 
					Fl/y94WR7/V4nVEQ0R3fjGC0rKh08BtKzxPjYBqjT5aI7+yfAoIBAQDAIzROr00o
 | 
				
			||||||
 | 
					65auD5TB2k/pSVKQpB/U6ESGafDxg4a+R9Ng2ESmDN5hUUZDefm0Uxf9AZqS6eno
 | 
				
			||||||
 | 
					GSAqxNDixWPy2WFzbZ0mUCoyQn+Eh09WBvt0IqDZ2+ngBEXiKA/8dsvXinBHLuHV
 | 
				
			||||||
 | 
					u+rJdLMzPfhWVo1/iXq7SOjBvDuthKROku5BEt6Q3Cs0nk6uneRIqKtaqa7KIInF
 | 
				
			||||||
 | 
					BPtUifZtCP09h6iFFGNv2g2OYRYDg8TXvjt3scKy0DUC3kTeTCPvNvHaOObGbMRU
 | 
				
			||||||
 | 
					Q85HkXburxWfsMto5m/lRsbY3COxddB47WIQ6QNewESfCab/R2lOdlXQeaH8fuxT
 | 
				
			||||||
 | 
					wWC5DMz4F0ZFAoIBAFm+EjZDnaNEnHIHB0MNIryXAabGev7beKUdzCTVCVmWuChO
 | 
				
			||||||
 | 
					/P45ZFTlppVNk9qKun2IJjsxTvyN3YHRB27XQ8xZlyiqABcudDfZlMmiH9QFNRUA
 | 
				
			||||||
 | 
					56DK8Fjetodgn0zDa8BpNqCPXw3TYVdkPX/3NEgvYtxuSJ4C4keHlv8cE+uw1bJ6
 | 
				
			||||||
 | 
					0OMO754iMyf5BlFrwaCxxyqPZauJT5sZ7Ok66lZbYC6bkukNGx+sUpWu2y5Bk2ab
 | 
				
			||||||
 | 
					jwXjDmAc7o9qCzaK82upNhI1zu0zPldsjmDfi/tS/1VYe0X/WicYWAesM7N+VPHb
 | 
				
			||||||
 | 
					eCVX98iEIqgdxKzo1QWsClyfkRrSraNrVLrVBqcCggEAaLIGK6YMNnMBPUGSPnt2
 | 
				
			||||||
 | 
					NdlVWymDiuExjcimmQOhZYf/33KZHZ4/gunljpklfqQUmzHHh6xcX7NpOsTaSedj
 | 
				
			||||||
 | 
					Sg43ss0U566g/5gKoi2VBnxxglvoKC5T51SMu+o2o8wb0QxHmBIszulBy5qClzZ6
 | 
				
			||||||
 | 
					Xpl1Kvy/2tOkuQSXxDpVydb4ao8cpfTCuj5VA4NXxFvcW1/AtbU7PRc02GEA3XMb
 | 
				
			||||||
 | 
					gu6r3jA46tb3shCnDS09Eo4/Gz7Kp+MaL8Dr5/G3Vv8qlE2TOqZD6OK1wXu7Qd43
 | 
				
			||||||
 | 
					uzd772I5sMZ7TenOrUFUYsB/QlWmF3hPLBX3YH0KHc4PfrT4lnyWzCDAUrVt7vXH
 | 
				
			||||||
 | 
					vQKCAQEAz1Q+jW+NG7CAqkOJ19n+KmGn+zddvy8x4txKx8kV0+3XIVNkseS6IT65
 | 
				
			||||||
 | 
					uemD85Gn7MYwHxcKUHghHSKyJsvCb1vSmB3JtCnrsodmQNMb6Lsq89VlM8Ylc/s3
 | 
				
			||||||
 | 
					F4AraiOlylqcEwLkanD3XSfPdQZhExZHEtpAW/Rr6zYT9J94VO1nK3+RkFI4a8kl
 | 
				
			||||||
 | 
					pEbY+tqJj3LGTuaQkshB9rDtcBT5/JsaxlMk783qkKziCPZF47BWqrJb+t7tm3qg
 | 
				
			||||||
 | 
					5gf+QUInEjdW3k3uZBL4316RP/TJlLvo29PkEwoC8X4R2EgDeHQhhRC2Fi2Wpy4O
 | 
				
			||||||
 | 
					ce4G+zZOOYXwvWGJLwNhgsve8C3oqg==
 | 
				
			||||||
 | 
					-----END PRIVATE KEY-----
 | 
				
			||||||
							
								
								
									
										40
									
								
								web/src/catchers.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								web/src/catchers.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,40 @@
 | 
				
			|||||||
 | 
					use std::collections::HashMap;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use rocket::serde::json::json;
 | 
				
			||||||
 | 
					use rocket_dyn_templates::Template;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::JsonValue;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[catch(403)]
 | 
				
			||||||
 | 
					pub(crate) async fn forbidden() -> Template {
 | 
				
			||||||
 | 
					    let map: HashMap<String, String> = HashMap::new();
 | 
				
			||||||
 | 
					    Template::render("errors/403", &map)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[catch(500)]
 | 
				
			||||||
 | 
					pub(crate) async fn internal_server_error() -> Template {
 | 
				
			||||||
 | 
					    let map: HashMap<String, String> = HashMap::new();
 | 
				
			||||||
 | 
					    Template::render("errors/500", &map)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[catch(401)]
 | 
				
			||||||
 | 
					pub(crate) async fn not_authorized() -> Template {
 | 
				
			||||||
 | 
					    let map: HashMap<String, String> = HashMap::new();
 | 
				
			||||||
 | 
					    Template::render("errors/401", &map)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[catch(404)]
 | 
				
			||||||
 | 
					pub(crate) async fn not_found() -> Template {
 | 
				
			||||||
 | 
					    let map: HashMap<String, String> = HashMap::new();
 | 
				
			||||||
 | 
					    Template::render("errors/404", &map)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[catch(413)]
 | 
				
			||||||
 | 
					pub(crate) async fn payload_too_large() -> JsonValue {
 | 
				
			||||||
 | 
					    json!({"error": "Data too large.", "errors": ["Data too large."]})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[catch(422)]
 | 
				
			||||||
 | 
					pub(crate) async fn unprocessable_entity() -> JsonValue {
 | 
				
			||||||
 | 
					    json!({"error": "Invalid request.", "errors": ["Invalid request."]})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										49
									
								
								web/src/consts.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								web/src/consts.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,49 @@
 | 
				
			|||||||
 | 
					pub const DISCORD_OAUTH_TOKEN: &'static str = "https://discord.com/api/oauth2/token";
 | 
				
			||||||
 | 
					pub const DISCORD_OAUTH_AUTHORIZE: &'static str = "https://discord.com/api/oauth2/authorize";
 | 
				
			||||||
 | 
					pub const DISCORD_API: &'static str = "https://discord.com/api";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub const MAX_NAME_LENGTH: usize = 100;
 | 
				
			||||||
 | 
					pub const MAX_CONTENT_LENGTH: usize = 2000;
 | 
				
			||||||
 | 
					pub const MAX_EMBED_DESCRIPTION_LENGTH: usize = 4096;
 | 
				
			||||||
 | 
					pub const MAX_EMBED_TITLE_LENGTH: usize = 256;
 | 
				
			||||||
 | 
					pub const MAX_EMBED_AUTHOR_LENGTH: usize = 256;
 | 
				
			||||||
 | 
					pub const MAX_EMBED_FOOTER_LENGTH: usize = 2048;
 | 
				
			||||||
 | 
					pub const MAX_URL_LENGTH: usize = 512;
 | 
				
			||||||
 | 
					pub const MAX_USERNAME_LENGTH: usize = 100;
 | 
				
			||||||
 | 
					pub const MAX_EMBED_FIELDS: usize = 25;
 | 
				
			||||||
 | 
					pub const MAX_EMBED_FIELD_TITLE_LENGTH: usize = 256;
 | 
				
			||||||
 | 
					pub const MAX_EMBED_FIELD_VALUE_LENGTH: usize = 1024;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub const MINUTE: usize = 60;
 | 
				
			||||||
 | 
					pub const HOUR: usize = 60 * MINUTE;
 | 
				
			||||||
 | 
					pub const DAY: usize = 24 * HOUR;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub const CHARACTERS: &str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use std::{collections::HashSet, env, iter::FromIterator};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use lazy_static::lazy_static;
 | 
				
			||||||
 | 
					use serenity::model::prelude::AttachmentType;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					lazy_static! {
 | 
				
			||||||
 | 
					    pub static ref DEFAULT_AVATAR: AttachmentType<'static> = (
 | 
				
			||||||
 | 
					        include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/../assets/webhook.jpg")) as &[u8],
 | 
				
			||||||
 | 
					        "webhook.jpg",
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					        .into();
 | 
				
			||||||
 | 
					    pub static ref SUBSCRIPTION_ROLES: HashSet<u64> = HashSet::from_iter(
 | 
				
			||||||
 | 
					        env::var("PATREON_ROLE_ID")
 | 
				
			||||||
 | 
					            .map(|var| var
 | 
				
			||||||
 | 
					                .split(',')
 | 
				
			||||||
 | 
					                .filter_map(|item| { item.parse::<u64>().ok() })
 | 
				
			||||||
 | 
					                .collect::<Vec<u64>>())
 | 
				
			||||||
 | 
					            .unwrap_or_else(|_| Vec::new())
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    pub static ref CNC_GUILD: Option<u64> =
 | 
				
			||||||
 | 
					        env::var("PATREON_GUILD_ID").map(|var| var.parse::<u64>().ok()).ok().flatten();
 | 
				
			||||||
 | 
					    pub static ref MIN_INTERVAL: u32 = env::var("MIN_INTERVAL")
 | 
				
			||||||
 | 
					        .ok()
 | 
				
			||||||
 | 
					        .map(|inner| inner.parse::<u32>().ok())
 | 
				
			||||||
 | 
					        .flatten()
 | 
				
			||||||
 | 
					        .unwrap_or(600);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										1
									
								
								web/src/guards/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								web/src/guards/mod.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					pub(crate) mod transaction;
 | 
				
			||||||
							
								
								
									
										44
									
								
								web/src/guards/transaction.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								web/src/guards/transaction.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,44 @@
 | 
				
			|||||||
 | 
					use rocket::{
 | 
				
			||||||
 | 
					    http::Status,
 | 
				
			||||||
 | 
					    request::{FromRequest, Outcome},
 | 
				
			||||||
 | 
					    Request, State,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					use sqlx::Pool;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::Database;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub struct Transaction<'a>(sqlx::Transaction<'a, Database>);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl Transaction<'_> {
 | 
				
			||||||
 | 
					    pub fn executor(&mut self) -> impl sqlx::Executor<'_, Database = Database> {
 | 
				
			||||||
 | 
					        &mut *(self.0)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub async fn commit(self) -> Result<(), sqlx::Error> {
 | 
				
			||||||
 | 
					        self.0.commit().await
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Debug)]
 | 
				
			||||||
 | 
					pub enum TransactionError {
 | 
				
			||||||
 | 
					    Error(sqlx::Error),
 | 
				
			||||||
 | 
					    Missing,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[rocket::async_trait]
 | 
				
			||||||
 | 
					impl<'r> FromRequest<'r> for Transaction<'r> {
 | 
				
			||||||
 | 
					    type Error = TransactionError;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
 | 
				
			||||||
 | 
					        match request.guard::<&State<Pool<Database>>>().await {
 | 
				
			||||||
 | 
					            Outcome::Success(pool) => match pool.begin().await {
 | 
				
			||||||
 | 
					                Ok(transaction) => Outcome::Success(Transaction(transaction)),
 | 
				
			||||||
 | 
					                Err(e) => {
 | 
				
			||||||
 | 
					                    Outcome::Failure((Status::InternalServerError, TransactionError::Error(e)))
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            Outcome::Failure(e) => Outcome::Failure((e.0, TransactionError::Missing)),
 | 
				
			||||||
 | 
					            Outcome::Forward(f) => Outcome::Forward(f),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										255
									
								
								web/src/lib.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										255
									
								
								web/src/lib.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,255 @@
 | 
				
			|||||||
 | 
					#[macro_use]
 | 
				
			||||||
 | 
					extern crate rocket;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					mod consts;
 | 
				
			||||||
 | 
					#[macro_use]
 | 
				
			||||||
 | 
					mod macros;
 | 
				
			||||||
 | 
					mod catchers;
 | 
				
			||||||
 | 
					mod guards;
 | 
				
			||||||
 | 
					mod routes;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use std::{env, path::Path};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use oauth2::{basic::BasicClient, AuthUrl, ClientId, ClientSecret, RedirectUrl, TokenUrl};
 | 
				
			||||||
 | 
					use rocket::{
 | 
				
			||||||
 | 
					    fs::FileServer,
 | 
				
			||||||
 | 
					    http::CookieJar,
 | 
				
			||||||
 | 
					    serde::json::{json, Value as JsonValue},
 | 
				
			||||||
 | 
					    tokio::sync::broadcast::Sender,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					use rocket_dyn_templates::Template;
 | 
				
			||||||
 | 
					use serenity::{
 | 
				
			||||||
 | 
					    client::Context,
 | 
				
			||||||
 | 
					    http::CacheHttp,
 | 
				
			||||||
 | 
					    model::id::{GuildId, UserId},
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					use sqlx::{MySql, Pool};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::consts::{CNC_GUILD, DISCORD_OAUTH_AUTHORIZE, DISCORD_OAUTH_TOKEN, SUBSCRIPTION_ROLES};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type Database = MySql;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Debug)]
 | 
				
			||||||
 | 
					enum Error {
 | 
				
			||||||
 | 
					    SQLx(sqlx::Error),
 | 
				
			||||||
 | 
					    Serenity(serenity::Error),
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub async fn initialize(
 | 
				
			||||||
 | 
					    kill_channel: Sender<()>,
 | 
				
			||||||
 | 
					    serenity_context: Context,
 | 
				
			||||||
 | 
					    db_pool: Pool<Database>,
 | 
				
			||||||
 | 
					) -> Result<(), Box<dyn std::error::Error>> {
 | 
				
			||||||
 | 
					    info!("Checking environment variables...");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if env::var("OFFLINE").map_or(true, |v| v != "1") {
 | 
				
			||||||
 | 
					        env::var("OAUTH2_CLIENT_ID").expect("`OAUTH2_CLIENT_ID' not supplied");
 | 
				
			||||||
 | 
					        env::var("OAUTH2_CLIENT_SECRET").expect("`OAUTH2_CLIENT_SECRET' not supplied");
 | 
				
			||||||
 | 
					        env::var("OAUTH2_DISCORD_CALLBACK").expect("`OAUTH2_DISCORD_CALLBACK' not supplied");
 | 
				
			||||||
 | 
					        env::var("PATREON_GUILD_ID").expect("`PATREON_GUILD_ID' not supplied");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    info!("Done!");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let oauth2_client = BasicClient::new(
 | 
				
			||||||
 | 
					        ClientId::new(env::var("OAUTH2_CLIENT_ID")?),
 | 
				
			||||||
 | 
					        Some(ClientSecret::new(env::var("OAUTH2_CLIENT_SECRET")?)),
 | 
				
			||||||
 | 
					        AuthUrl::new(DISCORD_OAUTH_AUTHORIZE.to_string())?,
 | 
				
			||||||
 | 
					        Some(TokenUrl::new(DISCORD_OAUTH_TOKEN.to_string())?),
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    .set_redirect_uri(RedirectUrl::new(env::var("OAUTH2_DISCORD_CALLBACK")?)?);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let reqwest_client = reqwest::Client::new();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let static_path =
 | 
				
			||||||
 | 
					        if Path::new("web/static").exists() { "web/static" } else { "/lib/reminder-rs/static" };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    rocket::build()
 | 
				
			||||||
 | 
					        .attach(Template::fairing())
 | 
				
			||||||
 | 
					        .register(
 | 
				
			||||||
 | 
					            "/",
 | 
				
			||||||
 | 
					            catchers![
 | 
				
			||||||
 | 
					                catchers::not_authorized,
 | 
				
			||||||
 | 
					                catchers::forbidden,
 | 
				
			||||||
 | 
					                catchers::not_found,
 | 
				
			||||||
 | 
					                catchers::internal_server_error,
 | 
				
			||||||
 | 
					                catchers::unprocessable_entity,
 | 
				
			||||||
 | 
					                catchers::payload_too_large,
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        .manage(oauth2_client)
 | 
				
			||||||
 | 
					        .manage(reqwest_client)
 | 
				
			||||||
 | 
					        .manage(serenity_context)
 | 
				
			||||||
 | 
					        .manage(db_pool)
 | 
				
			||||||
 | 
					        .mount("/static", FileServer::from(static_path))
 | 
				
			||||||
 | 
					        .mount(
 | 
				
			||||||
 | 
					            "/",
 | 
				
			||||||
 | 
					            routes![
 | 
				
			||||||
 | 
					                routes::index,
 | 
				
			||||||
 | 
					                routes::cookies,
 | 
				
			||||||
 | 
					                routes::privacy,
 | 
				
			||||||
 | 
					                routes::terms,
 | 
				
			||||||
 | 
					                routes::return_to_same_site,
 | 
				
			||||||
 | 
					                routes::report::report_error,
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        .mount(
 | 
				
			||||||
 | 
					            "/help",
 | 
				
			||||||
 | 
					            routes![
 | 
				
			||||||
 | 
					                routes::help,
 | 
				
			||||||
 | 
					                routes::help_timezone,
 | 
				
			||||||
 | 
					                routes::help_create_reminder,
 | 
				
			||||||
 | 
					                routes::help_delete_reminder,
 | 
				
			||||||
 | 
					                routes::help_timers,
 | 
				
			||||||
 | 
					                routes::help_todo_lists,
 | 
				
			||||||
 | 
					                routes::help_macros,
 | 
				
			||||||
 | 
					                routes::help_intervals,
 | 
				
			||||||
 | 
					                routes::help_dashboard,
 | 
				
			||||||
 | 
					                routes::help_iemanager,
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        .mount(
 | 
				
			||||||
 | 
					            "/login",
 | 
				
			||||||
 | 
					            routes![
 | 
				
			||||||
 | 
					                routes::login::discord_login,
 | 
				
			||||||
 | 
					                routes::login::discord_logout,
 | 
				
			||||||
 | 
					                routes::login::discord_callback
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        .mount(
 | 
				
			||||||
 | 
					            "/dashboard",
 | 
				
			||||||
 | 
					            routes![
 | 
				
			||||||
 | 
					                routes::dashboard::dashboard,
 | 
				
			||||||
 | 
					                routes::dashboard::dashboard_home,
 | 
				
			||||||
 | 
					                routes::dashboard::api::user::get_user_info,
 | 
				
			||||||
 | 
					                routes::dashboard::api::user::update_user_info,
 | 
				
			||||||
 | 
					                routes::dashboard::api::user::get_user_guilds,
 | 
				
			||||||
 | 
					                routes::dashboard::api::guild::get_guild_info,
 | 
				
			||||||
 | 
					                routes::dashboard::api::guild::get_guild_channels,
 | 
				
			||||||
 | 
					                routes::dashboard::api::guild::get_guild_roles,
 | 
				
			||||||
 | 
					                routes::dashboard::api::guild::get_reminder_templates,
 | 
				
			||||||
 | 
					                routes::dashboard::api::guild::create_reminder_template,
 | 
				
			||||||
 | 
					                routes::dashboard::api::guild::delete_reminder_template,
 | 
				
			||||||
 | 
					                routes::dashboard::api::guild::create_guild_reminder,
 | 
				
			||||||
 | 
					                routes::dashboard::api::guild::get_reminders,
 | 
				
			||||||
 | 
					                routes::dashboard::api::guild::edit_reminder,
 | 
				
			||||||
 | 
					                routes::dashboard::api::guild::delete_reminder,
 | 
				
			||||||
 | 
					                routes::dashboard::export::export_reminders,
 | 
				
			||||||
 | 
					                routes::dashboard::export::export_reminder_templates,
 | 
				
			||||||
 | 
					                routes::dashboard::export::export_todos,
 | 
				
			||||||
 | 
					                routes::dashboard::export::import_reminders,
 | 
				
			||||||
 | 
					                routes::dashboard::export::import_todos,
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        .mount("/admin", routes![routes::admin::admin_dashboard_home, routes::admin::bot_data])
 | 
				
			||||||
 | 
					        .launch()
 | 
				
			||||||
 | 
					        .await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    warn!("Exiting rocket runtime");
 | 
				
			||||||
 | 
					    // distribute kill signal
 | 
				
			||||||
 | 
					    match kill_channel.send(()) {
 | 
				
			||||||
 | 
					        Ok(_) => {}
 | 
				
			||||||
 | 
					        Err(e) => {
 | 
				
			||||||
 | 
					            error!("Failed to issue kill signal: {:?}", e);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Ok(())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub async fn check_subscription(cache_http: impl CacheHttp, user_id: impl Into<UserId>) -> bool {
 | 
				
			||||||
 | 
					    offline!(true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if let Some(subscription_guild) = *CNC_GUILD {
 | 
				
			||||||
 | 
					        let guild_member = GuildId(subscription_guild).member(cache_http, user_id).await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if let Ok(member) = guild_member {
 | 
				
			||||||
 | 
					            for role in member.roles {
 | 
				
			||||||
 | 
					                if SUBSCRIPTION_ROLES.contains(role.as_u64()) {
 | 
				
			||||||
 | 
					                    return true;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        false
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        true
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub async fn check_guild_subscription(
 | 
				
			||||||
 | 
					    cache_http: impl CacheHttp,
 | 
				
			||||||
 | 
					    guild_id: impl Into<GuildId>,
 | 
				
			||||||
 | 
					) -> bool {
 | 
				
			||||||
 | 
					    offline!(true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if let Some(guild) = cache_http.cache().unwrap().guild(guild_id) {
 | 
				
			||||||
 | 
					        let owner = guild.owner_id;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        check_subscription(&cache_http, owner).await
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        false
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub async fn check_authorization(
 | 
				
			||||||
 | 
					    cookies: &CookieJar<'_>,
 | 
				
			||||||
 | 
					    ctx: &Context,
 | 
				
			||||||
 | 
					    guild: u64,
 | 
				
			||||||
 | 
					) -> Result<(), JsonValue> {
 | 
				
			||||||
 | 
					    let user_id = cookies.get_private("userid").map(|c| c.value().parse::<u64>().ok()).flatten();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if std::env::var("OFFLINE").map_or(true, |v| v != "1") {
 | 
				
			||||||
 | 
					        match user_id {
 | 
				
			||||||
 | 
					            Some(user_id) => {
 | 
				
			||||||
 | 
					                let admin_id = std::env::var("ADMIN_ID")
 | 
				
			||||||
 | 
					                    .map_or(false, |u| u.parse::<u64>().map_or(false, |u| u == user_id));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if admin_id {
 | 
				
			||||||
 | 
					                    return Ok(());
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                match GuildId(guild).to_guild_cached(ctx) {
 | 
				
			||||||
 | 
					                    Some(guild) => {
 | 
				
			||||||
 | 
					                        let member_res = guild.member(ctx, UserId(user_id)).await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        match member_res {
 | 
				
			||||||
 | 
					                            Err(_) => {
 | 
				
			||||||
 | 
					                                return Err(json!({"error": "User not in guild"}));
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                            Ok(member) => {
 | 
				
			||||||
 | 
					                                let permissions_res = member.permissions(ctx);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                                match permissions_res {
 | 
				
			||||||
 | 
					                                    Err(_) => {
 | 
				
			||||||
 | 
					                                        return Err(json!({"error": "Couldn't fetch permissions"}));
 | 
				
			||||||
 | 
					                                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                                    Ok(permissions) => {
 | 
				
			||||||
 | 
					                                        if !(permissions.manage_messages()
 | 
				
			||||||
 | 
					                                            || permissions.manage_guild()
 | 
				
			||||||
 | 
					                                            || permissions.administrator())
 | 
				
			||||||
 | 
					                                        {
 | 
				
			||||||
 | 
					                                            return Err(json!({"error": "Incorrect permissions"}));
 | 
				
			||||||
 | 
					                                        }
 | 
				
			||||||
 | 
					                                    }
 | 
				
			||||||
 | 
					                                }
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    None => {
 | 
				
			||||||
 | 
					                        return Err(json!({"error": "Bot not in guild"}));
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            None => {
 | 
				
			||||||
 | 
					                return Err(json!({"error": "User not authorized"}));
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Ok(())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										99
									
								
								web/src/macros.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										99
									
								
								web/src/macros.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,99 @@
 | 
				
			|||||||
 | 
					macro_rules! offline {
 | 
				
			||||||
 | 
					    ($field:expr) => {
 | 
				
			||||||
 | 
					        if std::env::var("OFFLINE").map_or(false, |v| v == "1") {
 | 
				
			||||||
 | 
					            return $field;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					macro_rules! check_length {
 | 
				
			||||||
 | 
					    ($max:ident, $field:expr) => {
 | 
				
			||||||
 | 
					        if $field.len() > $max {
 | 
				
			||||||
 | 
					            return Err(json!({ "error": format!("{} exceeded", stringify!($max)) }));
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    ($max:ident, $field:expr, $($fields:expr),+) => {
 | 
				
			||||||
 | 
					        check_length!($max, $field);
 | 
				
			||||||
 | 
					        check_length!($max, $($fields),+);
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					macro_rules! check_length_opt {
 | 
				
			||||||
 | 
					    ($max:ident, $field:expr) => {
 | 
				
			||||||
 | 
					        if let Some(field) = &$field {
 | 
				
			||||||
 | 
					            check_length!($max, field);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    ($max:ident, $field:expr, $($fields:expr),+) => {
 | 
				
			||||||
 | 
					        check_length_opt!($max, $field);
 | 
				
			||||||
 | 
					        check_length_opt!($max, $($fields),+);
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					macro_rules! check_url {
 | 
				
			||||||
 | 
					    ($field:expr) => {
 | 
				
			||||||
 | 
					        if !($field.starts_with("http://") || $field.starts_with("https://")) {
 | 
				
			||||||
 | 
					            return Err(json!({ "error": "URL invalid" }));
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    ($field:expr, $($fields:expr),+) => {
 | 
				
			||||||
 | 
					        check_url!($max, $field);
 | 
				
			||||||
 | 
					        check_url!($max, $($fields),+);
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					macro_rules! check_url_opt {
 | 
				
			||||||
 | 
					    ($field:expr) => {
 | 
				
			||||||
 | 
					        if let Some(field) = &$field {
 | 
				
			||||||
 | 
					            check_url!(field);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    ($field:expr, $($fields:expr),+) => {
 | 
				
			||||||
 | 
					        check_url_opt!($field);
 | 
				
			||||||
 | 
					        check_url_opt!($($fields),+);
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					macro_rules! update_field {
 | 
				
			||||||
 | 
					    ($pool:expr, $error:ident, $reminder:ident.[$field:ident]) => {
 | 
				
			||||||
 | 
					        if let Some(value) = &$reminder.$field {
 | 
				
			||||||
 | 
					            match sqlx::query(concat!(
 | 
				
			||||||
 | 
					                "UPDATE reminders SET `",
 | 
				
			||||||
 | 
					                stringify!($field),
 | 
				
			||||||
 | 
					                "` = ? WHERE uid = ?"
 | 
				
			||||||
 | 
					            ))
 | 
				
			||||||
 | 
					            .bind(value)
 | 
				
			||||||
 | 
					            .bind(&$reminder.uid)
 | 
				
			||||||
 | 
					            .execute($pool)
 | 
				
			||||||
 | 
					            .await
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                Ok(_) => {}
 | 
				
			||||||
 | 
					                Err(e) => {
 | 
				
			||||||
 | 
					                    warn!(
 | 
				
			||||||
 | 
					                        concat!(
 | 
				
			||||||
 | 
					                            "Error in `update_field!(",
 | 
				
			||||||
 | 
					                            stringify!($pool),
 | 
				
			||||||
 | 
					                            stringify!($reminder),
 | 
				
			||||||
 | 
					                            stringify!($field),
 | 
				
			||||||
 | 
					                            ")': {:?}"
 | 
				
			||||||
 | 
					                        ),
 | 
				
			||||||
 | 
					                        e
 | 
				
			||||||
 | 
					                    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    $error.push(format!("Error setting field {}", stringify!($field)));
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    ($pool:expr, $error:ident, $reminder:ident.[$field:ident, $($fields:ident),+]) => {
 | 
				
			||||||
 | 
					        update_field!($pool, $error, $reminder.[$field]);
 | 
				
			||||||
 | 
					        update_field!($pool, $error, $reminder.[$($fields),+]);
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					macro_rules! json_err {
 | 
				
			||||||
 | 
					    ($message:expr) => {
 | 
				
			||||||
 | 
					        Err(json!({ "error": $message }))
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										218
									
								
								web/src/routes/admin.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										218
									
								
								web/src/routes/admin.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,218 @@
 | 
				
			|||||||
 | 
					use std::{collections::HashMap, env};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use chrono::{DateTime, Utc};
 | 
				
			||||||
 | 
					use rocket::{
 | 
				
			||||||
 | 
					    http::{CookieJar, Status},
 | 
				
			||||||
 | 
					    serde::json::json,
 | 
				
			||||||
 | 
					    State,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					use rocket_dyn_templates::Template;
 | 
				
			||||||
 | 
					use serde::Serialize;
 | 
				
			||||||
 | 
					use sqlx::{MySql, Pool};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::routes::JsonResult;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fn is_admin(cookies: &CookieJar<'_>) -> bool {
 | 
				
			||||||
 | 
					    cookies
 | 
				
			||||||
 | 
					        .get_private("userid")
 | 
				
			||||||
 | 
					        .map_or(false, |cookie| Some(cookie.value().to_string()) == env::var("ADMIN_ID").ok())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[get("/")]
 | 
				
			||||||
 | 
					pub async fn admin_dashboard_home(cookies: &CookieJar<'_>) -> Result<Template, Status> {
 | 
				
			||||||
 | 
					    if let Some(cookie) = cookies.get_private("userid") {
 | 
				
			||||||
 | 
					        let map: HashMap<&str, String> = HashMap::new();
 | 
				
			||||||
 | 
					        if Some(cookie.value().to_string()) == env::var("ADMIN_ID").ok() {
 | 
				
			||||||
 | 
					            Ok(Template::render("admin_dashboard", &map))
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            Err(Status::Forbidden)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        Err(Status::Unauthorized)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Serialize)]
 | 
				
			||||||
 | 
					struct TimeFrame {
 | 
				
			||||||
 | 
					    time_key: DateTime<Utc>,
 | 
				
			||||||
 | 
					    count: i64,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[get("/data")]
 | 
				
			||||||
 | 
					pub async fn bot_data(cookies: &CookieJar<'_>, pool: &State<Pool<MySql>>) -> JsonResult {
 | 
				
			||||||
 | 
					    if !is_admin(cookies) {
 | 
				
			||||||
 | 
					        return json_err!("Not authorized");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let backlog = sqlx::query!(
 | 
				
			||||||
 | 
					        "SELECT COUNT(1) AS backlog FROM reminders WHERE `utc_time` < NOW() AND enabled = 1 AND `status` = 'pending'"
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    .fetch_one(pool.inner())
 | 
				
			||||||
 | 
					    .await
 | 
				
			||||||
 | 
					    .unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let schedule_once = sqlx::query_as_unchecked!(
 | 
				
			||||||
 | 
					        TimeFrame,
 | 
				
			||||||
 | 
					        "SELECT
 | 
				
			||||||
 | 
					            FROM_UNIXTIME(FLOOR(UNIX_TIMESTAMP(`utc_time`) / 300) * 300) AS `time_key`,
 | 
				
			||||||
 | 
					            COUNT(1) AS `count`
 | 
				
			||||||
 | 
					        FROM reminders
 | 
				
			||||||
 | 
					        WHERE
 | 
				
			||||||
 | 
					            `utc_time` < DATE_ADD(NOW(), INTERVAL 1 DAY) AND
 | 
				
			||||||
 | 
					            `utc_time` >= NOW() AND
 | 
				
			||||||
 | 
					            `enabled` = 1 AND
 | 
				
			||||||
 | 
					            `status` = 'pending' AND
 | 
				
			||||||
 | 
					            `interval_seconds` IS NULL AND
 | 
				
			||||||
 | 
					            `interval_months` IS NULL AND
 | 
				
			||||||
 | 
					            `interval_days` IS NULL
 | 
				
			||||||
 | 
					        GROUP BY `time_key`
 | 
				
			||||||
 | 
					        ORDER BY `time_key`"
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    .fetch_all(pool.inner())
 | 
				
			||||||
 | 
					    .await
 | 
				
			||||||
 | 
					    .unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let schedule_interval = sqlx::query_as_unchecked!(
 | 
				
			||||||
 | 
					        TimeFrame,
 | 
				
			||||||
 | 
					        "SELECT
 | 
				
			||||||
 | 
					            FROM_UNIXTIME(FLOOR(UNIX_TIMESTAMP(`utc_time`) / 300) * 300) AS `time_key`,
 | 
				
			||||||
 | 
					            COUNT(1) AS `count`
 | 
				
			||||||
 | 
					        FROM reminders
 | 
				
			||||||
 | 
					        WHERE
 | 
				
			||||||
 | 
					            `utc_time` < DATE_ADD(NOW(), INTERVAL 1 DAY) AND
 | 
				
			||||||
 | 
					            `utc_time` >= NOW() AND
 | 
				
			||||||
 | 
					            `status` = 'pending' AND
 | 
				
			||||||
 | 
					            `enabled` = 1 AND (
 | 
				
			||||||
 | 
					                `interval_seconds` IS NOT NULL OR
 | 
				
			||||||
 | 
					                `interval_months` IS NOT NULL OR
 | 
				
			||||||
 | 
					                `interval_days` IS NOT NULL
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        GROUP BY `time_key`
 | 
				
			||||||
 | 
					        ORDER BY `time_key`"
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    .fetch_all(pool.inner())
 | 
				
			||||||
 | 
					    .await
 | 
				
			||||||
 | 
					    .unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let schedule_once_long = sqlx::query_as_unchecked!(
 | 
				
			||||||
 | 
					        TimeFrame,
 | 
				
			||||||
 | 
					        "SELECT
 | 
				
			||||||
 | 
					            FROM_UNIXTIME(FLOOR(UNIX_TIMESTAMP(`utc_time`) / 86400) * 86400) AS `time_key`,
 | 
				
			||||||
 | 
					            COUNT(1) AS `count`
 | 
				
			||||||
 | 
					        FROM reminders
 | 
				
			||||||
 | 
					        WHERE
 | 
				
			||||||
 | 
					            `utc_time` < DATE_ADD(NOW(), INTERVAL 31 DAY) AND
 | 
				
			||||||
 | 
					            `utc_time` >= NOW() AND
 | 
				
			||||||
 | 
					            `enabled` = 1 AND
 | 
				
			||||||
 | 
					            `status` = 'pending' AND
 | 
				
			||||||
 | 
					            `interval_seconds` IS NULL AND
 | 
				
			||||||
 | 
					            `interval_months` IS NULL AND
 | 
				
			||||||
 | 
					            `interval_days` IS NULL
 | 
				
			||||||
 | 
					        GROUP BY `time_key`
 | 
				
			||||||
 | 
					        ORDER BY `time_key`"
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    .fetch_all(pool.inner())
 | 
				
			||||||
 | 
					    .await
 | 
				
			||||||
 | 
					    .unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let schedule_interval_long = sqlx::query_as_unchecked!(
 | 
				
			||||||
 | 
					        TimeFrame,
 | 
				
			||||||
 | 
					        "SELECT
 | 
				
			||||||
 | 
					            FROM_UNIXTIME(FLOOR(UNIX_TIMESTAMP(`utc_time`) / 86400) * 86400) AS `time_key`,
 | 
				
			||||||
 | 
					            COUNT(1) AS `count`
 | 
				
			||||||
 | 
					        FROM reminders
 | 
				
			||||||
 | 
					        WHERE
 | 
				
			||||||
 | 
					            `utc_time` < DATE_ADD(NOW(), INTERVAL 31 DAY) AND
 | 
				
			||||||
 | 
					            `utc_time` >= NOW() AND
 | 
				
			||||||
 | 
					            `status` = 'pending' AND
 | 
				
			||||||
 | 
					            `enabled` = 1 AND (
 | 
				
			||||||
 | 
					                `interval_seconds` IS NOT NULL OR
 | 
				
			||||||
 | 
					                `interval_months` IS NOT NULL OR
 | 
				
			||||||
 | 
					                `interval_days` IS NOT NULL
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        GROUP BY `time_key`
 | 
				
			||||||
 | 
					        ORDER BY `time_key`"
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    .fetch_all(pool.inner())
 | 
				
			||||||
 | 
					    .await
 | 
				
			||||||
 | 
					    .unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let history = sqlx::query_as_unchecked!(
 | 
				
			||||||
 | 
					        TimeFrame,
 | 
				
			||||||
 | 
					        "SELECT
 | 
				
			||||||
 | 
					            FROM_UNIXTIME(FLOOR(UNIX_TIMESTAMP(`utc_time`) / 86400) * 86400) AS `time_key`,
 | 
				
			||||||
 | 
					            COUNT(1) AS `count`
 | 
				
			||||||
 | 
					        FROM stat
 | 
				
			||||||
 | 
					        WHERE
 | 
				
			||||||
 | 
					            `utc_time` > DATE_SUB(NOW(), INTERVAL 31 DAY) AND
 | 
				
			||||||
 | 
					            `type` = 'reminder_sent'
 | 
				
			||||||
 | 
					        GROUP BY `time_key`
 | 
				
			||||||
 | 
					        ORDER BY `time_key`"
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    .fetch_all(pool.inner())
 | 
				
			||||||
 | 
					    .await
 | 
				
			||||||
 | 
					    .unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let history_failed = sqlx::query_as_unchecked!(
 | 
				
			||||||
 | 
					        TimeFrame,
 | 
				
			||||||
 | 
					        "SELECT
 | 
				
			||||||
 | 
					            FROM_UNIXTIME(FLOOR(UNIX_TIMESTAMP(`utc_time`) / 86400) * 86400) AS `time_key`,
 | 
				
			||||||
 | 
					            COUNT(1) AS `count`
 | 
				
			||||||
 | 
					        FROM stat
 | 
				
			||||||
 | 
					        WHERE
 | 
				
			||||||
 | 
					            `utc_time` > DATE_SUB(NOW(), INTERVAL 31 DAY) AND
 | 
				
			||||||
 | 
					            `type` = 'reminder_failed'
 | 
				
			||||||
 | 
					        GROUP BY `time_key`
 | 
				
			||||||
 | 
					        ORDER BY `time_key`"
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    .fetch_all(pool.inner())
 | 
				
			||||||
 | 
					    .await
 | 
				
			||||||
 | 
					    .unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let interval_count = sqlx::query!(
 | 
				
			||||||
 | 
					        "SELECT COUNT(1) AS count
 | 
				
			||||||
 | 
					        FROM reminders
 | 
				
			||||||
 | 
					        WHERE
 | 
				
			||||||
 | 
					            `status` = 'pending' AND (
 | 
				
			||||||
 | 
					                `interval_seconds` IS NOT NULL OR
 | 
				
			||||||
 | 
					                `interval_months` IS NOT NULL OR
 | 
				
			||||||
 | 
					                `interval_days` IS NOT NULL
 | 
				
			||||||
 | 
					            )"
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    .fetch_one(pool.inner())
 | 
				
			||||||
 | 
					    .await
 | 
				
			||||||
 | 
					    .unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let reminder_count = sqlx::query!(
 | 
				
			||||||
 | 
					        "SELECT COUNT(1) AS count
 | 
				
			||||||
 | 
					        FROM reminders
 | 
				
			||||||
 | 
					        WHERE
 | 
				
			||||||
 | 
					            `status` = 'pending' AND
 | 
				
			||||||
 | 
					            `interval_seconds` IS NULL AND
 | 
				
			||||||
 | 
					            `interval_months` IS NULL AND
 | 
				
			||||||
 | 
					            `interval_days` IS NULL"
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    .fetch_one(pool.inner())
 | 
				
			||||||
 | 
					    .await
 | 
				
			||||||
 | 
					    .unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Ok(json!({
 | 
				
			||||||
 | 
					        "backlog": backlog.backlog,
 | 
				
			||||||
 | 
					        "scheduleShort": {
 | 
				
			||||||
 | 
					            "once": schedule_once,
 | 
				
			||||||
 | 
					            "interval": schedule_interval
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        "scheduleLong": {
 | 
				
			||||||
 | 
					            "once": schedule_once_long,
 | 
				
			||||||
 | 
					            "interval": schedule_interval_long,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        "historyLong": {
 | 
				
			||||||
 | 
					            "sent": history,
 | 
				
			||||||
 | 
					            "failed": history_failed,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        "count": {
 | 
				
			||||||
 | 
					            "reminders": reminder_count.count,
 | 
				
			||||||
 | 
					            "intervals": interval_count.count,
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										61
									
								
								web/src/routes/dashboard/api/guild/channels.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								web/src/routes/dashboard/api/guild/channels.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,61 @@
 | 
				
			|||||||
 | 
					use rocket::{http::CookieJar, serde::json::json, State};
 | 
				
			||||||
 | 
					use serde::Serialize;
 | 
				
			||||||
 | 
					use serenity::{
 | 
				
			||||||
 | 
					    client::Context,
 | 
				
			||||||
 | 
					    model::{
 | 
				
			||||||
 | 
					        channel::GuildChannel,
 | 
				
			||||||
 | 
					        id::{ChannelId, GuildId},
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::{check_authorization, routes::JsonResult};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Serialize)]
 | 
				
			||||||
 | 
					struct ChannelInfo {
 | 
				
			||||||
 | 
					    id: String,
 | 
				
			||||||
 | 
					    name: String,
 | 
				
			||||||
 | 
					    webhook_avatar: Option<String>,
 | 
				
			||||||
 | 
					    webhook_name: Option<String>,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[get("/api/guild/<id>/channels")]
 | 
				
			||||||
 | 
					pub async fn get_guild_channels(
 | 
				
			||||||
 | 
					    id: u64,
 | 
				
			||||||
 | 
					    cookies: &CookieJar<'_>,
 | 
				
			||||||
 | 
					    ctx: &State<Context>,
 | 
				
			||||||
 | 
					) -> JsonResult {
 | 
				
			||||||
 | 
					    offline!(Ok(json!(vec![ChannelInfo {
 | 
				
			||||||
 | 
					        name: "general".to_string(),
 | 
				
			||||||
 | 
					        id: "1".to_string(),
 | 
				
			||||||
 | 
					        webhook_avatar: None,
 | 
				
			||||||
 | 
					        webhook_name: None,
 | 
				
			||||||
 | 
					    }])));
 | 
				
			||||||
 | 
					    check_authorization(cookies, ctx.inner(), id).await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    match GuildId(id).to_guild_cached(ctx.inner()) {
 | 
				
			||||||
 | 
					        Some(guild) => {
 | 
				
			||||||
 | 
					            let mut channels = guild
 | 
				
			||||||
 | 
					                .channels
 | 
				
			||||||
 | 
					                .iter()
 | 
				
			||||||
 | 
					                .filter_map(|(id, channel)| channel.to_owned().guild().map(|c| (id.to_owned(), c)))
 | 
				
			||||||
 | 
					                .filter(|(_, channel)| channel.is_text_based())
 | 
				
			||||||
 | 
					                .collect::<Vec<(ChannelId, GuildChannel)>>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            channels.sort_by(|(_, c1), (_, c2)| c1.position.cmp(&c2.position));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            let channel_info = channels
 | 
				
			||||||
 | 
					                .iter()
 | 
				
			||||||
 | 
					                .map(|(channel_id, channel)| ChannelInfo {
 | 
				
			||||||
 | 
					                    name: channel.name.to_string(),
 | 
				
			||||||
 | 
					                    id: channel_id.to_string(),
 | 
				
			||||||
 | 
					                    webhook_avatar: None,
 | 
				
			||||||
 | 
					                    webhook_name: None,
 | 
				
			||||||
 | 
					                })
 | 
				
			||||||
 | 
					                .collect::<Vec<ChannelInfo>>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            Ok(json!(channel_info))
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        None => json_err!("Bot not in guild"),
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										42
									
								
								web/src/routes/dashboard/api/guild/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								web/src/routes/dashboard/api/guild/mod.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,42 @@
 | 
				
			|||||||
 | 
					mod channels;
 | 
				
			||||||
 | 
					mod reminders;
 | 
				
			||||||
 | 
					mod roles;
 | 
				
			||||||
 | 
					mod templates;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use std::env;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub use channels::*;
 | 
				
			||||||
 | 
					pub use reminders::*;
 | 
				
			||||||
 | 
					use rocket::{http::CookieJar, serde::json::json, State};
 | 
				
			||||||
 | 
					pub use roles::*;
 | 
				
			||||||
 | 
					use serenity::{
 | 
				
			||||||
 | 
					    client::Context,
 | 
				
			||||||
 | 
					    model::id::{GuildId, RoleId},
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					pub use templates::*;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::{check_authorization, routes::JsonResult};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[get("/api/guild/<id>")]
 | 
				
			||||||
 | 
					pub async fn get_guild_info(id: u64, cookies: &CookieJar<'_>, ctx: &State<Context>) -> JsonResult {
 | 
				
			||||||
 | 
					    offline!(Ok(json!({ "patreon": true, "name": "Guild" })));
 | 
				
			||||||
 | 
					    check_authorization(cookies, ctx.inner(), id).await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    match GuildId(id).to_guild_cached(ctx.inner()) {
 | 
				
			||||||
 | 
					        Some(guild) => {
 | 
				
			||||||
 | 
					            let member_res = GuildId(env::var("PATREON_GUILD_ID").unwrap().parse().unwrap())
 | 
				
			||||||
 | 
					                .member(&ctx.inner(), guild.owner_id)
 | 
				
			||||||
 | 
					                .await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            let patreon = member_res.map_or(false, |member| {
 | 
				
			||||||
 | 
					                member
 | 
				
			||||||
 | 
					                    .roles
 | 
				
			||||||
 | 
					                    .contains(&RoleId(env::var("PATREON_ROLE_ID").unwrap().parse().unwrap()))
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            Ok(json!({ "patreon": patreon, "name": guild.name }))
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        None => json_err!("Bot not in guild"),
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										373
									
								
								web/src/routes/dashboard/api/guild/reminders.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										373
									
								
								web/src/routes/dashboard/api/guild/reminders.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,373 @@
 | 
				
			|||||||
 | 
					use rocket::{
 | 
				
			||||||
 | 
					    http::CookieJar,
 | 
				
			||||||
 | 
					    serde::json::{json, Json},
 | 
				
			||||||
 | 
					    State,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					use serenity::{
 | 
				
			||||||
 | 
					    client::Context,
 | 
				
			||||||
 | 
					    model::id::{ChannelId, GuildId, UserId},
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					use sqlx::{MySql, Pool};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::{
 | 
				
			||||||
 | 
					    check_authorization, check_guild_subscription, check_subscription,
 | 
				
			||||||
 | 
					    consts::MIN_INTERVAL,
 | 
				
			||||||
 | 
					    guards::transaction::Transaction,
 | 
				
			||||||
 | 
					    routes::{
 | 
				
			||||||
 | 
					        dashboard::{
 | 
				
			||||||
 | 
					            create_database_channel, create_reminder, DeleteReminder, PatchReminder, Reminder,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        JsonResult,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    Database,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[post("/api/guild/<id>/reminders", data = "<reminder>")]
 | 
				
			||||||
 | 
					pub async fn create_guild_reminder(
 | 
				
			||||||
 | 
					    id: u64,
 | 
				
			||||||
 | 
					    reminder: Json<Reminder>,
 | 
				
			||||||
 | 
					    cookies: &CookieJar<'_>,
 | 
				
			||||||
 | 
					    ctx: &State<Context>,
 | 
				
			||||||
 | 
					    mut transaction: Transaction<'_>,
 | 
				
			||||||
 | 
					) -> JsonResult {
 | 
				
			||||||
 | 
					    check_authorization(cookies, ctx.inner(), id).await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let user_id =
 | 
				
			||||||
 | 
					        cookies.get_private("userid").map(|c| c.value().parse::<u64>().ok()).flatten().unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    match create_reminder(
 | 
				
			||||||
 | 
					        ctx.inner(),
 | 
				
			||||||
 | 
					        &mut transaction,
 | 
				
			||||||
 | 
					        GuildId(id),
 | 
				
			||||||
 | 
					        UserId(user_id),
 | 
				
			||||||
 | 
					        reminder.into_inner(),
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    .await
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        Ok(r) => match transaction.commit().await {
 | 
				
			||||||
 | 
					            Ok(_) => Ok(r),
 | 
				
			||||||
 | 
					            Err(e) => {
 | 
				
			||||||
 | 
					                warn!("Couldn't commit transaction: {:?}", e);
 | 
				
			||||||
 | 
					                json_err!("Couldn't commit transaction.")
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Err(e) => Err(e),
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[get("/api/guild/<id>/reminders")]
 | 
				
			||||||
 | 
					pub async fn get_reminders(
 | 
				
			||||||
 | 
					    id: u64,
 | 
				
			||||||
 | 
					    cookies: &CookieJar<'_>,
 | 
				
			||||||
 | 
					    ctx: &State<Context>,
 | 
				
			||||||
 | 
					    pool: &State<Pool<MySql>>,
 | 
				
			||||||
 | 
					) -> JsonResult {
 | 
				
			||||||
 | 
					    check_authorization(cookies, ctx.inner(), id).await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let channels_res = GuildId(id).channels(&ctx.inner()).await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    match channels_res {
 | 
				
			||||||
 | 
					        Ok(channels) => {
 | 
				
			||||||
 | 
					            let channels = channels
 | 
				
			||||||
 | 
					                .keys()
 | 
				
			||||||
 | 
					                .into_iter()
 | 
				
			||||||
 | 
					                .map(|k| k.as_u64().to_string())
 | 
				
			||||||
 | 
					                .collect::<Vec<String>>()
 | 
				
			||||||
 | 
					                .join(",");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            sqlx::query_as_unchecked!(
 | 
				
			||||||
 | 
					                Reminder,
 | 
				
			||||||
 | 
					                "SELECT
 | 
				
			||||||
 | 
					                 reminders.attachment,
 | 
				
			||||||
 | 
					                 reminders.attachment_name,
 | 
				
			||||||
 | 
					                 reminders.avatar,
 | 
				
			||||||
 | 
					                 channels.channel,
 | 
				
			||||||
 | 
					                 reminders.content,
 | 
				
			||||||
 | 
					                 reminders.embed_author,
 | 
				
			||||||
 | 
					                 reminders.embed_author_url,
 | 
				
			||||||
 | 
					                 reminders.embed_color,
 | 
				
			||||||
 | 
					                 reminders.embed_description,
 | 
				
			||||||
 | 
					                 reminders.embed_footer,
 | 
				
			||||||
 | 
					                 reminders.embed_footer_url,
 | 
				
			||||||
 | 
					                 reminders.embed_image_url,
 | 
				
			||||||
 | 
					                 reminders.embed_thumbnail_url,
 | 
				
			||||||
 | 
					                 reminders.embed_title,
 | 
				
			||||||
 | 
					                 IFNULL(reminders.embed_fields, '[]') AS embed_fields,
 | 
				
			||||||
 | 
					                 reminders.enabled,
 | 
				
			||||||
 | 
					                 reminders.expires,
 | 
				
			||||||
 | 
					                 reminders.interval_seconds,
 | 
				
			||||||
 | 
					                 reminders.interval_days,
 | 
				
			||||||
 | 
					                 reminders.interval_months,
 | 
				
			||||||
 | 
					                 reminders.name,
 | 
				
			||||||
 | 
					                 reminders.restartable,
 | 
				
			||||||
 | 
					                 reminders.tts,
 | 
				
			||||||
 | 
					                 reminders.uid,
 | 
				
			||||||
 | 
					                 reminders.username,
 | 
				
			||||||
 | 
					                 reminders.utc_time
 | 
				
			||||||
 | 
					                FROM reminders
 | 
				
			||||||
 | 
					                LEFT JOIN channels ON channels.id = reminders.channel_id
 | 
				
			||||||
 | 
					                WHERE `status` = 'pending' AND FIND_IN_SET(channels.channel, ?)",
 | 
				
			||||||
 | 
					                channels
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            .fetch_all(pool.inner())
 | 
				
			||||||
 | 
					            .await
 | 
				
			||||||
 | 
					            .map(|r| Ok(json!(r)))
 | 
				
			||||||
 | 
					            .unwrap_or_else(|e| {
 | 
				
			||||||
 | 
					                warn!("Failed to complete SQL query: {:?}", e);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                json_err!("Could not load reminders")
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        Err(e) => {
 | 
				
			||||||
 | 
					            warn!("Could not fetch channels from {}: {:?}", id, e);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            Ok(json!([]))
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[patch("/api/guild/<id>/reminders", data = "<reminder>")]
 | 
				
			||||||
 | 
					pub async fn edit_reminder(
 | 
				
			||||||
 | 
					    id: u64,
 | 
				
			||||||
 | 
					    reminder: Json<PatchReminder>,
 | 
				
			||||||
 | 
					    ctx: &State<Context>,
 | 
				
			||||||
 | 
					    mut transaction: Transaction<'_>,
 | 
				
			||||||
 | 
					    pool: &State<Pool<Database>>,
 | 
				
			||||||
 | 
					    cookies: &CookieJar<'_>,
 | 
				
			||||||
 | 
					) -> JsonResult {
 | 
				
			||||||
 | 
					    check_authorization(cookies, ctx.inner(), id).await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let mut error = vec![];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let user_id =
 | 
				
			||||||
 | 
					        cookies.get_private("userid").map(|c| c.value().parse::<u64>().ok()).flatten().unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if reminder.message_ok() {
 | 
				
			||||||
 | 
					        update_field!(transaction.executor(), error, reminder.[
 | 
				
			||||||
 | 
					            content,
 | 
				
			||||||
 | 
					            embed_author,
 | 
				
			||||||
 | 
					            embed_description,
 | 
				
			||||||
 | 
					            embed_footer,
 | 
				
			||||||
 | 
					            embed_title,
 | 
				
			||||||
 | 
					            embed_fields,
 | 
				
			||||||
 | 
					            username
 | 
				
			||||||
 | 
					        ]);
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        error.push("Message exceeds limits.".to_string());
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    update_field!(transaction.executor(), error, reminder.[
 | 
				
			||||||
 | 
					        attachment,
 | 
				
			||||||
 | 
					        attachment_name,
 | 
				
			||||||
 | 
					        avatar,
 | 
				
			||||||
 | 
					        embed_author_url,
 | 
				
			||||||
 | 
					        embed_color,
 | 
				
			||||||
 | 
					        embed_footer_url,
 | 
				
			||||||
 | 
					        embed_image_url,
 | 
				
			||||||
 | 
					        embed_thumbnail_url,
 | 
				
			||||||
 | 
					        enabled,
 | 
				
			||||||
 | 
					        expires,
 | 
				
			||||||
 | 
					        name,
 | 
				
			||||||
 | 
					        restartable,
 | 
				
			||||||
 | 
					        tts,
 | 
				
			||||||
 | 
					        utc_time
 | 
				
			||||||
 | 
					    ]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if reminder.interval_days.flatten().is_some()
 | 
				
			||||||
 | 
					        || reminder.interval_months.flatten().is_some()
 | 
				
			||||||
 | 
					        || reminder.interval_seconds.flatten().is_some()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        if check_guild_subscription(&ctx.inner(), id).await
 | 
				
			||||||
 | 
					            || check_subscription(&ctx.inner(), user_id).await
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            let new_interval_length = match reminder.interval_days {
 | 
				
			||||||
 | 
					                Some(interval) => interval.unwrap_or(0),
 | 
				
			||||||
 | 
					                None => sqlx::query!(
 | 
				
			||||||
 | 
					                    "SELECT interval_days AS days FROM reminders WHERE uid = ?",
 | 
				
			||||||
 | 
					                    reminder.uid
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					                .fetch_one(transaction.executor())
 | 
				
			||||||
 | 
					                .await
 | 
				
			||||||
 | 
					                .map_err(|e| {
 | 
				
			||||||
 | 
					                    warn!("Error updating reminder interval: {:?}", e);
 | 
				
			||||||
 | 
					                    json!({ "reminder": Option::<Reminder>::None, "errors": vec!["Unknown error"] })
 | 
				
			||||||
 | 
					                })?
 | 
				
			||||||
 | 
					                .days
 | 
				
			||||||
 | 
					                .unwrap_or(0),
 | 
				
			||||||
 | 
					            } * 86400 + match reminder.interval_months {
 | 
				
			||||||
 | 
					                Some(interval) => interval.unwrap_or(0),
 | 
				
			||||||
 | 
					                None => sqlx::query!(
 | 
				
			||||||
 | 
					                    "SELECT interval_months AS months FROM reminders WHERE uid = ?",
 | 
				
			||||||
 | 
					                    reminder.uid
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					                .fetch_one(transaction.executor())
 | 
				
			||||||
 | 
					                .await
 | 
				
			||||||
 | 
					                .map_err(|e| {
 | 
				
			||||||
 | 
					                    warn!("Error updating reminder interval: {:?}", e);
 | 
				
			||||||
 | 
					                    json!({ "reminder": Option::<Reminder>::None, "errors": vec!["Unknown error"] })
 | 
				
			||||||
 | 
					                })?
 | 
				
			||||||
 | 
					                .months
 | 
				
			||||||
 | 
					                .unwrap_or(0),
 | 
				
			||||||
 | 
					            } * 2592000 + match reminder.interval_seconds {
 | 
				
			||||||
 | 
					                Some(interval) => interval.unwrap_or(0),
 | 
				
			||||||
 | 
					                None => sqlx::query!(
 | 
				
			||||||
 | 
					                    "SELECT interval_seconds AS seconds FROM reminders WHERE uid = ?",
 | 
				
			||||||
 | 
					                    reminder.uid
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					                .fetch_one(transaction.executor())
 | 
				
			||||||
 | 
					                .await
 | 
				
			||||||
 | 
					                .map_err(|e| {
 | 
				
			||||||
 | 
					                    warn!("Error updating reminder interval: {:?}", e);
 | 
				
			||||||
 | 
					                    json!({ "reminder": Option::<Reminder>::None, "errors": vec!["Unknown error"] })
 | 
				
			||||||
 | 
					                })?
 | 
				
			||||||
 | 
					                .seconds
 | 
				
			||||||
 | 
					                .unwrap_or(0),
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if new_interval_length < *MIN_INTERVAL {
 | 
				
			||||||
 | 
					                error.push(String::from("New interval is too short."));
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                update_field!(transaction.executor(), error, reminder.[
 | 
				
			||||||
 | 
					                    interval_days,
 | 
				
			||||||
 | 
					                    interval_months,
 | 
				
			||||||
 | 
					                    interval_seconds
 | 
				
			||||||
 | 
					                ]);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if reminder.channel > 0 {
 | 
				
			||||||
 | 
					        let channel = ChannelId(reminder.channel).to_channel_cached(&ctx.inner());
 | 
				
			||||||
 | 
					        match channel {
 | 
				
			||||||
 | 
					            Some(channel) => {
 | 
				
			||||||
 | 
					                let channel_matches_guild = channel.guild().map_or(false, |c| c.guild_id.0 == id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if !channel_matches_guild {
 | 
				
			||||||
 | 
					                    warn!(
 | 
				
			||||||
 | 
					                        "Error in `edit_reminder`: channel {:?} not found for guild {}",
 | 
				
			||||||
 | 
					                        reminder.channel, id
 | 
				
			||||||
 | 
					                    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    return Err(json!({"error": "Channel not found"}));
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                let channel = create_database_channel(
 | 
				
			||||||
 | 
					                    ctx.inner(),
 | 
				
			||||||
 | 
					                    ChannelId(reminder.channel),
 | 
				
			||||||
 | 
					                    &mut transaction,
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					                .await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if let Err(e) = channel {
 | 
				
			||||||
 | 
					                    warn!("`create_database_channel` returned an error code: {:?}", e);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    return Err(
 | 
				
			||||||
 | 
					                        json!({"error": "Failed to configure channel for reminders. Please check the bot permissions"}),
 | 
				
			||||||
 | 
					                    );
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                let channel = channel.unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                match sqlx::query!(
 | 
				
			||||||
 | 
					                    "UPDATE reminders SET channel_id = ? WHERE uid = ?",
 | 
				
			||||||
 | 
					                    channel,
 | 
				
			||||||
 | 
					                    reminder.uid
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					                .execute(transaction.executor())
 | 
				
			||||||
 | 
					                .await
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    Ok(_) => {}
 | 
				
			||||||
 | 
					                    Err(e) => {
 | 
				
			||||||
 | 
					                        warn!("Error setting channel: {:?}", e);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        error.push("Couldn't set channel".to_string())
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            None => {
 | 
				
			||||||
 | 
					                warn!(
 | 
				
			||||||
 | 
					                    "Error in `edit_reminder`: channel {:?} not found for guild {}",
 | 
				
			||||||
 | 
					                    reminder.channel, id
 | 
				
			||||||
 | 
					                );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                return Err(json!({"error": "Channel not found"}));
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if let Err(e) = transaction.commit().await {
 | 
				
			||||||
 | 
					        warn!("Couldn't commit transaction: {:?}", e);
 | 
				
			||||||
 | 
					        return json_err!("Couldn't commit transaction");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    match sqlx::query_as_unchecked!(
 | 
				
			||||||
 | 
					        Reminder,
 | 
				
			||||||
 | 
					        "SELECT reminders.attachment,
 | 
				
			||||||
 | 
					         reminders.attachment_name,
 | 
				
			||||||
 | 
					         reminders.avatar,
 | 
				
			||||||
 | 
					         channels.channel,
 | 
				
			||||||
 | 
					         reminders.content,
 | 
				
			||||||
 | 
					         reminders.embed_author,
 | 
				
			||||||
 | 
					         reminders.embed_author_url,
 | 
				
			||||||
 | 
					         reminders.embed_color,
 | 
				
			||||||
 | 
					         reminders.embed_description,
 | 
				
			||||||
 | 
					         reminders.embed_footer,
 | 
				
			||||||
 | 
					         reminders.embed_footer_url,
 | 
				
			||||||
 | 
					         reminders.embed_image_url,
 | 
				
			||||||
 | 
					         reminders.embed_thumbnail_url,
 | 
				
			||||||
 | 
					         reminders.embed_title,
 | 
				
			||||||
 | 
					         reminders.embed_fields,
 | 
				
			||||||
 | 
					         reminders.enabled,
 | 
				
			||||||
 | 
					         reminders.expires,
 | 
				
			||||||
 | 
					         reminders.interval_seconds,
 | 
				
			||||||
 | 
					         reminders.interval_days,
 | 
				
			||||||
 | 
					         reminders.interval_months,
 | 
				
			||||||
 | 
					         reminders.name,
 | 
				
			||||||
 | 
					         reminders.restartable,
 | 
				
			||||||
 | 
					         reminders.tts,
 | 
				
			||||||
 | 
					         reminders.uid,
 | 
				
			||||||
 | 
					         reminders.username,
 | 
				
			||||||
 | 
					         reminders.utc_time
 | 
				
			||||||
 | 
					        FROM reminders
 | 
				
			||||||
 | 
					        LEFT JOIN channels ON channels.id = reminders.channel_id
 | 
				
			||||||
 | 
					        WHERE uid = ?",
 | 
				
			||||||
 | 
					        reminder.uid
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    .fetch_one(pool.inner())
 | 
				
			||||||
 | 
					    .await
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        Ok(reminder) => Ok(json!({"reminder": reminder, "errors": error})),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Err(e) => {
 | 
				
			||||||
 | 
					            warn!("Error exiting `edit_reminder': {:?}", e);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            Err(json!({"reminder": Option::<Reminder>::None, "errors": vec!["Unknown error"]}))
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[delete("/api/guild/<id>/reminders", data = "<reminder>")]
 | 
				
			||||||
 | 
					pub async fn delete_reminder(
 | 
				
			||||||
 | 
					    cookies: &CookieJar<'_>,
 | 
				
			||||||
 | 
					    id: u64,
 | 
				
			||||||
 | 
					    reminder: Json<DeleteReminder>,
 | 
				
			||||||
 | 
					    ctx: &State<Context>,
 | 
				
			||||||
 | 
					    pool: &State<Pool<MySql>>,
 | 
				
			||||||
 | 
					) -> JsonResult {
 | 
				
			||||||
 | 
					    check_authorization(cookies, ctx.inner(), id).await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    match sqlx::query!("UPDATE reminders SET `status` = 'deleted' WHERE uid = ?", reminder.uid)
 | 
				
			||||||
 | 
					        .execute(pool.inner())
 | 
				
			||||||
 | 
					        .await
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        Ok(_) => Ok(json!({})),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Err(e) => {
 | 
				
			||||||
 | 
					            warn!("Error in `delete_reminder`: {:?}", e);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            Err(json!({"error": "Could not delete reminder"}))
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										35
									
								
								web/src/routes/dashboard/api/guild/roles.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								web/src/routes/dashboard/api/guild/roles.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,35 @@
 | 
				
			|||||||
 | 
					use rocket::{http::CookieJar, serde::json::json, State};
 | 
				
			||||||
 | 
					use serde::Serialize;
 | 
				
			||||||
 | 
					use serenity::client::Context;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::{check_authorization, routes::JsonResult};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Serialize)]
 | 
				
			||||||
 | 
					struct RoleInfo {
 | 
				
			||||||
 | 
					    id: String,
 | 
				
			||||||
 | 
					    name: String,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[get("/api/guild/<id>/roles")]
 | 
				
			||||||
 | 
					pub async fn get_guild_roles(id: u64, cookies: &CookieJar<'_>, ctx: &State<Context>) -> JsonResult {
 | 
				
			||||||
 | 
					    offline!(Ok(json!(vec![RoleInfo { name: "@everyone".to_string(), id: "1".to_string() }])));
 | 
				
			||||||
 | 
					    check_authorization(cookies, ctx.inner(), id).await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let roles_res = ctx.cache.guild_roles(id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    match roles_res {
 | 
				
			||||||
 | 
					        Some(roles) => {
 | 
				
			||||||
 | 
					            let roles = roles
 | 
				
			||||||
 | 
					                .iter()
 | 
				
			||||||
 | 
					                .map(|(_, r)| RoleInfo { id: r.id.to_string(), name: r.name.to_string() })
 | 
				
			||||||
 | 
					                .collect::<Vec<RoleInfo>>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            Ok(json!(roles))
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        None => {
 | 
				
			||||||
 | 
					            warn!("Could not fetch roles from {}", id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            json_err!("Could not get roles")
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										181
									
								
								web/src/routes/dashboard/api/guild/templates.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										181
									
								
								web/src/routes/dashboard/api/guild/templates.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,181 @@
 | 
				
			|||||||
 | 
					use rocket::{
 | 
				
			||||||
 | 
					    http::CookieJar,
 | 
				
			||||||
 | 
					    serde::json::{json, Json},
 | 
				
			||||||
 | 
					    State,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					use serenity::client::Context;
 | 
				
			||||||
 | 
					use sqlx::{MySql, Pool};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::{
 | 
				
			||||||
 | 
					    check_authorization,
 | 
				
			||||||
 | 
					    consts::{
 | 
				
			||||||
 | 
					        MAX_CONTENT_LENGTH, MAX_EMBED_AUTHOR_LENGTH, MAX_EMBED_DESCRIPTION_LENGTH,
 | 
				
			||||||
 | 
					        MAX_EMBED_FIELDS, MAX_EMBED_FIELD_TITLE_LENGTH, MAX_EMBED_FIELD_VALUE_LENGTH,
 | 
				
			||||||
 | 
					        MAX_EMBED_FOOTER_LENGTH, MAX_EMBED_TITLE_LENGTH, MAX_URL_LENGTH, MAX_USERNAME_LENGTH,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    routes::{
 | 
				
			||||||
 | 
					        dashboard::{template_name_default, DeleteReminderTemplate, ReminderTemplate},
 | 
				
			||||||
 | 
					        JsonResult,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[get("/api/guild/<id>/templates")]
 | 
				
			||||||
 | 
					pub async fn get_reminder_templates(
 | 
				
			||||||
 | 
					    id: u64,
 | 
				
			||||||
 | 
					    cookies: &CookieJar<'_>,
 | 
				
			||||||
 | 
					    ctx: &State<Context>,
 | 
				
			||||||
 | 
					    pool: &State<Pool<MySql>>,
 | 
				
			||||||
 | 
					) -> JsonResult {
 | 
				
			||||||
 | 
					    check_authorization(cookies, ctx.inner(), id).await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    match sqlx::query_as_unchecked!(
 | 
				
			||||||
 | 
					        ReminderTemplate,
 | 
				
			||||||
 | 
					        "SELECT * FROM reminder_template WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?)",
 | 
				
			||||||
 | 
					        id
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    .fetch_all(pool.inner())
 | 
				
			||||||
 | 
					    .await
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        Ok(templates) => Ok(json!(templates)),
 | 
				
			||||||
 | 
					        Err(e) => {
 | 
				
			||||||
 | 
					            warn!("Could not fetch templates from {}: {:?}", id, e);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            json_err!("Could not get templates")
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[post("/api/guild/<id>/templates", data = "<reminder_template>")]
 | 
				
			||||||
 | 
					pub async fn create_reminder_template(
 | 
				
			||||||
 | 
					    id: u64,
 | 
				
			||||||
 | 
					    reminder_template: Json<ReminderTemplate>,
 | 
				
			||||||
 | 
					    cookies: &CookieJar<'_>,
 | 
				
			||||||
 | 
					    ctx: &State<Context>,
 | 
				
			||||||
 | 
					    pool: &State<Pool<MySql>>,
 | 
				
			||||||
 | 
					) -> JsonResult {
 | 
				
			||||||
 | 
					    check_authorization(cookies, ctx.inner(), id).await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // validate lengths
 | 
				
			||||||
 | 
					    check_length!(MAX_CONTENT_LENGTH, reminder_template.content);
 | 
				
			||||||
 | 
					    check_length!(MAX_EMBED_DESCRIPTION_LENGTH, reminder_template.embed_description);
 | 
				
			||||||
 | 
					    check_length!(MAX_EMBED_TITLE_LENGTH, reminder_template.embed_title);
 | 
				
			||||||
 | 
					    check_length!(MAX_EMBED_AUTHOR_LENGTH, reminder_template.embed_author);
 | 
				
			||||||
 | 
					    check_length!(MAX_EMBED_FOOTER_LENGTH, reminder_template.embed_footer);
 | 
				
			||||||
 | 
					    check_length_opt!(MAX_EMBED_FIELDS, reminder_template.embed_fields);
 | 
				
			||||||
 | 
					    if let Some(fields) = &reminder_template.embed_fields {
 | 
				
			||||||
 | 
					        for field in &fields.0 {
 | 
				
			||||||
 | 
					            check_length!(MAX_EMBED_FIELD_VALUE_LENGTH, field.value);
 | 
				
			||||||
 | 
					            check_length!(MAX_EMBED_FIELD_TITLE_LENGTH, field.title);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    check_length_opt!(MAX_USERNAME_LENGTH, reminder_template.username);
 | 
				
			||||||
 | 
					    check_length_opt!(
 | 
				
			||||||
 | 
					        MAX_URL_LENGTH,
 | 
				
			||||||
 | 
					        reminder_template.embed_footer_url,
 | 
				
			||||||
 | 
					        reminder_template.embed_thumbnail_url,
 | 
				
			||||||
 | 
					        reminder_template.embed_author_url,
 | 
				
			||||||
 | 
					        reminder_template.embed_image_url,
 | 
				
			||||||
 | 
					        reminder_template.avatar
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // validate urls
 | 
				
			||||||
 | 
					    check_url_opt!(
 | 
				
			||||||
 | 
					        reminder_template.embed_footer_url,
 | 
				
			||||||
 | 
					        reminder_template.embed_thumbnail_url,
 | 
				
			||||||
 | 
					        reminder_template.embed_author_url,
 | 
				
			||||||
 | 
					        reminder_template.embed_image_url,
 | 
				
			||||||
 | 
					        reminder_template.avatar
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let name = if reminder_template.name.is_empty() {
 | 
				
			||||||
 | 
					        template_name_default()
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        reminder_template.name.clone()
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    match sqlx::query!(
 | 
				
			||||||
 | 
					        "INSERT INTO reminder_template
 | 
				
			||||||
 | 
					        (guild_id,
 | 
				
			||||||
 | 
					         name,
 | 
				
			||||||
 | 
					         attachment,
 | 
				
			||||||
 | 
					         attachment_name,
 | 
				
			||||||
 | 
					         avatar,
 | 
				
			||||||
 | 
					         content,
 | 
				
			||||||
 | 
					         embed_author,
 | 
				
			||||||
 | 
					         embed_author_url,
 | 
				
			||||||
 | 
					         embed_color,
 | 
				
			||||||
 | 
					         embed_description,
 | 
				
			||||||
 | 
					         embed_footer,
 | 
				
			||||||
 | 
					         embed_footer_url,
 | 
				
			||||||
 | 
					         embed_image_url,
 | 
				
			||||||
 | 
					         embed_thumbnail_url,
 | 
				
			||||||
 | 
					         embed_title,
 | 
				
			||||||
 | 
					         embed_fields,
 | 
				
			||||||
 | 
					         interval_seconds,
 | 
				
			||||||
 | 
					         interval_days,
 | 
				
			||||||
 | 
					         interval_months,
 | 
				
			||||||
 | 
					         tts,
 | 
				
			||||||
 | 
					         username
 | 
				
			||||||
 | 
					        ) VALUES ((SELECT id FROM guilds WHERE guild = ?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
 | 
				
			||||||
 | 
					         ?, ?, ?, ?, ?, ?, ?)",
 | 
				
			||||||
 | 
					        id,
 | 
				
			||||||
 | 
					        name,
 | 
				
			||||||
 | 
					        reminder_template.attachment,
 | 
				
			||||||
 | 
					        reminder_template.attachment_name,
 | 
				
			||||||
 | 
					        reminder_template.avatar,
 | 
				
			||||||
 | 
					        reminder_template.content,
 | 
				
			||||||
 | 
					        reminder_template.embed_author,
 | 
				
			||||||
 | 
					        reminder_template.embed_author_url,
 | 
				
			||||||
 | 
					        reminder_template.embed_color,
 | 
				
			||||||
 | 
					        reminder_template.embed_description,
 | 
				
			||||||
 | 
					        reminder_template.embed_footer,
 | 
				
			||||||
 | 
					        reminder_template.embed_footer_url,
 | 
				
			||||||
 | 
					        reminder_template.embed_image_url,
 | 
				
			||||||
 | 
					        reminder_template.embed_thumbnail_url,
 | 
				
			||||||
 | 
					        reminder_template.embed_title,
 | 
				
			||||||
 | 
					        reminder_template.embed_fields,
 | 
				
			||||||
 | 
					        reminder_template.interval_seconds,
 | 
				
			||||||
 | 
					        reminder_template.interval_days,
 | 
				
			||||||
 | 
					        reminder_template.interval_months,
 | 
				
			||||||
 | 
					        reminder_template.tts,
 | 
				
			||||||
 | 
					        reminder_template.username,
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    .fetch_all(pool.inner())
 | 
				
			||||||
 | 
					    .await
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        Ok(_) => Ok(json!({})),
 | 
				
			||||||
 | 
					        Err(e) => {
 | 
				
			||||||
 | 
					            warn!("Could not create template for {}: {:?}", id, e);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            json_err!("Could not create template")
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[delete("/api/guild/<id>/templates", data = "<delete_reminder_template>")]
 | 
				
			||||||
 | 
					pub async fn delete_reminder_template(
 | 
				
			||||||
 | 
					    id: u64,
 | 
				
			||||||
 | 
					    delete_reminder_template: Json<DeleteReminderTemplate>,
 | 
				
			||||||
 | 
					    cookies: &CookieJar<'_>,
 | 
				
			||||||
 | 
					    ctx: &State<Context>,
 | 
				
			||||||
 | 
					    pool: &State<Pool<MySql>>,
 | 
				
			||||||
 | 
					) -> JsonResult {
 | 
				
			||||||
 | 
					    check_authorization(cookies, ctx.inner(), id).await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    match sqlx::query!(
 | 
				
			||||||
 | 
					        "DELETE FROM reminder_template WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?) AND id = ?",
 | 
				
			||||||
 | 
					        id, delete_reminder_template.id
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    .fetch_all(pool.inner())
 | 
				
			||||||
 | 
					    .await
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        Ok(_) => {
 | 
				
			||||||
 | 
					            Ok(json!({}))
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        Err(e) => {
 | 
				
			||||||
 | 
					            warn!("Could not delete template from {}: {:?}", id, e);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            json_err!("Could not delete template")
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										2
									
								
								web/src/routes/dashboard/api/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								web/src/routes/dashboard/api/mod.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,2 @@
 | 
				
			|||||||
 | 
					pub mod guild;
 | 
				
			||||||
 | 
					pub mod user;
 | 
				
			||||||
							
								
								
									
										81
									
								
								web/src/routes/dashboard/api/user/guilds.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								web/src/routes/dashboard/api/user/guilds.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,81 @@
 | 
				
			|||||||
 | 
					use reqwest::Client;
 | 
				
			||||||
 | 
					use rocket::{
 | 
				
			||||||
 | 
					    http::CookieJar,
 | 
				
			||||||
 | 
					    serde::json::{json, Value as JsonValue},
 | 
				
			||||||
 | 
					    State,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					use serde::{Deserialize, Serialize};
 | 
				
			||||||
 | 
					use serenity::model::{id::GuildId, permissions::Permissions};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::consts::DISCORD_API;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Serialize)]
 | 
				
			||||||
 | 
					struct GuildInfo {
 | 
				
			||||||
 | 
					    id: String,
 | 
				
			||||||
 | 
					    name: String,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Deserialize)]
 | 
				
			||||||
 | 
					struct PartialGuild {
 | 
				
			||||||
 | 
					    pub id: GuildId,
 | 
				
			||||||
 | 
					    pub name: String,
 | 
				
			||||||
 | 
					    #[serde(default)]
 | 
				
			||||||
 | 
					    pub owner: bool,
 | 
				
			||||||
 | 
					    #[serde(rename = "permissions_new")]
 | 
				
			||||||
 | 
					    pub permissions: Option<String>,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[get("/api/user/guilds")]
 | 
				
			||||||
 | 
					pub async fn get_user_guilds(cookies: &CookieJar<'_>, reqwest_client: &State<Client>) -> JsonValue {
 | 
				
			||||||
 | 
					    offline!(json!(vec![GuildInfo { id: "1".to_string(), name: "Guild".to_string() }]));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if let Some(access_token) = cookies.get_private("access_token") {
 | 
				
			||||||
 | 
					        let request_res = reqwest_client
 | 
				
			||||||
 | 
					            .get(format!("{}/users/@me/guilds", DISCORD_API))
 | 
				
			||||||
 | 
					            .bearer_auth(access_token.value())
 | 
				
			||||||
 | 
					            .send()
 | 
				
			||||||
 | 
					            .await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        match request_res {
 | 
				
			||||||
 | 
					            Ok(response) => {
 | 
				
			||||||
 | 
					                let guilds_res = response.json::<Vec<PartialGuild>>().await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                match guilds_res {
 | 
				
			||||||
 | 
					                    Ok(guilds) => {
 | 
				
			||||||
 | 
					                        let reduced_guilds = guilds
 | 
				
			||||||
 | 
					                            .iter()
 | 
				
			||||||
 | 
					                            .filter(|g| {
 | 
				
			||||||
 | 
					                                g.owner
 | 
				
			||||||
 | 
					                                    || g.permissions.as_ref().map_or(false, |p| {
 | 
				
			||||||
 | 
					                                        let permissions =
 | 
				
			||||||
 | 
					                                            Permissions::from_bits_truncate(p.parse().unwrap());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                                        permissions.manage_messages()
 | 
				
			||||||
 | 
					                                            || permissions.manage_guild()
 | 
				
			||||||
 | 
					                                            || permissions.administrator()
 | 
				
			||||||
 | 
					                                    })
 | 
				
			||||||
 | 
					                            })
 | 
				
			||||||
 | 
					                            .map(|g| GuildInfo { id: g.id.to_string(), name: g.name.to_string() })
 | 
				
			||||||
 | 
					                            .collect::<Vec<GuildInfo>>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        json!(reduced_guilds)
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    Err(e) => {
 | 
				
			||||||
 | 
					                        warn!("Error constructing user from request: {:?}", e);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        json!({"error": "Could not get user details"})
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            Err(e) => {
 | 
				
			||||||
 | 
					                warn!("Error getting user guilds: {:?}", e);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                json!({"error": "Could not reach Discord"})
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        json!({"error": "Not authorized"})
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										97
									
								
								web/src/routes/dashboard/api/user/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										97
									
								
								web/src/routes/dashboard/api/user/mod.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,97 @@
 | 
				
			|||||||
 | 
					mod guilds;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use std::env;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use chrono_tz::Tz;
 | 
				
			||||||
 | 
					pub use guilds::*;
 | 
				
			||||||
 | 
					use rocket::{
 | 
				
			||||||
 | 
					    http::CookieJar,
 | 
				
			||||||
 | 
					    serde::json::{json, Json, Value as JsonValue},
 | 
				
			||||||
 | 
					    State,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					use serde::{Deserialize, Serialize};
 | 
				
			||||||
 | 
					use serenity::{
 | 
				
			||||||
 | 
					    client::Context,
 | 
				
			||||||
 | 
					    model::id::{GuildId, RoleId},
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					use sqlx::{MySql, Pool};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Serialize)]
 | 
				
			||||||
 | 
					struct UserInfo {
 | 
				
			||||||
 | 
					    name: String,
 | 
				
			||||||
 | 
					    patreon: bool,
 | 
				
			||||||
 | 
					    timezone: Option<String>,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Deserialize)]
 | 
				
			||||||
 | 
					pub struct UpdateUser {
 | 
				
			||||||
 | 
					    timezone: String,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[get("/api/user")]
 | 
				
			||||||
 | 
					pub async fn get_user_info(
 | 
				
			||||||
 | 
					    cookies: &CookieJar<'_>,
 | 
				
			||||||
 | 
					    ctx: &State<Context>,
 | 
				
			||||||
 | 
					    pool: &State<Pool<MySql>>,
 | 
				
			||||||
 | 
					) -> JsonValue {
 | 
				
			||||||
 | 
					    offline!(json!(UserInfo { name: "Discord".to_string(), patreon: true, timezone: None }));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if let Some(user_id) =
 | 
				
			||||||
 | 
					        cookies.get_private("userid").map(|u| u.value().parse::<u64>().ok()).flatten()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        let member_res = GuildId(env::var("PATREON_GUILD_ID").unwrap().parse().unwrap())
 | 
				
			||||||
 | 
					            .member(&ctx.inner(), user_id)
 | 
				
			||||||
 | 
					            .await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let timezone = sqlx::query!(
 | 
				
			||||||
 | 
					            "SELECT IFNULL(timezone, 'UTC') AS timezone FROM users WHERE user = ?",
 | 
				
			||||||
 | 
					            user_id
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        .fetch_one(pool.inner())
 | 
				
			||||||
 | 
					        .await
 | 
				
			||||||
 | 
					        .map_or(None, |q| Some(q.timezone));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let user_info = UserInfo {
 | 
				
			||||||
 | 
					            name: cookies
 | 
				
			||||||
 | 
					                .get_private("username")
 | 
				
			||||||
 | 
					                .map_or("DiscordUser#0000".to_string(), |c| c.value().to_string()),
 | 
				
			||||||
 | 
					            patreon: member_res.map_or(false, |member| {
 | 
				
			||||||
 | 
					                member
 | 
				
			||||||
 | 
					                    .roles
 | 
				
			||||||
 | 
					                    .contains(&RoleId(env::var("PATREON_ROLE_ID").unwrap().parse().unwrap()))
 | 
				
			||||||
 | 
					            }),
 | 
				
			||||||
 | 
					            timezone,
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        json!(user_info)
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        json!({"error": "Not authorized"})
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[patch("/api/user", data = "<user>")]
 | 
				
			||||||
 | 
					pub async fn update_user_info(
 | 
				
			||||||
 | 
					    cookies: &CookieJar<'_>,
 | 
				
			||||||
 | 
					    user: Json<UpdateUser>,
 | 
				
			||||||
 | 
					    pool: &State<Pool<MySql>>,
 | 
				
			||||||
 | 
					) -> JsonValue {
 | 
				
			||||||
 | 
					    if let Some(user_id) =
 | 
				
			||||||
 | 
					        cookies.get_private("userid").map(|u| u.value().parse::<u64>().ok()).flatten()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        if user.timezone.parse::<Tz>().is_ok() {
 | 
				
			||||||
 | 
					            let _ = sqlx::query!(
 | 
				
			||||||
 | 
					                "UPDATE users SET timezone = ? WHERE user = ?",
 | 
				
			||||||
 | 
					                user.timezone,
 | 
				
			||||||
 | 
					                user_id,
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            .execute(pool.inner())
 | 
				
			||||||
 | 
					            .await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            json!({})
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            json!({"error": "Timezone not recognized"})
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        json!({"error": "Not authorized"})
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										20
									
								
								web/src/routes/dashboard/api/user/models.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								web/src/routes/dashboard/api/user/models.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,20 @@
 | 
				
			|||||||
 | 
					use std::env;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use chrono_tz::Tz;
 | 
				
			||||||
 | 
					use reqwest::Client;
 | 
				
			||||||
 | 
					use rocket::{
 | 
				
			||||||
 | 
					    http::CookieJar,
 | 
				
			||||||
 | 
					    serde::json::{json, Json, Value as JsonValue},
 | 
				
			||||||
 | 
					    State,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					use serde::{Deserialize, Serialize};
 | 
				
			||||||
 | 
					use serenity::{
 | 
				
			||||||
 | 
					    client::Context,
 | 
				
			||||||
 | 
					    model::{
 | 
				
			||||||
 | 
					        id::{GuildId, RoleId},
 | 
				
			||||||
 | 
					        permissions::Permissions,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					use sqlx::{MySql, Pool};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::{consts::DISCORD_API, routes::JsonResult};
 | 
				
			||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user