diff options
| -rw-r--r-- | Cargo.lock | 397 | ||||
| -rw-r--r-- | Cargo.toml | 15 | ||||
| -rw-r--r-- | src/bot.rs | 169 | ||||
| -rw-r--r-- | src/commands/game.rs (renamed from src/game.rs) | 186 | ||||
| -rw-r--r-- | src/commands/help.rs | 31 | ||||
| -rw-r--r-- | src/commands/meme/create.rs | 104 | ||||
| -rw-r--r-- | src/commands/meme/delete.rs | 29 | ||||
| -rw-r--r-- | src/commands/meme/history.rs | 151 | ||||
| -rw-r--r-- | src/commands/meme/invoke.rs | 149 | ||||
| -rw-r--r-- | src/commands/meme/mod.rs | 75 | ||||
| -rw-r--r-- | src/commands/mod.rs | 90 | ||||
| -rw-r--r-- | src/commands/playback.rs | 153 | ||||
| -rw-r--r-- | src/commands/roll.rs | 107 | ||||
| -rw-r--r-- | src/commands/sound_levels.rs | 79 | ||||
| -rw-r--r-- | src/commands/today/mod.rs | 32 | ||||
| -rw-r--r-- | src/lib.rs | 31 | ||||
| -rw-r--r-- | src/main.rs | 34 | ||||
| -rw-r--r-- | src/util/mod.rs (renamed from src/util.rs) | 76 | ||||
| -rw-r--r-- | src/util/rest_vec.rs | 84 |
19 files changed, 1156 insertions, 836 deletions
@@ -52,6 +52,55 @@ dependencies = [ ] [[package]] +name = "anstream" +version = "0.6.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" + +[[package]] +name = "anstyle-parse" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" +dependencies = [ + "anstyle", + "windows-sys 0.52.0", +] + +[[package]] name = "anyhow" version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -140,6 +189,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" [[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] name = "bitflags" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -167,6 +222,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] +name = "bytecount" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ce89b21cab1437276d2650d57e971f9d548a2d9037cc231abdc0562b97498ce" + +[[package]] name = "bytemuck" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -185,6 +246,37 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" [[package]] +name = "camino" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0ec6b951b160caa93cc0c7b209e5a3bff7aae9062213451ac99493cd844c239" +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.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4acbb09d9ee8e23699b9634375c72795d095bf268439da88562cf9b501f181fa" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", +] + +[[package]] name = "cc" version = "1.0.97" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -206,6 +298,7 @@ dependencies = [ "iana-time-zone", "js-sys", "num-traits", + "serde", "wasm-bindgen", "windows-targets 0.52.5", ] @@ -222,6 +315,46 @@ dependencies = [ ] [[package]] +name = "clap" +version = "4.5.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fbb260a053428790f3de475e304ff84cdbc4face759ea7a3e64c1edd938a7fc" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64b17d7ea74e9f833c7dbf2cbe4fb12ff26783eda4782a8975b72f895c9b4d99" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "501d359d5f3dcaf6ecdeee48833ae73ec6e42723a1e52419c79abf9507eec0a0" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.61", +] + +[[package]] +name = "clap_lex" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" + +[[package]] name = "cmake" version = "0.1.50" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -231,6 +364,12 @@ dependencies = [ ] [[package]] +name = "colorchoice" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" + +[[package]] name = "colored" version = "1.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -243,9 +382,9 @@ dependencies = [ [[package]] name = "command_attr" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f08c85a02e066b7b4f7dcb60eee6ae0793ef7d6452a3547d1f19665df070a9" +checksum = "88da8d7e9fe6f30d8e3fcf72d0f84102b49de70fece952633e8439e89bdc7631" dependencies = [ "proc-macro2", "quote", @@ -328,6 +467,41 @@ dependencies = [ ] [[package]] +name = "darling" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.61", +] + +[[package]] +name = "darling_macro" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.61", +] + +[[package]] name = "dashmap" version = "5.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -565,6 +739,15 @@ dependencies = [ ] [[package]] +name = "error-chain" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d2f06b9cac1506ece98fe3231e3cc9c4410ec3d5b1f24ae1c8946f0742cdefc" +dependencies = [ + "version_check", +] + +[[package]] name = "extended" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -792,6 +975,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" [[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + +[[package]] name = "h2" version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -817,6 +1006,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" [[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] name = "hermit-abi" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -951,6 +1146,12 @@ dependencies = [ ] [[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] name = "idna" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -997,6 +1198,12 @@ dependencies = [ ] [[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] name = "isolang" version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1142,6 +1349,21 @@ dependencies = [ ] [[package]] +name = "mini-moka" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c325dfab65f261f386debee8b0969da215b3fa0037e74c8a1234db7ba986d803" +dependencies = [ + "crossbeam-channel", + "crossbeam-utils", + "dashmap", + "skeptic", + "smallvec", + "tagptr", + "triomphe", +] + +[[package]] name = "miniz_oxide" version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1540,6 +1762,35 @@ dependencies = [ ] [[package]] +name = "poise" +version = "0.6.1" +source = "git+https://pub.npry.dev/poise#f72f91ad0c11f403e9ceee5b4581a348c38b3bb6" +dependencies = [ + "async-trait", + "derivative", + "futures-util", + "indexmap", + "parking_lot", + "poise_macros", + "regex", + "serenity", + "tokio", + "tracing", + "trim-in-place", +] + +[[package]] +name = "poise_macros" +version = "0.6.1" +source = "git+https://pub.npry.dev/poise#f72f91ad0c11f403e9ceee5b4581a348c38b3bb6" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.61", +] + +[[package]] name = "poly1305" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1611,6 +1862,17 @@ dependencies = [ ] [[package]] +name = "pulldown-cmark" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57206b407293d2bcd3af849ce869d52068623f19e1b5ff8e8778e3309439682b" +dependencies = [ + "bitflags 2.5.0", + "memchr", + "unicase", +] + +[[package]] name = "quote" version = "1.0.36" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2004,6 +2266,15 @@ dependencies = [ ] [[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] name = "schannel" version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2078,6 +2349,15 @@ dependencies = [ ] [[package]] +name = "semver" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" +dependencies = [ + "serde", +] + +[[package]] name = "serde" version = "1.0.201" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2108,6 +2388,15 @@ dependencies = [ ] [[package]] +name = "serde_cow" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7bbbec7196bfde255ab54b65e34087c0849629280028238e67ee25d6a4b7da" +dependencies = [ + "serde", +] + +[[package]] name = "serde_derive" version = "1.0.201" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2154,15 +2443,16 @@ dependencies = [ [[package]] name = "serenity" -version = "0.12.1" +version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c64da29158bb55d70677cacd4f4f8eab1acef005fb830d9c3bea411b090e96a9" +checksum = "880a04106592d0a8f5bdacb1d935889bfbccb4a14f7074984d9cd857235d34ac" dependencies = [ "arrayvec", "async-trait", - "base64 0.21.7", + "base64 0.22.1", "bitflags 2.5.0", "bytes", + "chrono", "command_attr", "dashmap", "flate2", @@ -2175,6 +2465,7 @@ dependencies = [ "reqwest", "secrecy", "serde", + "serde_cow", "serde_json", "static_assertions", "time", @@ -2182,6 +2473,7 @@ dependencies = [ "tokio-tungstenite 0.21.0", "tracing", "typemap_rev", + "typesize", "url", "uwl", ] @@ -2259,6 +2551,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" [[package]] +name = "skeptic" +version = "0.13.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16d23b015676c90a0f01c197bfdc786c20342c73a0afdda9025adb0bc42940a8" +dependencies = [ + "bytecount", + "cargo_metadata", + "error-chain", + "glob", + "pulldown-cmark", + "tempfile", + "walkdir", +] + +[[package]] name = "slab" version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2390,6 +2697,12 @@ dependencies = [ ] [[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] name = "subtle" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2640,6 +2953,12 @@ dependencies = [ ] [[package]] +name = "tagptr" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" + +[[package]] name = "tap" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2693,6 +3012,7 @@ version = "0.2.0" dependencies = [ "anyhow", "chrono", + "clap", "deadpool-postgres", "diesel", "diesel-async", @@ -2708,6 +3028,7 @@ dependencies = [ "log", "pest", "pest_derive", + "poise", "rand", "regex", "reqwest", @@ -3022,6 +3343,18 @@ dependencies = [ ] [[package]] +name = "trim-in-place" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "343e926fc669bc8cde4fa3129ab681c63671bae288b1f1081ceee6d9d37904fc" + +[[package]] +name = "triomphe" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6631e42e10b40c0690bf92f404ebcfe6e1fdb480391d15f17cc8e96eeed5369" + +[[package]] name = "try-lock" version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3134,6 +3467,35 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] +name = "typesize" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb704842c709bc76f63e99e704cb208beeccca2abbabd0d9aec02e48ca1cee0f" +dependencies = [ + "chrono", + "dashmap", + "hashbrown", + "mini-moka", + "parking_lot", + "secrecy", + "serde_json", + "time", + "typesize-derive", + "url", +] + +[[package]] +name = "typesize-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "905e88c2a4cc27686bd57e495121d451f027e441388a67f773be729ad4be1ea8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.61", +] + +[[package]] name = "ucd-trie" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3219,6 +3581,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" [[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] name = "uuid" version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3252,6 +3620,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] name = "want" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3424,6 +3802,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3,6 +3,11 @@ name = "thulani" version = "0.2.0" authors = ["Nathan Perry <np@npry.dev>"] edition = "2021" +default-run = "thulani" + +[[bin]] +name = "batch_delmeme" +required-features = ["db"] [features] default = ["db", "games"] @@ -32,6 +37,8 @@ timeago = "0.4" statrs = "0.16" fnv = "1.0" +clap = { version = "4.5", features = ["derive"] } + pest = "2.7" pest_derive = "2.7" @@ -45,6 +52,8 @@ tokio = { version = "1.37", features = ["full"] } songbird = { version = "0.4", features = ["builtin-queue"] } symphonia = { version = "0.5", features = ["all"] } +poise = { git = "https://pub.npry.dev/poise" } + diesel = { version = "2.1", features = ["chrono"], optional = true } diesel-async = { version = "0.4", optional = true, features = ["deadpool", "postgres"] } diesel_async_migrations = { version = "0.12", optional = true } @@ -56,3 +65,9 @@ version = "0.12" default-features = false features = ["builder", "cache", "client", "framework", "gateway", "http", "model", "utils", "voice", "standard_framework", "rustls_backend"] +[profile.release] +codegen-units = 1 +lto = "fat" +overflow-checks = false +debug-assertions = false +opt-level = 3 @@ -1,9 +1,9 @@ use std::{ + collections::HashSet, fs::File, future::Future, path::PathBuf, pin::Pin, - result::Result as StdResult, str::FromStr, sync::Arc, }; @@ -21,22 +21,16 @@ use log::{ trace, warn, }; +use poise::{ + BoxFuture, + FrameworkError, +}; use serenity::{ all::{ GuildId, ReactionType, }, - async_trait, - framework::{ - standard::{ - BucketBuilder, - CommandError, - Configuration, - }, - StandardFramework, - }, model::{ - channel::Message, event::ResumedEvent, gateway::Ready, id::{ @@ -56,11 +50,13 @@ use songbird::{ use tokio::sync::Mutex; use crate::{ - commands::register_commands, + commands, config::CONFIG, util, util::OAUTH_URL, Error, + PoiseContext, + PoiseData, Result, }; @@ -123,7 +119,7 @@ impl EventHandler for Handler { struct SongbirdHandler(Arc<Mutex<Call>>); -#[async_trait] +#[serenity::async_trait] impl songbird::events::EventHandler for SongbirdHandler { async fn act(&self, _ctx: &EventContext<'_>) -> Option<Event> { let mut call = self.0.lock().await; @@ -161,112 +157,125 @@ lazy_static! { let result = restrict_ids.unwrap_or_default().into_iter().collect::<FnvHashSet<_>>(); - info!("restricted ids: {:?}", result); + info!("restricted ids: {result:?}"); result }; } -async fn framework() -> StandardFramework { - let builder = BucketBuilder::default().delay(1).limit(20).time_span(60); +fn on_err(err: FrameworkError<PoiseData, anyhow::Error>) -> BoxFuture<()> { + Box::pin(async move { + error!("error encountered: {err:?}"); - let framework = StandardFramework::new() - .before(before_handle) - .after(after_handle) - .bucket("Standard", builder) - .await; + if let Some(ctx) = err.ctx() { + if let Err(e) = util::react(ctx, ReactionType::Unicode("❌".to_owned())).await { + error!("reacting to failed message: {e}"); + } - let config = Configuration::default() - .allow_dm(false) - .with_whitespace(true) - .prefixes(ALL_PREFIXES.iter().map(|x| x.to_string())) - .ignore_bots(true) - .on_mention(None) - .owners(vec![CONFIG.discord.owner()].into_iter().collect()) - .case_insensitivity(true); + if let Err(e) = util::reply(ctx, "BANIC").await { + error!("sending BANIC: {e}"); + } + } + }) +} - framework.configure(config); +async fn framework() -> poise::Framework<PoiseData, anyhow::Error> { + let additional_prefixes = + ALL_PREFIXES.iter().skip(1).map(|x| poise::Prefix::Literal(x.to_owned())).collect(); - register_commands(framework) -} + let framework = poise::Framework::builder() + .options(poise::FrameworkOptions { + pre_command: before_handle, + post_command: after_handle, + on_error: on_err, + + command_check: Some(check), + + prefix_options: poise::PrefixFrameworkOptions { + prefix: ALL_PREFIXES.get(0).map(|&x| x.to_owned()), + additional_prefixes, + case_insensitive_commands: true, + mention_as_prefix: false, + ignore_bots: true, + ..Default::default() + }, + + commands: commands::commands(), + owners: HashSet::from_iter([CONFIG.discord.owner()]), + initialize_owners: false, + skip_checks_for_owners: true, -fn before_handle<'fut>( - ctx: &'fut Context, - message: &'fut Message, - cmd: &'fut str, -) -> Pin<Box<dyn Future<Output = bool> + Send + 'fut>> { - debug!("got command '{}' from user '{}' ({})", cmd, message.author.name, message.author.id); + ..Default::default() + }) + .setup(|_ctx, _ready, _framework| Box::pin(async move { Ok(()) })) + .build(); + + framework +} +fn check(ctx: PoiseContext) -> BoxFuture<anyhow::Result<bool>> { Box::pin(async move { - if !message.guild_id.map_or(false, |x| x == CONFIG.discord.guild()) { - info!("rejecting command '{}' from user '{}': wrong guild", cmd, message.author.name); - return false; + if !ctx.guild_id().map_or(false, |x| x == CONFIG.discord.guild()) { + info!( + "rejecting command '{}' from user '{}': wrong guild", + ctx.command().name, + ctx.author().name + ); + return Ok(false); } - if message.author.id == CONFIG.discord.owner() { - return true; + if ctx.author().id == CONFIG.discord.owner() { + return Ok(true); } - let restricted_prefix = - RESTRICTED_PREFIXES.iter().any(|prefix| message.content.starts_with(prefix)); + let restricted_prefix = RESTRICTED_PREFIXES.iter().any(|&prefix| ctx.prefix() == prefix); if !restricted_prefix { - return true; + return Ok(true); } const PERMITTED_WEEKDAY: chrono::Weekday = chrono::Weekday::Tue; - let user_is_restricted = RESTRICT_IDS.contains(&message.author.id.get()); + let user_is_restricted = RESTRICT_IDS.contains(&ctx.author().id.get()); let restrictions_flipped = chrono::Local::now().weekday() == PERMITTED_WEEKDAY; if user_is_restricted == restrictions_flipped { - return true; + return Ok(true); } let reason = if !restrictions_flipped { "restricted prefix".to_owned() } else { - format!("it is {:?}", PERMITTED_WEEKDAY) + format!("it is {PERMITTED_WEEKDAY:?}") }; - info!("rejecting command '{}' from user '{}': {}", cmd, message.author.name, reason); + info!( + "rejecting command '{}' from user '{}': {}", + ctx.command().name, + ctx.author().name, + reason + ); - match util::send_result(ctx, message.channel_id, "no", message.tts).await { - Err(e) => error!("sending restricted prefix response: {}", e), - Ok(msg_id) => { - let mut mp = MESSAGE_WATCH.lock().await; - mp.insert(message.id, msg_id); - }, - } + util::reply(ctx, "no").await?; - false + Ok(false) }) } -fn after_handle<'fut>( - ctx: &'fut Context, - msg: &'fut Message, - cmd: &'fut str, - err: StdResult<(), CommandError>, -) -> Pin<Box<dyn Future<Output = ()> + Send + 'fut>> { - Box::pin(async move { - match err { - Ok(()) => { - trace!("command '{}' completed successfully", cmd); - }, - - Err(e) => { - if let Err(e) = msg.react(&ctx, ReactionType::Unicode("❌".to_owned())).await { - error!("reacting to failed message: {}", e); - } +fn before_handle<'fut>(ctx: PoiseContext<'fut>) -> Pin<Box<dyn Future<Output = ()> + Send + 'fut>> { + debug!( + "got command '{}' from user '{}' ({})", + ctx.command().name, + ctx.author().name, + ctx.author().id + ); - if let Err(e) = util::send(ctx, msg.channel_id, "BANIC", msg.tts).await { - error!("sending BANIC: {}", e); - } + Box::pin(async {}) +} - error!("error encountered handling command '{}': {:?}", cmd, e); - }, - } +fn after_handle(ctx: PoiseContext) -> BoxFuture<()> { + Box::pin(async move { + trace!("command '{}' completed successfully", ctx.command().name); }) } diff --git a/src/game.rs b/src/commands/game.rs index 4011a5d..72633b5 100644 --- a/src/game.rs +++ b/src/commands/game.rs @@ -1,5 +1,4 @@ use std::{ - convert::Infallible, fs, iter, path::PathBuf, @@ -10,10 +9,7 @@ use std::{ }, }; -use anyhow::{ - anyhow, - Error, -}; +use anyhow::anyhow; use fnv::{ FnvHashMap, FnvHashSet, @@ -26,24 +22,9 @@ use log::{ info, }; use serde::Deserialize; -use serenity::{ - framework::standard::{ - macros::{ - command, - group, - }, - ArgError, - Args, - CommandError, - CommandResult, - }, - futures::TryFutureExt, - model::{ - channel::Message, - guild::Guild, - id::UserId, - }, - prelude::*, +use serenity::model::{ + guild::Guild, + id::UserId, }; use tap::Pipe; use url::Url; @@ -51,14 +32,11 @@ use url::Url; use crate::{ bot::HttpKey, util, + PoiseContext, Result, CONFIG, }; -#[group] -#[commands(game, installedgame, ownedgame, updategaem)] -pub struct Game; - lazy_static! { static ref SPREADSHEET_URL: Url = Url::parse(&format!( "https://sheets.googleapis.com/v4/spreadsheets/{}/values:batchGet", @@ -142,7 +120,7 @@ enum GameStatus { } impl FromStr for GameStatus { - type Err = Error; + type Err = anyhow::Error; fn from_str(s: &str) -> Result<Self> { use std::char; @@ -161,16 +139,18 @@ impl FromStr for GameStatus { } } -#[command] -#[aliases("installedgaem")] -pub async fn installedgame(ctx: &Context, msg: &Message, args: Args) -> CommandResult { - _game(ctx, msg, args, GameStatus::Installed).await +pub fn commands() -> Vec<poise::Command<crate::PoiseData, anyhow::Error>> { + vec![installedgame(), ownedgame(), game(), updategaem()] } -#[command] -#[aliases("ownedgaem")] -pub async fn ownedgame(ctx: &Context, msg: &Message, args: Args) -> CommandResult { - _game(ctx, msg, args, GameStatus::NotInstalled).await +#[poise::command(prefix_command, guild_only, category = "gaem", aliases("installedgaem"))] +pub async fn installedgame(ctx: PoiseContext<'_>, args: util::RestVec) -> anyhow::Result<()> { + _game(ctx, args.into_inner(), GameStatus::Installed).await +} + +#[poise::command(prefix_command, guild_only, category = "gaem", aliases("ownedgaem"))] +pub async fn ownedgame(ctx: PoiseContext<'_>, args: util::RestVec) -> anyhow::Result<()> { + _game(ctx, args.into_inner(), GameStatus::NotInstalled).await } #[derive(Copy, Clone, Debug, thiserror::Error, PartialEq, Eq, Hash)] @@ -222,29 +202,25 @@ pub fn get_user_id<S: AsRef<str>>(g: &Guild, s: S) -> StdResult<UserId, UserLook } } -#[command] -#[aliases("gaem")] -async fn game(ctx: &Context, msg: &Message, args: Args) -> CommandResult { - _game(ctx, msg, args, GameStatus::Installed).await +#[poise::command(prefix_command, guild_only, category = "gaem", aliases("gaem"))] +async fn game(ctx: PoiseContext<'_>, args: util::RestVec) -> anyhow::Result<()> { + _game(ctx, args.into_inner(), GameStatus::Installed).await } async fn _game( - ctx: &Context, - msg: &Message, - mut args: Args, + ctx: PoiseContext<'_>, + user_args: Vec<String>, min_status: GameStatus, -) -> CommandResult { - let users = { - let guild = - msg.channel_id.to_channel(&ctx).await?.guild().ok_or(anyhow!("couldn't find guild"))?; - - let user_args: Vec<String> = if args.rest().is_empty() { - Vec::new() - } else { - args.quoted().iter::<String>().collect::<StdResult<Vec<_>, ArgError<Infallible>>>()? - }; +) -> anyhow::Result<()> { + use serenity::futures::StreamExt; - use serenity::futures::StreamExt; + let users = { + let guild = ctx + .channel_id() + .to_channel(&ctx) + .await? + .guild() + .ok_or(anyhow!("couldn't find guild"))?; let mut users = user_args .into_iter() @@ -268,25 +244,15 @@ async fn _game( debug!("parsed userid {:?}", possible); match possible { + Ok(x) => Some(x), Err(UserLookupError::NotFound) => { - let _ = util::send( - ctx, - msg.channel_id, - &format!("didn't recognize {}", &u), - msg.tts, - ) - .await; + let _ = util::reply(ctx, format!("didn't recognize {u}")).await; + None }, - Ok(x) => Some(x), Err(UserLookupError::Ambiguous(x)) => { - let _ = util::send( - ctx, - msg.channel_id, - &format!("too many matches ({}) for {}", x, &u), - msg.tts, - ) - .await; + let _ = + util::reply(ctx, format!("too many matches ({x}) for {u}")).await; None }, } @@ -296,7 +262,7 @@ async fn _game( let res = DISCORD_MAP.get(&uid).map(|s| s.to_lowercase()); if res.is_none() { - info!("user {} is not recognized", uid); + info!("user {uid} is not recognized"); } res @@ -314,7 +280,7 @@ async fn _game( .collect::<FnvHashMap<_, _>>(); let channel = - pairs.get(&msg.author.id).cloned().unwrap_or(CONFIG.discord.voice_channel()); + pairs.get(&ctx.author().id).cloned().unwrap_or(CONFIG.discord.voice_channel()); users = pairs .iter() @@ -335,12 +301,12 @@ async fn _game( if inferred && users.len() < 2 || !inferred && users.is_empty() { info!("too few known users to make game comparison"); - util::send(ctx, msg.channel_id, "yer too lonely", msg.tts).await?; + util::reply(ctx, "yer too lonely").await?; return Ok(()); } let client = { - let data = ctx.data.read().await; + let data = ctx.serenity_context().data.read().await; data.get::<HttpKey>().unwrap().clone() }; let data = load_spreadsheet(&client).await?; @@ -418,7 +384,7 @@ async fn _game( games_formatted = "**LITERALLY NOTHING**".to_owned(); } - util::send(ctx, msg.channel_id, &games_formatted, msg.tts).await?; + util::reply(ctx, games_formatted).await?; Ok(()) } @@ -451,29 +417,36 @@ async fn load_spreadsheet(client: &reqwest::Client) -> Result<Vec<Vec<String>>> Ok(resp.value_ranges.into_iter().next().unwrap().values) } -#[command] -#[aliases("updategame")] -pub async fn updategaem(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { +#[poise::command( + slash_command, + prefix_command, + guild_only, + category = "gaem", + aliases("updategame") +)] +pub async fn updategaem(ctx: PoiseContext<'_>, user: Option<String>) -> anyhow::Result<()> { use regex::Regex; + use std::borrow::Borrow; let client = { - let data = ctx.data.read().await; + let data = ctx.serenity_context().data.read().await; data.get::<HttpKey>().unwrap().clone() }; - let arg_user = args.single_quoted::<String>(); + let user = match user { + None => ctx.author().id, + Some(user) => { + let guild = ctx + .channel_id() + .to_channel(&ctx) + .await? + .guild() + .ok_or(anyhow!("couldn't find guild"))?; - let user = if arg_user.is_err() { - msg.author.id - } else { - use std::borrow::Borrow; - - let guild = - msg.channel_id.to_channel(&ctx).await?.guild().ok_or(anyhow!("couldn't find guild"))?; + let guild = guild.guild(&ctx).ok_or(anyhow!("couldn't find guild"))?; - let guild = guild.guild(&ctx).ok_or(anyhow!("couldn't find guild"))?; - - get_user_id(guild.borrow(), arg_user.unwrap()).map_err(Error::from)? + get_user_id(guild.borrow(), user).map_err(anyhow::Error::from)? + }, }; debug!("parsed userid {:?}", user); @@ -481,18 +454,16 @@ pub async fn updategaem(ctx: &Context, msg: &Message, mut args: Args) -> Command let username = match DISCORD_MAP.get(&user) { Some(s) => s, None => { - return util::send(ctx, msg.channel_id, "WHO THE FUCK ARE YE", msg.tts) - .map_err(CommandError::from) - .await; + util::reply(ctx, "WHO THE FUCK ARE YE").await?; + return Ok(()); }, }; let steam_id = match STEAM_MAP.get(&user) { Some(u) => u, None => { - return util::send(ctx, msg.channel_id, "WHO ARE YE ON STEAM", msg.tts) - .map_err(CommandError::from) - .await; + util::reply(ctx, "WHO ARE YE ON STEAM").await?; + return Ok(()); }, }; @@ -504,9 +475,8 @@ pub async fn updategaem(ctx: &Context, msg: &Message, mut args: Args) -> Command let user_column = match user_column { Some(c) => &spreadsheet[c][1..], None => { - return util::send(ctx, msg.channel_id, "YER NOT IN THE SPREADSHEET", msg.tts) - .map_err(CommandError::from) - .await; + util::reply(ctx, "YER NOT IN THE SPREADSHEET").await?; + return Ok(()); }, }; @@ -520,9 +490,8 @@ pub async fn updategaem(ctx: &Context, msg: &Message, mut args: Args) -> Command Some(c) => &spreadsheet[c][1..], None => { error!("didn't find an appid column in the spreadsheet"); - return util::send(ctx, msg.channel_id, "SPREADSHEET BROKE", msg.tts) - .map_err(CommandError::from) - .await; + util::reply(ctx, "SPREADSHEET BROKE").await?; + return Ok(()); }, }; @@ -560,7 +529,7 @@ pub async fn updategaem(ctx: &Context, msg: &Message, mut args: Args) -> Command } let client = { - let data = ctx.data.read().await; + let data = ctx.serenity_context().data.read().await; data.get::<HttpKey>().unwrap().clone() }; @@ -587,19 +556,16 @@ pub async fn updategaem(ctx: &Context, msg: &Message, mut args: Args) -> Command .join("\n"); if !found_games.is_empty() { - util::send( + let n_missing = found_games.chars().filter(|x| *x == '\n').count() + 1; + util::reply( ctx, - msg.channel_id, - &format!( - "{} games owned on steam that are missing from the list:\n{}", - found_games.chars().filter(|x| *x == '\n').count() + 1, - found_games + format!( + "{n_missing} games owned on steam that are missing from the list:\n{found_games}" ), - msg.tts, ) .await?; } else { - util::send(ctx, msg.channel_id, "up to date", msg.tts).await?; + util::reply(ctx, "up to date").await?; } Ok(()) diff --git a/src/commands/help.rs b/src/commands/help.rs deleted file mode 100644 index 0d84b2d..0000000 --- a/src/commands/help.rs +++ /dev/null @@ -1,31 +0,0 @@ -use std::collections::HashSet; - -use serenity::{ - framework::standard::{ - help_commands, - macros::help, - Args, - CommandGroup, - CommandResult, - HelpOptions, - }, - model::{ - channel::Message, - id::UserId, - }, - prelude::*, -}; - -#[help] -pub async fn help( - ctx: &Context, - msg: &Message, - args: Args, - opts: &'static HelpOptions, - groups: &[&'static CommandGroup], - owners: HashSet<UserId>, -) -> CommandResult { - help_commands::with_embeds(ctx, msg, args, opts, groups, owners).await?; - - Ok(()) -} diff --git a/src/commands/meme/create.rs b/src/commands/meme/create.rs index 9aff370..cad9bfc 100644 --- a/src/commands/meme/create.rs +++ b/src/commands/meme/create.rs @@ -2,25 +2,12 @@ use std::process::Stdio; use anyhow::anyhow; use diesel::result::Error as DieselError; -use lazy_static::lazy_static; use log::{ debug, error, warn, }; -use serenity::{ - all::ReactionType, - framework::standard::{ - macros::command, - Args, - CommandError, - CommandResult, - Delimiter, - }, - futures::TryFutureExt, - model::channel::Message, - prelude::*, -}; +use serenity::all::ReactionType; use tap::Pipe; use tokio::{ io::AsyncReadExt, @@ -37,20 +24,16 @@ use crate::{ }, parse_times, util, + PoiseContext, FFMPEG_COMMAND, }; -lazy_static! { - static ref DELIMS: Vec<Delimiter> = vec![' '.into(), '\n'.into(), '\t'.into()]; -} - -#[command] -pub async fn addmeme(ctx: &Context, msg: &Message, args: Args) -> CommandResult { - let mut args = Args::new(args.rest(), DELIMS.as_ref()); - - let title = args.single_quoted::<String>()?; - let text = args.rest().to_owned(); - +#[poise::command(slash_command, prefix_command, guild_only, category = "memes")] +pub async fn addmeme( + ctx: PoiseContext<'_>, + title: String, + #[rest] text: String, +) -> anyhow::Result<()> { let text = if text.is_empty() { None } else { @@ -59,20 +42,21 @@ pub async fn addmeme(ctx: &Context, msg: &Message, args: Args) -> CommandResult let mut conn = connection().await?; - let image = msg.attachments.first(); + let image = util::msg(ctx).and_then(|msg| msg.attachments.first()); if image.is_none() && text.is_none() { warn!("tried to create non-audio meme with no image or text"); - return util::send(ctx, msg.channel_id, "hahAA it's empty xdddd", msg.tts) - .map_err(CommandError::from) - .await; + + util::reply(ctx, "hahAA it's empty xdddd").await?; + return Ok(()); } let mut image_id = None; if let Some(att) = image { let data = att.download().await?; - image_id = Some(Image::create(&mut conn, &att.filename, data, msg.author.id.get()).await?); + image_id = + Some(Image::create(&mut conn, &att.filename, data, ctx.author().id.get()).await?); }; let save_result = NewMeme { @@ -82,24 +66,25 @@ pub async fn addmeme(ctx: &Context, msg: &Message, args: Args) -> CommandResult audio_id: None, metadata_id: 0, } - .save(&mut conn, msg.author.id.get()) + .save(&mut conn, ctx.author().id.get()) .await .map(|_| {}); use diesel::result::DatabaseErrorKind; match save_result { Ok(_) => { - msg.react(&ctx, ReactionType::Unicode("👌".to_string())).await?; + util::react(ctx, ReactionType::Unicode("👌".to_string())).await?; }, Err(e) => { if let Some(DieselError::DatabaseError(DatabaseErrorKind::UniqueViolation, _)) = e.downcast_ref::<DieselError>() { error!("tried to create meme that already exists"); - msg.react(&ctx, ReactionType::Unicode("❌".to_owned())).await?; - return util::send(ctx, msg.channel_id, "that meme already exists", msg.tts) - .map_err(CommandError::from) - .await; + + util::react(ctx, ReactionType::Unicode("❌".to_owned())).await?; + util::reply(ctx, "that meme already exists").await?; + + return Ok(()); } return Err(e.into()); @@ -109,19 +94,19 @@ pub async fn addmeme(ctx: &Context, msg: &Message, args: Args) -> CommandResult Ok(()) } -#[command] -pub async fn addaudiomeme(ctx: &Context, msg: &Message, args: Args) -> CommandResult { +#[poise::command(slash_command, prefix_command, guild_only, category = "memes")] +pub async fn addaudiomeme( + ctx: PoiseContext<'_>, + title: String, + audio_str: String, + #[rest] rest: String, +) -> anyhow::Result<()> { debug!("running addaudiomeme"); - let mut args = Args::new(args.rest(), DELIMS.as_ref()); - - let title = args.single_quoted::<String>()?; - let audio_str = args.single_quoted::<String>()?; - let elems = audio_str.split_whitespace().collect::<Vec<_>>(); if elems.is_empty() { - util::send(ctx, msg.channel_id, "are you stupid", msg.tts).await?; + util::reply(ctx, "are you stupid").await?; return Err(anyhow!("no audio link was provided").into()); } @@ -168,23 +153,23 @@ pub async fn addaudiomeme(ctx: &Context, msg: &Message, args: Args) -> CommandRe let mut audio_reader = ffmpeg_command.stdout.unwrap(); - let text = args.rest().to_owned(); - let text = if text.is_empty() { + let text = if rest.is_empty() { None } else { - Some(text) + Some(rest) }; let mut conn = connection().await?; - let image_att = msg.attachments.first().ok_or(anyhow!("no attachment")); + let image_att = + util::msg(ctx).and_then(|x| x.attachments.first()).ok_or(anyhow!("no attachment")); let mut image_id = None; if let Ok(att) = image_att { let data = att.download().await?; image_id = - Image::create(&mut conn, &att.filename, data, msg.author.id.get()).await?.pipe(Some); + Image::create(&mut conn, &att.filename, data, ctx.author().id.get()).await?.pipe(Some); } let mut audio_data = Vec::new(); @@ -193,12 +178,12 @@ pub async fn addaudiomeme(ctx: &Context, msg: &Message, args: Args) -> CommandRe if bytes == 0 { debug!("read 0 bytes from audio reader"); - return util::send(ctx, msg.channel_id, "🔇🔇🔇🔕🔕🔕🔕🔕🔇🔕🔕🔇🔕🔕📣📢📣📢📣", msg.tts) - .map_err(CommandError::from) - .await; + + util::reply(ctx, "🔇🔇🔇🔕🔕🔕🔕🔕🔇🔕🔕🔇🔕🔕📣📢📣📢📣").await?; + return Ok(()); } - let audio_id = Audio::create(&mut conn, audio_data, msg.author.id.get()).await?; + let audio_id = Audio::create(&mut conn, audio_data, ctx.author().id.get()).await?; let save_result = NewMeme { title, @@ -207,24 +192,25 @@ pub async fn addaudiomeme(ctx: &Context, msg: &Message, args: Args) -> CommandRe audio_id: Some(audio_id), metadata_id: 0, } - .save(&mut conn, msg.author.id.get()) + .save(&mut conn, ctx.author().id.get()) .await .map(|_| {}); use diesel::result::DatabaseErrorKind; match save_result { Ok(_) => { - msg.react(&ctx, ReactionType::Unicode("👌".to_owned())).await?; + util::react(ctx, ReactionType::Unicode("👌".to_owned())).await?; }, Err(e) => { if let Some(DieselError::DatabaseError(DatabaseErrorKind::UniqueViolation, _)) = e.downcast_ref::<DieselError>() { error!("tried to create meme that already exists"); - msg.react(&ctx, ReactionType::Unicode("❌".to_owned())).await?; - return util::send(ctx, msg.channel_id, "that meme already exists", msg.tts) - .map_err(CommandError::from) - .await; + + util::react(ctx, ReactionType::Unicode("❌".to_owned())).await?; + util::reply(ctx, "that meme already exists").await?; + + return Ok(()); } return Err(e.into()); diff --git a/src/commands/meme/delete.rs b/src/commands/meme/delete.rs index 6af1b6b..25ddf0d 100644 --- a/src/commands/meme/delete.rs +++ b/src/commands/meme/delete.rs @@ -3,42 +3,33 @@ use diesel::{ NotFound, }; use log::info; -use serenity::{ - all::ReactionType, - framework::standard::{ - macros::command, - Args, - CommandResult, - }, - model::channel::Message, - prelude::*, -}; +use serenity::all::ReactionType; use crate::{ db::{ connection, delete_meme, }, + msg, util, + PoiseContext, }; -#[command] -#[aliases("delmem")] -pub async fn delmeme(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { - let title = args.single_quoted::<String>()?; - +#[poise::command(slash_command, prefix_command, guild_only, category = "memes", aliases("delmem"))] +pub async fn delmeme(ctx: PoiseContext<'_>, title: String) -> anyhow::Result<()> { let mut conn = connection().await?; - match delete_meme(&mut conn, &title, msg.author.id.get()).await { + match delete_meme(&mut conn, &title, ctx.author().id.get()).await { Ok(_) => { - msg.react(ctx, ReactionType::Unicode("💀".to_owned())).await?; + util::react(ctx, ReactionType::Unicode("💀".to_owned())).await?; Ok(()) }, Err(e) => { if let Some(NotFound) = e.downcast_ref::<DieselError>() { - msg.react(&ctx, ReactionType::Unicode("❓".to_owned())).await?; info!("attempted to delete nonexistent meme: '{}'", title); - util::send(ctx, msg.channel_id, "nice try", msg.tts).await?; + + util::react(ctx, ReactionType::Unicode("❓".to_owned())).await?; + util::reply(ctx, "nice try").await?; return Ok(()); } diff --git a/src/commands/meme/history.rs b/src/commands/meme/history.rs index edc75cd..cfd78df 100644 --- a/src/commands/meme/history.rs +++ b/src/commands/meme/history.rs @@ -1,4 +1,3 @@ -use anyhow::anyhow; use diesel::{ result::Error as DieselError, NotFound, @@ -11,18 +10,10 @@ use log::{ info, }; use serenity::{ - framework::standard::{ - macros::command, - Args, - CommandError, - CommandResult, - }, futures::{ StreamExt, - TryFutureExt, TryStreamExt, }, - model::channel::Message, prelude::*, }; use tap::Pipe; @@ -32,6 +23,7 @@ use timeago::{ }; use crate::{ + commands::game::get_user_id, db::{ self, connection, @@ -40,6 +32,7 @@ use crate::{ Metadata, }, util, + PoiseContext, CONFIG, }; @@ -55,9 +48,14 @@ lazy_static! { static CLEAN_DATE_FORMAT: &str = "%b %-e %Y"; -#[command] -#[aliases("what", "hwaet", "hwæt")] -pub async fn wat(ctx: &Context, msg: &Message, _: Args) -> CommandResult { +#[poise::command( + slash_command, + prefix_command, + guild_only, + category = "memes", + aliases("what", "hwaet", "hwæt") +)] +pub async fn wat(ctx: PoiseContext<'_>) -> anyhow::Result<()> { let mut conn = connection().await?; let record = match InvocationRecord::last(&mut conn).await { @@ -65,12 +63,12 @@ pub async fn wat(ctx: &Context, msg: &Message, _: Args) -> CommandResult { Err(e) => { if let Some(NotFound) = e.downcast_ref::<DieselError>() { info!("found no memes in history"); - return util::send(ctx, msg.channel_id, "no one has ever memed before", msg.tts) - .map_err(CommandError::from) - .await; + + util::reply(ctx, "no one has ever memed before").await?; + return Ok(()); } - util::send(ctx, msg.channel_id, "BAD MEME BAD MEME", msg.tts).await?; + util::reply(ctx, "BAD MEME BAD MEME").await?; return Err(e.into()); }, }; @@ -82,44 +80,41 @@ pub async fn wat(ctx: &Context, msg: &Message, _: Args) -> CommandResult { let metadata = Metadata::find(&mut conn, meme.metadata_id).await?; let author = CONFIG.discord.guild().member(&ctx, metadata.created_by as u64).await?; - util::send( + util::reply( ctx, - msg.channel_id, &format!( "that was \"{}\" by {} ({})", meme.title, author.mention(), metadata.created.date().format(CLEAN_DATE_FORMAT) ), - msg.tts, ) - .await? + .await?; }, Err(e) => { if let Some(NotFound) = e.downcast_ref::<DieselError>() { info!("last meme not found in database"); - return util::send(ctx, msg.channel_id, "heuueueeeeh?", msg.tts) - .await - .map_err(CommandError::from); + + util::reply(ctx, "heuueueeeeh?").await?; + return Ok(()); } - util::send(ctx, msg.channel_id, "do i look like i know what a jpeg is", msg.tts) - .await?; + util::reply(ctx, "do i look like i know what a jpeg is").await?; return Err(e.into()); }, }; - meme.map(|_| {}).map_err(CommandError::from) + let _meme = meme?; + Ok(()) } -#[command] -#[aliases("hist")] -pub async fn history(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { - let n = args.single_quoted::<usize>().unwrap_or(CONFIG.default_hist); - +#[poise::command(slash_command, prefix_command, guild_only, category = "memes", aliases("hist"))] +pub async fn history(ctx: PoiseContext<'_>, n: Option<usize>) -> anyhow::Result<()> { + let n = n.unwrap_or(CONFIG.default_hist); + if n > CONFIG.max_hist { debug!("user requested more than MAX_HIST ({}) items from history", CONFIG.max_hist); - util::send(ctx, msg.channel_id, "YER PUSHIN ME OVER THE FUCKIN LINE", true).await?; + util::reply(ctx, "YER PUSHIN ME OVER THE FUCKIN LINE").await?; } let n = n.min(CONFIG.max_hist); @@ -131,9 +126,9 @@ pub async fn history(ctx: &Context, msg: &Message, mut args: Args) -> CommandRes if records.is_empty() { info!("no memes in history"); - return util::send(ctx, msg.channel_id, "i don't remember anything :(", msg.tts) - .map_err(CommandError::from) - .await; + util::reply(ctx, "i don't remember anything :(").await?; + + return Ok(()); } info!("reporting meme history (len {})", n); @@ -198,19 +193,19 @@ pub async fn history(ctx: &Context, msg: &Message, mut args: Args) -> CommandRes }, }; - Result::<_, CommandError>::Ok(result) + anyhow::Ok(result) }) .try_collect::<Vec<String>>() .await?; let resp = resp.join("\n"); + util::reply(ctx, resp).await?; - util::send(ctx, msg.channel_id, &resp, false).await.map_err(CommandError::from) + Ok(()) } -#[command] -#[aliases("stat")] -pub async fn stats(ctx: &Context, msg: &Message, _: Args) -> CommandResult { +#[poise::command(slash_command, prefix_command, guild_only, category = "memes", aliases("stat"))] +pub async fn stats(ctx: PoiseContext<'_>) -> anyhow::Result<()> { use db; use serenity::model::{ id::UserId, @@ -277,11 +272,12 @@ and *{}* was the most-memed overall ({})"#, stats.most_popular_meme_overall_count, ); - util::send(ctx, msg.channel_id, s, msg.tts).map_err(CommandError::from).await + util::reply(ctx, s).await?; + Ok(()) } -#[command] -pub async fn memers(ctx: &Context, msg: &Message, _args: Args) -> CommandResult { +#[poise::command(slash_command, prefix_command, guild_only, category = "memes")] +pub async fn memers(ctx: PoiseContext<'_>) -> anyhow::Result<()> { use serenity::model::id::UserId; let s = db::memers() @@ -302,25 +298,25 @@ pub async fn memers(ctx: &Context, msg: &Message, _args: Args) -> CommandResult info.most_used_meme_count, ); - Result::<_, CommandError>::Ok(res) + anyhow::Ok(res) }) .try_collect::<Vec<String>>() .await? .into_iter() .join("\n"); - util::send(ctx, msg.channel_id, &s, msg.tts).map_err(CommandError::from).await + util::reply(ctx, s).await?; + + Ok(()) } -#[command] -pub async fn query(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { +#[poise::command(prefix_command, guild_only, category = "memes")] +pub async fn query(ctx: PoiseContext<'_>, rest: util::RestVec) -> anyhow::Result<()> { use regex::Regex; use serenity::model::id::UserId; - use std::borrow::Borrow; use crate::{ db, - game::get_user_id, CONFIG, }; @@ -329,42 +325,31 @@ pub async fn query(ctx: &Context, msg: &Message, mut args: Args) -> CommandResul static ref AGE_REGEX: Regex = Regex::new(r"(?i)(?:age|order)=(.*)").unwrap(); } - let creator: Option<u64> = { - let guild = - msg.channel_id.to_channel(&ctx).await?.guild().ok_or(anyhow!("couldn't find guild"))?; + let mut rest = rest.into_inner(); - let guild = guild.guild(&ctx).ok_or(anyhow!("couldn't find guild"))?; + let creator: Option<u64> = try { + let fst = rest.first()?; + let captures = CREATOR_REGEX.captures(fst)?; + let creator = captures.get(1)?.as_str().to_owned(); - let creator = args.quoted().current().map(|s| CREATOR_REGEX.is_match(s)).unwrap_or(false); - if creator { - args.single_quoted::<String>() - .ok() - .and_then(|s| { - CREATOR_REGEX.captures(&s).and_then(|c| c.get(1)).map(|x| x.as_str().to_owned()) - }) - .and_then(|s| get_user_id(guild.borrow(), s).ok().map(UserId::get)) - } else { - None - } + let guild = ctx.guild()?; + let user_id = get_user_id(&guild, creator).ok()?.get(); + rest.pop(); + + user_id }; - let order = { - let order = args.quoted().current().map(|s| AGE_REGEX.is_match(s)).unwrap_or(false); + let order: Option<String> = try { + let fst = rest.first()?; + let captures = AGE_REGEX.captures(fst)?; + let order = captures.get(1)?.as_str().to_owned(); - if order { - args.single_quoted::<String>() - .ok() - .and_then(|s| { - AGE_REGEX.captures(&s).and_then(|c| c.get(1)).map(|x| x.as_str().to_owned()) - }) - .map(|s: String| s.contains("new")) - .unwrap_or(true) - } else { - true - } + order }; - let iter = db::query_meme(args.rest(), creator, order).await?.into_iter(); + let order = order.is_some_and(|o| o.contains("new")); + + let iter = db::query_meme(rest.join(" "), creator, order).await?.into_iter(); let result = iter .pipe(serenity::futures::stream::iter) @@ -380,7 +365,7 @@ pub async fn query(ctx: &Context, msg: &Message, mut args: Args) -> CommandResul meme.content.map_or(0, |s| s.len()), meme.image_id.map_or("NO", |_s| "YES"), meme.audio_id.map_or("NO", |_s| "YES"), - )) as Result<String, CommandError> + )) as anyhow::Result<String> }) .try_collect::<Vec<String>>() .await; @@ -401,10 +386,10 @@ pub async fn query(ctx: &Context, msg: &Message, mut args: Args) -> CommandResul if result.is_empty() { info!("no memes matched query"); - return util::send(ctx, msg.channel_id, "no match".to_owned(), msg.tts) - .map_err(CommandError::from) - .await; + util::reply(ctx, "no match").await?; + return Ok(()); } - util::send(ctx, msg.channel_id, &result, msg.tts).map_err(CommandError::from).await + util::reply(ctx, result).await?; + Ok(()) } diff --git a/src/commands/meme/invoke.rs b/src/commands/meme/invoke.rs index 1400452..e399e82 100644 --- a/src/commands/meme/invoke.rs +++ b/src/commands/meme/invoke.rs @@ -2,19 +2,7 @@ use diesel::{ result::Error as DieselError, NotFound, }; -use itertools::Itertools; use log::info; -use serenity::{ - framework::standard::{ - macros::command, - Args, - CommandError, - CommandResult, - }, - futures::TryFutureExt, - model::channel::Message, - prelude::*, -}; use crate::{ commands::meme::send_meme, @@ -25,42 +13,49 @@ use crate::{ InvocationRecord, }, util, + PoiseContext, }; -#[command] -#[aliases("mem")] -pub async fn meme(ctx: &Context, msg: &Message, args: Args) -> CommandResult { - _meme(ctx, msg, args, AudioPlayback::Optional).await +#[poise::command(slash_command, prefix_command, guild_only, category = "memes", aliases("mem"))] +pub async fn meme(ctx: PoiseContext<'_>, #[rest] rest: String) -> anyhow::Result<()> { + _meme(ctx, rest, AudioPlayback::Optional).await } -#[command] -pub async fn omen(ctx: &Context, msg: &Message, _args: Args) -> CommandResult { - let args = Args::new("", &[]); - _meme(ctx, msg, args, AudioPlayback::Optional).await +#[poise::command(slash_command, prefix_command, guild_only, category = "memes")] +pub async fn omen(ctx: PoiseContext<'_>) -> anyhow::Result<()> { + _meme(ctx, "", AudioPlayback::Optional).await } -#[command] -pub async fn silentomen(ctx: &Context, msg: &Message, _args: Args) -> CommandResult { - let args = Args::new("", &[]); - _meme(ctx, msg, args, AudioPlayback::Prohibited).await +#[poise::command(slash_command, prefix_command, guild_only, category = "memes")] +pub async fn silentomen(ctx: PoiseContext<'_>) -> anyhow::Result<()> { + _meme(ctx, "", AudioPlayback::Prohibited).await } -#[command] -pub async fn audioomen(ctx: &Context, msg: &Message, _args: Args) -> CommandResult { - let args = Args::new("", &[]); - _meme(ctx, msg, args, AudioPlayback::Required).await +#[poise::command(slash_command, prefix_command, guild_only, category = "memes")] +pub async fn audioomen(ctx: PoiseContext<'_>) -> anyhow::Result<()> { + _meme(ctx, "", AudioPlayback::Required).await } -#[command] -#[aliases("audiomeme", "audiomem")] -pub async fn audio_meme(ctx: &Context, msg: &Message, args: Args) -> CommandResult { - _meme(ctx, msg, args, AudioPlayback::Required).await +#[poise::command( + slash_command, + prefix_command, + guild_only, + category = "memes", + aliases("audiomeme", "audiomem") +)] +pub async fn audio_meme(ctx: PoiseContext<'_>, #[rest] rest: String) -> anyhow::Result<()> { + _meme(ctx, rest, AudioPlayback::Required).await } -#[command] -#[aliases("silentmeme", "silentmem")] -pub async fn silent_meme(ctx: &Context, msg: &Message, args: Args) -> CommandResult { - _meme(ctx, msg, args, AudioPlayback::Prohibited).await +#[poise::command( + slash_command, + prefix_command, + guild_only, + category = "memes", + aliases("silentmeme", "silentmem") +)] +pub async fn silent_meme(ctx: PoiseContext<'_>, #[rest] rest: String) -> anyhow::Result<()> { + _meme(ctx, rest, AudioPlayback::Prohibited).await } #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] @@ -71,21 +66,20 @@ enum AudioPlayback { } async fn _meme( - ctx: &Context, - msg: &Message, - args: Args, + ctx: PoiseContext<'_>, + args: impl AsRef<str>, audio_playback: AudioPlayback, -) -> CommandResult { +) -> anyhow::Result<()> { + let args = args.as_ref().trim(); + if args.is_empty() || audio_playback != AudioPlayback::Optional { - return rand_meme(ctx, msg, audio_playback).await; + return rand_meme(ctx, audio_playback).await; } - let search = args.raw().join(" "); - let mut conn = connection().await?; - let mem = match find_meme(&mut conn, search).await { + let mem = match find_meme(&mut conn, args).await { Ok(x) => { - InvocationRecord::create(&mut conn, msg.author.id.get(), msg.id.get(), x.id, false) + InvocationRecord::create(&mut conn, ctx.author().id.get(), ctx.id(), x.id, false) .await?; x @@ -93,27 +87,23 @@ async fn _meme( Err(e) => { return if let Some(NotFound) = e.downcast_ref::<DieselError>() { info!("requested meme not found in database"); - util::send(ctx, msg.channel_id, "c'mon baby, guesstimate", msg.tts) - .await - .map_err(CommandError::from) + + util::reply(ctx, "c'mon baby, guesstimate").await?; + Ok(()) } else { - util::send(ctx, msg.channel_id, "what in ryan's name", msg.tts).await?; + util::reply(ctx, "what in ryan's name").await?; Err(e.into()) }; }, }; - send_meme(ctx, &mem, &mut conn, msg).await + send_meme(ctx, &mem, &mut conn).await } -async fn rand_meme( - ctx: &Context, - message: &Message, - audio_playback: AudioPlayback, -) -> CommandResult { +async fn rand_meme(ctx: PoiseContext<'_>, audio_playback: AudioPlayback) -> anyhow::Result<()> { let mut conn = connection().await?; - let should_audio = util::users_listening(ctx).await?; + let should_audio = util::users_listening(ctx.serenity_context()).await?; let mem = match audio_playback { AudioPlayback::Required => db::rand_audio_meme(&mut conn).await, @@ -123,56 +113,53 @@ async fn rand_meme( match mem { Ok(mem) => { - InvocationRecord::create( - &mut conn, - message.author.id.get(), - message.id.get(), - mem.id, - true, - ) - .await?; - send_meme(ctx, &mem, &mut conn, message).await?; + InvocationRecord::create(&mut conn, ctx.author().id.get(), ctx.id(), mem.id, true) + .await?; + send_meme(ctx, &mem, &mut conn).await?; Ok(()) }, Err(e) => { if let Some(NotFound) = e.downcast_ref::<DieselError>() { info!("random meme not found"); - return util::send(ctx, message.channel_id, "i don't know any :(", message.tts) - .map_err(CommandError::from) - .await; + + util::reply(ctx, "i don't know any :(").await?; + return Ok(()); } - util::send(ctx, message.channel_id, "HELP", message.tts).await?; + util::reply(ctx, "HELP").await?; Err(e.into()) }, } } -#[command] -#[aliases("rarememe", "raremem")] -pub async fn rare_meme(ctx: &Context, msg: &Message, _args: Args) -> CommandResult { - let should_audio = util::users_listening(ctx).await?; +#[poise::command( + slash_command, + prefix_command, + guild_only, + category = "memes", + aliases("raremem", "rarememe") +)] +pub async fn rare_meme(ctx: PoiseContext<'_>) -> anyhow::Result<()> { + let should_audio = util::users_listening(ctx.serenity_context()).await?; let mut conn = connection().await?; let meme = db::rare_meme(&mut conn, should_audio).await; match meme { Ok(meme) => { - InvocationRecord::create(&mut conn, msg.author.id.get(), msg.id.get(), meme.id, true) + InvocationRecord::create(&mut conn, ctx.author().id.get(), ctx.id(), meme.id, true) .await?; - send_meme(ctx, &meme, &mut conn, msg).await + send_meme(ctx, &meme, &mut conn).await }, Err(e) => { if let Some(NotFound) = e.downcast_ref::<DieselError>() { info!("rare meme not found"); - return util::send(ctx, msg.channel_id, "i don't know any :(", msg.tts) - .map_err(CommandError::from) - .await; + util::reply(ctx, "i don't know any :(").await?; + + return Ok(()); } - util::send(ctx, msg.channel_id, "THE MEME MARKET IS IN FREEFALL", msg.tts) - .map_err(CommandError::from) - .await?; + util::reply(ctx, "THE MEME MARKET IS IN FREEFALL").await?; Err(e.into()) }, diff --git a/src/commands/meme/mod.rs b/src/commands/meme/mod.rs index cfe02ee..0108219 100644 --- a/src/commands/meme/mod.rs +++ b/src/commands/meme/mod.rs @@ -8,12 +8,6 @@ use serenity::{ CreateAttachment, CreateMessage, }, - framework::standard::{ - macros::group, - CommandResult, - }, - model::channel::Message, - prelude::*, }; use songbird::input::{ core::{ @@ -26,53 +20,54 @@ use songbird::input::{ Input, }; +pub use self::{ + create::*, + delete::*, + history::*, + invoke::*, +}; use crate::{ - commands::songbird, + commands::playback::songbird, db::{ Audio, Meme, }, + msg, + util, + PoiseContext, CONFIG, }; -pub use self::{ - create::*, - delete::*, - history::*, - invoke::*, -}; - mod create; mod delete; mod history; mod invoke; -#[group] -#[commands( - meme, - audio_meme, - silent_meme, - omen, - audioomen, - silentomen, - addmeme, - addaudiomeme, - delmeme, - wat, - stats, - history, - rare_meme, - memers, - query -)] -struct Memes; +pub fn commands() -> Vec<poise::Command<crate::PoiseData, anyhow::Error>> { + vec![ + meme(), + silent_meme(), + audio_meme(), + rare_meme(), + omen(), + silentomen(), + audioomen(), + addmeme(), + addaudiomeme(), + delmeme(), + history(), + stats(), + memers(), + wat(), + query(), + ] +} async fn send_meme( - ctx: &Context, + ctx: PoiseContext<'_>, t: &Meme, conn: &mut AsyncPgConnection, - msg: &Message, -) -> CommandResult { +) -> anyhow::Result<()> { let should_tts = t.content.as_ref().map(|t| !t.is_empty()).unwrap_or(false) && random::<u32>() % 25 == 0; @@ -95,12 +90,12 @@ async fn send_meme( let image = image?; let att = CreateAttachment::bytes(image.data.as_slice(), &image.filename); - msg.channel_id.send_files(ctx, vec![att], cmsg).await?; + ctx.channel_id().send_files(ctx, vec![att], cmsg).await?; }, None => { if t.content.is_some() { - msg.channel_id.send_message(ctx, cmsg).await?; + ctx.channel_id().send_message(ctx, cmsg).await?; } }, }; @@ -108,7 +103,7 @@ async fn send_meme( if let Some(audio) = audio { let audio = audio?; - let (_sb, call) = songbird(ctx, msg).await?; + let (_sb, call) = songbird(ctx).await?; let mut call = call.lock().await; if call.current_channel().is_none() { @@ -117,7 +112,7 @@ async fn send_meme( call.enqueue_input(Input::Lazy(Box::new(audio))).await; - msg.react(ctx, ReactionType::Unicode("📣".to_owned())).await?; + util::react(ctx, ReactionType::Unicode("📣".to_owned())).await?; } Ok(()) diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 69c9185..ba87adb 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -1,18 +1,14 @@ -use crate::util; -use log::info; -use serenity::framework::{ - standard::macros::group, - StandardFramework, -}; +use poise::builtins::PrettyHelpConfiguration; -#[cfg(feature = "db")] -pub use self::meme::*; -pub use self::{ - playback::*, - roll::ROLL_COMMAND, - today::TODAY_COMMAND, +use crate::{ + commands::playback::_play, + util, + PoiseContext, }; +#[cfg(feature = "games")] +pub mod game; + #[cfg(feature = "db")] pub(crate) mod meme; pub(crate) mod playback; @@ -20,40 +16,52 @@ pub(crate) mod roll; pub(crate) mod sound_levels; pub(crate) mod today; -mod help; +#[cfg(feature = "db")] +pub use self::meme::*; -#[group] -#[only_in(guild)] -#[commands(roll, today)] -struct General; +pub fn commands() -> Vec<poise::Command<crate::PoiseData, anyhow::Error>> { + let mut commands = vec![ + playback::play(), + playback::pause(), + playback::resume(), + playback::die(), + playback::list(), + sound_levels::mute(), + sound_levels::unmute(), + roll::roll(), + help(), + ]; -pub fn register_commands(f: StandardFramework) -> StandardFramework { - let result = f.group(&PLAYBACK_GROUP).group(&GENERAL_GROUP); + #[cfg(feature = "games")] + commands.extend(game::commands()); #[cfg(feature = "db")] - let result = result.group(&MEMES_GROUP); + commands.extend(meme::commands()); - #[cfg(feature = "games")] - let result = result.group(&crate::game::GAME_GROUP); + commands +} + +#[poise::command(slash_command, prefix_command, aliases("halp"))] +pub async fn help(ctx: PoiseContext<'_>, command: Option<String>) -> anyhow::Result<()> { + poise::builtins::pretty_help( + ctx, + command.as_ref().map(|x| x.as_str()), + PrettyHelpConfiguration { + ..Default::default() + }, + ) + .await?; + + Ok(()) +} + +pub async fn unrecognized(ctx: PoiseContext<'_>, u: url::Url) -> anyhow::Result<()> { + if !u.scheme().starts_with("http") { + util::reply(ctx, "format your commands right. fuck you.").await?; + return Ok(()); + } - result.help(&help::HELP).unrecognised_command(|ctx, msg, unrec| { - Box::pin(async move { - let url = match msg.content.split_whitespace().nth(1) { - Some(x) if x.starts_with("http") => x, - _ => { - info!("bad command formatting: '{}'", unrec); - let _ = util::send( - ctx, - msg.channel_id, - "format your commands right. fuck you.", - msg.tts, - ) - .await; - return; - }, - }; + let _ = _play(ctx, &u).await?; - let _ = _play(ctx, msg, url).await; - }) - }) + Ok(()) } diff --git a/src/commands/playback.rs b/src/commands/playback.rs index 1c3ab95..98ae613 100644 --- a/src/commands/playback.rs +++ b/src/commands/playback.rs @@ -1,78 +1,46 @@ +use std::sync::Arc; + use log::{ debug, - error, info, warn, }; -use serenity::{ - framework::standard::{ - macros::{ - command, - group, - }, - Args, - CommandError, - CommandResult, - }, - model::channel::Message, - prelude::*, -}; +use serenity::prelude::*; use songbird::{ input::YoutubeDl, Call, Songbird, }; -use std::sync::Arc; -use tap::Conv; use crate::{ bot::HttpKey, - commands::sound_levels::*, util, + PoiseContext, CONFIG, }; -#[group] -#[commands(skip, pause, resume, list, die, mute, unmute, play)] -#[only_in(guild)] -struct Playback; - -pub async fn songbird( - ctx: &Context, - msg: &Message, -) -> Result<(Arc<Songbird>, Arc<Mutex<Call>>), CommandError> { - let Some(gid) = msg.guild_id else { +pub async fn songbird(ctx: PoiseContext<'_>) -> anyhow::Result<(Arc<Songbird>, Arc<Mutex<Call>>)> { + let Some(gid) = ctx.guild_id() else { return Err(anyhow::anyhow!("no guild id").into()); }; - let sb = songbird::get(ctx).await.expect("acquiring songbird handle"); + let sb = songbird::get(ctx.serenity_context()).await.expect("acquiring songbird handle"); let call = sb.get_or_insert(gid); Ok((sb, call)) } -pub async fn _play(ctx: &Context, msg: &Message, url: &str) -> CommandResult { - use url::{ - Host, - Url, - }; +pub async fn _play(ctx: PoiseContext<'_>, url: &url::Url) -> anyhow::Result<()> { + use url::Host; debug!("playing '{}'", url); - if !url.starts_with("http") { + if !url.scheme().starts_with("http") { warn!("got bad url argument to play: {}", url); - util::send(ctx, msg.channel_id, "bAD LiNk", msg.tts).await?; + + util::reply(ctx, "bAD LiNk").await?; return Ok(()); } - let url = match Url::parse(url) { - Err(e) => { - error!("bad url: {}", e); - util::send(ctx, msg.channel_id, "INVALID URL", msg.tts).await?; - return Ok(()); - }, - Ok(u) => u, - }; - let host = url.host().and_then(|u| match u { Host::Domain(h) => Some(h.to_owned()), _ => None, @@ -81,16 +49,16 @@ pub async fn _play(ctx: &Context, msg: &Message, url: &str) -> CommandResult { if host.map(|h| h.to_lowercase().contains("imgur")).unwrap_or(false) { info!("detected imgur link"); - if msg.author.id.get() == 106160362109272064 { - util::send(ctx, msg.channel_id, "fuck you conway", true).await?; + if ctx.author().id == 106160362109272064 { + util::reply(ctx, "fuck you conway").await?; } else { - util::send(ctx, msg.channel_id, "IMGUR IS BAD, YOU TRASH CAN MAN", msg.tts).await?; + util::reply(ctx, "IMGUR IS BAD, YOU TRASH CAN MAN").await?; } return Ok(()); } - let (_sb, call) = songbird(ctx, msg).await?; + let (_sb, call) = songbird(ctx).await?; let mut call = call.lock().await; if call.current_channel().is_none() { @@ -98,38 +66,31 @@ pub async fn _play(ctx: &Context, msg: &Message, url: &str) -> CommandResult { } let client = { - let data = ctx.data.read().await; + let data = ctx.serenity_context().data.read().await; data.get::<HttpKey>().unwrap().clone() }; - let input = YoutubeDl::new_ytdl_like("yt-dlp", client.clone(), url.conv::<String>()); + let input = YoutubeDl::new_ytdl_like("yt-dlp", client.clone(), url.to_string()); call.enqueue_input(input.into()).await; Ok(()) } -#[command] -pub async fn play(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { - if args.is_empty() { - return _resume(ctx, msg).await; - } - - let url = match args.single::<String>() { - Ok(url) => url, - Err(e) => { - error!("unable to parse url from args: {}", e); - return util::send(ctx, msg.channel_id, "BAD LINK", msg.tts) - .await - .map_err(CommandError::from); - }, +#[poise::command(slash_command, prefix_command, guild_only, category = "playback")] +pub async fn play( + ctx: PoiseContext<'_>, + #[description = "link to play (if absent, resumes playback)"] u: Option<url::Url>, +) -> anyhow::Result<()> { + let Some(u) = u else { + return _resume(ctx).await; }; - _play(ctx, msg, &url).await + _play(ctx, &u).await } -#[command] -pub async fn pause(ctx: &Context, msg: &Message, _: Args) -> CommandResult { - let (_sb, call) = songbird(ctx, msg).await?; +#[poise::command(slash_command, prefix_command, guild_only, category = "playback")] +pub async fn pause(ctx: PoiseContext<'_>) -> anyhow::Result<()> { + let (_sb, call) = songbird(ctx).await?; let call = call.lock().await; call.queue().pause()?; @@ -137,14 +98,19 @@ pub async fn pause(ctx: &Context, msg: &Message, _: Args) -> CommandResult { Ok(()) } -#[command] -#[aliases("continue")] -pub async fn resume(ctx: &Context, msg: &Message, _: Args) -> CommandResult { - _resume(ctx, msg).await +#[poise::command( + slash_command, + prefix_command, + guild_only, + aliases("continue"), + category = "playback" +)] +pub async fn resume(ctx: PoiseContext<'_>) -> anyhow::Result<()> { + _resume(ctx).await } -async fn _resume(ctx: &Context, msg: &Message) -> CommandResult { - let (_sb, call) = songbird(ctx, msg).await?; +async fn _resume(ctx: PoiseContext<'_>) -> anyhow::Result<()> { + let (_sb, call) = songbird(ctx).await?; let call = call.lock().await; call.queue().resume()?; @@ -152,10 +118,9 @@ async fn _resume(ctx: &Context, msg: &Message) -> CommandResult { Ok(()) } -#[command] -#[aliases("next")] -pub async fn skip(ctx: &Context, msg: &Message, _args: Args) -> CommandResult { - let (_sb, call) = songbird(ctx, msg).await?; +#[poise::command(slash_command, prefix_command, guild_only, category = "playback", aliases("next"))] +pub async fn skip(ctx: PoiseContext<'_>) -> anyhow::Result<()> { + let (_sb, call) = songbird(ctx).await?; let call = call.lock().await; call.queue().skip()?; @@ -163,10 +128,15 @@ pub async fn skip(ctx: &Context, msg: &Message, _args: Args) -> CommandResult { Ok(()) } -#[command] -#[aliases("sudoku", "fuckoff", "stop")] -pub async fn die(ctx: &Context, msg: &Message, _: Args) -> CommandResult { - let (_sb, call) = songbird(ctx, msg).await?; +#[poise::command( + slash_command, + prefix_command, + guild_only, + category = "playback", + aliases("sudoku", "fuckoff", "stop") +)] +pub async fn die(ctx: PoiseContext<'_>) -> anyhow::Result<()> { + let (_sb, call) = songbird(ctx).await?; let mut call = call.lock().await; call.queue().stop(); @@ -176,21 +146,26 @@ pub async fn die(ctx: &Context, msg: &Message, _: Args) -> CommandResult { Ok(()) } -#[command] -#[aliases("queue")] -pub async fn list(ctx: &Context, msg: &Message, _: Args) -> CommandResult { - let (_sb, call) = songbird(ctx, msg).await?; +#[poise::command( + slash_command, + prefix_command, + guild_only, + category = "playback", + aliases("queue") +)] +pub async fn list(ctx: PoiseContext<'_>) -> anyhow::Result<()> { + let (_sb, call) = songbird(ctx).await?; let call = call.lock().await; let queue = call.queue(); - util::send(ctx, msg.channel_id, "(command fix work-in-progress)", msg.tts).await?; + util::reply(ctx, "(command fix work-in-progress)").await?; for track in queue.current_queue().into_iter() { let info = track.get_info().await?; - util::send(ctx, msg.channel_id, format!("track playing for {:?}", info.play_time), msg.tts) - .await?; + let fmt = format!("track playing for {:?}", info.play_time); + util::reply(ctx, fmt).await?; } Ok(()) diff --git a/src/commands/roll.rs b/src/commands/roll.rs index 45e3ba8..6cd084c 100644 --- a/src/commands/roll.rs +++ b/src/commands/roll.rs @@ -1,31 +1,18 @@ use std::result::Result as StdResult; +use lazy_static::lazy_static; use log::{ debug, error, }; use rand::prelude::*; -use serenity::{ - framework::standard::{ - macros::command, - Args, - }, - model::channel::Message, - prelude::*, -}; use thiserror::Error; -use lazy_static::lazy_static; -use serenity::{ - framework::standard::{ - CommandError, - CommandResult, - }, - futures::TryFutureExt, +use crate::{ + util, + PoiseContext, }; -use crate::util; - #[derive(pest_derive::Parser)] #[grammar = "commands/calc.pest"] struct Calc; @@ -49,27 +36,26 @@ impl Calc { Pair, Pairs, }, - prec_climber::PrecClimber, + pratt_parser::PrattParser, Parser, }; use self::Rule::*; lazy_static! { - static ref CLIMBER: PrecClimber<Rule> = { - use pest::prec_climber::{ + static ref CLIMBER: PrattParser<Rule> = { + use pest::pratt_parser::{ Assoc::*, - Operator, + Op as Operator, }; - PrecClimber::new(vec![ - Operator::new(add, Left) - | Operator::new(sub, Left) - | Operator::new(modulo, Left), - Operator::new(mul, Left) | Operator::new(div, Left), - Operator::new(dice, Left), - Operator::new(pow, Right), - ]) + PrattParser::new() + .op(Operator::infix(add, Left) + | Operator::infix(sub, Left) + | Operator::infix(modulo, Left)) + .op(Operator::infix(mul, Left) | Operator::infix(div, Left)) + .op(Operator::infix(dice, Left)) + .op(Operator::infix(pow, Right)) }; } @@ -160,29 +146,33 @@ impl Calc { } fn eval_expr(p: Pairs<Rule>) -> StdResult<f64, CalcError> { - CLIMBER.climb(p, eval_single_pair, |lhs, op, rhs| { - let lhs = lhs?; - let rhs = rhs?; + CLIMBER + .map_primary(eval_single_pair) + .map_infix(|lhs, op, rhs| { + let lhs = lhs?; + let rhs = rhs?; - let result = match op.as_rule() { - add => lhs + rhs, - sub => lhs - rhs, - mul => lhs * rhs, - div => lhs / rhs, - pow => lhs.powf(rhs), - dice => { - let dice_count = lhs as usize; - let dice_faces = rhs as usize; + let result = match op.as_rule() { + add => lhs + rhs, + sub => lhs - rhs, + mul => lhs * rhs, + div => lhs / rhs, + pow => lhs.powf(rhs), + dice => { + let dice_count = lhs as usize; + let dice_faces = rhs as usize; - let mut rng = thread_rng(); - (0..dice_count).map(|_| rng.gen_range(1..(dice_faces + 1))).sum::<usize>() - as f64 - }, - _ => unreachable!(), - }; + let mut rng = thread_rng(); + (0..dice_count) + .map(|_| rng.gen_range(1..(dice_faces + 1))) + .sum::<usize>() as f64 + }, + _ => unreachable!(), + }; - Ok(result) - }) + Ok(result) + }) + .parse(p) } eval_expr(result) @@ -212,21 +202,18 @@ mod test { } } -#[command] -#[aliases("calc", "calculate")] -pub async fn roll(ctx: &Context, msg: &Message, args: Args) -> CommandResult { - match Calc::eval(args.rest()) { +#[poise::command(slash_command, prefix_command, guild_only, aliases("calc", "calculate"))] +pub async fn roll(ctx: PoiseContext<'_>, #[rest] rest: String) -> anyhow::Result<()> { + match Calc::eval(&rest) { Ok(result) => { debug!("got calc result '{}'", result); - util::send(ctx, msg.channel_id, &format!("{}", result), msg.tts) - .map_err(CommandError::from) - .await + util::reply(ctx, result.to_string()).await?; }, Err(e) => { - error!("error encountered reading calc '{}': {}", args.rest(), e); - util::send(ctx, msg.channel_id, "I COULDN'T READ THAT YOU FUCK", msg.tts) - .map_err(CommandError::from) - .await + error!("error encountered reading calc '{}': {}", rest, e); + util::reply(ctx, "I COULDN'T READ THAT YOU FUCK").await?; }, } + + Ok(()) } diff --git a/src/commands/sound_levels.rs b/src/commands/sound_levels.rs index 8c75b37..9a6cfc6 100644 --- a/src/commands/sound_levels.rs +++ b/src/commands/sound_levels.rs @@ -1,21 +1,14 @@ -use serenity::{ - framework::standard::{ - macros::command, - Args, - CommandResult, - }, - model::channel::Message, - prelude::*, +use crate::{ + commands::playback::songbird, + PoiseContext, }; -use crate::commands::songbird; - pub const DEFAULT_VOLUME: f32 = 0.20; const MAX_VOLUME: f32 = 5.0; -#[command] -pub async fn mute(ctx: &Context, msg: &Message, _: Args) -> CommandResult { - let (_sb, call) = songbird(ctx, msg).await?; +#[poise::command(slash_command, prefix_command, guild_only)] +pub async fn mute(ctx: PoiseContext<'_>) -> anyhow::Result<()> { + let (_sb, call) = songbird(ctx).await?; let mut call = call.lock().await; call.mute(true).await?; @@ -23,66 +16,12 @@ pub async fn mute(ctx: &Context, msg: &Message, _: Args) -> CommandResult { Ok(()) } -#[command] -pub async fn unmute(ctx: &Context, msg: &Message, _: Args) -> CommandResult { - let (_sb, call) = songbird(ctx, msg).await?; +#[poise::command(slash_command, prefix_command, guild_only)] +pub async fn unmute(ctx: PoiseContext<'_>) -> anyhow::Result<()> { + let (_sb, call) = songbird(ctx).await?; let mut call = call.lock().await; call.mute(true).await?; Ok(()) } - -// #[command] -// pub async fn volume(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { -// if args.len() == 0 { -// let vol = { -// let queue_lock = ctx.data.write().await.get::<PlayQueue>().cloned().unwrap(); -// let play_queue = queue_lock.read().unwrap(); -// (play_queue.volume / DEFAULT_VOLUME * 100.0) as usize -// }; -// -// trace!("reporting volume {}", vol); -// -// return util::send(ctx, msg.channel_id, &format!("volume: {}%", vol), msg.tts) -// .map_err(CommandError::from) -// .await; -// } -// -// let vol: usize = match args.single::<f32>() { -// Ok(vol) if vol.is_nan() => { -// warn!("reporting NaN volume"); -// return util::send(ctx, msg.channel_id, "you're a fuck", msg.tts) -// .map_err(CommandError::from) -// .await; -// }, -// Ok(vol) => vol as usize, -// Err(e) => { -// error!("parsing volume arg: {}", e); -// return util::send(ctx, msg.channel_id, "???????", msg.tts) -// .map_err(CommandError::from) -// .await; -// }, -// }; -// -// let mut vol: f32 = (vol as f32) / 100.0; // force aliasing to reasonable values -// let adjusted_text = if vol > MAX_VOLUME { -// format!(" ({:.0}% max)", MAX_VOLUME * 100.0) -// } else { -// "".to_owned() -// }; -// -// vol = vol.clamp(0.0, MAX_VOLUME); -// -// let queue_lock = ctx.data.write().await.get::<PlayQueue>().cloned().unwrap(); -// -// { -// let mut play_queue = queue_lock.write().unwrap(); -// play_queue.volume = vol * DEFAULT_VOLUME; -// info!("volume updated to {}", vol); -// } -// -// util::send(ctx, msg.channel_id, format!("volume adjusted{}", adjusted_text), msg.tts).await?; -// -// Ok(()) -// } diff --git a/src/commands/today/mod.rs b/src/commands/today/mod.rs index 7f1dca7..c1a02d5 100644 --- a/src/commands/today/mod.rs +++ b/src/commands/today/mod.rs @@ -5,22 +5,14 @@ use rand::{ seq::SliceRandom, thread_rng, }; -use serenity::{ - framework::standard::{ - macros::command, - Args, - CommandResult, - }, - model::channel::Message, - prelude::*, -}; use songbird::input::YoutubeDl; use tap::Conv; use crate::{ bot::HttpKey, - commands::songbird, + commands::playback::songbird, util, + PoiseContext, CONFIG, }; @@ -66,16 +58,16 @@ lazy_static! { ]; } -#[command] -pub async fn today(ctx: &Context, msg: &Message, _args: Args) -> CommandResult { +#[poise::command(slash_command, prefix_command, guild_only)] +pub async fn today(ctx: PoiseContext<'_>, #[rest] _rest: String) -> anyhow::Result<()> { let today = { #[allow(unused_mut)] let mut result = chrono::Local::now().naive_local(); #[cfg(debug_assertions)] { - let dt = _args.parse::<chrono::NaiveDateTime>().or_else(|_| { - _args.parse::<chrono::NaiveDate>().map(|date| { + let dt = _rest.parse::<chrono::NaiveDateTime>().or_else(|_| { + _rest.parse::<chrono::NaiveDate>().map(|date| { let time = chrono::NaiveTime::from_hms_opt(12, 0, 0).unwrap(); date.and_time(time) }) @@ -83,11 +75,11 @@ pub async fn today(ctx: &Context, msg: &Message, _args: Args) -> CommandResult { match dt { Ok(dt) => { - log::debug!("overriding with datetime: {}", dt); + debug!("overriding with datetime: {dt}"); result = dt; }, Err(e) => { - log::debug!("parsing datetime: {:?}", e); + debug!("parsing datetime: {e:?}"); }, }; } @@ -102,7 +94,7 @@ pub async fn today(ctx: &Context, msg: &Message, _args: Args) -> CommandResult { let play_args = options.choose(&mut thread_rng()); if let Some(play_args) = play_args { - let (_sb, call) = songbird(ctx, msg).await?; + let (_sb, call) = songbird(ctx).await?; let mut call = call.lock().await; if call.current_channel().is_none() { @@ -110,7 +102,7 @@ pub async fn today(ctx: &Context, msg: &Message, _args: Args) -> CommandResult { } let client = { - let data = ctx.data.read().await; + let data = ctx.serenity_context().data.read().await; data.get::<HttpKey>().unwrap().clone() }; @@ -130,8 +122,8 @@ pub async fn today(ctx: &Context, msg: &Message, _args: Args) -> CommandResult { }); q.resume()?; } else { - util::send(ctx, msg.channel_id, "no", false).await?; - util::send(ctx, msg.channel_id, ":angry:", false).await?; + util::reply(ctx, "no").await?; + util::reply(ctx, ":angry:").await?; } Ok(()) diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..4ed3e44 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,31 @@ +#![feature(try_blocks)] + +#[cfg(feature = "db")] +pub mod db; + +#[cfg(not(feature = "games"))] +pub mod game { + use serenity::framework::StandardFramework; + + #[inline] + fn register(f: StandardFramework) -> StandardFramework { + return f; + } +} + +pub mod bot; +pub mod commands; +pub mod config; +pub mod log_setup; +pub mod util; + +pub use crate::{ + config::*, + util::*, +}; + +pub type Error = anyhow::Error; +pub type Result<T> = anyhow::Result<T>; + +pub type PoiseData = (); +pub type PoiseContext<'a> = poise::Context<'a, PoiseData, anyhow::Error>; diff --git a/src/main.rs b/src/main.rs index be2fc90..93d4d2b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,36 +15,6 @@ use log::{ info, }; -pub use self::{ - config::*, - util::*, -}; - -#[cfg(feature = "db")] -mod db; - -#[cfg(feature = "games")] -mod game; - -#[cfg(not(feature = "games"))] -mod game { - use serenity::framework::StandardFramework; - - #[inline] - fn register(f: StandardFramework) -> StandardFramework { - return f; - } -} - -mod bot; -mod commands; -mod config; -mod log_setup; -mod util; - -pub type Error = anyhow::Error; -pub type Result<T> = anyhow::Result<T>; - const BACKOFF_FACTOR: f64 = 2.0; const MAX_BACKOFFS: usize = 3; const BACKOFF_INIT: f64 = 100.0; @@ -53,7 +23,7 @@ const MIN_RUN_DURATION: Duration = Duration::from_secs(120); #[tokio::main] async fn main() { - log_setup::init(false).expect("initializing logging"); + thulani::log_setup::init(false).expect("initializing logging"); let mut backoff_count: usize = 0; @@ -61,7 +31,7 @@ async fn main() { let start = Instant::now(); info!("starting bot"); - match bot::run().await { + match thulani::bot::run().await { Err(e) => { error!("error encountered running client: {:?}", e); }, diff --git a/src/util.rs b/src/util/mod.rs index e88e7b0..a0105ac 100644 --- a/src/util.rs +++ b/src/util/mod.rs @@ -1,5 +1,20 @@ +use std::process::Stdio; + use chrono::Duration; +use lazy_static::lazy_static; +use log::debug; +use poise::CreateReply; +use regex::{ + Match, + Regex, +}; use serenity::{ + all::{ + CreateMessage, + Message, + Reaction, + ReactionType, + }, client::Context, model::{ id::{ @@ -9,28 +24,21 @@ use serenity::{ permissions::Permissions, }, }; -use std::process::Stdio; - -use lazy_static::lazy_static; -use log::debug; -use regex::{ - Match, - Regex, -}; -use serenity::all::{ - CreateMessage, - Message, -}; use url::Url; use crate::{ - commands::songbird, + commands::playback::songbird, + PoiseContext, Result, CONFIG, }; -pub async fn currently_playing(ctx: &Context, msg: &Message) -> bool { - let (_sb, call) = songbird(ctx, msg).await.expect("no songbird"); +mod rest_vec; + +pub use rest_vec::RestVec; + +pub async fn currently_playing(ctx: PoiseContext<'_>) -> bool { + let (_sb, call) = songbird(ctx).await.expect("no songbird"); let call = call.lock().await; call.queue().current().is_some() @@ -53,6 +61,27 @@ pub async fn users_listening(ctx: &Context) -> Result<bool> { } #[inline] +pub fn msg(ctx: PoiseContext<'_>) -> Option<&Message> { + match ctx { + PoiseContext::Prefix(poise::PrefixContext { + msg, + .. + }) => Some(msg), + _ => None, + } +} + +#[inline] +pub fn tts(ctx: PoiseContext<'_>) -> Option<bool> { + msg(ctx).map(|msg| msg.tts) +} + +#[inline] +pub fn unwrap_tts(ctx: PoiseContext<'_>) -> bool { + tts(ctx).unwrap_or(false) +} + +#[inline] pub async fn send( ctx: &Context, channel: ChannelId, @@ -62,6 +91,21 @@ pub async fn send( send_result(ctx, channel, text, tts).await.map(|_| ()) } +#[inline] +pub async fn reply(ctx: PoiseContext<'_>, text: impl AsRef<str>) -> Result<poise::ReplyHandle> { + let handle = + poise::send_reply(ctx, CreateReply::default().tts(unwrap_tts(ctx)).content(text.as_ref())) + .await?; + + Ok(handle) +} + +#[inline] +pub async fn react(ctx: PoiseContext<'_>, react: ReactionType) -> Result<Reaction> { + let react = msg(ctx).ok_or_else(|| anyhow::anyhow!("elp"))?.react(ctx, react).await?; + Ok(react) +} + pub async fn send_result( ctx: &Context, channel: ChannelId, @@ -105,7 +149,7 @@ pub async fn ytdl_url(uri: &str) -> Result<String> { lazy_static! { static ref YTDL_COMMAND: String = { let result = CONFIG.ytdl.clone().unwrap_or("youtube-dl".to_owned()); - log::debug!("got ytdl: {}", result); + debug!("got ytdl: {}", result); result }; diff --git a/src/util/rest_vec.rs b/src/util/rest_vec.rs new file mode 100644 index 0000000..82889cd --- /dev/null +++ b/src/util/rest_vec.rs @@ -0,0 +1,84 @@ +use serenity::all::{ + Context, + Message, +}; +use std::error::Error; + +/// Pop a whitespace-separated word from the front of the arguments. Supports quotes and quote +/// escaping. +/// +/// Leading whitespace will be trimmed; trailing whitespace is not consumed. +// From https://github.com/serenity-rs/poise/blob/current/src/prefix_argument/mod.rs +fn pop_string(args: &str) -> Result<(&str, String), poise::TooFewArguments> { + // TODO: consider changing the behavior to parse quotes literally if they're in the middle + // of the string: + // - `"hello world"` => `hello world` + // - `"hello "world"` => `"hello "world` + // - `"hello" world"` => `hello` + + let args = args.trim_start(); + if args.is_empty() { + return Err(poise::TooFewArguments::default()); + } + + let mut output = String::new(); + let mut inside_string = false; + let mut escaping = false; + + let mut chars = args.chars(); + // .clone().next() is poor man's .peek(), but we can't do peekable because then we can't + // call as_str on the Chars iterator + while let Some(c) = chars.clone().next() { + if escaping { + output.push(c); + escaping = false; + } else if !inside_string && c.is_whitespace() { + break; + } else if c == '"' { + inside_string = !inside_string; + } else if c == '\\' { + escaping = true; + } else { + output.push(c); + } + + chars.next(); + } + + Ok((chars.as_str(), output)) +} + +pub struct RestVec(Vec<String>); + +impl RestVec { + #[inline] + pub fn into_inner(self) -> Vec<String> { + self.0 + } +} + +impl From<RestVec> for Vec<String> { + #[inline] + fn from(value: RestVec) -> Self { + value.0 + } +} + +#[poise::async_trait] +impl<'a> poise::PopArgument<'a> for RestVec { + async fn pop_from( + mut args: &'a str, + attachment_index: usize, + _ctx: &Context, + _msg: &Message, + ) -> Result<(&'a str, usize, Self), (Box<dyn Error + Send + Sync>, Option<String>)> { + let mut v = vec![]; + + while let Ok((remaining, s)) = pop_string(args) { + args = remaining; + v.push(s); + } + + Ok(("", attachment_index, Self(v))) + } +} |
