From c3cc5c9d03bcfba193daac5ec0729da748f2e953 Mon Sep 17 00:00:00 2001 From: alyssa Date: Fri, 13 Sep 2024 16:02:30 +0900 Subject: [PATCH 001/179] init rust command parser --- .gitignore | 3 +- Cargo.lock | 381 ++++++++++++++++-- Cargo.toml | 1 + .../CommandSystem/Context/Context.cs | 14 +- PluralKit.Bot/CommandSystem/ParametersFFI.cs | 66 +++ PluralKit.Bot/PluralKit.Bot.csproj | 1 + lib/command_system_macros/Cargo.toml | 7 + lib/command_system_macros/src/lib.rs | 109 +++++ lib/commands/Cargo.toml | 16 + lib/commands/build.rs | 3 + lib/commands/src/commands.udl | 13 + lib/commands/src/lib.rs | 208 ++++++++++ lib/commands/src/string.rs | 87 ++++ lib/commands/src/token.rs | 84 ++++ lib/commands/uniffi.toml | 2 + 15 files changed, 968 insertions(+), 27 deletions(-) create mode 100644 PluralKit.Bot/CommandSystem/ParametersFFI.cs create mode 100644 lib/command_system_macros/Cargo.toml create mode 100644 lib/command_system_macros/src/lib.rs create mode 100644 lib/commands/Cargo.toml create mode 100644 lib/commands/build.rs create mode 100644 lib/commands/src/commands.udl create mode 100644 lib/commands/src/lib.rs create mode 100644 lib/commands/src/string.rs create mode 100644 lib/commands/src/token.rs create mode 100644 lib/commands/uniffi.toml diff --git a/.gitignore b/.gitignore index d3ef7ada..94e68792 100644 --- a/.gitignore +++ b/.gitignore @@ -28,4 +28,5 @@ pluralkit.*.conf logs/ .version recipe.json -.docker-bin/ \ No newline at end of file +.docker-bin/ +PluralKit.Bot/commands.cs diff --git a/Cargo.lock b/Cargo.lock index 76f6764f..abc1e061 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -112,6 +112,47 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f907281554a3d0312bb7aab855a8e0ef6cbf1614d06de54105039ca8b34460e" +[[package]] +name = "askama" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b79091df18a97caea757e28cd2d5fda49c6cd4bd01ddffd7ff01ace0c0ad2c28" +dependencies = [ + "askama_derive", + "askama_escape", +] + +[[package]] +name = "askama_derive" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19fe8d6cb13c4714962c072ea496f3392015f0989b1a2847bb4b2d9effd71d83" +dependencies = [ + "askama_parser", + "basic-toml", + "mime", + "mime_guess", + "proc-macro2", + "quote", + "serde", + "syn 2.0.77", +] + +[[package]] +name = "askama_escape" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "619743e34b5ba4e9703bba34deac3427c72507c7159f5fd030aea8cac0cfe341" + +[[package]] +name = "askama_parser" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acb1161c6b64d1c3d83108213c2a2533a342ac225aabd0bda218278c2ddb00c0" +dependencies = [ + "nom", +] + [[package]] name = "async-trait" version = "0.1.80" @@ -120,7 +161,7 @@ checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.77", ] [[package]] @@ -293,6 +334,24 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" +[[package]] +name = "basic-toml" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "823388e228f614e9558c6804262db37960ec8821856535f5c3f59913140558f8" +dependencies = [ + "serde", +] + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -354,6 +413,38 @@ dependencies = [ "either", ] +[[package]] +name = "camino" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b96ec4966b5813e2c0507c1f86115c8c5abaadc3980879c3424042a02fd1ad3" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo-platform" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24b1f0365a6c6bb4020cd05806fd0d33c44d38046b8bd7f0e40814b9763cabfc" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eee4243f1f26fc7a42710e7439c149e2b10b05472f88090acce52632f231a73a" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror", +] + [[package]] name = "cc" version = "1.0.98" @@ -378,6 +469,19 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "command_system_macros" +version = "0.1.0" + +[[package]] +name = "commands" +version = "0.1.0" +dependencies = [ + "command_system_macros", + "lazy_static", + "uniffi", +] + [[package]] name = "config" version = "0.13.3" @@ -572,7 +676,7 @@ dependencies = [ "heck 0.4.1", "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.77", ] [[package]] @@ -697,6 +801,15 @@ dependencies = [ "url", ] +[[package]] +name = "fs-err" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a41f105fe1d5b6b34b2055e3dc59bb79b46b48b2040b9e6c7b4b5de097aa41" +dependencies = [ + "autocfg", +] + [[package]] name = "futures" version = "0.3.30" @@ -764,7 +877,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.77", ] [[package]] @@ -834,6 +947,23 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + +[[package]] +name = "goblin" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d6b4de4a8eb6c46a8c77e1d3be942cb9a8bf073c22374578e5ba4b08ed0ff68" +dependencies = [ + "log", + "plain", + "scroll", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -1374,9 +1504,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.5.0" +version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "memoffset" @@ -1451,6 +1581,16 @@ version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -1585,6 +1725,12 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +[[package]] +name = "oneshot-uniffi" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c548d5c78976f6955d72d0ced18c48ca07030f7a1d4024529fedd7c1c01b29c" + [[package]] name = "opaque-debug" version = "0.3.0" @@ -1795,6 +1941,12 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + [[package]] name = "portable-atomic" version = "0.3.19" @@ -1824,7 +1976,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f12335488a2f3b0a83b14edad48dca9879ce89b2edd10e80237e4e852dd645e" dependencies = [ "proc-macro2", - "syn 2.0.66", + "syn 2.0.77", ] [[package]] @@ -1863,7 +2015,7 @@ dependencies = [ "prost", "prost-types", "regex", - "syn 2.0.66", + "syn 2.0.77", "tempfile", ] @@ -1877,7 +2029,7 @@ dependencies = [ "itertools", "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.77", ] [[package]] @@ -2051,14 +2203,14 @@ dependencies = [ [[package]] name = "regex" -version = "1.9.4" +version = "1.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12de2eff854e5fa4b1295edd650e227e9d8fb0c9e90b12e7f36d6a6811791a29" +checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.3.7", - "regex-syntax 0.7.5", + "regex-automata 0.4.7", + "regex-syntax 0.8.4", ] [[package]] @@ -2072,13 +2224,13 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.3.7" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49530408a136e16e5b486e883fbb6ba058e8e4e8ae6621a77b048b314336e629" +checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.7.5", + "regex-syntax 0.8.4", ] [[package]] @@ -2089,9 +2241,9 @@ checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" [[package]] name = "regex-syntax" -version = "0.7.5" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" +checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" [[package]] name = "reqwest" @@ -2289,11 +2441,34 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" +[[package]] +name = "scroll" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04c565b551bafbef4157586fa379538366e4385d42082f255bfd96e4fe8519da" +dependencies = [ + "scroll_derive", +] + +[[package]] +name = "scroll_derive" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1db149f81d46d2deba7cd3c50772474707729550221e69588478ebf9ada425ae" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.77", +] + [[package]] name = "semver" version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "58bc9567378fc7690d6b2addae4e60ac2eeea07becb2c64b9f218b53865cba2a" +dependencies = [ + "serde", +] [[package]] name = "serde" @@ -2312,7 +2487,7 @@ checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.77", ] [[package]] @@ -2410,6 +2585,12 @@ dependencies = [ "rand_core", ] +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + [[package]] name = "sketches-ddsketch" version = "0.2.0" @@ -2684,6 +2865,12 @@ dependencies = [ "urlencoding", ] +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "stringprep" version = "0.1.5" @@ -2714,9 +2901,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.66" +version = "2.0.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5" +checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" dependencies = [ "proc-macro2", "quote", @@ -2829,7 +3016,7 @@ checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.77", ] [[package]] @@ -3036,6 +3223,15 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e79c4d996edb816c91e4308506774452e55e95c3c9de07b6729e17e15a5ef81" +[[package]] +name = "unicase" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" +dependencies = [ + "version_check", +] + [[package]] name = "unicode-bidi" version = "0.3.10" @@ -3075,6 +3271,134 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" +[[package]] +name = "uniffi" +version = "0.25.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21345172d31092fd48c47fd56c53d4ae9e41c4b1f559fb8c38c1ab1685fd919f" +dependencies = [ + "anyhow", + "uniffi_build", + "uniffi_core", + "uniffi_macros", +] + +[[package]] +name = "uniffi_bindgen" +version = "0.25.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd992f2929a053829d5875af1eff2ee3d7a7001cb3b9a46cc7895f2caede6940" +dependencies = [ + "anyhow", + "askama", + "camino", + "cargo_metadata", + "fs-err", + "glob", + "goblin", + "heck 0.4.1", + "once_cell", + "paste", + "serde", + "toml", + "uniffi_meta", + "uniffi_testing", + "uniffi_udl", +] + +[[package]] +name = "uniffi_build" +version = "0.25.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "001964dd3682d600084b3aaf75acf9c3426699bc27b65e96bb32d175a31c74e9" +dependencies = [ + "anyhow", + "camino", + "uniffi_bindgen", +] + +[[package]] +name = "uniffi_checksum_derive" +version = "0.25.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55137c122f712d9330fd985d66fa61bdc381752e89c35708c13ce63049a3002c" +dependencies = [ + "quote", + "syn 2.0.77", +] + +[[package]] +name = "uniffi_core" +version = "0.25.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6121a127a3af1665cd90d12dd2b3683c2643c5103281d0fed5838324ca1fad5b" +dependencies = [ + "anyhow", + "bytes", + "camino", + "log", + "once_cell", + "oneshot-uniffi", + "paste", + "static_assertions", +] + +[[package]] +name = "uniffi_macros" +version = "0.25.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11cf7a58f101fcedafa5b77ea037999b88748607f0ef3a33eaa0efc5392e92e4" +dependencies = [ + "bincode", + "camino", + "fs-err", + "once_cell", + "proc-macro2", + "quote", + "serde", + "syn 2.0.77", + "toml", + "uniffi_build", + "uniffi_meta", +] + +[[package]] +name = "uniffi_meta" +version = "0.25.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71dc8573a7b1ac4b71643d6da34888273ebfc03440c525121f1b3634ad3417a2" +dependencies = [ + "anyhow", + "bytes", + "siphasher", + "uniffi_checksum_derive", +] + +[[package]] +name = "uniffi_testing" +version = "0.25.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "118448debffcb676ddbe8c5305fb933ab7e0123753e659a71dc4a693f8d9f23c" +dependencies = [ + "anyhow", + "camino", + "cargo_metadata", + "fs-err", + "once_cell", +] + +[[package]] +name = "uniffi_udl" +version = "0.25.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "889edb7109c6078abe0e53e9b4070cf74a6b3468d141bdf5ef1bd4d1dc24a1c3" +dependencies = [ + "anyhow", + "uniffi_meta", + "uniffi_testing", + "weedle2", +] + [[package]] name = "untrusted" version = "0.9.0" @@ -3165,7 +3489,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.77", "wasm-bindgen-shared", ] @@ -3199,7 +3523,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.77", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -3229,6 +3553,15 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "weedle2" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e79c5206e1f43a2306fd64bdb95025ee4228960f2e6c5a8b173f3caaf807741" +dependencies = [ + "nom", +] + [[package]] name = "whoami" version = "1.5.1" @@ -3555,7 +3888,7 @@ checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.77", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index cd075886..3866b321 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [workspace] members = [ "./lib/libpk", + "./lib/commands", "./services/api", "./services/dispatch" ] diff --git a/PluralKit.Bot/CommandSystem/Context/Context.cs b/PluralKit.Bot/CommandSystem/Context/Context.cs index 99d4a39b..f5a6c9fb 100644 --- a/PluralKit.Bot/CommandSystem/Context/Context.cs +++ b/PluralKit.Bot/CommandSystem/Context/Context.cs @@ -45,9 +45,19 @@ public class Context _provider = provider; _commandMessageService = provider.Resolve(); CommandPrefix = message.Content?.Substring(0, commandParseOffset); - Parameters = new Parameters(message.Content?.Substring(commandParseOffset)); Rest = provider.Resolve(); Cluster = provider.Resolve(); + + try + { + Parameters = new ParametersFFI(message.Content?.Substring(commandParseOffset)); + } + catch (PKError e) + { + // todo: not this + Reply($"{Emojis.Error} {e.Message}"); + throw; + } } public readonly IDiscordCache Cache; @@ -71,7 +81,7 @@ public class Context public DateTimeZone Zone => Config?.Zone ?? DateTimeZone.Utc; public readonly string CommandPrefix; - public readonly Parameters Parameters; + public readonly ParametersFFI Parameters; internal readonly IDatabase Database; internal readonly ModelRepository Repository; diff --git a/PluralKit.Bot/CommandSystem/ParametersFFI.cs b/PluralKit.Bot/CommandSystem/ParametersFFI.cs new file mode 100644 index 00000000..dcd1097b --- /dev/null +++ b/PluralKit.Bot/CommandSystem/ParametersFFI.cs @@ -0,0 +1,66 @@ +using uniffi.commands; + +namespace PluralKit.Bot; + +public class ParametersFFI +{ + private string _cb { get; init; } + private List _args { get; init; } + public int _ptr = -1; + private Dictionary _flags { get; init; } + + // just used for errors, temporarily + public string FullCommand { get; init; } + + public ParametersFFI(string cmd) + { + FullCommand = cmd; + var result = CommandsMethods.ParseCommand(cmd); + if (result is CommandResult.Ok) + { + var command = ((CommandResult.Ok)result).@command; + _cb = command.@commandRef; + _args = command.@args; + _flags = command.@flags; + } + else + { + throw new PKError(((CommandResult.Err)result).@error); + } + } + + public string Pop() + { + if (_args.Count > _ptr + 1) Console.WriteLine($"pop: {_ptr + 1}, {_args[_ptr + 1]}"); + else Console.WriteLine("pop: no more arguments"); + if (_args.Count() == _ptr + 1) return ""; + _ptr++; + return _args[_ptr]; + } + + public string Peek() + { + if (_args.Count > _ptr + 1) Console.WriteLine($"peek: {_ptr + 1}, {_args[_ptr + 1]}"); + else Console.WriteLine("peek: no more arguments"); + if (_args.Count() == _ptr + 1) return ""; + return _args[_ptr + 1]; + } + + // this might not work quite right + public string PeekWithPtr(ref int ptr) + { + return _args[ptr]; + } + + public ISet Flags() + { + return new HashSet(_flags.Keys); + } + + // parsed differently in new commands, does this work right? + // note: skipFlags here does nothing + public string Remainder(bool skipFlags = false) + { + return Pop(); + } +} \ No newline at end of file diff --git a/PluralKit.Bot/PluralKit.Bot.csproj b/PluralKit.Bot/PluralKit.Bot.csproj index 479a50a1..27e61fa3 100644 --- a/PluralKit.Bot/PluralKit.Bot.csproj +++ b/PluralKit.Bot/PluralKit.Bot.csproj @@ -4,6 +4,7 @@ Exe net6.0 annotations + true enable diff --git a/lib/command_system_macros/Cargo.toml b/lib/command_system_macros/Cargo.toml new file mode 100644 index 00000000..cf660792 --- /dev/null +++ b/lib/command_system_macros/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "command_system_macros" +version = "0.1.0" +edition = "2021" + +[lib] +proc-macro = true diff --git a/lib/command_system_macros/src/lib.rs b/lib/command_system_macros/src/lib.rs new file mode 100644 index 00000000..854844c6 --- /dev/null +++ b/lib/command_system_macros/src/lib.rs @@ -0,0 +1,109 @@ +use proc_macro::{Delimiter, TokenStream, TokenTree}; + +fn make_command(tokens: Vec, help: String, cb: String) -> String { + let tokens = tokens + .iter() + .map(|v| format!("Token::{v}")) + .collect::>() + .join(","); + format!( + r#"Command {{ tokens: vec![{tokens}], help: {help}.to_string(), cb: "{cb}".to_string() }}"# + ) +} + +fn command_from_stream(stream: TokenStream) -> String { + let mut part = 0; + let mut found_tokens: Vec = Vec::new(); + let mut found_cb: Option = None; + let mut found_help: Option = None; + + let mut is_token_lit = false; + let mut tokens = stream.clone().into_iter(); + 'a: loop { + let cur_token = tokens.next(); + match cur_token { + None if part == 2 && found_help.is_some() => break 'a, + Some(TokenTree::Ident(ident)) if part == 0 => { + found_tokens.push(if is_token_lit { + format!("{ident}") + } else { + format!("Value(vec![\"{ident}\".to_string()])") + }); + // reset this + is_token_lit = false; + } + Some(TokenTree::Punct(punct)) if part == 0 && format!("{punct}") == "@" => { + is_token_lit = true + } + Some(TokenTree::Punct(punct)) + if ((part == 0 && found_tokens.len() > 0) || (part == 1 && found_cb.is_some())) + && format!("{punct}") == "," => + { + part += 1 + } + Some(TokenTree::Ident(ident)) if part == 1 => found_cb = Some(format!("{ident}")), + Some(TokenTree::Literal(lit)) if part == 2 => found_help = Some(format!("{lit}")), + _ => panic!("invalid command definition: {stream}"), + } + } + make_command(found_tokens, found_help.unwrap(), found_cb.unwrap()) +} + +#[proc_macro] +pub fn commands(stream: TokenStream) -> TokenStream { + let mut commands: Vec = Vec::new(); + + let mut top_level_tokens = stream.into_iter(); + 'a: loop { + // "command" + match top_level_tokens.next() { + Some(TokenTree::Ident(ident)) if format!("{ident}") == "command" => {} + None => break 'a, + _ => panic!("contents of commands! macro is invalid"), + } + // + match top_level_tokens.next() { + Some(TokenTree::Group(group)) if group.delimiter() == Delimiter::Parenthesis => { + commands.push(command_from_stream(group.stream())); + } + _ => panic!("contents of commands! macro is invalid"), + } + // ; + match top_level_tokens.next() { + Some(TokenTree::Punct(punct)) if format!("{punct}") == ";" => {} + _ => panic!("contents of commands! macro is invalid"), + } + } + + let command_registrations = commands + .iter() + .map(|v| format!("tree.register_command({v});")) + .collect::>() + .join("\n"); + + let res = format!( + r#" +lazy_static::lazy_static! {{ + static ref COMMAND_TREE: TreeBranch = {{ + let mut tree = TreeBranch {{ + current_command_key: None, + possible_tokens: vec![], + branches: HashMap::new(), + }}; + + {command_registrations} + + tree.sort_tokens(); + + // println!("{{tree:#?}}"); + + tree + }}; +}} + "# + ); + + // panic!("{res}"); + + res.parse().unwrap() +} diff --git a/lib/commands/Cargo.toml b/lib/commands/Cargo.toml new file mode 100644 index 00000000..1d1ac480 --- /dev/null +++ b/lib/commands/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "commands" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +lazy_static = { workspace = true } +command_system_macros = { path = "../command_system_macros" } + +uniffi = { version = "0.25" } + +[build-dependencies] +uniffi = { version = "0.25", features = [ "build" ] } diff --git a/lib/commands/build.rs b/lib/commands/build.rs new file mode 100644 index 00000000..3f31f453 --- /dev/null +++ b/lib/commands/build.rs @@ -0,0 +1,3 @@ +fn main() { + uniffi::generate_scaffolding("src/commands.udl").unwrap(); +} diff --git a/lib/commands/src/commands.udl b/lib/commands/src/commands.udl new file mode 100644 index 00000000..cc7af428 --- /dev/null +++ b/lib/commands/src/commands.udl @@ -0,0 +1,13 @@ +namespace commands { + CommandResult parse_command(string input); +}; +[Enum] +interface CommandResult { + Ok(ParsedCommand command); + Err(string error); +}; +dictionary ParsedCommand { + string command_ref; + sequence args; + record flags; +}; diff --git a/lib/commands/src/lib.rs b/lib/commands/src/lib.rs new file mode 100644 index 00000000..0217dec7 --- /dev/null +++ b/lib/commands/src/lib.rs @@ -0,0 +1,208 @@ +#![feature(let_chains)] + +use core::panic; +use std::{cmp::Ordering, collections::HashMap}; + +uniffi::include_scaffolding!("commands"); + +mod string; +mod token; +use token::*; + +use command_system_macros::commands; + +// todo!: move all this stuff into a different file +// lib.rs should just have exported symbols and command definitions + +#[derive(Debug, Clone)] +struct TreeBranch { + current_command_key: Option, + /// branches.keys(), but sorted by specificity + possible_tokens: Vec, + branches: HashMap, +} + +impl TreeBranch { + fn register_command(&mut self, command: Command) { + let mut current_branch = self; + // iterate over tokens in command + for token in command.tokens { + // recursively get or create a sub-branch for each token + current_branch = current_branch.branches.entry(token).or_insert(TreeBranch { + current_command_key: None, + possible_tokens: vec![], + branches: HashMap::new(), + }) + } + // when we're out of tokens, add an Empty branch with the callback and no sub-branches + current_branch.branches.insert( + Token::Empty, + TreeBranch { + current_command_key: Some(command.cb), + possible_tokens: vec![], + branches: HashMap::new(), + }, + ); + } + + fn sort_tokens(&mut self) { + for branch in self.branches.values_mut() { + branch.sort_tokens(); + } + + // put Value tokens at the end + // i forget exactly how this works + // todo!: document this before PR mergs + self.possible_tokens = self + .branches + .keys() + .into_iter() + .map(|v| v.clone()) + .collect(); + self.possible_tokens.sort_by(|v, _| { + if matches!(v, Token::Value(_)) { + Ordering::Greater + } else { + Ordering::Less + } + }); + } +} + +struct Command { + tokens: Vec, + help: String, + cb: String, +} + +// todo: aliases +// todo: categories +commands! { + command(help, help, "Shows the help command"); + + command(member new, member_new, "Creates a new system member"); + command(member @MemberRef, member_show, "Shows information about a member"); + command(member @MemberRef description, member_desc_show, "Shows a member's description"); + command(member @MemberRef description @FullString, member_desc_update, "Changes a member's description"); + command(member @MemberRef privacy, member_privacy_show, "Displays a member's current privacy settings"); + command(member @MemberRef privacy @MemberPrivacyTarget @PrivacyLevel, member_privacy_update, "Changes a member's privacy settings"); +} + +pub enum CommandResult { + Ok { command: ParsedCommand }, + Err { error: String }, +} + +pub struct ParsedCommand { + pub command_ref: String, + pub args: Vec, + pub flags: HashMap>, +} + +/// Find the next token from an either raw or partially parsed command string +/// +/// Returns: +/// - matched token, to move deeper into the tree +/// - matched value (if this command matched an user-provided value such as a member name) +/// - end position of matched token +/// - optionally a short-circuit error +fn next_token( + possible_tokens: Vec, + input: String, + current_pos: usize, +) -> Result<(Token, Option, usize), Option> { + // get next parameter, matching quotes + let param = crate::string::next_param(input.clone(), current_pos); + println!("matched: {param:?}\n---"); + + // try checking if this is a flag + // todo!: this breaks full text matching if the full text starts with a flag + // (but that's kinda already broken anyway) + if let Some((value, new_pos)) = param.clone() + && value.starts_with('-') + { + return Ok(( + Token::Flag, + Some(value.trim_start_matches('-').to_string()), + new_pos, + )); + } + + // iterate over tokens and run try_match + for token in possible_tokens { + if let TokenMatchResult::Match(value) = + // for FullString just send the whole string + token.try_match(if matches!(token, Token::FullString) { + if input.is_empty() { + None + } else { + Some(input.clone()) + } + } else { + param.clone().map(|v| v.0) + }) + { + return Ok((token, value, param.map(|v| v.1).unwrap_or(current_pos))); + } + } + + Err(None) +} + +fn parse_command(input: String) -> CommandResult { + let mut local_tree: TreeBranch = COMMAND_TREE.clone(); + + // end position of all currently matched tokens + let mut current_pos = 0; + + let mut args: Vec = Vec::new(); + let mut flags: HashMap> = HashMap::new(); + + loop { + match next_token( + local_tree.possible_tokens.clone(), + input.clone(), + current_pos, + ) { + Ok((found_token, arg, new_pos)) => { + current_pos = new_pos; + if let Token::Flag = found_token { + flags.insert(arg.unwrap(), None); + // don't try matching flags as tree elements + continue; + } + + if let Some(arg) = arg { + args.push(arg); + } + + if let Some(next_tree) = local_tree.branches.get(&found_token) { + local_tree = next_tree.clone(); + } else { + panic!("found token could not match tree, at {input}"); + } + } + Err(None) => { + if let Some(command_ref) = local_tree.current_command_key { + return CommandResult::Ok { + command: ParsedCommand { + command_ref, + args, + flags, + }, + }; + } + // todo: check if last token is a common incorrect unquote (multi-member names etc) + // todo: check if this is a system name in pk;s command + return CommandResult::Err { + error: "Command not found.".to_string(), + }; + } + Err(Some(short_circuit)) => { + return CommandResult::Err { + error: short_circuit, + }; + } + } + } +} diff --git a/lib/commands/src/string.rs b/lib/commands/src/string.rs new file mode 100644 index 00000000..bc65c8a0 --- /dev/null +++ b/lib/commands/src/string.rs @@ -0,0 +1,87 @@ +use std::collections::HashMap; + +lazy_static::lazy_static! { + // Dictionary of (left, right) quote pairs + // Each char in the string is an individual quote, multi-char strings imply "one of the following chars" + pub static ref QUOTE_PAIRS: HashMap = { + let mut pairs = HashMap::new(); + + macro_rules! insert_pair { + ($a:literal, $b:literal) => { + pairs.insert($a.to_string(), $b.to_string()); + // make it easier to look up right quotes + for char in $a.chars() { + pairs.insert(char.to_string(), $b.to_string()); + } + } + } + + // Basic + insert_pair!( "'", "'" ); // ASCII single quotes + insert_pair!( "\"", "\"" ); // ASCII double quotes + + // "Smart quotes" + // Specifically ignore the left/right status of the quotes and match any combination of them + // Left string also includes "low" quotes to allow for the low-high style used in some locales + insert_pair!( "\u{201C}\u{201D}\u{201F}\u{201E}", "\u{201C}\u{201D}\u{201F}" ); // double quotes + insert_pair!( "\u{2018}\u{2019}\u{201B}\u{201A}", "\u{2018}\u{2019}\u{201B}" ); // single quotes + + // Chevrons (normal and "fullwidth" variants) + insert_pair!( "\u{00AB}\u{300A}", "\u{00BB}\u{300B}" ); // double chevrons, pointing away (<>) + insert_pair!( "\u{00BB}\u{300B}", "\u{00AB}\u{300A}" ); // double chevrons, pointing together (>>text<<) + insert_pair!( "\u{2039}\u{3008}", "\u{203A}\u{3009}" ); // single chevrons, pointing away () + insert_pair!( "\u{203A}\u{3009}", "\u{2039}\u{3008}" ); // single chevrons, pointing together (>text<) + + // Other + insert_pair!( "\u{300C}\u{300E}", "\u{300D}\u{300F}" ); // corner brackets (Japanese/Chinese) + + pairs + }; +} + +// very very simple quote matching +// quotes need to be at start/end of words, and are ignored if a closing quote is not present +// WTB POSIX quoting: https://pubs.opengroup.org/onlinepubs/9799919799/utilities/V3_chap02.html +pub fn next_param(input: String, current_pos: usize) -> Option<(String, usize)> { + if input.len() == current_pos { + return None; + } + + let leading_whitespace_count = + input[..current_pos].len() - input[..current_pos].trim_start().len(); + let substr_to_match = input[current_pos + leading_whitespace_count..].to_string(); + println!("stuff: {input} {current_pos} {leading_whitespace_count}"); + println!("to match: {substr_to_match}"); + + // try matching end quote + if let Some(right) = QUOTE_PAIRS.get(&substr_to_match[0..1]) { + for possible_quote in right.chars() { + for (pos, _) in substr_to_match.match_indices(possible_quote) { + if substr_to_match.len() == pos + 1 + || substr_to_match + .chars() + .nth(pos + 1) + .unwrap() + .is_whitespace() + { + // return quoted string, without quotes + return Some(( + substr_to_match[1..pos - 1].to_string(), + current_pos + pos + 1, + )); + } + } + } + } + + // find next whitespace character + for (pos, char) in substr_to_match.clone().char_indices() { + if char.is_whitespace() { + return Some((substr_to_match[..pos].to_string(), current_pos + pos + 1)); + } + } + + // if we're here, we went to EOF and didn't match any whitespace + // so we return the whole string + Some((substr_to_match.clone(), current_pos + substr_to_match.len())) +} diff --git a/lib/commands/src/token.rs b/lib/commands/src/token.rs new file mode 100644 index 00000000..9d952a24 --- /dev/null +++ b/lib/commands/src/token.rs @@ -0,0 +1,84 @@ +#[derive(Debug, Clone, Eq, Hash, PartialEq)] +pub enum Token { + /// Token used to represent a finished command (i.e. no more parameters required) + // todo: this is likely not the right way to represent this + Empty, + + /// A bot-defined value ("member" in `pk;member MyName`) + Value(Vec), + /// A command defined by multiple values + // todo! + MultiValue(Vec>), + + FullString, + + /// Member reference (hid or member name) + MemberRef, + MemberPrivacyTarget, + + PrivacyLevel, + + // currently not included in command definitions + // todo: flags with values + Flag, +} + +pub enum TokenMatchResult { + NoMatch, + /// Token matched, optionally with a value. + Match(Option), +} + +// move this somewhere else +lazy_static::lazy_static!( + static ref MEMBER_PRIVACY_TARGETS: Vec = vec![ + "visibility".to_string(), + "name".to_string(), + "todo".to_string() + ]; +); + +impl Token { + pub fn try_match(&self, input: Option) -> TokenMatchResult { + // short circuit on empty things + if matches!(self, Self::Empty) && input.is_none() { + return TokenMatchResult::Match(None); + } else if input.is_none() { + return TokenMatchResult::NoMatch; + } + + let input = input.unwrap(); + + // try actually matching stuff + match self { + Self::Empty => return TokenMatchResult::NoMatch, + Self::Flag => unreachable!(), // matched upstream + Self::Value(values) => { + for v in values { + if input.trim() == v { + // c# bot currently needs subcommands provided as arguments + // todo!: remove this + return TokenMatchResult::Match(Some(v.clone())); + } + } + } + Self::MultiValue(_) => todo!(), + Self::FullString => return TokenMatchResult::Match(Some(input)), + Self::MemberRef => return TokenMatchResult::Match(Some(input)), + Self::MemberPrivacyTarget + if MEMBER_PRIVACY_TARGETS.contains(&input.trim().to_string()) => + { + return TokenMatchResult::Match(Some(input)) + } + Self::MemberPrivacyTarget => {} + Self::PrivacyLevel if input == "public" || input == "private" => { + return TokenMatchResult::Match(Some(input)) + } + Self::PrivacyLevel => {} + } + // note: must not add a _ case to the above match + // instead, for conditional matches, also add generic cases with no return + + return TokenMatchResult::NoMatch; + } +} diff --git a/lib/commands/uniffi.toml b/lib/commands/uniffi.toml new file mode 100644 index 00000000..86d3d6f1 --- /dev/null +++ b/lib/commands/uniffi.toml @@ -0,0 +1,2 @@ +[bindings.csharp] +cdylib_name = "commands" From fd7d3e6a3a75f943c812bb3a319b0f245af02362 Mon Sep 17 00:00:00 2001 From: alyssa Date: Fri, 13 Sep 2024 18:34:32 +0900 Subject: [PATCH 002/179] use quote! for generating code instead --- lib/command_system_macros/Cargo.toml | 4 ++ lib/command_system_macros/src/lib.rs | 82 ++++++++++++++++------------ 2 files changed, 50 insertions(+), 36 deletions(-) diff --git a/lib/command_system_macros/Cargo.toml b/lib/command_system_macros/Cargo.toml index cf660792..269371c6 100644 --- a/lib/command_system_macros/Cargo.toml +++ b/lib/command_system_macros/Cargo.toml @@ -5,3 +5,7 @@ edition = "2021" [lib] proc-macro = true + +[dependencies] +quote = "1.0" +proc-macro2 = "1.0" diff --git a/lib/command_system_macros/src/lib.rs b/lib/command_system_macros/src/lib.rs index 854844c6..6a8190ee 100644 --- a/lib/command_system_macros/src/lib.rs +++ b/lib/command_system_macros/src/lib.rs @@ -1,19 +1,28 @@ use proc_macro::{Delimiter, TokenStream, TokenTree}; +use proc_macro2::{Ident, Literal, Span}; +use quote::quote; -fn make_command(tokens: Vec, help: String, cb: String) -> String { - let tokens = tokens - .iter() - .map(|v| format!("Token::{v}")) - .collect::>() - .join(","); - format!( - r#"Command {{ tokens: vec![{tokens}], help: {help}.to_string(), cb: "{cb}".to_string() }}"# - ) +fn make_command( + tokens: Vec, + help: String, + cb: String, +) -> proc_macro2::TokenStream { + let help = Literal::string(&help); + let cb = Literal::string(&cb); + + quote! { + Command { tokens: vec![#(#tokens),*], help: #help.to_string(), cb: #cb.to_string() } + } } -fn command_from_stream(stream: TokenStream) -> String { +// horrible, but the best way i could find to do this +fn token_to_string(i: String) -> String { + i.to_string()[1..i.to_string().len() - 1].to_string() +} + +fn command_from_stream(stream: TokenStream) -> proc_macro2::TokenStream { let mut part = 0; - let mut found_tokens: Vec = Vec::new(); + let mut found_tokens: Vec = Vec::new(); let mut found_cb: Option = None; let mut found_help: Option = None; @@ -25,9 +34,11 @@ fn command_from_stream(stream: TokenStream) -> String { None if part == 2 && found_help.is_some() => break 'a, Some(TokenTree::Ident(ident)) if part == 0 => { found_tokens.push(if is_token_lit { - format!("{ident}") + let ident = Ident::new(ident.to_string().as_str(), Span::call_site()); + quote! { Token::#ident }.into() } else { - format!("Value(vec![\"{ident}\".to_string()])") + let ident = Literal::string(format!("{ident}").as_str()); + quote! { Token::Value(vec![#ident.to_string() ]) } }); // reset this is_token_lit = false; @@ -42,7 +53,9 @@ fn command_from_stream(stream: TokenStream) -> String { part += 1 } Some(TokenTree::Ident(ident)) if part == 1 => found_cb = Some(format!("{ident}")), - Some(TokenTree::Literal(lit)) if part == 2 => found_help = Some(format!("{lit}")), + Some(TokenTree::Literal(lit)) if part == 2 => { + found_help = Some(token_to_string(lit.to_string())) + } _ => panic!("invalid command definition: {stream}"), } } @@ -51,7 +64,7 @@ fn command_from_stream(stream: TokenStream) -> String { #[proc_macro] pub fn commands(stream: TokenStream) -> TokenStream { - let mut commands: Vec = Vec::new(); + let mut commands: Vec = Vec::new(); let mut top_level_tokens = stream.into_iter(); 'a: loop { @@ -77,33 +90,30 @@ pub fn commands(stream: TokenStream) -> TokenStream { let command_registrations = commands .iter() - .map(|v| format!("tree.register_command({v});")) - .collect::>() - .join("\n"); + .map(|v| -> proc_macro2::TokenStream { quote! { tree.register_command(#v); }.into() }) + .collect::(); - let res = format!( - r#" -lazy_static::lazy_static! {{ - static ref COMMAND_TREE: TreeBranch = {{ - let mut tree = TreeBranch {{ - current_command_key: None, - possible_tokens: vec![], - branches: HashMap::new(), - }}; + let res = quote! { + lazy_static::lazy_static! { + static ref COMMAND_TREE: TreeBranch = { + let mut tree = TreeBranch { + current_command_key: None, + possible_tokens: vec![], + branches: HashMap::new(), + }; - {command_registrations} + #command_registrations - tree.sort_tokens(); + tree.sort_tokens(); - // println!("{{tree:#?}}"); + // println!("{{tree:#?}}"); - tree - }}; -}} - "# - ); + tree + }; + } + }; // panic!("{res}"); - res.parse().unwrap() + res.into() } From c99b59673a369b1375d60188d0a8be9c1091b40e Mon Sep 17 00:00:00 2001 From: Iris System Date: Fri, 13 Sep 2024 22:17:11 +1200 Subject: [PATCH 003/179] use proc_macro2 as much as possible --- lib/command_system_macros/src/lib.rs | 43 ++++++++++++---------------- 1 file changed, 18 insertions(+), 25 deletions(-) diff --git a/lib/command_system_macros/src/lib.rs b/lib/command_system_macros/src/lib.rs index 6a8190ee..034f377a 100644 --- a/lib/command_system_macros/src/lib.rs +++ b/lib/command_system_macros/src/lib.rs @@ -1,30 +1,21 @@ -use proc_macro::{Delimiter, TokenStream, TokenTree}; -use proc_macro2::{Ident, Literal, Span}; +use proc_macro2::{Delimiter, TokenStream, TokenTree, Ident, Literal, Span}; use quote::quote; fn make_command( - tokens: Vec, - help: String, - cb: String, -) -> proc_macro2::TokenStream { - let help = Literal::string(&help); - let cb = Literal::string(&cb); - + tokens: Vec, + help: Literal, + cb: Literal, +) -> TokenStream { quote! { Command { tokens: vec![#(#tokens),*], help: #help.to_string(), cb: #cb.to_string() } } } -// horrible, but the best way i could find to do this -fn token_to_string(i: String) -> String { - i.to_string()[1..i.to_string().len() - 1].to_string() -} - -fn command_from_stream(stream: TokenStream) -> proc_macro2::TokenStream { +fn command_from_stream(stream: TokenStream) -> TokenStream { let mut part = 0; - let mut found_tokens: Vec = Vec::new(); - let mut found_cb: Option = None; - let mut found_help: Option = None; + let mut found_tokens: Vec = Vec::new(); + let mut found_cb: Option = None; + let mut found_help: Option = None; let mut is_token_lit = false; let mut tokens = stream.clone().into_iter(); @@ -34,11 +25,10 @@ fn command_from_stream(stream: TokenStream) -> proc_macro2::TokenStream { None if part == 2 && found_help.is_some() => break 'a, Some(TokenTree::Ident(ident)) if part == 0 => { found_tokens.push(if is_token_lit { - let ident = Ident::new(ident.to_string().as_str(), Span::call_site()); quote! { Token::#ident }.into() } else { - let ident = Literal::string(format!("{ident}").as_str()); - quote! { Token::Value(vec![#ident.to_string() ]) } + let lit = Literal::string(&format!("{ident}")); + quote! { Token::Value(vec![#lit.to_string() ]) } }); // reset this is_token_lit = false; @@ -52,9 +42,11 @@ fn command_from_stream(stream: TokenStream) -> proc_macro2::TokenStream { { part += 1 } - Some(TokenTree::Ident(ident)) if part == 1 => found_cb = Some(format!("{ident}")), + Some(TokenTree::Ident(ident)) if part == 1 => { + found_cb = Some(Literal::string(&format!("{ident}"))) + } Some(TokenTree::Literal(lit)) if part == 2 => { - found_help = Some(token_to_string(lit.to_string())) + found_help = Some(lit) } _ => panic!("invalid command definition: {stream}"), } @@ -63,8 +55,9 @@ fn command_from_stream(stream: TokenStream) -> proc_macro2::TokenStream { } #[proc_macro] -pub fn commands(stream: TokenStream) -> TokenStream { - let mut commands: Vec = Vec::new(); +pub fn commands(stream: proc_macro::TokenStream) -> proc_macro::TokenStream { + let stream: TokenStream = stream.into(); + let mut commands: Vec = Vec::new(); let mut top_level_tokens = stream.into_iter(); 'a: loop { From fce23c2b90f892780ec9197595f2d04976c40bbe Mon Sep 17 00:00:00 2001 From: Iris System Date: Fri, 13 Sep 2024 22:17:42 +1200 Subject: [PATCH 004/179] force resolver="2" for cargo workspace, update lockfile --- Cargo.lock | 4 ++++ Cargo.toml | 1 + 2 files changed, 5 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index abc1e061..69322627 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -472,6 +472,10 @@ dependencies = [ [[package]] name = "command_system_macros" version = "0.1.0" +dependencies = [ + "proc-macro2", + "quote", +] [[package]] name = "commands" diff --git a/Cargo.toml b/Cargo.toml index 3866b321..0dc72064 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,4 +1,5 @@ [workspace] +resolver = "2" members = [ "./lib/libpk", "./lib/commands", From 737d6d3216b57168d0c4c3d092e8d52efad4275a Mon Sep 17 00:00:00 2001 From: Iris System Date: Fri, 13 Sep 2024 23:58:56 +1200 Subject: [PATCH 005/179] make the proc macro DSL parser far more readable --- Cargo.lock | 1 + lib/command_system_macros/Cargo.toml | 1 + lib/command_system_macros/src/lib.rs | 137 +++++++++++++++++---------- 3 files changed, 90 insertions(+), 49 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 69322627..495b0dd9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -475,6 +475,7 @@ version = "0.1.0" dependencies = [ "proc-macro2", "quote", + "syn 2.0.77", ] [[package]] diff --git a/lib/command_system_macros/Cargo.toml b/lib/command_system_macros/Cargo.toml index 269371c6..7d2aa1b0 100644 --- a/lib/command_system_macros/Cargo.toml +++ b/lib/command_system_macros/Cargo.toml @@ -9,3 +9,4 @@ proc-macro = true [dependencies] quote = "1.0" proc-macro2 = "1.0" +syn = "2.0" diff --git a/lib/command_system_macros/src/lib.rs b/lib/command_system_macros/src/lib.rs index 034f377a..1f863ec3 100644 --- a/lib/command_system_macros/src/lib.rs +++ b/lib/command_system_macros/src/lib.rs @@ -1,57 +1,95 @@ -use proc_macro2::{Delimiter, TokenStream, TokenTree, Ident, Literal, Span}; -use quote::quote; +use proc_macro2::{Delimiter, TokenStream, TokenTree, Literal, Span}; +use syn::parse::{Parse, ParseStream, Result as ParseResult}; +use syn::{parse_macro_input, Token, Ident}; +use quote::{quote, quote_spanned}; -fn make_command( - tokens: Vec, - help: Literal, - cb: Literal, -) -> TokenStream { - quote! { - Command { tokens: vec![#(#tokens),*], help: #help.to_string(), cb: #cb.to_string() } +enum CommandToken { + /// "typed argument" being a member of the `Token` enum in the + /// command parser crate. + /// + /// prefixed with `@` in the command macro. + TypedArgument(Ident, Span), + + /// interpreted as a literal string in the command input. + /// + /// no prefix in the command macro. + Literal(Literal, Span), +} + +impl Parse for CommandToken { + fn parse(input: ParseStream) -> ParseResult { + let lookahead = input.lookahead1(); + if lookahead.peek(Token![@]) { + // typed argument + input.parse::()?; + let ident = input.parse::()?; + Ok(Self::TypedArgument(ident.clone(), ident.span())) + } else if lookahead.peek(Ident) { + // literal string + let ident = input.parse::()?; + let lit = Literal::string(&format!("{ident}")); + Ok(Self::Literal(lit, ident.span())) + } else { + Err(input.error("expected a command token")) + } } } -fn command_from_stream(stream: TokenStream) -> TokenStream { - let mut part = 0; - let mut found_tokens: Vec = Vec::new(); - let mut found_cb: Option = None; - let mut found_help: Option = None; +impl Into for CommandToken { + fn into(self) -> TokenStream { + match self { + Self::TypedArgument(ident, span) => quote_spanned! {span=> + Token::#ident + }, - let mut is_token_lit = false; - let mut tokens = stream.clone().into_iter(); - 'a: loop { - let cur_token = tokens.next(); - match cur_token { - None if part == 2 && found_help.is_some() => break 'a, - Some(TokenTree::Ident(ident)) if part == 0 => { - found_tokens.push(if is_token_lit { - quote! { Token::#ident }.into() - } else { - let lit = Literal::string(&format!("{ident}")); - quote! { Token::Value(vec![#lit.to_string() ]) } - }); - // reset this - is_token_lit = false; + Self::Literal(lit, span) => quote_spanned! {span=> + Token::Value(vec![ #lit.to_string(), ]) + }, + }.into() + } +} + +struct Command { + tokens: Vec, + help: Literal, + cb: Literal, +} + +impl Parse for Command { + fn parse(input: ParseStream) -> ParseResult { + let mut tokens = Vec::::new(); + loop { + if input.peek(Token![,]) { + break; } - Some(TokenTree::Punct(punct)) if part == 0 && format!("{punct}") == "@" => { - is_token_lit = true - } - Some(TokenTree::Punct(punct)) - if ((part == 0 && found_tokens.len() > 0) || (part == 1 && found_cb.is_some())) - && format!("{punct}") == "," => - { - part += 1 - } - Some(TokenTree::Ident(ident)) if part == 1 => { - found_cb = Some(Literal::string(&format!("{ident}"))) - } - Some(TokenTree::Literal(lit)) if part == 2 => { - found_help = Some(lit) - } - _ => panic!("invalid command definition: {stream}"), + + tokens.push(input.parse::()?); + } + input.parse::()?; + + let cb_ident = input.parse::()?; + let cb = Literal::string(&format!("{cb_ident}")); + input.parse::()?; + + let help = input.parse::()?; + + Ok(Self { + tokens, + cb, + help, + }) + } +} + +impl Into for Command { + fn into(self) -> TokenStream { + let Self { tokens, help, cb } = self; + let tokens = tokens.into_iter().map(Into::into).collect::>(); + + quote! { + Command { tokens: vec![#(#tokens),*], help: #help.to_string(), cb: #cb.to_string() } } } - make_command(found_tokens, found_help.unwrap(), found_cb.unwrap()) } #[proc_macro] @@ -70,7 +108,8 @@ pub fn commands(stream: proc_macro::TokenStream) -> proc_macro::TokenStream { // match top_level_tokens.next() { Some(TokenTree::Group(group)) if group.delimiter() == Delimiter::Parenthesis => { - commands.push(command_from_stream(group.stream())); + let group_stream: proc_macro::TokenStream = group.stream().into(); + commands.push(parse_macro_input!(group_stream as Command).into()); } _ => panic!("contents of commands! macro is invalid"), } @@ -83,8 +122,8 @@ pub fn commands(stream: proc_macro::TokenStream) -> proc_macro::TokenStream { let command_registrations = commands .iter() - .map(|v| -> proc_macro2::TokenStream { quote! { tree.register_command(#v); }.into() }) - .collect::(); + .map(|v| -> TokenStream { quote! { tree.register_command(#v); }.into() }) + .collect::(); let res = quote! { lazy_static::lazy_static! { From b6eec3784dfe43c2cddef0c2dcff72c0b39e6d90 Mon Sep 17 00:00:00 2001 From: libglfw Date: Sat, 2 Nov 2024 14:55:06 -0700 Subject: [PATCH 006/179] remove command_system_macros --- lib/command_system_macros/Cargo.toml | 12 --- lib/command_system_macros/src/lib.rs | 151 --------------------------- lib/commands/Cargo.toml | 1 - lib/commands/src/lib.rs | 104 +++++++++++++++--- lib/commands/src/string.rs | 1 + 5 files changed, 93 insertions(+), 176 deletions(-) delete mode 100644 lib/command_system_macros/Cargo.toml delete mode 100644 lib/command_system_macros/src/lib.rs diff --git a/lib/command_system_macros/Cargo.toml b/lib/command_system_macros/Cargo.toml deleted file mode 100644 index 7d2aa1b0..00000000 --- a/lib/command_system_macros/Cargo.toml +++ /dev/null @@ -1,12 +0,0 @@ -[package] -name = "command_system_macros" -version = "0.1.0" -edition = "2021" - -[lib] -proc-macro = true - -[dependencies] -quote = "1.0" -proc-macro2 = "1.0" -syn = "2.0" diff --git a/lib/command_system_macros/src/lib.rs b/lib/command_system_macros/src/lib.rs deleted file mode 100644 index 1f863ec3..00000000 --- a/lib/command_system_macros/src/lib.rs +++ /dev/null @@ -1,151 +0,0 @@ -use proc_macro2::{Delimiter, TokenStream, TokenTree, Literal, Span}; -use syn::parse::{Parse, ParseStream, Result as ParseResult}; -use syn::{parse_macro_input, Token, Ident}; -use quote::{quote, quote_spanned}; - -enum CommandToken { - /// "typed argument" being a member of the `Token` enum in the - /// command parser crate. - /// - /// prefixed with `@` in the command macro. - TypedArgument(Ident, Span), - - /// interpreted as a literal string in the command input. - /// - /// no prefix in the command macro. - Literal(Literal, Span), -} - -impl Parse for CommandToken { - fn parse(input: ParseStream) -> ParseResult { - let lookahead = input.lookahead1(); - if lookahead.peek(Token![@]) { - // typed argument - input.parse::()?; - let ident = input.parse::()?; - Ok(Self::TypedArgument(ident.clone(), ident.span())) - } else if lookahead.peek(Ident) { - // literal string - let ident = input.parse::()?; - let lit = Literal::string(&format!("{ident}")); - Ok(Self::Literal(lit, ident.span())) - } else { - Err(input.error("expected a command token")) - } - } -} - -impl Into for CommandToken { - fn into(self) -> TokenStream { - match self { - Self::TypedArgument(ident, span) => quote_spanned! {span=> - Token::#ident - }, - - Self::Literal(lit, span) => quote_spanned! {span=> - Token::Value(vec![ #lit.to_string(), ]) - }, - }.into() - } -} - -struct Command { - tokens: Vec, - help: Literal, - cb: Literal, -} - -impl Parse for Command { - fn parse(input: ParseStream) -> ParseResult { - let mut tokens = Vec::::new(); - loop { - if input.peek(Token![,]) { - break; - } - - tokens.push(input.parse::()?); - } - input.parse::()?; - - let cb_ident = input.parse::()?; - let cb = Literal::string(&format!("{cb_ident}")); - input.parse::()?; - - let help = input.parse::()?; - - Ok(Self { - tokens, - cb, - help, - }) - } -} - -impl Into for Command { - fn into(self) -> TokenStream { - let Self { tokens, help, cb } = self; - let tokens = tokens.into_iter().map(Into::into).collect::>(); - - quote! { - Command { tokens: vec![#(#tokens),*], help: #help.to_string(), cb: #cb.to_string() } - } - } -} - -#[proc_macro] -pub fn commands(stream: proc_macro::TokenStream) -> proc_macro::TokenStream { - let stream: TokenStream = stream.into(); - let mut commands: Vec = Vec::new(); - - let mut top_level_tokens = stream.into_iter(); - 'a: loop { - // "command" - match top_level_tokens.next() { - Some(TokenTree::Ident(ident)) if format!("{ident}") == "command" => {} - None => break 'a, - _ => panic!("contents of commands! macro is invalid"), - } - // - match top_level_tokens.next() { - Some(TokenTree::Group(group)) if group.delimiter() == Delimiter::Parenthesis => { - let group_stream: proc_macro::TokenStream = group.stream().into(); - commands.push(parse_macro_input!(group_stream as Command).into()); - } - _ => panic!("contents of commands! macro is invalid"), - } - // ; - match top_level_tokens.next() { - Some(TokenTree::Punct(punct)) if format!("{punct}") == ";" => {} - _ => panic!("contents of commands! macro is invalid"), - } - } - - let command_registrations = commands - .iter() - .map(|v| -> TokenStream { quote! { tree.register_command(#v); }.into() }) - .collect::(); - - let res = quote! { - lazy_static::lazy_static! { - static ref COMMAND_TREE: TreeBranch = { - let mut tree = TreeBranch { - current_command_key: None, - possible_tokens: vec![], - branches: HashMap::new(), - }; - - #command_registrations - - tree.sort_tokens(); - - // println!("{{tree:#?}}"); - - tree - }; - } - }; - - // panic!("{res}"); - - res.into() -} diff --git a/lib/commands/Cargo.toml b/lib/commands/Cargo.toml index 1d1ac480..efff1dea 100644 --- a/lib/commands/Cargo.toml +++ b/lib/commands/Cargo.toml @@ -8,7 +8,6 @@ crate-type = ["cdylib"] [dependencies] lazy_static = { workspace = true } -command_system_macros = { path = "../command_system_macros" } uniffi = { version = "0.25" } diff --git a/lib/commands/src/lib.rs b/lib/commands/src/lib.rs index 0217dec7..6b787403 100644 --- a/lib/commands/src/lib.rs +++ b/lib/commands/src/lib.rs @@ -9,8 +9,6 @@ mod string; mod token; use token::*; -use command_system_macros::commands; - // todo!: move all this stuff into a different file // lib.rs should just have exported symbols and command definitions @@ -69,23 +67,105 @@ impl TreeBranch { } } +#[derive(Clone)] struct Command { tokens: Vec, help: String, cb: String, } -// todo: aliases -// todo: categories -commands! { - command(help, help, "Shows the help command"); +fn command(tokens: &[&Token], help: &str, cb: &str) -> Command { + Command { + tokens: tokens.iter().map(|&x| x.clone()).collect(), + help: help.to_string(), + cb: cb.to_string(), + } +} - command(member new, member_new, "Creates a new system member"); - command(member @MemberRef, member_show, "Shows information about a member"); - command(member @MemberRef description, member_desc_show, "Shows a member's description"); - command(member @MemberRef description @FullString, member_desc_update, "Changes a member's description"); - command(member @MemberRef privacy, member_privacy_show, "Displays a member's current privacy settings"); - command(member @MemberRef privacy @MemberPrivacyTarget @PrivacyLevel, member_privacy_update, "Changes a member's privacy settings"); +mod commands { + use super::Token; + + use super::command; + use super::Token::*; + + fn cmd(value: &str) -> Token { + Token::Value(vec![value.to_string()]) + } + + pub fn cmd_with_alias(value: &[&str]) -> Token { + Token::Value(value.iter().map(|x| x.to_string()).collect()) + } + + // todo: this needs to have less ampersands -alyssa + pub fn happy() -> Vec { + let system = &cmd_with_alias(&["system", "s"]); + let member = &cmd_with_alias(&["member", "m"]); + let description = &cmd_with_alias(&["description", "desc"]); + let privacy = &cmd_with_alias(&["privacy", "priv"]); + vec![ + command(&[&cmd("help")], "help", "Shows the help command"), + command( + &[system], + "system_show", + "Shows information about your system", + ), + command(&[system, &cmd("new")], "system_new", "Creates a new system"), + command( + &[member, &cmd_with_alias(&["new", "n"])], + "member_new", + "Creates a new system member", + ), + command( + &[member, &MemberRef], + "member_show", + "Shows information about a member", + ), + command( + &[member, &MemberRef, description], + "member_desc_show", + "Shows a member's description", + ), + command( + &[member, &MemberRef, description, &FullString], + "member_desc_update", + "Changes a member's description", + ), + command( + &[member, &MemberRef, privacy], + "member_privacy_show", + "Displays a member's current privacy settings", + ), + command( + &[ + member, + &MemberRef, + privacy, + &MemberPrivacyTarget, + &PrivacyLevel, + ], + "member_privacy_update", + "Changes a member's privacy settings", + ), + ] + } +} + +lazy_static::lazy_static! { + static ref COMMAND_TREE: TreeBranch = { + let mut tree = TreeBranch { + current_command_key: None, + possible_tokens: vec![], + branches: HashMap::new(), + }; + + commands::happy().iter().for_each(|x| tree.register_command(x.clone())); + + tree.sort_tokens(); + + // println!("{{tree:#?}}"); + + tree + }; } pub enum CommandResult { diff --git a/lib/commands/src/string.rs b/lib/commands/src/string.rs index bc65c8a0..0ea7659a 100644 --- a/lib/commands/src/string.rs +++ b/lib/commands/src/string.rs @@ -3,6 +3,7 @@ use std::collections::HashMap; lazy_static::lazy_static! { // Dictionary of (left, right) quote pairs // Each char in the string is an individual quote, multi-char strings imply "one of the following chars" + // Certain languages can have quote patterns that have a different character for open and close pub static ref QUOTE_PAIRS: HashMap = { let mut pairs = HashMap::new(); From 26f855b7b9a111fed3f8b52577c4282058f9b545 Mon Sep 17 00:00:00 2001 From: libglfw Date: Sat, 2 Nov 2024 14:55:19 -0700 Subject: [PATCH 007/179] improve nix devshell --- flake.lock | 269 ++++++++++++++++++++++++++++++++++++++++++++ flake.nix | 46 ++++++++ rust-toolchain.toml | 3 + 3 files changed, 318 insertions(+) create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 rust-toolchain.toml diff --git a/flake.lock b/flake.lock new file mode 100644 index 00000000..1ef62906 --- /dev/null +++ b/flake.lock @@ -0,0 +1,269 @@ +{ + "nodes": { + "crane": { + "flake": false, + "locked": { + "lastModified": 1727316705, + "narHash": "sha256-/mumx8AQ5xFuCJqxCIOFCHTVlxHkMT21idpbgbm/TIE=", + "owner": "ipetkov", + "repo": "crane", + "rev": "5b03654ce046b5167e7b0bccbd8244cb56c16f0e", + "type": "github" + }, + "original": { + "owner": "ipetkov", + "ref": "v0.19.0", + "repo": "crane", + "type": "github" + } + }, + "dream2nix": { + "inputs": { + "nixpkgs": [ + "nci", + "nixpkgs" + ], + "purescript-overlay": "purescript-overlay", + "pyproject-nix": "pyproject-nix" + }, + "locked": { + "lastModified": 1728585693, + "narHash": "sha256-rhx5SYpIkPu7d+rjF9FGGBVxS0BwAEkmYIsJg2a3E20=", + "owner": "nix-community", + "repo": "dream2nix", + "rev": "c6935471f7e1a9e190aaa9ac9823dca34e00d92a", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "dream2nix", + "type": "github" + } + }, + "flake-compat": { + "flake": false, + "locked": { + "lastModified": 1696426674, + "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=", + "owner": "edolstra", + "repo": "flake-compat", + "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33", + "type": "github" + }, + "original": { + "owner": "edolstra", + "repo": "flake-compat", + "type": "github" + } + }, + "mk-naked-shell": { + "flake": false, + "locked": { + "lastModified": 1681286841, + "narHash": "sha256-3XlJrwlR0nBiREnuogoa5i1b4+w/XPe0z8bbrJASw0g=", + "owner": "yusdacra", + "repo": "mk-naked-shell", + "rev": "7612f828dd6f22b7fb332cc69440e839d7ffe6bd", + "type": "github" + }, + "original": { + "owner": "yusdacra", + "repo": "mk-naked-shell", + "type": "github" + } + }, + "nci": { + "inputs": { + "crane": "crane", + "dream2nix": "dream2nix", + "mk-naked-shell": "mk-naked-shell", + "nixpkgs": [ + "nixpkgs" + ], + "parts": "parts", + "rust-overlay": "rust-overlay", + "treefmt": "treefmt" + }, + "locked": { + "lastModified": 1729836952, + "narHash": "sha256-1XE0s+JiwzoHaAul4wqeGjo28IxF81nZOWuubfXc1vo=", + "owner": "yusdacra", + "repo": "nix-cargo-integration", + "rev": "712068c3707990320ee0b225bd82ecd8687dca96", + "type": "github" + }, + "original": { + "owner": "yusdacra", + "repo": "nix-cargo-integration", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1729665710, + "narHash": "sha256-AlcmCXJZPIlO5dmFzV3V2XF6x/OpNWUV8Y/FMPGd8Z4=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "2768c7d042a37de65bb1b5b3268fc987e534c49d", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "parts": { + "inputs": { + "nixpkgs-lib": [ + "nci", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1727826117, + "narHash": "sha256-K5ZLCyfO/Zj9mPFldf3iwS6oZStJcU4tSpiXTMYaaL0=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "3d04084d54bedc3d6b8b736c70ef449225c361b1", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, + "parts_2": { + "inputs": { + "nixpkgs-lib": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1727826117, + "narHash": "sha256-K5ZLCyfO/Zj9mPFldf3iwS6oZStJcU4tSpiXTMYaaL0=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "3d04084d54bedc3d6b8b736c70ef449225c361b1", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, + "purescript-overlay": { + "inputs": { + "flake-compat": "flake-compat", + "nixpkgs": [ + "nci", + "dream2nix", + "nixpkgs" + ], + "slimlock": "slimlock" + }, + "locked": { + "lastModified": 1724504251, + "narHash": "sha256-TIw+sac0NX0FeAneud+sQZT+ql1G/WEb7/Vb436rUXM=", + "owner": "thomashoneyman", + "repo": "purescript-overlay", + "rev": "988b09676c2a0e6a46dfa3589aa6763c90476b8a", + "type": "github" + }, + "original": { + "owner": "thomashoneyman", + "repo": "purescript-overlay", + "type": "github" + } + }, + "pyproject-nix": { + "flake": false, + "locked": { + "lastModified": 1702448246, + "narHash": "sha256-hFg5s/hoJFv7tDpiGvEvXP0UfFvFEDgTdyHIjDVHu1I=", + "owner": "davhau", + "repo": "pyproject.nix", + "rev": "5a06a2697b228c04dd2f35659b4b659ca74f7aeb", + "type": "github" + }, + "original": { + "owner": "davhau", + "ref": "dream2nix", + "repo": "pyproject.nix", + "type": "github" + } + }, + "root": { + "inputs": { + "nci": "nci", + "nixpkgs": "nixpkgs", + "parts": "parts_2" + } + }, + "rust-overlay": { + "flake": false, + "locked": { + "lastModified": 1729823394, + "narHash": "sha256-RiinJqorqSLKh1oSpiMHnBe6nQdJzE45lX6fSnAuDnI=", + "owner": "oxalica", + "repo": "rust-overlay", + "rev": "7e52e80f5faa374ad4c607d62c6d362589cb523f", + "type": "github" + }, + "original": { + "owner": "oxalica", + "repo": "rust-overlay", + "type": "github" + } + }, + "slimlock": { + "inputs": { + "nixpkgs": [ + "nci", + "dream2nix", + "purescript-overlay", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1688756706, + "narHash": "sha256-xzkkMv3neJJJ89zo3o2ojp7nFeaZc2G0fYwNXNJRFlo=", + "owner": "thomashoneyman", + "repo": "slimlock", + "rev": "cf72723f59e2340d24881fd7bf61cb113b4c407c", + "type": "github" + }, + "original": { + "owner": "thomashoneyman", + "repo": "slimlock", + "type": "github" + } + }, + "treefmt": { + "inputs": { + "nixpkgs": [ + "nci", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1729613947, + "narHash": "sha256-XGOvuIPW1XRfPgHtGYXd5MAmJzZtOuwlfKDgxX5KT3s=", + "owner": "numtide", + "repo": "treefmt-nix", + "rev": "aac86347fb5063960eccb19493e0cadcdb4205ca", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "treefmt-nix", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 00000000..c0da6660 --- /dev/null +++ b/flake.nix @@ -0,0 +1,46 @@ +{ + inputs.nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; + inputs.nci.url = "github:yusdacra/nix-cargo-integration"; + inputs.nci.inputs.nixpkgs.follows = "nixpkgs"; + inputs.parts.url = "github:hercules-ci/flake-parts"; + inputs.parts.inputs.nixpkgs-lib.follows = "nixpkgs"; + + outputs = inputs @ { + parts, + nci, + ... + }: + parts.lib.mkFlake {inherit inputs;} { + systems = ["x86_64-linux"]; + imports = [nci.flakeModule]; + perSystem = { + config, + pkgs, + ... + }: let + rustOutputs = config.nci.outputs; + rustDeps = with pkgs; [rust-analyzer]; + webDeps = with pkgs; [yarn nodejs]; + csDeps = with pkgs; [gcc protobuf dotnet-sdk_6 go]; + in { + nci.toolchainConfig = ./rust-toolchain.toml; + nci.projects."pluralkit" = { + path = ./.; + export = true; + }; + # configure crates + nci.crates = { + # see for usage: https://github.com/yusdacra/nix-cargo-integration/blob/master/examples/simple-workspace/crates.nix + }; + + devShells.default = rustOutputs."pluralkit".devShell.overrideAttrs (old: { + nativeBuildInputs = old.nativeBuildInputs ++ webDeps ++ csDeps ++ rustDeps; + }); + devShells.web = pkgs.mkShell {nativeBuildInputs = webDeps;}; + devShells.rust = rustOutputs."pluralkit".devShell.overrideAttrs (old: { nativeBuildInputs = old.nativeBuildInputs ++ rustDeps; }); + devShells.cs= pkgs.mkShell { + nativeBuildInputs = csDeps; + }; + }; + }; +} diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 00000000..2792be0d --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,3 @@ +[toolchain] +channel = "nightly-2024-08-20" + From 3bf14256d82df26123b983586d40c7092ae05ecf Mon Sep 17 00:00:00 2001 From: libglfw Date: Sat, 2 Nov 2024 15:08:43 -0700 Subject: [PATCH 008/179] update cargo lock --- Cargo.lock | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 495b0dd9..6e060b33 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -469,20 +469,10 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "command_system_macros" -version = "0.1.0" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.77", -] - [[package]] name = "commands" version = "0.1.0" dependencies = [ - "command_system_macros", "lazy_static", "uniffi", ] From 4d4138c4b14b6002c748a7290ebec9dce13fe97a Mon Sep 17 00:00:00 2001 From: libglfw Date: Sat, 2 Nov 2024 15:14:53 -0700 Subject: [PATCH 009/179] fix Cargo.lock --- Cargo.lock | 192 ++++++++++++++++++++++++++++------------------------- 1 file changed, 103 insertions(+), 89 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6e060b33..12e7ae2d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -19,9 +19,9 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "ahash" -version = "0.7.6" +version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" dependencies = [ "getrandom", "once_cell", @@ -135,7 +135,7 @@ dependencies = [ "proc-macro2", "quote", "serde", - "syn 2.0.77", + "syn 2.0.66", ] [[package]] @@ -161,7 +161,7 @@ checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.66", ] [[package]] @@ -399,9 +399,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.4.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" +checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" [[package]] name = "bytes-utils" @@ -479,9 +479,9 @@ dependencies = [ [[package]] name = "config" -version = "0.13.3" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d379af7f68bfc21714c6c7dea883544201741d2ce8274bb12fa54f89507f52a7" +checksum = "23738e11972c7643e4ec947840fc463b6a571afcd3e735bdfce7d03c7a784aca" dependencies = [ "async-trait", "json5", @@ -671,7 +671,7 @@ dependencies = [ "heck 0.4.1", "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.66", ] [[package]] @@ -872,7 +872,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.66", ] [[package]] @@ -965,7 +965,7 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" dependencies = [ - "ahash 0.7.6", + "ahash 0.7.8", ] [[package]] @@ -1230,9 +1230,9 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.27.2" +version = "0.27.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee4be2c948921a1a5320b629c4193916ed787a7f7f293fd3f7f5a6c9de74155" +checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333" dependencies = [ "futures-util", "http 1.1.0", @@ -1311,9 +1311,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "1.9.2" +version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", "hashbrown 0.12.3", @@ -1390,9 +1390,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.155" +version = "0.2.161" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" +checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1" [[package]] name = "libm" @@ -1499,9 +1499,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.4" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" [[package]] name = "memoffset" @@ -1518,9 +1518,9 @@ version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b9b8653cec6897f73b519a43fba5ee3d50f62fe9af80b428accdcc093b4a849" dependencies = [ - "ahash 0.7.6", + "ahash 0.7.8", "metrics-macros", - "portable-atomic", + "portable-atomic 0.3.20", ] [[package]] @@ -1530,12 +1530,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8603921e1f54ef386189335f288441af761e0fc61bcb552168d9cedfe63ebc70" dependencies = [ "hyper 0.14.24", - "indexmap 1.9.2", + "indexmap 1.9.3", "ipnet", "metrics", "metrics-util", "parking_lot 0.12.1", - "portable-atomic", + "portable-atomic 0.3.20", "quanta", "thiserror", "tokio", @@ -1565,7 +1565,7 @@ dependencies = [ "metrics", "num_cpus", "parking_lot 0.12.1", - "portable-atomic", + "portable-atomic 0.3.20", "quanta", "sketches-ddsketch", ] @@ -1944,9 +1944,18 @@ checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" [[package]] name = "portable-atomic" -version = "0.3.19" +version = "0.3.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26f6a7b87c2e435a3241addceeeff740ff8b7e76b74c13bf9acb17fa454ea00b" +checksum = "e30165d31df606f5726b090ec7592c308a0eaf61721ff64c9a3018e344a8753e" +dependencies = [ + "portable-atomic 1.8.0", +] + +[[package]] +name = "portable-atomic" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d30538d42559de6b034bc76fd6dd4c38961b1ee5c6c56e3808c50128fdbc22ce" [[package]] name = "ppv-lite86" @@ -1971,7 +1980,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f12335488a2f3b0a83b14edad48dca9879ce89b2edd10e80237e4e852dd645e" dependencies = [ "proc-macro2", - "syn 2.0.77", + "syn 2.0.66", ] [[package]] @@ -2010,7 +2019,7 @@ dependencies = [ "prost", "prost-types", "regex", - "syn 2.0.77", + "syn 2.0.66", "tempfile", ] @@ -2024,7 +2033,7 @@ dependencies = [ "itertools", "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.66", ] [[package]] @@ -2060,9 +2069,9 @@ checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" [[package]] name = "quinn" -version = "0.11.3" +version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b22d8e7369034b9a7132bc2008cac12f2013c8132b45e0554e6e20e2617f2156" +checksum = "8c7c5fdde3cdae7203427dc4f0a68fe0ed09833edc525a03456b153b79828684" dependencies = [ "bytes", "pin-project-lite", @@ -2078,9 +2087,9 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.6" +version = "0.11.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba92fb39ec7ad06ca2582c0ca834dfeadcaf06ddfc8e635c80aa7e1c05315fdd" +checksum = "fadfaed2cd7f389d0161bb73eeb07b7b78f8691047a6f3e73caaeae55310a4a6" dependencies = [ "bytes", "rand", @@ -2095,15 +2104,15 @@ dependencies = [ [[package]] name = "quinn-udp" -version = "0.5.4" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bffec3605b73c6f1754535084a85229fa8a30f86014e6c81aeec4abb68b0285" +checksum = "4fe68c2e9e1a1234e218683dbdf9f9dfcb094113c5ac2b938dfcb9bab4c4140b" dependencies = [ "libc", "once_cell", "socket2 0.5.7", "tracing", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -2198,14 +2207,14 @@ dependencies = [ [[package]] name = "regex" -version = "1.10.6" +version = "1.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" +checksum = "12de2eff854e5fa4b1295edd650e227e9d8fb0c9e90b12e7f36d6a6811791a29" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.7", - "regex-syntax 0.8.4", + "regex-automata 0.3.7", + "regex-syntax 0.7.5", ] [[package]] @@ -2219,13 +2228,13 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.7" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" +checksum = "49530408a136e16e5b486e883fbb6ba058e8e4e8ae6621a77b048b314336e629" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.4", + "regex-syntax 0.7.5", ] [[package]] @@ -2236,15 +2245,15 @@ checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" [[package]] name = "regex-syntax" -version = "0.8.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" +checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" [[package]] name = "reqwest" -version = "0.12.7" +version = "0.12.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8f4955649ef5c38cc7f9e8aa41761d48fb9677197daea9984dc54f56aad5e63" +checksum = "f713147fbe92361e52392c73b8c9e48c04c6625bce969ef54dc901e58e042a7b" dependencies = [ "base64 0.22.1", "bytes", @@ -2379,9 +2388,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.12" +version = "0.23.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c58f8c84392efc0a126acce10fa59ff7b3d2ac06ab451a33f2741989b806b044" +checksum = "05cff451f60db80f490f3c182b77c35260baace73209e9cdbbe526bfe3a4d402" dependencies = [ "once_cell", "ring", @@ -2393,9 +2402,9 @@ dependencies = [ [[package]] name = "rustls-pemfile" -version = "2.1.3" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "196fe16b00e106300d3e45ecfcb764fa292a535d7326a29a5875c579c7417425" +checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d" dependencies = [ "base64 0.22.1", "rustls-pki-types", @@ -2403,15 +2412,15 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.8.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0a2ce646f8655401bb81e7927b812614bd5d91dbc968696be50603510fcaf0" +checksum = "16f1201b3c9a7ee8039bcadc17b7e605e2945b27eee7631788c1bd2b0643674b" [[package]] name = "rustls-webpki" -version = "0.102.6" +version = "0.102.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e6b52d4fda176fd835fdc55a835d4a89b8499cad995885a21149d5ad62f852e" +checksum = "ff448f7e92e913c4b7d4c6d8e4540a1724b319b4152b8aef6d4cf8339712b33e" dependencies = [ "ring", "rustls-pki-types", @@ -2453,7 +2462,7 @@ checksum = "1db149f81d46d2deba7cd3c50772474707729550221e69588478ebf9ada425ae" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.66", ] [[package]] @@ -2482,7 +2491,7 @@ checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.66", ] [[package]] @@ -2543,9 +2552,9 @@ dependencies = [ [[package]] name = "sha2" -version = "0.10.6" +version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" dependencies = [ "cfg-if", "cpufeatures", @@ -2896,9 +2905,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.77" +version = "2.0.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" +checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5" dependencies = [ "proc-macro2", "quote", @@ -2943,22 +2952,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.38" +version = "1.0.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a9cd18aa97d5c45c6603caea1da6628790b37f7a34b6ca89522331c5180fed0" +checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.38" +version = "1.0.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f" +checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" dependencies = [ "proc-macro2", "quote", - "syn 1.0.107", + "syn 2.0.66", ] [[package]] @@ -3011,7 +3020,7 @@ checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.66", ] [[package]] @@ -3126,11 +3135,10 @@ checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" [[package]] name = "tracing" -version = "0.1.37" +version = "0.1.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ - "cfg-if", "log", "pin-project-lite", "tracing-attributes", @@ -3139,20 +3147,20 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.23" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 1.0.107", + "syn 2.0.66", ] [[package]] name = "tracing-core" -version = "0.1.30" +version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" dependencies = [ "once_cell", "valuable", @@ -3220,12 +3228,9 @@ checksum = "9e79c4d996edb816c91e4308506774452e55e95c3c9de07b6729e17e15a5ef81" [[package]] name = "unicase" -version = "2.7.0" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" -dependencies = [ - "version_check", -] +checksum = "7e51b68083f157f853b6379db119d1c1be0e6e4dec98101079dec41f6f5cf6df" [[package]] name = "unicode-bidi" @@ -3319,7 +3324,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55137c122f712d9330fd985d66fa61bdc381752e89c35708c13ce63049a3002c" dependencies = [ "quote", - "syn 2.0.77", + "syn 2.0.66", ] [[package]] @@ -3351,7 +3356,7 @@ dependencies = [ "proc-macro2", "quote", "serde", - "syn 2.0.77", + "syn 2.0.66", "toml", "uniffi_build", "uniffi_meta", @@ -3484,7 +3489,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.66", "wasm-bindgen-shared", ] @@ -3518,7 +3523,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.66", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -3531,9 +3536,9 @@ checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" [[package]] name = "web-sys" -version = "0.3.61" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e33b99f4b23ba3eec1a53ac264e35a755f00e966e0065077d6027c0f575b0b97" +checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" dependencies = [ "js-sys", "wasm-bindgen", @@ -3541,9 +3546,9 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "0.26.3" +version = "0.26.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd7c23921eeb1713a4e851530e9b9756e4fb0e89978582942612524cf09f01cd" +checksum = "841c67bff177718f1d4dfefde8d8f0e78f9b6589319ba88312f567fc5841a958" dependencies = [ "rustls-pki-types", ] @@ -3679,6 +3684,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-targets" version = "0.42.1" @@ -3883,7 +3897,7 @@ checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.66", ] [[package]] From 63a35c78acc9cadef3e75b2f84562a4ddc41706d Mon Sep 17 00:00:00 2001 From: dusk Date: Fri, 3 Jan 2025 20:19:50 +0900 Subject: [PATCH 010/179] chore: move command parser into crates --- {lib => crates}/commands/Cargo.toml | 0 {lib => crates}/commands/build.rs | 0 {lib => crates}/commands/src/commands.udl | 0 {lib => crates}/commands/src/lib.rs | 0 {lib => crates}/commands/src/string.rs | 0 {lib => crates}/commands/src/token.rs | 0 {lib => crates}/commands/uniffi.toml | 0 7 files changed, 0 insertions(+), 0 deletions(-) rename {lib => crates}/commands/Cargo.toml (100%) rename {lib => crates}/commands/build.rs (100%) rename {lib => crates}/commands/src/commands.udl (100%) rename {lib => crates}/commands/src/lib.rs (100%) rename {lib => crates}/commands/src/string.rs (100%) rename {lib => crates}/commands/src/token.rs (100%) rename {lib => crates}/commands/uniffi.toml (100%) diff --git a/lib/commands/Cargo.toml b/crates/commands/Cargo.toml similarity index 100% rename from lib/commands/Cargo.toml rename to crates/commands/Cargo.toml diff --git a/lib/commands/build.rs b/crates/commands/build.rs similarity index 100% rename from lib/commands/build.rs rename to crates/commands/build.rs diff --git a/lib/commands/src/commands.udl b/crates/commands/src/commands.udl similarity index 100% rename from lib/commands/src/commands.udl rename to crates/commands/src/commands.udl diff --git a/lib/commands/src/lib.rs b/crates/commands/src/lib.rs similarity index 100% rename from lib/commands/src/lib.rs rename to crates/commands/src/lib.rs diff --git a/lib/commands/src/string.rs b/crates/commands/src/string.rs similarity index 100% rename from lib/commands/src/string.rs rename to crates/commands/src/string.rs diff --git a/lib/commands/src/token.rs b/crates/commands/src/token.rs similarity index 100% rename from lib/commands/src/token.rs rename to crates/commands/src/token.rs diff --git a/lib/commands/uniffi.toml b/crates/commands/uniffi.toml similarity index 100% rename from lib/commands/uniffi.toml rename to crates/commands/uniffi.toml From a6482d929c22879058a77a3a3fbbd7900a8bfc55 Mon Sep 17 00:00:00 2001 From: dusk Date: Fri, 3 Jan 2025 20:26:52 +0900 Subject: [PATCH 011/179] chore: update cargo lock, ignore generated commands file --- .gitignore | 1 + Cargo.lock | 382 +++++++++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 356 insertions(+), 27 deletions(-) diff --git a/.gitignore b/.gitignore index e088092e..a4f9031e 100644 --- a/.gitignore +++ b/.gitignore @@ -31,6 +31,7 @@ logs/ .version recipe.json .docker-bin/ +PluralKit.Bot/commands.cs # nix .nix-process-compose diff --git a/Cargo.lock b/Cargo.lock index 132356fb..444d0a91 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -120,6 +120,47 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +[[package]] +name = "askama" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b79091df18a97caea757e28cd2d5fda49c6cd4bd01ddffd7ff01ace0c0ad2c28" +dependencies = [ + "askama_derive", + "askama_escape", +] + +[[package]] +name = "askama_derive" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19fe8d6cb13c4714962c072ea496f3392015f0989b1a2847bb4b2d9effd71d83" +dependencies = [ + "askama_parser", + "basic-toml", + "mime", + "mime_guess", + "proc-macro2", + "quote", + "serde", + "syn 2.0.87", +] + +[[package]] +name = "askama_escape" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "619743e34b5ba4e9703bba34deac3427c72507c7159f5fd030aea8cac0cfe341" + +[[package]] +name = "askama_parser" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acb1161c6b64d1c3d83108213c2a2533a342ac225aabd0bda218278c2ddb00c0" +dependencies = [ + "nom", +] + [[package]] name = "async-trait" version = "0.1.80" @@ -128,7 +169,7 @@ checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.87", ] [[package]] @@ -362,6 +403,24 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" +[[package]] +name = "basic-toml" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "823388e228f614e9558c6804262db37960ec8821856535f5c3f59913140558f8" +dependencies = [ + "serde", +] + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -420,6 +479,38 @@ dependencies = [ "either", ] +[[package]] +name = "camino" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b96ec4966b5813e2c0507c1f86115c8c5abaadc3980879c3424042a02fd1ad3" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo-platform" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eee4243f1f26fc7a42710e7439c149e2b10b05472f88090acce52632f231a73a" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror", +] + [[package]] name = "cc" version = "1.1.31" @@ -458,6 +549,14 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" +[[package]] +name = "commands" +version = "0.1.0" +dependencies = [ + "lazy_static", + "uniffi", +] + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -483,7 +582,7 @@ dependencies = [ "rust-ini 0.19.0", "serde", "serde_json", - "toml", + "toml 0.8.19", "yaml-rust", ] @@ -656,7 +755,7 @@ dependencies = [ "ident_case", "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.87", ] [[package]] @@ -667,7 +766,7 @@ checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" dependencies = [ "darling_core", "quote", - "syn 2.0.66", + "syn 2.0.87", ] [[package]] @@ -821,7 +920,7 @@ dependencies = [ "heck 0.4.1", "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.87", ] [[package]] @@ -972,7 +1071,16 @@ checksum = "1458c6e22d36d61507034d5afecc64f105c1d39712b7ac6ec3b352c423f715cc" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.87", +] + +[[package]] +name = "fs-err" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a41f105fe1d5b6b34b2055e3dc59bb79b46b48b2040b9e6c7b4b5de097aa41" +dependencies = [ + "autocfg", ] [[package]] @@ -1042,7 +1150,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.87", ] [[package]] @@ -1143,6 +1251,17 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" +[[package]] +name = "goblin" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d6b4de4a8eb6c46a8c77e1d3be942cb9a8bf073c22374578e5ba4b08ed0ff68" +dependencies = [ + "log", + "plain", + "scroll", +] + [[package]] name = "h2" version = "0.3.26" @@ -1600,7 +1719,7 @@ checksum = "0122b7114117e64a63ac49f752a5ca4624d534c7b1c7de796ac196381cd2d947" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.87", ] [[package]] @@ -1801,7 +1920,7 @@ checksum = "5cf92c10c7e361d6b99666ec1c6f9805b0bea2c3bd8c78dc6fe98ac5bd78db11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.87", ] [[package]] @@ -1886,6 +2005,16 @@ version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -1929,7 +2058,7 @@ version = "0.1.0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.87", ] [[package]] @@ -2049,6 +2178,12 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +[[package]] +name = "oneshot-uniffi" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c548d5c78976f6955d72d0ced18c48ca07030f7a1d4024529fedd7c1c01b29c" + [[package]] name = "openssl-probe" version = "0.1.5" @@ -2260,6 +2395,12 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + [[package]] name = "pluralkit_models" version = "0.1.0" @@ -2936,6 +3077,26 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" +[[package]] +name = "scroll" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04c565b551bafbef4157586fa379538366e4385d42082f255bfd96e4fe8519da" +dependencies = [ + "scroll_derive", +] + +[[package]] +name = "scroll_derive" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1db149f81d46d2deba7cd3c50772474707729550221e69588478ebf9ada425ae" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + [[package]] name = "sct" version = "0.7.1" @@ -2966,7 +3127,7 @@ dependencies = [ "heck 0.4.1", "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.87", "thiserror", ] @@ -2998,6 +3159,9 @@ name = "semver" version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "58bc9567378fc7690d6b2addae4e60ac2eeea07becb2c64b9f218b53865cba2a" +dependencies = [ + "serde", +] [[package]] name = "sentry" @@ -3110,9 +3274,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.203" +version = "1.0.217" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094" +checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" dependencies = [ "serde_derive", ] @@ -3129,13 +3293,13 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.203" +version = "1.0.217" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" +checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.87", ] [[package]] @@ -3167,7 +3331,7 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.87", ] [[package]] @@ -3295,6 +3459,12 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f27f6278552951f1f2b8cf9da965d10969b2efdea95a6ec47987ab46edfe263a" +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + [[package]] name = "sketches-ddsketch" version = "0.2.0" @@ -3439,7 +3609,7 @@ dependencies = [ "quote", "sqlx-core", "sqlx-macros-core", - "syn 2.0.66", + "syn 2.0.87", ] [[package]] @@ -3462,7 +3632,7 @@ dependencies = [ "sqlx-mysql", "sqlx-postgres", "sqlx-sqlite", - "syn 2.0.66", + "syn 2.0.87", "tempfile", "tokio", "url", @@ -3580,6 +3750,12 @@ dependencies = [ "uuid", ] +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "stringprep" version = "0.1.5" @@ -3610,9 +3786,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.66" +version = "2.0.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5" +checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" dependencies = [ "proc-macro2", "quote", @@ -3684,7 +3860,7 @@ checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.87", ] [[package]] @@ -3788,7 +3964,7 @@ checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.87", ] [[package]] @@ -3871,6 +4047,15 @@ dependencies = [ "tracing", ] +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + [[package]] name = "toml" version = "0.8.19" @@ -3990,7 +4175,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.87", ] [[package]] @@ -4172,6 +4357,12 @@ dependencies = [ "libc", ] +[[package]] +name = "unicase" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" + [[package]] name = "unicode-bidi" version = "0.3.10" @@ -4211,6 +4402,134 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" +[[package]] +name = "uniffi" +version = "0.25.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21345172d31092fd48c47fd56c53d4ae9e41c4b1f559fb8c38c1ab1685fd919f" +dependencies = [ + "anyhow", + "uniffi_build", + "uniffi_core", + "uniffi_macros", +] + +[[package]] +name = "uniffi_bindgen" +version = "0.25.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd992f2929a053829d5875af1eff2ee3d7a7001cb3b9a46cc7895f2caede6940" +dependencies = [ + "anyhow", + "askama", + "camino", + "cargo_metadata", + "fs-err", + "glob", + "goblin", + "heck 0.4.1", + "once_cell", + "paste", + "serde", + "toml 0.5.11", + "uniffi_meta", + "uniffi_testing", + "uniffi_udl", +] + +[[package]] +name = "uniffi_build" +version = "0.25.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "001964dd3682d600084b3aaf75acf9c3426699bc27b65e96bb32d175a31c74e9" +dependencies = [ + "anyhow", + "camino", + "uniffi_bindgen", +] + +[[package]] +name = "uniffi_checksum_derive" +version = "0.25.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55137c122f712d9330fd985d66fa61bdc381752e89c35708c13ce63049a3002c" +dependencies = [ + "quote", + "syn 2.0.87", +] + +[[package]] +name = "uniffi_core" +version = "0.25.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6121a127a3af1665cd90d12dd2b3683c2643c5103281d0fed5838324ca1fad5b" +dependencies = [ + "anyhow", + "bytes", + "camino", + "log", + "once_cell", + "oneshot-uniffi", + "paste", + "static_assertions", +] + +[[package]] +name = "uniffi_macros" +version = "0.25.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11cf7a58f101fcedafa5b77ea037999b88748607f0ef3a33eaa0efc5392e92e4" +dependencies = [ + "bincode", + "camino", + "fs-err", + "once_cell", + "proc-macro2", + "quote", + "serde", + "syn 2.0.87", + "toml 0.5.11", + "uniffi_build", + "uniffi_meta", +] + +[[package]] +name = "uniffi_meta" +version = "0.25.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71dc8573a7b1ac4b71643d6da34888273ebfc03440c525121f1b3634ad3417a2" +dependencies = [ + "anyhow", + "bytes", + "siphasher", + "uniffi_checksum_derive", +] + +[[package]] +name = "uniffi_testing" +version = "0.25.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "118448debffcb676ddbe8c5305fb933ab7e0123753e659a71dc4a693f8d9f23c" +dependencies = [ + "anyhow", + "camino", + "cargo_metadata", + "fs-err", + "once_cell", +] + +[[package]] +name = "uniffi_udl" +version = "0.25.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "889edb7109c6078abe0e53e9b4070cf74a6b3468d141bdf5ef1bd4d1dc24a1c3" +dependencies = [ + "anyhow", + "uniffi_meta", + "uniffi_testing", + "weedle2", +] + [[package]] name = "untrusted" version = "0.7.1" @@ -4327,7 +4646,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.87", "wasm-bindgen-shared", ] @@ -4361,7 +4680,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.87", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -4439,6 +4758,15 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "weedle2" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e79c5206e1f43a2306fd64bdb95025ee4228960f2e6c5a8b173f3caaf807741" +dependencies = [ + "nom", +] + [[package]] name = "weezl" version = "0.1.8" @@ -4785,7 +5113,7 @@ checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.87", ] [[package]] From 59bba77ae9cb3ae21229d98bb4ee60fc054e4d3e Mon Sep 17 00:00:00 2001 From: dusk Date: Sat, 4 Jan 2025 00:18:10 +0900 Subject: [PATCH 012/179] build: fix bot build, add generate commands binding app --- .../CommandSystem/Context/Context.cs | 3 +- flake.lock | 33 +- flake.nix | 290 ++++++++++-------- 3 files changed, 184 insertions(+), 142 deletions(-) diff --git a/PluralKit.Bot/CommandSystem/Context/Context.cs b/PluralKit.Bot/CommandSystem/Context/Context.cs index 6a5eaf19..c212e2e5 100644 --- a/PluralKit.Bot/CommandSystem/Context/Context.cs +++ b/PluralKit.Bot/CommandSystem/Context/Context.cs @@ -48,7 +48,6 @@ public class Context _commandMessageService = provider.Resolve(); CommandPrefix = message.Content?.Substring(0, commandParseOffset); DefaultPrefix = prefixes[0]; - Parameters = new Parameters(message.Content?.Substring(commandParseOffset)); Rest = provider.Resolve(); Cluster = provider.Resolve(); @@ -87,7 +86,7 @@ public class Context public readonly string CommandPrefix; public readonly string DefaultPrefix; - public readonly Parameters Parameters; + public readonly ParametersFFI Parameters; internal readonly IDatabase Database; internal readonly ModelRepository Repository; diff --git a/flake.lock b/flake.lock index a13615a1..0e29c367 100644 --- a/flake.lock +++ b/flake.lock @@ -104,11 +104,11 @@ ] }, "locked": { - "lastModified": 1734953472, - "narHash": "sha256-zWPAJFo7NNhSXbOc6YRAXbrWzcJGxNPtutKTTZz46Bs=", + "lastModified": 1735917398, + "narHash": "sha256-RkwVkqozmbYvwX63Q4GNkNCsPuHR8sUIax40J5A4l3A=", "owner": "yusdacra", "repo": "nix-cargo-integration", - "rev": "e71c873cf3b0dfa52e9550d580531e41eb4b4c6a", + "rev": "aff54b572b75af13a6b31108ff7732d17674ad43", "type": "github" }, "original": { @@ -227,7 +227,8 @@ "process-compose": "process-compose", "services": "services", "systems": "systems", - "treefmt": "treefmt" + "treefmt": "treefmt", + "uniffi-bindgen-cs": "uniffi-bindgen-cs" } }, "rust-overlay": { @@ -238,11 +239,11 @@ ] }, "locked": { - "lastModified": 1734834660, - "narHash": "sha256-bm8V+Cu8rWJA+vKQnc94mXTpSDgvedyoDKxTVi/uJfw=", + "lastModified": 1735871325, + "narHash": "sha256-6Ta5E4mhSfCP6LdkzkG2+BciLOCPeLKuYTJ6lOHW+mI=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "b070e6030118680977bc2388868c4b3963872134", + "rev": "a599f011db521766cbaf7c2f5874182485554f00", "type": "github" }, "original": { @@ -322,6 +323,24 @@ "repo": "treefmt-nix", "type": "github" } + }, + "uniffi-bindgen-cs": { + "flake": false, + "locked": { + "lastModified": 1732196488, + "narHash": "sha256-zqNUUFd3OSAwmMh+hnN6AVpGnwu+ZJ1jjivbzN1k5Io=", + "ref": "refs/heads/main", + "rev": "fe5cd23943fd3aec335e2bb8f709ec1956992ae9", + "revCount": 115, + "submodules": true, + "type": "git", + "url": "https://github.com/NordSecurity/uniffi-bindgen-cs?tag=v0.8.3%2Bv0.25.0" + }, + "original": { + "submodules": true, + "type": "git", + "url": "https://github.com/NordSecurity/uniffi-bindgen-cs?tag=v0.8.3%2Bv0.25.0" + } } }, "root": "root", diff --git a/flake.nix b/flake.nix index ac600aa4..238ae0e1 100644 --- a/flake.nix +++ b/flake.nix @@ -16,6 +16,8 @@ nci.inputs.nixpkgs.follows = "nixpkgs"; nci.inputs.dream2nix.follows = "d2n"; nci.inputs.treefmt.follows = "treefmt"; + uniffi-bindgen-cs.url = "git+https://github.com/NordSecurity/uniffi-bindgen-cs?tag=v0.8.3+v0.25.0&submodules=1"; + uniffi-bindgen-cs.flake = false; # misc treefmt.url = "github:numtide/treefmt-nix"; treefmt.inputs.nixpkgs.follows = "nixpkgs"; @@ -57,39 +59,58 @@ ]; runScript = cmd; }; + uniffi-bindgen-cs = config.nci.lib.buildCrate { + src = inp.uniffi-bindgen-cs; + cratePath = "bindgen"; + # TODO: uniffi fails to build with our toolchain because the ahash dep that uniffi-bindgen-cs uses is too old and uses removed stdsimd feature + mkRustToolchain = pkgs: pkgs.cargo; + }; rustOutputs = config.nci.outputs; composeCfg = config.process-compose."dev"; in { - # _module.args.pkgs = import inp.nixpkgs { - # inherit system; - # config.permittedInsecurePackages = [ "dotnet-sdk-6.0.428" ]; - # }; - treefmt = { projectRootFile = "flake.nix"; programs.nixfmt.enable = true; }; - nci.toolchainConfig = { - channel = "nightly"; - }; nci.projects."pluralkit-services" = { path = ./.; export = false; }; - # nci.crates."gateway" = { - # depsDrvConfig.mkDerivation = { - # nativeBuildInputs = [ pkgs.protobuf ]; - # }; - # drvConfig.mkDerivation = { - # nativeBuildInputs = [ pkgs.protobuf ]; - # }; - # }; + nci.crates."commands" = rec { + depsDrvConfig.env = { + # we don't really need this since the lib is just used to generate the bindings + doNotRemoveReferencesToVendorDir = true; + }; + depsDrvConfig.mkDerivation = { + # also not really needed + dontPatchShebangs = true; + }; + drvConfig = depsDrvConfig; + }; + + apps = { + generate-command-parser-bindings.program = pkgs.writeShellApplication { + name = "generate-command-parser-bindings"; + runtimeInputs = [ + (config.nci.toolchains.mkBuild pkgs) + self'.devShells.services.stdenv.cc + pkgs.csharpier + pkgs.coreutils + uniffi-bindgen-cs + ]; + text = '' + set -x + [ "''${1:-}" == "" ] && cargo build --package commands --release + uniffi-bindgen-cs "''${1:-target/debug/libcommands.so}" --library --out-dir="''${2:-./PluralKit.Bot}" + ''; + }; + }; # TODO: expose other rust packages after it's verified they build and work properly - packages = lib.genAttrs ["gateway"] (name: rustOutputs.${name}.packages.release); + packages = lib.genAttrs [ "gateway" "commands" ] (name: rustOutputs.${name}.packages.release); # TODO: package the bot itself (dotnet) devShells = { @@ -97,136 +118,139 @@ bot = (mkBotEnv "bash").env; }; - process-compose."dev" = let - dataDir = ".nix-process-compose"; - pluralkitConfCheck = '' - [[ -f "pluralkit.conf" ]] || (echo "pluralkit config not found, please copy pluralkit.conf.example to pluralkit.conf and edit it" && exit 1) - ''; - sourceDotenv = '' - [[ -f ".env" ]] && echo "sourcing .env file..." && export "$(xargs < .env)" - ''; - in { - imports = [ inp.services.processComposeModules.default ]; - - settings.log_location = "${dataDir}/log"; - - settings.environment = { - DOTNET_CLI_TELEMETRY_OPTOUT = "1"; - NODE_OPTIONS = "--openssl-legacy-provider"; - }; - - services.redis."redis" = { - enable = true; - dataDir = "${dataDir}/redis"; - }; - services.postgres."postgres" = { - enable = true; - dataDir = "${dataDir}/postgres"; - initialScript.before = '' - CREATE DATABASE pluralkit; - CREATE USER postgres WITH password 'postgres'; - GRANT ALL PRIVILEGES ON DATABASE pluralkit TO postgres; - ALTER DATABASE pluralkit OWNER TO postgres; + process-compose."dev" = + let + dataDir = ".nix-process-compose"; + pluralkitConfCheck = '' + [[ -f "pluralkit.conf" ]] || (echo "pluralkit config not found, please copy pluralkit.conf.example to pluralkit.conf and edit it" && exit 1) ''; - }; + sourceDotenv = '' + [[ -f ".env" ]] && echo "sourcing .env file..." && export "$(xargs < .env)" + ''; + in + { + imports = [ inp.services.processComposeModules.default ]; - settings.processes = - let - procCfg = composeCfg.settings.processes; - mkServiceInitProcess = - { - name, - inputs ? [ ], - ... - }: - let - shell = rustOutputs.${name}.devShell; - in - { + settings.log_location = "${dataDir}/log"; + + settings.environment = { + DOTNET_CLI_TELEMETRY_OPTOUT = "1"; + NODE_OPTIONS = "--openssl-legacy-provider"; + }; + + services.redis."redis" = { + enable = true; + dataDir = "${dataDir}/redis"; + }; + services.postgres."postgres" = { + enable = true; + dataDir = "${dataDir}/postgres"; + initialScript.before = '' + CREATE DATABASE pluralkit; + CREATE USER postgres WITH password 'postgres'; + GRANT ALL PRIVILEGES ON DATABASE pluralkit TO postgres; + ALTER DATABASE pluralkit OWNER TO postgres; + ''; + }; + + settings.processes = + let + procCfg = composeCfg.settings.processes; + mkServiceInitProcess = + { + name, + inputs ? [ ], + ... + }: + let + shell = rustOutputs.${name}.devShell; + in + { + command = pkgs.writeShellApplication { + name = "pluralkit-${name}-init"; + runtimeInputs = + (with pkgs; [ + coreutils + shell.stdenv.cc + ]) + ++ shell.nativeBuildInputs + ++ inputs; + text = '' + ${sourceDotenv} + set -x + ${pluralkitConfCheck} + exec cargo build --package ${name} + ''; + }; + }; + in + { + ### bot ### + pluralkit-bot-init = { command = pkgs.writeShellApplication { - name = "pluralkit-${name}-init"; - runtimeInputs = - (with pkgs; [ - coreutils - shell.stdenv.cc - ]) - ++ shell.nativeBuildInputs - ++ inputs; + name = "pluralkit-bot-init"; + runtimeInputs = [ + pkgs.coreutils + pkgs.git + ]; text = '' ${sourceDotenv} set -x ${pluralkitConfCheck} - exec cargo build --package ${name} + ${self'.apps.generate-command-parser-bindings.program} + exec ${mkBotEnv "dotnet build -c Release -o obj/"}/bin/env ''; }; }; - in - { - ### bot ### - pluralkit-bot-init = { - command = pkgs.writeShellApplication { - name = "pluralkit-bot-init"; - runtimeInputs = [ - pkgs.coreutils - pkgs.git - ]; - text = '' - ${sourceDotenv} - set -x - ${pluralkitConfCheck} - exec ${mkBotEnv "dotnet build -c Release -o obj/"}/bin/env - ''; + pluralkit-bot = { + command = pkgs.writeShellApplication { + name = "pluralkit-bot"; + runtimeInputs = [ pkgs.coreutils ]; + text = '' + ${sourceDotenv} + set -x + exec ${mkBotEnv "dotnet obj/PluralKit.Bot.dll"}/bin/env + ''; + }; + depends_on.pluralkit-bot-init.condition = "process_completed_successfully"; + depends_on.postgres.condition = "process_healthy"; + depends_on.redis.condition = "process_healthy"; + depends_on.pluralkit-gateway.condition = "process_healthy"; + # TODO: add liveness check + ready_log_line = "Received Ready"; }; - }; - pluralkit-bot = { - command = pkgs.writeShellApplication { - name = "pluralkit-bot"; - runtimeInputs = [ pkgs.coreutils ]; - text = '' - ${sourceDotenv} - set -x - exec ${mkBotEnv "dotnet obj/PluralKit.Bot.dll"}/bin/env - ''; + ### gateway ### + pluralkit-gateway-init = mkServiceInitProcess { + name = "gateway"; }; - depends_on.pluralkit-bot-init.condition = "process_completed_successfully"; - depends_on.postgres.condition = "process_healthy"; - depends_on.redis.condition = "process_healthy"; - depends_on.pluralkit-gateway.condition = "process_healthy"; - # TODO: add liveness check - ready_log_line = "Received Ready"; - }; - ### gateway ### - pluralkit-gateway-init = mkServiceInitProcess { - name = "gateway"; - }; - pluralkit-gateway = { - command = pkgs.writeShellApplication { - name = "pluralkit-gateway"; - runtimeInputs = with pkgs; [ - coreutils - curl - gnugrep - ]; - text = '' - ${sourceDotenv} - set -x - exec target/debug/gateway - ''; + pluralkit-gateway = { + command = pkgs.writeShellApplication { + name = "pluralkit-gateway"; + runtimeInputs = with pkgs; [ + coreutils + curl + gnugrep + ]; + text = '' + ${sourceDotenv} + set -x + exec target/debug/gateway + ''; + }; + depends_on.postgres.condition = "process_healthy"; + depends_on.redis.condition = "process_healthy"; + depends_on.pluralkit-gateway-init.condition = "process_completed_successfully"; + # configure health checks + # TODO: don't assume port? + liveness_probe.exec.command = ''curl -s -o /dev/null -w "%{http_code}" http://localhost:5000/stats | grep "302"''; + liveness_probe.period_seconds = 5; + readiness_probe.exec.command = procCfg.pluralkit-gateway.liveness_probe.exec.command; + readiness_probe.period_seconds = 5; + readiness_probe.initial_delay_seconds = 3; }; - depends_on.postgres.condition = "process_healthy"; - depends_on.redis.condition = "process_healthy"; - depends_on.pluralkit-gateway-init.condition = "process_completed_successfully"; - # configure health checks - # TODO: don't assume port? - liveness_probe.exec.command = ''curl -s -o /dev/null -w "%{http_code}" http://localhost:5000/stats | grep "302"''; - liveness_probe.period_seconds = 5; - readiness_probe.exec.command = procCfg.pluralkit-gateway.liveness_probe.exec.command; - readiness_probe.period_seconds = 5; - readiness_probe.initial_delay_seconds = 3; + # TODO: add the rest of the services }; - # TODO: add the rest of the services - }; - }; + }; }; }; } From 313261e46e3fda8ec364e0491608ee0c0ccd3ded Mon Sep 17 00:00:00 2001 From: dusk Date: Sat, 4 Jan 2025 00:38:27 +0900 Subject: [PATCH 013/179] build: add rust-src componenet for rust-analyzer and such --- rust-toolchain.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 2792be0d..15df6b03 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,3 +1,3 @@ [toolchain] channel = "nightly-2024-08-20" - +components = ["rust-src", "rustfmt"] \ No newline at end of file From 6a0c8fdc26bd0285da6353fae85697286c3375c4 Mon Sep 17 00:00:00 2001 From: dusk Date: Sat, 4 Jan 2025 00:38:51 +0900 Subject: [PATCH 014/179] build(nix): use release path for commands lib output --- flake.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flake.nix b/flake.nix index 238ae0e1..58aa5a2b 100644 --- a/flake.nix +++ b/flake.nix @@ -104,7 +104,7 @@ text = '' set -x [ "''${1:-}" == "" ] && cargo build --package commands --release - uniffi-bindgen-cs "''${1:-target/debug/libcommands.so}" --library --out-dir="''${2:-./PluralKit.Bot}" + uniffi-bindgen-cs "''${1:-target/release/libcommands.so}" --library --out-dir="''${2:-./PluralKit.Bot}" ''; }; }; From 7949e7e1f9c491d409c553b095e223faba36fd2e Mon Sep 17 00:00:00 2001 From: dusk Date: Sat, 4 Jan 2025 00:39:21 +0900 Subject: [PATCH 015/179] fix: enable chrono on sqlx, libpk uses it --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 33eee2b6..f397843f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,7 @@ sentry = { version = "0.34.0", default-features = false, features = ["backtrace serde = { version = "1.0.196", features = ["derive"] } serde_json = "1.0.117" signal-hook = "0.3.17" -sqlx = { version = "0.8.2", features = ["runtime-tokio", "postgres", "time", "macros", "uuid"] } +sqlx = { version = "0.8.2", features = ["runtime-tokio", "postgres", "time", "chrono", "macros", "uuid"] } tokio = { version = "1.36.0", features = ["full"] } tracing = "0.1.40" tracing-subscriber = { version = "0.3.16", features = ["env-filter", "json"] } From 11842e76373aed339c6e0e0ee1c8cb49f5a3b622 Mon Sep 17 00:00:00 2001 From: dusk Date: Sat, 4 Jan 2025 00:45:48 +0900 Subject: [PATCH 016/179] build(nix): dont forget to copy commands lib for bot to use --- flake.nix | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/flake.nix b/flake.nix index 58aa5a2b..dc5d11c3 100644 --- a/flake.nix +++ b/flake.nix @@ -103,8 +103,13 @@ ]; text = '' set -x - [ "''${1:-}" == "" ] && cargo build --package commands --release - uniffi-bindgen-cs "''${1:-target/release/libcommands.so}" --library --out-dir="''${2:-./PluralKit.Bot}" + commandslib="''${1:-}" + if [ "$commandslib" == "" ]; then + cargo build --package commands --release + commandslib="target/release/libcommands.so" + fi + uniffi-bindgen-cs "$commandslib" --library --out-dir="''${2:-./PluralKit.Bot}" + cp -f "$commandslib" obj/ ''; }; }; From 405ac11d745849ad16555287d0c922b3f445effa Mon Sep 17 00:00:00 2001 From: dusk Date: Sat, 4 Jan 2025 02:49:04 +0900 Subject: [PATCH 017/179] refactor(commands): use smolstr, use a decl macro to get rid of all the borrows while creating commands --- Cargo.lock | 26 +++++++++ crates/commands/Cargo.toml | 1 + crates/commands/src/lib.rs | 99 +++++++++++++++++++---------------- crates/commands/src/string.rs | 10 ++-- crates/commands/src/token.rs | 24 +++++---- 5 files changed, 101 insertions(+), 59 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 444d0a91..62650368 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -445,6 +445,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "borsh" +version = "1.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2506947f73ad44e344215ccd6403ac2ae18cd8e046e581a441bf8d199f257f03" +dependencies = [ + "cfg_aliases", +] + [[package]] name = "bumpalo" version = "3.12.0" @@ -528,6 +537,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "chrono" version = "0.4.38" @@ -554,6 +569,7 @@ name = "commands" version = "0.1.0" dependencies = [ "lazy_static", + "smol_str", "uniffi", ] @@ -3489,6 +3505,16 @@ dependencies = [ "serde", ] +[[package]] +name = "smol_str" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9676b89cd56310a87b93dec47b11af744f34d5fc9f367b829474eec0a891350d" +dependencies = [ + "borsh", + "serde", +] + [[package]] name = "socket2" version = "0.4.7" diff --git a/crates/commands/Cargo.toml b/crates/commands/Cargo.toml index efff1dea..65e28ad4 100644 --- a/crates/commands/Cargo.toml +++ b/crates/commands/Cargo.toml @@ -10,6 +10,7 @@ crate-type = ["cdylib"] lazy_static = { workspace = true } uniffi = { version = "0.25" } +smol_str = "0.3.2" [build-dependencies] uniffi = { version = "0.25", features = [ "build" ] } diff --git a/crates/commands/src/lib.rs b/crates/commands/src/lib.rs index 6b787403..a98bdd9e 100644 --- a/crates/commands/src/lib.rs +++ b/crates/commands/src/lib.rs @@ -7,6 +7,7 @@ uniffi::include_scaffolding!("commands"); mod string; mod token; +use smol_str::SmolStr; use token::*; // todo!: move all this stuff into a different file @@ -74,77 +75,84 @@ struct Command { cb: String, } -fn command(tokens: &[&Token], help: &str, cb: &str) -> Command { +fn command(tokens: impl IntoIterator, help: impl ToString, cb: impl ToString) -> Command { Command { - tokens: tokens.iter().map(|&x| x.clone()).collect(), + tokens: tokens.into_iter().collect(), help: help.to_string(), cb: cb.to_string(), } } +macro_rules! command { + ([$($v:expr),+], $help:expr, $cb:expr) => { + $crate::command([$($v.clone()),*], $help, $cb) + }; +} + mod commands { + use smol_str::SmolStr; + use super::Token; - use super::command; - use super::Token::*; - - fn cmd(value: &str) -> Token { - Token::Value(vec![value.to_string()]) + fn cmd(value: impl Into) -> Token { + Token::Value(vec![value.into()]) } - pub fn cmd_with_alias(value: &[&str]) -> Token { - Token::Value(value.iter().map(|x| x.to_string()).collect()) + pub fn cmd_with_alias(value: impl IntoIterator>) -> Token { + Token::Value(value.into_iter().map(Into::into).collect()) } // todo: this needs to have less ampersands -alyssa pub fn happy() -> Vec { - let system = &cmd_with_alias(&["system", "s"]); - let member = &cmd_with_alias(&["member", "m"]); - let description = &cmd_with_alias(&["description", "desc"]); - let privacy = &cmd_with_alias(&["privacy", "priv"]); + use Token::*; + + let system = cmd_with_alias(["system", "s"]); + let member = cmd_with_alias(["member", "m"]); + let description = cmd_with_alias(["description", "desc"]); + let privacy = cmd_with_alias(["privacy", "priv"]); vec![ - command(&[&cmd("help")], "help", "Shows the help command"), - command( - &[system], + command!([cmd("help")], "help", "Shows the help command"), + command!( + [system], "system_show", - "Shows information about your system", + "Shows information about your system" ), - command(&[system, &cmd("new")], "system_new", "Creates a new system"), - command( - &[member, &cmd_with_alias(&["new", "n"])], + command!([system, cmd("new")], "system_new", "Creates a new system"), + command!( + [member, cmd_with_alias(["new", "n"])], "member_new", - "Creates a new system member", + "Creates a new system member" ), - command( - &[member, &MemberRef], + command!( + [member, MemberRef], "member_show", - "Shows information about a member", + "Shows information about a member" ), - command( - &[member, &MemberRef, description], + command!( + [member, MemberRef, description], "member_desc_show", - "Shows a member's description", + "Shows a member's description" ), - command( - &[member, &MemberRef, description, &FullString], + command!( + [member, MemberRef, description, FullString], "member_desc_update", - "Changes a member's description", + "Changes a member's description" ), - command( - &[member, &MemberRef, privacy], + command!( + [member, MemberRef, privacy], "member_privacy_show", - "Displays a member's current privacy settings", + "Displays a member's current privacy settings" ), - command( - &[ + command!( + [ member, - &MemberRef, + MemberRef, privacy, - &MemberPrivacyTarget, - &PrivacyLevel, + MemberPrivacyTarget, + PrivacyLevel ], "member_privacy_update", - "Changes a member's privacy settings", + "Changes a member's privacy settings" ), ] } @@ -188,9 +196,9 @@ pub struct ParsedCommand { /// - optionally a short-circuit error fn next_token( possible_tokens: Vec, - input: String, + input: SmolStr, current_pos: usize, -) -> Result<(Token, Option, usize), Option> { +) -> Result<(Token, Option, usize), Option> { // get next parameter, matching quotes let param = crate::string::next_param(input.clone(), current_pos); println!("matched: {param:?}\n---"); @@ -203,7 +211,7 @@ fn next_token( { return Ok(( Token::Flag, - Some(value.trim_start_matches('-').to_string()), + Some(value.trim_start_matches('-').into()), new_pos, )); } @@ -230,6 +238,7 @@ fn next_token( } fn parse_command(input: String) -> CommandResult { + let input: SmolStr = input.into(); let mut local_tree: TreeBranch = COMMAND_TREE.clone(); // end position of all currently matched tokens @@ -247,13 +256,13 @@ fn parse_command(input: String) -> CommandResult { Ok((found_token, arg, new_pos)) => { current_pos = new_pos; if let Token::Flag = found_token { - flags.insert(arg.unwrap(), None); + flags.insert(arg.unwrap().into(), None); // don't try matching flags as tree elements continue; } if let Some(arg) = arg { - args.push(arg); + args.push(arg.into()); } if let Some(next_tree) = local_tree.branches.get(&found_token) { @@ -280,7 +289,7 @@ fn parse_command(input: String) -> CommandResult { } Err(Some(short_circuit)) => { return CommandResult::Err { - error: short_circuit, + error: short_circuit.into(), }; } } diff --git a/crates/commands/src/string.rs b/crates/commands/src/string.rs index 0ea7659a..dd48fbf0 100644 --- a/crates/commands/src/string.rs +++ b/crates/commands/src/string.rs @@ -1,5 +1,7 @@ use std::collections::HashMap; +use smol_str::SmolStr; + lazy_static::lazy_static! { // Dictionary of (left, right) quote pairs // Each char in the string is an individual quote, multi-char strings imply "one of the following chars" @@ -43,14 +45,14 @@ lazy_static::lazy_static! { // very very simple quote matching // quotes need to be at start/end of words, and are ignored if a closing quote is not present // WTB POSIX quoting: https://pubs.opengroup.org/onlinepubs/9799919799/utilities/V3_chap02.html -pub fn next_param(input: String, current_pos: usize) -> Option<(String, usize)> { +pub(super) fn next_param(input: SmolStr, current_pos: usize) -> Option<(SmolStr, usize)> { if input.len() == current_pos { return None; } let leading_whitespace_count = input[..current_pos].len() - input[..current_pos].trim_start().len(); - let substr_to_match = input[current_pos + leading_whitespace_count..].to_string(); + let substr_to_match: SmolStr = input[current_pos + leading_whitespace_count..].into(); println!("stuff: {input} {current_pos} {leading_whitespace_count}"); println!("to match: {substr_to_match}"); @@ -67,7 +69,7 @@ pub fn next_param(input: String, current_pos: usize) -> Option<(String, usize)> { // return quoted string, without quotes return Some(( - substr_to_match[1..pos - 1].to_string(), + substr_to_match[1..pos - 1].into(), current_pos + pos + 1, )); } @@ -78,7 +80,7 @@ pub fn next_param(input: String, current_pos: usize) -> Option<(String, usize)> // find next whitespace character for (pos, char) in substr_to_match.clone().char_indices() { if char.is_whitespace() { - return Some((substr_to_match[..pos].to_string(), current_pos + pos + 1)); + return Some((substr_to_match[..pos].into(), current_pos + pos + 1)); } } diff --git a/crates/commands/src/token.rs b/crates/commands/src/token.rs index 9d952a24..e66ca9d4 100644 --- a/crates/commands/src/token.rs +++ b/crates/commands/src/token.rs @@ -1,3 +1,5 @@ +use smol_str::SmolStr; + #[derive(Debug, Clone, Eq, Hash, PartialEq)] pub enum Token { /// Token used to represent a finished command (i.e. no more parameters required) @@ -5,10 +7,10 @@ pub enum Token { Empty, /// A bot-defined value ("member" in `pk;member MyName`) - Value(Vec), + Value(Vec), /// A command defined by multiple values // todo! - MultiValue(Vec>), + MultiValue(Vec>), FullString, @@ -26,20 +28,20 @@ pub enum Token { pub enum TokenMatchResult { NoMatch, /// Token matched, optionally with a value. - Match(Option), + Match(Option), } // move this somewhere else lazy_static::lazy_static!( - static ref MEMBER_PRIVACY_TARGETS: Vec = vec![ - "visibility".to_string(), - "name".to_string(), - "todo".to_string() - ]; + static ref MEMBER_PRIVACY_TARGETS: Vec = [ + "visibility", + "name", + "todo", + ].into_iter().map(SmolStr::new_static).collect(); ); impl Token { - pub fn try_match(&self, input: Option) -> TokenMatchResult { + pub fn try_match(&self, input: Option) -> TokenMatchResult { // short circuit on empty things if matches!(self, Self::Empty) && input.is_none() { return TokenMatchResult::Match(None); @@ -66,7 +68,9 @@ impl Token { Self::FullString => return TokenMatchResult::Match(Some(input)), Self::MemberRef => return TokenMatchResult::Match(Some(input)), Self::MemberPrivacyTarget - if MEMBER_PRIVACY_TARGETS.contains(&input.trim().to_string()) => + if MEMBER_PRIVACY_TARGETS + .iter() + .any(|target| target.eq(input.trim())) => { return TokenMatchResult::Match(Some(input)) } From af523a4c232e5f08dae2cd14bb1e0ef85433849b Mon Sep 17 00:00:00 2001 From: dusk Date: Sat, 4 Jan 2025 07:35:04 +0900 Subject: [PATCH 018/179] refactor(commands): separate commands definitions and other code into modules --- crates/commands/src/commands.rs | 57 ++++ crates/commands/src/commands/admin.rs | 1 + crates/commands/src/commands/api.rs | 1 + crates/commands/src/commands/autoproxy.rs | 1 + crates/commands/src/commands/checks.rs | 1 + crates/commands/src/commands/commands.rs | 1 + crates/commands/src/commands/config.rs | 1 + crates/commands/src/commands/dashboard.rs | 1 + crates/commands/src/commands/debug.rs | 1 + crates/commands/src/commands/fun.rs | 1 + crates/commands/src/commands/group.rs | 1 + crates/commands/src/commands/help.rs | 10 + crates/commands/src/commands/import_export.rs | 1 + crates/commands/src/commands/member.rs | 50 ++++ crates/commands/src/commands/message.rs | 1 + crates/commands/src/commands/misc.rs | 1 + crates/commands/src/commands/random.rs | 1 + crates/commands/src/commands/server_config.rs | 1 + crates/commands/src/commands/switch.rs | 1 + crates/commands/src/commands/system.rs | 23 ++ crates/commands/src/lib.rs | 283 +++++------------- crates/commands/src/string.rs | 5 +- crates/commands/src/token.rs | 8 + crates/commands/src/tree.rs | 57 ++++ 24 files changed, 293 insertions(+), 216 deletions(-) create mode 100644 crates/commands/src/commands.rs create mode 100644 crates/commands/src/commands/admin.rs create mode 100644 crates/commands/src/commands/api.rs create mode 100644 crates/commands/src/commands/autoproxy.rs create mode 100644 crates/commands/src/commands/checks.rs create mode 100644 crates/commands/src/commands/commands.rs create mode 100644 crates/commands/src/commands/config.rs create mode 100644 crates/commands/src/commands/dashboard.rs create mode 100644 crates/commands/src/commands/debug.rs create mode 100644 crates/commands/src/commands/fun.rs create mode 100644 crates/commands/src/commands/group.rs create mode 100644 crates/commands/src/commands/help.rs create mode 100644 crates/commands/src/commands/import_export.rs create mode 100644 crates/commands/src/commands/member.rs create mode 100644 crates/commands/src/commands/message.rs create mode 100644 crates/commands/src/commands/misc.rs create mode 100644 crates/commands/src/commands/random.rs create mode 100644 crates/commands/src/commands/server_config.rs create mode 100644 crates/commands/src/commands/switch.rs create mode 100644 crates/commands/src/commands/system.rs create mode 100644 crates/commands/src/tree.rs diff --git a/crates/commands/src/commands.rs b/crates/commands/src/commands.rs new file mode 100644 index 00000000..1e9e06e8 --- /dev/null +++ b/crates/commands/src/commands.rs @@ -0,0 +1,57 @@ +pub mod admin; +pub mod api; +pub mod autoproxy; +pub mod checks; +pub mod commands; +pub mod config; +pub mod dashboard; +pub mod debug; +pub mod fun; +pub mod group; +pub mod help; +pub mod import_export; +pub mod member; +pub mod message; +pub mod misc; +pub mod random; +pub mod server_config; +pub mod switch; +pub mod system; + +use crate::{command, token::Token}; + +#[derive(Clone)] +pub struct Command { + // TODO: fix hygiene + pub tokens: Vec, + pub help: String, + pub cb: String, +} + +impl Command { + pub fn new( + tokens: impl IntoIterator, + help: impl ToString, + cb: impl ToString, + ) -> Self { + Self { + tokens: tokens.into_iter().collect(), + help: help.to_string(), + cb: cb.to_string(), + } + } +} + +#[macro_export] +macro_rules! command { + ([$($v:expr),+], $cb:expr, $help:expr) => { + $crate::commands::Command::new([$($v.clone()),*], $help, $cb) + }; +} + +pub fn all() -> Vec { + (help::cmds()) + .chain(system::cmds()) + .chain(member::cmds()) + .collect() +} diff --git a/crates/commands/src/commands/admin.rs b/crates/commands/src/commands/admin.rs new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/crates/commands/src/commands/admin.rs @@ -0,0 +1 @@ + diff --git a/crates/commands/src/commands/api.rs b/crates/commands/src/commands/api.rs new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/crates/commands/src/commands/api.rs @@ -0,0 +1 @@ + diff --git a/crates/commands/src/commands/autoproxy.rs b/crates/commands/src/commands/autoproxy.rs new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/crates/commands/src/commands/autoproxy.rs @@ -0,0 +1 @@ + diff --git a/crates/commands/src/commands/checks.rs b/crates/commands/src/commands/checks.rs new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/crates/commands/src/commands/checks.rs @@ -0,0 +1 @@ + diff --git a/crates/commands/src/commands/commands.rs b/crates/commands/src/commands/commands.rs new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/crates/commands/src/commands/commands.rs @@ -0,0 +1 @@ + diff --git a/crates/commands/src/commands/config.rs b/crates/commands/src/commands/config.rs new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/crates/commands/src/commands/config.rs @@ -0,0 +1 @@ + diff --git a/crates/commands/src/commands/dashboard.rs b/crates/commands/src/commands/dashboard.rs new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/crates/commands/src/commands/dashboard.rs @@ -0,0 +1 @@ + diff --git a/crates/commands/src/commands/debug.rs b/crates/commands/src/commands/debug.rs new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/crates/commands/src/commands/debug.rs @@ -0,0 +1 @@ + diff --git a/crates/commands/src/commands/fun.rs b/crates/commands/src/commands/fun.rs new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/crates/commands/src/commands/fun.rs @@ -0,0 +1 @@ + diff --git a/crates/commands/src/commands/group.rs b/crates/commands/src/commands/group.rs new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/crates/commands/src/commands/group.rs @@ -0,0 +1 @@ + diff --git a/crates/commands/src/commands/help.rs b/crates/commands/src/commands/help.rs new file mode 100644 index 00000000..e91d13f3 --- /dev/null +++ b/crates/commands/src/commands/help.rs @@ -0,0 +1,10 @@ +use super::*; + +pub fn cmds() -> impl Iterator { + [command!( + [Token::cmd("help")], + "help", + "Shows the help command" + )] + .into_iter() +} diff --git a/crates/commands/src/commands/import_export.rs b/crates/commands/src/commands/import_export.rs new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/crates/commands/src/commands/import_export.rs @@ -0,0 +1 @@ + diff --git a/crates/commands/src/commands/member.rs b/crates/commands/src/commands/member.rs new file mode 100644 index 00000000..d92e59cc --- /dev/null +++ b/crates/commands/src/commands/member.rs @@ -0,0 +1,50 @@ +use super::*; + +pub fn cmds() -> impl Iterator { + use Token::*; + + let member = Token::cmd_with_alias(["member", "m"]); + let description = Token::cmd_with_alias(["description", "desc"]); + let privacy = Token::cmd_with_alias(["privacy", "priv"]); + let new = Token::cmd_with_alias(["new", "n"]); + + [ + command!( + [member, new, MemberRef], + "member_new", + "Creates a new system member" + ), + command!( + [member, MemberRef], + "member_show", + "Shows information about a member" + ), + command!( + [member, MemberRef, description], + "member_desc_show", + "Shows a member's description" + ), + command!( + [member, MemberRef, description, FullString], + "member_desc_update", + "Changes a member's description" + ), + command!( + [member, MemberRef, privacy], + "member_privacy_show", + "Displays a member's current privacy settings" + ), + command!( + [ + member, + MemberRef, + privacy, + MemberPrivacyTarget, + PrivacyLevel + ], + "member_privacy_update", + "Changes a member's privacy settings" + ), + ] + .into_iter() +} diff --git a/crates/commands/src/commands/message.rs b/crates/commands/src/commands/message.rs new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/crates/commands/src/commands/message.rs @@ -0,0 +1 @@ + diff --git a/crates/commands/src/commands/misc.rs b/crates/commands/src/commands/misc.rs new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/crates/commands/src/commands/misc.rs @@ -0,0 +1 @@ + diff --git a/crates/commands/src/commands/random.rs b/crates/commands/src/commands/random.rs new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/crates/commands/src/commands/random.rs @@ -0,0 +1 @@ + diff --git a/crates/commands/src/commands/server_config.rs b/crates/commands/src/commands/server_config.rs new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/crates/commands/src/commands/server_config.rs @@ -0,0 +1 @@ + diff --git a/crates/commands/src/commands/switch.rs b/crates/commands/src/commands/switch.rs new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/crates/commands/src/commands/switch.rs @@ -0,0 +1 @@ + diff --git a/crates/commands/src/commands/system.rs b/crates/commands/src/commands/system.rs new file mode 100644 index 00000000..9d1bdf1f --- /dev/null +++ b/crates/commands/src/commands/system.rs @@ -0,0 +1,23 @@ +use super::*; + +pub fn cmds() -> impl Iterator { + use Token::*; + + let system = Token::cmd_with_alias(["system", "s"]); + let new = Token::cmd_with_alias(["new", "n"]); + + [ + command!( + [system], + "system_show", + "Shows information about your system" + ), + command!([system, new], "system_new", "Creates a new system"), + command!( + [system, new, FullString], + "system_new", + "Creates a new system" + ), + ] + .into_iter() +} diff --git a/crates/commands/src/lib.rs b/crates/commands/src/lib.rs index a98bdd9e..4a86d841 100644 --- a/crates/commands/src/lib.rs +++ b/crates/commands/src/lib.rs @@ -1,162 +1,20 @@ #![feature(let_chains)] -use core::panic; -use std::{cmp::Ordering, collections::HashMap}; +mod commands; +mod string; +mod token; +mod tree; uniffi::include_scaffolding!("commands"); -mod string; -mod token; +use core::panic; +use std::collections::HashMap; + use smol_str::SmolStr; -use token::*; +use tree::TreeBranch; -// todo!: move all this stuff into a different file -// lib.rs should just have exported symbols and command definitions - -#[derive(Debug, Clone)] -struct TreeBranch { - current_command_key: Option, - /// branches.keys(), but sorted by specificity - possible_tokens: Vec, - branches: HashMap, -} - -impl TreeBranch { - fn register_command(&mut self, command: Command) { - let mut current_branch = self; - // iterate over tokens in command - for token in command.tokens { - // recursively get or create a sub-branch for each token - current_branch = current_branch.branches.entry(token).or_insert(TreeBranch { - current_command_key: None, - possible_tokens: vec![], - branches: HashMap::new(), - }) - } - // when we're out of tokens, add an Empty branch with the callback and no sub-branches - current_branch.branches.insert( - Token::Empty, - TreeBranch { - current_command_key: Some(command.cb), - possible_tokens: vec![], - branches: HashMap::new(), - }, - ); - } - - fn sort_tokens(&mut self) { - for branch in self.branches.values_mut() { - branch.sort_tokens(); - } - - // put Value tokens at the end - // i forget exactly how this works - // todo!: document this before PR mergs - self.possible_tokens = self - .branches - .keys() - .into_iter() - .map(|v| v.clone()) - .collect(); - self.possible_tokens.sort_by(|v, _| { - if matches!(v, Token::Value(_)) { - Ordering::Greater - } else { - Ordering::Less - } - }); - } -} - -#[derive(Clone)] -struct Command { - tokens: Vec, - help: String, - cb: String, -} - -fn command(tokens: impl IntoIterator, help: impl ToString, cb: impl ToString) -> Command { - Command { - tokens: tokens.into_iter().collect(), - help: help.to_string(), - cb: cb.to_string(), - } -} - -macro_rules! command { - ([$($v:expr),+], $help:expr, $cb:expr) => { - $crate::command([$($v.clone()),*], $help, $cb) - }; -} - -mod commands { - use smol_str::SmolStr; - - use super::Token; - - fn cmd(value: impl Into) -> Token { - Token::Value(vec![value.into()]) - } - - pub fn cmd_with_alias(value: impl IntoIterator>) -> Token { - Token::Value(value.into_iter().map(Into::into).collect()) - } - - // todo: this needs to have less ampersands -alyssa - pub fn happy() -> Vec { - use Token::*; - - let system = cmd_with_alias(["system", "s"]); - let member = cmd_with_alias(["member", "m"]); - let description = cmd_with_alias(["description", "desc"]); - let privacy = cmd_with_alias(["privacy", "priv"]); - vec![ - command!([cmd("help")], "help", "Shows the help command"), - command!( - [system], - "system_show", - "Shows information about your system" - ), - command!([system, cmd("new")], "system_new", "Creates a new system"), - command!( - [member, cmd_with_alias(["new", "n"])], - "member_new", - "Creates a new system member" - ), - command!( - [member, MemberRef], - "member_show", - "Shows information about a member" - ), - command!( - [member, MemberRef, description], - "member_desc_show", - "Shows a member's description" - ), - command!( - [member, MemberRef, description, FullString], - "member_desc_update", - "Changes a member's description" - ), - command!( - [member, MemberRef, privacy], - "member_privacy_show", - "Displays a member's current privacy settings" - ), - command!( - [ - member, - MemberRef, - privacy, - MemberPrivacyTarget, - PrivacyLevel - ], - "member_privacy_update", - "Changes a member's privacy settings" - ), - ] - } -} +pub use commands::Command; +pub use token::*; lazy_static::lazy_static! { static ref COMMAND_TREE: TreeBranch = { @@ -166,7 +24,7 @@ lazy_static::lazy_static! { branches: HashMap::new(), }; - commands::happy().iter().for_each(|x| tree.register_command(x.clone())); + crate::commands::all().iter().for_each(|x| tree.register_command(x.clone())); tree.sort_tokens(); @@ -187,6 +45,66 @@ pub struct ParsedCommand { pub flags: HashMap>, } +fn parse_command(input: String) -> CommandResult { + let input: SmolStr = input.into(); + let mut local_tree: TreeBranch = COMMAND_TREE.clone(); + + // end position of all currently matched tokens + let mut current_pos = 0; + + let mut args: Vec = Vec::new(); + let mut flags: HashMap> = HashMap::new(); + + loop { + let next = next_token( + local_tree.possible_tokens.clone(), + input.clone(), + current_pos, + ); + match next { + Ok((found_token, arg, new_pos)) => { + current_pos = new_pos; + if let Token::Flag = found_token { + flags.insert(arg.unwrap().into(), None); + // don't try matching flags as tree elements + continue; + } + + if let Some(arg) = arg { + args.push(arg.into()); + } + + if let Some(next_tree) = local_tree.branches.get(&found_token) { + local_tree = next_tree.clone(); + } else { + panic!("found token could not match tree, at {input}"); + } + } + Err(None) => { + if let Some(command_ref) = local_tree.current_command_key { + return CommandResult::Ok { + command: ParsedCommand { + command_ref: command_ref.to_owned(), + args, + flags, + }, + }; + } + // todo: check if last token is a common incorrect unquote (multi-member names etc) + // todo: check if this is a system name in pk;s command + return CommandResult::Err { + error: "Command not found.".to_string(), + }; + } + Err(Some(short_circuit)) => { + return CommandResult::Err { + error: short_circuit.into(), + }; + } + } + } +} + /// Find the next token from an either raw or partially parsed command string /// /// Returns: @@ -236,62 +154,3 @@ fn next_token( Err(None) } - -fn parse_command(input: String) -> CommandResult { - let input: SmolStr = input.into(); - let mut local_tree: TreeBranch = COMMAND_TREE.clone(); - - // end position of all currently matched tokens - let mut current_pos = 0; - - let mut args: Vec = Vec::new(); - let mut flags: HashMap> = HashMap::new(); - - loop { - match next_token( - local_tree.possible_tokens.clone(), - input.clone(), - current_pos, - ) { - Ok((found_token, arg, new_pos)) => { - current_pos = new_pos; - if let Token::Flag = found_token { - flags.insert(arg.unwrap().into(), None); - // don't try matching flags as tree elements - continue; - } - - if let Some(arg) = arg { - args.push(arg.into()); - } - - if let Some(next_tree) = local_tree.branches.get(&found_token) { - local_tree = next_tree.clone(); - } else { - panic!("found token could not match tree, at {input}"); - } - } - Err(None) => { - if let Some(command_ref) = local_tree.current_command_key { - return CommandResult::Ok { - command: ParsedCommand { - command_ref, - args, - flags, - }, - }; - } - // todo: check if last token is a common incorrect unquote (multi-member names etc) - // todo: check if this is a system name in pk;s command - return CommandResult::Err { - error: "Command not found.".to_string(), - }; - } - Err(Some(short_circuit)) => { - return CommandResult::Err { - error: short_circuit.into(), - }; - } - } - } -} diff --git a/crates/commands/src/string.rs b/crates/commands/src/string.rs index dd48fbf0..e6ca6c72 100644 --- a/crates/commands/src/string.rs +++ b/crates/commands/src/string.rs @@ -68,10 +68,7 @@ pub(super) fn next_param(input: SmolStr, current_pos: usize) -> Option<(SmolStr, .is_whitespace() { // return quoted string, without quotes - return Some(( - substr_to_match[1..pos - 1].into(), - current_pos + pos + 1, - )); + return Some((substr_to_match[1..pos - 1].into(), current_pos + pos + 1)); } } } diff --git a/crates/commands/src/token.rs b/crates/commands/src/token.rs index e66ca9d4..60df6dfc 100644 --- a/crates/commands/src/token.rs +++ b/crates/commands/src/token.rs @@ -41,6 +41,14 @@ lazy_static::lazy_static!( ); impl Token { + pub fn cmd(value: impl Into) -> Self { + Self::Value(vec![value.into()]) + } + + pub fn cmd_with_alias(value: impl IntoIterator>) -> Self { + Self::Value(value.into_iter().map(Into::into).collect()) + } + pub fn try_match(&self, input: Option) -> TokenMatchResult { // short circuit on empty things if matches!(self, Self::Empty) && input.is_none() { diff --git a/crates/commands/src/tree.rs b/crates/commands/src/tree.rs new file mode 100644 index 00000000..d897dc4f --- /dev/null +++ b/crates/commands/src/tree.rs @@ -0,0 +1,57 @@ +use crate::{commands::Command, Token}; +use std::{cmp::Ordering, collections::HashMap}; + +#[derive(Debug, Clone)] +pub struct TreeBranch { + pub current_command_key: Option, + /// branches.keys(), but sorted by specificity + pub possible_tokens: Vec, + pub branches: HashMap, +} + +impl TreeBranch { + pub fn register_command(&mut self, command: Command) { + let mut current_branch = self; + // iterate over tokens in command + for token in command.tokens { + // recursively get or create a sub-branch for each token + current_branch = current_branch.branches.entry(token).or_insert(TreeBranch { + current_command_key: None, + possible_tokens: vec![], + branches: HashMap::new(), + }) + } + // when we're out of tokens, add an Empty branch with the callback and no sub-branches + current_branch.branches.insert( + Token::Empty, + TreeBranch { + current_command_key: Some(command.cb), + possible_tokens: vec![], + branches: HashMap::new(), + }, + ); + } + + pub fn sort_tokens(&mut self) { + for branch in self.branches.values_mut() { + branch.sort_tokens(); + } + + // put Value tokens at the end + // i forget exactly how this works + // todo!: document this before PR mergs + self.possible_tokens = self + .branches + .keys() + .into_iter() + .map(|v| v.clone()) + .collect(); + self.possible_tokens.sort_by(|v, _| { + if matches!(v, Token::Value(_)) { + Ordering::Greater + } else { + Ordering::Less + } + }); + } +} From b9867e7ea36eba9b4b81b2364e692b34d7442bec Mon Sep 17 00:00:00 2001 From: dusk Date: Sat, 4 Jan 2025 07:43:55 +0900 Subject: [PATCH 019/179] refactor(commands): also use smolstr for commands themselves --- crates/commands/src/commands.rs | 14 ++++++++------ crates/commands/src/lib.rs | 2 +- crates/commands/src/tree.rs | 4 +++- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/crates/commands/src/commands.rs b/crates/commands/src/commands.rs index 1e9e06e8..b87dc878 100644 --- a/crates/commands/src/commands.rs +++ b/crates/commands/src/commands.rs @@ -18,26 +18,28 @@ pub mod server_config; pub mod switch; pub mod system; +use smol_str::SmolStr; + use crate::{command, token::Token}; #[derive(Clone)] pub struct Command { // TODO: fix hygiene pub tokens: Vec, - pub help: String, - pub cb: String, + pub help: SmolStr, + pub cb: SmolStr, } impl Command { pub fn new( tokens: impl IntoIterator, - help: impl ToString, - cb: impl ToString, + help: impl Into, + cb: impl Into, ) -> Self { Self { tokens: tokens.into_iter().collect(), - help: help.to_string(), - cb: cb.to_string(), + help: help.into(), + cb: cb.into(), } } } diff --git a/crates/commands/src/lib.rs b/crates/commands/src/lib.rs index 4a86d841..4b701e1d 100644 --- a/crates/commands/src/lib.rs +++ b/crates/commands/src/lib.rs @@ -84,7 +84,7 @@ fn parse_command(input: String) -> CommandResult { if let Some(command_ref) = local_tree.current_command_key { return CommandResult::Ok { command: ParsedCommand { - command_ref: command_ref.to_owned(), + command_ref: command_ref.into(), args, flags, }, diff --git a/crates/commands/src/tree.rs b/crates/commands/src/tree.rs index d897dc4f..56c53336 100644 --- a/crates/commands/src/tree.rs +++ b/crates/commands/src/tree.rs @@ -1,9 +1,11 @@ +use smol_str::SmolStr; + use crate::{commands::Command, Token}; use std::{cmp::Ordering, collections::HashMap}; #[derive(Debug, Clone)] pub struct TreeBranch { - pub current_command_key: Option, + pub current_command_key: Option, /// branches.keys(), but sorted by specificity pub possible_tokens: Vec, pub branches: HashMap, From b89bc44a27e87826e101b92eae3506ef8fce6049 Mon Sep 17 00:00:00 2001 From: dusk Date: Sat, 4 Jan 2025 07:56:40 +0900 Subject: [PATCH 020/179] refactor(commands): quote pairs code now uses smolstr and doesnt use macro --- crates/commands/src/string.rs | 40 +++++++++++++++++------------------ 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/crates/commands/src/string.rs b/crates/commands/src/string.rs index e6ca6c72..e8a1ffb3 100644 --- a/crates/commands/src/string.rs +++ b/crates/commands/src/string.rs @@ -1,42 +1,42 @@ use std::collections::HashMap; -use smol_str::SmolStr; +use smol_str::{SmolStr, ToSmolStr}; lazy_static::lazy_static! { // Dictionary of (left, right) quote pairs // Each char in the string is an individual quote, multi-char strings imply "one of the following chars" // Certain languages can have quote patterns that have a different character for open and close - pub static ref QUOTE_PAIRS: HashMap = { - let mut pairs = HashMap::new(); + pub static ref QUOTE_PAIRS: HashMap = { + let mut pairs: HashMap = HashMap::new(); - macro_rules! insert_pair { - ($a:literal, $b:literal) => { - pairs.insert($a.to_string(), $b.to_string()); - // make it easier to look up right quotes - for char in $a.chars() { - pairs.insert(char.to_string(), $b.to_string()); - } + let mut insert_pair = |a: &'static str, b: &'static str| { + let a = SmolStr::new_static(a); + let b = SmolStr::new_static(b); + pairs.insert(a.clone(), b.clone()); + // make it easier to look up right quotes + for char in a.chars() { + pairs.insert(char.to_smolstr(), b.clone()); } - } + }; // Basic - insert_pair!( "'", "'" ); // ASCII single quotes - insert_pair!( "\"", "\"" ); // ASCII double quotes + insert_pair( "'", "'" ); // ASCII single quotes + insert_pair( "\"", "\"" ); // ASCII double quotes // "Smart quotes" // Specifically ignore the left/right status of the quotes and match any combination of them // Left string also includes "low" quotes to allow for the low-high style used in some locales - insert_pair!( "\u{201C}\u{201D}\u{201F}\u{201E}", "\u{201C}\u{201D}\u{201F}" ); // double quotes - insert_pair!( "\u{2018}\u{2019}\u{201B}\u{201A}", "\u{2018}\u{2019}\u{201B}" ); // single quotes + insert_pair( "\u{201C}\u{201D}\u{201F}\u{201E}", "\u{201C}\u{201D}\u{201F}" ); // double quotes + insert_pair( "\u{2018}\u{2019}\u{201B}\u{201A}", "\u{2018}\u{2019}\u{201B}" ); // single quotes // Chevrons (normal and "fullwidth" variants) - insert_pair!( "\u{00AB}\u{300A}", "\u{00BB}\u{300B}" ); // double chevrons, pointing away (<>) - insert_pair!( "\u{00BB}\u{300B}", "\u{00AB}\u{300A}" ); // double chevrons, pointing together (>>text<<) - insert_pair!( "\u{2039}\u{3008}", "\u{203A}\u{3009}" ); // single chevrons, pointing away () - insert_pair!( "\u{203A}\u{3009}", "\u{2039}\u{3008}" ); // single chevrons, pointing together (>text<) + insert_pair( "\u{00AB}\u{300A}", "\u{00BB}\u{300B}" ); // double chevrons, pointing away (<>) + insert_pair( "\u{00BB}\u{300B}", "\u{00AB}\u{300A}" ); // double chevrons, pointing together (>>text<<) + insert_pair( "\u{2039}\u{3008}", "\u{203A}\u{3009}" ); // single chevrons, pointing away () + insert_pair( "\u{203A}\u{3009}", "\u{2039}\u{3008}" ); // single chevrons, pointing together (>text<) // Other - insert_pair!( "\u{300C}\u{300E}", "\u{300D}\u{300F}" ); // corner brackets (Japanese/Chinese) + insert_pair( "\u{300C}\u{300E}", "\u{300D}\u{300F}" ); // corner brackets (Japanese/Chinese) pairs }; From e70d69e45c3e4d957e9690bc128827ed9eb9e22a Mon Sep 17 00:00:00 2001 From: dusk Date: Sun, 5 Jan 2025 00:58:48 +0900 Subject: [PATCH 021/179] refactor: start to stop using ctx.Match --- PluralKit.Bot/CommandMeta/CommandTree.cs | 21 ++++---- PluralKit.Bot/CommandSystem/ParametersFFI.cs | 5 ++ crates/commands/src/commands.rs | 2 +- crates/commands/src/commands/fun.rs | 9 ++++ crates/commands/src/lib.rs | 4 +- flake.nix | 50 ++++++++++---------- 6 files changed, 56 insertions(+), 35 deletions(-) diff --git a/PluralKit.Bot/CommandMeta/CommandTree.cs b/PluralKit.Bot/CommandMeta/CommandTree.cs index 3c620e63..2c11d02f 100644 --- a/PluralKit.Bot/CommandMeta/CommandTree.cs +++ b/PluralKit.Bot/CommandMeta/CommandTree.cs @@ -6,6 +6,19 @@ public partial class CommandTree { public Task ExecuteCommand(Context ctx) { + switch (ctx.Parameters.Callback()) + { + case "fun_thunder": + return ctx.Execute(null, m => m.Thunder(ctx)); + default: + // don't send an "invalid command" response if the guild has those turned off + if (ctx.GuildConfig != null && ctx.GuildConfig!.InvalidCommandResponseEnabled != true) + return Task.CompletedTask; + + // remove compiler warning + return ctx.Reply( + $"{Emojis.Error} Unknown command {ctx.PeekArgument().AsCode()}. For a list of possible commands, see ."); + } if (ctx.Match("system", "s")) return HandleSystemCommand(ctx); if (ctx.Match("member", "m")) @@ -107,14 +120,6 @@ public partial class CommandTree return ctx.Execute(MemberRandom, m => m.Member(ctx, ctx.System)); if (ctx.Match("dashboard", "dash")) return ctx.Execute(Dashboard, m => m.Dashboard(ctx)); - - // don't send an "invalid command" response if the guild has those turned off - if (ctx.GuildConfig != null && ctx.GuildConfig!.InvalidCommandResponseEnabled != true) - return Task.CompletedTask; - - // remove compiler warning - return ctx.Reply( - $"{Emojis.Error} Unknown command {ctx.PeekArgument().AsCode()}. For a list of possible commands, see ."); } private async Task HandleAdminAbuseLogCommand(Context ctx) diff --git a/PluralKit.Bot/CommandSystem/ParametersFFI.cs b/PluralKit.Bot/CommandSystem/ParametersFFI.cs index dcd1097b..474b5833 100644 --- a/PluralKit.Bot/CommandSystem/ParametersFFI.cs +++ b/PluralKit.Bot/CommandSystem/ParametersFFI.cs @@ -29,6 +29,11 @@ public class ParametersFFI } } + public string Callback() + { + return _cb; + } + public string Pop() { if (_args.Count > _ptr + 1) Console.WriteLine($"pop: {_ptr + 1}, {_args[_ptr + 1]}"); diff --git a/crates/commands/src/commands.rs b/crates/commands/src/commands.rs index b87dc878..789240ab 100644 --- a/crates/commands/src/commands.rs +++ b/crates/commands/src/commands.rs @@ -22,7 +22,7 @@ use smol_str::SmolStr; use crate::{command, token::Token}; -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct Command { // TODO: fix hygiene pub tokens: Vec, diff --git a/crates/commands/src/commands/fun.rs b/crates/commands/src/commands/fun.rs index 8b137891..e14da01b 100644 --- a/crates/commands/src/commands/fun.rs +++ b/crates/commands/src/commands/fun.rs @@ -1 +1,10 @@ +use super::*; +pub fn cmds() -> impl Iterator { + [command!( + [Token::cmd("thunder")], + "fun_thunder", + "Shows the help command" + )] + .into_iter() +} diff --git a/crates/commands/src/lib.rs b/crates/commands/src/lib.rs index 4b701e1d..f2b4a969 100644 --- a/crates/commands/src/lib.rs +++ b/crates/commands/src/lib.rs @@ -1,6 +1,6 @@ #![feature(let_chains)] -mod commands; +pub mod commands; mod string; mod token; mod tree; @@ -17,7 +17,7 @@ pub use commands::Command; pub use token::*; lazy_static::lazy_static! { - static ref COMMAND_TREE: TreeBranch = { + pub static ref COMMAND_TREE: TreeBranch = { let mut tree = TreeBranch { current_command_key: None, possible_tokens: vec![], diff --git a/flake.nix b/flake.nix index dc5d11c3..a310bfeb 100644 --- a/flake.nix +++ b/flake.nix @@ -43,22 +43,6 @@ ... }: let - # this is used as devshell for bot, and in the process-compose processes as environment - mkBotEnv = - cmd: - pkgs.buildFHSEnv { - name = "env"; - targetPkgs = - pkgs: with pkgs; [ - coreutils - git - dotnet-sdk_8 - gcc - omnisharp-roslyn - bashInteractive - ]; - runScript = cmd; - }; uniffi-bindgen-cs = config.nci.lib.buildCrate { src = inp.uniffi-bindgen-cs; cratePath = "bindgen"; @@ -75,7 +59,7 @@ programs.nixfmt.enable = true; }; - nci.projects."pluralkit-services" = { + nci.projects."pk-services" = { path = ./.; export = false; }; @@ -118,9 +102,23 @@ packages = lib.genAttrs [ "gateway" "commands" ] (name: rustOutputs.${name}.packages.release); # TODO: package the bot itself (dotnet) - devShells = { - services = rustOutputs."pluralkit-services".devShell; - bot = (mkBotEnv "bash").env; + devShells = rec { + services = rustOutputs."pk-services".devShell; + bot = pkgs.mkShell { + name = "pkbot-devshell"; + nativeBuildInputs = with pkgs; [ + coreutils + git + dotnet-sdk_8 + gcc + omnisharp-roslyn + bashInteractive + ]; + }; + all = (pkgs.mkShell.override { stdenv = services.stdenv; }) { + name = "pk-devshell"; + nativeBuildInputs = bot.nativeBuildInputs ++ services.nativeBuildInputs; + }; }; process-compose."dev" = @@ -194,27 +192,31 @@ pluralkit-bot-init = { command = pkgs.writeShellApplication { name = "pluralkit-bot-init"; - runtimeInputs = [ + runtimeInputs = self'.devShells.bot.nativeBuildInputs ++ [ pkgs.coreutils pkgs.git + self'.devShells.bot.stdenv.cc ]; text = '' ${sourceDotenv} set -x ${pluralkitConfCheck} ${self'.apps.generate-command-parser-bindings.program} - exec ${mkBotEnv "dotnet build -c Release -o obj/"}/bin/env + exec dotnet build -c Release -o obj/ ''; }; }; pluralkit-bot = { command = pkgs.writeShellApplication { name = "pluralkit-bot"; - runtimeInputs = [ pkgs.coreutils ]; + runtimeInputs = self'.devShells.bot.nativeBuildInputs ++ [ + pkgs.coreutils + self'.devShells.bot.stdenv.cc + ]; text = '' ${sourceDotenv} set -x - exec ${mkBotEnv "dotnet obj/PluralKit.Bot.dll"}/bin/env + exec dotnet obj/PluralKit.Bot.dll ''; }; depends_on.pluralkit-bot-init.condition = "process_completed_successfully"; From ff121ecc5161c70dd52615e30c130488f31f7fe8 Mon Sep 17 00:00:00 2001 From: dusk Date: Sun, 5 Jan 2025 00:59:59 +0900 Subject: [PATCH 022/179] refactor(commands): ToToken trait for easier conversion into tokens --- crates/commands/src/commands.rs | 8 +++++-- crates/commands/src/commands/fun.rs | 2 +- crates/commands/src/commands/help.rs | 18 +++++++++++---- crates/commands/src/commands/member.rs | 8 +++---- crates/commands/src/commands/system.rs | 4 ++-- crates/commands/src/token.rs | 32 ++++++++++++++++++-------- 6 files changed, 49 insertions(+), 23 deletions(-) diff --git a/crates/commands/src/commands.rs b/crates/commands/src/commands.rs index 789240ab..b904bb59 100644 --- a/crates/commands/src/commands.rs +++ b/crates/commands/src/commands.rs @@ -20,7 +20,10 @@ pub mod system; use smol_str::SmolStr; -use crate::{command, token::Token}; +use crate::{ + command, + token::{ToToken, Token}, +}; #[derive(Clone, Debug)] pub struct Command { @@ -47,7 +50,7 @@ impl Command { #[macro_export] macro_rules! command { ([$($v:expr),+], $cb:expr, $help:expr) => { - $crate::commands::Command::new([$($v.clone()),*], $help, $cb) + $crate::commands::Command::new([$($v.to_token()),*], $help, $cb) }; } @@ -55,5 +58,6 @@ pub fn all() -> Vec { (help::cmds()) .chain(system::cmds()) .chain(member::cmds()) + .chain(fun::cmds()) .collect() } diff --git a/crates/commands/src/commands/fun.rs b/crates/commands/src/commands/fun.rs index e14da01b..a3ba0d9e 100644 --- a/crates/commands/src/commands/fun.rs +++ b/crates/commands/src/commands/fun.rs @@ -2,7 +2,7 @@ use super::*; pub fn cmds() -> impl Iterator { [command!( - [Token::cmd("thunder")], + ["thunder"], "fun_thunder", "Shows the help command" )] diff --git a/crates/commands/src/commands/help.rs b/crates/commands/src/commands/help.rs index e91d13f3..269d87a9 100644 --- a/crates/commands/src/commands/help.rs +++ b/crates/commands/src/commands/help.rs @@ -1,10 +1,18 @@ use super::*; pub fn cmds() -> impl Iterator { - [command!( - [Token::cmd("help")], - "help", - "Shows the help command" - )] + let help = ["help", "h"]; + [ + command!( + [help], + "help", + "Shows the help command" + ), + command!( + [help, "commands"], + "help_commands", + "Commands" + ), + ] .into_iter() } diff --git a/crates/commands/src/commands/member.rs b/crates/commands/src/commands/member.rs index d92e59cc..ae5c1e85 100644 --- a/crates/commands/src/commands/member.rs +++ b/crates/commands/src/commands/member.rs @@ -3,10 +3,10 @@ use super::*; pub fn cmds() -> impl Iterator { use Token::*; - let member = Token::cmd_with_alias(["member", "m"]); - let description = Token::cmd_with_alias(["description", "desc"]); - let privacy = Token::cmd_with_alias(["privacy", "priv"]); - let new = Token::cmd_with_alias(["new", "n"]); + let member = ["member", "m"]; + let description = ["description", "desc"]; + let privacy = ["privacy", "priv"]; + let new = ["new", "n"]; [ command!( diff --git a/crates/commands/src/commands/system.rs b/crates/commands/src/commands/system.rs index 9d1bdf1f..bbc449ed 100644 --- a/crates/commands/src/commands/system.rs +++ b/crates/commands/src/commands/system.rs @@ -3,8 +3,8 @@ use super::*; pub fn cmds() -> impl Iterator { use Token::*; - let system = Token::cmd_with_alias(["system", "s"]); - let new = Token::cmd_with_alias(["new", "n"]); + let system = ["system", "s"]; + let new = ["new", "n"]; [ command!( diff --git a/crates/commands/src/token.rs b/crates/commands/src/token.rs index 60df6dfc..3b59fe38 100644 --- a/crates/commands/src/token.rs +++ b/crates/commands/src/token.rs @@ -1,4 +1,4 @@ -use smol_str::SmolStr; +use smol_str::{SmolStr, ToSmolStr}; #[derive(Debug, Clone, Eq, Hash, PartialEq)] pub enum Token { @@ -41,14 +41,6 @@ lazy_static::lazy_static!( ); impl Token { - pub fn cmd(value: impl Into) -> Self { - Self::Value(vec![value.into()]) - } - - pub fn cmd_with_alias(value: impl IntoIterator>) -> Self { - Self::Value(value.into_iter().map(Into::into).collect()) - } - pub fn try_match(&self, input: Option) -> TokenMatchResult { // short circuit on empty things if matches!(self, Self::Empty) && input.is_none() { @@ -94,3 +86,25 @@ impl Token { return TokenMatchResult::NoMatch; } } + +pub trait ToToken { + fn to_token(&self) -> Token; +} + +impl ToToken for Token { + fn to_token(&self) -> Token { + self.clone() + } +} + +impl ToToken for &str { + fn to_token(&self) -> Token { + Token::Value(vec![self.to_smolstr()]) + } +} + +impl ToToken for [&str] { + fn to_token(&self) -> Token { + Token::Value(self.into_iter().map(|s| s.to_smolstr()).collect()) + } +} \ No newline at end of file From 42ea1e1bb116b2122c5a65b0193b647059afabc3 Mon Sep 17 00:00:00 2001 From: dusk Date: Sun, 5 Jan 2025 01:18:59 +0900 Subject: [PATCH 023/179] build(nix): simplify process compose config --- flake.nix | 55 +++++++++++++------------------------------------------ 1 file changed, 13 insertions(+), 42 deletions(-) diff --git a/flake.nix b/flake.nix index a310bfeb..4f421d74 100644 --- a/flake.nix +++ b/flake.nix @@ -81,6 +81,7 @@ runtimeInputs = [ (config.nci.toolchains.mkBuild pkgs) self'.devShells.services.stdenv.cc + pkgs.dotnet-sdk_8 pkgs.csharpier pkgs.coreutils uniffi-bindgen-cs @@ -159,18 +160,19 @@ settings.processes = let procCfg = composeCfg.settings.processes; - mkServiceInitProcess = + mkServiceProcess = + name: { - name, inputs ? [ ], ... - }: + }@attrs: let shell = rustOutputs.${name}.devShell; + filteredAttrs = lib.removeAttrs attrs ["inputs"]; in - { + filteredAttrs // { command = pkgs.writeShellApplication { - name = "pluralkit-${name}-init"; + name = "pluralkit-${name}"; runtimeInputs = (with pkgs; [ coreutils @@ -182,16 +184,16 @@ ${sourceDotenv} set -x ${pluralkitConfCheck} - exec cargo build --package ${name} + exec cargo run --package ${name} ''; }; }; in { ### bot ### - pluralkit-bot-init = { + pluralkit-bot = { command = pkgs.writeShellApplication { - name = "pluralkit-bot-init"; + name = "pluralkit-bot"; runtimeInputs = self'.devShells.bot.nativeBuildInputs ++ [ pkgs.coreutils pkgs.git @@ -202,24 +204,9 @@ set -x ${pluralkitConfCheck} ${self'.apps.generate-command-parser-bindings.program} - exec dotnet build -c Release -o obj/ + exec dotnet run -c Release --project PluralKit.Bot ''; }; - }; - pluralkit-bot = { - command = pkgs.writeShellApplication { - name = "pluralkit-bot"; - runtimeInputs = self'.devShells.bot.nativeBuildInputs ++ [ - pkgs.coreutils - self'.devShells.bot.stdenv.cc - ]; - text = '' - ${sourceDotenv} - set -x - exec dotnet obj/PluralKit.Bot.dll - ''; - }; - depends_on.pluralkit-bot-init.condition = "process_completed_successfully"; depends_on.postgres.condition = "process_healthy"; depends_on.redis.condition = "process_healthy"; depends_on.pluralkit-gateway.condition = "process_healthy"; @@ -227,26 +214,10 @@ ready_log_line = "Received Ready"; }; ### gateway ### - pluralkit-gateway-init = mkServiceInitProcess { - name = "gateway"; - }; - pluralkit-gateway = { - command = pkgs.writeShellApplication { - name = "pluralkit-gateway"; - runtimeInputs = with pkgs; [ - coreutils - curl - gnugrep - ]; - text = '' - ${sourceDotenv} - set -x - exec target/debug/gateway - ''; - }; + pluralkit-gateway = mkServiceProcess "gateway" { + inputs = with pkgs; [curl gnugrep]; depends_on.postgres.condition = "process_healthy"; depends_on.redis.condition = "process_healthy"; - depends_on.pluralkit-gateway-init.condition = "process_completed_successfully"; # configure health checks # TODO: don't assume port? liveness_probe.exec.command = ''curl -s -o /dev/null -w "%{http_code}" http://localhost:5000/stats | grep "302"''; From 00ba1753a2916faeba12ce36e47657bad95848f3 Mon Sep 17 00:00:00 2001 From: dusk Date: Sun, 5 Jan 2025 01:22:19 +0900 Subject: [PATCH 024/179] build(nix): build pk to correct build dir so it picks up libcommands.so --- flake.nix | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/flake.nix b/flake.nix index 4f421d74..e99d2e03 100644 --- a/flake.nix +++ b/flake.nix @@ -204,7 +204,8 @@ set -x ${pluralkitConfCheck} ${self'.apps.generate-command-parser-bindings.program} - exec dotnet run -c Release --project PluralKit.Bot + dotnet build -c Release -o obj/ + exec dotnet obj/PluralKit.Bot.dll ''; }; depends_on.postgres.condition = "process_healthy"; From 2027da40ad163921c3df83b02879710639823c4f Mon Sep 17 00:00:00 2001 From: dusk Date: Sun, 5 Jan 2025 01:52:19 +0900 Subject: [PATCH 025/179] build(nix): actually make sure libcommands.so is outputted to correct dir (obj) --- flake.nix | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/flake.nix b/flake.nix index e99d2e03..2a44085a 100644 --- a/flake.nix +++ b/flake.nix @@ -90,11 +90,12 @@ set -x commandslib="''${1:-}" if [ "$commandslib" == "" ]; then - cargo build --package commands --release - commandslib="target/release/libcommands.so" + cargo -Z unstable-options build --package commands --release --artifact-dir obj/ + commandslib="obj/libcommands.so" + else + cp -f "$commandslib" obj/ fi uniffi-bindgen-cs "$commandslib" --library --out-dir="''${2:-./PluralKit.Bot}" - cp -f "$commandslib" obj/ ''; }; }; @@ -204,7 +205,7 @@ set -x ${pluralkitConfCheck} ${self'.apps.generate-command-parser-bindings.program} - dotnet build -c Release -o obj/ + dotnet build ./PluralKit.Bot/PluralKit.Bot.csproj -c Release -o obj/ exec dotnet obj/PluralKit.Bot.dll ''; }; From 1a781014bdd0897fc2baf0ff6af450041c5480e9 Mon Sep 17 00:00:00 2001 From: dusk Date: Sun, 5 Jan 2025 02:21:23 +0900 Subject: [PATCH 026/179] fix: send correct error message if a parsed command is not implemented, etc --- PluralKit.Bot/CommandMeta/CommandTree.cs | 23 ++++++++----------- .../CommandSystem/Context/Context.cs | 8 +++++-- crates/commands/src/commands/fun.rs | 2 +- crates/commands/src/commands/help.rs | 7 +++++- crates/commands/src/commands/system.rs | 2 +- crates/commands/src/lib.rs | 2 +- crates/commands/src/token.rs | 20 ++++++---------- 7 files changed, 31 insertions(+), 33 deletions(-) diff --git a/PluralKit.Bot/CommandMeta/CommandTree.cs b/PluralKit.Bot/CommandMeta/CommandTree.cs index 2c11d02f..3ecdc070 100644 --- a/PluralKit.Bot/CommandMeta/CommandTree.cs +++ b/PluralKit.Bot/CommandMeta/CommandTree.cs @@ -10,14 +10,16 @@ public partial class CommandTree { case "fun_thunder": return ctx.Execute(null, m => m.Thunder(ctx)); - default: - // don't send an "invalid command" response if the guild has those turned off - if (ctx.GuildConfig != null && ctx.GuildConfig!.InvalidCommandResponseEnabled != true) - return Task.CompletedTask; - - // remove compiler warning + case "help": + return ctx.Execute(Help, m => m.HelpRoot(ctx)); + case "help_commands": + return ctx.Reply("For the list of commands, see the website: "); + case "help_proxy": return ctx.Reply( - $"{Emojis.Error} Unknown command {ctx.PeekArgument().AsCode()}. For a list of possible commands, see ."); + "The proxy help page has been moved! See the website: https://pluralkit.me/guide#proxying"); + default: + // remove compiler warning + return ctx.Reply($"{Emojis.Error} Parsed command {ctx.Parameters.Callback().AsCode()} not implemented in PluralKit.Bot!"); } if (ctx.Match("system", "s")) return HandleSystemCommand(ctx); @@ -50,13 +52,6 @@ public partial class CommandTree return ctx.Execute(Import, m => m.Import(ctx)); if (ctx.Match("export")) return ctx.Execute(Export, m => m.Export(ctx)); - if (ctx.Match("help", "h")) - if (ctx.Match("commands")) - return ctx.Reply("For the list of commands, see the website: "); - else if (ctx.Match("proxy")) - return ctx.Reply( - "The proxy help page has been moved! See the website: https://pluralkit.me/guide#proxying"); - else return ctx.Execute(Help, m => m.HelpRoot(ctx)); if (ctx.Match("explain")) return ctx.Execute(Explain, m => m.Explain(ctx)); if (ctx.Match("message", "msg", "messageinfo")) diff --git a/PluralKit.Bot/CommandSystem/Context/Context.cs b/PluralKit.Bot/CommandSystem/Context/Context.cs index c212e2e5..244725f8 100644 --- a/PluralKit.Bot/CommandSystem/Context/Context.cs +++ b/PluralKit.Bot/CommandSystem/Context/Context.cs @@ -57,8 +57,12 @@ public class Context } catch (PKError e) { - // todo: not this - Reply($"{Emojis.Error} {e.Message}"); + // don't send an "invalid command" response if the guild has those turned off + if (!(GuildConfig != null && GuildConfig!.InvalidCommandResponseEnabled != true)) + { + // todo: not this + Reply($"{Emojis.Error} {e.Message}"); + } throw; } } diff --git a/crates/commands/src/commands/fun.rs b/crates/commands/src/commands/fun.rs index a3ba0d9e..472997ac 100644 --- a/crates/commands/src/commands/fun.rs +++ b/crates/commands/src/commands/fun.rs @@ -4,7 +4,7 @@ pub fn cmds() -> impl Iterator { [command!( ["thunder"], "fun_thunder", - "Shows the help command" + "fun thunder" )] .into_iter() } diff --git a/crates/commands/src/commands/help.rs b/crates/commands/src/commands/help.rs index 269d87a9..86ec0f97 100644 --- a/crates/commands/src/commands/help.rs +++ b/crates/commands/src/commands/help.rs @@ -11,7 +11,12 @@ pub fn cmds() -> impl Iterator { command!( [help, "commands"], "help_commands", - "Commands" + "help commands" + ), + command!( + [help, "proxy"], + "help_proxy", + "help proxy" ), ] .into_iter() diff --git a/crates/commands/src/commands/system.rs b/crates/commands/src/commands/system.rs index bbc449ed..3e10e992 100644 --- a/crates/commands/src/commands/system.rs +++ b/crates/commands/src/commands/system.rs @@ -14,7 +14,7 @@ pub fn cmds() -> impl Iterator { ), command!([system, new], "system_new", "Creates a new system"), command!( - [system, new, FullString], + [system, new, SystemRef], "system_new", "Creates a new system" ), diff --git a/crates/commands/src/lib.rs b/crates/commands/src/lib.rs index f2b4a969..4b239cd1 100644 --- a/crates/commands/src/lib.rs +++ b/crates/commands/src/lib.rs @@ -93,7 +93,7 @@ fn parse_command(input: String) -> CommandResult { // todo: check if last token is a common incorrect unquote (multi-member names etc) // todo: check if this is a system name in pk;s command return CommandResult::Err { - error: "Command not found.".to_string(), + error: format!("Unknown command `{input}`. For a list of possible commands, see ."), }; } Err(Some(short_circuit)) => { diff --git a/crates/commands/src/token.rs b/crates/commands/src/token.rs index 3b59fe38..3d933668 100644 --- a/crates/commands/src/token.rs +++ b/crates/commands/src/token.rs @@ -18,6 +18,9 @@ pub enum Token { MemberRef, MemberPrivacyTarget, + /// System reference + SystemRef, + PrivacyLevel, // currently not included in command definitions @@ -32,13 +35,7 @@ pub enum TokenMatchResult { } // move this somewhere else -lazy_static::lazy_static!( - static ref MEMBER_PRIVACY_TARGETS: Vec = [ - "visibility", - "name", - "todo", - ].into_iter().map(SmolStr::new_static).collect(); -); +const MEMBER_PRIVACY_TARGETS: &[&str] = &["visibility", "name", "todo"]; impl Token { pub fn try_match(&self, input: Option) -> TokenMatchResult { @@ -66,12 +63,9 @@ impl Token { } Self::MultiValue(_) => todo!(), Self::FullString => return TokenMatchResult::Match(Some(input)), + Self::SystemRef => return TokenMatchResult::Match(Some(input)), Self::MemberRef => return TokenMatchResult::Match(Some(input)), - Self::MemberPrivacyTarget - if MEMBER_PRIVACY_TARGETS - .iter() - .any(|target| target.eq(input.trim())) => - { + Self::MemberPrivacyTarget if MEMBER_PRIVACY_TARGETS.contains(&input.trim()) => { return TokenMatchResult::Match(Some(input)) } Self::MemberPrivacyTarget => {} @@ -107,4 +101,4 @@ impl ToToken for [&str] { fn to_token(&self) -> Token { Token::Value(self.into_iter().map(|s| s.to_smolstr()).collect()) } -} \ No newline at end of file +} From eec9f64026c0019d0af9a466a87b922e93bbfbd9 Mon Sep 17 00:00:00 2001 From: dusk Date: Sun, 5 Jan 2025 13:00:06 +0900 Subject: [PATCH 027/179] feat: implement proper ("static") parameters handling command parser -> bot feat: handle few more commands bot side fix(commands): handle missing parameters and return error refactor(commands): use ordermap instead of relying on a sort function to sort tokens --- Cargo.lock | 22 +- PluralKit.Bot/CommandMeta/CommandTree.cs | 218 +++++++++--------- .../CommandSystem/Context/Context.cs | 16 -- .../Context/ContextArgumentsExt.cs | 82 +------ .../Context/ContextEntityArgumentsExt.cs | 25 +- PluralKit.Bot/CommandSystem/Parameters.cs | 185 --------------- PluralKit.Bot/CommandSystem/ParametersFFI.cs | 72 ++++-- PluralKit.Bot/Commands/Member.cs | 4 +- PluralKit.Bot/Handlers/MessageCreated.cs | 18 +- crates/commands/Cargo.toml | 1 + crates/commands/src/commands.udl | 13 ++ crates/commands/src/commands/member.rs | 16 +- crates/commands/src/commands/system.rs | 2 +- crates/commands/src/lib.rs | 99 ++++++-- crates/commands/src/token.rs | 55 +++-- crates/commands/src/tree.rs | 32 +-- 16 files changed, 358 insertions(+), 502 deletions(-) delete mode 100644 PluralKit.Bot/CommandSystem/Parameters.cs diff --git a/Cargo.lock b/Cargo.lock index 62650368..4e9094bf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -569,6 +569,7 @@ name = "commands" version = "0.1.0" dependencies = [ "lazy_static", + "ordermap", "smol_str", "uniffi", ] @@ -1341,6 +1342,12 @@ dependencies = [ "allocator-api2", ] +[[package]] +name = "hashbrown" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" + [[package]] name = "hashlink" version = "0.9.1" @@ -1719,12 +1726,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.2.6" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" +checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" dependencies = [ "equivalent", - "hashbrown 0.14.5", + "hashbrown 0.15.2", ] [[package]] @@ -2235,6 +2242,15 @@ dependencies = [ "hashbrown 0.13.2", ] +[[package]] +name = "ordermap" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f80a48eb68b6a7da9829b8b0429011708f775af80676a91063d023a66a656106" +dependencies = [ + "indexmap", +] + [[package]] name = "os_info" version = "3.8.2" diff --git a/PluralKit.Bot/CommandMeta/CommandTree.cs b/PluralKit.Bot/CommandMeta/CommandTree.cs index 3ecdc070..554e623c 100644 --- a/PluralKit.Bot/CommandMeta/CommandTree.cs +++ b/PluralKit.Bot/CommandMeta/CommandTree.cs @@ -4,22 +4,27 @@ namespace PluralKit.Bot; public partial class CommandTree { - public Task ExecuteCommand(Context ctx) + public Task ExecuteCommand(Context ctx, ResolvedParameters parameters) { - switch (ctx.Parameters.Callback()) + switch (parameters.Raw.Callback()) { case "fun_thunder": return ctx.Execute(null, m => m.Thunder(ctx)); case "help": return ctx.Execute(Help, m => m.HelpRoot(ctx)); case "help_commands": - return ctx.Reply("For the list of commands, see the website: "); + return ctx.Reply( + "For the list of commands, see the website: "); case "help_proxy": return ctx.Reply( "The proxy help page has been moved! See the website: https://pluralkit.me/guide#proxying"); + case "member_show": + return ctx.Execute(MemberInfo, m => m.ViewMember(ctx, parameters.MemberParams["target"])); + case "member_new": + return ctx.Execute(MemberNew, m => m.NewMember(ctx, parameters.Raw.Params()["name"])); default: - // remove compiler warning - return ctx.Reply($"{Emojis.Error} Parsed command {ctx.Parameters.Callback().AsCode()} not implemented in PluralKit.Bot!"); + return ctx.Reply( + $"{Emojis.Error} Parsed command {parameters.Raw.Callback().AsCode()} not implemented in PluralKit.Bot!"); } if (ctx.Match("system", "s")) return HandleSystemCommand(ctx); @@ -224,43 +229,44 @@ public partial class CommandTree // finally, parse commands that *can* take a system target else { - // try matching a system ID - var target = await ctx.MatchSystem(); - var previousPtr = ctx.Parameters._ptr; + // TODO: actually implement this + // // try matching a system ID + // var target = await ctx.MatchSystem(); + // var previousPtr = ctx.Parameters._ptr; - // if we have a parsed target and no more commands, don't bother with the command flow - // we skip the `target != null` check here since the argument isn't be popped if it's not a system - if (!ctx.HasNext()) - { - await ctx.Execute(SystemInfo, m => m.Query(ctx, target ?? ctx.System)); - return; - } + // // if we have a parsed target and no more commands, don't bother with the command flow + // // we skip the `target != null` check here since the argument isn't be popped if it's not a system + // if (!ctx.HasNext()) + // { + // await ctx.Execute(SystemInfo, m => m.Query(ctx, target ?? ctx.System)); + // return; + // } - // hacky, but we need to CheckSystem(target) which throws a PKError - // normally PKErrors are only handled in ctx.Execute - try - { - await HandleSystemCommandTargeted(ctx, target ?? ctx.System); - } - catch (PKError e) - { - await ctx.Reply($"{Emojis.Error} {e.Message}"); - return; - } + // // hacky, but we need to CheckSystem(target) which throws a PKError + // // normally PKErrors are only handled in ctx.Execute + // try + // { + // await HandleSystemCommandTargeted(ctx, target ?? ctx.System); + // } + // catch (PKError e) + // { + // await ctx.Reply($"{Emojis.Error} {e.Message}"); + // return; + // } - // if we *still* haven't matched anything, the user entered an invalid command name or system reference - if (ctx.Parameters._ptr == previousPtr) - { - if (!ctx.Parameters.Peek().TryParseHid(out _) && !ctx.Parameters.Peek().TryParseMention(out _)) - { - await PrintCommandNotFoundError(ctx, SystemCommands); - return; - } + // // if we *still* haven't matched anything, the user entered an invalid command name or system reference + // if (ctx.Parameters._ptr == previousPtr) + // { + // if (!ctx.Parameters.Peek().TryParseHid(out _) && !ctx.Parameters.Peek().TryParseMention(out _)) + // { + // await PrintCommandNotFoundError(ctx, SystemCommands); + // return; + // } - var list = CreatePotentialCommandList(ctx.DefaultPrefix, SystemCommands); - await ctx.Reply($"{Emojis.Error} {await CreateSystemNotFoundError(ctx)}\n\n" - + $"Perhaps you meant to use one of the following commands?\n{list}"); - } + // var list = CreatePotentialCommandList(ctx.DefaultPrefix, SystemCommands); + // await ctx.Reply($"{Emojis.Error} {await CreateSystemNotFoundError(ctx)}\n\n" + // + $"Perhaps you meant to use one of the following commands?\n{list}"); + // } } } @@ -324,20 +330,21 @@ public partial class CommandTree private async Task HandleMemberCommand(Context ctx) { - if (ctx.Match("new", "n", "add", "create", "register")) - await ctx.Execute(MemberNew, m => m.NewMember(ctx)); - else if (ctx.Match("list")) - await ctx.Execute(SystemList, m => m.MemberList(ctx, ctx.System)); - else if (ctx.Match("commands", "help")) - await PrintCommandList(ctx, "members", MemberCommands); - else if (await ctx.MatchMember() is PKMember target) - await HandleMemberCommandTargeted(ctx, target); - else if (!ctx.HasNext()) - await PrintCommandExpectedError(ctx, MemberNew, MemberInfo, MemberRename, MemberDisplayName, - MemberServerName, MemberDesc, MemberPronouns, - MemberColor, MemberBirthday, MemberProxy, MemberDelete, MemberAvatar); - else - await ctx.Reply($"{Emojis.Error} {ctx.CreateNotFoundError("Member", ctx.PopArgument())}"); + // TODO: implement + // if (ctx.Match("new", "n", "add", "create", "register")) + // await ctx.Execute(MemberNew, m => m.NewMember(ctx)); + // else if (ctx.Match("list")) + // await ctx.Execute(SystemList, m => m.MemberList(ctx, ctx.System)); + // else if (ctx.Match("commands", "help")) + // await PrintCommandList(ctx, "members", MemberCommands); + // else if (await ctx.MatchMember() is PKMember target) + // await HandleMemberCommandTargeted(ctx, target); + // else if (!ctx.HasNext()) + // await PrintCommandExpectedError(ctx, MemberNew, MemberInfo, MemberRename, MemberDisplayName, + // MemberServerName, MemberDesc, MemberPronouns, + // MemberColor, MemberBirthday, MemberProxy, MemberDelete, MemberAvatar); + // else + // await ctx.Reply($"{Emojis.Error} {ctx.CreateNotFoundError("Member", ctx.PopArgument())}"); } private async Task HandleMemberCommandTargeted(Context ctx, PKMember target) @@ -408,59 +415,60 @@ public partial class CommandTree private async Task HandleGroupCommand(Context ctx) { - // Commands with no group argument - if (ctx.Match("n", "new")) - await ctx.Execute(GroupNew, g => g.CreateGroup(ctx)); - else if (ctx.Match("list", "l")) - await ctx.Execute(GroupList, g => g.ListSystemGroups(ctx, null)); - else if (ctx.Match("commands", "help")) - await PrintCommandList(ctx, "groups", GroupCommands); - else if (await ctx.MatchGroup() is { } target) - { - // Commands with group argument - if (ctx.Match("rename", "name", "changename", "setname", "rn")) - await ctx.Execute(GroupRename, g => g.RenameGroup(ctx, target)); - else if (ctx.Match("nick", "dn", "displayname", "nickname")) - await ctx.Execute(GroupDisplayName, g => g.GroupDisplayName(ctx, target)); - else if (ctx.Match("description", "desc", "describe", "d", "bio", "info", "text", "intro")) - await ctx.Execute(GroupDesc, g => g.GroupDescription(ctx, target)); - else if (ctx.Match("add", "a")) - await ctx.Execute(GroupAdd, - g => g.AddRemoveMembers(ctx, target, Groups.AddRemoveOperation.Add)); - else if (ctx.Match("remove", "rem")) - await ctx.Execute(GroupRemove, - g => g.AddRemoveMembers(ctx, target, Groups.AddRemoveOperation.Remove)); - else if (ctx.Match("members", "list", "ms", "l", "ls")) - await ctx.Execute(GroupMemberList, g => g.ListGroupMembers(ctx, target)); - else if (ctx.Match("random", "rand", "r")) - await ctx.Execute(GroupMemberRandom, r => r.GroupMember(ctx, target)); - else if (ctx.Match("privacy")) - await ctx.Execute(GroupPrivacy, g => g.GroupPrivacy(ctx, target, null)); - else if (ctx.Match("public", "pub")) - await ctx.Execute(GroupPrivacy, g => g.GroupPrivacy(ctx, target, PrivacyLevel.Public)); - else if (ctx.Match("private", "priv")) - await ctx.Execute(GroupPrivacy, g => g.GroupPrivacy(ctx, target, PrivacyLevel.Private)); - else if (ctx.Match("delete", "destroy", "erase", "yeet")) - await ctx.Execute(GroupDelete, g => g.DeleteGroup(ctx, target)); - else if (ctx.Match("avatar", "picture", "icon", "image", "pic", "pfp")) - await ctx.Execute(GroupIcon, g => g.GroupIcon(ctx, target)); - else if (ctx.Match("banner", "splash", "cover")) - await ctx.Execute(GroupBannerImage, g => g.GroupBannerImage(ctx, target)); - else if (ctx.Match("fp", "frontpercent", "front%", "frontbreakdown")) - await ctx.Execute(GroupFrontPercent, g => g.FrontPercent(ctx, group: target)); - else if (ctx.Match("color", "colour")) - await ctx.Execute(GroupColor, g => g.GroupColor(ctx, target)); - else if (ctx.Match("id")) - await ctx.Execute(GroupId, g => g.DisplayId(ctx, target)); - else if (!ctx.HasNext()) - await ctx.Execute(GroupInfo, g => g.ShowGroupCard(ctx, target)); - else - await PrintCommandNotFoundError(ctx, GroupCommandsTargeted); - } - else if (!ctx.HasNext()) - await PrintCommandExpectedError(ctx, GroupCommands); - else - await ctx.Reply($"{Emojis.Error} {ctx.CreateNotFoundError("Group", ctx.PopArgument())}"); + // TODO: implement + // // Commands with no group argument + // if (ctx.Match("n", "new")) + // await ctx.Execute(GroupNew, g => g.CreateGroup(ctx)); + // else if (ctx.Match("list", "l")) + // await ctx.Execute(GroupList, g => g.ListSystemGroups(ctx, null)); + // else if (ctx.Match("commands", "help")) + // await PrintCommandList(ctx, "groups", GroupCommands); + // else if (await ctx.MatchGroup() is { } target) + // { + // // Commands with group argument + // if (ctx.Match("rename", "name", "changename", "setname", "rn")) + // await ctx.Execute(GroupRename, g => g.RenameGroup(ctx, target)); + // else if (ctx.Match("nick", "dn", "displayname", "nickname")) + // await ctx.Execute(GroupDisplayName, g => g.GroupDisplayName(ctx, target)); + // else if (ctx.Match("description", "desc", "describe", "d", "bio", "info", "text", "intro")) + // await ctx.Execute(GroupDesc, g => g.GroupDescription(ctx, target)); + // else if (ctx.Match("add", "a")) + // await ctx.Execute(GroupAdd, + // g => g.AddRemoveMembers(ctx, target, Groups.AddRemoveOperation.Add)); + // else if (ctx.Match("remove", "rem")) + // await ctx.Execute(GroupRemove, + // g => g.AddRemoveMembers(ctx, target, Groups.AddRemoveOperation.Remove)); + // else if (ctx.Match("members", "list", "ms", "l", "ls")) + // await ctx.Execute(GroupMemberList, g => g.ListGroupMembers(ctx, target)); + // else if (ctx.Match("random", "rand", "r")) + // await ctx.Execute(GroupMemberRandom, r => r.GroupMember(ctx, target)); + // else if (ctx.Match("privacy")) + // await ctx.Execute(GroupPrivacy, g => g.GroupPrivacy(ctx, target, null)); + // else if (ctx.Match("public", "pub")) + // await ctx.Execute(GroupPrivacy, g => g.GroupPrivacy(ctx, target, PrivacyLevel.Public)); + // else if (ctx.Match("private", "priv")) + // await ctx.Execute(GroupPrivacy, g => g.GroupPrivacy(ctx, target, PrivacyLevel.Private)); + // else if (ctx.Match("delete", "destroy", "erase", "yeet")) + // await ctx.Execute(GroupDelete, g => g.DeleteGroup(ctx, target)); + // else if (ctx.Match("avatar", "picture", "icon", "image", "pic", "pfp")) + // await ctx.Execute(GroupIcon, g => g.GroupIcon(ctx, target)); + // else if (ctx.Match("banner", "splash", "cover")) + // await ctx.Execute(GroupBannerImage, g => g.GroupBannerImage(ctx, target)); + // else if (ctx.Match("fp", "frontpercent", "front%", "frontbreakdown")) + // await ctx.Execute(GroupFrontPercent, g => g.FrontPercent(ctx, group: target)); + // else if (ctx.Match("color", "colour")) + // await ctx.Execute(GroupColor, g => g.GroupColor(ctx, target)); + // else if (ctx.Match("id")) + // await ctx.Execute(GroupId, g => g.DisplayId(ctx, target)); + // else if (!ctx.HasNext()) + // await ctx.Execute(GroupInfo, g => g.ShowGroupCard(ctx, target)); + // else + // await PrintCommandNotFoundError(ctx, GroupCommandsTargeted); + // } + // else if (!ctx.HasNext()) + // await PrintCommandExpectedError(ctx, GroupCommands); + // else + // await ctx.Reply($"{Emojis.Error} {ctx.CreateNotFoundError("Group", ctx.PopArgument())}"); } private async Task HandleSwitchCommand(Context ctx) diff --git a/PluralKit.Bot/CommandSystem/Context/Context.cs b/PluralKit.Bot/CommandSystem/Context/Context.cs index 244725f8..344e85d5 100644 --- a/PluralKit.Bot/CommandSystem/Context/Context.cs +++ b/PluralKit.Bot/CommandSystem/Context/Context.cs @@ -50,21 +50,6 @@ public class Context DefaultPrefix = prefixes[0]; Rest = provider.Resolve(); Cluster = provider.Resolve(); - - try - { - Parameters = new ParametersFFI(message.Content?.Substring(commandParseOffset)); - } - catch (PKError e) - { - // don't send an "invalid command" response if the guild has those turned off - if (!(GuildConfig != null && GuildConfig!.InvalidCommandResponseEnabled != true)) - { - // todo: not this - Reply($"{Emojis.Error} {e.Message}"); - } - throw; - } } public readonly IDiscordCache Cache; @@ -90,7 +75,6 @@ public class Context public readonly string CommandPrefix; public readonly string DefaultPrefix; - public readonly ParametersFFI Parameters; internal readonly IDatabase Database; internal readonly ModelRepository Repository; diff --git a/PluralKit.Bot/CommandSystem/Context/ContextArgumentsExt.cs b/PluralKit.Bot/CommandSystem/Context/ContextArgumentsExt.cs index 982eec77..293cc118 100644 --- a/PluralKit.Bot/CommandSystem/Context/ContextArgumentsExt.cs +++ b/PluralKit.Bot/CommandSystem/Context/ContextArgumentsExt.cs @@ -8,20 +8,15 @@ namespace PluralKit.Bot; public static class ContextArgumentsExt { - public static string PopArgument(this Context ctx) => - ctx.Parameters.Pop(); + public static string PopArgument(this Context ctx) => throw new PKError("todo: PopArgument"); - public static string PeekArgument(this Context ctx) => - ctx.Parameters.Peek(); + public static string PeekArgument(this Context ctx) => throw new PKError("todo: PeekArgument"); - public static string RemainderOrNull(this Context ctx, bool skipFlags = true) => - ctx.Parameters.Remainder(skipFlags).Length == 0 ? null : ctx.Parameters.Remainder(skipFlags); + public static string RemainderOrNull(this Context ctx, bool skipFlags = true) => throw new PKError("todo: RemainderOrNull"); - public static bool HasNext(this Context ctx, bool skipFlags = true) => - ctx.RemainderOrNull(skipFlags) != null; + public static bool HasNext(this Context ctx, bool skipFlags = true) => throw new PKError("todo: HasNext"); - public static string FullCommand(this Context ctx) => - ctx.Parameters.FullCommand; + public static string FullCommand(this Context ctx) => throw new PKError("todo: FullCommand"); /// /// Checks if the next parameter is equal to one of the given keywords and pops it from the stack. Case-insensitive. @@ -53,12 +48,7 @@ public static class ContextArgumentsExt /// public static bool PeekMatch(this Context ctx, ref int ptr, string[] potentialMatches) { - var arg = ctx.Parameters.PeekWithPtr(ref ptr); - foreach (var match in potentialMatches) - if (arg.Equals(match, StringComparison.InvariantCultureIgnoreCase)) - return true; - - return false; + throw new PKError("todo: PeekMatch"); } /// @@ -69,23 +59,14 @@ public static class ContextArgumentsExt /// public static bool MatchMultiple(this Context ctx, params string[][] potentialParametersMatches) { - int ptr = ctx.Parameters._ptr; - - foreach (var param in potentialParametersMatches) - if (!ctx.PeekMatch(ref ptr, param)) return false; - - ctx.Parameters._ptr = ptr; - - return true; + throw new PKError("todo: MatchMultiple"); } public static bool MatchFlag(this Context ctx, params string[] potentialMatches) { // Flags are *ALWAYS PARSED LOWERCASE*. This means we skip out on a "ToLower" call here. // Can assume the caller array only contains lowercase *and* the set below only contains lowercase - - var flags = ctx.Parameters.Flags(); - return potentialMatches.Any(potentialMatch => flags.Contains(potentialMatch)); + throw new NotImplementedException(); } public static bool MatchClear(this Context ctx) @@ -100,11 +81,7 @@ public static class ContextArgumentsExt public static ReplyFormat PeekMatchFormat(this Context ctx) { - int ptr1 = ctx.Parameters._ptr; - int ptr2 = ctx.Parameters._ptr; - if (ctx.PeekMatch(ref ptr1, new[] { "r", "raw" }) || ctx.MatchFlag("r", "raw")) return ReplyFormat.Raw; - if (ctx.PeekMatch(ref ptr2, new[] { "pt", "plaintext" }) || ctx.MatchFlag("pt", "plaintext")) return ReplyFormat.Plaintext; - return ReplyFormat.Standard; + throw new PKError("todo: PeekMatchFormat"); } public static bool MatchToggle(this Context ctx, bool? defaultValue = null) @@ -153,49 +130,12 @@ public static class ContextArgumentsExt public static async Task> ParseMemberList(this Context ctx, SystemId? restrictToSystem) { - var members = new List(); - - // Loop through all the given arguments - while (ctx.HasNext()) - { - // and attempt to match a member - var member = await ctx.MatchMember(restrictToSystem); - - if (member == null) - // if we can't, big error. Every member name must be valid. - throw new PKError(ctx.CreateNotFoundError("Member", ctx.PopArgument())); - - members.Add(member); // Then add to the final output list - } - - if (members.Count == 0) throw new PKSyntaxError("You must input at least one member."); - - return members; + throw new NotImplementedException(); } public static async Task> ParseGroupList(this Context ctx, SystemId? restrictToSystem) { - var groups = new List(); - - // Loop through all the given arguments - while (ctx.HasNext()) - { - // and attempt to match a group - var group = await ctx.MatchGroup(restrictToSystem); - if (group == null) - // if we can't, big error. Every group name must be valid. - throw new PKError(ctx.CreateNotFoundError("Group", ctx.PopArgument())); - - // todo: remove this, the database query enforces the restriction - if (restrictToSystem != null && group.System != restrictToSystem) - throw Errors.NotOwnGroupError; // TODO: name *which* group? - - groups.Add(group); // Then add to the final output list - } - - if (groups.Count == 0) throw new PKSyntaxError("You must input at least one group."); - - return groups; + throw new NotImplementedException(); } } diff --git a/PluralKit.Bot/CommandSystem/Context/ContextEntityArgumentsExt.cs b/PluralKit.Bot/CommandSystem/Context/ContextEntityArgumentsExt.cs index 533e374f..13d31cdb 100644 --- a/PluralKit.Bot/CommandSystem/Context/ContextEntityArgumentsExt.cs +++ b/PluralKit.Bot/CommandSystem/Context/ContextEntityArgumentsExt.cs @@ -34,19 +34,15 @@ public static class ContextEntityArgumentsExt return id != 0; } - public static Task PeekSystem(this Context ctx) => ctx.MatchSystemInner(); + public static Task PeekSystem(this Context ctx) => throw new NotImplementedException(); public static async Task MatchSystem(this Context ctx) { - var system = await ctx.MatchSystemInner(); - if (system != null) ctx.PopArgument(); - return system; + throw new NotImplementedException(); } - private static async Task MatchSystemInner(this Context ctx) + public static async Task ParseSystem(this Context ctx, string input) { - var input = ctx.PeekArgument(); - // System references can take three forms: // - The direct user ID of an account connected to the system // - A @mention of an account connected to the system (<@uid>) @@ -63,10 +59,8 @@ public static class ContextEntityArgumentsExt return null; } - public static async Task PeekMember(this Context ctx, SystemId? restrictToSystem = null) + public static async Task ParseMember(this Context ctx, Parameters parameters, string input, SystemId? restrictToSystem = null) { - var input = ctx.PeekArgument(); - // Member references can have one of three forms, depending on // whether you're in a system or not: // - A member hid @@ -75,7 +69,7 @@ public static class ContextEntityArgumentsExt // Skip name / display name matching if the user does not have a system // or if they specifically request by-HID matching - if (ctx.System != null && !ctx.MatchFlag("id", "by-id")) + if (ctx.System != null && !parameters.HasFlag("id", "by-id")) { // First, try finding by member name in system if (await ctx.Repository.GetMemberByName(ctx.System.Id, input) is PKMember memberByName) @@ -124,6 +118,11 @@ public static class ContextEntityArgumentsExt return null; } + public static async Task PeekMember(this Context ctx, SystemId? restrictToSystem = null) + { + throw new NotImplementedException(); + } + /// /// Attempts to pop a member descriptor from the stack, returning it if present. If a member could not be /// resolved by the next word in the argument stack, does *not* touch the stack, and returns null. @@ -170,9 +169,9 @@ public static class ContextEntityArgumentsExt return group; } - public static string CreateNotFoundError(this Context ctx, string entity, string input) + public static string CreateNotFoundError(this Context ctx, Parameters parameters, string entity, string input) { - var isIDOnlyQuery = ctx.System == null || ctx.MatchFlag("id", "by-id"); + var isIDOnlyQuery = ctx.System == null || parameters.HasFlag("id", "by-id"); var inputIsHid = HidUtils.ParseHid(input) != null; if (isIDOnlyQuery) diff --git a/PluralKit.Bot/CommandSystem/Parameters.cs b/PluralKit.Bot/CommandSystem/Parameters.cs deleted file mode 100644 index 05e1bdb5..00000000 --- a/PluralKit.Bot/CommandSystem/Parameters.cs +++ /dev/null @@ -1,185 +0,0 @@ -namespace PluralKit.Bot; - -public class Parameters -{ - // Dictionary of (left, right) quote pairs - // Each char in the string is an individual quote, multi-char strings imply "one of the following chars" - private static readonly Dictionary _quotePairs = new() - { - // Basic - { "'", "'" }, // ASCII single quotes - { "\"", "\"" }, // ASCII double quotes - - // "Smart quotes" - // Specifically ignore the left/right status of the quotes and match any combination of them - // Left string also includes "low" quotes to allow for the low-high style used in some locales - { "\u201C\u201D\u201F\u201E", "\u201C\u201D\u201F" }, // double quotes - { "\u2018\u2019\u201B\u201A", "\u2018\u2019\u201B" }, // single quotes - - // Chevrons (normal and "fullwidth" variants) - { "\u00AB\u300A", "\u00BB\u300B" }, // double chevrons, pointing away (<>) - { "\u00BB\u300B", "\u00AB\u300A" }, // double chevrons, pointing together (>>text<<) - { "\u2039\u3008", "\u203A\u3009" }, // single chevrons, pointing away () - { "\u203A\u3009", "\u2039\u3008" }, // single chevrons, pointing together (>text<) - - // Other - { "\u300C\u300E", "\u300D\u300F" } // corner brackets (Japanese/Chinese) - }; - - private ISet _flags; // Only parsed when requested first time - public int _ptr; - - public string FullCommand { get; } - - private struct WordPosition - { - // Start of the word - internal readonly int startPos; - - // End of the word - internal readonly int endPos; - - // How much to advance word pointer afterwards to point at the start of the *next* word - internal readonly int advanceAfterWord; - - internal readonly bool wasQuoted; - - public WordPosition(int startPos, int endPos, int advanceAfterWord, bool wasQuoted) - { - this.startPos = startPos; - this.endPos = endPos; - this.advanceAfterWord = advanceAfterWord; - this.wasQuoted = wasQuoted; - } - } - - public Parameters(string cmd) - { - // This is a SUPER dirty hack to avoid having to match both spaces and newlines in the word detection below - // Instead, we just add a space before every newline (which then gets stripped out later). - FullCommand = cmd.Replace("\n", " \n"); - _ptr = 0; - } - - private void ParseFlags() - { - _flags = new HashSet(); - - var ptr = 0; - while (NextWordPosition(ptr) is { } wp) - { - ptr = wp.endPos + wp.advanceAfterWord; - - // Is this word a *flag* (as in, starts with a - AND is not quoted) - if (FullCommand[wp.startPos] != '-' || wp.wasQuoted) continue; // (if not, carry on w/ next word) - - // Find the *end* of the flag start (technically allowing arbitrary amounts of dashes) - var flagNameStart = wp.startPos; - while (flagNameStart < FullCommand.Length && FullCommand[flagNameStart] == '-') - flagNameStart++; - - // Then add the word to the flag set - var word = FullCommand.Substring(flagNameStart, wp.endPos - flagNameStart).Trim(); - if (word.Length > 0) - _flags.Add(word.ToLowerInvariant()); - } - } - - public string Pop() - { - // Loop to ignore and skip past flags - while (NextWordPosition(_ptr) is { } pos) - { - _ptr = pos.endPos + pos.advanceAfterWord; - if (FullCommand[pos.startPos] == '-' && !pos.wasQuoted) continue; - return FullCommand.Substring(pos.startPos, pos.endPos - pos.startPos).Trim(); - } - - return ""; - } - - public string Peek() - { - // temp ptr so we don't move the real ptr - int ptr = _ptr; - - return PeekWithPtr(ref ptr); - } - - public string PeekWithPtr(ref int ptr) - { - // Loop to ignore and skip past flags - while (NextWordPosition(ptr) is { } pos) - { - ptr = pos.endPos + pos.advanceAfterWord; - if (FullCommand[pos.startPos] == '-' && !pos.wasQuoted) continue; - return FullCommand.Substring(pos.startPos, pos.endPos - pos.startPos).Trim(); - } - - return ""; - } - - public ISet Flags() - { - if (_flags == null) ParseFlags(); - return _flags; - } - - public string Remainder(bool skipFlags = true) - { - if (skipFlags) - // Skip all *leading* flags when taking the remainder - while (NextWordPosition(_ptr) is { } wp) - { - if (FullCommand[wp.startPos] != '-' || wp.wasQuoted) break; - _ptr = wp.endPos + wp.advanceAfterWord; - } - - // *Then* get the remainder - return FullCommand.Substring(Math.Min(_ptr, FullCommand.Length)).Trim(); - } - - private WordPosition? NextWordPosition(int position) - { - // Skip leading spaces before actual content - while (position < FullCommand.Length && FullCommand[position] == ' ') position++; - - // Is this the end of the string? - if (FullCommand.Length <= position) return null; - - // Is this a quoted word? - if (TryCheckQuote(FullCommand[position], out var endQuotes)) - { - // We found a quoted word - find an instance of one of the corresponding end quotes - var endQuotePosition = -1; - for (var i = position + 1; i < FullCommand.Length; i++) - if (endQuotePosition == -1 && endQuotes.Contains(FullCommand[i])) - endQuotePosition = i; // need a break; don't feel like brackets tho lol - - // Position after the end quote should be EOL or a space - // Otherwise we fallthrough to the unquoted word handler below - if (FullCommand.Length == endQuotePosition + 1 || FullCommand[endQuotePosition + 1] == ' ') - return new WordPosition(position + 1, endQuotePosition, 2, true); - } - - // Not a quoted word, just find the next space and return if it's the end of the command - var wordEnd = FullCommand.IndexOf(' ', position + 1); - - return wordEnd == -1 - ? new WordPosition(position, FullCommand.Length, 0, false) - : new WordPosition(position, wordEnd, 1, false); - } - - private bool TryCheckQuote(char potentialLeftQuote, out string correspondingRightQuotes) - { - foreach (var (left, right) in _quotePairs) - if (left.Contains(potentialLeftQuote)) - { - correspondingRightQuotes = right; - return true; - } - - correspondingRightQuotes = null; - return false; - } -} \ No newline at end of file diff --git a/PluralKit.Bot/CommandSystem/ParametersFFI.cs b/PluralKit.Bot/CommandSystem/ParametersFFI.cs index 474b5833..ac97af75 100644 --- a/PluralKit.Bot/CommandSystem/ParametersFFI.cs +++ b/PluralKit.Bot/CommandSystem/ParametersFFI.cs @@ -1,18 +1,19 @@ +using PluralKit.Core; using uniffi.commands; namespace PluralKit.Bot; -public class ParametersFFI +public class Parameters { private string _cb { get; init; } private List _args { get; init; } - public int _ptr = -1; private Dictionary _flags { get; init; } + private Dictionary _params { get; init; } // just used for errors, temporarily public string FullCommand { get; init; } - public ParametersFFI(string cmd) + public Parameters(string cmd) { FullCommand = cmd; var result = CommandsMethods.ParseCommand(cmd); @@ -22,6 +23,7 @@ public class ParametersFFI _cb = command.@commandRef; _args = command.@args; _flags = command.@flags; + _params = command.@params; } else { @@ -29,43 +31,67 @@ public class ParametersFFI } } + public async Task ResolveParameters(Context ctx) + { + var parsed_members = await MemberParams().ToAsyncEnumerable().ToDictionaryAwaitAsync(async item => item.Key, async item => + await ctx.ParseMember(this, item.Value) ?? throw new PKError(ctx.CreateNotFoundError(this, "Member", item.Value)) + ); + var parsed_systems = await SystemParams().ToAsyncEnumerable().ToDictionaryAwaitAsync(async item => item.Key, async item => + await ctx.ParseSystem(item.Value) ?? throw new PKError(ctx.CreateNotFoundError(this, "System", item.Value)) + ); + return new ResolvedParameters(this, parsed_members, parsed_systems); + } + public string Callback() { return _cb; } - public string Pop() + public IDictionary Flags() { - if (_args.Count > _ptr + 1) Console.WriteLine($"pop: {_ptr + 1}, {_args[_ptr + 1]}"); - else Console.WriteLine("pop: no more arguments"); - if (_args.Count() == _ptr + 1) return ""; - _ptr++; - return _args[_ptr]; + return _flags; } - public string Peek() + private Dictionary Params(Func filter) { - if (_args.Count > _ptr + 1) Console.WriteLine($"peek: {_ptr + 1}, {_args[_ptr + 1]}"); - else Console.WriteLine("peek: no more arguments"); - if (_args.Count() == _ptr + 1) return ""; - return _args[_ptr + 1]; + return _params.Where(item => filter(item.Value.@kind)).ToDictionary(item => item.Key, item => item.Value.@raw); } - // this might not work quite right - public string PeekWithPtr(ref int ptr) + public IDictionary Params() { - return _args[ptr]; + return Params(_ => true); } - public ISet Flags() + public IDictionary MemberParams() { - return new HashSet(_flags.Keys); + return Params(kind => kind == ParameterKind.MemberRef); } - // parsed differently in new commands, does this work right? - // note: skipFlags here does nothing - public string Remainder(bool skipFlags = false) + public IDictionary SystemParams() { - return Pop(); + return Params(kind => kind == ParameterKind.SystemRef); + } +} + +public class ResolvedParameters +{ + public readonly Parameters Raw; + public readonly Dictionary MemberParams; + public readonly Dictionary SystemParams; + + public ResolvedParameters(Parameters parameters, Dictionary member_params, Dictionary system_params) + { + Raw = parameters; + MemberParams = member_params; + SystemParams = system_params; + } +} + +// TODO: move this to another file +public static class ParametersExt +{ + public static bool HasFlag(this Parameters parameters, params string[] potentialMatches) + { + return potentialMatches.Any(parameters.Flags().ContainsKey); } } \ No newline at end of file diff --git a/PluralKit.Bot/Commands/Member.cs b/PluralKit.Bot/Commands/Member.cs index 2a82f85c..a32b1e58 100644 --- a/PluralKit.Bot/Commands/Member.cs +++ b/PluralKit.Bot/Commands/Member.cs @@ -27,10 +27,10 @@ public class Member _avatarHosting = avatarHosting; } - public async Task NewMember(Context ctx) + public async Task NewMember(Context ctx, string memberName) { if (ctx.System == null) throw Errors.NoSystemError(ctx.DefaultPrefix); - var memberName = ctx.RemainderOrNull() ?? throw new PKSyntaxError("You must pass a member name."); + memberName = memberName ?? throw new PKSyntaxError("You must pass a member name."); // Hard name length cap if (memberName.Length > Limits.MaxMemberNameLength) diff --git a/PluralKit.Bot/Handlers/MessageCreated.cs b/PluralKit.Bot/Handlers/MessageCreated.cs index 2bb6bfcf..70ae757b 100644 --- a/PluralKit.Bot/Handlers/MessageCreated.cs +++ b/PluralKit.Bot/Handlers/MessageCreated.cs @@ -137,8 +137,22 @@ public class MessageCreated: IEventHandler var system = await _repo.GetSystemByAccount(evt.Author.Id); var config = system != null ? await _repo.GetSystemConfig(system.Id) : null; var guildConfig = guild != null ? await _repo.GetGuild(guild.Id) : null; - - await _tree.ExecuteCommand(new Context(_services, shardId, guild, channel, evt, cmdStart, system, config, guildConfig, _config.Prefixes ?? BotConfig.DefaultPrefixes)); + var ctx = new Context(_services, shardId, guild, channel, evt, cmdStart, system, config, guildConfig, _config.Prefixes ?? BotConfig.DefaultPrefixes); + try + { + var parameters = new Parameters(evt.Content?.Substring(cmdStart)); + var resolved_parameters = await parameters.ResolveParameters(ctx); + await _tree.ExecuteCommand(ctx, resolved_parameters); + } + catch (PKError e) + { + // don't send an "invalid command" response if the guild has those turned off + if (!(ctx.GuildConfig != null && ctx.GuildConfig!.InvalidCommandResponseEnabled != true)) + { + await ctx.Reply($"{Emojis.Error} {e.Message}"); + } + throw; + } } catch (PKError) { diff --git a/crates/commands/Cargo.toml b/crates/commands/Cargo.toml index 65e28ad4..04a68b9c 100644 --- a/crates/commands/Cargo.toml +++ b/crates/commands/Cargo.toml @@ -11,6 +11,7 @@ lazy_static = { workspace = true } uniffi = { version = "0.25" } smol_str = "0.3.2" +ordermap = "0.5" [build-dependencies] uniffi = { version = "0.25", features = [ "build" ] } diff --git a/crates/commands/src/commands.udl b/crates/commands/src/commands.udl index cc7af428..22396de4 100644 --- a/crates/commands/src/commands.udl +++ b/crates/commands/src/commands.udl @@ -6,8 +6,21 @@ interface CommandResult { Ok(ParsedCommand command); Err(string error); }; +[Enum] +interface ParameterKind { + MemberRef(); + SystemRef(); + MemberPrivacyTarget(); + PrivacyLevel(); + OpaqueString(); +}; +dictionary Parameter { + string raw; + ParameterKind kind; +}; dictionary ParsedCommand { string command_ref; sequence args; + record params; record flags; }; diff --git a/crates/commands/src/commands/member.rs b/crates/commands/src/commands/member.rs index ae5c1e85..4ea763a1 100644 --- a/crates/commands/src/commands/member.rs +++ b/crates/commands/src/commands/member.rs @@ -10,37 +10,37 @@ pub fn cmds() -> impl Iterator { [ command!( - [member, new, MemberRef], + [member, new, FullString("name")], "member_new", "Creates a new system member" ), command!( - [member, MemberRef], + [member, MemberRef("target")], "member_show", "Shows information about a member" ), command!( - [member, MemberRef, description], + [member, MemberRef("target"), description], "member_desc_show", "Shows a member's description" ), command!( - [member, MemberRef, description, FullString], + [member, MemberRef("target"), description, FullString("description")], "member_desc_update", "Changes a member's description" ), command!( - [member, MemberRef, privacy], + [member, MemberRef("target"), privacy], "member_privacy_show", "Displays a member's current privacy settings" ), command!( [ member, - MemberRef, + MemberRef("target"), privacy, - MemberPrivacyTarget, - PrivacyLevel + MemberPrivacyTarget("privacy_target"), + PrivacyLevel("new_privacy_level") ], "member_privacy_update", "Changes a member's privacy settings" diff --git a/crates/commands/src/commands/system.rs b/crates/commands/src/commands/system.rs index 3e10e992..37a67613 100644 --- a/crates/commands/src/commands/system.rs +++ b/crates/commands/src/commands/system.rs @@ -14,7 +14,7 @@ pub fn cmds() -> impl Iterator { ), command!([system, new], "system_new", "Creates a new system"), command!( - [system, new, SystemRef], + [system, new, FullString("name")], "system_new", "Creates a new system" ), diff --git a/crates/commands/src/lib.rs b/crates/commands/src/lib.rs index 4b239cd1..e9f3bd7a 100644 --- a/crates/commands/src/lib.rs +++ b/crates/commands/src/lib.rs @@ -10,7 +10,8 @@ uniffi::include_scaffolding!("commands"); use core::panic; use std::collections::HashMap; -use smol_str::SmolStr; +use ordermap::OrderMap; +use smol_str::{format_smolstr, SmolStr}; use tree::TreeBranch; pub use commands::Command; @@ -21,27 +22,70 @@ lazy_static::lazy_static! { let mut tree = TreeBranch { current_command_key: None, possible_tokens: vec![], - branches: HashMap::new(), + branches: OrderMap::new(), }; - crate::commands::all().iter().for_each(|x| tree.register_command(x.clone())); - - tree.sort_tokens(); - - // println!("{{tree:#?}}"); + crate::commands::all().into_iter().for_each(|x| tree.register_command(x)); tree }; } +#[derive(Debug)] pub enum CommandResult { Ok { command: ParsedCommand }, Err { error: String }, } +#[derive(Debug)] +pub enum ParameterKind { + MemberRef, + SystemRef, + MemberPrivacyTarget, + PrivacyLevel, + OpaqueString, +} + +#[derive(Debug)] +pub struct Parameter { + raw: String, + kind: ParameterKind, +} + +impl Parameter { + fn new(raw: impl ToString, kind: ParameterKind) -> Self { + Self { + raw: raw.to_string(), + kind, + } + } +} + +macro_rules! parameter_impl { + ($($name:ident $kind:ident),*) => { + impl Parameter { + $( + fn $name(raw: impl ToString) -> Self { + Self::new(raw, $crate::ParameterKind::$kind) + } + )* + } + }; +} + +parameter_impl! { + opaque OpaqueString, + member MemberRef, + system SystemRef, + member_privacy_target MemberPrivacyTarget, + privacy_level PrivacyLevel +} + +#[derive(Debug)] pub struct ParsedCommand { pub command_ref: String, pub args: Vec, + pub params: HashMap, pub flags: HashMap>, } @@ -53,9 +97,11 @@ fn parse_command(input: String) -> CommandResult { let mut current_pos = 0; let mut args: Vec = Vec::new(); + let mut params: HashMap = HashMap::new(); let mut flags: HashMap> = HashMap::new(); loop { + println!("{:?}", local_tree.possible_tokens); let next = next_token( local_tree.possible_tokens.clone(), input.clone(), @@ -70,8 +116,22 @@ fn parse_command(input: String) -> CommandResult { continue; } - if let Some(arg) = arg { - args.push(arg.into()); + if let Some(arg) = arg.as_ref() { + // get param name from token + // TODO: idk if this should be on token itself, doesn't feel right, but does work + let param = match &found_token { + Token::FullString(n) => Some((n, Parameter::opaque(arg))), + Token::MemberRef(n) => Some((n, Parameter::member(arg))), + Token::MemberPrivacyTarget(n) => Some((n, Parameter::member_privacy_target(arg))), + Token::SystemRef(n) => Some((n, Parameter::system(arg))), + Token::PrivacyLevel(n) => Some((n, Parameter::privacy_level(arg))), + _ => None, + }; + // insert arg as paramater if this is a parameter + if let Some((param_name, param)) = param { + params.insert(param_name.to_string(), param); + } + args.push(arg.to_string()); } if let Some(next_tree) = local_tree.branches.get(&found_token) { @@ -82,9 +142,11 @@ fn parse_command(input: String) -> CommandResult { } Err(None) => { if let Some(command_ref) = local_tree.current_command_key { + println!("{command_ref} {params:?}"); return CommandResult::Ok { command: ParsedCommand { command_ref: command_ref.into(), + params, args, flags, }, @@ -136,19 +198,12 @@ fn next_token( // iterate over tokens and run try_match for token in possible_tokens { - if let TokenMatchResult::Match(value) = - // for FullString just send the whole string - token.try_match(if matches!(token, Token::FullString) { - if input.is_empty() { - None - } else { - Some(input.clone()) - } - } else { - param.clone().map(|v| v.0) - }) - { - return Ok((token, value, param.map(|v| v.1).unwrap_or(current_pos))); + // for FullString just send the whole string + let input_to_match = param.clone().map(|v| v.0); + match token.try_match(input_to_match) { + TokenMatchResult::Match(value) => return Ok((token, value, param.map(|v| v.1).unwrap_or(current_pos))), + TokenMatchResult::MissingParameter { name } => return Err(Some(format_smolstr!("Missing parameter `{name}` in command `{input} [{name}]`."))), + TokenMatchResult::NoMatch => {} } } diff --git a/crates/commands/src/token.rs b/crates/commands/src/token.rs index 3d933668..6588fc8d 100644 --- a/crates/commands/src/token.rs +++ b/crates/commands/src/token.rs @@ -1,5 +1,7 @@ use smol_str::{SmolStr, ToSmolStr}; +type ParamName = &'static str; + #[derive(Debug, Clone, Eq, Hash, PartialEq)] pub enum Token { /// Token used to represent a finished command (i.e. no more parameters required) @@ -12,16 +14,16 @@ pub enum Token { // todo! MultiValue(Vec>), - FullString, + FullString(ParamName), /// Member reference (hid or member name) - MemberRef, - MemberPrivacyTarget, + MemberRef(ParamName), + MemberPrivacyTarget(ParamName), /// System reference - SystemRef, + SystemRef(ParamName), - PrivacyLevel, + PrivacyLevel(ParamName), // currently not included in command definitions // todo: flags with values @@ -32,6 +34,9 @@ pub enum TokenMatchResult { NoMatch, /// Token matched, optionally with a value. Match(Option), + MissingParameter { + name: ParamName, + }, } // move this somewhere else @@ -43,36 +48,38 @@ impl Token { if matches!(self, Self::Empty) && input.is_none() { return TokenMatchResult::Match(None); } else if input.is_none() { - return TokenMatchResult::NoMatch; + return match self { + Self::FullString(param_name) => TokenMatchResult::MissingParameter { name: param_name }, + Self::MemberRef(param_name) => TokenMatchResult::MissingParameter { name: param_name }, + Self::MemberPrivacyTarget(param_name) => TokenMatchResult::MissingParameter { name: param_name }, + Self::SystemRef(param_name) => TokenMatchResult::MissingParameter { name: param_name }, + Self::PrivacyLevel(param_name) => TokenMatchResult::MissingParameter { name: param_name }, + _ => TokenMatchResult::NoMatch, + } } - let input = input.unwrap(); + let input = input.as_ref().map(|s| s.trim()).unwrap(); // try actually matching stuff match self { Self::Empty => return TokenMatchResult::NoMatch, Self::Flag => unreachable!(), // matched upstream - Self::Value(values) => { - for v in values { - if input.trim() == v { - // c# bot currently needs subcommands provided as arguments - // todo!: remove this - return TokenMatchResult::Match(Some(v.clone())); - } - } + Self::Value(values) if values.iter().any(|v| v.eq(input)) => { + return TokenMatchResult::Match(None); } + Self::Value(_) => {} Self::MultiValue(_) => todo!(), - Self::FullString => return TokenMatchResult::Match(Some(input)), - Self::SystemRef => return TokenMatchResult::Match(Some(input)), - Self::MemberRef => return TokenMatchResult::Match(Some(input)), - Self::MemberPrivacyTarget if MEMBER_PRIVACY_TARGETS.contains(&input.trim()) => { - return TokenMatchResult::Match(Some(input)) + Self::FullString(_) => return TokenMatchResult::Match(Some(input.into())), + Self::SystemRef(_) => return TokenMatchResult::Match(Some(input.into())), + Self::MemberRef(_) => return TokenMatchResult::Match(Some(input.into())), + Self::MemberPrivacyTarget(_) if MEMBER_PRIVACY_TARGETS.contains(&input) => { + return TokenMatchResult::Match(Some(input.into())) } - Self::MemberPrivacyTarget => {} - Self::PrivacyLevel if input == "public" || input == "private" => { - return TokenMatchResult::Match(Some(input)) + Self::MemberPrivacyTarget(_) => {} + Self::PrivacyLevel(_) if input == "public" || input == "private" => { + return TokenMatchResult::Match(Some(input.into())) } - Self::PrivacyLevel => {} + Self::PrivacyLevel(_) => {} } // note: must not add a _ case to the above match // instead, for conditional matches, also add generic cases with no return diff --git a/crates/commands/src/tree.rs b/crates/commands/src/tree.rs index 56c53336..6dee7f0a 100644 --- a/crates/commands/src/tree.rs +++ b/crates/commands/src/tree.rs @@ -1,14 +1,15 @@ +use ordermap::OrderMap; use smol_str::SmolStr; use crate::{commands::Command, Token}; -use std::{cmp::Ordering, collections::HashMap}; +use std::cmp::Ordering; #[derive(Debug, Clone)] pub struct TreeBranch { pub current_command_key: Option, /// branches.keys(), but sorted by specificity pub possible_tokens: Vec, - pub branches: HashMap, + pub branches: OrderMap, } impl TreeBranch { @@ -20,7 +21,7 @@ impl TreeBranch { current_branch = current_branch.branches.entry(token).or_insert(TreeBranch { current_command_key: None, possible_tokens: vec![], - branches: HashMap::new(), + branches: OrderMap::new(), }) } // when we're out of tokens, add an Empty branch with the callback and no sub-branches @@ -29,31 +30,8 @@ impl TreeBranch { TreeBranch { current_command_key: Some(command.cb), possible_tokens: vec![], - branches: HashMap::new(), + branches: OrderMap::new(), }, ); } - - pub fn sort_tokens(&mut self) { - for branch in self.branches.values_mut() { - branch.sort_tokens(); - } - - // put Value tokens at the end - // i forget exactly how this works - // todo!: document this before PR mergs - self.possible_tokens = self - .branches - .keys() - .into_iter() - .map(|v| v.clone()) - .collect(); - self.possible_tokens.sort_by(|v, _| { - if matches!(v, Token::Value(_)) { - Ordering::Greater - } else { - Ordering::Less - } - }); - } } From b477ade3fe8b571237b7729164142a2db2915186 Mon Sep 17 00:00:00 2001 From: dusk Date: Sun, 5 Jan 2025 13:12:02 +0900 Subject: [PATCH 028/179] fix(commands): don't use possible tokens, directly use branches.keys() since its ordered now --- crates/commands/src/lib.rs | 6 +++--- crates/commands/src/tree.rs | 5 ----- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/crates/commands/src/lib.rs b/crates/commands/src/lib.rs index e9f3bd7a..927f1503 100644 --- a/crates/commands/src/lib.rs +++ b/crates/commands/src/lib.rs @@ -21,7 +21,6 @@ lazy_static::lazy_static! { pub static ref COMMAND_TREE: TreeBranch = { let mut tree = TreeBranch { current_command_key: None, - possible_tokens: vec![], branches: OrderMap::new(), }; @@ -101,12 +100,13 @@ fn parse_command(input: String) -> CommandResult { let mut flags: HashMap> = HashMap::new(); loop { - println!("{:?}", local_tree.possible_tokens); + println!("possible: {:?}", local_tree.branches.keys()); let next = next_token( - local_tree.possible_tokens.clone(), + local_tree.branches.keys().cloned().collect(), input.clone(), current_pos, ); + println!("next: {:?}", next); match next { Ok((found_token, arg, new_pos)) => { current_pos = new_pos; diff --git a/crates/commands/src/tree.rs b/crates/commands/src/tree.rs index 6dee7f0a..77bd2f8c 100644 --- a/crates/commands/src/tree.rs +++ b/crates/commands/src/tree.rs @@ -2,13 +2,10 @@ use ordermap::OrderMap; use smol_str::SmolStr; use crate::{commands::Command, Token}; -use std::cmp::Ordering; #[derive(Debug, Clone)] pub struct TreeBranch { pub current_command_key: Option, - /// branches.keys(), but sorted by specificity - pub possible_tokens: Vec, pub branches: OrderMap, } @@ -20,7 +17,6 @@ impl TreeBranch { // recursively get or create a sub-branch for each token current_branch = current_branch.branches.entry(token).or_insert(TreeBranch { current_command_key: None, - possible_tokens: vec![], branches: OrderMap::new(), }) } @@ -29,7 +25,6 @@ impl TreeBranch { Token::Empty, TreeBranch { current_command_key: Some(command.cb), - possible_tokens: vec![], branches: OrderMap::new(), }, ); From 77f564230740e6638310da8d713d2df04fff7bfb Mon Sep 17 00:00:00 2001 From: dusk Date: Sun, 5 Jan 2025 13:12:07 +0900 Subject: [PATCH 029/179] feat: meow --- PluralKit.Bot/CommandMeta/CommandTree.cs | 6 ++++-- PluralKit.Bot/Commands/Fun.cs | 3 +++ crates/commands/src/commands/fun.rs | 9 ++++----- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/PluralKit.Bot/CommandMeta/CommandTree.cs b/PluralKit.Bot/CommandMeta/CommandTree.cs index 554e623c..60e8d7e4 100644 --- a/PluralKit.Bot/CommandMeta/CommandTree.cs +++ b/PluralKit.Bot/CommandMeta/CommandTree.cs @@ -8,8 +8,6 @@ public partial class CommandTree { switch (parameters.Raw.Callback()) { - case "fun_thunder": - return ctx.Execute(null, m => m.Thunder(ctx)); case "help": return ctx.Execute(Help, m => m.HelpRoot(ctx)); case "help_commands": @@ -22,6 +20,10 @@ public partial class CommandTree return ctx.Execute(MemberInfo, m => m.ViewMember(ctx, parameters.MemberParams["target"])); case "member_new": return ctx.Execute(MemberNew, m => m.NewMember(ctx, parameters.Raw.Params()["name"])); + case "fun_thunder": + return ctx.Execute(null, m => m.Thunder(ctx)); + case "fun_meow": + return ctx.Execute(null, m => m.Meow(ctx)); default: return ctx.Reply( $"{Emojis.Error} Parsed command {parameters.Raw.Callback().AsCode()} not implemented in PluralKit.Bot!"); diff --git a/PluralKit.Bot/Commands/Fun.cs b/PluralKit.Bot/Commands/Fun.cs index b1ab53e0..9763f186 100644 --- a/PluralKit.Bot/Commands/Fun.cs +++ b/PluralKit.Bot/Commands/Fun.cs @@ -34,6 +34,9 @@ public class Fun public Task Sus(Context ctx) => ctx.Reply("\U0001F4EE"); + public Task Meow(Context ctx) => + ctx.Reply("*mrrp :3*"); + public Task Error(Context ctx) { if (ctx.Match("message")) diff --git a/crates/commands/src/commands/fun.rs b/crates/commands/src/commands/fun.rs index 472997ac..a8bddf45 100644 --- a/crates/commands/src/commands/fun.rs +++ b/crates/commands/src/commands/fun.rs @@ -1,10 +1,9 @@ use super::*; pub fn cmds() -> impl Iterator { - [command!( - ["thunder"], - "fun_thunder", - "fun thunder" - )] + [ + command!(["thunder"], "fun_thunder", "fun thunder"), + command!(["meow"], "fun_meow", "fun meow"), + ] .into_iter() } From 021a5ae897c2c69347174e1acbedf12da376b0f7 Mon Sep 17 00:00:00 2001 From: dusk Date: Sun, 5 Jan 2025 16:30:05 +0900 Subject: [PATCH 030/179] chore: more todos --- PluralKit.Bot/CommandSystem/ParametersFFI.cs | 5 +++-- PluralKit.Bot/Handlers/MessageCreated.cs | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/PluralKit.Bot/CommandSystem/ParametersFFI.cs b/PluralKit.Bot/CommandSystem/ParametersFFI.cs index ac97af75..f7831e80 100644 --- a/PluralKit.Bot/CommandSystem/ParametersFFI.cs +++ b/PluralKit.Bot/CommandSystem/ParametersFFI.cs @@ -73,6 +73,7 @@ public class Parameters } } +// TODO: im not really sure if this should be the way to go public class ResolvedParameters { public readonly Parameters Raw; @@ -87,11 +88,11 @@ public class ResolvedParameters } } -// TODO: move this to another file +// TODO: move this to another file (?) public static class ParametersExt { public static bool HasFlag(this Parameters parameters, params string[] potentialMatches) { return potentialMatches.Any(parameters.Flags().ContainsKey); } -} \ No newline at end of file +} diff --git a/PluralKit.Bot/Handlers/MessageCreated.cs b/PluralKit.Bot/Handlers/MessageCreated.cs index 70ae757b..293f7bb4 100644 --- a/PluralKit.Bot/Handlers/MessageCreated.cs +++ b/PluralKit.Bot/Handlers/MessageCreated.cs @@ -147,6 +147,7 @@ public class MessageCreated: IEventHandler catch (PKError e) { // don't send an "invalid command" response if the guild has those turned off + // TODO: only dont send command not found, not every parse error (eg. missing params, syntax error...) if (!(ctx.GuildConfig != null && ctx.GuildConfig!.InvalidCommandResponseEnabled != true)) { await ctx.Reply($"{Emojis.Error} {e.Message}"); @@ -211,4 +212,4 @@ public class MessageCreated: IEventHandler return false; } -} \ No newline at end of file +} From 7496ae1c45e92ddcf247598f3697851ce4d73f1b Mon Sep 17 00:00:00 2001 From: dusk Date: Sun, 5 Jan 2025 16:31:02 +0900 Subject: [PATCH 031/179] refactor: rename parameters file since that's the only one we are using now --- PluralKit.Bot/CommandSystem/{ParametersFFI.cs => Parameters.cs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename PluralKit.Bot/CommandSystem/{ParametersFFI.cs => Parameters.cs} (100%) diff --git a/PluralKit.Bot/CommandSystem/ParametersFFI.cs b/PluralKit.Bot/CommandSystem/Parameters.cs similarity index 100% rename from PluralKit.Bot/CommandSystem/ParametersFFI.cs rename to PluralKit.Bot/CommandSystem/Parameters.cs From b29c51f103b7579c70c828d176e8826a80c4eaa7 Mon Sep 17 00:00:00 2001 From: dusk Date: Tue, 7 Jan 2025 14:11:10 +0900 Subject: [PATCH 032/179] build(nix): use nix develop in process scripts instead of repeating inputs in runtimeInputs --- flake.nix | 45 ++++++++++++++++----------------------------- 1 file changed, 16 insertions(+), 29 deletions(-) diff --git a/flake.nix b/flake.nix index 2a44085a..69b3d73a 100644 --- a/flake.nix +++ b/flake.nix @@ -162,30 +162,20 @@ let procCfg = composeCfg.settings.processes; mkServiceProcess = - name: - { - inputs ? [ ], - ... - }@attrs: + name: attrs: let shell = rustOutputs.${name}.devShell; - filteredAttrs = lib.removeAttrs attrs ["inputs"]; in - filteredAttrs // { + attrs + // { command = pkgs.writeShellApplication { name = "pluralkit-${name}"; - runtimeInputs = - (with pkgs; [ - coreutils - shell.stdenv.cc - ]) - ++ shell.nativeBuildInputs - ++ inputs; + runtimeInputs = [ pkgs.coreutils ]; text = '' ${sourceDotenv} set -x ${pluralkitConfCheck} - exec cargo run --package ${name} + nix develop .#services -c cargo run --package ${name} ''; }; }; @@ -195,38 +185,35 @@ pluralkit-bot = { command = pkgs.writeShellApplication { name = "pluralkit-bot"; - runtimeInputs = self'.devShells.bot.nativeBuildInputs ++ [ - pkgs.coreutils - pkgs.git - self'.devShells.bot.stdenv.cc - ]; + runtimeInputs = [ pkgs.coreutils ]; text = '' ${sourceDotenv} set -x ${pluralkitConfCheck} ${self'.apps.generate-command-parser-bindings.program} - dotnet build ./PluralKit.Bot/PluralKit.Bot.csproj -c Release -o obj/ - exec dotnet obj/PluralKit.Bot.dll + nix develop .#bot -c bash -c "dotnet build ./PluralKit.Bot/PluralKit.Bot.csproj -c Release -o obj/ && dotnet obj/PluralKit.Bot.dll" ''; }; depends_on.postgres.condition = "process_healthy"; depends_on.redis.condition = "process_healthy"; - depends_on.pluralkit-gateway.condition = "process_healthy"; + depends_on.pluralkit-gateway.condition = "process_log_ready"; # TODO: add liveness check ready_log_line = "Received Ready"; + availability.restart = "on_failure"; + availability.max_restarts = 3; }; ### gateway ### pluralkit-gateway = mkServiceProcess "gateway" { - inputs = with pkgs; [curl gnugrep]; depends_on.postgres.condition = "process_healthy"; depends_on.redis.condition = "process_healthy"; # configure health checks # TODO: don't assume port? - liveness_probe.exec.command = ''curl -s -o /dev/null -w "%{http_code}" http://localhost:5000/stats | grep "302"''; - liveness_probe.period_seconds = 5; - readiness_probe.exec.command = procCfg.pluralkit-gateway.liveness_probe.exec.command; - readiness_probe.period_seconds = 5; - readiness_probe.initial_delay_seconds = 3; + liveness_probe.exec.command = ''${pkgs.curl}/bin/curl -s -o /dev/null -w "%{http_code}" http://localhost:5000/stats | ${pkgs.busybox}/bin/grep "302"''; + liveness_probe.period_seconds = 7; + # TODO: add actual listening or running line in gateway + ready_log_line = "Running "; + availability.restart = "on_failure"; + availability.max_restarts = 3; }; # TODO: add the rest of the services }; From 482c9235072919b342700cddb38058c681c15bfd Mon Sep 17 00:00:00 2001 From: dusk Date: Tue, 7 Jan 2025 23:15:18 +0900 Subject: [PATCH 033/179] feat: better parameters handling, implement multi-token matching --- PluralKit.Bot/CommandMeta/CommandTree.cs | 51 ++- .../CommandSystem/Context/Context.cs | 12 +- .../Context/ContextEntityArgumentsExt.cs | 8 +- .../Context/ContextParametersExt.cs | 64 ++++ PluralKit.Bot/CommandSystem/Parameters.cs | 112 ++++--- PluralKit.Bot/Commands/Config.cs | 53 +-- PluralKit.Bot/Commands/Member.cs | 11 +- PluralKit.Bot/Handlers/MessageCreated.cs | 20 +- crates/commands/src/commands.rs | 1 + crates/commands/src/commands.udl | 18 +- crates/commands/src/commands/config.rs | 31 ++ crates/commands/src/commands/member.rs | 5 + crates/commands/src/lib.rs | 76 +---- crates/commands/src/token.rs | 310 +++++++++++++++--- 14 files changed, 521 insertions(+), 251 deletions(-) create mode 100644 PluralKit.Bot/CommandSystem/Context/ContextParametersExt.cs diff --git a/PluralKit.Bot/CommandMeta/CommandTree.cs b/PluralKit.Bot/CommandMeta/CommandTree.cs index 60e8d7e4..7ce43b3d 100644 --- a/PluralKit.Bot/CommandMeta/CommandTree.cs +++ b/PluralKit.Bot/CommandMeta/CommandTree.cs @@ -4,30 +4,29 @@ namespace PluralKit.Bot; public partial class CommandTree { - public Task ExecuteCommand(Context ctx, ResolvedParameters parameters) + public Task ExecuteCommand(Context ctx) { - switch (parameters.Raw.Callback()) + return ctx.Parameters.Callback() switch { - case "help": - return ctx.Execute(Help, m => m.HelpRoot(ctx)); - case "help_commands": - return ctx.Reply( - "For the list of commands, see the website: "); - case "help_proxy": - return ctx.Reply( - "The proxy help page has been moved! See the website: https://pluralkit.me/guide#proxying"); - case "member_show": - return ctx.Execute(MemberInfo, m => m.ViewMember(ctx, parameters.MemberParams["target"])); - case "member_new": - return ctx.Execute(MemberNew, m => m.NewMember(ctx, parameters.Raw.Params()["name"])); - case "fun_thunder": - return ctx.Execute(null, m => m.Thunder(ctx)); - case "fun_meow": - return ctx.Execute(null, m => m.Meow(ctx)); - default: - return ctx.Reply( - $"{Emojis.Error} Parsed command {parameters.Raw.Callback().AsCode()} not implemented in PluralKit.Bot!"); - } + "help" => ctx.Execute(Help, m => m.HelpRoot(ctx)), + "help_commands" => ctx.Reply( + "For the list of commands, see the website: "), + "help_proxy" => ctx.Reply( + "The proxy help page has been moved! See the website: https://pluralkit.me/guide#proxying"), + "member_show" => ctx.Execute(MemberInfo, m => m.ViewMember(ctx)), + "member_new" => ctx.Execute(MemberNew, m => m.NewMember(ctx)), + "member_soulscream" => ctx.Execute(MemberInfo, m => m.Soulscream(ctx)), + "cfg_ap_account_show" => ctx.Execute(null, m => m.ViewAutoproxyAccount(ctx)), + "cfg_ap_account_update" => ctx.Execute(null, m => m.EditAutoproxyAccount(ctx)), + "cfg_ap_timeout_show" => ctx.Execute(null, m => m.ViewAutoproxyTimeout(ctx)), + "cfg_ap_timeout_update" => ctx.Execute(null, m => m.EditAutoproxyTimeout(ctx)), + "fun_thunder" => ctx.Execute(null, m => m.Thunder(ctx)), + "fun_meow" => ctx.Execute(null, m => m.Meow(ctx)), + _ => + // this should only ever occur when deving if commands are not implemented... + ctx.Reply( + $"{Emojis.Error} Parsed command {ctx.Parameters.Callback().AsCode()} not implemented in PluralKit.Bot!"), + }; if (ctx.Match("system", "s")) return HandleSystemCommand(ctx); if (ctx.Match("member", "m")) @@ -405,10 +404,6 @@ public partial class CommandTree await ctx.Execute(MemberPrivacy, m => m.Privacy(ctx, target, PrivacyLevel.Private)); else if (ctx.Match("public", "shown", "show", "unhide", "unhidden")) await ctx.Execute(MemberPrivacy, m => m.Privacy(ctx, target, PrivacyLevel.Public)); - else if (ctx.Match("soulscream")) - await ctx.Execute(MemberInfo, m => m.Soulscream(ctx, target)); - else if (!ctx.HasNext()) // Bare command - await ctx.Execute(MemberInfo, m => m.ViewMember(ctx, target)); else await PrintCommandNotFoundError(ctx, MemberInfo, MemberRename, MemberDisplayName, MemberServerName, MemberDesc, MemberPronouns, MemberColor, MemberBirthday, MemberProxy, MemberDelete, MemberAvatar, @@ -576,10 +571,6 @@ public partial class CommandTree if (!ctx.HasNext()) return ctx.Execute(null, m => m.ShowConfig(ctx)); - if (ctx.MatchMultiple(new[] { "autoproxy", "ap" }, new[] { "account", "ac" })) - return ctx.Execute(null, m => m.AutoproxyAccount(ctx)); - if (ctx.MatchMultiple(new[] { "autoproxy", "ap" }, new[] { "timeout", "tm" })) - return ctx.Execute(null, m => m.AutoproxyTimeout(ctx)); if (ctx.Match("timezone", "zone", "tz")) return ctx.Execute(null, m => m.SystemTimezone(ctx)); if (ctx.Match("ping")) diff --git a/PluralKit.Bot/CommandSystem/Context/Context.cs b/PluralKit.Bot/CommandSystem/Context/Context.cs index 344e85d5..ad075cde 100644 --- a/PluralKit.Bot/CommandSystem/Context/Context.cs +++ b/PluralKit.Bot/CommandSystem/Context/Context.cs @@ -26,11 +26,9 @@ public class Context private readonly IMetrics _metrics; private readonly CommandMessageService _commandMessageService; - private Command? _currentCommand; - public Context(ILifetimeScope provider, int shardId, Guild? guild, Channel channel, MessageCreateEvent message, int commandParseOffset, PKSystem senderSystem, SystemConfig config, - GuildConfig? guildConfig, string[] prefixes) + GuildConfig? guildConfig, string[] prefixes, Parameters parameters) { Message = (Message)message; ShardId = shardId; @@ -50,6 +48,7 @@ public class Context DefaultPrefix = prefixes[0]; Rest = provider.Resolve(); Cluster = provider.Resolve(); + Parameters = parameters; } public readonly IDiscordCache Cache; @@ -75,6 +74,7 @@ public class Context public readonly string CommandPrefix; public readonly string DefaultPrefix; + public readonly Parameters Parameters; internal readonly IDatabase Database; internal readonly ModelRepository Repository; @@ -111,8 +111,6 @@ public class Context public async Task Execute(Command? commandDef, Func handler, bool deprecated = false) { - _currentCommand = commandDef; - if (deprecated && commandDef != null) { await Reply($"{Emojis.Warn} Server configuration has moved to `{DefaultPrefix}serverconfig`. The command you are trying to run is now `{DefaultPrefix}{commandDef.Key}`."); @@ -153,8 +151,8 @@ public class Context public LookupContext LookupContextFor(SystemId systemId) { - var hasPrivateOverride = this.MatchFlag("private", "priv"); - var hasPublicOverride = this.MatchFlag("public", "pub"); + var hasPrivateOverride = Parameters.HasFlag("private", "priv"); + var hasPublicOverride = Parameters.HasFlag("public", "pub"); if (hasPrivateOverride && hasPublicOverride) throw new PKError("Cannot match both public and private flags at the same time."); diff --git a/PluralKit.Bot/CommandSystem/Context/ContextEntityArgumentsExt.cs b/PluralKit.Bot/CommandSystem/Context/ContextEntityArgumentsExt.cs index 13d31cdb..104f1ad8 100644 --- a/PluralKit.Bot/CommandSystem/Context/ContextEntityArgumentsExt.cs +++ b/PluralKit.Bot/CommandSystem/Context/ContextEntityArgumentsExt.cs @@ -59,7 +59,7 @@ public static class ContextEntityArgumentsExt return null; } - public static async Task ParseMember(this Context ctx, Parameters parameters, string input, SystemId? restrictToSystem = null) + public static async Task ParseMember(this Context ctx, string input, bool byId, SystemId? restrictToSystem = null) { // Member references can have one of three forms, depending on // whether you're in a system or not: @@ -69,7 +69,7 @@ public static class ContextEntityArgumentsExt // Skip name / display name matching if the user does not have a system // or if they specifically request by-HID matching - if (ctx.System != null && !parameters.HasFlag("id", "by-id")) + if (ctx.System != null && !byId) { // First, try finding by member name in system if (await ctx.Repository.GetMemberByName(ctx.System.Id, input) is PKMember memberByName) @@ -169,9 +169,9 @@ public static class ContextEntityArgumentsExt return group; } - public static string CreateNotFoundError(this Context ctx, Parameters parameters, string entity, string input) + public static string CreateNotFoundError(this Context ctx, string entity, string input, bool byId = false) { - var isIDOnlyQuery = ctx.System == null || parameters.HasFlag("id", "by-id"); + var isIDOnlyQuery = ctx.System == null || byId; var inputIsHid = HidUtils.ParseHid(input) != null; if (isIDOnlyQuery) diff --git a/PluralKit.Bot/CommandSystem/Context/ContextParametersExt.cs b/PluralKit.Bot/CommandSystem/Context/ContextParametersExt.cs new file mode 100644 index 00000000..754403b0 --- /dev/null +++ b/PluralKit.Bot/CommandSystem/Context/ContextParametersExt.cs @@ -0,0 +1,64 @@ +using PluralKit.Core; + +namespace PluralKit.Bot; + +public static class ContextParametersExt +{ + public static async Task ParamResolveOpaque(this Context ctx, string param_name) + { + return await ctx.Parameters.ResolveParameter( + ctx, param_name, + param => (param as Parameter.Opaque)?.value + ); + } + + public static async Task ParamResolveMember(this Context ctx, string param_name) + { + return await ctx.Parameters.ResolveParameter( + ctx, param_name, + param => (param as Parameter.MemberRef)?.member + ); + } + + public static async Task ParamResolveSystem(this Context ctx, string param_name) + { + return await ctx.Parameters.ResolveParameter( + ctx, param_name, + param => (param as Parameter.SystemRef)?.system + ); + } + + public static async Task ParamResolveMemberPrivacyTarget(this Context ctx, string param_name) + { + return await ctx.Parameters.ResolveParameter( + ctx, param_name, + param => (param as Parameter.MemberPrivacyTarget)?.target + ); + } + + public static async Task ParamResolvePrivacyLevel(this Context ctx, string param_name) + { + return await ctx.Parameters.ResolveParameter( + ctx, param_name, + param => (param as Parameter.PrivacyLevel)?.level + ); + } + + public static async Task ParamResolveToggle(this Context ctx, string param_name) + { + return await ctx.Parameters.ResolveParameter( + ctx, param_name, + param => (param as Parameter.Toggle)?.value + ); + } + + // this can never really be false (either it's present and is true or it's not present) + // but we keep it nullable for consistency with the other methods + public static async Task ParamResolveReset(this Context ctx, string param_name) + { + return await ctx.Parameters.ResolveParameter( + ctx, param_name, + param => param is Parameter.Reset + ); + } +} \ No newline at end of file diff --git a/PluralKit.Bot/CommandSystem/Parameters.cs b/PluralKit.Bot/CommandSystem/Parameters.cs index f7831e80..e5e0efd2 100644 --- a/PluralKit.Bot/CommandSystem/Parameters.cs +++ b/PluralKit.Bot/CommandSystem/Parameters.cs @@ -1,14 +1,27 @@ +using System.Diagnostics; using PluralKit.Core; using uniffi.commands; namespace PluralKit.Bot; +// corresponds to the ffi Paramater type, but with stricter types (also avoiding exposing ffi types!) +public abstract record Parameter() +{ + public record MemberRef(PKMember member): Parameter; + public record SystemRef(PKSystem system): Parameter; + public record MemberPrivacyTarget(MemberPrivacySubject target): Parameter; + public record PrivacyLevel(string level): Parameter; + public record Toggle(bool value): Parameter; + public record Opaque(string value): Parameter; + public record Reset(): Parameter; +} + public class Parameters { private string _cb { get; init; } private List _args { get; init; } private Dictionary _flags { get; init; } - private Dictionary _params { get; init; } + private Dictionary _params { get; init; } // just used for errors, temporarily public string FullCommand { get; init; } @@ -31,68 +44,61 @@ public class Parameters } } - public async Task ResolveParameters(Context ctx) - { - var parsed_members = await MemberParams().ToAsyncEnumerable().ToDictionaryAwaitAsync(async item => item.Key, async item => - await ctx.ParseMember(this, item.Value) ?? throw new PKError(ctx.CreateNotFoundError(this, "Member", item.Value)) - ); - var parsed_systems = await SystemParams().ToAsyncEnumerable().ToDictionaryAwaitAsync(async item => item.Key, async item => - await ctx.ParseSystem(item.Value) ?? throw new PKError(ctx.CreateNotFoundError(this, "System", item.Value)) - ); - return new ResolvedParameters(this, parsed_members, parsed_systems); - } - public string Callback() { return _cb; } - public IDictionary Flags() + public bool HasFlag(params string[] potentialMatches) { - return _flags; + return potentialMatches.Any(_flags.ContainsKey); } - private Dictionary Params(Func filter) + // resolves a single parameter + private async Task ResolveParameter(Context ctx, string param_name) { - return _params.Where(item => filter(item.Value.@kind)).ToDictionary(item => item.Key, item => item.Value.@raw); + if (!_params.ContainsKey(param_name)) return null; + switch (_params[param_name]) + { + case uniffi.commands.Parameter.MemberRef memberRef: + var byId = HasFlag("id", "by-id"); + return new Parameter.MemberRef( + await ctx.ParseMember(memberRef.member, byId) + ?? throw new PKError(ctx.CreateNotFoundError("Member", memberRef.member, byId)) + ); + case uniffi.commands.Parameter.SystemRef systemRef: + // todo: do we need byId here? + return new Parameter.SystemRef( + await ctx.ParseSystem(systemRef.system) + ?? throw new PKError(ctx.CreateNotFoundError("System", systemRef.system)) + ); + case uniffi.commands.Parameter.MemberPrivacyTarget memberPrivacyTarget: + // this should never really fail... + // todo: we shouldn't have *three* different MemberPrivacyTarget types (rust, ffi, c#) syncing the cases will be annoying... + if (!MemberPrivacyUtils.TryParseMemberPrivacy(memberPrivacyTarget.target, out var target)) + throw new PKError($"Invalid member privacy target {memberPrivacyTarget.target}"); + return new Parameter.MemberPrivacyTarget(target); + case uniffi.commands.Parameter.PrivacyLevel privacyLevel: + return new Parameter.PrivacyLevel(privacyLevel.level); + case uniffi.commands.Parameter.Toggle toggle: + return new Parameter.Toggle(toggle.toggle); + case uniffi.commands.Parameter.OpaqueString opaque: + return new Parameter.Opaque(opaque.raw); + case uniffi.commands.Parameter.Reset _: + return new Parameter.Reset(); + } + // this should also never happen + throw new PKError($"Unknown parameter type for parameter {param_name}"); } - public IDictionary Params() + public async Task ResolveParameter(Context ctx, string param_name, Func extract_func) { - return Params(_ => true); + var param = await ResolveParameter(ctx, param_name); + // todo: i think this should return null for everything...? + if (param == null) return default; + return extract_func(param) + // this should never really happen (hopefully!), but in case the parameter names dont match up (typos...) between rust <-> c#... + // (it would be very cool to have this statically checked somehow..?) + ?? throw new PKError($"Parameter {param_name.AsCode()} was not found for command {Callback().AsCode()} -- this is a bug!!"); } - - public IDictionary MemberParams() - { - return Params(kind => kind == ParameterKind.MemberRef); - } - - public IDictionary SystemParams() - { - return Params(kind => kind == ParameterKind.SystemRef); - } -} - -// TODO: im not really sure if this should be the way to go -public class ResolvedParameters -{ - public readonly Parameters Raw; - public readonly Dictionary MemberParams; - public readonly Dictionary SystemParams; - - public ResolvedParameters(Parameters parameters, Dictionary member_params, Dictionary system_params) - { - Raw = parameters; - MemberParams = member_params; - SystemParams = system_params; - } -} - -// TODO: move this to another file (?) -public static class ParametersExt -{ - public static bool HasFlag(this Parameters parameters, params string[] potentialMatches) - { - return potentialMatches.Any(parameters.Flags().ContainsKey); - } -} +} \ No newline at end of file diff --git a/PluralKit.Bot/Commands/Config.cs b/PluralKit.Bot/Commands/Config.cs index 1b8efab1..e4fecbdc 100644 --- a/PluralKit.Bot/Commands/Config.cs +++ b/PluralKit.Bot/Commands/Config.cs @@ -190,17 +190,17 @@ public class Config } private string EnabledDisabled(bool value) => value ? "enabled" : "disabled"; - public async Task AutoproxyAccount(Context ctx) + public async Task ViewAutoproxyAccount(Context ctx) { var allowAutoproxy = await ctx.Repository.GetAutoproxyEnabled(ctx.Author.Id); - if (!ctx.HasNext()) - { - await ctx.Reply($"Autoproxy is currently **{EnabledDisabled(allowAutoproxy)}** for account <@{ctx.Author.Id}>."); - return; - } + await ctx.Reply($"Autoproxy is currently **{EnabledDisabled(allowAutoproxy)}** for account <@{ctx.Author.Id}>."); + } - var allow = ctx.MatchToggle(true); + public async Task EditAutoproxyAccount(Context ctx) + { + var allowAutoproxy = await ctx.Repository.GetAutoproxyEnabled(ctx.Author.Id); + var allow = await ctx.ParamResolveToggle("toggle") ?? throw new PKSyntaxError("You need to specify whether to enable or disable autoproxy for this account."); var statusString = EnabledDisabled(allow); if (allowAutoproxy == allow) @@ -213,31 +213,34 @@ public class Config await ctx.Reply($"{Emojis.Success} Autoproxy {statusString} for account <@{ctx.Author.Id}>."); } - - public async Task AutoproxyTimeout(Context ctx) + public async Task ViewAutoproxyTimeout(Context ctx) { - if (!ctx.HasNext()) - { - var timeout = ctx.Config.LatchTimeout.HasValue - ? Duration.FromSeconds(ctx.Config.LatchTimeout.Value) - : (Duration?)null; + var timeout = ctx.Config.LatchTimeout.HasValue + ? Duration.FromSeconds(ctx.Config.LatchTimeout.Value) + : (Duration?)null; - if (timeout == null) - await ctx.Reply($"You do not have a custom autoproxy timeout duration set. The default latch timeout duration is {ProxyMatcher.DefaultLatchExpiryTime.ToTimeSpan().Humanize(4)}."); - else if (timeout == Duration.Zero) - await ctx.Reply("Latch timeout is currently **disabled** for your system. Latch mode autoproxy will never time out."); - else - await ctx.Reply($"The current latch timeout duration for your system is {timeout.Value.ToTimeSpan().Humanize(4)}."); - return; - } + if (timeout == null) + await ctx.Reply($"You do not have a custom autoproxy timeout duration set. The default latch timeout duration is {ProxyMatcher.DefaultLatchExpiryTime.ToTimeSpan().Humanize(4)}."); + else if (timeout == Duration.Zero) + await ctx.Reply("Latch timeout is currently **disabled** for your system. Latch mode autoproxy will never time out."); + else + await ctx.Reply($"The current latch timeout duration for your system is {timeout.Value.ToTimeSpan().Humanize(4)}."); + } + + public async Task EditAutoproxyTimeout(Context ctx) + { + var _newTimeout = await ctx.ParamResolveOpaque("timeout"); + var _reset = await ctx.ParamResolveReset("reset"); + var _toggle = await ctx.ParamResolveToggle("toggle"); Duration? newTimeout; Duration overflow = Duration.Zero; - if (ctx.Match("off", "stop", "cancel", "no", "disable", "remove")) newTimeout = Duration.Zero; - else if (ctx.MatchClear()) newTimeout = null; + if (_toggle == false) newTimeout = Duration.Zero; + else if (_reset == true) newTimeout = null; else { - var timeoutStr = ctx.RemainderOrNull(); + // todo: we should parse date in the command parser + var timeoutStr = _newTimeout; var timeoutPeriod = DateUtils.ParsePeriod(timeoutStr); if (timeoutPeriod == null) throw new PKError($"Could not parse '{timeoutStr}' as a valid duration. Try using a syntax such as \"3h5m\" (i.e. 3 hours and 5 minutes)."); if (timeoutPeriod.Value.TotalHours > 100000) diff --git a/PluralKit.Bot/Commands/Member.cs b/PluralKit.Bot/Commands/Member.cs index a32b1e58..ef2065cc 100644 --- a/PluralKit.Bot/Commands/Member.cs +++ b/PluralKit.Bot/Commands/Member.cs @@ -1,4 +1,5 @@ using System.Net; +using System.Reflection.Metadata; using System.Web; using Dapper; @@ -27,8 +28,10 @@ public class Member _avatarHosting = avatarHosting; } - public async Task NewMember(Context ctx, string memberName) + public async Task NewMember(Context ctx) { + var memberName = await ctx.ParamResolveOpaque("name"); + if (ctx.System == null) throw Errors.NoSystemError(ctx.DefaultPrefix); memberName = memberName ?? throw new PKSyntaxError("You must pass a member name."); @@ -124,17 +127,19 @@ public class Member $"{Emojis.Warn} You are approaching the per-system member limit ({memberCount} / {memberLimit} members). Once you reach this limit, you will be unable to create new members until existing members are deleted, or you can ask for your limit to be raised in the PluralKit support server: "); } - public async Task ViewMember(Context ctx, PKMember target) + public async Task ViewMember(Context ctx) { + var target = await ctx.ParamResolveMember("target"); var system = await ctx.Repository.GetSystem(target.System); await ctx.Reply( embed: await _embeds.CreateMemberEmbed(system, target, ctx.Guild, ctx.Config, ctx.LookupContextFor(system.Id), ctx.Zone)); } - public async Task Soulscream(Context ctx, PKMember target) + public async Task Soulscream(Context ctx) { // this is for a meme, please don't take this code seriously. :) + var target = await ctx.ParamResolveMember("target"); var name = target.NameFor(ctx.LookupContextFor(target.System)); var encoded = HttpUtility.UrlEncode(name); diff --git a/PluralKit.Bot/Handlers/MessageCreated.cs b/PluralKit.Bot/Handlers/MessageCreated.cs index 293f7bb4..a2b34483 100644 --- a/PluralKit.Bot/Handlers/MessageCreated.cs +++ b/PluralKit.Bot/Handlers/MessageCreated.cs @@ -137,23 +137,29 @@ public class MessageCreated: IEventHandler var system = await _repo.GetSystemByAccount(evt.Author.Id); var config = system != null ? await _repo.GetSystemConfig(system.Id) : null; var guildConfig = guild != null ? await _repo.GetGuild(guild.Id) : null; - var ctx = new Context(_services, shardId, guild, channel, evt, cmdStart, system, config, guildConfig, _config.Prefixes ?? BotConfig.DefaultPrefixes); + + // parse parameters + Parameters parameters; try { - var parameters = new Parameters(evt.Content?.Substring(cmdStart)); - var resolved_parameters = await parameters.ResolveParameters(ctx); - await _tree.ExecuteCommand(ctx, resolved_parameters); + parameters = new Parameters(evt.Content?.Substring(cmdStart)); } catch (PKError e) { // don't send an "invalid command" response if the guild has those turned off // TODO: only dont send command not found, not every parse error (eg. missing params, syntax error...) - if (!(ctx.GuildConfig != null && ctx.GuildConfig!.InvalidCommandResponseEnabled != true)) + if (!(guildConfig != null && guildConfig!.InvalidCommandResponseEnabled != true)) { - await ctx.Reply($"{Emojis.Error} {e.Message}"); + await _rest.CreateMessage(channel.Id, new MessageRequest + { + Content = $"{Emojis.Error} {e.Message}", + }); } throw; } + + var ctx = new Context(_services, shardId, guild, channel, evt, cmdStart, system, config, guildConfig, _config.Prefixes ?? BotConfig.DefaultPrefixes, parameters); + await _tree.ExecuteCommand(ctx); } catch (PKError) { @@ -212,4 +218,4 @@ public class MessageCreated: IEventHandler return false; } -} +} \ No newline at end of file diff --git a/crates/commands/src/commands.rs b/crates/commands/src/commands.rs index b904bb59..692e0d50 100644 --- a/crates/commands/src/commands.rs +++ b/crates/commands/src/commands.rs @@ -58,6 +58,7 @@ pub fn all() -> Vec { (help::cmds()) .chain(system::cmds()) .chain(member::cmds()) + .chain(config::cmds()) .chain(fun::cmds()) .collect() } diff --git a/crates/commands/src/commands.udl b/crates/commands/src/commands.udl index 22396de4..9eb372a3 100644 --- a/crates/commands/src/commands.udl +++ b/crates/commands/src/commands.udl @@ -7,16 +7,14 @@ interface CommandResult { Err(string error); }; [Enum] -interface ParameterKind { - MemberRef(); - SystemRef(); - MemberPrivacyTarget(); - PrivacyLevel(); - OpaqueString(); -}; -dictionary Parameter { - string raw; - ParameterKind kind; +interface Parameter { + MemberRef(string member); + SystemRef(string system); + MemberPrivacyTarget(string target); + PrivacyLevel(string level); + OpaqueString(string raw); + Toggle(boolean toggle); + Reset(); }; dictionary ParsedCommand { string command_ref; diff --git a/crates/commands/src/commands/config.rs b/crates/commands/src/commands/config.rs index 8b137891..f75018be 100644 --- a/crates/commands/src/commands/config.rs +++ b/crates/commands/src/commands/config.rs @@ -1 +1,32 @@ +use super::*; +pub fn cmds() -> impl Iterator { + use Token::*; + + let cfg = ["config", "cfg"]; + let autoproxy = ["autoproxy", "ap"]; + + [ + command!( + [cfg, autoproxy, ["account", "ac"]], + "cfg_ap_account_show", + "Shows autoproxy status for the account" + ), + command!( + [cfg, autoproxy, ["account", "ac"], Toggle("toggle")], + "cfg_ap_account_update", + "Toggles autoproxy for the account" + ), + command!( + [cfg, autoproxy, ["timeout", "tm"]], + "cfg_ap_timeout_show", + "Shows the autoproxy timeout" + ), + command!( + [cfg, autoproxy, ["timeout", "tm"], [Toggle("toggle"), Reset("reset"), FullString("timeout")]], + "cfg_ap_timeout_update", + "Sets the autoproxy timeout" + ), + ] + .into_iter() +} diff --git a/crates/commands/src/commands/member.rs b/crates/commands/src/commands/member.rs index 4ea763a1..42c3e1d2 100644 --- a/crates/commands/src/commands/member.rs +++ b/crates/commands/src/commands/member.rs @@ -19,6 +19,11 @@ pub fn cmds() -> impl Iterator { "member_show", "Shows information about a member" ), + command!( + [member, MemberRef("target"), "soulscream"], + "member_soulscream", + "todo" + ), command!( [member, MemberRef("target"), description], "member_desc_show", diff --git a/crates/commands/src/lib.rs b/crates/commands/src/lib.rs index 927f1503..b0d417e1 100644 --- a/crates/commands/src/lib.rs +++ b/crates/commands/src/lib.rs @@ -36,48 +36,15 @@ pub enum CommandResult { Err { error: String }, } -#[derive(Debug)] -pub enum ParameterKind { - MemberRef, - SystemRef, - MemberPrivacyTarget, - PrivacyLevel, - OpaqueString, -} - -#[derive(Debug)] -pub struct Parameter { - raw: String, - kind: ParameterKind, -} - -impl Parameter { - fn new(raw: impl ToString, kind: ParameterKind) -> Self { - Self { - raw: raw.to_string(), - kind, - } - } -} - -macro_rules! parameter_impl { - ($($name:ident $kind:ident),*) => { - impl Parameter { - $( - fn $name(raw: impl ToString) -> Self { - Self::new(raw, $crate::ParameterKind::$kind) - } - )* - } - }; -} - -parameter_impl! { - opaque OpaqueString, - member MemberRef, - system SystemRef, - member_privacy_target MemberPrivacyTarget, - privacy_level PrivacyLevel +#[derive(Debug, Clone)] +pub enum Parameter { + MemberRef { member: String }, + SystemRef { system: String }, + MemberPrivacyTarget { target: String }, + PrivacyLevel { level: String }, + OpaqueString { raw: String }, + Toggle { toggle: bool }, + Reset, } #[derive(Debug)] @@ -111,27 +78,17 @@ fn parse_command(input: String) -> CommandResult { Ok((found_token, arg, new_pos)) => { current_pos = new_pos; if let Token::Flag = found_token { - flags.insert(arg.unwrap().into(), None); + flags.insert(arg.unwrap().raw.into(), None); // don't try matching flags as tree elements continue; } if let Some(arg) = arg.as_ref() { - // get param name from token - // TODO: idk if this should be on token itself, doesn't feel right, but does work - let param = match &found_token { - Token::FullString(n) => Some((n, Parameter::opaque(arg))), - Token::MemberRef(n) => Some((n, Parameter::member(arg))), - Token::MemberPrivacyTarget(n) => Some((n, Parameter::member_privacy_target(arg))), - Token::SystemRef(n) => Some((n, Parameter::system(arg))), - Token::PrivacyLevel(n) => Some((n, Parameter::privacy_level(arg))), - _ => None, - }; // insert arg as paramater if this is a parameter - if let Some((param_name, param)) = param { - params.insert(param_name.to_string(), param); + if let Some((param_name, param)) = arg.param.as_ref() { + params.insert(param_name.to_string(), param.clone()); } - args.push(arg.to_string()); + args.push(arg.raw.to_string()); } if let Some(next_tree) = local_tree.branches.get(&found_token) { @@ -178,7 +135,7 @@ fn next_token( possible_tokens: Vec, input: SmolStr, current_pos: usize, -) -> Result<(Token, Option, usize), Option> { +) -> Result<(Token, Option, usize), Option> { // get next parameter, matching quotes let param = crate::string::next_param(input.clone(), current_pos); println!("matched: {param:?}\n---"); @@ -191,7 +148,10 @@ fn next_token( { return Ok(( Token::Flag, - Some(value.trim_start_matches('-').into()), + Some(TokenMatchedValue { + raw: value, + param: None, + }), new_pos, )); } diff --git a/crates/commands/src/token.rs b/crates/commands/src/token.rs index 6588fc8d..2f82b207 100644 --- a/crates/commands/src/token.rs +++ b/crates/commands/src/token.rs @@ -1,5 +1,9 @@ +use std::str::FromStr; + use smol_str::{SmolStr, ToSmolStr}; +use crate::Parameter; + type ParamName = &'static str; #[derive(Debug, Clone, Eq, Hash, PartialEq)] @@ -8,86 +12,180 @@ pub enum Token { // todo: this is likely not the right way to represent this Empty, - /// A bot-defined value ("member" in `pk;member MyName`) - Value(Vec), - /// A command defined by multiple values - // todo! - MultiValue(Vec>), + /// multi-token matching + Any(Vec), + /// A bot-defined command / subcommand (usually) (eg. "member" in `pk;member MyName`) + Value(Vec), + + /// Opaque string (eg. "name" in `pk;member new name`) FullString(ParamName), /// Member reference (hid or member name) MemberRef(ParamName), + /// todo: doc MemberPrivacyTarget(ParamName), /// System reference SystemRef(ParamName), + /// todo: doc PrivacyLevel(ParamName), - // currently not included in command definitions + /// on, off; yes, no; true, false + Toggle(ParamName), + + /// reset, clear, default + Reset(ParamName), + + // todo: currently not included in command definitions // todo: flags with values Flag, } +// #[macro_export] +// macro_rules! any { +// ($($token:expr),+) => { +// Token::Any(vec![$($token.to_token()),+]) +// }; +// } + +#[derive(Debug)] pub enum TokenMatchResult { + /// Token did not match. NoMatch, /// Token matched, optionally with a value. - Match(Option), - MissingParameter { - name: ParamName, - }, + Match(Option), + /// A required parameter was missing. + MissingParameter { name: ParamName }, } -// move this somewhere else -const MEMBER_PRIVACY_TARGETS: &[&str] = &["visibility", "name", "todo"]; +#[derive(Debug)] +pub struct TokenMatchedValue { + pub raw: SmolStr, + pub param: Option<(ParamName, Parameter)>, +} -impl Token { - pub fn try_match(&self, input: Option) -> TokenMatchResult { - // short circuit on empty things - if matches!(self, Self::Empty) && input.is_none() { - return TokenMatchResult::Match(None); - } else if input.is_none() { - return match self { - Self::FullString(param_name) => TokenMatchResult::MissingParameter { name: param_name }, - Self::MemberRef(param_name) => TokenMatchResult::MissingParameter { name: param_name }, - Self::MemberPrivacyTarget(param_name) => TokenMatchResult::MissingParameter { name: param_name }, - Self::SystemRef(param_name) => TokenMatchResult::MissingParameter { name: param_name }, - Self::PrivacyLevel(param_name) => TokenMatchResult::MissingParameter { name: param_name }, - _ => TokenMatchResult::NoMatch, - } - } +impl TokenMatchResult { + fn new_match(raw: impl Into) -> Self { + Self::Match(Some(TokenMatchedValue { + raw: raw.into(), + param: None, + })) + } - let input = input.as_ref().map(|s| s.trim()).unwrap(); - - // try actually matching stuff - match self { - Self::Empty => return TokenMatchResult::NoMatch, - Self::Flag => unreachable!(), // matched upstream - Self::Value(values) if values.iter().any(|v| v.eq(input)) => { - return TokenMatchResult::Match(None); - } - Self::Value(_) => {} - Self::MultiValue(_) => todo!(), - Self::FullString(_) => return TokenMatchResult::Match(Some(input.into())), - Self::SystemRef(_) => return TokenMatchResult::Match(Some(input.into())), - Self::MemberRef(_) => return TokenMatchResult::Match(Some(input.into())), - Self::MemberPrivacyTarget(_) if MEMBER_PRIVACY_TARGETS.contains(&input) => { - return TokenMatchResult::Match(Some(input.into())) - } - Self::MemberPrivacyTarget(_) => {} - Self::PrivacyLevel(_) if input == "public" || input == "private" => { - return TokenMatchResult::Match(Some(input.into())) - } - Self::PrivacyLevel(_) => {} - } - // note: must not add a _ case to the above match - // instead, for conditional matches, also add generic cases with no return - - return TokenMatchResult::NoMatch; + fn new_match_param(raw: impl Into, param_name: ParamName, param: Parameter) -> Self { + Self::Match(Some(TokenMatchedValue { + raw: raw.into(), + param: Some((param_name, param)), + })) } } +impl Token { + pub fn try_match(&self, input: Option) -> TokenMatchResult { + use TokenMatchResult::*; + + let input = match input { + Some(input) => input, + None => { + // short circuit on: + return match self { + // empty token + Self::Empty => Match(None), + // missing paramaters + Self::FullString(param_name) + | Self::MemberRef(param_name) + | Self::MemberPrivacyTarget(param_name) + | Self::SystemRef(param_name) + | Self::PrivacyLevel(param_name) + | Self::Toggle(param_name) + | Self::Reset(param_name) => MissingParameter { name: param_name }, + Self::Any(tokens) => tokens.is_empty().then_some(NoMatch).unwrap_or_else(|| { + let mut results = tokens.iter().map(|t| t.try_match(None)); + results.find(|r| !matches!(r, NoMatch)).unwrap_or(NoMatch) + }), + // everything else doesnt match if no input anyway + Token::Value(_) => NoMatch, + Token::Flag => NoMatch, + // don't add a _ match here! + }; + } + }; + let input = input.trim(); + + // try actually matching stuff + match self { + Self::Empty => NoMatch, + Self::Flag => unreachable!(), // matched upstream (dusk: i don't really like this tbh) + Self::Any(tokens) => tokens + .iter() + .map(|t| t.try_match(Some(input.into()))) + .find(|r| !matches!(r, NoMatch)) + .unwrap_or(NoMatch), + Self::Value(values) => values + .iter() + .any(|v| v.eq(input)) + .then(|| TokenMatchResult::new_match(input)) + .unwrap_or(NoMatch), + Self::FullString(param_name) => TokenMatchResult::new_match_param( + input, + param_name, + Parameter::OpaqueString { raw: input.into() }, + ), + Self::SystemRef(param_name) => TokenMatchResult::new_match_param( + input, + param_name, + Parameter::SystemRef { + system: input.into(), + }, + ), + Self::MemberRef(param_name) => TokenMatchResult::new_match_param( + input, + param_name, + Parameter::MemberRef { + member: input.into(), + }, + ), + Self::MemberPrivacyTarget(param_name) => match MemberPrivacyTarget::from_str(input) { + Ok(target) => TokenMatchResult::new_match_param( + input, + param_name, + Parameter::MemberPrivacyTarget { + target: target.as_ref().into(), + }, + ), + Err(_) => NoMatch, + }, + Self::PrivacyLevel(param_name) => match PrivacyLevel::from_str(input) { + Ok(level) => TokenMatchResult::new_match_param( + input, + param_name, + Parameter::PrivacyLevel { + level: level.as_ref().into(), + }, + ), + Err(_) => NoMatch, + }, + + Self::Toggle(param_name) => match Toggle::from_str(input) { + Ok(t) => TokenMatchResult::new_match_param( + input, + param_name, + Parameter::Toggle { toggle: t.0 }, + ), + Err(_) => NoMatch, + }, + Self::Reset(param_name) => match Reset::from_str(input) { + Ok(_) => TokenMatchResult::new_match_param(input, param_name, Parameter::Reset), + Err(_) => NoMatch, + }, + // don't add a _ match here! + } + } +} + +/// Convenience trait to convert types into [`Token`]s. pub trait ToToken { fn to_token(&self) -> Token; } @@ -109,3 +207,107 @@ impl ToToken for [&str] { Token::Value(self.into_iter().map(|s| s.to_smolstr()).collect()) } } + +impl ToToken for [Token] { + fn to_token(&self) -> Token { + Token::Any(self.into_iter().map(|s| s.clone()).collect()) + } +} + +#[derive(Debug, Clone, Eq, Hash, PartialEq)] +pub enum MemberPrivacyTarget { + Visibility, + Name, + // todo +} + +impl AsRef for MemberPrivacyTarget { + fn as_ref(&self) -> &str { + match self { + Self::Visibility => "visibility", + Self::Name => "name", + } + } +} + +impl FromStr for MemberPrivacyTarget { + // todo: figure out how to represent these errors best + type Err = (); + + fn from_str(s: &str) -> Result { + match s { + "visibility" => Ok(Self::Visibility), + "name" => Ok(Self::Name), + _ => Err(()), + } + } +} + +#[derive(Debug, Clone, Eq, Hash, PartialEq)] +pub enum PrivacyLevel { + Public, + Private, +} + +impl AsRef for PrivacyLevel { + fn as_ref(&self) -> &str { + match self { + Self::Public => "public", + Self::Private => "private", + } + } +} + +impl FromStr for PrivacyLevel { + type Err = (); // todo + + fn from_str(s: &str) -> Result { + match s { + "public" => Ok(Self::Public), + "private" => Ok(Self::Private), + _ => Err(()), + } + } +} + +#[derive(Debug, Clone, Eq, Hash, PartialEq)] +pub struct Toggle(bool); + +impl AsRef for Toggle { + fn as_ref(&self) -> &str { + // on / off better than others for docs and stuff? + self.0.then_some("on").unwrap_or("off") + } +} + +impl FromStr for Toggle { + type Err = (); + + fn from_str(s: &str) -> Result { + match s { + "on" | "yes" | "true" | "enable" | "enabled" => Ok(Self(true)), + "off" | "no" | "false" | "disable" | "disabled" => Ok(Self(false)), + _ => Err(()), + } + } +} + +#[derive(Debug, Clone, Eq, Hash, PartialEq)] +pub struct Reset; + +impl AsRef for Reset { + fn as_ref(&self) -> &str { + "reset" + } +} + +impl FromStr for Reset { + type Err = (); + + fn from_str(s: &str) -> Result { + match s { + "reset" | "clear" | "default" => Ok(Self), + _ => Err(()), + } + } +} From 4f7e9c22a1a9216d35a9bcecf1a160145cf2fb12 Mon Sep 17 00:00:00 2001 From: dusk Date: Wed, 8 Jan 2025 18:31:59 +0900 Subject: [PATCH 034/179] feat(commands): implement Display traits for Token and Command to have some basic 'doc gen', split Toggle into Enable and Disable --- crates/commands/Cargo.toml | 2 +- crates/commands/src/commands.rs | 14 ++ crates/commands/src/commands/config.rs | 7 +- crates/commands/src/commands/help.rs | 18 +-- crates/commands/src/commands/member.rs | 7 +- crates/commands/src/lib.rs | 17 ++- crates/commands/src/main.rs | 7 + crates/commands/src/token.rs | 169 ++++++++++++++++++++----- flake.nix | 2 +- 9 files changed, 187 insertions(+), 56 deletions(-) create mode 100644 crates/commands/src/main.rs diff --git a/crates/commands/Cargo.toml b/crates/commands/Cargo.toml index 04a68b9c..4983cb53 100644 --- a/crates/commands/Cargo.toml +++ b/crates/commands/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" edition = "2021" [lib] -crate-type = ["cdylib"] +crate-type = ["cdylib", "lib"] [dependencies] lazy_static = { workspace = true } diff --git a/crates/commands/src/commands.rs b/crates/commands/src/commands.rs index 692e0d50..68836497 100644 --- a/crates/commands/src/commands.rs +++ b/crates/commands/src/commands.rs @@ -18,6 +18,8 @@ pub mod server_config; pub mod switch; pub mod system; +use std::fmt::Display; + use smol_str::SmolStr; use crate::{ @@ -47,6 +49,18 @@ impl Command { } } +impl Display for Command { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + for (idx, token) in self.tokens.iter().enumerate() { + write!(f, "{}", token)?; + if idx < self.tokens.len() - 1 { + write!(f, " ")?; + } + } + write!(f, " - {}", self.help) + } +} + #[macro_export] macro_rules! command { ([$($v:expr),+], $cb:expr, $help:expr) => { diff --git a/crates/commands/src/commands/config.rs b/crates/commands/src/commands/config.rs index f75018be..db16e729 100644 --- a/crates/commands/src/commands/config.rs +++ b/crates/commands/src/commands/config.rs @@ -23,7 +23,12 @@ pub fn cmds() -> impl Iterator { "Shows the autoproxy timeout" ), command!( - [cfg, autoproxy, ["timeout", "tm"], [Toggle("toggle"), Reset("reset"), FullString("timeout")]], + [ + cfg, + autoproxy, + ["timeout", "tm"], + [Disable("toggle"), Reset("reset"), FullString("timeout")] + ], "cfg_ap_timeout_update", "Sets the autoproxy timeout" ), diff --git a/crates/commands/src/commands/help.rs b/crates/commands/src/commands/help.rs index 86ec0f97..f663ee68 100644 --- a/crates/commands/src/commands/help.rs +++ b/crates/commands/src/commands/help.rs @@ -3,21 +3,9 @@ use super::*; pub fn cmds() -> impl Iterator { let help = ["help", "h"]; [ - command!( - [help], - "help", - "Shows the help command" - ), - command!( - [help, "commands"], - "help_commands", - "help commands" - ), - command!( - [help, "proxy"], - "help_proxy", - "help proxy" - ), + command!([help], "help", "Shows the help command"), + command!([help, "commands"], "help_commands", "help commands"), + command!([help, "proxy"], "help_proxy", "help proxy"), ] .into_iter() } diff --git a/crates/commands/src/commands/member.rs b/crates/commands/src/commands/member.rs index 42c3e1d2..12b66155 100644 --- a/crates/commands/src/commands/member.rs +++ b/crates/commands/src/commands/member.rs @@ -30,7 +30,12 @@ pub fn cmds() -> impl Iterator { "Shows a member's description" ), command!( - [member, MemberRef("target"), description, FullString("description")], + [ + member, + MemberRef("target"), + description, + FullString("description") + ], "member_desc_update", "Changes a member's description" ), diff --git a/crates/commands/src/lib.rs b/crates/commands/src/lib.rs index b0d417e1..989d2f9b 100644 --- a/crates/commands/src/lib.rs +++ b/crates/commands/src/lib.rs @@ -40,9 +40,9 @@ pub enum CommandResult { pub enum Parameter { MemberRef { member: String }, SystemRef { system: String }, - MemberPrivacyTarget { target: String }, - PrivacyLevel { level: String }, - OpaqueString { raw: String }, + MemberPrivacyTarget { target: String }, + PrivacyLevel { level: String }, + OpaqueString { raw: String }, Toggle { toggle: bool }, Reset, } @@ -109,6 +109,7 @@ fn parse_command(input: String) -> CommandResult { }, }; } + // todo: check if last token is a common incorrect unquote (multi-member names etc) // todo: check if this is a system name in pk;s command return CommandResult::Err { @@ -161,8 +162,14 @@ fn next_token( // for FullString just send the whole string let input_to_match = param.clone().map(|v| v.0); match token.try_match(input_to_match) { - TokenMatchResult::Match(value) => return Ok((token, value, param.map(|v| v.1).unwrap_or(current_pos))), - TokenMatchResult::MissingParameter { name } => return Err(Some(format_smolstr!("Missing parameter `{name}` in command `{input} [{name}]`."))), + TokenMatchResult::Match(value) => { + return Ok((token, value, param.map(|v| v.1).unwrap_or(current_pos))) + } + TokenMatchResult::MissingParameter { name } => { + return Err(Some(format_smolstr!( + "Missing parameter `{name}` in command `{input} [{name}]`." + ))) + } TokenMatchResult::NoMatch => {} } } diff --git a/crates/commands/src/main.rs b/crates/commands/src/main.rs new file mode 100644 index 00000000..aa8ce9e4 --- /dev/null +++ b/crates/commands/src/main.rs @@ -0,0 +1,7 @@ +use commands::commands as cmds; + +fn main() { + for command in cmds::all() { + println!("{}", command); + } +} diff --git a/crates/commands/src/token.rs b/crates/commands/src/token.rs index 2f82b207..fb4371ba 100644 --- a/crates/commands/src/token.rs +++ b/crates/commands/src/token.rs @@ -1,4 +1,4 @@ -use std::str::FromStr; +use std::{fmt::Display, ops::Not, str::FromStr}; use smol_str::{SmolStr, ToSmolStr}; @@ -33,6 +33,8 @@ pub enum Token { PrivacyLevel(ParamName), /// on, off; yes, no; true, false + Enable(ParamName), + Disable(ParamName), Toggle(ParamName), /// reset, clear, default @@ -100,11 +102,15 @@ impl Token { | Self::SystemRef(param_name) | Self::PrivacyLevel(param_name) | Self::Toggle(param_name) + | Self::Enable(param_name) + | Self::Disable(param_name) | Self::Reset(param_name) => MissingParameter { name: param_name }, - Self::Any(tokens) => tokens.is_empty().then_some(NoMatch).unwrap_or_else(|| { - let mut results = tokens.iter().map(|t| t.try_match(None)); - results.find(|r| !matches!(r, NoMatch)).unwrap_or(NoMatch) - }), + Self::Any(tokens) => { + tokens.is_empty().then_some(NoMatch).unwrap_or_else(|| { + let mut results = tokens.iter().map(|t| t.try_match(None)); + results.find(|r| !matches!(r, NoMatch)).unwrap_or(NoMatch) + }) + } // everything else doesnt match if no input anyway Token::Value(_) => NoMatch, Token::Flag => NoMatch, @@ -167,12 +173,30 @@ impl Token { ), Err(_) => NoMatch, }, - - Self::Toggle(param_name) => match Toggle::from_str(input) { + Self::Toggle(param_name) => match Enable::from_str(input) + .map(Into::::into) + .or_else(|_| Disable::from_str(input).map(Into::::into)) + { + Ok(toggle) => TokenMatchResult::new_match_param( + input, + param_name, + Parameter::Toggle { toggle }, + ), + Err(_) => NoMatch, + }, + Self::Enable(param_name) => match Enable::from_str(input) { Ok(t) => TokenMatchResult::new_match_param( input, param_name, - Parameter::Toggle { toggle: t.0 }, + Parameter::Toggle { toggle: t.into() }, + ), + Err(_) => NoMatch, + }, + Self::Disable(param_name) => match Disable::from_str(input) { + Ok(t) => TokenMatchResult::new_match_param( + input, + param_name, + Parameter::Toggle { toggle: t.into() }, ), Err(_) => NoMatch, }, @@ -185,6 +209,37 @@ impl Token { } } +impl Display for Token { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Token::Empty => write!(f, ""), + Token::Any(vec) => { + write!(f, "(")?; + for (i, token) in vec.iter().enumerate() { + if i != 0 { + write!(f, " | ")?; + } + write!(f, "{}", token)?; + } + write!(f, ")") + } + Token::Value(vec) if vec.is_empty().not() => write!(f, "{}", vec.first().unwrap()), + Token::Value(_) => Ok(()), // if value token has no values (lol), don't print anything + // todo: it might not be the best idea to directly use param name here (what if we want to display something else but keep the name? or translations?) + Token::FullString(param_name) => write!(f, "[{}]", param_name), + Token::MemberRef(param_name) => write!(f, "<{}>", param_name), + Token::SystemRef(param_name) => write!(f, "<{}>", param_name), + Token::MemberPrivacyTarget(param_name) => write!(f, "[{}]", param_name), + Token::PrivacyLevel(param_name) => write!(f, "[{}]", param_name), + Token::Enable(_) => write!(f, "on"), + Token::Disable(_) => write!(f, "off"), + Token::Toggle(_) => write!(f, "on/off"), + Token::Reset(_) => write!(f, "reset"), + Token::Flag => unreachable!("flag tokens should never be in command definitions"), + } + } +} + /// Convenience trait to convert types into [`Token`]s. pub trait ToToken { fn to_token(&self) -> Token; @@ -218,7 +273,13 @@ impl ToToken for [Token] { pub enum MemberPrivacyTarget { Visibility, Name, - // todo + Description, + Banner, + Avatar, + Birthday, + Pronouns, + Proxy, + Metadata, } impl AsRef for MemberPrivacyTarget { @@ -226,6 +287,13 @@ impl AsRef for MemberPrivacyTarget { match self { Self::Visibility => "visibility", Self::Name => "name", + Self::Description => "description", + Self::Banner => "banner", + Self::Avatar => "avatar", + Self::Birthday => "birthday", + Self::Pronouns => "pronouns", + Self::Proxy => "proxy", + Self::Metadata => "metadata", } } } @@ -235,9 +303,16 @@ impl FromStr for MemberPrivacyTarget { type Err = (); fn from_str(s: &str) -> Result { - match s { + match s.to_lowercase().as_str() { "visibility" => Ok(Self::Visibility), "name" => Ok(Self::Name), + "description" => Ok(Self::Description), + "banner" => Ok(Self::Banner), + "avatar" => Ok(Self::Avatar), + "birthday" => Ok(Self::Birthday), + "pronouns" => Ok(Self::Pronouns), + "proxy" => Ok(Self::Proxy), + "metadata" => Ok(Self::Metadata), _ => Err(()), } } @@ -270,28 +345,6 @@ impl FromStr for PrivacyLevel { } } -#[derive(Debug, Clone, Eq, Hash, PartialEq)] -pub struct Toggle(bool); - -impl AsRef for Toggle { - fn as_ref(&self) -> &str { - // on / off better than others for docs and stuff? - self.0.then_some("on").unwrap_or("off") - } -} - -impl FromStr for Toggle { - type Err = (); - - fn from_str(s: &str) -> Result { - match s { - "on" | "yes" | "true" | "enable" | "enabled" => Ok(Self(true)), - "off" | "no" | "false" | "disable" | "disabled" => Ok(Self(false)), - _ => Err(()), - } - } -} - #[derive(Debug, Clone, Eq, Hash, PartialEq)] pub struct Reset; @@ -311,3 +364,55 @@ impl FromStr for Reset { } } } + +#[derive(Debug, Clone, Eq, Hash, PartialEq)] +pub struct Enable; + +impl AsRef for Enable { + fn as_ref(&self) -> &str { + "on" + } +} + +impl FromStr for Enable { + type Err = (); + + fn from_str(s: &str) -> Result { + match s { + "on" | "yes" | "true" | "enable" | "enabled" => Ok(Self), + _ => Err(()), + } + } +} + +impl Into for Enable { + fn into(self) -> bool { + true + } +} + +#[derive(Debug, Clone, Eq, Hash, PartialEq)] +pub struct Disable; + +impl AsRef for Disable { + fn as_ref(&self) -> &str { + "off" + } +} + +impl FromStr for Disable { + type Err = (); + + fn from_str(s: &str) -> Result { + match s { + "off" | "no" | "false" | "disable" | "disabled" => Ok(Self), + _ => Err(()), + } + } +} + +impl Into for Disable { + fn into(self) -> bool { + false + } +} diff --git a/flake.nix b/flake.nix index 69b3d73a..3c043f57 100644 --- a/flake.nix +++ b/flake.nix @@ -90,7 +90,7 @@ set -x commandslib="''${1:-}" if [ "$commandslib" == "" ]; then - cargo -Z unstable-options build --package commands --release --artifact-dir obj/ + cargo -Z unstable-options build --package commands --lib --release --artifact-dir obj/ commandslib="obj/libcommands.so" else cp -f "$commandslib" obj/ From c43a8551843b69e96b6a63c4002e255f8974bfba Mon Sep 17 00:00:00 2001 From: dusk Date: Sat, 11 Jan 2025 19:49:16 +0900 Subject: [PATCH 035/179] feat(commands): make bin parse any commands passed to it --- crates/commands/src/lib.rs | 2 +- crates/commands/src/main.rs | 15 +++++++++++++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/crates/commands/src/lib.rs b/crates/commands/src/lib.rs index 989d2f9b..2944e222 100644 --- a/crates/commands/src/lib.rs +++ b/crates/commands/src/lib.rs @@ -55,7 +55,7 @@ pub struct ParsedCommand { pub flags: HashMap>, } -fn parse_command(input: String) -> CommandResult { +pub fn parse_command(input: String) -> CommandResult { let input: SmolStr = input.into(); let mut local_tree: TreeBranch = COMMAND_TREE.clone(); diff --git a/crates/commands/src/main.rs b/crates/commands/src/main.rs index aa8ce9e4..eb5748e5 100644 --- a/crates/commands/src/main.rs +++ b/crates/commands/src/main.rs @@ -1,7 +1,18 @@ +#![feature(iter_intersperse)] + use commands::commands as cmds; fn main() { - for command in cmds::all() { - println!("{}", command); + let cmd = std::env::args() + .skip(1) + .intersperse(" ".to_string()) + .collect::(); + if !cmd.is_empty() { + let parsed = commands::parse_command(cmd); + println!("{:#?}", parsed); + } else { + for command in cmds::all() { + println!("{}", command); + } } } From 3120e62dda0f2a2285b8ca3166f16ff0ce1145bc Mon Sep 17 00:00:00 2001 From: dusk Date: Sat, 11 Jan 2025 19:51:45 +0900 Subject: [PATCH 036/179] refactor(commands): move tree branch construct code into type method --- crates/commands/src/lib.rs | 6 +----- crates/commands/src/tree.rs | 7 +++++++ 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/crates/commands/src/lib.rs b/crates/commands/src/lib.rs index 2944e222..f7739cd2 100644 --- a/crates/commands/src/lib.rs +++ b/crates/commands/src/lib.rs @@ -10,7 +10,6 @@ uniffi::include_scaffolding!("commands"); use core::panic; use std::collections::HashMap; -use ordermap::OrderMap; use smol_str::{format_smolstr, SmolStr}; use tree::TreeBranch; @@ -19,10 +18,7 @@ pub use token::*; lazy_static::lazy_static! { pub static ref COMMAND_TREE: TreeBranch = { - let mut tree = TreeBranch { - current_command_key: None, - branches: OrderMap::new(), - }; + let mut tree = TreeBranch::empty(); crate::commands::all().into_iter().for_each(|x| tree.register_command(x)); diff --git a/crates/commands/src/tree.rs b/crates/commands/src/tree.rs index 77bd2f8c..27ee0791 100644 --- a/crates/commands/src/tree.rs +++ b/crates/commands/src/tree.rs @@ -10,6 +10,13 @@ pub struct TreeBranch { } impl TreeBranch { + pub fn empty() -> Self { + Self { + current_command_key: None, + branches: OrderMap::new(), + } + } + pub fn register_command(&mut self, command: Command) { let mut current_branch = self; // iterate over tokens in command From ee45fca6ab35b47f494304c970738c95a8f5d6b2 Mon Sep 17 00:00:00 2001 From: dusk Date: Sat, 11 Jan 2025 20:25:41 +0900 Subject: [PATCH 037/179] refactor(commands): make tree type properly hygienic --- crates/commands/src/lib.rs | 15 +++++++-------- crates/commands/src/tree.rs | 16 ++++++++++++++-- 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/crates/commands/src/lib.rs b/crates/commands/src/lib.rs index f7739cd2..24228f9b 100644 --- a/crates/commands/src/lib.rs +++ b/crates/commands/src/lib.rs @@ -63,12 +63,9 @@ pub fn parse_command(input: String) -> CommandResult { let mut flags: HashMap> = HashMap::new(); loop { - println!("possible: {:?}", local_tree.branches.keys()); - let next = next_token( - local_tree.branches.keys().cloned().collect(), - input.clone(), - current_pos, - ); + let possible_tokens = local_tree.possible_tokens().cloned().collect::>(); + println!("possible: {:?}", possible_tokens); + let next = next_token(possible_tokens.clone(), input.clone(), current_pos); println!("next: {:?}", next); match next { Ok((found_token, arg, new_pos)) => { @@ -87,14 +84,14 @@ pub fn parse_command(input: String) -> CommandResult { args.push(arg.raw.to_string()); } - if let Some(next_tree) = local_tree.branches.get(&found_token) { + if let Some(next_tree) = local_tree.get_branch(&found_token) { local_tree = next_tree.clone(); } else { panic!("found token could not match tree, at {input}"); } } Err(None) => { - if let Some(command_ref) = local_tree.current_command_key { + if let Some(command_ref) = local_tree.callback() { println!("{command_ref} {params:?}"); return CommandResult::Ok { command: ParsedCommand { @@ -106,6 +103,8 @@ pub fn parse_command(input: String) -> CommandResult { }; } + println!("{possible_tokens:?}"); + // todo: check if last token is a common incorrect unquote (multi-member names etc) // todo: check if this is a system name in pk;s command return CommandResult::Err { diff --git a/crates/commands/src/tree.rs b/crates/commands/src/tree.rs index 27ee0791..911c21f0 100644 --- a/crates/commands/src/tree.rs +++ b/crates/commands/src/tree.rs @@ -5,8 +5,8 @@ use crate::{commands::Command, Token}; #[derive(Debug, Clone)] pub struct TreeBranch { - pub current_command_key: Option, - pub branches: OrderMap, + current_command_key: Option, + branches: OrderMap, } impl TreeBranch { @@ -36,4 +36,16 @@ impl TreeBranch { }, ); } + + pub fn callback(&self) -> Option { + self.current_command_key.clone() + } + + pub fn possible_tokens(&self) -> impl Iterator { + self.branches.keys() + } + + pub fn get_branch(&self, token: &Token) -> Option<&TreeBranch> { + self.branches.get(token) + } } From f0d287b8731910e78a4cbef08d87f943e2e28501 Mon Sep 17 00:00:00 2001 From: dusk Date: Sat, 11 Jan 2025 22:38:29 +0900 Subject: [PATCH 038/179] feat(commands): show command suggestions if a command was not found --- PluralKit.Bot/CommandSystem/Parameters.cs | 4 +-- PluralKit.Bot/Handlers/MessageCreated.cs | 2 +- crates/commands/src/commands.rs | 2 +- crates/commands/src/commands.udl | 2 +- crates/commands/src/lib.rs | 30 +++++++++++++++------ crates/commands/src/main.rs | 8 ++++-- crates/commands/src/tree.rs | 33 +++++++++++++++++------ 7 files changed, 58 insertions(+), 23 deletions(-) diff --git a/PluralKit.Bot/CommandSystem/Parameters.cs b/PluralKit.Bot/CommandSystem/Parameters.cs index e5e0efd2..d9acf1c7 100644 --- a/PluralKit.Bot/CommandSystem/Parameters.cs +++ b/PluralKit.Bot/CommandSystem/Parameters.cs @@ -26,10 +26,10 @@ public class Parameters // just used for errors, temporarily public string FullCommand { get; init; } - public Parameters(string cmd) + public Parameters(string prefix, string cmd) { FullCommand = cmd; - var result = CommandsMethods.ParseCommand(cmd); + var result = CommandsMethods.ParseCommand(prefix, cmd); if (result is CommandResult.Ok) { var command = ((CommandResult.Ok)result).@command; diff --git a/PluralKit.Bot/Handlers/MessageCreated.cs b/PluralKit.Bot/Handlers/MessageCreated.cs index a2b34483..594b178a 100644 --- a/PluralKit.Bot/Handlers/MessageCreated.cs +++ b/PluralKit.Bot/Handlers/MessageCreated.cs @@ -142,7 +142,7 @@ public class MessageCreated: IEventHandler Parameters parameters; try { - parameters = new Parameters(evt.Content?.Substring(cmdStart)); + parameters = new Parameters(evt.Content?.Substring(0, cmdStart), evt.Content?.Substring(cmdStart)); } catch (PKError e) { diff --git a/crates/commands/src/commands.rs b/crates/commands/src/commands.rs index 68836497..8c2c5357 100644 --- a/crates/commands/src/commands.rs +++ b/crates/commands/src/commands.rs @@ -57,7 +57,7 @@ impl Display for Command { write!(f, " ")?; } } - write!(f, " - {}", self.help) + Ok(()) } } diff --git a/crates/commands/src/commands.udl b/crates/commands/src/commands.udl index 9eb372a3..53bf8dbd 100644 --- a/crates/commands/src/commands.udl +++ b/crates/commands/src/commands.udl @@ -1,5 +1,5 @@ namespace commands { - CommandResult parse_command(string input); + CommandResult parse_command(string prefix, string input); }; [Enum] interface CommandResult { diff --git a/crates/commands/src/lib.rs b/crates/commands/src/lib.rs index 24228f9b..64bcf33a 100644 --- a/crates/commands/src/lib.rs +++ b/crates/commands/src/lib.rs @@ -9,6 +9,7 @@ uniffi::include_scaffolding!("commands"); use core::panic; use std::collections::HashMap; +use std::fmt::Write; use smol_str::{format_smolstr, SmolStr}; use tree::TreeBranch; @@ -51,7 +52,7 @@ pub struct ParsedCommand { pub flags: HashMap>, } -pub fn parse_command(input: String) -> CommandResult { +pub fn parse_command(prefix: String, input: String) -> CommandResult { let input: SmolStr = input.into(); let mut local_tree: TreeBranch = COMMAND_TREE.clone(); @@ -91,11 +92,11 @@ pub fn parse_command(input: String) -> CommandResult { } } Err(None) => { - if let Some(command_ref) = local_tree.callback() { - println!("{command_ref} {params:?}"); + if let Some(command) = local_tree.command() { + println!("{} {params:?}", command.cb); return CommandResult::Ok { command: ParsedCommand { - command_ref: command_ref.into(), + command_ref: command.cb.into(), params, args, flags, @@ -103,13 +104,26 @@ pub fn parse_command(input: String) -> CommandResult { }; } - println!("{possible_tokens:?}"); + let mut error = format!("Unknown command `{prefix}{input}`."); + + let possible_commands = local_tree.possible_commands(2); + if !possible_commands.is_empty() { + error.push_str(" Perhaps you meant to use one of the commands below:\n"); + for command in possible_commands { + writeln!(&mut error, "- **{prefix}{command}** - *{}*", command.help) + .expect("oom"); + } + } else { + error.push_str("\n"); + } + + error.push_str( + "For a list of possible commands, see .", + ); // todo: check if last token is a common incorrect unquote (multi-member names etc) // todo: check if this is a system name in pk;s command - return CommandResult::Err { - error: format!("Unknown command `{input}`. For a list of possible commands, see ."), - }; + return CommandResult::Err { error }; } Err(Some(short_circuit)) => { return CommandResult::Err { diff --git a/crates/commands/src/main.rs b/crates/commands/src/main.rs index eb5748e5..110915ea 100644 --- a/crates/commands/src/main.rs +++ b/crates/commands/src/main.rs @@ -8,8 +8,12 @@ fn main() { .intersperse(" ".to_string()) .collect::(); if !cmd.is_empty() { - let parsed = commands::parse_command(cmd); - println!("{:#?}", parsed); + use commands::CommandResult; + let parsed = commands::parse_command("pk;".to_string(), cmd); + match parsed { + CommandResult::Ok { command } => println!("{command:#?}"), + CommandResult::Err { error } => println!("{error}"), + } } else { for command in cmds::all() { println!("{}", command); diff --git a/crates/commands/src/tree.rs b/crates/commands/src/tree.rs index 911c21f0..520208df 100644 --- a/crates/commands/src/tree.rs +++ b/crates/commands/src/tree.rs @@ -1,18 +1,17 @@ use ordermap::OrderMap; -use smol_str::SmolStr; use crate::{commands::Command, Token}; #[derive(Debug, Clone)] pub struct TreeBranch { - current_command_key: Option, + current_command: Option, branches: OrderMap, } impl TreeBranch { pub fn empty() -> Self { Self { - current_command_key: None, + current_command: None, branches: OrderMap::new(), } } @@ -20,10 +19,10 @@ impl TreeBranch { pub fn register_command(&mut self, command: Command) { let mut current_branch = self; // iterate over tokens in command - for token in command.tokens { + for token in command.tokens.clone() { // recursively get or create a sub-branch for each token current_branch = current_branch.branches.entry(token).or_insert(TreeBranch { - current_command_key: None, + current_command: None, branches: OrderMap::new(), }) } @@ -31,20 +30,38 @@ impl TreeBranch { current_branch.branches.insert( Token::Empty, TreeBranch { - current_command_key: Some(command.cb), + current_command: Some(command), branches: OrderMap::new(), }, ); } - pub fn callback(&self) -> Option { - self.current_command_key.clone() + pub fn command(&self) -> Option { + self.current_command.clone() } pub fn possible_tokens(&self) -> impl Iterator { self.branches.keys() } + pub fn possible_commands(&self, max_depth: usize) -> Vec { + if max_depth == 0 { + return Vec::new(); + } + let mut commands = Vec::new(); + for token in self.possible_tokens() { + if let Some(tree) = self.get_branch(token) { + if let Some(command) = tree.command() { + commands.push(command); + // we dont need to look further if we found a command + continue; + } + commands.append(&mut tree.possible_commands(max_depth - 1)); + } + } + commands + } + pub fn get_branch(&self, token: &Token) -> Option<&TreeBranch> { self.branches.get(token) } From 58f07c3baae6236621135641ce3a066d74da4447 Mon Sep 17 00:00:00 2001 From: dusk Date: Sat, 11 Jan 2025 23:05:29 +0900 Subject: [PATCH 039/179] feat(commands): allow commands to not be suggested --- crates/commands/src/commands.rs | 8 +++++++- crates/commands/src/commands/member.rs | 11 ++++++----- crates/commands/src/lib.rs | 1 + 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/crates/commands/src/commands.rs b/crates/commands/src/commands.rs index 8c2c5357..d9ab69b7 100644 --- a/crates/commands/src/commands.rs +++ b/crates/commands/src/commands.rs @@ -33,6 +33,7 @@ pub struct Command { pub tokens: Vec, pub help: SmolStr, pub cb: SmolStr, + pub show_in_suggestions: bool, } impl Command { @@ -40,11 +41,13 @@ impl Command { tokens: impl IntoIterator, help: impl Into, cb: impl Into, + show_in_suggestions: bool, ) -> Self { Self { tokens: tokens.into_iter().collect(), help: help.into(), cb: cb.into(), + show_in_suggestions, } } } @@ -63,8 +66,11 @@ impl Display for Command { #[macro_export] macro_rules! command { + ([$($v:expr),+], $cb:expr, $help:expr, suggest = $suggest:expr) => { + $crate::commands::Command::new([$($v.to_token()),*], $help, $cb, $suggest) + }; ([$($v:expr),+], $cb:expr, $help:expr) => { - $crate::commands::Command::new([$($v.to_token()),*], $help, $cb) + $crate::command!([$($v),+], $cb, $help, suggest = true) }; } diff --git a/crates/commands/src/commands/member.rs b/crates/commands/src/commands/member.rs index 12b66155..534c7417 100644 --- a/crates/commands/src/commands/member.rs +++ b/crates/commands/src/commands/member.rs @@ -19,11 +19,6 @@ pub fn cmds() -> impl Iterator { "member_show", "Shows information about a member" ), - command!( - [member, MemberRef("target"), "soulscream"], - "member_soulscream", - "todo" - ), command!( [member, MemberRef("target"), description], "member_desc_show", @@ -55,6 +50,12 @@ pub fn cmds() -> impl Iterator { "member_privacy_update", "Changes a member's privacy settings" ), + command!( + [member, MemberRef("target"), "soulscream"], + "member_soulscream", + "todo", + suggest = false + ), ] .into_iter() } diff --git a/crates/commands/src/lib.rs b/crates/commands/src/lib.rs index 64bcf33a..22a1e6a4 100644 --- a/crates/commands/src/lib.rs +++ b/crates/commands/src/lib.rs @@ -110,6 +110,7 @@ pub fn parse_command(prefix: String, input: String) -> CommandResult { if !possible_commands.is_empty() { error.push_str(" Perhaps you meant to use one of the commands below:\n"); for command in possible_commands { + if !command.show_in_suggestions { continue } writeln!(&mut error, "- **{prefix}{command}** - *{}*", command.help) .expect("oom"); } From 4cf17263d11ae0416fb2fd8a72e30b7fd458537d Mon Sep 17 00:00:00 2001 From: dusk Date: Sat, 11 Jan 2025 23:11:15 +0900 Subject: [PATCH 040/179] feat(commands): only show max N amount of suggestions --- crates/commands/src/lib.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/commands/src/lib.rs b/crates/commands/src/lib.rs index 22a1e6a4..b82813ed 100644 --- a/crates/commands/src/lib.rs +++ b/crates/commands/src/lib.rs @@ -17,6 +17,9 @@ use tree::TreeBranch; pub use commands::Command; pub use token::*; +// todo: this should come from the bot probably +const MAX_SUGGESTIONS: usize = 7; + lazy_static::lazy_static! { pub static ref COMMAND_TREE: TreeBranch = { let mut tree = TreeBranch::empty(); @@ -109,7 +112,7 @@ pub fn parse_command(prefix: String, input: String) -> CommandResult { let possible_commands = local_tree.possible_commands(2); if !possible_commands.is_empty() { error.push_str(" Perhaps you meant to use one of the commands below:\n"); - for command in possible_commands { + for command in possible_commands.iter().take(MAX_SUGGESTIONS) { if !command.show_in_suggestions { continue } writeln!(&mut error, "- **{prefix}{command}** - *{}*", command.help) .expect("oom"); From 319a79d1d6b582f6694e460cfc1dc531bcc4a05d Mon Sep 17 00:00:00 2001 From: dusk Date: Sat, 11 Jan 2025 23:19:50 +0900 Subject: [PATCH 041/179] fix(commands): prefix in missing parameter error --- crates/commands/src/lib.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/crates/commands/src/lib.rs b/crates/commands/src/lib.rs index b82813ed..b1e235c1 100644 --- a/crates/commands/src/lib.rs +++ b/crates/commands/src/lib.rs @@ -69,7 +69,7 @@ pub fn parse_command(prefix: String, input: String) -> CommandResult { loop { let possible_tokens = local_tree.possible_tokens().cloned().collect::>(); println!("possible: {:?}", possible_tokens); - let next = next_token(possible_tokens.clone(), input.clone(), current_pos); + let next = next_token(possible_tokens.clone(), &prefix, input.clone(), current_pos); println!("next: {:?}", next); match next { Ok((found_token, arg, new_pos)) => { @@ -113,7 +113,9 @@ pub fn parse_command(prefix: String, input: String) -> CommandResult { if !possible_commands.is_empty() { error.push_str(" Perhaps you meant to use one of the commands below:\n"); for command in possible_commands.iter().take(MAX_SUGGESTIONS) { - if !command.show_in_suggestions { continue } + if !command.show_in_suggestions { + continue; + } writeln!(&mut error, "- **{prefix}{command}** - *{}*", command.help) .expect("oom"); } @@ -147,6 +149,7 @@ pub fn parse_command(prefix: String, input: String) -> CommandResult { /// - optionally a short-circuit error fn next_token( possible_tokens: Vec, + prefix: &str, input: SmolStr, current_pos: usize, ) -> Result<(Token, Option, usize), Option> { @@ -180,7 +183,7 @@ fn next_token( } TokenMatchResult::MissingParameter { name } => { return Err(Some(format_smolstr!( - "Missing parameter `{name}` in command `{input} [{name}]`." + "Missing parameter `{name}` in command `{prefix}{input} [{name}]`." ))) } TokenMatchResult::NoMatch => {} From 877592588ce667005ef5710205060f3ab7e9a094 Mon Sep 17 00:00:00 2001 From: dusk Date: Sat, 11 Jan 2025 23:22:00 +0900 Subject: [PATCH 042/179] fix(commands): use token for display in missing param error instead of using param name directly --- crates/commands/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/commands/src/lib.rs b/crates/commands/src/lib.rs index b1e235c1..8b77b83e 100644 --- a/crates/commands/src/lib.rs +++ b/crates/commands/src/lib.rs @@ -183,7 +183,7 @@ fn next_token( } TokenMatchResult::MissingParameter { name } => { return Err(Some(format_smolstr!( - "Missing parameter `{name}` in command `{prefix}{input} [{name}]`." + "Missing parameter `{name}` in command `{prefix}{input} {token}`." ))) } TokenMatchResult::NoMatch => {} From d5c271be200c7487eabcb068ba86cf1dfa68f135 Mon Sep 17 00:00:00 2001 From: dusk Date: Sun, 12 Jan 2025 04:23:46 +0900 Subject: [PATCH 043/179] refactor(commands): clearer token match typing, make tree.possible_commads return iterator instead of traversing the whole tree immediately --- crates/commands/src/commands.rs | 4 +- crates/commands/src/lib.rs | 88 ++++++++++++++++----------- crates/commands/src/token.rs | 104 ++++++++++++++++---------------- crates/commands/src/tree.rs | 31 ++++++---- 4 files changed, 125 insertions(+), 102 deletions(-) diff --git a/crates/commands/src/commands.rs b/crates/commands/src/commands.rs index d9ab69b7..04ada503 100644 --- a/crates/commands/src/commands.rs +++ b/crates/commands/src/commands.rs @@ -18,7 +18,7 @@ pub mod server_config; pub mod switch; pub mod system; -use std::fmt::Display; +use std::fmt::{Debug, Display}; use smol_str::SmolStr; @@ -27,7 +27,7 @@ use crate::{ token::{ToToken, Token}, }; -#[derive(Clone, Debug)] +#[derive(Debug, Clone)] pub struct Command { // TODO: fix hygiene pub tokens: Vec, diff --git a/crates/commands/src/lib.rs b/crates/commands/src/lib.rs index 8b77b83e..fd1e794a 100644 --- a/crates/commands/src/lib.rs +++ b/crates/commands/src/lib.rs @@ -1,4 +1,5 @@ #![feature(let_chains)] +#![feature(anonymous_lifetime_in_impl_trait)] pub mod commands; mod string; @@ -10,8 +11,9 @@ uniffi::include_scaffolding!("commands"); use core::panic; use std::collections::HashMap; use std::fmt::Write; +use std::ops::Not; -use smol_str::{format_smolstr, SmolStr}; +use smol_str::SmolStr; use tree::TreeBranch; pub use commands::Command; @@ -69,10 +71,10 @@ pub fn parse_command(prefix: String, input: String) -> CommandResult { loop { let possible_tokens = local_tree.possible_tokens().cloned().collect::>(); println!("possible: {:?}", possible_tokens); - let next = next_token(possible_tokens.clone(), &prefix, input.clone(), current_pos); + let next = next_token(possible_tokens.clone(), input.clone(), current_pos); println!("next: {:?}", next); match next { - Ok((found_token, arg, new_pos)) => { + Some(Ok((found_token, arg, new_pos))) => { current_pos = new_pos; if let Token::Flag = found_token { flags.insert(arg.unwrap().raw.into(), None); @@ -94,7 +96,15 @@ pub fn parse_command(prefix: String, input: String) -> CommandResult { panic!("found token could not match tree, at {input}"); } } - Err(None) => { + Some(Err((token, err))) => { + let error_msg = match err { + TokenMatchError::MissingParameter { name } => { + format!("Expected parameter `{name}` in command `{prefix}{input} {token}`.") + } + }; + return CommandResult::Err { error: error_msg }; + } + None => { if let Some(command) = local_tree.command() { println!("{} {params:?}", command.cb); return CommandResult::Ok { @@ -109,33 +119,19 @@ pub fn parse_command(prefix: String, input: String) -> CommandResult { let mut error = format!("Unknown command `{prefix}{input}`."); - let possible_commands = local_tree.possible_commands(2); - if !possible_commands.is_empty() { - error.push_str(" Perhaps you meant to use one of the commands below:\n"); - for command in possible_commands.iter().take(MAX_SUGGESTIONS) { - if !command.show_in_suggestions { - continue; - } - writeln!(&mut error, "- **{prefix}{command}** - *{}*", command.help) - .expect("oom"); - } - } else { - error.push_str("\n"); + if fmt_possible_commands(&mut error, &prefix, local_tree.possible_commands(2)).not() + { + error.push_str(" "); } error.push_str( - "For a list of possible commands, see .", + "For a list of all possible commands, see .", ); // todo: check if last token is a common incorrect unquote (multi-member names etc) // todo: check if this is a system name in pk;s command return CommandResult::Err { error }; } - Err(Some(short_circuit)) => { - return CommandResult::Err { - error: short_circuit.into(), - }; - } } } } @@ -143,16 +139,16 @@ pub fn parse_command(prefix: String, input: String) -> CommandResult { /// Find the next token from an either raw or partially parsed command string /// /// Returns: +/// - nothing (none matched) /// - matched token, to move deeper into the tree /// - matched value (if this command matched an user-provided value such as a member name) /// - end position of matched token -/// - optionally a short-circuit error +/// - error when matching fn next_token( possible_tokens: Vec, - prefix: &str, input: SmolStr, current_pos: usize, -) -> Result<(Token, Option, usize), Option> { +) -> Option, usize), (Token, TokenMatchError)>> { // get next parameter, matching quotes let param = crate::string::next_param(input.clone(), current_pos); println!("matched: {param:?}\n---"); @@ -163,14 +159,14 @@ fn next_token( if let Some((value, new_pos)) = param.clone() && value.starts_with('-') { - return Ok(( + return Some(Ok(( Token::Flag, Some(TokenMatchedValue { raw: value, param: None, }), new_pos, - )); + ))); } // iterate over tokens and run try_match @@ -178,17 +174,39 @@ fn next_token( // for FullString just send the whole string let input_to_match = param.clone().map(|v| v.0); match token.try_match(input_to_match) { - TokenMatchResult::Match(value) => { - return Ok((token, value, param.map(|v| v.1).unwrap_or(current_pos))) - } - TokenMatchResult::MissingParameter { name } => { - return Err(Some(format_smolstr!( - "Missing parameter `{name}` in command `{prefix}{input} {token}`." + Some(Ok(value)) => { + return Some(Ok(( + token, + value, + param.map(|v| v.1).unwrap_or(current_pos), ))) } - TokenMatchResult::NoMatch => {} + Some(Err(err)) => { + return Some(Err((token, err))); + } + None => {} // continue matching until we exhaust all tokens } } - Err(None) + None +} + +// todo: should probably move this somewhere else +/// returns true if wrote possible commands, false if not +fn fmt_possible_commands( + f: &mut String, + prefix: &str, + mut possible_commands: impl Iterator, +) -> bool { + if let Some(first) = possible_commands.next() { + f.push_str(" Perhaps you meant to use one of the commands below:\n"); + for command in std::iter::once(first).chain(possible_commands.take(MAX_SUGGESTIONS - 1)) { + if !command.show_in_suggestions { + continue; + } + writeln!(f, "- **{prefix}{command}** - *{}*", command.help).expect("oom"); + } + return true; + } + return false; } diff --git a/crates/commands/src/token.rs b/crates/commands/src/token.rs index fb4371ba..f216fa4a 100644 --- a/crates/commands/src/token.rs +++ b/crates/commands/src/token.rs @@ -45,20 +45,8 @@ pub enum Token { Flag, } -// #[macro_export] -// macro_rules! any { -// ($($token:expr),+) => { -// Token::Any(vec![$($token.to_token()),+]) -// }; -// } - #[derive(Debug)] -pub enum TokenMatchResult { - /// Token did not match. - NoMatch, - /// Token matched, optionally with a value. - Match(Option), - /// A required parameter was missing. +pub enum TokenMatchError { MissingParameter { name: ParamName }, } @@ -68,33 +56,45 @@ pub struct TokenMatchedValue { pub param: Option<(ParamName, Parameter)>, } -impl TokenMatchResult { - fn new_match(raw: impl Into) -> Self { - Self::Match(Some(TokenMatchedValue { +impl TokenMatchedValue { + fn new_match(raw: impl Into) -> TryMatchResult { + Some(Ok(Some(Self { raw: raw.into(), param: None, - })) + }))) } - fn new_match_param(raw: impl Into, param_name: ParamName, param: Parameter) -> Self { - Self::Match(Some(TokenMatchedValue { + fn new_match_param( + raw: impl Into, + param_name: ParamName, + param: Parameter, + ) -> TryMatchResult { + Some(Ok(Some(Self { raw: raw.into(), param: Some((param_name, param)), - })) + }))) } } -impl Token { - pub fn try_match(&self, input: Option) -> TokenMatchResult { - use TokenMatchResult::*; +/// None -> no match +/// Some(Ok(None)) -> match, no value +/// Some(Ok(Some(_))) -> match, with value +/// Some(Err(_)) -> error while matching +// q: why do this while we could have a NoMatch in TokenMatchError? +// a: because we want to differentiate between no match and match failure (it matched with an error) +// "no match" has a different charecteristic because we want to continue matching other tokens... +// ...while "match failure" means we should stop matching and return the error +type TryMatchResult = Option, TokenMatchError>>; +impl Token { + pub fn try_match(&self, input: Option) -> TryMatchResult { let input = match input { Some(input) => input, None => { // short circuit on: return match self { // empty token - Self::Empty => Match(None), + Self::Empty => Some(Ok(None)), // missing paramaters Self::FullString(param_name) | Self::MemberRef(param_name) @@ -104,16 +104,16 @@ impl Token { | Self::Toggle(param_name) | Self::Enable(param_name) | Self::Disable(param_name) - | Self::Reset(param_name) => MissingParameter { name: param_name }, - Self::Any(tokens) => { - tokens.is_empty().then_some(NoMatch).unwrap_or_else(|| { - let mut results = tokens.iter().map(|t| t.try_match(None)); - results.find(|r| !matches!(r, NoMatch)).unwrap_or(NoMatch) - }) + | Self::Reset(param_name) => { + Some(Err(TokenMatchError::MissingParameter { name: param_name })) } + Self::Any(tokens) => tokens.is_empty().then_some(None).unwrap_or_else(|| { + let mut results = tokens.iter().map(|t| t.try_match(None)); + results.find(|r| !matches!(r, None)).unwrap_or(None) + }), // everything else doesnt match if no input anyway - Token::Value(_) => NoMatch, - Token::Flag => NoMatch, + Token::Value(_) => None, + Token::Flag => None, // don't add a _ match here! }; } @@ -122,31 +122,31 @@ impl Token { // try actually matching stuff match self { - Self::Empty => NoMatch, + Self::Empty => None, Self::Flag => unreachable!(), // matched upstream (dusk: i don't really like this tbh) Self::Any(tokens) => tokens .iter() .map(|t| t.try_match(Some(input.into()))) - .find(|r| !matches!(r, NoMatch)) - .unwrap_or(NoMatch), + .find(|r| !matches!(r, None)) + .unwrap_or(None), Self::Value(values) => values .iter() .any(|v| v.eq(input)) - .then(|| TokenMatchResult::new_match(input)) - .unwrap_or(NoMatch), - Self::FullString(param_name) => TokenMatchResult::new_match_param( + .then(|| TokenMatchedValue::new_match(input)) + .unwrap_or(None), + Self::FullString(param_name) => TokenMatchedValue::new_match_param( input, param_name, Parameter::OpaqueString { raw: input.into() }, ), - Self::SystemRef(param_name) => TokenMatchResult::new_match_param( + Self::SystemRef(param_name) => TokenMatchedValue::new_match_param( input, param_name, Parameter::SystemRef { system: input.into(), }, ), - Self::MemberRef(param_name) => TokenMatchResult::new_match_param( + Self::MemberRef(param_name) => TokenMatchedValue::new_match_param( input, param_name, Parameter::MemberRef { @@ -154,55 +154,55 @@ impl Token { }, ), Self::MemberPrivacyTarget(param_name) => match MemberPrivacyTarget::from_str(input) { - Ok(target) => TokenMatchResult::new_match_param( + Ok(target) => TokenMatchedValue::new_match_param( input, param_name, Parameter::MemberPrivacyTarget { target: target.as_ref().into(), }, ), - Err(_) => NoMatch, + Err(_) => None, }, Self::PrivacyLevel(param_name) => match PrivacyLevel::from_str(input) { - Ok(level) => TokenMatchResult::new_match_param( + Ok(level) => TokenMatchedValue::new_match_param( input, param_name, Parameter::PrivacyLevel { level: level.as_ref().into(), }, ), - Err(_) => NoMatch, + Err(_) => None, }, Self::Toggle(param_name) => match Enable::from_str(input) .map(Into::::into) .or_else(|_| Disable::from_str(input).map(Into::::into)) { - Ok(toggle) => TokenMatchResult::new_match_param( + Ok(toggle) => TokenMatchedValue::new_match_param( input, param_name, Parameter::Toggle { toggle }, ), - Err(_) => NoMatch, + Err(_) => None, }, Self::Enable(param_name) => match Enable::from_str(input) { - Ok(t) => TokenMatchResult::new_match_param( + Ok(t) => TokenMatchedValue::new_match_param( input, param_name, Parameter::Toggle { toggle: t.into() }, ), - Err(_) => NoMatch, + Err(_) => None, }, Self::Disable(param_name) => match Disable::from_str(input) { - Ok(t) => TokenMatchResult::new_match_param( + Ok(t) => TokenMatchedValue::new_match_param( input, param_name, Parameter::Toggle { toggle: t.into() }, ), - Err(_) => NoMatch, + Err(_) => None, }, Self::Reset(param_name) => match Reset::from_str(input) { - Ok(_) => TokenMatchResult::new_match_param(input, param_name, Parameter::Reset), - Err(_) => NoMatch, + Ok(_) => TokenMatchedValue::new_match_param(input, param_name, Parameter::Reset), + Err(_) => None, }, // don't add a _ match here! } diff --git a/crates/commands/src/tree.rs b/crates/commands/src/tree.rs index 520208df..f6810f3c 100644 --- a/crates/commands/src/tree.rs +++ b/crates/commands/src/tree.rs @@ -24,7 +24,7 @@ impl TreeBranch { current_branch = current_branch.branches.entry(token).or_insert(TreeBranch { current_command: None, branches: OrderMap::new(), - }) + }); } // when we're out of tokens, add an Empty branch with the callback and no sub-branches current_branch.branches.insert( @@ -44,20 +44,25 @@ impl TreeBranch { self.branches.keys() } - pub fn possible_commands(&self, max_depth: usize) -> Vec { - if max_depth == 0 { - return Vec::new(); + pub fn possible_commands(&self, max_depth: usize) -> impl Iterator { + // dusk: i am too lazy to write an iterator for this without using recursion so we box everything + fn box_iter<'a>( + iter: impl Iterator + 'a, + ) -> Box + 'a> { + Box::new(iter) } - let mut commands = Vec::new(); - for token in self.possible_tokens() { - if let Some(tree) = self.get_branch(token) { - if let Some(command) = tree.command() { - commands.push(command); - // we dont need to look further if we found a command - continue; - } - commands.append(&mut tree.possible_commands(max_depth - 1)); + + if max_depth == 0 { + return box_iter(std::iter::empty()); + } + let mut commands = box_iter(std::iter::empty()); + for branch in self.branches.values() { + if let Some(command) = branch.current_command.as_ref() { + commands = box_iter(commands.chain(std::iter::once(command))); + // we dont need to look further if we found a command (only Empty tokens have commands) + continue; } + commands = box_iter(commands.chain(branch.possible_commands(max_depth - 1))); } commands } From 8a01d7d37a130649baac618176fab453d40b925c Mon Sep 17 00:00:00 2001 From: dusk Date: Sun, 12 Jan 2025 04:27:02 +0900 Subject: [PATCH 044/179] style: format --- 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 7ce43b3d..edbc8428 100644 --- a/PluralKit.Bot/CommandMeta/CommandTree.cs +++ b/PluralKit.Bot/CommandMeta/CommandTree.cs @@ -22,7 +22,7 @@ public partial class CommandTree "cfg_ap_timeout_update" => ctx.Execute(null, m => m.EditAutoproxyTimeout(ctx)), "fun_thunder" => ctx.Execute(null, m => m.Thunder(ctx)), "fun_meow" => ctx.Execute(null, m => m.Meow(ctx)), - _ => + _ => // this should only ever occur when deving if commands are not implemented... ctx.Reply( $"{Emojis.Error} Parsed command {ctx.Parameters.Callback().AsCode()} not implemented in PluralKit.Bot!"), From 58b5a26ecac1bc53a219639156b6badf0e0b8089 Mon Sep 17 00:00:00 2001 From: dusk Date: Sun, 12 Jan 2025 05:35:04 +0900 Subject: [PATCH 045/179] fix(commands): add separate missing error for Token::Any to relay a better error message to user --- crates/commands/src/commands/config.rs | 2 +- crates/commands/src/lib.rs | 15 +++++++++++++++ crates/commands/src/token.rs | 6 ++++-- 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/crates/commands/src/commands/config.rs b/crates/commands/src/commands/config.rs index db16e729..d39024d5 100644 --- a/crates/commands/src/commands/config.rs +++ b/crates/commands/src/commands/config.rs @@ -27,7 +27,7 @@ pub fn cmds() -> impl Iterator { cfg, autoproxy, ["timeout", "tm"], - [Disable("toggle"), Reset("reset"), FullString("timeout")] + [Disable("toggle"), Reset("reset"), FullString("timeout")] // todo: we should parse duration / time values ], "cfg_ap_timeout_update", "Sets the autoproxy timeout" diff --git a/crates/commands/src/lib.rs b/crates/commands/src/lib.rs index fd1e794a..de6becb0 100644 --- a/crates/commands/src/lib.rs +++ b/crates/commands/src/lib.rs @@ -101,6 +101,21 @@ pub fn parse_command(prefix: String, input: String) -> CommandResult { TokenMatchError::MissingParameter { name } => { format!("Expected parameter `{name}` in command `{prefix}{input} {token}`.") } + TokenMatchError::MissingAny { tokens } => { + let mut msg = format!("Expected one of "); + for (idx, token) in tokens.iter().enumerate() { + write!(&mut msg, "`{token}`").expect("oom"); + if idx < tokens.len() - 1 { + if tokens.len() > 2 && idx == tokens.len() - 2 { + msg.push_str(" or "); + } else { + msg.push_str(", "); + } + } + } + write!(&mut msg, " in command `{prefix}{input} {token}`.").expect("oom"); + msg + } }; return CommandResult::Err { error: error_msg }; } diff --git a/crates/commands/src/token.rs b/crates/commands/src/token.rs index f216fa4a..e705a359 100644 --- a/crates/commands/src/token.rs +++ b/crates/commands/src/token.rs @@ -48,6 +48,7 @@ pub enum Token { #[derive(Debug)] pub enum TokenMatchError { MissingParameter { name: ParamName }, + MissingAny { tokens: Vec }, } #[derive(Debug)] @@ -108,8 +109,9 @@ impl Token { Some(Err(TokenMatchError::MissingParameter { name: param_name })) } Self::Any(tokens) => tokens.is_empty().then_some(None).unwrap_or_else(|| { - let mut results = tokens.iter().map(|t| t.try_match(None)); - results.find(|r| !matches!(r, None)).unwrap_or(None) + Some(Err(TokenMatchError::MissingAny { + tokens: tokens.clone(), + })) }), // everything else doesnt match if no input anyway Token::Value(_) => None, From a21210f3cecc1fdde5a2b70f5c7af8e40a167c6c Mon Sep 17 00:00:00 2001 From: dusk Date: Sun, 12 Jan 2025 05:55:25 +0900 Subject: [PATCH 046/179] refactor(commands): remove args from parse command as it is no longer used anymore --- PluralKit.Bot/CommandSystem/Parameters.cs | 2 -- crates/commands/src/commands.udl | 1 - crates/commands/src/lib.rs | 4 ---- 3 files changed, 7 deletions(-) diff --git a/PluralKit.Bot/CommandSystem/Parameters.cs b/PluralKit.Bot/CommandSystem/Parameters.cs index d9acf1c7..6455fdd3 100644 --- a/PluralKit.Bot/CommandSystem/Parameters.cs +++ b/PluralKit.Bot/CommandSystem/Parameters.cs @@ -19,7 +19,6 @@ public abstract record Parameter() public class Parameters { private string _cb { get; init; } - private List _args { get; init; } private Dictionary _flags { get; init; } private Dictionary _params { get; init; } @@ -34,7 +33,6 @@ public class Parameters { var command = ((CommandResult.Ok)result).@command; _cb = command.@commandRef; - _args = command.@args; _flags = command.@flags; _params = command.@params; } diff --git a/crates/commands/src/commands.udl b/crates/commands/src/commands.udl index 53bf8dbd..5080c4b9 100644 --- a/crates/commands/src/commands.udl +++ b/crates/commands/src/commands.udl @@ -18,7 +18,6 @@ interface Parameter { }; dictionary ParsedCommand { string command_ref; - sequence args; record params; record flags; }; diff --git a/crates/commands/src/lib.rs b/crates/commands/src/lib.rs index de6becb0..bef98461 100644 --- a/crates/commands/src/lib.rs +++ b/crates/commands/src/lib.rs @@ -52,7 +52,6 @@ pub enum Parameter { #[derive(Debug)] pub struct ParsedCommand { pub command_ref: String, - pub args: Vec, pub params: HashMap, pub flags: HashMap>, } @@ -64,7 +63,6 @@ pub fn parse_command(prefix: String, input: String) -> CommandResult { // end position of all currently matched tokens let mut current_pos = 0; - let mut args: Vec = Vec::new(); let mut params: HashMap = HashMap::new(); let mut flags: HashMap> = HashMap::new(); @@ -87,7 +85,6 @@ pub fn parse_command(prefix: String, input: String) -> CommandResult { if let Some((param_name, param)) = arg.param.as_ref() { params.insert(param_name.to_string(), param.clone()); } - args.push(arg.raw.to_string()); } if let Some(next_tree) = local_tree.get_branch(&found_token) { @@ -126,7 +123,6 @@ pub fn parse_command(prefix: String, input: String) -> CommandResult { command: ParsedCommand { command_ref: command.cb.into(), params, - args, flags, }, }; From 413b8c19155764b8a32c608e3cce98e216037629 Mon Sep 17 00:00:00 2001 From: dusk Date: Sun, 12 Jan 2025 19:30:21 +0900 Subject: [PATCH 047/179] fix(commands): make flags not match if param was in quotes --- crates/commands/src/lib.rs | 21 +++++++++++---------- crates/commands/src/string.rs | 31 +++++++++++++++++++++++++------ crates/commands/src/token.rs | 3 ++- 3 files changed, 38 insertions(+), 17 deletions(-) diff --git a/crates/commands/src/lib.rs b/crates/commands/src/lib.rs index bef98461..2f6aa4ff 100644 --- a/crates/commands/src/lib.rs +++ b/crates/commands/src/lib.rs @@ -69,7 +69,7 @@ pub fn parse_command(prefix: String, input: String) -> CommandResult { loop { let possible_tokens = local_tree.possible_tokens().cloned().collect::>(); println!("possible: {:?}", possible_tokens); - let next = next_token(possible_tokens.clone(), input.clone(), current_pos); + let next = next_token(possible_tokens.clone(), &input, current_pos); println!("next: {:?}", next); match next { Some(Ok((found_token, arg, new_pos))) => { @@ -157,39 +157,40 @@ pub fn parse_command(prefix: String, input: String) -> CommandResult { /// - error when matching fn next_token( possible_tokens: Vec, - input: SmolStr, + input: &str, current_pos: usize, ) -> Option, usize), (Token, TokenMatchError)>> { // get next parameter, matching quotes - let param = crate::string::next_param(input.clone(), current_pos); - println!("matched: {param:?}\n---"); + let matched = crate::string::next_param(&input, current_pos); + println!("matched: {matched:?}\n---"); // try checking if this is a flag // todo!: this breaks full text matching if the full text starts with a flag // (but that's kinda already broken anyway) - if let Some((value, new_pos)) = param.clone() - && value.starts_with('-') + if let Some(param) = matched.as_ref() + && param.in_quotes.not() + && param.value.starts_with('-') { return Some(Ok(( Token::Flag, Some(TokenMatchedValue { - raw: value, + raw: param.value.into(), param: None, }), - new_pos, + param.next_pos, ))); } // iterate over tokens and run try_match for token in possible_tokens { // for FullString just send the whole string - let input_to_match = param.clone().map(|v| v.0); + let input_to_match = matched.as_ref().map(|v| v.value); match token.try_match(input_to_match) { Some(Ok(value)) => { return Some(Ok(( token, value, - param.map(|v| v.1).unwrap_or(current_pos), + matched.map(|v| v.next_pos).unwrap_or(current_pos), ))) } Some(Err(err)) => { diff --git a/crates/commands/src/string.rs b/crates/commands/src/string.rs index e8a1ffb3..1ca2b2c4 100644 --- a/crates/commands/src/string.rs +++ b/crates/commands/src/string.rs @@ -42,17 +42,24 @@ lazy_static::lazy_static! { }; } +#[derive(Debug)] +pub(super) struct MatchedParam<'a> { + pub(super) value: &'a str, + pub(super) next_pos: usize, + pub(super) in_quotes: bool, +} + // very very simple quote matching // quotes need to be at start/end of words, and are ignored if a closing quote is not present // WTB POSIX quoting: https://pubs.opengroup.org/onlinepubs/9799919799/utilities/V3_chap02.html -pub(super) fn next_param(input: SmolStr, current_pos: usize) -> Option<(SmolStr, usize)> { +pub(super) fn next_param<'a>(input: &'a str, current_pos: usize) -> Option> { if input.len() == current_pos { return None; } let leading_whitespace_count = input[..current_pos].len() - input[..current_pos].trim_start().len(); - let substr_to_match: SmolStr = input[current_pos + leading_whitespace_count..].into(); + let substr_to_match = &input[current_pos + leading_whitespace_count..]; println!("stuff: {input} {current_pos} {leading_whitespace_count}"); println!("to match: {substr_to_match}"); @@ -68,20 +75,32 @@ pub(super) fn next_param(input: SmolStr, current_pos: usize) -> Option<(SmolStr, .is_whitespace() { // return quoted string, without quotes - return Some((substr_to_match[1..pos - 1].into(), current_pos + pos + 1)); + return Some(MatchedParam { + value: &substr_to_match[1..pos - 1], + next_pos: current_pos + pos + 1, + in_quotes: true, + }); } } } } // find next whitespace character - for (pos, char) in substr_to_match.clone().char_indices() { + for (pos, char) in substr_to_match.char_indices() { if char.is_whitespace() { - return Some((substr_to_match[..pos].into(), current_pos + pos + 1)); + return Some(MatchedParam { + value: &substr_to_match[..pos], + next_pos: current_pos + pos + 1, + in_quotes: false, + }); } } // if we're here, we went to EOF and didn't match any whitespace // so we return the whole string - Some((substr_to_match.clone(), current_pos + substr_to_match.len())) + Some(MatchedParam { + value: substr_to_match, + next_pos: current_pos + substr_to_match.len(), + in_quotes: false, + }) } diff --git a/crates/commands/src/token.rs b/crates/commands/src/token.rs index e705a359..bdc30013 100644 --- a/crates/commands/src/token.rs +++ b/crates/commands/src/token.rs @@ -88,7 +88,7 @@ impl TokenMatchedValue { type TryMatchResult = Option, TokenMatchError>>; impl Token { - pub fn try_match(&self, input: Option) -> TryMatchResult { + pub fn try_match(&self, input: Option<&str>) -> TryMatchResult { let input = match input { Some(input) => input, None => { @@ -305,6 +305,7 @@ impl FromStr for MemberPrivacyTarget { type Err = (); fn from_str(s: &str) -> Result { + // todo: this doesnt parse all the possible ways match s.to_lowercase().as_str() { "visibility" => Ok(Self::Visibility), "name" => Ok(Self::Name), From b020e0a8597fe0b7c1d6c486065b0f9df3a3d889 Mon Sep 17 00:00:00 2001 From: dusk Date: Sun, 12 Jan 2025 20:35:38 +0900 Subject: [PATCH 048/179] fix(commands): make full string actually match the rest of the input again --- crates/commands/src/lib.rs | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/crates/commands/src/lib.rs b/crates/commands/src/lib.rs index 2f6aa4ff..d62b71cf 100644 --- a/crates/commands/src/lib.rs +++ b/crates/commands/src/lib.rs @@ -183,15 +183,27 @@ fn next_token( // iterate over tokens and run try_match for token in possible_tokens { - // for FullString just send the whole string - let input_to_match = matched.as_ref().map(|v| v.value); + // check if this is a token that matches the rest of the input + let match_remaining = matches!(token, Token::FullString(_)); + // either use matched param or rest of the input if matching remaining + let input_to_match = matched.as_ref().map(|v| { + match_remaining + .then_some(&input[current_pos..]) + .unwrap_or(v.value) + }); match token.try_match(input_to_match) { Some(Ok(value)) => { - return Some(Ok(( - token, - value, - matched.map(|v| v.next_pos).unwrap_or(current_pos), - ))) + // return last possible pos if we matched remaining, + // otherwise use matched param next pos, + // and if didnt match anything we stay where we are + let next_pos = matched + .map(|v| { + match_remaining + .then_some(input.len()) + .unwrap_or(v.next_pos) + }) + .unwrap_or(current_pos); + return Some(Ok((token, value, next_pos))); } Some(Err(err)) => { return Some(Err((token, err))); From 816aa68b33c8b9c308986ad795f54196cd4f27d8 Mon Sep 17 00:00:00 2001 From: dusk Date: Sun, 12 Jan 2025 21:10:38 +0900 Subject: [PATCH 049/179] fix(commands): mark Any tokens as 'match remainder' if it has a 'match remainder' token in it --- crates/commands/src/lib.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/crates/commands/src/lib.rs b/crates/commands/src/lib.rs index d62b71cf..d96727a3 100644 --- a/crates/commands/src/lib.rs +++ b/crates/commands/src/lib.rs @@ -165,8 +165,8 @@ fn next_token( println!("matched: {matched:?}\n---"); // try checking if this is a flag - // todo!: this breaks full text matching if the full text starts with a flag - // (but that's kinda already broken anyway) + // note: if the param starts with - and if a "match remainder" token was going to be matched + // this is going to override that. to prevent that the param should be quoted if let Some(param) = matched.as_ref() && param.in_quotes.not() && param.value.starts_with('-') @@ -183,8 +183,12 @@ fn next_token( // iterate over tokens and run try_match for token in possible_tokens { + let is_match_remaining_token = |token: &Token| matches!(token, Token::FullString(_)); // check if this is a token that matches the rest of the input - let match_remaining = matches!(token, Token::FullString(_)); + let match_remaining = is_match_remaining_token(&token) + // check for Any here if it has a "match remainder" token in it + // if there is a "match remainder" token in a command there shouldn't be a command descending from that + || matches!(token, Token::Any(ref tokens) if tokens.iter().any(is_match_remaining_token)); // either use matched param or rest of the input if matching remaining let input_to_match = matched.as_ref().map(|v| { match_remaining @@ -197,11 +201,7 @@ fn next_token( // otherwise use matched param next pos, // and if didnt match anything we stay where we are let next_pos = matched - .map(|v| { - match_remaining - .then_some(input.len()) - .unwrap_or(v.next_pos) - }) + .map(|v| match_remaining.then_some(input.len()).unwrap_or(v.next_pos)) .unwrap_or(current_pos); return Some(Ok((token, value, next_pos))); } From 300539fdda6ad5e38214ec7f7945635f9b9378cd Mon Sep 17 00:00:00 2001 From: dusk Date: Tue, 14 Jan 2025 11:53:56 +0900 Subject: [PATCH 050/179] feat(commands): add typed flags, misplaced and non-applicable flags error reporting --- crates/commands/src/commands.rs | 77 ++++++++-- crates/commands/src/commands.udl | 2 +- crates/commands/src/commands/member.rs | 9 +- crates/commands/src/flag.rs | 102 +++++++++++++ crates/commands/src/lib.rs | 204 ++++++++++++++++++------- crates/commands/src/string.rs | 118 +++++++++++--- crates/commands/src/token.rs | 71 ++++----- crates/commands/src/tree.rs | 10 +- 8 files changed, 454 insertions(+), 139 deletions(-) create mode 100644 crates/commands/src/flag.rs diff --git a/crates/commands/src/commands.rs b/crates/commands/src/commands.rs index 04ada503..3326ca1b 100644 --- a/crates/commands/src/commands.rs +++ b/crates/commands/src/commands.rs @@ -24,16 +24,19 @@ use smol_str::SmolStr; use crate::{ command, - token::{ToToken, Token}, + flag::{Flag, FlagValue}, + token::Token, }; #[derive(Debug, Clone)] pub struct Command { // TODO: fix hygiene pub tokens: Vec, + pub flags: Vec, pub help: SmolStr, pub cb: SmolStr, pub show_in_suggestions: bool, + pub parse_flags_before: usize, } impl Command { @@ -41,36 +44,88 @@ impl Command { tokens: impl IntoIterator, help: impl Into, cb: impl Into, - show_in_suggestions: bool, ) -> Self { + let tokens = tokens.into_iter().collect::>(); + assert!(tokens.len() > 0); + let mut parse_flags_before = tokens.len(); + let mut was_parameter = true; + for (idx, token) in tokens.iter().enumerate().rev() { + match token { + Token::FullString(_) + | Token::MemberRef(_) + | Token::MemberPrivacyTarget(_) + | Token::SystemRef(_) + | Token::PrivacyLevel(_) + | Token::Toggle(_) + | Token::Enable(_) + | Token::Disable(_) + | Token::Reset(_) + | Token::Any(_) => { + parse_flags_before = idx; + was_parameter = true; + } + Token::Empty | Token::Value(_) => { + if was_parameter { + break; + } + } + } + } Self { - tokens: tokens.into_iter().collect(), + flags: Vec::new(), help: help.into(), cb: cb.into(), - show_in_suggestions, + show_in_suggestions: true, + parse_flags_before, + tokens, } } + + pub fn show_in_suggestions(mut self, v: bool) -> Self { + self.show_in_suggestions = v; + self + } + + pub fn flag(mut self, name: impl Into) -> Self { + self.flags.push(Flag::new(name)); + self + } + + pub fn value_flag(mut self, name: impl Into, value: FlagValue) -> Self { + self.flags.push(Flag::new(name).with_value(value)); + self + } } impl Display for Command { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { for (idx, token) in self.tokens.iter().enumerate() { - write!(f, "{}", token)?; - if idx < self.tokens.len() - 1 { - write!(f, " ")?; + if idx == self.parse_flags_before { + for flag in &self.flags { + write!(f, "[{flag}] ")?; + } + } + write!( + f, + "{token}{}", + (idx < self.tokens.len() - 1).then_some(" ").unwrap_or("") + )?; + } + if self.tokens.len() == self.parse_flags_before { + for flag in &self.flags { + write!(f, " [{flag}]")?; } } Ok(()) } } +// a macro is required because generic cant be different types at the same time (which means you couldnt have ["member", MemberRef, "subcmd"] etc) +// (and something like &dyn Trait would require everything to be referenced which doesnt look nice anyway) #[macro_export] macro_rules! command { - ([$($v:expr),+], $cb:expr, $help:expr, suggest = $suggest:expr) => { - $crate::commands::Command::new([$($v.to_token()),*], $help, $cb, $suggest) - }; ([$($v:expr),+], $cb:expr, $help:expr) => { - $crate::command!([$($v),+], $cb, $help, suggest = true) + $crate::commands::Command::new([$(Token::from($v)),*], $help, $cb) }; } diff --git a/crates/commands/src/commands.udl b/crates/commands/src/commands.udl index 5080c4b9..824ec5a5 100644 --- a/crates/commands/src/commands.udl +++ b/crates/commands/src/commands.udl @@ -19,5 +19,5 @@ interface Parameter { dictionary ParsedCommand { string command_ref; record params; - record flags; + record flags; }; diff --git a/crates/commands/src/commands/member.rs b/crates/commands/src/commands/member.rs index 534c7417..894e09a6 100644 --- a/crates/commands/src/commands/member.rs +++ b/crates/commands/src/commands/member.rs @@ -18,7 +18,8 @@ pub fn cmds() -> impl Iterator { [member, MemberRef("target")], "member_show", "Shows information about a member" - ), + ) + .value_flag("pt", FlagValue::OpaqueString), command!( [member, MemberRef("target"), description], "member_desc_show", @@ -53,9 +54,9 @@ pub fn cmds() -> impl Iterator { command!( [member, MemberRef("target"), "soulscream"], "member_soulscream", - "todo", - suggest = false - ), + "todo" + ) + .show_in_suggestions(false), ] .into_iter() } diff --git a/crates/commands/src/flag.rs b/crates/commands/src/flag.rs new file mode 100644 index 00000000..f9facb4e --- /dev/null +++ b/crates/commands/src/flag.rs @@ -0,0 +1,102 @@ +use std::fmt::Display; + +use smol_str::SmolStr; + +use crate::Parameter; + +#[derive(Debug, Clone)] +pub enum FlagValue { + OpaqueString, +} + +impl FlagValue { + fn try_match(&self, input: &str) -> Result { + if input.is_empty() { + return Err(FlagValueMatchError::ValueMissing); + } + + match self { + Self::OpaqueString => Ok(Parameter::OpaqueString { raw: input.into() }), + } + } +} + +impl Display for FlagValue { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + FlagValue::OpaqueString => write!(f, "value"), + } + } +} + +#[derive(Debug)] +pub enum FlagValueMatchError { + ValueMissing, +} + +#[derive(Debug, Clone)] +pub struct Flag { + name: SmolStr, + value: Option, +} + +impl Display for Flag { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "-{}", self.name)?; + if let Some(value) = self.value.as_ref() { + write!(f, "={value}")?; + } + Ok(()) + } +} + +#[derive(Debug)] +pub enum FlagMatchError { + ValueMatchFailed(FlagValueMatchError), +} + +type TryMatchFlagResult = Option, FlagMatchError>>; + +impl Flag { + pub fn new(name: impl Into) -> Self { + Self { + name: name.into(), + value: None, + } + } + + pub fn with_value(mut self, value: FlagValue) -> Self { + self.value = Some(value); + self + } + + pub fn name(&self) -> &str { + &self.name + } + + pub fn value(&self) -> Option<&FlagValue> { + self.value.as_ref() + } + + pub fn try_match(&self, input_name: &str, input_value: Option<&str>) -> TryMatchFlagResult { + // if not matching flag then skip anymore matching + if self.name != input_name { + return None; + } + // get token to try matching with, if flag doesn't have one then that means it is matched (it is without any value) + let Some(value) = self.value() else { + return Some(Ok(None)); + }; + // check if we have a non-empty flag value, we return error if not (because flag requested a value) + let Some(input_value) = input_value else { + return Some(Err(FlagMatchError::ValueMatchFailed( + FlagValueMatchError::ValueMissing, + ))); + }; + // try matching the value + match value.try_match(input_value) { + Ok(param) => Some(Ok(Some(param))), + Err(err) => Some(Err(FlagMatchError::ValueMatchFailed(err))), + } + } +} diff --git a/crates/commands/src/lib.rs b/crates/commands/src/lib.rs index d96727a3..422109ef 100644 --- a/crates/commands/src/lib.rs +++ b/crates/commands/src/lib.rs @@ -2,6 +2,7 @@ #![feature(anonymous_lifetime_in_impl_trait)] pub mod commands; +mod flag; mod string; mod token; mod tree; @@ -13,7 +14,9 @@ use std::collections::HashMap; use std::fmt::Write; use std::ops::Not; +use flag::{Flag, FlagMatchError, FlagValueMatchError}; use smol_str::SmolStr; +use string::MatchedFlag; use tree::TreeBranch; pub use commands::Command; @@ -53,7 +56,7 @@ pub enum Parameter { pub struct ParsedCommand { pub command_ref: String, pub params: HashMap, - pub flags: HashMap>, + pub flags: HashMap>, } pub fn parse_command(prefix: String, input: String) -> CommandResult { @@ -61,24 +64,23 @@ pub fn parse_command(prefix: String, input: String) -> CommandResult { let mut local_tree: TreeBranch = COMMAND_TREE.clone(); // end position of all currently matched tokens - let mut current_pos = 0; + let mut current_pos: usize = 0; + let mut current_token_idx: usize = 0; let mut params: HashMap = HashMap::new(); - let mut flags: HashMap> = HashMap::new(); + let mut raw_flags: Vec<(usize, MatchedFlag)> = Vec::new(); loop { - let possible_tokens = local_tree.possible_tokens().cloned().collect::>(); - println!("possible: {:?}", possible_tokens); - let next = next_token(possible_tokens.clone(), &input, current_pos); + println!( + "possible: {:?}", + local_tree.possible_tokens().collect::>() + ); + let next = next_token(local_tree.possible_tokens(), &input, current_pos); println!("next: {:?}", next); match next { Some(Ok((found_token, arg, new_pos))) => { current_pos = new_pos; - if let Token::Flag = found_token { - flags.insert(arg.unwrap().raw.into(), None); - // don't try matching flags as tree elements - continue; - } + current_token_idx += 1; if let Some(arg) = arg.as_ref() { // insert arg as paramater if this is a parameter @@ -117,17 +119,7 @@ pub fn parse_command(prefix: String, input: String) -> CommandResult { return CommandResult::Err { error: error_msg }; } None => { - if let Some(command) = local_tree.command() { - println!("{} {params:?}", command.cb); - return CommandResult::Ok { - command: ParsedCommand { - command_ref: command.cb.into(), - params, - flags, - }, - }; - } - + // if it said command not found on a flag, output better error message let mut error = format!("Unknown command `{prefix}{input}`."); if fmt_possible_commands(&mut error, &prefix, local_tree.possible_commands(2)).not() @@ -144,9 +136,127 @@ pub fn parse_command(prefix: String, input: String) -> CommandResult { return CommandResult::Err { error }; } } + // match flags until there are none left + while let Some(matched_flag) = string::next_flag(&input, current_pos) { + current_pos = matched_flag.next_pos; + println!("flag matched {matched_flag:?}"); + raw_flags.push((current_token_idx, matched_flag)); + } + // if we have a command, stop parsing and return it + if let Some(command) = local_tree.command() { + // match the flags against this commands flags + let mut flags: HashMap> = HashMap::new(); + let mut misplaced_flags: Vec = Vec::new(); + let mut invalid_flags: Vec = Vec::new(); + for (token_idx, matched_flag) in raw_flags { + if token_idx != command.parse_flags_before { + misplaced_flags.push(matched_flag); + continue; + } + let Some(matched_flag) = match_flag(command.flags.iter(), matched_flag.clone()) + else { + invalid_flags.push(matched_flag); + continue; + }; + match matched_flag { + // a flag was matched + Ok((name, value)) => { + flags.insert(name.into(), value); + } + Err((flag, err)) => { + match err { + FlagMatchError::ValueMatchFailed(FlagValueMatchError::ValueMissing) => { + return CommandResult::Err { + error: format!( + "Flag `-{name}` in command `{prefix}{input}` is missing a value, try passing `-{name}={value}`.", + name = flag.name(), + value = flag.value().expect("value missing error cant happen without a value"), + ), + } + } + } + } + } + } + if misplaced_flags.is_empty().not() { + let mut error = format!( + "Flag{} ", + (misplaced_flags.len() > 1).then_some("s").unwrap_or("") + ); + for (idx, matched_flag) in misplaced_flags.iter().enumerate() { + write!(&mut error, "`-{}`", matched_flag.name).expect("oom"); + if idx < misplaced_flags.len() - 1 { + error.push_str(", "); + } + } + write!( + &mut error, + " in command `{prefix}{input}` {} misplaced. Try reordering to match the command usage `{prefix}{command}`.", + (misplaced_flags.len() > 1).then_some("are").unwrap_or("is") + ).expect("oom"); + return CommandResult::Err { error }; + } + if invalid_flags.is_empty().not() { + let mut error = format!( + "Flag{} ", + (misplaced_flags.len() > 1).then_some("s").unwrap_or("") + ); + for (idx, matched_flag) in invalid_flags.iter().enumerate() { + write!(&mut error, "`-{}`", matched_flag.name).expect("oom"); + if idx < invalid_flags.len() - 1 { + error.push_str(", "); + } + } + write!( + &mut error, + " {} not applicable in this command (`{prefix}{input}`). Applicable flags are the following:", + (invalid_flags.len() > 1).then_some("are").unwrap_or("is") + ).expect("oom"); + for (idx, flag) in command.flags.iter().enumerate() { + write!(&mut error, " `{flag}`").expect("oom"); + if idx < command.flags.len() - 1 { + error.push_str(", "); + } + } + error.push_str("."); + return CommandResult::Err { error }; + } + println!("{} {flags:?} {params:?}", command.cb); + return CommandResult::Ok { + command: ParsedCommand { + command_ref: command.cb.into(), + params, + flags, + }, + }; + } } } +fn match_flag<'a>( + possible_flags: impl Iterator, + matched_flag: MatchedFlag<'a>, +) -> Option), (&'a Flag, FlagMatchError)>> { + // skip if 0 length (we could just take an array ref here and in next_token aswell but its nice to keep it flexible) + if let (_, Some(len)) = possible_flags.size_hint() + && len == 0 + { + return None; + } + + // check for all (possible) flags, see if token matches + for flag in possible_flags { + println!("matching flag {flag:?}"); + match flag.try_match(matched_flag.name, matched_flag.value) { + Some(Ok(param)) => return Some(Ok((flag.name().into(), param))), + Some(Err(err)) => return Some(Err((flag, err))), + None => {} + } + } + + None +} + /// Find the next token from an either raw or partially parsed command string /// /// Returns: @@ -155,37 +265,27 @@ pub fn parse_command(prefix: String, input: String) -> CommandResult { /// - matched value (if this command matched an user-provided value such as a member name) /// - end position of matched token /// - error when matching -fn next_token( - possible_tokens: Vec, +fn next_token<'a>( + possible_tokens: impl Iterator, input: &str, current_pos: usize, -) -> Option, usize), (Token, TokenMatchError)>> { - // get next parameter, matching quotes - let matched = crate::string::next_param(&input, current_pos); - println!("matched: {matched:?}\n---"); - - // try checking if this is a flag - // note: if the param starts with - and if a "match remainder" token was going to be matched - // this is going to override that. to prevent that the param should be quoted - if let Some(param) = matched.as_ref() - && param.in_quotes.not() - && param.value.starts_with('-') +) -> Option, usize), (&'a Token, TokenMatchError)>> { + // skip if 0 length + if let (_, Some(len)) = possible_tokens.size_hint() + && len == 0 { - return Some(Ok(( - Token::Flag, - Some(TokenMatchedValue { - raw: param.value.into(), - param: None, - }), - param.next_pos, - ))); + return None; } + // get next parameter, matching quotes + let matched = string::next_param(&input, current_pos); + println!("matched: {matched:?}\n---"); + // iterate over tokens and run try_match for token in possible_tokens { let is_match_remaining_token = |token: &Token| matches!(token, Token::FullString(_)); // check if this is a token that matches the rest of the input - let match_remaining = is_match_remaining_token(&token) + let match_remaining = is_match_remaining_token(token) // check for Any here if it has a "match remainder" token in it // if there is a "match remainder" token in a command there shouldn't be a command descending from that || matches!(token, Token::Any(ref tokens) if tokens.iter().any(is_match_remaining_token)); @@ -197,12 +297,14 @@ fn next_token( }); match token.try_match(input_to_match) { Some(Ok(value)) => { - // return last possible pos if we matched remaining, - // otherwise use matched param next pos, - // and if didnt match anything we stay where we are - let next_pos = matched - .map(|v| match_remaining.then_some(input.len()).unwrap_or(v.next_pos)) - .unwrap_or(current_pos); + let next_pos = match matched { + // return last possible pos if we matched remaining, + Some(_) if match_remaining => input.len(), + // otherwise use matched param next pos, + Some(param) => param.next_pos, + // and if didnt match anything we stay where we are + None => current_pos, + }; return Some(Ok((token, value, next_pos))); } Some(Err(err)) => { @@ -223,7 +325,7 @@ fn fmt_possible_commands( mut possible_commands: impl Iterator, ) -> bool { if let Some(first) = possible_commands.next() { - f.push_str(" Perhaps you meant to use one of the commands below:\n"); + f.push_str(" Perhaps you meant one of the following commands:\n"); for command in std::iter::once(first).chain(possible_commands.take(MAX_SUGGESTIONS - 1)) { if !command.show_in_suggestions { continue; diff --git a/crates/commands/src/string.rs b/crates/commands/src/string.rs index 1ca2b2c4..9b66da4c 100644 --- a/crates/commands/src/string.rs +++ b/crates/commands/src/string.rs @@ -42,16 +42,35 @@ lazy_static::lazy_static! { }; } +// very very simple quote matching +// expects match_str to be trimmed (no whitespace, from the start at least) +// returns the position of an end quote if any is found +// quotes need to be at start/end of words, and are ignored if a closing quote is not present +// WTB POSIX quoting: https://pubs.opengroup.org/onlinepubs/9799919799/utilities/V3_chap02.html +fn find_quotes(match_str: &str) -> Option { + if let Some(right) = QUOTE_PAIRS.get(&match_str[0..1]) { + // try matching end quote + for possible_quote in right.chars() { + for (pos, _) in match_str.match_indices(possible_quote) { + if match_str.len() == pos + 1 + || match_str.chars().nth(pos + 1).unwrap().is_whitespace() + { + return Some(pos); + } + } + } + } + None +} + #[derive(Debug)] pub(super) struct MatchedParam<'a> { pub(super) value: &'a str, pub(super) next_pos: usize, + #[allow(dead_code)] // this'll prolly be useful sometime later pub(super) in_quotes: bool, } -// very very simple quote matching -// quotes need to be at start/end of words, and are ignored if a closing quote is not present -// WTB POSIX quoting: https://pubs.opengroup.org/onlinepubs/9799919799/utilities/V3_chap02.html pub(super) fn next_param<'a>(input: &'a str, current_pos: usize) -> Option> { if input.len() == current_pos { return None; @@ -63,26 +82,13 @@ pub(super) fn next_param<'a>(input: &'a str, current_pos: usize) -> Option(input: &'a str, current_pos: usize) -> Option { + pub(super) name: &'a str, + pub(super) value: Option<&'a str>, + pub(super) next_pos: usize, +} + +pub(super) fn next_flag<'a>(input: &'a str, mut current_pos: usize) -> Option> { + if input.len() == current_pos { + return None; + } + + let leading_whitespace_count = + input[..current_pos].len() - input[..current_pos].trim_start().len(); + let substr_to_match = &input[current_pos + leading_whitespace_count..]; + + // if the param is quoted, it should not be processed as a flag + if find_quotes(substr_to_match).is_some() { + return None; + } + + println!("flag input {substr_to_match}"); + // strip the - + let Some(substr_to_match) = substr_to_match.strip_prefix('-') else { + // if it doesn't have one, then it is not a flag + return None; + }; + current_pos += 1; + + // try finding = or whitespace + for (pos, char) in substr_to_match.char_indices() { + println!("flag find char {char} at {pos}"); + if char == '=' { + let name = &substr_to_match[..pos]; + println!("flag find {name}"); + // try to get the value + let Some(param) = next_param(input, current_pos + pos + 1) else { + return Some(MatchedFlag { + name, + value: Some(""), + next_pos: current_pos + pos + 1, + }); + }; + return Some(MatchedFlag { + name, + value: Some(param.value), + next_pos: param.next_pos, + }); + } else if char.is_whitespace() { + // no value if whitespace + return Some(MatchedFlag { + name: &substr_to_match[..pos], + value: None, + next_pos: current_pos + pos + 1, + }); + } + } + + // if eof then no value + Some(MatchedFlag { + name: substr_to_match, + value: None, + next_pos: current_pos + substr_to_match.len(), + }) +} diff --git a/crates/commands/src/token.rs b/crates/commands/src/token.rs index bdc30013..594517a7 100644 --- a/crates/commands/src/token.rs +++ b/crates/commands/src/token.rs @@ -13,6 +13,7 @@ pub enum Token { Empty, /// multi-token matching + /// todo: FullString tokens don't work properly in this (they don't get passed the rest of the input) Any(Vec), /// A bot-defined command / subcommand (usually) (eg. "member" in `pk;member MyName`) @@ -39,10 +40,6 @@ pub enum Token { /// reset, clear, default Reset(ParamName), - - // todo: currently not included in command definitions - // todo: flags with values - Flag, } #[derive(Debug)] @@ -52,12 +49,12 @@ pub enum TokenMatchError { } #[derive(Debug)] -pub struct TokenMatchedValue { +pub struct TokenMatchValue { pub raw: SmolStr, pub param: Option<(ParamName, Parameter)>, } -impl TokenMatchedValue { +impl TokenMatchValue { fn new_match(raw: impl Into) -> TryMatchResult { Some(Ok(Some(Self { raw: raw.into(), @@ -85,7 +82,7 @@ impl TokenMatchedValue { // a: because we want to differentiate between no match and match failure (it matched with an error) // "no match" has a different charecteristic because we want to continue matching other tokens... // ...while "match failure" means we should stop matching and return the error -type TryMatchResult = Option, TokenMatchError>>; +type TryMatchResult = Option, TokenMatchError>>; impl Token { pub fn try_match(&self, input: Option<&str>) -> TryMatchResult { @@ -114,8 +111,7 @@ impl Token { })) }), // everything else doesnt match if no input anyway - Token::Value(_) => None, - Token::Flag => None, + Self::Value(_) => None, // don't add a _ match here! }; } @@ -125,30 +121,29 @@ impl Token { // try actually matching stuff match self { Self::Empty => None, - Self::Flag => unreachable!(), // matched upstream (dusk: i don't really like this tbh) Self::Any(tokens) => tokens .iter() - .map(|t| t.try_match(Some(input.into()))) + .map(|t| t.try_match(Some(input))) .find(|r| !matches!(r, None)) .unwrap_or(None), Self::Value(values) => values .iter() .any(|v| v.eq(input)) - .then(|| TokenMatchedValue::new_match(input)) + .then(|| TokenMatchValue::new_match(input)) .unwrap_or(None), - Self::FullString(param_name) => TokenMatchedValue::new_match_param( + Self::FullString(param_name) => TokenMatchValue::new_match_param( input, param_name, Parameter::OpaqueString { raw: input.into() }, ), - Self::SystemRef(param_name) => TokenMatchedValue::new_match_param( + Self::SystemRef(param_name) => TokenMatchValue::new_match_param( input, param_name, Parameter::SystemRef { system: input.into(), }, ), - Self::MemberRef(param_name) => TokenMatchedValue::new_match_param( + Self::MemberRef(param_name) => TokenMatchValue::new_match_param( input, param_name, Parameter::MemberRef { @@ -156,7 +151,7 @@ impl Token { }, ), Self::MemberPrivacyTarget(param_name) => match MemberPrivacyTarget::from_str(input) { - Ok(target) => TokenMatchedValue::new_match_param( + Ok(target) => TokenMatchValue::new_match_param( input, param_name, Parameter::MemberPrivacyTarget { @@ -166,7 +161,7 @@ impl Token { Err(_) => None, }, Self::PrivacyLevel(param_name) => match PrivacyLevel::from_str(input) { - Ok(level) => TokenMatchedValue::new_match_param( + Ok(level) => TokenMatchValue::new_match_param( input, param_name, Parameter::PrivacyLevel { @@ -179,7 +174,7 @@ impl Token { .map(Into::::into) .or_else(|_| Disable::from_str(input).map(Into::::into)) { - Ok(toggle) => TokenMatchedValue::new_match_param( + Ok(toggle) => TokenMatchValue::new_match_param( input, param_name, Parameter::Toggle { toggle }, @@ -187,7 +182,7 @@ impl Token { Err(_) => None, }, Self::Enable(param_name) => match Enable::from_str(input) { - Ok(t) => TokenMatchedValue::new_match_param( + Ok(t) => TokenMatchValue::new_match_param( input, param_name, Parameter::Toggle { toggle: t.into() }, @@ -195,7 +190,7 @@ impl Token { Err(_) => None, }, Self::Disable(param_name) => match Disable::from_str(input) { - Ok(t) => TokenMatchedValue::new_match_param( + Ok(t) => TokenMatchValue::new_match_param( input, param_name, Parameter::Toggle { toggle: t.into() }, @@ -203,7 +198,7 @@ impl Token { Err(_) => None, }, Self::Reset(param_name) => match Reset::from_str(input) { - Ok(_) => TokenMatchedValue::new_match_param(input, param_name, Parameter::Reset), + Ok(_) => TokenMatchValue::new_match_param(input, param_name, Parameter::Reset), Err(_) => None, }, // don't add a _ match here! @@ -219,7 +214,7 @@ impl Display for Token { write!(f, "(")?; for (i, token) in vec.iter().enumerate() { if i != 0 { - write!(f, " | ")?; + write!(f, "|")?; } write!(f, "{}", token)?; } @@ -231,43 +226,31 @@ impl Display for Token { Token::FullString(param_name) => write!(f, "[{}]", param_name), Token::MemberRef(param_name) => write!(f, "<{}>", param_name), Token::SystemRef(param_name) => write!(f, "<{}>", param_name), - Token::MemberPrivacyTarget(param_name) => write!(f, "[{}]", param_name), + Token::MemberPrivacyTarget(param_name) => write!(f, "<{}>", param_name), Token::PrivacyLevel(param_name) => write!(f, "[{}]", param_name), Token::Enable(_) => write!(f, "on"), Token::Disable(_) => write!(f, "off"), Token::Toggle(_) => write!(f, "on/off"), Token::Reset(_) => write!(f, "reset"), - Token::Flag => unreachable!("flag tokens should never be in command definitions"), } } } -/// Convenience trait to convert types into [`Token`]s. -pub trait ToToken { - fn to_token(&self) -> Token; -} - -impl ToToken for Token { - fn to_token(&self) -> Token { - self.clone() +impl From<&str> for Token { + fn from(value: &str) -> Self { + Token::Value(vec![value.to_smolstr()]) } } -impl ToToken for &str { - fn to_token(&self) -> Token { - Token::Value(vec![self.to_smolstr()]) +impl From<[&str; L]> for Token { + fn from(value: [&str; L]) -> Self { + Token::Value(value.into_iter().map(|s| s.to_smolstr()).collect()) } } -impl ToToken for [&str] { - fn to_token(&self) -> Token { - Token::Value(self.into_iter().map(|s| s.to_smolstr()).collect()) - } -} - -impl ToToken for [Token] { - fn to_token(&self) -> Token { - Token::Any(self.into_iter().map(|s| s.clone()).collect()) +impl From<[Token; L]> for Token { + fn from(value: [Token; L]) -> Self { + Token::Any(value.into_iter().map(|s| s.clone()).collect()) } } diff --git a/crates/commands/src/tree.rs b/crates/commands/src/tree.rs index f6810f3c..859ba9ed 100644 --- a/crates/commands/src/tree.rs +++ b/crates/commands/src/tree.rs @@ -21,17 +21,17 @@ impl TreeBranch { // iterate over tokens in command for token in command.tokens.clone() { // recursively get or create a sub-branch for each token - current_branch = current_branch.branches.entry(token).or_insert(TreeBranch { - current_command: None, - branches: OrderMap::new(), - }); + current_branch = current_branch + .branches + .entry(token) + .or_insert_with(TreeBranch::empty); } // when we're out of tokens, add an Empty branch with the callback and no sub-branches current_branch.branches.insert( Token::Empty, TreeBranch { - current_command: Some(command), branches: OrderMap::new(), + current_command: Some(command), }, ); } From 50981c175035c2a69196d0cc01311785c8b59c3c Mon Sep 17 00:00:00 2001 From: dusk Date: Tue, 14 Jan 2025 11:56:02 +0900 Subject: [PATCH 051/179] style: format --- crates/commands/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/commands/src/lib.rs b/crates/commands/src/lib.rs index 422109ef..e7105789 100644 --- a/crates/commands/src/lib.rs +++ b/crates/commands/src/lib.rs @@ -225,7 +225,7 @@ pub fn parse_command(prefix: String, input: String) -> CommandResult { return CommandResult::Ok { command: ParsedCommand { command_ref: command.cb.into(), - params, + params, flags, }, }; From 6c54551a9ef0bd374ae35aa355b2e1bf033a4710 Mon Sep 17 00:00:00 2001 From: dusk Date: Tue, 14 Jan 2025 23:30:53 +0900 Subject: [PATCH 052/179] fix(bot): fix ffi with flags --- PluralKit.Bot/CommandSystem/Parameters.cs | 44 +++++++++++++++++++---- 1 file changed, 37 insertions(+), 7 deletions(-) diff --git a/PluralKit.Bot/CommandSystem/Parameters.cs b/PluralKit.Bot/CommandSystem/Parameters.cs index 6455fdd3..87666673 100644 --- a/PluralKit.Bot/CommandSystem/Parameters.cs +++ b/PluralKit.Bot/CommandSystem/Parameters.cs @@ -19,7 +19,7 @@ public abstract record Parameter() public class Parameters { private string _cb { get; init; } - private Dictionary _flags { get; init; } + private Dictionary _flags { get; init; } private Dictionary _params { get; init; } // just used for errors, temporarily @@ -52,11 +52,9 @@ public class Parameters return potentialMatches.Any(_flags.ContainsKey); } - // resolves a single parameter - private async Task ResolveParameter(Context ctx, string param_name) + private async Task ResolveFfiParam(Context ctx, uniffi.commands.Parameter ffi_param) { - if (!_params.ContainsKey(param_name)) return null; - switch (_params[param_name]) + switch (ffi_param) { case uniffi.commands.Parameter.MemberRef memberRef: var byId = HasFlag("id", "by-id"); @@ -85,8 +83,40 @@ public class Parameters case uniffi.commands.Parameter.Reset _: return new Parameter.Reset(); } - // this should also never happen - throw new PKError($"Unknown parameter type for parameter {param_name}"); + return null; + } + + // resolves a single flag with value + private async Task ResolveFlag(Context ctx, string flag_name) + { + if (!HasFlag(flag_name)) return null; + var flag_value = _flags[flag_name]; + if (flag_value == null) return null; + var resolved = await ResolveFfiParam(ctx, flag_value); + if (resolved != null) return resolved; + // this should never happen, types are handled rust side + return null; + } + + // resolves a single parameter + private async Task ResolveParameter(Context ctx, string param_name) + { + if (!_params.ContainsKey(param_name)) return null; + var resolved = await ResolveFfiParam(ctx, _params[param_name]); + if (resolved != null) return resolved; + // this should never happen, types are handled rust side + return null; + } + + public async Task ResolveFlag(Context ctx, string flag_name, Func extract_func) + { + var param = await ResolveFlag(ctx, flag_name); + // todo: i think this should return null for everything...? + if (param == null) return default; + return extract_func(param) + // this should never really happen (hopefully!), but in case the parameter names dont match up (typos...) between rust <-> c#... + // (it would be very cool to have this statically checked somehow..?) + ?? throw new PKError($"Flag {flag_name.AsCode()} was not found or did not have a value defined for command {Callback().AsCode()} -- this is a bug!!"); } public async Task ResolveParameter(Context ctx, string param_name, Func extract_func) From 2a063442ead16d47788418bbc8de32a4e2cd9111 Mon Sep 17 00:00:00 2001 From: dusk Date: Wed, 15 Jan 2025 00:21:11 +0900 Subject: [PATCH 053/179] refactor(commands): remove Reset as a parameter type --- .../CommandSystem/Context/ContextParametersExt.cs | 10 ---------- PluralKit.Bot/CommandSystem/Parameters.cs | 3 --- PluralKit.Bot/Commands/Config.cs | 2 +- crates/commands/src/commands.udl | 1 - crates/commands/src/lib.rs | 1 - crates/commands/src/token.rs | 6 +++++- 6 files changed, 6 insertions(+), 17 deletions(-) diff --git a/PluralKit.Bot/CommandSystem/Context/ContextParametersExt.cs b/PluralKit.Bot/CommandSystem/Context/ContextParametersExt.cs index 754403b0..4a34a43a 100644 --- a/PluralKit.Bot/CommandSystem/Context/ContextParametersExt.cs +++ b/PluralKit.Bot/CommandSystem/Context/ContextParametersExt.cs @@ -51,14 +51,4 @@ public static class ContextParametersExt param => (param as Parameter.Toggle)?.value ); } - - // this can never really be false (either it's present and is true or it's not present) - // but we keep it nullable for consistency with the other methods - public static async Task ParamResolveReset(this Context ctx, string param_name) - { - return await ctx.Parameters.ResolveParameter( - ctx, param_name, - param => param is Parameter.Reset - ); - } } \ No newline at end of file diff --git a/PluralKit.Bot/CommandSystem/Parameters.cs b/PluralKit.Bot/CommandSystem/Parameters.cs index 87666673..263367cd 100644 --- a/PluralKit.Bot/CommandSystem/Parameters.cs +++ b/PluralKit.Bot/CommandSystem/Parameters.cs @@ -13,7 +13,6 @@ public abstract record Parameter() public record PrivacyLevel(string level): Parameter; public record Toggle(bool value): Parameter; public record Opaque(string value): Parameter; - public record Reset(): Parameter; } public class Parameters @@ -80,8 +79,6 @@ public class Parameters return new Parameter.Toggle(toggle.toggle); case uniffi.commands.Parameter.OpaqueString opaque: return new Parameter.Opaque(opaque.raw); - case uniffi.commands.Parameter.Reset _: - return new Parameter.Reset(); } return null; } diff --git a/PluralKit.Bot/Commands/Config.cs b/PluralKit.Bot/Commands/Config.cs index e4fecbdc..4acd00dc 100644 --- a/PluralKit.Bot/Commands/Config.cs +++ b/PluralKit.Bot/Commands/Config.cs @@ -230,7 +230,7 @@ public class Config public async Task EditAutoproxyTimeout(Context ctx) { var _newTimeout = await ctx.ParamResolveOpaque("timeout"); - var _reset = await ctx.ParamResolveReset("reset"); + var _reset = await ctx.ParamResolveToggle("reset"); var _toggle = await ctx.ParamResolveToggle("toggle"); Duration? newTimeout; diff --git a/crates/commands/src/commands.udl b/crates/commands/src/commands.udl index 824ec5a5..d1b5ae1c 100644 --- a/crates/commands/src/commands.udl +++ b/crates/commands/src/commands.udl @@ -14,7 +14,6 @@ interface Parameter { PrivacyLevel(string level); OpaqueString(string raw); Toggle(boolean toggle); - Reset(); }; dictionary ParsedCommand { string command_ref; diff --git a/crates/commands/src/lib.rs b/crates/commands/src/lib.rs index e7105789..abad8c0c 100644 --- a/crates/commands/src/lib.rs +++ b/crates/commands/src/lib.rs @@ -49,7 +49,6 @@ pub enum Parameter { PrivacyLevel { level: String }, OpaqueString { raw: String }, Toggle { toggle: bool }, - Reset, } #[derive(Debug)] diff --git a/crates/commands/src/token.rs b/crates/commands/src/token.rs index 594517a7..367c85b6 100644 --- a/crates/commands/src/token.rs +++ b/crates/commands/src/token.rs @@ -198,7 +198,11 @@ impl Token { Err(_) => None, }, Self::Reset(param_name) => match Reset::from_str(input) { - Ok(_) => TokenMatchValue::new_match_param(input, param_name, Parameter::Reset), + Ok(_) => TokenMatchValue::new_match_param( + input, + param_name, + Parameter::Toggle { toggle: true }, + ), Err(_) => None, }, // don't add a _ match here! From a1f7656276c17b1991617da64da3aa61b0a275e2 Mon Sep 17 00:00:00 2001 From: dusk Date: Wed, 15 Jan 2025 00:35:22 +0900 Subject: [PATCH 054/179] refactor(commands): FullString -> OpaqueRemainder and add OpaqueString --- crates/commands/src/commands.rs | 3 ++- crates/commands/src/commands/config.rs | 2 +- crates/commands/src/commands/member.rs | 4 ++-- crates/commands/src/commands/system.rs | 2 +- crates/commands/src/lib.rs | 2 +- crates/commands/src/token.rs | 22 ++++++++++++++-------- 6 files changed, 21 insertions(+), 14 deletions(-) diff --git a/crates/commands/src/commands.rs b/crates/commands/src/commands.rs index 3326ca1b..9e3c9a13 100644 --- a/crates/commands/src/commands.rs +++ b/crates/commands/src/commands.rs @@ -51,7 +51,8 @@ impl Command { let mut was_parameter = true; for (idx, token) in tokens.iter().enumerate().rev() { match token { - Token::FullString(_) + Token::OpaqueRemainder(_) + | Token::OpaqueString(_) | Token::MemberRef(_) | Token::MemberPrivacyTarget(_) | Token::SystemRef(_) diff --git a/crates/commands/src/commands/config.rs b/crates/commands/src/commands/config.rs index d39024d5..7df70321 100644 --- a/crates/commands/src/commands/config.rs +++ b/crates/commands/src/commands/config.rs @@ -27,7 +27,7 @@ pub fn cmds() -> impl Iterator { cfg, autoproxy, ["timeout", "tm"], - [Disable("toggle"), Reset("reset"), FullString("timeout")] // todo: we should parse duration / time values + [Disable("toggle"), Reset("reset"), OpaqueString("timeout")] // todo: we should parse duration / time values ], "cfg_ap_timeout_update", "Sets the autoproxy timeout" diff --git a/crates/commands/src/commands/member.rs b/crates/commands/src/commands/member.rs index 894e09a6..ac4043ce 100644 --- a/crates/commands/src/commands/member.rs +++ b/crates/commands/src/commands/member.rs @@ -10,7 +10,7 @@ pub fn cmds() -> impl Iterator { [ command!( - [member, new, FullString("name")], + [member, new, OpaqueString("name")], "member_new", "Creates a new system member" ), @@ -30,7 +30,7 @@ pub fn cmds() -> impl Iterator { member, MemberRef("target"), description, - FullString("description") + OpaqueRemainder("description") ], "member_desc_update", "Changes a member's description" diff --git a/crates/commands/src/commands/system.rs b/crates/commands/src/commands/system.rs index 37a67613..b505e23f 100644 --- a/crates/commands/src/commands/system.rs +++ b/crates/commands/src/commands/system.rs @@ -14,7 +14,7 @@ pub fn cmds() -> impl Iterator { ), command!([system, new], "system_new", "Creates a new system"), command!( - [system, new, FullString("name")], + [system, new, OpaqueString("name")], "system_new", "Creates a new system" ), diff --git a/crates/commands/src/lib.rs b/crates/commands/src/lib.rs index abad8c0c..04b66b2f 100644 --- a/crates/commands/src/lib.rs +++ b/crates/commands/src/lib.rs @@ -282,7 +282,7 @@ fn next_token<'a>( // iterate over tokens and run try_match for token in possible_tokens { - let is_match_remaining_token = |token: &Token| matches!(token, Token::FullString(_)); + let is_match_remaining_token = |token: &Token| matches!(token, Token::OpaqueRemainder(_)); // check if this is a token that matches the rest of the input let match_remaining = is_match_remaining_token(token) // check for Any here if it has a "match remainder" token in it diff --git a/crates/commands/src/token.rs b/crates/commands/src/token.rs index 367c85b6..7f736ee5 100644 --- a/crates/commands/src/token.rs +++ b/crates/commands/src/token.rs @@ -20,7 +20,9 @@ pub enum Token { Value(Vec), /// Opaque string (eg. "name" in `pk;member new name`) - FullString(ParamName), + OpaqueString(ParamName), + /// Remainder of a command (eg. "desc" in `pk;member description [desc...]`) + OpaqueRemainder(ParamName), /// Member reference (hid or member name) MemberRef(ParamName), @@ -94,7 +96,8 @@ impl Token { // empty token Self::Empty => Some(Ok(None)), // missing paramaters - Self::FullString(param_name) + Self::OpaqueRemainder(param_name) + | Self::OpaqueString(param_name) | Self::MemberRef(param_name) | Self::MemberPrivacyTarget(param_name) | Self::SystemRef(param_name) @@ -131,11 +134,13 @@ impl Token { .any(|v| v.eq(input)) .then(|| TokenMatchValue::new_match(input)) .unwrap_or(None), - Self::FullString(param_name) => TokenMatchValue::new_match_param( - input, - param_name, - Parameter::OpaqueString { raw: input.into() }, - ), + Self::OpaqueRemainder(param_name) | Self::OpaqueString(param_name) => { + TokenMatchValue::new_match_param( + input, + param_name, + Parameter::OpaqueString { raw: input.into() }, + ) + } Self::SystemRef(param_name) => TokenMatchValue::new_match_param( input, param_name, @@ -227,7 +232,8 @@ impl Display for Token { Token::Value(vec) if vec.is_empty().not() => write!(f, "{}", vec.first().unwrap()), Token::Value(_) => Ok(()), // if value token has no values (lol), don't print anything // todo: it might not be the best idea to directly use param name here (what if we want to display something else but keep the name? or translations?) - Token::FullString(param_name) => write!(f, "[{}]", param_name), + Token::OpaqueRemainder(param_name) => write!(f, "[{}...]", param_name), + Token::OpaqueString(param_name) => write!(f, "[{}]", param_name), Token::MemberRef(param_name) => write!(f, "<{}>", param_name), Token::SystemRef(param_name) => write!(f, "<{}>", param_name), Token::MemberPrivacyTarget(param_name) => write!(f, "<{}>", param_name), From 07541d9926254b9a413df55e0dc1edd7689d06b0 Mon Sep 17 00:00:00 2001 From: dusk Date: Wed, 15 Jan 2025 03:52:32 +0900 Subject: [PATCH 055/179] refactor(commands): rewrite how parameters are handled so they work same across cmd params / flag params, and make it easier to add new parameters --- crates/commands/src/commands.rs | 20 +- crates/commands/src/commands/config.rs | 6 +- crates/commands/src/commands/member.rs | 24 +- crates/commands/src/commands/system.rs | 4 +- crates/commands/src/flag.rs | 56 ++-- crates/commands/src/lib.rs | 34 ++- crates/commands/src/parameter.rs | 314 ++++++++++++++++++++++ crates/commands/src/token.rs | 348 +++++-------------------- 8 files changed, 443 insertions(+), 363 deletions(-) create mode 100644 crates/commands/src/parameter.rs diff --git a/crates/commands/src/commands.rs b/crates/commands/src/commands.rs index 9e3c9a13..c331a3fe 100644 --- a/crates/commands/src/commands.rs +++ b/crates/commands/src/commands.rs @@ -22,11 +22,7 @@ use std::fmt::{Debug, Display}; use smol_str::SmolStr; -use crate::{ - command, - flag::{Flag, FlagValue}, - token::Token, -}; +use crate::{any, command, flag::Flag, parameter::*, token::Token}; #[derive(Debug, Clone)] pub struct Command { @@ -51,17 +47,7 @@ impl Command { let mut was_parameter = true; for (idx, token) in tokens.iter().enumerate().rev() { match token { - Token::OpaqueRemainder(_) - | Token::OpaqueString(_) - | Token::MemberRef(_) - | Token::MemberPrivacyTarget(_) - | Token::SystemRef(_) - | Token::PrivacyLevel(_) - | Token::Toggle(_) - | Token::Enable(_) - | Token::Disable(_) - | Token::Reset(_) - | Token::Any(_) => { + Token::Parameter(_, _) | Token::Any(_) => { parse_flags_before = idx; was_parameter = true; } @@ -92,7 +78,7 @@ impl Command { self } - pub fn value_flag(mut self, name: impl Into, value: FlagValue) -> Self { + pub fn value_flag(mut self, name: impl Into, value: impl Parameter + 'static) -> Self { self.flags.push(Flag::new(name).with_value(value)); self } diff --git a/crates/commands/src/commands/config.rs b/crates/commands/src/commands/config.rs index 7df70321..47c46cbc 100644 --- a/crates/commands/src/commands/config.rs +++ b/crates/commands/src/commands/config.rs @@ -1,8 +1,6 @@ use super::*; pub fn cmds() -> impl Iterator { - use Token::*; - let cfg = ["config", "cfg"]; let autoproxy = ["autoproxy", "ap"]; @@ -13,7 +11,7 @@ pub fn cmds() -> impl Iterator { "Shows autoproxy status for the account" ), command!( - [cfg, autoproxy, ["account", "ac"], Toggle("toggle")], + [cfg, autoproxy, ["account", "ac"], Toggle], "cfg_ap_account_update", "Toggles autoproxy for the account" ), @@ -27,7 +25,7 @@ pub fn cmds() -> impl Iterator { cfg, autoproxy, ["timeout", "tm"], - [Disable("toggle"), Reset("reset"), OpaqueString("timeout")] // todo: we should parse duration / time values + any!(Disable, Reset, ("timeout", OpaqueString::SINGLE)) // todo: we should parse duration / time values ], "cfg_ap_timeout_update", "Sets the autoproxy timeout" diff --git a/crates/commands/src/commands/member.rs b/crates/commands/src/commands/member.rs index ac4043ce..6f33e615 100644 --- a/crates/commands/src/commands/member.rs +++ b/crates/commands/src/commands/member.rs @@ -1,8 +1,6 @@ use super::*; pub fn cmds() -> impl Iterator { - use Token::*; - let member = ["member", "m"]; let description = ["description", "desc"]; let privacy = ["privacy", "priv"]; @@ -10,49 +8,49 @@ pub fn cmds() -> impl Iterator { [ command!( - [member, new, OpaqueString("name")], + [member, new, ("name", OpaqueString::SINGLE)], "member_new", "Creates a new system member" ), command!( - [member, MemberRef("target")], + [member, MemberRef], "member_show", "Shows information about a member" ) - .value_flag("pt", FlagValue::OpaqueString), + .value_flag("pt", Disable), command!( - [member, MemberRef("target"), description], + [member, MemberRef, description], "member_desc_show", "Shows a member's description" ), command!( [ member, - MemberRef("target"), + MemberRef, description, - OpaqueRemainder("description") + ("description", OpaqueString::REMAINDER) ], "member_desc_update", "Changes a member's description" ), command!( - [member, MemberRef("target"), privacy], + [member, MemberRef, privacy], "member_privacy_show", "Displays a member's current privacy settings" ), command!( [ member, - MemberRef("target"), + MemberRef, privacy, - MemberPrivacyTarget("privacy_target"), - PrivacyLevel("new_privacy_level") + MemberPrivacyTarget, + ("new_privacy_level", PrivacyLevel) ], "member_privacy_update", "Changes a member's privacy settings" ), command!( - [member, MemberRef("target"), "soulscream"], + [member, MemberRef, "soulscream"], "member_soulscream", "todo" ) diff --git a/crates/commands/src/commands/system.rs b/crates/commands/src/commands/system.rs index b505e23f..47eb6591 100644 --- a/crates/commands/src/commands/system.rs +++ b/crates/commands/src/commands/system.rs @@ -1,8 +1,6 @@ use super::*; pub fn cmds() -> impl Iterator { - use Token::*; - let system = ["system", "s"]; let new = ["new", "n"]; @@ -14,7 +12,7 @@ pub fn cmds() -> impl Iterator { ), command!([system, new], "system_new", "Creates a new system"), command!( - [system, new, OpaqueString("name")], + [system, new, ("name", OpaqueString::SINGLE)], "system_new", "Creates a new system" ), diff --git a/crates/commands/src/flag.rs b/crates/commands/src/flag.rs index f9facb4e..0b3fce4f 100644 --- a/crates/commands/src/flag.rs +++ b/crates/commands/src/flag.rs @@ -1,50 +1,27 @@ -use std::fmt::Display; +use std::{fmt::Display, sync::Arc}; use smol_str::SmolStr; -use crate::Parameter; - -#[derive(Debug, Clone)] -pub enum FlagValue { - OpaqueString, -} - -impl FlagValue { - fn try_match(&self, input: &str) -> Result { - if input.is_empty() { - return Err(FlagValueMatchError::ValueMissing); - } - - match self { - Self::OpaqueString => Ok(Parameter::OpaqueString { raw: input.into() }), - } - } -} - -impl Display for FlagValue { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - FlagValue::OpaqueString => write!(f, "value"), - } - } -} +use crate::{parameter::Parameter, Parameter as FfiParam}; #[derive(Debug)] pub enum FlagValueMatchError { ValueMissing, + InvalidValue { raw: SmolStr, msg: SmolStr }, } #[derive(Debug, Clone)] pub struct Flag { name: SmolStr, - value: Option, + value: Option>, } impl Display for Flag { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "-{}", self.name)?; if let Some(value) = self.value.as_ref() { - write!(f, "={value}")?; + write!(f, "=")?; + value.format(f, value.default_name())?; } Ok(()) } @@ -55,7 +32,7 @@ pub enum FlagMatchError { ValueMatchFailed(FlagValueMatchError), } -type TryMatchFlagResult = Option, FlagMatchError>>; +type TryMatchFlagResult = Option, FlagMatchError>>; impl Flag { pub fn new(name: impl Into) -> Self { @@ -65,8 +42,8 @@ impl Flag { } } - pub fn with_value(mut self, value: FlagValue) -> Self { - self.value = Some(value); + pub fn with_value(mut self, param: impl Parameter + 'static) -> Self { + self.value = Some(Arc::new(param)); self } @@ -74,17 +51,13 @@ impl Flag { &self.name } - pub fn value(&self) -> Option<&FlagValue> { - self.value.as_ref() - } - pub fn try_match(&self, input_name: &str, input_value: Option<&str>) -> TryMatchFlagResult { // if not matching flag then skip anymore matching if self.name != input_name { return None; } // get token to try matching with, if flag doesn't have one then that means it is matched (it is without any value) - let Some(value) = self.value() else { + let Some(value) = self.value.as_deref() else { return Some(Ok(None)); }; // check if we have a non-empty flag value, we return error if not (because flag requested a value) @@ -94,9 +67,14 @@ impl Flag { ))); }; // try matching the value - match value.try_match(input_value) { + match value.match_value(input_value) { Ok(param) => Some(Ok(Some(param))), - Err(err) => Some(Err(FlagMatchError::ValueMatchFailed(err))), + Err(err) => Some(Err(FlagMatchError::ValueMatchFailed( + FlagValueMatchError::InvalidValue { + raw: input_value.into(), + msg: err, + }, + ))), } } } diff --git a/crates/commands/src/lib.rs b/crates/commands/src/lib.rs index 04b66b2f..3e3a5a49 100644 --- a/crates/commands/src/lib.rs +++ b/crates/commands/src/lib.rs @@ -3,6 +3,7 @@ pub mod commands; mod flag; +mod parameter; mod string; mod token; mod tree; @@ -91,7 +92,7 @@ pub fn parse_command(prefix: String, input: String) -> CommandResult { if let Some(next_tree) = local_tree.get_branch(&found_token) { local_tree = next_tree.clone(); } else { - panic!("found token could not match tree, at {input}"); + panic!("found token {found_token:?} could not match tree, at {input}"); } } Some(Err((token, err))) => { @@ -114,6 +115,9 @@ pub fn parse_command(prefix: String, input: String) -> CommandResult { write!(&mut msg, " in command `{prefix}{input} {token}`.").expect("oom"); msg } + TokenMatchError::ParameterMatchError { input: raw, msg } => { + format!("Parameter `{raw}` in command `{prefix}{input}` could not be parsed: {msg}.") + } }; return CommandResult::Err { error: error_msg }; } @@ -163,17 +167,23 @@ pub fn parse_command(prefix: String, input: String) -> CommandResult { flags.insert(name.into(), value); } Err((flag, err)) => { - match err { + let error = match err { FlagMatchError::ValueMatchFailed(FlagValueMatchError::ValueMissing) => { - return CommandResult::Err { - error: format!( - "Flag `-{name}` in command `{prefix}{input}` is missing a value, try passing `-{name}={value}`.", - name = flag.name(), - value = flag.value().expect("value missing error cant happen without a value"), - ), - } + format!( + "Flag `-{name}` in command `{prefix}{input}` is missing a value, try passing `{flag}`.", + name = flag.name() + ) } - } + FlagMatchError::ValueMatchFailed( + FlagValueMatchError::InvalidValue { msg, raw }, + ) => { + format!( + "Flag `-{name}` in command `{prefix}{input}` has a value (`{raw}`) that could not be parsed: {msg}.", + name = flag.name() + ) + } + }; + return CommandResult::Err { error }; } } } @@ -282,7 +292,8 @@ fn next_token<'a>( // iterate over tokens and run try_match for token in possible_tokens { - let is_match_remaining_token = |token: &Token| matches!(token, Token::OpaqueRemainder(_)); + let is_match_remaining_token = + |token: &Token| matches!(token, Token::Parameter(_, param) if param.remainder()); // check if this is a token that matches the rest of the input let match_remaining = is_match_remaining_token(token) // check for Any here if it has a "match remainder" token in it @@ -296,6 +307,7 @@ fn next_token<'a>( }); match token.try_match(input_to_match) { Some(Ok(value)) => { + println!("matched token: {}", token); let next_pos = match matched { // return last possible pos if we matched remaining, Some(_) if match_remaining => input.len(), diff --git a/crates/commands/src/parameter.rs b/crates/commands/src/parameter.rs new file mode 100644 index 00000000..7833a43e --- /dev/null +++ b/crates/commands/src/parameter.rs @@ -0,0 +1,314 @@ +use std::{fmt::Debug, str::FromStr}; + +use smol_str::SmolStr; + +use crate::{ParamName, Parameter as FfiParam}; + +pub trait Parameter: Debug + Send + Sync { + fn remainder(&self) -> bool { + false + } + fn default_name(&self) -> ParamName; + fn format(&self, f: &mut std::fmt::Formatter, name: &str) -> std::fmt::Result; + fn match_value(&self, input: &str) -> Result; +} + +#[derive(Debug, Clone, Eq, Hash, PartialEq)] +pub struct OpaqueString(bool); + +impl OpaqueString { + pub const SINGLE: Self = Self(false); + pub const REMAINDER: Self = Self(true); +} + +impl Parameter for OpaqueString { + fn remainder(&self) -> bool { + self.0 + } + + fn default_name(&self) -> ParamName { + "string" + } + + fn format(&self, f: &mut std::fmt::Formatter, name: &str) -> std::fmt::Result { + write!(f, "[{name}]") + } + + fn match_value(&self, input: &str) -> Result { + Ok(FfiParam::OpaqueString { raw: input.into() }) + } +} + +#[derive(Debug, Clone, Eq, Hash, PartialEq)] +pub struct MemberRef; + +impl Parameter for MemberRef { + fn default_name(&self) -> ParamName { + "member" + } + + fn format(&self, f: &mut std::fmt::Formatter, _: &str) -> std::fmt::Result { + write!(f, "") + } + + fn match_value(&self, input: &str) -> Result { + Ok(FfiParam::MemberRef { + member: input.into(), + }) + } +} + +#[derive(Debug, Clone, Eq, Hash, PartialEq)] +pub struct SystemRef; + +impl Parameter for SystemRef { + fn default_name(&self) -> ParamName { + "system" + } + + fn format(&self, f: &mut std::fmt::Formatter, _: &str) -> std::fmt::Result { + write!(f, "") + } + + fn match_value(&self, input: &str) -> Result { + Ok(FfiParam::SystemRef { + system: input.into(), + }) + } +} + +#[derive(Debug, Clone, Eq, Hash, PartialEq)] +pub struct MemberPrivacyTarget; + +pub enum MemberPrivacyTargetKind { + Visibility, + Name, + Description, + Banner, + Avatar, + Birthday, + Pronouns, + Proxy, + Metadata, +} + +impl AsRef for MemberPrivacyTargetKind { + fn as_ref(&self) -> &str { + match self { + Self::Visibility => "visibility", + Self::Name => "name", + Self::Description => "description", + Self::Banner => "banner", + Self::Avatar => "avatar", + Self::Birthday => "birthday", + Self::Pronouns => "pronouns", + Self::Proxy => "proxy", + Self::Metadata => "metadata", + } + } +} + +impl FromStr for MemberPrivacyTargetKind { + // todo: figure out how to represent these errors best + type Err = SmolStr; + + fn from_str(s: &str) -> Result { + // todo: this doesnt parse all the possible ways + match s.to_lowercase().as_str() { + "visibility" => Ok(Self::Visibility), + "name" => Ok(Self::Name), + "description" => Ok(Self::Description), + "banner" => Ok(Self::Banner), + "avatar" => Ok(Self::Avatar), + "birthday" => Ok(Self::Birthday), + "pronouns" => Ok(Self::Pronouns), + "proxy" => Ok(Self::Proxy), + "metadata" => Ok(Self::Metadata), + _ => Err("invalid member privacy target".into()), + } + } +} + +impl Parameter for MemberPrivacyTarget { + fn default_name(&self) -> ParamName { + "member_privacy_target" + } + + fn format(&self, f: &mut std::fmt::Formatter, _: &str) -> std::fmt::Result { + write!(f, "") + } + + fn match_value(&self, input: &str) -> Result { + MemberPrivacyTargetKind::from_str(input).map(|target| FfiParam::MemberPrivacyTarget { + target: target.as_ref().into(), + }) + } +} + +#[derive(Debug, Clone, Eq, Hash, PartialEq)] +pub struct PrivacyLevel; + +pub enum PrivacyLevelKind { + Public, + Private, +} + +impl AsRef for PrivacyLevelKind { + fn as_ref(&self) -> &str { + match self { + Self::Public => "public", + Self::Private => "private", + } + } +} + +impl FromStr for PrivacyLevelKind { + type Err = SmolStr; // todo + + fn from_str(s: &str) -> Result { + match s { + "public" => Ok(PrivacyLevelKind::Public), + "private" => Ok(PrivacyLevelKind::Private), + _ => Err("invalid privacy level".into()), + } + } +} + +impl Parameter for PrivacyLevel { + fn default_name(&self) -> ParamName { + "privacy_level" + } + + fn format(&self, f: &mut std::fmt::Formatter, _: &str) -> std::fmt::Result { + write!(f, "[privacy level]") + } + + fn match_value(&self, input: &str) -> Result { + PrivacyLevelKind::from_str(input).map(|level| FfiParam::PrivacyLevel { + level: level.as_ref().into(), + }) + } +} + +#[derive(Debug, Clone, Eq, Hash, PartialEq)] +pub struct Reset; + +impl AsRef for Reset { + fn as_ref(&self) -> &str { + "reset" + } +} + +impl FromStr for Reset { + type Err = SmolStr; + + fn from_str(s: &str) -> Result { + match s { + "reset" | "clear" | "default" => Ok(Self), + _ => Err("not reset".into()), + } + } +} + +impl Parameter for Reset { + fn default_name(&self) -> ParamName { + "reset" + } + + fn format(&self, f: &mut std::fmt::Formatter, _: &str) -> std::fmt::Result { + write!(f, "reset") + } + + fn match_value(&self, input: &str) -> Result { + Self::from_str(input).map(|_| FfiParam::Toggle { toggle: true }) + } +} + +#[derive(Debug, Clone, Eq, Hash, PartialEq)] +pub struct Toggle; + +impl Parameter for Toggle { + fn default_name(&self) -> ParamName { + "toggle" + } + + fn format(&self, f: &mut std::fmt::Formatter, _: &str) -> std::fmt::Result { + write!(f, "on/off") + } + + fn match_value(&self, input: &str) -> Result { + Enable::from_str(input) + .map(Into::::into) + .or_else(|_| Disable::from_str(input).map(Into::::into)) + .map(|toggle| FfiParam::Toggle { toggle }) + .map_err(|_| "invalid toggle".into()) + } +} + +#[derive(Debug, Clone, Eq, Hash, PartialEq)] +pub struct Enable; + +impl FromStr for Enable { + type Err = SmolStr; + + fn from_str(s: &str) -> Result { + match s { + "on" | "yes" | "true" | "enable" | "enabled" => Ok(Self), + _ => Err("invalid enable".into()), + } + } +} + +impl Parameter for Enable { + fn default_name(&self) -> ParamName { + "enable" + } + + fn format(&self, f: &mut std::fmt::Formatter, _: &str) -> std::fmt::Result { + write!(f, "on") + } + + fn match_value(&self, input: &str) -> Result { + Self::from_str(input).map(|e| FfiParam::Toggle { toggle: e.into() }) + } +} + +impl Into for Enable { + fn into(self) -> bool { + true + } +} + +#[derive(Debug, Clone, Eq, Hash, PartialEq)] +pub struct Disable; + +impl FromStr for Disable { + type Err = SmolStr; + + fn from_str(s: &str) -> Result { + match s { + "off" | "no" | "false" | "disable" | "disabled" => Ok(Self), + _ => Err("invalid disable".into()), + } + } +} + +impl Into for Disable { + fn into(self) -> bool { + false + } +} + +impl Parameter for Disable { + fn default_name(&self) -> ParamName { + "disable" + } + + fn format(&self, f: &mut std::fmt::Formatter, _: &str) -> std::fmt::Result { + write!(f, "off") + } + + fn match_value(&self, input: &str) -> Result { + Self::from_str(input).map(|e| FfiParam::Toggle { toggle: e.into() }) + } +} diff --git a/crates/commands/src/token.rs b/crates/commands/src/token.rs index 7f736ee5..7a525312 100644 --- a/crates/commands/src/token.rs +++ b/crates/commands/src/token.rs @@ -1,12 +1,17 @@ -use std::{fmt::Display, ops::Not, str::FromStr}; +use std::{ + fmt::{Debug, Display}, + hash::Hash, + ops::Not, + sync::Arc, +}; use smol_str::{SmolStr, ToSmolStr}; -use crate::Parameter; +use crate::{parameter::Parameter, Parameter as FfiParam}; -type ParamName = &'static str; +pub type ParamName = &'static str; -#[derive(Debug, Clone, Eq, Hash, PartialEq)] +#[derive(Debug, Clone)] pub enum Token { /// Token used to represent a finished command (i.e. no more parameters required) // todo: this is likely not the right way to represent this @@ -19,33 +24,45 @@ pub enum Token { /// A bot-defined command / subcommand (usually) (eg. "member" in `pk;member MyName`) Value(Vec), - /// Opaque string (eg. "name" in `pk;member new name`) - OpaqueString(ParamName), - /// Remainder of a command (eg. "desc" in `pk;member description [desc...]`) - OpaqueRemainder(ParamName), + /// A parameter that must be provided a value + Parameter(ParamName, Arc), +} - /// Member reference (hid or member name) - MemberRef(ParamName), - /// todo: doc - MemberPrivacyTarget(ParamName), +#[macro_export] +macro_rules! any { + ($($t:expr),+) => { + Token::Any(vec![$(Token::from($t)),+]) + }; +} - /// System reference - SystemRef(ParamName), +impl PartialEq for Token { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Self::Any(l0), Self::Any(r0)) => l0 == r0, + (Self::Value(l0), Self::Value(r0)) => l0 == r0, + (Self::Parameter(l0, _), Self::Parameter(r0, _)) => l0 == r0, + (Self::Empty, Self::Empty) => true, + _ => false, + } + } +} +impl Eq for Token {} - /// todo: doc - PrivacyLevel(ParamName), - - /// on, off; yes, no; true, false - Enable(ParamName), - Disable(ParamName), - Toggle(ParamName), - - /// reset, clear, default - Reset(ParamName), +impl Hash for Token { + fn hash(&self, state: &mut H) { + core::mem::discriminant(self).hash(state); + match self { + Token::Empty => {} + Token::Any(vec) => vec.hash(state), + Token::Value(vec) => vec.hash(state), + Token::Parameter(name, _) => name.hash(state), + } + } } #[derive(Debug)] pub enum TokenMatchError { + ParameterMatchError { input: SmolStr, msg: SmolStr }, MissingParameter { name: ParamName }, MissingAny { tokens: Vec }, } @@ -53,7 +70,7 @@ pub enum TokenMatchError { #[derive(Debug)] pub struct TokenMatchValue { pub raw: SmolStr, - pub param: Option<(ParamName, Parameter)>, + pub param: Option<(ParamName, FfiParam)>, } impl TokenMatchValue { @@ -67,7 +84,7 @@ impl TokenMatchValue { fn new_match_param( raw: impl Into, param_name: ParamName, - param: Parameter, + param: FfiParam, ) -> TryMatchResult { Some(Ok(Some(Self { raw: raw.into(), @@ -96,17 +113,8 @@ impl Token { // empty token Self::Empty => Some(Ok(None)), // missing paramaters - Self::OpaqueRemainder(param_name) - | Self::OpaqueString(param_name) - | Self::MemberRef(param_name) - | Self::MemberPrivacyTarget(param_name) - | Self::SystemRef(param_name) - | Self::PrivacyLevel(param_name) - | Self::Toggle(param_name) - | Self::Enable(param_name) - | Self::Disable(param_name) - | Self::Reset(param_name) => { - Some(Err(TokenMatchError::MissingParameter { name: param_name })) + Self::Parameter(name, _) => { + Some(Err(TokenMatchError::MissingParameter { name })) } Self::Any(tokens) => tokens.is_empty().then_some(None).unwrap_or_else(|| { Some(Err(TokenMatchError::MissingAny { @@ -134,83 +142,13 @@ impl Token { .any(|v| v.eq(input)) .then(|| TokenMatchValue::new_match(input)) .unwrap_or(None), - Self::OpaqueRemainder(param_name) | Self::OpaqueString(param_name) => { - TokenMatchValue::new_match_param( - input, - param_name, - Parameter::OpaqueString { raw: input.into() }, - ) - } - Self::SystemRef(param_name) => TokenMatchValue::new_match_param( - input, - param_name, - Parameter::SystemRef { - system: input.into(), - }, - ), - Self::MemberRef(param_name) => TokenMatchValue::new_match_param( - input, - param_name, - Parameter::MemberRef { - member: input.into(), - }, - ), - Self::MemberPrivacyTarget(param_name) => match MemberPrivacyTarget::from_str(input) { - Ok(target) => TokenMatchValue::new_match_param( - input, - param_name, - Parameter::MemberPrivacyTarget { - target: target.as_ref().into(), - }, - ), - Err(_) => None, - }, - Self::PrivacyLevel(param_name) => match PrivacyLevel::from_str(input) { - Ok(level) => TokenMatchValue::new_match_param( - input, - param_name, - Parameter::PrivacyLevel { - level: level.as_ref().into(), - }, - ), - Err(_) => None, - }, - Self::Toggle(param_name) => match Enable::from_str(input) - .map(Into::::into) - .or_else(|_| Disable::from_str(input).map(Into::::into)) - { - Ok(toggle) => TokenMatchValue::new_match_param( - input, - param_name, - Parameter::Toggle { toggle }, - ), - Err(_) => None, - }, - Self::Enable(param_name) => match Enable::from_str(input) { - Ok(t) => TokenMatchValue::new_match_param( - input, - param_name, - Parameter::Toggle { toggle: t.into() }, - ), - Err(_) => None, - }, - Self::Disable(param_name) => match Disable::from_str(input) { - Ok(t) => TokenMatchValue::new_match_param( - input, - param_name, - Parameter::Toggle { toggle: t.into() }, - ), - Err(_) => None, - }, - Self::Reset(param_name) => match Reset::from_str(input) { - Ok(_) => TokenMatchValue::new_match_param( - input, - param_name, - Parameter::Toggle { toggle: true }, - ), - Err(_) => None, - }, - // don't add a _ match here! + Self::Parameter(name, param) => match param.match_value(input) { + Ok(matched) => TokenMatchValue::new_match_param(input, name, matched), + Err(err) => Some(Err(TokenMatchError::ParameterMatchError { + input: input.into(), + msg: err, + })), + }, // don't add a _ match here! } } } @@ -231,17 +169,7 @@ impl Display for Token { } Token::Value(vec) if vec.is_empty().not() => write!(f, "{}", vec.first().unwrap()), Token::Value(_) => Ok(()), // if value token has no values (lol), don't print anything - // todo: it might not be the best idea to directly use param name here (what if we want to display something else but keep the name? or translations?) - Token::OpaqueRemainder(param_name) => write!(f, "[{}...]", param_name), - Token::OpaqueString(param_name) => write!(f, "[{}]", param_name), - Token::MemberRef(param_name) => write!(f, "<{}>", param_name), - Token::SystemRef(param_name) => write!(f, "<{}>", param_name), - Token::MemberPrivacyTarget(param_name) => write!(f, "<{}>", param_name), - Token::PrivacyLevel(param_name) => write!(f, "[{}]", param_name), - Token::Enable(_) => write!(f, "on"), - Token::Disable(_) => write!(f, "off"), - Token::Toggle(_) => write!(f, "on/off"), - Token::Reset(_) => write!(f, "reset"), + Token::Parameter(name, param) => param.format(f, name), } } } @@ -252,163 +180,31 @@ impl From<&str> for Token { } } -impl From<[&str; L]> for Token { - fn from(value: [&str; L]) -> Self { - Token::Value(value.into_iter().map(|s| s.to_smolstr()).collect()) +impl From

for Token { + fn from(value: P) -> Self { + Token::Parameter(value.default_name(), Arc::new(value)) } } -impl From<[Token; L]> for Token { - fn from(value: [Token; L]) -> Self { - Token::Any(value.into_iter().map(|s| s.clone()).collect()) +impl From<(ParamName, P)> for Token { + fn from(value: (ParamName, P)) -> Self { + Token::Parameter(value.0, Arc::new(value.1)) } } -#[derive(Debug, Clone, Eq, Hash, PartialEq)] -pub enum MemberPrivacyTarget { - Visibility, - Name, - Description, - Banner, - Avatar, - Birthday, - Pronouns, - Proxy, - Metadata, -} - -impl AsRef for MemberPrivacyTarget { - fn as_ref(&self) -> &str { - match self { - Self::Visibility => "visibility", - Self::Name => "name", - Self::Description => "description", - Self::Banner => "banner", - Self::Avatar => "avatar", - Self::Birthday => "birthday", - Self::Pronouns => "pronouns", - Self::Proxy => "proxy", - Self::Metadata => "metadata", +impl> From<[T; L]> for Token { + fn from(value: [T; L]) -> Self { + let tokens = value.into_iter().map(|s| s.into()).collect::>(); + if tokens.iter().all(|t| matches!(t, Token::Value(_))) { + let values = tokens + .into_iter() + .flat_map(|t| match t { + Token::Value(v) => v, + _ => unreachable!(), + }) + .collect::>(); + return Token::Value(values); } - } -} - -impl FromStr for MemberPrivacyTarget { - // todo: figure out how to represent these errors best - type Err = (); - - fn from_str(s: &str) -> Result { - // todo: this doesnt parse all the possible ways - match s.to_lowercase().as_str() { - "visibility" => Ok(Self::Visibility), - "name" => Ok(Self::Name), - "description" => Ok(Self::Description), - "banner" => Ok(Self::Banner), - "avatar" => Ok(Self::Avatar), - "birthday" => Ok(Self::Birthday), - "pronouns" => Ok(Self::Pronouns), - "proxy" => Ok(Self::Proxy), - "metadata" => Ok(Self::Metadata), - _ => Err(()), - } - } -} - -#[derive(Debug, Clone, Eq, Hash, PartialEq)] -pub enum PrivacyLevel { - Public, - Private, -} - -impl AsRef for PrivacyLevel { - fn as_ref(&self) -> &str { - match self { - Self::Public => "public", - Self::Private => "private", - } - } -} - -impl FromStr for PrivacyLevel { - type Err = (); // todo - - fn from_str(s: &str) -> Result { - match s { - "public" => Ok(Self::Public), - "private" => Ok(Self::Private), - _ => Err(()), - } - } -} - -#[derive(Debug, Clone, Eq, Hash, PartialEq)] -pub struct Reset; - -impl AsRef for Reset { - fn as_ref(&self) -> &str { - "reset" - } -} - -impl FromStr for Reset { - type Err = (); - - fn from_str(s: &str) -> Result { - match s { - "reset" | "clear" | "default" => Ok(Self), - _ => Err(()), - } - } -} - -#[derive(Debug, Clone, Eq, Hash, PartialEq)] -pub struct Enable; - -impl AsRef for Enable { - fn as_ref(&self) -> &str { - "on" - } -} - -impl FromStr for Enable { - type Err = (); - - fn from_str(s: &str) -> Result { - match s { - "on" | "yes" | "true" | "enable" | "enabled" => Ok(Self), - _ => Err(()), - } - } -} - -impl Into for Enable { - fn into(self) -> bool { - true - } -} - -#[derive(Debug, Clone, Eq, Hash, PartialEq)] -pub struct Disable; - -impl AsRef for Disable { - fn as_ref(&self) -> &str { - "off" - } -} - -impl FromStr for Disable { - type Err = (); - - fn from_str(s: &str) -> Result { - match s { - "off" | "no" | "false" | "disable" | "disabled" => Ok(Self), - _ => Err(()), - } - } -} - -impl Into for Disable { - fn into(self) -> bool { - false + Token::Any(tokens) } } From c6db96115eb148555ae8caf5b6800969c18f9f74 Mon Sep 17 00:00:00 2001 From: dusk Date: Mon, 20 Jan 2025 22:50:45 +0900 Subject: [PATCH 056/179] refactor(commands): remove help text from command macro and use method to set it --- crates/commands/src/commands.rs | 20 +++++------ crates/commands/src/commands/config.rs | 26 ++++++-------- crates/commands/src/commands/fun.rs | 4 +-- crates/commands/src/commands/help.rs | 6 ++-- crates/commands/src/commands/member.rs | 47 ++++++++------------------ crates/commands/src/commands/system.rs | 15 +++----- 6 files changed, 44 insertions(+), 74 deletions(-) diff --git a/crates/commands/src/commands.rs b/crates/commands/src/commands.rs index c331a3fe..8626a52a 100644 --- a/crates/commands/src/commands.rs +++ b/crates/commands/src/commands.rs @@ -36,11 +36,7 @@ pub struct Command { } impl Command { - pub fn new( - tokens: impl IntoIterator, - help: impl Into, - cb: impl Into, - ) -> Self { + pub fn new(tokens: impl IntoIterator, cb: impl Into) -> Self { let tokens = tokens.into_iter().collect::>(); assert!(tokens.len() > 0); let mut parse_flags_before = tokens.len(); @@ -60,7 +56,7 @@ impl Command { } Self { flags: Vec::new(), - help: help.into(), + help: SmolStr::new_static(""), cb: cb.into(), show_in_suggestions: true, parse_flags_before, @@ -68,6 +64,11 @@ impl Command { } } + pub fn help(mut self, v: impl Into) -> Self { + self.help = v.into(); + self + } + pub fn show_in_suggestions(mut self, v: bool) -> Self { self.show_in_suggestions = v; self @@ -111,16 +112,15 @@ impl Display for Command { // (and something like &dyn Trait would require everything to be referenced which doesnt look nice anyway) #[macro_export] macro_rules! command { - ([$($v:expr),+], $cb:expr, $help:expr) => { - $crate::commands::Command::new([$(Token::from($v)),*], $help, $cb) + ([$($v:expr),+], $cb:expr$(,)*) => { + $crate::commands::Command::new([$(Token::from($v)),*], $cb) }; } -pub fn all() -> Vec { +pub fn all() -> impl Iterator { (help::cmds()) .chain(system::cmds()) .chain(member::cmds()) .chain(config::cmds()) .chain(fun::cmds()) - .collect() } diff --git a/crates/commands/src/commands/config.rs b/crates/commands/src/commands/config.rs index 47c46cbc..b2f5b80d 100644 --- a/crates/commands/src/commands/config.rs +++ b/crates/commands/src/commands/config.rs @@ -5,21 +5,15 @@ pub fn cmds() -> impl Iterator { let autoproxy = ["autoproxy", "ap"]; [ - command!( - [cfg, autoproxy, ["account", "ac"]], - "cfg_ap_account_show", - "Shows autoproxy status for the account" - ), + command!([cfg, autoproxy, ["account", "ac"]], "cfg_ap_account_show") + .help("Shows autoproxy status for the account"), command!( [cfg, autoproxy, ["account", "ac"], Toggle], - "cfg_ap_account_update", - "Toggles autoproxy for the account" - ), - command!( - [cfg, autoproxy, ["timeout", "tm"]], - "cfg_ap_timeout_show", - "Shows the autoproxy timeout" - ), + "cfg_ap_account_update" + ) + .help("Toggles autoproxy for the account"), + command!([cfg, autoproxy, ["timeout", "tm"]], "cfg_ap_timeout_show") + .help("Shows the autoproxy timeout"), command!( [ cfg, @@ -27,9 +21,9 @@ pub fn cmds() -> impl Iterator { ["timeout", "tm"], any!(Disable, Reset, ("timeout", OpaqueString::SINGLE)) // todo: we should parse duration / time values ], - "cfg_ap_timeout_update", - "Sets the autoproxy timeout" - ), + "cfg_ap_timeout_update" + ) + .help("Sets the autoproxy timeout"), ] .into_iter() } diff --git a/crates/commands/src/commands/fun.rs b/crates/commands/src/commands/fun.rs index a8bddf45..720eadc0 100644 --- a/crates/commands/src/commands/fun.rs +++ b/crates/commands/src/commands/fun.rs @@ -2,8 +2,8 @@ use super::*; pub fn cmds() -> impl Iterator { [ - command!(["thunder"], "fun_thunder", "fun thunder"), - command!(["meow"], "fun_meow", "fun meow"), + command!(["thunder"], "fun_thunder"), + command!(["meow"], "fun_meow"), ] .into_iter() } diff --git a/crates/commands/src/commands/help.rs b/crates/commands/src/commands/help.rs index f663ee68..411b3565 100644 --- a/crates/commands/src/commands/help.rs +++ b/crates/commands/src/commands/help.rs @@ -3,9 +3,9 @@ use super::*; pub fn cmds() -> impl Iterator { let help = ["help", "h"]; [ - command!([help], "help", "Shows the help command"), - command!([help, "commands"], "help_commands", "help commands"), - command!([help, "proxy"], "help_proxy", "help proxy"), + command!([help], "help").help("Shows the help command"), + command!([help, "commands"], "help_commands").help("help commands"), + command!([help, "proxy"], "help_proxy").help("help proxy"), ] .into_iter() } diff --git a/crates/commands/src/commands/member.rs b/crates/commands/src/commands/member.rs index 6f33e615..5346bd53 100644 --- a/crates/commands/src/commands/member.rs +++ b/crates/commands/src/commands/member.rs @@ -7,22 +7,13 @@ pub fn cmds() -> impl Iterator { let new = ["new", "n"]; [ - command!( - [member, new, ("name", OpaqueString::SINGLE)], - "member_new", - "Creates a new system member" - ), - command!( - [member, MemberRef], - "member_show", - "Shows information about a member" - ) - .value_flag("pt", Disable), - command!( - [member, MemberRef, description], - "member_desc_show", - "Shows a member's description" - ), + command!([member, new, ("name", OpaqueString::SINGLE)], "member_new") + .help("Creates a new system member"), + command!([member, MemberRef], "member_show") + .help("Shows information about a member") + .value_flag("pt", Disable), + command!([member, MemberRef, description], "member_desc_show") + .help("Shows a member's description"), command!( [ member, @@ -30,14 +21,11 @@ pub fn cmds() -> impl Iterator { description, ("description", OpaqueString::REMAINDER) ], - "member_desc_update", - "Changes a member's description" - ), - command!( - [member, MemberRef, privacy], - "member_privacy_show", - "Displays a member's current privacy settings" - ), + "member_desc_update" + ) + .help("Changes a member's description"), + command!([member, MemberRef, privacy], "member_privacy_show") + .help("Displays a member's current privacy settings"), command!( [ member, @@ -46,15 +34,10 @@ pub fn cmds() -> impl Iterator { MemberPrivacyTarget, ("new_privacy_level", PrivacyLevel) ], - "member_privacy_update", - "Changes a member's privacy settings" - ), - command!( - [member, MemberRef, "soulscream"], - "member_soulscream", - "todo" + "member_privacy_update" ) - .show_in_suggestions(false), + .help("Changes a member's privacy settings"), + command!([member, MemberRef, "soulscream"], "member_soulscream").show_in_suggestions(false), ] .into_iter() } diff --git a/crates/commands/src/commands/system.rs b/crates/commands/src/commands/system.rs index 47eb6591..fd2148a3 100644 --- a/crates/commands/src/commands/system.rs +++ b/crates/commands/src/commands/system.rs @@ -5,17 +5,10 @@ pub fn cmds() -> impl Iterator { let new = ["new", "n"]; [ - command!( - [system], - "system_show", - "Shows information about your system" - ), - command!([system, new], "system_new", "Creates a new system"), - command!( - [system, new, ("name", OpaqueString::SINGLE)], - "system_new", - "Creates a new system" - ), + command!([system], "system_show").help("Shows information about your system"), + command!([system, new], "system_new").help("Creates a new system"), + command!([system, new, ("name", OpaqueString::SINGLE)], "system_new") + .help("Creates a new system"), ] .into_iter() } From 2a66e8b4cfcd055c590efd2718db066545521ae1 Mon Sep 17 00:00:00 2001 From: dusk Date: Tue, 21 Jan 2025 00:39:25 +0900 Subject: [PATCH 057/179] refactor(commands): remove general From array impl for tokens because it doesnt make sense --- crates/commands/src/commands.rs | 3 +++ crates/commands/src/main.rs | 2 +- crates/commands/src/token.rs | 27 ++++++++------------------- 3 files changed, 12 insertions(+), 20 deletions(-) diff --git a/crates/commands/src/commands.rs b/crates/commands/src/commands.rs index 8626a52a..eef0a9ec 100644 --- a/crates/commands/src/commands.rs +++ b/crates/commands/src/commands.rs @@ -39,10 +39,13 @@ impl Command { pub fn new(tokens: impl IntoIterator, cb: impl Into) -> Self { let tokens = tokens.into_iter().collect::>(); assert!(tokens.len() > 0); + // figure out which token to parse / put flags after + // (by default, put flags after the last token) let mut parse_flags_before = tokens.len(); let mut was_parameter = true; for (idx, token) in tokens.iter().enumerate().rev() { match token { + // we want flags to go before any parameters Token::Parameter(_, _) | Token::Any(_) => { parse_flags_before = idx; was_parameter = true; diff --git a/crates/commands/src/main.rs b/crates/commands/src/main.rs index 110915ea..6c722be3 100644 --- a/crates/commands/src/main.rs +++ b/crates/commands/src/main.rs @@ -16,7 +16,7 @@ fn main() { } } else { for command in cmds::all() { - println!("{}", command); + println!("{} - {}", command, command.help); } } } diff --git a/crates/commands/src/token.rs b/crates/commands/src/token.rs index 7a525312..c21ee16e 100644 --- a/crates/commands/src/token.rs +++ b/crates/commands/src/token.rs @@ -5,7 +5,7 @@ use std::{ sync::Arc, }; -use smol_str::{SmolStr, ToSmolStr}; +use smol_str::SmolStr; use crate::{parameter::Parameter, Parameter as FfiParam}; @@ -176,7 +176,13 @@ impl Display for Token { impl From<&str> for Token { fn from(value: &str) -> Self { - Token::Value(vec![value.to_smolstr()]) + Token::Value(vec![SmolStr::new(value)]) + } +} + +impl From<[&str; L]> for Token { + fn from(value: [&str; L]) -> Self { + Token::Value(value.into_iter().map(SmolStr::from).collect::>()) } } @@ -191,20 +197,3 @@ impl From<(ParamName, P)> for Token { Token::Parameter(value.0, Arc::new(value.1)) } } - -impl> From<[T; L]> for Token { - fn from(value: [T; L]) -> Self { - let tokens = value.into_iter().map(|s| s.into()).collect::>(); - if tokens.iter().all(|t| matches!(t, Token::Value(_))) { - let values = tokens - .into_iter() - .flat_map(|t| match t { - Token::Value(v) => v, - _ => unreachable!(), - }) - .collect::>(); - return Token::Value(values); - } - Token::Any(tokens) - } -} From 4f390e2a14ae8f4f981dda9581f267f2f784b752 Mon Sep 17 00:00:00 2001 From: dusk Date: Tue, 21 Jan 2025 01:01:49 +0900 Subject: [PATCH 058/179] refactor(commands): remove some unnecessary branching --- crates/commands/src/lib.rs | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/crates/commands/src/lib.rs b/crates/commands/src/lib.rs index 3e3a5a49..4eb10a56 100644 --- a/crates/commands/src/lib.rs +++ b/crates/commands/src/lib.rs @@ -246,13 +246,6 @@ fn match_flag<'a>( possible_flags: impl Iterator, matched_flag: MatchedFlag<'a>, ) -> Option), (&'a Flag, FlagMatchError)>> { - // skip if 0 length (we could just take an array ref here and in next_token aswell but its nice to keep it flexible) - if let (_, Some(len)) = possible_flags.size_hint() - && len == 0 - { - return None; - } - // check for all (possible) flags, see if token matches for flag in possible_flags { println!("matching flag {flag:?}"); @@ -279,13 +272,6 @@ fn next_token<'a>( input: &str, current_pos: usize, ) -> Option, usize), (&'a Token, TokenMatchError)>> { - // skip if 0 length - if let (_, Some(len)) = possible_tokens.size_hint() - && len == 0 - { - return None; - } - // get next parameter, matching quotes let matched = string::next_param(&input, current_pos); println!("matched: {matched:?}\n---"); From 0c012e98b5ed545f680cae15eefba8a989c4f9ee Mon Sep 17 00:00:00 2001 From: dusk Date: Tue, 21 Jan 2025 04:31:03 +0900 Subject: [PATCH 059/179] refactor: separate commands into command_parser, command_definitions crates --- Cargo.lock | 18 +- crates/command_definitions/Cargo.toml | 7 + .../src}/admin.rs | 0 .../src}/api.rs | 0 .../src}/autoproxy.rs | 0 .../src}/checks.rs | 0 .../src}/commands.rs | 0 .../src}/config.rs | 0 .../src}/dashboard.rs | 0 .../src}/debug.rs | 0 .../src}/fun.rs | 0 .../src}/group.rs | 0 .../src}/help.rs | 0 .../src}/import_export.rs | 0 crates/command_definitions/src/lib.rs | 29 ++ .../src}/member.rs | 0 .../src}/message.rs | 0 .../src}/misc.rs | 0 .../src}/random.rs | 0 .../src}/server_config.rs | 0 .../src}/switch.rs | 0 .../src}/system.rs | 0 crates/command_parser/Cargo.toml | 9 + .../src/command.rs} | 32 +- .../{commands => command_parser}/src/flag.rs | 4 +- crates/command_parser/src/lib.rs | 310 ++++++++++++++++ .../src/parameter.rs | 60 ++-- .../src/string.rs | 0 .../{commands => command_parser}/src/token.rs | 12 +- .../{commands => command_parser}/src/tree.rs | 18 +- crates/commands/Cargo.toml | 7 +- crates/commands/src/lib.rs | 332 ++---------------- crates/commands/src/main.rs | 4 +- 33 files changed, 464 insertions(+), 378 deletions(-) create mode 100644 crates/command_definitions/Cargo.toml rename crates/{commands/src/commands => command_definitions/src}/admin.rs (100%) rename crates/{commands/src/commands => command_definitions/src}/api.rs (100%) rename crates/{commands/src/commands => command_definitions/src}/autoproxy.rs (100%) rename crates/{commands/src/commands => command_definitions/src}/checks.rs (100%) rename crates/{commands/src/commands => command_definitions/src}/commands.rs (100%) rename crates/{commands/src/commands => command_definitions/src}/config.rs (100%) rename crates/{commands/src/commands => command_definitions/src}/dashboard.rs (100%) rename crates/{commands/src/commands => command_definitions/src}/debug.rs (100%) rename crates/{commands/src/commands => command_definitions/src}/fun.rs (100%) rename crates/{commands/src/commands => command_definitions/src}/group.rs (100%) rename crates/{commands/src/commands => command_definitions/src}/help.rs (100%) rename crates/{commands/src/commands => command_definitions/src}/import_export.rs (100%) create mode 100644 crates/command_definitions/src/lib.rs rename crates/{commands/src/commands => command_definitions/src}/member.rs (100%) rename crates/{commands/src/commands => command_definitions/src}/message.rs (100%) rename crates/{commands/src/commands => command_definitions/src}/misc.rs (100%) rename crates/{commands/src/commands => command_definitions/src}/random.rs (100%) rename crates/{commands/src/commands => command_definitions/src}/server_config.rs (100%) rename crates/{commands/src/commands => command_definitions/src}/switch.rs (100%) rename crates/{commands/src/commands => command_definitions/src}/system.rs (100%) create mode 100644 crates/command_parser/Cargo.toml rename crates/{commands/src/commands.rs => command_parser/src/command.rs} (82%) rename crates/{commands => command_parser}/src/flag.rs (94%) create mode 100644 crates/command_parser/src/lib.rs rename crates/{commands => command_parser}/src/parameter.rs (79%) rename crates/{commands => command_parser}/src/string.rs (100%) rename crates/{commands => command_parser}/src/token.rs (94%) rename crates/{commands => command_parser}/src/tree.rs (81%) diff --git a/Cargo.lock b/Cargo.lock index 50dcba38..241b57f1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -565,12 +565,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" [[package]] -name = "commands" +name = "command_definitions" +version = "0.1.0" +dependencies = [ + "command_parser", +] + +[[package]] +name = "command_parser" version = "0.1.0" dependencies = [ "lazy_static", "ordermap", "smol_str", +] + +[[package]] +name = "commands" +version = "0.1.0" +dependencies = [ + "command_definitions", + "command_parser", + "lazy_static", "uniffi", ] diff --git a/crates/command_definitions/Cargo.toml b/crates/command_definitions/Cargo.toml new file mode 100644 index 00000000..e17e23a9 --- /dev/null +++ b/crates/command_definitions/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "command_definitions" +version = "0.1.0" +edition = "2021" + +[dependencies] +command_parser = { path = "../command_parser"} \ No newline at end of file diff --git a/crates/commands/src/commands/admin.rs b/crates/command_definitions/src/admin.rs similarity index 100% rename from crates/commands/src/commands/admin.rs rename to crates/command_definitions/src/admin.rs diff --git a/crates/commands/src/commands/api.rs b/crates/command_definitions/src/api.rs similarity index 100% rename from crates/commands/src/commands/api.rs rename to crates/command_definitions/src/api.rs diff --git a/crates/commands/src/commands/autoproxy.rs b/crates/command_definitions/src/autoproxy.rs similarity index 100% rename from crates/commands/src/commands/autoproxy.rs rename to crates/command_definitions/src/autoproxy.rs diff --git a/crates/commands/src/commands/checks.rs b/crates/command_definitions/src/checks.rs similarity index 100% rename from crates/commands/src/commands/checks.rs rename to crates/command_definitions/src/checks.rs diff --git a/crates/commands/src/commands/commands.rs b/crates/command_definitions/src/commands.rs similarity index 100% rename from crates/commands/src/commands/commands.rs rename to crates/command_definitions/src/commands.rs diff --git a/crates/commands/src/commands/config.rs b/crates/command_definitions/src/config.rs similarity index 100% rename from crates/commands/src/commands/config.rs rename to crates/command_definitions/src/config.rs diff --git a/crates/commands/src/commands/dashboard.rs b/crates/command_definitions/src/dashboard.rs similarity index 100% rename from crates/commands/src/commands/dashboard.rs rename to crates/command_definitions/src/dashboard.rs diff --git a/crates/commands/src/commands/debug.rs b/crates/command_definitions/src/debug.rs similarity index 100% rename from crates/commands/src/commands/debug.rs rename to crates/command_definitions/src/debug.rs diff --git a/crates/commands/src/commands/fun.rs b/crates/command_definitions/src/fun.rs similarity index 100% rename from crates/commands/src/commands/fun.rs rename to crates/command_definitions/src/fun.rs diff --git a/crates/commands/src/commands/group.rs b/crates/command_definitions/src/group.rs similarity index 100% rename from crates/commands/src/commands/group.rs rename to crates/command_definitions/src/group.rs diff --git a/crates/commands/src/commands/help.rs b/crates/command_definitions/src/help.rs similarity index 100% rename from crates/commands/src/commands/help.rs rename to crates/command_definitions/src/help.rs diff --git a/crates/commands/src/commands/import_export.rs b/crates/command_definitions/src/import_export.rs similarity index 100% rename from crates/commands/src/commands/import_export.rs rename to crates/command_definitions/src/import_export.rs diff --git a/crates/command_definitions/src/lib.rs b/crates/command_definitions/src/lib.rs new file mode 100644 index 00000000..9e8adb49 --- /dev/null +++ b/crates/command_definitions/src/lib.rs @@ -0,0 +1,29 @@ +pub mod admin; +pub mod api; +pub mod autoproxy; +pub mod checks; +pub mod commands; +pub mod config; +pub mod dashboard; +pub mod debug; +pub mod fun; +pub mod group; +pub mod help; +pub mod import_export; +pub mod member; +pub mod message; +pub mod misc; +pub mod random; +pub mod server_config; +pub mod switch; +pub mod system; + +use command_parser::{any, command, command::Command, parameter::*}; + +pub fn all() -> impl Iterator { + (help::cmds()) + .chain(system::cmds()) + .chain(member::cmds()) + .chain(config::cmds()) + .chain(fun::cmds()) +} diff --git a/crates/commands/src/commands/member.rs b/crates/command_definitions/src/member.rs similarity index 100% rename from crates/commands/src/commands/member.rs rename to crates/command_definitions/src/member.rs diff --git a/crates/commands/src/commands/message.rs b/crates/command_definitions/src/message.rs similarity index 100% rename from crates/commands/src/commands/message.rs rename to crates/command_definitions/src/message.rs diff --git a/crates/commands/src/commands/misc.rs b/crates/command_definitions/src/misc.rs similarity index 100% rename from crates/commands/src/commands/misc.rs rename to crates/command_definitions/src/misc.rs diff --git a/crates/commands/src/commands/random.rs b/crates/command_definitions/src/random.rs similarity index 100% rename from crates/commands/src/commands/random.rs rename to crates/command_definitions/src/random.rs diff --git a/crates/commands/src/commands/server_config.rs b/crates/command_definitions/src/server_config.rs similarity index 100% rename from crates/commands/src/commands/server_config.rs rename to crates/command_definitions/src/server_config.rs diff --git a/crates/commands/src/commands/switch.rs b/crates/command_definitions/src/switch.rs similarity index 100% rename from crates/commands/src/commands/switch.rs rename to crates/command_definitions/src/switch.rs diff --git a/crates/commands/src/commands/system.rs b/crates/command_definitions/src/system.rs similarity index 100% rename from crates/commands/src/commands/system.rs rename to crates/command_definitions/src/system.rs diff --git a/crates/command_parser/Cargo.toml b/crates/command_parser/Cargo.toml new file mode 100644 index 00000000..749d348c --- /dev/null +++ b/crates/command_parser/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "command_parser" +version = "0.1.0" +edition = "2021" + +[dependencies] +lazy_static = { workspace = true } +smol_str = "0.3.2" +ordermap = "0.5" diff --git a/crates/commands/src/commands.rs b/crates/command_parser/src/command.rs similarity index 82% rename from crates/commands/src/commands.rs rename to crates/command_parser/src/command.rs index eef0a9ec..0410834b 100644 --- a/crates/commands/src/commands.rs +++ b/crates/command_parser/src/command.rs @@ -1,28 +1,8 @@ -pub mod admin; -pub mod api; -pub mod autoproxy; -pub mod checks; -pub mod commands; -pub mod config; -pub mod dashboard; -pub mod debug; -pub mod fun; -pub mod group; -pub mod help; -pub mod import_export; -pub mod member; -pub mod message; -pub mod misc; -pub mod random; -pub mod server_config; -pub mod switch; -pub mod system; - use std::fmt::{Debug, Display}; use smol_str::SmolStr; -use crate::{any, command, flag::Flag, parameter::*, token::Token}; +use crate::{flag::Flag, parameter::*, token::Token}; #[derive(Debug, Clone)] pub struct Command { @@ -116,14 +96,6 @@ impl Display for Command { #[macro_export] macro_rules! command { ([$($v:expr),+], $cb:expr$(,)*) => { - $crate::commands::Command::new([$(Token::from($v)),*], $cb) + $crate::command::Command::new([$($crate::token::Token::from($v)),*], $cb) }; } - -pub fn all() -> impl Iterator { - (help::cmds()) - .chain(system::cmds()) - .chain(member::cmds()) - .chain(config::cmds()) - .chain(fun::cmds()) -} diff --git a/crates/commands/src/flag.rs b/crates/command_parser/src/flag.rs similarity index 94% rename from crates/commands/src/flag.rs rename to crates/command_parser/src/flag.rs index 0b3fce4f..6bd12f2b 100644 --- a/crates/commands/src/flag.rs +++ b/crates/command_parser/src/flag.rs @@ -2,7 +2,7 @@ use std::{fmt::Display, sync::Arc}; use smol_str::SmolStr; -use crate::{parameter::Parameter, Parameter as FfiParam}; +use crate::parameter::{Parameter, ParameterValue}; #[derive(Debug)] pub enum FlagValueMatchError { @@ -32,7 +32,7 @@ pub enum FlagMatchError { ValueMatchFailed(FlagValueMatchError), } -type TryMatchFlagResult = Option, FlagMatchError>>; +type TryMatchFlagResult = Option, FlagMatchError>>; impl Flag { pub fn new(name: impl Into) -> Self { diff --git a/crates/command_parser/src/lib.rs b/crates/command_parser/src/lib.rs new file mode 100644 index 00000000..5da79c36 --- /dev/null +++ b/crates/command_parser/src/lib.rs @@ -0,0 +1,310 @@ +#![feature(let_chains)] +#![feature(anonymous_lifetime_in_impl_trait)] + +pub mod command; +mod flag; +pub mod parameter; +mod string; +pub mod token; +pub mod tree; + +use core::panic; +use std::collections::HashMap; +use std::fmt::Write; +use std::ops::Not; + +use command::Command; +use flag::{Flag, FlagMatchError, FlagValueMatchError}; +use parameter::ParameterValue; +use smol_str::SmolStr; +use string::MatchedFlag; +use token::{Token, TokenMatchError, TokenMatchValue}; + +// todo: this should come from the bot probably +const MAX_SUGGESTIONS: usize = 7; + +pub type Tree = tree::TreeBranch; + +#[derive(Debug)] +pub struct ParsedCommand { + pub command_def: Command, + pub parameters: HashMap, + pub flags: HashMap>, +} + +pub fn parse_command( + command_tree: Tree, + prefix: String, + input: String, +) -> Result { + let input: SmolStr = input.into(); + let mut local_tree: Tree = command_tree.clone(); + + // end position of all currently matched tokens + let mut current_pos: usize = 0; + let mut current_token_idx: usize = 0; + + let mut params: HashMap = HashMap::new(); + let mut raw_flags: Vec<(usize, MatchedFlag)> = Vec::new(); + + loop { + println!( + "possible: {:?}", + local_tree.possible_tokens().collect::>() + ); + let next = next_token(local_tree.possible_tokens(), &input, current_pos); + println!("next: {:?}", next); + match next { + Some(Ok((found_token, arg, new_pos))) => { + current_pos = new_pos; + current_token_idx += 1; + + if let Some(arg) = arg.as_ref() { + // insert arg as paramater if this is a parameter + if let Some((param_name, param)) = arg.param.as_ref() { + params.insert(param_name.to_string(), param.clone()); + } + } + + if let Some(next_tree) = local_tree.get_branch(&found_token) { + local_tree = next_tree.clone(); + } else { + panic!("found token {found_token:?} could not match tree, at {input}"); + } + } + Some(Err((token, err))) => { + let error_msg = match err { + TokenMatchError::MissingParameter { name } => { + format!("Expected parameter `{name}` in command `{prefix}{input} {token}`.") + } + TokenMatchError::MissingAny { tokens } => { + let mut msg = format!("Expected one of "); + for (idx, token) in tokens.iter().enumerate() { + write!(&mut msg, "`{token}`").expect("oom"); + if idx < tokens.len() - 1 { + if tokens.len() > 2 && idx == tokens.len() - 2 { + msg.push_str(" or "); + } else { + msg.push_str(", "); + } + } + } + write!(&mut msg, " in command `{prefix}{input} {token}`.").expect("oom"); + msg + } + TokenMatchError::ParameterMatchError { input: raw, msg } => { + format!("Parameter `{raw}` in command `{prefix}{input}` could not be parsed: {msg}.") + } + }; + return Err(error_msg); + } + None => { + // if it said command not found on a flag, output better error message + let mut error = format!("Unknown command `{prefix}{input}`."); + + if fmt_possible_commands(&mut error, &prefix, local_tree.possible_commands(2)).not() + { + error.push_str(" "); + } + + error.push_str( + "For a list of all possible commands, see .", + ); + + // todo: check if last token is a common incorrect unquote (multi-member names etc) + // todo: check if this is a system name in pk;s command + return Err(error); + } + } + // match flags until there are none left + while let Some(matched_flag) = string::next_flag(&input, current_pos) { + current_pos = matched_flag.next_pos; + println!("flag matched {matched_flag:?}"); + raw_flags.push((current_token_idx, matched_flag)); + } + // if we have a command, stop parsing and return it + if let Some(command) = local_tree.command() { + // match the flags against this commands flags + let mut flags: HashMap> = HashMap::new(); + let mut misplaced_flags: Vec = Vec::new(); + let mut invalid_flags: Vec = Vec::new(); + for (token_idx, matched_flag) in raw_flags { + if token_idx != command.parse_flags_before { + misplaced_flags.push(matched_flag); + continue; + } + let Some(matched_flag) = match_flag(command.flags.iter(), matched_flag.clone()) + else { + invalid_flags.push(matched_flag); + continue; + }; + match matched_flag { + // a flag was matched + Ok((name, value)) => { + flags.insert(name.into(), value); + } + Err((flag, err)) => { + let error = match err { + FlagMatchError::ValueMatchFailed(FlagValueMatchError::ValueMissing) => { + format!( + "Flag `-{name}` in command `{prefix}{input}` is missing a value, try passing `{flag}`.", + name = flag.name() + ) + } + FlagMatchError::ValueMatchFailed( + FlagValueMatchError::InvalidValue { msg, raw }, + ) => { + format!( + "Flag `-{name}` in command `{prefix}{input}` has a value (`{raw}`) that could not be parsed: {msg}.", + name = flag.name() + ) + } + }; + return Err(error); + } + } + } + if misplaced_flags.is_empty().not() { + let mut error = format!( + "Flag{} ", + (misplaced_flags.len() > 1).then_some("s").unwrap_or("") + ); + for (idx, matched_flag) in misplaced_flags.iter().enumerate() { + write!(&mut error, "`-{}`", matched_flag.name).expect("oom"); + if idx < misplaced_flags.len() - 1 { + error.push_str(", "); + } + } + write!( + &mut error, + " in command `{prefix}{input}` {} misplaced. Try reordering to match the command usage `{prefix}{command}`.", + (misplaced_flags.len() > 1).then_some("are").unwrap_or("is") + ).expect("oom"); + return Err(error); + } + if invalid_flags.is_empty().not() { + let mut error = format!( + "Flag{} ", + (misplaced_flags.len() > 1).then_some("s").unwrap_or("") + ); + for (idx, matched_flag) in invalid_flags.iter().enumerate() { + write!(&mut error, "`-{}`", matched_flag.name).expect("oom"); + if idx < invalid_flags.len() - 1 { + error.push_str(", "); + } + } + write!( + &mut error, + " {} not applicable in this command (`{prefix}{input}`). Applicable flags are the following:", + (invalid_flags.len() > 1).then_some("are").unwrap_or("is") + ).expect("oom"); + for (idx, flag) in command.flags.iter().enumerate() { + write!(&mut error, " `{flag}`").expect("oom"); + if idx < command.flags.len() - 1 { + error.push_str(", "); + } + } + error.push_str("."); + return Err(error); + } + println!("{} {flags:?} {params:?}", command.cb); + return Ok(ParsedCommand { + command_def: command, + flags, + parameters: params, + }); + } + } +} + +fn match_flag<'a>( + possible_flags: impl Iterator, + matched_flag: MatchedFlag<'a>, +) -> Option), (&'a Flag, FlagMatchError)>> { + // check for all (possible) flags, see if token matches + for flag in possible_flags { + println!("matching flag {flag:?}"); + match flag.try_match(matched_flag.name, matched_flag.value) { + Some(Ok(param)) => return Some(Ok((flag.name().into(), param))), + Some(Err(err)) => return Some(Err((flag, err))), + None => {} + } + } + + None +} + +/// Find the next token from an either raw or partially parsed command string +/// +/// Returns: +/// - nothing (none matched) +/// - matched token, to move deeper into the tree +/// - matched value (if this command matched an user-provided value such as a member name) +/// - end position of matched token +/// - error when matching +fn next_token<'a>( + possible_tokens: impl Iterator, + input: &str, + current_pos: usize, +) -> Option, usize), (&'a Token, TokenMatchError)>> { + // get next parameter, matching quotes + let matched = string::next_param(&input, current_pos); + println!("matched: {matched:?}\n---"); + + // iterate over tokens and run try_match + for token in possible_tokens { + let is_match_remaining_token = + |token: &Token| matches!(token, Token::Parameter(_, param) if param.remainder()); + // check if this is a token that matches the rest of the input + let match_remaining = is_match_remaining_token(token) + // check for Any here if it has a "match remainder" token in it + // if there is a "match remainder" token in a command there shouldn't be a command descending from that + || matches!(token, Token::Any(ref tokens) if tokens.iter().any(is_match_remaining_token)); + // either use matched param or rest of the input if matching remaining + let input_to_match = matched.as_ref().map(|v| { + match_remaining + .then_some(&input[current_pos..]) + .unwrap_or(v.value) + }); + match token.try_match(input_to_match) { + Some(Ok(value)) => { + println!("matched token: {}", token); + let next_pos = match matched { + // return last possible pos if we matched remaining, + Some(_) if match_remaining => input.len(), + // otherwise use matched param next pos, + Some(param) => param.next_pos, + // and if didnt match anything we stay where we are + None => current_pos, + }; + return Some(Ok((token, value, next_pos))); + } + Some(Err(err)) => { + return Some(Err((token, err))); + } + None => {} // continue matching until we exhaust all tokens + } + } + + None +} + +// todo: should probably move this somewhere else +/// returns true if wrote possible commands, false if not +fn fmt_possible_commands( + f: &mut String, + prefix: &str, + mut possible_commands: impl Iterator, +) -> bool { + if let Some(first) = possible_commands.next() { + f.push_str(" Perhaps you meant one of the following commands:\n"); + for command in std::iter::once(first).chain(possible_commands.take(MAX_SUGGESTIONS - 1)) { + if !command.show_in_suggestions { + continue; + } + writeln!(f, "- **{prefix}{command}** - *{}*", command.help).expect("oom"); + } + return true; + } + return false; +} diff --git a/crates/commands/src/parameter.rs b/crates/command_parser/src/parameter.rs similarity index 79% rename from crates/commands/src/parameter.rs rename to crates/command_parser/src/parameter.rs index 7833a43e..ff67ee7e 100644 --- a/crates/commands/src/parameter.rs +++ b/crates/command_parser/src/parameter.rs @@ -2,7 +2,17 @@ use std::{fmt::Debug, str::FromStr}; use smol_str::SmolStr; -use crate::{ParamName, Parameter as FfiParam}; +use crate::token::ParamName; + +#[derive(Debug, Clone)] +pub enum ParameterValue { + MemberRef(String), + SystemRef(String), + MemberPrivacyTarget(String), + PrivacyLevel(String), + OpaqueString(String), + Toggle(bool), +} pub trait Parameter: Debug + Send + Sync { fn remainder(&self) -> bool { @@ -10,7 +20,7 @@ pub trait Parameter: Debug + Send + Sync { } fn default_name(&self) -> ParamName; fn format(&self, f: &mut std::fmt::Formatter, name: &str) -> std::fmt::Result; - fn match_value(&self, input: &str) -> Result; + fn match_value(&self, input: &str) -> Result; } #[derive(Debug, Clone, Eq, Hash, PartialEq)] @@ -34,8 +44,8 @@ impl Parameter for OpaqueString { write!(f, "[{name}]") } - fn match_value(&self, input: &str) -> Result { - Ok(FfiParam::OpaqueString { raw: input.into() }) + fn match_value(&self, input: &str) -> Result { + Ok(ParameterValue::OpaqueString(input.into())) } } @@ -51,10 +61,8 @@ impl Parameter for MemberRef { write!(f, "") } - fn match_value(&self, input: &str) -> Result { - Ok(FfiParam::MemberRef { - member: input.into(), - }) + fn match_value(&self, input: &str) -> Result { + Ok(ParameterValue::MemberRef(input.into())) } } @@ -70,10 +78,8 @@ impl Parameter for SystemRef { write!(f, "") } - fn match_value(&self, input: &str) -> Result { - Ok(FfiParam::SystemRef { - system: input.into(), - }) + fn match_value(&self, input: &str) -> Result { + Ok(ParameterValue::SystemRef(input.into())) } } @@ -138,10 +144,9 @@ impl Parameter for MemberPrivacyTarget { write!(f, "") } - fn match_value(&self, input: &str) -> Result { - MemberPrivacyTargetKind::from_str(input).map(|target| FfiParam::MemberPrivacyTarget { - target: target.as_ref().into(), - }) + fn match_value(&self, input: &str) -> Result { + MemberPrivacyTargetKind::from_str(input) + .map(|target| ParameterValue::MemberPrivacyTarget(target.as_ref().into())) } } @@ -183,10 +188,9 @@ impl Parameter for PrivacyLevel { write!(f, "[privacy level]") } - fn match_value(&self, input: &str) -> Result { - PrivacyLevelKind::from_str(input).map(|level| FfiParam::PrivacyLevel { - level: level.as_ref().into(), - }) + fn match_value(&self, input: &str) -> Result { + PrivacyLevelKind::from_str(input) + .map(|level| ParameterValue::PrivacyLevel(level.as_ref().into())) } } @@ -219,8 +223,8 @@ impl Parameter for Reset { write!(f, "reset") } - fn match_value(&self, input: &str) -> Result { - Self::from_str(input).map(|_| FfiParam::Toggle { toggle: true }) + fn match_value(&self, input: &str) -> Result { + Self::from_str(input).map(|_| ParameterValue::Toggle(true)) } } @@ -236,11 +240,11 @@ impl Parameter for Toggle { write!(f, "on/off") } - fn match_value(&self, input: &str) -> Result { + fn match_value(&self, input: &str) -> Result { Enable::from_str(input) .map(Into::::into) .or_else(|_| Disable::from_str(input).map(Into::::into)) - .map(|toggle| FfiParam::Toggle { toggle }) + .map(ParameterValue::Toggle) .map_err(|_| "invalid toggle".into()) } } @@ -268,8 +272,8 @@ impl Parameter for Enable { write!(f, "on") } - fn match_value(&self, input: &str) -> Result { - Self::from_str(input).map(|e| FfiParam::Toggle { toggle: e.into() }) + fn match_value(&self, input: &str) -> Result { + Self::from_str(input).map(|e| ParameterValue::Toggle(e.into())) } } @@ -308,7 +312,7 @@ impl Parameter for Disable { write!(f, "off") } - fn match_value(&self, input: &str) -> Result { - Self::from_str(input).map(|e| FfiParam::Toggle { toggle: e.into() }) + fn match_value(&self, input: &str) -> Result { + Self::from_str(input).map(|e| ParameterValue::Toggle(e.into())) } } diff --git a/crates/commands/src/string.rs b/crates/command_parser/src/string.rs similarity index 100% rename from crates/commands/src/string.rs rename to crates/command_parser/src/string.rs diff --git a/crates/commands/src/token.rs b/crates/command_parser/src/token.rs similarity index 94% rename from crates/commands/src/token.rs rename to crates/command_parser/src/token.rs index c21ee16e..aae2f06e 100644 --- a/crates/commands/src/token.rs +++ b/crates/command_parser/src/token.rs @@ -7,7 +7,7 @@ use std::{ use smol_str::SmolStr; -use crate::{parameter::Parameter, Parameter as FfiParam}; +use crate::parameter::{Parameter, ParameterValue}; pub type ParamName = &'static str; @@ -31,7 +31,7 @@ pub enum Token { #[macro_export] macro_rules! any { ($($t:expr),+) => { - Token::Any(vec![$(Token::from($t)),+]) + $crate::token::Token::Any(vec![$($crate::token::Token::from($t)),+]) }; } @@ -68,9 +68,9 @@ pub enum TokenMatchError { } #[derive(Debug)] -pub struct TokenMatchValue { +pub(super) struct TokenMatchValue { pub raw: SmolStr, - pub param: Option<(ParamName, FfiParam)>, + pub param: Option<(ParamName, ParameterValue)>, } impl TokenMatchValue { @@ -84,7 +84,7 @@ impl TokenMatchValue { fn new_match_param( raw: impl Into, param_name: ParamName, - param: FfiParam, + param: ParameterValue, ) -> TryMatchResult { Some(Ok(Some(Self { raw: raw.into(), @@ -104,7 +104,7 @@ impl TokenMatchValue { type TryMatchResult = Option, TokenMatchError>>; impl Token { - pub fn try_match(&self, input: Option<&str>) -> TryMatchResult { + pub(super) fn try_match(&self, input: Option<&str>) -> TryMatchResult { let input = match input { Some(input) => input, None => { diff --git a/crates/commands/src/tree.rs b/crates/command_parser/src/tree.rs similarity index 81% rename from crates/commands/src/tree.rs rename to crates/command_parser/src/tree.rs index 859ba9ed..5f78eff6 100644 --- a/crates/commands/src/tree.rs +++ b/crates/command_parser/src/tree.rs @@ -1,6 +1,6 @@ use ordermap::OrderMap; -use crate::{commands::Command, Token}; +use crate::{command::Command, token::Token}; #[derive(Debug, Clone)] pub struct TreeBranch { @@ -8,14 +8,16 @@ pub struct TreeBranch { branches: OrderMap, } -impl TreeBranch { - pub fn empty() -> Self { +impl Default for TreeBranch { + fn default() -> Self { Self { current_command: None, branches: OrderMap::new(), } } +} +impl TreeBranch { pub fn register_command(&mut self, command: Command) { let mut current_branch = self; // iterate over tokens in command @@ -24,7 +26,7 @@ impl TreeBranch { current_branch = current_branch .branches .entry(token) - .or_insert_with(TreeBranch::empty); + .or_insert_with(TreeBranch::default); } // when we're out of tokens, add an Empty branch with the callback and no sub-branches current_branch.branches.insert( @@ -36,15 +38,15 @@ impl TreeBranch { ); } - pub fn command(&self) -> Option { + pub(super) fn command(&self) -> Option { self.current_command.clone() } - pub fn possible_tokens(&self) -> impl Iterator { + pub(super) fn possible_tokens(&self) -> impl Iterator { self.branches.keys() } - pub fn possible_commands(&self, max_depth: usize) -> impl Iterator { + pub(super) fn possible_commands(&self, max_depth: usize) -> impl Iterator { // dusk: i am too lazy to write an iterator for this without using recursion so we box everything fn box_iter<'a>( iter: impl Iterator + 'a, @@ -67,7 +69,7 @@ impl TreeBranch { commands } - pub fn get_branch(&self, token: &Token) -> Option<&TreeBranch> { + pub(super) fn get_branch(&self, token: &Token) -> Option<&TreeBranch> { self.branches.get(token) } } diff --git a/crates/commands/Cargo.toml b/crates/commands/Cargo.toml index 4983cb53..46bca6a7 100644 --- a/crates/commands/Cargo.toml +++ b/crates/commands/Cargo.toml @@ -8,10 +8,9 @@ crate-type = ["cdylib", "lib"] [dependencies] lazy_static = { workspace = true } - +command_parser = { path = "../command_parser"} +command_definitions = { path = "../command_definitions"} uniffi = { version = "0.25" } -smol_str = "0.3.2" -ordermap = "0.5" [build-dependencies] -uniffi = { version = "0.25", features = [ "build" ] } +uniffi = { version = "0.25", features = [ "build" ] } \ No newline at end of file diff --git a/crates/commands/src/lib.rs b/crates/commands/src/lib.rs index 4eb10a56..21c1db13 100644 --- a/crates/commands/src/lib.rs +++ b/crates/commands/src/lib.rs @@ -1,36 +1,14 @@ -#![feature(let_chains)] -#![feature(anonymous_lifetime_in_impl_trait)] +use std::collections::HashMap; -pub mod commands; -mod flag; -mod parameter; -mod string; -mod token; -mod tree; +use command_parser::{parameter::ParameterValue, Tree}; uniffi::include_scaffolding!("commands"); -use core::panic; -use std::collections::HashMap; -use std::fmt::Write; -use std::ops::Not; - -use flag::{Flag, FlagMatchError, FlagValueMatchError}; -use smol_str::SmolStr; -use string::MatchedFlag; -use tree::TreeBranch; - -pub use commands::Command; -pub use token::*; - -// todo: this should come from the bot probably -const MAX_SUGGESTIONS: usize = 7; - lazy_static::lazy_static! { - pub static ref COMMAND_TREE: TreeBranch = { - let mut tree = TreeBranch::empty(); + pub static ref COMMAND_TREE: Tree = { + let mut tree = Tree::default(); - crate::commands::all().into_iter().for_each(|x| tree.register_command(x)); + command_definitions::all().into_iter().for_each(|x| tree.register_command(x)); tree }; @@ -52,6 +30,19 @@ pub enum Parameter { Toggle { toggle: bool }, } +impl From for Parameter { + fn from(value: ParameterValue) -> Self { + match value { + ParameterValue::MemberRef(member) => Self::MemberRef { member }, + ParameterValue::SystemRef(system) => Self::SystemRef { system }, + ParameterValue::MemberPrivacyTarget(target) => Self::MemberPrivacyTarget { target }, + ParameterValue::PrivacyLevel(level) => Self::PrivacyLevel { level }, + ParameterValue::OpaqueString(raw) => Self::OpaqueString { raw }, + ParameterValue::Toggle(toggle) => Self::Toggle { toggle }, + } + } +} + #[derive(Debug)] pub struct ParsedCommand { pub command_ref: String, @@ -60,276 +51,25 @@ pub struct ParsedCommand { } pub fn parse_command(prefix: String, input: String) -> CommandResult { - let input: SmolStr = input.into(); - let mut local_tree: TreeBranch = COMMAND_TREE.clone(); - - // end position of all currently matched tokens - let mut current_pos: usize = 0; - let mut current_token_idx: usize = 0; - - let mut params: HashMap = HashMap::new(); - let mut raw_flags: Vec<(usize, MatchedFlag)> = Vec::new(); - - loop { - println!( - "possible: {:?}", - local_tree.possible_tokens().collect::>() - ); - let next = next_token(local_tree.possible_tokens(), &input, current_pos); - println!("next: {:?}", next); - match next { - Some(Ok((found_token, arg, new_pos))) => { - current_pos = new_pos; - current_token_idx += 1; - - if let Some(arg) = arg.as_ref() { - // insert arg as paramater if this is a parameter - if let Some((param_name, param)) = arg.param.as_ref() { - params.insert(param_name.to_string(), param.clone()); - } + command_parser::parse_command(COMMAND_TREE.clone(), prefix, input).map_or_else( + |error| CommandResult::Err { error }, + |parsed| CommandResult::Ok { + command: { + let command_ref = parsed.command_def.cb.into(); + let mut flags = HashMap::with_capacity(parsed.flags.capacity()); + for (name, value) in parsed.flags { + flags.insert(name, value.map(Parameter::from)); } - - if let Some(next_tree) = local_tree.get_branch(&found_token) { - local_tree = next_tree.clone(); - } else { - panic!("found token {found_token:?} could not match tree, at {input}"); + let mut params = HashMap::with_capacity(parsed.parameters.capacity()); + for (name, value) in parsed.parameters { + params.insert(name, Parameter::from(value)); } - } - Some(Err((token, err))) => { - let error_msg = match err { - TokenMatchError::MissingParameter { name } => { - format!("Expected parameter `{name}` in command `{prefix}{input} {token}`.") - } - TokenMatchError::MissingAny { tokens } => { - let mut msg = format!("Expected one of "); - for (idx, token) in tokens.iter().enumerate() { - write!(&mut msg, "`{token}`").expect("oom"); - if idx < tokens.len() - 1 { - if tokens.len() > 2 && idx == tokens.len() - 2 { - msg.push_str(" or "); - } else { - msg.push_str(", "); - } - } - } - write!(&mut msg, " in command `{prefix}{input} {token}`.").expect("oom"); - msg - } - TokenMatchError::ParameterMatchError { input: raw, msg } => { - format!("Parameter `{raw}` in command `{prefix}{input}` could not be parsed: {msg}.") - } - }; - return CommandResult::Err { error: error_msg }; - } - None => { - // if it said command not found on a flag, output better error message - let mut error = format!("Unknown command `{prefix}{input}`."); - - if fmt_possible_commands(&mut error, &prefix, local_tree.possible_commands(2)).not() - { - error.push_str(" "); - } - - error.push_str( - "For a list of all possible commands, see .", - ); - - // todo: check if last token is a common incorrect unquote (multi-member names etc) - // todo: check if this is a system name in pk;s command - return CommandResult::Err { error }; - } - } - // match flags until there are none left - while let Some(matched_flag) = string::next_flag(&input, current_pos) { - current_pos = matched_flag.next_pos; - println!("flag matched {matched_flag:?}"); - raw_flags.push((current_token_idx, matched_flag)); - } - // if we have a command, stop parsing and return it - if let Some(command) = local_tree.command() { - // match the flags against this commands flags - let mut flags: HashMap> = HashMap::new(); - let mut misplaced_flags: Vec = Vec::new(); - let mut invalid_flags: Vec = Vec::new(); - for (token_idx, matched_flag) in raw_flags { - if token_idx != command.parse_flags_before { - misplaced_flags.push(matched_flag); - continue; - } - let Some(matched_flag) = match_flag(command.flags.iter(), matched_flag.clone()) - else { - invalid_flags.push(matched_flag); - continue; - }; - match matched_flag { - // a flag was matched - Ok((name, value)) => { - flags.insert(name.into(), value); - } - Err((flag, err)) => { - let error = match err { - FlagMatchError::ValueMatchFailed(FlagValueMatchError::ValueMissing) => { - format!( - "Flag `-{name}` in command `{prefix}{input}` is missing a value, try passing `{flag}`.", - name = flag.name() - ) - } - FlagMatchError::ValueMatchFailed( - FlagValueMatchError::InvalidValue { msg, raw }, - ) => { - format!( - "Flag `-{name}` in command `{prefix}{input}` has a value (`{raw}`) that could not be parsed: {msg}.", - name = flag.name() - ) - } - }; - return CommandResult::Err { error }; - } - } - } - if misplaced_flags.is_empty().not() { - let mut error = format!( - "Flag{} ", - (misplaced_flags.len() > 1).then_some("s").unwrap_or("") - ); - for (idx, matched_flag) in misplaced_flags.iter().enumerate() { - write!(&mut error, "`-{}`", matched_flag.name).expect("oom"); - if idx < misplaced_flags.len() - 1 { - error.push_str(", "); - } - } - write!( - &mut error, - " in command `{prefix}{input}` {} misplaced. Try reordering to match the command usage `{prefix}{command}`.", - (misplaced_flags.len() > 1).then_some("are").unwrap_or("is") - ).expect("oom"); - return CommandResult::Err { error }; - } - if invalid_flags.is_empty().not() { - let mut error = format!( - "Flag{} ", - (misplaced_flags.len() > 1).then_some("s").unwrap_or("") - ); - for (idx, matched_flag) in invalid_flags.iter().enumerate() { - write!(&mut error, "`-{}`", matched_flag.name).expect("oom"); - if idx < invalid_flags.len() - 1 { - error.push_str(", "); - } - } - write!( - &mut error, - " {} not applicable in this command (`{prefix}{input}`). Applicable flags are the following:", - (invalid_flags.len() > 1).then_some("are").unwrap_or("is") - ).expect("oom"); - for (idx, flag) in command.flags.iter().enumerate() { - write!(&mut error, " `{flag}`").expect("oom"); - if idx < command.flags.len() - 1 { - error.push_str(", "); - } - } - error.push_str("."); - return CommandResult::Err { error }; - } - println!("{} {flags:?} {params:?}", command.cb); - return CommandResult::Ok { - command: ParsedCommand { - command_ref: command.cb.into(), - params, + ParsedCommand { + command_ref, flags, - }, - }; - } - } -} - -fn match_flag<'a>( - possible_flags: impl Iterator, - matched_flag: MatchedFlag<'a>, -) -> Option), (&'a Flag, FlagMatchError)>> { - // check for all (possible) flags, see if token matches - for flag in possible_flags { - println!("matching flag {flag:?}"); - match flag.try_match(matched_flag.name, matched_flag.value) { - Some(Ok(param)) => return Some(Ok((flag.name().into(), param))), - Some(Err(err)) => return Some(Err((flag, err))), - None => {} - } - } - - None -} - -/// Find the next token from an either raw or partially parsed command string -/// -/// Returns: -/// - nothing (none matched) -/// - matched token, to move deeper into the tree -/// - matched value (if this command matched an user-provided value such as a member name) -/// - end position of matched token -/// - error when matching -fn next_token<'a>( - possible_tokens: impl Iterator, - input: &str, - current_pos: usize, -) -> Option, usize), (&'a Token, TokenMatchError)>> { - // get next parameter, matching quotes - let matched = string::next_param(&input, current_pos); - println!("matched: {matched:?}\n---"); - - // iterate over tokens and run try_match - for token in possible_tokens { - let is_match_remaining_token = - |token: &Token| matches!(token, Token::Parameter(_, param) if param.remainder()); - // check if this is a token that matches the rest of the input - let match_remaining = is_match_remaining_token(token) - // check for Any here if it has a "match remainder" token in it - // if there is a "match remainder" token in a command there shouldn't be a command descending from that - || matches!(token, Token::Any(ref tokens) if tokens.iter().any(is_match_remaining_token)); - // either use matched param or rest of the input if matching remaining - let input_to_match = matched.as_ref().map(|v| { - match_remaining - .then_some(&input[current_pos..]) - .unwrap_or(v.value) - }); - match token.try_match(input_to_match) { - Some(Ok(value)) => { - println!("matched token: {}", token); - let next_pos = match matched { - // return last possible pos if we matched remaining, - Some(_) if match_remaining => input.len(), - // otherwise use matched param next pos, - Some(param) => param.next_pos, - // and if didnt match anything we stay where we are - None => current_pos, - }; - return Some(Ok((token, value, next_pos))); - } - Some(Err(err)) => { - return Some(Err((token, err))); - } - None => {} // continue matching until we exhaust all tokens - } - } - - None -} - -// todo: should probably move this somewhere else -/// returns true if wrote possible commands, false if not -fn fmt_possible_commands( - f: &mut String, - prefix: &str, - mut possible_commands: impl Iterator, -) -> bool { - if let Some(first) = possible_commands.next() { - f.push_str(" Perhaps you meant one of the following commands:\n"); - for command in std::iter::once(first).chain(possible_commands.take(MAX_SUGGESTIONS - 1)) { - if !command.show_in_suggestions { - continue; - } - writeln!(f, "- **{prefix}{command}** - *{}*", command.help).expect("oom"); - } - return true; - } - return false; + params, + } + }, + }, + ) } diff --git a/crates/commands/src/main.rs b/crates/commands/src/main.rs index 6c722be3..313c6d74 100644 --- a/crates/commands/src/main.rs +++ b/crates/commands/src/main.rs @@ -1,7 +1,5 @@ #![feature(iter_intersperse)] -use commands::commands as cmds; - fn main() { let cmd = std::env::args() .skip(1) @@ -15,7 +13,7 @@ fn main() { CommandResult::Err { error } => println!("{error}"), } } else { - for command in cmds::all() { + for command in command_definitions::all() { println!("{} - {}", command, command.help); } } From 07e8a4851a03d639362be2c7fa238ac552af4398 Mon Sep 17 00:00:00 2001 From: dusk Date: Tue, 21 Jan 2025 12:36:54 +0900 Subject: [PATCH 060/179] feat(commands): add cs codegen to statically use params and flags in bot code, remove Any --- .gitignore | 1 + PluralKit.Bot/CommandMeta/CommandTree.cs | 30 +-- PluralKit.Bot/Commands/Config.cs | 54 +++-- PluralKit.Bot/Commands/Member.cs | 10 +- PluralKit.Bot/Handlers/MessageCreated.cs | 14 +- crates/command_definitions/src/config.rs | 36 ++- crates/command_definitions/src/fun.rs | 4 +- crates/command_definitions/src/help.rs | 6 +- crates/command_definitions/src/lib.rs | 4 +- crates/command_definitions/src/member.rs | 41 ++-- crates/command_definitions/src/system.rs | 10 +- crates/command_parser/src/command.rs | 28 ++- crates/command_parser/src/flag.rs | 16 +- crates/command_parser/src/lib.rs | 22 +- crates/command_parser/src/parameter.rs | 293 ++++++++--------------- crates/command_parser/src/token.rs | 105 ++------ crates/command_parser/src/tree.rs | 12 +- crates/commands/Cargo.toml | 5 + crates/commands/src/main.rs | 21 ++ flake.nix | 2 + 20 files changed, 297 insertions(+), 417 deletions(-) diff --git a/.gitignore b/.gitignore index a4f9031e..31332235 100644 --- a/.gitignore +++ b/.gitignore @@ -32,6 +32,7 @@ logs/ recipe.json .docker-bin/ PluralKit.Bot/commands.cs +PluralKit.Bot/commandtypes.cs # nix .nix-process-compose diff --git a/PluralKit.Bot/CommandMeta/CommandTree.cs b/PluralKit.Bot/CommandMeta/CommandTree.cs index edbc8428..71b6fd71 100644 --- a/PluralKit.Bot/CommandMeta/CommandTree.cs +++ b/PluralKit.Bot/CommandMeta/CommandTree.cs @@ -4,24 +4,26 @@ namespace PluralKit.Bot; public partial class CommandTree { - public Task ExecuteCommand(Context ctx) + public Task ExecuteCommand(Context ctx, Commands command) { - return ctx.Parameters.Callback() switch + return command switch { - "help" => ctx.Execute(Help, m => m.HelpRoot(ctx)), - "help_commands" => ctx.Reply( + Commands.Help => ctx.Execute(Help, m => m.HelpRoot(ctx)), + Commands.HelpCommands => ctx.Reply( "For the list of commands, see the website: "), - "help_proxy" => ctx.Reply( + Commands.HelpProxy => ctx.Reply( "The proxy help page has been moved! See the website: https://pluralkit.me/guide#proxying"), - "member_show" => ctx.Execute(MemberInfo, m => m.ViewMember(ctx)), - "member_new" => ctx.Execute(MemberNew, m => m.NewMember(ctx)), - "member_soulscream" => ctx.Execute(MemberInfo, m => m.Soulscream(ctx)), - "cfg_ap_account_show" => ctx.Execute(null, m => m.ViewAutoproxyAccount(ctx)), - "cfg_ap_account_update" => ctx.Execute(null, m => m.EditAutoproxyAccount(ctx)), - "cfg_ap_timeout_show" => ctx.Execute(null, m => m.ViewAutoproxyTimeout(ctx)), - "cfg_ap_timeout_update" => ctx.Execute(null, m => m.EditAutoproxyTimeout(ctx)), - "fun_thunder" => ctx.Execute(null, m => m.Thunder(ctx)), - "fun_meow" => ctx.Execute(null, m => m.Meow(ctx)), + Commands.MemberShow(MemberShowParams param, _) => ctx.Execute(MemberInfo, m => m.ViewMember(ctx, param.target)), + Commands.MemberNew(MemberNewParams param, _) => ctx.Execute(MemberNew, m => m.NewMember(ctx, param.name)), + Commands.MemberSoulscream(MemberSoulscreamParams param, _) => ctx.Execute(MemberInfo, m => m.Soulscream(ctx, param.target)), + Commands.CfgApAccountShow => ctx.Execute(null, m => m.ViewAutoproxyAccount(ctx)), + Commands.CfgApAccountUpdate(CfgApAccountUpdateParams param, _) => ctx.Execute(null, m => m.EditAutoproxyAccount(ctx, param.toggle)), + Commands.CfgApTimeoutShow => ctx.Execute(null, m => m.ViewAutoproxyTimeout(ctx)), + Commands.CfgApTimeoutOff => ctx.Execute(null, m => m.DisableAutoproxyTimeout(ctx)), + Commands.CfgApTimeoutReset => ctx.Execute(null, m => m.ResetAutoproxyTimeout(ctx)), + Commands.CfgApTimeoutUpdate(CfgApTimeoutUpdateParams param, _) => ctx.Execute(null, m => m.EditAutoproxyTimeout(ctx, param.timeout)), + Commands.FunThunder => ctx.Execute(null, m => m.Thunder(ctx)), + Commands.FunMeow => ctx.Execute(null, m => m.Meow(ctx)), _ => // this should only ever occur when deving if commands are not implemented... ctx.Reply( diff --git a/PluralKit.Bot/Commands/Config.cs b/PluralKit.Bot/Commands/Config.cs index 4acd00dc..d4267d5b 100644 --- a/PluralKit.Bot/Commands/Config.cs +++ b/PluralKit.Bot/Commands/Config.cs @@ -197,10 +197,9 @@ public class Config await ctx.Reply($"Autoproxy is currently **{EnabledDisabled(allowAutoproxy)}** for account <@{ctx.Author.Id}>."); } - public async Task EditAutoproxyAccount(Context ctx) + public async Task EditAutoproxyAccount(Context ctx, bool allow) { var allowAutoproxy = await ctx.Repository.GetAutoproxyEnabled(ctx.Author.Id); - var allow = await ctx.ParamResolveToggle("toggle") ?? throw new PKSyntaxError("You need to specify whether to enable or disable autoproxy for this account."); var statusString = EnabledDisabled(allow); if (allowAutoproxy == allow) @@ -227,41 +226,44 @@ public class Config await ctx.Reply($"The current latch timeout duration for your system is {timeout.Value.ToTimeSpan().Humanize(4)}."); } - public async Task EditAutoproxyTimeout(Context ctx) + public async Task DisableAutoproxyTimeout(Context ctx) { - var _newTimeout = await ctx.ParamResolveOpaque("timeout"); - var _reset = await ctx.ParamResolveToggle("reset"); - var _toggle = await ctx.ParamResolveToggle("toggle"); + await ctx.Repository.UpdateSystemConfig(ctx.System.Id, new() { LatchTimeout = (int)Duration.Zero.TotalSeconds }); - Duration? newTimeout; + await ctx.Reply($"{Emojis.Success} Latch timeout disabled. Latch mode autoproxy will never time out."); + } + + public async Task ResetAutoproxyTimeout(Context ctx) + { + await ctx.Repository.UpdateSystemConfig(ctx.System.Id, new() { LatchTimeout = null }); + + await ctx.Reply($"{Emojis.Success} Latch timeout reset to default ({ProxyMatcher.DefaultLatchExpiryTime.ToTimeSpan().Humanize(4)})."); + } + + public async Task EditAutoproxyTimeout(Context ctx, string timeout) + { + Duration newTimeout; Duration overflow = Duration.Zero; - if (_toggle == false) newTimeout = Duration.Zero; - else if (_reset == true) newTimeout = null; - else + // todo: we should parse date in the command parser + var timeoutStr = timeout; + var timeoutPeriod = DateUtils.ParsePeriod(timeoutStr) + ?? throw new PKError($"Could not parse '{timeoutStr}' as a valid duration. Try using a syntax such as \"3h5m\" (i.e. 3 hours and 5 minutes)."); + if (timeoutPeriod.TotalHours > 100000) { - // todo: we should parse date in the command parser - var timeoutStr = _newTimeout; - var timeoutPeriod = DateUtils.ParsePeriod(timeoutStr); - if (timeoutPeriod == null) throw new PKError($"Could not parse '{timeoutStr}' as a valid duration. Try using a syntax such as \"3h5m\" (i.e. 3 hours and 5 minutes)."); - if (timeoutPeriod.Value.TotalHours > 100000) - { - // sanity check to prevent seconds overflow if someone types in 999999999 - overflow = timeoutPeriod.Value; - newTimeout = Duration.Zero; - } - else newTimeout = timeoutPeriod; + // sanity check to prevent seconds overflow if someone types in 999999999 + overflow = timeoutPeriod; + newTimeout = Duration.Zero; } + else newTimeout = timeoutPeriod; - await ctx.Repository.UpdateSystemConfig(ctx.System.Id, new() { LatchTimeout = (int?)newTimeout?.TotalSeconds }); + await ctx.Repository.UpdateSystemConfig(ctx.System.Id, new() { LatchTimeout = (int?)newTimeout.TotalSeconds }); - if (newTimeout == null) - await ctx.Reply($"{Emojis.Success} Latch timeout reset to default ({ProxyMatcher.DefaultLatchExpiryTime.ToTimeSpan().Humanize(4)})."); - else if (newTimeout == Duration.Zero && overflow != Duration.Zero) + if (newTimeout == Duration.Zero && overflow != Duration.Zero) await ctx.Reply($"{Emojis.Success} Latch timeout disabled. Latch mode autoproxy will never time out. ({overflow.ToTimeSpan().Humanize(4)} is too long)"); else if (newTimeout == Duration.Zero) await ctx.Reply($"{Emojis.Success} Latch timeout disabled. Latch mode autoproxy will never time out."); else - await ctx.Reply($"{Emojis.Success} Latch timeout set to {newTimeout.Value!.ToTimeSpan().Humanize(4)}."); + await ctx.Reply($"{Emojis.Success} Latch timeout set to {newTimeout.ToTimeSpan().Humanize(4)}."); } public async Task SystemTimezone(Context ctx) diff --git a/PluralKit.Bot/Commands/Member.cs b/PluralKit.Bot/Commands/Member.cs index a8fd478f..a3fbefbf 100644 --- a/PluralKit.Bot/Commands/Member.cs +++ b/PluralKit.Bot/Commands/Member.cs @@ -28,10 +28,8 @@ public class Member _avatarHosting = avatarHosting; } - public async Task NewMember(Context ctx) + public async Task NewMember(Context ctx, string? memberName) { - var memberName = await ctx.ParamResolveOpaque("name"); - if (ctx.System == null) throw Errors.NoSystemError(ctx.DefaultPrefix); memberName = memberName ?? throw new PKSyntaxError("You must pass a member name."); @@ -122,19 +120,17 @@ public class Member await ctx.Reply(replyStr); } - public async Task ViewMember(Context ctx) + public async Task ViewMember(Context ctx, PKMember target) { - var target = await ctx.ParamResolveMember("target"); var system = await ctx.Repository.GetSystem(target.System); await ctx.Reply( embed: await _embeds.CreateMemberEmbed(system, target, ctx.Guild, ctx.Config, ctx.LookupContextFor(system.Id), ctx.Zone)); } - public async Task Soulscream(Context ctx) + public async Task Soulscream(Context ctx, PKMember target) { // this is for a meme, please don't take this code seriously. :) - var target = await ctx.ParamResolveMember("target"); var name = target.NameFor(ctx.LookupContextFor(target.System)); var encoded = HttpUtility.UrlEncode(name); diff --git a/PluralKit.Bot/Handlers/MessageCreated.cs b/PluralKit.Bot/Handlers/MessageCreated.cs index 594b178a..cd16d6c9 100644 --- a/PluralKit.Bot/Handlers/MessageCreated.cs +++ b/PluralKit.Bot/Handlers/MessageCreated.cs @@ -159,7 +159,19 @@ public class MessageCreated: IEventHandler } var ctx = new Context(_services, shardId, guild, channel, evt, cmdStart, system, config, guildConfig, _config.Prefixes ?? BotConfig.DefaultPrefixes, parameters); - await _tree.ExecuteCommand(ctx); + + Commands command; + try + { + command = await Commands.FromContext(ctx); + } + catch (PKError e) + { + await ctx.Reply($"{Emojis.Error} {e.Message}"); + throw; + } + + await _tree.ExecuteCommand(ctx, command); } catch (PKError) { diff --git a/crates/command_definitions/src/config.rs b/crates/command_definitions/src/config.rs index b2f5b80d..ab185663 100644 --- a/crates/command_definitions/src/config.rs +++ b/crates/command_definitions/src/config.rs @@ -1,29 +1,25 @@ +use command_parser::parameter; + use super::*; pub fn cmds() -> impl Iterator { - let cfg = ["config", "cfg"]; - let autoproxy = ["autoproxy", "ap"]; + let ap = tokens!(["config", "cfg"], ["autoproxy", "ap"]); + + let ap_account = concat_tokens!(ap, [["account", "ac"]]); + let ap_timeout = concat_tokens!(ap, [["timeout", "tm"]]); [ - command!([cfg, autoproxy, ["account", "ac"]], "cfg_ap_account_show") + command!(ap_account => "cfg_ap_account_show") .help("Shows autoproxy status for the account"), - command!( - [cfg, autoproxy, ["account", "ac"], Toggle], - "cfg_ap_account_update" - ) - .help("Toggles autoproxy for the account"), - command!([cfg, autoproxy, ["timeout", "tm"]], "cfg_ap_timeout_show") - .help("Shows the autoproxy timeout"), - command!( - [ - cfg, - autoproxy, - ["timeout", "tm"], - any!(Disable, Reset, ("timeout", OpaqueString::SINGLE)) // todo: we should parse duration / time values - ], - "cfg_ap_timeout_update" - ) - .help("Sets the autoproxy timeout"), + command!(ap_account, Toggle => "cfg_ap_account_update") + .help("Toggles autoproxy for the account"), + command!(ap_timeout => "cfg_ap_timeout_show").help("Shows the autoproxy timeout"), + command!(ap_timeout, parameter::RESET => "cfg_ap_timeout_reset") + .help("Resets the autoproxy timeout"), + command!(ap_timeout, parameter::DISABLE => "cfg_ap_timeout_off") + .help("Disables the autoproxy timeout"), + command!(ap_timeout, ("timeout", OpaqueString) => "cfg_ap_timeout_update") + .help("Sets the autoproxy timeout"), ] .into_iter() } diff --git a/crates/command_definitions/src/fun.rs b/crates/command_definitions/src/fun.rs index 720eadc0..63ec0054 100644 --- a/crates/command_definitions/src/fun.rs +++ b/crates/command_definitions/src/fun.rs @@ -2,8 +2,8 @@ use super::*; pub fn cmds() -> impl Iterator { [ - command!(["thunder"], "fun_thunder"), - command!(["meow"], "fun_meow"), + command!(["thunder"] => "fun_thunder"), + command!(["meow"] => "fun_meow"), ] .into_iter() } diff --git a/crates/command_definitions/src/help.rs b/crates/command_definitions/src/help.rs index 411b3565..2719fa9d 100644 --- a/crates/command_definitions/src/help.rs +++ b/crates/command_definitions/src/help.rs @@ -3,9 +3,9 @@ use super::*; pub fn cmds() -> impl Iterator { let help = ["help", "h"]; [ - command!([help], "help").help("Shows the help command"), - command!([help, "commands"], "help_commands").help("help commands"), - command!([help, "proxy"], "help_proxy").help("help proxy"), + command!([help] => "help").help("Shows the help command"), + command!([help, "commands"] => "help_commands").help("help commands"), + command!([help, "proxy"] => "help_proxy").help("help proxy"), ] .into_iter() } diff --git a/crates/command_definitions/src/lib.rs b/crates/command_definitions/src/lib.rs index 9e8adb49..96df8091 100644 --- a/crates/command_definitions/src/lib.rs +++ b/crates/command_definitions/src/lib.rs @@ -18,7 +18,9 @@ pub mod server_config; pub mod switch; pub mod system; -use command_parser::{any, command, command::Command, parameter::*}; +use command_parser::{ + command, command::Command, concat_tokens, parameter::ParameterKind::*, tokens, +}; pub fn all() -> impl Iterator { (help::cmds()) diff --git a/crates/command_definitions/src/member.rs b/crates/command_definitions/src/member.rs index 5346bd53..5ab968b9 100644 --- a/crates/command_definitions/src/member.rs +++ b/crates/command_definitions/src/member.rs @@ -6,38 +6,27 @@ pub fn cmds() -> impl Iterator { let privacy = ["privacy", "priv"]; let new = ["new", "n"]; + let member_target = tokens!(member, MemberRef); + let member_desc = concat_tokens!(member_target, [description]); + let member_privacy = concat_tokens!(member_target, [privacy]); + [ - command!([member, new, ("name", OpaqueString::SINGLE)], "member_new") + command!([member, new, ("name", OpaqueString)] => "member_new") .help("Creates a new system member"), - command!([member, MemberRef], "member_show") - .help("Shows information about a member") - .value_flag("pt", Disable), - command!([member, MemberRef, description], "member_desc_show") - .help("Shows a member's description"), - command!( - [ - member, - MemberRef, - description, - ("description", OpaqueString::REMAINDER) - ], - "member_desc_update" - ) - .help("Changes a member's description"), - command!([member, MemberRef, privacy], "member_privacy_show") + command!(member_target => "member_show") + .flag("pt") + .help("Shows information about a member"), + command!(member_desc => "member_desc_show").help("Shows a member's description"), + command!(member_desc, ("description", OpaqueStringRemainder) => "member_desc_update") + .help("Changes a member's description"), + command!(member_privacy => "member_privacy_show") .help("Displays a member's current privacy settings"), command!( - [ - member, - MemberRef, - privacy, - MemberPrivacyTarget, - ("new_privacy_level", PrivacyLevel) - ], - "member_privacy_update" + member_privacy, MemberPrivacyTarget, ("new_privacy_level", PrivacyLevel) + => "member_privacy_update" ) .help("Changes a member's privacy settings"), - command!([member, MemberRef, "soulscream"], "member_soulscream").show_in_suggestions(false), + command!(member_target, "soulscream" => "member_soulscream").show_in_suggestions(false), ] .into_iter() } diff --git a/crates/command_definitions/src/system.rs b/crates/command_definitions/src/system.rs index fd2148a3..6093f6f1 100644 --- a/crates/command_definitions/src/system.rs +++ b/crates/command_definitions/src/system.rs @@ -4,11 +4,13 @@ pub fn cmds() -> impl Iterator { let system = ["system", "s"]; let new = ["new", "n"]; + let system_new = tokens!(system, new); + [ - command!([system], "system_show").help("Shows information about your system"), - command!([system, new], "system_new").help("Creates a new system"), - command!([system, new, ("name", OpaqueString::SINGLE)], "system_new") - .help("Creates a new system"), + command!([system] => "system_show").help("Shows information about your system"), + command!(system_new => "system_new").help("Creates a new system"), + command!(system_new, ("name", OpaqueString) => "system_new_name") + .help("Creates a new system (using the provided name)"), ] .into_iter() } diff --git a/crates/command_parser/src/command.rs b/crates/command_parser/src/command.rs index 0410834b..35bd9b10 100644 --- a/crates/command_parser/src/command.rs +++ b/crates/command_parser/src/command.rs @@ -26,7 +26,7 @@ impl Command { for (idx, token) in tokens.iter().enumerate().rev() { match token { // we want flags to go before any parameters - Token::Parameter(_, _) | Token::Any(_) => { + Token::Parameter(_) => { parse_flags_before = idx; was_parameter = true; } @@ -62,7 +62,7 @@ impl Command { self } - pub fn value_flag(mut self, name: impl Into, value: impl Parameter + 'static) -> Self { + pub fn value_flag(mut self, name: impl Into, value: ParameterKind) -> Self { self.flags.push(Flag::new(name).with_value(value)); self } @@ -95,7 +95,27 @@ impl Display for Command { // (and something like &dyn Trait would require everything to be referenced which doesnt look nice anyway) #[macro_export] macro_rules! command { - ([$($v:expr),+], $cb:expr$(,)*) => { - $crate::command::Command::new([$($crate::token::Token::from($v)),*], $cb) + ([$($v:expr),+] => $cb:expr$(,)*) => { + $crate::command::Command::new($crate::tokens!($($v),+), $cb) + }; + ($tokens:expr => $cb:expr$(,)*) => { + $crate::command::Command::new($tokens.clone(), $cb) + }; + ($tokens:expr, $($v:expr),+ => $cb:expr$(,)*) => { + $crate::command::Command::new($crate::concat_tokens!($tokens.clone(), [$($v),+]), $cb) + }; +} + +#[macro_export] +macro_rules! tokens { + ($($v:expr),+$(,)*) => { + [$($crate::token::Token::from($v)),+] + }; +} + +#[macro_export] +macro_rules! concat_tokens { + ($tokens:expr, [$($v:expr),+]$(,)*) => { + $tokens.clone().into_iter().chain($crate::tokens!($($v),+).into_iter()).collect::>() }; } diff --git a/crates/command_parser/src/flag.rs b/crates/command_parser/src/flag.rs index 6bd12f2b..e0ff236c 100644 --- a/crates/command_parser/src/flag.rs +++ b/crates/command_parser/src/flag.rs @@ -1,8 +1,8 @@ -use std::{fmt::Display, sync::Arc}; +use std::fmt::Display; use smol_str::SmolStr; -use crate::parameter::{Parameter, ParameterValue}; +use crate::parameter::{ParameterKind, ParameterValue}; #[derive(Debug)] pub enum FlagValueMatchError { @@ -13,7 +13,7 @@ pub enum FlagValueMatchError { #[derive(Debug, Clone)] pub struct Flag { name: SmolStr, - value: Option>, + value: Option, } impl Display for Flag { @@ -42,8 +42,8 @@ impl Flag { } } - pub fn with_value(mut self, param: impl Parameter + 'static) -> Self { - self.value = Some(Arc::new(param)); + pub fn with_value(mut self, param: ParameterKind) -> Self { + self.value = Some(param); self } @@ -51,13 +51,17 @@ impl Flag { &self.name } + pub fn value_kind(&self) -> Option { + self.value + } + pub fn try_match(&self, input_name: &str, input_value: Option<&str>) -> TryMatchFlagResult { // if not matching flag then skip anymore matching if self.name != input_name { return None; } // get token to try matching with, if flag doesn't have one then that means it is matched (it is without any value) - let Some(value) = self.value.as_deref() else { + let Some(value) = self.value.as_ref() else { return Some(Ok(None)); }; // check if we have a non-empty flag value, we return error if not (because flag requested a value) diff --git a/crates/command_parser/src/lib.rs b/crates/command_parser/src/lib.rs index 5da79c36..d152a581 100644 --- a/crates/command_parser/src/lib.rs +++ b/crates/command_parser/src/lib.rs @@ -77,21 +77,6 @@ pub fn parse_command( TokenMatchError::MissingParameter { name } => { format!("Expected parameter `{name}` in command `{prefix}{input} {token}`.") } - TokenMatchError::MissingAny { tokens } => { - let mut msg = format!("Expected one of "); - for (idx, token) in tokens.iter().enumerate() { - write!(&mut msg, "`{token}`").expect("oom"); - if idx < tokens.len() - 1 { - if tokens.len() > 2 && idx == tokens.len() - 2 { - msg.push_str(" or "); - } else { - msg.push_str(", "); - } - } - } - write!(&mut msg, " in command `{prefix}{input} {token}`.").expect("oom"); - msg - } TokenMatchError::ParameterMatchError { input: raw, msg } => { format!("Parameter `{raw}` in command `{prefix}{input}` could not be parsed: {msg}.") } @@ -254,12 +239,9 @@ fn next_token<'a>( // iterate over tokens and run try_match for token in possible_tokens { let is_match_remaining_token = - |token: &Token| matches!(token, Token::Parameter(_, param) if param.remainder()); + |token: &Token| matches!(token, Token::Parameter(param) if param.kind().remainder()); // check if this is a token that matches the rest of the input - let match_remaining = is_match_remaining_token(token) - // check for Any here if it has a "match remainder" token in it - // if there is a "match remainder" token in a command there shouldn't be a command descending from that - || matches!(token, Token::Any(ref tokens) if tokens.iter().any(is_match_remaining_token)); + let match_remaining = is_match_remaining_token(token); // either use matched param or rest of the input if matching remaining let input_to_match = matched.as_ref().map(|v| { match_remaining diff --git a/crates/command_parser/src/parameter.rs b/crates/command_parser/src/parameter.rs index ff67ee7e..c1ad77b7 100644 --- a/crates/command_parser/src/parameter.rs +++ b/crates/command_parser/src/parameter.rs @@ -2,89 +2,108 @@ use std::{fmt::Debug, str::FromStr}; use smol_str::SmolStr; -use crate::token::ParamName; - #[derive(Debug, Clone)] pub enum ParameterValue { + OpaqueString(String), MemberRef(String), SystemRef(String), MemberPrivacyTarget(String), PrivacyLevel(String), - OpaqueString(String), Toggle(bool), } -pub trait Parameter: Debug + Send + Sync { - fn remainder(&self) -> bool { - false - } - fn default_name(&self) -> ParamName; - fn format(&self, f: &mut std::fmt::Formatter, name: &str) -> std::fmt::Result; - fn match_value(&self, input: &str) -> Result; +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Parameter { + name: SmolStr, + kind: ParameterKind, } -#[derive(Debug, Clone, Eq, Hash, PartialEq)] -pub struct OpaqueString(bool); - -impl OpaqueString { - pub const SINGLE: Self = Self(false); - pub const REMAINDER: Self = Self(true); -} - -impl Parameter for OpaqueString { - fn remainder(&self) -> bool { - self.0 +impl Parameter { + pub fn name(&self) -> &str { + &self.name } - fn default_name(&self) -> ParamName { - "string" - } - - fn format(&self, f: &mut std::fmt::Formatter, name: &str) -> std::fmt::Result { - write!(f, "[{name}]") - } - - fn match_value(&self, input: &str) -> Result { - Ok(ParameterValue::OpaqueString(input.into())) + pub fn kind(&self) -> ParameterKind { + self.kind } } -#[derive(Debug, Clone, Eq, Hash, PartialEq)] -pub struct MemberRef; - -impl Parameter for MemberRef { - fn default_name(&self) -> ParamName { - "member" - } - - fn format(&self, f: &mut std::fmt::Formatter, _: &str) -> std::fmt::Result { - write!(f, "") - } - - fn match_value(&self, input: &str) -> Result { - Ok(ParameterValue::MemberRef(input.into())) +impl From for Parameter { + fn from(value: ParameterKind) -> Self { + Parameter { + name: value.default_name().into(), + kind: value, + } } } -#[derive(Debug, Clone, Eq, Hash, PartialEq)] -pub struct SystemRef; - -impl Parameter for SystemRef { - fn default_name(&self) -> ParamName { - "system" - } - - fn format(&self, f: &mut std::fmt::Formatter, _: &str) -> std::fmt::Result { - write!(f, "") - } - - fn match_value(&self, input: &str) -> Result { - Ok(ParameterValue::SystemRef(input.into())) +impl From<(&str, ParameterKind)> for Parameter { + fn from((name, kind): (&str, ParameterKind)) -> Self { + Parameter { + name: name.into(), + kind, + } } } -#[derive(Debug, Clone, Eq, Hash, PartialEq)] -pub struct MemberPrivacyTarget; +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum ParameterKind { + OpaqueString, + OpaqueStringRemainder, + MemberRef, + SystemRef, + MemberPrivacyTarget, + PrivacyLevel, + Toggle, +} + +impl ParameterKind { + pub(crate) fn default_name(&self) -> &str { + match self { + ParameterKind::OpaqueString => "string", + ParameterKind::OpaqueStringRemainder => "string", + ParameterKind::MemberRef => "target", + ParameterKind::SystemRef => "target", + ParameterKind::MemberPrivacyTarget => "member_privacy_target", + ParameterKind::PrivacyLevel => "privacy_level", + ParameterKind::Toggle => "toggle", + } + } + + pub(crate) fn remainder(&self) -> bool { + matches!(self, ParameterKind::OpaqueStringRemainder) + } + + pub(crate) fn format(&self, f: &mut std::fmt::Formatter, param_name: &str) -> std::fmt::Result { + match self { + ParameterKind::OpaqueString | ParameterKind::OpaqueStringRemainder => { + write!(f, "[{param_name}]") + } + ParameterKind::MemberRef => write!(f, ""), + ParameterKind::SystemRef => write!(f, ""), + ParameterKind::MemberPrivacyTarget => write!(f, ""), + ParameterKind::PrivacyLevel => write!(f, "[privacy level]"), + ParameterKind::Toggle => write!(f, "on/off"), + } + } + + pub(crate) fn match_value(&self, input: &str) -> Result { + match self { + ParameterKind::OpaqueString | ParameterKind::OpaqueStringRemainder => { + Ok(ParameterValue::OpaqueString(input.into())) + } + ParameterKind::MemberRef => Ok(ParameterValue::MemberRef(input.into())), + ParameterKind::SystemRef => Ok(ParameterValue::SystemRef(input.into())), + ParameterKind::MemberPrivacyTarget => MemberPrivacyTargetKind::from_str(input) + .map(|target| ParameterValue::MemberPrivacyTarget(target.as_ref().into())), + ParameterKind::PrivacyLevel => PrivacyLevelKind::from_str(input) + .map(|level| ParameterValue::PrivacyLevel(level.as_ref().into())), + ParameterKind::Toggle => { + Toggle::from_str(input).map(|t| ParameterValue::Toggle(t.into())) + } + } + } +} pub enum MemberPrivacyTargetKind { Visibility, @@ -135,24 +154,6 @@ impl FromStr for MemberPrivacyTargetKind { } } -impl Parameter for MemberPrivacyTarget { - fn default_name(&self) -> ParamName { - "member_privacy_target" - } - - fn format(&self, f: &mut std::fmt::Formatter, _: &str) -> std::fmt::Result { - write!(f, "") - } - - fn match_value(&self, input: &str) -> Result { - MemberPrivacyTargetKind::from_str(input) - .map(|target| ParameterValue::MemberPrivacyTarget(target.as_ref().into())) - } -} - -#[derive(Debug, Clone, Eq, Hash, PartialEq)] -pub struct PrivacyLevel; - pub enum PrivacyLevelKind { Public, Private, @@ -179,140 +180,34 @@ impl FromStr for PrivacyLevelKind { } } -impl Parameter for PrivacyLevel { - fn default_name(&self) -> ParamName { - "privacy_level" - } - - fn format(&self, f: &mut std::fmt::Formatter, _: &str) -> std::fmt::Result { - write!(f, "[privacy level]") - } - - fn match_value(&self, input: &str) -> Result { - PrivacyLevelKind::from_str(input) - .map(|level| ParameterValue::PrivacyLevel(level.as_ref().into())) - } -} +pub const ENABLE: [&str; 5] = ["on", "yes", "true", "enable", "enabled"]; +pub const DISABLE: [&str; 5] = ["off", "no", "false", "disable", "disabled"]; #[derive(Debug, Clone, Eq, Hash, PartialEq)] -pub struct Reset; - -impl AsRef for Reset { - fn as_ref(&self) -> &str { - "reset" - } +pub enum Toggle { + On, + Off, } -impl FromStr for Reset { +impl FromStr for Toggle { type Err = SmolStr; fn from_str(s: &str) -> Result { match s { - "reset" | "clear" | "default" => Ok(Self), - _ => Err("not reset".into()), + ref s if ENABLE.contains(s) => Ok(Self::On), + ref s if DISABLE.contains(s) => Ok(Self::Off), + _ => Err("invalid toggle, must be on/off".into()), } } } -impl Parameter for Reset { - fn default_name(&self) -> ParamName { - "reset" - } - - fn format(&self, f: &mut std::fmt::Formatter, _: &str) -> std::fmt::Result { - write!(f, "reset") - } - - fn match_value(&self, input: &str) -> Result { - Self::from_str(input).map(|_| ParameterValue::Toggle(true)) - } -} - -#[derive(Debug, Clone, Eq, Hash, PartialEq)] -pub struct Toggle; - -impl Parameter for Toggle { - fn default_name(&self) -> ParamName { - "toggle" - } - - fn format(&self, f: &mut std::fmt::Formatter, _: &str) -> std::fmt::Result { - write!(f, "on/off") - } - - fn match_value(&self, input: &str) -> Result { - Enable::from_str(input) - .map(Into::::into) - .or_else(|_| Disable::from_str(input).map(Into::::into)) - .map(ParameterValue::Toggle) - .map_err(|_| "invalid toggle".into()) - } -} - -#[derive(Debug, Clone, Eq, Hash, PartialEq)] -pub struct Enable; - -impl FromStr for Enable { - type Err = SmolStr; - - fn from_str(s: &str) -> Result { - match s { - "on" | "yes" | "true" | "enable" | "enabled" => Ok(Self), - _ => Err("invalid enable".into()), - } - } -} - -impl Parameter for Enable { - fn default_name(&self) -> ParamName { - "enable" - } - - fn format(&self, f: &mut std::fmt::Formatter, _: &str) -> std::fmt::Result { - write!(f, "on") - } - - fn match_value(&self, input: &str) -> Result { - Self::from_str(input).map(|e| ParameterValue::Toggle(e.into())) - } -} - -impl Into for Enable { +impl Into for Toggle { fn into(self) -> bool { - true - } -} - -#[derive(Debug, Clone, Eq, Hash, PartialEq)] -pub struct Disable; - -impl FromStr for Disable { - type Err = SmolStr; - - fn from_str(s: &str) -> Result { - match s { - "off" | "no" | "false" | "disable" | "disabled" => Ok(Self), - _ => Err("invalid disable".into()), + match self { + Toggle::On => true, + Toggle::Off => false, } } } -impl Into for Disable { - fn into(self) -> bool { - false - } -} - -impl Parameter for Disable { - fn default_name(&self) -> ParamName { - "disable" - } - - fn format(&self, f: &mut std::fmt::Formatter, _: &str) -> std::fmt::Result { - write!(f, "off") - } - - fn match_value(&self, input: &str) -> Result { - Self::from_str(input).map(|e| ParameterValue::Toggle(e.into())) - } -} +pub const RESET: [&str; 3] = ["reset", "clear", "default"]; diff --git a/crates/command_parser/src/token.rs b/crates/command_parser/src/token.rs index aae2f06e..4d90c2ec 100644 --- a/crates/command_parser/src/token.rs +++ b/crates/command_parser/src/token.rs @@ -1,76 +1,35 @@ use std::{ fmt::{Debug, Display}, - hash::Hash, ops::Not, - sync::Arc, }; use smol_str::SmolStr; -use crate::parameter::{Parameter, ParameterValue}; +use crate::parameter::{Parameter, ParameterKind, ParameterValue}; -pub type ParamName = &'static str; - -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum Token { /// Token used to represent a finished command (i.e. no more parameters required) // todo: this is likely not the right way to represent this Empty, - /// multi-token matching - /// todo: FullString tokens don't work properly in this (they don't get passed the rest of the input) - Any(Vec), - /// A bot-defined command / subcommand (usually) (eg. "member" in `pk;member MyName`) Value(Vec), /// A parameter that must be provided a value - Parameter(ParamName, Arc), -} - -#[macro_export] -macro_rules! any { - ($($t:expr),+) => { - $crate::token::Token::Any(vec![$($crate::token::Token::from($t)),+]) - }; -} - -impl PartialEq for Token { - fn eq(&self, other: &Self) -> bool { - match (self, other) { - (Self::Any(l0), Self::Any(r0)) => l0 == r0, - (Self::Value(l0), Self::Value(r0)) => l0 == r0, - (Self::Parameter(l0, _), Self::Parameter(r0, _)) => l0 == r0, - (Self::Empty, Self::Empty) => true, - _ => false, - } - } -} -impl Eq for Token {} - -impl Hash for Token { - fn hash(&self, state: &mut H) { - core::mem::discriminant(self).hash(state); - match self { - Token::Empty => {} - Token::Any(vec) => vec.hash(state), - Token::Value(vec) => vec.hash(state), - Token::Parameter(name, _) => name.hash(state), - } - } + Parameter(Parameter), } #[derive(Debug)] pub enum TokenMatchError { ParameterMatchError { input: SmolStr, msg: SmolStr }, - MissingParameter { name: ParamName }, - MissingAny { tokens: Vec }, + MissingParameter { name: SmolStr }, } #[derive(Debug)] pub(super) struct TokenMatchValue { pub raw: SmolStr, - pub param: Option<(ParamName, ParameterValue)>, + pub param: Option<(SmolStr, ParameterValue)>, } impl TokenMatchValue { @@ -83,12 +42,12 @@ impl TokenMatchValue { fn new_match_param( raw: impl Into, - param_name: ParamName, + param_name: impl Into, param: ParameterValue, ) -> TryMatchResult { Some(Ok(Some(Self { raw: raw.into(), - param: Some((param_name, param)), + param: Some((param_name.into(), param)), }))) } } @@ -113,14 +72,9 @@ impl Token { // empty token Self::Empty => Some(Ok(None)), // missing paramaters - Self::Parameter(name, _) => { - Some(Err(TokenMatchError::MissingParameter { name })) - } - Self::Any(tokens) => tokens.is_empty().then_some(None).unwrap_or_else(|| { - Some(Err(TokenMatchError::MissingAny { - tokens: tokens.clone(), - })) - }), + Self::Parameter(param) => Some(Err(TokenMatchError::MissingParameter { + name: param.name().into(), + })), // everything else doesnt match if no input anyway Self::Value(_) => None, // don't add a _ match here! @@ -132,18 +86,13 @@ impl Token { // try actually matching stuff match self { Self::Empty => None, - Self::Any(tokens) => tokens - .iter() - .map(|t| t.try_match(Some(input))) - .find(|r| !matches!(r, None)) - .unwrap_or(None), Self::Value(values) => values .iter() .any(|v| v.eq(input)) .then(|| TokenMatchValue::new_match(input)) .unwrap_or(None), - Self::Parameter(name, param) => match param.match_value(input) { - Ok(matched) => TokenMatchValue::new_match_param(input, name, matched), + Self::Parameter(param) => match param.kind().match_value(input) { + Ok(matched) => TokenMatchValue::new_match_param(input, param.name(), matched), Err(err) => Some(Err(TokenMatchError::ParameterMatchError { input: input.into(), msg: err, @@ -157,19 +106,9 @@ impl Display for Token { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Token::Empty => write!(f, ""), - Token::Any(vec) => { - write!(f, "(")?; - for (i, token) in vec.iter().enumerate() { - if i != 0 { - write!(f, "|")?; - } - write!(f, "{}", token)?; - } - write!(f, ")") - } Token::Value(vec) if vec.is_empty().not() => write!(f, "{}", vec.first().unwrap()), Token::Value(_) => Ok(()), // if value token has no values (lol), don't print anything - Token::Parameter(name, param) => param.format(f, name), + Token::Parameter(param) => param.kind().format(f, param.name()), } } } @@ -186,14 +125,20 @@ impl From<[&str; L]> for Token { } } -impl From

for Token { - fn from(value: P) -> Self { - Token::Parameter(value.default_name(), Arc::new(value)) +impl From for Token { + fn from(value: Parameter) -> Self { + Token::Parameter(value) } } -impl From<(ParamName, P)> for Token { - fn from(value: (ParamName, P)) -> Self { - Token::Parameter(value.0, Arc::new(value.1)) +impl From for Token { + fn from(value: ParameterKind) -> Self { + Token::from(Parameter::from(value)) + } +} + +impl From<(&str, ParameterKind)> for Token { + fn from(value: (&str, ParameterKind)) -> Self { + Token::from(Parameter::from(value)) } } diff --git a/crates/command_parser/src/tree.rs b/crates/command_parser/src/tree.rs index 5f78eff6..53057a69 100644 --- a/crates/command_parser/src/tree.rs +++ b/crates/command_parser/src/tree.rs @@ -38,15 +38,15 @@ impl TreeBranch { ); } - pub(super) fn command(&self) -> Option { + pub fn command(&self) -> Option { self.current_command.clone() } - pub(super) fn possible_tokens(&self) -> impl Iterator { + pub fn possible_tokens(&self) -> impl Iterator { self.branches.keys() } - pub(super) fn possible_commands(&self, max_depth: usize) -> impl Iterator { + pub fn possible_commands(&self, max_depth: usize) -> impl Iterator { // dusk: i am too lazy to write an iterator for this without using recursion so we box everything fn box_iter<'a>( iter: impl Iterator + 'a, @@ -69,7 +69,11 @@ impl TreeBranch { commands } - pub(super) fn get_branch(&self, token: &Token) -> Option<&TreeBranch> { + pub fn get_branch(&self, token: &Token) -> Option<&Self> { self.branches.get(token) } + + pub fn branches(&self) -> impl Iterator { + self.branches.iter() + } } diff --git a/crates/commands/Cargo.toml b/crates/commands/Cargo.toml index 46bca6a7..e853ff83 100644 --- a/crates/commands/Cargo.toml +++ b/crates/commands/Cargo.toml @@ -2,6 +2,11 @@ name = "commands" version = "0.1.0" edition = "2021" +default-run = "commands" + +[[bin]] +name = "write_cs_glue" +path = "src/bin/write_cs_glue.rs" [lib] crate-type = ["cdylib", "lib"] diff --git a/crates/commands/src/main.rs b/crates/commands/src/main.rs index 313c6d74..2c863447 100644 --- a/crates/commands/src/main.rs +++ b/crates/commands/src/main.rs @@ -1,5 +1,8 @@ #![feature(iter_intersperse)] +use command_parser::{token::Token, Tree}; +use commands::COMMAND_TREE; + fn main() { let cmd = std::env::args() .skip(1) @@ -18,3 +21,21 @@ fn main() { } } } + +fn print_tree(tree: &Tree, depth: usize) { + println!(); + for (token, branch) in tree.branches() { + for _ in 0..depth { + print!(" "); + } + for _ in 0..depth { + print!("-"); + } + print!("> {token:?}"); + if matches!(token, Token::Empty) { + println!(": {}", branch.command().unwrap().cb) + } else { + print_tree(branch, depth + 1) + } + } +} diff --git a/flake.nix b/flake.nix index 6eed4bb1..5debf1e1 100644 --- a/flake.nix +++ b/flake.nix @@ -96,6 +96,8 @@ cp -f "$commandslib" obj/ fi uniffi-bindgen-cs "$commandslib" --library --out-dir="''${2:-./PluralKit.Bot}" + cargo run --package commands --bin write_cs_glue -- "''${2:-./PluralKit.Bot}"/commandtypes.cs + dotnet format ./PluralKit.Bot/PluralKit.Bot.csproj ''; }; }; From bf5e448aad096baf3de3b9f340f873d118449299 Mon Sep 17 00:00:00 2001 From: dusk Date: Tue, 21 Jan 2025 23:57:10 +0900 Subject: [PATCH 061/179] fix: add the helper methods for resolving flag values --- .../CommandSystem/Context/ContextFlagsExt.cs | 54 +++++++++++++++++++ crates/command_definitions/src/help.rs | 4 +- 2 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 PluralKit.Bot/CommandSystem/Context/ContextFlagsExt.cs diff --git a/PluralKit.Bot/CommandSystem/Context/ContextFlagsExt.cs b/PluralKit.Bot/CommandSystem/Context/ContextFlagsExt.cs new file mode 100644 index 00000000..564a5f30 --- /dev/null +++ b/PluralKit.Bot/CommandSystem/Context/ContextFlagsExt.cs @@ -0,0 +1,54 @@ +using PluralKit.Core; + +namespace PluralKit.Bot; + +public static class ContextFlagsExt +{ + public static async Task FlagResolveOpaque(this Context ctx, string param_name) + { + return await ctx.Parameters.ResolveFlag( + ctx, param_name, + param => (param as Parameter.Opaque)?.value + ); + } + + public static async Task FlagResolveMember(this Context ctx, string param_name) + { + return await ctx.Parameters.ResolveFlag( + ctx, param_name, + param => (param as Parameter.MemberRef)?.member + ); + } + + public static async Task FlagResolveSystem(this Context ctx, string param_name) + { + return await ctx.Parameters.ResolveFlag( + ctx, param_name, + param => (param as Parameter.SystemRef)?.system + ); + } + + public static async Task FlagResolveMemberPrivacyTarget(this Context ctx, string param_name) + { + return await ctx.Parameters.ResolveFlag( + ctx, param_name, + param => (param as Parameter.MemberPrivacyTarget)?.target + ); + } + + public static async Task FlagResolvePrivacyLevel(this Context ctx, string param_name) + { + return await ctx.Parameters.ResolveFlag( + ctx, param_name, + param => (param as Parameter.PrivacyLevel)?.level + ); + } + + public static async Task FlagResolveToggle(this Context ctx, string param_name) + { + return await ctx.Parameters.ResolveFlag( + ctx, param_name, + param => (param as Parameter.Toggle)?.value + ); + } +} \ No newline at end of file diff --git a/crates/command_definitions/src/help.rs b/crates/command_definitions/src/help.rs index 2719fa9d..7b206e03 100644 --- a/crates/command_definitions/src/help.rs +++ b/crates/command_definitions/src/help.rs @@ -3,7 +3,9 @@ use super::*; pub fn cmds() -> impl Iterator { let help = ["help", "h"]; [ - command!([help] => "help").help("Shows the help command"), + command!([help] => "help") + .value_flag("foo", OpaqueString) // todo: just for testing + .help("Shows the help command"), command!([help, "commands"] => "help_commands").help("help commands"), command!([help, "proxy"] => "help_proxy").help("help proxy"), ] From ff6dc12cae29e1e1bdbbb5615942dc9ba3f4772f Mon Sep 17 00:00:00 2001 From: dusk Date: Wed, 22 Jan 2025 00:50:17 +0900 Subject: [PATCH 062/179] feat(command_parser): allow aliases in flags --- crates/command_definitions/src/help.rs | 2 +- crates/command_parser/src/command.rs | 11 ++---- crates/command_parser/src/flag.rs | 49 +++++++++++++++++++++++--- crates/command_parser/src/lib.rs | 6 ++-- 4 files changed, 51 insertions(+), 17 deletions(-) diff --git a/crates/command_definitions/src/help.rs b/crates/command_definitions/src/help.rs index 7b206e03..d18cca3e 100644 --- a/crates/command_definitions/src/help.rs +++ b/crates/command_definitions/src/help.rs @@ -4,7 +4,7 @@ pub fn cmds() -> impl Iterator { let help = ["help", "h"]; [ command!([help] => "help") - .value_flag("foo", OpaqueString) // todo: just for testing + .flag(("foo", OpaqueString)) // todo: just for testing .help("Shows the help command"), command!([help, "commands"] => "help_commands").help("help commands"), command!([help, "proxy"] => "help_proxy").help("help proxy"), diff --git a/crates/command_parser/src/command.rs b/crates/command_parser/src/command.rs index 35bd9b10..504cab9e 100644 --- a/crates/command_parser/src/command.rs +++ b/crates/command_parser/src/command.rs @@ -2,7 +2,7 @@ use std::fmt::{Debug, Display}; use smol_str::SmolStr; -use crate::{flag::Flag, parameter::*, token::Token}; +use crate::{flag::Flag, token::Token}; #[derive(Debug, Clone)] pub struct Command { @@ -57,13 +57,8 @@ impl Command { self } - pub fn flag(mut self, name: impl Into) -> Self { - self.flags.push(Flag::new(name)); - self - } - - pub fn value_flag(mut self, name: impl Into, value: ParameterKind) -> Self { - self.flags.push(Flag::new(name).with_value(value)); + pub fn flag(mut self, flag: impl Into) -> Self { + self.flags.push(flag.into()); self } } diff --git a/crates/command_parser/src/flag.rs b/crates/command_parser/src/flag.rs index e0ff236c..11eae9b3 100644 --- a/crates/command_parser/src/flag.rs +++ b/crates/command_parser/src/flag.rs @@ -13,6 +13,7 @@ pub enum FlagValueMatchError { #[derive(Debug, Clone)] pub struct Flag { name: SmolStr, + aliases: Vec, value: Option, } @@ -38,26 +39,36 @@ impl Flag { pub fn new(name: impl Into) -> Self { Self { name: name.into(), + aliases: Vec::new(), value: None, } } - pub fn with_value(mut self, param: ParameterKind) -> Self { + pub fn value(mut self, param: ParameterKind) -> Self { self.value = Some(param); self } - pub fn name(&self) -> &str { + pub fn alias(mut self, alias: impl Into) -> Self { + self.aliases.push(alias.into()); + self + } + + pub fn get_name(&self) -> &str { &self.name } - pub fn value_kind(&self) -> Option { + pub fn get_value(&self) -> Option { self.value } + pub fn get_aliases(&self) -> impl Iterator { + self.aliases.iter().map(|s| s.as_str()) + } + pub fn try_match(&self, input_name: &str, input_value: Option<&str>) -> TryMatchFlagResult { - // if not matching flag then skip anymore matching - if self.name != input_name { + // if not matching the name or any aliases then skip anymore matching + if self.name != input_name && self.get_aliases().all(|s| s.ne(input_name)) { return None; } // get token to try matching with, if flag doesn't have one then that means it is matched (it is without any value) @@ -82,3 +93,31 @@ impl Flag { } } } + +impl From<&str> for Flag { + fn from(name: &str) -> Self { + Flag::new(name) + } +} + +impl From<(&str, ParameterKind)> for Flag { + fn from((name, value): (&str, ParameterKind)) -> Self { + Flag::new(name).value(value) + } +} + +impl From<[&str; L]> for Flag { + fn from(value: [&str; L]) -> Self { + let mut flag = Flag::new(value[0]); + for alias in &value[1..] { + flag = flag.alias(*alias); + } + flag + } +} + +impl From<([&str; L], ParameterKind)> for Flag { + fn from((names, value): ([&str; L], ParameterKind)) -> Self { + Flag::from(names).value(value) + } +} diff --git a/crates/command_parser/src/lib.rs b/crates/command_parser/src/lib.rs index d152a581..1e7c94ea 100644 --- a/crates/command_parser/src/lib.rs +++ b/crates/command_parser/src/lib.rs @@ -133,7 +133,7 @@ pub fn parse_command( FlagMatchError::ValueMatchFailed(FlagValueMatchError::ValueMissing) => { format!( "Flag `-{name}` in command `{prefix}{input}` is missing a value, try passing `{flag}`.", - name = flag.name() + name = flag.get_name() ) } FlagMatchError::ValueMatchFailed( @@ -141,7 +141,7 @@ pub fn parse_command( ) => { format!( "Flag `-{name}` in command `{prefix}{input}` has a value (`{raw}`) that could not be parsed: {msg}.", - name = flag.name() + name = flag.get_name() ) } }; @@ -210,7 +210,7 @@ fn match_flag<'a>( for flag in possible_flags { println!("matching flag {flag:?}"); match flag.try_match(matched_flag.name, matched_flag.value) { - Some(Ok(param)) => return Some(Ok((flag.name().into(), param))), + Some(Ok(param)) => return Some(Ok((flag.get_name().into(), param))), Some(Err(err)) => return Some(Err((flag, err))), None => {} } From 35f7bdbaf5080f21edaedb59cc2757cb2bee25d1 Mon Sep 17 00:00:00 2001 From: dusk Date: Wed, 22 Jan 2025 02:12:17 +0900 Subject: [PATCH 063/179] refactor(command_parser): separate Token::Value field into name and aliases --- crates/command_parser/src/command.rs | 2 +- crates/command_parser/src/token.rs | 31 ++++++++++++++++------------ 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/crates/command_parser/src/command.rs b/crates/command_parser/src/command.rs index 504cab9e..027e162f 100644 --- a/crates/command_parser/src/command.rs +++ b/crates/command_parser/src/command.rs @@ -30,7 +30,7 @@ impl Command { parse_flags_before = idx; was_parameter = true; } - Token::Empty | Token::Value(_) => { + Token::Empty | Token::Value { .. } => { if was_parameter { break; } diff --git a/crates/command_parser/src/token.rs b/crates/command_parser/src/token.rs index 4d90c2ec..7188012e 100644 --- a/crates/command_parser/src/token.rs +++ b/crates/command_parser/src/token.rs @@ -1,7 +1,4 @@ -use std::{ - fmt::{Debug, Display}, - ops::Not, -}; +use std::fmt::{Debug, Display}; use smol_str::SmolStr; @@ -14,7 +11,10 @@ pub enum Token { Empty, /// A bot-defined command / subcommand (usually) (eg. "member" in `pk;member MyName`) - Value(Vec), + Value { + name: SmolStr, + aliases: Vec, + }, /// A parameter that must be provided a value Parameter(Parameter), @@ -76,7 +76,7 @@ impl Token { name: param.name().into(), })), // everything else doesnt match if no input anyway - Self::Value(_) => None, + Self::Value { .. } => None, // don't add a _ match here! }; } @@ -86,8 +86,7 @@ impl Token { // try actually matching stuff match self { Self::Empty => None, - Self::Value(values) => values - .iter() + Self::Value { name, aliases } => (aliases.iter().chain(std::iter::once(name))) .any(|v| v.eq(input)) .then(|| TokenMatchValue::new_match(input)) .unwrap_or(None), @@ -106,22 +105,28 @@ impl Display for Token { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Token::Empty => write!(f, ""), - Token::Value(vec) if vec.is_empty().not() => write!(f, "{}", vec.first().unwrap()), - Token::Value(_) => Ok(()), // if value token has no values (lol), don't print anything + Token::Value { name, .. } => write!(f, "{name}"), Token::Parameter(param) => param.kind().format(f, param.name()), } } } impl From<&str> for Token { - fn from(value: &str) -> Self { - Token::Value(vec![SmolStr::new(value)]) + fn from(name: &str) -> Self { + Token::Value { + name: SmolStr::new(name), + aliases: Vec::new(), + } } } impl From<[&str; L]> for Token { fn from(value: [&str; L]) -> Self { - Token::Value(value.into_iter().map(SmolStr::from).collect::>()) + assert!(value.len() > 0, "can't create a Token::Value from nothing"); + Token::Value { + name: value[0].into(), + aliases: value.into_iter().skip(1).map(SmolStr::new).collect::>(), + } } } From f804e7629fd1e3775537000a7e4a829c93220664 Mon Sep 17 00:00:00 2001 From: dusk Date: Fri, 24 Jan 2025 01:57:13 +0900 Subject: [PATCH 064/179] fix(commands): add csharp glue codegen binary, it was gitignored --- crates/commands/src/bin/write_cs_glue.rs | 173 +++++++++++++++++++++++ 1 file changed, 173 insertions(+) create mode 100644 crates/commands/src/bin/write_cs_glue.rs diff --git a/crates/commands/src/bin/write_cs_glue.rs b/crates/commands/src/bin/write_cs_glue.rs new file mode 100644 index 00000000..baa825c0 --- /dev/null +++ b/crates/commands/src/bin/write_cs_glue.rs @@ -0,0 +1,173 @@ +use std::{env, fmt::Write, fs, path::PathBuf, str::FromStr}; + +use command_parser::{ + parameter::{Parameter, ParameterKind}, + token::Token, +}; + +fn main() -> Result<(), Box> { + let write_location = env::args() + .nth(1) + .expect("file location should be provided"); + let write_location = PathBuf::from_str(&write_location).unwrap(); + + let commands = command_definitions::all().collect::>(); + + let mut glue = String::new(); + + writeln!(&mut glue, "#nullable enable\n")?; + writeln!(&mut glue, "using PluralKit.Core;\n")?; + writeln!(&mut glue, "namespace PluralKit.Bot;\n")?; + + let mut record_fields = String::new(); + for command in &commands { + writeln!( + &mut record_fields, + r#"public record {command_name}({command_name}Params parameters, {command_name}Flags flags): Commands;"#, + command_name = command_callback_to_name(&command.cb), + )?; + } + let mut match_branches = String::new(); + for command in &commands { + let mut command_params_init = String::new(); + let command_params = find_parameters(&command.tokens); + for param in &command_params { + writeln!( + &mut command_params_init, + r#"{name} = await ctx.ParamResolve{extract_fn_name}("{name}") ?? throw new PKError("this is a bug"),"#, + name = param.name(), + extract_fn_name = get_param_param_ty(param.kind()), + )?; + } + let mut command_flags_init = String::new(); + for flag in &command.flags { + if let Some(kind) = flag.get_value() { + writeln!( + &mut command_flags_init, + r#"{name} = await ctx.FlagResolve{extract_fn_name}("{name}"),"#, + name = flag.get_name(), + extract_fn_name = get_param_param_ty(kind), + )?; + } else { + writeln!( + &mut command_flags_init, + r#"{name} = ctx.Parameters.HasFlag("{name}"),"#, + name = flag.get_name(), + )?; + } + } + write!( + &mut match_branches, + r#" + "{command_callback}" => new {command_name}( + new {command_name}Params {{ {command_params_init} }}, + new {command_name}Flags {{ {command_flags_init} }} + ), + "#, + command_name = command_callback_to_name(&command.cb), + command_callback = command.cb, + )?; + } + write!( + &mut glue, + r#" + public abstract record Commands() + {{ + {record_fields} + + public static async Task FromContext(Context ctx) + {{ + return ctx.Parameters.Callback() switch + {{ + {match_branches} + _ => null, + }}; + }} + }} + "#, + )?; + for command in &commands { + let mut command_params_fields = String::new(); + let command_params = find_parameters(&command.tokens); + for param in &command_params { + writeln!( + &mut command_params_fields, + r#"public required {ty} {name};"#, + name = param.name(), + ty = get_param_ty(param.kind()), + )?; + } + let mut command_flags_fields = String::new(); + for flag in &command.flags { + if let Some(kind) = flag.get_value() { + writeln!( + &mut command_flags_fields, + r#"public {ty}? {name};"#, + name = flag.get_name(), + ty = get_param_ty(kind), + )?; + } else { + writeln!( + &mut command_flags_fields, + r#"public required bool {name};"#, + name = flag.get_name(), + )?; + } + } + write!( + &mut glue, + r#" + public class {command_name}Params + {{ + {command_params_fields} + }} + public class {command_name}Flags + {{ + {command_flags_fields} + }} + "#, + command_name = command_callback_to_name(&command.cb), + )?; + } + fs::write(write_location, glue)?; + Ok(()) +} + +fn command_callback_to_name(cb: &str) -> String { + cb.split("_") + .map(|w| w.chars().nth(0).unwrap().to_uppercase().collect::() + &w[1..]) + .collect() +} + +fn get_param_ty(kind: ParameterKind) -> &'static str { + match kind { + ParameterKind::OpaqueString | ParameterKind::OpaqueStringRemainder => "string", + ParameterKind::MemberRef => "PKMember", + ParameterKind::SystemRef => "PKSystem", + ParameterKind::MemberPrivacyTarget => "MemberPrivacySubject", + ParameterKind::PrivacyLevel => "string", + ParameterKind::Toggle => "bool", + } +} + +fn get_param_param_ty(kind: ParameterKind) -> &'static str { + match kind { + ParameterKind::OpaqueString | ParameterKind::OpaqueStringRemainder => "Opaque", + ParameterKind::MemberRef => "Member", + ParameterKind::SystemRef => "System", + ParameterKind::MemberPrivacyTarget => "MemberPrivacyTarget", + ParameterKind::PrivacyLevel => "PrivacyLevel", + ParameterKind::Toggle => "Toggle", + } +} + +fn find_parameters(tokens: &[Token]) -> Vec<&Parameter> { + let mut result = Vec::new(); + for token in tokens { + match token { + Token::Parameter(param) => result.push(param), + _ => {} + } + } + result +} From 071db3d6d64799ec6386e93c94a8564bf8ba4f07 Mon Sep 17 00:00:00 2001 From: dusk Date: Fri, 24 Jan 2025 04:08:59 +0900 Subject: [PATCH 065/179] refactor(command_parser): simplify how tokens are defined in commands --- crates/command_definitions/src/config.rs | 11 +-- crates/command_definitions/src/fun.rs | 4 +- crates/command_definitions/src/help.rs | 8 +-- crates/command_definitions/src/lib.rs | 6 +- crates/command_definitions/src/member.rs | 14 ++-- crates/command_definitions/src/system.rs | 6 +- crates/command_parser/src/command.rs | 22 +----- crates/command_parser/src/parameter.rs | 27 ++++--- crates/command_parser/src/token.rs | 92 ++++++++++++++++++------ 9 files changed, 114 insertions(+), 76 deletions(-) diff --git a/crates/command_definitions/src/config.rs b/crates/command_definitions/src/config.rs index ab185663..d2ca10e1 100644 --- a/crates/command_definitions/src/config.rs +++ b/crates/command_definitions/src/config.rs @@ -3,10 +3,11 @@ use command_parser::parameter; use super::*; pub fn cmds() -> impl Iterator { - let ap = tokens!(["config", "cfg"], ["autoproxy", "ap"]); + let cfg = ("config", ["cfg"]); + let ap = tokens!(cfg, ("autoproxy", ["ap"])); - let ap_account = concat_tokens!(ap, [["account", "ac"]]); - let ap_timeout = concat_tokens!(ap, [["timeout", "tm"]]); + let ap_account = tokens!(ap, ("account", ["ac"])); + let ap_timeout = tokens!(ap, ("timeout", ["tm"])); [ command!(ap_account => "cfg_ap_account_show") @@ -14,9 +15,9 @@ pub fn cmds() -> impl Iterator { command!(ap_account, Toggle => "cfg_ap_account_update") .help("Toggles autoproxy for the account"), command!(ap_timeout => "cfg_ap_timeout_show").help("Shows the autoproxy timeout"), - command!(ap_timeout, parameter::RESET => "cfg_ap_timeout_reset") + command!(ap_timeout, ("reset", ["clear", "default"]) => "cfg_ap_timeout_reset") .help("Resets the autoproxy timeout"), - command!(ap_timeout, parameter::DISABLE => "cfg_ap_timeout_off") + command!(ap_timeout, parameter::Toggle::Off => "cfg_ap_timeout_off") .help("Disables the autoproxy timeout"), command!(ap_timeout, ("timeout", OpaqueString) => "cfg_ap_timeout_update") .help("Sets the autoproxy timeout"), diff --git a/crates/command_definitions/src/fun.rs b/crates/command_definitions/src/fun.rs index 63ec0054..4119b71d 100644 --- a/crates/command_definitions/src/fun.rs +++ b/crates/command_definitions/src/fun.rs @@ -2,8 +2,8 @@ use super::*; pub fn cmds() -> impl Iterator { [ - command!(["thunder"] => "fun_thunder"), - command!(["meow"] => "fun_meow"), + command!("thunder" => "fun_thunder"), + command!("meow" => "fun_meow"), ] .into_iter() } diff --git a/crates/command_definitions/src/help.rs b/crates/command_definitions/src/help.rs index d18cca3e..dd5942cd 100644 --- a/crates/command_definitions/src/help.rs +++ b/crates/command_definitions/src/help.rs @@ -1,13 +1,13 @@ use super::*; pub fn cmds() -> impl Iterator { - let help = ["help", "h"]; + let help = ("help", ["h"]); [ - command!([help] => "help") + command!(help => "help") .flag(("foo", OpaqueString)) // todo: just for testing .help("Shows the help command"), - command!([help, "commands"] => "help_commands").help("help commands"), - command!([help, "proxy"] => "help_proxy").help("help proxy"), + command!(help, "commands" => "help_commands").help("help commands"), + command!(help, "proxy" => "help_proxy").help("help proxy"), ] .into_iter() } diff --git a/crates/command_definitions/src/lib.rs b/crates/command_definitions/src/lib.rs index 96df8091..d25f1daf 100644 --- a/crates/command_definitions/src/lib.rs +++ b/crates/command_definitions/src/lib.rs @@ -18,9 +18,7 @@ pub mod server_config; pub mod switch; pub mod system; -use command_parser::{ - command, command::Command, concat_tokens, parameter::ParameterKind::*, tokens, -}; +use command_parser::{command, command::Command, parameter::ParameterKind::*, tokens}; pub fn all() -> impl Iterator { (help::cmds()) @@ -29,3 +27,5 @@ pub fn all() -> impl Iterator { .chain(config::cmds()) .chain(fun::cmds()) } + +pub const RESET: (&str, [&str; 2]) = ("reset", ["clear", "default"]); diff --git a/crates/command_definitions/src/member.rs b/crates/command_definitions/src/member.rs index 5ab968b9..504995d4 100644 --- a/crates/command_definitions/src/member.rs +++ b/crates/command_definitions/src/member.rs @@ -1,17 +1,17 @@ use super::*; pub fn cmds() -> impl Iterator { - let member = ["member", "m"]; - let description = ["description", "desc"]; - let privacy = ["privacy", "priv"]; - let new = ["new", "n"]; + let member = ("member", ["m"]); + let description = ("description", ["desc"]); + let privacy = ("privacy", ["priv"]); + let new = ("new", ["n"]); let member_target = tokens!(member, MemberRef); - let member_desc = concat_tokens!(member_target, [description]); - let member_privacy = concat_tokens!(member_target, [privacy]); + let member_desc = tokens!(member_target, description); + let member_privacy = tokens!(member_target, privacy); [ - command!([member, new, ("name", OpaqueString)] => "member_new") + command!(member, new, ("name", OpaqueString) => "member_new") .help("Creates a new system member"), command!(member_target => "member_show") .flag("pt") diff --git a/crates/command_definitions/src/system.rs b/crates/command_definitions/src/system.rs index 6093f6f1..da1d85f1 100644 --- a/crates/command_definitions/src/system.rs +++ b/crates/command_definitions/src/system.rs @@ -1,13 +1,13 @@ use super::*; pub fn cmds() -> impl Iterator { - let system = ["system", "s"]; - let new = ["new", "n"]; + let system = ("system", ["s"]); + let new = ("new", ["n"]); let system_new = tokens!(system, new); [ - command!([system] => "system_show").help("Shows information about your system"), + command!(system => "system_show").help("Shows information about your system"), command!(system_new => "system_new").help("Creates a new system"), command!(system_new, ("name", OpaqueString) => "system_new_name") .help("Creates a new system (using the provided name)"), diff --git a/crates/command_parser/src/command.rs b/crates/command_parser/src/command.rs index 027e162f..b7fd65ce 100644 --- a/crates/command_parser/src/command.rs +++ b/crates/command_parser/src/command.rs @@ -90,27 +90,7 @@ impl Display for Command { // (and something like &dyn Trait would require everything to be referenced which doesnt look nice anyway) #[macro_export] macro_rules! command { - ([$($v:expr),+] => $cb:expr$(,)*) => { + ($($v:expr),+ => $cb:expr$(,)*) => { $crate::command::Command::new($crate::tokens!($($v),+), $cb) }; - ($tokens:expr => $cb:expr$(,)*) => { - $crate::command::Command::new($tokens.clone(), $cb) - }; - ($tokens:expr, $($v:expr),+ => $cb:expr$(,)*) => { - $crate::command::Command::new($crate::concat_tokens!($tokens.clone(), [$($v),+]), $cb) - }; -} - -#[macro_export] -macro_rules! tokens { - ($($v:expr),+$(,)*) => { - [$($crate::token::Token::from($v)),+] - }; -} - -#[macro_export] -macro_rules! concat_tokens { - ($tokens:expr, [$($v:expr),+]$(,)*) => { - $tokens.clone().into_iter().chain($crate::tokens!($($v),+).into_iter()).collect::>() - }; } diff --git a/crates/command_parser/src/parameter.rs b/crates/command_parser/src/parameter.rs index c1ad77b7..297b91e9 100644 --- a/crates/command_parser/src/parameter.rs +++ b/crates/command_parser/src/parameter.rs @@ -2,6 +2,8 @@ use std::{fmt::Debug, str::FromStr}; use smol_str::SmolStr; +use crate::token::Token; + #[derive(Debug, Clone)] pub enum ParameterValue { OpaqueString(String), @@ -180,10 +182,7 @@ impl FromStr for PrivacyLevelKind { } } -pub const ENABLE: [&str; 5] = ["on", "yes", "true", "enable", "enabled"]; -pub const DISABLE: [&str; 5] = ["off", "no", "false", "disable", "disabled"]; - -#[derive(Debug, Clone, Eq, Hash, PartialEq)] +#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq)] pub enum Toggle { On, Off, @@ -193,10 +192,20 @@ impl FromStr for Toggle { type Err = SmolStr; fn from_str(s: &str) -> Result { - match s { - ref s if ENABLE.contains(s) => Ok(Self::On), - ref s if DISABLE.contains(s) => Ok(Self::Off), - _ => Err("invalid toggle, must be on/off".into()), + let matches_self = + |toggle: &Self| matches!(Token::from(*toggle).try_match(Some(s)), Some(Ok(None))); + [Self::On, Self::Off] + .into_iter() + .find(matches_self) + .ok_or_else(|| SmolStr::new("invalid toggle, must be on/off")) + } +} + +impl From for Token { + fn from(toggle: Toggle) -> Self { + match toggle { + Toggle::On => Self::from(("on", ["yes", "true", "enable", "enabled"])), + Toggle::Off => Self::from(("off", ["no", "false", "disable", "disabled"])), } } } @@ -209,5 +218,3 @@ impl Into for Toggle { } } } - -pub const RESET: [&str; 3] = ["reset", "clear", "default"]; diff --git a/crates/command_parser/src/token.rs b/crates/command_parser/src/token.rs index 7188012e..79300279 100644 --- a/crates/command_parser/src/token.rs +++ b/crates/command_parser/src/token.rs @@ -104,46 +104,96 @@ impl Token { impl Display for Token { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - Token::Empty => write!(f, ""), - Token::Value { name, .. } => write!(f, "{name}"), - Token::Parameter(param) => param.kind().format(f, param.name()), + Self::Empty => write!(f, ""), + Self::Value { name, .. } => write!(f, "{name}"), + Self::Parameter(param) => param.kind().format(f, param.name()), + } + } +} + +impl From<(&str, [&str; L])> for Token { + fn from((name, aliases): (&str, [&str; L])) -> Self { + Self::Value { + name: name.into(), + aliases: aliases.into_iter().map(SmolStr::new).collect::>(), } } } impl From<&str> for Token { - fn from(name: &str) -> Self { - Token::Value { - name: SmolStr::new(name), - aliases: Vec::new(), - } - } -} - -impl From<[&str; L]> for Token { - fn from(value: [&str; L]) -> Self { - assert!(value.len() > 0, "can't create a Token::Value from nothing"); - Token::Value { - name: value[0].into(), - aliases: value.into_iter().skip(1).map(SmolStr::new).collect::>(), - } + fn from(value: &str) -> Self { + Self::from((value, [])) } } impl From for Token { fn from(value: Parameter) -> Self { - Token::Parameter(value) + Self::Parameter(value) } } impl From for Token { fn from(value: ParameterKind) -> Self { - Token::from(Parameter::from(value)) + Self::from(Parameter::from(value)) } } impl From<(&str, ParameterKind)> for Token { fn from(value: (&str, ParameterKind)) -> Self { - Token::from(Parameter::from(value)) + Self::from(Parameter::from(value)) } } + +#[derive(Debug, Clone)] +pub struct TokensIterator { + inner: Vec, +} + +impl Iterator for TokensIterator { + type Item = Token; + + fn next(&mut self) -> Option { + (self.inner.len() > 0).then(|| self.inner.remove(0)) + } +} + +impl> From for TokensIterator { + fn from(value: T) -> Self { + Self { + inner: vec![value.into()], + } + } +} + +impl From<[Token; L]> for TokensIterator { + fn from(value: [Token; L]) -> Self { + Self { + inner: value.into_iter().collect(), + } + } +} + +impl From<[Self; L]> for TokensIterator { + fn from(value: [Self; L]) -> Self { + Self { + inner: value + .into_iter() + .map(|t| t.collect::>()) + .flatten() + .collect(), + } + } +} + +impl From> for TokensIterator { + fn from(value: Vec) -> Self { + Self { inner: value } + } +} + +#[macro_export] +macro_rules! tokens { + ($($v:expr),+$(,)*) => { + $crate::token::TokensIterator::from([$($crate::token::TokensIterator::from($v.clone())),+]) + }; +} From 92276a720e92a51ea657b96df24fc4a411a5d497 Mon Sep 17 00:00:00 2001 From: dusk Date: Fri, 24 Jan 2025 04:13:06 +0900 Subject: [PATCH 066/179] refactor(command_parser): remove the Empty token, we don't need it --- crates/command_parser/src/command.rs | 2 +- crates/command_parser/src/token.rs | 8 -------- crates/command_parser/src/tree.rs | 12 +++--------- crates/commands/src/main.rs | 6 +++--- 4 files changed, 7 insertions(+), 21 deletions(-) diff --git a/crates/command_parser/src/command.rs b/crates/command_parser/src/command.rs index b7fd65ce..6810a82d 100644 --- a/crates/command_parser/src/command.rs +++ b/crates/command_parser/src/command.rs @@ -30,7 +30,7 @@ impl Command { parse_flags_before = idx; was_parameter = true; } - Token::Empty | Token::Value { .. } => { + Token::Value { .. } => { if was_parameter { break; } diff --git a/crates/command_parser/src/token.rs b/crates/command_parser/src/token.rs index 79300279..b737a8cd 100644 --- a/crates/command_parser/src/token.rs +++ b/crates/command_parser/src/token.rs @@ -6,10 +6,6 @@ use crate::parameter::{Parameter, ParameterKind, ParameterValue}; #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum Token { - /// Token used to represent a finished command (i.e. no more parameters required) - // todo: this is likely not the right way to represent this - Empty, - /// A bot-defined command / subcommand (usually) (eg. "member" in `pk;member MyName`) Value { name: SmolStr, @@ -69,8 +65,6 @@ impl Token { None => { // short circuit on: return match self { - // empty token - Self::Empty => Some(Ok(None)), // missing paramaters Self::Parameter(param) => Some(Err(TokenMatchError::MissingParameter { name: param.name().into(), @@ -85,7 +79,6 @@ impl Token { // try actually matching stuff match self { - Self::Empty => None, Self::Value { name, aliases } => (aliases.iter().chain(std::iter::once(name))) .any(|v| v.eq(input)) .then(|| TokenMatchValue::new_match(input)) @@ -104,7 +97,6 @@ impl Token { impl Display for Token { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - Self::Empty => write!(f, ""), Self::Value { name, .. } => write!(f, "{name}"), Self::Parameter(param) => param.kind().format(f, param.name()), } diff --git a/crates/command_parser/src/tree.rs b/crates/command_parser/src/tree.rs index 53057a69..9b1466a3 100644 --- a/crates/command_parser/src/tree.rs +++ b/crates/command_parser/src/tree.rs @@ -28,14 +28,8 @@ impl TreeBranch { .entry(token) .or_insert_with(TreeBranch::default); } - // when we're out of tokens, add an Empty branch with the callback and no sub-branches - current_branch.branches.insert( - Token::Empty, - TreeBranch { - branches: OrderMap::new(), - current_command: Some(command), - }, - ); + // when we're out of tokens add the command to the last branch + current_branch.current_command = Some(command); } pub fn command(&self) -> Option { @@ -61,7 +55,7 @@ impl TreeBranch { for branch in self.branches.values() { if let Some(command) = branch.current_command.as_ref() { commands = box_iter(commands.chain(std::iter::once(command))); - // we dont need to look further if we found a command (only Empty tokens have commands) + // we dont need to look further if we found a command continue; } commands = box_iter(commands.chain(branch.possible_commands(max_depth - 1))); diff --git a/crates/commands/src/main.rs b/crates/commands/src/main.rs index 2c863447..376d2469 100644 --- a/crates/commands/src/main.rs +++ b/crates/commands/src/main.rs @@ -1,6 +1,6 @@ #![feature(iter_intersperse)] -use command_parser::{token::Token, Tree}; +use command_parser::Tree; use commands::COMMAND_TREE; fn main() { @@ -32,8 +32,8 @@ fn print_tree(tree: &Tree, depth: usize) { print!("-"); } print!("> {token:?}"); - if matches!(token, Token::Empty) { - println!(": {}", branch.command().unwrap().cb) + if let Some(command) = branch.command() { + println!(": {}", command.cb) } else { print_tree(branch, depth + 1) } From 58d493ac0ab38f10f3b4be102aa39d5ef78a6c5e Mon Sep 17 00:00:00 2001 From: dusk Date: Fri, 24 Jan 2025 04:40:07 +0900 Subject: [PATCH 067/179] refactor(command_parser): combine TokenMatchError and TokenMatchedValue into a single result type --- crates/command_parser/src/lib.rs | 73 +++++++++++++------------- crates/command_parser/src/parameter.rs | 10 ++-- crates/command_parser/src/token.rs | 71 ++++++++++--------------- 3 files changed, 70 insertions(+), 84 deletions(-) diff --git a/crates/command_parser/src/lib.rs b/crates/command_parser/src/lib.rs index 1e7c94ea..460a556a 100644 --- a/crates/command_parser/src/lib.rs +++ b/crates/command_parser/src/lib.rs @@ -18,7 +18,7 @@ use flag::{Flag, FlagMatchError, FlagValueMatchError}; use parameter::ParameterValue; use smol_str::SmolStr; use string::MatchedFlag; -use token::{Token, TokenMatchError, TokenMatchValue}; +use token::{Token, TokenMatchResult}; // todo: this should come from the bot probably const MAX_SUGGESTIONS: usize = 7; @@ -55,40 +55,42 @@ pub fn parse_command( let next = next_token(local_tree.possible_tokens(), &input, current_pos); println!("next: {:?}", next); match next { - Some(Ok((found_token, arg, new_pos))) => { - current_pos = new_pos; - current_token_idx += 1; - - if let Some(arg) = arg.as_ref() { - // insert arg as paramater if this is a parameter - if let Some((param_name, param)) = arg.param.as_ref() { - params.insert(param_name.to_string(), param.clone()); + Some((found_token, result, new_pos)) => { + match &result { + // todo: better error messages for these? + TokenMatchResult::MissingParameter { name } => { + return Err(format!("Expected parameter `{name}` in command `{prefix}{input} {found_token}`.")); } + TokenMatchResult::ParameterMatchError { input: raw, msg } => { + return Err(format!("Parameter `{raw}` in command `{prefix}{input}` could not be parsed: {msg}.")); + } + // don't use a catch-all here, we want to make sure compiler errors when new errors are added + TokenMatchResult::MatchedParameter { .. } | TokenMatchResult::MatchedValue => {} } + // add parameter if any + if let TokenMatchResult::MatchedParameter { name, value } = result { + params.insert(name.to_string(), value); + } + + // move to the next branch if let Some(next_tree) = local_tree.get_branch(&found_token) { local_tree = next_tree.clone(); } else { panic!("found token {found_token:?} could not match tree, at {input}"); } - } - Some(Err((token, err))) => { - let error_msg = match err { - TokenMatchError::MissingParameter { name } => { - format!("Expected parameter `{name}` in command `{prefix}{input} {token}`.") - } - TokenMatchError::ParameterMatchError { input: raw, msg } => { - format!("Parameter `{raw}` in command `{prefix}{input}` could not be parsed: {msg}.") - } - }; - return Err(error_msg); + + // advance our position on the input + current_pos = new_pos; + current_token_idx += 1; } None => { - // if it said command not found on a flag, output better error message let mut error = format!("Unknown command `{prefix}{input}`."); - if fmt_possible_commands(&mut error, &prefix, local_tree.possible_commands(2)).not() + if fmt_possible_commands(&mut error, &prefix, local_tree.possible_commands(1)).not() { + // add a space between the unknown command and "for a list of all possible commands" + // message if we didn't add any possible suggestions error.push_str(" "); } @@ -231,7 +233,7 @@ fn next_token<'a>( possible_tokens: impl Iterator, input: &str, current_pos: usize, -) -> Option, usize), (&'a Token, TokenMatchError)>> { +) -> Option<(&'a Token, TokenMatchResult, usize)> { // get next parameter, matching quotes let matched = string::next_param(&input, current_pos); println!("matched: {matched:?}\n---"); @@ -248,21 +250,18 @@ fn next_token<'a>( .then_some(&input[current_pos..]) .unwrap_or(v.value) }); + let next_pos = match matched { + // return last possible pos if we matched remaining, + Some(_) if match_remaining => input.len(), + // otherwise use matched param next pos, + Some(ref param) => param.next_pos, + // and if didnt match anything we stay where we are + None => current_pos, + }; match token.try_match(input_to_match) { - Some(Ok(value)) => { - println!("matched token: {}", token); - let next_pos = match matched { - // return last possible pos if we matched remaining, - Some(_) if match_remaining => input.len(), - // otherwise use matched param next pos, - Some(param) => param.next_pos, - // and if didnt match anything we stay where we are - None => current_pos, - }; - return Some(Ok((token, value, next_pos))); - } - Some(Err(err)) => { - return Some(Err((token, err))); + Some(result) => { + //println!("matched token: {}", token); + return Some((token, result, next_pos)); } None => {} // continue matching until we exhaust all tokens } diff --git a/crates/command_parser/src/parameter.rs b/crates/command_parser/src/parameter.rs index 297b91e9..f7ad07a9 100644 --- a/crates/command_parser/src/parameter.rs +++ b/crates/command_parser/src/parameter.rs @@ -2,7 +2,7 @@ use std::{fmt::Debug, str::FromStr}; use smol_str::SmolStr; -use crate::token::Token; +use crate::token::{Token, TokenMatchResult}; #[derive(Debug, Clone)] pub enum ParameterValue { @@ -192,8 +192,12 @@ impl FromStr for Toggle { type Err = SmolStr; fn from_str(s: &str) -> Result { - let matches_self = - |toggle: &Self| matches!(Token::from(*toggle).try_match(Some(s)), Some(Ok(None))); + let matches_self = |toggle: &Self| { + matches!( + Token::from(*toggle).try_match(Some(s)), + Some(TokenMatchResult::MatchedValue) + ) + }; [Self::On, Self::Off] .into_iter() .find(matches_self) diff --git a/crates/command_parser/src/token.rs b/crates/command_parser/src/token.rs index b737a8cd..0d0fce2f 100644 --- a/crates/command_parser/src/token.rs +++ b/crates/command_parser/src/token.rs @@ -17,46 +17,26 @@ pub enum Token { } #[derive(Debug)] -pub enum TokenMatchError { - ParameterMatchError { input: SmolStr, msg: SmolStr }, - MissingParameter { name: SmolStr }, +pub enum TokenMatchResult { + MatchedValue, + MatchedParameter { + name: SmolStr, + value: ParameterValue, + }, + ParameterMatchError { + input: SmolStr, + msg: SmolStr, + }, + MissingParameter { + name: SmolStr, + }, } -#[derive(Debug)] -pub(super) struct TokenMatchValue { - pub raw: SmolStr, - pub param: Option<(SmolStr, ParameterValue)>, -} - -impl TokenMatchValue { - fn new_match(raw: impl Into) -> TryMatchResult { - Some(Ok(Some(Self { - raw: raw.into(), - param: None, - }))) - } - - fn new_match_param( - raw: impl Into, - param_name: impl Into, - param: ParameterValue, - ) -> TryMatchResult { - Some(Ok(Some(Self { - raw: raw.into(), - param: Some((param_name.into(), param)), - }))) - } -} - -/// None -> no match -/// Some(Ok(None)) -> match, no value -/// Some(Ok(Some(_))) -> match, with value -/// Some(Err(_)) -> error while matching -// q: why do this while we could have a NoMatch in TokenMatchError? +// q: why not have a NoMatch variant in TokenMatchResult? // a: because we want to differentiate between no match and match failure (it matched with an error) // "no match" has a different charecteristic because we want to continue matching other tokens... // ...while "match failure" means we should stop matching and return the error -type TryMatchResult = Option, TokenMatchError>>; +type TryMatchResult = Option; impl Token { pub(super) fn try_match(&self, input: Option<&str>) -> TryMatchResult { @@ -66,9 +46,9 @@ impl Token { // short circuit on: return match self { // missing paramaters - Self::Parameter(param) => Some(Err(TokenMatchError::MissingParameter { + Self::Parameter(param) => Some(TokenMatchResult::MissingParameter { name: param.name().into(), - })), + }), // everything else doesnt match if no input anyway Self::Value { .. } => None, // don't add a _ match here! @@ -81,15 +61,18 @@ impl Token { match self { Self::Value { name, aliases } => (aliases.iter().chain(std::iter::once(name))) .any(|v| v.eq(input)) - .then(|| TokenMatchValue::new_match(input)) - .unwrap_or(None), - Self::Parameter(param) => match param.kind().match_value(input) { - Ok(matched) => TokenMatchValue::new_match_param(input, param.name(), matched), - Err(err) => Some(Err(TokenMatchError::ParameterMatchError { + .then(|| TokenMatchResult::MatchedValue), + Self::Parameter(param) => Some(match param.kind().match_value(input) { + Ok(matched) => TokenMatchResult::MatchedParameter { + name: param.name().into(), + value: matched, + }, + Err(err) => TokenMatchResult::ParameterMatchError { input: input.into(), msg: err, - })), - }, // don't add a _ match here! + }, + }), + // don't add a _ match here! } } } From e3778b9bb8b6c4bfa9341bd528df41b4e09cce6b Mon Sep 17 00:00:00 2001 From: dusk Date: Fri, 24 Jan 2025 05:16:15 +0900 Subject: [PATCH 068/179] refactor(command_parser): move parameter fmt to Parameter types Display impl --- crates/command_parser/src/flag.rs | 4 +-- crates/command_parser/src/parameter.rs | 33 +++++++++++--------- crates/command_parser/src/token.rs | 42 ++++++++++++++------------ 3 files changed, 43 insertions(+), 36 deletions(-) diff --git a/crates/command_parser/src/flag.rs b/crates/command_parser/src/flag.rs index 11eae9b3..fbeb0b1b 100644 --- a/crates/command_parser/src/flag.rs +++ b/crates/command_parser/src/flag.rs @@ -2,7 +2,7 @@ use std::fmt::Display; use smol_str::SmolStr; -use crate::parameter::{ParameterKind, ParameterValue}; +use crate::parameter::{Parameter, ParameterKind, ParameterValue}; #[derive(Debug)] pub enum FlagValueMatchError { @@ -22,7 +22,7 @@ impl Display for Flag { write!(f, "-{}", self.name)?; if let Some(value) = self.value.as_ref() { write!(f, "=")?; - value.format(f, value.default_name())?; + Parameter::from(*value).fmt(f)?; } Ok(()) } diff --git a/crates/command_parser/src/parameter.rs b/crates/command_parser/src/parameter.rs index f7ad07a9..b2c9d5ff 100644 --- a/crates/command_parser/src/parameter.rs +++ b/crates/command_parser/src/parameter.rs @@ -1,4 +1,7 @@ -use std::{fmt::Debug, str::FromStr}; +use std::{ + fmt::{Debug, Display}, + str::FromStr, +}; use smol_str::SmolStr; @@ -30,6 +33,21 @@ impl Parameter { } } +impl Display for Parameter { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self.kind { + ParameterKind::OpaqueString | ParameterKind::OpaqueStringRemainder => { + write!(f, "[{}]", self.name) + } + ParameterKind::MemberRef => write!(f, ""), + ParameterKind::SystemRef => write!(f, ""), + ParameterKind::MemberPrivacyTarget => write!(f, ""), + ParameterKind::PrivacyLevel => write!(f, "[privacy level]"), + ParameterKind::Toggle => write!(f, "on/off"), + } + } +} + impl From for Parameter { fn from(value: ParameterKind) -> Self { Parameter { @@ -76,19 +94,6 @@ impl ParameterKind { matches!(self, ParameterKind::OpaqueStringRemainder) } - pub(crate) fn format(&self, f: &mut std::fmt::Formatter, param_name: &str) -> std::fmt::Result { - match self { - ParameterKind::OpaqueString | ParameterKind::OpaqueStringRemainder => { - write!(f, "[{param_name}]") - } - ParameterKind::MemberRef => write!(f, ""), - ParameterKind::SystemRef => write!(f, ""), - ParameterKind::MemberPrivacyTarget => write!(f, ""), - ParameterKind::PrivacyLevel => write!(f, "[privacy level]"), - ParameterKind::Toggle => write!(f, "on/off"), - } - } - pub(crate) fn match_value(&self, input: &str) -> Result { match self { ParameterKind::OpaqueString | ParameterKind::OpaqueStringRemainder => { diff --git a/crates/command_parser/src/token.rs b/crates/command_parser/src/token.rs index 0d0fce2f..996ee017 100644 --- a/crates/command_parser/src/token.rs +++ b/crates/command_parser/src/token.rs @@ -81,11 +81,12 @@ impl Display for Token { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::Value { name, .. } => write!(f, "{name}"), - Self::Parameter(param) => param.kind().format(f, param.name()), + Self::Parameter(param) => write!(f, "{param}"), } } } +// (name, aliases) -> Token::Value impl From<(&str, [&str; L])> for Token { fn from((name, aliases): (&str, [&str; L])) -> Self { Self::Value { @@ -95,6 +96,7 @@ impl From<(&str, [&str; L])> for Token { } } +// name -> Token::Value impl From<&str> for Token { fn from(value: &str) -> Self { Self::from((value, [])) @@ -119,11 +121,21 @@ impl From<(&str, ParameterKind)> for Token { } } +/// Iterator that produces [`Token`]s. +/// +/// This is more of a convenience type that the [`tokens!`] macro uses in order +/// to more easily combine tokens together. #[derive(Debug, Clone)] pub struct TokensIterator { inner: Vec, } +impl TokensIterator { + pub(crate) fn new(tokens: Vec) -> Self { + Self { inner: tokens } + } +} + impl Iterator for TokensIterator { type Item = Token; @@ -132,37 +144,27 @@ impl Iterator for TokensIterator { } } +impl From> for TokensIterator { + fn from(value: Vec) -> Self { + Self::new(value) + } +} + impl> From for TokensIterator { fn from(value: T) -> Self { - Self { - inner: vec![value.into()], - } + Self::new(vec![value.into()]) } } impl From<[Token; L]> for TokensIterator { fn from(value: [Token; L]) -> Self { - Self { - inner: value.into_iter().collect(), - } + Self::new(value.into_iter().collect()) } } impl From<[Self; L]> for TokensIterator { fn from(value: [Self; L]) -> Self { - Self { - inner: value - .into_iter() - .map(|t| t.collect::>()) - .flatten() - .collect(), - } - } -} - -impl From> for TokensIterator { - fn from(value: Vec) -> Self { - Self { inner: value } + Self::new(value.into_iter().map(|t| t.inner).flatten().collect()) } } From a2329de95b75c6025df60912fe4b380bdbc1ea94 Mon Sep 17 00:00:00 2001 From: dusk Date: Fri, 24 Jan 2025 05:25:46 +0900 Subject: [PATCH 069/179] fix(command_definitions): actually use the RESET constant we defined before --- crates/command_definitions/src/config.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/command_definitions/src/config.rs b/crates/command_definitions/src/config.rs index d2ca10e1..b6dca8f6 100644 --- a/crates/command_definitions/src/config.rs +++ b/crates/command_definitions/src/config.rs @@ -15,8 +15,7 @@ pub fn cmds() -> impl Iterator { command!(ap_account, Toggle => "cfg_ap_account_update") .help("Toggles autoproxy for the account"), command!(ap_timeout => "cfg_ap_timeout_show").help("Shows the autoproxy timeout"), - command!(ap_timeout, ("reset", ["clear", "default"]) => "cfg_ap_timeout_reset") - .help("Resets the autoproxy timeout"), + command!(ap_timeout, RESET => "cfg_ap_timeout_reset").help("Resets the autoproxy timeout"), command!(ap_timeout, parameter::Toggle::Off => "cfg_ap_timeout_off") .help("Disables the autoproxy timeout"), command!(ap_timeout, ("timeout", OpaqueString) => "cfg_ap_timeout_update") From a5576ad22532b9243c1e34e8ba9336ba6f7eea65 Mon Sep 17 00:00:00 2001 From: dusk Date: Sat, 29 Mar 2025 14:09:40 +0900 Subject: [PATCH 070/179] fix(command_parser): don't return a command early if there is still input left --- crates/command_parser/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/command_parser/src/lib.rs b/crates/command_parser/src/lib.rs index 460a556a..1b411b56 100644 --- a/crates/command_parser/src/lib.rs +++ b/crates/command_parser/src/lib.rs @@ -109,8 +109,8 @@ pub fn parse_command( println!("flag matched {matched_flag:?}"); raw_flags.push((current_token_idx, matched_flag)); } - // if we have a command, stop parsing and return it - if let Some(command) = local_tree.command() { + // if we have a command, stop parsing and return it (only if there is no remaining input) + if current_pos >= input.len() && let Some(command) = local_tree.command() { // match the flags against this commands flags let mut flags: HashMap> = HashMap::new(); let mut misplaced_flags: Vec = Vec::new(); From 3e65d74bc41cefe0caccea086fa7bc1ac11e9327 Mon Sep 17 00:00:00 2001 From: dusk Date: Sat, 29 Mar 2025 14:34:15 +0900 Subject: [PATCH 071/179] fix(command_parser): use correct invalid_flags variable when constructing that error --- crates/command_parser/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/command_parser/src/lib.rs b/crates/command_parser/src/lib.rs index 1b411b56..f149aac3 100644 --- a/crates/command_parser/src/lib.rs +++ b/crates/command_parser/src/lib.rs @@ -172,7 +172,7 @@ pub fn parse_command( if invalid_flags.is_empty().not() { let mut error = format!( "Flag{} ", - (misplaced_flags.len() > 1).then_some("s").unwrap_or("") + (invalid_flags.len() > 1).then_some("s").unwrap_or("") ); for (idx, matched_flag) in invalid_flags.iter().enumerate() { write!(&mut error, "`-{}`", matched_flag.name).expect("oom"); From 87f6fe9d75798883b29621297e74d7a002b1961c Mon Sep 17 00:00:00 2001 From: dusk Date: Sat, 29 Mar 2025 14:51:58 +0900 Subject: [PATCH 072/179] refactor(command_parser): improve the invalid flags error --- crates/command_parser/src/lib.rs | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/crates/command_parser/src/lib.rs b/crates/command_parser/src/lib.rs index f149aac3..0a444915 100644 --- a/crates/command_parser/src/lib.rs +++ b/crates/command_parser/src/lib.rs @@ -182,16 +182,9 @@ pub fn parse_command( } write!( &mut error, - " {} not applicable in this command (`{prefix}{input}`). Applicable flags are the following:", - (invalid_flags.len() > 1).then_some("are").unwrap_or("is") + " {} seem to be applicable in this command (`{prefix}{command}`).", + (invalid_flags.len() > 1).then_some("don't").unwrap_or("doesn't") ).expect("oom"); - for (idx, flag) in command.flags.iter().enumerate() { - write!(&mut error, " `{flag}`").expect("oom"); - if idx < command.flags.len() - 1 { - error.push_str(", "); - } - } - error.push_str("."); return Err(error); } println!("{} {flags:?} {params:?}", command.cb); From ac52b5c257afee64a40c9097f9d05660e3524e49 Mon Sep 17 00:00:00 2001 From: dusk Date: Mon, 31 Mar 2025 22:22:38 +0900 Subject: [PATCH 073/179] feat: implement system name etc. commands --- PluralKit.Bot/CommandMeta/CommandTree.cs | 24 ++-- .../CommandSystem/Context/Context.cs | 7 +- PluralKit.Bot/CommandSystem/Parameters.cs | 6 +- PluralKit.Bot/Commands/System.cs | 7 +- PluralKit.Bot/Commands/SystemEdit.cs | 112 +++++++++--------- PluralKit.Bot/Services/EmbedService.cs | 4 +- crates/command_definitions/src/lib.rs | 2 + crates/command_definitions/src/system.rs | 46 ++++++- crates/command_parser/src/flag.rs | 16 +-- crates/commands/src/bin/write_cs_glue.rs | 34 ++++-- 10 files changed, 155 insertions(+), 103 deletions(-) diff --git a/PluralKit.Bot/CommandMeta/CommandTree.cs b/PluralKit.Bot/CommandMeta/CommandTree.cs index 71b6fd71..f62d24b4 100644 --- a/PluralKit.Bot/CommandMeta/CommandTree.cs +++ b/PluralKit.Bot/CommandMeta/CommandTree.cs @@ -24,6 +24,16 @@ public partial class CommandTree Commands.CfgApTimeoutUpdate(CfgApTimeoutUpdateParams param, _) => ctx.Execute(null, m => m.EditAutoproxyTimeout(ctx, param.timeout)), Commands.FunThunder => ctx.Execute(null, m => m.Thunder(ctx)), Commands.FunMeow => ctx.Execute(null, m => m.Meow(ctx)), + Commands.SystemInfo(SystemInfoParams param, SystemInfoFlags flags) => ctx.Execute(SystemInfo, m => m.Query(ctx, param.target, flags.all, flags.@public, flags.@private)), + Commands.SystemInfoSelf(_, SystemInfoSelfFlags flags) => ctx.Execute(SystemInfo, m => m.Query(ctx, ctx.System, flags.all, flags.@public, flags.@private)), + Commands.SystemNew(SystemNewParams param, _) => ctx.Execute(SystemNew, m => m.New(ctx, null)), + Commands.SystemNewName(SystemNewNameParams param, _) => ctx.Execute(SystemNew, m => m.New(ctx, param.name)), + Commands.SystemShowName(SystemShowNameParams param, SystemShowNameFlags flags) => ctx.Execute(SystemRename, m => m.ShowName(ctx, param.target, flags.GetReplyFormat())), + Commands.SystemRename(SystemRenameParams param, _) => ctx.Execute(SystemRename, m => m.Rename(ctx, param.target, param.name)), + Commands.SystemClearName(SystemClearNameParams param, _) => ctx.Execute(SystemRename, m => m.ClearName(ctx, param.target)), + Commands.SystemShowServerName(SystemShowServerNameParams param, SystemShowServerNameFlags flags) => ctx.Execute(SystemServerName, m => m.ShowServerName(ctx, param.target, flags.GetReplyFormat())), + Commands.SystemClearServerName(SystemClearServerNameParams param, _) => ctx.Execute(SystemServerName, m => m.ClearServerName(ctx, param.target)), + Commands.SystemRenameServerName(SystemRenameServerNameParams param, _) => ctx.Execute(SystemServerName, m => m.RenameServerName(ctx, param.target, param.name)), _ => // this should only ever occur when deving if commands are not implemented... ctx.Reply( @@ -217,10 +227,7 @@ public partial class CommandTree private async Task HandleSystemCommand(Context ctx) { - // these commands never take a system target - if (ctx.Match("new", "create", "make", "add", "register", "init", "n")) - await ctx.Execute(SystemNew, m => m.New(ctx)); - else if (ctx.Match("commands", "help")) + if (ctx.Match("commands", "help")) await PrintCommandList(ctx, "systems", SystemCommands); // todo: these aren't deprecated but also shouldn't be here @@ -275,12 +282,7 @@ public partial class CommandTree private async Task HandleSystemCommandTargeted(Context ctx, PKSystem target) { - if (ctx.Match("name", "rename", "changename", "rn")) - await ctx.CheckSystem(target).Execute(SystemRename, m => m.Name(ctx, target)); - else if (ctx.Match("servername", "sn", "sname", "snick", "snickname", "servernick", "servernickname", - "serverdisplayname", "guildname", "guildnick", "guildnickname", "serverdn")) - await ctx.Execute(SystemServerName, m => m.ServerName(ctx, target)); - else if (ctx.Match("tag", "t")) + if (ctx.Match("tag", "t")) await ctx.CheckSystem(target).Execute(SystemTag, m => m.Tag(ctx, target)); else if (ctx.Match("servertag", "st", "stag", "deer")) await ctx.CheckSystem(target).Execute(SystemServerTag, m => m.ServerTag(ctx, target)); @@ -314,8 +316,6 @@ public partial class CommandTree await ctx.CheckSystem(target).Execute(SystemFrontHistory, m => m.SystemFrontHistory(ctx, target)); else if (ctx.Match("fp", "frontpercent", "front%", "frontbreakdown")) await ctx.CheckSystem(target).Execute(SystemFrontPercent, m => m.FrontPercent(ctx, system: target)); - else if (ctx.Match("info", "view", "show")) - await ctx.CheckSystem(target).Execute(SystemInfo, m => m.Query(ctx, target)); else if (ctx.Match("groups", "gs")) await ctx.CheckSystem(target).Execute(GroupList, g => g.ListSystemGroups(ctx, target)); else if (ctx.Match("privacy")) diff --git a/PluralKit.Bot/CommandSystem/Context/Context.cs b/PluralKit.Bot/CommandSystem/Context/Context.cs index ad075cde..69c695da 100644 --- a/PluralKit.Bot/CommandSystem/Context/Context.cs +++ b/PluralKit.Bot/CommandSystem/Context/Context.cs @@ -149,10 +149,11 @@ public class Context public LookupContext DirectLookupContextFor(SystemId systemId) => System?.Id == systemId ? LookupContext.ByOwner : LookupContext.ByNonOwner; - public LookupContext LookupContextFor(SystemId systemId) + public LookupContext LookupContextFor(SystemId systemId, bool? _hasPrivateOverride = null, bool? _hasPublicOverride = null) { - var hasPrivateOverride = Parameters.HasFlag("private", "priv"); - var hasPublicOverride = Parameters.HasFlag("public", "pub"); + // TODO(yusdacra): these should be passed as a parameter to this method all the way from command tree + bool hasPrivateOverride = _hasPrivateOverride ?? Parameters.HasFlag("private", "priv"); + bool hasPublicOverride = _hasPublicOverride ?? Parameters.HasFlag("public", "pub"); if (hasPrivateOverride && hasPublicOverride) throw new PKError("Cannot match both public and private flags at the same time."); diff --git a/PluralKit.Bot/CommandSystem/Parameters.cs b/PluralKit.Bot/CommandSystem/Parameters.cs index 263367cd..d4aa2e79 100644 --- a/PluralKit.Bot/CommandSystem/Parameters.cs +++ b/PluralKit.Bot/CommandSystem/Parameters.cs @@ -111,8 +111,7 @@ public class Parameters // todo: i think this should return null for everything...? if (param == null) return default; return extract_func(param) - // this should never really happen (hopefully!), but in case the parameter names dont match up (typos...) between rust <-> c#... - // (it would be very cool to have this statically checked somehow..?) + // this should never happen unless codegen somehow uses a wrong name ?? throw new PKError($"Flag {flag_name.AsCode()} was not found or did not have a value defined for command {Callback().AsCode()} -- this is a bug!!"); } @@ -122,8 +121,7 @@ public class Parameters // todo: i think this should return null for everything...? if (param == null) return default; return extract_func(param) - // this should never really happen (hopefully!), but in case the parameter names dont match up (typos...) between rust <-> c#... - // (it would be very cool to have this statically checked somehow..?) + // this should never happen unless codegen somehow uses a wrong name ?? throw new PKError($"Parameter {param_name.AsCode()} was not found for command {Callback().AsCode()} -- this is a bug!!"); } } \ No newline at end of file diff --git a/PluralKit.Bot/Commands/System.cs b/PluralKit.Bot/Commands/System.cs index 8d4026d2..3c1cadde 100644 --- a/PluralKit.Bot/Commands/System.cs +++ b/PluralKit.Bot/Commands/System.cs @@ -14,18 +14,17 @@ public class System _embeds = embeds; } - public async Task Query(Context ctx, PKSystem system) + public async Task Query(Context ctx, PKSystem system, bool all, bool @public, bool @private) { if (system == null) throw Errors.NoSystemError(ctx.DefaultPrefix); - await ctx.Reply(embed: await _embeds.CreateSystemEmbed(ctx, system, ctx.LookupContextFor(system.Id))); + await ctx.Reply(embed: await _embeds.CreateSystemEmbed(ctx, system, ctx.LookupContextFor(system.Id, @private, @public), all)); } - public async Task New(Context ctx) + public async Task New(Context ctx, string? systemName) { ctx.CheckNoSystem(); - var systemName = ctx.RemainderOrNull(); if (systemName != null && systemName.Length > Limits.MaxSystemNameLength) throw Errors.StringTooLongError("System name", systemName.Length, Limits.MaxSystemNameLength); diff --git a/PluralKit.Bot/Commands/SystemEdit.cs b/PluralKit.Bot/Commands/SystemEdit.cs index 7df7efff..b9e2c6f1 100644 --- a/PluralKit.Bot/Commands/SystemEdit.cs +++ b/PluralKit.Bot/Commands/SystemEdit.cs @@ -29,7 +29,7 @@ public class SystemEdit _avatarHosting = avatarHosting; } - public async Task Name(Context ctx, PKSystem target) + public async Task ShowName(Context ctx, PKSystem target, ReplyFormat format) { ctx.CheckSystemPrivacy(target.Id, target.NamePrivacy); var isOwnSystem = target.Id == ctx.System?.Id; @@ -38,15 +38,11 @@ public class SystemEdit if (isOwnSystem) noNameSetMessage += $" Type `{ctx.DefaultPrefix}system name ` to set one."; - 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 (target.Name == null) + { + await ctx.Reply(noNameSetMessage); + return; + } if (format == ReplyFormat.Raw) { @@ -61,37 +57,40 @@ public class SystemEdit return; } - if (!ctx.HasNext(false)) - { - await ctx.Reply( - $"{(isOwnSystem ? "Your" : "This")} system's name is currently **{target.Name}**." - + (isOwnSystem ? $" Type `{ctx.DefaultPrefix}system name -clear` to clear it." - + $" Using {target.Name.Length}/{Limits.MaxSystemNameLength} characters." : "")); - return; - } + await ctx.Reply( + $"{(isOwnSystem ? "Your" : "This")} system's name is currently **{target.Name}**." + + (isOwnSystem ? $" Type `{ctx.DefaultPrefix}system name -clear` to clear it." + + $" Using {target.Name.Length}/{Limits.MaxSystemNameLength} characters." : "")); + return; + } + public async Task ClearName(Context ctx, PKSystem target) + { + ctx.CheckSystemPrivacy(target.Id, target.NamePrivacy); ctx.CheckSystem().CheckOwnSystem(target); - if (ctx.MatchClear() && await ctx.ConfirmClear("your system's name")) + if (await ctx.ConfirmClear("your system's name")) { await ctx.Repository.UpdateSystem(target.Id, new SystemPatch { Name = null }); await ctx.Reply($"{Emojis.Success} System name cleared."); } - else - { - var newSystemName = ctx.RemainderOrNull(false).NormalizeLineEndSpacing(); - - if (newSystemName.Length > Limits.MaxSystemNameLength) - throw Errors.StringTooLongError("System name", newSystemName.Length, Limits.MaxSystemNameLength); - - await ctx.Repository.UpdateSystem(target.Id, new SystemPatch { Name = newSystemName }); - - await ctx.Reply($"{Emojis.Success} System name changed (using {newSystemName.Length}/{Limits.MaxSystemNameLength} characters)."); - } } - public async Task ServerName(Context ctx, PKSystem target) + public async Task Rename(Context ctx, PKSystem target, string newSystemName) + { + ctx.CheckSystemPrivacy(target.Id, target.NamePrivacy); + ctx.CheckSystem().CheckOwnSystem(target); + + if (newSystemName.Length > Limits.MaxSystemNameLength) + throw Errors.StringTooLongError("System name", newSystemName.Length, Limits.MaxSystemNameLength); + + await ctx.Repository.UpdateSystem(target.Id, new SystemPatch { Name = newSystemName }); + + await ctx.Reply($"{Emojis.Success} System name changed (using {newSystemName.Length}/{Limits.MaxSystemNameLength} characters)."); + } + + public async Task ShowServerName(Context ctx, PKSystem target, ReplyFormat format) { ctx.CheckGuildContext(); @@ -103,15 +102,11 @@ public class SystemEdit var settings = await ctx.Repository.GetSystemGuild(ctx.Guild.Id, target.Id); - 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 (settings.DisplayName == null) + { + await ctx.Reply(noNameSetMessage); + return; + } if (format == ReplyFormat.Raw) { @@ -126,34 +121,37 @@ public class SystemEdit return; } - if (!ctx.HasNext(false)) - { - await ctx.Reply( - $"{(isOwnSystem ? "Your" : "This")} system's name for this server is currently **{settings.DisplayName}**." - + (isOwnSystem ? $" Type `{ctx.DefaultPrefix}system servername -clear` to clear it." - + $" Using {settings.DisplayName.Length}/{Limits.MaxSystemNameLength} characters." : "")); - return; - } + await ctx.Reply( + $"{(isOwnSystem ? "Your" : "This")} system's name for this server is currently **{settings.DisplayName}**." + + (isOwnSystem ? $" Type `{ctx.DefaultPrefix}system servername -clear` to clear it." + + $" Using {settings.DisplayName.Length}/{Limits.MaxSystemNameLength} characters." : "")); + return; + } + public async Task ClearServerName(Context ctx, PKSystem target) + { + ctx.CheckGuildContext(); ctx.CheckSystem().CheckOwnSystem(target); - if (ctx.MatchClear() && await ctx.ConfirmClear("your system's name for this server")) + if (await ctx.ConfirmClear("your system's name for this server")) { await ctx.Repository.UpdateSystemGuild(target.Id, ctx.Guild.Id, new SystemGuildPatch { DisplayName = null }); await ctx.Reply($"{Emojis.Success} System name for this server cleared."); } - else - { - var newSystemGuildName = ctx.RemainderOrNull(false).NormalizeLineEndSpacing(); + } - if (newSystemGuildName.Length > Limits.MaxSystemNameLength) - throw Errors.StringTooLongError("System name for this server", newSystemGuildName.Length, Limits.MaxSystemNameLength); + public async Task RenameServerName(Context ctx, PKSystem target, string newSystemGuildName) + { + ctx.CheckGuildContext(); + ctx.CheckSystem().CheckOwnSystem(target); - await ctx.Repository.UpdateSystemGuild(target.Id, ctx.Guild.Id, new SystemGuildPatch { DisplayName = newSystemGuildName }); + if (newSystemGuildName.Length > Limits.MaxSystemNameLength) + throw Errors.StringTooLongError("System name for this server", newSystemGuildName.Length, Limits.MaxSystemNameLength); - await ctx.Reply($"{Emojis.Success} System name for this server changed (using {newSystemGuildName.Length}/{Limits.MaxSystemNameLength} characters)."); - } + await ctx.Repository.UpdateSystemGuild(target.Id, ctx.Guild.Id, new SystemGuildPatch { DisplayName = newSystemGuildName }); + + await ctx.Reply($"{Emojis.Success} System name for this server changed (using {newSystemGuildName.Length}/{Limits.MaxSystemNameLength} characters)."); } public async Task Description(Context ctx, PKSystem target) diff --git a/PluralKit.Bot/Services/EmbedService.cs b/PluralKit.Bot/Services/EmbedService.cs index 178d1339..53551199 100644 --- a/PluralKit.Bot/Services/EmbedService.cs +++ b/PluralKit.Bot/Services/EmbedService.cs @@ -39,14 +39,14 @@ public class EmbedService return Task.WhenAll(ids.Select(Inner)); } - public async Task CreateSystemEmbed(Context cctx, PKSystem system, LookupContext ctx) + public async Task CreateSystemEmbed(Context cctx, PKSystem system, LookupContext ctx, bool countctxByOwner) { // Fetch/render info for all accounts simultaneously var accounts = await _repo.GetSystemAccounts(system.Id); var users = (await GetUsers(accounts)).Select(x => x.User?.NameAndMention() ?? $"(deleted account {x.Id})"); var countctx = LookupContext.ByNonOwner; - if (cctx.MatchFlag("a", "all")) + if (countctxByOwner) { if (system.Id == cctx.System.Id) countctx = LookupContext.ByOwner; diff --git a/crates/command_definitions/src/lib.rs b/crates/command_definitions/src/lib.rs index d25f1daf..b7336a3e 100644 --- a/crates/command_definitions/src/lib.rs +++ b/crates/command_definitions/src/lib.rs @@ -26,6 +26,8 @@ pub fn all() -> impl Iterator { .chain(member::cmds()) .chain(config::cmds()) .chain(fun::cmds()) + .map(|cmd| cmd.flag(("plaintext", ["pt"]))) + .map(|cmd| cmd.flag(("raw", ["r"]))) } pub const RESET: (&str, [&str; 2]) = ("reset", ["clear", "default"]); diff --git a/crates/command_definitions/src/system.rs b/crates/command_definitions/src/system.rs index da1d85f1..e08e61c4 100644 --- a/crates/command_definitions/src/system.rs +++ b/crates/command_definitions/src/system.rs @@ -2,15 +2,51 @@ use super::*; pub fn cmds() -> impl Iterator { let system = ("system", ["s"]); - let new = ("new", ["n"]); + let system_target = tokens!(system, SystemRef); - let system_new = tokens!(system, new); + let system_info_cmd = [ + command!(system => "system_info_self").help("Shows information about your system"), + command!(system_target, ("info", ["show", "view"]) => "system_info") + .help("Shows information about your system"), + ] + .into_iter() + .map(|cmd| { + cmd.flag(("public", ["pub"])) + .flag(("private", ["priv"])) + .flag(("all", ["a"])) + }); - [ - command!(system => "system_show").help("Shows information about your system"), + let system_name = tokens!(system_target, "name"); + let system_name_cmd = [ + command!(system_name => "system_show_name").help("Shows the systems name"), + command!(system_name, ("clear", ["c"]) => "system_clear_name") + .help("Clears the system's name"), + command!(system_name, ("name", OpaqueString) => "system_rename") + .help("Renames a given system"), + ] + .into_iter(); + + let system_server_name = tokens!(system_target, ("servername", ["sn", "guildname"])); + let system_server_name_cmd = [ + command!(system_server_name => "system_show_server_name") + .help("Shows the system's server name"), + command!(system_server_name, ("clear", ["c"]) => "system_clear_server_name") + .help("Clears the system's server name"), + command!(system_server_name, ("name", OpaqueString) => "system_rename_server_name") + .help("Renames the system's server name"), + ] + .into_iter(); + + let system_new = tokens!(system, ("new", ["n"])); + let system_new_cmd = [ command!(system_new => "system_new").help("Creates a new system"), command!(system_new, ("name", OpaqueString) => "system_new_name") .help("Creates a new system (using the provided name)"), ] - .into_iter() + .into_iter(); + + system_info_cmd + .chain(system_name_cmd) + .chain(system_server_name_cmd) + .chain(system_new_cmd) } diff --git a/crates/command_parser/src/flag.rs b/crates/command_parser/src/flag.rs index fbeb0b1b..ca61f64d 100644 --- a/crates/command_parser/src/flag.rs +++ b/crates/command_parser/src/flag.rs @@ -106,18 +106,18 @@ impl From<(&str, ParameterKind)> for Flag { } } -impl From<[&str; L]> for Flag { - fn from(value: [&str; L]) -> Self { - let mut flag = Flag::new(value[0]); - for alias in &value[1..] { - flag = flag.alias(*alias); +impl From<(&str, [&str; L])> for Flag { + fn from((name, aliases): (&str, [&str; L])) -> Self { + let mut flag = Flag::new(name); + for alias in aliases { + flag = flag.alias(alias); } flag } } -impl From<([&str; L], ParameterKind)> for Flag { - fn from((names, value): ([&str; L], ParameterKind)) -> Self { - Flag::from(names).value(value) +impl From<((&str, [&str; L]), ParameterKind)> for Flag { + fn from(((name, aliases), value): ((&str, [&str; L]), ParameterKind)) -> Self { + Flag::from((name, aliases)).value(value) } } diff --git a/crates/commands/src/bin/write_cs_glue.rs b/crates/commands/src/bin/write_cs_glue.rs index baa825c0..1d64a91a 100644 --- a/crates/commands/src/bin/write_cs_glue.rs +++ b/crates/commands/src/bin/write_cs_glue.rs @@ -1,8 +1,7 @@ use std::{env, fmt::Write, fs, path::PathBuf, str::FromStr}; use command_parser::{ - parameter::{Parameter, ParameterKind}, - token::Token, + command, parameter::{Parameter, ParameterKind}, token::Token }; fn main() -> Result<(), Box> { @@ -34,7 +33,7 @@ fn main() -> Result<(), Box> { for param in &command_params { writeln!( &mut command_params_init, - r#"{name} = await ctx.ParamResolve{extract_fn_name}("{name}") ?? throw new PKError("this is a bug"),"#, + r#"@{name} = await ctx.ParamResolve{extract_fn_name}("{name}") ?? throw new PKError("this is a bug"),"#, name = param.name(), extract_fn_name = get_param_param_ty(param.kind()), )?; @@ -44,14 +43,14 @@ fn main() -> Result<(), Box> { if let Some(kind) = flag.get_value() { writeln!( &mut command_flags_init, - r#"{name} = await ctx.FlagResolve{extract_fn_name}("{name}"),"#, + r#"@{name} = await ctx.FlagResolve{extract_fn_name}("{name}"),"#, name = flag.get_name(), extract_fn_name = get_param_param_ty(kind), )?; } else { writeln!( &mut command_flags_init, - r#"{name} = ctx.Parameters.HasFlag("{name}"),"#, + r#"@{name} = ctx.Parameters.HasFlag("{name}"),"#, name = flag.get_name(), )?; } @@ -92,7 +91,7 @@ fn main() -> Result<(), Box> { for param in &command_params { writeln!( &mut command_params_fields, - r#"public required {ty} {name};"#, + r#"public required {ty} @{name};"#, name = param.name(), ty = get_param_ty(param.kind()), )?; @@ -102,18 +101,32 @@ fn main() -> Result<(), Box> { if let Some(kind) = flag.get_value() { writeln!( &mut command_flags_fields, - r#"public {ty}? {name};"#, + r#"public {ty}? @{name};"#, name = flag.get_name(), ty = get_param_ty(kind), )?; } else { writeln!( &mut command_flags_fields, - r#"public required bool {name};"#, + r#"public required bool @{name};"#, name = flag.get_name(), )?; } } + let mut command_reply_format = String::new(); + if command.flags.iter().any(|flag| flag.get_name() == "plaintext") { + writeln!( + &mut command_reply_format, + r#"if (plaintext) return ReplyFormat.Plaintext;"#, + )?; + } + if command.flags.iter().any(|flag| flag.get_name() == "raw") { + writeln!( + &mut command_reply_format, + r#"if (raw) return ReplyFormat.Raw;"#, + )?; + } + command_reply_format.push_str("return ReplyFormat.Standard;\n"); write!( &mut glue, r#" @@ -124,6 +137,11 @@ fn main() -> Result<(), Box> { public class {command_name}Flags {{ {command_flags_fields} + + public ReplyFormat GetReplyFormat() + {{ + {command_reply_format} + }} }} "#, command_name = command_callback_to_name(&command.cb), From 4cc729c93c525d20be75caa68d651fb8173782fd Mon Sep 17 00:00:00 2001 From: dusk Date: Tue, 1 Apr 2025 01:03:49 +0900 Subject: [PATCH 074/179] feat: add color, tag, description, server tag system commands --- PluralKit.Bot/CommandMeta/CommandTree.cs | 58 +-- PluralKit.Bot/Commands/SystemEdit.cs | 501 +++++++++++------------ crates/command_definitions/src/system.rs | 123 +++++- 3 files changed, 385 insertions(+), 297 deletions(-) diff --git a/PluralKit.Bot/CommandMeta/CommandTree.cs b/PluralKit.Bot/CommandMeta/CommandTree.cs index f62d24b4..3f03c84c 100644 --- a/PluralKit.Bot/CommandMeta/CommandTree.cs +++ b/PluralKit.Bot/CommandMeta/CommandTree.cs @@ -13,27 +13,45 @@ public partial class CommandTree "For the list of commands, see the website: "), Commands.HelpProxy => ctx.Reply( "The proxy help page has been moved! See the website: https://pluralkit.me/guide#proxying"), - Commands.MemberShow(MemberShowParams param, _) => ctx.Execute(MemberInfo, m => m.ViewMember(ctx, param.target)), - Commands.MemberNew(MemberNewParams param, _) => ctx.Execute(MemberNew, m => m.NewMember(ctx, param.name)), - Commands.MemberSoulscream(MemberSoulscreamParams param, _) => ctx.Execute(MemberInfo, m => m.Soulscream(ctx, param.target)), + Commands.MemberShow(var param, _) => ctx.Execute(MemberInfo, m => m.ViewMember(ctx, param.target)), + Commands.MemberNew(var param, _) => ctx.Execute(MemberNew, m => m.NewMember(ctx, param.name)), + Commands.MemberSoulscream(var param, _) => ctx.Execute(MemberInfo, m => m.Soulscream(ctx, param.target)), Commands.CfgApAccountShow => ctx.Execute(null, m => m.ViewAutoproxyAccount(ctx)), - Commands.CfgApAccountUpdate(CfgApAccountUpdateParams param, _) => ctx.Execute(null, m => m.EditAutoproxyAccount(ctx, param.toggle)), + Commands.CfgApAccountUpdate(var param, _) => ctx.Execute(null, m => m.EditAutoproxyAccount(ctx, param.toggle)), Commands.CfgApTimeoutShow => ctx.Execute(null, m => m.ViewAutoproxyTimeout(ctx)), Commands.CfgApTimeoutOff => ctx.Execute(null, m => m.DisableAutoproxyTimeout(ctx)), Commands.CfgApTimeoutReset => ctx.Execute(null, m => m.ResetAutoproxyTimeout(ctx)), - Commands.CfgApTimeoutUpdate(CfgApTimeoutUpdateParams param, _) => ctx.Execute(null, m => m.EditAutoproxyTimeout(ctx, param.timeout)), + Commands.CfgApTimeoutUpdate(var param, _) => ctx.Execute(null, m => m.EditAutoproxyTimeout(ctx, param.timeout)), Commands.FunThunder => ctx.Execute(null, m => m.Thunder(ctx)), Commands.FunMeow => ctx.Execute(null, m => m.Meow(ctx)), - Commands.SystemInfo(SystemInfoParams param, SystemInfoFlags flags) => ctx.Execute(SystemInfo, m => m.Query(ctx, param.target, flags.all, flags.@public, flags.@private)), - Commands.SystemInfoSelf(_, SystemInfoSelfFlags flags) => ctx.Execute(SystemInfo, m => m.Query(ctx, ctx.System, flags.all, flags.@public, flags.@private)), - Commands.SystemNew(SystemNewParams param, _) => ctx.Execute(SystemNew, m => m.New(ctx, null)), - Commands.SystemNewName(SystemNewNameParams param, _) => ctx.Execute(SystemNew, m => m.New(ctx, param.name)), - Commands.SystemShowName(SystemShowNameParams param, SystemShowNameFlags flags) => ctx.Execute(SystemRename, m => m.ShowName(ctx, param.target, flags.GetReplyFormat())), - Commands.SystemRename(SystemRenameParams param, _) => ctx.Execute(SystemRename, m => m.Rename(ctx, param.target, param.name)), - Commands.SystemClearName(SystemClearNameParams param, _) => ctx.Execute(SystemRename, m => m.ClearName(ctx, param.target)), - Commands.SystemShowServerName(SystemShowServerNameParams param, SystemShowServerNameFlags flags) => ctx.Execute(SystemServerName, m => m.ShowServerName(ctx, param.target, flags.GetReplyFormat())), - Commands.SystemClearServerName(SystemClearServerNameParams param, _) => ctx.Execute(SystemServerName, m => m.ClearServerName(ctx, param.target)), - Commands.SystemRenameServerName(SystemRenameServerNameParams param, _) => ctx.Execute(SystemServerName, m => m.RenameServerName(ctx, param.target, param.name)), + Commands.SystemInfo(var param, var flags) => ctx.Execute(SystemInfo, m => m.Query(ctx, param.target, flags.all, flags.@public, flags.@private)), + Commands.SystemInfoSelf(_, var flags) => ctx.Execute(SystemInfo, m => m.Query(ctx, ctx.System, flags.all, flags.@public, flags.@private)), + Commands.SystemNew(var param, _) => ctx.Execute(SystemNew, m => m.New(ctx, null)), + Commands.SystemNewName(var param, _) => ctx.Execute(SystemNew, m => m.New(ctx, param.name)), + Commands.SystemShowNameSelf(_, var flags) => ctx.Execute(SystemRename, m => m.ShowName(ctx, ctx.System, flags.GetReplyFormat())), + Commands.SystemShowName(var param, var flags) => ctx.Execute(SystemRename, m => m.ShowName(ctx, param.target, flags.GetReplyFormat())), + Commands.SystemRename(var param, _) => ctx.Execute(SystemRename, m => m.Rename(ctx, ctx.System, param.name)), + Commands.SystemClearName(var param, _) => ctx.Execute(SystemRename, m => m.ClearName(ctx, ctx.System)), + Commands.SystemShowServerNameSelf(_, var flags) => ctx.Execute(SystemServerName, m => m.ShowServerName(ctx, ctx.System, flags.GetReplyFormat())), + Commands.SystemShowServerName(var param, var flags) => ctx.Execute(SystemServerName, m => m.ShowServerName(ctx, param.target, flags.GetReplyFormat())), + Commands.SystemClearServerName(var param, _) => ctx.Execute(SystemServerName, m => m.ClearServerName(ctx, ctx.System)), + Commands.SystemRenameServerName(var param, _) => ctx.Execute(SystemServerName, m => m.RenameServerName(ctx, ctx.System, param.name)), + Commands.SystemShowDescriptionSelf(_, var flags) => ctx.Execute(SystemDesc, m => m.ShowDescription(ctx, ctx.System, flags.GetReplyFormat())), + Commands.SystemShowDescription(var param, var flags) => ctx.Execute(SystemDesc, m => m.ShowDescription(ctx, param.target, flags.GetReplyFormat())), + Commands.SystemClearDescription(var param, _) => ctx.Execute(SystemDesc, m => m.ClearDescription(ctx, ctx.System)), + Commands.SystemChangeDescription(var param, _) => ctx.Execute(SystemDesc, m => m.ChangeDescription(ctx, ctx.System, param.description)), + Commands.SystemShowColorSelf(_, var flags) => ctx.Execute(SystemColor, m => m.ShowColor(ctx, ctx.System, flags.GetReplyFormat())), + Commands.SystemShowColor(var param, var flags) => ctx.Execute(SystemColor, m => m.ShowColor(ctx, param.target, flags.GetReplyFormat())), + Commands.SystemClearColor(var param, _) => ctx.Execute(SystemColor, m => m.ClearColor(ctx, ctx.System)), + Commands.SystemChangeColor(var param, _) => ctx.Execute(SystemColor, m => m.ChangeColor(ctx, ctx.System, param.color)), + Commands.SystemShowTagSelf(_, var flags) => ctx.Execute(SystemTag, m => m.ShowTag(ctx, ctx.System, flags.GetReplyFormat())), + Commands.SystemShowTag(var param, var flags) => ctx.Execute(SystemTag, m => m.ShowTag(ctx, param.target, flags.GetReplyFormat())), + Commands.SystemClearTag(var param, _) => ctx.Execute(SystemTag, m => m.ClearTag(ctx, ctx.System)), + Commands.SystemChangeTag(var param, _) => ctx.Execute(SystemTag, m => m.ChangeTag(ctx, ctx.System, param.tag)), + Commands.SystemShowServerTagSelf(_, var flags) => ctx.Execute(SystemServerTag, m => m.ShowServerTag(ctx, ctx.System, flags.GetReplyFormat())), + Commands.SystemShowServerTag(var param, var flags) => ctx.Execute(SystemServerTag, m => m.ShowServerTag(ctx, param.target, flags.GetReplyFormat())), + Commands.SystemClearServerTag(var param, _) => ctx.Execute(SystemServerTag, m => m.ClearServerTag(ctx, ctx.System)), + Commands.SystemChangeServerTag(var param, _) => ctx.Execute(SystemServerTag, m => m.ChangeServerTag(ctx, ctx.System, param.tag)), _ => // this should only ever occur when deving if commands are not implemented... ctx.Reply( @@ -282,16 +300,8 @@ public partial class CommandTree private async Task HandleSystemCommandTargeted(Context ctx, PKSystem target) { - if (ctx.Match("tag", "t")) - await ctx.CheckSystem(target).Execute(SystemTag, m => m.Tag(ctx, target)); - else if (ctx.Match("servertag", "st", "stag", "deer")) - await ctx.CheckSystem(target).Execute(SystemServerTag, m => m.ServerTag(ctx, target)); - else if (ctx.Match("description", "desc", "describe", "d", "bio", "info", "text", "intro")) - await ctx.CheckSystem(target).Execute(SystemDesc, m => m.Description(ctx, target)); - else if (ctx.Match("pronouns", "pronoun", "prns", "pn")) + if (ctx.Match("pronouns", "pronoun", "prns", "pn")) await ctx.CheckSystem(target).Execute(SystemPronouns, m => m.Pronouns(ctx, target)); - else if (ctx.Match("color", "colour")) - await ctx.CheckSystem(target).Execute(SystemColor, m => m.Color(ctx, target)); else if (ctx.Match("banner", "splash", "cover")) await ctx.CheckSystem(target).Execute(SystemBannerImage, m => m.BannerImage(ctx, target)); else if (ctx.Match("avatar", "picture", "icon", "image", "pic", "pfp")) diff --git a/PluralKit.Bot/Commands/SystemEdit.cs b/PluralKit.Bot/Commands/SystemEdit.cs index b9e2c6f1..32e87da2 100644 --- a/PluralKit.Bot/Commands/SystemEdit.cs +++ b/PluralKit.Bot/Commands/SystemEdit.cs @@ -154,7 +154,20 @@ public class SystemEdit await ctx.Reply($"{Emojis.Success} System name for this server changed (using {newSystemGuildName.Length}/{Limits.MaxSystemNameLength} characters)."); } - public async Task Description(Context ctx, PKSystem target) + public async Task ClearDescription(Context ctx, PKSystem target) + { + ctx.CheckSystemPrivacy(target.Id, target.DescriptionPrivacy); + ctx.CheckSystem().CheckOwnSystem(target); + + if (await ctx.ConfirmClear("your system's description")) + { + await ctx.Repository.UpdateSystem(target.Id, new SystemPatch { Description = null }); + + await ctx.Reply($"{Emojis.Success} System description cleared."); + } + } + + public async Task ShowDescription(Context ctx, PKSystem target, ReplyFormat format) { ctx.CheckSystemPrivacy(target.Id, target.DescriptionPrivacy); @@ -164,15 +177,11 @@ public class SystemEdit if (isOwnSystem) noDescriptionSetMessage += $" To set one, type `{ctx.DefaultPrefix}s description `."; - 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); - return; - } + if (target.Description == null) + { + await ctx.Reply(noDescriptionSetMessage); + return; + } if (format == ReplyFormat.Raw) { @@ -187,92 +196,142 @@ public class SystemEdit return; } - if (!ctx.HasNext(false)) - { - await ctx.Reply(embed: new EmbedBuilder() - .Title("System description") - .Description(target.Description) - .Footer(new Embed.EmbedFooter( - $"To print the description with formatting, type `{ctx.DefaultPrefix}s description -raw`." - + (isOwnSystem ? $" To clear it, type `{ctx.DefaultPrefix}s description -clear`. To change it, type `{ctx.DefaultPrefix}s description `." - + $" Using {target.Description.Length}/{Limits.MaxDescriptionLength} characters." : ""))) - .Build()); - return; - } - - ctx.CheckSystem().CheckOwnSystem(target); - - if (ctx.MatchClear() && await ctx.ConfirmClear("your system's description")) - { - await ctx.Repository.UpdateSystem(target.Id, new SystemPatch { Description = null }); - - await ctx.Reply($"{Emojis.Success} System description cleared."); - } - else - { - var newDescription = ctx.RemainderOrNull(false).NormalizeLineEndSpacing(); - if (newDescription.Length > Limits.MaxDescriptionLength) - throw Errors.StringTooLongError("Description", newDescription.Length, Limits.MaxDescriptionLength); - - await ctx.Repository.UpdateSystem(target.Id, new SystemPatch { Description = newDescription }); - - await ctx.Reply($"{Emojis.Success} System description changed (using {newDescription.Length}/{Limits.MaxDescriptionLength} characters)."); - } + await ctx.Reply(embed: new EmbedBuilder() + .Title("System description") + .Description(target.Description) + .Footer(new Embed.EmbedFooter( + $"To print the description with formatting, type `{ctx.DefaultPrefix}s description -raw`." + + (isOwnSystem ? $" To clear it, type `{ctx.DefaultPrefix}s description -clear`. To change it, type `{ctx.DefaultPrefix}s description `." + + $" Using {target.Description.Length}/{Limits.MaxDescriptionLength} characters." : ""))) + .Build()); } - public async Task Color(Context ctx, PKSystem target) + public async Task ChangeDescription(Context ctx, PKSystem target, string newDescription) + { + ctx.CheckSystemPrivacy(target.Id, target.DescriptionPrivacy); + ctx.CheckSystem().CheckOwnSystem(target); + + newDescription = newDescription.NormalizeLineEndSpacing(); + if (newDescription.Length > Limits.MaxDescriptionLength) + throw Errors.StringTooLongError("Description", newDescription.Length, Limits.MaxDescriptionLength); + + await ctx.Repository.UpdateSystem(target.Id, new SystemPatch { Description = newDescription }); + + await ctx.Reply($"{Emojis.Success} System description changed (using {newDescription.Length}/{Limits.MaxDescriptionLength} characters)."); + } + + public async Task ChangeColor(Context ctx, PKSystem target, string newColor) + { + ctx.CheckSystem().CheckOwnSystem(target); + + if (newColor.StartsWith("#")) newColor = newColor.Substring(1); + if (!Regex.IsMatch(newColor, "^[0-9a-fA-F]{6}$")) throw Errors.InvalidColorError(newColor); + + await ctx.Repository.UpdateSystem(target.Id, + new SystemPatch { Color = Partial.Present(newColor.ToLowerInvariant()) }); + + await ctx.Reply(embed: new EmbedBuilder() + .Title($"{Emojis.Success} System color changed.") + .Color(newColor.ToDiscordColor()) + .Thumbnail(new Embed.EmbedThumbnail($"https://fakeimg.pl/256x256/{newColor}/?text=%20")) + .Build()); + } + + public async Task ClearColor(Context ctx, PKSystem target) + { + ctx.CheckSystem().CheckOwnSystem(target); + + await ctx.Repository.UpdateSystem(target.Id, new SystemPatch { Color = Partial.Null() }); + await ctx.Reply($"{Emojis.Success} System color cleared."); + } + + public async Task ShowColor(Context ctx, PKSystem target, ReplyFormat format) { var isOwnSystem = ctx.System?.Id == target.Id; - var matchedFormat = ctx.MatchFormat(); - var matchedClear = ctx.MatchClear(); - if (!isOwnSystem || !(ctx.HasNext() || matchedClear)) + if (target.Color == null) { - if (target.Color == null) - await ctx.Reply( - "This system does not have a color set." + (isOwnSystem ? $" To set one, type `{ctx.DefaultPrefix}system color `." : "")); - 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") - .Color(target.Color.ToDiscordColor()) - .Thumbnail(new Embed.EmbedThumbnail($"https://fakeimg.pl/256x256/{target.Color}/?text=%20")) - .Description( - $"This system's color is **#{target.Color}**." + (isOwnSystem ? $" To clear it, type `{ctx.DefaultPrefix}s color -clear`." : "")) - .Build()); + await ctx.Reply( + "This system does not have a color set." + (isOwnSystem ? $" To set one, type `{ctx.DefaultPrefix}system color `." : "")); return; } + if (format == ReplyFormat.Raw) + { + await ctx.Reply("```\n#" + target.Color + "\n```"); + return; + } + + if (format == ReplyFormat.Plaintext) + { + await ctx.Reply(target.Color); + return; + } + + await ctx.Reply(embed: new EmbedBuilder() + .Title("System color") + .Color(target.Color.ToDiscordColor()) + .Thumbnail(new Embed.EmbedThumbnail($"https://fakeimg.pl/256x256/{target.Color}/?text=%20")) + .Description( + $"This system's color is **#{target.Color}**." + (isOwnSystem ? $" To clear it, type `{ctx.DefaultPrefix}s color -clear`." : "")) + .Build()); + } + + public async Task ClearTag(Context ctx, PKSystem target) + { ctx.CheckSystem().CheckOwnSystem(target); - if (matchedClear) + if (await ctx.ConfirmClear("your system's tag")) { - await ctx.Repository.UpdateSystem(target.Id, new SystemPatch { Color = Partial.Null() }); + await ctx.Repository.UpdateSystem(target.Id, new SystemPatch { Tag = null }); - await ctx.Reply($"{Emojis.Success} System color cleared."); - } - else - { - var color = ctx.RemainderOrNull(); + var replyStr = $"{Emojis.Success} System tag cleared."; - if (color.StartsWith("#")) color = color.Substring(1); - if (!Regex.IsMatch(color, "^[0-9a-fA-F]{6}$")) throw Errors.InvalidColorError(color); + if (ctx.Guild != null) + { + var servertag = (await ctx.Repository.GetSystemGuild(ctx.Guild.Id, target.Id)).Tag; + if (servertag is not null) + replyStr += $"\n{Emojis.Note} You have a server tag set in this server ({servertag}) so it will still be shown on proxies."; + else if (ctx.GuildConfig.RequireSystemTag) + replyStr += $"\n{Emojis.Warn} This server requires a tag in order to proxy. If you do not add a new tag you will not be able to proxy in this server."; + } - await ctx.Repository.UpdateSystem(target.Id, - new SystemPatch { Color = Partial.Present(color.ToLowerInvariant()) }); - - await ctx.Reply(embed: new EmbedBuilder() - .Title($"{Emojis.Success} System color changed.") - .Color(color.ToDiscordColor()) - .Thumbnail(new Embed.EmbedThumbnail($"https://fakeimg.pl/256x256/{color}/?text=%20")) - .Build()); + await ctx.Reply(replyStr); } } - public async Task Tag(Context ctx, PKSystem target) + public async Task ChangeTag(Context ctx, PKSystem target, string newTag) + { + ctx.CheckSystem().CheckOwnSystem(target); + + newTag = newTag.NormalizeLineEndSpacing(); + if (newTag.Length > Limits.MaxSystemTagLength) + throw Errors.StringTooLongError("System tag", newTag.Length, Limits.MaxSystemTagLength); + + await ctx.Repository.UpdateSystem(target.Id, new SystemPatch { Tag = newTag }); + + var replyStr = $"{Emojis.Success} System tag changed (using {newTag.Length}/{Limits.MaxSystemTagLength} characters)."; + if (ctx.Config.NameFormat is null || ctx.Config.NameFormat.Contains("{tag}")) + replyStr += $"Member names will now have the tag {newTag.AsCode()} when proxied.\n{Emojis.Note}To check or change where your tag appears in your name use the command `{ctx.DefaultPrefix}cfg name format`."; + else + replyStr += $"\n{Emojis.Warn} You do not have a designated place for a tag in your name format so it **will not be put in proxy names**. To change this type `{ctx.DefaultPrefix}cfg name format`."; + + if (ctx.Guild != null) + { + var guildSettings = await ctx.Repository.GetSystemGuild(ctx.Guild.Id, target.Id); + + if (guildSettings.Tag is not null) + replyStr += $"\n{Emojis.Note} Note that you have a server tag set ({guildSettings.Tag}) and it will be shown in proxies instead."; + if (!guildSettings.TagEnabled) + replyStr += $"\n{Emojis.Note} Note that your tag is disabled in this server and will not be shown in proxies. To change this type `{ctx.DefaultPrefix}system servertag enable`."; + if (guildSettings.NameFormat is not null && !guildSettings.NameFormat.Contains("{tag}")) + replyStr += $"\n{Emojis.Note} You do not have a designated place for a tag in your server name format so it **will not be put in proxy names**. To change this type `{ctx.DefaultPrefix}cfg server name format`."; + } + + await ctx.Reply(replyStr); + } + + public async Task ShowTag(Context ctx, PKSystem target, ReplyFormat format) { var isOwnSystem = ctx.System?.Id == target.Id; @@ -280,15 +339,11 @@ public class SystemEdit ? $"You currently have no system tag set. To set one, type `{ctx.DefaultPrefix}s tag `." : "This system currently has no system tag set."; - 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); - return; - } + if (target.Tag == null) + { + await ctx.Reply(noTagSetMessage); + return; + } if (format == ReplyFormat.Raw) { @@ -303,193 +358,125 @@ public class SystemEdit return; } - if (!ctx.HasNext(false)) - { - await ctx.Reply($"{(isOwnSystem ? "Your" : "This system's")} current system tag is {target.Tag.AsCode()}." - + (isOwnSystem ? $"To change it, type `{ctx.DefaultPrefix}s tag `. To clear it, type `{ctx.DefaultPrefix}s tag -clear`." : "")); - return; - } - - ctx.CheckSystem().CheckOwnSystem(target); - - if (ctx.MatchClear() && await ctx.ConfirmClear("your system's tag")) - { - await ctx.Repository.UpdateSystem(target.Id, new SystemPatch { Tag = null }); - - var replyStr = $"{Emojis.Success} System tag cleared."; - - if (ctx.Guild != null) - { - var servertag = (await ctx.Repository.GetSystemGuild(ctx.Guild.Id, target.Id)).Tag; - if (servertag is not null) - replyStr += $"\n{Emojis.Note} You have a server tag set in this server ({servertag}) so it will still be shown on proxies."; - - else if (ctx.GuildConfig.RequireSystemTag) - replyStr += $"\n{Emojis.Warn} This server requires a tag in order to proxy. If you do not add a new tag you will not be able to proxy in this server."; - } - - await ctx.Reply(replyStr); - } - else - { - var newTag = ctx.RemainderOrNull(false).NormalizeLineEndSpacing(); - if (newTag != null) - if (newTag.Length > Limits.MaxSystemTagLength) - throw Errors.StringTooLongError("System tag", newTag.Length, Limits.MaxSystemTagLength); - - await ctx.Repository.UpdateSystem(target.Id, new SystemPatch { Tag = newTag }); - - var replyStr = $"{Emojis.Success} System tag changed (using {newTag.Length}/{Limits.MaxSystemTagLength} characters)."; - if (ctx.Config.NameFormat is null || ctx.Config.NameFormat.Contains("{tag}")) - replyStr += $"Member names will now have the tag {newTag.AsCode()} when proxied.\n{Emojis.Note}To check or change where your tag appears in your name use the command `{ctx.DefaultPrefix}cfg name format`."; - else - replyStr += $"\n{Emojis.Warn} You do not have a designated place for a tag in your name format so it **will not be put in proxy names**. To change this type `{ctx.DefaultPrefix}cfg name format`."; - if (ctx.Guild != null) - { - var guildSettings = await ctx.Repository.GetSystemGuild(ctx.Guild.Id, target.Id); - - if (guildSettings.Tag is not null) - replyStr += $"\n{Emojis.Note} Note that you have a server tag set ({guildSettings.Tag}) and it will be shown in proxies instead."; - if (!guildSettings.TagEnabled) - replyStr += $"\n{Emojis.Note} Note that your tag is disabled in this server and will not be shown in proxies. To change this type `{ctx.DefaultPrefix}system servertag enable`."; - if (guildSettings.NameFormat is not null && !guildSettings.NameFormat.Contains("{tag}")) - replyStr += $"\n{Emojis.Note} You do not have a designated place for a tag in your server name format so it **will not be put in proxy names**. To change this type `{ctx.DefaultPrefix}cfg server name format`."; - } - - await ctx.Reply(replyStr); - } + await ctx.Reply($"{(isOwnSystem ? "Your" : "This system's")} current system tag is {target.Tag.AsCode()}." + + (isOwnSystem ? $"To change it, type `{ctx.DefaultPrefix}s tag `. To clear it, type `{ctx.DefaultPrefix}s tag -clear`." : "")); } - public async Task ServerTag(Context ctx, PKSystem target) + public async Task ShowServerTag(Context ctx, PKSystem target, ReplyFormat format) { ctx.CheckSystem().CheckOwnSystem(target).CheckGuildContext(); - var setDisabledWarning = - $"{Emojis.Warn} Your system tag is currently **disabled** in this server. No tag will be applied when proxying.\nTo re-enable the system tag in the current server, type `{ctx.DefaultPrefix}s servertag -enable`."; - var settings = await ctx.Repository.GetSystemGuild(ctx.Guild.Id, target.Id); - async Task Show(ReplyFormat format = ReplyFormat.Standard) + if (settings.Tag != null) { - if (settings.Tag != null) + if (format == ReplyFormat.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(ctx.Config)}`"); - 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) - msg += $", but it is currently **disabled**. To re-enable it, type `{ctx.DefaultPrefix}s servertag -enable`."; - else - msg += - $". To change it, type `{ctx.DefaultPrefix}s servertag `. To clear it, type `{ctx.DefaultPrefix}s servertag -clear`."; - - await ctx.Reply(msg); + await ctx.Reply($"```{settings.Tag}```"); + return; + } + if (format == ReplyFormat.Plaintext) + { + var eb = new EmbedBuilder() + .Description($"Showing servertag for system `{target.DisplayHid(ctx.Config)}`"); + 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) - await ctx.Reply( - $"Your global system tag is {target.Tag}, but it is **disabled** in this server. To re-enable it, type `{ctx.DefaultPrefix}s servertag -enable`"); + msg += $", but it is currently **disabled**. To re-enable it, type `{ctx.DefaultPrefix}s servertag -enable`."; else - await ctx.Reply( - $"You currently have no system tag specific to the server '{ctx.Guild.Name}'. To set one, type `{ctx.DefaultPrefix}s servertag `. To disable the system tag in the current server, type `{ctx.DefaultPrefix}s servertag -disable`."); + msg += + $". To change it, type `{ctx.DefaultPrefix}s servertag `. To clear it, type `{ctx.DefaultPrefix}s servertag -clear`."; + + await ctx.Reply(msg); + return; } - async Task Set() - { - var newTag = ctx.RemainderOrNull(false); - if (newTag != null && newTag.Length > Limits.MaxSystemTagLength) - throw Errors.StringTooLongError("System server tag", newTag.Length, Limits.MaxSystemTagLength); - - await ctx.Repository.UpdateSystemGuild(target.Id, ctx.Guild.Id, new SystemGuildPatch { Tag = newTag }); - - var replyStr = $"{Emojis.Success} System server tag changed (using {newTag.Length}/{Limits.MaxSystemTagLength} characters). Member names will now have the tag {newTag.AsCode()} when proxied in the current server '{ctx.Guild.Name}'.\n\nTo check or change where your tag appears in your name use the command `{ctx.DefaultPrefix}cfg name format`."; - - if (!settings.TagEnabled) - replyStr += "\n" + setDisabledWarning; - - await ctx.Reply(replyStr); - } - - async Task Clear() - { - await ctx.Repository.UpdateSystemGuild(target.Id, ctx.Guild.Id, new SystemGuildPatch { Tag = null }); - - var replyStr = $"{Emojis.Success} System server tag cleared. Member names will now use the global system tag, if there is one set.\n\nTo check or change where your tag appears in your name use the command `{ctx.DefaultPrefix}cfg name format`."; - - if (!settings.TagEnabled) - replyStr += "\n" + setDisabledWarning; - - await ctx.Reply(replyStr); - } - - async Task EnableDisable(bool newValue) - { - await ctx.Repository.UpdateSystemGuild(target.Id, ctx.Guild.Id, - new SystemGuildPatch { TagEnabled = newValue }); - - await ctx.Reply(PrintEnableDisableResult(newValue, newValue != settings.TagEnabled)); - } - - string PrintEnableDisableResult(bool newValue, bool changedValue) - { - var opStr = newValue ? "enabled" : "disabled"; - var str = ""; - - if (!changedValue) - str = $"{Emojis.Note} The system tag is already {opStr} in this server."; - else - str = $"{Emojis.Success} System tag {opStr} in this server."; - - if (newValue) - { - if (settings.TagEnabled) - { - if (settings.Tag == null) - str += - " However, you do not have a system tag specific to this server. Messages will be proxied using your global system tag, if there is one set."; - else - str += - $" Your current system tag in '{ctx.Guild.Name}' is {settings.Tag.AsCode()}."; - } - else - { - if (settings.Tag != null) - str += - $" Member names will now use the server-specific tag {settings.Tag.AsCode()} when proxied in the current server '{ctx.Guild.Name}'." - + $"\n\nTo check or change where your tag appears in your name use the command `{ctx.DefaultPrefix}cfg name format`."; - else - str += - " Member names will now use the global system tag when proxied in the current server, if there is one set." - + "\n\nTo check or change where your tag appears in your name use the command `{ctx.DefaultPrefix}cfg name format`."; - } - } - - return str; - } - - if (ctx.MatchClear() && await ctx.ConfirmClear("your system's server tag")) - await Clear(); - else if (ctx.Match("disable") || ctx.MatchFlag("disable")) - await EnableDisable(false); - else if (ctx.Match("enable") || ctx.MatchFlag("enable")) - await EnableDisable(true); - else if (ctx.MatchFormat() != ReplyFormat.Standard) - await Show(ctx.MatchFormat()); - else if (!ctx.HasNext(false)) - await Show(); + if (!settings.TagEnabled) + await ctx.Reply( + $"Your global system tag is {target.Tag}, but it is **disabled** in this server. To re-enable it, type `{ctx.DefaultPrefix}s servertag -enable`"); else - await Set(); + await ctx.Reply( + $"You currently have no system tag specific to the server '{ctx.Guild.Name}'. To set one, type `{ctx.DefaultPrefix}s servertag `. To disable the system tag in the current server, type `{ctx.DefaultPrefix}s servertag -disable`."); + } + + public async Task ClearServerTag(Context ctx, PKSystem target) + { + ctx.CheckSystem().CheckOwnSystem(target).CheckGuildContext(); + + if (!await ctx.ConfirmClear("your system's server tag")) + return; + + var settings = await ctx.Repository.GetSystemGuild(ctx.Guild.Id, target.Id); + await ctx.Repository.UpdateSystemGuild(target.Id, ctx.Guild.Id, new SystemGuildPatch { Tag = null }); + + var replyStr = $"{Emojis.Success} System server tag cleared. Member names will now use the global system tag, if there is one set.\n\nTo check or change where your tag appears in your name use the command `{ctx.DefaultPrefix}cfg name format`."; + + if (!settings.TagEnabled) + replyStr += $"\n{Emojis.Warn} Your system tag is currently **disabled** in this server. No tag will be applied when proxying.\nTo re-enable the system tag in the current server, type `{ctx.DefaultPrefix}s servertag -enable`."; + + await ctx.Reply(replyStr); + } + + public async Task ChangeServerTag(Context ctx, PKSystem target, string newTag) + { + ctx.CheckSystem().CheckOwnSystem(target).CheckGuildContext(); + + var settings = await ctx.Repository.GetSystemGuild(ctx.Guild.Id, target.Id); + + if (newTag != null && newTag.Length > Limits.MaxSystemTagLength) + throw Errors.StringTooLongError("System server tag", newTag.Length, Limits.MaxSystemTagLength); + + await ctx.Repository.UpdateSystemGuild(target.Id, ctx.Guild.Id, new SystemGuildPatch { Tag = newTag }); + + var replyStr = $"{Emojis.Success} System server tag changed (using {newTag.Length}/{Limits.MaxSystemTagLength} characters). Member names will now have the tag {newTag.AsCode()} when proxied in the current server '{ctx.Guild.Name}'.\n\nTo check or change where your tag appears in your name use the command `{ctx.DefaultPrefix}cfg name format`."; + + if (!settings.TagEnabled) + replyStr += $"\n{Emojis.Warn} Your system tag is currently **disabled** in this server. No tag will be applied when proxying.\nTo re-enable the system tag in the current server, type `{ctx.DefaultPrefix}s servertag -enable`."; + + await ctx.Reply(replyStr); + } + + public async Task ToggleServerTag(Context ctx, PKSystem target, bool newValue) + { + ctx.CheckSystem().CheckOwnSystem(target).CheckGuildContext(); + + var settings = await ctx.Repository.GetSystemGuild(ctx.Guild.Id, target.Id); + await ctx.Repository.UpdateSystemGuild(target.Id, ctx.Guild.Id, new SystemGuildPatch { TagEnabled = newValue }); + + var opStr = newValue ? "enabled" : "disabled"; + string str; + + if (newValue == settings.TagEnabled) + str = $"{Emojis.Note} The system tag is already {opStr} in this server."; + else + str = $"{Emojis.Success} System tag {opStr} in this server."; + + if (newValue) + { + if (settings.TagEnabled) + { + if (settings.Tag == null) + str += " However, you do not have a system tag specific to this server. Messages will be proxied using your global system tag, if there is one set."; + else + str += $" Your current system tag in '{ctx.Guild.Name}' is {settings.Tag.AsCode()}."; + } + else + { + if (settings.Tag != null) + str += + $" Member names will now use the server-specific tag {settings.Tag.AsCode()} when proxied in the current server '{ctx.Guild.Name}'." + + $"\n\nTo check or change where your tag appears in your name use the command `{ctx.DefaultPrefix}cfg name format`."; + else + str += + " Member names will now use the global system tag when proxied in the current server, if there is one set." + + "\n\nTo check or change where your tag appears in your name use the command `{ctx.DefaultPrefix}cfg name format`."; + } + } + + await ctx.Reply(str); } public async Task Pronouns(Context ctx, PKSystem target) diff --git a/crates/command_definitions/src/system.rs b/crates/command_definitions/src/system.rs index e08e61c4..f7262832 100644 --- a/crates/command_definitions/src/system.rs +++ b/crates/command_definitions/src/system.rs @@ -1,9 +1,21 @@ use super::*; pub fn cmds() -> impl Iterator { + edit() +} + +pub fn edit() -> impl Iterator { let system = ("system", ["s"]); let system_target = tokens!(system, SystemRef); + let system_new = tokens!(system, ("new", ["n"])); + let system_new_cmd = [ + command!(system_new => "system_new").help("Creates a new system"), + command!(system_new, ("name", OpaqueString) => "system_new_name") + .help("Creates a new system (using the provided name)"), + ] + .into_iter(); + let system_info_cmd = [ command!(system => "system_info_self").help("Shows information about your system"), command!(system_target, ("info", ["show", "view"]) => "system_info") @@ -19,10 +31,16 @@ pub fn cmds() -> impl Iterator { let system_name = tokens!(system_target, "name"); let system_name_cmd = [ command!(system_name => "system_show_name").help("Shows the systems name"), - command!(system_name, ("clear", ["c"]) => "system_clear_name") - .help("Clears the system's name"), - command!(system_name, ("name", OpaqueString) => "system_rename") - .help("Renames a given system"), + ] + .into_iter(); + + let system_name_self = tokens!(system, "name"); + let system_name_self_cmd = [ + command!(system_name_self => "system_show_name_self").help("Shows your system's name"), + command!(system_name_self, ("clear", ["c"]) => "system_clear_name") + .help("Clears your system's name"), + command!(system_name_self, ("name", OpaqueString) => "system_rename") + .help("Renames your system"), ] .into_iter(); @@ -30,23 +48,96 @@ pub fn cmds() -> impl Iterator { let system_server_name_cmd = [ command!(system_server_name => "system_show_server_name") .help("Shows the system's server name"), - command!(system_server_name, ("clear", ["c"]) => "system_clear_server_name") - .help("Clears the system's server name"), - command!(system_server_name, ("name", OpaqueString) => "system_rename_server_name") - .help("Renames the system's server name"), ] .into_iter(); - let system_new = tokens!(system, ("new", ["n"])); - let system_new_cmd = [ - command!(system_new => "system_new").help("Creates a new system"), - command!(system_new, ("name", OpaqueString) => "system_new_name") - .help("Creates a new system (using the provided name)"), + let system_server_name_self = tokens!(system, ("servername", ["sn", "guildname"])); + let system_server_name_self_cmd = [ + command!(system_server_name_self => "system_show_server_name_self") + .help("Shows your system's server name"), + command!(system_server_name_self, ("clear", ["c"]) => "system_clear_server_name") + .help("Clears your system's server name"), + command!(system_server_name_self, ("name", OpaqueString) => "system_rename_server_name") + .help("Renames your system's server name"), ] .into_iter(); - system_info_cmd + let system_description = tokens!(system_target, ("description", ["desc", "d"])); + let system_description_cmd = [ + command!(system_description => "system_show_description").help("Shows the system's description"), + ] + .into_iter(); + + let system_description_self = tokens!(system, ("description", ["desc", "d"])); + let system_description_self_cmd = [ + command!(system_description_self => "system_show_description_self").help("Shows your system's description"), + command!(system_description_self, ("clear", ["c"]) => "system_clear_description") + .help("Clears your system's description"), + command!(system_description_self, ("description", OpaqueString) => "system_change_description") + .help("Changes your system's description"), + ] + .into_iter(); + + let system_color = tokens!(system_target, ("color", ["colour"])); + let system_color_cmd = [ + command!(system_color => "system_show_color").help("Shows the system's color"), + ] + .into_iter(); + + let system_color_self = tokens!(system, ("color", ["colour"])); + let system_color_self_cmd = [ + command!(system_color_self => "system_show_color_self").help("Shows your system's color"), + command!(system_color_self, ("clear", ["c"]) => "system_clear_color") + .help("Clears your system's color"), + command!(system_color_self, ("color", OpaqueString) => "system_change_color") + .help("Changes your system's color"), + ] + .into_iter(); + + let system_tag = tokens!(system_target, ("tag", ["suffix"])); + let system_tag_cmd = [ + command!(system_tag => "system_show_tag").help("Shows the system's tag"), + ] + .into_iter(); + + let system_tag_self = tokens!(system, ("tag", ["suffix"])); + let system_tag_self_cmd = [ + command!(system_tag_self => "system_show_tag_self").help("Shows your system's tag"), + command!(system_tag_self, ("clear", ["c"]) => "system_clear_tag") + .help("Clears your system's tag"), + command!(system_tag_self, ("tag", OpaqueString) => "system_change_tag") + .help("Changes your system's tag"), + ] + .into_iter(); + + let system_server_tag = tokens!(system_target, ("servertag", ["st", "guildtag"])); + let system_server_tag_cmd = [ + command!(system_server_tag => "system_show_server_tag").help("Shows the system's server tag"), + ] + .into_iter(); + + let system_server_tag_self = tokens!(system, ("servertag", ["st", "guildtag"])); + let system_server_tag_self_cmd = [ + command!(system_server_tag_self => "system_show_server_tag_self").help("Shows your system's server tag"), + command!(system_server_tag_self, ("clear", ["c"]) => "system_clear_server_tag") + .help("Clears your system's server tag"), + command!(system_server_tag_self, ("tag", OpaqueString) => "system_change_server_tag") + .help("Changes your system's server tag"), + ] + .into_iter(); + + system_new_cmd + .chain(system_name_self_cmd) + .chain(system_server_name_self_cmd) + .chain(system_description_self_cmd) + .chain(system_color_self_cmd) + .chain(system_tag_self_cmd) + .chain(system_server_tag_self_cmd) .chain(system_name_cmd) .chain(system_server_name_cmd) - .chain(system_new_cmd) -} + .chain(system_description_cmd) + .chain(system_color_cmd) + .chain(system_tag_cmd) + .chain(system_server_tag_cmd) + .chain(system_info_cmd) +} \ No newline at end of file From ae2703f5d951e951b4537be5aef99f070ceda7f1 Mon Sep 17 00:00:00 2001 From: dusk Date: Tue, 1 Apr 2025 16:33:00 +0900 Subject: [PATCH 075/179] feat: implement system pronouns commands --- PluralKit.Bot/CommandMeta/CommandTree.cs | 8 ++- PluralKit.Bot/Commands/SystemEdit.cs | 75 ++++++++++++------------ PluralKit.Bot/Utils/ContextUtils.cs | 8 +-- crates/command_definitions/src/system.rs | 19 ++++++ 4 files changed, 64 insertions(+), 46 deletions(-) diff --git a/PluralKit.Bot/CommandMeta/CommandTree.cs b/PluralKit.Bot/CommandMeta/CommandTree.cs index 3f03c84c..6cbf3245 100644 --- a/PluralKit.Bot/CommandMeta/CommandTree.cs +++ b/PluralKit.Bot/CommandMeta/CommandTree.cs @@ -52,6 +52,10 @@ public partial class CommandTree Commands.SystemShowServerTag(var param, var flags) => ctx.Execute(SystemServerTag, m => m.ShowServerTag(ctx, param.target, flags.GetReplyFormat())), Commands.SystemClearServerTag(var param, _) => ctx.Execute(SystemServerTag, m => m.ClearServerTag(ctx, ctx.System)), Commands.SystemChangeServerTag(var param, _) => ctx.Execute(SystemServerTag, m => m.ChangeServerTag(ctx, ctx.System, param.tag)), + Commands.SystemShowPronounsSelf(_, var flags) => ctx.Execute(SystemPronouns, m => m.ShowPronouns(ctx, ctx.System, flags.GetReplyFormat())), + Commands.SystemShowPronouns(var param, var flags) => ctx.Execute(SystemPronouns, m => m.ShowPronouns(ctx, param.target, flags.GetReplyFormat())), + Commands.SystemClearPronouns(var param, var flags) => ctx.Execute(SystemPronouns, m => m.ClearPronouns(ctx, ctx.System, flags.yes)), + Commands.SystemChangePronouns(var param, _) => ctx.Execute(SystemPronouns, m => m.ChangePronouns(ctx, ctx.System, param.pronouns)), _ => // this should only ever occur when deving if commands are not implemented... ctx.Reply( @@ -300,9 +304,7 @@ public partial class CommandTree private async Task HandleSystemCommandTargeted(Context ctx, PKSystem target) { - if (ctx.Match("pronouns", "pronoun", "prns", "pn")) - await ctx.CheckSystem(target).Execute(SystemPronouns, m => m.Pronouns(ctx, target)); - else if (ctx.Match("banner", "splash", "cover")) + if (ctx.Match("banner", "splash", "cover")) await ctx.CheckSystem(target).Execute(SystemBannerImage, m => m.BannerImage(ctx, target)); else if (ctx.Match("avatar", "picture", "icon", "image", "pic", "pfp")) await ctx.CheckSystem(target).Execute(SystemAvatar, m => m.Avatar(ctx, target)); diff --git a/PluralKit.Bot/Commands/SystemEdit.cs b/PluralKit.Bot/Commands/SystemEdit.cs index 32e87da2..c0eba729 100644 --- a/PluralKit.Bot/Commands/SystemEdit.cs +++ b/PluralKit.Bot/Commands/SystemEdit.cs @@ -479,25 +479,47 @@ public class SystemEdit await ctx.Reply(str); } - public async Task Pronouns(Context ctx, PKSystem target) + public async Task ClearPronouns(Context ctx, PKSystem target, bool flagConfirmYes) + { + ctx.CheckSystemPrivacy(target.Id, target.PronounPrivacy); + ctx.CheckSystem().CheckOwnSystem(target); + + if (await ctx.ConfirmClear("your system's pronouns", flagConfirmYes)) + { + await ctx.Repository.UpdateSystem(target.Id, new SystemPatch { Pronouns = null }); + await ctx.Reply($"{Emojis.Success} System pronouns cleared."); + } + } + + public async Task ChangePronouns(Context ctx, PKSystem target, string newPronouns) + { + ctx.CheckSystemPrivacy(target.Id, target.PronounPrivacy); + ctx.CheckSystem().CheckOwnSystem(target); + + newPronouns = newPronouns.NormalizeLineEndSpacing(); + if (newPronouns.Length > Limits.MaxPronounsLength) + throw Errors.StringTooLongError("Pronouns", newPronouns.Length, Limits.MaxPronounsLength); + + await ctx.Repository.UpdateSystem(target.Id, new SystemPatch { Pronouns = newPronouns }); + + await ctx.Reply($"{Emojis.Success} System pronouns changed (using {newPronouns.Length}/{Limits.MaxPronounsLength} characters)."); + } + + public async Task ShowPronouns(Context ctx, PKSystem target, ReplyFormat format) { ctx.CheckSystemPrivacy(target.Id, target.PronounPrivacy); var isOwnSystem = ctx.System.Id == target.Id; - var noPronounsSetMessage = "This system does not have pronouns set."; - if (isOwnSystem) - noPronounsSetMessage += $" To set some, type `{ctx.DefaultPrefix}system pronouns `"; + if (target.Pronouns == null) + { + var noPronounsSetMessage = "This system does not have pronouns set."; + if (isOwnSystem) + noPronounsSetMessage += $" To set some, type `{ctx.DefaultPrefix}system pronouns `"; - 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); - return; - } + await ctx.Reply(noPronounsSetMessage); + return; + } if (format == ReplyFormat.Raw) { @@ -512,34 +534,9 @@ public class SystemEdit return; } - if (!ctx.HasNext(false)) - { - await ctx.Reply($"{(isOwnSystem ? "Your" : "This system's")} current pronouns are **{target.Pronouns}**.\nTo print the pronouns with formatting, type `{ctx.DefaultPrefix}system pronouns -raw`." + await ctx.Reply($"{(isOwnSystem ? "Your" : "This system's")} current pronouns are **{target.Pronouns}**.\nTo print the pronouns with formatting, type `{ctx.DefaultPrefix}system pronouns -raw`." + (isOwnSystem ? $" To clear them, type `{ctx.DefaultPrefix}system pronouns -clear`." + $" Using {target.Pronouns.Length}/{Limits.MaxPronounsLength} characters." : "")); - return; - } - - ctx.CheckSystem().CheckOwnSystem(target); - - if (ctx.MatchClear() && await ctx.ConfirmClear("your system's pronouns")) - { - await ctx.Repository.UpdateSystem(target.Id, new SystemPatch { Pronouns = null }); - - await ctx.Reply($"{Emojis.Success} System pronouns cleared."); - } - else - { - var newPronouns = ctx.RemainderOrNull(false).NormalizeLineEndSpacing(); - if (newPronouns != null) - if (newPronouns.Length > Limits.MaxPronounsLength) - throw Errors.StringTooLongError("Pronouns", newPronouns.Length, Limits.MaxPronounsLength); - - await ctx.Repository.UpdateSystem(target.Id, new SystemPatch { Pronouns = newPronouns }); - - await ctx.Reply( - $"{Emojis.Success} System pronouns changed (using {newPronouns.Length}/{Limits.MaxPronounsLength} characters)."); - } } public async Task Avatar(Context ctx, PKSystem target) diff --git a/PluralKit.Bot/Utils/ContextUtils.cs b/PluralKit.Bot/Utils/ContextUtils.cs index ce353472..fbadc2e5 100644 --- a/PluralKit.Bot/Utils/ContextUtils.cs +++ b/PluralKit.Bot/Utils/ContextUtils.cs @@ -15,17 +15,17 @@ namespace PluralKit.Bot; public static class ContextUtils { - public static async Task ConfirmClear(this Context ctx, string toClear) + public static async Task ConfirmClear(this Context ctx, string toClear, bool? confirmYes = null) { - if (!await ctx.PromptYesNo($"{Emojis.Warn} Are you sure you want to clear {toClear}?", "Clear")) + if (!await ctx.PromptYesNo($"{Emojis.Warn} Are you sure you want to clear {toClear}?", "Clear", null, true, confirmYes)) throw Errors.GenericCancelled(); return true; } public static async Task PromptYesNo(this Context ctx, string msgString, string acceptButton, - User user = null, bool matchFlag = true) + User user = null, bool matchFlag = true, bool? flagValue = null) { - if (matchFlag && ctx.MatchFlag("y", "yes")) return true; + if (matchFlag && (flagValue ?? ctx.MatchFlag("y", "yes"))) return true; var prompt = new YesNoPrompt(ctx) { diff --git a/crates/command_definitions/src/system.rs b/crates/command_definitions/src/system.rs index f7262832..133400d0 100644 --- a/crates/command_definitions/src/system.rs +++ b/crates/command_definitions/src/system.rs @@ -126,6 +126,23 @@ pub fn edit() -> impl Iterator { ] .into_iter(); + let system_pronouns = tokens!(system_target, ("pronouns", ["prns"])); + let system_pronouns_cmd = [ + command!(system_pronouns => "system_show_pronouns").help("Shows the system's pronouns"), + ] + .into_iter(); + + let system_pronouns_self = tokens!(system, ("pronouns", ["prns"])); + let system_pronouns_self_cmd = [ + command!(system_pronouns_self => "system_show_pronouns_self").help("Shows your system's pronouns"), + command!(system_pronouns_self, ("clear", ["c"]) => "system_clear_pronouns") + .flag(("yes", ["y"])) + .help("Clears your system's pronouns"), + command!(system_pronouns_self, ("pronouns", OpaqueString) => "system_change_pronouns") + .help("Changes your system's pronouns"), + ] + .into_iter(); + system_new_cmd .chain(system_name_self_cmd) .chain(system_server_name_self_cmd) @@ -133,11 +150,13 @@ pub fn edit() -> impl Iterator { .chain(system_color_self_cmd) .chain(system_tag_self_cmd) .chain(system_server_tag_self_cmd) + .chain(system_pronouns_self_cmd) .chain(system_name_cmd) .chain(system_server_name_cmd) .chain(system_description_cmd) .chain(system_color_cmd) .chain(system_tag_cmd) .chain(system_server_tag_cmd) + .chain(system_pronouns_cmd) .chain(system_info_cmd) } \ No newline at end of file From 293570c91c49ff7befd58d4dd499f84af22f99b5 Mon Sep 17 00:00:00 2001 From: dusk Date: Tue, 1 Apr 2025 16:50:43 +0900 Subject: [PATCH 076/179] feat: add confirm yes flag to clear handlers for system edit --- PluralKit.Bot/CommandMeta/CommandTree.cs | 18 ++++---- PluralKit.Bot/Commands/SystemEdit.cs | 29 ++++++------ crates/command_definitions/src/system.rs | 56 ++++++++++++------------ 3 files changed, 52 insertions(+), 51 deletions(-) diff --git a/PluralKit.Bot/CommandMeta/CommandTree.cs b/PluralKit.Bot/CommandMeta/CommandTree.cs index 6cbf3245..2446940e 100644 --- a/PluralKit.Bot/CommandMeta/CommandTree.cs +++ b/PluralKit.Bot/CommandMeta/CommandTree.cs @@ -31,35 +31,35 @@ public partial class CommandTree Commands.SystemShowNameSelf(_, var flags) => ctx.Execute(SystemRename, m => m.ShowName(ctx, ctx.System, flags.GetReplyFormat())), Commands.SystemShowName(var param, var flags) => ctx.Execute(SystemRename, m => m.ShowName(ctx, param.target, flags.GetReplyFormat())), Commands.SystemRename(var param, _) => ctx.Execute(SystemRename, m => m.Rename(ctx, ctx.System, param.name)), - Commands.SystemClearName(var param, _) => ctx.Execute(SystemRename, m => m.ClearName(ctx, ctx.System)), + Commands.SystemClearName(var param, var flags) => ctx.Execute(SystemRename, m => m.ClearName(ctx, ctx.System, flags.yes)), Commands.SystemShowServerNameSelf(_, var flags) => ctx.Execute(SystemServerName, m => m.ShowServerName(ctx, ctx.System, flags.GetReplyFormat())), Commands.SystemShowServerName(var param, var flags) => ctx.Execute(SystemServerName, m => m.ShowServerName(ctx, param.target, flags.GetReplyFormat())), - Commands.SystemClearServerName(var param, _) => ctx.Execute(SystemServerName, m => m.ClearServerName(ctx, ctx.System)), + Commands.SystemClearServerName(var param, var flags) => ctx.Execute(SystemServerName, m => m.ClearServerName(ctx, ctx.System, flags.yes)), Commands.SystemRenameServerName(var param, _) => ctx.Execute(SystemServerName, m => m.RenameServerName(ctx, ctx.System, param.name)), Commands.SystemShowDescriptionSelf(_, var flags) => ctx.Execute(SystemDesc, m => m.ShowDescription(ctx, ctx.System, flags.GetReplyFormat())), Commands.SystemShowDescription(var param, var flags) => ctx.Execute(SystemDesc, m => m.ShowDescription(ctx, param.target, flags.GetReplyFormat())), - Commands.SystemClearDescription(var param, _) => ctx.Execute(SystemDesc, m => m.ClearDescription(ctx, ctx.System)), + Commands.SystemClearDescription(var param, var flags) => ctx.Execute(SystemDesc, m => m.ClearDescription(ctx, ctx.System, flags.yes)), Commands.SystemChangeDescription(var param, _) => ctx.Execute(SystemDesc, m => m.ChangeDescription(ctx, ctx.System, param.description)), Commands.SystemShowColorSelf(_, var flags) => ctx.Execute(SystemColor, m => m.ShowColor(ctx, ctx.System, flags.GetReplyFormat())), Commands.SystemShowColor(var param, var flags) => ctx.Execute(SystemColor, m => m.ShowColor(ctx, param.target, flags.GetReplyFormat())), - Commands.SystemClearColor(var param, _) => ctx.Execute(SystemColor, m => m.ClearColor(ctx, ctx.System)), + Commands.SystemClearColor(var param, var flags) => ctx.Execute(SystemColor, m => m.ClearColor(ctx, ctx.System, flags.yes)), Commands.SystemChangeColor(var param, _) => ctx.Execute(SystemColor, m => m.ChangeColor(ctx, ctx.System, param.color)), Commands.SystemShowTagSelf(_, var flags) => ctx.Execute(SystemTag, m => m.ShowTag(ctx, ctx.System, flags.GetReplyFormat())), Commands.SystemShowTag(var param, var flags) => ctx.Execute(SystemTag, m => m.ShowTag(ctx, param.target, flags.GetReplyFormat())), - Commands.SystemClearTag(var param, _) => ctx.Execute(SystemTag, m => m.ClearTag(ctx, ctx.System)), + Commands.SystemClearTag(var param, var flags) => ctx.Execute(SystemTag, m => m.ClearTag(ctx, ctx.System, flags.yes)), Commands.SystemChangeTag(var param, _) => ctx.Execute(SystemTag, m => m.ChangeTag(ctx, ctx.System, param.tag)), Commands.SystemShowServerTagSelf(_, var flags) => ctx.Execute(SystemServerTag, m => m.ShowServerTag(ctx, ctx.System, flags.GetReplyFormat())), Commands.SystemShowServerTag(var param, var flags) => ctx.Execute(SystemServerTag, m => m.ShowServerTag(ctx, param.target, flags.GetReplyFormat())), - Commands.SystemClearServerTag(var param, _) => ctx.Execute(SystemServerTag, m => m.ClearServerTag(ctx, ctx.System)), + Commands.SystemClearServerTag(var param, var flags) => ctx.Execute(SystemServerTag, m => m.ClearServerTag(ctx, ctx.System, flags.yes)), Commands.SystemChangeServerTag(var param, _) => ctx.Execute(SystemServerTag, m => m.ChangeServerTag(ctx, ctx.System, param.tag)), Commands.SystemShowPronounsSelf(_, var flags) => ctx.Execute(SystemPronouns, m => m.ShowPronouns(ctx, ctx.System, flags.GetReplyFormat())), Commands.SystemShowPronouns(var param, var flags) => ctx.Execute(SystemPronouns, m => m.ShowPronouns(ctx, param.target, flags.GetReplyFormat())), Commands.SystemClearPronouns(var param, var flags) => ctx.Execute(SystemPronouns, m => m.ClearPronouns(ctx, ctx.System, flags.yes)), Commands.SystemChangePronouns(var param, _) => ctx.Execute(SystemPronouns, m => m.ChangePronouns(ctx, ctx.System, param.pronouns)), _ => - // this should only ever occur when deving if commands are not implemented... - ctx.Reply( - $"{Emojis.Error} Parsed command {ctx.Parameters.Callback().AsCode()} not implemented in PluralKit.Bot!"), + // this should only ever occur when deving if commands are not implemented... + ctx.Reply( + $"{Emojis.Error} Parsed command {ctx.Parameters.Callback().AsCode()} not implemented in PluralKit.Bot!"), }; if (ctx.Match("system", "s")) return HandleSystemCommand(ctx); diff --git a/PluralKit.Bot/Commands/SystemEdit.cs b/PluralKit.Bot/Commands/SystemEdit.cs index c0eba729..fd46a6e2 100644 --- a/PluralKit.Bot/Commands/SystemEdit.cs +++ b/PluralKit.Bot/Commands/SystemEdit.cs @@ -64,12 +64,12 @@ public class SystemEdit return; } - public async Task ClearName(Context ctx, PKSystem target) + public async Task ClearName(Context ctx, PKSystem target, bool flagConfirmYes) { ctx.CheckSystemPrivacy(target.Id, target.NamePrivacy); ctx.CheckSystem().CheckOwnSystem(target); - if (await ctx.ConfirmClear("your system's name")) + if (await ctx.ConfirmClear("your system's name", flagConfirmYes)) { await ctx.Repository.UpdateSystem(target.Id, new SystemPatch { Name = null }); @@ -128,12 +128,12 @@ public class SystemEdit return; } - public async Task ClearServerName(Context ctx, PKSystem target) + public async Task ClearServerName(Context ctx, PKSystem target, bool flagConfirmYes) { ctx.CheckGuildContext(); ctx.CheckSystem().CheckOwnSystem(target); - if (await ctx.ConfirmClear("your system's name for this server")) + if (await ctx.ConfirmClear("your system's name for this server", flagConfirmYes)) { await ctx.Repository.UpdateSystemGuild(target.Id, ctx.Guild.Id, new SystemGuildPatch { DisplayName = null }); @@ -154,12 +154,12 @@ public class SystemEdit await ctx.Reply($"{Emojis.Success} System name for this server changed (using {newSystemGuildName.Length}/{Limits.MaxSystemNameLength} characters)."); } - public async Task ClearDescription(Context ctx, PKSystem target) + public async Task ClearDescription(Context ctx, PKSystem target, bool flagConfirmYes) { ctx.CheckSystemPrivacy(target.Id, target.DescriptionPrivacy); ctx.CheckSystem().CheckOwnSystem(target); - if (await ctx.ConfirmClear("your system's description")) + if (await ctx.ConfirmClear("your system's description", flagConfirmYes)) { await ctx.Repository.UpdateSystem(target.Id, new SystemPatch { Description = null }); @@ -237,12 +237,15 @@ public class SystemEdit .Build()); } - public async Task ClearColor(Context ctx, PKSystem target) + public async Task ClearColor(Context ctx, PKSystem target, bool flagConfirmYes) { ctx.CheckSystem().CheckOwnSystem(target); - await ctx.Repository.UpdateSystem(target.Id, new SystemPatch { Color = Partial.Null() }); - await ctx.Reply($"{Emojis.Success} System color cleared."); + if (await ctx.ConfirmClear("your system's color", flagConfirmYes)) + { + await ctx.Repository.UpdateSystem(target.Id, new SystemPatch { Color = Partial.Null() }); + await ctx.Reply($"{Emojis.Success} System color cleared."); + } } public async Task ShowColor(Context ctx, PKSystem target, ReplyFormat format) @@ -277,11 +280,11 @@ public class SystemEdit .Build()); } - public async Task ClearTag(Context ctx, PKSystem target) + public async Task ClearTag(Context ctx, PKSystem target, bool flagConfirmYes) { ctx.CheckSystem().CheckOwnSystem(target); - if (await ctx.ConfirmClear("your system's tag")) + if (await ctx.ConfirmClear("your system's tag", flagConfirmYes)) { await ctx.Repository.UpdateSystem(target.Id, new SystemPatch { Tag = null }); @@ -402,11 +405,11 @@ public class SystemEdit $"You currently have no system tag specific to the server '{ctx.Guild.Name}'. To set one, type `{ctx.DefaultPrefix}s servertag `. To disable the system tag in the current server, type `{ctx.DefaultPrefix}s servertag -disable`."); } - public async Task ClearServerTag(Context ctx, PKSystem target) + public async Task ClearServerTag(Context ctx, PKSystem target, bool flagConfirmYes) { ctx.CheckSystem().CheckOwnSystem(target).CheckGuildContext(); - if (!await ctx.ConfirmClear("your system's server tag")) + if (!await ctx.ConfirmClear("your system's server tag", flagConfirmYes)) return; var settings = await ctx.Repository.GetSystemGuild(ctx.Guild.Id, target.Id); diff --git a/crates/command_definitions/src/system.rs b/crates/command_definitions/src/system.rs index 133400d0..08386c94 100644 --- a/crates/command_definitions/src/system.rs +++ b/crates/command_definitions/src/system.rs @@ -29,15 +29,14 @@ pub fn edit() -> impl Iterator { }); let system_name = tokens!(system_target, "name"); - let system_name_cmd = [ - command!(system_name => "system_show_name").help("Shows the systems name"), - ] - .into_iter(); + let system_name_cmd = + [command!(system_name => "system_show_name").help("Shows the systems name")].into_iter(); let system_name_self = tokens!(system, "name"); let system_name_self_cmd = [ command!(system_name_self => "system_show_name_self").help("Shows your system's name"), command!(system_name_self, ("clear", ["c"]) => "system_clear_name") + .flag(("yes", ["y"])) .help("Clears your system's name"), command!(system_name_self, ("name", OpaqueString) => "system_rename") .help("Renames your system"), @@ -45,10 +44,8 @@ pub fn edit() -> impl Iterator { .into_iter(); let system_server_name = tokens!(system_target, ("servername", ["sn", "guildname"])); - let system_server_name_cmd = [ - command!(system_server_name => "system_show_server_name") - .help("Shows the system's server name"), - ] + let system_server_name_cmd = [command!(system_server_name => "system_show_server_name") + .help("Shows the system's server name")] .into_iter(); let system_server_name_self = tokens!(system, ("servername", ["sn", "guildname"])); @@ -56,6 +53,7 @@ pub fn edit() -> impl Iterator { command!(system_server_name_self => "system_show_server_name_self") .help("Shows your system's server name"), command!(system_server_name_self, ("clear", ["c"]) => "system_clear_server_name") + .flag(("yes", ["y"])) .help("Clears your system's server name"), command!(system_server_name_self, ("name", OpaqueString) => "system_rename_server_name") .help("Renames your system's server name"), @@ -63,15 +61,15 @@ pub fn edit() -> impl Iterator { .into_iter(); let system_description = tokens!(system_target, ("description", ["desc", "d"])); - let system_description_cmd = [ - command!(system_description => "system_show_description").help("Shows the system's description"), - ] + let system_description_cmd = [command!(system_description => "system_show_description") + .help("Shows the system's description")] .into_iter(); let system_description_self = tokens!(system, ("description", ["desc", "d"])); let system_description_self_cmd = [ command!(system_description_self => "system_show_description_self").help("Shows your system's description"), command!(system_description_self, ("clear", ["c"]) => "system_clear_description") + .flag(("yes", ["y"])) .help("Clears your system's description"), command!(system_description_self, ("description", OpaqueString) => "system_change_description") .help("Changes your system's description"), @@ -79,15 +77,15 @@ pub fn edit() -> impl Iterator { .into_iter(); let system_color = tokens!(system_target, ("color", ["colour"])); - let system_color_cmd = [ - command!(system_color => "system_show_color").help("Shows the system's color"), - ] - .into_iter(); + let system_color_cmd = + [command!(system_color => "system_show_color").help("Shows the system's color")] + .into_iter(); let system_color_self = tokens!(system, ("color", ["colour"])); let system_color_self_cmd = [ command!(system_color_self => "system_show_color_self").help("Shows your system's color"), command!(system_color_self, ("clear", ["c"]) => "system_clear_color") + .flag(("yes", ["y"])) .help("Clears your system's color"), command!(system_color_self, ("color", OpaqueString) => "system_change_color") .help("Changes your system's color"), @@ -95,15 +93,14 @@ pub fn edit() -> impl Iterator { .into_iter(); let system_tag = tokens!(system_target, ("tag", ["suffix"])); - let system_tag_cmd = [ - command!(system_tag => "system_show_tag").help("Shows the system's tag"), - ] - .into_iter(); + let system_tag_cmd = + [command!(system_tag => "system_show_tag").help("Shows the system's tag")].into_iter(); let system_tag_self = tokens!(system, ("tag", ["suffix"])); let system_tag_self_cmd = [ command!(system_tag_self => "system_show_tag_self").help("Shows your system's tag"), command!(system_tag_self, ("clear", ["c"]) => "system_clear_tag") + .flag(("yes", ["y"])) .help("Clears your system's tag"), command!(system_tag_self, ("tag", OpaqueString) => "system_change_tag") .help("Changes your system's tag"), @@ -111,15 +108,16 @@ pub fn edit() -> impl Iterator { .into_iter(); let system_server_tag = tokens!(system_target, ("servertag", ["st", "guildtag"])); - let system_server_tag_cmd = [ - command!(system_server_tag => "system_show_server_tag").help("Shows the system's server tag"), - ] + let system_server_tag_cmd = [command!(system_server_tag => "system_show_server_tag") + .help("Shows the system's server tag")] .into_iter(); let system_server_tag_self = tokens!(system, ("servertag", ["st", "guildtag"])); let system_server_tag_self_cmd = [ - command!(system_server_tag_self => "system_show_server_tag_self").help("Shows your system's server tag"), + command!(system_server_tag_self => "system_show_server_tag_self") + .help("Shows your system's server tag"), command!(system_server_tag_self, ("clear", ["c"]) => "system_clear_server_tag") + .flag(("yes", ["y"])) .help("Clears your system's server tag"), command!(system_server_tag_self, ("tag", OpaqueString) => "system_change_server_tag") .help("Changes your system's server tag"), @@ -127,14 +125,14 @@ pub fn edit() -> impl Iterator { .into_iter(); let system_pronouns = tokens!(system_target, ("pronouns", ["prns"])); - let system_pronouns_cmd = [ - command!(system_pronouns => "system_show_pronouns").help("Shows the system's pronouns"), - ] - .into_iter(); + let system_pronouns_cmd = + [command!(system_pronouns => "system_show_pronouns").help("Shows the system's pronouns")] + .into_iter(); let system_pronouns_self = tokens!(system, ("pronouns", ["prns"])); let system_pronouns_self_cmd = [ - command!(system_pronouns_self => "system_show_pronouns_self").help("Shows your system's pronouns"), + command!(system_pronouns_self => "system_show_pronouns_self") + .help("Shows your system's pronouns"), command!(system_pronouns_self, ("clear", ["c"]) => "system_clear_pronouns") .flag(("yes", ["y"])) .help("Clears your system's pronouns"), @@ -159,4 +157,4 @@ pub fn edit() -> impl Iterator { .chain(system_server_tag_cmd) .chain(system_pronouns_cmd) .chain(system_info_cmd) -} \ No newline at end of file +} From b62340cbb33f9bd57c69482e50255b93ec5e3777 Mon Sep 17 00:00:00 2001 From: dusk Date: Fri, 4 Apr 2025 03:50:07 +0900 Subject: [PATCH 077/179] feat: implement system avatar commands --- PluralKit.Bot/CommandMeta/CommandTree.cs | 15 +- .../CommandSystem/Context/ContextAvatarExt.cs | 55 +++---- .../Context/ContextEntityArgumentsExt.cs | 8 ++ .../Context/ContextParametersExt.cs | 8 ++ PluralKit.Bot/CommandSystem/Parameters.cs | 4 + PluralKit.Bot/Commands/SystemEdit.cs | 136 +++++++++--------- crates/command_definitions/src/system.rs | 19 +++ crates/command_parser/src/parameter.rs | 6 + crates/commands/src/bin/write_cs_glue.rs | 2 + crates/commands/src/commands.udl | 1 + crates/commands/src/lib.rs | 2 + 11 files changed, 155 insertions(+), 101 deletions(-) diff --git a/PluralKit.Bot/CommandMeta/CommandTree.cs b/PluralKit.Bot/CommandMeta/CommandTree.cs index 2446940e..ab46b58e 100644 --- a/PluralKit.Bot/CommandMeta/CommandTree.cs +++ b/PluralKit.Bot/CommandMeta/CommandTree.cs @@ -56,6 +56,19 @@ public partial class CommandTree Commands.SystemShowPronouns(var param, var flags) => ctx.Execute(SystemPronouns, m => m.ShowPronouns(ctx, param.target, flags.GetReplyFormat())), Commands.SystemClearPronouns(var param, var flags) => ctx.Execute(SystemPronouns, m => m.ClearPronouns(ctx, ctx.System, flags.yes)), Commands.SystemChangePronouns(var param, _) => ctx.Execute(SystemPronouns, m => m.ChangePronouns(ctx, ctx.System, param.pronouns)), + Commands.SystemShowAvatarSelf(_, var flags) => ((Func)(() => + { + // we want to change avatar if an attached image is passed + // we can't have a separate parsed command for this since the parser can't be aware of any attachments + var attachedImage = ctx.ExtractImageFromAttachment(); + if (attachedImage is { } image) + return ctx.Execute(SystemAvatar, m => m.ChangeAvatar(ctx, ctx.System, image)); + // if no attachment show the avatar like intended + return ctx.Execute(SystemAvatar, m => m.ShowAvatar(ctx, ctx.System, flags.GetReplyFormat())); + }))(), + Commands.SystemShowAvatar(var param, var flags) => ctx.Execute(SystemAvatar, m => m.ShowAvatar(ctx, param.target, flags.GetReplyFormat())), + Commands.SystemClearAvatar(var param, var flags) => ctx.Execute(SystemAvatar, m => m.ClearAvatar(ctx, ctx.System, flags.yes)), + Commands.SystemChangeAvatar(var param, _) => ctx.Execute(SystemAvatar, m => m.ChangeAvatar(ctx, ctx.System, param.avatar)), _ => // this should only ever occur when deving if commands are not implemented... ctx.Reply( @@ -306,8 +319,6 @@ public partial class CommandTree { if (ctx.Match("banner", "splash", "cover")) await ctx.CheckSystem(target).Execute(SystemBannerImage, m => m.BannerImage(ctx, target)); - else if (ctx.Match("avatar", "picture", "icon", "image", "pic", "pfp")) - await ctx.CheckSystem(target).Execute(SystemAvatar, m => m.Avatar(ctx, target)); else if (ctx.Match("serveravatar", "sa", "servericon", "serverimage", "serverpfp", "serverpic", "savatar", "spic", "guildavatar", "guildpic", "guildicon", "sicon", "spfp")) await ctx.CheckSystem(target).Execute(SystemServerAvatar, m => m.ServerAvatar(ctx, target)); diff --git a/PluralKit.Bot/CommandSystem/Context/ContextAvatarExt.cs b/PluralKit.Bot/CommandSystem/Context/ContextAvatarExt.cs index 3501b53b..0c99c94f 100644 --- a/PluralKit.Bot/CommandSystem/Context/ContextAvatarExt.cs +++ b/PluralKit.Bot/CommandSystem/Context/ContextAvatarExt.cs @@ -6,34 +6,8 @@ namespace PluralKit.Bot; public static class ContextAvatarExt { - public static async Task MatchImage(this Context ctx) + public static ParsedImage? ExtractImageFromAttachment(this Context ctx) { - // If we have a user @mention/ID, use their avatar - if (await ctx.MatchUser() is { } user) - { - var url = user.AvatarUrl("png", 256); - return new ParsedImage { Url = url, Source = AvatarSource.User, SourceUser = user }; - } - - // If we have raw or plaintext, don't try to parse as a URL - if (ctx.PeekMatchFormat() != ReplyFormat.Standard) - return null; - - // If we have a positional argument, try to parse it as a URL - var arg = ctx.RemainderOrNull(); - if (arg != null) - { - // Allow surrounding the URL with to "de-embed" - if (arg.StartsWith("<") && arg.EndsWith(">")) - arg = arg.Substring(1, arg.Length - 2); - - if (!Core.MiscUtils.TryMatchUri(arg, out var uri)) - throw Errors.InvalidUrl; - - // ToString URL-decodes, which breaks URLs to spaces; AbsoluteUri doesn't - return new ParsedImage { Url = uri.AbsoluteUri, Source = AvatarSource.Url }; - } - // If we have an attachment, use that if (ctx.Message.Attachments.FirstOrDefault() is { } attachment) { @@ -51,6 +25,33 @@ public static class ContextAvatarExt // and if there are no attachments (which would have been caught just before) return null; } + public static async Task GetUserPfp(this Context ctx, string arg) + { + // If we have a user @mention/ID, use their avatar + if (await ctx.ParseUser(arg) is { } user) + { + var url = user.AvatarUrl("png", 256); + return new ParsedImage { Url = url, Source = AvatarSource.User, SourceUser = user }; + } + + return null; + } + public static ParsedImage ParseImage(this Context ctx, string arg) + { + // Allow surrounding the URL with to "de-embed" + if (arg.StartsWith("<") && arg.EndsWith(">")) + arg = arg.Substring(1, arg.Length - 2); + + if (!Core.MiscUtils.TryMatchUri(arg, out var uri)) + throw Errors.InvalidUrl; + + // ToString URL-decodes, which breaks URLs to spaces; AbsoluteUri doesn't + return new ParsedImage { Url = uri.AbsoluteUri, Source = AvatarSource.Url }; + } + public static async Task MatchImage(this Context ctx) + { + throw new NotImplementedException(); + } } public struct ParsedImage diff --git a/PluralKit.Bot/CommandSystem/Context/ContextEntityArgumentsExt.cs b/PluralKit.Bot/CommandSystem/Context/ContextEntityArgumentsExt.cs index 104f1ad8..6eba5039 100644 --- a/PluralKit.Bot/CommandSystem/Context/ContextEntityArgumentsExt.cs +++ b/PluralKit.Bot/CommandSystem/Context/ContextEntityArgumentsExt.cs @@ -10,6 +10,14 @@ namespace PluralKit.Bot; public static class ContextEntityArgumentsExt { + public static async Task ParseUser(this Context ctx, string arg) + { + if (arg.TryParseMention(out var id)) + return await ctx.Cache.GetOrFetchUser(ctx.Rest, id); + + return null; + } + public static async Task MatchUser(this Context ctx) { var text = ctx.PeekArgument(); diff --git a/PluralKit.Bot/CommandSystem/Context/ContextParametersExt.cs b/PluralKit.Bot/CommandSystem/Context/ContextParametersExt.cs index 4a34a43a..cf2fa6c7 100644 --- a/PluralKit.Bot/CommandSystem/Context/ContextParametersExt.cs +++ b/PluralKit.Bot/CommandSystem/Context/ContextParametersExt.cs @@ -51,4 +51,12 @@ public static class ContextParametersExt param => (param as Parameter.Toggle)?.value ); } + + public static async Task ParamResolveAvatar(this Context ctx, string param_name) + { + return await ctx.Parameters.ResolveParameter( + ctx, param_name, + param => (param as Parameter.Avatar)?.avatar + ); + } } \ No newline at end of file diff --git a/PluralKit.Bot/CommandSystem/Parameters.cs b/PluralKit.Bot/CommandSystem/Parameters.cs index d4aa2e79..48aaa583 100644 --- a/PluralKit.Bot/CommandSystem/Parameters.cs +++ b/PluralKit.Bot/CommandSystem/Parameters.cs @@ -1,4 +1,5 @@ using System.Diagnostics; +using Myriad.Types; using PluralKit.Core; using uniffi.commands; @@ -13,6 +14,7 @@ public abstract record Parameter() public record PrivacyLevel(string level): Parameter; public record Toggle(bool value): Parameter; public record Opaque(string value): Parameter; + public record Avatar(ParsedImage avatar): Parameter; } public class Parameters @@ -79,6 +81,8 @@ public class Parameters return new Parameter.Toggle(toggle.toggle); case uniffi.commands.Parameter.OpaqueString opaque: return new Parameter.Opaque(opaque.raw); + case uniffi.commands.Parameter.Avatar avatar: + return new Parameter.Avatar(await ctx.GetUserPfp(avatar.avatar) ?? ctx.ParseImage(avatar.avatar)); } return null; } diff --git a/PluralKit.Bot/Commands/SystemEdit.cs b/PluralKit.Bot/Commands/SystemEdit.cs index fd46a6e2..582d0df3 100644 --- a/PluralKit.Bot/Commands/SystemEdit.cs +++ b/PluralKit.Bot/Commands/SystemEdit.cs @@ -542,90 +542,82 @@ public class SystemEdit + $" Using {target.Pronouns.Length}/{Limits.MaxPronounsLength} characters." : "")); } - public async Task Avatar(Context ctx, PKSystem target) + public async Task ClearAvatar(Context ctx, PKSystem target, bool flagConfirmYes) { - async Task ClearIcon() - { - ctx.CheckOwnSystem(target); + ctx.CheckSystemPrivacy(target.Id, target.AvatarPrivacy); + ctx.CheckSystem().CheckOwnSystem(target); + if (await ctx.ConfirmClear("your system's icon", flagConfirmYes)) + { await ctx.Repository.UpdateSystem(target.Id, new SystemPatch { AvatarUrl = null }); await ctx.Reply($"{Emojis.Success} System icon cleared."); } + } - async Task SetIcon(ParsedImage img) + public async Task ShowAvatar(Context ctx, PKSystem target, ReplyFormat format) + { + ctx.CheckSystemPrivacy(target.Id, target.AvatarPrivacy); + var isOwnSystem = target.Id == ctx.System?.Id; + + if ((target.AvatarUrl?.Trim() ?? "").Length > 0) { - ctx.CheckOwnSystem(target); + if (!target.AvatarPrivacy.CanAccess(ctx.DirectLookupContextFor(target.Id))) + throw new PKSyntaxError("This system does not have an icon set or it is private."); - img = await _avatarHosting.TryRehostImage(img, AvatarHostingService.RehostedImageType.Avatar, ctx.Author.Id, ctx.System); - await _avatarHosting.VerifyAvatarOrThrow(img.Url); - - await ctx.Repository.UpdateSystem(target.Id, new SystemPatch { AvatarUrl = img.CleanUrl ?? img.Url }); - - var msg = img.Source switch + switch (format) { - AvatarSource.User => - $"{Emojis.Success} System icon changed to {img.SourceUser?.Username}'s avatar!\n{Emojis.Warn} If {img.SourceUser?.Username} changes their avatar, the system icon will need to be re-set.", - AvatarSource.Url => $"{Emojis.Success} System icon changed to the image at the given URL.", - AvatarSource.HostedCdn => $"{Emojis.Success} System icon changed to attached image.", - AvatarSource.Attachment => - $"{Emojis.Success} System icon changed to attached image.\n{Emojis.Warn} If you delete the message containing the attachment, the system icon will stop working.", - _ => throw new ArgumentOutOfRangeException() - }; - - // The attachment's already right there, no need to preview it. - var hasEmbed = img.Source != AvatarSource.Attachment && img.Source != AvatarSource.HostedCdn; - await (hasEmbed - ? ctx.Reply(msg, new EmbedBuilder().Image(new Embed.EmbedImage(img.Url)).Build()) - : ctx.Reply(msg)); - } - - async Task ShowIcon() - { - if ((target.AvatarUrl?.Trim() ?? "").Length > 0) - { - if (!target.AvatarPrivacy.CanAccess(ctx.DirectLookupContextFor(target.Id))) - throw new PKSyntaxError("This system does not have an icon set or it is private."); - switch (ctx.MatchFormat()) - { - case ReplyFormat.Raw: - await ctx.Reply($"`{target.AvatarUrl.TryGetCleanCdnUrl()}`"); - break; - case ReplyFormat.Plaintext: - var ebP = new EmbedBuilder() - .Description($"Showing icon for system {target.NameFor(ctx)} (`{target.DisplayHid(ctx.Config)}`)"); - await ctx.Reply(text: $"<{target.AvatarUrl.TryGetCleanCdnUrl()}>", embed: ebP.Build()); - break; - default: - var ebS = new EmbedBuilder() - .Title("System icon") - .Image(new Embed.EmbedImage(target.AvatarUrl.TryGetCleanCdnUrl())); - if (target.Id == ctx.System?.Id) - ebS.Description($"To clear, use `{ctx.DefaultPrefix}system icon clear`."); - await ctx.Reply(embed: ebS.Build()); - break; - } - } - else - { - var isOwner = target.Id == ctx.System?.Id; - throw new PKSyntaxError( - $"This system does not have an icon set{(isOwner ? "" : " or it is private")}." - + (isOwner ? " Set one by attaching an image to this command, or by passing an image URL or @mention." : "")); + case ReplyFormat.Raw: + await ctx.Reply($"`{target.AvatarUrl.TryGetCleanCdnUrl()}`"); + break; + case ReplyFormat.Plaintext: + var ebP = new EmbedBuilder() + .Description($"Showing icon for system {target.NameFor(ctx)} (`{target.DisplayHid(ctx.Config)}`)"); + await ctx.Reply(text: $"<{target.AvatarUrl.TryGetCleanCdnUrl()}>", embed: ebP.Build()); + break; + default: + var ebS = new EmbedBuilder() + .Title("System icon") + .Image(new Embed.EmbedImage(target.AvatarUrl.TryGetCleanCdnUrl())); + if (target.Id == ctx.System?.Id) + ebS.Description($"To clear, use `{ctx.DefaultPrefix}system icon clear`."); + await ctx.Reply(embed: ebS.Build()); + break; } } - - if (target != null && target?.Id != ctx.System?.Id) - { - await ShowIcon(); - return; - } - - if (ctx.MatchClear() && await ctx.ConfirmClear("your system's icon")) - await ClearIcon(); - else if (await ctx.MatchImage() is { } img) - await SetIcon(img); else - await ShowIcon(); + { + throw new PKSyntaxError( + $"This system does not have an icon set{(isOwnSystem ? "" : " or it is private")}." + + (isOwnSystem ? " Set one by attaching an image to this command, or by passing an image URL or @mention." : "")); + } + } + + public async Task ChangeAvatar(Context ctx, PKSystem target, ParsedImage img) + { + ctx.CheckSystemPrivacy(target.Id, target.AvatarPrivacy); + ctx.CheckSystem().CheckOwnSystem(target); + + img = await _avatarHosting.TryRehostImage(img, AvatarHostingService.RehostedImageType.Avatar, ctx.Author.Id, ctx.System); + await _avatarHosting.VerifyAvatarOrThrow(img.Url); + + await ctx.Repository.UpdateSystem(target.Id, new SystemPatch { AvatarUrl = img.CleanUrl ?? img.Url }); + + var msg = img.Source switch + { + AvatarSource.User => + $"{Emojis.Success} System icon changed to {img.SourceUser?.Username}'s avatar!\n{Emojis.Warn} If {img.SourceUser?.Username} changes their avatar, the system icon will need to be re-set.", + AvatarSource.Url => $"{Emojis.Success} System icon changed to the image at the given URL.", + AvatarSource.HostedCdn => $"{Emojis.Success} System icon changed to attached image.", + AvatarSource.Attachment => + $"{Emojis.Success} System icon changed to attached image.\n{Emojis.Warn} If you delete the message containing the attachment, the system icon will stop working.", + _ => throw new ArgumentOutOfRangeException() + }; + + // The attachment's already right there, no need to preview it. + var hasEmbed = img.Source != AvatarSource.Attachment && img.Source != AvatarSource.HostedCdn; + await (hasEmbed + ? ctx.Reply(msg, new EmbedBuilder().Image(new Embed.EmbedImage(img.Url)).Build()) + : ctx.Reply(msg)); } public async Task ServerAvatar(Context ctx, PKSystem target) diff --git a/crates/command_definitions/src/system.rs b/crates/command_definitions/src/system.rs index 08386c94..0980ca87 100644 --- a/crates/command_definitions/src/system.rs +++ b/crates/command_definitions/src/system.rs @@ -141,6 +141,23 @@ pub fn edit() -> impl Iterator { ] .into_iter(); + let system_avatar = tokens!(system_target, ("avatar", ["pfp"])); + let system_avatar_cmd = + [command!(system_avatar => "system_show_avatar").help("Shows the system's avatar")] + .into_iter(); + + let system_avatar_self = tokens!(system, ("avatar", ["pfp"])); + let system_avatar_self_cmd = [ + command!(system_avatar_self => "system_show_avatar_self") + .help("Shows your system's avatar"), + command!(system_avatar_self, ("clear", ["c"]) => "system_clear_avatar") + .flag(("yes", ["y"])) + .help("Clears your system's avatar"), + command!(system_avatar_self, ("avatar", Avatar) => "system_change_avatar") + .help("Changes your system's avatar"), + ] + .into_iter(); + system_new_cmd .chain(system_name_self_cmd) .chain(system_server_name_self_cmd) @@ -149,6 +166,7 @@ pub fn edit() -> impl Iterator { .chain(system_tag_self_cmd) .chain(system_server_tag_self_cmd) .chain(system_pronouns_self_cmd) + .chain(system_avatar_self_cmd) .chain(system_name_cmd) .chain(system_server_name_cmd) .chain(system_description_cmd) @@ -156,5 +174,6 @@ pub fn edit() -> impl Iterator { .chain(system_tag_cmd) .chain(system_server_tag_cmd) .chain(system_pronouns_cmd) + .chain(system_avatar_cmd) .chain(system_info_cmd) } diff --git a/crates/command_parser/src/parameter.rs b/crates/command_parser/src/parameter.rs index b2c9d5ff..14e79437 100644 --- a/crates/command_parser/src/parameter.rs +++ b/crates/command_parser/src/parameter.rs @@ -15,6 +15,7 @@ pub enum ParameterValue { MemberPrivacyTarget(String), PrivacyLevel(String), Toggle(bool), + Avatar(String), } #[derive(Debug, Clone, PartialEq, Eq, Hash)] @@ -44,6 +45,7 @@ impl Display for Parameter { ParameterKind::MemberPrivacyTarget => write!(f, ""), ParameterKind::PrivacyLevel => write!(f, "[privacy level]"), ParameterKind::Toggle => write!(f, "on/off"), + ParameterKind::Avatar => write!(f, ""), } } } @@ -75,6 +77,7 @@ pub enum ParameterKind { MemberPrivacyTarget, PrivacyLevel, Toggle, + Avatar, } impl ParameterKind { @@ -87,6 +90,7 @@ impl ParameterKind { ParameterKind::MemberPrivacyTarget => "member_privacy_target", ParameterKind::PrivacyLevel => "privacy_level", ParameterKind::Toggle => "toggle", + ParameterKind::Avatar => "avatar", } } @@ -96,6 +100,7 @@ impl ParameterKind { pub(crate) fn match_value(&self, input: &str) -> Result { match self { + // TODO: actually parse image url ParameterKind::OpaqueString | ParameterKind::OpaqueStringRemainder => { Ok(ParameterValue::OpaqueString(input.into())) } @@ -108,6 +113,7 @@ impl ParameterKind { ParameterKind::Toggle => { Toggle::from_str(input).map(|t| ParameterValue::Toggle(t.into())) } + ParameterKind::Avatar => Ok(ParameterValue::Avatar(input.into())), } } } diff --git a/crates/commands/src/bin/write_cs_glue.rs b/crates/commands/src/bin/write_cs_glue.rs index 1d64a91a..6eb1f593 100644 --- a/crates/commands/src/bin/write_cs_glue.rs +++ b/crates/commands/src/bin/write_cs_glue.rs @@ -165,6 +165,7 @@ fn get_param_ty(kind: ParameterKind) -> &'static str { ParameterKind::MemberPrivacyTarget => "MemberPrivacySubject", ParameterKind::PrivacyLevel => "string", ParameterKind::Toggle => "bool", + ParameterKind::Avatar => "ParsedImage", } } @@ -176,6 +177,7 @@ fn get_param_param_ty(kind: ParameterKind) -> &'static str { ParameterKind::MemberPrivacyTarget => "MemberPrivacyTarget", ParameterKind::PrivacyLevel => "PrivacyLevel", ParameterKind::Toggle => "Toggle", + ParameterKind::Avatar => "Avatar", } } diff --git a/crates/commands/src/commands.udl b/crates/commands/src/commands.udl index d1b5ae1c..5ab50c63 100644 --- a/crates/commands/src/commands.udl +++ b/crates/commands/src/commands.udl @@ -14,6 +14,7 @@ interface Parameter { PrivacyLevel(string level); OpaqueString(string raw); Toggle(boolean toggle); + Avatar(string avatar); }; dictionary ParsedCommand { string command_ref; diff --git a/crates/commands/src/lib.rs b/crates/commands/src/lib.rs index 21c1db13..0af12856 100644 --- a/crates/commands/src/lib.rs +++ b/crates/commands/src/lib.rs @@ -28,6 +28,7 @@ pub enum Parameter { PrivacyLevel { level: String }, OpaqueString { raw: String }, Toggle { toggle: bool }, + Avatar { avatar: String }, } impl From for Parameter { @@ -39,6 +40,7 @@ impl From for Parameter { ParameterValue::PrivacyLevel(level) => Self::PrivacyLevel { level }, ParameterValue::OpaqueString(raw) => Self::OpaqueString { raw }, ParameterValue::Toggle(toggle) => Self::Toggle { toggle }, + ParameterValue::Avatar(avatar) => Self::Avatar { avatar }, } } } From 6a840f768f6d2743d78d28e45cc6c69f056b82a8 Mon Sep 17 00:00:00 2001 From: dusk Date: Fri, 4 Apr 2025 04:06:51 +0900 Subject: [PATCH 078/179] feat: implement system serveravatar commands --- PluralKit.Bot/CommandMeta/CommandTree.cs | 16 ++- PluralKit.Bot/Commands/SystemEdit.cs | 155 +++++++++++------------ crates/command_definitions/src/system.rs | 19 +++ 3 files changed, 104 insertions(+), 86 deletions(-) diff --git a/PluralKit.Bot/CommandMeta/CommandTree.cs b/PluralKit.Bot/CommandMeta/CommandTree.cs index ab46b58e..4a4bb498 100644 --- a/PluralKit.Bot/CommandMeta/CommandTree.cs +++ b/PluralKit.Bot/CommandMeta/CommandTree.cs @@ -69,6 +69,19 @@ public partial class CommandTree Commands.SystemShowAvatar(var param, var flags) => ctx.Execute(SystemAvatar, m => m.ShowAvatar(ctx, param.target, flags.GetReplyFormat())), Commands.SystemClearAvatar(var param, var flags) => ctx.Execute(SystemAvatar, m => m.ClearAvatar(ctx, ctx.System, flags.yes)), Commands.SystemChangeAvatar(var param, _) => ctx.Execute(SystemAvatar, m => m.ChangeAvatar(ctx, ctx.System, param.avatar)), + Commands.SystemShowServerAvatarSelf(_, var flags) => ((Func)(() => + { + // we want to change avatar if an attached image is passed + // we can't have a separate parsed command for this since the parser can't be aware of any attachments + var attachedImage = ctx.ExtractImageFromAttachment(); + if (attachedImage is { } image) + return ctx.Execute(SystemServerAvatar, m => m.ChangeServerAvatar(ctx, ctx.System, image)); + // if no attachment show the avatar like intended + return ctx.Execute(SystemServerAvatar, m => m.ShowServerAvatar(ctx, ctx.System, flags.GetReplyFormat())); + }))(), + Commands.SystemShowServerAvatar(var param, var flags) => ctx.Execute(SystemServerAvatar, m => m.ShowServerAvatar(ctx, param.target, flags.GetReplyFormat())), + Commands.SystemClearServerAvatar(var param, var flags) => ctx.Execute(SystemServerAvatar, m => m.ClearServerAvatar(ctx, ctx.System, flags.yes)), + Commands.SystemChangeServerAvatar(var param, _) => ctx.Execute(SystemServerAvatar, m => m.ChangeServerAvatar(ctx, ctx.System, param.avatar)), _ => // this should only ever occur when deving if commands are not implemented... ctx.Reply( @@ -319,9 +332,6 @@ public partial class CommandTree { if (ctx.Match("banner", "splash", "cover")) await ctx.CheckSystem(target).Execute(SystemBannerImage, m => m.BannerImage(ctx, target)); - else if (ctx.Match("serveravatar", "sa", "servericon", "serverimage", "serverpfp", "serverpic", "savatar", "spic", - "guildavatar", "guildpic", "guildicon", "sicon", "spfp")) - await ctx.CheckSystem(target).Execute(SystemServerAvatar, m => m.ServerAvatar(ctx, target)); else if (ctx.Match("list", "l", "members", "ls")) await ctx.CheckSystem(target).Execute(SystemList, m => m.MemberList(ctx, target)); else if (ctx.Match("find", "search", "query", "fd", "s")) diff --git a/PluralKit.Bot/Commands/SystemEdit.cs b/PluralKit.Bot/Commands/SystemEdit.cs index 582d0df3..ab4d3c1c 100644 --- a/PluralKit.Bot/Commands/SystemEdit.cs +++ b/PluralKit.Bot/Commands/SystemEdit.cs @@ -620,96 +620,85 @@ public class SystemEdit : ctx.Reply(msg)); } - public async Task ServerAvatar(Context ctx, PKSystem target) + public async Task ClearServerAvatar(Context ctx, PKSystem target, bool flagConfirmYes) { - - async Task ClearIcon() - { - ctx.CheckOwnSystem(target); - - await ctx.Repository.UpdateSystemGuild(target.Id, ctx.Guild.Id, new SystemGuildPatch { AvatarUrl = null }); - await ctx.Reply($"{Emojis.Success} System server avatar cleared."); - } - - async Task SetIcon(ParsedImage img) - { - ctx.CheckOwnSystem(target); - - img = await _avatarHosting.TryRehostImage(img, AvatarHostingService.RehostedImageType.Avatar, ctx.Author.Id, ctx.System); - await _avatarHosting.VerifyAvatarOrThrow(img.Url); - - await ctx.Repository.UpdateSystemGuild(target.Id, ctx.Guild.Id, new SystemGuildPatch { AvatarUrl = img.CleanUrl ?? img.Url }); - - var msg = img.Source switch - { - AvatarSource.User => - $"{Emojis.Success} System icon for this server changed to {img.SourceUser?.Username}'s avatar! It will now be used for anything that uses system avatar in this server.\n{Emojis.Warn} If {img.SourceUser?.Username} changes their avatar, the system icon for this server will need to be re-set.", - AvatarSource.Url => - $"{Emojis.Success} System icon for this server changed to the image at the given URL. It will now be used for anything that uses system avatar in this server.", - AvatarSource.HostedCdn => $"{Emojis.Success} System icon for this server changed to attached image.", - AvatarSource.Attachment => - $"{Emojis.Success} System icon for this server changed to attached image. It will now be used for anything that uses system avatar in this server.\n{Emojis.Warn} If you delete the message containing the attachment, the system icon for this server will stop working.", - _ => throw new ArgumentOutOfRangeException() - }; - - // The attachment's already right there, no need to preview it. - var hasEmbed = img.Source != AvatarSource.Attachment && img.Source != AvatarSource.HostedCdn; - await (hasEmbed - ? ctx.Reply(msg, new EmbedBuilder().Image(new Embed.EmbedImage(img.Url)).Build()) - : ctx.Reply(msg)); - } - - async Task ShowIcon() - { - var settings = await ctx.Repository.GetSystemGuild(ctx.Guild.Id, target.Id); - - if ((settings.AvatarUrl?.Trim() ?? "").Length > 0) - { - if (!target.AvatarPrivacy.CanAccess(ctx.DirectLookupContextFor(target.Id))) - throw new PKSyntaxError("This system does not have a icon specific to this server or it is private."); - switch (ctx.MatchFormat()) - { - case ReplyFormat.Raw: - await ctx.Reply($"`{settings.AvatarUrl.TryGetCleanCdnUrl()}`"); - break; - case ReplyFormat.Plaintext: - var ebP = new EmbedBuilder() - .Description($"Showing icon for system {target.NameFor(ctx)} (`{target.DisplayHid(ctx.Config)}`)"); - await ctx.Reply(text: $"<{settings.AvatarUrl.TryGetCleanCdnUrl()}>", embed: ebP.Build()); - break; - default: - var ebS = new EmbedBuilder() - .Title("System server icon") - .Image(new Embed.EmbedImage(settings.AvatarUrl.TryGetCleanCdnUrl())); - if (target.Id == ctx.System?.Id) - ebS.Description($"To clear, use `{ctx.DefaultPrefix}system servericon clear`."); - await ctx.Reply(embed: ebS.Build()); - break; - } - } - else - { - var isOwner = target.Id == ctx.System?.Id; - throw new PKSyntaxError( - $"This system does not have a icon specific to this server{(isOwner ? "" : " or it is private")}." - + (isOwner ? " Set one by attaching an image to this command, or by passing an image URL or @mention." : "")); - } - } - ctx.CheckGuildContext(); + ctx.CheckSystem().CheckOwnSystem(target); - if (target != null && target?.Id != ctx.System?.Id) + if (await ctx.ConfirmClear("your system's icon for this server", flagConfirmYes)) { - await ShowIcon(); - return; + await ctx.Repository.UpdateSystemGuild(target.Id, ctx.Guild.Id, new SystemGuildPatch { AvatarUrl = null }); + await ctx.Reply($"{Emojis.Success} System server icon cleared."); } + } - if (ctx.MatchClear() && await ctx.ConfirmClear("your system's icon for this server")) - await ClearIcon(); - else if (await ctx.MatchImage() is { } img) - await SetIcon(img); + public async Task ShowServerAvatar(Context ctx, PKSystem target, ReplyFormat format) + { + ctx.CheckGuildContext(); + var isOwnSystem = target.Id == ctx.System?.Id; + + var settings = await ctx.Repository.GetSystemGuild(ctx.Guild.Id, target.Id); + + if ((settings.AvatarUrl?.Trim() ?? "").Length > 0) + { + if (!target.AvatarPrivacy.CanAccess(ctx.DirectLookupContextFor(target.Id))) + throw new PKSyntaxError("This system does not have a icon specific to this server or it is private."); + + switch (format) + { + case ReplyFormat.Raw: + await ctx.Reply($"`{settings.AvatarUrl.TryGetCleanCdnUrl()}`"); + break; + case ReplyFormat.Plaintext: + var ebP = new EmbedBuilder() + .Description($"Showing icon for system {target.NameFor(ctx)} (`{target.DisplayHid(ctx.Config)}`)"); + await ctx.Reply(text: $"<{settings.AvatarUrl.TryGetCleanCdnUrl()}>", embed: ebP.Build()); + break; + default: + var ebS = new EmbedBuilder() + .Title("System server icon") + .Image(new Embed.EmbedImage(settings.AvatarUrl.TryGetCleanCdnUrl())); + if (target.Id == ctx.System?.Id) + ebS.Description($"To clear, use `{ctx.DefaultPrefix}system servericon clear`."); + await ctx.Reply(embed: ebS.Build()); + break; + } + } else - await ShowIcon(); + { + throw new PKSyntaxError( + $"This system does not have a icon specific to this server{(isOwnSystem ? "" : " or it is private")}." + + (isOwnSystem ? " Set one by attaching an image to this command, or by passing an image URL or @mention." : "")); + } + } + + public async Task ChangeServerAvatar(Context ctx, PKSystem target, ParsedImage img) + { + ctx.CheckGuildContext(); + ctx.CheckSystem().CheckOwnSystem(target); + + img = await _avatarHosting.TryRehostImage(img, AvatarHostingService.RehostedImageType.Avatar, ctx.Author.Id, ctx.System); + await _avatarHosting.VerifyAvatarOrThrow(img.Url); + + await ctx.Repository.UpdateSystemGuild(target.Id, ctx.Guild.Id, new SystemGuildPatch { AvatarUrl = img.CleanUrl ?? img.Url }); + + var msg = img.Source switch + { + AvatarSource.User => + $"{Emojis.Success} System icon for this server changed to {img.SourceUser?.Username}'s avatar! It will now be used for anything that uses system avatar in this server.\n{Emojis.Warn} If {img.SourceUser?.Username} changes their avatar, the system icon for this server will need to be re-set.", + AvatarSource.Url => + $"{Emojis.Success} System icon for this server changed to the image at the given URL. It will now be used for anything that uses system avatar in this server.", + AvatarSource.HostedCdn => $"{Emojis.Success} System icon for this server changed to attached image.", + AvatarSource.Attachment => + $"{Emojis.Success} System icon for this server changed to attached image. It will now be used for anything that uses system avatar in this server.\n{Emojis.Warn} If you delete the message containing the attachment, the system icon for this server will stop working.", + _ => throw new ArgumentOutOfRangeException() + }; + + // The attachment's already right there, no need to preview it. + var hasEmbed = img.Source != AvatarSource.Attachment && img.Source != AvatarSource.HostedCdn; + await (hasEmbed + ? ctx.Reply(msg, new EmbedBuilder().Image(new Embed.EmbedImage(img.Url)).Build()) + : ctx.Reply(msg)); } public async Task BannerImage(Context ctx, PKSystem target) diff --git a/crates/command_definitions/src/system.rs b/crates/command_definitions/src/system.rs index 0980ca87..b5e58789 100644 --- a/crates/command_definitions/src/system.rs +++ b/crates/command_definitions/src/system.rs @@ -158,6 +158,23 @@ pub fn edit() -> impl Iterator { ] .into_iter(); + let system_server_avatar = tokens!(system_target, ("serveravatar", ["spfp"])); + let system_server_avatar_cmd = [command!(system_server_avatar => "system_show_server_avatar") + .help("Shows the system's server avatar")] + .into_iter(); + + let system_server_avatar_self = tokens!(system, ("serveravatar", ["spfp"])); + let system_server_avatar_self_cmd = [ + command!(system_server_avatar_self => "system_show_server_avatar_self") + .help("Shows your system's server avatar"), + command!(system_server_avatar_self, ("clear", ["c"]) => "system_clear_server_avatar") + .flag(("yes", ["y"])) + .help("Clears your system's server avatar"), + command!(system_server_avatar_self, ("avatar", Avatar) => "system_change_server_avatar") + .help("Changes your system's server avatar"), + ] + .into_iter(); + system_new_cmd .chain(system_name_self_cmd) .chain(system_server_name_self_cmd) @@ -167,6 +184,7 @@ pub fn edit() -> impl Iterator { .chain(system_server_tag_self_cmd) .chain(system_pronouns_self_cmd) .chain(system_avatar_self_cmd) + .chain(system_server_avatar_self_cmd) .chain(system_name_cmd) .chain(system_server_name_cmd) .chain(system_description_cmd) @@ -175,5 +193,6 @@ pub fn edit() -> impl Iterator { .chain(system_server_tag_cmd) .chain(system_pronouns_cmd) .chain(system_avatar_cmd) + .chain(system_server_avatar_cmd) .chain(system_info_cmd) } From 9e74835e4b3bb8e51d7b1df25244724b087ddb98 Mon Sep 17 00:00:00 2001 From: dusk Date: Fri, 4 Apr 2025 04:27:28 +0900 Subject: [PATCH 079/179] feat: implement system banner commands --- PluralKit.Bot/CommandMeta/CommandTree.cs | 17 +++- PluralKit.Bot/Commands/SystemEdit.cs | 113 ++++++++++++----------- crates/command_definitions/src/system.rs | 19 ++++ 3 files changed, 92 insertions(+), 57 deletions(-) diff --git a/PluralKit.Bot/CommandMeta/CommandTree.cs b/PluralKit.Bot/CommandMeta/CommandTree.cs index 4a4bb498..9d6a860f 100644 --- a/PluralKit.Bot/CommandMeta/CommandTree.cs +++ b/PluralKit.Bot/CommandMeta/CommandTree.cs @@ -82,6 +82,19 @@ public partial class CommandTree Commands.SystemShowServerAvatar(var param, var flags) => ctx.Execute(SystemServerAvatar, m => m.ShowServerAvatar(ctx, param.target, flags.GetReplyFormat())), Commands.SystemClearServerAvatar(var param, var flags) => ctx.Execute(SystemServerAvatar, m => m.ClearServerAvatar(ctx, ctx.System, flags.yes)), Commands.SystemChangeServerAvatar(var param, _) => ctx.Execute(SystemServerAvatar, m => m.ChangeServerAvatar(ctx, ctx.System, param.avatar)), + Commands.SystemShowBannerSelf(_, var flags) => ((Func)(() => + { + // we want to change banner if an attached image is passed + // we can't have a separate parsed command for this since the parser can't be aware of any attachments + var attachedImage = ctx.ExtractImageFromAttachment(); + if (attachedImage is { } image) + return ctx.Execute(SystemBannerImage, m => m.ChangeBannerImage(ctx, ctx.System, image)); + // if no attachment show the banner like intended + return ctx.Execute(SystemBannerImage, m => m.ShowBannerImage(ctx, ctx.System, flags.GetReplyFormat())); + }))(), + Commands.SystemShowBanner(var param, var flags) => ctx.Execute(SystemBannerImage, m => m.ShowBannerImage(ctx, param.target, flags.GetReplyFormat())), + Commands.SystemClearBanner(var param, var flags) => ctx.Execute(SystemBannerImage, m => m.ClearBannerImage(ctx, ctx.System, flags.yes)), + Commands.SystemChangeBanner(var param, _) => ctx.Execute(SystemBannerImage, m => m.ChangeBannerImage(ctx, ctx.System, param.banner)), _ => // this should only ever occur when deving if commands are not implemented... ctx.Reply( @@ -330,9 +343,7 @@ public partial class CommandTree private async Task HandleSystemCommandTargeted(Context ctx, PKSystem target) { - if (ctx.Match("banner", "splash", "cover")) - await ctx.CheckSystem(target).Execute(SystemBannerImage, m => m.BannerImage(ctx, target)); - else if (ctx.Match("list", "l", "members", "ls")) + if (ctx.Match("list", "l", "members", "ls")) await ctx.CheckSystem(target).Execute(SystemList, m => m.MemberList(ctx, target)); else if (ctx.Match("find", "search", "query", "fd", "s")) await ctx.CheckSystem(target).Execute(SystemFind, m => m.MemberList(ctx, target)); diff --git a/PluralKit.Bot/Commands/SystemEdit.cs b/PluralKit.Bot/Commands/SystemEdit.cs index ab4d3c1c..66a51a39 100644 --- a/PluralKit.Bot/Commands/SystemEdit.cs +++ b/PluralKit.Bot/Commands/SystemEdit.cs @@ -701,72 +701,77 @@ public class SystemEdit : ctx.Reply(msg)); } - public async Task BannerImage(Context ctx, PKSystem target) + public async Task ClearBannerImage(Context ctx, PKSystem target, bool flagConfirmYes) { ctx.CheckSystemPrivacy(target.Id, target.BannerPrivacy); - - var isOwnSystem = target.Id == ctx.System?.Id; - - if ((!ctx.HasNext() && ctx.Message.Attachments.Length == 0) || ctx.PeekMatchFormat() != ReplyFormat.Standard) - { - if ((target.BannerImage?.Trim() ?? "").Length > 0) - switch (ctx.MatchFormat()) - { - case ReplyFormat.Raw: - await ctx.Reply($"`{target.BannerImage.TryGetCleanCdnUrl()}`"); - break; - case ReplyFormat.Plaintext: - var ebP = new EmbedBuilder() - .Description($"Showing banner for system {target.NameFor(ctx)} (`{target.DisplayHid(ctx.Config)}`)"); - await ctx.Reply(text: $"<{target.BannerImage.TryGetCleanCdnUrl()}>", embed: ebP.Build()); - break; - default: - var ebS = new EmbedBuilder() - .Title("System banner image") - .Image(new Embed.EmbedImage(target.BannerImage.TryGetCleanCdnUrl())); - if (target.Id == ctx.System?.Id) - ebS.Description($"To clear, use `{ctx.DefaultPrefix}system banner clear`."); - await ctx.Reply(embed: ebS.Build()); - break; - } - else - throw new PKSyntaxError("This system does not have a banner image set." - + (isOwnSystem ? "Set one by attaching an image to this command, or by passing an image URL or @mention." : "")); - - return; - } - ctx.CheckSystem().CheckOwnSystem(target); - if (ctx.MatchClear() && await ctx.ConfirmClear("your system's banner image")) + if (await ctx.ConfirmClear("your system's banner image", flagConfirmYes)) { await ctx.Repository.UpdateSystem(target.Id, new SystemPatch { BannerImage = null }); await ctx.Reply($"{Emojis.Success} System banner image cleared."); } + } - else if (await ctx.MatchImage() is { } img) + public async Task ShowBannerImage(Context ctx, PKSystem target, ReplyFormat format) + { + ctx.CheckSystemPrivacy(target.Id, target.BannerPrivacy); + var isOwnSystem = target.Id == ctx.System?.Id; + + if ((target.BannerImage?.Trim() ?? "").Length > 0) { - img = await _avatarHosting.TryRehostImage(img, AvatarHostingService.RehostedImageType.Banner, ctx.Author.Id, ctx.System); - await _avatarHosting.VerifyAvatarOrThrow(img.Url, true); - - await ctx.Repository.UpdateSystem(target.Id, new SystemPatch { BannerImage = img.CleanUrl ?? img.Url }); - - var msg = img.Source switch + switch (format) { - AvatarSource.Url => $"{Emojis.Success} System banner image changed to the image at the given URL.", - AvatarSource.HostedCdn => $"{Emojis.Success} System banner image changed to attached image.", - AvatarSource.Attachment => - $"{Emojis.Success} System banner image changed to attached image.\n{Emojis.Warn} If you delete the message containing the attachment, the banner image will stop working.", - AvatarSource.User => throw new PKError("Cannot set a banner image to an user's avatar."), - _ => throw new ArgumentOutOfRangeException() - }; - - // The attachment's already right there, no need to preview it. - var hasEmbed = img.Source != AvatarSource.Attachment && img.Source != AvatarSource.HostedCdn; - await (hasEmbed - ? ctx.Reply(msg, new EmbedBuilder().Image(new Embed.EmbedImage(img.Url)).Build()) - : ctx.Reply(msg)); + case ReplyFormat.Raw: + await ctx.Reply($"`{target.BannerImage.TryGetCleanCdnUrl()}`"); + break; + case ReplyFormat.Plaintext: + var ebP = new EmbedBuilder() + .Description($"Showing banner for system {target.NameFor(ctx)} (`{target.DisplayHid(ctx.Config)}`)"); + await ctx.Reply(text: $"<{target.BannerImage.TryGetCleanCdnUrl()}>", embed: ebP.Build()); + break; + default: + var ebS = new EmbedBuilder() + .Title("System banner image") + .Image(new Embed.EmbedImage(target.BannerImage.TryGetCleanCdnUrl())); + if (target.Id == ctx.System?.Id) + ebS.Description($"To clear, use `{ctx.DefaultPrefix}system banner clear`."); + await ctx.Reply(embed: ebS.Build()); + break; + } } + else + { + throw new PKSyntaxError("This system does not have a banner image set." + + (isOwnSystem ? " Set one by attaching an image to this command, or by passing an image URL or @mention." : "")); + } + } + + public async Task ChangeBannerImage(Context ctx, PKSystem target, ParsedImage img) + { + ctx.CheckSystemPrivacy(target.Id, target.BannerPrivacy); + ctx.CheckSystem().CheckOwnSystem(target); + + img = await _avatarHosting.TryRehostImage(img, AvatarHostingService.RehostedImageType.Banner, ctx.Author.Id, ctx.System); + await _avatarHosting.VerifyAvatarOrThrow(img.Url, true); + + await ctx.Repository.UpdateSystem(target.Id, new SystemPatch { BannerImage = img.CleanUrl ?? img.Url }); + + var msg = img.Source switch + { + AvatarSource.Url => $"{Emojis.Success} System banner image changed to the image at the given URL.", + AvatarSource.HostedCdn => $"{Emojis.Success} System banner image changed to attached image.", + AvatarSource.Attachment => + $"{Emojis.Success} System banner image changed to attached image.\n{Emojis.Warn} If you delete the message containing the attachment, the banner image will stop working.", + AvatarSource.User => throw new PKError("Cannot set a banner image to an user's avatar."), + _ => throw new ArgumentOutOfRangeException() + }; + + // The attachment's already right there, no need to preview it. + var hasEmbed = img.Source != AvatarSource.Attachment && img.Source != AvatarSource.HostedCdn; + await (hasEmbed + ? ctx.Reply(msg, new EmbedBuilder().Image(new Embed.EmbedImage(img.Url)).Build()) + : ctx.Reply(msg)); } public async Task Delete(Context ctx, PKSystem target) diff --git a/crates/command_definitions/src/system.rs b/crates/command_definitions/src/system.rs index b5e58789..e7befa9a 100644 --- a/crates/command_definitions/src/system.rs +++ b/crates/command_definitions/src/system.rs @@ -175,6 +175,23 @@ pub fn edit() -> impl Iterator { ] .into_iter(); + let system_banner = tokens!(system_target, ("banner", ["cover"])); + let system_banner_cmd = + [command!(system_banner => "system_show_banner").help("Shows the system's banner")] + .into_iter(); + + let system_banner_self = tokens!(system, ("banner", ["cover"])); + let system_banner_self_cmd = [ + command!(system_banner_self => "system_show_banner_self") + .help("Shows your system's banner"), + command!(system_banner_self, ("clear", ["c"]) => "system_clear_banner") + .flag(("yes", ["y"])) + .help("Clears your system's banner"), + command!(system_banner_self, ("banner", Avatar) => "system_change_banner") + .help("Changes your system's banner"), + ] + .into_iter(); + system_new_cmd .chain(system_name_self_cmd) .chain(system_server_name_self_cmd) @@ -185,6 +202,7 @@ pub fn edit() -> impl Iterator { .chain(system_pronouns_self_cmd) .chain(system_avatar_self_cmd) .chain(system_server_avatar_self_cmd) + .chain(system_banner_self_cmd) .chain(system_name_cmd) .chain(system_server_name_cmd) .chain(system_description_cmd) @@ -194,5 +212,6 @@ pub fn edit() -> impl Iterator { .chain(system_pronouns_cmd) .chain(system_avatar_cmd) .chain(system_server_avatar_cmd) + .chain(system_banner_cmd) .chain(system_info_cmd) } From 047bdd870de13cd1ea36ea35db131d336d2abe53 Mon Sep 17 00:00:00 2001 From: dusk Date: Fri, 4 Apr 2025 04:47:00 +0900 Subject: [PATCH 080/179] feat: implement system delete command --- PluralKit.Bot/CommandMeta/CommandTree.cs | 3 +-- PluralKit.Bot/Commands/SystemEdit.cs | 3 +-- crates/command_definitions/src/system.rs | 13 +++++++++++-- crates/commands/src/bin/write_cs_glue.rs | 12 ++++++------ 4 files changed, 19 insertions(+), 12 deletions(-) diff --git a/PluralKit.Bot/CommandMeta/CommandTree.cs b/PluralKit.Bot/CommandMeta/CommandTree.cs index 9d6a860f..38e49001 100644 --- a/PluralKit.Bot/CommandMeta/CommandTree.cs +++ b/PluralKit.Bot/CommandMeta/CommandTree.cs @@ -95,6 +95,7 @@ public partial class CommandTree Commands.SystemShowBanner(var param, var flags) => ctx.Execute(SystemBannerImage, m => m.ShowBannerImage(ctx, param.target, flags.GetReplyFormat())), Commands.SystemClearBanner(var param, var flags) => ctx.Execute(SystemBannerImage, m => m.ClearBannerImage(ctx, ctx.System, flags.yes)), Commands.SystemChangeBanner(var param, _) => ctx.Execute(SystemBannerImage, m => m.ChangeBannerImage(ctx, ctx.System, param.banner)), + Commands.SystemDelete(_, var flags) => ctx.Execute(SystemDelete, m => m.Delete(ctx, ctx.System, flags.no_export)), _ => // this should only ever occur when deving if commands are not implemented... ctx.Reply( @@ -364,8 +365,6 @@ public partial class CommandTree await ctx.CheckSystem(target).Execute(GroupList, g => g.ListSystemGroups(ctx, target)); else if (ctx.Match("privacy")) await ctx.CheckSystem(target).Execute(SystemPrivacy, m => m.SystemPrivacy(ctx, target)); - else if (ctx.Match("delete", "remove", "destroy", "erase", "yeet")) - await ctx.CheckSystem(target).Execute(SystemDelete, m => m.Delete(ctx, target)); else if (ctx.Match("id")) await ctx.CheckSystem(target).Execute(SystemId, m => m.DisplayId(ctx, target)); else if (ctx.Match("random", "rand", "r")) diff --git a/PluralKit.Bot/Commands/SystemEdit.cs b/PluralKit.Bot/Commands/SystemEdit.cs index 66a51a39..8fe73c77 100644 --- a/PluralKit.Bot/Commands/SystemEdit.cs +++ b/PluralKit.Bot/Commands/SystemEdit.cs @@ -774,10 +774,9 @@ public class SystemEdit : ctx.Reply(msg)); } - public async Task Delete(Context ctx, PKSystem target) + public async Task Delete(Context ctx, PKSystem target, bool noExport) { ctx.CheckSystem().CheckOwnSystem(target); - var noExport = ctx.MatchFlag("ne", "no-export"); var warnMsg = $"{Emojis.Warn} Are you sure you want to delete your system? If so, reply to this message with your system's ID (`{target.DisplayHid(ctx.Config)}`).\n"; if (!noExport) diff --git a/crates/command_definitions/src/system.rs b/crates/command_definitions/src/system.rs index e7befa9a..83750af8 100644 --- a/crates/command_definitions/src/system.rs +++ b/crates/command_definitions/src/system.rs @@ -159,8 +159,10 @@ pub fn edit() -> impl Iterator { .into_iter(); let system_server_avatar = tokens!(system_target, ("serveravatar", ["spfp"])); - let system_server_avatar_cmd = [command!(system_server_avatar => "system_show_server_avatar") - .help("Shows the system's server avatar")] + let system_server_avatar_cmd = [ + command!(system_server_avatar => "system_show_server_avatar") + .help("Shows the system's server avatar"), + ] .into_iter(); let system_server_avatar_self = tokens!(system, ("serveravatar", ["spfp"])); @@ -192,6 +194,12 @@ pub fn edit() -> impl Iterator { ] .into_iter(); + let system_delete = std::iter::once( + command!(system, ("delete", ["erase", "remove", "yeet"]) => "system_delete") + .flag(("no-export", ["ne"])) + .help("Deletes the system"), + ); + system_new_cmd .chain(system_name_self_cmd) .chain(system_server_name_self_cmd) @@ -203,6 +211,7 @@ pub fn edit() -> impl Iterator { .chain(system_avatar_self_cmd) .chain(system_server_avatar_self_cmd) .chain(system_banner_self_cmd) + .chain(system_delete) .chain(system_name_cmd) .chain(system_server_name_cmd) .chain(system_description_cmd) diff --git a/crates/commands/src/bin/write_cs_glue.rs b/crates/commands/src/bin/write_cs_glue.rs index 6eb1f593..cd69b3b1 100644 --- a/crates/commands/src/bin/write_cs_glue.rs +++ b/crates/commands/src/bin/write_cs_glue.rs @@ -34,7 +34,7 @@ fn main() -> Result<(), Box> { writeln!( &mut command_params_init, r#"@{name} = await ctx.ParamResolve{extract_fn_name}("{name}") ?? throw new PKError("this is a bug"),"#, - name = param.name(), + name = param.name().replace("-", "_"), extract_fn_name = get_param_param_ty(param.kind()), )?; } @@ -44,14 +44,14 @@ fn main() -> Result<(), Box> { writeln!( &mut command_flags_init, r#"@{name} = await ctx.FlagResolve{extract_fn_name}("{name}"),"#, - name = flag.get_name(), + name = flag.get_name().replace("-", "_"), extract_fn_name = get_param_param_ty(kind), )?; } else { writeln!( &mut command_flags_init, r#"@{name} = ctx.Parameters.HasFlag("{name}"),"#, - name = flag.get_name(), + name = flag.get_name().replace("-", "_"), )?; } } @@ -92,7 +92,7 @@ fn main() -> Result<(), Box> { writeln!( &mut command_params_fields, r#"public required {ty} @{name};"#, - name = param.name(), + name = param.name().replace("-", "_"), ty = get_param_ty(param.kind()), )?; } @@ -102,14 +102,14 @@ fn main() -> Result<(), Box> { writeln!( &mut command_flags_fields, r#"public {ty}? @{name};"#, - name = flag.get_name(), + name = flag.get_name().replace("-", "_"), ty = get_param_ty(kind), )?; } else { writeln!( &mut command_flags_fields, r#"public required bool @{name};"#, - name = flag.get_name(), + name = flag.get_name().replace("-", "_"), )?; } } From cb0a9eaf9fc390dc7bbb951251bddeadd8572c7d Mon Sep 17 00:00:00 2001 From: dusk Date: Fri, 4 Apr 2025 05:24:09 +0900 Subject: [PATCH 081/179] feat: implement system proxy commands --- PluralKit.Bot/CommandMeta/CommandTree.cs | 8 ++-- .../Context/ContextEntityArgumentsExt.cs | 8 ++++ .../Context/ContextParametersExt.cs | 8 ++++ PluralKit.Bot/CommandSystem/Parameters.cs | 3 ++ PluralKit.Bot/Commands/SystemEdit.cs | 48 +++++++++++-------- crates/command_definitions/src/system.rs | 11 +++++ crates/command_parser/src/parameter.rs | 9 ++++ crates/command_parser/src/token.rs | 12 +++-- crates/commands/src/bin/write_cs_glue.rs | 12 ++++- crates/commands/src/commands.udl | 1 + crates/commands/src/lib.rs | 2 + 11 files changed, 93 insertions(+), 29 deletions(-) diff --git a/PluralKit.Bot/CommandMeta/CommandTree.cs b/PluralKit.Bot/CommandMeta/CommandTree.cs index 38e49001..17258e55 100644 --- a/PluralKit.Bot/CommandMeta/CommandTree.cs +++ b/PluralKit.Bot/CommandMeta/CommandTree.cs @@ -96,6 +96,10 @@ public partial class CommandTree Commands.SystemClearBanner(var param, var flags) => ctx.Execute(SystemBannerImage, m => m.ClearBannerImage(ctx, ctx.System, flags.yes)), Commands.SystemChangeBanner(var param, _) => ctx.Execute(SystemBannerImage, m => m.ChangeBannerImage(ctx, ctx.System, param.banner)), Commands.SystemDelete(_, var flags) => ctx.Execute(SystemDelete, m => m.Delete(ctx, ctx.System, flags.no_export)), + Commands.SystemShowProxyCurrent(_, _) => ctx.Execute(SystemProxy, m => m.ShowSystemProxy(ctx, ctx.Guild)), + Commands.SystemShowProxy(var param, _) => ctx.Execute(SystemProxy, m => m.ShowSystemProxy(ctx, param.target)), + Commands.SystemToggleProxyCurrent(var param, _) => ctx.Execute(SystemProxy, m => m.ToggleSystemProxy(ctx, ctx.Guild, param.toggle)), + Commands.SystemToggleProxy(var param, _) => ctx.Execute(SystemProxy, m => m.ToggleSystemProxy(ctx, param.target, param.toggle)), _ => // this should only ever occur when deving if commands are not implemented... ctx.Reply( @@ -167,8 +171,6 @@ public partial class CommandTree if (ctx.Match("proxy")) if (ctx.Match("debug")) return ctx.Execute(ProxyCheck, m => m.MessageProxyCheck(ctx)); - else - return ctx.Execute(SystemProxy, m => m.SystemProxy(ctx)); if (ctx.Match("invite")) return ctx.Execute(Invite, m => m.Invite(ctx)); if (ctx.Match("mn")) return ctx.Execute(null, m => m.Mn(ctx)); if (ctx.Match("fire")) return ctx.Execute(null, m => m.Fire(ctx)); @@ -295,8 +297,6 @@ public partial class CommandTree // todo: these aren't deprecated but also shouldn't be here else if (ctx.Match("webhook", "hook")) await ctx.Execute(null, m => m.SystemWebhook(ctx)); - else if (ctx.Match("proxy")) - await ctx.Execute(SystemProxy, m => m.SystemProxy(ctx)); // finally, parse commands that *can* take a system target else diff --git a/PluralKit.Bot/CommandSystem/Context/ContextEntityArgumentsExt.cs b/PluralKit.Bot/CommandSystem/Context/ContextEntityArgumentsExt.cs index 6eba5039..b1cddf17 100644 --- a/PluralKit.Bot/CommandSystem/Context/ContextEntityArgumentsExt.cs +++ b/PluralKit.Bot/CommandSystem/Context/ContextEntityArgumentsExt.cs @@ -213,6 +213,14 @@ public static class ContextEntityArgumentsExt return channel; } + public static async Task ParseGuild(this Context ctx, string input) + { + if (!ulong.TryParse(input, out var id)) + return null; + + return await ctx.Rest.GetGuildOrNull(id); + } + public static async Task MatchGuild(this Context ctx) { if (!ulong.TryParse(ctx.PeekArgument(), out var id)) diff --git a/PluralKit.Bot/CommandSystem/Context/ContextParametersExt.cs b/PluralKit.Bot/CommandSystem/Context/ContextParametersExt.cs index cf2fa6c7..fd67ad76 100644 --- a/PluralKit.Bot/CommandSystem/Context/ContextParametersExt.cs +++ b/PluralKit.Bot/CommandSystem/Context/ContextParametersExt.cs @@ -59,4 +59,12 @@ public static class ContextParametersExt param => (param as Parameter.Avatar)?.avatar ); } + + public static async Task ParamResolveGuild(this Context ctx, string param_name) + { + return await ctx.Parameters.ResolveParameter( + ctx, param_name, + param => (param as Parameter.GuildRef)?.guild + ); + } } \ No newline at end of file diff --git a/PluralKit.Bot/CommandSystem/Parameters.cs b/PluralKit.Bot/CommandSystem/Parameters.cs index 48aaa583..6846938e 100644 --- a/PluralKit.Bot/CommandSystem/Parameters.cs +++ b/PluralKit.Bot/CommandSystem/Parameters.cs @@ -10,6 +10,7 @@ public abstract record Parameter() { public record MemberRef(PKMember member): Parameter; public record SystemRef(PKSystem system): Parameter; + public record GuildRef(Guild guild): Parameter; public record MemberPrivacyTarget(MemberPrivacySubject target): Parameter; public record PrivacyLevel(string level): Parameter; public record Toggle(bool value): Parameter; @@ -83,6 +84,8 @@ public class Parameters return new Parameter.Opaque(opaque.raw); case uniffi.commands.Parameter.Avatar avatar: return new Parameter.Avatar(await ctx.GetUserPfp(avatar.avatar) ?? ctx.ParseImage(avatar.avatar)); + case uniffi.commands.Parameter.GuildRef guildRef: + return new Parameter.GuildRef(await ctx.ParseGuild(guildRef.guild) ?? throw new PKError($"Guild {guildRef.guild} not found")); } return null; } diff --git a/PluralKit.Bot/Commands/SystemEdit.cs b/PluralKit.Bot/Commands/SystemEdit.cs index 8fe73c77..f162ee9b 100644 --- a/PluralKit.Bot/Commands/SystemEdit.cs +++ b/PluralKit.Bot/Commands/SystemEdit.cs @@ -830,11 +830,32 @@ public class SystemEdit await ctx.Repository.DeleteSystem(target.Id); } - public async Task SystemProxy(Context ctx) + public async Task ToggleSystemProxy(Context ctx, Guild guildArg, bool newValue) { ctx.CheckSystem(); - var guild = await ctx.MatchGuild() ?? ctx.Guild ?? + var guild = guildArg ?? + throw new PKError("You must run this command in a server or pass a server ID."); + + string serverText; + if (guild.Id == ctx.Guild?.Id) + serverText = $"this server ({guild.Name.EscapeMarkdown()})"; + else + serverText = $"the server {guild.Name.EscapeMarkdown()}"; + + await ctx.Repository.UpdateSystemGuild(ctx.System.Id, guild.Id, new SystemGuildPatch { ProxyEnabled = newValue }); + + if (newValue) + await ctx.Reply($"Message proxying in {serverText} is now **enabled** for your system."); + else + await ctx.Reply($"Message proxying in {serverText} is now **disabled** for your system."); + } + + public async Task ShowSystemProxy(Context ctx, Guild guildArg) + { + ctx.CheckSystem(); + + var guild = guildArg ?? throw new PKError("You must run this command in a server or pass a server ID."); var gs = await ctx.Repository.GetSystemGuild(guild.Id, ctx.System.Id); @@ -845,25 +866,12 @@ public class SystemEdit else serverText = $"the server {guild.Name.EscapeMarkdown()}"; - if (!ctx.HasNext()) - { - if (gs.ProxyEnabled) - await ctx.Reply( - $"Proxying in {serverText} is currently **enabled** for your system. To disable it, type `{ctx.DefaultPrefix}system proxy off`."); - else - await ctx.Reply( - $"Proxying in {serverText} is currently **disabled** for your system. To enable it, type `{ctx.DefaultPrefix}system proxy on`."); - return; - } - - var newValue = ctx.MatchToggle(); - - await ctx.Repository.UpdateSystemGuild(ctx.System.Id, guild.Id, new SystemGuildPatch { ProxyEnabled = newValue }); - - if (newValue) - await ctx.Reply($"Message proxying in {serverText} is now **enabled** for your system."); + if (gs.ProxyEnabled) + await ctx.Reply( + $"Proxying in {serverText} is currently **enabled** for your system. To disable it, type `{ctx.DefaultPrefix}system proxy off`."); else - await ctx.Reply($"Message proxying in {serverText} is now **disabled** for your system."); + await ctx.Reply( + $"Proxying in {serverText} is currently **disabled** for your system. To enable it, type `{ctx.DefaultPrefix}system proxy on`."); } public async Task SystemPrivacy(Context ctx, PKSystem target) diff --git a/crates/command_definitions/src/system.rs b/crates/command_definitions/src/system.rs index 83750af8..59b26ecc 100644 --- a/crates/command_definitions/src/system.rs +++ b/crates/command_definitions/src/system.rs @@ -200,6 +200,16 @@ pub fn edit() -> impl Iterator { .help("Deletes the system"), ); + let system_proxy = tokens!(system, "proxy"); + let system_proxy_cmd = [ + command!(system_proxy => "system_show_proxy_current").help("Shows your system's proxy setting for the guild you are in"), + command!(system_proxy, Toggle => "system_toggle_proxy_current") + .help("Toggle your system's proxy for the guild you are in"), + command!(system_proxy, GuildRef => "system_show_proxy").help("Shows your system's proxy setting for a guild"), + command!(system_proxy, GuildRef, Toggle => "system_toggle_proxy") + .help("Toggle your system's proxy for a guild"), + ].into_iter(); + system_new_cmd .chain(system_name_self_cmd) .chain(system_server_name_self_cmd) @@ -212,6 +222,7 @@ pub fn edit() -> impl Iterator { .chain(system_server_avatar_self_cmd) .chain(system_banner_self_cmd) .chain(system_delete) + .chain(system_proxy_cmd) .chain(system_name_cmd) .chain(system_server_name_cmd) .chain(system_description_cmd) diff --git a/crates/command_parser/src/parameter.rs b/crates/command_parser/src/parameter.rs index 14e79437..dde6b535 100644 --- a/crates/command_parser/src/parameter.rs +++ b/crates/command_parser/src/parameter.rs @@ -12,6 +12,7 @@ pub enum ParameterValue { OpaqueString(String), MemberRef(String), SystemRef(String), + GuildRef(String), MemberPrivacyTarget(String), PrivacyLevel(String), Toggle(bool), @@ -42,6 +43,7 @@ impl Display for Parameter { } ParameterKind::MemberRef => write!(f, ""), ParameterKind::SystemRef => write!(f, ""), + ParameterKind::GuildRef => write!(f, ""), ParameterKind::MemberPrivacyTarget => write!(f, ""), ParameterKind::PrivacyLevel => write!(f, "[privacy level]"), ParameterKind::Toggle => write!(f, "on/off"), @@ -74,6 +76,7 @@ pub enum ParameterKind { OpaqueStringRemainder, MemberRef, SystemRef, + GuildRef, MemberPrivacyTarget, PrivacyLevel, Toggle, @@ -87,6 +90,7 @@ impl ParameterKind { ParameterKind::OpaqueStringRemainder => "string", ParameterKind::MemberRef => "target", ParameterKind::SystemRef => "target", + ParameterKind::GuildRef => "target", ParameterKind::MemberPrivacyTarget => "member_privacy_target", ParameterKind::PrivacyLevel => "privacy_level", ParameterKind::Toggle => "toggle", @@ -114,8 +118,13 @@ impl ParameterKind { Toggle::from_str(input).map(|t| ParameterValue::Toggle(t.into())) } ParameterKind::Avatar => Ok(ParameterValue::Avatar(input.into())), + ParameterKind::GuildRef => Ok(ParameterValue::GuildRef(input.into())), } } + + pub(crate) fn skip_if_cant_match(&self) -> bool { + matches!(self, ParameterKind::Toggle) + } } pub enum MemberPrivacyTargetKind { diff --git a/crates/command_parser/src/token.rs b/crates/command_parser/src/token.rs index 996ee017..863cb936 100644 --- a/crates/command_parser/src/token.rs +++ b/crates/command_parser/src/token.rs @@ -67,9 +67,15 @@ impl Token { name: param.name().into(), value: matched, }, - Err(err) => TokenMatchResult::ParameterMatchError { - input: input.into(), - msg: err, + Err(err) => { + if param.kind().skip_if_cant_match() { + return None; + } else { + TokenMatchResult::ParameterMatchError { + input: input.into(), + msg: err, + } + } }, }), // don't add a _ match here! diff --git a/crates/commands/src/bin/write_cs_glue.rs b/crates/commands/src/bin/write_cs_glue.rs index cd69b3b1..dda4e1b3 100644 --- a/crates/commands/src/bin/write_cs_glue.rs +++ b/crates/commands/src/bin/write_cs_glue.rs @@ -1,7 +1,8 @@ use std::{env, fmt::Write, fs, path::PathBuf, str::FromStr}; use command_parser::{ - command, parameter::{Parameter, ParameterKind}, token::Token + parameter::{Parameter, ParameterKind}, + token::Token, }; fn main() -> Result<(), Box> { @@ -16,6 +17,7 @@ fn main() -> Result<(), Box> { writeln!(&mut glue, "#nullable enable\n")?; writeln!(&mut glue, "using PluralKit.Core;\n")?; + writeln!(&mut glue, "using Myriad.Types;")?; writeln!(&mut glue, "namespace PluralKit.Bot;\n")?; let mut record_fields = String::new(); @@ -114,7 +116,11 @@ fn main() -> Result<(), Box> { } } let mut command_reply_format = String::new(); - if command.flags.iter().any(|flag| flag.get_name() == "plaintext") { + if command + .flags + .iter() + .any(|flag| flag.get_name() == "plaintext") + { writeln!( &mut command_reply_format, r#"if (plaintext) return ReplyFormat.Plaintext;"#, @@ -166,6 +172,7 @@ fn get_param_ty(kind: ParameterKind) -> &'static str { ParameterKind::PrivacyLevel => "string", ParameterKind::Toggle => "bool", ParameterKind::Avatar => "ParsedImage", + ParameterKind::GuildRef => "Guild", } } @@ -178,6 +185,7 @@ fn get_param_param_ty(kind: ParameterKind) -> &'static str { ParameterKind::PrivacyLevel => "PrivacyLevel", ParameterKind::Toggle => "Toggle", ParameterKind::Avatar => "Avatar", + ParameterKind::GuildRef => "Guild", } } diff --git a/crates/commands/src/commands.udl b/crates/commands/src/commands.udl index 5ab50c63..1b9f9ff1 100644 --- a/crates/commands/src/commands.udl +++ b/crates/commands/src/commands.udl @@ -10,6 +10,7 @@ interface CommandResult { interface Parameter { MemberRef(string member); SystemRef(string system); + GuildRef(string guild); MemberPrivacyTarget(string target); PrivacyLevel(string level); OpaqueString(string raw); diff --git a/crates/commands/src/lib.rs b/crates/commands/src/lib.rs index 0af12856..0cbd975a 100644 --- a/crates/commands/src/lib.rs +++ b/crates/commands/src/lib.rs @@ -24,6 +24,7 @@ pub enum CommandResult { pub enum Parameter { MemberRef { member: String }, SystemRef { system: String }, + GuildRef { guild: String }, MemberPrivacyTarget { target: String }, PrivacyLevel { level: String }, OpaqueString { raw: String }, @@ -41,6 +42,7 @@ impl From for Parameter { ParameterValue::OpaqueString(raw) => Self::OpaqueString { raw }, ParameterValue::Toggle(toggle) => Self::Toggle { toggle }, ParameterValue::Avatar(avatar) => Self::Avatar { avatar }, + ParameterValue::GuildRef(guild) => Self::GuildRef { guild }, } } } From 3eece261fdba5cc0a55aa2004eeaa3ba57099490 Mon Sep 17 00:00:00 2001 From: dusk Date: Fri, 4 Apr 2025 06:14:17 +0900 Subject: [PATCH 082/179] feat: implement system privacy commands (yay system edit done) --- PluralKit.Bot/CommandMeta/CommandTree.cs | 5 +- .../CommandSystem/Context/ContextFlagsExt.cs | 2 +- .../Context/ContextParametersExt.cs | 10 +- PluralKit.Bot/CommandSystem/Parameters.cs | 16 ++- PluralKit.Bot/Commands/SystemEdit.cs | 117 ++++++++---------- crates/command_definitions/src/system.rs | 18 ++- crates/command_parser/src/parameter.rs | 54 ++++++++ crates/commands/src/bin/write_cs_glue.rs | 4 +- crates/commands/src/commands.udl | 1 + crates/commands/src/lib.rs | 2 + 10 files changed, 154 insertions(+), 75 deletions(-) diff --git a/PluralKit.Bot/CommandMeta/CommandTree.cs b/PluralKit.Bot/CommandMeta/CommandTree.cs index 17258e55..63539cca 100644 --- a/PluralKit.Bot/CommandMeta/CommandTree.cs +++ b/PluralKit.Bot/CommandMeta/CommandTree.cs @@ -100,6 +100,9 @@ public partial class CommandTree Commands.SystemShowProxy(var param, _) => ctx.Execute(SystemProxy, m => m.ShowSystemProxy(ctx, param.target)), Commands.SystemToggleProxyCurrent(var param, _) => ctx.Execute(SystemProxy, m => m.ToggleSystemProxy(ctx, ctx.Guild, param.toggle)), Commands.SystemToggleProxy(var param, _) => ctx.Execute(SystemProxy, m => m.ToggleSystemProxy(ctx, param.target, param.toggle)), + Commands.SystemShowPrivacy(var param, _) => ctx.Execute(SystemPrivacy, m => m.ShowSystemPrivacy(ctx, ctx.System)), + Commands.SystemChangePrivacyAll(var param, _) => ctx.Execute(SystemPrivacy, m => m.ChangeSystemPrivacyAll(ctx, ctx.System, param.level)), + Commands.SystemChangePrivacy(var param, _) => ctx.Execute(SystemPrivacy, m => m.ChangeSystemPrivacy(ctx, ctx.System, param.privacy, param.level)), _ => // this should only ever occur when deving if commands are not implemented... ctx.Reply( @@ -363,8 +366,6 @@ public partial class CommandTree await ctx.CheckSystem(target).Execute(SystemFrontPercent, m => m.FrontPercent(ctx, system: target)); else if (ctx.Match("groups", "gs")) await ctx.CheckSystem(target).Execute(GroupList, g => g.ListSystemGroups(ctx, target)); - else if (ctx.Match("privacy")) - await ctx.CheckSystem(target).Execute(SystemPrivacy, m => m.SystemPrivacy(ctx, target)); else if (ctx.Match("id")) await ctx.CheckSystem(target).Execute(SystemId, m => m.DisplayId(ctx, target)); else if (ctx.Match("random", "rand", "r")) diff --git a/PluralKit.Bot/CommandSystem/Context/ContextFlagsExt.cs b/PluralKit.Bot/CommandSystem/Context/ContextFlagsExt.cs index 564a5f30..cf636322 100644 --- a/PluralKit.Bot/CommandSystem/Context/ContextFlagsExt.cs +++ b/PluralKit.Bot/CommandSystem/Context/ContextFlagsExt.cs @@ -36,7 +36,7 @@ public static class ContextFlagsExt ); } - public static async Task FlagResolvePrivacyLevel(this Context ctx, string param_name) + public static async Task FlagResolvePrivacyLevel(this Context ctx, string param_name) { return await ctx.Parameters.ResolveFlag( ctx, param_name, diff --git a/PluralKit.Bot/CommandSystem/Context/ContextParametersExt.cs b/PluralKit.Bot/CommandSystem/Context/ContextParametersExt.cs index fd67ad76..45efb250 100644 --- a/PluralKit.Bot/CommandSystem/Context/ContextParametersExt.cs +++ b/PluralKit.Bot/CommandSystem/Context/ContextParametersExt.cs @@ -36,7 +36,15 @@ public static class ContextParametersExt ); } - public static async Task ParamResolvePrivacyLevel(this Context ctx, string param_name) + public static async Task ParamResolveSystemPrivacyTarget(this Context ctx, string param_name) + { + return await ctx.Parameters.ResolveParameter( + ctx, param_name, + param => (param as Parameter.SystemPrivacyTarget)?.target + ); + } + + public static async Task ParamResolvePrivacyLevel(this Context ctx, string param_name) { return await ctx.Parameters.ResolveParameter( ctx, param_name, diff --git a/PluralKit.Bot/CommandSystem/Parameters.cs b/PluralKit.Bot/CommandSystem/Parameters.cs index 6846938e..28656ce6 100644 --- a/PluralKit.Bot/CommandSystem/Parameters.cs +++ b/PluralKit.Bot/CommandSystem/Parameters.cs @@ -1,4 +1,3 @@ -using System.Diagnostics; using Myriad.Types; using PluralKit.Core; using uniffi.commands; @@ -12,7 +11,8 @@ public abstract record Parameter() public record SystemRef(PKSystem system): Parameter; public record GuildRef(Guild guild): Parameter; public record MemberPrivacyTarget(MemberPrivacySubject target): Parameter; - public record PrivacyLevel(string level): Parameter; + public record SystemPrivacyTarget(SystemPrivacySubject target): Parameter; + public record PrivacyLevel(Core.PrivacyLevel level): Parameter; public record Toggle(bool value): Parameter; public record Opaque(string value): Parameter; public record Avatar(ParsedImage avatar): Parameter; @@ -72,12 +72,16 @@ public class Parameters ); case uniffi.commands.Parameter.MemberPrivacyTarget memberPrivacyTarget: // this should never really fail... - // todo: we shouldn't have *three* different MemberPrivacyTarget types (rust, ffi, c#) syncing the cases will be annoying... - if (!MemberPrivacyUtils.TryParseMemberPrivacy(memberPrivacyTarget.target, out var target)) + if (!MemberPrivacyUtils.TryParseMemberPrivacy(memberPrivacyTarget.target, out var memberPrivacy)) throw new PKError($"Invalid member privacy target {memberPrivacyTarget.target}"); - return new Parameter.MemberPrivacyTarget(target); + return new Parameter.MemberPrivacyTarget(memberPrivacy); + case uniffi.commands.Parameter.SystemPrivacyTarget systemPrivacyTarget: + // this should never really fail... + if (!SystemPrivacyUtils.TryParseSystemPrivacy(systemPrivacyTarget.target, out var systemPrivacy)) + throw new PKError($"Invalid system privacy target {systemPrivacyTarget.target}"); + return new Parameter.SystemPrivacyTarget(systemPrivacy); case uniffi.commands.Parameter.PrivacyLevel privacyLevel: - return new Parameter.PrivacyLevel(privacyLevel.level); + return new Parameter.PrivacyLevel(privacyLevel.level == "public" ? PrivacyLevel.Public : privacyLevel.level == "private" ? PrivacyLevel.Private : throw new PKError($"Invalid privacy level {privacyLevel.level}")); case uniffi.commands.Parameter.Toggle toggle: return new Parameter.Toggle(toggle.toggle); case uniffi.commands.Parameter.OpaqueString opaque: diff --git a/PluralKit.Bot/Commands/SystemEdit.cs b/PluralKit.Bot/Commands/SystemEdit.cs index f162ee9b..3e72def3 100644 --- a/PluralKit.Bot/Commands/SystemEdit.cs +++ b/PluralKit.Bot/Commands/SystemEdit.cs @@ -874,79 +874,72 @@ public class SystemEdit $"Proxying in {serverText} is currently **disabled** for your system. To enable it, type `{ctx.DefaultPrefix}system proxy on`."); } - public async Task SystemPrivacy(Context ctx, PKSystem target) + public async Task ShowSystemPrivacy(Context ctx, PKSystem target) { ctx.CheckSystem().CheckOwnSystem(target); - Task PrintEmbed() + var eb = new EmbedBuilder() + .Title("Current privacy settings for your system") + .Field(new Embed.Field("Name", target.NamePrivacy.Explanation())) + .Field(new Embed.Field("Avatar", target.AvatarPrivacy.Explanation())) + .Field(new Embed.Field("Description", target.DescriptionPrivacy.Explanation())) + .Field(new Embed.Field("Banner", target.BannerPrivacy.Explanation())) + .Field(new Embed.Field("Pronouns", target.PronounPrivacy.Explanation())) + .Field(new Embed.Field("Member list", target.MemberListPrivacy.Explanation())) + .Field(new Embed.Field("Group list", target.GroupListPrivacy.Explanation())) + .Field(new Embed.Field("Current fronter(s)", target.FrontPrivacy.Explanation())) + .Field(new Embed.Field("Front/switch history", target.FrontHistoryPrivacy.Explanation())) + .Description( + $"To edit privacy settings, use the command:\n`{ctx.DefaultPrefix}system privacy `\n\n- `subject` is one of `name`, `avatar`, `description`, `banner`, `pronouns`, `list`, `front`, `fronthistory`, `groups`, or `all` \n- `level` is either `public` or `private`."); + await ctx.Reply(embed: eb.Build()); + } + + public async Task ChangeSystemPrivacy(Context ctx, PKSystem target, SystemPrivacySubject subject, PrivacyLevel level) + { + ctx.CheckSystem().CheckOwnSystem(target); + + await ctx.Repository.UpdateSystem(target.Id, new SystemPatch().WithPrivacy(subject, level)); + + var levelExplanation = level switch { - var eb = new EmbedBuilder() - .Title("Current privacy settings for your system") - .Field(new Embed.Field("Name", target.NamePrivacy.Explanation())) - .Field(new Embed.Field("Avatar", target.AvatarPrivacy.Explanation())) - .Field(new Embed.Field("Description", target.DescriptionPrivacy.Explanation())) - .Field(new Embed.Field("Banner", target.BannerPrivacy.Explanation())) - .Field(new Embed.Field("Pronouns", target.PronounPrivacy.Explanation())) - .Field(new Embed.Field("Member list", target.MemberListPrivacy.Explanation())) - .Field(new Embed.Field("Group list", target.GroupListPrivacy.Explanation())) - .Field(new Embed.Field("Current fronter(s)", target.FrontPrivacy.Explanation())) - .Field(new Embed.Field("Front/switch history", target.FrontHistoryPrivacy.Explanation())) - .Description( - $"To edit privacy settings, use the command:\n`{ctx.DefaultPrefix}system privacy `\n\n- `subject` is one of `name`, `avatar`, `description`, `banner`, `pronouns`, `list`, `front`, `fronthistory`, `groups`, or `all` \n- `level` is either `public` or `private`."); - return ctx.Reply(embed: eb.Build()); - } + PrivacyLevel.Public => "be able to query", + PrivacyLevel.Private => "*not* be able to query", + _ => "" + }; - async Task SetLevel(SystemPrivacySubject subject, PrivacyLevel level) + var subjectStr = subject switch { - await ctx.Repository.UpdateSystem(target.Id, new SystemPatch().WithPrivacy(subject, level)); + SystemPrivacySubject.Name => "name", + SystemPrivacySubject.Avatar => "avatar", + SystemPrivacySubject.Description => "description", + SystemPrivacySubject.Banner => "banner", + SystemPrivacySubject.Pronouns => "pronouns", + SystemPrivacySubject.Front => "front", + SystemPrivacySubject.FrontHistory => "front history", + SystemPrivacySubject.MemberList => "member list", + SystemPrivacySubject.GroupList => "group list", + _ => "" + }; - var levelExplanation = level switch - { - PrivacyLevel.Public => "be able to query", - PrivacyLevel.Private => "*not* be able to query", - _ => "" - }; + var msg = $"System {subjectStr} privacy has been set to **{level.LevelName()}**. Other accounts will now {levelExplanation} your system {subjectStr}."; + await ctx.Reply($"{Emojis.Success} {msg}"); + } - var subjectStr = subject switch - { - SystemPrivacySubject.Name => "name", - SystemPrivacySubject.Avatar => "avatar", - SystemPrivacySubject.Description => "description", - SystemPrivacySubject.Banner => "banner", - SystemPrivacySubject.Pronouns => "pronouns", - SystemPrivacySubject.Front => "front", - SystemPrivacySubject.FrontHistory => "front history", - SystemPrivacySubject.MemberList => "member list", - SystemPrivacySubject.GroupList => "group list", - _ => "" - }; + public async Task ChangeSystemPrivacyAll(Context ctx, PKSystem target, PrivacyLevel level) + { + ctx.CheckSystem().CheckOwnSystem(target); - var msg = - $"System {subjectStr} privacy has been set to **{level.LevelName()}**. Other accounts will now {levelExplanation} your system {subjectStr}."; - await ctx.Reply($"{Emojis.Success} {msg}"); - } + await ctx.Repository.UpdateSystem(target.Id, new SystemPatch().WithAllPrivacy(level)); - async Task SetAll(PrivacyLevel level) + var msg = level switch { - await ctx.Repository.UpdateSystem(target.Id, new SystemPatch().WithAllPrivacy(level)); + PrivacyLevel.Private => + $"All system privacy settings have been set to **{level.LevelName()}**. Other accounts will now not be able to view your member list, group list, front history, or system description.", + PrivacyLevel.Public => + $"All system privacy settings have been set to **{level.LevelName()}**. Other accounts will now be able to view everything.", + _ => "" + }; - var msg = level switch - { - PrivacyLevel.Private => - $"All system privacy settings have been set to **{level.LevelName()}**. Other accounts will now not be able to view your member list, group list, front history, or system description.", - PrivacyLevel.Public => - $"All system privacy settings have been set to **{level.LevelName()}**. Other accounts will now be able to view everything.", - _ => "" - }; - - await ctx.Reply($"{Emojis.Success} {msg}"); - } - - if (!ctx.HasNext()) - await PrintEmbed(); - else if (ctx.Match("all")) - await SetAll(ctx.PopPrivacyLevel()); - else - await SetLevel(ctx.PopSystemPrivacySubject(), ctx.PopPrivacyLevel()); + await ctx.Reply($"{Emojis.Success} {msg}"); } } \ No newline at end of file diff --git a/crates/command_definitions/src/system.rs b/crates/command_definitions/src/system.rs index 59b26ecc..a4fe0037 100644 --- a/crates/command_definitions/src/system.rs +++ b/crates/command_definitions/src/system.rs @@ -202,12 +202,25 @@ pub fn edit() -> impl Iterator { let system_proxy = tokens!(system, "proxy"); let system_proxy_cmd = [ - command!(system_proxy => "system_show_proxy_current").help("Shows your system's proxy setting for the guild you are in"), + command!(system_proxy => "system_show_proxy_current") + .help("Shows your system's proxy setting for the guild you are in"), command!(system_proxy, Toggle => "system_toggle_proxy_current") .help("Toggle your system's proxy for the guild you are in"), - command!(system_proxy, GuildRef => "system_show_proxy").help("Shows your system's proxy setting for a guild"), + command!(system_proxy, GuildRef => "system_show_proxy") + .help("Shows your system's proxy setting for a guild"), command!(system_proxy, GuildRef, Toggle => "system_toggle_proxy") .help("Toggle your system's proxy for a guild"), + ] + .into_iter(); + + let system_privacy = tokens!(system, ("privacy", ["priv"])); + let system_privacy_cmd = [ + command!(system_privacy => "system_show_privacy") + .help("Shows your system's privacy settings"), + command!(system_privacy, ("all", ["a"]), ("level", PrivacyLevel) => "system_change_privacy_all") + .help("Changes all privacy settings for your system"), + command!(system_privacy, ("privacy", SystemPrivacyTarget), ("level", PrivacyLevel) => "system_change_privacy") + .help("Changes a specific privacy setting for your system"), ].into_iter(); system_new_cmd @@ -222,6 +235,7 @@ pub fn edit() -> impl Iterator { .chain(system_server_avatar_self_cmd) .chain(system_banner_self_cmd) .chain(system_delete) + .chain(system_privacy_cmd) .chain(system_proxy_cmd) .chain(system_name_cmd) .chain(system_server_name_cmd) diff --git a/crates/command_parser/src/parameter.rs b/crates/command_parser/src/parameter.rs index dde6b535..282cfdc8 100644 --- a/crates/command_parser/src/parameter.rs +++ b/crates/command_parser/src/parameter.rs @@ -14,6 +14,7 @@ pub enum ParameterValue { SystemRef(String), GuildRef(String), MemberPrivacyTarget(String), + SystemPrivacyTarget(String), PrivacyLevel(String), Toggle(bool), Avatar(String), @@ -45,6 +46,7 @@ impl Display for Parameter { ParameterKind::SystemRef => write!(f, ""), ParameterKind::GuildRef => write!(f, ""), ParameterKind::MemberPrivacyTarget => write!(f, ""), + ParameterKind::SystemPrivacyTarget => write!(f, ""), ParameterKind::PrivacyLevel => write!(f, "[privacy level]"), ParameterKind::Toggle => write!(f, "on/off"), ParameterKind::Avatar => write!(f, ""), @@ -78,6 +80,7 @@ pub enum ParameterKind { SystemRef, GuildRef, MemberPrivacyTarget, + SystemPrivacyTarget, PrivacyLevel, Toggle, Avatar, @@ -92,6 +95,7 @@ impl ParameterKind { ParameterKind::SystemRef => "target", ParameterKind::GuildRef => "target", ParameterKind::MemberPrivacyTarget => "member_privacy_target", + ParameterKind::SystemPrivacyTarget => "system_privacy_target", ParameterKind::PrivacyLevel => "privacy_level", ParameterKind::Toggle => "toggle", ParameterKind::Avatar => "avatar", @@ -112,6 +116,9 @@ impl ParameterKind { ParameterKind::SystemRef => Ok(ParameterValue::SystemRef(input.into())), ParameterKind::MemberPrivacyTarget => MemberPrivacyTargetKind::from_str(input) .map(|target| ParameterValue::MemberPrivacyTarget(target.as_ref().into())), + ParameterKind::SystemPrivacyTarget => SystemPrivacyTargetKind::from_str(input).map( + |target| ParameterValue::SystemPrivacyTarget(target.as_ref().into()), + ), ParameterKind::PrivacyLevel => PrivacyLevelKind::from_str(input) .map(|level| ParameterValue::PrivacyLevel(level.as_ref().into())), ParameterKind::Toggle => { @@ -176,6 +183,53 @@ impl FromStr for MemberPrivacyTargetKind { } } +pub enum SystemPrivacyTargetKind { + Name, + Avatar, + Description, + Banner, + Pronouns, + MemberList, + GroupList, + Front, + FrontHistory, +} + +impl AsRef for SystemPrivacyTargetKind { + fn as_ref(&self) -> &str { + match self { + Self::Name => "name", + Self::Avatar => "avatar", + Self::Description => "description", + Self::Banner => "banner", + Self::Pronouns => "pronouns", + Self::MemberList => "members", + Self::GroupList => "groups", + Self::Front => "front", + Self::FrontHistory => "fronthistory", + } + } +} + +impl FromStr for SystemPrivacyTargetKind { + type Err = SmolStr; + + fn from_str(s: &str) -> Result { + match s.to_lowercase().as_str() { + "name" => Ok(Self::Name), + "avatar" | "pfp" | "pic" | "icon" => Ok(Self::Avatar), + "description" | "desc" | "bio" | "info" => Ok(Self::Description), + "banner" | "splash" | "cover" => Ok(Self::Banner), + "pronouns" | "prns" | "pn" => Ok(Self::Pronouns), + "members" | "memberlist" | "list" => Ok(Self::MemberList), + "groups" | "gs" => Ok(Self::GroupList), + "front" | "fronter" | "fronters" => Ok(Self::Front), + "fronthistory" | "fh" | "switches" => Ok(Self::FrontHistory), + _ => Err("invalid system privacy target".into()), + } + } +} + pub enum PrivacyLevelKind { Public, Private, diff --git a/crates/commands/src/bin/write_cs_glue.rs b/crates/commands/src/bin/write_cs_glue.rs index dda4e1b3..dbee8d80 100644 --- a/crates/commands/src/bin/write_cs_glue.rs +++ b/crates/commands/src/bin/write_cs_glue.rs @@ -169,7 +169,8 @@ fn get_param_ty(kind: ParameterKind) -> &'static str { ParameterKind::MemberRef => "PKMember", ParameterKind::SystemRef => "PKSystem", ParameterKind::MemberPrivacyTarget => "MemberPrivacySubject", - ParameterKind::PrivacyLevel => "string", + ParameterKind::SystemPrivacyTarget => "SystemPrivacySubject", + ParameterKind::PrivacyLevel => "PrivacyLevel", ParameterKind::Toggle => "bool", ParameterKind::Avatar => "ParsedImage", ParameterKind::GuildRef => "Guild", @@ -182,6 +183,7 @@ fn get_param_param_ty(kind: ParameterKind) -> &'static str { ParameterKind::MemberRef => "Member", ParameterKind::SystemRef => "System", ParameterKind::MemberPrivacyTarget => "MemberPrivacyTarget", + ParameterKind::SystemPrivacyTarget => "SystemPrivacyTarget", ParameterKind::PrivacyLevel => "PrivacyLevel", ParameterKind::Toggle => "Toggle", ParameterKind::Avatar => "Avatar", diff --git a/crates/commands/src/commands.udl b/crates/commands/src/commands.udl index 1b9f9ff1..5a368266 100644 --- a/crates/commands/src/commands.udl +++ b/crates/commands/src/commands.udl @@ -12,6 +12,7 @@ interface Parameter { SystemRef(string system); GuildRef(string guild); MemberPrivacyTarget(string target); + SystemPrivacyTarget(string target); PrivacyLevel(string level); OpaqueString(string raw); Toggle(boolean toggle); diff --git a/crates/commands/src/lib.rs b/crates/commands/src/lib.rs index 0cbd975a..92ca7e4f 100644 --- a/crates/commands/src/lib.rs +++ b/crates/commands/src/lib.rs @@ -26,6 +26,7 @@ pub enum Parameter { SystemRef { system: String }, GuildRef { guild: String }, MemberPrivacyTarget { target: String }, + SystemPrivacyTarget { target: String }, PrivacyLevel { level: String }, OpaqueString { raw: String }, Toggle { toggle: bool }, @@ -38,6 +39,7 @@ impl From for Parameter { ParameterValue::MemberRef(member) => Self::MemberRef { member }, ParameterValue::SystemRef(system) => Self::SystemRef { system }, ParameterValue::MemberPrivacyTarget(target) => Self::MemberPrivacyTarget { target }, + ParameterValue::SystemPrivacyTarget(target) => Self::SystemPrivacyTarget { target }, ParameterValue::PrivacyLevel(level) => Self::PrivacyLevel { level }, ParameterValue::OpaqueString(raw) => Self::OpaqueString { raw }, ParameterValue::Toggle(toggle) => Self::Toggle { toggle }, From c6a5d1c8b200b0da88be667e1788a83bf408afea Mon Sep 17 00:00:00 2001 From: dusk Date: Sat, 12 Apr 2025 23:44:12 +0900 Subject: [PATCH 083/179] build(nix): use ref instead of tag in git input for uniffi --- flake.lock | 15 ++++++++------- flake.nix | 2 +- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/flake.lock b/flake.lock index 0e29c367..3a7c37d4 100644 --- a/flake.lock +++ b/flake.lock @@ -327,19 +327,20 @@ "uniffi-bindgen-cs": { "flake": false, "locked": { - "lastModified": 1732196488, - "narHash": "sha256-zqNUUFd3OSAwmMh+hnN6AVpGnwu+ZJ1jjivbzN1k5Io=", - "ref": "refs/heads/main", - "rev": "fe5cd23943fd3aec335e2bb8f709ec1956992ae9", - "revCount": 115, + "lastModified": 1725356776, + "narHash": "sha256-w4K8BWMfUxohWE0nWpy3Qbc2CtRQTQ4dbER+sls61WI=", + "ref": "refs/tags/v0.8.3+v0.25.0", + "rev": "f68639fbc720b50ebe561ba75c66c84dc456bdce", + "revCount": 110, "submodules": true, "type": "git", - "url": "https://github.com/NordSecurity/uniffi-bindgen-cs?tag=v0.8.3%2Bv0.25.0" + "url": "https://github.com/NordSecurity/uniffi-bindgen-cs" }, "original": { + "ref": "refs/tags/v0.8.3+v0.25.0", "submodules": true, "type": "git", - "url": "https://github.com/NordSecurity/uniffi-bindgen-cs?tag=v0.8.3%2Bv0.25.0" + "url": "https://github.com/NordSecurity/uniffi-bindgen-cs" } } }, diff --git a/flake.nix b/flake.nix index 5debf1e1..6ff4a7a8 100644 --- a/flake.nix +++ b/flake.nix @@ -16,7 +16,7 @@ nci.inputs.nixpkgs.follows = "nixpkgs"; nci.inputs.dream2nix.follows = "d2n"; nci.inputs.treefmt.follows = "treefmt"; - uniffi-bindgen-cs.url = "git+https://github.com/NordSecurity/uniffi-bindgen-cs?tag=v0.8.3+v0.25.0&submodules=1"; + uniffi-bindgen-cs.url = "git+https://github.com/NordSecurity/uniffi-bindgen-cs?ref=refs/tags/v0.8.3+v0.25.0&submodules=1"; uniffi-bindgen-cs.flake = false; # misc treefmt.url = "github:numtide/treefmt-nix"; From 915d8b449bd87df656a8e745303656b0f745f78a Mon Sep 17 00:00:00 2001 From: dusk Date: Wed, 13 Aug 2025 02:41:41 +0300 Subject: [PATCH 084/179] fix(bot): get rid of syntax error in SystemEdit --- PluralKit.Bot/Commands/SystemEdit.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/PluralKit.Bot/Commands/SystemEdit.cs b/PluralKit.Bot/Commands/SystemEdit.cs index 245625c5..31d7d761 100644 --- a/PluralKit.Bot/Commands/SystemEdit.cs +++ b/PluralKit.Bot/Commands/SystemEdit.cs @@ -235,7 +235,8 @@ public class SystemEdit .Color(newColor.ToDiscordColor()) .Thumbnail(new Embed.EmbedThumbnail($"attachment://color.gif")) .Build(), - files: [MiscUtils.GenerateColorPreview(color)]);); + files: [MiscUtils.GenerateColorPreview(newColor)] + ); } public async Task ClearColor(Context ctx, PKSystem target, bool flagConfirmYes) From 5d57bd9320be94d008faf0fcc86fd2d683330ea3 Mon Sep 17 00:00:00 2001 From: dusk Date: Wed, 13 Aug 2025 02:42:16 +0300 Subject: [PATCH 085/179] build(nix): update flake dependencies and fixup the flake --- flake.lock | 91 +++++++++++++++++++++++++++++------------------------- flake.nix | 23 ++++++-------- 2 files changed, 58 insertions(+), 56 deletions(-) diff --git a/flake.lock b/flake.lock index 3a7c37d4..497a50b1 100644 --- a/flake.lock +++ b/flake.lock @@ -26,11 +26,11 @@ "pyproject-nix": "pyproject-nix" }, "locked": { - "lastModified": 1734729217, - "narHash": "sha256-UaBik0h7veLw+VqsK5EP2ucC68BEkHLDJkcfmY+wEuY=", + "lastModified": 1753366881, + "narHash": "sha256-jsoTEhkmn3weESMNRMLNk/ROW3fcHCr8Wgf5amzs5z8=", "owner": "nix-community", "repo": "dream2nix", - "rev": "98c1c2e934995a2c6ce740d4ff43ce0daa19b79f", + "rev": "e6566e4ce924a8258499c379ee9552dba1883bce", "type": "github" }, "original": { @@ -57,12 +57,12 @@ }, "flake-compat_2": { "locked": { - "lastModified": 1696426674, - "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=", - "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33", - "revCount": 57, + "lastModified": 1733328505, + "narHash": "sha256-NeCCThCEP3eCl2l/+27kNNK7QrwZB1IJCrXfrbv5oqU=", + "rev": "ff81ac966bb2cae68946d5ed5fc4994f96d0ffec", + "revCount": 69, "type": "tarball", - "url": "https://api.flakehub.com/f/pinned/edolstra/flake-compat/1.0.1/018afb31-abd1-7bff-a5e4-cff7e18efb7a/source.tar.gz" + "url": "https://api.flakehub.com/f/pinned/edolstra/flake-compat/1.1.0/01948eb7-9cba-704f-bbf3-3fa956735b52/source.tar.gz?rev=ff81ac966bb2cae68946d5ed5fc4994f96d0ffec&revCount=69" }, "original": { "type": "tarball", @@ -104,11 +104,11 @@ ] }, "locked": { - "lastModified": 1735917398, - "narHash": "sha256-RkwVkqozmbYvwX63Q4GNkNCsPuHR8sUIax40J5A4l3A=", + "lastModified": 1754720322, + "narHash": "sha256-86ic12SviaoGIIQORsbOBs01ZOtRi/fbPqKfQvJdWxY=", "owner": "yusdacra", "repo": "nix-cargo-integration", - "rev": "aff54b572b75af13a6b31108ff7732d17674ad43", + "rev": "dcf65e1bf185b81c2c5ddee50bf464f634f38453", "type": "github" }, "original": { @@ -119,11 +119,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1734435836, - "narHash": "sha256-kMBQ5PRiFLagltK0sH+08aiNt3zGERC2297iB6vrvlU=", + "lastModified": 1754651824, + "narHash": "sha256-aB7ft6njy9EJfuW+rdToNChfRrHNRw/yTg5cSEnG+HI=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "4989a246d7a390a859852baddb1013f825435cee", + "rev": "b069b7c1e2fe1a3a24221428558bf44128d3d5c8", "type": "github" }, "original": { @@ -134,14 +134,17 @@ }, "nixpkgs-lib": { "locked": { - "lastModified": 1733096140, - "narHash": "sha256-1qRH7uAUsyQI7R1Uwl4T+XvdNv778H0Nb5njNrqvylY=", - "type": "tarball", - "url": "https://github.com/NixOS/nixpkgs/archive/5487e69da40cbd611ab2cadee0b4637225f7cfae.tar.gz" + "lastModified": 1753579242, + "narHash": "sha256-zvaMGVn14/Zz8hnp4VWT9xVnhc8vuL3TStRqwk22biA=", + "owner": "nix-community", + "repo": "nixpkgs.lib", + "rev": "0f36c44e01a6129be94e3ade315a5883f0228a6e", + "type": "github" }, "original": { - "type": "tarball", - "url": "https://github.com/NixOS/nixpkgs/archive/5487e69da40cbd611ab2cadee0b4637225f7cfae.tar.gz" + "owner": "nix-community", + "repo": "nixpkgs.lib", + "type": "github" } }, "parts": { @@ -149,11 +152,11 @@ "nixpkgs-lib": "nixpkgs-lib" }, "locked": { - "lastModified": 1733312601, - "narHash": "sha256-4pDvzqnegAfRkPwO3wmwBhVi/Sye1mzps0zHWYnP88c=", + "lastModified": 1754487366, + "narHash": "sha256-pHYj8gUBapuUzKV/kN/tR3Zvqc7o6gdFB9XKXIp1SQ8=", "owner": "hercules-ci", "repo": "flake-parts", - "rev": "205b12d8b7cd4802fbcb8e8ef6a0f1408781a4f9", + "rev": "af66ad14b28a127c5c0f3bbb298218fc63528a18", "type": "github" }, "original": { @@ -164,11 +167,11 @@ }, "process-compose": { "locked": { - "lastModified": 1733325752, - "narHash": "sha256-79tzPuXNRo1NUllafYW6SjeLtjqfnLGq7tHCM7cAXNg=", + "lastModified": 1749418557, + "narHash": "sha256-wJHHckWz4Gvj8HXtM5WVJzSKXAEPvskQANVoRiu2w1w=", "owner": "Platonic-Systems", "repo": "process-compose-flake", - "rev": "1012530b582f1bd3b102295c799358d95abf42d7", + "rev": "91dcc48a6298e47e2441ec76df711f4e38eab94e", "type": "github" }, "original": { @@ -201,18 +204,22 @@ } }, "pyproject-nix": { - "flake": false, + "inputs": { + "nixpkgs": [ + "d2n", + "nixpkgs" + ] + }, "locked": { - "lastModified": 1702448246, - "narHash": "sha256-hFg5s/hoJFv7tDpiGvEvXP0UfFvFEDgTdyHIjDVHu1I=", - "owner": "davhau", + "lastModified": 1752481895, + "narHash": "sha256-luVj97hIMpCbwhx3hWiRwjP2YvljWy8FM+4W9njDhLA=", + "owner": "pyproject-nix", "repo": "pyproject.nix", - "rev": "5a06a2697b228c04dd2f35659b4b659ca74f7aeb", + "rev": "16ee295c25107a94e59a7fc7f2e5322851781162", "type": "github" }, "original": { - "owner": "davhau", - "ref": "dream2nix", + "owner": "pyproject-nix", "repo": "pyproject.nix", "type": "github" } @@ -239,11 +246,11 @@ ] }, "locked": { - "lastModified": 1735871325, - "narHash": "sha256-6Ta5E4mhSfCP6LdkzkG2+BciLOCPeLKuYTJ6lOHW+mI=", + "lastModified": 1754707163, + "narHash": "sha256-wgVgOsyJUDn2ZRpzu2gELKALoJXlBSoZJSln+Tlg5Pw=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "a599f011db521766cbaf7c2f5874182485554f00", + "rev": "ac39ab4c8ed7cefe48d5ae5750f864422df58f01", "type": "github" }, "original": { @@ -254,11 +261,11 @@ }, "services": { "locked": { - "lastModified": 1734242477, - "narHash": "sha256-u+fkdD8+0/0J8k0/YKDc3ReUcYZZGiftGL+Sz2wdRqM=", + "lastModified": 1754187875, + "narHash": "sha256-0JoDuijBaB5g7bZpUIJxgTz5yPi/C+iLnNWtKIz/qas=", "owner": "juspay", "repo": "services-flake", - "rev": "acc7f3f9f30621b469ca3ee511592a68a4437312", + "rev": "f625a3ef44d579013bca08cf5ee86006a093230e", "type": "github" }, "original": { @@ -311,11 +318,11 @@ ] }, "locked": { - "lastModified": 1734704479, - "narHash": "sha256-MMi74+WckoyEWBRcg/oaGRvXC9BVVxDZNRMpL+72wBI=", + "lastModified": 1754492133, + "narHash": "sha256-B+3g9+76KlGe34Yk9za8AF3RL+lnbHXkLiVHLjYVOAc=", "owner": "numtide", "repo": "treefmt-nix", - "rev": "65712f5af67234dad91a5a4baee986a8b62dbf8f", + "rev": "1298185c05a56bff66383a20be0b41a307f52228", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 6ff4a7a8..168b3773 100644 --- a/flake.nix +++ b/flake.nix @@ -39,7 +39,6 @@ self', pkgs, lib, - system, ... }: let @@ -51,7 +50,11 @@ }; rustOutputs = config.nci.outputs; - composeCfg = config.process-compose."dev"; + + sourceDotenv = '' + # shellcheck disable=SC1091 + [[ -f ".env" ]] && echo "sourcing .env file..." && set -a && source .env && set +a + ''; in { treefmt = { @@ -117,11 +120,15 @@ gcc omnisharp-roslyn bashInteractive + postgresql ]; }; all = (pkgs.mkShell.override { stdenv = services.stdenv; }) { name = "pk-devshell"; nativeBuildInputs = bot.nativeBuildInputs ++ services.nativeBuildInputs; + shellHook = '' + ${sourceDotenv} + ''; }; docs = pkgs.mkShellNoCC { buildInputs = with pkgs; [ nodejs yarn ]; @@ -132,12 +139,6 @@ process-compose."dev" = let dataDir = ".nix-process-compose"; - pluralkitConfCheck = '' - [[ -f "pluralkit.conf" ]] || (echo "pluralkit config not found, please copy pluralkit.conf.example to pluralkit.conf and edit it" && exit 1) - ''; - sourceDotenv = '' - [[ -f ".env" ]] && echo "sourcing .env file..." && export "$(xargs < .env)" - ''; in { imports = [ inp.services.processComposeModules.default ]; @@ -166,12 +167,8 @@ settings.processes = let - procCfg = composeCfg.settings.processes; mkServiceProcess = name: attrs: - let - shell = rustOutputs.${name}.devShell; - in attrs // { command = pkgs.writeShellApplication { @@ -180,7 +177,6 @@ text = '' ${sourceDotenv} set -x - ${pluralkitConfCheck} nix develop .#services -c cargo run --package ${name} ''; }; @@ -195,7 +191,6 @@ text = '' ${sourceDotenv} set -x - ${pluralkitConfCheck} ${self'.apps.generate-command-parser-bindings.program} nix develop .#bot -c bash -c "dotnet build ./PluralKit.Bot/PluralKit.Bot.csproj -c Release -o obj/ && dotnet obj/PluralKit.Bot.dll" ''; From 4a865b45cda79babf901cbd24e20713bb471d673 Mon Sep 17 00:00:00 2001 From: dusk Date: Wed, 13 Aug 2025 02:42:53 +0300 Subject: [PATCH 086/179] build: define rust-toolchain.toml and point to 'latest' nightly, fix rust crates on it --- crates/api/Cargo.toml | 2 +- crates/api/src/main.rs | 2 -- crates/command_parser/Cargo.toml | 2 +- crates/command_parser/src/lib.rs | 1 - crates/gateway/Cargo.toml | 2 +- crates/gateway/src/main.rs | 3 +-- crates/gdpr_worker/Cargo.toml | 2 +- crates/libpk/Cargo.toml | 4 ++-- crates/libpk/src/_config.rs | 8 ++++++-- crates/libpk/src/lib.rs | 1 - crates/migrate/Cargo.toml | 2 +- crates/migrate/src/main.rs | 2 -- rust-toolchain.toml | 4 ++-- 13 files changed, 16 insertions(+), 19 deletions(-) diff --git a/crates/api/Cargo.toml b/crates/api/Cargo.toml index d2f883d7..b37ac12e 100644 --- a/crates/api/Cargo.toml +++ b/crates/api/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "api" version = "0.1.0" -edition = "2021" +edition = "2024" [dependencies] pluralkit_models = { path = "../models" } diff --git a/crates/api/src/main.rs b/crates/api/src/main.rs index e3a201cb..05ad7718 100644 --- a/crates/api/src/main.rs +++ b/crates/api/src/main.rs @@ -1,5 +1,3 @@ -#![feature(let_chains)] - use auth::{AuthState, INTERNAL_APPID_HEADER, INTERNAL_SYSTEMID_HEADER}; use axum::{ body::Body, diff --git a/crates/command_parser/Cargo.toml b/crates/command_parser/Cargo.toml index 749d348c..169bef16 100644 --- a/crates/command_parser/Cargo.toml +++ b/crates/command_parser/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "command_parser" version = "0.1.0" -edition = "2021" +edition = "2024" [dependencies] lazy_static = { workspace = true } diff --git a/crates/command_parser/src/lib.rs b/crates/command_parser/src/lib.rs index 0a444915..34614f2d 100644 --- a/crates/command_parser/src/lib.rs +++ b/crates/command_parser/src/lib.rs @@ -1,4 +1,3 @@ -#![feature(let_chains)] #![feature(anonymous_lifetime_in_impl_trait)] pub mod command; diff --git a/crates/gateway/Cargo.toml b/crates/gateway/Cargo.toml index c707b29b..0222ab18 100644 --- a/crates/gateway/Cargo.toml +++ b/crates/gateway/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "gateway" version = "0.1.0" -edition = "2021" +edition = "2024" [dependencies] anyhow = { workspace = true } diff --git a/crates/gateway/src/main.rs b/crates/gateway/src/main.rs index e61c3445..0e49601e 100644 --- a/crates/gateway/src/main.rs +++ b/crates/gateway/src/main.rs @@ -1,6 +1,5 @@ -#![feature(let_chains)] +#![feature(duration_constructors_lite)] #![feature(if_let_guard)] -#![feature(duration_constructors)] use chrono::Timelike; use discord::gateway::cluster_config; diff --git a/crates/gdpr_worker/Cargo.toml b/crates/gdpr_worker/Cargo.toml index a30751f9..b57ccddf 100644 --- a/crates/gdpr_worker/Cargo.toml +++ b/crates/gdpr_worker/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "gdpr_worker" version = "0.1.0" -edition = "2021" +edition = "2024" [dependencies] libpk = { path = "../libpk" } diff --git a/crates/libpk/Cargo.toml b/crates/libpk/Cargo.toml index 30d77ae0..23d912c0 100644 --- a/crates/libpk/Cargo.toml +++ b/crates/libpk/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "libpk" version = "0.1.0" -edition = "2021" +edition = "2024" [dependencies] anyhow = { workspace = true } @@ -12,7 +12,7 @@ pk_macros = { path = "../macros" } sentry = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } -sqlx = { workspace = true } +sqlx = { workspace = true, features = ["chrono"] } tokio = { workspace = true } tracing = { workspace = true } tracing-subscriber = { workspace = true} diff --git a/crates/libpk/src/_config.rs b/crates/libpk/src/_config.rs index 8358440b..92ee45ba 100644 --- a/crates/libpk/src/_config.rs +++ b/crates/libpk/src/_config.rs @@ -148,11 +148,15 @@ lazy_static! { // hacks if let Ok(var) = std::env::var("NOMAD_ALLOC_INDEX") && std::env::var("pluralkit__discord__cluster__total_nodes").is_ok() { - std::env::set_var("pluralkit__discord__cluster__node_id", var); + unsafe { + std::env::set_var("pluralkit__discord__cluster__node_id", var); + } } if let Ok(var) = std::env::var("STATEFULSET_NAME_FOR_INDEX") && std::env::var("pluralkit__discord__cluster__total_nodes").is_ok() { - std::env::set_var("pluralkit__discord__cluster__node_id", var.split("-").last().unwrap()); + unsafe { + std::env::set_var("pluralkit__discord__cluster__node_id", var.split("-").last().unwrap()); + } } Arc::new(Config::builder() diff --git a/crates/libpk/src/lib.rs b/crates/libpk/src/lib.rs index 55031bf3..fda8ba29 100644 --- a/crates/libpk/src/lib.rs +++ b/crates/libpk/src/lib.rs @@ -1,4 +1,3 @@ -#![feature(let_chains)] use std::net::SocketAddr; use metrics_exporter_prometheus::PrometheusBuilder; diff --git a/crates/migrate/Cargo.toml b/crates/migrate/Cargo.toml index cf4eff2d..0843cb3f 100644 --- a/crates/migrate/Cargo.toml +++ b/crates/migrate/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "migrate" version = "0.1.0" -edition = "2021" +edition = "2024" [dependencies] libpk = { path = "../libpk" } diff --git a/crates/migrate/src/main.rs b/crates/migrate/src/main.rs index 85b15e33..0ee621e2 100644 --- a/crates/migrate/src/main.rs +++ b/crates/migrate/src/main.rs @@ -1,5 +1,3 @@ -#![feature(let_chains)] - use tracing::info; include!(concat!(env!("OUT_DIR"), "/data.rs")); diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 15df6b03..cbf2fc12 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,3 +1,3 @@ [toolchain] -channel = "nightly-2024-08-20" -components = ["rust-src", "rustfmt"] \ No newline at end of file +channel = "nightly-2025-08-09" +components = ["rust-src", "rustfmt"] From aa397137f2c20b65234727dcc0234033106333bb Mon Sep 17 00:00:00 2001 From: dusk Date: Wed, 13 Aug 2025 19:42:58 +0300 Subject: [PATCH 087/179] chore(crates): remove some warnings --- crates/avatars/src/main.rs | 2 +- crates/dispatch/src/main.rs | 3 +-- crates/gdpr_worker/src/main.rs | 2 -- crates/libpk/src/db/repository/avatars.rs | 4 ++-- 4 files changed, 4 insertions(+), 7 deletions(-) diff --git a/crates/avatars/src/main.rs b/crates/avatars/src/main.rs index c8399086..fb3d23b5 100644 --- a/crates/avatars/src/main.rs +++ b/crates/avatars/src/main.rs @@ -153,7 +153,7 @@ async fn verify( ) .await?; - let encoded = process::process_async(result.data, req.kind).await?; + let _ = process::process_async(result.data, req.kind).await?; Ok(()) } diff --git a/crates/dispatch/src/main.rs b/crates/dispatch/src/main.rs index 6570cf19..040884f2 100644 --- a/crates/dispatch/src/main.rs +++ b/crates/dispatch/src/main.rs @@ -12,8 +12,7 @@ use std::{ time::Duration, }; use tokio::{net::UdpSocket, sync::RwLock}; -use tracing::{debug, error, info}; -use tracing_subscriber::EnvFilter; +use tracing::{debug, error}; use axum::{extract::State, http::Uri, routing::post, Json, Router}; diff --git a/crates/gdpr_worker/src/main.rs b/crates/gdpr_worker/src/main.rs index b40557c0..8fefb153 100644 --- a/crates/gdpr_worker/src/main.rs +++ b/crates/gdpr_worker/src/main.rs @@ -1,5 +1,3 @@ -#![feature(let_chains)] - use sqlx::prelude::FromRow; use std::{sync::Arc, time::Duration}; use tracing::{error, info, warn}; diff --git a/crates/libpk/src/db/repository/avatars.rs b/crates/libpk/src/db/repository/avatars.rs index 1ff10cc7..66f48768 100644 --- a/crates/libpk/src/db/repository/avatars.rs +++ b/crates/libpk/src/db/repository/avatars.rs @@ -51,8 +51,8 @@ pub async fn remove_deletion_queue(pool: &PgPool, attachment_id: u64) -> anyhow: } pub async fn pop_queue( - pool: &PgPool, -) -> anyhow::Result, ImageQueueEntry)>> { + pool: &'_ PgPool, +) -> anyhow::Result, ImageQueueEntry)>> { let mut tx = pool.begin().await?; let res: Option = sqlx::query_as("delete from image_queue where itemid = (select itemid from image_queue order by itemid for update skip locked limit 1) returning *") .fetch_optional(&mut *tx).await?; From 4a7ee0deb066bafa0d10e4038407f422a0efca6f Mon Sep 17 00:00:00 2001 From: dusk Date: Thu, 4 Sep 2025 00:12:31 +0300 Subject: [PATCH 088/179] feat: implement member edit commands --- PluralKit.Bot/CommandMeta/CommandTree.cs | 96 +- PluralKit.Bot/Commands/MemberEdit.cs | 1133 +++++++++++----------- crates/command_definitions/src/member.rs | 206 +++- 3 files changed, 794 insertions(+), 641 deletions(-) diff --git a/PluralKit.Bot/CommandMeta/CommandTree.cs b/PluralKit.Bot/CommandMeta/CommandTree.cs index d1264a2f..e550a81e 100644 --- a/PluralKit.Bot/CommandMeta/CommandTree.cs +++ b/PluralKit.Bot/CommandMeta/CommandTree.cs @@ -16,6 +16,41 @@ public partial class CommandTree Commands.MemberShow(var param, _) => ctx.Execute(MemberInfo, m => m.ViewMember(ctx, param.target)), Commands.MemberNew(var param, _) => ctx.Execute(MemberNew, m => m.NewMember(ctx, param.name)), Commands.MemberSoulscream(var param, _) => ctx.Execute(MemberInfo, m => m.Soulscream(ctx, param.target)), + Commands.MemberPronounsShow(var param, var flags) => ctx.Execute(MemberPronouns, m => m.ShowPronouns(ctx, param.target, flags.GetReplyFormat())), + Commands.MemberPronounsClear(var param, var flags) => ctx.Execute(MemberPronouns, m => m.ClearPronouns(ctx, param.target, flags.yes)), + Commands.MemberPronounsUpdate(var param, _) => ctx.Execute(MemberPronouns, m => m.ChangePronouns(ctx, param.target, param.pronouns)), + Commands.MemberDescShow(var param, var flags) => ctx.Execute(MemberDesc, m => m.ShowDescription(ctx, param.target, flags.GetReplyFormat())), + Commands.MemberDescClear(var param, var flags) => ctx.Execute(MemberDesc, m => m.ClearDescription(ctx, param.target, flags.yes)), + Commands.MemberDescUpdate(var param, _) => ctx.Execute(MemberDesc, m => m.ChangeDescription(ctx, param.target, param.description)), + Commands.MemberNameShow(var param, var flags) => ctx.Execute(MemberInfo, m => m.ShowName(ctx, param.target, flags.GetReplyFormat())), + Commands.MemberNameUpdate(var param, _) => ctx.Execute(MemberInfo, m => m.ChangeName(ctx, param.target, param.name)), + Commands.MemberBannerShow(var param, var flags) => ctx.Execute(MemberBannerImage, m => m.ShowBannerImage(ctx, param.target, flags.GetReplyFormat())), + Commands.MemberBannerClear(var param, var flags) => ctx.Execute(MemberBannerImage, m => m.ClearBannerImage(ctx, param.target, flags.yes)), + Commands.MemberBannerUpdate(var param, _) => ctx.Execute(MemberBannerImage, m => m.ChangeBannerImage(ctx, param.target, param.banner)), + Commands.MemberColorShow(var param, var flags) => ctx.Execute(MemberColor, m => m.ShowColor(ctx, param.target, flags.GetReplyFormat())), + Commands.MemberColorClear(var param, var flags) => ctx.Execute(MemberColor, m => m.ClearColor(ctx, param.target, flags.yes)), + Commands.MemberColorUpdate(var param, _) => ctx.Execute(MemberColor, m => m.ChangeColor(ctx, param.target, param.color)), + Commands.MemberBirthdayShow(var param, var flags) => ctx.Execute(MemberBirthday, m => m.ShowBirthday(ctx, param.target, flags.GetReplyFormat())), + Commands.MemberBirthdayClear(var param, var flags) => ctx.Execute(MemberBirthday, m => m.ClearBirthday(ctx, param.target, flags.yes)), + Commands.MemberBirthdayUpdate(var param, _) => ctx.Execute(MemberBirthday, m => m.ChangeBirthday(ctx, param.target, param.birthday)), + Commands.MemberDisplaynameShow(var param, var flags) => ctx.Execute(MemberDisplayName, m => m.ShowDisplayName(ctx, param.target, flags.GetReplyFormat())), + Commands.MemberDisplaynameClear(var param, var flags) => ctx.Execute(MemberDisplayName, m => m.ClearDisplayName(ctx, param.target, flags.yes)), + Commands.MemberDisplaynameUpdate(var param, _) => ctx.Execute(MemberDisplayName, m => m.ChangeDisplayName(ctx, param.target, param.name)), + Commands.MemberServernameShow(var param, var flags) => ctx.Execute(MemberServerName, m => m.ShowServerName(ctx, param.target, flags.GetReplyFormat())), + Commands.MemberServernameClear(var param, var flags) => ctx.Execute(MemberServerName, m => m.ClearServerName(ctx, param.target, flags.yes)), + Commands.MemberServernameUpdate(var param, _) => ctx.Execute(MemberServerName, m => m.ChangeServerName(ctx, param.target, param.name)), + Commands.MemberKeepproxyShow(var param, _) => ctx.Execute(MemberKeepProxy, m => m.ShowKeepProxy(ctx, param.target)), + Commands.MemberKeepproxyUpdate(var param, _) => ctx.Execute(MemberKeepProxy, m => m.ChangeKeepProxy(ctx, param.target, param.value)), + Commands.MemberServerKeepproxyShow(var param, _) => ctx.Execute(MemberServerKeepProxy, m => m.ShowServerKeepProxy(ctx, param.target)), + Commands.MemberServerKeepproxyUpdate(var param, _) => ctx.Execute(MemberServerKeepProxy, m => m.ChangeServerKeepProxy(ctx, param.target, param.value)), + Commands.MemberServerKeepproxyClear(var param, var flags) => ctx.Execute(MemberServerKeepProxy, m => m.ClearServerKeepProxy(ctx, param.target, flags.yes)), + Commands.MemberTtsShow(var param, _) => ctx.Execute(MemberTts, m => m.ShowTts(ctx, param.target)), + Commands.MemberTtsUpdate(var param, _) => ctx.Execute(MemberTts, m => m.ChangeTts(ctx, param.target, param.value)), + Commands.MemberAutoproxyShow(var param, _) => ctx.Execute(MemberAutoproxy, m => m.ShowAutoproxy(ctx, param.target)), + Commands.MemberAutoproxyUpdate(var param, _) => ctx.Execute(MemberAutoproxy, m => m.ChangeAutoproxy(ctx, param.target, param.value)), + Commands.MemberDelete(var param, _) => ctx.Execute(MemberDelete, m => m.Delete(ctx, param.target)), + Commands.MemberPrivacyShow(var param, _) => ctx.Execute(MemberPrivacy, m => m.ShowPrivacy(ctx, param.target)), + Commands.MemberPrivacyUpdate(var param, _) => ctx.Execute(MemberPrivacy, m => m.ChangePrivacy(ctx, param.target, param.member_privacy_target, param.new_privacy_level)), Commands.CfgApAccountShow => ctx.Execute(null, m => m.ViewAutoproxyAccount(ctx)), Commands.CfgApAccountUpdate(var param, _) => ctx.Execute(null, m => m.EditAutoproxyAccount(ctx, param.toggle)), Commands.CfgApTimeoutShow => ctx.Execute(null, m => m.ViewAutoproxyTimeout(ctx)), @@ -378,45 +413,29 @@ public partial class CommandTree private async Task HandleMemberCommand(Context ctx) { // TODO: implement - // if (ctx.Match("new", "n", "add", "create", "register")) - // await ctx.Execute(MemberNew, m => m.NewMember(ctx)); - // else if (ctx.Match("list")) - // await ctx.Execute(SystemList, m => m.MemberList(ctx, ctx.System)); - // else if (ctx.Match("commands", "help")) - // await PrintCommandList(ctx, "members", MemberCommands); - // else if (await ctx.MatchMember() is PKMember target) - // await HandleMemberCommandTargeted(ctx, target); - // else if (!ctx.HasNext()) - // await PrintCommandExpectedError(ctx, MemberNew, MemberInfo, MemberRename, MemberDisplayName, - // MemberServerName, MemberDesc, MemberPronouns, - // MemberColor, MemberBirthday, MemberProxy, MemberDelete, MemberAvatar); - // else - // await ctx.Reply($"{Emojis.Error} {ctx.CreateNotFoundError("Member", ctx.PopArgument())}"); + if (ctx.Match("list")) + await ctx.Execute(SystemList, m => m.MemberList(ctx, ctx.System)); + else if (ctx.Match("commands", "help")) + await PrintCommandList(ctx, "members", MemberCommands); + else if (await ctx.MatchMember() is PKMember target) + await HandleMemberCommandTargeted(ctx, target); + else if (!ctx.HasNext()) + await PrintCommandExpectedError(ctx, MemberNew, MemberInfo, MemberRename, MemberDisplayName, + MemberServerName, MemberDesc, MemberPronouns, + MemberColor, MemberBirthday, MemberProxy, MemberDelete, MemberAvatar); + else + await ctx.Reply($"{Emojis.Error} {ctx.CreateNotFoundError("Member", ctx.PopArgument())}"); } private async Task HandleMemberCommandTargeted(Context ctx, PKMember target) { // Commands that have a member target (eg. pk;member delete) - if (ctx.Match("rename", "name", "changename", "setname", "rn")) - await ctx.Execute(MemberRename, m => m.Name(ctx, target)); - else if (ctx.Match("description", "desc", "describe", "d", "bio", "info", "text", "intro")) - await ctx.Execute(MemberDesc, m => m.Description(ctx, target)); - else if (ctx.Match("pronouns", "pronoun", "prns", "pn")) - await ctx.Execute(MemberPronouns, m => m.Pronouns(ctx, target)); - else if (ctx.Match("color", "colour")) - await ctx.Execute(MemberColor, m => m.Color(ctx, target)); - else if (ctx.Match("birthday", "birth", "bday", "birthdate", "cakeday", "bdate", "bd")) - await ctx.Execute(MemberBirthday, m => m.Birthday(ctx, target)); - else if (ctx.Match("proxy", "tags", "proxytags", "brackets")) + if (ctx.Match("proxy", "tags", "proxytags", "brackets")) await ctx.Execute(MemberProxy, m => m.Proxy(ctx, target)); - else if (ctx.Match("delete", "remove", "destroy", "erase", "yeet")) - await ctx.Execute(MemberDelete, m => m.Delete(ctx, target)); else if (ctx.Match("avatar", "profile", "picture", "icon", "image", "pfp", "pic")) await ctx.Execute(MemberAvatar, m => m.Avatar(ctx, target)); else if (ctx.Match("proxyavatar", "proxypfp", "webhookavatar", "webhookpfp", "pa", "pavatar", "ppfp")) await ctx.Execute(MemberAvatar, m => m.WebhookAvatar(ctx, target)); - else if (ctx.Match("banner", "splash", "cover")) - await ctx.Execute(MemberBannerImage, m => m.BannerImage(ctx, target)); else if (ctx.Match("group", "groups", "g")) if (ctx.Match("add", "a")) await ctx.Execute(MemberGroupAdd, @@ -429,27 +448,8 @@ public partial class CommandTree else if (ctx.Match("serveravatar", "sa", "servericon", "serverimage", "serverpfp", "serverpic", "savatar", "spic", "guildavatar", "guildpic", "guildicon", "sicon", "spfp")) await ctx.Execute(MemberServerAvatar, m => m.ServerAvatar(ctx, target)); - else if (ctx.Match("displayname", "dn", "dname", "nick", "nickname", "dispname")) - await ctx.Execute(MemberDisplayName, m => m.DisplayName(ctx, target)); - else if (ctx.Match("servername", "sn", "sname", "snick", "snickname", "servernick", "servernickname", - "serverdisplayname", "guildname", "guildnick", "guildnickname", "serverdn")) - await ctx.Execute(MemberServerName, m => m.ServerName(ctx, target)); - else if (ctx.Match("autoproxy", "ap")) - await ctx.Execute(MemberAutoproxy, m => m.MemberAutoproxy(ctx, target)); - else if (ctx.Match("keepproxy", "keeptags", "showtags", "kp")) - await ctx.Execute(MemberKeepProxy, m => m.KeepProxy(ctx, target)); - else if (ctx.Match("texttospeech", "text-to-speech", "tts")) - await ctx.Execute(MemberTts, m => m.Tts(ctx, target)); - else if (ctx.Match("serverkeepproxy", "servershowtags", "guildshowtags", "guildkeeptags", "serverkeeptags", "skp")) - await ctx.Execute(MemberServerKeepProxy, m => m.ServerKeepProxy(ctx, target)); else if (ctx.Match("id")) await ctx.Execute(MemberId, m => m.DisplayId(ctx, target)); - else if (ctx.Match("privacy")) - await ctx.Execute(MemberPrivacy, m => m.Privacy(ctx, target, null)); - else if (ctx.Match("private", "hidden", "hide")) - await ctx.Execute(MemberPrivacy, m => m.Privacy(ctx, target, PrivacyLevel.Private)); - else if (ctx.Match("public", "shown", "show", "unhide", "unhidden")) - await ctx.Execute(MemberPrivacy, m => m.Privacy(ctx, target, PrivacyLevel.Public)); else await PrintCommandNotFoundError(ctx, MemberInfo, MemberRename, MemberDisplayName, MemberServerName, MemberDesc, MemberPronouns, MemberColor, MemberBirthday, MemberProxy, MemberDelete, MemberAvatar, diff --git a/PluralKit.Bot/Commands/MemberEdit.cs b/PluralKit.Bot/Commands/MemberEdit.cs index 525d60ab..94b127d2 100644 --- a/PluralKit.Bot/Commands/MemberEdit.cs +++ b/PluralKit.Bot/Commands/MemberEdit.cs @@ -21,38 +21,33 @@ public class MemberEdit _avatarHosting = avatarHosting; } - public async Task Name(Context ctx, PKMember target) + public async Task ShowName(Context ctx, PKMember target, ReplyFormat format) { - var format = ctx.MatchFormat(); - - if (!ctx.HasNext() || format != ReplyFormat.Standard) + var lctx = ctx.DirectLookupContextFor(target.System); + switch (format) { - var lctx = ctx.DirectLookupContextFor(target.System); - switch (format) - { - case ReplyFormat.Raw: - await ctx.Reply($"```{target.NameFor(lctx)}```"); - break; - case ReplyFormat.Plaintext: - var eb = new EmbedBuilder() - .Description($"Showing name for member {target.NameFor(ctx)} (`{target.DisplayHid(ctx.Config)}`)"); - await ctx.Reply(target.NameFor(lctx), embed: eb.Build()); - break; - default: - var replyStrQ = $"Name for member {target.DisplayHid(ctx.Config)} is **{target.NameFor(lctx)}**."; - if (target.System == ctx.System?.Id) - replyStrQ += $"\nTo rename {target.DisplayHid(ctx.Config)} type `{ctx.DefaultPrefix}member {target.NameFor(ctx)} rename `." - + $" Using {target.NameFor(lctx).Length}/{Limits.MaxMemberNameLength} characters."; - await ctx.Reply(replyStrQ); - break; - } - return; + case ReplyFormat.Raw: + await ctx.Reply($"```{target.NameFor(lctx)}```"); + break; + case ReplyFormat.Plaintext: + var eb = new EmbedBuilder() + .Description($"Showing name for member {target.NameFor(ctx)} (`{target.DisplayHid(ctx.Config)}`)"); + await ctx.Reply(target.NameFor(lctx), embed: eb.Build()); + break; + default: + var replyStrQ = $"Name for member {target.DisplayHid(ctx.Config)} is **{target.NameFor(lctx)}**."; + if (target.System == ctx.System?.Id) + replyStrQ += $"\nTo rename {target.DisplayHid(ctx.Config)} type `{ctx.DefaultPrefix}member {target.NameFor(ctx)} rename `." + + $" Using {target.NameFor(lctx).Length}/{Limits.MaxMemberNameLength} characters."; + await ctx.Reply(replyStrQ); + break; } + } + public async Task ChangeName(Context ctx, PKMember target, string newName) + { ctx.CheckSystem().CheckOwnMember(target); - var newName = ctx.RemainderOrNull() ?? throw new PKSyntaxError("You must pass a new name for the member."); - // Hard name length cap if (newName.Length > Limits.MaxMemberNameLength) throw Errors.StringTooLongError("Member name", newName.Length, Limits.MaxMemberNameLength); @@ -85,7 +80,7 @@ public class MemberEdit await ctx.Reply(replyStr); } - public async Task Description(Context ctx, PKMember target) + public async Task ShowDescription(Context ctx, PKMember target, ReplyFormat format) { ctx.CheckSystemPrivacy(target.System, target.DescriptionPrivacy); @@ -94,10 +89,8 @@ public class MemberEdit noDescriptionSetMessage += $" To set one, type `{ctx.DefaultPrefix}member {target.Reference(ctx)} description `."; - 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 (format != ReplyFormat.Standard) if (target.Description == null) { await ctx.Reply(noDescriptionSetMessage); @@ -117,54 +110,55 @@ public class MemberEdit return; } - if (!ctx.HasNext(false)) - { - await ctx.Reply(embed: new EmbedBuilder() - .Title("Member description") - .Description(target.Description) - .Field(new Embed.Field("\u200B", - $"To print the description with formatting, type `{ctx.DefaultPrefix}member {target.Reference(ctx)} description -raw`." - + (ctx.System?.Id == target.System - ? $" To clear it, type `{ctx.DefaultPrefix}member {target.Reference(ctx)} description -clear`." - + $" Using {target.Description.Length}/{Limits.MaxDescriptionLength} characters." - : ""))) - .Build()); - return; - } + await ctx.Reply(embed: new EmbedBuilder() + .Title("Member description") + .Description(target.Description) + .Field(new Embed.Field("\u200B", + $"To print the description with formatting, type `{ctx.DefaultPrefix}member {target.Reference(ctx)} description -raw`." + + (ctx.System?.Id == target.System + ? $" To clear it, type `{ctx.DefaultPrefix}member {target.Reference(ctx)} description -clear`." + + $" Using {target.Description.Length}/{Limits.MaxDescriptionLength} characters." + : ""))) + .Build()); + } + public async Task ClearDescription(Context ctx, PKMember target, bool confirmYes) + { + ctx.CheckSystemPrivacy(target.System, target.DescriptionPrivacy); ctx.CheckOwnMember(target); - if (ctx.MatchClear() && await ctx.ConfirmClear("this member's description")) + if (await ctx.ConfirmClear("this member's description", confirmYes)) { var patch = new MemberPatch { Description = Partial.Null() }; await ctx.Repository.UpdateMember(target.Id, patch); await ctx.Reply($"{Emojis.Success} Member description cleared."); } - else - { - var description = ctx.RemainderOrNull(false).NormalizeLineEndSpacing(); - if (description.IsLongerThan(Limits.MaxDescriptionLength)) - throw Errors.StringTooLongError("Description", description.Length, Limits.MaxDescriptionLength); - - var patch = new MemberPatch { Description = Partial.Present(description) }; - await ctx.Repository.UpdateMember(target.Id, patch); - - await ctx.Reply($"{Emojis.Success} Member description changed (using {description.Length}/{Limits.MaxDescriptionLength} characters)."); - } } - public async Task Pronouns(Context ctx, PKMember target) + public async Task ChangeDescription(Context ctx, PKMember target, string _description) { + ctx.CheckSystemPrivacy(target.System, target.DescriptionPrivacy); + ctx.CheckOwnMember(target); + + var description = _description.NormalizeLineEndSpacing(); + if (description.IsLongerThan(Limits.MaxDescriptionLength)) + throw Errors.StringTooLongError("Description", description.Length, Limits.MaxDescriptionLength); + + var patch = new MemberPatch { Description = Partial.Present(description) }; + await ctx.Repository.UpdateMember(target.Id, patch); + + await ctx.Reply($"{Emojis.Success} Member description changed (using {description.Length}/{Limits.MaxDescriptionLength} characters)."); + } + + public async Task ShowPronouns(Context ctx, PKMember target, ReplyFormat format) + { + ctx.CheckSystemPrivacy(target.System, target.PronounPrivacy); + var noPronounsSetMessage = "This member does not have pronouns set."; if (ctx.System?.Id == target.System) noPronounsSetMessage += $" To set some, type `{ctx.DefaultPrefix}member {target.Reference(ctx)} pronouns `."; - ctx.CheckSystemPrivacy(target.System, target.PronounPrivacy); - - 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 (format != ReplyFormat.Standard) if (target.Pronouns == null) { await ctx.Reply(noPronounsSetMessage); @@ -184,210 +178,240 @@ public class MemberEdit return; } - if (!ctx.HasNext(false)) - { - await ctx.Reply( - $"**{target.NameFor(ctx)}**'s pronouns are **{target.Pronouns}**.\nTo print the pronouns with formatting, type `{ctx.DefaultPrefix}member {target.Reference(ctx)} pronouns -raw`." - + (ctx.System?.Id == target.System - ? $" To clear them, type `{ctx.DefaultPrefix}member {target.Reference(ctx)} pronouns -clear`." - + $" Using {target.Pronouns.Length}/{Limits.MaxPronounsLength} characters." - : "")); - return; - } + await ctx.Reply( + $"**{target.NameFor(ctx)}**'s pronouns are **{target.Pronouns}**.\nTo print the pronouns with formatting, type `{ctx.DefaultPrefix}member {target.Reference(ctx)} pronouns -raw`." + + (ctx.System?.Id == target.System + ? $" To clear them, type `{ctx.DefaultPrefix}member {target.Reference(ctx)} pronouns -clear`." + + $" Using {target.Pronouns.Length}/{Limits.MaxPronounsLength} characters." + : "")); + } + public async Task ClearPronouns(Context ctx, PKMember target, bool confirmYes) + { ctx.CheckOwnMember(target); - if (ctx.MatchClear() && await ctx.ConfirmClear("this member's pronouns")) + if (await ctx.ConfirmClear("this member's pronouns", confirmYes)) { var patch = new MemberPatch { Pronouns = Partial.Null() }; await ctx.Repository.UpdateMember(target.Id, patch); await ctx.Reply($"{Emojis.Success} Member pronouns cleared."); } - else - { - var pronouns = ctx.RemainderOrNull(false).NormalizeLineEndSpacing(); - if (pronouns.IsLongerThan(Limits.MaxPronounsLength)) - throw Errors.StringTooLongError("Pronouns", pronouns.Length, Limits.MaxPronounsLength); - - var patch = new MemberPatch { Pronouns = Partial.Present(pronouns) }; - await ctx.Repository.UpdateMember(target.Id, patch); - - await ctx.Reply($"{Emojis.Success} Member pronouns changed (using {pronouns.Length}/{Limits.MaxPronounsLength} characters)."); - } } - public async Task BannerImage(Context ctx, PKMember target) + public async Task ChangePronouns(Context ctx, PKMember target, string pronouns) { - async Task ClearBannerImage() - { - ctx.CheckOwnMember(target); - await ctx.ConfirmClear("this member's banner image"); + ctx.CheckOwnMember(target); - await ctx.Repository.UpdateMember(target.Id, new MemberPatch { BannerImage = null }); - await ctx.Reply($"{Emojis.Success} Member banner image cleared."); - } + pronouns = pronouns.NormalizeLineEndSpacing(); + if (pronouns.IsLongerThan(Limits.MaxPronounsLength)) + throw Errors.StringTooLongError("Pronouns", pronouns.Length, Limits.MaxPronounsLength); - async Task SetBannerImage(ParsedImage img) - { - ctx.CheckOwnMember(target); - img = await _avatarHosting.TryRehostImage(img, AvatarHostingService.RehostedImageType.Banner, ctx.Author.Id, ctx.System); - await _avatarHosting.VerifyAvatarOrThrow(img.Url, true); + var patch = new MemberPatch { Pronouns = Partial.Present(pronouns) }; + await ctx.Repository.UpdateMember(target.Id, patch); - await ctx.Repository.UpdateMember(target.Id, new MemberPatch { BannerImage = img.CleanUrl ?? img.Url }); + await ctx.Reply($"{Emojis.Success} Member pronouns changed (using {pronouns.Length}/{Limits.MaxPronounsLength} characters)."); + } - var msg = img.Source switch + public async Task ShowBannerImage(Context ctx, PKMember target, ReplyFormat format) + { + ctx.CheckSystemPrivacy(target.System, target.BannerPrivacy); + + var noBannerSetMessage = "This member does not have a banner image set."; + if (ctx.System?.Id == target.System) + noBannerSetMessage += $" To set one, type `{ctx.DefaultPrefix}member {target.Reference(ctx)} banner ` or attach an image."; + + if (format != ReplyFormat.Standard) + if (string.IsNullOrWhiteSpace(target.BannerImage)) { - AvatarSource.Url => $"{Emojis.Success} Member banner image changed to the image at the given URL.", - AvatarSource.HostedCdn => $"{Emojis.Success} Member banner image changed to attached image.", - AvatarSource.Attachment => - $"{Emojis.Success} Member banner image changed to attached image.\n{Emojis.Warn} If you delete the message containing the attachment, the banner image will stop working.", - AvatarSource.User => throw new PKError("Cannot set a banner image to an user's avatar."), - _ => throw new ArgumentOutOfRangeException() - }; + await ctx.Reply(noBannerSetMessage); + return; + } - // The attachment's already right there, no need to preview it. - var hasEmbed = img.Source != AvatarSource.Attachment && img.Source != AvatarSource.HostedCdn; - await (hasEmbed - ? ctx.Reply(msg, new EmbedBuilder().Image(new Embed.EmbedImage(img.Url)).Build()) - : ctx.Reply(msg)); - } - - async Task ShowBannerImage() + if (format == ReplyFormat.Raw) { - ctx.CheckSystemPrivacy(target.System, target.BannerPrivacy); - - if ((target.BannerImage?.Trim() ?? "").Length > 0) - switch (ctx.MatchFormat()) - { - case ReplyFormat.Raw: - await ctx.Reply($"`{target.BannerImage.TryGetCleanCdnUrl()}`"); - break; - case ReplyFormat.Plaintext: - var ebP = new EmbedBuilder() - .Description($"Showing banner for member {target.NameFor(ctx)} (`{target.DisplayHid(ctx.Config)}`)"); - await ctx.Reply(text: $"<{target.BannerImage.TryGetCleanCdnUrl()}>", embed: ebP.Build()); - break; - default: - var ebS = new EmbedBuilder() - .Title($"{target.NameFor(ctx)}'s banner image") - .Image(new Embed.EmbedImage(target.BannerImage.TryGetCleanCdnUrl())); - if (target.System == ctx.System?.Id) - ebS.Description($"To clear, use `{ctx.DefaultPrefix}member {target.Reference(ctx)} banner clear`."); - await ctx.Reply(embed: ebS.Build()); - break; - } - else - throw new PKSyntaxError( - "This member does not have a banner image set." + ((target.System == ctx.System?.Id) ? " Set one by attaching an image to this command, or by passing an image URL." : "")); + await ctx.Reply($"```\n{target.BannerImage.TryGetCleanCdnUrl()}\n```"); + return; } - - if (ctx.MatchClear()) - await ClearBannerImage(); - else if (await ctx.MatchImage() is { } img) - await SetBannerImage(img); - else - await ShowBannerImage(); - } - - public async Task Color(Context ctx, PKMember target) - { - var isOwnSystem = ctx.System?.Id == target.System; - var matchedFormat = ctx.MatchFormat(); - var matchedClear = ctx.MatchClear(); - - if (!isOwnSystem || !(ctx.HasNext() || matchedClear)) + if (format == ReplyFormat.Plaintext) { - if (target.Color == null) - await ctx.Reply( - "This member does not have a color set." + (isOwnSystem ? $" To set one, type `{ctx.DefaultPrefix}member {target.Reference(ctx)} color `." : "")); - 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") - .Color(target.Color.ToDiscordColor()) - .Thumbnail(new Embed.EmbedThumbnail($"attachment://color.gif")) - .Description($"This member's color is **#{target.Color}**." - + (isOwnSystem ? $" To clear it, type `{ctx.DefaultPrefix}member {target.Reference(ctx)} color -clear`." : "")) - .Build(), - files: [MiscUtils.GenerateColorPreview(target.Color)]); + var eb = new EmbedBuilder() + .Description($"Showing banner for member {target.NameFor(ctx)} (`{target.DisplayHid(ctx.Config)}`)"); + await ctx.Reply(text: $"<{target.BannerImage.TryGetCleanCdnUrl()}>", embed: eb.Build()); return; } - ctx.CheckSystem().CheckOwnMember(target); + var embed = new EmbedBuilder() + .Title($"{target.NameFor(ctx)}'s banner image") + .Image(new Embed.EmbedImage(target.BannerImage.TryGetCleanCdnUrl())); + if (target.System == ctx.System?.Id) + embed.Description($"To clear, type `{ctx.DefaultPrefix}member {target.Reference(ctx)} banner -clear`."); + await ctx.Reply(embed: embed.Build()); + } - if (matchedClear) + public async Task ClearBannerImage(Context ctx, PKMember target, bool confirmYes) + { + ctx.CheckOwnMember(target); + + if (await ctx.ConfirmClear("this member's banner image", confirmYes)) { - await ctx.Repository.UpdateMember(target.Id, new() { Color = Partial.Null() }); - - await ctx.Reply($"{Emojis.Success} Member color cleared."); - } - else - { - var color = ctx.RemainderOrNull(); - - if (color.StartsWith("#")) color = color.Substring(1); - if (!Regex.IsMatch(color, "^[0-9a-fA-F]{6}$")) throw Errors.InvalidColorError(color); - - var patch = new MemberPatch { Color = Partial.Present(color.ToLowerInvariant()) }; - await ctx.Repository.UpdateMember(target.Id, patch); - - await ctx.Reply(embed: new EmbedBuilder() - .Title($"{Emojis.Success} Member color changed.") - .Color(color.ToDiscordColor()) - .Thumbnail(new Embed.EmbedThumbnail($"attachment://color.gif")) - .Build(), - files: [MiscUtils.GenerateColorPreview(color)]); + await ctx.Repository.UpdateMember(target.Id, new MemberPatch { BannerImage = null }); + await ctx.Reply($"{Emojis.Success} Member banner image cleared."); } } - public async Task Birthday(Context ctx, PKMember target) + public async Task ChangeBannerImage(Context ctx, PKMember target, ParsedImage img) { - if (ctx.MatchClear() && await ctx.ConfirmClear("this member's birthday")) - { - ctx.CheckOwnMember(target); + ctx.CheckOwnMember(target); + img = await _avatarHosting.TryRehostImage(img, AvatarHostingService.RehostedImageType.Banner, ctx.Author.Id, ctx.System); + await _avatarHosting.VerifyAvatarOrThrow(img.Url, true); + + await ctx.Repository.UpdateMember(target.Id, new MemberPatch { BannerImage = img.CleanUrl ?? img.Url }); + + var msg = img.Source switch + { + AvatarSource.Url => $"{Emojis.Success} Member banner image changed to the image at the given URL.", + AvatarSource.HostedCdn => $"{Emojis.Success} Member banner image changed to attached image.", + AvatarSource.Attachment => + $"{Emojis.Success} Member banner image changed to attached image.\n{Emojis.Warn} If you delete the message containing the attachment, the banner image will stop working.", + AvatarSource.User => throw new PKError("Cannot set a banner image to an user's avatar."), + _ => throw new ArgumentOutOfRangeException() + }; + + // The attachment's already right there, no need to preview it. + var hasEmbed = img.Source != AvatarSource.Attachment && img.Source != AvatarSource.HostedCdn; + await (hasEmbed + ? ctx.Reply(msg, new EmbedBuilder().Image(new Embed.EmbedImage(img.Url)).Build()) + : ctx.Reply(msg)); + } + + public async Task ShowColor(Context ctx, PKMember target, ReplyFormat format) + { + if (target.Color == null) + { + await ctx.Reply( + "This member does not have a color set." + (ctx.System?.Id == target.System ? $" To set one, type `{ctx.DefaultPrefix}member {target.Reference(ctx)} color `." : "")); + return; + } + + if (format == ReplyFormat.Raw) + { + await ctx.Reply("```\n#" + target.Color + "\n```"); + return; + } + + if (format == ReplyFormat.Plaintext) + { + await ctx.Reply(target.Color); + return; + } + + await ctx.Reply(embed: new EmbedBuilder() + .Title("Member color") + .Color(target.Color.ToDiscordColor()) + .Thumbnail(new Embed.EmbedThumbnail($"attachment://color.gif")) + .Description($"This member's color is **#{target.Color}**." + + (ctx.System?.Id == target.System ? $" To clear it, type `{ctx.DefaultPrefix}member {target.Reference(ctx)} color -clear`." : "")) + .Build(), + files: [MiscUtils.GenerateColorPreview(target.Color)]); + } + + public async Task ClearColor(Context ctx, PKMember target, bool confirmYes) + { + ctx.CheckSystem().CheckOwnMember(target); + + if (await ctx.ConfirmClear("this member's color", confirmYes)) + { + await ctx.Repository.UpdateMember(target.Id, new() { Color = Partial.Null() }); + await ctx.Reply($"{Emojis.Success} Member color cleared."); + } + } + + public async Task ChangeColor(Context ctx, PKMember target, string color) + { + ctx.CheckSystem().CheckOwnMember(target); + + if (color.StartsWith("#")) + color = color.Substring(1); + + if (!Regex.IsMatch(color, "^[0-9a-fA-F]{6}$")) + throw Errors.InvalidColorError(color); + + var patch = new MemberPatch { Color = Partial.Present(color.ToLowerInvariant()) }; + await ctx.Repository.UpdateMember(target.Id, patch); + + await ctx.Reply(embed: new EmbedBuilder() + .Title($"{Emojis.Success} Member color changed.") + .Color(color.ToDiscordColor()) + .Thumbnail(new Embed.EmbedThumbnail($"attachment://color.gif")) + .Build(), + files: [MiscUtils.GenerateColorPreview(color)]); + } + + public async Task ShowBirthday(Context ctx, PKMember target, ReplyFormat format) + { + ctx.CheckSystemPrivacy(target.System, target.BirthdayPrivacy); + + var noBirthdaySetMessage = "This member does not have a birthdate set."; + if (ctx.System?.Id == target.System) + noBirthdaySetMessage += $" To set one, type `{ctx.DefaultPrefix}member {target.Reference(ctx)} birthday `."; + + // if what's next is "raw"/"plaintext" we need to check for null + if (format != ReplyFormat.Standard) + if (target.Birthday == null) + { + await ctx.Reply(noBirthdaySetMessage); + return; + } + + if (format == ReplyFormat.Raw) + { + await ctx.Reply($"```\n{target.Birthday}\n```"); + return; + } + if (format == ReplyFormat.Plaintext) + { + var eb = new EmbedBuilder() + .Description($"Showing birthday for member {target.NameFor(ctx)} (`{target.DisplayHid(ctx.Config)}`)"); + await ctx.Reply(target.BirthdayString, embed: eb.Build()); + return; + } + + await ctx.Reply($"This member's birthdate is **{target.BirthdayString}**." + + (ctx.System?.Id == target.System + ? $" To clear it, type `{ctx.DefaultPrefix}member {target.Reference(ctx)} birthday -clear`." + : "")); + } + + public async Task ClearBirthday(Context ctx, PKMember target, bool confirmYes) + { + ctx.CheckOwnMember(target); + + if (await ctx.ConfirmClear("this member's birthday", confirmYes)) + { var patch = new MemberPatch { Birthday = Partial.Null() }; await ctx.Repository.UpdateMember(target.Id, patch); - await ctx.Reply($"{Emojis.Success} Member birthdate cleared."); } - else if (!ctx.HasNext()) - { - ctx.CheckSystemPrivacy(target.System, target.BirthdayPrivacy); + } - if (target.Birthday == null) - await ctx.Reply("This member does not have a birthdate set." - + (ctx.System?.Id == target.System - ? $" To set one, type `{ctx.DefaultPrefix}member {target.Reference(ctx)} birthdate `." - : "")); - else - await ctx.Reply($"This member's birthdate is **{target.BirthdayString}**." - + (ctx.System?.Id == target.System - ? $" To clear it, type `{ctx.DefaultPrefix}member {target.Reference(ctx)} birthdate -clear`." - : "")); - } + public async Task ChangeBirthday(Context ctx, PKMember target, string birthdayStr) + { + ctx.CheckOwnMember(target); + + LocalDate? birthday; + if (birthdayStr == "today" || birthdayStr == "now") + birthday = SystemClock.Instance.InZone(ctx.Zone).GetCurrentDate(); else - { - ctx.CheckOwnMember(target); + birthday = DateUtils.ParseDate(birthdayStr, true); - var birthdayStr = ctx.RemainderOrNull(); + if (birthday == null) + throw Errors.BirthdayParseError(birthdayStr); - LocalDate? birthday; - if (birthdayStr == "today" || birthdayStr == "now") - birthday = SystemClock.Instance.InZone(ctx.Zone).GetCurrentDate(); - else - birthday = DateUtils.ParseDate(birthdayStr, true); + var patch = new MemberPatch { Birthday = Partial.Present(birthday) }; + await ctx.Repository.UpdateMember(target.Id, patch); - if (birthday == null) throw Errors.BirthdayParseError(birthdayStr); - - var patch = new MemberPatch { Birthday = Partial.Present(birthday) }; - await ctx.Repository.UpdateMember(target.Id, patch); - - await ctx.Reply($"{Emojis.Success} Member birthdate changed."); - } + await ctx.Reply($"{Emojis.Success} Member birthdate changed."); } private string boldIf(string str, bool condition) => condition ? $"**{str}**" : str; @@ -429,11 +453,54 @@ public class MemberEdit return eb; } - public async Task DisplayName(Context ctx, PKMember target) + public async Task ShowDisplayName(Context ctx, PKMember target, ReplyFormat format) { - async Task PrintSuccess(string text) + var isOwner = ctx.System?.Id == target.System; + var noDisplayNameSetMessage = $"This member does not have a display name set{(isOwner ? "" : " or name is private")}." + + (isOwner ? $" To set one, type `{ctx.DefaultPrefix}member {target.Reference(ctx)} displayname `." : ""); + + // Whether displayname is shown or not should depend on if member name privacy is set. + // If name privacy is on then displayname should look like name. + if (target.DisplayName == null || !target.NamePrivacy.CanAccess(ctx.DirectLookupContextFor(target.System))) { - var successStr = text; + await ctx.Reply(noDisplayNameSetMessage); + 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.NameFor(ctx)} (`{target.DisplayHid(ctx.Config)}`)"); + await ctx.Reply(target.DisplayName, embed: _eb.Build()); + return; + } + + var eb = await CreateMemberNameInfoEmbed(ctx, target); + var reference = target.Reference(ctx); + if (ctx.System?.Id == target.System) + eb.Description( + $"To change display name, type `{ctx.DefaultPrefix}member {reference} displayname `.\n" + + $"To clear it, type `{ctx.DefaultPrefix}member {reference} displayname -clear`.\n" + + $"To print the raw display name, type `{ctx.DefaultPrefix}member {reference} displayname -raw`."); + await ctx.Reply(embed: eb.Build()); + } + + public async Task ClearDisplayName(Context ctx, PKMember target, bool confirmYes) + { + ctx.CheckOwnMember(target); + + if (await ctx.ConfirmClear("this member's display name", confirmYes)) + { + var patch = new MemberPatch { DisplayName = Partial.Null() }; + await ctx.Repository.UpdateMember(target.Id, patch); + + var successStr = $"{Emojis.Success} Member display name cleared. This member will now be proxied using their member name \"{target.Name}\"."; + if (ctx.Guild != null) { var memberGuildConfig = await ctx.Repository.GetMemberGuild(ctx.Guild.Id, target.Id); @@ -443,80 +510,37 @@ public class MemberEdit } await ctx.Reply(successStr); - } - - var isOwner = ctx.System?.Id == target.System; - var noDisplayNameSetMessage = $"This member does not have a display name set{(isOwner ? "" : " or name is private")}." - + (isOwner ? $" To set one, type `{ctx.DefaultPrefix}member {target.Reference(ctx)} displayname `." : ""); - - // Whether displayname is shown or not should depend on if member name privacy is set. - // If name privacy is on then displayname should look like name. - - 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 || !target.NamePrivacy.CanAccess(ctx.DirectLookupContextFor(target.System))) - { - await ctx.Reply(noDisplayNameSetMessage); - 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.NameFor(ctx)} (`{target.DisplayHid(ctx.Config)}`)"); - await ctx.Reply(target.DisplayName, embed: eb.Build()); - return; - } - - if (!ctx.HasNext(false)) - { - var eb = await CreateMemberNameInfoEmbed(ctx, target); - var reference = target.Reference(ctx); - if (ctx.System?.Id == target.System) - eb.Description( - $"To change display name, type `{ctx.DefaultPrefix}member {reference} displayname `.\n" - + $"To clear it, type `{ctx.DefaultPrefix}member {reference} displayname -clear`.\n" - + $"To print the raw display name, type `{ctx.DefaultPrefix}member {reference} displayname -raw`."); - await ctx.Reply(embed: eb.Build()); - return; - } - - ctx.CheckOwnMember(target); - - if (ctx.MatchClear() && await ctx.ConfirmClear("this member's display name")) - { - var patch = new MemberPatch { DisplayName = Partial.Null() }; - await ctx.Repository.UpdateMember(target.Id, patch); - - await PrintSuccess( - $"{Emojis.Success} Member display name cleared. This member will now be proxied using their member name \"{target.Name}\"."); if (target.NamePrivacy == PrivacyLevel.Private) await ctx.Reply($"{Emojis.Warn} Since this member no longer has a display name set, their name privacy **can no longer take effect**."); } - else - { - var newDisplayName = ctx.RemainderOrNull(false).NormalizeLineEndSpacing(); - - if (newDisplayName.Length > Limits.MaxMemberNameLength) - throw Errors.StringTooLongError("Member display name", newDisplayName.Length, Limits.MaxMemberNameLength); - - var patch = new MemberPatch { DisplayName = Partial.Present(newDisplayName) }; - await ctx.Repository.UpdateMember(target.Id, patch); - - await PrintSuccess( - $"{Emojis.Success} Member display name changed (using {newDisplayName.Length}/{Limits.MaxMemberNameLength} characters). This member will now be proxied using the name \"{newDisplayName}\"."); - } } - public async Task ServerName(Context ctx, PKMember target) + public async Task ChangeDisplayName(Context ctx, PKMember target, string newDisplayName) + { + ctx.CheckOwnMember(target); + + newDisplayName = newDisplayName.NormalizeLineEndSpacing(); + if (newDisplayName.Length > Limits.MaxMemberNameLength) + throw Errors.StringTooLongError("Member display name", newDisplayName.Length, Limits.MaxMemberNameLength); + + var patch = new MemberPatch { DisplayName = Partial.Present(newDisplayName) }; + await ctx.Repository.UpdateMember(target.Id, patch); + + var successStr = $"{Emojis.Success} Member display name changed (using {newDisplayName.Length}/{Limits.MaxMemberNameLength} characters). This member will now be proxied using the name \"{newDisplayName}\"."; + + if (ctx.Guild != null) + { + var memberGuildConfig = await ctx.Repository.GetMemberGuild(ctx.Guild.Id, target.Id); + if (memberGuildConfig.DisplayName != null) + successStr += + $" However, this member has a server name set in this server ({ctx.Guild.Name}), and will be proxied using that name, \"{memberGuildConfig.DisplayName}\", here."; + } + + await ctx.Reply(successStr); + } + + public async Task ShowServerName(Context ctx, PKMember target, ReplyFormat format) { ctx.CheckGuildContext(); @@ -525,12 +549,8 @@ public class MemberEdit noServerNameSetMessage += $" To set one, type `{ctx.DefaultPrefix}member {target.Reference(ctx)} servername `."; - // No perms check, display name isn't covered by member privacy var memberGuildConfig = await ctx.Repository.GetMemberGuild(ctx.Guild.Id, target.Id); - 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) { @@ -545,26 +565,26 @@ public class MemberEdit } if (format == ReplyFormat.Plaintext) { - var eb = new EmbedBuilder() + var _eb = new EmbedBuilder() .Description($"Showing servername for member {target.NameFor(ctx)} (`{target.DisplayHid(ctx.Config)}`)"); - await ctx.Reply(memberGuildConfig.DisplayName, embed: eb.Build()); + await ctx.Reply(memberGuildConfig.DisplayName, embed: _eb.Build()); return; } - if (!ctx.HasNext(false)) - { - var eb = await CreateMemberNameInfoEmbed(ctx, target); - var reference = target.Reference(ctx); - if (ctx.System?.Id == target.System) - eb.Description( - $"To change server name, type `{ctx.DefaultPrefix}member {reference} servername `.\nTo clear it, type `{ctx.DefaultPrefix}member {reference} servername -clear`.\nTo print the raw server name, type `{ctx.DefaultPrefix}member {reference} servername -raw`."); - await ctx.Reply(embed: eb.Build()); - return; - } + var eb = await CreateMemberNameInfoEmbed(ctx, target); + var reference = target.Reference(ctx); + if (ctx.System?.Id == target.System) + eb.Description( + $"To change server name, type `{ctx.DefaultPrefix}member {reference} servername `.\nTo clear it, type `{ctx.DefaultPrefix}member {reference} servername -clear`.\nTo print the raw server name, type `{ctx.DefaultPrefix}member {reference} servername -raw`."); + await ctx.Reply(embed: eb.Build()); + } + public async Task ClearServerName(Context ctx, PKMember target, bool confirmYes) + { + ctx.CheckGuildContext(); ctx.CheckOwnMember(target); - if (ctx.MatchClear() && await ctx.ConfirmClear("this member's server name")) + if (await ctx.ConfirmClear("this member's server name", confirmYes)) { await ctx.Repository.UpdateMemberGuild(target.Id, ctx.Guild.Id, new MemberGuildPatch { DisplayName = null }); @@ -575,59 +595,52 @@ public class MemberEdit await ctx.Reply( $"{Emojis.Success} Member server name cleared. This member will now be proxied using their member name \"{target.NameFor(ctx)}\" in this server ({ctx.Guild.Name})."); } - else - { - var newServerName = ctx.RemainderOrNull(false).NormalizeLineEndSpacing(); - - await ctx.Repository.UpdateMemberGuild(target.Id, ctx.Guild.Id, - new MemberGuildPatch { DisplayName = newServerName }); - - await ctx.Reply( - $"{Emojis.Success} Member server name changed (using {newServerName.Length}/{Limits.MaxMemberNameLength} characters). This member will now be proxied using the name \"{newServerName}\" in this server ({ctx.Guild.Name})."); - } } - public async Task KeepProxy(Context ctx, PKMember target) + public async Task ChangeServerName(Context ctx, PKMember target, string newServerName) { - ctx.CheckSystem().CheckOwnMember(target); - MemberGuildSettings? memberGuildConfig = null; + ctx.CheckGuildContext(); + ctx.CheckOwnMember(target); + + newServerName = newServerName.NormalizeLineEndSpacing(); + if (newServerName.Length > Limits.MaxMemberNameLength) + throw Errors.StringTooLongError("Server name", newServerName.Length, Limits.MaxMemberNameLength); + + await ctx.Repository.UpdateMemberGuild(target.Id, ctx.Guild.Id, + new MemberGuildPatch { DisplayName = newServerName }); + + await ctx.Reply( + $"{Emojis.Success} Member server name changed (using {newServerName.Length}/{Limits.MaxMemberNameLength} characters). This member will now be proxied using the name \"{newServerName}\" in this server ({ctx.Guild.Name})."); + } + + public async Task ShowKeepProxy(Context ctx, PKMember target) + { + string keepProxyStatusMessage = ""; + + if (target.KeepProxy) + keepProxyStatusMessage += "This member has keepproxy **enabled**. Proxy tags will be **included** in the resulting message when proxying."; + else + keepProxyStatusMessage += "This member has keepproxy **disabled**. Proxy tags will **not** be included in the resulting message when proxying."; + if (ctx.Guild != null) { - memberGuildConfig = await ctx.Repository.GetMemberGuild(ctx.Guild.Id, target.Id); + var memberGuildConfig = await ctx.Repository.GetMemberGuild(ctx.Guild.Id, target.Id); + + if (memberGuildConfig?.KeepProxy.HasValue == true) + { + if (memberGuildConfig.KeepProxy.Value) + keepProxyStatusMessage += $"\n{Emojis.Warn} This member has keepproxy **enabled in this server**, which means proxy tags will **always** be included when proxying in this server, regardless of the global keepproxy. To clear this setting in this server, type `{ctx.DefaultPrefix}m serverkeepproxy clear`."; + else + keepProxyStatusMessage += $"\n{Emojis.Warn} This member has keepproxy **disabled in this server**, which means proxy tags will **never** be included when proxying in this server, regardless of the global keepproxy. To clear this setting in this server, type `{ctx.DefaultPrefix}m serverkeepproxy clear`."; + } } - bool newValue; - if (ctx.Match("on", "enabled", "true", "yes")) - { - newValue = true; - } - else if (ctx.Match("off", "disabled", "false", "no")) - { - newValue = false; - } - else if (ctx.HasNext()) - { - throw new PKSyntaxError("You must pass either \"on\" or \"off\"."); - } - else - { - string keepProxyStatusMessage = ""; + await ctx.Reply(keepProxyStatusMessage); + } - if (target.KeepProxy) - keepProxyStatusMessage += "This member has keepproxy **enabled**. Proxy tags will be **included** in the resulting message when proxying."; - else - keepProxyStatusMessage += "This member has keepproxy **disabled**. Proxy tags will **not** be included in the resulting message when proxying."; - - if (memberGuildConfig != null && memberGuildConfig.KeepProxy.HasValue && memberGuildConfig.KeepProxy.Value) - keepProxyStatusMessage += $"\n{Emojis.Warn} This member has keepproxy **enabled in this server**, which means proxy tags will **always** be included when proxying in this server, regardless of the global keepproxy. To clear this setting in this server, type `{ctx.DefaultPrefix}m serverkeepproxy clear`."; - else if (memberGuildConfig != null && memberGuildConfig.KeepProxy.HasValue && !memberGuildConfig.KeepProxy.Value) - keepProxyStatusMessage += $"\n{Emojis.Warn} This member has keepproxy **disabled in this server**, which means proxy tags will **never** be included when proxying in this server, regardless of the global keepproxy. To clear this setting in this server, type `{ctx.DefaultPrefix}m serverkeepproxy clear`."; - - await ctx.Reply(keepProxyStatusMessage); - return; - } - - ; + public async Task ChangeKeepProxy(Context ctx, PKMember target, bool newValue) + { + ctx.CheckSystem().CheckOwnMember(target); var patch = new MemberPatch { KeepProxy = Partial.Present(newValue) }; await ctx.Repository.UpdateMember(target.Id, patch); @@ -639,74 +652,58 @@ public class MemberEdit else keepProxyUpdateMessage += $"{Emojis.Success} this member now has keepproxy **disabled**. Member proxy tags will **not** be included in the resulting message when proxying."; - if (memberGuildConfig != null && memberGuildConfig.KeepProxy.HasValue && memberGuildConfig.KeepProxy.Value) - keepProxyUpdateMessage += $"\n{Emojis.Warn} This member has keepproxy **enabled in this server**, which means proxy tags will **always** be included when proxying in this server, regardless of the global keepproxy. To clear this setting in this server, type `{ctx.DefaultPrefix}m serverkeepproxy clear`."; - else if (memberGuildConfig != null && memberGuildConfig.KeepProxy.HasValue && !memberGuildConfig.KeepProxy.Value) - keepProxyUpdateMessage += $"\n{Emojis.Warn} This member has keepproxy **disabled in this server**, which means proxy tags will **never** be included when proxying in this server, regardless of the global keepproxy. To clear this setting in this server, type `{ctx.DefaultPrefix}m serverkeepproxy clear`."; + if (ctx.Guild != null) + { + var memberGuildConfig = await ctx.Repository.GetMemberGuild(ctx.Guild.Id, target.Id); + + if (memberGuildConfig?.KeepProxy.HasValue == true) + { + if (memberGuildConfig.KeepProxy.Value) + keepProxyUpdateMessage += $"\n{Emojis.Warn} This member has keepproxy **enabled in this server**, which means proxy tags will **always** be included when proxying in this server, regardless of the global keepproxy. To clear this setting in this server, type `{ctx.DefaultPrefix}m serverkeepproxy clear`."; + else + keepProxyUpdateMessage += $"\n{Emojis.Warn} This member has keepproxy **disabled in this server**, which means proxy tags will **never** be included when proxying in this server, regardless of the global keepproxy. To clear this setting in this server, type `{ctx.DefaultPrefix}m serverkeepproxy clear`."; + } + } await ctx.Reply(keepProxyUpdateMessage); } - public async Task ServerKeepProxy(Context ctx, PKMember target) + public async Task ShowServerKeepProxy(Context ctx, PKMember target) + { + ctx.CheckGuildContext(); + + var memberGuildConfig = await ctx.Repository.GetMemberGuild(ctx.Guild.Id, target.Id); + + if (memberGuildConfig.KeepProxy.HasValue) + { + if (memberGuildConfig.KeepProxy.Value) + await ctx.Reply($"This member has keepproxy **enabled** in the current server, which means proxy tags will be **included** in the resulting message when proxying. To clear this setting in this server, type `{ctx.DefaultPrefix}m serverkeepproxy clear`."); + else + await ctx.Reply($"This member has keepproxy **disabled** in the current server, which means proxy tags will **not** be included in the resulting message when proxying. To clear this setting in this server, type `{ctx.DefaultPrefix}m serverkeepproxy clear`."); + } + else + { + var noServerKeepProxySetMessage = "This member does not have a server keepproxy override set."; + if (target.KeepProxy) + noServerKeepProxySetMessage += " The global keepproxy is **enabled**, which means proxy tags will be **included** when proxying."; + else + noServerKeepProxySetMessage += " The global keepproxy is **disabled**, which means proxy tags will **not** be included when proxying."; + + await ctx.Reply(noServerKeepProxySetMessage); + } + } + + public async Task ClearServerKeepProxy(Context ctx, PKMember target, bool confirmYes) { ctx.CheckGuildContext(); ctx.CheckSystem().CheckOwnMember(target); - var memberGuildConfig = await ctx.Repository.GetMemberGuild(ctx.Guild.Id, target.Id); + if (await ctx.ConfirmClear("this member's server keepproxy setting", confirmYes)) + { + var patch = new MemberGuildPatch { KeepProxy = Partial.Present(null) }; + await ctx.Repository.UpdateMemberGuild(target.Id, ctx.Guild.Id, patch); - bool? newValue; - if (ctx.Match("on", "enabled", "true", "yes")) - { - newValue = true; - } - else if (ctx.Match("off", "disabled", "false", "no")) - { - newValue = false; - } - else if (ctx.MatchClear()) - { - newValue = null; - } - else if (ctx.HasNext()) - { - throw new PKSyntaxError("You must pass either \"on\", \"off\" or \"clear\"."); - } - else - { - if (memberGuildConfig.KeepProxy.HasValue) - if (memberGuildConfig.KeepProxy.Value) - await ctx.Reply( - $"This member has keepproxy **enabled** in the current server, which means proxy tags will be **included** in the resulting message when proxying. To clear this setting in this server, type `{ctx.DefaultPrefix}m serverkeepproxy clear`."); - else - await ctx.Reply( - $"This member has keepproxy **disabled** in the current server, which means proxy tags will **not** be included in the resulting message when proxying. To clear this setting in this server, type `{ctx.DefaultPrefix}m serverkeepproxy clear`."); - else - { - var noServerKeepProxySetMessage = "This member does not have a server keepproxy override set."; - if (target.KeepProxy) - noServerKeepProxySetMessage += " The global keepproxy is **enabled**, which means proxy tags will be **included** when proxying."; - else - noServerKeepProxySetMessage += " The global keepproxy is **disabled**, which means proxy tags will **not** be included when proxying."; - - await ctx.Reply(noServerKeepProxySetMessage); - } - return; - } - - var patch = new MemberGuildPatch { KeepProxy = Partial.Present(newValue) }; - await ctx.Repository.UpdateMemberGuild(target.Id, ctx.Guild.Id, patch); - - if (newValue.HasValue) - if (newValue.Value) - await ctx.Reply( - $"{Emojis.Success} Member proxy tags will now be **included** in the resulting message when proxying **in the current server**. To clear this setting in this server, type `{ctx.DefaultPrefix}m serverkeepproxy clear`."); - else - await ctx.Reply( - $"{Emojis.Success} Member proxy tags will now **not** be included in the resulting message when proxying **in the current server**. To clear this setting in this server, type `{ctx.DefaultPrefix}m serverkeepproxy clear`."); - else - { var serverKeepProxyClearedMessage = $"{Emojis.Success} Cleared server keepproxy settings for this member."; - if (target.KeepProxy) serverKeepProxyClearedMessage += " Member proxy tags will now be **included** in the resulting message when proxying."; else @@ -716,35 +713,33 @@ public class MemberEdit } } - public async Task Tts(Context ctx, PKMember target) + public async Task ChangeServerKeepProxy(Context ctx, PKMember target, bool newValue) { + ctx.CheckGuildContext(); ctx.CheckSystem().CheckOwnMember(target); - bool newValue; - if (ctx.Match("on", "enabled", "true", "yes")) - { - newValue = true; - } - else if (ctx.Match("off", "disabled", "false", "no")) - { - newValue = false; - } - else if (ctx.HasNext()) - { - throw new PKSyntaxError("You must pass either \"on\" or \"off\"."); - } - else - { - if (target.Tts) - await ctx.Reply( - "This member has text-to-speech **enabled**, which means their messages **will be** sent as text-to-speech messages."); - else - await ctx.Reply( - "This member has text-to-speech **disabled**, which means their messages **will not** be sent as text-to-speech messages."); - return; - } + var patch = new MemberGuildPatch { KeepProxy = Partial.Present(newValue) }; + await ctx.Repository.UpdateMemberGuild(target.Id, ctx.Guild.Id, patch); - ; + if (newValue) + await ctx.Reply($"{Emojis.Success} Member proxy tags will now be **included** in the resulting message when proxying **in the current server**. To clear this setting in this server, type `{ctx.DefaultPrefix}m serverkeepproxy clear`."); + else + await ctx.Reply($"{Emojis.Success} Member proxy tags will now **not** be included in the resulting message when proxying **in the current server**. To clear this setting in this server, type `{ctx.DefaultPrefix}m serverkeepproxy clear`."); + } + + public async Task ShowTts(Context ctx, PKMember target) + { + if (target.Tts) + await ctx.Reply( + "This member has text-to-speech **enabled**, which means their messages **will be** sent as text-to-speech messages."); + else + await ctx.Reply( + "This member has text-to-speech **disabled**, which means their messages **will not** be sent as text-to-speech messages."); + } + + public async Task ChangeTts(Context ctx, PKMember target, bool newValue) + { + ctx.CheckSystem().CheckOwnMember(target); var patch = new MemberPatch { Tts = Partial.Present(newValue) }; await ctx.Repository.UpdateMember(target.Id, patch); @@ -757,23 +752,19 @@ public class MemberEdit $"{Emojis.Success} Member messages will no longer be sent as text-to-speech messages."); } - public async Task MemberAutoproxy(Context ctx, PKMember target) + public async Task ShowAutoproxy(Context ctx, PKMember target) { - if (ctx.System == null) throw Errors.NoSystemError(ctx.DefaultPrefix); - if (target.System != ctx.System.Id) throw Errors.NotOwnMemberError; + if (target.AllowAutoproxy) + await ctx.Reply( + "Latch/front autoproxy are **enabled** for this member. This member will be automatically proxied when autoproxy is set to latch or front mode."); + else + await ctx.Reply( + "Latch/front autoproxy are **disabled** for this member. This member will not be automatically proxied when autoproxy is set to latch or front mode."); + } - if (!ctx.HasNext()) - { - if (target.AllowAutoproxy) - await ctx.Reply( - "Latch/front autoproxy are **enabled** for this member. This member will be automatically proxied when autoproxy is set to latch or front mode."); - else - await ctx.Reply( - "Latch/front autoproxy are **disabled** for this member. This member will not be automatically proxied when autoproxy is set to latch or front mode."); - return; - } - - var newValue = ctx.MatchToggle(); + public async Task ChangeAutoproxy(Context ctx, PKMember target, bool newValue) + { + ctx.CheckSystem().CheckOwnMember(target); var patch = new MemberPatch { AllowAutoproxy = Partial.Present(newValue) }; await ctx.Repository.UpdateMember(target.Id, patch); @@ -784,128 +775,118 @@ public class MemberEdit await ctx.Reply($"{Emojis.Success} Latch / front autoproxy have been **disabled** for this member."); } - public async Task Privacy(Context ctx, PKMember target, PrivacyLevel? newValueFromCommand) + public async Task ShowPrivacy(Context ctx, PKMember target) + { + await ctx.Reply(embed: new EmbedBuilder() + .Title($"Current privacy settings for {target.NameFor(ctx)}") + .Field(new Embed.Field("Name (replaces name with display name if member has one)", + target.NamePrivacy.Explanation())) + .Field(new Embed.Field("Description", target.DescriptionPrivacy.Explanation())) + .Field(new Embed.Field("Banner", target.BannerPrivacy.Explanation())) + .Field(new Embed.Field("Avatar", target.AvatarPrivacy.Explanation())) + .Field(new Embed.Field("Birthday", target.BirthdayPrivacy.Explanation())) + .Field(new Embed.Field("Pronouns", target.PronounPrivacy.Explanation())) + .Field(new Embed.Field("Proxy Tags", target.ProxyPrivacy.Explanation())) + .Field(new Embed.Field("Meta (creation date, message count, last front, last message)", + target.MetadataPrivacy.Explanation())) + .Field(new Embed.Field("Visibility", target.MemberVisibility.Explanation())) + .Description( + $"To edit privacy settings, use the command:\n`{ctx.DefaultPrefix}member privacy `\n\n- `subject` is one of `name`, `description`, `banner`, `avatar`, `birthday`, `pronouns`, `proxies`, `metadata`, `visibility`, or `all`\n- `level` is either `public` or `private`.") + .Build()); + } + + public async Task ChangeAllPrivacy(Context ctx, PKMember target, PrivacyLevel level) { ctx.CheckSystem().CheckOwnMember(target); - // Display privacy settings - if (!ctx.HasNext() && newValueFromCommand == null) - { - await ctx.Reply(embed: new EmbedBuilder() - .Title($"Current privacy settings for {target.NameFor(ctx)}") - .Field(new Embed.Field("Name (replaces name with display name if member has one)", - target.NamePrivacy.Explanation())) - .Field(new Embed.Field("Description", target.DescriptionPrivacy.Explanation())) - .Field(new Embed.Field("Banner", target.BannerPrivacy.Explanation())) - .Field(new Embed.Field("Avatar", target.AvatarPrivacy.Explanation())) - .Field(new Embed.Field("Birthday", target.BirthdayPrivacy.Explanation())) - .Field(new Embed.Field("Pronouns", target.PronounPrivacy.Explanation())) - .Field(new Embed.Field("Proxy Tags", target.ProxyPrivacy.Explanation())) - .Field(new Embed.Field("Meta (creation date, message count, last front, last message)", - target.MetadataPrivacy.Explanation())) - .Field(new Embed.Field("Visibility", target.MemberVisibility.Explanation())) - .Description( - $"To edit privacy settings, use the command:\n`{ctx.DefaultPrefix}member privacy `\n\n- `subject` is one of `name`, `description`, `banner`, `avatar`, `birthday`, `pronouns`, `proxies`, `metadata`, `visibility`, or `all`\n- `level` is either `public` or `private`.") - .Build()); - return; - } + await ctx.Repository.UpdateMember(target.Id, new MemberPatch().WithAllPrivacy(level)); - // Get guild settings (mostly for warnings and such) - MemberGuildSettings guildSettings = null; - if (ctx.Guild != null) - guildSettings = await ctx.Repository.GetMemberGuild(ctx.Guild.Id, target.Id); - - async Task SetAll(PrivacyLevel level) - { - await ctx.Repository.UpdateMember(target.Id, new MemberPatch().WithAllPrivacy(level)); - - if (level == PrivacyLevel.Private) - await ctx.Reply( - $"{Emojis.Success} All {target.NameFor(ctx)}'s privacy settings have been set to **{level.LevelName()}**. Other accounts will now see nothing on the member card."); - else - await ctx.Reply( - $"{Emojis.Success} All {target.NameFor(ctx)}'s privacy settings have been set to **{level.LevelName()}**. Other accounts will now see everything on the member card."); - } - - async Task SetLevel(MemberPrivacySubject subject, PrivacyLevel level) - { - await ctx.Repository.UpdateMember(target.Id, new MemberPatch().WithPrivacy(subject, level)); - - var subjectName = subject switch - { - MemberPrivacySubject.Name => "name privacy", - MemberPrivacySubject.Description => "description privacy", - MemberPrivacySubject.Banner => "banner privacy", - MemberPrivacySubject.Avatar => "avatar privacy", - MemberPrivacySubject.Pronouns => "pronoun privacy", - MemberPrivacySubject.Birthday => "birthday privacy", - MemberPrivacySubject.Proxy => "proxy tag privacy", - MemberPrivacySubject.Metadata => "metadata privacy", - MemberPrivacySubject.Visibility => "visibility", - _ => throw new ArgumentOutOfRangeException($"Unknown privacy subject {subject}") - }; - - var explanation = (subject, level) switch - { - (MemberPrivacySubject.Name, PrivacyLevel.Private) => - "This member's name is now hidden from other systems, and will be replaced by the member's display name.", - (MemberPrivacySubject.Description, PrivacyLevel.Private) => - "This member's description is now hidden from other systems.", - (MemberPrivacySubject.Banner, PrivacyLevel.Private) => - "This member's banner is now hidden from other systems.", - (MemberPrivacySubject.Avatar, PrivacyLevel.Private) => - "This member's avatar is now hidden from other systems.", - (MemberPrivacySubject.Birthday, PrivacyLevel.Private) => - "This member's birthday is now hidden from other systems.", - (MemberPrivacySubject.Pronouns, PrivacyLevel.Private) => - "This member's pronouns are now hidden from other systems.", - (MemberPrivacySubject.Proxy, PrivacyLevel.Private) => - "This member's proxy tags are now hidden from other systems.", - (MemberPrivacySubject.Metadata, PrivacyLevel.Private) => - "This member's metadata (eg. created timestamp, message count, etc) is now hidden from other systems.", - (MemberPrivacySubject.Visibility, PrivacyLevel.Private) => - "This member is now hidden from member lists.", - - (MemberPrivacySubject.Name, PrivacyLevel.Public) => - "This member's name is no longer hidden from other systems.", - (MemberPrivacySubject.Description, PrivacyLevel.Public) => - "This member's description is no longer hidden from other systems.", - (MemberPrivacySubject.Banner, PrivacyLevel.Public) => - "This member's banner is no longer hidden from other systems.", - (MemberPrivacySubject.Avatar, PrivacyLevel.Public) => - "This member's avatar is no longer hidden from other systems.", - (MemberPrivacySubject.Birthday, PrivacyLevel.Public) => - "This member's birthday is no longer hidden from other systems.", - (MemberPrivacySubject.Pronouns, PrivacyLevel.Public) => - "This member's pronouns are no longer hidden from other systems.", - (MemberPrivacySubject.Proxy, PrivacyLevel.Public) => - "This member's proxy tags are no longer hidden from other systems.", - (MemberPrivacySubject.Metadata, PrivacyLevel.Public) => - "This member's metadata (eg. created timestamp, message count, etc) is no longer hidden from other systems.", - (MemberPrivacySubject.Visibility, PrivacyLevel.Public) => - "This member is no longer hidden from member lists.", - - _ => throw new InvalidOperationException($"Invalid subject/level tuple ({subject}, {level})") - }; - - var replyStr = $"{Emojis.Success} {target.NameFor(ctx)}'s **{subjectName}** has been set to **{level.LevelName()}**. {explanation}"; - - // Name privacy only works given a display name - if (subject == MemberPrivacySubject.Name && level == PrivacyLevel.Private && target.DisplayName == null) - replyStr += $"\n{Emojis.Warn} This member does not have a display name set, and name privacy **will not take effect**."; - - // Avatar privacy doesn't apply when proxying if no server avatar is set - if (subject == MemberPrivacySubject.Avatar && level == PrivacyLevel.Private && - guildSettings?.AvatarUrl == null) - replyStr += $"\n{Emojis.Warn} This member does not have a server avatar set, so *proxying* will **still show the member avatar**. If you want to hide your avatar when proxying here, set a server avatar: `{ctx.DefaultPrefix}member {target.Reference(ctx)} serveravatar`"; - - await ctx.Reply(replyStr); - } - - if (ctx.Match("all") || newValueFromCommand != null) - await SetAll(newValueFromCommand ?? ctx.PopPrivacyLevel()); + if (level == PrivacyLevel.Private) + await ctx.Reply( + $"{Emojis.Success} All {target.NameFor(ctx)}'s privacy settings have been set to **{level.LevelName()}**. Other accounts will now see nothing on the member card."); else - await SetLevel(ctx.PopMemberPrivacySubject(), ctx.PopPrivacyLevel()); + await ctx.Reply( + $"{Emojis.Success} All {target.NameFor(ctx)}'s privacy settings have been set to **{level.LevelName()}**. Other accounts will now see everything on the member card."); + } + + public async Task ChangePrivacy(Context ctx, PKMember target, MemberPrivacySubject subject, PrivacyLevel level) + { + ctx.CheckSystem().CheckOwnMember(target); + + await ctx.Repository.UpdateMember(target.Id, new MemberPatch().WithPrivacy(subject, level)); + + var subjectName = subject switch + { + MemberPrivacySubject.Name => "name privacy", + MemberPrivacySubject.Description => "description privacy", + MemberPrivacySubject.Banner => "banner privacy", + MemberPrivacySubject.Avatar => "avatar privacy", + MemberPrivacySubject.Pronouns => "pronoun privacy", + MemberPrivacySubject.Birthday => "birthday privacy", + MemberPrivacySubject.Proxy => "proxy tag privacy", + MemberPrivacySubject.Metadata => "metadata privacy", + MemberPrivacySubject.Visibility => "visibility", + _ => throw new ArgumentOutOfRangeException($"Unknown privacy subject {subject}") + }; + + var explanation = (subject, level) switch + { + (MemberPrivacySubject.Name, PrivacyLevel.Private) => + "This member's name is now hidden from other systems, and will be replaced by the member's display name.", + (MemberPrivacySubject.Description, PrivacyLevel.Private) => + "This member's description is now hidden from other systems.", + (MemberPrivacySubject.Banner, PrivacyLevel.Private) => + "This member's banner is now hidden from other systems.", + (MemberPrivacySubject.Avatar, PrivacyLevel.Private) => + "This member's avatar is now hidden from other systems.", + (MemberPrivacySubject.Birthday, PrivacyLevel.Private) => + "This member's birthday is now hidden from other systems.", + (MemberPrivacySubject.Pronouns, PrivacyLevel.Private) => + "This member's pronouns are now hidden from other systems.", + (MemberPrivacySubject.Proxy, PrivacyLevel.Private) => + "This member's proxy tags are now hidden from other systems.", + (MemberPrivacySubject.Metadata, PrivacyLevel.Private) => + "This member's metadata (eg. created timestamp, message count, etc) is now hidden from other systems.", + (MemberPrivacySubject.Visibility, PrivacyLevel.Private) => + "This member is now hidden from member lists.", + + (MemberPrivacySubject.Name, PrivacyLevel.Public) => + "This member's name is no longer hidden from other systems.", + (MemberPrivacySubject.Description, PrivacyLevel.Public) => + "This member's description is no longer hidden from other systems.", + (MemberPrivacySubject.Banner, PrivacyLevel.Public) => + "This member's banner is no longer hidden from other systems.", + (MemberPrivacySubject.Avatar, PrivacyLevel.Public) => + "This member's avatar is no longer hidden from other systems.", + (MemberPrivacySubject.Birthday, PrivacyLevel.Public) => + "This member's birthday is no longer hidden from other systems.", + (MemberPrivacySubject.Pronouns, PrivacyLevel.Public) => + "This member's pronouns are no longer hidden from other systems.", + (MemberPrivacySubject.Proxy, PrivacyLevel.Public) => + "This member's proxy tags are no longer hidden from other systems.", + (MemberPrivacySubject.Metadata, PrivacyLevel.Public) => + "This member's metadata (eg. created timestamp, message count, etc) is no longer hidden from other systems.", + (MemberPrivacySubject.Visibility, PrivacyLevel.Public) => + "This member is no longer hidden from member lists.", + + _ => throw new InvalidOperationException($"Invalid subject/level tuple ({subject}, {level})") + }; + + var replyStr = $"{Emojis.Success} {target.NameFor(ctx)}'s **{subjectName}** has been set to **{level.LevelName()}**. {explanation}"; + + // Name privacy only works given a display name + if (subject == MemberPrivacySubject.Name && level == PrivacyLevel.Private && target.DisplayName == null) + replyStr += $"\n{Emojis.Warn} This member does not have a display name set, and name privacy **will not take effect**."; + + // Avatar privacy doesn't apply when proxying if no server avatar is set + if (subject == MemberPrivacySubject.Avatar && level == PrivacyLevel.Private) + { + var guildSettings = ctx.Guild != null ? await ctx.Repository.GetMemberGuild(ctx.Guild.Id, target.Id) : null; + if (guildSettings?.AvatarUrl == null) + replyStr += $"\n{Emojis.Warn} This member does not have a server avatar set, so *proxying* will **still show the member avatar**. If you want to hide your avatar when proxying here, set a server avatar: `{ctx.DefaultPrefix}member {target.Reference(ctx)} serveravatar`"; + } + + await ctx.Reply(replyStr); } public async Task Delete(Context ctx, PKMember target) diff --git a/crates/command_definitions/src/member.rs b/crates/command_definitions/src/member.rs index 504995d4..4268499e 100644 --- a/crates/command_definitions/src/member.rs +++ b/crates/command_definitions/src/member.rs @@ -2,31 +2,203 @@ use super::*; pub fn cmds() -> impl Iterator { let member = ("member", ["m"]); + let member_target = tokens!(member, MemberRef); + + let name = ("name", ["n"]); let description = ("description", ["desc"]); + let pronouns = ("pronouns", ["pronoun", "prns", "pn"]); let privacy = ("privacy", ["priv"]); let new = ("new", ["n"]); + let banner = ("banner", ["bn"]); + let color = ("color", ["colour"]); + let birthday = ("birthday", ["bday", "bd"]); + let display_name = ("displayname", ["dname", "dn"]); + let server_name = ("servername", ["sname", "sn"]); + let keep_proxy = ("keepproxy", ["kp"]); + let server_keep_proxy = ("serverkeepproxy", ["skp"]); + let autoproxy = ("autoproxy", ["ap"]); + let tts = ("tts", ["texttospeech"]); + let delete = ("delete", ["del", "remove"]); - let member_target = tokens!(member, MemberRef); - let member_desc = tokens!(member_target, description); - let member_privacy = tokens!(member_target, privacy); - - [ + // Group commands by functionality + let member_new_cmd = [ command!(member, new, ("name", OpaqueString) => "member_new") .help("Creates a new system member"), + ].into_iter(); + + let member_info_cmd = [ command!(member_target => "member_show") .flag("pt") .help("Shows information about a member"), - command!(member_desc => "member_desc_show").help("Shows a member's description"), - command!(member_desc, ("description", OpaqueStringRemainder) => "member_desc_update") - .help("Changes a member's description"), - command!(member_privacy => "member_privacy_show") - .help("Displays a member's current privacy settings"), - command!( - member_privacy, MemberPrivacyTarget, ("new_privacy_level", PrivacyLevel) - => "member_privacy_update" - ) - .help("Changes a member's privacy settings"), + ].into_iter(); + + let member_name_cmd = { + let member_name = tokens!(member_target, name); + [ + command!(member_name => "member_name_show").help("Shows a member's name"), + command!(member_name, ("name", OpaqueStringRemainder) => "member_name_update") + .help("Changes a member's name"), + ].into_iter() + }; + + let member_description_cmd = { + let member_desc = tokens!(member_target, description); + [ + command!(member_desc => "member_desc_show").help("Shows a member's description"), + command!(member_desc, ("clear", ["c"]) => "member_desc_clear") + .flag(("yes", ["y"])) + .help("Clears a member's description"), + command!(member_desc, ("description", OpaqueStringRemainder) => "member_desc_update") + .help("Changes a member's description"), + ].into_iter() + }; + + let member_privacy_cmd = { + let member_privacy = tokens!(member_target, privacy); + [ + command!(member_privacy => "member_privacy_show") + .help("Displays a member's current privacy settings"), + command!( + member_privacy, MemberPrivacyTarget, ("new_privacy_level", PrivacyLevel) + => "member_privacy_update" + ) + .help("Changes a member's privacy settings"), + ].into_iter() + }; + + let member_pronouns_cmd = { + let member_pronouns = tokens!(member_target, pronouns); + [ + command!(member_pronouns => "member_pronouns_show") + .help("Shows a member's pronouns"), + command!(member_pronouns, ("pronouns", OpaqueStringRemainder) => "member_pronouns_update") + .help("Changes a member's pronouns"), + command!(member_pronouns, ("clear", ["c"]) => "member_pronouns_clear") + .flag(("yes", ["y"])) + .help("Clears a member's pronouns"), + ].into_iter() + }; + + let member_banner_cmd = { + let member_banner = tokens!(member_target, banner); + [ + command!(member_banner => "member_banner_show") + .help("Shows a member's banner image"), + command!(member_banner, ("banner", Avatar) => "member_banner_update") + .help("Changes a member's banner image"), + command!(member_banner, ("clear", ["c"]) => "member_banner_clear") + .flag(("yes", ["y"])) + .help("Clears a member's banner image"), + ].into_iter() + }; + + let member_color_cmd = { + let member_color = tokens!(member_target, color); + [ + command!(member_color => "member_color_show") + .help("Shows a member's color"), + command!(member_color, ("color", OpaqueString) => "member_color_update") + .help("Changes a member's color"), + command!(member_color, ("clear", ["c"]) => "member_color_clear") + .flag(("yes", ["y"])) + .help("Clears a member's color"), + ].into_iter() + }; + + let member_birthday_cmd = { + let member_birthday = tokens!(member_target, birthday); + [ + command!(member_birthday => "member_birthday_show") + .help("Shows a member's birthday"), + command!(member_birthday, ("birthday", OpaqueString) => "member_birthday_update") + .help("Changes a member's birthday"), + command!(member_birthday, ("clear", ["c"]) => "member_birthday_clear") + .flag(("yes", ["y"])) + .help("Clears a member's birthday"), + ].into_iter() + }; + + let member_display_name_cmd = { + let member_display_name = tokens!(member_target, display_name); + [ + command!(member_display_name => "member_displayname_show") + .help("Shows a member's display name"), + command!(member_display_name, ("name", OpaqueStringRemainder) => "member_displayname_update") + .help("Changes a member's display name"), + command!(member_display_name, ("clear", ["c"]) => "member_displayname_clear") + .flag(("yes", ["y"])) + .help("Clears a member's display name"), + ].into_iter() + }; + + let member_server_name_cmd = { + let member_server_name = tokens!(member_target, server_name); + [ + command!(member_server_name => "member_servername_show") + .help("Shows a member's server name"), + command!(member_server_name, ("name", OpaqueStringRemainder) => "member_servername_update") + .help("Changes a member's server name"), + command!(member_server_name, ("clear", ["c"]) => "member_servername_clear") + .flag(("yes", ["y"])) + .help("Clears a member's server name"), + ].into_iter() + }; + + let member_proxy_settings_cmd = { + let member_keep_proxy = tokens!(member_target, keep_proxy); + let member_server_keep_proxy = tokens!(member_target, server_keep_proxy); + [ + command!(member_keep_proxy => "member_keepproxy_show") + .help("Shows a member's keep-proxy setting"), + command!(member_keep_proxy, ("value", Toggle) => "member_keepproxy_update") + .help("Changes a member's keep-proxy setting"), + command!(member_server_keep_proxy => "member_server_keepproxy_show") + .help("Shows a member's server-specific keep-proxy setting"), + command!(member_server_keep_proxy, ("value", Toggle) => "member_server_keepproxy_update") + .help("Changes a member's server-specific keep-proxy setting"), + command!(member_server_keep_proxy, ("clear", ["c"]) => "member_server_keepproxy_clear") + .flag(("yes", ["y"])) + .help("Clears a member's server-specific keep-proxy setting"), + ].into_iter() + }; + + let member_message_settings_cmd = { + let member_tts = tokens!(member_target, tts); + let member_autoproxy = tokens!(member_target, autoproxy); + [ + command!(member_tts => "member_tts_show") + .help("Shows whether a member's messages are sent as TTS"), + command!(member_tts, ("value", Toggle) => "member_tts_update") + .help("Changes whether a member's messages are sent as TTS"), + command!(member_autoproxy => "member_autoproxy_show") + .help("Shows whether a member can be autoproxied"), + command!(member_autoproxy, ("value", Toggle) => "member_autoproxy_update") + .help("Changes whether a member can be autoproxied"), + ].into_iter() + }; + + let member_delete_cmd = [ + command!(member_target, delete => "member_delete") + .help("Deletes a member"), + ].into_iter(); + + let member_easter_eggs = [ command!(member_target, "soulscream" => "member_soulscream").show_in_suggestions(false), - ] - .into_iter() + ].into_iter(); + + member_new_cmd + .chain(member_info_cmd) + .chain(member_name_cmd) + .chain(member_description_cmd) + .chain(member_privacy_cmd) + .chain(member_pronouns_cmd) + .chain(member_banner_cmd) + .chain(member_color_cmd) + .chain(member_birthday_cmd) + .chain(member_display_name_cmd) + .chain(member_server_name_cmd) + .chain(member_proxy_settings_cmd) + .chain(member_message_settings_cmd) + .chain(member_delete_cmd) + .chain(member_easter_eggs) } From 1196d87fe796d0f84c2cf61b0bd6d1763bce7993 Mon Sep 17 00:00:00 2001 From: dusk Date: Thu, 4 Sep 2025 01:22:34 +0300 Subject: [PATCH 089/179] feat: implement member proxy commands --- PluralKit.Bot/CommandMeta/CommandTree.cs | 9 +- PluralKit.Bot/Commands/MemberProxy.cs | 225 +++++++++++------------ crates/command_definitions/src/member.rs | 74 +++++--- 3 files changed, 159 insertions(+), 149 deletions(-) diff --git a/PluralKit.Bot/CommandMeta/CommandTree.cs b/PluralKit.Bot/CommandMeta/CommandTree.cs index e550a81e..a90ad7f5 100644 --- a/PluralKit.Bot/CommandMeta/CommandTree.cs +++ b/PluralKit.Bot/CommandMeta/CommandTree.cs @@ -44,6 +44,11 @@ public partial class CommandTree Commands.MemberServerKeepproxyShow(var param, _) => ctx.Execute(MemberServerKeepProxy, m => m.ShowServerKeepProxy(ctx, param.target)), Commands.MemberServerKeepproxyUpdate(var param, _) => ctx.Execute(MemberServerKeepProxy, m => m.ChangeServerKeepProxy(ctx, param.target, param.value)), Commands.MemberServerKeepproxyClear(var param, var flags) => ctx.Execute(MemberServerKeepProxy, m => m.ClearServerKeepProxy(ctx, param.target, flags.yes)), + Commands.MemberProxyShow(var param, _) => ctx.Execute(MemberProxy, m => m.ShowProxy(ctx, param.target)), + Commands.MemberProxyClear(var param, var flags) => ctx.Execute(MemberProxy, m => m.ClearProxy(ctx, param.target)), + Commands.MemberProxyAdd(var param, _) => ctx.Execute(MemberProxy, m => m.AddProxy(ctx, param.target, param.tag)), + Commands.MemberProxyRemove(var param, _) => ctx.Execute(MemberProxy, m => m.RemoveProxy(ctx, param.target, param.tag)), + Commands.MemberProxySet(var param, _) => ctx.Execute(MemberProxy, m => m.SetProxy(ctx, param.target, param.tags)), Commands.MemberTtsShow(var param, _) => ctx.Execute(MemberTts, m => m.ShowTts(ctx, param.target)), Commands.MemberTtsUpdate(var param, _) => ctx.Execute(MemberTts, m => m.ChangeTts(ctx, param.target, param.value)), Commands.MemberAutoproxyShow(var param, _) => ctx.Execute(MemberAutoproxy, m => m.ShowAutoproxy(ctx, param.target)), @@ -430,9 +435,7 @@ public partial class CommandTree private async Task HandleMemberCommandTargeted(Context ctx, PKMember target) { // Commands that have a member target (eg. pk;member delete) - if (ctx.Match("proxy", "tags", "proxytags", "brackets")) - await ctx.Execute(MemberProxy, m => m.Proxy(ctx, target)); - else if (ctx.Match("avatar", "profile", "picture", "icon", "image", "pfp", "pic")) + if (ctx.Match("avatar", "profile", "picture", "icon", "image", "pfp", "pic")) await ctx.Execute(MemberAvatar, m => m.Avatar(ctx, target)); else if (ctx.Match("proxyavatar", "proxypfp", "webhookavatar", "webhookpfp", "pa", "pavatar", "ppfp")) await ctx.Execute(MemberAvatar, m => m.WebhookAvatar(ctx, target)); diff --git a/PluralKit.Bot/Commands/MemberProxy.cs b/PluralKit.Bot/Commands/MemberProxy.cs index e1bcb19b..d2b128d8 100644 --- a/PluralKit.Bot/Commands/MemberProxy.cs +++ b/PluralKit.Bot/Commands/MemberProxy.cs @@ -6,133 +6,120 @@ namespace PluralKit.Bot; public class MemberProxy { - public async Task Proxy(Context ctx, PKMember target) + public async Task ShowProxy(Context ctx, PKMember target) + { + if (target.ProxyTags.Count == 0) + await ctx.Reply("This member does not have any proxy tags."); + else + await ctx.Reply($"This member's proxy tags are:\n{target.ProxyTagsString("\n")}"); + } + + public async Task ClearProxy(Context ctx, PKMember target) { ctx.CheckSystem().CheckOwnMember(target); - ProxyTag ParseProxyTags(string exampleProxy) + // If we already have multiple tags, this would clear everything, so prompt that + if (target.ProxyTags.Count > 1) { - // // Make sure there's one and only one instance of "text" in the example proxy given - var prefixAndSuffix = exampleProxy.Split("text"); - if (prefixAndSuffix.Length == 1) prefixAndSuffix = prefixAndSuffix[0].Split("TEXT"); - if (prefixAndSuffix.Length < 2) throw Errors.ProxyMustHaveText; - if (prefixAndSuffix.Length > 2) throw Errors.ProxyMultipleText; - return new ProxyTag(prefixAndSuffix[0], prefixAndSuffix[1]); - } - - async Task WarnOnConflict(ProxyTag newTag) - { - var query = "select * from (select *, (unnest(proxy_tags)).prefix as prefix, (unnest(proxy_tags)).suffix as suffix from members where system = @System) as _ where prefix is not distinct from @Prefix and suffix is not distinct from @Suffix and id != @Existing"; - var conflicts = (await ctx.Database.Execute(conn => conn.QueryAsync(query, - new { newTag.Prefix, newTag.Suffix, Existing = target.Id, system = target.System }))).ToList(); - - if (conflicts.Count <= 0) return true; - - var conflictList = conflicts.Select(m => $"- **{m.NameFor(ctx)}**"); - var msg = $"{Emojis.Warn} The following members have conflicting proxy tags:\n{string.Join('\n', conflictList)}\nDo you want to proceed anyway?"; - return await ctx.PromptYesNo(msg, "Proceed"); - } - - // "Sub"command: clear flag - if (ctx.MatchClear()) - { - // If we already have multiple tags, this would clear everything, so prompt that - if (target.ProxyTags.Count > 1) - { - var msg = $"{Emojis.Warn} You already have multiple proxy tags set: {target.ProxyTagsString()}\nDo you want to clear them all?"; - if (!await ctx.PromptYesNo(msg, "Clear")) - throw Errors.GenericCancelled(); - } - - var patch = new MemberPatch { ProxyTags = Partial.Present(new ProxyTag[0]) }; - await ctx.Repository.UpdateMember(target.Id, patch); - - await ctx.Reply($"{Emojis.Success} Proxy tags cleared."); - } - // "Sub"command: no arguments; will print proxy tags - else if (!ctx.HasNext(false)) - { - if (target.ProxyTags.Count == 0) - await ctx.Reply("This member does not have any proxy tags."); - else - await ctx.Reply($"This member's proxy tags are:\n{target.ProxyTagsString("\n")}"); - } - // Subcommand: "add" - else if (ctx.Match("add", "append")) - { - if (!ctx.HasNext(false)) - throw new PKSyntaxError("You must pass an example proxy to add (eg. `[text]` or `J:text`)."); - - var tagToAdd = ParseProxyTags(ctx.RemainderOrNull(false).NormalizeLineEndSpacing()); - if (tagToAdd.IsEmpty) throw Errors.EmptyProxyTags(target, ctx); - if (target.ProxyTags.Contains(tagToAdd)) - throw Errors.ProxyTagAlreadyExists(tagToAdd, target); - if (tagToAdd.ProxyString.Length > Limits.MaxProxyTagLength) - throw new PKError( - $"Proxy tag too long ({tagToAdd.ProxyString.Length} > {Limits.MaxProxyTagLength} characters)."); - - if (!await WarnOnConflict(tagToAdd)) + var msg = $"{Emojis.Warn} You already have multiple proxy tags set: {target.ProxyTagsString()}\nDo you want to clear them all?"; + if (!await ctx.PromptYesNo(msg, "Clear")) throw Errors.GenericCancelled(); - - var newTags = target.ProxyTags.ToList(); - newTags.Add(tagToAdd); - var patch = new MemberPatch { ProxyTags = Partial.Present(newTags.ToArray()) }; - await ctx.Repository.UpdateMember(target.Id, patch); - - await ctx.Reply($"{Emojis.Success} Added proxy tags {tagToAdd.ProxyString.AsCode()} (using {tagToAdd.ProxyString.Length}/{Limits.MaxProxyTagLength} characters)."); } - // Subcommand: "remove" - else if (ctx.Match("remove", "delete")) + + var patch = new MemberPatch { ProxyTags = Partial.Present(new ProxyTag[0]) }; + await ctx.Repository.UpdateMember(target.Id, patch); + + await ctx.Reply($"{Emojis.Success} Proxy tags cleared."); + } + + public async Task AddProxy(Context ctx, PKMember target, string proxyString) + { + ctx.CheckSystem().CheckOwnMember(target); + + var tagToAdd = ParseProxyTag(proxyString); + if (tagToAdd.IsEmpty) throw Errors.EmptyProxyTags(target, ctx); + if (target.ProxyTags.Contains(tagToAdd)) + throw Errors.ProxyTagAlreadyExists(tagToAdd, target); + if (tagToAdd.ProxyString.Length > Limits.MaxProxyTagLength) + throw new PKError( + $"Proxy tag too long ({tagToAdd.ProxyString.Length} > {Limits.MaxProxyTagLength} characters)."); + + if (!await WarnOnConflict(ctx, target, tagToAdd)) + throw Errors.GenericCancelled(); + + var newTags = target.ProxyTags.ToList(); + newTags.Add(tagToAdd); + var patch = new MemberPatch { ProxyTags = Partial.Present(newTags.ToArray()) }; + await ctx.Repository.UpdateMember(target.Id, patch); + + await ctx.Reply($"{Emojis.Success} Added proxy tags {tagToAdd.ProxyString.AsCode()} (using {tagToAdd.ProxyString.Length}/{Limits.MaxProxyTagLength} characters)."); + } + + public async Task RemoveProxy(Context ctx, PKMember target, string proxyString) + { + ctx.CheckSystem().CheckOwnMember(target); + + var tagToRemove = ParseProxyTag(proxyString); + if (tagToRemove.IsEmpty) throw Errors.EmptyProxyTags(target, ctx); + if (!target.ProxyTags.Contains(tagToRemove)) + throw Errors.ProxyTagDoesNotExist(tagToRemove, target); + + var newTags = target.ProxyTags.ToList(); + newTags.Remove(tagToRemove); + var patch = new MemberPatch { ProxyTags = Partial.Present(newTags.ToArray()) }; + await ctx.Repository.UpdateMember(target.Id, patch); + + await ctx.Reply($"{Emojis.Success} Removed proxy tags {tagToRemove.ProxyString.AsCode()}."); + } + + public async Task SetProxy(Context ctx, PKMember target, string proxyString) + { + ctx.CheckSystem().CheckOwnMember(target); + + var requestedTag = ParseProxyTag(proxyString); + if (requestedTag.IsEmpty) throw Errors.EmptyProxyTags(target, ctx); + + if (target.ProxyTags.Count > 1) { - if (!ctx.HasNext(false)) - throw new PKSyntaxError("You must pass a proxy tag to remove (eg. `[text]` or `J:text`)."); - - var remainder = ctx.RemainderOrNull(false); - var tagToRemove = ParseProxyTags(remainder.NormalizeLineEndSpacing()); - if (tagToRemove.IsEmpty) throw Errors.EmptyProxyTags(target, ctx); - if (!target.ProxyTags.Contains(tagToRemove)) - { - // Legacy support for when line endings weren't normalized - tagToRemove = ParseProxyTags(remainder); - if (!target.ProxyTags.Contains(tagToRemove)) - throw Errors.ProxyTagDoesNotExist(tagToRemove, target); - } - - - var newTags = target.ProxyTags.ToList(); - newTags.Remove(tagToRemove); - var patch = new MemberPatch { ProxyTags = Partial.Present(newTags.ToArray()) }; - await ctx.Repository.UpdateMember(target.Id, patch); - - await ctx.Reply($"{Emojis.Success} Removed proxy tags {tagToRemove.ProxyString.AsCode()}."); - } - // Subcommand: bare proxy tag given - else - { - var requestedTag = ParseProxyTags(ctx.RemainderOrNull(false).NormalizeLineEndSpacing()); - if (requestedTag.IsEmpty) throw Errors.EmptyProxyTags(target, ctx); - - // This is mostly a legacy command, so it's gonna warn if there's - // already more than one proxy tag. - if (target.ProxyTags.Count > 1) - { - var msg = $"This member already has more than one proxy tag set: {target.ProxyTagsString()}\nDo you want to replace them?"; - if (!await ctx.PromptYesNo(msg, "Replace")) - throw Errors.GenericCancelled(); - } - - if (requestedTag.ProxyString.Length > Limits.MaxProxyTagLength) - throw new PKError( - $"Proxy tag too long ({requestedTag.ProxyString.Length} > {Limits.MaxProxyTagLength} characters)."); - - if (!await WarnOnConflict(requestedTag)) + var msg = $"This member already has more than one proxy tag set: {target.ProxyTagsString()}\nDo you want to replace them?"; + if (!await ctx.PromptYesNo(msg, "Replace")) throw Errors.GenericCancelled(); - - var newTags = new[] { requestedTag }; - var patch = new MemberPatch { ProxyTags = Partial.Present(newTags) }; - await ctx.Repository.UpdateMember(target.Id, patch); - - await ctx.Reply($"{Emojis.Success} Member proxy tags set to {requestedTag.ProxyString.AsCode()} (using {requestedTag.ProxyString.Length}/{Limits.MaxProxyTagLength} characters)."); } + + if (requestedTag.ProxyString.Length > Limits.MaxProxyTagLength) + throw new PKError( + $"Proxy tag too long ({requestedTag.ProxyString.Length} > {Limits.MaxProxyTagLength} characters)."); + + if (!await WarnOnConflict(ctx, target, requestedTag)) + throw Errors.GenericCancelled(); + + var newTags = new[] { requestedTag }; + var patch = new MemberPatch { ProxyTags = Partial.Present(newTags) }; + await ctx.Repository.UpdateMember(target.Id, patch); + + await ctx.Reply($"{Emojis.Success} Member proxy tags set to {requestedTag.ProxyString.AsCode()} (using {requestedTag.ProxyString.Length}/{Limits.MaxProxyTagLength} characters)."); + } + + private ProxyTag ParseProxyTag(string proxyString) + { + // Make sure there's one and only one instance of "text" in the example proxy given + var prefixAndSuffix = proxyString.Split("text"); + if (prefixAndSuffix.Length == 1) prefixAndSuffix = prefixAndSuffix[0].Split("TEXT"); + if (prefixAndSuffix.Length < 2) throw Errors.ProxyMustHaveText; + if (prefixAndSuffix.Length > 2) throw Errors.ProxyMultipleText; + return new ProxyTag(prefixAndSuffix[0], prefixAndSuffix[1]); + } + + private async Task WarnOnConflict(Context ctx, PKMember target, ProxyTag newTag) + { + var query = "select * from (select *, (unnest(proxy_tags)).prefix as prefix, (unnest(proxy_tags)).suffix as suffix from members where system = @System) as _ where prefix is not distinct from @Prefix and suffix is not distinct from @Suffix and id != @Existing"; + var conflicts = (await ctx.Database.Execute(conn => conn.QueryAsync(query, + new { newTag.Prefix, newTag.Suffix, Existing = target.Id, system = target.System }))).ToList(); + + if (conflicts.Count <= 0) return true; + + var conflictList = conflicts.Select(m => $"- **{m.NameFor(ctx)}**"); + var msg = $"{Emojis.Warn} The following members have conflicting proxy tags:\n{string.Join('\n', conflictList)}\nDo you want to proceed anyway?"; + return await ctx.PromptYesNo(msg, "Proceed"); } } \ No newline at end of file diff --git a/crates/command_definitions/src/member.rs b/crates/command_definitions/src/member.rs index 4268499e..0a13eea1 100644 --- a/crates/command_definitions/src/member.rs +++ b/crates/command_definitions/src/member.rs @@ -17,20 +17,20 @@ pub fn cmds() -> impl Iterator { let keep_proxy = ("keepproxy", ["kp"]); let server_keep_proxy = ("serverkeepproxy", ["skp"]); let autoproxy = ("autoproxy", ["ap"]); + let proxy = ("proxy", ["tags", "proxytags", "brackets"]); let tts = ("tts", ["texttospeech"]); let delete = ("delete", ["del", "remove"]); - // Group commands by functionality let member_new_cmd = [ command!(member, new, ("name", OpaqueString) => "member_new") .help("Creates a new system member"), - ].into_iter(); + ] + .into_iter(); - let member_info_cmd = [ - command!(member_target => "member_show") - .flag("pt") - .help("Shows information about a member"), - ].into_iter(); + let member_info_cmd = [command!(member_target => "member_show") + .flag("pt") + .help("Shows information about a member")] + .into_iter(); let member_name_cmd = { let member_name = tokens!(member_target, name); @@ -38,7 +38,8 @@ pub fn cmds() -> impl Iterator { command!(member_name => "member_name_show").help("Shows a member's name"), command!(member_name, ("name", OpaqueStringRemainder) => "member_name_update") .help("Changes a member's name"), - ].into_iter() + ] + .into_iter() }; let member_description_cmd = { @@ -50,7 +51,8 @@ pub fn cmds() -> impl Iterator { .help("Clears a member's description"), command!(member_desc, ("description", OpaqueStringRemainder) => "member_desc_update") .help("Changes a member's description"), - ].into_iter() + ] + .into_iter() }; let member_privacy_cmd = { @@ -63,7 +65,8 @@ pub fn cmds() -> impl Iterator { => "member_privacy_update" ) .help("Changes a member's privacy settings"), - ].into_iter() + ] + .into_iter() }; let member_pronouns_cmd = { @@ -82,40 +85,40 @@ pub fn cmds() -> impl Iterator { let member_banner_cmd = { let member_banner = tokens!(member_target, banner); [ - command!(member_banner => "member_banner_show") - .help("Shows a member's banner image"), + command!(member_banner => "member_banner_show").help("Shows a member's banner image"), command!(member_banner, ("banner", Avatar) => "member_banner_update") .help("Changes a member's banner image"), command!(member_banner, ("clear", ["c"]) => "member_banner_clear") .flag(("yes", ["y"])) .help("Clears a member's banner image"), - ].into_iter() + ] + .into_iter() }; let member_color_cmd = { let member_color = tokens!(member_target, color); [ - command!(member_color => "member_color_show") - .help("Shows a member's color"), + command!(member_color => "member_color_show").help("Shows a member's color"), command!(member_color, ("color", OpaqueString) => "member_color_update") .help("Changes a member's color"), command!(member_color, ("clear", ["c"]) => "member_color_clear") .flag(("yes", ["y"])) .help("Clears a member's color"), - ].into_iter() + ] + .into_iter() }; let member_birthday_cmd = { let member_birthday = tokens!(member_target, birthday); [ - command!(member_birthday => "member_birthday_show") - .help("Shows a member's birthday"), + command!(member_birthday => "member_birthday_show").help("Shows a member's birthday"), command!(member_birthday, ("birthday", OpaqueString) => "member_birthday_update") .help("Changes a member's birthday"), command!(member_birthday, ("clear", ["c"]) => "member_birthday_clear") .flag(("yes", ["y"])) .help("Clears a member's birthday"), - ].into_iter() + ] + .into_iter() }; let member_display_name_cmd = { @@ -144,6 +147,23 @@ pub fn cmds() -> impl Iterator { ].into_iter() }; + let member_proxy_cmd = { + let member_proxy = tokens!(member_target, proxy); + [ + command!(member_proxy => "member_proxy_show") + .help("Shows a member's proxy tags"), + command!(member_proxy, ("tags", OpaqueString) => "member_proxy_set") + .help("Sets a member's proxy tags"), + command!(member_proxy, ("add", ["a"]), ("tag", OpaqueString) => "member_proxy_add") + .help("Adds proxy tag to a member"), + command!(member_proxy, ("remove", ["r", "rm"]), ("tag", OpaqueString) => "member_proxy_remove") + .help("Removes proxy tag from a member"), + command!(member_proxy, ("clear", ["c"]) => "member_proxy_clear") + .flag(("yes", ["y"])) + .help("Clears all proxy tags from a member"), + ].into_iter() + }; + let member_proxy_settings_cmd = { let member_keep_proxy = tokens!(member_target, keep_proxy); let member_server_keep_proxy = tokens!(member_target, server_keep_proxy); @@ -174,17 +194,16 @@ pub fn cmds() -> impl Iterator { .help("Shows whether a member can be autoproxied"), command!(member_autoproxy, ("value", Toggle) => "member_autoproxy_update") .help("Changes whether a member can be autoproxied"), - ].into_iter() + ] + .into_iter() }; - let member_delete_cmd = [ - command!(member_target, delete => "member_delete") - .help("Deletes a member"), - ].into_iter(); + let member_delete_cmd = + [command!(member_target, delete => "member_delete").help("Deletes a member")].into_iter(); - let member_easter_eggs = [ - command!(member_target, "soulscream" => "member_soulscream").show_in_suggestions(false), - ].into_iter(); + let member_easter_eggs = + [command!(member_target, "soulscream" => "member_soulscream").show_in_suggestions(false)] + .into_iter(); member_new_cmd .chain(member_info_cmd) @@ -197,6 +216,7 @@ pub fn cmds() -> impl Iterator { .chain(member_birthday_cmd) .chain(member_display_name_cmd) .chain(member_server_name_cmd) + .chain(member_proxy_cmd) .chain(member_proxy_settings_cmd) .chain(member_message_settings_cmd) .chain(member_delete_cmd) From 15191171f5dcec0e51bd13dc11113e2317192c42 Mon Sep 17 00:00:00 2001 From: dusk Date: Thu, 4 Sep 2025 04:01:21 +0300 Subject: [PATCH 090/179] feat: implement member avatar commands --- PluralKit.Bot/CommandMeta/CommandTree.cs | 18 +-- PluralKit.Bot/Commands/MemberAvatar.cs | 135 ++++++++++++++--------- crates/command_definitions/src/member.rs | 84 ++++++++++++++ 3 files changed, 178 insertions(+), 59 deletions(-) diff --git a/PluralKit.Bot/CommandMeta/CommandTree.cs b/PluralKit.Bot/CommandMeta/CommandTree.cs index a90ad7f5..663f506b 100644 --- a/PluralKit.Bot/CommandMeta/CommandTree.cs +++ b/PluralKit.Bot/CommandMeta/CommandTree.cs @@ -16,6 +16,15 @@ public partial class CommandTree Commands.MemberShow(var param, _) => ctx.Execute(MemberInfo, m => m.ViewMember(ctx, param.target)), Commands.MemberNew(var param, _) => ctx.Execute(MemberNew, m => m.NewMember(ctx, param.name)), Commands.MemberSoulscream(var param, _) => ctx.Execute(MemberInfo, m => m.Soulscream(ctx, param.target)), + Commands.MemberAvatarShow(var param, var flags) => ctx.Execute(MemberAvatar, m => m.ShowAvatar(ctx, param.target, flags.GetReplyFormat())), + Commands.MemberAvatarClear(var param, var flags) => ctx.Execute(MemberAvatar, m => m.ClearAvatar(ctx, param.target)), + Commands.MemberAvatarUpdate(var param, _) => ctx.Execute(MemberAvatar, m => m.ChangeAvatar(ctx, param.target, param.avatar)), + Commands.MemberWebhookAvatarShow(var param, var flags) => ctx.Execute(MemberAvatar, m => m.ShowWebhookAvatar(ctx, param.target, flags.GetReplyFormat())), + Commands.MemberWebhookAvatarClear(var param, var flags) => ctx.Execute(MemberAvatar, m => m.ClearWebhookAvatar(ctx, param.target)), + Commands.MemberWebhookAvatarUpdate(var param, _) => ctx.Execute(MemberAvatar, m => m.ChangeWebhookAvatar(ctx, param.target, param.avatar)), + Commands.MemberServerAvatarShow(var param, var flags) => ctx.Execute(MemberAvatar, m => m.ShowServerAvatar(ctx, param.target, flags.GetReplyFormat())), + Commands.MemberServerAvatarClear(var param, var flags) => ctx.Execute(MemberAvatar, m => m.ClearServerAvatar(ctx, param.target)), + Commands.MemberServerAvatarUpdate(var param, _) => ctx.Execute(MemberAvatar, m => m.ChangeServerAvatar(ctx, param.target, param.avatar)), Commands.MemberPronounsShow(var param, var flags) => ctx.Execute(MemberPronouns, m => m.ShowPronouns(ctx, param.target, flags.GetReplyFormat())), Commands.MemberPronounsClear(var param, var flags) => ctx.Execute(MemberPronouns, m => m.ClearPronouns(ctx, param.target, flags.yes)), Commands.MemberPronounsUpdate(var param, _) => ctx.Execute(MemberPronouns, m => m.ChangePronouns(ctx, param.target, param.pronouns)), @@ -435,11 +444,7 @@ public partial class CommandTree private async Task HandleMemberCommandTargeted(Context ctx, PKMember target) { // Commands that have a member target (eg. pk;member delete) - if (ctx.Match("avatar", "profile", "picture", "icon", "image", "pfp", "pic")) - await ctx.Execute(MemberAvatar, m => m.Avatar(ctx, target)); - else if (ctx.Match("proxyavatar", "proxypfp", "webhookavatar", "webhookpfp", "pa", "pavatar", "ppfp")) - await ctx.Execute(MemberAvatar, m => m.WebhookAvatar(ctx, target)); - else if (ctx.Match("group", "groups", "g")) + if (ctx.Match("group", "groups", "g")) if (ctx.Match("add", "a")) await ctx.Execute(MemberGroupAdd, m => m.AddRemoveGroups(ctx, target, Groups.AddRemoveOperation.Add)); @@ -448,9 +453,6 @@ public partial class CommandTree m => m.AddRemoveGroups(ctx, target, Groups.AddRemoveOperation.Remove)); else await ctx.Execute(MemberGroups, m => m.ListMemberGroups(ctx, target)); - else if (ctx.Match("serveravatar", "sa", "servericon", "serverimage", "serverpfp", "serverpic", "savatar", "spic", - "guildavatar", "guildpic", "guildicon", "sicon", "spfp")) - await ctx.Execute(MemberServerAvatar, m => m.ServerAvatar(ctx, target)); else if (ctx.Match("id")) await ctx.Execute(MemberId, m => m.DisplayId(ctx, target)); else diff --git a/PluralKit.Bot/Commands/MemberAvatar.cs b/PluralKit.Bot/Commands/MemberAvatar.cs index 26eb310e..8c5289f8 100644 --- a/PluralKit.Bot/Commands/MemberAvatar.cs +++ b/PluralKit.Bot/Commands/MemberAvatar.cs @@ -19,6 +19,9 @@ public class MemberAvatar private async Task AvatarClear(MemberAvatarLocation location, Context ctx, PKMember target, MemberGuildSettings? mgs) { + ctx.CheckSystem().CheckOwnMember(target); + await ctx.ConfirmClear("this member's " + location.Name()); + await UpdateAvatar(location, ctx, target, null); if (location == MemberAvatarLocation.Server) { @@ -47,7 +50,7 @@ public class MemberAvatar } private async Task AvatarShow(MemberAvatarLocation location, Context ctx, PKMember target, - MemberGuildSettings? guildData) + MemberGuildSettings? guildData, ReplyFormat format) { // todo: this privacy code is really confusing // for now, we skip privacy flag/config parsing for this, but it would be good to fix that at some point @@ -86,7 +89,6 @@ public class MemberAvatar if (location == MemberAvatarLocation.Server) field += $" (for {ctx.Guild.Name})"; - var format = ctx.MatchFormat(); if (format == ReplyFormat.Raw) { await ctx.Reply($"`{currentValue?.TryGetCleanCdnUrl()}`"); @@ -110,58 +112,89 @@ public class MemberAvatar else throw new PKError("Format Not Recognized"); } - public async Task ServerAvatar(Context ctx, PKMember target) + private async Task AvatarChange(MemberAvatarLocation location, Context ctx, PKMember target, + MemberGuildSettings? guildData, ParsedImage avatar) { - ctx.CheckGuildContext(); - var guildData = await ctx.Repository.GetMemberGuild(ctx.Guild.Id, target.Id); - await AvatarCommandTree(MemberAvatarLocation.Server, ctx, target, guildData); - } - - public async Task Avatar(Context ctx, PKMember target) - { - var guildData = ctx.Guild != null - ? await ctx.Repository.GetMemberGuild(ctx.Guild.Id, target.Id) - : null; - - await AvatarCommandTree(MemberAvatarLocation.Member, ctx, target, guildData); - } - - public async Task WebhookAvatar(Context ctx, PKMember target) - { - var guildData = ctx.Guild != null - ? await ctx.Repository.GetMemberGuild(ctx.Guild.Id, target.Id) - : null; - - await AvatarCommandTree(MemberAvatarLocation.MemberWebhook, ctx, target, guildData); - } - - private async Task AvatarCommandTree(MemberAvatarLocation location, Context ctx, PKMember target, - MemberGuildSettings? guildData) - { - // First, see if we need to *clear* - if (ctx.MatchClear()) - { - ctx.CheckSystem().CheckOwnMember(target); - await ctx.ConfirmClear("this member's " + location.Name()); - await AvatarClear(location, ctx, target, guildData); - return; - } - - // Then, parse an image from the command (from various sources...) - var avatarArg = await ctx.MatchImage(); - if (avatarArg == null) - { - // If we didn't get any, just show the current avatar - await AvatarShow(location, ctx, target, guildData); - return; - } - ctx.CheckSystem().CheckOwnMember(target); - avatarArg = await _avatarHosting.TryRehostImage(avatarArg.Value, AvatarHostingService.RehostedImageType.Avatar, ctx.Author.Id, ctx.System); - await _avatarHosting.VerifyAvatarOrThrow(avatarArg.Value.Url); - await UpdateAvatar(location, ctx, target, avatarArg.Value.CleanUrl ?? avatarArg.Value.Url); - await PrintResponse(location, ctx, target, avatarArg.Value, guildData); + avatar = await _avatarHosting.TryRehostImage(avatar, AvatarHostingService.RehostedImageType.Avatar, ctx.Author.Id, ctx.System); + await _avatarHosting.VerifyAvatarOrThrow(avatar.Url); + await UpdateAvatar(location, ctx, target, avatar.CleanUrl ?? avatar.Url); + await PrintResponse(location, ctx, target, avatar, guildData); + } + + private Task GetServerAvatarGuildData(Context ctx, PKMember target) + { + ctx.CheckGuildContext(); + return ctx.Repository.GetMemberGuild(ctx.Guild.Id, target.Id); + } + + private async Task GetAvatarGuildData(Context ctx, PKMember target) + { + return ctx.Guild != null + ? await ctx.Repository.GetMemberGuild(ctx.Guild.Id, target.Id) + : null; + } + + private async Task GetWebhookAvatarGuildData(Context ctx, PKMember target) + { + return ctx.Guild != null + ? await ctx.Repository.GetMemberGuild(ctx.Guild.Id, target.Id) + : null; + } + + public async Task ShowServerAvatar(Context ctx, PKMember target, ReplyFormat format) + { + var guildData = await GetServerAvatarGuildData(ctx, target); + await AvatarShow(MemberAvatarLocation.Server, ctx, target, guildData, format); + } + + public async Task ClearServerAvatar(Context ctx, PKMember target) + { + var guildData = await GetServerAvatarGuildData(ctx, target); + await AvatarClear(MemberAvatarLocation.Server, ctx, target, guildData); + } + + public async Task ChangeServerAvatar(Context ctx, PKMember target, ParsedImage avatar) + { + var guildData = await GetServerAvatarGuildData(ctx, target); + await AvatarChange(MemberAvatarLocation.Server, ctx, target, guildData, avatar); + } + + public async Task ShowAvatar(Context ctx, PKMember target, ReplyFormat format) + { + var guildData = await GetAvatarGuildData(ctx, target); + await AvatarShow(MemberAvatarLocation.Member, ctx, target, guildData, format); + } + + public async Task ClearAvatar(Context ctx, PKMember target) + { + var guildData = await GetAvatarGuildData(ctx, target); + await AvatarClear(MemberAvatarLocation.Member, ctx, target, guildData); + } + + public async Task ChangeAvatar(Context ctx, PKMember target, ParsedImage avatar) + { + var guildData = await GetAvatarGuildData(ctx, target); + await AvatarChange(MemberAvatarLocation.Member, ctx, target, guildData, avatar); + } + + public async Task ShowWebhookAvatar(Context ctx, PKMember target, ReplyFormat format) + { + var guildData = await GetWebhookAvatarGuildData(ctx, target); + await AvatarShow(MemberAvatarLocation.MemberWebhook, ctx, target, guildData, format); + } + + public async Task ClearWebhookAvatar(Context ctx, PKMember target) + { + var guildData = await GetWebhookAvatarGuildData(ctx, target); + await AvatarClear(MemberAvatarLocation.MemberWebhook, ctx, target, guildData); + } + + public async Task ChangeWebhookAvatar(Context ctx, PKMember target, ParsedImage avatar) + { + var guildData = await GetWebhookAvatarGuildData(ctx, target); + await AvatarChange(MemberAvatarLocation.MemberWebhook, ctx, target, guildData, avatar); } private Task PrintResponse(MemberAvatarLocation location, Context ctx, PKMember target, ParsedImage avatar, diff --git a/crates/command_definitions/src/member.rs b/crates/command_definitions/src/member.rs index 0a13eea1..7c070773 100644 --- a/crates/command_definitions/src/member.rs +++ b/crates/command_definitions/src/member.rs @@ -198,6 +198,89 @@ pub fn cmds() -> impl Iterator { .into_iter() }; + let member_avatar_cmd = { + let member_avatar = tokens!( + member_target, + ( + "avatar", + ["profile", "picture", "icon", "image", "pfp", "pic"] + ) + ); + [ + command!(member_avatar => "member_avatar_show").help("Shows a member's avatar"), + command!(member_avatar, ("avatar", Avatar) => "member_avatar_update") + .help("Changes a member's avatar"), + command!(member_avatar, ("clear", ["c"]) => "member_avatar_clear") + .flag(("yes", ["y"])) + .help("Clears a member's avatar"), + ] + .into_iter() + }; + + let member_webhook_avatar_cmd = { + let member_webhook_avatar = tokens!( + member_target, + ( + "proxyavatar", + [ + "proxypfp", + "webhookavatar", + "webhookpfp", + "pa", + "pavatar", + "ppfp" + ] + ) + ); + [ + command!(member_webhook_avatar => "member_webhook_avatar_show") + .help("Shows a member's proxy avatar"), + command!(member_webhook_avatar, ("avatar", Avatar) => "member_webhook_avatar_update") + .help("Changes a member's proxy avatar"), + command!(member_webhook_avatar, ("clear", ["c"]) => "member_webhook_avatar_clear") + .flag(("yes", ["y"])) + .help("Clears a member's proxy avatar"), + ] + .into_iter() + }; + + let member_server_avatar_cmd = { + let member_server_avatar = tokens!( + member_target, + ( + "serveravatar", + [ + "sa", + "servericon", + "serverimage", + "serverpfp", + "serverpic", + "savatar", + "spic", + "guildavatar", + "guildpic", + "guildicon", + "sicon", + "spfp" + ] + ) + ); + [ + command!(member_server_avatar => "member_server_avatar_show") + .help("Shows a member's server-specific avatar"), + command!(member_server_avatar, ("avatar", Avatar) => "member_server_avatar_update") + .help("Changes a member's server-specific avatar"), + command!(member_server_avatar, ("clear", ["c"]) => "member_server_avatar_clear") + .flag(("yes", ["y"])) + .help("Clears a member's server-specific avatar"), + ] + .into_iter() + }; + + let member_avatar_cmds = member_avatar_cmd + .chain(member_webhook_avatar_cmd) + .chain(member_server_avatar_cmd); + let member_delete_cmd = [command!(member_target, delete => "member_delete").help("Deletes a member")].into_iter(); @@ -217,6 +300,7 @@ pub fn cmds() -> impl Iterator { .chain(member_display_name_cmd) .chain(member_server_name_cmd) .chain(member_proxy_cmd) + .chain(member_avatar_cmds) .chain(member_proxy_settings_cmd) .chain(member_message_settings_cmd) .chain(member_delete_cmd) From 10dd499835ccddb668be0741bde8b65168e88a9f Mon Sep 17 00:00:00 2001 From: dusk Date: Wed, 24 Sep 2025 21:32:42 +0300 Subject: [PATCH 091/179] feat: implement switch commands --- PluralKit.Bot/CommandMeta/CommandTree.cs | 29 +++++++--------------- PluralKit.Bot/CommandSystem/Parameters.cs | 11 ++++++++- PluralKit.Bot/Commands/Switch.cs | 29 +++++++++------------- crates/command_definitions/src/lib.rs | 4 +-- crates/command_definitions/src/switch.rs | 30 +++++++++++++++++++++++ crates/command_parser/src/parameter.rs | 22 +++++++++++++---- crates/commands/src/bin/write_cs_glue.rs | 2 ++ crates/commands/src/commands.udl | 1 + crates/commands/src/lib.rs | 2 ++ 9 files changed, 85 insertions(+), 45 deletions(-) diff --git a/PluralKit.Bot/CommandMeta/CommandTree.cs b/PluralKit.Bot/CommandMeta/CommandTree.cs index 663f506b..d2891529 100644 --- a/PluralKit.Bot/CommandMeta/CommandTree.cs +++ b/PluralKit.Bot/CommandMeta/CommandTree.cs @@ -152,6 +152,13 @@ public partial class CommandTree Commands.SystemShowPrivacy(var param, _) => ctx.Execute(SystemPrivacy, m => m.ShowSystemPrivacy(ctx, ctx.System)), Commands.SystemChangePrivacyAll(var param, _) => ctx.Execute(SystemPrivacy, m => m.ChangeSystemPrivacyAll(ctx, ctx.System, param.level)), Commands.SystemChangePrivacy(var param, _) => ctx.Execute(SystemPrivacy, m => m.ChangeSystemPrivacy(ctx, ctx.System, param.privacy, param.level)), + Commands.SwitchOut(_, _) => ctx.Execute(SwitchOut, m => m.SwitchOut(ctx)), + Commands.SwitchDo(var param, _) => ctx.Execute(Switch, m => m.SwitchDo(ctx, param.targets)), + Commands.SwitchMove(var param, _) => ctx.Execute(SwitchMove, m => m.SwitchMove(ctx, param.@string)), + Commands.SwitchEdit(var param, var flags) => ctx.Execute(SwitchEdit, m => m.SwitchEdit(ctx, param.targets, false, flags.first, flags.remove, flags.append, flags.prepend)), + Commands.SwitchEditOut(_, _) => ctx.Execute(SwitchEditOut, m => m.SwitchEditOut(ctx)), + Commands.SwitchDelete(var param, var flags) => ctx.Execute(SwitchDelete, m => m.SwitchDelete(ctx, flags.all)), + Commands.SwitchCopy(var param, var flags) => ctx.Execute(SwitchCopy, m => m.SwitchEdit(ctx, param.targets, true, flags.first, flags.remove, flags.append, flags.prepend)), _ => // this should only ever occur when deving if commands are not implemented... ctx.Reply( @@ -521,26 +528,8 @@ public partial class CommandTree private async Task HandleSwitchCommand(Context ctx) { - if (ctx.Match("out")) - await ctx.Execute(SwitchOut, m => m.SwitchOut(ctx)); - else if (ctx.Match("move", "m", "shift", "offset")) - await ctx.Execute(SwitchMove, m => m.SwitchMove(ctx)); - else if (ctx.Match("edit", "e", "replace")) - if (ctx.Match("out")) - await ctx.Execute(SwitchEditOut, m => m.SwitchEditOut(ctx)); - else - 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, SwitchCopy, SystemFronter, SystemFrontHistory); + await PrintCommandNotFoundError(ctx, Switch, SwitchOut, SwitchMove, SwitchEdit, SwitchEditOut, + SwitchDelete, SwitchCopy, SystemFronter, SystemFrontHistory); } private async Task CommandHelpRoot(Context ctx) diff --git a/PluralKit.Bot/CommandSystem/Parameters.cs b/PluralKit.Bot/CommandSystem/Parameters.cs index 28656ce6..2c753fde 100644 --- a/PluralKit.Bot/CommandSystem/Parameters.cs +++ b/PluralKit.Bot/CommandSystem/Parameters.cs @@ -1,3 +1,4 @@ +using Humanizer; using Myriad.Types; using PluralKit.Core; using uniffi.commands; @@ -8,6 +9,7 @@ namespace PluralKit.Bot; public abstract record Parameter() { public record MemberRef(PKMember member): Parameter; + public record MemberRefs(List members): Parameter; public record SystemRef(PKSystem system): Parameter; public record GuildRef(Guild guild): Parameter; public record MemberPrivacyTarget(MemberPrivacySubject target): Parameter; @@ -56,14 +58,21 @@ public class Parameters private async Task ResolveFfiParam(Context ctx, uniffi.commands.Parameter ffi_param) { + var byId = HasFlag("id", "by-id"); switch (ffi_param) { case uniffi.commands.Parameter.MemberRef memberRef: - var byId = HasFlag("id", "by-id"); return new Parameter.MemberRef( await ctx.ParseMember(memberRef.member, byId) ?? throw new PKError(ctx.CreateNotFoundError("Member", memberRef.member, byId)) ); + case uniffi.commands.Parameter.MemberRefs memberRefs: + return new Parameter.MemberRefs( + await memberRefs.members.ToAsyncEnumerable().SelectAwait(async m => + await ctx.ParseMember(m, byId) + ?? throw new PKError(ctx.CreateNotFoundError("Member", m, byId)) + ).ToListAsync() + ); case uniffi.commands.Parameter.SystemRef systemRef: // todo: do we need byId here? return new Parameter.SystemRef( diff --git a/PluralKit.Bot/Commands/Switch.cs b/PluralKit.Bot/Commands/Switch.cs index 83465bd4..624774da 100644 --- a/PluralKit.Bot/Commands/Switch.cs +++ b/PluralKit.Bot/Commands/Switch.cs @@ -8,11 +8,10 @@ namespace PluralKit.Bot; public class Switch { - public async Task SwitchDo(Context ctx) + public async Task SwitchDo(Context ctx, ICollection members) { ctx.CheckSystem(); - var members = await ctx.ParseMemberList(ctx.System.Id); await DoSwitchCommand(ctx, members); } @@ -21,7 +20,7 @@ public class Switch ctx.CheckSystem(); // Switch with no members = switch-out - await DoSwitchCommand(ctx, new PKMember[] { }); + await DoSwitchCommand(ctx, []); } private async Task DoSwitchCommand(Context ctx, ICollection members) @@ -57,12 +56,10 @@ public class Switch $"{Emojis.Success} Switch registered. Current fronters are now {string.Join(", ", members.Select(m => m.NameFor(ctx)))}."); } - public async Task SwitchMove(Context ctx) + public async Task SwitchMove(Context ctx, string timeToMove) { ctx.CheckSystem(); - var timeToMove = ctx.RemainderOrNull() ?? - throw new PKSyntaxError("Must pass a date or time to move the switch to."); var tz = TzdbDateTimeZoneSource.Default.ForId(ctx.Config?.UiTz ?? "UTC"); var result = DateUtils.ParseDateTime(timeToMove, true, tz); @@ -104,31 +101,29 @@ public class Switch await ctx.Reply($"{Emojis.Success} Switch moved to ({newSwitchDeltaStr} ago)."); } - public async Task SwitchEdit(Context ctx, bool newSwitch = false) + public async Task SwitchEdit(Context ctx, List newMembers, bool newSwitch = false, bool first = false, bool remove = false, bool append = false, bool prepend = false) { ctx.CheckSystem(); - 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); if (currentSwitch == null) throw Errors.NoRegisteredSwitches; var currentSwitchMembers = await ctx.Repository.GetSwitchMembers(conn, currentSwitch.Id).ToListAsync().AsTask(); - if (ctx.MatchFlag("first", "f")) + if (first) newMembers = FirstInSwitch(newMembers[0], currentSwitchMembers); - else if (ctx.MatchFlag("remove", "r")) + else if (remove) newMembers = RemoveFromSwitch(newMembers, currentSwitchMembers); - else if (ctx.MatchFlag("append", "a")) + else if (append) newMembers = AppendToSwitch(newMembers, currentSwitchMembers); - else if (ctx.MatchFlag("prepend", "p")) + else if (prepend) 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")) + if (!prepend && !append && !remove && !first) newMembers = AppendToSwitch(newMembers, currentSwitchMembers); await DoSwitchCommand(ctx, newMembers); } @@ -172,7 +167,7 @@ public class Switch public async Task SwitchEditOut(Context ctx) { ctx.CheckSystem(); - await DoEditCommand(ctx, new PKMember[] { }); + await DoEditCommand(ctx, []); } public async Task DoEditCommand(Context ctx, ICollection members) @@ -217,11 +212,11 @@ public class Switch await ctx.Reply($"{Emojis.Success} Switch edited. Current fronters are now {newSwitchMemberStr}."); } - public async Task SwitchDelete(Context ctx) + public async Task SwitchDelete(Context ctx, bool all) { ctx.CheckSystem(); - if (ctx.Match("all", "clear") || ctx.MatchFlag("all", "clear", "c")) + if (all) { // Subcommand: "delete all" var purgeMsg = diff --git a/crates/command_definitions/src/lib.rs b/crates/command_definitions/src/lib.rs index b7336a3e..28715d5f 100644 --- a/crates/command_definitions/src/lib.rs +++ b/crates/command_definitions/src/lib.rs @@ -26,8 +26,8 @@ pub fn all() -> impl Iterator { .chain(member::cmds()) .chain(config::cmds()) .chain(fun::cmds()) - .map(|cmd| cmd.flag(("plaintext", ["pt"]))) - .map(|cmd| cmd.flag(("raw", ["r"]))) + .chain(switch::cmds()) + .map(|cmd| cmd.flag(("plaintext", ["pt"])).flag(("raw", ["r"]))) } pub const RESET: (&str, [&str; 2]) = ("reset", ["clear", "default"]); diff --git a/crates/command_definitions/src/switch.rs b/crates/command_definitions/src/switch.rs index 8b137891..fce7f760 100644 --- a/crates/command_definitions/src/switch.rs +++ b/crates/command_definitions/src/switch.rs @@ -1 +1,31 @@ +use super::*; +pub fn cmds() -> impl Iterator { + let switch = ("switch", ["sw"]); + + let edit = ("edit", ["e", "replace"]); + let r#move = ("move", ["m", "shift", "offset"]); + let delete = ("delete", ["remove", "erase", "cancel", "yeet"]); + let copy = ("copy", ["add", "duplicate", "dupe"]); + let out = "out"; + + [ + command!(switch, out => "switch_out"), + command!(switch, r#move, OpaqueString => "switch_move"), // TODO: datetime parsing + command!(switch, delete => "switch_delete").flag(("all", ["clear", "c"])), + command!(switch, edit, out => "switch_edit_out"), + command!(switch, edit, MemberRefs => "switch_edit") + .flag(("first", ["f"])) + .flag(("remove", ["r"])) + .flag(("append", ["a"])) + .flag(("prepend", ["p"])), + command!(switch, copy, MemberRefs => "switch_copy") + .flag(("first", ["f"])) + .flag(("remove", ["r"])) + .flag(("append", ["a"])) + .flag(("prepend", ["p"])), + command!(switch, ("commands", ["help"]) => "switch_commands"), + command!(switch, MemberRefs => "switch_do"), + ] + .into_iter() +} diff --git a/crates/command_parser/src/parameter.rs b/crates/command_parser/src/parameter.rs index 282cfdc8..08971d99 100644 --- a/crates/command_parser/src/parameter.rs +++ b/crates/command_parser/src/parameter.rs @@ -11,6 +11,7 @@ use crate::token::{Token, TokenMatchResult}; pub enum ParameterValue { OpaqueString(String), MemberRef(String), + MemberRefs(Vec), SystemRef(String), GuildRef(String), MemberPrivacyTarget(String), @@ -39,10 +40,14 @@ impl Parameter { impl Display for Parameter { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self.kind { - ParameterKind::OpaqueString | ParameterKind::OpaqueStringRemainder => { + ParameterKind::OpaqueString => { write!(f, "[{}]", self.name) } + ParameterKind::OpaqueStringRemainder => { + write!(f, "[{}]...", self.name) + } ParameterKind::MemberRef => write!(f, ""), + ParameterKind::MemberRefs => write!(f, " ..."), ParameterKind::SystemRef => write!(f, ""), ParameterKind::GuildRef => write!(f, ""), ParameterKind::MemberPrivacyTarget => write!(f, ""), @@ -77,6 +82,7 @@ pub enum ParameterKind { OpaqueString, OpaqueStringRemainder, MemberRef, + MemberRefs, SystemRef, GuildRef, MemberPrivacyTarget, @@ -92,6 +98,7 @@ impl ParameterKind { ParameterKind::OpaqueString => "string", ParameterKind::OpaqueStringRemainder => "string", ParameterKind::MemberRef => "target", + ParameterKind::MemberRefs => "targets", ParameterKind::SystemRef => "target", ParameterKind::GuildRef => "target", ParameterKind::MemberPrivacyTarget => "member_privacy_target", @@ -103,7 +110,10 @@ impl ParameterKind { } pub(crate) fn remainder(&self) -> bool { - matches!(self, ParameterKind::OpaqueStringRemainder) + matches!( + self, + ParameterKind::OpaqueStringRemainder | ParameterKind::MemberRefs + ) } pub(crate) fn match_value(&self, input: &str) -> Result { @@ -113,12 +123,14 @@ impl ParameterKind { Ok(ParameterValue::OpaqueString(input.into())) } ParameterKind::MemberRef => Ok(ParameterValue::MemberRef(input.into())), + ParameterKind::MemberRefs => Ok(ParameterValue::MemberRefs( + input.split(' ').map(|s| s.trim().to_string()).collect(), + )), ParameterKind::SystemRef => Ok(ParameterValue::SystemRef(input.into())), ParameterKind::MemberPrivacyTarget => MemberPrivacyTargetKind::from_str(input) .map(|target| ParameterValue::MemberPrivacyTarget(target.as_ref().into())), - ParameterKind::SystemPrivacyTarget => SystemPrivacyTargetKind::from_str(input).map( - |target| ParameterValue::SystemPrivacyTarget(target.as_ref().into()), - ), + ParameterKind::SystemPrivacyTarget => SystemPrivacyTargetKind::from_str(input) + .map(|target| ParameterValue::SystemPrivacyTarget(target.as_ref().into())), ParameterKind::PrivacyLevel => PrivacyLevelKind::from_str(input) .map(|level| ParameterValue::PrivacyLevel(level.as_ref().into())), ParameterKind::Toggle => { diff --git a/crates/commands/src/bin/write_cs_glue.rs b/crates/commands/src/bin/write_cs_glue.rs index dbee8d80..0559de88 100644 --- a/crates/commands/src/bin/write_cs_glue.rs +++ b/crates/commands/src/bin/write_cs_glue.rs @@ -167,6 +167,7 @@ fn get_param_ty(kind: ParameterKind) -> &'static str { match kind { ParameterKind::OpaqueString | ParameterKind::OpaqueStringRemainder => "string", ParameterKind::MemberRef => "PKMember", + ParameterKind::MemberRefs => "List", ParameterKind::SystemRef => "PKSystem", ParameterKind::MemberPrivacyTarget => "MemberPrivacySubject", ParameterKind::SystemPrivacyTarget => "SystemPrivacySubject", @@ -181,6 +182,7 @@ fn get_param_param_ty(kind: ParameterKind) -> &'static str { match kind { ParameterKind::OpaqueString | ParameterKind::OpaqueStringRemainder => "Opaque", ParameterKind::MemberRef => "Member", + ParameterKind::MemberRefs => "Members", ParameterKind::SystemRef => "System", ParameterKind::MemberPrivacyTarget => "MemberPrivacyTarget", ParameterKind::SystemPrivacyTarget => "SystemPrivacyTarget", diff --git a/crates/commands/src/commands.udl b/crates/commands/src/commands.udl index 5a368266..1a8da927 100644 --- a/crates/commands/src/commands.udl +++ b/crates/commands/src/commands.udl @@ -9,6 +9,7 @@ interface CommandResult { [Enum] interface Parameter { MemberRef(string member); + MemberRefs(sequence members); SystemRef(string system); GuildRef(string guild); MemberPrivacyTarget(string target); diff --git a/crates/commands/src/lib.rs b/crates/commands/src/lib.rs index 92ca7e4f..bc79e0ca 100644 --- a/crates/commands/src/lib.rs +++ b/crates/commands/src/lib.rs @@ -23,6 +23,7 @@ pub enum CommandResult { #[derive(Debug, Clone)] pub enum Parameter { MemberRef { member: String }, + MemberRefs { members: Vec }, SystemRef { system: String }, GuildRef { guild: String }, MemberPrivacyTarget { target: String }, @@ -37,6 +38,7 @@ impl From for Parameter { fn from(value: ParameterValue) -> Self { match value { ParameterValue::MemberRef(member) => Self::MemberRef { member }, + ParameterValue::MemberRefs(members) => Self::MemberRefs { members }, ParameterValue::SystemRef(system) => Self::SystemRef { system }, ParameterValue::MemberPrivacyTarget(target) => Self::MemberPrivacyTarget { target }, ParameterValue::SystemPrivacyTarget(target) => Self::SystemPrivacyTarget { target }, From 4e0c56f6cb7194359184dd25c592f7821905f055 Mon Sep 17 00:00:00 2001 From: dusk Date: Wed, 24 Sep 2025 21:36:33 +0000 Subject: [PATCH 092/179] fix: add missing parameters ext method, fix SwitchDelete usage --- .gitignore | 1 + .../CommandSystem/Context/ContextParametersExt.cs | 8 ++++++++ PluralKit.Bot/Commands/SystemFront.cs | 2 +- flake.nix | 2 +- 4 files changed, 11 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 31332235..b72e1aaa 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ target/ .idea/ .run/ .vscode/ +.zed/ .mono/ tags/ .DS_Store diff --git a/PluralKit.Bot/CommandSystem/Context/ContextParametersExt.cs b/PluralKit.Bot/CommandSystem/Context/ContextParametersExt.cs index 45efb250..9f0c3f22 100644 --- a/PluralKit.Bot/CommandSystem/Context/ContextParametersExt.cs +++ b/PluralKit.Bot/CommandSystem/Context/ContextParametersExt.cs @@ -20,6 +20,14 @@ public static class ContextParametersExt ); } + public static async Task> ParamResolveMembers(this Context ctx, string param_name) + { + return await ctx.Parameters.ResolveParameter( + ctx, param_name, + param => (param as Parameter.MemberRefs)?.members + ); + } + public static async Task ParamResolveSystem(this Context ctx, string param_name) { return await ctx.Parameters.ResolveParameter( diff --git a/PluralKit.Bot/Commands/SystemFront.cs b/PluralKit.Bot/Commands/SystemFront.cs index f5b06f87..8139d5e4 100644 --- a/PluralKit.Bot/Commands/SystemFront.cs +++ b/PluralKit.Bot/Commands/SystemFront.cs @@ -30,7 +30,7 @@ public class SystemFront { if (ctx.MatchFlag("clear", "c") || ctx.PeekArgument() == "clear") { - await new Switch().SwitchDelete(ctx); + await new Switch().SwitchDelete(ctx, true); return; } diff --git a/flake.nix b/flake.nix index 168b3773..692142a8 100644 --- a/flake.nix +++ b/flake.nix @@ -199,7 +199,7 @@ depends_on.redis.condition = "process_healthy"; depends_on.pluralkit-gateway.condition = "process_log_ready"; # TODO: add liveness check - ready_log_line = "Received Ready"; + ready_log_line = "Connected! All is good (probably)."; availability.restart = "on_failure"; availability.max_restarts = 3; }; From 104083aac17de5dd3521b685d4cd34c6018ab5e7 Mon Sep 17 00:00:00 2001 From: dusk Date: Fri, 26 Sep 2025 14:58:59 +0000 Subject: [PATCH 093/179] feat: implement system front commands --- PluralKit.Bot/CommandMeta/CommandTree.cs | 16 +++------------- PluralKit.Bot/Commands/SystemFront.cs | 13 ++++--------- crates/command_definitions/src/system.rs | 13 +++++++++++++ crates/command_parser/src/lib.rs | 19 ++++++++++++++----- crates/command_parser/src/token.rs | 2 +- 5 files changed, 35 insertions(+), 28 deletions(-) diff --git a/PluralKit.Bot/CommandMeta/CommandTree.cs b/PluralKit.Bot/CommandMeta/CommandTree.cs index d2891529..0aad03f6 100644 --- a/PluralKit.Bot/CommandMeta/CommandTree.cs +++ b/PluralKit.Bot/CommandMeta/CommandTree.cs @@ -159,6 +159,9 @@ public partial class CommandTree Commands.SwitchEditOut(_, _) => ctx.Execute(SwitchEditOut, m => m.SwitchEditOut(ctx)), Commands.SwitchDelete(var param, var flags) => ctx.Execute(SwitchDelete, m => m.SwitchDelete(ctx, flags.all)), Commands.SwitchCopy(var param, var flags) => ctx.Execute(SwitchCopy, m => m.SwitchEdit(ctx, param.targets, true, flags.first, flags.remove, flags.append, flags.prepend)), + Commands.SystemFronter(var param, var flags) => ctx.Execute(SystemFronter, m => m.Fronter(ctx, param.target)), + Commands.SystemFronterHistory(var param, var flags) => ctx.Execute(SystemFrontHistory, m => m.FrontHistory(ctx, param.target, flags.clear)), + Commands.SystemFronterPercent(var param, var flags) => ctx.Execute(SystemFrontPercent, m => m.FrontPercent(ctx, param.target, flags.duration, flags.fronters_only, flags.flat)), _ => // this should only ever occur when deving if commands are not implemented... ctx.Reply( @@ -407,19 +410,6 @@ public partial class CommandTree await ctx.CheckSystem(target).Execute(SystemList, m => m.MemberList(ctx, target)); else if (ctx.Match("find", "search", "query", "fd", "s")) await ctx.CheckSystem(target).Execute(SystemFind, m => m.MemberList(ctx, target)); - else if (ctx.Match("f", "front", "fronter", "fronters")) - { - if (ctx.Match("h", "history")) - await ctx.CheckSystem(target).Execute(SystemFrontHistory, m => m.SystemFrontHistory(ctx, target)); - else if (ctx.Match("p", "percent", "%")) - await ctx.CheckSystem(target).Execute(SystemFrontPercent, m => m.FrontPercent(ctx, system: target)); - else - await ctx.CheckSystem(target).Execute(SystemFronter, m => m.SystemFronter(ctx, target)); - } - else if (ctx.Match("fh", "fronthistory", "history", "switches")) - await ctx.CheckSystem(target).Execute(SystemFrontHistory, m => m.SystemFrontHistory(ctx, target)); - else if (ctx.Match("fp", "frontpercent", "front%", "frontbreakdown")) - await ctx.CheckSystem(target).Execute(SystemFrontPercent, m => m.FrontPercent(ctx, system: target)); else if (ctx.Match("groups", "gs")) await ctx.CheckSystem(target).Execute(GroupList, g => g.ListSystemGroups(ctx, target)); else if (ctx.Match("id")) diff --git a/PluralKit.Bot/Commands/SystemFront.cs b/PluralKit.Bot/Commands/SystemFront.cs index 8139d5e4..6d9cf049 100644 --- a/PluralKit.Bot/Commands/SystemFront.cs +++ b/PluralKit.Bot/Commands/SystemFront.cs @@ -15,7 +15,7 @@ public class SystemFront _embeds = embeds; } - public async Task SystemFronter(Context ctx, PKSystem system) + public async Task Fronter(Context ctx, PKSystem system) { if (system == null) throw Errors.NoSystemError(ctx.DefaultPrefix); ctx.CheckSystemPrivacy(system.Id, system.FrontPrivacy); @@ -26,9 +26,9 @@ public class SystemFront await ctx.Reply(embed: await _embeds.CreateFronterEmbed(sw, ctx.Zone, ctx.LookupContextFor(system.Id))); } - public async Task SystemFrontHistory(Context ctx, PKSystem system) + public async Task FrontHistory(Context ctx, PKSystem system, bool clear = false) { - if (ctx.MatchFlag("clear", "c") || ctx.PeekArgument() == "clear") + if (clear) { await new Switch().SwitchDelete(ctx, true); return; @@ -106,7 +106,7 @@ public class SystemFront ); } - public async Task FrontPercent(Context ctx, PKSystem? system = null, PKGroup? group = null) + public async Task FrontPercent(Context ctx, PKSystem? system = null, string durationStr = "30d", bool ignoreNoFronters = false, bool showFlat = false, PKGroup? group = null) { if (system == null && group == null) throw Errors.NoSystemError(ctx.DefaultPrefix); if (system == null) system = await GetGroupSystem(ctx, group); @@ -116,11 +116,6 @@ public class SystemFront var totalSwitches = await ctx.Repository.GetSwitchCount(system.Id); if (totalSwitches == 0) throw Errors.NoRegisteredSwitches; - var ignoreNoFronters = ctx.MatchFlag("fo", "fronters-only"); - var showFlat = ctx.MatchFlag("flat"); - - var durationStr = ctx.RemainderOrNull() ?? "30d"; - // Picked the UNIX epoch as a random date // even though we don't store switch timestamps in UNIX time // I assume most people won't have switches logged previously to that (?) diff --git a/crates/command_definitions/src/system.rs b/crates/command_definitions/src/system.rs index a4fe0037..7de7e274 100644 --- a/crates/command_definitions/src/system.rs +++ b/crates/command_definitions/src/system.rs @@ -223,6 +223,18 @@ pub fn edit() -> impl Iterator { .help("Changes a specific privacy setting for your system"), ].into_iter(); + let system_front = tokens!(system_target, ("front", ["fronter", "fronters", "f"])); + let system_front_cmd = [ + command!(system_front => "system_fronter"), + command!(system_front, ("history", ["h"]) => "system_fronter_history") + .flag(("clear", ["c"])), + command!(system_front, ("percent", ["p", "%"]) => "system_fronter_percent") + .flag(("duration", OpaqueString)) + .flag(("fronters-only", ["fo"])) + .flag("flat"), + ] + .into_iter(); + system_new_cmd .chain(system_name_self_cmd) .chain(system_server_name_self_cmd) @@ -248,4 +260,5 @@ pub fn edit() -> impl Iterator { .chain(system_server_avatar_cmd) .chain(system_banner_cmd) .chain(system_info_cmd) + .chain(system_front_cmd) } diff --git a/crates/command_parser/src/lib.rs b/crates/command_parser/src/lib.rs index 34614f2d..c02fc463 100644 --- a/crates/command_parser/src/lib.rs +++ b/crates/command_parser/src/lib.rs @@ -58,10 +58,14 @@ pub fn parse_command( match &result { // todo: better error messages for these? TokenMatchResult::MissingParameter { name } => { - return Err(format!("Expected parameter `{name}` in command `{prefix}{input} {found_token}`.")); + return Err(format!( + "Expected parameter `{name}` in command `{prefix}{input} {found_token}`." + )); } TokenMatchResult::ParameterMatchError { input: raw, msg } => { - return Err(format!("Parameter `{raw}` in command `{prefix}{input}` could not be parsed: {msg}.")); + return Err(format!( + "Parameter `{raw}` in command `{prefix}{input}` could not be parsed: {msg}." + )); } // don't use a catch-all here, we want to make sure compiler errors when new errors are added TokenMatchResult::MatchedParameter { .. } | TokenMatchResult::MatchedValue => {} @@ -109,7 +113,9 @@ pub fn parse_command( raw_flags.push((current_token_idx, matched_flag)); } // if we have a command, stop parsing and return it (only if there is no remaining input) - if current_pos >= input.len() && let Some(command) = local_tree.command() { + if current_pos >= input.len() + && let Some(command) = local_tree.command() + { // match the flags against this commands flags let mut flags: HashMap> = HashMap::new(); let mut misplaced_flags: Vec = Vec::new(); @@ -182,8 +188,11 @@ pub fn parse_command( write!( &mut error, " {} seem to be applicable in this command (`{prefix}{command}`).", - (invalid_flags.len() > 1).then_some("don't").unwrap_or("doesn't") - ).expect("oom"); + (invalid_flags.len() > 1) + .then_some("don't") + .unwrap_or("doesn't") + ) + .expect("oom"); return Err(error); } println!("{} {flags:?} {params:?}", command.cb); diff --git a/crates/command_parser/src/token.rs b/crates/command_parser/src/token.rs index 863cb936..653b8a65 100644 --- a/crates/command_parser/src/token.rs +++ b/crates/command_parser/src/token.rs @@ -76,7 +76,7 @@ impl Token { msg: err, } } - }, + } }), // don't add a _ match here! } From 02a99025dc5a1b04a61f57a9c041495faa7f01d6 Mon Sep 17 00:00:00 2001 From: dusk Date: Fri, 26 Sep 2025 15:41:18 +0000 Subject: [PATCH 094/179] fix flake services name --- flake.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flake.nix b/flake.nix index 2f333d80..2769372d 100644 --- a/flake.nix +++ b/flake.nix @@ -63,7 +63,7 @@ }; nci.toolchainConfig = ./rust-toolchain.toml; - nci.projects."pluralkit-services" = { + nci.projects."pk-services" = { path = ./.; export = false; }; From c00ff2f371d318e11308ecb03bf8b6e4877fd135 Mon Sep 17 00:00:00 2001 From: dusk Date: Fri, 26 Sep 2025 18:47:54 +0000 Subject: [PATCH 095/179] implement show-embed flags --- PluralKit.Bot/CommandMeta/CommandTree.cs | 4 ++-- PluralKit.Bot/Commands/Groups.cs | 4 ++-- PluralKit.Bot/Commands/Help.cs | 4 ++-- PluralKit.Bot/Commands/Member.cs | 4 ++-- PluralKit.Bot/Commands/Random.cs | 12 ++++++------ PluralKit.Bot/Commands/System.cs | 6 +++--- PluralKit.Bot/Services/EmbedService.cs | 4 ++-- crates/command_definitions/src/lib.rs | 6 +++++- 8 files changed, 24 insertions(+), 20 deletions(-) diff --git a/PluralKit.Bot/CommandMeta/CommandTree.cs b/PluralKit.Bot/CommandMeta/CommandTree.cs index eb8c4f60..1f546baa 100644 --- a/PluralKit.Bot/CommandMeta/CommandTree.cs +++ b/PluralKit.Bot/CommandMeta/CommandTree.cs @@ -8,12 +8,12 @@ public partial class CommandTree { return command switch { - Commands.Help => ctx.Execute(Help, m => m.HelpRoot(ctx)), + Commands.Help(_, var flags) => ctx.Execute(Help, m => m.HelpRoot(ctx, flags.show_embed)), Commands.HelpCommands => ctx.Reply( "For the list of commands, see the website: "), Commands.HelpProxy => ctx.Reply( "The proxy help page has been moved! See the website: https://pluralkit.me/guide#proxying"), - Commands.MemberShow(var param, _) => ctx.Execute(MemberInfo, m => m.ViewMember(ctx, param.target)), + Commands.MemberShow(var param, var flags) => ctx.Execute(MemberInfo, m => m.ViewMember(ctx, param.target, flags.show_embed)), Commands.MemberNew(var param, _) => ctx.Execute(MemberNew, m => m.NewMember(ctx, param.name)), Commands.MemberSoulscream(var param, _) => ctx.Execute(MemberInfo, m => m.Soulscream(ctx, param.target)), Commands.MemberAvatarShow(var param, var flags) => ctx.Execute(MemberAvatar, m => m.ShowAvatar(ctx, param.target, flags.GetReplyFormat())), diff --git a/PluralKit.Bot/Commands/Groups.cs b/PluralKit.Bot/Commands/Groups.cs index b18764d2..e350ee58 100644 --- a/PluralKit.Bot/Commands/Groups.cs +++ b/PluralKit.Bot/Commands/Groups.cs @@ -517,10 +517,10 @@ public class Groups return title.ToString(); } - public async Task ShowGroupCard(Context ctx, PKGroup target) + public async Task ShowGroupCard(Context ctx, PKGroup target, bool showEmbed = false) { var system = await GetGroupSystem(ctx, target); - if (ctx.MatchFlag("show-embed", "se")) + if (showEmbed) { await ctx.Reply(text: EmbedService.LEGACY_EMBED_WARNING, embed: await _embeds.CreateGroupEmbed(ctx, system, target)); return; diff --git a/PluralKit.Bot/Commands/Help.cs b/PluralKit.Bot/Commands/Help.cs index 333f4997..36aa73f7 100644 --- a/PluralKit.Bot/Commands/Help.cs +++ b/PluralKit.Bot/Commands/Help.cs @@ -7,9 +7,9 @@ namespace PluralKit.Bot; public class Help { - public Task HelpRoot(Context ctx) + public Task HelpRoot(Context ctx, bool showEmbed = false) { - if (ctx.MatchFlag("show-embed", "se")) + if (showEmbed) return HelpRootOld(ctx); return ctx.Reply(BuildComponents(ctx.Author.Id, Help.Description.Replace("{prefix}", ctx.DefaultPrefix), -1)); diff --git a/PluralKit.Bot/Commands/Member.cs b/PluralKit.Bot/Commands/Member.cs index 196ec25f..45010f34 100644 --- a/PluralKit.Bot/Commands/Member.cs +++ b/PluralKit.Bot/Commands/Member.cs @@ -120,10 +120,10 @@ public class Member await ctx.Reply(replyStr); } - public async Task ViewMember(Context ctx, PKMember target) + public async Task ViewMember(Context ctx, PKMember target, bool showEmbed = false) { var system = await ctx.Repository.GetSystem(target.System); - if (ctx.MatchFlag("show-embed", "se")) + if (showEmbed) { await ctx.Reply( text: EmbedService.LEGACY_EMBED_WARNING, diff --git a/PluralKit.Bot/Commands/Random.cs b/PluralKit.Bot/Commands/Random.cs index 4b0aa8a4..b16fff6e 100644 --- a/PluralKit.Bot/Commands/Random.cs +++ b/PluralKit.Bot/Commands/Random.cs @@ -15,7 +15,7 @@ public class Random // todo: get postgresql to return one random member/group instead of querying all members/groups - public async Task Member(Context ctx, PKSystem target) + public async Task Member(Context ctx, PKSystem target, bool showEmbed = false) { if (target == null) throw Errors.NoSystemError(ctx.DefaultPrefix); @@ -37,7 +37,7 @@ public class Random var randInt = randGen.Next(members.Count); - if (ctx.MatchFlag("show-embed", "se")) + if (showEmbed) { await ctx.Reply( text: EmbedService.LEGACY_EMBED_WARNING, @@ -49,7 +49,7 @@ public class Random components: await _embeds.CreateMemberMessageComponents(target, members[randInt], ctx.Guild, ctx.Config, ctx.LookupContextFor(target.Id), ctx.Zone)); } - public async Task Group(Context ctx, PKSystem target) + public async Task Group(Context ctx, PKSystem target, bool showEmbed = false) { if (target == null) throw Errors.NoSystemError(ctx.DefaultPrefix); @@ -70,7 +70,7 @@ public class Random var randInt = randGen.Next(groups.Count()); - if (ctx.MatchFlag("show-embed", "se")) + if (showEmbed) { await ctx.Reply( text: EmbedService.LEGACY_EMBED_WARNING, @@ -82,7 +82,7 @@ public class Random components: await _embeds.CreateGroupMessageComponents(ctx, target, groups.ToArray()[randInt])); } - public async Task GroupMember(Context ctx, PKGroup group) + public async Task GroupMember(Context ctx, PKGroup group, bool showEmbed = false) { ctx.CheckSystemPrivacy(group.System, group.ListPrivacy); @@ -112,7 +112,7 @@ public class Random var randInt = randGen.Next(ms.Count); - if (ctx.MatchFlag("show-embed", "se")) + if (showEmbed) { await ctx.Reply( text: EmbedService.LEGACY_EMBED_WARNING, diff --git a/PluralKit.Bot/Commands/System.cs b/PluralKit.Bot/Commands/System.cs index 1c50beac..f803abc8 100644 --- a/PluralKit.Bot/Commands/System.cs +++ b/PluralKit.Bot/Commands/System.cs @@ -14,16 +14,16 @@ public class System _embeds = embeds; } - public async Task Query(Context ctx, PKSystem system, bool all, bool @public, bool @private) + public async Task Query(Context ctx, PKSystem system, bool all, bool @public, bool @private, bool showEmbed = false) { if (system == null) throw Errors.NoSystemError(ctx.DefaultPrefix); - if (ctx.MatchFlag("show-embed", "se")) + if (showEmbed) { await ctx.Reply(text: EmbedService.LEGACY_EMBED_WARNING, embed: await _embeds.CreateSystemEmbed(ctx, system, ctx.LookupContextFor(system.Id), all)); return; } - await ctx.Reply(components: await _embeds.CreateSystemMessageComponents(ctx, system, ctx.LookupContextFor(system.Id))); + await ctx.Reply(components: await _embeds.CreateSystemMessageComponents(ctx, system, ctx.LookupContextFor(system.Id), all)); } public async Task New(Context ctx, string? systemName) diff --git a/PluralKit.Bot/Services/EmbedService.cs b/PluralKit.Bot/Services/EmbedService.cs index 1a5cfdf2..4cf5158f 100644 --- a/PluralKit.Bot/Services/EmbedService.cs +++ b/PluralKit.Bot/Services/EmbedService.cs @@ -43,7 +43,7 @@ public class EmbedService return Task.WhenAll(ids.Select(Inner)); } - public async Task CreateSystemMessageComponents(Context cctx, PKSystem system, LookupContext ctx) + public async Task CreateSystemMessageComponents(Context cctx, PKSystem system, LookupContext ctx, bool countctxByOwner) { // Fetch/render info for all accounts simultaneously var accounts = await _repo.GetSystemAccounts(system.Id); @@ -55,7 +55,7 @@ public class EmbedService }; var countctx = LookupContext.ByNonOwner; - if (cctx.MatchFlag("a", "all")) + if (countctxByOwner) { if (system.Id == cctx.System?.Id) countctx = LookupContext.ByOwner; diff --git a/crates/command_definitions/src/lib.rs b/crates/command_definitions/src/lib.rs index 28715d5f..500499de 100644 --- a/crates/command_definitions/src/lib.rs +++ b/crates/command_definitions/src/lib.rs @@ -27,7 +27,11 @@ pub fn all() -> impl Iterator { .chain(config::cmds()) .chain(fun::cmds()) .chain(switch::cmds()) - .map(|cmd| cmd.flag(("plaintext", ["pt"])).flag(("raw", ["r"]))) + .map(|cmd| { + cmd.flag(("plaintext", ["pt"])) + .flag(("raw", ["r"])) + .flag(("show-embed", ["se"])) + }) } pub const RESET: (&str, [&str; 2]) = ("reset", ["clear", "default"]); From c92c3f84f0c0a667dbbcf7c3cd558a15c4d3fb01 Mon Sep 17 00:00:00 2001 From: dusk Date: Fri, 26 Sep 2025 23:56:49 +0000 Subject: [PATCH 096/179] implement random commands, dont keep the subcommands only the flags --- PluralKit.Bot/CommandMeta/CommandTree.cs | 19 +++++++++---------- .../Context/ContextEntityArgumentsExt.cs | 13 +++++++------ .../Context/ContextParametersExt.cs | 8 ++++++++ PluralKit.Bot/CommandSystem/Parameters.cs | 6 ++++++ PluralKit.Bot/Commands/Random.cs | 12 ++++++------ crates/command_definitions/src/group.rs | 10 ++++++++++ crates/command_definitions/src/lib.rs | 1 + crates/command_definitions/src/random.rs | 13 +++++++++++++ crates/command_definitions/src/system.rs | 14 ++++++++++++-- crates/command_parser/src/parameter.rs | 5 +++++ crates/commands/src/bin/write_cs_glue.rs | 2 ++ crates/commands/src/commands.udl | 1 + crates/commands/src/lib.rs | 2 ++ 13 files changed, 82 insertions(+), 24 deletions(-) diff --git a/PluralKit.Bot/CommandMeta/CommandTree.cs b/PluralKit.Bot/CommandMeta/CommandTree.cs index 1f546baa..b542a87a 100644 --- a/PluralKit.Bot/CommandMeta/CommandTree.cs +++ b/PluralKit.Bot/CommandMeta/CommandTree.cs @@ -162,6 +162,15 @@ public partial class CommandTree Commands.SystemFronter(var param, var flags) => ctx.Execute(SystemFronter, m => m.Fronter(ctx, param.target)), Commands.SystemFronterHistory(var param, var flags) => ctx.Execute(SystemFrontHistory, m => m.FrontHistory(ctx, param.target, flags.clear)), Commands.SystemFronterPercent(var param, var flags) => ctx.Execute(SystemFrontPercent, m => m.FrontPercent(ctx, param.target, flags.duration, flags.fronters_only, flags.flat)), + Commands.RandomSelf(_, var flags) => + flags.group + ? ctx.Execute(GroupRandom, m => m.Group(ctx, ctx.System, flags.all, flags.show_embed)) + : ctx.Execute(MemberRandom, m => m.Member(ctx, ctx.System, flags.all, flags.show_embed)), + Commands.SystemRandom(var param, var flags) => + flags.group + ? ctx.Execute(GroupRandom, m => m.Group(ctx, param.target, flags.all, flags.show_embed)) + : ctx.Execute(MemberRandom, m => m.Member(ctx, param.target, flags.all, flags.show_embed)), + Commands.GroupRandomMember(var param, var flags) => ctx.Execute(GroupMemberRandom, m => m.GroupMember(ctx, param.target, flags.all, flags.show_embed)), _ => // this should only ever occur when deving if commands are not implemented... ctx.Reply( @@ -252,11 +261,6 @@ public partial class CommandTree return HandleDebugCommand(ctx); if (ctx.Match("admin")) return HandleAdminCommand(ctx); - if (ctx.Match("random", "rand", "r")) - if (ctx.Match("group", "g") || ctx.MatchFlag("group", "g")) - return ctx.Execute(GroupRandom, r => r.Group(ctx, ctx.System)); - else - return ctx.Execute(MemberRandom, m => m.Member(ctx, ctx.System)); if (ctx.Match("dashboard", "dash")) return ctx.Execute(Dashboard, m => m.Dashboard(ctx)); } @@ -416,11 +420,6 @@ public partial class CommandTree await ctx.CheckSystem(target).Execute(GroupList, g => g.ListSystemGroups(ctx, target)); else if (ctx.Match("id")) await ctx.CheckSystem(target).Execute(SystemId, m => m.DisplayId(ctx, target)); - else if (ctx.Match("random", "rand", "r")) - if (ctx.Match("group", "g") || ctx.MatchFlag("group", "g")) - await ctx.CheckSystem(target).Execute(GroupRandom, r => r.Group(ctx, target)); - else - await ctx.CheckSystem(target).Execute(MemberRandom, m => m.Member(ctx, target)); } private async Task HandleMemberCommand(Context ctx) diff --git a/PluralKit.Bot/CommandSystem/Context/ContextEntityArgumentsExt.cs b/PluralKit.Bot/CommandSystem/Context/ContextEntityArgumentsExt.cs index b1cddf17..bf0fe27b 100644 --- a/PluralKit.Bot/CommandSystem/Context/ContextEntityArgumentsExt.cs +++ b/PluralKit.Bot/CommandSystem/Context/ContextEntityArgumentsExt.cs @@ -147,13 +147,9 @@ public static class ContextEntityArgumentsExt return member; } - public static async Task PeekGroup(this Context ctx, SystemId? restrictToSystem = null) + public static async Task ParseGroup(this Context ctx, string input, bool byId, SystemId? restrictToSystem = null) { - var input = ctx.PeekArgument(); - - // see PeekMember for an explanation of the logic used here - - if (ctx.System != null && !ctx.MatchFlag("id", "by-id")) + if (ctx.System != null && !byId) { if (await ctx.Repository.GetGroupByName(ctx.System.Id, input) is { } byName) return byName; @@ -170,6 +166,11 @@ public static class ContextEntityArgumentsExt return null; } + public static async Task PeekGroup(this Context ctx, SystemId? restrictToSystem = null) + { + throw new NotImplementedException(); + } + public static async Task MatchGroup(this Context ctx, SystemId? restrictToSystem = null) { var group = await ctx.PeekGroup(restrictToSystem); diff --git a/PluralKit.Bot/CommandSystem/Context/ContextParametersExt.cs b/PluralKit.Bot/CommandSystem/Context/ContextParametersExt.cs index 9f0c3f22..13b0ce99 100644 --- a/PluralKit.Bot/CommandSystem/Context/ContextParametersExt.cs +++ b/PluralKit.Bot/CommandSystem/Context/ContextParametersExt.cs @@ -28,6 +28,14 @@ public static class ContextParametersExt ); } + public static async Task ParamResolveGroup(this Context ctx, string param_name) + { + return await ctx.Parameters.ResolveParameter( + ctx, param_name, + param => (param as Parameter.GroupRef)?.group + ); + } + public static async Task ParamResolveSystem(this Context ctx, string param_name) { return await ctx.Parameters.ResolveParameter( diff --git a/PluralKit.Bot/CommandSystem/Parameters.cs b/PluralKit.Bot/CommandSystem/Parameters.cs index 2c753fde..d3331de1 100644 --- a/PluralKit.Bot/CommandSystem/Parameters.cs +++ b/PluralKit.Bot/CommandSystem/Parameters.cs @@ -10,6 +10,7 @@ public abstract record Parameter() { public record MemberRef(PKMember member): Parameter; public record MemberRefs(List members): Parameter; + public record GroupRef(PKGroup group): Parameter; public record SystemRef(PKSystem system): Parameter; public record GuildRef(Guild guild): Parameter; public record MemberPrivacyTarget(MemberPrivacySubject target): Parameter; @@ -73,6 +74,11 @@ public class Parameters ?? throw new PKError(ctx.CreateNotFoundError("Member", m, byId)) ).ToListAsync() ); + case uniffi.commands.Parameter.GroupRef groupRef: + return new Parameter.GroupRef( + await ctx.ParseGroup(groupRef.group, byId) + ?? throw new PKError(ctx.CreateNotFoundError("Group", groupRef.group)) + ); case uniffi.commands.Parameter.SystemRef systemRef: // todo: do we need byId here? return new Parameter.SystemRef( diff --git a/PluralKit.Bot/Commands/Random.cs b/PluralKit.Bot/Commands/Random.cs index b16fff6e..7c451afa 100644 --- a/PluralKit.Bot/Commands/Random.cs +++ b/PluralKit.Bot/Commands/Random.cs @@ -15,7 +15,7 @@ public class Random // todo: get postgresql to return one random member/group instead of querying all members/groups - public async Task Member(Context ctx, PKSystem target, bool showEmbed = false) + public async Task Member(Context ctx, PKSystem target, bool all, bool showEmbed = false) { if (target == null) throw Errors.NoSystemError(ctx.DefaultPrefix); @@ -24,7 +24,7 @@ public class Random var members = await ctx.Repository.GetSystemMembers(target.Id).ToListAsync(); - if (!ctx.MatchFlag("all", "a")) + if (!all) members = members.Where(m => m.MemberVisibility == PrivacyLevel.Public).ToList(); else ctx.CheckOwnSystem(target); @@ -49,7 +49,7 @@ public class Random components: await _embeds.CreateMemberMessageComponents(target, members[randInt], ctx.Guild, ctx.Config, ctx.LookupContextFor(target.Id), ctx.Zone)); } - public async Task Group(Context ctx, PKSystem target, bool showEmbed = false) + public async Task Group(Context ctx, PKSystem target, bool all, bool showEmbed = false) { if (target == null) throw Errors.NoSystemError(ctx.DefaultPrefix); @@ -57,7 +57,7 @@ public class Random ctx.CheckSystemPrivacy(target.Id, target.GroupListPrivacy); var groups = await ctx.Repository.GetSystemGroups(target.Id).ToListAsync(); - if (!ctx.MatchFlag("all", "a")) + if (!all) groups = groups.Where(g => g.Visibility == PrivacyLevel.Public).ToList(); else ctx.CheckOwnSystem(target); @@ -82,7 +82,7 @@ public class Random components: await _embeds.CreateGroupMessageComponents(ctx, target, groups.ToArray()[randInt])); } - public async Task GroupMember(Context ctx, PKGroup group, bool showEmbed = false) + public async Task GroupMember(Context ctx, PKGroup group, bool all, bool showEmbed = false) { ctx.CheckSystemPrivacy(group.System, group.ListPrivacy); @@ -96,7 +96,7 @@ public class Random "This group has no members!" + (ctx.System?.Id == group.System ? " Please add at least one member to this group before using this command." : "")); - if (!ctx.MatchFlag("all", "a")) + if (!all) members = members.Where(g => g.MemberVisibility == PrivacyLevel.Public); else ctx.CheckOwnGroup(group); diff --git a/crates/command_definitions/src/group.rs b/crates/command_definitions/src/group.rs index 8b137891..44847ee6 100644 --- a/crates/command_definitions/src/group.rs +++ b/crates/command_definitions/src/group.rs @@ -1 +1,11 @@ +use command_parser::token::TokensIterator; +use super::*; + +pub fn group() -> (&'static str, [&'static str; 1]) { + ("group", ["g"]) +} + +pub fn targeted() -> TokensIterator { + tokens!(group(), GroupRef) +} diff --git a/crates/command_definitions/src/lib.rs b/crates/command_definitions/src/lib.rs index 500499de..f8deb48b 100644 --- a/crates/command_definitions/src/lib.rs +++ b/crates/command_definitions/src/lib.rs @@ -27,6 +27,7 @@ pub fn all() -> impl Iterator { .chain(config::cmds()) .chain(fun::cmds()) .chain(switch::cmds()) + .chain(random::cmds()) .map(|cmd| { cmd.flag(("plaintext", ["pt"])) .flag(("raw", ["r"])) diff --git a/crates/command_definitions/src/random.rs b/crates/command_definitions/src/random.rs index 8b137891..2f48c9f0 100644 --- a/crates/command_definitions/src/random.rs +++ b/crates/command_definitions/src/random.rs @@ -1 +1,14 @@ +use super::*; +pub fn cmds() -> impl Iterator { + let random = ("random", ["rand"]); + let group = group::group(); + + [ + command!(random => "random_self").flag(group), + command!(system::targeted(), random => "system_random").flag(group), + command!(group::targeted(), random => "group_random_member"), + ] + .into_iter() + .map(|cmd| cmd.flag(("all", ["a"]))) +} diff --git a/crates/command_definitions/src/system.rs b/crates/command_definitions/src/system.rs index 7de7e274..e6fd5b66 100644 --- a/crates/command_definitions/src/system.rs +++ b/crates/command_definitions/src/system.rs @@ -1,12 +1,22 @@ +use command_parser::token::TokensIterator; + use super::*; pub fn cmds() -> impl Iterator { edit() } +pub fn system() -> (&'static str, [&'static str; 1]) { + ("system", ["s"]) +} + +pub fn targeted() -> TokensIterator { + tokens!(system(), SystemRef) +} + pub fn edit() -> impl Iterator { - let system = ("system", ["s"]); - let system_target = tokens!(system, SystemRef); + let system = system(); + let system_target = targeted(); let system_new = tokens!(system, ("new", ["n"])); let system_new_cmd = [ diff --git a/crates/command_parser/src/parameter.rs b/crates/command_parser/src/parameter.rs index 08971d99..0c38f532 100644 --- a/crates/command_parser/src/parameter.rs +++ b/crates/command_parser/src/parameter.rs @@ -12,6 +12,7 @@ pub enum ParameterValue { OpaqueString(String), MemberRef(String), MemberRefs(Vec), + GroupRef(String), SystemRef(String), GuildRef(String), MemberPrivacyTarget(String), @@ -48,6 +49,7 @@ impl Display for Parameter { } ParameterKind::MemberRef => write!(f, ""), ParameterKind::MemberRefs => write!(f, " ..."), + ParameterKind::GroupRef => write!(f, ""), ParameterKind::SystemRef => write!(f, ""), ParameterKind::GuildRef => write!(f, ""), ParameterKind::MemberPrivacyTarget => write!(f, ""), @@ -83,6 +85,7 @@ pub enum ParameterKind { OpaqueStringRemainder, MemberRef, MemberRefs, + GroupRef, SystemRef, GuildRef, MemberPrivacyTarget, @@ -99,6 +102,7 @@ impl ParameterKind { ParameterKind::OpaqueStringRemainder => "string", ParameterKind::MemberRef => "target", ParameterKind::MemberRefs => "targets", + ParameterKind::GroupRef => "target", ParameterKind::SystemRef => "target", ParameterKind::GuildRef => "target", ParameterKind::MemberPrivacyTarget => "member_privacy_target", @@ -122,6 +126,7 @@ impl ParameterKind { ParameterKind::OpaqueString | ParameterKind::OpaqueStringRemainder => { Ok(ParameterValue::OpaqueString(input.into())) } + ParameterKind::GroupRef => Ok(ParameterValue::GroupRef(input.into())), ParameterKind::MemberRef => Ok(ParameterValue::MemberRef(input.into())), ParameterKind::MemberRefs => Ok(ParameterValue::MemberRefs( input.split(' ').map(|s| s.trim().to_string()).collect(), diff --git a/crates/commands/src/bin/write_cs_glue.rs b/crates/commands/src/bin/write_cs_glue.rs index 0559de88..1d0e61df 100644 --- a/crates/commands/src/bin/write_cs_glue.rs +++ b/crates/commands/src/bin/write_cs_glue.rs @@ -168,6 +168,7 @@ fn get_param_ty(kind: ParameterKind) -> &'static str { ParameterKind::OpaqueString | ParameterKind::OpaqueStringRemainder => "string", ParameterKind::MemberRef => "PKMember", ParameterKind::MemberRefs => "List", + ParameterKind::GroupRef => "PKGroup", ParameterKind::SystemRef => "PKSystem", ParameterKind::MemberPrivacyTarget => "MemberPrivacySubject", ParameterKind::SystemPrivacyTarget => "SystemPrivacySubject", @@ -183,6 +184,7 @@ fn get_param_param_ty(kind: ParameterKind) -> &'static str { ParameterKind::OpaqueString | ParameterKind::OpaqueStringRemainder => "Opaque", ParameterKind::MemberRef => "Member", ParameterKind::MemberRefs => "Members", + ParameterKind::GroupRef => "Group", ParameterKind::SystemRef => "System", ParameterKind::MemberPrivacyTarget => "MemberPrivacyTarget", ParameterKind::SystemPrivacyTarget => "SystemPrivacyTarget", diff --git a/crates/commands/src/commands.udl b/crates/commands/src/commands.udl index 1a8da927..15e9849c 100644 --- a/crates/commands/src/commands.udl +++ b/crates/commands/src/commands.udl @@ -10,6 +10,7 @@ interface CommandResult { interface Parameter { MemberRef(string member); MemberRefs(sequence members); + GroupRef(string group); SystemRef(string system); GuildRef(string guild); MemberPrivacyTarget(string target); diff --git a/crates/commands/src/lib.rs b/crates/commands/src/lib.rs index bc79e0ca..368cb81f 100644 --- a/crates/commands/src/lib.rs +++ b/crates/commands/src/lib.rs @@ -24,6 +24,7 @@ pub enum CommandResult { pub enum Parameter { MemberRef { member: String }, MemberRefs { members: Vec }, + GroupRef { group: String }, SystemRef { system: String }, GuildRef { guild: String }, MemberPrivacyTarget { target: String }, @@ -39,6 +40,7 @@ impl From for Parameter { match value { ParameterValue::MemberRef(member) => Self::MemberRef { member }, ParameterValue::MemberRefs(members) => Self::MemberRefs { members }, + ParameterValue::GroupRef(group) => Self::GroupRef { group }, ParameterValue::SystemRef(system) => Self::SystemRef { system }, ParameterValue::MemberPrivacyTarget(target) => Self::MemberPrivacyTarget { target }, ParameterValue::SystemPrivacyTarget(target) => Self::SystemPrivacyTarget { target }, From 6d6dcc389f4864397ea9da643ea707c88d3cd0f7 Mon Sep 17 00:00:00 2001 From: dusk Date: Sat, 27 Sep 2025 00:21:34 +0000 Subject: [PATCH 097/179] add hidden flags that dont show up in suggestions, mainly for global flags --- crates/command_definitions/src/lib.rs | 6 ++-- crates/command_parser/src/command.rs | 49 +++++++++++++++++---------- 2 files changed, 35 insertions(+), 20 deletions(-) diff --git a/crates/command_definitions/src/lib.rs b/crates/command_definitions/src/lib.rs index f8deb48b..34f3b4d9 100644 --- a/crates/command_definitions/src/lib.rs +++ b/crates/command_definitions/src/lib.rs @@ -29,9 +29,9 @@ pub fn all() -> impl Iterator { .chain(switch::cmds()) .chain(random::cmds()) .map(|cmd| { - cmd.flag(("plaintext", ["pt"])) - .flag(("raw", ["r"])) - .flag(("show-embed", ["se"])) + cmd.hidden_flag(("plaintext", ["pt"])) + .hidden_flag(("raw", ["r"])) + .hidden_flag(("show-embed", ["se"])) }) } diff --git a/crates/command_parser/src/command.rs b/crates/command_parser/src/command.rs index 6810a82d..a8376cf2 100644 --- a/crates/command_parser/src/command.rs +++ b/crates/command_parser/src/command.rs @@ -1,4 +1,7 @@ -use std::fmt::{Debug, Display}; +use std::{ + collections::HashSet, + fmt::{Debug, Display}, +}; use smol_str::SmolStr; @@ -13,6 +16,7 @@ pub struct Command { pub cb: SmolStr, pub show_in_suggestions: bool, pub parse_flags_before: usize, + pub hidden_flags: HashSet, } impl Command { @@ -22,19 +26,11 @@ impl Command { // figure out which token to parse / put flags after // (by default, put flags after the last token) let mut parse_flags_before = tokens.len(); - let mut was_parameter = true; for (idx, token) in tokens.iter().enumerate().rev() { match token { // we want flags to go before any parameters - Token::Parameter(_) => { - parse_flags_before = idx; - was_parameter = true; - } - Token::Value { .. } => { - if was_parameter { - break; - } - } + Token::Parameter(_) => parse_flags_before = idx, + Token::Value { .. } => break, } } Self { @@ -44,6 +40,7 @@ impl Command { show_in_suggestions: true, parse_flags_before, tokens, + hidden_flags: HashSet::new(), } } @@ -61,15 +58,35 @@ impl Command { self.flags.push(flag.into()); self } + + pub fn hidden_flag(mut self, flag: impl Into) -> Self { + let flag = flag.into(); + self.hidden_flags.insert(flag.get_name().into()); + self.flags.push(flag); + self + } } impl Display for Command { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let write_flags = |f: &mut std::fmt::Formatter<'_>, space: bool| { + for flag in &self.flags { + if self.hidden_flags.contains(flag.get_name()) { + continue; + } + write!( + f, + "{}[{flag}]{}", + space.then_some(" ").unwrap_or(""), + space.then_some("").unwrap_or(" ") + )?; + } + std::fmt::Result::Ok(()) + }; + for (idx, token) in self.tokens.iter().enumerate() { if idx == self.parse_flags_before { - for flag in &self.flags { - write!(f, "[{flag}] ")?; - } + write_flags(f, false)?; } write!( f, @@ -78,9 +95,7 @@ impl Display for Command { )?; } if self.tokens.len() == self.parse_flags_before { - for flag in &self.flags { - write!(f, " [{flag}]")?; - } + write_flags(f, true)?; } Ok(()) } From 228a177ea3d5b056ec9d58559d84454258db72a0 Mon Sep 17 00:00:00 2001 From: dusk Date: Sun, 28 Sep 2025 15:03:28 +0000 Subject: [PATCH 098/179] implement system link / unlink cmds --- PluralKit.Bot/CommandMeta/CommandTree.cs | 6 ++---- PluralKit.Bot/Commands/SystemLink.cs | 4 ++-- crates/command_definitions/src/system.rs | 7 +++++++ 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/PluralKit.Bot/CommandMeta/CommandTree.cs b/PluralKit.Bot/CommandMeta/CommandTree.cs index b542a87a..f16e4360 100644 --- a/PluralKit.Bot/CommandMeta/CommandTree.cs +++ b/PluralKit.Bot/CommandMeta/CommandTree.cs @@ -171,6 +171,8 @@ public partial class CommandTree ? ctx.Execute(GroupRandom, m => m.Group(ctx, param.target, flags.all, flags.show_embed)) : ctx.Execute(MemberRandom, m => m.Member(ctx, param.target, flags.all, flags.show_embed)), Commands.GroupRandomMember(var param, var flags) => ctx.Execute(GroupMemberRandom, m => m.GroupMember(ctx, param.target, flags.all, flags.show_embed)), + Commands.SystemLink => ctx.Execute(Link, m => m.LinkSystem(ctx)), + Commands.SystemUnlink(var param, _) => ctx.Execute(Unlink, m => m.UnlinkAccount(ctx, param.target)), _ => // this should only ever occur when deving if commands are not implemented... ctx.Reply( @@ -194,10 +196,6 @@ public partial class CommandTree return HandleServerConfigCommand(ctx); if (ctx.Match("list", "find", "members", "search", "query", "l", "f", "fd", "ls")) return ctx.Execute(SystemList, m => m.MemberList(ctx, ctx.System)); - if (ctx.Match("link")) - return ctx.Execute(Link, m => m.LinkSystem(ctx)); - if (ctx.Match("unlink")) - return ctx.Execute(Unlink, m => m.UnlinkAccount(ctx)); if (ctx.Match("token")) if (ctx.Match("refresh", "renew", "invalidate", "reroll", "regen")) return ctx.Execute(TokenRefresh, m => m.RefreshToken(ctx)); diff --git a/PluralKit.Bot/Commands/SystemLink.cs b/PluralKit.Bot/Commands/SystemLink.cs index df0743fb..8b4b1881 100644 --- a/PluralKit.Bot/Commands/SystemLink.cs +++ b/PluralKit.Bot/Commands/SystemLink.cs @@ -26,12 +26,12 @@ public class SystemLink await ctx.Reply($"{Emojis.Success} Account linked to system."); } - public async Task UnlinkAccount(Context ctx) + public async Task UnlinkAccount(Context ctx, string idRaw) { ctx.CheckSystem(); ulong id; - if (!ctx.MatchUserRaw(out id)) + if (!idRaw.TryParseMention(out id)) throw new PKSyntaxError("You must pass an account to unlink from (either ID or @mention)."); var accountIds = (await ctx.Repository.GetSystemAccounts(ctx.System.Id)).ToList(); diff --git a/crates/command_definitions/src/system.rs b/crates/command_definitions/src/system.rs index e6fd5b66..7de89f84 100644 --- a/crates/command_definitions/src/system.rs +++ b/crates/command_definitions/src/system.rs @@ -245,6 +245,12 @@ pub fn edit() -> impl Iterator { ] .into_iter(); + let system_link = [ + command!("link" => "system_link"), + command!("unlink", ("target", OpaqueString) => "system_unlink"), + ] + .into_iter(); + system_new_cmd .chain(system_name_self_cmd) .chain(system_server_name_self_cmd) @@ -271,4 +277,5 @@ pub fn edit() -> impl Iterator { .chain(system_banner_cmd) .chain(system_info_cmd) .chain(system_front_cmd) + .chain(system_link) } From 3e7898e5cc9487292ffe34348d5e0f238a951cc4 Mon Sep 17 00:00:00 2001 From: dusk Date: Sun, 28 Sep 2025 15:34:47 +0000 Subject: [PATCH 099/179] implement members list, search later --- PluralKit.Bot/CommandMeta/CommandTree.cs | 8 ++++---- crates/command_definitions/src/member.rs | 17 +++++++++++++++-- crates/command_definitions/src/system.rs | 4 ++++ 3 files changed, 23 insertions(+), 6 deletions(-) diff --git a/PluralKit.Bot/CommandMeta/CommandTree.cs b/PluralKit.Bot/CommandMeta/CommandTree.cs index f16e4360..036df4bb 100644 --- a/PluralKit.Bot/CommandMeta/CommandTree.cs +++ b/PluralKit.Bot/CommandMeta/CommandTree.cs @@ -173,6 +173,8 @@ public partial class CommandTree Commands.GroupRandomMember(var param, var flags) => ctx.Execute(GroupMemberRandom, m => m.GroupMember(ctx, param.target, flags.all, flags.show_embed)), Commands.SystemLink => ctx.Execute(Link, m => m.LinkSystem(ctx)), Commands.SystemUnlink(var param, _) => ctx.Execute(Unlink, m => m.UnlinkAccount(ctx, param.target)), + Commands.MembersList => ctx.Execute(SystemList, m => m.MemberList(ctx, ctx.System)), + Commands.SystemMembersList(var param, _) => ctx.Execute(SystemList, m => m.MemberList(ctx, param.target)), _ => // this should only ever occur when deving if commands are not implemented... ctx.Reply( @@ -410,10 +412,8 @@ public partial class CommandTree private async Task HandleSystemCommandTargeted(Context ctx, PKSystem target) { - if (ctx.Match("list", "l", "members", "ls")) - await ctx.CheckSystem(target).Execute(SystemList, m => m.MemberList(ctx, target)); - else if (ctx.Match("find", "search", "query", "fd", "s")) - await ctx.CheckSystem(target).Execute(SystemFind, m => m.MemberList(ctx, target)); + if (ctx.Match("find", "search", "query", "fd", "s")) + await ctx.CheckSystem(target).Execute(SystemFind, m => m.MemberList(ctx, target)); // TODO: this lmao (ParseListOptions) else if (ctx.Match("groups", "gs")) await ctx.CheckSystem(target).Execute(GroupList, g => g.ListSystemGroups(ctx, target)); else if (ctx.Match("id")) diff --git a/crates/command_definitions/src/member.rs b/crates/command_definitions/src/member.rs index 7c070773..14ad283b 100644 --- a/crates/command_definitions/src/member.rs +++ b/crates/command_definitions/src/member.rs @@ -1,8 +1,18 @@ +use command_parser::token::TokensIterator; + use super::*; +pub fn member() -> (&'static str, [&'static str; 1]) { + ("member", ["m"]) +} + +pub fn targetted() -> TokensIterator { + tokens!(member(), MemberRef) +} + pub fn cmds() -> impl Iterator { - let member = ("member", ["m"]); - let member_target = tokens!(member, MemberRef); + let member = member(); + let member_target = targetted(); let name = ("name", ["n"]); let description = ("description", ["desc"]); @@ -288,7 +298,10 @@ pub fn cmds() -> impl Iterator { [command!(member_target, "soulscream" => "member_soulscream").show_in_suggestions(false)] .into_iter(); + let member_list = [command!(member, "list" => "members_list")].into_iter(); + member_new_cmd + .chain(member_list) .chain(member_info_cmd) .chain(member_name_cmd) .chain(member_description_cmd) diff --git a/crates/command_definitions/src/system.rs b/crates/command_definitions/src/system.rs index 7de89f84..fcb3c20f 100644 --- a/crates/command_definitions/src/system.rs +++ b/crates/command_definitions/src/system.rs @@ -251,6 +251,9 @@ pub fn edit() -> impl Iterator { ] .into_iter(); + let system_list = + [command!(system_target, ("members", ["list"]) => "system_members_list")].into_iter(); + system_new_cmd .chain(system_name_self_cmd) .chain(system_server_name_self_cmd) @@ -278,4 +281,5 @@ pub fn edit() -> impl Iterator { .chain(system_info_cmd) .chain(system_front_cmd) .chain(system_link) + .chain(system_list) } From 95fc7e9f60d4047e5d736ea116ddcb5fdc9e8587 Mon Sep 17 00:00:00 2001 From: dusk Date: Tue, 30 Sep 2025 18:45:35 +0000 Subject: [PATCH 100/179] implement parse list options and related commands --- PluralKit.Bot/CommandMeta/CommandTree.cs | 142 +++++++++--------- PluralKit.Bot/Commands/GroupMember.cs | 10 +- PluralKit.Bot/Commands/Groups.cs | 6 +- .../Commands/Lists/ContextListExt.cs | 92 +----------- PluralKit.Bot/Commands/Lists/ListOptions.cs | 1 + PluralKit.Bot/Commands/Random.cs | 8 +- PluralKit.Bot/Commands/SystemList.cs | 8 +- crates/command_definitions/src/group.rs | 31 +++- crates/command_definitions/src/lib.rs | 3 + crates/command_definitions/src/member.rs | 16 +- crates/command_definitions/src/random.rs | 4 +- crates/command_definitions/src/system.rs | 35 ++++- crates/command_definitions/src/utils.rs | 52 +++++++ crates/command_parser/src/command.rs | 45 ++++-- crates/command_parser/src/flag.rs | 16 +- crates/command_parser/src/lib.rs | 2 +- crates/commands/src/bin/write_cs_glue.rs | 93 +++++++++++- crates/commands/src/main.rs | 2 +- 18 files changed, 367 insertions(+), 199 deletions(-) create mode 100644 crates/command_definitions/src/utils.rs diff --git a/PluralKit.Bot/CommandMeta/CommandTree.cs b/PluralKit.Bot/CommandMeta/CommandTree.cs index 036df4bb..85cf1ac2 100644 --- a/PluralKit.Bot/CommandMeta/CommandTree.cs +++ b/PluralKit.Bot/CommandMeta/CommandTree.cs @@ -170,11 +170,21 @@ public partial class CommandTree flags.group ? ctx.Execute(GroupRandom, m => m.Group(ctx, param.target, flags.all, flags.show_embed)) : ctx.Execute(MemberRandom, m => m.Member(ctx, param.target, flags.all, flags.show_embed)), - Commands.GroupRandomMember(var param, var flags) => ctx.Execute(GroupMemberRandom, m => m.GroupMember(ctx, param.target, flags.all, flags.show_embed)), + Commands.GroupRandomMember(var param, var flags) => ctx.Execute(GroupMemberRandom, m => m.GroupMember(ctx, param.target, flags)), Commands.SystemLink => ctx.Execute(Link, m => m.LinkSystem(ctx)), Commands.SystemUnlink(var param, _) => ctx.Execute(Unlink, m => m.UnlinkAccount(ctx, param.target)), - Commands.MembersList => ctx.Execute(SystemList, m => m.MemberList(ctx, ctx.System)), - Commands.SystemMembersList(var param, _) => ctx.Execute(SystemList, m => m.MemberList(ctx, param.target)), + Commands.SystemMembersListSelf(var param, var flags) => ctx.Execute(SystemList, m => m.MemberList(ctx, ctx.System, null, flags)), + Commands.SystemMembersSearchSelf(var param, var flags) => ctx.Execute(SystemFind, m => m.MemberList(ctx, ctx.System, param.query, flags)), + Commands.SystemMembersList(var param, var flags) => ctx.Execute(SystemList, m => m.MemberList(ctx, param.target, null, flags)), + Commands.SystemMembersSearch(var param, var flags) => ctx.Execute(SystemFind, m => m.MemberList(ctx, param.target, param.query, flags)), + Commands.MemberListGroups(var param, var flags) => ctx.Execute(MemberGroups, m => m.ListMemberGroups(ctx, param.target, null, flags)), + Commands.MemberSearchGroups(var param, var flags) => ctx.Execute(MemberGroups, m => m.ListMemberGroups(ctx, param.target, param.query, flags)), + Commands.GroupListMembers(var param, var flags) => ctx.Execute(GroupMemberList, m => m.ListGroupMembers(ctx, param.target, null, flags)), + Commands.GroupSearchMembers(var param, var flags) => ctx.Execute(GroupMemberList, m => m.ListGroupMembers(ctx, param.target, param.query, flags)), + Commands.SystemListGroups(var param, var flags) => ctx.Execute(GroupList, g => g.ListSystemGroups(ctx, param.target, null, flags)), + Commands.SystemSearchGroups(var param, var flags) => ctx.Execute(GroupList, g => g.ListSystemGroups(ctx, param.target, param.query, flags)), + Commands.GroupListGroups(var param, var flags) => ctx.Execute(GroupList, g => g.ListSystemGroups(ctx, ctx.System, null, flags)), + Commands.GroupSearchGroups(var param, var flags) => ctx.Execute(GroupList, g => g.ListSystemGroups(ctx, ctx.System, param.query, flags)), _ => // this should only ever occur when deving if commands are not implemented... ctx.Reply( @@ -196,8 +206,6 @@ public partial class CommandTree return HandleConfigCommand(ctx); if (ctx.Match("serverconfig", "guildconfig", "scfg")) return HandleServerConfigCommand(ctx); - if (ctx.Match("list", "find", "members", "search", "query", "l", "f", "fd", "ls")) - return ctx.Execute(SystemList, m => m.MemberList(ctx, ctx.System)); if (ctx.Match("token")) if (ctx.Match("refresh", "renew", "invalidate", "reroll", "regen")) return ctx.Execute(TokenRefresh, m => m.RefreshToken(ctx)); @@ -412,20 +420,13 @@ public partial class CommandTree private async Task HandleSystemCommandTargeted(Context ctx, PKSystem target) { - if (ctx.Match("find", "search", "query", "fd", "s")) - await ctx.CheckSystem(target).Execute(SystemFind, m => m.MemberList(ctx, target)); // TODO: this lmao (ParseListOptions) - else if (ctx.Match("groups", "gs")) - await ctx.CheckSystem(target).Execute(GroupList, g => g.ListSystemGroups(ctx, target)); - else if (ctx.Match("id")) + if (ctx.Match("id")) await ctx.CheckSystem(target).Execute(SystemId, m => m.DisplayId(ctx, target)); } private async Task HandleMemberCommand(Context ctx) { - // TODO: implement - if (ctx.Match("list")) - await ctx.Execute(SystemList, m => m.MemberList(ctx, ctx.System)); - else if (ctx.Match("commands", "help")) + if (ctx.Match("commands", "help")) await PrintCommandList(ctx, "members", MemberCommands); else if (await ctx.MatchMember() is PKMember target) await HandleMemberCommandTargeted(ctx, target); @@ -447,72 +448,63 @@ public partial class CommandTree else if (ctx.Match("remove", "rem")) await ctx.Execute(MemberGroupRemove, m => m.AddRemoveGroups(ctx, target, Groups.AddRemoveOperation.Remove)); + else if (ctx.Match("id")) + await ctx.Execute(MemberId, m => m.DisplayId(ctx, target)); else - await ctx.Execute(MemberGroups, m => m.ListMemberGroups(ctx, target)); - else if (ctx.Match("id")) - await ctx.Execute(MemberId, m => m.DisplayId(ctx, target)); - else - await PrintCommandNotFoundError(ctx, MemberInfo, MemberRename, MemberDisplayName, MemberServerName, - MemberDesc, MemberPronouns, MemberColor, MemberBirthday, MemberProxy, MemberDelete, MemberAvatar, - SystemList); + await PrintCommandNotFoundError(ctx, MemberInfo, MemberRename, MemberDisplayName, MemberServerName, + MemberDesc, MemberPronouns, MemberColor, MemberBirthday, MemberProxy, MemberDelete, MemberAvatar, + SystemList); } private async Task HandleGroupCommand(Context ctx) { - // TODO: implement - // // Commands with no group argument - // if (ctx.Match("n", "new")) - // await ctx.Execute(GroupNew, g => g.CreateGroup(ctx)); - // else if (ctx.Match("list", "l")) - // await ctx.Execute(GroupList, g => g.ListSystemGroups(ctx, null)); - // else if (ctx.Match("commands", "help")) - // await PrintCommandList(ctx, "groups", GroupCommands); - // else if (await ctx.MatchGroup() is { } target) - // { - // // Commands with group argument - // if (ctx.Match("rename", "name", "changename", "setname", "rn")) - // await ctx.Execute(GroupRename, g => g.RenameGroup(ctx, target)); - // else if (ctx.Match("nick", "dn", "displayname", "nickname")) - // await ctx.Execute(GroupDisplayName, g => g.GroupDisplayName(ctx, target)); - // else if (ctx.Match("description", "desc", "describe", "d", "bio", "info", "text", "intro")) - // await ctx.Execute(GroupDesc, g => g.GroupDescription(ctx, target)); - // else if (ctx.Match("add", "a")) - // await ctx.Execute(GroupAdd, - // g => g.AddRemoveMembers(ctx, target, Groups.AddRemoveOperation.Add)); - // else if (ctx.Match("remove", "rem")) - // await ctx.Execute(GroupRemove, - // g => g.AddRemoveMembers(ctx, target, Groups.AddRemoveOperation.Remove)); - // else if (ctx.Match("members", "list", "ms", "l", "ls")) - // await ctx.Execute(GroupMemberList, g => g.ListGroupMembers(ctx, target)); - // else if (ctx.Match("random", "rand", "r")) - // await ctx.Execute(GroupMemberRandom, r => r.GroupMember(ctx, target)); - // else if (ctx.Match("privacy")) - // await ctx.Execute(GroupPrivacy, g => g.GroupPrivacy(ctx, target, null)); - // else if (ctx.Match("public", "pub")) - // await ctx.Execute(GroupPrivacy, g => g.GroupPrivacy(ctx, target, PrivacyLevel.Public)); - // else if (ctx.Match("private", "priv")) - // await ctx.Execute(GroupPrivacy, g => g.GroupPrivacy(ctx, target, PrivacyLevel.Private)); - // else if (ctx.Match("delete", "destroy", "erase", "yeet")) - // await ctx.Execute(GroupDelete, g => g.DeleteGroup(ctx, target)); - // else if (ctx.Match("avatar", "picture", "icon", "image", "pic", "pfp")) - // await ctx.Execute(GroupIcon, g => g.GroupIcon(ctx, target)); - // else if (ctx.Match("banner", "splash", "cover")) - // await ctx.Execute(GroupBannerImage, g => g.GroupBannerImage(ctx, target)); - // else if (ctx.Match("fp", "frontpercent", "front%", "frontbreakdown")) - // await ctx.Execute(GroupFrontPercent, g => g.FrontPercent(ctx, group: target)); - // else if (ctx.Match("color", "colour")) - // await ctx.Execute(GroupColor, g => g.GroupColor(ctx, target)); - // else if (ctx.Match("id")) - // await ctx.Execute(GroupId, g => g.DisplayId(ctx, target)); - // else if (!ctx.HasNext()) - // await ctx.Execute(GroupInfo, g => g.ShowGroupCard(ctx, target)); - // else - // await PrintCommandNotFoundError(ctx, GroupCommandsTargeted); - // } - // else if (!ctx.HasNext()) - // await PrintCommandExpectedError(ctx, GroupCommands); - // else - // await ctx.Reply($"{Emojis.Error} {ctx.CreateNotFoundError("Group", ctx.PopArgument())}"); + // Commands with no group argument + if (ctx.Match("n", "new")) + await ctx.Execute(GroupNew, g => g.CreateGroup(ctx)); + else if (ctx.Match("commands", "help")) + await PrintCommandList(ctx, "groups", GroupCommands); + else if (await ctx.MatchGroup() is { } target) + { + // Commands with group argument + if (ctx.Match("rename", "name", "changename", "setname", "rn")) + await ctx.Execute(GroupRename, g => g.RenameGroup(ctx, target)); + else if (ctx.Match("nick", "dn", "displayname", "nickname")) + await ctx.Execute(GroupDisplayName, g => g.GroupDisplayName(ctx, target)); + else if (ctx.Match("description", "desc", "describe", "d", "bio", "info", "text", "intro")) + await ctx.Execute(GroupDesc, g => g.GroupDescription(ctx, target)); + else if (ctx.Match("add", "a")) + await ctx.Execute(GroupAdd, + g => g.AddRemoveMembers(ctx, target, Groups.AddRemoveOperation.Add)); + else if (ctx.Match("remove", "rem")) + await ctx.Execute(GroupRemove, + g => g.AddRemoveMembers(ctx, target, Groups.AddRemoveOperation.Remove)); + else if (ctx.Match("privacy")) + await ctx.Execute(GroupPrivacy, g => g.GroupPrivacy(ctx, target, null)); + else if (ctx.Match("public", "pub")) + await ctx.Execute(GroupPrivacy, g => g.GroupPrivacy(ctx, target, PrivacyLevel.Public)); + else if (ctx.Match("private", "priv")) + await ctx.Execute(GroupPrivacy, g => g.GroupPrivacy(ctx, target, PrivacyLevel.Private)); + else if (ctx.Match("delete", "destroy", "erase", "yeet")) + await ctx.Execute(GroupDelete, g => g.DeleteGroup(ctx, target)); + else if (ctx.Match("avatar", "picture", "icon", "image", "pic", "pfp")) + await ctx.Execute(GroupIcon, g => g.GroupIcon(ctx, target)); + else if (ctx.Match("banner", "splash", "cover")) + await ctx.Execute(GroupBannerImage, g => g.GroupBannerImage(ctx, target)); + else if (ctx.Match("fp", "frontpercent", "front%", "frontbreakdown")) + await ctx.Execute(GroupFrontPercent, g => g.FrontPercent(ctx, group: target)); + else if (ctx.Match("color", "colour")) + await ctx.Execute(GroupColor, g => g.GroupColor(ctx, target)); + else if (ctx.Match("id")) + await ctx.Execute(GroupId, g => g.DisplayId(ctx, target)); + else if (!ctx.HasNext()) + await ctx.Execute(GroupInfo, g => g.ShowGroupCard(ctx, target)); + else + await PrintCommandNotFoundError(ctx, GroupCommandsTargeted); + } + else if (!ctx.HasNext()) + await PrintCommandExpectedError(ctx, GroupCommands); + else + await ctx.Reply($"{Emojis.Error} {ctx.CreateNotFoundError("Group", ctx.PopArgument())}"); } private async Task HandleSwitchCommand(Context ctx) diff --git a/PluralKit.Bot/Commands/GroupMember.cs b/PluralKit.Bot/Commands/GroupMember.cs index b30abb24..e5ccc1bd 100644 --- a/PluralKit.Bot/Commands/GroupMember.cs +++ b/PluralKit.Bot/Commands/GroupMember.cs @@ -51,11 +51,12 @@ public class GroupMember groups.Count - toAction.Count)); } - public async Task ListMemberGroups(Context ctx, PKMember target) + public async Task ListMemberGroups(Context ctx, PKMember target, string? query, IHasListOptions flags) { var targetSystem = await ctx.Repository.GetSystem(target.System); - var opts = ctx.ParseListOptions(ctx.DirectLookupContextFor(target.System), ctx.LookupContextFor(target.System)); + var opts = flags.GetListOptions(ctx, target.System); opts.MemberFilter = target.Id; + opts.Search = query; var title = new StringBuilder($"Groups containing {target.NameFor(ctx)} (`{target.DisplayHid(ctx.Config)}`) in "); if (ctx.Guild != null) @@ -137,15 +138,16 @@ public class GroupMember members.Count - toAction.Count)); } - public async Task ListGroupMembers(Context ctx, PKGroup target) + public async Task ListGroupMembers(Context ctx, PKGroup target, string? query, IHasListOptions flags) { // see global system list for explanation of how privacy settings are used here var targetSystem = await GetGroupSystem(ctx, target); ctx.CheckSystemPrivacy(targetSystem.Id, target.ListPrivacy); - var opts = ctx.ParseListOptions(ctx.DirectLookupContextFor(target.System), ctx.LookupContextFor(target.System)); + var opts = flags.GetListOptions(ctx, target.System); opts.GroupFilter = target.Id; + opts.Search = query; var title = new StringBuilder($"Members of {target.DisplayName ?? target.Name} (`{target.DisplayHid(ctx.Config)}`) in "); if (ctx.Guild != null) diff --git a/PluralKit.Bot/Commands/Groups.cs b/PluralKit.Bot/Commands/Groups.cs index e350ee58..8289525b 100644 --- a/PluralKit.Bot/Commands/Groups.cs +++ b/PluralKit.Bot/Commands/Groups.cs @@ -478,7 +478,7 @@ public class Groups } } - public async Task ListSystemGroups(Context ctx, PKSystem system) + public async Task ListSystemGroups(Context ctx, PKSystem system, string? query, IHasListOptions flags) { if (system == null) { @@ -492,7 +492,9 @@ public class Groups // - ParseListOptions checks list access privacy and sets the privacy filter (which members show up in list) // - RenderGroupList checks the indivual privacy for each member (NameFor, etc) // the own system is always allowed to look up their list - var opts = ctx.ParseListOptions(ctx.DirectLookupContextFor(system.Id), ctx.LookupContextFor(system.Id)); + var opts = flags.GetListOptions(ctx, system.Id); + opts.Search = query; + await ctx.RenderGroupList( ctx.LookupContextFor(system.Id), system.Id, diff --git a/PluralKit.Bot/Commands/Lists/ContextListExt.cs b/PluralKit.Bot/Commands/Lists/ContextListExt.cs index 15257218..875a1862 100644 --- a/PluralKit.Bot/Commands/Lists/ContextListExt.cs +++ b/PluralKit.Bot/Commands/Lists/ContextListExt.cs @@ -9,95 +9,13 @@ using PluralKit.Core; namespace PluralKit.Bot; +public interface IHasListOptions +{ + ListOptions GetListOptions(Context ctx, SystemId system); +} + public static class ContextListExt { - public static ListOptions ParseListOptions(this Context ctx, LookupContext directLookupCtx, LookupContext lookupContext) - { - var p = new ListOptions(); - - // Short or long list? (parse this first, as it can potentially take a positional argument) - var isFull = ctx.Match("f", "full", "big", "details", "long") || ctx.MatchFlag("f", "full"); - p.Type = isFull ? ListType.Long : ListType.Short; - - // Search query - if (ctx.HasNext()) - p.Search = ctx.RemainderOrNull(); - - // Include description in search? - if (ctx.MatchFlag( - "search-description", - "filter-description", - "in-description", - "sd", - "description", - "desc" - )) - p.SearchDescription = true; - - // Sort property (default is by name, but adding a flag anyway, 'cause why not) - if (ctx.MatchFlag("by-name", "bn")) p.SortProperty = SortProperty.Name; - if (ctx.MatchFlag("by-display-name", "bdn")) p.SortProperty = SortProperty.DisplayName; - if (ctx.MatchFlag("by-id", "bid")) p.SortProperty = SortProperty.Hid; - if (ctx.MatchFlag("by-message-count", "bmc")) p.SortProperty = SortProperty.MessageCount; - if (ctx.MatchFlag("by-created", "bc", "bcd")) p.SortProperty = SortProperty.CreationDate; - if (ctx.MatchFlag("by-last-fronted", "by-last-front", "by-last-switch", "blf", "bls")) - p.SortProperty = SortProperty.LastSwitch; - if (ctx.MatchFlag("by-last-message", "blm", "blp")) p.SortProperty = SortProperty.LastMessage; - if (ctx.MatchFlag("by-birthday", "by-birthdate", "bbd")) p.SortProperty = SortProperty.Birthdate; - if (ctx.MatchFlag("random", "rand")) p.SortProperty = SortProperty.Random; - - // Sort reverse? - if (ctx.MatchFlag("r", "rev", "reverse")) - p.Reverse = true; - - // Privacy filter (default is public only) - if (ctx.MatchFlag("a", "all")) p.PrivacyFilter = null; - if (ctx.MatchFlag("private-only", "po")) p.PrivacyFilter = PrivacyLevel.Private; - - // PERM CHECK: If we're trying to access non-public members of another system, error - if (p.PrivacyFilter != PrivacyLevel.Public && directLookupCtx != LookupContext.ByOwner) - // TODO: should this just return null instead of throwing or something? >.> - throw Errors.NotOwnInfo; - - //this is for searching - p.Context = lookupContext; - - // Additional fields to include in the search results - if (ctx.MatchFlag("with-last-switch", "with-last-fronted", "with-last-front", "wls", "wlf")) - p.IncludeLastSwitch = true; - if (ctx.MatchFlag("with-last-message", "with-last-proxy", "wlm", "wlp")) - p.IncludeLastMessage = true; - if (ctx.MatchFlag("with-message-count", "wmc")) - p.IncludeMessageCount = true; - if (ctx.MatchFlag("with-created", "wc")) - p.IncludeCreated = true; - if (ctx.MatchFlag("with-avatar", "with-image", "with-icon", "wa", "wi", "ia", "ii", "img")) - p.IncludeAvatar = true; - if (ctx.MatchFlag("with-pronouns", "wp", "wprns")) - p.IncludePronouns = true; - if (ctx.MatchFlag("with-displayname", "wdn")) - p.IncludeDisplayName = true; - if (ctx.MatchFlag("with-birthday", "wbd", "wb")) - p.IncludeBirthday = true; - - // Always show the sort property, too (unless this is the short list and we are already showing something else) - if (p.Type != ListType.Short || p.includedCount == 0) - { - if (p.SortProperty == SortProperty.DisplayName) p.IncludeDisplayName = true; - if (p.SortProperty == SortProperty.MessageCount) p.IncludeMessageCount = true; - if (p.SortProperty == SortProperty.CreationDate) p.IncludeCreated = true; - if (p.SortProperty == SortProperty.LastSwitch) p.IncludeLastSwitch = true; - if (p.SortProperty == SortProperty.LastMessage) p.IncludeLastMessage = true; - if (p.SortProperty == SortProperty.Birthdate) p.IncludeBirthday = true; - } - - // Make sure the options are valid - p.AssertIsValid(); - - // Done! - return p; - } - public static async Task RenderMemberList(this Context ctx, LookupContext lookupCtx, SystemId system, string embedTitle, string color, ListOptions opts) { diff --git a/PluralKit.Bot/Commands/Lists/ListOptions.cs b/PluralKit.Bot/Commands/Lists/ListOptions.cs index 991b0a8e..f225da75 100644 --- a/PluralKit.Bot/Commands/Lists/ListOptions.cs +++ b/PluralKit.Bot/Commands/Lists/ListOptions.cs @@ -184,6 +184,7 @@ public static class ListOptionsExt // the check for multiple *sorting* property flags is done in SortProperty setter } + } public enum SortProperty diff --git a/PluralKit.Bot/Commands/Random.cs b/PluralKit.Bot/Commands/Random.cs index 7c451afa..2356ce4a 100644 --- a/PluralKit.Bot/Commands/Random.cs +++ b/PluralKit.Bot/Commands/Random.cs @@ -82,11 +82,11 @@ public class Random components: await _embeds.CreateGroupMessageComponents(ctx, target, groups.ToArray()[randInt])); } - public async Task GroupMember(Context ctx, PKGroup group, bool all, bool showEmbed = false) + public async Task GroupMember(Context ctx, PKGroup group, GroupRandomMemberFlags flags) { ctx.CheckSystemPrivacy(group.System, group.ListPrivacy); - var opts = ctx.ParseListOptions(ctx.DirectLookupContextFor(group.System), ctx.LookupContextFor(group.System)); + var opts = flags.GetListOptions(ctx, group.System); opts.GroupFilter = group.Id; var members = await ctx.Database.Execute(conn => conn.QueryMemberList(group.System, opts.ToQueryOptions())); @@ -96,7 +96,7 @@ public class Random "This group has no members!" + (ctx.System?.Id == group.System ? " Please add at least one member to this group before using this command." : "")); - if (!all) + if (!flags.all) members = members.Where(g => g.MemberVisibility == PrivacyLevel.Public); else ctx.CheckOwnGroup(group); @@ -112,7 +112,7 @@ public class Random var randInt = randGen.Next(ms.Count); - if (showEmbed) + if (flags.show_embed) { await ctx.Reply( text: EmbedService.LEGACY_EMBED_WARNING, diff --git a/PluralKit.Bot/Commands/SystemList.cs b/PluralKit.Bot/Commands/SystemList.cs index 6fc3ff75..f100ac05 100644 --- a/PluralKit.Bot/Commands/SystemList.cs +++ b/PluralKit.Bot/Commands/SystemList.cs @@ -8,16 +8,20 @@ namespace PluralKit.Bot; public class SystemList { - public async Task MemberList(Context ctx, PKSystem target) + public async Task MemberList(Context ctx, PKSystem target, string? query, IHasListOptions flags) { + ctx.CheckSystem(target); + if (target == null) throw Errors.NoSystemError(ctx.DefaultPrefix); ctx.CheckSystemPrivacy(target.Id, target.MemberListPrivacy); + var opts = flags.GetListOptions(ctx, target.Id); + opts.Search = query; + // explanation of privacy lookup here: // - ParseListOptions checks list access privacy and sets the privacy filter (which members show up in list) // - RenderMemberList checks the indivual privacy for each member (NameFor, etc) // the own system is always allowed to look up their list - var opts = ctx.ParseListOptions(ctx.DirectLookupContextFor(target.Id), ctx.LookupContextFor(target.Id)); await ctx.RenderMemberList( ctx.LookupContextFor(target.Id), target.Id, diff --git a/crates/command_definitions/src/group.rs b/crates/command_definitions/src/group.rs index 44847ee6..f884ed3b 100644 --- a/crates/command_definitions/src/group.rs +++ b/crates/command_definitions/src/group.rs @@ -1,11 +1,38 @@ use command_parser::token::TokensIterator; +use crate::utils::get_list_flags; + use super::*; -pub fn group() -> (&'static str, [&'static str; 1]) { - ("group", ["g"]) +pub fn group() -> (&'static str, [&'static str; 2]) { + ("group", ["g", "groups"]) } pub fn targeted() -> TokensIterator { tokens!(group(), GroupRef) } + +pub fn cmds() -> impl Iterator { + let group = group(); + let group_target = targeted(); + + let apply_list_opts = |cmd: Command| cmd.flags(get_list_flags()); + + let group_list_members = tokens!(group_target, ("members", ["list", "ls"])); + let group_list_members_cmd = [ + command!(group_list_members => "group_list_members"), + command!(group_list_members, "list" => "group_list_members"), + command!(group_list_members, ("search", ["find", "query"]), ("query", OpaqueStringRemainder) => "group_search_members"), + ] + .into_iter() + .map(apply_list_opts); + + let system_groups_cmd = [ + command!(group, ("list", ["ls"]) => "group_list_groups"), + command!(group, ("search", ["find", "query"]), ("query", OpaqueStringRemainder) => "group_search_groups"), + ] + .into_iter() + .map(apply_list_opts); + + system_groups_cmd.chain(group_list_members_cmd) +} diff --git a/crates/command_definitions/src/lib.rs b/crates/command_definitions/src/lib.rs index 34f3b4d9..ceb05a49 100644 --- a/crates/command_definitions/src/lib.rs +++ b/crates/command_definitions/src/lib.rs @@ -18,11 +18,14 @@ pub mod server_config; pub mod switch; pub mod system; +pub mod utils; + use command_parser::{command, command::Command, parameter::ParameterKind::*, tokens}; pub fn all() -> impl Iterator { (help::cmds()) .chain(system::cmds()) + .chain(group::cmds()) .chain(member::cmds()) .chain(config::cmds()) .chain(fun::cmds()) diff --git a/crates/command_definitions/src/member.rs b/crates/command_definitions/src/member.rs index 14ad283b..00b90b6a 100644 --- a/crates/command_definitions/src/member.rs +++ b/crates/command_definitions/src/member.rs @@ -1,5 +1,7 @@ use command_parser::token::TokensIterator; +use crate::utils::get_list_flags; + use super::*; pub fn member() -> (&'static str, [&'static str; 1]) { @@ -291,6 +293,16 @@ pub fn cmds() -> impl Iterator { .chain(member_webhook_avatar_cmd) .chain(member_server_avatar_cmd); + let member_group = tokens!(member_target, group::group()); + let member_list_group_cmds = [ + command!(member_group => "member_list_groups"), + command!(member_group, "list" => "member_list_groups"), + command!(member_group, ("search", ["find", "query"]), ("query", OpaqueStringRemainder) => "member_search_groups"), + ] + .into_iter() + .map(|cmd| cmd.flags(get_list_flags())); + let member_group_cmds = member_list_group_cmds; + let member_delete_cmd = [command!(member_target, delete => "member_delete").help("Deletes a member")].into_iter(); @@ -298,10 +310,7 @@ pub fn cmds() -> impl Iterator { [command!(member_target, "soulscream" => "member_soulscream").show_in_suggestions(false)] .into_iter(); - let member_list = [command!(member, "list" => "members_list")].into_iter(); - member_new_cmd - .chain(member_list) .chain(member_info_cmd) .chain(member_name_cmd) .chain(member_description_cmd) @@ -318,4 +327,5 @@ pub fn cmds() -> impl Iterator { .chain(member_message_settings_cmd) .chain(member_delete_cmd) .chain(member_easter_eggs) + .chain(member_group_cmds) } diff --git a/crates/command_definitions/src/random.rs b/crates/command_definitions/src/random.rs index 2f48c9f0..e62701a5 100644 --- a/crates/command_definitions/src/random.rs +++ b/crates/command_definitions/src/random.rs @@ -1,3 +1,5 @@ +use crate::utils::get_list_flags; + use super::*; pub fn cmds() -> impl Iterator { @@ -7,7 +9,7 @@ pub fn cmds() -> impl Iterator { [ command!(random => "random_self").flag(group), command!(system::targeted(), random => "system_random").flag(group), - command!(group::targeted(), random => "group_random_member"), + command!(group::targeted(), random => "group_random_member").flags(get_list_flags()), ] .into_iter() .map(|cmd| cmd.flag(("all", ["a"]))) diff --git a/crates/command_definitions/src/system.rs b/crates/command_definitions/src/system.rs index fcb3c20f..86fcf55e 100644 --- a/crates/command_definitions/src/system.rs +++ b/crates/command_definitions/src/system.rs @@ -1,5 +1,7 @@ use command_parser::token::TokensIterator; +use crate::utils::get_list_flags; + use super::*; pub fn cmds() -> impl Iterator { @@ -251,8 +253,33 @@ pub fn edit() -> impl Iterator { ] .into_iter(); - let system_list = - [command!(system_target, ("members", ["list"]) => "system_members_list")].into_iter(); + let system_list = ("members", ["list"]); + let system_search = tokens!( + ("search", ["query", "find"]), + ("query", OpaqueStringRemainder), + ); + let add_list_flags = |cmd: Command| cmd.flags(get_list_flags()); + let system_list_cmd = [ + command!(system_target, system_list => "system_members_list"), + command!(system_target, system_search => "system_members_search"), + ] + .into_iter() + .map(add_list_flags); + let system_list_self_cmd = [ + command!(system, system_list => "system_members_list_self"), + command!(system, system_search => "system_members_search_self"), + ] + .into_iter() + .map(add_list_flags); + + let system_groups = tokens!(system_target, ("groups", ["gs"])); + let system_groups_cmd = [ + command!(system_groups => "system_list_groups"), + command!(system_groups, ("list", ["ls"]) => "system_list_groups"), + command!(system_groups, ("search", ["find", "query"]), ("query", OpaqueStringRemainder) => "system_search_groups"), + ] + .into_iter() + .map(add_list_flags); system_new_cmd .chain(system_name_self_cmd) @@ -265,6 +292,7 @@ pub fn edit() -> impl Iterator { .chain(system_avatar_self_cmd) .chain(system_server_avatar_self_cmd) .chain(system_banner_self_cmd) + .chain(system_list_self_cmd) .chain(system_delete) .chain(system_privacy_cmd) .chain(system_proxy_cmd) @@ -281,5 +309,6 @@ pub fn edit() -> impl Iterator { .chain(system_info_cmd) .chain(system_front_cmd) .chain(system_link) - .chain(system_list) + .chain(system_list_cmd) + .chain(system_groups_cmd) } diff --git a/crates/command_definitions/src/utils.rs b/crates/command_definitions/src/utils.rs new file mode 100644 index 00000000..8fd2d2c5 --- /dev/null +++ b/crates/command_definitions/src/utils.rs @@ -0,0 +1,52 @@ +use command_parser::flag::Flag; + +pub fn get_list_flags() -> [Flag; 22] { + [ + // Short or long list + Flag::from(("full", ["f", "big", "details", "long"])), + // Search description + Flag::from(( + "search-description", + [ + "filter-description", + "in-description", + "sd", + "description", + "desc", + ], + )), + // Sort properties + Flag::from(("by-name", ["bn"])), + Flag::from(("by-display-name", ["bdn"])), + Flag::from(("by-id", ["bid"])), + Flag::from(("by-message-count", ["bmc"])), + Flag::from(("by-created", ["bc", "bcd"])), + Flag::from(( + "by-last-fronted", + ["by-last-front", "by-last-switch", "blf", "bls"], + )), + Flag::from(("by-last-message", ["blm", "blp"])), + Flag::from(("by-birthday", ["by-birthdate", "bbd"])), + Flag::from(("random", ["rand"])), + // Sort reverse + Flag::from(("reverse", ["r", "rev"])), + // Privacy filter + Flag::from(("all", ["a"])), + Flag::from(("private-only", ["po"])), + // Additional fields to include + Flag::from(( + "with-last-switch", + ["with-last-fronted", "with-last-front", "wls", "wlf"], + )), + Flag::from(("with-last-message", ["with-last-proxy", "wlm", "wlp"])), + Flag::from(("with-message-count", ["wmc"])), + Flag::from(("with-created", ["wc"])), + Flag::from(( + "with-avatar", + ["with-image", "with-icon", "wa", "wi", "ia", "ii", "img"], + )), + Flag::from(("with-pronouns", ["wp", "wprns"])), + Flag::from(("with-displayname", ["wdn"])), + Flag::from(("with-birthday", ["wbd", "wb"])), + ] +} diff --git a/crates/command_parser/src/command.rs b/crates/command_parser/src/command.rs index a8376cf2..6ae62e60 100644 --- a/crates/command_parser/src/command.rs +++ b/crates/command_parser/src/command.rs @@ -11,7 +11,7 @@ use crate::{flag::Flag, token::Token}; pub struct Command { // TODO: fix hygiene pub tokens: Vec, - pub flags: Vec, + pub flags: HashSet, pub help: SmolStr, pub cb: SmolStr, pub show_in_suggestions: bool, @@ -34,7 +34,7 @@ impl Command { } } Self { - flags: Vec::new(), + flags: HashSet::new(), help: SmolStr::new_static(""), cb: cb.into(), show_in_suggestions: true, @@ -54,34 +54,57 @@ impl Command { self } + pub fn flags(mut self, flags: impl IntoIterator>) -> Self { + self.flags.extend(flags.into_iter().map(Into::into)); + self + } + pub fn flag(mut self, flag: impl Into) -> Self { - self.flags.push(flag.into()); + self.flags.insert(flag.into()); self } pub fn hidden_flag(mut self, flag: impl Into) -> Self { let flag = flag.into(); self.hidden_flags.insert(flag.get_name().into()); - self.flags.push(flag); + self.flags.insert(flag); self } } impl Display for Command { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let visible_flags = self + .flags + .iter() + .filter(|flag| !self.hidden_flags.contains(flag.get_name())) + .collect::>(); let write_flags = |f: &mut std::fmt::Formatter<'_>, space: bool| { - for flag in &self.flags { - if self.hidden_flags.contains(flag.get_name()) { - continue; + if visible_flags.is_empty() { + return Ok(()); + } + write!(f, "{}(", space.then_some(" ").unwrap_or(""))?; + let mut written = 0; + let max_flags = visible_flags.len().min(5); + for flag in &visible_flags { + if written > max_flags { + break; } + write!(f, "{flag}")?; + if max_flags - 1 > written { + write!(f, " ")?; + } + written += 1; + } + if visible_flags.len() > written { + let rest_count = visible_flags.len() - written; write!( f, - "{}[{flag}]{}", - space.then_some(" ").unwrap_or(""), - space.then_some("").unwrap_or(" ") + " ...and {rest_count} flag{}...", + (rest_count > 1).then_some("s").unwrap_or(""), )?; } - std::fmt::Result::Ok(()) + write!(f, "){}", space.then_some("").unwrap_or(" ")) }; for (idx, token) in self.tokens.iter().enumerate() { diff --git a/crates/command_parser/src/flag.rs b/crates/command_parser/src/flag.rs index ca61f64d..9fa8ad38 100644 --- a/crates/command_parser/src/flag.rs +++ b/crates/command_parser/src/flag.rs @@ -1,4 +1,4 @@ -use std::fmt::Display; +use std::{fmt::Display, hash::Hash}; use smol_str::SmolStr; @@ -28,6 +28,20 @@ impl Display for Flag { } } +impl PartialEq for Flag { + fn eq(&self, other: &Self) -> bool { + self.name.eq(&other.name) + } +} + +impl Eq for Flag {} + +impl Hash for Flag { + fn hash(&self, state: &mut H) { + self.name.hash(state); + } +} + #[derive(Debug)] pub enum FlagMatchError { ValueMatchFailed(FlagValueMatchError), diff --git a/crates/command_parser/src/lib.rs b/crates/command_parser/src/lib.rs index c02fc463..5c170f02 100644 --- a/crates/command_parser/src/lib.rs +++ b/crates/command_parser/src/lib.rs @@ -1,7 +1,7 @@ #![feature(anonymous_lifetime_in_impl_trait)] pub mod command; -mod flag; +pub mod flag; pub mod parameter; mod string; pub mod token; diff --git a/crates/commands/src/bin/write_cs_glue.rs b/crates/commands/src/bin/write_cs_glue.rs index 1d0e61df..d5eeb526 100644 --- a/crates/commands/src/bin/write_cs_glue.rs +++ b/crates/commands/src/bin/write_cs_glue.rs @@ -1,4 +1,4 @@ -use std::{env, fmt::Write, fs, path::PathBuf, str::FromStr}; +use std::{collections::HashSet, env, fmt::Write, fs, path::PathBuf, str::FromStr}; use command_parser::{ parameter::{Parameter, ParameterKind}, @@ -20,16 +20,26 @@ fn main() -> Result<(), Box> { writeln!(&mut glue, "using Myriad.Types;")?; writeln!(&mut glue, "namespace PluralKit.Bot;\n")?; + let mut commands_seen = HashSet::new(); let mut record_fields = String::new(); for command in &commands { + if commands_seen.contains(&command.cb) { + continue; + } writeln!( &mut record_fields, r#"public record {command_name}({command_name}Params parameters, {command_name}Flags flags): Commands;"#, command_name = command_callback_to_name(&command.cb), )?; + commands_seen.insert(command.cb.clone()); } + + commands_seen.clear(); let mut match_branches = String::new(); for command in &commands { + if commands_seen.contains(&command.cb) { + continue; + } let mut command_params_init = String::new(); let command_params = find_parameters(&command.tokens); for param in &command_params { @@ -68,6 +78,7 @@ fn main() -> Result<(), Box> { command_name = command_callback_to_name(&command.cb), command_callback = command.cb, )?; + commands_seen.insert(command.cb.clone()); } write!( &mut glue, @@ -87,7 +98,12 @@ fn main() -> Result<(), Box> { }} "#, )?; + + commands_seen.clear(); for command in &commands { + if commands_seen.contains(&command.cb) { + continue; + } let mut command_params_fields = String::new(); let command_params = find_parameters(&command.tokens); for param in &command_params { @@ -133,6 +149,76 @@ fn main() -> Result<(), Box> { )?; } command_reply_format.push_str("return ReplyFormat.Standard;\n"); + let mut command_list_options = String::new(); + let mut command_list_options_class = String::new(); + let list_flags = command_definitions::utils::get_list_flags(); + if list_flags.iter().all(|flag| command.flags.contains(&flag)) { + write!(&mut command_list_options_class, ": IHasListOptions")?; + writeln!( + &mut command_list_options, + r#" + public ListOptions GetListOptions(Context ctx, SystemId target) + {{ + var directLookupCtx = ctx.DirectLookupContextFor(target); + var lookupCtx = ctx.LookupContextFor(target); + + var p = new ListOptions(); + p.Type = full ? ListType.Long : ListType.Short; + // Search description filter + p.SearchDescription = search_description; + + // Sort property + if (by_name) p.SortProperty = SortProperty.Name; + if (by_display_name) p.SortProperty = SortProperty.DisplayName; + if (by_id) p.SortProperty = SortProperty.Hid; + if (by_message_count) p.SortProperty = SortProperty.MessageCount; + if (by_created) p.SortProperty = SortProperty.CreationDate; + if (by_last_fronted) p.SortProperty = SortProperty.LastSwitch; + if (by_last_message) p.SortProperty = SortProperty.LastMessage; + if (by_birthday) p.SortProperty = SortProperty.Birthdate; + if (random) p.SortProperty = SortProperty.Random; + + // Sort reverse + p.Reverse = reverse; + + // Privacy filter + if (all) p.PrivacyFilter = null; + if (private_only) p.PrivacyFilter = PrivacyLevel.Private; + // PERM CHECK: If we're trying to access non-public members of another system, error + if (p.PrivacyFilter != PrivacyLevel.Public && directLookupCtx != LookupContext.ByOwner) + // TODO: should this just return null instead of throwing or something? >.> + throw Errors.NotOwnInfo; + + // this is for searching + p.Context = lookupCtx; + + // Additional fields to include + p.IncludeLastSwitch = with_last_switch; + p.IncludeLastMessage = with_last_message; + p.IncludeMessageCount = with_message_count; + p.IncludeCreated = with_created; + p.IncludeAvatar = with_avatar; + p.IncludePronouns = with_pronouns; + p.IncludeDisplayName = with_displayname; + p.IncludeBirthday = with_birthday; + + // Always show the sort property (unless short list and already showing something else) + if (p.Type != ListType.Short || p.includedCount == 0) + {{ + if (p.SortProperty == SortProperty.DisplayName) p.IncludeDisplayName = true; + if (p.SortProperty == SortProperty.MessageCount) p.IncludeMessageCount = true; + if (p.SortProperty == SortProperty.CreationDate) p.IncludeCreated = true; + if (p.SortProperty == SortProperty.LastSwitch) p.IncludeLastSwitch = true; + if (p.SortProperty == SortProperty.LastMessage) p.IncludeLastMessage = true; + if (p.SortProperty == SortProperty.Birthdate) p.IncludeBirthday = true; + }} + + p.AssertIsValid(); + return p; + }} + "#, + )?; + } write!( &mut glue, r#" @@ -140,7 +226,7 @@ fn main() -> Result<(), Box> { {{ {command_params_fields} }} - public class {command_name}Flags + public class {command_name}Flags {command_list_options_class} {{ {command_flags_fields} @@ -148,10 +234,13 @@ fn main() -> Result<(), Box> { {{ {command_reply_format} }} + + {command_list_options} }} "#, command_name = command_callback_to_name(&command.cb), )?; + commands_seen.insert(command.cb.clone()); } fs::write(write_location, glue)?; Ok(()) diff --git a/crates/commands/src/main.rs b/crates/commands/src/main.rs index 376d2469..07128bfe 100644 --- a/crates/commands/src/main.rs +++ b/crates/commands/src/main.rs @@ -17,7 +17,7 @@ fn main() { } } else { for command in command_definitions::all() { - println!("{} - {}", command, command.help); + println!("{} => {} - {}", command.cb, command, command.help); } } } From 1943687c7046fd54ad9816ed4e19bf1812126bea Mon Sep 17 00:00:00 2001 From: dusk Date: Wed, 1 Oct 2025 00:51:45 +0000 Subject: [PATCH 101/179] implement rest of group and member commands --- PluralKit.Bot/CommandMeta/CommandTree.cs | 95 +-- .../Context/ContextArgumentsExt.cs | 10 - .../Context/ContextParametersExt.cs | 16 + PluralKit.Bot/CommandSystem/Parameters.cs | 15 + PluralKit.Bot/Commands/GroupMember.cs | 19 +- PluralKit.Bot/Commands/Groups.cs | 733 +++++++++--------- crates/command_definitions/src/group.rs | 156 +++- crates/command_definitions/src/member.rs | 16 +- crates/command_parser/src/parameter.rs | 68 +- crates/command_parser/src/token.rs | 10 +- crates/commands/src/bin/write_cs_glue.rs | 4 + crates/commands/src/commands.udl | 2 + crates/commands/src/lib.rs | 4 + 13 files changed, 705 insertions(+), 443 deletions(-) diff --git a/PluralKit.Bot/CommandMeta/CommandTree.cs b/PluralKit.Bot/CommandMeta/CommandTree.cs index 85cf1ac2..e5d89931 100644 --- a/PluralKit.Bot/CommandMeta/CommandTree.cs +++ b/PluralKit.Bot/CommandMeta/CommandTree.cs @@ -65,6 +65,9 @@ public partial class CommandTree Commands.MemberDelete(var param, _) => ctx.Execute(MemberDelete, m => m.Delete(ctx, param.target)), Commands.MemberPrivacyShow(var param, _) => ctx.Execute(MemberPrivacy, m => m.ShowPrivacy(ctx, param.target)), Commands.MemberPrivacyUpdate(var param, _) => ctx.Execute(MemberPrivacy, m => m.ChangePrivacy(ctx, param.target, param.member_privacy_target, param.new_privacy_level)), + Commands.MemberGroupAdd(var param, _) => ctx.Execute(MemberGroupAdd, m => m.AddRemoveGroups(ctx, param.target, param.groups, Groups.AddRemoveOperation.Add)), + Commands.MemberGroupRemove(var param, _) => ctx.Execute(MemberGroupRemove, m => m.AddRemoveGroups(ctx, param.target, param.groups, Groups.AddRemoveOperation.Remove)), + Commands.MemberId(var param, _) => ctx.Execute(MemberId, m => m.DisplayId(ctx, param.target)), Commands.CfgApAccountShow => ctx.Execute(null, m => m.ViewAutoproxyAccount(ctx)), Commands.CfgApAccountUpdate(var param, _) => ctx.Execute(null, m => m.EditAutoproxyAccount(ctx, param.toggle)), Commands.CfgApTimeoutShow => ctx.Execute(null, m => m.ViewAutoproxyTimeout(ctx)), @@ -185,6 +188,36 @@ public partial class CommandTree Commands.SystemSearchGroups(var param, var flags) => ctx.Execute(GroupList, g => g.ListSystemGroups(ctx, param.target, param.query, flags)), Commands.GroupListGroups(var param, var flags) => ctx.Execute(GroupList, g => g.ListSystemGroups(ctx, ctx.System, null, flags)), Commands.GroupSearchGroups(var param, var flags) => ctx.Execute(GroupList, g => g.ListSystemGroups(ctx, ctx.System, param.query, flags)), + Commands.GroupNew(var param, _) => ctx.Execute(GroupNew, g => g.CreateGroup(ctx, param.name)), + Commands.GroupInfo(var param, _) => ctx.Execute(GroupInfo, g => g.ShowGroupCard(ctx, param.target)), + Commands.GroupShowName(var param, var flags) => ctx.Execute(GroupRename, g => g.ShowGroupDisplayName(ctx, param.target, flags.GetReplyFormat())), + Commands.GroupClearName(var param, var flags) => ctx.Execute(GroupRename, g => g.RenameGroup(ctx, param.target, null)), + Commands.GroupRename(var param, _) => ctx.Execute(GroupRename, g => g.RenameGroup(ctx, param.target, param.name)), + Commands.GroupShowDisplayName(var param, var flags) => ctx.Execute(GroupDisplayName, g => g.ShowGroupDisplayName(ctx, param.target, flags.GetReplyFormat())), + Commands.GroupClearDisplayName(var param, var flags) => ctx.Execute(GroupDisplayName, g => g.ClearGroupDisplayName(ctx, param.target)), + Commands.GroupChangeDisplayName(var param, _) => ctx.Execute(GroupDisplayName, g => g.ChangeGroupDisplayName(ctx, param.target, param.name)), + Commands.GroupShowDescription(var param, var flags) => ctx.Execute(GroupDesc, g => g.ShowGroupDescription(ctx, param.target, flags.GetReplyFormat())), + Commands.GroupClearDescription(var param, var flags) => ctx.Execute(GroupDesc, g => g.ClearGroupDescription(ctx, param.target)), + Commands.GroupChangeDescription(var param, _) => ctx.Execute(GroupDesc, g => g.ChangeGroupDescription(ctx, param.target, param.description)), + Commands.GroupShowIcon(var param, var flags) => ctx.Execute(GroupIcon, g => g.ShowGroupIcon(ctx, param.target, flags.GetReplyFormat())), + Commands.GroupClearIcon(var param, var flags) => ctx.Execute(GroupIcon, g => g.ClearGroupIcon(ctx, param.target)), + Commands.GroupChangeIcon(var param, _) => ctx.Execute(GroupIcon, g => g.ChangeGroupIcon(ctx, param.target, param.icon)), + Commands.GroupShowBanner(var param, var flags) => ctx.Execute(GroupBannerImage, g => g.ShowGroupBanner(ctx, param.target, flags.GetReplyFormat())), + Commands.GroupClearBanner(var param, var flags) => ctx.Execute(GroupBannerImage, g => g.ClearGroupBanner(ctx, param.target)), + Commands.GroupChangeBanner(var param, _) => ctx.Execute(GroupBannerImage, g => g.ChangeGroupBanner(ctx, param.target, param.banner)), + Commands.GroupShowColor(var param, var flags) => ctx.Execute(GroupColor, g => g.ShowGroupColor(ctx, param.target, flags.GetReplyFormat())), + Commands.GroupClearColor(var param, var flags) => ctx.Execute(GroupColor, g => g.ClearGroupColor(ctx, param.target)), + Commands.GroupChangeColor(var param, _) => ctx.Execute(GroupColor, g => g.ChangeGroupColor(ctx, param.target, param.color)), + Commands.GroupAddMember(var param, var flags) => ctx.Execute(GroupAdd, g => g.AddRemoveMembers(ctx, param.target, param.targets, Groups.AddRemoveOperation.Add, flags.all)), + Commands.GroupRemoveMember(var param, var flags) => ctx.Execute(GroupRemove, g => g.AddRemoveMembers(ctx, param.target, param.targets, Groups.AddRemoveOperation.Remove, flags.all)), + Commands.GroupShowPrivacy(var param, _) => ctx.Execute(GroupPrivacy, g => g.ShowGroupPrivacy(ctx, param.target)), + Commands.GroupChangePrivacyAll(var param, _) => ctx.Execute(GroupPrivacy, g => g.SetAllGroupPrivacy(ctx, param.target, param.level)), + Commands.GroupChangePrivacy(var param, _) => ctx.Execute(GroupPrivacy, g => g.SetGroupPrivacy(ctx, param.target, param.privacy, param.level)), + Commands.GroupSetPublic(var param, _) => ctx.Execute(GroupPrivacy, g => g.SetAllGroupPrivacy(ctx, param.target, PrivacyLevel.Public)), + Commands.GroupSetPrivate(var param, _) => ctx.Execute(GroupPrivacy, g => g.SetAllGroupPrivacy(ctx, param.target, PrivacyLevel.Private)), + Commands.GroupDelete(var param, var flags) => ctx.Execute(GroupDelete, g => g.DeleteGroup(ctx, param.target)), + Commands.GroupId(var param, _) => ctx.Execute(GroupId, g => g.DisplayId(ctx, param.target)), + Commands.GroupFronterPercent(var param, var flags) => ctx.Execute(GroupFrontPercent, g => g.FrontPercent(ctx, null, flags.duration, flags.fronters_only, flags.flat, param.target)), _ => // this should only ever occur when deving if commands are not implemented... ctx.Reply( @@ -428,8 +461,6 @@ public partial class CommandTree { if (ctx.Match("commands", "help")) await PrintCommandList(ctx, "members", MemberCommands); - else if (await ctx.MatchMember() is PKMember target) - await HandleMemberCommandTargeted(ctx, target); else if (!ctx.HasNext()) await PrintCommandExpectedError(ctx, MemberNew, MemberInfo, MemberRename, MemberDisplayName, MemberServerName, MemberDesc, MemberPronouns, @@ -438,69 +469,11 @@ public partial class CommandTree await ctx.Reply($"{Emojis.Error} {ctx.CreateNotFoundError("Member", ctx.PopArgument())}"); } - private async Task HandleMemberCommandTargeted(Context ctx, PKMember target) - { - // Commands that have a member target (eg. pk;member delete) - if (ctx.Match("group", "groups", "g")) - if (ctx.Match("add", "a")) - await ctx.Execute(MemberGroupAdd, - m => m.AddRemoveGroups(ctx, target, Groups.AddRemoveOperation.Add)); - else if (ctx.Match("remove", "rem")) - await ctx.Execute(MemberGroupRemove, - m => m.AddRemoveGroups(ctx, target, Groups.AddRemoveOperation.Remove)); - else if (ctx.Match("id")) - await ctx.Execute(MemberId, m => m.DisplayId(ctx, target)); - else - await PrintCommandNotFoundError(ctx, MemberInfo, MemberRename, MemberDisplayName, MemberServerName, - MemberDesc, MemberPronouns, MemberColor, MemberBirthday, MemberProxy, MemberDelete, MemberAvatar, - SystemList); - } - private async Task HandleGroupCommand(Context ctx) { // Commands with no group argument - if (ctx.Match("n", "new")) - await ctx.Execute(GroupNew, g => g.CreateGroup(ctx)); - else if (ctx.Match("commands", "help")) + if (ctx.Match("commands", "help")) await PrintCommandList(ctx, "groups", GroupCommands); - else if (await ctx.MatchGroup() is { } target) - { - // Commands with group argument - if (ctx.Match("rename", "name", "changename", "setname", "rn")) - await ctx.Execute(GroupRename, g => g.RenameGroup(ctx, target)); - else if (ctx.Match("nick", "dn", "displayname", "nickname")) - await ctx.Execute(GroupDisplayName, g => g.GroupDisplayName(ctx, target)); - else if (ctx.Match("description", "desc", "describe", "d", "bio", "info", "text", "intro")) - await ctx.Execute(GroupDesc, g => g.GroupDescription(ctx, target)); - else if (ctx.Match("add", "a")) - await ctx.Execute(GroupAdd, - g => g.AddRemoveMembers(ctx, target, Groups.AddRemoveOperation.Add)); - else if (ctx.Match("remove", "rem")) - await ctx.Execute(GroupRemove, - g => g.AddRemoveMembers(ctx, target, Groups.AddRemoveOperation.Remove)); - else if (ctx.Match("privacy")) - await ctx.Execute(GroupPrivacy, g => g.GroupPrivacy(ctx, target, null)); - else if (ctx.Match("public", "pub")) - await ctx.Execute(GroupPrivacy, g => g.GroupPrivacy(ctx, target, PrivacyLevel.Public)); - else if (ctx.Match("private", "priv")) - await ctx.Execute(GroupPrivacy, g => g.GroupPrivacy(ctx, target, PrivacyLevel.Private)); - else if (ctx.Match("delete", "destroy", "erase", "yeet")) - await ctx.Execute(GroupDelete, g => g.DeleteGroup(ctx, target)); - else if (ctx.Match("avatar", "picture", "icon", "image", "pic", "pfp")) - await ctx.Execute(GroupIcon, g => g.GroupIcon(ctx, target)); - else if (ctx.Match("banner", "splash", "cover")) - await ctx.Execute(GroupBannerImage, g => g.GroupBannerImage(ctx, target)); - else if (ctx.Match("fp", "frontpercent", "front%", "frontbreakdown")) - await ctx.Execute(GroupFrontPercent, g => g.FrontPercent(ctx, group: target)); - else if (ctx.Match("color", "colour")) - await ctx.Execute(GroupColor, g => g.GroupColor(ctx, target)); - else if (ctx.Match("id")) - await ctx.Execute(GroupId, g => g.DisplayId(ctx, target)); - else if (!ctx.HasNext()) - await ctx.Execute(GroupInfo, g => g.ShowGroupCard(ctx, target)); - else - await PrintCommandNotFoundError(ctx, GroupCommandsTargeted); - } else if (!ctx.HasNext()) await PrintCommandExpectedError(ctx, GroupCommands); else diff --git a/PluralKit.Bot/CommandSystem/Context/ContextArgumentsExt.cs b/PluralKit.Bot/CommandSystem/Context/ContextArgumentsExt.cs index 293cc118..b967d62e 100644 --- a/PluralKit.Bot/CommandSystem/Context/ContextArgumentsExt.cs +++ b/PluralKit.Bot/CommandSystem/Context/ContextArgumentsExt.cs @@ -127,16 +127,6 @@ public static class ContextArgumentsExt ctx.PopArgument(); return (messageId, channelId); } - - public static async Task> ParseMemberList(this Context ctx, SystemId? restrictToSystem) - { - throw new NotImplementedException(); - } - - public static async Task> ParseGroupList(this Context ctx, SystemId? restrictToSystem) - { - throw new NotImplementedException(); - } } public enum ReplyFormat diff --git a/PluralKit.Bot/CommandSystem/Context/ContextParametersExt.cs b/PluralKit.Bot/CommandSystem/Context/ContextParametersExt.cs index 13b0ce99..63e701d5 100644 --- a/PluralKit.Bot/CommandSystem/Context/ContextParametersExt.cs +++ b/PluralKit.Bot/CommandSystem/Context/ContextParametersExt.cs @@ -36,6 +36,14 @@ public static class ContextParametersExt ); } + public static async Task> ParamResolveGroups(this Context ctx, string param_name) + { + return await ctx.Parameters.ResolveParameter( + ctx, param_name, + param => (param as Parameter.GroupRefs)?.groups + ); + } + public static async Task ParamResolveSystem(this Context ctx, string param_name) { return await ctx.Parameters.ResolveParameter( @@ -52,6 +60,14 @@ public static class ContextParametersExt ); } + public static async Task ParamResolveGroupPrivacyTarget(this Context ctx, string param_name) + { + return await ctx.Parameters.ResolveParameter( + ctx, param_name, + param => (param as Parameter.GroupPrivacyTarget)?.target + ); + } + public static async Task ParamResolveSystemPrivacyTarget(this Context ctx, string param_name) { return await ctx.Parameters.ResolveParameter( diff --git a/PluralKit.Bot/CommandSystem/Parameters.cs b/PluralKit.Bot/CommandSystem/Parameters.cs index d3331de1..0c440b61 100644 --- a/PluralKit.Bot/CommandSystem/Parameters.cs +++ b/PluralKit.Bot/CommandSystem/Parameters.cs @@ -11,9 +11,11 @@ public abstract record Parameter() public record MemberRef(PKMember member): Parameter; public record MemberRefs(List members): Parameter; public record GroupRef(PKGroup group): Parameter; + public record GroupRefs(List groups): Parameter; public record SystemRef(PKSystem system): Parameter; public record GuildRef(Guild guild): Parameter; public record MemberPrivacyTarget(MemberPrivacySubject target): Parameter; + public record GroupPrivacyTarget(GroupPrivacySubject target): Parameter; public record SystemPrivacyTarget(SystemPrivacySubject target): Parameter; public record PrivacyLevel(Core.PrivacyLevel level): Parameter; public record Toggle(bool value): Parameter; @@ -79,17 +81,30 @@ public class Parameters await ctx.ParseGroup(groupRef.group, byId) ?? throw new PKError(ctx.CreateNotFoundError("Group", groupRef.group)) ); + case uniffi.commands.Parameter.GroupRefs groupRefs: + return new Parameter.GroupRefs( + await groupRefs.groups.ToAsyncEnumerable().SelectAwait(async g => + await ctx.ParseGroup(g, byId) + ?? throw new PKError(ctx.CreateNotFoundError("Group", g, byId)) + ).ToListAsync() + ); case uniffi.commands.Parameter.SystemRef systemRef: // todo: do we need byId here? return new Parameter.SystemRef( await ctx.ParseSystem(systemRef.system) ?? throw new PKError(ctx.CreateNotFoundError("System", systemRef.system)) ); + // todo(dusk): ideally generate enums for these from rust code in the cs glue case uniffi.commands.Parameter.MemberPrivacyTarget memberPrivacyTarget: // this should never really fail... if (!MemberPrivacyUtils.TryParseMemberPrivacy(memberPrivacyTarget.target, out var memberPrivacy)) throw new PKError($"Invalid member privacy target {memberPrivacyTarget.target}"); return new Parameter.MemberPrivacyTarget(memberPrivacy); + case uniffi.commands.Parameter.GroupPrivacyTarget groupPrivacyTarget: + // this should never really fail... + if (!GroupPrivacyUtils.TryParseGroupPrivacy(groupPrivacyTarget.target, out var groupPrivacy)) + throw new PKError($"Invalid group privacy target {groupPrivacyTarget.target}"); + return new Parameter.GroupPrivacyTarget(groupPrivacy); case uniffi.commands.Parameter.SystemPrivacyTarget systemPrivacyTarget: // this should never really fail... if (!SystemPrivacyUtils.TryParseSystemPrivacy(systemPrivacyTarget.target, out var systemPrivacy)) diff --git a/PluralKit.Bot/Commands/GroupMember.cs b/PluralKit.Bot/Commands/GroupMember.cs index e5ccc1bd..69a9b269 100644 --- a/PluralKit.Bot/Commands/GroupMember.cs +++ b/PluralKit.Bot/Commands/GroupMember.cs @@ -10,11 +10,11 @@ namespace PluralKit.Bot; public class GroupMember { - public async Task AddRemoveGroups(Context ctx, PKMember target, Groups.AddRemoveOperation op) + public async Task AddRemoveGroups(Context ctx, PKMember target, List _groups, Groups.AddRemoveOperation op) { ctx.CheckSystem().CheckOwnMember(target); - var groups = (await ctx.ParseGroupList(ctx.System.Id)) + var groups = _groups.FindAll(g => g.System == ctx.System.Id) .Select(g => g.Id) .Distinct() .ToList(); @@ -83,12 +83,12 @@ public class GroupMember target.Color, opts); } - public async Task AddRemoveMembers(Context ctx, PKGroup target, Groups.AddRemoveOperation op) + public async Task AddRemoveMembers(Context ctx, PKGroup target, List _members, Groups.AddRemoveOperation op, bool all) { ctx.CheckOwnGroup(target); List members; - if (ctx.MatchFlag("all", "a")) + if (all) { members = (await ctx.Database.Execute(conn => conn.QueryMemberList(target.System, new DatabaseViewsExt.ListQueryOptions { }))) @@ -98,10 +98,11 @@ public class GroupMember } else { - members = (await ctx.ParseMemberList(ctx.System.Id)) - .Select(m => m.Id) - .Distinct() - .ToList(); + members = _members + .FindAll(m => m.System == ctx.System.Id) + .Select(m => m.Id) + .Distinct() + .ToList(); } var existingMembersInGroup = (await ctx.Database.Execute(conn => conn.QueryMemberList(target.System, @@ -125,7 +126,7 @@ public class GroupMember .Where(m => existingMembersInGroup.Contains(m.Value)) .ToList(); - if (ctx.MatchFlag("all", "a") && !await ctx.PromptYesNo($"Are you sure you want to remove all members from group {target.Reference(ctx)}?", "Empty Group")) throw Errors.GenericCancelled(); + if (all && !await ctx.PromptYesNo($"Are you sure you want to remove all members from group {target.Reference(ctx)}?", "Empty Group")) throw Errors.GenericCancelled(); await ctx.Repository.RemoveMembersFromGroup(target.Id, toAction); } diff --git a/PluralKit.Bot/Commands/Groups.cs b/PluralKit.Bot/Commands/Groups.cs index 8289525b..a405f527 100644 --- a/PluralKit.Bot/Commands/Groups.cs +++ b/PluralKit.Bot/Commands/Groups.cs @@ -32,12 +32,11 @@ public class Groups _avatarHosting = avatarHosting; } - public async Task CreateGroup(Context ctx) + public async Task CreateGroup(Context ctx, string groupName) { ctx.CheckSystem(); // Check group name length - var groupName = ctx.RemainderOrNull() ?? throw new PKSyntaxError("You must pass a group name."); if (groupName.Length > Limits.MaxGroupNameLength) throw new PKError($"Group name too long ({groupName.Length}/{Limits.MaxGroupNameLength} characters)."); @@ -99,12 +98,11 @@ public class Groups await ctx.Reply(replyStr, eb.Build()); } - public async Task RenameGroup(Context ctx, PKGroup target) + public async Task RenameGroup(Context ctx, PKGroup target, string newName) { ctx.CheckOwnGroup(target); // Check group name length - var newName = ctx.RemainderOrNull() ?? throw new PKSyntaxError("You must pass a new group name."); if (newName.Length > Limits.MaxGroupNameLength) throw new PKError( $"New group name too long ({newName.Length}/{Limits.MaxMemberNameLength} characters)."); @@ -124,7 +122,7 @@ public class Groups await ctx.Reply($"{Emojis.Success} Group name changed from **{target.Name}** to **{newName}** (using {newName.Length}/{Limits.MaxGroupNameLength} characters)."); } - public async Task GroupDisplayName(Context ctx, PKGroup target) + public async Task ShowGroupDisplayName(Context ctx, PKGroup target, ReplyFormat format) { var noDisplayNameSetMessage = "This group does not have a display name set" + (ctx.System?.Id == target.System @@ -134,8 +132,6 @@ public class Groups // Whether displayname is shown or not should depend on if group name privacy is set. // If name privacy is on then displayname should look like name. - var format = ctx.MatchFormat(); - // if we're doing a raw or plaintext query check for null if (format != ReplyFormat.Standard) if (target.DisplayName == null || !target.NamePrivacy.CanAccess(ctx.DirectLookupContextFor(target.System))) @@ -157,69 +153,62 @@ public class Groups return; } - if (!ctx.HasNext(false)) - { - var showDisplayName = target.NamePrivacy.CanAccess(ctx.LookupContextFor(target.System)) && target.DisplayName != null; + var showDisplayName = target.NamePrivacy.CanAccess(ctx.LookupContextFor(target.System)) && target.DisplayName != null; - var eb = new EmbedBuilder() - .Title("Group names") - .Field(new Embed.Field("Name", target.NameFor(ctx))) - .Field(new Embed.Field("Display Name", showDisplayName ? target.DisplayName : "*(no displayname set or name is private)*")); + var eb2 = new EmbedBuilder() + .Title("Group names") + .Field(new Embed.Field("Name", target.NameFor(ctx))) + .Field(new Embed.Field("Display Name", showDisplayName ? target.DisplayName : "*(no displayname set or name is private)*")); - var reference = target.Reference(ctx); + var reference = target.Reference(ctx); - if (ctx.System?.Id == target.System) - eb.Description( - $"To change display name, type `{ctx.DefaultPrefix}group {reference} displayname `.\n" - + $"To clear it, type `{ctx.DefaultPrefix}group {reference} displayname -clear`.\n" - + $"To print the raw display name, type `{ctx.DefaultPrefix}group {reference} displayname -raw`."); + if (ctx.System?.Id == target.System) + eb2.Description( + $"To change display name, type `{ctx.DefaultPrefix}group {reference} displayname `.\n" + + $"To clear it, type `{ctx.DefaultPrefix}group {reference} displayname -clear`.\n" + + $"To print the raw display name, type `{ctx.DefaultPrefix}group {reference} displayname -raw`."); - if (ctx.System?.Id == target.System && showDisplayName) - eb.Footer(new Embed.EmbedFooter($"Using {target.DisplayName.Length}/{Limits.MaxGroupNameLength} characters.")); + if (ctx.System?.Id == target.System && showDisplayName) + eb2.Footer(new Embed.EmbedFooter($"Using {target.DisplayName.Length}/{Limits.MaxGroupNameLength} characters.")); - await ctx.Reply(embed: eb.Build()); - - return; - } - - ctx.CheckOwnGroup(target); - - if (ctx.MatchClear() && await ctx.ConfirmClear("this group's display name")) - { - var patch = new GroupPatch { DisplayName = Partial.Null() }; - await ctx.Repository.UpdateGroup(target.Id, patch); - - var replyStr = $"{Emojis.Success} Group display name cleared."; - if (target.NamePrivacy == PrivacyLevel.Private) - replyStr += $"\n{Emojis.Warn} Since this group no longer has a display name set, their name privacy **can no longer take effect**."; - await ctx.Reply(replyStr); - } - else - { - var newDisplayName = ctx.RemainderOrNull(false).NormalizeLineEndSpacing(); - if (newDisplayName.Length > Limits.MaxGroupNameLength) - throw new PKError($"Group name too long ({newDisplayName.Length}/{Limits.MaxGroupNameLength} characters)."); - - var patch = new GroupPatch { DisplayName = Partial.Present(newDisplayName) }; - await ctx.Repository.UpdateGroup(target.Id, patch); - - await ctx.Reply($"{Emojis.Success} Group display name changed (using {newDisplayName.Length}/{Limits.MaxGroupNameLength} characters)."); - } + await ctx.Reply(embed: eb2.Build()); } - public async Task GroupDescription(Context ctx, PKGroup target) + public async Task ClearGroupDisplayName(Context ctx, PKGroup target) { - ctx.CheckSystemPrivacy(target.System, target.DescriptionPrivacy); + ctx.CheckOwnGroup(target); - var noDescriptionSetMessage = "This group does not have a description set."; - if (ctx.System?.Id == target.System) - noDescriptionSetMessage += - $" To set one, type `{ctx.DefaultPrefix}group {target.Reference(ctx)} description `."; + var patch = new GroupPatch { DisplayName = Partial.Null() }; + await ctx.Repository.UpdateGroup(target.Id, patch); - var format = ctx.MatchFormat(); + var replyStr = $"{Emojis.Success} Group display name cleared."; + if (target.NamePrivacy == PrivacyLevel.Private) + replyStr += $"\n{Emojis.Warn} Since this group no longer has a display name set, their name privacy **can no longer take effect**."; + await ctx.Reply(replyStr); + } - // 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) + public async Task ChangeGroupDisplayName(Context ctx, PKGroup target, string newDisplayName) + { + ctx.CheckOwnGroup(target); + + if (newDisplayName.Length > Limits.MaxGroupNameLength) + throw new PKError($"Group name too long ({newDisplayName.Length}/{Limits.MaxGroupNameLength} characters)."); + + var patch = new GroupPatch { DisplayName = Partial.Present(newDisplayName) }; + await ctx.Repository.UpdateGroup(target.Id, patch); + + await ctx.Reply($"{Emojis.Success} Group display name changed (using {newDisplayName.Length}/{Limits.MaxGroupNameLength} characters)."); + } + + public async Task ShowGroupDescription(Context ctx, PKGroup target, ReplyFormat format) + { + var noDescriptionSetMessage = "This group does not have a description set" + + (ctx.System?.Id == target.System + ? $". To set one, type `{ctx.DefaultPrefix}group {target.Reference(ctx)} description `." + : "."); + + // if we're doing a raw or plaintext query check for null + if (format != ReplyFormat.Standard) if (target.Description == null) { await ctx.Reply(noDescriptionSetMessage); @@ -239,243 +228,282 @@ public class Groups return; } - if (!ctx.HasNext(false)) + if (target.Description == null) { - await ctx.Reply(embed: new EmbedBuilder() - .Title("Group description") - .Description(target.Description) - .Field(new Embed.Field("\u200B", - $"To print the description with formatting, type `{ctx.DefaultPrefix}group {target.Reference(ctx)} description -raw`." - + (ctx.System?.Id == target.System - ? $" To clear it, type `{ctx.DefaultPrefix}group {target.Reference(ctx)} description -clear`." - : "") - + $" Using {target.Description.Length}/{Limits.MaxDescriptionLength} characters.")) - .Build()); + await ctx.Reply(noDescriptionSetMessage); return; } + var eb2 = new EmbedBuilder() + .Title("Group description") + .Description(target.Description); + + var reference = target.Reference(ctx); + + if (ctx.System?.Id == target.System) + eb2.Field(new Embed.Field("\u200B", + $"To print the description with formatting, type `{ctx.DefaultPrefix}group {reference} description -raw`." + + $" To clear it, type `{ctx.DefaultPrefix}group {reference} description -clear`." + + $" Using {target.Description.Length}/{Limits.MaxDescriptionLength} characters.")); + else + eb2.Field(new Embed.Field("\u200B", + $"To print the description with formatting, type `{ctx.DefaultPrefix}group {reference} description -raw`." + + $" Using {target.Description.Length}/{Limits.MaxDescriptionLength} characters.")); + + await ctx.Reply(embed: eb2.Build()); + } + + public async Task ClearGroupDescription(Context ctx, PKGroup target) + { ctx.CheckOwnGroup(target); - if (ctx.MatchClear() && await ctx.ConfirmClear("this group's description")) - { - var patch = new GroupPatch { Description = Partial.Null() }; - await ctx.Repository.UpdateGroup(target.Id, patch); - await ctx.Reply($"{Emojis.Success} Group description cleared."); - } - else - { - var description = ctx.RemainderOrNull(false).NormalizeLineEndSpacing(); - if (description.IsLongerThan(Limits.MaxDescriptionLength)) - throw Errors.StringTooLongError("Description", description.Length, Limits.MaxDescriptionLength); + var patch = new GroupPatch { Description = Partial.Null() }; + await ctx.Repository.UpdateGroup(target.Id, patch); - var patch = new GroupPatch { Description = Partial.Present(description) }; - await ctx.Repository.UpdateGroup(target.Id, patch); - - await ctx.Reply($"{Emojis.Success} Group description changed (using {description.Length}/{Limits.MaxDescriptionLength} characters)."); - } + await ctx.Reply($"{Emojis.Success} Group description cleared."); } - public async Task GroupIcon(Context ctx, PKGroup target) + public async Task ChangeGroupDescription(Context ctx, PKGroup target, string newDescription) { - async Task ClearIcon() - { - await ctx.ConfirmClear("this group's icon"); - ctx.CheckOwnGroup(target); + ctx.CheckOwnGroup(target); - await ctx.Repository.UpdateGroup(target.Id, new GroupPatch { Icon = null }); - await ctx.Reply($"{Emojis.Success} Group icon cleared."); - } + if (newDescription.IsLongerThan(Limits.MaxDescriptionLength)) + throw Errors.StringTooLongError("Description", newDescription.Length, Limits.MaxDescriptionLength); - async Task SetIcon(ParsedImage img) - { - ctx.CheckOwnGroup(target); + var patch = new GroupPatch { Description = Partial.Present(newDescription) }; + await ctx.Repository.UpdateGroup(target.Id, patch); - img = await _avatarHosting.TryRehostImage(img, AvatarHostingService.RehostedImageType.Avatar, ctx.Author.Id, ctx.System); - await _avatarHosting.VerifyAvatarOrThrow(img.Url); + await ctx.Reply($"{Emojis.Success} Group description changed (using {newDescription.Length}/{Limits.MaxDescriptionLength} characters)."); + } - await ctx.Repository.UpdateGroup(target.Id, new GroupPatch { Icon = img.CleanUrl ?? img.Url }); + public async Task ShowGroupIcon(Context ctx, PKGroup target, ReplyFormat format) + { + var noIconSetMessage = "This group does not have an avatar set" + + (ctx.System?.Id == target.System + ? ". Set one by attaching an image to this command, or by passing an image URL or @mention." + : "."); - var msg = img.Source switch + ctx.CheckSystemPrivacy(target.System, target.IconPrivacy); + + // if we're doing a raw or plaintext query check for null + if (format != ReplyFormat.Standard) + if ((target.Icon?.Trim() ?? "").Length == 0) { - AvatarSource.User => - $"{Emojis.Success} Group icon changed to {img.SourceUser?.Username}'s avatar!\n{Emojis.Warn} If {img.SourceUser?.Username} changes their avatar, the group icon will need to be re-set.", - AvatarSource.Url => $"{Emojis.Success} Group icon changed to the image at the given URL.", - AvatarSource.HostedCdn => $"{Emojis.Success} Group icon changed to attached image.", - AvatarSource.Attachment => - $"{Emojis.Success} Group icon changed to attached image.\n{Emojis.Warn} If you delete the message containing the attachment, the group icon will stop working.", - _ => throw new ArgumentOutOfRangeException() - }; + await ctx.Reply(noIconSetMessage); + return; + } - // The attachment's already right there, no need to preview it. - var hasEmbed = img.Source != AvatarSource.Attachment && img.Source != AvatarSource.HostedCdn; - await (hasEmbed - ? ctx.Reply(msg, new EmbedBuilder().Image(new Embed.EmbedImage(img.Url)).Build()) - : ctx.Reply(msg)); - } - - async Task ShowIcon() + if (format == ReplyFormat.Raw) { - ctx.CheckSystemPrivacy(target.System, target.IconPrivacy); - - if ((target.Icon?.Trim() ?? "").Length > 0) - switch (ctx.MatchFormat()) - { - case ReplyFormat.Raw: - await ctx.Reply($"`{target.Icon.TryGetCleanCdnUrl()}`"); - break; - case ReplyFormat.Plaintext: - var ebP = new EmbedBuilder() - .Description($"Showing avatar for group {target.NameFor(ctx)} (`{target.DisplayHid(ctx.Config)}`)"); - await ctx.Reply(text: $"<{target.Icon.TryGetCleanCdnUrl()}>", embed: ebP.Build()); - break; - default: - var ebS = new EmbedBuilder() - .Title("Group icon") - .Image(new Embed.EmbedImage(target.Icon.TryGetCleanCdnUrl())); - if (target.System == ctx.System?.Id) - ebS.Description($"To clear, use `{ctx.DefaultPrefix}group {target.Reference(ctx)} icon -clear`."); - await ctx.Reply(embed: ebS.Build()); - break; - } - else - throw new PKSyntaxError( - "This group does not have an avatar set. Set one by attaching an image to this command, or by passing an image URL or @mention."); + await ctx.Reply($"`{target.Icon.TryGetCleanCdnUrl()}`"); + return; } - - if (ctx.MatchClear()) - await ClearIcon(); - else if (await ctx.MatchImage() is { } img) - await SetIcon(img); - else - await ShowIcon(); - } - - public async Task GroupBannerImage(Context ctx, PKGroup target) - { - async Task ClearBannerImage() + if (format == ReplyFormat.Plaintext) { - ctx.CheckOwnGroup(target); - await ctx.ConfirmClear("this group's banner image"); - - await ctx.Repository.UpdateGroup(target.Id, new GroupPatch { BannerImage = null }); - await ctx.Reply($"{Emojis.Success} Group banner image cleared."); - } - - async Task SetBannerImage(ParsedImage img) - { - ctx.CheckOwnGroup(target); - - img = await _avatarHosting.TryRehostImage(img, AvatarHostingService.RehostedImageType.Banner, ctx.Author.Id, ctx.System); - await _avatarHosting.VerifyAvatarOrThrow(img.Url, true); - - await ctx.Repository.UpdateGroup(target.Id, new GroupPatch { BannerImage = img.CleanUrl ?? img.Url }); - - var msg = img.Source switch - { - AvatarSource.Url => $"{Emojis.Success} Group banner image changed to the image at the given URL.", - AvatarSource.HostedCdn => $"{Emojis.Success} Group banner image changed to attached image.", - AvatarSource.Attachment => - $"{Emojis.Success} Group banner image changed to attached image.\n{Emojis.Warn} If you delete the message containing the attachment, the banner image will stop working.", - AvatarSource.User => throw new PKError("Cannot set a banner image to an user's avatar."), - _ => throw new ArgumentOutOfRangeException() - }; - - // The attachment's already right there, no need to preview it. - var hasEmbed = img.Source != AvatarSource.Attachment && img.Source != AvatarSource.HostedCdn; - await (hasEmbed - ? ctx.Reply(msg, new EmbedBuilder().Image(new Embed.EmbedImage(img.Url)).Build()) - : ctx.Reply(msg)); - } - - async Task ShowBannerImage() - { - ctx.CheckSystemPrivacy(target.System, target.BannerPrivacy); - - if ((target.BannerImage?.Trim() ?? "").Length > 0) - switch (ctx.MatchFormat()) - { - case ReplyFormat.Raw: - await ctx.Reply($"`{target.BannerImage.TryGetCleanCdnUrl()}`"); - break; - case ReplyFormat.Plaintext: - var ebP = new EmbedBuilder() - .Description($"Showing banner for group {target.NameFor(ctx)} (`{target.DisplayHid(ctx.Config)}`)"); - await ctx.Reply(text: $"<{target.BannerImage.TryGetCleanCdnUrl()}>", embed: ebP.Build()); - break; - default: - var ebS = new EmbedBuilder() - .Title("Group banner image") - .Image(new Embed.EmbedImage(target.BannerImage.TryGetCleanCdnUrl())); - if (target.System == ctx.System?.Id) - ebS.Description($"To clear, use `{ctx.DefaultPrefix}group {target.Reference(ctx)} banner clear`."); - await ctx.Reply(embed: ebS.Build()); - break; - } - else - throw new PKSyntaxError( - "This group does not have a banner image set. Set one by attaching an image to this command, or by passing an image URL or @mention."); - } - - if (ctx.MatchClear()) - await ClearBannerImage(); - else if (await ctx.MatchImage() is { } img) - await SetBannerImage(img); - else - await ShowBannerImage(); - } - - public async Task GroupColor(Context ctx, PKGroup target) - { - var isOwnSystem = ctx.System?.Id == target.System; - var matchedFormat = ctx.MatchFormat(); - var matchedClear = ctx.MatchClear(); - - if (!isOwnSystem || !(ctx.HasNext() || matchedClear)) - { - if (target.Color == null) - await ctx.Reply( - "This group does not have a color set." + (isOwnSystem ? $" To set one, type `{ctx.DefaultPrefix}group {target.Reference(ctx)} color `." : "")); - 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") - .Color(target.Color.ToDiscordColor()) - .Thumbnail(new Embed.EmbedThumbnail($"attachment://color.gif")) - .Description($"This group's color is **#{target.Color}**." - + (isOwnSystem ? $" To clear it, type `{ctx.DefaultPrefix}group {target.Reference(ctx)} color -clear`." : "")) - .Build(), - files: [MiscUtils.GenerateColorPreview(target.Color)]); + var ebP = new EmbedBuilder() + .Description($"Showing avatar for group {target.NameFor(ctx)} (`{target.DisplayHid(ctx.Config)}`)"); + await ctx.Reply(text: $"<{target.Icon.TryGetCleanCdnUrl()}>", embed: ebP.Build()); return; } - ctx.CheckSystem().CheckOwnGroup(target); - - if (matchedClear) + if ((target.Icon?.Trim() ?? "").Length == 0) { - await ctx.Repository.UpdateGroup(target.Id, new() { Color = Partial.Null() }); - - await ctx.Reply($"{Emojis.Success} Group color cleared."); + await ctx.Reply(noIconSetMessage); + return; } - else + + var ebS = new EmbedBuilder() + .Title("Group icon") + .Image(new Embed.EmbedImage(target.Icon.TryGetCleanCdnUrl())); + if (target.System == ctx.System?.Id) + ebS.Description($"To clear, use `{ctx.DefaultPrefix}group {target.Reference(ctx)} icon -clear`."); + await ctx.Reply(embed: ebS.Build()); + } + + public async Task ClearGroupIcon(Context ctx, PKGroup target) + { + ctx.CheckOwnGroup(target); + await ctx.ConfirmClear("this group's icon"); + + await ctx.Repository.UpdateGroup(target.Id, new GroupPatch { Icon = null }); + await ctx.Reply($"{Emojis.Success} Group icon cleared."); + } + + public async Task ChangeGroupIcon(Context ctx, PKGroup target, ParsedImage img) + { + ctx.CheckOwnGroup(target); + + img = await _avatarHosting.TryRehostImage(img, AvatarHostingService.RehostedImageType.Avatar, ctx.Author.Id, ctx.System); + await _avatarHosting.VerifyAvatarOrThrow(img.Url); + + await ctx.Repository.UpdateGroup(target.Id, new GroupPatch { Icon = img.CleanUrl ?? img.Url }); + + var msg = img.Source switch { - var color = ctx.RemainderOrNull(); + AvatarSource.User => + $"{Emojis.Success} Group icon changed to {img.SourceUser?.Username}'s avatar!\n{Emojis.Warn} If {img.SourceUser?.Username} changes their avatar, the group icon will need to be re-set.", + AvatarSource.Url => $"{Emojis.Success} Group icon changed to the image at the given URL.", + AvatarSource.HostedCdn => $"{Emojis.Success} Group icon changed to attached image.", + AvatarSource.Attachment => + $"{Emojis.Success} Group icon changed to attached image.\n{Emojis.Warn} If you delete the message containing the attachment, the group icon will stop working.", + _ => throw new ArgumentOutOfRangeException() + }; - if (color.StartsWith("#")) color = color.Substring(1); - if (!Regex.IsMatch(color, "^[0-9a-fA-F]{6}$")) throw Errors.InvalidColorError(color); + // The attachment's already right there, no need to preview it. + var hasEmbed = img.Source != AvatarSource.Attachment && img.Source != AvatarSource.HostedCdn; + await (hasEmbed + ? ctx.Reply(msg, new EmbedBuilder().Image(new Embed.EmbedImage(img.Url)).Build()) + : ctx.Reply(msg)); + } - var patch = new GroupPatch { Color = Partial.Present(color.ToLowerInvariant()) }; - await ctx.Repository.UpdateGroup(target.Id, patch); + public async Task ShowGroupBanner(Context ctx, PKGroup target, ReplyFormat format) + { + var noBannerSetMessage = "This group does not have a banner image set" + + (ctx.System?.Id == target.System + ? ". Set one by attaching an image to this command, or by passing an image URL or @mention." + : "."); - await ctx.Reply(embed: new EmbedBuilder() - .Title($"{Emojis.Success} Group color changed.") - .Color(color.ToDiscordColor()) - .Thumbnail(new Embed.EmbedThumbnail($"attachment://color.gif")) - .Build(), - files: [MiscUtils.GenerateColorPreview(color)]); + ctx.CheckSystemPrivacy(target.System, target.BannerPrivacy); + + // if we're doing a raw or plaintext query check for null + if (format != ReplyFormat.Standard) + if ((target.BannerImage?.Trim() ?? "").Length == 0) + { + await ctx.Reply(noBannerSetMessage); + return; + } + + if (format == ReplyFormat.Raw) + { + await ctx.Reply($"`{target.BannerImage.TryGetCleanCdnUrl()}`"); + return; } + if (format == ReplyFormat.Plaintext) + { + var ebP = new EmbedBuilder() + .Description($"Showing banner for group {target.NameFor(ctx)} (`{target.DisplayHid(ctx.Config)}`)"); + await ctx.Reply(text: $"<{target.BannerImage.TryGetCleanCdnUrl()}>", embed: ebP.Build()); + return; + } + + if ((target.BannerImage?.Trim() ?? "").Length == 0) + { + await ctx.Reply(noBannerSetMessage); + return; + } + + var ebS = new EmbedBuilder() + .Title("Group banner image") + .Image(new Embed.EmbedImage(target.BannerImage.TryGetCleanCdnUrl())); + if (target.System == ctx.System?.Id) + ebS.Description($"To clear, use `{ctx.DefaultPrefix}group {target.Reference(ctx)} banner clear`."); + await ctx.Reply(embed: ebS.Build()); + } + + public async Task ClearGroupBanner(Context ctx, PKGroup target) + { + ctx.CheckOwnGroup(target); + await ctx.ConfirmClear("this group's banner image"); + + await ctx.Repository.UpdateGroup(target.Id, new GroupPatch { BannerImage = null }); + await ctx.Reply($"{Emojis.Success} Group banner image cleared."); + } + + public async Task ChangeGroupBanner(Context ctx, PKGroup target, ParsedImage img) + { + ctx.CheckOwnGroup(target); + + img = await _avatarHosting.TryRehostImage(img, AvatarHostingService.RehostedImageType.Banner, ctx.Author.Id, ctx.System); + await _avatarHosting.VerifyAvatarOrThrow(img.Url, true); + + await ctx.Repository.UpdateGroup(target.Id, new GroupPatch { BannerImage = img.CleanUrl ?? img.Url }); + + var msg = img.Source switch + { + AvatarSource.Url => $"{Emojis.Success} Group banner image changed to the image at the given URL.", + AvatarSource.HostedCdn => $"{Emojis.Success} Group banner image changed to attached image.", + AvatarSource.Attachment => + $"{Emojis.Success} Group banner image changed to attached image.\n{Emojis.Warn} If you delete the message containing the attachment, the banner image will stop working.", + AvatarSource.User => throw new PKError("Cannot set a banner image to an user's avatar."), + _ => throw new ArgumentOutOfRangeException() + }; + + // The attachment's already right there, no need to preview it. + var hasEmbed = img.Source != AvatarSource.Attachment && img.Source != AvatarSource.HostedCdn; + await (hasEmbed + ? ctx.Reply(msg, new EmbedBuilder().Image(new Embed.EmbedImage(img.Url)).Build()) + : ctx.Reply(msg)); + } + + public async Task ShowGroupColor(Context ctx, PKGroup target, ReplyFormat format) + { + var noColorSetMessage = "This group does not have a color set" + + (ctx.System?.Id == target.System + ? $". To set one, type `{ctx.DefaultPrefix}group {target.Reference(ctx)} color `." + : "."); + + // if we're doing a raw or plaintext query check for null + if (format != ReplyFormat.Standard) + if (target.Color == null) + { + await ctx.Reply(noColorSetMessage); + return; + } + + if (format == ReplyFormat.Raw) + { + await ctx.Reply("```\n#" + target.Color + "\n```"); + return; + } + if (format == ReplyFormat.Plaintext) + { + await ctx.Reply(target.Color); + return; + } + + if (target.Color == null) + { + await ctx.Reply(noColorSetMessage); + return; + } + + var eb = new EmbedBuilder() + .Title("Group color") + .Color(target.Color.ToDiscordColor()) + .Thumbnail(new Embed.EmbedThumbnail($"attachment://color.gif")) + .Description($"This group's color is **#{target.Color}**."); + + if (ctx.System?.Id == target.System) + eb.Description(eb.Build().Description + $" To clear it, type `{ctx.DefaultPrefix}group {target.Reference(ctx)} color -clear`."); + + await ctx.Reply(embed: eb.Build(), files: [MiscUtils.GenerateColorPreview(target.Color)]); + } + + public async Task ClearGroupColor(Context ctx, PKGroup target) + { + ctx.CheckOwnGroup(target); + + await ctx.Repository.UpdateGroup(target.Id, new GroupPatch { Color = Partial.Null() }); + + await ctx.Reply($"{Emojis.Success} Group color cleared."); + } + + public async Task ChangeGroupColor(Context ctx, PKGroup target, string color) + { + ctx.CheckOwnGroup(target); + + if (color.StartsWith("#")) color = color.Substring(1); + if (!Regex.IsMatch(color, "^[0-9a-fA-F]{6}$")) throw Errors.InvalidColorError(color); + + var patch = new GroupPatch { Color = Partial.Present(color.ToLowerInvariant()) }; + await ctx.Repository.UpdateGroup(target.Id, patch); + + await ctx.Reply(embed: new EmbedBuilder() + .Title($"{Emojis.Success} Group color changed.") + .Color(color.ToDiscordColor()) + .Thumbnail(new Embed.EmbedThumbnail($"attachment://color.gif")) + .Build(), + files: [MiscUtils.GenerateColorPreview(color)]); } public async Task ListSystemGroups(Context ctx, PKSystem system, string? query, IHasListOptions flags) @@ -531,102 +559,97 @@ public class Groups await ctx.Reply(components: await _embeds.CreateGroupMessageComponents(ctx, system, target)); } - public async Task GroupPrivacy(Context ctx, PKGroup target, PrivacyLevel? newValueFromCommand) + public async Task ShowGroupPrivacy(Context ctx, PKGroup target) { ctx.CheckSystem().CheckOwnGroup(target); - // Display privacy settings - if (!ctx.HasNext() && newValueFromCommand == null) - { - await ctx.Reply(embed: new EmbedBuilder() - .Title($"Current privacy settings for {target.Name}") - .Field(new Embed.Field("Name", target.NamePrivacy.Explanation())) - .Field(new Embed.Field("Description", target.DescriptionPrivacy.Explanation())) - .Field(new Embed.Field("Banner", target.BannerPrivacy.Explanation())) - .Field(new Embed.Field("Icon", target.IconPrivacy.Explanation())) - .Field(new Embed.Field("Member list", target.ListPrivacy.Explanation())) - .Field(new Embed.Field("Metadata (creation date)", target.MetadataPrivacy.Explanation())) - .Field(new Embed.Field("Visibility", target.Visibility.Explanation())) - .Description( - $"To edit privacy settings, use the command:\n> {ctx.DefaultPrefix}group **{target.Reference(ctx)}** privacy **** ****\n\n- `subject` is one of `name`, `description`, `banner`, `icon`, `members`, `metadata`, `visibility`, or `all`\n- `level` is either `public` or `private`.") - .Build()); - return; - } - async Task SetAll(PrivacyLevel level) - { - await ctx.Repository.UpdateGroup(target.Id, new GroupPatch().WithAllPrivacy(level)); + await ctx.Reply(embed: new EmbedBuilder() + .Title($"Current privacy settings for {target.Name}") + .Field(new Embed.Field("Name", target.NamePrivacy.Explanation())) + .Field(new Embed.Field("Description", target.DescriptionPrivacy.Explanation())) + .Field(new Embed.Field("Banner", target.BannerPrivacy.Explanation())) + .Field(new Embed.Field("Icon", target.IconPrivacy.Explanation())) + .Field(new Embed.Field("Member list", target.ListPrivacy.Explanation())) + .Field(new Embed.Field("Metadata (creation date)", target.MetadataPrivacy.Explanation())) + .Field(new Embed.Field("Visibility", target.Visibility.Explanation())) + .Description( + $"To edit privacy settings, use the command:\n> {ctx.DefaultPrefix}group **{target.Reference(ctx)}** privacy **** ****\n\n- `subject` is one of `name`, `description`, `banner`, `icon`, `members`, `metadata`, `visibility`, or `all`\n- `level` is either `public` or `private`.") + .Build()); + } - if (level == PrivacyLevel.Private) - await ctx.Reply( - $"{Emojis.Success} All {target.Name}'s privacy settings have been set to **{level.LevelName()}**. Other accounts will now see nothing on the group card."); - else - await ctx.Reply( - $"{Emojis.Success} All {target.Name}'s privacy settings have been set to **{level.LevelName()}**. Other accounts will now see everything on the group card."); - } + public async Task SetAllGroupPrivacy(Context ctx, PKGroup target, PrivacyLevel level) + { + ctx.CheckOwnGroup(target); - async Task SetLevel(GroupPrivacySubject subject, PrivacyLevel level) - { - await ctx.Repository.UpdateGroup(target.Id, new GroupPatch().WithPrivacy(subject, level)); + await ctx.Repository.UpdateGroup(target.Id, new GroupPatch().WithAllPrivacy(level)); - var subjectName = subject switch - { - GroupPrivacySubject.Name => "name privacy", - GroupPrivacySubject.Description => "description privacy", - GroupPrivacySubject.Banner => "banner privacy", - GroupPrivacySubject.Icon => "icon privacy", - GroupPrivacySubject.List => "member list", - GroupPrivacySubject.Metadata => "metadata", - GroupPrivacySubject.Visibility => "visibility", - _ => throw new ArgumentOutOfRangeException($"Unknown privacy subject {subject}") - }; - - var explanation = (subject, level) switch - { - (GroupPrivacySubject.Name, PrivacyLevel.Private) => - "This group's name is now hidden from other systems, and will be replaced by the group's display name.", - (GroupPrivacySubject.Description, PrivacyLevel.Private) => - "This group's description is now hidden from other systems.", - (GroupPrivacySubject.Banner, PrivacyLevel.Private) => - "This group's banner is now hidden from other systems.", - (GroupPrivacySubject.Icon, PrivacyLevel.Private) => - "This group's icon is now hidden from other systems.", - (GroupPrivacySubject.Visibility, PrivacyLevel.Private) => - "This group is now hidden from group lists and member cards.", - (GroupPrivacySubject.Metadata, PrivacyLevel.Private) => - "This group's metadata (eg. creation date) is now hidden from other systems.", - (GroupPrivacySubject.List, PrivacyLevel.Private) => - "This group's member list is now hidden from other systems.", - - (GroupPrivacySubject.Name, PrivacyLevel.Public) => - "This group's name is no longer hidden from other systems.", - (GroupPrivacySubject.Description, PrivacyLevel.Public) => - "This group's description is no longer hidden from other systems.", - (GroupPrivacySubject.Banner, PrivacyLevel.Public) => - "This group's banner is no longer hidden from other systems.", - (GroupPrivacySubject.Icon, PrivacyLevel.Public) => - "This group's icon is no longer hidden from other systems.", - (GroupPrivacySubject.Visibility, PrivacyLevel.Public) => - "This group is no longer hidden from group lists and member cards.", - (GroupPrivacySubject.Metadata, PrivacyLevel.Public) => - "This group's metadata (eg. creation date) is no longer hidden from other systems.", - (GroupPrivacySubject.List, PrivacyLevel.Public) => - "This group's member list is no longer hidden from other systems.", - - _ => throw new InvalidOperationException($"Invalid subject/level tuple ({subject}, {level})") - }; - - var replyStr = $"{Emojis.Success} {target.Name}'s **{subjectName}** has been set to **{level.LevelName()}**. {explanation}"; - - if (subject == GroupPrivacySubject.Name && level == PrivacyLevel.Private && target.DisplayName == null) - replyStr += $"\n{Emojis.Warn} This group does not have a display name set, and name privacy **will not take effect**."; - - await ctx.Reply(replyStr); - } - - if (ctx.Match("all") || newValueFromCommand != null) - await SetAll(newValueFromCommand ?? ctx.PopPrivacyLevel()); + if (level == PrivacyLevel.Private) + await ctx.Reply( + $"{Emojis.Success} All {target.Name}'s privacy settings have been set to **{level.LevelName()}**. Other accounts will now see nothing on the group card."); else - await SetLevel(ctx.PopGroupPrivacySubject(), ctx.PopPrivacyLevel()); + await ctx.Reply( + $"{Emojis.Success} All {target.Name}'s privacy settings have been set to **{level.LevelName()}**. Other accounts will now see everything on the group card."); + } + + public async Task SetGroupPrivacy(Context ctx, PKGroup target, GroupPrivacySubject subject, PrivacyLevel level) + { + ctx.CheckOwnGroup(target); + + await ctx.Repository.UpdateGroup(target.Id, new GroupPatch().WithPrivacy(subject, level)); + + var subjectName = subject switch + { + GroupPrivacySubject.Name => "name privacy", + GroupPrivacySubject.Description => "description privacy", + GroupPrivacySubject.Banner => "banner privacy", + GroupPrivacySubject.Icon => "icon privacy", + GroupPrivacySubject.List => "member list", + GroupPrivacySubject.Metadata => "metadata", + GroupPrivacySubject.Visibility => "visibility", + _ => throw new ArgumentOutOfRangeException($"Unknown privacy subject {subject}") + }; + + var explanation = (subject, level) switch + { + (GroupPrivacySubject.Name, PrivacyLevel.Private) => + "This group's name is now hidden from other systems, and will be replaced by the group's display name.", + (GroupPrivacySubject.Description, PrivacyLevel.Private) => + "This group's description is now hidden from other systems.", + (GroupPrivacySubject.Banner, PrivacyLevel.Private) => + "This group's banner is now hidden from other systems.", + (GroupPrivacySubject.Icon, PrivacyLevel.Private) => + "This group's icon is now hidden from other systems.", + (GroupPrivacySubject.Visibility, PrivacyLevel.Private) => + "This group is now hidden from group lists and member cards.", + (GroupPrivacySubject.Metadata, PrivacyLevel.Private) => + "This group's metadata (eg. creation date) is now hidden from other systems.", + (GroupPrivacySubject.List, PrivacyLevel.Private) => + "This group's member list is now hidden from other systems.", + + (GroupPrivacySubject.Name, PrivacyLevel.Public) => + "This group's name is no longer hidden from other systems.", + (GroupPrivacySubject.Description, PrivacyLevel.Public) => + "This group's description is no longer hidden from other systems.", + (GroupPrivacySubject.Banner, PrivacyLevel.Public) => + "This group's banner is no longer hidden from other systems.", + (GroupPrivacySubject.Icon, PrivacyLevel.Public) => + "This group's icon is no longer hidden from other systems.", + (GroupPrivacySubject.Visibility, PrivacyLevel.Public) => + "This group is no longer hidden from group lists and member cards.", + (GroupPrivacySubject.Metadata, PrivacyLevel.Public) => + "This group's metadata (eg. creation date) is no longer hidden from other systems.", + (GroupPrivacySubject.List, PrivacyLevel.Public) => + "This group's member list is no longer hidden from other systems.", + + _ => throw new InvalidOperationException($"Invalid subject/level tuple ({subject}, {level})") + }; + + var replyStr = $"{Emojis.Success} {target.Name}'s **{subjectName}** has been set to **{level.LevelName()}**. {explanation}"; + + if (subject == GroupPrivacySubject.Name && level == PrivacyLevel.Private && target.DisplayName == null) + replyStr += $"\n{Emojis.Warn} This group does not have a display name set, and name privacy **will not take effect**."; + + await ctx.Reply(replyStr); } public async Task DeleteGroup(Context ctx, PKGroup target) diff --git a/crates/command_definitions/src/group.rs b/crates/command_definitions/src/group.rs index f884ed3b..99fe4047 100644 --- a/crates/command_definitions/src/group.rs +++ b/crates/command_definitions/src/group.rs @@ -16,6 +16,136 @@ pub fn cmds() -> impl Iterator { let group = group(); let group_target = targeted(); + let group_new = tokens!(group, ("new", ["n"])); + let group_new_cmd = + [command!(group_new, ("name", OpaqueString) => "group_new").help("Creates a new group")] + .into_iter(); + + let group_info_cmd = + [command!(group_target => "group_info").help("Shows information about a group")] + .into_iter(); + + let group_name = tokens!( + group_target, + ("name", ["rename", "changename", "setname", "rn"]) + ); + let group_name_cmd = [ + command!(group_name => "group_show_name").help("Shows the group's name"), + command!(group_name, ("clear", ["c"]) => "group_clear_name") + .flag(("yes", ["y"])) + .help("Clears the group's name"), + command!(group_name, ("name", OpaqueString) => "group_rename").help("Renames the group"), + ] + .into_iter(); + + let group_display_name = tokens!(group_target, ("displayname", ["dn", "nick", "nickname"])); + let group_display_name_cmd = [ + command!(group_display_name => "group_show_display_name") + .help("Shows the group's display name"), + command!(group_display_name, ("clear", ["c"]) => "group_clear_display_name") + .flag(("yes", ["y"])) + .help("Clears the group's display name"), + command!(group_display_name, ("name", OpaqueString) => "group_change_display_name") + .help("Changes the group's display name"), + ] + .into_iter(); + + let group_description = tokens!( + group_target, + ( + "description", + ["desc", "describe", "d", "bio", "info", "text", "intro"] + ) + ); + let group_description_cmd = [ + command!(group_description => "group_show_description") + .help("Shows the group's description"), + command!(group_description, ("clear", ["c"]) => "group_clear_description") + .flag(("yes", ["y"])) + .help("Clears the group's description"), + command!(group_description, ("description", OpaqueString) => "group_change_description") + .help("Changes the group's description"), + ] + .into_iter(); + + let group_icon = tokens!( + group_target, + ("icon", ["avatar", "picture", "image", "pic", "pfp"]) + ); + let group_icon_cmd = [ + command!(group_icon => "group_show_icon").help("Shows the group's icon"), + command!(group_icon, ("clear", ["c"]) => "group_clear_icon") + .flag(("yes", ["y"])) + .help("Clears the group's icon"), + command!(group_icon, ("icon", Avatar) => "group_change_icon") + .help("Changes the group's icon"), + ] + .into_iter(); + + let group_banner = tokens!(group_target, ("banner", ["splash", "cover"])); + let group_banner_cmd = [ + command!(group_banner => "group_show_banner").help("Shows the group's banner"), + command!(group_banner, ("clear", ["c"]) => "group_clear_banner") + .flag(("yes", ["y"])) + .help("Clears the group's banner"), + command!(group_banner, ("banner", Avatar) => "group_change_banner") + .help("Changes the group's banner"), + ] + .into_iter(); + + let group_color = tokens!(group_target, ("color", ["colour"])); + let group_color_cmd = [ + command!(group_color => "group_show_color").help("Shows the group's color"), + command!(group_color, ("clear", ["c"]) => "group_clear_color") + .flag(("yes", ["y"])) + .help("Clears the group's color"), + command!(group_color, ("color", OpaqueString) => "group_change_color") + .help("Changes the group's color"), + ] + .into_iter(); + + let group_privacy = tokens!(group_target, ("privacy", ["priv"])); + let group_privacy_cmd = [ + command!(group_privacy => "group_show_privacy") + .help("Shows the group's privacy settings"), + command!(group_privacy, ("all", ["a"]), ("level", PrivacyLevel) => "group_change_privacy_all") + .help("Changes all privacy settings for the group"), + command!(group_privacy, ("privacy", GroupPrivacyTarget), ("level", PrivacyLevel) => "group_change_privacy") + .help("Changes a specific privacy setting for the group"), + ] + .into_iter(); + + let group_public_cmd = [ + command!(group_target, ("public", ["pub"]) => "group_set_public") + .help("Sets the group to public"), + ] + .into_iter(); + + let group_private_cmd = [ + command!(group_target, ("private", ["priv"]) => "group_set_private") + .help("Sets the group to private"), + ] + .into_iter(); + + let group_delete_cmd = [ + command!(group_target, ("delete", ["destroy", "erase", "yeet"]) => "group_delete") + .flag(("yes", ["y"])) + .help("Deletes the group"), + ] + .into_iter(); + + let group_id_cmd = + [command!(group_target, "id" => "group_id").help("Shows the group's ID")].into_iter(); + + let group_front = tokens!(group_target, ("front", ["fronter", "fronters", "f"])); + let group_front_cmd = [ + command!(group_front, ("percent", ["p", "%"]) => "group_fronter_percent") + .flag(("duration", OpaqueString)) + .flag(("fronters-only", ["fo"])) + .flag("flat"), + ] + .into_iter(); + let apply_list_opts = |cmd: Command| cmd.flags(get_list_flags()); let group_list_members = tokens!(group_target, ("members", ["list", "ls"])); @@ -27,6 +157,14 @@ pub fn cmds() -> impl Iterator { .into_iter() .map(apply_list_opts); + let group_modify_members_cmd = [ + command!(group_target, "add", MemberRefs => "group_add_member") + .flag(("all", ["a"])), + command!(group_target, ("remove", ["delete", "del", "rem"]), MemberRefs => "group_remove_member") + .flag(("all", ["a"])), + ] + .into_iter(); + let system_groups_cmd = [ command!(group, ("list", ["ls"]) => "group_list_groups"), command!(group, ("search", ["find", "query"]), ("query", OpaqueStringRemainder) => "group_search_groups"), @@ -34,5 +172,21 @@ pub fn cmds() -> impl Iterator { .into_iter() .map(apply_list_opts); - system_groups_cmd.chain(group_list_members_cmd) + group_new_cmd + .chain(group_info_cmd) + .chain(group_name_cmd) + .chain(group_display_name_cmd) + .chain(group_description_cmd) + .chain(group_icon_cmd) + .chain(group_banner_cmd) + .chain(group_color_cmd) + .chain(group_privacy_cmd) + .chain(group_public_cmd) + .chain(group_private_cmd) + .chain(group_front_cmd) + .chain(group_modify_members_cmd) + .chain(group_delete_cmd) + .chain(group_id_cmd) + .chain(group_list_members_cmd) + .chain(system_groups_cmd) } diff --git a/crates/command_definitions/src/member.rs b/crates/command_definitions/src/member.rs index 00b90b6a..e855d4f0 100644 --- a/crates/command_definitions/src/member.rs +++ b/crates/command_definitions/src/member.rs @@ -301,7 +301,17 @@ pub fn cmds() -> impl Iterator { ] .into_iter() .map(|cmd| cmd.flags(get_list_flags())); - let member_group_cmds = member_list_group_cmds; + + let member_add_remove_group_cmds = [ + command!(member_group, "add", ("groups", GroupRefs) => "member_group_add") + .help("Adds a member to one or more groups"), + command!(member_group, ("remove", ["rem"]), ("groups", GroupRefs) => "member_group_remove") + .help("Removes a member from one or more groups"), + ] + .into_iter(); + + let member_display_id_cmd = + [command!(member_target, "id" => "member_id").help("Displays a member's ID")].into_iter(); let member_delete_cmd = [command!(member_target, delete => "member_delete").help("Deletes a member")].into_iter(); @@ -325,7 +335,9 @@ pub fn cmds() -> impl Iterator { .chain(member_avatar_cmds) .chain(member_proxy_settings_cmd) .chain(member_message_settings_cmd) + .chain(member_display_id_cmd) .chain(member_delete_cmd) .chain(member_easter_eggs) - .chain(member_group_cmds) + .chain(member_list_group_cmds) + .chain(member_add_remove_group_cmds) } diff --git a/crates/command_parser/src/parameter.rs b/crates/command_parser/src/parameter.rs index 0c38f532..9236ed86 100644 --- a/crates/command_parser/src/parameter.rs +++ b/crates/command_parser/src/parameter.rs @@ -13,9 +13,11 @@ pub enum ParameterValue { MemberRef(String), MemberRefs(Vec), GroupRef(String), + GroupRefs(Vec), SystemRef(String), GuildRef(String), MemberPrivacyTarget(String), + GroupPrivacyTarget(String), SystemPrivacyTarget(String), PrivacyLevel(String), Toggle(bool), @@ -50,9 +52,11 @@ impl Display for Parameter { ParameterKind::MemberRef => write!(f, ""), ParameterKind::MemberRefs => write!(f, " ..."), ParameterKind::GroupRef => write!(f, ""), + ParameterKind::GroupRefs => write!(f, " ..."), ParameterKind::SystemRef => write!(f, ""), ParameterKind::GuildRef => write!(f, ""), ParameterKind::MemberPrivacyTarget => write!(f, ""), + ParameterKind::GroupPrivacyTarget => write!(f, ""), ParameterKind::SystemPrivacyTarget => write!(f, ""), ParameterKind::PrivacyLevel => write!(f, "[privacy level]"), ParameterKind::Toggle => write!(f, "on/off"), @@ -86,9 +90,11 @@ pub enum ParameterKind { MemberRef, MemberRefs, GroupRef, + GroupRefs, SystemRef, GuildRef, MemberPrivacyTarget, + GroupPrivacyTarget, SystemPrivacyTarget, PrivacyLevel, Toggle, @@ -103,9 +109,11 @@ impl ParameterKind { ParameterKind::MemberRef => "target", ParameterKind::MemberRefs => "targets", ParameterKind::GroupRef => "target", + ParameterKind::GroupRefs => "targets", ParameterKind::SystemRef => "target", ParameterKind::GuildRef => "target", ParameterKind::MemberPrivacyTarget => "member_privacy_target", + ParameterKind::GroupPrivacyTarget => "group_privacy_target", ParameterKind::SystemPrivacyTarget => "system_privacy_target", ParameterKind::PrivacyLevel => "privacy_level", ParameterKind::Toggle => "toggle", @@ -116,7 +124,9 @@ impl ParameterKind { pub(crate) fn remainder(&self) -> bool { matches!( self, - ParameterKind::OpaqueStringRemainder | ParameterKind::MemberRefs + ParameterKind::OpaqueStringRemainder + | ParameterKind::MemberRefs + | ParameterKind::GroupRefs ) } @@ -127,6 +137,9 @@ impl ParameterKind { Ok(ParameterValue::OpaqueString(input.into())) } ParameterKind::GroupRef => Ok(ParameterValue::GroupRef(input.into())), + ParameterKind::GroupRefs => Ok(ParameterValue::GroupRefs( + input.split(' ').map(|s| s.trim().to_string()).collect(), + )), ParameterKind::MemberRef => Ok(ParameterValue::MemberRef(input.into())), ParameterKind::MemberRefs => Ok(ParameterValue::MemberRefs( input.split(' ').map(|s| s.trim().to_string()).collect(), @@ -134,6 +147,8 @@ impl ParameterKind { ParameterKind::SystemRef => Ok(ParameterValue::SystemRef(input.into())), ParameterKind::MemberPrivacyTarget => MemberPrivacyTargetKind::from_str(input) .map(|target| ParameterValue::MemberPrivacyTarget(target.as_ref().into())), + ParameterKind::GroupPrivacyTarget => GroupPrivacyTargetKind::from_str(input) + .map(|target| ParameterValue::GroupPrivacyTarget(target.as_ref().into())), ParameterKind::SystemPrivacyTarget => SystemPrivacyTargetKind::from_str(input) .map(|target| ParameterValue::SystemPrivacyTarget(target.as_ref().into())), ParameterKind::PrivacyLevel => PrivacyLevelKind::from_str(input) @@ -146,8 +161,13 @@ impl ParameterKind { } } - pub(crate) fn skip_if_cant_match(&self) -> bool { - matches!(self, ParameterKind::Toggle) + pub(crate) fn skip_if_cant_match(&self) -> Option> { + match self { + ParameterKind::Toggle => Some(None), + ParameterKind::MemberRefs => Some(Some(ParameterValue::MemberRefs(Vec::new()))), + ParameterKind::GroupRefs => Some(Some(ParameterValue::GroupRefs(Vec::new()))), + _ => None, + } } } @@ -200,6 +220,48 @@ impl FromStr for MemberPrivacyTargetKind { } } +pub enum GroupPrivacyTargetKind { + Name, + Icon, + Description, + Banner, + List, + Metadata, + Visibility, +} + +impl AsRef for GroupPrivacyTargetKind { + fn as_ref(&self) -> &str { + match self { + Self::Name => "name", + Self::Icon => "icon", + Self::Description => "description", + Self::Banner => "banner", + Self::List => "list", + Self::Metadata => "metadata", + Self::Visibility => "visibility", + } + } +} + +impl FromStr for GroupPrivacyTargetKind { + type Err = SmolStr; + + fn from_str(s: &str) -> Result { + // todo: this doesnt parse all the possible ways + match s.to_lowercase().as_str() { + "name" => Ok(Self::Name), + "avatar" | "icon" => Ok(Self::Icon), + "description" => Ok(Self::Description), + "banner" => Ok(Self::Banner), + "list" => Ok(Self::List), + "metadata" => Ok(Self::Metadata), + "visibility" => Ok(Self::Visibility), + _ => Err("invalid group privacy target".into()), + } + } +} + pub enum SystemPrivacyTargetKind { Name, Avatar, diff --git a/crates/command_parser/src/token.rs b/crates/command_parser/src/token.rs index 653b8a65..93c4e210 100644 --- a/crates/command_parser/src/token.rs +++ b/crates/command_parser/src/token.rs @@ -68,8 +68,14 @@ impl Token { value: matched, }, Err(err) => { - if param.kind().skip_if_cant_match() { - return None; + if let Some(maybe_empty) = param.kind().skip_if_cant_match() { + match maybe_empty { + Some(matched) => TokenMatchResult::MatchedParameter { + name: param.name().into(), + value: matched, + }, + None => return None, + } } else { TokenMatchResult::ParameterMatchError { input: input.into(), diff --git a/crates/commands/src/bin/write_cs_glue.rs b/crates/commands/src/bin/write_cs_glue.rs index d5eeb526..8108772b 100644 --- a/crates/commands/src/bin/write_cs_glue.rs +++ b/crates/commands/src/bin/write_cs_glue.rs @@ -258,8 +258,10 @@ fn get_param_ty(kind: ParameterKind) -> &'static str { ParameterKind::MemberRef => "PKMember", ParameterKind::MemberRefs => "List", ParameterKind::GroupRef => "PKGroup", + ParameterKind::GroupRefs => "List", ParameterKind::SystemRef => "PKSystem", ParameterKind::MemberPrivacyTarget => "MemberPrivacySubject", + ParameterKind::GroupPrivacyTarget => "GroupPrivacySubject", ParameterKind::SystemPrivacyTarget => "SystemPrivacySubject", ParameterKind::PrivacyLevel => "PrivacyLevel", ParameterKind::Toggle => "bool", @@ -274,8 +276,10 @@ fn get_param_param_ty(kind: ParameterKind) -> &'static str { ParameterKind::MemberRef => "Member", ParameterKind::MemberRefs => "Members", ParameterKind::GroupRef => "Group", + ParameterKind::GroupRefs => "Groups", ParameterKind::SystemRef => "System", ParameterKind::MemberPrivacyTarget => "MemberPrivacyTarget", + ParameterKind::GroupPrivacyTarget => "GroupPrivacyTarget", ParameterKind::SystemPrivacyTarget => "SystemPrivacyTarget", ParameterKind::PrivacyLevel => "PrivacyLevel", ParameterKind::Toggle => "Toggle", diff --git a/crates/commands/src/commands.udl b/crates/commands/src/commands.udl index 15e9849c..7011c463 100644 --- a/crates/commands/src/commands.udl +++ b/crates/commands/src/commands.udl @@ -11,9 +11,11 @@ interface Parameter { MemberRef(string member); MemberRefs(sequence members); GroupRef(string group); + GroupRefs(sequence groups); SystemRef(string system); GuildRef(string guild); MemberPrivacyTarget(string target); + GroupPrivacyTarget(string target); SystemPrivacyTarget(string target); PrivacyLevel(string level); OpaqueString(string raw); diff --git a/crates/commands/src/lib.rs b/crates/commands/src/lib.rs index 368cb81f..0e363781 100644 --- a/crates/commands/src/lib.rs +++ b/crates/commands/src/lib.rs @@ -25,9 +25,11 @@ pub enum Parameter { MemberRef { member: String }, MemberRefs { members: Vec }, GroupRef { group: String }, + GroupRefs { groups: Vec }, SystemRef { system: String }, GuildRef { guild: String }, MemberPrivacyTarget { target: String }, + GroupPrivacyTarget { target: String }, SystemPrivacyTarget { target: String }, PrivacyLevel { level: String }, OpaqueString { raw: String }, @@ -41,8 +43,10 @@ impl From for Parameter { ParameterValue::MemberRef(member) => Self::MemberRef { member }, ParameterValue::MemberRefs(members) => Self::MemberRefs { members }, ParameterValue::GroupRef(group) => Self::GroupRef { group }, + ParameterValue::GroupRefs(groups) => Self::GroupRefs { groups }, ParameterValue::SystemRef(system) => Self::SystemRef { system }, ParameterValue::MemberPrivacyTarget(target) => Self::MemberPrivacyTarget { target }, + ParameterValue::GroupPrivacyTarget(target) => Self::GroupPrivacyTarget { target }, ParameterValue::SystemPrivacyTarget(target) => Self::SystemPrivacyTarget { target }, ParameterValue::PrivacyLevel(level) => Self::PrivacyLevel { level }, ParameterValue::OpaqueString(raw) => Self::OpaqueString { raw }, From 1556b119f30608aab723f8f96681f7c4ca618fd1 Mon Sep 17 00:00:00 2001 From: dusk Date: Wed, 1 Oct 2025 12:27:06 +0000 Subject: [PATCH 102/179] implement rest of commands for system --- PluralKit.Bot/CommandMeta/CommandTree.cs | 99 +----------------------- crates/command_definitions/src/system.rs | 11 +++ 2 files changed, 14 insertions(+), 96 deletions(-) diff --git a/PluralKit.Bot/CommandMeta/CommandTree.cs b/PluralKit.Bot/CommandMeta/CommandTree.cs index e5d89931..c3107953 100644 --- a/PluralKit.Bot/CommandMeta/CommandTree.cs +++ b/PluralKit.Bot/CommandMeta/CommandTree.cs @@ -165,6 +165,9 @@ public partial class CommandTree Commands.SystemFronter(var param, var flags) => ctx.Execute(SystemFronter, m => m.Fronter(ctx, param.target)), Commands.SystemFronterHistory(var param, var flags) => ctx.Execute(SystemFrontHistory, m => m.FrontHistory(ctx, param.target, flags.clear)), Commands.SystemFronterPercent(var param, var flags) => ctx.Execute(SystemFrontPercent, m => m.FrontPercent(ctx, param.target, flags.duration, flags.fronters_only, flags.flat)), + Commands.SystemDisplayId(var param, _) => ctx.Execute(SystemId, m => m.DisplayId(ctx, param.target)), + Commands.SystemDisplayIdSelf => ctx.Execute(SystemId, m => m.DisplayId(ctx, ctx.System)), + Commands.SystemWebhook => ctx.Execute(null, m => m.SystemWebhook(ctx)), Commands.RandomSelf(_, var flags) => flags.group ? ctx.Execute(GroupRandom, m => m.Group(ctx, ctx.System, flags.all, flags.show_embed)) @@ -223,14 +226,6 @@ public partial class CommandTree ctx.Reply( $"{Emojis.Error} Parsed command {ctx.Parameters.Callback().AsCode()} not implemented in PluralKit.Bot!"), }; - if (ctx.Match("system", "s", "account", "acc")) - return HandleSystemCommand(ctx); - if (ctx.Match("member", "m")) - return HandleMemberCommand(ctx); - if (ctx.Match("group", "g")) - return HandleGroupCommand(ctx); - if (ctx.Match("switch", "sw")) - return HandleSwitchCommand(ctx); if (ctx.Match("commands", "cmd", "c")) return CommandHelpRoot(ctx); if (ctx.Match("ap", "autoproxy", "auto")) @@ -398,94 +393,6 @@ public partial class CommandTree $"{Emojis.Error} Unknown debug command {ctx.PeekArgument().AsCode()}. {availableCommandsStr}"); } - private async Task HandleSystemCommand(Context ctx) - { - if (ctx.Match("commands", "help")) - await PrintCommandList(ctx, "systems", SystemCommands); - - // todo: these aren't deprecated but also shouldn't be here - else if (ctx.Match("webhook", "hook")) - await ctx.Execute(null, m => m.SystemWebhook(ctx)); - - // finally, parse commands that *can* take a system target - else - { - // TODO: actually implement this - // // try matching a system ID - // var target = await ctx.MatchSystem(); - // var previousPtr = ctx.Parameters._ptr; - - // // if we have a parsed target and no more commands, don't bother with the command flow - // // we skip the `target != null` check here since the argument isn't be popped if it's not a system - // if (!ctx.HasNext()) - // { - // await ctx.Execute(SystemInfo, m => m.Query(ctx, target ?? ctx.System)); - // return; - // } - - // // hacky, but we need to CheckSystem(target) which throws a PKError - // // normally PKErrors are only handled in ctx.Execute - // try - // { - // await HandleSystemCommandTargeted(ctx, target ?? ctx.System); - // } - // catch (PKError e) - // { - // await ctx.Reply($"{Emojis.Error} {e.Message}"); - // return; - // } - - // // if we *still* haven't matched anything, the user entered an invalid command name or system reference - // if (ctx.Parameters._ptr == previousPtr) - // { - // if (!ctx.Parameters.Peek().TryParseHid(out _) && !ctx.Parameters.Peek().TryParseMention(out _)) - // { - // await PrintCommandNotFoundError(ctx, SystemCommands); - // return; - // } - - // var list = CreatePotentialCommandList(ctx.DefaultPrefix, SystemCommands); - // await ctx.Reply($"{Emojis.Error} {await CreateSystemNotFoundError(ctx)}\n\n" - // + $"Perhaps you meant to use one of the following commands?\n{list}"); - // } - } - } - - private async Task HandleSystemCommandTargeted(Context ctx, PKSystem target) - { - if (ctx.Match("id")) - await ctx.CheckSystem(target).Execute(SystemId, m => m.DisplayId(ctx, target)); - } - - private async Task HandleMemberCommand(Context ctx) - { - if (ctx.Match("commands", "help")) - await PrintCommandList(ctx, "members", MemberCommands); - else if (!ctx.HasNext()) - await PrintCommandExpectedError(ctx, MemberNew, MemberInfo, MemberRename, MemberDisplayName, - MemberServerName, MemberDesc, MemberPronouns, - MemberColor, MemberBirthday, MemberProxy, MemberDelete, MemberAvatar); - else - await ctx.Reply($"{Emojis.Error} {ctx.CreateNotFoundError("Member", ctx.PopArgument())}"); - } - - private async Task HandleGroupCommand(Context ctx) - { - // Commands with no group argument - if (ctx.Match("commands", "help")) - await PrintCommandList(ctx, "groups", GroupCommands); - else if (!ctx.HasNext()) - await PrintCommandExpectedError(ctx, GroupCommands); - else - await ctx.Reply($"{Emojis.Error} {ctx.CreateNotFoundError("Group", ctx.PopArgument())}"); - } - - private async Task HandleSwitchCommand(Context ctx) - { - await PrintCommandNotFoundError(ctx, Switch, SwitchOut, SwitchMove, SwitchEdit, SwitchEditOut, - SwitchDelete, SwitchCopy, SystemFronter, SystemFrontHistory); - } - private async Task CommandHelpRoot(Context ctx) { if (!ctx.HasNext()) diff --git a/crates/command_definitions/src/system.rs b/crates/command_definitions/src/system.rs index 86fcf55e..1d90398c 100644 --- a/crates/command_definitions/src/system.rs +++ b/crates/command_definitions/src/system.rs @@ -28,6 +28,10 @@ pub fn edit() -> impl Iterator { ] .into_iter(); + let system_webhook_cmd = [command!(system, ("webhook", ["hook"]) => "system_webhook") + .help("Creates a webhook for your system")] + .into_iter(); + let system_info_cmd = [ command!(system => "system_info_self").help("Shows information about your system"), command!(system_target, ("info", ["show", "view"]) => "system_info") @@ -281,7 +285,12 @@ pub fn edit() -> impl Iterator { .into_iter() .map(add_list_flags); + let system_display_id_self_cmd = + [command!(system, "id" => "system_display_id_self")].into_iter(); + let system_display_id_cmd = [command!(system_target, "id" => "system_display_id")].into_iter(); + system_new_cmd + .chain(system_webhook_cmd) .chain(system_name_self_cmd) .chain(system_server_name_self_cmd) .chain(system_description_self_cmd) @@ -293,6 +302,7 @@ pub fn edit() -> impl Iterator { .chain(system_server_avatar_self_cmd) .chain(system_banner_self_cmd) .chain(system_list_self_cmd) + .chain(system_display_id_self_cmd) .chain(system_delete) .chain(system_privacy_cmd) .chain(system_proxy_cmd) @@ -311,4 +321,5 @@ pub fn edit() -> impl Iterator { .chain(system_link) .chain(system_list_cmd) .chain(system_groups_cmd) + .chain(system_display_id_cmd) } From d2807f402da2a9ae98ef0784e5eeabb71b4bc9c8 Mon Sep 17 00:00:00 2001 From: dusk Date: Wed, 1 Oct 2025 13:54:37 +0000 Subject: [PATCH 103/179] implement rest of the fun commands --- PluralKit.Bot/CommandMeta/CommandTree.cs | 17 ++++++++--------- crates/command_definitions/src/fun.rs | 8 ++++++++ 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/PluralKit.Bot/CommandMeta/CommandTree.cs b/PluralKit.Bot/CommandMeta/CommandTree.cs index c3107953..3f9955ae 100644 --- a/PluralKit.Bot/CommandMeta/CommandTree.cs +++ b/PluralKit.Bot/CommandMeta/CommandTree.cs @@ -76,6 +76,14 @@ public partial class CommandTree Commands.CfgApTimeoutUpdate(var param, _) => ctx.Execute(null, m => m.EditAutoproxyTimeout(ctx, param.timeout)), Commands.FunThunder => ctx.Execute(null, m => m.Thunder(ctx)), Commands.FunMeow => ctx.Execute(null, m => m.Meow(ctx)), + Commands.FunPokemon => ctx.Execute(null, m => m.Mn(ctx)), + Commands.FunFire => ctx.Execute(null, m => m.Fire(ctx)), + Commands.FunFreeze => ctx.Execute(null, m => m.Freeze(ctx)), + Commands.FunStarstorm => ctx.Execute(null, m => m.Starstorm(ctx)), + Commands.FunFlash => ctx.Execute(null, m => m.Flash(ctx)), + Commands.FunRool => ctx.Execute(null, m => m.Rool(ctx)), + Commands.Amogus => ctx.Execute(null, m => m.Sus(ctx)), + Commands.FunError => ctx.Execute(null, m => m.Error(ctx)), Commands.SystemInfo(var param, var flags) => ctx.Execute(SystemInfo, m => m.Query(ctx, param.target, flags.all, flags.@public, flags.@private)), Commands.SystemInfoSelf(_, var flags) => ctx.Execute(SystemInfo, m => m.Query(ctx, ctx.System, flags.all, flags.@public, flags.@private)), Commands.SystemNew(var param, _) => ctx.Execute(SystemNew, m => m.New(ctx, null)), @@ -279,15 +287,6 @@ public partial class CommandTree if (ctx.Match("debug")) return ctx.Execute(ProxyCheck, m => m.MessageProxyCheck(ctx)); if (ctx.Match("invite")) return ctx.Execute(Invite, m => m.Invite(ctx)); - if (ctx.Match("mn")) return ctx.Execute(null, m => m.Mn(ctx)); - if (ctx.Match("fire")) return ctx.Execute(null, m => m.Fire(ctx)); - if (ctx.Match("thunder")) return ctx.Execute(null, m => m.Thunder(ctx)); - if (ctx.Match("freeze")) return ctx.Execute(null, m => m.Freeze(ctx)); - if (ctx.Match("starstorm")) return ctx.Execute(null, m => m.Starstorm(ctx)); - if (ctx.Match("flash")) return ctx.Execute(null, m => m.Flash(ctx)); - if (ctx.Match("rool")) return ctx.Execute(null, m => m.Rool(ctx)); - if (ctx.Match("sus")) return ctx.Execute(null, m => m.Sus(ctx)); - if (ctx.Match("error")) return ctx.Execute(null, m => m.Error(ctx)); if (ctx.Match("stats", "status")) return ctx.Execute(null, m => m.Stats(ctx)); if (ctx.Match("permcheck")) return ctx.Execute(PermCheck, m => m.PermCheckGuild(ctx)); diff --git a/crates/command_definitions/src/fun.rs b/crates/command_definitions/src/fun.rs index 4119b71d..f16c09e1 100644 --- a/crates/command_definitions/src/fun.rs +++ b/crates/command_definitions/src/fun.rs @@ -4,6 +4,14 @@ pub fn cmds() -> impl Iterator { [ command!("thunder" => "fun_thunder"), command!("meow" => "fun_meow"), + command!("mn" => "fun_pokemon"), + command!("fire" => "fun_fire"), + command!("freeze" => "fun_freeze"), + command!("starstorm" => "fun_starstorm"), + command!("flash" => "fun_flash"), + command!("rool" => "fun_rool"), + command!("sus" => "amogus"), + command!("error" => "fun_error"), ] .into_iter() } From c42385f01c5f489834ae19eccc3087def8364109 Mon Sep 17 00:00:00 2001 From: dusk Date: Wed, 1 Oct 2025 14:06:29 +0000 Subject: [PATCH 104/179] implement rest of api (tokens) commands --- PluralKit.Bot/CommandMeta/CommandTree.cs | 7 ++----- crates/command_definitions/src/api.rs | 8 ++++++++ crates/command_definitions/src/lib.rs | 1 + 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/PluralKit.Bot/CommandMeta/CommandTree.cs b/PluralKit.Bot/CommandMeta/CommandTree.cs index 3f9955ae..51448a3c 100644 --- a/PluralKit.Bot/CommandMeta/CommandTree.cs +++ b/PluralKit.Bot/CommandMeta/CommandTree.cs @@ -229,6 +229,8 @@ public partial class CommandTree Commands.GroupDelete(var param, var flags) => ctx.Execute(GroupDelete, g => g.DeleteGroup(ctx, param.target)), Commands.GroupId(var param, _) => ctx.Execute(GroupId, g => g.DisplayId(ctx, param.target)), Commands.GroupFronterPercent(var param, var flags) => ctx.Execute(GroupFrontPercent, g => g.FrontPercent(ctx, null, flags.duration, flags.fronters_only, flags.flat, param.target)), + Commands.TokenDisplay => ctx.Execute(TokenGet, m => m.GetToken(ctx)), + Commands.TokenRefresh => ctx.Execute(TokenRefresh, m => m.RefreshToken(ctx)), _ => // this should only ever occur when deving if commands are not implemented... ctx.Reply( @@ -242,11 +244,6 @@ public partial class CommandTree return HandleConfigCommand(ctx); if (ctx.Match("serverconfig", "guildconfig", "scfg")) return HandleServerConfigCommand(ctx); - if (ctx.Match("token")) - if (ctx.Match("refresh", "renew", "invalidate", "reroll", "regen")) - return ctx.Execute(TokenRefresh, m => m.RefreshToken(ctx)); - else - return ctx.Execute(TokenGet, m => m.GetToken(ctx)); if (ctx.Match("import")) return ctx.Execute(Import, m => m.Import(ctx)); if (ctx.Match("export")) diff --git a/crates/command_definitions/src/api.rs b/crates/command_definitions/src/api.rs index 8b137891..e9cd5746 100644 --- a/crates/command_definitions/src/api.rs +++ b/crates/command_definitions/src/api.rs @@ -1 +1,9 @@ +use super::*; +pub fn cmds() -> impl Iterator { + [ + command!("token" => "token_display"), + command!("token", ("refresh", ["renew", "regen", "reroll"]) => "token_refresh"), + ] + .into_iter() +} diff --git a/crates/command_definitions/src/lib.rs b/crates/command_definitions/src/lib.rs index ceb05a49..df197cbd 100644 --- a/crates/command_definitions/src/lib.rs +++ b/crates/command_definitions/src/lib.rs @@ -31,6 +31,7 @@ pub fn all() -> impl Iterator { .chain(fun::cmds()) .chain(switch::cmds()) .chain(random::cmds()) + .chain(api::cmds()) .map(|cmd| { cmd.hidden_flag(("plaintext", ["pt"])) .hidden_flag(("raw", ["r"])) From 2b304457cccc3c5113942dce29e8965b57f5a909 Mon Sep 17 00:00:00 2001 From: dusk Date: Wed, 1 Oct 2025 14:44:56 +0000 Subject: [PATCH 105/179] implement autoproxy commands --- PluralKit.Bot/CommandMeta/CommandTree.cs | 18 ++---- PluralKit.Bot/Commands/Autoproxy.cs | 61 ++++++++++++++------- crates/command_definitions/src/autoproxy.rs | 20 +++++++ crates/command_definitions/src/lib.rs | 1 + 4 files changed, 66 insertions(+), 34 deletions(-) diff --git a/PluralKit.Bot/CommandMeta/CommandTree.cs b/PluralKit.Bot/CommandMeta/CommandTree.cs index 51448a3c..1749aaec 100644 --- a/PluralKit.Bot/CommandMeta/CommandTree.cs +++ b/PluralKit.Bot/CommandMeta/CommandTree.cs @@ -231,6 +231,11 @@ public partial class CommandTree Commands.GroupFronterPercent(var param, var flags) => ctx.Execute(GroupFrontPercent, g => g.FrontPercent(ctx, null, flags.duration, flags.fronters_only, flags.flat, param.target)), Commands.TokenDisplay => ctx.Execute(TokenGet, m => m.GetToken(ctx)), Commands.TokenRefresh => ctx.Execute(TokenRefresh, m => m.RefreshToken(ctx)), + Commands.AutoproxyShow => ctx.Execute(AutoproxySet, m => m.SetAutoproxyMode(ctx, null)), + Commands.AutoproxyOff => ctx.Execute(AutoproxySet, m => m.SetAutoproxyMode(ctx, new Autoproxy.Mode.Off())), + Commands.AutoproxyLatch => ctx.Execute(AutoproxySet, m => m.SetAutoproxyMode(ctx, new Autoproxy.Mode.Latch())), + Commands.AutoproxyFront => ctx.Execute(AutoproxySet, m => m.SetAutoproxyMode(ctx, new Autoproxy.Mode.Front())), + Commands.AutoproxyMember(var param, _) => ctx.Execute(AutoproxySet, m => m.SetAutoproxyMode(ctx, new Autoproxy.Mode.Member(param.target))), _ => // this should only ever occur when deving if commands are not implemented... ctx.Reply( @@ -238,8 +243,6 @@ public partial class CommandTree }; if (ctx.Match("commands", "cmd", "c")) return CommandHelpRoot(ctx); - if (ctx.Match("ap", "autoproxy", "auto")) - return HandleAutoproxyCommand(ctx); if (ctx.Match("config", "cfg", "configure")) return HandleConfigCommand(ctx); if (ctx.Match("serverconfig", "guildconfig", "scfg")) @@ -451,17 +454,6 @@ public partial class CommandTree } } - private Task HandleAutoproxyCommand(Context ctx) - { - // ctx.CheckSystem(); - // oops, that breaks stuff! PKErrors before ctx.Execute don't actually do anything. - // so we just emulate checking and throwing an error. - if (ctx.System == null) - return ctx.Reply($"{Emojis.Error} {Errors.NoSystemError(ctx.DefaultPrefix).Message}"); - - return ctx.Execute(AutoproxySet, m => m.SetAutoproxyMode(ctx)); - } - private Task HandleConfigCommand(Context ctx) { if (ctx.System == null) diff --git a/PluralKit.Bot/Commands/Autoproxy.cs b/PluralKit.Bot/Commands/Autoproxy.cs index ddff335b..97d6ddd2 100644 --- a/PluralKit.Bot/Commands/Autoproxy.cs +++ b/PluralKit.Bot/Commands/Autoproxy.cs @@ -11,37 +11,51 @@ public class Autoproxy { private readonly IClock _clock; + public abstract record Mode() + { + public record Off() : Mode; + public record Latch() : Mode; + public record Front() : Mode; + public record Member(PKMember member) : Mode; + } + public Autoproxy(IClock clock) { _clock = clock; } - public async Task SetAutoproxyMode(Context ctx) + public async Task SetAutoproxyMode(Context ctx, Mode? mode = null) { - // no need to check account here, it's already done at CommandTree - ctx.CheckGuildContext(); + ctx.CheckSystem().CheckGuildContext(); // for now, just for guild // this also creates settings if there are none present var settings = await ctx.Repository.GetAutoproxySettings(ctx.System.Id, ctx.Guild.Id, null); - if (ctx.Match("off", "stop", "cancel", "no", "disable", "remove")) - await AutoproxyOff(ctx, settings); - else if (ctx.Match("latch", "last", "proxy", "stick", "sticky", "l")) - await AutoproxyLatch(ctx, settings); - else if (ctx.Match("front", "fronter", "switch", "f")) - await AutoproxyFront(ctx, settings); - else if (ctx.Match("member")) - throw new PKSyntaxError($"Member-mode autoproxy must target a specific member. Use the `{ctx.DefaultPrefix}autoproxy ` command, where `member` is the name or ID of a member in your system."); - else if (await ctx.MatchMember() is PKMember member) - await AutoproxyMember(ctx, member); - else if (!ctx.HasNext()) - await ctx.Reply(embed: await CreateAutoproxyStatusEmbed(ctx, settings)); - else - throw new PKSyntaxError($"Invalid autoproxy mode {ctx.PopArgument().AsCode()}."); + if (mode == null) + { + await AutoproxyShow(ctx, settings); + return; + } + + switch (mode) + { + case Mode.Off: + await AutoproxyOff(ctx, settings); + break; + case Mode.Latch: + await AutoproxyLatch(ctx, settings); + break; + case Mode.Front: + await AutoproxyFront(ctx, settings); + break; + case Mode.Member(var member): + await AutoproxyMember(ctx, member); + break; + } } - private async Task AutoproxyOff(Context ctx, AutoproxySettings settings) + public async Task AutoproxyOff(Context ctx, AutoproxySettings settings) { if (settings.AutoproxyMode == AutoproxyMode.Off) { @@ -54,7 +68,7 @@ public class Autoproxy } } - private async Task AutoproxyLatch(Context ctx, AutoproxySettings settings) + public async Task AutoproxyLatch(Context ctx, AutoproxySettings settings) { if (settings.AutoproxyMode == AutoproxyMode.Latch) { @@ -67,7 +81,7 @@ public class Autoproxy } } - private async Task AutoproxyFront(Context ctx, AutoproxySettings settings) + public async Task AutoproxyFront(Context ctx, AutoproxySettings settings) { if (settings.AutoproxyMode == AutoproxyMode.Front) { @@ -80,7 +94,7 @@ public class Autoproxy } } - private async Task AutoproxyMember(Context ctx, PKMember member) + public async Task AutoproxyMember(Context ctx, PKMember member) { ctx.CheckOwnMember(member); @@ -90,6 +104,11 @@ public class Autoproxy await ctx.Reply($"{Emojis.Success} Autoproxy set to **{member.NameFor(ctx)}** in this server."); } + public async Task AutoproxyShow(Context ctx, AutoproxySettings settings) + { + await ctx.Reply(embed: await CreateAutoproxyStatusEmbed(ctx, settings)); + } + private async Task CreateAutoproxyStatusEmbed(Context ctx, AutoproxySettings settings) { var commandList = $"**{ctx.DefaultPrefix}autoproxy latch** - Autoproxies as last-proxied member" diff --git a/crates/command_definitions/src/autoproxy.rs b/crates/command_definitions/src/autoproxy.rs index 8b137891..68a1925b 100644 --- a/crates/command_definitions/src/autoproxy.rs +++ b/crates/command_definitions/src/autoproxy.rs @@ -1 +1,21 @@ +use super::*; +pub fn autoproxy() -> (&'static str, [&'static str; 2]) { + ("autoproxy", ["ap", "auto"]) +} + +pub fn cmds() -> impl Iterator { + let ap = autoproxy(); + + [ + command!(ap => "autoproxy_show").help("Shows your current autoproxy settings"), + command!(ap, ("off", ["stop", "cancel", "no", "disable", "remove"]) => "autoproxy_off") + .help("Disables autoproxy"), + command!(ap, ("latch", ["last", "proxy", "stick", "sticky", "l"]) => "autoproxy_latch") + .help("Sets autoproxy to latch mode"), + command!(ap, ("front", ["fronter", "switch", "f"]) => "autoproxy_front") + .help("Sets autoproxy to front mode"), + command!(ap, MemberRef => "autoproxy_member").help("Sets autoproxy to a specific member"), + ] + .into_iter() +} diff --git a/crates/command_definitions/src/lib.rs b/crates/command_definitions/src/lib.rs index df197cbd..e79558e4 100644 --- a/crates/command_definitions/src/lib.rs +++ b/crates/command_definitions/src/lib.rs @@ -32,6 +32,7 @@ pub fn all() -> impl Iterator { .chain(switch::cmds()) .chain(random::cmds()) .chain(api::cmds()) + .chain(autoproxy::cmds()) .map(|cmd| { cmd.hidden_flag(("plaintext", ["pt"])) .hidden_flag(("raw", ["r"])) From e4f38c76a98d6f12438fe3dbad98f7744c39d096 Mon Sep 17 00:00:00 2001 From: dusk Date: Fri, 3 Oct 2025 02:21:12 +0000 Subject: [PATCH 106/179] implement proxied message and permcheck commands --- Cargo.lock | 1 + PluralKit.Bot/CommandMeta/CommandTree.cs | 48 +++---------- .../Context/ContextArgumentsExt.cs | 15 ++-- .../Context/ContextEntityArgumentsExt.cs | 20 ------ .../Context/ContextParametersExt.cs | 16 +++++ PluralKit.Bot/CommandSystem/Parameters.cs | 10 ++- PluralKit.Bot/Commands/Autoproxy.cs | 8 +-- PluralKit.Bot/Commands/Checks.cs | 50 +++----------- PluralKit.Bot/Commands/Message.cs | 31 ++------- crates/command_definitions/src/checks.rs | 1 - crates/command_definitions/src/debug.rs | 15 ++++ crates/command_definitions/src/help.rs | 1 + crates/command_definitions/src/lib.rs | 3 +- crates/command_definitions/src/message.rs | 31 +++++++++ crates/command_parser/Cargo.toml | 1 + crates/command_parser/src/parameter.rs | 61 ++++++++++++++++- crates/commands/src/bin/write_cs_glue.rs | 4 ++ crates/commands/src/commands.udl | 4 +- crates/commands/src/lib.rs | 68 +++++++++++++++---- 19 files changed, 233 insertions(+), 155 deletions(-) delete mode 100644 crates/command_definitions/src/checks.rs diff --git a/Cargo.lock b/Cargo.lock index 42154562..f0469c2a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -664,6 +664,7 @@ version = "0.1.0" dependencies = [ "lazy_static", "ordermap", + "regex", "smol_str", ] diff --git a/PluralKit.Bot/CommandMeta/CommandTree.cs b/PluralKit.Bot/CommandMeta/CommandTree.cs index 1749aaec..41b6af9c 100644 --- a/PluralKit.Bot/CommandMeta/CommandTree.cs +++ b/PluralKit.Bot/CommandMeta/CommandTree.cs @@ -8,6 +8,7 @@ public partial class CommandTree { return command switch { + Commands.Explain => ctx.Execute(Explain, m => m.Explain(ctx)), Commands.Help(_, var flags) => ctx.Execute(Help, m => m.HelpRoot(ctx, flags.show_embed)), Commands.HelpCommands => ctx.Reply( "For the list of commands, see the website: "), @@ -236,6 +237,14 @@ public partial class CommandTree Commands.AutoproxyLatch => ctx.Execute(AutoproxySet, m => m.SetAutoproxyMode(ctx, new Autoproxy.Mode.Latch())), Commands.AutoproxyFront => ctx.Execute(AutoproxySet, m => m.SetAutoproxyMode(ctx, new Autoproxy.Mode.Front())), Commands.AutoproxyMember(var param, _) => ctx.Execute(AutoproxySet, m => m.SetAutoproxyMode(ctx, new Autoproxy.Mode.Member(param.target))), + Commands.PermcheckChannel(var param, _) => ctx.Execute(PermCheck, m => m.PermCheckChannel(ctx, param.target)), + Commands.PermcheckGuild(var param, _) => ctx.Execute(PermCheck, m => m.PermCheckGuild(ctx, param.target)), + Commands.MessageProxyCheck(var param, _) => ctx.Execute(ProxyCheck, m => m.MessageProxyCheck(ctx, param.target)), + Commands.MessageInfo(var param, var flags) => ctx.Execute(Message, m => m.GetMessage(ctx, param.target.MessageId, flags.GetReplyFormat(), flags.delete, flags.author)), + Commands.MessageAuthor(var param, var flags) => ctx.Execute(Message, m => m.GetMessage(ctx, param.target.MessageId, flags.GetReplyFormat(), false, true)), + Commands.MessageDelete(var param, var flags) => ctx.Execute(Message, m => m.GetMessage(ctx, param.target.MessageId, flags.GetReplyFormat(), true, false)), + Commands.MessageEdit(var param, var flags) => ctx.Execute(MessageEdit, m => m.EditMessage(ctx, param.target.MessageId, param.new_content, flags.regex, flags.mutate_space, flags.append, flags.prepend, flags.clear_embeds, flags.clear_attachments)), + Commands.MessageReproxy(var param, _) => ctx.Execute(MessageReproxy, m => m.ReproxyMessage(ctx, param.target.MessageId)), _ => // this should only ever occur when deving if commands are not implemented... ctx.Reply( @@ -251,16 +260,6 @@ public partial class CommandTree return ctx.Execute(Import, m => m.Import(ctx)); if (ctx.Match("export")) return ctx.Execute(Export, m => m.Export(ctx)); - if (ctx.Match("explain")) - return ctx.Execute(Explain, m => m.Explain(ctx)); - if (ctx.Match("message", "msg", "messageinfo")) - return ctx.Execute(Message, m => m.GetMessage(ctx)); - if (ctx.Match("edit", "e")) - return ctx.Execute(MessageEdit, m => m.EditMessage(ctx, false)); - if (ctx.Match("x")) - return ctx.Execute(MessageEdit, m => m.EditMessage(ctx, true)); - if (ctx.Match("reproxy", "rp", "crimes", "crime")) - return ctx.Execute(MessageReproxy, m => m.ReproxyMessage(ctx)); if (ctx.Match("log")) if (ctx.Match("channel")) return ctx.Execute(LogChannel, m => m.SetLogChannel(ctx), true); @@ -283,17 +282,8 @@ public partial class CommandTree return ctx.Execute(BlacklistShow, m => m.ShowProxyBlacklisted(ctx), true); else return ctx.Reply($"{Emojis.Warn} Blacklist commands have moved to `{ctx.DefaultPrefix}serverconfig`."); - if (ctx.Match("proxy")) - if (ctx.Match("debug")) - return ctx.Execute(ProxyCheck, m => m.MessageProxyCheck(ctx)); if (ctx.Match("invite")) return ctx.Execute(Invite, m => m.Invite(ctx)); if (ctx.Match("stats", "status")) return ctx.Execute(null, m => m.Stats(ctx)); - if (ctx.Match("permcheck")) - return ctx.Execute(PermCheck, m => m.PermCheckGuild(ctx)); - if (ctx.Match("proxycheck")) - return ctx.Execute(ProxyCheck, m => m.MessageProxyCheck(ctx)); - if (ctx.Match("debug")) - return HandleDebugCommand(ctx); if (ctx.Match("admin")) return HandleAdminCommand(ctx); if (ctx.Match("dashboard", "dash")) @@ -372,26 +362,6 @@ public partial class CommandTree await ctx.Reply($"{Emojis.Error} Unknown command."); } - private async Task HandleDebugCommand(Context ctx) - { - var availableCommandsStr = "Available debug targets: `permissions`, `proxying`"; - - if (ctx.Match("permissions", "perms", "permcheck")) - if (ctx.Match("channel", "ch")) - await ctx.Execute(PermCheck, m => m.PermCheckChannel(ctx)); - else - await ctx.Execute(PermCheck, m => m.PermCheckGuild(ctx)); - else if (ctx.Match("channel")) - await ctx.Execute(PermCheck, m => m.PermCheckChannel(ctx)); - else if (ctx.Match("proxy", "proxying", "proxycheck")) - await ctx.Execute(ProxyCheck, m => m.MessageProxyCheck(ctx)); - else if (!ctx.HasNext()) - await ctx.Reply($"{Emojis.Error} You need to pass a command. {availableCommandsStr}"); - else - await ctx.Reply( - $"{Emojis.Error} Unknown debug command {ctx.PeekArgument().AsCode()}. {availableCommandsStr}"); - } - private async Task CommandHelpRoot(Context ctx) { if (!ctx.HasNext()) diff --git a/PluralKit.Bot/CommandSystem/Context/ContextArgumentsExt.cs b/PluralKit.Bot/CommandSystem/Context/ContextArgumentsExt.cs index b967d62e..48006cbb 100644 --- a/PluralKit.Bot/CommandSystem/Context/ContextArgumentsExt.cs +++ b/PluralKit.Bot/CommandSystem/Context/ContextArgumentsExt.cs @@ -106,25 +106,24 @@ public static class ContextArgumentsExt else return null; } - public static (ulong? messageId, ulong? channelId) MatchMessage(this Context ctx, bool parseRawMessageId) + public static (ulong? messageId, ulong? channelId) GetRepliedTo(this Context ctx) { if (ctx.Message.Type == Message.MessageType.Reply && ctx.Message.MessageReference?.MessageId != null) return (ctx.Message.MessageReference.MessageId, ctx.Message.MessageReference.ChannelId); + return (null, null); + } - var word = ctx.PeekArgument(); - if (word == null) - return (null, null); - - if (parseRawMessageId && ulong.TryParse(word, out var mid)) + public static (ulong? messageId, ulong? channelId) ParseMessage(this Context ctx, string maybeMessageRef, bool parseRawMessageId) + { + if (parseRawMessageId && ulong.TryParse(maybeMessageRef, out var mid)) return (mid, null); - var match = Regex.Match(word, "https://(?:\\w+.)?discord(?:app)?.com/channels/\\d+/(\\d+)/(\\d+)"); + var match = Regex.Match(maybeMessageRef, "https://(?:\\w+.)?discord(?:app)?.com/channels/\\d+/(\\d+)/(\\d+)"); if (!match.Success) return (null, null); var channelId = ulong.Parse(match.Groups[1].Value); var messageId = ulong.Parse(match.Groups[2].Value); - ctx.PopArgument(); return (messageId, channelId); } } diff --git a/PluralKit.Bot/CommandSystem/Context/ContextEntityArgumentsExt.cs b/PluralKit.Bot/CommandSystem/Context/ContextEntityArgumentsExt.cs index bf0fe27b..43d87375 100644 --- a/PluralKit.Bot/CommandSystem/Context/ContextEntityArgumentsExt.cs +++ b/PluralKit.Bot/CommandSystem/Context/ContextEntityArgumentsExt.cs @@ -213,24 +213,4 @@ public static class ContextEntityArgumentsExt ctx.PopArgument(); return channel; } - - public static async Task ParseGuild(this Context ctx, string input) - { - if (!ulong.TryParse(input, out var id)) - return null; - - return await ctx.Rest.GetGuildOrNull(id); - } - - public static async Task MatchGuild(this Context ctx) - { - if (!ulong.TryParse(ctx.PeekArgument(), out var id)) - return null; - - var guild = await ctx.Rest.GetGuildOrNull(id); - if (guild != null) - ctx.PopArgument(); - - return guild; - } } \ No newline at end of file diff --git a/PluralKit.Bot/CommandSystem/Context/ContextParametersExt.cs b/PluralKit.Bot/CommandSystem/Context/ContextParametersExt.cs index 63e701d5..fe60eb5f 100644 --- a/PluralKit.Bot/CommandSystem/Context/ContextParametersExt.cs +++ b/PluralKit.Bot/CommandSystem/Context/ContextParametersExt.cs @@ -100,6 +100,22 @@ public static class ContextParametersExt ); } + public static async Task ParamResolveMessage(this Context ctx, string param_name) + { + return await ctx.Parameters.ResolveParameter( + ctx, param_name, + param => (param as Parameter.MessageRef)?.message + ); + } + + public static async Task ParamResolveChannel(this Context ctx, string param_name) + { + return await ctx.Parameters.ResolveParameter( + ctx, param_name, + param => (param as Parameter.ChannelRef)?.channel + ); + } + public static async Task ParamResolveGuild(this Context ctx, string param_name) { return await ctx.Parameters.ResolveParameter( diff --git a/PluralKit.Bot/CommandSystem/Parameters.cs b/PluralKit.Bot/CommandSystem/Parameters.cs index 0c440b61..3d63efdd 100644 --- a/PluralKit.Bot/CommandSystem/Parameters.cs +++ b/PluralKit.Bot/CommandSystem/Parameters.cs @@ -13,6 +13,8 @@ public abstract record Parameter() public record GroupRef(PKGroup group): Parameter; public record GroupRefs(List groups): Parameter; public record SystemRef(PKSystem system): Parameter; + public record MessageRef(Message.Reference message): Parameter; + public record ChannelRef(Channel channel): Parameter; public record GuildRef(Guild guild): Parameter; public record MemberPrivacyTarget(MemberPrivacySubject target): Parameter; public record GroupPrivacyTarget(GroupPrivacySubject target): Parameter; @@ -118,8 +120,12 @@ public class Parameters return new Parameter.Opaque(opaque.raw); case uniffi.commands.Parameter.Avatar avatar: return new Parameter.Avatar(await ctx.GetUserPfp(avatar.avatar) ?? ctx.ParseImage(avatar.avatar)); - case uniffi.commands.Parameter.GuildRef guildRef: - return new Parameter.GuildRef(await ctx.ParseGuild(guildRef.guild) ?? throw new PKError($"Guild {guildRef.guild} not found")); + case uniffi.commands.Parameter.MessageRef(var guildId, var channelId, var messageId): + return new Parameter.MessageRef(new Message.Reference(guildId, channelId, messageId)); + case uniffi.commands.Parameter.ChannelRef(var channelId): + return new Parameter.ChannelRef(await ctx.Rest.GetChannelOrNull(channelId) ?? throw new PKError($"Channel {channelId} not found")); + case uniffi.commands.Parameter.GuildRef(var guildId): + return new Parameter.GuildRef(await ctx.Rest.GetGuildOrNull(guildId) ?? throw new PKError($"Guild {guildId} not found")); } return null; } diff --git a/PluralKit.Bot/Commands/Autoproxy.cs b/PluralKit.Bot/Commands/Autoproxy.cs index 97d6ddd2..39e57e42 100644 --- a/PluralKit.Bot/Commands/Autoproxy.cs +++ b/PluralKit.Bot/Commands/Autoproxy.cs @@ -13,10 +13,10 @@ public class Autoproxy public abstract record Mode() { - public record Off() : Mode; - public record Latch() : Mode; - public record Front() : Mode; - public record Member(PKMember member) : Mode; + public record Off(): Mode; + public record Latch(): Mode; + public record Front(): Mode; + public record Member(PKMember member): Mode; } public Autoproxy(IClock clock) diff --git a/PluralKit.Bot/Commands/Checks.cs b/PluralKit.Bot/Commands/Checks.cs index fa2f6e37..26a53354 100644 --- a/PluralKit.Bot/Commands/Checks.cs +++ b/PluralKit.Bot/Commands/Checks.cs @@ -36,37 +36,11 @@ public class Checks _cache = cache; } - public async Task PermCheckGuild(Context ctx) + public async Task PermCheckGuild(Context ctx, Guild guild) { - Guild guild; - GuildMemberPartial senderGuildUser = null; - - if (ctx.Guild != null && !ctx.HasNext()) - { - guild = ctx.Guild; - senderGuildUser = ctx.Member; - } - else - { - var guildIdStr = ctx.RemainderOrNull() ?? - throw new PKSyntaxError("You must pass a server ID or run this command in a server."); - if (!ulong.TryParse(guildIdStr, out var guildId)) - throw new PKSyntaxError($"Could not parse {guildIdStr.AsCode()} as an ID."); - - try - { - guild = await _rest.GetGuild(guildId); - } - catch (ForbiddenException) - { - throw Errors.GuildNotFound(guildId); - } - - if (guild != null) - senderGuildUser = await _rest.GetGuildMember(guildId, ctx.Author.Id); - if (guild == null || senderGuildUser == null) - throw Errors.GuildNotFound(guildId); - } + var senderGuildUser = await _rest.GetGuildMember(guild.Id, ctx.Author.Id); + if (senderGuildUser == null) + throw Errors.GuildNotFound(guild.Id); var guildMember = await _rest.GetGuildMember(guild.Id, _botConfig.ClientId); @@ -135,17 +109,13 @@ public class Checks await ctx.Reply(embed: eb.Build()); } - public async Task PermCheckChannel(Context ctx) + public async Task PermCheckChannel(Context ctx, Channel channel) { - if (!ctx.HasNext()) - throw new PKSyntaxError("You need to specify a channel."); - var error = "Channel not found or you do not have permissions to access it."; // todo: this breaks if channel is not in cache and bot does not have View Channel permissions // with new cache it breaks if channel is not in current guild - var channel = await ctx.MatchChannel(); - if (channel == null || channel.GuildId == null) + if (channel.GuildId == null) throw new PKError(error); var guild = await _rest.GetGuildOrNull(channel.GuildId.Value); @@ -189,15 +159,17 @@ public class Checks await ctx.Reply(embed: eb.Build()); } - public async Task MessageProxyCheck(Context ctx) + public async Task MessageProxyCheck(Context ctx, Message.Reference? messageReference) { - if (!ctx.HasNext() && ctx.Message.MessageReference == null) + if (messageReference == null && ctx.Message.MessageReference == null) throw new PKSyntaxError("You need to specify a message."); var failedToGetMessage = "Could not find a valid message to check, was not able to fetch the message, or the message was not sent by you."; - var (messageId, channelId) = ctx.MatchMessage(false); + var (messageId, channelId) = ctx.GetRepliedTo(); + if (messageReference != null) + (messageId, channelId) = (messageReference.MessageId, messageReference.ChannelId); if (messageId == null || channelId == null) throw new PKError(failedToGetMessage); diff --git a/PluralKit.Bot/Commands/Message.cs b/PluralKit.Bot/Commands/Message.cs index ec34cea9..0395cf87 100644 --- a/PluralKit.Bot/Commands/Message.cs +++ b/PluralKit.Bot/Commands/Message.cs @@ -58,9 +58,9 @@ public class ProxiedMessage _redisService = redisService; } - public async Task ReproxyMessage(Context ctx) + public async Task ReproxyMessage(Context ctx, ulong? messageId) { - var (msg, systemId) = await GetMessageToEdit(ctx, ReproxyTimeout, true); + var (msg, systemId) = await GetMessageToEdit(ctx, messageId, ReproxyTimeout, true); if (ctx.System.Id != systemId) throw new PKError("Can't reproxy a message sent by a different system."); @@ -93,9 +93,9 @@ public class ProxiedMessage } } - public async Task EditMessage(Context ctx, bool useRegex) + public async Task EditMessage(Context ctx, ulong? messageId, string newContent, bool useRegex, bool mutateSpace, bool append, bool prepend, bool clearEmbeds, bool clearAttachments) { - var (msg, systemId) = await GetMessageToEdit(ctx, EditTimeout, false); + var (msg, systemId) = await GetMessageToEdit(ctx, messageId, EditTimeout, false); if (ctx.System.Id != systemId) throw new PKError("Can't edit a message sent by a different system."); @@ -104,21 +104,10 @@ public class ProxiedMessage if (originalMsg == null) throw new PKError("Could not edit message."); - // Regex flag - useRegex = useRegex || ctx.MatchFlag("regex", "x"); - - // Check if we should append or prepend - var mutateSpace = ctx.MatchFlag("nospace", "ns") ? "" : " "; - var append = ctx.MatchFlag("append", "a"); - var prepend = ctx.MatchFlag("prepend", "p"); - // Grab the original message content and new message content var originalContent = originalMsg.Content; - var newContent = ctx.RemainderOrNull()?.NormalizeLineEndSpacing(); // Should we clear embeds? - var clearEmbeds = ctx.MatchFlag("clear-embed", "ce"); - var clearAttachments = ctx.MatchFlag("clear-attachments", "ca"); if ((clearEmbeds || clearAttachments) && newContent == null) newContent = originalMsg.Content!; @@ -249,14 +238,13 @@ public class ProxiedMessage } } - private async Task<(PKMessage, SystemId)> GetMessageToEdit(Context ctx, Duration timeout, bool isReproxy) + private async Task<(PKMessage, SystemId)> GetMessageToEdit(Context ctx, ulong? referencedMessage, Duration timeout, bool isReproxy) { var editType = isReproxy ? "reproxy" : "edit"; var editTypeAction = isReproxy ? "reproxied" : "edited"; PKMessage? msg = null; - var (referencedMessage, _) = ctx.MatchMessage(false); if (referencedMessage != null) { await using var conn = await ctx.Database.Obtain(); @@ -332,9 +320,8 @@ public class ProxiedMessage return lastMessage; } - public async Task GetMessage(Context ctx) + public async Task GetMessage(Context ctx, ulong? messageId, ReplyFormat format, bool isDelete, bool author) { - var (messageId, _) = ctx.MatchMessage(true); if (messageId == null) { if (!ctx.HasNext()) @@ -342,8 +329,6 @@ public class ProxiedMessage throw new PKSyntaxError($"Could not parse {ctx.PeekArgument().AsCode()} as a message ID or link."); } - var isDelete = ctx.Match("delete") || ctx.MatchFlag("delete"); - var message = await ctx.Repository.GetFullMessage(messageId.Value); if (message == null) { @@ -360,8 +345,6 @@ public class ProxiedMessage else if (!await ctx.CheckPermissionsInGuildChannel(channel, PermissionSet.ViewChannel)) showContent = false; - var format = ctx.MatchFormat(); - if (format != ReplyFormat.Standard) { var discordMessage = await _rest.GetMessageOrNull(message.Message.Channel, message.Message.Mid); @@ -423,7 +406,7 @@ public class ProxiedMessage return; } - if (ctx.Match("author") || ctx.MatchFlag("author")) + if (author) { var user = await _rest.GetUser(message.Message.Sender); var eb = new EmbedBuilder() diff --git a/crates/command_definitions/src/checks.rs b/crates/command_definitions/src/checks.rs deleted file mode 100644 index 8b137891..00000000 --- a/crates/command_definitions/src/checks.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/crates/command_definitions/src/debug.rs b/crates/command_definitions/src/debug.rs index 8b137891..36f0e224 100644 --- a/crates/command_definitions/src/debug.rs +++ b/crates/command_definitions/src/debug.rs @@ -1 +1,16 @@ +use super::*; +pub fn debug() -> (&'static str, [&'static str; 1]) { + ("debug", ["dbg"]) +} + +pub fn cmds() -> impl Iterator { + let debug = debug(); + let perms = ("permissions", ["perms", "permcheck"]); + [ + command!(debug, perms, ("channel", ["ch"]), ChannelRef => "permcheck_channel"), + command!(debug, perms, ("guild", ["g"]), GuildRef => "permcheck_guild"), + command!(debug, ("proxy", ["proxying", "proxycheck"]), MessageRef => "message_proxy_check"), + ] + .into_iter() +} diff --git a/crates/command_definitions/src/help.rs b/crates/command_definitions/src/help.rs index dd5942cd..da83e879 100644 --- a/crates/command_definitions/src/help.rs +++ b/crates/command_definitions/src/help.rs @@ -3,6 +3,7 @@ use super::*; pub fn cmds() -> impl Iterator { let help = ("help", ["h"]); [ + command!("explain" => "explain"), command!(help => "help") .flag(("foo", OpaqueString)) // todo: just for testing .help("Shows the help command"), diff --git a/crates/command_definitions/src/lib.rs b/crates/command_definitions/src/lib.rs index e79558e4..68dace2f 100644 --- a/crates/command_definitions/src/lib.rs +++ b/crates/command_definitions/src/lib.rs @@ -1,7 +1,6 @@ pub mod admin; pub mod api; pub mod autoproxy; -pub mod checks; pub mod commands; pub mod config; pub mod dashboard; @@ -33,6 +32,8 @@ pub fn all() -> impl Iterator { .chain(random::cmds()) .chain(api::cmds()) .chain(autoproxy::cmds()) + .chain(debug::cmds()) + .chain(message::cmds()) .map(|cmd| { cmd.hidden_flag(("plaintext", ["pt"])) .hidden_flag(("raw", ["r"])) diff --git a/crates/command_definitions/src/message.rs b/crates/command_definitions/src/message.rs index 8b137891..eeb2e53e 100644 --- a/crates/command_definitions/src/message.rs +++ b/crates/command_definitions/src/message.rs @@ -1 +1,32 @@ +use super::*; +pub fn cmds() -> impl Iterator { + let message = tokens!(("message", ["msg", "messageinfo"]), MessageRef); + + let edit = tokens!(("edit", ["e"]), ("new_content", OpaqueStringRemainder)); + let apply_edit = |cmd: Command| { + cmd.flag(("append", ["a"])) + .flag(("prepend", ["p"])) + .flag(("regex", ["r"])) + .flag(("mutate-space", ["ms"])) + .flag(("clear-embeds", ["ce"])) + .flag(("clear-attachments", ["ca"])) + .help("Edits a proxied message") + }; + + [ + command!(message => "message_info") + .flag(("delete", ["d"])) + .flag(("author", ["a"])) + .help("Shows information about a proxied message"), + command!(message, ("author", ["sender"]) => "message_author") + .help("Shows the author of a proxied message"), + command!(message, ("delete", ["del"]) => "message_delete") + .help("Deletes a proxied message"), + apply_edit(command!(message, edit => "message_edit")), + apply_edit(command!(edit => "message_edit")), + command!(("reproxy", ["rp", "crimes", "crime"]), MessageRef => "message_reproxy") + .help("Reproxies a message with a different member"), + ] + .into_iter() +} diff --git a/crates/command_parser/Cargo.toml b/crates/command_parser/Cargo.toml index 169bef16..639d4a44 100644 --- a/crates/command_parser/Cargo.toml +++ b/crates/command_parser/Cargo.toml @@ -7,3 +7,4 @@ edition = "2024" lazy_static = { workspace = true } smol_str = "0.3.2" ordermap = "0.5" +regex = "1" \ No newline at end of file diff --git a/crates/command_parser/src/parameter.rs b/crates/command_parser/src/parameter.rs index 9236ed86..de187aef 100644 --- a/crates/command_parser/src/parameter.rs +++ b/crates/command_parser/src/parameter.rs @@ -15,7 +15,9 @@ pub enum ParameterValue { GroupRef(String), GroupRefs(Vec), SystemRef(String), - GuildRef(String), + MessageRef(Option, Option, u64), + ChannelRef(u64), + GuildRef(u64), MemberPrivacyTarget(String), GroupPrivacyTarget(String), SystemPrivacyTarget(String), @@ -54,6 +56,8 @@ impl Display for Parameter { ParameterKind::GroupRef => write!(f, ""), ParameterKind::GroupRefs => write!(f, " ..."), ParameterKind::SystemRef => write!(f, ""), + ParameterKind::MessageRef => write!(f, ""), + ParameterKind::ChannelRef => write!(f, ""), ParameterKind::GuildRef => write!(f, ""), ParameterKind::MemberPrivacyTarget => write!(f, ""), ParameterKind::GroupPrivacyTarget => write!(f, ""), @@ -92,6 +96,8 @@ pub enum ParameterKind { GroupRef, GroupRefs, SystemRef, + MessageRef, + ChannelRef, GuildRef, MemberPrivacyTarget, GroupPrivacyTarget, @@ -111,6 +117,8 @@ impl ParameterKind { ParameterKind::GroupRef => "target", ParameterKind::GroupRefs => "targets", ParameterKind::SystemRef => "target", + ParameterKind::MessageRef => "target", + ParameterKind::ChannelRef => "target", ParameterKind::GuildRef => "target", ParameterKind::MemberPrivacyTarget => "member_privacy_target", ParameterKind::GroupPrivacyTarget => "group_privacy_target", @@ -157,7 +165,56 @@ impl ParameterKind { Toggle::from_str(input).map(|t| ParameterValue::Toggle(t.into())) } ParameterKind::Avatar => Ok(ParameterValue::Avatar(input.into())), - ParameterKind::GuildRef => Ok(ParameterValue::GuildRef(input.into())), + ParameterKind::MessageRef => { + if let Ok(message_id) = input.parse::() { + return Ok(ParameterValue::MessageRef(None, None, message_id)); + } + + static RE: std::sync::LazyLock = std::sync::LazyLock::new(|| { + regex::Regex::new( + r"https://(?:\w+\.)?discord(?:app)?\.com/channels/(\d+)/(\d+)/(\d+)", + ) + .unwrap() + }); + + if let Some(captures) = RE.captures(input) { + let guild_id = captures + .get(1) + .and_then(|m| m.as_str().parse::().ok()) + .ok_or_else(|| SmolStr::new("invalid guild ID in message link"))?; + let channel_id = captures + .get(2) + .and_then(|m| m.as_str().parse::().ok()) + .ok_or_else(|| SmolStr::new("invalid channel ID in message link"))?; + let message_id = captures + .get(3) + .and_then(|m| m.as_str().parse::().ok()) + .ok_or_else(|| SmolStr::new("invalid message ID in message link"))?; + + Ok(ParameterValue::MessageRef( + Some(guild_id), + Some(channel_id), + message_id, + )) + } else { + Err(SmolStr::new("invalid message reference")) + } + } + ParameterKind::ChannelRef => { + let mut text = input; + + if text.len() > 3 && text.starts_with("<#") && text.ends_with('>') { + text = &text[2..text.len() - 1]; + } + + text.parse::() + .map(ParameterValue::ChannelRef) + .map_err(|_| SmolStr::new("invalid channel ID")) + } + ParameterKind::GuildRef => input + .parse::() + .map(ParameterValue::GuildRef) + .map_err(|_| SmolStr::new("invalid guild ID")), } } diff --git a/crates/commands/src/bin/write_cs_glue.rs b/crates/commands/src/bin/write_cs_glue.rs index 8108772b..d2aeb5ef 100644 --- a/crates/commands/src/bin/write_cs_glue.rs +++ b/crates/commands/src/bin/write_cs_glue.rs @@ -266,6 +266,8 @@ fn get_param_ty(kind: ParameterKind) -> &'static str { ParameterKind::PrivacyLevel => "PrivacyLevel", ParameterKind::Toggle => "bool", ParameterKind::Avatar => "ParsedImage", + ParameterKind::MessageRef => "Message.Reference", + ParameterKind::ChannelRef => "Channel", ParameterKind::GuildRef => "Guild", } } @@ -284,6 +286,8 @@ fn get_param_param_ty(kind: ParameterKind) -> &'static str { ParameterKind::PrivacyLevel => "PrivacyLevel", ParameterKind::Toggle => "Toggle", ParameterKind::Avatar => "Avatar", + ParameterKind::MessageRef => "Message", + ParameterKind::ChannelRef => "Channel", ParameterKind::GuildRef => "Guild", } } diff --git a/crates/commands/src/commands.udl b/crates/commands/src/commands.udl index 7011c463..899e1cc4 100644 --- a/crates/commands/src/commands.udl +++ b/crates/commands/src/commands.udl @@ -13,7 +13,9 @@ interface Parameter { GroupRef(string group); GroupRefs(sequence groups); SystemRef(string system); - GuildRef(string guild); + MessageRef(u64? guild_id, u64? channel_id, u64 message_id); + ChannelRef(u64 channel_id); + GuildRef(u64 guild_id); MemberPrivacyTarget(string target); GroupPrivacyTarget(string target); SystemPrivacyTarget(string target); diff --git a/crates/commands/src/lib.rs b/crates/commands/src/lib.rs index 0e363781..dd4f6025 100644 --- a/crates/commands/src/lib.rs +++ b/crates/commands/src/lib.rs @@ -22,19 +22,53 @@ pub enum CommandResult { #[derive(Debug, Clone)] pub enum Parameter { - MemberRef { member: String }, - MemberRefs { members: Vec }, - GroupRef { group: String }, - GroupRefs { groups: Vec }, - SystemRef { system: String }, - GuildRef { guild: String }, - MemberPrivacyTarget { target: String }, - GroupPrivacyTarget { target: String }, - SystemPrivacyTarget { target: String }, - PrivacyLevel { level: String }, - OpaqueString { raw: String }, - Toggle { toggle: bool }, - Avatar { avatar: String }, + MemberRef { + member: String, + }, + MemberRefs { + members: Vec, + }, + GroupRef { + group: String, + }, + GroupRefs { + groups: Vec, + }, + SystemRef { + system: String, + }, + MessageRef { + guild_id: Option, + channel_id: Option, + message_id: u64, + }, + ChannelRef { + channel_id: u64, + }, + GuildRef { + guild_id: u64, + }, + MemberPrivacyTarget { + target: String, + }, + GroupPrivacyTarget { + target: String, + }, + SystemPrivacyTarget { + target: String, + }, + PrivacyLevel { + level: String, + }, + OpaqueString { + raw: String, + }, + Toggle { + toggle: bool, + }, + Avatar { + avatar: String, + }, } impl From for Parameter { @@ -52,7 +86,13 @@ impl From for Parameter { ParameterValue::OpaqueString(raw) => Self::OpaqueString { raw }, ParameterValue::Toggle(toggle) => Self::Toggle { toggle }, ParameterValue::Avatar(avatar) => Self::Avatar { avatar }, - ParameterValue::GuildRef(guild) => Self::GuildRef { guild }, + ParameterValue::MessageRef(guild_id, channel_id, message_id) => Self::MessageRef { + guild_id, + channel_id, + message_id, + }, + ParameterValue::ChannelRef(channel_id) => Self::ChannelRef { channel_id }, + ParameterValue::GuildRef(guild_id) => Self::GuildRef { guild_id }, } } } From 5198f7d83bcb6473ffb1ce147b125806d0b46fe2 Mon Sep 17 00:00:00 2001 From: dusk Date: Fri, 3 Oct 2025 15:50:54 +0000 Subject: [PATCH 107/179] better parameters handling, implement import export --- PluralKit.Bot/CommandMeta/CommandTree.cs | 6 +- .../Context/ContextParametersExt.cs | 4 +- PluralKit.Bot/CommandSystem/Parameters.cs | 2 + PluralKit.Bot/Commands/ImportExport.cs | 4 +- PluralKit.Bot/Commands/Switch.cs | 11 +- crates/command_definitions/src/group.rs | 4 +- crates/command_definitions/src/help.rs | 1 + .../command_definitions/src/import_export.rs | 8 + crates/command_definitions/src/lib.rs | 8 +- crates/command_definitions/src/member.rs | 8 +- crates/command_definitions/src/switch.rs | 21 +- crates/command_definitions/src/system.rs | 2 +- crates/command_parser/src/flag.rs | 12 +- crates/command_parser/src/lib.rs | 2 +- crates/command_parser/src/parameter.rs | 264 +++++++++++------- crates/command_parser/src/token.rs | 47 ++-- crates/commands/src/bin/write_cs_glue.rs | 17 +- crates/commands/src/commands.udl | 1 + crates/commands/src/lib.rs | 2 + 19 files changed, 250 insertions(+), 174 deletions(-) diff --git a/PluralKit.Bot/CommandMeta/CommandTree.cs b/PluralKit.Bot/CommandMeta/CommandTree.cs index 41b6af9c..bdb9f831 100644 --- a/PluralKit.Bot/CommandMeta/CommandTree.cs +++ b/PluralKit.Bot/CommandMeta/CommandTree.cs @@ -245,6 +245,8 @@ public partial class CommandTree Commands.MessageDelete(var param, var flags) => ctx.Execute(Message, m => m.GetMessage(ctx, param.target.MessageId, flags.GetReplyFormat(), true, false)), Commands.MessageEdit(var param, var flags) => ctx.Execute(MessageEdit, m => m.EditMessage(ctx, param.target.MessageId, param.new_content, flags.regex, flags.mutate_space, flags.append, flags.prepend, flags.clear_embeds, flags.clear_attachments)), Commands.MessageReproxy(var param, _) => ctx.Execute(MessageReproxy, m => m.ReproxyMessage(ctx, param.target.MessageId)), + Commands.Import(var param, _) => ctx.Execute(Import, m => m.Import(ctx, param.url)), + Commands.Export(_, _) => ctx.Execute(Export, m => m.Export(ctx)), _ => // this should only ever occur when deving if commands are not implemented... ctx.Reply( @@ -256,10 +258,6 @@ public partial class CommandTree return HandleConfigCommand(ctx); if (ctx.Match("serverconfig", "guildconfig", "scfg")) return HandleServerConfigCommand(ctx); - if (ctx.Match("import")) - return ctx.Execute(Import, m => m.Import(ctx)); - if (ctx.Match("export")) - return ctx.Execute(Export, m => m.Export(ctx)); if (ctx.Match("log")) if (ctx.Match("channel")) return ctx.Execute(LogChannel, m => m.SetLogChannel(ctx), true); diff --git a/PluralKit.Bot/CommandSystem/Context/ContextParametersExt.cs b/PluralKit.Bot/CommandSystem/Context/ContextParametersExt.cs index fe60eb5f..cc101e46 100644 --- a/PluralKit.Bot/CommandSystem/Context/ContextParametersExt.cs +++ b/PluralKit.Bot/CommandSystem/Context/ContextParametersExt.cs @@ -20,7 +20,7 @@ public static class ContextParametersExt ); } - public static async Task> ParamResolveMembers(this Context ctx, string param_name) + public static async Task?> ParamResolveMembers(this Context ctx, string param_name) { return await ctx.Parameters.ResolveParameter( ctx, param_name, @@ -36,7 +36,7 @@ public static class ContextParametersExt ); } - public static async Task> ParamResolveGroups(this Context ctx, string param_name) + public static async Task?> ParamResolveGroups(this Context ctx, string param_name) { return await ctx.Parameters.ResolveParameter( ctx, param_name, diff --git a/PluralKit.Bot/CommandSystem/Parameters.cs b/PluralKit.Bot/CommandSystem/Parameters.cs index 3d63efdd..22bbebf4 100644 --- a/PluralKit.Bot/CommandSystem/Parameters.cs +++ b/PluralKit.Bot/CommandSystem/Parameters.cs @@ -126,6 +126,8 @@ public class Parameters return new Parameter.ChannelRef(await ctx.Rest.GetChannelOrNull(channelId) ?? throw new PKError($"Channel {channelId} not found")); case uniffi.commands.Parameter.GuildRef(var guildId): return new Parameter.GuildRef(await ctx.Rest.GetGuildOrNull(guildId) ?? throw new PKError($"Guild {guildId} not found")); + case uniffi.commands.Parameter.Null: + return null; } return null; } diff --git a/PluralKit.Bot/Commands/ImportExport.cs b/PluralKit.Bot/Commands/ImportExport.cs index d626c70d..dbecc3f7 100644 --- a/PluralKit.Bot/Commands/ImportExport.cs +++ b/PluralKit.Bot/Commands/ImportExport.cs @@ -31,9 +31,9 @@ public class ImportExport _dmCache = dmCache; } - public async Task Import(Context ctx) + public async Task Import(Context ctx, string? inputUrl) { - var inputUrl = ctx.RemainderOrNull() ?? ctx.Message.Attachments.FirstOrDefault()?.Url; + inputUrl = inputUrl ?? ctx.Message.Attachments.FirstOrDefault()?.Url; if (inputUrl == null) throw Errors.NoImportFilePassed; if (!Core.MiscUtils.TryMatchUri(inputUrl, out var url)) diff --git a/PluralKit.Bot/Commands/Switch.cs b/PluralKit.Bot/Commands/Switch.cs index 624774da..82c63e14 100644 --- a/PluralKit.Bot/Commands/Switch.cs +++ b/PluralKit.Bot/Commands/Switch.cs @@ -23,8 +23,9 @@ public class Switch await DoSwitchCommand(ctx, []); } - private async Task DoSwitchCommand(Context ctx, ICollection members) + private async Task DoSwitchCommand(Context ctx, ICollection? members) { + if (members == null) members = new List(); // Make sure there are no dupes in the list // We do this by checking if removing duplicate member IDs results in a list of different length if (members.Select(m => m.Id).Distinct().Count() != members.Count) throw Errors.DuplicateSwitchMembers; @@ -101,10 +102,12 @@ public class Switch await ctx.Reply($"{Emojis.Success} Switch moved to ({newSwitchDeltaStr} ago)."); } - public async Task SwitchEdit(Context ctx, List newMembers, bool newSwitch = false, bool first = false, bool remove = false, bool append = false, bool prepend = false) + public async Task SwitchEdit(Context ctx, List? newMembers, bool newSwitch = false, bool first = false, bool remove = false, bool append = false, bool prepend = false) { ctx.CheckSystem(); + if (newMembers == null) newMembers = new List(); + await using var conn = await ctx.Database.Obtain(); var currentSwitch = await ctx.Repository.GetLatestSwitch(ctx.System.Id); if (currentSwitch == null) @@ -170,8 +173,10 @@ public class Switch await DoEditCommand(ctx, []); } - public async Task DoEditCommand(Context ctx, ICollection members) + public async Task DoEditCommand(Context ctx, ICollection? members) { + if (members == null) members = new List(); + // Make sure there are no dupes in the list // We do this by checking if removing duplicate member IDs results in a list of different length if (members.Select(m => m.Id).Distinct().Count() != members.Count) throw Errors.DuplicateSwitchMembers; diff --git a/crates/command_definitions/src/group.rs b/crates/command_definitions/src/group.rs index 99fe4047..fec62ccf 100644 --- a/crates/command_definitions/src/group.rs +++ b/crates/command_definitions/src/group.rs @@ -158,9 +158,9 @@ pub fn cmds() -> impl Iterator { .map(apply_list_opts); let group_modify_members_cmd = [ - command!(group_target, "add", MemberRefs => "group_add_member") + command!(group_target, "add", Optional(MemberRefs) => "group_add_member") .flag(("all", ["a"])), - command!(group_target, ("remove", ["delete", "del", "rem"]), MemberRefs => "group_remove_member") + command!(group_target, ("remove", ["delete", "del", "rem"]), Optional(MemberRefs) => "group_remove_member") .flag(("all", ["a"])), ] .into_iter(); diff --git a/crates/command_definitions/src/help.rs b/crates/command_definitions/src/help.rs index da83e879..9a94dea8 100644 --- a/crates/command_definitions/src/help.rs +++ b/crates/command_definitions/src/help.rs @@ -3,6 +3,7 @@ use super::*; pub fn cmds() -> impl Iterator { let help = ("help", ["h"]); [ + command!(("dashboard", ["dash"]) => "dashboard"), command!("explain" => "explain"), command!(help => "help") .flag(("foo", OpaqueString)) // todo: just for testing diff --git a/crates/command_definitions/src/import_export.rs b/crates/command_definitions/src/import_export.rs index 8b137891..49b66f11 100644 --- a/crates/command_definitions/src/import_export.rs +++ b/crates/command_definitions/src/import_export.rs @@ -1 +1,9 @@ +use super::*; +pub fn cmds() -> impl Iterator { + [ + command!("import", Optional(("url", OpaqueStringRemainder)) => "import"), + command!("export" => "export"), + ] + .into_iter() +} diff --git a/crates/command_definitions/src/lib.rs b/crates/command_definitions/src/lib.rs index 68dace2f..4204dad2 100644 --- a/crates/command_definitions/src/lib.rs +++ b/crates/command_definitions/src/lib.rs @@ -19,7 +19,12 @@ pub mod system; pub mod utils; -use command_parser::{command, command::Command, parameter::ParameterKind::*, tokens}; +use command_parser::{ + command, + command::Command, + parameter::{Optional, Parameter, ParameterKind::*, Remainder, Skip}, + tokens, +}; pub fn all() -> impl Iterator { (help::cmds()) @@ -34,6 +39,7 @@ pub fn all() -> impl Iterator { .chain(autoproxy::cmds()) .chain(debug::cmds()) .chain(message::cmds()) + .chain(import_export::cmds()) .map(|cmd| { cmd.hidden_flag(("plaintext", ["pt"])) .hidden_flag(("raw", ["r"])) diff --git a/crates/command_definitions/src/member.rs b/crates/command_definitions/src/member.rs index e855d4f0..f6acb28e 100644 --- a/crates/command_definitions/src/member.rs +++ b/crates/command_definitions/src/member.rs @@ -182,11 +182,11 @@ pub fn cmds() -> impl Iterator { [ command!(member_keep_proxy => "member_keepproxy_show") .help("Shows a member's keep-proxy setting"), - command!(member_keep_proxy, ("value", Toggle) => "member_keepproxy_update") + command!(member_keep_proxy, Skip(("value", Toggle)) => "member_keepproxy_update") .help("Changes a member's keep-proxy setting"), command!(member_server_keep_proxy => "member_server_keepproxy_show") .help("Shows a member's server-specific keep-proxy setting"), - command!(member_server_keep_proxy, ("value", Toggle) => "member_server_keepproxy_update") + command!(member_server_keep_proxy, Skip(("value", Toggle)) => "member_server_keepproxy_update") .help("Changes a member's server-specific keep-proxy setting"), command!(member_server_keep_proxy, ("clear", ["c"]) => "member_server_keepproxy_clear") .flag(("yes", ["y"])) @@ -303,9 +303,9 @@ pub fn cmds() -> impl Iterator { .map(|cmd| cmd.flags(get_list_flags())); let member_add_remove_group_cmds = [ - command!(member_group, "add", ("groups", GroupRefs) => "member_group_add") + command!(member_group, "add", Optional(("groups", GroupRefs)) => "member_group_add") .help("Adds a member to one or more groups"), - command!(member_group, ("remove", ["rem"]), ("groups", GroupRefs) => "member_group_remove") + command!(member_group, ("remove", ["rem"]), Optional(("groups", GroupRefs)) => "member_group_remove") .help("Removes a member from one or more groups"), ] .into_iter(); diff --git a/crates/command_definitions/src/switch.rs b/crates/command_definitions/src/switch.rs index fce7f760..14ef6a0d 100644 --- a/crates/command_definitions/src/switch.rs +++ b/crates/command_definitions/src/switch.rs @@ -9,23 +9,22 @@ pub fn cmds() -> impl Iterator { let copy = ("copy", ["add", "duplicate", "dupe"]); let out = "out"; + let edit_flags = [ + ("first", ["f"]), + ("remove", ["r"]), + ("append", ["a"]), + ("prepend", ["p"]), + ]; + [ command!(switch, out => "switch_out"), command!(switch, r#move, OpaqueString => "switch_move"), // TODO: datetime parsing command!(switch, delete => "switch_delete").flag(("all", ["clear", "c"])), command!(switch, edit, out => "switch_edit_out"), - command!(switch, edit, MemberRefs => "switch_edit") - .flag(("first", ["f"])) - .flag(("remove", ["r"])) - .flag(("append", ["a"])) - .flag(("prepend", ["p"])), - command!(switch, copy, MemberRefs => "switch_copy") - .flag(("first", ["f"])) - .flag(("remove", ["r"])) - .flag(("append", ["a"])) - .flag(("prepend", ["p"])), + command!(switch, edit, Optional(MemberRefs) => "switch_edit").flags(edit_flags), + command!(switch, copy, Optional(MemberRefs) => "switch_copy").flags(edit_flags), command!(switch, ("commands", ["help"]) => "switch_commands"), - command!(switch, MemberRefs => "switch_do"), + command!(switch, Optional(MemberRefs) => "switch_do"), ] .into_iter() } diff --git a/crates/command_definitions/src/system.rs b/crates/command_definitions/src/system.rs index 1d90398c..88dbb39b 100644 --- a/crates/command_definitions/src/system.rs +++ b/crates/command_definitions/src/system.rs @@ -220,7 +220,7 @@ pub fn edit() -> impl Iterator { let system_proxy_cmd = [ command!(system_proxy => "system_show_proxy_current") .help("Shows your system's proxy setting for the guild you are in"), - command!(system_proxy, Toggle => "system_toggle_proxy_current") + command!(system_proxy, Skip(Toggle) => "system_toggle_proxy_current") .help("Toggle your system's proxy for the guild you are in"), command!(system_proxy, GuildRef => "system_show_proxy") .help("Shows your system's proxy setting for a guild"), diff --git a/crates/command_parser/src/flag.rs b/crates/command_parser/src/flag.rs index 9fa8ad38..2df4221a 100644 --- a/crates/command_parser/src/flag.rs +++ b/crates/command_parser/src/flag.rs @@ -14,7 +14,7 @@ pub enum FlagValueMatchError { pub struct Flag { name: SmolStr, aliases: Vec, - value: Option, + value: Option, } impl Display for Flag { @@ -22,7 +22,7 @@ impl Display for Flag { write!(f, "-{}", self.name)?; if let Some(value) = self.value.as_ref() { write!(f, "=")?; - Parameter::from(*value).fmt(f)?; + value.fmt(f)?; } Ok(()) } @@ -58,8 +58,8 @@ impl Flag { } } - pub fn value(mut self, param: ParameterKind) -> Self { - self.value = Some(param); + pub fn value(mut self, param: impl Into) -> Self { + self.value = Some(param.into()); self } @@ -72,8 +72,8 @@ impl Flag { &self.name } - pub fn get_value(&self) -> Option { - self.value + pub fn get_value(&self) -> Option<&Parameter> { + self.value.as_ref() } pub fn get_aliases(&self) -> impl Iterator { diff --git a/crates/command_parser/src/lib.rs b/crates/command_parser/src/lib.rs index 5c170f02..ee3a8ee3 100644 --- a/crates/command_parser/src/lib.rs +++ b/crates/command_parser/src/lib.rs @@ -242,7 +242,7 @@ fn next_token<'a>( // iterate over tokens and run try_match for token in possible_tokens { let is_match_remaining_token = - |token: &Token| matches!(token, Token::Parameter(param) if param.kind().remainder()); + |token: &Token| matches!(token, Token::Parameter(param) if param.is_remainder()); // check if this is a token that matches the rest of the input let match_remaining = is_match_remaining_token(token); // either use matched param or rest of the input if matching remaining diff --git a/crates/command_parser/src/parameter.rs b/crates/command_parser/src/parameter.rs index de187aef..28d7a693 100644 --- a/crates/command_parser/src/parameter.rs +++ b/crates/command_parser/src/parameter.rs @@ -24,12 +24,23 @@ pub enum ParameterValue { PrivacyLevel(String), Toggle(bool), Avatar(String), + Null, +} + +fn is_remainder(kind: ParameterKind) -> bool { + matches!( + kind, + ParameterKind::OpaqueStringRemainder | ParameterKind::MemberRefs | ParameterKind::GroupRefs + ) } #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct Parameter { name: SmolStr, kind: ParameterKind, + remainder: bool, + optional: bool, + skip: bool, } impl Parameter { @@ -40,106 +51,36 @@ impl Parameter { pub fn kind(&self) -> ParameterKind { self.kind } -} -impl Display for Parameter { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + pub fn remainder(mut self) -> Self { + self.remainder = true; + self + } + + pub fn optional(mut self) -> Self { + self.optional = true; + self + } + + pub fn skip(mut self) -> Self { + self.skip = true; + self + } + + pub fn is_remainder(&self) -> bool { + self.remainder + } + + pub fn is_optional(&self) -> bool { + self.optional + } + + pub fn is_skip(&self) -> bool { + self.skip + } + + pub fn match_value(&self, input: &str) -> Result { match self.kind { - ParameterKind::OpaqueString => { - write!(f, "[{}]", self.name) - } - ParameterKind::OpaqueStringRemainder => { - write!(f, "[{}]...", self.name) - } - ParameterKind::MemberRef => write!(f, ""), - ParameterKind::MemberRefs => write!(f, " ..."), - ParameterKind::GroupRef => write!(f, ""), - ParameterKind::GroupRefs => write!(f, " ..."), - ParameterKind::SystemRef => write!(f, ""), - ParameterKind::MessageRef => write!(f, ""), - ParameterKind::ChannelRef => write!(f, ""), - ParameterKind::GuildRef => write!(f, ""), - ParameterKind::MemberPrivacyTarget => write!(f, ""), - ParameterKind::GroupPrivacyTarget => write!(f, ""), - ParameterKind::SystemPrivacyTarget => write!(f, ""), - ParameterKind::PrivacyLevel => write!(f, "[privacy level]"), - ParameterKind::Toggle => write!(f, "on/off"), - ParameterKind::Avatar => write!(f, ""), - } - } -} - -impl From for Parameter { - fn from(value: ParameterKind) -> Self { - Parameter { - name: value.default_name().into(), - kind: value, - } - } -} - -impl From<(&str, ParameterKind)> for Parameter { - fn from((name, kind): (&str, ParameterKind)) -> Self { - Parameter { - name: name.into(), - kind, - } - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum ParameterKind { - OpaqueString, - OpaqueStringRemainder, - MemberRef, - MemberRefs, - GroupRef, - GroupRefs, - SystemRef, - MessageRef, - ChannelRef, - GuildRef, - MemberPrivacyTarget, - GroupPrivacyTarget, - SystemPrivacyTarget, - PrivacyLevel, - Toggle, - Avatar, -} - -impl ParameterKind { - pub(crate) fn default_name(&self) -> &str { - match self { - ParameterKind::OpaqueString => "string", - ParameterKind::OpaqueStringRemainder => "string", - ParameterKind::MemberRef => "target", - ParameterKind::MemberRefs => "targets", - ParameterKind::GroupRef => "target", - ParameterKind::GroupRefs => "targets", - ParameterKind::SystemRef => "target", - ParameterKind::MessageRef => "target", - ParameterKind::ChannelRef => "target", - ParameterKind::GuildRef => "target", - ParameterKind::MemberPrivacyTarget => "member_privacy_target", - ParameterKind::GroupPrivacyTarget => "group_privacy_target", - ParameterKind::SystemPrivacyTarget => "system_privacy_target", - ParameterKind::PrivacyLevel => "privacy_level", - ParameterKind::Toggle => "toggle", - ParameterKind::Avatar => "avatar", - } - } - - pub(crate) fn remainder(&self) -> bool { - matches!( - self, - ParameterKind::OpaqueStringRemainder - | ParameterKind::MemberRefs - | ParameterKind::GroupRefs - ) - } - - pub(crate) fn match_value(&self, input: &str) -> Result { - match self { // TODO: actually parse image url ParameterKind::OpaqueString | ParameterKind::OpaqueStringRemainder => { Ok(ParameterValue::OpaqueString(input.into())) @@ -217,13 +158,130 @@ impl ParameterKind { .map_err(|_| SmolStr::new("invalid guild ID")), } } +} - pub(crate) fn skip_if_cant_match(&self) -> Option> { +impl Display for Parameter { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self.kind { + ParameterKind::OpaqueString => { + write!(f, "[{}]", self.name) + } + ParameterKind::OpaqueStringRemainder => { + write!(f, "[{}]...", self.name) + } + ParameterKind::MemberRef => write!(f, ""), + ParameterKind::MemberRefs => write!(f, " ..."), + ParameterKind::GroupRef => write!(f, ""), + ParameterKind::GroupRefs => write!(f, " ..."), + ParameterKind::SystemRef => write!(f, ""), + ParameterKind::MessageRef => write!(f, ""), + ParameterKind::ChannelRef => write!(f, ""), + ParameterKind::GuildRef => write!(f, ""), + ParameterKind::MemberPrivacyTarget => write!(f, ""), + ParameterKind::GroupPrivacyTarget => write!(f, ""), + ParameterKind::SystemPrivacyTarget => write!(f, ""), + ParameterKind::PrivacyLevel => write!(f, "[privacy level]"), + ParameterKind::Toggle => write!(f, "on/off"), + ParameterKind::Avatar => write!(f, ""), + } + } +} + +impl From for Parameter { + fn from(value: ParameterKind) -> Self { + Parameter { + name: value.default_name().into(), + kind: value, + remainder: is_remainder(value), + optional: false, + skip: false, + } + } +} + +impl From<(&str, ParameterKind)> for Parameter { + fn from((name, kind): (&str, ParameterKind)) -> Self { + Parameter { + name: name.into(), + kind, + remainder: is_remainder(kind), + optional: false, + skip: false, + } + } +} + +#[derive(Clone)] +pub struct Optional>(pub P); + +impl> From> for Parameter { + fn from(value: Optional

) -> Self { + let p = value.0.into(); + p.optional() + } +} + +#[derive(Clone)] +pub struct Remainder>(pub P); + +impl> From> for Parameter { + fn from(value: Remainder

) -> Self { + let p = value.0.into(); + p.remainder() + } +} + +// todo(dusk): this is kind of annoying to use, should probably introduce +// a way to match multiple parameters in a single parameter +#[derive(Clone)] +pub struct Skip>(pub P); + +impl> From> for Parameter { + fn from(value: Skip

) -> Self { + let p = value.0.into(); + p.skip() + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum ParameterKind { + OpaqueString, + OpaqueStringRemainder, + MemberRef, + MemberRefs, + GroupRef, + GroupRefs, + SystemRef, + MessageRef, + ChannelRef, + GuildRef, + MemberPrivacyTarget, + GroupPrivacyTarget, + SystemPrivacyTarget, + PrivacyLevel, + Toggle, + Avatar, +} + +impl ParameterKind { + pub(crate) fn default_name(&self) -> &str { match self { - ParameterKind::Toggle => Some(None), - ParameterKind::MemberRefs => Some(Some(ParameterValue::MemberRefs(Vec::new()))), - ParameterKind::GroupRefs => Some(Some(ParameterValue::GroupRefs(Vec::new()))), - _ => None, + ParameterKind::OpaqueString => "string", + ParameterKind::OpaqueStringRemainder => "string", + ParameterKind::MemberRef => "target", + ParameterKind::MemberRefs => "targets", + ParameterKind::GroupRef => "target", + ParameterKind::GroupRefs => "targets", + ParameterKind::SystemRef => "target", + ParameterKind::MessageRef => "target", + ParameterKind::ChannelRef => "target", + ParameterKind::GuildRef => "target", + ParameterKind::MemberPrivacyTarget => "member_privacy_target", + ParameterKind::GroupPrivacyTarget => "group_privacy_target", + ParameterKind::SystemPrivacyTarget => "system_privacy_target", + ParameterKind::PrivacyLevel => "privacy_level", + ParameterKind::Toggle => "toggle", + ParameterKind::Avatar => "avatar", } } } diff --git a/crates/command_parser/src/token.rs b/crates/command_parser/src/token.rs index 93c4e210..c18ce0f3 100644 --- a/crates/command_parser/src/token.rs +++ b/crates/command_parser/src/token.rs @@ -2,7 +2,7 @@ use std::fmt::{Debug, Display}; use smol_str::SmolStr; -use crate::parameter::{Parameter, ParameterKind, ParameterValue}; +use crate::parameter::{Optional, Parameter, ParameterKind, ParameterValue}; #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum Token { @@ -46,9 +46,17 @@ impl Token { // short circuit on: return match self { // missing paramaters - Self::Parameter(param) => Some(TokenMatchResult::MissingParameter { - name: param.name().into(), - }), + Self::Parameter(param) => Some( + param + .is_optional() + .then(|| TokenMatchResult::MatchedParameter { + name: param.name().into(), + value: ParameterValue::Null, + }) + .unwrap_or_else(|| TokenMatchResult::MissingParameter { + name: param.name().into(), + }), + ), // everything else doesnt match if no input anyway Self::Value { .. } => None, // don't add a _ match here! @@ -62,20 +70,14 @@ impl Token { Self::Value { name, aliases } => (aliases.iter().chain(std::iter::once(name))) .any(|v| v.eq(input)) .then(|| TokenMatchResult::MatchedValue), - Self::Parameter(param) => Some(match param.kind().match_value(input) { + Self::Parameter(param) => Some(match param.match_value(input) { Ok(matched) => TokenMatchResult::MatchedParameter { name: param.name().into(), value: matched, }, Err(err) => { - if let Some(maybe_empty) = param.kind().skip_if_cant_match() { - match maybe_empty { - Some(matched) => TokenMatchResult::MatchedParameter { - name: param.name().into(), - value: matched, - }, - None => return None, - } + if param.is_skip() { + return None; } else { TokenMatchResult::ParameterMatchError { input: input.into(), @@ -115,21 +117,10 @@ impl From<&str> for Token { } } -impl From for Token { - fn from(value: Parameter) -> Self { - Self::Parameter(value) - } -} - -impl From for Token { - fn from(value: ParameterKind) -> Self { - Self::from(Parameter::from(value)) - } -} - -impl From<(&str, ParameterKind)> for Token { - fn from(value: (&str, ParameterKind)) -> Self { - Self::from(Parameter::from(value)) +// parameter -> Token::Parameter +impl> From

for Token { + fn from(value: P) -> Self { + Self::Parameter(value.into()) } } diff --git a/crates/commands/src/bin/write_cs_glue.rs b/crates/commands/src/bin/write_cs_glue.rs index d2aeb5ef..ff1a0886 100644 --- a/crates/commands/src/bin/write_cs_glue.rs +++ b/crates/commands/src/bin/write_cs_glue.rs @@ -45,19 +45,23 @@ fn main() -> Result<(), Box> { for param in &command_params { writeln!( &mut command_params_init, - r#"@{name} = await ctx.ParamResolve{extract_fn_name}("{name}") ?? throw new PKError("this is a bug"),"#, + r#"@{name} = await ctx.ParamResolve{extract_fn_name}("{name}"){throw_null},"#, name = param.name().replace("-", "_"), extract_fn_name = get_param_param_ty(param.kind()), + throw_null = param + .is_optional() + .then_some("") + .unwrap_or(" ?? throw new PKError(\"this is a bug\")"), )?; } let mut command_flags_init = String::new(); for flag in &command.flags { - if let Some(kind) = flag.get_value() { + if let Some(param) = flag.get_value() { writeln!( &mut command_flags_init, r#"@{name} = await ctx.FlagResolve{extract_fn_name}("{name}"),"#, name = flag.get_name().replace("-", "_"), - extract_fn_name = get_param_param_ty(kind), + extract_fn_name = get_param_param_ty(param.kind()), )?; } else { writeln!( @@ -109,19 +113,20 @@ fn main() -> Result<(), Box> { for param in &command_params { writeln!( &mut command_params_fields, - r#"public required {ty} @{name};"#, + r#"public required {ty}{nullable} @{name};"#, name = param.name().replace("-", "_"), ty = get_param_ty(param.kind()), + nullable = param.is_optional().then_some("?").unwrap_or(""), )?; } let mut command_flags_fields = String::new(); for flag in &command.flags { - if let Some(kind) = flag.get_value() { + if let Some(param) = flag.get_value() { writeln!( &mut command_flags_fields, r#"public {ty}? @{name};"#, name = flag.get_name().replace("-", "_"), - ty = get_param_ty(kind), + ty = get_param_ty(param.kind()), )?; } else { writeln!( diff --git a/crates/commands/src/commands.udl b/crates/commands/src/commands.udl index 899e1cc4..757e6615 100644 --- a/crates/commands/src/commands.udl +++ b/crates/commands/src/commands.udl @@ -23,6 +23,7 @@ interface Parameter { OpaqueString(string raw); Toggle(boolean toggle); Avatar(string avatar); + Null(); }; dictionary ParsedCommand { string command_ref; diff --git a/crates/commands/src/lib.rs b/crates/commands/src/lib.rs index dd4f6025..65199a7d 100644 --- a/crates/commands/src/lib.rs +++ b/crates/commands/src/lib.rs @@ -69,6 +69,7 @@ pub enum Parameter { Avatar { avatar: String, }, + Null, } impl From for Parameter { @@ -93,6 +94,7 @@ impl From for Parameter { }, ParameterValue::ChannelRef(channel_id) => Self::ChannelRef { channel_id }, ParameterValue::GuildRef(guild_id) => Self::GuildRef { guild_id }, + ParameterValue::Null => Self::Null, } } } From a268f75d3269fe861155f1d421ee1a49e29d27c7 Mon Sep 17 00:00:00 2001 From: dusk Date: Sat, 4 Oct 2025 01:57:48 +0000 Subject: [PATCH 108/179] implement admin commands --- PluralKit.Bot/CommandMeta/CommandTree.cs | 103 +++------ .../Context/ContextEntityArgumentsExt.cs | 94 +------- .../Context/ContextParametersExt.cs | 17 ++ PluralKit.Bot/CommandSystem/Parameters.cs | 10 + PluralKit.Bot/Commands/Admin.cs | 200 ++++++++---------- PluralKit.Bot/Commands/Message.cs | 6 +- crates/command_definitions/src/admin.rs | 63 ++++++ crates/command_definitions/src/dashboard.rs | 1 - crates/command_definitions/src/help.rs | 4 +- crates/command_definitions/src/lib.rs | 3 +- crates/command_definitions/src/message.rs | 2 +- crates/command_parser/src/parameter.rs | 33 ++- crates/commands/src/bin/write_cs_glue.rs | 4 + crates/commands/src/commands.udl | 2 + crates/commands/src/lib.rs | 8 + 15 files changed, 263 insertions(+), 287 deletions(-) delete mode 100644 crates/command_definitions/src/dashboard.rs diff --git a/PluralKit.Bot/CommandMeta/CommandTree.cs b/PluralKit.Bot/CommandMeta/CommandTree.cs index bdb9f831..6559a6e9 100644 --- a/PluralKit.Bot/CommandMeta/CommandTree.cs +++ b/PluralKit.Bot/CommandMeta/CommandTree.cs @@ -8,6 +8,7 @@ public partial class CommandTree { return command switch { + Commands.Dashboard => ctx.Execute(Dashboard, m => m.Dashboard(ctx)), Commands.Explain => ctx.Execute(Explain, m => m.Explain(ctx)), Commands.Help(_, var flags) => ctx.Execute(Help, m => m.HelpRoot(ctx, flags.show_embed)), Commands.HelpCommands => ctx.Reply( @@ -244,9 +245,33 @@ public partial class CommandTree Commands.MessageAuthor(var param, var flags) => ctx.Execute(Message, m => m.GetMessage(ctx, param.target.MessageId, flags.GetReplyFormat(), false, true)), Commands.MessageDelete(var param, var flags) => ctx.Execute(Message, m => m.GetMessage(ctx, param.target.MessageId, flags.GetReplyFormat(), true, false)), Commands.MessageEdit(var param, var flags) => ctx.Execute(MessageEdit, m => m.EditMessage(ctx, param.target.MessageId, param.new_content, flags.regex, flags.mutate_space, flags.append, flags.prepend, flags.clear_embeds, flags.clear_attachments)), - Commands.MessageReproxy(var param, _) => ctx.Execute(MessageReproxy, m => m.ReproxyMessage(ctx, param.target.MessageId)), + Commands.MessageReproxy(var param, _) => ctx.Execute(MessageReproxy, m => m.ReproxyMessage(ctx, param.msg.MessageId, param.member)), Commands.Import(var param, _) => ctx.Execute(Import, m => m.Import(ctx, param.url)), Commands.Export(_, _) => ctx.Execute(Export, m => m.Export(ctx)), + Commands.AdminUpdateSystemId(var param, _) => ctx.Execute(null, m => m.UpdateSystemId(ctx, param.target, param.new_hid)), + Commands.AdminUpdateMemberId(var param, _) => ctx.Execute(null, m => m.UpdateMemberId(ctx, param.target, param.new_hid)), + Commands.AdminUpdateGroupId(var param, _) => ctx.Execute(null, m => m.UpdateGroupId(ctx, param.target, param.new_hid)), + Commands.AdminRerollSystemId(var param, _) => ctx.Execute(null, m => m.RerollSystemId(ctx, param.target)), + Commands.AdminRerollMemberId(var param, _) => ctx.Execute(null, m => m.RerollMemberId(ctx, param.target)), + Commands.AdminRerollGroupId(var param, _) => ctx.Execute(null, m => m.RerollGroupId(ctx, param.target)), + Commands.AdminSystemMemberLimit(var param, _) => ctx.Execute(null, m => m.SystemMemberLimit(ctx, param.target, param.limit)), + Commands.AdminSystemGroupLimit(var param, _) => ctx.Execute(null, m => m.SystemGroupLimit(ctx, param.target, param.limit)), + Commands.AdminSystemRecover(var param, var flags) => ctx.Execute(null, m => m.SystemRecover(ctx, param.token, param.account, flags.reroll_token)), + Commands.AdminSystemDelete(var param, _) => ctx.Execute(null, m => m.SystemDelete(ctx, param.target)), + Commands.AdminSendMessage(var param, _) => ctx.Execute(null, m => m.SendAdminMessage(ctx, param.account, param.content)), + Commands.AdminAbuselogCreate(var param, var flags) => ctx.Execute(null, m => m.AbuseLogCreate(ctx, param.account, flags.deny_boy_usage, param.description)), + Commands.AdminAbuselogShowAccount(var param, _) => ctx.Execute(null, m => m.AbuseLogShow(ctx, param.account, null)), + Commands.AdminAbuselogFlagDenyAccount(var param, _) => ctx.Execute(null, m => m.AbuseLogFlagDeny(ctx, param.account, null, param.value)), + Commands.AdminAbuselogDescriptionAccount(var param, var flags) => ctx.Execute(null, m => m.AbuseLogDescription(ctx, param.account, null, param.desc, flags.clear)), + Commands.AdminAbuselogAddUserAccount(var param, _) => ctx.Execute(null, m => m.AbuseLogAddUser(ctx, param.account, null, ctx.Author)), + Commands.AdminAbuselogRemoveUserAccount(var param, _) => ctx.Execute(null, m => m.AbuseLogRemoveUser(ctx, param.account, null, ctx.Author)), + Commands.AdminAbuselogDeleteAccount(var param, _) => ctx.Execute(null, m => m.AbuseLogDelete(ctx, param.account, null)), + Commands.AdminAbuselogShowLogId(var param, _) => ctx.Execute(null, m => m.AbuseLogShow(ctx, null, param.log_id)), + Commands.AdminAbuselogFlagDenyLogId(var param, _) => ctx.Execute(null, m => m.AbuseLogFlagDeny(ctx, null, param.log_id, param.value)), + Commands.AdminAbuselogDescriptionLogId(var param, var flags) => ctx.Execute(null, m => m.AbuseLogDescription(ctx, null, param.log_id, param.desc, flags.clear)), + Commands.AdminAbuselogAddUserLogId(var param, _) => ctx.Execute(null, m => m.AbuseLogAddUser(ctx, null, param.log_id, ctx.Author)), + Commands.AdminAbuselogRemoveUserLogId(var param, _) => ctx.Execute(null, m => m.AbuseLogRemoveUser(ctx, null, param.log_id, ctx.Author)), + Commands.AdminAbuselogDeleteLogId(var param, _) => ctx.Execute(null, m => m.AbuseLogDelete(ctx, null, param.log_id)), _ => // this should only ever occur when deving if commands are not implemented... ctx.Reply( @@ -282,82 +307,6 @@ public partial class CommandTree return ctx.Reply($"{Emojis.Warn} Blacklist commands have moved to `{ctx.DefaultPrefix}serverconfig`."); if (ctx.Match("invite")) return ctx.Execute(Invite, m => m.Invite(ctx)); if (ctx.Match("stats", "status")) return ctx.Execute(null, m => m.Stats(ctx)); - if (ctx.Match("admin")) - return HandleAdminCommand(ctx); - if (ctx.Match("dashboard", "dash")) - return ctx.Execute(Dashboard, m => m.Dashboard(ctx)); - } - - private async Task HandleAdminAbuseLogCommand(Context ctx) - { - ctx.AssertBotAdmin(); - - if (ctx.Match("n", "new", "create")) - await ctx.Execute(Admin, a => a.AbuseLogCreate(ctx)); - else - { - AbuseLog? abuseLog = null!; - var account = await ctx.MatchUser(); - if (account != null) - { - abuseLog = await ctx.Repository.GetAbuseLogByAccount(account.Id); - } - else - { - abuseLog = await ctx.Repository.GetAbuseLogByGuid(new Guid(ctx.PopArgument())); - } - - if (abuseLog == null) - { - await ctx.Reply($"{Emojis.Error} Could not find an existing abuse log entry for that query."); - return; - } - - if (!ctx.HasNext()) - await ctx.Execute(Admin, a => a.AbuseLogShow(ctx, abuseLog)); - else if (ctx.Match("au", "adduser")) - await ctx.Execute(Admin, a => a.AbuseLogAddUser(ctx, abuseLog)); - else if (ctx.Match("ru", "removeuser")) - await ctx.Execute(Admin, a => a.AbuseLogRemoveUser(ctx, abuseLog)); - else if (ctx.Match("desc", "description")) - await ctx.Execute(Admin, a => a.AbuseLogDescription(ctx, abuseLog)); - else if (ctx.Match("deny", "deny-bot-usage")) - await ctx.Execute(Admin, a => a.AbuseLogFlagDeny(ctx, abuseLog)); - else if (ctx.Match("yeet", "remove", "delete")) - await ctx.Execute(Admin, a => a.AbuseLogDelete(ctx, abuseLog)); - else - await ctx.Reply($"{Emojis.Error} Unknown subcommand {ctx.PeekArgument().AsCode()}."); - } - } - - private async Task HandleAdminCommand(Context ctx) - { - if (ctx.Match("usid", "updatesystemid")) - await ctx.Execute(Admin, a => a.UpdateSystemId(ctx)); - else if (ctx.Match("umid", "updatememberid")) - await ctx.Execute(Admin, a => a.UpdateMemberId(ctx)); - else if (ctx.Match("ugid", "updategroupid")) - await ctx.Execute(Admin, a => a.UpdateGroupId(ctx)); - else if (ctx.Match("rsid", "rerollsystemid")) - await ctx.Execute(Admin, a => a.RerollSystemId(ctx)); - else if (ctx.Match("rmid", "rerollmemberid")) - await ctx.Execute(Admin, a => a.RerollMemberId(ctx)); - else if (ctx.Match("rgid", "rerollgroupid")) - await ctx.Execute(Admin, a => a.RerollGroupId(ctx)); - else if (ctx.Match("uml", "updatememberlimit")) - await ctx.Execute(Admin, a => a.SystemMemberLimit(ctx)); - else if (ctx.Match("ugl", "updategrouplimit")) - await ctx.Execute(Admin, a => a.SystemGroupLimit(ctx)); - else if (ctx.Match("sr", "systemrecover")) - await ctx.Execute(Admin, a => a.SystemRecover(ctx)); - else if (ctx.Match("sd", "systemdelete")) - await ctx.Execute(Admin, a => a.SystemDelete(ctx)); - else if (ctx.Match("sendmsg", "sendmessage")) - await ctx.Execute(Admin, a => a.SendAdminMessage(ctx)); - else if (ctx.Match("al", "abuselog")) - await HandleAdminAbuseLogCommand(ctx); - else - await ctx.Reply($"{Emojis.Error} Unknown command."); } private async Task CommandHelpRoot(Context ctx) diff --git a/PluralKit.Bot/CommandSystem/Context/ContextEntityArgumentsExt.cs b/PluralKit.Bot/CommandSystem/Context/ContextEntityArgumentsExt.cs index 43d87375..627c1185 100644 --- a/PluralKit.Bot/CommandSystem/Context/ContextEntityArgumentsExt.cs +++ b/PluralKit.Bot/CommandSystem/Context/ContextEntityArgumentsExt.cs @@ -18,37 +18,6 @@ public static class ContextEntityArgumentsExt return null; } - public static async Task MatchUser(this Context ctx) - { - var text = ctx.PeekArgument(); - if (text.TryParseMention(out var id)) - { - var user = await ctx.Cache.GetOrFetchUser(ctx.Rest, id); - if (user != null) ctx.PopArgument(); - return user; - } - - return null; - } - - public static bool MatchUserRaw(this Context ctx, out ulong id) - { - id = 0; - - var text = ctx.PeekArgument(); - if (text.TryParseMention(out var mentionId)) - id = mentionId; - - return id != 0; - } - - public static Task PeekSystem(this Context ctx) => throw new NotImplementedException(); - - public static async Task MatchSystem(this Context ctx) - { - throw new NotImplementedException(); - } - public static async Task ParseSystem(this Context ctx, string input) { // System references can take three forms: @@ -67,7 +36,7 @@ public static class ContextEntityArgumentsExt return null; } - public static async Task ParseMember(this Context ctx, string input, bool byId, SystemId? restrictToSystem = null) + public static async Task ParseMember(this Context ctx, string input, bool byId) { // Member references can have one of three forms, depending on // whether you're in a system or not: @@ -100,53 +69,22 @@ public static class ContextEntityArgumentsExt // If we are supposed to restrict it to a system anyway we can just do that PKMember memberByHid = null; - if (restrictToSystem != null) - { - memberByHid = await ctx.Repository.GetMemberByHid(hid, restrictToSystem); - if (memberByHid != null) - return memberByHid; - } - // otherwise we try the querier's system and if that doesn't work we do global - else - { - memberByHid = await ctx.Repository.GetMemberByHid(hid, ctx.System?.Id); - if (memberByHid != null) - return memberByHid; + memberByHid = await ctx.Repository.GetMemberByHid(hid, ctx.System?.Id); + if (memberByHid != null) + return memberByHid; - // ff ctx.System was null then this would be a duplicate of above and we don't want to run it again - if (ctx.System != null) - { - memberByHid = await ctx.Repository.GetMemberByHid(hid); - if (memberByHid != null) - return memberByHid; - } + // ff ctx.System was null then this would be a duplicate of above and we don't want to run it again + if (ctx.System != null) + { + memberByHid = await ctx.Repository.GetMemberByHid(hid); + if (memberByHid != null) + return memberByHid; } // We didn't find anything, so we return null. return null; } - public static async Task PeekMember(this Context ctx, SystemId? restrictToSystem = null) - { - throw new NotImplementedException(); - } - - ///

- /// Attempts to pop a member descriptor from the stack, returning it if present. If a member could not be - /// resolved by the next word in the argument stack, does *not* touch the stack, and returns null. - /// - public static async Task MatchMember(this Context ctx, SystemId? restrictToSystem = null) - { - // First, peek a member - var member = await ctx.PeekMember(restrictToSystem); - - // If the peek was successful, we've used up the next argument, so we pop that just to get rid of it. - if (member != null) ctx.PopArgument(); - - // Finally, we return the member value. - return member; - } - public static async Task ParseGroup(this Context ctx, string input, bool byId, SystemId? restrictToSystem = null) { if (ctx.System != null && !byId) @@ -166,18 +104,6 @@ public static class ContextEntityArgumentsExt return null; } - public static async Task PeekGroup(this Context ctx, SystemId? restrictToSystem = null) - { - throw new NotImplementedException(); - } - - public static async Task MatchGroup(this Context ctx, SystemId? restrictToSystem = null) - { - var group = await ctx.PeekGroup(restrictToSystem); - if (group != null) ctx.PopArgument(); - return group; - } - public static string CreateNotFoundError(this Context ctx, string entity, string input, bool byId = false) { var isIDOnlyQuery = ctx.System == null || byId; diff --git a/PluralKit.Bot/CommandSystem/Context/ContextParametersExt.cs b/PluralKit.Bot/CommandSystem/Context/ContextParametersExt.cs index cc101e46..53e6de76 100644 --- a/PluralKit.Bot/CommandSystem/Context/ContextParametersExt.cs +++ b/PluralKit.Bot/CommandSystem/Context/ContextParametersExt.cs @@ -1,4 +1,5 @@ using PluralKit.Core; +using Myriad.Types; namespace PluralKit.Bot; @@ -12,6 +13,14 @@ public static class ContextParametersExt ); } + public static async Task ParamResolveNumber(this Context ctx, string param_name) + { + return await ctx.Parameters.ResolveParameter( + ctx, param_name, + param => (param as Parameter.Number)?.value + ); + } + public static async Task ParamResolveMember(this Context ctx, string param_name) { return await ctx.Parameters.ResolveParameter( @@ -52,6 +61,14 @@ public static class ContextParametersExt ); } + public static async Task ParamResolveUser(this Context ctx, string param_name) + { + return await ctx.Parameters.ResolveParameter( + ctx, param_name, + param => (param as Parameter.UserRef)?.user + ); + } + public static async Task ParamResolveMemberPrivacyTarget(this Context ctx, string param_name) { return await ctx.Parameters.ResolveParameter( diff --git a/PluralKit.Bot/CommandSystem/Parameters.cs b/PluralKit.Bot/CommandSystem/Parameters.cs index 22bbebf4..d6ae35e4 100644 --- a/PluralKit.Bot/CommandSystem/Parameters.cs +++ b/PluralKit.Bot/CommandSystem/Parameters.cs @@ -1,5 +1,6 @@ using Humanizer; using Myriad.Types; +using Myriad.Extensions; using PluralKit.Core; using uniffi.commands; @@ -13,6 +14,7 @@ public abstract record Parameter() public record GroupRef(PKGroup group): Parameter; public record GroupRefs(List groups): Parameter; public record SystemRef(PKSystem system): Parameter; + public record UserRef(User user): Parameter; public record MessageRef(Message.Reference message): Parameter; public record ChannelRef(Channel channel): Parameter; public record GuildRef(Guild guild): Parameter; @@ -22,6 +24,7 @@ public abstract record Parameter() public record PrivacyLevel(Core.PrivacyLevel level): Parameter; public record Toggle(bool value): Parameter; public record Opaque(string value): Parameter; + public record Number(int value): Parameter; public record Avatar(ParsedImage avatar): Parameter; } @@ -96,6 +99,11 @@ public class Parameters await ctx.ParseSystem(systemRef.system) ?? throw new PKError(ctx.CreateNotFoundError("System", systemRef.system)) ); + case uniffi.commands.Parameter.UserRef(var userId): + return new Parameter.UserRef( + await ctx.Cache.GetOrFetchUser(ctx.Rest, userId) + ?? throw new PKError(ctx.CreateNotFoundError("User", userId.ToString())) + ); // todo(dusk): ideally generate enums for these from rust code in the cs glue case uniffi.commands.Parameter.MemberPrivacyTarget memberPrivacyTarget: // this should never really fail... @@ -118,6 +126,8 @@ public class Parameters return new Parameter.Toggle(toggle.toggle); case uniffi.commands.Parameter.OpaqueString opaque: return new Parameter.Opaque(opaque.raw); + case uniffi.commands.Parameter.OpaqueInt number: + return new Parameter.Number(number.raw); case uniffi.commands.Parameter.Avatar avatar: return new Parameter.Avatar(await ctx.GetUserPfp(avatar.avatar) ?? ctx.ParseImage(avatar.avatar)); case uniffi.commands.Parameter.MessageRef(var guildId, var channelId, var messageId): diff --git a/PluralKit.Bot/Commands/Admin.cs b/PluralKit.Bot/Commands/Admin.cs index 44171345..e65e6bc1 100644 --- a/PluralKit.Bot/Commands/Admin.cs +++ b/PluralKit.Bot/Commands/Admin.cs @@ -1,5 +1,3 @@ -using System.Text.RegularExpressions; - using Humanizer; using Dapper; using SqlKata; @@ -113,18 +111,10 @@ public class Admin return eb.Build(); } - public async Task UpdateSystemId(Context ctx) + public async Task UpdateSystemId(Context ctx, PKSystem target, string newHid) { ctx.AssertBotAdmin(); - var target = await ctx.MatchSystem(); - if (target == null) - throw new PKError("Unknown system."); - - var input = ctx.PopArgument(); - if (!input.TryParseHid(out var newHid)) - throw new PKError($"Invalid new system ID `{input}`."); - var existingSystem = await ctx.Repository.GetSystemByHid(newHid); if (existingSystem != null) throw new PKError($"Another system already exists with ID `{newHid}`."); @@ -138,18 +128,10 @@ public class Admin await ctx.Reply($"{Emojis.Success} System ID updated (`{target.Hid}` -> `{newHid}`)."); } - public async Task UpdateMemberId(Context ctx) + public async Task UpdateMemberId(Context ctx, PKMember target, string newHid) { ctx.AssertBotAdmin(); - var target = await ctx.MatchMember(); - if (target == null) - throw new PKError("Unknown member."); - - var input = ctx.PopArgument(); - if (!input.TryParseHid(out var newHid)) - throw new PKError($"Invalid new member ID `{input}`."); - var existingMember = await ctx.Repository.GetMemberByHid(newHid); if (existingMember != null) throw new PKError($"Another member already exists with ID `{newHid}`."); @@ -167,18 +149,10 @@ public class Admin await ctx.Reply($"{Emojis.Success} Member ID updated (`{target.Hid}` -> `{newHid}`)."); } - public async Task UpdateGroupId(Context ctx) + public async Task UpdateGroupId(Context ctx, PKGroup target, string newHid) { ctx.AssertBotAdmin(); - var target = await ctx.MatchGroup(); - if (target == null) - throw new PKError("Unknown group."); - - var input = ctx.PopArgument(); - if (!input.TryParseHid(out var newHid)) - throw new PKError($"Invalid new group ID `{input}`."); - var existingGroup = await ctx.Repository.GetGroupByHid(newHid); if (existingGroup != null) throw new PKError($"Another group already exists with ID `{newHid}`."); @@ -195,14 +169,10 @@ public class Admin await ctx.Reply($"{Emojis.Success} Group ID updated (`{target.Hid}` -> `{newHid}`)."); } - public async Task RerollSystemId(Context ctx) + public async Task RerollSystemId(Context ctx, PKSystem target) { ctx.AssertBotAdmin(); - var target = await ctx.MatchSystem(); - if (target == null) - throw new PKError("Unknown system."); - await ctx.Reply(null, await CreateEmbed(ctx, target)); if (!await ctx.PromptYesNo($"Reroll system ID `{target.Hid}`?", "Reroll")) @@ -218,14 +188,10 @@ public class Admin await ctx.Reply($"{Emojis.Success} System ID updated (`{target.Hid}` -> `{newHid}`)."); } - public async Task RerollMemberId(Context ctx) + public async Task RerollMemberId(Context ctx, PKMember target) { ctx.AssertBotAdmin(); - var target = await ctx.MatchMember(); - if (target == null) - throw new PKError("Unknown member."); - var system = await ctx.Repository.GetSystem(target.System); await ctx.Reply(null, await CreateEmbed(ctx, system)); @@ -245,14 +211,10 @@ public class Admin await ctx.Reply($"{Emojis.Success} Member ID updated (`{target.Hid}` -> `{newHid}`)."); } - public async Task RerollGroupId(Context ctx) + public async Task RerollGroupId(Context ctx, PKGroup target) { ctx.AssertBotAdmin(); - var target = await ctx.MatchGroup(); - if (target == null) - throw new PKError("Unknown group."); - var system = await ctx.Repository.GetSystem(target.System); await ctx.Reply(null, await CreateEmbed(ctx, system)); @@ -271,27 +233,19 @@ public class Admin await ctx.Reply($"{Emojis.Success} Group ID updated (`{target.Hid}` -> `{newHid}`)."); } - public async Task SystemMemberLimit(Context ctx) + public async Task SystemMemberLimit(Context ctx, PKSystem target, int? newLimit) { ctx.AssertBotAdmin(); - var target = await ctx.MatchSystem(); - if (target == null) - throw new PKError("Unknown system."); - var config = await ctx.Repository.GetSystemConfig(target.Id); var currentLimit = config.MemberLimitOverride ?? Limits.MaxMemberCount; - if (!ctx.HasNext()) + if (newLimit == null) { await ctx.Reply(null, await CreateEmbed(ctx, target)); return; } - var newLimitStr = ctx.PopArgument().ToLower().Replace(",", null).Replace("k", "000"); - if (!int.TryParse(newLimitStr, out var newLimit)) - throw new PKError($"Couldn't parse `{newLimitStr}` as number."); - await ctx.Reply(null, await CreateEmbed(ctx, target)); if (!await ctx.PromptYesNo($"Update member limit from **{currentLimit}** to **{newLimit}**?", "Update")) throw new PKError("Member limit change cancelled."); @@ -300,27 +254,19 @@ public class Admin await ctx.Reply($"{Emojis.Success} Member limit updated."); } - public async Task SystemGroupLimit(Context ctx) + public async Task SystemGroupLimit(Context ctx, PKSystem target, int? newLimit) { ctx.AssertBotAdmin(); - var target = await ctx.MatchSystem(); - if (target == null) - throw new PKError("Unknown system."); - var config = await ctx.Repository.GetSystemConfig(target.Id); var currentLimit = config.GroupLimitOverride ?? Limits.MaxGroupCount; - if (!ctx.HasNext()) + if (newLimit == null) { await ctx.Reply(null, await CreateEmbed(ctx, target)); return; } - var newLimitStr = ctx.PopArgument().ToLower().Replace(",", null).Replace("k", "000"); - if (!int.TryParse(newLimitStr, out var newLimit)) - throw new PKError($"Couldn't parse `{newLimitStr}` as number."); - await ctx.Reply(null, await CreateEmbed(ctx, target)); if (!await ctx.PromptYesNo($"Update group limit from **{currentLimit}** to **{newLimit}**?", "Update")) throw new PKError("Group limit change cancelled."); @@ -329,13 +275,10 @@ public class Admin await ctx.Reply($"{Emojis.Success} Group limit updated."); } - public async Task SystemRecover(Context ctx) + public async Task SystemRecover(Context ctx, string systemToken, User account, bool rerollToken) { ctx.AssertBotAdmin(); - var rerollToken = ctx.MatchFlag("rt", "reroll-token"); - - var systemToken = ctx.PopArgument(); var systemId = await ctx.Database.Execute(conn => conn.QuerySingleOrDefaultAsync( "select id from systems where token = @token", new { token = systemToken } @@ -344,10 +287,6 @@ public class Admin if (systemId == null) throw new PKError("Could not retrieve a system with that token."); - var account = await ctx.MatchUser(); - if (account == null) - throw new PKError("You must pass an account to associate the system with (either ID or @mention)."); - var existingAccount = await ctx.Repository.GetSystemByAccount(account.Id); if (existingAccount != null) throw Errors.AccountInOtherSystem(existingAccount, ctx.Config, ctx.DefaultPrefix); @@ -378,14 +317,10 @@ public class Admin }); } - public async Task SystemDelete(Context ctx) + public async Task SystemDelete(Context ctx, PKSystem target) { ctx.AssertBotAdmin(); - var target = await ctx.MatchSystem(); - if (target == null) - throw new PKError("Unknown system."); - await ctx.Reply($"To delete the following system, reply with the system's UUID: `{target.Uuid.ToString()}`", await CreateEmbed(ctx, target)); if (!await ctx.ConfirmWithReply(target.Uuid.ToString())) @@ -396,18 +331,11 @@ public class Admin await ctx.Reply($"{Emojis.Success} System deletion succesful."); } - public async Task AbuseLogCreate(Context ctx) + public async Task AbuseLogCreate(Context ctx, User account, bool denyBotUsage, string? description) { - var denyBotUsage = ctx.MatchFlag("deny", "deny-bot-usage"); - var account = await ctx.MatchUser(); - if (account == null) - throw new PKError("You must pass an account to associate the abuse log with (either ID or @mention)."); + ctx.AssertBotAdmin(); - string? desc = null!; - if (ctx.HasNext(false)) - desc = ctx.RemainderOrNull(false).NormalizeLineEndSpacing(); - - var abuseLog = await ctx.Repository.CreateAbuseLog(desc, denyBotUsage); + var abuseLog = await ctx.Repository.CreateAbuseLog(description, denyBotUsage); await ctx.Repository.AddAbuseLogAccount(abuseLog.Id, account.Id); await ctx.Reply( @@ -415,14 +343,49 @@ public class Admin await CreateAbuseLogEmbed(ctx, abuseLog)); } - public async Task AbuseLogShow(Context ctx, AbuseLog abuseLog) + public async Task GetAbuseLog(Context ctx, User? account, string? id) { + ctx.AssertBotAdmin(); + + AbuseLog? abuseLog = null!; + if (account != null) + { + abuseLog = await ctx.Repository.GetAbuseLogByAccount(account.Id); + } + else + { + abuseLog = await ctx.Repository.GetAbuseLogByGuid(new Guid(id)); + } + + if (abuseLog == null) + { + await ctx.Reply($"{Emojis.Error} Could not find an existing abuse log entry for that query."); + return null; + } + + return abuseLog; + } + + public async Task AbuseLogShow(Context ctx, User? account, string? id) + { + ctx.AssertBotAdmin(); + + AbuseLog? abuseLog = await GetAbuseLog(ctx, account, id); + if (abuseLog == null) + return; + await ctx.Reply(null, await CreateAbuseLogEmbed(ctx, abuseLog)); } - public async Task AbuseLogFlagDeny(Context ctx, AbuseLog abuseLog) + public async Task AbuseLogFlagDeny(Context ctx, User? account, string? id, bool? value) { - if (!ctx.HasNext()) + ctx.AssertBotAdmin(); + + AbuseLog? abuseLog = await GetAbuseLog(ctx, account, id); + if (abuseLog == null) + return; + + if (value == null) { await ctx.Reply( $"Bot usage is currently {(abuseLog.DenyBotUsage ? "denied" : "allowed")} " @@ -430,27 +393,31 @@ public class Admin } else { - var value = ctx.MatchToggle(true); if (abuseLog.DenyBotUsage != value) - await ctx.Repository.UpdateAbuseLog(abuseLog.Id, new AbuseLogPatch { DenyBotUsage = value }); + await ctx.Repository.UpdateAbuseLog(abuseLog.Id, new AbuseLogPatch { DenyBotUsage = value.Value }); await ctx.Reply( - $"Bot usage is now **{(value ? "denied" : "allowed")}** " + $"Bot usage is now **{(value.Value ? "denied" : "allowed")}** " + $"for accounts associated with abuse log `{abuseLog.Uuid}`."); } } - public async Task AbuseLogDescription(Context ctx, AbuseLog abuseLog) + public async Task AbuseLogDescription(Context ctx, User? account, string? id, string? description, bool clear) { - if (ctx.MatchClear() && await ctx.ConfirmClear("this abuse log description")) + ctx.AssertBotAdmin(); + + AbuseLog? abuseLog = await GetAbuseLog(ctx, account, id); + if (abuseLog == null) + return; + + if (clear && await ctx.ConfirmClear("this abuse log description")) { await ctx.Repository.UpdateAbuseLog(abuseLog.Id, new AbuseLogPatch { Description = null }); await ctx.Reply($"{Emojis.Success} Abuse log description cleared."); } - else if (ctx.HasNext()) + else if (description != null) { - var desc = ctx.RemainderOrNull(false).NormalizeLineEndSpacing(); - await ctx.Repository.UpdateAbuseLog(abuseLog.Id, new AbuseLogPatch { Description = desc }); + await ctx.Repository.UpdateAbuseLog(abuseLog.Id, new AbuseLogPatch { Description = description }); await ctx.Reply($"{Emojis.Success} Abuse log description updated."); } else @@ -461,11 +428,13 @@ public class Admin } } - public async Task AbuseLogAddUser(Context ctx, AbuseLog abuseLog) + public async Task AbuseLogAddUser(Context ctx, User? accountToFind, string? id, User account) { - var account = await ctx.MatchUser(); - if (account == null) - throw new PKError("You must pass an account to associate the abuse log with (either ID or @mention)."); + ctx.AssertBotAdmin(); + + AbuseLog? abuseLog = await GetAbuseLog(ctx, accountToFind, id); + if (abuseLog == null) + return; await ctx.Repository.AddAbuseLogAccount(abuseLog.Id, account.Id); await ctx.Reply( @@ -473,11 +442,13 @@ public class Admin await CreateAbuseLogEmbed(ctx, abuseLog)); } - public async Task AbuseLogRemoveUser(Context ctx, AbuseLog abuseLog) + public async Task AbuseLogRemoveUser(Context ctx, User? accountToFind, string? id, User account) { - var account = await ctx.MatchUser(); - if (account == null) - throw new PKError("You must pass an account to remove from the abuse log (either ID or @mention)."); + ctx.AssertBotAdmin(); + + AbuseLog? abuseLog = await GetAbuseLog(ctx, accountToFind, id); + if (abuseLog == null) + return; await ctx.Repository.UpdateAccount(account.Id, new() { @@ -489,8 +460,14 @@ public class Admin await CreateAbuseLogEmbed(ctx, abuseLog)); } - public async Task AbuseLogDelete(Context ctx, AbuseLog abuseLog) + public async Task AbuseLogDelete(Context ctx, User? account, string? id) { + ctx.AssertBotAdmin(); + + AbuseLog? abuseLog = await GetAbuseLog(ctx, account, id); + if (abuseLog == null) + return; + if (!await ctx.PromptYesNo($"Really delete abuse log entry `{abuseLog.Uuid}`?", "Delete", matchFlag: false)) { await ctx.Reply($"{Emojis.Error} Deletion cancelled."); @@ -501,17 +478,10 @@ public class Admin await ctx.Reply($"{Emojis.Success} Successfully deleted abuse log entry."); } - public async Task SendAdminMessage(Context ctx) + public async Task SendAdminMessage(Context ctx, User account, string content) { ctx.AssertBotAdmin(); - var account = await ctx.MatchUser(); - if (account == null) - throw new PKError("You must pass an account to send an admin message to (either ID or @mention)."); - if (!ctx.HasNext()) - throw new PKError("You must provide a message to send."); - - var content = ctx.RemainderOrNull(false).NormalizeLineEndSpacing(); var messageContent = $"## [Admin Message]\n\n{content}\n\nWe cannot read replies sent to this DM. If you wish to contact the staff team, please join the support server () or send us an email at ."; try diff --git a/PluralKit.Bot/Commands/Message.cs b/PluralKit.Bot/Commands/Message.cs index 0395cf87..7bb1f681 100644 --- a/PluralKit.Bot/Commands/Message.cs +++ b/PluralKit.Bot/Commands/Message.cs @@ -58,16 +58,14 @@ public class ProxiedMessage _redisService = redisService; } - public async Task ReproxyMessage(Context ctx, ulong? messageId) + public async Task ReproxyMessage(Context ctx, ulong? messageId, PKMember target) { var (msg, systemId) = await GetMessageToEdit(ctx, messageId, ReproxyTimeout, true); if (ctx.System.Id != systemId) throw new PKError("Can't reproxy a message sent by a different system."); - // Get target member ID - var target = await ctx.MatchMember(restrictToSystem: ctx.System.Id); - if (target == null) + if (target == null || target.System != ctx.System.Id) throw new PKError("Could not find a member to reproxy the message with."); // Fetch members and get the ProxyMember for `target` diff --git a/crates/command_definitions/src/admin.rs b/crates/command_definitions/src/admin.rs index 8b137891..56f4dd71 100644 --- a/crates/command_definitions/src/admin.rs +++ b/crates/command_definitions/src/admin.rs @@ -1 +1,64 @@ +use super::*; +pub fn admin() -> &'static str { + "admin" +} + +pub fn cmds() -> impl Iterator { + let admin = admin(); + + let abuselog = tokens!(admin, ("abuselog", ["al"])); + let make_abuselog_cmds = |log_param: Parameter| { + [ + command!(abuselog, ("show", ["s"]), log_param => format!("admin_abuselog_show_{}", log_param.name())) + .help("Shows an abuse log entry"), + command!(abuselog, ("flagdeny", ["fd"]), log_param, Optional(("value", Toggle)) => format!("admin_abuselog_flag_deny_{}", log_param.name())) + .help("Sets the deny flag on an abuse log entry"), + command!(abuselog, ("description", ["desc"]), log_param, Optional(("desc", OpaqueStringRemainder)) => format!("admin_abuselog_description_{}", log_param.name())) + .flag(("clear", ["c"])) + .help("Sets the description of an abuse log entry"), + command!(abuselog, ("adduser", ["au"]), log_param => format!("admin_abuselog_add_user_{}", log_param.name())) + .help("Adds a user to an abuse log entry"), + command!(abuselog, ("removeuser", ["ru"]), log_param => format!("admin_abuselog_remove_user_{}", log_param.name())) + .help("Removes a user from an abuse log entry"), + command!(abuselog, ("delete", ["d"]), log_param => format!("admin_abuselog_delete_{}", log_param.name())) + .help("Deletes an abuse log entry"), + ].into_iter() + }; + let abuselog_cmds = [ + command!(abuselog, ("create", ["c", "new"]), ("account", UserRef), Optional(("description", OpaqueStringRemainder)) => "admin_abuselog_create") + .flag(("deny-boy-usage", ["deny"])) + .help("Creates an abuse log entry") + ] + .into_iter() + .chain(make_abuselog_cmds(Skip(("account", UserRef)).into())) // falls through to log_id + .chain(make_abuselog_cmds(("log_id", OpaqueString).into())); + + [ + command!(admin, ("updatesystemid", ["usid"]), SystemRef, ("new_hid", OpaqueString) => "admin_update_system_id") + .help("Updates a system's ID"), + command!(admin, ("updatememberid", ["umid"]), MemberRef, ("new_hid", OpaqueString) => "admin_update_member_id") + .help("Updates a member's ID"), + command!(admin, ("updategroupid", ["ugid"]), GroupRef, ("new_hid", OpaqueString) => "admin_update_group_id") + .help("Updates a group's ID"), + command!(admin, ("rerollsystemid", ["rsid"]), SystemRef => "admin_reroll_system_id") + .help("Rerolls a system's ID"), + command!(admin, ("rerollmemberid", ["rmid"]), MemberRef => "admin_reroll_member_id") + .help("Rerolls a member's ID"), + command!(admin, ("rerollgroupid", ["rgid"]), GroupRef => "admin_reroll_group_id") + .help("Rerolls a group's ID"), + command!(admin, ("updatememberlimit", ["uml"]), SystemRef, Optional(("limit", OpaqueInt)) => "admin_system_member_limit") + .help("Updates a system's member limit"), + command!(admin, ("updategrouplimit", ["ugl"]), SystemRef, Optional(("limit", OpaqueInt)) => "admin_system_group_limit") + .help("Updates a system's group limit"), + command!(admin, ("systemrecover", ["sr"]), ("token", OpaqueString), ("account", UserRef) => "admin_system_recover") + .flag(("reroll-token", ["rt"])) + .help("Recovers a system"), + command!(admin, ("systemdelete", ["sd"]), SystemRef => "admin_system_delete") + .help("Deletes a system"), + command!(admin, ("sendmessage", ["sendmsg"]), ("account", UserRef), ("content", OpaqueStringRemainder) => "admin_send_message") + .help("Sends a message to a user"), + ] + .into_iter() + .chain(abuselog_cmds) +} diff --git a/crates/command_definitions/src/dashboard.rs b/crates/command_definitions/src/dashboard.rs deleted file mode 100644 index 8b137891..00000000 --- a/crates/command_definitions/src/dashboard.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/crates/command_definitions/src/help.rs b/crates/command_definitions/src/help.rs index 9a94dea8..8991cf72 100644 --- a/crates/command_definitions/src/help.rs +++ b/crates/command_definitions/src/help.rs @@ -5,9 +5,7 @@ pub fn cmds() -> impl Iterator { [ command!(("dashboard", ["dash"]) => "dashboard"), command!("explain" => "explain"), - command!(help => "help") - .flag(("foo", OpaqueString)) // todo: just for testing - .help("Shows the help command"), + command!(help => "help").help("Shows the help command"), command!(help, "commands" => "help_commands").help("help commands"), command!(help, "proxy" => "help_proxy").help("help proxy"), ] diff --git a/crates/command_definitions/src/lib.rs b/crates/command_definitions/src/lib.rs index 4204dad2..2fbc00fb 100644 --- a/crates/command_definitions/src/lib.rs +++ b/crates/command_definitions/src/lib.rs @@ -3,7 +3,6 @@ pub mod api; pub mod autoproxy; pub mod commands; pub mod config; -pub mod dashboard; pub mod debug; pub mod fun; pub mod group; @@ -40,10 +39,12 @@ pub fn all() -> impl Iterator { .chain(debug::cmds()) .chain(message::cmds()) .chain(import_export::cmds()) + .chain(admin::cmds()) .map(|cmd| { cmd.hidden_flag(("plaintext", ["pt"])) .hidden_flag(("raw", ["r"])) .hidden_flag(("show-embed", ["se"])) + .hidden_flag(("by-id", ["id"])) }) } diff --git a/crates/command_definitions/src/message.rs b/crates/command_definitions/src/message.rs index eeb2e53e..e36969c2 100644 --- a/crates/command_definitions/src/message.rs +++ b/crates/command_definitions/src/message.rs @@ -25,7 +25,7 @@ pub fn cmds() -> impl Iterator { .help("Deletes a proxied message"), apply_edit(command!(message, edit => "message_edit")), apply_edit(command!(edit => "message_edit")), - command!(("reproxy", ["rp", "crimes", "crime"]), MessageRef => "message_reproxy") + command!(("reproxy", ["rp", "crimes", "crime"]), ("msg", MessageRef), ("member", MemberRef) => "message_reproxy") .help("Reproxies a message with a different member"), ] .into_iter() diff --git a/crates/command_parser/src/parameter.rs b/crates/command_parser/src/parameter.rs index 28d7a693..04d035ea 100644 --- a/crates/command_parser/src/parameter.rs +++ b/crates/command_parser/src/parameter.rs @@ -3,18 +3,21 @@ use std::{ str::FromStr, }; -use smol_str::SmolStr; +use regex::Regex; +use smol_str::{SmolStr, format_smolstr}; use crate::token::{Token, TokenMatchResult}; #[derive(Debug, Clone)] pub enum ParameterValue { OpaqueString(String), + OpaqueInt(i32), MemberRef(String), MemberRefs(Vec), GroupRef(String), GroupRefs(Vec), SystemRef(String), + UserRef(u64), MessageRef(Option, Option, u64), ChannelRef(u64), GuildRef(u64), @@ -85,6 +88,10 @@ impl Parameter { ParameterKind::OpaqueString | ParameterKind::OpaqueStringRemainder => { Ok(ParameterValue::OpaqueString(input.into())) } + ParameterKind::OpaqueInt => input + .parse::() + .map(|num| ParameterValue::OpaqueInt(num)) + .map_err(|err| format_smolstr!("invalid integer: {err}")), ParameterKind::GroupRef => Ok(ParameterValue::GroupRef(input.into())), ParameterKind::GroupRefs => Ok(ParameterValue::GroupRefs( input.split(' ').map(|s| s.trim().to_string()).collect(), @@ -94,6 +101,22 @@ impl Parameter { input.split(' ').map(|s| s.trim().to_string()).collect(), )), ParameterKind::SystemRef => Ok(ParameterValue::SystemRef(input.into())), + ParameterKind::UserRef => { + if let Ok(user_id) = input.parse::() { + return Ok(ParameterValue::UserRef(user_id)); + } + + static RE: std::sync::LazyLock = + std::sync::LazyLock::new(|| Regex::new(r"<@!?(\\d{17,19})>").unwrap()); + if let Some(captures) = RE.captures(&input) { + return captures[1] + .parse::() + .map(|id| ParameterValue::UserRef(id)) + .map_err(|_| SmolStr::new("invalid user ID")); + } + + Err(SmolStr::new("invalid user ID")) + } ParameterKind::MemberPrivacyTarget => MemberPrivacyTargetKind::from_str(input) .map(|target| ParameterValue::MemberPrivacyTarget(target.as_ref().into())), ParameterKind::GroupPrivacyTarget => GroupPrivacyTargetKind::from_str(input) @@ -166,6 +189,9 @@ impl Display for Parameter { ParameterKind::OpaqueString => { write!(f, "[{}]", self.name) } + ParameterKind::OpaqueInt => { + write!(f, "[{}]", self.name) + } ParameterKind::OpaqueStringRemainder => { write!(f, "[{}]...", self.name) } @@ -174,6 +200,7 @@ impl Display for Parameter { ParameterKind::GroupRef => write!(f, ""), ParameterKind::GroupRefs => write!(f, " ..."), ParameterKind::SystemRef => write!(f, ""), + ParameterKind::UserRef => write!(f, ""), ParameterKind::MessageRef => write!(f, ""), ParameterKind::ChannelRef => write!(f, ""), ParameterKind::GuildRef => write!(f, ""), @@ -246,12 +273,14 @@ impl> From> for Parameter { #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum ParameterKind { OpaqueString, + OpaqueInt, OpaqueStringRemainder, MemberRef, MemberRefs, GroupRef, GroupRefs, SystemRef, + UserRef, MessageRef, ChannelRef, GuildRef, @@ -267,12 +296,14 @@ impl ParameterKind { pub(crate) fn default_name(&self) -> &str { match self { ParameterKind::OpaqueString => "string", + ParameterKind::OpaqueInt => "number", ParameterKind::OpaqueStringRemainder => "string", ParameterKind::MemberRef => "target", ParameterKind::MemberRefs => "targets", ParameterKind::GroupRef => "target", ParameterKind::GroupRefs => "targets", ParameterKind::SystemRef => "target", + ParameterKind::UserRef => "target", ParameterKind::MessageRef => "target", ParameterKind::ChannelRef => "target", ParameterKind::GuildRef => "target", diff --git a/crates/commands/src/bin/write_cs_glue.rs b/crates/commands/src/bin/write_cs_glue.rs index ff1a0886..f94aed77 100644 --- a/crates/commands/src/bin/write_cs_glue.rs +++ b/crates/commands/src/bin/write_cs_glue.rs @@ -260,11 +260,13 @@ fn command_callback_to_name(cb: &str) -> String { fn get_param_ty(kind: ParameterKind) -> &'static str { match kind { ParameterKind::OpaqueString | ParameterKind::OpaqueStringRemainder => "string", + ParameterKind::OpaqueInt => "int", ParameterKind::MemberRef => "PKMember", ParameterKind::MemberRefs => "List", ParameterKind::GroupRef => "PKGroup", ParameterKind::GroupRefs => "List", ParameterKind::SystemRef => "PKSystem", + ParameterKind::UserRef => "User", ParameterKind::MemberPrivacyTarget => "MemberPrivacySubject", ParameterKind::GroupPrivacyTarget => "GroupPrivacySubject", ParameterKind::SystemPrivacyTarget => "SystemPrivacySubject", @@ -280,11 +282,13 @@ fn get_param_ty(kind: ParameterKind) -> &'static str { fn get_param_param_ty(kind: ParameterKind) -> &'static str { match kind { ParameterKind::OpaqueString | ParameterKind::OpaqueStringRemainder => "Opaque", + ParameterKind::OpaqueInt => "Number", ParameterKind::MemberRef => "Member", ParameterKind::MemberRefs => "Members", ParameterKind::GroupRef => "Group", ParameterKind::GroupRefs => "Groups", ParameterKind::SystemRef => "System", + ParameterKind::UserRef => "User", ParameterKind::MemberPrivacyTarget => "MemberPrivacyTarget", ParameterKind::GroupPrivacyTarget => "GroupPrivacyTarget", ParameterKind::SystemPrivacyTarget => "SystemPrivacyTarget", diff --git a/crates/commands/src/commands.udl b/crates/commands/src/commands.udl index 757e6615..7fd3b312 100644 --- a/crates/commands/src/commands.udl +++ b/crates/commands/src/commands.udl @@ -13,6 +13,7 @@ interface Parameter { GroupRef(string group); GroupRefs(sequence groups); SystemRef(string system); + UserRef(u64 user_id); MessageRef(u64? guild_id, u64? channel_id, u64 message_id); ChannelRef(u64 channel_id); GuildRef(u64 guild_id); @@ -21,6 +22,7 @@ interface Parameter { SystemPrivacyTarget(string target); PrivacyLevel(string level); OpaqueString(string raw); + OpaqueInt(i32 raw); Toggle(boolean toggle); Avatar(string avatar); Null(); diff --git a/crates/commands/src/lib.rs b/crates/commands/src/lib.rs index 65199a7d..b91cf137 100644 --- a/crates/commands/src/lib.rs +++ b/crates/commands/src/lib.rs @@ -37,6 +37,9 @@ pub enum Parameter { SystemRef { system: String, }, + UserRef { + user_id: u64, + }, MessageRef { guild_id: Option, channel_id: Option, @@ -63,6 +66,9 @@ pub enum Parameter { OpaqueString { raw: String, }, + OpaqueInt { + raw: i32, + }, Toggle { toggle: bool, }, @@ -80,11 +86,13 @@ impl From for Parameter { ParameterValue::GroupRef(group) => Self::GroupRef { group }, ParameterValue::GroupRefs(groups) => Self::GroupRefs { groups }, ParameterValue::SystemRef(system) => Self::SystemRef { system }, + ParameterValue::UserRef(user_id) => Self::UserRef { user_id }, ParameterValue::MemberPrivacyTarget(target) => Self::MemberPrivacyTarget { target }, ParameterValue::GroupPrivacyTarget(target) => Self::GroupPrivacyTarget { target }, ParameterValue::SystemPrivacyTarget(target) => Self::SystemPrivacyTarget { target }, ParameterValue::PrivacyLevel(level) => Self::PrivacyLevel { level }, ParameterValue::OpaqueString(raw) => Self::OpaqueString { raw }, + ParameterValue::OpaqueInt(raw) => Self::OpaqueInt { raw }, ParameterValue::Toggle(toggle) => Self::Toggle { toggle }, ParameterValue::Avatar(avatar) => Self::Avatar { avatar }, ParameterValue::MessageRef(guild_id, channel_id, message_id) => Self::MessageRef { From 0f26a69f1beac5e0ff6213738673451f931df9b7 Mon Sep 17 00:00:00 2001 From: dusk Date: Sat, 4 Oct 2025 19:32:58 +0000 Subject: [PATCH 109/179] implement the rest of the config commands --- PluralKit.Bot/CommandMeta/CommandTree.cs | 82 ++-- .../Context/ContextParametersExt.cs | 8 + PluralKit.Bot/CommandSystem/Parameters.cs | 14 + PluralKit.Bot/Commands/Config.cs | 430 ++++++++---------- PluralKit.Bot/Commands/SystemLink.cs | 5 +- crates/command_definitions/src/config.rs | 183 +++++++- crates/command_definitions/src/system.rs | 4 +- crates/command_parser/src/parameter.rs | 41 +- crates/commands/src/bin/write_cs_glue.rs | 2 + crates/commands/src/commands.udl | 1 + crates/commands/src/lib.rs | 6 + 11 files changed, 480 insertions(+), 296 deletions(-) diff --git a/PluralKit.Bot/CommandMeta/CommandTree.cs b/PluralKit.Bot/CommandMeta/CommandTree.cs index 6559a6e9..000c3d27 100644 --- a/PluralKit.Bot/CommandMeta/CommandTree.cs +++ b/PluralKit.Bot/CommandMeta/CommandTree.cs @@ -70,12 +70,45 @@ public partial class CommandTree Commands.MemberGroupAdd(var param, _) => ctx.Execute(MemberGroupAdd, m => m.AddRemoveGroups(ctx, param.target, param.groups, Groups.AddRemoveOperation.Add)), Commands.MemberGroupRemove(var param, _) => ctx.Execute(MemberGroupRemove, m => m.AddRemoveGroups(ctx, param.target, param.groups, Groups.AddRemoveOperation.Remove)), Commands.MemberId(var param, _) => ctx.Execute(MemberId, m => m.DisplayId(ctx, param.target)), + Commands.CfgShow => ctx.Execute(null, m => m.ShowConfig(ctx)), Commands.CfgApAccountShow => ctx.Execute(null, m => m.ViewAutoproxyAccount(ctx)), Commands.CfgApAccountUpdate(var param, _) => ctx.Execute(null, m => m.EditAutoproxyAccount(ctx, param.toggle)), Commands.CfgApTimeoutShow => ctx.Execute(null, m => m.ViewAutoproxyTimeout(ctx)), Commands.CfgApTimeoutOff => ctx.Execute(null, m => m.DisableAutoproxyTimeout(ctx)), Commands.CfgApTimeoutReset => ctx.Execute(null, m => m.ResetAutoproxyTimeout(ctx)), Commands.CfgApTimeoutUpdate(var param, _) => ctx.Execute(null, m => m.EditAutoproxyTimeout(ctx, param.timeout)), + Commands.CfgTimezoneShow => ctx.Execute(null, m => m.ViewSystemTimezone(ctx)), + Commands.CfgTimezoneReset => ctx.Execute(null, m => m.ResetSystemTimezone(ctx)), + Commands.CfgTimezoneUpdate(var param, _) => ctx.Execute(null, m => m.EditSystemTimezone(ctx, param.timezone)), + Commands.CfgPingShow => ctx.Execute(null, m => m.ViewSystemPing(ctx)), + Commands.CfgPingUpdate(var param, _) => ctx.Execute(null, m => m.EditSystemPing(ctx, param.toggle)), + Commands.CfgMemberPrivacyShow => ctx.Execute(null, m => m.ViewMemberDefaultPrivacy(ctx)), + Commands.CfgMemberPrivacyUpdate(var param, _) => ctx.Execute(null, m => m.EditMemberDefaultPrivacy(ctx, param.toggle)), + Commands.CfgGroupPrivacyShow => ctx.Execute(null, m => m.ViewGroupDefaultPrivacy(ctx)), + Commands.CfgGroupPrivacyUpdate(var param, _) => ctx.Execute(null, m => m.EditGroupDefaultPrivacy(ctx, param.toggle)), + Commands.CfgShowPrivateInfoShow => ctx.Execute(null, m => m.ViewShowPrivateInfo(ctx)), + Commands.CfgShowPrivateInfoUpdate(var param, _) => ctx.Execute(null, m => m.EditShowPrivateInfo(ctx, param.toggle)), + Commands.CfgCaseSensitiveProxyTagsShow => ctx.Execute(null, m => m.ViewCaseSensitiveProxyTags(ctx)), + Commands.CfgCaseSensitiveProxyTagsUpdate(var param, _) => ctx.Execute(null, m => m.EditCaseSensitiveProxyTags(ctx, param.toggle)), + Commands.CfgProxyErrorMessageShow => ctx.Execute(null, m => m.ViewProxyErrorMessageEnabled(ctx)), + Commands.CfgProxyErrorMessageUpdate(var param, _) => ctx.Execute(null, m => m.EditProxyErrorMessageEnabled(ctx, param.toggle)), + Commands.CfgHidSplitShow => ctx.Execute(null, m => m.ViewHidDisplaySplit(ctx)), + Commands.CfgHidSplitUpdate(var param, _) => ctx.Execute(null, m => m.EditHidDisplaySplit(ctx, param.toggle)), + Commands.CfgHidCapsShow => ctx.Execute(null, m => m.ViewHidDisplayCaps(ctx)), + Commands.CfgHidCapsUpdate(var param, _) => ctx.Execute(null, m => m.EditHidDisplayCaps(ctx, param.toggle)), + Commands.CfgHidPaddingShow => ctx.Execute(null, m => m.ViewHidListPadding(ctx)), + Commands.CfgHidPaddingUpdate(var param, _) => ctx.Execute(null, m => m.EditHidListPadding(ctx, param.padding)), + Commands.CfgCardShowColorHexShow => ctx.Execute(null, m => m.ViewCardShowColorHex(ctx)), + Commands.CfgCardShowColorHexUpdate(var param, _) => ctx.Execute(null, m => m.EditCardShowColorHex(ctx, param.toggle)), + Commands.CfgProxySwitchShow => ctx.Execute(null, m => m.ViewProxySwitch(ctx)), + Commands.CfgProxySwitchUpdate(var param, _) => ctx.Execute(null, m => m.EditProxySwitch(ctx, param.proxy_switch_action)), + Commands.CfgNameFormatShow => ctx.Execute(null, m => m.ViewNameFormat(ctx)), + Commands.CfgNameFormatReset => ctx.Execute(null, m => m.ResetNameFormat(ctx)), + Commands.CfgNameFormatUpdate(var param, _) => ctx.Execute(null, m => m.EditNameFormat(ctx, param.format)), + Commands.CfgServerNameFormatShow(_, var flags) => ctx.Execute(null, m => m.ViewServerNameFormat(ctx, flags.GetReplyFormat())), + Commands.CfgServerNameFormatReset => ctx.Execute(null, m => m.ResetServerNameFormat(ctx)), + Commands.CfgServerNameFormatUpdate(var param, _) => ctx.Execute(null, m => m.EditServerNameFormat(ctx, param.format)), + Commands.CfgLimitsUpdate => ctx.Execute(null, m => m.LimitUpdate(ctx)), Commands.FunThunder => ctx.Execute(null, m => m.Thunder(ctx)), Commands.FunMeow => ctx.Execute(null, m => m.Meow(ctx)), Commands.FunPokemon => ctx.Execute(null, m => m.Mn(ctx)), @@ -187,8 +220,8 @@ public partial class CommandTree ? ctx.Execute(GroupRandom, m => m.Group(ctx, param.target, flags.all, flags.show_embed)) : ctx.Execute(MemberRandom, m => m.Member(ctx, param.target, flags.all, flags.show_embed)), Commands.GroupRandomMember(var param, var flags) => ctx.Execute(GroupMemberRandom, m => m.GroupMember(ctx, param.target, flags)), - Commands.SystemLink => ctx.Execute(Link, m => m.LinkSystem(ctx)), - Commands.SystemUnlink(var param, _) => ctx.Execute(Unlink, m => m.UnlinkAccount(ctx, param.target)), + Commands.SystemLink(var param, _) => ctx.Execute(Link, m => m.LinkSystem(ctx, param.account)), + Commands.SystemUnlink(var param, _) => ctx.Execute(Unlink, m => m.UnlinkAccount(ctx, param.account)), Commands.SystemMembersListSelf(var param, var flags) => ctx.Execute(SystemList, m => m.MemberList(ctx, ctx.System, null, flags)), Commands.SystemMembersSearchSelf(var param, var flags) => ctx.Execute(SystemFind, m => m.MemberList(ctx, ctx.System, param.query, flags)), Commands.SystemMembersList(var param, var flags) => ctx.Execute(SystemList, m => m.MemberList(ctx, param.target, null, flags)), @@ -279,8 +312,6 @@ public partial class CommandTree }; if (ctx.Match("commands", "cmd", "c")) return CommandHelpRoot(ctx); - if (ctx.Match("config", "cfg", "configure")) - return HandleConfigCommand(ctx); if (ctx.Match("serverconfig", "guildconfig", "scfg")) return HandleServerConfigCommand(ctx); if (ctx.Match("log")) @@ -371,49 +402,6 @@ public partial class CommandTree } } - private Task HandleConfigCommand(Context ctx) - { - if (ctx.System == null) - return ctx.Reply($"{Emojis.Error} {Errors.NoSystemError(ctx.DefaultPrefix).Message}"); - - if (!ctx.HasNext()) - return ctx.Execute(null, m => m.ShowConfig(ctx)); - - if (ctx.Match("timezone", "zone", "tz")) - return ctx.Execute(null, m => m.SystemTimezone(ctx)); - if (ctx.Match("ping")) - return ctx.Execute(null, m => m.SystemPing(ctx)); - if (ctx.MatchMultiple(new[] { "private" }, new[] { "member" }) || ctx.Match("mp")) - return ctx.Execute(null, m => m.MemberDefaultPrivacy(ctx)); - if (ctx.MatchMultiple(new[] { "private" }, new[] { "group" }) || ctx.Match("gp")) - return ctx.Execute(null, m => m.GroupDefaultPrivacy(ctx)); - if (ctx.MatchMultiple(new[] { "show" }, new[] { "private" }) || ctx.Match("sp")) - return ctx.Execute(null, m => m.ShowPrivateInfo(ctx)); - if (ctx.MatchMultiple(new[] { "proxy" }, new[] { "case" })) - return ctx.Execute(null, m => m.CaseSensitiveProxyTags(ctx)); - if (ctx.MatchMultiple(new[] { "proxy" }, new[] { "error" }) || ctx.Match("pe")) - return ctx.Execute(null, m => m.ProxyErrorMessageEnabled(ctx)); - if (ctx.MatchMultiple(new[] { "split" }, new[] { "id", "ids" }) || ctx.Match("sid", "sids")) - return ctx.Execute(null, m => m.HidDisplaySplit(ctx)); - if (ctx.MatchMultiple(new[] { "cap", "caps", "capitalize", "capitalise" }, new[] { "id", "ids" }) || ctx.Match("capid", "capids")) - return ctx.Execute(null, m => m.HidDisplayCaps(ctx)); - if (ctx.MatchMultiple(new[] { "pad" }, new[] { "id", "ids" }) || ctx.MatchMultiple(new[] { "id" }, new[] { "pad", "padding" }) || ctx.Match("idpad", "padid", "padids")) - return ctx.Execute(null, m => m.HidListPadding(ctx)); - if (ctx.MatchMultiple(new[] { "show" }, new[] { "color", "colour", "colors", "colours" }) || ctx.Match("showcolor", "showcolour", "showcolors", "showcolours", "colorcode", "colorhex")) - return ctx.Execute(null, m => m.CardShowColorHex(ctx)); - if (ctx.MatchMultiple(new[] { "name" }, new[] { "format" }) || ctx.Match("nameformat", "nf")) - return ctx.Execute(null, m => m.NameFormat(ctx)); - if (ctx.MatchMultiple(new[] { "member", "group" }, new[] { "limit" }) || ctx.Match("limit")) - return ctx.Execute(null, m => m.LimitUpdate(ctx)); - if (ctx.MatchMultiple(new[] { "proxy" }, new[] { "switch" }) || ctx.Match("proxyswitch", "ps")) - return ctx.Execute(null, m => m.ProxySwitch(ctx)); - if (ctx.MatchMultiple(new[] { "server" }, new[] { "name" }, new[] { "format" }) || ctx.MatchMultiple(new[] { "server", "servername" }, new[] { "format", "nameformat", "nf" }) || ctx.Match("snf", "servernf", "servernameformat", "snameformat")) - return ctx.Execute(null, m => m.ServerNameFormat(ctx)); - - // todo: maybe add the list of configuration keys here? - return ctx.Reply($"{Emojis.Error} Could not find a setting with that name. Please see `{ctx.DefaultPrefix}commands config` for the list of possible config settings."); - } - private Task HandleServerConfigCommand(Context ctx) { if (!ctx.HasNext()) diff --git a/PluralKit.Bot/CommandSystem/Context/ContextParametersExt.cs b/PluralKit.Bot/CommandSystem/Context/ContextParametersExt.cs index 53e6de76..61086919 100644 --- a/PluralKit.Bot/CommandSystem/Context/ContextParametersExt.cs +++ b/PluralKit.Bot/CommandSystem/Context/ContextParametersExt.cs @@ -101,6 +101,14 @@ public static class ContextParametersExt ); } + public static async Task ParamResolveProxySwitchAction(this Context ctx, string param_name) + { + return await ctx.Parameters.ResolveParameter( + ctx, param_name, + param => (param as Parameter.ProxySwitchAction)?.action + ); + } + public static async Task ParamResolveToggle(this Context ctx, string param_name) { return await ctx.Parameters.ResolveParameter( diff --git a/PluralKit.Bot/CommandSystem/Parameters.cs b/PluralKit.Bot/CommandSystem/Parameters.cs index d6ae35e4..b359e80b 100644 --- a/PluralKit.Bot/CommandSystem/Parameters.cs +++ b/PluralKit.Bot/CommandSystem/Parameters.cs @@ -26,6 +26,7 @@ public abstract record Parameter() public record Opaque(string value): Parameter; public record Number(int value): Parameter; public record Avatar(ParsedImage avatar): Parameter; + public record ProxySwitchAction(SystemConfig.ProxySwitchAction action): Parameter; } public class Parameters @@ -122,6 +123,19 @@ public class Parameters return new Parameter.SystemPrivacyTarget(systemPrivacy); case uniffi.commands.Parameter.PrivacyLevel privacyLevel: return new Parameter.PrivacyLevel(privacyLevel.level == "public" ? PrivacyLevel.Public : privacyLevel.level == "private" ? PrivacyLevel.Private : throw new PKError($"Invalid privacy level {privacyLevel.level}")); + case uniffi.commands.Parameter.ProxySwitchAction(var action): + SystemConfig.ProxySwitchAction newVal; + + if (action.Equals("off", StringComparison.InvariantCultureIgnoreCase)) + newVal = SystemConfig.ProxySwitchAction.Off; + else if (action.Equals("new", StringComparison.InvariantCultureIgnoreCase) || action.Equals("n", StringComparison.InvariantCultureIgnoreCase) || action.Equals("on", StringComparison.InvariantCultureIgnoreCase)) + newVal = SystemConfig.ProxySwitchAction.New; + else if (action.Equals("add", StringComparison.InvariantCultureIgnoreCase) || action.Equals("a", StringComparison.InvariantCultureIgnoreCase)) + newVal = SystemConfig.ProxySwitchAction.Add; + else + throw new PKError("You must pass either \"new\", \"add\", or \"off\" to this command."); + + return new Parameter.ProxySwitchAction(newVal); case uniffi.commands.Parameter.Toggle toggle: return new Parameter.Toggle(toggle.toggle); case uniffi.commands.Parameter.OpaqueString opaque: diff --git a/PluralKit.Bot/Commands/Config.cs b/PluralKit.Bot/Commands/Config.cs index 65d1cd9e..ff34e3d8 100644 --- a/PluralKit.Bot/Commands/Config.cs +++ b/PluralKit.Bot/Commands/Config.cs @@ -273,25 +273,26 @@ public class Config await ctx.Reply($"{Emojis.Success} Latch timeout set to {newTimeout.ToTimeSpan().Humanize(4)}."); } - public async Task SystemTimezone(Context ctx) + public async Task ViewSystemTimezone(Context ctx) { if (ctx.System == null) throw Errors.NoSystemError(ctx.DefaultPrefix); - if (ctx.MatchClear()) - { - await ctx.Repository.UpdateSystemConfig(ctx.System.Id, new() { UiTz = "UTC" }); + await ctx.Reply( + $"Your current system time zone is set to **{ctx.Config.UiTz}**. It is currently **{SystemClock.Instance.GetCurrentInstant().FormatZoned(ctx.Config.Zone)}** in that time zone. To change your system time zone, type `{ctx.DefaultPrefix}config tz `."); + } - await ctx.Reply($"{Emojis.Success} System time zone cleared (set to UTC)."); - return; - } + public async Task ResetSystemTimezone(Context ctx) + { + if (ctx.System == null) throw Errors.NoSystemError(ctx.DefaultPrefix); - var zoneStr = ctx.RemainderOrNull(); - if (zoneStr == null) - { - await ctx.Reply( - $"Your current system time zone is set to **{ctx.Config.UiTz}**. It is currently **{SystemClock.Instance.GetCurrentInstant().FormatZoned(ctx.Config.Zone)}** in that time zone. To change your system time zone, type `{ctx.DefaultPrefix}config tz `."); - return; - } + await ctx.Repository.UpdateSystemConfig(ctx.System.Id, new() { UiTz = "UTC" }); + + await ctx.Reply($"{Emojis.Success} System time zone cleared (set to UTC)."); + } + + public async Task EditSystemTimezone(Context ctx, string zoneStr) + { + if (ctx.System == null) throw Errors.NoSystemError(ctx.DefaultPrefix); var zone = await FindTimeZone(ctx, zoneStr); if (zone == null) throw Errors.InvalidTimeZone(zoneStr); @@ -365,27 +366,24 @@ public class Config }); } - public async Task SystemPing(Context ctx) + public async Task ViewSystemPing(Context ctx) { // note: this is here because this is also used in `pk;system ping`, which does not CheckSystem ctx.CheckSystem(); - // todo: move all the other config settings to this format + await ctx.Reply($"Reaction pings are currently **{EnabledDisabled(ctx.Config.PingsEnabled)}** for your system. " + + $"To {EnabledDisabled(!ctx.Config.PingsEnabled)[..^1]} reaction pings, type `{ctx.DefaultPrefix}config ping {EnabledDisabled(!ctx.Config.PingsEnabled)[..^1]}`."); + } - String Response(bool isError, bool val) - => $"Reaction pings are {(isError ? "already" : "currently")} **{EnabledDisabled(val)}** for your system. " - + $"To {EnabledDisabled(!val)[..^1]} reaction pings, type `{ctx.DefaultPrefix}config ping {EnabledDisabled(!val)[..^1]}`."; - - if (!ctx.HasNext()) - { - await ctx.Reply(Response(false, ctx.Config.PingsEnabled)); - return; - } - - var value = ctx.MatchToggle(true); + public async Task EditSystemPing(Context ctx, bool value) + { + ctx.CheckSystem(); if (ctx.Config.PingsEnabled == value) - await ctx.Reply(Response(true, ctx.Config.PingsEnabled)); + { + await ctx.Reply($"Reaction pings are already **{EnabledDisabled(ctx.Config.PingsEnabled)}** for your system. " + + $"To {EnabledDisabled(!value)[..^1]} reaction pings, type `{ctx.DefaultPrefix}config ping {EnabledDisabled(!value)[..^1]}`."); + } else { await ctx.Repository.UpdateSystemConfig(ctx.System.Id, new() { PingsEnabled = value }); @@ -393,230 +391,182 @@ public class Config } } - public async Task MemberDefaultPrivacy(Context ctx) + public async Task ViewMemberDefaultPrivacy(Context ctx) { - if (!ctx.HasNext()) - { - if (ctx.Config.MemberDefaultPrivate) { await ctx.Reply($"Newly created members will currently have their privacy settings set to private. To change this, type `{ctx.DefaultPrefix}config private member off`"); } - else { await ctx.Reply($"Newly created members will currently have their privacy settings set to public. To automatically set new members' privacy settings to private, type `{ctx.DefaultPrefix}config private member on`"); } - } + if (ctx.Config.MemberDefaultPrivate) + await ctx.Reply($"Newly created members will currently have their privacy settings set to private. To change this, type `{ctx.DefaultPrefix}config private member off`"); else - { - if (ctx.MatchToggle(false)) - { - await ctx.Repository.UpdateSystemConfig(ctx.System.Id, new() { MemberDefaultPrivate = true }); - - await ctx.Reply("Newly created members will now have their privacy settings set to private."); - } - else - { - await ctx.Repository.UpdateSystemConfig(ctx.System.Id, new() { MemberDefaultPrivate = false }); - - await ctx.Reply("Newly created members will now have their privacy settings set to public."); - } - } + await ctx.Reply($"Newly created members will currently have their privacy settings set to public. To automatically set new members' privacy settings to private, type `{ctx.DefaultPrefix}config private member on`"); } - public async Task GroupDefaultPrivacy(Context ctx) + public async Task EditMemberDefaultPrivacy(Context ctx, bool value) { - if (!ctx.HasNext()) - { - if (ctx.Config.GroupDefaultPrivate) { await ctx.Reply($"Newly created groups will currently have their privacy settings set to private. To change this, type `{ctx.DefaultPrefix}config private group off`"); } - else { await ctx.Reply($"Newly created groups will currently have their privacy settings set to public. To automatically set new groups' privacy settings to private, type `{ctx.DefaultPrefix}config private group on`"); } - } + await ctx.Repository.UpdateSystemConfig(ctx.System.Id, new() { MemberDefaultPrivate = value }); + + if (value) + await ctx.Reply("Newly created members will now have their privacy settings set to private."); else - { - if (ctx.MatchToggle(false)) - { - await ctx.Repository.UpdateSystemConfig(ctx.System.Id, new() { GroupDefaultPrivate = true }); - - await ctx.Reply("Newly created groups will now have their privacy settings set to private."); - } - else - { - await ctx.Repository.UpdateSystemConfig(ctx.System.Id, new() { GroupDefaultPrivate = false }); - - await ctx.Reply("Newly created groups will now have their privacy settings set to public."); - } - } + await ctx.Reply("Newly created members will now have their privacy settings set to public."); } - public async Task ShowPrivateInfo(Context ctx) + public async Task ViewGroupDefaultPrivacy(Context ctx) { - if (!ctx.HasNext()) - { - if (ctx.Config.ShowPrivateInfo) await ctx.Reply("Private information is currently **shown** when looking up your own info. Use the `-public` flag to hide it."); - else await ctx.Reply("Private information is currently **hidden** when looking up your own info. Use the `-private` flag to show it."); - return; - } + if (ctx.Config.GroupDefaultPrivate) + await ctx.Reply($"Newly created groups will currently have their privacy settings set to private. To change this, type `{ctx.DefaultPrefix}config private group off`"); + else + await ctx.Reply($"Newly created groups will currently have their privacy settings set to public. To automatically set new groups' privacy settings to private, type `{ctx.DefaultPrefix}config private group on`"); + } - if (ctx.MatchToggle(true)) - { - await ctx.Repository.UpdateSystemConfig(ctx.System.Id, new() { ShowPrivateInfo = true }); + public async Task EditGroupDefaultPrivacy(Context ctx, bool value) + { + await ctx.Repository.UpdateSystemConfig(ctx.System.Id, new() { GroupDefaultPrivate = value }); + if (value) + await ctx.Reply("Newly created groups will now have their privacy settings set to private."); + else + await ctx.Reply("Newly created groups will now have their privacy settings set to public."); + } + + public async Task ViewShowPrivateInfo(Context ctx) + { + if (ctx.Config.ShowPrivateInfo) + await ctx.Reply("Private information is currently **shown** when looking up your own info. Use the `-public` flag to hide it."); + else + await ctx.Reply("Private information is currently **hidden** when looking up your own info. Use the `-private` flag to show it."); + } + + public async Task EditShowPrivateInfo(Context ctx, bool value) + { + await ctx.Repository.UpdateSystemConfig(ctx.System.Id, new() { ShowPrivateInfo = value }); + + if (value) await ctx.Reply("Private information will now be **shown** when looking up your own info. Use the `-public` flag to hide it."); - } else - { - await ctx.Repository.UpdateSystemConfig(ctx.System.Id, new() { ShowPrivateInfo = false }); - await ctx.Reply("Private information will now be **hidden** when looking up your own info. Use the `-private` flag to show it."); - } } - public async Task CaseSensitiveProxyTags(Context ctx) + public async Task ViewCaseSensitiveProxyTags(Context ctx) { - if (!ctx.HasNext()) - { - if (ctx.Config.CaseSensitiveProxyTags) { await ctx.Reply("Proxy tags are currently case **sensitive**."); } - else { await ctx.Reply("Proxy tags are currently case **insensitive**."); } - return; - } + if (ctx.Config.CaseSensitiveProxyTags) + await ctx.Reply("Proxy tags are currently case **sensitive**."); + else + await ctx.Reply("Proxy tags are currently case **insensitive**."); + } - if (ctx.MatchToggle(true)) - { - await ctx.Repository.UpdateSystemConfig(ctx.System.Id, new() { CaseSensitiveProxyTags = true }); + public async Task EditCaseSensitiveProxyTags(Context ctx, bool value) + { + await ctx.Repository.UpdateSystemConfig(ctx.System.Id, new() { CaseSensitiveProxyTags = value }); + if (value) await ctx.Reply("Proxy tags are now case sensitive."); - } else - { - await ctx.Repository.UpdateSystemConfig(ctx.System.Id, new() { CaseSensitiveProxyTags = false }); - await ctx.Reply("Proxy tags are now case insensitive."); - } } - public async Task ProxyErrorMessageEnabled(Context ctx) + public async Task ViewProxyErrorMessageEnabled(Context ctx) { - if (!ctx.HasNext()) - { - if (ctx.Config.ProxyErrorMessageEnabled) { await ctx.Reply("Proxy error messages are currently **enabled**."); } - else { await ctx.Reply("Proxy error messages are currently **disabled**. Messages that fail to proxy (due to message or attachment size) will not throw an error message."); } - return; - } - - if (ctx.MatchToggle(true)) - { - await ctx.Repository.UpdateSystemConfig(ctx.System.Id, new() { ProxyErrorMessageEnabled = true }); - - await ctx.Reply("Proxy error messages are now enabled."); - } + if (ctx.Config.ProxyErrorMessageEnabled) + await ctx.Reply("Proxy error messages are currently **enabled**."); else - { - await ctx.Repository.UpdateSystemConfig(ctx.System.Id, new() { ProxyErrorMessageEnabled = false }); + await ctx.Reply("Proxy error messages are currently **disabled**. Messages that fail to proxy (due to message or attachment size) will not throw an error message."); + } + public async Task EditProxyErrorMessageEnabled(Context ctx, bool value) + { + await ctx.Repository.UpdateSystemConfig(ctx.System.Id, new() { ProxyErrorMessageEnabled = value }); + + if (value) + await ctx.Reply("Proxy error messages are now enabled."); + else await ctx.Reply("Proxy error messages are now disabled. Messages that fail to proxy (due to message or attachment size) will not throw an error message."); - } } - public async Task HidDisplaySplit(Context ctx) + public async Task ViewHidDisplaySplit(Context ctx) { - if (!ctx.HasNext()) - { - var msg = $"Splitting of 6-character IDs with a hyphen is currently **{EnabledDisabled(ctx.Config.HidDisplaySplit)}**."; - await ctx.Reply(msg); - return; - } - - var newVal = ctx.MatchToggle(false); - await ctx.Repository.UpdateSystemConfig(ctx.System.Id, new() { HidDisplaySplit = newVal }); - await ctx.Reply($"Splitting of 6-character IDs with a hyphen is now {EnabledDisabled(newVal)}."); + await ctx.Reply($"Splitting of 6-character IDs with a hyphen is currently **{EnabledDisabled(ctx.Config.HidDisplaySplit)}**."); } - public async Task HidDisplayCaps(Context ctx) + public async Task EditHidDisplaySplit(Context ctx, bool value) { - if (!ctx.HasNext()) - { - var msg = $"Displaying IDs as capital letters is currently **{EnabledDisabled(ctx.Config.HidDisplayCaps)}**."; - await ctx.Reply(msg); - return; - } - - var newVal = ctx.MatchToggle(false); - await ctx.Repository.UpdateSystemConfig(ctx.System.Id, new() { HidDisplayCaps = newVal }); - await ctx.Reply($"Displaying IDs as capital letters is now {EnabledDisabled(newVal)}."); + await ctx.Repository.UpdateSystemConfig(ctx.System.Id, new() { HidDisplaySplit = value }); + await ctx.Reply($"Splitting of 6-character IDs with a hyphen is now {EnabledDisabled(value)}."); } - public async Task HidListPadding(Context ctx) + public async Task ViewHidDisplayCaps(Context ctx) { - if (!ctx.HasNext()) - { - string message; - switch (ctx.Config.HidListPadding) - { - case SystemConfig.HidPadFormat.None: message = "Padding 5-character IDs in lists is currently disabled."; break; - case SystemConfig.HidPadFormat.Left: message = "5-character IDs displayed in lists will have a padding space added to the beginning."; break; - case SystemConfig.HidPadFormat.Right: message = "5-character IDs displayed in lists will have a padding space added to the end."; break; - default: throw new Exception("unreachable"); - } - await ctx.Reply(message); - return; - } + await ctx.Reply($"Displaying IDs as capital letters is currently **{EnabledDisabled(ctx.Config.HidDisplayCaps)}**."); + } + public async Task EditHidDisplayCaps(Context ctx, bool value) + { + await ctx.Repository.UpdateSystemConfig(ctx.System.Id, new() { HidDisplayCaps = value }); + await ctx.Reply($"Displaying IDs as capital letters is now {EnabledDisabled(value)}."); + } + + public async Task ViewHidListPadding(Context ctx) + { + string message = ctx.Config.HidListPadding switch + { + SystemConfig.HidPadFormat.None => "Padding 5-character IDs in lists is currently disabled.", + SystemConfig.HidPadFormat.Left => "5-character IDs displayed in lists will have a padding space added to the beginning.", + SystemConfig.HidPadFormat.Right => "5-character IDs displayed in lists will have a padding space added to the end.", + _ => throw new Exception("unreachable") + }; + await ctx.Reply(message); + } + + public async Task EditHidListPadding(Context ctx, string padding) + { var badInputError = "Valid padding settings are `left`, `right`, or `off`."; - var toggleOff = ctx.MatchToggleOrNull(false); - - switch (toggleOff) + if (padding.Equals("off", StringComparison.InvariantCultureIgnoreCase)) { - case true: throw new PKError(badInputError); - case false: - { - await ctx.Repository.UpdateSystemConfig(ctx.System.Id, new() { HidListPadding = SystemConfig.HidPadFormat.None }); - await ctx.Reply("Padding 5-character IDs in lists has been disabled."); - return; - } + await ctx.Repository.UpdateSystemConfig(ctx.System.Id, new() { HidListPadding = SystemConfig.HidPadFormat.None }); + await ctx.Reply("Padding 5-character IDs in lists has been disabled."); } - - if (ctx.Match("left", "l")) + else if (padding.Equals("left", StringComparison.InvariantCultureIgnoreCase) || padding.Equals("l", StringComparison.InvariantCultureIgnoreCase)) { await ctx.Repository.UpdateSystemConfig(ctx.System.Id, new() { HidListPadding = SystemConfig.HidPadFormat.Left }); await ctx.Reply("5-character IDs displayed in lists will now have a padding space added to the beginning."); } - else if (ctx.Match("right", "r")) + else if (padding.Equals("right", StringComparison.InvariantCultureIgnoreCase) || padding.Equals("r", StringComparison.InvariantCultureIgnoreCase)) { await ctx.Repository.UpdateSystemConfig(ctx.System.Id, new() { HidListPadding = SystemConfig.HidPadFormat.Right }); await ctx.Reply("5-character IDs displayed in lists will now have a padding space added to the end."); } - else throw new PKError(badInputError); + else + { + throw new PKError(badInputError); + } } - public async Task CardShowColorHex(Context ctx) + public async Task ViewCardShowColorHex(Context ctx) { - if (!ctx.HasNext()) - { - var msg = $"Showing color codes on system/member/group cards is currently **{EnabledDisabled(ctx.Config.CardShowColorHex)}**."; - await ctx.Reply(msg); - return; - } - - var newVal = ctx.MatchToggle(false); - await ctx.Repository.UpdateSystemConfig(ctx.System.Id, new() { CardShowColorHex = newVal }); - await ctx.Reply($"Showing color codes on system/member/group cards is now {EnabledDisabled(newVal)}."); + await ctx.Reply($"Showing color codes on system/member/group cards is currently **{EnabledDisabled(ctx.Config.CardShowColorHex)}**."); } - public async Task ProxySwitch(Context ctx) + public async Task EditCardShowColorHex(Context ctx, bool value) { - if (!ctx.HasNext()) + await ctx.Repository.UpdateSystemConfig(ctx.System.Id, new() { CardShowColorHex = value }); + await ctx.Reply($"Showing color codes on system/member/group cards is now {EnabledDisabled(value)}."); + } + + public async Task ViewProxySwitch(Context ctx) + { + string msg = ctx.Config.ProxySwitch switch { - string msg = ctx.Config.ProxySwitch switch - { - SystemConfig.ProxySwitchAction.Off => "Currently, when you proxy as a member, no switches are logged or changed.", - SystemConfig.ProxySwitchAction.New => "When you proxy as a member, currently it makes a new switch.", - SystemConfig.ProxySwitchAction.Add => "When you proxy as a member, currently it adds them to the current switch.", - _ => throw new Exception("unreachable"), - }; - await ctx.Reply(msg); - return; - } + SystemConfig.ProxySwitchAction.Off => "Currently, when you proxy as a member, no switches are logged or changed.", + SystemConfig.ProxySwitchAction.New => "When you proxy as a member, currently it makes a new switch.", + SystemConfig.ProxySwitchAction.Add => "When you proxy as a member, currently it adds them to the current switch.", + _ => throw new Exception("unreachable"), + }; + await ctx.Reply(msg); + } - // toggle = false means off, toggle = true means new, otherwise if they said add that means add or if they said new they mean new. If none of those, error - var toggle = ctx.MatchToggleOrNull(false); - var newVal = toggle == false ? SystemConfig.ProxySwitchAction.Off : toggle == true ? SystemConfig.ProxySwitchAction.New : ctx.Match("add", "a") ? SystemConfig.ProxySwitchAction.Add : ctx.Match("new", "n") ? SystemConfig.ProxySwitchAction.New : throw new PKError("You must pass either \"new\", \"add\", or \"off\" to this command."); - - await ctx.Repository.UpdateSystemConfig(ctx.System.Id, new() { ProxySwitch = newVal }); - switch (newVal) + public async Task EditProxySwitch(Context ctx, SystemConfig.ProxySwitchAction action) + { + await ctx.Repository.UpdateSystemConfig(ctx.System.Id, new() { ProxySwitch = action }); + switch (action) { case SystemConfig.ProxySwitchAction.Off: await ctx.Reply("Now when you proxy as a member, no switches are logged or changed."); break; case SystemConfig.ProxySwitchAction.New: await ctx.Reply("When you proxy as a member, it now makes a new switch."); break; @@ -625,65 +575,61 @@ public class Config } } - public async Task NameFormat(Context ctx) + public async Task ViewNameFormat(Context ctx) { - var clearFlag = ctx.MatchClear(); - if (!ctx.HasNext() && !clearFlag) - { - await ctx.Reply($"Member names are currently formatted as `{ctx.Config.NameFormat ?? ProxyMember.DefaultFormat}`"); - return; - } + await ctx.Reply($"Member names are currently formatted as `{ctx.Config.NameFormat ?? ProxyMember.DefaultFormat}`"); + } - string formatString; - if (clearFlag) - formatString = ProxyMember.DefaultFormat; - else - formatString = ctx.RemainderOrNull(); + public async Task ResetNameFormat(Context ctx) + { + await ctx.Repository.UpdateSystemConfig(ctx.System.Id, new() { NameFormat = ProxyMember.DefaultFormat }); + await ctx.Reply($"Member names are now formatted as `{ProxyMember.DefaultFormat}`"); + } + public async Task EditNameFormat(Context ctx, string formatString) + { await ctx.Repository.UpdateSystemConfig(ctx.System.Id, new() { NameFormat = formatString }); await ctx.Reply($"Member names are now formatted as `{formatString}`"); } - public async Task ServerNameFormat(Context ctx) + public async Task ViewServerNameFormat(Context ctx, ReplyFormat format) { ctx.CheckGuildContext(); - var clearFlag = ctx.MatchClear(); - var format = ctx.MatchFormat(); - // if there's nothing next or what's next is raw/plaintext and we're not clearing, it's a query - if ((!ctx.HasNext() || format != ReplyFormat.Standard) && !clearFlag) - { - var guildCfg = await ctx.Repository.GetSystemGuild(ctx.Guild.Id, ctx.System.Id); - if (guildCfg.NameFormat == null) - await ctx.Reply("You do not have a specific name format set for this server and member names are formatted with your global name format."); - else - switch (format) - { - case ReplyFormat.Raw: - await ctx.Reply($"`{guildCfg.NameFormat}`"); - break; - case ReplyFormat.Plaintext: - var eb = new EmbedBuilder() - .Description($"Showing guild Name Format for system {ctx.System.DisplayHid(ctx.Config)}"); - await ctx.Reply(guildCfg.NameFormat, eb.Build()); - break; - default: - await ctx.Reply($"Your member names in this server are currently formatted as `{guildCfg.NameFormat}`"); - break; - } - return; - } - - string? formatString = null; - if (!clearFlag) - { - formatString = ctx.RemainderOrNull(); - } - await ctx.Repository.UpdateSystemGuild(ctx.System.Id, ctx.Guild.Id, new() { NameFormat = formatString }); - if (formatString == null) - await ctx.Reply($"Member names are now formatted with your global name format in this server."); + var guildCfg = await ctx.Repository.GetSystemGuild(ctx.Guild.Id, ctx.System.Id); + if (guildCfg.NameFormat == null) + await ctx.Reply("You do not have a specific name format set for this server and member names are formatted with your global name format."); else - await ctx.Reply($"Member names are now formatted as `{formatString}` in this server."); + switch (format) + { + case ReplyFormat.Raw: + await ctx.Reply($"`{guildCfg.NameFormat}`"); + break; + case ReplyFormat.Plaintext: + var eb = new EmbedBuilder() + .Description($"Showing guild Name Format for system {ctx.System.DisplayHid(ctx.Config)}"); + await ctx.Reply(guildCfg.NameFormat, eb.Build()); + break; + default: + await ctx.Reply($"Your member names in this server are currently formatted as `{guildCfg.NameFormat}`"); + break; + } + } + + public async Task ResetServerNameFormat(Context ctx) + { + ctx.CheckGuildContext(); + + await ctx.Repository.UpdateSystemGuild(ctx.System.Id, ctx.Guild.Id, new() { NameFormat = null }); + await ctx.Reply($"Member names are now formatted with your global name format in this server."); + } + + public async Task EditServerNameFormat(Context ctx, string formatString) + { + ctx.CheckGuildContext(); + + await ctx.Repository.UpdateSystemGuild(ctx.System.Id, ctx.Guild.Id, new() { NameFormat = formatString }); + await ctx.Reply($"Member names are now formatted as `{formatString}` in this server."); } public Task LimitUpdate(Context ctx) diff --git a/PluralKit.Bot/Commands/SystemLink.cs b/PluralKit.Bot/Commands/SystemLink.cs index 8b4b1881..38a0cc47 100644 --- a/PluralKit.Bot/Commands/SystemLink.cs +++ b/PluralKit.Bot/Commands/SystemLink.cs @@ -1,4 +1,5 @@ using Myriad.Extensions; +using Myriad.Types; using PluralKit.Core; @@ -6,12 +7,10 @@ namespace PluralKit.Bot; public class SystemLink { - public async Task LinkSystem(Context ctx) + public async Task LinkSystem(Context ctx, User account) { ctx.CheckSystem(); - var account = await ctx.MatchUser() ?? - throw new PKSyntaxError("You must pass an account to link with (either ID or @mention)."); var accountIds = await ctx.Repository.GetSystemAccounts(ctx.System.Id); if (accountIds.Contains(account.Id)) throw Errors.AccountAlreadyLinked; diff --git a/crates/command_definitions/src/config.rs b/crates/command_definitions/src/config.rs index b6dca8f6..e8f4d625 100644 --- a/crates/command_definitions/src/config.rs +++ b/crates/command_definitions/src/config.rs @@ -3,13 +3,85 @@ use command_parser::parameter; use super::*; pub fn cmds() -> impl Iterator { - let cfg = ("config", ["cfg"]); + let cfg = ("config", ["cfg", "configure"]); let ap = tokens!(cfg, ("autoproxy", ["ap"])); let ap_account = tokens!(ap, ("account", ["ac"])); let ap_timeout = tokens!(ap, ("timeout", ["tm"])); + let timezone = tokens!(cfg, ("timezone", ["zone", "tz"])); + let ping = tokens!(cfg, ("ping", ["ping"])); + + let priv_ = ("private", ["priv"]); + let member_privacy = tokens!(cfg, priv_, ("member", ["mem"])); + let member_privacy_short = tokens!(cfg, ("mp", ["mp"])); + let group_privacy = tokens!(cfg, priv_, ("group", ["grp"])); + let group_privacy_short = tokens!(cfg, ("gp", ["gp"])); + + let show = ("show", ["show"]); + let show_private = tokens!(cfg, show, priv_); + let show_private_short = tokens!(cfg, ("sp", ["sp"])); + + let proxy = ("proxy", ["px"]); + let proxy_case = tokens!(cfg, proxy, ("case", ["caps", "capitalize", "capitalise"])); + let proxy_error = tokens!(cfg, proxy, ("error", ["errors"])); + let proxy_error_short = tokens!(cfg, ("pe", ["pe"])); + + let id = ("id", ["ids"]); + let split_id = tokens!(cfg, ("split", ["split"]), id); + let split_id_short = tokens!(cfg, ("sid", ["sid", "sids"])); + let cap_id = tokens!(cfg, ("cap", ["caps", "capitalize", "capitalise"]), id); + let cap_id_short = tokens!(cfg, ("capid", ["capid", "capids"])); + + let pad = ("pad", ["padding"]); + let pad_id = tokens!(cfg, pad, id); + let id_pad = tokens!(cfg, id, pad); + let id_pad_short = tokens!(cfg, ("idpad", ["idpad", "padid", "padids"])); + + let show_color = tokens!(cfg, show, ("color", ["colour", "colors", "colours"])); + let show_color_short = tokens!( + cfg, + ( + "showcolor", + [ + "showcolour", + "showcolors", + "showcolours", + "colorcode", + "colorhex" + ] + ) + ); + + let proxy_switch = tokens!(cfg, ("proxy", ["proxy"]), ("switch", ["switch"])); + let proxy_switch_short = tokens!(cfg, ("proxyswitch", ["proxyswitch", "ps"])); + + let format = ("format", ["format"]); + let name_format = tokens!(cfg, ("name", ["name"]), format); + let name_format_short = tokens!(cfg, ("nameformat", ["nameformat", "nf"])); + + let server = ("server", ["server"]); + let server_name_format = tokens!(cfg, server, ("name", ["name"]), format); + let server_format = tokens!( + cfg, + ("server", ["server", "servername"]), + ("format", ["format", "nameformat", "nf"]) + ); + let server_format_short = tokens!( + cfg, + ( + "snf", + ["snf", "servernf", "servernameformat", "snameformat"] + ) + ); + + let limit_ = ("limit", ["limit", "lim"]); + let member_limit = tokens!(cfg, ("member", ["mem"]), limit_); + let group_limit = tokens!(cfg, ("group", ["grp"]), limit_); + let limit = tokens!(cfg, limit_); + [ + command!(cfg => "cfg_show").help("Shows the current configuration"), command!(ap_account => "cfg_ap_account_show") .help("Shows autoproxy status for the account"), command!(ap_account, Toggle => "cfg_ap_account_update") @@ -20,6 +92,115 @@ pub fn cmds() -> impl Iterator { .help("Disables the autoproxy timeout"), command!(ap_timeout, ("timeout", OpaqueString) => "cfg_ap_timeout_update") .help("Sets the autoproxy timeout"), + command!(timezone => "cfg_timezone_show").help("Shows the system timezone"), + command!(timezone, RESET => "cfg_timezone_reset").help("Resets the system timezone"), + command!(timezone, ("timezone", OpaqueString) => "cfg_timezone_update") + .help("Sets the system timezone"), + command!(ping => "cfg_ping_show").help("Shows the ping setting"), + command!(ping, Toggle => "cfg_ping_update").help("Updates the ping setting"), + command!(member_privacy => "cfg_member_privacy_show") + .help("Shows the default privacy for new members"), + command!(member_privacy, Toggle => "cfg_member_privacy_update") + .help("Sets the default privacy for new members"), + command!(member_privacy_short => "cfg_member_privacy_show") + .help("Shows the default privacy for new members"), + command!(member_privacy_short, Toggle => "cfg_member_privacy_update") + .help("Sets the default privacy for new members"), + command!(group_privacy => "cfg_group_privacy_show") + .help("Shows the default privacy for new groups"), + command!(group_privacy, Toggle => "cfg_group_privacy_update") + .help("Sets the default privacy for new groups"), + command!(group_privacy_short => "cfg_group_privacy_show") + .help("Shows the default privacy for new groups"), + command!(group_privacy_short, Toggle => "cfg_group_privacy_update") + .help("Sets the default privacy for new groups"), + command!(show_private => "cfg_show_private_info_show") + .help("Shows whether private info is shown"), + command!(show_private, Toggle => "cfg_show_private_info_update") + .help("Toggles showing private info"), + command!(show_private_short => "cfg_show_private_info_show") + .help("Shows whether private info is shown"), + command!(show_private_short, Toggle => "cfg_show_private_info_update") + .help("Toggles showing private info"), + command!(proxy_case => "cfg_case_sensitive_proxy_tags_show") + .help("Shows whether proxy tags are case-sensitive"), + command!(proxy_case, Toggle => "cfg_case_sensitive_proxy_tags_update") + .help("Toggles case sensitivity for proxy tags"), + command!(proxy_error => "cfg_proxy_error_message_show") + .help("Shows whether proxy error messages are enabled"), + command!(proxy_error, Toggle => "cfg_proxy_error_message_update") + .help("Toggles proxy error messages"), + command!(proxy_error_short => "cfg_proxy_error_message_show") + .help("Shows whether proxy error messages are enabled"), + command!(proxy_error_short, Toggle => "cfg_proxy_error_message_update") + .help("Toggles proxy error messages"), + command!(split_id => "cfg_hid_split_show").help("Shows whether IDs are split in lists"), + command!(split_id, Toggle => "cfg_hid_split_update").help("Toggles splitting IDs in lists"), + command!(split_id_short => "cfg_hid_split_show") + .help("Shows whether IDs are split in lists"), + command!(split_id_short, Toggle => "cfg_hid_split_update") + .help("Toggles splitting IDs in lists"), + command!(cap_id => "cfg_hid_caps_show").help("Shows whether IDs are capitalized in lists"), + command!(cap_id, Toggle => "cfg_hid_caps_update") + .help("Toggles capitalization of IDs in lists"), + command!(cap_id_short => "cfg_hid_caps_show") + .help("Shows whether IDs are capitalized in lists"), + command!(cap_id_short, Toggle => "cfg_hid_caps_update") + .help("Toggles capitalization of IDs in lists"), + command!(pad_id => "cfg_hid_padding_show").help("Shows the ID padding for lists"), + command!(pad_id, ("padding", OpaqueString) => "cfg_hid_padding_update") + .help("Sets the ID padding for lists"), + command!(id_pad => "cfg_hid_padding_show").help("Shows the ID padding for lists"), + command!(id_pad, ("padding", OpaqueString) => "cfg_hid_padding_update") + .help("Sets the ID padding for lists"), + command!(id_pad_short => "cfg_hid_padding_show").help("Shows the ID padding for lists"), + command!(id_pad_short, ("padding", OpaqueString) => "cfg_hid_padding_update") + .help("Sets the ID padding for lists"), + command!(show_color => "cfg_card_show_color_hex_show") + .help("Shows whether color hex codes are shown on cards"), + command!(show_color, Toggle => "cfg_card_show_color_hex_update") + .help("Toggles showing color hex codes on cards"), + command!(show_color_short => "cfg_card_show_color_hex_show") + .help("Shows whether color hex codes are shown on cards"), + command!(show_color_short, Toggle => "cfg_card_show_color_hex_update") + .help("Toggles showing color hex codes on cards"), + command!(proxy_switch => "cfg_proxy_switch_show").help("Shows the proxy switch behavior"), + command!(proxy_switch, ProxySwitchAction => "cfg_proxy_switch_update") + .help("Sets the proxy switch behavior"), + command!(proxy_switch_short => "cfg_proxy_switch_show") + .help("Shows the proxy switch behavior"), + command!(proxy_switch_short, ProxySwitchAction => "cfg_proxy_switch_update") + .help("Sets the proxy switch behavior"), + command!(name_format => "cfg_name_format_show").help("Shows the name format"), + command!(name_format, RESET => "cfg_name_format_reset").help("Resets the name format"), + command!(name_format, ("format", OpaqueString) => "cfg_name_format_update") + .help("Sets the name format"), + command!(name_format_short => "cfg_name_format_show").help("Shows the name format"), + command!(name_format_short, RESET => "cfg_name_format_reset") + .help("Resets the name format"), + command!(name_format_short, ("format", OpaqueString) => "cfg_name_format_update") + .help("Sets the name format"), + command!(server_name_format => "cfg_server_name_format_show") + .help("Shows the server name format"), + command!(server_name_format, RESET => "cfg_server_name_format_reset") + .help("Resets the server name format"), + command!(server_name_format, ("format", OpaqueString) => "cfg_server_name_format_update") + .help("Sets the server name format"), + command!(server_format => "cfg_server_name_format_show") + .help("Shows the server name format"), + command!(server_format, RESET => "cfg_server_name_format_reset") + .help("Resets the server name format"), + command!(server_format, ("format", OpaqueString) => "cfg_server_name_format_update") + .help("Sets the server name format"), + command!(server_format_short => "cfg_server_name_format_show") + .help("Shows the server name format"), + command!(server_format_short, RESET => "cfg_server_name_format_reset") + .help("Resets the server name format"), + command!(server_format_short, ("format", OpaqueString) => "cfg_server_name_format_update") + .help("Sets the server name format"), + command!(member_limit => "cfg_limits_update").help("Refreshes member/group limits"), + command!(group_limit => "cfg_limits_update").help("Refreshes member/group limits"), + command!(limit => "cfg_limits_update").help("Refreshes member/group limits"), ] .into_iter() } diff --git a/crates/command_definitions/src/system.rs b/crates/command_definitions/src/system.rs index 88dbb39b..4e12ceb9 100644 --- a/crates/command_definitions/src/system.rs +++ b/crates/command_definitions/src/system.rs @@ -252,8 +252,8 @@ pub fn edit() -> impl Iterator { .into_iter(); let system_link = [ - command!("link" => "system_link"), - command!("unlink", ("target", OpaqueString) => "system_unlink"), + command!("link", ("account", UserRef) => "system_link"), + command!("unlink", ("account", OpaqueString) => "system_unlink"), ] .into_iter(); diff --git a/crates/command_parser/src/parameter.rs b/crates/command_parser/src/parameter.rs index 04d035ea..77bfde8b 100644 --- a/crates/command_parser/src/parameter.rs +++ b/crates/command_parser/src/parameter.rs @@ -27,6 +27,7 @@ pub enum ParameterValue { PrivacyLevel(String), Toggle(bool), Avatar(String), + ProxySwitchAction(ProxySwitchAction), Null, } @@ -179,6 +180,9 @@ impl Parameter { .parse::() .map(ParameterValue::GuildRef) .map_err(|_| SmolStr::new("invalid guild ID")), + ParameterKind::ProxySwitchAction => ProxySwitchAction::from_str(input) + .map(ParameterValue::ProxySwitchAction) + .map_err(|_| SmolStr::new("invalid proxy switch action, must be new/add/off")), } } } @@ -208,8 +212,9 @@ impl Display for Parameter { ParameterKind::GroupPrivacyTarget => write!(f, ""), ParameterKind::SystemPrivacyTarget => write!(f, ""), ParameterKind::PrivacyLevel => write!(f, "[privacy level]"), - ParameterKind::Toggle => write!(f, "on/off"), + ParameterKind::Toggle => write!(f, ""), ParameterKind::Avatar => write!(f, ""), + ParameterKind::ProxySwitchAction => write!(f, ""), } } } @@ -290,6 +295,7 @@ pub enum ParameterKind { PrivacyLevel, Toggle, Avatar, + ProxySwitchAction, } impl ParameterKind { @@ -313,6 +319,7 @@ impl ParameterKind { ParameterKind::PrivacyLevel => "privacy_level", ParameterKind::Toggle => "toggle", ParameterKind::Avatar => "avatar", + ParameterKind::ProxySwitchAction => "proxy_switch_action", } } } @@ -521,3 +528,35 @@ impl Into for Toggle { } } } + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum ProxySwitchAction { + New, + Add, + Off, +} + +impl AsRef for ProxySwitchAction { + fn as_ref(&self) -> &str { + match self { + ProxySwitchAction::New => "new", + ProxySwitchAction::Add => "add", + ProxySwitchAction::Off => "off", + } + } +} + +impl FromStr for ProxySwitchAction { + type Err = SmolStr; + + fn from_str(s: &str) -> Result { + [ + ProxySwitchAction::New, + ProxySwitchAction::Add, + ProxySwitchAction::Off, + ] + .into_iter() + .find(|action| action.as_ref() == s) + .ok_or_else(|| SmolStr::new("invalid proxy switch action, must be new/add/off")) + } +} diff --git a/crates/commands/src/bin/write_cs_glue.rs b/crates/commands/src/bin/write_cs_glue.rs index f94aed77..d6486a71 100644 --- a/crates/commands/src/bin/write_cs_glue.rs +++ b/crates/commands/src/bin/write_cs_glue.rs @@ -276,6 +276,7 @@ fn get_param_ty(kind: ParameterKind) -> &'static str { ParameterKind::MessageRef => "Message.Reference", ParameterKind::ChannelRef => "Channel", ParameterKind::GuildRef => "Guild", + ParameterKind::ProxySwitchAction => "SystemConfig.ProxySwitchAction", } } @@ -298,6 +299,7 @@ fn get_param_param_ty(kind: ParameterKind) -> &'static str { ParameterKind::MessageRef => "Message", ParameterKind::ChannelRef => "Channel", ParameterKind::GuildRef => "Guild", + ParameterKind::ProxySwitchAction => "ProxySwitchAction", } } diff --git a/crates/commands/src/commands.udl b/crates/commands/src/commands.udl index 7fd3b312..f531f870 100644 --- a/crates/commands/src/commands.udl +++ b/crates/commands/src/commands.udl @@ -25,6 +25,7 @@ interface Parameter { OpaqueInt(i32 raw); Toggle(boolean toggle); Avatar(string avatar); + ProxySwitchAction(string action); Null(); }; dictionary ParsedCommand { diff --git a/crates/commands/src/lib.rs b/crates/commands/src/lib.rs index b91cf137..c78b0c69 100644 --- a/crates/commands/src/lib.rs +++ b/crates/commands/src/lib.rs @@ -75,6 +75,9 @@ pub enum Parameter { Avatar { avatar: String, }, + ProxySwitchAction { + action: String, + }, Null, } @@ -103,6 +106,9 @@ impl From for Parameter { ParameterValue::ChannelRef(channel_id) => Self::ChannelRef { channel_id }, ParameterValue::GuildRef(guild_id) => Self::GuildRef { guild_id }, ParameterValue::Null => Self::Null, + ParameterValue::ProxySwitchAction(action) => Self::ProxySwitchAction { + action: action.as_ref().to_string(), + }, } } } From 1627e25268c87963d9636395a5737516fdba3137 Mon Sep 17 00:00:00 2001 From: dusk Date: Tue, 7 Oct 2025 14:27:21 +0000 Subject: [PATCH 110/179] implement server config commands --- PluralKit.Bot/CommandMeta/CommandTree.cs | 81 +---- PluralKit.Bot/Commands/ServerConfig.cs | 330 ++++++++++-------- crates/command_definitions/src/lib.rs | 2 + crates/command_definitions/src/misc.rs | 4 + .../command_definitions/src/server_config.rs | 148 ++++++++ 5 files changed, 350 insertions(+), 215 deletions(-) diff --git a/PluralKit.Bot/CommandMeta/CommandTree.cs b/PluralKit.Bot/CommandMeta/CommandTree.cs index 000c3d27..1aefe854 100644 --- a/PluralKit.Bot/CommandMeta/CommandTree.cs +++ b/PluralKit.Bot/CommandMeta/CommandTree.cs @@ -281,6 +281,24 @@ public partial class CommandTree Commands.MessageReproxy(var param, _) => ctx.Execute(MessageReproxy, m => m.ReproxyMessage(ctx, param.msg.MessageId, param.member)), Commands.Import(var param, _) => ctx.Execute(Import, m => m.Import(ctx, param.url)), Commands.Export(_, _) => ctx.Execute(Export, m => m.Export(ctx)), + Commands.ServerConfigShow => ctx.Execute(null, m => m.ShowConfig(ctx)), + Commands.ServerConfigLogChannelShow => ctx.Execute(null, m => m.ShowLogChannel(ctx)), + Commands.ServerConfigLogChannelSet(var param, _) => ctx.Execute(null, m => m.SetLogChannel(ctx, param.channel)), + Commands.ServerConfigLogChannelClear => ctx.Execute(null, m => m.ClearLogChannel(ctx)), + Commands.ServerConfigLogCleanupShow => ctx.Execute(null, m => m.ShowLogCleanup(ctx)), + Commands.ServerConfigLogCleanupSet(var param, _) => ctx.Execute(null, m => m.SetLogCleanup(ctx, param.toggle)), + Commands.ServerConfigLogBlacklistShow => ctx.Execute(null, m => m.ShowLogBlacklist(ctx)), + Commands.ServerConfigLogBlacklistAdd(var param, var flags) => ctx.Execute(null, m => m.AddLogBlacklist(ctx, param.channel, flags.all)), + Commands.ServerConfigLogBlacklistRemove(var param, var flags) => ctx.Execute(null, m => m.RemoveLogBlacklist(ctx, param.channel, flags.all)), + Commands.ServerConfigProxyBlacklistShow => ctx.Execute(null, m => m.ShowProxyBlacklist(ctx)), + Commands.ServerConfigProxyBlacklistAdd(var param, var flags) => ctx.Execute(null, m => m.AddProxyBlacklist(ctx, param.channel, flags.all)), + Commands.ServerConfigProxyBlacklistRemove(var param, var flags) => ctx.Execute(null, m => m.RemoveProxyBlacklist(ctx, param.channel, flags.all)), + Commands.ServerConfigInvalidCommandResponseShow => ctx.Execute(null, m => m.ShowInvalidCommandResponse(ctx)), + Commands.ServerConfigInvalidCommandResponseSet(var param, _) => ctx.Execute(null, m => m.SetInvalidCommandResponse(ctx, param.toggle)), + Commands.ServerConfigRequireSystemTagShow => ctx.Execute(null, m => m.ShowRequireSystemTag(ctx)), + Commands.ServerConfigRequireSystemTagSet(var param, _) => ctx.Execute(null, m => m.SetRequireSystemTag(ctx, param.toggle)), + Commands.ServerConfigSuppressNotificationsShow => ctx.Execute(null, m => m.ShowSuppressNotifications(ctx)), + Commands.ServerConfigSuppressNotificationsSet(var param, _) => ctx.Execute(null, m => m.SetSuppressNotifications(ctx, param.toggle)), Commands.AdminUpdateSystemId(var param, _) => ctx.Execute(null, m => m.UpdateSystemId(ctx, param.target, param.new_hid)), Commands.AdminUpdateMemberId(var param, _) => ctx.Execute(null, m => m.UpdateMemberId(ctx, param.target, param.new_hid)), Commands.AdminUpdateGroupId(var param, _) => ctx.Execute(null, m => m.UpdateGroupId(ctx, param.target, param.new_hid)), @@ -310,32 +328,9 @@ public partial class CommandTree ctx.Reply( $"{Emojis.Error} Parsed command {ctx.Parameters.Callback().AsCode()} not implemented in PluralKit.Bot!"), }; + // Legacy command routing - these are kept for backwards compatibility until fully migrated to new system if (ctx.Match("commands", "cmd", "c")) return CommandHelpRoot(ctx); - if (ctx.Match("serverconfig", "guildconfig", "scfg")) - return HandleServerConfigCommand(ctx); - if (ctx.Match("log")) - if (ctx.Match("channel")) - return ctx.Execute(LogChannel, m => m.SetLogChannel(ctx), true); - else if (ctx.Match("enable", "on")) - return ctx.Execute(LogEnable, m => m.SetLogEnabled(ctx, true), true); - else if (ctx.Match("disable", "off")) - return ctx.Execute(LogDisable, m => m.SetLogEnabled(ctx, false), true); - else if (ctx.Match("list", "show")) - return ctx.Execute(LogShow, m => m.ShowLogDisabledChannels(ctx), true); - else - return ctx.Reply($"{Emojis.Warn} Message logging commands have moved to `{ctx.DefaultPrefix}serverconfig`."); - if (ctx.Match("logclean")) - return ctx.Execute(ServerConfigLogClean, m => m.SetLogCleanup(ctx), true); - if (ctx.Match("blacklist", "bl")) - if (ctx.Match("enable", "on", "add", "deny")) - return ctx.Execute(BlacklistAdd, m => m.SetProxyBlacklisted(ctx, true), true); - else if (ctx.Match("disable", "off", "remove", "allow")) - return ctx.Execute(BlacklistRemove, m => m.SetProxyBlacklisted(ctx, false), true); - else if (ctx.Match("list", "show")) - return ctx.Execute(BlacklistShow, m => m.ShowProxyBlacklisted(ctx), true); - else - return ctx.Reply($"{Emojis.Warn} Blacklist commands have moved to `{ctx.DefaultPrefix}serverconfig`."); if (ctx.Match("invite")) return ctx.Execute(Invite, m => m.Invite(ctx)); if (ctx.Match("stats", "status")) return ctx.Execute(null, m => m.Stats(ctx)); } @@ -401,42 +396,4 @@ public partial class CommandTree break; } } - - private Task HandleServerConfigCommand(Context ctx) - { - if (!ctx.HasNext()) - return ctx.Execute(null, m => m.ShowConfig(ctx)); - - if (ctx.MatchMultiple(new[] { "log" }, new[] { "cleanup", "clean" }) || ctx.Match("logclean")) - return ctx.Execute(null, m => m.SetLogCleanup(ctx)); - if (ctx.MatchMultiple(new[] { "invalid", "unknown" }, new[] { "command" }, new[] { "error", "response" }) || ctx.Match("invalidcommanderror", "unknowncommanderror")) - return ctx.Execute(null, m => m.InvalidCommandResponse(ctx)); - if (ctx.MatchMultiple(new[] { "require", "enforce" }, new[] { "tag", "systemtag" }) || ctx.Match("requiretag", "enforcetag")) - return ctx.Execute(null, m => m.RequireSystemTag(ctx)); - if (ctx.MatchMultiple(new[] { "suppress" }, new[] { "notifications" }) || ctx.Match("proxysilent")) - return ctx.Execute(null, m => m.SuppressNotifications(ctx)); - if (ctx.MatchMultiple(new[] { "log" }, new[] { "channel" })) - return ctx.Execute(null, m => m.SetLogChannel(ctx)); - if (ctx.MatchMultiple(new[] { "log" }, new[] { "blacklist" })) - { - if (ctx.Match("enable", "on", "add", "deny")) - return ctx.Execute(null, m => m.SetLogBlacklisted(ctx, true)); - else if (ctx.Match("disable", "off", "remove", "allow")) - return ctx.Execute(null, m => m.SetLogBlacklisted(ctx, false)); - else - return ctx.Execute(null, m => m.ShowLogDisabledChannels(ctx)); - } - if (ctx.MatchMultiple(new[] { "proxy", "proxying" }, new[] { "blacklist" })) - { - if (ctx.Match("enable", "on", "add", "deny")) - return ctx.Execute(null, m => m.SetProxyBlacklisted(ctx, true)); - else if (ctx.Match("disable", "off", "remove", "allow")) - return ctx.Execute(null, m => m.SetProxyBlacklisted(ctx, false)); - else - return ctx.Execute(null, m => m.ShowProxyBlacklisted(ctx)); - } - - // todo: maybe add the list of configuration keys here? - return ctx.Reply($"{Emojis.Error} Could not find a setting with that name. Please see `{ctx.DefaultPrefix}commands serverconfig` for the list of possible config settings."); - } } \ No newline at end of file diff --git a/PluralKit.Bot/Commands/ServerConfig.cs b/PluralKit.Bot/Commands/ServerConfig.cs index ef648438..e7b24a3c 100644 --- a/PluralKit.Bot/Commands/ServerConfig.cs +++ b/PluralKit.Bot/Commands/ServerConfig.cs @@ -110,34 +110,27 @@ public class ServerConfig ); } - public async Task SetLogChannel(Context ctx) + public async Task ShowLogChannel(Context ctx) { await ctx.CheckGuildContext().CheckAuthorPermission(PermissionSet.ManageGuild, "Manage Server"); var settings = await ctx.Repository.GetGuild(ctx.Guild.Id); - if (ctx.MatchClear() && await ctx.ConfirmClear("the server log channel")) + if (settings.LogChannel == null) { - await ctx.Repository.UpdateGuild(ctx.Guild.Id, new GuildPatch { LogChannel = null }); - await ctx.Reply($"{Emojis.Success} Proxy logging channel cleared."); + await ctx.Reply("This server does not have a log channel set."); return; } - if (!ctx.HasNext()) - { - if (settings.LogChannel == null) - { - await ctx.Reply("This server does not have a log channel set."); - return; - } + await ctx.Reply($"This server's log channel is currently set to <#{settings.LogChannel}>."); + } - await ctx.Reply($"This server's log channel is currently set to <#{settings.LogChannel}>."); - return; - } + public async Task SetLogChannel(Context ctx, Channel channel) + { + await ctx.CheckGuildContext().CheckAuthorPermission(PermissionSet.ManageGuild, "Manage Server"); + + if (channel.GuildId != ctx.Guild.Id) + throw Errors.ChannelNotFound(channel.Id.ToString()); - Channel channel = null; - var channelString = ctx.PeekArgument(); - channel = await ctx.MatchChannel(); - if (channel == null || channel.GuildId != ctx.Guild.Id) throw Errors.ChannelNotFound(channelString); if (channel.Type != Channel.ChannelType.GuildText && channel.Type != Channel.ChannelType.GuildPublicThread && channel.Type != Channel.ChannelType.GuildPrivateThread) throw new PKError("PluralKit cannot log messages to this type of channel."); @@ -151,46 +144,18 @@ public class ServerConfig await ctx.Reply($"{Emojis.Success} Proxy logging channel set to <#{channel.Id}>."); } - // legacy behaviour: enable/disable logging for commands - // new behaviour is add/remove from log blacklist (see #LogBlacklistNew) - public async Task SetLogEnabled(Context ctx, bool enable) + public async Task ClearLogChannel(Context ctx) { await ctx.CheckGuildContext().CheckAuthorPermission(PermissionSet.ManageGuild, "Manage Server"); - var affectedChannels = new List(); - if (ctx.Match("all")) - affectedChannels = (await _cache.GetGuildChannels(ctx.Guild.Id)) - .Where(x => x.Type == Channel.ChannelType.GuildText).ToList(); - else if (!ctx.HasNext()) throw new PKSyntaxError("You must pass one or more #channels."); - else - while (ctx.HasNext()) - { - var channelString = ctx.PeekArgument(); - var channel = await ctx.MatchChannel(); - if (channel == null || channel.GuildId != ctx.Guild.Id) throw Errors.ChannelNotFound(channelString); - affectedChannels.Add(channel); - } + if (!await ctx.ConfirmClear("the server log channel")) + return; - ulong? logChannel = null; - var config = await ctx.Repository.GetGuild(ctx.Guild.Id); - logChannel = config.LogChannel; - - var blacklist = config.LogBlacklist.ToHashSet(); - if (enable) - blacklist.ExceptWith(affectedChannels.Select(c => c.Id)); - else - blacklist.UnionWith(affectedChannels.Select(c => c.Id)); - - await ctx.Repository.UpdateGuild(ctx.Guild.Id, new GuildPatch { LogBlacklist = blacklist.ToArray() }); - - await ctx.Reply( - $"{Emojis.Success} Message logging for the given channels {(enable ? "enabled" : "disabled")}." + - (logChannel == null - ? $"\n{Emojis.Warn} Please note that no logging channel is set, so there is nowhere to log messages to. You can set a logging channel using `{ctx.DefaultPrefix}serverconfig log channel #your-log-channel`." - : "")); + await ctx.Repository.UpdateGuild(ctx.Guild.Id, new GuildPatch { LogChannel = null }); + await ctx.Reply($"{Emojis.Success} Proxy logging channel cleared."); } - public async Task ShowProxyBlacklisted(Context ctx) + public async Task ShowProxyBlacklist(Context ctx) { await ctx.CheckGuildContext().CheckAuthorPermission(PermissionSet.ManageGuild, "Manage Server"); @@ -240,14 +205,73 @@ public class ServerConfig }); } - public async Task ShowLogDisabledChannels(Context ctx) + public async Task AddProxyBlacklist(Context ctx, Channel? channel, bool all) + { + await ctx.CheckGuildContext().CheckAuthorPermission(PermissionSet.ManageGuild, "Manage Server"); + + var affectedChannels = new List(); + if (all) + { + affectedChannels = (await _cache.GetGuildChannels(ctx.Guild.Id)) + .Where(x => DiscordUtils.IsValidGuildChannel(x)).ToList(); + } + else if (channel != null) + { + if (channel.GuildId != ctx.Guild.Id) + throw Errors.ChannelNotFound(channel.Id.ToString()); + affectedChannels.Add(channel); + } + else + { + throw new PKSyntaxError("You must specify a channel or use the --all flag."); + } + + var guild = await ctx.Repository.GetGuild(ctx.Guild.Id); + var blacklist = guild.Blacklist.ToHashSet(); + blacklist.UnionWith(affectedChannels.Select(c => c.Id)); + + await ctx.Repository.UpdateGuild(ctx.Guild.Id, new GuildPatch { Blacklist = blacklist.ToArray() }); + + await ctx.Reply($"{Emojis.Success} {(all ? "All channels" : "Channel")} added to the proxy blacklist."); + } + + public async Task RemoveProxyBlacklist(Context ctx, Channel? channel, bool all) + { + await ctx.CheckGuildContext().CheckAuthorPermission(PermissionSet.ManageGuild, "Manage Server"); + + var affectedChannels = new List(); + if (all) + { + affectedChannels = (await _cache.GetGuildChannels(ctx.Guild.Id)) + .Where(x => DiscordUtils.IsValidGuildChannel(x)).ToList(); + } + else if (channel != null) + { + if (channel.GuildId != ctx.Guild.Id) + throw Errors.ChannelNotFound(channel.Id.ToString()); + affectedChannels.Add(channel); + } + else + { + throw new PKSyntaxError("You must specify a channel or use the --all flag."); + } + + var guild = await ctx.Repository.GetGuild(ctx.Guild.Id); + var blacklist = guild.Blacklist.ToHashSet(); + blacklist.ExceptWith(affectedChannels.Select(c => c.Id)); + + await ctx.Repository.UpdateGuild(ctx.Guild.Id, new GuildPatch { Blacklist = blacklist.ToArray() }); + + await ctx.Reply($"{Emojis.Success} {(all ? "All channels" : "Channel")} removed from the proxy blacklist."); + } + + public async Task ShowLogBlacklist(Context ctx) { await ctx.CheckGuildContext().CheckAuthorPermission(PermissionSet.ManageGuild, "Manage Server"); var config = await ctx.Repository.GetGuild(ctx.Guild.Id); // Resolve all channels from the cache and order by position - // todo: GetAllChannels? var channels = (await Task.WhenAll(config.LogBlacklist .Select(id => _cache.TryGetChannel(ctx.Guild.Id, id)))) .Where(c => c != null) @@ -291,78 +315,75 @@ public class ServerConfig }); } - - - public async Task SetProxyBlacklisted(Context ctx, bool shouldAdd) + public async Task AddLogBlacklist(Context ctx, Channel? channel, bool all) { await ctx.CheckGuildContext().CheckAuthorPermission(PermissionSet.ManageGuild, "Manage Server"); var affectedChannels = new List(); - if (ctx.Match("all")) + if (all) + { affectedChannels = (await _cache.GetGuildChannels(ctx.Guild.Id)) - // All the channel types you can proxy in .Where(x => DiscordUtils.IsValidGuildChannel(x)).ToList(); - else if (!ctx.HasNext()) throw new PKSyntaxError("You must pass one or more #channels."); + } + else if (channel != null) + { + if (channel.GuildId != ctx.Guild.Id) + throw Errors.ChannelNotFound(channel.Id.ToString()); + affectedChannels.Add(channel); + } else - while (ctx.HasNext()) - { - var channelString = ctx.PeekArgument(); - var channel = await ctx.MatchChannel(); - if (channel == null || channel.GuildId != ctx.Guild.Id) throw Errors.ChannelNotFound(channelString); - affectedChannels.Add(channel); - } + { + throw new PKSyntaxError("You must specify a channel or use the --all flag."); + } var guild = await ctx.Repository.GetGuild(ctx.Guild.Id); - - var blacklist = guild.Blacklist.ToHashSet(); - if (shouldAdd) - blacklist.UnionWith(affectedChannels.Select(c => c.Id)); - else - blacklist.ExceptWith(affectedChannels.Select(c => c.Id)); - - await ctx.Repository.UpdateGuild(ctx.Guild.Id, new GuildPatch { Blacklist = blacklist.ToArray() }); - - await ctx.Reply( - $"{Emojis.Success} Channels {(shouldAdd ? "added to" : "removed from")} the proxy blacklist."); - } - - public async Task SetLogBlacklisted(Context ctx, bool shouldAdd) - { - await ctx.CheckGuildContext().CheckAuthorPermission(PermissionSet.ManageGuild, "Manage Server"); - - var affectedChannels = new List(); - if (ctx.Match("all")) - affectedChannels = (await _cache.GetGuildChannels(ctx.Guild.Id)) - // All the channel types you can proxy in - .Where(x => DiscordUtils.IsValidGuildChannel(x)).ToList(); - else if (!ctx.HasNext()) throw new PKSyntaxError("You must pass one or more #channels."); - else - while (ctx.HasNext()) - { - var channelString = ctx.PeekArgument(); - var channel = await ctx.MatchChannel(); - if (channel == null || channel.GuildId != ctx.Guild.Id) throw Errors.ChannelNotFound(channelString); - affectedChannels.Add(channel); - } - - var guild = await ctx.Repository.GetGuild(ctx.Guild.Id); - var blacklist = guild.LogBlacklist.ToHashSet(); - if (shouldAdd) - blacklist.UnionWith(affectedChannels.Select(c => c.Id)); - else - blacklist.ExceptWith(affectedChannels.Select(c => c.Id)); + blacklist.UnionWith(affectedChannels.Select(c => c.Id)); await ctx.Repository.UpdateGuild(ctx.Guild.Id, new GuildPatch { LogBlacklist = blacklist.ToArray() }); await ctx.Reply( - $"{Emojis.Success} Channels {(shouldAdd ? "added to" : "removed from")} the logging blacklist." + + $"{Emojis.Success} {(all ? "All channels" : "Channel")} added to the logging blacklist." + (guild.LogChannel == null ? $"\n{Emojis.Warn} Please note that no logging channel is set, so there is nowhere to log messages to. You can set a logging channel using `{ctx.DefaultPrefix}serverconfig log channel #your-log-channel`." : "")); } - public async Task SetLogCleanup(Context ctx) + public async Task RemoveLogBlacklist(Context ctx, Channel? channel, bool all) + { + await ctx.CheckGuildContext().CheckAuthorPermission(PermissionSet.ManageGuild, "Manage Server"); + + var affectedChannels = new List(); + if (all) + { + affectedChannels = (await _cache.GetGuildChannels(ctx.Guild.Id)) + .Where(x => DiscordUtils.IsValidGuildChannel(x)).ToList(); + } + else if (channel != null) + { + if (channel.GuildId != ctx.Guild.Id) + throw Errors.ChannelNotFound(channel.Id.ToString()); + affectedChannels.Add(channel); + } + else + { + throw new PKSyntaxError("You must specify a channel or use the --all flag."); + } + + var guild = await ctx.Repository.GetGuild(ctx.Guild.Id); + var blacklist = guild.LogBlacklist.ToHashSet(); + blacklist.ExceptWith(affectedChannels.Select(c => c.Id)); + + await ctx.Repository.UpdateGuild(ctx.Guild.Id, new GuildPatch { LogBlacklist = blacklist.ToArray() }); + + await ctx.Reply( + $"{Emojis.Success} {(all ? "All channels" : "Channel")} removed from the logging blacklist." + + (guild.LogChannel == null + ? $"\n{Emojis.Warn} Please note that no logging channel is set, so there is nowhere to log messages to. You can set a logging channel using `{ctx.DefaultPrefix}serverconfig log channel #your-log-channel`." + : "")); + } + + public async Task ShowLogCleanup(Context ctx) { var botList = string.Join(", ", LoggerCleanService.Bots.Select(b => b.Name).OrderBy(x => x.ToLowerInvariant())); var eb = new EmbedBuilder() @@ -377,74 +398,77 @@ public class ServerConfig } await ctx.CheckGuildContext().CheckAuthorPermission(PermissionSet.ManageGuild, "Manage Server"); - bool? newValue = ctx.MatchToggleOrNull(); - if (newValue == null) - { - if (ctx.GuildConfig!.LogCleanupEnabled) - eb.Description( - $"Log cleanup is currently **on** for this server. To disable it, type `{ctx.DefaultPrefix}serverconfig logclean off`."); - else - eb.Description( - $"Log cleanup is currently **off** for this server. To enable it, type `{ctx.DefaultPrefix}serverconfig logclean on`."); - await ctx.Reply(embed: eb.Build()); - return; - } + if (ctx.GuildConfig!.LogCleanupEnabled) + eb.Description( + $"Log cleanup is currently **on** for this server. To disable it, type `{ctx.DefaultPrefix}serverconfig logclean off`."); + else + eb.Description( + $"Log cleanup is currently **off** for this server. To enable it, type `{ctx.DefaultPrefix}serverconfig logclean on`."); - await ctx.Repository.UpdateGuild(ctx.Guild.Id, new GuildPatch { LogCleanupEnabled = newValue.Value }); + await ctx.Reply(embed: eb.Build()); + } - if (newValue.Value) + public async Task SetLogCleanup(Context ctx, bool value) + { + await ctx.CheckGuildContext().CheckAuthorPermission(PermissionSet.ManageGuild, "Manage Server"); + + var botList = string.Join(", ", LoggerCleanService.Bots.Select(b => b.Name).OrderBy(x => x.ToLowerInvariant())); + + await ctx.Repository.UpdateGuild(ctx.Guild.Id, new GuildPatch { LogCleanupEnabled = value }); + + if (value) await ctx.Reply( $"{Emojis.Success} Log cleanup has been **enabled** for this server. Messages deleted by PluralKit will now be cleaned up from logging channels managed by the following bots:\n- **{botList}**\n\n{Emojis.Note} Make sure PluralKit has the **Manage Messages** permission in the channels in question.\n{Emojis.Note} Also, make sure to blacklist the logging channel itself from the bots in question to prevent conflicts."); else await ctx.Reply($"{Emojis.Success} Log cleanup has been **disabled** for this server."); } - public async Task InvalidCommandResponse(Context ctx) + public async Task ShowInvalidCommandResponse(Context ctx) { await ctx.CheckGuildContext().CheckAuthorPermission(PermissionSet.ManageGuild, "Manage Server"); - if (!ctx.HasNext()) - { - var msg = $"Error responses for unknown/invalid commands are currently **{EnabledDisabled(ctx.GuildConfig!.InvalidCommandResponseEnabled)}**."; - await ctx.Reply(msg); - return; - } - - var newVal = ctx.MatchToggle(false); - await ctx.Repository.UpdateGuild(ctx.Guild.Id, new() { InvalidCommandResponseEnabled = newVal }); - await ctx.Reply($"Error responses for unknown/invalid commands are now {EnabledDisabled(newVal)}."); + var msg = $"Error responses for unknown/invalid commands are currently **{EnabledDisabled(ctx.GuildConfig!.InvalidCommandResponseEnabled)}**."; + await ctx.Reply(msg); } - public async Task RequireSystemTag(Context ctx) + public async Task SetInvalidCommandResponse(Context ctx, bool value) { await ctx.CheckGuildContext().CheckAuthorPermission(PermissionSet.ManageGuild, "Manage Server"); - if (!ctx.HasNext()) - { - var msg = $"System tags are currently **{(ctx.GuildConfig!.RequireSystemTag ? "required" : "not required")}** for PluralKit users in this server."; - await ctx.Reply(msg); - return; - } - - var newVal = ctx.MatchToggle(false); - await ctx.Repository.UpdateGuild(ctx.Guild.Id, new() { RequireSystemTag = newVal }); - await ctx.Reply($"System tags are now **{(newVal ? "required" : "not required")}** for PluralKit users in this server."); + await ctx.Repository.UpdateGuild(ctx.Guild.Id, new() { InvalidCommandResponseEnabled = value }); + await ctx.Reply($"Error responses for unknown/invalid commands are now {EnabledDisabled(value)}."); } - public async Task SuppressNotifications(Context ctx) + public async Task ShowRequireSystemTag(Context ctx) { await ctx.CheckGuildContext().CheckAuthorPermission(PermissionSet.ManageGuild, "Manage Server"); - if (!ctx.HasNext()) - { - var msg = $"Suppressing notifications for proxied messages is currently **{EnabledDisabled(ctx.GuildConfig!.SuppressNotifications)}**."; - await ctx.Reply(msg); - return; - } + var msg = $"System tags are currently **{(ctx.GuildConfig!.RequireSystemTag ? "required" : "not required")}** for PluralKit users in this server."; + await ctx.Reply(msg); + } - var newVal = ctx.MatchToggle(false); - await ctx.Repository.UpdateGuild(ctx.Guild.Id, new() { SuppressNotifications = newVal }); - await ctx.Reply($"Suppressing notifications for proxied messages is now {EnabledDisabled(newVal)}."); + public async Task SetRequireSystemTag(Context ctx, bool value) + { + await ctx.CheckGuildContext().CheckAuthorPermission(PermissionSet.ManageGuild, "Manage Server"); + + await ctx.Repository.UpdateGuild(ctx.Guild.Id, new() { RequireSystemTag = value }); + await ctx.Reply($"System tags are now **{(value ? "required" : "not required")}** for PluralKit users in this server."); + } + + public async Task ShowSuppressNotifications(Context ctx) + { + await ctx.CheckGuildContext().CheckAuthorPermission(PermissionSet.ManageGuild, "Manage Server"); + + var msg = $"Suppressing notifications for proxied messages is currently **{EnabledDisabled(ctx.GuildConfig!.SuppressNotifications)}**."; + await ctx.Reply(msg); + } + + public async Task SetSuppressNotifications(Context ctx, bool value) + { + await ctx.CheckGuildContext().CheckAuthorPermission(PermissionSet.ManageGuild, "Manage Server"); + + await ctx.Repository.UpdateGuild(ctx.Guild.Id, new() { SuppressNotifications = value }); + await ctx.Reply($"Suppressing notifications for proxied messages is now {EnabledDisabled(value)}."); } } \ No newline at end of file diff --git a/crates/command_definitions/src/lib.rs b/crates/command_definitions/src/lib.rs index 2fbc00fb..3445d6a6 100644 --- a/crates/command_definitions/src/lib.rs +++ b/crates/command_definitions/src/lib.rs @@ -31,6 +31,7 @@ pub fn all() -> impl Iterator { .chain(group::cmds()) .chain(member::cmds()) .chain(config::cmds()) + .chain(server_config::cmds()) .chain(fun::cmds()) .chain(switch::cmds()) .chain(random::cmds()) @@ -40,6 +41,7 @@ pub fn all() -> impl Iterator { .chain(message::cmds()) .chain(import_export::cmds()) .chain(admin::cmds()) + .chain(misc::cmds()) .map(|cmd| { cmd.hidden_flag(("plaintext", ["pt"])) .hidden_flag(("raw", ["r"])) diff --git a/crates/command_definitions/src/misc.rs b/crates/command_definitions/src/misc.rs index 8b137891..a649599d 100644 --- a/crates/command_definitions/src/misc.rs +++ b/crates/command_definitions/src/misc.rs @@ -1 +1,5 @@ +use super::*; +pub fn cmds() -> impl Iterator { + [].into_iter() +} diff --git a/crates/command_definitions/src/server_config.rs b/crates/command_definitions/src/server_config.rs index 8b137891..7119d528 100644 --- a/crates/command_definitions/src/server_config.rs +++ b/crates/command_definitions/src/server_config.rs @@ -1 +1,149 @@ +use super::*; +pub fn cmds() -> impl Iterator { + let server_config = ("serverconfig", ["guildconfig", "scfg", "gcfg"]); + + let log = tokens!(server_config, ("log", ["log", "logging"])); + let log_channel = tokens!(log, ("channel", ["ch", "chan"])); + let log_cleanup = tokens!(log, ("cleanup", ["clean"])); + let log_cleanup_short = tokens!(server_config, ("logclean", ["logclean", "logcleanup"])); + let log_blacklist = tokens!(log, ("blacklist", ["bl", "ignore"])); + + let proxy = tokens!(server_config, ("proxy", ["proxy", "proxying"])); + let proxy_blacklist = tokens!(proxy, ("blacklist", ["bl", "ignore", "disable"])); + + let invalid = tokens!( + server_config, + ("invalid", ["invalid", "unknown"]), + ("command", ["command", "cmd"]), + ("error", ["error", "response"]) + ); + let invalid_short = tokens!( + server_config, + ( + "invalidcommanderror", + ["invalidcommanderror", "unknowncommanderror", "ice"] + ) + ); + + let require_tag = tokens!( + server_config, + ("require", ["require", "enforce"]), + ("tag", ["tag", "systemtag"]) + ); + let require_tag_short = tokens!(server_config, ("requiretag", ["requiretag", "enforcetag"])); + + let suppress = tokens!( + server_config, + ("suppress", ["suppress"]), + ("notifications", ["notifications", "notifs"]) + ); + let suppress_short = tokens!(server_config, ("proxysilent", ["proxysilent", "silent"])); + + // Common tokens for add/remove operations + let add = ("add", ["enable", "on", "deny"]); + let remove = ("remove", ["disable", "off", "allow"]); + + // Log channel commands + let log_channel_cmds = [ + command!(log_channel => "server_config_log_channel_show") + .help("Shows the current log channel"), + command!(log_channel, ("channel", ChannelRef) => "server_config_log_channel_set") + .help("Sets the log channel"), + command!(log_channel, ("clear", ["c"]) => "server_config_log_channel_clear") + .help("Clears the log channel"), + ] + .into_iter(); + + // Log cleanup commands + let log_cleanup_cmds = [ + command!(log_cleanup => "server_config_log_cleanup_show") + .help("Shows whether log cleanup is enabled"), + command!(log_cleanup, Toggle => "server_config_log_cleanup_set") + .help("Enables or disables log cleanup"), + command!(log_cleanup_short => "server_config_log_cleanup_show") + .help("Shows whether log cleanup is enabled"), + command!(log_cleanup_short, Toggle => "server_config_log_cleanup_set") + .help("Enables or disables log cleanup"), + ] + .into_iter(); + + // Log blacklist commands + let log_blacklist_cmds = [ + command!(log_blacklist => "server_config_log_blacklist_show") + .help("Shows channels where logging is disabled"), + command!(log_blacklist, add, Optional(("channel", ChannelRef)) => "server_config_log_blacklist_add") + .flag(("all", ["a"])) + .help("Adds a channel (or all channels with --all) to the log blacklist"), + command!(log_blacklist, remove, Optional(("channel", ChannelRef)) => "server_config_log_blacklist_remove") + .flag(("all", ["a"])) + .help("Removes a channel (or all channels with --all) from the log blacklist"), + ] + .into_iter(); + + // Proxy blacklist commands + let proxy_blacklist_cmds = [ + command!(proxy_blacklist => "server_config_proxy_blacklist_show") + .help("Shows channels where proxying is disabled"), + command!(proxy_blacklist, add, Optional(("channel", ChannelRef)) => "server_config_proxy_blacklist_add") + .flag(("all", ["a"])) + .help("Adds a channel (or all channels with --all) to the proxy blacklist"), + command!(proxy_blacklist, remove, Optional(("channel", ChannelRef)) => "server_config_proxy_blacklist_remove") + .flag(("all", ["a"])) + .help("Removes a channel (or all channels with --all) from the proxy blacklist"), + ] + .into_iter(); + + // Invalid command error commands + let invalid_cmds = [ + command!(invalid => "server_config_invalid_command_response_show") + .help("Shows whether error responses for invalid commands are enabled"), + command!(invalid, Toggle => "server_config_invalid_command_response_set") + .help("Enables or disables error responses for invalid commands"), + command!(invalid_short => "server_config_invalid_command_response_show") + .help("Shows whether error responses for invalid commands are enabled"), + command!(invalid_short, Toggle => "server_config_invalid_command_response_set") + .help("Enables or disables error responses for invalid commands"), + ] + .into_iter(); + + // Require system tag commands + let require_tag_cmds = [ + command!(require_tag => "server_config_require_system_tag_show") + .help("Shows whether system tags are required"), + command!(require_tag, Toggle => "server_config_require_system_tag_set") + .help("Requires or unrequires system tags for proxied messages"), + command!(require_tag_short => "server_config_require_system_tag_show") + .help("Shows whether system tags are required"), + command!(require_tag_short, Toggle => "server_config_require_system_tag_set") + .help("Requires or unrequires system tags for proxied messages"), + ] + .into_iter(); + + // Suppress notifications commands + let suppress_cmds = [ + command!(suppress => "server_config_suppress_notifications_show") + .help("Shows whether notifications are suppressed for proxied messages"), + command!(suppress, Toggle => "server_config_suppress_notifications_set") + .help("Enables or disables notification suppression for proxied messages"), + command!(suppress_short => "server_config_suppress_notifications_show") + .help("Shows whether notifications are suppressed for proxied messages"), + command!(suppress_short, Toggle => "server_config_suppress_notifications_set") + .help("Enables or disables notification suppression for proxied messages"), + ] + .into_iter(); + + // Main config overview + let main_cmd = [command!(server_config => "server_config_show") + .help("Shows the current server configuration")] + .into_iter(); + + main_cmd + .chain(log_channel_cmds) + .chain(log_cleanup_cmds) + .chain(log_blacklist_cmds) + .chain(proxy_blacklist_cmds) + .chain(invalid_cmds) + .chain(require_tag_cmds) + .chain(suppress_cmds) +} From f14901a4e38a7bec4b0148ea7f9a3cf9e032d06a Mon Sep 17 00:00:00 2001 From: dusk Date: Tue, 7 Oct 2025 14:47:35 +0000 Subject: [PATCH 111/179] implement misc commands --- PluralKit.Bot/CommandMeta/CommandTree.cs | 4 ++-- crates/command_definitions/src/misc.rs | 7 ++++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/PluralKit.Bot/CommandMeta/CommandTree.cs b/PluralKit.Bot/CommandMeta/CommandTree.cs index 1aefe854..2aa901ca 100644 --- a/PluralKit.Bot/CommandMeta/CommandTree.cs +++ b/PluralKit.Bot/CommandMeta/CommandTree.cs @@ -15,6 +15,8 @@ public partial class CommandTree "For the list of commands, see the website: "), Commands.HelpProxy => ctx.Reply( "The proxy help page has been moved! See the website: https://pluralkit.me/guide#proxying"), + Commands.Invite => ctx.Execute(Invite, m => m.Invite(ctx)), + Commands.Stats => ctx.Execute(null, m => m.Stats(ctx)), Commands.MemberShow(var param, var flags) => ctx.Execute(MemberInfo, m => m.ViewMember(ctx, param.target, flags.show_embed)), Commands.MemberNew(var param, _) => ctx.Execute(MemberNew, m => m.NewMember(ctx, param.name)), Commands.MemberSoulscream(var param, _) => ctx.Execute(MemberInfo, m => m.Soulscream(ctx, param.target)), @@ -331,8 +333,6 @@ public partial class CommandTree // Legacy command routing - these are kept for backwards compatibility until fully migrated to new system if (ctx.Match("commands", "cmd", "c")) return CommandHelpRoot(ctx); - if (ctx.Match("invite")) return ctx.Execute(Invite, m => m.Invite(ctx)); - if (ctx.Match("stats", "status")) return ctx.Execute(null, m => m.Stats(ctx)); } private async Task CommandHelpRoot(Context ctx) diff --git a/crates/command_definitions/src/misc.rs b/crates/command_definitions/src/misc.rs index a649599d..a87f4d1b 100644 --- a/crates/command_definitions/src/misc.rs +++ b/crates/command_definitions/src/misc.rs @@ -1,5 +1,10 @@ use super::*; pub fn cmds() -> impl Iterator { - [].into_iter() + [ + command!("invite" => "invite").help("Gets a link to invite PluralKit to other servers"), + command!(("stats", ["status"]) => "stats") + .help("Shows statistics and information about PluralKit"), + ] + .into_iter() } From c1ed7487d7108c15c9e5762b46f9523f0e32730e Mon Sep 17 00:00:00 2001 From: dusk Date: Tue, 7 Oct 2025 18:22:04 +0000 Subject: [PATCH 112/179] rank possible commands by input similarity --- Cargo.lock | 7 ++ crates/command_parser/Cargo.toml | 3 +- crates/command_parser/src/command.rs | 14 ++++ crates/command_parser/src/lib.rs | 120 +++++++++++++++++++++++++-- crates/command_parser/src/token.rs | 2 +- 5 files changed, 135 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f0469c2a..9e133dcf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -666,6 +666,7 @@ dependencies = [ "ordermap", "regex", "smol_str", + "strsim", ] [[package]] @@ -4219,6 +4220,12 @@ dependencies = [ "unicode-properties", ] +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "subtle" version = "2.6.1" diff --git a/crates/command_parser/Cargo.toml b/crates/command_parser/Cargo.toml index 639d4a44..74c1a769 100644 --- a/crates/command_parser/Cargo.toml +++ b/crates/command_parser/Cargo.toml @@ -7,4 +7,5 @@ edition = "2024" lazy_static = { workspace = true } smol_str = "0.3.2" ordermap = "0.5" -regex = "1" \ No newline at end of file +regex = "1" +strsim = "0.11" \ No newline at end of file diff --git a/crates/command_parser/src/command.rs b/crates/command_parser/src/command.rs index 6ae62e60..dacb271f 100644 --- a/crates/command_parser/src/command.rs +++ b/crates/command_parser/src/command.rs @@ -72,6 +72,20 @@ impl Command { } } +impl PartialEq for Command { + fn eq(&self, other: &Self) -> bool { + self.cb == other.cb + } +} + +impl Eq for Command {} + +impl std::hash::Hash for Command { + fn hash(&self, state: &mut H) { + self.cb.hash(state); + } +} + impl Display for Command { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let visible_flags = self diff --git a/crates/command_parser/src/lib.rs b/crates/command_parser/src/lib.rs index ee3a8ee3..d736b61f 100644 --- a/crates/command_parser/src/lib.rs +++ b/crates/command_parser/src/lib.rs @@ -8,9 +8,9 @@ pub mod token; pub mod tree; use core::panic; -use std::collections::HashMap; use std::fmt::Write; use std::ops::Not; +use std::{collections::HashMap, usize}; use command::Command; use flag::{Flag, FlagMatchError, FlagValueMatchError}; @@ -20,7 +20,7 @@ use string::MatchedFlag; use token::{Token, TokenMatchResult}; // todo: this should come from the bot probably -const MAX_SUGGESTIONS: usize = 7; +const MAX_SUGGESTIONS: usize = 5; pub type Tree = tree::TreeBranch; @@ -90,8 +90,13 @@ pub fn parse_command( None => { let mut error = format!("Unknown command `{prefix}{input}`."); - if fmt_possible_commands(&mut error, &prefix, local_tree.possible_commands(1)).not() - { + let wrote_possible_commands = fmt_possible_commands( + &mut error, + &prefix, + &input, + local_tree.possible_commands(usize::MAX), + ); + if wrote_possible_commands.not() { // add a space between the unknown command and "for a list of all possible commands" // message if we didn't add any possible suggestions error.push_str(" "); @@ -276,17 +281,114 @@ fn next_token<'a>( fn fmt_possible_commands( f: &mut String, prefix: &str, + input: &str, mut possible_commands: impl Iterator, ) -> bool { if let Some(first) = possible_commands.next() { - f.push_str(" Perhaps you meant one of the following commands:\n"); - for command in std::iter::once(first).chain(possible_commands.take(MAX_SUGGESTIONS - 1)) { - if !command.show_in_suggestions { - continue; + let mut commands_with_scores: Vec<(&Command, String, f64, bool)> = std::iter::once(first) + .chain(possible_commands) + .filter(|cmd| cmd.show_in_suggestions) + .flat_map(|cmd| { + let versions = generate_command_versions(cmd); + versions.into_iter().map(move |(version, is_alias)| { + let similarity = strsim::jaro_winkler(&input, &version); + (cmd, version, similarity, is_alias) + }) + }) + .collect(); + + commands_with_scores + .sort_by(|a, b| b.2.partial_cmp(&a.2).unwrap_or(std::cmp::Ordering::Equal)); + + // remove duplicate commands + let mut seen_commands = std::collections::HashSet::new(); + let mut best_commands = Vec::new(); + for (cmd, version, score, is_alias) in commands_with_scores { + if seen_commands.insert(cmd) { + best_commands.push((cmd, version, score, is_alias)); } - writeln!(f, "- **{prefix}{command}** - *{}*", command.help).expect("oom"); + } + + const MIN_SCORE_THRESHOLD: f64 = 0.8; + if best_commands.is_empty() || best_commands[0].2 < MIN_SCORE_THRESHOLD { + return false; + } + + // if score falls off too much, don't show + let mut falloff_threshold: f64 = 0.2; + let best_score = best_commands[0].2; + + let mut commands_to_show = Vec::new(); + for (command, version, score, is_alias) in best_commands.iter().take(MAX_SUGGESTIONS) { + let delta = best_score - score; + falloff_threshold -= delta; + if delta > falloff_threshold { + break; + } + commands_to_show.push((command, version, score, is_alias)); + } + + if commands_to_show.is_empty() { + return false; + } + + f.push_str(" Perhaps you meant one of the following commands:\n"); + for (command, version, _score, is_alias) in commands_to_show { + writeln!( + f, + "- **{prefix}{version}**{alias} - *{help}*", + help = command.help, + alias = is_alias + .then(|| format!( + " (alias of **{prefix}{base_version}**)", + base_version = build_command_string(command, None) + )) + .unwrap_or_else(String::new), + ) + .expect("oom"); } return true; } return false; } + +fn generate_command_versions(cmd: &Command) -> Vec<(String, bool)> { + let mut versions = Vec::new(); + + // Start with base version using primary names + let base_version = build_command_string(cmd, None); + versions.push((base_version, false)); + + // Generate versions for each alias combination + for (idx, token) in cmd.tokens.iter().enumerate() { + if let Token::Value { aliases, .. } = token { + for alias in aliases { + versions.push((build_command_string(cmd, Some((idx, alias.as_str()))), true)); + } + } + } + + versions +} + +fn build_command_string(cmd: &Command, alias_replacement: Option<(usize, &str)>) -> String { + let mut result = String::new(); + for (idx, token) in cmd.tokens.iter().enumerate() { + if idx > 0 { + result.push(' '); + } + + // Check if we should use an alias for this token + let replacement = alias_replacement + .filter(|(i, _)| *i == idx) + .map(|(_, alias)| alias); + + match token { + Token::Value { name, .. } => { + result.push_str(replacement.unwrap_or(name)); + } + Token::Parameter(param) => write!(&mut result, "{param}").unwrap(), + } + } + result +} diff --git a/crates/command_parser/src/token.rs b/crates/command_parser/src/token.rs index c18ce0f3..f158f269 100644 --- a/crates/command_parser/src/token.rs +++ b/crates/command_parser/src/token.rs @@ -2,7 +2,7 @@ use std::fmt::{Debug, Display}; use smol_str::SmolStr; -use crate::parameter::{Optional, Parameter, ParameterKind, ParameterValue}; +use crate::parameter::{Parameter, ParameterValue}; #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum Token { From 15ffd16c01ef39518ae27547692bbbe6479b8ef7 Mon Sep 17 00:00:00 2001 From: dusk Date: Tue, 7 Oct 2025 21:59:26 +0000 Subject: [PATCH 113/179] implement command root --- .../CommandMeta/CommandParseErrors.cs | 28 +--- PluralKit.Bot/CommandMeta/CommandTree.cs | 66 +-------- PluralKit.Bot/CommandSystem/Parameters.cs | 5 + crates/command_definitions/src/commands.rs | 1 - crates/command_definitions/src/help.rs | 1 + crates/command_definitions/src/lib.rs | 1 - crates/command_parser/src/lib.rs | 137 ++++++++---------- crates/command_parser/src/token.rs | 4 +- crates/commands/src/commands.udl | 1 + crates/commands/src/lib.rs | 23 ++- crates/commands/src/main.rs | 10 ++ 11 files changed, 107 insertions(+), 170 deletions(-) delete mode 100644 crates/command_definitions/src/commands.rs diff --git a/PluralKit.Bot/CommandMeta/CommandParseErrors.cs b/PluralKit.Bot/CommandMeta/CommandParseErrors.cs index 334a271d..fb415b95 100644 --- a/PluralKit.Bot/CommandMeta/CommandParseErrors.cs +++ b/PluralKit.Bot/CommandMeta/CommandParseErrors.cs @@ -1,40 +1,16 @@ -using Humanizer; - using Myriad.Types; -using PluralKit.Core; - namespace PluralKit.Bot; public partial class CommandTree { - private async Task PrintCommandNotFoundError(Context ctx, params Command[] potentialCommands) + private async Task PrintCommandList(Context ctx, string subject, string commands) { - var commandListStr = CreatePotentialCommandList(ctx.DefaultPrefix, potentialCommands); - await ctx.Reply( - $"{Emojis.Error} Unknown command `{ctx.DefaultPrefix}{ctx.FullCommand().Truncate(100)}`. Perhaps you meant to use one of the following commands?\n{commandListStr}\n\nFor a full list of possible commands, see ."); - } - - private async Task PrintCommandExpectedError(Context ctx, params Command[] potentialCommands) - { - var commandListStr = CreatePotentialCommandList(ctx.DefaultPrefix, potentialCommands); - await ctx.Reply( - $"{Emojis.Error} You need to pass a command. Perhaps you meant to use one of the following commands?\n{commandListStr}\n\nFor a full list of possible commands, see ."); - } - - private static string CreatePotentialCommandList(string prefix, params Command[] potentialCommands) - { - return string.Join("\n", potentialCommands.Select(cmd => $"- **{prefix}{cmd.Usage}** - *{cmd.Description}*")); - } - - private async Task PrintCommandList(Context ctx, string subject, params Command[] commands) - { - var str = CreatePotentialCommandList(ctx.DefaultPrefix, commands); await ctx.Reply( $"Here is a list of commands related to {subject}:", embed: new Embed() { - Description = $"{str}\nFor a full list of possible commands, see .", + Description = $"{commands}\nFor a full list of possible commands, see .", Color = DiscordUtils.Blue, } ); diff --git a/PluralKit.Bot/CommandMeta/CommandTree.cs b/PluralKit.Bot/CommandMeta/CommandTree.cs index 2aa901ca..e8c3eaa5 100644 --- a/PluralKit.Bot/CommandMeta/CommandTree.cs +++ b/PluralKit.Bot/CommandMeta/CommandTree.cs @@ -8,6 +8,7 @@ public partial class CommandTree { return command switch { + Commands.CommandsList(var param, _) => PrintCommandList(ctx, param.subject, Parameters.GetRelatedCommands(ctx.DefaultPrefix, param.subject)), Commands.Dashboard => ctx.Execute(Dashboard, m => m.Dashboard(ctx)), Commands.Explain => ctx.Execute(Explain, m => m.Explain(ctx)), Commands.Help(_, var flags) => ctx.Execute(Help, m => m.HelpRoot(ctx, flags.show_embed)), @@ -330,70 +331,5 @@ public partial class CommandTree ctx.Reply( $"{Emojis.Error} Parsed command {ctx.Parameters.Callback().AsCode()} not implemented in PluralKit.Bot!"), }; - // Legacy command routing - these are kept for backwards compatibility until fully migrated to new system - if (ctx.Match("commands", "cmd", "c")) - return CommandHelpRoot(ctx); - } - - private async Task CommandHelpRoot(Context ctx) - { - if (!ctx.HasNext()) - { - await ctx.Reply( - "Available command help targets: `system`, `member`, `group`, `switch`, `config`, `autoproxy`, `log`, `blacklist`." - + $"\n- **{ctx.DefaultPrefix}commands ** - *View commands related to a help target.*" - + "\n\nFor the full list of commands, see the website: "); - return; - } - - switch (ctx.PeekArgument()) - { - case "system": - case "systems": - case "s": - case "account": - case "acc": - await PrintCommandList(ctx, "systems", SystemCommands); - break; - case "member": - case "members": - case "m": - await PrintCommandList(ctx, "members", MemberCommands); - break; - case "group": - case "groups": - case "g": - await PrintCommandList(ctx, "groups", GroupCommands); - break; - case "switch": - case "switches": - case "switching": - case "sw": - await PrintCommandList(ctx, "switching", SwitchCommands); - break; - case "log": - await PrintCommandList(ctx, "message logging", LogCommands); - break; - case "blacklist": - case "bl": - await PrintCommandList(ctx, "channel blacklisting", BlacklistCommands); - break; - case "config": - case "cfg": - await PrintCommandList(ctx, "settings", ConfigCommands); - break; - case "serverconfig": - case "guildconfig": - case "scfg": - await PrintCommandList(ctx, "server settings", ServerConfigCommands); - break; - case "autoproxy": - case "ap": - await PrintCommandList(ctx, "autoproxy", AutoproxyCommands); - break; - default: - await ctx.Reply("For the full list of commands, see the website: "); - break; - } } } \ No newline at end of file diff --git a/PluralKit.Bot/CommandSystem/Parameters.cs b/PluralKit.Bot/CommandSystem/Parameters.cs index b359e80b..1add6fe2 100644 --- a/PluralKit.Bot/CommandSystem/Parameters.cs +++ b/PluralKit.Bot/CommandSystem/Parameters.cs @@ -55,6 +55,11 @@ public class Parameters } } + public static string GetRelatedCommands(string prefix, string subject) + { + return CommandsMethods.GetRelatedCommands(prefix, subject); + } + public string Callback() { return _cb; diff --git a/crates/command_definitions/src/commands.rs b/crates/command_definitions/src/commands.rs deleted file mode 100644 index 8b137891..00000000 --- a/crates/command_definitions/src/commands.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/crates/command_definitions/src/help.rs b/crates/command_definitions/src/help.rs index 8991cf72..95e73d5c 100644 --- a/crates/command_definitions/src/help.rs +++ b/crates/command_definitions/src/help.rs @@ -3,6 +3,7 @@ use super::*; pub fn cmds() -> impl Iterator { let help = ("help", ["h"]); [ + command!(("commands", ["cmd", "c"]), ("subject", OpaqueString) => "commands_list"), command!(("dashboard", ["dash"]) => "dashboard"), command!("explain" => "explain"), command!(help => "help").help("Shows the help command"), diff --git a/crates/command_definitions/src/lib.rs b/crates/command_definitions/src/lib.rs index 3445d6a6..065e95b3 100644 --- a/crates/command_definitions/src/lib.rs +++ b/crates/command_definitions/src/lib.rs @@ -1,7 +1,6 @@ pub mod admin; pub mod api; pub mod autoproxy; -pub mod commands; pub mod config; pub mod debug; pub mod fun; diff --git a/crates/command_parser/src/lib.rs b/crates/command_parser/src/lib.rs index d736b61f..db065cc5 100644 --- a/crates/command_parser/src/lib.rs +++ b/crates/command_parser/src/lib.rs @@ -90,13 +90,12 @@ pub fn parse_command( None => { let mut error = format!("Unknown command `{prefix}{input}`."); - let wrote_possible_commands = fmt_possible_commands( - &mut error, - &prefix, - &input, - local_tree.possible_commands(usize::MAX), - ); - if wrote_possible_commands.not() { + let possible_commands = + rank_possible_commands(&input, local_tree.possible_commands(usize::MAX)); + if possible_commands.is_empty().not() { + error.push_str(" Perhaps you meant one of the following commands:\n"); + fmt_commands_list(&mut error, &prefix, possible_commands); + } else { // add a space between the unknown command and "for a list of all possible commands" // message if we didn't add any possible suggestions error.push_str(" "); @@ -278,78 +277,70 @@ fn next_token<'a>( // todo: should probably move this somewhere else /// returns true if wrote possible commands, false if not -fn fmt_possible_commands( - f: &mut String, - prefix: &str, +fn rank_possible_commands( input: &str, - mut possible_commands: impl Iterator, -) -> bool { - if let Some(first) = possible_commands.next() { - let mut commands_with_scores: Vec<(&Command, String, f64, bool)> = std::iter::once(first) - .chain(possible_commands) - .filter(|cmd| cmd.show_in_suggestions) - .flat_map(|cmd| { - let versions = generate_command_versions(cmd); - versions.into_iter().map(move |(version, is_alias)| { - let similarity = strsim::jaro_winkler(&input, &version); - (cmd, version, similarity, is_alias) - }) + possible_commands: impl IntoIterator, +) -> Vec<(Command, String, bool)> { + let mut commands_with_scores: Vec<(&Command, String, f64, bool)> = possible_commands + .into_iter() + .filter(|cmd| cmd.show_in_suggestions) + .flat_map(|cmd| { + let versions = generate_command_versions(cmd); + versions.into_iter().map(move |(version, is_alias)| { + let similarity = strsim::jaro_winkler(&input, &version); + (cmd, version, similarity, is_alias) }) - .collect(); + }) + .collect(); - commands_with_scores - .sort_by(|a, b| b.2.partial_cmp(&a.2).unwrap_or(std::cmp::Ordering::Equal)); + commands_with_scores.sort_by(|a, b| b.2.partial_cmp(&a.2).unwrap_or(std::cmp::Ordering::Equal)); - // remove duplicate commands - let mut seen_commands = std::collections::HashSet::new(); - let mut best_commands = Vec::new(); - for (cmd, version, score, is_alias) in commands_with_scores { - if seen_commands.insert(cmd) { - best_commands.push((cmd, version, score, is_alias)); - } + // remove duplicate commands + let mut seen_commands = std::collections::HashSet::new(); + let mut best_commands = Vec::new(); + for (cmd, version, score, is_alias) in commands_with_scores { + if seen_commands.insert(cmd) { + best_commands.push((cmd, version, score, is_alias)); } - - const MIN_SCORE_THRESHOLD: f64 = 0.8; - if best_commands.is_empty() || best_commands[0].2 < MIN_SCORE_THRESHOLD { - return false; - } - - // if score falls off too much, don't show - let mut falloff_threshold: f64 = 0.2; - let best_score = best_commands[0].2; - - let mut commands_to_show = Vec::new(); - for (command, version, score, is_alias) in best_commands.iter().take(MAX_SUGGESTIONS) { - let delta = best_score - score; - falloff_threshold -= delta; - if delta > falloff_threshold { - break; - } - commands_to_show.push((command, version, score, is_alias)); - } - - if commands_to_show.is_empty() { - return false; - } - - f.push_str(" Perhaps you meant one of the following commands:\n"); - for (command, version, _score, is_alias) in commands_to_show { - writeln!( - f, - "- **{prefix}{version}**{alias} - *{help}*", - help = command.help, - alias = is_alias - .then(|| format!( - " (alias of **{prefix}{base_version}**)", - base_version = build_command_string(command, None) - )) - .unwrap_or_else(String::new), - ) - .expect("oom"); - } - return true; } - return false; + + const MIN_SCORE_THRESHOLD: f64 = 0.8; + if best_commands.is_empty() || best_commands[0].2 < MIN_SCORE_THRESHOLD { + return Vec::new(); + } + + // if score falls off too much, don't show + let mut falloff_threshold: f64 = 0.2; + let best_score = best_commands[0].2; + + let mut commands_to_show = Vec::new(); + for (command, version, score, is_alias) in best_commands.into_iter().take(MAX_SUGGESTIONS) { + let delta = best_score - score; + falloff_threshold -= delta; + if delta > falloff_threshold { + break; + } + commands_to_show.push((command.clone(), version, is_alias)); + } + + commands_to_show +} + +fn fmt_commands_list(f: &mut String, prefix: &str, commands_to_show: Vec<(Command, String, bool)>) { + for (command, version, is_alias) in commands_to_show { + writeln!( + f, + "- **{prefix}{version}**{alias} - *{help}*", + help = command.help, + alias = is_alias + .then(|| format!( + " (alias of **{prefix}{base_version}**)", + base_version = build_command_string(&command, None) + )) + .unwrap_or_else(String::new), + ) + .expect("oom"); + } } fn generate_command_versions(cmd: &Command) -> Vec<(String, bool)> { diff --git a/crates/command_parser/src/token.rs b/crates/command_parser/src/token.rs index f158f269..e87c702a 100644 --- a/crates/command_parser/src/token.rs +++ b/crates/command_parser/src/token.rs @@ -36,10 +36,10 @@ pub enum TokenMatchResult { // a: because we want to differentiate between no match and match failure (it matched with an error) // "no match" has a different charecteristic because we want to continue matching other tokens... // ...while "match failure" means we should stop matching and return the error -type TryMatchResult = Option; +pub type TryMatchResult = Option; impl Token { - pub(super) fn try_match(&self, input: Option<&str>) -> TryMatchResult { + pub fn try_match(&self, input: Option<&str>) -> TryMatchResult { let input = match input { Some(input) => input, None => { diff --git a/crates/commands/src/commands.udl b/crates/commands/src/commands.udl index f531f870..3bffa3cb 100644 --- a/crates/commands/src/commands.udl +++ b/crates/commands/src/commands.udl @@ -1,5 +1,6 @@ namespace commands { CommandResult parse_command(string prefix, string input); + string get_related_commands(string prefix, string input); }; [Enum] interface CommandResult { diff --git a/crates/commands/src/lib.rs b/crates/commands/src/lib.rs index c78b0c69..959683c7 100644 --- a/crates/commands/src/lib.rs +++ b/crates/commands/src/lib.rs @@ -1,6 +1,6 @@ -use std::collections::HashMap; +use std::{collections::HashMap, fmt::Write, usize}; -use command_parser::{parameter::ParameterValue, Tree}; +use command_parser::{parameter::ParameterValue, token::TokenMatchResult, Tree}; uniffi::include_scaffolding!("commands"); @@ -143,3 +143,22 @@ pub fn parse_command(prefix: String, input: String) -> CommandResult { }, ) } + +pub fn get_related_commands(prefix: String, input: String) -> String { + let mut s = String::new(); + for command in command_definitions::all() { + if command.tokens.first().map_or(false, |token| { + token + .try_match(Some(&input)) + .map_or(false, |r| matches!(r, TokenMatchResult::MatchedValue)) + }) { + writeln!( + &mut s, + "- **{prefix}{command}** - *{help}*", + help = command.help + ) + .unwrap(); + } + } + s +} diff --git a/crates/commands/src/main.rs b/crates/commands/src/main.rs index 07128bfe..325fc469 100644 --- a/crates/commands/src/main.rs +++ b/crates/commands/src/main.rs @@ -4,6 +4,16 @@ use command_parser::Tree; use commands::COMMAND_TREE; fn main() { + parse(); +} + +fn related() { + let cmd = std::env::args().nth(1).unwrap(); + let related = commands::get_related_commands("pk;".to_string(), cmd); + println!("Related commands:\n{related}"); +} + +fn parse() { let cmd = std::env::args() .skip(1) .intersperse(" ".to_string()) From 479e0a59b55c684793d6b77dc9813a44600aa0f1 Mon Sep 17 00:00:00 2001 From: dusk Date: Wed, 8 Oct 2025 03:26:40 +0000 Subject: [PATCH 114/179] remove rest of the parsing in csharp bot --- .../CommandMeta/CommandParseErrors.cs | 29 ++--- PluralKit.Bot/CommandMeta/CommandTree.cs | 66 ++++++------ .../Context/ContextArgumentsExt.cs | 100 ------------------ .../CommandSystem/Context/ContextAvatarExt.cs | 4 - .../Context/ContextEntityArgumentsExt.cs | 22 ---- .../Context/ContextPrivacyExt.cs | 51 --------- PluralKit.Bot/CommandSystem/Parameters.cs | 2 +- PluralKit.Bot/Commands/Admin.cs | 40 +++---- PluralKit.Bot/Commands/Api.cs | 34 +++--- PluralKit.Bot/Commands/Config.cs | 4 +- PluralKit.Bot/Commands/Fun.cs | 26 ++--- PluralKit.Bot/Commands/GroupMember.cs | 8 +- PluralKit.Bot/Commands/Groups.cs | 27 ++--- PluralKit.Bot/Commands/ImportExport.cs | 6 +- .../Commands/Lists/ContextListExt.cs | 6 +- PluralKit.Bot/Commands/Member.cs | 4 +- PluralKit.Bot/Commands/MemberAvatar.cs | 16 +-- PluralKit.Bot/Commands/MemberEdit.cs | 4 +- PluralKit.Bot/Commands/MemberProxy.cs | 18 ++-- PluralKit.Bot/Commands/Message.cs | 4 +- PluralKit.Bot/Commands/Random.cs | 4 +- PluralKit.Bot/Commands/ServerConfig.cs | 4 +- PluralKit.Bot/Commands/Switch.cs | 26 ++--- PluralKit.Bot/Commands/SystemFront.cs | 4 +- PluralKit.Bot/Commands/SystemLink.cs | 4 +- PluralKit.Bot/Services/EmbedService.cs | 8 +- PluralKit.Bot/Utils/ContextUtils.cs | 8 +- crates/command_definitions/src/admin.rs | 10 ++ crates/command_definitions/src/group.rs | 11 +- .../command_definitions/src/import_export.rs | 3 +- crates/command_definitions/src/member.rs | 1 + .../command_definitions/src/server_config.rs | 1 + crates/command_definitions/src/switch.rs | 2 +- crates/command_definitions/src/system.rs | 92 +++++++++------- crates/commands/src/main.rs | 2 +- 35 files changed, 242 insertions(+), 409 deletions(-) delete mode 100644 PluralKit.Bot/CommandSystem/Context/ContextPrivacyExt.cs diff --git a/PluralKit.Bot/CommandMeta/CommandParseErrors.cs b/PluralKit.Bot/CommandMeta/CommandParseErrors.cs index fb415b95..5b54df32 100644 --- a/PluralKit.Bot/CommandMeta/CommandParseErrors.cs +++ b/PluralKit.Bot/CommandMeta/CommandParseErrors.cs @@ -7,28 +7,13 @@ public partial class CommandTree private async Task PrintCommandList(Context ctx, string subject, string commands) { await ctx.Reply( - $"Here is a list of commands related to {subject}:", - embed: new Embed() - { - Description = $"{commands}\nFor a full list of possible commands, see .", - Color = DiscordUtils.Blue, - } + components: [ + new MessageComponent() + { + Type = ComponentType.Text, + Content = $"Here is a list of commands related to {subject}:\n{commands}\nFor a full list of possible commands, see .", + } + ] ); } - - private async Task CreateSystemNotFoundError(Context ctx) - { - var input = ctx.PopArgument(); - if (input.TryParseMention(out var id)) - { - // Try to resolve the user ID to find the associated account, - // so we can print their username. - var user = await ctx.Rest.GetUser(id); - if (user != null) - return $"Account **{user.Username}#{user.Discriminator}** does not have a system registered."; - return $"Account with ID `{id}` not found."; - } - - return $"System with ID {input.AsCode()} not found."; - } } \ No newline at end of file diff --git a/PluralKit.Bot/CommandMeta/CommandTree.cs b/PluralKit.Bot/CommandMeta/CommandTree.cs index e8c3eaa5..72a43288 100644 --- a/PluralKit.Bot/CommandMeta/CommandTree.cs +++ b/PluralKit.Bot/CommandMeta/CommandTree.cs @@ -22,13 +22,13 @@ public partial class CommandTree Commands.MemberNew(var param, _) => ctx.Execute(MemberNew, m => m.NewMember(ctx, param.name)), Commands.MemberSoulscream(var param, _) => ctx.Execute(MemberInfo, m => m.Soulscream(ctx, param.target)), Commands.MemberAvatarShow(var param, var flags) => ctx.Execute(MemberAvatar, m => m.ShowAvatar(ctx, param.target, flags.GetReplyFormat())), - Commands.MemberAvatarClear(var param, var flags) => ctx.Execute(MemberAvatar, m => m.ClearAvatar(ctx, param.target)), + Commands.MemberAvatarClear(var param, var flags) => ctx.Execute(MemberAvatar, m => m.ClearAvatar(ctx, param.target, flags.yes)), Commands.MemberAvatarUpdate(var param, _) => ctx.Execute(MemberAvatar, m => m.ChangeAvatar(ctx, param.target, param.avatar)), Commands.MemberWebhookAvatarShow(var param, var flags) => ctx.Execute(MemberAvatar, m => m.ShowWebhookAvatar(ctx, param.target, flags.GetReplyFormat())), - Commands.MemberWebhookAvatarClear(var param, var flags) => ctx.Execute(MemberAvatar, m => m.ClearWebhookAvatar(ctx, param.target)), + Commands.MemberWebhookAvatarClear(var param, var flags) => ctx.Execute(MemberAvatar, m => m.ClearWebhookAvatar(ctx, param.target, flags.yes)), Commands.MemberWebhookAvatarUpdate(var param, _) => ctx.Execute(MemberAvatar, m => m.ChangeWebhookAvatar(ctx, param.target, param.avatar)), Commands.MemberServerAvatarShow(var param, var flags) => ctx.Execute(MemberAvatar, m => m.ShowServerAvatar(ctx, param.target, flags.GetReplyFormat())), - Commands.MemberServerAvatarClear(var param, var flags) => ctx.Execute(MemberAvatar, m => m.ClearServerAvatar(ctx, param.target)), + Commands.MemberServerAvatarClear(var param, var flags) => ctx.Execute(MemberAvatar, m => m.ClearServerAvatar(ctx, param.target, flags.yes)), Commands.MemberServerAvatarUpdate(var param, _) => ctx.Execute(MemberAvatar, m => m.ChangeServerAvatar(ctx, param.target, param.avatar)), Commands.MemberPronounsShow(var param, var flags) => ctx.Execute(MemberPronouns, m => m.ShowPronouns(ctx, param.target, flags.GetReplyFormat())), Commands.MemberPronounsClear(var param, var flags) => ctx.Execute(MemberPronouns, m => m.ClearPronouns(ctx, param.target, flags.yes)), @@ -37,7 +37,7 @@ public partial class CommandTree Commands.MemberDescClear(var param, var flags) => ctx.Execute(MemberDesc, m => m.ClearDescription(ctx, param.target, flags.yes)), Commands.MemberDescUpdate(var param, _) => ctx.Execute(MemberDesc, m => m.ChangeDescription(ctx, param.target, param.description)), Commands.MemberNameShow(var param, var flags) => ctx.Execute(MemberInfo, m => m.ShowName(ctx, param.target, flags.GetReplyFormat())), - Commands.MemberNameUpdate(var param, _) => ctx.Execute(MemberInfo, m => m.ChangeName(ctx, param.target, param.name)), + Commands.MemberNameUpdate(var param, var flags) => ctx.Execute(MemberInfo, m => m.ChangeName(ctx, param.target, param.name, flags.yes)), Commands.MemberBannerShow(var param, var flags) => ctx.Execute(MemberBannerImage, m => m.ShowBannerImage(ctx, param.target, flags.GetReplyFormat())), Commands.MemberBannerClear(var param, var flags) => ctx.Execute(MemberBannerImage, m => m.ClearBannerImage(ctx, param.target, flags.yes)), Commands.MemberBannerUpdate(var param, _) => ctx.Execute(MemberBannerImage, m => m.ChangeBannerImage(ctx, param.target, param.banner)), @@ -59,7 +59,7 @@ public partial class CommandTree Commands.MemberServerKeepproxyUpdate(var param, _) => ctx.Execute(MemberServerKeepProxy, m => m.ChangeServerKeepProxy(ctx, param.target, param.value)), Commands.MemberServerKeepproxyClear(var param, var flags) => ctx.Execute(MemberServerKeepProxy, m => m.ClearServerKeepProxy(ctx, param.target, flags.yes)), Commands.MemberProxyShow(var param, _) => ctx.Execute(MemberProxy, m => m.ShowProxy(ctx, param.target)), - Commands.MemberProxyClear(var param, var flags) => ctx.Execute(MemberProxy, m => m.ClearProxy(ctx, param.target)), + Commands.MemberProxyClear(var param, var flags) => ctx.Execute(MemberProxy, m => m.ClearProxy(ctx, param.target, flags.yes)), Commands.MemberProxyAdd(var param, _) => ctx.Execute(MemberProxy, m => m.AddProxy(ctx, param.target, param.tag)), Commands.MemberProxyRemove(var param, _) => ctx.Execute(MemberProxy, m => m.RemoveProxy(ctx, param.target, param.tag)), Commands.MemberProxySet(var param, _) => ctx.Execute(MemberProxy, m => m.SetProxy(ctx, param.target, param.tags)), @@ -205,7 +205,7 @@ public partial class CommandTree Commands.SwitchDo(var param, _) => ctx.Execute(Switch, m => m.SwitchDo(ctx, param.targets)), Commands.SwitchMove(var param, _) => ctx.Execute(SwitchMove, m => m.SwitchMove(ctx, param.@string)), Commands.SwitchEdit(var param, var flags) => ctx.Execute(SwitchEdit, m => m.SwitchEdit(ctx, param.targets, false, flags.first, flags.remove, flags.append, flags.prepend)), - Commands.SwitchEditOut(_, _) => ctx.Execute(SwitchEditOut, m => m.SwitchEditOut(ctx)), + Commands.SwitchEditOut(_, var flags) => ctx.Execute(SwitchEditOut, m => m.SwitchEditOut(ctx, flags.yes)), Commands.SwitchDelete(var param, var flags) => ctx.Execute(SwitchDelete, m => m.SwitchDelete(ctx, flags.all)), Commands.SwitchCopy(var param, var flags) => ctx.Execute(SwitchCopy, m => m.SwitchEdit(ctx, param.targets, true, flags.first, flags.remove, flags.append, flags.prepend)), Commands.SystemFronter(var param, var flags) => ctx.Execute(SystemFronter, m => m.Fronter(ctx, param.target)), @@ -213,7 +213,9 @@ public partial class CommandTree Commands.SystemFronterPercent(var param, var flags) => ctx.Execute(SystemFrontPercent, m => m.FrontPercent(ctx, param.target, flags.duration, flags.fronters_only, flags.flat)), Commands.SystemDisplayId(var param, _) => ctx.Execute(SystemId, m => m.DisplayId(ctx, param.target)), Commands.SystemDisplayIdSelf => ctx.Execute(SystemId, m => m.DisplayId(ctx, ctx.System)), - Commands.SystemWebhook => ctx.Execute(null, m => m.SystemWebhook(ctx)), + Commands.SystemWebhookShow => ctx.Execute(null, m => m.GetSystemWebhook(ctx)), + Commands.SystemWebhookClear(_, var flags) => ctx.Execute(null, m => m.ClearSystemWebhook(ctx, flags.yes)), + Commands.SystemWebhookSet(var param, _) => ctx.Execute(null, m => m.SetSystemWebhook(ctx, param.url)), Commands.RandomSelf(_, var flags) => flags.group ? ctx.Execute(GroupRandom, m => m.Group(ctx, ctx.System, flags.all, flags.show_embed)) @@ -224,21 +226,21 @@ public partial class CommandTree : ctx.Execute(MemberRandom, m => m.Member(ctx, param.target, flags.all, flags.show_embed)), Commands.GroupRandomMember(var param, var flags) => ctx.Execute(GroupMemberRandom, m => m.GroupMember(ctx, param.target, flags)), Commands.SystemLink(var param, _) => ctx.Execute(Link, m => m.LinkSystem(ctx, param.account)), - Commands.SystemUnlink(var param, _) => ctx.Execute(Unlink, m => m.UnlinkAccount(ctx, param.account)), + Commands.SystemUnlink(var param, var flags) => ctx.Execute(Unlink, m => m.UnlinkAccount(ctx, param.account, flags.yes)), Commands.SystemMembersListSelf(var param, var flags) => ctx.Execute(SystemList, m => m.MemberList(ctx, ctx.System, null, flags)), Commands.SystemMembersSearchSelf(var param, var flags) => ctx.Execute(SystemFind, m => m.MemberList(ctx, ctx.System, param.query, flags)), Commands.SystemMembersList(var param, var flags) => ctx.Execute(SystemList, m => m.MemberList(ctx, param.target, null, flags)), Commands.SystemMembersSearch(var param, var flags) => ctx.Execute(SystemFind, m => m.MemberList(ctx, param.target, param.query, flags)), - Commands.MemberListGroups(var param, var flags) => ctx.Execute(MemberGroups, m => m.ListMemberGroups(ctx, param.target, null, flags)), - Commands.MemberSearchGroups(var param, var flags) => ctx.Execute(MemberGroups, m => m.ListMemberGroups(ctx, param.target, param.query, flags)), + Commands.MemberListGroups(var param, var flags) => ctx.Execute(MemberGroups, m => m.ListMemberGroups(ctx, param.target, null, flags, flags.all)), + Commands.MemberSearchGroups(var param, var flags) => ctx.Execute(MemberGroups, m => m.ListMemberGroups(ctx, param.target, param.query, flags, flags.all)), Commands.GroupListMembers(var param, var flags) => ctx.Execute(GroupMemberList, m => m.ListGroupMembers(ctx, param.target, null, flags)), Commands.GroupSearchMembers(var param, var flags) => ctx.Execute(GroupMemberList, m => m.ListGroupMembers(ctx, param.target, param.query, flags)), - Commands.SystemListGroups(var param, var flags) => ctx.Execute(GroupList, g => g.ListSystemGroups(ctx, param.target, null, flags)), - Commands.SystemSearchGroups(var param, var flags) => ctx.Execute(GroupList, g => g.ListSystemGroups(ctx, param.target, param.query, flags)), - Commands.GroupListGroups(var param, var flags) => ctx.Execute(GroupList, g => g.ListSystemGroups(ctx, ctx.System, null, flags)), - Commands.GroupSearchGroups(var param, var flags) => ctx.Execute(GroupList, g => g.ListSystemGroups(ctx, ctx.System, param.query, flags)), + Commands.SystemListGroups(var param, var flags) => ctx.Execute(GroupList, g => g.ListSystemGroups(ctx, param.target, null, flags, flags.all)), + Commands.SystemSearchGroups(var param, var flags) => ctx.Execute(GroupList, g => g.ListSystemGroups(ctx, param.target, param.query, flags, flags.all)), + Commands.GroupListGroups(var param, var flags) => ctx.Execute(GroupList, g => g.ListSystemGroups(ctx, ctx.System, null, flags, flags.all)), + Commands.GroupSearchGroups(var param, var flags) => ctx.Execute(GroupList, g => g.ListSystemGroups(ctx, ctx.System, param.query, flags, flags.all)), Commands.GroupNew(var param, _) => ctx.Execute(GroupNew, g => g.CreateGroup(ctx, param.name)), - Commands.GroupInfo(var param, _) => ctx.Execute(GroupInfo, g => g.ShowGroupCard(ctx, param.target)), + Commands.GroupInfo(var param, var flags) => ctx.Execute(GroupInfo, g => g.ShowGroupCard(ctx, param.target, flags.show_embed, flags.all)), Commands.GroupShowName(var param, var flags) => ctx.Execute(GroupRename, g => g.ShowGroupDisplayName(ctx, param.target, flags.GetReplyFormat())), Commands.GroupClearName(var param, var flags) => ctx.Execute(GroupRename, g => g.RenameGroup(ctx, param.target, null)), Commands.GroupRename(var param, _) => ctx.Execute(GroupRename, g => g.RenameGroup(ctx, param.target, param.name)), @@ -249,16 +251,16 @@ public partial class CommandTree Commands.GroupClearDescription(var param, var flags) => ctx.Execute(GroupDesc, g => g.ClearGroupDescription(ctx, param.target)), Commands.GroupChangeDescription(var param, _) => ctx.Execute(GroupDesc, g => g.ChangeGroupDescription(ctx, param.target, param.description)), Commands.GroupShowIcon(var param, var flags) => ctx.Execute(GroupIcon, g => g.ShowGroupIcon(ctx, param.target, flags.GetReplyFormat())), - Commands.GroupClearIcon(var param, var flags) => ctx.Execute(GroupIcon, g => g.ClearGroupIcon(ctx, param.target)), + Commands.GroupClearIcon(var param, var flags) => ctx.Execute(GroupIcon, g => g.ClearGroupIcon(ctx, param.target, flags.yes)), Commands.GroupChangeIcon(var param, _) => ctx.Execute(GroupIcon, g => g.ChangeGroupIcon(ctx, param.target, param.icon)), Commands.GroupShowBanner(var param, var flags) => ctx.Execute(GroupBannerImage, g => g.ShowGroupBanner(ctx, param.target, flags.GetReplyFormat())), - Commands.GroupClearBanner(var param, var flags) => ctx.Execute(GroupBannerImage, g => g.ClearGroupBanner(ctx, param.target)), + Commands.GroupClearBanner(var param, var flags) => ctx.Execute(GroupBannerImage, g => g.ClearGroupBanner(ctx, param.target, flags.yes)), Commands.GroupChangeBanner(var param, _) => ctx.Execute(GroupBannerImage, g => g.ChangeGroupBanner(ctx, param.target, param.banner)), Commands.GroupShowColor(var param, var flags) => ctx.Execute(GroupColor, g => g.ShowGroupColor(ctx, param.target, flags.GetReplyFormat())), Commands.GroupClearColor(var param, var flags) => ctx.Execute(GroupColor, g => g.ClearGroupColor(ctx, param.target)), Commands.GroupChangeColor(var param, _) => ctx.Execute(GroupColor, g => g.ChangeGroupColor(ctx, param.target, param.color)), - Commands.GroupAddMember(var param, var flags) => ctx.Execute(GroupAdd, g => g.AddRemoveMembers(ctx, param.target, param.targets, Groups.AddRemoveOperation.Add, flags.all)), - Commands.GroupRemoveMember(var param, var flags) => ctx.Execute(GroupRemove, g => g.AddRemoveMembers(ctx, param.target, param.targets, Groups.AddRemoveOperation.Remove, flags.all)), + Commands.GroupAddMember(var param, var flags) => ctx.Execute(GroupAdd, g => g.AddRemoveMembers(ctx, param.target, param.targets, Groups.AddRemoveOperation.Add, flags.all, flags.yes)), + Commands.GroupRemoveMember(var param, var flags) => ctx.Execute(GroupRemove, g => g.AddRemoveMembers(ctx, param.target, param.targets, Groups.AddRemoveOperation.Remove, flags.all, flags.yes)), Commands.GroupShowPrivacy(var param, _) => ctx.Execute(GroupPrivacy, g => g.ShowGroupPrivacy(ctx, param.target)), Commands.GroupChangePrivacyAll(var param, _) => ctx.Execute(GroupPrivacy, g => g.SetAllGroupPrivacy(ctx, param.target, param.level)), Commands.GroupChangePrivacy(var param, _) => ctx.Execute(GroupPrivacy, g => g.SetGroupPrivacy(ctx, param.target, param.privacy, param.level)), @@ -282,12 +284,12 @@ public partial class CommandTree Commands.MessageDelete(var param, var flags) => ctx.Execute(Message, m => m.GetMessage(ctx, param.target.MessageId, flags.GetReplyFormat(), true, false)), Commands.MessageEdit(var param, var flags) => ctx.Execute(MessageEdit, m => m.EditMessage(ctx, param.target.MessageId, param.new_content, flags.regex, flags.mutate_space, flags.append, flags.prepend, flags.clear_embeds, flags.clear_attachments)), Commands.MessageReproxy(var param, _) => ctx.Execute(MessageReproxy, m => m.ReproxyMessage(ctx, param.msg.MessageId, param.member)), - Commands.Import(var param, _) => ctx.Execute(Import, m => m.Import(ctx, param.url)), + Commands.Import(var param, var flags) => ctx.Execute(Import, m => m.Import(ctx, param.url, flags.yes)), Commands.Export(_, _) => ctx.Execute(Export, m => m.Export(ctx)), Commands.ServerConfigShow => ctx.Execute(null, m => m.ShowConfig(ctx)), Commands.ServerConfigLogChannelShow => ctx.Execute(null, m => m.ShowLogChannel(ctx)), Commands.ServerConfigLogChannelSet(var param, _) => ctx.Execute(null, m => m.SetLogChannel(ctx, param.channel)), - Commands.ServerConfigLogChannelClear => ctx.Execute(null, m => m.ClearLogChannel(ctx)), + Commands.ServerConfigLogChannelClear(_, var flags) => ctx.Execute(null, m => m.ClearLogChannel(ctx, flags.yes)), Commands.ServerConfigLogCleanupShow => ctx.Execute(null, m => m.ShowLogCleanup(ctx)), Commands.ServerConfigLogCleanupSet(var param, _) => ctx.Execute(null, m => m.SetLogCleanup(ctx, param.toggle)), Commands.ServerConfigLogBlacklistShow => ctx.Execute(null, m => m.ShowLogBlacklist(ctx)), @@ -302,27 +304,27 @@ public partial class CommandTree Commands.ServerConfigRequireSystemTagSet(var param, _) => ctx.Execute(null, m => m.SetRequireSystemTag(ctx, param.toggle)), Commands.ServerConfigSuppressNotificationsShow => ctx.Execute(null, m => m.ShowSuppressNotifications(ctx)), Commands.ServerConfigSuppressNotificationsSet(var param, _) => ctx.Execute(null, m => m.SetSuppressNotifications(ctx, param.toggle)), - Commands.AdminUpdateSystemId(var param, _) => ctx.Execute(null, m => m.UpdateSystemId(ctx, param.target, param.new_hid)), - Commands.AdminUpdateMemberId(var param, _) => ctx.Execute(null, m => m.UpdateMemberId(ctx, param.target, param.new_hid)), - Commands.AdminUpdateGroupId(var param, _) => ctx.Execute(null, m => m.UpdateGroupId(ctx, param.target, param.new_hid)), - Commands.AdminRerollSystemId(var param, _) => ctx.Execute(null, m => m.RerollSystemId(ctx, param.target)), - Commands.AdminRerollMemberId(var param, _) => ctx.Execute(null, m => m.RerollMemberId(ctx, param.target)), - Commands.AdminRerollGroupId(var param, _) => ctx.Execute(null, m => m.RerollGroupId(ctx, param.target)), - Commands.AdminSystemMemberLimit(var param, _) => ctx.Execute(null, m => m.SystemMemberLimit(ctx, param.target, param.limit)), - Commands.AdminSystemGroupLimit(var param, _) => ctx.Execute(null, m => m.SystemGroupLimit(ctx, param.target, param.limit)), - Commands.AdminSystemRecover(var param, var flags) => ctx.Execute(null, m => m.SystemRecover(ctx, param.token, param.account, flags.reroll_token)), + Commands.AdminUpdateSystemId(var param, var flags) => ctx.Execute(null, m => m.UpdateSystemId(ctx, param.target, param.new_hid, flags.yes)), + Commands.AdminUpdateMemberId(var param, var flags) => ctx.Execute(null, m => m.UpdateMemberId(ctx, param.target, param.new_hid, flags.yes)), + Commands.AdminUpdateGroupId(var param, var flags) => ctx.Execute(null, m => m.UpdateGroupId(ctx, param.target, param.new_hid, flags.yes)), + Commands.AdminRerollSystemId(var param, var flags) => ctx.Execute(null, m => m.RerollSystemId(ctx, param.target, flags.yes)), + Commands.AdminRerollMemberId(var param, var flags) => ctx.Execute(null, m => m.RerollMemberId(ctx, param.target, flags.yes)), + Commands.AdminRerollGroupId(var param, var flags) => ctx.Execute(null, m => m.RerollGroupId(ctx, param.target, flags.yes)), + Commands.AdminSystemMemberLimit(var param, var flags) => ctx.Execute(null, m => m.SystemMemberLimit(ctx, param.target, param.limit, flags.yes)), + Commands.AdminSystemGroupLimit(var param, var flags) => ctx.Execute(null, m => m.SystemGroupLimit(ctx, param.target, param.limit, flags.yes)), + Commands.AdminSystemRecover(var param, var flags) => ctx.Execute(null, m => m.SystemRecover(ctx, param.token, param.account, flags.reroll_token, flags.yes)), Commands.AdminSystemDelete(var param, _) => ctx.Execute(null, m => m.SystemDelete(ctx, param.target)), Commands.AdminSendMessage(var param, _) => ctx.Execute(null, m => m.SendAdminMessage(ctx, param.account, param.content)), Commands.AdminAbuselogCreate(var param, var flags) => ctx.Execute(null, m => m.AbuseLogCreate(ctx, param.account, flags.deny_boy_usage, param.description)), Commands.AdminAbuselogShowAccount(var param, _) => ctx.Execute(null, m => m.AbuseLogShow(ctx, param.account, null)), Commands.AdminAbuselogFlagDenyAccount(var param, _) => ctx.Execute(null, m => m.AbuseLogFlagDeny(ctx, param.account, null, param.value)), - Commands.AdminAbuselogDescriptionAccount(var param, var flags) => ctx.Execute(null, m => m.AbuseLogDescription(ctx, param.account, null, param.desc, flags.clear)), + Commands.AdminAbuselogDescriptionAccount(var param, var flags) => ctx.Execute(null, m => m.AbuseLogDescription(ctx, param.account, null, param.desc, flags.clear, flags.yes)), Commands.AdminAbuselogAddUserAccount(var param, _) => ctx.Execute(null, m => m.AbuseLogAddUser(ctx, param.account, null, ctx.Author)), Commands.AdminAbuselogRemoveUserAccount(var param, _) => ctx.Execute(null, m => m.AbuseLogRemoveUser(ctx, param.account, null, ctx.Author)), Commands.AdminAbuselogDeleteAccount(var param, _) => ctx.Execute(null, m => m.AbuseLogDelete(ctx, param.account, null)), Commands.AdminAbuselogShowLogId(var param, _) => ctx.Execute(null, m => m.AbuseLogShow(ctx, null, param.log_id)), Commands.AdminAbuselogFlagDenyLogId(var param, _) => ctx.Execute(null, m => m.AbuseLogFlagDeny(ctx, null, param.log_id, param.value)), - Commands.AdminAbuselogDescriptionLogId(var param, var flags) => ctx.Execute(null, m => m.AbuseLogDescription(ctx, null, param.log_id, param.desc, flags.clear)), + Commands.AdminAbuselogDescriptionLogId(var param, var flags) => ctx.Execute(null, m => m.AbuseLogDescription(ctx, null, param.log_id, param.desc, flags.clear, flags.yes)), Commands.AdminAbuselogAddUserLogId(var param, _) => ctx.Execute(null, m => m.AbuseLogAddUser(ctx, null, param.log_id, ctx.Author)), Commands.AdminAbuselogRemoveUserLogId(var param, _) => ctx.Execute(null, m => m.AbuseLogRemoveUser(ctx, null, param.log_id, ctx.Author)), Commands.AdminAbuselogDeleteLogId(var param, _) => ctx.Execute(null, m => m.AbuseLogDelete(ctx, null, param.log_id)), diff --git a/PluralKit.Bot/CommandSystem/Context/ContextArgumentsExt.cs b/PluralKit.Bot/CommandSystem/Context/ContextArgumentsExt.cs index 48006cbb..730b4e8d 100644 --- a/PluralKit.Bot/CommandSystem/Context/ContextArgumentsExt.cs +++ b/PluralKit.Bot/CommandSystem/Context/ContextArgumentsExt.cs @@ -2,110 +2,10 @@ using System.Text.RegularExpressions; using Myriad.Types; -using PluralKit.Core; - namespace PluralKit.Bot; public static class ContextArgumentsExt { - public static string PopArgument(this Context ctx) => throw new PKError("todo: PopArgument"); - - public static string PeekArgument(this Context ctx) => throw new PKError("todo: PeekArgument"); - - public static string RemainderOrNull(this Context ctx, bool skipFlags = true) => throw new PKError("todo: RemainderOrNull"); - - public static bool HasNext(this Context ctx, bool skipFlags = true) => throw new PKError("todo: HasNext"); - - public static string FullCommand(this Context ctx) => throw new PKError("todo: FullCommand"); - - /// - /// Checks if the next parameter is equal to one of the given keywords and pops it from the stack. Case-insensitive. - /// - public static bool Match(this Context ctx, ref string used, params string[] potentialMatches) - { - var arg = ctx.PeekArgument(); - foreach (var match in potentialMatches) - if (arg.Equals(match, StringComparison.InvariantCultureIgnoreCase)) - { - used = ctx.PopArgument(); - return true; - } - - return false; - } - - /// - /// Checks if the next parameter is equal to one of the given keywords. Case-insensitive. - /// - public static bool Match(this Context ctx, params string[] potentialMatches) - { - string used = null; // Unused and unreturned, we just yeet it - return ctx.Match(ref used, potentialMatches); - } - - /// - /// Checks if the next parameter (starting from `ptr`) is equal to one of the given keywords, and leaves it on the stack. Case-insensitive. - /// - public static bool PeekMatch(this Context ctx, ref int ptr, string[] potentialMatches) - { - throw new PKError("todo: PeekMatch"); - } - - /// - /// Matches the next *n* parameters against each parameter consecutively. - ///
- /// Note that this is handled differently than single-parameter Match: - /// each method parameter is an array of potential matches for the *n*th command string parameter. - ///
- public static bool MatchMultiple(this Context ctx, params string[][] potentialParametersMatches) - { - throw new PKError("todo: MatchMultiple"); - } - - public static bool MatchFlag(this Context ctx, params string[] potentialMatches) - { - // Flags are *ALWAYS PARSED LOWERCASE*. This means we skip out on a "ToLower" call here. - // Can assume the caller array only contains lowercase *and* the set below only contains lowercase - throw new NotImplementedException(); - } - - public static bool MatchClear(this Context ctx) - => ctx.Match("clear", "reset", "default") || ctx.MatchFlag("c", "clear"); - - 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 ReplyFormat PeekMatchFormat(this Context ctx) - { - throw new PKError("todo: PeekMatchFormat"); - } - - public static bool MatchToggle(this Context ctx, bool? defaultValue = null) - { - var value = ctx.MatchToggleOrNull(defaultValue); - if (value == null) throw new PKError("You must pass either \"on\" or \"off\" to this command."); - return value.Value; - } - - public static bool? MatchToggleOrNull(this Context ctx, bool? defaultValue = null) - { - if (defaultValue != null && ctx.MatchClear()) - return defaultValue.Value; - - var yesToggles = new[] { "yes", "on", "enable", "enabled", "true" }; - var noToggles = new[] { "no", "off", "disable", "disabled", "false" }; - - if (ctx.Match(yesToggles) || ctx.MatchFlag(yesToggles)) - return true; - else if (ctx.Match(noToggles) || ctx.MatchFlag(noToggles)) - return false; - else return null; - } - public static (ulong? messageId, ulong? channelId) GetRepliedTo(this Context ctx) { if (ctx.Message.Type == Message.MessageType.Reply && ctx.Message.MessageReference?.MessageId != null) diff --git a/PluralKit.Bot/CommandSystem/Context/ContextAvatarExt.cs b/PluralKit.Bot/CommandSystem/Context/ContextAvatarExt.cs index 0c99c94f..21d24c4f 100644 --- a/PluralKit.Bot/CommandSystem/Context/ContextAvatarExt.cs +++ b/PluralKit.Bot/CommandSystem/Context/ContextAvatarExt.cs @@ -48,10 +48,6 @@ public static class ContextAvatarExt // ToString URL-decodes, which breaks URLs to spaces; AbsoluteUri doesn't return new ParsedImage { Url = uri.AbsoluteUri, Source = AvatarSource.Url }; } - public static async Task MatchImage(this Context ctx) - { - throw new NotImplementedException(); - } } public struct ParsedImage diff --git a/PluralKit.Bot/CommandSystem/Context/ContextEntityArgumentsExt.cs b/PluralKit.Bot/CommandSystem/Context/ContextEntityArgumentsExt.cs index 627c1185..5c429c96 100644 --- a/PluralKit.Bot/CommandSystem/Context/ContextEntityArgumentsExt.cs +++ b/PluralKit.Bot/CommandSystem/Context/ContextEntityArgumentsExt.cs @@ -1,9 +1,6 @@ -using System.Text.RegularExpressions; - using Myriad.Extensions; using Myriad.Types; -using PluralKit.Bot.Utils; using PluralKit.Core; namespace PluralKit.Bot; @@ -120,23 +117,4 @@ public static class ContextEntityArgumentsExt return $"{entity} with ID or name \"{input}\" not found."; return $"{entity} with name \"{input}\" not found. Note that a {entity.ToLower()} ID is 5 or 6 characters long."; } - - public static async Task MatchChannel(this Context ctx) - { - if (!MentionUtils.TryParseChannel(ctx.PeekArgument(), out var id)) - return null; - - // todo: match channels in other guilds - var channel = await ctx.Cache.TryGetChannel(ctx.Guild!.Id, id); - if (channel == null) - channel = await ctx.Rest.GetChannelOrNull(id); - if (channel == null) - return null; - - if (!DiscordUtils.IsValidGuildChannel(channel)) - return null; - - ctx.PopArgument(); - return channel; - } } \ No newline at end of file diff --git a/PluralKit.Bot/CommandSystem/Context/ContextPrivacyExt.cs b/PluralKit.Bot/CommandSystem/Context/ContextPrivacyExt.cs deleted file mode 100644 index 2f120f8a..00000000 --- a/PluralKit.Bot/CommandSystem/Context/ContextPrivacyExt.cs +++ /dev/null @@ -1,51 +0,0 @@ -using PluralKit.Core; - -namespace PluralKit.Bot; - -public static class ContextPrivacyExt -{ - public static PrivacyLevel PopPrivacyLevel(this Context ctx) - { - if (ctx.Match("public", "pub", "show", "shown", "visible", "unhide", "unhidden")) - return PrivacyLevel.Public; - - if (ctx.Match("private", "priv", "hide", "hidden")) - return PrivacyLevel.Private; - - if (!ctx.HasNext()) - throw new PKSyntaxError("You must pass a privacy level (`public` or `private`)"); - - throw new PKSyntaxError( - $"Invalid privacy level {ctx.PopArgument().AsCode()} (must be `public` or `private`)."); - } - - public static SystemPrivacySubject PopSystemPrivacySubject(this Context ctx) - { - if (!SystemPrivacyUtils.TryParseSystemPrivacy(ctx.PeekArgument(), out var subject)) - throw new PKSyntaxError( - $"Invalid privacy subject {ctx.PopArgument().AsCode()} (must be `description`, `members`, `front`, `fronthistory`, `groups`, or `all`)."); - - ctx.PopArgument(); - return subject; - } - - public static MemberPrivacySubject PopMemberPrivacySubject(this Context ctx) - { - if (!MemberPrivacyUtils.TryParseMemberPrivacy(ctx.PeekArgument(), out var subject)) - throw new PKSyntaxError( - $"Invalid privacy subject {ctx.PopArgument().AsCode()} (must be `name`, `description`, `avatar`, `birthday`, `pronouns`, `proxy`, `metadata`, `visibility`, or `all`)."); - - ctx.PopArgument(); - return subject; - } - - public static GroupPrivacySubject PopGroupPrivacySubject(this Context ctx) - { - if (!GroupPrivacyUtils.TryParseGroupPrivacy(ctx.PeekArgument(), out var subject)) - throw new PKSyntaxError( - $"Invalid privacy subject {ctx.PopArgument().AsCode()} (must be `name`, `description`, `icon`, `metadata`, `visibility`, or `all`)."); - - ctx.PopArgument(); - return subject; - } -} \ No newline at end of file diff --git a/PluralKit.Bot/CommandSystem/Parameters.cs b/PluralKit.Bot/CommandSystem/Parameters.cs index 1add6fe2..0278f661 100644 --- a/PluralKit.Bot/CommandSystem/Parameters.cs +++ b/PluralKit.Bot/CommandSystem/Parameters.cs @@ -72,7 +72,7 @@ public class Parameters private async Task ResolveFfiParam(Context ctx, uniffi.commands.Parameter ffi_param) { - var byId = HasFlag("id", "by-id"); + var byId = HasFlag("id", "by-id"); // this is added as a hidden flag to all command definitions switch (ffi_param) { case uniffi.commands.Parameter.MemberRef memberRef: diff --git a/PluralKit.Bot/Commands/Admin.cs b/PluralKit.Bot/Commands/Admin.cs index e65e6bc1..7726466b 100644 --- a/PluralKit.Bot/Commands/Admin.cs +++ b/PluralKit.Bot/Commands/Admin.cs @@ -111,7 +111,7 @@ public class Admin return eb.Build(); } - public async Task UpdateSystemId(Context ctx, PKSystem target, string newHid) + public async Task UpdateSystemId(Context ctx, PKSystem target, string newHid, bool confirmYes) { ctx.AssertBotAdmin(); @@ -121,14 +121,14 @@ public class Admin await ctx.Reply(null, await CreateEmbed(ctx, target)); - if (!await ctx.PromptYesNo($"Change system ID of `{target.Hid}` to `{newHid}`?", "Change")) + if (!await ctx.PromptYesNo($"Change system ID of `{target.Hid}` to `{newHid}`?", "Change", flagValue: confirmYes)) throw new PKError("ID change cancelled."); await ctx.Repository.UpdateSystem(target.Id, new SystemPatch { Hid = newHid }); await ctx.Reply($"{Emojis.Success} System ID updated (`{target.Hid}` -> `{newHid}`)."); } - public async Task UpdateMemberId(Context ctx, PKMember target, string newHid) + public async Task UpdateMemberId(Context ctx, PKMember target, string newHid, bool confirmYes) { ctx.AssertBotAdmin(); @@ -141,7 +141,7 @@ public class Admin if (!await ctx.PromptYesNo( $"Change member ID of **{target.NameFor(LookupContext.ByNonOwner)}** (`{target.Hid}`) to `{newHid}`?", - "Change" + "Change", flagValue: confirmYes )) throw new PKError("ID change cancelled."); @@ -149,7 +149,7 @@ public class Admin await ctx.Reply($"{Emojis.Success} Member ID updated (`{target.Hid}` -> `{newHid}`)."); } - public async Task UpdateGroupId(Context ctx, PKGroup target, string newHid) + public async Task UpdateGroupId(Context ctx, PKGroup target, string newHid, bool confirmYes) { ctx.AssertBotAdmin(); @@ -161,7 +161,7 @@ public class Admin await ctx.Reply(null, await CreateEmbed(ctx, system)); if (!await ctx.PromptYesNo($"Change group ID of **{target.Name}** (`{target.Hid}`) to `{newHid}`?", - "Change" + "Change", flagValue: confirmYes )) throw new PKError("ID change cancelled."); @@ -169,13 +169,13 @@ public class Admin await ctx.Reply($"{Emojis.Success} Group ID updated (`{target.Hid}` -> `{newHid}`)."); } - public async Task RerollSystemId(Context ctx, PKSystem target) + public async Task RerollSystemId(Context ctx, PKSystem target, bool confirmYes) { ctx.AssertBotAdmin(); await ctx.Reply(null, await CreateEmbed(ctx, target)); - if (!await ctx.PromptYesNo($"Reroll system ID `{target.Hid}`?", "Reroll")) + if (!await ctx.PromptYesNo($"Reroll system ID `{target.Hid}`?", "Reroll", flagValue: confirmYes)) throw new PKError("ID change cancelled."); var query = new Query("systems").AsUpdate(new @@ -188,7 +188,7 @@ public class Admin await ctx.Reply($"{Emojis.Success} System ID updated (`{target.Hid}` -> `{newHid}`)."); } - public async Task RerollMemberId(Context ctx, PKMember target) + public async Task RerollMemberId(Context ctx, PKMember target, bool confirmYes) { ctx.AssertBotAdmin(); @@ -197,7 +197,7 @@ public class Admin if (!await ctx.PromptYesNo( $"Reroll member ID for **{target.NameFor(LookupContext.ByNonOwner)}** (`{target.Hid}`)?", - "Reroll" + "Reroll", flagValue: confirmYes )) throw new PKError("ID change cancelled."); @@ -211,7 +211,7 @@ public class Admin await ctx.Reply($"{Emojis.Success} Member ID updated (`{target.Hid}` -> `{newHid}`)."); } - public async Task RerollGroupId(Context ctx, PKGroup target) + public async Task RerollGroupId(Context ctx, PKGroup target, bool confirmYes) { ctx.AssertBotAdmin(); @@ -219,7 +219,7 @@ public class Admin await ctx.Reply(null, await CreateEmbed(ctx, system)); if (!await ctx.PromptYesNo($"Reroll group ID for **{target.Name}** (`{target.Hid}`)?", - "Change" + "Change", flagValue: confirmYes )) throw new PKError("ID change cancelled."); @@ -233,7 +233,7 @@ public class Admin await ctx.Reply($"{Emojis.Success} Group ID updated (`{target.Hid}` -> `{newHid}`)."); } - public async Task SystemMemberLimit(Context ctx, PKSystem target, int? newLimit) + public async Task SystemMemberLimit(Context ctx, PKSystem target, int? newLimit, bool confirmYes) { ctx.AssertBotAdmin(); @@ -247,14 +247,14 @@ public class Admin } await ctx.Reply(null, await CreateEmbed(ctx, target)); - if (!await ctx.PromptYesNo($"Update member limit from **{currentLimit}** to **{newLimit}**?", "Update")) + if (!await ctx.PromptYesNo($"Update member limit from **{currentLimit}** to **{newLimit}**?", "Update", flagValue: confirmYes)) throw new PKError("Member limit change cancelled."); await ctx.Repository.UpdateSystemConfig(target.Id, new SystemConfigPatch { MemberLimitOverride = newLimit }); await ctx.Reply($"{Emojis.Success} Member limit updated."); } - public async Task SystemGroupLimit(Context ctx, PKSystem target, int? newLimit) + public async Task SystemGroupLimit(Context ctx, PKSystem target, int? newLimit, bool confirmYes) { ctx.AssertBotAdmin(); @@ -268,14 +268,14 @@ public class Admin } await ctx.Reply(null, await CreateEmbed(ctx, target)); - if (!await ctx.PromptYesNo($"Update group limit from **{currentLimit}** to **{newLimit}**?", "Update")) + if (!await ctx.PromptYesNo($"Update group limit from **{currentLimit}** to **{newLimit}**?", "Update", flagValue: confirmYes)) throw new PKError("Group limit change cancelled."); await ctx.Repository.UpdateSystemConfig(target.Id, new SystemConfigPatch { GroupLimitOverride = newLimit }); await ctx.Reply($"{Emojis.Success} Group limit updated."); } - public async Task SystemRecover(Context ctx, string systemToken, User account, bool rerollToken) + public async Task SystemRecover(Context ctx, string systemToken, User account, bool rerollToken, bool confirmYes) { ctx.AssertBotAdmin(); @@ -294,7 +294,7 @@ public class Admin var system = await ctx.Repository.GetSystem(systemId.Value!); await ctx.Reply(null, await CreateEmbed(ctx, system)); - if (!await ctx.PromptYesNo($"Associate account {account.NameAndMention()} with system `{system.Hid}`?", "Recover account")) + if (!await ctx.PromptYesNo($"Associate account {account.NameAndMention()} with system `{system.Hid}`?", "Recover account", flagValue: confirmYes)) throw new PKError("System recovery cancelled."); await ctx.Repository.AddAccount(system.Id, account.Id); @@ -402,7 +402,7 @@ public class Admin } } - public async Task AbuseLogDescription(Context ctx, User? account, string? id, string? description, bool clear) + public async Task AbuseLogDescription(Context ctx, User? account, string? id, string? description, bool clear, bool confirmClear) { ctx.AssertBotAdmin(); @@ -410,7 +410,7 @@ public class Admin if (abuseLog == null) return; - if (clear && await ctx.ConfirmClear("this abuse log description")) + if (clear && await ctx.ConfirmClear("this abuse log description", confirmClear)) { await ctx.Repository.UpdateAbuseLog(abuseLog.Id, new AbuseLogPatch { Description = null }); await ctx.Reply($"{Emojis.Success} Abuse log description cleared."); diff --git a/PluralKit.Bot/Commands/Api.cs b/PluralKit.Bot/Commands/Api.cs index 626bcd4a..380e8cf1 100644 --- a/PluralKit.Bot/Commands/Api.cs +++ b/PluralKit.Bot/Commands/Api.cs @@ -115,28 +115,32 @@ public class Api } } - public async Task SystemWebhook(Context ctx) + public async Task GetSystemWebhook(Context ctx) { ctx.CheckSystem().CheckDMContext(); - if (!ctx.HasNext(false)) - { - if (ctx.System.WebhookUrl == null) - await ctx.Reply($"Your system does not have a webhook URL set. Set one with `{ctx.DefaultPrefix}system webhook `!"); - else - await ctx.Reply($"Your system's webhook URL is <{ctx.System.WebhookUrl}>."); + if (ctx.System.WebhookUrl == null) + await ctx.Reply($"Your system does not have a webhook URL set. Set one with `{ctx.DefaultPrefix}system webhook `!"); + else + await ctx.Reply($"Your system's webhook URL is <{ctx.System.WebhookUrl}>."); + } + + public async Task ClearSystemWebhook(Context ctx, bool confirmYes) + { + ctx.CheckSystem().CheckDMContext(); + + if (!await ctx.ConfirmClear("your system's webhook URL", confirmYes)) return; - } - if (ctx.MatchClear() && await ctx.ConfirmClear("your system's webhook URL")) - { - await ctx.Repository.UpdateSystem(ctx.System.Id, new SystemPatch { WebhookUrl = null, WebhookToken = null }); + await ctx.Repository.UpdateSystem(ctx.System.Id, new SystemPatch { WebhookUrl = null, WebhookToken = null }); - await ctx.Reply($"{Emojis.Success} System webhook URL removed."); - return; - } + await ctx.Reply($"{Emojis.Success} System webhook URL removed."); + } + + public async Task SetSystemWebhook(Context ctx, string newUrl) + { + ctx.CheckSystem().CheckDMContext(); - var newUrl = ctx.RemainderOrNull(); if (!await DispatchExt.ValidateUri(newUrl)) throw new PKError($"The URL {newUrl.AsCode()} is invalid or I cannot access it. Are you sure this is a valid, publicly accessible URL?"); diff --git a/PluralKit.Bot/Commands/Config.cs b/PluralKit.Bot/Commands/Config.cs index ff34e3d8..9d8b175c 100644 --- a/PluralKit.Bot/Commands/Config.cs +++ b/PluralKit.Bot/Commands/Config.cs @@ -290,7 +290,7 @@ public class Config await ctx.Reply($"{Emojis.Success} System time zone cleared (set to UTC)."); } - public async Task EditSystemTimezone(Context ctx, string zoneStr) + public async Task EditSystemTimezone(Context ctx, string zoneStr, bool confirmYes = false) { if (ctx.System == null) throw Errors.NoSystemError(ctx.DefaultPrefix); @@ -299,7 +299,7 @@ public class Config var currentTime = SystemClock.Instance.GetCurrentInstant().InZone(zone); var msg = $"This will change the system time zone to **{zone.Id}**. The current time is **{currentTime.FormatZoned()}**. Is this correct?"; - if (!await ctx.PromptYesNo(msg, "Change Timezone")) throw Errors.TimezoneChangeCancelled; + if (!await ctx.PromptYesNo(msg, "Change Timezone", flagValue: confirmYes)) throw Errors.TimezoneChangeCancelled; await ctx.Repository.UpdateSystemConfig(ctx.System.Id, new() { UiTz = zone.Id }); diff --git a/PluralKit.Bot/Commands/Fun.cs b/PluralKit.Bot/Commands/Fun.cs index 9763f186..609fffd5 100644 --- a/PluralKit.Bot/Commands/Fun.cs +++ b/PluralKit.Bot/Commands/Fun.cs @@ -37,20 +37,16 @@ public class Fun public Task Meow(Context ctx) => ctx.Reply("*mrrp :3*"); - public Task Error(Context ctx) - { - if (ctx.Match("message")) - return ctx.Reply("> **Error code:** `50f3c7b439d111ecab2023a5431fffbd`", new EmbedBuilder() - .Color(0xE74C3C) - .Title("Internal error occurred") - .Description( - "For support, please send the error code above in **#bug-reports-and-errors** on **[the support server *(click to join)*](https://discord.gg/PczBt78)** with a description of what you were doing at the time.") - .Footer(new Embed.EmbedFooter("50f3c7b439d111ecab2023a5431fffbd")) - .Timestamp(SystemClock.Instance.GetCurrentInstant().ToDateTimeOffset().ToString("O")) - .Build() - ); + public Task ErrorMessage(Context ctx) => ctx.Reply("> **Error code:** `50f3c7b439d111ecab2023a5431fffbd`", new EmbedBuilder() + .Color(0xE74C3C) + .Title("Internal error occurred") + .Description( + "For support, please send the error code above in **#bug-reports-and-errors** on **[the support server *(click to join)*](https://discord.gg/PczBt78)** with a description of what you were doing at the time.") + .Footer(new Embed.EmbedFooter("50f3c7b439d111ecab2023a5431fffbd")) + .Timestamp(SystemClock.Instance.GetCurrentInstant().ToDateTimeOffset().ToString("O")) + .Build() + ); - return ctx.Reply( - $"{Emojis.Error} Unknown command {"error".AsCode()}. For a list of possible commands, see ."); - } + public Task Error(Context ctx) => ctx.Reply( + $"{Emojis.Error} Unknown command {"error".AsCode()}. For a list of possible commands, see ."); } \ No newline at end of file diff --git a/PluralKit.Bot/Commands/GroupMember.cs b/PluralKit.Bot/Commands/GroupMember.cs index 69a9b269..a53bdd37 100644 --- a/PluralKit.Bot/Commands/GroupMember.cs +++ b/PluralKit.Bot/Commands/GroupMember.cs @@ -51,7 +51,7 @@ public class GroupMember groups.Count - toAction.Count)); } - public async Task ListMemberGroups(Context ctx, PKMember target, string? query, IHasListOptions flags) + public async Task ListMemberGroups(Context ctx, PKMember target, string? query, IHasListOptions flags, bool all) { var targetSystem = await ctx.Repository.GetSystem(target.System); var opts = flags.GetListOptions(ctx, target.System); @@ -80,10 +80,10 @@ public class GroupMember title.Append($" matching **{opts.Search.Truncate(100)}**"); await ctx.RenderGroupList(ctx.LookupContextFor(target.System), target.System, title.ToString(), - target.Color, opts); + target.Color, opts, all); } - public async Task AddRemoveMembers(Context ctx, PKGroup target, List _members, Groups.AddRemoveOperation op, bool all) + public async Task AddRemoveMembers(Context ctx, PKGroup target, List _members, Groups.AddRemoveOperation op, bool all, bool confirmYes) { ctx.CheckOwnGroup(target); @@ -126,7 +126,7 @@ public class GroupMember .Where(m => existingMembersInGroup.Contains(m.Value)) .ToList(); - if (all && !await ctx.PromptYesNo($"Are you sure you want to remove all members from group {target.Reference(ctx)}?", "Empty Group")) throw Errors.GenericCancelled(); + if (all && !await ctx.PromptYesNo($"Are you sure you want to remove all members from group {target.Reference(ctx)}?", "Empty Group", flagValue: confirmYes)) throw Errors.GenericCancelled(); await ctx.Repository.RemoveMembersFromGroup(target.Id, toAction); } diff --git a/PluralKit.Bot/Commands/Groups.cs b/PluralKit.Bot/Commands/Groups.cs index a405f527..aa2d67b9 100644 --- a/PluralKit.Bot/Commands/Groups.cs +++ b/PluralKit.Bot/Commands/Groups.cs @@ -32,7 +32,7 @@ public class Groups _avatarHosting = avatarHosting; } - public async Task CreateGroup(Context ctx, string groupName) + public async Task CreateGroup(Context ctx, string groupName, bool confirmYes = false) { ctx.CheckSystem(); @@ -53,7 +53,7 @@ public class Groups { var msg = $"{Emojis.Warn} You already have a group in your system with the name \"{existingGroup.Name}\" (with ID `{existingGroup.DisplayHid(ctx.Config)}`). Do you want to create another group with the same name?"; - if (!await ctx.PromptYesNo(msg, "Create")) + if (!await ctx.PromptYesNo(msg, "Create", flagValue: confirmYes)) throw new PKError("Group creation cancelled."); } @@ -98,7 +98,7 @@ public class Groups await ctx.Reply(replyStr, eb.Build()); } - public async Task RenameGroup(Context ctx, PKGroup target, string newName) + public async Task RenameGroup(Context ctx, PKGroup target, string? newName, bool confirmYes = false) { ctx.CheckOwnGroup(target); @@ -113,7 +113,7 @@ public class Groups { var msg = $"{Emojis.Warn} You already have a group in your system with the name \"{existingGroup.Name}\" (with ID `{existingGroup.DisplayHid(ctx.Config)}`). Do you want to rename this group to that name too?"; - if (!await ctx.PromptYesNo(msg, "Rename")) + if (!await ctx.PromptYesNo(msg, "Rename", flagValue: confirmYes)) throw new PKError("Group rename cancelled."); } @@ -320,10 +320,10 @@ public class Groups await ctx.Reply(embed: ebS.Build()); } - public async Task ClearGroupIcon(Context ctx, PKGroup target) + public async Task ClearGroupIcon(Context ctx, PKGroup target, bool confirmYes) { ctx.CheckOwnGroup(target); - await ctx.ConfirmClear("this group's icon"); + await ctx.ConfirmClear("this group's icon", confirmYes); await ctx.Repository.UpdateGroup(target.Id, new GroupPatch { Icon = null }); await ctx.Reply($"{Emojis.Success} Group icon cleared."); @@ -400,10 +400,10 @@ public class Groups await ctx.Reply(embed: ebS.Build()); } - public async Task ClearGroupBanner(Context ctx, PKGroup target) + public async Task ClearGroupBanner(Context ctx, PKGroup target, bool confirmYes) { ctx.CheckOwnGroup(target); - await ctx.ConfirmClear("this group's banner image"); + await ctx.ConfirmClear("this group's banner image", confirmYes); await ctx.Repository.UpdateGroup(target.Id, new GroupPatch { BannerImage = null }); await ctx.Reply($"{Emojis.Success} Group banner image cleared."); @@ -506,7 +506,7 @@ public class Groups files: [MiscUtils.GenerateColorPreview(color)]); } - public async Task ListSystemGroups(Context ctx, PKSystem system, string? query, IHasListOptions flags) + public async Task ListSystemGroups(Context ctx, PKSystem system, string? query, IHasListOptions flags, bool all) { if (system == null) { @@ -528,7 +528,8 @@ public class Groups system.Id, GetEmbedTitle(ctx, system, opts), system.Color, - opts + opts, + all ); } @@ -547,16 +548,16 @@ public class Groups return title.ToString(); } - public async Task ShowGroupCard(Context ctx, PKGroup target, bool showEmbed = false) + public async Task ShowGroupCard(Context ctx, PKGroup target, bool showEmbed, bool all) { var system = await GetGroupSystem(ctx, target); if (showEmbed) { - await ctx.Reply(text: EmbedService.LEGACY_EMBED_WARNING, embed: await _embeds.CreateGroupEmbed(ctx, system, target)); + await ctx.Reply(text: EmbedService.LEGACY_EMBED_WARNING, embed: await _embeds.CreateGroupEmbed(ctx, system, target, all)); return; } - await ctx.Reply(components: await _embeds.CreateGroupMessageComponents(ctx, system, target)); + await ctx.Reply(components: await _embeds.CreateGroupMessageComponents(ctx, system, target, all)); } public async Task ShowGroupPrivacy(Context ctx, PKGroup target) diff --git a/PluralKit.Bot/Commands/ImportExport.cs b/PluralKit.Bot/Commands/ImportExport.cs index dbecc3f7..76118afe 100644 --- a/PluralKit.Bot/Commands/ImportExport.cs +++ b/PluralKit.Bot/Commands/ImportExport.cs @@ -31,7 +31,7 @@ public class ImportExport _dmCache = dmCache; } - public async Task Import(Context ctx, string? inputUrl) + public async Task Import(Context ctx, string? inputUrl, bool confirmYes) { inputUrl = inputUrl ?? ctx.Message.Attachments.FirstOrDefault()?.Url; if (inputUrl == null) throw Errors.NoImportFilePassed; @@ -77,7 +77,7 @@ public class ImportExport async Task ConfirmImport(string message) { var msg = $"{message}\n\nDo you want to proceed with the import?"; - if (!await ctx.PromptYesNo(msg, "Proceed")) + if (!await ctx.PromptYesNo(msg, "Proceed", flagValue: confirmYes)) throw Errors.ImportCancelled; } @@ -86,7 +86,7 @@ public class ImportExport && data.Value("accounts").Contains(ctx.Author.Id.ToString())) { var msg = $"{Emojis.Warn} You seem to importing a system profile belonging to another account. Are you sure you want to proceed?"; - if (!await ctx.PromptYesNo(msg, "Import")) throw Errors.ImportCancelled; + if (!await ctx.PromptYesNo(msg, "Import", flagValue: confirmYes)) throw Errors.ImportCancelled; } var result = await _dataFiles.ImportSystem(ctx.Author.Id, ctx.System, data, ConfirmImport); diff --git a/PluralKit.Bot/Commands/Lists/ContextListExt.cs b/PluralKit.Bot/Commands/Lists/ContextListExt.cs index 875a1862..bdd1b034 100644 --- a/PluralKit.Bot/Commands/Lists/ContextListExt.cs +++ b/PluralKit.Bot/Commands/Lists/ContextListExt.cs @@ -130,7 +130,7 @@ public static class ContextListExt } public static async Task RenderGroupList(this Context ctx, LookupContext lookupCtx, - SystemId system, string embedTitle, string color, ListOptions opts) + SystemId system, string embedTitle, string color, ListOptions opts, bool all) { // We take an IDatabase instead of a IPKConnection so we don't keep the handle open for the entire runtime // We wanna release it as soon as the member list is actually *fetched*, instead of potentially minutes later (paginate timeout) @@ -204,7 +204,7 @@ public static class ContextListExt { if (g.ListPrivacy == PrivacyLevel.Public || lookupCtx == LookupContext.ByOwner) { - if (ctx.MatchFlag("all", "a")) + if (all) { ret += $"({"member".ToQuantity(g.TotalMemberCount)})"; } @@ -242,7 +242,7 @@ public static class ContextListExt if (g.ListPrivacy == PrivacyLevel.Public || lookupCtx == LookupContext.ByOwner) { - if (ctx.MatchFlag("all", "a") && ctx.DirectLookupContextFor(system) == LookupContext.ByOwner) + if (all && ctx.DirectLookupContextFor(system) == LookupContext.ByOwner) profile.Append($"\n**Member Count:** {g.TotalMemberCount}"); else profile.Append($"\n**Member Count:** {g.PublicMemberCount}"); diff --git a/PluralKit.Bot/Commands/Member.cs b/PluralKit.Bot/Commands/Member.cs index 45010f34..6d5ee812 100644 --- a/PluralKit.Bot/Commands/Member.cs +++ b/PluralKit.Bot/Commands/Member.cs @@ -28,7 +28,7 @@ public class Member _avatarHosting = avatarHosting; } - public async Task NewMember(Context ctx, string? memberName) + public async Task NewMember(Context ctx, string? memberName, bool confirmYes = false) { if (ctx.System == null) throw Errors.NoSystemError(ctx.DefaultPrefix); memberName = memberName ?? throw new PKSyntaxError("You must pass a member name."); @@ -42,7 +42,7 @@ public class Member if (existingMember != null) { var msg = $"{Emojis.Warn} You already have a member in your system with the name \"{existingMember.NameFor(ctx)}\" (with ID `{existingMember.DisplayHid(ctx.Config)}`). Do you want to create another member with the same name?"; - if (!await ctx.PromptYesNo(msg, "Create")) throw new PKError("Member creation cancelled."); + if (!await ctx.PromptYesNo(msg, "Create", flagValue: confirmYes)) throw new PKError("Member creation cancelled."); } await using var conn = await ctx.Database.Obtain(); diff --git a/PluralKit.Bot/Commands/MemberAvatar.cs b/PluralKit.Bot/Commands/MemberAvatar.cs index 8c5289f8..81500a0e 100644 --- a/PluralKit.Bot/Commands/MemberAvatar.cs +++ b/PluralKit.Bot/Commands/MemberAvatar.cs @@ -17,10 +17,10 @@ public class MemberAvatar _avatarHosting = avatarHosting; } - private async Task AvatarClear(MemberAvatarLocation location, Context ctx, PKMember target, MemberGuildSettings? mgs) + private async Task AvatarClear(MemberAvatarLocation location, Context ctx, PKMember target, MemberGuildSettings? mgs, bool confirmYes) { ctx.CheckSystem().CheckOwnMember(target); - await ctx.ConfirmClear("this member's " + location.Name()); + await ctx.ConfirmClear("this member's " + location.Name(), confirmYes); await UpdateAvatar(location, ctx, target, null); if (location == MemberAvatarLocation.Server) @@ -149,10 +149,10 @@ public class MemberAvatar await AvatarShow(MemberAvatarLocation.Server, ctx, target, guildData, format); } - public async Task ClearServerAvatar(Context ctx, PKMember target) + public async Task ClearServerAvatar(Context ctx, PKMember target, bool confirmYes) { var guildData = await GetServerAvatarGuildData(ctx, target); - await AvatarClear(MemberAvatarLocation.Server, ctx, target, guildData); + await AvatarClear(MemberAvatarLocation.Server, ctx, target, guildData, confirmYes); } public async Task ChangeServerAvatar(Context ctx, PKMember target, ParsedImage avatar) @@ -167,10 +167,10 @@ public class MemberAvatar await AvatarShow(MemberAvatarLocation.Member, ctx, target, guildData, format); } - public async Task ClearAvatar(Context ctx, PKMember target) + public async Task ClearAvatar(Context ctx, PKMember target, bool confirmYes) { var guildData = await GetAvatarGuildData(ctx, target); - await AvatarClear(MemberAvatarLocation.Member, ctx, target, guildData); + await AvatarClear(MemberAvatarLocation.Member, ctx, target, guildData, confirmYes); } public async Task ChangeAvatar(Context ctx, PKMember target, ParsedImage avatar) @@ -185,10 +185,10 @@ public class MemberAvatar await AvatarShow(MemberAvatarLocation.MemberWebhook, ctx, target, guildData, format); } - public async Task ClearWebhookAvatar(Context ctx, PKMember target) + public async Task ClearWebhookAvatar(Context ctx, PKMember target, bool confirmYes) { var guildData = await GetWebhookAvatarGuildData(ctx, target); - await AvatarClear(MemberAvatarLocation.MemberWebhook, ctx, target, guildData); + await AvatarClear(MemberAvatarLocation.MemberWebhook, ctx, target, guildData, confirmYes); } public async Task ChangeWebhookAvatar(Context ctx, PKMember target, ParsedImage avatar) diff --git a/PluralKit.Bot/Commands/MemberEdit.cs b/PluralKit.Bot/Commands/MemberEdit.cs index 94b127d2..e4ccc6ea 100644 --- a/PluralKit.Bot/Commands/MemberEdit.cs +++ b/PluralKit.Bot/Commands/MemberEdit.cs @@ -44,7 +44,7 @@ public class MemberEdit } } - public async Task ChangeName(Context ctx, PKMember target, string newName) + public async Task ChangeName(Context ctx, PKMember target, string newName, bool confirmYes) { ctx.CheckSystem().CheckOwnMember(target); @@ -58,7 +58,7 @@ public class MemberEdit { var msg = $"{Emojis.Warn} You already have a member in your system with the name \"{existingMember.NameFor(ctx)}\" (`{existingMember.DisplayHid(ctx.Config)}`). Do you want to rename this member to that name too?"; - if (!await ctx.PromptYesNo(msg, "Rename")) throw new PKError("Member renaming cancelled."); + if (!await ctx.PromptYesNo(msg, "Rename", flagValue: confirmYes)) throw new PKError("Member renaming cancelled."); } // Rename the member diff --git a/PluralKit.Bot/Commands/MemberProxy.cs b/PluralKit.Bot/Commands/MemberProxy.cs index d2b128d8..6e47dce0 100644 --- a/PluralKit.Bot/Commands/MemberProxy.cs +++ b/PluralKit.Bot/Commands/MemberProxy.cs @@ -14,7 +14,7 @@ public class MemberProxy await ctx.Reply($"This member's proxy tags are:\n{target.ProxyTagsString("\n")}"); } - public async Task ClearProxy(Context ctx, PKMember target) + public async Task ClearProxy(Context ctx, PKMember target, bool confirmYes = false) { ctx.CheckSystem().CheckOwnMember(target); @@ -22,7 +22,7 @@ public class MemberProxy if (target.ProxyTags.Count > 1) { var msg = $"{Emojis.Warn} You already have multiple proxy tags set: {target.ProxyTagsString()}\nDo you want to clear them all?"; - if (!await ctx.PromptYesNo(msg, "Clear")) + if (!await ctx.PromptYesNo(msg, "Clear", flagValue: confirmYes)) throw Errors.GenericCancelled(); } @@ -32,7 +32,7 @@ public class MemberProxy await ctx.Reply($"{Emojis.Success} Proxy tags cleared."); } - public async Task AddProxy(Context ctx, PKMember target, string proxyString) + public async Task AddProxy(Context ctx, PKMember target, string proxyString, bool confirmYes = false) { ctx.CheckSystem().CheckOwnMember(target); @@ -44,7 +44,7 @@ public class MemberProxy throw new PKError( $"Proxy tag too long ({tagToAdd.ProxyString.Length} > {Limits.MaxProxyTagLength} characters)."); - if (!await WarnOnConflict(ctx, target, tagToAdd)) + if (!await WarnOnConflict(ctx, target, tagToAdd, confirmYes)) throw Errors.GenericCancelled(); var newTags = target.ProxyTags.ToList(); @@ -72,7 +72,7 @@ public class MemberProxy await ctx.Reply($"{Emojis.Success} Removed proxy tags {tagToRemove.ProxyString.AsCode()}."); } - public async Task SetProxy(Context ctx, PKMember target, string proxyString) + public async Task SetProxy(Context ctx, PKMember target, string proxyString, bool confirmYes = false) { ctx.CheckSystem().CheckOwnMember(target); @@ -82,7 +82,7 @@ public class MemberProxy if (target.ProxyTags.Count > 1) { var msg = $"This member already has more than one proxy tag set: {target.ProxyTagsString()}\nDo you want to replace them?"; - if (!await ctx.PromptYesNo(msg, "Replace")) + if (!await ctx.PromptYesNo(msg, "Replace", flagValue: confirmYes)) throw Errors.GenericCancelled(); } @@ -90,7 +90,7 @@ public class MemberProxy throw new PKError( $"Proxy tag too long ({requestedTag.ProxyString.Length} > {Limits.MaxProxyTagLength} characters)."); - if (!await WarnOnConflict(ctx, target, requestedTag)) + if (!await WarnOnConflict(ctx, target, requestedTag, confirmYes)) throw Errors.GenericCancelled(); var newTags = new[] { requestedTag }; @@ -110,7 +110,7 @@ public class MemberProxy return new ProxyTag(prefixAndSuffix[0], prefixAndSuffix[1]); } - private async Task WarnOnConflict(Context ctx, PKMember target, ProxyTag newTag) + private async Task WarnOnConflict(Context ctx, PKMember target, ProxyTag newTag, bool confirmYes = false) { var query = "select * from (select *, (unnest(proxy_tags)).prefix as prefix, (unnest(proxy_tags)).suffix as suffix from members where system = @System) as _ where prefix is not distinct from @Prefix and suffix is not distinct from @Suffix and id != @Existing"; var conflicts = (await ctx.Database.Execute(conn => conn.QueryAsync(query, @@ -120,6 +120,6 @@ public class MemberProxy var conflictList = conflicts.Select(m => $"- **{m.NameFor(ctx)}**"); var msg = $"{Emojis.Warn} The following members have conflicting proxy tags:\n{string.Join('\n', conflictList)}\nDo you want to proceed anyway?"; - return await ctx.PromptYesNo(msg, "Proceed"); + return await ctx.PromptYesNo(msg, "Proceed", flagValue: confirmYes); } } \ No newline at end of file diff --git a/PluralKit.Bot/Commands/Message.cs b/PluralKit.Bot/Commands/Message.cs index 7bb1f681..6e25b67f 100644 --- a/PluralKit.Bot/Commands/Message.cs +++ b/PluralKit.Bot/Commands/Message.cs @@ -322,9 +322,7 @@ public class ProxiedMessage { if (messageId == null) { - if (!ctx.HasNext()) - throw new PKSyntaxError("You must pass a message ID or link."); - throw new PKSyntaxError($"Could not parse {ctx.PeekArgument().AsCode()} as a message ID or link."); + throw new PKSyntaxError("You must pass a message ID or link."); } var message = await ctx.Repository.GetFullMessage(messageId.Value); diff --git a/PluralKit.Bot/Commands/Random.cs b/PluralKit.Bot/Commands/Random.cs index 2356ce4a..d34c3e73 100644 --- a/PluralKit.Bot/Commands/Random.cs +++ b/PluralKit.Bot/Commands/Random.cs @@ -74,12 +74,12 @@ public class Random { await ctx.Reply( text: EmbedService.LEGACY_EMBED_WARNING, - embed: await _embeds.CreateGroupEmbed(ctx, target, groups.ToArray()[randInt])); + embed: await _embeds.CreateGroupEmbed(ctx, target, groups.ToArray()[randInt], all)); return; } await ctx.Reply( - components: await _embeds.CreateGroupMessageComponents(ctx, target, groups.ToArray()[randInt])); + components: await _embeds.CreateGroupMessageComponents(ctx, target, groups.ToArray()[randInt], all)); } public async Task GroupMember(Context ctx, PKGroup group, GroupRandomMemberFlags flags) diff --git a/PluralKit.Bot/Commands/ServerConfig.cs b/PluralKit.Bot/Commands/ServerConfig.cs index e7b24a3c..9ecc5170 100644 --- a/PluralKit.Bot/Commands/ServerConfig.cs +++ b/PluralKit.Bot/Commands/ServerConfig.cs @@ -144,11 +144,11 @@ public class ServerConfig await ctx.Reply($"{Emojis.Success} Proxy logging channel set to <#{channel.Id}>."); } - public async Task ClearLogChannel(Context ctx) + public async Task ClearLogChannel(Context ctx, bool confirmYes) { await ctx.CheckGuildContext().CheckAuthorPermission(PermissionSet.ManageGuild, "Manage Server"); - if (!await ctx.ConfirmClear("the server log channel")) + if (!await ctx.ConfirmClear("the server log channel", confirmYes)) return; await ctx.Repository.UpdateGuild(ctx.Guild.Id, new GuildPatch { LogChannel = null }); diff --git a/PluralKit.Bot/Commands/Switch.cs b/PluralKit.Bot/Commands/Switch.cs index 82c63e14..f566ef6a 100644 --- a/PluralKit.Bot/Commands/Switch.cs +++ b/PluralKit.Bot/Commands/Switch.cs @@ -57,14 +57,14 @@ public class Switch $"{Emojis.Success} Switch registered. Current fronters are now {string.Join(", ", members.Select(m => m.NameFor(ctx)))}."); } - public async Task SwitchMove(Context ctx, string timeToMove) + public async Task SwitchMove(Context ctx, string str, bool confirmYes = false) { ctx.CheckSystem(); var tz = TzdbDateTimeZoneSource.Default.ForId(ctx.Config?.UiTz ?? "UTC"); - var result = DateUtils.ParseDateTime(timeToMove, true, tz); - if (result == null) throw Errors.InvalidDateTime(timeToMove); + var result = DateUtils.ParseDateTime(str, true, tz); + if (result == null) throw Errors.InvalidDateTime(str); var time = result.Value; @@ -95,14 +95,14 @@ public class Switch // yeet var msg = $"{Emojis.Warn} This will move the latest switch ({lastSwitchMemberStr}) from ({lastSwitchDeltaStr} ago) to ({newSwitchDeltaStr} ago). Is this OK?"; - if (!await ctx.PromptYesNo(msg, "Move Switch")) throw Errors.SwitchMoveCancelled; + if (!await ctx.PromptYesNo(msg, "Move Switch", flagValue: confirmYes)) throw Errors.SwitchMoveCancelled; // aaaand *now* we do the move await ctx.Repository.MoveSwitch(lastTwoSwitches[0].Id, time.ToInstant()); await ctx.Reply($"{Emojis.Success} Switch moved to ({newSwitchDeltaStr} ago)."); } - public async Task SwitchEdit(Context ctx, List? newMembers, bool newSwitch = false, bool first = false, bool remove = false, bool append = false, bool prepend = false) + public async Task SwitchEdit(Context ctx, List? newMembers, bool newSwitch = false, bool first = false, bool remove = false, bool append = false, bool prepend = false, bool confirmYes = false) { ctx.CheckSystem(); @@ -131,7 +131,7 @@ public class Switch await DoSwitchCommand(ctx, newMembers); } else - await DoEditCommand(ctx, newMembers); + await DoEditCommand(ctx, newMembers, confirmYes); } public List PrependToSwitch(List members, List currentSwitchMembers) @@ -167,13 +167,13 @@ public class Switch return members; } - public async Task SwitchEditOut(Context ctx) + public async Task SwitchEditOut(Context ctx, bool confirmYes) { ctx.CheckSystem(); - await DoEditCommand(ctx, []); + await DoEditCommand(ctx, [], confirmYes); } - public async Task DoEditCommand(Context ctx, ICollection? members) + public async Task DoEditCommand(Context ctx, ICollection? members, bool confirmYes) { if (members == null) members = new List(); @@ -203,7 +203,7 @@ public class Switch msg = $"{Emojis.Warn} This will turn the latest switch ({lastSwitchMemberStr}, {lastSwitchDeltaStr} ago) into a switch-out. Is this okay?"; else msg = $"{Emojis.Warn} This will change the latest switch ({lastSwitchMemberStr}, {lastSwitchDeltaStr} ago) to {newSwitchMemberStr}. Is this okay?"; - if (!await ctx.PromptYesNo(msg, "Edit")) throw Errors.SwitchEditCancelled; + if (!await ctx.PromptYesNo(msg, "Edit", flagValue: confirmYes)) throw Errors.SwitchEditCancelled; // Actually edit the switch await ctx.Repository.EditSwitch(conn, lastSwitch.Id, members.Select(m => m.Id).ToList()); @@ -217,7 +217,7 @@ public class Switch await ctx.Reply($"{Emojis.Success} Switch edited. Current fronters are now {newSwitchMemberStr}."); } - public async Task SwitchDelete(Context ctx, bool all) + public async Task SwitchDelete(Context ctx, bool all = false, bool confirmYes = false) { ctx.CheckSystem(); @@ -226,7 +226,7 @@ public class Switch // Subcommand: "delete all" var purgeMsg = $"{Emojis.Warn} This will delete *all registered switches* in your system. Are you sure you want to proceed?"; - if (!await ctx.PromptYesNo(purgeMsg, "Clear Switches")) + if (!await ctx.PromptYesNo(purgeMsg, "Clear Switches", flagValue: confirmYes)) throw Errors.GenericCancelled(); await ctx.Repository.DeleteAllSwitches(ctx.System.Id); await ctx.Reply($"{Emojis.Success} Cleared system switches!"); @@ -258,7 +258,7 @@ public class Switch msg = $"{Emojis.Warn} This will delete the latest switch ({lastSwitchMemberStr}, {lastSwitchDeltaStr} ago). The next latest switch is {secondSwitchMemberStr} ({secondSwitchDeltaStr} ago). Is this okay?"; } - if (!await ctx.PromptYesNo(msg, "Delete Switch")) throw Errors.SwitchDeleteCancelled; + if (!await ctx.PromptYesNo(msg, "Delete Switch", flagValue: confirmYes)) throw Errors.SwitchDeleteCancelled; await ctx.Repository.DeleteSwitch(lastTwoSwitches[0].Id); await ctx.Reply($"{Emojis.Success} Switch deleted."); diff --git a/PluralKit.Bot/Commands/SystemFront.cs b/PluralKit.Bot/Commands/SystemFront.cs index 6d9cf049..330a9f77 100644 --- a/PluralKit.Bot/Commands/SystemFront.cs +++ b/PluralKit.Bot/Commands/SystemFront.cs @@ -26,7 +26,7 @@ public class SystemFront await ctx.Reply(embed: await _embeds.CreateFronterEmbed(sw, ctx.Zone, ctx.LookupContextFor(system.Id))); } - public async Task FrontHistory(Context ctx, PKSystem system, bool clear = false) + public async Task FrontHistory(Context ctx, PKSystem system, bool showMemberId, bool clear = false) { if (clear) { @@ -55,8 +55,6 @@ public class SystemFront embedTitle = $"Front history of {guildSettings.DisplayName} (`{system.Hid}`)"; } - var showMemberId = ctx.MatchFlag("with-id", "wid"); - await ctx.Paginate( sws, totalSwitches, diff --git a/PluralKit.Bot/Commands/SystemLink.cs b/PluralKit.Bot/Commands/SystemLink.cs index 38a0cc47..ef5f89af 100644 --- a/PluralKit.Bot/Commands/SystemLink.cs +++ b/PluralKit.Bot/Commands/SystemLink.cs @@ -25,7 +25,7 @@ public class SystemLink await ctx.Reply($"{Emojis.Success} Account linked to system."); } - public async Task UnlinkAccount(Context ctx, string idRaw) + public async Task UnlinkAccount(Context ctx, string idRaw, bool confirmYes) { ctx.CheckSystem(); @@ -38,7 +38,7 @@ public class SystemLink if (accountIds.Count == 1) throw Errors.UnlinkingLastAccount(ctx.DefaultPrefix); var msg = $"Are you sure you want to unlink <@{id}> from your system?"; - if (!await ctx.PromptYesNo(msg, "Unlink")) throw Errors.MemberUnlinkCancelled; + if (!await ctx.PromptYesNo(msg, "Unlink", flagValue: confirmYes)) throw Errors.MemberUnlinkCancelled; await ctx.Repository.RemoveAccount(ctx.System.Id, id); await ctx.Reply($"{Emojis.Success} Account unlinked."); diff --git a/PluralKit.Bot/Services/EmbedService.cs b/PluralKit.Bot/Services/EmbedService.cs index 4cf5158f..620d7229 100644 --- a/PluralKit.Bot/Services/EmbedService.cs +++ b/PluralKit.Bot/Services/EmbedService.cs @@ -560,7 +560,7 @@ public class EmbedService return eb.Build(); } - public async Task CreateGroupMessageComponents(Context ctx, PKSystem system, PKGroup target) + public async Task CreateGroupMessageComponents(Context ctx, PKSystem system, PKGroup target, bool all) { var pctx = ctx.LookupContextFor(system.Id); var name = target.NameFor(ctx); @@ -568,7 +568,7 @@ public class EmbedService var systemName = (ctx.Guild != null && systemGuildSettings?.DisplayName != null) ? systemGuildSettings?.DisplayName! : system.NameFor(ctx); var countctx = LookupContext.ByNonOwner; - if (ctx.MatchFlag("a", "all")) + if (all) { if (system.Id == ctx.System?.Id) countctx = LookupContext.ByOwner; @@ -673,12 +673,12 @@ public class EmbedService ]; } - public async Task CreateGroupEmbed(Context ctx, PKSystem system, PKGroup target) + public async Task CreateGroupEmbed(Context ctx, PKSystem system, PKGroup target, bool all) { var pctx = ctx.LookupContextFor(system.Id); var countctx = LookupContext.ByNonOwner; - if (ctx.MatchFlag("a", "all")) + if (all) { if (system.Id == ctx.System?.Id) countctx = LookupContext.ByOwner; diff --git a/PluralKit.Bot/Utils/ContextUtils.cs b/PluralKit.Bot/Utils/ContextUtils.cs index 4b0a4f5b..f40785d2 100644 --- a/PluralKit.Bot/Utils/ContextUtils.cs +++ b/PluralKit.Bot/Utils/ContextUtils.cs @@ -16,17 +16,17 @@ namespace PluralKit.Bot; public static class ContextUtils { - public static async Task ConfirmClear(this Context ctx, string toClear, bool? confirmYes = null) + public static async Task ConfirmClear(this Context ctx, string toClear, bool confirmYes) { - if (!await ctx.PromptYesNo($"{Emojis.Warn} Are you sure you want to clear {toClear}?", "Clear", null, true, confirmYes)) + if (!await ctx.PromptYesNo($"{Emojis.Warn} Are you sure you want to clear {toClear}?", "Clear", flagValue: confirmYes)) throw Errors.GenericCancelled(); return true; } public static async Task PromptYesNo(this Context ctx, string msgString, string acceptButton, - User user = null, bool matchFlag = true, bool? flagValue = null) + User user = null, bool matchFlag = true, bool flagValue = false) { - if (matchFlag && (flagValue ?? ctx.MatchFlag("y", "yes"))) return true; + if (matchFlag && flagValue) return true; var prompt = new YesNoPrompt(ctx) { diff --git a/crates/command_definitions/src/admin.rs b/crates/command_definitions/src/admin.rs index 56f4dd71..a7817d4a 100644 --- a/crates/command_definitions/src/admin.rs +++ b/crates/command_definitions/src/admin.rs @@ -16,6 +16,7 @@ pub fn cmds() -> impl Iterator { .help("Sets the deny flag on an abuse log entry"), command!(abuselog, ("description", ["desc"]), log_param, Optional(("desc", OpaqueStringRemainder)) => format!("admin_abuselog_description_{}", log_param.name())) .flag(("clear", ["c"])) + .flag(("yes", ["y"])) .help("Sets the description of an abuse log entry"), command!(abuselog, ("adduser", ["au"]), log_param => format!("admin_abuselog_add_user_{}", log_param.name())) .help("Adds a user to an abuse log entry"), @@ -36,22 +37,31 @@ pub fn cmds() -> impl Iterator { [ command!(admin, ("updatesystemid", ["usid"]), SystemRef, ("new_hid", OpaqueString) => "admin_update_system_id") + .flag(("yes", ["y"])) .help("Updates a system's ID"), command!(admin, ("updatememberid", ["umid"]), MemberRef, ("new_hid", OpaqueString) => "admin_update_member_id") + .flag(("yes", ["y"])) .help("Updates a member's ID"), command!(admin, ("updategroupid", ["ugid"]), GroupRef, ("new_hid", OpaqueString) => "admin_update_group_id") + .flag(("yes", ["y"])) .help("Updates a group's ID"), command!(admin, ("rerollsystemid", ["rsid"]), SystemRef => "admin_reroll_system_id") + .flag(("yes", ["y"])) .help("Rerolls a system's ID"), command!(admin, ("rerollmemberid", ["rmid"]), MemberRef => "admin_reroll_member_id") + .flag(("yes", ["y"])) .help("Rerolls a member's ID"), command!(admin, ("rerollgroupid", ["rgid"]), GroupRef => "admin_reroll_group_id") + .flag(("yes", ["y"])) .help("Rerolls a group's ID"), command!(admin, ("updatememberlimit", ["uml"]), SystemRef, Optional(("limit", OpaqueInt)) => "admin_system_member_limit") + .flag(("yes", ["y"])) .help("Updates a system's member limit"), command!(admin, ("updategrouplimit", ["ugl"]), SystemRef, Optional(("limit", OpaqueInt)) => "admin_system_group_limit") + .flag(("yes", ["y"])) .help("Updates a system's group limit"), command!(admin, ("systemrecover", ["sr"]), ("token", OpaqueString), ("account", UserRef) => "admin_system_recover") + .flag(("yes", ["y"])) .flag(("reroll-token", ["rt"])) .help("Recovers a system"), command!(admin, ("systemdelete", ["sd"]), SystemRef => "admin_system_delete") diff --git a/crates/command_definitions/src/group.rs b/crates/command_definitions/src/group.rs index fec62ccf..e0fdf2fb 100644 --- a/crates/command_definitions/src/group.rs +++ b/crates/command_definitions/src/group.rs @@ -21,9 +21,10 @@ pub fn cmds() -> impl Iterator { [command!(group_new, ("name", OpaqueString) => "group_new").help("Creates a new group")] .into_iter(); - let group_info_cmd = - [command!(group_target => "group_info").help("Shows information about a group")] - .into_iter(); + let group_info_cmd = [command!(group_target => "group_info") + .flag(("all", ["a"])) + .help("Shows information about a group")] + .into_iter(); let group_name = tokens!( group_target, @@ -159,9 +160,9 @@ pub fn cmds() -> impl Iterator { let group_modify_members_cmd = [ command!(group_target, "add", Optional(MemberRefs) => "group_add_member") - .flag(("all", ["a"])), + .flag(("all", ["a"])).flag(("yes", ["y"])), command!(group_target, ("remove", ["delete", "del", "rem"]), Optional(MemberRefs) => "group_remove_member") - .flag(("all", ["a"])), + .flag(("all", ["a"])).flag(("yes", ["y"])), ] .into_iter(); diff --git a/crates/command_definitions/src/import_export.rs b/crates/command_definitions/src/import_export.rs index 49b66f11..16c27a06 100644 --- a/crates/command_definitions/src/import_export.rs +++ b/crates/command_definitions/src/import_export.rs @@ -2,7 +2,8 @@ use super::*; pub fn cmds() -> impl Iterator { [ - command!("import", Optional(("url", OpaqueStringRemainder)) => "import"), + command!("import", Optional(("url", OpaqueStringRemainder)) => "import") + .flag(("yes", ["y"])), command!("export" => "export"), ] .into_iter() diff --git a/crates/command_definitions/src/member.rs b/crates/command_definitions/src/member.rs index f6acb28e..8da757b5 100644 --- a/crates/command_definitions/src/member.rs +++ b/crates/command_definitions/src/member.rs @@ -49,6 +49,7 @@ pub fn cmds() -> impl Iterator { [ command!(member_name => "member_name_show").help("Shows a member's name"), command!(member_name, ("name", OpaqueStringRemainder) => "member_name_update") + .flag(("yes", ["y"])) .help("Changes a member's name"), ] .into_iter() diff --git a/crates/command_definitions/src/server_config.rs b/crates/command_definitions/src/server_config.rs index 7119d528..c0cdfa0b 100644 --- a/crates/command_definitions/src/server_config.rs +++ b/crates/command_definitions/src/server_config.rs @@ -51,6 +51,7 @@ pub fn cmds() -> impl Iterator { command!(log_channel, ("channel", ChannelRef) => "server_config_log_channel_set") .help("Sets the log channel"), command!(log_channel, ("clear", ["c"]) => "server_config_log_channel_clear") + .flag(("yes", ["y"])) .help("Clears the log channel"), ] .into_iter(); diff --git a/crates/command_definitions/src/switch.rs b/crates/command_definitions/src/switch.rs index 14ef6a0d..3b51849a 100644 --- a/crates/command_definitions/src/switch.rs +++ b/crates/command_definitions/src/switch.rs @@ -20,7 +20,7 @@ pub fn cmds() -> impl Iterator { command!(switch, out => "switch_out"), command!(switch, r#move, OpaqueString => "switch_move"), // TODO: datetime parsing command!(switch, delete => "switch_delete").flag(("all", ["clear", "c"])), - command!(switch, edit, out => "switch_edit_out"), + command!(switch, edit, out => "switch_edit_out").flag(("yes", ["y"])), command!(switch, edit, Optional(MemberRefs) => "switch_edit").flags(edit_flags), command!(switch, copy, Optional(MemberRefs) => "switch_copy").flags(edit_flags), command!(switch, ("commands", ["help"]) => "switch_commands"), diff --git a/crates/command_definitions/src/system.rs b/crates/command_definitions/src/system.rs index 4e12ceb9..7abe106f 100644 --- a/crates/command_definitions/src/system.rs +++ b/crates/command_definitions/src/system.rs @@ -28,25 +28,33 @@ pub fn edit() -> impl Iterator { ] .into_iter(); - let system_webhook_cmd = [command!(system, ("webhook", ["hook"]) => "system_webhook") - .help("Creates a webhook for your system")] + let system_webhook = tokens!(system, ("webhook", ["hook"])); + let system_webhook_cmd = [ + command!(system_webhook => "system_webhook_show").help("Shows your system's webhook URL"), + command!(system_webhook, ("clear", ["c"]) => "system_webhook_clear") + .flag(("yes", ["y"])) + .help("Clears your system's webhook URL"), + command!(system_webhook, ("url", OpaqueString) => "system_webhook_set") + .help("Sets your system's webhook URL"), + ] .into_iter(); - let system_info_cmd = [ - command!(system => "system_info_self").help("Shows information about your system"), - command!(system_target, ("info", ["show", "view"]) => "system_info") - .help("Shows information about your system"), - ] - .into_iter() - .map(|cmd| { + let add_info_flags = |cmd: Command| { cmd.flag(("public", ["pub"])) .flag(("private", ["priv"])) .flag(("all", ["a"])) - }); + }; + let system_info_cmd_self = std::iter::once(add_info_flags( + command!(system => "system_info_self").help("Shows information about your system"), + )); + let system_info_cmd = std::iter::once(add_info_flags( + command!(system_target, ("info", ["show", "view"]) => "system_info") + .help("Shows information about your system"), + )); let system_name = tokens!(system_target, "name"); let system_name_cmd = - [command!(system_name => "system_show_name").help("Shows the systems name")].into_iter(); + std::iter::once(command!(system_name => "system_show_name").help("Shows the systems name")); let system_name_self = tokens!(system, "name"); let system_name_self_cmd = [ @@ -60,9 +68,10 @@ pub fn edit() -> impl Iterator { .into_iter(); let system_server_name = tokens!(system_target, ("servername", ["sn", "guildname"])); - let system_server_name_cmd = [command!(system_server_name => "system_show_server_name") - .help("Shows the system's server name")] - .into_iter(); + let system_server_name_cmd = std::iter::once( + command!(system_server_name => "system_show_server_name") + .help("Shows the system's server name"), + ); let system_server_name_self = tokens!(system, ("servername", ["sn", "guildname"])); let system_server_name_self_cmd = [ @@ -77,9 +86,10 @@ pub fn edit() -> impl Iterator { .into_iter(); let system_description = tokens!(system_target, ("description", ["desc", "d"])); - let system_description_cmd = [command!(system_description => "system_show_description") - .help("Shows the system's description")] - .into_iter(); + let system_description_cmd = std::iter::once( + command!(system_description => "system_show_description") + .help("Shows the system's description"), + ); let system_description_self = tokens!(system, ("description", ["desc", "d"])); let system_description_self_cmd = [ @@ -93,9 +103,9 @@ pub fn edit() -> impl Iterator { .into_iter(); let system_color = tokens!(system_target, ("color", ["colour"])); - let system_color_cmd = - [command!(system_color => "system_show_color").help("Shows the system's color")] - .into_iter(); + let system_color_cmd = std::iter::once( + command!(system_color => "system_show_color").help("Shows the system's color"), + ); let system_color_self = tokens!(system, ("color", ["colour"])); let system_color_self_cmd = [ @@ -110,7 +120,7 @@ pub fn edit() -> impl Iterator { let system_tag = tokens!(system_target, ("tag", ["suffix"])); let system_tag_cmd = - [command!(system_tag => "system_show_tag").help("Shows the system's tag")].into_iter(); + std::iter::once(command!(system_tag => "system_show_tag").help("Shows the system's tag")); let system_tag_self = tokens!(system, ("tag", ["suffix"])); let system_tag_self_cmd = [ @@ -124,9 +134,10 @@ pub fn edit() -> impl Iterator { .into_iter(); let system_server_tag = tokens!(system_target, ("servertag", ["st", "guildtag"])); - let system_server_tag_cmd = [command!(system_server_tag => "system_show_server_tag") - .help("Shows the system's server tag")] - .into_iter(); + let system_server_tag_cmd = std::iter::once( + command!(system_server_tag => "system_show_server_tag") + .help("Shows the system's server tag"), + ); let system_server_tag_self = tokens!(system, ("servertag", ["st", "guildtag"])); let system_server_tag_self_cmd = [ @@ -141,9 +152,9 @@ pub fn edit() -> impl Iterator { .into_iter(); let system_pronouns = tokens!(system_target, ("pronouns", ["prns"])); - let system_pronouns_cmd = - [command!(system_pronouns => "system_show_pronouns").help("Shows the system's pronouns")] - .into_iter(); + let system_pronouns_cmd = std::iter::once( + command!(system_pronouns => "system_show_pronouns").help("Shows the system's pronouns"), + ); let system_pronouns_self = tokens!(system, ("pronouns", ["prns"])); let system_pronouns_self_cmd = [ @@ -158,9 +169,9 @@ pub fn edit() -> impl Iterator { .into_iter(); let system_avatar = tokens!(system_target, ("avatar", ["pfp"])); - let system_avatar_cmd = - [command!(system_avatar => "system_show_avatar").help("Shows the system's avatar")] - .into_iter(); + let system_avatar_cmd = std::iter::once( + command!(system_avatar => "system_show_avatar").help("Shows the system's avatar"), + ); let system_avatar_self = tokens!(system, ("avatar", ["pfp"])); let system_avatar_self_cmd = [ @@ -175,11 +186,10 @@ pub fn edit() -> impl Iterator { .into_iter(); let system_server_avatar = tokens!(system_target, ("serveravatar", ["spfp"])); - let system_server_avatar_cmd = [ + let system_server_avatar_cmd = std::iter::once( command!(system_server_avatar => "system_show_server_avatar") .help("Shows the system's server avatar"), - ] - .into_iter(); + ); let system_server_avatar_self = tokens!(system, ("serveravatar", ["spfp"])); let system_server_avatar_self_cmd = [ @@ -194,9 +204,9 @@ pub fn edit() -> impl Iterator { .into_iter(); let system_banner = tokens!(system_target, ("banner", ["cover"])); - let system_banner_cmd = - [command!(system_banner => "system_show_banner").help("Shows the system's banner")] - .into_iter(); + let system_banner_cmd = std::iter::once( + command!(system_banner => "system_show_banner").help("Shows the system's banner"), + ); let system_banner_self = tokens!(system, ("banner", ["cover"])); let system_banner_self_cmd = [ @@ -253,7 +263,7 @@ pub fn edit() -> impl Iterator { let system_link = [ command!("link", ("account", UserRef) => "system_link"), - command!("unlink", ("account", OpaqueString) => "system_unlink"), + command!("unlink", ("account", OpaqueString) => "system_unlink").flag(("yes", ["y"])), ] .into_iter(); @@ -286,10 +296,12 @@ pub fn edit() -> impl Iterator { .map(add_list_flags); let system_display_id_self_cmd = - [command!(system, "id" => "system_display_id_self")].into_iter(); - let system_display_id_cmd = [command!(system_target, "id" => "system_display_id")].into_iter(); + std::iter::once(command!(system, "id" => "system_display_id_self")); + let system_display_id_cmd = + std::iter::once(command!(system_target, "id" => "system_display_id")); - system_new_cmd + system_info_cmd_self + .chain(system_new_cmd) .chain(system_webhook_cmd) .chain(system_name_self_cmd) .chain(system_server_name_self_cmd) diff --git a/crates/commands/src/main.rs b/crates/commands/src/main.rs index 325fc469..8784b5c2 100644 --- a/crates/commands/src/main.rs +++ b/crates/commands/src/main.rs @@ -4,7 +4,7 @@ use command_parser::Tree; use commands::COMMAND_TREE; fn main() { - parse(); + related(); } fn related() { From f4216a83a939e4c92590ba98ccc146dea8c962bb Mon Sep 17 00:00:00 2001 From: dusk Date: Wed, 8 Oct 2025 04:29:48 +0000 Subject: [PATCH 115/179] make some common flags into constants --- crates/command_definitions/src/admin.rs | 22 ++++---- crates/command_definitions/src/group.rs | 34 ++++++------ .../command_definitions/src/import_export.rs | 2 +- crates/command_definitions/src/lib.rs | 4 ++ crates/command_definitions/src/member.rs | 50 +++++++++--------- crates/command_definitions/src/random.rs | 2 +- .../command_definitions/src/server_config.rs | 12 ++--- crates/command_definitions/src/switch.rs | 2 +- crates/command_definitions/src/system.rs | 52 +++++++++---------- crates/command_definitions/src/utils.rs | 4 +- 10 files changed, 95 insertions(+), 89 deletions(-) diff --git a/crates/command_definitions/src/admin.rs b/crates/command_definitions/src/admin.rs index a7817d4a..032f137a 100644 --- a/crates/command_definitions/src/admin.rs +++ b/crates/command_definitions/src/admin.rs @@ -15,8 +15,8 @@ pub fn cmds() -> impl Iterator { command!(abuselog, ("flagdeny", ["fd"]), log_param, Optional(("value", Toggle)) => format!("admin_abuselog_flag_deny_{}", log_param.name())) .help("Sets the deny flag on an abuse log entry"), command!(abuselog, ("description", ["desc"]), log_param, Optional(("desc", OpaqueStringRemainder)) => format!("admin_abuselog_description_{}", log_param.name())) - .flag(("clear", ["c"])) - .flag(("yes", ["y"])) + .flag(CLEAR) + .flag(YES) .help("Sets the description of an abuse log entry"), command!(abuselog, ("adduser", ["au"]), log_param => format!("admin_abuselog_add_user_{}", log_param.name())) .help("Adds a user to an abuse log entry"), @@ -37,31 +37,31 @@ pub fn cmds() -> impl Iterator { [ command!(admin, ("updatesystemid", ["usid"]), SystemRef, ("new_hid", OpaqueString) => "admin_update_system_id") - .flag(("yes", ["y"])) + .flag(YES) .help("Updates a system's ID"), command!(admin, ("updatememberid", ["umid"]), MemberRef, ("new_hid", OpaqueString) => "admin_update_member_id") - .flag(("yes", ["y"])) + .flag(YES) .help("Updates a member's ID"), command!(admin, ("updategroupid", ["ugid"]), GroupRef, ("new_hid", OpaqueString) => "admin_update_group_id") - .flag(("yes", ["y"])) + .flag(YES) .help("Updates a group's ID"), command!(admin, ("rerollsystemid", ["rsid"]), SystemRef => "admin_reroll_system_id") - .flag(("yes", ["y"])) + .flag(YES) .help("Rerolls a system's ID"), command!(admin, ("rerollmemberid", ["rmid"]), MemberRef => "admin_reroll_member_id") - .flag(("yes", ["y"])) + .flag(YES) .help("Rerolls a member's ID"), command!(admin, ("rerollgroupid", ["rgid"]), GroupRef => "admin_reroll_group_id") - .flag(("yes", ["y"])) + .flag(YES) .help("Rerolls a group's ID"), command!(admin, ("updatememberlimit", ["uml"]), SystemRef, Optional(("limit", OpaqueInt)) => "admin_system_member_limit") - .flag(("yes", ["y"])) + .flag(YES) .help("Updates a system's member limit"), command!(admin, ("updategrouplimit", ["ugl"]), SystemRef, Optional(("limit", OpaqueInt)) => "admin_system_group_limit") - .flag(("yes", ["y"])) + .flag(YES) .help("Updates a system's group limit"), command!(admin, ("systemrecover", ["sr"]), ("token", OpaqueString), ("account", UserRef) => "admin_system_recover") - .flag(("yes", ["y"])) + .flag(YES) .flag(("reroll-token", ["rt"])) .help("Recovers a system"), command!(admin, ("systemdelete", ["sd"]), SystemRef => "admin_system_delete") diff --git a/crates/command_definitions/src/group.rs b/crates/command_definitions/src/group.rs index e0fdf2fb..36ef917b 100644 --- a/crates/command_definitions/src/group.rs +++ b/crates/command_definitions/src/group.rs @@ -22,7 +22,7 @@ pub fn cmds() -> impl Iterator { .into_iter(); let group_info_cmd = [command!(group_target => "group_info") - .flag(("all", ["a"])) + .flag(ALL) .help("Shows information about a group")] .into_iter(); @@ -32,8 +32,8 @@ pub fn cmds() -> impl Iterator { ); let group_name_cmd = [ command!(group_name => "group_show_name").help("Shows the group's name"), - command!(group_name, ("clear", ["c"]) => "group_clear_name") - .flag(("yes", ["y"])) + command!(group_name, CLEAR => "group_clear_name") + .flag(YES) .help("Clears the group's name"), command!(group_name, ("name", OpaqueString) => "group_rename").help("Renames the group"), ] @@ -43,8 +43,8 @@ pub fn cmds() -> impl Iterator { let group_display_name_cmd = [ command!(group_display_name => "group_show_display_name") .help("Shows the group's display name"), - command!(group_display_name, ("clear", ["c"]) => "group_clear_display_name") - .flag(("yes", ["y"])) + command!(group_display_name, CLEAR => "group_clear_display_name") + .flag(YES) .help("Clears the group's display name"), command!(group_display_name, ("name", OpaqueString) => "group_change_display_name") .help("Changes the group's display name"), @@ -61,8 +61,8 @@ pub fn cmds() -> impl Iterator { let group_description_cmd = [ command!(group_description => "group_show_description") .help("Shows the group's description"), - command!(group_description, ("clear", ["c"]) => "group_clear_description") - .flag(("yes", ["y"])) + command!(group_description, CLEAR => "group_clear_description") + .flag(YES) .help("Clears the group's description"), command!(group_description, ("description", OpaqueString) => "group_change_description") .help("Changes the group's description"), @@ -75,8 +75,8 @@ pub fn cmds() -> impl Iterator { ); let group_icon_cmd = [ command!(group_icon => "group_show_icon").help("Shows the group's icon"), - command!(group_icon, ("clear", ["c"]) => "group_clear_icon") - .flag(("yes", ["y"])) + command!(group_icon, CLEAR => "group_clear_icon") + .flag(YES) .help("Clears the group's icon"), command!(group_icon, ("icon", Avatar) => "group_change_icon") .help("Changes the group's icon"), @@ -86,8 +86,8 @@ pub fn cmds() -> impl Iterator { let group_banner = tokens!(group_target, ("banner", ["splash", "cover"])); let group_banner_cmd = [ command!(group_banner => "group_show_banner").help("Shows the group's banner"), - command!(group_banner, ("clear", ["c"]) => "group_clear_banner") - .flag(("yes", ["y"])) + command!(group_banner, CLEAR => "group_clear_banner") + .flag(YES) .help("Clears the group's banner"), command!(group_banner, ("banner", Avatar) => "group_change_banner") .help("Changes the group's banner"), @@ -97,8 +97,8 @@ pub fn cmds() -> impl Iterator { let group_color = tokens!(group_target, ("color", ["colour"])); let group_color_cmd = [ command!(group_color => "group_show_color").help("Shows the group's color"), - command!(group_color, ("clear", ["c"]) => "group_clear_color") - .flag(("yes", ["y"])) + command!(group_color, CLEAR => "group_clear_color") + .flag(YES) .help("Clears the group's color"), command!(group_color, ("color", OpaqueString) => "group_change_color") .help("Changes the group's color"), @@ -109,7 +109,7 @@ pub fn cmds() -> impl Iterator { let group_privacy_cmd = [ command!(group_privacy => "group_show_privacy") .help("Shows the group's privacy settings"), - command!(group_privacy, ("all", ["a"]), ("level", PrivacyLevel) => "group_change_privacy_all") + command!(group_privacy, ALL, ("level", PrivacyLevel) => "group_change_privacy_all") .help("Changes all privacy settings for the group"), command!(group_privacy, ("privacy", GroupPrivacyTarget), ("level", PrivacyLevel) => "group_change_privacy") .help("Changes a specific privacy setting for the group"), @@ -130,7 +130,7 @@ pub fn cmds() -> impl Iterator { let group_delete_cmd = [ command!(group_target, ("delete", ["destroy", "erase", "yeet"]) => "group_delete") - .flag(("yes", ["y"])) + .flag(YES) .help("Deletes the group"), ] .into_iter(); @@ -160,9 +160,9 @@ pub fn cmds() -> impl Iterator { let group_modify_members_cmd = [ command!(group_target, "add", Optional(MemberRefs) => "group_add_member") - .flag(("all", ["a"])).flag(("yes", ["y"])), + .flag(ALL).flag(YES), command!(group_target, ("remove", ["delete", "del", "rem"]), Optional(MemberRefs) => "group_remove_member") - .flag(("all", ["a"])).flag(("yes", ["y"])), + .flag(ALL).flag(YES), ] .into_iter(); diff --git a/crates/command_definitions/src/import_export.rs b/crates/command_definitions/src/import_export.rs index 16c27a06..1fcfdb4d 100644 --- a/crates/command_definitions/src/import_export.rs +++ b/crates/command_definitions/src/import_export.rs @@ -3,7 +3,7 @@ use super::*; pub fn cmds() -> impl Iterator { [ command!("import", Optional(("url", OpaqueStringRemainder)) => "import") - .flag(("yes", ["y"])), + .flag(YES), command!("export" => "export"), ] .into_iter() diff --git a/crates/command_definitions/src/lib.rs b/crates/command_definitions/src/lib.rs index 065e95b3..0f20ddb9 100644 --- a/crates/command_definitions/src/lib.rs +++ b/crates/command_definitions/src/lib.rs @@ -50,3 +50,7 @@ pub fn all() -> impl Iterator { } pub const RESET: (&str, [&str; 2]) = ("reset", ["clear", "default"]); + +pub const CLEAR: (&str, [&str; 1]) = ("clear", ["c"]); +pub const YES: (&str, [&str; 1]) = ("yes", ["y"]); +pub const ALL: (&str, [&str; 1]) = ("all", ["a"]); diff --git a/crates/command_definitions/src/member.rs b/crates/command_definitions/src/member.rs index 8da757b5..e18a04e3 100644 --- a/crates/command_definitions/src/member.rs +++ b/crates/command_definitions/src/member.rs @@ -49,7 +49,7 @@ pub fn cmds() -> impl Iterator { [ command!(member_name => "member_name_show").help("Shows a member's name"), command!(member_name, ("name", OpaqueStringRemainder) => "member_name_update") - .flag(("yes", ["y"])) + .flag(YES) .help("Changes a member's name"), ] .into_iter() @@ -59,8 +59,8 @@ pub fn cmds() -> impl Iterator { let member_desc = tokens!(member_target, description); [ command!(member_desc => "member_desc_show").help("Shows a member's description"), - command!(member_desc, ("clear", ["c"]) => "member_desc_clear") - .flag(("yes", ["y"])) + command!(member_desc, CLEAR => "member_desc_clear") + .flag(YES) .help("Clears a member's description"), command!(member_desc, ("description", OpaqueStringRemainder) => "member_desc_update") .help("Changes a member's description"), @@ -89,8 +89,8 @@ pub fn cmds() -> impl Iterator { .help("Shows a member's pronouns"), command!(member_pronouns, ("pronouns", OpaqueStringRemainder) => "member_pronouns_update") .help("Changes a member's pronouns"), - command!(member_pronouns, ("clear", ["c"]) => "member_pronouns_clear") - .flag(("yes", ["y"])) + command!(member_pronouns, CLEAR => "member_pronouns_clear") + .flag(YES) .help("Clears a member's pronouns"), ].into_iter() }; @@ -101,8 +101,8 @@ pub fn cmds() -> impl Iterator { command!(member_banner => "member_banner_show").help("Shows a member's banner image"), command!(member_banner, ("banner", Avatar) => "member_banner_update") .help("Changes a member's banner image"), - command!(member_banner, ("clear", ["c"]) => "member_banner_clear") - .flag(("yes", ["y"])) + command!(member_banner, CLEAR => "member_banner_clear") + .flag(YES) .help("Clears a member's banner image"), ] .into_iter() @@ -114,8 +114,8 @@ pub fn cmds() -> impl Iterator { command!(member_color => "member_color_show").help("Shows a member's color"), command!(member_color, ("color", OpaqueString) => "member_color_update") .help("Changes a member's color"), - command!(member_color, ("clear", ["c"]) => "member_color_clear") - .flag(("yes", ["y"])) + command!(member_color, CLEAR => "member_color_clear") + .flag(YES) .help("Clears a member's color"), ] .into_iter() @@ -127,8 +127,8 @@ pub fn cmds() -> impl Iterator { command!(member_birthday => "member_birthday_show").help("Shows a member's birthday"), command!(member_birthday, ("birthday", OpaqueString) => "member_birthday_update") .help("Changes a member's birthday"), - command!(member_birthday, ("clear", ["c"]) => "member_birthday_clear") - .flag(("yes", ["y"])) + command!(member_birthday, CLEAR => "member_birthday_clear") + .flag(YES) .help("Clears a member's birthday"), ] .into_iter() @@ -141,8 +141,8 @@ pub fn cmds() -> impl Iterator { .help("Shows a member's display name"), command!(member_display_name, ("name", OpaqueStringRemainder) => "member_displayname_update") .help("Changes a member's display name"), - command!(member_display_name, ("clear", ["c"]) => "member_displayname_clear") - .flag(("yes", ["y"])) + command!(member_display_name, CLEAR => "member_displayname_clear") + .flag(YES) .help("Clears a member's display name"), ].into_iter() }; @@ -154,8 +154,8 @@ pub fn cmds() -> impl Iterator { .help("Shows a member's server name"), command!(member_server_name, ("name", OpaqueStringRemainder) => "member_servername_update") .help("Changes a member's server name"), - command!(member_server_name, ("clear", ["c"]) => "member_servername_clear") - .flag(("yes", ["y"])) + command!(member_server_name, CLEAR => "member_servername_clear") + .flag(YES) .help("Clears a member's server name"), ].into_iter() }; @@ -171,8 +171,8 @@ pub fn cmds() -> impl Iterator { .help("Adds proxy tag to a member"), command!(member_proxy, ("remove", ["r", "rm"]), ("tag", OpaqueString) => "member_proxy_remove") .help("Removes proxy tag from a member"), - command!(member_proxy, ("clear", ["c"]) => "member_proxy_clear") - .flag(("yes", ["y"])) + command!(member_proxy, CLEAR => "member_proxy_clear") + .flag(YES) .help("Clears all proxy tags from a member"), ].into_iter() }; @@ -189,8 +189,8 @@ pub fn cmds() -> impl Iterator { .help("Shows a member's server-specific keep-proxy setting"), command!(member_server_keep_proxy, Skip(("value", Toggle)) => "member_server_keepproxy_update") .help("Changes a member's server-specific keep-proxy setting"), - command!(member_server_keep_proxy, ("clear", ["c"]) => "member_server_keepproxy_clear") - .flag(("yes", ["y"])) + command!(member_server_keep_proxy, CLEAR => "member_server_keepproxy_clear") + .flag(YES) .help("Clears a member's server-specific keep-proxy setting"), ].into_iter() }; @@ -223,8 +223,8 @@ pub fn cmds() -> impl Iterator { command!(member_avatar => "member_avatar_show").help("Shows a member's avatar"), command!(member_avatar, ("avatar", Avatar) => "member_avatar_update") .help("Changes a member's avatar"), - command!(member_avatar, ("clear", ["c"]) => "member_avatar_clear") - .flag(("yes", ["y"])) + command!(member_avatar, CLEAR => "member_avatar_clear") + .flag(YES) .help("Clears a member's avatar"), ] .into_iter() @@ -250,8 +250,8 @@ pub fn cmds() -> impl Iterator { .help("Shows a member's proxy avatar"), command!(member_webhook_avatar, ("avatar", Avatar) => "member_webhook_avatar_update") .help("Changes a member's proxy avatar"), - command!(member_webhook_avatar, ("clear", ["c"]) => "member_webhook_avatar_clear") - .flag(("yes", ["y"])) + command!(member_webhook_avatar, CLEAR => "member_webhook_avatar_clear") + .flag(YES) .help("Clears a member's proxy avatar"), ] .into_iter() @@ -283,8 +283,8 @@ pub fn cmds() -> impl Iterator { .help("Shows a member's server-specific avatar"), command!(member_server_avatar, ("avatar", Avatar) => "member_server_avatar_update") .help("Changes a member's server-specific avatar"), - command!(member_server_avatar, ("clear", ["c"]) => "member_server_avatar_clear") - .flag(("yes", ["y"])) + command!(member_server_avatar, CLEAR => "member_server_avatar_clear") + .flag(YES) .help("Clears a member's server-specific avatar"), ] .into_iter() diff --git a/crates/command_definitions/src/random.rs b/crates/command_definitions/src/random.rs index e62701a5..ce4d2288 100644 --- a/crates/command_definitions/src/random.rs +++ b/crates/command_definitions/src/random.rs @@ -12,5 +12,5 @@ pub fn cmds() -> impl Iterator { command!(group::targeted(), random => "group_random_member").flags(get_list_flags()), ] .into_iter() - .map(|cmd| cmd.flag(("all", ["a"]))) + .map(|cmd| cmd.flag(ALL)) } diff --git a/crates/command_definitions/src/server_config.rs b/crates/command_definitions/src/server_config.rs index c0cdfa0b..bdceade9 100644 --- a/crates/command_definitions/src/server_config.rs +++ b/crates/command_definitions/src/server_config.rs @@ -50,8 +50,8 @@ pub fn cmds() -> impl Iterator { .help("Shows the current log channel"), command!(log_channel, ("channel", ChannelRef) => "server_config_log_channel_set") .help("Sets the log channel"), - command!(log_channel, ("clear", ["c"]) => "server_config_log_channel_clear") - .flag(("yes", ["y"])) + command!(log_channel, CLEAR => "server_config_log_channel_clear") + .flag(YES) .help("Clears the log channel"), ] .into_iter(); @@ -74,10 +74,10 @@ pub fn cmds() -> impl Iterator { command!(log_blacklist => "server_config_log_blacklist_show") .help("Shows channels where logging is disabled"), command!(log_blacklist, add, Optional(("channel", ChannelRef)) => "server_config_log_blacklist_add") - .flag(("all", ["a"])) + .flag(ALL) .help("Adds a channel (or all channels with --all) to the log blacklist"), command!(log_blacklist, remove, Optional(("channel", ChannelRef)) => "server_config_log_blacklist_remove") - .flag(("all", ["a"])) + .flag(ALL) .help("Removes a channel (or all channels with --all) from the log blacklist"), ] .into_iter(); @@ -87,10 +87,10 @@ pub fn cmds() -> impl Iterator { command!(proxy_blacklist => "server_config_proxy_blacklist_show") .help("Shows channels where proxying is disabled"), command!(proxy_blacklist, add, Optional(("channel", ChannelRef)) => "server_config_proxy_blacklist_add") - .flag(("all", ["a"])) + .flag(ALL) .help("Adds a channel (or all channels with --all) to the proxy blacklist"), command!(proxy_blacklist, remove, Optional(("channel", ChannelRef)) => "server_config_proxy_blacklist_remove") - .flag(("all", ["a"])) + .flag(ALL) .help("Removes a channel (or all channels with --all) from the proxy blacklist"), ] .into_iter(); diff --git a/crates/command_definitions/src/switch.rs b/crates/command_definitions/src/switch.rs index 3b51849a..42a3fda7 100644 --- a/crates/command_definitions/src/switch.rs +++ b/crates/command_definitions/src/switch.rs @@ -20,7 +20,7 @@ pub fn cmds() -> impl Iterator { command!(switch, out => "switch_out"), command!(switch, r#move, OpaqueString => "switch_move"), // TODO: datetime parsing command!(switch, delete => "switch_delete").flag(("all", ["clear", "c"])), - command!(switch, edit, out => "switch_edit_out").flag(("yes", ["y"])), + command!(switch, edit, out => "switch_edit_out").flag(YES), command!(switch, edit, Optional(MemberRefs) => "switch_edit").flags(edit_flags), command!(switch, copy, Optional(MemberRefs) => "switch_copy").flags(edit_flags), command!(switch, ("commands", ["help"]) => "switch_commands"), diff --git a/crates/command_definitions/src/system.rs b/crates/command_definitions/src/system.rs index 7abe106f..8fc1c440 100644 --- a/crates/command_definitions/src/system.rs +++ b/crates/command_definitions/src/system.rs @@ -31,8 +31,8 @@ pub fn edit() -> impl Iterator { let system_webhook = tokens!(system, ("webhook", ["hook"])); let system_webhook_cmd = [ command!(system_webhook => "system_webhook_show").help("Shows your system's webhook URL"), - command!(system_webhook, ("clear", ["c"]) => "system_webhook_clear") - .flag(("yes", ["y"])) + command!(system_webhook, CLEAR => "system_webhook_clear") + .flag(YES) .help("Clears your system's webhook URL"), command!(system_webhook, ("url", OpaqueString) => "system_webhook_set") .help("Sets your system's webhook URL"), @@ -42,7 +42,7 @@ pub fn edit() -> impl Iterator { let add_info_flags = |cmd: Command| { cmd.flag(("public", ["pub"])) .flag(("private", ["priv"])) - .flag(("all", ["a"])) + .flag(ALL) }; let system_info_cmd_self = std::iter::once(add_info_flags( command!(system => "system_info_self").help("Shows information about your system"), @@ -59,8 +59,8 @@ pub fn edit() -> impl Iterator { let system_name_self = tokens!(system, "name"); let system_name_self_cmd = [ command!(system_name_self => "system_show_name_self").help("Shows your system's name"), - command!(system_name_self, ("clear", ["c"]) => "system_clear_name") - .flag(("yes", ["y"])) + command!(system_name_self, CLEAR => "system_clear_name") + .flag(YES) .help("Clears your system's name"), command!(system_name_self, ("name", OpaqueString) => "system_rename") .help("Renames your system"), @@ -77,8 +77,8 @@ pub fn edit() -> impl Iterator { let system_server_name_self_cmd = [ command!(system_server_name_self => "system_show_server_name_self") .help("Shows your system's server name"), - command!(system_server_name_self, ("clear", ["c"]) => "system_clear_server_name") - .flag(("yes", ["y"])) + command!(system_server_name_self, CLEAR => "system_clear_server_name") + .flag(YES) .help("Clears your system's server name"), command!(system_server_name_self, ("name", OpaqueString) => "system_rename_server_name") .help("Renames your system's server name"), @@ -94,8 +94,8 @@ pub fn edit() -> impl Iterator { let system_description_self = tokens!(system, ("description", ["desc", "d"])); let system_description_self_cmd = [ command!(system_description_self => "system_show_description_self").help("Shows your system's description"), - command!(system_description_self, ("clear", ["c"]) => "system_clear_description") - .flag(("yes", ["y"])) + command!(system_description_self, CLEAR => "system_clear_description") + .flag(YES) .help("Clears your system's description"), command!(system_description_self, ("description", OpaqueString) => "system_change_description") .help("Changes your system's description"), @@ -110,8 +110,8 @@ pub fn edit() -> impl Iterator { let system_color_self = tokens!(system, ("color", ["colour"])); let system_color_self_cmd = [ command!(system_color_self => "system_show_color_self").help("Shows your system's color"), - command!(system_color_self, ("clear", ["c"]) => "system_clear_color") - .flag(("yes", ["y"])) + command!(system_color_self, CLEAR => "system_clear_color") + .flag(YES) .help("Clears your system's color"), command!(system_color_self, ("color", OpaqueString) => "system_change_color") .help("Changes your system's color"), @@ -125,8 +125,8 @@ pub fn edit() -> impl Iterator { let system_tag_self = tokens!(system, ("tag", ["suffix"])); let system_tag_self_cmd = [ command!(system_tag_self => "system_show_tag_self").help("Shows your system's tag"), - command!(system_tag_self, ("clear", ["c"]) => "system_clear_tag") - .flag(("yes", ["y"])) + command!(system_tag_self, CLEAR => "system_clear_tag") + .flag(YES) .help("Clears your system's tag"), command!(system_tag_self, ("tag", OpaqueString) => "system_change_tag") .help("Changes your system's tag"), @@ -143,8 +143,8 @@ pub fn edit() -> impl Iterator { let system_server_tag_self_cmd = [ command!(system_server_tag_self => "system_show_server_tag_self") .help("Shows your system's server tag"), - command!(system_server_tag_self, ("clear", ["c"]) => "system_clear_server_tag") - .flag(("yes", ["y"])) + command!(system_server_tag_self, CLEAR => "system_clear_server_tag") + .flag(YES) .help("Clears your system's server tag"), command!(system_server_tag_self, ("tag", OpaqueString) => "system_change_server_tag") .help("Changes your system's server tag"), @@ -160,8 +160,8 @@ pub fn edit() -> impl Iterator { let system_pronouns_self_cmd = [ command!(system_pronouns_self => "system_show_pronouns_self") .help("Shows your system's pronouns"), - command!(system_pronouns_self, ("clear", ["c"]) => "system_clear_pronouns") - .flag(("yes", ["y"])) + command!(system_pronouns_self, CLEAR => "system_clear_pronouns") + .flag(YES) .help("Clears your system's pronouns"), command!(system_pronouns_self, ("pronouns", OpaqueString) => "system_change_pronouns") .help("Changes your system's pronouns"), @@ -177,8 +177,8 @@ pub fn edit() -> impl Iterator { let system_avatar_self_cmd = [ command!(system_avatar_self => "system_show_avatar_self") .help("Shows your system's avatar"), - command!(system_avatar_self, ("clear", ["c"]) => "system_clear_avatar") - .flag(("yes", ["y"])) + command!(system_avatar_self, CLEAR => "system_clear_avatar") + .flag(YES) .help("Clears your system's avatar"), command!(system_avatar_self, ("avatar", Avatar) => "system_change_avatar") .help("Changes your system's avatar"), @@ -195,8 +195,8 @@ pub fn edit() -> impl Iterator { let system_server_avatar_self_cmd = [ command!(system_server_avatar_self => "system_show_server_avatar_self") .help("Shows your system's server avatar"), - command!(system_server_avatar_self, ("clear", ["c"]) => "system_clear_server_avatar") - .flag(("yes", ["y"])) + command!(system_server_avatar_self, CLEAR => "system_clear_server_avatar") + .flag(YES) .help("Clears your system's server avatar"), command!(system_server_avatar_self, ("avatar", Avatar) => "system_change_server_avatar") .help("Changes your system's server avatar"), @@ -212,8 +212,8 @@ pub fn edit() -> impl Iterator { let system_banner_self_cmd = [ command!(system_banner_self => "system_show_banner_self") .help("Shows your system's banner"), - command!(system_banner_self, ("clear", ["c"]) => "system_clear_banner") - .flag(("yes", ["y"])) + command!(system_banner_self, CLEAR => "system_clear_banner") + .flag(YES) .help("Clears your system's banner"), command!(system_banner_self, ("banner", Avatar) => "system_change_banner") .help("Changes your system's banner"), @@ -243,7 +243,7 @@ pub fn edit() -> impl Iterator { let system_privacy_cmd = [ command!(system_privacy => "system_show_privacy") .help("Shows your system's privacy settings"), - command!(system_privacy, ("all", ["a"]), ("level", PrivacyLevel) => "system_change_privacy_all") + command!(system_privacy, ALL, ("level", PrivacyLevel) => "system_change_privacy_all") .help("Changes all privacy settings for your system"), command!(system_privacy, ("privacy", SystemPrivacyTarget), ("level", PrivacyLevel) => "system_change_privacy") .help("Changes a specific privacy setting for your system"), @@ -253,7 +253,7 @@ pub fn edit() -> impl Iterator { let system_front_cmd = [ command!(system_front => "system_fronter"), command!(system_front, ("history", ["h"]) => "system_fronter_history") - .flag(("clear", ["c"])), + .flag(CLEAR), command!(system_front, ("percent", ["p", "%"]) => "system_fronter_percent") .flag(("duration", OpaqueString)) .flag(("fronters-only", ["fo"])) @@ -263,7 +263,7 @@ pub fn edit() -> impl Iterator { let system_link = [ command!("link", ("account", UserRef) => "system_link"), - command!("unlink", ("account", OpaqueString) => "system_unlink").flag(("yes", ["y"])), + command!("unlink", ("account", OpaqueString) => "system_unlink").flag(YES), ] .into_iter(); diff --git a/crates/command_definitions/src/utils.rs b/crates/command_definitions/src/utils.rs index 8fd2d2c5..6eb40c57 100644 --- a/crates/command_definitions/src/utils.rs +++ b/crates/command_definitions/src/utils.rs @@ -1,5 +1,7 @@ use command_parser::flag::Flag; +use crate::ALL; + pub fn get_list_flags() -> [Flag; 22] { [ // Short or long list @@ -31,7 +33,7 @@ pub fn get_list_flags() -> [Flag; 22] { // Sort reverse Flag::from(("reverse", ["r", "rev"])), // Privacy filter - Flag::from(("all", ["a"])), + Flag::from(ALL), Flag::from(("private-only", ["po"])), // Additional fields to include Flag::from(( From ac5a1c6bea7eb032713d6179094b96af9f75d613 Mon Sep 17 00:00:00 2001 From: dusk Date: Wed, 8 Oct 2025 13:34:51 +0000 Subject: [PATCH 116/179] generate command parser bindings in ci --- .github/workflows/dotnet.yml | 33 ++++++++++++++++++++++----------- flake.nix | 1 - 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 996a7cb7..ef0ac304 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -3,22 +3,22 @@ name: .net checks on: push: paths: - - .github/workflows/dotnet.yml - - 'Myriad/**' - - 'PluralKit.API/**' - - 'PluralKit.Bot/**' - - 'PluralKit.Core/**' + - .github/workflows/dotnet.yml + - "Myriad/**" + - "PluralKit.API/**" + - "PluralKit.Bot/**" + - "PluralKit.Core/**" pull_request: paths: - - .github/workflows/dotnet.yml - - 'Myriad/**' - - 'PluralKit.API/**' - - 'PluralKit.Bot/**' - - 'PluralKit.Core/**' + - .github/workflows/dotnet.yml + - "Myriad/**" + - "PluralKit.API/**" + - "PluralKit.Bot/**" + - "PluralKit.Core/**" jobs: test: - name: 'run .net tests' + name: "run .net tests" runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -30,6 +30,17 @@ jobs: with: dotnet-version: 8.0.x + - uses: actions-rust-lang/setup-rust-toolchain@v1 + + - name: install uniffi + run: cargo install uniffi-bindgen-cs --git https://github.com/NordSecurity/uniffi-bindgen-cs --tag v0.8.3+v0.25.0 + + - name: generate command parser bindings + run: | + cargo -Z unstable-options build --package commands --lib --release --artifact-dir obj/ + uniffi-bindgen-cs "obj/libcommands.so" --library --out-dir="./PluralKit.Bot" + cargo run --package commands --bin write_cs_glue -- "./PluralKit.Bot"/commandtypes.cs + - name: Run automated tests run: dotnet test --configuration Release diff --git a/flake.nix b/flake.nix index 2769372d..68f01771 100644 --- a/flake.nix +++ b/flake.nix @@ -101,7 +101,6 @@ fi uniffi-bindgen-cs "$commandslib" --library --out-dir="''${2:-./PluralKit.Bot}" cargo run --package commands --bin write_cs_glue -- "''${2:-./PluralKit.Bot}"/commandtypes.cs - dotnet format ./PluralKit.Bot/PluralKit.Bot.csproj ''; }; }; From 643f14847a1a1c8fabdc4785cd930c30990e4f20 Mon Sep 17 00:00:00 2001 From: dusk Date: Wed, 8 Oct 2025 13:35:05 +0000 Subject: [PATCH 117/179] cargo fmt --- crates/command_definitions/src/import_export.rs | 3 +-- crates/command_definitions/src/system.rs | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/crates/command_definitions/src/import_export.rs b/crates/command_definitions/src/import_export.rs index 1fcfdb4d..beed5e72 100644 --- a/crates/command_definitions/src/import_export.rs +++ b/crates/command_definitions/src/import_export.rs @@ -2,8 +2,7 @@ use super::*; pub fn cmds() -> impl Iterator { [ - command!("import", Optional(("url", OpaqueStringRemainder)) => "import") - .flag(YES), + command!("import", Optional(("url", OpaqueStringRemainder)) => "import").flag(YES), command!("export" => "export"), ] .into_iter() diff --git a/crates/command_definitions/src/system.rs b/crates/command_definitions/src/system.rs index 8fc1c440..4d3b7c4c 100644 --- a/crates/command_definitions/src/system.rs +++ b/crates/command_definitions/src/system.rs @@ -252,8 +252,7 @@ pub fn edit() -> impl Iterator { let system_front = tokens!(system_target, ("front", ["fronter", "fronters", "f"])); let system_front_cmd = [ command!(system_front => "system_fronter"), - command!(system_front, ("history", ["h"]) => "system_fronter_history") - .flag(CLEAR), + command!(system_front, ("history", ["h"]) => "system_fronter_history").flag(CLEAR), command!(system_front, ("percent", ["p", "%"]) => "system_fronter_percent") .flag(("duration", OpaqueString)) .flag(("fronters-only", ["fo"])) From dbab8c0eb56c24e23cac3cc183b4d2fd1d881467 Mon Sep 17 00:00:00 2001 From: dusk Date: Wed, 8 Oct 2025 14:22:10 +0000 Subject: [PATCH 118/179] update uniffi to latest --- .github/workflows/dotnet.yml | 2 +- Cargo.lock | 244 +++++++++++++++++++---------------- crates/commands/Cargo.toml | 4 +- flake.lock | 16 +-- flake.nix | 4 +- 5 files changed, 146 insertions(+), 124 deletions(-) diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index ef0ac304..1051c9cb 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -33,7 +33,7 @@ jobs: - uses: actions-rust-lang/setup-rust-toolchain@v1 - name: install uniffi - run: cargo install uniffi-bindgen-cs --git https://github.com/NordSecurity/uniffi-bindgen-cs --tag v0.8.3+v0.25.0 + run: cargo install uniffi-bindgen-cs --git https://github.com/90-008/uniffi-bindgen-cs - name: generate command parser bindings run: | diff --git a/Cargo.lock b/Cargo.lock index 9e133dcf..8e52d160 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -118,43 +118,44 @@ checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" [[package]] name = "askama" -version = "0.12.1" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b79091df18a97caea757e28cd2d5fda49c6cd4bd01ddffd7ff01ace0c0ad2c28" +checksum = "5d4744ed2eef2645831b441d8f5459689ade2ab27c854488fbab1fbe94fce1a7" dependencies = [ "askama_derive", - "askama_escape", + "itoa", + "percent-encoding", + "serde", + "serde_json", ] [[package]] name = "askama_derive" -version = "0.12.5" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19fe8d6cb13c4714962c072ea496f3392015f0989b1a2847bb4b2d9effd71d83" +checksum = "d661e0f57be36a5c14c48f78d09011e67e0cb618f269cca9f2fd8d15b68c46ac" dependencies = [ "askama_parser", "basic-toml", - "mime", - "mime_guess", + "memchr", "proc-macro2", "quote", + "rustc-hash", "serde", + "serde_derive", "syn", ] -[[package]] -name = "askama_escape" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "619743e34b5ba4e9703bba34deac3427c72507c7159f5fd030aea8cac0cfe341" - [[package]] name = "askama_parser" -version = "0.2.1" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acb1161c6b64d1c3d83108213c2a2533a342ac225aabd0bda218278c2ddb00c0" +checksum = "cf315ce6524c857bb129ff794935cf6d42c82a6cff60526fe2a63593de4d0d4f" dependencies = [ - "nom", + "memchr", + "serde", + "serde_derive", + "winnow", ] [[package]] @@ -428,15 +429,6 @@ dependencies = [ "serde", ] -[[package]] -name = "bincode" -version = "1.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" -dependencies = [ - "serde", -] - [[package]] name = "bindgen" version = "0.72.1" @@ -550,16 +542,16 @@ dependencies = [ [[package]] name = "cargo_metadata" -version = "0.15.4" +version = "0.19.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eee4243f1f26fc7a42710e7439c149e2b10b05472f88090acce52632f231a73a" +checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" dependencies = [ "camino", "cargo-platform", "semver", "serde", "serde_json", - "thiserror 1.0.69", + "thiserror 2.0.16", ] [[package]] @@ -1075,6 +1067,16 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.1", +] + [[package]] name = "etcetera" version = "0.8.0" @@ -1458,9 +1460,9 @@ checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" [[package]] name = "goblin" -version = "0.6.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d6b4de4a8eb6c46a8c77e1d3be942cb9a8bf073c22374578e5ba4b08ed0ff68" +checksum = "1b363a30c165f666402fe6a3024d3bec7ebc898f96a4a23bd1c99f8dbf3f4f47" dependencies = [ "log", "plain", @@ -2228,6 +2230,12 @@ dependencies = [ "glob", ] +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + [[package]] name = "litemap" version = "0.8.0" @@ -2372,16 +2380,6 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" -[[package]] -name = "mime_guess" -version = "2.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" -dependencies = [ - "mime", - "unicase", -] - [[package]] name = "minimal-lexical" version = "0.2.1" @@ -2535,12 +2533,6 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" -[[package]] -name = "oneshot-uniffi" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c548d5c78976f6955d72d0ced18c48ca07030f7a1d4024529fedd7c1c01b29c" - [[package]] name = "openssl-probe" version = "0.1.6" @@ -2626,12 +2618,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "paste" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" - [[package]] name = "pathdiff" version = "0.2.3" @@ -3351,6 +3337,19 @@ dependencies = [ "semver", ] +[[package]] +name = "rustix" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +dependencies = [ + "bitflags 2.9.4", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.1", +] + [[package]] name = "rustls" version = "0.20.9" @@ -3530,18 +3529,18 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "scroll" -version = "0.11.0" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04c565b551bafbef4157586fa379538366e4385d42082f255bfd96e4fe8519da" +checksum = "6ab8598aa408498679922eff7fa985c25d58a90771bd6be794434c5277eab1a6" dependencies = [ "scroll_derive", ] [[package]] name = "scroll_derive" -version = "0.11.1" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1db149f81d46d2deba7cd3c50772474707729550221e69588478ebf9ada425ae" +checksum = "1783eabc414609e28a5ba76aee5ddd52199f7107a0b24c2e9746a1ecc34a683d" dependencies = [ "proc-macro2", "quote", @@ -3942,6 +3941,12 @@ dependencies = [ "serde", ] +[[package]] +name = "smawk" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" + [[package]] name = "smol_str" version = "0.3.2" @@ -4290,6 +4295,28 @@ dependencies = [ "libc", ] +[[package]] +name = "tempfile" +version = "3.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +dependencies = [ + "fastrand", + "getrandom 0.3.3", + "once_cell", + "rustix", + "windows-sys 0.61.1", +] + +[[package]] +name = "textwrap" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057" +dependencies = [ + "smawk", +] + [[package]] name = "thiserror" version = "1.0.69" @@ -4848,12 +4875,6 @@ dependencies = [ "libc", ] -[[package]] -name = "unicase" -version = "2.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" - [[package]] name = "unicode-bidi" version = "0.3.18" @@ -4889,21 +4910,24 @@ checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" [[package]] name = "uniffi" -version = "0.25.3" +version = "0.29.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21345172d31092fd48c47fd56c53d4ae9e41c4b1f559fb8c38c1ab1685fd919f" +checksum = "c6d968cb62160c11f2573e6be724ef8b1b18a277aededd17033f8a912d73e2b4" dependencies = [ "anyhow", + "cargo_metadata", + "uniffi_bindgen", "uniffi_build", "uniffi_core", "uniffi_macros", + "uniffi_pipeline", ] [[package]] name = "uniffi_bindgen" -version = "0.25.3" +version = "0.29.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd992f2929a053829d5875af1eff2ee3d7a7001cb3b9a46cc7895f2caede6940" +checksum = "f6b39ef1acbe1467d5d210f274fae344cb6f8766339330cb4c9688752899bf6b" dependencies = [ "anyhow", "askama", @@ -4912,21 +4936,24 @@ dependencies = [ "fs-err", "glob", "goblin", - "heck 0.4.1", + "heck 0.5.0", + "indexmap", "once_cell", - "paste", "serde", + "tempfile", + "textwrap", "toml 0.5.11", + "uniffi_internal_macros", "uniffi_meta", - "uniffi_testing", + "uniffi_pipeline", "uniffi_udl", ] [[package]] name = "uniffi_build" -version = "0.25.3" +version = "0.29.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "001964dd3682d600084b3aaf75acf9c3426699bc27b65e96bb32d175a31c74e9" +checksum = "6683e6b665423cddeacd89a3f97312cf400b2fb245a26f197adaf65c45d505b2" dependencies = [ "anyhow", "camino", @@ -4934,38 +4961,36 @@ dependencies = [ ] [[package]] -name = "uniffi_checksum_derive" -version = "0.25.3" +name = "uniffi_core" +version = "0.29.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55137c122f712d9330fd985d66fa61bdc381752e89c35708c13ce63049a3002c" +checksum = "c2d990b553d6b9a7ee9c3ae71134674739913d52350b56152b0e613595bb5a6f" dependencies = [ + "anyhow", + "bytes", + "once_cell", + "static_assertions", +] + +[[package]] +name = "uniffi_internal_macros" +version = "0.29.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04f4f224becf14885c10e6e400b95cc4d1985738140cb194ccc2044563f8a56b" +dependencies = [ + "anyhow", + "indexmap", + "proc-macro2", "quote", "syn", ] -[[package]] -name = "uniffi_core" -version = "0.25.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6121a127a3af1665cd90d12dd2b3683c2643c5103281d0fed5838324ca1fad5b" -dependencies = [ - "anyhow", - "bytes", - "camino", - "log", - "once_cell", - "oneshot-uniffi", - "paste", - "static_assertions", -] - [[package]] name = "uniffi_macros" -version = "0.25.3" +version = "0.29.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11cf7a58f101fcedafa5b77ea037999b88748607f0ef3a33eaa0efc5392e92e4" +checksum = "b481d385af334871d70904e6a5f129be7cd38c18fcf8dd8fd1f646b426a56d58" dependencies = [ - "bincode", "camino", "fs-err", "once_cell", @@ -4974,44 +4999,43 @@ dependencies = [ "serde", "syn", "toml 0.5.11", - "uniffi_build", "uniffi_meta", ] [[package]] name = "uniffi_meta" -version = "0.25.3" +version = "0.29.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71dc8573a7b1ac4b71643d6da34888273ebfc03440c525121f1b3634ad3417a2" +checksum = "10f817868a3b171bb7bf259e882138d104deafde65684689b4694c846d322491" dependencies = [ "anyhow", - "bytes", "siphasher", - "uniffi_checksum_derive", + "uniffi_internal_macros", + "uniffi_pipeline", ] [[package]] -name = "uniffi_testing" -version = "0.25.3" +name = "uniffi_pipeline" +version = "0.29.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "118448debffcb676ddbe8c5305fb933ab7e0123753e659a71dc4a693f8d9f23c" +checksum = "4b147e133ad7824e32426b90bc41fda584363563f2ba747f590eca1fd6fd14e6" dependencies = [ "anyhow", - "camino", - "cargo_metadata", - "fs-err", - "once_cell", + "heck 0.5.0", + "indexmap", + "tempfile", + "uniffi_internal_macros", ] [[package]] name = "uniffi_udl" -version = "0.25.3" +version = "0.29.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "889edb7109c6078abe0e53e9b4070cf74a6b3468d141bdf5ef1bd4d1dc24a1c3" +checksum = "caed654fb73da5abbc7a7e9c741532284532ba4762d6fe5071372df22a41730a" dependencies = [ "anyhow", + "textwrap", "uniffi_meta", - "uniffi_testing", "weedle2", ] @@ -5323,9 +5347,9 @@ dependencies = [ [[package]] name = "weedle2" -version = "4.0.0" +version = "5.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e79c5206e1f43a2306fd64bdb95025ee4228960f2e6c5a8b173f3caaf807741" +checksum = "998d2c24ec099a87daf9467808859f9d82b61f1d9c9701251aea037f514eae0e" dependencies = [ "nom", ] diff --git a/crates/commands/Cargo.toml b/crates/commands/Cargo.toml index e853ff83..6bc1d977 100644 --- a/crates/commands/Cargo.toml +++ b/crates/commands/Cargo.toml @@ -15,7 +15,7 @@ crate-type = ["cdylib", "lib"] lazy_static = { workspace = true } command_parser = { path = "../command_parser"} command_definitions = { path = "../command_definitions"} -uniffi = { version = "0.25" } +uniffi = { version = "0.29" } [build-dependencies] -uniffi = { version = "0.25", features = [ "build" ] } \ No newline at end of file +uniffi = { version = "0.29", features = [ "build" ] } \ No newline at end of file diff --git a/flake.lock b/flake.lock index 5ca2103f..8191b650 100644 --- a/flake.lock +++ b/flake.lock @@ -334,20 +334,20 @@ "uniffi-bindgen-cs": { "flake": false, "locked": { - "lastModified": 1725356776, - "narHash": "sha256-w4K8BWMfUxohWE0nWpy3Qbc2CtRQTQ4dbER+sls61WI=", - "ref": "refs/tags/v0.8.3+v0.25.0", - "rev": "f68639fbc720b50ebe561ba75c66c84dc456bdce", - "revCount": 110, + "lastModified": 1759932560, + "narHash": "sha256-CnfB7/n1W5hbeC+cniJZthkpWO9kLyog/q5ldL6yS9g=", + "ref": "refs/heads/main", + "rev": "66c316454a04c025a88f8cc8af495bfc2b422d2f", + "revCount": 181, "submodules": true, "type": "git", - "url": "https://github.com/NordSecurity/uniffi-bindgen-cs" + "url": "https://github.com/90-008/uniffi-bindgen-cs" }, "original": { - "ref": "refs/tags/v0.8.3+v0.25.0", + "ref": "refs/heads/main", "submodules": true, "type": "git", - "url": "https://github.com/NordSecurity/uniffi-bindgen-cs" + "url": "https://github.com/90-008/uniffi-bindgen-cs" } } }, diff --git a/flake.nix b/flake.nix index 68f01771..6a43619e 100644 --- a/flake.nix +++ b/flake.nix @@ -16,7 +16,7 @@ nci.inputs.nixpkgs.follows = "nixpkgs"; nci.inputs.dream2nix.follows = "d2n"; nci.inputs.treefmt.follows = "treefmt"; - uniffi-bindgen-cs.url = "git+https://github.com/NordSecurity/uniffi-bindgen-cs?ref=refs/tags/v0.8.3+v0.25.0&submodules=1"; + uniffi-bindgen-cs.url = "git+https://github.com/90-008/uniffi-bindgen-cs?ref=refs/heads/main&submodules=1"; uniffi-bindgen-cs.flake = false; # misc treefmt.url = "github:numtide/treefmt-nix"; @@ -45,8 +45,6 @@ uniffi-bindgen-cs = config.nci.lib.buildCrate { src = inp.uniffi-bindgen-cs; cratePath = "bindgen"; - # TODO: uniffi fails to build with our toolchain because the ahash dep that uniffi-bindgen-cs uses is too old and uses removed stdsimd feature - mkRustToolchain = pkgs: pkgs.cargo; }; rustOutputs = config.nci.outputs; From 0429fa636bf83b4a8c61692c2d6de9a8a7bb0b50 Mon Sep 17 00:00:00 2001 From: dusk Date: Wed, 8 Oct 2025 14:28:10 +0000 Subject: [PATCH 119/179] allow warnings for compiling uniffi --- .github/workflows/dotnet.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 1051c9cb..1a0d852e 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -31,6 +31,8 @@ jobs: dotnet-version: 8.0.x - uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + rustflags: "" - name: install uniffi run: cargo install uniffi-bindgen-cs --git https://github.com/90-008/uniffi-bindgen-cs From 77444bff2e359190dc5f22fbc1d61619d19917d1 Mon Sep 17 00:00:00 2001 From: dusk Date: Wed, 8 Oct 2025 17:57:56 +0000 Subject: [PATCH 120/179] add flags for public / private --- PluralKit.Bot/CommandSystem/Context/Context.cs | 2 +- crates/command_definitions/src/lib.rs | 2 ++ crates/commands/src/main.rs | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/PluralKit.Bot/CommandSystem/Context/Context.cs b/PluralKit.Bot/CommandSystem/Context/Context.cs index f179a8a3..3a8f61a2 100644 --- a/PluralKit.Bot/CommandSystem/Context/Context.cs +++ b/PluralKit.Bot/CommandSystem/Context/Context.cs @@ -194,7 +194,7 @@ public class Context public LookupContext LookupContextFor(SystemId systemId, bool? _hasPrivateOverride = null, bool? _hasPublicOverride = null) { - // TODO(yusdacra): these should be passed as a parameter to this method all the way from command tree + // todo(dusk): these should be passed as a parameter ideally bool hasPrivateOverride = _hasPrivateOverride ?? Parameters.HasFlag("private", "priv"); bool hasPublicOverride = _hasPublicOverride ?? Parameters.HasFlag("public", "pub"); diff --git a/crates/command_definitions/src/lib.rs b/crates/command_definitions/src/lib.rs index 0f20ddb9..40617f62 100644 --- a/crates/command_definitions/src/lib.rs +++ b/crates/command_definitions/src/lib.rs @@ -46,6 +46,8 @@ pub fn all() -> impl Iterator { .hidden_flag(("raw", ["r"])) .hidden_flag(("show-embed", ["se"])) .hidden_flag(("by-id", ["id"])) + .hidden_flag(("private", ["priv"])) + .hidden_flag(("public", ["pub"])) }) } diff --git a/crates/commands/src/main.rs b/crates/commands/src/main.rs index 8784b5c2..325fc469 100644 --- a/crates/commands/src/main.rs +++ b/crates/commands/src/main.rs @@ -4,7 +4,7 @@ use command_parser::Tree; use commands::COMMAND_TREE; fn main() { - related(); + parse(); } fn related() { From 323377c217a900e636cc45c2038745cd1df28f6f Mon Sep 17 00:00:00 2001 From: dusk Date: Wed, 8 Oct 2025 21:00:51 +0000 Subject: [PATCH 121/179] docker images? --- .github/workflows/dotnet-docker.yml | 20 ++++++++++---------- .github/workflows/rust-docker.yml | 20 ++++++++++---------- ci/Dockerfile.dotnet | 24 +++++++++++++++++++++++- 3 files changed, 43 insertions(+), 21 deletions(-) diff --git a/.github/workflows/dotnet-docker.yml b/.github/workflows/dotnet-docker.yml index 9939320b..3ad3b108 100644 --- a/.github/workflows/dotnet-docker.yml +++ b/.github/workflows/dotnet-docker.yml @@ -1,23 +1,23 @@ name: Build and push Docker image on: + workflow_dispatch: push: paths: - - '.dockerignore' - - '.github/workflows/dotnet-docker.yml' - - 'ci/Dockerfile.dotnet' - - 'ci/dotnet-version.sh' - - 'Myriad/**' - - 'PluralKit.API/**' - - 'PluralKit.Bot/**' - - 'PluralKit.Core/**' + - ".dockerignore" + - ".github/workflows/dotnet-docker.yml" + - "ci/Dockerfile.dotnet" + - "ci/dotnet-version.sh" + - "Myriad/**" + - "PluralKit.API/**" + - "PluralKit.Bot/**" + - "PluralKit.Core/**" jobs: build: - name: '.net docker build' + name: ".net docker build" runs-on: ubuntu-latest permissions: packages: write - if: github.repository == 'PluralKit/PluralKit' steps: - uses: docker/login-action@v1 with: diff --git a/.github/workflows/rust-docker.yml b/.github/workflows/rust-docker.yml index a46adf67..cde407f7 100644 --- a/.github/workflows/rust-docker.yml +++ b/.github/workflows/rust-docker.yml @@ -1,22 +1,22 @@ name: Build and push Rust service Docker images on: + workflow_dispatch: push: paths: - - 'crates/**' - - '.dockerignore' - - '.github/workflows/rust.yml' - - 'ci/Dockerfile.rust' - - 'ci/rust-docker-target.sh' - - 'Cargo.toml' - - 'Cargo.lock' + - "crates/**" + - ".dockerignore" + - ".github/workflows/rust.yml" + - "ci/Dockerfile.rust" + - "ci/rust-docker-target.sh" + - "Cargo.toml" + - "Cargo.lock" jobs: build: - name: 'rust docker build' + name: "rust docker build" runs-on: ubuntu-latest permissions: packages: write - if: github.repository == 'PluralKit/PluralKit' steps: - uses: docker/login-action@v1 if: ${{ !env.ACT }} @@ -35,7 +35,7 @@ jobs: # https://github.com/docker/build-push-action/issues/378 context: . file: ci/Dockerfile.rust - push: false + push: false cache-from: type=registry,ref=ghcr.io/pluralkit/docker-cache:rust cache-to: type=registry,ref=ghcr.io/pluralkit/docker-cache:rust,mode=max outputs: .docker-bin diff --git a/ci/Dockerfile.dotnet b/ci/Dockerfile.dotnet index c10952b3..000b7bae 100644 --- a/ci/Dockerfile.dotnet +++ b/ci/Dockerfile.dotnet @@ -2,6 +2,15 @@ FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build WORKDIR /app +RUN apt-get update && apt-get install -y curl build-essential && \ + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y + +ENV PATH="/root/.cargo/bin:${PATH}" +ENV RUSTFLAGS='-C link-arg=-s' + +# Install uniffi-bindgen-cs +RUN cargo install uniffi-bindgen-cs --git https://github.com/90-008/uniffi-bindgen-cs + # Restore/fetch dependencies excluding app code to make use of caching COPY PluralKit.sln /app/ COPY Myriad/Myriad.csproj /app/Myriad/ @@ -13,8 +22,21 @@ COPY .git/ /app/.git COPY Serilog/ /app/Serilog/ RUN dotnet restore PluralKit.sln -# Copy the rest of the code and build +# Copy the rest of the code COPY . /app + +# copy parser code +COPY Cargo.toml /app/ +COPY Cargo.lock /app/ + +COPY crates/ /app/crates + +# Generate command parser bindings +RUN mkdir -p app/obj && cargo -Z unstable-options build --package commands --lib --release --artifact-dir app/obj/ +RUN uniffi-bindgen-cs "app/obj/libcommands.so" --library --out-dir="app/PluralKit.Bot" +RUN cargo run --package commands --bin write_cs_glue -- "app/PluralKit.Bot/commandtypes.cs" + +# build bot RUN dotnet build -c Release -o bin # Build runtime stage (doesn't include SDK) From a9355654dfceac1002c9b33982e4e305e7c2f398 Mon Sep 17 00:00:00 2001 From: alyssa Date: Wed, 8 Oct 2025 21:50:58 +0000 Subject: [PATCH 122/179] fix docker build --- ci/Dockerfile.dotnet | 6 +++--- crates/commands/Cargo.toml | 4 ++-- crates/commands/src/{bin => }/write_cs_glue.rs | 0 3 files changed, 5 insertions(+), 5 deletions(-) rename crates/commands/src/{bin => }/write_cs_glue.rs (100%) diff --git a/ci/Dockerfile.dotnet b/ci/Dockerfile.dotnet index 000b7bae..6054e74c 100644 --- a/ci/Dockerfile.dotnet +++ b/ci/Dockerfile.dotnet @@ -32,9 +32,9 @@ COPY Cargo.lock /app/ COPY crates/ /app/crates # Generate command parser bindings -RUN mkdir -p app/obj && cargo -Z unstable-options build --package commands --lib --release --artifact-dir app/obj/ -RUN uniffi-bindgen-cs "app/obj/libcommands.so" --library --out-dir="app/PluralKit.Bot" -RUN cargo run --package commands --bin write_cs_glue -- "app/PluralKit.Bot/commandtypes.cs" +RUN mkdir -p /app/bin && cargo -Z unstable-options build --package commands --lib --release --artifact-dir /app/bin/ +RUN uniffi-bindgen-cs "/app/bin/libcommands.so" --library --out-dir="/app/PluralKit.Bot" +RUN cargo run --package commands --bin write_cs_glue -- "/app/PluralKit.Bot/commandtypes.cs" # build bot RUN dotnet build -c Release -o bin diff --git a/crates/commands/Cargo.toml b/crates/commands/Cargo.toml index 6bc1d977..6c274a33 100644 --- a/crates/commands/Cargo.toml +++ b/crates/commands/Cargo.toml @@ -6,7 +6,7 @@ default-run = "commands" [[bin]] name = "write_cs_glue" -path = "src/bin/write_cs_glue.rs" +path = "src/write_cs_glue.rs" [lib] crate-type = ["cdylib", "lib"] @@ -18,4 +18,4 @@ command_definitions = { path = "../command_definitions"} uniffi = { version = "0.29" } [build-dependencies] -uniffi = { version = "0.29", features = [ "build" ] } \ No newline at end of file +uniffi = { version = "0.29", features = [ "build" ] } diff --git a/crates/commands/src/bin/write_cs_glue.rs b/crates/commands/src/write_cs_glue.rs similarity index 100% rename from crates/commands/src/bin/write_cs_glue.rs rename to crates/commands/src/write_cs_glue.rs From bbed401314e9ba6960c8e41183f3ff3bbc7ed635 Mon Sep 17 00:00:00 2001 From: dusk Date: Sat, 11 Oct 2025 03:54:43 +0000 Subject: [PATCH 123/179] fix panic when operating inside graphemes while trying to find quotes --- crates/command_parser/src/lib.rs | 1 + crates/command_parser/src/string.rs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/command_parser/src/lib.rs b/crates/command_parser/src/lib.rs index db065cc5..1d2d0549 100644 --- a/crates/command_parser/src/lib.rs +++ b/crates/command_parser/src/lib.rs @@ -1,4 +1,5 @@ #![feature(anonymous_lifetime_in_impl_trait)] +#![feature(round_char_boundary)] pub mod command; pub mod flag; diff --git a/crates/command_parser/src/string.rs b/crates/command_parser/src/string.rs index 9b66da4c..73239699 100644 --- a/crates/command_parser/src/string.rs +++ b/crates/command_parser/src/string.rs @@ -48,7 +48,7 @@ lazy_static::lazy_static! { // quotes need to be at start/end of words, and are ignored if a closing quote is not present // WTB POSIX quoting: https://pubs.opengroup.org/onlinepubs/9799919799/utilities/V3_chap02.html fn find_quotes(match_str: &str) -> Option { - if let Some(right) = QUOTE_PAIRS.get(&match_str[0..1]) { + if let Some(right) = QUOTE_PAIRS.get(&match_str[0..match_str.ceil_char_boundary(1)]) { // try matching end quote for possible_quote in right.chars() { for (pos, _) in match_str.match_indices(possible_quote) { From da8ff942c91977ffb89c29174a06671facd6d750 Mon Sep 17 00:00:00 2001 From: dusk Date: Sat, 11 Oct 2025 04:19:25 +0000 Subject: [PATCH 124/179] fix switch commands ordering so they work properly (sw, sw edit out) --- crates/command_definitions/src/switch.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/command_definitions/src/switch.rs b/crates/command_definitions/src/switch.rs index 42a3fda7..94f0e0d8 100644 --- a/crates/command_definitions/src/switch.rs +++ b/crates/command_definitions/src/switch.rs @@ -17,14 +17,14 @@ pub fn cmds() -> impl Iterator { ]; [ + command!(switch, ("commands", ["help"]) => "switch_commands"), command!(switch, out => "switch_out"), - command!(switch, r#move, OpaqueString => "switch_move"), // TODO: datetime parsing command!(switch, delete => "switch_delete").flag(("all", ["clear", "c"])), + command!(switch, r#move, OpaqueString => "switch_move"), // TODO: datetime parsing command!(switch, edit, out => "switch_edit_out").flag(YES), command!(switch, edit, Optional(MemberRefs) => "switch_edit").flags(edit_flags), command!(switch, copy, Optional(MemberRefs) => "switch_copy").flags(edit_flags), - command!(switch, ("commands", ["help"]) => "switch_commands"), - command!(switch, Optional(MemberRefs) => "switch_do"), + command!(switch, MemberRefs => "switch_do"), ] .into_iter() } From 7dd2a4e7e151e1fcf5600210bc0a1e872c221c65 Mon Sep 17 00:00:00 2001 From: dusk Date: Sat, 11 Oct 2025 05:09:53 +0000 Subject: [PATCH 125/179] add system list alias to top level --- crates/command_definitions/src/system.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/command_definitions/src/system.rs b/crates/command_definitions/src/system.rs index 4d3b7c4c..b398eee2 100644 --- a/crates/command_definitions/src/system.rs +++ b/crates/command_definitions/src/system.rs @@ -266,7 +266,7 @@ pub fn edit() -> impl Iterator { ] .into_iter(); - let system_list = ("members", ["list"]); + let system_list = ("members", ["list", "l", "find", "f"]); let system_search = tokens!( ("search", ["query", "find"]), ("query", OpaqueStringRemainder), @@ -279,6 +279,7 @@ pub fn edit() -> impl Iterator { .into_iter() .map(add_list_flags); let system_list_self_cmd = [ + command!(system_list => "system_members_list_self"), command!(system, system_list => "system_members_list_self"), command!(system, system_search => "system_members_search_self"), ] From 42c94299538353c89afd99540fa454cd962f58db Mon Sep 17 00:00:00 2001 From: dusk Date: Sat, 11 Oct 2025 05:17:13 +0000 Subject: [PATCH 126/179] hide admin commands from suggestions etc. --- PluralKit.Bot/CommandMeta/CommandParseErrors.cs | 6 ++++++ crates/command_definitions/src/admin.rs | 1 + crates/commands/src/lib.rs | 3 +++ 3 files changed, 10 insertions(+) diff --git a/PluralKit.Bot/CommandMeta/CommandParseErrors.cs b/PluralKit.Bot/CommandMeta/CommandParseErrors.cs index 5b54df32..fae70988 100644 --- a/PluralKit.Bot/CommandMeta/CommandParseErrors.cs +++ b/PluralKit.Bot/CommandMeta/CommandParseErrors.cs @@ -6,6 +6,12 @@ public partial class CommandTree { private async Task PrintCommandList(Context ctx, string subject, string commands) { + if (commands.Length == 0) + { + await ctx.Reply($"No commands related to {subject} was found. For the full list of commands, see the website: "); + return; + } + await ctx.Reply( components: [ new MessageComponent() diff --git a/crates/command_definitions/src/admin.rs b/crates/command_definitions/src/admin.rs index 032f137a..73a8e892 100644 --- a/crates/command_definitions/src/admin.rs +++ b/crates/command_definitions/src/admin.rs @@ -71,4 +71,5 @@ pub fn cmds() -> impl Iterator { ] .into_iter() .chain(abuselog_cmds) + .map(|cmd| cmd.show_in_suggestions(false)) } diff --git a/crates/commands/src/lib.rs b/crates/commands/src/lib.rs index 959683c7..ed67b282 100644 --- a/crates/commands/src/lib.rs +++ b/crates/commands/src/lib.rs @@ -147,6 +147,9 @@ pub fn parse_command(prefix: String, input: String) -> CommandResult { pub fn get_related_commands(prefix: String, input: String) -> String { let mut s = String::new(); for command in command_definitions::all() { + if !command.show_in_suggestions { + continue; + } if command.tokens.first().map_or(false, |token| { token .try_match(Some(&input)) From ca9f25ff6407ebd1f1ce99c739d5b621595e90bc Mon Sep 17 00:00:00 2001 From: dusk Date: Sat, 11 Oct 2025 05:49:01 +0000 Subject: [PATCH 127/179] use correct name for resolving flags and params in codegen --- PluralKit.Bot/CommandMeta/CommandParseErrors.cs | 4 ++-- crates/commands/src/write_cs_glue.rs | 15 +++++++++------ 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/PluralKit.Bot/CommandMeta/CommandParseErrors.cs b/PluralKit.Bot/CommandMeta/CommandParseErrors.cs index fae70988..ab9c9fae 100644 --- a/PluralKit.Bot/CommandMeta/CommandParseErrors.cs +++ b/PluralKit.Bot/CommandMeta/CommandParseErrors.cs @@ -8,7 +8,7 @@ public partial class CommandTree { if (commands.Length == 0) { - await ctx.Reply($"No commands related to {subject} was found. For the full list of commands, see the website: "); + await ctx.Reply($"No commands related to `{subject}` was found. For the full list of commands, see the website: "); return; } @@ -17,7 +17,7 @@ public partial class CommandTree new MessageComponent() { Type = ComponentType.Text, - Content = $"Here is a list of commands related to {subject}:\n{commands}\nFor a full list of possible commands, see .", + Content = $"Here is a list of commands related to `{subject}`:\n{commands}\nFor a full list of possible commands, see .", } ] ); diff --git a/crates/commands/src/write_cs_glue.rs b/crates/commands/src/write_cs_glue.rs index d6486a71..05340595 100644 --- a/crates/commands/src/write_cs_glue.rs +++ b/crates/commands/src/write_cs_glue.rs @@ -45,8 +45,9 @@ fn main() -> Result<(), Box> { for param in &command_params { writeln!( &mut command_params_init, - r#"@{name} = await ctx.ParamResolve{extract_fn_name}("{name}"){throw_null},"#, - name = param.name().replace("-", "_"), + r#"@{fieldName} = await ctx.ParamResolve{extract_fn_name}("{name}"){throw_null},"#, + fieldName = param.name().replace("-", "_"), + name = param.name(), extract_fn_name = get_param_param_ty(param.kind()), throw_null = param .is_optional() @@ -59,15 +60,17 @@ fn main() -> Result<(), Box> { if let Some(param) = flag.get_value() { writeln!( &mut command_flags_init, - r#"@{name} = await ctx.FlagResolve{extract_fn_name}("{name}"),"#, - name = flag.get_name().replace("-", "_"), + r#"@{fieldName} = await ctx.FlagResolve{extract_fn_name}("{name}"),"#, + fieldName = flag.get_name().replace("-", "_"), + name = flag.get_name(), extract_fn_name = get_param_param_ty(param.kind()), )?; } else { writeln!( &mut command_flags_init, - r#"@{name} = ctx.Parameters.HasFlag("{name}"),"#, - name = flag.get_name().replace("-", "_"), + r#"@{fieldName} = ctx.Parameters.HasFlag("{name}"),"#, + fieldName = flag.get_name().replace("-", "_"), + name = flag.get_name(), )?; } } From 134855f8f868113dfe51b9b3be98e869ae9ead75 Mon Sep 17 00:00:00 2001 From: dusk Date: Mon, 13 Oct 2025 08:16:53 +0000 Subject: [PATCH 128/179] fix reproxy command not accepting member ref --- PluralKit.Bot/CommandMeta/CommandTree.cs | 3 +- crates/command_definitions/src/member.rs | 6 ++-- crates/command_definitions/src/message.rs | 18 ++++++---- crates/command_parser/src/lib.rs | 41 +++++++++++++++++++---- crates/command_parser/src/token.rs | 3 +- 5 files changed, 52 insertions(+), 19 deletions(-) diff --git a/PluralKit.Bot/CommandMeta/CommandTree.cs b/PluralKit.Bot/CommandMeta/CommandTree.cs index 72a43288..9420b80a 100644 --- a/PluralKit.Bot/CommandMeta/CommandTree.cs +++ b/PluralKit.Bot/CommandMeta/CommandTree.cs @@ -283,7 +283,8 @@ public partial class CommandTree Commands.MessageAuthor(var param, var flags) => ctx.Execute(Message, m => m.GetMessage(ctx, param.target.MessageId, flags.GetReplyFormat(), false, true)), Commands.MessageDelete(var param, var flags) => ctx.Execute(Message, m => m.GetMessage(ctx, param.target.MessageId, flags.GetReplyFormat(), true, false)), Commands.MessageEdit(var param, var flags) => ctx.Execute(MessageEdit, m => m.EditMessage(ctx, param.target.MessageId, param.new_content, flags.regex, flags.mutate_space, flags.append, flags.prepend, flags.clear_embeds, flags.clear_attachments)), - Commands.MessageReproxy(var param, _) => ctx.Execute(MessageReproxy, m => m.ReproxyMessage(ctx, param.msg.MessageId, param.member)), + Commands.MessageReproxySpecified(var param, _) => ctx.Execute(MessageReproxy, m => m.ReproxyMessage(ctx, param.msg.MessageId, param.member)), + Commands.MessageReproxy(var param, _) => ctx.Execute(MessageReproxy, m => m.ReproxyMessage(ctx, null, param.member)), Commands.Import(var param, var flags) => ctx.Execute(Import, m => m.Import(ctx, param.url, flags.yes)), Commands.Export(_, _) => ctx.Execute(Export, m => m.Export(ctx)), Commands.ServerConfigShow => ctx.Execute(null, m => m.ShowConfig(ctx)), diff --git a/crates/command_definitions/src/member.rs b/crates/command_definitions/src/member.rs index e18a04e3..f35bb3d8 100644 --- a/crates/command_definitions/src/member.rs +++ b/crates/command_definitions/src/member.rs @@ -183,15 +183,15 @@ pub fn cmds() -> impl Iterator { [ command!(member_keep_proxy => "member_keepproxy_show") .help("Shows a member's keep-proxy setting"), - command!(member_keep_proxy, Skip(("value", Toggle)) => "member_keepproxy_update") + command!(member_keep_proxy, ("value", Toggle) => "member_keepproxy_update") .help("Changes a member's keep-proxy setting"), command!(member_server_keep_proxy => "member_server_keepproxy_show") .help("Shows a member's server-specific keep-proxy setting"), - command!(member_server_keep_proxy, Skip(("value", Toggle)) => "member_server_keepproxy_update") - .help("Changes a member's server-specific keep-proxy setting"), command!(member_server_keep_proxy, CLEAR => "member_server_keepproxy_clear") .flag(YES) .help("Clears a member's server-specific keep-proxy setting"), + command!(member_server_keep_proxy, ("value", Toggle) => "member_server_keepproxy_update") + .help("Changes a member's server-specific keep-proxy setting"), ].into_iter() }; diff --git a/crates/command_definitions/src/message.rs b/crates/command_definitions/src/message.rs index e36969c2..06a5c0ca 100644 --- a/crates/command_definitions/src/message.rs +++ b/crates/command_definitions/src/message.rs @@ -3,6 +3,10 @@ use super::*; pub fn cmds() -> impl Iterator { let message = tokens!(("message", ["msg", "messageinfo"]), MessageRef); + let author = ("author", ["sender", "a"]); + let delete = ("delete", ["del", "d"]); + let reproxy = ("reproxy", ["rp", "crimes", "crime"]); + let edit = tokens!(("edit", ["e"]), ("new_content", OpaqueStringRemainder)); let apply_edit = |cmd: Command| { cmd.flag(("append", ["a"])) @@ -16,16 +20,16 @@ pub fn cmds() -> impl Iterator { [ command!(message => "message_info") - .flag(("delete", ["d"])) - .flag(("author", ["a"])) + .flag(delete) + .flag(author) .help("Shows information about a proxied message"), - command!(message, ("author", ["sender"]) => "message_author") - .help("Shows the author of a proxied message"), - command!(message, ("delete", ["del"]) => "message_delete") - .help("Deletes a proxied message"), + command!(message, author => "message_author").help("Shows the author of a proxied message"), + command!(message, delete => "message_delete").help("Deletes a proxied message"), apply_edit(command!(message, edit => "message_edit")), apply_edit(command!(edit => "message_edit")), - command!(("reproxy", ["rp", "crimes", "crime"]), ("msg", MessageRef), ("member", MemberRef) => "message_reproxy") + command!(reproxy, ("member", MemberRef) => "message_reproxy") + .help("Reproxies a message with a different member"), + command!(reproxy, ("msg", MessageRef), ("member", MemberRef) => "message_reproxy_specified") .help("Reproxies a message with a different member"), ] .into_iter() diff --git a/crates/command_parser/src/lib.rs b/crates/command_parser/src/lib.rs index 1d2d0549..f33021cf 100644 --- a/crates/command_parser/src/lib.rs +++ b/crates/command_parser/src/lib.rs @@ -47,14 +47,25 @@ pub fn parse_command( let mut params: HashMap = HashMap::new(); let mut raw_flags: Vec<(usize, MatchedFlag)> = Vec::new(); + let mut matched_tokens: Vec<(Tree, (Token, TokenMatchResult, usize))> = Vec::new(); + let mut filtered_tokens: Vec = Vec::new(); loop { println!( "possible: {:?}", - local_tree.possible_tokens().collect::>() + local_tree + .possible_tokens() + .filter(|t| filtered_tokens.contains(t)) + .collect::>() + ); + let next = next_token( + local_tree + .possible_tokens() + .filter(|t| !filtered_tokens.contains(t)), + &input, + current_pos, ); - let next = next_token(local_tree.possible_tokens(), &input, current_pos); println!("next: {:?}", next); - match next { + match &next { Some((found_token, result, new_pos)) => { match &result { // todo: better error messages for these? @@ -74,21 +85,37 @@ pub fn parse_command( // add parameter if any if let TokenMatchResult::MatchedParameter { name, value } = result { - params.insert(name.to_string(), value); + params.insert(name.to_string(), value.clone()); } // move to the next branch if let Some(next_tree) = local_tree.get_branch(&found_token) { + matched_tokens.push(( + local_tree.clone(), + (found_token.clone(), result.clone(), *new_pos), + )); + filtered_tokens.clear(); // new branch, new tokens local_tree = next_tree.clone(); } else { panic!("found token {found_token:?} could not match tree, at {input}"); } // advance our position on the input - current_pos = new_pos; + current_pos = *new_pos; current_token_idx += 1; } None => { + // redo the previous branches if we didnt match on a parameter + // this is a bit of a hack, but its necessary for making parameters on the same depth work + if let Some((match_tree, match_next)) = matched_tokens + .pop() + .and_then(|m| matches!(m.1, (Token::Parameter(_), _, _)).then_some(m)) + { + local_tree = match_tree; + filtered_tokens.push(match_next.0); + continue; + } + let mut error = format!("Unknown command `{prefix}{input}`."); let possible_commands = @@ -239,7 +266,7 @@ fn next_token<'a>( possible_tokens: impl Iterator, input: &str, current_pos: usize, -) -> Option<(&'a Token, TokenMatchResult, usize)> { +) -> Option<(Token, TokenMatchResult, usize)> { // get next parameter, matching quotes let matched = string::next_param(&input, current_pos); println!("matched: {matched:?}\n---"); @@ -267,7 +294,7 @@ fn next_token<'a>( match token.try_match(input_to_match) { Some(result) => { //println!("matched token: {}", token); - return Some((token, result, next_pos)); + return Some((token.clone(), result, next_pos)); } None => {} // continue matching until we exhaust all tokens } diff --git a/crates/command_parser/src/token.rs b/crates/command_parser/src/token.rs index e87c702a..99fc6fb3 100644 --- a/crates/command_parser/src/token.rs +++ b/crates/command_parser/src/token.rs @@ -16,7 +16,7 @@ pub enum Token { Parameter(Parameter), } -#[derive(Debug)] +#[derive(Clone, Debug)] pub enum TokenMatchResult { MatchedValue, MatchedParameter { @@ -36,6 +36,7 @@ pub enum TokenMatchResult { // a: because we want to differentiate between no match and match failure (it matched with an error) // "no match" has a different charecteristic because we want to continue matching other tokens... // ...while "match failure" means we should stop matching and return the error +// Option fits this better (and it makes some code look a bit nicer) pub type TryMatchResult = Option; impl Token { From a307dd37c4d04014775f9af38c7e429592844f3c Mon Sep 17 00:00:00 2001 From: dusk Date: Mon, 13 Oct 2025 10:25:55 +0000 Subject: [PATCH 129/179] fix message edit commands, parse DM message links for ids --- PluralKit.Bot/CommandMeta/CommandTree.cs | 3 +- PluralKit.Bot/CommandSystem/Parameters.cs | 40 ++++++++++-------- crates/command_definitions/src/message.rs | 7 ++-- crates/command_parser/src/lib.rs | 16 +++---- crates/command_parser/src/parameter.rs | 51 ++++++++++++++++------- crates/commands/src/write_cs_glue.rs | 4 +- 6 files changed, 74 insertions(+), 47 deletions(-) diff --git a/PluralKit.Bot/CommandMeta/CommandTree.cs b/PluralKit.Bot/CommandMeta/CommandTree.cs index 9420b80a..d7c32fb4 100644 --- a/PluralKit.Bot/CommandMeta/CommandTree.cs +++ b/PluralKit.Bot/CommandMeta/CommandTree.cs @@ -282,7 +282,8 @@ public partial class CommandTree Commands.MessageInfo(var param, var flags) => ctx.Execute(Message, m => m.GetMessage(ctx, param.target.MessageId, flags.GetReplyFormat(), flags.delete, flags.author)), Commands.MessageAuthor(var param, var flags) => ctx.Execute(Message, m => m.GetMessage(ctx, param.target.MessageId, flags.GetReplyFormat(), false, true)), Commands.MessageDelete(var param, var flags) => ctx.Execute(Message, m => m.GetMessage(ctx, param.target.MessageId, flags.GetReplyFormat(), true, false)), - Commands.MessageEdit(var param, var flags) => ctx.Execute(MessageEdit, m => m.EditMessage(ctx, param.target.MessageId, param.new_content, flags.regex, flags.mutate_space, flags.append, flags.prepend, flags.clear_embeds, flags.clear_attachments)), + Commands.MessageEditSpecified(var param, var flags) => ctx.Execute(MessageEdit, m => m.EditMessage(ctx, param.target.MessageId, param.new_content, flags.regex, flags.mutate_space, flags.append, flags.prepend, flags.clear_embeds, flags.clear_attachments)), + Commands.MessageEdit(var param, var flags) => ctx.Execute(MessageEdit, m => m.EditMessage(ctx, null, param.new_content, flags.regex, flags.mutate_space, flags.append, flags.prepend, flags.clear_embeds, flags.clear_attachments)), Commands.MessageReproxySpecified(var param, _) => ctx.Execute(MessageReproxy, m => m.ReproxyMessage(ctx, param.msg.MessageId, param.member)), Commands.MessageReproxy(var param, _) => ctx.Execute(MessageReproxy, m => m.ReproxyMessage(ctx, null, param.member)), Commands.Import(var param, var flags) => ctx.Execute(Import, m => m.Import(ctx, param.url, flags.yes)), diff --git a/PluralKit.Bot/CommandSystem/Parameters.cs b/PluralKit.Bot/CommandSystem/Parameters.cs index 0278f661..cbde613d 100644 --- a/PluralKit.Bot/CommandSystem/Parameters.cs +++ b/PluralKit.Bot/CommandSystem/Parameters.cs @@ -9,24 +9,24 @@ namespace PluralKit.Bot; // corresponds to the ffi Paramater type, but with stricter types (also avoiding exposing ffi types!) public abstract record Parameter() { - public record MemberRef(PKMember member): Parameter; - public record MemberRefs(List members): Parameter; - public record GroupRef(PKGroup group): Parameter; - public record GroupRefs(List groups): Parameter; - public record SystemRef(PKSystem system): Parameter; - public record UserRef(User user): Parameter; - public record MessageRef(Message.Reference message): Parameter; - public record ChannelRef(Channel channel): Parameter; - public record GuildRef(Guild guild): Parameter; - public record MemberPrivacyTarget(MemberPrivacySubject target): Parameter; - public record GroupPrivacyTarget(GroupPrivacySubject target): Parameter; - public record SystemPrivacyTarget(SystemPrivacySubject target): Parameter; - public record PrivacyLevel(Core.PrivacyLevel level): Parameter; - public record Toggle(bool value): Parameter; - public record Opaque(string value): Parameter; - public record Number(int value): Parameter; - public record Avatar(ParsedImage avatar): Parameter; - public record ProxySwitchAction(SystemConfig.ProxySwitchAction action): Parameter; + public record MemberRef(PKMember member) : Parameter; + public record MemberRefs(List members) : Parameter; + public record GroupRef(PKGroup group) : Parameter; + public record GroupRefs(List groups) : Parameter; + public record SystemRef(PKSystem system) : Parameter; + public record UserRef(User user) : Parameter; + public record MessageRef(Message.Reference message) : Parameter; + public record ChannelRef(Channel channel) : Parameter; + public record GuildRef(Guild guild) : Parameter; + public record MemberPrivacyTarget(MemberPrivacySubject target) : Parameter; + public record GroupPrivacyTarget(GroupPrivacySubject target) : Parameter; + public record SystemPrivacyTarget(SystemPrivacySubject target) : Parameter; + public record PrivacyLevel(Core.PrivacyLevel level) : Parameter; + public record Toggle(bool value) : Parameter; + public record Opaque(string value) : Parameter; + public record Number(int value) : Parameter; + public record Avatar(ParsedImage avatar) : Parameter; + public record ProxySwitchAction(SystemConfig.ProxySwitchAction action) : Parameter; } public class Parameters @@ -48,6 +48,10 @@ public class Parameters _cb = command.@commandRef; _flags = command.@flags; _params = command.@params; + foreach (var param in _params) + { + Console.WriteLine($"{param.Key}: {param.Value}"); + } } else { diff --git a/crates/command_definitions/src/message.rs b/crates/command_definitions/src/message.rs index 06a5c0ca..f9bfec49 100644 --- a/crates/command_definitions/src/message.rs +++ b/crates/command_definitions/src/message.rs @@ -7,7 +7,7 @@ pub fn cmds() -> impl Iterator { let delete = ("delete", ["del", "d"]); let reproxy = ("reproxy", ["rp", "crimes", "crime"]); - let edit = tokens!(("edit", ["e"]), ("new_content", OpaqueStringRemainder)); + let edit = ("edit", ["e"]); let apply_edit = |cmd: Command| { cmd.flag(("append", ["a"])) .flag(("prepend", ["p"])) @@ -25,8 +25,9 @@ pub fn cmds() -> impl Iterator { .help("Shows information about a proxied message"), command!(message, author => "message_author").help("Shows the author of a proxied message"), command!(message, delete => "message_delete").help("Deletes a proxied message"), - apply_edit(command!(message, edit => "message_edit")), - apply_edit(command!(edit => "message_edit")), + apply_edit(command!(message, edit, ("new_content", OpaqueStringRemainder) => "message_edit_specified")), + apply_edit(command!(edit, Skip(MessageRef), ("new_content", OpaqueStringRemainder) => "message_edit_specified")), + apply_edit(command!(edit, ("new_content", OpaqueStringRemainder) => "message_edit")), command!(reproxy, ("member", MemberRef) => "message_reproxy") .help("Reproxies a message with a different member"), command!(reproxy, ("msg", MessageRef), ("member", MemberRef) => "message_reproxy_specified") diff --git a/crates/command_parser/src/lib.rs b/crates/command_parser/src/lib.rs index f33021cf..52807e7f 100644 --- a/crates/command_parser/src/lib.rs +++ b/crates/command_parser/src/lib.rs @@ -50,13 +50,13 @@ pub fn parse_command( let mut matched_tokens: Vec<(Tree, (Token, TokenMatchResult, usize))> = Vec::new(); let mut filtered_tokens: Vec = Vec::new(); loop { - println!( - "possible: {:?}", - local_tree - .possible_tokens() - .filter(|t| filtered_tokens.contains(t)) - .collect::>() - ); + // println!( + // "possible: {:?}", + // local_tree + // .possible_tokens() + // .filter(|t| filtered_tokens.contains(t)) + // .collect::>() + // ); let next = next_token( local_tree .possible_tokens() @@ -64,7 +64,7 @@ pub fn parse_command( &input, current_pos, ); - println!("next: {:?}", next); + // println!("next: {:?}", next); match &next { Some((found_token, result, new_pos)) => { match &result { diff --git a/crates/command_parser/src/parameter.rs b/crates/command_parser/src/parameter.rs index 77bfde8b..eaab7c8c 100644 --- a/crates/command_parser/src/parameter.rs +++ b/crates/command_parser/src/parameter.rs @@ -135,32 +135,41 @@ impl Parameter { return Ok(ParameterValue::MessageRef(None, None, message_id)); } - static RE: std::sync::LazyLock = std::sync::LazyLock::new(|| { + static SERVER_RE: std::sync::LazyLock = std::sync::LazyLock::new( + || { + regex::Regex::new( + r"https://(?:\w+\.)?discord(?:app)?\.com/channels/(?P\d+)/(?P\d+)/(?P\d+)", + ) + .unwrap() + }, + ); + + static DM_RE: std::sync::LazyLock = std::sync::LazyLock::new(|| { regex::Regex::new( - r"https://(?:\w+\.)?discord(?:app)?\.com/channels/(\d+)/(\d+)/(\d+)", + r"https://(?:\w+\.)?discord(?:app)?\.com/channels/@me/(?P\d+)/(?P\d+)", ) .unwrap() }); - if let Some(captures) = RE.captures(input) { - let guild_id = captures - .get(1) - .and_then(|m| m.as_str().parse::().ok()) - .ok_or_else(|| SmolStr::new("invalid guild ID in message link"))?; - let channel_id = captures - .get(2) - .and_then(|m| m.as_str().parse::().ok()) - .ok_or_else(|| SmolStr::new("invalid channel ID in message link"))?; - let message_id = captures - .get(3) - .and_then(|m| m.as_str().parse::().ok()) - .ok_or_else(|| SmolStr::new("invalid message ID in message link"))?; + if let Some(captures) = SERVER_RE.captures(input) { + let guild_id = captures.parse_id("guild")?; + let channel_id = captures.parse_id("channel")?; + let message_id = captures.parse_id("message")?; Ok(ParameterValue::MessageRef( Some(guild_id), Some(channel_id), message_id, )) + } else if let Some(captures) = DM_RE.captures(input) { + let channel_id = captures.parse_id("channel")?; + let message_id = captures.parse_id("message")?; + + Ok(ParameterValue::MessageRef( + None, + Some(channel_id), + message_id, + )) } else { Err(SmolStr::new("invalid message reference")) } @@ -560,3 +569,15 @@ impl FromStr for ProxySwitchAction { .ok_or_else(|| SmolStr::new("invalid proxy switch action, must be new/add/off")) } } + +trait ParseMessageLink { + fn parse_id(&self, name: &str) -> Result; +} + +impl ParseMessageLink for regex::Captures<'_> { + fn parse_id(&self, name: &str) -> Result { + self.name(name) + .and_then(|m| m.as_str().parse::().ok()) + .ok_or_else(|| SmolStr::new(format!("invalid {} in message link", name))) + } +} diff --git a/crates/commands/src/write_cs_glue.rs b/crates/commands/src/write_cs_glue.rs index 05340595..de1b5e74 100644 --- a/crates/commands/src/write_cs_glue.rs +++ b/crates/commands/src/write_cs_glue.rs @@ -51,8 +51,8 @@ fn main() -> Result<(), Box> { extract_fn_name = get_param_param_ty(param.kind()), throw_null = param .is_optional() - .then_some("") - .unwrap_or(" ?? throw new PKError(\"this is a bug\")"), + .then(String::new) + .unwrap_or(format!(" ?? throw new PKError(\"parameter {} not found but was required, this is a bug in the command parser, for command: {}!\")", param.name(), command.cb)), )?; } let mut command_flags_init = String::new(); From 2fe747d704c0f9cd60aff78b30599ebcb4e133b1 Mon Sep 17 00:00:00 2001 From: dusk Date: Wed, 15 Oct 2025 19:10:44 +0000 Subject: [PATCH 130/179] fix some parameters that should parse remainder but werent --- crates/command_definitions/src/group.rs | 23 +++++++++++++++-------- crates/command_definitions/src/switch.rs | 2 +- crates/command_definitions/src/system.rs | 22 +++++++++++----------- crates/command_parser/src/parameter.rs | 2 +- 4 files changed, 28 insertions(+), 21 deletions(-) diff --git a/crates/command_definitions/src/group.rs b/crates/command_definitions/src/group.rs index 36ef917b..63d3f7a5 100644 --- a/crates/command_definitions/src/group.rs +++ b/crates/command_definitions/src/group.rs @@ -17,9 +17,11 @@ pub fn cmds() -> impl Iterator { let group_target = targeted(); let group_new = tokens!(group, ("new", ["n"])); - let group_new_cmd = - [command!(group_new, ("name", OpaqueString) => "group_new").help("Creates a new group")] - .into_iter(); + let group_new_cmd = [ + command!(group_new, ("name", OpaqueStringRemainder) => "group_new") + .help("Creates a new group"), + ] + .into_iter(); let group_info_cmd = [command!(group_target => "group_info") .flag(ALL) @@ -35,7 +37,8 @@ pub fn cmds() -> impl Iterator { command!(group_name, CLEAR => "group_clear_name") .flag(YES) .help("Clears the group's name"), - command!(group_name, ("name", OpaqueString) => "group_rename").help("Renames the group"), + command!(group_name, ("name", OpaqueStringRemainder) => "group_rename") + .help("Renames the group"), ] .into_iter(); @@ -46,7 +49,7 @@ pub fn cmds() -> impl Iterator { command!(group_display_name, CLEAR => "group_clear_display_name") .flag(YES) .help("Clears the group's display name"), - command!(group_display_name, ("name", OpaqueString) => "group_change_display_name") + command!(group_display_name, ("name", OpaqueStringRemainder) => "group_change_display_name") .help("Changes the group's display name"), ] .into_iter(); @@ -64,7 +67,7 @@ pub fn cmds() -> impl Iterator { command!(group_description, CLEAR => "group_clear_description") .flag(YES) .help("Clears the group's description"), - command!(group_description, ("description", OpaqueString) => "group_change_description") + command!(group_description, ("description", OpaqueStringRemainder) => "group_change_description") .help("Changes the group's description"), ] .into_iter(); @@ -149,11 +152,15 @@ pub fn cmds() -> impl Iterator { let apply_list_opts = |cmd: Command| cmd.flags(get_list_flags()); + let search = tokens!( + ("search", ["find", "query"]), + ("query", OpaqueStringRemainder) + ); let group_list_members = tokens!(group_target, ("members", ["list", "ls"])); let group_list_members_cmd = [ command!(group_list_members => "group_list_members"), command!(group_list_members, "list" => "group_list_members"), - command!(group_list_members, ("search", ["find", "query"]), ("query", OpaqueStringRemainder) => "group_search_members"), + command!(group_list_members, search => "group_search_members"), ] .into_iter() .map(apply_list_opts); @@ -168,7 +175,7 @@ pub fn cmds() -> impl Iterator { let system_groups_cmd = [ command!(group, ("list", ["ls"]) => "group_list_groups"), - command!(group, ("search", ["find", "query"]), ("query", OpaqueStringRemainder) => "group_search_groups"), + command!(group, search => "group_search_groups"), ] .into_iter() .map(apply_list_opts); diff --git a/crates/command_definitions/src/switch.rs b/crates/command_definitions/src/switch.rs index 94f0e0d8..ab5167c3 100644 --- a/crates/command_definitions/src/switch.rs +++ b/crates/command_definitions/src/switch.rs @@ -20,7 +20,7 @@ pub fn cmds() -> impl Iterator { command!(switch, ("commands", ["help"]) => "switch_commands"), command!(switch, out => "switch_out"), command!(switch, delete => "switch_delete").flag(("all", ["clear", "c"])), - command!(switch, r#move, OpaqueString => "switch_move"), // TODO: datetime parsing + command!(switch, r#move, OpaqueStringRemainder => "switch_move"), // TODO: datetime parsing command!(switch, edit, out => "switch_edit_out").flag(YES), command!(switch, edit, Optional(MemberRefs) => "switch_edit").flags(edit_flags), command!(switch, copy, Optional(MemberRefs) => "switch_copy").flags(edit_flags), diff --git a/crates/command_definitions/src/system.rs b/crates/command_definitions/src/system.rs index b398eee2..4b4847cf 100644 --- a/crates/command_definitions/src/system.rs +++ b/crates/command_definitions/src/system.rs @@ -23,7 +23,7 @@ pub fn edit() -> impl Iterator { let system_new = tokens!(system, ("new", ["n"])); let system_new_cmd = [ command!(system_new => "system_new").help("Creates a new system"), - command!(system_new, ("name", OpaqueString) => "system_new_name") + command!(system_new, ("name", OpaqueStringRemainder) => "system_new_name") .help("Creates a new system (using the provided name)"), ] .into_iter(); @@ -62,7 +62,7 @@ pub fn edit() -> impl Iterator { command!(system_name_self, CLEAR => "system_clear_name") .flag(YES) .help("Clears your system's name"), - command!(system_name_self, ("name", OpaqueString) => "system_rename") + command!(system_name_self, ("name", OpaqueStringRemainder) => "system_rename") .help("Renames your system"), ] .into_iter(); @@ -80,7 +80,7 @@ pub fn edit() -> impl Iterator { command!(system_server_name_self, CLEAR => "system_clear_server_name") .flag(YES) .help("Clears your system's server name"), - command!(system_server_name_self, ("name", OpaqueString) => "system_rename_server_name") + command!(system_server_name_self, ("name", OpaqueStringRemainder) => "system_rename_server_name") .help("Renames your system's server name"), ] .into_iter(); @@ -97,7 +97,7 @@ pub fn edit() -> impl Iterator { command!(system_description_self, CLEAR => "system_clear_description") .flag(YES) .help("Clears your system's description"), - command!(system_description_self, ("description", OpaqueString) => "system_change_description") + command!(system_description_self, ("description", OpaqueStringRemainder) => "system_change_description") .help("Changes your system's description"), ] .into_iter(); @@ -128,7 +128,7 @@ pub fn edit() -> impl Iterator { command!(system_tag_self, CLEAR => "system_clear_tag") .flag(YES) .help("Clears your system's tag"), - command!(system_tag_self, ("tag", OpaqueString) => "system_change_tag") + command!(system_tag_self, ("tag", OpaqueStringRemainder) => "system_change_tag") .help("Changes your system's tag"), ] .into_iter(); @@ -146,7 +146,7 @@ pub fn edit() -> impl Iterator { command!(system_server_tag_self, CLEAR => "system_clear_server_tag") .flag(YES) .help("Clears your system's server tag"), - command!(system_server_tag_self, ("tag", OpaqueString) => "system_change_server_tag") + command!(system_server_tag_self, ("tag", OpaqueStringRemainder) => "system_change_server_tag") .help("Changes your system's server tag"), ] .into_iter(); @@ -163,7 +163,7 @@ pub fn edit() -> impl Iterator { command!(system_pronouns_self, CLEAR => "system_clear_pronouns") .flag(YES) .help("Clears your system's pronouns"), - command!(system_pronouns_self, ("pronouns", OpaqueString) => "system_change_pronouns") + command!(system_pronouns_self, ("pronouns", OpaqueStringRemainder) => "system_change_pronouns") .help("Changes your system's pronouns"), ] .into_iter(); @@ -267,21 +267,21 @@ pub fn edit() -> impl Iterator { .into_iter(); let system_list = ("members", ["list", "l", "find", "f"]); - let system_search = tokens!( + let search = tokens!( ("search", ["query", "find"]), ("query", OpaqueStringRemainder), ); let add_list_flags = |cmd: Command| cmd.flags(get_list_flags()); let system_list_cmd = [ command!(system_target, system_list => "system_members_list"), - command!(system_target, system_search => "system_members_search"), + command!(system_target, search => "system_members_search"), ] .into_iter() .map(add_list_flags); let system_list_self_cmd = [ command!(system_list => "system_members_list_self"), command!(system, system_list => "system_members_list_self"), - command!(system, system_search => "system_members_search_self"), + command!(system, search => "system_members_search_self"), ] .into_iter() .map(add_list_flags); @@ -290,7 +290,7 @@ pub fn edit() -> impl Iterator { let system_groups_cmd = [ command!(system_groups => "system_list_groups"), command!(system_groups, ("list", ["ls"]) => "system_list_groups"), - command!(system_groups, ("search", ["find", "query"]), ("query", OpaqueStringRemainder) => "system_search_groups"), + command!(system_groups, search => "system_search_groups"), ] .into_iter() .map(add_list_flags); diff --git a/crates/command_parser/src/parameter.rs b/crates/command_parser/src/parameter.rs index eaab7c8c..a4f11656 100644 --- a/crates/command_parser/src/parameter.rs +++ b/crates/command_parser/src/parameter.rs @@ -578,6 +578,6 @@ impl ParseMessageLink for regex::Captures<'_> { fn parse_id(&self, name: &str) -> Result { self.name(name) .and_then(|m| m.as_str().parse::().ok()) - .ok_or_else(|| SmolStr::new(format!("invalid {} in message link", name))) + .ok_or_else(|| SmolStr::new(format!("invalid {} ID in message link", name))) } } From 376f688ff4abbb36ca108ef795dbdb519a7d130c Mon Sep 17 00:00:00 2001 From: dusk Date: Wed, 15 Oct 2025 20:18:53 +0000 Subject: [PATCH 131/179] improve error messages for the enum parameters --- crates/command_parser/src/lib.rs | 1 + crates/command_parser/src/parameter.rs | 228 ++++++++++++++----------- 2 files changed, 125 insertions(+), 104 deletions(-) diff --git a/crates/command_parser/src/lib.rs b/crates/command_parser/src/lib.rs index 52807e7f..360a170b 100644 --- a/crates/command_parser/src/lib.rs +++ b/crates/command_parser/src/lib.rs @@ -1,5 +1,6 @@ #![feature(anonymous_lifetime_in_impl_trait)] #![feature(round_char_boundary)] +#![feature(iter_intersperse)] pub mod command; pub mod flag; diff --git a/crates/command_parser/src/parameter.rs b/crates/command_parser/src/parameter.rs index a4f11656..5645ba7f 100644 --- a/crates/command_parser/src/parameter.rs +++ b/crates/command_parser/src/parameter.rs @@ -8,6 +8,55 @@ use smol_str::{SmolStr, format_smolstr}; use crate::token::{Token, TokenMatchResult}; +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum ParameterKind { + OpaqueString, + OpaqueInt, + OpaqueStringRemainder, + MemberRef, + MemberRefs, + GroupRef, + GroupRefs, + SystemRef, + UserRef, + MessageRef, + ChannelRef, + GuildRef, + MemberPrivacyTarget, + GroupPrivacyTarget, + SystemPrivacyTarget, + PrivacyLevel, + Toggle, + Avatar, + ProxySwitchAction, +} + +impl ParameterKind { + pub(crate) fn default_name(&self) -> &str { + match self { + ParameterKind::OpaqueString => "string", + ParameterKind::OpaqueInt => "number", + ParameterKind::OpaqueStringRemainder => "string", + ParameterKind::MemberRef => "target", + ParameterKind::MemberRefs => "targets", + ParameterKind::GroupRef => "target", + ParameterKind::GroupRefs => "targets", + ParameterKind::SystemRef => "target", + ParameterKind::UserRef => "target", + ParameterKind::MessageRef => "target", + ParameterKind::ChannelRef => "target", + ParameterKind::GuildRef => "target", + ParameterKind::MemberPrivacyTarget => "member_privacy_target", + ParameterKind::GroupPrivacyTarget => "group_privacy_target", + ParameterKind::SystemPrivacyTarget => "system_privacy_target", + ParameterKind::PrivacyLevel => "privacy_level", + ParameterKind::Toggle => "toggle", + ParameterKind::Avatar => "avatar", + ParameterKind::ProxySwitchAction => "proxy_switch_action", + } + } +} + #[derive(Debug, Clone)] pub enum ParameterValue { OpaqueString(String), @@ -284,65 +333,44 @@ impl> From> for Parameter { } } -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum ParameterKind { - OpaqueString, - OpaqueInt, - OpaqueStringRemainder, - MemberRef, - MemberRefs, - GroupRef, - GroupRefs, - SystemRef, - UserRef, - MessageRef, - ChannelRef, - GuildRef, - MemberPrivacyTarget, - GroupPrivacyTarget, - SystemPrivacyTarget, - PrivacyLevel, - Toggle, - Avatar, - ProxySwitchAction, -} - -impl ParameterKind { - pub(crate) fn default_name(&self) -> &str { - match self { - ParameterKind::OpaqueString => "string", - ParameterKind::OpaqueInt => "number", - ParameterKind::OpaqueStringRemainder => "string", - ParameterKind::MemberRef => "target", - ParameterKind::MemberRefs => "targets", - ParameterKind::GroupRef => "target", - ParameterKind::GroupRefs => "targets", - ParameterKind::SystemRef => "target", - ParameterKind::UserRef => "target", - ParameterKind::MessageRef => "target", - ParameterKind::ChannelRef => "target", - ParameterKind::GuildRef => "target", - ParameterKind::MemberPrivacyTarget => "member_privacy_target", - ParameterKind::GroupPrivacyTarget => "group_privacy_target", - ParameterKind::SystemPrivacyTarget => "system_privacy_target", - ParameterKind::PrivacyLevel => "privacy_level", - ParameterKind::Toggle => "toggle", - ParameterKind::Avatar => "avatar", - ParameterKind::ProxySwitchAction => "proxy_switch_action", +macro_rules! impl_enum { + ($name:ident ($pretty_name:expr): $($variant:ident),*) => { + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] + pub enum $name { + $($variant),* } - } + + impl $name { + pub const PRETTY_NAME: &'static str = $pretty_name; + + pub fn variants() -> impl Iterator { + [$(Self::$variant),*].into_iter() + } + + pub fn variants_str() -> impl Iterator { + [$(Self::$variant.as_ref()),*].into_iter() + } + + pub fn get_error() -> SmolStr { + let pretty_name = Self::PRETTY_NAME; + let vars = Self::variants_str().intersperse("/").collect::(); + format_smolstr!("invalid {pretty_name}, must be one of {vars}") + } + } + }; } -pub enum MemberPrivacyTargetKind { - Visibility, - Name, - Description, - Banner, - Avatar, - Birthday, - Pronouns, - Proxy, - Metadata, +impl_enum! { + MemberPrivacyTargetKind("member privacy target"): + Visibility, + Name, + Description, + Banner, + Avatar, + Birthday, + Pronouns, + Proxy, + Metadata } impl AsRef for MemberPrivacyTargetKind { @@ -362,7 +390,6 @@ impl AsRef for MemberPrivacyTargetKind { } impl FromStr for MemberPrivacyTargetKind { - // todo: figure out how to represent these errors best type Err = SmolStr; fn from_str(s: &str) -> Result { @@ -377,19 +404,20 @@ impl FromStr for MemberPrivacyTargetKind { "pronouns" => Ok(Self::Pronouns), "proxy" => Ok(Self::Proxy), "metadata" => Ok(Self::Metadata), - _ => Err("invalid member privacy target".into()), + _ => Err(Self::get_error()), } } } -pub enum GroupPrivacyTargetKind { - Name, - Icon, - Description, - Banner, - List, - Metadata, - Visibility, +impl_enum! { + GroupPrivacyTargetKind("group privacy target"): + Name, + Icon, + Description, + Banner, + List, + Metadata, + Visibility } impl AsRef for GroupPrivacyTargetKind { @@ -419,21 +447,22 @@ impl FromStr for GroupPrivacyTargetKind { "list" => Ok(Self::List), "metadata" => Ok(Self::Metadata), "visibility" => Ok(Self::Visibility), - _ => Err("invalid group privacy target".into()), + _ => Err(Self::get_error()), } } } -pub enum SystemPrivacyTargetKind { - Name, - Avatar, - Description, - Banner, - Pronouns, - MemberList, - GroupList, - Front, - FrontHistory, +impl_enum! { + SystemPrivacyTargetKind("system privacy target"): + Name, + Avatar, + Description, + Banner, + Pronouns, + MemberList, + GroupList, + Front, + FrontHistory } impl AsRef for SystemPrivacyTargetKind { @@ -466,15 +495,12 @@ impl FromStr for SystemPrivacyTargetKind { "groups" | "gs" => Ok(Self::GroupList), "front" | "fronter" | "fronters" => Ok(Self::Front), "fronthistory" | "fh" | "switches" => Ok(Self::FrontHistory), - _ => Err("invalid system privacy target".into()), + _ => Err(Self::get_error()), } } } -pub enum PrivacyLevelKind { - Public, - Private, -} +impl_enum!(PrivacyLevelKind("privacy level"): Public, Private); impl AsRef for PrivacyLevelKind { fn as_ref(&self) -> &str { @@ -492,15 +518,20 @@ impl FromStr for PrivacyLevelKind { match s { "public" => Ok(PrivacyLevelKind::Public), "private" => Ok(PrivacyLevelKind::Private), - _ => Err("invalid privacy level".into()), + _ => Err(Self::get_error()), } } } -#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq)] -pub enum Toggle { - On, - Off, +impl_enum!(Toggle("toggle"): On, Off); + +impl AsRef for Toggle { + fn as_ref(&self) -> &str { + match self { + Self::On => "on", + Self::Off => "off", + } + } } impl FromStr for Toggle { @@ -513,10 +544,9 @@ impl FromStr for Toggle { Some(TokenMatchResult::MatchedValue) ) }; - [Self::On, Self::Off] - .into_iter() + Self::variants() .find(matches_self) - .ok_or_else(|| SmolStr::new("invalid toggle, must be on/off")) + .ok_or_else(Self::get_error) } } @@ -538,12 +568,7 @@ impl Into for Toggle { } } -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum ProxySwitchAction { - New, - Add, - Off, -} +impl_enum!(ProxySwitchAction("proxy switch action"): New, Add, Off); impl AsRef for ProxySwitchAction { fn as_ref(&self) -> &str { @@ -559,14 +584,9 @@ impl FromStr for ProxySwitchAction { type Err = SmolStr; fn from_str(s: &str) -> Result { - [ - ProxySwitchAction::New, - ProxySwitchAction::Add, - ProxySwitchAction::Off, - ] - .into_iter() - .find(|action| action.as_ref() == s) - .ok_or_else(|| SmolStr::new("invalid proxy switch action, must be new/add/off")) + Self::variants() + .find(|action| action.as_ref() == s) + .ok_or_else(Self::get_error) } } From f29e48ea744ada2e48af8f2ea84e9ee1644e64fa Mon Sep 17 00:00:00 2001 From: dusk Date: Wed, 15 Oct 2025 20:39:50 +0000 Subject: [PATCH 132/179] fix quotes value being cut off --- crates/command_parser/src/string.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/command_parser/src/string.rs b/crates/command_parser/src/string.rs index 73239699..cc2dee63 100644 --- a/crates/command_parser/src/string.rs +++ b/crates/command_parser/src/string.rs @@ -85,7 +85,7 @@ pub(super) fn next_param<'a>(input: &'a str, current_pos: usize) -> Option Date: Thu, 6 Nov 2025 18:34:17 +0000 Subject: [PATCH 133/179] fix: 'pk;s f' should query fronter, not list members --- PluralKit.Bot/CommandMeta/CommandTree.cs | 3 ++ PluralKit.Bot/CommandSystem/Parameters.cs | 36 +++++++++++------------ crates/command_definitions/src/system.rs | 30 +++++++++++-------- 3 files changed, 39 insertions(+), 30 deletions(-) diff --git a/PluralKit.Bot/CommandMeta/CommandTree.cs b/PluralKit.Bot/CommandMeta/CommandTree.cs index d7c32fb4..f5aad9c1 100644 --- a/PluralKit.Bot/CommandMeta/CommandTree.cs +++ b/PluralKit.Bot/CommandMeta/CommandTree.cs @@ -211,6 +211,9 @@ public partial class CommandTree Commands.SystemFronter(var param, var flags) => ctx.Execute(SystemFronter, m => m.Fronter(ctx, param.target)), Commands.SystemFronterHistory(var param, var flags) => ctx.Execute(SystemFrontHistory, m => m.FrontHistory(ctx, param.target, flags.clear)), Commands.SystemFronterPercent(var param, var flags) => ctx.Execute(SystemFrontPercent, m => m.FrontPercent(ctx, param.target, flags.duration, flags.fronters_only, flags.flat)), + Commands.SystemFronterSelf(_, var flags) => ctx.Execute(SystemFronter, m => m.Fronter(ctx, ctx.System)), + Commands.SystemFronterHistorySelf(_, var flags) => ctx.Execute(SystemFrontHistory, m => m.FrontHistory(ctx, ctx.System, flags.clear)), + Commands.SystemFronterPercentSelf(_, var flags) => ctx.Execute(SystemFrontPercent, m => m.FrontPercent(ctx, ctx.System, flags.duration, flags.fronters_only, flags.flat)), Commands.SystemDisplayId(var param, _) => ctx.Execute(SystemId, m => m.DisplayId(ctx, param.target)), Commands.SystemDisplayIdSelf => ctx.Execute(SystemId, m => m.DisplayId(ctx, ctx.System)), Commands.SystemWebhookShow => ctx.Execute(null, m => m.GetSystemWebhook(ctx)), diff --git a/PluralKit.Bot/CommandSystem/Parameters.cs b/PluralKit.Bot/CommandSystem/Parameters.cs index cbde613d..8de534fd 100644 --- a/PluralKit.Bot/CommandSystem/Parameters.cs +++ b/PluralKit.Bot/CommandSystem/Parameters.cs @@ -9,24 +9,24 @@ namespace PluralKit.Bot; // corresponds to the ffi Paramater type, but with stricter types (also avoiding exposing ffi types!) public abstract record Parameter() { - public record MemberRef(PKMember member) : Parameter; - public record MemberRefs(List members) : Parameter; - public record GroupRef(PKGroup group) : Parameter; - public record GroupRefs(List groups) : Parameter; - public record SystemRef(PKSystem system) : Parameter; - public record UserRef(User user) : Parameter; - public record MessageRef(Message.Reference message) : Parameter; - public record ChannelRef(Channel channel) : Parameter; - public record GuildRef(Guild guild) : Parameter; - public record MemberPrivacyTarget(MemberPrivacySubject target) : Parameter; - public record GroupPrivacyTarget(GroupPrivacySubject target) : Parameter; - public record SystemPrivacyTarget(SystemPrivacySubject target) : Parameter; - public record PrivacyLevel(Core.PrivacyLevel level) : Parameter; - public record Toggle(bool value) : Parameter; - public record Opaque(string value) : Parameter; - public record Number(int value) : Parameter; - public record Avatar(ParsedImage avatar) : Parameter; - public record ProxySwitchAction(SystemConfig.ProxySwitchAction action) : Parameter; + public record MemberRef(PKMember member): Parameter; + public record MemberRefs(List members): Parameter; + public record GroupRef(PKGroup group): Parameter; + public record GroupRefs(List groups): Parameter; + public record SystemRef(PKSystem system): Parameter; + public record UserRef(User user): Parameter; + public record MessageRef(Message.Reference message): Parameter; + public record ChannelRef(Channel channel): Parameter; + public record GuildRef(Guild guild): Parameter; + public record MemberPrivacyTarget(MemberPrivacySubject target): Parameter; + public record GroupPrivacyTarget(GroupPrivacySubject target): Parameter; + public record SystemPrivacyTarget(SystemPrivacySubject target): Parameter; + public record PrivacyLevel(Core.PrivacyLevel level): Parameter; + public record Toggle(bool value): Parameter; + public record Opaque(string value): Parameter; + public record Number(int value): Parameter; + public record Avatar(ParsedImage avatar): Parameter; + public record ProxySwitchAction(SystemConfig.ProxySwitchAction action): Parameter; } public class Parameters diff --git a/crates/command_definitions/src/system.rs b/crates/command_definitions/src/system.rs index 4b4847cf..2bacba40 100644 --- a/crates/command_definitions/src/system.rs +++ b/crates/command_definitions/src/system.rs @@ -249,16 +249,20 @@ pub fn edit() -> impl Iterator { .help("Changes a specific privacy setting for your system"), ].into_iter(); - let system_front = tokens!(system_target, ("front", ["fronter", "fronters", "f"])); - let system_front_cmd = [ - command!(system_front => "system_fronter"), - command!(system_front, ("history", ["h"]) => "system_fronter_history").flag(CLEAR), - command!(system_front, ("percent", ["p", "%"]) => "system_fronter_percent") - .flag(("duration", OpaqueString)) - .flag(("fronters-only", ["fo"])) - .flag("flat"), - ] - .into_iter(); + let front = ("front", ["fronter", "fronters", "f"]); + let make_system_front_cmd = |prefix: TokensIterator, suffix: &str| { + [ + command!(prefix => format!("system_fronter{}", suffix)), + command!(prefix, ("history", ["h"]) => format!("system_fronter_history{}", suffix)).flag(CLEAR), + command!(prefix, ("percent", ["p", "%"]) => format!("system_fronter_percent{}", suffix)) + .flag(("duration", OpaqueString)) + .flag(("fronters-only", ["fo"])) + .flag("flat"), + ] + .into_iter() + }; + let system_front_cmd = make_system_front_cmd(tokens!(system_target, front), ""); + let system_front_self_cmd = make_system_front_cmd(tokens!(system, front), "_self"); let system_link = [ command!("link", ("account", UserRef) => "system_link"), @@ -266,7 +270,8 @@ pub fn edit() -> impl Iterator { ] .into_iter(); - let system_list = ("members", ["list", "l", "find", "f"]); + let list = ("list", ["ls", "l"]); + let system_list = tokens!("members", list); let search = tokens!( ("search", ["query", "find"]), ("query", OpaqueStringRemainder), @@ -289,7 +294,7 @@ pub fn edit() -> impl Iterator { let system_groups = tokens!(system_target, ("groups", ["gs"])); let system_groups_cmd = [ command!(system_groups => "system_list_groups"), - command!(system_groups, ("list", ["ls"]) => "system_list_groups"), + command!(system_groups, list => "system_list_groups"), command!(system_groups, search => "system_search_groups"), ] .into_iter() @@ -315,6 +320,7 @@ pub fn edit() -> impl Iterator { .chain(system_banner_self_cmd) .chain(system_list_self_cmd) .chain(system_display_id_self_cmd) + .chain(system_front_self_cmd) .chain(system_delete) .chain(system_privacy_cmd) .chain(system_proxy_cmd) From d8748b1efcda51c2dba768b45560ba421ab4cded Mon Sep 17 00:00:00 2001 From: dusk Date: Sun, 23 Nov 2025 10:28:46 +0000 Subject: [PATCH 134/179] remove OpaqueStringRemainder and use Remainder like intended --- crates/command_definitions/src/admin.rs | 6 ++-- crates/command_definitions/src/group.rs | 10 +++--- .../command_definitions/src/import_export.rs | 2 +- crates/command_definitions/src/member.rs | 14 ++++---- crates/command_definitions/src/message.rs | 7 ++-- crates/command_definitions/src/switch.rs | 2 +- crates/command_definitions/src/system.rs | 16 ++++----- crates/command_parser/src/parameter.rs | 33 ++++++++----------- crates/commands/src/lib.rs | 2 +- crates/commands/src/write_cs_glue.rs | 4 +-- 10 files changed, 46 insertions(+), 50 deletions(-) diff --git a/crates/command_definitions/src/admin.rs b/crates/command_definitions/src/admin.rs index 73a8e892..24608f58 100644 --- a/crates/command_definitions/src/admin.rs +++ b/crates/command_definitions/src/admin.rs @@ -14,7 +14,7 @@ pub fn cmds() -> impl Iterator { .help("Shows an abuse log entry"), command!(abuselog, ("flagdeny", ["fd"]), log_param, Optional(("value", Toggle)) => format!("admin_abuselog_flag_deny_{}", log_param.name())) .help("Sets the deny flag on an abuse log entry"), - command!(abuselog, ("description", ["desc"]), log_param, Optional(("desc", OpaqueStringRemainder)) => format!("admin_abuselog_description_{}", log_param.name())) + command!(abuselog, ("description", ["desc"]), log_param, Optional(Remainder(("desc", OpaqueString))) => format!("admin_abuselog_description_{}", log_param.name())) .flag(CLEAR) .flag(YES) .help("Sets the description of an abuse log entry"), @@ -27,7 +27,7 @@ pub fn cmds() -> impl Iterator { ].into_iter() }; let abuselog_cmds = [ - command!(abuselog, ("create", ["c", "new"]), ("account", UserRef), Optional(("description", OpaqueStringRemainder)) => "admin_abuselog_create") + command!(abuselog, ("create", ["c", "new"]), ("account", UserRef), Optional(Remainder(("description", OpaqueString))) => "admin_abuselog_create") .flag(("deny-boy-usage", ["deny"])) .help("Creates an abuse log entry") ] @@ -66,7 +66,7 @@ pub fn cmds() -> impl Iterator { .help("Recovers a system"), command!(admin, ("systemdelete", ["sd"]), SystemRef => "admin_system_delete") .help("Deletes a system"), - command!(admin, ("sendmessage", ["sendmsg"]), ("account", UserRef), ("content", OpaqueStringRemainder) => "admin_send_message") + command!(admin, ("sendmessage", ["sendmsg"]), ("account", UserRef), Remainder(("content", OpaqueString)) => "admin_send_message") .help("Sends a message to a user"), ] .into_iter() diff --git a/crates/command_definitions/src/group.rs b/crates/command_definitions/src/group.rs index 63d3f7a5..fbf611be 100644 --- a/crates/command_definitions/src/group.rs +++ b/crates/command_definitions/src/group.rs @@ -18,7 +18,7 @@ pub fn cmds() -> impl Iterator { let group_new = tokens!(group, ("new", ["n"])); let group_new_cmd = [ - command!(group_new, ("name", OpaqueStringRemainder) => "group_new") + command!(group_new, Remainder(("name", OpaqueString)) => "group_new") .help("Creates a new group"), ] .into_iter(); @@ -37,7 +37,7 @@ pub fn cmds() -> impl Iterator { command!(group_name, CLEAR => "group_clear_name") .flag(YES) .help("Clears the group's name"), - command!(group_name, ("name", OpaqueStringRemainder) => "group_rename") + command!(group_name, Remainder(("name", OpaqueString)) => "group_rename") .help("Renames the group"), ] .into_iter(); @@ -49,7 +49,7 @@ pub fn cmds() -> impl Iterator { command!(group_display_name, CLEAR => "group_clear_display_name") .flag(YES) .help("Clears the group's display name"), - command!(group_display_name, ("name", OpaqueStringRemainder) => "group_change_display_name") + command!(group_display_name, Remainder(("name", OpaqueString)) => "group_change_display_name") .help("Changes the group's display name"), ] .into_iter(); @@ -67,7 +67,7 @@ pub fn cmds() -> impl Iterator { command!(group_description, CLEAR => "group_clear_description") .flag(YES) .help("Clears the group's description"), - command!(group_description, ("description", OpaqueStringRemainder) => "group_change_description") + command!(group_description, Remainder(("description", OpaqueString)) => "group_change_description") .help("Changes the group's description"), ] .into_iter(); @@ -154,7 +154,7 @@ pub fn cmds() -> impl Iterator { let search = tokens!( ("search", ["find", "query"]), - ("query", OpaqueStringRemainder) + Remainder(("query", OpaqueString)) ); let group_list_members = tokens!(group_target, ("members", ["list", "ls"])); let group_list_members_cmd = [ diff --git a/crates/command_definitions/src/import_export.rs b/crates/command_definitions/src/import_export.rs index beed5e72..7e2e9404 100644 --- a/crates/command_definitions/src/import_export.rs +++ b/crates/command_definitions/src/import_export.rs @@ -2,7 +2,7 @@ use super::*; pub fn cmds() -> impl Iterator { [ - command!("import", Optional(("url", OpaqueStringRemainder)) => "import").flag(YES), + command!("import", Optional(Remainder(("url", OpaqueString))) => "import").flag(YES), command!("export" => "export"), ] .into_iter() diff --git a/crates/command_definitions/src/member.rs b/crates/command_definitions/src/member.rs index f35bb3d8..655384ff 100644 --- a/crates/command_definitions/src/member.rs +++ b/crates/command_definitions/src/member.rs @@ -48,7 +48,7 @@ pub fn cmds() -> impl Iterator { let member_name = tokens!(member_target, name); [ command!(member_name => "member_name_show").help("Shows a member's name"), - command!(member_name, ("name", OpaqueStringRemainder) => "member_name_update") + command!(member_name, Remainder(("name", OpaqueString)) => "member_name_update") .flag(YES) .help("Changes a member's name"), ] @@ -62,7 +62,7 @@ pub fn cmds() -> impl Iterator { command!(member_desc, CLEAR => "member_desc_clear") .flag(YES) .help("Clears a member's description"), - command!(member_desc, ("description", OpaqueStringRemainder) => "member_desc_update") + command!(member_desc, Remainder(("description", OpaqueString)) => "member_desc_update") .help("Changes a member's description"), ] .into_iter() @@ -87,7 +87,7 @@ pub fn cmds() -> impl Iterator { [ command!(member_pronouns => "member_pronouns_show") .help("Shows a member's pronouns"), - command!(member_pronouns, ("pronouns", OpaqueStringRemainder) => "member_pronouns_update") + command!(member_pronouns, Remainder(("pronouns", OpaqueString)) => "member_pronouns_update") .help("Changes a member's pronouns"), command!(member_pronouns, CLEAR => "member_pronouns_clear") .flag(YES) @@ -139,7 +139,7 @@ pub fn cmds() -> impl Iterator { [ command!(member_display_name => "member_displayname_show") .help("Shows a member's display name"), - command!(member_display_name, ("name", OpaqueStringRemainder) => "member_displayname_update") + command!(member_display_name, Remainder(("name", OpaqueString)) => "member_displayname_update") .help("Changes a member's display name"), command!(member_display_name, CLEAR => "member_displayname_clear") .flag(YES) @@ -152,7 +152,7 @@ pub fn cmds() -> impl Iterator { [ command!(member_server_name => "member_servername_show") .help("Shows a member's server name"), - command!(member_server_name, ("name", OpaqueStringRemainder) => "member_servername_update") + command!(member_server_name, Remainder(("name", OpaqueString)) => "member_servername_update") .help("Changes a member's server name"), command!(member_server_name, CLEAR => "member_servername_clear") .flag(YES) @@ -165,7 +165,7 @@ pub fn cmds() -> impl Iterator { [ command!(member_proxy => "member_proxy_show") .help("Shows a member's proxy tags"), - command!(member_proxy, ("tags", OpaqueString) => "member_proxy_set") + command!(member_proxy, Remainder(("tags", OpaqueString)) => "member_proxy_set") .help("Sets a member's proxy tags"), command!(member_proxy, ("add", ["a"]), ("tag", OpaqueString) => "member_proxy_add") .help("Adds proxy tag to a member"), @@ -298,7 +298,7 @@ pub fn cmds() -> impl Iterator { let member_list_group_cmds = [ command!(member_group => "member_list_groups"), command!(member_group, "list" => "member_list_groups"), - command!(member_group, ("search", ["find", "query"]), ("query", OpaqueStringRemainder) => "member_search_groups"), + command!(member_group, ("search", ["find", "query"]), Remainder(("query", OpaqueString)) => "member_search_groups"), ] .into_iter() .map(|cmd| cmd.flags(get_list_flags())); diff --git a/crates/command_definitions/src/message.rs b/crates/command_definitions/src/message.rs index f9bfec49..f341fa78 100644 --- a/crates/command_definitions/src/message.rs +++ b/crates/command_definitions/src/message.rs @@ -8,6 +8,7 @@ pub fn cmds() -> impl Iterator { let reproxy = ("reproxy", ["rp", "crimes", "crime"]); let edit = ("edit", ["e"]); + let new_content_param = Remainder(("new_content", OpaqueString)); let apply_edit = |cmd: Command| { cmd.flag(("append", ["a"])) .flag(("prepend", ["p"])) @@ -25,9 +26,9 @@ pub fn cmds() -> impl Iterator { .help("Shows information about a proxied message"), command!(message, author => "message_author").help("Shows the author of a proxied message"), command!(message, delete => "message_delete").help("Deletes a proxied message"), - apply_edit(command!(message, edit, ("new_content", OpaqueStringRemainder) => "message_edit_specified")), - apply_edit(command!(edit, Skip(MessageRef), ("new_content", OpaqueStringRemainder) => "message_edit_specified")), - apply_edit(command!(edit, ("new_content", OpaqueStringRemainder) => "message_edit")), + apply_edit(command!(message, edit, new_content_param => "message_edit_specified")), + apply_edit(command!(edit, Skip(MessageRef), new_content_param => "message_edit_specified")), + apply_edit(command!(edit, new_content_param => "message_edit")), command!(reproxy, ("member", MemberRef) => "message_reproxy") .help("Reproxies a message with a different member"), command!(reproxy, ("msg", MessageRef), ("member", MemberRef) => "message_reproxy_specified") diff --git a/crates/command_definitions/src/switch.rs b/crates/command_definitions/src/switch.rs index ab5167c3..ef9e15de 100644 --- a/crates/command_definitions/src/switch.rs +++ b/crates/command_definitions/src/switch.rs @@ -20,7 +20,7 @@ pub fn cmds() -> impl Iterator { command!(switch, ("commands", ["help"]) => "switch_commands"), command!(switch, out => "switch_out"), command!(switch, delete => "switch_delete").flag(("all", ["clear", "c"])), - command!(switch, r#move, OpaqueStringRemainder => "switch_move"), // TODO: datetime parsing + command!(switch, r#move, Remainder(OpaqueString) => "switch_move"), // TODO: datetime parsing command!(switch, edit, out => "switch_edit_out").flag(YES), command!(switch, edit, Optional(MemberRefs) => "switch_edit").flags(edit_flags), command!(switch, copy, Optional(MemberRefs) => "switch_copy").flags(edit_flags), diff --git a/crates/command_definitions/src/system.rs b/crates/command_definitions/src/system.rs index 2bacba40..6af66566 100644 --- a/crates/command_definitions/src/system.rs +++ b/crates/command_definitions/src/system.rs @@ -23,7 +23,7 @@ pub fn edit() -> impl Iterator { let system_new = tokens!(system, ("new", ["n"])); let system_new_cmd = [ command!(system_new => "system_new").help("Creates a new system"), - command!(system_new, ("name", OpaqueStringRemainder) => "system_new_name") + command!(system_new, Remainder(("name", OpaqueString)) => "system_new_name") .help("Creates a new system (using the provided name)"), ] .into_iter(); @@ -62,7 +62,7 @@ pub fn edit() -> impl Iterator { command!(system_name_self, CLEAR => "system_clear_name") .flag(YES) .help("Clears your system's name"), - command!(system_name_self, ("name", OpaqueStringRemainder) => "system_rename") + command!(system_name_self, Remainder(("name", OpaqueString)) => "system_rename") .help("Renames your system"), ] .into_iter(); @@ -80,7 +80,7 @@ pub fn edit() -> impl Iterator { command!(system_server_name_self, CLEAR => "system_clear_server_name") .flag(YES) .help("Clears your system's server name"), - command!(system_server_name_self, ("name", OpaqueStringRemainder) => "system_rename_server_name") + command!(system_server_name_self, Remainder(("name", OpaqueString)) => "system_rename_server_name") .help("Renames your system's server name"), ] .into_iter(); @@ -97,7 +97,7 @@ pub fn edit() -> impl Iterator { command!(system_description_self, CLEAR => "system_clear_description") .flag(YES) .help("Clears your system's description"), - command!(system_description_self, ("description", OpaqueStringRemainder) => "system_change_description") + command!(system_description_self, Remainder(("description", OpaqueString)) => "system_change_description") .help("Changes your system's description"), ] .into_iter(); @@ -128,7 +128,7 @@ pub fn edit() -> impl Iterator { command!(system_tag_self, CLEAR => "system_clear_tag") .flag(YES) .help("Clears your system's tag"), - command!(system_tag_self, ("tag", OpaqueStringRemainder) => "system_change_tag") + command!(system_tag_self, Remainder(("tag", OpaqueString)) => "system_change_tag") .help("Changes your system's tag"), ] .into_iter(); @@ -146,7 +146,7 @@ pub fn edit() -> impl Iterator { command!(system_server_tag_self, CLEAR => "system_clear_server_tag") .flag(YES) .help("Clears your system's server tag"), - command!(system_server_tag_self, ("tag", OpaqueStringRemainder) => "system_change_server_tag") + command!(system_server_tag_self, Remainder(("tag", OpaqueString)) => "system_change_server_tag") .help("Changes your system's server tag"), ] .into_iter(); @@ -163,7 +163,7 @@ pub fn edit() -> impl Iterator { command!(system_pronouns_self, CLEAR => "system_clear_pronouns") .flag(YES) .help("Clears your system's pronouns"), - command!(system_pronouns_self, ("pronouns", OpaqueStringRemainder) => "system_change_pronouns") + command!(system_pronouns_self, Remainder(("pronouns", OpaqueString)) => "system_change_pronouns") .help("Changes your system's pronouns"), ] .into_iter(); @@ -274,7 +274,7 @@ pub fn edit() -> impl Iterator { let system_list = tokens!("members", list); let search = tokens!( ("search", ["query", "find"]), - ("query", OpaqueStringRemainder), + Remainder(("query", OpaqueString)) ); let add_list_flags = |cmd: Command| cmd.flags(get_list_flags()); let system_list_cmd = [ diff --git a/crates/command_parser/src/parameter.rs b/crates/command_parser/src/parameter.rs index 5645ba7f..0d13d773 100644 --- a/crates/command_parser/src/parameter.rs +++ b/crates/command_parser/src/parameter.rs @@ -12,7 +12,6 @@ use crate::token::{Token, TokenMatchResult}; pub enum ParameterKind { OpaqueString, OpaqueInt, - OpaqueStringRemainder, MemberRef, MemberRefs, GroupRef, @@ -36,7 +35,6 @@ impl ParameterKind { match self { ParameterKind::OpaqueString => "string", ParameterKind::OpaqueInt => "number", - ParameterKind::OpaqueStringRemainder => "string", ParameterKind::MemberRef => "target", ParameterKind::MemberRefs => "targets", ParameterKind::GroupRef => "target", @@ -80,13 +78,6 @@ pub enum ParameterValue { Null, } -fn is_remainder(kind: ParameterKind) -> bool { - matches!( - kind, - ParameterKind::OpaqueStringRemainder | ParameterKind::MemberRefs | ParameterKind::GroupRefs - ) -} - #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct Parameter { name: SmolStr, @@ -135,9 +126,7 @@ impl Parameter { pub fn match_value(&self, input: &str) -> Result { match self.kind { // TODO: actually parse image url - ParameterKind::OpaqueString | ParameterKind::OpaqueStringRemainder => { - Ok(ParameterValue::OpaqueString(input.into())) - } + ParameterKind::OpaqueString => Ok(ParameterValue::OpaqueString(input.into())), ParameterKind::OpaqueInt => input .parse::() .map(|num| ParameterValue::OpaqueInt(num)) @@ -254,13 +243,10 @@ impl Display for Parameter { ParameterKind::OpaqueInt => { write!(f, "[{}]", self.name) } - ParameterKind::OpaqueStringRemainder => { - write!(f, "[{}]...", self.name) - } ParameterKind::MemberRef => write!(f, ""), - ParameterKind::MemberRefs => write!(f, " ..."), + ParameterKind::MemberRefs => write!(f, " "), ParameterKind::GroupRef => write!(f, ""), - ParameterKind::GroupRefs => write!(f, " ..."), + ParameterKind::GroupRefs => write!(f, " "), ParameterKind::SystemRef => write!(f, ""), ParameterKind::UserRef => write!(f, ""), ParameterKind::MessageRef => write!(f, ""), @@ -273,10 +259,18 @@ impl Display for Parameter { ParameterKind::Toggle => write!(f, ""), ParameterKind::Avatar => write!(f, ""), ParameterKind::ProxySwitchAction => write!(f, ""), + }?; + if self.is_remainder() { + write!(f, "...")?; } + Ok(()) } } +fn is_remainder(kind: ParameterKind) -> bool { + matches!(kind, ParameterKind::MemberRefs | ParameterKind::GroupRefs) +} + impl From for Parameter { fn from(value: ParameterKind) -> Self { Parameter { @@ -301,6 +295,7 @@ impl From<(&str, ParameterKind)> for Parameter { } } +/// if no input is left to parse, this parameter matches to Null #[derive(Clone)] pub struct Optional>(pub P); @@ -311,6 +306,7 @@ impl> From> for Parameter { } } +/// tells the parser to use the remainder of the input as the input to this parameter #[derive(Clone)] pub struct Remainder>(pub P); @@ -321,8 +317,7 @@ impl> From> for Parameter { } } -// todo(dusk): this is kind of annoying to use, should probably introduce -// a way to match multiple parameters in a single parameter +/// skips the branch this parameter is in if it does not match #[derive(Clone)] pub struct Skip>(pub P); diff --git a/crates/commands/src/lib.rs b/crates/commands/src/lib.rs index ed67b282..08a094f2 100644 --- a/crates/commands/src/lib.rs +++ b/crates/commands/src/lib.rs @@ -1,4 +1,4 @@ -use std::{collections::HashMap, fmt::Write, usize}; +use std::{collections::HashMap, fmt::Write}; use command_parser::{parameter::ParameterValue, token::TokenMatchResult, Tree}; diff --git a/crates/commands/src/write_cs_glue.rs b/crates/commands/src/write_cs_glue.rs index de1b5e74..d2269d03 100644 --- a/crates/commands/src/write_cs_glue.rs +++ b/crates/commands/src/write_cs_glue.rs @@ -262,7 +262,7 @@ fn command_callback_to_name(cb: &str) -> String { fn get_param_ty(kind: ParameterKind) -> &'static str { match kind { - ParameterKind::OpaqueString | ParameterKind::OpaqueStringRemainder => "string", + ParameterKind::OpaqueString => "string", ParameterKind::OpaqueInt => "int", ParameterKind::MemberRef => "PKMember", ParameterKind::MemberRefs => "List", @@ -285,7 +285,7 @@ fn get_param_ty(kind: ParameterKind) -> &'static str { fn get_param_param_ty(kind: ParameterKind) -> &'static str { match kind { - ParameterKind::OpaqueString | ParameterKind::OpaqueStringRemainder => "Opaque", + ParameterKind::OpaqueString => "Opaque", ParameterKind::OpaqueInt => "Number", ParameterKind::MemberRef => "Member", ParameterKind::MemberRefs => "Members", From 40febd4288fa8a0bc66c7701d0db37e3b2d91cc3 Mon Sep 17 00:00:00 2001 From: dusk Date: Sun, 23 Nov 2025 10:48:07 +0000 Subject: [PATCH 135/179] fix cfg proxy switch not being parsed correctly --- crates/command_definitions/src/config.rs | 46 +++++++++++------------- crates/command_definitions/src/group.rs | 4 +-- 2 files changed, 23 insertions(+), 27 deletions(-) diff --git a/crates/command_definitions/src/config.rs b/crates/command_definitions/src/config.rs index e8f4d625..af63e633 100644 --- a/crates/command_definitions/src/config.rs +++ b/crates/command_definitions/src/config.rs @@ -10,33 +10,35 @@ pub fn cmds() -> impl Iterator { let ap_timeout = tokens!(ap, ("timeout", ["tm"])); let timezone = tokens!(cfg, ("timezone", ["zone", "tz"])); - let ping = tokens!(cfg, ("ping", ["ping"])); + let ping = tokens!(cfg, "ping"); let priv_ = ("private", ["priv"]); let member_privacy = tokens!(cfg, priv_, ("member", ["mem"])); - let member_privacy_short = tokens!(cfg, ("mp", ["mp"])); + let member_privacy_short = tokens!(cfg, "mp"); let group_privacy = tokens!(cfg, priv_, ("group", ["grp"])); - let group_privacy_short = tokens!(cfg, ("gp", ["gp"])); + let group_privacy_short = tokens!(cfg, "gp"); - let show = ("show", ["show"]); + let show = "show"; let show_private = tokens!(cfg, show, priv_); - let show_private_short = tokens!(cfg, ("sp", ["sp"])); + let show_private_short = tokens!(cfg, "sp"); let proxy = ("proxy", ["px"]); let proxy_case = tokens!(cfg, proxy, ("case", ["caps", "capitalize", "capitalise"])); let proxy_error = tokens!(cfg, proxy, ("error", ["errors"])); - let proxy_error_short = tokens!(cfg, ("pe", ["pe"])); + let proxy_error_short = tokens!(cfg, "pe"); + let proxy_switch = tokens!(cfg, proxy, "switch"); + let proxy_switch_short = tokens!(cfg, ("proxyswitch", ["ps"])); let id = ("id", ["ids"]); - let split_id = tokens!(cfg, ("split", ["split"]), id); - let split_id_short = tokens!(cfg, ("sid", ["sid", "sids"])); + let split_id = tokens!(cfg, "split", id); + let split_id_short = tokens!(cfg, ("sid", ["sids"])); let cap_id = tokens!(cfg, ("cap", ["caps", "capitalize", "capitalise"]), id); - let cap_id_short = tokens!(cfg, ("capid", ["capid", "capids"])); + let cap_id_short = tokens!(cfg, ("capid", ["capids"])); let pad = ("pad", ["padding"]); let pad_id = tokens!(cfg, pad, id); let id_pad = tokens!(cfg, id, pad); - let id_pad_short = tokens!(cfg, ("idpad", ["idpad", "padid", "padids"])); + let id_pad_short = tokens!(cfg, ("idpad", ["padid", "padids"])); let show_color = tokens!(cfg, show, ("color", ["colour", "colors", "colours"])); let show_color_short = tokens!( @@ -53,29 +55,23 @@ pub fn cmds() -> impl Iterator { ) ); - let proxy_switch = tokens!(cfg, ("proxy", ["proxy"]), ("switch", ["switch"])); - let proxy_switch_short = tokens!(cfg, ("proxyswitch", ["proxyswitch", "ps"])); + let format = "format"; + let name_format = tokens!(cfg, "name", format); + let name_format_short = tokens!(cfg, ("nameformat", ["nf"])); - let format = ("format", ["format"]); - let name_format = tokens!(cfg, ("name", ["name"]), format); - let name_format_short = tokens!(cfg, ("nameformat", ["nameformat", "nf"])); - - let server = ("server", ["server"]); - let server_name_format = tokens!(cfg, server, ("name", ["name"]), format); + let server = "server"; + let server_name_format = tokens!(cfg, server, "name", format); let server_format = tokens!( cfg, - ("server", ["server", "servername"]), - ("format", ["format", "nameformat", "nf"]) + ("server", ["servername"]), + ("format", ["nameformat", "nf"]) ); let server_format_short = tokens!( cfg, - ( - "snf", - ["snf", "servernf", "servernameformat", "snameformat"] - ) + ("snf", ["servernf", "servernameformat", "snameformat"]) ); - let limit_ = ("limit", ["limit", "lim"]); + let limit_ = ("limit", ["lim"]); let member_limit = tokens!(cfg, ("member", ["mem"]), limit_); let group_limit = tokens!(cfg, ("group", ["grp"]), limit_); let limit = tokens!(cfg, limit_); diff --git a/crates/command_definitions/src/group.rs b/crates/command_definitions/src/group.rs index fbf611be..6512722d 100644 --- a/crates/command_definitions/src/group.rs +++ b/crates/command_definitions/src/group.rs @@ -180,7 +180,8 @@ pub fn cmds() -> impl Iterator { .into_iter() .map(apply_list_opts); - group_new_cmd + system_groups_cmd + .chain(group_new_cmd) .chain(group_info_cmd) .chain(group_name_cmd) .chain(group_display_name_cmd) @@ -196,5 +197,4 @@ pub fn cmds() -> impl Iterator { .chain(group_delete_cmd) .chain(group_id_cmd) .chain(group_list_members_cmd) - .chain(system_groups_cmd) } From 5c2c2dceffebc73459de7c2283929d3c1008072b Mon Sep 17 00:00:00 2001 From: dusk Date: Sun, 23 Nov 2025 11:07:41 +0000 Subject: [PATCH 136/179] s g now also works --- crates/command_definitions/src/system.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/command_definitions/src/system.rs b/crates/command_definitions/src/system.rs index 6af66566..3d9e3d1e 100644 --- a/crates/command_definitions/src/system.rs +++ b/crates/command_definitions/src/system.rs @@ -291,7 +291,7 @@ pub fn edit() -> impl Iterator { .into_iter() .map(add_list_flags); - let system_groups = tokens!(system_target, ("groups", ["gs"])); + let system_groups = tokens!(system_target, group::group()); let system_groups_cmd = [ command!(system_groups => "system_list_groups"), command!(system_groups, list => "system_list_groups"), From d4c80aed000c15b9739baa318da467ffa97b7d13 Mon Sep 17 00:00:00 2001 From: dusk Date: Sun, 23 Nov 2025 11:56:14 +0000 Subject: [PATCH 137/179] make random commands consistent by adding missing ones --- PluralKit.Bot/CommandMeta/CommandTree.cs | 6 +++- PluralKit.Bot/Commands/Random.cs | 6 ++-- crates/command_definitions/src/random.rs | 3 ++ crates/command_definitions/src/system.rs | 43 +++++++++++------------- 4 files changed, 31 insertions(+), 27 deletions(-) diff --git a/PluralKit.Bot/CommandMeta/CommandTree.cs b/PluralKit.Bot/CommandMeta/CommandTree.cs index f5aad9c1..600ab47b 100644 --- a/PluralKit.Bot/CommandMeta/CommandTree.cs +++ b/PluralKit.Bot/CommandMeta/CommandTree.cs @@ -223,11 +223,15 @@ public partial class CommandTree flags.group ? ctx.Execute(GroupRandom, m => m.Group(ctx, ctx.System, flags.all, flags.show_embed)) : ctx.Execute(MemberRandom, m => m.Member(ctx, ctx.System, flags.all, flags.show_embed)), + Commands.RandomGroupSelf(_, var flags) => ctx.Execute(GroupRandom, m => m.Group(ctx, ctx.System, flags.all, flags.show_embed)), + Commands.RandomGroupMemberSelf(var param, var flags) => ctx.Execute(GroupMemberRandom, m => m.GroupMember(ctx, param.target, flags.all, flags.show_embed, flags)), Commands.SystemRandom(var param, var flags) => flags.group ? ctx.Execute(GroupRandom, m => m.Group(ctx, param.target, flags.all, flags.show_embed)) : ctx.Execute(MemberRandom, m => m.Member(ctx, param.target, flags.all, flags.show_embed)), - Commands.GroupRandomMember(var param, var flags) => ctx.Execute(GroupMemberRandom, m => m.GroupMember(ctx, param.target, flags)), + Commands.SystemRandomGroup(var param, var flags) => + ctx.Execute(GroupRandom, m => m.Group(ctx, param.target, flags.all, flags.show_embed)), + Commands.GroupRandomMember(var param, var flags) => ctx.Execute(GroupMemberRandom, m => m.GroupMember(ctx, param.target, flags.all, flags.show_embed, flags)), Commands.SystemLink(var param, _) => ctx.Execute(Link, m => m.LinkSystem(ctx, param.account)), Commands.SystemUnlink(var param, var flags) => ctx.Execute(Unlink, m => m.UnlinkAccount(ctx, param.account, flags.yes)), Commands.SystemMembersListSelf(var param, var flags) => ctx.Execute(SystemList, m => m.MemberList(ctx, ctx.System, null, flags)), diff --git a/PluralKit.Bot/Commands/Random.cs b/PluralKit.Bot/Commands/Random.cs index d34c3e73..180d9b0c 100644 --- a/PluralKit.Bot/Commands/Random.cs +++ b/PluralKit.Bot/Commands/Random.cs @@ -82,7 +82,7 @@ public class Random components: await _embeds.CreateGroupMessageComponents(ctx, target, groups.ToArray()[randInt], all)); } - public async Task GroupMember(Context ctx, PKGroup group, GroupRandomMemberFlags flags) + public async Task GroupMember(Context ctx, PKGroup group, bool all, bool show_embed, IHasListOptions flags) { ctx.CheckSystemPrivacy(group.System, group.ListPrivacy); @@ -96,7 +96,7 @@ public class Random "This group has no members!" + (ctx.System?.Id == group.System ? " Please add at least one member to this group before using this command." : "")); - if (!flags.all) + if (!all) members = members.Where(g => g.MemberVisibility == PrivacyLevel.Public); else ctx.CheckOwnGroup(group); @@ -112,7 +112,7 @@ public class Random var randInt = randGen.Next(ms.Count); - if (flags.show_embed) + if (show_embed) { await ctx.Reply( text: EmbedService.LEGACY_EMBED_WARNING, diff --git a/crates/command_definitions/src/random.rs b/crates/command_definitions/src/random.rs index ce4d2288..a5892cc9 100644 --- a/crates/command_definitions/src/random.rs +++ b/crates/command_definitions/src/random.rs @@ -8,7 +8,10 @@ pub fn cmds() -> impl Iterator { [ command!(random => "random_self").flag(group), + command!(random, group => "random_group_self"), + command!(random, group::targeted() => "random_group_member_self").flags(get_list_flags()), command!(system::targeted(), random => "system_random").flag(group), + command!(system::targeted(), random, group => "system_random_group"), command!(group::targeted(), random => "group_random_member").flags(get_list_flags()), ] .into_iter() diff --git a/crates/command_definitions/src/system.rs b/crates/command_definitions/src/system.rs index 3d9e3d1e..7b6d77e0 100644 --- a/crates/command_definitions/src/system.rs +++ b/crates/command_definitions/src/system.rs @@ -1,3 +1,5 @@ +use std::iter::once; + use command_parser::token::TokensIterator; use crate::utils::get_list_flags; @@ -44,17 +46,17 @@ pub fn edit() -> impl Iterator { .flag(("private", ["priv"])) .flag(ALL) }; - let system_info_cmd_self = std::iter::once(add_info_flags( + let system_info_cmd_self = once(add_info_flags( command!(system => "system_info_self").help("Shows information about your system"), )); - let system_info_cmd = std::iter::once(add_info_flags( + let system_info_cmd = once(add_info_flags( command!(system_target, ("info", ["show", "view"]) => "system_info") .help("Shows information about your system"), )); let system_name = tokens!(system_target, "name"); let system_name_cmd = - std::iter::once(command!(system_name => "system_show_name").help("Shows the systems name")); + once(command!(system_name => "system_show_name").help("Shows the systems name")); let system_name_self = tokens!(system, "name"); let system_name_self_cmd = [ @@ -68,7 +70,7 @@ pub fn edit() -> impl Iterator { .into_iter(); let system_server_name = tokens!(system_target, ("servername", ["sn", "guildname"])); - let system_server_name_cmd = std::iter::once( + let system_server_name_cmd = once( command!(system_server_name => "system_show_server_name") .help("Shows the system's server name"), ); @@ -86,7 +88,7 @@ pub fn edit() -> impl Iterator { .into_iter(); let system_description = tokens!(system_target, ("description", ["desc", "d"])); - let system_description_cmd = std::iter::once( + let system_description_cmd = once( command!(system_description => "system_show_description") .help("Shows the system's description"), ); @@ -103,9 +105,8 @@ pub fn edit() -> impl Iterator { .into_iter(); let system_color = tokens!(system_target, ("color", ["colour"])); - let system_color_cmd = std::iter::once( - command!(system_color => "system_show_color").help("Shows the system's color"), - ); + let system_color_cmd = + once(command!(system_color => "system_show_color").help("Shows the system's color")); let system_color_self = tokens!(system, ("color", ["colour"])); let system_color_self_cmd = [ @@ -120,7 +121,7 @@ pub fn edit() -> impl Iterator { let system_tag = tokens!(system_target, ("tag", ["suffix"])); let system_tag_cmd = - std::iter::once(command!(system_tag => "system_show_tag").help("Shows the system's tag")); + once(command!(system_tag => "system_show_tag").help("Shows the system's tag")); let system_tag_self = tokens!(system, ("tag", ["suffix"])); let system_tag_self_cmd = [ @@ -134,7 +135,7 @@ pub fn edit() -> impl Iterator { .into_iter(); let system_server_tag = tokens!(system_target, ("servertag", ["st", "guildtag"])); - let system_server_tag_cmd = std::iter::once( + let system_server_tag_cmd = once( command!(system_server_tag => "system_show_server_tag") .help("Shows the system's server tag"), ); @@ -152,7 +153,7 @@ pub fn edit() -> impl Iterator { .into_iter(); let system_pronouns = tokens!(system_target, ("pronouns", ["prns"])); - let system_pronouns_cmd = std::iter::once( + let system_pronouns_cmd = once( command!(system_pronouns => "system_show_pronouns").help("Shows the system's pronouns"), ); @@ -169,9 +170,8 @@ pub fn edit() -> impl Iterator { .into_iter(); let system_avatar = tokens!(system_target, ("avatar", ["pfp"])); - let system_avatar_cmd = std::iter::once( - command!(system_avatar => "system_show_avatar").help("Shows the system's avatar"), - ); + let system_avatar_cmd = + once(command!(system_avatar => "system_show_avatar").help("Shows the system's avatar")); let system_avatar_self = tokens!(system, ("avatar", ["pfp"])); let system_avatar_self_cmd = [ @@ -186,7 +186,7 @@ pub fn edit() -> impl Iterator { .into_iter(); let system_server_avatar = tokens!(system_target, ("serveravatar", ["spfp"])); - let system_server_avatar_cmd = std::iter::once( + let system_server_avatar_cmd = once( command!(system_server_avatar => "system_show_server_avatar") .help("Shows the system's server avatar"), ); @@ -204,9 +204,8 @@ pub fn edit() -> impl Iterator { .into_iter(); let system_banner = tokens!(system_target, ("banner", ["cover"])); - let system_banner_cmd = std::iter::once( - command!(system_banner => "system_show_banner").help("Shows the system's banner"), - ); + let system_banner_cmd = + once(command!(system_banner => "system_show_banner").help("Shows the system's banner")); let system_banner_self = tokens!(system, ("banner", ["cover"])); let system_banner_self_cmd = [ @@ -220,7 +219,7 @@ pub fn edit() -> impl Iterator { ] .into_iter(); - let system_delete = std::iter::once( + let system_delete = once( command!(system, ("delete", ["erase", "remove", "yeet"]) => "system_delete") .flag(("no-export", ["ne"])) .help("Deletes the system"), @@ -300,10 +299,8 @@ pub fn edit() -> impl Iterator { .into_iter() .map(add_list_flags); - let system_display_id_self_cmd = - std::iter::once(command!(system, "id" => "system_display_id_self")); - let system_display_id_cmd = - std::iter::once(command!(system_target, "id" => "system_display_id")); + let system_display_id_self_cmd = once(command!(system, "id" => "system_display_id_self")); + let system_display_id_cmd = once(command!(system_target, "id" => "system_display_id")); system_info_cmd_self .chain(system_new_cmd) From 6bc39d23fb657eb5c5e2a5ed0ca7d9df56a64b7e Mon Sep 17 00:00:00 2001 From: dusk Date: Sun, 23 Nov 2025 14:06:18 +0000 Subject: [PATCH 138/179] detect invalid flags before misplaced flags --- crates/command_parser/src/lib.rs | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/crates/command_parser/src/lib.rs b/crates/command_parser/src/lib.rs index 360a170b..8aba4d21 100644 --- a/crates/command_parser/src/lib.rs +++ b/crates/command_parser/src/lib.rs @@ -153,16 +153,15 @@ pub fn parse_command( let mut flags: HashMap> = HashMap::new(); let mut misplaced_flags: Vec = Vec::new(); let mut invalid_flags: Vec = Vec::new(); - for (token_idx, matched_flag) in raw_flags { - if token_idx != command.parse_flags_before { - misplaced_flags.push(matched_flag); - continue; - } - let Some(matched_flag) = match_flag(command.flags.iter(), matched_flag.clone()) - else { - invalid_flags.push(matched_flag); + for (token_idx, raw_flag) in raw_flags { + let Some(matched_flag) = match_flag(command.flags.iter(), raw_flag.clone()) else { + invalid_flags.push(raw_flag); continue; }; + if token_idx != command.parse_flags_before { + misplaced_flags.push(raw_flag); + continue; + } match matched_flag { // a flag was matched Ok((name, value)) => { From b3fdaec68c44046e5462d7c545507310efeb80c3 Mon Sep 17 00:00:00 2001 From: dusk Date: Sun, 23 Nov 2025 14:46:18 +0000 Subject: [PATCH 139/179] fix group, member list related commands to work like on prod --- PluralKit.Bot/CommandMeta/CommandTree.cs | 16 ++++----- crates/command_definitions/src/group.rs | 30 ++++++---------- crates/command_definitions/src/member.rs | 16 ++++----- crates/command_definitions/src/system.rs | 46 ++++++++++-------------- crates/command_definitions/src/utils.rs | 2 +- 5 files changed, 42 insertions(+), 68 deletions(-) diff --git a/PluralKit.Bot/CommandMeta/CommandTree.cs b/PluralKit.Bot/CommandMeta/CommandTree.cs index 600ab47b..4393b798 100644 --- a/PluralKit.Bot/CommandMeta/CommandTree.cs +++ b/PluralKit.Bot/CommandMeta/CommandTree.cs @@ -234,18 +234,14 @@ public partial class CommandTree Commands.GroupRandomMember(var param, var flags) => ctx.Execute(GroupMemberRandom, m => m.GroupMember(ctx, param.target, flags.all, flags.show_embed, flags)), Commands.SystemLink(var param, _) => ctx.Execute(Link, m => m.LinkSystem(ctx, param.account)), Commands.SystemUnlink(var param, var flags) => ctx.Execute(Unlink, m => m.UnlinkAccount(ctx, param.account, flags.yes)), - Commands.SystemMembersListSelf(var param, var flags) => ctx.Execute(SystemList, m => m.MemberList(ctx, ctx.System, null, flags)), - Commands.SystemMembersSearchSelf(var param, var flags) => ctx.Execute(SystemFind, m => m.MemberList(ctx, ctx.System, param.query, flags)), - Commands.SystemMembersList(var param, var flags) => ctx.Execute(SystemList, m => m.MemberList(ctx, param.target, null, flags)), - Commands.SystemMembersSearch(var param, var flags) => ctx.Execute(SystemFind, m => m.MemberList(ctx, param.target, param.query, flags)), + Commands.SystemMembersSelf(var param, var flags) => ctx.Execute(SystemList, m => m.MemberList(ctx, ctx.System, param.query, flags)), + Commands.SystemMembers(var param, var flags) => ctx.Execute(SystemList, m => m.MemberList(ctx, param.target, param.query, flags)), Commands.MemberListGroups(var param, var flags) => ctx.Execute(MemberGroups, m => m.ListMemberGroups(ctx, param.target, null, flags, flags.all)), Commands.MemberSearchGroups(var param, var flags) => ctx.Execute(MemberGroups, m => m.ListMemberGroups(ctx, param.target, param.query, flags, flags.all)), - Commands.GroupListMembers(var param, var flags) => ctx.Execute(GroupMemberList, m => m.ListGroupMembers(ctx, param.target, null, flags)), - Commands.GroupSearchMembers(var param, var flags) => ctx.Execute(GroupMemberList, m => m.ListGroupMembers(ctx, param.target, param.query, flags)), - Commands.SystemListGroups(var param, var flags) => ctx.Execute(GroupList, g => g.ListSystemGroups(ctx, param.target, null, flags, flags.all)), - Commands.SystemSearchGroups(var param, var flags) => ctx.Execute(GroupList, g => g.ListSystemGroups(ctx, param.target, param.query, flags, flags.all)), - Commands.GroupListGroups(var param, var flags) => ctx.Execute(GroupList, g => g.ListSystemGroups(ctx, ctx.System, null, flags, flags.all)), - Commands.GroupSearchGroups(var param, var flags) => ctx.Execute(GroupList, g => g.ListSystemGroups(ctx, ctx.System, param.query, flags, flags.all)), + Commands.GroupMembers(var param, var flags) => ctx.Execute(GroupMemberList, m => m.ListGroupMembers(ctx, param.target, param.query, flags)), + Commands.SystemGroups(var param, var flags) => ctx.Execute(GroupList, g => g.ListSystemGroups(ctx, param.target, param.query, flags, flags.all)), + Commands.SystemGroupsSelf(var param, var flags) => ctx.Execute(GroupList, g => g.ListSystemGroups(ctx, ctx.System, param.query, flags, flags.all)), + Commands.GroupsSelf(var param, var flags) => ctx.Execute(GroupList, g => g.ListSystemGroups(ctx, ctx.System, param.query, flags, flags.all)), Commands.GroupNew(var param, _) => ctx.Execute(GroupNew, g => g.CreateGroup(ctx, param.name)), Commands.GroupInfo(var param, var flags) => ctx.Execute(GroupInfo, g => g.ShowGroupCard(ctx, param.target, flags.show_embed, flags.all)), Commands.GroupShowName(var param, var flags) => ctx.Execute(GroupRename, g => g.ShowGroupDisplayName(ctx, param.target, flags.GetReplyFormat())), diff --git a/crates/command_definitions/src/group.rs b/crates/command_definitions/src/group.rs index 6512722d..8dc7f468 100644 --- a/crates/command_definitions/src/group.rs +++ b/crates/command_definitions/src/group.rs @@ -1,11 +1,13 @@ +use std::iter::once; + use command_parser::token::TokensIterator; use crate::utils::get_list_flags; use super::*; -pub fn group() -> (&'static str, [&'static str; 2]) { - ("group", ["g", "groups"]) +pub fn group() -> (&'static str, [&'static str; 1]) { + ("group", ["g"]) } pub fn targeted() -> TokensIterator { @@ -151,19 +153,11 @@ pub fn cmds() -> impl Iterator { .into_iter(); let apply_list_opts = |cmd: Command| cmd.flags(get_list_flags()); + let search_param = Optional(Remainder(("query", OpaqueString))); - let search = tokens!( - ("search", ["find", "query"]), - Remainder(("query", OpaqueString)) - ); - let group_list_members = tokens!(group_target, ("members", ["list", "ls"])); - let group_list_members_cmd = [ - command!(group_list_members => "group_list_members"), - command!(group_list_members, "list" => "group_list_members"), - command!(group_list_members, search => "group_search_members"), - ] - .into_iter() - .map(apply_list_opts); + let group_list_members_cmd = + once(command!(group_target, ("members", ["list", "ls"]), search_param => "group_members")) + .map(apply_list_opts); let group_modify_members_cmd = [ command!(group_target, "add", Optional(MemberRefs) => "group_add_member") @@ -173,12 +167,8 @@ pub fn cmds() -> impl Iterator { ] .into_iter(); - let system_groups_cmd = [ - command!(group, ("list", ["ls"]) => "group_list_groups"), - command!(group, search => "group_search_groups"), - ] - .into_iter() - .map(apply_list_opts); + let system_groups_cmd = + once(command!(group, ("list", ["ls"]), search_param => "groups_self")).map(apply_list_opts); system_groups_cmd .chain(group_new_cmd) diff --git a/crates/command_definitions/src/member.rs b/crates/command_definitions/src/member.rs index 655384ff..a8be537e 100644 --- a/crates/command_definitions/src/member.rs +++ b/crates/command_definitions/src/member.rs @@ -1,3 +1,5 @@ +use std::iter::once; + use command_parser::token::TokensIterator; use crate::utils::get_list_flags; @@ -294,15 +296,11 @@ pub fn cmds() -> impl Iterator { .chain(member_webhook_avatar_cmd) .chain(member_server_avatar_cmd); - let member_group = tokens!(member_target, group::group()); - let member_list_group_cmds = [ - command!(member_group => "member_list_groups"), - command!(member_group, "list" => "member_list_groups"), - command!(member_group, ("search", ["find", "query"]), Remainder(("query", OpaqueString)) => "member_search_groups"), - ] - .into_iter() + let member_group = tokens!(member_target, ("groups", ["group"])); + let member_list_group_cmds = once( + command!(member_group, Optional(Remainder(("query", OpaqueString))) => "member_groups"), + ) .map(|cmd| cmd.flags(get_list_flags())); - let member_add_remove_group_cmds = [ command!(member_group, "add", Optional(("groups", GroupRefs)) => "member_group_add") .help("Adds a member to one or more groups"), @@ -339,6 +337,6 @@ pub fn cmds() -> impl Iterator { .chain(member_display_id_cmd) .chain(member_delete_cmd) .chain(member_easter_eggs) - .chain(member_list_group_cmds) .chain(member_add_remove_group_cmds) + .chain(member_list_group_cmds) } diff --git a/crates/command_definitions/src/system.rs b/crates/command_definitions/src/system.rs index 7b6d77e0..4ec0f017 100644 --- a/crates/command_definitions/src/system.rs +++ b/crates/command_definitions/src/system.rs @@ -269,35 +269,24 @@ pub fn edit() -> impl Iterator { ] .into_iter(); - let list = ("list", ["ls", "l"]); - let system_list = tokens!("members", list); - let search = tokens!( - ("search", ["query", "find"]), - Remainder(("query", OpaqueString)) - ); - let add_list_flags = |cmd: Command| cmd.flags(get_list_flags()); - let system_list_cmd = [ - command!(system_target, system_list => "system_members_list"), - command!(system_target, search => "system_members_search"), - ] - .into_iter() - .map(add_list_flags); - let system_list_self_cmd = [ - command!(system_list => "system_members_list_self"), - command!(system, system_list => "system_members_list_self"), - command!(system, search => "system_members_search_self"), - ] - .into_iter() - .map(add_list_flags); + let search_param = Optional(Remainder(("query", OpaqueString))); + let apply_list_opts = |cmd: Command| cmd.flags(get_list_flags()); - let system_groups = tokens!(system_target, group::group()); - let system_groups_cmd = [ - command!(system_groups => "system_list_groups"), - command!(system_groups, list => "system_list_groups"), - command!(system_groups, search => "system_search_groups"), + let members_subcmd = tokens!(("members", ["ls", "list"]), search_param); + let system_members_cmd = + once(command!(system_target, members_subcmd => "system_members")).map(apply_list_opts); + let system_members_self_cmd = [ + command!(system, members_subcmd => "system_members_self"), + command!(members_subcmd => "system_members_self"), ] .into_iter() - .map(add_list_flags); + .map(apply_list_opts); + + let groups_subcmd = tokens!("groups", search_param); + let system_groups_cmd = + once(command!(system_target, groups_subcmd => "system_groups")).map(apply_list_opts); + let system_group_self_cmd = + once(command!(system, groups_subcmd => "system_groups_self")).map(apply_list_opts); let system_display_id_self_cmd = once(command!(system, "id" => "system_display_id_self")); let system_display_id_cmd = once(command!(system_target, "id" => "system_display_id")); @@ -315,7 +304,8 @@ pub fn edit() -> impl Iterator { .chain(system_avatar_self_cmd) .chain(system_server_avatar_self_cmd) .chain(system_banner_self_cmd) - .chain(system_list_self_cmd) + .chain(system_members_self_cmd) + .chain(system_group_self_cmd) .chain(system_display_id_self_cmd) .chain(system_front_self_cmd) .chain(system_delete) @@ -334,7 +324,7 @@ pub fn edit() -> impl Iterator { .chain(system_info_cmd) .chain(system_front_cmd) .chain(system_link) - .chain(system_list_cmd) + .chain(system_members_cmd) .chain(system_groups_cmd) .chain(system_display_id_cmd) } diff --git a/crates/command_definitions/src/utils.rs b/crates/command_definitions/src/utils.rs index 6eb40c57..1c589ef0 100644 --- a/crates/command_definitions/src/utils.rs +++ b/crates/command_definitions/src/utils.rs @@ -48,7 +48,7 @@ pub fn get_list_flags() -> [Flag; 22] { ["with-image", "with-icon", "wa", "wi", "ia", "ii", "img"], )), Flag::from(("with-pronouns", ["wp", "wprns"])), - Flag::from(("with-displayname", ["wdn"])), + Flag::from(("with-display-name", ["wdn"])), Flag::from(("with-birthday", ["wbd", "wb"])), ] } From 960f735db9c74cab0db959bfb221ab77d47d53ca Mon Sep 17 00:00:00 2001 From: dusk Date: Sun, 23 Nov 2025 14:51:53 +0000 Subject: [PATCH 140/179] system refs now first parse for a user ref --- crates/command_parser/src/parameter.rs | 36 ++++++++++++++------------ 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/crates/command_parser/src/parameter.rs b/crates/command_parser/src/parameter.rs index 0d13d773..a7032dca 100644 --- a/crates/command_parser/src/parameter.rs +++ b/crates/command_parser/src/parameter.rs @@ -139,23 +139,10 @@ impl Parameter { ParameterKind::MemberRefs => Ok(ParameterValue::MemberRefs( input.split(' ').map(|s| s.trim().to_string()).collect(), )), - ParameterKind::SystemRef => Ok(ParameterValue::SystemRef(input.into())), - ParameterKind::UserRef => { - if let Ok(user_id) = input.parse::() { - return Ok(ParameterValue::UserRef(user_id)); - } - - static RE: std::sync::LazyLock = - std::sync::LazyLock::new(|| Regex::new(r"<@!?(\\d{17,19})>").unwrap()); - if let Some(captures) = RE.captures(&input) { - return captures[1] - .parse::() - .map(|id| ParameterValue::UserRef(id)) - .map_err(|_| SmolStr::new("invalid user ID")); - } - - Err(SmolStr::new("invalid user ID")) + ParameterKind::SystemRef => { + Ok(parse_user_ref(input).unwrap_or(ParameterValue::SystemRef(input.into()))) } + ParameterKind::UserRef => parse_user_ref(input), ParameterKind::MemberPrivacyTarget => MemberPrivacyTargetKind::from_str(input) .map(|target| ParameterValue::MemberPrivacyTarget(target.as_ref().into())), ParameterKind::GroupPrivacyTarget => GroupPrivacyTargetKind::from_str(input) @@ -328,6 +315,23 @@ impl> From> for Parameter { } } +fn parse_user_ref(input: &str) -> Result { + if let Ok(user_id) = input.parse::() { + return Ok(ParameterValue::UserRef(user_id)); + } + + static RE: std::sync::LazyLock = + std::sync::LazyLock::new(|| Regex::new(r"<@!?(\\d{17,19})>").unwrap()); + if let Some(captures) = RE.captures(&input) { + return captures[1] + .parse::() + .map(|id| ParameterValue::UserRef(id)) + .map_err(|_| SmolStr::new("invalid user ID")); + } + + Err(SmolStr::new("invalid user ID")) +} + macro_rules! impl_enum { ($name:ident ($pretty_name:expr): $($variant:ident),*) => { #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] From e9933c36ce70cabf539fbd0e27acc68a0acce596 Mon Sep 17 00:00:00 2001 From: dusk Date: Sun, 23 Nov 2025 15:05:12 +0000 Subject: [PATCH 141/179] make fronthistory and frontpercent work in system commands --- crates/command_definitions/src/system.rs | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/crates/command_definitions/src/system.rs b/crates/command_definitions/src/system.rs index 4ec0f017..6106e574 100644 --- a/crates/command_definitions/src/system.rs +++ b/crates/command_definitions/src/system.rs @@ -250,18 +250,26 @@ pub fn edit() -> impl Iterator { let front = ("front", ["fronter", "fronters", "f"]); let make_system_front_cmd = |prefix: TokensIterator, suffix: &str| { - [ - command!(prefix => format!("system_fronter{}", suffix)), - command!(prefix, ("history", ["h"]) => format!("system_fronter_history{}", suffix)).flag(CLEAR), - command!(prefix, ("percent", ["p", "%"]) => format!("system_fronter_percent{}", suffix)) + let make_front_history = |subcmd: TokensIterator| { + command!(prefix, subcmd => format!("system_fronter_history{}", suffix)).flag(CLEAR) + }; + let make_front_percent = |subcmd: TokensIterator| { + command!(prefix, subcmd => format!("system_fronter_percent{}", suffix)) .flag(("duration", OpaqueString)) .flag(("fronters-only", ["fo"])) - .flag("flat"), + .flag("flat") + }; + [ + command!(prefix, front => format!("system_fronter{}", suffix)), + make_front_history(tokens!(front, ("history", ["h"]))), + make_front_history(tokens!(("fronthistory", ["fh"]))), + make_front_percent(tokens!(front, ("percent", ["p", "%"]))), + make_front_percent(tokens!(("frontpercent", ["fp"]))), ] .into_iter() }; - let system_front_cmd = make_system_front_cmd(tokens!(system_target, front), ""); - let system_front_self_cmd = make_system_front_cmd(tokens!(system, front), "_self"); + let system_front_cmd = make_system_front_cmd(tokens!(system_target), ""); + let system_front_self_cmd = make_system_front_cmd(tokens!(system), "_self"); let system_link = [ command!("link", ("account", UserRef) => "system_link"), From 6aed93ba21ad079c7b350d22c20709fd0ce268eb Mon Sep 17 00:00:00 2001 From: dusk Date: Sun, 23 Nov 2025 16:06:26 +0000 Subject: [PATCH 142/179] fix edit message no-space flag --- PluralKit.Bot/CommandMeta/CommandTree.cs | 7 +++---- PluralKit.Bot/Commands/Message.cs | 4 +++- crates/command_definitions/src/message.rs | 2 +- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/PluralKit.Bot/CommandMeta/CommandTree.cs b/PluralKit.Bot/CommandMeta/CommandTree.cs index 4393b798..d456728c 100644 --- a/PluralKit.Bot/CommandMeta/CommandTree.cs +++ b/PluralKit.Bot/CommandMeta/CommandTree.cs @@ -236,8 +236,7 @@ public partial class CommandTree Commands.SystemUnlink(var param, var flags) => ctx.Execute(Unlink, m => m.UnlinkAccount(ctx, param.account, flags.yes)), Commands.SystemMembersSelf(var param, var flags) => ctx.Execute(SystemList, m => m.MemberList(ctx, ctx.System, param.query, flags)), Commands.SystemMembers(var param, var flags) => ctx.Execute(SystemList, m => m.MemberList(ctx, param.target, param.query, flags)), - Commands.MemberListGroups(var param, var flags) => ctx.Execute(MemberGroups, m => m.ListMemberGroups(ctx, param.target, null, flags, flags.all)), - Commands.MemberSearchGroups(var param, var flags) => ctx.Execute(MemberGroups, m => m.ListMemberGroups(ctx, param.target, param.query, flags, flags.all)), + Commands.MemberGroups(var param, var flags) => ctx.Execute(MemberGroups, m => m.ListMemberGroups(ctx, param.target, param.query, flags, flags.all)), Commands.GroupMembers(var param, var flags) => ctx.Execute(GroupMemberList, m => m.ListGroupMembers(ctx, param.target, param.query, flags)), Commands.SystemGroups(var param, var flags) => ctx.Execute(GroupList, g => g.ListSystemGroups(ctx, param.target, param.query, flags, flags.all)), Commands.SystemGroupsSelf(var param, var flags) => ctx.Execute(GroupList, g => g.ListSystemGroups(ctx, ctx.System, param.query, flags, flags.all)), @@ -285,8 +284,8 @@ public partial class CommandTree Commands.MessageInfo(var param, var flags) => ctx.Execute(Message, m => m.GetMessage(ctx, param.target.MessageId, flags.GetReplyFormat(), flags.delete, flags.author)), Commands.MessageAuthor(var param, var flags) => ctx.Execute(Message, m => m.GetMessage(ctx, param.target.MessageId, flags.GetReplyFormat(), false, true)), Commands.MessageDelete(var param, var flags) => ctx.Execute(Message, m => m.GetMessage(ctx, param.target.MessageId, flags.GetReplyFormat(), true, false)), - Commands.MessageEditSpecified(var param, var flags) => ctx.Execute(MessageEdit, m => m.EditMessage(ctx, param.target.MessageId, param.new_content, flags.regex, flags.mutate_space, flags.append, flags.prepend, flags.clear_embeds, flags.clear_attachments)), - Commands.MessageEdit(var param, var flags) => ctx.Execute(MessageEdit, m => m.EditMessage(ctx, null, param.new_content, flags.regex, flags.mutate_space, flags.append, flags.prepend, flags.clear_embeds, flags.clear_attachments)), + Commands.MessageEditSpecified(var param, var flags) => ctx.Execute(MessageEdit, m => m.EditMessage(ctx, param.target.MessageId, param.new_content, flags.regex, flags.no_space, flags.append, flags.prepend, flags.clear_embeds, flags.clear_attachments)), + Commands.MessageEdit(var param, var flags) => ctx.Execute(MessageEdit, m => m.EditMessage(ctx, null, param.new_content, flags.regex, flags.no_space, flags.append, flags.prepend, flags.clear_embeds, flags.clear_attachments)), Commands.MessageReproxySpecified(var param, _) => ctx.Execute(MessageReproxy, m => m.ReproxyMessage(ctx, param.msg.MessageId, param.member)), Commands.MessageReproxy(var param, _) => ctx.Execute(MessageReproxy, m => m.ReproxyMessage(ctx, null, param.member)), Commands.Import(var param, var flags) => ctx.Execute(Import, m => m.Import(ctx, param.url, flags.yes)), diff --git a/PluralKit.Bot/Commands/Message.cs b/PluralKit.Bot/Commands/Message.cs index 6e25b67f..70e48472 100644 --- a/PluralKit.Bot/Commands/Message.cs +++ b/PluralKit.Bot/Commands/Message.cs @@ -91,7 +91,7 @@ public class ProxiedMessage } } - public async Task EditMessage(Context ctx, ulong? messageId, string newContent, bool useRegex, bool mutateSpace, bool append, bool prepend, bool clearEmbeds, bool clearAttachments) + public async Task EditMessage(Context ctx, ulong? messageId, string newContent, bool useRegex, bool noSpace, bool append, bool prepend, bool clearEmbeds, bool clearAttachments) { var (msg, systemId) = await GetMessageToEdit(ctx, messageId, EditTimeout, false); @@ -102,6 +102,8 @@ public class ProxiedMessage if (originalMsg == null) throw new PKError("Could not edit message."); + var mutateSpace = noSpace ? "" : " "; + // Grab the original message content and new message content var originalContent = originalMsg.Content; diff --git a/crates/command_definitions/src/message.rs b/crates/command_definitions/src/message.rs index f341fa78..4ba2ae46 100644 --- a/crates/command_definitions/src/message.rs +++ b/crates/command_definitions/src/message.rs @@ -13,7 +13,7 @@ pub fn cmds() -> impl Iterator { cmd.flag(("append", ["a"])) .flag(("prepend", ["p"])) .flag(("regex", ["r"])) - .flag(("mutate-space", ["ms"])) + .flag(("no-space", ["nospace", "ns"])) .flag(("clear-embeds", ["ce"])) .flag(("clear-attachments", ["ca"])) .help("Edits a proxied message") From 74e7af0ee1c90902da6f389b9fd47679082d938a Mon Sep 17 00:00:00 2001 From: dusk Date: Sun, 23 Nov 2025 17:09:22 +0000 Subject: [PATCH 143/179] remove new MatchFlag usage, fix glue gen --- PluralKit.Bot/CommandMeta/CommandTree.cs | 6 +++--- PluralKit.Bot/Commands/Message.cs | 12 ++++++------ crates/commands/src/write_cs_glue.rs | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/PluralKit.Bot/CommandMeta/CommandTree.cs b/PluralKit.Bot/CommandMeta/CommandTree.cs index d456728c..0e66ef00 100644 --- a/PluralKit.Bot/CommandMeta/CommandTree.cs +++ b/PluralKit.Bot/CommandMeta/CommandTree.cs @@ -281,9 +281,9 @@ public partial class CommandTree Commands.PermcheckChannel(var param, _) => ctx.Execute(PermCheck, m => m.PermCheckChannel(ctx, param.target)), Commands.PermcheckGuild(var param, _) => ctx.Execute(PermCheck, m => m.PermCheckGuild(ctx, param.target)), Commands.MessageProxyCheck(var param, _) => ctx.Execute(ProxyCheck, m => m.MessageProxyCheck(ctx, param.target)), - Commands.MessageInfo(var param, var flags) => ctx.Execute(Message, m => m.GetMessage(ctx, param.target.MessageId, flags.GetReplyFormat(), flags.delete, flags.author)), - Commands.MessageAuthor(var param, var flags) => ctx.Execute(Message, m => m.GetMessage(ctx, param.target.MessageId, flags.GetReplyFormat(), false, true)), - Commands.MessageDelete(var param, var flags) => ctx.Execute(Message, m => m.GetMessage(ctx, param.target.MessageId, flags.GetReplyFormat(), true, false)), + Commands.MessageInfo(var param, var flags) => ctx.Execute(Message, m => m.GetMessage(ctx, param.target.MessageId, flags.GetReplyFormat(), flags.delete, flags.author, flags.show_embed)), + Commands.MessageAuthor(var param, var flags) => ctx.Execute(Message, m => m.GetMessage(ctx, param.target.MessageId, flags.GetReplyFormat(), false, true, flags.show_embed)), + Commands.MessageDelete(var param, var flags) => ctx.Execute(Message, m => m.GetMessage(ctx, param.target.MessageId, flags.GetReplyFormat(), true, false, flags.show_embed)), Commands.MessageEditSpecified(var param, var flags) => ctx.Execute(MessageEdit, m => m.EditMessage(ctx, param.target.MessageId, param.new_content, flags.regex, flags.no_space, flags.append, flags.prepend, flags.clear_embeds, flags.clear_attachments)), Commands.MessageEdit(var param, var flags) => ctx.Execute(MessageEdit, m => m.EditMessage(ctx, null, param.new_content, flags.regex, flags.no_space, flags.append, flags.prepend, flags.clear_embeds, flags.clear_attachments)), Commands.MessageReproxySpecified(var param, _) => ctx.Execute(MessageReproxy, m => m.ReproxyMessage(ctx, param.msg.MessageId, param.member)), diff --git a/PluralKit.Bot/Commands/Message.cs b/PluralKit.Bot/Commands/Message.cs index 4a0eb699..6284abee 100644 --- a/PluralKit.Bot/Commands/Message.cs +++ b/PluralKit.Bot/Commands/Message.cs @@ -320,7 +320,7 @@ public class ProxiedMessage return lastMessage; } - public async Task GetMessage(Context ctx, ulong? messageId, ReplyFormat format, bool isDelete, bool author) + public async Task GetMessage(Context ctx, ulong? messageId, ReplyFormat format, bool isDelete, bool author, bool showEmbed) { if (messageId == null) { @@ -330,7 +330,7 @@ public class ProxiedMessage var message = await ctx.Repository.GetFullMessage(messageId.Value); if (message == null) { - await GetCommandMessage(ctx, messageId.Value, isDelete); + await GetCommandMessage(ctx, messageId.Value, isDelete, showEmbed); return; } @@ -407,7 +407,7 @@ public class ProxiedMessage if (author) { var user = await _rest.GetUser(message.Message.Sender); - if (ctx.MatchFlag("show-embed", "se")) + if (showEmbed) { var eb = new EmbedBuilder() .Author(new Embed.EmbedAuthor( @@ -427,7 +427,7 @@ public class ProxiedMessage return; } - if (ctx.MatchFlag("show-embed", "se")) + if (showEmbed) { await ctx.Reply(embed: await _embeds.CreateMessageInfoEmbed(message, showContent, ctx.Config)); return; @@ -436,7 +436,7 @@ public class ProxiedMessage await ctx.Reply(components: await _embeds.CreateMessageInfoMessageComponents(message, showContent, ctx.Config)); } - private async Task GetCommandMessage(Context ctx, ulong messageId, bool isDelete) + private async Task GetCommandMessage(Context ctx, ulong messageId, bool isDelete, bool showEmbed) { var msg = await _repo.GetCommandMessage(messageId); if (msg == null) @@ -465,7 +465,7 @@ public class ProxiedMessage else if (!await ctx.CheckPermissionsInGuildChannel(channel, PermissionSet.ViewChannel)) showContent = false; - if (ctx.MatchFlag("show-embed", "se")) + if (showEmbed) { await ctx.Reply(embed: await _embeds.CreateCommandMessageInfoEmbed(msg, showContent)); return; diff --git a/crates/commands/src/write_cs_glue.rs b/crates/commands/src/write_cs_glue.rs index d2269d03..6d6fb414 100644 --- a/crates/commands/src/write_cs_glue.rs +++ b/crates/commands/src/write_cs_glue.rs @@ -207,7 +207,7 @@ fn main() -> Result<(), Box> { p.IncludeCreated = with_created; p.IncludeAvatar = with_avatar; p.IncludePronouns = with_pronouns; - p.IncludeDisplayName = with_displayname; + p.IncludeDisplayName = with_display_name; p.IncludeBirthday = with_birthday; // Always show the sort property (unless short list and already showing something else) From b9fb453c735911220a4f04f157fef30a524642ed Mon Sep 17 00:00:00 2001 From: dusk Date: Sun, 23 Nov 2025 19:27:18 +0000 Subject: [PATCH 144/179] fix user ref regex, system ref parsing and add s --- crates/command_definitions/src/system.rs | 13 ++++++++----- crates/command_parser/src/parameter.rs | 6 ++---- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/crates/command_definitions/src/system.rs b/crates/command_definitions/src/system.rs index 6106e574..905f81cc 100644 --- a/crates/command_definitions/src/system.rs +++ b/crates/command_definitions/src/system.rs @@ -46,13 +46,16 @@ pub fn edit() -> impl Iterator { .flag(("private", ["priv"])) .flag(ALL) }; - let system_info_cmd_self = once(add_info_flags( - command!(system => "system_info_self").help("Shows information about your system"), - )); - let system_info_cmd = once(add_info_flags( + let system_info_cmd_self = + once(command!(system => "system_info_self").help("Shows information about your system")) + .map(add_info_flags); + let system_info_cmd = [ + command!(system_target => "system_info").help("Shows information about your system"), command!(system_target, ("info", ["show", "view"]) => "system_info") .help("Shows information about your system"), - )); + ] + .into_iter() + .map(add_info_flags); let system_name = tokens!(system_target, "name"); let system_name_cmd = diff --git a/crates/command_parser/src/parameter.rs b/crates/command_parser/src/parameter.rs index a7032dca..d9a24f3d 100644 --- a/crates/command_parser/src/parameter.rs +++ b/crates/command_parser/src/parameter.rs @@ -139,9 +139,7 @@ impl Parameter { ParameterKind::MemberRefs => Ok(ParameterValue::MemberRefs( input.split(' ').map(|s| s.trim().to_string()).collect(), )), - ParameterKind::SystemRef => { - Ok(parse_user_ref(input).unwrap_or(ParameterValue::SystemRef(input.into()))) - } + ParameterKind::SystemRef => Ok(ParameterValue::SystemRef(input.into())), ParameterKind::UserRef => parse_user_ref(input), ParameterKind::MemberPrivacyTarget => MemberPrivacyTargetKind::from_str(input) .map(|target| ParameterValue::MemberPrivacyTarget(target.as_ref().into())), @@ -321,7 +319,7 @@ fn parse_user_ref(input: &str) -> Result { } static RE: std::sync::LazyLock = - std::sync::LazyLock::new(|| Regex::new(r"<@!?(\\d{17,19})>").unwrap()); + std::sync::LazyLock::new(|| Regex::new(r"<@!?(\d{17,19})>").unwrap()); if let Some(captures) = RE.captures(&input) { return captures[1] .parse::() From 3612cc22778fd5391e360bdf7e2a1302816231e7 Mon Sep 17 00:00:00 2001 From: dusk Date: Sun, 23 Nov 2025 19:56:47 +0000 Subject: [PATCH 145/179] fix member proxy tags set --- crates/command_definitions/src/member.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/command_definitions/src/member.rs b/crates/command_definitions/src/member.rs index a8be537e..607cc0a7 100644 --- a/crates/command_definitions/src/member.rs +++ b/crates/command_definitions/src/member.rs @@ -167,8 +167,6 @@ pub fn cmds() -> impl Iterator { [ command!(member_proxy => "member_proxy_show") .help("Shows a member's proxy tags"), - command!(member_proxy, Remainder(("tags", OpaqueString)) => "member_proxy_set") - .help("Sets a member's proxy tags"), command!(member_proxy, ("add", ["a"]), ("tag", OpaqueString) => "member_proxy_add") .help("Adds proxy tag to a member"), command!(member_proxy, ("remove", ["r", "rm"]), ("tag", OpaqueString) => "member_proxy_remove") @@ -176,6 +174,8 @@ pub fn cmds() -> impl Iterator { command!(member_proxy, CLEAR => "member_proxy_clear") .flag(YES) .help("Clears all proxy tags from a member"), + command!(member_proxy, Remainder(("tags", OpaqueString)) => "member_proxy_set") + .help("Sets a member's proxy tags"), ].into_iter() }; From c68a77bb32449aaa87f39ee0b7910e593ec577f4 Mon Sep 17 00:00:00 2001 From: dusk Date: Sun, 23 Nov 2025 20:07:14 +0000 Subject: [PATCH 146/179] add clear-embed and clear-attachment aliases for respective flags to edit cmd --- crates/command_definitions/src/message.rs | 4 ++-- crates/command_parser/src/command.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/command_definitions/src/message.rs b/crates/command_definitions/src/message.rs index 4ba2ae46..77427741 100644 --- a/crates/command_definitions/src/message.rs +++ b/crates/command_definitions/src/message.rs @@ -14,8 +14,8 @@ pub fn cmds() -> impl Iterator { .flag(("prepend", ["p"])) .flag(("regex", ["r"])) .flag(("no-space", ["nospace", "ns"])) - .flag(("clear-embeds", ["ce"])) - .flag(("clear-attachments", ["ca"])) + .flag(("clear-embeds", ["clear-embed", "ce"])) + .flag(("clear-attachments", ["clear-attachment", "ca"])) .help("Edits a proxied message") }; diff --git a/crates/command_parser/src/command.rs b/crates/command_parser/src/command.rs index dacb271f..83b57ae0 100644 --- a/crates/command_parser/src/command.rs +++ b/crates/command_parser/src/command.rs @@ -105,7 +105,7 @@ impl Display for Command { break; } write!(f, "{flag}")?; - if max_flags - 1 > written { + if max_flags > written { write!(f, " ")?; } written += 1; From c730820614bc53a493697ffd3456e66212a46fbd Mon Sep 17 00:00:00 2001 From: dusk Date: Sun, 23 Nov 2025 20:19:23 +0000 Subject: [PATCH 147/179] error out if no members to operate on were provided when -all is not passed in group member add/remove --- PluralKit.Bot/Commands/GroupMember.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/PluralKit.Bot/Commands/GroupMember.cs b/PluralKit.Bot/Commands/GroupMember.cs index a53bdd37..43ccbba9 100644 --- a/PluralKit.Bot/Commands/GroupMember.cs +++ b/PluralKit.Bot/Commands/GroupMember.cs @@ -83,7 +83,7 @@ public class GroupMember target.Color, opts, all); } - public async Task AddRemoveMembers(Context ctx, PKGroup target, List _members, Groups.AddRemoveOperation op, bool all, bool confirmYes) + public async Task AddRemoveMembers(Context ctx, PKGroup target, List? _members, Groups.AddRemoveOperation op, bool all, bool confirmYes) { ctx.CheckOwnGroup(target); @@ -98,6 +98,9 @@ public class GroupMember } else { + if (_members == null) + throw new PKError("Please provide a list of members to add/remove."); + members = _members .FindAll(m => m.System == ctx.System.Id) .Select(m => m.Id) From 271ea9c27b7fc928f0a7ae3479cfc1e1362e849a Mon Sep 17 00:00:00 2001 From: dusk Date: Sun, 23 Nov 2025 20:23:31 +0000 Subject: [PATCH 148/179] remove aliases from group member delete so it doesnt conflict with group delete --- crates/command_definitions/src/group.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/command_definitions/src/group.rs b/crates/command_definitions/src/group.rs index 8dc7f468..ce96ac27 100644 --- a/crates/command_definitions/src/group.rs +++ b/crates/command_definitions/src/group.rs @@ -162,7 +162,7 @@ pub fn cmds() -> impl Iterator { let group_modify_members_cmd = [ command!(group_target, "add", Optional(MemberRefs) => "group_add_member") .flag(ALL).flag(YES), - command!(group_target, ("remove", ["delete", "del", "rem"]), Optional(MemberRefs) => "group_remove_member") + command!(group_target, ("remove", ["rem", "rm"]), Optional(MemberRefs) => "group_remove_member") .flag(ALL).flag(YES), ] .into_iter(); From 25bf0d85d4750556f977b7f36e78aa7dc9859c4d Mon Sep 17 00:00:00 2001 From: dusk Date: Wed, 26 Nov 2025 17:03:01 +0000 Subject: [PATCH 149/179] duration can be passed as null from the parser so handle that in frontpercent --- PluralKit.Bot/Commands/SystemFront.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/PluralKit.Bot/Commands/SystemFront.cs b/PluralKit.Bot/Commands/SystemFront.cs index 330a9f77..7b8021c8 100644 --- a/PluralKit.Bot/Commands/SystemFront.cs +++ b/PluralKit.Bot/Commands/SystemFront.cs @@ -104,7 +104,7 @@ public class SystemFront ); } - public async Task FrontPercent(Context ctx, PKSystem? system = null, string durationStr = "30d", bool ignoreNoFronters = false, bool showFlat = false, PKGroup? group = null) + public async Task FrontPercent(Context ctx, PKSystem? system, string? durationStr, bool ignoreNoFronters = false, bool showFlat = false, PKGroup? group = null) { if (system == null && group == null) throw Errors.NoSystemError(ctx.DefaultPrefix); if (system == null) system = await GetGroupSystem(ctx, group); @@ -114,6 +114,9 @@ public class SystemFront var totalSwitches = await ctx.Repository.GetSwitchCount(system.Id); if (totalSwitches == 0) throw Errors.NoRegisteredSwitches; + if (durationStr == null) + durationStr = "30d"; + // Picked the UNIX epoch as a random date // even though we don't store switch timestamps in UNIX time // I assume most people won't have switches logged previously to that (?) From 81e0cebb8e2f6d5ac48fac5d36e6f3d7e298f9e5 Mon Sep 17 00:00:00 2001 From: dusk Date: Wed, 26 Nov 2025 17:06:15 +0000 Subject: [PATCH 150/179] add l alias for list, ls in pk;group list --- crates/command_definitions/src/group.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/command_definitions/src/group.rs b/crates/command_definitions/src/group.rs index ce96ac27..24ab6f96 100644 --- a/crates/command_definitions/src/group.rs +++ b/crates/command_definitions/src/group.rs @@ -168,7 +168,8 @@ pub fn cmds() -> impl Iterator { .into_iter(); let system_groups_cmd = - once(command!(group, ("list", ["ls"]), search_param => "groups_self")).map(apply_list_opts); + once(command!(group, ("list", ["ls", "l"]), search_param => "groups_self")) + .map(apply_list_opts); system_groups_cmd .chain(group_new_cmd) From 3c59ad62bdfa5db4df6e031fd5d546bc759225a2 Mon Sep 17 00:00:00 2001 From: dusk Date: Thu, 27 Nov 2025 00:40:53 +0000 Subject: [PATCH 151/179] partial broken fix for optional parameters (mostly message and reproxy commands) --- PluralKit.Bot/CommandMeta/CommandTree.cs | 12 +++--- PluralKit.Bot/Commands/Message.cs | 19 ++++++---- crates/command_definitions/src/message.rs | 17 ++++----- crates/command_parser/src/lib.rs | 46 +++++++++++++++-------- crates/command_parser/src/token.rs | 14 ++----- crates/command_parser/src/tree.rs | 16 ++++++-- 6 files changed, 71 insertions(+), 53 deletions(-) diff --git a/PluralKit.Bot/CommandMeta/CommandTree.cs b/PluralKit.Bot/CommandMeta/CommandTree.cs index 0e66ef00..6816b885 100644 --- a/PluralKit.Bot/CommandMeta/CommandTree.cs +++ b/PluralKit.Bot/CommandMeta/CommandTree.cs @@ -281,13 +281,11 @@ public partial class CommandTree Commands.PermcheckChannel(var param, _) => ctx.Execute(PermCheck, m => m.PermCheckChannel(ctx, param.target)), Commands.PermcheckGuild(var param, _) => ctx.Execute(PermCheck, m => m.PermCheckGuild(ctx, param.target)), Commands.MessageProxyCheck(var param, _) => ctx.Execute(ProxyCheck, m => m.MessageProxyCheck(ctx, param.target)), - Commands.MessageInfo(var param, var flags) => ctx.Execute(Message, m => m.GetMessage(ctx, param.target.MessageId, flags.GetReplyFormat(), flags.delete, flags.author, flags.show_embed)), - Commands.MessageAuthor(var param, var flags) => ctx.Execute(Message, m => m.GetMessage(ctx, param.target.MessageId, flags.GetReplyFormat(), false, true, flags.show_embed)), - Commands.MessageDelete(var param, var flags) => ctx.Execute(Message, m => m.GetMessage(ctx, param.target.MessageId, flags.GetReplyFormat(), true, false, flags.show_embed)), - Commands.MessageEditSpecified(var param, var flags) => ctx.Execute(MessageEdit, m => m.EditMessage(ctx, param.target.MessageId, param.new_content, flags.regex, flags.no_space, flags.append, flags.prepend, flags.clear_embeds, flags.clear_attachments)), - Commands.MessageEdit(var param, var flags) => ctx.Execute(MessageEdit, m => m.EditMessage(ctx, null, param.new_content, flags.regex, flags.no_space, flags.append, flags.prepend, flags.clear_embeds, flags.clear_attachments)), - Commands.MessageReproxySpecified(var param, _) => ctx.Execute(MessageReproxy, m => m.ReproxyMessage(ctx, param.msg.MessageId, param.member)), - Commands.MessageReproxy(var param, _) => ctx.Execute(MessageReproxy, m => m.ReproxyMessage(ctx, null, param.member)), + Commands.MessageInfo(var param, var flags) => ctx.Execute(Message, m => m.GetMessage(ctx, param.target, flags.GetReplyFormat(), flags.delete, flags.author, flags.show_embed)), + Commands.MessageAuthor(var param, var flags) => ctx.Execute(Message, m => m.GetMessage(ctx, param.target, flags.GetReplyFormat(), false, true, flags.show_embed)), + Commands.MessageDelete(var param, var flags) => ctx.Execute(Message, m => m.GetMessage(ctx, param.target, flags.GetReplyFormat(), true, false, flags.show_embed)), + Commands.MessageEdit(var param, var flags) => ctx.Execute(MessageEdit, m => m.EditMessage(ctx, param.target, param.new_content, flags.regex, flags.no_space, flags.append, flags.prepend, flags.clear_embeds, flags.clear_attachments)), + Commands.MessageReproxy(var param, _) => ctx.Execute(MessageReproxy, m => m.ReproxyMessage(ctx, param.msg, param.member)), Commands.Import(var param, var flags) => ctx.Execute(Import, m => m.Import(ctx, param.url, flags.yes)), Commands.Export(_, _) => ctx.Execute(Export, m => m.Export(ctx)), Commands.ServerConfigShow => ctx.Execute(null, m => m.ShowConfig(ctx)), diff --git a/PluralKit.Bot/Commands/Message.cs b/PluralKit.Bot/Commands/Message.cs index 6284abee..b7ac474c 100644 --- a/PluralKit.Bot/Commands/Message.cs +++ b/PluralKit.Bot/Commands/Message.cs @@ -58,9 +58,9 @@ public class ProxiedMessage _redisService = redisService; } - public async Task ReproxyMessage(Context ctx, ulong? messageId, PKMember target) + public async Task ReproxyMessage(Context ctx, Message.Reference? messageRef, PKMember target) { - var (msg, systemId) = await GetMessageToEdit(ctx, messageId, ReproxyTimeout, true); + var (msg, systemId) = await GetMessageToEdit(ctx, messageRef?.MessageId, ReproxyTimeout, true); if (ctx.System.Id != systemId) throw new PKError("Can't reproxy a message sent by a different system."); @@ -91,9 +91,9 @@ public class ProxiedMessage } } - public async Task EditMessage(Context ctx, ulong? messageId, string newContent, bool useRegex, bool noSpace, bool append, bool prepend, bool clearEmbeds, bool clearAttachments) + public async Task EditMessage(Context ctx, Message.Reference? messageRef, string newContent, bool useRegex, bool noSpace, bool append, bool prepend, bool clearEmbeds, bool clearAttachments) { - var (msg, systemId) = await GetMessageToEdit(ctx, messageId, EditTimeout, false); + var (msg, systemId) = await GetMessageToEdit(ctx, messageRef?.MessageId, EditTimeout, false); if (ctx.System.Id != systemId) throw new PKError("Can't edit a message sent by a different system."); @@ -320,17 +320,20 @@ public class ProxiedMessage return lastMessage; } - public async Task GetMessage(Context ctx, ulong? messageId, ReplyFormat format, bool isDelete, bool author, bool showEmbed) + public async Task GetMessage(Context ctx, Message.Reference? messageRef, ReplyFormat format, bool isDelete, bool author, bool showEmbed) { - if (messageId == null) + if (ctx.Message.Type == Message.MessageType.Reply && ctx.Message.MessageReference?.MessageId != null) + messageRef = ctx.Message.MessageReference; + + if (messageRef == null || messageRef.MessageId == null) { throw new PKSyntaxError("You must pass a message ID or link."); } - var message = await ctx.Repository.GetFullMessage(messageId.Value); + var message = await ctx.Repository.GetFullMessage(messageRef.MessageId.Value); if (message == null) { - await GetCommandMessage(ctx, messageId.Value, isDelete, showEmbed); + await GetCommandMessage(ctx, messageRef.MessageId.Value, isDelete, showEmbed); return; } diff --git a/crates/command_definitions/src/message.rs b/crates/command_definitions/src/message.rs index 77427741..7030ddfb 100644 --- a/crates/command_definitions/src/message.rs +++ b/crates/command_definitions/src/message.rs @@ -1,7 +1,7 @@ use super::*; pub fn cmds() -> impl Iterator { - let message = tokens!(("message", ["msg", "messageinfo"]), MessageRef); + let message = tokens!(("message", ["msg", "messageinfo"]), Optional(MessageRef)); let author = ("author", ["sender", "a"]); let delete = ("delete", ["del", "d"]); @@ -20,19 +20,16 @@ pub fn cmds() -> impl Iterator { }; [ + apply_edit(command!(edit, Optional(MessageRef), new_content_param => "message_edit")), + command!(reproxy, Optional(("msg", MessageRef)), ("member", MemberRef) => "message_reproxy") + .help("Reproxies a message with a different member"), + command!(message, author => "message_author").help("Shows the author of a proxied message"), + command!(message, delete => "message_delete").help("Deletes a proxied message"), + apply_edit(command!(message, edit, new_content_param => "message_edit")), command!(message => "message_info") .flag(delete) .flag(author) .help("Shows information about a proxied message"), - command!(message, author => "message_author").help("Shows the author of a proxied message"), - command!(message, delete => "message_delete").help("Deletes a proxied message"), - apply_edit(command!(message, edit, new_content_param => "message_edit_specified")), - apply_edit(command!(edit, Skip(MessageRef), new_content_param => "message_edit_specified")), - apply_edit(command!(edit, new_content_param => "message_edit")), - command!(reproxy, ("member", MemberRef) => "message_reproxy") - .help("Reproxies a message with a different member"), - command!(reproxy, ("msg", MessageRef), ("member", MemberRef) => "message_reproxy_specified") - .help("Reproxies a message with a different member"), ] .into_iter() } diff --git a/crates/command_parser/src/lib.rs b/crates/command_parser/src/lib.rs index 8aba4d21..4a83489e 100644 --- a/crates/command_parser/src/lib.rs +++ b/crates/command_parser/src/lib.rs @@ -51,21 +51,31 @@ pub fn parse_command( let mut matched_tokens: Vec<(Tree, (Token, TokenMatchResult, usize))> = Vec::new(); let mut filtered_tokens: Vec = Vec::new(); loop { - // println!( - // "possible: {:?}", - // local_tree - // .possible_tokens() - // .filter(|t| filtered_tokens.contains(t)) - // .collect::>() - // ); - let next = next_token( - local_tree - .possible_tokens() - .filter(|t| !filtered_tokens.contains(t)), - &input, - current_pos, - ); - // println!("next: {:?}", next); + let mut possible_tokens = local_tree + .possible_tokens() + .filter(|t| !filtered_tokens.contains(t)) + // .filter(|t| { + // if !filtered_tokens.is_empty() { + // !matches!(t, Token::Parameter(param) if param.is_optional()) + // } else { + // true + // } + // }) + .collect::>(); + // sort so parameters come last + // we always want to test values first + // parameters that parse the remainder come last (otherwise they would always match) + possible_tokens.sort_by(|a, b| match (a, b) { + (Token::Parameter(param), _) if param.is_remainder() => std::cmp::Ordering::Greater, + (_, Token::Parameter(param)) if param.is_remainder() => std::cmp::Ordering::Less, + (Token::Parameter(_), Token::Parameter(_)) => std::cmp::Ordering::Equal, + (Token::Parameter(_), _) => std::cmp::Ordering::Greater, + (_, Token::Parameter(_)) => std::cmp::Ordering::Less, + _ => std::cmp::Ordering::Equal, + }); + println!("possible: {:?}", possible_tokens); + let next = next_token(possible_tokens.iter().cloned(), &input, current_pos); + println!("next: {:?}", next); match &next { Some((found_token, result, new_pos)) => { match &result { @@ -76,6 +86,11 @@ pub fn parse_command( )); } TokenMatchResult::ParameterMatchError { input: raw, msg } => { + if matches!(found_token, Token::Parameter(param) if param.is_skip()) + && possible_tokens.len() > 1 + { + continue; + } return Err(format!( "Parameter `{raw}` in command `{prefix}{input}` could not be parsed: {msg}." )); @@ -112,6 +127,7 @@ pub fn parse_command( .pop() .and_then(|m| matches!(m.1, (Token::Parameter(_), _, _)).then_some(m)) { + println!("redoing previous branch: {:?}", match_next.0); local_tree = match_tree; filtered_tokens.push(match_next.0); continue; diff --git a/crates/command_parser/src/token.rs b/crates/command_parser/src/token.rs index 99fc6fb3..67f7ddcd 100644 --- a/crates/command_parser/src/token.rs +++ b/crates/command_parser/src/token.rs @@ -76,16 +76,10 @@ impl Token { name: param.name().into(), value: matched, }, - Err(err) => { - if param.is_skip() { - return None; - } else { - TokenMatchResult::ParameterMatchError { - input: input.into(), - msg: err, - } - } - } + Err(err) => TokenMatchResult::ParameterMatchError { + input: input.into(), + msg: err, + }, }), // don't add a _ match here! } diff --git a/crates/command_parser/src/tree.rs b/crates/command_parser/src/tree.rs index 9b1466a3..ac5420e2 100644 --- a/crates/command_parser/src/tree.rs +++ b/crates/command_parser/src/tree.rs @@ -1,6 +1,6 @@ use ordermap::OrderMap; -use crate::{command::Command, token::Token}; +use crate::{command::Command, parameter::Skip, token::Token}; #[derive(Debug, Clone)] pub struct TreeBranch { @@ -21,7 +21,17 @@ impl TreeBranch { pub fn register_command(&mut self, command: Command) { let mut current_branch = self; // iterate over tokens in command - for token in command.tokens.clone() { + for (index, token) in command.tokens.clone().into_iter().enumerate() { + // if the token is an optional parameter, register rest of the tokens to a separate branch + // this allows optional parameters to work if they are not the last token + if matches!(token, Token::Parameter(ref param) if param.is_optional()) + && index < command.tokens.len() - 1 + { + current_branch.register_command(Command { + tokens: command.tokens[index + 1..].to_vec(), + ..command.clone() + }); + } // recursively get or create a sub-branch for each token current_branch = current_branch .branches @@ -36,7 +46,7 @@ impl TreeBranch { self.current_command.clone() } - pub fn possible_tokens(&self) -> impl Iterator { + pub fn possible_tokens(&self) -> impl Iterator + Clone { self.branches.keys() } From 2020939ac0ed0b7a96e864a3099ccd995f9fc6ba Mon Sep 17 00:00:00 2001 From: dusk Date: Thu, 27 Nov 2025 00:43:15 +0000 Subject: [PATCH 152/179] aliases for displayname flags --- crates/command_definitions/src/utils.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/command_definitions/src/utils.rs b/crates/command_definitions/src/utils.rs index 1c589ef0..2c847ac7 100644 --- a/crates/command_definitions/src/utils.rs +++ b/crates/command_definitions/src/utils.rs @@ -19,7 +19,7 @@ pub fn get_list_flags() -> [Flag; 22] { )), // Sort properties Flag::from(("by-name", ["bn"])), - Flag::from(("by-display-name", ["bdn"])), + Flag::from(("by-display-name", ["by-displayname", "bdn"])), Flag::from(("by-id", ["bid"])), Flag::from(("by-message-count", ["bmc"])), Flag::from(("by-created", ["bc", "bcd"])), @@ -48,7 +48,7 @@ pub fn get_list_flags() -> [Flag; 22] { ["with-image", "with-icon", "wa", "wi", "ia", "ii", "img"], )), Flag::from(("with-pronouns", ["wp", "wprns"])), - Flag::from(("with-display-name", ["wdn"])), + Flag::from(("with-display-name", ["with-displayname", "wdn"])), Flag::from(("with-birthday", ["wbd", "wb"])), ] } From 00d3840fd16f3242423c9b8fc0fcbb6637af4d31 Mon Sep 17 00:00:00 2001 From: dusk Date: Thu, 27 Nov 2025 01:28:10 +0000 Subject: [PATCH 153/179] fix parsing of optional parameters --- crates/command_parser/src/lib.rs | 34 +++++++++++++++++++++----------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/crates/command_parser/src/lib.rs b/crates/command_parser/src/lib.rs index 4a83489e..18de3446 100644 --- a/crates/command_parser/src/lib.rs +++ b/crates/command_parser/src/lib.rs @@ -48,19 +48,14 @@ pub fn parse_command( let mut params: HashMap = HashMap::new(); let mut raw_flags: Vec<(usize, MatchedFlag)> = Vec::new(); - let mut matched_tokens: Vec<(Tree, (Token, TokenMatchResult, usize))> = Vec::new(); + let mut matched_tokens: Vec<(Tree, (Token, TokenMatchResult, usize), usize)> = Vec::new(); let mut filtered_tokens: Vec = Vec::new(); + let mut last_optional_param_error: Option<(SmolStr, SmolStr)> = None; + loop { let mut possible_tokens = local_tree .possible_tokens() .filter(|t| !filtered_tokens.contains(t)) - // .filter(|t| { - // if !filtered_tokens.is_empty() { - // !matches!(t, Token::Parameter(param) if param.is_optional()) - // } else { - // true - // } - // }) .collect::>(); // sort so parameters come last // we always want to test values first @@ -86,17 +81,26 @@ pub fn parse_command( )); } TokenMatchResult::ParameterMatchError { input: raw, msg } => { - if matches!(found_token, Token::Parameter(param) if param.is_skip()) + // we can try other branches if the parameter is optional or skip-on-error parameter + if matches!(found_token, Token::Parameter(param) if param.is_optional() || param.is_skip()) && possible_tokens.len() > 1 { + // save error for later, will be used if no other tokens match + last_optional_param_error = Some((raw.clone(), msg.clone())); + // try the other branches first + filtered_tokens.push(found_token.clone()); continue; } + return Err(format!( "Parameter `{raw}` in command `{prefix}{input}` could not be parsed: {msg}." )); } // don't use a catch-all here, we want to make sure compiler errors when new errors are added - TokenMatchResult::MatchedParameter { .. } | TokenMatchResult::MatchedValue => {} + TokenMatchResult::MatchedParameter { .. } | TokenMatchResult::MatchedValue => { + // clear the error since we successfully matched forward, we dont need it anymore + last_optional_param_error = None; + } } // add parameter if any @@ -109,6 +113,7 @@ pub fn parse_command( matched_tokens.push(( local_tree.clone(), (found_token.clone(), result.clone(), *new_pos), + current_pos, )); filtered_tokens.clear(); // new branch, new tokens local_tree = next_tree.clone(); @@ -123,16 +128,23 @@ pub fn parse_command( None => { // redo the previous branches if we didnt match on a parameter // this is a bit of a hack, but its necessary for making parameters on the same depth work - if let Some((match_tree, match_next)) = matched_tokens + if let Some((match_tree, match_next, old_pos)) = matched_tokens .pop() .and_then(|m| matches!(m.1, (Token::Parameter(_), _, _)).then_some(m)) { println!("redoing previous branch: {:?}", match_next.0); local_tree = match_tree; + current_pos = old_pos; // reset position to previous branch's start filtered_tokens.push(match_next.0); continue; } + if let Some((raw, msg)) = last_optional_param_error { + return Err(format!( + "Parameter `{raw}` in command `{prefix}{input}` could not be parsed: {msg}." + )); + } + let mut error = format!("Unknown command `{prefix}{input}`."); let possible_commands = From 32d481c6b9e210ffbb3d5bdb4d62d4c3cd300533 Mon Sep 17 00:00:00 2001 From: dusk Date: Thu, 27 Nov 2025 02:00:13 +0000 Subject: [PATCH 154/179] make use of the new optional parsing and remove the _self commands --- PluralKit.Bot/CommandMeta/CommandTree.cs | 96 +++++----- crates/command_definitions/src/system.rs | 213 ++++++++++------------- crates/command_parser/src/parameter.rs | 1 + 3 files changed, 136 insertions(+), 174 deletions(-) diff --git a/PluralKit.Bot/CommandMeta/CommandTree.cs b/PluralKit.Bot/CommandMeta/CommandTree.cs index 6816b885..4632fca7 100644 --- a/PluralKit.Bot/CommandMeta/CommandTree.cs +++ b/PluralKit.Bot/CommandMeta/CommandTree.cs @@ -122,75 +122,72 @@ public partial class CommandTree Commands.FunRool => ctx.Execute(null, m => m.Rool(ctx)), Commands.Amogus => ctx.Execute(null, m => m.Sus(ctx)), Commands.FunError => ctx.Execute(null, m => m.Error(ctx)), - Commands.SystemInfo(var param, var flags) => ctx.Execute(SystemInfo, m => m.Query(ctx, param.target, flags.all, flags.@public, flags.@private)), - Commands.SystemInfoSelf(_, var flags) => ctx.Execute(SystemInfo, m => m.Query(ctx, ctx.System, flags.all, flags.@public, flags.@private)), - Commands.SystemNew(var param, _) => ctx.Execute(SystemNew, m => m.New(ctx, null)), - Commands.SystemNewName(var param, _) => ctx.Execute(SystemNew, m => m.New(ctx, param.name)), - Commands.SystemShowNameSelf(_, var flags) => ctx.Execute(SystemRename, m => m.ShowName(ctx, ctx.System, flags.GetReplyFormat())), - Commands.SystemShowName(var param, var flags) => ctx.Execute(SystemRename, m => m.ShowName(ctx, param.target, flags.GetReplyFormat())), + Commands.SystemInfo(var param, var flags) => ctx.Execute(SystemInfo, m => m.Query(ctx, param.target ?? ctx.System, flags.all, flags.@public, flags.@private)), + Commands.SystemNew(var param, _) => ctx.Execute(SystemNew, m => m.New(ctx, param.name)), + Commands.SystemShowName(var param, var flags) => ctx.Execute(SystemRename, m => m.ShowName(ctx, param.target ?? ctx.System, flags.GetReplyFormat())), Commands.SystemRename(var param, _) => ctx.Execute(SystemRename, m => m.Rename(ctx, ctx.System, param.name)), Commands.SystemClearName(var param, var flags) => ctx.Execute(SystemRename, m => m.ClearName(ctx, ctx.System, flags.yes)), - Commands.SystemShowServerNameSelf(_, var flags) => ctx.Execute(SystemServerName, m => m.ShowServerName(ctx, ctx.System, flags.GetReplyFormat())), - Commands.SystemShowServerName(var param, var flags) => ctx.Execute(SystemServerName, m => m.ShowServerName(ctx, param.target, flags.GetReplyFormat())), + Commands.SystemShowServerName(var param, var flags) => ctx.Execute(SystemServerName, m => m.ShowServerName(ctx, param.target ?? ctx.System, flags.GetReplyFormat())), Commands.SystemClearServerName(var param, var flags) => ctx.Execute(SystemServerName, m => m.ClearServerName(ctx, ctx.System, flags.yes)), Commands.SystemRenameServerName(var param, _) => ctx.Execute(SystemServerName, m => m.RenameServerName(ctx, ctx.System, param.name)), - Commands.SystemShowDescriptionSelf(_, var flags) => ctx.Execute(SystemDesc, m => m.ShowDescription(ctx, ctx.System, flags.GetReplyFormat())), - Commands.SystemShowDescription(var param, var flags) => ctx.Execute(SystemDesc, m => m.ShowDescription(ctx, param.target, flags.GetReplyFormat())), + Commands.SystemShowDescription(var param, var flags) => ctx.Execute(SystemDesc, m => m.ShowDescription(ctx, param.target ?? ctx.System, flags.GetReplyFormat())), Commands.SystemClearDescription(var param, var flags) => ctx.Execute(SystemDesc, m => m.ClearDescription(ctx, ctx.System, flags.yes)), Commands.SystemChangeDescription(var param, _) => ctx.Execute(SystemDesc, m => m.ChangeDescription(ctx, ctx.System, param.description)), - Commands.SystemShowColorSelf(_, var flags) => ctx.Execute(SystemColor, m => m.ShowColor(ctx, ctx.System, flags.GetReplyFormat())), - Commands.SystemShowColor(var param, var flags) => ctx.Execute(SystemColor, m => m.ShowColor(ctx, param.target, flags.GetReplyFormat())), + Commands.SystemShowColor(var param, var flags) => ctx.Execute(SystemColor, m => m.ShowColor(ctx, param.target ?? ctx.System, flags.GetReplyFormat())), Commands.SystemClearColor(var param, var flags) => ctx.Execute(SystemColor, m => m.ClearColor(ctx, ctx.System, flags.yes)), Commands.SystemChangeColor(var param, _) => ctx.Execute(SystemColor, m => m.ChangeColor(ctx, ctx.System, param.color)), - Commands.SystemShowTagSelf(_, var flags) => ctx.Execute(SystemTag, m => m.ShowTag(ctx, ctx.System, flags.GetReplyFormat())), - Commands.SystemShowTag(var param, var flags) => ctx.Execute(SystemTag, m => m.ShowTag(ctx, param.target, flags.GetReplyFormat())), + Commands.SystemShowTag(var param, var flags) => ctx.Execute(SystemTag, m => m.ShowTag(ctx, param.target ?? ctx.System, flags.GetReplyFormat())), Commands.SystemClearTag(var param, var flags) => ctx.Execute(SystemTag, m => m.ClearTag(ctx, ctx.System, flags.yes)), Commands.SystemChangeTag(var param, _) => ctx.Execute(SystemTag, m => m.ChangeTag(ctx, ctx.System, param.tag)), - Commands.SystemShowServerTagSelf(_, var flags) => ctx.Execute(SystemServerTag, m => m.ShowServerTag(ctx, ctx.System, flags.GetReplyFormat())), - Commands.SystemShowServerTag(var param, var flags) => ctx.Execute(SystemServerTag, m => m.ShowServerTag(ctx, param.target, flags.GetReplyFormat())), + Commands.SystemShowServerTag(var param, var flags) => ctx.Execute(SystemServerTag, m => m.ShowServerTag(ctx, param.target ?? ctx.System, flags.GetReplyFormat())), Commands.SystemClearServerTag(var param, var flags) => ctx.Execute(SystemServerTag, m => m.ClearServerTag(ctx, ctx.System, flags.yes)), Commands.SystemChangeServerTag(var param, _) => ctx.Execute(SystemServerTag, m => m.ChangeServerTag(ctx, ctx.System, param.tag)), - Commands.SystemShowPronounsSelf(_, var flags) => ctx.Execute(SystemPronouns, m => m.ShowPronouns(ctx, ctx.System, flags.GetReplyFormat())), - Commands.SystemShowPronouns(var param, var flags) => ctx.Execute(SystemPronouns, m => m.ShowPronouns(ctx, param.target, flags.GetReplyFormat())), + Commands.SystemShowPronouns(var param, var flags) => ctx.Execute(SystemPronouns, m => m.ShowPronouns(ctx, param.target ?? ctx.System, flags.GetReplyFormat())), Commands.SystemClearPronouns(var param, var flags) => ctx.Execute(SystemPronouns, m => m.ClearPronouns(ctx, ctx.System, flags.yes)), Commands.SystemChangePronouns(var param, _) => ctx.Execute(SystemPronouns, m => m.ChangePronouns(ctx, ctx.System, param.pronouns)), - Commands.SystemShowAvatarSelf(_, var flags) => ((Func)(() => + Commands.SystemShowAvatar(var param, var flags) => ((Func)(() => { - // we want to change avatar if an attached image is passed - // we can't have a separate parsed command for this since the parser can't be aware of any attachments - var attachedImage = ctx.ExtractImageFromAttachment(); - if (attachedImage is { } image) - return ctx.Execute(SystemAvatar, m => m.ChangeAvatar(ctx, ctx.System, image)); + if (param.target == null) + { + // we want to change avatar if an attached image is passed + // we can't have a separate parsed command for this since the parser can't be aware of any attachments + var attachedImage = ctx.ExtractImageFromAttachment(); + if (attachedImage is { } image) + return ctx.Execute(SystemAvatar, m => m.ChangeAvatar(ctx, ctx.System, image)); + } // if no attachment show the avatar like intended - return ctx.Execute(SystemAvatar, m => m.ShowAvatar(ctx, ctx.System, flags.GetReplyFormat())); + return ctx.Execute(SystemAvatar, m => m.ShowAvatar(ctx, param.target ?? ctx.System, flags.GetReplyFormat())); }))(), - Commands.SystemShowAvatar(var param, var flags) => ctx.Execute(SystemAvatar, m => m.ShowAvatar(ctx, param.target, flags.GetReplyFormat())), Commands.SystemClearAvatar(var param, var flags) => ctx.Execute(SystemAvatar, m => m.ClearAvatar(ctx, ctx.System, flags.yes)), Commands.SystemChangeAvatar(var param, _) => ctx.Execute(SystemAvatar, m => m.ChangeAvatar(ctx, ctx.System, param.avatar)), - Commands.SystemShowServerAvatarSelf(_, var flags) => ((Func)(() => + Commands.SystemShowServerAvatar(var param, var flags) => ((Func)(() => { - // we want to change avatar if an attached image is passed - // we can't have a separate parsed command for this since the parser can't be aware of any attachments - var attachedImage = ctx.ExtractImageFromAttachment(); - if (attachedImage is { } image) - return ctx.Execute(SystemServerAvatar, m => m.ChangeServerAvatar(ctx, ctx.System, image)); + if (param.target == null) + { + // we want to change avatar if an attached image is passed + // we can't have a separate parsed command for this since the parser can't be aware of any attachments + var attachedImage = ctx.ExtractImageFromAttachment(); + if (attachedImage is { } image) + return ctx.Execute(SystemServerAvatar, m => m.ChangeServerAvatar(ctx, ctx.System, image)); + } // if no attachment show the avatar like intended - return ctx.Execute(SystemServerAvatar, m => m.ShowServerAvatar(ctx, ctx.System, flags.GetReplyFormat())); + return ctx.Execute(SystemServerAvatar, m => m.ShowServerAvatar(ctx, param.target ?? ctx.System, flags.GetReplyFormat())); }))(), - Commands.SystemShowServerAvatar(var param, var flags) => ctx.Execute(SystemServerAvatar, m => m.ShowServerAvatar(ctx, param.target, flags.GetReplyFormat())), Commands.SystemClearServerAvatar(var param, var flags) => ctx.Execute(SystemServerAvatar, m => m.ClearServerAvatar(ctx, ctx.System, flags.yes)), Commands.SystemChangeServerAvatar(var param, _) => ctx.Execute(SystemServerAvatar, m => m.ChangeServerAvatar(ctx, ctx.System, param.avatar)), - Commands.SystemShowBannerSelf(_, var flags) => ((Func)(() => + Commands.SystemShowBanner(var param, var flags) => ((Func)(() => { - // we want to change banner if an attached image is passed - // we can't have a separate parsed command for this since the parser can't be aware of any attachments - var attachedImage = ctx.ExtractImageFromAttachment(); - if (attachedImage is { } image) - return ctx.Execute(SystemBannerImage, m => m.ChangeBannerImage(ctx, ctx.System, image)); + if (param.target == null) + { + // we want to change banner if an attached image is passed + // we can't have a separate parsed command for this since the parser can't be aware of any attachments + var attachedImage = ctx.ExtractImageFromAttachment(); + if (attachedImage is { } image) + return ctx.Execute(SystemBannerImage, m => m.ChangeBannerImage(ctx, ctx.System, image)); + } // if no attachment show the banner like intended - return ctx.Execute(SystemBannerImage, m => m.ShowBannerImage(ctx, ctx.System, flags.GetReplyFormat())); + return ctx.Execute(SystemBannerImage, m => m.ShowBannerImage(ctx, param.target ?? ctx.System, flags.GetReplyFormat())); }))(), - Commands.SystemShowBanner(var param, var flags) => ctx.Execute(SystemBannerImage, m => m.ShowBannerImage(ctx, param.target, flags.GetReplyFormat())), Commands.SystemClearBanner(var param, var flags) => ctx.Execute(SystemBannerImage, m => m.ClearBannerImage(ctx, ctx.System, flags.yes)), Commands.SystemChangeBanner(var param, _) => ctx.Execute(SystemBannerImage, m => m.ChangeBannerImage(ctx, ctx.System, param.banner)), Commands.SystemDelete(_, var flags) => ctx.Execute(SystemDelete, m => m.Delete(ctx, ctx.System, flags.no_export)), @@ -208,14 +205,10 @@ public partial class CommandTree Commands.SwitchEditOut(_, var flags) => ctx.Execute(SwitchEditOut, m => m.SwitchEditOut(ctx, flags.yes)), Commands.SwitchDelete(var param, var flags) => ctx.Execute(SwitchDelete, m => m.SwitchDelete(ctx, flags.all)), Commands.SwitchCopy(var param, var flags) => ctx.Execute(SwitchCopy, m => m.SwitchEdit(ctx, param.targets, true, flags.first, flags.remove, flags.append, flags.prepend)), - Commands.SystemFronter(var param, var flags) => ctx.Execute(SystemFronter, m => m.Fronter(ctx, param.target)), - Commands.SystemFronterHistory(var param, var flags) => ctx.Execute(SystemFrontHistory, m => m.FrontHistory(ctx, param.target, flags.clear)), - Commands.SystemFronterPercent(var param, var flags) => ctx.Execute(SystemFrontPercent, m => m.FrontPercent(ctx, param.target, flags.duration, flags.fronters_only, flags.flat)), - Commands.SystemFronterSelf(_, var flags) => ctx.Execute(SystemFronter, m => m.Fronter(ctx, ctx.System)), - Commands.SystemFronterHistorySelf(_, var flags) => ctx.Execute(SystemFrontHistory, m => m.FrontHistory(ctx, ctx.System, flags.clear)), - Commands.SystemFronterPercentSelf(_, var flags) => ctx.Execute(SystemFrontPercent, m => m.FrontPercent(ctx, ctx.System, flags.duration, flags.fronters_only, flags.flat)), - Commands.SystemDisplayId(var param, _) => ctx.Execute(SystemId, m => m.DisplayId(ctx, param.target)), - Commands.SystemDisplayIdSelf => ctx.Execute(SystemId, m => m.DisplayId(ctx, ctx.System)), + Commands.SystemFronter(var param, var flags) => ctx.Execute(SystemFronter, m => m.Fronter(ctx, param.target ?? ctx.System)), + Commands.SystemFronterHistory(var param, var flags) => ctx.Execute(SystemFrontHistory, m => m.FrontHistory(ctx, param.target ?? ctx.System, flags.clear)), + Commands.SystemFronterPercent(var param, var flags) => ctx.Execute(SystemFrontPercent, m => m.FrontPercent(ctx, param.target ?? ctx.System, flags.duration, flags.fronters_only, flags.flat)), + Commands.SystemDisplayId(var param, _) => ctx.Execute(SystemId, m => m.DisplayId(ctx, param.target ?? ctx.System)), Commands.SystemWebhookShow => ctx.Execute(null, m => m.GetSystemWebhook(ctx)), Commands.SystemWebhookClear(_, var flags) => ctx.Execute(null, m => m.ClearSystemWebhook(ctx, flags.yes)), Commands.SystemWebhookSet(var param, _) => ctx.Execute(null, m => m.SetSystemWebhook(ctx, param.url)), @@ -238,8 +231,7 @@ public partial class CommandTree Commands.SystemMembers(var param, var flags) => ctx.Execute(SystemList, m => m.MemberList(ctx, param.target, param.query, flags)), Commands.MemberGroups(var param, var flags) => ctx.Execute(MemberGroups, m => m.ListMemberGroups(ctx, param.target, param.query, flags, flags.all)), Commands.GroupMembers(var param, var flags) => ctx.Execute(GroupMemberList, m => m.ListGroupMembers(ctx, param.target, param.query, flags)), - Commands.SystemGroups(var param, var flags) => ctx.Execute(GroupList, g => g.ListSystemGroups(ctx, param.target, param.query, flags, flags.all)), - Commands.SystemGroupsSelf(var param, var flags) => ctx.Execute(GroupList, g => g.ListSystemGroups(ctx, ctx.System, param.query, flags, flags.all)), + Commands.SystemGroups(var param, var flags) => ctx.Execute(GroupList, g => g.ListSystemGroups(ctx, param.target ?? ctx.System, param.query, flags, flags.all)), Commands.GroupsSelf(var param, var flags) => ctx.Execute(GroupList, g => g.ListSystemGroups(ctx, ctx.System, param.query, flags, flags.all)), Commands.GroupNew(var param, _) => ctx.Execute(GroupNew, g => g.CreateGroup(ctx, param.name)), Commands.GroupInfo(var param, var flags) => ctx.Execute(GroupInfo, g => g.ShowGroupCard(ctx, param.target, flags.show_embed, flags.all)), diff --git a/crates/command_definitions/src/system.rs b/crates/command_definitions/src/system.rs index 905f81cc..7860cad0 100644 --- a/crates/command_definitions/src/system.rs +++ b/crates/command_definitions/src/system.rs @@ -22,13 +22,11 @@ pub fn edit() -> impl Iterator { let system = system(); let system_target = targeted(); - let system_new = tokens!(system, ("new", ["n"])); - let system_new_cmd = [ - command!(system_new => "system_new").help("Creates a new system"), - command!(system_new, Remainder(("name", OpaqueString)) => "system_new_name") - .help("Creates a new system (using the provided name)"), - ] - .into_iter(); + let system_new_cmd = + once( + command!(system, ("new", ["n"]), Optional(Remainder(("name", OpaqueString))) => "system_new") + .help("Creates a new system") + ); let system_webhook = tokens!(system, ("webhook", ["hook"])); let system_webhook_cmd = [ @@ -46,24 +44,22 @@ pub fn edit() -> impl Iterator { .flag(("private", ["priv"])) .flag(ALL) }; - let system_info_cmd_self = - once(command!(system => "system_info_self").help("Shows information about your system")) - .map(add_info_flags); let system_info_cmd = [ - command!(system_target => "system_info").help("Shows information about your system"), - command!(system_target, ("info", ["show", "view"]) => "system_info") + command!(system, Optional(SystemRef) => "system_info") + .help("Shows information about your system"), + command!(system, Optional(SystemRef), ("info", ["show", "view"]) => "system_info") .help("Shows information about your system"), ] .into_iter() .map(add_info_flags); - let system_name = tokens!(system_target, "name"); - let system_name_cmd = - once(command!(system_name => "system_show_name").help("Shows the systems name")); - - let system_name_self = tokens!(system, "name"); + let name = "name"; + let system_name_cmd = once( + command!(system, Optional(SystemRef), name => "system_show_name") + .help("Shows the systems name"), + ); + let system_name_self = tokens!(system, name); let system_name_self_cmd = [ - command!(system_name_self => "system_show_name_self").help("Shows your system's name"), command!(system_name_self, CLEAR => "system_clear_name") .flag(YES) .help("Clears your system's name"), @@ -72,16 +68,13 @@ pub fn edit() -> impl Iterator { ] .into_iter(); - let system_server_name = tokens!(system_target, ("servername", ["sn", "guildname"])); + let server_name = ("servername", ["sn", "guildname"]); let system_server_name_cmd = once( - command!(system_server_name => "system_show_server_name") + command!(system, Optional(SystemRef), server_name => "system_show_server_name") .help("Shows the system's server name"), ); - - let system_server_name_self = tokens!(system, ("servername", ["sn", "guildname"])); + let system_server_name_self = tokens!(system, server_name); let system_server_name_self_cmd = [ - command!(system_server_name_self => "system_show_server_name_self") - .help("Shows your system's server name"), command!(system_server_name_self, CLEAR => "system_clear_server_name") .flag(YES) .help("Clears your system's server name"), @@ -90,15 +83,13 @@ pub fn edit() -> impl Iterator { ] .into_iter(); - let system_description = tokens!(system_target, ("description", ["desc", "d"])); + let description = ("description", ["desc", "d"]); let system_description_cmd = once( - command!(system_description => "system_show_description") + command!(system, Optional(SystemRef), description => "system_show_description") .help("Shows the system's description"), ); - - let system_description_self = tokens!(system, ("description", ["desc", "d"])); + let system_description_self = tokens!(system, description); let system_description_self_cmd = [ - command!(system_description_self => "system_show_description_self").help("Shows your system's description"), command!(system_description_self, CLEAR => "system_clear_description") .flag(YES) .help("Clears your system's description"), @@ -107,13 +98,13 @@ pub fn edit() -> impl Iterator { ] .into_iter(); - let system_color = tokens!(system_target, ("color", ["colour"])); - let system_color_cmd = - once(command!(system_color => "system_show_color").help("Shows the system's color")); - - let system_color_self = tokens!(system, ("color", ["colour"])); + let color = ("color", ["colour"]); + let system_color_cmd = once( + command!(system, Optional(SystemRef), color => "system_show_color") + .help("Shows the system's color"), + ); + let system_color_self = tokens!(system, color); let system_color_self_cmd = [ - command!(system_color_self => "system_show_color_self").help("Shows your system's color"), command!(system_color_self, CLEAR => "system_clear_color") .flag(YES) .help("Clears your system's color"), @@ -122,13 +113,13 @@ pub fn edit() -> impl Iterator { ] .into_iter(); - let system_tag = tokens!(system_target, ("tag", ["suffix"])); - let system_tag_cmd = - once(command!(system_tag => "system_show_tag").help("Shows the system's tag")); - - let system_tag_self = tokens!(system, ("tag", ["suffix"])); + let tag = ("tag", ["suffix"]); + let system_tag_cmd = once( + command!(system, Optional(SystemRef), tag => "system_show_tag") + .help("Shows the system's tag"), + ); + let system_tag_self = tokens!(system, tag); let system_tag_self_cmd = [ - command!(system_tag_self => "system_show_tag_self").help("Shows your system's tag"), command!(system_tag_self, CLEAR => "system_clear_tag") .flag(YES) .help("Clears your system's tag"), @@ -137,16 +128,13 @@ pub fn edit() -> impl Iterator { ] .into_iter(); - let system_server_tag = tokens!(system_target, ("servertag", ["st", "guildtag"])); + let servertag = ("servertag", ["st", "guildtag"]); let system_server_tag_cmd = once( - command!(system_server_tag => "system_show_server_tag") + command!(system, Optional(SystemRef) => "system_show_server_tag") .help("Shows the system's server tag"), ); - - let system_server_tag_self = tokens!(system, ("servertag", ["st", "guildtag"])); + let system_server_tag_self = tokens!(system, servertag); let system_server_tag_self_cmd = [ - command!(system_server_tag_self => "system_show_server_tag_self") - .help("Shows your system's server tag"), command!(system_server_tag_self, CLEAR => "system_clear_server_tag") .flag(YES) .help("Clears your system's server tag"), @@ -155,15 +143,13 @@ pub fn edit() -> impl Iterator { ] .into_iter(); - let system_pronouns = tokens!(system_target, ("pronouns", ["prns"])); + let pronouns = ("pronouns", ["prns"]); let system_pronouns_cmd = once( - command!(system_pronouns => "system_show_pronouns").help("Shows the system's pronouns"), + command!(system, Optional(SystemRef), pronouns => "system_show_pronouns") + .help("Shows the system's pronouns"), ); - - let system_pronouns_self = tokens!(system, ("pronouns", ["prns"])); + let system_pronouns_self = tokens!(system, pronouns); let system_pronouns_self_cmd = [ - command!(system_pronouns_self => "system_show_pronouns_self") - .help("Shows your system's pronouns"), command!(system_pronouns_self, CLEAR => "system_clear_pronouns") .flag(YES) .help("Clears your system's pronouns"), @@ -172,14 +158,13 @@ pub fn edit() -> impl Iterator { ] .into_iter(); - let system_avatar = tokens!(system_target, ("avatar", ["pfp"])); - let system_avatar_cmd = - once(command!(system_avatar => "system_show_avatar").help("Shows the system's avatar")); - - let system_avatar_self = tokens!(system, ("avatar", ["pfp"])); + let avatar = ("avatar", ["pfp"]); + let system_avatar_cmd = once( + command!(system, Optional(SystemRef), avatar => "system_show_avatar") + .help("Shows the system's avatar"), + ); + let system_avatar_self = tokens!(system, avatar); let system_avatar_self_cmd = [ - command!(system_avatar_self => "system_show_avatar_self") - .help("Shows your system's avatar"), command!(system_avatar_self, CLEAR => "system_clear_avatar") .flag(YES) .help("Clears your system's avatar"), @@ -188,16 +173,14 @@ pub fn edit() -> impl Iterator { ] .into_iter(); - let system_server_avatar = tokens!(system_target, ("serveravatar", ["spfp"])); + let serveravatar = ("serveravatar", ["spfp"]); + let system_server_avatar = tokens!(system_target, serveravatar); let system_server_avatar_cmd = once( - command!(system_server_avatar => "system_show_server_avatar") + command!(system, Optional(SystemRef), serveravatar => "system_show_server_avatar") .help("Shows the system's server avatar"), ); - - let system_server_avatar_self = tokens!(system, ("serveravatar", ["spfp"])); + let system_server_avatar_self = tokens!(system, serveravatar); let system_server_avatar_self_cmd = [ - command!(system_server_avatar_self => "system_show_server_avatar_self") - .help("Shows your system's server avatar"), command!(system_server_avatar_self, CLEAR => "system_clear_server_avatar") .flag(YES) .help("Clears your system's server avatar"), @@ -206,14 +189,13 @@ pub fn edit() -> impl Iterator { ] .into_iter(); - let system_banner = tokens!(system_target, ("banner", ["cover"])); - let system_banner_cmd = - once(command!(system_banner => "system_show_banner").help("Shows the system's banner")); - - let system_banner_self = tokens!(system, ("banner", ["cover"])); + let banner = ("banner", ["cover"]); + let system_banner_cmd = once( + command!(system, Optional(SystemRef), banner => "system_show_banner") + .help("Shows the system's banner"), + ); + let system_banner_self = tokens!(system, banner); let system_banner_self_cmd = [ - command!(system_banner_self => "system_show_banner_self") - .help("Shows your system's banner"), command!(system_banner_self, CLEAR => "system_clear_banner") .flag(YES) .help("Clears your system's banner"), @@ -222,12 +204,6 @@ pub fn edit() -> impl Iterator { ] .into_iter(); - let system_delete = once( - command!(system, ("delete", ["erase", "remove", "yeet"]) => "system_delete") - .flag(("no-export", ["ne"])) - .help("Deletes the system"), - ); - let system_proxy = tokens!(system, "proxy"); let system_proxy_cmd = [ command!(system_proxy => "system_show_proxy_current") @@ -245,38 +221,28 @@ pub fn edit() -> impl Iterator { let system_privacy_cmd = [ command!(system_privacy => "system_show_privacy") .help("Shows your system's privacy settings"), - command!(system_privacy, ALL, ("level", PrivacyLevel) => "system_change_privacy_all") + command!(system_privacy, ALL, ("level", PrivacyLevel) => "system_change_privacy_all") .help("Changes all privacy settings for your system"), command!(system_privacy, ("privacy", SystemPrivacyTarget), ("level", PrivacyLevel) => "system_change_privacy") .help("Changes a specific privacy setting for your system"), ].into_iter(); let front = ("front", ["fronter", "fronters", "f"]); - let make_system_front_cmd = |prefix: TokensIterator, suffix: &str| { - let make_front_history = |subcmd: TokensIterator| { - command!(prefix, subcmd => format!("system_fronter_history{}", suffix)).flag(CLEAR) - }; - let make_front_percent = |subcmd: TokensIterator| { - command!(prefix, subcmd => format!("system_fronter_percent{}", suffix)) - .flag(("duration", OpaqueString)) - .flag(("fronters-only", ["fo"])) - .flag("flat") - }; - [ - command!(prefix, front => format!("system_fronter{}", suffix)), - make_front_history(tokens!(front, ("history", ["h"]))), - make_front_history(tokens!(("fronthistory", ["fh"]))), - make_front_percent(tokens!(front, ("percent", ["p", "%"]))), - make_front_percent(tokens!(("frontpercent", ["fp"]))), - ] - .into_iter() + let make_front_history = |subcmd: TokensIterator| { + command!(system, Optional(SystemRef), subcmd => "system_fronter_history").flag(CLEAR) }; - let system_front_cmd = make_system_front_cmd(tokens!(system_target), ""); - let system_front_self_cmd = make_system_front_cmd(tokens!(system), "_self"); - - let system_link = [ - command!("link", ("account", UserRef) => "system_link"), - command!("unlink", ("account", OpaqueString) => "system_unlink").flag(YES), + let make_front_percent = |subcmd: TokensIterator| { + command!(system, Optional(SystemRef), subcmd => "system_fronter_percent") + .flag(("duration", OpaqueString)) + .flag(("fronters-only", ["fo"])) + .flag("flat") + }; + let system_front_cmd = [ + command!(system, Optional(SystemRef), front => "system_fronter"), + make_front_history(tokens!(front, ("history", ["h"]))), + make_front_history(tokens!(("fronthistory", ["fh"]))), + make_front_percent(tokens!(front, ("percent", ["p", "%"]))), + make_front_percent(tokens!(("frontpercent", ["fp"]))), ] .into_iter(); @@ -285,25 +251,31 @@ pub fn edit() -> impl Iterator { let members_subcmd = tokens!(("members", ["ls", "list"]), search_param); let system_members_cmd = - once(command!(system_target, members_subcmd => "system_members")).map(apply_list_opts); - let system_members_self_cmd = [ - command!(system, members_subcmd => "system_members_self"), - command!(members_subcmd => "system_members_self"), - ] - .into_iter() - .map(apply_list_opts); + once(command!(system, Optional(SystemRef), members_subcmd => "system_members")) + .map(apply_list_opts); + let system_members_self_cmd = + once(command!(members_subcmd => "system_members_self")).map(apply_list_opts); - let groups_subcmd = tokens!("groups", search_param); let system_groups_cmd = - once(command!(system_target, groups_subcmd => "system_groups")).map(apply_list_opts); - let system_group_self_cmd = - once(command!(system, groups_subcmd => "system_groups_self")).map(apply_list_opts); + once(command!(system, Optional(SystemRef), "groups", search_param => "system_groups")) + .map(apply_list_opts); - let system_display_id_self_cmd = once(command!(system, "id" => "system_display_id_self")); - let system_display_id_cmd = once(command!(system_target, "id" => "system_display_id")); + let system_display_id_cmd = + once(command!(system, Optional(SystemRef), "id" => "system_display_id")); - system_info_cmd_self - .chain(system_new_cmd) + let system_delete = once( + command!(system, ("delete", ["erase", "remove", "yeet"]) => "system_delete") + .flag(("no-export", ["ne"])) + .help("Deletes the system"), + ); + + let system_link = [ + command!("link", ("account", UserRef) => "system_link"), + command!("unlink", ("account", OpaqueString) => "system_unlink").flag(YES), + ] + .into_iter(); + + system_new_cmd .chain(system_webhook_cmd) .chain(system_name_self_cmd) .chain(system_server_name_self_cmd) @@ -316,9 +288,6 @@ pub fn edit() -> impl Iterator { .chain(system_server_avatar_self_cmd) .chain(system_banner_self_cmd) .chain(system_members_self_cmd) - .chain(system_group_self_cmd) - .chain(system_display_id_self_cmd) - .chain(system_front_self_cmd) .chain(system_delete) .chain(system_privacy_cmd) .chain(system_proxy_cmd) diff --git a/crates/command_parser/src/parameter.rs b/crates/command_parser/src/parameter.rs index d9a24f3d..518c3e98 100644 --- a/crates/command_parser/src/parameter.rs +++ b/crates/command_parser/src/parameter.rs @@ -302,6 +302,7 @@ impl> From> for Parameter { } } +// todo: this should ideally be removed in favor of making Token::Parameter take multiple parameters /// skips the branch this parameter is in if it does not match #[derive(Clone)] pub struct Skip>(pub P); From a66735a798af53d86e9b3899ee6a45f95587c1d6 Mon Sep 17 00:00:00 2001 From: dawn <90008@gaze.systems> Date: Wed, 14 Jan 2026 02:55:27 +0300 Subject: [PATCH 155/179] update flake deps --- flake.lock | 78 +++++++++++++++++++++++++++--------------------------- flake.nix | 7 +++-- 2 files changed, 42 insertions(+), 43 deletions(-) diff --git a/flake.lock b/flake.lock index 8191b650..590c05d6 100644 --- a/flake.lock +++ b/flake.lock @@ -3,16 +3,16 @@ "crane": { "flake": false, "locked": { - "lastModified": 1727316705, - "narHash": "sha256-/mumx8AQ5xFuCJqxCIOFCHTVlxHkMT21idpbgbm/TIE=", + "lastModified": 1758758545, + "narHash": "sha256-NU5WaEdfwF6i8faJ2Yh+jcK9vVFrofLcwlD/mP65JrI=", "owner": "ipetkov", "repo": "crane", - "rev": "5b03654ce046b5167e7b0bccbd8244cb56c16f0e", + "rev": "95d528a5f54eaba0d12102249ce42f4d01f4e364", "type": "github" }, "original": { "owner": "ipetkov", - "ref": "v0.19.0", + "ref": "v0.21.1", "repo": "crane", "type": "github" } @@ -26,11 +26,11 @@ "pyproject-nix": "pyproject-nix" }, "locked": { - "lastModified": 1754978539, - "narHash": "sha256-nrDovydywSKRbWim9Ynmgj8SBm8LK3DI2WuhIqzOHYI=", + "lastModified": 1765953015, + "narHash": "sha256-5FBZbbWR1Csp3Y2icfRkxMJw/a/5FGg8hCXej2//bbI=", "owner": "nix-community", "repo": "dream2nix", - "rev": "fbec3263cb4895ac86ee9506cdc4e6919a1a2214", + "rev": "69eb01fa0995e1e90add49d8ca5bcba213b0416f", "type": "github" }, "original": { @@ -62,7 +62,7 @@ "rev": "ff81ac966bb2cae68946d5ed5fc4994f96d0ffec", "revCount": 69, "type": "tarball", - "url": "https://api.flakehub.com/f/pinned/edolstra/flake-compat/1.1.0/01948eb7-9cba-704f-bbf3-3fa956735b52/source.tar.gz" + "url": "https://api.flakehub.com/f/pinned/edolstra/flake-compat/1.1.0/01948eb7-9cba-704f-bbf3-3fa956735b52/source.tar.gz?rev=ff81ac966bb2cae68946d5ed5fc4994f96d0ffec&revCount=69" }, "original": { "type": "tarball", @@ -74,13 +74,13 @@ "locked": { "lastModified": 1681286841, "narHash": "sha256-3XlJrwlR0nBiREnuogoa5i1b4+w/XPe0z8bbrJASw0g=", - "owner": "yusdacra", + "owner": "90-008", "repo": "mk-naked-shell", "rev": "7612f828dd6f22b7fb332cc69440e839d7ffe6bd", "type": "github" }, "original": { - "owner": "yusdacra", + "owner": "90-008", "repo": "mk-naked-shell", "type": "github" } @@ -104,26 +104,26 @@ ] }, "locked": { - "lastModified": 1758867498, - "narHash": "sha256-UHtxhZ90LmOOqc+c163hRhaaq9Qmq/KQMGzRiNOG2WU=", - "owner": "yusdacra", + "lastModified": 1768285363, + "narHash": "sha256-n4dqIGCz2+/pyP0jtuTZxFTjuyBkgiKMwtOJrmbipDA=", + "owner": "90-008", "repo": "nix-cargo-integration", - "rev": "7308f6cf032e2264c449f3663897b9c99da0b20c", + "rev": "90432aa96bd7bb603ff710ffa2c02959dc338bd3", "type": "github" }, "original": { - "owner": "yusdacra", + "owner": "90-008", "repo": "nix-cargo-integration", "type": "github" } }, "nixpkgs": { "locked": { - "lastModified": 1758763312, - "narHash": "sha256-puBMviZhYlqOdUUgEmMVJpXqC/ToEqSvkyZ30qQ09xM=", + "lastModified": 1768302833, + "narHash": "sha256-h5bRFy9bco+8QcK7rGoOiqMxMbmn21moTACofNLRMP4=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "e57b3b16ad8758fd681511a078f35c416a8cc939", + "rev": "61db79b0c6b838d9894923920b612048e1201926", "type": "github" }, "original": { @@ -134,11 +134,11 @@ }, "nixpkgs-lib": { "locked": { - "lastModified": 1754788789, - "narHash": "sha256-x2rJ+Ovzq0sCMpgfgGaaqgBSwY+LST+WbZ6TytnT9Rk=", + "lastModified": 1765674936, + "narHash": "sha256-k00uTP4JNfmejrCLJOwdObYC9jHRrr/5M/a/8L2EIdo=", "owner": "nix-community", "repo": "nixpkgs.lib", - "rev": "a73b9c743612e4244d865a2fdee11865283c04e6", + "rev": "2075416fcb47225d9b68ac469a5c4801a9c4dd85", "type": "github" }, "original": { @@ -152,11 +152,11 @@ "nixpkgs-lib": "nixpkgs-lib" }, "locked": { - "lastModified": 1756770412, - "narHash": "sha256-+uWLQZccFHwqpGqr2Yt5VsW/PbeJVTn9Dk6SHWhNRPw=", + "lastModified": 1768135262, + "narHash": "sha256-PVvu7OqHBGWN16zSi6tEmPwwHQ4rLPU9Plvs8/1TUBY=", "owner": "hercules-ci", "repo": "flake-parts", - "rev": "4524271976b625a4a605beefd893f270620fd751", + "rev": "80daad04eddbbf5a4d883996a73f3f542fa437ac", "type": "github" }, "original": { @@ -167,11 +167,11 @@ }, "process-compose": { "locked": { - "lastModified": 1758658658, - "narHash": "sha256-y5GSCqlGe/uZzlocCPZcjc7Gj+mTq7m0P6xPGx88+No=", + "lastModified": 1767863885, + "narHash": "sha256-XXekPAxzbv1DmHFo3Elmj/vDnvWc1V0jdDUvM0/Wf7k=", "owner": "Platonic-Systems", "repo": "process-compose-flake", - "rev": "e968a94633788f5d9595d727f41c2baf0714be7b", + "rev": "99bea96cf269cfd235833ebdf645b567069fd398", "type": "github" }, "original": { @@ -211,11 +211,11 @@ ] }, "locked": { - "lastModified": 1752481895, - "narHash": "sha256-luVj97hIMpCbwhx3hWiRwjP2YvljWy8FM+4W9njDhLA=", + "lastModified": 1763017646, + "narHash": "sha256-Z+R2lveIp6Skn1VPH3taQIuMhABg1IizJd8oVdmdHsQ=", "owner": "pyproject-nix", "repo": "pyproject.nix", - "rev": "16ee295c25107a94e59a7fc7f2e5322851781162", + "rev": "47bd6f296502842643078d66128f7b5e5370790c", "type": "github" }, "original": { @@ -246,11 +246,11 @@ ] }, "locked": { - "lastModified": 1758854041, - "narHash": "sha256-kZ+24pbf4FiHlYlcvts64BhpxpHkPKIQXBmx1OmBAIo=", + "lastModified": 1768272338, + "narHash": "sha256-Tg/kL8eKMpZtceDvBDQYU8zowgpr7ucFRnpP/AtfuRM=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "02227ca8c229c968dbb5de95584cfb12b4313104", + "rev": "03dda130a8701b08b0347fcaf850a190c53a3c1e", "type": "github" }, "original": { @@ -261,11 +261,11 @@ }, "services": { "locked": { - "lastModified": 1758483444, - "narHash": "sha256-ZomYewjCu5DhyrndJ66JFSzSx3BM9Zg1tiNLbyPOwqo=", + "lastModified": 1765168239, + "narHash": "sha256-NZ7H4lbbytPNwe4ZyvovycuS1BMBFwJrptgX7NiF+F0=", "owner": "juspay", "repo": "services-flake", - "rev": "807ae843a84e3c9cfde796f106e4b127064960de", + "rev": "8b6244f2b310f229568d5cadf7dfcb5ebe6f8bda", "type": "github" }, "original": { @@ -318,11 +318,11 @@ ] }, "locked": { - "lastModified": 1758728421, - "narHash": "sha256-ySNJ008muQAds2JemiyrWYbwbG+V7S5wg3ZVKGHSFu8=", + "lastModified": 1768158989, + "narHash": "sha256-67vyT1+xClLldnumAzCTBvU0jLZ1YBcf4vANRWP3+Ak=", "owner": "numtide", "repo": "treefmt-nix", - "rev": "5eda4ee8121f97b218f7cc73f5172098d458f1d1", + "rev": "e96d59dff5c0d7fddb9d113ba108f03c3ef99eca", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 6a43619e..fa27a418 100644 --- a/flake.nix +++ b/flake.nix @@ -11,7 +11,7 @@ # rust d2n.url = "github:nix-community/dream2nix"; d2n.inputs.nixpkgs.follows = "nixpkgs"; - nci.url = "github:yusdacra/nix-cargo-integration"; + nci.url = "github:90-008/nix-cargo-integration"; nci.inputs.parts.follows = "parts"; nci.inputs.nixpkgs.follows = "nixpkgs"; nci.inputs.dream2nix.follows = "d2n"; @@ -167,8 +167,7 @@ let mkServiceProcess = name: attrs: - attrs - // { + { command = pkgs.writeShellApplication { name = "pluralkit-${name}"; runtimeInputs = [ pkgs.coreutils ]; @@ -178,7 +177,7 @@ nix develop .#services -c cargo run --package ${name} ''; }; - }; + } // attrs; in { ### migrations ### From 5a6d03ca1afdbaafc9f46d58d474af75b679ea2d Mon Sep 17 00:00:00 2001 From: dawn <90008@gaze.systems> Date: Sat, 17 Jan 2026 07:49:21 +0300 Subject: [PATCH 156/179] remove system_members_self since we use optional param in system_members --- PluralKit.Bot/CommandMeta/CommandTree.cs | 3 +-- crates/command_definitions/src/system.rs | 3 --- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/PluralKit.Bot/CommandMeta/CommandTree.cs b/PluralKit.Bot/CommandMeta/CommandTree.cs index 4632fca7..69da30d0 100644 --- a/PluralKit.Bot/CommandMeta/CommandTree.cs +++ b/PluralKit.Bot/CommandMeta/CommandTree.cs @@ -227,8 +227,7 @@ public partial class CommandTree Commands.GroupRandomMember(var param, var flags) => ctx.Execute(GroupMemberRandom, m => m.GroupMember(ctx, param.target, flags.all, flags.show_embed, flags)), Commands.SystemLink(var param, _) => ctx.Execute(Link, m => m.LinkSystem(ctx, param.account)), Commands.SystemUnlink(var param, var flags) => ctx.Execute(Unlink, m => m.UnlinkAccount(ctx, param.account, flags.yes)), - Commands.SystemMembersSelf(var param, var flags) => ctx.Execute(SystemList, m => m.MemberList(ctx, ctx.System, param.query, flags)), - Commands.SystemMembers(var param, var flags) => ctx.Execute(SystemList, m => m.MemberList(ctx, param.target, param.query, flags)), + Commands.SystemMembers(var param, var flags) => ctx.Execute(SystemList, m => m.MemberList(ctx, param.target ?? ctx.System, param.query, flags)), Commands.MemberGroups(var param, var flags) => ctx.Execute(MemberGroups, m => m.ListMemberGroups(ctx, param.target, param.query, flags, flags.all)), Commands.GroupMembers(var param, var flags) => ctx.Execute(GroupMemberList, m => m.ListGroupMembers(ctx, param.target, param.query, flags)), Commands.SystemGroups(var param, var flags) => ctx.Execute(GroupList, g => g.ListSystemGroups(ctx, param.target ?? ctx.System, param.query, flags, flags.all)), diff --git a/crates/command_definitions/src/system.rs b/crates/command_definitions/src/system.rs index 7860cad0..eb8fdf2b 100644 --- a/crates/command_definitions/src/system.rs +++ b/crates/command_definitions/src/system.rs @@ -253,8 +253,6 @@ pub fn edit() -> impl Iterator { let system_members_cmd = once(command!(system, Optional(SystemRef), members_subcmd => "system_members")) .map(apply_list_opts); - let system_members_self_cmd = - once(command!(members_subcmd => "system_members_self")).map(apply_list_opts); let system_groups_cmd = once(command!(system, Optional(SystemRef), "groups", search_param => "system_groups")) @@ -287,7 +285,6 @@ pub fn edit() -> impl Iterator { .chain(system_avatar_self_cmd) .chain(system_server_avatar_self_cmd) .chain(system_banner_self_cmd) - .chain(system_members_self_cmd) .chain(system_delete) .chain(system_privacy_cmd) .chain(system_proxy_cmd) From 9fbd68d0afd447310922bdb28af991549bed6883 Mon Sep 17 00:00:00 2001 From: dawn <90008@gaze.systems> Date: Sat, 17 Jan 2026 07:59:33 +0300 Subject: [PATCH 157/179] check for null pronouns in ShowPronouns always --- PluralKit.Bot/Commands/MemberEdit.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/PluralKit.Bot/Commands/MemberEdit.cs b/PluralKit.Bot/Commands/MemberEdit.cs index e4ccc6ea..e13dc919 100644 --- a/PluralKit.Bot/Commands/MemberEdit.cs +++ b/PluralKit.Bot/Commands/MemberEdit.cs @@ -158,12 +158,12 @@ public class MemberEdit if (ctx.System?.Id == target.System) noPronounsSetMessage += $" To set some, type `{ctx.DefaultPrefix}member {target.Reference(ctx)} pronouns `."; - if (format != ReplyFormat.Standard) - if (target.Pronouns == null) - { - await ctx.Reply(noPronounsSetMessage); - return; - } + // check for null since we are doing a query + if (target.Pronouns == null) + { + await ctx.Reply(noPronounsSetMessage); + return; + } if (format == ReplyFormat.Raw) { From 553b566595667e776b80041d5f54db20547acae3 Mon Sep 17 00:00:00 2001 From: dawn <90008@gaze.systems> Date: Sat, 17 Jan 2026 09:42:48 +0300 Subject: [PATCH 158/179] improve parameter enum macros --- crates/command_parser/src/parameter.rs | 244 +++++++------------------ 1 file changed, 69 insertions(+), 175 deletions(-) diff --git a/crates/command_parser/src/parameter.rs b/crates/command_parser/src/parameter.rs index 518c3e98..6c6d63c6 100644 --- a/crates/command_parser/src/parameter.rs +++ b/crates/command_parser/src/parameter.rs @@ -331,8 +331,8 @@ fn parse_user_ref(input: &str) -> Result { Err(SmolStr::new("invalid user ID")) } -macro_rules! impl_enum { - ($name:ident ($pretty_name:expr): $($variant:ident),*) => { +macro_rules! define_enum { + ($name:ident ($pretty_name:expr): $($variant:ident),* $(,)?) => { #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum $name { $($variant),* @@ -358,179 +358,82 @@ macro_rules! impl_enum { }; } -impl_enum! { +macro_rules! str_enum { + ($name:ident: $($variant:ident = $variant_str:literal),* $(,)?) => { + impl AsRef for $name { + fn as_ref(&self) -> &str { + match self { + $(Self::$variant => $variant_str),* + } + } + } + }; +} + +macro_rules! auto_enum { + ($name:ident ($pretty_name:expr): $($variant:ident = $variant_str:literal $(| $variant_matches:literal)*),* $(,)?) => { + define_enum!($name($pretty_name): $($variant),*); + + str_enum!($name: $($variant = $variant_str),*); + + impl FromStr for $name { + type Err = SmolStr; + + fn from_str(s: &str) -> Result { + match s.to_lowercase().as_str() { + $($variant_str $(| $variant_matches)* => Ok(Self::$variant),)* + _ => Err(Self::get_error()), + } + } + } + }; +} + +auto_enum! { MemberPrivacyTargetKind("member privacy target"): - Visibility, - Name, - Description, - Banner, - Avatar, - Birthday, - Pronouns, - Proxy, - Metadata + Visibility = "visibility", + Name = "name", + Description = "description", + Banner = "banner", + Avatar = "avatar", + Birthday = "birthday", + Pronouns = "pronouns", + Proxy = "proxy", + Metadata = "metadata", } -impl AsRef for MemberPrivacyTargetKind { - fn as_ref(&self) -> &str { - match self { - Self::Visibility => "visibility", - Self::Name => "name", - Self::Description => "description", - Self::Banner => "banner", - Self::Avatar => "avatar", - Self::Birthday => "birthday", - Self::Pronouns => "pronouns", - Self::Proxy => "proxy", - Self::Metadata => "metadata", - } - } -} - -impl FromStr for MemberPrivacyTargetKind { - type Err = SmolStr; - - fn from_str(s: &str) -> Result { - // todo: this doesnt parse all the possible ways - match s.to_lowercase().as_str() { - "visibility" => Ok(Self::Visibility), - "name" => Ok(Self::Name), - "description" => Ok(Self::Description), - "banner" => Ok(Self::Banner), - "avatar" => Ok(Self::Avatar), - "birthday" => Ok(Self::Birthday), - "pronouns" => Ok(Self::Pronouns), - "proxy" => Ok(Self::Proxy), - "metadata" => Ok(Self::Metadata), - _ => Err(Self::get_error()), - } - } -} - -impl_enum! { +auto_enum! { GroupPrivacyTargetKind("group privacy target"): - Name, - Icon, - Description, - Banner, - List, - Metadata, - Visibility + Name = "name", + Icon = "icon" | "avatar", + Description = "description", + Banner = "banner", + List = "list", + Metadata = "metadata", + Visibility = "visibility", } -impl AsRef for GroupPrivacyTargetKind { - fn as_ref(&self) -> &str { - match self { - Self::Name => "name", - Self::Icon => "icon", - Self::Description => "description", - Self::Banner => "banner", - Self::List => "list", - Self::Metadata => "metadata", - Self::Visibility => "visibility", - } - } -} - -impl FromStr for GroupPrivacyTargetKind { - type Err = SmolStr; - - fn from_str(s: &str) -> Result { - // todo: this doesnt parse all the possible ways - match s.to_lowercase().as_str() { - "name" => Ok(Self::Name), - "avatar" | "icon" => Ok(Self::Icon), - "description" => Ok(Self::Description), - "banner" => Ok(Self::Banner), - "list" => Ok(Self::List), - "metadata" => Ok(Self::Metadata), - "visibility" => Ok(Self::Visibility), - _ => Err(Self::get_error()), - } - } -} - -impl_enum! { +auto_enum! { SystemPrivacyTargetKind("system privacy target"): - Name, - Avatar, - Description, - Banner, - Pronouns, - MemberList, - GroupList, - Front, - FrontHistory + Name = "name", + Avatar = "avatar" | "pfp" | "pic" | "icon", + Description = "description" | "desc" | "bio" | "info", + Banner = "banner" | "splash" | "cover", + Pronouns = "pronouns" | "prns" | "pn", + MemberList = "members" | "memberlist" | "list", + GroupList = "groups" | "gs", + Front = "front" | "fronter" | "fronters", + FrontHistory = "fronthistory" | "fh" | "switches", } -impl AsRef for SystemPrivacyTargetKind { - fn as_ref(&self) -> &str { - match self { - Self::Name => "name", - Self::Avatar => "avatar", - Self::Description => "description", - Self::Banner => "banner", - Self::Pronouns => "pronouns", - Self::MemberList => "members", - Self::GroupList => "groups", - Self::Front => "front", - Self::FrontHistory => "fronthistory", - } - } +auto_enum! { + PrivacyLevelKind("privacy level"): + Public = "public", + Private = "private", } -impl FromStr for SystemPrivacyTargetKind { - type Err = SmolStr; - - fn from_str(s: &str) -> Result { - match s.to_lowercase().as_str() { - "name" => Ok(Self::Name), - "avatar" | "pfp" | "pic" | "icon" => Ok(Self::Avatar), - "description" | "desc" | "bio" | "info" => Ok(Self::Description), - "banner" | "splash" | "cover" => Ok(Self::Banner), - "pronouns" | "prns" | "pn" => Ok(Self::Pronouns), - "members" | "memberlist" | "list" => Ok(Self::MemberList), - "groups" | "gs" => Ok(Self::GroupList), - "front" | "fronter" | "fronters" => Ok(Self::Front), - "fronthistory" | "fh" | "switches" => Ok(Self::FrontHistory), - _ => Err(Self::get_error()), - } - } -} - -impl_enum!(PrivacyLevelKind("privacy level"): Public, Private); - -impl AsRef for PrivacyLevelKind { - fn as_ref(&self) -> &str { - match self { - Self::Public => "public", - Self::Private => "private", - } - } -} - -impl FromStr for PrivacyLevelKind { - type Err = SmolStr; // todo - - fn from_str(s: &str) -> Result { - match s { - "public" => Ok(PrivacyLevelKind::Public), - "private" => Ok(PrivacyLevelKind::Private), - _ => Err(Self::get_error()), - } - } -} - -impl_enum!(Toggle("toggle"): On, Off); - -impl AsRef for Toggle { - fn as_ref(&self) -> &str { - match self { - Self::On => "on", - Self::Off => "off", - } - } -} +define_enum!(Toggle("toggle"): On, Off); +str_enum!(Toggle: On = "on", Off = "off"); impl FromStr for Toggle { type Err = SmolStr; @@ -566,17 +469,8 @@ impl Into for Toggle { } } -impl_enum!(ProxySwitchAction("proxy switch action"): New, Add, Off); - -impl AsRef for ProxySwitchAction { - fn as_ref(&self) -> &str { - match self { - ProxySwitchAction::New => "new", - ProxySwitchAction::Add => "add", - ProxySwitchAction::Off => "off", - } - } -} +define_enum!(ProxySwitchAction("proxy switch action"): New, Add, Off); +str_enum!(ProxySwitchAction: New = "new", Add = "add", Off = "off"); impl FromStr for ProxySwitchAction { type Err = SmolStr; From d1451a858d6f8ace3ea1554c0c9c2a49212510af Mon Sep 17 00:00:00 2001 From: dawn <90008@gaze.systems> Date: Sat, 17 Jan 2026 10:25:22 +0300 Subject: [PATCH 159/179] cleanup unneeded .into_iters() and stuff --- crates/command_definitions/src/admin.rs | 4 +- crates/command_definitions/src/api.rs | 3 +- crates/command_definitions/src/autoproxy.rs | 3 +- crates/command_definitions/src/config.rs | 3 +- crates/command_definitions/src/debug.rs | 3 +- crates/command_definitions/src/fun.rs | 3 +- crates/command_definitions/src/group.rs | 53 +++++++------------ crates/command_definitions/src/help.rs | 3 +- .../command_definitions/src/import_export.rs | 3 +- crates/command_definitions/src/lib.rs | 3 +- crates/command_definitions/src/member.rs | 52 +++++++----------- crates/command_definitions/src/message.rs | 3 +- crates/command_definitions/src/misc.rs | 3 +- .../command_definitions/src/server_config.rs | 38 +++++-------- crates/command_definitions/src/switch.rs | 3 +- crates/command_definitions/src/system.rs | 46 ++++++---------- crates/command_parser/src/tree.rs | 2 +- 17 files changed, 82 insertions(+), 146 deletions(-) diff --git a/crates/command_definitions/src/admin.rs b/crates/command_definitions/src/admin.rs index 24608f58..00da3289 100644 --- a/crates/command_definitions/src/admin.rs +++ b/crates/command_definitions/src/admin.rs @@ -4,7 +4,7 @@ pub fn admin() -> &'static str { "admin" } -pub fn cmds() -> impl Iterator { +pub fn cmds() -> impl IntoIterator { let admin = admin(); let abuselog = tokens!(admin, ("abuselog", ["al"])); @@ -24,7 +24,7 @@ pub fn cmds() -> impl Iterator { .help("Removes a user from an abuse log entry"), command!(abuselog, ("delete", ["d"]), log_param => format!("admin_abuselog_delete_{}", log_param.name())) .help("Deletes an abuse log entry"), - ].into_iter() + ] }; let abuselog_cmds = [ command!(abuselog, ("create", ["c", "new"]), ("account", UserRef), Optional(Remainder(("description", OpaqueString))) => "admin_abuselog_create") diff --git a/crates/command_definitions/src/api.rs b/crates/command_definitions/src/api.rs index e9cd5746..d2e669b9 100644 --- a/crates/command_definitions/src/api.rs +++ b/crates/command_definitions/src/api.rs @@ -1,9 +1,8 @@ use super::*; -pub fn cmds() -> impl Iterator { +pub fn cmds() -> impl IntoIterator { [ command!("token" => "token_display"), command!("token", ("refresh", ["renew", "regen", "reroll"]) => "token_refresh"), ] - .into_iter() } diff --git a/crates/command_definitions/src/autoproxy.rs b/crates/command_definitions/src/autoproxy.rs index 68a1925b..f7d4e5dc 100644 --- a/crates/command_definitions/src/autoproxy.rs +++ b/crates/command_definitions/src/autoproxy.rs @@ -4,7 +4,7 @@ pub fn autoproxy() -> (&'static str, [&'static str; 2]) { ("autoproxy", ["ap", "auto"]) } -pub fn cmds() -> impl Iterator { +pub fn cmds() -> impl IntoIterator { let ap = autoproxy(); [ @@ -17,5 +17,4 @@ pub fn cmds() -> impl Iterator { .help("Sets autoproxy to front mode"), command!(ap, MemberRef => "autoproxy_member").help("Sets autoproxy to a specific member"), ] - .into_iter() } diff --git a/crates/command_definitions/src/config.rs b/crates/command_definitions/src/config.rs index af63e633..87741f86 100644 --- a/crates/command_definitions/src/config.rs +++ b/crates/command_definitions/src/config.rs @@ -2,7 +2,7 @@ use command_parser::parameter; use super::*; -pub fn cmds() -> impl Iterator { +pub fn cmds() -> impl IntoIterator { let cfg = ("config", ["cfg", "configure"]); let ap = tokens!(cfg, ("autoproxy", ["ap"])); @@ -198,5 +198,4 @@ pub fn cmds() -> impl Iterator { command!(group_limit => "cfg_limits_update").help("Refreshes member/group limits"), command!(limit => "cfg_limits_update").help("Refreshes member/group limits"), ] - .into_iter() } diff --git a/crates/command_definitions/src/debug.rs b/crates/command_definitions/src/debug.rs index 36f0e224..afd204e4 100644 --- a/crates/command_definitions/src/debug.rs +++ b/crates/command_definitions/src/debug.rs @@ -4,7 +4,7 @@ pub fn debug() -> (&'static str, [&'static str; 1]) { ("debug", ["dbg"]) } -pub fn cmds() -> impl Iterator { +pub fn cmds() -> impl IntoIterator { let debug = debug(); let perms = ("permissions", ["perms", "permcheck"]); [ @@ -12,5 +12,4 @@ pub fn cmds() -> impl Iterator { command!(debug, perms, ("guild", ["g"]), GuildRef => "permcheck_guild"), command!(debug, ("proxy", ["proxying", "proxycheck"]), MessageRef => "message_proxy_check"), ] - .into_iter() } diff --git a/crates/command_definitions/src/fun.rs b/crates/command_definitions/src/fun.rs index f16c09e1..9c0e80e4 100644 --- a/crates/command_definitions/src/fun.rs +++ b/crates/command_definitions/src/fun.rs @@ -1,6 +1,6 @@ use super::*; -pub fn cmds() -> impl Iterator { +pub fn cmds() -> impl IntoIterator { [ command!("thunder" => "fun_thunder"), command!("meow" => "fun_meow"), @@ -13,5 +13,4 @@ pub fn cmds() -> impl Iterator { command!("sus" => "amogus"), command!("error" => "fun_error"), ] - .into_iter() } diff --git a/crates/command_definitions/src/group.rs b/crates/command_definitions/src/group.rs index 24ab6f96..3d845b90 100644 --- a/crates/command_definitions/src/group.rs +++ b/crates/command_definitions/src/group.rs @@ -19,16 +19,16 @@ pub fn cmds() -> impl Iterator { let group_target = targeted(); let group_new = tokens!(group, ("new", ["n"])); - let group_new_cmd = [ + let group_new_cmd = once( command!(group_new, Remainder(("name", OpaqueString)) => "group_new") .help("Creates a new group"), - ] - .into_iter(); + ); - let group_info_cmd = [command!(group_target => "group_info") - .flag(ALL) - .help("Shows information about a group")] - .into_iter(); + let group_info_cmd = once( + command!(group_target => "group_info") + .flag(ALL) + .help("Shows information about a group"), + ); let group_name = tokens!( group_target, @@ -41,8 +41,7 @@ pub fn cmds() -> impl Iterator { .help("Clears the group's name"), command!(group_name, Remainder(("name", OpaqueString)) => "group_rename") .help("Renames the group"), - ] - .into_iter(); + ]; let group_display_name = tokens!(group_target, ("displayname", ["dn", "nick", "nickname"])); let group_display_name_cmd = [ @@ -53,8 +52,7 @@ pub fn cmds() -> impl Iterator { .help("Clears the group's display name"), command!(group_display_name, Remainder(("name", OpaqueString)) => "group_change_display_name") .help("Changes the group's display name"), - ] - .into_iter(); + ]; let group_description = tokens!( group_target, @@ -71,8 +69,7 @@ pub fn cmds() -> impl Iterator { .help("Clears the group's description"), command!(group_description, Remainder(("description", OpaqueString)) => "group_change_description") .help("Changes the group's description"), - ] - .into_iter(); + ]; let group_icon = tokens!( group_target, @@ -85,8 +82,7 @@ pub fn cmds() -> impl Iterator { .help("Clears the group's icon"), command!(group_icon, ("icon", Avatar) => "group_change_icon") .help("Changes the group's icon"), - ] - .into_iter(); + ]; let group_banner = tokens!(group_target, ("banner", ["splash", "cover"])); let group_banner_cmd = [ @@ -96,8 +92,7 @@ pub fn cmds() -> impl Iterator { .help("Clears the group's banner"), command!(group_banner, ("banner", Avatar) => "group_change_banner") .help("Changes the group's banner"), - ] - .into_iter(); + ]; let group_color = tokens!(group_target, ("color", ["colour"])); let group_color_cmd = [ @@ -107,8 +102,7 @@ pub fn cmds() -> impl Iterator { .help("Clears the group's color"), command!(group_color, ("color", OpaqueString) => "group_change_color") .help("Changes the group's color"), - ] - .into_iter(); + ]; let group_privacy = tokens!(group_target, ("privacy", ["priv"])); let group_privacy_cmd = [ @@ -118,30 +112,25 @@ pub fn cmds() -> impl Iterator { .help("Changes all privacy settings for the group"), command!(group_privacy, ("privacy", GroupPrivacyTarget), ("level", PrivacyLevel) => "group_change_privacy") .help("Changes a specific privacy setting for the group"), - ] - .into_iter(); + ]; let group_public_cmd = [ command!(group_target, ("public", ["pub"]) => "group_set_public") .help("Sets the group to public"), - ] - .into_iter(); + ]; let group_private_cmd = [ command!(group_target, ("private", ["priv"]) => "group_set_private") .help("Sets the group to private"), - ] - .into_iter(); + ]; let group_delete_cmd = [ command!(group_target, ("delete", ["destroy", "erase", "yeet"]) => "group_delete") .flag(YES) .help("Deletes the group"), - ] - .into_iter(); + ]; - let group_id_cmd = - [command!(group_target, "id" => "group_id").help("Shows the group's ID")].into_iter(); + let group_id_cmd = [command!(group_target, "id" => "group_id").help("Shows the group's ID")]; let group_front = tokens!(group_target, ("front", ["fronter", "fronters", "f"])); let group_front_cmd = [ @@ -149,8 +138,7 @@ pub fn cmds() -> impl Iterator { .flag(("duration", OpaqueString)) .flag(("fronters-only", ["fo"])) .flag("flat"), - ] - .into_iter(); + ]; let apply_list_opts = |cmd: Command| cmd.flags(get_list_flags()); let search_param = Optional(Remainder(("query", OpaqueString))); @@ -164,8 +152,7 @@ pub fn cmds() -> impl Iterator { .flag(ALL).flag(YES), command!(group_target, ("remove", ["rem", "rm"]), Optional(MemberRefs) => "group_remove_member") .flag(ALL).flag(YES), - ] - .into_iter(); + ]; let system_groups_cmd = once(command!(group, ("list", ["ls", "l"]), search_param => "groups_self")) diff --git a/crates/command_definitions/src/help.rs b/crates/command_definitions/src/help.rs index 95e73d5c..793934db 100644 --- a/crates/command_definitions/src/help.rs +++ b/crates/command_definitions/src/help.rs @@ -1,6 +1,6 @@ use super::*; -pub fn cmds() -> impl Iterator { +pub fn cmds() -> impl IntoIterator { let help = ("help", ["h"]); [ command!(("commands", ["cmd", "c"]), ("subject", OpaqueString) => "commands_list"), @@ -10,5 +10,4 @@ pub fn cmds() -> impl Iterator { command!(help, "commands" => "help_commands").help("help commands"), command!(help, "proxy" => "help_proxy").help("help proxy"), ] - .into_iter() } diff --git a/crates/command_definitions/src/import_export.rs b/crates/command_definitions/src/import_export.rs index 7e2e9404..bf095263 100644 --- a/crates/command_definitions/src/import_export.rs +++ b/crates/command_definitions/src/import_export.rs @@ -1,9 +1,8 @@ use super::*; -pub fn cmds() -> impl Iterator { +pub fn cmds() -> impl IntoIterator { [ command!("import", Optional(Remainder(("url", OpaqueString))) => "import").flag(YES), command!("export" => "export"), ] - .into_iter() } diff --git a/crates/command_definitions/src/lib.rs b/crates/command_definitions/src/lib.rs index 40617f62..54550b57 100644 --- a/crates/command_definitions/src/lib.rs +++ b/crates/command_definitions/src/lib.rs @@ -25,7 +25,8 @@ use command_parser::{ }; pub fn all() -> impl Iterator { - (help::cmds()) + std::iter::empty() + .chain(help::cmds()) .chain(system::cmds()) .chain(group::cmds()) .chain(member::cmds()) diff --git a/crates/command_definitions/src/member.rs b/crates/command_definitions/src/member.rs index 607cc0a7..e466a26a 100644 --- a/crates/command_definitions/src/member.rs +++ b/crates/command_definitions/src/member.rs @@ -35,16 +35,16 @@ pub fn cmds() -> impl Iterator { let tts = ("tts", ["texttospeech"]); let delete = ("delete", ["del", "remove"]); - let member_new_cmd = [ + let member_new_cmd = once( command!(member, new, ("name", OpaqueString) => "member_new") .help("Creates a new system member"), - ] - .into_iter(); + ); - let member_info_cmd = [command!(member_target => "member_show") - .flag("pt") - .help("Shows information about a member")] - .into_iter(); + let member_info_cmd = once( + command!(member_target => "member_show") + .flag("pt") + .help("Shows information about a member"), + ); let member_name_cmd = { let member_name = tokens!(member_target, name); @@ -54,7 +54,6 @@ pub fn cmds() -> impl Iterator { .flag(YES) .help("Changes a member's name"), ] - .into_iter() }; let member_description_cmd = { @@ -67,7 +66,6 @@ pub fn cmds() -> impl Iterator { command!(member_desc, Remainder(("description", OpaqueString)) => "member_desc_update") .help("Changes a member's description"), ] - .into_iter() }; let member_privacy_cmd = { @@ -81,7 +79,6 @@ pub fn cmds() -> impl Iterator { ) .help("Changes a member's privacy settings"), ] - .into_iter() }; let member_pronouns_cmd = { @@ -94,7 +91,7 @@ pub fn cmds() -> impl Iterator { command!(member_pronouns, CLEAR => "member_pronouns_clear") .flag(YES) .help("Clears a member's pronouns"), - ].into_iter() + ] }; let member_banner_cmd = { @@ -107,7 +104,6 @@ pub fn cmds() -> impl Iterator { .flag(YES) .help("Clears a member's banner image"), ] - .into_iter() }; let member_color_cmd = { @@ -120,7 +116,6 @@ pub fn cmds() -> impl Iterator { .flag(YES) .help("Clears a member's color"), ] - .into_iter() }; let member_birthday_cmd = { @@ -133,7 +128,6 @@ pub fn cmds() -> impl Iterator { .flag(YES) .help("Clears a member's birthday"), ] - .into_iter() }; let member_display_name_cmd = { @@ -146,7 +140,7 @@ pub fn cmds() -> impl Iterator { command!(member_display_name, CLEAR => "member_displayname_clear") .flag(YES) .help("Clears a member's display name"), - ].into_iter() + ] }; let member_server_name_cmd = { @@ -159,7 +153,7 @@ pub fn cmds() -> impl Iterator { command!(member_server_name, CLEAR => "member_servername_clear") .flag(YES) .help("Clears a member's server name"), - ].into_iter() + ] }; let member_proxy_cmd = { @@ -176,7 +170,7 @@ pub fn cmds() -> impl Iterator { .help("Clears all proxy tags from a member"), command!(member_proxy, Remainder(("tags", OpaqueString)) => "member_proxy_set") .help("Sets a member's proxy tags"), - ].into_iter() + ] }; let member_proxy_settings_cmd = { @@ -194,7 +188,7 @@ pub fn cmds() -> impl Iterator { .help("Clears a member's server-specific keep-proxy setting"), command!(member_server_keep_proxy, ("value", Toggle) => "member_server_keepproxy_update") .help("Changes a member's server-specific keep-proxy setting"), - ].into_iter() + ] }; let member_message_settings_cmd = { @@ -210,7 +204,6 @@ pub fn cmds() -> impl Iterator { command!(member_autoproxy, ("value", Toggle) => "member_autoproxy_update") .help("Changes whether a member can be autoproxied"), ] - .into_iter() }; let member_avatar_cmd = { @@ -229,7 +222,6 @@ pub fn cmds() -> impl Iterator { .flag(YES) .help("Clears a member's avatar"), ] - .into_iter() }; let member_webhook_avatar_cmd = { @@ -256,7 +248,6 @@ pub fn cmds() -> impl Iterator { .flag(YES) .help("Clears a member's proxy avatar"), ] - .into_iter() }; let member_server_avatar_cmd = { @@ -289,13 +280,8 @@ pub fn cmds() -> impl Iterator { .flag(YES) .help("Clears a member's server-specific avatar"), ] - .into_iter() }; - let member_avatar_cmds = member_avatar_cmd - .chain(member_webhook_avatar_cmd) - .chain(member_server_avatar_cmd); - let member_group = tokens!(member_target, ("groups", ["group"])); let member_list_group_cmds = once( command!(member_group, Optional(Remainder(("query", OpaqueString))) => "member_groups"), @@ -306,18 +292,16 @@ pub fn cmds() -> impl Iterator { .help("Adds a member to one or more groups"), command!(member_group, ("remove", ["rem"]), Optional(("groups", GroupRefs)) => "member_group_remove") .help("Removes a member from one or more groups"), - ] - .into_iter(); + ]; let member_display_id_cmd = - [command!(member_target, "id" => "member_id").help("Displays a member's ID")].into_iter(); + [command!(member_target, "id" => "member_id").help("Displays a member's ID")]; let member_delete_cmd = - [command!(member_target, delete => "member_delete").help("Deletes a member")].into_iter(); + [command!(member_target, delete => "member_delete").help("Deletes a member")]; let member_easter_eggs = - [command!(member_target, "soulscream" => "member_soulscream").show_in_suggestions(false)] - .into_iter(); + [command!(member_target, "soulscream" => "member_soulscream").show_in_suggestions(false)]; member_new_cmd .chain(member_info_cmd) @@ -331,7 +315,9 @@ pub fn cmds() -> impl Iterator { .chain(member_display_name_cmd) .chain(member_server_name_cmd) .chain(member_proxy_cmd) - .chain(member_avatar_cmds) + .chain(member_avatar_cmd) + .chain(member_webhook_avatar_cmd) + .chain(member_server_avatar_cmd) .chain(member_proxy_settings_cmd) .chain(member_message_settings_cmd) .chain(member_display_id_cmd) diff --git a/crates/command_definitions/src/message.rs b/crates/command_definitions/src/message.rs index 7030ddfb..812b059d 100644 --- a/crates/command_definitions/src/message.rs +++ b/crates/command_definitions/src/message.rs @@ -1,6 +1,6 @@ use super::*; -pub fn cmds() -> impl Iterator { +pub fn cmds() -> impl IntoIterator { let message = tokens!(("message", ["msg", "messageinfo"]), Optional(MessageRef)); let author = ("author", ["sender", "a"]); @@ -31,5 +31,4 @@ pub fn cmds() -> impl Iterator { .flag(author) .help("Shows information about a proxied message"), ] - .into_iter() } diff --git a/crates/command_definitions/src/misc.rs b/crates/command_definitions/src/misc.rs index a87f4d1b..7e7f1641 100644 --- a/crates/command_definitions/src/misc.rs +++ b/crates/command_definitions/src/misc.rs @@ -1,10 +1,9 @@ use super::*; -pub fn cmds() -> impl Iterator { +pub fn cmds() -> impl IntoIterator { [ command!("invite" => "invite").help("Gets a link to invite PluralKit to other servers"), command!(("stats", ["status"]) => "stats") .help("Shows statistics and information about PluralKit"), ] - .into_iter() } diff --git a/crates/command_definitions/src/server_config.rs b/crates/command_definitions/src/server_config.rs index bdceade9..366b1236 100644 --- a/crates/command_definitions/src/server_config.rs +++ b/crates/command_definitions/src/server_config.rs @@ -1,3 +1,5 @@ +use std::iter::once; + use super::*; pub fn cmds() -> impl Iterator { @@ -44,7 +46,6 @@ pub fn cmds() -> impl Iterator { let add = ("add", ["enable", "on", "deny"]); let remove = ("remove", ["disable", "off", "allow"]); - // Log channel commands let log_channel_cmds = [ command!(log_channel => "server_config_log_channel_show") .help("Shows the current log channel"), @@ -53,10 +54,8 @@ pub fn cmds() -> impl Iterator { command!(log_channel, CLEAR => "server_config_log_channel_clear") .flag(YES) .help("Clears the log channel"), - ] - .into_iter(); + ]; - // Log cleanup commands let log_cleanup_cmds = [ command!(log_cleanup => "server_config_log_cleanup_show") .help("Shows whether log cleanup is enabled"), @@ -66,10 +65,8 @@ pub fn cmds() -> impl Iterator { .help("Shows whether log cleanup is enabled"), command!(log_cleanup_short, Toggle => "server_config_log_cleanup_set") .help("Enables or disables log cleanup"), - ] - .into_iter(); + ]; - // Log blacklist commands let log_blacklist_cmds = [ command!(log_blacklist => "server_config_log_blacklist_show") .help("Shows channels where logging is disabled"), @@ -79,10 +76,8 @@ pub fn cmds() -> impl Iterator { command!(log_blacklist, remove, Optional(("channel", ChannelRef)) => "server_config_log_blacklist_remove") .flag(ALL) .help("Removes a channel (or all channels with --all) from the log blacklist"), - ] - .into_iter(); + ]; - // Proxy blacklist commands let proxy_blacklist_cmds = [ command!(proxy_blacklist => "server_config_proxy_blacklist_show") .help("Shows channels where proxying is disabled"), @@ -92,10 +87,8 @@ pub fn cmds() -> impl Iterator { command!(proxy_blacklist, remove, Optional(("channel", ChannelRef)) => "server_config_proxy_blacklist_remove") .flag(ALL) .help("Removes a channel (or all channels with --all) from the proxy blacklist"), - ] - .into_iter(); + ]; - // Invalid command error commands let invalid_cmds = [ command!(invalid => "server_config_invalid_command_response_show") .help("Shows whether error responses for invalid commands are enabled"), @@ -105,10 +98,8 @@ pub fn cmds() -> impl Iterator { .help("Shows whether error responses for invalid commands are enabled"), command!(invalid_short, Toggle => "server_config_invalid_command_response_set") .help("Enables or disables error responses for invalid commands"), - ] - .into_iter(); + ]; - // Require system tag commands let require_tag_cmds = [ command!(require_tag => "server_config_require_system_tag_show") .help("Shows whether system tags are required"), @@ -118,10 +109,8 @@ pub fn cmds() -> impl Iterator { .help("Shows whether system tags are required"), command!(require_tag_short, Toggle => "server_config_require_system_tag_set") .help("Requires or unrequires system tags for proxied messages"), - ] - .into_iter(); + ]; - // Suppress notifications commands let suppress_cmds = [ command!(suppress => "server_config_suppress_notifications_show") .help("Shows whether notifications are suppressed for proxied messages"), @@ -131,13 +120,12 @@ pub fn cmds() -> impl Iterator { .help("Shows whether notifications are suppressed for proxied messages"), command!(suppress_short, Toggle => "server_config_suppress_notifications_set") .help("Enables or disables notification suppression for proxied messages"), - ] - .into_iter(); + ]; - // Main config overview - let main_cmd = [command!(server_config => "server_config_show") - .help("Shows the current server configuration")] - .into_iter(); + let main_cmd = once( + command!(server_config => "server_config_show") + .help("Shows the current server configuration"), + ); main_cmd .chain(log_channel_cmds) diff --git a/crates/command_definitions/src/switch.rs b/crates/command_definitions/src/switch.rs index ef9e15de..c52525ad 100644 --- a/crates/command_definitions/src/switch.rs +++ b/crates/command_definitions/src/switch.rs @@ -1,6 +1,6 @@ use super::*; -pub fn cmds() -> impl Iterator { +pub fn cmds() -> impl IntoIterator { let switch = ("switch", ["sw"]); let edit = ("edit", ["e", "replace"]); @@ -26,5 +26,4 @@ pub fn cmds() -> impl Iterator { command!(switch, copy, Optional(MemberRefs) => "switch_copy").flags(edit_flags), command!(switch, MemberRefs => "switch_do"), ] - .into_iter() } diff --git a/crates/command_definitions/src/system.rs b/crates/command_definitions/src/system.rs index eb8fdf2b..ec25f7db 100644 --- a/crates/command_definitions/src/system.rs +++ b/crates/command_definitions/src/system.rs @@ -20,7 +20,6 @@ pub fn targeted() -> TokensIterator { pub fn edit() -> impl Iterator { let system = system(); - let system_target = targeted(); let system_new_cmd = once( @@ -36,8 +35,7 @@ pub fn edit() -> impl Iterator { .help("Clears your system's webhook URL"), command!(system_webhook, ("url", OpaqueString) => "system_webhook_set") .help("Sets your system's webhook URL"), - ] - .into_iter(); + ]; let add_info_flags = |cmd: Command| { cmd.flag(("public", ["pub"])) @@ -65,8 +63,7 @@ pub fn edit() -> impl Iterator { .help("Clears your system's name"), command!(system_name_self, Remainder(("name", OpaqueString)) => "system_rename") .help("Renames your system"), - ] - .into_iter(); + ]; let server_name = ("servername", ["sn", "guildname"]); let system_server_name_cmd = once( @@ -80,8 +77,7 @@ pub fn edit() -> impl Iterator { .help("Clears your system's server name"), command!(system_server_name_self, Remainder(("name", OpaqueString)) => "system_rename_server_name") .help("Renames your system's server name"), - ] - .into_iter(); + ]; let description = ("description", ["desc", "d"]); let system_description_cmd = once( @@ -95,8 +91,7 @@ pub fn edit() -> impl Iterator { .help("Clears your system's description"), command!(system_description_self, Remainder(("description", OpaqueString)) => "system_change_description") .help("Changes your system's description"), - ] - .into_iter(); + ]; let color = ("color", ["colour"]); let system_color_cmd = once( @@ -110,8 +105,7 @@ pub fn edit() -> impl Iterator { .help("Clears your system's color"), command!(system_color_self, ("color", OpaqueString) => "system_change_color") .help("Changes your system's color"), - ] - .into_iter(); + ]; let tag = ("tag", ["suffix"]); let system_tag_cmd = once( @@ -125,8 +119,7 @@ pub fn edit() -> impl Iterator { .help("Clears your system's tag"), command!(system_tag_self, Remainder(("tag", OpaqueString)) => "system_change_tag") .help("Changes your system's tag"), - ] - .into_iter(); + ]; let servertag = ("servertag", ["st", "guildtag"]); let system_server_tag_cmd = once( @@ -140,8 +133,7 @@ pub fn edit() -> impl Iterator { .help("Clears your system's server tag"), command!(system_server_tag_self, Remainder(("tag", OpaqueString)) => "system_change_server_tag") .help("Changes your system's server tag"), - ] - .into_iter(); + ]; let pronouns = ("pronouns", ["prns"]); let system_pronouns_cmd = once( @@ -155,8 +147,7 @@ pub fn edit() -> impl Iterator { .help("Clears your system's pronouns"), command!(system_pronouns_self, Remainder(("pronouns", OpaqueString)) => "system_change_pronouns") .help("Changes your system's pronouns"), - ] - .into_iter(); + ]; let avatar = ("avatar", ["pfp"]); let system_avatar_cmd = once( @@ -170,11 +161,9 @@ pub fn edit() -> impl Iterator { .help("Clears your system's avatar"), command!(system_avatar_self, ("avatar", Avatar) => "system_change_avatar") .help("Changes your system's avatar"), - ] - .into_iter(); + ]; let serveravatar = ("serveravatar", ["spfp"]); - let system_server_avatar = tokens!(system_target, serveravatar); let system_server_avatar_cmd = once( command!(system, Optional(SystemRef), serveravatar => "system_show_server_avatar") .help("Shows the system's server avatar"), @@ -186,8 +175,7 @@ pub fn edit() -> impl Iterator { .help("Clears your system's server avatar"), command!(system_server_avatar_self, ("avatar", Avatar) => "system_change_server_avatar") .help("Changes your system's server avatar"), - ] - .into_iter(); + ]; let banner = ("banner", ["cover"]); let system_banner_cmd = once( @@ -201,8 +189,7 @@ pub fn edit() -> impl Iterator { .help("Clears your system's banner"), command!(system_banner_self, ("banner", Avatar) => "system_change_banner") .help("Changes your system's banner"), - ] - .into_iter(); + ]; let system_proxy = tokens!(system, "proxy"); let system_proxy_cmd = [ @@ -214,8 +201,7 @@ pub fn edit() -> impl Iterator { .help("Shows your system's proxy setting for a guild"), command!(system_proxy, GuildRef, Toggle => "system_toggle_proxy") .help("Toggle your system's proxy for a guild"), - ] - .into_iter(); + ]; let system_privacy = tokens!(system, ("privacy", ["priv"])); let system_privacy_cmd = [ @@ -225,7 +211,7 @@ pub fn edit() -> impl Iterator { .help("Changes all privacy settings for your system"), command!(system_privacy, ("privacy", SystemPrivacyTarget), ("level", PrivacyLevel) => "system_change_privacy") .help("Changes a specific privacy setting for your system"), - ].into_iter(); + ]; let front = ("front", ["fronter", "fronters", "f"]); let make_front_history = |subcmd: TokensIterator| { @@ -243,8 +229,7 @@ pub fn edit() -> impl Iterator { make_front_history(tokens!(("fronthistory", ["fh"]))), make_front_percent(tokens!(front, ("percent", ["p", "%"]))), make_front_percent(tokens!(("frontpercent", ["fp"]))), - ] - .into_iter(); + ]; let search_param = Optional(Remainder(("query", OpaqueString))); let apply_list_opts = |cmd: Command| cmd.flags(get_list_flags()); @@ -270,8 +255,7 @@ pub fn edit() -> impl Iterator { let system_link = [ command!("link", ("account", UserRef) => "system_link"), command!("unlink", ("account", OpaqueString) => "system_unlink").flag(YES), - ] - .into_iter(); + ]; system_new_cmd .chain(system_webhook_cmd) diff --git a/crates/command_parser/src/tree.rs b/crates/command_parser/src/tree.rs index ac5420e2..42939a3b 100644 --- a/crates/command_parser/src/tree.rs +++ b/crates/command_parser/src/tree.rs @@ -1,6 +1,6 @@ use ordermap::OrderMap; -use crate::{command::Command, parameter::Skip, token::Token}; +use crate::{command::Command, token::Token}; #[derive(Debug, Clone)] pub struct TreeBranch { From bdf6a6c3458a7c6c0942cf144a2ee7ae274c948c Mon Sep 17 00:00:00 2001 From: dawn <90008@gaze.systems> Date: Sat, 17 Jan 2026 10:35:23 +0300 Subject: [PATCH 160/179] organize config command definitions --- crates/command_definitions/src/config.rs | 201 +++++++++++++---------- 1 file changed, 117 insertions(+), 84 deletions(-) diff --git a/crates/command_definitions/src/config.rs b/crates/command_definitions/src/config.rs index 87741f86..68723301 100644 --- a/crates/command_definitions/src/config.rs +++ b/crates/command_definitions/src/config.rs @@ -4,80 +4,13 @@ use super::*; pub fn cmds() -> impl IntoIterator { let cfg = ("config", ["cfg", "configure"]); - let ap = tokens!(cfg, ("autoproxy", ["ap"])); + let base = [command!(cfg => "cfg_show").help("Shows the current configuration")]; + + let ap = tokens!(cfg, ("autoproxy", ["ap"])); let ap_account = tokens!(ap, ("account", ["ac"])); let ap_timeout = tokens!(ap, ("timeout", ["tm"])); - - let timezone = tokens!(cfg, ("timezone", ["zone", "tz"])); - let ping = tokens!(cfg, "ping"); - - let priv_ = ("private", ["priv"]); - let member_privacy = tokens!(cfg, priv_, ("member", ["mem"])); - let member_privacy_short = tokens!(cfg, "mp"); - let group_privacy = tokens!(cfg, priv_, ("group", ["grp"])); - let group_privacy_short = tokens!(cfg, "gp"); - - let show = "show"; - let show_private = tokens!(cfg, show, priv_); - let show_private_short = tokens!(cfg, "sp"); - - let proxy = ("proxy", ["px"]); - let proxy_case = tokens!(cfg, proxy, ("case", ["caps", "capitalize", "capitalise"])); - let proxy_error = tokens!(cfg, proxy, ("error", ["errors"])); - let proxy_error_short = tokens!(cfg, "pe"); - let proxy_switch = tokens!(cfg, proxy, "switch"); - let proxy_switch_short = tokens!(cfg, ("proxyswitch", ["ps"])); - - let id = ("id", ["ids"]); - let split_id = tokens!(cfg, "split", id); - let split_id_short = tokens!(cfg, ("sid", ["sids"])); - let cap_id = tokens!(cfg, ("cap", ["caps", "capitalize", "capitalise"]), id); - let cap_id_short = tokens!(cfg, ("capid", ["capids"])); - - let pad = ("pad", ["padding"]); - let pad_id = tokens!(cfg, pad, id); - let id_pad = tokens!(cfg, id, pad); - let id_pad_short = tokens!(cfg, ("idpad", ["padid", "padids"])); - - let show_color = tokens!(cfg, show, ("color", ["colour", "colors", "colours"])); - let show_color_short = tokens!( - cfg, - ( - "showcolor", - [ - "showcolour", - "showcolors", - "showcolours", - "colorcode", - "colorhex" - ] - ) - ); - - let format = "format"; - let name_format = tokens!(cfg, "name", format); - let name_format_short = tokens!(cfg, ("nameformat", ["nf"])); - - let server = "server"; - let server_name_format = tokens!(cfg, server, "name", format); - let server_format = tokens!( - cfg, - ("server", ["servername"]), - ("format", ["nameformat", "nf"]) - ); - let server_format_short = tokens!( - cfg, - ("snf", ["servernf", "servernameformat", "snameformat"]) - ); - - let limit_ = ("limit", ["lim"]); - let member_limit = tokens!(cfg, ("member", ["mem"]), limit_); - let group_limit = tokens!(cfg, ("group", ["grp"]), limit_); - let limit = tokens!(cfg, limit_); - - [ - command!(cfg => "cfg_show").help("Shows the current configuration"), + let autoproxy = [ command!(ap_account => "cfg_ap_account_show") .help("Shows autoproxy status for the account"), command!(ap_account, Toggle => "cfg_ap_account_update") @@ -88,12 +21,28 @@ pub fn cmds() -> impl IntoIterator { .help("Disables the autoproxy timeout"), command!(ap_timeout, ("timeout", OpaqueString) => "cfg_ap_timeout_update") .help("Sets the autoproxy timeout"), - command!(timezone => "cfg_timezone_show").help("Shows the system timezone"), - command!(timezone, RESET => "cfg_timezone_reset").help("Resets the system timezone"), - command!(timezone, ("timezone", OpaqueString) => "cfg_timezone_update") + ]; + + let timezone_tokens = tokens!(cfg, ("timezone", ["zone", "tz"])); + let timezone = [ + command!(timezone_tokens => "cfg_timezone_show").help("Shows the system timezone"), + command!(timezone_tokens, RESET => "cfg_timezone_reset").help("Resets the system timezone"), + command!(timezone_tokens, ("timezone", OpaqueString) => "cfg_timezone_update") .help("Sets the system timezone"), - command!(ping => "cfg_ping_show").help("Shows the ping setting"), - command!(ping, Toggle => "cfg_ping_update").help("Updates the ping setting"), + ]; + + let ping_tokens = tokens!(cfg, "ping"); + let ping = [ + command!(ping_tokens => "cfg_ping_show").help("Shows the ping setting"), + command!(ping_tokens, Toggle => "cfg_ping_update").help("Updates the ping setting"), + ]; + + let priv_ = ("private", ["priv"]); + let member_privacy = tokens!(cfg, priv_, ("member", ["mem"])); + let member_privacy_short = tokens!(cfg, "mp"); + let group_privacy = tokens!(cfg, priv_, ("group", ["grp"])); + let group_privacy_short = tokens!(cfg, "gp"); + let privacy = [ command!(member_privacy => "cfg_member_privacy_show") .help("Shows the default privacy for new members"), command!(member_privacy, Toggle => "cfg_member_privacy_update") @@ -110,6 +59,12 @@ pub fn cmds() -> impl IntoIterator { .help("Shows the default privacy for new groups"), command!(group_privacy_short, Toggle => "cfg_group_privacy_update") .help("Sets the default privacy for new groups"), + ]; + + let show = "show"; + let show_private = tokens!(cfg, show, priv_); + let show_private_short = tokens!(cfg, "sp"); + let private_info = [ command!(show_private => "cfg_show_private_info_show") .help("Shows whether private info is shown"), command!(show_private, Toggle => "cfg_show_private_info_update") @@ -118,6 +73,15 @@ pub fn cmds() -> impl IntoIterator { .help("Shows whether private info is shown"), command!(show_private_short, Toggle => "cfg_show_private_info_update") .help("Toggles showing private info"), + ]; + + let proxy = ("proxy", ["px"]); + let proxy_case = tokens!(cfg, proxy, ("case", ["caps", "capitalize", "capitalise"])); + let proxy_error = tokens!(cfg, proxy, ("error", ["errors"])); + let proxy_error_short = tokens!(cfg, "pe"); + let proxy_switch = tokens!(cfg, proxy, "switch"); + let proxy_switch_short = tokens!(cfg, ("proxyswitch", ["ps"])); + let proxy_settings = [ command!(proxy_case => "cfg_case_sensitive_proxy_tags_show") .help("Shows whether proxy tags are case-sensitive"), command!(proxy_case, Toggle => "cfg_case_sensitive_proxy_tags_update") @@ -130,6 +94,25 @@ pub fn cmds() -> impl IntoIterator { .help("Shows whether proxy error messages are enabled"), command!(proxy_error_short, Toggle => "cfg_proxy_error_message_update") .help("Toggles proxy error messages"), + command!(proxy_switch => "cfg_proxy_switch_show").help("Shows the proxy switch behavior"), + command!(proxy_switch, ProxySwitchAction => "cfg_proxy_switch_update") + .help("Sets the proxy switch behavior"), + command!(proxy_switch_short => "cfg_proxy_switch_show") + .help("Shows the proxy switch behavior"), + command!(proxy_switch_short, ProxySwitchAction => "cfg_proxy_switch_update") + .help("Sets the proxy switch behavior"), + ]; + + let id = ("id", ["ids"]); + let split_id = tokens!(cfg, "split", id); + let split_id_short = tokens!(cfg, ("sid", ["sids"])); + let cap_id = tokens!(cfg, ("cap", ["caps", "capitalize", "capitalise"]), id); + let cap_id_short = tokens!(cfg, ("capid", ["capids"])); + let pad = ("pad", ["padding"]); + let pad_id = tokens!(cfg, pad, id); + let id_pad = tokens!(cfg, id, pad); + let id_pad_short = tokens!(cfg, ("idpad", ["padid", "padids"])); + let id_settings = [ command!(split_id => "cfg_hid_split_show").help("Shows whether IDs are split in lists"), command!(split_id, Toggle => "cfg_hid_split_update").help("Toggles splitting IDs in lists"), command!(split_id_short => "cfg_hid_split_show") @@ -152,6 +135,23 @@ pub fn cmds() -> impl IntoIterator { command!(id_pad_short => "cfg_hid_padding_show").help("Shows the ID padding for lists"), command!(id_pad_short, ("padding", OpaqueString) => "cfg_hid_padding_update") .help("Sets the ID padding for lists"), + ]; + + let show_color = tokens!(cfg, show, ("color", ["colour", "colors", "colours"])); + let show_color_short = tokens!( + cfg, + ( + "showcolor", + [ + "showcolour", + "showcolors", + "showcolours", + "colorcode", + "colorhex" + ] + ) + ); + let color_settings = [ command!(show_color => "cfg_card_show_color_hex_show") .help("Shows whether color hex codes are shown on cards"), command!(show_color, Toggle => "cfg_card_show_color_hex_update") @@ -160,13 +160,12 @@ pub fn cmds() -> impl IntoIterator { .help("Shows whether color hex codes are shown on cards"), command!(show_color_short, Toggle => "cfg_card_show_color_hex_update") .help("Toggles showing color hex codes on cards"), - command!(proxy_switch => "cfg_proxy_switch_show").help("Shows the proxy switch behavior"), - command!(proxy_switch, ProxySwitchAction => "cfg_proxy_switch_update") - .help("Sets the proxy switch behavior"), - command!(proxy_switch_short => "cfg_proxy_switch_show") - .help("Shows the proxy switch behavior"), - command!(proxy_switch_short, ProxySwitchAction => "cfg_proxy_switch_update") - .help("Sets the proxy switch behavior"), + ]; + + let format = "format"; + let name_format = tokens!(cfg, "name", format); + let name_format_short = tokens!(cfg, ("nameformat", ["nf"])); + let name_formatting = [ command!(name_format => "cfg_name_format_show").help("Shows the name format"), command!(name_format, RESET => "cfg_name_format_reset").help("Resets the name format"), command!(name_format, ("format", OpaqueString) => "cfg_name_format_update") @@ -176,6 +175,20 @@ pub fn cmds() -> impl IntoIterator { .help("Resets the name format"), command!(name_format_short, ("format", OpaqueString) => "cfg_name_format_update") .help("Sets the name format"), + ]; + + let server = "server"; + let server_name_format = tokens!(cfg, server, "name", format); + let server_format = tokens!( + cfg, + ("server", ["servername"]), + ("format", ["nameformat", "nf"]) + ); + let server_format_short = tokens!( + cfg, + ("snf", ["servernf", "servernameformat", "snameformat"]) + ); + let server_name_formatting = [ command!(server_name_format => "cfg_server_name_format_show") .help("Shows the server name format"), command!(server_name_format, RESET => "cfg_server_name_format_reset") @@ -194,8 +207,28 @@ pub fn cmds() -> impl IntoIterator { .help("Resets the server name format"), command!(server_format_short, ("format", OpaqueString) => "cfg_server_name_format_update") .help("Sets the server name format"), + ]; + + let limit_ = ("limit", ["lim"]); + let member_limit = tokens!(cfg, ("member", ["mem"]), limit_); + let group_limit = tokens!(cfg, ("group", ["grp"]), limit_); + let limit = tokens!(cfg, limit_); + let limits = [ command!(member_limit => "cfg_limits_update").help("Refreshes member/group limits"), command!(group_limit => "cfg_limits_update").help("Refreshes member/group limits"), command!(limit => "cfg_limits_update").help("Refreshes member/group limits"), - ] + ]; + + base.into_iter() + .chain(autoproxy) + .chain(timezone) + .chain(ping) + .chain(privacy) + .chain(private_info) + .chain(proxy_settings) + .chain(id_settings) + .chain(color_settings) + .chain(name_formatting) + .chain(server_name_formatting) + .chain(limits) } From e2b354aae1f53ed86b7ceca26ae189f08b4f7fc5 Mon Sep 17 00:00:00 2001 From: dawn <90008@gaze.systems> Date: Sat, 17 Jan 2026 11:29:42 +0300 Subject: [PATCH 161/179] add missing help text and redo the existing ones based on CommandHelp.cs --- crates/command_definitions/src/api.rs | 5 ++- crates/command_definitions/src/autoproxy.rs | 8 ++-- crates/command_definitions/src/config.rs | 37 ++++++++++--------- crates/command_definitions/src/debug.rs | 9 +++-- crates/command_definitions/src/fun.rs | 21 ++++++----- crates/command_definitions/src/group.rs | 19 ++++++---- crates/command_definitions/src/help.rs | 8 ++-- .../command_definitions/src/import_export.rs | 6 ++- crates/command_definitions/src/member.rs | 23 ++++++------ crates/command_definitions/src/message.rs | 4 +- crates/command_definitions/src/random.rs | 22 ++++++++--- .../command_definitions/src/server_config.rs | 32 ++++++++-------- crates/command_definitions/src/switch.rs | 26 +++++++++---- crates/command_definitions/src/system.rs | 21 ++++++++--- 14 files changed, 142 insertions(+), 99 deletions(-) diff --git a/crates/command_definitions/src/api.rs b/crates/command_definitions/src/api.rs index d2e669b9..82fca08b 100644 --- a/crates/command_definitions/src/api.rs +++ b/crates/command_definitions/src/api.rs @@ -2,7 +2,8 @@ use super::*; pub fn cmds() -> impl IntoIterator { [ - command!("token" => "token_display"), - command!("token", ("refresh", ["renew", "regen", "reroll"]) => "token_refresh"), + command!("token" => "token_display").help("Gets your system's API token"), + command!("token", ("refresh", ["renew", "regen", "reroll"]) => "token_refresh") + .help("Generates a new API token and invalidates the old one"), ] } diff --git a/crates/command_definitions/src/autoproxy.rs b/crates/command_definitions/src/autoproxy.rs index f7d4e5dc..fd65be01 100644 --- a/crates/command_definitions/src/autoproxy.rs +++ b/crates/command_definitions/src/autoproxy.rs @@ -10,11 +10,11 @@ pub fn cmds() -> impl IntoIterator { [ command!(ap => "autoproxy_show").help("Shows your current autoproxy settings"), command!(ap, ("off", ["stop", "cancel", "no", "disable", "remove"]) => "autoproxy_off") - .help("Disables autoproxy"), + .help("Disables autoproxying for your system in the current server"), command!(ap, ("latch", ["last", "proxy", "stick", "sticky", "l"]) => "autoproxy_latch") - .help("Sets autoproxy to latch mode"), + .help("Sets your system's autoproxy in this server to proxy the last manually proxied member"), command!(ap, ("front", ["fronter", "switch", "f"]) => "autoproxy_front") - .help("Sets autoproxy to front mode"), - command!(ap, MemberRef => "autoproxy_member").help("Sets autoproxy to a specific member"), + .help("Sets your system's autoproxy in this server to proxy the first member currently registered as front"), + command!(ap, MemberRef => "autoproxy_member").help("Sets your system's autoproxy in this server to proxy a specific member"), ] } diff --git a/crates/command_definitions/src/config.rs b/crates/command_definitions/src/config.rs index 68723301..68bd245f 100644 --- a/crates/command_definitions/src/config.rs +++ b/crates/command_definitions/src/config.rs @@ -14,13 +14,13 @@ pub fn cmds() -> impl IntoIterator { command!(ap_account => "cfg_ap_account_show") .help("Shows autoproxy status for the account"), command!(ap_account, Toggle => "cfg_ap_account_update") - .help("Toggles autoproxy for the account"), + .help("Toggles autoproxy globally for the current account"), command!(ap_timeout => "cfg_ap_timeout_show").help("Shows the autoproxy timeout"), command!(ap_timeout, RESET => "cfg_ap_timeout_reset").help("Resets the autoproxy timeout"), command!(ap_timeout, parameter::Toggle::Off => "cfg_ap_timeout_off") .help("Disables the autoproxy timeout"), command!(ap_timeout, ("timeout", OpaqueString) => "cfg_ap_timeout_update") - .help("Sets the autoproxy timeout"), + .help("Sets the latch timeout duration for your system"), ]; let timezone_tokens = tokens!(cfg, ("timezone", ["zone", "tz"])); @@ -28,13 +28,14 @@ pub fn cmds() -> impl IntoIterator { command!(timezone_tokens => "cfg_timezone_show").help("Shows the system timezone"), command!(timezone_tokens, RESET => "cfg_timezone_reset").help("Resets the system timezone"), command!(timezone_tokens, ("timezone", OpaqueString) => "cfg_timezone_update") - .help("Sets the system timezone"), + .help("Changes your system's time zone"), ]; let ping_tokens = tokens!(cfg, "ping"); let ping = [ - command!(ping_tokens => "cfg_ping_show").help("Shows the ping setting"), - command!(ping_tokens, Toggle => "cfg_ping_update").help("Updates the ping setting"), + command!(ping_tokens => "cfg_ping_show").help("Shows ping preferences"), + command!(ping_tokens, Toggle => "cfg_ping_update") + .help("Changes your system's ping preferences"), ]; let priv_ = ("private", ["priv"]); @@ -46,19 +47,19 @@ pub fn cmds() -> impl IntoIterator { command!(member_privacy => "cfg_member_privacy_show") .help("Shows the default privacy for new members"), command!(member_privacy, Toggle => "cfg_member_privacy_update") - .help("Sets the default privacy for new members"), + .help("Sets whether member privacy is automatically set to private when creating a new member"), command!(member_privacy_short => "cfg_member_privacy_show") .help("Shows the default privacy for new members"), command!(member_privacy_short, Toggle => "cfg_member_privacy_update") - .help("Sets the default privacy for new members"), + .help("Sets whether member privacy is automatically set to private when creating a new member"), command!(group_privacy => "cfg_group_privacy_show") .help("Shows the default privacy for new groups"), command!(group_privacy, Toggle => "cfg_group_privacy_update") - .help("Sets the default privacy for new groups"), + .help("Sets whether group privacy is automatically set to private when creating a new group"), command!(group_privacy_short => "cfg_group_privacy_show") .help("Shows the default privacy for new groups"), command!(group_privacy_short, Toggle => "cfg_group_privacy_update") - .help("Sets the default privacy for new groups"), + .help("Sets whether group privacy is automatically set to private when creating a new group"), ]; let show = "show"; @@ -68,11 +69,11 @@ pub fn cmds() -> impl IntoIterator { command!(show_private => "cfg_show_private_info_show") .help("Shows whether private info is shown"), command!(show_private, Toggle => "cfg_show_private_info_update") - .help("Toggles showing private info"), + .help("Sets whether private information is shown to linked accounts by default"), command!(show_private_short => "cfg_show_private_info_show") .help("Shows whether private info is shown"), command!(show_private_short, Toggle => "cfg_show_private_info_update") - .help("Toggles showing private info"), + .help("Sets whether private information is shown to linked accounts by default"), ]; let proxy = ("proxy", ["px"]); @@ -96,11 +97,11 @@ pub fn cmds() -> impl IntoIterator { .help("Toggles proxy error messages"), command!(proxy_switch => "cfg_proxy_switch_show").help("Shows the proxy switch behavior"), command!(proxy_switch, ProxySwitchAction => "cfg_proxy_switch_update") - .help("Sets the proxy switch behavior"), + .help("Sets the switching behavior when proxy tags are used"), command!(proxy_switch_short => "cfg_proxy_switch_show") .help("Shows the proxy switch behavior"), command!(proxy_switch_short, ProxySwitchAction => "cfg_proxy_switch_update") - .help("Sets the proxy switch behavior"), + .help("Sets the switching behavior when proxy tags are used"), ]; let id = ("id", ["ids"]); @@ -169,12 +170,12 @@ pub fn cmds() -> impl IntoIterator { command!(name_format => "cfg_name_format_show").help("Shows the name format"), command!(name_format, RESET => "cfg_name_format_reset").help("Resets the name format"), command!(name_format, ("format", OpaqueString) => "cfg_name_format_update") - .help("Sets the name format"), + .help("Changes your system's username formatting"), command!(name_format_short => "cfg_name_format_show").help("Shows the name format"), command!(name_format_short, RESET => "cfg_name_format_reset") .help("Resets the name format"), command!(name_format_short, ("format", OpaqueString) => "cfg_name_format_update") - .help("Sets the name format"), + .help("Changes your system's username formatting"), ]; let server = "server"; @@ -194,19 +195,19 @@ pub fn cmds() -> impl IntoIterator { command!(server_name_format, RESET => "cfg_server_name_format_reset") .help("Resets the server name format"), command!(server_name_format, ("format", OpaqueString) => "cfg_server_name_format_update") - .help("Sets the server name format"), + .help("Changes your system's username formatting in the current server"), command!(server_format => "cfg_server_name_format_show") .help("Shows the server name format"), command!(server_format, RESET => "cfg_server_name_format_reset") .help("Resets the server name format"), command!(server_format, ("format", OpaqueString) => "cfg_server_name_format_update") - .help("Sets the server name format"), + .help("Changes your system's username formatting in the current server"), command!(server_format_short => "cfg_server_name_format_show") .help("Shows the server name format"), command!(server_format_short, RESET => "cfg_server_name_format_reset") .help("Resets the server name format"), command!(server_format_short, ("format", OpaqueString) => "cfg_server_name_format_update") - .help("Sets the server name format"), + .help("Changes your system's username formatting in the current server"), ]; let limit_ = ("limit", ["lim"]); diff --git a/crates/command_definitions/src/debug.rs b/crates/command_definitions/src/debug.rs index afd204e4..ac449eb3 100644 --- a/crates/command_definitions/src/debug.rs +++ b/crates/command_definitions/src/debug.rs @@ -8,8 +8,11 @@ pub fn cmds() -> impl IntoIterator { let debug = debug(); let perms = ("permissions", ["perms", "permcheck"]); [ - command!(debug, perms, ("channel", ["ch"]), ChannelRef => "permcheck_channel"), - command!(debug, perms, ("guild", ["g"]), GuildRef => "permcheck_guild"), - command!(debug, ("proxy", ["proxying", "proxycheck"]), MessageRef => "message_proxy_check"), + command!(debug, perms, ("channel", ["ch"]), ChannelRef => "permcheck_channel") + .help("Checks if PluralKit has the required permissions in a channel"), + command!(debug, perms, ("guild", ["g"]), GuildRef => "permcheck_guild") + .help("Checks whether a server's permission setup is correct"), + command!(debug, ("proxy", ["proxying", "proxycheck"]), MessageRef => "message_proxy_check") + .help("Checks why a message has not been proxied"), ] } diff --git a/crates/command_definitions/src/fun.rs b/crates/command_definitions/src/fun.rs index 9c0e80e4..99423269 100644 --- a/crates/command_definitions/src/fun.rs +++ b/crates/command_definitions/src/fun.rs @@ -2,15 +2,16 @@ use super::*; pub fn cmds() -> impl IntoIterator { [ - command!("thunder" => "fun_thunder"), - command!("meow" => "fun_meow"), - command!("mn" => "fun_pokemon"), - command!("fire" => "fun_fire"), - command!("freeze" => "fun_freeze"), - command!("starstorm" => "fun_starstorm"), - command!("flash" => "fun_flash"), - command!("rool" => "fun_rool"), - command!("sus" => "amogus"), - command!("error" => "fun_error"), + command!("thunder" => "fun_thunder").help("Vanquishes your opponent with a lightning bolt"), + command!("meow" => "fun_meow").help("mrrp :3"), + command!("mn" => "fun_pokemon").help("Gotta catch 'em all!"), + command!("fire" => "fun_fire").help("Engulfs your opponent in a pillar of fire"), + command!("freeze" => "fun_freeze").help("Freezes your opponent solid"), + command!("starstorm" => "fun_starstorm") + .help("Summons a storm of meteors to strike your opponent"), + command!("flash" => "fun_flash").help("Explodes your opponent with a ball of green light"), + command!("rool" => "fun_rool").help("\"What the fuck is a Pokémon?\""), + command!("sus" => "amogus").help("ඞ"), + command!("error" => "fun_error").help("Shows a fake error message"), ] } diff --git a/crates/command_definitions/src/group.rs b/crates/command_definitions/src/group.rs index 3d845b90..2de9883c 100644 --- a/crates/command_definitions/src/group.rs +++ b/crates/command_definitions/src/group.rs @@ -27,7 +27,7 @@ pub fn cmds() -> impl Iterator { let group_info_cmd = once( command!(group_target => "group_info") .flag(ALL) - .help("Shows information about a group"), + .help("Looks up information about a group"), ); let group_name = tokens!( @@ -40,7 +40,7 @@ pub fn cmds() -> impl Iterator { .flag(YES) .help("Clears the group's name"), command!(group_name, Remainder(("name", OpaqueString)) => "group_rename") - .help("Renames the group"), + .help("Renames a group"), ]; let group_display_name = tokens!(group_target, ("displayname", ["dn", "nick", "nickname"])); @@ -81,17 +81,17 @@ pub fn cmds() -> impl Iterator { .flag(YES) .help("Clears the group's icon"), command!(group_icon, ("icon", Avatar) => "group_change_icon") - .help("Changes the group's icon"), + .help("Changes a group's icon"), ]; let group_banner = tokens!(group_target, ("banner", ["splash", "cover"])); let group_banner_cmd = [ - command!(group_banner => "group_show_banner").help("Shows the group's banner"), + command!(group_banner => "group_show_banner").help("Sets the group's banner image"), command!(group_banner, CLEAR => "group_clear_banner") .flag(YES) .help("Clears the group's banner"), command!(group_banner, ("banner", Avatar) => "group_change_banner") - .help("Changes the group's banner"), + .help("Sets the group's banner image"), ]; let group_color = tokens!(group_target, ("color", ["colour"])); @@ -101,7 +101,7 @@ pub fn cmds() -> impl Iterator { .flag(YES) .help("Clears the group's color"), command!(group_color, ("color", OpaqueString) => "group_change_color") - .help("Changes the group's color"), + .help("Changes a group's color"), ]; let group_privacy = tokens!(group_target, ("privacy", ["priv"])); @@ -127,14 +127,15 @@ pub fn cmds() -> impl Iterator { let group_delete_cmd = [ command!(group_target, ("delete", ["destroy", "erase", "yeet"]) => "group_delete") .flag(YES) - .help("Deletes the group"), + .help("Deletes a group"), ]; - let group_id_cmd = [command!(group_target, "id" => "group_id").help("Shows the group's ID")]; + let group_id_cmd = [command!(group_target, "id" => "group_id").help("Prints a group's ID")]; let group_front = tokens!(group_target, ("front", ["fronter", "fronters", "f"])); let group_front_cmd = [ command!(group_front, ("percent", ["p", "%"]) => "group_fronter_percent") + .help("Shows a group's front breakdown") .flag(("duration", OpaqueString)) .flag(("fronters-only", ["fo"])) .flag("flat"), @@ -149,8 +150,10 @@ pub fn cmds() -> impl Iterator { let group_modify_members_cmd = [ command!(group_target, "add", Optional(MemberRefs) => "group_add_member") + .help("Adds one or more members to a group") .flag(ALL).flag(YES), command!(group_target, ("remove", ["rem", "rm"]), Optional(MemberRefs) => "group_remove_member") + .help("Removes one or more members from a group") .flag(ALL).flag(YES), ]; diff --git a/crates/command_definitions/src/help.rs b/crates/command_definitions/src/help.rs index 793934db..0f32658e 100644 --- a/crates/command_definitions/src/help.rs +++ b/crates/command_definitions/src/help.rs @@ -3,9 +3,11 @@ use super::*; pub fn cmds() -> impl IntoIterator { let help = ("help", ["h"]); [ - command!(("commands", ["cmd", "c"]), ("subject", OpaqueString) => "commands_list"), - command!(("dashboard", ["dash"]) => "dashboard"), - command!("explain" => "explain"), + command!(("commands", ["cmd", "c"]), ("subject", OpaqueString) => "commands_list") + .help("Lists all commands or commands in a specific category"), + command!(("dashboard", ["dash"]) => "dashboard") + .help("Gets a link to the PluralKit web dashboard"), + command!("explain" => "explain").help("Explains the basics of systems and proxying"), command!(help => "help").help("Shows the help command"), command!(help, "commands" => "help_commands").help("help commands"), command!(help, "proxy" => "help_proxy").help("help proxy"), diff --git a/crates/command_definitions/src/import_export.rs b/crates/command_definitions/src/import_export.rs index bf095263..9a73e7a2 100644 --- a/crates/command_definitions/src/import_export.rs +++ b/crates/command_definitions/src/import_export.rs @@ -2,7 +2,9 @@ use super::*; pub fn cmds() -> impl IntoIterator { [ - command!("import", Optional(Remainder(("url", OpaqueString))) => "import").flag(YES), - command!("export" => "export"), + command!("import", Optional(Remainder(("url", OpaqueString))) => "import") + .help("Imports system information from a data file") + .flag(YES), + command!("export" => "export").help("Exports system information to a file"), ] } diff --git a/crates/command_definitions/src/member.rs b/crates/command_definitions/src/member.rs index e466a26a..4b140edf 100644 --- a/crates/command_definitions/src/member.rs +++ b/crates/command_definitions/src/member.rs @@ -36,14 +36,13 @@ pub fn cmds() -> impl Iterator { let delete = ("delete", ["del", "remove"]); let member_new_cmd = once( - command!(member, new, ("name", OpaqueString) => "member_new") - .help("Creates a new system member"), + command!(member, new, ("name", OpaqueString) => "member_new").help("Creates a new member"), ); let member_info_cmd = once( command!(member_target => "member_show") .flag("pt") - .help("Shows information about a member"), + .help("Looks up information about a member"), ); let member_name_cmd = { @@ -52,7 +51,7 @@ pub fn cmds() -> impl Iterator { command!(member_name => "member_name_show").help("Shows a member's name"), command!(member_name, Remainder(("name", OpaqueString)) => "member_name_update") .flag(YES) - .help("Changes a member's name"), + .help("Renames a member"), ] }; @@ -99,7 +98,7 @@ pub fn cmds() -> impl Iterator { [ command!(member_banner => "member_banner_show").help("Shows a member's banner image"), command!(member_banner, ("banner", Avatar) => "member_banner_update") - .help("Changes a member's banner image"), + .help("Sets the member's banner image"), command!(member_banner, CLEAR => "member_banner_clear") .flag(YES) .help("Clears a member's banner image"), @@ -149,7 +148,7 @@ pub fn cmds() -> impl Iterator { command!(member_server_name => "member_servername_show") .help("Shows a member's server name"), command!(member_server_name, Remainder(("name", OpaqueString)) => "member_servername_update") - .help("Changes a member's server name"), + .help("Changes a member's display name in the current server"), command!(member_server_name, CLEAR => "member_servername_clear") .flag(YES) .help("Clears a member's server name"), @@ -180,14 +179,14 @@ pub fn cmds() -> impl Iterator { command!(member_keep_proxy => "member_keepproxy_show") .help("Shows a member's keep-proxy setting"), command!(member_keep_proxy, ("value", Toggle) => "member_keepproxy_update") - .help("Changes a member's keep-proxy setting"), + .help("Sets whether to include a member's proxy tags when proxying"), command!(member_server_keep_proxy => "member_server_keepproxy_show") .help("Shows a member's server-specific keep-proxy setting"), command!(member_server_keep_proxy, CLEAR => "member_server_keepproxy_clear") .flag(YES) .help("Clears a member's server-specific keep-proxy setting"), command!(member_server_keep_proxy, ("value", Toggle) => "member_server_keepproxy_update") - .help("Changes a member's server-specific keep-proxy setting"), + .help("Sets whether to include a member's proxy tags when proxying in the current server"), ] }; @@ -198,11 +197,11 @@ pub fn cmds() -> impl Iterator { command!(member_tts => "member_tts_show") .help("Shows whether a member's messages are sent as TTS"), command!(member_tts, ("value", Toggle) => "member_tts_update") - .help("Changes whether a member's messages are sent as TTS"), + .help("Sets whether to send a member's messages as text-to-speech messages"), command!(member_autoproxy => "member_autoproxy_show") .help("Shows whether a member can be autoproxied"), command!(member_autoproxy, ("value", Toggle) => "member_autoproxy_update") - .help("Changes whether a member can be autoproxied"), + .help("Sets whether a member will be autoproxied when autoproxy is set to latch or front mode"), ] }; @@ -275,7 +274,7 @@ pub fn cmds() -> impl Iterator { command!(member_server_avatar => "member_server_avatar_show") .help("Shows a member's server-specific avatar"), command!(member_server_avatar, ("avatar", Avatar) => "member_server_avatar_update") - .help("Changes a member's server-specific avatar"), + .help("Changes a member's avatar in the current server"), command!(member_server_avatar, CLEAR => "member_server_avatar_clear") .flag(YES) .help("Clears a member's server-specific avatar"), @@ -295,7 +294,7 @@ pub fn cmds() -> impl Iterator { ]; let member_display_id_cmd = - [command!(member_target, "id" => "member_id").help("Displays a member's ID")]; + [command!(member_target, "id" => "member_id").help("Prints a member's ID")]; let member_delete_cmd = [command!(member_target, delete => "member_delete").help("Deletes a member")]; diff --git a/crates/command_definitions/src/message.rs b/crates/command_definitions/src/message.rs index 812b059d..9c930e4d 100644 --- a/crates/command_definitions/src/message.rs +++ b/crates/command_definitions/src/message.rs @@ -16,13 +16,13 @@ pub fn cmds() -> impl IntoIterator { .flag(("no-space", ["nospace", "ns"])) .flag(("clear-embeds", ["clear-embed", "ce"])) .flag(("clear-attachments", ["clear-attachment", "ca"])) - .help("Edits a proxied message") + .help("Edits a previously proxied message") }; [ apply_edit(command!(edit, Optional(MessageRef), new_content_param => "message_edit")), command!(reproxy, Optional(("msg", MessageRef)), ("member", MemberRef) => "message_reproxy") - .help("Reproxies a message with a different member"), + .help("Reproxies a previously proxied message with a different member"), command!(message, author => "message_author").help("Shows the author of a proxied message"), command!(message, delete => "message_delete").help("Deletes a proxied message"), apply_edit(command!(message, edit, new_content_param => "message_edit")), diff --git a/crates/command_definitions/src/random.rs b/crates/command_definitions/src/random.rs index a5892cc9..e24df15c 100644 --- a/crates/command_definitions/src/random.rs +++ b/crates/command_definitions/src/random.rs @@ -7,12 +7,22 @@ pub fn cmds() -> impl Iterator { let group = group::group(); [ - command!(random => "random_self").flag(group), - command!(random, group => "random_group_self"), - command!(random, group::targeted() => "random_group_member_self").flags(get_list_flags()), - command!(system::targeted(), random => "system_random").flag(group), - command!(system::targeted(), random, group => "system_random_group"), - command!(group::targeted(), random => "group_random_member").flags(get_list_flags()), + command!(random => "random_self") + .help("Shows the info card of a randomly selected member in your system") + .flag(group), + command!(random, group => "random_group_self") + .help("Shows the info card of a randomly selected group in your system"), + command!(random, group::targeted() => "random_group_member_self") + .help("Shows the info card of a randomly selected member in a group in your system") + .flags(get_list_flags()), + command!(system::targeted(), random => "system_random") + .help("Shows the info card of a randomly selected member in a system") + .flag(group), + command!(system::targeted(), random, group => "system_random_group") + .help("Shows the info card of a randomly selected group in a system"), + command!(group::targeted(), random => "group_random_member") + .help("Shows the info card of a randomly selected member in a group") + .flags(get_list_flags()), ] .into_iter() .map(|cmd| cmd.flag(ALL)) diff --git a/crates/command_definitions/src/server_config.rs b/crates/command_definitions/src/server_config.rs index 366b1236..ceca4db9 100644 --- a/crates/command_definitions/src/server_config.rs +++ b/crates/command_definitions/src/server_config.rs @@ -50,7 +50,7 @@ pub fn cmds() -> impl Iterator { command!(log_channel => "server_config_log_channel_show") .help("Shows the current log channel"), command!(log_channel, ("channel", ChannelRef) => "server_config_log_channel_set") - .help("Sets the log channel"), + .help("Designates a channel to post proxied messages to"), command!(log_channel, CLEAR => "server_config_log_channel_clear") .flag(YES) .help("Clears the log channel"), @@ -60,11 +60,11 @@ pub fn cmds() -> impl Iterator { command!(log_cleanup => "server_config_log_cleanup_show") .help("Shows whether log cleanup is enabled"), command!(log_cleanup, Toggle => "server_config_log_cleanup_set") - .help("Enables or disables log cleanup"), + .help("Toggles whether to clean up other bots' log channels"), command!(log_cleanup_short => "server_config_log_cleanup_show") .help("Shows whether log cleanup is enabled"), command!(log_cleanup_short, Toggle => "server_config_log_cleanup_set") - .help("Enables or disables log cleanup"), + .help("Toggles whether to clean up other bots' log channels"), ]; let log_blacklist_cmds = [ @@ -72,10 +72,10 @@ pub fn cmds() -> impl Iterator { .help("Shows channels where logging is disabled"), command!(log_blacklist, add, Optional(("channel", ChannelRef)) => "server_config_log_blacklist_add") .flag(ALL) - .help("Adds a channel (or all channels with --all) to the log blacklist"), + .help("Disables message logging in certain channels"), command!(log_blacklist, remove, Optional(("channel", ChannelRef)) => "server_config_log_blacklist_remove") .flag(ALL) - .help("Removes a channel (or all channels with --all) from the log blacklist"), + .help("Enables message logging in certain channels"), ]; let proxy_blacklist_cmds = [ @@ -83,43 +83,45 @@ pub fn cmds() -> impl Iterator { .help("Shows channels where proxying is disabled"), command!(proxy_blacklist, add, Optional(("channel", ChannelRef)) => "server_config_proxy_blacklist_add") .flag(ALL) - .help("Adds a channel (or all channels with --all) to the proxy blacklist"), + .help("Disables message proxying in certain channels"), command!(proxy_blacklist, remove, Optional(("channel", ChannelRef)) => "server_config_proxy_blacklist_remove") .flag(ALL) - .help("Removes a channel (or all channels with --all) from the proxy blacklist"), + .help("Enables message proxying in certain channels"), ]; let invalid_cmds = [ command!(invalid => "server_config_invalid_command_response_show") .help("Shows whether error responses for invalid commands are enabled"), command!(invalid, Toggle => "server_config_invalid_command_response_set") - .help("Enables or disables error responses for invalid commands"), + .help("Sets whether to show an error message when an unknown command is sent"), command!(invalid_short => "server_config_invalid_command_response_show") .help("Shows whether error responses for invalid commands are enabled"), command!(invalid_short, Toggle => "server_config_invalid_command_response_set") - .help("Enables or disables error responses for invalid commands"), + .help("Sets whether to show an error message when an unknown command is sent"), ]; let require_tag_cmds = [ command!(require_tag => "server_config_require_system_tag_show") .help("Shows whether system tags are required"), - command!(require_tag, Toggle => "server_config_require_system_tag_set") - .help("Requires or unrequires system tags for proxied messages"), + command!(require_tag, Toggle => "server_config_require_system_tag_set").help( + "Sets whether server users are required to have a system tag on proxied messages", + ), command!(require_tag_short => "server_config_require_system_tag_show") .help("Shows whether system tags are required"), - command!(require_tag_short, Toggle => "server_config_require_system_tag_set") - .help("Requires or unrequires system tags for proxied messages"), + command!(require_tag_short, Toggle => "server_config_require_system_tag_set").help( + "Sets whether server users are required to have a system tag on proxied messages", + ), ]; let suppress_cmds = [ command!(suppress => "server_config_suppress_notifications_show") .help("Shows whether notifications are suppressed for proxied messages"), command!(suppress, Toggle => "server_config_suppress_notifications_set") - .help("Enables or disables notification suppression for proxied messages"), + .help("Sets whether all proxied messages will have notifications suppressed (sent as `@silent` messages)"), command!(suppress_short => "server_config_suppress_notifications_show") .help("Shows whether notifications are suppressed for proxied messages"), command!(suppress_short, Toggle => "server_config_suppress_notifications_set") - .help("Enables or disables notification suppression for proxied messages"), + .help("Sets whether all proxied messages will have notifications suppressed (sent as `@silent` messages)"), ]; let main_cmd = once( diff --git a/crates/command_definitions/src/switch.rs b/crates/command_definitions/src/switch.rs index c52525ad..ab87c6ec 100644 --- a/crates/command_definitions/src/switch.rs +++ b/crates/command_definitions/src/switch.rs @@ -17,13 +17,23 @@ pub fn cmds() -> impl IntoIterator { ]; [ - command!(switch, ("commands", ["help"]) => "switch_commands"), - command!(switch, out => "switch_out"), - command!(switch, delete => "switch_delete").flag(("all", ["clear", "c"])), - command!(switch, r#move, Remainder(OpaqueString) => "switch_move"), // TODO: datetime parsing - command!(switch, edit, out => "switch_edit_out").flag(YES), - command!(switch, edit, Optional(MemberRefs) => "switch_edit").flags(edit_flags), - command!(switch, copy, Optional(MemberRefs) => "switch_copy").flags(edit_flags), - command!(switch, MemberRefs => "switch_do"), + command!(switch, ("commands", ["help"]) => "switch_commands") + .help("Shows help for switch commands"), + command!(switch, out => "switch_out").help("Registers a switch with no members"), + command!(switch, delete => "switch_delete") + .help("Deletes the latest switch") + .flag(("all", ["clear", "c"])), + command!(switch, r#move, Remainder(OpaqueString) => "switch_move") + .help("Moves the latest switch in time"), // TODO: datetime parsing + command!(switch, edit, out => "switch_edit_out") + .help("Turns the latest switch into a switch-out") + .flag(YES), + command!(switch, edit, Optional(MemberRefs) => "switch_edit") + .help("Edits the members in the latest switch") + .flags(edit_flags), + command!(switch, copy, Optional(MemberRefs) => "switch_copy") + .help("Makes a new switch with the listed members added") + .flags(edit_flags), + command!(switch, MemberRefs => "switch_do").help("Registers a switch"), ] } diff --git a/crates/command_definitions/src/system.rs b/crates/command_definitions/src/system.rs index ec25f7db..6938bd56 100644 --- a/crates/command_definitions/src/system.rs +++ b/crates/command_definitions/src/system.rs @@ -215,16 +215,20 @@ pub fn edit() -> impl Iterator { let front = ("front", ["fronter", "fronters", "f"]); let make_front_history = |subcmd: TokensIterator| { - command!(system, Optional(SystemRef), subcmd => "system_fronter_history").flag(CLEAR) + command!(system, Optional(SystemRef), subcmd => "system_fronter_history") + .help("Shows a system's front history") + .flag(CLEAR) }; let make_front_percent = |subcmd: TokensIterator| { command!(system, Optional(SystemRef), subcmd => "system_fronter_percent") + .help("Shows a system's front breakdown") .flag(("duration", OpaqueString)) .flag(("fronters-only", ["fo"])) .flag("flat") }; let system_front_cmd = [ - command!(system, Optional(SystemRef), front => "system_fronter"), + command!(system, Optional(SystemRef), front => "system_fronter") + .help("Shows a system's fronter(s)"), make_front_history(tokens!(front, ("history", ["h"]))), make_front_history(tokens!(("fronthistory", ["fh"]))), make_front_percent(tokens!(front, ("percent", ["p", "%"]))), @@ -243,8 +247,10 @@ pub fn edit() -> impl Iterator { once(command!(system, Optional(SystemRef), "groups", search_param => "system_groups")) .map(apply_list_opts); - let system_display_id_cmd = - once(command!(system, Optional(SystemRef), "id" => "system_display_id")); + let system_display_id_cmd = once( + command!(system, Optional(SystemRef), "id" => "system_display_id") + .help("Prints a system's ID"), + ); let system_delete = once( command!(system, ("delete", ["erase", "remove", "yeet"]) => "system_delete") @@ -253,8 +259,11 @@ pub fn edit() -> impl Iterator { ); let system_link = [ - command!("link", ("account", UserRef) => "system_link"), - command!("unlink", ("account", OpaqueString) => "system_unlink").flag(YES), + command!("link", ("account", UserRef) => "system_link") + .help("Links another Discord account to your system"), + command!("unlink", ("account", OpaqueString) => "system_unlink") + .help("Unlinks a Discord account from your system") + .flag(YES), ]; system_new_cmd From c5ce6fb6c0d249a00fe2bdf3fe8b8d50dd128d59 Mon Sep 17 00:00:00 2001 From: dawn <90008@gaze.systems> Date: Sat, 17 Jan 2026 15:40:07 +0300 Subject: [PATCH 162/179] fix issue with optional branches having wrong flag insert positions --- crates/command_parser/src/command.rs | 3 +++ crates/command_parser/src/lib.rs | 11 +++++++++-- crates/command_parser/src/tree.rs | 20 ++++++++++++++++---- 3 files changed, 28 insertions(+), 6 deletions(-) diff --git a/crates/command_parser/src/command.rs b/crates/command_parser/src/command.rs index 83b57ae0..3bbf636d 100644 --- a/crates/command_parser/src/command.rs +++ b/crates/command_parser/src/command.rs @@ -1,6 +1,7 @@ use std::{ collections::HashSet, fmt::{Debug, Display}, + sync::Arc, }; use smol_str::SmolStr; @@ -17,6 +18,7 @@ pub struct Command { pub show_in_suggestions: bool, pub parse_flags_before: usize, pub hidden_flags: HashSet, + pub original: Option>, } impl Command { @@ -41,6 +43,7 @@ impl Command { parse_flags_before, tokens, hidden_flags: HashSet::new(), + original: None, } } diff --git a/crates/command_parser/src/lib.rs b/crates/command_parser/src/lib.rs index 18de3446..e1b5b79b 100644 --- a/crates/command_parser/src/lib.rs +++ b/crates/command_parser/src/lib.rs @@ -181,15 +181,18 @@ pub fn parse_command( let mut flags: HashMap> = HashMap::new(); let mut misplaced_flags: Vec = Vec::new(); let mut invalid_flags: Vec = Vec::new(); + for (token_idx, raw_flag) in raw_flags { let Some(matched_flag) = match_flag(command.flags.iter(), raw_flag.clone()) else { invalid_flags.push(raw_flag); continue; }; + if token_idx != command.parse_flags_before { misplaced_flags.push(raw_flag); continue; } + match matched_flag { // a flag was matched Ok((name, value)) => { @@ -216,6 +219,8 @@ pub fn parse_command( } } } + + let full_cmd = command.original.as_deref().unwrap_or(&command); if misplaced_flags.is_empty().not() { let mut error = format!( "Flag{} ", @@ -230,7 +235,8 @@ pub fn parse_command( write!( &mut error, " in command `{prefix}{input}` {} misplaced. Try reordering to match the command usage `{prefix}{command}`.", - (misplaced_flags.len() > 1).then_some("are").unwrap_or("is") + (misplaced_flags.len() > 1).then_some("are").unwrap_or("is"), + command = full_cmd ).expect("oom"); return Err(error); } @@ -250,7 +256,8 @@ pub fn parse_command( " {} seem to be applicable in this command (`{prefix}{command}`).", (invalid_flags.len() > 1) .then_some("don't") - .unwrap_or("doesn't") + .unwrap_or("doesn't"), + command = full_cmd ) .expect("oom"); return Err(error); diff --git a/crates/command_parser/src/tree.rs b/crates/command_parser/src/tree.rs index 42939a3b..84fcca7f 100644 --- a/crates/command_parser/src/tree.rs +++ b/crates/command_parser/src/tree.rs @@ -1,3 +1,5 @@ +use std::sync::Arc; + use ordermap::OrderMap; use crate::{command::Command, token::Token}; @@ -27,10 +29,20 @@ impl TreeBranch { if matches!(token, Token::Parameter(ref param) if param.is_optional()) && index < command.tokens.len() - 1 { - current_branch.register_command(Command { - tokens: command.tokens[index + 1..].to_vec(), - ..command.clone() - }); + let mut new_command = command.clone(); + new_command.tokens = command.tokens[index + 1..].to_vec(); + new_command.original = command + .original + .clone() + .or_else(|| Some(Arc::new(command.clone()))); + + // if the optional parameter we're skipping is *before* the flag insertion point, + // we need to shift the index left by 1 to account for the removed token + if new_command.parse_flags_before > index { + new_command.parse_flags_before -= 1; + } + + current_branch.register_command(new_command); } // recursively get or create a sub-branch for each token current_branch = current_branch From aec01d62459fc7dd8b17563e05223e1a904f6fe8 Mon Sep 17 00:00:00 2001 From: dawn <90008@gaze.systems> Date: Sun, 18 Jan 2026 21:24:31 +0300 Subject: [PATCH 163/179] add g alias for member_groups --- crates/command_definitions/src/member.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/command_definitions/src/member.rs b/crates/command_definitions/src/member.rs index 4b140edf..1589e6da 100644 --- a/crates/command_definitions/src/member.rs +++ b/crates/command_definitions/src/member.rs @@ -281,7 +281,7 @@ pub fn cmds() -> impl Iterator { ] }; - let member_group = tokens!(member_target, ("groups", ["group"])); + let member_group = tokens!(member_target, ("groups", ["group", "g"])); let member_list_group_cmds = once( command!(member_group, Optional(Remainder(("query", OpaqueString))) => "member_groups"), ) From 5daa4777f56ec569b5723b4d02f2db74337b110d Mon Sep 17 00:00:00 2001 From: dawn <90008@gaze.systems> Date: Sun, 18 Jan 2026 22:09:58 +0300 Subject: [PATCH 164/179] fix pk;e taking in id, only take message ref --- crates/command_definitions/src/debug.rs | 4 +- crates/command_definitions/src/message.rs | 8 +- crates/command_parser/src/parameter.rs | 101 ++++++++++++++-------- crates/commands/src/write_cs_glue.rs | 4 +- 4 files changed, 73 insertions(+), 44 deletions(-) diff --git a/crates/command_definitions/src/debug.rs b/crates/command_definitions/src/debug.rs index ac449eb3..52f50307 100644 --- a/crates/command_definitions/src/debug.rs +++ b/crates/command_definitions/src/debug.rs @@ -1,3 +1,5 @@ +use command_parser::parameter::MESSAGE_REF; + use super::*; pub fn debug() -> (&'static str, [&'static str; 1]) { @@ -12,7 +14,7 @@ pub fn cmds() -> impl IntoIterator { .help("Checks if PluralKit has the required permissions in a channel"), command!(debug, perms, ("guild", ["g"]), GuildRef => "permcheck_guild") .help("Checks whether a server's permission setup is correct"), - command!(debug, ("proxy", ["proxying", "proxycheck"]), MessageRef => "message_proxy_check") + command!(debug, ("proxy", ["proxying", "proxycheck"]), MESSAGE_REF => "message_proxy_check") .help("Checks why a message has not been proxied"), ] } diff --git a/crates/command_definitions/src/message.rs b/crates/command_definitions/src/message.rs index 9c930e4d..8531efd4 100644 --- a/crates/command_definitions/src/message.rs +++ b/crates/command_definitions/src/message.rs @@ -1,7 +1,9 @@ +use command_parser::parameter::{MESSAGE_LINK, MESSAGE_REF}; + use super::*; pub fn cmds() -> impl IntoIterator { - let message = tokens!(("message", ["msg", "messageinfo"]), Optional(MessageRef)); + let message = tokens!(("message", ["msg", "messageinfo"]), Optional(MESSAGE_REF)); let author = ("author", ["sender", "a"]); let delete = ("delete", ["del", "d"]); @@ -20,8 +22,8 @@ pub fn cmds() -> impl IntoIterator { }; [ - apply_edit(command!(edit, Optional(MessageRef), new_content_param => "message_edit")), - command!(reproxy, Optional(("msg", MessageRef)), ("member", MemberRef) => "message_reproxy") + apply_edit(command!(edit, Optional(MESSAGE_LINK), new_content_param => "message_edit")), + command!(reproxy, Optional(("msg", MESSAGE_REF)), ("member", MemberRef) => "message_reproxy") .help("Reproxies a previously proxied message with a different member"), command!(message, author => "message_author").help("Shows the author of a proxied message"), command!(message, delete => "message_delete").help("Deletes a proxied message"), diff --git a/crates/command_parser/src/parameter.rs b/crates/command_parser/src/parameter.rs index 6c6d63c6..6b8e7e5f 100644 --- a/crates/command_parser/src/parameter.rs +++ b/crates/command_parser/src/parameter.rs @@ -8,6 +8,15 @@ use smol_str::{SmolStr, format_smolstr}; use crate::token::{Token, TokenMatchResult}; +pub const MESSAGE_REF: ParameterKind = ParameterKind::MessageRef { + id: true, + link: true, +}; +pub const MESSAGE_LINK: ParameterKind = ParameterKind::MessageRef { + id: false, + link: true, +}; + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum ParameterKind { OpaqueString, @@ -18,7 +27,7 @@ pub enum ParameterKind { GroupRefs, SystemRef, UserRef, - MessageRef, + MessageRef { id: bool, link: bool }, ChannelRef, GuildRef, MemberPrivacyTarget, @@ -41,7 +50,7 @@ impl ParameterKind { ParameterKind::GroupRefs => "targets", ParameterKind::SystemRef => "target", ParameterKind::UserRef => "target", - ParameterKind::MessageRef => "target", + ParameterKind::MessageRef { .. } => "target", ParameterKind::ChannelRef => "target", ParameterKind::GuildRef => "target", ParameterKind::MemberPrivacyTarget => "member_privacy_target", @@ -153,48 +162,56 @@ impl Parameter { Toggle::from_str(input).map(|t| ParameterValue::Toggle(t.into())) } ParameterKind::Avatar => Ok(ParameterValue::Avatar(input.into())), - ParameterKind::MessageRef => { - if let Ok(message_id) = input.parse::() { - return Ok(ParameterValue::MessageRef(None, None, message_id)); + ParameterKind::MessageRef { id, link } => { + if id { + if let Ok(message_id) = input.parse::() { + return Ok(ParameterValue::MessageRef(None, None, message_id)); + } } - static SERVER_RE: std::sync::LazyLock = std::sync::LazyLock::new( - || { - regex::Regex::new( - r"https://(?:\w+\.)?discord(?:app)?\.com/channels/(?P\d+)/(?P\d+)/(?P\d+)", - ) - .unwrap() - }, - ); + if link { + static SERVER_RE: std::sync::LazyLock = std::sync::LazyLock::new( + || { + regex::Regex::new( + r"https://(?:\w+\.)?discord(?:app)?\.com/channels/(?P\d+)/(?P\d+)/(?P\d+)", + ) + .unwrap() + }, + ); - static DM_RE: std::sync::LazyLock = std::sync::LazyLock::new(|| { - regex::Regex::new( - r"https://(?:\w+\.)?discord(?:app)?\.com/channels/@me/(?P\d+)/(?P\d+)", - ) - .unwrap() - }); + static DM_RE: std::sync::LazyLock = std::sync::LazyLock::new( + || { + regex::Regex::new( + r"https://(?:\w+\.)?discord(?:app)?\.com/channels/@me/(?P\d+)/(?P\d+)", + ) + .unwrap() + }, + ); - if let Some(captures) = SERVER_RE.captures(input) { - let guild_id = captures.parse_id("guild")?; - let channel_id = captures.parse_id("channel")?; - let message_id = captures.parse_id("message")?; + if let Some(captures) = SERVER_RE.captures(input) { + let guild_id = captures.parse_id("guild")?; + let channel_id = captures.parse_id("channel")?; + let message_id = captures.parse_id("message")?; - Ok(ParameterValue::MessageRef( - Some(guild_id), - Some(channel_id), - message_id, - )) - } else if let Some(captures) = DM_RE.captures(input) { - let channel_id = captures.parse_id("channel")?; - let message_id = captures.parse_id("message")?; + Ok(ParameterValue::MessageRef( + Some(guild_id), + Some(channel_id), + message_id, + )) + } else if let Some(captures) = DM_RE.captures(input) { + let channel_id = captures.parse_id("channel")?; + let message_id = captures.parse_id("message")?; - Ok(ParameterValue::MessageRef( - None, - Some(channel_id), - message_id, - )) + Ok(ParameterValue::MessageRef( + None, + Some(channel_id), + message_id, + )) + } else { + Err(SmolStr::new("invalid message reference")) + } } else { - Err(SmolStr::new("invalid message reference")) + unreachable!("link and id both cant be false") } } ParameterKind::ChannelRef => { @@ -234,7 +251,15 @@ impl Display for Parameter { ParameterKind::GroupRefs => write!(f, " "), ParameterKind::SystemRef => write!(f, ""), ParameterKind::UserRef => write!(f, ""), - ParameterKind::MessageRef => write!(f, ""), + ParameterKind::MessageRef { link, id } => write!( + f, + "", + link.then_some("link") + .into_iter() + .chain(id.then_some("id")) + .collect::>() + .join("/") + ), ParameterKind::ChannelRef => write!(f, ""), ParameterKind::GuildRef => write!(f, ""), ParameterKind::MemberPrivacyTarget => write!(f, ""), diff --git a/crates/commands/src/write_cs_glue.rs b/crates/commands/src/write_cs_glue.rs index 6d6fb414..6d693f20 100644 --- a/crates/commands/src/write_cs_glue.rs +++ b/crates/commands/src/write_cs_glue.rs @@ -276,7 +276,7 @@ fn get_param_ty(kind: ParameterKind) -> &'static str { ParameterKind::PrivacyLevel => "PrivacyLevel", ParameterKind::Toggle => "bool", ParameterKind::Avatar => "ParsedImage", - ParameterKind::MessageRef => "Message.Reference", + ParameterKind::MessageRef { .. } => "Message.Reference", ParameterKind::ChannelRef => "Channel", ParameterKind::GuildRef => "Guild", ParameterKind::ProxySwitchAction => "SystemConfig.ProxySwitchAction", @@ -299,7 +299,7 @@ fn get_param_param_ty(kind: ParameterKind) -> &'static str { ParameterKind::PrivacyLevel => "PrivacyLevel", ParameterKind::Toggle => "Toggle", ParameterKind::Avatar => "Avatar", - ParameterKind::MessageRef => "Message", + ParameterKind::MessageRef { .. } => "Message", ParameterKind::ChannelRef => "Channel", ParameterKind::GuildRef => "Guild", ParameterKind::ProxySwitchAction => "ProxySwitchAction", From f72145b3dbecc78282c2e8f0ecf59534f0751a41 Mon Sep 17 00:00:00 2001 From: dawn <90008@gaze.systems> Date: Sun, 18 Jan 2026 23:09:27 +0300 Subject: [PATCH 165/179] add members command to root --- crates/command_definitions/src/system.rs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/crates/command_definitions/src/system.rs b/crates/command_definitions/src/system.rs index 6938bd56..d2d4fc35 100644 --- a/crates/command_definitions/src/system.rs +++ b/crates/command_definitions/src/system.rs @@ -238,10 +238,13 @@ pub fn edit() -> impl Iterator { let search_param = Optional(Remainder(("query", OpaqueString))); let apply_list_opts = |cmd: Command| cmd.flags(get_list_flags()); - let members_subcmd = tokens!(("members", ["ls", "list"]), search_param); - let system_members_cmd = - once(command!(system, Optional(SystemRef), members_subcmd => "system_members")) - .map(apply_list_opts); + let members_subcmd = tokens!(("members", ["l", "ls", "list"]), search_param); + let system_members_cmd = [ + command!(system, Optional(SystemRef), members_subcmd => "system_members") + .help("Lists a system's members"), + command!(members_subcmd => "system_members").help("Lists your system's members"), + ] + .map(apply_list_opts); let system_groups_cmd = once(command!(system, Optional(SystemRef), "groups", search_param => "system_groups")) From dc9b7b3e6bd08a3f6b1f25bb897dcd2ac42989a5 Mon Sep 17 00:00:00 2001 From: dawn <90008@gaze.systems> Date: Sun, 18 Jan 2026 23:34:58 +0300 Subject: [PATCH 166/179] use original command when ranking for possible commands if the command was from an optional branch --- crates/command_definitions/src/system.rs | 8 +++++--- crates/command_parser/src/lib.rs | 3 ++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/crates/command_definitions/src/system.rs b/crates/command_definitions/src/system.rs index d2d4fc35..60c9a2cc 100644 --- a/crates/command_definitions/src/system.rs +++ b/crates/command_definitions/src/system.rs @@ -246,9 +246,11 @@ pub fn edit() -> impl Iterator { ] .map(apply_list_opts); - let system_groups_cmd = - once(command!(system, Optional(SystemRef), "groups", search_param => "system_groups")) - .map(apply_list_opts); + let system_groups_cmd = once( + command!(system, Optional(SystemRef), "groups", search_param => "system_groups") + .help("Lists groups in a system"), + ) + .map(apply_list_opts); let system_display_id_cmd = once( command!(system, Optional(SystemRef), "id" => "system_display_id") diff --git a/crates/command_parser/src/lib.rs b/crates/command_parser/src/lib.rs index e1b5b79b..c6322ddd 100644 --- a/crates/command_parser/src/lib.rs +++ b/crates/command_parser/src/lib.rs @@ -219,7 +219,7 @@ pub fn parse_command( } } } - + let full_cmd = command.original.as_deref().unwrap_or(&command); if misplaced_flags.is_empty().not() { let mut error = format!( @@ -346,6 +346,7 @@ fn rank_possible_commands( ) -> Vec<(Command, String, bool)> { let mut commands_with_scores: Vec<(&Command, String, f64, bool)> = possible_commands .into_iter() + .map(|cmd| cmd.original.as_deref().unwrap_or(cmd)) .filter(|cmd| cmd.show_in_suggestions) .flat_map(|cmd| { let versions = generate_command_versions(cmd); From 9122e64a414dd501fc3c9d407dcd2217b5deffc8 Mon Sep 17 00:00:00 2001 From: dawn <90008@gaze.systems> Date: Mon, 19 Jan 2026 00:35:01 +0300 Subject: [PATCH 167/179] fix command suggestions breaking if specifying a parameter --- crates/command_parser/src/lib.rs | 43 +++++++++++++++++-- crates/command_parser/tests/ranking.rs | 57 ++++++++++++++++++++++++++ 2 files changed, 97 insertions(+), 3 deletions(-) create mode 100644 crates/command_parser/tests/ranking.rs diff --git a/crates/command_parser/src/lib.rs b/crates/command_parser/src/lib.rs index c6322ddd..4310ce77 100644 --- a/crates/command_parser/src/lib.rs +++ b/crates/command_parser/src/lib.rs @@ -52,6 +52,14 @@ pub fn parse_command( let mut filtered_tokens: Vec = Vec::new(); let mut last_optional_param_error: Option<(SmolStr, SmolStr)> = None; + // track the best attempt at parsing (deepest matched tokens) + // so we can use it for error messages/suggestions even if we backtrack later + let mut best_attempt: Option<( + Tree, + Vec<(Tree, (Token, TokenMatchResult, usize), usize)>, + usize, + )> = None; + loop { let mut possible_tokens = local_tree .possible_tokens() @@ -115,6 +123,17 @@ pub fn parse_command( (found_token.clone(), result.clone(), *new_pos), current_pos, )); + + // update best attempt if we're deeper + if best_attempt.as_ref().map(|x| x.1.len()).unwrap_or(0) < matched_tokens.len() + { + best_attempt = Some(( + next_tree.clone(), + matched_tokens.clone(), + *new_pos, + )); + } + filtered_tokens.clear(); // new branch, new tokens local_tree = next_tree.clone(); } else { @@ -145,10 +164,29 @@ pub fn parse_command( )); } + // restore best attempt if it's deeper than current state + // this helps when we backtracked out of the correct path because of a later error + if let Some((best_tree, best_matched, best_pos)) = best_attempt { + if best_matched.len() > matched_tokens.len() { + local_tree = best_tree; + matched_tokens = best_matched; + current_pos = best_pos; + } + } + let mut error = format!("Unknown command `{prefix}{input}`."); - let possible_commands = - rank_possible_commands(&input, local_tree.possible_commands(usize::MAX)); + // normalize input by replacing parameters with placeholders + let mut normalized_input = String::new(); + for (_, (token, _, _), _) in &matched_tokens { + write!(&mut normalized_input, "{token} ").unwrap(); + } + normalized_input.push_str(&input[current_pos..].trim_start()); + + let possible_commands = rank_possible_commands( + &normalized_input, + local_tree.possible_commands(usize::MAX), + ); if possible_commands.is_empty().not() { error.push_str(" Perhaps you meant one of the following commands:\n"); fmt_commands_list(&mut error, &prefix, possible_commands); @@ -339,7 +377,6 @@ fn next_token<'a>( } // todo: should probably move this somewhere else -/// returns true if wrote possible commands, false if not fn rank_possible_commands( input: &str, possible_commands: impl IntoIterator, diff --git a/crates/command_parser/tests/ranking.rs b/crates/command_parser/tests/ranking.rs new file mode 100644 index 00000000..0f185fc7 --- /dev/null +++ b/crates/command_parser/tests/ranking.rs @@ -0,0 +1,57 @@ +use command_parser::{parse_command, Tree, command::Command, parameter::*, tokens}; + +#[test] +fn test_typoed_command_with_parameter() { + let message_token = ("message", ["msg", "messageinfo"]); + let author_token = ("author", ["sender", "a"]); + + // message author + let cmd = Command::new( + tokens!(message_token, Optional(MESSAGE_REF), author_token), + "message_author" + ).help("Shows the author of a proxied message"); + + let mut tree = Tree::default(); + tree.register_command(cmd); + + let input = "message 1 auth"; + let result = parse_command(tree, "pk;".to_string(), input.to_string()); + + match result { + Ok(_) => panic!("Should have failed to parse"), + Err(msg) => { + println!("Error: {}", msg); + assert!(msg.contains("Perhaps you meant one of the following commands")); + assert!(msg.contains("message author")); + } + } +} + +#[test] +fn test_typoed_command_with_flags() { + let message_token = ("message", ["msg", "messageinfo"]); + let author_token = ("author", ["sender", "a"]); + + let cmd = Command::new( + tokens!(message_token, author_token), + "message_author" + ) + .flag(("flag", ["f"])) + .flag(("flag2", ["f2"])) + .help("Shows the author of a proxied message"); + + let mut tree = Tree::default(); + tree.register_command(cmd); + + let input = "message auth -f -flag2"; + let result = parse_command(tree, "pk;".to_string(), input.to_string()); + + match result { + Ok(_) => panic!("Should have failed to parse"), + Err(msg) => { + println!("Error: {}", msg); + assert!(msg.contains("Perhaps you meant one of the following commands")); + assert!(msg.contains("message author")); + } + } +} \ No newline at end of file From f9367ea04141e597d0f41c1e822af0af7d4b5b07 Mon Sep 17 00:00:00 2001 From: dawn <90008@gaze.systems> Date: Mon, 19 Jan 2026 17:49:06 +0300 Subject: [PATCH 168/179] fix infinite loop backtracking for valid branches, cleanup matched tokens type --- crates/command_parser/src/lib.rs | 65 ++++++++++++++++---------- crates/command_parser/tests/parser.rs | 45 ++++++++++++++++++ crates/command_parser/tests/ranking.rs | 2 +- 3 files changed, 86 insertions(+), 26 deletions(-) create mode 100644 crates/command_parser/tests/parser.rs diff --git a/crates/command_parser/src/lib.rs b/crates/command_parser/src/lib.rs index 4310ce77..239b9531 100644 --- a/crates/command_parser/src/lib.rs +++ b/crates/command_parser/src/lib.rs @@ -33,6 +33,15 @@ pub struct ParsedCommand { pub flags: HashMap>, } +#[derive(Clone, Debug)] +struct MatchedTokenState { + tree: Tree, + token: Token, + match_result: TokenMatchResult, + start_pos: usize, + filtered_tokens: Vec, +} + pub fn parse_command( command_tree: Tree, prefix: String, @@ -44,21 +53,16 @@ pub fn parse_command( // end position of all currently matched tokens let mut current_pos: usize = 0; let mut current_token_idx: usize = 0; - - let mut params: HashMap = HashMap::new(); let mut raw_flags: Vec<(usize, MatchedFlag)> = Vec::new(); - let mut matched_tokens: Vec<(Tree, (Token, TokenMatchResult, usize), usize)> = Vec::new(); - let mut filtered_tokens: Vec = Vec::new(); + let mut matched_tokens: Vec = Vec::new(); + let mut filtered_tokens: Vec = Vec::new(); // these are tokens that we've already tried (and failed) + let mut last_optional_param_error: Option<(SmolStr, SmolStr)> = None; // track the best attempt at parsing (deepest matched tokens) // so we can use it for error messages/suggestions even if we backtrack later - let mut best_attempt: Option<( - Tree, - Vec<(Tree, (Token, TokenMatchResult, usize), usize)>, - usize, - )> = None; + let mut best_attempt: Option<(Tree, Vec, usize)> = None; loop { let mut possible_tokens = local_tree @@ -111,18 +115,21 @@ pub fn parse_command( } } - // add parameter if any - if let TokenMatchResult::MatchedParameter { name, value } = result { - params.insert(name.to_string(), value.clone()); + if let TokenMatchResult::MatchedParameter { .. } = result { + // we don't add params here, but wait until we matched a full command + // then we can use matched_tokens to extract the params + // this is so we don't have to keep track of "params" when trying branches } // move to the next branch if let Some(next_tree) = local_tree.get_branch(&found_token) { - matched_tokens.push(( - local_tree.clone(), - (found_token.clone(), result.clone(), *new_pos), - current_pos, - )); + matched_tokens.push(MatchedTokenState { + tree: local_tree.clone(), + token: found_token.clone(), + match_result: result.clone(), + start_pos: current_pos, + filtered_tokens: filtered_tokens.clone(), + }); // update best attempt if we're deeper if best_attempt.as_ref().map(|x| x.1.len()).unwrap_or(0) < matched_tokens.len() @@ -147,14 +154,15 @@ pub fn parse_command( None => { // redo the previous branches if we didnt match on a parameter // this is a bit of a hack, but its necessary for making parameters on the same depth work - if let Some((match_tree, match_next, old_pos)) = matched_tokens + if let Some(state) = matched_tokens .pop() - .and_then(|m| matches!(m.1, (Token::Parameter(_), _, _)).then_some(m)) + .and_then(|m| matches!(m.token, Token::Parameter(_)).then_some(m)) { - println!("redoing previous branch: {:?}", match_next.0); - local_tree = match_tree; - current_pos = old_pos; // reset position to previous branch's start - filtered_tokens.push(match_next.0); + println!("redoing previous branch: {:?}", state.token); + local_tree = state.tree; + current_pos = state.start_pos; // reset position to previous branch's start + filtered_tokens = state.filtered_tokens; // reset filtered tokens to the previous branch's + filtered_tokens.push(state.token); continue; } @@ -178,8 +186,8 @@ pub fn parse_command( // normalize input by replacing parameters with placeholders let mut normalized_input = String::new(); - for (_, (token, _, _), _) in &matched_tokens { - write!(&mut normalized_input, "{token} ").unwrap(); + for state in &matched_tokens { + write!(&mut normalized_input, "{} ", state.token).unwrap(); } normalized_input.push_str(&input[current_pos..].trim_start()); @@ -220,6 +228,13 @@ pub fn parse_command( let mut misplaced_flags: Vec = Vec::new(); let mut invalid_flags: Vec = Vec::new(); + let mut params: HashMap = HashMap::new(); + for state in &matched_tokens { + if let TokenMatchResult::MatchedParameter { name, value } = &state.match_result { + params.insert(name.to_string(), value.clone()); + } + } + for (token_idx, raw_flag) in raw_flags { let Some(matched_flag) = match_flag(command.flags.iter(), raw_flag.clone()) else { invalid_flags.push(raw_flag); diff --git a/crates/command_parser/tests/parser.rs b/crates/command_parser/tests/parser.rs new file mode 100644 index 00000000..75df16b7 --- /dev/null +++ b/crates/command_parser/tests/parser.rs @@ -0,0 +1,45 @@ +use command_parser::{parse_command, Tree, command::Command, parameter::*, tokens}; + +/// this checks if we properly keep track of filtered tokens (eg. branches we failed on) +/// when we backtrack. a previous parser bug would cause infinite loops since it did not +/// (the parser would "flip-flop" between branches) this is here for reference. +#[test] +fn test_infinite_loop_repro() { + let p1 = Optional(("param1", ParameterKind::OpaqueString)); + let p2 = Optional(("param2", ParameterKind::OpaqueString)); + + let cmd1 = Command::new(tokens!("s", p1, "A"), "cmd1"); + let cmd2 = Command::new(tokens!("s", p2, "B"), "cmd2"); + + let mut tree = Tree::default(); + tree.register_command(cmd1); + tree.register_command(cmd2); + + let input = "s foo C"; + // this should fail and not loop + let result = parse_command(tree, "pk;".to_string(), input.to_string()); + assert!(result.is_err()); +} + +/// check if we have params from other branches when we trying to match them and they succeeded +/// but then we backtracked, making them invalid. this should no longer happen since we just +/// extract params from matched tokens when we match the command, but keeping here just for reference. +#[test] +fn test_dirty_params() { + let p1 = Optional(("param1", ParameterKind::OpaqueString)); + let p2 = Optional(("param2", ParameterKind::OpaqueString)); + + let cmd1 = Command::new(tokens!("s", p1, "A"), "cmd1"); + let cmd2 = Command::new(tokens!("s", p2, "B"), "cmd2"); + + let mut tree = Tree::default(); + tree.register_command(cmd1); + tree.register_command(cmd2); + + let input = "s foo B"; + let result = parse_command(tree, "pk;".to_string(), input.to_string()).unwrap(); + + println!("params: {:?}", result.parameters); + assert!(!result.parameters.contains_key("param1"), "params should not contain 'param1' from failed branch"); + assert!(result.parameters.contains_key("param2"), "params should contain 'param2'"); +} diff --git a/crates/command_parser/tests/ranking.rs b/crates/command_parser/tests/ranking.rs index 0f185fc7..bcee3a51 100644 --- a/crates/command_parser/tests/ranking.rs +++ b/crates/command_parser/tests/ranking.rs @@ -54,4 +54,4 @@ fn test_typoed_command_with_flags() { assert!(msg.contains("message author")); } } -} \ No newline at end of file +} From c18e72450b4e2a01dfd2abf736615fc8b9089e01 Mon Sep 17 00:00:00 2001 From: dawn <90008@gaze.systems> Date: Mon, 19 Jan 2026 17:57:50 +0300 Subject: [PATCH 169/179] use Arc throught the parser --- crates/command_parser/src/lib.rs | 12 +++++++----- crates/command_parser/src/tree.rs | 24 +++++++++++++----------- crates/commands/src/lib.rs | 8 ++++---- 3 files changed, 24 insertions(+), 20 deletions(-) diff --git a/crates/command_parser/src/lib.rs b/crates/command_parser/src/lib.rs index 239b9531..8a3695b2 100644 --- a/crates/command_parser/src/lib.rs +++ b/crates/command_parser/src/lib.rs @@ -2,6 +2,8 @@ #![feature(round_char_boundary)] #![feature(iter_intersperse)] +use std::sync::Arc; + pub mod command; pub mod flag; pub mod parameter; @@ -28,14 +30,14 @@ pub type Tree = tree::TreeBranch; #[derive(Debug)] pub struct ParsedCommand { - pub command_def: Command, + pub command_def: Arc, pub parameters: HashMap, pub flags: HashMap>, } #[derive(Clone, Debug)] struct MatchedTokenState { - tree: Tree, + tree: Arc, token: Token, match_result: TokenMatchResult, start_pos: usize, @@ -43,12 +45,12 @@ struct MatchedTokenState { } pub fn parse_command( - command_tree: Tree, + command_tree: impl Into>, prefix: String, input: String, ) -> Result { let input: SmolStr = input.into(); - let mut local_tree: Tree = command_tree.clone(); + let mut local_tree = command_tree.into(); // end position of all currently matched tokens let mut current_pos: usize = 0; @@ -62,7 +64,7 @@ pub fn parse_command( // track the best attempt at parsing (deepest matched tokens) // so we can use it for error messages/suggestions even if we backtrack later - let mut best_attempt: Option<(Tree, Vec, usize)> = None; + let mut best_attempt: Option<(Arc, Vec, usize)> = None; loop { let mut possible_tokens = local_tree diff --git a/crates/command_parser/src/tree.rs b/crates/command_parser/src/tree.rs index 84fcca7f..022ef981 100644 --- a/crates/command_parser/src/tree.rs +++ b/crates/command_parser/src/tree.rs @@ -6,8 +6,8 @@ use crate::{command::Command, token::Token}; #[derive(Debug, Clone)] pub struct TreeBranch { - current_command: Option, - branches: OrderMap, + current_command: Option>, + branches: OrderMap>, } impl Default for TreeBranch { @@ -45,16 +45,18 @@ impl TreeBranch { current_branch.register_command(new_command); } // recursively get or create a sub-branch for each token - current_branch = current_branch - .branches - .entry(token) - .or_insert_with(TreeBranch::default); + current_branch = Arc::make_mut( + current_branch + .branches + .entry(token) + .or_insert_with(|| Arc::new(TreeBranch::default())), + ); } // when we're out of tokens add the command to the last branch - current_branch.current_command = Some(command); + current_branch.current_command = Some(Arc::new(command)); } - pub fn command(&self) -> Option { + pub fn command(&self) -> Option> { self.current_command.clone() } @@ -76,7 +78,7 @@ impl TreeBranch { let mut commands = box_iter(std::iter::empty()); for branch in self.branches.values() { if let Some(command) = branch.current_command.as_ref() { - commands = box_iter(commands.chain(std::iter::once(command))); + commands = box_iter(commands.chain(std::iter::once(command.as_ref()))); // we dont need to look further if we found a command continue; } @@ -85,11 +87,11 @@ impl TreeBranch { commands } - pub fn get_branch(&self, token: &Token) -> Option<&Self> { + pub fn get_branch(&self, token: &Token) -> Option<&Arc> { self.branches.get(token) } - pub fn branches(&self) -> impl Iterator { + pub fn branches(&self) -> impl Iterator)> { self.branches.iter() } } diff --git a/crates/commands/src/lib.rs b/crates/commands/src/lib.rs index 08a094f2..32869cab 100644 --- a/crates/commands/src/lib.rs +++ b/crates/commands/src/lib.rs @@ -1,16 +1,16 @@ -use std::{collections::HashMap, fmt::Write}; +use std::{collections::HashMap, fmt::Write, sync::Arc}; use command_parser::{parameter::ParameterValue, token::TokenMatchResult, Tree}; uniffi::include_scaffolding!("commands"); lazy_static::lazy_static! { - pub static ref COMMAND_TREE: Tree = { + pub static ref COMMAND_TREE: Arc = { let mut tree = Tree::default(); command_definitions::all().into_iter().for_each(|x| tree.register_command(x)); - tree + Arc::new(tree) }; } @@ -125,7 +125,7 @@ pub fn parse_command(prefix: String, input: String) -> CommandResult { |error| CommandResult::Err { error }, |parsed| CommandResult::Ok { command: { - let command_ref = parsed.command_def.cb.into(); + let command_ref = parsed.command_def.cb.clone().into(); let mut flags = HashMap::with_capacity(parsed.flags.capacity()); for (name, value) in parsed.flags { flags.insert(name, value.map(Parameter::from)); From 728913235a020ead5491ed67b8c5f9f584a7fcb0 Mon Sep 17 00:00:00 2001 From: dawn <90008@gaze.systems> Date: Mon, 19 Jan 2026 18:03:13 +0300 Subject: [PATCH 170/179] system name should also accept rename --- crates/command_definitions/src/system.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/command_definitions/src/system.rs b/crates/command_definitions/src/system.rs index 60c9a2cc..82ff0a2b 100644 --- a/crates/command_definitions/src/system.rs +++ b/crates/command_definitions/src/system.rs @@ -56,7 +56,7 @@ pub fn edit() -> impl Iterator { command!(system, Optional(SystemRef), name => "system_show_name") .help("Shows the systems name"), ); - let system_name_self = tokens!(system, name); + let system_name_self = tokens!(system, ("rename", [name])); let system_name_self_cmd = [ command!(system_name_self, CLEAR => "system_clear_name") .flag(YES) From beec38ef8bc1d245b330789d937d933b9fa374d2 Mon Sep 17 00:00:00 2001 From: dawn <90008@gaze.systems> Date: Mon, 19 Jan 2026 18:44:50 +0300 Subject: [PATCH 171/179] improve command suggestions by matching previous failed branch if parameter, and substitute parameters for better jaro winkler --- crates/command_parser/src/lib.rs | 87 ++++++++++++++++++++------ crates/command_parser/tests/ranking.rs | 2 +- 2 files changed, 70 insertions(+), 19 deletions(-) diff --git a/crates/command_parser/src/lib.rs b/crates/command_parser/src/lib.rs index 8a3695b2..da13bf50 100644 --- a/crates/command_parser/src/lib.rs +++ b/crates/command_parser/src/lib.rs @@ -193,11 +193,48 @@ pub fn parse_command( } normalized_input.push_str(&input[current_pos..].trim_start()); - let possible_commands = rank_possible_commands( + let input_tokens = input.split_whitespace().collect::>(); + let mut possible_commands = rank_possible_commands( &normalized_input, local_tree.possible_commands(usize::MAX), + &input_tokens, ); + + // checks if we matched a parameter last + // if we did, we might have matched a parameter "by accident" (ie. `pk;s renam` matched `s `) + // so we also want to suggest commands from the *previous* branch + if let Some(state) = matched_tokens.last() + && matches!(state.token, Token::Parameter(_)) + { + let mut parent_input = String::new(); + // recreate input string up to the parameter + for parent_state in matched_tokens.iter().take(matched_tokens.len() - 1) { + write!(&mut parent_input, "{} ", parent_state.token).unwrap(); + } + // assume the user intended to type a command here, so we use the raw input + // (eg. `s renam` -> `s renam`) + parent_input.push_str(&input[state.start_pos..].trim_start()); + + let input_tokens = parent_input.split_whitespace().collect::>(); + let parent_commands = rank_possible_commands( + &parent_input, + state.tree.possible_commands(usize::MAX), + &input_tokens, + ); + possible_commands.extend(parent_commands); + + // re-deduplicate + possible_commands.dedup_by(|a, b| { + let cmd_a = a.0.original.as_deref().unwrap_or(&a.0); + let cmd_b = b.0.original.as_deref().unwrap_or(&b.0); + cmd_a == cmd_b + }); + // re-sort after extending + possible_commands.sort_by(|a, b| b.2.partial_cmp(&a.2).unwrap_or(std::cmp::Ordering::Equal)); + } + if possible_commands.is_empty().not() { + error.push_str(" Perhaps you meant one of the following commands:\n"); fmt_commands_list(&mut error, &prefix, possible_commands); } else { @@ -397,16 +434,20 @@ fn next_token<'a>( fn rank_possible_commands( input: &str, possible_commands: impl IntoIterator, -) -> Vec<(Command, String, bool)> { + input_tokens: &[&str], +) -> Vec<(Command, String, f64, bool)> { let mut commands_with_scores: Vec<(&Command, String, f64, bool)> = possible_commands .into_iter() .map(|cmd| cmd.original.as_deref().unwrap_or(cmd)) .filter(|cmd| cmd.show_in_suggestions) .flat_map(|cmd| { - let versions = generate_command_versions(cmd); - versions.into_iter().map(move |(version, is_alias)| { - let similarity = strsim::jaro_winkler(&input, &version); - (cmd, version, similarity, is_alias) + let versions = generate_command_versions(cmd, input_tokens); + versions.into_iter().map(move |(display, scoring, is_alias)| { + let similarity = strsim::jaro_winkler(&input, &scoring); + // if similarity > 0.7 { + // println!("DEBUG: ranking: '{}' vs '{}' = {}", input, scoring, similarity); + // } + (cmd, display, similarity, is_alias) }) }) .collect(); @@ -428,24 +469,23 @@ fn rank_possible_commands( } // if score falls off too much, don't show - let mut falloff_threshold: f64 = 0.2; + let falloff_threshold: f64 = 0.2; let best_score = best_commands[0].2; let mut commands_to_show = Vec::new(); for (command, version, score, is_alias) in best_commands.into_iter().take(MAX_SUGGESTIONS) { let delta = best_score - score; - falloff_threshold -= delta; if delta > falloff_threshold { break; } - commands_to_show.push((command.clone(), version, is_alias)); + commands_to_show.push((command.clone(), version, score, is_alias)); } commands_to_show } -fn fmt_commands_list(f: &mut String, prefix: &str, commands_to_show: Vec<(Command, String, bool)>) { - for (command, version, is_alias) in commands_to_show { +fn fmt_commands_list(f: &mut String, prefix: &str, commands_to_show: Vec<(Command, String, f64, bool)>) { + for (command, version, _, is_alias) in commands_to_show { writeln!( f, "- **{prefix}{version}**{alias} - *{help}*", @@ -453,7 +493,7 @@ fn fmt_commands_list(f: &mut String, prefix: &str, commands_to_show: Vec<(Comman alias = is_alias .then(|| format!( " (alias of **{prefix}{base_version}**)", - base_version = build_command_string(&command, None) + base_version = build_command_string(&command, None, &[]) )) .unwrap_or_else(String::new), ) @@ -461,18 +501,21 @@ fn fmt_commands_list(f: &mut String, prefix: &str, commands_to_show: Vec<(Comman } } -fn generate_command_versions(cmd: &Command) -> Vec<(String, bool)> { +fn generate_command_versions(cmd: &Command, input_tokens: &[&str]) -> Vec<(String, String, bool)> { let mut versions = Vec::new(); // Start with base version using primary names - let base_version = build_command_string(cmd, None); - versions.push((base_version, false)); + let base_display = build_command_string(cmd, None, &[]); + let base_scoring = build_command_string(cmd, None, input_tokens); + versions.push((base_display, base_scoring, false)); // Generate versions for each alias combination for (idx, token) in cmd.tokens.iter().enumerate() { if let Token::Value { aliases, .. } = token { for alias in aliases { - versions.push((build_command_string(cmd, Some((idx, alias.as_str()))), true)); + let display = build_command_string(cmd, Some((idx, alias.as_str())), &[]); + let scoring = build_command_string(cmd, Some((idx, alias.as_str())), input_tokens); + versions.push((display, scoring, true)); } } } @@ -480,7 +523,7 @@ fn generate_command_versions(cmd: &Command) -> Vec<(String, bool)> { versions } -fn build_command_string(cmd: &Command, alias_replacement: Option<(usize, &str)>) -> String { +fn build_command_string(cmd: &Command, alias_replacement: Option<(usize, &str)>, input_tokens: &[&str]) -> String { let mut result = String::new(); for (idx, token) in cmd.tokens.iter().enumerate() { if idx > 0 { @@ -496,7 +539,15 @@ fn build_command_string(cmd: &Command, alias_replacement: Option<(usize, &str)>) Token::Value { name, .. } => { result.push_str(replacement.unwrap_or(name)); } - Token::Parameter(param) => write!(&mut result, "{param}").unwrap(), + Token::Parameter(param) => { + // if we have an input token at this position, use it + // otherwise use the placeholder + if let Some(input_token) = input_tokens.get(idx) { + result.push_str(input_token); + } else { + write!(&mut result, "{param}").unwrap() + } + }, } } result diff --git a/crates/command_parser/tests/ranking.rs b/crates/command_parser/tests/ranking.rs index bcee3a51..0f185fc7 100644 --- a/crates/command_parser/tests/ranking.rs +++ b/crates/command_parser/tests/ranking.rs @@ -54,4 +54,4 @@ fn test_typoed_command_with_flags() { assert!(msg.contains("message author")); } } -} +} \ No newline at end of file From d8043036152eaa132613bb887b3e9755be7ed68d Mon Sep 17 00:00:00 2001 From: dawn <90008@gaze.systems> Date: Mon, 19 Jan 2026 18:46:40 +0300 Subject: [PATCH 172/179] format rust code --- crates/command_parser/src/lib.rs | 46 +++++++++++++++----------- crates/command_parser/tests/parser.rs | 26 +++++++++------ crates/command_parser/tests/ranking.rs | 36 ++++++++++---------- 3 files changed, 59 insertions(+), 49 deletions(-) diff --git a/crates/command_parser/src/lib.rs b/crates/command_parser/src/lib.rs index da13bf50..1c77b88c 100644 --- a/crates/command_parser/src/lib.rs +++ b/crates/command_parser/src/lib.rs @@ -136,11 +136,7 @@ pub fn parse_command( // update best attempt if we're deeper if best_attempt.as_ref().map(|x| x.1.len()).unwrap_or(0) < matched_tokens.len() { - best_attempt = Some(( - next_tree.clone(), - matched_tokens.clone(), - *new_pos, - )); + best_attempt = Some((next_tree.clone(), matched_tokens.clone(), *new_pos)); } filtered_tokens.clear(); // new branch, new tokens @@ -204,7 +200,7 @@ pub fn parse_command( // if we did, we might have matched a parameter "by accident" (ie. `pk;s renam` matched `s `) // so we also want to suggest commands from the *previous* branch if let Some(state) = matched_tokens.last() - && matches!(state.token, Token::Parameter(_)) + && matches!(state.token, Token::Parameter(_)) { let mut parent_input = String::new(); // recreate input string up to the parameter @@ -222,7 +218,7 @@ pub fn parse_command( &input_tokens, ); possible_commands.extend(parent_commands); - + // re-deduplicate possible_commands.dedup_by(|a, b| { let cmd_a = a.0.original.as_deref().unwrap_or(&a.0); @@ -230,11 +226,11 @@ pub fn parse_command( cmd_a == cmd_b }); // re-sort after extending - possible_commands.sort_by(|a, b| b.2.partial_cmp(&a.2).unwrap_or(std::cmp::Ordering::Equal)); + possible_commands + .sort_by(|a, b| b.2.partial_cmp(&a.2).unwrap_or(std::cmp::Ordering::Equal)); } if possible_commands.is_empty().not() { - error.push_str(" Perhaps you meant one of the following commands:\n"); fmt_commands_list(&mut error, &prefix, possible_commands); } else { @@ -442,13 +438,15 @@ fn rank_possible_commands( .filter(|cmd| cmd.show_in_suggestions) .flat_map(|cmd| { let versions = generate_command_versions(cmd, input_tokens); - versions.into_iter().map(move |(display, scoring, is_alias)| { - let similarity = strsim::jaro_winkler(&input, &scoring); - // if similarity > 0.7 { - // println!("DEBUG: ranking: '{}' vs '{}' = {}", input, scoring, similarity); - // } - (cmd, display, similarity, is_alias) - }) + versions + .into_iter() + .map(move |(display, scoring, is_alias)| { + let similarity = strsim::jaro_winkler(&input, &scoring); + // if similarity > 0.7 { + // println!("DEBUG: ranking: '{}' vs '{}' = {}", input, scoring, similarity); + // } + (cmd, display, similarity, is_alias) + }) }) .collect(); @@ -484,7 +482,11 @@ fn rank_possible_commands( commands_to_show } -fn fmt_commands_list(f: &mut String, prefix: &str, commands_to_show: Vec<(Command, String, f64, bool)>) { +fn fmt_commands_list( + f: &mut String, + prefix: &str, + commands_to_show: Vec<(Command, String, f64, bool)>, +) { for (command, version, _, is_alias) in commands_to_show { writeln!( f, @@ -523,7 +525,11 @@ fn generate_command_versions(cmd: &Command, input_tokens: &[&str]) -> Vec<(Strin versions } -fn build_command_string(cmd: &Command, alias_replacement: Option<(usize, &str)>, input_tokens: &[&str]) -> String { +fn build_command_string( + cmd: &Command, + alias_replacement: Option<(usize, &str)>, + input_tokens: &[&str], +) -> String { let mut result = String::new(); for (idx, token) in cmd.tokens.iter().enumerate() { if idx > 0 { @@ -542,12 +548,12 @@ fn build_command_string(cmd: &Command, alias_replacement: Option<(usize, &str)>, Token::Parameter(param) => { // if we have an input token at this position, use it // otherwise use the placeholder - if let Some(input_token) = input_tokens.get(idx) { + if let Some(input_token) = input_tokens.get(idx) { result.push_str(input_token); } else { write!(&mut result, "{param}").unwrap() } - }, + } } } result diff --git a/crates/command_parser/tests/parser.rs b/crates/command_parser/tests/parser.rs index 75df16b7..20813153 100644 --- a/crates/command_parser/tests/parser.rs +++ b/crates/command_parser/tests/parser.rs @@ -1,4 +1,4 @@ -use command_parser::{parse_command, Tree, command::Command, parameter::*, tokens}; +use command_parser::{Tree, command::Command, parameter::*, parse_command, tokens}; /// this checks if we properly keep track of filtered tokens (eg. branches we failed on) /// when we backtrack. a previous parser bug would cause infinite loops since it did not @@ -7,14 +7,14 @@ use command_parser::{parse_command, Tree, command::Command, parameter::*, tokens fn test_infinite_loop_repro() { let p1 = Optional(("param1", ParameterKind::OpaqueString)); let p2 = Optional(("param2", ParameterKind::OpaqueString)); - + let cmd1 = Command::new(tokens!("s", p1, "A"), "cmd1"); let cmd2 = Command::new(tokens!("s", p2, "B"), "cmd2"); - + let mut tree = Tree::default(); tree.register_command(cmd1); tree.register_command(cmd2); - + let input = "s foo C"; // this should fail and not loop let result = parse_command(tree, "pk;".to_string(), input.to_string()); @@ -28,18 +28,24 @@ fn test_infinite_loop_repro() { fn test_dirty_params() { let p1 = Optional(("param1", ParameterKind::OpaqueString)); let p2 = Optional(("param2", ParameterKind::OpaqueString)); - + let cmd1 = Command::new(tokens!("s", p1, "A"), "cmd1"); let cmd2 = Command::new(tokens!("s", p2, "B"), "cmd2"); - + let mut tree = Tree::default(); tree.register_command(cmd1); tree.register_command(cmd2); - + let input = "s foo B"; let result = parse_command(tree, "pk;".to_string(), input.to_string()).unwrap(); - + println!("params: {:?}", result.parameters); - assert!(!result.parameters.contains_key("param1"), "params should not contain 'param1' from failed branch"); - assert!(result.parameters.contains_key("param2"), "params should contain 'param2'"); + assert!( + !result.parameters.contains_key("param1"), + "params should not contain 'param1' from failed branch" + ); + assert!( + result.parameters.contains_key("param2"), + "params should contain 'param2'" + ); } diff --git a/crates/command_parser/tests/ranking.rs b/crates/command_parser/tests/ranking.rs index 0f185fc7..6ddaeed8 100644 --- a/crates/command_parser/tests/ranking.rs +++ b/crates/command_parser/tests/ranking.rs @@ -1,22 +1,23 @@ -use command_parser::{parse_command, Tree, command::Command, parameter::*, tokens}; +use command_parser::{Tree, command::Command, parameter::*, parse_command, tokens}; #[test] fn test_typoed_command_with_parameter() { let message_token = ("message", ["msg", "messageinfo"]); let author_token = ("author", ["sender", "a"]); - + // message author let cmd = Command::new( tokens!(message_token, Optional(MESSAGE_REF), author_token), - "message_author" - ).help("Shows the author of a proxied message"); - + "message_author", + ) + .help("Shows the author of a proxied message"); + let mut tree = Tree::default(); tree.register_command(cmd); - + let input = "message 1 auth"; let result = parse_command(tree, "pk;".to_string(), input.to_string()); - + match result { Ok(_) => panic!("Should have failed to parse"), Err(msg) => { @@ -31,21 +32,18 @@ fn test_typoed_command_with_parameter() { fn test_typoed_command_with_flags() { let message_token = ("message", ["msg", "messageinfo"]); let author_token = ("author", ["sender", "a"]); - - let cmd = Command::new( - tokens!(message_token, author_token), - "message_author" - ) - .flag(("flag", ["f"])) - .flag(("flag2", ["f2"])) - .help("Shows the author of a proxied message"); - + + let cmd = Command::new(tokens!(message_token, author_token), "message_author") + .flag(("flag", ["f"])) + .flag(("flag2", ["f2"])) + .help("Shows the author of a proxied message"); + let mut tree = Tree::default(); tree.register_command(cmd); - + let input = "message auth -f -flag2"; let result = parse_command(tree, "pk;".to_string(), input.to_string()); - + match result { Ok(_) => panic!("Should have failed to parse"), Err(msg) => { @@ -54,4 +52,4 @@ fn test_typoed_command_with_flags() { assert!(msg.contains("message author")); } } -} \ No newline at end of file +} From 8cad05ccda9038da1940226d61895c8bd0e35177 Mon Sep 17 00:00:00 2001 From: dawn <90008@gaze.systems> Date: Tue, 20 Jan 2026 00:04:22 +0300 Subject: [PATCH 173/179] fix edit message requiring new_content param, fix both edit and rp not using referenced message --- PluralKit.Bot/CommandSystem/Context/ContextArgumentsExt.cs | 6 +++--- PluralKit.Bot/Commands/Checks.cs | 5 ++--- PluralKit.Bot/Commands/Message.cs | 6 +++--- crates/command_definitions/src/message.rs | 2 +- 4 files changed, 9 insertions(+), 10 deletions(-) diff --git a/PluralKit.Bot/CommandSystem/Context/ContextArgumentsExt.cs b/PluralKit.Bot/CommandSystem/Context/ContextArgumentsExt.cs index 730b4e8d..d04babb7 100644 --- a/PluralKit.Bot/CommandSystem/Context/ContextArgumentsExt.cs +++ b/PluralKit.Bot/CommandSystem/Context/ContextArgumentsExt.cs @@ -6,11 +6,11 @@ namespace PluralKit.Bot; public static class ContextArgumentsExt { - public static (ulong? messageId, ulong? channelId) GetRepliedTo(this Context ctx) + public static Message.Reference? GetRepliedTo(this Context ctx) { if (ctx.Message.Type == Message.MessageType.Reply && ctx.Message.MessageReference?.MessageId != null) - return (ctx.Message.MessageReference.MessageId, ctx.Message.MessageReference.ChannelId); - return (null, null); + return ctx.Message.MessageReference; + return null; } public static (ulong? messageId, ulong? channelId) ParseMessage(this Context ctx, string maybeMessageRef, bool parseRawMessageId) diff --git a/PluralKit.Bot/Commands/Checks.cs b/PluralKit.Bot/Commands/Checks.cs index 26a53354..8134cbba 100644 --- a/PluralKit.Bot/Commands/Checks.cs +++ b/PluralKit.Bot/Commands/Checks.cs @@ -167,9 +167,8 @@ public class Checks var failedToGetMessage = "Could not find a valid message to check, was not able to fetch the message, or the message was not sent by you."; - var (messageId, channelId) = ctx.GetRepliedTo(); - if (messageReference != null) - (messageId, channelId) = (messageReference.MessageId, messageReference.ChannelId); + messageReference = ctx.GetRepliedTo(); + var (messageId, channelId) = (messageReference?.MessageId, messageReference?.ChannelId); if (messageId == null || channelId == null) throw new PKError(failedToGetMessage); diff --git a/PluralKit.Bot/Commands/Message.cs b/PluralKit.Bot/Commands/Message.cs index b7ac474c..4a73d92c 100644 --- a/PluralKit.Bot/Commands/Message.cs +++ b/PluralKit.Bot/Commands/Message.cs @@ -60,7 +60,7 @@ public class ProxiedMessage public async Task ReproxyMessage(Context ctx, Message.Reference? messageRef, PKMember target) { - var (msg, systemId) = await GetMessageToEdit(ctx, messageRef?.MessageId, ReproxyTimeout, true); + var (msg, systemId) = await GetMessageToEdit(ctx, messageRef?.MessageId ?? ctx.GetRepliedTo()?.MessageId, ReproxyTimeout, true); if (ctx.System.Id != systemId) throw new PKError("Can't reproxy a message sent by a different system."); @@ -91,9 +91,9 @@ public class ProxiedMessage } } - public async Task EditMessage(Context ctx, Message.Reference? messageRef, string newContent, bool useRegex, bool noSpace, bool append, bool prepend, bool clearEmbeds, bool clearAttachments) + public async Task EditMessage(Context ctx, Message.Reference? messageRef, string? newContent, bool useRegex, bool noSpace, bool append, bool prepend, bool clearEmbeds, bool clearAttachments) { - var (msg, systemId) = await GetMessageToEdit(ctx, messageRef?.MessageId, EditTimeout, false); + var (msg, systemId) = await GetMessageToEdit(ctx, messageRef?.MessageId ?? ctx.GetRepliedTo()?.MessageId, EditTimeout, false); if (ctx.System.Id != systemId) throw new PKError("Can't edit a message sent by a different system."); diff --git a/crates/command_definitions/src/message.rs b/crates/command_definitions/src/message.rs index 8531efd4..1f021b27 100644 --- a/crates/command_definitions/src/message.rs +++ b/crates/command_definitions/src/message.rs @@ -10,7 +10,7 @@ pub fn cmds() -> impl IntoIterator { let reproxy = ("reproxy", ["rp", "crimes", "crime"]); let edit = ("edit", ["e"]); - let new_content_param = Remainder(("new_content", OpaqueString)); + let new_content_param = Optional(Remainder(("new_content", OpaqueString))); let apply_edit = |cmd: Command| { cmd.flag(("append", ["a"])) .flag(("prepend", ["p"])) From e1c61d627288111c0a33315b08946e514bf2b0ea Mon Sep 17 00:00:00 2001 From: dawn <90008@gaze.systems> Date: Tue, 20 Jan 2026 00:24:58 +0300 Subject: [PATCH 174/179] add g as groups alias for system ' groups', add 'random member' --- crates/command_definitions/src/random.rs | 2 ++ crates/command_definitions/src/system.rs | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/command_definitions/src/random.rs b/crates/command_definitions/src/random.rs index e24df15c..0c14b002 100644 --- a/crates/command_definitions/src/random.rs +++ b/crates/command_definitions/src/random.rs @@ -5,11 +5,13 @@ use super::*; pub fn cmds() -> impl Iterator { let random = ("random", ["rand"]); let group = group::group(); + let member = member::member(); [ command!(random => "random_self") .help("Shows the info card of a randomly selected member in your system") .flag(group), + command!(random, member => "random_self"), command!(random, group => "random_group_self") .help("Shows the info card of a randomly selected group in your system"), command!(random, group::targeted() => "random_group_member_self") diff --git a/crates/command_definitions/src/system.rs b/crates/command_definitions/src/system.rs index 82ff0a2b..3310efa7 100644 --- a/crates/command_definitions/src/system.rs +++ b/crates/command_definitions/src/system.rs @@ -247,7 +247,7 @@ pub fn edit() -> impl Iterator { .map(apply_list_opts); let system_groups_cmd = once( - command!(system, Optional(SystemRef), "groups", search_param => "system_groups") + command!(system, Optional(SystemRef), ("groups", ["g"]), search_param => "system_groups") .help("Lists groups in a system"), ) .map(apply_list_opts); From 0a3b15eb4579e559cd44cfdce580553f15f96de8 Mon Sep 17 00:00:00 2001 From: dawn <90008@gaze.systems> Date: Tue, 20 Jan 2026 00:39:38 +0300 Subject: [PATCH 175/179] fix servertag show command, add stag and deer as aliases --- crates/command_definitions/src/system.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/command_definitions/src/system.rs b/crates/command_definitions/src/system.rs index 3310efa7..04a73c5c 100644 --- a/crates/command_definitions/src/system.rs +++ b/crates/command_definitions/src/system.rs @@ -121,9 +121,9 @@ pub fn edit() -> impl Iterator { .help("Changes your system's tag"), ]; - let servertag = ("servertag", ["st", "guildtag"]); + let servertag = ("servertag", ["st", "guildtag", "stag", "deer"]); let system_server_tag_cmd = once( - command!(system, Optional(SystemRef) => "system_show_server_tag") + command!(system, Optional(SystemRef), servertag => "system_show_server_tag") .help("Shows the system's server tag"), ); let system_server_tag_self = tokens!(system, servertag); From 516eaa5292f26e24f2a6887d2b6053972cdfbded Mon Sep 17 00:00:00 2001 From: dawn <90008@gaze.systems> Date: Tue, 20 Jan 2026 00:51:30 +0300 Subject: [PATCH 176/179] add x alias for edit -regex --- crates/command_definitions/src/message.rs | 14 +++++++++++--- crates/command_parser/src/command.rs | 15 +++++++++++++-- crates/command_parser/src/lib.rs | 5 +++++ 3 files changed, 29 insertions(+), 5 deletions(-) diff --git a/crates/command_definitions/src/message.rs b/crates/command_definitions/src/message.rs index 1f021b27..00a5767f 100644 --- a/crates/command_definitions/src/message.rs +++ b/crates/command_definitions/src/message.rs @@ -1,4 +1,7 @@ -use command_parser::parameter::{MESSAGE_LINK, MESSAGE_REF}; +use command_parser::{ + parameter::{MESSAGE_LINK, MESSAGE_REF}, + token::TokensIterator, +}; use super::*; @@ -11,6 +14,8 @@ pub fn cmds() -> impl IntoIterator { let edit = ("edit", ["e"]); let new_content_param = Optional(Remainder(("new_content", OpaqueString))); + let edit_short_subcmd = tokens!(Optional(MESSAGE_LINK), new_content_param); + let apply_edit = |cmd: Command| { cmd.flag(("append", ["a"])) .flag(("prepend", ["p"])) @@ -20,14 +25,17 @@ pub fn cmds() -> impl IntoIterator { .flag(("clear-attachments", ["clear-attachment", "ca"])) .help("Edits a previously proxied message") }; + let make_edit_cmd = |tokens: TokensIterator| apply_edit(command!(tokens => "message_edit")); [ - apply_edit(command!(edit, Optional(MESSAGE_LINK), new_content_param => "message_edit")), + make_edit_cmd(tokens!(edit, edit_short_subcmd)), + // this one always does regex + make_edit_cmd(tokens!("x", edit_short_subcmd)).flag_value("regex", None), command!(reproxy, Optional(("msg", MESSAGE_REF)), ("member", MemberRef) => "message_reproxy") .help("Reproxies a previously proxied message with a different member"), command!(message, author => "message_author").help("Shows the author of a proxied message"), command!(message, delete => "message_delete").help("Deletes a proxied message"), - apply_edit(command!(message, edit, new_content_param => "message_edit")), + make_edit_cmd(tokens!(message, edit, new_content_param)), command!(message => "message_info") .flag(delete) .flag(author) diff --git a/crates/command_parser/src/command.rs b/crates/command_parser/src/command.rs index 3bbf636d..88434638 100644 --- a/crates/command_parser/src/command.rs +++ b/crates/command_parser/src/command.rs @@ -1,12 +1,12 @@ use std::{ - collections::HashSet, + collections::{HashMap, HashSet}, fmt::{Debug, Display}, sync::Arc, }; use smol_str::SmolStr; -use crate::{flag::Flag, token::Token}; +use crate::{flag::Flag, parameter::ParameterValue, token::Token}; #[derive(Debug, Clone)] pub struct Command { @@ -18,6 +18,7 @@ pub struct Command { pub show_in_suggestions: bool, pub parse_flags_before: usize, pub hidden_flags: HashSet, + pub flag_values: HashMap>, pub original: Option>, } @@ -43,6 +44,7 @@ impl Command { parse_flags_before, tokens, hidden_flags: HashSet::new(), + flag_values: HashMap::new(), original: None, } } @@ -73,6 +75,15 @@ impl Command { self.flags.insert(flag); self } + + pub fn flag_value( + mut self, + flag_name: impl Into, + value: impl Into>, + ) -> Self { + self.flag_values.insert(flag_name.into(), value.into()); + self + } } impl PartialEq for Command { diff --git a/crates/command_parser/src/lib.rs b/crates/command_parser/src/lib.rs index 1c77b88c..6f290667 100644 --- a/crates/command_parser/src/lib.rs +++ b/crates/command_parser/src/lib.rs @@ -350,6 +350,11 @@ pub fn parse_command( .expect("oom"); return Err(error); } + + for (name, value) in &full_cmd.flag_values { + flags.insert(name.to_string(), value.clone()); + } + println!("{} {flags:?} {params:?}", command.cb); return Ok(ParsedCommand { command_def: command, From 8431255930ebc7b7bd0e872242c0bcfc004658fc Mon Sep 17 00:00:00 2001 From: dawn <90008@gaze.systems> Date: Tue, 20 Jan 2026 00:55:27 +0300 Subject: [PATCH 177/179] return the def for the full commandd if we have it from parse_command --- crates/command_parser/src/lib.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/command_parser/src/lib.rs b/crates/command_parser/src/lib.rs index 6f290667..c168248b 100644 --- a/crates/command_parser/src/lib.rs +++ b/crates/command_parser/src/lib.rs @@ -308,7 +308,7 @@ pub fn parse_command( } } - let full_cmd = command.original.as_deref().unwrap_or(&command); + let full_cmd = command.original.as_ref().unwrap_or(&command); if misplaced_flags.is_empty().not() { let mut error = format!( "Flag{} ", @@ -355,9 +355,9 @@ pub fn parse_command( flags.insert(name.to_string(), value.clone()); } - println!("{} {flags:?} {params:?}", command.cb); + println!("{} {flags:?} {params:?}", full_cmd.cb); return Ok(ParsedCommand { - command_def: command, + command_def: full_cmd.clone(), flags, parameters: params, }); From 12655fb5390ea44764981451f7ace6b755fc4b02 Mon Sep 17 00:00:00 2001 From: dawn <90008@gaze.systems> Date: Mon, 26 Jan 2026 02:22:54 +0300 Subject: [PATCH 178/179] add missing -yes flags to command definitions, use log crate instead of printlns in command parser, also accept double dash for flags --- Cargo.lock | 36 +++++++++++++++++++++++ PluralKit.Bot/CommandMeta/CommandTree.cs | 25 ++++++++-------- PluralKit.Bot/CommandSystem/Parameters.cs | 4 --- PluralKit.Bot/Commands/GroupMember.cs | 2 +- PluralKit.Bot/Commands/SystemLink.cs | 4 +-- crates/command_definitions/src/config.rs | 10 +++++-- crates/command_definitions/src/group.rs | 9 ++---- crates/command_definitions/src/member.rs | 6 +++- crates/command_definitions/src/random.rs | 2 +- crates/command_definitions/src/switch.rs | 3 ++ crates/command_definitions/src/system.rs | 16 ++++++++-- crates/command_parser/Cargo.toml | 3 +- crates/command_parser/src/lib.rs | 19 ++++++------ crates/command_parser/src/string.rs | 24 ++++++++++----- crates/commands/Cargo.toml | 2 ++ crates/commands/src/lib.rs | 18 +++++++++++- 16 files changed, 130 insertions(+), 53 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0bc78644..c8afef88 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -647,6 +647,16 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" +[[package]] +name = "colored" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" +dependencies = [ + "lazy_static", + "windows-sys 0.59.0", +] + [[package]] name = "combine" version = "4.6.7" @@ -669,6 +679,7 @@ name = "command_parser" version = "0.1.0" dependencies = [ "lazy_static", + "log", "ordermap", "regex", "smol_str", @@ -682,6 +693,8 @@ dependencies = [ "command_definitions", "command_parser", "lazy_static", + "log", + "simple_logger", "uniffi", ] @@ -2532,6 +2545,15 @@ dependencies = [ "libc", ] +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + [[package]] name = "object" version = "0.37.3" @@ -3928,6 +3950,18 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" +[[package]] +name = "simple_logger" +version = "4.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e7e46c8c90251d47d08b28b8a419ffb4aede0f87c2eea95e17d1d5bacbf3ef1" +dependencies = [ + "colored", + "log", + "time", + "windows-sys 0.48.0", +] + [[package]] name = "siphasher" version = "0.3.11" @@ -4402,7 +4436,9 @@ checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" dependencies = [ "deranged", "itoa", + "libc", "num-conv", + "num_threads", "powerfmt", "serde", "time-core", diff --git a/PluralKit.Bot/CommandMeta/CommandTree.cs b/PluralKit.Bot/CommandMeta/CommandTree.cs index 69da30d0..210c36a0 100644 --- a/PluralKit.Bot/CommandMeta/CommandTree.cs +++ b/PluralKit.Bot/CommandMeta/CommandTree.cs @@ -19,7 +19,7 @@ public partial class CommandTree Commands.Invite => ctx.Execute(Invite, m => m.Invite(ctx)), Commands.Stats => ctx.Execute(null, m => m.Stats(ctx)), Commands.MemberShow(var param, var flags) => ctx.Execute(MemberInfo, m => m.ViewMember(ctx, param.target, flags.show_embed)), - Commands.MemberNew(var param, _) => ctx.Execute(MemberNew, m => m.NewMember(ctx, param.name)), + Commands.MemberNew(var param, var flags) => ctx.Execute(MemberNew, m => m.NewMember(ctx, param.name, flags.yes)), Commands.MemberSoulscream(var param, _) => ctx.Execute(MemberInfo, m => m.Soulscream(ctx, param.target)), Commands.MemberAvatarShow(var param, var flags) => ctx.Execute(MemberAvatar, m => m.ShowAvatar(ctx, param.target, flags.GetReplyFormat())), Commands.MemberAvatarClear(var param, var flags) => ctx.Execute(MemberAvatar, m => m.ClearAvatar(ctx, param.target, flags.yes)), @@ -60,9 +60,9 @@ public partial class CommandTree Commands.MemberServerKeepproxyClear(var param, var flags) => ctx.Execute(MemberServerKeepProxy, m => m.ClearServerKeepProxy(ctx, param.target, flags.yes)), Commands.MemberProxyShow(var param, _) => ctx.Execute(MemberProxy, m => m.ShowProxy(ctx, param.target)), Commands.MemberProxyClear(var param, var flags) => ctx.Execute(MemberProxy, m => m.ClearProxy(ctx, param.target, flags.yes)), - Commands.MemberProxyAdd(var param, _) => ctx.Execute(MemberProxy, m => m.AddProxy(ctx, param.target, param.tag)), + Commands.MemberProxyAdd(var param, var flags) => ctx.Execute(MemberProxy, m => m.AddProxy(ctx, param.target, param.tag, flags.yes)), Commands.MemberProxyRemove(var param, _) => ctx.Execute(MemberProxy, m => m.RemoveProxy(ctx, param.target, param.tag)), - Commands.MemberProxySet(var param, _) => ctx.Execute(MemberProxy, m => m.SetProxy(ctx, param.target, param.tags)), + Commands.MemberProxySet(var param, var flags) => ctx.Execute(MemberProxy, m => m.SetProxy(ctx, param.target, param.tags, flags.yes)), Commands.MemberTtsShow(var param, _) => ctx.Execute(MemberTts, m => m.ShowTts(ctx, param.target)), Commands.MemberTtsUpdate(var param, _) => ctx.Execute(MemberTts, m => m.ChangeTts(ctx, param.target, param.value)), Commands.MemberAutoproxyShow(var param, _) => ctx.Execute(MemberAutoproxy, m => m.ShowAutoproxy(ctx, param.target)), @@ -82,7 +82,7 @@ public partial class CommandTree Commands.CfgApTimeoutUpdate(var param, _) => ctx.Execute(null, m => m.EditAutoproxyTimeout(ctx, param.timeout)), Commands.CfgTimezoneShow => ctx.Execute(null, m => m.ViewSystemTimezone(ctx)), Commands.CfgTimezoneReset => ctx.Execute(null, m => m.ResetSystemTimezone(ctx)), - Commands.CfgTimezoneUpdate(var param, _) => ctx.Execute(null, m => m.EditSystemTimezone(ctx, param.timezone)), + Commands.CfgTimezoneUpdate(var param, var flags) => ctx.Execute(null, m => m.EditSystemTimezone(ctx, param.timezone, flags.yes)), Commands.CfgPingShow => ctx.Execute(null, m => m.ViewSystemPing(ctx)), Commands.CfgPingUpdate(var param, _) => ctx.Execute(null, m => m.EditSystemPing(ctx, param.toggle)), Commands.CfgMemberPrivacyShow => ctx.Execute(null, m => m.ViewMemberDefaultPrivacy(ctx)), @@ -126,7 +126,6 @@ public partial class CommandTree Commands.SystemNew(var param, _) => ctx.Execute(SystemNew, m => m.New(ctx, param.name)), Commands.SystemShowName(var param, var flags) => ctx.Execute(SystemRename, m => m.ShowName(ctx, param.target ?? ctx.System, flags.GetReplyFormat())), Commands.SystemRename(var param, _) => ctx.Execute(SystemRename, m => m.Rename(ctx, ctx.System, param.name)), - Commands.SystemClearName(var param, var flags) => ctx.Execute(SystemRename, m => m.ClearName(ctx, ctx.System, flags.yes)), Commands.SystemShowServerName(var param, var flags) => ctx.Execute(SystemServerName, m => m.ShowServerName(ctx, param.target ?? ctx.System, flags.GetReplyFormat())), Commands.SystemClearServerName(var param, var flags) => ctx.Execute(SystemServerName, m => m.ClearServerName(ctx, ctx.System, flags.yes)), Commands.SystemRenameServerName(var param, _) => ctx.Execute(SystemServerName, m => m.RenameServerName(ctx, ctx.System, param.name)), @@ -200,11 +199,11 @@ public partial class CommandTree Commands.SystemChangePrivacy(var param, _) => ctx.Execute(SystemPrivacy, m => m.ChangeSystemPrivacy(ctx, ctx.System, param.privacy, param.level)), Commands.SwitchOut(_, _) => ctx.Execute(SwitchOut, m => m.SwitchOut(ctx)), Commands.SwitchDo(var param, _) => ctx.Execute(Switch, m => m.SwitchDo(ctx, param.targets)), - Commands.SwitchMove(var param, _) => ctx.Execute(SwitchMove, m => m.SwitchMove(ctx, param.@string)), - Commands.SwitchEdit(var param, var flags) => ctx.Execute(SwitchEdit, m => m.SwitchEdit(ctx, param.targets, false, flags.first, flags.remove, flags.append, flags.prepend)), + Commands.SwitchMove(var param, var flags) => ctx.Execute(SwitchMove, m => m.SwitchMove(ctx, param.@string, flags.yes)), + Commands.SwitchEdit(var param, var flags) => ctx.Execute(SwitchEdit, m => m.SwitchEdit(ctx, param.targets, false, flags.first, flags.remove, flags.append, flags.prepend, flags.yes)), Commands.SwitchEditOut(_, var flags) => ctx.Execute(SwitchEditOut, m => m.SwitchEditOut(ctx, flags.yes)), - Commands.SwitchDelete(var param, var flags) => ctx.Execute(SwitchDelete, m => m.SwitchDelete(ctx, flags.all)), - Commands.SwitchCopy(var param, var flags) => ctx.Execute(SwitchCopy, m => m.SwitchEdit(ctx, param.targets, true, flags.first, flags.remove, flags.append, flags.prepend)), + Commands.SwitchDelete(var param, var flags) => ctx.Execute(SwitchDelete, m => m.SwitchDelete(ctx, flags.all, flags.yes)), + Commands.SwitchCopy(var param, var flags) => ctx.Execute(SwitchCopy, m => m.SwitchEdit(ctx, param.targets, true, flags.first, flags.remove, flags.append, flags.prepend, false)), Commands.SystemFronter(var param, var flags) => ctx.Execute(SystemFronter, m => m.Fronter(ctx, param.target ?? ctx.System)), Commands.SystemFronterHistory(var param, var flags) => ctx.Execute(SystemFrontHistory, m => m.FrontHistory(ctx, param.target ?? ctx.System, flags.clear)), Commands.SystemFronterPercent(var param, var flags) => ctx.Execute(SystemFrontPercent, m => m.FrontPercent(ctx, param.target ?? ctx.System, flags.duration, flags.fronters_only, flags.flat)), @@ -225,18 +224,18 @@ public partial class CommandTree Commands.SystemRandomGroup(var param, var flags) => ctx.Execute(GroupRandom, m => m.Group(ctx, param.target, flags.all, flags.show_embed)), Commands.GroupRandomMember(var param, var flags) => ctx.Execute(GroupMemberRandom, m => m.GroupMember(ctx, param.target, flags.all, flags.show_embed, flags)), - Commands.SystemLink(var param, _) => ctx.Execute(Link, m => m.LinkSystem(ctx, param.account)), + Commands.SystemLink(var param, var flags) => ctx.Execute(Link, m => m.LinkSystem(ctx, param.account, flags.yes)), Commands.SystemUnlink(var param, var flags) => ctx.Execute(Unlink, m => m.UnlinkAccount(ctx, param.account, flags.yes)), Commands.SystemMembers(var param, var flags) => ctx.Execute(SystemList, m => m.MemberList(ctx, param.target ?? ctx.System, param.query, flags)), Commands.MemberGroups(var param, var flags) => ctx.Execute(MemberGroups, m => m.ListMemberGroups(ctx, param.target, param.query, flags, flags.all)), Commands.GroupMembers(var param, var flags) => ctx.Execute(GroupMemberList, m => m.ListGroupMembers(ctx, param.target, param.query, flags)), Commands.SystemGroups(var param, var flags) => ctx.Execute(GroupList, g => g.ListSystemGroups(ctx, param.target ?? ctx.System, param.query, flags, flags.all)), Commands.GroupsSelf(var param, var flags) => ctx.Execute(GroupList, g => g.ListSystemGroups(ctx, ctx.System, param.query, flags, flags.all)), - Commands.GroupNew(var param, _) => ctx.Execute(GroupNew, g => g.CreateGroup(ctx, param.name)), + Commands.GroupNew(var param, var flags) => ctx.Execute(GroupNew, g => g.CreateGroup(ctx, param.name, flags.yes)), Commands.GroupInfo(var param, var flags) => ctx.Execute(GroupInfo, g => g.ShowGroupCard(ctx, param.target, flags.show_embed, flags.all)), Commands.GroupShowName(var param, var flags) => ctx.Execute(GroupRename, g => g.ShowGroupDisplayName(ctx, param.target, flags.GetReplyFormat())), Commands.GroupClearName(var param, var flags) => ctx.Execute(GroupRename, g => g.RenameGroup(ctx, param.target, null)), - Commands.GroupRename(var param, _) => ctx.Execute(GroupRename, g => g.RenameGroup(ctx, param.target, param.name)), + Commands.GroupRename(var param, var flags) => ctx.Execute(GroupRename, g => g.RenameGroup(ctx, param.target, param.name, flags.yes)), Commands.GroupShowDisplayName(var param, var flags) => ctx.Execute(GroupDisplayName, g => g.ShowGroupDisplayName(ctx, param.target, flags.GetReplyFormat())), Commands.GroupClearDisplayName(var param, var flags) => ctx.Execute(GroupDisplayName, g => g.ClearGroupDisplayName(ctx, param.target)), Commands.GroupChangeDisplayName(var param, _) => ctx.Execute(GroupDisplayName, g => g.ChangeGroupDisplayName(ctx, param.target, param.name)), @@ -252,7 +251,7 @@ public partial class CommandTree Commands.GroupShowColor(var param, var flags) => ctx.Execute(GroupColor, g => g.ShowGroupColor(ctx, param.target, flags.GetReplyFormat())), Commands.GroupClearColor(var param, var flags) => ctx.Execute(GroupColor, g => g.ClearGroupColor(ctx, param.target)), Commands.GroupChangeColor(var param, _) => ctx.Execute(GroupColor, g => g.ChangeGroupColor(ctx, param.target, param.color)), - Commands.GroupAddMember(var param, var flags) => ctx.Execute(GroupAdd, g => g.AddRemoveMembers(ctx, param.target, param.targets, Groups.AddRemoveOperation.Add, flags.all, flags.yes)), + Commands.GroupAddMember(var param, var flags) => ctx.Execute(GroupAdd, g => g.AddRemoveMembers(ctx, param.target, param.targets, Groups.AddRemoveOperation.Add, flags.all)), Commands.GroupRemoveMember(var param, var flags) => ctx.Execute(GroupRemove, g => g.AddRemoveMembers(ctx, param.target, param.targets, Groups.AddRemoveOperation.Remove, flags.all, flags.yes)), Commands.GroupShowPrivacy(var param, _) => ctx.Execute(GroupPrivacy, g => g.ShowGroupPrivacy(ctx, param.target)), Commands.GroupChangePrivacyAll(var param, _) => ctx.Execute(GroupPrivacy, g => g.SetAllGroupPrivacy(ctx, param.target, param.level)), diff --git a/PluralKit.Bot/CommandSystem/Parameters.cs b/PluralKit.Bot/CommandSystem/Parameters.cs index 8de534fd..0278f661 100644 --- a/PluralKit.Bot/CommandSystem/Parameters.cs +++ b/PluralKit.Bot/CommandSystem/Parameters.cs @@ -48,10 +48,6 @@ public class Parameters _cb = command.@commandRef; _flags = command.@flags; _params = command.@params; - foreach (var param in _params) - { - Console.WriteLine($"{param.Key}: {param.Value}"); - } } else { diff --git a/PluralKit.Bot/Commands/GroupMember.cs b/PluralKit.Bot/Commands/GroupMember.cs index 43ccbba9..9dc007f8 100644 --- a/PluralKit.Bot/Commands/GroupMember.cs +++ b/PluralKit.Bot/Commands/GroupMember.cs @@ -83,7 +83,7 @@ public class GroupMember target.Color, opts, all); } - public async Task AddRemoveMembers(Context ctx, PKGroup target, List? _members, Groups.AddRemoveOperation op, bool all, bool confirmYes) + public async Task AddRemoveMembers(Context ctx, PKGroup target, List? _members, Groups.AddRemoveOperation op, bool all, bool confirmYes = false) { ctx.CheckOwnGroup(target); diff --git a/PluralKit.Bot/Commands/SystemLink.cs b/PluralKit.Bot/Commands/SystemLink.cs index ef5f89af..16838367 100644 --- a/PluralKit.Bot/Commands/SystemLink.cs +++ b/PluralKit.Bot/Commands/SystemLink.cs @@ -7,7 +7,7 @@ namespace PluralKit.Bot; public class SystemLink { - public async Task LinkSystem(Context ctx, User account) + public async Task LinkSystem(Context ctx, User account, bool confirmYes = false) { ctx.CheckSystem(); @@ -20,7 +20,7 @@ public class SystemLink throw Errors.AccountInOtherSystem(existingAccount, ctx.Config, ctx.DefaultPrefix); var msg = $"{account.Mention()}, please confirm the link."; - if (!await ctx.PromptYesNo(msg, "Confirm", account, false)) throw Errors.MemberLinkCancelled; + if (!await ctx.PromptYesNo(msg, "Confirm", account, true, confirmYes)) throw Errors.MemberLinkCancelled; await ctx.Repository.AddAccount(ctx.System.Id, account.Id); await ctx.Reply($"{Emojis.Success} Account linked to system."); } diff --git a/crates/command_definitions/src/config.rs b/crates/command_definitions/src/config.rs index 68bd245f..ebd73491 100644 --- a/crates/command_definitions/src/config.rs +++ b/crates/command_definitions/src/config.rs @@ -16,7 +16,8 @@ pub fn cmds() -> impl IntoIterator { command!(ap_account, Toggle => "cfg_ap_account_update") .help("Toggles autoproxy globally for the current account"), command!(ap_timeout => "cfg_ap_timeout_show").help("Shows the autoproxy timeout"), - command!(ap_timeout, RESET => "cfg_ap_timeout_reset").help("Resets the autoproxy timeout"), + command!(ap_timeout, RESET => "cfg_ap_timeout_reset") + .help("Resets the autoproxy timeout"), command!(ap_timeout, parameter::Toggle::Off => "cfg_ap_timeout_off") .help("Disables the autoproxy timeout"), command!(ap_timeout, ("timeout", OpaqueString) => "cfg_ap_timeout_update") @@ -26,8 +27,10 @@ pub fn cmds() -> impl IntoIterator { let timezone_tokens = tokens!(cfg, ("timezone", ["zone", "tz"])); let timezone = [ command!(timezone_tokens => "cfg_timezone_show").help("Shows the system timezone"), - command!(timezone_tokens, RESET => "cfg_timezone_reset").help("Resets the system timezone"), + command!(timezone_tokens, RESET => "cfg_timezone_reset") + .help("Resets the system timezone"), command!(timezone_tokens, ("timezone", OpaqueString) => "cfg_timezone_update") + .flag(YES) .help("Changes your system's time zone"), ]; @@ -168,7 +171,8 @@ pub fn cmds() -> impl IntoIterator { let name_format_short = tokens!(cfg, ("nameformat", ["nf"])); let name_formatting = [ command!(name_format => "cfg_name_format_show").help("Shows the name format"), - command!(name_format, RESET => "cfg_name_format_reset").help("Resets the name format"), + command!(name_format, RESET => "cfg_name_format_reset") + .help("Resets the name format"), command!(name_format, ("format", OpaqueString) => "cfg_name_format_update") .help("Changes your system's username formatting"), command!(name_format_short => "cfg_name_format_show").help("Shows the name format"), diff --git a/crates/command_definitions/src/group.rs b/crates/command_definitions/src/group.rs index 2de9883c..449bc26e 100644 --- a/crates/command_definitions/src/group.rs +++ b/crates/command_definitions/src/group.rs @@ -21,6 +21,7 @@ pub fn cmds() -> impl Iterator { let group_new = tokens!(group, ("new", ["n"])); let group_new_cmd = once( command!(group_new, Remainder(("name", OpaqueString)) => "group_new") + .flag(YES) .help("Creates a new group"), ); @@ -37,9 +38,9 @@ pub fn cmds() -> impl Iterator { let group_name_cmd = [ command!(group_name => "group_show_name").help("Shows the group's name"), command!(group_name, CLEAR => "group_clear_name") - .flag(YES) .help("Clears the group's name"), command!(group_name, Remainder(("name", OpaqueString)) => "group_rename") + .flag(YES) .help("Renames a group"), ]; @@ -48,7 +49,6 @@ pub fn cmds() -> impl Iterator { command!(group_display_name => "group_show_display_name") .help("Shows the group's display name"), command!(group_display_name, CLEAR => "group_clear_display_name") - .flag(YES) .help("Clears the group's display name"), command!(group_display_name, Remainder(("name", OpaqueString)) => "group_change_display_name") .help("Changes the group's display name"), @@ -65,7 +65,6 @@ pub fn cmds() -> impl Iterator { command!(group_description => "group_show_description") .help("Shows the group's description"), command!(group_description, CLEAR => "group_clear_description") - .flag(YES) .help("Clears the group's description"), command!(group_description, Remainder(("description", OpaqueString)) => "group_change_description") .help("Changes the group's description"), @@ -98,7 +97,6 @@ pub fn cmds() -> impl Iterator { let group_color_cmd = [ command!(group_color => "group_show_color").help("Shows the group's color"), command!(group_color, CLEAR => "group_clear_color") - .flag(YES) .help("Clears the group's color"), command!(group_color, ("color", OpaqueString) => "group_change_color") .help("Changes a group's color"), @@ -126,7 +124,6 @@ pub fn cmds() -> impl Iterator { let group_delete_cmd = [ command!(group_target, ("delete", ["destroy", "erase", "yeet"]) => "group_delete") - .flag(YES) .help("Deletes a group"), ]; @@ -151,7 +148,7 @@ pub fn cmds() -> impl Iterator { let group_modify_members_cmd = [ command!(group_target, "add", Optional(MemberRefs) => "group_add_member") .help("Adds one or more members to a group") - .flag(ALL).flag(YES), + .flag(ALL), command!(group_target, ("remove", ["rem", "rm"]), Optional(MemberRefs) => "group_remove_member") .help("Removes one or more members from a group") .flag(ALL).flag(YES), diff --git a/crates/command_definitions/src/member.rs b/crates/command_definitions/src/member.rs index 1589e6da..69022e96 100644 --- a/crates/command_definitions/src/member.rs +++ b/crates/command_definitions/src/member.rs @@ -36,7 +36,9 @@ pub fn cmds() -> impl Iterator { let delete = ("delete", ["del", "remove"]); let member_new_cmd = once( - command!(member, new, ("name", OpaqueString) => "member_new").help("Creates a new member"), + command!(member, new, ("name", OpaqueString) => "member_new") + .flag(YES) + .help("Creates a new member"), ); let member_info_cmd = once( @@ -161,6 +163,7 @@ pub fn cmds() -> impl Iterator { command!(member_proxy => "member_proxy_show") .help("Shows a member's proxy tags"), command!(member_proxy, ("add", ["a"]), ("tag", OpaqueString) => "member_proxy_add") + .flag(YES) .help("Adds proxy tag to a member"), command!(member_proxy, ("remove", ["r", "rm"]), ("tag", OpaqueString) => "member_proxy_remove") .help("Removes proxy tag from a member"), @@ -168,6 +171,7 @@ pub fn cmds() -> impl Iterator { .flag(YES) .help("Clears all proxy tags from a member"), command!(member_proxy, Remainder(("tags", OpaqueString)) => "member_proxy_set") + .flag(YES) .help("Sets a member's proxy tags"), ] }; diff --git a/crates/command_definitions/src/random.rs b/crates/command_definitions/src/random.rs index 0c14b002..c8e6c414 100644 --- a/crates/command_definitions/src/random.rs +++ b/crates/command_definitions/src/random.rs @@ -3,7 +3,7 @@ use crate::utils::get_list_flags; use super::*; pub fn cmds() -> impl Iterator { - let random = ("random", ["rand"]); + let random = ("random", ["rand", "r"]); let group = group::group(); let member = member::member(); diff --git a/crates/command_definitions/src/switch.rs b/crates/command_definitions/src/switch.rs index ab87c6ec..c33410ea 100644 --- a/crates/command_definitions/src/switch.rs +++ b/crates/command_definitions/src/switch.rs @@ -21,14 +21,17 @@ pub fn cmds() -> impl IntoIterator { .help("Shows help for switch commands"), command!(switch, out => "switch_out").help("Registers a switch with no members"), command!(switch, delete => "switch_delete") + .flag(YES) .help("Deletes the latest switch") .flag(("all", ["clear", "c"])), command!(switch, r#move, Remainder(OpaqueString) => "switch_move") + .flag(YES) .help("Moves the latest switch in time"), // TODO: datetime parsing command!(switch, edit, out => "switch_edit_out") .help("Turns the latest switch into a switch-out") .flag(YES), command!(switch, edit, Optional(MemberRefs) => "switch_edit") + .flag(YES) .help("Edits the members in the latest switch") .flags(edit_flags), command!(switch, copy, Optional(MemberRefs) => "switch_copy") diff --git a/crates/command_definitions/src/system.rs b/crates/command_definitions/src/system.rs index 04a73c5c..5a76cfb0 100644 --- a/crates/command_definitions/src/system.rs +++ b/crates/command_definitions/src/system.rs @@ -230,15 +230,24 @@ pub fn edit() -> impl Iterator { command!(system, Optional(SystemRef), front => "system_fronter") .help("Shows a system's fronter(s)"), make_front_history(tokens!(front, ("history", ["h"]))), - make_front_history(tokens!(("fronthistory", ["fh"]))), + make_front_history(tokens!(("fronthistory", ["fh", "history", "switches"]))), make_front_percent(tokens!(front, ("percent", ["p", "%"]))), - make_front_percent(tokens!(("frontpercent", ["fp"]))), + make_front_percent(tokens!(( + "frontpercent", + ["fp", "front%", "frontbreakdown"] + ))), ]; let search_param = Optional(Remainder(("query", OpaqueString))); let apply_list_opts = |cmd: Command| cmd.flags(get_list_flags()); - let members_subcmd = tokens!(("members", ["l", "ls", "list"]), search_param); + let members_subcmd = tokens!( + ( + "members", + ["l", "ls", "list", "find", "search", "query", "fd"] + ), + search_param + ); let system_members_cmd = [ command!(system, Optional(SystemRef), members_subcmd => "system_members") .help("Lists a system's members"), @@ -265,6 +274,7 @@ pub fn edit() -> impl Iterator { let system_link = [ command!("link", ("account", UserRef) => "system_link") + .flag(YES) .help("Links another Discord account to your system"), command!("unlink", ("account", OpaqueString) => "system_unlink") .help("Unlinks a Discord account from your system") diff --git a/crates/command_parser/Cargo.toml b/crates/command_parser/Cargo.toml index 74c1a769..248a5ab2 100644 --- a/crates/command_parser/Cargo.toml +++ b/crates/command_parser/Cargo.toml @@ -8,4 +8,5 @@ lazy_static = { workspace = true } smol_str = "0.3.2" ordermap = "0.5" regex = "1" -strsim = "0.11" \ No newline at end of file +strsim = "0.11" +log = "0.4" \ No newline at end of file diff --git a/crates/command_parser/src/lib.rs b/crates/command_parser/src/lib.rs index c168248b..bf457c3d 100644 --- a/crates/command_parser/src/lib.rs +++ b/crates/command_parser/src/lib.rs @@ -18,6 +18,7 @@ use std::{collections::HashMap, usize}; use command::Command; use flag::{Flag, FlagMatchError, FlagValueMatchError}; +use log::debug; use parameter::ParameterValue; use smol_str::SmolStr; use string::MatchedFlag; @@ -82,9 +83,9 @@ pub fn parse_command( (_, Token::Parameter(_)) => std::cmp::Ordering::Less, _ => std::cmp::Ordering::Equal, }); - println!("possible: {:?}", possible_tokens); + debug!("possible: {:?}", possible_tokens); let next = next_token(possible_tokens.iter().cloned(), &input, current_pos); - println!("next: {:?}", next); + debug!("next: {:?}", next); match &next { Some((found_token, result, new_pos)) => { match &result { @@ -156,7 +157,7 @@ pub fn parse_command( .pop() .and_then(|m| matches!(m.token, Token::Parameter(_)).then_some(m)) { - println!("redoing previous branch: {:?}", state.token); + debug!("redoing previous branch: {:?}", state.token); local_tree = state.tree; current_pos = state.start_pos; // reset position to previous branch's start filtered_tokens = state.filtered_tokens; // reset filtered tokens to the previous branch's @@ -251,7 +252,7 @@ pub fn parse_command( // match flags until there are none left while let Some(matched_flag) = string::next_flag(&input, current_pos) { current_pos = matched_flag.next_pos; - println!("flag matched {matched_flag:?}"); + debug!("flag matched {matched_flag:?}"); raw_flags.push((current_token_idx, matched_flag)); } // if we have a command, stop parsing and return it (only if there is no remaining input) @@ -355,7 +356,7 @@ pub fn parse_command( flags.insert(name.to_string(), value.clone()); } - println!("{} {flags:?} {params:?}", full_cmd.cb); + debug!("{} {flags:?} {params:?}", full_cmd.cb); return Ok(ParsedCommand { command_def: full_cmd.clone(), flags, @@ -371,7 +372,7 @@ fn match_flag<'a>( ) -> Option), (&'a Flag, FlagMatchError)>> { // check for all (possible) flags, see if token matches for flag in possible_flags { - println!("matching flag {flag:?}"); + debug!("matching flag {flag:?}"); match flag.try_match(matched_flag.name, matched_flag.value) { Some(Ok(param)) => return Some(Ok((flag.get_name().into(), param))), Some(Err(err)) => return Some(Err((flag, err))), @@ -397,7 +398,7 @@ fn next_token<'a>( ) -> Option<(Token, TokenMatchResult, usize)> { // get next parameter, matching quotes let matched = string::next_param(&input, current_pos); - println!("matched: {matched:?}\n---"); + debug!("matched: {matched:?}\n---"); // iterate over tokens and run try_match for token in possible_tokens { @@ -421,7 +422,7 @@ fn next_token<'a>( }; match token.try_match(input_to_match) { Some(result) => { - //println!("matched token: {}", token); + //debug!("matched token: {}", token); return Some((token.clone(), result, next_pos)); } None => {} // continue matching until we exhaust all tokens @@ -448,7 +449,7 @@ fn rank_possible_commands( .map(move |(display, scoring, is_alias)| { let similarity = strsim::jaro_winkler(&input, &scoring); // if similarity > 0.7 { - // println!("DEBUG: ranking: '{}' vs '{}' = {}", input, scoring, similarity); + // debug!("DEBUG: ranking: '{}' vs '{}' = {}", input, scoring, similarity); // } (cmd, display, similarity, is_alias) }) diff --git a/crates/command_parser/src/string.rs b/crates/command_parser/src/string.rs index cc2dee63..2d07331e 100644 --- a/crates/command_parser/src/string.rs +++ b/crates/command_parser/src/string.rs @@ -1,5 +1,6 @@ use std::collections::HashMap; +use log::debug; use smol_str::{SmolStr, ToSmolStr}; lazy_static::lazy_static! { @@ -79,8 +80,8 @@ pub(super) fn next_param<'a>(input: &'a str, current_pos: usize) -> Option(input: &'a str, mut current_pos: usize) -> Option 2 { // if it doesn't have one, then it is not a flag + // or if it has more dashes than 2, assume its not a flag return None; - }; - current_pos += 1; + } + + let substr_to_match = substr_without_dashes; + current_pos += dash_count; // try finding = or whitespace for (pos, char) in substr_to_match.char_indices() { - println!("flag find char {char} at {pos}"); + debug!("flag find char {char} at {pos}"); if char == '=' { let name = &substr_to_match[..pos]; - println!("flag find {name}"); + debug!("flag find {name}"); // try to get the value let Some(param) = next_param(input, current_pos + pos + 1) else { return Some(MatchedFlag { diff --git a/crates/commands/Cargo.toml b/crates/commands/Cargo.toml index 6c274a33..1d9dbebd 100644 --- a/crates/commands/Cargo.toml +++ b/crates/commands/Cargo.toml @@ -16,6 +16,8 @@ lazy_static = { workspace = true } command_parser = { path = "../command_parser"} command_definitions = { path = "../command_definitions"} uniffi = { version = "0.29" } +log = "0.4" +simple_logger = "4.3.3" [build-dependencies] uniffi = { version = "0.29", features = [ "build" ] } diff --git a/crates/commands/src/lib.rs b/crates/commands/src/lib.rs index 32869cab..a54fb3f0 100644 --- a/crates/commands/src/lib.rs +++ b/crates/commands/src/lib.rs @@ -1,4 +1,8 @@ -use std::{collections::HashMap, fmt::Write, sync::Arc}; +use std::{ + collections::HashMap, + fmt::Write, + sync::{Arc, Once}, +}; use command_parser::{parameter::ParameterValue, token::TokenMatchResult, Tree}; @@ -14,6 +18,8 @@ lazy_static::lazy_static! { }; } +static LOG_INIT: Once = Once::new(); + #[derive(Debug)] pub enum CommandResult { Ok { command: ParsedCommand }, @@ -121,6 +127,16 @@ pub struct ParsedCommand { } pub fn parse_command(prefix: String, input: String) -> CommandResult { + LOG_INIT.call_once(|| { + if let Err(err) = simple_logger::SimpleLogger::new() + .with_level(log::LevelFilter::Info) + .env() + .init() + { + eprintln!("cant initialize logger: {err}"); + } + }); + command_parser::parse_command(COMMAND_TREE.clone(), prefix, input).map_or_else( |error| CommandResult::Err { error }, |parsed| CommandResult::Ok { From 498d657cd496437f57ed7f04ba01ac6c97fa377c Mon Sep 17 00:00:00 2001 From: dawn <90008@gaze.systems> Date: Mon, 26 Jan 2026 02:38:53 +0300 Subject: [PATCH 179/179] add missing confirms to command handlers and -yes flags to the definitions --- PluralKit.Bot/CommandMeta/CommandTree.cs | 6 +++--- PluralKit.Bot/Commands/Groups.cs | 15 ++++++++++++--- crates/command_definitions/src/group.rs | 4 ++++ 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/PluralKit.Bot/CommandMeta/CommandTree.cs b/PluralKit.Bot/CommandMeta/CommandTree.cs index 210c36a0..20cfb7d2 100644 --- a/PluralKit.Bot/CommandMeta/CommandTree.cs +++ b/PluralKit.Bot/CommandMeta/CommandTree.cs @@ -237,10 +237,10 @@ public partial class CommandTree Commands.GroupClearName(var param, var flags) => ctx.Execute(GroupRename, g => g.RenameGroup(ctx, param.target, null)), Commands.GroupRename(var param, var flags) => ctx.Execute(GroupRename, g => g.RenameGroup(ctx, param.target, param.name, flags.yes)), Commands.GroupShowDisplayName(var param, var flags) => ctx.Execute(GroupDisplayName, g => g.ShowGroupDisplayName(ctx, param.target, flags.GetReplyFormat())), - Commands.GroupClearDisplayName(var param, var flags) => ctx.Execute(GroupDisplayName, g => g.ClearGroupDisplayName(ctx, param.target)), + Commands.GroupClearDisplayName(var param, var flags) => ctx.Execute(GroupDisplayName, g => g.ClearGroupDisplayName(ctx, param.target, flags.yes)), Commands.GroupChangeDisplayName(var param, _) => ctx.Execute(GroupDisplayName, g => g.ChangeGroupDisplayName(ctx, param.target, param.name)), Commands.GroupShowDescription(var param, var flags) => ctx.Execute(GroupDesc, g => g.ShowGroupDescription(ctx, param.target, flags.GetReplyFormat())), - Commands.GroupClearDescription(var param, var flags) => ctx.Execute(GroupDesc, g => g.ClearGroupDescription(ctx, param.target)), + Commands.GroupClearDescription(var param, var flags) => ctx.Execute(GroupDesc, g => g.ClearGroupDescription(ctx, param.target, flags.yes)), Commands.GroupChangeDescription(var param, _) => ctx.Execute(GroupDesc, g => g.ChangeGroupDescription(ctx, param.target, param.description)), Commands.GroupShowIcon(var param, var flags) => ctx.Execute(GroupIcon, g => g.ShowGroupIcon(ctx, param.target, flags.GetReplyFormat())), Commands.GroupClearIcon(var param, var flags) => ctx.Execute(GroupIcon, g => g.ClearGroupIcon(ctx, param.target, flags.yes)), @@ -249,7 +249,7 @@ public partial class CommandTree Commands.GroupClearBanner(var param, var flags) => ctx.Execute(GroupBannerImage, g => g.ClearGroupBanner(ctx, param.target, flags.yes)), Commands.GroupChangeBanner(var param, _) => ctx.Execute(GroupBannerImage, g => g.ChangeGroupBanner(ctx, param.target, param.banner)), Commands.GroupShowColor(var param, var flags) => ctx.Execute(GroupColor, g => g.ShowGroupColor(ctx, param.target, flags.GetReplyFormat())), - Commands.GroupClearColor(var param, var flags) => ctx.Execute(GroupColor, g => g.ClearGroupColor(ctx, param.target)), + Commands.GroupClearColor(var param, var flags) => ctx.Execute(GroupColor, g => g.ClearGroupColor(ctx, param.target, flags.yes)), Commands.GroupChangeColor(var param, _) => ctx.Execute(GroupColor, g => g.ChangeGroupColor(ctx, param.target, param.color)), Commands.GroupAddMember(var param, var flags) => ctx.Execute(GroupAdd, g => g.AddRemoveMembers(ctx, param.target, param.targets, Groups.AddRemoveOperation.Add, flags.all)), Commands.GroupRemoveMember(var param, var flags) => ctx.Execute(GroupRemove, g => g.AddRemoveMembers(ctx, param.target, param.targets, Groups.AddRemoveOperation.Remove, flags.all, flags.yes)), diff --git a/PluralKit.Bot/Commands/Groups.cs b/PluralKit.Bot/Commands/Groups.cs index aa2d67b9..5b672176 100644 --- a/PluralKit.Bot/Commands/Groups.cs +++ b/PluralKit.Bot/Commands/Groups.cs @@ -174,10 +174,13 @@ public class Groups await ctx.Reply(embed: eb2.Build()); } - public async Task ClearGroupDisplayName(Context ctx, PKGroup target) + public async Task ClearGroupDisplayName(Context ctx, PKGroup target, bool confirmYes = false) { ctx.CheckOwnGroup(target); + if (!await ctx.ConfirmClear("this group's display name", confirmYes)) + return; + var patch = new GroupPatch { DisplayName = Partial.Null() }; await ctx.Repository.UpdateGroup(target.Id, patch); @@ -253,10 +256,13 @@ public class Groups await ctx.Reply(embed: eb2.Build()); } - public async Task ClearGroupDescription(Context ctx, PKGroup target) + public async Task ClearGroupDescription(Context ctx, PKGroup target, bool confirmYes = false) { ctx.CheckOwnGroup(target); + if (!await ctx.ConfirmClear("this group's description", confirmYes)) + return; + var patch = new GroupPatch { Description = Partial.Null() }; await ctx.Repository.UpdateGroup(target.Id, patch); @@ -479,10 +485,13 @@ public class Groups await ctx.Reply(embed: eb.Build(), files: [MiscUtils.GenerateColorPreview(target.Color)]); } - public async Task ClearGroupColor(Context ctx, PKGroup target) + public async Task ClearGroupColor(Context ctx, PKGroup target, bool confirmYes = false) { ctx.CheckOwnGroup(target); + if (!await ctx.ConfirmClear("this group's color", confirmYes)) + return; + await ctx.Repository.UpdateGroup(target.Id, new GroupPatch { Color = Partial.Null() }); await ctx.Reply($"{Emojis.Success} Group color cleared."); diff --git a/crates/command_definitions/src/group.rs b/crates/command_definitions/src/group.rs index 449bc26e..32ffe96a 100644 --- a/crates/command_definitions/src/group.rs +++ b/crates/command_definitions/src/group.rs @@ -38,6 +38,7 @@ pub fn cmds() -> impl Iterator { let group_name_cmd = [ command!(group_name => "group_show_name").help("Shows the group's name"), command!(group_name, CLEAR => "group_clear_name") + .flag(YES) .help("Clears the group's name"), command!(group_name, Remainder(("name", OpaqueString)) => "group_rename") .flag(YES) @@ -49,6 +50,7 @@ pub fn cmds() -> impl Iterator { command!(group_display_name => "group_show_display_name") .help("Shows the group's display name"), command!(group_display_name, CLEAR => "group_clear_display_name") + .flag(YES) .help("Clears the group's display name"), command!(group_display_name, Remainder(("name", OpaqueString)) => "group_change_display_name") .help("Changes the group's display name"), @@ -65,6 +67,7 @@ pub fn cmds() -> impl Iterator { command!(group_description => "group_show_description") .help("Shows the group's description"), command!(group_description, CLEAR => "group_clear_description") + .flag(YES) .help("Clears the group's description"), command!(group_description, Remainder(("description", OpaqueString)) => "group_change_description") .help("Changes the group's description"), @@ -97,6 +100,7 @@ pub fn cmds() -> impl Iterator { let group_color_cmd = [ command!(group_color => "group_show_color").help("Shows the group's color"), command!(group_color, CLEAR => "group_clear_color") + .flag(YES) .help("Clears the group's color"), command!(group_color, ("color", OpaqueString) => "group_change_color") .help("Changes a group's color"),