Compare commits
	
		
			289 Commits
		
	
	
		
			postgres
			...
			jude/fix-d
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					9a6b65f3a3 | ||
| 
						 | 
					b6ff149d51 | ||
| 
						 | 
					748e33566b | ||
| 
						 | 
					d7e90614c8 | ||
| 
						 | 
					b5dbfe336d | ||
| 
						 | 
					218be2f0b1 | ||
| 
						 | 
					d7515f3611 | ||
| 
						 | 
					6ae1096d79 | ||
| 
						 | 
					1f0d7adae3 | ||
| 
						 | 
					fc96ae526f | ||
| 
						 | 
					8881ef0f85 | ||
| 
						 | 
					5e82a687f9 | ||
| 
						 | 
					de4ecf8dd6 | ||
| 
						 | 
					064efd4386 | ||
| 
						 | 
					65b8ba3b47 | ||
| 9d452ed8cb | |||
| 
						 | 
					441419b92b | ||
| 
						 | 
					aecf2c15be | ||
| 
						 | 
					79da56c794 | ||
| 
						 | 
					ef10902c1e | ||
| 
						 | 
					c277f85c2a | ||
| 
						 | 
					035653c7fa | ||
| 
						 | 
					6358bc3deb | ||
| 
						 | 
					9f5066f982 | ||
| 
						 | 
					1d06999e41 | ||
| 
						 | 
					1cf707140c | ||
| 
						 | 
					e38c63f5ba | ||
| 
						 | 
					d52b8b26f2 | ||
| bb2128a7ed | |||
| 5e99a6f9de | |||
| 5406e6b8ec | |||
| 
						 | 
					4ee0bc4e37 | ||
| 
						 | 
					b99bb7dcbf | ||
| 
						 | 
					98f925dc84 | ||
| 
						 | 
					24e316b12f | ||
| 
						 | 
					4063334953 | ||
| 
						 | 
					e128b9848f | ||
| 
						 | 
					9989ab3b35 | ||
| 
						 | 
					b951db3f55 | ||
| 
						 | 
					884a47bf36 | ||
| 
						 | 
					b0f932445c | ||
| 
						 | 
					2861cdda0b | ||
| 
						 | 
					7ba8fcd6b7 | ||
| 
						 | 
					850f0fad57 | ||
| 
						 | 
					a770a17ee7 | ||
| 
						 | 
					d15a66d9d9 | ||
| 
						 | 
					30f011fcd5 | ||
| 
						 | 
					15dbed2f0f | ||
| 
						 | 
					18cac0345b | ||
| 
						 | 
					334b1bc084 | ||
| 
						 | 
					ba3c76c25f | ||
| 
						 | 
					67b6f30c62 | ||
| 
						 | 
					8ae311190f | ||
| 
						 | 
					016164affb | ||
| 
						 | 
					2c0aeef700 | ||
| 
						 | 
					ecd75d6f55 | ||
| 
						 | 
					4a80d42f86 | ||
| 
						 | 
					075fde71df | ||
| 
						 | 
					55136aecdc | ||
| 
						 | 
					63fc2cdcbc | ||
| 
						 | 
					3190738fc5 | ||
| 
						 | 
					8f4810b532 | ||
| 
						 | 
					a5e6c41fa5 | ||
| 
						 | 
					5f0aa0f834 | ||
| 
						 | 
					dbe8e8e358 | ||
| 
						 | 
					85a114e55c | ||
| 
						 | 
					329492b244 | ||
| 
						 | 
					66135ecd08 | ||
| 
						 | 
					382c2a5a1e | ||
| 
						 | 
					b91245a3f7 | ||
| 
						 | 
					6f0bdf9852 | ||
| 
						 | 
					dcee9e0d2a | ||
| 
						 | 
					8e6e1a18b7 | ||
| 
						 | 
					72af0532fa | ||
| 
						 | 
					e83b643d86 | ||
| 
						 | 
					0e0ab053f3 | ||
| 
						 | 
					8c2296b9c8 | ||
| 
						 | 
					1c6103142f | ||
| 
						 | 
					328127c55e | ||
| 
						 | 
					b0e37b56c0 | ||
| 
						 | 
					45f5b6261a | ||
| 
						 | 
					5f6326179c | ||
| 
						 | 
					6254f91841 | ||
| 
						 | 
					60b90a61d4 | ||
| 
						 | 
					90f05758d0 | ||
| 
						 | 
					74b7b5d711 | ||
| 
						 | 
					90550dc2c7 | ||
| 
						 | 
					79e6498245 | ||
| 
						 | 
					a8ef3d03f9 | ||
| 
						 | 
					53e13844f9 | ||
| 
						 | 
					dd7e681285 | ||
| 
						 | 
					6c20bf2a0f | ||
| 
						 | 
					15aa9ccffd | ||
| 
						 | 
					525471bcad | ||
| 
						 | 
					86d53b63b6 | ||
| 
						 | 
					d8f266852a | ||
| 
						 | 
					76a286076b | ||
| 
						 | 
					5e39e16060 | ||
| 
						 | 
					c1305cfb36 | ||
| 
						 | 
					4823754955 | ||
| 
						 | 
					eb92eacb90 | ||
| 
						 | 
					d0833b7bca | ||
| 
						 | 
					b81c3c80c1 | ||
| 
						 | 
					2f6d035efe | ||
| 
						 | 
					96012ce43c | ||
| 
						 | 
					fa7ec8731b | ||
| 
						 | 
					def43bfa78 | ||
| 
						 | 
					e4e9af2bb4 | ||
| 
						 | 
					cce0de7c75 | ||
| e7803b98e8 | |||
| 
						 | 
					7aae246388 | ||
| a2d442bc54 | |||
| 59982df827 | |||
| 
						 | 
					7a6372ed02 | ||
| 
						 | 
					14a54471f7 | ||
| 
						 | 
					5d3b77f1cd | ||
| 
						 | 
					1d64c8bb79 | ||
| 8ba0f02b98 | |||
| d36438c6ce | |||
| e0c60e2ce3 | |||
| 
						 | 
					e7160215b0 | ||
| 
						 | 
					6eaa6f0f28 | ||
| 
						 | 
					9db0fa2513 | ||
| 
						 | 
					ca13fd4fa7 | ||
| 
						 | 
					55acc8fd16 | ||
| 
						 | 
					145711fa5d | ||
| 
						 | 
					5524215786 | ||
| 
						 | 
					e8bd05893f | ||
| 
						 | 
					e3d3418f99 | ||
| 
						 | 
					2681280a39 | ||
| 
						 | 
					00579428a1 | ||
| 
						 | 
					b8ef999710 | ||
| 
						 | 
					e8f84e281a | ||
| 
						 | 
					8ddff698e5 | ||
| 
						 | 
					541633270c | ||
| 
						 | 
					25286da5e0 | ||
| 
						 | 
					4bad1324b9 | ||
| 
						 | 
					bd1462a00c | ||
| 
						 | 
					56ffc43616 | ||
| 
						 | 
					52cf642455 | ||
| 
						 | 
					0bf578357a | ||
| 
						 | 
					6e9eccb62e | ||
| 
						 | 
					6ea28284ce | ||
| 
						 | 
					a6525f3052 | ||
| 
						 | 
					348639270d | ||
| 
						 | 
					37177c2431 | ||
| 
						 | 
					8587bed703 | ||
| 
						 | 
					6c9af1ae8e | ||
| 
						 | 
					7695b7a476 | ||
| 651da7b28e | |||
| eb086146bf | |||
| 4ebd705e5e | |||
| 5a85f1d83a | |||
| 68ba25886a | |||
| 
						 | 
					e25bf6b828 | ||
| 
						 | 
					5a386daa9d | ||
| 
						 | 
					0d4a02fb1e | ||
| 
						 | 
					e135a74a9b | ||
| 
						 | 
					77f17c8dc2 | ||
| 
						 | 
					6a94f990cf | ||
| 
						 | 
					3aa5bd37aa | ||
| 
						 | 
					fa83fed1af | ||
| 
						 | 
					666cb7fa2f | ||
| 
						 | 
					a5678e15dc | ||
| 
						 | 
					9405cfcee9 | ||
| 
						 | 
					cb25d02cdf | ||
| 
						 | 
					bfe651a125 | ||
| 
						 | 
					dc5e52d9ce | ||
| 
						 | 
					229ada83e1 | ||
| 
						 | 
					13171d6744 | ||
| 
						 | 
					2ad941c94c | ||
| 
						 | 
					924d31e978 | ||
| 
						 | 
					f9a1b23212 | ||
| 
						 | 
					ae5795a7ea | ||
| 
						 | 
					ee36c38eda | ||
| 
						 | 
					eca7df3d9f | ||
| 
						 | 
					902b7e1b4a | ||
| 
						 | 
					db1a53a797 | ||
| 
						 | 
					3605d71b73 | ||
| 
						 | 
					ea2cea573e | ||
| 
						 | 
					d5fa8036e8 | ||
| 
						 | 
					b8707bbc9a | ||
| 
						 | 
					99eea16f62 | ||
| 
						 | 
					88737302f3 | ||
| 
						 | 
					213e3a5100 | ||
| 
						 | 
					8fa1402ecc | ||
| 
						 | 
					e63996bb61 | ||
| 
						 | 
					9ede879630 | ||
| 
						 | 
					88e9826a62 | ||
| 
						 | 
					5d655c7e6d | ||
| 
						 | 
					51c9d8a7ae | ||
| 
						 | 
					90df265114 | ||
| 
						 | 
					e65429aa9c | ||
| 
						 | 
					8d2232f0da | ||
| 
						 | 
					a58b9866ea | ||
| 
						 | 
					b1f25be5d7 | ||
| 
						 | 
					f0f9787326 | ||
| 
						 | 
					302f5835e6 | ||
| 
						 | 
					58c778632e | ||
| 
						 | 
					5671fd462b | ||
| 
						 | 
					5ac9733f15 | ||
| 
						 | 
					01dc0334fd | ||
| 
						 | 
					4a17aac15c | ||
| 
						 | 
					8ce4fc9c6d | ||
| 
						 | 
					b4f07cfc1c | ||
| 
						 | 
					8799089b2d | ||
| 
						 | 
					88c4830209 | ||
| 
						 | 
					4dd3df5cc2 | ||
| 
						 | 
					369a325a46 | ||
| 
						 | 
					1a1a0fdefb | ||
| 
						 | 
					dda8bd3e10 | ||
| 
						 | 
					edbfc92cb9 | ||
| 
						 | 
					6de11f09db | ||
| 
						 | 
					284bfcd9ad | ||
| 
						 | 
					3d627b5bf0 | ||
| 
						 | 
					c3c0dbbbae | ||
| 
						 | 
					64dd81e941 | ||
| 
						 | 
					799298ca34 | ||
| 
						 | 
					fa542bb24f | ||
| 
						 | 
					e025d945cf | ||
| 
						 | 
					bb1c61d0b9 | ||
| 
						 | 
					1519474f93 | ||
| 
						 | 
					9d8622f418 | ||
| 
						 | 
					a66db37b33 | ||
| 
						 | 
					c8c1a171d4 | ||
| 
						 | 
					88cfb829e3 | ||
| 
						 | 
					16be7a328e | ||
| 
						 | 
					04babf7930 | ||
| 
						 | 
					96bc09e8b5 | ||
| 
						 | 
					976fb91ecc | ||
| 
						 | 
					1305b6e64e | ||
| 
						 | 
					cdfe44d958 | ||
| 
						 | 
					c824a36832 | ||
| 
						 | 
					c4bd2c1d18 | ||
| 
						 | 
					561555ab7e | ||
| 
						 | 
					115fbd44cb | ||
| 
						 | 
					aa931328b0 | ||
| 4b42966284 | |||
| 523ab7f03a | |||
| 6e831c8253 | |||
| 
						 | 
					4416e5d175 | ||
| 
						 | 
					734a39a001 | ||
| 
						 | 
					98191d29ee | ||
| 
						 | 
					1c4c4a8b31 | ||
| 
						 | 
					d496c81003 | ||
| 
						 | 
					094d210f64 | ||
| 
						 | 
					314c72e132 | ||
| 
						 | 
					4e0163f2cb | ||
| 
						 | 
					e5b8c418af | ||
| 
						 | 
					3ef8584189 | ||
| 
						 | 
					df2ad09c86 | ||
| 
						 | 
					d70fb24eb1 | ||
| 
						 | 
					3150c7267d | ||
| 
						 | 
					6e65e4ff3d | ||
| 
						 | 
					67a4db2e9a | ||
| 
						 | 
					e9bcb1973f | ||
| 
						 | 
					9b87fd4258 | ||
| 
						 | 
					a49a849917 | ||
| 
						 | 
					aa74a7f9a3 | ||
| 
						 | 
					08e4c6cb57 | ||
| 
						 | 
					6e087bd2dd | ||
| e9792e6322 | |||
| 130504b964 | |||
| 2a8117d0c1 | |||
| 94bfd39085 | |||
| 40cd5f8a36 | |||
| 133b00a2ce | |||
| 57336f5c81 | |||
| b62d24c024 | |||
| 8f8235a86e | |||
| c8f646a8fa | |||
| ecaa382a1e | |||
| 8991198fd3 | |||
| 
						 | 
					f20b95a482 | ||
| 
						 | 
					8dd7dc6409 | ||
| 
						 | 
					c799d10727 | ||
| 
						 | 
					ceb6fb7b12 | ||
| 
						 | 
					6708abdb0f | ||
| 
						 | 
					a38f6024c1 | ||
| 
						 | 
					7d8748e3ef | ||
| 
						 | 
					bb3386c4e8 | ||
| 
						 | 
					25b84880a5 | ||
| 
						 | 
					7b6e967a5d | ||
| 
						 | 
					2781f2923e | ||
| 
						 | 
					03f08f0a18 | ||
| 
						 | 
					79c86d43f2 | ||
| 
						 | 
					e19af54caf | ||
| 
						 | 
					f4213c6a83 | ||
| 
						 | 
					f56db14720 | 
							
								
								
									
										31
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -1,7 +1,30 @@
 | 
				
			|||||||
/target
 | 
					target
 | 
				
			||||||
.env
 | 
					.env
 | 
				
			||||||
/venv
 | 
					/venv
 | 
				
			||||||
.cargo
 | 
					.cargo
 | 
				
			||||||
assets
 | 
					.idea
 | 
				
			||||||
out.json
 | 
					web/static/index.html
 | 
				
			||||||
