Compare commits
	
		
			377 Commits
		
	
	
		
			discord-ti
			...
			jude/remov
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					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 | ||
| 
						 | 
					6f7d0f67b3 | ||
| 
						 | 
					bfc2d71ca0 | ||
| 
						 | 
					8eb46f1f23 | ||
| 
						 | 
					c4087bf569 | ||
| 
						 | 
					f25cfed8d7 | ||
| 
						 | 
					d2a8bd1982 | ||
| 
						 | 
					437ee6b446 | ||
| 
						 | 
					7d43aa5918 | ||
| 
						 | 
					8bad95510d | ||
| 
						 | 
					d7a0b727fb | ||
| 
						 | 
					1c1f5662d3 | ||
| ded750aa2d | |||
| 4c4f0927f1 | |||
| 
						 | 
					0f05018cab | ||
| 
						 | 
					85d27c5bba | ||
| 
						 | 
					d946ef1dca | ||
| 
						 | 
					f21d522435 | ||
| 
						 | 
					3add718cdf | ||
| 
						 | 
					f4ef7afea0 | ||
| 
						 | 
					f8547bba70 | ||
| 
						 | 
					08fd88ce54 | ||
| 
						 | 
					abfe492192 | ||
| 
						 | 
					afb2fbe4ff | ||
| 
						 | 
					878ea11502 | ||
| 
						 | 
					93da746bdc | ||
| 
						 | 
					9e6a387f82 | ||
| 
						 | 
					af9d8bea62 | ||
| 
						 | 
					318be1fa5e | ||
| 
						 | 
					3b6e02e16e | ||
| 
						 | 
					a56f84f659 | ||
| 
						 | 
					3e4dd0fa48 | ||
| 
						 | 
					d0d2d50966 | ||
| 
						 | 
					e2e5b022a0 | ||
| 
						 | 
					6ae2353c92 | ||
| 
						 | 
					06c4deeaa9 | ||
| 
						 | 
					afc376c44f | ||
| 
						 | 
					84ee7e77c5 | ||
| 
						 | 
					620f054703 | ||
| 
						 | 
					cb471c52f3 | ||
| 
						 | 
					37420b2b1f | ||
| 
						 | 
					49974b7153 | ||
| 
						 | 
					a3844dde9e | ||
| d62c8c95c2 | |||
| 05606dfec1 | |||
| 68ee42f244 | |||
| fad28faabb | |||
| e5ab99f67b | |||
| e47715917e | |||
| 4f9eb58c16 | |||
| c953bc0cd3 | |||
| 610779a293 | |||
| ebd1efa990 | |||
| 5230101a8d | |||
| d8f42c1b25 | |||
| 23c6b3869e | |||
| a21f518b21 | |||
| f1bfc11160 | |||
| 72228911f2 | |||
| db7cca6296 | |||
| e36e718f28 | |||
| 44debf93c5 | |||
| 9b54fba5e5 | |||
| 6cf660c7ee | |||
| 4490f19c04 | |||
| a362a24cfc | |||
| 903daf65e6 | |||
| b310e99085 | |||
| ebabe0e85a | |||
| 6b5d6ae288 | |||
| 379e488f7a | |||
| d84d7ab62b | |||
| a0974795e1 | |||
| a9c91bee93 | |||
| b2207e308a | |||
| 3c1eeed92f | |||
| 395a8481f1 | |||
| bae0433bd9 | |||
| 3e547861ea | |||
| 9b5333dc87 | |||
| 471948bed3 | |||
| c148cdf556 | |||
| 98aed91d21 | |||
| 40630c0014 | |||
| 85a8ae625d | |||
| 43bbcb3fe0 | |||
| 1556318d07 | |||
| ea2b0f4b0a | |||
| f02c04b313 | |||
| 320060b1bd | |||
| bef33c6dac | |||
| 7bcb3c4a70 | 
							
								
								
									
										31
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -1,7 +1,30 @@
 | 
				
			|||||||
/target
 | 
					target
 | 
				
			||||||
.env
 | 
					.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?
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										2
									
								
								.prettierrc.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,2 @@
 | 
				
			|||||||
 | 
					printWidth = 90
 | 
				
			||||||
 | 
					tabWidth = 4
 | 
				
			||||||
							
								
								
									
										3818
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
							
								
								
									
										75
									
								
								Cargo.toml
									
									
									
									
									
								
							
							
						
						@@ -1,30 +1,71 @@
 | 
				
			|||||||
[package]
 | 
					[package]
 | 
				
			||||||
name = "reminder_rs"
 | 
					name = "reminder-rs"
 | 
				
			||||||
version = "1.5.0-2"
 | 
					version = "1.7.24"
 | 
				
			||||||
authors = ["jellywx <judesouthworth@pm.me>"]
 | 
					authors = ["Jude Southworth <judesouthworth@pm.me>"]
 | 
				
			||||||
edition = "2018"
 | 
					edition = "2021"
 | 
				
			||||||
 | 
					license = "AGPL-3.0 only"
 | 
				
			||||||
 | 
					description = "Reminder Bot for Discord, now in Rust"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[dependencies]
 | 
					[dependencies]
 | 
				
			||||||
dashmap = "4.0"
 | 
					poise = "0.6.1"
 | 
				
			||||||
dotenv = "0.15"
 | 
					dotenv = "0.15"
 | 
				
			||||||
humantime = "2.1"
 | 
					 | 
				
			||||||
tokio = { version = "1", features = ["process", "full"] }
 | 
					tokio = { version = "1", features = ["process", "full"] }
 | 
				
			||||||
reqwest = "0.11"
 | 
					reqwest = { 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 = "0.5"
 | 
					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"
 | 
				
			||||||
rand = "0.7"
 | 
					serde_repr = "0.1"
 | 
				
			||||||
Inflector = "0.11"
 | 
					rmp-serde = "1.1"
 | 
				
			||||||
 | 
					rand = "0.8"
 | 
				
			||||||
levenshtein = "1.0"
 | 
					levenshtein = "1.0"
 | 
				
			||||||
# serenity = { version = "0.10", features = ["collector"] }
 | 
					sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "macros", "mysql", "bigdecimal", "chrono", "migrate"] }
 | 
				
			||||||
serenity = { path = "/home/jude/serenity", features = ["collector", "unstable_discord_api"] }
 | 
					base64 = "0.22"
 | 
				
			||||||
sqlx = { version = "0.5", features = ["runtime-tokio-rustls", "macros", "mysql", "bigdecimal", "chrono"]}
 | 
					secrecy = "0.8.0"
 | 
				
			||||||
 | 
					futures = "0.3.30"
 | 
				
			||||||
 | 
					prometheus = "0.13.3"
 | 
				
			||||||
 | 
					rocket = { version = "0.5.0", features = ["tls", "secrets", "json"] }
 | 
				
			||||||
 | 
					rocket_dyn_templates = { version = "0.1.0", features = ["tera"] }
 | 
				
			||||||
 | 
					serenity = { version = "0.12", default-features = false, features = ["builder", "cache", "client", "gateway", "http", "model", "utils", "rustls_backend"] }
 | 
				
			||||||
 | 
					oauth2 = "4"
 | 
				
			||||||
 | 
					csv = "1.2"
 | 
				
			||||||
 | 
					sd-notify = "0.4.1"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[dependencies.regex_command_attr]
 | 
					[dependencies.extract_derive]
 | 
				
			||||||
