From 2fbd8858550290757a93df009f32761ffc9722b8 Mon Sep 17 00:00:00 2001 From: alyssa Date: Fri, 27 Sep 2024 04:29:16 +0900 Subject: [PATCH 01/54] fix(api): missing null check on /members/:id/groups endpoint --- PluralKit.API/Controllers/v2/GroupMemberControllerV2.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/PluralKit.API/Controllers/v2/GroupMemberControllerV2.cs b/PluralKit.API/Controllers/v2/GroupMemberControllerV2.cs index 7a40f244..bdd1771d 100644 --- a/PluralKit.API/Controllers/v2/GroupMemberControllerV2.cs +++ b/PluralKit.API/Controllers/v2/GroupMemberControllerV2.cs @@ -147,6 +147,8 @@ public class GroupMemberControllerV2: PKControllerBase public async Task GetMemberGroups(string memberRef) { var member = await ResolveMember(memberRef); + if (member == null) + throw Errors.MemberNotFoundWithRef(memberRef); var ctx = ContextFor(member); var system = await _repo.GetSystem(member.System); From d0f41dd9288a473b3067caf5f6f9be96f89457e8 Mon Sep 17 00:00:00 2001 From: alyssa Date: Fri, 27 Sep 2024 04:30:00 +0900 Subject: [PATCH 02/54] fix(api): add x-ratelimit-scope to cors allowed headers --- services/api/src/middleware/cors.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/api/src/middleware/cors.rs b/services/api/src/middleware/cors.rs index a3c73279..c94b3f6f 100644 --- a/services/api/src/middleware/cors.rs +++ b/services/api/src/middleware/cors.rs @@ -11,7 +11,7 @@ fn add_cors_headers(headers: &mut HeaderMap) { headers.append("Access-Control-Allow-Methods", HeaderValue::from_static("*")); headers.append("Access-Control-Allow-Credentials", HeaderValue::from_static("true")); headers.append("Access-Control-Allow-Headers", HeaderValue::from_static("Content-Type, Authorization, sentry-trace, User-Agent")); - headers.append("Access-Control-Expose-Headers", HeaderValue::from_static("X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset")); + headers.append("Access-Control-Expose-Headers", HeaderValue::from_static("X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-RateLimit-Scope")); headers.append("Access-Control-Max-Age", HeaderValue::from_static("86400")); } From a0d56cd667af9e2f04b3af2f7119e705fa049abc Mon Sep 17 00:00:00 2001 From: alyssa Date: Fri, 27 Sep 2024 04:37:20 +0900 Subject: [PATCH 03/54] fix(api): add x-pluralkit-version as well --- services/api/src/middleware/cors.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/api/src/middleware/cors.rs b/services/api/src/middleware/cors.rs index c94b3f6f..2632dea6 100644 --- a/services/api/src/middleware/cors.rs +++ b/services/api/src/middleware/cors.rs @@ -11,7 +11,7 @@ fn add_cors_headers(headers: &mut HeaderMap) { headers.append("Access-Control-Allow-Methods", HeaderValue::from_static("*")); headers.append("Access-Control-Allow-Credentials", HeaderValue::from_static("true")); headers.append("Access-Control-Allow-Headers", HeaderValue::from_static("Content-Type, Authorization, sentry-trace, User-Agent")); - headers.append("Access-Control-Expose-Headers", HeaderValue::from_static("X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-RateLimit-Scope")); + headers.append("Access-Control-Expose-Headers", HeaderValue::from_static("X-PluralKit-Version, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-RateLimit-Scope")); headers.append("Access-Control-Max-Age", HeaderValue::from_static("86400")); } From 873643153d38894812fe4c4d184e5ff0f9c82f97 Mon Sep 17 00:00:00 2001 From: alyssa Date: Fri, 27 Sep 2024 16:17:09 +0900 Subject: [PATCH 04/54] fix(rust): update some deps to build correctly on nightly --- Cargo.lock | 321 ++++++++++++++++------- Cargo.toml | 2 +- lib/libpk/Cargo.toml | 5 +- services/api/src/middleware/logger.rs | 4 +- services/api/src/middleware/ratelimit.rs | 4 +- 5 files changed, 228 insertions(+), 108 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 76f6764f..2b79edd1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,17 +17,6 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" -[[package]] -name = "ahash" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" -dependencies = [ - "getrandom", - "once_cell", - "version_check", -] - [[package]] name = "ahash" version = "0.8.11" @@ -132,6 +121,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "atty" version = "0.2.14" @@ -269,12 +264,6 @@ dependencies = [ "rustc-demangle", ] -[[package]] -name = "base64" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" - [[package]] name = "base64" version = "0.21.7" @@ -380,11 +369,12 @@ dependencies = [ [[package]] name = "config" -version = "0.13.3" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d379af7f68bfc21714c6c7dea883544201741d2ce8274bb12fa54f89507f52a7" +checksum = "7328b20597b53c2454f0b1919720c25c7339051c02b72b7e05409e00b14132be" dependencies = [ "async-trait", + "convert_case", "json5", "lazy_static", "nom", @@ -403,6 +393,35 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "const-random" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom", + "once_cell", + "tiny-keccak", +] + +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "cookie-factory" version = "0.3.2" @@ -473,6 +492,12 @@ version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + [[package]] name = "crypto-common" version = "0.1.6" @@ -538,9 +563,12 @@ dependencies = [ [[package]] name = "dlv-list" -version = "0.3.0" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0688c2a7f92e427f44895cd63841bff7b29f8d7a1648b9e7e07a4a365b2e1257" +checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f" +dependencies = [ + "const-random", +] [[package]] name = "dotenvy" @@ -692,7 +720,7 @@ dependencies = [ "sha-1", "tokio", "tokio-stream", - "tokio-util", + "tokio-util 0.6.10", "tracing", "url", ] @@ -825,7 +853,7 @@ checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi", ] [[package]] @@ -835,21 +863,37 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" [[package]] -name = "hashbrown" -version = "0.12.3" +name = "h2" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +checksum = "524e8ac6999421f49a846c2d4411f337e53497d8ec55d67753beffa43c5d9205" dependencies = [ - "ahash 0.7.6", + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http 1.1.0", + "indexmap", + "slab", + "tokio", + "tokio-util 0.7.12", + "tracing", ] +[[package]] +name = "hashbrown" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" + [[package]] name = "hashbrown" version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" dependencies = [ - "ahash 0.8.11", + "ahash", "allocator-api2", ] @@ -977,6 +1021,17 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "hostname" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867" +dependencies = [ + "libc", + "match_cfg", + "winapi", +] + [[package]] name = "http" version = "0.2.8" @@ -1092,6 +1147,7 @@ dependencies = [ "bytes", "futures-channel", "futures-util", + "h2", "http 1.1.0", "http-body 1.0.0", "httparse", @@ -1184,16 +1240,6 @@ dependencies = [ "unicode-normalization", ] -[[package]] -name = "indexmap" -version = "1.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399" -dependencies = [ - "autocfg", - "hashbrown 0.12.3", -] - [[package]] name = "indexmap" version = "2.2.6" @@ -1293,6 +1339,7 @@ dependencies = [ "sqlx", "tokio", "tracing", + "tracing-gelf", "tracing-subscriber", ] @@ -1339,13 +1386,10 @@ dependencies = [ ] [[package]] -name = "mach" -version = "0.3.2" +name = "match_cfg" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b823e83b2affd8f40a9ee8c29dbc56404c1e34cd2710921f2801e2cf29527afa" -dependencies = [ - "libc", -] +checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" [[package]] name = "matchers" @@ -1389,58 +1433,45 @@ dependencies = [ [[package]] name = "metrics" -version = "0.20.1" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b9b8653cec6897f73b519a43fba5ee3d50f62fe9af80b428accdcc093b4a849" +checksum = "884adb57038347dfbaf2d5065887b6cf4312330dc8e94bc30a1a839bd79d3261" dependencies = [ - "ahash 0.7.6", - "metrics-macros", + "ahash", "portable-atomic", ] [[package]] name = "metrics-exporter-prometheus" -version = "0.11.0" +version = "0.15.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8603921e1f54ef386189335f288441af761e0fc61bcb552168d9cedfe63ebc70" +checksum = "b4f0c8427b39666bf970460908b213ec09b3b350f20c0c2eabcbba51704a08e6" dependencies = [ - "hyper 0.14.24", - "indexmap 1.9.2", + "base64 0.22.1", + "http-body-util", + "hyper 1.3.1", + "hyper-util", + "indexmap", "ipnet", "metrics", "metrics-util", - "parking_lot 0.12.1", - "portable-atomic", "quanta", "thiserror", "tokio", "tracing", ] -[[package]] -name = "metrics-macros" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "731f8ecebd9f3a4aa847dfe75455e4757a45da40a7793d2f0b1f9b6ed18b23f3" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.107", -] - [[package]] name = "metrics-util" -version = "0.14.0" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7d24dc2dbae22bff6f1f9326ffce828c9f07ef9cc1e8002e5279f845432a30a" +checksum = "4259040465c955f9f2f1a4a8a16dc46726169bca0f88e8fb2dbeced487c3e828" dependencies = [ "crossbeam-epoch", "crossbeam-utils", - "hashbrown 0.12.3", + "hashbrown 0.14.3", "metrics", "num_cpus", - "parking_lot 0.12.1", - "portable-atomic", "quanta", "sketches-ddsketch", ] @@ -1474,7 +1505,7 @@ checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" dependencies = [ "hermit-abi 0.3.9", "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi", "windows-sys 0.52.0", ] @@ -1593,12 +1624,12 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" [[package]] name = "ordered-multimap" -version = "0.4.3" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccd746e37177e1711c20dd619a1620f34f5c8b569c53590a72dedd5344d8924a" +checksum = "4ed8acf08e98e744e5384c8bc63ceb0364e68a6854187221c18df61c4797690e" dependencies = [ "dlv-list", - "hashbrown 0.12.3", + "hashbrown 0.13.2", ] [[package]] @@ -1733,7 +1764,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" dependencies = [ "fixedbitset", - "indexmap 2.2.6", + "indexmap", ] [[package]] @@ -1797,9 +1828,9 @@ checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" [[package]] name = "portable-atomic" -version = "0.3.19" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26f6a7b87c2e435a3241addceeeff740ff8b7e76b74c13bf9acb17fa454ea00b" +checksum = "d30538d42559de6b034bc76fd6dd4c38961b1ee5c6c56e3808c50128fdbc22ce" [[package]] name = "ppv-lite86" @@ -1891,16 +1922,15 @@ dependencies = [ [[package]] name = "quanta" -version = "0.10.1" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7e31331286705f455e56cca62e0e717158474ff02b7936c1fa596d983f4ae27" +checksum = "8e5167a477619228a0b284fac2674e3c388cba90631d7b7de620e6f1fcd08da5" dependencies = [ "crossbeam-utils", "libc", - "mach", "once_cell", "raw-cpuid", - "wasi 0.10.2+wasi-snapshot-preview1", + "wasi", "web-sys", "winapi", ] @@ -2010,11 +2040,11 @@ dependencies = [ [[package]] name = "raw-cpuid" -version = "10.7.0" +version = "11.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c297679cb867470fa8c9f67dbba74a78d78e3e98d7cf2b08d6d71540f797332" +checksum = "cb9ee317cfe3fbd54b36a511efc1edd42e216903c9cd575e686dd68a2ba90d8d" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.5.0", ] [[package]] @@ -2166,13 +2196,14 @@ dependencies = [ [[package]] name = "ron" -version = "0.7.1" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88073939a61e5b7680558e6be56b419e208420c2adb92be54921fa6b72283f1a" +checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94" dependencies = [ - "base64 0.13.1", - "bitflags 1.3.2", + "base64 0.21.7", + "bitflags 2.5.0", "serde", + "serde_derive", ] [[package]] @@ -2197,9 +2228,9 @@ dependencies = [ [[package]] name = "rust-ini" -version = "0.18.0" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6d5f2436026b4f6e79dc829837d467cc7e9a55ee40e750d716713540715a2df" +checksum = "7e2a3bcec1f113553ef1c88aae6c020a369d03d55b58de9869a0908930385091" dependencies = [ "cfg-if", "ordered-multimap", @@ -2335,6 +2366,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_spanned" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +dependencies = [ + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -2505,7 +2545,7 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24ba59a9342a3d9bab6c56c118be528b27c9b60e490080e9711a04dccac83ef6" dependencies = [ - "ahash 0.8.11", + "ahash", "atoi", "byteorder", "bytes", @@ -2521,7 +2561,7 @@ dependencies = [ "futures-util", "hashlink", "hex", - "indexmap 2.2.6", + "indexmap", "log", "memchr", "once_cell", @@ -2788,6 +2828,15 @@ dependencies = [ "once_cell", ] +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + [[package]] name = "tinyvec" version = "1.6.0" @@ -2869,12 +2918,50 @@ dependencies = [ ] [[package]] -name = "toml" -version = "0.5.11" +name = "tokio-util" +version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" dependencies = [ "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", ] [[package]] @@ -2976,6 +3063,35 @@ dependencies = [ "valuable", ] +[[package]] +name = "tracing-futures" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2" +dependencies = [ + "pin-project", + "tracing", +] + +[[package]] +name = "tracing-gelf" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c0170f1bf67b749d4377c2da1d99d6e722600051ee53870cfb6f618611e29e" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "hostname", + "serde_json", + "thiserror", + "tokio", + "tokio-util 0.7.12", + "tracing-core", + "tracing-futures", + "tracing-subscriber", +] + [[package]] name = "tracing-log" version = "0.1.3" @@ -3126,12 +3242,6 @@ dependencies = [ "try-lock", ] -[[package]] -name = "wasi" -version = "0.10.2+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" - [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -3529,6 +3639,15 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "winnow" +version = "0.6.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" +dependencies = [ + "memchr", +] + [[package]] name = "yaml-rust" version = "0.4.5" diff --git a/Cargo.toml b/Cargo.toml index cd075886..d882f150 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,7 @@ anyhow = "1" axum = "0.7.5" fred = { version = "5.2.0", default-features = false, features = ["tracing", "pool-prefer-active"] } lazy_static = "1.4.0" -metrics = "0.20.1" +metrics = "0.23.0" serde = "1.0.152" serde_json = "1.0.117" sqlx = { version = "0.7.4", features = ["runtime-tokio", "postgres", "chrono", "macros"] } diff --git a/lib/libpk/Cargo.toml b/lib/libpk/Cargo.toml index eae2cef5..a5ec39c5 100644 --- a/lib/libpk/Cargo.toml +++ b/lib/libpk/Cargo.toml @@ -5,16 +5,17 @@ edition = "2021" [dependencies] anyhow = { workspace = true } -config = "0.13.3" +config = "0.14.0" fred = { workspace = true } gethostname = "0.4.1" lazy_static = { workspace = true } metrics = { workspace = true } -metrics-exporter-prometheus = { version = "0.11.0", default-features = false, features = ["tokio", "http-listener", "tracing"] } +metrics-exporter-prometheus = { version = "0.15.3", default-features = false, features = ["tokio", "http-listener", "tracing"] } serde = { workspace = true } sqlx = { workspace = true } tokio = { workspace = true } tracing = { workspace = true } +tracing-gelf = "0.7.1" tracing-subscriber = { workspace = true} prost = { workspace = true } diff --git a/services/api/src/middleware/logger.rs b/services/api/src/middleware/logger.rs index e17422a4..7250f225 100644 --- a/services/api/src/middleware/logger.rs +++ b/services/api/src/middleware/logger.rs @@ -49,11 +49,11 @@ pub async fn logger(request: Request, next: Next) -> Response { ); histogram!( "pk_http_requests", - (elapsed as f64) / 1_000_f64, "method" => method.to_string(), "route" => endpoint.clone(), "status" => response.status().to_string() - ); + ) + .record((elapsed as f64) / 1_000_f64); if elapsed > MIN_LOG_TIME { warn!( diff --git a/services/api/src/middleware/ratelimit.rs b/services/api/src/middleware/ratelimit.rs index eb98bd66..e7cd35b1 100644 --- a/services/api/src/middleware/ratelimit.rs +++ b/services/api/src/middleware/ratelimit.rs @@ -7,7 +7,7 @@ use axum::{ response::Response, }; use fred::{pool::RedisPool, prelude::LuaInterface, types::ReconnectPolicy, util::sha1_hash}; -use metrics::increment_counter; +use metrics::counter; use tracing::{debug, error, info, warn}; use crate::util::{header_or_unknown, json_err}; @@ -165,7 +165,7 @@ pub async fn do_request_ratelimited( } else { let retry_after = (retry_after * 1_000_f64).ceil() as u64; debug!("ratelimited request from {rl_key}, retry_after={retry_after}",); - increment_counter!("pk_http_requests_ratelimited"); + counter!("pk_http_requests_ratelimited").increment(1); json_err( StatusCode::TOO_MANY_REQUESTS, format!( From 21d647f423c69b165a751daeeab818b6f06ae622 Mon Sep 17 00:00:00 2001 From: alyssa Date: Fri, 27 Sep 2024 16:57:32 +0900 Subject: [PATCH 05/54] fix(api): do sliding window rate limiting correctly (hopefully) --- services/api/src/middleware/ratelimit.lua | 10 ++++------ services/api/src/middleware/ratelimit.rs | 14 +++++--------- 2 files changed, 9 insertions(+), 15 deletions(-) diff --git a/services/api/src/middleware/ratelimit.lua b/services/api/src/middleware/ratelimit.lua index 30beadcd..f272cb9f 100644 --- a/services/api/src/middleware/ratelimit.lua +++ b/services/api/src/middleware/ratelimit.lua @@ -2,13 +2,11 @@ -- redis.replicate_commands() local rate_limit_key = KEYS[1] -local burst = ARGV[1] -local rate = ARGV[2] -local period = ARGV[3] +local rate = ARGV[1] +local period = ARGV[2] +local cost = tonumber(ARGV[3]) --- we're only ever asking for 1 request at a time --- todo: this is no longer true -local cost = 1 --local cost = tonumber(ARGV[4]) +local burst = rate local emission_interval = period / rate local increment = emission_interval * cost diff --git a/services/api/src/middleware/ratelimit.rs b/services/api/src/middleware/ratelimit.rs index e7cd35b1..05e55815 100644 --- a/services/api/src/middleware/ratelimit.rs +++ b/services/api/src/middleware/ratelimit.rs @@ -137,24 +137,23 @@ pub async fn do_request_ratelimited( rlimit.key() ); - let burst = 5; let period = 1; // seconds + let cost = 1; // todo: update this for group member endpoints // local rate_limit_key = KEYS[1] - // local burst = ARGV[1] - // local rate = ARGV[2] - // local period = ARGV[3] + // local rate = ARGV[1] + // local period = ARGV[2] // return {remaining, tostring(retry_after), reset_after} let resp = redis .evalsha::<(i32, String, u64), String, Vec, Vec>( LUA_SCRIPT_SHA.to_string(), vec![rl_key.clone()], - vec![burst, rlimit.rate(), period], + vec![rlimit.rate(), period, cost], ) .await; match resp { - Ok((mut remaining, retry_after, reset_after)) => { + Ok((remaining, retry_after, reset_after)) => { // redis's lua doesn't support returning floats let retry_after: f64 = retry_after .parse() @@ -175,9 +174,6 @@ pub async fn do_request_ratelimited( ) }; - // the redis script puts burst in remaining for ??? some reason - remaining -= burst - rlimit.rate(); - let reset_time = SystemTime::now() .checked_add(Duration::from_secs(reset_after)) .expect("invalid timestamp") From b72805c51c3240b4b22ccc7981add83b2f92dbe8 Mon Sep 17 00:00:00 2001 From: Petal Ladenson Date: Tue, 1 Oct 2024 05:38:26 -0600 Subject: [PATCH 06/54] feat(bot): add pk;configure alias --- PluralKit.Bot/CommandMeta/CommandTree.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PluralKit.Bot/CommandMeta/CommandTree.cs b/PluralKit.Bot/CommandMeta/CommandTree.cs index 92441f9d..64d0a929 100644 --- a/PluralKit.Bot/CommandMeta/CommandTree.cs +++ b/PluralKit.Bot/CommandMeta/CommandTree.cs @@ -18,7 +18,7 @@ public partial class CommandTree return CommandHelpRoot(ctx); if (ctx.Match("ap", "autoproxy", "auto")) return HandleAutoproxyCommand(ctx); - if (ctx.Match("config", "cfg")) + if (ctx.Match("config", "cfg", "configure")) return HandleConfigCommand(ctx); if (ctx.Match("list", "find", "members", "search", "query", "l", "f", "fd", "ls")) return ctx.Execute(SystemList, m => m.MemberList(ctx, ctx.System)); From 96994906746fc36af9e3f5b54ab6694a4cc347bc Mon Sep 17 00:00:00 2001 From: ambdroid <61042504+ambdroid@users.noreply.github.com> Date: Tue, 1 Oct 2024 08:38:35 -0400 Subject: [PATCH 07/54] feat(bot): add poll proxying --- .../Types/Requests/ExecuteWebhookRequest.cs | 10 ++++++++ Myriad/Types/Message.cs | 15 ++++++++++++ PluralKit.Bot/Proxy/ProxyService.cs | 8 +++++-- .../Services/WebhookExecutorService.cs | 24 +++++++++++++++++++ 4 files changed, 55 insertions(+), 2 deletions(-) diff --git a/Myriad/Rest/Types/Requests/ExecuteWebhookRequest.cs b/Myriad/Rest/Types/Requests/ExecuteWebhookRequest.cs index 9378650e..93601076 100644 --- a/Myriad/Rest/Types/Requests/ExecuteWebhookRequest.cs +++ b/Myriad/Rest/Types/Requests/ExecuteWebhookRequest.cs @@ -13,4 +13,14 @@ public record ExecuteWebhookRequest public AllowedMentions? AllowedMentions { get; init; } public bool? Tts { get; init; } public Message.MessageFlags? Flags { get; set; } + public WebhookPoll? Poll { get; set; } + + public record WebhookPoll + { + public Message.PollMedia Question { get; init; } + public Message.PollAnswer[] Answers { get; init; } + public int? Duration { get; init; } + public bool AllowMultiselect { get; init; } + public int LayoutType { get; init; } + } } \ No newline at end of file diff --git a/Myriad/Types/Message.cs b/Myriad/Types/Message.cs index b12ed142..70013ecc 100644 --- a/Myriad/Types/Message.cs +++ b/Myriad/Types/Message.cs @@ -70,6 +70,8 @@ public record Message [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public Optional ReferencedMessage { get; init; } + public MessagePoll? Poll { get; init; } + // public MessageComponent[]? Components { get; init; } public record Reference(ulong? GuildId, ulong? ChannelId, ulong? MessageId); @@ -96,4 +98,17 @@ public record Message public bool Me { get; init; } public Emoji Emoji { get; init; } } + + public record PollMedia(string? Text, Emoji? Emoji); + + public record PollAnswer(PollMedia PollMedia); + + public record MessagePoll + { + public PollMedia Question { get; init; } + public PollAnswer[] Answers { get; init; } + public string? Expiry { get; init; } + public bool AllowMultiselect { get; init; } + public int LayoutType { get; init; } + } } \ No newline at end of file diff --git a/PluralKit.Bot/Proxy/ProxyService.cs b/PluralKit.Bot/Proxy/ProxyService.cs index b8a533ef..94e3b2af 100644 --- a/PluralKit.Bot/Proxy/ProxyService.cs +++ b/PluralKit.Bot/Proxy/ProxyService.cs @@ -189,9 +189,9 @@ public class ProxyService throw new ProxyChecksFailedException( "Your system has proxying disabled in this server. Type `pk;proxy on` to enable it."); - // Make sure we have either an attachment or message content + // Make sure we have an attachment, message content, or poll var isMessageBlank = msg.Content == null || msg.Content.Trim().Length == 0; - if (isMessageBlank && msg.Attachments.Length == 0) + if (isMessageBlank && msg.Attachments.Length == 0 && msg.Poll == null) throw new ProxyChecksFailedException("Message cannot be blank."); if (msg.Activity != null) @@ -242,6 +242,7 @@ public class ProxyService GuildId = trigger.GuildId!.Value, ChannelId = rootChannel.Id, ThreadId = threadId, + MessageId = trigger.Id, Name = await FixSameName(messageChannel.Id, ctx, match.Member), AvatarUrl = AvatarUtils.TryRewriteCdnUrl(match.Member.ProxyAvatar(ctx)), Content = content, @@ -252,6 +253,7 @@ public class ProxyService AllowEveryone = allowEveryone, Flags = trigger.Flags.HasFlag(Message.MessageFlags.VoiceMessage) ? Message.MessageFlags.VoiceMessage : null, Tts = tts, + Poll = trigger.Poll, }); await HandleProxyExecutedActions(ctx, autoproxySettings, trigger, proxyMessage, match); } @@ -310,6 +312,7 @@ public class ProxyService GuildId = guild.Id, ChannelId = rootChannel.Id, ThreadId = threadId, + MessageId = originalMsg.Id, Name = match.Member.ProxyName(ctx), AvatarUrl = AvatarUtils.TryRewriteCdnUrl(match.Member.ProxyAvatar(ctx)), Content = match.ProxyContent!, @@ -320,6 +323,7 @@ public class ProxyService AllowEveryone = allowEveryone, Flags = originalMsg.Flags.HasFlag(Message.MessageFlags.VoiceMessage) ? Message.MessageFlags.VoiceMessage : null, Tts = tts, + Poll = originalMsg.Poll, }); diff --git a/PluralKit.Bot/Services/WebhookExecutorService.cs b/PluralKit.Bot/Services/WebhookExecutorService.cs index fa38d22f..e45ca67c 100644 --- a/PluralKit.Bot/Services/WebhookExecutorService.cs +++ b/PluralKit.Bot/Services/WebhookExecutorService.cs @@ -4,6 +4,8 @@ using App.Metrics; using Humanizer; +using NodaTime.Text; + using Myriad.Cache; using Myriad.Extensions; using Myriad.Rest; @@ -35,6 +37,7 @@ public record ProxyRequest public ulong GuildId { get; init; } public ulong ChannelId { get; init; } public ulong? ThreadId { get; init; } + public ulong MessageId { get; init; } public string Name { get; init; } public string? AvatarUrl { get; init; } public string? Content { get; init; } @@ -45,6 +48,7 @@ public record ProxyRequest public bool AllowEveryone { get; init; } public Message.MessageFlags? Flags { get; init; } public bool Tts { get; init; } + public Message.MessagePoll? Poll { get; init; } } public class WebhookExecutorService @@ -154,6 +158,26 @@ public class WebhookExecutorService }).ToArray(); } + if (req.Poll is Message.MessagePoll poll) + { + int? duration = null; + if (poll.Expiry is string expiry) + { + var then = OffsetDateTimePattern.ExtendedIso.Parse(expiry).Value.ToInstant(); + var now = DiscordUtils.SnowflakeToInstant(req.MessageId); + // in theory .TotalHours should be exact, but just in case + duration = (int)Math.Round((then - now).TotalMinutes / 60.0); + } + webhookReq.Poll = new ExecuteWebhookRequest.WebhookPoll + { + Question = poll.Question, + Answers = poll.Answers, + Duration = duration, + AllowMultiselect = poll.AllowMultiselect, + LayoutType = poll.LayoutType + }; + } + Message webhookMessage; using (_metrics.Measure.Timer.Time(BotMetrics.WebhookResponseTime)) { From 3a4cc5b05edf3e6c69ac06a70a4cb13414b056cf Mon Sep 17 00:00:00 2001 From: Anna Date: Tue, 1 Oct 2024 08:43:01 -0400 Subject: [PATCH 08/54] fix(docs): correct avatar size restrictions --- docs/content/getting-started.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/content/getting-started.md b/docs/content/getting-started.md index 9aeee195..d6ae7e7a 100644 --- a/docs/content/getting-started.md +++ b/docs/content/getting-started.md @@ -61,7 +61,7 @@ If you don't have a link, you can leave that out entirely, and then **attach** t Avatars have some restrictions: - The image must be in **.jpg**, **.png**, or **.webp** format - The image must be under **1024 KB** in size -- The image must be below **1024 x 1024 pixels** in resolution (along the smallest axis). +- The image must be below **1000 x 1000 pixels** in resolution (along the smallest axis). - Animated GIFs are **not** supported (even if you have Nitro). ::: @@ -73,4 +73,4 @@ You could... - [configure privacy settings](/guide/#privacy) - or something else! -See the [User Guide](/guide) for a more complete reference of the bot's features. \ No newline at end of file +See the [User Guide](/guide) for a more complete reference of the bot's features. From 87d027f2d45326e44d751c4d4b1576fa79dff0bc Mon Sep 17 00:00:00 2001 From: Jake Fulmine Date: Tue, 1 Oct 2024 08:56:38 -0400 Subject: [PATCH 09/54] tweak: use global list formatting for member group lists (#628) * tweak: use global list formatting for member group lists * fix: use DisplayHid --- PluralKit.Bot/Commands/GroupMember.cs | 44 ++++++++++--------- PluralKit.Bot/Commands/Lists/ListOptions.cs | 2 + .../Database/Views/DatabaseViewsExt.cs | 9 +++- 3 files changed, 32 insertions(+), 23 deletions(-) diff --git a/PluralKit.Bot/Commands/GroupMember.cs b/PluralKit.Bot/Commands/GroupMember.cs index c2569d43..f3522df1 100644 --- a/PluralKit.Bot/Commands/GroupMember.cs +++ b/PluralKit.Bot/Commands/GroupMember.cs @@ -53,31 +53,33 @@ public class GroupMember public async Task ListMemberGroups(Context ctx, PKMember target) { - var pctx = ctx.DirectLookupContextFor(target.System); + var targetSystem = await ctx.Repository.GetSystem(target.System); + var opts = ctx.ParseListOptions(ctx.DirectLookupContextFor(target.System)); + opts.MemberFilter = target.Id; - var groups = await ctx.Repository.GetMemberGroups(target.Id) - .Where(g => g.Visibility.CanAccess(pctx)) - .OrderBy(g => (g.DisplayName ?? g.Name), StringComparer.InvariantCultureIgnoreCase) - .ToListAsync(); - - var description = ""; - var msg = ""; - - if (groups.Count == 0) - description = "This member has no groups."; - else - description = string.Join("\n", groups.Select(g => $"[`{g.DisplayHid(ctx.Config, isList: true)}`] **{g.DisplayName ?? g.Name}**")); - - if (pctx == LookupContext.ByOwner) + var title = new StringBuilder($"Groups containing {target.Name} (`{target.DisplayHid(ctx.Config)}`) in "); + if (ctx.Guild != null) { - msg += - $"\n\nTo add this member to one or more groups, use `pk;m {target.Reference(ctx)} group add [group 2] [group 3...]`"; - if (groups.Count > 0) - msg += - $"\nTo remove this member from one or more groups, use `pk;m {target.Reference(ctx)} group remove [group 2] [group 3...]`"; + var guildSettings = await ctx.Repository.GetSystemGuild(ctx.Guild.Id, targetSystem.Id); + if (guildSettings.DisplayName != null) + title.Append($"{guildSettings.DisplayName} (`{targetSystem.DisplayHid(ctx.Config)}`)"); + else if (targetSystem.NameFor(ctx) != null) + title.Append($"{targetSystem.NameFor(ctx)} (`{targetSystem.DisplayHid(ctx.Config)}`)"); + else + title.Append($"`{targetSystem.DisplayHid(ctx.Config)}`"); } + else + { + if (targetSystem.NameFor(ctx) != null) + title.Append($"{targetSystem.NameFor(ctx)} (`{targetSystem.DisplayHid(ctx.Config)}`)"); + else + title.Append($"`{targetSystem.DisplayHid(ctx.Config)}`"); + } + if (opts.Search != null) + title.Append($" matching **{opts.Search.Truncate(100)}**"); - await ctx.Reply(msg, new EmbedBuilder().Title($"{target.Name}'s groups").Description(description).Build()); + await ctx.RenderGroupList(ctx.LookupContextFor(target.System), target.System, title.ToString(), + target.Color, opts); } public async Task AddRemoveMembers(Context ctx, PKGroup target, Groups.AddRemoveOperation op) diff --git a/PluralKit.Bot/Commands/Lists/ListOptions.cs b/PluralKit.Bot/Commands/Lists/ListOptions.cs index ae2fb6a5..82aa0d67 100644 --- a/PluralKit.Bot/Commands/Lists/ListOptions.cs +++ b/PluralKit.Bot/Commands/Lists/ListOptions.cs @@ -29,6 +29,7 @@ public class ListOptions public PrivacyLevel? PrivacyFilter { get; set; } = PrivacyLevel.Public; public GroupId? GroupFilter { get; set; } + public MemberId? MemberFilter { get; set; } public string? Search { get; set; } public bool SearchDescription { get; set; } @@ -96,6 +97,7 @@ public class ListOptions { PrivacyFilter = PrivacyFilter, GroupFilter = GroupFilter, + MemberFilter = MemberFilter, Search = Search, SearchDescription = SearchDescription }; diff --git a/PluralKit.Core/Database/Views/DatabaseViewsExt.cs b/PluralKit.Core/Database/Views/DatabaseViewsExt.cs index f94aae61..761fc3d9 100644 --- a/PluralKit.Core/Database/Views/DatabaseViewsExt.cs +++ b/PluralKit.Core/Database/Views/DatabaseViewsExt.cs @@ -10,7 +10,11 @@ public static class DatabaseViewsExt public static Task> QueryGroupList(this IPKConnection conn, SystemId system, ListQueryOptions opts) { - StringBuilder query = new StringBuilder("select * from group_list where system = @system"); + StringBuilder query; + if (opts.MemberFilter == null) + query = new StringBuilder("select * from group_list where system = @system"); + else + query = new StringBuilder("select group_list.* from group_members inner join group_list on group_list.id = group_members.group_id where member_id = @MemberFilter"); if (opts.PrivacyFilter != null) query.Append($" and visibility = {(int)opts.PrivacyFilter}"); @@ -36,7 +40,7 @@ public static class DatabaseViewsExt return conn.QueryAsync( query.ToString(), - new { system, filter = opts.Search }); + new { system, filter = opts.Search, memberFilter = opts.MemberFilter }); } public static Task> QueryMemberList(this IPKConnection conn, SystemId system, ListQueryOptions opts) @@ -81,5 +85,6 @@ public static class DatabaseViewsExt public bool SearchDescription; public LookupContext Context; public GroupId? GroupFilter; + public MemberId? MemberFilter; } } \ No newline at end of file From 54dd38f2ae5bcd64c760afd98d7e1f53222e92e6 Mon Sep 17 00:00:00 2001 From: Jake Fulmine Date: Tue, 1 Oct 2024 20:30:43 -0400 Subject: [PATCH 10/54] fix: trim string fields before validating them --- dashboard/src/components/group/Edit.svelte | 6 +++--- dashboard/src/components/member/Edit.svelte | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/dashboard/src/components/group/Edit.svelte b/dashboard/src/components/group/Edit.svelte index c1958454..d51ec2eb 100644 --- a/dashboard/src/components/group/Edit.svelte +++ b/dashboard/src/components/group/Edit.svelte @@ -25,6 +25,9 @@ err = []; success = false; + // trim all string fields + Object.keys(data).forEach(k => data[k] = typeof data[k] == "string" ? data[k].trim() : data[k]); + if (!data.name) err.push("Group name cannot be empty.") if (data.color && !/^#?[A-Fa-f0-9]{6}$/.test(input.color)) { @@ -35,9 +38,6 @@ } } - // trim all string fields - Object.keys(data).forEach(k => data[k] = typeof data[k] == "string" ? data[k].trim() : data[k]); - err = err; if (err.length > 0) return; diff --git a/dashboard/src/components/member/Edit.svelte b/dashboard/src/components/member/Edit.svelte index bf9a2734..ccf12590 100644 --- a/dashboard/src/components/member/Edit.svelte +++ b/dashboard/src/components/member/Edit.svelte @@ -24,6 +24,9 @@ let data = input; err = []; + // trim all string fields + Object.keys(data).forEach(k => data[k] = typeof data[k] == "string" ? data[k].trim() : data[k]); + if (!data.name) err.push("Member name cannot be empty.") if (data.color && !/^#?[A-Fa-f0-9]{6}$/.test(input.color)) { @@ -58,9 +61,6 @@ } } - // trim all string fields - Object.keys(data).forEach(k => data[k] = typeof data[k] == "string" ? data[k].trim() : data[k]); - err = err; if (err.length > 0) return; From 913805f336a8f7098d0491758efbd42ca9495e57 Mon Sep 17 00:00:00 2001 From: Jake Fulmine Date: Tue, 1 Oct 2024 20:39:03 -0400 Subject: [PATCH 11/54] chore: update discord-markdown, add small text styling --- dashboard/package.json | 2 +- dashboard/styles/dark.scss | 4 ++++ dashboard/styles/generic.scss | 6 ++++++ dashboard/yarn.lock | 4 ++-- 4 files changed, 13 insertions(+), 3 deletions(-) diff --git a/dashboard/package.json b/dashboard/package.json index 225a8627..26725e70 100644 --- a/dashboard/package.json +++ b/dashboard/package.json @@ -27,7 +27,7 @@ "bootstrap": "^5.1.3", "bootstrap-dark-5": "^1.1.3", "core-js-pure": "^3.23.4", - "discord-markdown": "github:draconizations/discord-markdown#1f74a7094777d5bdfd123c0aac59d8b10db89b30", + "discord-markdown": "github:draconizations/discord-markdown#9d25e45015766779916baea52c37ae0fe12aac73", "gh-pages": "^3.2.3", "highlight.js": "^11.7.0", "import": "^0.0.6", diff --git a/dashboard/styles/dark.scss b/dashboard/styles/dark.scss index c8ce5947..0b69b8e0 100644 --- a/dashboard/styles/dark.scss +++ b/dashboard/styles/dark.scss @@ -52,4 +52,8 @@ .d-spoiler:active { color: $body-color-alt; //overwrite } + + small { + color: #707070; + } } \ No newline at end of file diff --git a/dashboard/styles/generic.scss b/dashboard/styles/generic.scss index 51ecc3d0..c63d2507 100644 --- a/dashboard/styles/generic.scss +++ b/dashboard/styles/generic.scss @@ -193,6 +193,12 @@ code { vertical-align: -0.1125em; } +small { + display: block; + color: #808080; + font-size: 0.5rem; +} + //twemoji img.emoji { height: 1.125em; diff --git a/dashboard/yarn.lock b/dashboard/yarn.lock index 0bdde3b5..62761637 100644 --- a/dashboard/yarn.lock +++ b/dashboard/yarn.lock @@ -341,9 +341,9 @@ detect-indent@^6.0.0: resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-6.1.0.tgz#592485ebbbf6b3b1ab2be175c8393d04ca0d57e6" integrity sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA== -"discord-markdown@github:draconizations/discord-markdown#1f74a7094777d5bdfd123c0aac59d8b10db89b30": +"discord-markdown@github:draconizations/discord-markdown#9d25e45015766779916baea52c37ae0fe12aac73": version "2.5.1" - resolved "https://codeload.github.com/draconizations/discord-markdown/tar.gz/1f74a7094777d5bdfd123c0aac59d8b10db89b30" + resolved "https://codeload.github.com/draconizations/discord-markdown/tar.gz/9d25e45015766779916baea52c37ae0fe12aac73" dependencies: js-base64 "^3.7.7" simple-markdown "^0.7.3" From debfc467760c6f2c17165205b889b44a14fe1276 Mon Sep 17 00:00:00 2001 From: Jake Fulmine Date: Tue, 1 Oct 2024 20:55:13 -0400 Subject: [PATCH 12/54] fix: correctly fall back to proxy avatar --- dashboard/src/components/common/CardsHeader.svelte | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/dashboard/src/components/common/CardsHeader.svelte b/dashboard/src/components/common/CardsHeader.svelte index d1e54e6a..ac842516 100644 --- a/dashboard/src/components/common/CardsHeader.svelte +++ b/dashboard/src/components/common/CardsHeader.svelte @@ -39,8 +39,6 @@ else icon_url = item.webhook_avatar_url ?? default_avatar } - $: icon_url_resized = icon_url ? resizeMedia(icon_url) : default_avatar - let avatarOpen = false; const toggleAvatarModal = () => (avatarOpen = !avatarOpen); @@ -65,8 +63,8 @@ ({item.id})
- {#if item && (item.avatar_url || item.icon)} - {if (event.key === "Enter") {avatarOpen = true}}} on:click|stopPropagation={toggleAvatarModal} class="rounded-circle avatar" src={icon_url_resized} alt={altText} /> + {#if item && (item.avatar_url || item.webhook_avatar_url || item.icon)} + {if (event.key === "Enter") {avatarOpen = true}}} on:click|stopPropagation={toggleAvatarModal} class="rounded-circle avatar" src={icon_url} alt={altText} /> {:else} icon (default) {/if} From bd67bc57e6a3485d2e64f924354744db8f69e8d1 Mon Sep 17 00:00:00 2001 From: rladenson Date: Thu, 3 Oct 2024 00:52:43 -0600 Subject: [PATCH 13/54] fix: respect sys name privacy in group embed --- PluralKit.Bot/Services/EmbedService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PluralKit.Bot/Services/EmbedService.cs b/PluralKit.Bot/Services/EmbedService.cs index 1ce59fbb..fa9d9a90 100644 --- a/PluralKit.Bot/Services/EmbedService.cs +++ b/PluralKit.Bot/Services/EmbedService.cs @@ -263,7 +263,7 @@ public class EmbedService else if (system.NameFor(ctx) != null) nameField = $"{nameField} ({system.NameFor(ctx)})"; else - nameField = $"{nameField} ({system.Name})"; + nameField = $"{nameField}"; var eb = new EmbedBuilder() .Author(new Embed.EmbedAuthor(nameField, IconUrl: target.IconFor(pctx), Url: $"https://dash.pluralkit.me/profile/g/{target.Hid}")) From 3d9be096cbc4dcc0910115483b9404d25cc4203d Mon Sep 17 00:00:00 2001 From: rladenson Date: Thu, 3 Oct 2024 00:59:31 -0600 Subject: [PATCH 14/54] fix: respect sys name privacy in group list --- PluralKit.Bot/Commands/Groups.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/PluralKit.Bot/Commands/Groups.cs b/PluralKit.Bot/Commands/Groups.cs index d2f421c7..7e4bc955 100644 --- a/PluralKit.Bot/Commands/Groups.cs +++ b/PluralKit.Bot/Commands/Groups.cs @@ -460,8 +460,8 @@ public class Groups { var title = new StringBuilder("Groups of "); - if (target.Name != null) - title.Append($"{target.Name} (`{target.DisplayHid(ctx.Config)}`)"); + if (target.NameFor(ctx) != null) + title.Append($"{target.NameFor(ctx)} (`{target.DisplayHid(ctx.Config)}`)"); else title.Append($"`{target.DisplayHid(ctx.Config)}`"); From 23fe9044645df5a6bc4ccf29344b18a1b5356831 Mon Sep 17 00:00:00 2001 From: Petal Ladenson Date: Thu, 3 Oct 2024 02:23:33 -0600 Subject: [PATCH 15/54] feat(bot): add -plaintext flag alongside -raw --- .../Context/ContextArgumentsExt.cs | 15 +- PluralKit.Bot/Commands/Groups.cs | 104 ++++++---- PluralKit.Bot/Commands/MemberEdit.cs | 132 ++++++++---- PluralKit.Bot/Commands/Message.cs | 39 ++-- PluralKit.Bot/Commands/SystemEdit.cs | 194 ++++++++++++------ 5 files changed, 322 insertions(+), 162 deletions(-) diff --git a/PluralKit.Bot/CommandSystem/Context/ContextArgumentsExt.cs b/PluralKit.Bot/CommandSystem/Context/ContextArgumentsExt.cs index 9a93440b..1a958c8e 100644 --- a/PluralKit.Bot/CommandSystem/Context/ContextArgumentsExt.cs +++ b/PluralKit.Bot/CommandSystem/Context/ContextArgumentsExt.cs @@ -91,8 +91,12 @@ public static class ContextArgumentsExt public static bool MatchClear(this Context ctx) => ctx.Match("clear", "reset", "default") || ctx.MatchFlag("c", "clear"); - public static bool MatchRaw(this Context ctx) => - ctx.Match("r", "raw") || ctx.MatchFlag("r", "raw"); + public static ReplyFormat MatchFormat(this Context ctx) + { + if (ctx.Match("r", "raw") || ctx.MatchFlag("r", "raw")) return ReplyFormat.Raw; + if (ctx.Match("pt", "plaintext") || ctx.MatchFlag("pt", "plaintext")) return ReplyFormat.Plaintext; + return ReplyFormat.Standard; + } public static bool MatchToggle(this Context ctx, bool? defaultValue = null) { @@ -184,4 +188,11 @@ public static class ContextArgumentsExt return groups; } +} + +public enum ReplyFormat +{ + Standard, + Raw, + Plaintext } \ No newline at end of file diff --git a/PluralKit.Bot/Commands/Groups.cs b/PluralKit.Bot/Commands/Groups.cs index 7e4bc955..36726056 100644 --- a/PluralKit.Bot/Commands/Groups.cs +++ b/PluralKit.Bot/Commands/Groups.cs @@ -132,40 +132,47 @@ public class Groups // No perms check, display name isn't covered by member privacy - if (ctx.MatchRaw()) - { + var format = ctx.MatchFormat(); + + // if there's nothing next or what's next is "raw"/"plaintext" we're doing a query, so check for null + if (!ctx.HasNext(false) || format != ReplyFormat.Standard) if (target.DisplayName == null) + { await ctx.Reply(noDisplayNameSetMessage); - else - await ctx.Reply($"```\n{target.DisplayName}\n```"); + return; + } + + if (format == ReplyFormat.Raw) + { + await ctx.Reply($"```\n{target.DisplayName}\n```"); + return; + } + if (format == ReplyFormat.Plaintext) + { + var eb = new EmbedBuilder() + .Description($"Showing displayname for group {target.Reference(ctx)}"); + await ctx.Reply(target.DisplayName, embed: eb.Build()); return; } if (!ctx.HasNext(false)) { - if (target.DisplayName == null) - { - await ctx.Reply(noDisplayNameSetMessage); - } - else - { - var eb = new EmbedBuilder() - .Field(new Embed.Field("Name", target.Name)) - .Field(new Embed.Field("Display Name", target.DisplayName)); + var eb = new EmbedBuilder() + .Field(new Embed.Field("Name", target.Name)) + .Field(new Embed.Field("Display Name", target.DisplayName)); - var reference = target.Reference(ctx); + var reference = target.Reference(ctx); - if (ctx.System?.Id == target.System) - eb.Description( - $"To change display name, type `pk;group {reference} displayname `.\n" - + $"To clear it, type `pk;group {reference} displayname -clear`.\n" - + $"To print the raw display name, type `pk;group {reference} displayname -raw`."); + if (ctx.System?.Id == target.System) + eb.Description( + $"To change display name, type `pk;group {reference} displayname `.\n" + + $"To clear it, type `pk;group {reference} displayname -clear`.\n" + + $"To print the raw display name, type `pk;group {reference} displayname -raw`."); - if (ctx.System?.Id == target.System) - eb.Footer(new Embed.EmbedFooter($"Using {target.DisplayName.Length}/{Limits.MaxGroupNameLength} characters.")); + if (ctx.System?.Id == target.System) + eb.Footer(new Embed.EmbedFooter($"Using {target.DisplayName.Length}/{Limits.MaxGroupNameLength} characters.")); - await ctx.Reply(embed: eb.Build()); - } + await ctx.Reply(embed: eb.Build()); return; } @@ -201,30 +208,41 @@ public class Groups noDescriptionSetMessage += $" To set one, type `pk;group {target.Reference(ctx)} description `."; - if (ctx.MatchRaw()) - { + var format = ctx.MatchFormat(); + + // if there's nothing next or what's next is "raw"/"plaintext" we're doing a query, so check for null + if (!ctx.HasNext(false) || format != ReplyFormat.Standard) if (target.Description == null) + { await ctx.Reply(noDescriptionSetMessage); - else - await ctx.Reply($"```\n{target.Description}\n```"); + return; + } + + if (format == ReplyFormat.Raw) + { + await ctx.Reply($"```\n{target.Description}\n```"); + return; + } + if (format == ReplyFormat.Plaintext) + { + var eb = new EmbedBuilder() + .Description($"Showing description for group {target.Reference(ctx)}"); + await ctx.Reply(target.Description, embed: eb.Build()); return; } if (!ctx.HasNext(false)) { - if (target.Description == null) - await ctx.Reply(noDescriptionSetMessage); - else - await ctx.Reply(embed: new EmbedBuilder() - .Title("Group description") - .Description(target.Description) - .Field(new Embed.Field("\u200B", - $"To print the description with formatting, type `pk;group {target.Reference(ctx)} description -raw`." - + (ctx.System?.Id == target.System - ? $" To clear it, type `pk;group {target.Reference(ctx)} description -clear`." - : "") - + $" Using {target.Description.Length}/{Limits.MaxDescriptionLength} characters.")) - .Build()); + await ctx.Reply(embed: new EmbedBuilder() + .Title("Group description") + .Description(target.Description) + .Field(new Embed.Field("\u200B", + $"To print the description with formatting, type `pk;group {target.Reference(ctx)} description -raw`." + + (ctx.System?.Id == target.System + ? $" To clear it, type `pk;group {target.Reference(ctx)} description -clear`." + : "") + + $" Using {target.Description.Length}/{Limits.MaxDescriptionLength} characters.")) + .Build()); return; } @@ -385,7 +403,7 @@ public class Groups public async Task GroupColor(Context ctx, PKGroup target) { var isOwnSystem = ctx.System?.Id == target.System; - var matchedRaw = ctx.MatchRaw(); + var matchedFormat = ctx.MatchFormat(); var matchedClear = ctx.MatchClear(); if (!isOwnSystem || !(ctx.HasNext() || matchedClear)) @@ -393,8 +411,10 @@ public class Groups if (target.Color == null) await ctx.Reply( "This group does not have a color set." + (isOwnSystem ? $" To set one, type `pk;group {target.Reference(ctx)} color `." : "")); - else if (matchedRaw) + else if (matchedFormat == ReplyFormat.Raw) await ctx.Reply("```\n#" + target.Color + "\n```"); + else if (matchedFormat == ReplyFormat.Plaintext) + await ctx.Reply(target.Color); else await ctx.Reply(embed: new EmbedBuilder() .Title("Group color") diff --git a/PluralKit.Bot/Commands/MemberEdit.cs b/PluralKit.Bot/Commands/MemberEdit.cs index 84afc443..3c19c171 100644 --- a/PluralKit.Bot/Commands/MemberEdit.cs +++ b/PluralKit.Bot/Commands/MemberEdit.cs @@ -70,30 +70,41 @@ public class MemberEdit noDescriptionSetMessage += $" To set one, type `pk;member {target.Reference(ctx)} description `."; - if (ctx.MatchRaw()) - { + var format = ctx.MatchFormat(); + + // if there's nothing next or what's next is "raw"/"plaintext" we're doing a query, so check for null + if (!ctx.HasNext(false) || format != ReplyFormat.Standard) if (target.Description == null) + { await ctx.Reply(noDescriptionSetMessage); - else - await ctx.Reply($"```\n{target.Description}\n```"); + return; + } + + if (format == ReplyFormat.Raw) + { + await ctx.Reply($"```\n{target.Description}\n```"); + return; + } + if (format == ReplyFormat.Plaintext) + { + var eb = new EmbedBuilder() + .Description($"Showing description for member {target.Reference(ctx)}"); + await ctx.Reply(target.Description, embed: eb.Build()); return; } if (!ctx.HasNext(false)) { - if (target.Description == null) - await ctx.Reply(noDescriptionSetMessage); - else - await ctx.Reply(embed: new EmbedBuilder() - .Title("Member description") - .Description(target.Description) - .Field(new Embed.Field("\u200B", - $"To print the description with formatting, type `pk;member {target.Reference(ctx)} description -raw`." - + (ctx.System?.Id == target.System - ? $" To clear it, type `pk;member {target.Reference(ctx)} description -clear`." - : "") - + $" Using {target.Description.Length}/{Limits.MaxDescriptionLength} characters.")) - .Build()); + await ctx.Reply(embed: new EmbedBuilder() + .Title("Member description") + .Description(target.Description) + .Field(new Embed.Field("\u200B", + $"To print the description with formatting, type `pk;member {target.Reference(ctx)} description -raw`." + + (ctx.System?.Id == target.System + ? $" To clear it, type `pk;member {target.Reference(ctx)} description -clear`." + : "") + + $" Using {target.Description.Length}/{Limits.MaxDescriptionLength} characters.")) + .Build()); return; } @@ -126,26 +137,37 @@ public class MemberEdit ctx.CheckSystemPrivacy(target.System, target.PronounPrivacy); - if (ctx.MatchRaw()) - { + var format = ctx.MatchFormat(); + + // if there's nothing next or what's next is "raw"/"plaintext" we're doing a query, so check for null + if (!ctx.HasNext(false) || format != ReplyFormat.Standard) if (target.Pronouns == null) + { await ctx.Reply(noPronounsSetMessage); - else - await ctx.Reply($"```\n{target.Pronouns}\n```"); + return; + } + + if (format == ReplyFormat.Raw) + { + await ctx.Reply($"```\n{target.Pronouns}\n```"); + return; + } + if (format == ReplyFormat.Plaintext) + { + var eb = new EmbedBuilder() + .Description($"Showing pronouns for member {target.Reference(ctx)}"); + await ctx.Reply(target.Pronouns, embed: eb.Build()); return; } if (!ctx.HasNext(false)) { - if (target.Pronouns == null) - await ctx.Reply(noPronounsSetMessage); - else - await ctx.Reply( - $"**{target.NameFor(ctx)}**'s pronouns are **{target.Pronouns}**.\nTo print the pronouns with formatting, type `pk;member {target.Reference(ctx)} pronouns -raw`." - + (ctx.System?.Id == target.System - ? $" To clear them, type `pk;member {target.Reference(ctx)} pronouns -clear`." - : "") - + $" Using {target.Pronouns.Length}/{Limits.MaxPronounsLength} characters."); + await ctx.Reply( + $"**{target.NameFor(ctx)}**'s pronouns are **{target.Pronouns}**.\nTo print the pronouns with formatting, type `pk;member {target.Reference(ctx)} pronouns -raw`." + + (ctx.System?.Id == target.System + ? $" To clear them, type `pk;member {target.Reference(ctx)} pronouns -clear`." + : "") + + $" Using {target.Pronouns.Length}/{Limits.MaxPronounsLength} characters."); return; } @@ -232,7 +254,7 @@ public class MemberEdit public async Task Color(Context ctx, PKMember target) { var isOwnSystem = ctx.System?.Id == target.System; - var matchedRaw = ctx.MatchRaw(); + var matchedFormat = ctx.MatchFormat(); var matchedClear = ctx.MatchClear(); if (!isOwnSystem || !(ctx.HasNext() || matchedClear)) @@ -240,8 +262,10 @@ public class MemberEdit if (target.Color == null) await ctx.Reply( "This member does not have a color set." + (isOwnSystem ? $" To set one, type `pk;member {target.Reference(ctx)} color `." : "")); - else if (matchedRaw) + else if (matchedFormat == ReplyFormat.Raw) await ctx.Reply("```\n#" + target.Color + "\n```"); + else if (matchedFormat == ReplyFormat.Plaintext) + await ctx.Reply(target.Color); else await ctx.Reply(embed: new EmbedBuilder() .Title("Member color") @@ -388,12 +412,26 @@ public class MemberEdit // No perms check, display name isn't covered by member privacy - if (ctx.MatchRaw()) - { + var format = ctx.MatchFormat(); + + // if what's next is "raw"/"plaintext" we need to check for null + if (format != ReplyFormat.Standard) if (target.DisplayName == null) + { await ctx.Reply(noDisplayNameSetMessage); - else - await ctx.Reply($"```\n{target.DisplayName}\n```"); + return; + } + + if (format == ReplyFormat.Raw) + { + await ctx.Reply($"```\n{target.DisplayName}\n```"); + return; + } + if (format == ReplyFormat.Plaintext) + { + var eb = new EmbedBuilder() + .Description($"Showing displayname for member {target.Reference(ctx)}"); + await ctx.Reply(target.DisplayName, embed: eb.Build()); return; } @@ -450,12 +488,26 @@ public class MemberEdit // No perms check, display name isn't covered by member privacy var memberGuildConfig = await ctx.Repository.GetMemberGuild(ctx.Guild.Id, target.Id); - if (ctx.MatchRaw()) - { + var format = ctx.MatchFormat(); + + // if what's next is "raw"/"plaintext" we need to check for null + if (format != ReplyFormat.Standard) if (memberGuildConfig.DisplayName == null) + { await ctx.Reply(noServerNameSetMessage); - else - await ctx.Reply($"```\n{memberGuildConfig.DisplayName}\n```"); + return; + } + + if (format == ReplyFormat.Raw) + { + await ctx.Reply($"```\n{memberGuildConfig.DisplayName}\n```"); + return; + } + if (format == ReplyFormat.Plaintext) + { + var eb = new EmbedBuilder() + .Description($"Showing servername for member {target.Reference(ctx)}"); + await ctx.Reply(memberGuildConfig.DisplayName, embed: eb.Build()); return; } diff --git a/PluralKit.Bot/Commands/Message.cs b/PluralKit.Bot/Commands/Message.cs index b45639d7..ca423f94 100644 --- a/PluralKit.Bot/Commands/Message.cs +++ b/PluralKit.Bot/Commands/Message.cs @@ -352,7 +352,9 @@ public class ProxiedMessage else if (!await ctx.CheckPermissionsInGuildChannel(channel, PermissionSet.ViewChannel)) showContent = false; - if (ctx.MatchRaw()) + var format = ctx.MatchFormat(); + + if (format != ReplyFormat.Standard) { var discordMessage = await _rest.GetMessageOrNull(message.Message.Channel, message.Message.Mid); if (discordMessage == null || !showContent) @@ -365,21 +367,32 @@ public class ProxiedMessage return; } - await ctx.Reply($"```{content}```"); - - if (Regex.IsMatch(content, "```.*```", RegexOptions.Singleline)) + if (format == ReplyFormat.Raw) { - var stream = new MemoryStream(Encoding.UTF8.GetBytes(content)); - await ctx.Rest.CreateMessage( - ctx.Channel.Id, - new MessageRequest - { - Content = $"{Emojis.Warn} Message contains codeblocks, raw source sent as an attachment." - }, - new[] { new MultipartFile("message.txt", stream, null, null, null) }); + await ctx.Reply($"```{content}```"); + + if (Regex.IsMatch(content, "```.*```", RegexOptions.Singleline)) + { + var stream = new MemoryStream(Encoding.UTF8.GetBytes(content)); + await ctx.Rest.CreateMessage( + ctx.Channel.Id, + new MessageRequest + { + Content = $"{Emojis.Warn} Message contains codeblocks, raw source sent as an attachment." + }, + new[] { new MultipartFile("message.txt", stream, null, null, null) }); + } + return; + } + + if (format == ReplyFormat.Plaintext) + { + var eb = new EmbedBuilder() + .Description($"Showing contents of message {message.Message.Mid}"); + await ctx.Reply(content, embed: eb.Build()); + return; } - return; } if (isDelete) diff --git a/PluralKit.Bot/Commands/SystemEdit.cs b/PluralKit.Bot/Commands/SystemEdit.cs index a3bcaa64..2ff0c528 100644 --- a/PluralKit.Bot/Commands/SystemEdit.cs +++ b/PluralKit.Bot/Commands/SystemEdit.cs @@ -37,24 +37,35 @@ public class SystemEdit if (isOwnSystem) noNameSetMessage += " Type `pk;system name ` to set one."; - if (ctx.MatchRaw()) - { - if (target.Name != null) - await ctx.Reply($"```\n{target.Name}\n```"); - else + var format = ctx.MatchFormat(); + + // if there's nothing next or what's next is "raw"/"plaintext" we're doing a query, so check for null + if (!ctx.HasNext(false) || format != ReplyFormat.Standard) + if (target.Name == null) + { await ctx.Reply(noNameSetMessage); + return; + } + + if (format == ReplyFormat.Raw) + { + await ctx.Reply($"` ``\n{target.Name}\n` ``"); + return; + } + if (format == ReplyFormat.Plaintext) + { + var eb = new EmbedBuilder() + .Description($"Showing name for system {target.DisplayHid()}"); + await ctx.Reply(target.Name, embed: eb.Build()); return; } if (!ctx.HasNext(false)) { - if (target.Name != null) - await ctx.Reply( - $"{(isOwnSystem ? "Your" : "This")} system's name is currently **{target.Name}**." - + (isOwnSystem ? " Type `pk;system name -clear` to clear it." : "") - + $" Using {target.Name.Length}/{Limits.MaxSystemNameLength} characters."); - else - await ctx.Reply(noNameSetMessage); + await ctx.Reply( + $"{(isOwnSystem ? "Your" : "This")} system's name is currently **{target.Name}**." + + (isOwnSystem ? " Type `pk;system name -clear` to clear it." : "") + + $" Using {target.Name.Length}/{Limits.MaxSystemNameLength} characters."); return; } @@ -91,24 +102,35 @@ public class SystemEdit var settings = await ctx.Repository.GetSystemGuild(ctx.Guild.Id, target.Id); - if (ctx.MatchRaw()) - { - if (settings.DisplayName != null) - await ctx.Reply($"```\n{settings.DisplayName}\n```"); - else + var format = ctx.MatchFormat(); + + // if there's nothing next or what's next is "raw"/"plaintext" we're doing a query, so check for null + if (!ctx.HasNext(false) || format != ReplyFormat.Standard) + if (settings.DisplayName == null) + { await ctx.Reply(noNameSetMessage); + return; + } + + if (format == ReplyFormat.Raw) + { + await ctx.Reply($"` ``\n{settings.DisplayName}\n` ``"); + return; + } + if (format == ReplyFormat.Plaintext) + { + var eb = new EmbedBuilder() + .Description($"Showing servername for system {target.DisplayHid()}"); + await ctx.Reply(settings.DisplayName, embed: eb.Build()); return; } if (!ctx.HasNext(false)) { - if (settings.DisplayName != null) - await ctx.Reply( - $"{(isOwnSystem ? "Your" : "This")} system's name for this server is currently **{settings.DisplayName}**." - + (isOwnSystem ? " Type `pk;system servername -clear` to clear it." : "") - + $" Using {settings.DisplayName.Length}/{Limits.MaxSystemNameLength} characters."); - else - await ctx.Reply(noNameSetMessage); + await ctx.Reply( + $"{(isOwnSystem ? "Your" : "This")} system's name for this server is currently **{settings.DisplayName}**." + + (isOwnSystem ? " Type `pk;system servername -clear` to clear it." : "") + + $" Using {settings.DisplayName.Length}/{Limits.MaxSystemNameLength} characters."); return; } @@ -143,28 +165,39 @@ public class SystemEdit if (isOwnSystem) noDescriptionSetMessage += " To set one, type `pk;s description `."; - if (ctx.MatchRaw()) - { + var format = ctx.MatchFormat(); + + // if there's nothing next or what's next is "raw"/"plaintext" we're doing a query, so check for null + if (!ctx.HasNext(false) || format != ReplyFormat.Standard) if (target.Description == null) + { await ctx.Reply(noDescriptionSetMessage); - else - await ctx.Reply($"```\n{target.Description}\n```"); + return; + } + + if (format == ReplyFormat.Raw) + { + await ctx.Reply($"` ``\n{target.Description}\n` ``"); + return; + } + if (format == ReplyFormat.Plaintext) + { + var eb = new EmbedBuilder() + .Description($"Showing description for system {target.DisplayHid()}"); + await ctx.Reply(target.Description, embed: eb.Build()); return; } if (!ctx.HasNext(false)) { - if (target.Description == null) - await ctx.Reply(noDescriptionSetMessage); - else - await ctx.Reply(embed: new EmbedBuilder() - .Title("System description") - .Description(target.Description) - .Footer(new Embed.EmbedFooter( - "To print the description with formatting, type `pk;s description -raw`." - + (isOwnSystem ? " To clear it, type `pk;s description -clear`. To change it, type `pk;s description `." : "") - + $" Using {target.Description.Length}/{Limits.MaxDescriptionLength} characters.")) - .Build()); + await ctx.Reply(embed: new EmbedBuilder() + .Title("System description") + .Description(target.Description) + .Footer(new Embed.EmbedFooter( + "To print the description with formatting, type `pk;s description -raw`." + + (isOwnSystem ? " To clear it, type `pk;s description -clear`. To change it, type `pk;s description `." : "") + + $" Using {target.Description.Length}/{Limits.MaxDescriptionLength} characters.")) + .Build()); return; } @@ -191,7 +224,7 @@ public class SystemEdit public async Task Color(Context ctx, PKSystem target) { var isOwnSystem = ctx.System?.Id == target.Id; - var matchedRaw = ctx.MatchRaw(); + var matchedFormat = ctx.MatchFormat(); var matchedClear = ctx.MatchClear(); if (!isOwnSystem || !(ctx.HasNext() || matchedClear)) @@ -199,8 +232,10 @@ public class SystemEdit if (target.Color == null) await ctx.Reply( "This system does not have a color set." + (isOwnSystem ? " To set one, type `pk;system color `." : "")); - else if (matchedRaw) + else if (matchedFormat == ReplyFormat.Raw) await ctx.Reply("```\n#" + target.Color + "\n```"); + else if (matchedFormat == ReplyFormat.Plaintext) + await ctx.Reply(target.Color); else await ctx.Reply(embed: new EmbedBuilder() .Title("System color") @@ -246,22 +281,33 @@ public class SystemEdit ? "You currently have no system tag set. To set one, type `pk;s tag `." : "This system currently has no system tag set."; - if (ctx.MatchRaw()) - { + var format = ctx.MatchFormat(); + + // if there's nothing next or what's next is "raw"/"plaintext" we're doing a query, so check for null + if (!ctx.HasNext(false) || format != ReplyFormat.Standard) if (target.Tag == null) + { await ctx.Reply(noTagSetMessage); - else - await ctx.Reply($"```\n{target.Tag}\n```"); + return; + } + + if (format == ReplyFormat.Raw) + { + await ctx.Reply($"```\n{target.Tag}\n```"); + return; + } + if (format == ReplyFormat.Plaintext) + { + var eb = new EmbedBuilder() + .Description($"Showing tag for system {target.DisplayHid()}"); + await ctx.Reply(target.Tag, embed: eb.Build()); return; } if (!ctx.HasNext(false)) { - if (target.Tag == null) - await ctx.Reply(noTagSetMessage); - else - await ctx.Reply($"{(isOwnSystem ? "Your" : "This system's")} current system tag is {target.Tag.AsCode()}." - + (isOwnSystem ? "To change it, type `pk;s tag `. To clear it, type `pk;s tag -clear`." : "")); + await ctx.Reply($"{(isOwnSystem ? "Your" : "This system's")} current system tag is {target.Tag.AsCode()}." + + (isOwnSystem ? "To change it, type `pk;s tag `. To clear it, type `pk;s tag -clear`." : "")); return; } @@ -296,15 +342,22 @@ public class SystemEdit var settings = await ctx.Repository.GetSystemGuild(ctx.Guild.Id, target.Id); - async Task Show(bool raw = false) + async Task Show(ReplyFormat format = ReplyFormat.Standard) { if (settings.Tag != null) { - if (raw) + if (format == ReplyFormat.Raw) { await ctx.Reply($"```{settings.Tag}```"); return; } + if (format == ReplyFormat.Plaintext) + { + var eb = new EmbedBuilder() + .Description($"Showing servertag for system {target.DisplayHid()}"); + await ctx.Reply(settings.Tag, embed: eb.Build()); + return; + } var msg = $"Your current system tag in '{ctx.Guild.Name}' is {settings.Tag.AsCode()}"; if (!settings.TagEnabled) @@ -400,8 +453,8 @@ public class SystemEdit await EnableDisable(false); else if (ctx.Match("enable") || ctx.MatchFlag("enable")) await EnableDisable(true); - else if (ctx.MatchRaw()) - await Show(true); + else if (ctx.MatchFormat() != ReplyFormat.Standard) + await Show(ctx.MatchFormat()); else if (!ctx.HasNext(false)) await Show(); else @@ -418,24 +471,35 @@ public class SystemEdit if (isOwnSystem) noPronounsSetMessage += " To set some, type `pk;system pronouns `"; - if (ctx.MatchRaw()) - { + var format = ctx.MatchFormat(); + + // if there's nothing next or what's next is "raw"/"plaintext" we're doing a query, so check for null + if (!ctx.HasNext(false) || format != ReplyFormat.Standard) if (target.Pronouns == null) + { await ctx.Reply(noPronounsSetMessage); - else - await ctx.Reply($"```\n{target.Pronouns}\n```"); + return; + } + + if (format == ReplyFormat.Raw) + { + await ctx.Reply($"```\n{target.Pronouns}\n```"); + return; + } + if (format == ReplyFormat.Plaintext) + { + var eb = new EmbedBuilder() + .Description($"Showing pronouns for system {target.DisplayHid()}"); + await ctx.Reply(target.Pronouns, embed: eb.Build()); return; } if (!ctx.HasNext(false)) { - if (target.Pronouns == null) - await ctx.Reply(noPronounsSetMessage); - else - await ctx.Reply($"{(isOwnSystem ? "Your" : "This system's")} current pronouns are **{target.Pronouns}**.\nTo print the pronouns with formatting, type `pk;system pronouns -raw`." - + (isOwnSystem ? " To clear them, type `pk;system pronouns -clear`." - : "") - + $" Using {target.Pronouns.Length}/{Limits.MaxPronounsLength} characters."); + await ctx.Reply($"{(isOwnSystem ? "Your" : "This system's")} current pronouns are **{target.Pronouns}**.\nTo print the pronouns with formatting, type `pk;system pronouns -raw`." + + (isOwnSystem ? " To clear them, type `pk;system pronouns -clear`." + : "") + + $" Using {target.Pronouns.Length}/{Limits.MaxPronounsLength} characters."); return; } From 4bd4407771ff7a853fb94ea626131b41e813a502 Mon Sep 17 00:00:00 2001 From: Petal Ladenson Date: Thu, 3 Oct 2024 02:29:38 -0600 Subject: [PATCH 16/54] feat(bot): add switch copying and more detailed switch editing --- PluralKit.Bot/CommandMeta/CommandHelp.cs | 3 +- PluralKit.Bot/CommandMeta/CommandTree.cs | 4 +- PluralKit.Bot/Commands/Switch.cs | 62 ++++++++++++++++++++++-- docs/content/command-list.md | 3 +- docs/content/tips-and-tricks.md | 4 ++ 5 files changed, 70 insertions(+), 6 deletions(-) diff --git a/PluralKit.Bot/CommandMeta/CommandHelp.cs b/PluralKit.Bot/CommandMeta/CommandHelp.cs index 5d3dc30e..843e2e1d 100644 --- a/PluralKit.Bot/CommandMeta/CommandHelp.cs +++ b/PluralKit.Bot/CommandMeta/CommandHelp.cs @@ -82,6 +82,7 @@ public partial class CommandTree public static Command SwitchMove = new Command("switch move", "switch move ", "Moves the latest switch in time"); public static Command SwitchEdit = new Command("switch edit", "switch edit [member 2] [member 3...]", "Edits the members in the latest switch"); public static Command SwitchEditOut = new Command("switch edit out", "switch edit out", "Turns the latest switch into a switch-out"); + public static Command SwitchCopy = new Command("switch copy", "switch copy [member 2] [member 3...]", "Makes a new switch with the listed members added"); public static Command SwitchDelete = new Command("switch delete", "switch delete", "Deletes the latest switch"); public static Command SwitchDeleteAll = new Command("switch delete", "switch delete all", "Deletes all logged switches"); public static Command Link = new Command("link", "link ", "Links your system to another account"); @@ -137,7 +138,7 @@ public partial class CommandTree public static Command[] SwitchCommands = { - Switch, SwitchOut, SwitchMove, SwitchEdit, SwitchEditOut, SwitchDelete, SwitchDeleteAll + Switch, SwitchOut, SwitchMove, SwitchEdit, SwitchEditOut, SwitchDelete, SwitchDeleteAll, SwitchCopy }; public static Command[] ConfigCommands = diff --git a/PluralKit.Bot/CommandMeta/CommandTree.cs b/PluralKit.Bot/CommandMeta/CommandTree.cs index 64d0a929..8864436c 100644 --- a/PluralKit.Bot/CommandMeta/CommandTree.cs +++ b/PluralKit.Bot/CommandMeta/CommandTree.cs @@ -430,13 +430,15 @@ public partial class CommandTree await ctx.Execute(SwitchEdit, m => m.SwitchEdit(ctx)); else if (ctx.Match("delete", "remove", "erase", "cancel", "yeet")) await ctx.Execute(SwitchDelete, m => m.SwitchDelete(ctx)); + else if (ctx.Match("copy", "add", "duplicate", "dupe")) + await ctx.Execute(SwitchCopy, m => m.SwitchEdit(ctx, true)); else if (ctx.Match("commands", "help")) await PrintCommandList(ctx, "switching", SwitchCommands); else if (ctx.HasNext()) // there are following arguments await ctx.Execute(Switch, m => m.SwitchDo(ctx)); else await PrintCommandNotFoundError(ctx, Switch, SwitchOut, SwitchMove, SwitchEdit, SwitchEditOut, - SwitchDelete, SystemFronter, SystemFrontHistory); + SwitchDelete, SwitchCopy, SystemFronter, SystemFrontHistory); } private async Task CommandHelpRoot(Context ctx) diff --git a/PluralKit.Bot/Commands/Switch.cs b/PluralKit.Bot/Commands/Switch.cs index 9272d911..d60da3b4 100644 --- a/PluralKit.Bot/Commands/Switch.cs +++ b/PluralKit.Bot/Commands/Switch.cs @@ -7,6 +7,7 @@ namespace PluralKit.Bot; public class Switch { + public async Task SwitchDo(Context ctx) { ctx.CheckSystem(); @@ -103,12 +104,67 @@ public class Switch await ctx.Reply($"{Emojis.Success} Switch moved to ({newSwitchDeltaStr} ago)."); } - public async Task SwitchEdit(Context ctx) + public async Task SwitchEdit(Context ctx, bool newSwitch = false) { ctx.CheckSystem(); - var members = await ctx.ParseMemberList(ctx.System.Id); - await DoEditCommand(ctx, members); + var newMembers = await ctx.ParseMemberList(ctx.System.Id); + + await using var conn = await ctx.Database.Obtain(); + var currentSwitch = await ctx.Repository.GetLatestSwitch(ctx.System.Id); + var currentSwitchMembers = await ctx.Repository.GetSwitchMembers(conn, currentSwitch.Id).ToListAsync().AsTask(); + + if (ctx.MatchFlag("first", "f")) + newMembers = FirstInSwitch(newMembers[0], currentSwitchMembers); + else if (ctx.MatchFlag("remove", "r")) + newMembers = RemoveFromSwitch(newMembers, currentSwitchMembers); + else if (ctx.MatchFlag("append", "a")) + newMembers = AppendToSwitch(newMembers, currentSwitchMembers); + else if (ctx.MatchFlag("prepend", "p")) + newMembers = PrependToSwitch(newMembers, currentSwitchMembers); + + if (newSwitch) + { + // if there's no edit flag, assume we're appending + if (!ctx.MatchFlag("first", "f", "remove", "r", "append", "a", "prepend", "p")) + newMembers = AppendToSwitch(newMembers, currentSwitchMembers); + await DoSwitchCommand(ctx, newMembers); + } + else + await DoEditCommand(ctx, newMembers); + } + + public List PrependToSwitch(List members, List currentSwitchMembers) + { + members.AddRange(currentSwitchMembers); + + return members; + } + + public List AppendToSwitch(List members, List currentSwitchMembers) + { + currentSwitchMembers.AddRange(members); + members = currentSwitchMembers; + + return members; + } + + public List RemoveFromSwitch(List members, List currentSwitchMembers) + { + var memberIds = members.Select(m => m.Id.Value); + currentSwitchMembers = currentSwitchMembers.Where(m => !memberIds.Contains(m.Id.Value)).ToList(); + members = currentSwitchMembers; + + return members; + } + + public List FirstInSwitch(PKMember member, List currentSwitchMembers) + { + currentSwitchMembers = currentSwitchMembers.Where(m => m.Id != member.Id).ToList(); + var members = new List { member }; + members.AddRange(currentSwitchMembers); + + return members; } public async Task SwitchEditOut(Context ctx) diff --git a/docs/content/command-list.md b/docs/content/command-list.md index 0df96f73..610b8858 100644 --- a/docs/content/command-list.md +++ b/docs/content/command-list.md @@ -123,7 +123,8 @@ You can have a space after `pk;`, e.g. `pk;system` and `pk; system` will do the ## Switching commands - `pk;switch [member...]` - Registers a switch with the given members. - `pk;switch out` - Registers a 'switch-out' - a switch with no associated members. -- `pk;switch edit ` - Edits the members in the latest switch. +- `pk;switch edit ` - Edits the members in the latest switch. +- `pk;switch add ` - Makes a new switch based off the current switch with the listed members added or removed. - `pk;switch move