/.idea
 | 
					web/static/assets
 | 
				
			||||||
 | 
					# Logs
 | 
				
			||||||
 | 
					logs
 | 
				
			||||||
 | 
					*.log
 | 
				
			||||||
 | 
					npm-debug.log*
 | 
				
			||||||
 | 
					yarn-debug.log*
 | 
				
			||||||
 | 
					yarn-error.log*
 | 
				
			||||||
 | 
					pnpm-debug.log*
 | 
				
			||||||
 | 
					lerna-debug.log*
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					node_modules
 | 
				
			||||||
 | 
					dist
 | 
				
			||||||
 | 
					dist-ssr
 | 
				
			||||||
 | 
					*.local
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Editor directories and files
 | 
				
			||||||
 | 
					.vscode/*
 | 
				
			||||||
 | 
					!.vscode/extensions.json
 | 
				
			||||||
 | 
					.DS_Store
 | 
				
			||||||
 | 
					*.suo
 | 
				
			||||||
 | 
					*.ntvs*
 | 
				
			||||||
 | 
					*.njsproj
 | 
				
			||||||
 | 
					*.sln
 | 
				
			||||||
 | 
					*.sw?
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										3355
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
							
								
								
									
										73
									
								
								Cargo.toml
									
									
									
									
									
								
							
							
						
						@@ -1,32 +1,71 @@
 | 
				
			|||||||
[package]
 | 
					[package]
 | 
				
			||||||
name = "reminder_rs"
 | 
					name = "reminder-rs"
 | 
				
			||||||
version = "1.6.0"
 | 
					version = "1.7.27"
 | 
				
			||||||
authors = ["jellywx <judesouthworth@pm.me>"]
 | 
					authors = ["Jude Southworth <judesouthworth@pm.me>"]
 | 
				
			||||||
edition = "2018"
 | 
					edition = "2021"
 | 
				
			||||||
workspaces = [".", "postman", "web", "entity", "migration"]
 | 
					license = "AGPL-3.0 only"
 | 
				
			||||||
 | 
					description = "Reminder Bot for Discord, now in Rust"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[dependencies]
 | 
					[dependencies]
 | 
				
			||||||
poise = "0.2"
 | 
					poise = "0.6.1"
 | 
				
			||||||
dotenv = "0.15"
 | 
					dotenv = "0.15"
 | 
				
			||||||
tokio = { version = "1", features = ["process", "full"] }
 | 
					tokio = { version = "1", features = ["process", "full"] }
 | 
				
			||||||
reqwest = "0.11"
 | 
					reqwest = { version = "0.12", features = ["json"] }
 | 
				
			||||||
regex = "1.4"
 | 
					regex = "1.10"
 | 
				
			||||||
log = "0.4"
 | 
					log = "0.4"
 | 
				
			||||||
env_logger = "0.8"
 | 
					env_logger = "0.11"
 | 
				
			||||||
chrono = "0.4"
 | 
					chrono = "0.4"
 | 
				
			||||||
chrono-tz = { version = "0.5", features = ["serde"] }
 | 
					chrono-tz = { version = "0.9", features = ["serde"] }
 | 
				
			||||||
lazy_static = "1.4"
 | 
					lazy_static = "1.4"
 | 
				
			||||||
num-integer = "0.1"
 | 
					num-integer = "0.1"
 | 
				
			||||||
serde = "1.0"
 | 
					serde = "1.0"
 | 
				
			||||||
serde_json = "1.0"
 | 
					serde_json = "1.0"
 | 
				
			||||||
serde_repr = "0.1"
 | 
					serde_repr = "0.1"
 | 
				
			||||||
rmp-serde = "0.15"
 | 
					rmp-serde = "1.1"
 | 
				
			||||||
rand = "0.7"
 | 
					rand = "0.8"
 | 
				
			||||||
levenshtein = "1.0"
 | 
					levenshtein = "1.0"
 | 
				
			||||||
base64 = "0.13.0"
 | 
					sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "macros", "mysql", "bigdecimal", "chrono", "migrate"] }
 | 
				
			||||||
 | 
					base64 = "0.22"
 | 
				
			||||||
 | 
					secrecy = "0.8.0"
 | 
				
			||||||
 | 
					futures = "0.3.30"
 | 
				
			||||||
 | 
					prometheus = "0.13.3"
 | 
				
			||||||
 | 
					rocket = { version = "0.5.0", features = ["tls", "secrets", "json"] }
 | 
				
			||||||
 | 
					rocket_dyn_templates = { version = "0.1.0", features = ["tera"] }
 | 
				
			||||||
 | 
					serenity = { version = "0.12", default-features = false, features = ["builder", "cache", "client", "gateway", "http", "model", "utils", "rustls_backend"] }
 | 
				
			||||||
 | 
					oauth2 = "4"
 | 
				
			||||||
 | 
					csv = "1.2"
 | 
				
			||||||
 | 
					sd-notify = "0.4.1"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[dependencies.postman]
 | 
					[dependencies.extract_derive]
 | 
				
			||||||
path = "postman"
 | 
					path = "extract_derive"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[dependencies.reminder_web]
 | 
					[dependencies.recordable_derive]
 | 
				
			||||||
path = "web"
 | 
					path = "recordable_derive"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[package.metadata.deb]
 | 
				
			||||||
 | 
					depends = "$auto, python3-dateparser (>= 1.0.0)"
 | 
				
			||||||
 | 
					suggests = "mysql-server-8.0, nginx"
 | 
				
			||||||
 | 
					maintainer-scripts = "debian"
 | 
				
			||||||
 | 
					assets = [
 | 
				
			||||||
 | 
					    ["target/release/reminder-rs", "usr/bin/reminder-rs", "755"],
 | 
				
			||||||
 | 
					    ["static/css/*", "lib/reminder-rs/static/css", "644"],
 | 
				
			||||||
 | 
					    ["static/favicon/*", "lib/reminder-rs/static/favicon", "644"],
 | 
				
			||||||
 | 
					    ["static/img/*", "lib/reminder-rs/static/img", "644"],
 | 
				
			||||||
 | 
					    ["static/js/*", "lib/reminder-rs/static/js", "644"],
 | 
				
			||||||
 | 
					    ["static/webfonts/*", "lib/reminder-rs/static/webfonts", "644"],
 | 
				
			||||||
 | 
					    ["static/site.webmanifest", "lib/reminder-rs/static/site.webmanifest", "644"],
 | 
				
			||||||
 | 
					    ["templates/**/*", "lib/reminder-rs/templates", "644"],
 | 
				
			||||||
 | 
					    ["reminder-dashboard/dist/static/assets/*", "lib/reminder-rs/static/assets", "644"],
 | 
				
			||||||
 | 
					    ["reminder-dashboard/dist/index.html", "lib/reminder-rs/static/index.html", "644"],
 | 
				
			||||||
 | 
					    ["conf/default.env", "etc/reminder-rs/config.env", "600"],
 | 
				
			||||||
 | 
					    ["conf/Rocket.toml", "etc/reminder-rs/Rocket.toml", "600"],
 | 
				
			||||||
 | 
					    # ["nginx/reminder-rs", "etc/nginx/sites-available/reminder-rs", "755"]
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					conf-files = [
 | 
				
			||||||
 | 
					    "/etc/reminder-rs/config.env",
 | 
				
			||||||
 | 
					    "/etc/reminder-rs/Rocket.toml",
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[package.metadata.deb.systemd-units]
 | 
				
			||||||
 | 
					unit-scripts = "systemd"
 | 
				
			||||||
 | 
					start = false
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										9
									
								
								Containerfile
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,9 @@
 | 
				
			|||||||
 | 
					FROM ubuntu:20.04
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ENV RUSTUP_HOME=/usr/local/rustup \
 | 
				
			||||||
 | 
					    CARGO_HOME=/usr/local/cargo \
 | 
				
			||||||
 | 
					    PATH=/usr/local/cargo/bin:$PATH
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					RUN apt update && DEBIAN_FRONTEND=noninteractive TZ=Etc/UTC apt install -y gcc gcc-multilib cmake pkg-config libssl-dev curl mysql-client-8.0 npm
 | 
				
			||||||
 | 
					RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --no-modify-path --profile minimal --default-toolchain nightly
 | 
				
			||||||
 | 
					RUN cargo install cargo-deb
 | 
				
			||||||
							
								
								
									
										44
									
								
								README.md
									
									
									
									
									
								
							
							
						
						@@ -7,25 +7,36 @@ reminders are paid on the hosted version of the bot. Keep reading if you want to
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
You'll need rustc and cargo for compilation. To run, you'll need Python 3 still (due to no suitable replacement for dateparser in Rust)
 | 
					You'll need rustc and cargo for compilation. To run, you'll need Python 3 still (due to no suitable replacement for dateparser in Rust)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### Compiling
 | 
					### Build APT package
 | 
				
			||||||
Install build requirements: 
 | 
					 | 
				
			||||||
`sudo apt install gcc gcc-multilib cmake libssl-dev build-essential`
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
Install Rust from https://rustup.rs
 | 
					Recommended method.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Reminder Bot can then be built by running `cargo build --release` in the top level directory. It is necessary to create a 
 | 
					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.
 | 
				
			||||||
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
 | 
					1. Install container software: `sudo apt install podman`.
 | 
				
			||||||
These environment variables must be provided when compiling the bot
 | 
					2. Install database server: `sudo apt install mysql-server-8.0`. Create a database called `reminders`
 | 
				
			||||||
 | 
					3. Install SQLx CLI: `cargo install sqlx-cli`
 | 
				
			||||||
 | 
					4. From the source code directory, execute `sqlx migrate run`
 | 
				
			||||||
 | 
					5. Build container image: `podman build -t reminder-rs .`
 | 
				
			||||||
 | 
					6. Build with podman: `podman run --rm --network=host -v "$PWD":/mnt -w /mnt -e "DATABASE_URL=mysql://user@localhost/reminders" reminder-rs cargo deb` 
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Compiling for other target
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					1. Install requirements: 
 | 
				
			||||||
 | 
					`sudo apt install gcc gcc-multilib cmake libssl-dev build-essential python3-dateparser`
 | 
				
			||||||
 | 
					2. Install rustup from https://rustup.rs
 | 
				
			||||||
 | 
					3. Install the nightly toolchain: `rustup toolchain default nightly`
 | 
				
			||||||
 | 
					4. Install database server: `sudo apt install mysql-server-8.0`. Create a database called `reminders`.
 | 
				
			||||||
 | 
					5. Install `sqlx-cli`: `cargo install sqlx-cli`.
 | 
				
			||||||
 | 
					6. Run migrations: `sqlx migrate run`.
 | 
				
			||||||
 | 
					7. Set environment variables:
 | 
				
			||||||
   * `DATABASE_URL` - the URL of your MySQL database (`mysql://user[:password]@domain/database`)
 | 
					   * `DATABASE_URL` - the URL of your MySQL database (`mysql://user[:password]@domain/database`)
 | 
				
			||||||
* `WEBHOOK_AVATAR` - accepts the name of an image file located in `$CARGO_MANIFEST_DIR/assets/` to be used as the avatar when creating webhooks. **IMPORTANT: image file must be 128x128 or smaller in size**
 | 
					8. Build: `cargo build --release`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### Setting up Python
 | 
					 | 
				
			||||||
Reminder Bot by default looks for a venv within it's working directory to run Python out of. To set up a venv, install `python3-venv` and run `python3 -m venv venv`. Then, run `source venv/bin/activate` to activate the venv, and do `pip install dateparser` to install the required library
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
### Environment Variables
 | 
					### Configuring
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Reminder Bot reads a number of environment variables. Some are essential, and others have hardcoded fallbacks. Environment variables can be loaded from a .env file in the working directory.
 | 
					Reminder Bot reads a number of environment variables. Some are essential, and others have hardcoded fallbacks. Environment variables can be loaded from a .env file in the working directory.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
__Required Variables__
 | 
					__Required Variables__
 | 
				
			||||||
@@ -37,10 +48,5 @@ __Other Variables__
 | 
				
			|||||||
* `LOCAL_TIMEZONE` - default `UTC`, necessary for calculations in the natural language processor
 | 
					* `LOCAL_TIMEZONE` - default `UTC`, necessary for calculations in the natural language processor
 | 
				
			||||||
* `SUBSCRIPTION_ROLES` - default `None`, accepts a list of Discord role IDs that are given to subscribed users
 | 
					* `SUBSCRIPTION_ROLES` - default `None`, accepts a list of Discord role IDs that are given to subscribed users
 | 
				
			||||||
* `CNC_GUILD` - default `None`, accepts a single Discord guild ID for the server that the subscription roles belong to
 | 
					* `CNC_GUILD` - default `None`, accepts a single Discord guild ID for the server that the subscription roles belong to
 | 
				
			||||||
* `PYTHON_LOCATION` - default `venv/bin/python3`. Can be changed if your Python executable is located somewhere else
 | 
					* `PYTHON_LOCATION` - default `/usr/bin/python3`. Can be changed if your Python executable is located somewhere else
 | 
				
			||||||
* `THEME_COLOR` - default `8fb677`. Specifies the hex value of the color to use on info message embeds 
 | 
					* `THEME_COLOR` - default `8fb677`. Specifies the hex value of the color to use on info message embeds 
 | 
				
			||||||
* `DM_ENABLED` - default `1`, if `1`, Reminder Bot will respond to direct messages
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### Todo List
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
* Convert aliases to macros
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										32
									
								
								Rocket.toml
									
									
									
									
									
								
							
							
						
						@@ -1,28 +1,28 @@
 | 
				
			|||||||
[default]
 | 
					[default]
 | 
				
			||||||
address = "0.0.0.0"
 | 
					address = "0.0.0.0"
 | 
				
			||||||
port = 5000
 | 
					port = 18920
 | 
				
			||||||
template_dir = "web/templates"
 | 
					template_dir = "templates"
 | 
				
			||||||
limits = { json = "10MiB" }
 | 
					limits = { json = "10MiB" }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[debug]
 | 
					[debug]
 | 
				
			||||||
secret_key = "tR8krio5FXTnnyIZNiJDXPondz0kI1v6X6BXZcBGIRY="
 | 
					secret_key = "tR8krio5FXTnnyIZNiJDXPondz0kI1v6X6BXZcBGIRY="
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[debug.tls]
 | 
					[debug.tls]
 | 
				
			||||||
certs = "web/private/rsa_sha256_cert.pem"
 | 
					certs = "private/rsa_sha256_cert.pem"
 | 
				
			||||||
key = "web/private/rsa_sha256_key.pem"
 | 
					key = "private/rsa_sha256_key.pem"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[rsa_sha256.tls]
 | 
					[debug.rsa_sha256.tls]
 | 
				
			||||||
certs = "web/private/rsa_sha256_cert.pem"
 | 
					certs = "private/rsa_sha256_cert.pem"
 | 
				
			||||||
key = "web/private/rsa_sha256_key.pem"
 | 
					key = "private/rsa_sha256_key.pem"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[ecdsa_nistp256_sha256.tls]
 | 
					[debug.ecdsa_nistp256_sha256.tls]
 | 
				
			||||||
certs = "web/private/ecdsa_nistp256_sha256_cert.pem"
 | 
					certs = "private/ecdsa_nistp256_sha256_cert.pem"
 | 
				
			||||||
key = "web/private/ecdsa_nistp256_sha256_key_pkcs8.pem"
 | 
					key = "private/ecdsa_nistp256_sha256_key_pkcs8.pem"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[ecdsa_nistp384_sha384.tls]
 | 
					[debug.ecdsa_nistp384_sha384.tls]
 | 
				
			||||||
certs = "web/private/ecdsa_nistp384_sha384_cert.pem"
 | 
					certs = "private/ecdsa_nistp384_sha384_cert.pem"
 | 
				
			||||||
key = "web/private/ecdsa_nistp384_sha384_key_pkcs8.pem"
 | 
					key = "private/ecdsa_nistp384_sha384_key_pkcs8.pem"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[ed25519.tls]
 | 
					[debug.ed25519.tls]
 | 
				
			||||||
certs = "web/private/ed25519_cert.pem"
 | 
					certs = "private/ed25519_cert.pem"
 | 
				
			||||||
key = "eb/private/ed25519_key.pem"
 | 
					key = "private/ed25519_key.pem"
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										
											BIN
										
									
								
								assets/webhook.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 21 KiB  | 
							
								
								
									
										13
									
								
								build.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,13 @@
 | 
				
			|||||||
 | 
					use std::{path::Path, process::Command};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fn main() {
 | 
				
			||||||
 | 
					    println!("cargo:rerun-if-changed=migrations");
 | 
				
			||||||
 | 
					    println!("cargo:rerun-if-changed=reminder-dashboard");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Command::new("npm")
 | 
				
			||||||
 | 
					        .arg("run")
 | 
				
			||||||
 | 
					        .arg("build")
 | 
				
			||||||
 | 
					        .current_dir(Path::new("reminder-dashboard"))
 | 
				
			||||||
 | 
					        .spawn()
 | 
				
			||||||
 | 
					        .expect("Failed to build NPM");
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										8
									
								
								conf/Rocket.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,8 @@
 | 
				
			|||||||
 | 
					[default]
 | 
				
			||||||
 | 
					address = "127.0.0.1"
 | 
				
			||||||
 | 
					port = 18920
 | 
				
			||||||
 | 
					template_dir = "/lib/reminder-rs/templates"
 | 
				
			||||||
 | 
					limits = { json = "10MiB" }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[release]
 | 
				
			||||||
 | 
					# secret_key = ""
 | 
				
			||||||
							
								
								
									
										19
									
								
								conf/default.env
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,19 @@
 | 
				
			|||||||
 | 
					DATABASE_URL=
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					DISCORD_TOKEN=
 | 
				
			||||||
 | 
					PATREON_GUILD_ID=
 | 
				
			||||||
 | 
					PATREON_ROLE_ID=
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					LOCAL_TIMEZONE=
 | 
				
			||||||
 | 
					MIN_INTERVAL=
 | 
				
			||||||
 | 
					PYTHON_LOCATION=/usr/bin/python3
 | 
				
			||||||
 | 
					DONTRUN=
 | 
				
			||||||
 | 
					SECRET_KEY=
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					REMIND_INTERVAL=
 | 
				
			||||||
 | 
					OAUTH2_DISCORD_CALLBACK=
 | 
				
			||||||
 | 
					OAUTH2_CLIENT_ID=
 | 
				
			||||||
 | 
					OAUTH2_CLIENT_SECRET=
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					REPORT_EMAIL=
 | 
				
			||||||
 | 
					LOG_TO_DATABASE=1
 | 
				
			||||||
							
								
								
									
										1
									
								
								cron.d/reminder_health
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					*/10 * * * * reminder /lib/reminder-rs/healthcheck
 | 
				
			||||||
							
								
								
									
										9
									
								
								debian/postinst
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,9 @@
 | 
				
			|||||||
 | 
					#!/bin/bash
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					set -e
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					id -u reminder &>/dev/null || useradd -r -M reminder
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					chown -R reminder /etc/reminder-rs
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#DEBHELPER#
 | 
				
			||||||
							
								
								
									
										7
									
								
								debian/postrm
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,7 @@
 | 
				
			|||||||
 | 
					#!/bin/bash
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					set -e
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					id -u reminder &>/dev/null || userdel reminder
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#DEBHELPER#
 | 
				
			||||||
							
								
								
									
										46
									
								
								extract_derive/Cargo.lock
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,46 @@
 | 
				
			|||||||
 | 
					# This file is automatically @generated by Cargo.
 | 
				
			||||||
 | 
					# It is not intended for manual editing.
 | 
				
			||||||
 | 
					version = 3
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "extract_macro"
 | 
				
			||||||
 | 
					version = "0.1.0"
 | 
				
			||||||
 | 
					dependencies = [
 | 
				
			||||||
 | 
					 "quote",
 | 
				
			||||||
 | 
					 "syn",
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "proc-macro2"
 | 
				
			||||||
 | 
					version = "1.0.78"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae"
 | 
				
			||||||
 | 
					dependencies = [
 | 
				
			||||||
 | 
					 "unicode-ident",
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "quote"
 | 
				
			||||||
 | 
					version = "1.0.35"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef"
 | 
				
			||||||
 | 
					dependencies = [
 | 
				
			||||||
 | 
					 "proc-macro2",
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "syn"
 | 
				
			||||||
 | 
					version = "2.0.49"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "915aea9e586f80826ee59f8453c1101f9d1c4b3964cd2460185ee8e299ada496"
 | 
				
			||||||
 | 
					dependencies = [
 | 
				
			||||||
 | 
					 "proc-macro2",
 | 
				
			||||||
 | 
					 "quote",
 | 
				
			||||||
 | 
					 "unicode-ident",
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "unicode-ident"
 | 
				
			||||||
 | 
					version = "1.0.12"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
 | 
				
			||||||
							
								
								
									
										11
									
								
								extract_derive/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,11 @@
 | 
				
			|||||||
 | 
					[package]
 | 
				
			||||||
 | 
					name = "extract_derive"
 | 
				
			||||||
 | 
					version = "0.1.0"
 | 
				
			||||||
 | 
					edition = "2021"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[lib]
 | 
				
			||||||
 | 
					proc-macro = true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[dependencies]
 | 
				
			||||||
 | 
					quote = "1.0.35"
 | 
				
			||||||
 | 
					syn = { version = "2.0.49", features = ["full"] }
 | 
				
			||||||
							
								
								
									
										53
									
								
								extract_derive/src/lib.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,53 @@
 | 
				
			|||||||
 | 
					use proc_macro::TokenStream;
 | 
				
			||||||
 | 
					use syn::{spanned::Spanned, Data, Fields};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[proc_macro_derive(Extract)]
 | 
				
			||||||
 | 
					pub fn extract(input: TokenStream) -> TokenStream {
 | 
				
			||||||
 | 
					    let ast = syn::parse_macro_input!(input);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    impl_extract(&ast)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fn impl_extract(ast: &syn::DeriveInput) -> TokenStream {
 | 
				
			||||||
 | 
					    let name = &ast.ident;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    match &ast.data {
 | 
				
			||||||
 | 
					        // Dispatch over struct: extract args directly from context
 | 
				
			||||||
 | 
					        Data::Struct(st) => match &st.fields {
 | 
				
			||||||
 | 
					            Fields::Named(fields) => {
 | 
				
			||||||
 | 
					                let extracted = fields.named.iter().map(|field| {
 | 
				
			||||||
 | 
					                    let ident = &field.ident;
 | 
				
			||||||
 | 
					                    let ty = &field.ty;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    quote::quote_spanned! {field.span()=>
 | 
				
			||||||
 | 
					                        #ident : crate::utils::extract_arg!(ctx, #ident, #ty)
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                TokenStream::from(quote::quote! {
 | 
				
			||||||
 | 
					                    impl Extract for #name {
 | 
				
			||||||
 | 
					                        fn extract(ctx: crate::ApplicationContext) -> Self {
 | 
				
			||||||
 | 
					                            Self {
 | 
				
			||||||
 | 
					                                #(#extracted,)*
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                })
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            Fields::Unit => TokenStream::from(quote::quote! {
 | 
				
			||||||
 | 
					                impl Extract for #name {
 | 
				
			||||||
 | 
					                    fn extract(ctx: crate::ApplicationContext) -> Self {
 | 
				
			||||||
 | 
					                        Self {}
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }),
 | 
				
			||||||
 | 
					            _ => {
 | 
				
			||||||
 | 
					                panic!("Only named/unit structs can derive Extract");
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        _ => {
 | 
				
			||||||
 | 
					            panic!("Only structs can derive Extract");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										229
									
								
								migrations/20210603000000_initial.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,229 @@
 | 
				
			|||||||
 | 
					SET FOREIGN_KEY_CHECKS=0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					CREATE TABLE guilds (
 | 
				
			||||||
 | 
					    id INT UNSIGNED UNIQUE NOT NULL AUTO_INCREMENT,
 | 
				
			||||||
 | 
					    guild BIGINT UNSIGNED UNIQUE NOT NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    name VARCHAR(100),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    prefix VARCHAR(5) DEFAULT '$' NOT NULL,
 | 
				
			||||||
 | 
					    timezone VARCHAR(32) DEFAULT 'UTC' NOT NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    default_channel_id INT UNSIGNED,
 | 
				
			||||||
 | 
					    default_username VARCHAR(32) DEFAULT 'Reminder' NOT NULL,
 | 
				
			||||||
 | 
					    default_avatar VARCHAR(512) DEFAULT 'https://raw.githubusercontent.com/reminder-bot/logos/master/Remind_Me_Bot_Logo_PPic.jpg' NOT NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    PRIMARY KEY (id),
 | 
				
			||||||
 | 
					    FOREIGN KEY (default_channel_id) REFERENCES channels(id) ON DELETE SET NULL
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					CREATE TABLE channels (
 | 
				
			||||||
 | 
					    id INT UNSIGNED UNIQUE NOT NULL AUTO_INCREMENT,
 | 
				
			||||||
 | 
					    channel BIGINT UNSIGNED UNIQUE NOT NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    name VARCHAR(100),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    nudge SMALLINT NOT NULL DEFAULT 0,
 | 
				
			||||||
 | 
					    blacklisted BOOL NOT NULL DEFAULT FALSE,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    webhook_id BIGINT UNSIGNED UNIQUE,
 | 
				
			||||||
 | 
					    webhook_token TEXT,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    paused BOOL NOT NULL DEFAULT 0,
 | 
				
			||||||
 | 
					    paused_until TIMESTAMP,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    guild_id INT UNSIGNED,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    PRIMARY KEY (id),
 | 
				
			||||||
 | 
					    FOREIGN KEY (guild_id) REFERENCES guilds(id) ON DELETE CASCADE
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					CREATE TABLE users (
 | 
				
			||||||
 | 
					    id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL,
 | 
				
			||||||
 | 
					    user BIGINT UNSIGNED UNIQUE NOT NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    name VARCHAR(37) NOT NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dm_channel INT UNSIGNED UNIQUE NOT NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    language VARCHAR(2) DEFAULT 'EN' NOT NULL,
 | 
				
			||||||
 | 
					    timezone VARCHAR(32) DEFAULT 'UTC' NOT NULL,
 | 
				
			||||||
 | 
					    meridian_time BOOLEAN DEFAULT 0 NOT NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    allowed_dm BOOLEAN DEFAULT 1 NOT NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    patreon BOOLEAN NOT NULL DEFAULT 0,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    PRIMARY KEY (id),
 | 
				
			||||||
 | 
					    FOREIGN KEY (dm_channel) REFERENCES channels(id) ON DELETE RESTRICT
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					CREATE TABLE roles (
 | 
				
			||||||
 | 
					    id INT UNSIGNED UNIQUE NOT NULL AUTO_INCREMENT,
 | 
				
			||||||
 | 
					    role BIGINT UNSIGNED UNIQUE NOT NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    name VARCHAR(100),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    guild_id INT UNSIGNED NOT NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    PRIMARY KEY (id),
 | 
				
			||||||
 | 
					    FOREIGN KEY (guild_id) REFERENCES guilds(id) ON DELETE CASCADE
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					CREATE TABLE embeds (
 | 
				
			||||||
 | 
					    id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    title VARCHAR(256) NOT NULL DEFAULT '',
 | 
				
			||||||
 | 
					    description VARCHAR(2048) NOT NULL DEFAULT '',
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    image_url VARCHAR(512),
 | 
				
			||||||
 | 
					    thumbnail_url VARCHAR(512),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    footer VARCHAR(2048) NOT NULL DEFAULT '',
 | 
				
			||||||
 | 
					    footer_icon VARCHAR(512),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    color MEDIUMINT UNSIGNED NOT NULL DEFAULT 0x0,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    PRIMARY KEY (id)
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					CREATE TABLE embed_fields (
 | 
				
			||||||
 | 
					    id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    title VARCHAR(256) NOT NULL DEFAULT '',
 | 
				
			||||||
 | 
					    value VARCHAR(1024) NOT NULL DEFAULT '',
 | 
				
			||||||
 | 
					    inline BOOL NOT NULL DEFAULT 0,
 | 
				
			||||||
 | 
					    embed_id INT UNSIGNED NOT NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    PRIMARY KEY (id),
 | 
				
			||||||
 | 
					    FOREIGN KEY (embed_id) REFERENCES embeds(id) ON DELETE CASCADE
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					CREATE TABLE messages (
 | 
				
			||||||
 | 
					    id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    content VARCHAR(2048) NOT NULL DEFAULT '',
 | 
				
			||||||
 | 
					    tts BOOL NOT NULL DEFAULT 0,
 | 
				
			||||||
 | 
					    embed_id INT UNSIGNED,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    attachment MEDIUMBLOB,
 | 
				
			||||||
 | 
					    attachment_name VARCHAR(260),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    PRIMARY KEY (id),
 | 
				
			||||||
 | 
					    FOREIGN KEY (embed_id) REFERENCES embeds(id) ON DELETE SET NULL
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					CREATE TABLE reminders (
 | 
				
			||||||
 | 
					    id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL,
 | 
				
			||||||
 | 
					    uid VARCHAR(64) UNIQUE NOT NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    name VARCHAR(24) NOT NULL DEFAULT 'Reminder',
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    message_id INT UNSIGNED NOT NULL,
 | 
				
			||||||
 | 
					    channel_id INT UNSIGNED NOT NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    `time` INT UNSIGNED DEFAULT 0 NOT NULL,
 | 
				
			||||||
 | 
					    `interval` INT UNSIGNED DEFAULT NULL,
 | 
				
			||||||
 | 
					    expires TIMESTAMP DEFAULT NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    enabled BOOLEAN DEFAULT 1 NOT NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    avatar VARCHAR(512),
 | 
				
			||||||
 | 
					    username VARCHAR(32),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    method ENUM('remind', 'natural', 'dashboard', 'todo', 'countdown'),
 | 
				
			||||||
 | 
					    set_at TIMESTAMP DEFAULT NOW(),
 | 
				
			||||||
 | 
					    set_by INT UNSIGNED,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    PRIMARY KEY (id),
 | 
				
			||||||
 | 
					    FOREIGN KEY (message_id) REFERENCES messages(id) ON DELETE RESTRICT,
 | 
				
			||||||
 | 
					    FOREIGN KEY (channel_id) REFERENCES channels(id) ON DELETE CASCADE,
 | 
				
			||||||
 | 
					    FOREIGN KEY (set_by) REFERENCES users(id) ON DELETE SET NULL
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					CREATE TRIGGER message_cleanup AFTER DELETE ON reminders
 | 
				
			||||||
 | 
					FOR EACH ROW
 | 
				
			||||||
 | 
					    DELETE FROM messages WHERE id = OLD.message_id;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					CREATE TRIGGER embed_cleanup AFTER DELETE ON messages
 | 
				
			||||||
 | 
					FOR EACH ROW
 | 
				
			||||||
 | 
					    DELETE FROM embeds WHERE id = OLD.embed_id;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					CREATE TABLE todos (
 | 
				
			||||||
 | 
					    id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL,
 | 
				
			||||||
 | 
					    user_id INT UNSIGNED,
 | 
				
			||||||
 | 
					    guild_id INT UNSIGNED,
 | 
				
			||||||
 | 
					    channel_id INT UNSIGNED,
 | 
				
			||||||
 | 
					    value VARCHAR(2000) NOT NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    PRIMARY KEY (id),
 | 
				
			||||||
 | 
					    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL,
 | 
				
			||||||
 | 
					    FOREIGN KEY (guild_id) REFERENCES guilds(id) ON DELETE CASCADE,
 | 
				
			||||||
 | 
					    FOREIGN KEY (channel_id) REFERENCES channels(id) ON DELETE SET NULL
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					CREATE TABLE command_restrictions (
 | 
				
			||||||
 | 
					    id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    role_id INT UNSIGNED NOT NULL,
 | 
				
			||||||
 | 
					    command ENUM('todos', 'natural', 'remind', 'interval', 'timer', 'del', 'look', 'alias', 'countdown') NOT NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    PRIMARY KEY (id),
 | 
				
			||||||
 | 
					    FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE,
 | 
				
			||||||
 | 
					    UNIQUE KEY (`role_id`, `command`)
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					CREATE TABLE timers (
 | 
				
			||||||
 | 
					    id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL,
 | 
				
			||||||
 | 
					    start_time TIMESTAMP NOT NULL DEFAULT NOW(),
 | 
				
			||||||
 | 
					    name VARCHAR(32) NOT NULL,
 | 
				
			||||||
 | 
					    owner BIGINT UNSIGNED NOT NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    PRIMARY KEY (id)
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					CREATE TABLE events (
 | 
				
			||||||
 | 
					    id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL,
 | 
				
			||||||
 | 
					    `time` TIMESTAMP NOT NULL DEFAULT NOW(),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    event_name ENUM('edit', 'enable', 'disable', 'delete') NOT NULL,
 | 
				
			||||||
 | 
					    bulk_count INT UNSIGNED,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    guild_id INT UNSIGNED NOT NULL,
 | 
				
			||||||
 | 
					    user_id INT UNSIGNED,
 | 
				
			||||||
 | 
					    reminder_id INT UNSIGNED,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    PRIMARY KEY (id),
 | 
				
			||||||
 | 
					    FOREIGN KEY (guild_id) REFERENCES guilds(id) ON DELETE CASCADE,
 | 
				
			||||||
 | 
					    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL,
 | 
				
			||||||
 | 
					    FOREIGN KEY (reminder_id) REFERENCES reminders(id) ON DELETE SET NULL
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					CREATE TABLE command_aliases (
 | 
				
			||||||
 | 
					    id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    guild_id INT UNSIGNED NOT NULL,
 | 
				
			||||||
 | 
					    name VARCHAR(12) NOT NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    command VARCHAR(2048) NOT NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    PRIMARY KEY (id),
 | 
				
			||||||
 | 
					    FOREIGN KEY (guild_id) REFERENCES guilds(id) ON DELETE CASCADE,
 | 
				
			||||||
 | 
					    UNIQUE KEY (`guild_id`, `name`)
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					CREATE TABLE guild_users (
 | 
				
			||||||
 | 
					    guild INT UNSIGNED NOT NULL,
 | 
				
			||||||
 | 
					    user INT UNSIGNED NOT NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    can_access BOOL NOT NULL DEFAULT 0,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    FOREIGN KEY (guild) REFERENCES guilds(id) ON DELETE CASCADE,
 | 
				
			||||||
 | 
					    FOREIGN KEY (user) REFERENCES users(id) ON DELETE CASCADE,
 | 
				
			||||||
 | 
					    UNIQUE KEY (guild, user)
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					CREATE EVENT event_cleanup
 | 
				
			||||||
 | 
					ON SCHEDULE AT CURRENT_TIMESTAMP + INTERVAL 1 DAY
 | 
				
			||||||
 | 
					ON COMPLETION PRESERVE
 | 
				
			||||||
 | 
					DO DELETE FROM events WHERE `time` < DATE_SUB(NOW(), INTERVAL 5 DAY);
 | 
				
			||||||
							
								
								
									
										163
									
								
								migrations/20210623000000_reminder_message_embed.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										11
									
								
								migrations/20210922000000_macro.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,11 @@
 | 
				
			|||||||
 | 
					CREATE TABLE macro (
 | 
				
			||||||
 | 
					    id INT UNSIGNED AUTO_INCREMENT,
 | 
				
			||||||
 | 
					    guild_id INT UNSIGNED NOT NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    name VARCHAR(100) NOT NULL,
 | 
				
			||||||
 | 
					    description VARCHAR(100),
 | 
				
			||||||
 | 
					    commands TEXT NOT NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    FOREIGN KEY (guild_id) REFERENCES guilds(id) ON DELETE CASCADE,
 | 
				
			||||||
 | 
					    PRIMARY KEY (id)
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
@@ -0,0 +1,2 @@
 | 
				
			|||||||
 | 
					ALTER TABLE reminders RENAME COLUMN `interval` TO `interval_seconds`;
 | 
				
			||||||
 | 
					ALTER TABLE reminders ADD COLUMN `interval_months` INT UNSIGNED DEFAULT NULL;
 | 
				
			||||||
							
								
								
									
										49
									
								
								migrations/20220211000000_reminder_templates.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,49 @@
 | 
				
			|||||||
 | 
					CREATE TABLE reminder_template (
 | 
				
			||||||
 | 
					    `id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    `name` VARCHAR(24) NOT NULL DEFAULT 'Reminder',
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    `guild_id` INT UNSIGNED NOT NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    `username` VARCHAR(32) DEFAULT NULL,
 | 
				
			||||||
 | 
					    `avatar` VARCHAR(512) DEFAULT NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    `content` VARCHAR(2048) NOT NULL DEFAULT '',
 | 
				
			||||||
 | 
					    `tts` BOOL NOT NULL DEFAULT 0,
 | 
				
			||||||
 | 
					    `attachment` MEDIUMBLOB,
 | 
				
			||||||
 | 
					    `attachment_name` VARCHAR(260),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    `embed_title` VARCHAR(256) NOT NULL DEFAULT '',
 | 
				
			||||||
 | 
					    `embed_description` VARCHAR(2048) NOT NULL DEFAULT '',
 | 
				
			||||||
 | 
					    `embed_image_url` VARCHAR(512),
 | 
				
			||||||
 | 
					    `embed_thumbnail_url` VARCHAR(512),
 | 
				
			||||||
 | 
					    `embed_footer` VARCHAR(2048) NOT NULL DEFAULT '',
 | 
				
			||||||
 | 
					    `embed_footer_url` VARCHAR(512),
 | 
				
			||||||
 | 
					    `embed_author` VARCHAR(256) NOT NULL DEFAULT '',
 | 
				
			||||||
 | 
					    `embed_author_url` VARCHAR(512),
 | 
				
			||||||
 | 
					    `embed_color` INT UNSIGNED NOT NULL DEFAULT 0x0,
 | 
				
			||||||
 | 
					    `embed_fields` JSON,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    PRIMARY KEY (id),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    FOREIGN KEY (`guild_id`) REFERENCES guilds (`id`) ON DELETE CASCADE
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ALTER TABLE reminders ADD COLUMN embed_fields JSON;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					update reminders
 | 
				
			||||||
 | 
					    inner join embed_fields as E
 | 
				
			||||||
 | 
					    on E.reminder_id = reminders.id
 | 
				
			||||||
 | 
					set embed_fields = (
 | 
				
			||||||
 | 
					    select JSON_ARRAYAGG(
 | 
				
			||||||
 | 
					        JSON_OBJECT(
 | 
				
			||||||
 | 
					            'title', E.title,
 | 
				
			||||||
 | 
					            'value', E.value,
 | 
				
			||||||
 | 
					            'inline',
 | 
				
			||||||
 | 
					            if(inline = 1, cast(TRUE as json), cast(FALSE as json))
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					    from embed_fields
 | 
				
			||||||
 | 
					    group by reminder_id
 | 
				
			||||||
 | 
					    having reminder_id = reminders.id
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
							
								
								
									
										1
									
								
								migrations/20221210000000_reminder_daily_intervals.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					ALTER TABLE reminders ADD COLUMN `interval_days` INT UNSIGNED DEFAULT NULL;
 | 
				
			||||||
							
								
								
									
										1
									
								
								migrations/20230511125236_reminder_threads.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					ALTER TABLE reminders ADD COLUMN `thread_id` BIGINT DEFAULT NULL;
 | 
				
			||||||
							
								
								
									
										1
									
								
								migrations/20230511180231_ephemeral_confirmations.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					ALTER TABLE guilds ADD COLUMN ephemeral_confirmations BOOL NOT NULL DEFAULT 0;
 | 
				
			||||||
							
								
								
									
										2
									
								
								migrations/20230722130906_increase_reminder_name.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,2 @@
 | 
				
			|||||||
 | 
					ALTER TABLE reminders MODIFY `name` VARCHAR(100) NOT NULL DEFAULT 'Reminder';
 | 
				
			||||||
 | 
					ALTER TABLE reminder_template MODIFY `name` VARCHAR(100) NOT NULL DEFAULT 'Reminder';
 | 
				
			||||||
							
								
								
									
										9
									
								
								migrations/20230730134827_stats.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,9 @@
 | 
				
			|||||||
 | 
					CREATE TABLE stat (
 | 
				
			||||||
 | 
					    `id` BIGINT NOT NULL AUTO_INCREMENT,
 | 
				
			||||||
 | 
					    `utc_time` DATETIME NOT NULL DEFAULT NOW(),
 | 
				
			||||||
 | 
					    `type` ENUM('reminder_sent', 'reminder_failed'),
 | 
				
			||||||
 | 
					    `reminder_id` INT UNSIGNED,
 | 
				
			||||||
 | 
					    `message` TEXT,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    PRIMARY KEY (`id`)
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
							
								
								
									
										2
									
								
								migrations/20230731170452_reminder_archive.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,2 @@
 | 
				
			|||||||
 | 
					ALTER TABLE reminders ADD COLUMN `status` ENUM ('pending', 'sent', 'failed', 'deleted') NOT NULL DEFAULT 'pending';
 | 
				
			||||||
 | 
					ALTER TABLE reminders ADD COLUMN `status_message` TEXT;
 | 
				
			||||||
@@ -0,0 +1,3 @@
 | 
				
			|||||||
 | 
					ALTER TABLE `reminder_template` ADD COLUMN `interval_seconds` INT UNSIGNED;
 | 
				
			||||||
 | 
					ALTER TABLE `reminder_template` ADD COLUMN `interval_days` INT UNSIGNED;
 | 
				
			||||||
 | 
					ALTER TABLE `reminder_template` ADD COLUMN `interval_months` INT UNSIGNED;
 | 
				
			||||||
							
								
								
									
										50
									
								
								migrations/20240210133900_macro_restructure.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,50 @@
 | 
				
			|||||||
 | 
					CREATE TABLE command_macro (
 | 
				
			||||||
 | 
					    id INT UNSIGNED AUTO_INCREMENT,
 | 
				
			||||||
 | 
					    guild_id INT UNSIGNED NOT NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    name VARCHAR(100) NOT NULL,
 | 
				
			||||||
 | 
					    description VARCHAR(100),
 | 
				
			||||||
 | 
					    commands JSON NOT NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    FOREIGN KEY (guild_id) REFERENCES guilds(id) ON DELETE CASCADE,
 | 
				
			||||||
 | 
					    PRIMARY KEY (id)
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# New JSON structure is {command_name: "Remind", "<option name>": "<option value>", ...}
 | 
				
			||||||
 | 
					INSERT INTO command_macro (guild_id, description, name, commands)
 | 
				
			||||||
 | 
					SELECT
 | 
				
			||||||
 | 
					    guild_id,
 | 
				
			||||||
 | 
					    description,
 | 
				
			||||||
 | 
					    name,
 | 
				
			||||||
 | 
					    (
 | 
				
			||||||
 | 
					        SELECT JSON_ARRAYAGG(
 | 
				
			||||||
 | 
					            (
 | 
				
			||||||
 | 
					                SELECT JSON_OBJECTAGG(t2.name, t2.value)
 | 
				
			||||||
 | 
					                FROM JSON_TABLE(
 | 
				
			||||||
 | 
					                    JSON_ARRAY_APPEND(t1.options, '$', JSON_OBJECT('name', 'command_name', 'value', t1.command_name)),
 | 
				
			||||||
 | 
					                    '$[*]' COLUMNS (
 | 
				
			||||||
 | 
					                        name VARCHAR(64) PATH '$.name' ERROR ON ERROR,
 | 
				
			||||||
 | 
					                        value TEXT PATH '$.value' ERROR ON ERROR
 | 
				
			||||||
 | 
					                    )) AS t2
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        FROM macro m2
 | 
				
			||||||
 | 
					        JOIN JSON_TABLE(
 | 
				
			||||||
 | 
					            commands,
 | 
				
			||||||
 | 
					            '$[*]' COLUMNS (
 | 
				
			||||||
 | 
					                command_name VARCHAR(64) PATH '$.command_name' ERROR ON ERROR,
 | 
				
			||||||
 | 
					                options JSON PATH '$.options' ERROR ON ERROR
 | 
				
			||||||
 | 
					            )) AS t1
 | 
				
			||||||
 | 
					        WHERE m1.id = m2.id
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					FROM macro m1;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# # Check which commands are used in macros
 | 
				
			||||||
 | 
					# SELECT DISTINCT command_name
 | 
				
			||||||
 | 
					# FROM macro m2
 | 
				
			||||||
 | 
					# JOIN JSON_TABLE(
 | 
				
			||||||
 | 
					#     commands,
 | 
				
			||||||
 | 
					#     '$[*]' COLUMNS (
 | 
				
			||||||
 | 
					#         command_name VARCHAR(64) PATH '$.command_name' ERROR ON ERROR,
 | 
				
			||||||
 | 
					#         options JSON PATH '$.options' ERROR ON ERROR
 | 
				
			||||||
 | 
					#     )) AS t1
 | 
				
			||||||
							
								
								
									
										5
									
								
								migrations/20240303125837_add_indexes.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,5 @@
 | 
				
			|||||||
 | 
					-- Add migration script here
 | 
				
			||||||
 | 
					ALTER TABLE reminders
 | 
				
			||||||
 | 
					    ADD INDEX `utc_time_index` (`utc_time`);
 | 
				
			||||||
 | 
					ALTER TABLE reminders
 | 
				
			||||||
 | 
					    ADD INDEX `status_index` (`status`);
 | 
				
			||||||
							
								
								
									
										7
									
								
								models/Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						@@ -1,7 +0,0 @@
 | 
				
			|||||||
# This file is automatically @generated by Cargo.
 | 
					 | 
				
			||||||
# It is not intended for manual editing.
 | 
					 | 
				
			||||||
version = 3
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
[[package]]
 | 
					 | 
				
			||||||
name = "models"
 | 
					 | 
				
			||||||
version = "0.1.0"
 | 
					 | 
				
			||||||
@@ -1,8 +0,0 @@
 | 
				
			|||||||
[package]
 | 
					 | 
				
			||||||
name = "models"
 | 
					 | 
				
			||||||
version = "0.1.0"
 | 
					 | 
				
			||||||
edition = "2021"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
[dependencies]
 | 
					 | 
				
			||||||
@@ -1,8 +0,0 @@
 | 
				
			|||||||
[package]
 | 
					 | 
				
			||||||
name = "entity"
 | 
					 | 
				
			||||||
version = "0.1.0"
 | 
					 | 
				
			||||||
edition = "2021"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
[dependencies]
 | 
					 | 
				
			||||||
chrono-tz = "^0.6"
 | 
					 | 
				
			||||||
sea-orm = { version = "^0.8", features = ["sqlx-postgres", "runtime-tokio-native-tls", "macros"] }
 | 
					 | 
				
			||||||
@@ -1,60 +0,0 @@
 | 
				
			|||||||
//! SeaORM Entity. Generated by sea-orm-codegen 0.8.0
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use sea_orm::entity::prelude::*;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
 | 
					 | 
				
			||||||
#[sea_orm(table_name = "channel")]
 | 
					 | 
				
			||||||
pub struct Model {
 | 
					 | 
				
			||||||
    #[sea_orm(primary_key, auto_increment = false)]
 | 
					 | 
				
			||||||
    pub id: i64,
 | 
					 | 
				
			||||||
    pub guild_id: Option<i64>,
 | 
					 | 
				
			||||||
    pub nudge: i32,
 | 
					 | 
				
			||||||
    pub webhook_id: Option<i64>,
 | 
					 | 
				
			||||||
    pub webhook_token: Option<String>,
 | 
					 | 
				
			||||||
    pub paused: bool,
 | 
					 | 
				
			||||||
    pub paused_until: Option<DateTimeUtc>,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
 | 
					 | 
				
			||||||
pub enum Relation {
 | 
					 | 
				
			||||||
    #[sea_orm(
 | 
					 | 
				
			||||||
        belongs_to = "super::guild::Entity",
 | 
					 | 
				
			||||||
        from = "Column::GuildId",
 | 
					 | 
				
			||||||
        to = "super::guild::Column::Id",
 | 
					 | 
				
			||||||
        on_update = "NoAction",
 | 
					 | 
				
			||||||
        on_delete = "Cascade"
 | 
					 | 
				
			||||||
    )]
 | 
					 | 
				
			||||||
    Guild,
 | 
					 | 
				
			||||||
    #[sea_orm(has_many = "super::user::Entity")]
 | 
					 | 
				
			||||||
    User,
 | 
					 | 
				
			||||||
    #[sea_orm(has_many = "super::reminder::Entity")]
 | 
					 | 
				
			||||||
    Reminder,
 | 
					 | 
				
			||||||
    #[sea_orm(has_many = "super::todo::Entity")]
 | 
					 | 
				
			||||||
    Todo,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl Related<super::guild::Entity> for Entity {
 | 
					 | 
				
			||||||
    fn to() -> RelationDef {
 | 
					 | 
				
			||||||
        Relation::Guild.def()
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl Related<super::user::Entity> for Entity {
 | 
					 | 
				
			||||||
    fn to() -> RelationDef {
 | 
					 | 
				
			||||||
        Relation::User.def()
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl Related<super::reminder::Entity> for Entity {
 | 
					 | 
				
			||||||
    fn to() -> RelationDef {
 | 
					 | 
				
			||||||
        Relation::Reminder.def()
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl Related<super::todo::Entity> for Entity {
 | 
					 | 
				
			||||||
    fn to() -> RelationDef {
 | 
					 | 
				
			||||||
        Relation::Todo.def()
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl ActiveModelBehavior for ActiveModel {}
 | 
					 | 
				
			||||||
@@ -1,34 +0,0 @@
 | 
				
			|||||||
//! SeaORM Entity. Generated by sea-orm-codegen 0.8.0
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use sea_orm::entity::prelude::*;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
 | 
					 | 
				
			||||||
#[sea_orm(table_name = "command_macro")]
 | 
					 | 
				
			||||||
pub struct Model {
 | 
					 | 
				
			||||||
    #[sea_orm(primary_key)]
 | 
					 | 
				
			||||||
    pub id: i32,
 | 
					 | 
				
			||||||
    pub guild_id: i64,
 | 
					 | 
				
			||||||
    pub name: String,
 | 
					 | 
				
			||||||
    pub description: Option<String>,
 | 
					 | 
				
			||||||
    pub commands: Option<Json>,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
 | 
					 | 
				
			||||||
pub enum Relation {
 | 
					 | 
				
			||||||
    #[sea_orm(
 | 
					 | 
				
			||||||
        belongs_to = "super::guild::Entity",
 | 
					 | 
				
			||||||
        from = "Column::GuildId",
 | 
					 | 
				
			||||||
        to = "super::guild::Column::Id",
 | 
					 | 
				
			||||||
        on_update = "NoAction",
 | 
					 | 
				
			||||||
        on_delete = "Cascade"
 | 
					 | 
				
			||||||
    )]
 | 
					 | 
				
			||||||
    Guild,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl Related<super::guild::Entity> for Entity {
 | 
					 | 
				
			||||||
    fn to() -> RelationDef {
 | 
					 | 
				
			||||||
        Relation::Guild.def()
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl ActiveModelBehavior for ActiveModel {}
 | 
					 | 
				
			||||||
@@ -1,48 +0,0 @@
 | 
				
			|||||||
//! SeaORM Entity. Generated by sea-orm-codegen 0.8.0
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use sea_orm::entity::prelude::*;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
 | 
					 | 
				
			||||||
#[sea_orm(table_name = "guild")]
 | 
					 | 
				
			||||||
pub struct Model {
 | 
					 | 
				
			||||||
    #[sea_orm(primary_key, auto_increment = false)]
 | 
					 | 
				
			||||||
    pub id: i64,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
 | 
					 | 
				
			||||||
pub enum Relation {
 | 
					 | 
				
			||||||
    #[sea_orm(has_many = "super::reminder_template::Entity")]
 | 
					 | 
				
			||||||
    ReminderTemplate,
 | 
					 | 
				
			||||||
    #[sea_orm(has_many = "super::channel::Entity")]
 | 
					 | 
				
			||||||
    Channel,
 | 
					 | 
				
			||||||
    #[sea_orm(has_many = "super::todo::Entity")]
 | 
					 | 
				
			||||||
    Todo,
 | 
					 | 
				
			||||||
    #[sea_orm(has_many = "super::command_macro::Entity")]
 | 
					 | 
				
			||||||
    CommandMacro,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl Related<super::reminder_template::Entity> for Entity {
 | 
					 | 
				
			||||||
    fn to() -> RelationDef {
 | 
					 | 
				
			||||||
        Relation::ReminderTemplate.def()
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl Related<super::channel::Entity> for Entity {
 | 
					 | 
				
			||||||
    fn to() -> RelationDef {
 | 
					 | 
				
			||||||
        Relation::Channel.def()
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl Related<super::todo::Entity> for Entity {
 | 
					 | 
				
			||||||
    fn to() -> RelationDef {
 | 
					 | 
				
			||||||
        Relation::Todo.def()
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl Related<super::command_macro::Entity> for Entity {
 | 
					 | 
				
			||||||
    fn to() -> RelationDef {
 | 
					 | 
				
			||||||
        Relation::CommandMacro.def()
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl ActiveModelBehavior for ActiveModel {}
 | 
					 | 
				
			||||||
@@ -1 +0,0 @@
 | 
				
			|||||||
 | 
					 | 
				
			||||||
@@ -1,14 +0,0 @@
 | 
				
			|||||||
//! SeaORM Entity. Generated by sea-orm-codegen 0.8.0
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
pub mod prelude;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
pub mod channel;
 | 
					 | 
				
			||||||
pub mod command_macro;
 | 
					 | 
				
			||||||
pub mod guild;
 | 
					 | 
				
			||||||
pub mod reminder;
 | 
					 | 
				
			||||||
pub mod reminder_template;
 | 
					 | 
				
			||||||
pub mod sea_orm_active_enums;
 | 
					 | 
				
			||||||
pub mod seaql_migrations;
 | 
					 | 
				
			||||||
pub mod timer;
 | 
					 | 
				
			||||||
pub mod todo;
 | 
					 | 
				
			||||||
pub mod user;
 | 
					 | 
				
			||||||
@@ -1,8 +0,0 @@
 | 
				
			|||||||
//! SeaORM Entity. Generated by sea-orm-codegen 0.8.0
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
pub use super::{
 | 
					 | 
				
			||||||
    channel::Entity as Channel, command_macro::Entity as CommandMacro, guild::Entity as Guild,
 | 
					 | 
				
			||||||
    reminder::Entity as Reminder, reminder_template::Entity as ReminderTemplate,
 | 
					 | 
				
			||||||
    seaql_migrations::Entity as SeaqlMigrations, timer::Entity as Timer, todo::Entity as Todo,
 | 
					 | 
				
			||||||
    user::Entity as User,
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
@@ -1,73 +0,0 @@
 | 
				
			|||||||
//! SeaORM Entity. Generated by sea-orm-codegen 0.8.0
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use sea_orm::entity::prelude::*;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use super::sea_orm_active_enums::Timezone;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
 | 
					 | 
				
			||||||
#[sea_orm(table_name = "reminder")]
 | 
					 | 
				
			||||||
pub struct Model {
 | 
					 | 
				
			||||||
    #[sea_orm(primary_key)]
 | 
					 | 
				
			||||||
    pub id: i32,
 | 
					 | 
				
			||||||
    pub uid: String,
 | 
					 | 
				
			||||||
    pub name: String,
 | 
					 | 
				
			||||||
    pub channel_id: i64,
 | 
					 | 
				
			||||||
    pub utc_time: DateTimeUtc,
 | 
					 | 
				
			||||||
    pub timezone: Timezone,
 | 
					 | 
				
			||||||
    pub interval_seconds: Option<i32>,
 | 
					 | 
				
			||||||
    pub interval_months: Option<i32>,
 | 
					 | 
				
			||||||
    pub enabled: bool,
 | 
					 | 
				
			||||||
    pub expires: Option<DateTimeUtc>,
 | 
					 | 
				
			||||||
    pub username: Option<String>,
 | 
					 | 
				
			||||||
    pub avatar: Option<String>,
 | 
					 | 
				
			||||||
    pub content: Option<String>,
 | 
					 | 
				
			||||||
    pub tts: bool,
 | 
					 | 
				
			||||||
    pub attachment: Option<Vec<u8>>,
 | 
					 | 
				
			||||||
    pub attachment_name: Option<String>,
 | 
					 | 
				
			||||||
    pub embed_title: Option<String>,
 | 
					 | 
				
			||||||
    pub embed_description: Option<String>,
 | 
					 | 
				
			||||||
    pub embed_image_url: Option<String>,
 | 
					 | 
				
			||||||
    pub embed_thumbnail_url: Option<String>,
 | 
					 | 
				
			||||||
    pub embed_footer: Option<String>,
 | 
					 | 
				
			||||||
    pub embed_footer_url: Option<String>,
 | 
					 | 
				
			||||||
    pub embed_author: Option<String>,
 | 
					 | 
				
			||||||
    pub embed_author_url: Option<String>,
 | 
					 | 
				
			||||||
    pub embed_color: Option<i32>,
 | 
					 | 
				
			||||||
    pub embed_fields: Option<Json>,
 | 
					 | 
				
			||||||
    pub set_at: DateTimeUtc,
 | 
					 | 
				
			||||||
    pub set_by: i64,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
 | 
					 | 
				
			||||||
pub enum Relation {
 | 
					 | 
				
			||||||
    #[sea_orm(
 | 
					 | 
				
			||||||
        belongs_to = "super::channel::Entity",
 | 
					 | 
				
			||||||
        from = "Column::ChannelId",
 | 
					 | 
				
			||||||
        to = "super::channel::Column::Id",
 | 
					 | 
				
			||||||
        on_update = "NoAction",
 | 
					 | 
				
			||||||
        on_delete = "Cascade"
 | 
					 | 
				
			||||||
    )]
 | 
					 | 
				
			||||||
    Channel,
 | 
					 | 
				
			||||||
    #[sea_orm(
 | 
					 | 
				
			||||||
        belongs_to = "super::user::Entity",
 | 
					 | 
				
			||||||
        from = "Column::SetBy",
 | 
					 | 
				
			||||||
        to = "super::user::Column::Id",
 | 
					 | 
				
			||||||
        on_update = "NoAction",
 | 
					 | 
				
			||||||
        on_delete = "Cascade"
 | 
					 | 
				
			||||||
    )]
 | 
					 | 
				
			||||||
    User,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl Related<super::channel::Entity> for Entity {
 | 
					 | 
				
			||||||
    fn to() -> RelationDef {
 | 
					 | 
				
			||||||
        Relation::Channel.def()
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl Related<super::user::Entity> for Entity {
 | 
					 | 
				
			||||||
    fn to() -> RelationDef {
 | 
					 | 
				
			||||||
        Relation::User.def()
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl ActiveModelBehavior for ActiveModel {}
 | 
					 | 
				
			||||||
@@ -1,48 +0,0 @@
 | 
				
			|||||||
//! SeaORM Entity. Generated by sea-orm-codegen 0.8.0
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use sea_orm::entity::prelude::*;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
 | 
					 | 
				
			||||||
#[sea_orm(table_name = "reminder_template")]
 | 
					 | 
				
			||||||
pub struct Model {
 | 
					 | 
				
			||||||
    #[sea_orm(primary_key)]
 | 
					 | 
				
			||||||
    pub id: i32,
 | 
					 | 
				
			||||||
    pub guild_id: i64,
 | 
					 | 
				
			||||||
    pub name: String,
 | 
					 | 
				
			||||||
    pub username: Option<String>,
 | 
					 | 
				
			||||||
    pub avatar: Option<String>,
 | 
					 | 
				
			||||||
    pub content: Option<String>,
 | 
					 | 
				
			||||||
    pub tts: bool,
 | 
					 | 
				
			||||||
    pub attachment: Option<Vec<u8>>,
 | 
					 | 
				
			||||||
    pub attachment_name: Option<String>,
 | 
					 | 
				
			||||||
    pub embed_title: Option<String>,
 | 
					 | 
				
			||||||
    pub embed_description: Option<String>,
 | 
					 | 
				
			||||||
    pub embed_image_url: Option<String>,
 | 
					 | 
				
			||||||
    pub embed_thumbnail_url: Option<String>,
 | 
					 | 
				
			||||||
    pub embed_footer: Option<String>,
 | 
					 | 
				
			||||||
    pub embed_footer_url: Option<String>,
 | 
					 | 
				
			||||||
    pub embed_author: Option<String>,
 | 
					 | 
				
			||||||
    pub embed_author_url: Option<String>,
 | 
					 | 
				
			||||||
    pub embed_color: Option<i32>,
 | 
					 | 
				
			||||||
    pub embed_fields: Option<Json>,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
 | 
					 | 
				
			||||||
pub enum Relation {
 | 
					 | 
				
			||||||
    #[sea_orm(
 | 
					 | 
				
			||||||
        belongs_to = "super::guild::Entity",
 | 
					 | 
				
			||||||
        from = "Column::GuildId",
 | 
					 | 
				
			||||||
        to = "super::guild::Column::Id",
 | 
					 | 
				
			||||||
        on_update = "NoAction",
 | 
					 | 
				
			||||||
        on_delete = "Cascade"
 | 
					 | 
				
			||||||
    )]
 | 
					 | 
				
			||||||
    Guild,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl Related<super::guild::Entity> for Entity {
 | 
					 | 
				
			||||||
    fn to() -> RelationDef {
 | 
					 | 
				
			||||||
        Relation::Guild.def()
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl ActiveModelBehavior for ActiveModel {}
 | 
					 | 
				
			||||||
@@ -1,22 +0,0 @@
 | 
				
			|||||||
//! SeaORM Entity. Generated by sea-orm-codegen 0.8.0
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use sea_orm::entity::prelude::*;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
 | 
					 | 
				
			||||||
#[sea_orm(table_name = "seaql_migrations")]
 | 
					 | 
				
			||||||
pub struct Model {
 | 
					 | 
				
			||||||
    #[sea_orm(primary_key, auto_increment = false)]
 | 
					 | 
				
			||||||
    pub version: String,
 | 
					 | 
				
			||||||
    pub applied_at: i64,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Copy, Clone, Debug, EnumIter)]
 | 
					 | 
				
			||||||
pub enum Relation {}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl RelationTrait for Relation {
 | 
					 | 
				
			||||||
    fn def(&self) -> RelationDef {
 | 
					 | 
				
			||||||
        panic!("No RelationDef")
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl ActiveModelBehavior for ActiveModel {}
 | 
					 | 
				
			||||||
@@ -1,36 +0,0 @@
 | 
				
			|||||||
//! SeaORM Entity. Generated by sea-orm-codegen 0.8.0
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use sea_orm::entity::prelude::*;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
 | 
					 | 
				
			||||||
#[sea_orm(table_name = "timer")]
 | 
					 | 
				
			||||||
pub struct Model {
 | 
					 | 
				
			||||||
    #[sea_orm(primary_key)]
 | 
					 | 
				
			||||||
    pub id: i32,
 | 
					 | 
				
			||||||
    pub start_time: DateTimeUtc,
 | 
					 | 
				
			||||||
    pub name: String,
 | 
					 | 
				
			||||||
    pub user_id: Option<i64>,
 | 
					 | 
				
			||||||
    pub guild_id: Option<i64>,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
 | 
					 | 
				
			||||||
pub enum Relation {
 | 
					 | 
				
			||||||
    #[sea_orm(
 | 
					 | 
				
			||||||
        belongs_to = "super::guild::Entity",
 | 
					 | 
				
			||||||
        from = "Column::GuildId",
 | 
					 | 
				
			||||||
        to = "super::guild::Column::Id",
 | 
					 | 
				
			||||||
        on_update = "NoAction",
 | 
					 | 
				
			||||||
        on_delete = "Cascade"
 | 
					 | 
				
			||||||
    )]
 | 
					 | 
				
			||||||
    Guild2,
 | 
					 | 
				
			||||||
    #[sea_orm(
 | 
					 | 
				
			||||||
        belongs_to = "super::guild::Entity",
 | 
					 | 
				
			||||||
        from = "Column::UserId",
 | 
					 | 
				
			||||||
        to = "super::guild::Column::Id",
 | 
					 | 
				
			||||||
        on_update = "NoAction",
 | 
					 | 
				
			||||||
        on_delete = "Cascade"
 | 
					 | 
				
			||||||
    )]
 | 
					 | 
				
			||||||
    Guild1,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl ActiveModelBehavior for ActiveModel {}
 | 
					 | 
				
			||||||
@@ -1,62 +0,0 @@
 | 
				
			|||||||
//! SeaORM Entity. Generated by sea-orm-codegen 0.8.0
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use sea_orm::entity::prelude::*;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
 | 
					 | 
				
			||||||
#[sea_orm(table_name = "todo")]
 | 
					 | 
				
			||||||
pub struct Model {
 | 
					 | 
				
			||||||
    #[sea_orm(primary_key)]
 | 
					 | 
				
			||||||
    pub id: i32,
 | 
					 | 
				
			||||||
    pub user_id: Option<i64>,
 | 
					 | 
				
			||||||
    pub guild_id: Option<i64>,
 | 
					 | 
				
			||||||
    pub channel_id: Option<i64>,
 | 
					 | 
				
			||||||
    pub value: String,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
 | 
					 | 
				
			||||||
pub enum Relation {
 | 
					 | 
				
			||||||
    #[sea_orm(
 | 
					 | 
				
			||||||
        belongs_to = "super::channel::Entity",
 | 
					 | 
				
			||||||
        from = "Column::ChannelId",
 | 
					 | 
				
			||||||
        to = "super::channel::Column::Id",
 | 
					 | 
				
			||||||
        on_update = "NoAction",
 | 
					 | 
				
			||||||
        on_delete = "Cascade"
 | 
					 | 
				
			||||||
    )]
 | 
					 | 
				
			||||||
    Channel,
 | 
					 | 
				
			||||||
    #[sea_orm(
 | 
					 | 
				
			||||||
        belongs_to = "super::guild::Entity",
 | 
					 | 
				
			||||||
        from = "Column::GuildId",
 | 
					 | 
				
			||||||
        to = "super::guild::Column::Id",
 | 
					 | 
				
			||||||
        on_update = "NoAction",
 | 
					 | 
				
			||||||
        on_delete = "Cascade"
 | 
					 | 
				
			||||||
    )]
 | 
					 | 
				
			||||||
    Guild,
 | 
					 | 
				
			||||||
    #[sea_orm(
 | 
					 | 
				
			||||||
        belongs_to = "super::user::Entity",
 | 
					 | 
				
			||||||
        from = "Column::UserId",
 | 
					 | 
				
			||||||
        to = "super::user::Column::Id",
 | 
					 | 
				
			||||||
        on_update = "NoAction",
 | 
					 | 
				
			||||||
        on_delete = "Cascade"
 | 
					 | 
				
			||||||
    )]
 | 
					 | 
				
			||||||
    User,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl Related<super::channel::Entity> for Entity {
 | 
					 | 
				
			||||||
    fn to() -> RelationDef {
 | 
					 | 
				
			||||||
        Relation::Channel.def()
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl Related<super::guild::Entity> for Entity {
 | 
					 | 
				
			||||||
    fn to() -> RelationDef {
 | 
					 | 
				
			||||||
        Relation::Guild.def()
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl Related<super::user::Entity> for Entity {
 | 
					 | 
				
			||||||
    fn to() -> RelationDef {
 | 
					 | 
				
			||||||
        Relation::User.def()
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl ActiveModelBehavior for ActiveModel {}
 | 
					 | 
				
			||||||
@@ -1,50 +0,0 @@
 | 
				
			|||||||
//! SeaORM Entity. Generated by sea-orm-codegen 0.8.0
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use sea_orm::entity::prelude::*;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use super::sea_orm_active_enums::Timezone;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
 | 
					 | 
				
			||||||
#[sea_orm(table_name = "user")]
 | 
					 | 
				
			||||||
pub struct Model {
 | 
					 | 
				
			||||||
    #[sea_orm(primary_key, auto_increment = false)]
 | 
					 | 
				
			||||||
    pub id: i64,
 | 
					 | 
				
			||||||
    pub dm_channel: i64,
 | 
					 | 
				
			||||||
    pub timezone: Timezone,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
 | 
					 | 
				
			||||||
pub enum Relation {
 | 
					 | 
				
			||||||
    #[sea_orm(
 | 
					 | 
				
			||||||
        belongs_to = "super::channel::Entity",
 | 
					 | 
				
			||||||
        from = "Column::DmChannel",
 | 
					 | 
				
			||||||
        to = "super::channel::Column::Id",
 | 
					 | 
				
			||||||
        on_update = "NoAction",
 | 
					 | 
				
			||||||
        on_delete = "Cascade"
 | 
					 | 
				
			||||||
    )]
 | 
					 | 
				
			||||||
    Channel,
 | 
					 | 
				
			||||||
    #[sea_orm(has_many = "super::reminder::Entity")]
 | 
					 | 
				
			||||||
    Reminder,
 | 
					 | 
				
			||||||
    #[sea_orm(has_many = "super::todo::Entity")]
 | 
					 | 
				
			||||||
    Todo,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl Related<super::channel::Entity> for Entity {
 | 
					 | 
				
			||||||
    fn to() -> RelationDef {
 | 
					 | 
				
			||||||
        Relation::Channel.def()
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl Related<super::reminder::Entity> for Entity {
 | 
					 | 
				
			||||||
    fn to() -> RelationDef {
 | 
					 | 
				
			||||||
        Relation::Reminder.def()
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl Related<super::todo::Entity> for Entity {
 | 
					 | 
				
			||||||
    fn to() -> RelationDef {
 | 
					 | 
				
			||||||
        Relation::Todo.def()
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl ActiveModelBehavior for ActiveModel {}
 | 
					 | 
				
			||||||
							
								
								
									
										2400
									
								
								models/migration/Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						@@ -1,16 +0,0 @@
 | 
				
			|||||||
[package]
 | 
					 | 
				
			||||||
name = "migration"
 | 
					 | 
				
			||||||
version = "0.1.0"
 | 
					 | 
				
			||||||
edition = "2021"
 | 
					 | 
				
			||||||
publish = false
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
[lib]
 | 
					 | 
				
			||||||
name = "migration"
 | 
					 | 
				
			||||||
path = "src/lib.rs"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
[dependencies]
 | 
					 | 
				
			||||||
entity = { path = "../entity" }
 | 
					 | 
				
			||||||
chrono-tz = "^0.6"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
[dependencies.sea-orm-migration]
 | 
					 | 
				
			||||||
version = "^0.8.0"
 | 
					 | 
				
			||||||
@@ -1,37 +0,0 @@
 | 
				
			|||||||
# Running Migrator CLI
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
- Apply all pending migrations
 | 
					 | 
				
			||||||
    ```sh
 | 
					 | 
				
			||||||
    cargo run
 | 
					 | 
				
			||||||
    ```
 | 
					 | 
				
			||||||
    ```sh
 | 
					 | 
				
			||||||
    cargo run -- up
 | 
					 | 
				
			||||||
    ```
 | 
					 | 
				
			||||||
- Apply first 10 pending migrations
 | 
					 | 
				
			||||||
    ```sh
 | 
					 | 
				
			||||||
    cargo run -- up -n 10
 | 
					 | 
				
			||||||
    ```
 | 
					 | 
				
			||||||
- Rollback last applied migrations
 | 
					 | 
				
			||||||
    ```sh
 | 
					 | 
				
			||||||
    cargo run -- down
 | 
					 | 
				
			||||||
    ```
 | 
					 | 
				
			||||||
- Rollback last 10 applied migrations
 | 
					 | 
				
			||||||
    ```sh
 | 
					 | 
				
			||||||
    cargo run -- down -n 10
 | 
					 | 
				
			||||||
    ```
 | 
					 | 
				
			||||||
- Drop all tables from the database, then reapply all migrations
 | 
					 | 
				
			||||||
    ```sh
 | 
					 | 
				
			||||||
    cargo run -- fresh
 | 
					 | 
				
			||||||
    ```
 | 
					 | 
				
			||||||
- Rollback all applied migrations, then reapply all migrations
 | 
					 | 
				
			||||||
    ```sh
 | 
					 | 
				
			||||||
    cargo run -- refresh
 | 
					 | 
				
			||||||
    ```
 | 
					 | 
				
			||||||
- Rollback all applied migrations
 | 
					 | 
				
			||||||
    ```sh
 | 
					 | 
				
			||||||
    cargo run -- reset
 | 
					 | 
				
			||||||
    ```
 | 
					 | 
				
			||||||
- Check the status of all migrations
 | 
					 | 
				
			||||||
    ```sh
 | 
					 | 
				
			||||||
    cargo run -- status
 | 
					 | 
				
			||||||
    ```
 | 
					 | 
				
			||||||
@@ -1,12 +0,0 @@
 | 
				
			|||||||
pub use sea_orm_migration::prelude::*;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
mod m20220101_000001_create_table;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
pub struct Migrator;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[async_trait::async_trait]
 | 
					 | 
				
			||||||
impl MigratorTrait for Migrator {
 | 
					 | 
				
			||||||
    fn migrations() -> Vec<Box<dyn MigrationTrait>> {
 | 
					 | 
				
			||||||
        vec![Box::new(m20220101_000001_create_table::Migration)]
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,553 +0,0 @@
 | 
				
			|||||||
use chrono_tz::{Tz, TZ_VARIANTS};
 | 
					 | 
				
			||||||
use sea_orm_migration::prelude::*;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use crate::extension::postgres::Type;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
pub struct Migration;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl MigrationName for Migration {
 | 
					 | 
				
			||||||
    fn name(&self) -> &str {
 | 
					 | 
				
			||||||
        "m20220101_000001_create_table"
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Iden)]
 | 
					 | 
				
			||||||
pub enum Guild {
 | 
					 | 
				
			||||||
    Table,
 | 
					 | 
				
			||||||
    Id,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Iden)]
 | 
					 | 
				
			||||||
pub enum Channel {
 | 
					 | 
				
			||||||
    Table,
 | 
					 | 
				
			||||||
    Id,
 | 
					 | 
				
			||||||
    GuildId,
 | 
					 | 
				
			||||||
    Nudge,
 | 
					 | 
				
			||||||
    WebhookId,
 | 
					 | 
				
			||||||
    WebhookToken,
 | 
					 | 
				
			||||||
    Paused,
 | 
					 | 
				
			||||||
    PausedUntil,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Iden)]
 | 
					 | 
				
			||||||
pub enum User {
 | 
					 | 
				
			||||||
    Table,
 | 
					 | 
				
			||||||
    Id,
 | 
					 | 
				
			||||||
    DmChannel,
 | 
					 | 
				
			||||||
    Timezone,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Iden)]
 | 
					 | 
				
			||||||
pub enum Reminder {
 | 
					 | 
				
			||||||
    Table,
 | 
					 | 
				
			||||||
    Id,
 | 
					 | 
				
			||||||
    Uid,
 | 
					 | 
				
			||||||
    Name,
 | 
					 | 
				
			||||||
    ChannelId,
 | 
					 | 
				
			||||||
    UtcTime,
 | 
					 | 
				
			||||||
    Timezone,
 | 
					 | 
				
			||||||
    IntervalSeconds,
 | 
					 | 
				
			||||||
    IntervalMonths,
 | 
					 | 
				
			||||||
    Enabled,
 | 
					 | 
				
			||||||
    Expires,
 | 
					 | 
				
			||||||
    Username,
 | 
					 | 
				
			||||||
    Avatar,
 | 
					 | 
				
			||||||
    Content,
 | 
					 | 
				
			||||||
    Tts,
 | 
					 | 
				
			||||||
    Attachment,
 | 
					 | 
				
			||||||
    AttachmentName,
 | 
					 | 
				
			||||||
    EmbedTitle,
 | 
					 | 
				
			||||||
    EmbedDescription,
 | 
					 | 
				
			||||||
    EmbedImageUrl,
 | 
					 | 
				
			||||||
    EmbedThumbnailUrl,
 | 
					 | 
				
			||||||
    EmbedFooter,
 | 
					 | 
				
			||||||
    EmbedFooterUrl,
 | 
					 | 
				
			||||||
    EmbedAuthor,
 | 
					 | 
				
			||||||
    EmbedAuthorUrl,
 | 
					 | 
				
			||||||
    EmbedColor,
 | 
					 | 
				
			||||||
    EmbedFields,
 | 
					 | 
				
			||||||
    SetAt,
 | 
					 | 
				
			||||||
    SetBy,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Iden)]
 | 
					 | 
				
			||||||
pub enum ReminderTemplate {
 | 
					 | 
				
			||||||
    Table,
 | 
					 | 
				
			||||||
    Id,
 | 
					 | 
				
			||||||
    GuildId,
 | 
					 | 
				
			||||||
    Name,
 | 
					 | 
				
			||||||
    Username,
 | 
					 | 
				
			||||||
    Avatar,
 | 
					 | 
				
			||||||
    Content,
 | 
					 | 
				
			||||||
    Tts,
 | 
					 | 
				
			||||||
    Attachment,
 | 
					 | 
				
			||||||
    AttachmentName,
 | 
					 | 
				
			||||||
    EmbedTitle,
 | 
					 | 
				
			||||||
    EmbedDescription,
 | 
					 | 
				
			||||||
    EmbedImageUrl,
 | 
					 | 
				
			||||||
    EmbedThumbnailUrl,
 | 
					 | 
				
			||||||
    EmbedFooter,
 | 
					 | 
				
			||||||
    EmbedFooterUrl,
 | 
					 | 
				
			||||||
    EmbedAuthor,
 | 
					 | 
				
			||||||
    EmbedAuthorUrl,
 | 
					 | 
				
			||||||
    EmbedColor,
 | 
					 | 
				
			||||||
    EmbedFields,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Iden)]
 | 
					 | 
				
			||||||
pub enum Timer {
 | 
					 | 
				
			||||||
    Table,
 | 
					 | 
				
			||||||
    Id,
 | 
					 | 
				
			||||||
    StartTime,
 | 
					 | 
				
			||||||
    Name,
 | 
					 | 
				
			||||||
    UserId,
 | 
					 | 
				
			||||||
    GuildId,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Iden)]
 | 
					 | 
				
			||||||
pub enum Todo {
 | 
					 | 
				
			||||||
    Table,
 | 
					 | 
				
			||||||
    Id,
 | 
					 | 
				
			||||||
    UserId,
 | 
					 | 
				
			||||||
    GuildId,
 | 
					 | 
				
			||||||
    ChannelId,
 | 
					 | 
				
			||||||
    Value,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Iden)]
 | 
					 | 
				
			||||||
pub enum CommandMacro {
 | 
					 | 
				
			||||||
    Table,
 | 
					 | 
				
			||||||
    Id,
 | 
					 | 
				
			||||||
    GuildId,
 | 
					 | 
				
			||||||
    Name,
 | 
					 | 
				
			||||||
    Description,
 | 
					 | 
				
			||||||
    Commands,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
pub enum Timezone {
 | 
					 | 
				
			||||||
    Type,
 | 
					 | 
				
			||||||
    Tz(Tz),
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl Iden for Timezone {
 | 
					 | 
				
			||||||
    fn unquoted(&self, s: &mut dyn Write) {
 | 
					 | 
				
			||||||
        write!(
 | 
					 | 
				
			||||||
            s,
 | 
					 | 
				
			||||||
            "{}",
 | 
					 | 
				
			||||||
            match self {
 | 
					 | 
				
			||||||
                Self::Type => "timezone".to_string(),
 | 
					 | 
				
			||||||
                Self::Tz(tz) => tz.to_string(),
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        .unwrap();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[async_trait::async_trait]
 | 
					 | 
				
			||||||
impl MigrationTrait for Migration {
 | 
					 | 
				
			||||||
    async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
 | 
					 | 
				
			||||||
        manager
 | 
					 | 
				
			||||||
            .create_type(
 | 
					 | 
				
			||||||
                Type::create()
 | 
					 | 
				
			||||||
                    .as_enum(Timezone::Type)
 | 
					 | 
				
			||||||
                    .values(TZ_VARIANTS.iter().map(|tz| Timezone::Tz(tz.to_owned())))
 | 
					 | 
				
			||||||
                    .to_owned(),
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
            .await?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        manager
 | 
					 | 
				
			||||||
            .create_table(
 | 
					 | 
				
			||||||
                Table::create()
 | 
					 | 
				
			||||||
                    .table(Guild::Table)
 | 
					 | 
				
			||||||
                    .if_not_exists()
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(Guild::Id).big_integer().not_null().primary_key())
 | 
					 | 
				
			||||||
                    .to_owned(),
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
            .await?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        manager
 | 
					 | 
				
			||||||
            .create_table(
 | 
					 | 
				
			||||||
                Table::create()
 | 
					 | 
				
			||||||
                    .table(Channel::Table)
 | 
					 | 
				
			||||||
                    .if_not_exists()
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(Channel::Id).big_integer().not_null().primary_key())
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(Channel::GuildId).big_integer())
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(Channel::Nudge).integer().not_null().default(0))
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(Channel::WebhookId).big_integer())
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(Channel::WebhookToken).string())
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(Channel::Paused).boolean().not_null().default(false))
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(Channel::PausedUntil).date_time())
 | 
					 | 
				
			||||||
                    .to_owned(),
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
            .await?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        manager
 | 
					 | 
				
			||||||
            .create_foreign_key(
 | 
					 | 
				
			||||||
                ForeignKey::create()
 | 
					 | 
				
			||||||
                    .name("fk_channel_guild")
 | 
					 | 
				
			||||||
                    .from(Channel::Table, Channel::GuildId)
 | 
					 | 
				
			||||||
                    .to(Guild::Table, Guild::Id)
 | 
					 | 
				
			||||||
                    .on_delete(ForeignKeyAction::Cascade)
 | 
					 | 
				
			||||||
                    .to_owned(),
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
            .await?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        manager
 | 
					 | 
				
			||||||
            .create_table(
 | 
					 | 
				
			||||||
                Table::create()
 | 
					 | 
				
			||||||
                    .table(User::Table)
 | 
					 | 
				
			||||||
                    .if_not_exists()
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(User::Id).big_integer().not_null().primary_key())
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(User::DmChannel).big_integer().not_null())
 | 
					 | 
				
			||||||
                    .col(
 | 
					 | 
				
			||||||
                        ColumnDef::new(User::Timezone)
 | 
					 | 
				
			||||||
                            .custom(Timezone::Type)
 | 
					 | 
				
			||||||
                            .not_null()
 | 
					 | 
				
			||||||
                            .default("UTC"),
 | 
					 | 
				
			||||||
                    )
 | 
					 | 
				
			||||||
                    .to_owned(),
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
            .await?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        manager
 | 
					 | 
				
			||||||
            .create_foreign_key(
 | 
					 | 
				
			||||||
                ForeignKey::create()
 | 
					 | 
				
			||||||
                    .name("fk_user_channel")
 | 
					 | 
				
			||||||
                    .from(User::Table, User::DmChannel)
 | 
					 | 
				
			||||||
                    .to(Channel::Table, Channel::Id)
 | 
					 | 
				
			||||||
                    .on_delete(ForeignKeyAction::Cascade)
 | 
					 | 
				
			||||||
                    .to_owned(),
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
            .await?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        manager
 | 
					 | 
				
			||||||
            .create_table(
 | 
					 | 
				
			||||||
                Table::create()
 | 
					 | 
				
			||||||
                    .table(Reminder::Table)
 | 
					 | 
				
			||||||
                    .if_not_exists()
 | 
					 | 
				
			||||||
                    .col(
 | 
					 | 
				
			||||||
                        ColumnDef::new(Reminder::Id)
 | 
					 | 
				
			||||||
                            .integer()
 | 
					 | 
				
			||||||
                            .not_null()
 | 
					 | 
				
			||||||
                            .auto_increment()
 | 
					 | 
				
			||||||
                            .primary_key(),
 | 
					 | 
				
			||||||
                    )
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(Reminder::Uid).string().char_len(64).not_null())
 | 
					 | 
				
			||||||
                    .col(
 | 
					 | 
				
			||||||
                        ColumnDef::new(Reminder::Name)
 | 
					 | 
				
			||||||
                            .string()
 | 
					 | 
				
			||||||
                            .char_len(24)
 | 
					 | 
				
			||||||
                            .default("Reminder")
 | 
					 | 
				
			||||||
                            .not_null(),
 | 
					 | 
				
			||||||
                    )
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(Reminder::ChannelId).big_integer().not_null())
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(Reminder::UtcTime).date_time().not_null())
 | 
					 | 
				
			||||||
                    .col(
 | 
					 | 
				
			||||||
                        ColumnDef::new(Reminder::Timezone)
 | 
					 | 
				
			||||||
                            .custom(Timezone::Type)
 | 
					 | 
				
			||||||
                            .not_null()
 | 
					 | 
				
			||||||
                            .default("UTC"),
 | 
					 | 
				
			||||||
                    )
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(Reminder::IntervalSeconds).integer())
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(Reminder::IntervalMonths).integer())
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(Reminder::Enabled).boolean().not_null().default(false))
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(Reminder::Expires).date_time())
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(Reminder::Username).string_len(32))
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(Reminder::Avatar).string_len(512))
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(Reminder::Content).string_len(2000))
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(Reminder::Tts).boolean().not_null().default(false))
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(Reminder::Attachment).binary_len(8 * 1024 * 1024))
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(Reminder::AttachmentName).string_len(260))
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(Reminder::EmbedTitle).string_len(256))
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(Reminder::EmbedDescription).string_len(4096))
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(Reminder::EmbedImageUrl).string_len(500))
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(Reminder::EmbedThumbnailUrl).string_len(500))
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(Reminder::EmbedFooter).string_len(2048))
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(Reminder::EmbedFooterUrl).string_len(500))
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(Reminder::EmbedAuthor).string_len(256))
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(Reminder::EmbedAuthorUrl).string_len(500))
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(Reminder::EmbedColor).integer())
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(Reminder::EmbedFields).json())
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(Reminder::SetAt).date_time().not_null().default("NOW()"))
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(Reminder::SetBy).big_integer().not_null())
 | 
					 | 
				
			||||||
                    .to_owned(),
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
            .await?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        manager
 | 
					 | 
				
			||||||
            .create_foreign_key(
 | 
					 | 
				
			||||||
                ForeignKey::create()
 | 
					 | 
				
			||||||
                    .name("fk_reminder_channel")
 | 
					 | 
				
			||||||
                    .from(Reminder::Table, Reminder::ChannelId)
 | 
					 | 
				
			||||||
                    .to(Channel::Table, Channel::Id)
 | 
					 | 
				
			||||||
                    .on_delete(ForeignKeyAction::Cascade)
 | 
					 | 
				
			||||||
                    .to_owned(),
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
            .await?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        manager
 | 
					 | 
				
			||||||
            .create_foreign_key(
 | 
					 | 
				
			||||||
                ForeignKey::create()
 | 
					 | 
				
			||||||
                    .name("fk_reminder_user")
 | 
					 | 
				
			||||||
                    .from(Reminder::Table, Reminder::SetBy)
 | 
					 | 
				
			||||||
                    .to(User::Table, User::Id)
 | 
					 | 
				
			||||||
                    .on_delete(ForeignKeyAction::Cascade)
 | 
					 | 
				
			||||||
                    .to_owned(),
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
            .await?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        manager
 | 
					 | 
				
			||||||
            .create_table(
 | 
					 | 
				
			||||||
                Table::create()
 | 
					 | 
				
			||||||
                    .table(ReminderTemplate::Table)
 | 
					 | 
				
			||||||
                    .if_not_exists()
 | 
					 | 
				
			||||||
                    .col(
 | 
					 | 
				
			||||||
                        ColumnDef::new(ReminderTemplate::Id)
 | 
					 | 
				
			||||||
                            .integer()
 | 
					 | 
				
			||||||
                            .not_null()
 | 
					 | 
				
			||||||
                            .auto_increment()
 | 
					 | 
				
			||||||
                            .primary_key(),
 | 
					 | 
				
			||||||
                    )
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(ReminderTemplate::GuildId).big_integer().not_null())
 | 
					 | 
				
			||||||
                    .col(
 | 
					 | 
				
			||||||
                        ColumnDef::new(ReminderTemplate::Name)
 | 
					 | 
				
			||||||
                            .string()
 | 
					 | 
				
			||||||
                            .char_len(24)
 | 
					 | 
				
			||||||
                            .default("Reminder")
 | 
					 | 
				
			||||||
                            .not_null(),
 | 
					 | 
				
			||||||
                    )
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(ReminderTemplate::Username).string_len(32))
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(ReminderTemplate::Avatar).string_len(512))
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(ReminderTemplate::Content).string_len(2000))
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(ReminderTemplate::Tts).boolean().not_null().default(false))
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(ReminderTemplate::Attachment).binary_len(8 * 1024 * 1024))
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(ReminderTemplate::AttachmentName).string_len(260))
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(ReminderTemplate::EmbedTitle).string_len(256))
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(ReminderTemplate::EmbedDescription).string_len(4096))
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(ReminderTemplate::EmbedImageUrl).string_len(500))
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(ReminderTemplate::EmbedThumbnailUrl).string_len(500))
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(ReminderTemplate::EmbedFooter).string_len(2048))
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(ReminderTemplate::EmbedFooterUrl).string_len(500))
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(ReminderTemplate::EmbedAuthor).string_len(256))
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(ReminderTemplate::EmbedAuthorUrl).string_len(500))
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(ReminderTemplate::EmbedColor).integer())
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(ReminderTemplate::EmbedFields).json())
 | 
					 | 
				
			||||||
                    .to_owned(),
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
            .await?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        manager
 | 
					 | 
				
			||||||
            .create_foreign_key(
 | 
					 | 
				
			||||||
                ForeignKey::create()
 | 
					 | 
				
			||||||
                    .name("fk_reminder_template_guild")
 | 
					 | 
				
			||||||
                    .from(ReminderTemplate::Table, ReminderTemplate::GuildId)
 | 
					 | 
				
			||||||
                    .to(Guild::Table, Guild::Id)
 | 
					 | 
				
			||||||
                    .on_delete(ForeignKeyAction::Cascade)
 | 
					 | 
				
			||||||
                    .to_owned(),
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
            .await?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        manager
 | 
					 | 
				
			||||||
            .create_table(
 | 
					 | 
				
			||||||
                Table::create()
 | 
					 | 
				
			||||||
                    .table(Timer::Table)
 | 
					 | 
				
			||||||
                    .if_not_exists()
 | 
					 | 
				
			||||||
                    .col(
 | 
					 | 
				
			||||||
                        ColumnDef::new(Timer::Id)
 | 
					 | 
				
			||||||
                            .integer()
 | 
					 | 
				
			||||||
                            .not_null()
 | 
					 | 
				
			||||||
                            .auto_increment()
 | 
					 | 
				
			||||||
                            .primary_key(),
 | 
					 | 
				
			||||||
                    )
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(Timer::StartTime).date_time().not_null().default("NOW()"))
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(Timer::Name).string_len(32).not_null().default("Timer"))
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(Timer::UserId).big_integer())
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(Timer::GuildId).big_integer())
 | 
					 | 
				
			||||||
                    .to_owned(),
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
            .await?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        manager
 | 
					 | 
				
			||||||
            .create_foreign_key(
 | 
					 | 
				
			||||||
                ForeignKey::create()
 | 
					 | 
				
			||||||
                    .name("fk_timer_user")
 | 
					 | 
				
			||||||
                    .from(Timer::Table, Timer::UserId)
 | 
					 | 
				
			||||||
                    .to(Guild::Table, Guild::Id)
 | 
					 | 
				
			||||||
                    .on_delete(ForeignKeyAction::Cascade)
 | 
					 | 
				
			||||||
                    .to_owned(),
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
            .await?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        manager
 | 
					 | 
				
			||||||
            .create_foreign_key(
 | 
					 | 
				
			||||||
                ForeignKey::create()
 | 
					 | 
				
			||||||
                    .name("fk_timer_guild")
 | 
					 | 
				
			||||||
                    .from(Timer::Table, Timer::GuildId)
 | 
					 | 
				
			||||||
                    .to(Guild::Table, Guild::Id)
 | 
					 | 
				
			||||||
                    .on_delete(ForeignKeyAction::Cascade)
 | 
					 | 
				
			||||||
                    .to_owned(),
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
            .await?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        manager
 | 
					 | 
				
			||||||
            .create_table(
 | 
					 | 
				
			||||||
                Table::create()
 | 
					 | 
				
			||||||
                    .table(Todo::Table)
 | 
					 | 
				
			||||||
                    .if_not_exists()
 | 
					 | 
				
			||||||
                    .col(
 | 
					 | 
				
			||||||
                        ColumnDef::new(Todo::Id)
 | 
					 | 
				
			||||||
                            .integer()
 | 
					 | 
				
			||||||
                            .not_null()
 | 
					 | 
				
			||||||
                            .auto_increment()
 | 
					 | 
				
			||||||
                            .primary_key(),
 | 
					 | 
				
			||||||
                    )
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(Todo::UserId).big_integer())
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(Todo::GuildId).big_integer())
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(Todo::ChannelId).big_integer())
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(Todo::Value).string_len(2000).not_null())
 | 
					 | 
				
			||||||
                    .to_owned(),
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
            .await?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        manager
 | 
					 | 
				
			||||||
            .create_foreign_key(
 | 
					 | 
				
			||||||
                ForeignKey::create()
 | 
					 | 
				
			||||||
                    .name("fk_todo_user")
 | 
					 | 
				
			||||||
                    .from(Todo::Table, Todo::UserId)
 | 
					 | 
				
			||||||
                    .to(User::Table, User::Id)
 | 
					 | 
				
			||||||
                    .on_delete(ForeignKeyAction::Cascade)
 | 
					 | 
				
			||||||
                    .to_owned(),
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
            .await?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        manager
 | 
					 | 
				
			||||||
            .create_foreign_key(
 | 
					 | 
				
			||||||
                ForeignKey::create()
 | 
					 | 
				
			||||||
                    .name("fk_todo_guild")
 | 
					 | 
				
			||||||
                    .from(Todo::Table, Todo::GuildId)
 | 
					 | 
				
			||||||
                    .to(Guild::Table, Guild::Id)
 | 
					 | 
				
			||||||
                    .on_delete(ForeignKeyAction::Cascade)
 | 
					 | 
				
			||||||
                    .to_owned(),
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
            .await?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        manager
 | 
					 | 
				
			||||||
            .create_foreign_key(
 | 
					 | 
				
			||||||
                ForeignKey::create()
 | 
					 | 
				
			||||||
                    .name("fk_todo_channel")
 | 
					 | 
				
			||||||
                    .from(Todo::Table, Todo::ChannelId)
 | 
					 | 
				
			||||||
                    .to(Channel::Table, Channel::Id)
 | 
					 | 
				
			||||||
                    .on_delete(ForeignKeyAction::Cascade)
 | 
					 | 
				
			||||||
                    .to_owned(),
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
            .await?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        manager
 | 
					 | 
				
			||||||
            .create_table(
 | 
					 | 
				
			||||||
                Table::create()
 | 
					 | 
				
			||||||
                    .table(CommandMacro::Table)
 | 
					 | 
				
			||||||
                    .if_not_exists()
 | 
					 | 
				
			||||||
                    .col(
 | 
					 | 
				
			||||||
                        ColumnDef::new(CommandMacro::Id)
 | 
					 | 
				
			||||||
                            .integer()
 | 
					 | 
				
			||||||
                            .not_null()
 | 
					 | 
				
			||||||
                            .auto_increment()
 | 
					 | 
				
			||||||
                            .primary_key(),
 | 
					 | 
				
			||||||
                    )
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(CommandMacro::GuildId).big_integer().not_null())
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(CommandMacro::Name).string_len(100).not_null())
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(CommandMacro::Description).string_len(100))
 | 
					 | 
				
			||||||
                    .col(ColumnDef::new(CommandMacro::Commands).json())
 | 
					 | 
				
			||||||
                    .to_owned(),
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
            .await?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        manager
 | 
					 | 
				
			||||||
            .create_foreign_key(
 | 
					 | 
				
			||||||
                ForeignKey::create()
 | 
					 | 
				
			||||||
                    .name("fk_command_macro_guild")
 | 
					 | 
				
			||||||
                    .from(CommandMacro::Table, CommandMacro::GuildId)
 | 
					 | 
				
			||||||
                    .to(Guild::Table, Guild::Id)
 | 
					 | 
				
			||||||
                    .on_delete(ForeignKeyAction::Cascade)
 | 
					 | 
				
			||||||
                    .to_owned(),
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
            .await?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        Ok(())
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
 | 
					 | 
				
			||||||
        manager
 | 
					 | 
				
			||||||
            .drop_foreign_key(
 | 
					 | 
				
			||||||
                ForeignKey::drop().table(Channel::Table).name("fk_channel_guild").to_owned(),
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
            .await?;
 | 
					 | 
				
			||||||
        manager
 | 
					 | 
				
			||||||
            .drop_foreign_key(
 | 
					 | 
				
			||||||
                ForeignKey::drop().table(User::Table).name("fk_user_channel").to_owned(),
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
            .await?;
 | 
					 | 
				
			||||||
        manager
 | 
					 | 
				
			||||||
            .drop_foreign_key(
 | 
					 | 
				
			||||||
                ForeignKey::drop().table(Reminder::Table).name("fk_reminder_channel").to_owned(),
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
            .await?;
 | 
					 | 
				
			||||||
        manager
 | 
					 | 
				
			||||||
            .drop_foreign_key(
 | 
					 | 
				
			||||||
                ForeignKey::drop().table(Reminder::Table).name("fk_reminder_user").to_owned(),
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
            .await?;
 | 
					 | 
				
			||||||
        manager
 | 
					 | 
				
			||||||
            .drop_foreign_key(
 | 
					 | 
				
			||||||
                ForeignKey::drop()
 | 
					 | 
				
			||||||
                    .table(ReminderTemplate::Table)
 | 
					 | 
				
			||||||
                    .name("fk_reminder_template_guild")
 | 
					 | 
				
			||||||
                    .to_owned(),
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
            .await?;
 | 
					 | 
				
			||||||
        manager
 | 
					 | 
				
			||||||
            .drop_foreign_key(
 | 
					 | 
				
			||||||
                ForeignKey::drop().table(Timer::Table).name("fk_timer_user").to_owned(),
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
            .await?;
 | 
					 | 
				
			||||||
        manager
 | 
					 | 
				
			||||||
            .drop_foreign_key(
 | 
					 | 
				
			||||||
                ForeignKey::drop().table(Timer::Table).name("fk_timer_guild").to_owned(),
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
            .await?;
 | 
					 | 
				
			||||||
        manager
 | 
					 | 
				
			||||||
            .drop_foreign_key(ForeignKey::drop().table(Todo::Table).name("fk_todo_user").to_owned())
 | 
					 | 
				
			||||||
            .await?;
 | 
					 | 
				
			||||||
        manager
 | 
					 | 
				
			||||||
            .drop_foreign_key(
 | 
					 | 
				
			||||||
                ForeignKey::drop().table(Todo::Table).name("fk_todo_guild").to_owned(),
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
            .await?;
 | 
					 | 
				
			||||||
        manager
 | 
					 | 
				
			||||||
            .drop_foreign_key(
 | 
					 | 
				
			||||||
                ForeignKey::drop().table(Todo::Table).name("fk_todo_channel").to_owned(),
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
            .await?;
 | 
					 | 
				
			||||||
        manager
 | 
					 | 
				
			||||||
            .drop_foreign_key(
 | 
					 | 
				
			||||||
                ForeignKey::drop()
 | 
					 | 
				
			||||||
                    .table(CommandMacro::Table)
 | 
					 | 
				
			||||||
                    .name("fk_command_macro_guild")
 | 
					 | 
				
			||||||
                    .to_owned(),
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
            .await?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        manager.drop_table(Table::drop().table(Guild::Table).to_owned()).await?;
 | 
					 | 
				
			||||||
        manager.drop_table(Table::drop().table(Channel::Table).to_owned()).await?;
 | 
					 | 
				
			||||||
        manager.drop_table(Table::drop().table(User::Table).to_owned()).await?;
 | 
					 | 
				
			||||||
        manager.drop_table(Table::drop().table(Reminder::Table).to_owned()).await?;
 | 
					 | 
				
			||||||
        manager.drop_table(Table::drop().table(ReminderTemplate::Table).to_owned()).await?;
 | 
					 | 
				
			||||||
        manager.drop_table(Table::drop().table(Timer::Table).to_owned()).await?;
 | 
					 | 
				
			||||||
        manager.drop_table(Table::drop().table(Todo::Table).to_owned()).await?;
 | 
					 | 
				
			||||||
        manager.drop_table(Table::drop().table(CommandMacro::Table).to_owned()).await?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        manager.drop_type(Type::drop().name(Timezone::Type).to_owned()).await?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        Ok(())
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,6 +0,0 @@
 | 
				
			|||||||
use sea_orm_migration::prelude::*;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[async_std::main]
 | 
					 | 
				
			||||||
async fn main() {
 | 
					 | 
				
			||||||
    cli::run_cli(migration::Migrator).await;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1 +0,0 @@
 | 
				
			|||||||
 | 
					 | 
				
			||||||
							
								
								
									
										53
									
								
								nginx/reminder-bot
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,53 @@
 | 
				
			|||||||
 | 
					server {
 | 
				
			||||||
 | 
					    server_name www.reminder-bot.com;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return 301 https://reminder-bot.com$request_uri;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					server {
 | 
				
			||||||
 | 
					    listen 80;
 | 
				
			||||||
 | 
					    server_name beta.reminder-bot.com;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return 301 https://reminder-bot.com$request_uri;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					server {
 | 
				
			||||||
 | 
					    listen 443 ssl;
 | 
				
			||||||
 | 
					    server_name beta.reminder-bot.com;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    ssl_certificate /etc/letsencrypt/live/beta.reminder-bot.com/fullchain.pem;
 | 
				
			||||||
 | 
					    ssl_certificate_key /etc/letsencrypt/live/beta.reminder-bot.com/privkey.pem;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return 301 https://reminder-bot.com$request_uri;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					server {
 | 
				
			||||||
 | 
					    listen 443 ssl;
 | 
				
			||||||
 | 
					    server_name reminder-bot.com;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    ssl_certificate /etc/letsencrypt/live/reminder-bot.com/fullchain.pem;
 | 
				
			||||||
 | 
					    ssl_certificate_key /etc/letsencrypt/live/reminder-bot.com/privkey.pem;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    access_log /var/log/nginx/access.log;
 | 
				
			||||||
 | 
					    error_log /var/log/nginx/error.log;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    proxy_buffer_size 128k;
 | 
				
			||||||
 | 
					    proxy_buffers 4 256k;
 | 
				
			||||||
 | 
					    proxy_busy_buffers_size 256k;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    client_max_body_size 10M;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    location / {
 | 
				
			||||||
 | 
					        proxy_pass http://localhost:18920;
 | 
				
			||||||
 | 
					        proxy_redirect off;
 | 
				
			||||||
 | 
					        proxy_set_header Host $host;
 | 
				
			||||||
 | 
					        proxy_set_header X-Real-IP $remote_addr;
 | 
				
			||||||
 | 
					        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
 | 
				
			||||||
 | 
					        proxy_set_header X-Forwarded-Proto $scheme;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    location /static {
 | 
				
			||||||
 | 
					        alias /var/www/reminder-rs/static;
 | 
				
			||||||
 | 
					        expires 30d;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,18 +0,0 @@
 | 
				
			|||||||
[package]
 | 
					 | 
				
			||||||
name = "postman"
 | 
					 | 
				
			||||||
version = "0.1.0"
 | 
					 | 
				
			||||||
edition = "2021"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
[dependencies]
 | 
					 | 
				
			||||||
tokio = { version = "1", features = ["process", "full"] }
 | 
					 | 
				
			||||||
regex = "1.4"
 | 
					 | 
				
			||||||
log = "0.4"
 | 
					 | 
				
			||||||
env_logger = "0.8"
 | 
					 | 
				
			||||||
chrono = "0.4"
 | 
					 | 
				
			||||||
chrono-tz = { version = "0.5", features = ["serde"] }
 | 
					 | 
				
			||||||
lazy_static = "1.4"
 | 
					 | 
				
			||||||
num-integer = "0.1"
 | 
					 | 
				
			||||||
serde = "1.0"
 | 
					 | 
				
			||||||
serde_json = "1.0"
 | 
					 | 
				
			||||||
sqlx = { version = "0.5", features = ["runtime-tokio-rustls", "macros", "mysql", "bigdecimal", "chrono", "json"]}
 | 
					 | 
				
			||||||
serenity = { version = "0.11.1", default-features = false, features = ["builder", "cache", "client", "gateway", "http", "model", "utils", "rustls_backend"] }
 | 
					 | 
				
			||||||
@@ -1,595 +0,0 @@
 | 
				
			|||||||
use chrono::Duration;
 | 
					 | 
				
			||||||
use chrono_tz::Tz;
 | 
					 | 
				
			||||||
use lazy_static::lazy_static;
 | 
					 | 
				
			||||||
use log::{error, info, warn};
 | 
					 | 
				
			||||||
use num_integer::Integer;
 | 
					 | 
				
			||||||
use regex::{Captures, Regex};
 | 
					 | 
				
			||||||
use serde::Deserialize;
 | 
					 | 
				
			||||||
use serenity::{
 | 
					 | 
				
			||||||
    builder::CreateEmbed,
 | 
					 | 
				
			||||||
    http::{CacheHttp, Http, HttpError, StatusCode},
 | 
					 | 
				
			||||||
    model::{
 | 
					 | 
				
			||||||
        channel::{Channel, Embed as SerenityEmbed},
 | 
					 | 
				
			||||||
        id::ChannelId,
 | 
					 | 
				
			||||||
        webhook::Webhook,
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    Error, Result,
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
use sqlx::{
 | 
					 | 
				
			||||||
    types::{
 | 
					 | 
				
			||||||
        chrono::{NaiveDateTime, Utc},
 | 
					 | 
				
			||||||
        Json,
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    Executor,
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use crate::Database;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
lazy_static! {
 | 
					 | 
				
			||||||
    pub static ref TIMEFROM_REGEX: Regex =
 | 
					 | 
				
			||||||
        Regex::new(r#"<<timefrom:(?P<time>\d+):(?P<format>.+)?>>"#).unwrap();
 | 
					 | 
				
			||||||
    pub static ref TIMENOW_REGEX: Regex =
 | 
					 | 
				
			||||||
        Regex::new(r#"<<timenow:(?P<timezone>(?:\w|/|_)+):(?P<format>.+)?>>"#).unwrap();
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
fn fmt_displacement(format: &str, seconds: u64) -> String {
 | 
					 | 
				
			||||||
    let mut seconds = seconds;
 | 
					 | 
				
			||||||
    let mut days: u64 = 0;
 | 
					 | 
				
			||||||
    let mut hours: u64 = 0;
 | 
					 | 
				
			||||||
    let mut minutes: u64 = 0;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    for (rep, time_type, div) in
 | 
					 | 
				
			||||||
        [("%d", &mut days, 86400), ("%h", &mut hours, 3600), ("%m", &mut minutes, 60)].iter_mut()
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        if format.contains(*rep) {
 | 
					 | 
				
			||||||
            let (divided, new_seconds) = seconds.div_rem(&div);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            **time_type = divided;
 | 
					 | 
				
			||||||
            seconds = new_seconds;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    format
 | 
					 | 
				
			||||||
        .replace("%s", &seconds.to_string())
 | 
					 | 
				
			||||||
        .replace("%m", &minutes.to_string())
 | 
					 | 
				
			||||||
        .replace("%h", &hours.to_string())
 | 
					 | 
				
			||||||
        .replace("%d", &days.to_string())
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
pub fn substitute(string: &str) -> String {
 | 
					 | 
				
			||||||
    let new = TIMEFROM_REGEX.replace(string, |caps: &Captures| {
 | 
					 | 
				
			||||||
        let final_time = caps.name("time").map(|m| m.as_str().parse::<i64>().ok()).flatten();
 | 
					 | 
				
			||||||
        let format = caps.name("format").map(|m| m.as_str());
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if let (Some(final_time), Some(format)) = (final_time, format) {
 | 
					 | 
				
			||||||
            let dt = NaiveDateTime::from_timestamp(final_time, 0);
 | 
					 | 
				
			||||||
            let now = Utc::now().naive_utc();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            let difference = {
 | 
					 | 
				
			||||||
                if now < dt {
 | 
					 | 
				
			||||||
                    dt - Utc::now().naive_utc()
 | 
					 | 
				
			||||||
                } else {
 | 
					 | 
				
			||||||
                    Utc::now().naive_utc() - dt
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            fmt_displacement(format, difference.num_seconds() as u64)
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
            String::new()
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    TIMENOW_REGEX
 | 
					 | 
				
			||||||
        .replace(&new, |caps: &Captures| {
 | 
					 | 
				
			||||||
            let timezone = caps.name("timezone").map(|m| m.as_str().parse::<Tz>().ok()).flatten();
 | 
					 | 
				
			||||||
            let format = caps.name("format").map(|m| m.as_str());
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if let (Some(timezone), Some(format)) = (timezone, format) {
 | 
					 | 
				
			||||||
                let now = Utc::now().with_timezone(&timezone);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                now.format(format).to_string()
 | 
					 | 
				
			||||||
            } else {
 | 
					 | 
				
			||||||
                String::new()
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        })
 | 
					 | 
				
			||||||
        .to_string()
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
struct Embed {
 | 
					 | 
				
			||||||
    title: String,
 | 
					 | 
				
			||||||
    description: String,
 | 
					 | 
				
			||||||
    image_url: Option<String>,
 | 
					 | 
				
			||||||
    thumbnail_url: Option<String>,
 | 
					 | 
				
			||||||
    footer: String,
 | 
					 | 
				
			||||||
    footer_url: Option<String>,
 | 
					 | 
				
			||||||
    author: String,
 | 
					 | 
				
			||||||
    author_url: Option<String>,
 | 
					 | 
				
			||||||
    color: u32,
 | 
					 | 
				
			||||||
    fields: Json<Vec<EmbedField>>,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Deserialize)]
 | 
					 | 
				
			||||||
struct EmbedField {
 | 
					 | 
				
			||||||
    title: String,
 | 
					 | 
				
			||||||
    value: String,
 | 
					 | 
				
			||||||
    inline: bool,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl Embed {
 | 
					 | 
				
			||||||
    pub async fn from_id(
 | 
					 | 
				
			||||||
        pool: impl Executor<'_, Database = Database> + Copy,
 | 
					 | 
				
			||||||
        id: u32,
 | 
					 | 
				
			||||||
    ) -> Option<Self> {
 | 
					 | 
				
			||||||
        match sqlx::query_as!(
 | 
					 | 
				
			||||||
            Self,
 | 
					 | 
				
			||||||
            r#"
 | 
					 | 
				
			||||||
            SELECT
 | 
					 | 
				
			||||||
             `embed_title` AS title,
 | 
					 | 
				
			||||||
             `embed_description` AS description,
 | 
					 | 
				
			||||||
             `embed_image_url` AS image_url,
 | 
					 | 
				
			||||||
             `embed_thumbnail_url` AS thumbnail_url,
 | 
					 | 
				
			||||||
             `embed_footer` AS footer,
 | 
					 | 
				
			||||||
             `embed_footer_url` AS footer_url,
 | 
					 | 
				
			||||||
             `embed_author` AS author,
 | 
					 | 
				
			||||||
             `embed_author_url` AS author_url,
 | 
					 | 
				
			||||||
             `embed_color` AS color,
 | 
					 | 
				
			||||||
             IFNULL(`embed_fields`, '[]') AS "fields:_"
 | 
					 | 
				
			||||||
            FROM reminders
 | 
					 | 
				
			||||||
            WHERE `id` = ?"#,
 | 
					 | 
				
			||||||
            id
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        .fetch_one(pool)
 | 
					 | 
				
			||||||
        .await
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            Ok(mut embed) => {
 | 
					 | 
				
			||||||
                embed.title = substitute(&embed.title);
 | 
					 | 
				
			||||||
                embed.description = substitute(&embed.description);
 | 
					 | 
				
			||||||
                embed.footer = substitute(&embed.footer);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                embed.fields.iter_mut().for_each(|mut field| {
 | 
					 | 
				
			||||||
                    field.title = substitute(&field.title);
 | 
					 | 
				
			||||||
                    field.value = substitute(&field.value);
 | 
					 | 
				
			||||||
                });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                if embed.has_content() {
 | 
					 | 
				
			||||||
                    Some(embed)
 | 
					 | 
				
			||||||
                } else {
 | 
					 | 
				
			||||||
                    None
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            Err(e) => {
 | 
					 | 
				
			||||||
                warn!("Error loading embed from reminder: {:?}", e);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                None
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    pub fn has_content(&self) -> bool {
 | 
					 | 
				
			||||||
        if self.title.is_empty()
 | 
					 | 
				
			||||||
            && self.description.is_empty()
 | 
					 | 
				
			||||||
            && self.image_url.is_none()
 | 
					 | 
				
			||||||
            && self.thumbnail_url.is_none()
 | 
					 | 
				
			||||||
            && self.footer.is_empty()
 | 
					 | 
				
			||||||
            && self.footer_url.is_none()
 | 
					 | 
				
			||||||
            && self.author.is_empty()
 | 
					 | 
				
			||||||
            && self.author_url.is_none()
 | 
					 | 
				
			||||||
            && self.fields.0.is_empty()
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            false
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
            true
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl Into<CreateEmbed> for Embed {
 | 
					 | 
				
			||||||
    fn into(self) -> CreateEmbed {
 | 
					 | 
				
			||||||
        let mut c = CreateEmbed::default();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        c.title(&self.title)
 | 
					 | 
				
			||||||
            .description(&self.description)
 | 
					 | 
				
			||||||
            .color(self.color)
 | 
					 | 
				
			||||||
            .author(|a| {
 | 
					 | 
				
			||||||
                a.name(&self.author);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                if let Some(author_icon) = &self.author_url {
 | 
					 | 
				
			||||||
                    a.icon_url(author_icon);
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                a
 | 
					 | 
				
			||||||
            })
 | 
					 | 
				
			||||||
            .footer(|f| {
 | 
					 | 
				
			||||||
                f.text(&self.footer);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                if let Some(footer_icon) = &self.footer_url {
 | 
					 | 
				
			||||||
                    f.icon_url(footer_icon);
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                f
 | 
					 | 
				
			||||||
            });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        for field in &self.fields.0 {
 | 
					 | 
				
			||||||
            c.field(&field.title, &field.value, field.inline);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if let Some(image_url) = &self.image_url {
 | 
					 | 
				
			||||||
            c.image(image_url);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if let Some(thumbnail_url) = &self.thumbnail_url {
 | 
					 | 
				
			||||||
            c.thumbnail(thumbnail_url);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        c
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Debug)]
 | 
					 | 
				
			||||||
pub struct Reminder {
 | 
					 | 
				
			||||||
    id: u32,
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    channel_id: u64,
 | 
					 | 
				
			||||||
    webhook_id: Option<u64>,
 | 
					 | 
				
			||||||
    webhook_token: Option<String>,
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    channel_paused: bool,
 | 
					 | 
				
			||||||
    channel_paused_until: Option<NaiveDateTime>,
 | 
					 | 
				
			||||||
    enabled: bool,
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    tts: bool,
 | 
					 | 
				
			||||||
    pin: bool,
 | 
					 | 
				
			||||||
    content: String,
 | 
					 | 
				
			||||||
    attachment: Option<Vec<u8>>,
 | 
					 | 
				
			||||||
    attachment_name: Option<String>,
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    utc_time: NaiveDateTime,
 | 
					 | 
				
			||||||
    timezone: String,
 | 
					 | 
				
			||||||
    restartable: bool,
 | 
					 | 
				
			||||||
    expires: Option<NaiveDateTime>,
 | 
					 | 
				
			||||||
    interval_seconds: Option<u32>,
 | 
					 | 
				
			||||||
    interval_months: Option<u32>,
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    avatar: Option<String>,
 | 
					 | 
				
			||||||
    username: Option<String>,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl Reminder {
 | 
					 | 
				
			||||||
    pub async fn fetch_reminders(pool: impl Executor<'_, Database = Database> + Copy) -> Vec<Self> {
 | 
					 | 
				
			||||||
        match sqlx::query_as_unchecked!(
 | 
					 | 
				
			||||||
            Reminder,
 | 
					 | 
				
			||||||
            r#"
 | 
					 | 
				
			||||||
SELECT
 | 
					 | 
				
			||||||
    reminders.`id` AS id,
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    channels.`channel` AS channel_id,
 | 
					 | 
				
			||||||
    channels.`webhook_id` AS webhook_id,
 | 
					 | 
				
			||||||
    channels.`webhook_token` AS webhook_token,
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    channels.`paused` AS 'channel_paused',
 | 
					 | 
				
			||||||
    channels.`paused_until` AS 'channel_paused_until',
 | 
					 | 
				
			||||||
    reminders.`enabled` AS 'enabled',
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    reminders.`tts` AS tts,
 | 
					 | 
				
			||||||
    reminders.`pin` AS pin,
 | 
					 | 
				
			||||||
    reminders.`content` AS content,
 | 
					 | 
				
			||||||
    reminders.`attachment` AS attachment,
 | 
					 | 
				
			||||||
    reminders.`attachment_name` AS attachment_name,
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    reminders.`utc_time` AS 'utc_time',
 | 
					 | 
				
			||||||
    reminders.`timezone` AS timezone,
 | 
					 | 
				
			||||||
    reminders.`restartable` AS restartable,
 | 
					 | 
				
			||||||
    reminders.`expires` AS 'expires',
 | 
					 | 
				
			||||||
    reminders.`interval_seconds` AS 'interval_seconds',
 | 
					 | 
				
			||||||
    reminders.`interval_months` AS 'interval_months',
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    reminders.`avatar` AS avatar,
 | 
					 | 
				
			||||||
    reminders.`username` AS username
 | 
					 | 
				
			||||||
FROM
 | 
					 | 
				
			||||||
    reminders
 | 
					 | 
				
			||||||
INNER JOIN
 | 
					 | 
				
			||||||
    channels
 | 
					 | 
				
			||||||
ON
 | 
					 | 
				
			||||||
    reminders.channel_id = channels.id
 | 
					 | 
				
			||||||
WHERE
 | 
					 | 
				
			||||||
    reminders.`utc_time` < NOW()
 | 
					 | 
				
			||||||
LIMIT 25
 | 
					 | 
				
			||||||
            "#,
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        .fetch_all(pool)
 | 
					 | 
				
			||||||
        .await
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            Ok(reminders) => reminders
 | 
					 | 
				
			||||||
                .into_iter()
 | 
					 | 
				
			||||||
                .map(|mut rem| {
 | 
					 | 
				
			||||||
                    rem.content = substitute(&rem.content);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    rem
 | 
					 | 
				
			||||||
                })
 | 
					 | 
				
			||||||
                .collect::<Vec<Self>>(),
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            Err(e) => {
 | 
					 | 
				
			||||||
                warn!("Could not fetch reminders: {:?}", e);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                vec![]
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    async fn reset_webhook(&self, pool: impl Executor<'_, Database = Database> + Copy) {
 | 
					 | 
				
			||||||
        let _ = sqlx::query!(
 | 
					 | 
				
			||||||
            "
 | 
					 | 
				
			||||||
UPDATE channels SET webhook_id = NULL, webhook_token = NULL WHERE channel = ?
 | 
					 | 
				
			||||||
            ",
 | 
					 | 
				
			||||||
            self.channel_id
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        .execute(pool)
 | 
					 | 
				
			||||||
        .await;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    async fn refresh(&self, pool: impl Executor<'_, Database = Database> + Copy) {
 | 
					 | 
				
			||||||
        if self.interval_seconds.is_some() || self.interval_months.is_some() {
 | 
					 | 
				
			||||||
            let now = Utc::now().naive_local();
 | 
					 | 
				
			||||||
            let mut updated_reminder_time = self.utc_time;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if let Some(interval) = self.interval_months {
 | 
					 | 
				
			||||||
                match sqlx::query!(
 | 
					 | 
				
			||||||
                    // use the second date_add to force return value to datetime
 | 
					 | 
				
			||||||
                    "SELECT DATE_ADD(DATE_ADD(?, INTERVAL ? MONTH), INTERVAL 0 SECOND) AS new_time",
 | 
					 | 
				
			||||||
                    updated_reminder_time,
 | 
					 | 
				
			||||||
                    interval
 | 
					 | 
				
			||||||
                )
 | 
					 | 
				
			||||||
                .fetch_one(pool)
 | 
					 | 
				
			||||||
                .await
 | 
					 | 
				
			||||||
                {
 | 
					 | 
				
			||||||
                    Ok(row) => match row.new_time {
 | 
					 | 
				
			||||||
                        Some(datetime) => {
 | 
					 | 
				
			||||||
                            updated_reminder_time = datetime;
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
                        None => {
 | 
					 | 
				
			||||||
                            warn!("Could not update interval by months: got NULL");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                            updated_reminder_time += Duration::days(30);
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
                    },
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    Err(e) => {
 | 
					 | 
				
			||||||
                        warn!("Could not update interval by months: {:?}", e);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                        // naively fallback to adding 30 days
 | 
					 | 
				
			||||||
                        updated_reminder_time += Duration::days(30);
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if let Some(interval) = self.interval_seconds {
 | 
					 | 
				
			||||||
                while updated_reminder_time < now {
 | 
					 | 
				
			||||||
                    updated_reminder_time += Duration::seconds(interval as i64);
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if self.expires.map_or(false, |expires| {
 | 
					 | 
				
			||||||
                NaiveDateTime::from_timestamp(updated_reminder_time.timestamp(), 0) > expires
 | 
					 | 
				
			||||||
            }) {
 | 
					 | 
				
			||||||
                self.force_delete(pool).await;
 | 
					 | 
				
			||||||
            } else {
 | 
					 | 
				
			||||||
                sqlx::query!(
 | 
					 | 
				
			||||||
                    "
 | 
					 | 
				
			||||||
UPDATE reminders SET `utc_time` = ? WHERE `id` = ?
 | 
					 | 
				
			||||||
                    ",
 | 
					 | 
				
			||||||
                    updated_reminder_time,
 | 
					 | 
				
			||||||
                    self.id
 | 
					 | 
				
			||||||
                )
 | 
					 | 
				
			||||||
                .execute(pool)
 | 
					 | 
				
			||||||
                .await
 | 
					 | 
				
			||||||
                .expect(&format!("Could not update time on Reminder {}", self.id));
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
            self.force_delete(pool).await;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    async fn force_delete(&self, pool: impl Executor<'_, Database = Database> + Copy) {
 | 
					 | 
				
			||||||
        sqlx::query!(
 | 
					 | 
				
			||||||
            "
 | 
					 | 
				
			||||||
DELETE FROM reminders WHERE `id` = ?
 | 
					 | 
				
			||||||
            ",
 | 
					 | 
				
			||||||
            self.id
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        .execute(pool)
 | 
					 | 
				
			||||||
        .await
 | 
					 | 
				
			||||||
        .expect(&format!("Could not delete Reminder {}", self.id));
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    async fn pin_message<M: Into<u64>>(&self, message_id: M, http: impl AsRef<Http>) {
 | 
					 | 
				
			||||||
        let _ = http.as_ref().pin_message(self.channel_id, message_id.into(), None).await;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    pub async fn send(
 | 
					 | 
				
			||||||
        &self,
 | 
					 | 
				
			||||||
        pool: impl Executor<'_, Database = Database> + Copy,
 | 
					 | 
				
			||||||
        cache_http: impl CacheHttp,
 | 
					 | 
				
			||||||
    ) {
 | 
					 | 
				
			||||||
        async fn send_to_channel(
 | 
					 | 
				
			||||||
            cache_http: impl CacheHttp,
 | 
					 | 
				
			||||||
            reminder: &Reminder,
 | 
					 | 
				
			||||||
            embed: Option<CreateEmbed>,
 | 
					 | 
				
			||||||
        ) -> Result<()> {
 | 
					 | 
				
			||||||
            let channel = ChannelId(reminder.channel_id).to_channel(&cache_http).await;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            match channel {
 | 
					 | 
				
			||||||
                Ok(Channel::Guild(channel)) => {
 | 
					 | 
				
			||||||
                    match channel
 | 
					 | 
				
			||||||
                        .send_message(&cache_http, |m| {
 | 
					 | 
				
			||||||
                            m.content(&reminder.content).tts(reminder.tts);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                            if let (Some(attachment), Some(name)) =
 | 
					 | 
				
			||||||
                                (&reminder.attachment, &reminder.attachment_name)
 | 
					 | 
				
			||||||
                            {
 | 
					 | 
				
			||||||
                                m.add_file((attachment as &[u8], name.as_str()));
 | 
					 | 
				
			||||||
                            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                            if let Some(embed) = embed {
 | 
					 | 
				
			||||||
                                m.set_embed(embed);
 | 
					 | 
				
			||||||
                            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                            m
 | 
					 | 
				
			||||||
                        })
 | 
					 | 
				
			||||||
                        .await
 | 
					 | 
				
			||||||
                    {
 | 
					 | 
				
			||||||
                        Ok(m) => {
 | 
					 | 
				
			||||||
                            if reminder.pin {
 | 
					 | 
				
			||||||
                                reminder.pin_message(m.id, cache_http.http()).await;
 | 
					 | 
				
			||||||
                            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                            Ok(())
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
                        Err(e) => Err(e),
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                Ok(Channel::Private(channel)) => {
 | 
					 | 
				
			||||||
                    match channel
 | 
					 | 
				
			||||||
                        .send_message(&cache_http.http(), |m| {
 | 
					 | 
				
			||||||
                            m.content(&reminder.content).tts(reminder.tts);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                            if let (Some(attachment), Some(name)) =
 | 
					 | 
				
			||||||
                                (&reminder.attachment, &reminder.attachment_name)
 | 
					 | 
				
			||||||
                            {
 | 
					 | 
				
			||||||
                                m.add_file((attachment as &[u8], name.as_str()));
 | 
					 | 
				
			||||||
                            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                            if let Some(embed) = embed {
 | 
					 | 
				
			||||||
                                m.set_embed(embed);
 | 
					 | 
				
			||||||
                            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                            m
 | 
					 | 
				
			||||||
                        })
 | 
					 | 
				
			||||||
                        .await
 | 
					 | 
				
			||||||
                    {
 | 
					 | 
				
			||||||
                        Ok(m) => {
 | 
					 | 
				
			||||||
                            if reminder.pin {
 | 
					 | 
				
			||||||
                                reminder.pin_message(m.id, cache_http.http()).await;
 | 
					 | 
				
			||||||
                            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                            Ok(())
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
                        Err(e) => Err(e),
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                Err(e) => Err(e),
 | 
					 | 
				
			||||||
                _ => Err(Error::Other("Channel not of valid type")),
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        async fn send_to_webhook(
 | 
					 | 
				
			||||||
            cache_http: impl CacheHttp,
 | 
					 | 
				
			||||||
            reminder: &Reminder,
 | 
					 | 
				
			||||||
            webhook: Webhook,
 | 
					 | 
				
			||||||
            embed: Option<CreateEmbed>,
 | 
					 | 
				
			||||||
        ) -> Result<()> {
 | 
					 | 
				
			||||||
            match webhook
 | 
					 | 
				
			||||||
                .execute(&cache_http.http(), reminder.pin || reminder.restartable, |w| {
 | 
					 | 
				
			||||||
                    w.content(&reminder.content).tts(reminder.tts);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    if let Some(username) = &reminder.username {
 | 
					 | 
				
			||||||
                        w.username(username);
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    if let Some(avatar) = &reminder.avatar {
 | 
					 | 
				
			||||||
                        w.avatar_url(avatar);
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    if let (Some(attachment), Some(name)) =
 | 
					 | 
				
			||||||
                        (&reminder.attachment, &reminder.attachment_name)
 | 
					 | 
				
			||||||
                    {
 | 
					 | 
				
			||||||
                        w.add_file((attachment as &[u8], name.as_str()));
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    if let Some(embed) = embed {
 | 
					 | 
				
			||||||
                        w.embeds(vec![SerenityEmbed::fake(|c| {
 | 
					 | 
				
			||||||
                            *c = embed;
 | 
					 | 
				
			||||||
                            c
 | 
					 | 
				
			||||||
                        })]);
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    w
 | 
					 | 
				
			||||||
                })
 | 
					 | 
				
			||||||
                .await
 | 
					 | 
				
			||||||
            {
 | 
					 | 
				
			||||||
                Ok(m) => {
 | 
					 | 
				
			||||||
                    if reminder.pin {
 | 
					 | 
				
			||||||
                        if let Some(message) = m {
 | 
					 | 
				
			||||||
                            reminder.pin_message(message.id, cache_http.http()).await;
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    Ok(())
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                Err(e) => Err(e),
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if self.enabled
 | 
					 | 
				
			||||||
            && !(self.channel_paused
 | 
					 | 
				
			||||||
                && self
 | 
					 | 
				
			||||||
                    .channel_paused_until
 | 
					 | 
				
			||||||
                    .map_or(true, |inner| inner >= Utc::now().naive_local()))
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            let _ = sqlx::query!(
 | 
					 | 
				
			||||||
                "
 | 
					 | 
				
			||||||
UPDATE `channels` SET paused = 0, paused_until = NULL WHERE `channel` = ?
 | 
					 | 
				
			||||||
                ",
 | 
					 | 
				
			||||||
                self.channel_id
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
            .execute(pool)
 | 
					 | 
				
			||||||
            .await;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            let embed = Embed::from_id(pool, self.id).await.map(|e| e.into());
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            let result = if let (Some(webhook_id), Some(webhook_token)) =
 | 
					 | 
				
			||||||
                (self.webhook_id, &self.webhook_token)
 | 
					 | 
				
			||||||
            {
 | 
					 | 
				
			||||||
                let webhook_res =
 | 
					 | 
				
			||||||
                    cache_http.http().get_webhook_with_token(webhook_id, webhook_token).await;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                if let Ok(webhook) = webhook_res {
 | 
					 | 
				
			||||||
                    send_to_webhook(cache_http, &self, webhook, embed).await
 | 
					 | 
				
			||||||
                } else {
 | 
					 | 
				
			||||||
                    warn!("Webhook vanished: {:?}", webhook_res);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    self.reset_webhook(pool).await;
 | 
					 | 
				
			||||||
                    send_to_channel(cache_http, &self, embed).await
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            } else {
 | 
					 | 
				
			||||||
                send_to_channel(cache_http, &self, embed).await
 | 
					 | 
				
			||||||
            };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if let Err(e) = result {
 | 
					 | 
				
			||||||
                error!("Error sending {:?}: {:?}", self, e);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                if let Error::Http(error) = e {
 | 
					 | 
				
			||||||
                    if error.status_code() == Some(StatusCode::NOT_FOUND) {
 | 
					 | 
				
			||||||
                        warn!("Seeing channel is deleted. Removing reminder");
 | 
					 | 
				
			||||||
                        self.force_delete(pool).await;
 | 
					 | 
				
			||||||
                    } else if let HttpError::UnsuccessfulRequest(error) = *error {
 | 
					 | 
				
			||||||
                        if error.error.code == 50007 {
 | 
					 | 
				
			||||||
                            warn!("User cannot receive DMs");
 | 
					 | 
				
			||||||
                            self.force_delete(pool).await;
 | 
					 | 
				
			||||||
                        } else {
 | 
					 | 
				
			||||||
                            self.refresh(pool).await;
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                } else {
 | 
					 | 
				
			||||||
                    self.refresh(pool).await;
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            } else {
 | 
					 | 
				
			||||||
                self.refresh(pool).await;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
            info!("Reminder {} is paused", self.id);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            self.refresh(pool).await;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
							
								
								
									
										11
									
								
								recordable_derive/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,11 @@
 | 
				
			|||||||
 | 
					[package]
 | 
				
			||||||
 | 
					name = "recordable_derive"
 | 
				
			||||||
 | 
					version = "0.1.0"
 | 
				
			||||||
 | 
					edition = "2021"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[lib]
 | 
				
			||||||
 | 
					proc-macro = true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[dependencies]
 | 
				
			||||||
 | 
					quote = "1.0.35"
 | 
				
			||||||
 | 
					syn = { version = "2.0.49", features = ["full"] }
 | 
				
			||||||
							
								
								
									
										42
									
								
								recordable_derive/src/lib.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,42 @@
 | 
				
			|||||||
 | 
					use proc_macro::TokenStream;
 | 
				
			||||||
 | 
					use syn::{spanned::Spanned, Data};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Macro to allow Recordable to be implemented on an enum by dispatching over each variant.
 | 
				
			||||||
 | 
					#[proc_macro_derive(Recordable)]
 | 
				
			||||||
 | 
					pub fn extract(input: TokenStream) -> TokenStream {
 | 
				
			||||||
 | 
					    let ast = syn::parse_macro_input!(input);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    impl_recordable(&ast)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fn impl_recordable(ast: &syn::DeriveInput) -> TokenStream {
 | 
				
			||||||
 | 
					    let name = &ast.ident;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    match &ast.data {
 | 
				
			||||||
 | 
					        Data::Enum(en) => {
 | 
				
			||||||
 | 
					            let extracted = en.variants.iter().map(|var| {
 | 
				
			||||||
 | 
					                let ident = &var.ident;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                quote::quote_spanned! {var.span()=>
 | 
				
			||||||
 | 
					                    Self::#ident (opt) => opt.run(ctx).await?
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            TokenStream::from(quote::quote! {
 | 
				
			||||||
 | 
					                impl Recordable for #name {
 | 
				
			||||||
 | 
					                    async fn run(self, ctx: crate::Context<'_>) -> Result<(), crate::Error> {
 | 
				
			||||||
 | 
					                        match self {
 | 
				
			||||||
 | 
					                            #(#extracted,)*
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        Ok(())
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        _ => {
 | 
				
			||||||
 | 
					            panic!("Only enums can derive Recordable");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										2
									
								
								reminder-dashboard/.prettierrc.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,2 @@
 | 
				
			|||||||
 | 
					printWidth = 100
 | 
				
			||||||
 | 
					tabWidth = 4
 | 
				
			||||||
							
								
								
									
										19
									
								
								reminder-dashboard/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,19 @@
 | 
				
			|||||||
 | 
					# reminder-dashboard
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					The re-re-rewrite of the dashboard.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Why
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					The existing beta variant of the dashboard is written using vanilla JavaScript. This is fine,
 | 
				
			||||||
 | 
					but annoying to update. This would've been okay if I was more dedicated to updating the vanilla
 | 
				
			||||||
 | 
					JavaScript too, but I want to experiment with "new" things.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					This also allows me to expand my frontend skills, which is relevant to part of my job.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Developing
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					1. Run both `npm run dev` and `cargo run`
 | 
				
			||||||
 | 
					2. Symlink assets: assuming cloned
 | 
				
			||||||
 | 
					   into `$HOME`, `ln -s $HOME/reminder-bot/reminder-dashboard/dist/index.html $HOME/reminder-bot/web/static/index.html`
 | 
				
			||||||
 | 
					   and
 | 
				
			||||||
 | 
					   `ln -s $HOME/reminder-bot/reminder-dashboard/dist/static/assets $HOME/reminder-bot/web/static/assets`
 | 
				
			||||||
							
								
								
									
										34
									
								
								reminder-dashboard/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,34 @@
 | 
				
			|||||||
 | 
					<!DOCTYPE html>
 | 
				
			||||||
 | 
					<html lang="EN">
 | 
				
			||||||
 | 
					<head>
 | 
				
			||||||
 | 
						<meta name="description" content="The most powerful Discord Reminders Bot">
 | 
				
			||||||
 | 
						<meta name="viewport" content="width=device-width, initial-scale=1.0">
 | 
				
			||||||
 | 
						<meta charset="UTF-8">
 | 
				
			||||||
 | 
						<meta name="yandex-verification" content="bb77b8681eb64a90"/>
 | 
				
			||||||
 | 
						<meta name="google-site-verification" content="7h7UVTeEe0AOzHiH3cFtsqMULYGN-zCZdMT_YCkW1Ho"/>
 | 
				
			||||||
 | 
						<!-- <meta http-equiv="Content-Security-Policy" content="default-src 'self'; img-src *; font-src fonts.gstatic.com 'self'"> -->
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						<!-- favicon -->
 | 
				
			||||||
 | 
						<link rel="apple-touch-icon" sizes="180x180"
 | 
				
			||||||
 | 
							  href="/static/favicon/apple-touch-icon.png">
 | 
				
			||||||
 | 
						<link rel="icon" type="image/png" sizes="32x32"
 | 
				
			||||||
 | 
							  href="/static/favicon/favicon-32x32.png">
 | 
				
			||||||
 | 
						<link rel="icon" type="image/png" sizes="16x16"
 | 
				
			||||||
 | 
							  href="/static/favicon/favicon-16x16.png">
 | 
				
			||||||
 | 
						<link rel="manifest" href="/static/site.webmanifest">
 | 
				
			||||||
 | 
						<meta name="msapplication-TileColor" content="#da532c">
 | 
				
			||||||
 | 
						<meta name="theme-color" content="#ffffff">
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						<title>Reminder Bot | Dashboard</title>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						<!-- styles -->
 | 
				
			||||||
 | 
						<link rel="stylesheet" href="/static/css/bulma.min.css">
 | 
				
			||||||
 | 
						<link rel="stylesheet" href="/static/css/fa.css">
 | 
				
			||||||
 | 
						<link rel="stylesheet" href="/static/css/font.css">
 | 
				
			||||||
 | 
						<link rel="stylesheet" href="/static/css/style.css">
 | 
				
			||||||
 | 
					</head>
 | 
				
			||||||
 | 
					<body>
 | 
				
			||||||
 | 
						<div id="app"></div>
 | 
				
			||||||
 | 
						<script type="module" src="/src/index.tsx"></script>
 | 
				
			||||||
 | 
					</body>
 | 
				
			||||||
 | 
					</html>
 | 
				
			||||||
							
								
								
									
										5626
									
								
								reminder-dashboard/package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										32
									
								
								reminder-dashboard/package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,32 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
						"name": "example",
 | 
				
			||||||
 | 
						"private": true,
 | 
				
			||||||
 | 
						"type": "module",
 | 
				
			||||||
 | 
						"scripts": {
 | 
				
			||||||
 | 
							"dev": "vite build --watch --mode development",
 | 
				
			||||||
 | 
							"build": "vite build",
 | 
				
			||||||
 | 
							"preview": "vite preview"
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						"dependencies": {
 | 
				
			||||||
 | 
							"axios": "^1.5.1",
 | 
				
			||||||
 | 
							"bulma": "^0.9.4",
 | 
				
			||||||
 | 
							"luxon": "^3.4.3",
 | 
				
			||||||
 | 
							"preact": "^10.13.1",
 | 
				
			||||||
 | 
							"react-colorful": "^5.6.1",
 | 
				
			||||||
 | 
							"react-query": "^3.39.3",
 | 
				
			||||||
 | 
							"tributejs": "^5.1.3",
 | 
				
			||||||
 | 
							"use-debounce": "^10.0.0",
 | 
				
			||||||
 | 
							"wouter": "^3.0"
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						"devDependencies": {
 | 
				
			||||||
 | 
							"@preact/preset-vite": "^2.5.0",
 | 
				
			||||||
 | 
							"@types/luxon": "^3.3.2",
 | 
				
			||||||
 | 
							"eslint": "^8.50.0",
 | 
				
			||||||
 | 
							"eslint-config-preact": "^1.3.0",
 | 
				
			||||||
 | 
							"prettier": "^3.0.3",
 | 
				
			||||||
 | 
							"react-datepicker": "^4.21.0",
 | 
				
			||||||
 | 
							"sass": "^1.71.1",
 | 
				
			||||||
 | 
							"typescript": "^5.2.2",
 | 
				
			||||||
 | 
							"vite": "^5.1"
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -11,10 +11,26 @@ div.reminderContent.is-collapsed .column.discord-frame {
 | 
				
			|||||||
    display: none;
 | 
					    display: none;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
div.reminderContent.is-collapsed .collapses {
 | 
					div.reminderContent.is-collapsed .column.settings {
 | 
				
			||||||
    display: none;
 | 
					    display: none;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					div.reminderContent.is-collapsed .reminder-settings {
 | 
				
			||||||
 | 
					    margin-bottom: 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					div.reminderContent.is-collapsed .button-row {
 | 
				
			||||||
 | 
					    display: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					div.reminderContent.is-collapsed .button-row-edit {
 | 
				
			||||||
 | 
					    display: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					div.reminderContent.is-collapsed .reminder-topbar {
 | 
				
			||||||
 | 
					    padding-bottom: 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
div.reminderContent.is-collapsed .invert-collapses {
 | 
					div.reminderContent.is-collapsed .invert-collapses {
 | 
				
			||||||
    display: inline-flex;
 | 
					    display: inline-flex;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -23,42 +39,42 @@ div.reminderContent .invert-collapses {
 | 
				
			|||||||
    display: none;
 | 
					    display: none;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
div.reminderContent.is-collapsed .settings {
 | 
					 | 
				
			||||||
    display: flex;
 | 
					 | 
				
			||||||
    flex-direction: row;
 | 
					 | 
				
			||||||
    padding-bottom: 0;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
div.reminderContent.is-collapsed .channel-field {
 | 
					 | 
				
			||||||
    display: inline-flex;
 | 
					 | 
				
			||||||
    order: 1;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
div.reminderContent.is-collapsed .reminder-topbar {
 | 
					 | 
				
			||||||
    display: inline-flex;
 | 
					 | 
				
			||||||
    margin-bottom: 0px;
 | 
					 | 
				
			||||||
    flex-grow: 1;
 | 
					 | 
				
			||||||
    order: 2;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
div.reminderContent.is-collapsed input[name="name"] {
 | 
					div.reminderContent.is-collapsed input[name="name"] {
 | 
				
			||||||
    display: inline-flex;
 | 
					    display: inline-flex;
 | 
				
			||||||
    flex-grow: 1;
 | 
					    flex-grow: 1;
 | 
				
			||||||
    border: none;
 | 
					    border: none;
 | 
				
			||||||
    font-weight: 700;
 | 
					 | 
				
			||||||
    background: none;
 | 
					    background: none;
 | 
				
			||||||
 | 
					    box-shadow: none;
 | 
				
			||||||
 | 
					    opacity: 1;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
div.reminderContent.is-collapsed button.hide-box {
 | 
					div.reminderContent.is-collapsed .hide-box {
 | 
				
			||||||
    display: inline-flex;
 | 
					    display: inline-flex;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
div.reminderContent.is-collapsed button.hide-box i {
 | 
					div.reminderContent.is-collapsed .hide-box i {
 | 
				
			||||||
    transform: rotate(90deg);
 | 
					    transform: rotate(90deg);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
/* END */
 | 
					/* END */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/* dashboard styles */
 | 
					/* dashboard styles */
 | 
				
			||||||
 | 
					.hide-box {
 | 
				
			||||||
 | 
					    border: none;
 | 
				
			||||||
 | 
					    background: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.hide-box:focus {
 | 
				
			||||||
 | 
					    outline: none;
 | 
				
			||||||
 | 
					    box-shadow: none !important;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.channel-bar {
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    justify-content: center;
 | 
				
			||||||
 | 
					    flex-direction: column;
 | 
				
			||||||
 | 
					    font-weight: bold;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
button.inline-btn {
 | 
					button.inline-btn {
 | 
				
			||||||
    height: 100%;
 | 
					    height: 100%;
 | 
				
			||||||
    padding: 5px;
 | 
					    padding: 5px;
 | 
				
			||||||
@@ -85,18 +101,86 @@ div.discord-embed {
 | 
				
			|||||||
    position: relative;
 | 
					    position: relative;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
div.reminderContent {
 | 
					div.split-controls {
 | 
				
			||||||
    padding: 2px;
 | 
					    display: flex;
 | 
				
			||||||
    background-color: #f5f5f5;
 | 
					    flex-direction: column;
 | 
				
			||||||
    border-radius: 8px;
 | 
					    justify-content: space-between;
 | 
				
			||||||
    margin: 8px;
 | 
					    flex-grow: 2;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
div.interval-group > button {
 | 
					.reminder-topbar > div {
 | 
				
			||||||
    margin-left: auto;
 | 
					    padding-left: 6px;
 | 
				
			||||||
 | 
					    padding-right: 6px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.settings {
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    flex-direction: column;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.name-bar {
 | 
				
			||||||
 | 
					    flex-grow: 1;
 | 
				
			||||||
 | 
					    flex-shrink: 1;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.hide-button-bar {
 | 
				
			||||||
 | 
					    flex-grow: 0;
 | 
				
			||||||
 | 
					    flex-shrink: 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.patreon-only {
 | 
				
			||||||
 | 
					    padding-bottom: 16px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.tts-row {
 | 
				
			||||||
 | 
					    padding-bottom: 10px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.reminder-topbar {
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    margin-bottom: 0 !important;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.reminder-settings {
 | 
				
			||||||
 | 
					    margin-top: 0 !important;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.reminder-settings > .column {
 | 
				
			||||||
 | 
					    flex-grow: 0;
 | 
				
			||||||
 | 
					    flex-shrink: 0;
 | 
				
			||||||
 | 
					    flex-basis: 50%;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					div.reminderContent {
 | 
				
			||||||
 | 
					    margin-top: 10px;
 | 
				
			||||||
 | 
					    margin-bottom: 10px;
 | 
				
			||||||
 | 
					    padding: 14px;
 | 
				
			||||||
 | 
					    background-color: #f5f5f5;
 | 
				
			||||||
 | 
					    border-radius: 8px;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/* Interval inputs */
 | 
					/* Interval inputs */
 | 
				
			||||||
 | 
					div.interval-group {
 | 
				
			||||||
 | 
					    height: unset !important;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					div.interval-group .clear:focus {
 | 
				
			||||||
 | 
					    outline: none;
 | 
				
			||||||
 | 
					    box-shadow: none !important;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					div.interval-group .no-break {
 | 
				
			||||||
 | 
					    text-wrap: avoid;
 | 
				
			||||||
 | 
					    white-space: nowrap;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					div.interval-group .clear {
 | 
				
			||||||
 | 
					    border: none;
 | 
				
			||||||
 | 
					    background: none;
 | 
				
			||||||
 | 
					    padding: 1px;
 | 
				
			||||||
 | 
					    margin-right: -3px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
div.interval-group > .interval-group-left input {
 | 
					div.interval-group > .interval-group-left input {
 | 
				
			||||||
    -webkit-appearance: none;
 | 
					    -webkit-appearance: none;
 | 
				
			||||||
    border-style: none;
 | 
					    border-style: none;
 | 
				
			||||||
@@ -110,12 +194,13 @@ div.interval-group > .interval-group-left input.w2 {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
div.interval-group > .interval-group-left input.w3 {
 | 
					div.interval-group > .interval-group-left input.w3 {
 | 
				
			||||||
    width: 6ch;
 | 
					    width: 3ch;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
div.interval-group {
 | 
					div.interval-group {
 | 
				
			||||||
    display: flex;
 | 
					    display: flex;
 | 
				
			||||||
    flex-direction: row;
 | 
					    flex-direction: row;
 | 
				
			||||||
 | 
					    justify-content: space-between;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
/* !Interval inputs */
 | 
					/* !Interval inputs */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -180,6 +265,23 @@ div#pageNavbar a {
 | 
				
			|||||||
    text-align: center;
 | 
					    text-align: center;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.navbar-burger {
 | 
				
			||||||
 | 
					    flex-shrink: 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.navbar-item.pageTitle {
 | 
				
			||||||
 | 
					    flex-shrink: 1;
 | 
				
			||||||
 | 
					    white-space: nowrap;
 | 
				
			||||||
 | 
					    overflow: hidden;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.dashboard-burger, .dashboard-burger:active, .dashboard-burger.is-active {
 | 
				
			||||||
 | 
					    background-color: #adc99c !important;
 | 
				
			||||||
 | 
					    border-radius: 14px;
 | 
				
			||||||
 | 
					    padding: 6px;
 | 
				
			||||||
 | 
					    background-clip: content-box;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
div#pageNavbar a:hover {
 | 
					div#pageNavbar a:hover {
 | 
				
			||||||
    background-color: #4a4a4a;
 | 
					    background-color: #4a4a4a;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -206,17 +308,24 @@ div.dashboard-sidebar {
 | 
				
			|||||||
    padding-right: 0;
 | 
					    padding-right: 0;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
div.dashboard-sidebar:not(.mobile-sidebar) {
 | 
					ul.guildList {
 | 
				
			||||||
    display: flex;
 | 
					    flex-grow: 1;
 | 
				
			||||||
    flex-direction: column;
 | 
					    flex-shrink: 1;
 | 
				
			||||||
 | 
					    overflow: auto;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
div.dashboard-sidebar:not(.mobile-sidebar) .aside-footer {
 | 
					div.dashboard-sidebar:not(.mobile-sidebar) .aside-footer {
 | 
				
			||||||
 | 
					    flex-shrink: 0;
 | 
				
			||||||
 | 
					    flex-grow: 0;
 | 
				
			||||||
    position: fixed;
 | 
					    position: fixed;
 | 
				
			||||||
    bottom: 0;
 | 
					    bottom: 0;
 | 
				
			||||||
    width: 226px;
 | 
					    width: 226px;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					div.dashboard-sidebar svg {
 | 
				
			||||||
 | 
					    flex-shrink: 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
div.mobile-sidebar {
 | 
					div.mobile-sidebar {
 | 
				
			||||||
    z-index: 100;
 | 
					    z-index: 100;
 | 
				
			||||||
    min-height: 100vh;
 | 
					    min-height: 100vh;
 | 
				
			||||||
@@ -293,10 +402,7 @@ input.default-width {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.message-input:placeholder-shown {
 | 
					.message-input:placeholder-shown {
 | 
				
			||||||
    border-top: none;
 | 
					    font-style: italic;
 | 
				
			||||||
    border-left: none;
 | 
					 | 
				
			||||||
    border-right: none;
 | 
					 | 
				
			||||||
    border-bottom-style: dashed;
 | 
					 | 
				
			||||||
    background-color: #40444b;
 | 
					    background-color: #40444b;
 | 
				
			||||||
    color: #fff;
 | 
					    color: #fff;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -367,8 +473,7 @@ input.default-width {
 | 
				
			|||||||
.customizable.is-400x300 img {
 | 
					.customizable.is-400x300 img {
 | 
				
			||||||
    margin-top: 10px;
 | 
					    margin-top: 10px;
 | 
				
			||||||
    width: 100%;
 | 
					    width: 100%;
 | 
				
			||||||
    min-height: 100px;
 | 
					    height: 100px;
 | 
				
			||||||
    max-height: 400px;
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.customizable.is-32x32 img {
 | 
					.customizable.is-32x32 img {
 | 
				
			||||||
@@ -462,6 +567,7 @@ input.default-width {
 | 
				
			|||||||
    flex-grow: 1;
 | 
					    flex-grow: 1;
 | 
				
			||||||
    flex-shrink: 1;
 | 
					    flex-shrink: 1;
 | 
				
			||||||
    flex-basis: auto;
 | 
					    flex-basis: auto;
 | 
				
			||||||
 | 
					    margin-right: 4px;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.embed-body input, .embed-body textarea {
 | 
					.embed-body input, .embed-body textarea {
 | 
				
			||||||
@@ -511,21 +617,84 @@ input.default-width {
 | 
				
			|||||||
    border-bottom: 1px solid #fff;
 | 
					    border-bottom: 1px solid #fff;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@media only screen and (max-width: 768px) {
 | 
					.channel-selector {
 | 
				
			||||||
 | 
					    width: 100%;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.select {
 | 
				
			||||||
 | 
					    width: 100%;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					li.highlight {
 | 
				
			||||||
 | 
					    margin-bottom: 0 !important;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.button-row {
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.button-row .button-row-reminder {
 | 
				
			||||||
 | 
					    flex-grow: 0;
 | 
				
			||||||
 | 
					    padding: 2px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.button-row-template {
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    flex-grow: 1;
 | 
				
			||||||
 | 
					    justify-content: space-between;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.button-row .button-row-template > div {
 | 
				
			||||||
 | 
					    padding: 2px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@media only screen and (max-width: 1023px) {
 | 
				
			||||||
 | 
					    p.title.pageTitle {
 | 
				
			||||||
 | 
					        display: none;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .dashboard-frame {
 | 
				
			||||||
 | 
					        margin-top: 4rem !important;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    .customizable.thumbnail img {
 | 
					    .customizable.thumbnail img {
 | 
				
			||||||
        width: 60px;
 | 
					        width: 60px;
 | 
				
			||||||
        height: 60px;
 | 
					        height: 60px;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    .customizable.is-24x24 img {
 | 
					@media only screen and (max-width: 768px) {
 | 
				
			||||||
        width: 16px;
 | 
					    .button-row {
 | 
				
			||||||
        height: 16px;
 | 
					        display: flex;
 | 
				
			||||||
 | 
					        flex-direction: column;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .button-row .button-row-reminder {
 | 
				
			||||||
 | 
					        width: 100%;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .button-row .button-row-template > div {
 | 
				
			||||||
 | 
					        flex-basis: 0;
 | 
				
			||||||
 | 
					        flex-grow: 1;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .button-row button {
 | 
				
			||||||
 | 
					        width: 100%;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .reminder-settings {
 | 
				
			||||||
 | 
					        margin-bottom: 0 !important;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .tts-row {
 | 
				
			||||||
 | 
					        padding-bottom: 0;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/* loader */
 | 
					/* loader */
 | 
				
			||||||
#loader {
 | 
					#loader {
 | 
				
			||||||
    position: fixed;
 | 
					    position: fixed;
 | 
				
			||||||
 | 
					    top: 0;
 | 
				
			||||||
    background-color: rgba(255, 255, 255, 0.8);
 | 
					    background-color: rgba(255, 255, 255, 0.8);
 | 
				
			||||||
    width: 100vw;
 | 
					    width: 100vw;
 | 
				
			||||||
    z-index: 999;
 | 
					    z-index: 999;
 | 
				
			||||||
@@ -537,6 +706,86 @@ input.default-width {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
/* END */
 | 
					/* END */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					div.reminderError {
 | 
				
			||||||
 | 
					    margin: 10px;
 | 
				
			||||||
 | 
					    padding: 14px;
 | 
				
			||||||
 | 
					    background-color: #f5f5f5;
 | 
				
			||||||
 | 
					    border-radius: 8px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					div.reminderError .errorHead {
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    flex-direction: row;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					div.reminderError .errorIcon {
 | 
				
			||||||
 | 
					    padding: 8px;
 | 
				
			||||||
 | 
					    border-radius: 4px;
 | 
				
			||||||
 | 
					    margin-right: 12px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					div.reminderError .errorIcon .fas {
 | 
				
			||||||
 | 
					    display: none
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					div.reminderError[data-case="deleted"] .errorIcon {
 | 
				
			||||||
 | 
					    background-color: #e7e5e4;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					div.reminderError[data-case="failed"] .errorIcon {
 | 
				
			||||||
 | 
					    background-color: #fecaca;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					div.reminderError[data-case="sent"] .errorIcon {
 | 
				
			||||||
 | 
					    background-color: #d9f99d;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					div.reminderError[data-case="deleted"] .errorIcon .fas.fa-trash {
 | 
				
			||||||
 | 
					    display: block;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					div.reminderError[data-case="failed"] .errorIcon .fas.fa-exclamation-triangle {
 | 
				
			||||||
 | 
					    display: block;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					div.reminderError[data-case="sent"] .errorIcon .fas.fa-check {
 | 
				
			||||||
 | 
					    display: block;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					div.reminderError .errorHead .reminderName {
 | 
				
			||||||
 | 
					    font-size: 1rem;
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    flex-direction: column;
 | 
				
			||||||
 | 
					    justify-content: center;
 | 
				
			||||||
 | 
					    color: rgb(54, 54, 54);
 | 
				
			||||||
 | 
					    flex-grow: 1;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					div.reminderError .errorHead .reminderTime {
 | 
				
			||||||
 | 
					    font-size: 1rem;
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    flex-direction: column;
 | 
				
			||||||
 | 
					    flex-shrink: 1;
 | 
				
			||||||
 | 
					    justify-content: center;
 | 
				
			||||||
 | 
					    color: rgb(54, 54, 54);
 | 
				
			||||||
 | 
					    background-color: #ffffff;
 | 
				
			||||||
 | 
					    padding: 8px;
 | 
				
			||||||
 | 
					    border-radius: 4px;
 | 
				
			||||||
 | 
					    border-color: #e5e5e5;
 | 
				
			||||||
 | 
					    border-width: 1px;
 | 
				
			||||||
 | 
					    border-style: solid;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					div.reminderError .reminderMessage {
 | 
				
			||||||
 | 
					    font-size: 1rem;
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    flex-direction: column;
 | 
				
			||||||
 | 
					    justify-content: center;
 | 
				
			||||||
 | 
					    color: rgb(54, 54, 54);
 | 
				
			||||||
 | 
					    flex-grow: 1;
 | 
				
			||||||
 | 
					    font-style: italic;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/* other stuff */
 | 
					/* other stuff */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.half-rem {
 | 
					.half-rem {
 | 
				
			||||||
@@ -568,11 +817,44 @@ input.default-width {
 | 
				
			|||||||
    background-color: white;
 | 
					    background-color: white;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					a.switch-pane {
 | 
				
			||||||
 | 
					    white-space: nowrap;
 | 
				
			||||||
 | 
					    overflow: hidden;
 | 
				
			||||||
 | 
					    text-overflow: ellipsis;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.guild-submenu {
 | 
				
			||||||
 | 
					    display: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.guild-submenu li {
 | 
				
			||||||
 | 
					    font-size: 0.8rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					a.switch-pane.is-active ~ .guild-submenu {
 | 
				
			||||||
 | 
					    display: block;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.feedback {
 | 
				
			||||||
 | 
					    background-color: #5865F2;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.is-locked {
 | 
					.is-locked {
 | 
				
			||||||
    pointer-events: none;
 | 
					    pointer-events: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.is-locked > :not(.patreon-invert) {
 | 
				
			||||||
    opacity: 0.4;
 | 
					    opacity: 0.4;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.is-locked .patreon-invert {
 | 
				
			||||||
 | 
					    display: block;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.patreon-invert {
 | 
				
			||||||
 | 
					    display: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.is-locked .foreground {
 | 
					.is-locked .foreground {
 | 
				
			||||||
    pointer-events: auto;
 | 
					    pointer-events: auto;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -580,3 +862,27 @@ input.default-width {
 | 
				
			|||||||
.is-locked .field:last-of-type {
 | 
					.is-locked .field:last-of-type {
 | 
				
			||||||
    display: none;
 | 
					    display: none;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.stat-row {
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    flex-direction: row;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.stat-box {
 | 
				
			||||||
 | 
					    flex-grow: 1;
 | 
				
			||||||
 | 
					    border-radius: 6px;
 | 
				
			||||||
 | 
					    background-color: #fcfcfc;
 | 
				
			||||||
 | 
					    border-color: #efefef;
 | 
				
			||||||
 | 
					    border-style: solid;
 | 
				
			||||||
 | 
					    border-width: 1px;
 | 
				
			||||||
 | 
					    margin: 4px;
 | 
				
			||||||
 | 
					    padding: 4px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.figure {
 | 
				
			||||||
 | 
					    text-align: center;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.figure-num {
 | 
				
			||||||
 | 
					    font-size: 2rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
		 Before Width: | Height: | Size: 7.2 KiB After Width: | Height: | Size: 7.2 KiB  | 
| 
		 Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB  | 
| 
		 Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 6.8 KiB  | 
| 
		 Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB  | 
| 
		 Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB  | 
| 
		 Before Width: | Height: | Size: 7.2 KiB After Width: | Height: | Size: 7.2 KiB  | 
| 
		 Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 4.9 KiB  | 
| 
		 Before Width: | Height: | Size: 762 B After Width: | Height: | Size: 762 B  | 
| 
		 Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB  | 
| 
		 Before Width: | Height: | Size: 323 KiB After Width: | Height: | Size: 323 KiB  | 
| 
		 Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 61 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								reminder-dashboard/public/static/img/logo_nobg.webp
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 81 KiB  | 
| 
		 Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB  | 
| 
		 Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB  | 
| 
		 Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 55 KiB  | 
| 
		 Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.6 KiB  | 
| 
		 Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB  | 
| 
		 Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB  | 
| 
		 Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB  | 
| 
		 Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB  |