path = "./regex_command_attr"
 | 
					path = "extract_derive"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[dependencies.recordable_derive]
 | 
				
			||||||
 | 
					path = "recordable_derive"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[package.metadata.deb]
 | 
				
			||||||
 | 
					depends = "$auto, python3-dateparser (>= 1.0.0)"
 | 
				
			||||||
 | 
					suggests = "mysql-server-8.0, nginx"
 | 
				
			||||||
 | 
					maintainer-scripts = "debian"
 | 
				
			||||||
 | 
					assets = [
 | 
				
			||||||
 | 
					    ["target/release/reminder-rs", "usr/bin/reminder-rs", "755"],
 | 
				
			||||||
 | 
					    ["static/css/*", "lib/reminder-rs/static/css", "644"],
 | 
				
			||||||
 | 
					    ["static/favicon/*", "lib/reminder-rs/static/favicon", "644"],
 | 
				
			||||||
 | 
					    ["static/img/*", "lib/reminder-rs/static/img", "644"],
 | 
				
			||||||
 | 
					    ["static/js/*", "lib/reminder-rs/static/js", "644"],
 | 
				
			||||||
 | 
					    ["static/webfonts/*", "lib/reminder-rs/static/webfonts", "644"],
 | 
				
			||||||
 | 
					    ["static/site.webmanifest", "lib/reminder-rs/static/site.webmanifest", "644"],
 | 
				
			||||||
 | 
					    ["templates/**/*", "lib/reminder-rs/templates", "644"],
 | 
				
			||||||
 | 
					    ["reminder-dashboard/dist/static/assets/*", "lib/reminder-rs/static/assets", "644"],
 | 
				
			||||||
 | 
					    ["reminder-dashboard/dist/index.html", "lib/reminder-rs/static/index.html", "644"],
 | 
				
			||||||
 | 
					    ["conf/default.env", "etc/reminder-rs/config.env", "600"],
 | 
				
			||||||
 | 
					    ["conf/Rocket.toml", "etc/reminder-rs/Rocket.toml", "600"],
 | 
				
			||||||
 | 
					    # ["nginx/reminder-rs", "etc/nginx/sites-available/reminder-rs", "755"]
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					conf-files = [
 | 
				
			||||||
 | 
					    "/etc/reminder-rs/config.env",
 | 
				
			||||||
 | 
					    "/etc/reminder-rs/Rocket.toml",
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[package.metadata.deb.systemd-units]
 | 
				
			||||||
 | 
					unit-scripts = "systemd"
 | 
				
			||||||
 | 
					start = false
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										9
									
								
								Containerfile
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,9 @@
 | 
				
			|||||||
 | 
					FROM ubuntu:20.04
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ENV RUSTUP_HOME=/usr/local/rustup \
 | 
				
			||||||
 | 
					    CARGO_HOME=/usr/local/cargo \
 | 
				
			||||||
 | 
					    PATH=/usr/local/cargo/bin:$PATH
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					RUN apt update && DEBIAN_FRONTEND=noninteractive TZ=Etc/UTC apt install -y gcc gcc-multilib cmake pkg-config libssl-dev curl mysql-client-8.0 npm
 | 
				
			||||||
 | 
					RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --no-modify-path --profile minimal --default-toolchain nightly
 | 
				
			||||||
 | 
					RUN cargo install cargo-deb
 | 
				
			||||||
							
								
								
									
										50
									
								
								README.md
									
									
									
									
									
								
							
							
						
						@@ -1,26 +1,42 @@
 | 
				
			|||||||
# reminder-rs
 | 
					# reminder-rs
 | 
				
			||||||
Reminder Bot for Discord, now in Rust.
 | 
					Reminder Bot for Discord.
 | 
				
			||||||
Old Python version: https://github.com/reminder-bot/bot
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
## How do I use it?
 | 
					## How do I use it?
 | 
				
			||||||
We offer a hosted version of the bot. You can invite it with: **https://invite.reminder-bot.com**. The catch is that repeating 
 | 
					I offer a hosted version of the bot. You can invite it with: **https://invite.reminder-bot.com**. The catch is that repeating 
 | 
				
			||||||
reminders are paid on the hosted version of the bot. Keep reading if you want to host it yourself.
 | 
					reminders are paid on the hosted version of the bot. Keep reading if you want to host it yourself.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
You'll need rustc and cargo for compilation. To run, you'll need Python 3 still (due to no suitable replacement for dateparser in Rust)
 | 
					You'll need rustc and cargo for compilation. To run, you'll need Python 3 still (due to no suitable replacement for dateparser in Rust)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### Compiling
 | 
					### Build APT package
 | 
				
			||||||
Reminder Bot can be built by running `cargo build --release` in the top level directory. It is necessary to create a folder called 'assets' containing an image file with its name specified in the environment as `WEBHOOK_AVATAR`, of dimensions 128x128px to be used as the webhook avatar.
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
#### Compilation environment variables
 | 
					Recommended method.
 | 
				
			||||||
These environment variables must be provided when compiling the bot
 | 
					
 | 
				
			||||||
 | 
					By default, this builds targeting Ubuntu 20.04. Modify the Containerfile if you wish to target a different platform. These instructions are written using `podman`, but `docker` should work too.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					1. Install container software: `sudo apt install podman`.
 | 
				
			||||||
 | 
					2. Install database server: `sudo apt install mysql-server-8.0`. Create a database called `reminders`
 | 
				
			||||||
 | 
					3. Install SQLx CLI: `cargo install sqlx-cli`
 | 
				
			||||||
 | 
					4. From the source code directory, execute `sqlx migrate run`
 | 
				
			||||||
 | 
					5. Build container image: `podman build -t reminder-rs .`
 | 
				
			||||||
 | 
					6. Build with podman: `podman run --rm --network=host -v "$PWD":/mnt -w /mnt -e "DATABASE_URL=mysql://user@localhost/reminders" reminder-rs cargo deb` 
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Compiling for other target
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					1. Install requirements: 
 | 
				
			||||||
 | 
					`sudo apt install gcc gcc-multilib cmake libssl-dev build-essential python3-dateparser`
 | 
				
			||||||
 | 
					2. Install rustup from https://rustup.rs
 | 
				
			||||||
 | 
					3. Install the nightly toolchain: `rustup toolchain default nightly`
 | 
				
			||||||
 | 
					4. Install database server: `sudo apt install mysql-server-8.0`. Create a database called `reminders`.
 | 
				
			||||||
 | 
					5. Install `sqlx-cli`: `cargo install sqlx-cli`.
 | 
				
			||||||
 | 
					6. Run migrations: `sqlx migrate run`.
 | 
				
			||||||
 | 
					7. Set environment variables:
 | 
				
			||||||
   * `DATABASE_URL` - the URL of your MySQL database (`mysql://user[:password]@domain/database`)
 | 
					   * `DATABASE_URL` - the URL of your MySQL database (`mysql://user[:password]@domain/database`)
 | 
				
			||||||
* `WEBHOOK_AVATAR` - accepts the name of an image file located in `$CARGO_MANIFEST_DIR/assets/` to be used as the avatar when creating webhooks. **IMPORTANT: image file must be 128x128 or smaller in size**
 | 
					8. Build: `cargo build --release`
 | 
				
			||||||
* `STRINGS_FILE` - accepts the name of a compiled strings file located in `$CARGO_MANIFEST_DIR/assets/` to be used for creating messages. Compiled string files can be generated with `compile.py` at https://github.com/reminder-bot/languages
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
### 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__
 | 
				
			||||||
@@ -29,16 +45,8 @@ __Required Variables__
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
__Other Variables__
 | 
					__Other Variables__
 | 
				
			||||||
* `MIN_INTERVAL` - default `600`, defines the shortest interval the bot should accept
 | 
					* `MIN_INTERVAL` - default `600`, defines the shortest interval the bot should accept
 | 
				
			||||||
* `MAX_TIME` - default `1576800000`, defines the maximum time ahead that reminders can be set for
 | 
					 | 
				
			||||||
* `LOCAL_TIMEZONE` - default `UTC`, necessary for calculations in the natural language processor
 | 
					* `LOCAL_TIMEZONE` - default `UTC`, necessary for calculations in the natural language processor
 | 
				
			||||||
* `DEFAULT_PREFIX` - default `$`, used for the default prefix on new guilds
 | 
					 | 
				
			||||||
* `SUBSCRIPTION_ROLES` - default `None`, accepts a list of Discord role IDs that are given to subscribed users
 | 
					* `SUBSCRIPTION_ROLES` - default `None`, accepts a list of Discord role IDs that are given to subscribed users
 | 
				
			||||||
* `CNC_GUILD` - default `None`, accepts a single Discord guild ID for the server that the subscription roles belong to
 | 
					* `CNC_GUILD` - default `None`, accepts a single Discord guild ID for the server that the subscription roles belong to
 | 
				
			||||||
* `IGNORE_BOTS` - default `1`, if `1`, Reminder Bot will ignore all other bots
 | 
					* `PYTHON_LOCATION` - default `/usr/bin/python3`. Can be changed if your Python executable is located somewhere else
 | 
				
			||||||
* `PYTHON_LOCATION` - default `venv/bin/python3`. Can be changed if your Python executable is located somewhere else
 | 
					 | 
				
			||||||
* `LOCAL_LANGUAGE` - default `EN`. Specifies the string set to fall back to if a string cannot be found (and to be used with new users)
 | 
					 | 
				
			||||||
* `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 
 | 
				
			||||||
* `CASE_INSENSITIVE` - default `1`, if `1`, commands will be treated with case insensitivity (so both `$help` and `$HELP` will work)
 | 
					 | 
				
			||||||
* `SHARD_COUNT` - default `None`, accepts the number of shards that are being ran
 | 
					 | 
				
			||||||
* `SHARD_RANGE` - default `None`, if `SHARD_COUNT` is specified, specifies what range of shards to start on this process 
 | 
					 | 
				
			||||||
* `DM_ENABLED` - default `1`, if `1`, Reminder Bot will respond to direct messages
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										28
									
								
								Rocket.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,28 @@
 | 
				
			|||||||
 | 
					[default]
 | 
				
			||||||
 | 
					address = "0.0.0.0"
 | 
				
			||||||
 | 
					port = 18920
 | 
				
			||||||
 | 
					template_dir = "templates"
 | 
				
			||||||
 | 
					limits = { json = "10MiB" }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[debug]
 | 
				
			||||||
 | 
					secret_key = "tR8krio5FXTnnyIZNiJDXPondz0kI1v6X6BXZcBGIRY="
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[debug.tls]
 | 
				
			||||||
 | 
					certs = "private/rsa_sha256_cert.pem"
 | 
				
			||||||
 | 
					key = "private/rsa_sha256_key.pem"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[debug.rsa_sha256.tls]
 | 
				
			||||||
 | 
					certs = "private/rsa_sha256_cert.pem"
 | 
				
			||||||
 | 
					key = "private/rsa_sha256_key.pem"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[debug.ecdsa_nistp256_sha256.tls]
 | 
				
			||||||
 | 
					certs = "private/ecdsa_nistp256_sha256_cert.pem"
 | 
				
			||||||
 | 
					key = "private/ecdsa_nistp256_sha256_key_pkcs8.pem"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[debug.ecdsa_nistp384_sha384.tls]
 | 
				
			||||||
 | 
					certs = "private/ecdsa_nistp384_sha384_cert.pem"
 | 
				
			||||||
 | 
					key = "private/ecdsa_nistp384_sha384_key_pkcs8.pem"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[debug.ed25519.tls]
 | 
				
			||||||
 | 
					certs = "private/ed25519_cert.pem"
 | 
				
			||||||
 | 
					key = "private/ed25519_key.pem"
 | 
				
			||||||
							
								
								
									
										
											BIN
										
									
								
								assets/webhook.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 21 KiB  | 
							
								
								
									
										13
									
								
								build.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,13 @@
 | 
				
			|||||||
 | 
					use std::{path::Path, process::Command};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fn main() {
 | 
				
			||||||
 | 
					    println!("cargo:rerun-if-changed=migrations");
 | 
				
			||||||
 | 
					    println!("cargo:rerun-if-changed=reminder-dashboard");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Command::new("npm")
 | 
				
			||||||
 | 
					        .arg("run")
 | 
				
			||||||
 | 
					        .arg("build")
 | 
				
			||||||
 | 
					        .current_dir(Path::new("reminder-dashboard"))
 | 
				
			||||||
 | 
					        .spawn()
 | 
				
			||||||
 | 
					        .expect("Failed to build NPM");
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										8
									
								
								conf/Rocket.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,8 @@
 | 
				
			|||||||
 | 
					[default]
 | 
				
			||||||
 | 
					address = "127.0.0.1"
 | 
				
			||||||
 | 
					port = 18920
 | 
				
			||||||
 | 
					template_dir = "/lib/reminder-rs/templates"
 | 
				
			||||||
 | 
					limits = { json = "10MiB" }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[release]
 | 
				
			||||||
 | 
					# secret_key = ""
 | 
				
			||||||
							
								
								
									
										19
									
								
								conf/default.env
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,19 @@
 | 
				
			|||||||
 | 
					DATABASE_URL=
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					DISCORD_TOKEN=
 | 
				
			||||||
 | 
					PATREON_GUILD_ID=
 | 
				
			||||||
 | 
					PATREON_ROLE_ID=
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					LOCAL_TIMEZONE=
 | 
				
			||||||
 | 
					MIN_INTERVAL=
 | 
				
			||||||
 | 
					PYTHON_LOCATION=/usr/bin/python3
 | 
				
			||||||
 | 
					DONTRUN=
 | 
				
			||||||
 | 
					SECRET_KEY=
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					REMIND_INTERVAL=
 | 
				
			||||||
 | 
					OAUTH2_DISCORD_CALLBACK=
 | 
				
			||||||
 | 
					OAUTH2_CLIENT_ID=
 | 
				
			||||||
 | 
					OAUTH2_CLIENT_SECRET=
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					REPORT_EMAIL=
 | 
				
			||||||
 | 
					LOG_TO_DATABASE=1
 | 
				
			||||||
							
								
								
									
										1
									
								
								cron.d/reminder_health
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					*/10 * * * * reminder /lib/reminder-rs/healthcheck
 | 
				
			||||||
							
								
								
									
										9
									
								
								debian/postinst
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,9 @@
 | 
				
			|||||||
 | 
					#!/bin/bash
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					set -e
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					id -u reminder &>/dev/null || useradd -r -M reminder
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					chown -R reminder /etc/reminder-rs
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#DEBHELPER#
 | 
				
			||||||
							
								
								
									
										7
									
								
								debian/postrm
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,7 @@
 | 
				
			|||||||
 | 
					#!/bin/bash
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					set -e
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					id -u reminder &>/dev/null || userdel reminder
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#DEBHELPER#
 | 
				
			||||||
							
								
								
									
										46
									
								
								extract_derive/Cargo.lock
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,46 @@
 | 
				
			|||||||
 | 
					# This file is automatically @generated by Cargo.
 | 
				
			||||||
 | 
					# It is not intended for manual editing.
 | 
				
			||||||
 | 
					version = 3
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "extract_macro"
 | 
				
			||||||
 | 
					version = "0.1.0"
 | 
				
			||||||
 | 
					dependencies = [
 | 
				
			||||||
 | 
					 "quote",
 | 
				
			||||||
 | 
					 "syn",
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "proc-macro2"
 | 
				
			||||||
 | 
					version = "1.0.78"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae"
 | 
				
			||||||
 | 
					dependencies = [
 | 
				
			||||||
 | 
					 "unicode-ident",
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "quote"
 | 
				
			||||||
 | 
					version = "1.0.35"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef"
 | 
				
			||||||
 | 
					dependencies = [
 | 
				
			||||||
 | 
					 "proc-macro2",
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "syn"
 | 
				
			||||||
 | 
					version = "2.0.49"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "915aea9e586f80826ee59f8453c1101f9d1c4b3964cd2460185ee8e299ada496"
 | 
				
			||||||
 | 
					dependencies = [
 | 
				
			||||||
 | 
					 "proc-macro2",
 | 
				
			||||||
 | 
					 "quote",
 | 
				
			||||||
 | 
					 "unicode-ident",
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "unicode-ident"
 | 
				
			||||||
 | 
					version = "1.0.12"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
 | 
				
			||||||
							
								
								
									
										11
									
								
								extract_derive/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,11 @@
 | 
				
			|||||||
 | 
					[package]
 | 
				
			||||||
 | 
					name = "extract_derive"
 | 
				
			||||||
 | 
					version = "0.1.0"
 | 
				
			||||||
 | 
					edition = "2021"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[lib]
 | 
				
			||||||
 | 
					proc-macro = true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[dependencies]
 | 
				
			||||||
 | 
					quote = "1.0.35"
 | 
				
			||||||
 | 
					syn = { version = "2.0.49", features = ["full"] }
 | 
				
			||||||
							
								
								
									
										53
									
								
								extract_derive/src/lib.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,53 @@
 | 
				
			|||||||
 | 
					use proc_macro::TokenStream;
 | 
				
			||||||
 | 
					use syn::{spanned::Spanned, Data, Fields};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[proc_macro_derive(Extract)]
 | 
				
			||||||
 | 
					pub fn extract(input: TokenStream) -> TokenStream {
 | 
				
			||||||
 | 
					    let ast = syn::parse_macro_input!(input);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    impl_extract(&ast)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fn impl_extract(ast: &syn::DeriveInput) -> TokenStream {
 | 
				
			||||||
 | 
					    let name = &ast.ident;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    match &ast.data {
 | 
				
			||||||
 | 
					        // Dispatch over struct: extract args directly from context
 | 
				
			||||||
 | 
					        Data::Struct(st) => match &st.fields {
 | 
				
			||||||
 | 
					            Fields::Named(fields) => {
 | 
				
			||||||
 | 
					                let extracted = fields.named.iter().map(|field| {
 | 
				
			||||||
 | 
					                    let ident = &field.ident;
 | 
				
			||||||
 | 
					                    let ty = &field.ty;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    quote::quote_spanned! {field.span()=>
 | 
				
			||||||
 | 
					                        #ident : crate::utils::extract_arg!(ctx, #ident, #ty)
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                TokenStream::from(quote::quote! {
 | 
				
			||||||
 | 
					                    impl Extract for #name {
 | 
				
			||||||
 | 
					                        fn extract(ctx: crate::ApplicationContext) -> Self {
 | 
				
			||||||
 | 
					                            Self {
 | 
				
			||||||
 | 
					                                #(#extracted,)*
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                })
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            Fields::Unit => TokenStream::from(quote::quote! {
 | 
				
			||||||
 | 
					                impl Extract for #name {
 | 
				
			||||||
 | 
					                    fn extract(ctx: crate::ApplicationContext) -> Self {
 | 
				
			||||||
 | 
					                        Self {}
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }),
 | 
				
			||||||
 | 
					            _ => {
 | 
				
			||||||
 | 
					                panic!("Only named/unit structs can derive Extract");
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        _ => {
 | 
				
			||||||
 | 
					            panic!("Only structs can derive Extract");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,10 +1,6 @@
 | 
				
			|||||||
CREATE DATABASE IF NOT EXISTS reminders;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
SET FOREIGN_KEY_CHECKS=0;
 | 
					SET FOREIGN_KEY_CHECKS=0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
USE reminders;
 | 
					CREATE TABLE guilds (
 | 
				
			||||||
 | 
					 | 
				
			||||||
CREATE TABLE reminders.guilds (
 | 
					 | 
				
			||||||
    id INT UNSIGNED UNIQUE NOT NULL AUTO_INCREMENT,
 | 
					    id INT UNSIGNED UNIQUE NOT NULL AUTO_INCREMENT,
 | 
				
			||||||
    guild BIGINT UNSIGNED UNIQUE NOT NULL,
 | 
					    guild BIGINT UNSIGNED UNIQUE NOT NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -18,10 +14,10 @@ CREATE TABLE reminders.guilds (
 | 
				
			|||||||
    default_avatar VARCHAR(512) DEFAULT 'https://raw.githubusercontent.com/reminder-bot/logos/master/Remind_Me_Bot_Logo_PPic.jpg' NOT NULL,
 | 
					    default_avatar VARCHAR(512) DEFAULT 'https://raw.githubusercontent.com/reminder-bot/logos/master/Remind_Me_Bot_Logo_PPic.jpg' NOT NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    PRIMARY KEY (id),
 | 
					    PRIMARY KEY (id),
 | 
				
			||||||
    FOREIGN KEY (default_channel_id) REFERENCES reminders.channels(id) ON DELETE SET NULL
 | 
					    FOREIGN KEY (default_channel_id) REFERENCES channels(id) ON DELETE SET NULL
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
CREATE TABLE reminders.channels (
 | 
					CREATE TABLE channels (
 | 
				
			||||||
    id INT UNSIGNED UNIQUE NOT NULL AUTO_INCREMENT,
 | 
					    id INT UNSIGNED UNIQUE NOT NULL AUTO_INCREMENT,
 | 
				
			||||||
    channel BIGINT UNSIGNED UNIQUE NOT NULL,
 | 
					    channel BIGINT UNSIGNED UNIQUE NOT NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -39,10 +35,10 @@ CREATE TABLE reminders.channels (
 | 
				
			|||||||
    guild_id INT UNSIGNED,
 | 
					    guild_id INT UNSIGNED,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    PRIMARY KEY (id),
 | 
					    PRIMARY KEY (id),
 | 
				
			||||||
    FOREIGN KEY (guild_id) REFERENCES reminders.guilds(id) ON DELETE CASCADE
 | 
					    FOREIGN KEY (guild_id) REFERENCES guilds(id) ON DELETE CASCADE
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
CREATE TABLE reminders.users (
 | 
					CREATE TABLE users (
 | 
				
			||||||
    id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL,
 | 
					    id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL,
 | 
				
			||||||
    user BIGINT UNSIGNED UNIQUE NOT NULL,
 | 
					    user BIGINT UNSIGNED UNIQUE NOT NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -59,10 +55,10 @@ CREATE TABLE reminders.users (
 | 
				
			|||||||
    patreon BOOLEAN NOT NULL DEFAULT 0,
 | 
					    patreon BOOLEAN NOT NULL DEFAULT 0,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    PRIMARY KEY (id),
 | 
					    PRIMARY KEY (id),
 | 
				
			||||||
    FOREIGN KEY (dm_channel) REFERENCES reminders.channels(id) ON DELETE RESTRICT
 | 
					    FOREIGN KEY (dm_channel) REFERENCES channels(id) ON DELETE RESTRICT
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
CREATE TABLE reminders.roles (
 | 
					CREATE TABLE roles (
 | 
				
			||||||
    id INT UNSIGNED UNIQUE NOT NULL AUTO_INCREMENT,
 | 
					    id INT UNSIGNED UNIQUE NOT NULL AUTO_INCREMENT,
 | 
				
			||||||
    role BIGINT UNSIGNED UNIQUE NOT NULL,
 | 
					    role BIGINT UNSIGNED UNIQUE NOT NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -71,10 +67,10 @@ CREATE TABLE reminders.roles (
 | 
				
			|||||||
    guild_id INT UNSIGNED NOT NULL,
 | 
					    guild_id INT UNSIGNED NOT NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    PRIMARY KEY (id),
 | 
					    PRIMARY KEY (id),
 | 
				
			||||||
    FOREIGN KEY (guild_id) REFERENCES reminders.guilds(id) ON DELETE CASCADE
 | 
					    FOREIGN KEY (guild_id) REFERENCES guilds(id) ON DELETE CASCADE
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
CREATE TABLE reminders.embeds (
 | 
					CREATE TABLE embeds (
 | 
				
			||||||
    id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL,
 | 
					    id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    title VARCHAR(256) NOT NULL DEFAULT '',
 | 
					    title VARCHAR(256) NOT NULL DEFAULT '',
 | 
				
			||||||
@@ -91,7 +87,7 @@ CREATE TABLE reminders.embeds (
 | 
				
			|||||||
    PRIMARY KEY (id)
 | 
					    PRIMARY KEY (id)
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
CREATE TABLE reminders.embed_fields (
 | 
					CREATE TABLE embed_fields (
 | 
				
			||||||
    id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL,
 | 
					    id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    title VARCHAR(256) NOT NULL DEFAULT '',
 | 
					    title VARCHAR(256) NOT NULL DEFAULT '',
 | 
				
			||||||
@@ -100,10 +96,10 @@ CREATE TABLE reminders.embed_fields (
 | 
				
			|||||||
    embed_id INT UNSIGNED NOT NULL,
 | 
					    embed_id INT UNSIGNED NOT NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    PRIMARY KEY (id),
 | 
					    PRIMARY KEY (id),
 | 
				
			||||||
    FOREIGN KEY (embed_id) REFERENCES reminders.embeds(id) ON DELETE CASCADE
 | 
					    FOREIGN KEY (embed_id) REFERENCES embeds(id) ON DELETE CASCADE
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
CREATE TABLE reminders.messages (
 | 
					CREATE TABLE messages (
 | 
				
			||||||
    id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL,
 | 
					    id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    content VARCHAR(2048) NOT NULL DEFAULT '',
 | 
					    content VARCHAR(2048) NOT NULL DEFAULT '',
 | 
				
			||||||
@@ -114,10 +110,10 @@ CREATE TABLE reminders.messages (
 | 
				
			|||||||
    attachment_name VARCHAR(260),
 | 
					    attachment_name VARCHAR(260),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    PRIMARY KEY (id),
 | 
					    PRIMARY KEY (id),
 | 
				
			||||||
    FOREIGN KEY (embed_id) REFERENCES reminders.embeds(id) ON DELETE SET NULL
 | 
					    FOREIGN KEY (embed_id) REFERENCES embeds(id) ON DELETE SET NULL
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
CREATE TABLE reminders.reminders (
 | 
					CREATE TABLE reminders (
 | 
				
			||||||
    id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL,
 | 
					    id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL,
 | 
				
			||||||
    uid VARCHAR(64) UNIQUE NOT NULL,
 | 
					    uid VARCHAR(64) UNIQUE NOT NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -140,20 +136,20 @@ CREATE TABLE reminders.reminders (
 | 
				
			|||||||
    set_by INT UNSIGNED,
 | 
					    set_by INT UNSIGNED,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    PRIMARY KEY (id),
 | 
					    PRIMARY KEY (id),
 | 
				
			||||||
    FOREIGN KEY (message_id) REFERENCES reminders.messages(id) ON DELETE RESTRICT,
 | 
					    FOREIGN KEY (message_id) REFERENCES messages(id) ON DELETE RESTRICT,
 | 
				
			||||||
    FOREIGN KEY (channel_id) REFERENCES reminders.channels(id) ON DELETE CASCADE,
 | 
					    FOREIGN KEY (channel_id) REFERENCES channels(id) ON DELETE CASCADE,
 | 
				
			||||||
    FOREIGN KEY (set_by) REFERENCES reminders.users(id) ON DELETE SET NULL
 | 
					    FOREIGN KEY (set_by) REFERENCES users(id) ON DELETE SET NULL
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
CREATE TRIGGER message_cleanup AFTER DELETE ON reminders.reminders
 | 
					CREATE TRIGGER message_cleanup AFTER DELETE ON reminders
 | 
				
			||||||
FOR EACH ROW
 | 
					FOR EACH ROW
 | 
				
			||||||
    DELETE FROM reminders.messages WHERE id = OLD.message_id;
 | 
					    DELETE FROM messages WHERE id = OLD.message_id;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
CREATE TRIGGER embed_cleanup AFTER DELETE ON reminders.messages
 | 
					CREATE TRIGGER embed_cleanup AFTER DELETE ON messages
 | 
				
			||||||
FOR EACH ROW
 | 
					FOR EACH ROW
 | 
				
			||||||
    DELETE FROM reminders.embeds WHERE id = OLD.embed_id;
 | 
					    DELETE FROM embeds WHERE id = OLD.embed_id;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
CREATE TABLE reminders.todos (
 | 
					CREATE TABLE todos (
 | 
				
			||||||
    id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL,
 | 
					    id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL,
 | 
				
			||||||
    user_id INT UNSIGNED,
 | 
					    user_id INT UNSIGNED,
 | 
				
			||||||
    guild_id INT UNSIGNED,
 | 
					    guild_id INT UNSIGNED,
 | 
				
			||||||
@@ -161,23 +157,23 @@ CREATE TABLE reminders.todos (
 | 
				
			|||||||
    value VARCHAR(2000) NOT NULL,
 | 
					    value VARCHAR(2000) NOT NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    PRIMARY KEY (id),
 | 
					    PRIMARY KEY (id),
 | 
				
			||||||
    FOREIGN KEY (user_id) REFERENCES reminders.users(id) ON DELETE SET NULL,
 | 
					    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL,
 | 
				
			||||||
    FOREIGN KEY (guild_id) REFERENCES reminders.guilds(id) ON DELETE CASCADE,
 | 
					    FOREIGN KEY (guild_id) REFERENCES guilds(id) ON DELETE CASCADE,
 | 
				
			||||||
    FOREIGN KEY (channel_id) REFERENCES reminders.channels(id) ON DELETE SET NULL
 | 
					    FOREIGN KEY (channel_id) REFERENCES channels(id) ON DELETE SET NULL
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
CREATE TABLE reminders.command_restrictions (
 | 
					CREATE TABLE command_restrictions (
 | 
				
			||||||
    id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL,
 | 
					    id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    role_id INT UNSIGNED NOT NULL,
 | 
					    role_id INT UNSIGNED NOT NULL,
 | 
				
			||||||
    command ENUM('todos', 'natural', 'remind', 'interval', 'timer', 'del', 'look', 'alias', 'countdown') NOT NULL,
 | 
					    command ENUM('todos', 'natural', 'remind', 'interval', 'timer', 'del', 'look', 'alias', 'countdown') NOT NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    PRIMARY KEY (id),
 | 
					    PRIMARY KEY (id),
 | 
				
			||||||
    FOREIGN KEY (role_id) REFERENCES reminders.roles(id) ON DELETE CASCADE,
 | 
					    FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE,
 | 
				
			||||||
    UNIQUE KEY (`role_id`, `command`)
 | 
					    UNIQUE KEY (`role_id`, `command`)
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
CREATE TABLE reminders.timers (
 | 
					CREATE TABLE timers (
 | 
				
			||||||
    id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL,
 | 
					    id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL,
 | 
				
			||||||
    start_time TIMESTAMP NOT NULL DEFAULT NOW(),
 | 
					    start_time TIMESTAMP NOT NULL DEFAULT NOW(),
 | 
				
			||||||
    name VARCHAR(32) NOT NULL,
 | 
					    name VARCHAR(32) NOT NULL,
 | 
				
			||||||
@@ -186,7 +182,7 @@ CREATE TABLE reminders.timers (
 | 
				
			|||||||
    PRIMARY KEY (id)
 | 
					    PRIMARY KEY (id)
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
CREATE TABLE reminders.events (
 | 
					CREATE TABLE events (
 | 
				
			||||||
    id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL,
 | 
					    id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL,
 | 
				
			||||||
    `time` TIMESTAMP NOT NULL DEFAULT NOW(),
 | 
					    `time` TIMESTAMP NOT NULL DEFAULT NOW(),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -198,12 +194,12 @@ CREATE TABLE reminders.events (
 | 
				
			|||||||
    reminder_id INT UNSIGNED,
 | 
					    reminder_id INT UNSIGNED,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    PRIMARY KEY (id),
 | 
					    PRIMARY KEY (id),
 | 
				
			||||||
    FOREIGN KEY (guild_id) REFERENCES reminders.guilds(id) ON DELETE CASCADE,
 | 
					    FOREIGN KEY (guild_id) REFERENCES guilds(id) ON DELETE CASCADE,
 | 
				
			||||||
    FOREIGN KEY (user_id) REFERENCES reminders.users(id) ON DELETE SET NULL,
 | 
					    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL,
 | 
				
			||||||
    FOREIGN KEY (reminder_id) REFERENCES reminders.reminders(id) ON DELETE SET NULL
 | 
					    FOREIGN KEY (reminder_id) REFERENCES reminders(id) ON DELETE SET NULL
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
CREATE TABLE reminders.command_aliases (
 | 
					CREATE TABLE command_aliases (
 | 
				
			||||||
    id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL,
 | 
					    id INT UNSIGNED AUTO_INCREMENT UNIQUE NOT NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    guild_id INT UNSIGNED NOT NULL,
 | 
					    guild_id INT UNSIGNED NOT NULL,
 | 
				
			||||||
@@ -212,22 +208,22 @@ CREATE TABLE reminders.command_aliases (
 | 
				
			|||||||
    command VARCHAR(2048) NOT NULL,
 | 
					    command VARCHAR(2048) NOT NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    PRIMARY KEY (id),
 | 
					    PRIMARY KEY (id),
 | 
				
			||||||
    FOREIGN KEY (guild_id) REFERENCES reminders.guilds(id) ON DELETE CASCADE,
 | 
					    FOREIGN KEY (guild_id) REFERENCES guilds(id) ON DELETE CASCADE,
 | 
				
			||||||
    UNIQUE KEY (`guild_id`, `name`)
 | 
					    UNIQUE KEY (`guild_id`, `name`)
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
CREATE TABLE reminders.guild_users (
 | 
					CREATE TABLE guild_users (
 | 
				
			||||||
    guild INT UNSIGNED NOT NULL,
 | 
					    guild INT UNSIGNED NOT NULL,
 | 
				
			||||||
    user INT UNSIGNED NOT NULL,
 | 
					    user INT UNSIGNED NOT NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    can_access BOOL NOT NULL DEFAULT 0,
 | 
					    can_access BOOL NOT NULL DEFAULT 0,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    FOREIGN KEY (guild) REFERENCES reminders.guilds(id) ON DELETE CASCADE,
 | 
					    FOREIGN KEY (guild) REFERENCES guilds(id) ON DELETE CASCADE,
 | 
				
			||||||
    FOREIGN KEY (user) REFERENCES reminders.users(id) ON DELETE CASCADE,
 | 
					    FOREIGN KEY (user) REFERENCES users(id) ON DELETE CASCADE,
 | 
				
			||||||
    UNIQUE KEY (guild, user)
 | 
					    UNIQUE KEY (guild, user)
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
CREATE EVENT reminders.event_cleanup
 | 
					CREATE EVENT event_cleanup
 | 
				
			||||||
ON SCHEDULE AT CURRENT_TIMESTAMP + INTERVAL 1 DAY
 | 
					ON SCHEDULE AT CURRENT_TIMESTAMP + INTERVAL 1 DAY
 | 
				
			||||||
ON COMPLETION PRESERVE
 | 
					ON COMPLETION PRESERVE
 | 
				
			||||||
DO DELETE FROM reminders.events WHERE `time` < DATE_SUB(NOW(), INTERVAL 5 DAY);
 | 
					DO DELETE FROM events WHERE `time` < DATE_SUB(NOW(), INTERVAL 5 DAY);
 | 
				
			||||||
@@ -1,5 +1,3 @@
 | 
				
			|||||||
USE reminders;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
SET FOREIGN_KEY_CHECKS = 0;
 | 
					SET FOREIGN_KEY_CHECKS = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
DROP TABLE IF EXISTS reminders_new;
 | 
					DROP TABLE IF EXISTS reminders_new;
 | 
				
			||||||
@@ -56,8 +54,7 @@ CREATE TABLE reminders_new (
 | 
				
			|||||||
    -- , CONSTRAINT interval_enabled_mutin CHECK (`enabled` = 1 OR `interval` IS NULL)
 | 
					    -- , CONSTRAINT interval_enabled_mutin CHECK (`enabled` = 1 OR `interval` IS NULL)
 | 
				
			||||||
    # disallow an expiry time if interval is unspecified
 | 
					    # disallow an expiry time if interval is unspecified
 | 
				
			||||||
    -- , CONSTRAINT interval_expires_mutin CHECK (`expires` IS NULL OR `interval` IS NOT NULL)
 | 
					    -- , CONSTRAINT interval_expires_mutin CHECK (`expires` IS NULL OR `interval` IS NOT NULL)
 | 
				
			||||||
)
 | 
					);
 | 
				
			||||||
COLLATE utf8mb4_unicode_ci;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
# import data from other tables
 | 
					# import data from other tables
 | 
				
			||||||
INSERT INTO reminders_new (
 | 
					INSERT INTO reminders_new (
 | 
				
			||||||
@@ -158,4 +155,9 @@ CREATE TABLE events (
 | 
				
			|||||||
    FOREIGN KEY (reminder_id) REFERENCES reminders_new(id) ON DELETE SET NULL
 | 
					    FOREIGN KEY (reminder_id) REFERENCES reminders_new(id) ON DELETE SET NULL
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					DROP TABLE reminders;
 | 
				
			||||||
 | 
					DROP TABLE embed_fields;
 | 
				
			||||||
 | 
					RENAME TABLE reminders_new TO reminders;
 | 
				
			||||||
 | 
					RENAME TABLE embed_fields_new TO embed_fields;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
SET FOREIGN_KEY_CHECKS = 1;
 | 
					SET FOREIGN_KEY_CHECKS = 1;
 | 
				
			||||||
							
								
								
									
										11
									
								
								migrations/20210922000000_macro.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,11 @@
 | 
				
			|||||||
 | 
					CREATE TABLE macro (
 | 
				
			||||||
 | 
					    id INT UNSIGNED AUTO_INCREMENT,
 | 
				
			||||||
 | 
					    guild_id INT UNSIGNED NOT NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    name VARCHAR(100) NOT NULL,
 | 
				
			||||||
 | 
					    description VARCHAR(100),
 | 
				
			||||||
 | 
					    commands TEXT NOT NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    FOREIGN KEY (guild_id) REFERENCES guilds(id) ON DELETE CASCADE,
 | 
				
			||||||
 | 
					    PRIMARY KEY (id)
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
@@ -0,0 +1,2 @@
 | 
				
			|||||||
 | 
					ALTER TABLE reminders RENAME COLUMN `interval` TO `interval_seconds`;
 | 
				
			||||||
 | 
					ALTER TABLE reminders ADD COLUMN `interval_months` INT UNSIGNED DEFAULT NULL;
 | 
				
			||||||
							
								
								
									
										49
									
								
								migrations/20220211000000_reminder_templates.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,49 @@
 | 
				
			|||||||
 | 
					CREATE TABLE reminder_template (
 | 
				
			||||||
 | 
					    `id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    `name` VARCHAR(24) NOT NULL DEFAULT 'Reminder',
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    `guild_id` INT UNSIGNED NOT NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    `username` VARCHAR(32) DEFAULT NULL,
 | 
				
			||||||
 | 
					    `avatar` VARCHAR(512) DEFAULT NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    `content` VARCHAR(2048) NOT NULL DEFAULT '',
 | 
				
			||||||
 | 
					    `tts` BOOL NOT NULL DEFAULT 0,
 | 
				
			||||||
 | 
					    `attachment` MEDIUMBLOB,
 | 
				
			||||||
 | 
					    `attachment_name` VARCHAR(260),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    `embed_title` VARCHAR(256) NOT NULL DEFAULT '',
 | 
				
			||||||
 | 
					    `embed_description` VARCHAR(2048) NOT NULL DEFAULT '',
 | 
				
			||||||
 | 
					    `embed_image_url` VARCHAR(512),
 | 
				
			||||||
 | 
					    `embed_thumbnail_url` VARCHAR(512),
 | 
				
			||||||
 | 
					    `embed_footer` VARCHAR(2048) NOT NULL DEFAULT '',
 | 
				
			||||||
 | 
					    `embed_footer_url` VARCHAR(512),
 | 
				
			||||||
 | 
					    `embed_author` VARCHAR(256) NOT NULL DEFAULT '',
 | 
				
			||||||
 | 
					    `embed_author_url` VARCHAR(512),
 | 
				
			||||||
 | 
					    `embed_color` INT UNSIGNED NOT NULL DEFAULT 0x0,
 | 
				
			||||||
 | 
					    `embed_fields` JSON,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    PRIMARY KEY (id),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    FOREIGN KEY (`guild_id`) REFERENCES guilds (`id`) ON DELETE CASCADE
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ALTER TABLE reminders ADD COLUMN embed_fields JSON;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					update reminders
 | 
				
			||||||
 | 
					    inner join embed_fields as E
 | 
				
			||||||
 | 
					    on E.reminder_id = reminders.id
 | 
				
			||||||
 | 
					set embed_fields = (
 | 
				
			||||||
 | 
					    select JSON_ARRAYAGG(
 | 
				
			||||||
 | 
					        JSON_OBJECT(
 | 
				
			||||||
 | 
					            'title', E.title,
 | 
				
			||||||
 | 
					            'value', E.value,
 | 
				
			||||||
 | 
					            'inline',
 | 
				
			||||||
 | 
					            if(inline = 1, cast(TRUE as json), cast(FALSE as json))
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					    from embed_fields
 | 
				
			||||||
 | 
					    group by reminder_id
 | 
				
			||||||
 | 
					    having reminder_id = reminders.id
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
							
								
								
									
										1
									
								
								migrations/20221210000000_reminder_daily_intervals.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					ALTER TABLE reminders ADD COLUMN `interval_days` INT UNSIGNED DEFAULT NULL;
 | 
				
			||||||
							
								
								
									
										1
									
								
								migrations/20230511125236_reminder_threads.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					ALTER TABLE reminders ADD COLUMN `thread_id` BIGINT DEFAULT NULL;
 | 
				
			||||||
							
								
								
									
										1
									
								
								migrations/20230511180231_ephemeral_confirmations.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					ALTER TABLE guilds ADD COLUMN ephemeral_confirmations BOOL NOT NULL DEFAULT 0;
 | 
				
			||||||
							
								
								
									
										2
									
								
								migrations/20230722130906_increase_reminder_name.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,2 @@
 | 
				
			|||||||
 | 
					ALTER TABLE reminders MODIFY `name` VARCHAR(100) NOT NULL DEFAULT 'Reminder';
 | 
				
			||||||
 | 
					ALTER TABLE reminder_template MODIFY `name` VARCHAR(100) NOT NULL DEFAULT 'Reminder';
 | 
				
			||||||
							
								
								
									
										9
									
								
								migrations/20230730134827_stats.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,9 @@
 | 
				
			|||||||
 | 
					CREATE TABLE stat (
 | 
				
			||||||
 | 
					    `id` BIGINT NOT NULL AUTO_INCREMENT,
 | 
				
			||||||
 | 
					    `utc_time` DATETIME NOT NULL DEFAULT NOW(),
 | 
				
			||||||
 | 
					    `type` ENUM('reminder_sent', 'reminder_failed'),
 | 
				
			||||||
 | 
					    `reminder_id` INT UNSIGNED,
 | 
				
			||||||
 | 
					    `message` TEXT,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    PRIMARY KEY (`id`)
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
							
								
								
									
										2
									
								
								migrations/20230731170452_reminder_archive.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,2 @@
 | 
				
			|||||||
 | 
					ALTER TABLE reminders ADD COLUMN `status` ENUM ('pending', 'sent', 'failed', 'deleted') NOT NULL DEFAULT 'pending';
 | 
				
			||||||
 | 
					ALTER TABLE reminders ADD COLUMN `status_message` TEXT;
 | 
				
			||||||
@@ -0,0 +1,3 @@
 | 
				
			|||||||
 | 
					ALTER TABLE `reminder_template` ADD COLUMN `interval_seconds` INT UNSIGNED;
 | 
				
			||||||
 | 
					ALTER TABLE `reminder_template` ADD COLUMN `interval_days` INT UNSIGNED;
 | 
				
			||||||
 | 
					ALTER TABLE `reminder_template` ADD COLUMN `interval_months` INT UNSIGNED;
 | 
				
			||||||
							
								
								
									
										50
									
								
								migrations/20240210133900_macro_restructure.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,50 @@
 | 
				
			|||||||
 | 
					CREATE TABLE command_macro (
 | 
				
			||||||
 | 
					    id INT UNSIGNED AUTO_INCREMENT,
 | 
				
			||||||
 | 
					    guild_id INT UNSIGNED NOT NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    name VARCHAR(100) NOT NULL,
 | 
				
			||||||
 | 
					    description VARCHAR(100),
 | 
				
			||||||
 | 
					    commands JSON NOT NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    FOREIGN KEY (guild_id) REFERENCES guilds(id) ON DELETE CASCADE,
 | 
				
			||||||
 | 
					    PRIMARY KEY (id)
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# New JSON structure is {command_name: "Remind", "<option name>": "<option value>", ...}
 | 
				
			||||||
 | 
					INSERT INTO command_macro (guild_id, description, name, commands)
 | 
				
			||||||
 | 
					SELECT
 | 
				
			||||||
 | 
					    guild_id,
 | 
				
			||||||
 | 
					    description,
 | 
				
			||||||
 | 
					    name,
 | 
				
			||||||
 | 
					    (
 | 
				
			||||||
 | 
					        SELECT JSON_ARRAYAGG(
 | 
				
			||||||
 | 
					            (
 | 
				
			||||||
 | 
					                SELECT JSON_OBJECTAGG(t2.name, t2.value)
 | 
				
			||||||
 | 
					                FROM JSON_TABLE(
 | 
				
			||||||
 | 
					                    JSON_ARRAY_APPEND(t1.options, '$', JSON_OBJECT('name', 'command_name', 'value', t1.command_name)),
 | 
				
			||||||
 | 
					                    '$[*]' COLUMNS (
 | 
				
			||||||
 | 
					                        name VARCHAR(64) PATH '$.name' ERROR ON ERROR,
 | 
				
			||||||
 | 
					                        value TEXT PATH '$.value' ERROR ON ERROR
 | 
				
			||||||
 | 
					                    )) AS t2
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        FROM macro m2
 | 
				
			||||||
 | 
					        JOIN JSON_TABLE(
 | 
				
			||||||
 | 
					            commands,
 | 
				
			||||||
 | 
					            '$[*]' COLUMNS (
 | 
				
			||||||
 | 
					                command_name VARCHAR(64) PATH '$.command_name' ERROR ON ERROR,
 | 
				
			||||||
 | 
					                options JSON PATH '$.options' ERROR ON ERROR
 | 
				
			||||||
 | 
					            )) AS t1
 | 
				
			||||||
 | 
					        WHERE m1.id = m2.id
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					FROM macro m1;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# # Check which commands are used in macros
 | 
				
			||||||
 | 
					# SELECT DISTINCT command_name
 | 
				
			||||||
 | 
					# FROM macro m2
 | 
				
			||||||
 | 
					# JOIN JSON_TABLE(
 | 
				
			||||||
 | 
					#     commands,
 | 
				
			||||||
 | 
					#     '$[*]' COLUMNS (
 | 
				
			||||||
 | 
					#         command_name VARCHAR(64) PATH '$.command_name' ERROR ON ERROR,
 | 
				
			||||||
 | 
					#         options JSON PATH '$.options' ERROR ON ERROR
 | 
				
			||||||
 | 
					#     )) AS t1
 | 
				
			||||||
							
								
								
									
										5
									
								
								migrations/20240303125837_add_indexes.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,5 @@
 | 
				
			|||||||
 | 
					-- Add migration script here
 | 
				
			||||||
 | 
					ALTER TABLE reminders
 | 
				
			||||||
 | 
					    ADD INDEX `utc_time_index` (`utc_time`);
 | 
				
			||||||
 | 
					ALTER TABLE reminders
 | 
				
			||||||
 | 
					    ADD INDEX `status_index` (`status`);
 | 
				
			||||||
							
								
								
									
										53
									
								
								nginx/reminder-bot
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,53 @@
 | 
				
			|||||||
 | 
					server {
 | 
				
			||||||
 | 
					    server_name www.reminder-bot.com;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return 301 https://reminder-bot.com$request_uri;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					server {
 | 
				
			||||||
 | 
					    listen 80;
 | 
				
			||||||
 | 
					    server_name beta.reminder-bot.com;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return 301 https://reminder-bot.com$request_uri;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					server {
 | 
				
			||||||
 | 
					    listen 443 ssl;
 | 
				
			||||||
 | 
					    server_name beta.reminder-bot.com;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    ssl_certificate /etc/letsencrypt/live/beta.reminder-bot.com/fullchain.pem;
 | 
				
			||||||
 | 
					    ssl_certificate_key /etc/letsencrypt/live/beta.reminder-bot.com/privkey.pem;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return 301 https://reminder-bot.com$request_uri;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					server {
 | 
				
			||||||
 | 
					    listen 443 ssl;
 | 
				
			||||||
 | 
					    server_name reminder-bot.com;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    ssl_certificate /etc/letsencrypt/live/reminder-bot.com/fullchain.pem;
 | 
				
			||||||
 | 
					    ssl_certificate_key /etc/letsencrypt/live/reminder-bot.com/privkey.pem;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    access_log /var/log/nginx/access.log;
 | 
				
			||||||
 | 
					    error_log /var/log/nginx/error.log;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    proxy_buffer_size 128k;
 | 
				
			||||||
 | 
					    proxy_buffers 4 256k;
 | 
				
			||||||
 | 
					    proxy_busy_buffers_size 256k;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    client_max_body_size 10M;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    location / {
 | 
				
			||||||
 | 
					        proxy_pass http://localhost:18920;
 | 
				
			||||||
 | 
					        proxy_redirect off;
 | 
				
			||||||
 | 
					        proxy_set_header Host $host;
 | 
				
			||||||
 | 
					        proxy_set_header X-Real-IP $remote_addr;
 | 
				
			||||||
 | 
					        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
 | 
				
			||||||
 | 
					        proxy_set_header X-Forwarded-Proto $scheme;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    location /static {
 | 
				
			||||||
 | 
					        alias /var/www/reminder-rs/static;
 | 
				
			||||||
 | 
					        expires 30d;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										32
									
								
								private/ca_cert.pem
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,32 @@
 | 
				
			|||||||
 | 
					-----BEGIN CERTIFICATE-----
 | 
				
			||||||
 | 
					MIIFbzCCA1egAwIBAgIUUY2fYP5h41dtqcuNLVUS99N7+jAwDQYJKoZIhvcNAQEL
 | 
				
			||||||
 | 
					BQAwRzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRIwEAYDVQQKDAlSb2NrZXQg
 | 
				
			||||||
 | 
					Q0ExFzAVBgNVBAMMDlJvY2tldCBSb290IENBMB4XDTIxMDUxNzE5MDIyNFoXDTMx
 | 
				
			||||||
 | 
					MDUxNTE5MDIyNFowRzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRIwEAYDVQQK
 | 
				
			||||||
 | 
					DAlSb2NrZXQgQ0ExFzAVBgNVBAMMDlJvY2tldCBSb290IENBMIICIjANBgkqhkiG
 | 
				
			||||||
 | 
					9w0BAQEFAAOCAg8AMIICCgKCAgEA3MmVOUxbZ0rtBBbXjWkkM2n2IoPuqfiENdrM
 | 
				
			||||||
 | 
					NH9IcIbEgFb7AQ/c2vGBLWWw2RFtXxe2ubbgcHyAySQ+ENGEf+W2yox5JpcOOBP+
 | 
				
			||||||
 | 
					/KrUQ4ROqxLBWOD6+fA/q5m9/CXXJosWXly3DFE9nUEUlNXcaSxJm/bxIqnxwEMQ
 | 
				
			||||||
 | 
					NESWdG1qlsGVhHLi5sflgwAcYH+nZzdtINLyvQ5AGIs8s6LpmvUVbC3I+5oNNSwW
 | 
				
			||||||
 | 
					rRsIfCFaYLuUx7jZMaOk8Dfz8MbFL68+7OCGwmhduSHoGW8jVzDXwXv/crAmFtau
 | 
				
			||||||
 | 
					zmu43/vPDwKa4w/kQJLNr1QmiGz9FBt8DXVJ0s5bokOIlodEgAvjsL3xwnrKak0F
 | 
				
			||||||
 | 
					8ak74IBetfDdVvuC9WWmJ9/dj3z91KvCWLdZGm8ijlYZ2OuaiiJ/oC6iebaPFHEY
 | 
				
			||||||
 | 
					IQmF5wzT/nZLIbQsti9wFnOpptgkhDAcKIAk3+pncuMwZ9WRvWO2IFt+ZCqKCCDU
 | 
				
			||||||
 | 
					JliDZmm3ow/zob8DD6Txex5OkJGUF6iu8o7fjI9BxsjSc76bLrxIhvR43UXjzoHl
 | 
				
			||||||
 | 
					t3lvDH8SzSN/kxzQMXVl48h3xydJ3+ZyDOLle1bqMga8Kcin9GcEUqmL1JcyFafe
 | 
				
			||||||
 | 
					CUE/Nk1pHyrlnhpMCYIDVI3pmNLB0/m/IEWLckSr9+hXyUPJQyWiyrixwC86SPxZ
 | 
				
			||||||
 | 
					AonU2aUCAwEAAaNTMFEwHQYDVR0OBBYEFJ5uQa9kD5fioNeS/Ff/mxDcMAzhMB8G
 | 
				
			||||||
 | 
					A1UdIwQYMBaAFJ5uQa9kD5fioNeS/Ff/mxDcMAzhMA8GA1UdEwEB/wQFMAMBAf8w
 | 
				
			||||||
 | 
					DQYJKoZIhvcNAQELBQADggIBABf7adF1J40/8EjfSJ5XdG7OBUrZTas3+nuULh8B
 | 
				
			||||||
 | 
					6h1igAq0BgX9v3awmPCaxqDLqwJCsFZCk0s9Vgd62McKVKasyW1TQchWbdvlqeAB
 | 
				
			||||||
 | 
					QQfZpJtAZK12dcMl6iznC/JBTPvYl9q1FKS8kYtg19in/xlhxiiEJaL5z+slpTgT
 | 
				
			||||||
 | 
					cN12650W2FqjT9sD9jshZI/a8o53d2yUBXBBecCZ49djceBGLbxbteEi/sb9qM4f
 | 
				
			||||||
 | 
					IUxeSrvK6owxKMZ5okUqZWaFseFCkdMKpMg9eecyfSTweac8yLlZmeqX5D/H85Hr
 | 
				
			||||||
 | 
					hnWHjI5NeVZAoDkdmNeaDbAIv4f3prqC8uFP3ub8D0rG/WiPdOAd79KNgqbUUFyp
 | 
				
			||||||
 | 
					NbjSiAesx4Rm1WIgjHljFQKXJdtnxt+v1VkKV9entXJX2tye7+Z1+zurNYyUyM1J
 | 
				
			||||||
 | 
					COdXeV4jpq2mP8cLpAqX7ip1tNx9YMy5ctbAE1WsUw7lPyO91+VN2Q6MN5OyemY3
 | 
				
			||||||
 | 
					4nBzRJU8X1nsDgeNQWjzb/ZC7eoS916Gywk8Hu6GoIwvYMm3zea25n6mAAOX17vE
 | 
				
			||||||
 | 
					1YdvSzUlnVbwnbgd4BgFRGUfBVjO7qvFC7ZWLngWhBzIIYIGUJfO2rippgkxAoHH
 | 
				
			||||||
 | 
					dgWYk1MNnk2yM8WSo0VKoe4yuF9NwPlfPqeCE683JHdwGEJKZWMocszzHrGZ2KX2
 | 
				
			||||||
 | 
					I4/u
 | 
				
			||||||
 | 
					-----END CERTIFICATE-----
 | 
				
			||||||
							
								
								
									
										51
									
								
								private/ca_key.pem
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,51 @@
 | 
				
			|||||||
 | 
					-----BEGIN RSA PRIVATE KEY-----
 | 
				
			||||||
 | 
					MIIJKAIBAAKCAgEA3MmVOUxbZ0rtBBbXjWkkM2n2IoPuqfiENdrMNH9IcIbEgFb7
 | 
				
			||||||
 | 
					AQ/c2vGBLWWw2RFtXxe2ubbgcHyAySQ+ENGEf+W2yox5JpcOOBP+/KrUQ4ROqxLB
 | 
				
			||||||
 | 
					WOD6+fA/q5m9/CXXJosWXly3DFE9nUEUlNXcaSxJm/bxIqnxwEMQNESWdG1qlsGV
 | 
				
			||||||
 | 
					hHLi5sflgwAcYH+nZzdtINLyvQ5AGIs8s6LpmvUVbC3I+5oNNSwWrRsIfCFaYLuU
 | 
				
			||||||
 | 
					x7jZMaOk8Dfz8MbFL68+7OCGwmhduSHoGW8jVzDXwXv/crAmFtauzmu43/vPDwKa
 | 
				
			||||||
 | 
					4w/kQJLNr1QmiGz9FBt8DXVJ0s5bokOIlodEgAvjsL3xwnrKak0F8ak74IBetfDd
 | 
				
			||||||
 | 
					VvuC9WWmJ9/dj3z91KvCWLdZGm8ijlYZ2OuaiiJ/oC6iebaPFHEYIQmF5wzT/nZL
 | 
				
			||||||
 | 
					IbQsti9wFnOpptgkhDAcKIAk3+pncuMwZ9WRvWO2IFt+ZCqKCCDUJliDZmm3ow/z
 | 
				
			||||||
 | 
					ob8DD6Txex5OkJGUF6iu8o7fjI9BxsjSc76bLrxIhvR43UXjzoHlt3lvDH8SzSN/
 | 
				
			||||||
 | 
					kxzQMXVl48h3xydJ3+ZyDOLle1bqMga8Kcin9GcEUqmL1JcyFafeCUE/Nk1pHyrl
 | 
				
			||||||
 | 
					nhpMCYIDVI3pmNLB0/m/IEWLckSr9+hXyUPJQyWiyrixwC86SPxZAonU2aUCAwEA
 | 
				
			||||||
 | 
					AQKCAgBVSXlfXOOiDwtnnPs/IPJe+fuecaBsABfyRcbEMLbm4OhfOzpSurHx0YC4
 | 
				
			||||||
 | 
					7KNX9qdtKFfpfX9NdIq7KEjhbk3kqfPmYkUaZxeTCgZhzAua2S0aYHBXyPCqQ+gU
 | 
				
			||||||
 | 
					fZsqH+Pwe6H0aZQ8KdXHPTCaHdK6veThXo7feQ5t2noT9rq31txpx/Xd6BNGWsmJ
 | 
				
			||||||
 | 
					xS0xCZ68/GgnWdVyumKAGKkmKzRaK3pPA5CzwFqBw7ouvFaWvLuQymU6kWk1B6Xb
 | 
				
			||||||
 | 
					NYIB7IaXWPbRwhnMV0x9C2ABEzFvqOpvT1rqDqloAR4dlvcfbsIZZkQ2mhjt6MeT
 | 
				
			||||||
 | 
					hsorwQ4yCjvtZvVRfW1gTP4iR7ZpmHgHIYQDJy7raZqRVNe9WgMxKTS/IYsHvEvH
 | 
				
			||||||
 | 
					MUBOfQEclAQ8FTUOgTg9+Hf9VXJOtjZRxY08gb+OgKxoAQKxRZmLDUZli9SNWzGe
 | 
				
			||||||
 | 
					R3UECh0gImm/CzWnV3fercLX+qHLTcyJFcb2gclAfG8v8cOT2qu8RtsJAQM2ZSn7
 | 
				
			||||||
 | 
					L8FaWXewjAwkZwCl8Zl5ky1RbBXUkcuLIkAeLAl0+ffM9xiwLhiIj8gRJoq0/jSr
 | 
				
			||||||
 | 
					K1pA8CQ/rZC+9RsI8hP4x2Fn1CU8bOyEBPeNFEIDJHBiCrs/Rl6EAzDMJHlhL/HT
 | 
				
			||||||
 | 
					f7bqssuRjnSbRCKaAdtfGTxQUEjefvqJ11xE3BfHGnKPqoasMQKCAQEA8nY+3MAB
 | 
				
			||||||
 | 
					eBPlE/H4JeVpj7/9/q+JWLFFH9E3Re25LbObzRzIQOd5bTCJkOPQXYRbRIZ1pMt9
 | 
				
			||||||
 | 
					+nZzeLKvWn5nuUFgG2S4n+BEJkBDV78LOkUuGIAPdztF2mwVumygEGJFFfZHbYrh
 | 
				
			||||||
 | 
					XtaGUKdNxn1R3Z4B8W5jWpNNkU+dvoq4Zt0LrL28HB4Sa2yrtHeXbhqSb0BA15+N
 | 
				
			||||||
 | 
					vO9mlfX2I6Yr8F+L/OBfxB0gaNLISJJsj5NSm302je/QrfoMAwbePoNPjEX1/qd2
 | 
				
			||||||
 | 
					rgj9JfM+NE2FsVnFhrV+q4DywRUdX4VBFP4+x7fPm8LxvtVUmLVA3/NtGwPdnC6U
 | 
				
			||||||
 | 
					mQh/+kKVhU2AQwKCAQEA6R2FrxZIUdmk45b/tSjlq7r1ycBYeSztjzXw6hx+w/K3
 | 
				
			||||||
 | 
					Lf+5OGyoGoKr5bZHMhHFxqoWzP6kiZ2Uyvu+pLAGLfenJPec89ZncN4FvW9yU8yL
 | 
				
			||||||
 | 
					nE2Sc8Vf2GnOw2KGJcJR11Ew4dDspHfnM3rof2G4gPC/EMZeGojXoc5GHmT4iasD
 | 
				
			||||||
 | 
					Xwje4qqx5WKvRu1Rw2y+XM5h4gDn3QLLojDOvBpt22yaqSTAupY7OliGFO9h2WPL
 | 
				
			||||||
 | 
					r9TL6va87nXNepCdtzuSlxqTVtgMu2wVu3CnQyw9RiD2M31YnUtBDLNy/UNFj/5Z
 | 
				
			||||||
 | 
					6uXYuakh8vSFFvjgCUyQwR1xCTJur/6LdpAnt9Bz9wKCAQAUqoN9KVh2tatm4c72
 | 
				
			||||||
 | 
					2/D9ca3ikW+xgZqUta5yZWrNPGvhNbzT22b8KZDwKprN/cQRuSw52aZpPMNm3EQa
 | 
				
			||||||
 | 
					AIAyyCG69ADQj7r/T6btybjZRKBDMlcfIIw5q9DGTQ/vlZCx6IX6DkZbYQmdwkTc
 | 
				
			||||||
 | 
					0D20GA2uWGxbggawhgq5/PTuv5SJKrrn4qBLS73u6eqcVeN5XA6q0kywd+9UhNxv
 | 
				
			||||||
 | 
					+W/xUxOJgE5pVto2VREBLonWSwZVfnyx6GjvC0sOzv0Ocv7KxAPNqtRwzQ9Wtr7s
 | 
				
			||||||
 | 
					klb84Nv3OW0MjTcjwfr481Cyy2DqgP5PFnSogWJuibR34jXAgbnX4BiGWrUdzaMU
 | 
				
			||||||
 | 
					86AlAoIBABnw9Bh41VFuc9/zxL7nLy++HW33HqFVc5Y1PXr/8sdhcisHQxhZVxek
 | 
				
			||||||
 | 
					JPbqIuAahDTIZsMnLy41QAKaoyt2fymMXqhJecjUuiwgOOlMxp82qu6Y30xM0Y6m
 | 
				
			||||||
 | 
					r6CkjSMUjcD1QwhOFJd01GCxM8BBIqQOpmR6fqxbQAu8hacKO3IuerCPryXwMt3A
 | 
				
			||||||
 | 
					7ppo/GlP55sySEg7K5I3pmuFHOxn0IPTgR6DfYMGBs9GXJ1lyjDD3z3Q42RhUsMC
 | 
				
			||||||
 | 
					jvwtra9fTL/N8EmAv2H39C8oqSRbfvIX5u3x6/ONFU8RhSFT5CDTADSYoVZ/0MxV
 | 
				
			||||||
 | 
					k53r0hqWz6D94r9QQmsJW4G1JwZYhx8CggEBANUybXsJRgDC5ZOlwbaq0FeTOpZ4
 | 
				
			||||||
 | 
					pSKx1RkVV+G93hK5XdXIVu365pSt3T68MgF+4wWlbC4MuKuzJltFnJBapzz5ygcU
 | 
				
			||||||
 | 
					jw+Q+l/jq+mJj8uvUfo5TAy9thuggj1a7SCs/dm5DBwYA7bfmamLbbBpA0qywMsF
 | 
				
			||||||
 | 
					/vL1bpePIFhbGMhBK7uiEzOAOZVuceZWqcUByjUYy925K/an0ANsQAch5PVeAsEv
 | 
				
			||||||
 | 
					wXC3soaCzfzUyv+eAYBy7LOcz+hpdRj1l4c9Zx6hZeBxMc5LSRoTs+HfE6GgTuI2
 | 
				
			||||||
 | 
					cCfCZNFw7DhDy1TBd3XjQ0AFykeaesImZVR6gp0NYH1kionsMtR5rl4C2tw=
 | 
				
			||||||
 | 
					-----END RSA PRIVATE KEY-----
 | 
				
			||||||
							
								
								
									
										21
									
								
								private/ecdsa_nistp256_sha256_cert.pem
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,21 @@
 | 
				
			|||||||
 | 
					-----BEGIN CERTIFICATE-----
 | 
				
			||||||
 | 
					MIIDYTCCAUmgAwIBAgIUA/EaCcXKgOxBiuaMNTackeF9DgcwDQYJKoZIhvcNAQEL
 | 
				
			||||||
 | 
					BQAwRzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRIwEAYDVQQKDAlSb2NrZXQg
 | 
				
			||||||
 | 
					Q0ExFzAVBgNVBAMMDlJvY2tldCBSb290IENBMB4XDTIxMDUxNzE5MDIyNFoXDTMx
 | 
				
			||||||
 | 
					MDUxNTE5MDIyNFowPzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMQ8wDQYDVQQK
 | 
				
			||||||
 | 
					DAZSb2NrZXQxEjAQBgNVBAMMCWxvY2FsaG9zdDBZMBMGByqGSM49AgEGCCqGSM49
 | 
				
			||||||
 | 
					AwEHA0IABOEVdmVd218y3Yy+ocjtt5siaFsNR7tknvS21pzKzxsFc05UrF74YqRx
 | 
				
			||||||
 | 
					Ic6/AQq56C48x6gDhUCdf+nMzD0NWTijGDAWMBQGA1UdEQQNMAuCCWxvY2FsaG9z
 | 
				
			||||||
 | 
					dDANBgkqhkiG9w0BAQsFAAOCAgEAW32kadHWEIsN5WXacjtgE9jbFii/rWJjrAO/
 | 
				
			||||||
 | 
					GWuARwLvjWeEFM5xSTny1cTDEz/7Bmfd9GRRpfgxKCFBGaU7zTmOV/AokrjUay+s
 | 
				
			||||||
 | 
					KPj0EV9ZE/84KVfr+MOuhwXhSSv+NvxduFw1sdhTUHs+Wopwjte+bnOUBXmnQH97
 | 
				
			||||||
 | 
					ITSQuUvEamPUS4tJD/kQ0qSGJKNv3CShhbe3XkjUd0UKK7lZzn6hY2fy3CrkkJDT
 | 
				
			||||||
 | 
					GyzFCRf3ZDfjW5/fGXN/TNoEK65pPQVr5taK/bovDesEO7rR4Y4YgtnGlJ7YToWh
 | 
				
			||||||
 | 
					E6I77CbsJCJdmgpjPNwRlwQ8upoWAfxOHqwg32s6uWq20v4TXZTOnEKOPEJG74bh
 | 
				
			||||||
 | 
					JHtwqsy2fIdGz4kZksKGerzJffyQPhX4f3qxxrijT9Hj2EoFIQ4u/jTRpK31IW3R
 | 
				
			||||||
 | 
					gEeNB8O4iyg3crx0oHRwc6zX63O6wBJCZ+0uxLoyjwtjKCxVYbJpccjoQh6zO3UO
 | 
				
			||||||
 | 
					pqAO+NKxQ469QDBV3uRyyQ7DRPu6p67/8gn1E/o8kyU1P7eLFIhBp4T4wCaMQBG6
 | 
				
			||||||
 | 
					IpL5W7eCyuOcqfdm471N07Z4KAqiV8eBJcKaAE+WzNIvrFvCZ/zq7Gj8p07nkjK8
 | 
				
			||||||
 | 
					+ZGDUieiY0mQEpiXLQZt1xNtT/MmlAzFYrK7t6gSygykQKjO1o4GG4g+eXu3h1YK
 | 
				
			||||||
 | 
					avsOwtc=
 | 
				
			||||||
 | 
					-----END CERTIFICATE-----
 | 
				
			||||||
							
								
								
									
										5
									
								
								private/ecdsa_nistp256_sha256_key_pkcs8.pem
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,5 @@
 | 
				
			|||||||
 | 
					-----BEGIN PRIVATE KEY-----
 | 
				
			||||||
 | 
					MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgeo/7wBIYVax9bj4m
 | 
				
			||||||
 | 
					1UEe2H+H4YLsbApDCZMslZbubRuhRANCAAThFXZlXdtfMt2MvqHI7bebImhbDUe7
 | 
				
			||||||
 | 
					ZJ70ttacys8bBXNOVKxe+GKkcSHOvwEKueguPMeoA4VAnX/pzMw9DVk4
 | 
				
			||||||
 | 
					-----END PRIVATE KEY-----
 | 
				
			||||||
							
								
								
									
										21
									
								
								private/ecdsa_nistp384_sha384_cert.pem
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,21 @@
 | 
				
			|||||||
 | 
					-----BEGIN CERTIFICATE-----
 | 
				
			||||||
 | 
					MIIDfjCCAWagAwIBAgIUfmkM99JpQUsQsh8TVILPQDBGOhowDQYJKoZIhvcNAQEM
 | 
				
			||||||
 | 
					BQAwRzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRIwEAYDVQQKDAlSb2NrZXQg
 | 
				
			||||||
 | 
					Q0ExFzAVBgNVBAMMDlJvY2tldCBSb290IENBMB4XDTIxMDUxNzE5MDIyNFoXDTMx
 | 
				
			||||||
 | 
					MDUxNTE5MDIyNFowPzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMQ8wDQYDVQQK
 | 
				
			||||||
 | 
					DAZSb2NrZXQxEjAQBgNVBAMMCWxvY2FsaG9zdDB2MBAGByqGSM49AgEGBSuBBAAi
 | 
				
			||||||
 | 
					A2IABM5rQQNZEGWeDyrg2dVQS15c+JsX/ginN9/ArHpTgGO6xaLfkbekIj7gewUR
 | 
				
			||||||
 | 
					VQcQrYulwu02HTFDzGVvf1DdCZzIJLJUpl+jagdI05yMPFg2s5lThzGB6HENyw1I
 | 
				
			||||||
 | 
					hU1dMaMYMBYwFAYDVR0RBA0wC4IJbG9jYWxob3N0MA0GCSqGSIb3DQEBDAUAA4IC
 | 
				
			||||||
 | 
					AQAzpXFEeCMvTBG+kzmHN/+esjCK8MO/Grrw3Qoa1stX0yjNWzfhlGHfWk9Mgwnp
 | 
				
			||||||
 | 
					DnIEFT9lB+nT9uTkKL81Opu2bkWXuL0MyjyYmcCfEgJeDblUbD4HYzPO/s2P7QZu
 | 
				
			||||||
 | 
					Oa1pNKBiSWe1xq+F/gkn0+nDfICq3QQOOQMzaAmlFmszN3v7pryz+x21MqTFsfuW
 | 
				
			||||||
 | 
					ymycRcwZ3VyYeceZuBxjOhR2l8I5yZbR+KPj8VS7A3vgqvkS8ixTK0LrxcLwrTIz
 | 
				
			||||||
 | 
					W9Z1skzVpux4K7iwn3qlWIJ7EbERi9Ydz0MzpjD+CHxGlgHTzT552DGZhPO8D7BE
 | 
				
			||||||
 | 
					+4IoS0YeXEGLeC2a9Hmiy3FXbM4nt3aIWMiWyhjslXIbvWOJL1Vi5GNpdQCG+rB7
 | 
				
			||||||
 | 
					lvA0IISp/tAi9duufdONjgRceRDsqf7o6TOBQdB3QxHRt9gCB2QquRh5llMUY8YH
 | 
				
			||||||
 | 
					PxFJAEzF4X5b0q0k4b50HRVNfDY0SC/XIR7S2xnLDGuoUqrOpUligBzfXV4JHrPv
 | 
				
			||||||
 | 
					YKHsWSOiVxU9eY1SYKuKqnOsGvXSSQlYqSK1ol94eOKDjNy8wekaVYAQNsdNL7o5
 | 
				
			||||||
 | 
					QSWZUcbZLkaNMFdD9twWGduAs0eTichVgyvRgPdevjTGDPBHKNSUURZRTYeW4aIJ
 | 
				
			||||||
 | 
					QzSQUQXwOXsQOmfCkaaTISsDwZAJ0wFqqST1AOhlCWD1OQ==
 | 
				
			||||||
 | 
					-----END CERTIFICATE-----
 | 
				
			||||||
							
								
								
									
										6
									
								
								private/ecdsa_nistp384_sha384_key_pkcs8.pem
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,6 @@
 | 
				
			|||||||
 | 
					-----BEGIN PRIVATE KEY-----
 | 
				
			||||||
 | 
					MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDCRBt7MeJXyJRq7grGQ
 | 
				
			||||||
 | 
					jEdBHE31hG5LuKQ5iL8yTVGk75Kc8ISll2LewbvQ3NhR6E+hZANiAATOa0EDWRBl
 | 
				
			||||||
 | 
					ng8q4NnVUEteXPibF/4IpzffwKx6U4BjusWi35G3pCI+4HsFEVUHEK2LpcLtNh0x
 | 
				
			||||||
 | 
					Q8xlb39Q3QmcyCSyVKZfo2oHSNOcjDxYNrOZU4cxgehxDcsNSIVNXTE=
 | 
				
			||||||
 | 
					-----END PRIVATE KEY-----
 | 
				
			||||||
							
								
								
									
										20
									
								
								private/ed25519_cert.pem
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,20 @@
 | 
				
			|||||||
 | 
					-----BEGIN CERTIFICATE-----
 | 
				
			||||||
 | 
					MIIDMjCCARqgAwIBAgIUSu1CGfaWlNPjjLG7SaEEgxI+AsAwDQYJKoZIhvcNAQEL
 | 
				
			||||||
 | 
					BQAwRzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRIwEAYDVQQKDAlSb2NrZXQg
 | 
				
			||||||
 | 
					Q0ExFzAVBgNVBAMMDlJvY2tldCBSb290IENBMB4XDTIxMDUxNzE5MDIyNFoXDTMx
 | 
				
			||||||
 | 
					MDUxNTE5MDIyNFowPzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMQ8wDQYDVQQK
 | 
				
			||||||
 | 
					DAZSb2NrZXQxEjAQBgNVBAMMCWxvY2FsaG9zdDAqMAUGAytlcAMhAKNv1fkv5rxY
 | 
				
			||||||
 | 
					xGT84C98g2xPpain+jsliHvYrqNkS1zhoxgwFjAUBgNVHREEDTALgglsb2NhbGhv
 | 
				
			||||||
 | 
					c3QwDQYJKoZIhvcNAQELBQADggIBAMgWZhr3whYbFPNYm5RZxjgEonMY7cH/Rza1
 | 
				
			||||||
 | 
					UrBkyoMRL9v00YKJTEvjl5Xl4ku7jqxY2qjValacDroD8hYTLhhkkbHxH6u+kJSC
 | 
				
			||||||
 | 
					cKRQ8QVBZMmoor8qD2Y2BuXZYGWEtgeoXh+DLHWOim7gXDD6GyFPnYFGHnRhNmkE
 | 
				
			||||||
 | 
					6fPcfw1FZmBrMM9rk31EeK9nLVRabL+vuLDEddX1LH4LwK4OuV51wQMZGYiC3M2b
 | 
				
			||||||
 | 
					JpmuPAUAUyiyN53Utuw/ubeih3cEglOwCyDPFt6XISYHO0UaQdw25uhpjxs8KTZB
 | 
				
			||||||
 | 
					qOPJAI4cvGaN3GARXvz2P/QHUiTzsf2XOXXtrO0g+F9P7t6x2xL35c0A8yxKkfsa
 | 
				
			||||||
 | 
					RKdCveRuVlsLXVLVBikqLE/OgPC6SIhIFvTYzXTaZyNfge6dRy2Wg6qWPcGwseeA
 | 
				
			||||||
 | 
					QpFKgWiY3cuy7nJm6uybR6zOEMj5GgNWIbICGguQ5161MLnv0bEmKh2d9/jQ/XN5
 | 
				
			||||||
 | 
					M+nixtGla/G4hL3FQIy9rhHVZzPWwSxl+/eE4qgnInEhmlQ2KrcpgneCt38t0vjJ
 | 
				
			||||||
 | 
					dwHZSzVv8yX7fE0OSq/DWQXJraWUcT7Ds0g7T7jWkWfS0qStVd9VJ+rYQ8dBbE9Y
 | 
				
			||||||
 | 
					gcmkm1DMUd+y9xDRDjxby7gMDWFiN2Au9FQgaIpIctTNCj0+agFBPnfXPVSIH6gX
 | 
				
			||||||
 | 
					10kA2ZVX
 | 
				
			||||||
 | 
					-----END CERTIFICATE-----
 | 
				
			||||||
							
								
								
									
										3
									
								
								private/ed25519_key.pem
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,3 @@
 | 
				
			|||||||
 | 
					-----BEGIN PRIVATE KEY-----
 | 
				
			||||||
 | 
					MC4CAQAwBQYDK2VwBCIEIGtLkaYE4D+P5HLkoc3a/tNhPP+gnhPLF3jEtdYcAtpd
 | 
				
			||||||
 | 
					-----END PRIVATE KEY-----
 | 
				
			||||||
							
								
								
									
										114
									
								
								private/gen_certs.sh
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,114 @@
 | 
				
			|||||||
 | 
					#! /bin/bash
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Usage:
 | 
				
			||||||
 | 
					#   ./gen_certs.sh [cert-kind]
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					# [cert-kind]:
 | 
				
			||||||
 | 
					#   ed25519
 | 
				
			||||||
 | 
					#   rsa_sha256
 | 
				
			||||||
 | 
					#   ecdsa_nistp256_sha256
 | 
				
			||||||
 | 
					#   ecdsa_nistp384_sha384
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					# Generate a certificate of the [cert-kind] key type, or if no cert-kind is
 | 
				
			||||||
 | 
					# specified, all of the certificates.
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					# Examples:
 | 
				
			||||||
 | 
					#   ./gen_certs.sh ed25519
 | 
				
			||||||
 | 
					#   ./gen_certs.sh rsa_sha256
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# TODO: `rustls` (really, `webpki`) doesn't currently use the CN in the subject
 | 
				
			||||||
 | 
					# to check if a certificate is valid for a server name sent via SNI. It's not
 | 
				
			||||||
 | 
					# clear if this is intended, since certificates _should_ have a `subjectAltName`
 | 
				
			||||||
 | 
					# with a DNS name, or if it simply hasn't been implemented yet. See
 | 
				
			||||||
 | 
					# https://bugzilla.mozilla.org/show_bug.cgi?id=552346 for a bit more info.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					CA_SUBJECT="/C=US/ST=CA/O=Rocket CA/CN=Rocket Root CA"
 | 
				
			||||||
 | 
					SUBJECT="/C=US/ST=CA/O=Rocket/CN=localhost"
 | 
				
			||||||
 | 
					ALT="DNS:localhost"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function gen_ca() {
 | 
				
			||||||
 | 
					  openssl genrsa -out ca_key.pem 4096
 | 
				
			||||||
 | 
					  openssl req -new -x509 -days 3650 -key ca_key.pem \
 | 
				
			||||||
 | 
					    -subj "${CA_SUBJECT}" -out ca_cert.pem
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function gen_ca_if_non_existent() {
 | 
				
			||||||
 | 
					  if ! [ -f ./ca_cert.pem ]; then gen_ca; fi
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function gen_rsa_sha256() {
 | 
				
			||||||
 | 
					  gen_ca_if_non_existent
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  openssl req -newkey rsa:4096 -nodes -sha256 -keyout rsa_sha256_key.pem \
 | 
				
			||||||
 | 
					    -subj "${SUBJECT}" -out server.csr
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  openssl x509 -req -sha256 -extfile <(printf "subjectAltName=${ALT}") -days 3650 \
 | 
				
			||||||
 | 
					    -CA ca_cert.pem -CAkey ca_key.pem -CAcreateserial \
 | 
				
			||||||
 | 
					    -in server.csr -out rsa_sha256_cert.pem
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  rm ca_cert.srl server.csr
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function gen_ed25519() {
 | 
				
			||||||
 | 
					  gen_ca_if_non_existent
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  openssl genpkey -algorithm ED25519 > ed25519_key.pem
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  openssl req -new -key ed25519_key.pem -subj "${SUBJECT}" -out server.csr
 | 
				
			||||||
 | 
					  openssl x509 -req -extfile <(printf "subjectAltName=${ALT}") -days 3650 \
 | 
				
			||||||
 | 
					    -CA ca_cert.pem -CAkey ca_key.pem -CAcreateserial \
 | 
				
			||||||
 | 
					    -in server.csr -out ed25519_cert.pem
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  rm ca_cert.srl server.csr
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function gen_ecdsa_nistp256_sha256() {
 | 
				
			||||||
 | 
					  gen_ca_if_non_existent
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  openssl ecparam -out ecdsa_nistp256_sha256_key.pem -name prime256v1 -genkey
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # Convert to pkcs8 format supported by rustls
 | 
				
			||||||
 | 
					  openssl pkcs8 -topk8 -nocrypt -in ecdsa_nistp256_sha256_key.pem \
 | 
				
			||||||
 | 
					    -out ecdsa_nistp256_sha256_key_pkcs8.pem
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  openssl req -new -nodes -sha256 -key ecdsa_nistp256_sha256_key_pkcs8.pem \
 | 
				
			||||||
 | 
					    -subj "${SUBJECT}" -out server.csr
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  openssl x509 -req -sha256 -extfile <(printf "subjectAltName=${ALT}") -days 3650 \
 | 
				
			||||||
 | 
					    -CA ca_cert.pem -CAkey ca_key.pem -CAcreateserial \
 | 
				
			||||||
 | 
					    -in server.csr -out ecdsa_nistp256_sha256_cert.pem
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  rm ca_cert.srl server.csr ecdsa_nistp256_sha256_key.pem
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function gen_ecdsa_nistp384_sha384() {
 | 
				
			||||||
 | 
					  gen_ca_if_non_existent
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  openssl ecparam -out ecdsa_nistp384_sha384_key.pem -name secp384r1 -genkey
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # Convert to pkcs8 format supported by rustls
 | 
				
			||||||
 | 
					  openssl pkcs8 -topk8 -nocrypt -in ecdsa_nistp384_sha384_key.pem \
 | 
				
			||||||
 | 
					    -out ecdsa_nistp384_sha384_key_pkcs8.pem
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  openssl req -new -nodes -sha384 -key ecdsa_nistp384_sha384_key_pkcs8.pem \
 | 
				
			||||||
 | 
					    -subj "${SUBJECT}" -out server.csr
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  openssl x509 -req -sha384 -extfile <(printf "subjectAltName=${ALT}") -days 3650 \
 | 
				
			||||||
 | 
					    -CA ca_cert.pem -CAkey ca_key.pem -CAcreateserial \
 | 
				
			||||||
 | 
					    -in server.csr -out ecdsa_nistp384_sha384_cert.pem
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  rm ca_cert.srl server.csr ecdsa_nistp384_sha384_key.pem
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					case $1 in
 | 
				
			||||||
 | 
					  ed25519) gen_ed25519 ;;
 | 
				
			||||||
 | 
					  rsa_sha256) gen_rsa_sha256 ;;
 | 
				
			||||||
 | 
					  ecdsa_nistp256_sha256) gen_ecdsa_nistp256_sha256 ;;
 | 
				
			||||||
 | 
					  ecdsa_nistp384_sha384) gen_ecdsa_nistp384_sha384 ;;
 | 
				
			||||||
 | 
					  *)
 | 
				
			||||||
 | 
					    gen_ed25519
 | 
				
			||||||
 | 
					    gen_rsa_sha256
 | 
				
			||||||
 | 
					    gen_ecdsa_nistp256_sha256
 | 
				
			||||||
 | 
					    gen_ecdsa_nistp384_sha384
 | 
				
			||||||
 | 
					    ;;
 | 
				
			||||||
 | 
					esac
 | 
				
			||||||
							
								
								
									
										30
									
								
								private/rsa_sha256_cert.pem
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,30 @@
 | 
				
			|||||||
 | 
					-----BEGIN CERTIFICATE-----
 | 
				
			||||||
 | 
					MIIFLDCCAxSgAwIBAgIUZ461KXDgTT25LP1S6PK6i9ZKtOYwDQYJKoZIhvcNAQEL
 | 
				
			||||||
 | 
					BQAwRzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRIwEAYDVQQKDAlSb2NrZXQg
 | 
				
			||||||
 | 
					Q0ExFzAVBgNVBAMMDlJvY2tldCBSb290IENBMB4XDTIxMDUxNzE5MDIyNFoXDTMx
 | 
				
			||||||
 | 
					MDUxNTE5MDIyNFowPzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMQ8wDQYDVQQK
 | 
				
			||||||
 | 
					DAZSb2NrZXQxEjAQBgNVBAMMCWxvY2FsaG9zdDCCAiIwDQYJKoZIhvcNAQEBBQAD
 | 
				
			||||||
 | 
					ggIPADCCAgoCggIBAKl8Re1dneFB2obYPOuZf3d6BR2xGcMhr2vmHpnVgkeg/xGI
 | 
				
			||||||
 | 
					cwHhhB04mEg4TqbZ0Q1wdKN4ruNigdrQAXDqnm4y2IB1q/uTdoXVWNsvAZmDq4N4
 | 
				
			||||||
 | 
					rPUDWagpkilV4HOHDQOI27Lo/+30wJX6H8i3J6Nag1y9spuySkL1F1SgQng9uzvP
 | 
				
			||||||
 | 
					3SwbsO9R/jS7qTZNEtirnJv3qJlxmT4BCvBuWNALShFlSZYnZk/2csYXEaVkWMUE
 | 
				
			||||||
 | 
					rnWGmTmzMZEzDOdQdPC3sJDCPQbbgD4SnQANqhOVjPSdJ6Y6joN6XYtB2EP+6LJ8
 | 
				
			||||||
 | 
					UBTICETnUROWeKqMm215Gsen32cyWkaCwEWtTFn7rJHbPvUY4vPYQZAB2/h07lYq
 | 
				
			||||||
 | 
					v67W3r5lTq1KuoStD8M/7nCIYIuNhmJLMzzWrSfJrQpYexoMII6kjOxv2kiUgb1y
 | 
				
			||||||
 | 
					bYKnFlVf4up3pTpi2/0We4SHRgc7xTcEPNvU5z5hc5JwHZIFjmi5dx50ZoAULrXl
 | 
				
			||||||
 | 
					OUhfdUPut7H38UQg3riJiNAzedUiTubek26po8qH1iS/EMgAi5RboaBovjWhgjwq
 | 
				
			||||||
 | 
					P/RP0vzpln6OdQIRaRlY9vZiik9HbFybafuA2zEkyc+zc4HqJk3vqX/0hgd9qWeL
 | 
				
			||||||
 | 
					zgsHr6A86tNUXsUaoInQbzznjpbFE85klxd6HeqasOyVDCeDUs4lWoDNp0DbAgMB
 | 
				
			||||||
 | 
					AAGjGDAWMBQGA1UdEQQNMAuCCWxvY2FsaG9zdDANBgkqhkiG9w0BAQsFAAOCAgEA
 | 
				
			||||||
 | 
					sS2TUKKYtNYC68TEQb5KAa8/zi6Lhz9lyn9rpBXAcAVeBdxCqrBU0nRN8EjXEff1
 | 
				
			||||||
 | 
					oBeau9Wi9PwdK6J+4xFuer9YrZZWuWLKUBXJL9ZXEfI+USo5WVz7v/puoKZSUSu2
 | 
				
			||||||
 | 
					+Ee4+v1eoAsCtaA8IR0Sh1KTc9kg1QZW1nIdBV0zgAERWTzm1iy2Ff+euowM/lUR
 | 
				
			||||||
 | 
					FczMAJsNq83O2kaH541fRx3gC2iMN/QLwRalBR2y8hTI5wQa0Yr8moC4IsTfRrKQ
 | 
				
			||||||
 | 
					/SZDjFT1XoA9TnrByIvGQNlxK1Rm2hvK1OQn4OL6sdYK6F+MxfUn/atQNyBQ6CF+
 | 
				
			||||||
 | 
					oKuvOnE2jces8GGZIU1jYJ2271SQkzp0CW3N1ZKjClSQFAYED+5ERTGyeEL4o3Vr
 | 
				
			||||||
 | 
					V/BUd0KC7HhbWBlFc2EDmPOoOTp/ID901Kf499M68pe2Fr/63/nCAON6WFk1NPYA
 | 
				
			||||||
 | 
					+sWMfS25hikU5rOHGQgXxXAz90pwpvpoLQvaq0/azsbHvq9B4ubdcLE0xcoK+DGq
 | 
				
			||||||
 | 
					+/aOeWlYkalWp93akPYpWN3UJXzDXzzagRID/1LEGS+Ssbh1WVhAhLKVoxzBvkgm
 | 
				
			||||||
 | 
					ozVAhR3zHXI2QpQ2FEbOmkcRtpxv2CiaTsgHg2MK8cdmPx3G+ufCZjRbQx/VXVdN
 | 
				
			||||||
 | 
					vaFRdmzr/5EQ7DT5Uy8w32h1to4Fm2sAtbAFYaFLXaM=
 | 
				
			||||||
 | 
					-----END CERTIFICATE-----
 | 
				
			||||||
							
								
								
									
										52
									
								
								private/rsa_sha256_key.pem
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,52 @@
 | 
				
			|||||||
 | 
					-----BEGIN PRIVATE KEY-----
 | 
				
			||||||
 | 
					MIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQCpfEXtXZ3hQdqG
 | 
				
			||||||
 | 
					2DzrmX93egUdsRnDIa9r5h6Z1YJHoP8RiHMB4YQdOJhIOE6m2dENcHSjeK7jYoHa
 | 
				
			||||||
 | 
					0AFw6p5uMtiAdav7k3aF1VjbLwGZg6uDeKz1A1moKZIpVeBzhw0DiNuy6P/t9MCV
 | 
				
			||||||
 | 
					+h/ItyejWoNcvbKbskpC9RdUoEJ4Pbs7z90sG7DvUf40u6k2TRLYq5yb96iZcZk+
 | 
				
			||||||
 | 
					AQrwbljQC0oRZUmWJ2ZP9nLGFxGlZFjFBK51hpk5szGRMwznUHTwt7CQwj0G24A+
 | 
				
			||||||
 | 
					Ep0ADaoTlYz0nSemOo6Del2LQdhD/uiyfFAUyAhE51ETlniqjJtteRrHp99nMlpG
 | 
				
			||||||
 | 
					gsBFrUxZ+6yR2z71GOLz2EGQAdv4dO5WKr+u1t6+ZU6tSrqErQ/DP+5wiGCLjYZi
 | 
				
			||||||
 | 
					SzM81q0nya0KWHsaDCCOpIzsb9pIlIG9cm2CpxZVX+Lqd6U6Ytv9FnuEh0YHO8U3
 | 
				
			||||||
 | 
					BDzb1Oc+YXOScB2SBY5ouXcedGaAFC615TlIX3VD7rex9/FEIN64iYjQM3nVIk7m
 | 
				
			||||||
 | 
					3pNuqaPKh9YkvxDIAIuUW6GgaL41oYI8Kj/0T9L86ZZ+jnUCEWkZWPb2YopPR2xc
 | 
				
			||||||
 | 
					m2n7gNsxJMnPs3OB6iZN76l/9IYHfalni84LB6+gPOrTVF7FGqCJ0G88546WxRPO
 | 
				
			||||||
 | 
					ZJcXeh3qmrDslQwng1LOJVqAzadA2wIDAQABAoICABT4gHp/Q+K0UEKxDNCl/ISe
 | 
				
			||||||
 | 
					/3UODb78MwVpws2MAoO0YvsbZAeOjNdEwmrlNK4mc1xzVqtHanROIv0dEaCUFyhR
 | 
				
			||||||
 | 
					eEJkzPPi6h5jKIxuQ4doKFerHdNvJ6/L/P7KVmxVAII4c96uP8SErTOhcD9Ykjn/
 | 
				
			||||||
 | 
					IBPgkPH83H1ucAWTksXn9XvQG3CyuHDUN1z0/1ntrXBLw6P0v9LEoI5weJcJQEn1
 | 
				
			||||||
 | 
					q6N9Yd6HX3xzZP4nqpJJWUZ/bsqx7dGa3340z9rrNJz4TYuLzRtFG5gSm4R/LFUi
 | 
				
			||||||
 | 
					Av/dViOWST3xbROnAQhgyRAUm6AGpCdKa9i9nI6VuUGRY4PivJy7OTpSQVIdwD2K
 | 
				
			||||||
 | 
					VFbFauCmsTY7B+Z+DXe9JJV+Tlazvlwop1DApxCgLMNr2pVB/1yPzMrNYDB4Sg1c
 | 
				
			||||||
 | 
					T3stu4A8PtljTykla2C2LPFrdIijvYv9n6PSPWV4vf1ix9uYs99FhZPQJMgm+CDr
 | 
				
			||||||
 | 
					n3cFfuofIWIRJpCuu3hn4woGgW2bXor4pyMNg1yCp1T2c8g6eJUYAAIRpse0HDbT
 | 
				
			||||||
 | 
					ZUifxlNBx819bf9aqv+lhwMU4UFlmWMv3NETcCBqgUn+z4scNJ3kZwO9ZodLRblK
 | 
				
			||||||
 | 
					SpalZ6SNejTnVukg3zbwJG8w5vIrJvfjBkikAL1O5X6Kn3YqfrXAl38bIvA50ZBe
 | 
				
			||||||
 | 
					eOFcgDPClLPGlNKxQXiZAoIBAQDh0aArTjWi8Kxt2Ga3Hu4jQ7IXklL9osGAlFCB
 | 
				
			||||||
 | 
					wZs831/ktLY0c7vY+mbwrY4AtqK21A00MrNSTsp/McHwAUi410DdcTqzkGnm9BBZ
 | 
				
			||||||
 | 
					FKVO1H9KGg2t344tOXcPsFTl6SR5a0aPqagyvgdH+YWC4ccfYZWMcLELn3sOGoEp
 | 
				
			||||||
 | 
					a7VBxjLZZ+/b+/oIzhwxEaBi8N0U3IZ3SsC9Lmojnb9+LMwGs14TFfahD3uM1hnU
 | 
				
			||||||
 | 
					vww1elwPwXafWc+7Ej1bXijWBXbyzkCITzHafmDsAatEZnemHL8zKKtTn2aSTUSj
 | 
				
			||||||
 | 
					Fl/y94WR7/V4nVEQ0R3fjGC0rKh08BtKzxPjYBqjT5aI7+yfAoIBAQDAIzROr00o
 | 
				
			||||||
 | 
					65auD5TB2k/pSVKQpB/U6ESGafDxg4a+R9Ng2ESmDN5hUUZDefm0Uxf9AZqS6eno
 | 
				
			||||||
 | 
					GSAqxNDixWPy2WFzbZ0mUCoyQn+Eh09WBvt0IqDZ2+ngBEXiKA/8dsvXinBHLuHV
 | 
				
			||||||
 | 
					u+rJdLMzPfhWVo1/iXq7SOjBvDuthKROku5BEt6Q3Cs0nk6uneRIqKtaqa7KIInF
 | 
				
			||||||
 | 
					BPtUifZtCP09h6iFFGNv2g2OYRYDg8TXvjt3scKy0DUC3kTeTCPvNvHaOObGbMRU
 | 
				
			||||||
 | 
					Q85HkXburxWfsMto5m/lRsbY3COxddB47WIQ6QNewESfCab/R2lOdlXQeaH8fuxT
 | 
				
			||||||
 | 
					wWC5DMz4F0ZFAoIBAFm+EjZDnaNEnHIHB0MNIryXAabGev7beKUdzCTVCVmWuChO
 | 
				
			||||||
 | 
					/P45ZFTlppVNk9qKun2IJjsxTvyN3YHRB27XQ8xZlyiqABcudDfZlMmiH9QFNRUA
 | 
				
			||||||
 | 
					56DK8Fjetodgn0zDa8BpNqCPXw3TYVdkPX/3NEgvYtxuSJ4C4keHlv8cE+uw1bJ6
 | 
				
			||||||
 | 
					0OMO754iMyf5BlFrwaCxxyqPZauJT5sZ7Ok66lZbYC6bkukNGx+sUpWu2y5Bk2ab
 | 
				
			||||||
 | 
					jwXjDmAc7o9qCzaK82upNhI1zu0zPldsjmDfi/tS/1VYe0X/WicYWAesM7N+VPHb
 | 
				
			||||||
 | 
					eCVX98iEIqgdxKzo1QWsClyfkRrSraNrVLrVBqcCggEAaLIGK6YMNnMBPUGSPnt2
 | 
				
			||||||
 | 
					NdlVWymDiuExjcimmQOhZYf/33KZHZ4/gunljpklfqQUmzHHh6xcX7NpOsTaSedj
 | 
				
			||||||
 | 
					Sg43ss0U566g/5gKoi2VBnxxglvoKC5T51SMu+o2o8wb0QxHmBIszulBy5qClzZ6
 | 
				
			||||||
 | 
					Xpl1Kvy/2tOkuQSXxDpVydb4ao8cpfTCuj5VA4NXxFvcW1/AtbU7PRc02GEA3XMb
 | 
				
			||||||
 | 
					gu6r3jA46tb3shCnDS09Eo4/Gz7Kp+MaL8Dr5/G3Vv8qlE2TOqZD6OK1wXu7Qd43
 | 
				
			||||||
 | 
					uzd772I5sMZ7TenOrUFUYsB/QlWmF3hPLBX3YH0KHc4PfrT4lnyWzCDAUrVt7vXH
 | 
				
			||||||
 | 
					vQKCAQEAz1Q+jW+NG7CAqkOJ19n+KmGn+zddvy8x4txKx8kV0+3XIVNkseS6IT65
 | 
				
			||||||
 | 
					uemD85Gn7MYwHxcKUHghHSKyJsvCb1vSmB3JtCnrsodmQNMb6Lsq89VlM8Ylc/s3
 | 
				
			||||||
 | 
					F4AraiOlylqcEwLkanD3XSfPdQZhExZHEtpAW/Rr6zYT9J94VO1nK3+RkFI4a8kl
 | 
				
			||||||
 | 
					pEbY+tqJj3LGTuaQkshB9rDtcBT5/JsaxlMk783qkKziCPZF47BWqrJb+t7tm3qg
 | 
				
			||||||
 | 
					5gf+QUInEjdW3k3uZBL4316RP/TJlLvo29PkEwoC8X4R2EgDeHQhhRC2Fi2Wpy4O
 | 
				
			||||||
 | 
					ce4G+zZOOYXwvWGJLwNhgsve8C3oqg==
 | 
				
			||||||
 | 
					-----END PRIVATE KEY-----
 | 
				
			||||||
							
								
								
									
										11
									
								
								recordable_derive/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,11 @@
 | 
				
			|||||||
 | 
					[package]
 | 
				
			||||||
 | 
					name = "recordable_derive"
 | 
				
			||||||
 | 
					version = "0.1.0"
 | 
				
			||||||
 | 
					edition = "2021"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[lib]
 | 
				
			||||||
 | 
					proc-macro = true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[dependencies]
 | 
				
			||||||
 | 
					quote = "1.0.35"
 | 
				
			||||||
 | 
					syn = { version = "2.0.49", features = ["full"] }
 | 
				
			||||||
							
								
								
									
										42
									
								
								recordable_derive/src/lib.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,42 @@
 | 
				
			|||||||
 | 
					use proc_macro::TokenStream;
 | 
				
			||||||
 | 
					use syn::{spanned::Spanned, Data};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Macro to allow Recordable to be implemented on an enum by dispatching over each variant.
 | 
				
			||||||
 | 
					#[proc_macro_derive(Recordable)]
 | 
				
			||||||
 | 
					pub fn extract(input: TokenStream) -> TokenStream {
 | 
				
			||||||
 | 
					    let ast = syn::parse_macro_input!(input);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    impl_recordable(&ast)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fn impl_recordable(ast: &syn::DeriveInput) -> TokenStream {
 | 
				
			||||||
 | 
					    let name = &ast.ident;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    match &ast.data {
 | 
				
			||||||
 | 
					        Data::Enum(en) => {
 | 
				
			||||||
 | 
					            let extracted = en.variants.iter().map(|var| {
 | 
				
			||||||
 | 
					                let ident = &var.ident;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                quote::quote_spanned! {var.span()=>
 | 
				
			||||||
 | 
					                    Self::#ident (opt) => opt.run(ctx).await?
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            TokenStream::from(quote::quote! {
 | 
				
			||||||
 | 
					                impl Recordable for #name {
 | 
				
			||||||
 | 
					                    async fn run(self, ctx: crate::Context<'_>) -> Result<(), crate::Error> {
 | 
				
			||||||
 | 
					                        match self {
 | 
				
			||||||
 | 
					                            #(#extracted,)*
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        Ok(())
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        _ => {
 | 
				
			||||||
 | 
					            panic!("Only enums can derive Recordable");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,14 +0,0 @@
 | 
				
			|||||||
[package]
 | 
					 | 
				
			||||||
name = "regex_command_attr"
 | 
					 | 
				
			||||||
version = "0.2.0"
 | 
					 | 
				
			||||||
authors = ["acdenisSK <acdenissk69@gmail.com>", "jellywx <judesouthworth@pm.me>"]
 | 
					 | 
				
			||||||
edition = "2018"
 | 
					 | 
				
			||||||
description = "Procedural macros for command creation for the RegexFramework for serenity."
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
[lib]
 | 
					 | 
				
			||||||
proc-macro = true
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
[dependencies]
 | 
					 | 
				
			||||||
quote = "^1.0"
 | 
					 | 
				
			||||||
syn = { version = "^1.0", features = ["full", "derive", "extra-traits"] }
 | 
					 | 
				
			||||||
proc-macro2 = "1.0"
 | 
					 | 
				
			||||||
@@ -1,293 +0,0 @@
 | 
				
			|||||||
use proc_macro2::Span;
 | 
					 | 
				
			||||||
use syn::parse::{Error, Result};
 | 
					 | 
				
			||||||
use syn::spanned::Spanned;
 | 
					 | 
				
			||||||
use syn::{Attribute, Ident, Lit, LitStr, Meta, NestedMeta, Path};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use crate::structures::PermissionLevel;
 | 
					 | 
				
			||||||
use crate::util::{AsOption, LitExt};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use std::fmt::{self, Write};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Debug, Clone, Copy, PartialEq)]
 | 
					 | 
				
			||||||
pub enum ValueKind {
 | 
					 | 
				
			||||||
    // #[<name>]
 | 
					 | 
				
			||||||
    Name,
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // #[<name> = <value>]
 | 
					 | 
				
			||||||
    Equals,
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // #[<name>([<value>, <value>, <value>, ...])]
 | 
					 | 
				
			||||||
    List,
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // #[<name>(<value>)]
 | 
					 | 
				
			||||||
    SingleList,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl fmt::Display for ValueKind {
 | 
					 | 
				
			||||||
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
 | 
					 | 
				
			||||||
        match self {
 | 
					 | 
				
			||||||
            ValueKind::Name => f.pad("`#[<name>]`"),
 | 
					 | 
				
			||||||
            ValueKind::Equals => f.pad("`#[<name> = <value>]`"),
 | 
					 | 
				
			||||||
            ValueKind::List => f.pad("`#[<name>([<value>, <value>, <value>, ...])]`"),
 | 
					 | 
				
			||||||
            ValueKind::SingleList => f.pad("`#[<name>(<value>)]`"),
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
fn to_ident(p: Path) -> Result<Ident> {
 | 
					 | 
				
			||||||
    if p.segments.is_empty() {
 | 
					 | 
				
			||||||
        return Err(Error::new(
 | 
					 | 
				
			||||||
            p.span(),
 | 
					 | 
				
			||||||
            "cannot convert an empty path to an identifier",
 | 
					 | 
				
			||||||
        ));
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if p.segments.len() > 1 {
 | 
					 | 
				
			||||||
        return Err(Error::new(
 | 
					 | 
				
			||||||
            p.span(),
 | 
					 | 
				
			||||||
            "the path must not have more than one segment",
 | 
					 | 
				
			||||||
        ));
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if !p.segments[0].arguments.is_empty() {
 | 
					 | 
				
			||||||
        return Err(Error::new(
 | 
					 | 
				
			||||||
            p.span(),
 | 
					 | 
				
			||||||
            "the singular path segment must not have any arguments",
 | 
					 | 
				
			||||||
        ));
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    Ok(p.segments[0].ident.clone())
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Debug)]
 | 
					 | 
				
			||||||
pub struct Values {
 | 
					 | 
				
			||||||
    pub name: Ident,
 | 
					 | 
				
			||||||
    pub literals: Vec<Lit>,
 | 
					 | 
				
			||||||
    pub kind: ValueKind,
 | 
					 | 
				
			||||||
    pub span: Span,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl Values {
 | 
					 | 
				
			||||||
    #[inline]
 | 
					 | 
				
			||||||
    pub fn new(name: Ident, kind: ValueKind, literals: Vec<Lit>, span: Span) -> Self {
 | 
					 | 
				
			||||||
        Values {
 | 
					 | 
				
			||||||
            name,
 | 
					 | 
				
			||||||
            literals,
 | 
					 | 
				
			||||||
            kind,
 | 
					 | 
				
			||||||
            span,
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
pub fn parse_values(attr: &Attribute) -> Result<Values> {
 | 
					 | 
				
			||||||
    let meta = attr.parse_meta()?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    match meta {
 | 
					 | 
				
			||||||
        Meta::Path(path) => {
 | 
					 | 
				
			||||||
            let name = to_ident(path)?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            Ok(Values::new(name, ValueKind::Name, Vec::new(), attr.span()))
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        Meta::List(meta) => {
 | 
					 | 
				
			||||||
            let name = to_ident(meta.path)?;
 | 
					 | 
				
			||||||
            let nested = meta.nested;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if nested.is_empty() {
 | 
					 | 
				
			||||||
                return Err(Error::new(attr.span(), "list cannot be empty"));
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            let mut lits = Vec::with_capacity(nested.len());
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            for meta in nested {
 | 
					 | 
				
			||||||
                match meta {
 | 
					 | 
				
			||||||
                    NestedMeta::Lit(l) => lits.push(l),
 | 
					 | 
				
			||||||
                    NestedMeta::Meta(m) => match m {
 | 
					 | 
				
			||||||
                        Meta::Path(path) => {
 | 
					 | 
				
			||||||
                            let i = to_ident(path)?;
 | 
					 | 
				
			||||||
                            lits.push(Lit::Str(LitStr::new(&i.to_string(), i.span())))
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
                        Meta::List(_) | Meta::NameValue(_) => {
 | 
					 | 
				
			||||||
                            return Err(Error::new(attr.span(), "cannot nest a list; only accept literals and identifiers at this level"))
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
                    },
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            let kind = if lits.len() == 1 {
 | 
					 | 
				
			||||||
                ValueKind::SingleList
 | 
					 | 
				
			||||||
            } else {
 | 
					 | 
				
			||||||
                ValueKind::List
 | 
					 | 
				
			||||||
            };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            Ok(Values::new(name, kind, lits, attr.span()))
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        Meta::NameValue(meta) => {
 | 
					 | 
				
			||||||
            let name = to_ident(meta.path)?;
 | 
					 | 
				
			||||||
            let lit = meta.lit;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            Ok(Values::new(name, ValueKind::Equals, vec![lit], attr.span()))
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Debug, Clone)]
 | 
					 | 
				
			||||||
struct DisplaySlice<'a, T>(&'a [T]);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl<'a, T: fmt::Display> fmt::Display for DisplaySlice<'a, T> {
 | 
					 | 
				
			||||||
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
 | 
					 | 
				
			||||||
        let mut iter = self.0.iter().enumerate();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        match iter.next() {
 | 
					 | 
				
			||||||
            None => f.write_str("nothing")?,
 | 
					 | 
				
			||||||
            Some((idx, elem)) => {
 | 
					 | 
				
			||||||
                write!(f, "{}: {}", idx, elem)?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                for (idx, elem) in iter {
 | 
					 | 
				
			||||||
                    f.write_char('\n')?;
 | 
					 | 
				
			||||||
                    write!(f, "{}: {}", idx, elem)?;
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        Ok(())
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[inline]
 | 
					 | 
				
			||||||
fn is_form_acceptable(expect: &[ValueKind], kind: ValueKind) -> bool {
 | 
					 | 
				
			||||||
    if expect.contains(&ValueKind::List) && kind == ValueKind::SingleList {
 | 
					 | 
				
			||||||
        true
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
        expect.contains(&kind)
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[inline]
 | 
					 | 
				
			||||||
fn validate(values: &Values, forms: &[ValueKind]) -> Result<()> {
 | 
					 | 
				
			||||||
    if !is_form_acceptable(forms, values.kind) {
 | 
					 | 
				
			||||||
        return Err(Error::new(
 | 
					 | 
				
			||||||
            values.span,
 | 
					 | 
				
			||||||
            // Using the `_args` version here to avoid an allocation.
 | 
					 | 
				
			||||||
            format_args!(
 | 
					 | 
				
			||||||
                "the attribute must be in of these forms:\n{}",
 | 
					 | 
				
			||||||
                DisplaySlice(forms)
 | 
					 | 
				
			||||||
            ),
 | 
					 | 
				
			||||||
        ));
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    Ok(())
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[inline]
 | 
					 | 
				
			||||||
pub fn parse<T: AttributeOption>(values: Values) -> Result<T> {
 | 
					 | 
				
			||||||
    T::parse(values)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
pub trait AttributeOption: Sized {
 | 
					 | 
				
			||||||
    fn parse(values: Values) -> Result<Self>;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl AttributeOption for Vec<String> {
 | 
					 | 
				
			||||||
    fn parse(values: Values) -> Result<Self> {
 | 
					 | 
				
			||||||
        validate(&values, &[ValueKind::List])?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        Ok(values
 | 
					 | 
				
			||||||
            .literals
 | 
					 | 
				
			||||||
            .into_iter()
 | 
					 | 
				
			||||||
            .map(|lit| lit.to_str())
 | 
					 | 
				
			||||||
            .collect())
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl AttributeOption for String {
 | 
					 | 
				
			||||||
    #[inline]
 | 
					 | 
				
			||||||
    fn parse(values: Values) -> Result<Self> {
 | 
					 | 
				
			||||||
        validate(&values, &[ValueKind::Equals, ValueKind::SingleList])?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        Ok(values.literals[0].to_str())
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl AttributeOption for bool {
 | 
					 | 
				
			||||||
    #[inline]
 | 
					 | 
				
			||||||
    fn parse(values: Values) -> Result<Self> {
 | 
					 | 
				
			||||||
        validate(&values, &[ValueKind::Name, ValueKind::SingleList])?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        Ok(values.literals.get(0).map_or(true, |l| l.to_bool()))
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl AttributeOption for Ident {
 | 
					 | 
				
			||||||
    #[inline]
 | 
					 | 
				
			||||||
    fn parse(values: Values) -> Result<Self> {
 | 
					 | 
				
			||||||
        validate(&values, &[ValueKind::SingleList])?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        Ok(values.literals[0].to_ident())
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl AttributeOption for Vec<Ident> {
 | 
					 | 
				
			||||||
    #[inline]
 | 
					 | 
				
			||||||
    fn parse(values: Values) -> Result<Self> {
 | 
					 | 
				
			||||||
        validate(&values, &[ValueKind::List])?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        Ok(values.literals.into_iter().map(|l| l.to_ident()).collect())
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl AttributeOption for Option<String> {
 | 
					 | 
				
			||||||
    fn parse(values: Values) -> Result<Self> {
 | 
					 | 
				
			||||||
        validate(&values, &[ValueKind::Name, ValueKind::Equals, ValueKind::SingleList])?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        Ok(values.literals.get(0).map(|l| l.to_str()))
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl AttributeOption for PermissionLevel {
 | 
					 | 
				
			||||||
    fn parse(values: Values) -> Result<Self> {
 | 
					 | 
				
			||||||
        validate(&values, &[ValueKind::SingleList])?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        Ok(values.literals.get(0).map(|l| PermissionLevel::from_str(&*l.to_str()).unwrap()).unwrap())
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl<T: AttributeOption> AttributeOption for AsOption<T> {
 | 
					 | 
				
			||||||
    #[inline]
 | 
					 | 
				
			||||||
    fn parse(values: Values) -> Result<Self> {
 | 
					 | 
				
			||||||
        Ok(AsOption(Some(T::parse(values)?)))
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
macro_rules! attr_option_num {
 | 
					 | 
				
			||||||
    ($($n:ty),*) => {
 | 
					 | 
				
			||||||
        $(
 | 
					 | 
				
			||||||
            impl AttributeOption for $n {
 | 
					 | 
				
			||||||
                fn parse(values: Values) -> Result<Self> {
 | 
					 | 
				
			||||||
                    validate(&values, &[ValueKind::SingleList])?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    Ok(match &values.literals[0] {
 | 
					 | 
				
			||||||
                        Lit::Int(l) => l.base10_parse::<$n>()?,
 | 
					 | 
				
			||||||
                        l => {
 | 
					 | 
				
			||||||
                            let s = l.to_str();
 | 
					 | 
				
			||||||
                            // Use `as_str` to guide the compiler to use `&str`'s parse method.
 | 
					 | 
				
			||||||
                            // We don't want to use our `parse` method here (`impl AttributeOption for String`).
 | 
					 | 
				
			||||||
                            match s.as_str().parse::<$n>() {
 | 
					 | 
				
			||||||
                                Ok(n) => n,
 | 
					 | 
				
			||||||
                                Err(_) => return Err(Error::new(l.span(), "invalid integer")),
 | 
					 | 
				
			||||||
                            }
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
                    })
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            impl AttributeOption for Option<$n> {
 | 
					 | 
				
			||||||
                #[inline]
 | 
					 | 
				
			||||||
                fn parse(values: Values) -> Result<Self> {
 | 
					 | 
				
			||||||
                    <$n as AttributeOption>::parse(values).map(Some)
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        )*
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
attr_option_num!(u16, u32, usize);
 | 
					 | 
				
			||||||
@@ -1,5 +0,0 @@
 | 
				
			|||||||
pub mod suffixes {
 | 
					 | 
				
			||||||
    pub const COMMAND: &str = "COMMAND";
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
pub use self::suffixes::*;
 | 
					 | 
				
			||||||
@@ -1,102 +0,0 @@
 | 
				
			|||||||
#![deny(rust_2018_idioms)]
 | 
					 | 
				
			||||||
// FIXME: Remove this in a foreseeable future.
 | 
					 | 
				
			||||||
// Currently exists for backwards compatibility to previous Rust versions.
 | 
					 | 
				
			||||||
#![recursion_limit = "128"]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[allow(unused_extern_crates)]
 | 
					 | 
				
			||||||
extern crate proc_macro;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use proc_macro::TokenStream;
 | 
					 | 
				
			||||||
use quote::quote;
 | 
					 | 
				
			||||||
use syn::{parse::Error, parse_macro_input, spanned::Spanned, Lit};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
pub(crate) mod attributes;
 | 
					 | 
				
			||||||
pub(crate) mod consts;
 | 
					 | 
				
			||||||
pub(crate) mod structures;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[macro_use]
 | 
					 | 
				
			||||||
pub(crate) mod util;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use attributes::*;
 | 
					 | 
				
			||||||
use consts::*;
 | 
					 | 
				
			||||||
use structures::*;
 | 
					 | 
				
			||||||
use util::*;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
macro_rules! match_options {
 | 
					 | 
				
			||||||
    ($v:expr, $values:ident, $options:ident, $span:expr => [$($name:ident);*]) => {
 | 
					 | 
				
			||||||
        match $v {
 | 
					 | 
				
			||||||
            $(
 | 
					 | 
				
			||||||
                stringify!($name) => $options.$name = propagate_err!($crate::attributes::parse($values)),
 | 
					 | 
				
			||||||
            )*
 | 
					 | 
				
			||||||
            _ => {
 | 
					 | 
				
			||||||
                return Error::new($span, format_args!("invalid attribute: {:?}", $v))
 | 
					 | 
				
			||||||
                    .to_compile_error()
 | 
					 | 
				
			||||||
                    .into();
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[proc_macro_attribute]
 | 
					 | 
				
			||||||
pub fn command(attr: TokenStream, input: TokenStream) -> TokenStream {
 | 
					 | 
				
			||||||
    let mut fun = parse_macro_input!(input as CommandFun);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let lit_name = if !attr.is_empty() {
 | 
					 | 
				
			||||||
        parse_macro_input!(attr as Lit).to_str()
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
        fun.name.to_string()
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let mut options = Options::new();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    for attribute in &fun.attributes {
 | 
					 | 
				
			||||||
        let span = attribute.span();
 | 
					 | 
				
			||||||
        let values = propagate_err!(parse_values(attribute));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        let name = values.name.to_string();
 | 
					 | 
				
			||||||
        let name = &name[..];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        match_options!(name, values, options, span => [
 | 
					 | 
				
			||||||
            permission_level;
 | 
					 | 
				
			||||||
            supports_dm;
 | 
					 | 
				
			||||||
            can_blacklist
 | 
					 | 
				
			||||||
        ]);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let Options {
 | 
					 | 
				
			||||||
        permission_level,
 | 
					 | 
				
			||||||
        supports_dm,
 | 
					 | 
				
			||||||
        can_blacklist,
 | 
					 | 
				
			||||||
    } = options;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let visibility = fun.visibility;
 | 
					 | 
				
			||||||
    let name = fun.name.clone();
 | 
					 | 
				
			||||||
    let body = fun.body;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let n = name.with_suffix(COMMAND);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let cooked = fun.cooked.clone();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let command_path = quote!(crate::framework::Command);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    populate_fut_lifetimes_on_refs(&mut fun.args);
 | 
					 | 
				
			||||||
    let args = fun.args;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    (quote! {
 | 
					 | 
				
			||||||
        #(#cooked)*
 | 
					 | 
				
			||||||
        pub static #n: #command_path = #command_path {
 | 
					 | 
				
			||||||
            func: #name,
 | 
					 | 
				
			||||||
            name: #lit_name,
 | 
					 | 
				
			||||||
            required_perms: #permission_level,
 | 
					 | 
				
			||||||
            supports_dm: #supports_dm,
 | 
					 | 
				
			||||||
            can_blacklist: #can_blacklist,
 | 
					 | 
				
			||||||
        };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        #visibility fn #name<'fut> (#(#args),*) -> ::serenity::futures::future::BoxFuture<'fut, ()> {
 | 
					 | 
				
			||||||
            use ::serenity::futures::future::FutureExt;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            async move { #(#body)* }.boxed()
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    })
 | 
					 | 
				
			||||||
    .into()
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,231 +0,0 @@
 | 
				
			|||||||
use crate::util::{Argument, Parenthesised};
 | 
					 | 
				
			||||||
use proc_macro2::Span;
 | 
					 | 
				
			||||||
use proc_macro2::TokenStream as TokenStream2;
 | 
					 | 
				
			||||||
use quote::{quote, ToTokens};
 | 
					 | 
				
			||||||
use syn::{
 | 
					 | 
				
			||||||
    braced,
 | 
					 | 
				
			||||||
    parse::{Error, Parse, ParseStream, Result},
 | 
					 | 
				
			||||||
    spanned::Spanned,
 | 
					 | 
				
			||||||
    Attribute, Block, FnArg, Ident, Pat, Path, PathSegment, Stmt, Token, Visibility,
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
fn parse_argument(arg: FnArg) -> Result<Argument> {
 | 
					 | 
				
			||||||
    match arg {
 | 
					 | 
				
			||||||
        FnArg::Typed(typed) => {
 | 
					 | 
				
			||||||
            let pat = typed.pat;
 | 
					 | 
				
			||||||
            let kind = typed.ty;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            match *pat {
 | 
					 | 
				
			||||||
                Pat::Ident(id) => {
 | 
					 | 
				
			||||||
                    let name = id.ident;
 | 
					 | 
				
			||||||
                    let mutable = id.mutability;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    Ok(Argument {
 | 
					 | 
				
			||||||
                        mutable,
 | 
					 | 
				
			||||||
                        name,
 | 
					 | 
				
			||||||
                        kind: *kind,
 | 
					 | 
				
			||||||
                    })
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                Pat::Wild(wild) => {
 | 
					 | 
				
			||||||
                    let token = wild.underscore_token;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    let name = Ident::new("_", token.spans[0]);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    Ok(Argument {
 | 
					 | 
				
			||||||
                        mutable: None,
 | 
					 | 
				
			||||||
                        name,
 | 
					 | 
				
			||||||
                        kind: *kind,
 | 
					 | 
				
			||||||
                    })
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                _ => Err(Error::new(
 | 
					 | 
				
			||||||
                    pat.span(),
 | 
					 | 
				
			||||||
                    format_args!("unsupported pattern: {:?}", pat),
 | 
					 | 
				
			||||||
                )),
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        FnArg::Receiver(_) => Err(Error::new(
 | 
					 | 
				
			||||||
            arg.span(),
 | 
					 | 
				
			||||||
            format_args!("`self` arguments are prohibited: {:?}", arg),
 | 
					 | 
				
			||||||
        )),
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/// Test if the attribute is cooked.
 | 
					 | 
				
			||||||
fn is_cooked(attr: &Attribute) -> bool {
 | 
					 | 
				
			||||||
    const COOKED_ATTRIBUTE_NAMES: &[&str] = &[
 | 
					 | 
				
			||||||
        "cfg", "cfg_attr", "doc", "derive", "inline", "allow", "warn", "deny", "forbid",
 | 
					 | 
				
			||||||
    ];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    COOKED_ATTRIBUTE_NAMES.iter().any(|n| attr.path.is_ident(n))
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/// Removes cooked attributes from a vector of attributes. Uncooked attributes are left in the vector.
 | 
					 | 
				
			||||||
///
 | 
					 | 
				
			||||||
/// # Return
 | 
					 | 
				
			||||||
///
 | 
					 | 
				
			||||||
/// Returns a vector of cooked attributes that have been removed from the input vector.
 | 
					 | 
				
			||||||
fn remove_cooked(attrs: &mut Vec<Attribute>) -> Vec<Attribute> {
 | 
					 | 
				
			||||||
    let mut cooked = Vec::new();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // FIXME: Replace with `Vec::drain_filter` once it is stable.
 | 
					 | 
				
			||||||
    let mut i = 0;
 | 
					 | 
				
			||||||
    while i < attrs.len() {
 | 
					 | 
				
			||||||
        if !is_cooked(&attrs[i]) {
 | 
					 | 
				
			||||||
            i += 1;
 | 
					 | 
				
			||||||
            continue;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        cooked.push(attrs.remove(i));
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    cooked
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Debug)]
 | 
					 | 
				
			||||||
pub struct CommandFun {
 | 
					 | 
				
			||||||
    /// `#[...]`-style attributes.
 | 
					 | 
				
			||||||
    pub attributes: Vec<Attribute>,
 | 
					 | 
				
			||||||
    /// Populated cooked attributes. These are attributes outside of the realm of this crate's procedural macros
 | 
					 | 
				
			||||||
    /// and will appear in generated output.
 | 
					 | 
				
			||||||
    pub cooked: Vec<Attribute>,
 | 
					 | 
				
			||||||
    pub visibility: Visibility,
 | 
					 | 
				
			||||||
    pub name: Ident,
 | 
					 | 
				
			||||||
    pub args: Vec<Argument>,
 | 
					 | 
				
			||||||
    pub body: Vec<Stmt>,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl Parse for CommandFun {
 | 
					 | 
				
			||||||
    fn parse(input: ParseStream<'_>) -> Result<Self> {
 | 
					 | 
				
			||||||
        let mut attributes = input.call(Attribute::parse_outer)?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // `#[doc = "..."]` is a cooked attribute but it is special-cased for commands.
 | 
					 | 
				
			||||||
        for attr in &mut attributes {
 | 
					 | 
				
			||||||
            // Rename documentation comment attributes (`#[doc = "..."]`) to `#[description = "..."]`.
 | 
					 | 
				
			||||||
            if attr.path.is_ident("doc") {
 | 
					 | 
				
			||||||
                attr.path = Path::from(PathSegment::from(Ident::new(
 | 
					 | 
				
			||||||
                    "description",
 | 
					 | 
				
			||||||
                    Span::call_site(),
 | 
					 | 
				
			||||||
                )));
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        let cooked = remove_cooked(&mut attributes);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        let visibility = input.parse::<Visibility>()?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        input.parse::<Token![async]>()?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        input.parse::<Token![fn]>()?;
 | 
					 | 
				
			||||||
        let name = input.parse()?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // (...)
 | 
					 | 
				
			||||||
        let Parenthesised(args) = input.parse::<Parenthesised<FnArg>>()?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // { ... }
 | 
					 | 
				
			||||||
        let bcont;
 | 
					 | 
				
			||||||
        braced!(bcont in input);
 | 
					 | 
				
			||||||
        let body = bcont.call(Block::parse_within)?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        let args = args
 | 
					 | 
				
			||||||
            .into_iter()
 | 
					 | 
				
			||||||
            .map(parse_argument)
 | 
					 | 
				
			||||||
            .collect::<Result<Vec<_>>>()?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        Ok(Self {
 | 
					 | 
				
			||||||
            attributes,
 | 
					 | 
				
			||||||
            cooked,
 | 
					 | 
				
			||||||
            visibility,
 | 
					 | 
				
			||||||
            name,
 | 
					 | 
				
			||||||
            args,
 | 
					 | 
				
			||||||
            body,
 | 
					 | 
				
			||||||
        })
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl ToTokens for CommandFun {
 | 
					 | 
				
			||||||
    fn to_tokens(&self, stream: &mut TokenStream2) {
 | 
					 | 
				
			||||||
        let Self {
 | 
					 | 
				
			||||||
            attributes: _,
 | 
					 | 
				
			||||||
            cooked,
 | 
					 | 
				
			||||||
            visibility,
 | 
					 | 
				
			||||||
            name,
 | 
					 | 
				
			||||||
            args,
 | 
					 | 
				
			||||||
            body,
 | 
					 | 
				
			||||||
        } = self;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        stream.extend(quote! {
 | 
					 | 
				
			||||||
            #(#cooked)*
 | 
					 | 
				
			||||||
            #visibility async fn #name (#(#args),*) -> () {
 | 
					 | 
				
			||||||
                #(#body)*
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Debug)]
 | 
					 | 
				
			||||||
pub enum PermissionLevel {
 | 
					 | 
				
			||||||
    Unrestricted,
 | 
					 | 
				
			||||||
    Managed,
 | 
					 | 
				
			||||||
    Restricted,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl Default for PermissionLevel {
 | 
					 | 
				
			||||||
    fn default() -> Self {
 | 
					 | 
				
			||||||
        Self::Unrestricted
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl PermissionLevel {
 | 
					 | 
				
			||||||
    pub fn from_str(s: &str) -> Option<Self> {
 | 
					 | 
				
			||||||
        Some(match s.to_uppercase().as_str() {
 | 
					 | 
				
			||||||
            "UNRESTRICTED" => Self::Unrestricted,
 | 
					 | 
				
			||||||
            "MANAGED" => Self::Managed,
 | 
					 | 
				
			||||||
            "RESTRICTED" => Self::Restricted,
 | 
					 | 
				
			||||||
            _ => return None,
 | 
					 | 
				
			||||||
        })
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl ToTokens for PermissionLevel {
 | 
					 | 
				
			||||||
    fn to_tokens(&self, stream: &mut TokenStream2) {
 | 
					 | 
				
			||||||
        let path = quote!(crate::framework::PermissionLevel);
 | 
					 | 
				
			||||||
        let variant;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        match self {
 | 
					 | 
				
			||||||
            Self::Unrestricted => {
 | 
					 | 
				
			||||||
                variant = quote!(Unrestricted);
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            Self::Managed => {
 | 
					 | 
				
			||||||
                variant = quote!(Managed);
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            Self::Restricted => {
 | 
					 | 
				
			||||||
                variant = quote!(Restricted);
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        stream.extend(quote! {
 | 
					 | 
				
			||||||
            #path::#variant
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Debug, Default)]
 | 
					 | 
				
			||||||
pub struct Options {
 | 
					 | 
				
			||||||
    pub permission_level: PermissionLevel,
 | 
					 | 
				
			||||||
    pub supports_dm: bool,
 | 
					 | 
				
			||||||
    pub can_blacklist: bool,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl Options {
 | 
					 | 
				
			||||||
    #[inline]
 | 
					 | 
				
			||||||
    pub fn new() -> Self {
 | 
					 | 
				
			||||||
        let mut options = Self::default();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        options.can_blacklist = true;
 | 
					 | 
				
			||||||
        options.supports_dm = true;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        options
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,160 +0,0 @@
 | 
				
			|||||||
use proc_macro::TokenStream;
 | 
					 | 
				
			||||||
use proc_macro2::Span;
 | 
					 | 
				
			||||||
use proc_macro2::TokenStream as TokenStream2;
 | 
					 | 
				
			||||||
use quote::{format_ident, quote, ToTokens};
 | 
					 | 
				
			||||||
use syn::{
 | 
					 | 
				
			||||||
    braced, bracketed, parenthesized,
 | 
					 | 
				
			||||||
    parse::{Error, Parse, ParseStream, Result as SynResult},
 | 
					 | 
				
			||||||
    punctuated::Punctuated,
 | 
					 | 
				
			||||||
    token::{Comma, Mut},
 | 
					 | 
				
			||||||
    Ident, Lifetime, Lit, Type,
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
pub trait LitExt {
 | 
					 | 
				
			||||||
    fn to_str(&self) -> String;
 | 
					 | 
				
			||||||
    fn to_bool(&self) -> bool;
 | 
					 | 
				
			||||||
    fn to_ident(&self) -> Ident;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl LitExt for Lit {
 | 
					 | 
				
			||||||
    fn to_str(&self) -> String {
 | 
					 | 
				
			||||||
        match self {
 | 
					 | 
				
			||||||
            Lit::Str(s) => s.value(),
 | 
					 | 
				
			||||||
            Lit::ByteStr(s) => unsafe { String::from_utf8_unchecked(s.value()) },
 | 
					 | 
				
			||||||
            Lit::Char(c) => c.value().to_string(),
 | 
					 | 
				
			||||||
            Lit::Byte(b) => (b.value() as char).to_string(),
 | 
					 | 
				
			||||||
            _ => panic!("values must be a (byte)string or a char"),
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    fn to_bool(&self) -> bool {
 | 
					 | 
				
			||||||
        if let Lit::Bool(b) = self {
 | 
					 | 
				
			||||||
            b.value
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
            self.to_str()
 | 
					 | 
				
			||||||
                .parse()
 | 
					 | 
				
			||||||
                .unwrap_or_else(|_| panic!("expected bool from {:?}", self))
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    #[inline]
 | 
					 | 
				
			||||||
    fn to_ident(&self) -> Ident {
 | 
					 | 
				
			||||||
        Ident::new(&self.to_str(), self.span())
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
pub trait IdentExt2: Sized {
 | 
					 | 
				
			||||||
    fn to_uppercase(&self) -> Self;
 | 
					 | 
				
			||||||
    fn with_suffix(&self, suf: &str) -> Ident;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl IdentExt2 for Ident {
 | 
					 | 
				
			||||||
    #[inline]
 | 
					 | 
				
			||||||
    fn to_uppercase(&self) -> Self {
 | 
					 | 
				
			||||||
        format_ident!("{}", self.to_string().to_uppercase())
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    #[inline]
 | 
					 | 
				
			||||||
    fn with_suffix(&self, suffix: &str) -> Ident {
 | 
					 | 
				
			||||||
        format_ident!("{}_{}", self.to_string().to_uppercase(), suffix)
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[inline]
 | 
					 | 
				
			||||||
pub fn into_stream(e: Error) -> TokenStream {
 | 
					 | 
				
			||||||
    e.to_compile_error().into()
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
macro_rules! propagate_err {
 | 
					 | 
				
			||||||
    ($res:expr) => {{
 | 
					 | 
				
			||||||
        match $res {
 | 
					 | 
				
			||||||
            Ok(v) => v,
 | 
					 | 
				
			||||||
            Err(e) => return $crate::util::into_stream(e),
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }};
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Debug)]
 | 
					 | 
				
			||||||
pub struct Bracketed<T>(pub Punctuated<T, Comma>);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl<T: Parse> Parse for Bracketed<T> {
 | 
					 | 
				
			||||||
    fn parse(input: ParseStream<'_>) -> SynResult<Self> {
 | 
					 | 
				
			||||||
        let content;
 | 
					 | 
				
			||||||
        bracketed!(content in input);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        Ok(Bracketed(content.parse_terminated(T::parse)?))
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Debug)]
 | 
					 | 
				
			||||||
pub struct Braced<T>(pub Punctuated<T, Comma>);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl<T: Parse> Parse for Braced<T> {
 | 
					 | 
				
			||||||
    fn parse(input: ParseStream<'_>) -> SynResult<Self> {
 | 
					 | 
				
			||||||
        let content;
 | 
					 | 
				
			||||||
        braced!(content in input);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        Ok(Braced(content.parse_terminated(T::parse)?))
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Debug)]
 | 
					 | 
				
			||||||
pub struct Parenthesised<T>(pub Punctuated<T, Comma>);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl<T: Parse> Parse for Parenthesised<T> {
 | 
					 | 
				
			||||||
    fn parse(input: ParseStream<'_>) -> SynResult<Self> {
 | 
					 | 
				
			||||||
        let content;
 | 
					 | 
				
			||||||
        parenthesized!(content in input);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        Ok(Parenthesised(content.parse_terminated(T::parse)?))
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Debug)]
 | 
					 | 
				
			||||||
pub struct AsOption<T>(pub Option<T>);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl<T: ToTokens> ToTokens for AsOption<T> {
 | 
					 | 
				
			||||||
    fn to_tokens(&self, stream: &mut TokenStream2) {
 | 
					 | 
				
			||||||
        match &self.0 {
 | 
					 | 
				
			||||||
            Some(o) => stream.extend(quote!(Some(#o))),
 | 
					 | 
				
			||||||
            None => stream.extend(quote!(None)),
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl<T> Default for AsOption<T> {
 | 
					 | 
				
			||||||
    #[inline]
 | 
					 | 
				
			||||||
    fn default() -> Self {
 | 
					 | 
				
			||||||
        AsOption(None)
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Debug)]
 | 
					 | 
				
			||||||
pub struct Argument {
 | 
					 | 
				
			||||||
    pub mutable: Option<Mut>,
 | 
					 | 
				
			||||||
    pub name: Ident,
 | 
					 | 
				
			||||||
    pub kind: Type,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl ToTokens for Argument {
 | 
					 | 
				
			||||||
    fn to_tokens(&self, stream: &mut TokenStream2) {
 | 
					 | 
				
			||||||
        let Argument {
 | 
					 | 
				
			||||||
            mutable,
 | 
					 | 
				
			||||||
            name,
 | 
					 | 
				
			||||||
            kind,
 | 
					 | 
				
			||||||
        } = self;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        stream.extend(quote! {
 | 
					 | 
				
			||||||
            #mutable #name: #kind
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[inline]
 | 
					 | 
				
			||||||
pub fn populate_fut_lifetimes_on_refs(args: &mut Vec<Argument>) {
 | 
					 | 
				
			||||||
    for arg in args {
 | 
					 | 
				
			||||||
        if let Type::Reference(reference) = &mut arg.kind {
 | 
					 | 
				
			||||||
            reference.lifetime = Some(Lifetime::new("'fut", Span::call_site()));
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
							
								
								
									
										2
									
								
								reminder-dashboard/.prettierrc.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,2 @@
 | 
				
			|||||||
 | 
					printWidth = 100
 | 
				
			||||||
 | 
					tabWidth = 4
 | 
				
			||||||
							
								
								
									
										19
									
								
								reminder-dashboard/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,19 @@
 | 
				
			|||||||
 | 
					# reminder-dashboard
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					The re-re-rewrite of the dashboard.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Why
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					The existing beta variant of the dashboard is written using vanilla JavaScript. This is fine,
 | 
				
			||||||
 | 
					but annoying to update. This would've been okay if I was more dedicated to updating the vanilla
 | 
				
			||||||
 | 
					JavaScript too, but I want to experiment with "new" things.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					This also allows me to expand my frontend skills, which is relevant to part of my job.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Developing
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					1. Run both `npm run dev` and `cargo run`
 | 
				
			||||||
 | 
					2. Symlink assets: assuming cloned
 | 
				
			||||||
 | 
					   into `$HOME`, `ln -s $HOME/reminder-bot/reminder-dashboard/dist/index.html $HOME/reminder-bot/web/static/index.html`
 | 
				
			||||||
 | 
					   and
 | 
				
			||||||
 | 
					   `ln -s $HOME/reminder-bot/reminder-dashboard/dist/static/assets $HOME/reminder-bot/web/static/assets`
 | 
				
			||||||
							
								
								
									
										34
									
								
								reminder-dashboard/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,34 @@
 | 
				
			|||||||
 | 
					<!DOCTYPE html>
 | 
				
			||||||
 | 
					<html lang="EN">
 | 
				
			||||||
 | 
					<head>
 | 
				
			||||||
 | 
						<meta name="description" content="The most powerful Discord Reminders Bot">
 | 
				
			||||||
 | 
						<meta name="viewport" content="width=device-width, initial-scale=1.0">
 | 
				
			||||||
 | 
						<meta charset="UTF-8">
 | 
				
			||||||
 | 
						<meta name="yandex-verification" content="bb77b8681eb64a90"/>
 | 
				
			||||||
 | 
						<meta name="google-site-verification" content="7h7UVTeEe0AOzHiH3cFtsqMULYGN-zCZdMT_YCkW1Ho"/>
 | 
				
			||||||
 | 
						<!-- <meta http-equiv="Content-Security-Policy" content="default-src 'self'; img-src *; font-src fonts.gstatic.com 'self'"> -->
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						<!-- favicon -->
 | 
				
			||||||
 | 
						<link rel="apple-touch-icon" sizes="180x180"
 | 
				
			||||||
 | 
							  href="/static/favicon/apple-touch-icon.png">
 | 
				
			||||||
 | 
						<link rel="icon" type="image/png" sizes="32x32"
 | 
				
			||||||
 | 
							  href="/static/favicon/favicon-32x32.png">
 | 
				
			||||||
 | 
						<link rel="icon" type="image/png" sizes="16x16"
 | 
				
			||||||
 | 
							  href="/static/favicon/favicon-16x16.png">
 | 
				
			||||||
 | 
						<link rel="manifest" href="/static/site.webmanifest">
 | 
				
			||||||
 | 
						<meta name="msapplication-TileColor" content="#da532c">
 | 
				
			||||||
 | 
						<meta name="theme-color" content="#ffffff">
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						<title>Reminder Bot | Dashboard</title>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						<!-- styles -->
 | 
				
			||||||
 | 
						<link rel="stylesheet" href="/static/css/bulma.min.css">
 | 
				
			||||||
 | 
						<link rel="stylesheet" href="/static/css/fa.css">
 | 
				
			||||||
 | 
						<link rel="stylesheet" href="/static/css/font.css">
 | 
				
			||||||
 | 
						<link rel="stylesheet" href="/static/css/style.css">
 | 
				
			||||||
 | 
					</head>
 | 
				
			||||||
 | 
					<body>
 | 
				
			||||||
 | 
						<div id="app"></div>
 | 
				
			||||||
 | 
						<script type="module" src="/src/index.tsx"></script>
 | 
				
			||||||
 | 
					</body>
 | 
				
			||||||
 | 
					</html>
 | 
				
			||||||
							
								
								
									
										5626
									
								
								reminder-dashboard/package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										32
									
								
								reminder-dashboard/package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,32 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
						"name": "example",
 | 
				
			||||||
 | 
						"private": true,
 | 
				
			||||||
 | 
						"type": "module",
 | 
				
			||||||
 | 
						"scripts": {
 | 
				
			||||||
 | 
							"dev": "vite build --watch --mode development",
 | 
				
			||||||
 | 
							"build": "vite build",
 | 
				
			||||||
 | 
							"preview": "vite preview"
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						"dependencies": {
 | 
				
			||||||
 | 
							"axios": "^1.5.1",
 | 
				
			||||||
 | 
							"bulma": "^0.9.4",
 | 
				
			||||||
 | 
							"luxon": "^3.4.3",
 | 
				
			||||||
 | 
							"preact": "^10.13.1",
 | 
				
			||||||
 | 
							"react-colorful": "^5.6.1",
 | 
				
			||||||
 | 
							"react-query": "^3.39.3",
 | 
				
			||||||
 | 
							"tributejs": "^5.1.3",
 | 
				
			||||||
 | 
							"use-debounce": "^10.0.0",
 | 
				
			||||||
 | 
							"wouter": "^3.0"
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						"devDependencies": {
 | 
				
			||||||
 | 
							"@preact/preset-vite": "^2.5.0",
 | 
				
			||||||
 | 
							"@types/luxon": "^3.3.2",
 | 
				
			||||||
 | 
							"eslint": "^8.50.0",
 | 
				
			||||||
 | 
							"eslint-config-preact": "^1.3.0",
 | 
				
			||||||
 | 
							"prettier": "^3.0.3",
 | 
				
			||||||
 | 
							"react-datepicker": "^4.21.0",
 | 
				
			||||||
 | 
							"sass": "^1.71.1",
 | 
				
			||||||
 | 
							"typescript": "^5.2.2",
 | 
				
			||||||
 | 
							"vite": "^5.1"
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										1
									
								
								reminder-dashboard/public/static/css/bulma.min.css
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										91
									
								
								reminder-dashboard/public/static/css/dtsel.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,91 @@
 | 
				
			|||||||
 | 
					.date-selector-wrapper {
 | 
				
			||||||
 | 
					    width: 200px;
 | 
				
			||||||
 | 
					    padding: 3px;
 | 
				
			||||||
 | 
					    background-color: #fff;
 | 
				
			||||||
 | 
					    box-shadow: 1px 1px 10px 1px #5c5c5c;
 | 
				
			||||||
 | 
					    position: absolute;
 | 
				
			||||||
 | 
					    font-size: 12px;
 | 
				
			||||||
 | 
					    -webkit-user-select: none;
 | 
				
			||||||
 | 
					    -khtml-user-select: none;
 | 
				
			||||||
 | 
					    -moz-user-select: none;
 | 
				
			||||||
 | 
					    -ms-user-select: none;
 | 
				
			||||||
 | 
					    -o-user-select: none;
 | 
				
			||||||
 | 
					    /* user-select: none; */
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.cal-header, .cal-row {
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    width: 100%;
 | 
				
			||||||
 | 
					    height: 30px;
 | 
				
			||||||
 | 
					    line-height: 30px;
 | 
				
			||||||
 | 
					    text-align: center;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.cal-cell, .cal-nav {
 | 
				
			||||||
 | 
					    cursor: pointer;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.cal-day-names {
 | 
				
			||||||
 | 
					    height: 25px;
 | 
				
			||||||
 | 
					    line-height: 25px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.cal-day-names .cal-cell {
 | 
				
			||||||
 | 
					    cursor: default;
 | 
				
			||||||
 | 
					    font-weight: bold;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.cal-cell-prev, .cal-cell-next {
 | 
				
			||||||
 | 
					    color: #777;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.cal-months .cal-row, .cal-years .cal-row {
 | 
				
			||||||
 | 
					    height: 60px;
 | 
				
			||||||
 | 
					    line-height: 60px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.cal-nav-prev, .cal-nav-next {
 | 
				
			||||||
 | 
					    flex: 0.15;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.cal-nav-current {
 | 
				
			||||||
 | 
					    flex: 0.75;
 | 
				
			||||||
 | 
					    font-weight: bold;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.cal-months .cal-cell, .cal-years .cal-cell {
 | 
				
			||||||
 | 
					    flex: 0.25;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.cal-days .cal-cell {
 | 
				
			||||||
 | 
					    flex: 0.143;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.cal-value {
 | 
				
			||||||
 | 
					    color: #fff;
 | 
				
			||||||
 | 
					    background-color: #286090;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.cal-cell:hover, .cal-nav:hover {
 | 
				
			||||||
 | 
					    background-color: #eee;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.cal-value:hover {
 | 
				
			||||||
 | 
					    background-color: #204d74;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* time footer */
 | 
				
			||||||
 | 
					.cal-time {
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    justify-content: flex-start;
 | 
				
			||||||
 | 
					    height: 27px;
 | 
				
			||||||
 | 
					    line-height: 27px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.cal-time-label, .cal-time-value {
 | 
				
			||||||
 | 
					    flex: 0.12;
 | 
				
			||||||
 | 
					    text-align: center;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.cal-time-slider {
 | 
				
			||||||
 | 
					    flex: 0.77;
 | 
				
			||||||
 | 
					    background-image: linear-gradient(to right, #d1d8dd, #d1d8dd);
 | 
				
			||||||
 | 
					    background-repeat: no-repeat;
 | 
				
			||||||
 | 
					    background-size: 100% 1px;
 | 
				
			||||||
 | 
					    background-position: left 50%;
 | 
				
			||||||
 | 
					    height: 100%;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.cal-time-slider input {
 | 
				
			||||||
 | 
					    width: 100%;
 | 
				
			||||||
 | 
					    -webkit-appearance: none;
 | 
				
			||||||
 | 
					    background: 0 0;
 | 
				
			||||||
 | 
					    cursor: pointer;
 | 
				
			||||||
 | 
					    height: 100%;
 | 
				
			||||||
 | 
					    outline: 0;
 | 
				
			||||||
 | 
					    user-select: auto;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										12749
									
								
								reminder-dashboard/public/static/css/fa.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										63
									
								
								reminder-dashboard/public/static/css/font.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,63 @@
 | 
				
			|||||||
 | 
					@font-face {
 | 
				
			||||||
 | 
					  font-family: 'Source Sans Pro';
 | 
				
			||||||
 | 
					  font-style: italic;
 | 
				
			||||||
 | 
					  font-weight: 300;
 | 
				
			||||||
 | 
					  src: local('Source Sans Pro Light Italic'), local('SourceSansPro-LightItalic'), url(https://fonts.gstatic.com/s/sourcesanspro/v13/6xKwdSBYKcSV-LCoeQqfX1RYOo3qPZZMkids18E.ttf) format('truetype');
 | 
				
			||||||
 | 
					  font-display: swap;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					@font-face {
 | 
				
			||||||
 | 
					  font-family: 'Source Sans Pro';
 | 
				
			||||||
 | 
					  font-style: italic;
 | 
				
			||||||
 | 
					  font-weight: 400;
 | 
				
			||||||
 | 
					  src: local('Source Sans Pro Italic'), local('SourceSansPro-Italic'), url(https://fonts.gstatic.com/s/sourcesanspro/v13/6xK1dSBYKcSV-LCoeQqfX1RYOo3qPZ7nsDc.ttf) format('truetype');
 | 
				
			||||||
 | 
					  font-display: swap;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					@font-face {
 | 
				
			||||||
 | 
					  font-family: 'Source Sans Pro';
 | 
				
			||||||
 | 
					  font-style: italic;
 | 
				
			||||||
 | 
					  font-weight: 600;
 | 
				
			||||||
 | 
					  src: local('Source Sans Pro SemiBold Italic'), local('SourceSansPro-SemiBoldItalic'), url(https://fonts.gstatic.com/s/sourcesanspro/v13/6xKwdSBYKcSV-LCoeQqfX1RYOo3qPZY4lCds18E.ttf) format('truetype');
 | 
				
			||||||
 | 
					  font-display: swap;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					@font-face {
 | 
				
			||||||
 | 
					  font-family: 'Source Sans Pro';
 | 
				
			||||||
 | 
					  font-style: normal;
 | 
				
			||||||
 | 
					  font-weight: 300;
 | 
				
			||||||
 | 
					  src: local('Source Sans Pro Light'), local('SourceSansPro-Light'), url(https://fonts.gstatic.com/s/sourcesanspro/v13/6xKydSBYKcSV-LCoeQqfX1RYOo3ik4zwlxdr.ttf) format('truetype');
 | 
				
			||||||
 | 
					  font-display: swap;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					@font-face {
 | 
				
			||||||
 | 
					  font-family: 'Source Sans Pro';
 | 
				
			||||||
 | 
					  font-style: normal;
 | 
				
			||||||
 | 
					  font-weight: 400;
 | 
				
			||||||
 | 
					  src: local('Source Sans Pro Regular'), local('SourceSansPro-Regular'), url(https://fonts.gstatic.com/s/sourcesanspro/v13/6xK3dSBYKcSV-LCoeQqfX1RYOo3qOK7g.ttf) format('truetype');
 | 
				
			||||||
 | 
					  font-display: swap;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					@font-face {
 | 
				
			||||||
 | 
					  font-family: 'Source Sans Pro';
 | 
				
			||||||
 | 
					  font-style: normal;
 | 
				
			||||||
 | 
					  font-weight: 600;
 | 
				
			||||||
 | 
					  src: local('Source Sans Pro SemiBold'), local('SourceSansPro-SemiBold'), url(https://fonts.gstatic.com/s/sourcesanspro/v13/6xKydSBYKcSV-LCoeQqfX1RYOo3i54rwlxdr.ttf) format('truetype');
 | 
				
			||||||
 | 
					  font-display: swap;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					@font-face {
 | 
				
			||||||
 | 
					  font-family: 'Source Sans Pro';
 | 
				
			||||||
 | 
					  font-style: normal;
 | 
				
			||||||
 | 
					  font-weight: 700;
 | 
				
			||||||
 | 
					  src: local('Source Sans Pro Bold'), local('SourceSansPro-Bold'), url(https://fonts.gstatic.com/s/sourcesanspro/v13/6xKydSBYKcSV-LCoeQqfX1RYOo3ig4vwlxdr.ttf) format('truetype');
 | 
				
			||||||
 | 
					  font-display: swap;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					@font-face {
 | 
				
			||||||
 | 
					  font-family: 'Ubuntu';
 | 
				
			||||||
 | 
					  font-style: normal;
 | 
				
			||||||
 | 
					  font-weight: 400;
 | 
				
			||||||
 | 
					  src: url(https://fonts.gstatic.com/s/ubuntu/v15/4iCs6KVjbNBYlgo6eA.ttf) format('truetype');
 | 
				
			||||||
 | 
					  font-display: swap;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					@font-face {
 | 
				
			||||||
 | 
					  font-family: 'Ubuntu';
 | 
				
			||||||
 | 
					  font-style: normal;
 | 
				
			||||||
 | 
					  font-weight: 700;
 | 
				
			||||||
 | 
					  src: url(https://fonts.gstatic.com/s/ubuntu/v15/4iCv6KVjbNBYlgoCxCvTtw.ttf) format('truetype');
 | 
				
			||||||
 | 
					  font-display: swap;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										888
									
								
								reminder-dashboard/public/static/css/style.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,888 @@
 | 
				
			|||||||
 | 
					* {
 | 
				
			||||||
 | 
					    font-family: "Ubuntu Bold", "Ubuntu", sans-serif;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					button {
 | 
				
			||||||
 | 
					    font-weight: 700;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* override styles for when the div is collapsed */
 | 
				
			||||||
 | 
					div.reminderContent.is-collapsed .column.discord-frame {
 | 
				
			||||||
 | 
					    display: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					div.reminderContent.is-collapsed .column.settings {
 | 
				
			||||||
 | 
					    display: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					div.reminderContent.is-collapsed .reminder-settings {
 | 
				
			||||||
 | 
					    margin-bottom: 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					div.reminderContent.is-collapsed .button-row {
 | 
				
			||||||
 | 
					    display: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					div.reminderContent.is-collapsed .button-row-edit {
 | 
				
			||||||
 | 
					    display: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					div.reminderContent.is-collapsed .reminder-topbar {
 | 
				
			||||||
 | 
					    padding-bottom: 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					div.reminderContent.is-collapsed .invert-collapses {
 | 
				
			||||||
 | 
					    display: inline-flex;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					div.reminderContent .invert-collapses {
 | 
				
			||||||
 | 
					    display: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					div.reminderContent.is-collapsed input[name="name"] {
 | 
				
			||||||
 | 
					    display: inline-flex;
 | 
				
			||||||
 | 
					    flex-grow: 1;
 | 
				
			||||||
 | 
					    border: none;
 | 
				
			||||||
 | 
					    background: none;
 | 
				
			||||||
 | 
					    box-shadow: none;
 | 
				
			||||||
 | 
					    opacity: 1;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					div.reminderContent.is-collapsed .hide-box {
 | 
				
			||||||
 | 
					    display: inline-flex;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					div.reminderContent.is-collapsed .hide-box i {
 | 
				
			||||||
 | 
					    transform: rotate(90deg);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					/* END */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* dashboard styles */
 | 
				
			||||||
 | 
					.hide-box {
 | 
				
			||||||
 | 
					    border: none;
 | 
				
			||||||
 | 
					    background: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.hide-box:focus {
 | 
				
			||||||
 | 
					    outline: none;
 | 
				
			||||||
 | 
					    box-shadow: none !important;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.channel-bar {
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    justify-content: center;
 | 
				
			||||||
 | 
					    flex-direction: column;
 | 
				
			||||||
 | 
					    font-weight: bold;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					button.inline-btn {
 | 
				
			||||||
 | 
					    height: 100%;
 | 
				
			||||||
 | 
					    padding: 5px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					button.change-color {
 | 
				
			||||||
 | 
					    position: absolute;
 | 
				
			||||||
 | 
					    left: calc(-1rem - 40px);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					button.disable-enable[data-action="enable"]:after {
 | 
				
			||||||
 | 
					    content: "Enable";
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					button.disable-enable[data-action="disable"]:after {
 | 
				
			||||||
 | 
					    content: "Disable";
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.media-content {
 | 
				
			||||||
 | 
					    overflow-x: visible;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					div.discord-embed {
 | 
				
			||||||
 | 
					    position: relative;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					div.split-controls {
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    flex-direction: column;
 | 
				
			||||||
 | 
					    justify-content: space-between;
 | 
				
			||||||
 | 
					    flex-grow: 2;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.reminder-topbar > div {
 | 
				
			||||||
 | 
					    padding-left: 6px;
 | 
				
			||||||
 | 
					    padding-right: 6px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.settings {
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    flex-direction: column;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.name-bar {
 | 
				
			||||||
 | 
					    flex-grow: 1;
 | 
				
			||||||
 | 
					    flex-shrink: 1;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.hide-button-bar {
 | 
				
			||||||
 | 
					    flex-grow: 0;
 | 
				
			||||||
 | 
					    flex-shrink: 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.patreon-only {
 | 
				
			||||||
 | 
					    padding-bottom: 16px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.tts-row {
 | 
				
			||||||
 | 
					    padding-bottom: 10px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.reminder-topbar {
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    margin-bottom: 0 !important;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.reminder-settings {
 | 
				
			||||||
 | 
					    margin-top: 0 !important;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.reminder-settings > .column {
 | 
				
			||||||
 | 
					    flex-grow: 0;
 | 
				
			||||||
 | 
					    flex-shrink: 0;
 | 
				
			||||||
 | 
					    flex-basis: 50%;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					div.reminderContent {
 | 
				
			||||||
 | 
					    margin-top: 10px;
 | 
				
			||||||
 | 
					    margin-bottom: 10px;
 | 
				
			||||||
 | 
					    padding: 14px;
 | 
				
			||||||
 | 
					    background-color: #f5f5f5;
 | 
				
			||||||
 | 
					    border-radius: 8px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* Interval inputs */
 | 
				
			||||||
 | 
					div.interval-group {
 | 
				
			||||||
 | 
					    height: unset !important;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					div.interval-group .clear:focus {
 | 
				
			||||||
 | 
					    outline: none;
 | 
				
			||||||
 | 
					    box-shadow: none !important;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					div.interval-group .no-break {
 | 
				
			||||||
 | 
					    text-wrap: avoid;
 | 
				
			||||||
 | 
					    white-space: nowrap;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					div.interval-group .clear {
 | 
				
			||||||
 | 
					    border: none;
 | 
				
			||||||
 | 
					    background: none;
 | 
				
			||||||
 | 
					    padding: 1px;
 | 
				
			||||||
 | 
					    margin-right: -3px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					div.interval-group > .interval-group-left input {
 | 
				
			||||||
 | 
					    -webkit-appearance: none;
 | 
				
			||||||
 | 
					    border-style: none;
 | 
				
			||||||
 | 
					    background-color: #eee;
 | 
				
			||||||
 | 
					    font-size: 1rem;
 | 
				
			||||||
 | 
					    font-family: monospace;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					div.interval-group > .interval-group-left input.w2 {
 | 
				
			||||||
 | 
					    width: 3ch;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					div.interval-group > .interval-group-left input.w3 {
 | 
				
			||||||
 | 
					    width: 3ch;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					div.interval-group {
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    flex-direction: row;
 | 
				
			||||||
 | 
					    justify-content: space-between;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					/* !Interval inputs */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.left-pad {
 | 
				
			||||||
 | 
					    padding-left: 1rem;
 | 
				
			||||||
 | 
					    padding-right: 0.2rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.notification {
 | 
				
			||||||
 | 
					    padding-right: 1.5rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					div.inset-content {
 | 
				
			||||||
 | 
					    margin-left: 10%;
 | 
				
			||||||
 | 
					    margin-right: 10%;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					div.flash-message {
 | 
				
			||||||
 | 
					    position: fixed;
 | 
				
			||||||
 | 
					    width: calc(100% - 32px);
 | 
				
			||||||
 | 
					    margin: 16px !important;
 | 
				
			||||||
 | 
					    z-index: 99;
 | 
				
			||||||
 | 
					    bottom: 0;
 | 
				
			||||||
 | 
					    display: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					div.flash-message.is-active {
 | 
				
			||||||
 | 
					    display: block;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					body {
 | 
				
			||||||
 | 
					    min-height: 100vh;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					span.spacer {
 | 
				
			||||||
 | 
					    width: 10px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					nav .dashboard-button {
 | 
				
			||||||
 | 
					    background: white ;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					span.patreon-color {
 | 
				
			||||||
 | 
					    color: #f96854;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					p.pageTitle {
 | 
				
			||||||
 | 
					    margin-left: 12px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#welcome > div {
 | 
				
			||||||
 | 
					    height: 100%;
 | 
				
			||||||
 | 
					    padding-top: 30vh;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					div#pageNavbar {
 | 
				
			||||||
 | 
					    background-color: #363636;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					div#pageNavbar a {
 | 
				
			||||||
 | 
					    color: #fff;
 | 
				
			||||||
 | 
					    text-align: center;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.navbar-burger {
 | 
				
			||||||
 | 
					    flex-shrink: 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.navbar-item.pageTitle {
 | 
				
			||||||
 | 
					    flex-shrink: 1;
 | 
				
			||||||
 | 
					    white-space: nowrap;
 | 
				
			||||||
 | 
					    overflow: hidden;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.dashboard-burger, .dashboard-burger:active, .dashboard-burger.is-active {
 | 
				
			||||||
 | 
					    background-color: #adc99c !important;
 | 
				
			||||||
 | 
					    border-radius: 14px;
 | 
				
			||||||
 | 
					    padding: 6px;
 | 
				
			||||||
 | 
					    background-clip: content-box;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					div#pageNavbar a:hover {
 | 
				
			||||||
 | 
					    background-color: #4a4a4a;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					img.rounded-corners {
 | 
				
			||||||
 | 
					    border-radius: 12px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					div.brand {
 | 
				
			||||||
 | 
					    text-align: center;
 | 
				
			||||||
 | 
					    height: 52px;
 | 
				
			||||||
 | 
					    background-color: #8fb677;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					img.dashboard-brand {
 | 
				
			||||||
 | 
					    text-align: center;
 | 
				
			||||||
 | 
					    height: 100%;
 | 
				
			||||||
 | 
					    width: auto;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					div.dashboard-sidebar {
 | 
				
			||||||
 | 
					    background-color: #363636;
 | 
				
			||||||
 | 
					    width: 230px !important;
 | 
				
			||||||
 | 
					    padding-right: 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ul.guildList {
 | 
				
			||||||
 | 
					    flex-grow: 1;
 | 
				
			||||||
 | 
					    flex-shrink: 1;
 | 
				
			||||||
 | 
					    overflow: auto;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					div.dashboard-sidebar:not(.mobile-sidebar) .aside-footer {
 | 
				
			||||||
 | 
					    flex-shrink: 0;
 | 
				
			||||||
 | 
					    flex-grow: 0;
 | 
				
			||||||
 | 
					    position: fixed;
 | 
				
			||||||
 | 
					    bottom: 0;
 | 
				
			||||||
 | 
					    width: 226px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					div.dashboard-sidebar svg {
 | 
				
			||||||
 | 
					    flex-shrink: 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					div.mobile-sidebar {
 | 
				
			||||||
 | 
					    z-index: 100;
 | 
				
			||||||
 | 
					    min-height: 100vh;
 | 
				
			||||||
 | 
					    position: absolute;
 | 
				
			||||||
 | 
					    top: 0;
 | 
				
			||||||
 | 
					    display: none;
 | 
				
			||||||
 | 
					    flex-direction: column;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#expandAll {
 | 
				
			||||||
 | 
					    width: 60px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					div.mobile-sidebar .aside-footer {
 | 
				
			||||||
 | 
					    margin-top: auto;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					div.mobile-sidebar.is-active {
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					aside.menu {
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    flex-direction: column;
 | 
				
			||||||
 | 
					    flex-grow: 1;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					div.dashboard-frame {
 | 
				
			||||||
 | 
					    min-height: 100vh;
 | 
				
			||||||
 | 
					    margin-bottom: 0 !important;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.embed-field-box[data-inlined="0"] .inline-btn > i {
 | 
				
			||||||
 | 
					    transform: rotate(90deg);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.embed-field-box[data-inlined="0"] {
 | 
				
			||||||
 | 
					    min-width: 100%;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.embed-field-box[data-inlined="1"] {
 | 
				
			||||||
 | 
					    min-width: auto;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.menu a {
 | 
				
			||||||
 | 
					    color: #fff;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.menu .menu-label {
 | 
				
			||||||
 | 
					    color: #bbb;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.menu {
 | 
				
			||||||
 | 
					    padding-left: 4px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.dashboard-navbar {
 | 
				
			||||||
 | 
					    background-color: #8fb677 !important;
 | 
				
			||||||
 | 
					    position: absolute;
 | 
				
			||||||
 | 
					    top: 0;
 | 
				
			||||||
 | 
					    width: 100%;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					textarea.autoresize {
 | 
				
			||||||
 | 
					    resize: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					textarea, input {
 | 
				
			||||||
 | 
					    width: 100%;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					input.default-width {
 | 
				
			||||||
 | 
					    width: initial;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.message-input:placeholder-shown {
 | 
				
			||||||
 | 
					    font-style: italic;
 | 
				
			||||||
 | 
					    background-color: #40444b;
 | 
				
			||||||
 | 
					    color: #fff;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.message-input {
 | 
				
			||||||
 | 
					    border: none;
 | 
				
			||||||
 | 
					    background-color: rgba(0, 0, 0, 0);
 | 
				
			||||||
 | 
					    color: #fff;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.time-input {
 | 
				
			||||||
 | 
					    border-top: none;
 | 
				
			||||||
 | 
					    border-left: none;
 | 
				
			||||||
 | 
					    border-right: none;
 | 
				
			||||||
 | 
					    border-bottom-style: solid;
 | 
				
			||||||
 | 
					    background-color: #40444b;
 | 
				
			||||||
 | 
					    color: #fff;
 | 
				
			||||||
 | 
					    width: 120px;
 | 
				
			||||||
 | 
					    font-size: 0.875rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.message-input::placeholder {
 | 
				
			||||||
 | 
					    color: #72767b;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.discord-title {
 | 
				
			||||||
 | 
					    font-weight: bold;
 | 
				
			||||||
 | 
					    font-size: 1rem;
 | 
				
			||||||
 | 
					    margin: 4px 0 4px 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.discord-description {
 | 
				
			||||||
 | 
					    font-size: 0.875rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.discord-username {
 | 
				
			||||||
 | 
					    font-size: 1rem;
 | 
				
			||||||
 | 
					    font-weight: bold;
 | 
				
			||||||
 | 
					    margin-bottom: 4px;
 | 
				
			||||||
 | 
					    width: initial;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.discord-message-header {
 | 
				
			||||||
 | 
					    white-space: nowrap;
 | 
				
			||||||
 | 
					    margin-bottom: 8px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.discord-content {
 | 
				
			||||||
 | 
					    margin-bottom: 8px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.customizable img {
 | 
				
			||||||
 | 
					    background-color: #72767b;
 | 
				
			||||||
 | 
					    border-radius: 8px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.customizable.is-20x20 img {
 | 
				
			||||||
 | 
					    width: 20px;
 | 
				
			||||||
 | 
					    height: 20px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.customizable.is-24x24 img {
 | 
				
			||||||
 | 
					    width: 24px;
 | 
				
			||||||
 | 
					    height: 24px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.customizable.is-400x300 img {
 | 
				
			||||||
 | 
					    margin-top: 10px;
 | 
				
			||||||
 | 
					    width: 100%;
 | 
				
			||||||
 | 
					    height: 100px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.customizable.is-32x32 img {
 | 
				
			||||||
 | 
					    width: 32px;
 | 
				
			||||||
 | 
					    height: 32px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.customizable.thumbnail img {
 | 
				
			||||||
 | 
					    width: 100px;
 | 
				
			||||||
 | 
					    height: 100px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.customizable input.imageInput {
 | 
				
			||||||
 | 
					    display: none;
 | 
				
			||||||
 | 
					    position: absolute;
 | 
				
			||||||
 | 
					    top: 0;
 | 
				
			||||||
 | 
					    left: 36px;
 | 
				
			||||||
 | 
					    width: 400px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.customizable.thumbnail input.imageInput {
 | 
				
			||||||
 | 
					    display: none;
 | 
				
			||||||
 | 
					    position: absolute;
 | 
				
			||||||
 | 
					    top: 0;
 | 
				
			||||||
 | 
					    left: -400px;
 | 
				
			||||||
 | 
					    width: 400px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.customizable input.is-active {
 | 
				
			||||||
 | 
					    display: block !important;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.discord-frame {
 | 
				
			||||||
 | 
					    color: #fff;
 | 
				
			||||||
 | 
					    padding: 10px;
 | 
				
			||||||
 | 
					    border-radius: 8px;
 | 
				
			||||||
 | 
					    background-color: #36393f;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.discord-embed {
 | 
				
			||||||
 | 
					    padding: 8px 16px 16px 12px;
 | 
				
			||||||
 | 
					    margin: 0 20px 4px 0;
 | 
				
			||||||
 | 
					    border-radius: 4px;
 | 
				
			||||||
 | 
					    border-left: 4px solid #fff;
 | 
				
			||||||
 | 
					    background-color: #2f3136;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.embed-author-box {
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    align-items: center;
 | 
				
			||||||
 | 
					    margin-bottom: 8px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.embed-author-box > .a {
 | 
				
			||||||
 | 
					    flex: initial;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.embed-author-box > .b {
 | 
				
			||||||
 | 
					    flex: auto;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.embed-footer-box {
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    align-items: center;
 | 
				
			||||||
 | 
					    margin-bottom: 8px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.embed-author-box .image {
 | 
				
			||||||
 | 
					    margin: 0 8px 0 0 !important;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.embed-footer-box .image {
 | 
				
			||||||
 | 
					    margin: 0 8px 0 0 !important;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.discord-embed-author {
 | 
				
			||||||
 | 
					    display: inline-block;
 | 
				
			||||||
 | 
					    font-size: 0.875rem;
 | 
				
			||||||
 | 
					    font-weight: bold;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.discord-embed-footer {
 | 
				
			||||||
 | 
					    font-size: 0.75rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.embed-body {
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.embed-body > .a {
 | 
				
			||||||
 | 
					    flex-grow: 1;
 | 
				
			||||||
 | 
					    flex-shrink: 1;
 | 
				
			||||||
 | 
					    flex-basis: auto;
 | 
				
			||||||
 | 
					    margin-right: 4px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.embed-body input, .embed-body textarea {
 | 
				
			||||||
 | 
					    min-width: 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.embed-body > .b {
 | 
				
			||||||
 | 
					    flex-grow: 0;
 | 
				
			||||||
 | 
					    flex-shrink: 0;
 | 
				
			||||||
 | 
					    flex-basis: auto;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.discord-field-title, .discord-field-value {
 | 
				
			||||||
 | 
					    max-width: 120px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.discord-field-title {
 | 
				
			||||||
 | 
					    font-weight: bold;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.embed-field-box {
 | 
				
			||||||
 | 
					    margin: 12px 8px 0 0;
 | 
				
			||||||
 | 
					    max-width: 120px;
 | 
				
			||||||
 | 
					    flex: initial;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.field-input {
 | 
				
			||||||
 | 
					    font-size: 0.875rem;
 | 
				
			||||||
 | 
					    width: 120px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.embed-multifield-box {
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    max-width: 100%;
 | 
				
			||||||
 | 
					    flex-wrap: wrap;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.channel-select {
 | 
				
			||||||
 | 
					    font-size: 1.125rem;
 | 
				
			||||||
 | 
					    margin-bottom: 4px;
 | 
				
			||||||
 | 
					    margin-left: 48px;
 | 
				
			||||||
 | 
					    display: inline-flex;
 | 
				
			||||||
 | 
					    font-weight: bold;
 | 
				
			||||||
 | 
					    color: #6e89da;
 | 
				
			||||||
 | 
					    width: auto;
 | 
				
			||||||
 | 
					    border-radius: 2px;
 | 
				
			||||||
 | 
					    border-bottom: 1px solid #fff;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.channel-selector {
 | 
				
			||||||
 | 
					    width: 100%;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.select {
 | 
				
			||||||
 | 
					    width: 100%;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					li.highlight {
 | 
				
			||||||
 | 
					    margin-bottom: 0 !important;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.button-row {
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.button-row .button-row-reminder {
 | 
				
			||||||
 | 
					    flex-grow: 0;
 | 
				
			||||||
 | 
					    padding: 2px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.button-row-template {
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    flex-grow: 1;
 | 
				
			||||||
 | 
					    justify-content: space-between;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.button-row .button-row-template > div {
 | 
				
			||||||
 | 
					    padding: 2px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@media only screen and (max-width: 1023px) {
 | 
				
			||||||
 | 
					    p.title.pageTitle {
 | 
				
			||||||
 | 
					        display: none;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .dashboard-frame {
 | 
				
			||||||
 | 
					        margin-top: 4rem !important;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .customizable.thumbnail img {
 | 
				
			||||||
 | 
					        width: 60px;
 | 
				
			||||||
 | 
					        height: 60px;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@media only screen and (max-width: 768px) {
 | 
				
			||||||
 | 
					    .button-row {
 | 
				
			||||||
 | 
					        display: flex;
 | 
				
			||||||
 | 
					        flex-direction: column;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .button-row .button-row-reminder {
 | 
				
			||||||
 | 
					        width: 100%;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .button-row .button-row-template > div {
 | 
				
			||||||
 | 
					        flex-basis: 0;
 | 
				
			||||||
 | 
					        flex-grow: 1;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .button-row button {
 | 
				
			||||||
 | 
					        width: 100%;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .reminder-settings {
 | 
				
			||||||
 | 
					        margin-bottom: 0 !important;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .tts-row {
 | 
				
			||||||
 | 
					        padding-bottom: 0;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* loader */
 | 
				
			||||||
 | 
					#loader {
 | 
				
			||||||
 | 
					    position: fixed;
 | 
				
			||||||
 | 
					    top: 0;
 | 
				
			||||||
 | 
					    background-color: rgba(255, 255, 255, 0.8);
 | 
				
			||||||
 | 
					    width: 100vw;
 | 
				
			||||||
 | 
					    z-index: 999;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#loader .title {
 | 
				
			||||||
 | 
					    font-size: 6rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* END */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					div.reminderError {
 | 
				
			||||||
 | 
					    margin: 10px;
 | 
				
			||||||
 | 
					    padding: 14px;
 | 
				
			||||||
 | 
					    background-color: #f5f5f5;
 | 
				
			||||||
 | 
					    border-radius: 8px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					div.reminderError .errorHead {
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    flex-direction: row;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					div.reminderError .errorIcon {
 | 
				
			||||||
 | 
					    padding: 8px;
 | 
				
			||||||
 | 
					    border-radius: 4px;
 | 
				
			||||||
 | 
					    margin-right: 12px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					div.reminderError .errorIcon .fas {
 | 
				
			||||||
 | 
					    display: none
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					div.reminderError[data-case="deleted"] .errorIcon {
 | 
				
			||||||
 | 
					    background-color: #e7e5e4;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					div.reminderError[data-case="failed"] .errorIcon {
 | 
				
			||||||
 | 
					    background-color: #fecaca;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					div.reminderError[data-case="sent"] .errorIcon {
 | 
				
			||||||
 | 
					    background-color: #d9f99d;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					div.reminderError[data-case="deleted"] .errorIcon .fas.fa-trash {
 | 
				
			||||||
 | 
					    display: block;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					div.reminderError[data-case="failed"] .errorIcon .fas.fa-exclamation-triangle {
 | 
				
			||||||
 | 
					    display: block;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					div.reminderError[data-case="sent"] .errorIcon .fas.fa-check {
 | 
				
			||||||
 | 
					    display: block;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					div.reminderError .errorHead .reminderName {
 | 
				
			||||||
 | 
					    font-size: 1rem;
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    flex-direction: column;
 | 
				
			||||||
 | 
					    justify-content: center;
 | 
				
			||||||
 | 
					    color: rgb(54, 54, 54);
 | 
				
			||||||
 | 
					    flex-grow: 1;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					div.reminderError .errorHead .reminderTime {
 | 
				
			||||||
 | 
					    font-size: 1rem;
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    flex-direction: column;
 | 
				
			||||||
 | 
					    flex-shrink: 1;
 | 
				
			||||||
 | 
					    justify-content: center;
 | 
				
			||||||
 | 
					    color: rgb(54, 54, 54);
 | 
				
			||||||
 | 
					    background-color: #ffffff;
 | 
				
			||||||
 | 
					    padding: 8px;
 | 
				
			||||||
 | 
					    border-radius: 4px;
 | 
				
			||||||
 | 
					    border-color: #e5e5e5;
 | 
				
			||||||
 | 
					    border-width: 1px;
 | 
				
			||||||
 | 
					    border-style: solid;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					div.reminderError .reminderMessage {
 | 
				
			||||||
 | 
					    font-size: 1rem;
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    flex-direction: column;
 | 
				
			||||||
 | 
					    justify-content: center;
 | 
				
			||||||
 | 
					    color: rgb(54, 54, 54);
 | 
				
			||||||
 | 
					    flex-grow: 1;
 | 
				
			||||||
 | 
					    font-style: italic;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* other stuff */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.half-rem {
 | 
				
			||||||
 | 
					    width: 0.5rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.pad-left {
 | 
				
			||||||
 | 
					    width: 12px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#dead {
 | 
				
			||||||
 | 
					    display: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.colorpicker-container {
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    justify-content: center;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.create-reminder {
 | 
				
			||||||
 | 
					    margin: 0 12px 12px 12px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.button.is-success:not(.is-outlined) {
 | 
				
			||||||
 | 
					    color: white;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.button.is-outlined.is-success {
 | 
				
			||||||
 | 
					    background-color: white;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					a.switch-pane {
 | 
				
			||||||
 | 
					    white-space: nowrap;
 | 
				
			||||||
 | 
					    overflow: hidden;
 | 
				
			||||||
 | 
					    text-overflow: ellipsis;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.guild-submenu {
 | 
				
			||||||
 | 
					    display: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.guild-submenu li {
 | 
				
			||||||
 | 
					    font-size: 0.8rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					a.switch-pane.is-active ~ .guild-submenu {
 | 
				
			||||||
 | 
					    display: block;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.feedback {
 | 
				
			||||||
 | 
					    background-color: #5865F2;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.is-locked {
 | 
				
			||||||
 | 
					    pointer-events: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.is-locked > :not(.patreon-invert) {
 | 
				
			||||||
 | 
					    opacity: 0.4;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.is-locked .patreon-invert {
 | 
				
			||||||
 | 
					    display: block;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.patreon-invert {
 | 
				
			||||||
 | 
					    display: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.is-locked .foreground {
 | 
				
			||||||
 | 
					    pointer-events: auto;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.is-locked .field:last-of-type {
 | 
				
			||||||
 | 
					    display: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.stat-row {
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    flex-direction: row;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.stat-box {
 | 
				
			||||||
 | 
					    flex-grow: 1;
 | 
				
			||||||
 | 
					    border-radius: 6px;
 | 
				
			||||||
 | 
					    background-color: #fcfcfc;
 | 
				
			||||||
 | 
					    border-color: #efefef;
 | 
				
			||||||
 | 
					    border-style: solid;
 | 
				
			||||||
 | 
					    border-width: 1px;
 | 
				
			||||||
 | 
					    margin: 4px;
 | 
				
			||||||
 | 
					    padding: 4px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.figure {
 | 
				
			||||||
 | 
					    text-align: center;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.figure-num {
 | 
				
			||||||
 | 
					    font-size: 2rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
		 After Width: | Height: | Size: 7.2 KiB  | 
| 
		 After Width: | Height: | Size: 20 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								reminder-dashboard/public/static/favicon/apple-touch-icon.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 6.8 KiB  | 
@@ -0,0 +1,9 @@
 | 
				
			|||||||
 | 
					<?xml version="1.0" encoding="utf-8"?>
 | 
				
			||||||
 | 
					<browserconfig>
 | 
				
			||||||
 | 
					    <msapplication>
 | 
				
			||||||
 | 
					        <tile>
 | 
				
			||||||
 | 
					            <square150x150logo src="/mstile-150x150.png"/>
 | 
				
			||||||
 | 
					            <TileColor>#da532c</TileColor>
 | 
				
			||||||
 | 
					        </tile>
 | 
				
			||||||
 | 
					    </msapplication>
 | 
				
			||||||
 | 
					</browserconfig>
 | 
				
			||||||
							
								
								
									
										
											BIN
										
									
								
								reminder-dashboard/public/static/favicon/favicon-16x16.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 1.8 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								reminder-dashboard/public/static/favicon/favicon-32x32.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 2.3 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								reminder-dashboard/public/static/favicon/favicon.ico
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 7.2 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								reminder-dashboard/public/static/favicon/mstile-150x150.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 4.9 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								reminder-dashboard/public/static/img/bg.webp
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 762 B  | 
							
								
								
									
										
											BIN
										
									
								
								reminder-dashboard/public/static/img/icon.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 11 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								reminder-dashboard/public/static/img/logo_flat.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 323 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								reminder-dashboard/public/static/img/logo_flat.webp
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 61 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								reminder-dashboard/public/static/img/logo_nobg.webp
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 81 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								reminder-dashboard/public/static/img/slash-commands.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 23 KiB  | 
| 
		 After Width: | Height: | Size: 35 KiB  | 
| 
		 After Width: | Height: | Size: 55 KiB  | 
| 
		 After Width: | Height: | Size: 3.6 KiB  | 
| 
		 After Width: | Height: | Size: 17 KiB  | 
| 
		 After Width: | Height: | Size: 14 KiB  | 
| 
		 After Width: | Height: | Size: 24 KiB  | 
| 
		 After Width: | Height: | Size: 17 KiB  | 
| 
		 After Width: | Height: | Size: 44 KiB  | 
| 
		 After Width: | Height: | Size: 40 KiB  | 
| 
		 After Width: | Height: | Size: 44 KiB  | 
| 
		 After Width: | Height: | Size: 18 KiB  | 
| 
		 After Width: | Height: | Size: 20 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								reminder-dashboard/public/static/img/tournament-demo.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 65 KiB  | 
							
								
								
									
										131
									
								
								reminder-dashboard/public/static/js/admin.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,131 @@
 | 
				
			|||||||
 | 
					document.addEventListener("DOMContentLoaded", () => {
 | 
				
			||||||
 | 
					    fetch("/admin/data")
 | 
				
			||||||
 | 
					        .then((resp) => resp.json())
 | 
				
			||||||
 | 
					        .then((data) => {
 | 
				
			||||||
 | 
					            document.querySelector("#backlog").textContent = data.backlog;
 | 
				
			||||||
 | 
					            document.querySelector("#reminders").textContent = data.count.reminders;
 | 
				
			||||||
 | 
					            document.querySelector("#intervals").textContent = data.count.intervals;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            let historySent = data.historyLong.sent.reduce(
 | 
				
			||||||
 | 
					                (iv, frame) => iv + frame.count,
 | 
				
			||||||
 | 
					                0
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					            let historyFailed = data.historyLong.failed.reduce(
 | 
				
			||||||
 | 
					                (iv, frame) => iv + frame.count,
 | 
				
			||||||
 | 
					                0
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					            let rate = historyFailed / (historySent + historyFailed);
 | 
				
			||||||
 | 
					            let formatted = Math.round(rate * 10000) / 100;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            document.querySelector("#historySent").textContent = historySent;
 | 
				
			||||||
 | 
					            document.querySelector("#historyFailed").textContent = historyFailed;
 | 
				
			||||||
 | 
					            document.querySelector("#failRate").textContent = `${formatted}%`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            new Chart(document.getElementById("schedule"), {
 | 
				
			||||||
 | 
					                type: "bar",
 | 
				
			||||||
 | 
					                data: {
 | 
				
			||||||
 | 
					                    labels: [
 | 
				
			||||||
 | 
					                        ...data.scheduleShort.once,
 | 
				
			||||||
 | 
					                        ...data.scheduleShort.interval,
 | 
				
			||||||
 | 
					                    ].map((row) => luxon.DateTime.fromISO(row.time_key)),
 | 
				
			||||||
 | 
					                    datasets: [
 | 
				
			||||||
 | 
					                        {
 | 
				
			||||||
 | 
					                            label: "Reminders",
 | 
				
			||||||
 | 
					                            data: data.scheduleShort.once.map((row) => row.count),
 | 
				
			||||||
 | 
					                        },
 | 
				
			||||||
 | 
					                        {
 | 
				
			||||||
 | 
					                            label: "Intervals",
 | 
				
			||||||
 | 
					                            data: data.scheduleShort.interval.map((row) => row.count),
 | 
				
			||||||
 | 
					                        },
 | 
				
			||||||
 | 
					                    ],
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					                options: {
 | 
				
			||||||
 | 
					                    responsive: true,
 | 
				
			||||||
 | 
					                    maintainAspectRatio: false,
 | 
				
			||||||
 | 
					                    scales: {
 | 
				
			||||||
 | 
					                        x: {
 | 
				
			||||||
 | 
					                            stacked: true,
 | 
				
			||||||
 | 
					                            type: "time",
 | 
				
			||||||
 | 
					                            time: {
 | 
				
			||||||
 | 
					                                unit: "minute",
 | 
				
			||||||
 | 
					                            },
 | 
				
			||||||
 | 
					                        },
 | 
				
			||||||
 | 
					                        y: {
 | 
				
			||||||
 | 
					                            stacked: true,
 | 
				
			||||||
 | 
					                        },
 | 
				
			||||||
 | 
					                    },
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            new Chart(document.getElementById("scheduleLong"), {
 | 
				
			||||||
 | 
					                type: "bar",
 | 
				
			||||||
 | 
					                data: {
 | 
				
			||||||
 | 
					                    labels: [
 | 
				
			||||||
 | 
					                        ...data.scheduleLong.once,
 | 
				
			||||||
 | 
					                        ...data.scheduleLong.interval,
 | 
				
			||||||
 | 
					                    ].map((row) => luxon.DateTime.fromISO(row.time_key)),
 | 
				
			||||||
 | 
					                    datasets: [
 | 
				
			||||||
 | 
					                        {
 | 
				
			||||||
 | 
					                            label: "Reminders",
 | 
				
			||||||
 | 
					                            data: data.scheduleLong.once.map((row) => row.count),
 | 
				
			||||||
 | 
					                        },
 | 
				
			||||||
 | 
					                        {
 | 
				
			||||||
 | 
					                            label: "Intervals",
 | 
				
			||||||
 | 
					                            data: data.scheduleLong.interval.map((row) => row.count),
 | 
				
			||||||
 | 
					                        },
 | 
				
			||||||
 | 
					                    ],
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					                options: {
 | 
				
			||||||
 | 
					                    responsive: true,
 | 
				
			||||||
 | 
					                    maintainAspectRatio: false,
 | 
				
			||||||
 | 
					                    scales: {
 | 
				
			||||||
 | 
					                        x: {
 | 
				
			||||||
 | 
					                            stacked: true,
 | 
				
			||||||
 | 
					                            type: "time",
 | 
				
			||||||
 | 
					                            time: {
 | 
				
			||||||
 | 
					                                unit: "day",
 | 
				
			||||||
 | 
					                            },
 | 
				
			||||||
 | 
					                        },
 | 
				
			||||||
 | 
					                        y: {
 | 
				
			||||||
 | 
					                            stacked: true,
 | 
				
			||||||
 | 
					                        },
 | 
				
			||||||
 | 
					                    },
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            new Chart(document.getElementById("historyLong"), {
 | 
				
			||||||
 | 
					                type: "bar",
 | 
				
			||||||
 | 
					                data: {
 | 
				
			||||||
 | 
					                    labels: [...data.historyLong.sent, ...data.historyLong.failed].map(
 | 
				
			||||||
 | 
					                        (row) => luxon.DateTime.fromISO(row.time_key)
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                    datasets: [
 | 
				
			||||||
 | 
					                        {
 | 
				
			||||||
 | 
					                            label: "Success",
 | 
				
			||||||
 | 
					                            data: data.historyLong.sent.map((row) => row.count),
 | 
				
			||||||
 | 
					                        },
 | 
				
			||||||
 | 
					                        {
 | 
				
			||||||
 | 
					                            label: "Fail",
 | 
				
			||||||
 | 
					                            data: data.historyLong.failed.map((row) => row.count),
 | 
				
			||||||
 | 
					                        },
 | 
				
			||||||
 | 
					                    ],
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					                options: {
 | 
				
			||||||
 | 
					                    responsive: true,
 | 
				
			||||||
 | 
					                    maintainAspectRatio: false,
 | 
				
			||||||
 | 
					                    scales: {
 | 
				
			||||||
 | 
					                        x: {
 | 
				
			||||||
 | 
					                            stacked: true,
 | 
				
			||||||
 | 
					                            type: "time",
 | 
				
			||||||
 | 
					                            time: {
 | 
				
			||||||
 | 
					                                unit: "day",
 | 
				
			||||||
 | 
					                            },
 | 
				
			||||||
 | 
					                        },
 | 
				
			||||||
 | 
					                        y: {
 | 
				
			||||||
 | 
					                            stacked: true,
 | 
				
			||||||
 | 
					                        },
 | 
				
			||||||
 | 
					                    },
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
							
								
								
									
										20
									
								
								reminder-dashboard/public/static/js/chart.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,7 @@
 | 
				
			|||||||
 | 
					/*!
 | 
				
			||||||
 | 
					 * chartjs-adapter-luxon v1.3.1
 | 
				
			||||||
 | 
					 * https://www.chartjs.org
 | 
				
			||||||
 | 
					 * (c) 2023 chartjs-adapter-luxon Contributors
 | 
				
			||||||
 | 
					 * Released under the MIT license
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(require("chart.js"),require("luxon")):"function"==typeof define&&define.amd?define(["chart.js","luxon"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).Chart,e.luxon)}(this,(function(e,t){"use strict";const n={datetime:t.DateTime.DATETIME_MED_WITH_SECONDS,millisecond:"h:mm:ss.SSS a",second:t.DateTime.TIME_WITH_SECONDS,minute:t.DateTime.TIME_SIMPLE,hour:{hour:"numeric"},day:{day:"numeric",month:"short"},week:"DD",month:{month:"short",year:"numeric"},quarter:"'Q'q - yyyy",year:{year:"numeric"}};e._adapters._date.override({_id:"luxon",_create:function(e){return t.DateTime.fromMillis(e,this.options)},init(e){this.options.locale||(this.options.locale=e.locale)},formats:function(){return n},parse:function(e,n){const i=this.options,r=typeof e;return null===e||"undefined"===r?null:("number"===r?e=this._create(e):"string"===r?e="string"==typeof n?t.DateTime.fromFormat(e,n,i):t.DateTime.fromISO(e,i):e instanceof Date?e=t.DateTime.fromJSDate(e,i):"object"!==r||e instanceof t.DateTime||(e=t.DateTime.fromObject(e,i)),e.isValid?e.valueOf():null)},format:function(e,t){const n=this._create(e);return"string"==typeof t?n.toFormat(t):n.toLocaleString(t)},add:function(e,t,n){const i={};return i[n]=t,this._create(e).plus(i).valueOf()},diff:function(e,t,n){return this._create(e).diff(this._create(t)).as(n).valueOf()},startOf:function(e,t,n){if("isoWeek"===t){n=Math.trunc(Math.min(Math.max(0,n),6));const t=this._create(e);return t.minus({days:(t.weekday-n+7)%7}).startOf("day").valueOf()}return t?this._create(e).startOf(t).valueOf():e},endOf:function(e,t){return this._create(e).endOf(t).valueOf()}})}));
 | 
				
			||||||
							
								
								
									
										931
									
								
								reminder-dashboard/public/static/js/dtsel.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,931 @@
 | 
				
			|||||||
 | 
					(function () {
 | 
				
			||||||
 | 
					    "use strict";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    var BODYTYPES = ["DAYS", "MONTHS", "YEARS"];
 | 
				
			||||||
 | 
					    var MONTHS = [
 | 
				
			||||||
 | 
					        "January", "February", "March", "April", "May", "June",
 | 
				
			||||||
 | 
					        "July", "August", "September", "October", "November", "December"
 | 
				
			||||||
 | 
					    ];
 | 
				
			||||||
 | 
					    var WEEKDAYS = [
 | 
				
			||||||
 | 
					        "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"
 | 
				
			||||||
 | 
					    ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /** @typedef {Object.<string, Function[]>} Handlers */
 | 
				
			||||||
 | 
					    /** @typedef {function(String, Function): null} AddHandler */
 | 
				
			||||||
 | 
					    /** @typedef {("DAYS"|"MONTHS"|"YEARS")} BodyType */
 | 
				
			||||||
 | 
					    /** @typedef {string|number} StringNum */
 | 
				
			||||||
 | 
					    /** @typedef {Object.<string, StringNum>} StringNumObj */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The local state
 | 
				
			||||||
 | 
					     * @typedef {Object} InstanceState
 | 
				
			||||||
 | 
					     * @property {Date} value
 | 
				
			||||||
 | 
					     * @property {Number} year
 | 
				
			||||||
 | 
					     * @property {Number} month
 | 
				
			||||||
 | 
					     * @property {Number} day
 | 
				
			||||||
 | 
					     * @property {Number} time
 | 
				
			||||||
 | 
					     * @property {Number} hours
 | 
				
			||||||
 | 
					     * @property {Number} minutes
 | 
				
			||||||
 | 
					     * @property {Number} seconds
 | 
				
			||||||
 | 
					     * @property {BodyType} bodyType
 | 
				
			||||||
 | 
					     * @property {Boolean} visible
 | 
				
			||||||
 | 
					     * @property {Number} cancelBlur
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /** 
 | 
				
			||||||
 | 
					     * @typedef {Object} Config
 | 
				
			||||||
 | 
					     * @property {String} dateFormat
 | 
				
			||||||
 | 
					     * @property {String} timeFormat
 | 
				
			||||||
 | 
					     * @property {Boolean} showDate
 | 
				
			||||||
 | 
					     * @property {Boolean} showTime
 | 
				
			||||||
 | 
					     * @property {Number} paddingX
 | 
				
			||||||
 | 
					     * @property {Number} paddingY
 | 
				
			||||||
 | 
					     * @property {BodyType} defaultView
 | 
				
			||||||
 | 
					     * @property {"TOP"|"BOTTOM"} direction
 | 
				
			||||||
 | 
					    */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @class
 | 
				
			||||||
 | 
					     * @param {HTMLElement} elem 
 | 
				
			||||||
 | 
					     * @param {Config} config 
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    function DTS(elem, config) {
 | 
				
			||||||
 | 
					        var config = config || {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /** @type {Config} */
 | 
				
			||||||
 | 
					        var defaultConfig = {
 | 
				
			||||||
 | 
					            defaultView: BODYTYPES[0],
 | 
				
			||||||
 | 
					            dateFormat: "yyyy-mm-dd",
 | 
				
			||||||
 | 
					            timeFormat: "HH:MM:SS",
 | 
				
			||||||
 | 
					            showDate: true,
 | 
				
			||||||
 | 
					            showTime: false,
 | 
				
			||||||
 | 
					            paddingX: 5,
 | 
				
			||||||
 | 
					            paddingY: 5,
 | 
				
			||||||
 | 
					            direction: 'TOP'
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (!elem) {
 | 
				
			||||||
 | 
					            throw TypeError("input element or selector required for contructor");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if (Object.getPrototypeOf(elem) === String.prototype) {
 | 
				
			||||||
 | 
					            var _elem = document.querySelectorAll(elem);
 | 
				
			||||||
 | 
					            if (!_elem[0]){
 | 
				
			||||||
 | 
					                throw Error('"' + elem + '" not found.');
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            elem = _elem[0];
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        this.config = setDefaults(config, defaultConfig);
 | 
				
			||||||
 | 
					        this.dateFormat = this.config.dateFormat;
 | 
				
			||||||
 | 
					        this.timeFormat = this.config.timeFormat;
 | 
				
			||||||
 | 
					        this.dateFormatRegEx = new RegExp("yyyy|yy|mm|dd", "gi");
 | 
				
			||||||
 | 
					        this.timeFormatRegEx = new RegExp("hh|mm|ss|a", "gi");
 | 
				
			||||||
 | 
					        this.inputElem = elem;
 | 
				
			||||||
 | 
					        this.dtbox = null;
 | 
				
			||||||
 | 
					        this.setup();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    DTS.prototype.setup = function () {
 | 
				
			||||||
 | 
					        var handler = this.inputElemHandler.bind(this);
 | 
				
			||||||
 | 
					        this.inputElem.addEventListener("focus", handler, false)
 | 
				
			||||||
 | 
					        this.inputElem.addEventListener("blur", handler, false);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    DTS.prototype.inputElemHandler = function (e) {
 | 
				
			||||||
 | 
					        if (e.type == "focus") {
 | 
				
			||||||
 | 
					            if (!this.dtbox) {
 | 
				
			||||||
 | 
					                this.dtbox = new DTBox(e.target, this);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            this.dtbox.visible = true;
 | 
				
			||||||
 | 
					        } else if (e.type == "blur" && this.dtbox && this.dtbox.visible) {
 | 
				
			||||||
 | 
					            var self = this;
 | 
				
			||||||
 | 
					            setTimeout(function () {
 | 
				
			||||||
 | 
					                if (self.dtbox.cancelBlur > 0) {
 | 
				
			||||||
 | 
					                    self.dtbox.cancelBlur -= 1;
 | 
				
			||||||
 | 
					                 } else {
 | 
				
			||||||
 | 
					                    self.dtbox.visible = false;
 | 
				
			||||||
 | 
					                    self.inputElem.blur();
 | 
				
			||||||
 | 
					                 }
 | 
				
			||||||
 | 
					            }, 100);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @class
 | 
				
			||||||
 | 
					     * @param {HTMLElement} elem 
 | 
				
			||||||
 | 
					     * @param {DTS} settings 
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    function DTBox(elem, settings) {
 | 
				
			||||||
 | 
					        /** @type {DTBox} */
 | 
				
			||||||
 | 
					        var self = this;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /** @type {Handlers} */
 | 
				
			||||||
 | 
					        var handlers = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /** @type {InstanceState} */
 | 
				
			||||||
 | 
					        var localState = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /**
 | 
				
			||||||
 | 
					         * @param {String} key 
 | 
				
			||||||
 | 
					         * @param {*} default_val 
 | 
				
			||||||
 | 
					         */
 | 
				
			||||||
 | 
					        function getterSetter(key, default_val) {
 | 
				
			||||||
 | 
					            return {
 | 
				
			||||||
 | 
					                get: function () {
 | 
				
			||||||
 | 
					                    var val = localState[key];
 | 
				
			||||||
 | 
					                    return val === undefined ? default_val : val;
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					                set: function (val) {
 | 
				
			||||||
 | 
					                    var prevState = self.state;
 | 
				
			||||||
 | 
					                    var _handlers = handlers[key] || [];
 | 
				
			||||||
 | 
					                    localState[key] = val;
 | 
				
			||||||
 | 
					                    for (var i = 0; i < _handlers.length; i++) {
 | 
				
			||||||
 | 
					                        _handlers[i].bind(self)(localState, prevState);
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /** @type {AddHandler} */
 | 
				
			||||||
 | 
					        function addHandler(key, handlerFn) {
 | 
				
			||||||
 | 
					            if (!key || !handlerFn) {
 | 
				
			||||||
 | 
					                return false;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            if (!handlers[key]) {
 | 
				
			||||||
 | 
					                handlers[key] = [];
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            handlers[key].push(handlerFn);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Object.defineProperties(this, {
 | 
				
			||||||
 | 
					            visible: getterSetter("visible", false),
 | 
				
			||||||
 | 
					            bodyType: getterSetter("bodyType", settings.config.defaultView),
 | 
				
			||||||
 | 
					            value: getterSetter("value"),
 | 
				
			||||||
 | 
					            year: getterSetter("year", 0),
 | 
				
			||||||
 | 
					            month: getterSetter("month", 0),
 | 
				
			||||||
 | 
					            day: getterSetter("day", 0),
 | 
				
			||||||
 | 
					            hours: getterSetter("hours", 0),
 | 
				
			||||||
 | 
					            minutes: getterSetter("minutes", 0),
 | 
				
			||||||
 | 
					            seconds: getterSetter("seconds", 0),
 | 
				
			||||||
 | 
					            cancelBlur: getterSetter("cancelBlur", 0),
 | 
				
			||||||
 | 
					            addHandler: {value: addHandler},
 | 
				
			||||||
 | 
					            month_long: {
 | 
				
			||||||
 | 
					                get: function () {
 | 
				
			||||||
 | 
					                    return MONTHS[self.month];
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            month_short: {
 | 
				
			||||||
 | 
					                get: function () {
 | 
				
			||||||
 | 
					                    return self.month_long.slice(0, 3);
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            state: {
 | 
				
			||||||
 | 
					                get: function () {
 | 
				
			||||||
 | 
					                    return Object.assign({}, localState);
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            time: {
 | 
				
			||||||
 | 
					                get: function() {
 | 
				
			||||||
 | 
					                    var hours = self.hours * 60 * 60 * 1000;
 | 
				
			||||||
 | 
					                    var minutes = self.minutes * 60 * 1000;
 | 
				
			||||||
 | 
					                    var seconds = self.seconds * 1000;
 | 
				
			||||||
 | 
					                    return  hours + minutes + seconds;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        this.el = {};
 | 
				
			||||||
 | 
					        this.settings = settings;
 | 
				
			||||||
 | 
					        this.elem = elem;
 | 
				
			||||||
 | 
					        this.setup();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    DTBox.prototype.setup = function () {
 | 
				
			||||||
 | 
					        Object.defineProperties(this.el, {
 | 
				
			||||||
 | 
					            wrapper: { value: null, configurable: true },
 | 
				
			||||||
 | 
					            header: { value: null, configurable: true },
 | 
				
			||||||
 | 
					            body: { value: null, configurable: true },
 | 
				
			||||||
 | 
					            footer: { value: null, configurable: true }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        this.setupWrapper();
 | 
				
			||||||
 | 
					        if (this.settings.config.showDate) {
 | 
				
			||||||
 | 
					            this.setupHeader();
 | 
				
			||||||
 | 
					            this.setupBody();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if (this.settings.config.showTime) {
 | 
				
			||||||
 | 
					            this.setupFooter();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        var self = this;
 | 
				
			||||||
 | 
					        this.addHandler("visible", function (state, prevState) {
 | 
				
			||||||
 | 
					            if (state.visible && !prevState.visible){
 | 
				
			||||||
 | 
					                document.body.appendChild(this.el.wrapper);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                var parts = self.elem.value.split(/\s*,\s*/);
 | 
				
			||||||
 | 
					                var startDate = undefined;
 | 
				
			||||||
 | 
					                var startTime = 0;
 | 
				
			||||||
 | 
					                if (self.settings.config.showDate) {
 | 
				
			||||||
 | 
					                    startDate = parseDate(parts[0], self.settings);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                if (self.settings.config.showTime) {
 | 
				
			||||||
 | 
					                    startTime = parseTime(parts[parts.length-1], self.settings);
 | 
				
			||||||
 | 
					                    startTime = startTime || 0;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                if (!(startDate && startDate.getTime())) {
 | 
				
			||||||
 | 
					                    startDate = new Date();
 | 
				
			||||||
 | 
					                    startDate = new Date(
 | 
				
			||||||
 | 
					                        startDate.getFullYear(),
 | 
				
			||||||
 | 
					                        startDate.getMonth(),
 | 
				
			||||||
 | 
					                        startDate.getDate()
 | 
				
			||||||
 | 
					                    );
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                var value = new Date(startDate.getTime() + startTime);
 | 
				
			||||||
 | 
					                self.value = value;
 | 
				
			||||||
 | 
					                self.year = value.getFullYear();
 | 
				
			||||||
 | 
					                self.month = value.getMonth();
 | 
				
			||||||
 | 
					                self.day = value.getDate();
 | 
				
			||||||
 | 
					                self.hours = value.getHours();
 | 
				
			||||||
 | 
					                self.minutes = value.getMinutes();
 | 
				
			||||||
 | 
					                self.seconds = value.getSeconds();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if (self.settings.config.showDate) {
 | 
				
			||||||
 | 
					                    self.setHeaderContent();
 | 
				
			||||||
 | 
					                    self.setBodyContent();
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                if (self.settings.config.showTime) {
 | 
				
			||||||
 | 
					                    self.setFooterContent();
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            } else if (!state.visible && prevState.visible) {
 | 
				
			||||||
 | 
					                document.body.removeChild(this.el.wrapper);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    DTBox.prototype.setupWrapper = function () {
 | 
				
			||||||
 | 
					        if (!this.el.wrapper) {
 | 
				
			||||||
 | 
					            var el = document.createElement("div");
 | 
				
			||||||
 | 
					            el.classList.add("date-selector-wrapper");
 | 
				
			||||||
 | 
					            Object.defineProperty(this.el, "wrapper", { value: el });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        var self = this;
 | 
				
			||||||
 | 
					        var htmlRoot = document.getElementsByTagName('html')[0];
 | 
				
			||||||
 | 
					        function setPosition(e){
 | 
				
			||||||
 | 
					            var minTopSpace = 300;
 | 
				
			||||||
 | 
					            var box = getOffset(self.elem);
 | 
				
			||||||
 | 
					            var config = self.settings.config;
 | 
				
			||||||
 | 
					            var paddingY = config.paddingY || 5;
 | 
				
			||||||
 | 
					            var paddingX = config.paddingX || 5;
 | 
				
			||||||
 | 
					            var top = box.top + self.elem.offsetHeight + paddingY;
 | 
				
			||||||
 | 
					            var left = box.left + paddingX;
 | 
				
			||||||
 | 
					            var bottom = htmlRoot.clientHeight - box.top + paddingY;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            self.el.wrapper.style.left = `${left}px`;
 | 
				
			||||||
 | 
					            if (box.top > minTopSpace && config.direction != 'BOTTOM') {
 | 
				
			||||||
 | 
					                self.el.wrapper.style.bottom = `${bottom}px`;
 | 
				
			||||||
 | 
					                self.el.wrapper.style.top = '';
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                self.el.wrapper.style.top = `${top}px`;
 | 
				
			||||||
 | 
					                self.el.wrapper.style.bottom = ''; 
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        function handler(e) {
 | 
				
			||||||
 | 
					            self.cancelBlur += 1;
 | 
				
			||||||
 | 
					            setTimeout(function(){
 | 
				
			||||||
 | 
					                self.elem.focus();
 | 
				
			||||||
 | 
					            }, 50);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        setPosition();
 | 
				
			||||||
 | 
					        this.setPosition = setPosition;
 | 
				
			||||||
 | 
					        this.el.wrapper.addEventListener("mousedown", handler, false);
 | 
				
			||||||
 | 
					        this.el.wrapper.addEventListener("touchstart", handler, false);
 | 
				
			||||||
 | 
					        window.addEventListener('resize', this.setPosition);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    DTBox.prototype.setupHeader = function () {
 | 
				
			||||||
 | 
					        if (!this.el.header) {
 | 
				
			||||||
 | 
					            var row = document.createElement("div");
 | 
				
			||||||
 | 
					            var classes = ["cal-nav-prev", "cal-nav-current", "cal-nav-next"];
 | 
				
			||||||
 | 
					            row.classList.add("cal-header");
 | 
				
			||||||
 | 
					            for (var i = 0; i < 3; i++) {
 | 
				
			||||||
 | 
					                var cell = document.createElement("div");
 | 
				
			||||||
 | 
					                cell.classList.add("cal-nav", classes[i]);
 | 
				
			||||||
 | 
					                cell.onclick = this.onHeaderChange.bind(this);
 | 
				
			||||||
 | 
					                row.appendChild(cell);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            row.children[0].innerHTML = "<";
 | 
				
			||||||
 | 
					            row.children[2].innerHTML = ">";
 | 
				
			||||||
 | 
					            Object.defineProperty(this.el, "header", { value: row });
 | 
				
			||||||
 | 
					            tryAppendChild(row, this.el.wrapper);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        this.setHeaderContent();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    DTBox.prototype.setHeaderContent = function () {
 | 
				
			||||||
 | 
					        var content = this.year;
 | 
				
			||||||
 | 
					        if ("DAYS" == this.bodyType) {
 | 
				
			||||||
 | 
					            content = this.month_long + " " + content;
 | 
				
			||||||
 | 
					        } else if ("YEARS" == this.bodyType) {
 | 
				
			||||||
 | 
					            var start = this.year + 10 - (this.year % 10);
 | 
				
			||||||
 | 
					            content = start - 10 + "-" + (start - 1);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        this.el.header.children[1].innerText = content;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    DTBox.prototype.setupBody = function () {
 | 
				
			||||||
 | 
					        if (!this.el.body) {
 | 
				
			||||||
 | 
					            var el = document.createElement("div");
 | 
				
			||||||
 | 
					            el.classList.add("cal-body");
 | 
				
			||||||
 | 
					            Object.defineProperty(this.el, "body", { value: el });
 | 
				
			||||||
 | 
					            tryAppendChild(el, this.el.wrapper);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        var toAppend = null;
 | 
				
			||||||
 | 
					        function makeGrid(rows, cols, className, firstRowClass, clickHandler) {
 | 
				
			||||||
 | 
					            var grid = document.createElement("div");
 | 
				
			||||||
 | 
					            grid.classList.add(className);
 | 
				
			||||||
 | 
					            for (var i = 1; i < rows + 1; i++) {
 | 
				
			||||||
 | 
					                var row = document.createElement("div");
 | 
				
			||||||
 | 
					                row.classList.add("cal-row", "cal-row-" + i);
 | 
				
			||||||
 | 
					                if (i == 1 && firstRowClass) {
 | 
				
			||||||
 | 
					                    row.classList.add(firstRowClass);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                for (var j = 1; j < cols + 1; j++) {
 | 
				
			||||||
 | 
					                    var col = document.createElement("div");
 | 
				
			||||||
 | 
					                    col.classList.add("cal-cell", "cal-col-" + j);
 | 
				
			||||||
 | 
					                    col.onclick = clickHandler;
 | 
				
			||||||
 | 
					                    row.appendChild(col);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                grid.appendChild(row);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            return grid;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if ("DAYS" == this.bodyType) {
 | 
				
			||||||
 | 
					            toAppend = this.el.body.calDays;
 | 
				
			||||||
 | 
					            if (!toAppend) {
 | 
				
			||||||
 | 
					                toAppend = makeGrid(7, 7, "cal-days", "cal-day-names", this.onDateSelected.bind(this));
 | 
				
			||||||
 | 
					                for (var i = 0; i < 7; i++) {
 | 
				
			||||||
 | 
					                    var cell = toAppend.children[0].children[i];
 | 
				
			||||||
 | 
					                    cell.innerText = WEEKDAYS[i].slice(0, 2);
 | 
				
			||||||
 | 
					                    cell.onclick = null;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                this.el.body.calDays = toAppend;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        } else if ("MONTHS" == this.bodyType) {
 | 
				
			||||||
 | 
					            toAppend = this.el.body.calMonths;
 | 
				
			||||||
 | 
					            if (!toAppend) {
 | 
				
			||||||
 | 
					                toAppend = makeGrid(3, 4, "cal-months", null, this.onMonthSelected.bind(this));
 | 
				
			||||||
 | 
					                for (var i = 0; i < 3; i++) {
 | 
				
			||||||
 | 
					                    for (var j = 0; j < 4; j++) {
 | 
				
			||||||
 | 
					                        var monthShort = MONTHS[4 * i + j].slice(0, 3);
 | 
				
			||||||
 | 
					                        toAppend.children[i].children[j].innerText = monthShort;
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                this.el.body.calMonths = toAppend;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        } else if ("YEARS" == this.bodyType) {
 | 
				
			||||||
 | 
					            toAppend = this.el.body.calYears;
 | 
				
			||||||
 | 
					            if (!toAppend) {
 | 
				
			||||||
 | 
					                toAppend = makeGrid(3, 4, "cal-years", null, this.onYearSelected.bind(this));
 | 
				
			||||||
 | 
					                this.el.body.calYears = toAppend;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        empty(this.el.body);
 | 
				
			||||||
 | 
					        tryAppendChild(toAppend, this.el.body);
 | 
				
			||||||
 | 
					        this.setBodyContent();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    DTBox.prototype.setBodyContent = function () {
 | 
				
			||||||
 | 
					        var grid = this.el.body.children[0];
 | 
				
			||||||
 | 
					        var classes = ["cal-cell-prev", "cal-cell-next", "cal-value"];
 | 
				
			||||||
 | 
					        if ("DAYS" == this.bodyType) {
 | 
				
			||||||
 | 
					            var oneDayMilliSecs = 24 * 60 * 60 * 1000;
 | 
				
			||||||
 | 
					            var start = new Date(this.year, this.month, 1);
 | 
				
			||||||
 | 
					            var adjusted = new Date(start.getTime() - oneDayMilliSecs * start.getDay());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            grid.children[6].style.display = "";
 | 
				
			||||||
 | 
					            for (var i = 1; i < 7; i++) {
 | 
				
			||||||
 | 
					                for (var j = 0; j < 7; j++) {
 | 
				
			||||||
 | 
					                    var cell = grid.children[i].children[j];
 | 
				
			||||||
 | 
					                    var month = adjusted.getMonth();
 | 
				
			||||||
 | 
					                    var date = adjusted.getDate();
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                    cell.innerText = date;
 | 
				
			||||||
 | 
					                    cell.classList.remove(classes[0], classes[1], classes[2]);
 | 
				
			||||||
 | 
					                    if (month != this.month) {
 | 
				
			||||||
 | 
					                        if (i == 6 && j == 0) {
 | 
				
			||||||
 | 
					                            grid.children[6].style.display = "none";
 | 
				
			||||||
 | 
					                            break;
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                        cell.classList.add(month < this.month ? classes[0] : classes[1]);
 | 
				
			||||||
 | 
					                    } else if (isEqualDate(adjusted, this.value)){
 | 
				
			||||||
 | 
					                        cell.classList.add(classes[2]);
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    adjusted = new Date(adjusted.getTime() + oneDayMilliSecs);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        } else if ("YEARS" == this.bodyType) {
 | 
				
			||||||
 | 
					            var year = this.year - (this.year % 10) - 1;
 | 
				
			||||||
 | 
					            for (i = 0; i < 3; i++) {
 | 
				
			||||||
 | 
					                for (j = 0; j < 4; j++) {
 | 
				
			||||||
 | 
					                    grid.children[i].children[j].innerText = year;
 | 
				
			||||||
 | 
					                    year += 1;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            grid.children[0].children[0].classList.add(classes[0]);
 | 
				
			||||||
 | 
					            grid.children[2].children[3].classList.add(classes[1]);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /** @param {Event} e */
 | 
				
			||||||
 | 
					    DTBox.prototype.onTimeChange = function(e) {
 | 
				
			||||||
 | 
					        e.stopPropagation();
 | 
				
			||||||
 | 
					        if (e.type == 'mousedown') {
 | 
				
			||||||
 | 
					            this.cancelBlur += 1;
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        var el = e.target;
 | 
				
			||||||
 | 
					        this[el.name] = parseInt(el.value) || 0;
 | 
				
			||||||
 | 
					        this.setupFooter();
 | 
				
			||||||
 | 
					        if (e.type == 'change') {
 | 
				
			||||||
 | 
					            var self = this;
 | 
				
			||||||
 | 
					            setTimeout(function(){
 | 
				
			||||||
 | 
					                self.elem.focus();
 | 
				
			||||||
 | 
					            }, 50);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        this.setInputValue();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    DTBox.prototype.setupFooter = function() {
 | 
				
			||||||
 | 
					        if (!this.el.footer) {
 | 
				
			||||||
 | 
					            var footer = document.createElement("div");
 | 
				
			||||||
 | 
					            var handler = this.onTimeChange.bind(this);
 | 
				
			||||||
 | 
					            var self = this;
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            function makeRow(label, name, range, changeHandler) {
 | 
				
			||||||
 | 
					                var row = document.createElement("div");
 | 
				
			||||||
 | 
					                row.classList.add('cal-time');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                var labelCol = row.appendChild(document.createElement("div"));
 | 
				
			||||||
 | 
					                labelCol.classList.add('cal-time-label');
 | 
				
			||||||
 | 
					                labelCol.innerText = label;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                var valueCol = row.appendChild(document.createElement("div"));
 | 
				
			||||||
 | 
					                valueCol.classList.add('cal-time-value');
 | 
				
			||||||
 | 
					                valueCol.innerText = '00';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                var inputCol = row.appendChild(document.createElement("div"));
 | 
				
			||||||
 | 
					                var slider = inputCol.appendChild(document.createElement("input"));
 | 
				
			||||||
 | 
					                Object.assign(slider, {step:1, min:0, max:range, name:name, type:'range'});
 | 
				
			||||||
 | 
					                Object.defineProperty(footer, name, {value: slider});
 | 
				
			||||||
 | 
					                inputCol.classList.add('cal-time-slider');
 | 
				
			||||||
 | 
					                slider.onchange = changeHandler;
 | 
				
			||||||
 | 
					                slider.oninput = changeHandler;
 | 
				
			||||||
 | 
					                slider.onmousedown = changeHandler;
 | 
				
			||||||
 | 
					                self[name] = self[name] || parseInt(slider.value) || 0;
 | 
				
			||||||
 | 
					                footer.appendChild(row)
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            makeRow('HH:', 'hours', 23, handler);
 | 
				
			||||||
 | 
					            makeRow('MM:', 'minutes', 59, handler);
 | 
				
			||||||
 | 
					            makeRow('SS:', 'seconds', 59, handler);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            footer.classList.add("cal-footer");
 | 
				
			||||||
 | 
					            Object.defineProperty(this.el, "footer", { value: footer });
 | 
				
			||||||
 | 
					            tryAppendChild(footer, this.el.wrapper);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        this.setFooterContent();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    DTBox.prototype.setFooterContent = function() {
 | 
				
			||||||
 | 
					        if (this.el.footer) {
 | 
				
			||||||
 | 
					            var footer = this.el.footer;
 | 
				
			||||||
 | 
					            footer.hours.value = this.hours;
 | 
				
			||||||
 | 
					            footer.children[0].children[1].innerText = padded(this.hours, 2);
 | 
				
			||||||
 | 
					            footer.minutes.value = this.minutes;
 | 
				
			||||||
 | 
					            footer.children[1].children[1].innerText = padded(this.minutes, 2);
 | 
				
			||||||
 | 
					            footer.seconds.value = this.seconds;
 | 
				
			||||||
 | 
					            footer.children[2].children[1].innerText = padded(this.seconds, 2);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    DTBox.prototype.setInputValue = function() {
 | 
				
			||||||
 | 
					        var date = new Date(this.year, this.month, this.day);
 | 
				
			||||||
 | 
					        var strings = [];
 | 
				
			||||||
 | 
					        if (this.settings.config.showDate) {
 | 
				
			||||||
 | 
					            strings.push(renderDate(date, this.settings));
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if (this.settings.config.showTime) {
 | 
				
			||||||
 | 
					            var joined = new Date(date.getTime() + this.time);
 | 
				
			||||||
 | 
					            strings.push(renderTime(joined, this.settings));
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        this.elem.value = strings.join(', ');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    DTBox.prototype.onDateSelected = function (e) {
 | 
				
			||||||
 | 
					        var row = e.target.parentNode;
 | 
				
			||||||
 | 
					        var date = parseInt(e.target.innerText);
 | 
				
			||||||
 | 
					        if (!(row.nextSibling && row.nextSibling.nextSibling) && date < 8) {
 | 
				
			||||||
 | 
					            this.month += 1;
 | 
				
			||||||
 | 
					        } else if (!(row.previousSibling && row.previousSibling.previousSibling) && date > 7) {
 | 
				
			||||||
 | 
					            this.month -= 1;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        this.day = parseInt(e.target.innerText);
 | 
				
			||||||
 | 
					        this.value = new Date(this.year, this.month, this.day);
 | 
				
			||||||
 | 
					        this.setInputValue();
 | 
				
			||||||
 | 
					        this.setHeaderContent();
 | 
				
			||||||
 | 
					        this.setBodyContent();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /** @param {Event} e */
 | 
				
			||||||
 | 
					    DTBox.prototype.onMonthSelected = function (e) {
 | 
				
			||||||
 | 
					        var col = 0;
 | 
				
			||||||
 | 
					        var row = 2;
 | 
				
			||||||
 | 
					        var cell = e.target;
 | 
				
			||||||
 | 
					        if (cell.parentNode.nextSibling){
 | 
				
			||||||
 | 
					            row = cell.parentNode.previousSibling ? 1: 0;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if (cell.previousSibling) {
 | 
				
			||||||
 | 
					            col = 3;
 | 
				
			||||||
 | 
					            if (cell.nextSibling) {
 | 
				
			||||||
 | 
					                col = cell.previousSibling.previousSibling ? 2 : 1;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        this.month = 4 * row + col;
 | 
				
			||||||
 | 
					        this.bodyType = "DAYS";
 | 
				
			||||||
 | 
					        this.setHeaderContent();
 | 
				
			||||||
 | 
					        this.setupBody();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /** @param {Event} e */
 | 
				
			||||||
 | 
					    DTBox.prototype.onYearSelected = function (e) {
 | 
				
			||||||
 | 
					        this.year = parseInt(e.target.innerText);
 | 
				
			||||||
 | 
					        this.bodyType = "MONTHS";
 | 
				
			||||||
 | 
					        this.setHeaderContent();
 | 
				
			||||||
 | 
					        this.setupBody();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /** @param {Event} e */
 | 
				
			||||||
 | 
					    DTBox.prototype.onHeaderChange = function (e) {
 | 
				
			||||||
 | 
					        var cell = e.target;
 | 
				
			||||||
 | 
					        if (cell.previousSibling && cell.nextSibling) {
 | 
				
			||||||
 | 
					            var idx = BODYTYPES.indexOf(this.bodyType);
 | 
				
			||||||
 | 
					            if (idx < 0 || !BODYTYPES[idx + 1]) {
 | 
				
			||||||
 | 
					                return;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            this.bodyType = BODYTYPES[idx + 1];
 | 
				
			||||||
 | 
					            this.setupBody();
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            var sign = cell.previousSibling ? 1 : -1;
 | 
				
			||||||
 | 
					            switch (this.bodyType) {
 | 
				
			||||||
 | 
					                case "DAYS":
 | 
				
			||||||
 | 
					                    this.month += sign * 1;
 | 
				
			||||||
 | 
					                    break;
 | 
				
			||||||
 | 
					                case "MONTHS":
 | 
				
			||||||
 | 
					                    this.year += sign * 1;
 | 
				
			||||||
 | 
					                    break;
 | 
				
			||||||
 | 
					                case "YEARS":
 | 
				
			||||||
 | 
					                    this.year += sign * 10;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            if (this.month > 11 || this.month < 0) {
 | 
				
			||||||
 | 
					                this.year += Math.floor(this.month / 11);
 | 
				
			||||||
 | 
					                this.month = this.month > 11 ? 0 : 11;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        this.setHeaderContent();
 | 
				
			||||||
 | 
					        this.setBodyContent();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @param {HTMLElement} elem 
 | 
				
			||||||
 | 
					     * @returns {{left:number, top:number}}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    function getOffset(elem) {
 | 
				
			||||||
 | 
					        var box = elem.getBoundingClientRect();
 | 
				
			||||||
 | 
					        var left = window.pageXOffset !== undefined ? window.pageXOffset : 
 | 
				
			||||||
 | 
					            (document.documentElement || document.body.parentNode || document.body).scrollLeft;
 | 
				
			||||||
 | 
					        var top = window.pageYOffset !== undefined ? window.pageYOffset : 
 | 
				
			||||||
 | 
					            (document.documentElement || document.body.parentNode || document.body).scrollTop;
 | 
				
			||||||
 | 
					        return { left: box.left + left, top: box.top + top };
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    function empty(e) {
 | 
				
			||||||
 | 
					        for (; e.children.length; ) e.removeChild(e.children[0]);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    function tryAppendChild(newChild, refNode) {
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            refNode.appendChild(newChild);
 | 
				
			||||||
 | 
					            return newChild;
 | 
				
			||||||
 | 
					        } catch (e) {
 | 
				
			||||||
 | 
					            console.trace(e);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /** @class */
 | 
				
			||||||
 | 
					    function hookFuncs() {
 | 
				
			||||||
 | 
					        /** @type {Handlers} */
 | 
				
			||||||
 | 
					        this._funcs = {};
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @param {string} key 
 | 
				
			||||||
 | 
					     * @param {Function} func 
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    hookFuncs.prototype.add = function(key, func){
 | 
				
			||||||
 | 
					        if (!this._funcs[key]){
 | 
				
			||||||
 | 
					            this._funcs[key] = [];
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        this._funcs[key].push(func)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @param {String} key 
 | 
				
			||||||
 | 
					     * @returns {Function[]} handlers
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    hookFuncs.prototype.get = function(key){
 | 
				
			||||||
 | 
					        return this._funcs[key] ? this._funcs[key] : [];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @param {Array.<string>} arr 
 | 
				
			||||||
 | 
					     * @param {String} string 
 | 
				
			||||||
 | 
					     * @returns {Array.<string>} sorted string
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    function sortByStringIndex(arr, string) {
 | 
				
			||||||
 | 
					        return arr.sort(function(a, b){
 | 
				
			||||||
 | 
					            var h = string.indexOf(a);
 | 
				
			||||||
 | 
					            var l = string.indexOf(b);
 | 
				
			||||||
 | 
					            var rank = 0;
 | 
				
			||||||
 | 
					            if (h < l) {
 | 
				
			||||||
 | 
					                rank = -1;
 | 
				
			||||||
 | 
					            } else if (l < h) {
 | 
				
			||||||
 | 
					                rank = 1;
 | 
				
			||||||
 | 
					            } else if (a.length > b.length) {
 | 
				
			||||||
 | 
					                rank = -1;
 | 
				
			||||||
 | 
					            } else if (b.length > a.length) {
 | 
				
			||||||
 | 
					                rank = 1;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            return rank;
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Remove keys from array that are not in format
 | 
				
			||||||
 | 
					     * @param {string[]} keys 
 | 
				
			||||||
 | 
					     * @param {string} format 
 | 
				
			||||||
 | 
					     * @returns {string[]} new filtered array
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    function filterFormatKeys(keys, format) {
 | 
				
			||||||
 | 
					        var out = [];
 | 
				
			||||||
 | 
					        var formatIdx = 0;
 | 
				
			||||||
 | 
					        for (var i = 0; i<keys.length; i++) {
 | 
				
			||||||
 | 
					            var key = keys[i];
 | 
				
			||||||
 | 
					            if (format.slice(formatIdx).indexOf(key) > -1) {
 | 
				
			||||||
 | 
					                formatIdx += key.length;
 | 
				
			||||||
 | 
					                out.push(key);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return out;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @template {StringNumObj} FormatObj
 | 
				
			||||||
 | 
					     * @param {string} value 
 | 
				
			||||||
 | 
					     * @param {string} format 
 | 
				
			||||||
 | 
					     * @param {FormatObj} formatObj 
 | 
				
			||||||
 | 
					     * @param {function(Object.<string, hookFuncs>): null} setHooks 
 | 
				
			||||||
 | 
					     * @returns {FormatObj} formatObj
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    function parseData(value, format, formatObj, setHooks) {
 | 
				
			||||||
 | 
					        var hooks = {
 | 
				
			||||||
 | 
					            canSkip: new hookFuncs(),
 | 
				
			||||||
 | 
					            updateValue: new hookFuncs(),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        var keys = sortByStringIndex(Object.keys(formatObj), format);
 | 
				
			||||||
 | 
					        var filterdKeys = filterFormatKeys(keys, format);
 | 
				
			||||||
 | 
					        var vstart = 0; // value start
 | 
				
			||||||
 | 
					        if (setHooks) {
 | 
				
			||||||
 | 
					            setHooks(hooks);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        for (var i = 0; i < keys.length; i++) {
 | 
				
			||||||
 | 
					            var key = keys[i];
 | 
				
			||||||
 | 
					            var fstart = format.indexOf(key);
 | 
				
			||||||
 | 
					            var _vstart = vstart; // next value start
 | 
				
			||||||
 | 
					            var val = null;
 | 
				
			||||||
 | 
					            var canSkip = false;
 | 
				
			||||||
 | 
					            var funcs = hooks.canSkip.get(key);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            vstart = vstart || fstart;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            for (var j = 0; j < funcs.length; j++) {
 | 
				
			||||||
 | 
					                if (funcs[j](formatObj)){
 | 
				
			||||||
 | 
					                    canSkip = true;
 | 
				
			||||||
 | 
					                    break;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            if (fstart > -1 && !canSkip) {
 | 
				
			||||||
 | 
					                var sep = null;
 | 
				
			||||||
 | 
					                var stop = vstart + key.length;
 | 
				
			||||||
 | 
					                var fnext = -1;
 | 
				
			||||||
 | 
					                var nextKeyIdx = i + 1;
 | 
				
			||||||
 | 
					                _vstart += key.length; // set next value start if current key is found
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                // get next format token used to determine separator
 | 
				
			||||||
 | 
					                while (fnext == -1 && nextKeyIdx < keys.length){
 | 
				
			||||||
 | 
					                    var nextKey = keys[nextKeyIdx];
 | 
				
			||||||
 | 
					                    nextKeyIdx += 1;
 | 
				
			||||||
 | 
					                    if (filterdKeys.indexOf(nextKey) === -1) {
 | 
				
			||||||
 | 
					                        continue;
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    fnext = nextKey ? format.indexOf(nextKey) : -1; // next format start
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                if (fnext > -1){
 | 
				
			||||||
 | 
					                    sep = format.slice(stop, fnext);
 | 
				
			||||||
 | 
					                    if (sep) {
 | 
				
			||||||
 | 
					                        var _stop = value.slice(vstart).indexOf(sep);
 | 
				
			||||||
 | 
					                        if (_stop && _stop > -1){
 | 
				
			||||||
 | 
					                            stop = _stop + vstart;
 | 
				
			||||||
 | 
					                            _vstart = stop + sep.length;
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                val = parseInt(value.slice(vstart, stop));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                var funcs = hooks.updateValue.get(key);
 | 
				
			||||||
 | 
					                for (var k = 0; k < funcs.length; k++) {
 | 
				
			||||||
 | 
					                    val = funcs[k](val, formatObj, vstart, stop);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            formatObj[key] = { index: vstart, value: val };
 | 
				
			||||||
 | 
					            vstart = _vstart; // set next value start
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return formatObj;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @param {String} value 
 | 
				
			||||||
 | 
					     * @param {DTS} settings 
 | 
				
			||||||
 | 
					     * @returns {Date} date object
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    function parseDate(value, settings) {
 | 
				
			||||||
 | 
					        /** @type {{yyyy:number=, yy:number=, mm:number=, dd:number=}} */
 | 
				
			||||||
 | 
					        var formatObj = {yyyy:null, yy:null, mm:null, dd:null};
 | 
				
			||||||
 | 
					        var format = ((settings.dateFormat) || '').toLowerCase();
 | 
				
			||||||
 | 
					        if (!format) {
 | 
				
			||||||
 | 
					            throw new TypeError('dateFormat not found (' + settings.dateFormat + ')');
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        var formatObj = parseData(value, format, formatObj, function(hooks){
 | 
				
			||||||
 | 
					            hooks.canSkip.add("yy", function(data){
 | 
				
			||||||
 | 
					                return data["yyyy"].value;
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					            hooks.updateValue.add("yy", function(val){
 | 
				
			||||||
 | 
					                return 100 * Math.floor(new Date().getFullYear() / 100) + val;
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        var year = formatObj["yyyy"].value || formatObj["yy"].value;
 | 
				
			||||||
 | 
					        var month = formatObj["mm"].value - 1;
 | 
				
			||||||
 | 
					        var date = formatObj["dd"].value;
 | 
				
			||||||
 | 
					        var result = new Date(year, month, date);
 | 
				
			||||||
 | 
					        return result;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @param {String} value 
 | 
				
			||||||
 | 
					     * @param {DTS} settings 
 | 
				
			||||||
 | 
					     * @returns {Number} time in milliseconds <= (24 * 60 * 60 * 1000) - 1
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    function parseTime(value, settings) {
 | 
				
			||||||
 | 
					        var format = ((settings.timeFormat) || '').toLowerCase();
 | 
				
			||||||
 | 
					        if (!format) {
 | 
				
			||||||
 | 
					            throw new TypeError('timeFormat not found (' + settings.timeFormat + ')');
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        /** @type {{hh:number=, mm:number=, ss:number=, a:string=}} */
 | 
				
			||||||
 | 
					        var formatObj = {hh:null, mm:null, ss:null, a:null};
 | 
				
			||||||
 | 
					        var formatObj = parseData(value, format, formatObj, function(hooks){
 | 
				
			||||||
 | 
					            hooks.updateValue.add("a", function(val, data, start, stop){
 | 
				
			||||||
 | 
					                return value.slice(start, start + 2);
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        var hours = formatObj["hh"].value;
 | 
				
			||||||
 | 
					        var minutes = formatObj["mm"].value;
 | 
				
			||||||
 | 
					        var seconds = formatObj["ss"].value;
 | 
				
			||||||
 | 
					        var am_pm = formatObj["a"].value;
 | 
				
			||||||
 | 
					        var am_pm_lower = am_pm ? am_pm.toLowerCase() : am_pm;
 | 
				
			||||||
 | 
					        if (am_pm && ["am", "pm"].indexOf(am_pm_lower) > -1){
 | 
				
			||||||
 | 
					            if (am_pm_lower == 'am' && hours == 12){
 | 
				
			||||||
 | 
					                hours = 0;
 | 
				
			||||||
 | 
					            } else if (am_pm_lower == 'pm') {
 | 
				
			||||||
 | 
					                hours += 12;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        var time = hours * 60 * 60 * 1000 + minutes * 60 * 1000 + seconds * 1000;
 | 
				
			||||||
 | 
					        return time;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @param {Date} value 
 | 
				
			||||||
 | 
					     * @param {DTS} settings 
 | 
				
			||||||
 | 
					     * @returns {String} date string
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    function renderDate(value, settings) {
 | 
				
			||||||
 | 
					        var format = settings.dateFormat.toLowerCase();
 | 
				
			||||||
 | 
					        var date = value.getDate();
 | 
				
			||||||
 | 
					        var month = value.getMonth() + 1;
 | 
				
			||||||
 | 
					        var year = value.getFullYear();
 | 
				
			||||||
 | 
					        var yearShort = year % 100;
 | 
				
			||||||
 | 
					        var formatObj = {
 | 
				
			||||||
 | 
					            dd: date < 10 ? "0" + date : date,
 | 
				
			||||||
 | 
					            mm: month < 10 ? "0" + month : month,
 | 
				
			||||||
 | 
					            yyyy: year,
 | 
				
			||||||
 | 
					            yy: yearShort < 10 ? "0" + yearShort : yearShort
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        var str = format.replace(settings.dateFormatRegEx, function (found) {
 | 
				
			||||||
 | 
					            return formatObj[found];
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        return str;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @param {Date} value 
 | 
				
			||||||
 | 
					     * @param {DTS} settings 
 | 
				
			||||||
 | 
					     * @returns {String} date string
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    function renderTime(value, settings) {
 | 
				
			||||||
 | 
					        var Format = settings.timeFormat;
 | 
				
			||||||
 | 
					        var format = Format.toLowerCase();
 | 
				
			||||||
 | 
					        var hours = value.getHours();
 | 
				
			||||||
 | 
					        var minutes = value.getMinutes();
 | 
				
			||||||
 | 
					        var seconds = value.getSeconds();
 | 
				
			||||||
 | 
					        var am_pm = null;
 | 
				
			||||||
 | 
					        var hh_am_pm = null;
 | 
				
			||||||
 | 
					        if (format.indexOf('a') > -1) {
 | 
				
			||||||
 | 
					            am_pm = hours >= 12 ? 'pm' : 'am';
 | 
				
			||||||
 | 
					            am_pm = Format.indexOf('A') > -1 ? am_pm.toUpperCase() : am_pm;
 | 
				
			||||||
 | 
					            hh_am_pm = hours == 0 ? '12' : (hours > 12 ? hours%12 : hours);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        var formatObj = {
 | 
				
			||||||
 | 
					            hh: am_pm ? hh_am_pm : (hours < 10 ? "0" + hours : hours),
 | 
				
			||||||
 | 
					            mm: minutes < 10 ? "0" + minutes : minutes,
 | 
				
			||||||
 | 
					            ss: seconds < 10 ? "0" + seconds : seconds,
 | 
				
			||||||
 | 
					            a: am_pm,
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        var str = format.replace(settings.timeFormatRegEx, function (found) {
 | 
				
			||||||
 | 
					            return formatObj[found];
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        return str;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * checks if two dates are equal
 | 
				
			||||||
 | 
					     * @param {Date} date1 
 | 
				
			||||||
 | 
					     * @param {Date} date2 
 | 
				
			||||||
 | 
					     * @returns {Boolean} true or false
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    function isEqualDate(date1, date2) {
 | 
				
			||||||
 | 
					        if (!(date1 && date2)) return false;
 | 
				
			||||||
 | 
					        return (date1.getFullYear() == date2.getFullYear() && 
 | 
				
			||||||
 | 
					                date1.getMonth() == date2.getMonth() && 
 | 
				
			||||||
 | 
					                date1.getDate() == date2.getDate());
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @param {Number} val 
 | 
				
			||||||
 | 
					     * @param {Number} pad 
 | 
				
			||||||
 | 
					     * @param {*} default_val 
 | 
				
			||||||
 | 
					     * @returns {String} padded string
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    function padded(val, pad, default_val) {
 | 
				
			||||||
 | 
					        var default_val = default_val || 0;
 | 
				
			||||||
 | 
					        var valStr = '' + (parseInt(val) || default_val);
 | 
				
			||||||
 | 
					        var diff = Math.max(pad, valStr.length) - valStr.length;
 | 
				
			||||||
 | 
					        return ('' + default_val).repeat(diff) + valStr;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * @template X
 | 
				
			||||||
 | 
					     * @template Y
 | 
				
			||||||
 | 
					     * @param {X} obj 
 | 
				
			||||||
 | 
					     * @param {Y} objDefaults 
 | 
				
			||||||
 | 
					     * @returns {X|Y} merged object
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    function setDefaults(obj, objDefaults) {
 | 
				
			||||||
 | 
					        var keys = Object.keys(objDefaults);
 | 
				
			||||||
 | 
					        for (var i=0; i<keys.length; i++) {
 | 
				
			||||||
 | 
					            var key = keys[i];
 | 
				
			||||||
 | 
					            if (!Object.prototype.hasOwnProperty.call(obj, key)) {
 | 
				
			||||||
 | 
					                obj[key] = objDefaults[key];
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return obj;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    window.dtsel = Object.create({},{
 | 
				
			||||||
 | 
					        DTS: { value: DTS },
 | 
				
			||||||
 | 
					        DTObj: { value: DTBox },
 | 
				
			||||||
 | 
					        fn: {
 | 
				
			||||||
 | 
					            value: Object.defineProperties({}, {
 | 
				
			||||||
 | 
					                empty: { value: empty },
 | 
				
			||||||
 | 
					                appendAfter: {
 | 
				
			||||||
 | 
					                    value: function (newElem, refNode) {
 | 
				
			||||||
 | 
					                        refNode.parentNode.insertBefore(newElem, refNode.nextSibling);
 | 
				
			||||||
 | 
					                    },
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					                getOffset: { value: getOffset },
 | 
				
			||||||
 | 
					                parseDate: { value: parseDate },
 | 
				
			||||||
 | 
					                renderDate: { value: renderDate },
 | 
				
			||||||
 | 
					                parseTime: {value: parseTime},
 | 
				
			||||||
 | 
					                renderTime: {value: renderTime},
 | 
				
			||||||
 | 
					                setDefaults: {value: setDefaults},
 | 
				
			||||||
 | 
					            }),
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					})();
 | 
				
			||||||
							
								
								
									
										23
									
								
								reminder-dashboard/public/static/js/expand.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,23 @@
 | 
				
			|||||||
 | 
					function collapse_all() {
 | 
				
			||||||
 | 
					    document.querySelectorAll("div.reminderContent:not(.creator)").forEach((el) => {
 | 
				
			||||||
 | 
					        el.classList.add("is-collapsed");
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function expand_all() {
 | 
				
			||||||
 | 
					    document.querySelectorAll("div.reminderContent:not(.creator)").forEach((el) => {
 | 
				
			||||||
 | 
					        el.classList.remove("is-collapsed");
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const expandAll = document.querySelector("#expandAll");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					expandAll.addEventListener("change", (ev) => {
 | 
				
			||||||
 | 
					    if (ev.target.value === "expand") {
 | 
				
			||||||
 | 
					        expand_all();
 | 
				
			||||||
 | 
					    } else if (ev.target.value === "collapse") {
 | 
				
			||||||
 | 
					        collapse_all();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    ev.target.value = "";
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
							
								
								
									
										94
									
								
								reminder-dashboard/public/static/js/interval.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,94 @@
 | 
				
			|||||||
 | 
					function get_interval(element) {
 | 
				
			||||||
 | 
					    let months = element.querySelector('input[name="interval_months"]').value;
 | 
				
			||||||
 | 
					    let days = element.querySelector('input[name="interval_days"]').value;
 | 
				
			||||||
 | 
					    let hours = element.querySelector('input[name="interval_hours"]').value;
 | 
				
			||||||
 | 
					    let minutes = element.querySelector('input[name="interval_minutes"]').value;
 | 
				
			||||||
 | 
					    let seconds = element.querySelector('input[name="interval_seconds"]').value;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					        months: parseInt(months) || null,
 | 
				
			||||||
 | 
					        days: parseInt(days) || null,
 | 
				
			||||||
 | 
					        seconds:
 | 
				
			||||||
 | 
					            (parseInt(hours) || 0) * 3600 +
 | 
				
			||||||
 | 
					                (parseInt(minutes) || 0) * 60 +
 | 
				
			||||||
 | 
					                (parseInt(seconds) || 0) || null,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function update_interval(element) {
 | 
				
			||||||
 | 
					    let months = element.querySelector('input[name="interval_months"]');
 | 
				
			||||||
 | 
					    let days = element.querySelector('input[name="interval_days"]');
 | 
				
			||||||
 | 
					    let hours = element.querySelector('input[name="interval_hours"]');
 | 
				
			||||||
 | 
					    let minutes = element.querySelector('input[name="interval_minutes"]');
 | 
				
			||||||
 | 
					    let seconds = element.querySelector('input[name="interval_seconds"]');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let interval = get_interval(element);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (interval.months === null && interval.days === null && interval.seconds === null) {
 | 
				
			||||||
 | 
					        months.value = "";
 | 
				
			||||||
 | 
					        days.value = "";
 | 
				
			||||||
 | 
					        hours.value = "";
 | 
				
			||||||
 | 
					        minutes.value = "";
 | 
				
			||||||
 | 
					        seconds.value = "";
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        months.value = months.value.padStart(1, "0");
 | 
				
			||||||
 | 
					        days.value = days.value.padStart(1, "0");
 | 
				
			||||||
 | 
					        hours.value = hours.value.padStart(2, "0");
 | 
				
			||||||
 | 
					        minutes.value = minutes.value.padStart(2, "0");
 | 
				
			||||||
 | 
					        seconds.value = seconds.value.padStart(2, "0");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (seconds.value >= 60) {
 | 
				
			||||||
 | 
					            let quotient = Math.floor(seconds.value / 60);
 | 
				
			||||||
 | 
					            let remainder = seconds.value % 60;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            seconds.value = String(remainder).padStart(2, "0");
 | 
				
			||||||
 | 
					            minutes.value = String(Number(minutes.value) + Number(quotient)).padStart(
 | 
				
			||||||
 | 
					                2,
 | 
				
			||||||
 | 
					                "0"
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if (minutes.value >= 60) {
 | 
				
			||||||
 | 
					            let quotient = Math.floor(minutes.value / 60);
 | 
				
			||||||
 | 
					            let remainder = minutes.value % 60;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            minutes.value = String(remainder).padStart(2, "0");
 | 
				
			||||||
 | 
					            hours.value = String(Number(hours.value) + Number(quotient)).padStart(2, "0");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const $intervalGroup = document.querySelector(".interval-group");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					document.querySelector(".interval-group").addEventListener(
 | 
				
			||||||
 | 
					    "blur",
 | 
				
			||||||
 | 
					    (ev) => {
 | 
				
			||||||
 | 
					        if (ev.target.nodeName !== "BUTTON") update_interval($intervalGroup);
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    true
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					$intervalGroup.querySelector("button.clear").addEventListener("click", () => {
 | 
				
			||||||
 | 
					    $intervalGroup.querySelectorAll("input").forEach((el) => {
 | 
				
			||||||
 | 
					        el.value = "";
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					document.addEventListener("remindersLoaded", (event) => {
 | 
				
			||||||
 | 
					    for (reminder of event.detail) {
 | 
				
			||||||
 | 
					        let $intervalGroup = reminder.node.querySelector(".interval-group");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $intervalGroup.addEventListener(
 | 
				
			||||||
 | 
					            "blur",
 | 
				
			||||||
 | 
					            (ev) => {
 | 
				
			||||||
 | 
					                if (ev.target.nodeName !== "BUTTON") update_interval($intervalGroup);
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            true
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $intervalGroup.querySelector("button.clear").addEventListener("click", () => {
 | 
				
			||||||
 | 
					            $intervalGroup.querySelectorAll("input").forEach((el) => {
 | 
				
			||||||
 | 
					                el.value = "";
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
							
								
								
									
										7
									
								
								reminder-dashboard/public/static/js/iro.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										2
									
								
								reminder-dashboard/public/static/js/js.cookie.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,2 @@
 | 
				
			|||||||
 | 
					/*! js-cookie v3.0.0-rc.0 | MIT */
 | 
				
			||||||
 | 
					!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e=e||self,function(){var r=e.Cookies,n=e.Cookies=t();n.noConflict=function(){return e.Cookies=r,n}}())}(this,function(){"use strict";function e(e){for(var t=1;t<arguments.length;t++){var r=arguments[t];for(var n in r)e[n]=r[n]}return e}var t={read:function(e){return e.replace(/%3B/g,";")},write:function(e){return e.replace(/;/g,"%3B")}};return function r(n,i){function o(r,o,u){if("undefined"!=typeof document){"number"==typeof(u=e({},i,u)).expires&&(u.expires=new Date(Date.now()+864e5*u.expires)),u.expires&&(u.expires=u.expires.toUTCString()),r=t.write(r).replace(/=/g,"%3D"),o=n.write(String(o),r);var c="";for(var f in u)u[f]&&(c+="; "+f,!0!==u[f]&&(c+="="+u[f].split(";")[0]));return document.cookie=r+"="+o+c}}return Object.create({set:o,get:function(e){if("undefined"!=typeof document&&(!arguments.length||e)){for(var r=document.cookie?document.cookie.split("; "):[],i={},o=0;o<r.length;o++){var u=r[o].split("="),c=u.slice(1).join("="),f=t.read(u[0]).replace(/%3D/g,"=");if(i[f]=n.read(c,f),e===f)break}return e?i[e]:i}},remove:function(t,r){o(t,"",e({},r,{expires:-1}))},withAttributes:function(t){return r(this.converter,e({},this.attributes,t))},withConverter:function(t){return r(e({},this.converter,t),this.attributes)}},{attributes:{value:Object.freeze(i)},converter:{value:Object.freeze(n)}})}(t,{path:"/"})});
 | 
				
			||||||
							
								
								
									
										1
									
								
								reminder-dashboard/public/static/js/luxon.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										1087
									
								
								reminder-dashboard/public/static/js/main.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										16
									
								
								reminder-dashboard/public/static/js/reporter.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,16 @@
 | 
				
			|||||||
 | 
					const REPORTER_ID = crypto.randomUUID();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					window.addEventListener("error", async (ev) => {
 | 
				
			||||||
 | 
					    await fetch("/report", {
 | 
				
			||||||
 | 
					        method: "POST",
 | 
				
			||||||
 | 
					        body: JSON.stringify({
 | 
				
			||||||
 | 
					            reporterId: REPORTER_ID,
 | 
				
			||||||
 | 
					            url: window.location.href,
 | 
				
			||||||
 | 
					            relativeTimestamp: ev.timeStamp,
 | 
				
			||||||
 | 
					            errorMessage: ev.message,
 | 
				
			||||||
 | 
					            errorLine: ev.lineno,
 | 
				
			||||||
 | 
					            errorFile: ev.filename,
 | 
				
			||||||
 | 
					            errorType: ev.type,
 | 
				
			||||||
 | 
					        }),
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
							
								
								
									
										70
									
								
								reminder-dashboard/public/static/js/sort.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,70 @@
 | 
				
			|||||||
 | 
					let guildReminders = document.querySelector("#guildReminders");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function sort_by(cond) {
 | 
				
			||||||
 | 
					    if (cond === "channel") {
 | 
				
			||||||
 | 
					        [...guildReminders.children]
 | 
				
			||||||
 | 
					            .sort((a, b) => {
 | 
				
			||||||
 | 
					                let channel1 = a.querySelector("select.channel-selector").value;
 | 
				
			||||||
 | 
					                let channel2 = b.querySelector("select.channel-selector").value;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                return channel1 > channel2 ? 1 : -1;
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					            .forEach((node) => guildReminders.appendChild(node));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // go through and add channel categories
 | 
				
			||||||
 | 
					        let currentChannelGroup = null;
 | 
				
			||||||
 | 
					        for (let child of guildReminders.querySelectorAll("div.reminderContent")) {
 | 
				
			||||||
 | 
					            let thisChannelGroup = child.querySelector("select.channel-selector").value;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (currentChannelGroup !== thisChannelGroup) {
 | 
				
			||||||
 | 
					                let newNode = document.createElement("div");
 | 
				
			||||||
 | 
					                newNode.textContent =
 | 
				
			||||||
 | 
					                    "#" + channels.find((a) => a.id === thisChannelGroup).name;
 | 
				
			||||||
 | 
					                newNode.classList.add("channel-tag");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                guildReminders.insertBefore(newNode, child);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                currentChannelGroup = thisChannelGroup;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        // remove any channel tags if previous ordering was by channel
 | 
				
			||||||
 | 
					        guildReminders.querySelectorAll("div.channel-tag").forEach((el) => {
 | 
				
			||||||
 | 
					            el.remove();
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (cond === "time") {
 | 
				
			||||||
 | 
					            [...guildReminders.children]
 | 
				
			||||||
 | 
					                .sort((a, b) => {
 | 
				
			||||||
 | 
					                    let time1 = luxon.DateTime.fromISO(
 | 
				
			||||||
 | 
					                        a.querySelector('input[name="time"]').value
 | 
				
			||||||
 | 
					                    );
 | 
				
			||||||
 | 
					                    let time2 = luxon.DateTime.fromISO(
 | 
				
			||||||
 | 
					                        b.querySelector('input[name="time"]').value
 | 
				
			||||||
 | 
					                    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    return time1 > time2 ? 1 : -1;
 | 
				
			||||||
 | 
					                })
 | 
				
			||||||
 | 
					                .forEach((node) => guildReminders.appendChild(node));
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            [...guildReminders.children]
 | 
				
			||||||
 | 
					                .sort((a, b) => {
 | 
				
			||||||
 | 
					                    let name1 = a.querySelector('input[name="name"]').value;
 | 
				
			||||||
 | 
					                    let name2 = b.querySelector('input[name="name"]').value;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    return name1 > name2 ? 1 : -1;
 | 
				
			||||||
 | 
					                })
 | 
				
			||||||
 | 
					                .forEach((node) => guildReminders.appendChild(node));
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const selector = document.querySelector("#orderBy");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					selector.addEventListener("change", () => {
 | 
				
			||||||
 | 
					    sort_by(selector.value);
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					document.addEventListener("remindersLoaded", () => {
 | 
				
			||||||
 | 
					    sort_by(selector.value);
 | 
				
			||||||
 | 
					});
 | 
				
			||||||