Compare commits
	
		
			30 Commits
		
	
	
		
			next
			...
			06e1474396
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 06e1474396 | |||
|  | adb9c728f4 | ||
|  | fc02eaea4a | ||
|  | 91f87302fb | ||
|  | 97f186dc33 | ||
|  | 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 | 
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -3,3 +3,5 @@ | |||||||
| /venv | /venv | ||||||
| .cargo | .cargo | ||||||
| /.idea | /.idea | ||||||
|  | web/static/index.html | ||||||
|  | web/static/assets | ||||||
|   | |||||||
							
								
								
									
										3
									
								
								.gitmodules
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								.gitmodules
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | |||||||
|  | [submodule "reminder-dashboard"] | ||||||
|  | 	path = reminder-dashboard | ||||||
|  | 	url = gitea@gitea.jellypro.xyz:jude/reminder-dashboard | ||||||
							
								
								
									
										128
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										128
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							| @@ -59,7 +59,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" | |||||||
| checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" | checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  "cfg-if", |  "cfg-if", | ||||||
|  "getrandom 0.2.10", |  "getrandom", | ||||||
|  "once_cell", |  "once_cell", | ||||||
|  "version_check", |  "version_check", | ||||||
| ] | ] | ||||||
| @@ -345,7 +345,7 @@ dependencies = [ | |||||||
|  "base64 0.21.4", |  "base64 0.21.4", | ||||||
|  "hkdf", |  "hkdf", | ||||||
|  "percent-encoding", |  "percent-encoding", | ||||||
|  "rand 0.8.5", |  "rand", | ||||||
|  "sha2", |  "sha2", | ||||||
|  "subtle", |  "subtle", | ||||||
|  "time", |  "time", | ||||||
| @@ -437,7 +437,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" | |||||||
| checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" | checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  "generic-array", |  "generic-array", | ||||||
|  "rand_core 0.6.4", |  "rand_core", | ||||||
|  "typenum", |  "typenum", | ||||||
| ] | ] | ||||||
|  |  | ||||||
| @@ -910,17 +910,6 @@ dependencies = [ | |||||||
|  "version_check", |  "version_check", | ||||||
| ] | ] | ||||||
|  |  | ||||||
| [[package]] |  | ||||||
| name = "getrandom" |  | ||||||
| version = "0.1.16" |  | ||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" |  | ||||||
| checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" |  | ||||||
| dependencies = [ |  | ||||||
|  "cfg-if", |  | ||||||
|  "libc", |  | ||||||
|  "wasi 0.9.0+wasi-snapshot-preview1", |  | ||||||
| ] |  | ||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "getrandom" | name = "getrandom" | ||||||
| version = "0.2.10" | version = "0.2.10" | ||||||
| @@ -930,7 +919,7 @@ dependencies = [ | |||||||
|  "cfg-if", |  "cfg-if", | ||||||
|  "js-sys", |  "js-sys", | ||||||
|  "libc", |  "libc", | ||||||
|  "wasi 0.11.0+wasi-snapshot-preview1", |  "wasi", | ||||||
|  "wasm-bindgen", |  "wasm-bindgen", | ||||||
| ] | ] | ||||||
|  |  | ||||||
| @@ -1507,7 +1496,7 @@ checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" | |||||||
| dependencies = [ | dependencies = [ | ||||||
|  "libc", |  "libc", | ||||||
|  "log", |  "log", | ||||||
|  "wasi 0.11.0+wasi-snapshot-preview1", |  "wasi", | ||||||
|  "windows-sys 0.48.0", |  "windows-sys 0.48.0", | ||||||
| ] | ] | ||||||
|  |  | ||||||
| @@ -1619,7 +1608,7 @@ dependencies = [ | |||||||
|  "num-integer", |  "num-integer", | ||||||
|  "num-iter", |  "num-iter", | ||||||
|  "num-traits", |  "num-traits", | ||||||
|  "rand 0.8.5", |  "rand", | ||||||
|  "smallvec", |  "smallvec", | ||||||
|  "zeroize", |  "zeroize", | ||||||
| ] | ] | ||||||
| @@ -1673,9 +1662,9 @@ checksum = "c38841cdd844847e3e7c8d29cef9dcfed8877f8f56f9071f77843ecf3baf937f" | |||||||
| dependencies = [ | dependencies = [ | ||||||
|  "base64 0.13.1", |  "base64 0.13.1", | ||||||
|  "chrono", |  "chrono", | ||||||
|  "getrandom 0.2.10", |  "getrandom", | ||||||
|  "http", |  "http", | ||||||
|  "rand 0.8.5", |  "rand", | ||||||
|  "reqwest", |  "reqwest", | ||||||
|  "serde", |  "serde", | ||||||
|  "serde_json", |  "serde_json", | ||||||
| @@ -1912,7 +1901,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" | |||||||
| checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" | checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  "phf_shared", |  "phf_shared", | ||||||
|  "rand 0.8.5", |  "rand", | ||||||
| ] | ] | ||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| @@ -2070,6 +2059,27 @@ dependencies = [ | |||||||
|  "yansi", |  "yansi", | ||||||
| ] | ] | ||||||
|  |  | ||||||
|  | [[package]] | ||||||
|  | name = "prometheus" | ||||||
|  | version = "0.13.3" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "449811d15fbdf5ceb5c1144416066429cf82316e2ec8ce0c1f6f8a02e7bbcf8c" | ||||||
|  | dependencies = [ | ||||||
|  |  "cfg-if", | ||||||
|  |  "fnv", | ||||||
|  |  "lazy_static", | ||||||
|  |  "memchr", | ||||||
|  |  "parking_lot", | ||||||
|  |  "protobuf", | ||||||
|  |  "thiserror", | ||||||
|  | ] | ||||||
|  |  | ||||||
|  | [[package]] | ||||||
|  | name = "protobuf" | ||||||
|  | version = "2.28.0" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "106dd99e98437432fed6519dedecfade6a06a73bb7b2a1e019fdd2bee5778d94" | ||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "quote" | name = "quote" | ||||||
| version = "1.0.33" | version = "1.0.33" | ||||||
| @@ -2079,19 +2089,6 @@ dependencies = [ | |||||||
|  "proc-macro2", |  "proc-macro2", | ||||||
| ] | ] | ||||||
|  |  | ||||||
| [[package]] |  | ||||||
| name = "rand" |  | ||||||
| version = "0.7.3" |  | ||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" |  | ||||||
| checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" |  | ||||||
| dependencies = [ |  | ||||||
|  "getrandom 0.1.16", |  | ||||||
|  "libc", |  | ||||||
|  "rand_chacha 0.2.2", |  | ||||||
|  "rand_core 0.5.1", |  | ||||||
|  "rand_hc", |  | ||||||
| ] |  | ||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "rand" | name = "rand" | ||||||
| version = "0.8.5" | version = "0.8.5" | ||||||
| @@ -2099,18 +2096,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" | |||||||
| checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  "libc", |  "libc", | ||||||
|  "rand_chacha 0.3.1", |  "rand_chacha", | ||||||
|  "rand_core 0.6.4", |  "rand_core", | ||||||
| ] |  | ||||||
|  |  | ||||||
| [[package]] |  | ||||||
| name = "rand_chacha" |  | ||||||
| version = "0.2.2" |  | ||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" |  | ||||||
| checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" |  | ||||||
| dependencies = [ |  | ||||||
|  "ppv-lite86", |  | ||||||
|  "rand_core 0.5.1", |  | ||||||
| ] | ] | ||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| @@ -2120,16 +2107,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" | |||||||
| checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  "ppv-lite86", |  "ppv-lite86", | ||||||
|  "rand_core 0.6.4", |  "rand_core", | ||||||
| ] |  | ||||||
|  |  | ||||||
| [[package]] |  | ||||||
| name = "rand_core" |  | ||||||
| version = "0.5.1" |  | ||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" |  | ||||||
| checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" |  | ||||||
| dependencies = [ |  | ||||||
|  "getrandom 0.1.16", |  | ||||||
| ] | ] | ||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| @@ -2138,16 +2116,7 @@ version = "0.6.4" | |||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  "getrandom 0.2.10", |  "getrandom", | ||||||
| ] |  | ||||||
|  |  | ||||||
| [[package]] |  | ||||||
| name = "rand_hc" |  | ||||||
| version = "0.2.0" |  | ||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" |  | ||||||
| checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" |  | ||||||
| dependencies = [ |  | ||||||
|  "rand_core 0.5.1", |  | ||||||
| ] | ] | ||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| @@ -2225,7 +2194,7 @@ checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" | |||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "reminder-rs" | name = "reminder-rs" | ||||||
| version = "1.6.40" | version = "1.6.48" | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  "base64 0.21.4", |  "base64 0.21.4", | ||||||
|  "chrono", |  "chrono", | ||||||
| @@ -2239,7 +2208,7 @@ dependencies = [ | |||||||
|  "num-integer", |  "num-integer", | ||||||
|  "poise", |  "poise", | ||||||
|  "postman", |  "postman", | ||||||
|  "rand 0.8.5", |  "rand", | ||||||
|  "regex", |  "regex", | ||||||
|  "reminder_web", |  "reminder_web", | ||||||
|  "reqwest", |  "reqwest", | ||||||
| @@ -2253,7 +2222,7 @@ dependencies = [ | |||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "reminder_web" | name = "reminder_web" | ||||||
| version = "0.1.0" | version = "0.1.3" | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  "base64 0.13.1", |  "base64 0.13.1", | ||||||
|  "chrono", |  "chrono", | ||||||
| @@ -2262,7 +2231,8 @@ dependencies = [ | |||||||
|  "lazy_static", |  "lazy_static", | ||||||
|  "log", |  "log", | ||||||
|  "oauth2", |  "oauth2", | ||||||
|  "rand 0.7.3", |  "prometheus", | ||||||
|  |  "rand", | ||||||
|  "reqwest", |  "reqwest", | ||||||
|  "rocket", |  "rocket", | ||||||
|  "rocket_dyn_templates", |  "rocket_dyn_templates", | ||||||
| @@ -2373,7 +2343,7 @@ dependencies = [ | |||||||
|  "num_cpus", |  "num_cpus", | ||||||
|  "parking_lot", |  "parking_lot", | ||||||
|  "pin-project-lite", |  "pin-project-lite", | ||||||
|  "rand 0.8.5", |  "rand", | ||||||
|  "ref-cast", |  "ref-cast", | ||||||
|  "rocket_codegen", |  "rocket_codegen", | ||||||
|  "rocket_http", |  "rocket_http", | ||||||
| @@ -2462,7 +2432,7 @@ dependencies = [ | |||||||
|  "num-traits", |  "num-traits", | ||||||
|  "pkcs1", |  "pkcs1", | ||||||
|  "pkcs8", |  "pkcs8", | ||||||
|  "rand_core 0.6.4", |  "rand_core", | ||||||
|  "signature", |  "signature", | ||||||
|  "spki", |  "spki", | ||||||
|  "subtle", |  "subtle", | ||||||
| @@ -2779,7 +2749,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" | |||||||
| checksum = "5e1788eed21689f9cf370582dfc467ef36ed9c707f073528ddafa8d83e3b8500" | checksum = "5e1788eed21689f9cf370582dfc467ef36ed9c707f073528ddafa8d83e3b8500" | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  "digest", |  "digest", | ||||||
|  "rand_core 0.6.4", |  "rand_core", | ||||||
| ] | ] | ||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| @@ -2996,7 +2966,7 @@ dependencies = [ | |||||||
|  "memchr", |  "memchr", | ||||||
|  "once_cell", |  "once_cell", | ||||||
|  "percent-encoding", |  "percent-encoding", | ||||||
|  "rand 0.8.5", |  "rand", | ||||||
|  "rsa", |  "rsa", | ||||||
|  "serde", |  "serde", | ||||||
|  "sha1", |  "sha1", | ||||||
| @@ -3038,7 +3008,7 @@ dependencies = [ | |||||||
|  "memchr", |  "memchr", | ||||||
|  "num-bigint", |  "num-bigint", | ||||||
|  "once_cell", |  "once_cell", | ||||||
|  "rand 0.8.5", |  "rand", | ||||||
|  "serde", |  "serde", | ||||||
|  "serde_json", |  "serde_json", | ||||||
|  "sha1", |  "sha1", | ||||||
| @@ -3164,7 +3134,7 @@ dependencies = [ | |||||||
|  "percent-encoding", |  "percent-encoding", | ||||||
|  "pest", |  "pest", | ||||||
|  "pest_derive", |  "pest_derive", | ||||||
|  "rand 0.8.5", |  "rand", | ||||||
|  "regex", |  "regex", | ||||||
|  "serde", |  "serde", | ||||||
|  "serde_json", |  "serde_json", | ||||||
| @@ -3461,7 +3431,7 @@ dependencies = [ | |||||||
|  "http", |  "http", | ||||||
|  "httparse", |  "httparse", | ||||||
|  "log", |  "log", | ||||||
|  "rand 0.8.5", |  "rand", | ||||||
|  "rustls 0.20.9", |  "rustls 0.20.9", | ||||||
|  "sha-1", |  "sha-1", | ||||||
|  "thiserror", |  "thiserror", | ||||||
| @@ -3676,12 +3646,6 @@ dependencies = [ | |||||||
|  "try-lock", |  "try-lock", | ||||||
| ] | ] | ||||||
|  |  | ||||||
| [[package]] |  | ||||||
| name = "wasi" |  | ||||||
| version = "0.9.0+wasi-snapshot-preview1" |  | ||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" |  | ||||||
| checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" |  | ||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "wasi" | name = "wasi" | ||||||
| version = "0.11.0+wasi-snapshot-preview1" | version = "0.11.0+wasi-snapshot-preview1" | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| [package] | [package] | ||||||
| name = "reminder-rs" | name = "reminder-rs" | ||||||
| version = "1.6.40" | version = "1.6.48" | ||||||
| authors = ["Jude Southworth <judesouthworth@pm.me>"] | authors = ["Jude Southworth <judesouthworth@pm.me>"] | ||||||
| edition = "2021" | edition = "2021" | ||||||
| license = "AGPL-3.0 only" | license = "AGPL-3.0 only" | ||||||
| @@ -11,7 +11,7 @@ poise = "0.5" | |||||||
| dotenv = "0.15" | dotenv = "0.15" | ||||||
| tokio = { version = "1", features = ["process", "full"] } | tokio = { version = "1", features = ["process", "full"] } | ||||||
| reqwest = "0.11" | reqwest = "0.11" | ||||||
| lazy-regex = "3.0" | lazy-regex = "3.0.2" | ||||||
| regex = "1.9" | regex = "1.9" | ||||||
| log = "0.4" | log = "0.4" | ||||||
| env_logger = "0.10" | env_logger = "0.10" | ||||||
| @@ -43,6 +43,7 @@ assets = [ | |||||||
|     ["conf/default.env", "etc/reminder-rs/config.env", "600"], |     ["conf/default.env", "etc/reminder-rs/config.env", "600"], | ||||||
|     ["conf/Rocket.toml", "etc/reminder-rs/Rocket.toml", "600"], |     ["conf/Rocket.toml", "etc/reminder-rs/Rocket.toml", "600"], | ||||||
|     ["web/static/**/*", "lib/reminder-rs/static", "644"], |     ["web/static/**/*", "lib/reminder-rs/static", "644"], | ||||||
|  |     ["reminder-dashboard/dist/static/**/*", "lib/reminder-rs/static", "644"], | ||||||
|     ["web/templates/**/*", "lib/reminder-rs/templates", "644"], |     ["web/templates/**/*", "lib/reminder-rs/templates", "644"], | ||||||
|     ["healthcheck", "lib/reminder-rs/healthcheck", "755"], |     ["healthcheck", "lib/reminder-rs/healthcheck", "755"], | ||||||
|     ["cron.d/reminder_health", "etc/cron.d/reminder_health", "644"], |     ["cron.d/reminder_health", "etc/cron.d/reminder_health", "644"], | ||||||
|   | |||||||
| @@ -1,19 +0,0 @@ | |||||||
| -- Drop existing constraint |  | ||||||
| ALTER TABLE `reminders` DROP CONSTRAINT `reminders_ibfk_1`; |  | ||||||
|  |  | ||||||
| ALTER TABLE `reminders` MODIFY COLUMN `channel_id` INT UNSIGNED; |  | ||||||
| ALTER TABLE `reminders` ADD COLUMN `guild_id` INT UNSIGNED; |  | ||||||
|  |  | ||||||
| ALTER TABLE `reminders` |  | ||||||
|     ADD CONSTRAINT `guild_id_fk` |  | ||||||
|         FOREIGN KEY (`guild_id`) |  | ||||||
|         REFERENCES `guilds`(`id`) |  | ||||||
|         ON DELETE CASCADE; |  | ||||||
|  |  | ||||||
| ALTER TABLE `reminders` |  | ||||||
|     ADD CONSTRAINT `channel_id_fk` |  | ||||||
|         FOREIGN KEY (`channel_id`) |  | ||||||
|         REFERENCES `channels`(`id`) |  | ||||||
|         ON DELETE SET NULL; |  | ||||||
|  |  | ||||||
| UPDATE `reminders` SET `guild_id` = (SELECT guilds.`id` FROM `channels` INNER JOIN `guilds` ON channels.guild_id = guilds.id WHERE reminders.channel_id = channels.id); |  | ||||||
| @@ -1,4 +0,0 @@ | |||||||
| ALTER TABLE reminders ADD COLUMN `status_change_time` DATETIME; |  | ||||||
|  |  | ||||||
| -- This is a best-guess as to the status change time. |  | ||||||
| UPDATE reminders SET `status_change_time` = `utc_time` WHERE `status` != 'pending'; |  | ||||||
| @@ -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; | ||||||
| @@ -237,11 +237,11 @@ impl Into<CreateEmbed> for Embed { | |||||||
| pub struct Reminder { | pub struct Reminder { | ||||||
|     id: u32, |     id: u32, | ||||||
|  |  | ||||||
|     channel_id: Option<u64>, |     channel_id: u64, | ||||||
|     webhook_id: Option<u64>, |     webhook_id: Option<u64>, | ||||||
|     webhook_token: Option<String>, |     webhook_token: Option<String>, | ||||||
|  |  | ||||||
|     channel_paused: Option<bool>, |     channel_paused: bool, | ||||||
|     channel_paused_until: Option<NaiveDateTime>, |     channel_paused_until: Option<NaiveDateTime>, | ||||||
|     enabled: bool, |     enabled: bool, | ||||||
|  |  | ||||||
| @@ -297,7 +297,7 @@ SELECT | |||||||
|     reminders.`username` AS username |     reminders.`username` AS username | ||||||
| FROM | FROM | ||||||
|     reminders |     reminders | ||||||
| LEFT JOIN | INNER JOIN | ||||||
|     channels |     channels | ||||||
| ON | ON | ||||||
|     reminders.channel_id = channels.id |     reminders.channel_id = channels.id | ||||||
| @@ -343,10 +343,7 @@ WHERE | |||||||
|  |  | ||||||
|     async fn reset_webhook(&self, pool: impl Executor<'_, Database = Database> + Copy) { |     async fn reset_webhook(&self, pool: impl Executor<'_, Database = Database> + Copy) { | ||||||
|         let _ = sqlx::query!( |         let _ = sqlx::query!( | ||||||
|             " |             "UPDATE channels SET webhook_id = NULL, webhook_token = NULL WHERE channel = ?", | ||||||
|             UPDATE channels SET webhook_id = NULL, webhook_token = NULL |  | ||||||
|             WHERE channel = ? |  | ||||||
|             ", |  | ||||||
|             self.channel_id |             self.channel_id | ||||||
|         ) |         ) | ||||||
|         .execute(pool) |         .execute(pool) | ||||||
| @@ -418,9 +415,7 @@ WHERE | |||||||
|                 self.set_sent(pool).await; |                 self.set_sent(pool).await; | ||||||
|             } else { |             } else { | ||||||
|                 sqlx::query!( |                 sqlx::query!( | ||||||
|                     " |                     "UPDATE reminders SET `utc_time` = ? WHERE `id` = ?", | ||||||
|                     UPDATE reminders SET `utc_time` = ? WHERE `id` = ? |  | ||||||
|                     ", |  | ||||||
|                     updated_reminder_time.with_timezone(&Utc), |                     updated_reminder_time.with_timezone(&Utc), | ||||||
|                     self.id |                     self.id | ||||||
|                 ) |                 ) | ||||||
| @@ -453,10 +448,7 @@ WHERE | |||||||
|  |  | ||||||
|         if *LOG_TO_DATABASE { |         if *LOG_TO_DATABASE { | ||||||
|             sqlx::query!( |             sqlx::query!( | ||||||
|                 " |                 "INSERT INTO stat (type, reminder_id, message) VALUES ('reminder_failed', ?, ?)", | ||||||
|                 INSERT INTO stat (type, reminder_id, message) |  | ||||||
|                 VALUES ('reminder_failed', ?, ?) |  | ||||||
|                 ", |  | ||||||
|                 self.id, |                 self.id, | ||||||
|                 message, |                 message, | ||||||
|             ) |             ) | ||||||
| @@ -469,10 +461,7 @@ WHERE | |||||||
|     async fn log_success(&self, pool: impl Executor<'_, Database = Database> + Copy) { |     async fn log_success(&self, pool: impl Executor<'_, Database = Database> + Copy) { | ||||||
|         if *LOG_TO_DATABASE { |         if *LOG_TO_DATABASE { | ||||||
|             sqlx::query!( |             sqlx::query!( | ||||||
|                 " |                 "INSERT INTO stat (type, reminder_id) VALUES ('reminder_sent', ?)", | ||||||
|                 INSERT INTO stat (type, reminder_id) |  | ||||||
|                 VALUES ('reminder_sent', ?) |  | ||||||
|                 ", |  | ||||||
|                 self.id, |                 self.id, | ||||||
|             ) |             ) | ||||||
|             .execute(pool) |             .execute(pool) | ||||||
| @@ -482,14 +471,7 @@ WHERE | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     async fn set_sent(&self, pool: impl Executor<'_, Database = Database> + Copy) { |     async fn set_sent(&self, pool: impl Executor<'_, Database = Database> + Copy) { | ||||||
|         sqlx::query!( |         sqlx::query!("UPDATE reminders SET `status` = 'sent' WHERE `id` = ?", self.id) | ||||||
|             " |  | ||||||
|             UPDATE reminders |  | ||||||
|             SET `status` = 'sent', `status_change_time` = NOW() |  | ||||||
|             WHERE `id` = ? |  | ||||||
|             ", |  | ||||||
|             self.id |  | ||||||
|         ) |  | ||||||
|             .execute(pool) |             .execute(pool) | ||||||
|             .await |             .await | ||||||
|             .expect(&format!("Could not delete Reminder {}", self.id)); |             .expect(&format!("Could not delete Reminder {}", self.id)); | ||||||
| @@ -501,11 +483,7 @@ WHERE | |||||||
|         message: &'static str, |         message: &'static str, | ||||||
|     ) { |     ) { | ||||||
|         sqlx::query!( |         sqlx::query!( | ||||||
|             " |             "UPDATE reminders SET `status` = 'failed', `status_message` = ? WHERE `id` = ?", | ||||||
|             UPDATE reminders |  | ||||||
|             SET `status` = 'failed', `status_message` = ?, `status_change_time` = NOW() |  | ||||||
|             WHERE `id` = ? |  | ||||||
|             ", |  | ||||||
|             message, |             message, | ||||||
|             self.id |             self.id | ||||||
|         ) |         ) | ||||||
| @@ -515,9 +493,7 @@ WHERE | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     async fn pin_message<M: Into<u64>>(&self, message_id: M, http: impl AsRef<Http>) { |     async fn pin_message<M: Into<u64>>(&self, message_id: M, http: impl AsRef<Http>) { | ||||||
|         if let Some(channel_id) = self.channel_id { |         let _ = http.as_ref().pin_message(self.channel_id, message_id.into(), None).await; | ||||||
|             let _ = http.as_ref().pin_message(channel_id, message_id.into(), None).await; |  | ||||||
|         } |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     pub async fn send( |     pub async fn send( | ||||||
| @@ -527,11 +503,10 @@ WHERE | |||||||
|     ) { |     ) { | ||||||
|         async fn send_to_channel( |         async fn send_to_channel( | ||||||
|             cache_http: impl CacheHttp, |             cache_http: impl CacheHttp, | ||||||
|             channel_id: u64, |  | ||||||
|             reminder: &Reminder, |             reminder: &Reminder, | ||||||
|             embed: Option<CreateEmbed>, |             embed: Option<CreateEmbed>, | ||||||
|         ) -> Result<()> { |         ) -> Result<()> { | ||||||
|             let channel = ChannelId(channel_id).to_channel(&cache_http).await; |             let channel = ChannelId(reminder.channel_id).to_channel(&cache_http).await; | ||||||
|  |  | ||||||
|             match channel { |             match channel { | ||||||
|                 Ok(Channel::Guild(channel)) => { |                 Ok(Channel::Guild(channel)) => { | ||||||
| @@ -563,7 +538,6 @@ WHERE | |||||||
|                         Err(e) => Err(e), |                         Err(e) => Err(e), | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|  |  | ||||||
|                 Ok(Channel::Private(channel)) => { |                 Ok(Channel::Private(channel)) => { | ||||||
|                     match channel |                     match channel | ||||||
|                         .send_message(&cache_http.http(), |m| { |                         .send_message(&cache_http.http(), |m| { | ||||||
| @@ -593,9 +567,7 @@ WHERE | |||||||
|                         Err(e) => Err(e), |                         Err(e) => Err(e), | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|  |  | ||||||
|                 Err(e) => Err(e), |                 Err(e) => Err(e), | ||||||
|  |  | ||||||
|                 _ => Err(Error::Other("Channel not of valid type")), |                 _ => Err(Error::Other("Channel not of valid type")), | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| @@ -650,20 +622,14 @@ WHERE | |||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         match self.channel_id { |  | ||||||
|             Some(channel_id) => { |  | ||||||
|         if self.enabled |         if self.enabled | ||||||
|                     && !(self.channel_paused.unwrap_or(false) |             && !(self.channel_paused | ||||||
|                 && self |                 && self | ||||||
|                     .channel_paused_until |                     .channel_paused_until | ||||||
|                     .map_or(true, |inner| inner >= Utc::now().naive_local())) |                     .map_or(true, |inner| inner >= Utc::now().naive_local())) | ||||||
|         { |         { | ||||||
|             let _ = sqlx::query!( |             let _ = sqlx::query!( | ||||||
|                         " |                 "UPDATE `channels` SET paused = 0, paused_until = NULL WHERE `channel` = ?", | ||||||
|                         UPDATE `channels` |  | ||||||
|                         SET paused = 0, paused_until = NULL |  | ||||||
|                         WHERE `channel` = ? |  | ||||||
|                         ", |  | ||||||
|                 self.channel_id |                 self.channel_id | ||||||
|             ) |             ) | ||||||
|             .execute(pool) |             .execute(pool) | ||||||
| @@ -674,10 +640,8 @@ WHERE | |||||||
|             let result = if let (Some(webhook_id), Some(webhook_token)) = |             let result = if let (Some(webhook_id), Some(webhook_token)) = | ||||||
|                 (self.webhook_id, &self.webhook_token) |                 (self.webhook_id, &self.webhook_token) | ||||||
|             { |             { | ||||||
|                         let webhook_res = cache_http |                 let webhook_res = | ||||||
|                             .http() |                     cache_http.http().get_webhook_with_token(webhook_id, webhook_token).await; | ||||||
|                             .get_webhook_with_token(webhook_id, webhook_token) |  | ||||||
|                             .await; |  | ||||||
|  |  | ||||||
|                 if let Ok(webhook) = webhook_res { |                 if let Ok(webhook) = webhook_res { | ||||||
|                     send_to_webhook(cache_http, &self, webhook, embed).await |                     send_to_webhook(cache_http, &self, webhook, embed).await | ||||||
| @@ -685,10 +649,10 @@ WHERE | |||||||
|                     warn!("Webhook vanished for reminder {}: {:?}", self.id, webhook_res); |                     warn!("Webhook vanished for reminder {}: {:?}", self.id, webhook_res); | ||||||
|  |  | ||||||
|                     self.reset_webhook(pool).await; |                     self.reset_webhook(pool).await; | ||||||
|                             send_to_channel(cache_http, channel_id, &self, embed).await |                     send_to_channel(cache_http, &self, embed).await | ||||||
|                 } |                 } | ||||||
|             } else { |             } else { | ||||||
|                         send_to_channel(cache_http, channel_id, &self, embed).await |                 send_to_channel(cache_http, &self, embed).await | ||||||
|             }; |             }; | ||||||
|  |  | ||||||
|             if let Err(e) = result { |             if let Err(e) = result { | ||||||
| @@ -715,10 +679,7 @@ WHERE | |||||||
|                                     None::<&'static str>, |                                     None::<&'static str>, | ||||||
|                                 ) |                                 ) | ||||||
|                                 .await; |                                 .await; | ||||||
|                                         self.set_failed( |                                 self.set_failed(pool, "Could not be sent as guild does not exist") | ||||||
|                                             pool, |  | ||||||
|                                             "Could not be sent as guild does not exist", |  | ||||||
|                                         ) |  | ||||||
|                                     .await; |                                     .await; | ||||||
|                             } |                             } | ||||||
|                             50001 => { |                             50001 => { | ||||||
| @@ -728,11 +689,7 @@ WHERE | |||||||
|                                     None::<&'static str>, |                                     None::<&'static str>, | ||||||
|                                 ) |                                 ) | ||||||
|                                 .await; |                                 .await; | ||||||
|                                         self.set_failed( |                                 self.set_failed(pool, "Could not be sent as missing access").await; | ||||||
|                                             pool, |  | ||||||
|                                             "Could not be sent as missing access", |  | ||||||
|                                         ) |  | ||||||
|                                         .await; |  | ||||||
|                             } |                             } | ||||||
|                             50007 => { |                             50007 => { | ||||||
|                                 self.log_error( |                                 self.log_error( | ||||||
| @@ -741,10 +698,7 @@ WHERE | |||||||
|                                     None::<&'static str>, |                                     None::<&'static str>, | ||||||
|                                 ) |                                 ) | ||||||
|                                 .await; |                                 .await; | ||||||
|                                         self.set_failed( |                                 self.set_failed(pool, "Could not be sent as user has DMs disabled") | ||||||
|                                             pool, |  | ||||||
|                                             "Could not be sent as user has DMs disabled", |  | ||||||
|                                         ) |  | ||||||
|                                     .await; |                                     .await; | ||||||
|                             } |                             } | ||||||
|                             50013 => { |                             50013 => { | ||||||
| @@ -788,13 +742,4 @@ WHERE | |||||||
|             self.refresh(pool).await; |             self.refresh(pool).await; | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|             None => { |  | ||||||
|                 info!("Reminder {} is orphaned", self.id); |  | ||||||
|  |  | ||||||
|                 self.log_error(pool, "Orphaned", Option::<u8>::None).await; |  | ||||||
|                 self.set_failed(pool, "Could not be sent as channel was deleted").await; |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										1
									
								
								reminder-dashboard
									
									
									
									
									
										Submodule
									
								
							
							
								
								
								
								
								
							
						
						
									
										1
									
								
								reminder-dashboard
									
									
									
									
									
										Submodule
									
								
							 Submodule reminder-dashboard added at 8ba7a39ce5
									
								
							| @@ -114,6 +114,8 @@ pub async fn offset( | |||||||
|     #[description = "Number of minutes to offset by"] minutes: Option<isize>, |     #[description = "Number of minutes to offset by"] minutes: Option<isize>, | ||||||
|     #[description = "Number of seconds to offset by"] seconds: Option<isize>, |     #[description = "Number of seconds to offset by"] seconds: Option<isize>, | ||||||
| ) -> Result<(), Error> { | ) -> Result<(), Error> { | ||||||
|  |     ctx.defer().await?; | ||||||
|  |  | ||||||
|     let combined_time = hours.map_or(0, |h| h * HOUR as isize) |     let combined_time = hours.map_or(0, |h| h * HOUR as isize) | ||||||
|         + minutes.map_or(0, |m| m * MINUTE as isize) |         + minutes.map_or(0, |m| m * MINUTE as isize) | ||||||
|         + seconds.map_or(0, |s| s); |         + seconds.map_or(0, |s| s); | ||||||
| @@ -619,7 +621,7 @@ pub async fn multiline( | |||||||
| )] | )] | ||||||
| pub async fn remind( | pub async fn remind( | ||||||
|     ctx: ApplicationContext<'_>, |     ctx: ApplicationContext<'_>, | ||||||
|     #[description = "A description of the time to set the reminder for"] |     #[description = "The time (and optionally date) to set the reminder for"] | ||||||
|     #[autocomplete = "time_hint_autocomplete"] |     #[autocomplete = "time_hint_autocomplete"] | ||||||
|     time: String, |     time: String, | ||||||
|     #[description = "The message content to send"] content: String, |     #[description = "The message content to send"] content: String, | ||||||
| @@ -773,7 +775,7 @@ async fn create_reminder( | |||||||
|                                     b.emoji(ReactionType::Unicode("📝".to_string())) |                                     b.emoji(ReactionType::Unicode("📝".to_string())) | ||||||
|                                         .label("Edit") |                                         .label("Edit") | ||||||
|                                         .style(ButtonStyle::Link) |                                         .style(ButtonStyle::Link) | ||||||
|                                         .url("https://reminder-bot.com/dashboard") |                                         .url("https://beta.reminder-bot.com/dashboard") | ||||||
|                                 }) |                                 }) | ||||||
|                             }) |                             }) | ||||||
|                         }) |                         }) | ||||||
|   | |||||||
| @@ -166,21 +166,15 @@ impl ComponentDataModel { | |||||||
|                     .await; |                     .await; | ||||||
|             } |             } | ||||||
|             ComponentDataModel::DelSelector(selector) => { |             ComponentDataModel::DelSelector(selector) => { | ||||||
|                 for id in &component.data.values { |                 let selected_id = component.data.values.join(","); | ||||||
|                     match id.parse::<u32>() { |  | ||||||
|                         Ok(id) => { |  | ||||||
|                             if let Some(reminder) = Reminder::from_id(&data.database, id).await { |  | ||||||
|                                 reminder.delete(&data.database).await.unwrap(); |  | ||||||
|                             } else { |  | ||||||
|                                 warn!("Attempt to delete non-existent reminder"); |  | ||||||
|                             } |  | ||||||
|                         } |  | ||||||
|  |  | ||||||
|                         Err(e) => { |                 sqlx::query!( | ||||||
|                             warn!("Error casting ID to integer: {:?}.", e); |                     "UPDATE reminders SET `status` = 'deleted' WHERE FIND_IN_SET(id, ?)", | ||||||
|                         } |                     selected_id | ||||||
|                     } |                 ) | ||||||
|                 } |                 .execute(&data.database) | ||||||
|  |                 .await | ||||||
|  |                 .unwrap(); | ||||||
|  |  | ||||||
|                 let reminders = Reminder::from_guild( |                 let reminders = Reminder::from_guild( | ||||||
|                     &ctx, |                     &ctx, | ||||||
|   | |||||||
| @@ -150,7 +150,7 @@ impl<'a> Parser<'a> { | |||||||
|             "hours" | "hour" | "hr" | "hrs" | "h" => (0, 0, n.mul(3600)?, 0), |             "hours" | "hour" | "hr" | "hrs" | "h" => (0, 0, n.mul(3600)?, 0), | ||||||
|             "days" | "day" | "d" => (0, n, 0, 0), |             "days" | "day" | "d" => (0, n, 0, 0), | ||||||
|             "weeks" | "week" | "w" => (0, n.mul(7)?, 0, 0), |             "weeks" | "week" | "w" => (0, n.mul(7)?, 0, 0), | ||||||
|             "months" | "month" | "M" => (n, 0, 0, 0), |             "months" | "month" => (n, 0, 0, 0), | ||||||
|             "years" | "year" | "y" => (n.mul(12)?, 0, 0, 0), |             "years" | "year" | "y" => (n.mul(12)?, 0, 0, 0), | ||||||
|             _ => { |             _ => { | ||||||
|                 return Err(Error::UnknownUnit { |                 return Err(Error::UnknownUnit { | ||||||
| @@ -255,7 +255,7 @@ impl<'a> Parser<'a> { | |||||||
| /// assert_eq!(parse_duration("32ms"), Ok(Duration::new(0, 32_000_000))); | /// assert_eq!(parse_duration("32ms"), Ok(Duration::new(0, 32_000_000))); | ||||||
| /// ``` | /// ``` | ||||||
| pub fn parse_duration(s: &str) -> Result<Interval, Error> { | pub fn parse_duration(s: &str) -> Result<Interval, Error> { | ||||||
|     Parser { iter: s.chars(), src: s, current: (0, 0, 0, 0) }.parse() |     Parser { iter: s.to_lowercase().chars(), src: &s.to_lowercase(), current: (0, 0, 0, 0) }.parse() | ||||||
| } | } | ||||||
|  |  | ||||||
| #[cfg(test)] | #[cfg(test)] | ||||||
| @@ -324,4 +324,13 @@ mod tests { | |||||||
|         assert_eq!(interval.day, 0); |         assert_eq!(interval.day, 0); | ||||||
|         assert_eq!(interval.month, 120); |         assert_eq!(interval.month, 120); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     #[test] | ||||||
|  |     fn parse_case() { | ||||||
|  |         let interval = parse_duration("200 Seconds").unwrap(); | ||||||
|  |  | ||||||
|  |         assert_eq!(interval.sec, 200); | ||||||
|  |         assert_eq!(interval.day, 0); | ||||||
|  |         assert_eq!(interval.month, 0); | ||||||
|  |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -10,7 +10,6 @@ pub struct ChannelData { | |||||||
|     pub webhook_id: Option<u64>, |     pub webhook_id: Option<u64>, | ||||||
|     pub webhook_token: Option<String>, |     pub webhook_token: Option<String>, | ||||||
|     pub paused: bool, |     pub paused: bool, | ||||||
|     pub db_guild_id: Option<u32>, |  | ||||||
|     pub paused_until: Option<NaiveDateTime>, |     pub paused_until: Option<NaiveDateTime>, | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -23,11 +22,7 @@ impl ChannelData { | |||||||
|  |  | ||||||
|         if let Ok(c) = sqlx::query_as_unchecked!( |         if let Ok(c) = sqlx::query_as_unchecked!( | ||||||
|             Self, |             Self, | ||||||
|             " |             "SELECT id, name, nudge, blacklisted, webhook_id, webhook_token, paused, paused_until FROM channels WHERE channel = ?", | ||||||
|             SELECT id, name, nudge, blacklisted, webhook_id, webhook_token, paused, paused_until, |  | ||||||
|                 guild_id AS db_guild_id |  | ||||||
|             FROM channels WHERE channel = ? |  | ||||||
|             ", |  | ||||||
|             channel_id |             channel_id | ||||||
|         ) |         ) | ||||||
|         .fetch_one(pool) |         .fetch_one(pool) | ||||||
| @@ -35,18 +30,12 @@ impl ChannelData { | |||||||
|         { |         { | ||||||
|             Ok(c) |             Ok(c) | ||||||
|         } else { |         } else { | ||||||
|             let props = |             let props = channel.to_owned().guild().map(|g| (g.guild_id.as_u64().to_owned(), g.name)); | ||||||
|                 channel.to_owned().guild().map(|g| (g.guild_id.as_u64().to_owned(), g.name)); |  | ||||||
|  |  | ||||||
|             let (guild_id, channel_name) = |             let (guild_id, channel_name) = if let Some((a, b)) = props { (Some(a), Some(b)) } else { (None, None) }; | ||||||
|                 if let Some((a, b)) = props { (Some(a), Some(b)) } else { (None, None) }; |  | ||||||
|  |  | ||||||
|             sqlx::query!( |             sqlx::query!( | ||||||
|                 " |                 "INSERT IGNORE INTO channels (channel, name, guild_id) VALUES (?, ?, (SELECT id FROM guilds WHERE guild = ?))", | ||||||
|                 INSERT IGNORE INTO channels |  | ||||||
|                 (channel, name, guild_id) |  | ||||||
|                 VALUES (?, ?, (SELECT id FROM guilds WHERE guild = ?)) |  | ||||||
|                 ", |  | ||||||
|                 channel_id, |                 channel_id, | ||||||
|                 channel_name, |                 channel_name, | ||||||
|                 guild_id |                 guild_id | ||||||
| @@ -57,10 +46,7 @@ impl ChannelData { | |||||||
|             Ok(sqlx::query_as_unchecked!( |             Ok(sqlx::query_as_unchecked!( | ||||||
|                 Self, |                 Self, | ||||||
|                 " |                 " | ||||||
|                 SELECT id, name, nudge, blacklisted, webhook_id, webhook_token, paused, | SELECT id, name, nudge, blacklisted, webhook_id, webhook_token, paused, paused_until FROM channels WHERE channel = ? | ||||||
|                     paused_until, guild_id AS db_guild_id |  | ||||||
|                 FROM channels |  | ||||||
|                 WHERE channel = ? |  | ||||||
|                 ", |                 ", | ||||||
|                 channel_id |                 channel_id | ||||||
|             ) |             ) | ||||||
| @@ -72,10 +58,8 @@ impl ChannelData { | |||||||
|     pub async fn commit_changes(&self, pool: &MySqlPool) { |     pub async fn commit_changes(&self, pool: &MySqlPool) { | ||||||
|         sqlx::query!( |         sqlx::query!( | ||||||
|             " |             " | ||||||
|             UPDATE channels | UPDATE channels SET name = ?, nudge = ?, blacklisted = ?, webhook_id = ?, webhook_token = ?, paused = ?, paused_until \ | ||||||
|             SET name = ?, nudge = ?, blacklisted = ?, webhook_id = ?, webhook_token = ?, |              = ? WHERE id = ? | ||||||
|                 paused = ?, paused_until = ? |  | ||||||
|             WHERE id = ? |  | ||||||
|             ", |             ", | ||||||
|             self.name, |             self.name, | ||||||
|             self.nudge, |             self.nudge, | ||||||
|   | |||||||
| @@ -51,7 +51,6 @@ pub struct ReminderBuilder { | |||||||
|     pool: MySqlPool, |     pool: MySqlPool, | ||||||
|     uid: String, |     uid: String, | ||||||
|     channel: u32, |     channel: u32, | ||||||
|     guild: Option<u32>, |  | ||||||
|     thread_id: Option<u64>, |     thread_id: Option<u64>, | ||||||
|     utc_time: NaiveDateTime, |     utc_time: NaiveDateTime, | ||||||
|     timezone: String, |     timezone: String, | ||||||
| @@ -87,7 +86,6 @@ impl ReminderBuilder { | |||||||
| INSERT INTO reminders ( | INSERT INTO reminders ( | ||||||
|     `uid`, |     `uid`, | ||||||
|     `channel_id`, |     `channel_id`, | ||||||
|     `guild_id`, |  | ||||||
|     `utc_time`, |     `utc_time`, | ||||||
|     `timezone`, |     `timezone`, | ||||||
|     `interval_seconds`, |     `interval_seconds`, | ||||||
| @@ -112,13 +110,11 @@ INSERT INTO reminders ( | |||||||
|     ?, |     ?, | ||||||
|     ?, |     ?, | ||||||
|     ?, |     ?, | ||||||
|     ?, |  | ||||||
|     ? |     ? | ||||||
| ) | ) | ||||||
|             ", |             ", | ||||||
|                         self.uid, |                         self.uid, | ||||||
|                         self.channel, |                         self.channel, | ||||||
|                         self.guild, |  | ||||||
|                         utc_time, |                         utc_time, | ||||||
|                         self.timezone, |                         self.timezone, | ||||||
|                         self.interval_seconds, |                         self.interval_seconds, | ||||||
| @@ -251,10 +247,10 @@ impl<'a> MultiReminderBuilder<'a> { | |||||||
|                                 { |                                 { | ||||||
|                                     Err(ReminderError::UserBlockedDm) |                                     Err(ReminderError::UserBlockedDm) | ||||||
|                                 } else { |                                 } else { | ||||||
|                                     Ok((user_data.dm_channel, None)) |                                     Ok(user_data.dm_channel) | ||||||
|                                 } |                                 } | ||||||
|                             } else { |                             } else { | ||||||
|                                 Ok((user_data.dm_channel, None)) |                                 Ok(user_data.dm_channel) | ||||||
|                             } |                             } | ||||||
|                         } else { |                         } else { | ||||||
|                             Err(ReminderError::InvalidTag) |                             Err(ReminderError::InvalidTag) | ||||||
| @@ -301,13 +297,13 @@ impl<'a> MultiReminderBuilder<'a> { | |||||||
|                                                 .commit_changes(&self.ctx.data().database) |                                                 .commit_changes(&self.ctx.data().database) | ||||||
|                                                 .await; |                                                 .await; | ||||||
|  |  | ||||||
|                                             Ok((channel_data.id, channel_data.db_guild_id)) |                                             Ok(channel_data.id) | ||||||
|                                         } |                                         } | ||||||
|  |  | ||||||
|                                         Err(e) => Err(ReminderError::DiscordError(e.to_string())), |                                         Err(e) => Err(ReminderError::DiscordError(e.to_string())), | ||||||
|                                     } |                                     } | ||||||
|                                 } else { |                                 } else { | ||||||
|                                     Ok((channel_data.id, channel_data.db_guild_id)) |                                     Ok(channel_data.id) | ||||||
|                                 } |                                 } | ||||||
|                             } |                             } | ||||||
|                         } else { |                         } else { | ||||||
| @@ -321,8 +317,7 @@ impl<'a> MultiReminderBuilder<'a> { | |||||||
|                         let builder = ReminderBuilder { |                         let builder = ReminderBuilder { | ||||||
|                             pool: self.ctx.data().database.clone(), |                             pool: self.ctx.data().database.clone(), | ||||||
|                             uid: generate_uid(), |                             uid: generate_uid(), | ||||||
|                             channel: c.0, |                             channel: c, | ||||||
|                             guild: c.1, |  | ||||||
|                             thread_id, |                             thread_id, | ||||||
|                             utc_time: self.utc_time, |                             utc_time: self.utc_time, | ||||||
|                             timezone: self.timezone.to_string(), |                             timezone: self.timezone.to_string(), | ||||||
|   | |||||||
| @@ -304,10 +304,7 @@ WHERE | |||||||
|         &self, |         &self, | ||||||
|         db: impl Executor<'_, Database = Database>, |         db: impl Executor<'_, Database = Database>, | ||||||
|     ) -> Result<(), sqlx::Error> { |     ) -> Result<(), sqlx::Error> { | ||||||
|         sqlx::query!( |         sqlx::query!("UPDATE reminders SET `status` = 'deleted' WHERE uid = ?", self.uid) | ||||||
|             "UPDATE reminders SET `status` = 'deleted', `status_change_time` = NOW() WHERE uid = ?", |  | ||||||
|             self.uid |  | ||||||
|         ) |  | ||||||
|             .execute(db) |             .execute(db) | ||||||
|             .await |             .await | ||||||
|             .map(|_| ()) |             .map(|_| ()) | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| [package] | [package] | ||||||
| name = "reminder_web" | name = "reminder_web" | ||||||
| version = "0.1.0" | version = "0.1.3" | ||||||
| authors = ["jellywx <judesouthworth@pm.me>"] | authors = ["jellywx <judesouthworth@pm.me>"] | ||||||
| edition = "2018" | edition = "2018" | ||||||
|  |  | ||||||
| @@ -16,6 +16,7 @@ sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "macros", "mysql", | |||||||
| chrono = "0.4" | chrono = "0.4" | ||||||
| chrono-tz = "0.8" | chrono-tz = "0.8" | ||||||
| lazy_static = "1.4.0" | lazy_static = "1.4.0" | ||||||
| rand = "0.7" | rand = "0.8" | ||||||
| base64 = "0.13" | base64 = "0.13" | ||||||
| csv = "1.1" | csv = "1.2" | ||||||
|  | prometheus = "0.13.3" | ||||||
|   | |||||||
							
								
								
									
										40
									
								
								web/src/catchers.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								web/src/catchers.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,40 @@ | |||||||
|  | use std::collections::HashMap; | ||||||
|  |  | ||||||
|  | use rocket::serde::json::json; | ||||||
|  | use rocket_dyn_templates::Template; | ||||||
|  |  | ||||||
|  | use crate::JsonValue; | ||||||
|  |  | ||||||
|  | #[catch(403)] | ||||||
|  | pub(crate) async fn forbidden() -> Template { | ||||||
|  |     let map: HashMap<String, String> = HashMap::new(); | ||||||
|  |     Template::render("errors/403", &map) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[catch(500)] | ||||||
|  | pub(crate) async fn internal_server_error() -> Template { | ||||||
|  |     let map: HashMap<String, String> = HashMap::new(); | ||||||
|  |     Template::render("errors/500", &map) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[catch(401)] | ||||||
|  | pub(crate) async fn not_authorized() -> Template { | ||||||
|  |     let map: HashMap<String, String> = HashMap::new(); | ||||||
|  |     Template::render("errors/401", &map) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[catch(404)] | ||||||
|  | pub(crate) async fn not_found() -> Template { | ||||||
|  |     let map: HashMap<String, String> = HashMap::new(); | ||||||
|  |     Template::render("errors/404", &map) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[catch(413)] | ||||||
|  | pub(crate) async fn payload_too_large() -> JsonValue { | ||||||
|  |     json!({"error": "Data too large.", "errors": ["Data too large."]}) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[catch(422)] | ||||||
|  | pub(crate) async fn unprocessable_entity() -> JsonValue { | ||||||
|  |     json!({"error": "Invalid request.", "errors": ["Invalid request."]}) | ||||||
|  | } | ||||||
							
								
								
									
										1
									
								
								web/src/guards/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								web/src/guards/mod.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | pub(crate) mod transaction; | ||||||
							
								
								
									
										44
									
								
								web/src/guards/transaction.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								web/src/guards/transaction.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,44 @@ | |||||||
|  | use rocket::{ | ||||||
|  |     http::Status, | ||||||
|  |     request::{FromRequest, Outcome}, | ||||||
|  |     Request, State, | ||||||
|  | }; | ||||||
|  | use sqlx::Pool; | ||||||
|  |  | ||||||
|  | use crate::Database; | ||||||
|  |  | ||||||
|  | pub struct Transaction<'a>(sqlx::Transaction<'a, Database>); | ||||||
|  |  | ||||||
|  | impl Transaction<'_> { | ||||||
|  |     pub fn executor(&mut self) -> impl sqlx::Executor<'_, Database = Database> { | ||||||
|  |         &mut *(self.0) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     pub async fn commit(self) -> Result<(), sqlx::Error> { | ||||||
|  |         self.0.commit().await | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[derive(Debug)] | ||||||
|  | pub enum TransactionError { | ||||||
|  |     Error(sqlx::Error), | ||||||
|  |     Missing, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[rocket::async_trait] | ||||||
|  | impl<'r> FromRequest<'r> for Transaction<'r> { | ||||||
|  |     type Error = TransactionError; | ||||||
|  |  | ||||||
|  |     async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> { | ||||||
|  |         match request.guard::<&State<Pool<Database>>>().await { | ||||||
|  |             Outcome::Success(pool) => match pool.begin().await { | ||||||
|  |                 Ok(transaction) => Outcome::Success(Transaction(transaction)), | ||||||
|  |                 Err(e) => { | ||||||
|  |                     Outcome::Failure((Status::InternalServerError, TransactionError::Error(e))) | ||||||
|  |                 } | ||||||
|  |             }, | ||||||
|  |             Outcome::Failure(e) => Outcome::Failure((e.0, TransactionError::Missing)), | ||||||
|  |             Outcome::Forward(f) => Outcome::Forward(f), | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										158
									
								
								web/src/lib.rs
									
									
									
									
									
								
							
							
						
						
									
										158
									
								
								web/src/lib.rs
									
									
									
									
									
								
							| @@ -4,13 +4,17 @@ extern crate rocket; | |||||||
| mod consts; | mod consts; | ||||||
| #[macro_use] | #[macro_use] | ||||||
| mod macros; | mod macros; | ||||||
|  | mod catchers; | ||||||
|  | mod guards; | ||||||
|  | mod metrics; | ||||||
| mod routes; | mod routes; | ||||||
|  |  | ||||||
| use std::{collections::HashMap, env, path::Path}; | use std::{env, path::Path}; | ||||||
|  |  | ||||||
| use oauth2::{basic::BasicClient, AuthUrl, ClientId, ClientSecret, RedirectUrl, TokenUrl}; | use oauth2::{basic::BasicClient, AuthUrl, ClientId, ClientSecret, RedirectUrl, TokenUrl}; | ||||||
| use rocket::{ | use rocket::{ | ||||||
|     fs::FileServer, |     fs::FileServer, | ||||||
|  |     http::CookieJar, | ||||||
|     serde::json::{json, Value as JsonValue}, |     serde::json::{json, Value as JsonValue}, | ||||||
|     tokio::sync::broadcast::Sender, |     tokio::sync::broadcast::Sender, | ||||||
| }; | }; | ||||||
| @@ -22,7 +26,10 @@ use serenity::{ | |||||||
| }; | }; | ||||||
| use sqlx::{MySql, Pool}; | use sqlx::{MySql, Pool}; | ||||||
|  |  | ||||||
| use crate::consts::{CNC_GUILD, DISCORD_OAUTH_AUTHORIZE, DISCORD_OAUTH_TOKEN, SUBSCRIPTION_ROLES}; | use crate::{ | ||||||
|  |     consts::{CNC_GUILD, DISCORD_OAUTH_AUTHORIZE, DISCORD_OAUTH_TOKEN, SUBSCRIPTION_ROLES}, | ||||||
|  |     metrics::{init_metrics, MetricProducer}, | ||||||
|  | }; | ||||||
|  |  | ||||||
| type Database = MySql; | type Database = MySql; | ||||||
|  |  | ||||||
| @@ -32,40 +39,6 @@ enum Error { | |||||||
|     Serenity(serenity::Error), |     Serenity(serenity::Error), | ||||||
| } | } | ||||||
|  |  | ||||||
| #[catch(401)] |  | ||||||
| async fn not_authorized() -> Template { |  | ||||||
|     let map: HashMap<String, String> = HashMap::new(); |  | ||||||
|     Template::render("errors/401", &map) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| #[catch(403)] |  | ||||||
| async fn forbidden() -> Template { |  | ||||||
|     let map: HashMap<String, String> = HashMap::new(); |  | ||||||
|     Template::render("errors/403", &map) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| #[catch(404)] |  | ||||||
| async fn not_found() -> Template { |  | ||||||
|     let map: HashMap<String, String> = HashMap::new(); |  | ||||||
|     Template::render("errors/404", &map) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| #[catch(413)] |  | ||||||
| async fn payload_too_large() -> JsonValue { |  | ||||||
|     json!({"error": "Data too large.", "errors": ["Data too large."]}) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| #[catch(422)] |  | ||||||
| async fn unprocessable_entity() -> JsonValue { |  | ||||||
|     json!({"error": "Invalid request.", "errors": ["Invalid request."]}) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| #[catch(500)] |  | ||||||
| async fn internal_server_error() -> Template { |  | ||||||
|     let map: HashMap<String, String> = HashMap::new(); |  | ||||||
|     Template::render("errors/500", &map) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| pub async fn initialize( | pub async fn initialize( | ||||||
|     kill_channel: Sender<()>, |     kill_channel: Sender<()>, | ||||||
|     serenity_context: Context, |     serenity_context: Context, | ||||||
| @@ -95,17 +68,20 @@ pub async fn initialize( | |||||||
|     let static_path = |     let static_path = | ||||||
|         if Path::new("web/static").exists() { "web/static" } else { "/lib/reminder-rs/static" }; |         if Path::new("web/static").exists() { "web/static" } else { "/lib/reminder-rs/static" }; | ||||||
|  |  | ||||||
|  |     init_metrics(); | ||||||
|  |  | ||||||
|     rocket::build() |     rocket::build() | ||||||
|  |         .attach(MetricProducer) | ||||||
|         .attach(Template::fairing()) |         .attach(Template::fairing()) | ||||||
|         .register( |         .register( | ||||||
|             "/", |             "/", | ||||||
|             catchers![ |             catchers![ | ||||||
|                 not_authorized, |                 catchers::not_authorized, | ||||||
|                 forbidden, |                 catchers::forbidden, | ||||||
|                 not_found, |                 catchers::not_found, | ||||||
|                 internal_server_error, |                 catchers::internal_server_error, | ||||||
|                 unprocessable_entity, |                 catchers::unprocessable_entity, | ||||||
|                 payload_too_large, |                 catchers::payload_too_large, | ||||||
|             ], |             ], | ||||||
|         ) |         ) | ||||||
|         .manage(oauth2_client) |         .manage(oauth2_client) | ||||||
| @@ -116,12 +92,13 @@ pub async fn initialize( | |||||||
|         .mount( |         .mount( | ||||||
|             "/", |             "/", | ||||||
|             routes![ |             routes![ | ||||||
|                 routes::index, |  | ||||||
|                 routes::cookies, |                 routes::cookies, | ||||||
|  |                 routes::index, | ||||||
|  |                 routes::metrics::metrics, | ||||||
|                 routes::privacy, |                 routes::privacy, | ||||||
|                 routes::terms, |  | ||||||
|                 routes::return_to_same_site, |  | ||||||
|                 routes::report::report_error, |                 routes::report::report_error, | ||||||
|  |                 routes::return_to_same_site, | ||||||
|  |                 routes::terms, | ||||||
|             ], |             ], | ||||||
|         ) |         ) | ||||||
|         .mount( |         .mount( | ||||||
| @@ -150,22 +127,21 @@ pub async fn initialize( | |||||||
|         .mount( |         .mount( | ||||||
|             "/dashboard", |             "/dashboard", | ||||||
|             routes![ |             routes![ | ||||||
|                 routes::dashboard::dashboard_1, |                 routes::dashboard::dashboard, | ||||||
|                 routes::dashboard::dashboard_2, |  | ||||||
|                 routes::dashboard::dashboard_home, |                 routes::dashboard::dashboard_home, | ||||||
|                 routes::dashboard::user::get_user_info, |                 routes::dashboard::api::user::get_user_info, | ||||||
|                 routes::dashboard::user::update_user_info, |                 routes::dashboard::api::user::update_user_info, | ||||||
|                 routes::dashboard::user::get_user_guilds, |                 routes::dashboard::api::user::get_user_guilds, | ||||||
|                 routes::dashboard::guild::get_guild_patreon, |                 routes::dashboard::api::guild::get_guild_info, | ||||||
|                 routes::dashboard::guild::get_guild_channels, |                 routes::dashboard::api::guild::get_guild_channels, | ||||||
|                 routes::dashboard::guild::get_guild_roles, |                 routes::dashboard::api::guild::get_guild_roles, | ||||||
|                 routes::dashboard::guild::get_reminder_templates, |                 routes::dashboard::api::guild::get_reminder_templates, | ||||||
|                 routes::dashboard::guild::create_reminder_template, |                 routes::dashboard::api::guild::create_reminder_template, | ||||||
|                 routes::dashboard::guild::delete_reminder_template, |                 routes::dashboard::api::guild::delete_reminder_template, | ||||||
|                 routes::dashboard::guild::create_guild_reminder, |                 routes::dashboard::api::guild::create_guild_reminder, | ||||||
|                 routes::dashboard::guild::get_reminders, |                 routes::dashboard::api::guild::get_reminders, | ||||||
|                 routes::dashboard::guild::edit_reminder, |                 routes::dashboard::api::guild::edit_reminder, | ||||||
|                 routes::dashboard::guild::delete_reminder, |                 routes::dashboard::api::guild::delete_reminder, | ||||||
|                 routes::dashboard::export::export_reminders, |                 routes::dashboard::export::export_reminders, | ||||||
|                 routes::dashboard::export::export_reminder_templates, |                 routes::dashboard::export::export_reminder_templates, | ||||||
|                 routes::dashboard::export::export_todos, |                 routes::dashboard::export::export_todos, | ||||||
| @@ -223,3 +199,65 @@ pub async fn check_guild_subscription( | |||||||
|         false |         false | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | pub async fn check_authorization( | ||||||
|  |     cookies: &CookieJar<'_>, | ||||||
|  |     ctx: &Context, | ||||||
|  |     guild: u64, | ||||||
|  | ) -> Result<(), JsonValue> { | ||||||
|  |     let user_id = cookies.get_private("userid").map(|c| c.value().parse::<u64>().ok()).flatten(); | ||||||
|  |  | ||||||
|  |     if std::env::var("OFFLINE").map_or(true, |v| v != "1") { | ||||||
|  |         match user_id { | ||||||
|  |             Some(user_id) => { | ||||||
|  |                 let admin_id = std::env::var("ADMIN_ID") | ||||||
|  |                     .map_or(false, |u| u.parse::<u64>().map_or(false, |u| u == user_id)); | ||||||
|  |  | ||||||
|  |                 if admin_id { | ||||||
|  |                     return Ok(()); | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 match GuildId(guild).to_guild_cached(ctx) { | ||||||
|  |                     Some(guild) => { | ||||||
|  |                         let member_res = guild.member(ctx, UserId(user_id)).await; | ||||||
|  |  | ||||||
|  |                         match member_res { | ||||||
|  |                             Err(_) => { | ||||||
|  |                                 return Err(json!({"error": "User not in guild"})); | ||||||
|  |                             } | ||||||
|  |  | ||||||
|  |                             Ok(member) => { | ||||||
|  |                                 let permissions_res = member.permissions(ctx); | ||||||
|  |  | ||||||
|  |                                 match permissions_res { | ||||||
|  |                                     Err(_) => { | ||||||
|  |                                         return Err(json!({"error": "Couldn't fetch permissions"})); | ||||||
|  |                                     } | ||||||
|  |  | ||||||
|  |                                     Ok(permissions) => { | ||||||
|  |                                         if !(permissions.manage_messages() | ||||||
|  |                                             || permissions.manage_guild() | ||||||
|  |                                             || permissions.administrator()) | ||||||
|  |                                         { | ||||||
|  |                                             return Err(json!({"error": "Incorrect permissions"})); | ||||||
|  |                                         } | ||||||
|  |                                     } | ||||||
|  |                                 } | ||||||
|  |                             } | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |  | ||||||
|  |                     None => { | ||||||
|  |                         return Err(json!({"error": "Bot not in guild"})); | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             None => { | ||||||
|  |                 return Err(json!({"error": "User not authorized"})); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     Ok(()) | ||||||
|  | } | ||||||
|   | |||||||
| @@ -54,56 +54,6 @@ macro_rules! check_url_opt { | |||||||
|     }; |     }; | ||||||
| } | } | ||||||
|  |  | ||||||
| macro_rules! check_authorization { |  | ||||||
|     ($cookies:expr, $ctx:expr, $guild:expr) => { |  | ||||||
|         use serenity::model::id::UserId; |  | ||||||
|  |  | ||||||
|         let user_id = $cookies.get_private("userid").map(|c| c.value().parse::<u64>().ok()).flatten(); |  | ||||||
|  |  | ||||||
|         if std::env::var("OFFLINE").map_or(true, |v| v != "1") { |  | ||||||
|             match user_id { |  | ||||||
|                 Some(user_id) => { |  | ||||||
|                     match GuildId($guild).to_guild_cached($ctx) { |  | ||||||
|                         Some(guild) => { |  | ||||||
|                             let member_res = guild.member($ctx, UserId(user_id)).await; |  | ||||||
|  |  | ||||||
|                             match member_res { |  | ||||||
|                                 Err(_) => { |  | ||||||
|                                     return Err(json!({"error": "User not in guild"})); |  | ||||||
|                                 } |  | ||||||
|  |  | ||||||
|                                 Ok(member) => { |  | ||||||
|                                     let permissions_res = member.permissions($ctx); |  | ||||||
|  |  | ||||||
|                                     match permissions_res { |  | ||||||
|                                         Err(_) => { |  | ||||||
|                                             return Err(json!({"error": "Couldn't fetch permissions"})); |  | ||||||
|                                         } |  | ||||||
|  |  | ||||||
|                                         Ok(permissions) => { |  | ||||||
|                                             if !(permissions.manage_messages() || permissions.manage_guild() || permissions.administrator()) { |  | ||||||
|                                                 return Err(json!({"error": "Incorrect permissions"})); |  | ||||||
|                                             } |  | ||||||
|                                         } |  | ||||||
|                                     } |  | ||||||
|                                 } |  | ||||||
|                             } |  | ||||||
|                         } |  | ||||||
|  |  | ||||||
|                         None => { |  | ||||||
|                             return Err(json!({"error": "Bot not in guild"})); |  | ||||||
|                         } |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 None => { |  | ||||||
|                     return Err(json!({"error": "User not authorized"})); |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| macro_rules! update_field { | macro_rules! update_field { | ||||||
|     ($pool:expr, $error:ident, $reminder:ident.[$field:ident]) => { |     ($pool:expr, $error:ident, $reminder:ident.[$field:ident]) => { | ||||||
|         if let Some(value) = &$reminder.$field { |         if let Some(value) = &$reminder.$field { | ||||||
|   | |||||||
							
								
								
									
										43
									
								
								web/src/metrics.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								web/src/metrics.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,43 @@ | |||||||
|  | use lazy_static::lazy_static; | ||||||
|  | use prometheus::{IntCounterVec, Opts, Registry}; | ||||||
|  | use rocket::{ | ||||||
|  |     fairing::{Fairing, Info, Kind}, | ||||||
|  |     Data, Request, Response, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | lazy_static! { | ||||||
|  |     pub static ref REGISTRY: Registry = Registry::new(); | ||||||
|  |     static ref REQUEST_COUNTER: IntCounterVec = | ||||||
|  |         IntCounterVec::new(Opts::new("requests", "Requests"), &["method", "route"]).unwrap(); | ||||||
|  |     static ref RESPONSE_COUNTER: IntCounterVec = | ||||||
|  |         IntCounterVec::new(Opts::new("responses", "Responses"), &["status", "route"]).unwrap(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | pub fn init_metrics() { | ||||||
|  |     REGISTRY.register(Box::new(REQUEST_COUNTER.clone())).unwrap(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | pub struct MetricProducer; | ||||||
|  |  | ||||||
|  | #[rocket::async_trait] | ||||||
|  | impl Fairing for MetricProducer { | ||||||
|  |     fn info(&self) -> Info { | ||||||
|  |         Info { name: "Metrics fairing", kind: Kind::Request } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     async fn on_request(&self, req: &mut Request<'_>, _data: &mut Data<'_>) { | ||||||
|  |         if let Some(route) = req.route() { | ||||||
|  |             REQUEST_COUNTER | ||||||
|  |                 .with_label_values(&[req.method().as_str(), &route.uri.to_string()]) | ||||||
|  |                 .inc(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     async fn on_response<'r>(&self, req: &'r Request<'_>, resp: &mut Response<'r>) { | ||||||
|  |         if let Some(route) = req.route() { | ||||||
|  |             RESPONSE_COUNTER | ||||||
|  |                 .with_label_values(&[&resp.status().code.to_string(), &route.uri.to_string()]) | ||||||
|  |                 .inc(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										61
									
								
								web/src/routes/dashboard/api/guild/channels.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								web/src/routes/dashboard/api/guild/channels.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,61 @@ | |||||||
|  | use rocket::{http::CookieJar, serde::json::json, State}; | ||||||
|  | use serde::Serialize; | ||||||
|  | use serenity::{ | ||||||
|  |     client::Context, | ||||||
|  |     model::{ | ||||||
|  |         channel::GuildChannel, | ||||||
|  |         id::{ChannelId, GuildId}, | ||||||
|  |     }, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | use crate::{check_authorization, routes::JsonResult}; | ||||||
|  |  | ||||||
|  | #[derive(Serialize)] | ||||||
|  | struct ChannelInfo { | ||||||
|  |     id: String, | ||||||
|  |     name: String, | ||||||
|  |     webhook_avatar: Option<String>, | ||||||
|  |     webhook_name: Option<String>, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[get("/api/guild/<id>/channels")] | ||||||
|  | pub async fn get_guild_channels( | ||||||
|  |     id: u64, | ||||||
|  |     cookies: &CookieJar<'_>, | ||||||
|  |     ctx: &State<Context>, | ||||||
|  | ) -> JsonResult { | ||||||
|  |     offline!(Ok(json!(vec![ChannelInfo { | ||||||
|  |         name: "general".to_string(), | ||||||
|  |         id: "1".to_string(), | ||||||
|  |         webhook_avatar: None, | ||||||
|  |         webhook_name: None, | ||||||
|  |     }]))); | ||||||
|  |     check_authorization(cookies, ctx.inner(), id).await?; | ||||||
|  |  | ||||||
|  |     match GuildId(id).to_guild_cached(ctx.inner()) { | ||||||
|  |         Some(guild) => { | ||||||
|  |             let mut channels = guild | ||||||
|  |                 .channels | ||||||
|  |                 .iter() | ||||||
|  |                 .filter_map(|(id, channel)| channel.to_owned().guild().map(|c| (id.to_owned(), c))) | ||||||
|  |                 .filter(|(_, channel)| channel.is_text_based()) | ||||||
|  |                 .collect::<Vec<(ChannelId, GuildChannel)>>(); | ||||||
|  |  | ||||||
|  |             channels.sort_by(|(_, c1), (_, c2)| c1.position.cmp(&c2.position)); | ||||||
|  |  | ||||||
|  |             let channel_info = channels | ||||||
|  |                 .iter() | ||||||
|  |                 .map(|(channel_id, channel)| ChannelInfo { | ||||||
|  |                     name: channel.name.to_string(), | ||||||
|  |                     id: channel_id.to_string(), | ||||||
|  |                     webhook_avatar: None, | ||||||
|  |                     webhook_name: None, | ||||||
|  |                 }) | ||||||
|  |                 .collect::<Vec<ChannelInfo>>(); | ||||||
|  |  | ||||||
|  |             Ok(json!(channel_info)) | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         None => json_err!("Bot not in guild"), | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										42
									
								
								web/src/routes/dashboard/api/guild/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								web/src/routes/dashboard/api/guild/mod.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,42 @@ | |||||||
|  | mod channels; | ||||||
|  | mod reminders; | ||||||
|  | mod roles; | ||||||
|  | mod templates; | ||||||
|  |  | ||||||
|  | use std::env; | ||||||
|  |  | ||||||
|  | pub use channels::*; | ||||||
|  | pub use reminders::*; | ||||||
|  | use rocket::{http::CookieJar, serde::json::json, State}; | ||||||
|  | pub use roles::*; | ||||||
|  | use serenity::{ | ||||||
|  |     client::Context, | ||||||
|  |     model::id::{GuildId, RoleId}, | ||||||
|  | }; | ||||||
|  | pub use templates::*; | ||||||
|  |  | ||||||
|  | use crate::{check_authorization, routes::JsonResult}; | ||||||
|  |  | ||||||
|  | #[get("/api/guild/<id>")] | ||||||
|  | pub async fn get_guild_info(id: u64, cookies: &CookieJar<'_>, ctx: &State<Context>) -> JsonResult { | ||||||
|  |     offline!(Ok(json!({ "patreon": true, "name": "Guild" }))); | ||||||
|  |     check_authorization(cookies, ctx.inner(), id).await?; | ||||||
|  |  | ||||||
|  |     match GuildId(id).to_guild_cached(ctx.inner()) { | ||||||
|  |         Some(guild) => { | ||||||
|  |             let member_res = GuildId(env::var("PATREON_GUILD_ID").unwrap().parse().unwrap()) | ||||||
|  |                 .member(&ctx.inner(), guild.owner_id) | ||||||
|  |                 .await; | ||||||
|  |  | ||||||
|  |             let patreon = member_res.map_or(false, |member| { | ||||||
|  |                 member | ||||||
|  |                     .roles | ||||||
|  |                     .contains(&RoleId(env::var("PATREON_ROLE_ID").unwrap().parse().unwrap())) | ||||||
|  |             }); | ||||||
|  |  | ||||||
|  |             Ok(json!({ "patreon": patreon, "name": guild.name })) | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         None => json_err!("Bot not in guild"), | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										373
									
								
								web/src/routes/dashboard/api/guild/reminders.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										373
									
								
								web/src/routes/dashboard/api/guild/reminders.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,373 @@ | |||||||
|  | use rocket::{ | ||||||
|  |     http::CookieJar, | ||||||
|  |     serde::json::{json, Json}, | ||||||
|  |     State, | ||||||
|  | }; | ||||||
|  | use serenity::{ | ||||||
|  |     client::Context, | ||||||
|  |     model::id::{ChannelId, GuildId, UserId}, | ||||||
|  | }; | ||||||
|  | use sqlx::{MySql, Pool}; | ||||||
|  |  | ||||||
|  | use crate::{ | ||||||
|  |     check_authorization, check_guild_subscription, check_subscription, | ||||||
|  |     consts::MIN_INTERVAL, | ||||||
|  |     guards::transaction::Transaction, | ||||||
|  |     routes::{ | ||||||
|  |         dashboard::{ | ||||||
|  |             create_database_channel, create_reminder, DeleteReminder, PatchReminder, Reminder, | ||||||
|  |         }, | ||||||
|  |         JsonResult, | ||||||
|  |     }, | ||||||
|  |     Database, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | #[post("/api/guild/<id>/reminders", data = "<reminder>")] | ||||||
|  | pub async fn create_guild_reminder( | ||||||
|  |     id: u64, | ||||||
|  |     reminder: Json<Reminder>, | ||||||
|  |     cookies: &CookieJar<'_>, | ||||||
|  |     ctx: &State<Context>, | ||||||
|  |     mut transaction: Transaction<'_>, | ||||||
|  | ) -> JsonResult { | ||||||
|  |     check_authorization(cookies, ctx.inner(), id).await?; | ||||||
|  |  | ||||||
|  |     let user_id = | ||||||
|  |         cookies.get_private("userid").map(|c| c.value().parse::<u64>().ok()).flatten().unwrap(); | ||||||
|  |  | ||||||
|  |     match create_reminder( | ||||||
|  |         ctx.inner(), | ||||||
|  |         &mut transaction, | ||||||
|  |         GuildId(id), | ||||||
|  |         UserId(user_id), | ||||||
|  |         reminder.into_inner(), | ||||||
|  |     ) | ||||||
|  |     .await | ||||||
|  |     { | ||||||
|  |         Ok(r) => match transaction.commit().await { | ||||||
|  |             Ok(_) => Ok(r), | ||||||
|  |             Err(e) => { | ||||||
|  |                 warn!("Couldn't commit transaction: {:?}", e); | ||||||
|  |                 json_err!("Couldn't commit transaction.") | ||||||
|  |             } | ||||||
|  |         }, | ||||||
|  |  | ||||||
|  |         Err(e) => Err(e), | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[get("/api/guild/<id>/reminders")] | ||||||
|  | pub async fn get_reminders( | ||||||
|  |     id: u64, | ||||||
|  |     cookies: &CookieJar<'_>, | ||||||
|  |     ctx: &State<Context>, | ||||||
|  |     pool: &State<Pool<MySql>>, | ||||||
|  | ) -> JsonResult { | ||||||
|  |     check_authorization(cookies, ctx.inner(), id).await?; | ||||||
|  |  | ||||||
|  |     let channels_res = GuildId(id).channels(&ctx.inner()).await; | ||||||
|  |  | ||||||
|  |     match channels_res { | ||||||
|  |         Ok(channels) => { | ||||||
|  |             let channels = channels | ||||||
|  |                 .keys() | ||||||
|  |                 .into_iter() | ||||||
|  |                 .map(|k| k.as_u64().to_string()) | ||||||
|  |                 .collect::<Vec<String>>() | ||||||
|  |                 .join(","); | ||||||
|  |  | ||||||
|  |             sqlx::query_as_unchecked!( | ||||||
|  |                 Reminder, | ||||||
|  |                 "SELECT | ||||||
|  |                  reminders.attachment, | ||||||
|  |                  reminders.attachment_name, | ||||||
|  |                  reminders.avatar, | ||||||
|  |                  channels.channel, | ||||||
|  |                  reminders.content, | ||||||
|  |                  reminders.embed_author, | ||||||
|  |                  reminders.embed_author_url, | ||||||
|  |                  reminders.embed_color, | ||||||
|  |                  reminders.embed_description, | ||||||
|  |                  reminders.embed_footer, | ||||||
|  |                  reminders.embed_footer_url, | ||||||
|  |                  reminders.embed_image_url, | ||||||
|  |                  reminders.embed_thumbnail_url, | ||||||
|  |                  reminders.embed_title, | ||||||
|  |                  IFNULL(reminders.embed_fields, '[]') AS embed_fields, | ||||||
|  |                  reminders.enabled, | ||||||
|  |                  reminders.expires, | ||||||
|  |                  reminders.interval_seconds, | ||||||
|  |                  reminders.interval_days, | ||||||
|  |                  reminders.interval_months, | ||||||
|  |                  reminders.name, | ||||||
|  |                  reminders.restartable, | ||||||
|  |                  reminders.tts, | ||||||
|  |                  reminders.uid, | ||||||
|  |                  reminders.username, | ||||||
|  |                  reminders.utc_time | ||||||
|  |                 FROM reminders | ||||||
|  |                 LEFT JOIN channels ON channels.id = reminders.channel_id | ||||||
|  |                 WHERE `status` = 'pending' AND FIND_IN_SET(channels.channel, ?)", | ||||||
|  |                 channels | ||||||
|  |             ) | ||||||
|  |             .fetch_all(pool.inner()) | ||||||
|  |             .await | ||||||
|  |             .map(|r| Ok(json!(r))) | ||||||
|  |             .unwrap_or_else(|e| { | ||||||
|  |                 warn!("Failed to complete SQL query: {:?}", e); | ||||||
|  |  | ||||||
|  |                 json_err!("Could not load reminders") | ||||||
|  |             }) | ||||||
|  |         } | ||||||
|  |         Err(e) => { | ||||||
|  |             warn!("Could not fetch channels from {}: {:?}", id, e); | ||||||
|  |  | ||||||
|  |             Ok(json!([])) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[patch("/api/guild/<id>/reminders", data = "<reminder>")] | ||||||
|  | pub async fn edit_reminder( | ||||||
|  |     id: u64, | ||||||
|  |     reminder: Json<PatchReminder>, | ||||||
|  |     ctx: &State<Context>, | ||||||
|  |     mut transaction: Transaction<'_>, | ||||||
|  |     pool: &State<Pool<Database>>, | ||||||
|  |     cookies: &CookieJar<'_>, | ||||||
|  | ) -> JsonResult { | ||||||
|  |     check_authorization(cookies, ctx.inner(), id).await?; | ||||||
|  |  | ||||||
|  |     let mut error = vec![]; | ||||||
|  |  | ||||||
|  |     let user_id = | ||||||
|  |         cookies.get_private("userid").map(|c| c.value().parse::<u64>().ok()).flatten().unwrap(); | ||||||
|  |  | ||||||
|  |     if reminder.message_ok() { | ||||||
|  |         update_field!(transaction.executor(), error, reminder.[ | ||||||
|  |             content, | ||||||
|  |             embed_author, | ||||||
|  |             embed_description, | ||||||
|  |             embed_footer, | ||||||
|  |             embed_title, | ||||||
|  |             embed_fields, | ||||||
|  |             username | ||||||
|  |         ]); | ||||||
|  |     } else { | ||||||
|  |         error.push("Message exceeds limits.".to_string()); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     update_field!(transaction.executor(), error, reminder.[ | ||||||
|  |         attachment, | ||||||
|  |         attachment_name, | ||||||
|  |         avatar, | ||||||
|  |         embed_author_url, | ||||||
|  |         embed_color, | ||||||
|  |         embed_footer_url, | ||||||
|  |         embed_image_url, | ||||||
|  |         embed_thumbnail_url, | ||||||
|  |         enabled, | ||||||
|  |         expires, | ||||||
|  |         name, | ||||||
|  |         restartable, | ||||||
|  |         tts, | ||||||
|  |         utc_time | ||||||
|  |     ]); | ||||||
|  |  | ||||||
|  |     if reminder.interval_days.flatten().is_some() | ||||||
|  |         || reminder.interval_months.flatten().is_some() | ||||||
|  |         || reminder.interval_seconds.flatten().is_some() | ||||||
|  |     { | ||||||
|  |         if check_guild_subscription(&ctx.inner(), id).await | ||||||
|  |             || check_subscription(&ctx.inner(), user_id).await | ||||||
|  |         { | ||||||
|  |             let new_interval_length = match reminder.interval_days { | ||||||
|  |                 Some(interval) => interval.unwrap_or(0), | ||||||
|  |                 None => sqlx::query!( | ||||||
|  |                     "SELECT interval_days AS days FROM reminders WHERE uid = ?", | ||||||
|  |                     reminder.uid | ||||||
|  |                 ) | ||||||
|  |                 .fetch_one(transaction.executor()) | ||||||
|  |                 .await | ||||||
|  |                 .map_err(|e| { | ||||||
|  |                     warn!("Error updating reminder interval: {:?}", e); | ||||||
|  |                     json!({ "reminder": Option::<Reminder>::None, "errors": vec!["Unknown error"] }) | ||||||
|  |                 })? | ||||||
|  |                 .days | ||||||
|  |                 .unwrap_or(0), | ||||||
|  |             } * 86400 + match reminder.interval_months { | ||||||
|  |                 Some(interval) => interval.unwrap_or(0), | ||||||
|  |                 None => sqlx::query!( | ||||||
|  |                     "SELECT interval_months AS months FROM reminders WHERE uid = ?", | ||||||
|  |                     reminder.uid | ||||||
|  |                 ) | ||||||
|  |                 .fetch_one(transaction.executor()) | ||||||
|  |                 .await | ||||||
|  |                 .map_err(|e| { | ||||||
|  |                     warn!("Error updating reminder interval: {:?}", e); | ||||||
|  |                     json!({ "reminder": Option::<Reminder>::None, "errors": vec!["Unknown error"] }) | ||||||
|  |                 })? | ||||||
|  |                 .months | ||||||
|  |                 .unwrap_or(0), | ||||||
|  |             } * 2592000 + match reminder.interval_seconds { | ||||||
|  |                 Some(interval) => interval.unwrap_or(0), | ||||||
|  |                 None => sqlx::query!( | ||||||
|  |                     "SELECT interval_seconds AS seconds FROM reminders WHERE uid = ?", | ||||||
|  |                     reminder.uid | ||||||
|  |                 ) | ||||||
|  |                 .fetch_one(transaction.executor()) | ||||||
|  |                 .await | ||||||
|  |                 .map_err(|e| { | ||||||
|  |                     warn!("Error updating reminder interval: {:?}", e); | ||||||
|  |                     json!({ "reminder": Option::<Reminder>::None, "errors": vec!["Unknown error"] }) | ||||||
|  |                 })? | ||||||
|  |                 .seconds | ||||||
|  |                 .unwrap_or(0), | ||||||
|  |             }; | ||||||
|  |  | ||||||
|  |             if new_interval_length < *MIN_INTERVAL { | ||||||
|  |                 error.push(String::from("New interval is too short.")); | ||||||
|  |             } else { | ||||||
|  |                 update_field!(transaction.executor(), error, reminder.[ | ||||||
|  |                     interval_days, | ||||||
|  |                     interval_months, | ||||||
|  |                     interval_seconds | ||||||
|  |                 ]); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if reminder.channel > 0 { | ||||||
|  |         let channel = ChannelId(reminder.channel).to_channel_cached(&ctx.inner()); | ||||||
|  |         match channel { | ||||||
|  |             Some(channel) => { | ||||||
|  |                 let channel_matches_guild = channel.guild().map_or(false, |c| c.guild_id.0 == id); | ||||||
|  |  | ||||||
|  |                 if !channel_matches_guild { | ||||||
|  |                     warn!( | ||||||
|  |                         "Error in `edit_reminder`: channel {:?} not found for guild {}", | ||||||
|  |                         reminder.channel, id | ||||||
|  |                     ); | ||||||
|  |  | ||||||
|  |                     return Err(json!({"error": "Channel not found"})); | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 let channel = create_database_channel( | ||||||
|  |                     ctx.inner(), | ||||||
|  |                     ChannelId(reminder.channel), | ||||||
|  |                     &mut transaction, | ||||||
|  |                 ) | ||||||
|  |                 .await; | ||||||
|  |  | ||||||
|  |                 if let Err(e) = channel { | ||||||
|  |                     warn!("`create_database_channel` returned an error code: {:?}", e); | ||||||
|  |  | ||||||
|  |                     return Err( | ||||||
|  |                         json!({"error": "Failed to configure channel for reminders. Please check the bot permissions"}), | ||||||
|  |                     ); | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 let channel = channel.unwrap(); | ||||||
|  |  | ||||||
|  |                 match sqlx::query!( | ||||||
|  |                     "UPDATE reminders SET channel_id = ? WHERE uid = ?", | ||||||
|  |                     channel, | ||||||
|  |                     reminder.uid | ||||||
|  |                 ) | ||||||
|  |                 .execute(transaction.executor()) | ||||||
|  |                 .await | ||||||
|  |                 { | ||||||
|  |                     Ok(_) => {} | ||||||
|  |                     Err(e) => { | ||||||
|  |                         warn!("Error setting channel: {:?}", e); | ||||||
|  |  | ||||||
|  |                         error.push("Couldn't set channel".to_string()) | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             None => { | ||||||
|  |                 warn!( | ||||||
|  |                     "Error in `edit_reminder`: channel {:?} not found for guild {}", | ||||||
|  |                     reminder.channel, id | ||||||
|  |                 ); | ||||||
|  |  | ||||||
|  |                 return Err(json!({"error": "Channel not found"})); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if let Err(e) = transaction.commit().await { | ||||||
|  |         warn!("Couldn't commit transaction: {:?}", e); | ||||||
|  |         return json_err!("Couldn't commit transaction"); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     match sqlx::query_as_unchecked!( | ||||||
|  |         Reminder, | ||||||
|  |         "SELECT reminders.attachment, | ||||||
|  |          reminders.attachment_name, | ||||||
|  |          reminders.avatar, | ||||||
|  |          channels.channel, | ||||||
|  |          reminders.content, | ||||||
|  |          reminders.embed_author, | ||||||
|  |          reminders.embed_author_url, | ||||||
|  |          reminders.embed_color, | ||||||
|  |          reminders.embed_description, | ||||||
|  |          reminders.embed_footer, | ||||||
|  |          reminders.embed_footer_url, | ||||||
|  |          reminders.embed_image_url, | ||||||
|  |          reminders.embed_thumbnail_url, | ||||||
|  |          reminders.embed_title, | ||||||
|  |          reminders.embed_fields, | ||||||
|  |          reminders.enabled, | ||||||
|  |          reminders.expires, | ||||||
|  |          reminders.interval_seconds, | ||||||
|  |          reminders.interval_days, | ||||||
|  |          reminders.interval_months, | ||||||
|  |          reminders.name, | ||||||
|  |          reminders.restartable, | ||||||
|  |          reminders.tts, | ||||||
|  |          reminders.uid, | ||||||
|  |          reminders.username, | ||||||
|  |          reminders.utc_time | ||||||
|  |         FROM reminders | ||||||
|  |         LEFT JOIN channels ON channels.id = reminders.channel_id | ||||||
|  |         WHERE uid = ?", | ||||||
|  |         reminder.uid | ||||||
|  |     ) | ||||||
|  |     .fetch_one(pool.inner()) | ||||||
|  |     .await | ||||||
|  |     { | ||||||
|  |         Ok(reminder) => Ok(json!({"reminder": reminder, "errors": error})), | ||||||
|  |  | ||||||
|  |         Err(e) => { | ||||||
|  |             warn!("Error exiting `edit_reminder': {:?}", e); | ||||||
|  |  | ||||||
|  |             Err(json!({"reminder": Option::<Reminder>::None, "errors": vec!["Unknown error"]})) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[delete("/api/guild/<id>/reminders", data = "<reminder>")] | ||||||
|  | pub async fn delete_reminder( | ||||||
|  |     cookies: &CookieJar<'_>, | ||||||
|  |     id: u64, | ||||||
|  |     reminder: Json<DeleteReminder>, | ||||||
|  |     ctx: &State<Context>, | ||||||
|  |     pool: &State<Pool<MySql>>, | ||||||
|  | ) -> JsonResult { | ||||||
|  |     check_authorization(cookies, ctx.inner(), id).await?; | ||||||
|  |  | ||||||
|  |     match sqlx::query!("UPDATE reminders SET `status` = 'deleted' WHERE uid = ?", reminder.uid) | ||||||
|  |         .execute(pool.inner()) | ||||||
|  |         .await | ||||||
|  |     { | ||||||
|  |         Ok(_) => Ok(json!({})), | ||||||
|  |  | ||||||
|  |         Err(e) => { | ||||||
|  |             warn!("Error in `delete_reminder`: {:?}", e); | ||||||
|  |  | ||||||
|  |             Err(json!({"error": "Could not delete reminder"})) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										35
									
								
								web/src/routes/dashboard/api/guild/roles.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								web/src/routes/dashboard/api/guild/roles.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,35 @@ | |||||||
|  | use rocket::{http::CookieJar, serde::json::json, State}; | ||||||
|  | use serde::Serialize; | ||||||
|  | use serenity::client::Context; | ||||||
|  |  | ||||||
|  | use crate::{check_authorization, routes::JsonResult}; | ||||||
|  |  | ||||||
|  | #[derive(Serialize)] | ||||||
|  | struct RoleInfo { | ||||||
|  |     id: String, | ||||||
|  |     name: String, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[get("/api/guild/<id>/roles")] | ||||||
|  | pub async fn get_guild_roles(id: u64, cookies: &CookieJar<'_>, ctx: &State<Context>) -> JsonResult { | ||||||
|  |     offline!(Ok(json!(vec![RoleInfo { name: "@everyone".to_string(), id: "1".to_string() }]))); | ||||||
|  |     check_authorization(cookies, ctx.inner(), id).await?; | ||||||
|  |  | ||||||
|  |     let roles_res = ctx.cache.guild_roles(id); | ||||||
|  |  | ||||||
|  |     match roles_res { | ||||||
|  |         Some(roles) => { | ||||||
|  |             let roles = roles | ||||||
|  |                 .iter() | ||||||
|  |                 .map(|(_, r)| RoleInfo { id: r.id.to_string(), name: r.name.to_string() }) | ||||||
|  |                 .collect::<Vec<RoleInfo>>(); | ||||||
|  |  | ||||||
|  |             Ok(json!(roles)) | ||||||
|  |         } | ||||||
|  |         None => { | ||||||
|  |             warn!("Could not fetch roles from {}", id); | ||||||
|  |  | ||||||
|  |             json_err!("Could not get roles") | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										181
									
								
								web/src/routes/dashboard/api/guild/templates.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										181
									
								
								web/src/routes/dashboard/api/guild/templates.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,181 @@ | |||||||
|  | use rocket::{ | ||||||
|  |     http::CookieJar, | ||||||
|  |     serde::json::{json, Json}, | ||||||
|  |     State, | ||||||
|  | }; | ||||||
|  | use serenity::client::Context; | ||||||
|  | use sqlx::{MySql, Pool}; | ||||||
|  |  | ||||||
|  | use crate::{ | ||||||
|  |     check_authorization, | ||||||
|  |     consts::{ | ||||||
|  |         MAX_CONTENT_LENGTH, MAX_EMBED_AUTHOR_LENGTH, MAX_EMBED_DESCRIPTION_LENGTH, | ||||||
|  |         MAX_EMBED_FIELDS, MAX_EMBED_FIELD_TITLE_LENGTH, MAX_EMBED_FIELD_VALUE_LENGTH, | ||||||
|  |         MAX_EMBED_FOOTER_LENGTH, MAX_EMBED_TITLE_LENGTH, MAX_URL_LENGTH, MAX_USERNAME_LENGTH, | ||||||
|  |     }, | ||||||
|  |     routes::{ | ||||||
|  |         dashboard::{template_name_default, DeleteReminderTemplate, ReminderTemplate}, | ||||||
|  |         JsonResult, | ||||||
|  |     }, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | #[get("/api/guild/<id>/templates")] | ||||||
|  | pub async fn get_reminder_templates( | ||||||
|  |     id: u64, | ||||||
|  |     cookies: &CookieJar<'_>, | ||||||
|  |     ctx: &State<Context>, | ||||||
|  |     pool: &State<Pool<MySql>>, | ||||||
|  | ) -> JsonResult { | ||||||
|  |     check_authorization(cookies, ctx.inner(), id).await?; | ||||||
|  |  | ||||||
|  |     match sqlx::query_as_unchecked!( | ||||||
|  |         ReminderTemplate, | ||||||
|  |         "SELECT * FROM reminder_template WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?)", | ||||||
|  |         id | ||||||
|  |     ) | ||||||
|  |     .fetch_all(pool.inner()) | ||||||
|  |     .await | ||||||
|  |     { | ||||||
|  |         Ok(templates) => Ok(json!(templates)), | ||||||
|  |         Err(e) => { | ||||||
|  |             warn!("Could not fetch templates from {}: {:?}", id, e); | ||||||
|  |  | ||||||
|  |             json_err!("Could not get templates") | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[post("/api/guild/<id>/templates", data = "<reminder_template>")] | ||||||
|  | pub async fn create_reminder_template( | ||||||
|  |     id: u64, | ||||||
|  |     reminder_template: Json<ReminderTemplate>, | ||||||
|  |     cookies: &CookieJar<'_>, | ||||||
|  |     ctx: &State<Context>, | ||||||
|  |     pool: &State<Pool<MySql>>, | ||||||
|  | ) -> JsonResult { | ||||||
|  |     check_authorization(cookies, ctx.inner(), id).await?; | ||||||
|  |  | ||||||
|  |     // validate lengths | ||||||
|  |     check_length!(MAX_CONTENT_LENGTH, reminder_template.content); | ||||||
|  |     check_length!(MAX_EMBED_DESCRIPTION_LENGTH, reminder_template.embed_description); | ||||||
|  |     check_length!(MAX_EMBED_TITLE_LENGTH, reminder_template.embed_title); | ||||||
|  |     check_length!(MAX_EMBED_AUTHOR_LENGTH, reminder_template.embed_author); | ||||||
|  |     check_length!(MAX_EMBED_FOOTER_LENGTH, reminder_template.embed_footer); | ||||||
|  |     check_length_opt!(MAX_EMBED_FIELDS, reminder_template.embed_fields); | ||||||
|  |     if let Some(fields) = &reminder_template.embed_fields { | ||||||
|  |         for field in &fields.0 { | ||||||
|  |             check_length!(MAX_EMBED_FIELD_VALUE_LENGTH, field.value); | ||||||
|  |             check_length!(MAX_EMBED_FIELD_TITLE_LENGTH, field.title); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     check_length_opt!(MAX_USERNAME_LENGTH, reminder_template.username); | ||||||
|  |     check_length_opt!( | ||||||
|  |         MAX_URL_LENGTH, | ||||||
|  |         reminder_template.embed_footer_url, | ||||||
|  |         reminder_template.embed_thumbnail_url, | ||||||
|  |         reminder_template.embed_author_url, | ||||||
|  |         reminder_template.embed_image_url, | ||||||
|  |         reminder_template.avatar | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     // validate urls | ||||||
|  |     check_url_opt!( | ||||||
|  |         reminder_template.embed_footer_url, | ||||||
|  |         reminder_template.embed_thumbnail_url, | ||||||
|  |         reminder_template.embed_author_url, | ||||||
|  |         reminder_template.embed_image_url, | ||||||
|  |         reminder_template.avatar | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     let name = if reminder_template.name.is_empty() { | ||||||
|  |         template_name_default() | ||||||
|  |     } else { | ||||||
|  |         reminder_template.name.clone() | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     match sqlx::query!( | ||||||
|  |         "INSERT INTO reminder_template | ||||||
|  |         (guild_id, | ||||||
|  |          name, | ||||||
|  |          attachment, | ||||||
|  |          attachment_name, | ||||||
|  |          avatar, | ||||||
|  |          content, | ||||||
|  |          embed_author, | ||||||
|  |          embed_author_url, | ||||||
|  |          embed_color, | ||||||
|  |          embed_description, | ||||||
|  |          embed_footer, | ||||||
|  |          embed_footer_url, | ||||||
|  |          embed_image_url, | ||||||
|  |          embed_thumbnail_url, | ||||||
|  |          embed_title, | ||||||
|  |          embed_fields, | ||||||
|  |          interval_seconds, | ||||||
|  |          interval_days, | ||||||
|  |          interval_months, | ||||||
|  |          tts, | ||||||
|  |          username | ||||||
|  |         ) VALUES ((SELECT id FROM guilds WHERE guild = ?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, | ||||||
|  |          ?, ?, ?, ?, ?, ?, ?)", | ||||||
|  |         id, | ||||||
|  |         name, | ||||||
|  |         reminder_template.attachment, | ||||||
|  |         reminder_template.attachment_name, | ||||||
|  |         reminder_template.avatar, | ||||||
|  |         reminder_template.content, | ||||||
|  |         reminder_template.embed_author, | ||||||
|  |         reminder_template.embed_author_url, | ||||||
|  |         reminder_template.embed_color, | ||||||
|  |         reminder_template.embed_description, | ||||||
|  |         reminder_template.embed_footer, | ||||||
|  |         reminder_template.embed_footer_url, | ||||||
|  |         reminder_template.embed_image_url, | ||||||
|  |         reminder_template.embed_thumbnail_url, | ||||||
|  |         reminder_template.embed_title, | ||||||
|  |         reminder_template.embed_fields, | ||||||
|  |         reminder_template.interval_seconds, | ||||||
|  |         reminder_template.interval_days, | ||||||
|  |         reminder_template.interval_months, | ||||||
|  |         reminder_template.tts, | ||||||
|  |         reminder_template.username, | ||||||
|  |     ) | ||||||
|  |     .fetch_all(pool.inner()) | ||||||
|  |     .await | ||||||
|  |     { | ||||||
|  |         Ok(_) => Ok(json!({})), | ||||||
|  |         Err(e) => { | ||||||
|  |             warn!("Could not create template for {}: {:?}", id, e); | ||||||
|  |  | ||||||
|  |             json_err!("Could not create template") | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[delete("/api/guild/<id>/templates", data = "<delete_reminder_template>")] | ||||||
|  | pub async fn delete_reminder_template( | ||||||
|  |     id: u64, | ||||||
|  |     delete_reminder_template: Json<DeleteReminderTemplate>, | ||||||
|  |     cookies: &CookieJar<'_>, | ||||||
|  |     ctx: &State<Context>, | ||||||
|  |     pool: &State<Pool<MySql>>, | ||||||
|  | ) -> JsonResult { | ||||||
|  |     check_authorization(cookies, ctx.inner(), id).await?; | ||||||
|  |  | ||||||
|  |     match sqlx::query!( | ||||||
|  |         "DELETE FROM reminder_template WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?) AND id = ?", | ||||||
|  |         id, delete_reminder_template.id | ||||||
|  |     ) | ||||||
|  |     .fetch_all(pool.inner()) | ||||||
|  |     .await | ||||||
|  |     { | ||||||
|  |         Ok(_) => { | ||||||
|  |             Ok(json!({})) | ||||||
|  |         } | ||||||
|  |         Err(e) => { | ||||||
|  |             warn!("Could not delete template from {}: {:?}", id, e); | ||||||
|  |  | ||||||
|  |             json_err!("Could not delete template") | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										2
									
								
								web/src/routes/dashboard/api/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								web/src/routes/dashboard/api/mod.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,2 @@ | |||||||
|  | pub mod guild; | ||||||
|  | pub mod user; | ||||||
							
								
								
									
										81
									
								
								web/src/routes/dashboard/api/user/guilds.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								web/src/routes/dashboard/api/user/guilds.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,81 @@ | |||||||
|  | use reqwest::Client; | ||||||
|  | use rocket::{ | ||||||
|  |     http::CookieJar, | ||||||
|  |     serde::json::{json, Value as JsonValue}, | ||||||
|  |     State, | ||||||
|  | }; | ||||||
|  | use serde::{Deserialize, Serialize}; | ||||||
|  | use serenity::model::{id::GuildId, permissions::Permissions}; | ||||||
|  |  | ||||||
|  | use crate::consts::DISCORD_API; | ||||||
|  |  | ||||||
|  | #[derive(Serialize)] | ||||||
|  | struct GuildInfo { | ||||||
|  |     id: String, | ||||||
|  |     name: String, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[derive(Deserialize)] | ||||||
|  | struct PartialGuild { | ||||||
|  |     pub id: GuildId, | ||||||
|  |     pub name: String, | ||||||
|  |     #[serde(default)] | ||||||
|  |     pub owner: bool, | ||||||
|  |     #[serde(rename = "permissions_new")] | ||||||
|  |     pub permissions: Option<String>, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[get("/api/user/guilds")] | ||||||
|  | pub async fn get_user_guilds(cookies: &CookieJar<'_>, reqwest_client: &State<Client>) -> JsonValue { | ||||||
|  |     offline!(json!(vec![GuildInfo { id: "1".to_string(), name: "Guild".to_string() }])); | ||||||
|  |  | ||||||
|  |     if let Some(access_token) = cookies.get_private("access_token") { | ||||||
|  |         let request_res = reqwest_client | ||||||
|  |             .get(format!("{}/users/@me/guilds", DISCORD_API)) | ||||||
|  |             .bearer_auth(access_token.value()) | ||||||
|  |             .send() | ||||||
|  |             .await; | ||||||
|  |  | ||||||
|  |         match request_res { | ||||||
|  |             Ok(response) => { | ||||||
|  |                 let guilds_res = response.json::<Vec<PartialGuild>>().await; | ||||||
|  |  | ||||||
|  |                 match guilds_res { | ||||||
|  |                     Ok(guilds) => { | ||||||
|  |                         let reduced_guilds = guilds | ||||||
|  |                             .iter() | ||||||
|  |                             .filter(|g| { | ||||||
|  |                                 g.owner | ||||||
|  |                                     || g.permissions.as_ref().map_or(false, |p| { | ||||||
|  |                                         let permissions = | ||||||
|  |                                             Permissions::from_bits_truncate(p.parse().unwrap()); | ||||||
|  |  | ||||||
|  |                                         permissions.manage_messages() | ||||||
|  |                                             || permissions.manage_guild() | ||||||
|  |                                             || permissions.administrator() | ||||||
|  |                                     }) | ||||||
|  |                             }) | ||||||
|  |                             .map(|g| GuildInfo { id: g.id.to_string(), name: g.name.to_string() }) | ||||||
|  |                             .collect::<Vec<GuildInfo>>(); | ||||||
|  |  | ||||||
|  |                         json!(reduced_guilds) | ||||||
|  |                     } | ||||||
|  |  | ||||||
|  |                     Err(e) => { | ||||||
|  |                         warn!("Error constructing user from request: {:?}", e); | ||||||
|  |  | ||||||
|  |                         json!({"error": "Could not get user details"}) | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             Err(e) => { | ||||||
|  |                 warn!("Error getting user guilds: {:?}", e); | ||||||
|  |  | ||||||
|  |                 json!({"error": "Could not reach Discord"}) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } else { | ||||||
|  |         json!({"error": "Not authorized"}) | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										97
									
								
								web/src/routes/dashboard/api/user/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										97
									
								
								web/src/routes/dashboard/api/user/mod.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,97 @@ | |||||||
|  | mod guilds; | ||||||
|  |  | ||||||
|  | use std::env; | ||||||
|  |  | ||||||
|  | use chrono_tz::Tz; | ||||||
|  | pub use guilds::*; | ||||||
|  | use rocket::{ | ||||||
|  |     http::CookieJar, | ||||||
|  |     serde::json::{json, Json, Value as JsonValue}, | ||||||
|  |     State, | ||||||
|  | }; | ||||||
|  | use serde::{Deserialize, Serialize}; | ||||||
|  | use serenity::{ | ||||||
|  |     client::Context, | ||||||
|  |     model::id::{GuildId, RoleId}, | ||||||
|  | }; | ||||||
|  | use sqlx::{MySql, Pool}; | ||||||
|  |  | ||||||
|  | #[derive(Serialize)] | ||||||
|  | struct UserInfo { | ||||||
|  |     name: String, | ||||||
|  |     patreon: bool, | ||||||
|  |     timezone: Option<String>, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[derive(Deserialize)] | ||||||
|  | pub struct UpdateUser { | ||||||
|  |     timezone: String, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[get("/api/user")] | ||||||
|  | pub async fn get_user_info( | ||||||
|  |     cookies: &CookieJar<'_>, | ||||||
|  |     ctx: &State<Context>, | ||||||
|  |     pool: &State<Pool<MySql>>, | ||||||
|  | ) -> JsonValue { | ||||||
|  |     offline!(json!(UserInfo { name: "Discord".to_string(), patreon: true, timezone: None })); | ||||||
|  |  | ||||||
|  |     if let Some(user_id) = | ||||||
|  |         cookies.get_private("userid").map(|u| u.value().parse::<u64>().ok()).flatten() | ||||||
|  |     { | ||||||
|  |         let member_res = GuildId(env::var("PATREON_GUILD_ID").unwrap().parse().unwrap()) | ||||||
|  |             .member(&ctx.inner(), user_id) | ||||||
|  |             .await; | ||||||
|  |  | ||||||
|  |         let timezone = sqlx::query!( | ||||||
|  |             "SELECT IFNULL(timezone, 'UTC') AS timezone FROM users WHERE user = ?", | ||||||
|  |             user_id | ||||||
|  |         ) | ||||||
|  |         .fetch_one(pool.inner()) | ||||||
|  |         .await | ||||||
|  |         .map_or(None, |q| Some(q.timezone)); | ||||||
|  |  | ||||||
|  |         let user_info = UserInfo { | ||||||
|  |             name: cookies | ||||||
|  |                 .get_private("username") | ||||||
|  |                 .map_or("DiscordUser#0000".to_string(), |c| c.value().to_string()), | ||||||
|  |             patreon: member_res.map_or(false, |member| { | ||||||
|  |                 member | ||||||
|  |                     .roles | ||||||
|  |                     .contains(&RoleId(env::var("PATREON_ROLE_ID").unwrap().parse().unwrap())) | ||||||
|  |             }), | ||||||
|  |             timezone, | ||||||
|  |         }; | ||||||
|  |  | ||||||
|  |         json!(user_info) | ||||||
|  |     } else { | ||||||
|  |         json!({"error": "Not authorized"}) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[patch("/api/user", data = "<user>")] | ||||||
|  | pub async fn update_user_info( | ||||||
|  |     cookies: &CookieJar<'_>, | ||||||
|  |     user: Json<UpdateUser>, | ||||||
|  |     pool: &State<Pool<MySql>>, | ||||||
|  | ) -> JsonValue { | ||||||
|  |     if let Some(user_id) = | ||||||
|  |         cookies.get_private("userid").map(|u| u.value().parse::<u64>().ok()).flatten() | ||||||
|  |     { | ||||||
|  |         if user.timezone.parse::<Tz>().is_ok() { | ||||||
|  |             let _ = sqlx::query!( | ||||||
|  |                 "UPDATE users SET timezone = ? WHERE user = ?", | ||||||
|  |                 user.timezone, | ||||||
|  |                 user_id, | ||||||
|  |             ) | ||||||
|  |             .execute(pool.inner()) | ||||||
|  |             .await; | ||||||
|  |  | ||||||
|  |             json!({}) | ||||||
|  |         } else { | ||||||
|  |             json!({"error": "Timezone not recognized"}) | ||||||
|  |         } | ||||||
|  |     } else { | ||||||
|  |         json!({"error": "Not authorized"}) | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										20
									
								
								web/src/routes/dashboard/api/user/models.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								web/src/routes/dashboard/api/user/models.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | |||||||
|  | use std::env; | ||||||
|  |  | ||||||
|  | use chrono_tz::Tz; | ||||||
|  | use reqwest::Client; | ||||||
|  | use rocket::{ | ||||||
|  |     http::CookieJar, | ||||||
|  |     serde::json::{json, Json, Value as JsonValue}, | ||||||
|  |     State, | ||||||
|  | }; | ||||||
|  | use serde::{Deserialize, Serialize}; | ||||||
|  | use serenity::{ | ||||||
|  |     client::Context, | ||||||
|  |     model::{ | ||||||
|  |         id::{GuildId, RoleId}, | ||||||
|  |         permissions::Permissions, | ||||||
|  |     }, | ||||||
|  | }; | ||||||
|  | use sqlx::{MySql, Pool}; | ||||||
|  |  | ||||||
|  | use crate::{consts::DISCORD_API, routes::JsonResult}; | ||||||
							
								
								
									
										29
									
								
								web/src/routes/dashboard/api/user/reminders.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								web/src/routes/dashboard/api/user/reminders.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,29 @@ | |||||||
|  | use std::env; | ||||||
|  |  | ||||||
|  | use chrono_tz::Tz; | ||||||
|  | use reqwest::Client; | ||||||
|  | use rocket::{ | ||||||
|  |     http::CookieJar, | ||||||
|  |     serde::json::{json, Json, Value as JsonValue}, | ||||||
|  |     State, | ||||||
|  | }; | ||||||
|  | use serde::{Deserialize, Serialize}; | ||||||
|  | use serenity::{ | ||||||
|  |     client::Context, | ||||||
|  |     model::{ | ||||||
|  |         id::{GuildId, RoleId}, | ||||||
|  |         permissions::Permissions, | ||||||
|  |     }, | ||||||
|  | }; | ||||||
|  | use sqlx::{MySql, Pool}; | ||||||
|  |  | ||||||
|  | use crate::{consts::DISCORD_API, routes::JsonResult}; | ||||||
|  |  | ||||||
|  | #[get("/api/user/reminders")] | ||||||
|  | pub async fn get_reminders( | ||||||
|  |     cookies: &CookieJar<'_>, | ||||||
|  |     ctx: &State<Context>, | ||||||
|  |     pool: &State<Pool<MySql>>, | ||||||
|  | ) -> JsonResult { | ||||||
|  |     Ok(json! {}) | ||||||
|  | } | ||||||
| @@ -6,15 +6,20 @@ use rocket::{ | |||||||
| }; | }; | ||||||
| use serenity::{ | use serenity::{ | ||||||
|     client::Context, |     client::Context, | ||||||
|     model::id::{ChannelId, GuildId}, |     model::id::{ChannelId, GuildId, UserId}, | ||||||
| }; | }; | ||||||
| use sqlx::{MySql, Pool}; | use sqlx::{MySql, Pool}; | ||||||
|  |  | ||||||
| use crate::routes::{ | use crate::{ | ||||||
|  |     check_authorization, | ||||||
|  |     guards::transaction::Transaction, | ||||||
|  |     routes::{ | ||||||
|         dashboard::{ |         dashboard::{ | ||||||
|         create_reminder, ImportBody, ReminderCreate, ReminderCsv, ReminderTemplateCsv, TodoCsv, |             create_reminder, generate_uid, ImportBody, Reminder, ReminderCsv, ReminderTemplateCsv, | ||||||
|  |             TodoCsv, | ||||||
|         }, |         }, | ||||||
|         JsonResult, |         JsonResult, | ||||||
|  |     }, | ||||||
| }; | }; | ||||||
|  |  | ||||||
| #[get("/api/guild/<id>/export/reminders")] | #[get("/api/guild/<id>/export/reminders")] | ||||||
| @@ -24,7 +29,7 @@ pub async fn export_reminders( | |||||||
|     ctx: &State<Context>, |     ctx: &State<Context>, | ||||||
|     pool: &State<Pool<MySql>>, |     pool: &State<Pool<MySql>>, | ||||||
| ) -> JsonResult { | ) -> JsonResult { | ||||||
|     check_authorization!(cookies, ctx.inner(), id); |     check_authorization(cookies, ctx.inner(), id).await?; | ||||||
|  |  | ||||||
|     let mut csv_writer = WriterBuilder::new().quote_style(QuoteStyle::Always).from_writer(vec![]); |     let mut csv_writer = WriterBuilder::new().quote_style(QuoteStyle::Always).from_writer(vec![]); | ||||||
|  |  | ||||||
| @@ -69,7 +74,7 @@ pub async fn export_reminders( | |||||||
|                  reminders.utc_time |                  reminders.utc_time | ||||||
|                 FROM reminders |                 FROM reminders | ||||||
|                 LEFT JOIN channels ON channels.id = reminders.channel_id |                 LEFT JOIN channels ON channels.id = reminders.channel_id | ||||||
|                 WHERE FIND_IN_SET(channels.channel, ?)", |                 WHERE FIND_IN_SET(channels.channel, ?) AND status = 'pending'", | ||||||
|                 channels |                 channels | ||||||
|             ) |             ) | ||||||
|             .fetch_all(pool.inner()) |             .fetch_all(pool.inner()) | ||||||
| @@ -117,14 +122,14 @@ pub async fn export_reminders( | |||||||
| } | } | ||||||
|  |  | ||||||
| #[put("/api/guild/<id>/export/reminders", data = "<body>")] | #[put("/api/guild/<id>/export/reminders", data = "<body>")] | ||||||
| pub async fn import_reminders( | pub(crate) async fn import_reminders( | ||||||
|     id: u64, |     id: u64, | ||||||
|     cookies: &CookieJar<'_>, |     cookies: &CookieJar<'_>, | ||||||
|     body: Json<ImportBody>, |     body: Json<ImportBody>, | ||||||
|     ctx: &State<Context>, |     ctx: &State<Context>, | ||||||
|     pool: &State<Pool<MySql>>, |     mut transaction: Transaction<'_>, | ||||||
| ) -> JsonResult { | ) -> JsonResult { | ||||||
|     check_authorization!(cookies, ctx.inner(), id); |     check_authorization(cookies, ctx.inner(), id).await?; | ||||||
|  |  | ||||||
|     let user_id = |     let user_id = | ||||||
|         cookies.get_private("userid").map(|c| c.value().parse::<u64>().ok()).flatten().unwrap(); |         cookies.get_private("userid").map(|c| c.value().parse::<u64>().ok()).flatten().unwrap(); | ||||||
| @@ -132,6 +137,7 @@ pub async fn import_reminders( | |||||||
|     match base64::decode(&body.body) { |     match base64::decode(&body.body) { | ||||||
|         Ok(body) => { |         Ok(body) => { | ||||||
|             let mut reader = csv::Reader::from_reader(body.as_slice()); |             let mut reader = csv::Reader::from_reader(body.as_slice()); | ||||||
|  |             let mut count = 0; | ||||||
|  |  | ||||||
|             for result in reader.deserialize::<ReminderCsv>() { |             for result in reader.deserialize::<ReminderCsv>() { | ||||||
|                 match result { |                 match result { | ||||||
| @@ -140,7 +146,7 @@ pub async fn import_reminders( | |||||||
|  |  | ||||||
|                         match channel_id.parse::<u64>() { |                         match channel_id.parse::<u64>() { | ||||||
|                             Ok(channel_id) => { |                             Ok(channel_id) => { | ||||||
|                                 let reminder = ReminderCreate { |                                 let reminder = Reminder { | ||||||
|                                     attachment: record.attachment, |                                     attachment: record.attachment, | ||||||
|                                     attachment_name: record.attachment_name, |                                     attachment_name: record.attachment_name, | ||||||
|                                     avatar: record.avatar, |                                     avatar: record.avatar, | ||||||
| @@ -167,18 +173,21 @@ pub async fn import_reminders( | |||||||
|                                     name: record.name, |                                     name: record.name, | ||||||
|                                     restartable: record.restartable, |                                     restartable: record.restartable, | ||||||
|                                     tts: record.tts, |                                     tts: record.tts, | ||||||
|  |                                     uid: generate_uid(), | ||||||
|                                     username: record.username, |                                     username: record.username, | ||||||
|                                     utc_time: record.utc_time, |                                     utc_time: record.utc_time, | ||||||
|                                 }; |                                 }; | ||||||
|  |  | ||||||
|                                 create_reminder( |                                 create_reminder( | ||||||
|                                     ctx.inner(), |                                     ctx.inner(), | ||||||
|                                     pool.inner(), |                                     &mut transaction, | ||||||
|                                     GuildId(id), |                                     GuildId(id), | ||||||
|                                     UserId(user_id), |                                     UserId(user_id), | ||||||
|                                     reminder, |                                     reminder, | ||||||
|                                 ) |                                 ) | ||||||
|                                 .await?; |                                 .await?; | ||||||
|  |  | ||||||
|  |                                 count += 1; | ||||||
|                             } |                             } | ||||||
|  |  | ||||||
|                             Err(_) => { |                             Err(_) => { | ||||||
| @@ -198,7 +207,16 @@ pub async fn import_reminders( | |||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             Ok(json!({})) |             match transaction.commit().await { | ||||||
|  |                 Ok(_) => Ok(json!({ | ||||||
|  |                     "message": format!("Imported {} reminders", count) | ||||||
|  |                 })), | ||||||
|  |  | ||||||
|  |                 Err(e) => { | ||||||
|  |                     warn!("Failed to commit transaction: {:?}", e); | ||||||
|  |                     json_err!("Couldn't commit transaction") | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         Err(_) => { |         Err(_) => { | ||||||
| @@ -214,7 +232,7 @@ pub async fn export_todos( | |||||||
|     ctx: &State<Context>, |     ctx: &State<Context>, | ||||||
|     pool: &State<Pool<MySql>>, |     pool: &State<Pool<MySql>>, | ||||||
| ) -> JsonResult { | ) -> JsonResult { | ||||||
|     check_authorization!(cookies, ctx.inner(), id); |     check_authorization(cookies, ctx.inner(), id).await?; | ||||||
|  |  | ||||||
|     let mut csv_writer = WriterBuilder::new().quote_style(QuoteStyle::Always).from_writer(vec![]); |     let mut csv_writer = WriterBuilder::new().quote_style(QuoteStyle::Always).from_writer(vec![]); | ||||||
|  |  | ||||||
| @@ -269,7 +287,7 @@ pub async fn import_todos( | |||||||
|     ctx: &State<Context>, |     ctx: &State<Context>, | ||||||
|     pool: &State<Pool<MySql>>, |     pool: &State<Pool<MySql>>, | ||||||
| ) -> JsonResult { | ) -> JsonResult { | ||||||
|     check_authorization!(cookies, ctx.inner(), id); |     check_authorization(cookies, ctx.inner(), id).await?; | ||||||
|  |  | ||||||
|     let channels_res = GuildId(id).channels(&ctx.inner()).await; |     let channels_res = GuildId(id).channels(&ctx.inner()).await; | ||||||
|  |  | ||||||
| @@ -364,7 +382,7 @@ pub async fn export_reminder_templates( | |||||||
|     ctx: &State<Context>, |     ctx: &State<Context>, | ||||||
|     pool: &State<Pool<MySql>>, |     pool: &State<Pool<MySql>>, | ||||||
| ) -> JsonResult { | ) -> JsonResult { | ||||||
|     check_authorization!(cookies, ctx.inner(), id); |     check_authorization(cookies, ctx.inner(), id).await?; | ||||||
|  |  | ||||||
|     let mut csv_writer = WriterBuilder::new().quote_style(QuoteStyle::Always).from_writer(vec![]); |     let mut csv_writer = WriterBuilder::new().quote_style(QuoteStyle::Always).from_writer(vec![]); | ||||||
|  |  | ||||||
| @@ -386,6 +404,9 @@ pub async fn export_reminder_templates( | |||||||
|          embed_thumbnail_url, |          embed_thumbnail_url, | ||||||
|          embed_title, |          embed_title, | ||||||
|          embed_fields, |          embed_fields, | ||||||
|  |          interval_seconds, | ||||||
|  |          interval_days, | ||||||
|  |          interval_months, | ||||||
|          tts, |          tts, | ||||||
|          username |          username | ||||||
|         FROM reminder_template WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?)", |         FROM reminder_template WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?)", | ||||||
|   | |||||||
| @@ -1,621 +0,0 @@ | |||||||
| use std::env; |  | ||||||
|  |  | ||||||
| use rocket::{ |  | ||||||
|     http::CookieJar, |  | ||||||
|     serde::json::{json, Json}, |  | ||||||
|     State, |  | ||||||
| }; |  | ||||||
| use serde::Serialize; |  | ||||||
| use serenity::{ |  | ||||||
|     client::Context, |  | ||||||
|     model::{ |  | ||||||
|         channel::GuildChannel, |  | ||||||
|         id::{ChannelId, GuildId, RoleId}, |  | ||||||
|     }, |  | ||||||
| }; |  | ||||||
| use sqlx::{MySql, Pool}; |  | ||||||
|  |  | ||||||
| use crate::{ |  | ||||||
|     check_guild_subscription, check_subscription, |  | ||||||
|     consts::{ |  | ||||||
|         MAX_CONTENT_LENGTH, MAX_EMBED_AUTHOR_LENGTH, MAX_EMBED_DESCRIPTION_LENGTH, |  | ||||||
|         MAX_EMBED_FIELDS, MAX_EMBED_FIELD_TITLE_LENGTH, MAX_EMBED_FIELD_VALUE_LENGTH, |  | ||||||
|         MAX_EMBED_FOOTER_LENGTH, MAX_EMBED_TITLE_LENGTH, MAX_URL_LENGTH, MAX_USERNAME_LENGTH, |  | ||||||
|         MIN_INTERVAL, |  | ||||||
|     }, |  | ||||||
|     routes::{ |  | ||||||
|         dashboard::{ |  | ||||||
|             create_database_channel, create_reminder, template_name_default, DeleteReminder, |  | ||||||
|             DeleteReminderTemplate, PatchReminder, Reminder, ReminderCreate, ReminderTemplate, |  | ||||||
|         }, |  | ||||||
|         JsonResult, |  | ||||||
|     }, |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| #[derive(Serialize)] |  | ||||||
| struct ChannelInfo { |  | ||||||
|     id: String, |  | ||||||
|     name: String, |  | ||||||
|     webhook_avatar: Option<String>, |  | ||||||
|     webhook_name: Option<String>, |  | ||||||
| } |  | ||||||
|  |  | ||||||
| #[get("/api/guild/<id>/patreon")] |  | ||||||
| pub async fn get_guild_patreon( |  | ||||||
|     id: u64, |  | ||||||
|     cookies: &CookieJar<'_>, |  | ||||||
|     ctx: &State<Context>, |  | ||||||
| ) -> JsonResult { |  | ||||||
|     offline!(Ok(json!({ "patreon": true }))); |  | ||||||
|     check_authorization!(cookies, ctx.inner(), id); |  | ||||||
|  |  | ||||||
|     match GuildId(id).to_guild_cached(ctx.inner()) { |  | ||||||
|         Some(guild) => { |  | ||||||
|             let member_res = GuildId(env::var("PATREON_GUILD_ID").unwrap().parse().unwrap()) |  | ||||||
|                 .member(&ctx.inner(), guild.owner_id) |  | ||||||
|                 .await; |  | ||||||
|  |  | ||||||
|             let patreon = member_res.map_or(false, |member| { |  | ||||||
|                 member |  | ||||||
|                     .roles |  | ||||||
|                     .contains(&RoleId(env::var("PATREON_ROLE_ID").unwrap().parse().unwrap())) |  | ||||||
|             }); |  | ||||||
|  |  | ||||||
|             Ok(json!({ "patreon": patreon })) |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         None => json_err!("Bot not in guild"), |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| #[get("/api/guild/<id>/channels")] |  | ||||||
| pub async fn get_guild_channels( |  | ||||||
|     id: u64, |  | ||||||
|     cookies: &CookieJar<'_>, |  | ||||||
|     ctx: &State<Context>, |  | ||||||
| ) -> JsonResult { |  | ||||||
|     offline!(Ok(json!(vec![ChannelInfo { |  | ||||||
|         name: "general".to_string(), |  | ||||||
|         id: "1".to_string(), |  | ||||||
|         webhook_avatar: None, |  | ||||||
|         webhook_name: None, |  | ||||||
|     }]))); |  | ||||||
|     check_authorization!(cookies, ctx.inner(), id); |  | ||||||
|  |  | ||||||
|     match GuildId(id).to_guild_cached(ctx.inner()) { |  | ||||||
|         Some(guild) => { |  | ||||||
|             let mut channels = guild |  | ||||||
|                 .channels |  | ||||||
|                 .iter() |  | ||||||
|                 .filter_map(|(id, channel)| channel.to_owned().guild().map(|c| (id.to_owned(), c))) |  | ||||||
|                 .filter(|(_, channel)| channel.is_text_based()) |  | ||||||
|                 .collect::<Vec<(ChannelId, GuildChannel)>>(); |  | ||||||
|  |  | ||||||
|             channels.sort_by(|(_, c1), (_, c2)| c1.position.cmp(&c2.position)); |  | ||||||
|  |  | ||||||
|             let channel_info = channels |  | ||||||
|                 .iter() |  | ||||||
|                 .map(|(channel_id, channel)| ChannelInfo { |  | ||||||
|                     name: channel.name.to_string(), |  | ||||||
|                     id: channel_id.to_string(), |  | ||||||
|                     webhook_avatar: None, |  | ||||||
|                     webhook_name: None, |  | ||||||
|                 }) |  | ||||||
|                 .collect::<Vec<ChannelInfo>>(); |  | ||||||
|  |  | ||||||
|             Ok(json!(channel_info)) |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         None => json_err!("Bot not in guild"), |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| #[derive(Serialize)] |  | ||||||
| struct RoleInfo { |  | ||||||
|     id: String, |  | ||||||
|     name: String, |  | ||||||
| } |  | ||||||
|  |  | ||||||
| #[get("/api/guild/<id>/roles")] |  | ||||||
| pub async fn get_guild_roles(id: u64, cookies: &CookieJar<'_>, ctx: &State<Context>) -> JsonResult { |  | ||||||
|     offline!(Ok(json!(vec![RoleInfo { name: "@everyone".to_string(), id: "1".to_string() }]))); |  | ||||||
|     check_authorization!(cookies, ctx.inner(), id); |  | ||||||
|  |  | ||||||
|     let roles_res = ctx.cache.guild_roles(id); |  | ||||||
|  |  | ||||||
|     match roles_res { |  | ||||||
|         Some(roles) => { |  | ||||||
|             let roles = roles |  | ||||||
|                 .iter() |  | ||||||
|                 .map(|(_, r)| RoleInfo { id: r.id.to_string(), name: r.name.to_string() }) |  | ||||||
|                 .collect::<Vec<RoleInfo>>(); |  | ||||||
|  |  | ||||||
|             Ok(json!(roles)) |  | ||||||
|         } |  | ||||||
|         None => { |  | ||||||
|             warn!("Could not fetch roles from {}", id); |  | ||||||
|  |  | ||||||
|             json_err!("Could not get roles") |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| #[get("/api/guild/<id>/templates")] |  | ||||||
| pub async fn get_reminder_templates( |  | ||||||
|     id: u64, |  | ||||||
|     cookies: &CookieJar<'_>, |  | ||||||
|     ctx: &State<Context>, |  | ||||||
|     pool: &State<Pool<MySql>>, |  | ||||||
| ) -> JsonResult { |  | ||||||
|     check_authorization!(cookies, ctx.inner(), id); |  | ||||||
|  |  | ||||||
|     match sqlx::query_as_unchecked!( |  | ||||||
|         ReminderTemplate, |  | ||||||
|         "SELECT * FROM reminder_template WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?)", |  | ||||||
|         id |  | ||||||
|     ) |  | ||||||
|     .fetch_all(pool.inner()) |  | ||||||
|     .await |  | ||||||
|     { |  | ||||||
|         Ok(templates) => Ok(json!(templates)), |  | ||||||
|         Err(e) => { |  | ||||||
|             warn!("Could not fetch templates from {}: {:?}", id, e); |  | ||||||
|  |  | ||||||
|             json_err!("Could not get templates") |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| #[post("/api/guild/<id>/templates", data = "<reminder_template>")] |  | ||||||
| pub async fn create_reminder_template( |  | ||||||
|     id: u64, |  | ||||||
|     reminder_template: Json<ReminderTemplate>, |  | ||||||
|     cookies: &CookieJar<'_>, |  | ||||||
|     ctx: &State<Context>, |  | ||||||
|     pool: &State<Pool<MySql>>, |  | ||||||
| ) -> JsonResult { |  | ||||||
|     check_authorization!(cookies, ctx.inner(), id); |  | ||||||
|  |  | ||||||
|     // validate lengths |  | ||||||
|     check_length!(MAX_CONTENT_LENGTH, reminder_template.content); |  | ||||||
|     check_length!(MAX_EMBED_DESCRIPTION_LENGTH, reminder_template.embed_description); |  | ||||||
|     check_length!(MAX_EMBED_TITLE_LENGTH, reminder_template.embed_title); |  | ||||||
|     check_length!(MAX_EMBED_AUTHOR_LENGTH, reminder_template.embed_author); |  | ||||||
|     check_length!(MAX_EMBED_FOOTER_LENGTH, reminder_template.embed_footer); |  | ||||||
|     check_length_opt!(MAX_EMBED_FIELDS, reminder_template.embed_fields); |  | ||||||
|     if let Some(fields) = &reminder_template.embed_fields { |  | ||||||
|         for field in &fields.0 { |  | ||||||
|             check_length!(MAX_EMBED_FIELD_VALUE_LENGTH, field.value); |  | ||||||
|             check_length!(MAX_EMBED_FIELD_TITLE_LENGTH, field.title); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|     check_length_opt!(MAX_USERNAME_LENGTH, reminder_template.username); |  | ||||||
|     check_length_opt!( |  | ||||||
|         MAX_URL_LENGTH, |  | ||||||
|         reminder_template.embed_footer_url, |  | ||||||
|         reminder_template.embed_thumbnail_url, |  | ||||||
|         reminder_template.embed_author_url, |  | ||||||
|         reminder_template.embed_image_url, |  | ||||||
|         reminder_template.avatar |  | ||||||
|     ); |  | ||||||
|  |  | ||||||
|     // validate urls |  | ||||||
|     check_url_opt!( |  | ||||||
|         reminder_template.embed_footer_url, |  | ||||||
|         reminder_template.embed_thumbnail_url, |  | ||||||
|         reminder_template.embed_author_url, |  | ||||||
|         reminder_template.embed_image_url, |  | ||||||
|         reminder_template.avatar |  | ||||||
|     ); |  | ||||||
|  |  | ||||||
|     let name = if reminder_template.name.is_empty() { |  | ||||||
|         template_name_default() |  | ||||||
|     } else { |  | ||||||
|         reminder_template.name.clone() |  | ||||||
|     }; |  | ||||||
|  |  | ||||||
|     match sqlx::query!( |  | ||||||
|         "INSERT INTO reminder_template |  | ||||||
|         (guild_id, |  | ||||||
|          name, |  | ||||||
|          attachment, |  | ||||||
|          attachment_name, |  | ||||||
|          avatar, |  | ||||||
|          content, |  | ||||||
|          embed_author, |  | ||||||
|          embed_author_url, |  | ||||||
|          embed_color, |  | ||||||
|          embed_description, |  | ||||||
|          embed_footer, |  | ||||||
|          embed_footer_url, |  | ||||||
|          embed_image_url, |  | ||||||
|          embed_thumbnail_url, |  | ||||||
|          embed_title, |  | ||||||
|          embed_fields, |  | ||||||
|          tts, |  | ||||||
|          username |  | ||||||
|         ) VALUES ((SELECT id FROM guilds WHERE guild = ?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", |  | ||||||
|         id, name, |  | ||||||
|         reminder_template.attachment, |  | ||||||
|         reminder_template.attachment_name, |  | ||||||
|         reminder_template.avatar, |  | ||||||
|         reminder_template.content, |  | ||||||
|         reminder_template.embed_author, |  | ||||||
|         reminder_template.embed_author_url, |  | ||||||
|         reminder_template.embed_color, |  | ||||||
|         reminder_template.embed_description, |  | ||||||
|         reminder_template.embed_footer, |  | ||||||
|         reminder_template.embed_footer_url, |  | ||||||
|         reminder_template.embed_image_url, |  | ||||||
|         reminder_template.embed_thumbnail_url, |  | ||||||
|         reminder_template.embed_title, |  | ||||||
|         reminder_template.embed_fields, |  | ||||||
|         reminder_template.tts, |  | ||||||
|         reminder_template.username, |  | ||||||
|     ) |  | ||||||
|     .fetch_all(pool.inner()) |  | ||||||
|     .await |  | ||||||
|     { |  | ||||||
|         Ok(_) => { |  | ||||||
|             Ok(json!({})) |  | ||||||
|         } |  | ||||||
|         Err(e) => { |  | ||||||
|             warn!("Could not create template for {}: {:?}", id, e); |  | ||||||
|  |  | ||||||
|             json_err!("Could not create template") |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| #[delete("/api/guild/<id>/templates", data = "<delete_reminder_template>")] |  | ||||||
| pub async fn delete_reminder_template( |  | ||||||
|     id: u64, |  | ||||||
|     delete_reminder_template: Json<DeleteReminderTemplate>, |  | ||||||
|     cookies: &CookieJar<'_>, |  | ||||||
|     ctx: &State<Context>, |  | ||||||
|     pool: &State<Pool<MySql>>, |  | ||||||
| ) -> JsonResult { |  | ||||||
|     check_authorization!(cookies, ctx.inner(), id); |  | ||||||
|  |  | ||||||
|     match sqlx::query!( |  | ||||||
|         "DELETE FROM reminder_template WHERE guild_id = (SELECT id FROM guilds WHERE guild = ?) AND id = ?", |  | ||||||
|         id, delete_reminder_template.id |  | ||||||
|     ) |  | ||||||
|     .fetch_all(pool.inner()) |  | ||||||
|     .await |  | ||||||
|     { |  | ||||||
|         Ok(_) => { |  | ||||||
|             Ok(json!({})) |  | ||||||
|         } |  | ||||||
|         Err(e) => { |  | ||||||
|             warn!("Could not delete template from {}: {:?}", id, e); |  | ||||||
|  |  | ||||||
|             json_err!("Could not delete template") |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| #[post("/api/guild/<id>/reminders", data = "<reminder>")] |  | ||||||
| pub async fn create_guild_reminder( |  | ||||||
|     id: u64, |  | ||||||
|     reminder: Json<ReminderCreate>, |  | ||||||
|     cookies: &CookieJar<'_>, |  | ||||||
|     serenity_context: &State<Context>, |  | ||||||
|     pool: &State<Pool<MySql>>, |  | ||||||
| ) -> JsonResult { |  | ||||||
|     check_authorization!(cookies, serenity_context.inner(), id); |  | ||||||
|  |  | ||||||
|     let user_id = |  | ||||||
|         cookies.get_private("userid").map(|c| c.value().parse::<u64>().ok()).flatten().unwrap(); |  | ||||||
|  |  | ||||||
|     create_reminder( |  | ||||||
|         serenity_context.inner(), |  | ||||||
|         pool.inner(), |  | ||||||
|         GuildId(id), |  | ||||||
|         UserId(user_id), |  | ||||||
|         reminder.into_inner(), |  | ||||||
|     ) |  | ||||||
|     .await |  | ||||||
| } |  | ||||||
|  |  | ||||||
| #[get("/api/guild/<id>/reminders?<status>")] |  | ||||||
| pub async fn get_reminders( |  | ||||||
|     id: u64, |  | ||||||
|     cookies: &CookieJar<'_>, |  | ||||||
|     serenity_context: &State<Context>, |  | ||||||
|     pool: &State<Pool<MySql>>, |  | ||||||
|     status: Option<String>, |  | ||||||
| ) -> JsonResult { |  | ||||||
|     check_authorization!(cookies, serenity_context.inner(), id); |  | ||||||
|  |  | ||||||
|     let status = status.unwrap_or("pending".to_string()); |  | ||||||
|  |  | ||||||
|     sqlx::query_as_unchecked!( |  | ||||||
|         Reminder, |  | ||||||
|         " |  | ||||||
|         SELECT |  | ||||||
|          reminders.attachment, |  | ||||||
|          reminders.attachment_name, |  | ||||||
|          reminders.avatar, |  | ||||||
|          channels.channel, |  | ||||||
|          reminders.content, |  | ||||||
|          reminders.embed_author, |  | ||||||
|          reminders.embed_author_url, |  | ||||||
|          reminders.embed_color, |  | ||||||
|          reminders.embed_description, |  | ||||||
|          reminders.embed_footer, |  | ||||||
|          reminders.embed_footer_url, |  | ||||||
|          reminders.embed_image_url, |  | ||||||
|          reminders.embed_thumbnail_url, |  | ||||||
|          reminders.embed_title, |  | ||||||
|          IFNULL(reminders.embed_fields, '[]') AS embed_fields, |  | ||||||
|          reminders.enabled, |  | ||||||
|          reminders.expires, |  | ||||||
|          reminders.interval_seconds, |  | ||||||
|          reminders.interval_days, |  | ||||||
|          reminders.interval_months, |  | ||||||
|          reminders.name, |  | ||||||
|          reminders.restartable, |  | ||||||
|          reminders.tts, |  | ||||||
|          reminders.uid, |  | ||||||
|          reminders.username, |  | ||||||
|          reminders.utc_time, |  | ||||||
|          reminders.status, |  | ||||||
|          reminders.status_change_time, |  | ||||||
|          reminders.status_message |  | ||||||
|         FROM reminders |  | ||||||
|         LEFT JOIN channels ON channels.id = reminders.channel_id |  | ||||||
|         WHERE FIND_IN_SET(`status`, ?) AND reminders.guild_id = (SELECT id FROM guilds WHERE guild = ?)", |  | ||||||
|         status, |  | ||||||
|         id |  | ||||||
|     ) |  | ||||||
|     .fetch_all(pool.inner()) |  | ||||||
|     .await |  | ||||||
|     .map(|r| Ok(json!(r))) |  | ||||||
|     .unwrap_or_else(|e| { |  | ||||||
|         warn!("Failed to complete SQL query: {:?}", e); |  | ||||||
|  |  | ||||||
|         json_err!("Could not load reminders") |  | ||||||
|     }) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| #[patch("/api/guild/<id>/reminders", data = "<reminder>")] |  | ||||||
| pub async fn edit_reminder( |  | ||||||
|     id: u64, |  | ||||||
|     reminder: Json<PatchReminder>, |  | ||||||
|     serenity_context: &State<Context>, |  | ||||||
|     pool: &State<Pool<MySql>>, |  | ||||||
|     cookies: &CookieJar<'_>, |  | ||||||
| ) -> JsonResult { |  | ||||||
|     check_authorization!(cookies, serenity_context.inner(), id); |  | ||||||
|  |  | ||||||
|     let mut error = vec![]; |  | ||||||
|  |  | ||||||
|     let user_id = |  | ||||||
|         cookies.get_private("userid").map(|c| c.value().parse::<u64>().ok()).flatten().unwrap(); |  | ||||||
|  |  | ||||||
|     if reminder.message_ok() { |  | ||||||
|         update_field!(pool.inner(), error, reminder.[ |  | ||||||
|             content, |  | ||||||
|             embed_author, |  | ||||||
|             embed_description, |  | ||||||
|             embed_footer, |  | ||||||
|             embed_title, |  | ||||||
|             embed_fields, |  | ||||||
|             username |  | ||||||
|         ]); |  | ||||||
|     } else { |  | ||||||
|         error.push("Message exceeds limits.".to_string()); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     update_field!(pool.inner(), error, reminder.[ |  | ||||||
|         attachment, |  | ||||||
|         attachment_name, |  | ||||||
|         avatar, |  | ||||||
|         embed_author_url, |  | ||||||
|         embed_color, |  | ||||||
|         embed_footer_url, |  | ||||||
|         embed_image_url, |  | ||||||
|         embed_thumbnail_url, |  | ||||||
|         enabled, |  | ||||||
|         expires, |  | ||||||
|         name, |  | ||||||
|         restartable, |  | ||||||
|         tts, |  | ||||||
|         utc_time |  | ||||||
|     ]); |  | ||||||
|  |  | ||||||
|     if reminder.interval_days.flatten().is_some() |  | ||||||
|         || reminder.interval_months.flatten().is_some() |  | ||||||
|         || reminder.interval_seconds.flatten().is_some() |  | ||||||
|     { |  | ||||||
|         if check_guild_subscription(&serenity_context.inner(), id).await |  | ||||||
|             || check_subscription(&serenity_context.inner(), user_id).await |  | ||||||
|         { |  | ||||||
|             let new_interval_length = match reminder.interval_days { |  | ||||||
|                 Some(interval) => interval.unwrap_or(0), |  | ||||||
|                 None => sqlx::query!( |  | ||||||
|                     "SELECT interval_days AS days FROM reminders WHERE uid = ?", |  | ||||||
|                     reminder.uid |  | ||||||
|                 ) |  | ||||||
|                 .fetch_one(pool.inner()) |  | ||||||
|                 .await |  | ||||||
|                 .map_err(|e| { |  | ||||||
|                     warn!("Error updating reminder interval: {:?}", e); |  | ||||||
|                     json!({ "reminder": Option::<Reminder>::None, "errors": vec!["Unknown error"] }) |  | ||||||
|                 })? |  | ||||||
|                 .days |  | ||||||
|                 .unwrap_or(0), |  | ||||||
|             } * 86400 + match reminder.interval_months { |  | ||||||
|                 Some(interval) => interval.unwrap_or(0), |  | ||||||
|                 None => sqlx::query!( |  | ||||||
|                     "SELECT interval_months AS months FROM reminders WHERE uid = ?", |  | ||||||
|                     reminder.uid |  | ||||||
|                 ) |  | ||||||
|                 .fetch_one(pool.inner()) |  | ||||||
|                 .await |  | ||||||
|                 .map_err(|e| { |  | ||||||
|                     warn!("Error updating reminder interval: {:?}", e); |  | ||||||
|                     json!({ "reminder": Option::<Reminder>::None, "errors": vec!["Unknown error"] }) |  | ||||||
|                 })? |  | ||||||
|                 .months |  | ||||||
|                 .unwrap_or(0), |  | ||||||
|             } * 2592000 + match reminder.interval_seconds { |  | ||||||
|                 Some(interval) => interval.unwrap_or(0), |  | ||||||
|                 None => sqlx::query!( |  | ||||||
|                     "SELECT interval_seconds AS seconds FROM reminders WHERE uid = ?", |  | ||||||
|                     reminder.uid |  | ||||||
|                 ) |  | ||||||
|                 .fetch_one(pool.inner()) |  | ||||||
|                 .await |  | ||||||
|                 .map_err(|e| { |  | ||||||
|                     warn!("Error updating reminder interval: {:?}", e); |  | ||||||
|                     json!({ "reminder": Option::<Reminder>::None, "errors": vec!["Unknown error"] }) |  | ||||||
|                 })? |  | ||||||
|                 .seconds |  | ||||||
|                 .unwrap_or(0), |  | ||||||
|             }; |  | ||||||
|  |  | ||||||
|             if new_interval_length < *MIN_INTERVAL { |  | ||||||
|                 error.push(String::from("New interval is too short.")); |  | ||||||
|             } else { |  | ||||||
|                 update_field!(pool.inner(), error, reminder.[ |  | ||||||
|                     interval_days, |  | ||||||
|                     interval_months, |  | ||||||
|                     interval_seconds |  | ||||||
|                 ]); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     if reminder.channel > 0 { |  | ||||||
|         let channel = ChannelId(reminder.channel).to_channel_cached(&serenity_context.inner()); |  | ||||||
|         match channel { |  | ||||||
|             Some(channel) => { |  | ||||||
|                 let channel_matches_guild = channel.guild().map_or(false, |c| c.guild_id.0 == id); |  | ||||||
|  |  | ||||||
|                 if !channel_matches_guild { |  | ||||||
|                     warn!( |  | ||||||
|                         "Error in `edit_reminder`: channel {:?} not found for guild {}", |  | ||||||
|                         reminder.channel, id |  | ||||||
|                     ); |  | ||||||
|  |  | ||||||
|                     return Err(json!({"error": "Channel not found"})); |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 let channel = create_database_channel( |  | ||||||
|                     serenity_context.inner(), |  | ||||||
|                     ChannelId(reminder.channel), |  | ||||||
|                     pool.inner(), |  | ||||||
|                 ) |  | ||||||
|                 .await; |  | ||||||
|  |  | ||||||
|                 if let Err(e) = channel { |  | ||||||
|                     warn!("`create_database_channel` returned an error code: {:?}", e); |  | ||||||
|  |  | ||||||
|                     return Err( |  | ||||||
|                         json!({"error": "Failed to configure channel for reminders. Please check the bot permissions"}), |  | ||||||
|                     ); |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 let channel = channel.unwrap(); |  | ||||||
|  |  | ||||||
|                 match sqlx::query!( |  | ||||||
|                     "UPDATE reminders SET channel_id = ? WHERE uid = ?", |  | ||||||
|                     channel, |  | ||||||
|                     reminder.uid |  | ||||||
|                 ) |  | ||||||
|                 .execute(pool.inner()) |  | ||||||
|                 .await |  | ||||||
|                 { |  | ||||||
|                     Ok(_) => {} |  | ||||||
|                     Err(e) => { |  | ||||||
|                         warn!("Error setting channel: {:?}", e); |  | ||||||
|  |  | ||||||
|                         error.push("Couldn't set channel".to_string()) |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             None => { |  | ||||||
|                 warn!( |  | ||||||
|                     "Error in `edit_reminder`: channel {:?} not found for guild {}", |  | ||||||
|                     reminder.channel, id |  | ||||||
|                 ); |  | ||||||
|  |  | ||||||
|                 return Err(json!({"error": "Channel not found"})); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     match sqlx::query_as_unchecked!( |  | ||||||
|         Reminder, |  | ||||||
|         " |  | ||||||
|         SELECT reminders.attachment, |  | ||||||
|          reminders.attachment_name, |  | ||||||
|          reminders.avatar, |  | ||||||
|          channels.channel, |  | ||||||
|          reminders.content, |  | ||||||
|          reminders.embed_author, |  | ||||||
|          reminders.embed_author_url, |  | ||||||
|          reminders.embed_color, |  | ||||||
|          reminders.embed_description, |  | ||||||
|          reminders.embed_footer, |  | ||||||
|          reminders.embed_footer_url, |  | ||||||
|          reminders.embed_image_url, |  | ||||||
|          reminders.embed_thumbnail_url, |  | ||||||
|          reminders.embed_title, |  | ||||||
|          reminders.embed_fields, |  | ||||||
|          reminders.enabled, |  | ||||||
|          reminders.expires, |  | ||||||
|          reminders.interval_seconds, |  | ||||||
|          reminders.interval_days, |  | ||||||
|          reminders.interval_months, |  | ||||||
|          reminders.name, |  | ||||||
|          reminders.restartable, |  | ||||||
|          reminders.tts, |  | ||||||
|          reminders.uid, |  | ||||||
|          reminders.username, |  | ||||||
|          reminders.utc_time, |  | ||||||
|          reminders.status, |  | ||||||
|          reminders.status_change_time, |  | ||||||
|          reminders.status_message |  | ||||||
|         FROM reminders |  | ||||||
|         LEFT JOIN channels ON channels.id = reminders.channel_id |  | ||||||
|         WHERE uid = ?", |  | ||||||
|         reminder.uid |  | ||||||
|     ) |  | ||||||
|     .fetch_one(pool.inner()) |  | ||||||
|     .await |  | ||||||
|     { |  | ||||||
|         Ok(reminder) => Ok(json!({"reminder": reminder, "errors": error})), |  | ||||||
|  |  | ||||||
|         Err(e) => { |  | ||||||
|             warn!("Error exiting `edit_reminder': {:?}", e); |  | ||||||
|  |  | ||||||
|             Err(json!({"reminder": Option::<Reminder>::None, "errors": vec!["Unknown error"]})) |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| #[delete("/api/guild/<_>/reminders", data = "<reminder>")] |  | ||||||
| pub async fn delete_reminder( |  | ||||||
|     reminder: Json<DeleteReminder>, |  | ||||||
|     pool: &State<Pool<MySql>>, |  | ||||||
| ) -> JsonResult { |  | ||||||
|     match sqlx::query!( |  | ||||||
|         "UPDATE reminders SET `status` = 'deleted', `status_change_time` = NOW() WHERE uid = ?", |  | ||||||
|         reminder.uid |  | ||||||
|     ) |  | ||||||
|     .execute(pool.inner()) |  | ||||||
|     .await |  | ||||||
|     { |  | ||||||
|         Ok(_) => Ok(json!({})), |  | ||||||
|  |  | ||||||
|         Err(e) => { |  | ||||||
|             warn!("Error in `delete_reminder`: {:?}", e); |  | ||||||
|  |  | ||||||
|             Err(json!({"error": "Could not delete reminder"})) |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,16 +1,20 @@ | |||||||
| use std::collections::HashMap; | use std::path::Path; | ||||||
|  |  | ||||||
| use chrono::{naive::NaiveDateTime, Utc}; | use chrono::{naive::NaiveDateTime, Utc}; | ||||||
| use rand::{rngs::OsRng, seq::IteratorRandom}; | use rand::{rngs::OsRng, seq::IteratorRandom}; | ||||||
| use rocket::{http::CookieJar, response::Redirect, serde::json::json}; | use rocket::{ | ||||||
| use rocket_dyn_templates::Template; |     fs::{relative, NamedFile}, | ||||||
|  |     http::CookieJar, | ||||||
|  |     response::Redirect, | ||||||
|  |     serde::json::json, | ||||||
|  | }; | ||||||
| use serde::{Deserialize, Deserializer, Serialize}; | use serde::{Deserialize, Deserializer, Serialize}; | ||||||
| use serenity::{ | use serenity::{ | ||||||
|     client::Context, |     client::Context, | ||||||
|     http::Http, |     http::Http, | ||||||
|     model::id::{ChannelId, GuildId, UserId}, |     model::id::{ChannelId, GuildId, UserId}, | ||||||
| }; | }; | ||||||
| use sqlx::{types::Json, Executor}; | use sqlx::types::Json; | ||||||
|  |  | ||||||
| use crate::{ | use crate::{ | ||||||
|     check_guild_subscription, check_subscription, |     check_guild_subscription, check_subscription, | ||||||
| @@ -20,13 +24,13 @@ use crate::{ | |||||||
|         MAX_EMBED_FIELD_VALUE_LENGTH, MAX_EMBED_FOOTER_LENGTH, MAX_EMBED_TITLE_LENGTH, |         MAX_EMBED_FIELD_VALUE_LENGTH, MAX_EMBED_FOOTER_LENGTH, MAX_EMBED_TITLE_LENGTH, | ||||||
|         MAX_NAME_LENGTH, MAX_URL_LENGTH, MAX_USERNAME_LENGTH, MIN_INTERVAL, |         MAX_NAME_LENGTH, MAX_URL_LENGTH, MAX_USERNAME_LENGTH, MIN_INTERVAL, | ||||||
|     }, |     }, | ||||||
|  |     guards::transaction::Transaction, | ||||||
|     routes::JsonResult, |     routes::JsonResult, | ||||||
|     Database, Error, |     Error, | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | pub mod api; | ||||||
| pub mod export; | pub mod export; | ||||||
| pub mod guild; |  | ||||||
| pub mod user; |  | ||||||
|  |  | ||||||
| type Unset<T> = Option<T>; | type Unset<T> = Option<T>; | ||||||
|  |  | ||||||
| @@ -80,6 +84,9 @@ pub struct ReminderTemplate { | |||||||
|     embed_thumbnail_url: Option<String>, |     embed_thumbnail_url: Option<String>, | ||||||
|     embed_title: String, |     embed_title: String, | ||||||
|     embed_fields: Option<Json<Vec<EmbedField>>>, |     embed_fields: Option<Json<Vec<EmbedField>>>, | ||||||
|  |     interval_seconds: Option<u32>, | ||||||
|  |     interval_days: Option<u32>, | ||||||
|  |     interval_months: Option<u32>, | ||||||
|     tts: bool, |     tts: bool, | ||||||
|     username: Option<String>, |     username: Option<String>, | ||||||
| } | } | ||||||
| @@ -102,6 +109,9 @@ pub struct ReminderTemplateCsv { | |||||||
|     embed_thumbnail_url: Option<String>, |     embed_thumbnail_url: Option<String>, | ||||||
|     embed_title: String, |     embed_title: String, | ||||||
|     embed_fields: Option<String>, |     embed_fields: Option<String>, | ||||||
|  |     interval_seconds: Option<u32>, | ||||||
|  |     interval_days: Option<u32>, | ||||||
|  |     interval_months: Option<u32>, | ||||||
|     tts: bool, |     tts: bool, | ||||||
|     username: Option<String>, |     username: Option<String>, | ||||||
| } | } | ||||||
| @@ -118,8 +128,8 @@ pub struct EmbedField { | |||||||
|     inline: bool, |     inline: bool, | ||||||
| } | } | ||||||
|  |  | ||||||
| #[derive(Deserialize)] | #[derive(Serialize, Deserialize)] | ||||||
| pub struct ReminderCreate { | pub struct Reminder { | ||||||
|     #[serde(with = "base64s")] |     #[serde(with = "base64s")] | ||||||
|     attachment: Option<Vec<u8>>, |     attachment: Option<Vec<u8>>, | ||||||
|     attachment_name: Option<String>, |     attachment_name: Option<String>, | ||||||
| @@ -146,45 +156,10 @@ pub struct ReminderCreate { | |||||||
|     name: String, |     name: String, | ||||||
|     restartable: bool, |     restartable: bool, | ||||||
|     tts: bool, |     tts: bool, | ||||||
|     username: Option<String>, |  | ||||||
|     utc_time: NaiveDateTime, |  | ||||||
| } |  | ||||||
|  |  | ||||||
| #[derive(Serialize, Deserialize)] |  | ||||||
| pub struct Reminder { |  | ||||||
|     #[serde(with = "base64s")] |  | ||||||
|     attachment: Option<Vec<u8>>, |  | ||||||
|     attachment_name: Option<String>, |  | ||||||
|     avatar: Option<String>, |  | ||||||
|     #[serde(with = "string_opt")] |  | ||||||
|     channel: Option<u64>, |  | ||||||
|     content: String, |  | ||||||
|     embed_author: String, |  | ||||||
|     embed_author_url: Option<String>, |  | ||||||
|     embed_color: u32, |  | ||||||
|     embed_description: String, |  | ||||||
|     embed_footer: String, |  | ||||||
|     embed_footer_url: Option<String>, |  | ||||||
|     embed_image_url: Option<String>, |  | ||||||
|     embed_thumbnail_url: Option<String>, |  | ||||||
|     embed_title: String, |  | ||||||
|     embed_fields: Option<Json<Vec<EmbedField>>>, |  | ||||||
|     enabled: bool, |  | ||||||
|     expires: Option<NaiveDateTime>, |  | ||||||
|     interval_seconds: Option<u32>, |  | ||||||
|     interval_days: Option<u32>, |  | ||||||
|     interval_months: Option<u32>, |  | ||||||
|     #[serde(default = "name_default")] |  | ||||||
|     name: String, |  | ||||||
|     restartable: bool, |  | ||||||
|     tts: bool, |  | ||||||
|     #[serde(default)] |     #[serde(default)] | ||||||
|     uid: String, |     uid: String, | ||||||
|     username: Option<String>, |     username: Option<String>, | ||||||
|     utc_time: NaiveDateTime, |     utc_time: NaiveDateTime, | ||||||
|     status: String, |  | ||||||
|     status_message: Option<String>, |  | ||||||
|     status_change_time: Option<NaiveDateTime>, |  | ||||||
| } | } | ||||||
|  |  | ||||||
| #[derive(Serialize, Deserialize)] | #[derive(Serialize, Deserialize)] | ||||||
| @@ -323,7 +298,15 @@ pub fn generate_uid() -> String { | |||||||
| mod string { | mod string { | ||||||
|     use std::{fmt::Display, str::FromStr}; |     use std::{fmt::Display, str::FromStr}; | ||||||
|  |  | ||||||
|     use serde::{de, Deserialize, Deserializer}; |     use serde::{de, Deserialize, Deserializer, Serializer}; | ||||||
|  |  | ||||||
|  |     pub fn serialize<T, S>(value: &T, serializer: S) -> Result<S::Ok, S::Error> | ||||||
|  |     where | ||||||
|  |         T: Display, | ||||||
|  |         S: Serializer, | ||||||
|  |     { | ||||||
|  |         serializer.collect_str(value) | ||||||
|  |     } | ||||||
|  |  | ||||||
|     pub fn deserialize<'de, T, D>(deserializer: D) -> Result<T, D::Error> |     pub fn deserialize<'de, T, D>(deserializer: D) -> Result<T, D::Error> | ||||||
|     where |     where | ||||||
| @@ -335,34 +318,6 @@ mod string { | |||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| mod string_opt { |  | ||||||
|     use std::{fmt::Display, str::FromStr}; |  | ||||||
|  |  | ||||||
|     use serde::{de, Deserialize, Deserializer, Serializer}; |  | ||||||
|  |  | ||||||
|     pub fn serialize<T, S>(value: &Option<T>, serializer: S) -> Result<S::Ok, S::Error> |  | ||||||
|     where |  | ||||||
|         T: Display, |  | ||||||
|         S: Serializer, |  | ||||||
|     { |  | ||||||
|         match value { |  | ||||||
|             Some(value) => serializer.collect_str(value), |  | ||||||
|             None => serializer.serialize_none(), |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     pub fn deserialize<'de, T, D>(deserializer: D) -> Result<Option<T>, D::Error> |  | ||||||
|     where |  | ||||||
|         T: FromStr, |  | ||||||
|         T::Err: Display, |  | ||||||
|         D: Deserializer<'de>, |  | ||||||
|     { |  | ||||||
|         Option::deserialize(deserializer)? |  | ||||||
|             .map(|d: String| d.parse().map_err(de::Error::custom)) |  | ||||||
|             .transpose() |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| mod base64s { | mod base64s { | ||||||
|     use serde::{de, Deserialize, Deserializer, Serializer}; |     use serde::{de, Deserialize, Deserializer, Serializer}; | ||||||
|  |  | ||||||
| @@ -402,21 +357,21 @@ pub struct TodoCsv { | |||||||
|     channel_id: Option<String>, |     channel_id: Option<String>, | ||||||
| } | } | ||||||
|  |  | ||||||
| pub async fn create_reminder( | pub(crate) async fn create_reminder( | ||||||
|     ctx: &Context, |     ctx: &Context, | ||||||
|     pool: impl sqlx::Executor<'_, Database = Database> + Copy, |     transaction: &mut Transaction<'_>, | ||||||
|     guild_id: GuildId, |     guild_id: GuildId, | ||||||
|     user_id: UserId, |     user_id: UserId, | ||||||
|     reminder: ReminderCreate, |     reminder: Reminder, | ||||||
| ) -> JsonResult { | ) -> JsonResult { | ||||||
|     // check guild in db |     // check guild in db | ||||||
|     match sqlx::query!("SELECT 1 as A FROM guilds WHERE guild = ?", guild_id.0) |     match sqlx::query!("SELECT 1 as A FROM guilds WHERE guild = ?", guild_id.0) | ||||||
|         .fetch_one(pool) |         .fetch_one(transaction.executor()) | ||||||
|         .await |         .await | ||||||
|     { |     { | ||||||
|         Err(sqlx::Error::RowNotFound) => { |         Err(sqlx::Error::RowNotFound) => { | ||||||
|             if sqlx::query!("INSERT INTO guilds (guild) VALUES (?)", guild_id.0) |             if sqlx::query!("INSERT INTO guilds (guild) VALUES (?)", guild_id.0) | ||||||
|                 .execute(pool) |                 .execute(transaction.executor()) | ||||||
|                 .await |                 .await | ||||||
|                 .is_err() |                 .is_err() | ||||||
|             { |             { | ||||||
| @@ -435,14 +390,14 @@ pub async fn create_reminder( | |||||||
|  |  | ||||||
|     if !channel_matches_guild || !channel_exists { |     if !channel_matches_guild || !channel_exists { | ||||||
|         warn!( |         warn!( | ||||||
|             "Error in `create_reminder`: channel {:?} not found for guild {} (channel exists: {})", |             "Error in `create_reminder`: channel {} not found for guild {} (channel exists: {})", | ||||||
|             reminder.channel, guild_id, channel_exists |             reminder.channel, guild_id, channel_exists | ||||||
|         ); |         ); | ||||||
|  |  | ||||||
|         return Err(json!({"error": "Channel not found"})); |         return Err(json!({"error": "Channel not found"})); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     let channel = create_database_channel(&ctx, ChannelId(reminder.channel), pool).await; |     let channel = create_database_channel(&ctx, ChannelId(reminder.channel), transaction).await; | ||||||
|  |  | ||||||
|     if let Err(e) = channel { |     if let Err(e) = channel { | ||||||
|         warn!("`create_database_channel` returned an error code: {:?}", e); |         warn!("`create_database_channel` returned an error code: {:?}", e); | ||||||
| @@ -516,8 +471,6 @@ pub async fn create_reminder( | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     // base64 decode error dropped here |  | ||||||
|     let attachment_data = reminder.attachment.as_ref().map(|s| base64::decode(s).ok()).flatten(); |  | ||||||
|     let name = if reminder.name.is_empty() { name_default() } else { reminder.name.clone() }; |     let name = if reminder.name.is_empty() { name_default() } else { reminder.name.clone() }; | ||||||
|     let username = if reminder.username.as_ref().map(|s| s.is_empty()).unwrap_or(true) { |     let username = if reminder.username.as_ref().map(|s| s.is_empty()).unwrap_or(true) { | ||||||
|         None |         None | ||||||
| @@ -529,13 +482,11 @@ pub async fn create_reminder( | |||||||
|  |  | ||||||
|     // write to db |     // write to db | ||||||
|     match sqlx::query!( |     match sqlx::query!( | ||||||
|         " |         "INSERT INTO reminders ( | ||||||
|         INSERT INTO reminders ( |  | ||||||
|          uid, |          uid, | ||||||
|          attachment, |          attachment, | ||||||
|          attachment_name, |          attachment_name, | ||||||
|          channel_id, |          channel_id, | ||||||
|          guild_id, |  | ||||||
|          avatar, |          avatar, | ||||||
|          content, |          content, | ||||||
|          embed_author, |          embed_author, | ||||||
| @@ -558,14 +509,11 @@ pub async fn create_reminder( | |||||||
|          tts, |          tts, | ||||||
|          username, |          username, | ||||||
|          `utc_time` |          `utc_time` | ||||||
|         ) VALUES (?, ?, ?, ?, |         ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", | ||||||
|         (SELECT id FROM guilds WHERE guild = ?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, |  | ||||||
|         ?, ?, ?, ?, ?)", |  | ||||||
|         new_uid, |         new_uid, | ||||||
|         attachment_data, |         reminder.attachment, | ||||||
|         reminder.attachment_name, |         reminder.attachment_name, | ||||||
|         channel, |         channel, | ||||||
|         guild_id.0, |  | ||||||
|         reminder.avatar, |         reminder.avatar, | ||||||
|         reminder.content, |         reminder.content, | ||||||
|         reminder.embed_author, |         reminder.embed_author, | ||||||
| @@ -589,7 +537,7 @@ pub async fn create_reminder( | |||||||
|         username, |         username, | ||||||
|         reminder.utc_time, |         reminder.utc_time, | ||||||
|     ) |     ) | ||||||
|     .execute(pool) |     .execute(transaction.executor()) | ||||||
|     .await |     .await | ||||||
|     { |     { | ||||||
|         Ok(_) => sqlx::query_as_unchecked!( |         Ok(_) => sqlx::query_as_unchecked!( | ||||||
| @@ -620,16 +568,13 @@ pub async fn create_reminder( | |||||||
|              reminders.tts, |              reminders.tts, | ||||||
|              reminders.uid, |              reminders.uid, | ||||||
|              reminders.username, |              reminders.username, | ||||||
|              reminders.utc_time, |              reminders.utc_time | ||||||
|              reminders.status, |  | ||||||
|              reminders.status_change_time, |  | ||||||
|              reminders.status_message |  | ||||||
|             FROM reminders |             FROM reminders | ||||||
|             LEFT JOIN channels ON channels.id = reminders.channel_id |             LEFT JOIN channels ON channels.id = reminders.channel_id | ||||||
|             WHERE uid = ?", |             WHERE uid = ?", | ||||||
|             new_uid |             new_uid | ||||||
|         ) |         ) | ||||||
|         .fetch_one(pool) |         .fetch_one(transaction.executor()) | ||||||
|         .await |         .await | ||||||
|         .map(|r| Ok(json!(r))) |         .map(|r| Ok(json!(r))) | ||||||
|         .unwrap_or_else(|e| { |         .unwrap_or_else(|e| { | ||||||
| @@ -649,11 +594,11 @@ pub async fn create_reminder( | |||||||
| async fn create_database_channel( | async fn create_database_channel( | ||||||
|     ctx: impl AsRef<Http>, |     ctx: impl AsRef<Http>, | ||||||
|     channel: ChannelId, |     channel: ChannelId, | ||||||
|     pool: impl Executor<'_, Database = Database> + Copy, |     transaction: &mut Transaction<'_>, | ||||||
| ) -> Result<u32, crate::Error> { | ) -> Result<u32, crate::Error> { | ||||||
|     let row = |     let row = | ||||||
|         sqlx::query!("SELECT webhook_token, webhook_id FROM channels WHERE channel = ?", channel.0) |         sqlx::query!("SELECT webhook_token, webhook_id FROM channels WHERE channel = ?", channel.0) | ||||||
|             .fetch_one(pool) |             .fetch_one(transaction.executor()) | ||||||
|             .await; |             .await; | ||||||
|  |  | ||||||
|     match row { |     match row { | ||||||
| @@ -670,7 +615,7 @@ async fn create_database_channel( | |||||||
|                     webhook.token, |                     webhook.token, | ||||||
|                     channel.0 |                     channel.0 | ||||||
|                 ) |                 ) | ||||||
|                 .execute(pool) |                 .execute(transaction.executor()) | ||||||
|                 .await |                 .await | ||||||
|                 .map_err(|e| Error::SQLx(e))?; |                 .map_err(|e| Error::SQLx(e))?; | ||||||
|             } |             } | ||||||
| @@ -696,7 +641,7 @@ async fn create_database_channel( | |||||||
|                 webhook.token, |                 webhook.token, | ||||||
|                 channel.0 |                 channel.0 | ||||||
|             ) |             ) | ||||||
|             .execute(pool) |             .execute(transaction.executor()) | ||||||
|             .await |             .await | ||||||
|             .map_err(|e| Error::SQLx(e))?; |             .map_err(|e| Error::SQLx(e))?; | ||||||
|  |  | ||||||
| @@ -707,7 +652,7 @@ async fn create_database_channel( | |||||||
|     }?; |     }?; | ||||||
|  |  | ||||||
|     let row = sqlx::query!("SELECT id FROM channels WHERE channel = ?", channel.0) |     let row = sqlx::query!("SELECT id FROM channels WHERE channel = ?", channel.0) | ||||||
|         .fetch_one(pool) |         .fetch_one(transaction.executor()) | ||||||
|         .await |         .await | ||||||
|         .map_err(|e| Error::SQLx(e))?; |         .map_err(|e| Error::SQLx(e))?; | ||||||
|  |  | ||||||
| @@ -715,30 +660,26 @@ async fn create_database_channel( | |||||||
| } | } | ||||||
|  |  | ||||||
| #[get("/")] | #[get("/")] | ||||||
| pub async fn dashboard_home(cookies: &CookieJar<'_>) -> Result<Template, Redirect> { | pub async fn dashboard_home(cookies: &CookieJar<'_>) -> Result<NamedFile, Redirect> { | ||||||
|     if cookies.get_private("userid").is_some() { |     if cookies.get_private("userid").is_some() { | ||||||
|         let map: HashMap<&str, String> = HashMap::new(); |         NamedFile::open(Path::new(relative!("static/index.html"))).await.map_err(|e| { | ||||||
|         Ok(Template::render("dashboard", &map)) |             warn!("Couldn't render dashboard: {:?}", e); | ||||||
|  |  | ||||||
|  |             Redirect::to("/login/discord") | ||||||
|  |         }) | ||||||
|     } else { |     } else { | ||||||
|         Err(Redirect::to("/login/discord")) |         Err(Redirect::to("/login/discord")) | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| #[get("/<_>")] | #[get("/<_..>")] | ||||||
| pub async fn dashboard_1(cookies: &CookieJar<'_>) -> Result<Template, Redirect> { | pub async fn dashboard(cookies: &CookieJar<'_>) -> Result<NamedFile, Redirect> { | ||||||
|     if cookies.get_private("userid").is_some() { |     if cookies.get_private("userid").is_some() { | ||||||
|         let map: HashMap<&str, String> = HashMap::new(); |         NamedFile::open(Path::new(relative!("static/index.html"))).await.map_err(|e| { | ||||||
|         Ok(Template::render("dashboard", &map)) |             warn!("Couldn't render dashboard: {:?}", e); | ||||||
|     } else { |  | ||||||
|         Err(Redirect::to("/login/discord")) |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| #[get("/<_>/<_>")] |             Redirect::to("/login/discord") | ||||||
| pub async fn dashboard_2(cookies: &CookieJar<'_>) -> Result<Template, Redirect> { |         }) | ||||||
|     if cookies.get_private("userid").is_some() { |  | ||||||
|         let map: HashMap<&str, String> = HashMap::new(); |  | ||||||
|         Ok(Template::render("dashboard", &map)) |  | ||||||
|     } else { |     } else { | ||||||
|         Err(Redirect::to("/login/discord")) |         Err(Redirect::to("/login/discord")) | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -1,172 +0,0 @@ | |||||||
| use std::env; |  | ||||||
|  |  | ||||||
| use chrono_tz::Tz; |  | ||||||
| use reqwest::Client; |  | ||||||
| use rocket::{ |  | ||||||
|     http::CookieJar, |  | ||||||
|     serde::json::{json, Json, Value as JsonValue}, |  | ||||||
|     State, |  | ||||||
| }; |  | ||||||
| use serde::{Deserialize, Serialize}; |  | ||||||
| use serenity::{ |  | ||||||
|     client::Context, |  | ||||||
|     model::{ |  | ||||||
|         id::{GuildId, RoleId}, |  | ||||||
|         permissions::Permissions, |  | ||||||
|     }, |  | ||||||
| }; |  | ||||||
| use sqlx::{MySql, Pool}; |  | ||||||
|  |  | ||||||
| use crate::consts::DISCORD_API; |  | ||||||
|  |  | ||||||
| #[derive(Serialize)] |  | ||||||
| struct UserInfo { |  | ||||||
|     name: String, |  | ||||||
|     patreon: bool, |  | ||||||
|     timezone: Option<String>, |  | ||||||
| } |  | ||||||
|  |  | ||||||
| #[derive(Deserialize)] |  | ||||||
| pub struct UpdateUser { |  | ||||||
|     timezone: String, |  | ||||||
| } |  | ||||||
|  |  | ||||||
| #[derive(Serialize)] |  | ||||||
| struct GuildInfo { |  | ||||||
|     id: String, |  | ||||||
|     name: String, |  | ||||||
| } |  | ||||||
|  |  | ||||||
| #[derive(Deserialize)] |  | ||||||
| pub struct PartialGuild { |  | ||||||
|     pub id: GuildId, |  | ||||||
|     pub icon: Option<String>, |  | ||||||
|     pub name: String, |  | ||||||
|     #[serde(default)] |  | ||||||
|     pub owner: bool, |  | ||||||
|     #[serde(rename = "permissions_new")] |  | ||||||
|     pub permissions: Option<String>, |  | ||||||
| } |  | ||||||
|  |  | ||||||
| #[get("/api/user")] |  | ||||||
| pub async fn get_user_info( |  | ||||||
|     cookies: &CookieJar<'_>, |  | ||||||
|     ctx: &State<Context>, |  | ||||||
|     pool: &State<Pool<MySql>>, |  | ||||||
| ) -> JsonValue { |  | ||||||
|     offline!(json!(UserInfo { name: "Discord".to_string(), patreon: true, timezone: None })); |  | ||||||
|  |  | ||||||
|     if let Some(user_id) = |  | ||||||
|         cookies.get_private("userid").map(|u| u.value().parse::<u64>().ok()).flatten() |  | ||||||
|     { |  | ||||||
|         let member_res = GuildId(env::var("PATREON_GUILD_ID").unwrap().parse().unwrap()) |  | ||||||
|             .member(&ctx.inner(), user_id) |  | ||||||
|             .await; |  | ||||||
|  |  | ||||||
|         let timezone = sqlx::query!( |  | ||||||
|             "SELECT IFNULL(timezone, 'UTC') AS timezone FROM users WHERE user = ?", |  | ||||||
|             user_id |  | ||||||
|         ) |  | ||||||
|         .fetch_one(pool.inner()) |  | ||||||
|         .await |  | ||||||
|         .map_or(None, |q| Some(q.timezone)); |  | ||||||
|  |  | ||||||
|         let user_info = UserInfo { |  | ||||||
|             name: cookies |  | ||||||
|                 .get_private("username") |  | ||||||
|                 .map_or("DiscordUser#0000".to_string(), |c| c.value().to_string()), |  | ||||||
|             patreon: member_res.map_or(false, |member| { |  | ||||||
|                 member |  | ||||||
|                     .roles |  | ||||||
|                     .contains(&RoleId(env::var("PATREON_ROLE_ID").unwrap().parse().unwrap())) |  | ||||||
|             }), |  | ||||||
|             timezone, |  | ||||||
|         }; |  | ||||||
|  |  | ||||||
|         json!(user_info) |  | ||||||
|     } else { |  | ||||||
|         json!({"error": "Not authorized"}) |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| #[patch("/api/user", data = "<user>")] |  | ||||||
| pub async fn update_user_info( |  | ||||||
|     cookies: &CookieJar<'_>, |  | ||||||
|     user: Json<UpdateUser>, |  | ||||||
|     pool: &State<Pool<MySql>>, |  | ||||||
| ) -> JsonValue { |  | ||||||
|     if let Some(user_id) = |  | ||||||
|         cookies.get_private("userid").map(|u| u.value().parse::<u64>().ok()).flatten() |  | ||||||
|     { |  | ||||||
|         if user.timezone.parse::<Tz>().is_ok() { |  | ||||||
|             let _ = sqlx::query!( |  | ||||||
|                 "UPDATE users SET timezone = ? WHERE user = ?", |  | ||||||
|                 user.timezone, |  | ||||||
|                 user_id, |  | ||||||
|             ) |  | ||||||
|             .execute(pool.inner()) |  | ||||||
|             .await; |  | ||||||
|  |  | ||||||
|             json!({}) |  | ||||||
|         } else { |  | ||||||
|             json!({"error": "Timezone not recognized"}) |  | ||||||
|         } |  | ||||||
|     } else { |  | ||||||
|         json!({"error": "Not authorized"}) |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| #[get("/api/user/guilds")] |  | ||||||
| pub async fn get_user_guilds(cookies: &CookieJar<'_>, reqwest_client: &State<Client>) -> JsonValue { |  | ||||||
|     offline!(json!(vec![GuildInfo { id: "1".to_string(), name: "Guild".to_string() }])); |  | ||||||
|  |  | ||||||
|     if let Some(access_token) = cookies.get_private("access_token") { |  | ||||||
|         let request_res = reqwest_client |  | ||||||
|             .get(format!("{}/users/@me/guilds", DISCORD_API)) |  | ||||||
|             .bearer_auth(access_token.value()) |  | ||||||
|             .send() |  | ||||||
|             .await; |  | ||||||
|  |  | ||||||
|         match request_res { |  | ||||||
|             Ok(response) => { |  | ||||||
|                 let guilds_res = response.json::<Vec<PartialGuild>>().await; |  | ||||||
|  |  | ||||||
|                 match guilds_res { |  | ||||||
|                     Ok(guilds) => { |  | ||||||
|                         let reduced_guilds = guilds |  | ||||||
|                             .iter() |  | ||||||
|                             .filter(|g| { |  | ||||||
|                                 g.owner |  | ||||||
|                                     || g.permissions.as_ref().map_or(false, |p| { |  | ||||||
|                                         let permissions = |  | ||||||
|                                             Permissions::from_bits_truncate(p.parse().unwrap()); |  | ||||||
|  |  | ||||||
|                                         permissions.manage_messages() |  | ||||||
|                                             || permissions.manage_guild() |  | ||||||
|                                             || permissions.administrator() |  | ||||||
|                                     }) |  | ||||||
|                             }) |  | ||||||
|                             .map(|g| GuildInfo { id: g.id.to_string(), name: g.name.to_string() }) |  | ||||||
|                             .collect::<Vec<GuildInfo>>(); |  | ||||||
|  |  | ||||||
|                         json!(reduced_guilds) |  | ||||||
|                     } |  | ||||||
|  |  | ||||||
|                     Err(e) => { |  | ||||||
|                         warn!("Error constructing user from request: {:?}", e); |  | ||||||
|  |  | ||||||
|                         json!({"error": "Could not get user details"}) |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             Err(e) => { |  | ||||||
|                 warn!("Error getting user guilds: {:?}", e); |  | ||||||
|  |  | ||||||
|                 json!({"error": "Could not reach Discord"}) |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } else { |  | ||||||
|         json!({"error": "Not authorized"}) |  | ||||||
|     } |  | ||||||
| } |  | ||||||
							
								
								
									
										18
									
								
								web/src/routes/metrics.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								web/src/routes/metrics.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | |||||||
|  | use prometheus; | ||||||
|  |  | ||||||
|  | use crate::metrics::REGISTRY; | ||||||
|  |  | ||||||
|  | #[get("/metrics")] | ||||||
|  | pub async fn metrics() -> String { | ||||||
|  |     let encoder = prometheus::TextEncoder::new(); | ||||||
|  |     let res_custom = encoder.encode_to_string(®ISTRY.gather()); | ||||||
|  |  | ||||||
|  |     match res_custom { | ||||||
|  |         Ok(s) => s, | ||||||
|  |         Err(e) => { | ||||||
|  |             warn!("Error encoding metrics: {:?}", e); | ||||||
|  |  | ||||||
|  |             String::new() | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -1,6 +1,7 @@ | |||||||
| pub mod admin; | pub mod admin; | ||||||
| pub mod dashboard; | pub mod dashboard; | ||||||
| pub mod login; | pub mod login; | ||||||
|  | pub mod metrics; | ||||||
| pub mod report; | pub mod report; | ||||||
|  |  | ||||||
| use std::collections::HashMap; | use std::collections::HashMap; | ||||||
|   | |||||||
| @@ -15,6 +15,10 @@ div.reminderContent.is-collapsed .column.settings { | |||||||
|     display: none; |     display: none; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | div.reminderContent.is-collapsed .reminder-settings { | ||||||
|  |     margin-bottom: 0; | ||||||
|  | } | ||||||
|  |  | ||||||
| div.reminderContent.is-collapsed .button-row { | div.reminderContent.is-collapsed .button-row { | ||||||
|     display: none; |     display: none; | ||||||
| } | } | ||||||
| @@ -214,17 +218,16 @@ div.inset-content { | |||||||
|     margin-right: 10%; |     margin-right: 10%; | ||||||
| } | } | ||||||
|  |  | ||||||
| div.flash-message { | div.flash-container { | ||||||
|     position: fixed; |     position: fixed; | ||||||
|  |     width: 100%; | ||||||
|  |     bottom: 0; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | div.flash-message { | ||||||
|     width: calc(100% - 32px); |     width: calc(100% - 32px); | ||||||
|     margin: 16px !important; |     margin: 16px !important; | ||||||
|     z-index: 99; |     z-index: 99; | ||||||
|     bottom: 0; |  | ||||||
|     display: none; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| div.flash-message.is-active { |  | ||||||
|     display: block; |  | ||||||
| } | } | ||||||
|  |  | ||||||
| body { | body { | ||||||
| @@ -304,11 +307,6 @@ div.dashboard-sidebar { | |||||||
|     padding-right: 0; |     padding-right: 0; | ||||||
| } | } | ||||||
|  |  | ||||||
| div.dashboard-sidebar:not(.mobile-sidebar) { |  | ||||||
|     display: flex; |  | ||||||
|     flex-direction: column; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| ul.guildList { | ul.guildList { | ||||||
|     flex-grow: 1; |     flex-grow: 1; | ||||||
|     flex-shrink: 1; |     flex-shrink: 1; | ||||||
| @@ -318,6 +316,9 @@ ul.guildList { | |||||||
| div.dashboard-sidebar:not(.mobile-sidebar) .aside-footer { | div.dashboard-sidebar:not(.mobile-sidebar) .aside-footer { | ||||||
|     flex-shrink: 0; |     flex-shrink: 0; | ||||||
|     flex-grow: 0; |     flex-grow: 0; | ||||||
|  |     position: fixed; | ||||||
|  |     bottom: 0; | ||||||
|  |     width: 226px; | ||||||
| } | } | ||||||
|  |  | ||||||
| div.dashboard-sidebar svg { | div.dashboard-sidebar svg { | ||||||
| @@ -631,6 +632,10 @@ li.highlight { | |||||||
|     display: flex; |     display: flex; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | .button-row-edit > button { | ||||||
|  |     margin-right: 4px; | ||||||
|  | } | ||||||
|  |  | ||||||
| .button-row .button-row-reminder { | .button-row .button-row-reminder { | ||||||
|     flex-grow: 0; |     flex-grow: 0; | ||||||
|     padding: 2px; |     padding: 2px; | ||||||
|   | |||||||
| @@ -18,7 +18,6 @@ const $downloader = document.querySelector("a#downloader"); | |||||||
| const $uploader = document.querySelector("input#uploader"); | const $uploader = document.querySelector("input#uploader"); | ||||||
|  |  | ||||||
| let channels = []; | let channels = []; | ||||||
| let reminderErrors = []; |  | ||||||
| let guildNames = {}; | let guildNames = {}; | ||||||
| let roles = []; | let roles = []; | ||||||
| let templates = {}; | let templates = {}; | ||||||
| @@ -34,11 +33,16 @@ let globalPatreon = false; | |||||||
| let guildPatreon = false; | let guildPatreon = false; | ||||||
|  |  | ||||||
| function guildId() { | function guildId() { | ||||||
|     return document.querySelector("li > a.is-active").parentElement.dataset["guild"]; |     return window.location.pathname.match(/dashboard\/(\d+)/)[1]; | ||||||
| } | } | ||||||
|  |  | ||||||
| function guildName() { | function pane() { | ||||||
|     return guildNames[guildId()]; |     const match = window.location.pathname.match(/dashboard\/\d+\/(.+)/); | ||||||
|  |     if (match === null) { | ||||||
|  |         return null; | ||||||
|  |     } else { | ||||||
|  |         return match[1]; | ||||||
|  |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| function colorToInt(r, g, b) { | function colorToInt(r, g, b) { | ||||||
| @@ -57,7 +61,7 @@ function switch_pane(selector) { | |||||||
|         el.classList.add("is-hidden"); |         el.classList.add("is-hidden"); | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     document.querySelector(`*[data-name=${selector}]`).classList.remove("is-hidden"); |     document.getElementById(selector).classList.remove("is-hidden"); | ||||||
| } | } | ||||||
|  |  | ||||||
| function update_select(sel) { | function update_select(sel) { | ||||||
| @@ -101,7 +105,7 @@ function reset_guild_pane() { | |||||||
| } | } | ||||||
|  |  | ||||||
| async function fetch_patreon(guild_id) { | async function fetch_patreon(guild_id) { | ||||||
|     fetch(`/dashboard/api/guild/${guild_id}/patreon`) |     fetch(`/dashboard/api/guild/${guild_id}`) | ||||||
|         .then((response) => response.json()) |         .then((response) => response.json()) | ||||||
|         .then((data) => { |         .then((data) => { | ||||||
|             if (data.error) { |             if (data.error) { | ||||||
| @@ -228,11 +232,10 @@ async function fetch_reminders(guild_id) { | |||||||
| } | } | ||||||
|  |  | ||||||
| async function serialize_reminder(node, mode) { | async function serialize_reminder(node, mode) { | ||||||
|     let interval, utc_time, expiration_time; |     let utc_time, expiration_time; | ||||||
|  |     let interval = get_interval(node); | ||||||
|  |  | ||||||
|     if (mode !== "template") { |     if (mode !== "template") { | ||||||
|         interval = get_interval(node); |  | ||||||
|  |  | ||||||
|         utc_time = luxon.DateTime.fromISO( |         utc_time = luxon.DateTime.fromISO( | ||||||
|             node.querySelector('input[name="time"]').value |             node.querySelector('input[name="time"]').value | ||||||
|         ).setZone("UTC"); |         ).setZone("UTC"); | ||||||
| @@ -361,9 +364,9 @@ async function serialize_reminder(node, mode) { | |||||||
|         embed_title: embed_title, |         embed_title: embed_title, | ||||||
|         embed_fields: fields, |         embed_fields: fields, | ||||||
|         expires: expiration_time, |         expires: expiration_time, | ||||||
|         interval_seconds: mode !== "template" ? interval.seconds : null, |         interval_seconds: interval.seconds, | ||||||
|         interval_days: mode !== "template" ? interval.days : null, |         interval_days: interval.days, | ||||||
|         interval_months: mode !== "template" ? interval.months : null, |         interval_months: interval.months, | ||||||
|         name: node.querySelector('input[name="name"]').value, |         name: node.querySelector('input[name="name"]').value, | ||||||
|         tts: node.querySelector('input[name="tts"]').checked, |         tts: node.querySelector('input[name="tts"]').checked, | ||||||
|         username: node.querySelector('input[name="username"]').value, |         username: node.querySelector('input[name="username"]').value, | ||||||
| @@ -425,9 +428,9 @@ function deserialize_reminder(reminder, frame, mode) { | |||||||
|             .insertBefore(embed_field, lastChild); |             .insertBefore(embed_field, lastChild); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     if (mode !== "template") { |  | ||||||
|     if (reminder["interval_seconds"]) update_interval(frame); |     if (reminder["interval_seconds"]) update_interval(frame); | ||||||
|  |  | ||||||
|  |     if (mode !== "template") { | ||||||
|         let $enableBtn = frame.querySelector(".disable-enable"); |         let $enableBtn = frame.querySelector(".disable-enable"); | ||||||
|         $enableBtn.dataset["action"] = reminder["enabled"] ? "disable" : "enable"; |         $enableBtn.dataset["action"] = reminder["enabled"] ? "disable" : "enable"; | ||||||
|  |  | ||||||
| @@ -454,27 +457,23 @@ document.addEventListener("guildSwitched", async (e) => { | |||||||
|         .querySelectorAll(".patreon-only") |         .querySelectorAll(".patreon-only") | ||||||
|         .forEach((el) => el.classList.add("is-locked")); |         .forEach((el) => el.classList.add("is-locked")); | ||||||
|  |  | ||||||
|     let $li = document.querySelectorAll(`li[data-guild="${e.detail.guild_id}"]`); |     let $anchor = document.querySelector( | ||||||
|  |         `.switch-pane[data-guild="${e.detail.guild_id}"]` | ||||||
|  |     ); | ||||||
|  |  | ||||||
|     if ($li.length === 0) { |     let hasError = false; | ||||||
|         switch_pane("user-error"); |  | ||||||
|         return; |     if (pane() === null) { | ||||||
|  |         window.history.replaceState({}, "", `/dashboard/${guildId()}/reminders`); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     switch_pane(pane()); | ||||||
|  |  | ||||||
|  |     if ($anchor !== null) { | ||||||
|  |         $anchor.classList.add("is-active"); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     switch_pane(e.detail.pane); |  | ||||||
|     reset_guild_pane(); |     reset_guild_pane(); | ||||||
|     document |  | ||||||
|         .querySelectorAll(`li[data-guild="${e.detail.guild_id}"] > a`) |  | ||||||
|         .forEach((el) => { |  | ||||||
|             el.classList.add("is-active"); |  | ||||||
|         }); |  | ||||||
|     document |  | ||||||
|         .querySelectorAll( |  | ||||||
|             `li[data-guild="${e.detail.guild_id}"] *[data-pane="${e.detail.pane}"]` |  | ||||||
|         ) |  | ||||||
|         .forEach((el) => { |  | ||||||
|             el.classList.add("is-active"); |  | ||||||
|         }); |  | ||||||
|  |  | ||||||
|     if (globalPatreon || (await fetch_patreon(e.detail.guild_id))) { |     if (globalPatreon || (await fetch_patreon(e.detail.guild_id))) { | ||||||
|         document |         document | ||||||
| @@ -482,26 +481,15 @@ document.addEventListener("guildSwitched", async (e) => { | |||||||
|             .forEach((el) => el.classList.remove("is-locked")); |             .forEach((el) => el.classList.remove("is-locked")); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     const event = new CustomEvent("paneLoad", { |     hasError = await fetch_channels(e.detail.guild_id); | ||||||
|         detail: { |  | ||||||
|             guild_id: e.detail.guild_id, |  | ||||||
|             pane: e.detail.pane, |  | ||||||
|         }, |  | ||||||
|     }); |  | ||||||
|     document.dispatchEvent(event); |  | ||||||
| }); |  | ||||||
|  |  | ||||||
| document.addEventListener("paneLoad", async (ev) => { |  | ||||||
|     const hasError = await fetch_channels(ev.detail.guild_id); |  | ||||||
|     if (!hasError) { |     if (!hasError) { | ||||||
|         fetch_roles(ev.detail.guild_id); |         fetch_roles(e.detail.guild_id); | ||||||
|         fetch_templates(ev.detail.guild_id); |         fetch_templates(e.detail.guild_id); | ||||||
|         fetch_reminders(ev.detail.guild_id); |         fetch_reminders(e.detail.guild_id); | ||||||
|  |  | ||||||
|         document.querySelectorAll("p.pageTitle").forEach((el) => { |         document.querySelectorAll("p.pageTitle").forEach((el) => { | ||||||
|             el.textContent = `${guildName()} Reminders`; |             el.textContent = `${e.detail.guild_name} Reminders`; | ||||||
|         }); |         }); | ||||||
|  |  | ||||||
|         document.querySelectorAll("select.channel-selector").forEach((el) => { |         document.querySelectorAll("select.channel-selector").forEach((el) => { | ||||||
|             el.addEventListener("change", (e) => { |             el.addEventListener("change", (e) => { | ||||||
|                 update_select(e.target); |                 update_select(e.target); | ||||||
| @@ -626,6 +614,16 @@ function show_error(error) { | |||||||
|     }, 5000); |     }, 5000); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | function show_success(error) { | ||||||
|  |     document.getElementById("success").querySelector("span.success-message").textContent = | ||||||
|  |         error; | ||||||
|  |     document.getElementById("success").classList.add("is-active"); | ||||||
|  |  | ||||||
|  |     window.setTimeout(() => { | ||||||
|  |         document.getElementById("success").classList.remove("is-active"); | ||||||
|  |     }, 5000); | ||||||
|  | } | ||||||
|  |  | ||||||
| $colorPickerInput.value = colorPicker.color.hexString; | $colorPickerInput.value = colorPicker.color.hexString; | ||||||
|  |  | ||||||
| $colorPickerInput.addEventListener("input", () => { | $colorPickerInput.addEventListener("input", () => { | ||||||
| @@ -706,56 +704,40 @@ document.addEventListener("DOMContentLoaded", async () => { | |||||||
|                             "%guildname%", |                             "%guildname%", | ||||||
|                             guild.name |                             guild.name | ||||||
|                         ); |                         ); | ||||||
|  |                         $anchor.dataset["guild"] = guild.id; | ||||||
|                         $anchor.dataset["name"] = guild.name; |                         $anchor.dataset["name"] = guild.name; | ||||||
|                         $anchor.href = `/dashboard/${guild.id}/reminders`; |                         $anchor.href = `/dashboard/${guild.id}/reminders`; | ||||||
|  |  | ||||||
|                         const $li = $anchor.parentElement; |                         $anchor.addEventListener("click", async (e) => { | ||||||
|                         $li.dataset["guild"] = guild.id; |  | ||||||
|  |  | ||||||
|                         $li.querySelectorAll("a").forEach((el) => { |  | ||||||
|                             el.addEventListener("click", (e) => { |  | ||||||
|                                 const pane = el.dataset["pane"]; |  | ||||||
|                                 const slug = el.dataset["slug"]; |  | ||||||
|  |  | ||||||
|                                 if (pane !== undefined && slug !== undefined) { |  | ||||||
|                             e.preventDefault(); |                             e.preventDefault(); | ||||||
|  |  | ||||||
|                                     switch_pane(pane); |  | ||||||
|  |  | ||||||
|                             window.history.pushState( |                             window.history.pushState( | ||||||
|                                 {}, |                                 {}, | ||||||
|                                 "", |                                 "", | ||||||
|                                         `/dashboard/${guild.id}/${slug}` |                                 `/dashboard/${guild.id}/reminders` | ||||||
|                             ); |                             ); | ||||||
|                             const event = new CustomEvent("guildSwitched", { |                             const event = new CustomEvent("guildSwitched", { | ||||||
|                                 detail: { |                                 detail: { | ||||||
|  |                                     guild_name: guild.name, | ||||||
|                                     guild_id: guild.id, |                                     guild_id: guild.id, | ||||||
|                                             pane, |  | ||||||
|                                 }, |                                 }, | ||||||
|                             }); |                             }); | ||||||
|  |  | ||||||
|                             document.dispatchEvent(event); |                             document.dispatchEvent(event); | ||||||
|                                 } |  | ||||||
|                             }); |  | ||||||
|                         }); |                         }); | ||||||
|  |  | ||||||
|                         element.append($clone); |                         element.append($clone); | ||||||
|                     }); |                     }); | ||||||
|                 } |                 } | ||||||
|  |  | ||||||
|                 const matches = window.location.href.match( |                 const matches = window.location.href.match(/dashboard\/(\d+)/); | ||||||
|                     /dashboard\/(\d+)(\/)?([a-zA-Z\-]+)?/ |  | ||||||
|                 ); |  | ||||||
|                 if (matches) { |                 if (matches) { | ||||||
|                     let id = matches[1]; |                     let id = matches[1]; | ||||||
|                     let kind = matches[3]; |  | ||||||
|                     let name = guildNames[id]; |                     let name = guildNames[id]; | ||||||
|  |  | ||||||
|                     const event = new CustomEvent("guildSwitched", { |                     const event = new CustomEvent("guildSwitched", { | ||||||
|                         detail: { |                         detail: { | ||||||
|                             guild_name: name, |                             guild_name: name, | ||||||
|                             guild_id: id, |                             guild_id: id, | ||||||
|                             pane: kind, |  | ||||||
|                         }, |                         }, | ||||||
|                     }); |                     }); | ||||||
|  |  | ||||||
| @@ -796,11 +778,25 @@ $uploader.addEventListener("change", (ev) => { | |||||||
|         fileReader.onload = (e) => resolve(fileReader.result); |         fileReader.onload = (e) => resolve(fileReader.result); | ||||||
|         fileReader.readAsDataURL($uploader.files[0]); |         fileReader.readAsDataURL($uploader.files[0]); | ||||||
|     }).then((dataUrl) => { |     }).then((dataUrl) => { | ||||||
|  |         $importBtn.setAttribute("disabled", true); | ||||||
|  |  | ||||||
|         fetch(`/dashboard/api/guild/${guildId()}/export/${urlTail}`, { |         fetch(`/dashboard/api/guild/${guildId()}/export/${urlTail}`, { | ||||||
|             method: "PUT", |             method: "PUT", | ||||||
|             body: JSON.stringify({ body: dataUrl.split(",")[1] }), |             body: JSON.stringify({ body: dataUrl.split(",")[1] }), | ||||||
|         }).then(() => { |         }) | ||||||
|  |             .then((response) => response.json()) | ||||||
|  |             .then((data) => { | ||||||
|  |                 $importBtn.removeAttribute("disabled"); | ||||||
|  |  | ||||||
|  |                 if (data.error) { | ||||||
|  |                     show_error(data.error); | ||||||
|  |                 } else { | ||||||
|  |                     show_success(data.message); | ||||||
|  |                 } | ||||||
|  |             }) | ||||||
|  |             .then(() => { | ||||||
|                 delete $uploader.files[0]; |                 delete $uploader.files[0]; | ||||||
|  |                 fetch_reminders(guild); | ||||||
|             }); |             }); | ||||||
|     }); |     }); | ||||||
| }); | }); | ||||||
|   | |||||||
| @@ -1,45 +0,0 @@ | |||||||
| function loadErrors() { |  | ||||||
|     return fetch( |  | ||||||
|         `/dashboard/api/guild/${guildId()}/reminders?status=deleted,sent,failed` |  | ||||||
|     ).then((response) => response.json()); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| document.addEventListener("paneLoad", (ev) => { |  | ||||||
|     if (ev.detail.pane !== "errors") { |  | ||||||
|         return; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     document.querySelectorAll(".reminderError").forEach((el) => el.remove()); |  | ||||||
|  |  | ||||||
|     const template = document.getElementById("reminderError"); |  | ||||||
|     const container = document.getElementById("reminderLog"); |  | ||||||
|  |  | ||||||
|     loadErrors() |  | ||||||
|         .then((res) => { |  | ||||||
|             res = res |  | ||||||
|                 .filter((r) => r.status_change_time !== null) |  | ||||||
|                 .sort((a, b) => a.status_change_time < b.status_change_time); |  | ||||||
|  |  | ||||||
|             for (const reminder of res) { |  | ||||||
|                 const newRow = template.content.cloneNode(true); |  | ||||||
|  |  | ||||||
|                 newRow.querySelector(".reminderError").dataset["case"] = reminder.status; |  | ||||||
|  |  | ||||||
|                 const statusTime = new luxon.DateTime.fromISO( |  | ||||||
|                     reminder.status_change_time, |  | ||||||
|                     { zone: "UTC" } |  | ||||||
|                 ); |  | ||||||
|                 newRow.querySelector(".reminderName").textContent = reminder.name; |  | ||||||
|                 newRow.querySelector(".reminderMessage").textContent = |  | ||||||
|                     reminder.status_message; |  | ||||||
|                 newRow.querySelector(".reminderTime").textContent = statusTime |  | ||||||
|                     .toLocal() |  | ||||||
|                     .toLocaleString(luxon.DateTime.DATETIME_MED); |  | ||||||
|  |  | ||||||
|                 container.appendChild(newRow); |  | ||||||
|             } |  | ||||||
|         }) |  | ||||||
|         .finally(() => { |  | ||||||
|             $loader.classList.add("is-hidden"); |  | ||||||
|         }); |  | ||||||
| }); |  | ||||||
| @@ -27,7 +27,7 @@ | |||||||
|     <link rel="stylesheet" href="/static/css/bulma.min.css"> |     <link rel="stylesheet" href="/static/css/bulma.min.css"> | ||||||
|     <link rel="stylesheet" href="/static/css/fa.css"> |     <link rel="stylesheet" href="/static/css/fa.css"> | ||||||
|     <link rel="stylesheet" href="/static/css/font.css"> |     <link rel="stylesheet" href="/static/css/font.css"> | ||||||
|     <link rel="stylesheet" href="/static/css/style.css"> |     <link rel="stylesheet" href="/static/css/style.css?v{{ version }}"> | ||||||
|     <link rel="stylesheet" href="/static/css/dtsel.css"> |     <link rel="stylesheet" href="/static/css/dtsel.css"> | ||||||
|     <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/tributejs/5.1.3/tribute.css" integrity="sha512-GnwBnXd+ZGO9CdP343MUr0jCcJXCr++JVtQRnllexRW2IDq4Zvrh/McTQjooAKnSUbXZ7wamp7AQSweTnfMVoA==" crossorigin="anonymous" referrerpolicy="no-referrer" /> |     <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/tributejs/5.1.3/tribute.css" integrity="sha512-GnwBnXd+ZGO9CdP343MUr0jCcJXCr++JVtQRnllexRW2IDq4Zvrh/McTQjooAKnSUbXZ7wamp7AQSweTnfMVoA==" crossorigin="anonymous" referrerpolicy="no-referrer" /> | ||||||
|  |  | ||||||
| @@ -76,6 +76,10 @@ | |||||||
|     <span class="icon"><i class="far fa-exclamation-circle"></i></span> <span class="error-message"></span> |     <span class="icon"><i class="far fa-exclamation-circle"></i></span> <span class="error-message"></span> | ||||||
| </div> | </div> | ||||||
|  |  | ||||||
|  | <div class="notification is-success flash-message" id="success"> | ||||||
|  |     <span class="icon"><i class="far fa-check"></i></span> <span class="success-message"></span> | ||||||
|  | </div> | ||||||
|  |  | ||||||
| <div class="modal" id="addImageModal"> | <div class="modal" id="addImageModal"> | ||||||
|     <div class="modal-background"></div> |     <div class="modal-background"></div> | ||||||
|     <div class="modal-card"> |     <div class="modal-card"> | ||||||
| @@ -185,14 +189,6 @@ | |||||||
|                     </label> |                     </label> | ||||||
|                 </div> |                 </div> | ||||||
|             </div> |             </div> | ||||||
|             <div class="control"> |  | ||||||
|                 <div class="field"> |  | ||||||
|                     <label> |  | ||||||
|                         <input type="radio" class="default-width" name="exportSelect" value="todos"> |  | ||||||
|                         Todo Lists |  | ||||||
|                     </label> |  | ||||||
|                 </div> |  | ||||||
|             </div> |  | ||||||
|             <br> |             <br> | ||||||
|             <div class="has-text-centered"> |             <div class="has-text-centered"> | ||||||
|                 <div style="color: red"> |                 <div style="color: red"> | ||||||
| @@ -257,11 +253,9 @@ | |||||||
|                 </p> |                 </p> | ||||||
|                 <ul class="menu-list"> |                 <ul class="menu-list"> | ||||||
|                     <li> |                     <li> | ||||||
|                         {# |  | ||||||
|                         <a class="show-modal" data-modal="dataManagerModal"> |                         <a class="show-modal" data-modal="dataManagerModal"> | ||||||
|                             <span class="icon"><i class="fas fa-exchange"></i></span> Import/Export |                             <span class="icon"><i class="fas fa-exchange"></i></span> Import/Export | ||||||
|                         </a> |                         </a> | ||||||
|                         #} |  | ||||||
|                         <a class="show-modal" data-modal="chooseTimezoneModal"> |                         <a class="show-modal" data-modal="chooseTimezoneModal"> | ||||||
|                             <span class="icon"><i class="fas fa-map-marked"></i></span> Timezone |                             <span class="icon"><i class="fas fa-map-marked"></i></span> Timezone | ||||||
|                         </a> |                         </a> | ||||||
| @@ -303,11 +297,9 @@ | |||||||
|                 </p> |                 </p> | ||||||
|                 <ul class="menu-list"> |                 <ul class="menu-list"> | ||||||
|                     <li> |                     <li> | ||||||
|                         {# |  | ||||||
|                         <a class="show-modal" data-modal="dataManagerModal"> |                         <a class="show-modal" data-modal="dataManagerModal"> | ||||||
|                             <span class="icon"><i class="fas fa-exchange"></i></span> Import/Export |                             <span class="icon"><i class="fas fa-exchange"></i></span> Import/Export | ||||||
|                         </a> |                         </a> | ||||||
|                         #} |  | ||||||
|                         <a class="show-modal" data-modal="chooseTimezoneModal"> |                         <a class="show-modal" data-modal="chooseTimezoneModal"> | ||||||
|                             <span class="icon"><i class="fas fa-map-marked"></i></span> Timezone |                             <span class="icon"><i class="fas fa-map-marked"></i></span> Timezone | ||||||
|                         </a> |                         </a> | ||||||
| @@ -333,16 +325,16 @@ | |||||||
|                 <p class="subtitle is-hidden-desktop">Press the <span class="icon"><i class="fal fa-bars"></i></span> to get started</p> |                 <p class="subtitle is-hidden-desktop">Press the <span class="icon"><i class="fal fa-bars"></i></span> to get started</p> | ||||||
|             </div> |             </div> | ||||||
|         </section> |         </section> | ||||||
|         <section data-name="reminders" class="is-hidden"> |         <section id="reminders" class="is-hidden"> | ||||||
|             {% include "reminder_dashboard/reminder_dashboard" %} |             {% include "reminder_dashboard/reminder_dashboard" %} | ||||||
|         </section> |         </section> | ||||||
|         <section data-name="errors" class="is-hidden"> |         <section id="reminder-errors" class="is-hidden"> | ||||||
|             {% include "reminder_dashboard/reminder_errors" %} |             {% include "reminder_dashboard/reminder_errors" %} | ||||||
|         </section> |         </section> | ||||||
|         <section data-name="guild-error" class="is-hidden"> |         <section id="guild-error" class="is-hidden"> | ||||||
|             {% include "reminder_dashboard/guild_error" %} |             {% include "reminder_dashboard/guild_error" %} | ||||||
|         </section> |         </section> | ||||||
|         <section data-name="user-error" class="is-hidden"> |         <section id="user-error" class="is-hidden"> | ||||||
|             {% include "reminder_dashboard/user_error" %} |             {% include "reminder_dashboard/user_error" %} | ||||||
|         </section> |         </section> | ||||||
|     </div> |     </div> | ||||||
| @@ -376,36 +368,22 @@ | |||||||
|  |  | ||||||
| <template id="guildListEntry"> | <template id="guildListEntry"> | ||||||
|     <li> |     <li> | ||||||
|         <a class="switch-pane" data-pane="reminders" data-slug="reminders"> |         <a class="switch-pane" data-pane="guild"> | ||||||
|             <span class="icon"><i class="fas fa-map-pin"></i></span> <span class="guild-name">%guildname%</span> |             <span class="icon"><i class="fas fa-map-pin"></i></span> <span class="guild-name">%guildname%</span> | ||||||
|         </a> |         </a> | ||||||
|         <ul class="guild-submenu"> |  | ||||||
|             <li> |  | ||||||
|                 <a class="switch-pane" data-pane="reminders" data-slug="reminders"> |  | ||||||
|                     <span class="icon"><i class="fas fa-calendar-alt"></i></span> Reminders |  | ||||||
|                 </a> |  | ||||||
|                 <a class="switch-pane" data-pane="errors" data-slug="errors"> |  | ||||||
|                     <span class="icon"><i class="fas fa-file-alt"></i></span> Logs |  | ||||||
|                 </a> |  | ||||||
|             </li> |  | ||||||
|         </ul> |  | ||||||
|     </li> |     </li> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <template id="guildReminder"> | <template id="guildReminder"> | ||||||
|     {% include "reminder_dashboard/templates/guild_reminder" %} |     {% include "reminder_dashboard/guild_reminder" %} | ||||||
| </template> |  | ||||||
|  |  | ||||||
| <template id="reminderError"> |  | ||||||
|     {% include "reminder_dashboard/templates/reminder_error" %} |  | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script src="/static/js/iro.js"></script> | <script src="/static/js/iro.js"></script> | ||||||
| <script src="/static/js/dtsel.js"></script> | <script src="/static/js/dtsel.js"></script> | ||||||
|  |  | ||||||
| <script src="/static/js/interval.js"></script> | <script src="/static/js/interval.js?v{{ version }}"></script> | ||||||
| <script src="/static/js/timezone.js" defer></script> | <script src="/static/js/timezone.js?v{{ version }}" defer></script> | ||||||
| <script src="/static/js/main.js" defer></script> | <script src="/static/js/main.js?v{{ version }}" defer></script> | ||||||
|  |  | ||||||
| </body> | </body> | ||||||
| </html> | </html> | ||||||
|   | |||||||
| @@ -2,7 +2,7 @@ | |||||||
|     <strong>Create Reminder</strong> |     <strong>Create Reminder</strong> | ||||||
|     <div id="reminderCreator"> |     <div id="reminderCreator"> | ||||||
|         {% set creating = true %} |         {% set creating = true %} | ||||||
|         {% include "reminder_dashboard/templates/guild_reminder" %} |         {% include "reminder_dashboard/guild_reminder" %} | ||||||
|         {% set creating = false %} |         {% set creating = false %} | ||||||
|     </div> |     </div> | ||||||
|     <br> |     <br> | ||||||
| @@ -46,10 +46,6 @@ | |||||||
|     <div id="guildReminders"> |     <div id="guildReminders"> | ||||||
|  |  | ||||||
|     </div> |     </div> | ||||||
|  |  | ||||||
|     <div id="guildErrors"> |  | ||||||
|  |  | ||||||
|     </div> |  | ||||||
| </div> | </div> | ||||||
|  |  | ||||||
| <script src="/static/js/sort.js"></script> | <script src="/static/js/sort.js"></script> | ||||||
|   | |||||||
| @@ -1,5 +1,5 @@ | |||||||
| <div id="reminderLog"> | <div> | ||||||
|  |  | ||||||
| </div> | </div> | ||||||
|  |  | ||||||
| <script src="/static/js/reminder_errors.js"></script> | <!--<script src="/static/js/reminder_errors.js"></script>--> | ||||||
|   | |||||||
| @@ -1,20 +0,0 @@ | |||||||
| <div class="reminderError" data-case="success"> |  | ||||||
|     <div class="errorHead"> |  | ||||||
|         <div class="errorIcon"> |  | ||||||
|             <span class="icon"> |  | ||||||
|                 <i class="fas fa-trash"></i> |  | ||||||
|                 <i class="fas fa-check"></i> |  | ||||||
|                 <i class="fas fa-exclamation-triangle"></i> |  | ||||||
|             </span> |  | ||||||
|         </div> |  | ||||||
|         <div class="reminderName"> |  | ||||||
|             Reminder |  | ||||||
|         </div> |  | ||||||
|         <div class="reminderMessage"> |  | ||||||
|  |  | ||||||
|         </div> |  | ||||||
|         <div class="reminderTime"> |  | ||||||
|  |  | ||||||
|         </div> |  | ||||||
|     </div> |  | ||||||
| </div> |  | ||||||
		Reference in New Issue
	
	Block a user