diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 00000000..958d7fca --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,3 @@ +[build] +rustflags = ["-C", "target-cpu=native"] + diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 39bd4a5a..d7e55963 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -3,16 +3,19 @@ # todo: don't use docker/build-push-action # todo: run builds on pull request -name: Build and push API Docker image +name: Build and push Rust service Docker images on: push: - branches: - - main paths: - 'lib/libpk/**' - 'services/api/**' + - 'services/gateway/**' + - 'services/avatars/**' - '.github/workflows/rust.yml' - 'Dockerfile.rust' + - 'Dockerfile.bin' + - 'Cargo.toml' + - 'Cargo.lock' jobs: deploy: @@ -45,7 +48,7 @@ jobs: # add more binaries here - run: | - for binary in "api"; do + for binary in "api" "gateway" "avatars"; do for tag in latest ${{ env.BRANCH_NAME }} ${{ github.sha }}; do cat Dockerfile.bin | sed "s/__BINARY__/$binary/g" | docker build -t ghcr.io/pluralkit/$binary:$tag -f - . done diff --git a/.github/workflows/rustfmt.yml b/.github/workflows/rustfmt.yml new file mode 100644 index 00000000..7be5f128 --- /dev/null +++ b/.github/workflows/rustfmt.yml @@ -0,0 +1,16 @@ +name: "Check Rust formatting" +on: + push: + pull_request: + +jobs: + rustfmt: + name: cargo fmt + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + components: rustfmt + - name: Rustfmt Check + uses: actions-rust-lang/rustfmt@v1 diff --git a/.github/workflows/scheduled_tasks.yml b/.github/workflows/scheduled_tasks.yml index fb863ecd..7bbd4450 100644 --- a/.github/workflows/scheduled_tasks.yml +++ b/.github/workflows/scheduled_tasks.yml @@ -2,8 +2,9 @@ name: Build scheduled tasks runner Docker image on: push: - branches: [main] + branches: [main, gateway-service] paths: + - .github/workflows/scheduled_tasks.yml - 'services/scheduled_tasks/**' jobs: diff --git a/Cargo.lock b/Cargo.lock index 12e7ae2d..f69aef72 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,12 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + [[package]] name = "ahash" version = "0.7.8" @@ -173,6 +179,28 @@ dependencies = [ "num-traits", ] +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "attohttpc" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fcf00bc6d5abb29b5f97e3c61a90b6d3caa12f3faf897d4a3e3607c050a35a7" +dependencies = [ + "http 0.2.8", + "log", + "rustls 0.20.9", + "serde", + "serde_json", + "url", + "webpki", + "webpki-roots 0.22.6", +] + [[package]] name = "atty" version = "0.2.14" @@ -190,6 +218,57 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "avatars" +version = "0.1.0" +dependencies = [ + "anyhow", + "axum 0.7.5", + "data-encoding", + "form_urlencoded", + "futures", + "gif", + "image", + "libpk", + "reqwest 0.12.8", + "rust-s3", + "serde", + "sha2", + "sqlx", + "thiserror", + "time", + "tokio", + "tracing", + "uuid", + "webp", +] + +[[package]] +name = "aws-creds" +version = "0.34.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3776743bb68d4ad02ba30ba8f64373f1be4e082fe47651767171ce75bb2f6cf5" +dependencies = [ + "attohttpc", + "dirs", + "log", + "quick-xml", + "rust-ini 0.18.0", + "serde", + "thiserror", + "time", + "url", +] + +[[package]] +name = "aws-region" +version = "0.25.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9aed3f9c7eac9be28662fdb3b0f4d1951e812f7c64fed4f0327ba702f459b3b" +dependencies = [ + "thiserror", +] + [[package]] name = "axum" version = "0.6.7" @@ -305,7 +384,7 @@ dependencies = [ "cc", "cfg-if", "libc", - "miniz_oxide", + "miniz_oxide 0.7.4", "object", "rustc-demangle", ] @@ -391,6 +470,12 @@ version = "3.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535" +[[package]] +name = "bytemuck" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8334215b81e418a0a7bdb8ef0849474f40bb10c8b71f1c4ed315cff49f32494d" + [[package]] name = "byteorder" version = "1.5.0" @@ -450,6 +535,11 @@ name = "cc" version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41c270e7540d725e65ac7f1b212ac8ce349719624d7bcff99f8e2e488e8cf03f" +dependencies = [ + "jobserver", + "libc", + "once_cell", +] [[package]] name = "cfg-if" @@ -465,31 +555,32 @@ checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" dependencies = [ "android-tzdata", "iana-time-zone", + "js-sys", "num-traits", + "wasm-bindgen", "windows-targets 0.52.6", ] [[package]] -name = "commands" -version = "0.1.0" -dependencies = [ - "lazy_static", - "uniffi", -] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" [[package]] name = "config" -version = "0.13.4" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23738e11972c7643e4ec947840fc463b6a571afcd3e735bdfce7d03c7a784aca" +checksum = "7328b20597b53c2454f0b1919720c25c7339051c02b72b7e05409e00b14132be" dependencies = [ "async-trait", + "convert_case", "json5", "lazy_static", "nom", "pathdiff", "ron", - "rust-ini", + "rust-ini 0.19.0", "serde", "serde_json", "toml", @@ -502,12 +593,51 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "const-random" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom", + "once_cell", + "tiny-keccak", +] + +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "cookie-factory" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "396de984970346b0d9e93d1415082923c679e5ae5c3ee3dcbd104f5610af126b" +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.6" @@ -544,6 +674,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "338089f42c427b86394a5ee60ff321da23a5c89c9d89514c829687b26359fcff" +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + [[package]] name = "crossbeam-epoch" version = "0.9.14" @@ -572,6 +711,12 @@ version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + [[package]] name = "crypto-common" version = "0.1.6" @@ -582,6 +727,19 @@ dependencies = [ "typenum", ] +[[package]] +name = "dashmap" +version = "5.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "907076dfda823b0b36d2a1bb5f90c96660a5bbcd7729e10727f07858f22c4edc" +dependencies = [ + "cfg-if", + "hashbrown 0.12.3", + "lock_api", + "once_cell", + "parking_lot_core 0.9.7", +] + [[package]] name = "data-encoding" version = "2.6.0" @@ -599,6 +757,16 @@ dependencies = [ "zeroize", ] +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", + "serde", +] + [[package]] name = "digest" version = "0.9.0" @@ -620,6 +788,26 @@ dependencies = [ "subtle", ] +[[package]] +name = "dirs" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + [[package]] name = "dispatch" version = "0.1.0" @@ -627,7 +815,7 @@ dependencies = [ "anyhow", "axum 0.7.5", "hickory-client", - "reqwest", + "reqwest 0.12.8", "serde", "serde_json", "tokio", @@ -641,6 +829,15 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0688c2a7f92e427f44895cd63841bff7b29f8d7a1648b9e7e07a4a365b2e1257" +[[package]] +name = "dlv-list" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f" +dependencies = [ + "const-random", +] + [[package]] name = "dotenvy" version = "0.15.7" @@ -656,6 +853,15 @@ dependencies = [ "serde", ] +[[package]] +name = "encoding_rs" +version = "0.8.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" +dependencies = [ + "cfg-if", +] + [[package]] name = "endian-type" version = "0.1.2" @@ -726,12 +932,32 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" +[[package]] +name = "fdeflate" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8090f921a24b04994d9929e204f50b498a33ea6ba559ffaa05e04f7ee7fb5ab" +dependencies = [ + "simd-adler32", +] + [[package]] name = "fixedbitset" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" +[[package]] +name = "flate2" +version = "1.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" +dependencies = [ + "crc32fast", + "libz-sys", + "miniz_oxide 0.7.4", +] + [[package]] name = "float-cmp" version = "0.8.0" @@ -791,7 +1017,7 @@ dependencies = [ "sha-1", "tokio", "tokio-stream", - "tokio-util", + "tokio-util 0.6.10", "tracing", "url", ] @@ -905,6 +1131,30 @@ dependencies = [ "slab", ] +[[package]] +name = "gateway" +version = "0.1.0" +dependencies = [ + "anyhow", + "axum 0.7.5", + "bytes", + "chrono", + "fred", + "futures", + "lazy_static", + "libpk", + "prost", + "serde_json", + "signal-hook", + "tokio", + "tracing", + "twilight-cache-inmemory", + "twilight-gateway", + "twilight-http", + "twilight-model", + "twilight-util", +] + [[package]] name = "generic-array" version = "0.14.6" @@ -933,7 +1183,17 @@ checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi", +] + +[[package]] +name = "gif" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fb2d69b19215e18bb912fa30f7ce15846e301408695e44e0ef719f1da9e19f2" +dependencies = [ + "color_quant", + "weezl", ] [[package]] @@ -949,14 +1209,41 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" [[package]] -name = "goblin" -version = "0.6.1" +name = "h2" +version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d6b4de4a8eb6c46a8c77e1d3be942cb9a8bf073c22374578e5ba4b08ed0ff68" +checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" dependencies = [ - "log", - "plain", - "scroll", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 0.2.8", + "indexmap", + "slab", + "tokio", + "tokio-util 0.7.12", + "tracing", +] + +[[package]] +name = "h2" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e8ac6999421f49a846c2d4411f337e53497d8ec55d67753beffa43c5d9205" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http 1.1.0", + "indexmap", + "slab", + "tokio", + "tokio-util 0.7.12", + "tracing", ] [[package]] @@ -968,6 +1255,12 @@ dependencies = [ "ahash 0.7.8", ] +[[package]] +name = "hashbrown" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" + [[package]] name = "hashbrown" version = "0.14.3" @@ -1102,6 +1395,17 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "hostname" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867" +dependencies = [ + "libc", + "match_cfg", + "winapi", +] + [[package]] name = "http" version = "0.2.8" @@ -1195,6 +1499,7 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", + "h2 0.3.26", "http 0.2.8", "http-body 0.4.5", "httparse", @@ -1217,6 +1522,7 @@ dependencies = [ "bytes", "futures-channel", "futures-util", + "h2 0.4.6", "http 1.1.0", "http-body 1.0.0", "httparse", @@ -1228,6 +1534,38 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-rustls" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" +dependencies = [ + "futures-util", + "http 0.2.8", + "hyper 0.14.24", + "rustls 0.21.12", + "tokio", + "tokio-rustls 0.24.1", +] + +[[package]] +name = "hyper-rustls" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0bea761b46ae2b24eb4aef630d8d1c398157b6fc29e6350ecf090a0b70c952c" +dependencies = [ + "futures-util", + "http 1.1.0", + "hyper 1.3.1", + "hyper-util", + "rustls 0.22.4", + "rustls-native-certs", + "rustls-pki-types", + "tokio", + "tokio-rustls 0.25.0", + "tower-service", +] + [[package]] name = "hyper-rustls" version = "0.27.3" @@ -1238,12 +1576,12 @@ dependencies = [ "http 1.1.0", "hyper 1.3.1", "hyper-util", - "rustls", + "rustls 0.23.10", "rustls-pki-types", "tokio", - "tokio-rustls", + "tokio-rustls 0.26.0", "tower-service", - "webpki-roots", + "webpki-roots 0.26.6", ] [[package]] @@ -1310,13 +1648,19 @@ dependencies = [ ] [[package]] -name = "indexmap" -version = "1.9.3" +name = "image" +version = "0.24.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +checksum = "5690139d2f55868e080017335e4b94cb7414274c74f1669c84fb5feba2c9f69d" dependencies = [ - "autocfg", - "hashbrown 0.12.3", + "bytemuck", + "byteorder", + "color_quant", + "gif", + "jpeg-decoder", + "num-traits", + "png", + "tiff", ] [[package]] @@ -1359,6 +1703,21 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fad582f4b9e86b6caa621cabeb0963332d92eea04729ab12892c2533951e6440" +[[package]] +name = "jobserver" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" +dependencies = [ + "libc", +] + +[[package]] +name = "jpeg-decoder" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0" + [[package]] name = "js-sys" version = "0.3.69" @@ -1416,9 +1775,23 @@ dependencies = [ "prost-types", "serde", "sqlx", + "time", "tokio", "tracing", + "tracing-gelf", "tracing-subscriber", + "twilight-model", + "uuid", +] + +[[package]] +name = "libredox" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +dependencies = [ + "bitflags 2.5.0", + "libc", ] [[package]] @@ -1432,6 +1805,27 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "libwebp-sys" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54cd30df7c7165ce74a456e4ca9732c603e8dc5e60784558c1c6dc047f876733" +dependencies = [ + "cc", + "glob", +] + +[[package]] +name = "libz-sys" +version = "1.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c15da26e5af7e25c90b37a2d75cdbf940cf4a55316de9d84c679c9b8bfabf82e" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + [[package]] name = "linked-hash-map" version = "0.5.6" @@ -1464,13 +1858,10 @@ dependencies = [ ] [[package]] -name = "mach" -version = "0.3.2" +name = "match_cfg" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b823e83b2affd8f40a9ee8c29dbc56404c1e34cd2710921f2801e2cf29527afa" -dependencies = [ - "libc", -] +checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" [[package]] name = "matchers" @@ -1487,6 +1878,17 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b87248edafb776e59e6ee64a79086f65890d3510f2c656c000bf2a7e8a0aea40" +[[package]] +name = "maybe-async" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cf92c10c7e361d6b99666ec1c6f9805b0bea2c3bd8c78dc6fe98ac5bd78db11" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.66", +] + [[package]] name = "md-5" version = "0.10.6" @@ -1497,6 +1899,12 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "md5" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" + [[package]] name = "memchr" version = "2.5.0" @@ -1514,58 +1922,45 @@ dependencies = [ [[package]] name = "metrics" -version = "0.20.1" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b9b8653cec6897f73b519a43fba5ee3d50f62fe9af80b428accdcc093b4a849" +checksum = "884adb57038347dfbaf2d5065887b6cf4312330dc8e94bc30a1a839bd79d3261" dependencies = [ - "ahash 0.7.8", - "metrics-macros", - "portable-atomic 0.3.20", + "ahash 0.8.11", + "portable-atomic", ] [[package]] name = "metrics-exporter-prometheus" -version = "0.11.0" +version = "0.15.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8603921e1f54ef386189335f288441af761e0fc61bcb552168d9cedfe63ebc70" +checksum = "b4f0c8427b39666bf970460908b213ec09b3b350f20c0c2eabcbba51704a08e6" dependencies = [ - "hyper 0.14.24", - "indexmap 1.9.3", + "base64 0.22.1", + "http-body-util", + "hyper 1.3.1", + "hyper-util", + "indexmap", "ipnet", "metrics", "metrics-util", - "parking_lot 0.12.1", - "portable-atomic 0.3.20", "quanta", "thiserror", "tokio", "tracing", ] -[[package]] -name = "metrics-macros" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "731f8ecebd9f3a4aa847dfe75455e4757a45da40a7793d2f0b1f9b6ed18b23f3" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.107", -] - [[package]] name = "metrics-util" -version = "0.14.0" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7d24dc2dbae22bff6f1f9326ffce828c9f07ef9cc1e8002e5279f845432a30a" +checksum = "4259040465c955f9f2f1a4a8a16dc46726169bca0f88e8fb2dbeced487c3e828" dependencies = [ "crossbeam-epoch", "crossbeam-utils", - "hashbrown 0.12.3", + "hashbrown 0.14.3", "metrics", "num_cpus", - "parking_lot 0.12.1", - "portable-atomic 0.3.20", "quanta", "sketches-ddsketch", ] @@ -1601,6 +1996,16 @@ dependencies = [ "adler", ] +[[package]] +name = "miniz_oxide" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +dependencies = [ + "adler2", + "simd-adler32", +] + [[package]] name = "mio" version = "1.0.2" @@ -1609,7 +2014,7 @@ checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" dependencies = [ "hermit-abi 0.3.9", "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi", "windows-sys 0.52.0", ] @@ -1665,6 +2070,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + [[package]] name = "num-integer" version = "0.1.46" @@ -1732,16 +2143,41 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "ordered-float" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c" +dependencies = [ + "num-traits", +] + [[package]] name = "ordered-multimap" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccd746e37177e1711c20dd619a1620f34f5c8b569c53590a72dedd5344d8924a" dependencies = [ - "dlv-list", + "dlv-list 0.3.0", "hashbrown 0.12.3", ] +[[package]] +name = "ordered-multimap" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ed8acf08e98e744e5384c8bc63ceb0364e68a6854187221c18df61c4797690e" +dependencies = [ + "dlv-list 0.5.2", + "hashbrown 0.13.2", +] + [[package]] name = "overload" version = "0.1.1" @@ -1874,7 +2310,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" dependencies = [ "fixedbitset", - "indexmap 2.2.6", + "indexmap", ] [[package]] @@ -1937,18 +2373,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" [[package]] -name = "plain" -version = "0.2.3" +name = "png" +version = "0.17.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" - -[[package]] -name = "portable-atomic" -version = "0.3.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e30165d31df606f5726b090ec7592c308a0eaf61721ff64c9a3018e344a8753e" +checksum = "52f9d46a34a05a6a57566bc2bfae066ef07585a6e3fa30fbbdff5936380623f0" dependencies = [ - "portable-atomic 1.8.0", + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide 0.8.0", ] [[package]] @@ -1957,6 +2391,12 @@ version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d30538d42559de6b034bc76fd6dd4c38961b1ee5c6c56e3808c50128fdbc22ce" +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.17" @@ -2047,16 +2487,15 @@ dependencies = [ [[package]] name = "quanta" -version = "0.10.1" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7e31331286705f455e56cca62e0e717158474ff02b7936c1fa596d983f4ae27" +checksum = "8e5167a477619228a0b284fac2674e3c388cba90631d7b7de620e6f1fcd08da5" dependencies = [ "crossbeam-utils", "libc", - "mach", "once_cell", "raw-cpuid", - "wasi 0.10.2+wasi-snapshot-preview1", + "wasi", "web-sys", "winapi", ] @@ -2067,6 +2506,16 @@ version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" +[[package]] +name = "quick-xml" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f50b1c63b38611e7d4d7f68b82d3ad0cc71a2ad2e7f61fc10f1328d917c93cd" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "quinn" version = "0.11.5" @@ -2078,7 +2527,7 @@ dependencies = [ "quinn-proto", "quinn-udp", "rustc-hash", - "rustls", + "rustls 0.23.10", "socket2 0.5.7", "thiserror", "tokio", @@ -2093,9 +2542,9 @@ checksum = "fadfaed2cd7f389d0161bb73eeb07b7b78f8691047a6f3e73caaeae55310a4a6" dependencies = [ "bytes", "rand", - "ring", + "ring 0.17.8", "rustc-hash", - "rustls", + "rustls 0.23.10", "slab", "thiserror", "tinyvec", @@ -2166,11 +2615,11 @@ dependencies = [ [[package]] name = "raw-cpuid" -version = "10.7.0" +version = "11.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c297679cb867470fa8c9f67dbba74a78d78e3e98d7cf2b08d6d71540f797332" +checksum = "cb9ee317cfe3fbd54b36a511efc1edd42e216903c9cd575e686dd68a2ba90d8d" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.5.0", ] [[package]] @@ -2205,6 +2654,17 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom", + "libredox", + "thiserror", +] + [[package]] name = "regex" version = "1.9.4" @@ -2249,6 +2709,49 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" +[[package]] +name = "reqwest" +version = "0.11.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" +dependencies = [ + "base64 0.21.7", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2 0.3.26", + "http 0.2.8", + "http-body 0.4.5", + "hyper 0.14.24", + "hyper-rustls 0.24.2", + "ipnet", + "js-sys", + "log", + "mime", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls 0.21.12", + "rustls-pemfile 1.0.4", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper 0.1.2", + "system-configuration", + "tokio", + "tokio-rustls 0.24.1", + "tokio-util 0.7.12", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "webpki-roots 0.25.4", + "winreg", +] + [[package]] name = "reqwest" version = "0.12.8" @@ -2263,7 +2766,7 @@ dependencies = [ "http-body 1.0.0", "http-body-util", "hyper 1.3.1", - "hyper-rustls", + "hyper-rustls 0.27.3", "hyper-util", "ipnet", "js-sys", @@ -2273,21 +2776,21 @@ dependencies = [ "percent-encoding", "pin-project-lite", "quinn", - "rustls", - "rustls-pemfile", + "rustls 0.23.10", + "rustls-pemfile 2.1.2", "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", "sync_wrapper 1.0.1", "tokio", - "tokio-rustls", + "tokio-rustls 0.26.0", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "webpki-roots", + "webpki-roots 0.26.6", "windows-registry", ] @@ -2305,6 +2808,21 @@ dependencies = [ "tower-service", ] +[[package]] +name = "ring" +version = "0.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin 0.5.2", + "untrusted 0.7.1", + "web-sys", + "winapi", +] + [[package]] name = "ring" version = "0.17.8" @@ -2316,19 +2834,20 @@ dependencies = [ "getrandom", "libc", "spin 0.9.8", - "untrusted", + "untrusted 0.9.0", "windows-sys 0.52.0", ] [[package]] name = "ron" -version = "0.7.1" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88073939a61e5b7680558e6be56b419e208420c2adb92be54921fa6b72283f1a" +checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94" dependencies = [ - "base64 0.13.1", - "bitflags 1.3.2", + "base64 0.21.7", + "bitflags 2.5.0", "serde", + "serde_derive", ] [[package]] @@ -2358,7 +2877,49 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6d5f2436026b4f6e79dc829837d467cc7e9a55ee40e750d716713540715a2df" dependencies = [ "cfg-if", - "ordered-multimap", + "ordered-multimap 0.4.3", +] + +[[package]] +name = "rust-ini" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e2a3bcec1f113553ef1c88aae6c020a369d03d55b58de9869a0908930385091" +dependencies = [ + "cfg-if", + "ordered-multimap 0.6.0", +] + +[[package]] +name = "rust-s3" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b2ac5ff6acfbe74226fa701b5ef793aaa054055c13ebb7060ad36942956e027" +dependencies = [ + "async-trait", + "aws-creds", + "aws-region", + "base64 0.13.1", + "bytes", + "cfg-if", + "futures", + "hex", + "hmac", + "http 0.2.8", + "log", + "maybe-async", + "md5", + "percent-encoding", + "quick-xml", + "reqwest 0.11.27", + "serde", + "serde_derive", + "sha2", + "thiserror", + "time", + "tokio", + "tokio-stream", + "url", ] [[package]] @@ -2386,6 +2947,43 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rustls" +version = "0.20.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b80e3dec595989ea8510028f30c408a4630db12c9cbb8de34203b89d6577e99" +dependencies = [ + "log", + "ring 0.16.20", + "sct", + "webpki", +] + +[[package]] +name = "rustls" +version = "0.21.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" +dependencies = [ + "log", + "ring 0.17.8", + "rustls-webpki 0.101.7", + "sct", +] + +[[package]] +name = "rustls" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf4ef73721ac7bcd79b2b315da7779d8fc09718c6b3d2d1b2d94850eb8c18432" +dependencies = [ + "ring 0.17.8", + "rustls-pki-types", + "rustls-webpki 0.102.4", + "subtle", + "zeroize", +] + [[package]] name = "rustls" version = "0.23.10" @@ -2393,13 +2991,35 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05cff451f60db80f490f3c182b77c35260baace73209e9cdbbe526bfe3a4d402" dependencies = [ "once_cell", - "ring", + "ring 0.17.8", "rustls-pki-types", - "rustls-webpki", + "rustls-webpki 0.102.4", "subtle", "zeroize", ] +[[package]] +name = "rustls-native-certs" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fb85efa936c42c6d5fc28d2629bb51e4b2f4b8a5211e297d599cc5a093792" +dependencies = [ + "openssl-probe", + "rustls-pemfile 2.1.2", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +dependencies = [ + "base64 0.21.7", +] + [[package]] name = "rustls-pemfile" version = "2.1.2" @@ -2416,15 +3036,25 @@ version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "16f1201b3c9a7ee8039bcadc17b7e605e2945b27eee7631788c1bd2b0643674b" +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring 0.17.8", + "untrusted 0.9.0", +] + [[package]] name = "rustls-webpki" version = "0.102.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff448f7e92e913c4b7d4c6d8e4540a1724b319b4152b8aef6d4cf8339712b33e" dependencies = [ - "ring", + "ring 0.17.8", "rustls-pki-types", - "untrusted", + "untrusted 0.9.0", ] [[package]] @@ -2439,6 +3069,15 @@ version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b4b9743ed687d4b4bcedf9ff5eaa7398495ae14e61cba0a295704edbc7decde" +[[package]] +name = "schannel" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" +dependencies = [ + "windows-sys 0.52.0", +] + [[package]] name = "scopeguard" version = "1.1.0" @@ -2446,23 +3085,36 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" [[package]] -name = "scroll" -version = "0.11.0" +name = "sct" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04c565b551bafbef4157586fa379538366e4385d42082f255bfd96e4fe8519da" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" dependencies = [ - "scroll_derive", + "ring 0.17.8", + "untrusted 0.9.0", ] [[package]] -name = "scroll_derive" -version = "0.11.1" +name = "security-framework" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1db149f81d46d2deba7cd3c50772474707729550221e69588478ebf9ada425ae" +checksum = "c627723fd09706bacdb5cf41499e95098555af3c3c29d014dc3c458ef6be11c0" dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.66", + "bitflags 2.5.0", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "317936bbbd05227752583946b9e66d7ce3b489f84e11a94a510b4437fef407d7" +dependencies = [ + "core-foundation-sys", + "libc", ] [[package]] @@ -2483,6 +3135,16 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-value" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c" +dependencies = [ + "ordered-float", + "serde", +] + [[package]] name = "serde_derive" version = "1.0.203" @@ -2514,6 +3176,26 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_repr" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.66", +] + +[[package]] +name = "serde_spanned" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +dependencies = [ + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -2550,6 +3232,12 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "sha1_smol" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012" + [[package]] name = "sha2" version = "0.10.8" @@ -2570,6 +3258,16 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "signal-hook" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" +dependencies = [ + "libc", + "signal-hook-registry", +] + [[package]] name = "signal-hook-registry" version = "1.4.1" @@ -2590,10 +3288,16 @@ dependencies = [ ] [[package]] -name = "siphasher" -version = "0.3.11" +name = "simd-adler32" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + +[[package]] +name = "simdutf8" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f27f6278552951f1f2b8cf9da965d10969b2efdea95a6ec47987ab46edfe263a" [[package]] name = "sketches-ddsketch" @@ -2694,7 +3398,6 @@ dependencies = [ "atoi", "byteorder", "bytes", - "chrono", "crc", "crossbeam-queue", "either", @@ -2706,7 +3409,7 @@ dependencies = [ "futures-util", "hashlink", "hex", - "indexmap 2.2.6", + "indexmap", "log", "memchr", "once_cell", @@ -2718,10 +3421,12 @@ dependencies = [ "smallvec", "sqlformat", "thiserror", + "time", "tokio", "tokio-stream", "tracing", "url", + "uuid", ] [[package]] @@ -2774,7 +3479,6 @@ dependencies = [ "bitflags 2.5.0", "byteorder", "bytes", - "chrono", "crc", "digest 0.10.7", "dotenvy", @@ -2802,7 +3506,9 @@ dependencies = [ "sqlx-core", "stringprep", "thiserror", + "time", "tracing", + "uuid", "whoami", ] @@ -2816,7 +3522,6 @@ dependencies = [ "base64 0.21.7", "bitflags 2.5.0", "byteorder", - "chrono", "crc", "dotenvy", "etcetera", @@ -2841,7 +3546,9 @@ dependencies = [ "sqlx-core", "stringprep", "thiserror", + "time", "tracing", + "uuid", "whoami", ] @@ -2852,7 +3559,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b244ef0a8414da0bed4bb1910426e890b19e5e9bccc27ada6b797d05c55ae0aa" dependencies = [ "atoi", - "chrono", "flume", "futures-channel", "futures-core", @@ -2864,9 +3570,11 @@ dependencies = [ "percent-encoding", "serde", "sqlx-core", + "time", "tracing", "url", "urlencoding", + "uuid", ] [[package]] @@ -2929,6 +3637,27 @@ dependencies = [ "futures-core", ] +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "tempfile" version = "3.10.1" @@ -2979,6 +3708,57 @@ dependencies = [ "once_cell", ] +[[package]] +name = "tiff" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba1310fcea54c6a9a4fd1aad794ecc02c31682f6bfbecdf460bf19533eed1e3e" +dependencies = [ + "flate2", + "jpeg-decoder", + "weezl", +] + +[[package]] +name = "time" +version = "0.3.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + [[package]] name = "tinyvec" version = "1.6.0" @@ -3023,13 +3803,34 @@ dependencies = [ "syn 2.0.66", ] +[[package]] +name = "tokio-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +dependencies = [ + "rustls 0.21.12", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f" +dependencies = [ + "rustls 0.22.4", + "rustls-pki-types", + "tokio", +] + [[package]] name = "tokio-rustls" version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" dependencies = [ - "rustls", + "rustls 0.23.10", "rustls-pki-types", "tokio", ] @@ -3060,12 +3861,74 @@ dependencies = [ ] [[package]] -name = "toml" -version = "0.5.11" +name = "tokio-util" +version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-websockets" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "988c6e20955aa5043e0822cb27093ebaabb430a126cda0223824b6d65ea900c1" +dependencies = [ + "base64 0.21.7", + "bytes", + "fastrand", + "futures-core", + "futures-sink", + "http 1.1.0", + "httparse", + "ring 0.17.8", + "rustls-native-certs", + "rustls-pki-types", + "sha1_smol", + "simdutf8", + "tokio", + "tokio-rustls 0.25.0", + "tokio-util 0.7.12", + "tracing", +] + +[[package]] +name = "toml" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" dependencies = [ "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", ] [[package]] @@ -3166,6 +4029,35 @@ dependencies = [ "valuable", ] +[[package]] +name = "tracing-futures" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2" +dependencies = [ + "pin-project", + "tracing", +] + +[[package]] +name = "tracing-gelf" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c0170f1bf67b749d4377c2da1d99d6e722600051ee53870cfb6f618611e29e" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "hostname", + "serde_json", + "thiserror", + "tokio", + "tokio-util 0.7.12", + "tracing-core", + "tracing-futures", + "tracing-subscriber", +] + [[package]] name = "tracing-log" version = "0.1.3" @@ -3214,6 +4106,105 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" +[[package]] +name = "twilight-cache-inmemory" +version = "0.16.0-rc.1" +source = "git+https://github.com/pluralkit/twilight#41a71cc74441b54ed10e813e76ad040ce4c366e2" +dependencies = [ + "bitflags 2.5.0", + "dashmap", + "serde", + "twilight-model", + "twilight-util", +] + +[[package]] +name = "twilight-gateway" +version = "0.16.0-rc.1" +source = "git+https://github.com/pluralkit/twilight#41a71cc74441b54ed10e813e76ad040ce4c366e2" +dependencies = [ + "bitflags 2.5.0", + "fastrand", + "flate2", + "futures-core", + "futures-sink", + "serde", + "serde_json", + "tokio", + "tokio-websockets", + "tracing", + "twilight-gateway-queue", + "twilight-http", + "twilight-model", +] + +[[package]] +name = "twilight-gateway-queue" +version = "0.16.0-rc.1" +source = "git+https://github.com/pluralkit/twilight#41a71cc74441b54ed10e813e76ad040ce4c366e2" +dependencies = [ + "tokio", + "tracing", +] + +[[package]] +name = "twilight-http" +version = "0.16.0-rc.1" +source = "git+https://github.com/pluralkit/twilight#41a71cc74441b54ed10e813e76ad040ce4c366e2" +dependencies = [ + "fastrand", + "http 1.1.0", + "http-body-util", + "hyper 1.3.1", + "hyper-rustls 0.26.0", + "hyper-util", + "percent-encoding", + "serde", + "serde_json", + "tokio", + "tracing", + "twilight-http-ratelimiting", + "twilight-model", + "twilight-validate", +] + +[[package]] +name = "twilight-http-ratelimiting" +version = "0.16.0-rc.1" +source = "git+https://github.com/pluralkit/twilight#41a71cc74441b54ed10e813e76ad040ce4c366e2" +dependencies = [ + "tokio", + "tracing", +] + +[[package]] +name = "twilight-model" +version = "0.16.0-rc.1" +source = "git+https://github.com/pluralkit/twilight#41a71cc74441b54ed10e813e76ad040ce4c366e2" +dependencies = [ + "bitflags 2.5.0", + "serde", + "serde-value", + "serde_repr", + "time", +] + +[[package]] +name = "twilight-util" +version = "0.16.0-rc.1" +source = "git+https://github.com/pluralkit/twilight#41a71cc74441b54ed10e813e76ad040ce4c366e2" +dependencies = [ + "twilight-model", +] + +[[package]] +name = "twilight-validate" +version = "0.16.0-rc.1" +source = "git+https://github.com/pluralkit/twilight#41a71cc74441b54ed10e813e76ad040ce4c366e2" +dependencies = [ + "twilight-model", +] + [[package]] name = "typenum" version = "1.16.0" @@ -3399,6 +4390,12 @@ dependencies = [ "weedle2", ] +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + [[package]] name = "untrusted" version = "0.9.0" @@ -3422,6 +4419,15 @@ version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" +[[package]] +name = "uuid" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" +dependencies = [ + "serde", +] + [[package]] name = "valuable" version = "0.1.0" @@ -3450,12 +4456,6 @@ dependencies = [ "try-lock", ] -[[package]] -name = "wasi" -version = "0.10.2+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" - [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -3534,6 +4534,19 @@ version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" +[[package]] +name = "wasm-streams" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e072d4e72f700fb3443d8fe94a39315df013eef1104903cdb0a2abd322bbecd" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "web-sys" version = "0.3.69" @@ -3544,6 +4557,41 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webp" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bb5d8e7814e92297b0e1c773ce43d290bef6c17452dafd9fc49e5edb5beba71" +dependencies = [ + "image", + "libwebp-sys", +] + +[[package]] +name = "webpki" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed63aea5ce73d0ff405984102c42de94fc55a6b75765d621c65262469b3c9b53" +dependencies = [ + "ring 0.17.8", + "untrusted 0.9.0", +] + +[[package]] +name = "webpki-roots" +version = "0.22.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c71e40d7d2c34a5106301fb632274ca37242cd0c9d3e64dbece371a40a2d87" +dependencies = [ + "webpki", +] + +[[package]] +name = "webpki-roots" +version = "0.25.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" + [[package]] name = "webpki-roots" version = "0.26.6" @@ -3554,13 +4602,10 @@ dependencies = [ ] [[package]] -name = "weedle2" -version = "4.0.0" +name = "weezl" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e79c5206e1f43a2306fd64bdb95025ee4228960f2e6c5a8b173f3caaf807741" -dependencies = [ - "nom", -] +checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082" [[package]] name = "whoami" @@ -3871,6 +4916,25 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "winnow" +version = "0.6.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + [[package]] name = "yaml-rust" version = "0.4.5" diff --git a/Cargo.toml b/Cargo.toml index 0dc72064..61f5e3fe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,21 +4,43 @@ members = [ "./lib/libpk", "./lib/commands", "./services/api", - "./services/dispatch" + "./services/dispatch", + "./services/gateway", + "./services/avatars" ] [workspace.dependencies] anyhow = "1" axum = "0.7.5" +axum-macros = "0.4.1" +bytes = "1.6.0" +chrono = "0.4" fred = { version = "5.2.0", default-features = false, features = ["tracing", "pool-prefer-active"] } +futures = "0.3.30" lazy_static = "1.4.0" -metrics = "0.20.1" -serde = "1.0.152" +metrics = "0.23.0" +reqwest = { version = "0.12.7" , default-features = false, features = ["rustls-tls", "trust-dns"]} +serde = { version = "1.0.196", features = ["derive"] } serde_json = "1.0.117" -sqlx = { version = "0.7.4", features = ["runtime-tokio", "postgres", "chrono", "macros"] } -tokio = { version = "1.25.0", features = ["full"] } -tracing = "0.1.37" +signal-hook = "0.3.17" +sqlx = { version = "0.7.4", features = ["runtime-tokio", "postgres", "time", "macros", "uuid"] } +time = "0.3.34" +tokio = { version = "1.36.0", features = ["full"] } +tracing = "0.1.40" tracing-subscriber = { version = "0.3.16", features = ["env-filter", "json"] } +uuid = { version = "1.7.0", features = ["serde"] } + +twilight-gateway = { git = "https://github.com/pluralkit/twilight" } +twilight-cache-inmemory = { git = "https://github.com/pluralkit/twilight", features = ["permission-calculator"] } +twilight-util = { git = "https://github.com/pluralkit/twilight", features = ["permission-calculator"] } +twilight-model = { git = "https://github.com/pluralkit/twilight" } +twilight-http = { git = "https://github.com/pluralkit/twilight", default-features = false, features = ["rustls-native-roots"] } + +#twilight-gateway = { path = "../twilight/twilight-gateway" } +#twilight-cache-inmemory = { path = "../twilight/twilight-cache-inmemory", features = ["permission-calculator"] } +#twilight-util = { path = "../twilight/twilight-util", features = ["permission-calculator"] } +#twilight-model = { path = "../twilight/twilight-model" } +#twilight-http = { path = "../twilight/twilight-http", default-features = false, features = ["rustls-native-roots"] } prost = "0.12" prost-types = "0.12" diff --git a/Dockerfile.rust b/Dockerfile.rust index 8f6341ed..2d94fe66 100644 --- a/Dockerfile.rust +++ b/Dockerfile.rust @@ -4,7 +4,7 @@ WORKDIR /build RUN apk add rustup build-base protoc # todo: arm64 target -RUN rustup-init --default-host x86_64-unknown-linux-musl --default-toolchain stable --profile default -y +RUN rustup-init --default-host x86_64-unknown-linux-musl --default-toolchain nightly-2024-08-20 --profile default -y ENV PATH=/root/.cargo/bin:$PATH ENV RUSTFLAGS='-C link-arg=-s' @@ -27,9 +27,15 @@ COPY proto/ /build/proto # this needs to match workspaces in Cargo.toml COPY lib/libpk /build/lib/libpk COPY services/api/ /build/services/api +COPY services/gateway/ /build/services/gateway +COPY services/avatars/ /build/services/avatars RUN cargo build --bin api --release --target x86_64-unknown-linux-musl +RUN cargo build --bin gateway --release --target x86_64-unknown-linux-musl +RUN cargo build --bin avatars --release --target x86_64-unknown-linux-musl FROM scratch COPY --from=binary-builder /build/target/x86_64-unknown-linux-musl/release/api /api +COPY --from=binary-builder /build/target/x86_64-unknown-linux-musl/release/gateway /gateway +COPY --from=binary-builder /build/target/x86_64-unknown-linux-musl/release/avatars /avatars diff --git a/Myriad/Cache/DiscordCacheExtensions.cs b/Myriad/Cache/DiscordCacheExtensions.cs index 6016bc84..cdf701a7 100644 --- a/Myriad/Cache/DiscordCacheExtensions.cs +++ b/Myriad/Cache/DiscordCacheExtensions.cs @@ -100,15 +100,19 @@ public static class DiscordCacheExtensions await cache.SaveChannel(thread); } - public static async Task BotPermissionsIn(this IDiscordCache cache, ulong channelId) + public static async Task BotPermissionsIn(this IDiscordCache cache, ulong guildId, ulong channelId) { - var channel = await cache.GetRootChannel(channelId); + // disable this for now + //if (cache is HttpDiscordCache) + // return await ((HttpDiscordCache)cache).BotChannelPermissions(guildId, channelId); + + var channel = await cache.GetRootChannel(guildId, channelId); if (channel.GuildId != null) { var userId = cache.GetOwnUser(); var member = await cache.TryGetSelfMember(channel.GuildId.Value); - return await cache.PermissionsFor2(channelId, userId, member); + return await cache.PermissionsFor2(guildId, channelId, userId, member); } return PermissionSet.Dm; diff --git a/Myriad/Cache/HTTPDiscordCache.cs b/Myriad/Cache/HTTPDiscordCache.cs new file mode 100644 index 00000000..c27fc349 --- /dev/null +++ b/Myriad/Cache/HTTPDiscordCache.cs @@ -0,0 +1,187 @@ +using Serilog; +using System.Net; +using System.Text.Json; + +using Myriad.Serialization; +using Myriad.Types; + +namespace Myriad.Cache; + +public class HttpDiscordCache: IDiscordCache +{ + private readonly ILogger _logger; + private readonly HttpClient _client; + private readonly Uri _cacheEndpoint; + private readonly int _shardCount; + private readonly ulong _ownUserId; + + private readonly MemoryDiscordCache _innerCache; + + private readonly JsonSerializerOptions _jsonSerializerOptions; + + public EventHandler<(bool?, string)> OnDebug; + + public HttpDiscordCache(ILogger logger, HttpClient client, string cacheEndpoint, int shardCount, ulong ownUserId, bool useInnerCache) + { + _logger = logger; + _client = client; + _cacheEndpoint = new Uri(cacheEndpoint); + _shardCount = shardCount; + _ownUserId = ownUserId; + _jsonSerializerOptions = new JsonSerializerOptions().ConfigureForMyriad(); + if (useInnerCache) _innerCache = new MemoryDiscordCache(ownUserId); + } + + public ValueTask SaveGuild(Guild guild) => _innerCache?.SaveGuild(guild) ?? default; + public ValueTask SaveChannel(Channel channel) => _innerCache?.SaveChannel(channel) ?? default; + public ValueTask SaveUser(User user) => default; + public ValueTask SaveSelfMember(ulong guildId, GuildMemberPartial member) => _innerCache?.SaveSelfMember(guildId, member) ?? default; + public ValueTask SaveRole(ulong guildId, Myriad.Types.Role role) => _innerCache?.SaveRole(guildId, role) ?? default; + public ValueTask SaveDmChannelStub(ulong channelId) => _innerCache?.SaveDmChannelStub(channelId) ?? default; + public ValueTask RemoveGuild(ulong guildId) => _innerCache?.RemoveGuild(guildId) ?? default; + public ValueTask RemoveChannel(ulong channelId) => _innerCache?.RemoveChannel(channelId) ?? default; + public ValueTask RemoveUser(ulong userId) => _innerCache?.RemoveUser(userId) ?? default; + public ValueTask RemoveRole(ulong guildId, ulong roleId) => _innerCache?.RemoveRole(guildId, roleId) ?? default; + + public ulong GetOwnUser() => _ownUserId; + + private async Task QueryCache(string endpoint, ulong guildId) + { + var cluster = _cacheEndpoint.Authority; + if (cluster.Contains(".service.consul")) + // int(((guild_id >> 22) % shard_count) / 16) + cluster = $"cluster{(int)(((guildId >> 22) % (ulong)_shardCount) / 16)}.{cluster}"; + + var response = await _client.GetAsync($"{_cacheEndpoint.Scheme}://{cluster}{endpoint}"); + + if (response.StatusCode == HttpStatusCode.NotFound) + return default; + + if (response.StatusCode != HttpStatusCode.Found) + throw new Exception($"failed to query http cache: {response.StatusCode}"); + + var plaintext = await response.Content.ReadAsStringAsync(); + return JsonSerializer.Deserialize(plaintext, _jsonSerializerOptions); + } + + public async Task TryGetGuild(ulong guildId) + { + var hres = await QueryCache($"/guilds/{guildId}", guildId); + if (_innerCache == null) return hres; + var lres = await _innerCache.TryGetGuild(guildId); + + if (lres == null && hres == null) return null; + if (lres == null) + { + _logger.Warning($"TryGetGuild({guildId}) was only successful on remote cache"); + OnDebug(null, (true, "guild")); + return hres; + } + if (hres == null) + { + _logger.Warning($"TryGetGuild({guildId}) was only successful on local cache"); + OnDebug(null, (false, "guild")); + return lres; + } + return hres; + } + + public async Task TryGetChannel(ulong guildId, ulong channelId) + { + var hres = await QueryCache($"/guilds/{guildId}/channels/{channelId}", guildId); + if (_innerCache == null) return hres; + var lres = await _innerCache.TryGetChannel(guildId, channelId); + if (lres == null && hres == null) return null; + if (lres == null) + { + _logger.Warning($"TryGetChannel({guildId}, {channelId}) was only successful on remote cache"); + OnDebug(null, (true, "channel")); + return hres; + } + if (hres == null) + { + _logger.Warning($"TryGetChannel({guildId}, {channelId}) was only successful on local cache"); + OnDebug(null, (false, "channel")); + return lres; + } + return hres; + } + + // this should be a GetUserCached method on nirn-proxy (it's always called as GetOrFetchUser) + // so just return nothing + public Task TryGetUser(ulong userId) + => Task.FromResult(null); + + public async Task TryGetSelfMember(ulong guildId) + { + var hres = await QueryCache($"/guilds/{guildId}/members/@me", guildId); + if (_innerCache == null) return hres; + var lres = await _innerCache.TryGetSelfMember(guildId); + if (lres == null && hres == null) return null; + if (lres == null) + { + _logger.Warning($"TryGetSelfMember({guildId}) was only successful on remote cache"); + OnDebug(null, (true, "self_member")); + return hres; + } + if (hres == null) + { + _logger.Warning($"TryGetSelfMember({guildId}) was only successful on local cache"); + OnDebug(null, (false, "self_member")); + return lres; + } + return hres; + } + + // public async Task BotChannelPermissions(ulong guildId, ulong channelId) + // { + // // todo: local cache throws rather than returning null + // // we need to throw too, and try/catch local cache here + // var lres = await _innerCache.BotPermissionsIn(guildId, channelId); + // var hres = await QueryCache($"/guilds/{guildId}/channels/{channelId}/permissions/@me", guildId); + // if (lres == null && hres == null) return null; + // if (lres == null) + // { + // _logger.Warning($"TryGetChannel({guildId}, {channelId}) was only successful on remote cache"); + // OnDebug(null, (true, "botchannelperms")); + // return hres; + // } + // if (hres == null) + // { + // _logger.Warning($"TryGetChannel({guildId}, {channelId}) was only successful on local cache"); + // OnDebug(null, (false, "botchannelperms")); + // return lres; + // } + // + // // this one is easy to check, so let's check it + // if ((int)lres != (int)hres) + // { + // // trust local + // _logger.Warning($"got different permissions for {channelId} (local {(int)lres}, remote {(int)hres})"); + // OnDebug(null, (null, "botchannelperms")); + // return lres; + // } + // return hres; + // } + + public async Task> GetGuildChannels(ulong guildId) + { + var hres = await QueryCache>($"/guilds/{guildId}/channels", guildId); + if (_innerCache == null) return hres; + var lres = await _innerCache.GetGuildChannels(guildId); + if (lres == null && hres == null) return null; + if (lres == null) + { + _logger.Warning($"GetGuildChannels({guildId}) was only successful on remote cache"); + OnDebug(null, (true, "guild_channels")); + return hres; + } + if (hres == null) + { + _logger.Warning($"GetGuildChannels({guildId}) was only successful on local cache"); + OnDebug(null, (false, "guild_channels")); + return lres; + } + return hres; + } +} \ No newline at end of file diff --git a/Myriad/Cache/IDiscordCache.cs b/Myriad/Cache/IDiscordCache.cs index e1b4fad4..f7a49bf6 100644 --- a/Myriad/Cache/IDiscordCache.cs +++ b/Myriad/Cache/IDiscordCache.cs @@ -18,11 +18,9 @@ public interface IDiscordCache internal ulong GetOwnUser(); public Task TryGetGuild(ulong guildId); - public Task TryGetChannel(ulong channelId); + public Task TryGetChannel(ulong guildId, ulong channelId); public Task TryGetUser(ulong userId); public Task TryGetSelfMember(ulong guildId); - public Task TryGetRole(ulong roleId); - public IAsyncEnumerable GetAllGuilds(); public Task> GetGuildChannels(ulong guildId); } \ No newline at end of file diff --git a/Myriad/Cache/MemoryDiscordCache.cs b/Myriad/Cache/MemoryDiscordCache.cs index 193d4606..e4ab34b9 100644 --- a/Myriad/Cache/MemoryDiscordCache.cs +++ b/Myriad/Cache/MemoryDiscordCache.cs @@ -137,7 +137,7 @@ public class MemoryDiscordCache: IDiscordCache return Task.FromResult(cg?.Guild); } - public Task TryGetChannel(ulong channelId) + public Task TryGetChannel(ulong _, ulong channelId) { _channels.TryGetValue(channelId, out var channel); return Task.FromResult(channel); @@ -155,19 +155,6 @@ public class MemoryDiscordCache: IDiscordCache return Task.FromResult(guildMember); } - public Task TryGetRole(ulong roleId) - { - _roles.TryGetValue(roleId, out var role); - return Task.FromResult(role); - } - - public IAsyncEnumerable GetAllGuilds() - { - return _guilds.Values - .Select(g => g.Guild) - .ToAsyncEnumerable(); - } - public Task> GetGuildChannels(ulong guildId) { if (!_guilds.TryGetValue(guildId, out var guild)) diff --git a/Myriad/Cache/RedisDiscordCache.cs b/Myriad/Cache/RedisDiscordCache.cs deleted file mode 100644 index fff7920e..00000000 --- a/Myriad/Cache/RedisDiscordCache.cs +++ /dev/null @@ -1,340 +0,0 @@ -using Google.Protobuf; - -using StackExchange.Redis; -using StackExchange.Redis.KeyspaceIsolation; - -using Serilog; - -using Myriad.Types; - -namespace Myriad.Cache; - -#pragma warning disable 4014 -public class RedisDiscordCache: IDiscordCache -{ - private readonly ILogger _logger; - private readonly ulong _ownUserId; - public RedisDiscordCache(ILogger logger, ulong ownUserId) - { - _logger = logger; - _ownUserId = ownUserId; - } - - private ConnectionMultiplexer _redis { get; set; } - - public async Task InitAsync(string addr) - { - _redis = await ConnectionMultiplexer.ConnectAsync(addr); - } - - private IDatabase db => _redis.GetDatabase().WithKeyPrefix("discord:"); - - public async ValueTask SaveGuild(Guild guild) - { - _logger.Verbose("Saving guild {GuildId} to redis", guild.Id); - - var g = new CachedGuild(); - g.Id = guild.Id; - g.Name = guild.Name; - g.OwnerId = guild.OwnerId; - g.PremiumTier = (int)guild.PremiumTier; - - var tr = db.CreateTransaction(); - - tr.HashSetAsync("guilds", guild.Id.HashWrapper(g)); - - foreach (var role in guild.Roles) - { - // Don't call SaveRole because that updates guild state - // and we just got a brand new one :) - // actually with redis it doesn't update guild state, but we're still doing it here because transaction - tr.HashSetAsync("roles", role.Id.HashWrapper(new CachedRole() - { - Id = role.Id, - Name = role.Name, - Position = role.Position, - Permissions = (ulong)role.Permissions, - Mentionable = role.Mentionable, - })); - - tr.HashSetAsync($"guild_roles:{guild.Id}", role.Id, true, When.NotExists); - } - - await tr.ExecuteAsync(); - } - - public async ValueTask SaveChannel(Channel channel) - { - _logger.Verbose("Saving channel {ChannelId} to redis", channel.Id); - - await db.HashSetAsync("channels", channel.Id.HashWrapper(channel.ToProtobuf())); - - if (channel.GuildId != null) - await db.HashSetAsync($"guild_channels:{channel.GuildId.Value}", channel.Id, true, When.NotExists); - - // todo: use a transaction for this? - if (channel.Recipients != null) - foreach (var recipient in channel.Recipients) - await SaveUser(recipient); - } - - public async ValueTask SaveUser(User user) - { - _logger.Verbose("Saving user {UserId} to redis", user.Id); - - var u = new CachedUser() - { - Id = user.Id, - Username = user.Username, - Discriminator = user.Discriminator, - Bot = user.Bot, - }; - - if (user.Avatar != null) - u.Avatar = user.Avatar; - - await db.HashSetAsync("users", user.Id.HashWrapper(u)); - } - - public async ValueTask SaveSelfMember(ulong guildId, GuildMemberPartial member) - { - _logger.Verbose("Saving self member for guild {GuildId} to redis", guildId); - - var gm = new CachedGuildMember(); - foreach (var role in member.Roles) - gm.Roles.Add(role); - - await db.HashSetAsync("members", guildId.HashWrapper(gm)); - } - - public async ValueTask SaveRole(ulong guildId, Myriad.Types.Role role) - { - _logger.Verbose("Saving role {RoleId} in {GuildId} to redis", role.Id, guildId); - - await db.HashSetAsync("roles", role.Id.HashWrapper(new CachedRole() - { - Id = role.Id, - Mentionable = role.Mentionable, - Name = role.Name, - Permissions = (ulong)role.Permissions, - Position = role.Position, - })); - - await db.HashSetAsync($"guild_roles:{guildId}", role.Id, true, When.NotExists); - } - - public async ValueTask SaveDmChannelStub(ulong channelId) - { - // Use existing channel object if present, otherwise add a stub - // We may get a message create before channel create and we want to have it saved - - if (await TryGetChannel(channelId) == null) - await db.HashSetAsync("channels", channelId.HashWrapper(new CachedChannel() - { - Id = channelId, - Type = (int)Channel.ChannelType.Dm, - })); - } - - public async ValueTask RemoveGuild(ulong guildId) - => await db.HashDeleteAsync("guilds", guildId); - - public async ValueTask RemoveChannel(ulong channelId) - { - var oldChannel = await TryGetChannel(channelId); - - if (oldChannel == null) - return; - - await db.HashDeleteAsync("channels", channelId); - - if (oldChannel.GuildId != null) - await db.HashDeleteAsync($"guild_channels:{oldChannel.GuildId.Value}", oldChannel.Id); - } - - public async ValueTask RemoveUser(ulong userId) - => await db.HashDeleteAsync("users", userId); - - public ulong GetOwnUser() => _ownUserId; - - public async ValueTask RemoveRole(ulong guildId, ulong roleId) - { - await db.HashDeleteAsync("roles", roleId); - await db.HashDeleteAsync($"guild_roles:{guildId}", roleId); - } - - public async Task TryGetGuild(ulong guildId) - { - var redisGuild = await db.HashGetAsync("guilds", guildId); - if (redisGuild.IsNullOrEmpty) - return null; - - var guild = ((byte[])redisGuild).Unmarshal(); - - var redisRoles = await db.HashGetAllAsync($"guild_roles:{guildId}"); - - // todo: put this in a transaction or something - var roles = await Task.WhenAll(redisRoles.Select(r => TryGetRole((ulong)r.Name))); - -#pragma warning disable 8619 - return guild.FromProtobuf() with { Roles = roles }; -#pragma warning restore 8619 - } - - public async Task TryGetChannel(ulong channelId) - { - var redisChannel = await db.HashGetAsync("channels", channelId); - if (redisChannel.IsNullOrEmpty) - return null; - - return ((byte[])redisChannel).Unmarshal().FromProtobuf(); - } - - public async Task TryGetUser(ulong userId) - { - var redisUser = await db.HashGetAsync("users", userId); - if (redisUser.IsNullOrEmpty) - return null; - - return ((byte[])redisUser).Unmarshal().FromProtobuf(); - } - - public async Task TryGetSelfMember(ulong guildId) - { - var redisMember = await db.HashGetAsync("members", guildId); - if (redisMember.IsNullOrEmpty) - return null; - - return new GuildMemberPartial() - { - Roles = ((byte[])redisMember).Unmarshal().Roles.ToArray() - }; - } - - public async Task TryGetRole(ulong roleId) - { - var redisRole = await db.HashGetAsync("roles", roleId); - if (redisRole.IsNullOrEmpty) - return null; - - var role = ((byte[])redisRole).Unmarshal(); - - return new Myriad.Types.Role() - { - Id = role.Id, - Name = role.Name, - Position = role.Position, - Permissions = (PermissionSet)role.Permissions, - Mentionable = role.Mentionable, - }; - } - - public IAsyncEnumerable GetAllGuilds() - { - // return _guilds.Values - // .Select(g => g.Guild) - // .ToAsyncEnumerable(); - return new Guild[] { }.ToAsyncEnumerable(); - } - - public async Task> GetGuildChannels(ulong guildId) - { - var redisChannels = await db.HashGetAllAsync($"guild_channels:{guildId}"); - if (redisChannels.Length == 0) - throw new ArgumentException("Guild not found", nameof(guildId)); - -#pragma warning disable 8619 - return await Task.WhenAll(redisChannels.Select(c => TryGetChannel((ulong)c.Name))); -#pragma warning restore 8619 - } -} - -internal static class CacheProtoExt -{ - public static Guild FromProtobuf(this CachedGuild guild) - => new Guild() - { - Id = guild.Id, - Name = guild.Name, - OwnerId = guild.OwnerId, - PremiumTier = (PremiumTier)guild.PremiumTier, - }; - - public static CachedChannel ToProtobuf(this Channel channel) - { - var c = new CachedChannel(); - c.Id = channel.Id; - c.Type = (int)channel.Type; - if (channel.Position != null) - c.Position = channel.Position.Value; - c.Name = channel.Name; - if (channel.PermissionOverwrites != null) - foreach (var overwrite in channel.PermissionOverwrites) - c.PermissionOverwrites.Add(new Overwrite() - { - Id = overwrite.Id, - Type = (int)overwrite.Type, - Allow = (ulong)overwrite.Allow, - Deny = (ulong)overwrite.Deny, - }); - if (channel.GuildId != null) - c.GuildId = channel.GuildId.Value; - - return c; - } - - public static Channel FromProtobuf(this CachedChannel channel) - => new Channel() - { - Id = channel.Id, - Type = (Channel.ChannelType)channel.Type, - Position = channel.Position, - Name = channel.Name, - PermissionOverwrites = channel.PermissionOverwrites - .Select(x => new Channel.Overwrite() - { - Id = x.Id, - Type = (Channel.OverwriteType)x.Type, - Allow = (PermissionSet)x.Allow, - Deny = (PermissionSet)x.Deny, - }).ToArray(), - GuildId = channel.HasGuildId ? channel.GuildId : null, - ParentId = channel.HasParentId ? channel.ParentId : null, - }; - - public static User FromProtobuf(this CachedUser user) - => new User() - { - Id = user.Id, - Username = user.Username, - Discriminator = user.Discriminator, - Avatar = user.HasAvatar ? user.Avatar : null, - Bot = user.Bot, - }; -} - -internal static class RedisExt -{ - // convenience method - public static HashEntry[] HashWrapper(this ulong key, T value) where T : IMessage - => new[] { new HashEntry(key, value.ToByteArray()) }; -} - -public static class ProtobufExt -{ - private static Dictionary _parser = new(); - - public static byte[] Marshal(this IMessage message) => message.ToByteArray(); - - public static T Unmarshal(this byte[] message) where T : IMessage, new() - { - var type = typeof(T).ToString(); - if (_parser.ContainsKey(type)) - return (T)_parser[type].ParseFrom(message); - else - { - _parser.Add(type, new MessageParser(() => new T())); - return Unmarshal(message); - } - } -} \ No newline at end of file diff --git a/Myriad/Extensions/CacheExtensions.cs b/Myriad/Extensions/CacheExtensions.cs index 17660002..0f6fb931 100644 --- a/Myriad/Extensions/CacheExtensions.cs +++ b/Myriad/Extensions/CacheExtensions.cs @@ -13,27 +13,13 @@ public static class CacheExtensions return guild; } - public static async Task GetChannel(this IDiscordCache cache, ulong channelId) + public static async Task GetChannel(this IDiscordCache cache, ulong guildId, ulong channelId) { - if (!(await cache.TryGetChannel(channelId) is Channel channel)) + if (!(await cache.TryGetChannel(guildId, channelId) is Channel channel)) throw new KeyNotFoundException($"Channel {channelId} not found in cache"); return channel; } - public static async Task GetUser(this IDiscordCache cache, ulong userId) - { - if (!(await cache.TryGetUser(userId) is User user)) - throw new KeyNotFoundException($"User {userId} not found in cache"); - return user; - } - - public static async Task GetRole(this IDiscordCache cache, ulong roleId) - { - if (!(await cache.TryGetRole(roleId) is Role role)) - throw new KeyNotFoundException($"Role {roleId} not found in cache"); - return role; - } - public static async ValueTask GetOrFetchUser(this IDiscordCache cache, DiscordApiClient rest, ulong userId) { @@ -47,9 +33,9 @@ public static class CacheExtensions } public static async ValueTask GetOrFetchChannel(this IDiscordCache cache, DiscordApiClient rest, - ulong channelId) + ulong guildId, ulong channelId) { - if (await cache.TryGetChannel(channelId) is { } cacheChannel) + if (await cache.TryGetChannel(guildId, channelId) is { } cacheChannel) return cacheChannel; var restChannel = await rest.GetChannel(channelId); @@ -58,13 +44,13 @@ public static class CacheExtensions return restChannel; } - public static async Task GetRootChannel(this IDiscordCache cache, ulong channelOrThread) + public static async Task GetRootChannel(this IDiscordCache cache, ulong guildId, ulong channelOrThread) { - var channel = await cache.GetChannel(channelOrThread); + var channel = await cache.GetChannel(guildId, channelOrThread); if (!channel.IsThread()) return channel; - var parent = await cache.GetChannel(channel.ParentId!.Value); + var parent = await cache.GetChannel(guildId, channel.ParentId!.Value); return parent; } } \ No newline at end of file diff --git a/Myriad/Extensions/PermissionExtensions.cs b/Myriad/Extensions/PermissionExtensions.cs index 503c5bf2..c55a73d3 100644 --- a/Myriad/Extensions/PermissionExtensions.cs +++ b/Myriad/Extensions/PermissionExtensions.cs @@ -32,23 +32,23 @@ public static class PermissionExtensions PermissionSet.EmbedLinks; public static Task PermissionsForMCE(this IDiscordCache cache, MessageCreateEvent message) => - PermissionsFor2(cache, message.ChannelId, message.Author.Id, message.Member, message.WebhookId != null); + PermissionsFor2(cache, message.GuildId ?? 0, message.ChannelId, message.Author.Id, message.Member, message.WebhookId != null); public static Task - PermissionsForMemberInChannel(this IDiscordCache cache, ulong channelId, GuildMember member) => - PermissionsFor2(cache, channelId, member.User.Id, member); + PermissionsForMemberInChannel(this IDiscordCache cache, ulong guildId, ulong channelId, GuildMember member) => + PermissionsFor2(cache, guildId, channelId, member.User.Id, member); - public static async Task PermissionsFor2(this IDiscordCache cache, ulong channelId, ulong userId, + public static async Task PermissionsFor2(this IDiscordCache cache, ulong guildId, ulong channelId, ulong userId, GuildMemberPartial? member, bool isThread = false) { - if (!(await cache.TryGetChannel(channelId) is Channel channel)) + if (!(await cache.TryGetChannel(guildId, channelId) is Channel channel)) // todo: handle channel not found better return PermissionSet.Dm; if (channel.GuildId == null) return PermissionSet.Dm; - var rootChannel = await cache.GetRootChannel(channelId); + var rootChannel = await cache.GetRootChannel(guildId, channelId); var guild = await cache.GetGuild(channel.GuildId.Value); diff --git a/Myriad/Gateway/ShardConnection.cs b/Myriad/Gateway/ShardConnection.cs index 158762b5..c1fea661 100644 --- a/Myriad/Gateway/ShardConnection.cs +++ b/Myriad/Gateway/ShardConnection.cs @@ -72,7 +72,8 @@ public class ShardConnection: IAsyncDisposable } catch (Exception e) { - _logger.Error(e, "Shard {ShardId}: Error reading from WebSocket"); + // these are never useful + // _logger.Error(e, "Shard {ShardId}: Error reading from WebSocket"); // force close so we can "reset" await CloseInner(WebSocketCloseStatus.NormalClosure, null); } diff --git a/Myriad/Rest/Types/Requests/ExecuteWebhookRequest.cs b/Myriad/Rest/Types/Requests/ExecuteWebhookRequest.cs index 9378650e..93601076 100644 --- a/Myriad/Rest/Types/Requests/ExecuteWebhookRequest.cs +++ b/Myriad/Rest/Types/Requests/ExecuteWebhookRequest.cs @@ -13,4 +13,14 @@ public record ExecuteWebhookRequest public AllowedMentions? AllowedMentions { get; init; } public bool? Tts { get; init; } public Message.MessageFlags? Flags { get; set; } + public WebhookPoll? Poll { get; set; } + + public record WebhookPoll + { + public Message.PollMedia Question { get; init; } + public Message.PollAnswer[] Answers { get; init; } + public int? Duration { get; init; } + public bool AllowMultiselect { get; init; } + public int LayoutType { get; init; } + } } \ No newline at end of file diff --git a/Myriad/Types/Message.cs b/Myriad/Types/Message.cs index b12ed142..70013ecc 100644 --- a/Myriad/Types/Message.cs +++ b/Myriad/Types/Message.cs @@ -70,6 +70,8 @@ public record Message [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public Optional ReferencedMessage { get; init; } + public MessagePoll? Poll { get; init; } + // public MessageComponent[]? Components { get; init; } public record Reference(ulong? GuildId, ulong? ChannelId, ulong? MessageId); @@ -96,4 +98,17 @@ public record Message public bool Me { get; init; } public Emoji Emoji { get; init; } } + + public record PollMedia(string? Text, Emoji? Emoji); + + public record PollAnswer(PollMedia PollMedia); + + public record MessagePoll + { + public PollMedia Question { get; init; } + public PollAnswer[] Answers { get; init; } + public string? Expiry { get; init; } + public bool AllowMultiselect { get; init; } + public int LayoutType { get; init; } + } } \ No newline at end of file diff --git a/PluralKit.API/Controllers/v2/GroupMemberControllerV2.cs b/PluralKit.API/Controllers/v2/GroupMemberControllerV2.cs index 7a40f244..bdd1771d 100644 --- a/PluralKit.API/Controllers/v2/GroupMemberControllerV2.cs +++ b/PluralKit.API/Controllers/v2/GroupMemberControllerV2.cs @@ -147,6 +147,8 @@ public class GroupMemberControllerV2: PKControllerBase public async Task GetMemberGroups(string memberRef) { var member = await ResolveMember(memberRef); + if (member == null) + throw Errors.MemberNotFoundWithRef(memberRef); var ctx = ContextFor(member); var system = await _repo.GetSystem(member.System); diff --git a/PluralKit.Bot/ApplicationCommands/Message.cs b/PluralKit.Bot/ApplicationCommands/Message.cs index e40a24d2..1fc43989 100644 --- a/PluralKit.Bot/ApplicationCommands/Message.cs +++ b/PluralKit.Bot/ApplicationCommands/Message.cs @@ -63,14 +63,14 @@ public class ApplicationCommandProxiedMessage var messageId = ctx.Event.Data!.TargetId!.Value; // check for command messages - var (authorId, channelId) = await ctx.Services.Resolve().GetCommandMessage(messageId); - if (authorId != null) + var cmessage = await ctx.Services.Resolve().GetCommandMessage(messageId); + if (cmessage != null) { - if (authorId != ctx.User.Id) + if (cmessage.AuthorId != ctx.User.Id) throw new PKError("You can only delete command messages queried by this account."); - var isDM = (await _repo.GetDmChannel(ctx.User!.Id)) == channelId; - await DeleteMessageInner(ctx, channelId!.Value, messageId, isDM); + var isDM = (await _repo.GetDmChannel(ctx.User!.Id)) == cmessage.ChannelId; + await DeleteMessageInner(ctx, cmessage.GuildId, cmessage.ChannelId, messageId, isDM); return; } @@ -78,10 +78,10 @@ public class ApplicationCommandProxiedMessage var message = await ctx.Repository.GetFullMessage(messageId); if (message != null) { - if (message.System?.Id != ctx.System.Id && message.Message.Sender != ctx.User.Id) + if (message.Message.Sender != ctx.User.Id && (ctx.System != null && message.System?.Id != ctx.System.Id)) throw new PKError("You can only delete your own messages."); - await DeleteMessageInner(ctx, message.Message.Channel, message.Message.Mid, false); + await DeleteMessageInner(ctx, message.Message.Guild ?? 0, message.Message.Channel, message.Message.Mid, false); return; } @@ -89,9 +89,9 @@ public class ApplicationCommandProxiedMessage throw Errors.MessageNotFound(messageId); } - internal async Task DeleteMessageInner(InteractionContext ctx, ulong channelId, ulong messageId, bool isDM = false) + internal async Task DeleteMessageInner(InteractionContext ctx, ulong guildId, ulong channelId, ulong messageId, bool isDM = false) { - if (!((await _cache.BotPermissionsIn(channelId)).HasFlag(PermissionSet.ManageMessages) || isDM)) + if (!((await _cache.BotPermissionsIn(guildId, channelId)).HasFlag(PermissionSet.ManageMessages) || isDM)) throw new PKError("PluralKit does not have the *Manage Messages* permission in this channel, and thus cannot delete the message." + " Please contact a server administrator to remedy this."); @@ -110,7 +110,7 @@ public class ApplicationCommandProxiedMessage // (if not, PK shouldn't send messages on their behalf) var member = await _rest.GetGuildMember(ctx.GuildId, ctx.User.Id); var requiredPerms = PermissionSet.ViewChannel | PermissionSet.SendMessages; - if (member == null || !(await _cache.PermissionsForMemberInChannel(ctx.ChannelId, member)).HasFlag(requiredPerms)) + if (member == null || !(await _cache.PermissionsForMemberInChannel(ctx.GuildId, ctx.ChannelId, member)).HasFlag(requiredPerms)) { throw new PKError("You do not have permission to send messages in this channel."); }; diff --git a/PluralKit.Bot/Bot.cs b/PluralKit.Bot/Bot.cs index 506382ef..96f1b568 100644 --- a/PluralKit.Bot/Bot.cs +++ b/PluralKit.Bot/Bot.cs @@ -101,9 +101,7 @@ public class Bot { // we HandleGatewayEvent **before** getting the own user, because the own user is set in HandleGatewayEvent for ReadyEvent await _cache.HandleGatewayEvent(evt); - await _cache.TryUpdateSelfMember(_config.ClientId, evt); - await OnEventReceivedInner(shardId, evt); } @@ -175,7 +173,16 @@ public class Bot } using var _ = LogContext.PushProperty("EventId", Guid.NewGuid()); - using var __ = LogContext.Push(await serviceScope.Resolve().GetEnricher(shardId, evt)); + // this fails when cache lookup fails, so put it in a try-catch + try + { + using var __ = LogContext.Push(await serviceScope.Resolve().GetEnricher(shardId, evt)); + } + catch (Exception exc) + { + + await HandleError(handler, evt, serviceScope, exc); + } _logger.Verbose("Received gateway event: {@Event}", evt); try @@ -243,7 +250,7 @@ public class Bot if (!exc.ShowToUser()) return; // Once we've sent it to Sentry, report it to the user (if we have permission to) - var reportChannel = handler.ErrorChannelFor(evt, _config.ClientId); + var (guildId, reportChannel) = handler.ErrorChannelFor(evt, _config.ClientId); if (reportChannel == null) { if (evt is InteractionCreateEvent ice && ice.Type == Interaction.InteractionType.ApplicationCommand) @@ -251,7 +258,7 @@ public class Bot return; } - var botPerms = await _cache.BotPermissionsIn(reportChannel.Value); + var botPerms = await _cache.BotPermissionsIn(guildId ?? 0, reportChannel.Value); if (botPerms.HasFlag(PermissionSet.SendMessages | PermissionSet.EmbedLinks)) await _errorMessageService.SendErrorMessage(reportChannel.Value, sentryEvent.EventId.ToString()); } diff --git a/PluralKit.Bot/BotConfig.cs b/PluralKit.Bot/BotConfig.cs index 4621717e..c5c05191 100644 --- a/PluralKit.Bot/BotConfig.cs +++ b/PluralKit.Bot/BotConfig.cs @@ -20,7 +20,9 @@ public class BotConfig public string? GatewayQueueUrl { get; set; } public bool UseRedisRatelimiter { get; set; } = false; - public bool UseRedisCache { get; set; } = false; + + public string? HttpCacheUrl { get; set; } + public bool HttpUseInnerCache { get; set; } = false; public string? RedisGatewayUrl { get; set; } diff --git a/PluralKit.Bot/BotMetrics.cs b/PluralKit.Bot/BotMetrics.cs index bc59642f..eef96ac0 100644 --- a/PluralKit.Bot/BotMetrics.cs +++ b/PluralKit.Bot/BotMetrics.cs @@ -136,4 +136,11 @@ public static class BotMetrics DurationUnit = TimeUnit.Seconds, Context = "Bot" }; + + public static MeterOptions CacheDebug => new() + { + Name = "Bad responses to cache lookups", + Context = "Bot", + MeasurementUnit = Unit.Calls + }; } \ No newline at end of file diff --git a/PluralKit.Bot/CommandMeta/CommandHelp.cs b/PluralKit.Bot/CommandMeta/CommandHelp.cs index 5d3dc30e..7f840854 100644 --- a/PluralKit.Bot/CommandMeta/CommandHelp.cs +++ b/PluralKit.Bot/CommandMeta/CommandHelp.cs @@ -82,6 +82,7 @@ public partial class CommandTree public static Command SwitchMove = new Command("switch move", "switch move ", "Moves the latest switch in time"); public static Command SwitchEdit = new Command("switch edit", "switch edit [member 2] [member 3...]", "Edits the members in the latest switch"); public static Command SwitchEditOut = new Command("switch edit out", "switch edit out", "Turns the latest switch into a switch-out"); + public static Command SwitchCopy = new Command("switch copy", "switch copy [member 2] [member 3...]", "Makes a new switch with the listed members added"); public static Command SwitchDelete = new Command("switch delete", "switch delete", "Deletes the latest switch"); public static Command SwitchDeleteAll = new Command("switch delete", "switch delete all", "Deletes all logged switches"); public static Command Link = new Command("link", "link ", "Links your system to another account"); @@ -92,6 +93,7 @@ public partial class CommandTree public static Command Export = new Command("export", "export", "Exports system information to a data file"); public static Command Help = new Command("help", "help", "Shows help information about PluralKit"); public static Command Explain = new Command("explain", "explain", "Explains the basics of systems and proxying"); + public static Command Dashboard = new Command("dashboard", "dashboard", "Get a link to the PluralKit dashboard"); public static Command Message = new Command("message", "message [delete|author]", "Looks up a proxied message"); public static Command MessageEdit = new Command("edit", "edit [link] ", "Edit a previously proxied message"); public static Command MessageReproxy = new Command("reproxy", "reproxy [link] ", "Reproxy a previously proxied message using a different member"); @@ -137,7 +139,7 @@ public partial class CommandTree public static Command[] SwitchCommands = { - Switch, SwitchOut, SwitchMove, SwitchEdit, SwitchEditOut, SwitchDelete, SwitchDeleteAll + Switch, SwitchOut, SwitchMove, SwitchEdit, SwitchEditOut, SwitchDelete, SwitchDeleteAll, SwitchCopy }; public static Command[] ConfigCommands = diff --git a/PluralKit.Bot/CommandMeta/CommandTree.cs b/PluralKit.Bot/CommandMeta/CommandTree.cs index 92441f9d..aa45ab6c 100644 --- a/PluralKit.Bot/CommandMeta/CommandTree.cs +++ b/PluralKit.Bot/CommandMeta/CommandTree.cs @@ -18,7 +18,7 @@ public partial class CommandTree return CommandHelpRoot(ctx); if (ctx.Match("ap", "autoproxy", "auto")) return HandleAutoproxyCommand(ctx); - if (ctx.Match("config", "cfg")) + if (ctx.Match("config", "cfg", "configure")) return HandleConfigCommand(ctx); if (ctx.Match("list", "find", "members", "search", "query", "l", "f", "fd", "ls")) return ctx.Execute(SystemList, m => m.MemberList(ctx, ctx.System)); @@ -105,6 +105,8 @@ public partial class CommandTree 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)); // remove compiler warning return ctx.Reply( @@ -430,13 +432,15 @@ public partial class CommandTree await ctx.Execute(SwitchEdit, m => m.SwitchEdit(ctx)); else if (ctx.Match("delete", "remove", "erase", "cancel", "yeet")) await ctx.Execute(SwitchDelete, m => m.SwitchDelete(ctx)); + else if (ctx.Match("copy", "add", "duplicate", "dupe")) + await ctx.Execute(SwitchCopy, m => m.SwitchEdit(ctx, true)); else if (ctx.Match("commands", "help")) await PrintCommandList(ctx, "switching", SwitchCommands); else if (ctx.HasNext()) // there are following arguments await ctx.Execute(Switch, m => m.SwitchDo(ctx)); else await PrintCommandNotFoundError(ctx, Switch, SwitchOut, SwitchMove, SwitchEdit, SwitchEditOut, - SwitchDelete, SystemFronter, SystemFrontHistory); + SwitchDelete, SwitchCopy, SystemFronter, SystemFrontHistory); } private async Task CommandHelpRoot(Context ctx) @@ -544,6 +548,8 @@ public partial class CommandTree 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[] { "member", "group" }, new[] { "limit" }) || ctx.Match("limit")) + return ctx.Execute(null, m => m.LimitUpdate(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 `pk;commands config` for the list of possible config settings."); diff --git a/PluralKit.Bot/CommandSystem/Context/Context.cs b/PluralKit.Bot/CommandSystem/Context/Context.cs index f5a6c9fb..a023ff8a 100644 --- a/PluralKit.Bot/CommandSystem/Context/Context.cs +++ b/PluralKit.Bot/CommandSystem/Context/Context.cs @@ -72,7 +72,7 @@ public class Context public readonly int ShardId; public readonly Cluster Cluster; - public Task BotPermissions => Cache.BotPermissionsIn(Channel.Id); + public Task BotPermissions => Cache.BotPermissionsIn(Guild?.Id ?? 0, Channel.Id); public Task UserPermissions => Cache.PermissionsForMCE((MessageCreateEvent)Message); @@ -110,7 +110,7 @@ public class Context // { // Sensitive information that might want to be deleted by :x: reaction is typically in an embed format (member cards, for example) // but since we can, we just store all sent messages for possible deletion - await _commandMessageService.RegisterMessage(msg.Id, msg.ChannelId, Author.Id); + await _commandMessageService.RegisterMessage(msg.Id, Guild?.Id ?? 0, msg.ChannelId, Author.Id); // } return msg; diff --git a/PluralKit.Bot/CommandSystem/Context/ContextArgumentsExt.cs b/PluralKit.Bot/CommandSystem/Context/ContextArgumentsExt.cs index 9a93440b..1a958c8e 100644 --- a/PluralKit.Bot/CommandSystem/Context/ContextArgumentsExt.cs +++ b/PluralKit.Bot/CommandSystem/Context/ContextArgumentsExt.cs @@ -91,8 +91,12 @@ public static class ContextArgumentsExt public static bool MatchClear(this Context ctx) => ctx.Match("clear", "reset", "default") || ctx.MatchFlag("c", "clear"); - public static bool MatchRaw(this Context ctx) => - ctx.Match("r", "raw") || ctx.MatchFlag("r", "raw"); + public static ReplyFormat MatchFormat(this Context ctx) + { + if (ctx.Match("r", "raw") || ctx.MatchFlag("r", "raw")) return ReplyFormat.Raw; + if (ctx.Match("pt", "plaintext") || ctx.MatchFlag("pt", "plaintext")) return ReplyFormat.Plaintext; + return ReplyFormat.Standard; + } public static bool MatchToggle(this Context ctx, bool? defaultValue = null) { @@ -184,4 +188,11 @@ public static class ContextArgumentsExt return groups; } +} + +public enum ReplyFormat +{ + Standard, + Raw, + Plaintext } \ No newline at end of file diff --git a/PluralKit.Bot/CommandSystem/Context/ContextEntityArgumentsExt.cs b/PluralKit.Bot/CommandSystem/Context/ContextEntityArgumentsExt.cs index 8bf273f1..c3bb1cb2 100644 --- a/PluralKit.Bot/CommandSystem/Context/ContextEntityArgumentsExt.cs +++ b/PluralKit.Bot/CommandSystem/Context/ContextEntityArgumentsExt.cs @@ -188,7 +188,8 @@ public static class ContextEntityArgumentsExt if (!MentionUtils.TryParseChannel(ctx.PeekArgument(), out var id)) return null; - var channel = await ctx.Cache.TryGetChannel(id); + // 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) diff --git a/PluralKit.Bot/Commands/Admin.cs b/PluralKit.Bot/Commands/Admin.cs index e7ed749b..9e47feb6 100644 --- a/PluralKit.Bot/Commands/Admin.cs +++ b/PluralKit.Bot/Commands/Admin.cs @@ -1,8 +1,12 @@ using System.Text.RegularExpressions; +using Humanizer; using Dapper; using SqlKata; +using Myriad.Builders; +using Myriad.Extensions; +using Myriad.Cache; using Myriad.Rest; using Myriad.Types; @@ -14,11 +18,64 @@ public class Admin { private readonly BotConfig _botConfig; private readonly DiscordApiClient _rest; + private readonly IDiscordCache _cache; - public Admin(BotConfig botConfig, DiscordApiClient rest) + public Admin(BotConfig botConfig, DiscordApiClient rest, IDiscordCache cache) { _botConfig = botConfig; _rest = rest; + _cache = cache; + } + + public async Task CreateEmbed(Context ctx, PKSystem system) + { + string UntilLimit(int count, int limit) + { + var brackets = new List { 10, 25, 50, 100 }; + if (count == limit) + return "(at limit)"; + + foreach (var x in brackets) + { + if (limit - x <= count) + return $"(approx. {x} to limit)"; + } + + return ""; + } + + Task<(ulong Id, User? User)[]> GetUsers(IEnumerable ids) + { + async Task<(ulong Id, User? User)> Inner(ulong id) + { + var user = await _cache.GetOrFetchUser(_rest, id); + return (id, user); + } + + return Task.WhenAll(ids.Select(Inner)); + } + + var config = await ctx.Repository.GetSystemConfig(system.Id); + + // Fetch/render info for all accounts simultaneously + var accounts = await ctx.Repository.GetSystemAccounts(system.Id); + var users = (await GetUsers(accounts)).Select(x => x.User?.NameAndMention() ?? $"(deleted: `{x.Id}`)"); + + var eb = new EmbedBuilder() + .Title("System info") + .Color(DiscordUtils.Green) + .Field(new Embed.Field("System ID", $"`{system.Hid}`")) + .Field(new Embed.Field("Linked accounts", string.Join("\n", users).Truncate(1000))); + + var memberLimit = config.MemberLimitOverride ?? Limits.MaxMemberCount; + var memberCount = await ctx.Repository.GetSystemMemberCount(system.Id); + eb.Field(new Embed.Field("Member limit", $"{memberLimit} {UntilLimit(memberCount, memberLimit)}", true)); + + var groupLimit = config.GroupLimitOverride ?? Limits.MaxGroupCount; + var groupCount = await ctx.Repository.GetSystemGroupCount(system.Id); + eb.Field(new Embed.Field("Group limit", $"{groupLimit} {UntilLimit(groupCount, groupLimit)}", true)); + + return eb.Build(); } public async Task UpdateSystemId(Context ctx) @@ -37,6 +94,8 @@ public class Admin if (existingSystem != null) throw new PKError($"Another system already exists with ID `{newHid}`."); + await ctx.Reply(null, await CreateEmbed(ctx, target)); + if (!await ctx.PromptYesNo($"Change system ID of `{target.Hid}` to `{newHid}`?", "Change")) throw new PKError("ID change cancelled."); @@ -60,6 +119,9 @@ public class Admin if (existingMember != null) throw new PKError($"Another member already exists with ID `{newHid}`."); + var system = await ctx.Repository.GetSystem(target.System); + await ctx.Reply(null, await CreateEmbed(ctx, system)); + if (!await ctx.PromptYesNo( $"Change member ID of **{target.NameFor(LookupContext.ByNonOwner)}** (`{target.Hid}`) to `{newHid}`?", "Change" @@ -86,6 +148,9 @@ public class Admin if (existingGroup != null) throw new PKError($"Another group already exists with ID `{newHid}`."); + var system = await ctx.Repository.GetSystem(target.System); + await ctx.Reply(null, await CreateEmbed(ctx, system)); + if (!await ctx.PromptYesNo($"Change group ID of **{target.Name}** (`{target.Hid}`) to `{newHid}`?", "Change" )) @@ -103,6 +168,8 @@ public class Admin 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")) throw new PKError("ID change cancelled."); @@ -124,6 +191,9 @@ public class Admin if (target == null) throw new PKError("Unknown member."); + var system = await ctx.Repository.GetSystem(target.System); + await ctx.Reply(null, await CreateEmbed(ctx, system)); + if (!await ctx.PromptYesNo( $"Reroll member ID for **{target.NameFor(LookupContext.ByNonOwner)}** (`{target.Hid}`)?", "Reroll" @@ -148,6 +218,9 @@ public class Admin if (target == null) throw new PKError("Unknown group."); + var system = await ctx.Repository.GetSystem(target.System); + await ctx.Reply(null, await CreateEmbed(ctx, system)); + if (!await ctx.PromptYesNo($"Reroll group ID for **{target.Name}** (`{target.Hid}`)?", "Change" )) @@ -176,7 +249,7 @@ public class Admin var currentLimit = config.MemberLimitOverride ?? Limits.MaxMemberCount; if (!ctx.HasNext()) { - await ctx.Reply($"Current member limit is **{currentLimit}** members."); + await ctx.Reply(null, await CreateEmbed(ctx, target)); return; } @@ -184,6 +257,7 @@ public class Admin 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."); @@ -204,7 +278,7 @@ public class Admin var currentLimit = config.GroupLimitOverride ?? Limits.MaxGroupCount; if (!ctx.HasNext()) { - await ctx.Reply($"Current group limit is **{currentLimit}** groups."); + await ctx.Reply(null, await CreateEmbed(ctx, target)); return; } @@ -212,6 +286,7 @@ public class Admin 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."); @@ -243,6 +318,7 @@ public class Admin throw Errors.AccountInOtherSystem(existingAccount, ctx.Config); 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")) throw new PKError("System recovery cancelled."); diff --git a/PluralKit.Bot/Commands/Checks.cs b/PluralKit.Bot/Commands/Checks.cs index 8d88177a..6b68c7b3 100644 --- a/PluralKit.Bot/Commands/Checks.cs +++ b/PluralKit.Bot/Commands/Checks.cs @@ -143,6 +143,7 @@ public class Checks 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) throw new PKError(error); @@ -156,7 +157,8 @@ public class Checks if (!await ctx.CheckPermissionsInGuildChannel(channel, PermissionSet.ViewChannel)) throw new PKError(error); - var botPermissions = await _cache.BotPermissionsIn(channel.Id); + // todo: permcheck channel outside of guild? + var botPermissions = await _cache.BotPermissionsIn(ctx.Guild.Id, channel.Id); // We use a bitfield so we can set individual permission bits ulong missingPermissions = 0; @@ -231,11 +233,11 @@ public class Checks var channel = await _rest.GetChannelOrNull(channelId.Value); if (channel == null) throw new PKError("Unable to get the channel associated with this message."); - - var rootChannel = await _cache.GetRootChannel(channel.Id); if (channel.GuildId == null) throw new PKError("PluralKit is not able to proxy messages in DMs."); + var rootChannel = await _cache.GetRootChannel(channel.GuildId!.Value, channel.Id); + // using channel.GuildId here since _rest.GetMessage() doesn't return the GuildId var context = await ctx.Repository.GetMessageContext(msg.Author.Id, channel.GuildId.Value, rootChannel.Id, msg.ChannelId); var members = (await ctx.Repository.GetProxyMembers(msg.Author.Id, channel.GuildId.Value)).ToList(); diff --git a/PluralKit.Bot/Commands/Config.cs b/PluralKit.Bot/Commands/Config.cs index 05ad9aa6..3e081f38 100644 --- a/PluralKit.Bot/Commands/Config.cs +++ b/PluralKit.Bot/Commands/Config.cs @@ -536,4 +536,10 @@ public class Config } else throw new PKError(badInputError); } + + public Task LimitUpdate(Context ctx) + { + throw new PKError("You cannot update your own member or group limits. If you need a limit update, please join the " + + "support server and ask in #bot-support: https://discord.gg/PczBt78"); + } } \ No newline at end of file diff --git a/PluralKit.Bot/Commands/GroupMember.cs b/PluralKit.Bot/Commands/GroupMember.cs index c2569d43..0b3c1e51 100644 --- a/PluralKit.Bot/Commands/GroupMember.cs +++ b/PluralKit.Bot/Commands/GroupMember.cs @@ -53,41 +53,55 @@ public class GroupMember public async Task ListMemberGroups(Context ctx, PKMember target) { - var pctx = ctx.DirectLookupContextFor(target.System); + var targetSystem = await ctx.Repository.GetSystem(target.System); + var opts = ctx.ParseListOptions(ctx.DirectLookupContextFor(target.System), ctx.LookupContextFor(target.System)); + opts.MemberFilter = target.Id; - var groups = await ctx.Repository.GetMemberGroups(target.Id) - .Where(g => g.Visibility.CanAccess(pctx)) - .OrderBy(g => (g.DisplayName ?? g.Name), StringComparer.InvariantCultureIgnoreCase) - .ToListAsync(); - - var description = ""; - var msg = ""; - - if (groups.Count == 0) - description = "This member has no groups."; - else - description = string.Join("\n", groups.Select(g => $"[`{g.DisplayHid(ctx.Config, isList: true)}`] **{g.DisplayName ?? g.Name}**")); - - if (pctx == LookupContext.ByOwner) + var title = new StringBuilder($"Groups containing {target.Name} (`{target.DisplayHid(ctx.Config)}`) in "); + if (ctx.Guild != null) { - msg += - $"\n\nTo add this member to one or more groups, use `pk;m {target.Reference(ctx)} group add [group 2] [group 3...]`"; - if (groups.Count > 0) - msg += - $"\nTo remove this member from one or more groups, use `pk;m {target.Reference(ctx)} group remove [group 2] [group 3...]`"; + var guildSettings = await ctx.Repository.GetSystemGuild(ctx.Guild.Id, targetSystem.Id); + if (guildSettings.DisplayName != null) + title.Append($"{guildSettings.DisplayName} (`{targetSystem.DisplayHid(ctx.Config)}`)"); + else if (targetSystem.NameFor(ctx) != null) + title.Append($"{targetSystem.NameFor(ctx)} (`{targetSystem.DisplayHid(ctx.Config)}`)"); + else + title.Append($"`{targetSystem.DisplayHid(ctx.Config)}`"); } + else + { + if (targetSystem.NameFor(ctx) != null) + title.Append($"{targetSystem.NameFor(ctx)} (`{targetSystem.DisplayHid(ctx.Config)}`)"); + else + title.Append($"`{targetSystem.DisplayHid(ctx.Config)}`"); + } + if (opts.Search != null) + title.Append($" matching **{opts.Search.Truncate(100)}**"); - await ctx.Reply(msg, new EmbedBuilder().Title($"{target.Name}'s groups").Description(description).Build()); + await ctx.RenderGroupList(ctx.LookupContextFor(target.System), target.System, title.ToString(), + target.Color, opts); } public async Task AddRemoveMembers(Context ctx, PKGroup target, Groups.AddRemoveOperation op) { ctx.CheckOwnGroup(target); - var members = (await ctx.ParseMemberList(ctx.System.Id)) + List members; + if (ctx.MatchFlag("all", "a")) + { + members = (await ctx.Database.Execute(conn => conn.QueryMemberList(target.System, + new DatabaseViewsExt.ListQueryOptions { }))) + .Select(m => m.Id) + .Distinct() + .ToList(); + } + else + { + members = (await ctx.ParseMemberList(ctx.System.Id)) .Select(m => m.Id) .Distinct() .ToList(); + } var existingMembersInGroup = (await ctx.Database.Execute(conn => conn.QueryMemberList(target.System, new DatabaseViewsExt.ListQueryOptions { GroupFilter = target.Id }))) @@ -127,7 +141,7 @@ public class GroupMember var targetSystem = await GetGroupSystem(ctx, target); ctx.CheckSystemPrivacy(targetSystem.Id, target.ListPrivacy); - var opts = ctx.ParseListOptions(ctx.DirectLookupContextFor(target.System)); + var opts = ctx.ParseListOptions(ctx.DirectLookupContextFor(target.System), ctx.LookupContextFor(target.System)); opts.GroupFilter = target.Id; var title = new StringBuilder($"Members of {target.DisplayName ?? target.Name} (`{target.DisplayHid(ctx.Config)}`) in "); diff --git a/PluralKit.Bot/Commands/Groups.cs b/PluralKit.Bot/Commands/Groups.cs index d2f421c7..05cc0a54 100644 --- a/PluralKit.Bot/Commands/Groups.cs +++ b/PluralKit.Bot/Commands/Groups.cs @@ -132,40 +132,47 @@ public class Groups // No perms check, display name isn't covered by member privacy - if (ctx.MatchRaw()) - { + var format = ctx.MatchFormat(); + + // if there's nothing next or what's next is "raw"/"plaintext" we're doing a query, so check for null + if (!ctx.HasNext(false) || format != ReplyFormat.Standard) if (target.DisplayName == null) + { await ctx.Reply(noDisplayNameSetMessage); - else - await ctx.Reply($"```\n{target.DisplayName}\n```"); + return; + } + + if (format == ReplyFormat.Raw) + { + await ctx.Reply($"```\n{target.DisplayName}\n```"); + return; + } + if (format == ReplyFormat.Plaintext) + { + var eb = new EmbedBuilder() + .Description($"Showing displayname for group {target.Reference(ctx)}"); + await ctx.Reply(target.DisplayName, embed: eb.Build()); return; } if (!ctx.HasNext(false)) { - if (target.DisplayName == null) - { - await ctx.Reply(noDisplayNameSetMessage); - } - else - { - var eb = new EmbedBuilder() - .Field(new Embed.Field("Name", target.Name)) - .Field(new Embed.Field("Display Name", target.DisplayName)); + var eb = new EmbedBuilder() + .Field(new Embed.Field("Name", target.Name)) + .Field(new Embed.Field("Display Name", target.DisplayName)); - var reference = target.Reference(ctx); + var reference = target.Reference(ctx); - if (ctx.System?.Id == target.System) - eb.Description( - $"To change display name, type `pk;group {reference} displayname `.\n" - + $"To clear it, type `pk;group {reference} displayname -clear`.\n" - + $"To print the raw display name, type `pk;group {reference} displayname -raw`."); + if (ctx.System?.Id == target.System) + eb.Description( + $"To change display name, type `pk;group {reference} displayname `.\n" + + $"To clear it, type `pk;group {reference} displayname -clear`.\n" + + $"To print the raw display name, type `pk;group {reference} displayname -raw`."); - if (ctx.System?.Id == target.System) - eb.Footer(new Embed.EmbedFooter($"Using {target.DisplayName.Length}/{Limits.MaxGroupNameLength} characters.")); + if (ctx.System?.Id == target.System) + eb.Footer(new Embed.EmbedFooter($"Using {target.DisplayName.Length}/{Limits.MaxGroupNameLength} characters.")); - await ctx.Reply(embed: eb.Build()); - } + await ctx.Reply(embed: eb.Build()); return; } @@ -184,6 +191,8 @@ public class Groups 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); @@ -201,30 +210,41 @@ public class Groups noDescriptionSetMessage += $" To set one, type `pk;group {target.Reference(ctx)} description `."; - if (ctx.MatchRaw()) - { + var format = ctx.MatchFormat(); + + // if there's nothing next or what's next is "raw"/"plaintext" we're doing a query, so check for null + if (!ctx.HasNext(false) || format != ReplyFormat.Standard) if (target.Description == null) + { await ctx.Reply(noDescriptionSetMessage); - else - await ctx.Reply($"```\n{target.Description}\n```"); + return; + } + + if (format == ReplyFormat.Raw) + { + await ctx.Reply($"```\n{target.Description}\n```"); + return; + } + if (format == ReplyFormat.Plaintext) + { + var eb = new EmbedBuilder() + .Description($"Showing description for group {target.Reference(ctx)}"); + await ctx.Reply(target.Description, embed: eb.Build()); return; } if (!ctx.HasNext(false)) { - if (target.Description == null) - await ctx.Reply(noDescriptionSetMessage); - else - await ctx.Reply(embed: new EmbedBuilder() - .Title("Group description") - .Description(target.Description) - .Field(new Embed.Field("\u200B", - $"To print the description with formatting, type `pk;group {target.Reference(ctx)} description -raw`." - + (ctx.System?.Id == target.System - ? $" To clear it, type `pk;group {target.Reference(ctx)} description -clear`." - : "") - + $" Using {target.Description.Length}/{Limits.MaxDescriptionLength} characters.")) - .Build()); + await ctx.Reply(embed: new EmbedBuilder() + .Title("Group description") + .Description(target.Description) + .Field(new Embed.Field("\u200B", + $"To print the description with formatting, type `pk;group {target.Reference(ctx)} description -raw`." + + (ctx.System?.Id == target.System + ? $" To clear it, type `pk;group {target.Reference(ctx)} description -clear`." + : "") + + $" Using {target.Description.Length}/{Limits.MaxDescriptionLength} characters.")) + .Build()); return; } @@ -385,7 +405,7 @@ public class Groups public async Task GroupColor(Context ctx, PKGroup target) { var isOwnSystem = ctx.System?.Id == target.System; - var matchedRaw = ctx.MatchRaw(); + var matchedFormat = ctx.MatchFormat(); var matchedClear = ctx.MatchClear(); if (!isOwnSystem || !(ctx.HasNext() || matchedClear)) @@ -393,8 +413,10 @@ public class Groups if (target.Color == null) await ctx.Reply( "This group does not have a color set." + (isOwnSystem ? $" To set one, type `pk;group {target.Reference(ctx)} color `." : "")); - else if (matchedRaw) + else if (matchedFormat == ReplyFormat.Raw) await ctx.Reply("```\n#" + target.Color + "\n```"); + else if (matchedFormat == ReplyFormat.Plaintext) + await ctx.Reply(target.Color); else await ctx.Reply(embed: new EmbedBuilder() .Title("Group color") @@ -446,7 +468,7 @@ 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)); + var opts = ctx.ParseListOptions(ctx.DirectLookupContextFor(system.Id), ctx.LookupContextFor(system.Id)); await ctx.RenderGroupList( ctx.LookupContextFor(system.Id), system.Id, @@ -460,8 +482,8 @@ public class Groups { var title = new StringBuilder("Groups of "); - if (target.Name != null) - title.Append($"{target.Name} (`{target.DisplayHid(ctx.Config)}`)"); + if (target.NameFor(ctx) != null) + title.Append($"{target.NameFor(ctx)} (`{target.DisplayHid(ctx.Config)}`)"); else title.Append($"`{target.DisplayHid(ctx.Config)}`"); diff --git a/PluralKit.Bot/Commands/Help.cs b/PluralKit.Bot/Commands/Help.cs index 76369428..47d18944 100644 --- a/PluralKit.Bot/Commands/Help.cs +++ b/PluralKit.Bot/Commands/Help.cs @@ -17,6 +17,26 @@ public class Help private static Dictionary helpEmbedPages = new Dictionary { + { + "default", + new Embed.Field[] + { + new + ( + "System Recovery", + "In the case of your Discord account getting lost or deleted, the PluralKit staff can help you recover your system. " + + "In order to do so, we will need your **PluralKit token**. This is the *only* way you can prove ownership so we can help you recover your system. " + + "To get it, run `pk;token` and then store it in a safe place.\n\n" + + "Keep your token safe, if other people get access to it they can also use it to access your system. " + + "If your token is ever compromised run `pk;token refresh` to invalidate the old token and get a new one." + ), + new + ( + "Use the buttons below to see more info!", + "" + ) + } + }, { "basicinfo", new Embed.Field[] @@ -31,7 +51,7 @@ public class Help ( "Why are people's names saying [APP] or [BOT] next to them?", "These people are not actually apps or bots, this is just a Discord limitation. See [the documentation](https://pluralkit.me/guide#proxying) for an in-depth explanation." - ), + ) } }, { @@ -137,7 +157,9 @@ public class Help public Task HelpRoot(Context ctx) => ctx.Rest.CreateMessage(ctx.Channel.Id, new MessageRequest { - Embeds = new[] { helpEmbed with { Description = helpEmbed.Description + "\n\n**Use the buttons below to see more info!**" } }, + Content = $"{Emojis.Warn} If you cannot see the rest of this message see [the FAQ]()", + Embeds = new[] { helpEmbed with { Description = helpEmbed.Description, + Fields = helpEmbedPages.GetValueOrDefault("default") } }, Components = new[] { helpPageButtons(ctx.Author.Id) }, }); @@ -151,7 +173,7 @@ public class Help if (ctx.Event.Message.Components.First().Components.Where(x => x.CustomId == ctx.CustomId).First().Style == ButtonStyle.Primary) return ctx.Respond(InteractionResponse.ResponseType.UpdateMessage, new() { - Embeds = new[] { helpEmbed with { Description = helpEmbed.Description + "\n\n**Use the buttons below to see more info!**" } }, + Embeds = new[] { helpEmbed with { Fields = helpEmbedPages.GetValueOrDefault("default") } }, Components = new[] { buttons } }); @@ -172,4 +194,6 @@ public class Help }); public Task Explain(Context ctx) => ctx.Reply(explanation); + + public Task Dashboard(Context ctx) => ctx.Reply("The PluralKit dashboard is at "); } \ No newline at end of file diff --git a/PluralKit.Bot/Commands/Lists/ContextListExt.cs b/PluralKit.Bot/Commands/Lists/ContextListExt.cs index 2821cf57..15257218 100644 --- a/PluralKit.Bot/Commands/Lists/ContextListExt.cs +++ b/PluralKit.Bot/Commands/Lists/ContextListExt.cs @@ -11,7 +11,7 @@ namespace PluralKit.Bot; public static class ContextListExt { - public static ListOptions ParseListOptions(this Context ctx, LookupContext lookupCtx) + public static ListOptions ParseListOptions(this Context ctx, LookupContext directLookupCtx, LookupContext lookupContext) { var p = new ListOptions(); @@ -55,10 +55,13 @@ public static class ContextListExt 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 && lookupCtx != LookupContext.ByOwner) + 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; @@ -124,11 +127,14 @@ public static class ContextListExt void ShortRenderer(EmbedBuilder eb, IEnumerable page) { + // if there are both 5 and 6 character Hids they should be padded to align correctly. + var shouldPad = page.Any(x => x.Hid.Length > 5); + // We may end up over the description character limit // so run it through a helper that "makes it work" :) eb.WithSimpleLineContent(page.Select(m => { - var ret = $"[`{m.DisplayHid(ctx.Config, isList: true)}`] **{m.NameFor(ctx)}** "; + var ret = $"[`{m.DisplayHid(ctx.Config, isList: true, shouldPad: shouldPad)}`] **{m.NameFor(ctx)}** "; if (opts.IncludeMessageCount && m.MessageCountFor(lookupCtx) is { } count) ret += $"({count} messages)"; @@ -234,11 +240,14 @@ public static class ContextListExt void ShortRenderer(EmbedBuilder eb, IEnumerable page) { + // if there are both 5 and 6 character Hids they should be padded to align correctly. + var shouldPad = page.Any(x => x.Hid.Length > 5); + // We may end up over the description character limit // so run it through a helper that "makes it work" :) eb.WithSimpleLineContent(page.Select(g => { - var ret = $"[`{g.DisplayHid(ctx.Config, isList: true)}`] **{g.NameFor(ctx)}** "; + var ret = $"[`{g.DisplayHid(ctx.Config, isList: true, shouldPad: shouldPad)}`] **{g.NameFor(ctx)}** "; switch (opts.SortProperty) { diff --git a/PluralKit.Bot/Commands/Lists/ListOptions.cs b/PluralKit.Bot/Commands/Lists/ListOptions.cs index ae2fb6a5..991b0a8e 100644 --- a/PluralKit.Bot/Commands/Lists/ListOptions.cs +++ b/PluralKit.Bot/Commands/Lists/ListOptions.cs @@ -28,7 +28,9 @@ public class ListOptions public bool Reverse { get; set; } public PrivacyLevel? PrivacyFilter { get; set; } = PrivacyLevel.Public; + public LookupContext Context { get; set; } = LookupContext.ByNonOwner; public GroupId? GroupFilter { get; set; } + public MemberId? MemberFilter { get; set; } public string? Search { get; set; } public bool SearchDescription { get; set; } @@ -96,8 +98,10 @@ public class ListOptions { PrivacyFilter = PrivacyFilter, GroupFilter = GroupFilter, + MemberFilter = MemberFilter, Search = Search, - SearchDescription = SearchDescription + SearchDescription = SearchDescription, + Context = Context }; } diff --git a/PluralKit.Bot/Commands/MemberEdit.cs b/PluralKit.Bot/Commands/MemberEdit.cs index 84afc443..3c19c171 100644 --- a/PluralKit.Bot/Commands/MemberEdit.cs +++ b/PluralKit.Bot/Commands/MemberEdit.cs @@ -70,30 +70,41 @@ public class MemberEdit noDescriptionSetMessage += $" To set one, type `pk;member {target.Reference(ctx)} description `."; - if (ctx.MatchRaw()) - { + var format = ctx.MatchFormat(); + + // if there's nothing next or what's next is "raw"/"plaintext" we're doing a query, so check for null + if (!ctx.HasNext(false) || format != ReplyFormat.Standard) if (target.Description == null) + { await ctx.Reply(noDescriptionSetMessage); - else - await ctx.Reply($"```\n{target.Description}\n```"); + return; + } + + if (format == ReplyFormat.Raw) + { + await ctx.Reply($"```\n{target.Description}\n```"); + return; + } + if (format == ReplyFormat.Plaintext) + { + var eb = new EmbedBuilder() + .Description($"Showing description for member {target.Reference(ctx)}"); + await ctx.Reply(target.Description, embed: eb.Build()); return; } if (!ctx.HasNext(false)) { - if (target.Description == null) - await ctx.Reply(noDescriptionSetMessage); - else - await ctx.Reply(embed: new EmbedBuilder() - .Title("Member description") - .Description(target.Description) - .Field(new Embed.Field("\u200B", - $"To print the description with formatting, type `pk;member {target.Reference(ctx)} description -raw`." - + (ctx.System?.Id == target.System - ? $" To clear it, type `pk;member {target.Reference(ctx)} description -clear`." - : "") - + $" Using {target.Description.Length}/{Limits.MaxDescriptionLength} characters.")) - .Build()); + await ctx.Reply(embed: new EmbedBuilder() + .Title("Member description") + .Description(target.Description) + .Field(new Embed.Field("\u200B", + $"To print the description with formatting, type `pk;member {target.Reference(ctx)} description -raw`." + + (ctx.System?.Id == target.System + ? $" To clear it, type `pk;member {target.Reference(ctx)} description -clear`." + : "") + + $" Using {target.Description.Length}/{Limits.MaxDescriptionLength} characters.")) + .Build()); return; } @@ -126,26 +137,37 @@ public class MemberEdit ctx.CheckSystemPrivacy(target.System, target.PronounPrivacy); - if (ctx.MatchRaw()) - { + var format = ctx.MatchFormat(); + + // if there's nothing next or what's next is "raw"/"plaintext" we're doing a query, so check for null + if (!ctx.HasNext(false) || format != ReplyFormat.Standard) if (target.Pronouns == null) + { await ctx.Reply(noPronounsSetMessage); - else - await ctx.Reply($"```\n{target.Pronouns}\n```"); + return; + } + + if (format == ReplyFormat.Raw) + { + await ctx.Reply($"```\n{target.Pronouns}\n```"); + return; + } + if (format == ReplyFormat.Plaintext) + { + var eb = new EmbedBuilder() + .Description($"Showing pronouns for member {target.Reference(ctx)}"); + await ctx.Reply(target.Pronouns, embed: eb.Build()); return; } if (!ctx.HasNext(false)) { - if (target.Pronouns == null) - await ctx.Reply(noPronounsSetMessage); - else - await ctx.Reply( - $"**{target.NameFor(ctx)}**'s pronouns are **{target.Pronouns}**.\nTo print the pronouns with formatting, type `pk;member {target.Reference(ctx)} pronouns -raw`." - + (ctx.System?.Id == target.System - ? $" To clear them, type `pk;member {target.Reference(ctx)} pronouns -clear`." - : "") - + $" Using {target.Pronouns.Length}/{Limits.MaxPronounsLength} characters."); + await ctx.Reply( + $"**{target.NameFor(ctx)}**'s pronouns are **{target.Pronouns}**.\nTo print the pronouns with formatting, type `pk;member {target.Reference(ctx)} pronouns -raw`." + + (ctx.System?.Id == target.System + ? $" To clear them, type `pk;member {target.Reference(ctx)} pronouns -clear`." + : "") + + $" Using {target.Pronouns.Length}/{Limits.MaxPronounsLength} characters."); return; } @@ -232,7 +254,7 @@ public class MemberEdit public async Task Color(Context ctx, PKMember target) { var isOwnSystem = ctx.System?.Id == target.System; - var matchedRaw = ctx.MatchRaw(); + var matchedFormat = ctx.MatchFormat(); var matchedClear = ctx.MatchClear(); if (!isOwnSystem || !(ctx.HasNext() || matchedClear)) @@ -240,8 +262,10 @@ public class MemberEdit if (target.Color == null) await ctx.Reply( "This member does not have a color set." + (isOwnSystem ? $" To set one, type `pk;member {target.Reference(ctx)} color `." : "")); - else if (matchedRaw) + else if (matchedFormat == ReplyFormat.Raw) await ctx.Reply("```\n#" + target.Color + "\n```"); + else if (matchedFormat == ReplyFormat.Plaintext) + await ctx.Reply(target.Color); else await ctx.Reply(embed: new EmbedBuilder() .Title("Member color") @@ -388,12 +412,26 @@ public class MemberEdit // No perms check, display name isn't covered by member privacy - if (ctx.MatchRaw()) - { + var format = ctx.MatchFormat(); + + // if what's next is "raw"/"plaintext" we need to check for null + if (format != ReplyFormat.Standard) if (target.DisplayName == null) + { await ctx.Reply(noDisplayNameSetMessage); - else - await ctx.Reply($"```\n{target.DisplayName}\n```"); + return; + } + + if (format == ReplyFormat.Raw) + { + await ctx.Reply($"```\n{target.DisplayName}\n```"); + return; + } + if (format == ReplyFormat.Plaintext) + { + var eb = new EmbedBuilder() + .Description($"Showing displayname for member {target.Reference(ctx)}"); + await ctx.Reply(target.DisplayName, embed: eb.Build()); return; } @@ -450,12 +488,26 @@ public class MemberEdit // No perms check, display name isn't covered by member privacy var memberGuildConfig = await ctx.Repository.GetMemberGuild(ctx.Guild.Id, target.Id); - if (ctx.MatchRaw()) - { + var format = ctx.MatchFormat(); + + // if what's next is "raw"/"plaintext" we need to check for null + if (format != ReplyFormat.Standard) if (memberGuildConfig.DisplayName == null) + { await ctx.Reply(noServerNameSetMessage); - else - await ctx.Reply($"```\n{memberGuildConfig.DisplayName}\n```"); + return; + } + + if (format == ReplyFormat.Raw) + { + await ctx.Reply($"```\n{memberGuildConfig.DisplayName}\n```"); + return; + } + if (format == ReplyFormat.Plaintext) + { + var eb = new EmbedBuilder() + .Description($"Showing servername for member {target.Reference(ctx)}"); + await ctx.Reply(memberGuildConfig.DisplayName, embed: eb.Build()); return; } diff --git a/PluralKit.Bot/Commands/MemberProxy.cs b/PluralKit.Bot/Commands/MemberProxy.cs index 02ee4555..e1bcb19b 100644 --- a/PluralKit.Bot/Commands/MemberProxy.cs +++ b/PluralKit.Bot/Commands/MemberProxy.cs @@ -63,7 +63,7 @@ public class MemberProxy 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)); + 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); @@ -87,10 +87,17 @@ public class MemberProxy if (!ctx.HasNext(false)) throw new PKSyntaxError("You must pass a proxy tag to remove (eg. `[text]` or `J:text`)."); - var tagToRemove = ParseProxyTags(ctx.RemainderOrNull(false)); + var remainder = ctx.RemainderOrNull(false); + var tagToRemove = ParseProxyTags(remainder.NormalizeLineEndSpacing()); if (tagToRemove.IsEmpty) throw Errors.EmptyProxyTags(target, ctx); if (!target.ProxyTags.Contains(tagToRemove)) - throw Errors.ProxyTagDoesNotExist(tagToRemove, target); + { + // 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); @@ -102,7 +109,7 @@ public class MemberProxy // Subcommand: bare proxy tag given else { - var requestedTag = ParseProxyTags(ctx.RemainderOrNull(false)); + 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 diff --git a/PluralKit.Bot/Commands/Message.cs b/PluralKit.Bot/Commands/Message.cs index b45639d7..e92ff1f5 100644 --- a/PluralKit.Bot/Commands/Message.cs +++ b/PluralKit.Bot/Commands/Message.cs @@ -218,7 +218,7 @@ public class ProxiedMessage try { var editedMsg = - await _webhookExecutor.EditWebhookMessage(msg.Channel, msg.Mid, newContent, clearEmbeds); + await _webhookExecutor.EditWebhookMessage(msg.Guild ?? 0, msg.Channel, msg.Mid, newContent, clearEmbeds); if (ctx.Guild == null) await _rest.CreateReaction(ctx.Channel.Id, ctx.Message.Id, new Emoji { Name = Emojis.Success }); @@ -352,7 +352,9 @@ public class ProxiedMessage else if (!await ctx.CheckPermissionsInGuildChannel(channel, PermissionSet.ViewChannel)) showContent = false; - if (ctx.MatchRaw()) + var format = ctx.MatchFormat(); + + if (format != ReplyFormat.Standard) { var discordMessage = await _rest.GetMessageOrNull(message.Message.Channel, message.Message.Mid); if (discordMessage == null || !showContent) @@ -365,21 +367,32 @@ public class ProxiedMessage return; } - await ctx.Reply($"```{content}```"); - - if (Regex.IsMatch(content, "```.*```", RegexOptions.Singleline)) + if (format == ReplyFormat.Raw) { - var stream = new MemoryStream(Encoding.UTF8.GetBytes(content)); - await ctx.Rest.CreateMessage( - ctx.Channel.Id, - new MessageRequest - { - Content = $"{Emojis.Warn} Message contains codeblocks, raw source sent as an attachment." - }, - new[] { new MultipartFile("message.txt", stream, null, null, null) }); + await ctx.Reply($"```{content}```"); + + if (Regex.IsMatch(content, "```.*```", RegexOptions.Singleline)) + { + var stream = new MemoryStream(Encoding.UTF8.GetBytes(content)); + await ctx.Rest.CreateMessage( + ctx.Channel.Id, + new MessageRequest + { + Content = $"{Emojis.Warn} Message contains codeblocks, raw source sent as an attachment." + }, + new[] { new MultipartFile("message.txt", stream, null, null, null) }); + } + return; + } + + if (format == ReplyFormat.Plaintext) + { + var eb = new EmbedBuilder() + .Description($"Showing contents of message {message.Message.Mid}"); + await ctx.Reply(content, embed: eb.Build()); + return; } - return; } if (isDelete) @@ -387,7 +400,7 @@ public class ProxiedMessage if (!showContent) throw new PKError(noShowContentError); - if (message.System?.Id != ctx.System.Id && message.Message.Sender != ctx.Author.Id) + if (message.Message.Sender != ctx.Author.Id && (ctx.System != null && message.System?.Id != ctx.System.Id)) throw new PKError("You can only delete your own messages."); await ctx.Rest.DeleteMessage(message.Message.Channel, message.Message.Mid); @@ -423,14 +436,14 @@ public class ProxiedMessage private async Task DeleteCommandMessage(Context ctx, ulong messageId) { - var (authorId, channelId) = await ctx.Services.Resolve().GetCommandMessage(messageId); - if (authorId == null) + var cmessage = await ctx.Services.Resolve().GetCommandMessage(messageId); + if (cmessage == null) throw Errors.MessageNotFound(messageId); - if (authorId != ctx.Author.Id) + if (cmessage!.AuthorId != ctx.Author.Id) throw new PKError("You can only delete command messages queried by this account."); - await ctx.Rest.DeleteMessage(channelId!.Value, messageId); + await ctx.Rest.DeleteMessage(cmessage.ChannelId, messageId); if (ctx.Guild != null) await ctx.Rest.DeleteMessage(ctx.Message); diff --git a/PluralKit.Bot/Commands/Random.cs b/PluralKit.Bot/Commands/Random.cs index 933ec7e1..8f0c07b2 100644 --- a/PluralKit.Bot/Commands/Random.cs +++ b/PluralKit.Bot/Commands/Random.cs @@ -67,7 +67,7 @@ public class Random { ctx.CheckSystemPrivacy(group.System, group.ListPrivacy); - var opts = ctx.ParseListOptions(ctx.DirectLookupContextFor(group.System)); + var opts = ctx.ParseListOptions(ctx.DirectLookupContextFor(group.System), ctx.LookupContextFor(group.System)); opts.GroupFilter = group.Id; var members = await ctx.Database.Execute(conn => conn.QueryMemberList(group.System, opts.ToQueryOptions())); diff --git a/PluralKit.Bot/Commands/ServerConfig.cs b/PluralKit.Bot/Commands/ServerConfig.cs index 77af6bac..f411be77 100644 --- a/PluralKit.Bot/Commands/ServerConfig.cs +++ b/PluralKit.Bot/Commands/ServerConfig.cs @@ -49,7 +49,7 @@ public class ServerConfig 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."); - var perms = await _cache.BotPermissionsIn(channel.Id); + var perms = await _cache.BotPermissionsIn(ctx.Guild.Id, channel.Id); if (!perms.HasFlag(PermissionSet.SendMessages)) throw new PKError("PluralKit is missing **Send Messages** permissions in the new log channel."); if (!perms.HasFlag(PermissionSet.EmbedLinks)) @@ -104,7 +104,7 @@ public class ServerConfig // Resolve all channels from the cache and order by position var channels = (await Task.WhenAll(blacklist.Blacklist - .Select(id => _cache.TryGetChannel(id)))) + .Select(id => _cache.TryGetChannel(ctx.Guild.Id, id)))) .Where(c => c != null) .OrderBy(c => c.Position) .ToList(); @@ -121,7 +121,7 @@ public class ServerConfig async (eb, l) => { async Task CategoryName(ulong? id) => - id != null ? (await _cache.GetChannel(id.Value)).Name : "(no category)"; + id != null ? (await _cache.GetChannel(ctx.Guild.Id, id.Value)).Name : "(no category)"; ulong? lastCategory = null; @@ -153,8 +153,9 @@ public class ServerConfig 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(id)))) + .Select(id => _cache.TryGetChannel(ctx.Guild.Id, id)))) .Where(c => c != null) .OrderBy(c => c.Position) .ToList(); @@ -171,7 +172,7 @@ public class ServerConfig async (eb, l) => { async Task CategoryName(ulong? id) => - id != null ? (await _cache.GetChannel(id.Value)).Name : "(no category)"; + id != null ? (await _cache.GetChannel(ctx.Guild.Id, id.Value)).Name : "(no category)"; ulong? lastCategory = null; @@ -204,7 +205,8 @@ public class ServerConfig var affectedChannels = new List(); if (ctx.Match("all")) affectedChannels = (await _cache.GetGuildChannels(ctx.Guild.Id)) - .Where(x => x.Type == Channel.ChannelType.GuildText).ToList(); + // 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()) diff --git a/PluralKit.Bot/Commands/Switch.cs b/PluralKit.Bot/Commands/Switch.cs index 9272d911..83465bd4 100644 --- a/PluralKit.Bot/Commands/Switch.cs +++ b/PluralKit.Bot/Commands/Switch.cs @@ -7,6 +7,7 @@ namespace PluralKit.Bot; public class Switch { + public async Task SwitchDo(Context ctx) { ctx.CheckSystem(); @@ -103,12 +104,69 @@ public class Switch await ctx.Reply($"{Emojis.Success} Switch moved to ({newSwitchDeltaStr} ago)."); } - public async Task SwitchEdit(Context ctx) + public async Task SwitchEdit(Context ctx, bool newSwitch = false) { ctx.CheckSystem(); - var members = await ctx.ParseMemberList(ctx.System.Id); - await DoEditCommand(ctx, members); + var newMembers = await ctx.ParseMemberList(ctx.System.Id); + + await using var conn = await ctx.Database.Obtain(); + var currentSwitch = await ctx.Repository.GetLatestSwitch(ctx.System.Id); + if (currentSwitch == null) + throw Errors.NoRegisteredSwitches; + var currentSwitchMembers = await ctx.Repository.GetSwitchMembers(conn, currentSwitch.Id).ToListAsync().AsTask(); + + if (ctx.MatchFlag("first", "f")) + newMembers = FirstInSwitch(newMembers[0], currentSwitchMembers); + else if (ctx.MatchFlag("remove", "r")) + newMembers = RemoveFromSwitch(newMembers, currentSwitchMembers); + else if (ctx.MatchFlag("append", "a")) + newMembers = AppendToSwitch(newMembers, currentSwitchMembers); + else if (ctx.MatchFlag("prepend", "p")) + newMembers = PrependToSwitch(newMembers, currentSwitchMembers); + + if (newSwitch) + { + // if there's no edit flag, assume we're appending + if (!ctx.MatchFlag("first", "f", "remove", "r", "append", "a", "prepend", "p")) + newMembers = AppendToSwitch(newMembers, currentSwitchMembers); + await DoSwitchCommand(ctx, newMembers); + } + else + await DoEditCommand(ctx, newMembers); + } + + public List PrependToSwitch(List members, List currentSwitchMembers) + { + members.AddRange(currentSwitchMembers); + + return members; + } + + public List AppendToSwitch(List members, List currentSwitchMembers) + { + currentSwitchMembers.AddRange(members); + members = currentSwitchMembers; + + return members; + } + + public List RemoveFromSwitch(List members, List currentSwitchMembers) + { + var memberIds = members.Select(m => m.Id.Value); + currentSwitchMembers = currentSwitchMembers.Where(m => !memberIds.Contains(m.Id.Value)).ToList(); + members = currentSwitchMembers; + + return members; + } + + public List FirstInSwitch(PKMember member, List currentSwitchMembers) + { + currentSwitchMembers = currentSwitchMembers.Where(m => m.Id != member.Id).ToList(); + var members = new List { member }; + members.AddRange(currentSwitchMembers); + + return members; } public async Task SwitchEditOut(Context ctx) diff --git a/PluralKit.Bot/Commands/System.cs b/PluralKit.Bot/Commands/System.cs index be64c63f..d75a5406 100644 --- a/PluralKit.Bot/Commands/System.cs +++ b/PluralKit.Bot/Commands/System.cs @@ -2,6 +2,9 @@ using PluralKit.Core; namespace PluralKit.Bot; +using Myriad.Builders; +using Myriad.Types; + public class System { private readonly EmbedService _embeds; @@ -29,9 +32,25 @@ public class System var system = await ctx.Repository.CreateSystem(systemName); await ctx.Repository.AddAccount(system.Id, ctx.Author.Id); - // TODO: better message, perhaps embed like in groups? - await ctx.Reply( - $"{Emojis.Success} Your system has been created. Type `pk;system` to view it, and type `pk;system help` for more information about commands you can use now. Now that you have that set up, check out the getting started guide on setting up members and proxies: "); + var eb = new EmbedBuilder() + .Title( + $"{Emojis.Success} Your system has been created.") + .Field(new Embed.Field("Getting Started", + "New to PK? Check out our Getting Started guide on setting up members and proxies: https://pluralkit.me/start\n" + + "Otherwise, type `pk;system` to view your system and `pk;system help` for more information about commands you can use.")) + .Field(new Embed.Field($"{Emojis.Warn} Notice {Emojis.Warn}", "PluralKit is a bot meant to help you share information about your system. " + + "Member descriptions are meant to be the equivalent to a Discord About Me. Because of this, any info you put in PK is **public by default**.\n" + + "Note that this does **not** include message content, only member fields. For more information, check out " + + "[the privacy section of the user guide](https://pluralkit.me/guide/#privacy). ")) + .Field(new Embed.Field("System Recovery", "In the case of your Discord account getting lost or deleted, the PluralKit staff can help you recover your system. " + + "In order to do so, we will need your **PluralKit token**. This is the *only* way you can prove ownership so we can help you recover your system. " + + "To get it, run `pk;token` and then store it in a safe place.\n\n" + + "Keep your token safe, if other people get access to it they can also use it to access your system. " + + "If your token is ever compromised run `pk;token refresh` to invalidate the old token and get a new one.")) + .Field(new Embed.Field("Questions?", + "Please join the PK server https://discord.gg/PczBt78 if you have any questions, we're happy to help")); + await ctx.Reply($"{Emojis.Warn} If you cannot see the rest of this message see [the FAQ]()", eb.Build()); + } public async Task DisplayId(Context ctx, PKSystem target) diff --git a/PluralKit.Bot/Commands/SystemEdit.cs b/PluralKit.Bot/Commands/SystemEdit.cs index a3bcaa64..2ff0c528 100644 --- a/PluralKit.Bot/Commands/SystemEdit.cs +++ b/PluralKit.Bot/Commands/SystemEdit.cs @@ -37,24 +37,35 @@ public class SystemEdit if (isOwnSystem) noNameSetMessage += " Type `pk;system name ` to set one."; - if (ctx.MatchRaw()) - { - if (target.Name != null) - await ctx.Reply($"```\n{target.Name}\n```"); - else + var format = ctx.MatchFormat(); + + // if there's nothing next or what's next is "raw"/"plaintext" we're doing a query, so check for null + if (!ctx.HasNext(false) || format != ReplyFormat.Standard) + if (target.Name == null) + { await ctx.Reply(noNameSetMessage); + return; + } + + if (format == ReplyFormat.Raw) + { + await ctx.Reply($"` ``\n{target.Name}\n` ``"); + return; + } + if (format == ReplyFormat.Plaintext) + { + var eb = new EmbedBuilder() + .Description($"Showing name for system {target.DisplayHid()}"); + await ctx.Reply(target.Name, embed: eb.Build()); return; } if (!ctx.HasNext(false)) { - if (target.Name != null) - await ctx.Reply( - $"{(isOwnSystem ? "Your" : "This")} system's name is currently **{target.Name}**." - + (isOwnSystem ? " Type `pk;system name -clear` to clear it." : "") - + $" Using {target.Name.Length}/{Limits.MaxSystemNameLength} characters."); - else - await ctx.Reply(noNameSetMessage); + await ctx.Reply( + $"{(isOwnSystem ? "Your" : "This")} system's name is currently **{target.Name}**." + + (isOwnSystem ? " Type `pk;system name -clear` to clear it." : "") + + $" Using {target.Name.Length}/{Limits.MaxSystemNameLength} characters."); return; } @@ -91,24 +102,35 @@ public class SystemEdit var settings = await ctx.Repository.GetSystemGuild(ctx.Guild.Id, target.Id); - if (ctx.MatchRaw()) - { - if (settings.DisplayName != null) - await ctx.Reply($"```\n{settings.DisplayName}\n```"); - else + var format = ctx.MatchFormat(); + + // if there's nothing next or what's next is "raw"/"plaintext" we're doing a query, so check for null + if (!ctx.HasNext(false) || format != ReplyFormat.Standard) + if (settings.DisplayName == null) + { await ctx.Reply(noNameSetMessage); + return; + } + + if (format == ReplyFormat.Raw) + { + await ctx.Reply($"` ``\n{settings.DisplayName}\n` ``"); + return; + } + if (format == ReplyFormat.Plaintext) + { + var eb = new EmbedBuilder() + .Description($"Showing servername for system {target.DisplayHid()}"); + await ctx.Reply(settings.DisplayName, embed: eb.Build()); return; } if (!ctx.HasNext(false)) { - if (settings.DisplayName != null) - await ctx.Reply( - $"{(isOwnSystem ? "Your" : "This")} system's name for this server is currently **{settings.DisplayName}**." - + (isOwnSystem ? " Type `pk;system servername -clear` to clear it." : "") - + $" Using {settings.DisplayName.Length}/{Limits.MaxSystemNameLength} characters."); - else - await ctx.Reply(noNameSetMessage); + await ctx.Reply( + $"{(isOwnSystem ? "Your" : "This")} system's name for this server is currently **{settings.DisplayName}**." + + (isOwnSystem ? " Type `pk;system servername -clear` to clear it." : "") + + $" Using {settings.DisplayName.Length}/{Limits.MaxSystemNameLength} characters."); return; } @@ -143,28 +165,39 @@ public class SystemEdit if (isOwnSystem) noDescriptionSetMessage += " To set one, type `pk;s description `."; - if (ctx.MatchRaw()) - { + var format = ctx.MatchFormat(); + + // if there's nothing next or what's next is "raw"/"plaintext" we're doing a query, so check for null + if (!ctx.HasNext(false) || format != ReplyFormat.Standard) if (target.Description == null) + { await ctx.Reply(noDescriptionSetMessage); - else - await ctx.Reply($"```\n{target.Description}\n```"); + return; + } + + if (format == ReplyFormat.Raw) + { + await ctx.Reply($"` ``\n{target.Description}\n` ``"); + return; + } + if (format == ReplyFormat.Plaintext) + { + var eb = new EmbedBuilder() + .Description($"Showing description for system {target.DisplayHid()}"); + await ctx.Reply(target.Description, embed: eb.Build()); return; } if (!ctx.HasNext(false)) { - if (target.Description == null) - await ctx.Reply(noDescriptionSetMessage); - else - await ctx.Reply(embed: new EmbedBuilder() - .Title("System description") - .Description(target.Description) - .Footer(new Embed.EmbedFooter( - "To print the description with formatting, type `pk;s description -raw`." - + (isOwnSystem ? " To clear it, type `pk;s description -clear`. To change it, type `pk;s description `." : "") - + $" Using {target.Description.Length}/{Limits.MaxDescriptionLength} characters.")) - .Build()); + await ctx.Reply(embed: new EmbedBuilder() + .Title("System description") + .Description(target.Description) + .Footer(new Embed.EmbedFooter( + "To print the description with formatting, type `pk;s description -raw`." + + (isOwnSystem ? " To clear it, type `pk;s description -clear`. To change it, type `pk;s description `." : "") + + $" Using {target.Description.Length}/{Limits.MaxDescriptionLength} characters.")) + .Build()); return; } @@ -191,7 +224,7 @@ public class SystemEdit public async Task Color(Context ctx, PKSystem target) { var isOwnSystem = ctx.System?.Id == target.Id; - var matchedRaw = ctx.MatchRaw(); + var matchedFormat = ctx.MatchFormat(); var matchedClear = ctx.MatchClear(); if (!isOwnSystem || !(ctx.HasNext() || matchedClear)) @@ -199,8 +232,10 @@ public class SystemEdit if (target.Color == null) await ctx.Reply( "This system does not have a color set." + (isOwnSystem ? " To set one, type `pk;system color `." : "")); - else if (matchedRaw) + else if (matchedFormat == ReplyFormat.Raw) await ctx.Reply("```\n#" + target.Color + "\n```"); + else if (matchedFormat == ReplyFormat.Plaintext) + await ctx.Reply(target.Color); else await ctx.Reply(embed: new EmbedBuilder() .Title("System color") @@ -246,22 +281,33 @@ public class SystemEdit ? "You currently have no system tag set. To set one, type `pk;s tag `." : "This system currently has no system tag set."; - if (ctx.MatchRaw()) - { + var format = ctx.MatchFormat(); + + // if there's nothing next or what's next is "raw"/"plaintext" we're doing a query, so check for null + if (!ctx.HasNext(false) || format != ReplyFormat.Standard) if (target.Tag == null) + { await ctx.Reply(noTagSetMessage); - else - await ctx.Reply($"```\n{target.Tag}\n```"); + return; + } + + if (format == ReplyFormat.Raw) + { + await ctx.Reply($"```\n{target.Tag}\n```"); + return; + } + if (format == ReplyFormat.Plaintext) + { + var eb = new EmbedBuilder() + .Description($"Showing tag for system {target.DisplayHid()}"); + await ctx.Reply(target.Tag, embed: eb.Build()); return; } if (!ctx.HasNext(false)) { - if (target.Tag == null) - await ctx.Reply(noTagSetMessage); - else - await ctx.Reply($"{(isOwnSystem ? "Your" : "This system's")} current system tag is {target.Tag.AsCode()}." - + (isOwnSystem ? "To change it, type `pk;s tag `. To clear it, type `pk;s tag -clear`." : "")); + await ctx.Reply($"{(isOwnSystem ? "Your" : "This system's")} current system tag is {target.Tag.AsCode()}." + + (isOwnSystem ? "To change it, type `pk;s tag `. To clear it, type `pk;s tag -clear`." : "")); return; } @@ -296,15 +342,22 @@ public class SystemEdit var settings = await ctx.Repository.GetSystemGuild(ctx.Guild.Id, target.Id); - async Task Show(bool raw = false) + async Task Show(ReplyFormat format = ReplyFormat.Standard) { if (settings.Tag != null) { - if (raw) + if (format == ReplyFormat.Raw) { await ctx.Reply($"```{settings.Tag}```"); return; } + if (format == ReplyFormat.Plaintext) + { + var eb = new EmbedBuilder() + .Description($"Showing servertag for system {target.DisplayHid()}"); + await ctx.Reply(settings.Tag, embed: eb.Build()); + return; + } var msg = $"Your current system tag in '{ctx.Guild.Name}' is {settings.Tag.AsCode()}"; if (!settings.TagEnabled) @@ -400,8 +453,8 @@ public class SystemEdit await EnableDisable(false); else if (ctx.Match("enable") || ctx.MatchFlag("enable")) await EnableDisable(true); - else if (ctx.MatchRaw()) - await Show(true); + else if (ctx.MatchFormat() != ReplyFormat.Standard) + await Show(ctx.MatchFormat()); else if (!ctx.HasNext(false)) await Show(); else @@ -418,24 +471,35 @@ public class SystemEdit if (isOwnSystem) noPronounsSetMessage += " To set some, type `pk;system pronouns `"; - if (ctx.MatchRaw()) - { + var format = ctx.MatchFormat(); + + // if there's nothing next or what's next is "raw"/"plaintext" we're doing a query, so check for null + if (!ctx.HasNext(false) || format != ReplyFormat.Standard) if (target.Pronouns == null) + { await ctx.Reply(noPronounsSetMessage); - else - await ctx.Reply($"```\n{target.Pronouns}\n```"); + return; + } + + if (format == ReplyFormat.Raw) + { + await ctx.Reply($"```\n{target.Pronouns}\n```"); + return; + } + if (format == ReplyFormat.Plaintext) + { + var eb = new EmbedBuilder() + .Description($"Showing pronouns for system {target.DisplayHid()}"); + await ctx.Reply(target.Pronouns, embed: eb.Build()); return; } if (!ctx.HasNext(false)) { - if (target.Pronouns == null) - await ctx.Reply(noPronounsSetMessage); - else - await ctx.Reply($"{(isOwnSystem ? "Your" : "This system's")} current pronouns are **{target.Pronouns}**.\nTo print the pronouns with formatting, type `pk;system pronouns -raw`." - + (isOwnSystem ? " To clear them, type `pk;system pronouns -clear`." - : "") - + $" Using {target.Pronouns.Length}/{Limits.MaxPronounsLength} characters."); + await ctx.Reply($"{(isOwnSystem ? "Your" : "This system's")} current pronouns are **{target.Pronouns}**.\nTo print the pronouns with formatting, type `pk;system pronouns -raw`." + + (isOwnSystem ? " To clear them, type `pk;system pronouns -clear`." + : "") + + $" Using {target.Pronouns.Length}/{Limits.MaxPronounsLength} characters."); return; } diff --git a/PluralKit.Bot/Commands/SystemList.cs b/PluralKit.Bot/Commands/SystemList.cs index beb81eda..af0b4cbe 100644 --- a/PluralKit.Bot/Commands/SystemList.cs +++ b/PluralKit.Bot/Commands/SystemList.cs @@ -17,7 +17,7 @@ public class SystemList // - 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)); + var opts = ctx.ParseListOptions(ctx.DirectLookupContextFor(target.Id), ctx.LookupContextFor(target.Id)); await ctx.RenderMemberList( ctx.LookupContextFor(target.Id), target.Id, diff --git a/PluralKit.Bot/Handlers/IEventHandler.cs b/PluralKit.Bot/Handlers/IEventHandler.cs index fd18849e..f6b6ba01 100644 --- a/PluralKit.Bot/Handlers/IEventHandler.cs +++ b/PluralKit.Bot/Handlers/IEventHandler.cs @@ -6,5 +6,5 @@ public interface IEventHandler where T : IGatewayEvent { Task Handle(int shardId, T evt); - ulong? ErrorChannelFor(T evt, ulong userId) => null; + (ulong?, ulong?) ErrorChannelFor(T evt, ulong userId) => (null, null); } \ No newline at end of file diff --git a/PluralKit.Bot/Handlers/MessageCreated.cs b/PluralKit.Bot/Handlers/MessageCreated.cs index aad80bd2..e0387505 100644 --- a/PluralKit.Bot/Handlers/MessageCreated.cs +++ b/PluralKit.Bot/Handlers/MessageCreated.cs @@ -52,7 +52,7 @@ public class MessageCreated: IEventHandler _dmCache = dmCache; } - public ulong? ErrorChannelFor(MessageCreateEvent evt, ulong userId) => evt.ChannelId; + public (ulong?, ulong?) ErrorChannelFor(MessageCreateEvent evt, ulong userId) => (evt.GuildId, evt.ChannelId); private bool IsDuplicateMessage(Message msg) => // We consider a message duplicate if it has the same ID as the previous message that hit the gateway _lastMessageCache.GetLastMessage(msg.ChannelId)?.Current.Id == msg.Id; @@ -63,7 +63,7 @@ public class MessageCreated: IEventHandler if (evt.Type != Message.MessageType.Default && evt.Type != Message.MessageType.Reply) return; if (IsDuplicateMessage(evt)) return; - var botPermissions = await _cache.BotPermissionsIn(evt.ChannelId); + var botPermissions = await _cache.BotPermissionsIn(evt.GuildId ?? 0, evt.ChannelId); if (!botPermissions.HasFlag(PermissionSet.SendMessages)) return; // spawn off saving the private channel into another thread @@ -71,8 +71,8 @@ public class MessageCreated: IEventHandler _ = _dmCache.TrySavePrivateChannel(evt); var guild = evt.GuildId != null ? await _cache.GetGuild(evt.GuildId.Value) : null; - var channel = await _cache.GetChannel(evt.ChannelId); - var rootChannel = await _cache.GetRootChannel(evt.ChannelId); + var channel = await _cache.GetChannel(evt.GuildId ?? 0, evt.ChannelId); + var rootChannel = await _cache.GetRootChannel(evt.GuildId ?? 0, evt.ChannelId); // Log metrics and message info _metrics.Measure.Meter.Mark(BotMetrics.MessagesReceived); @@ -90,7 +90,8 @@ public class MessageCreated: IEventHandler if (await TryHandleCommand(shardId, evt, guild, channel)) return; - await TryHandleProxy(evt, guild, channel, rootChannel.Id, botPermissions); + if (evt.GuildId != null) + await TryHandleProxy(evt, guild, channel, rootChannel.Id, botPermissions); } private async Task TryHandleLogClean(Channel channel, MessageCreateEvent evt) diff --git a/PluralKit.Bot/Handlers/MessageEdited.cs b/PluralKit.Bot/Handlers/MessageEdited.cs index bb33bc15..2ba523f2 100644 --- a/PluralKit.Bot/Handlers/MessageEdited.cs +++ b/PluralKit.Bot/Handlers/MessageEdited.cs @@ -52,10 +52,12 @@ public class MessageEdited: IEventHandler if (!evt.Content.HasValue || !evt.Author.HasValue || !evt.Member.HasValue) return; - var channel = await _cache.GetChannel(evt.ChannelId); + var guildIdMaybe = evt.GuildId.HasValue ? evt.GuildId.Value ?? 0 : 0; + + var channel = await _cache.GetChannel(guildIdMaybe, evt.ChannelId); // todo: is this correct for message update? if (!DiscordUtils.IsValidGuildChannel(channel)) return; - var rootChannel = await _cache.GetRootChannel(channel.Id); + var rootChannel = await _cache.GetRootChannel(guildIdMaybe, channel.Id); var guild = await _cache.GetGuild(channel.GuildId!.Value); var lastMessage = _lastMessageCache.GetLastMessage(evt.ChannelId)?.Current; @@ -69,7 +71,7 @@ public class MessageEdited: IEventHandler ctx = await _repo.GetMessageContext(evt.Author.Value!.Id, channel.GuildId!.Value, rootChannel.Id, evt.ChannelId); var equivalentEvt = await GetMessageCreateEvent(evt, lastMessage, channel); - var botPermissions = await _cache.BotPermissionsIn(channel.Id); + var botPermissions = await _cache.BotPermissionsIn(guildIdMaybe, channel.Id); try { @@ -91,7 +93,7 @@ public class MessageEdited: IEventHandler private async Task GetMessageCreateEvent(MessageUpdateEvent evt, CachedMessage lastMessage, Channel channel) { - var referencedMessage = await GetReferencedMessage(evt.ChannelId, lastMessage.ReferencedMessage); + var referencedMessage = await GetReferencedMessage(evt.GuildId.HasValue ? evt.GuildId.Value ?? 0 : 0, evt.ChannelId, lastMessage.ReferencedMessage); var messageReference = lastMessage.ReferencedMessage != null ? new Message.Reference(channel.GuildId, evt.ChannelId, lastMessage.ReferencedMessage.Value) @@ -118,12 +120,12 @@ public class MessageEdited: IEventHandler return equivalentEvt; } - private async Task GetReferencedMessage(ulong channelId, ulong? referencedMessageId) + private async Task GetReferencedMessage(ulong guildId, ulong channelId, ulong? referencedMessageId) { if (referencedMessageId == null) return null; - var botPermissions = await _cache.BotPermissionsIn(channelId); + var botPermissions = await _cache.BotPermissionsIn(guildId, channelId); if (!botPermissions.HasFlag(PermissionSet.ReadMessageHistory)) { _logger.Warning( diff --git a/PluralKit.Bot/Handlers/ReactionAdded.cs b/PluralKit.Bot/Handlers/ReactionAdded.cs index de6a2c69..633d8f01 100644 --- a/PluralKit.Bot/Handlers/ReactionAdded.cs +++ b/PluralKit.Bot/Handlers/ReactionAdded.cs @@ -62,7 +62,7 @@ public class ReactionAdded: IEventHandler // but we aren't able to get DMs from bots anyway, so it's not really needed if (evt.GuildId != null && (evt.Member?.User?.Bot ?? false)) return; - var channel = await _cache.GetChannel(evt.ChannelId); + var channel = await _cache.GetChannel(evt.GuildId ?? 0, evt.ChannelId); // check if it's a command message first // since this can happen in DMs as well @@ -75,10 +75,10 @@ public class ReactionAdded: IEventHandler return; } - var (authorId, _) = await _commandMessageService.GetCommandMessage(evt.MessageId); - if (authorId != null) + var cmessage = await _commandMessageService.GetCommandMessage(evt.MessageId); + if (cmessage != null) { - await HandleCommandDeleteReaction(evt, authorId.Value, false); + await HandleCommandDeleteReaction(evt, cmessage.AuthorId, false); return; } } @@ -123,7 +123,7 @@ public class ReactionAdded: IEventHandler private async ValueTask HandleProxyDeleteReaction(MessageReactionAddEvent evt, PKMessage msg) { - if (!(await _cache.BotPermissionsIn(evt.ChannelId)).HasFlag(PermissionSet.ManageMessages)) + if (!(await _cache.BotPermissionsIn(evt.GuildId ?? 0, evt.ChannelId)).HasFlag(PermissionSet.ManageMessages)) return; var isSameSystem = msg.Member != null && await _repo.IsMemberOwnedByAccount(msg.Member.Value, evt.UserId); @@ -150,7 +150,7 @@ public class ReactionAdded: IEventHandler if (authorId != null && authorId != evt.UserId) return; - if (!((await _cache.BotPermissionsIn(evt.ChannelId)).HasFlag(PermissionSet.ManageMessages) || isDM)) + if (!((await _cache.BotPermissionsIn(evt.GuildId ?? 0, evt.ChannelId)).HasFlag(PermissionSet.ManageMessages) || isDM)) return; // todo: don't try to delete the user's own messages in DMs @@ -206,14 +206,14 @@ public class ReactionAdded: IEventHandler private async ValueTask HandlePingReaction(MessageReactionAddEvent evt, FullMessage msg) { - if (!(await _cache.BotPermissionsIn(evt.ChannelId)).HasFlag(PermissionSet.ManageMessages)) + if (!(await _cache.BotPermissionsIn(evt.GuildId ?? 0, evt.ChannelId)).HasFlag(PermissionSet.ManageMessages)) return; // Check if the "pinger" has permission to send messages in this channel // (if not, PK shouldn't send messages on their behalf) var member = await _rest.GetGuildMember(evt.GuildId!.Value, evt.UserId); var requiredPerms = PermissionSet.ViewChannel | PermissionSet.SendMessages; - if (member == null || !(await _cache.PermissionsForMemberInChannel(evt.ChannelId, member)).HasFlag(requiredPerms)) return; + if (member == null || !(await _cache.PermissionsForMemberInChannel(evt.GuildId ?? 0, evt.ChannelId, member)).HasFlag(requiredPerms)) return; if (msg.Member == null) return; @@ -266,7 +266,7 @@ public class ReactionAdded: IEventHandler private async Task TryRemoveOriginalReaction(MessageReactionAddEvent evt) { - if ((await _cache.BotPermissionsIn(evt.ChannelId)).HasFlag(PermissionSet.ManageMessages)) + if ((await _cache.BotPermissionsIn(evt.GuildId ?? 0, evt.ChannelId)).HasFlag(PermissionSet.ManageMessages)) await _rest.DeleteUserReaction(evt.ChannelId, evt.MessageId, evt.Emoji, evt.UserId); } } \ No newline at end of file diff --git a/PluralKit.Bot/Init.cs b/PluralKit.Bot/Init.cs index 9e882370..b111a6a4 100644 --- a/PluralKit.Bot/Init.cs +++ b/PluralKit.Bot/Init.cs @@ -42,10 +42,10 @@ public class Init using var _ = SentrySdk.Init(opts => { - opts.Dsn = services.Resolve().SentryUrl; + opts.Dsn = services.Resolve().SentryUrl ?? ""; opts.Release = BuildInfoService.FullVersion; opts.AutoSessionTracking = true; - opts.DisableTaskUnobservedTaskExceptionCapture(); + // opts.DisableTaskUnobservedTaskExceptionCapture(); }); var config = services.Resolve(); @@ -56,8 +56,6 @@ public class Init await redis.InitAsync(coreConfig); var cache = services.Resolve(); - if (cache is RedisDiscordCache) - await (cache as RedisDiscordCache).InitAsync(coreConfig.RedisAddr); if (config.Cluster == null) { diff --git a/PluralKit.Bot/Interactive/YesNoPrompt.cs b/PluralKit.Bot/Interactive/YesNoPrompt.cs index a32d7bba..110e4bb9 100644 --- a/PluralKit.Bot/Interactive/YesNoPrompt.cs +++ b/PluralKit.Bot/Interactive/YesNoPrompt.cs @@ -84,20 +84,32 @@ public class YesNoPrompt: BaseInteractive var queue = _ctx.Services.Resolve>(); - var messageDispatch = queue.WaitFor(MessagePredicate, Timeout, cts.Token); + async Task WaitForMessage() + { + try + { + await queue.WaitFor(MessagePredicate, Timeout, cts.Token); + } + catch (TimeoutException e) + { + if (e.Message != "HandlerQueue#WaitFor timed out") + throw; + } + } await Start(); - cts.Token.Register(() => _tcs.TrySetException(new TimeoutException("Action timed out"))); + var messageDispatch = WaitForMessage(); + + cts.Token.Register(() => _tcs.TrySetException(new TimeoutException("YesNoPrompt timed out"))); try { var doneTask = await Task.WhenAny(_tcs.Task, messageDispatch); - if (doneTask == messageDispatch) - await Finish(); } finally { + await Finish(); Cleanup(); } } diff --git a/PluralKit.Bot/Modules.cs b/PluralKit.Bot/Modules.cs index 730908a9..ea790f61 100644 --- a/PluralKit.Bot/Modules.cs +++ b/PluralKit.Bot/Modules.cs @@ -48,8 +48,25 @@ public class BotModule: Module { var botConfig = c.Resolve(); - if (botConfig.UseRedisCache) - return new RedisDiscordCache(c.Resolve(), botConfig.ClientId); + if (botConfig.HttpCacheUrl != null) + { + var cache = new HttpDiscordCache(c.Resolve(), + c.Resolve(), botConfig.HttpCacheUrl, botConfig.Cluster?.TotalShards ?? 1, botConfig.ClientId, botConfig.HttpUseInnerCache); + + var metrics = c.Resolve(); + + cache.OnDebug += (_, ev) => + { + var (remote, key) = ev; + metrics.Measure.Meter.Mark(BotMetrics.CacheDebug, new MetricTags( + new[] { "remote", "key" }, + new[] { remote.ToString(), key } + )); + }; + + return cache; + } + return new MemoryDiscordCache(botConfig.ClientId); }).AsSelf().SingleInstance(); builder.RegisterType().AsSelf().SingleInstance(); diff --git a/PluralKit.Bot/PluralKit.Bot.csproj b/PluralKit.Bot/PluralKit.Bot.csproj index 27e61fa3..26273422 100644 --- a/PluralKit.Bot/PluralKit.Bot.csproj +++ b/PluralKit.Bot/PluralKit.Bot.csproj @@ -27,7 +27,7 @@ - + diff --git a/PluralKit.Bot/Proxy/ProxyService.cs b/PluralKit.Bot/Proxy/ProxyService.cs index b8a533ef..83299d09 100644 --- a/PluralKit.Bot/Proxy/ProxyService.cs +++ b/PluralKit.Bot/Proxy/ProxyService.cs @@ -59,7 +59,7 @@ public class ProxyService public async Task HandleIncomingMessage(MessageCreateEvent message, MessageContext ctx, Guild guild, Channel channel, bool allowAutoproxy, PermissionSet botPermissions) { - var rootChannel = await _cache.GetRootChannel(message.ChannelId); + var rootChannel = await _cache.GetRootChannel(message.GuildId!.Value, message.ChannelId); if (!ShouldProxy(channel, rootChannel, message, ctx)) return false; @@ -111,31 +111,10 @@ public class ProxyService return true; } -#pragma warning disable CA1822 // Mark members as static - internal bool CanProxyInChannel(Channel ch, bool isRootChannel = false) -#pragma warning restore CA1822 // Mark members as static - { - // this is explicitly selecting known channel types so that when Discord add new - // ones, users don't get flooded with error codes if that new channel type doesn't - // support a feature we need for proxying - return ch.Type switch - { - Channel.ChannelType.GuildText => true, - Channel.ChannelType.GuildPublicThread => true, - Channel.ChannelType.GuildPrivateThread => true, - Channel.ChannelType.GuildNews => true, - Channel.ChannelType.GuildNewsThread => true, - Channel.ChannelType.GuildVoice => true, - Channel.ChannelType.GuildStageVoice => true, - Channel.ChannelType.GuildForum => isRootChannel, - Channel.ChannelType.GuildMedia => isRootChannel, - _ => false, - }; - } - + // Proxy checks that give user errors public async Task CanProxy(Channel channel, Channel rootChannel, Message msg, MessageContext ctx) { - if (!(CanProxyInChannel(channel) && CanProxyInChannel(rootChannel, true))) + if (!DiscordUtils.IsValidGuildChannel(channel)) return $"PluralKit cannot proxy messages in this type of channel."; // Check if the message does not go over any Discord Nitro limits @@ -159,6 +138,7 @@ public class ProxyService return null; } + // Proxy checks that don't give user errors unless `pk;debug proxy` is used public bool ShouldProxy(Channel channel, Channel rootChannel, Message msg, MessageContext ctx) { // Make sure author has a system @@ -189,9 +169,9 @@ public class ProxyService throw new ProxyChecksFailedException( "Your system has proxying disabled in this server. Type `pk;proxy on` to enable it."); - // Make sure we have either an attachment or message content + // Make sure we have an attachment, message content, or poll var isMessageBlank = msg.Content == null || msg.Content.Trim().Length == 0; - if (isMessageBlank && msg.Attachments.Length == 0) + if (isMessageBlank && msg.Attachments.Length == 0 && msg.Poll == null) throw new ProxyChecksFailedException("Message cannot be blank."); if (msg.Activity != null) @@ -227,8 +207,8 @@ public class ProxyService var content = match.ProxyContent; if (!allowEmbeds) content = content.BreakLinkEmbeds(); - var messageChannel = await _cache.GetChannel(trigger.ChannelId); - var rootChannel = await _cache.GetRootChannel(trigger.ChannelId); + var messageChannel = await _cache.GetChannel(trigger.GuildId!.Value, trigger.ChannelId); + var rootChannel = await _cache.GetRootChannel(trigger.GuildId!.Value, trigger.ChannelId); var threadId = messageChannel.IsThread() ? messageChannel.Id : (ulong?)null; var guild = await _cache.GetGuild(trigger.GuildId.Value); var guildMember = await _rest.GetGuildMember(trigger.GuildId!.Value, trigger.Author.Id); @@ -242,6 +222,7 @@ public class ProxyService GuildId = trigger.GuildId!.Value, ChannelId = rootChannel.Id, ThreadId = threadId, + MessageId = trigger.Id, Name = await FixSameName(messageChannel.Id, ctx, match.Member), AvatarUrl = AvatarUtils.TryRewriteCdnUrl(match.Member.ProxyAvatar(ctx)), Content = content, @@ -252,6 +233,7 @@ public class ProxyService AllowEveryone = allowEveryone, Flags = trigger.Flags.HasFlag(Message.MessageFlags.VoiceMessage) ? Message.MessageFlags.VoiceMessage : null, Tts = tts, + Poll = trigger.Poll, }); await HandleProxyExecutedActions(ctx, autoproxySettings, trigger, proxyMessage, match); } @@ -310,6 +292,7 @@ public class ProxyService GuildId = guild.Id, ChannelId = rootChannel.Id, ThreadId = threadId, + MessageId = originalMsg.Id, Name = match.Member.ProxyName(ctx), AvatarUrl = AvatarUtils.TryRewriteCdnUrl(match.Member.ProxyAvatar(ctx)), Content = match.ProxyContent!, @@ -320,6 +303,7 @@ public class ProxyService AllowEveryone = allowEveryone, Flags = originalMsg.Flags.HasFlag(Message.MessageFlags.VoiceMessage) ? Message.MessageFlags.VoiceMessage : null, Tts = tts, + Poll = originalMsg.Poll, }); diff --git a/PluralKit.Bot/Services/AvatarHostingService.cs b/PluralKit.Bot/Services/AvatarHostingService.cs index a080bae8..6e88c4af 100644 --- a/PluralKit.Bot/Services/AvatarHostingService.cs +++ b/PluralKit.Bot/Services/AvatarHostingService.cs @@ -9,22 +9,35 @@ public class AvatarHostingService private readonly BotConfig _config; private readonly HttpClient _client; - public AvatarHostingService(BotConfig config, HttpClient client) + public AvatarHostingService(BotConfig config) { _config = config; - _client = client; + _client = new HttpClient + { + Timeout = TimeSpan.FromSeconds(10), + }; } public async Task TryRehostImage(ParsedImage input, RehostedImageType type, ulong userId, PKSystem? system) { - var uploaded = await TryUploadAvatar(input.Url, type, userId, system); - if (uploaded != null) + try { - // todo: make new image type called Cdn? - return new ParsedImage { Url = uploaded, Source = AvatarSource.HostedCdn }; - } + var uploaded = await TryUploadAvatar(input.Url, type, userId, system); + if (uploaded != null) + { + // todo: make new image type called Cdn? + return new ParsedImage { Url = uploaded, Source = AvatarSource.HostedCdn }; + } - return input; + return input; + } + catch (TaskCanceledException e) + { + // don't show an internal error to users + if (e.Message.Contains("HttpClient.Timeout")) + throw new PKError("Temporary error setting image, please try again later"); + throw; + } } public async Task TryUploadAvatar(string? avatarUrl, RehostedImageType type, ulong userId, PKSystem? system) diff --git a/PluralKit.Bot/Services/CommandMessageService.cs b/PluralKit.Bot/Services/CommandMessageService.cs index 5ef84ad1..796f7f0a 100644 --- a/PluralKit.Bot/Services/CommandMessageService.cs +++ b/PluralKit.Bot/Services/CommandMessageService.cs @@ -18,7 +18,7 @@ public class CommandMessageService _logger = logger.ForContext(); } - public async Task RegisterMessage(ulong messageId, ulong channelId, ulong authorId) + public async Task RegisterMessage(ulong messageId, ulong guildId, ulong channelId, ulong authorId) { if (_redis.Connection == null) return; @@ -27,17 +27,19 @@ public class CommandMessageService messageId, authorId, channelId ); - await _redis.Connection.GetDatabase().StringSetAsync(messageId.ToString(), $"{authorId}-{channelId}", expiry: CommandMessageRetention); + await _redis.Connection.GetDatabase().StringSetAsync(messageId.ToString(), $"{authorId}-{channelId}-{guildId}", expiry: CommandMessageRetention); } - public async Task<(ulong?, ulong?)> GetCommandMessage(ulong messageId) + public async Task GetCommandMessage(ulong messageId) { var str = await _redis.Connection.GetDatabase().StringGetAsync(messageId.ToString()); if (str.HasValue) { var split = ((string)str).Split("-"); - return (ulong.Parse(split[0]), ulong.Parse(split[1])); + return new CommandMessage(ulong.Parse(split[0]), ulong.Parse(split[1]), ulong.Parse(split[2])); } - return (null, null); + return null; } -} \ No newline at end of file +} + +public record CommandMessage(ulong AuthorId, ulong ChannelId, ulong GuildId); \ No newline at end of file diff --git a/PluralKit.Bot/Services/EmbedService.cs b/PluralKit.Bot/Services/EmbedService.cs index 1ce59fbb..73874c7e 100644 --- a/PluralKit.Bot/Services/EmbedService.cs +++ b/PluralKit.Bot/Services/EmbedService.cs @@ -263,7 +263,7 @@ public class EmbedService else if (system.NameFor(ctx) != null) nameField = $"{nameField} ({system.NameFor(ctx)})"; else - nameField = $"{nameField} ({system.Name})"; + nameField = $"{nameField}"; var eb = new EmbedBuilder() .Author(new Embed.EmbedAuthor(nameField, IconUrl: target.IconFor(pctx), Url: $"https://dash.pluralkit.me/profile/g/{target.Hid}")) @@ -336,7 +336,7 @@ public class EmbedService public async Task CreateMessageInfoEmbed(FullMessage msg, bool showContent, SystemConfig? ccfg = null) { - var channel = await _cache.GetOrFetchChannel(_rest, msg.Message.Channel); + var channel = await _cache.GetOrFetchChannel(_rest, msg.Message.Guild ?? 0, msg.Message.Channel); var ctx = LookupContext.ByNonOwner; var serverMsg = await _rest.GetMessageOrNull(msg.Message.Channel, msg.Message.Mid); @@ -403,14 +403,15 @@ public class EmbedService var roles = memberInfo?.Roles?.ToList(); if (roles != null && roles.Count > 0 && showContent) { - var rolesString = string.Join(", ", (await Task.WhenAll(roles - .Select(async id => + var guild = await _cache.GetGuild(channel.GuildId!.Value); + var rolesString = string.Join(", ", (roles + .Select(id => { - var role = await _cache.TryGetRole(id); + var role = Array.Find(guild.Roles, r => r.Id == id); if (role != null) return role; return new Role { Name = "*(unknown role)*", Position = 0 }; - }))) + })) .OrderByDescending(role => role.Position) .Select(role => role.Name)); eb.Field(new Embed.Field($"Account roles ({roles.Count})", rolesString.Truncate(1024))); diff --git a/PluralKit.Bot/Services/LogChannelService.cs b/PluralKit.Bot/Services/LogChannelService.cs index c8b21282..f7149e75 100644 --- a/PluralKit.Bot/Services/LogChannelService.cs +++ b/PluralKit.Bot/Services/LogChannelService.cs @@ -42,7 +42,7 @@ public class LogChannelService if (logChannelId == null) return; - var triggerChannel = await _cache.GetChannel(proxiedMessage.Channel); + var triggerChannel = await _cache.GetChannel(proxiedMessage.Guild!.Value, proxiedMessage.Channel); var member = await _repo.GetMember(proxiedMessage.Member!.Value); var system = await _repo.GetSystem(member.System); @@ -63,7 +63,7 @@ public class LogChannelService return null; var guildId = proxiedMessage.Guild ?? trigger.GuildId.Value; - var rootChannel = await _cache.GetRootChannel(trigger.ChannelId); + var rootChannel = await _cache.GetRootChannel(guildId, trigger.ChannelId); // get log channel info from the database var guild = await _repo.GetGuild(guildId); @@ -109,7 +109,7 @@ public class LogChannelService private async Task FindLogChannel(ulong guildId, ulong channelId) { // TODO: fetch it directly on cache miss? - if (await _cache.TryGetChannel(channelId) is Channel channel) + if (await _cache.TryGetChannel(guildId, channelId) is Channel channel) return channel; if (await _rest.GetChannelOrNull(channelId) is Channel restChannel) diff --git a/PluralKit.Bot/Services/LoggerCleanService.cs b/PluralKit.Bot/Services/LoggerCleanService.cs index 0f623e68..120bcda8 100644 --- a/PluralKit.Bot/Services/LoggerCleanService.cs +++ b/PluralKit.Bot/Services/LoggerCleanService.cs @@ -23,6 +23,7 @@ public class LoggerCleanService private static readonly Regex _basicRegex = new("(\\d{17,19})"); private static readonly Regex _dynoRegex = new("Message ID: (\\d{17,19})"); private static readonly Regex _carlRegex = new("Message ID: (\\d{17,19})"); + private static readonly Regex _makiRegex = new("Message ID: (\\d{17,19})"); private static readonly Regex _circleRegex = new("\\(`(\\d{17,19})`\\)"); private static readonly Regex _loggerARegex = new("Message = (\\d{17,19})"); private static readonly Regex _loggerBRegex = new("MessageID:(\\d{17,19})"); @@ -60,6 +61,7 @@ public class LoggerCleanService new LoggerBot("Dyno#8389", 470724017205149701, ExtractDyno), // webhook new LoggerBot("Dyno#5714", 470723870270160917, ExtractDyno), // webhook new LoggerBot("Dyno#1961", 347378323418251264, ExtractDyno), // webhook + new LoggerBot("Maki", 563434444321587202, ExtractMaki), // webhook new LoggerBot("Auttaja", 242730576195354624, ExtractAuttaja), // webhook new LoggerBot("GenericBot", 295329346590343168, ExtractGenericBot), new LoggerBot("blargbot", 134133271750639616, ExtractBlargBot), @@ -98,10 +100,10 @@ public class LoggerCleanService public async ValueTask HandleLoggerBotCleanup(Message msg) { - var channel = await _cache.GetChannel(msg.ChannelId); + var channel = await _cache.GetChannel(msg.GuildId!.Value, msg.ChannelId!); if (channel.Type != Channel.ChannelType.GuildText) return; - if (!(await _cache.BotPermissionsIn(channel.Id)).HasFlag(PermissionSet.ManageMessages)) return; + if (!(await _cache.BotPermissionsIn(msg.GuildId!.Value, channel.Id)).HasFlag(PermissionSet.ManageMessages)) return; // If this message is from a *webhook*, check if the application ID matches one of the bots we know // If it's from a *bot*, check the bot ID to see if we know it. @@ -231,6 +233,15 @@ public class LoggerCleanService return match.Success ? ulong.Parse(match.Groups[1].Value) : null; } + private static ulong? ExtractMaki(Message msg) + { + // Embed, Message Author Name field: "Message Deleted", footer is "Message ID: [id]" + var embed = msg.Embeds?.FirstOrDefault(); + if (embed.Author?.Name == null || embed?.Footer == null || !embed.Author.Name.StartsWith("Message Deleted")) return null; + var match = _makiRegex.Match(embed.Footer.Text ?? ""); + return match.Success ? ulong.Parse(match.Groups[1].Value) : null; + } + private static FuzzyExtractResult? ExtractCircle(Message msg) { // Like Auttaja, Circle has both embed and compact modes, but the regex works for both. diff --git a/PluralKit.Bot/Services/PeriodicStatCollector.cs b/PluralKit.Bot/Services/PeriodicStatCollector.cs index 92c5ea05..c73294a6 100644 --- a/PluralKit.Bot/Services/PeriodicStatCollector.cs +++ b/PluralKit.Bot/Services/PeriodicStatCollector.cs @@ -54,33 +54,6 @@ public class PeriodicStatCollector var stopwatch = new Stopwatch(); stopwatch.Start(); - // Aggregate guild/channel stats - var guildCount = 0; - var channelCount = 0; - - // No LINQ today, sorry - await foreach (var guild in _cache.GetAllGuilds()) - { - guildCount++; - foreach (var channel in await _cache.GetGuildChannels(guild.Id)) - if (DiscordUtils.IsValidGuildChannel(channel)) - channelCount++; - } - - if (_config.UseRedisMetrics) - { - var db = _redis.Connection.GetDatabase(); - await db.HashSetAsync("pluralkit:cluster_stats", new StackExchange.Redis.HashEntry[] { - new(_botConfig.Cluster.NodeIndex, JsonConvert.SerializeObject(new ClusterMetricInfo - { - GuildCount = guildCount, - ChannelCount = channelCount, - DatabaseConnectionCount = _countHolder.ConnectionCount, - WebhookCacheSize = _webhookCache.CacheSize, - })), - }); - } - // Process info var process = Process.GetCurrentProcess(); _metrics.Measure.Gauge.SetValue(CoreMetrics.ProcessPhysicalMemory, process.WorkingSet64); diff --git a/PluralKit.Bot/Services/WebhookExecutorService.cs b/PluralKit.Bot/Services/WebhookExecutorService.cs index fa38d22f..fc23388c 100644 --- a/PluralKit.Bot/Services/WebhookExecutorService.cs +++ b/PluralKit.Bot/Services/WebhookExecutorService.cs @@ -4,6 +4,8 @@ using App.Metrics; using Humanizer; +using NodaTime.Text; + using Myriad.Cache; using Myriad.Extensions; using Myriad.Rest; @@ -35,6 +37,7 @@ public record ProxyRequest public ulong GuildId { get; init; } public ulong ChannelId { get; init; } public ulong? ThreadId { get; init; } + public ulong MessageId { get; init; } public string Name { get; init; } public string? AvatarUrl { get; init; } public string? Content { get; init; } @@ -45,6 +48,7 @@ public record ProxyRequest public bool AllowEveryone { get; init; } public Message.MessageFlags? Flags { get; init; } public bool Tts { get; init; } + public Message.MessagePoll? Poll { get; init; } } public class WebhookExecutorService @@ -83,7 +87,7 @@ public class WebhookExecutorService return webhookMessage; } - public async Task EditWebhookMessage(ulong channelId, ulong messageId, string newContent, bool clearEmbeds = false) + public async Task EditWebhookMessage(ulong guildId, ulong channelId, ulong messageId, string newContent, bool clearEmbeds = false) { var allowedMentions = newContent.ParseMentions() with { @@ -92,7 +96,7 @@ public class WebhookExecutorService }; ulong? threadId = null; - var channel = await _cache.GetOrFetchChannel(_rest, channelId); + var channel = await _cache.GetOrFetchChannel(_rest, guildId, channelId); if (channel.IsThread()) { threadId = channelId; @@ -154,6 +158,26 @@ public class WebhookExecutorService }).ToArray(); } + if (req.Poll is Message.MessagePoll poll) + { + int? duration = null; + if (poll.Expiry is string expiry) + { + var then = OffsetDateTimePattern.ExtendedIso.Parse(expiry).Value.ToInstant(); + var now = DiscordUtils.SnowflakeToInstant(req.MessageId); + // in theory .TotalHours should be exact, but just in case + duration = (int)Math.Round((then - now).TotalMinutes / 60.0); + } + webhookReq.Poll = new ExecuteWebhookRequest.WebhookPoll + { + Question = poll.Question, + Answers = poll.Answers, + Duration = duration, + AllowMultiselect = poll.AllowMultiselect, + LayoutType = poll.LayoutType + }; + } + Message webhookMessage; using (_metrics.Measure.Timer.Time(BotMetrics.WebhookResponseTime)) { diff --git a/PluralKit.Bot/Utils/MiscUtils.cs b/PluralKit.Bot/Utils/MiscUtils.cs index ed05fd01..bb7a3aa7 100644 --- a/PluralKit.Bot/Utils/MiscUtils.cs +++ b/PluralKit.Bot/Utils/MiscUtils.cs @@ -49,17 +49,17 @@ public static class MiscUtils if (e is WebhookExecutionErrorOnDiscordsEnd) return false; // Socket errors are *not our problem* - if (e.GetBaseException() is SocketException) return false; + // if (e.GetBaseException() is SocketException) return false; // Tasks being cancelled for whatver reason are, you guessed it, also not our problem. - if (e is TaskCanceledException) return false; + // if (e is TaskCanceledException) return false; // Sometimes Discord just times everything out. - if (e is TimeoutException) return false; + // if (e is TimeoutException) return false; if (e is UnknownDiscordRequestException tde && tde.Message == "Request Timeout") return false; // HTTP/2 streams are complicated and break sometimes. - if (e is HttpRequestException) return false; + // if (e is HttpRequestException) return false; // This may expanded at some point. return true; diff --git a/PluralKit.Bot/Utils/ModelUtils.cs b/PluralKit.Bot/Utils/ModelUtils.cs index 513f4e1d..ddeedfee 100644 --- a/PluralKit.Bot/Utils/ModelUtils.cs +++ b/PluralKit.Bot/Utils/ModelUtils.cs @@ -29,14 +29,14 @@ public static class ModelUtils public static string DisplayHid(this PKSystem system, SystemConfig? cfg = null, bool isList = false) => HidTransform(system.Hid, cfg, isList); - public static string DisplayHid(this PKGroup group, SystemConfig? cfg = null, bool isList = false) => HidTransform(group.Hid, cfg, isList); - public static string DisplayHid(this PKMember member, SystemConfig? cfg = null, bool isList = false) => HidTransform(member.Hid, cfg, isList); - private static string HidTransform(string hid, SystemConfig? cfg = null, bool isList = false) => + public static string DisplayHid(this PKGroup group, SystemConfig? cfg = null, bool isList = false, bool shouldPad = true) => HidTransform(group.Hid, cfg, isList, shouldPad); + public static string DisplayHid(this PKMember member, SystemConfig? cfg = null, bool isList = false, bool shouldPad = true) => HidTransform(member.Hid, cfg, isList, shouldPad); + private static string HidTransform(string hid, SystemConfig? cfg = null, bool isList = false, bool shouldPad = true) => HidUtils.HidTransform( hid, cfg != null && cfg.HidDisplaySplit, cfg != null && cfg.HidDisplayCaps, - isList ? (cfg?.HidListPadding ?? SystemConfig.HidPadFormat.None) : SystemConfig.HidPadFormat.None // padding only on lists + isList && shouldPad ? (cfg?.HidListPadding ?? SystemConfig.HidPadFormat.None) : SystemConfig.HidPadFormat.None // padding only on lists ); private static string EntityReference(string hid, string name) diff --git a/PluralKit.Bot/Utils/SerilogGatewayEnricherFactory.cs b/PluralKit.Bot/Utils/SerilogGatewayEnricherFactory.cs index 4fba3f24..76e00a80 100644 --- a/PluralKit.Bot/Utils/SerilogGatewayEnricherFactory.cs +++ b/PluralKit.Bot/Utils/SerilogGatewayEnricherFactory.cs @@ -38,9 +38,11 @@ public class SerilogGatewayEnricherFactory { props.Add(new LogEventProperty("ChannelId", new ScalarValue(channel.Value))); - if (await _cache.TryGetChannel(channel.Value) != null) + var guildIdForCache = guild != null ? guild.Value : 0; + + if (await _cache.TryGetChannel(guildIdForCache, channel.Value) != null) { - var botPermissions = await _cache.BotPermissionsIn(channel.Value); + var botPermissions = await _cache.BotPermissionsIn(guildIdForCache, channel.Value); props.Add(new LogEventProperty("BotPermissions", new ScalarValue(botPermissions))); } } diff --git a/PluralKit.Bot/packages.lock.json b/PluralKit.Bot/packages.lock.json index 3262ef22..df9348cc 100644 --- a/PluralKit.Bot/packages.lock.json +++ b/PluralKit.Bot/packages.lock.json @@ -36,9 +36,9 @@ }, "Sentry": { "type": "Direct", - "requested": "[3.11.1, )", - "resolved": "3.11.1", - "contentHash": "T/NLfs6MMkUSYsPEDajB9ad0124T18I0uUod5MNOev3iwjvcnIEQBrStEX2olbIxzqfvGXzQ/QFqTfA2ElLPlA==" + "requested": "[4.12.1, )", + "resolved": "4.12.1", + "contentHash": "OLf7885OKHWLaTLTyw884mwOT4XKCWj2Hz5Wuz/TJemJqXwCIdIljkJBIoeHviRUPvtB7ulDgeYXf/Z7ScToSA==" }, "SixLabors.ImageSharp": { "type": "Direct", diff --git a/PluralKit.Core/CoreConfig.cs b/PluralKit.Core/CoreConfig.cs index cac76e3d..4adf815b 100644 --- a/PluralKit.Core/CoreConfig.cs +++ b/PluralKit.Core/CoreConfig.cs @@ -8,7 +8,6 @@ public class CoreConfig public string? MessagesDatabase { get; set; } public string? DatabasePassword { get; set; } public string RedisAddr { get; set; } - public bool UseRedisMetrics { get; set; } = false; public string SentryUrl { get; set; } public string InfluxUrl { get; set; } public string InfluxDb { get; set; } diff --git a/PluralKit.Core/Database/Views/DatabaseViewsExt.cs b/PluralKit.Core/Database/Views/DatabaseViewsExt.cs index f94aae61..877c767d 100644 --- a/PluralKit.Core/Database/Views/DatabaseViewsExt.cs +++ b/PluralKit.Core/Database/Views/DatabaseViewsExt.cs @@ -10,7 +10,11 @@ public static class DatabaseViewsExt public static Task> QueryGroupList(this IPKConnection conn, SystemId system, ListQueryOptions opts) { - StringBuilder query = new StringBuilder("select * from group_list where system = @system"); + StringBuilder query; + if (opts.MemberFilter == null) + query = new StringBuilder("select * from group_list where system = @system"); + else + query = new StringBuilder("select group_list.* from group_members inner join group_list on group_list.id = group_members.group_id where member_id = @MemberFilter"); if (opts.PrivacyFilter != null) query.Append($" and visibility = {(int)opts.PrivacyFilter}"); @@ -20,7 +24,8 @@ public static class DatabaseViewsExt static string Filter(string column) => $"(position(lower(@filter) in lower(coalesce({column}, ''))) > 0)"; - query.Append($" and ({Filter("name")} or {Filter("display_name")}"); + var nameColumn = opts.Context == LookupContext.ByOwner ? "name" : "public_name"; + query.Append($" and ({Filter(nameColumn)} or {Filter("display_name")}"); if (opts.SearchDescription) { // We need to account for the possibility of description privacy when searching @@ -36,7 +41,7 @@ public static class DatabaseViewsExt return conn.QueryAsync( query.ToString(), - new { system, filter = opts.Search }); + new { system, filter = opts.Search, memberFilter = opts.MemberFilter }); } public static Task> QueryMemberList(this IPKConnection conn, SystemId system, ListQueryOptions opts) @@ -56,7 +61,8 @@ public static class DatabaseViewsExt static string Filter(string column) => $"(position(lower(@filter) in lower(coalesce({column}, ''))) > 0)"; - query.Append($" and ({Filter("name")} or {Filter("display_name")}"); + var nameColumn = opts.Context == LookupContext.ByOwner ? "name" : "public_name"; + query.Append($" and ({Filter(nameColumn)} or {Filter("display_name")}"); if (opts.SearchDescription) { // We need to account for the possibility of description privacy when searching @@ -81,5 +87,6 @@ public static class DatabaseViewsExt public bool SearchDescription; public LookupContext Context; public GroupId? GroupFilter; + public MemberId? MemberFilter; } } \ No newline at end of file diff --git a/PluralKit.Core/Database/Views/views.sql b/PluralKit.Core/Database/Views/views.sql index e23ebb6f..edd06096 100644 --- a/PluralKit.Core/Database/Views/views.sql +++ b/PluralKit.Core/Database/Views/views.sql @@ -30,7 +30,15 @@ select members.*, -- Privacy '1' = public; just return description as normal when members.description_privacy = 1 then members.description -- Any other privacy (rn just '2'), return null description (missing case = null in SQL) - end as public_description + end as public_description, + + -- Extract member name as seen by "the public" + case + -- Privacy '1' = public; just return name as normal + when members.name_privacy = 1 then members.name + -- Any other privacy (rn just '2'), return display name + else coalesce(members.display_name, members.name) + end as public_name from members; create view group_list as @@ -48,5 +56,20 @@ select groups.*, inner join members on group_members.member_id = members.id where group_members.group_id = groups.id - ) as total_member_count -from groups; \ No newline at end of file + ) as total_member_count, + + -- Extract group description as seen by "the public" + case + -- Privacy '1' = public; just return description as normal + when groups.description_privacy = 1 then groups.description + -- Any other privacy (rn just '2'), return null description (missing case = null in SQL) + end as public_description, + + -- Extract member name as seen by "the public" + case + -- Privacy '1' = public; just return name as normal + when groups.name_privacy = 1 then groups.name + -- Any other privacy (rn just '2'), return display name + else coalesce(groups.display_name, groups.name) + end as public_name +from groups; diff --git a/PluralKit.Core/Dispatch/DispatchService.cs b/PluralKit.Core/Dispatch/DispatchService.cs index f9f79169..832e63d3 100644 --- a/PluralKit.Core/Dispatch/DispatchService.cs +++ b/PluralKit.Core/Dispatch/DispatchService.cs @@ -247,7 +247,7 @@ public class DispatchService { var repo = _provider.Resolve(); var system = await repo.GetSystemByAccount(accountId); - if (system.WebhookUrl == null) + if (system?.WebhookUrl == null) return; var data = new UpdateDispatchData(); diff --git a/PluralKit.Core/Utils/HandlerQueue.cs b/PluralKit.Core/Utils/HandlerQueue.cs index 076937cd..e91392fe 100644 --- a/PluralKit.Core/Utils/HandlerQueue.cs +++ b/PluralKit.Core/Utils/HandlerQueue.cs @@ -29,7 +29,7 @@ public class HandlerQueue { var theTask = await Task.WhenAny(timeoutTask, tcs.Task); if (theTask == timeoutTask) - throw new TimeoutException(); + throw new TimeoutException("HandlerQueue#WaitFor timed out"); } finally { diff --git a/dashboard/package.json b/dashboard/package.json index 225a8627..26725e70 100644 --- a/dashboard/package.json +++ b/dashboard/package.json @@ -27,7 +27,7 @@ "bootstrap": "^5.1.3", "bootstrap-dark-5": "^1.1.3", "core-js-pure": "^3.23.4", - "discord-markdown": "github:draconizations/discord-markdown#1f74a7094777d5bdfd123c0aac59d8b10db89b30", + "discord-markdown": "github:draconizations/discord-markdown#9d25e45015766779916baea52c37ae0fe12aac73", "gh-pages": "^3.2.3", "highlight.js": "^11.7.0", "import": "^0.0.6", diff --git a/dashboard/src/components/common/CardsHeader.svelte b/dashboard/src/components/common/CardsHeader.svelte index d1e54e6a..ac842516 100644 --- a/dashboard/src/components/common/CardsHeader.svelte +++ b/dashboard/src/components/common/CardsHeader.svelte @@ -39,8 +39,6 @@ else icon_url = item.webhook_avatar_url ?? default_avatar } - $: icon_url_resized = icon_url ? resizeMedia(icon_url) : default_avatar - let avatarOpen = false; const toggleAvatarModal = () => (avatarOpen = !avatarOpen); @@ -65,8 +63,8 @@ ({item.id})
- {#if item && (item.avatar_url || item.icon)} - {if (event.key === "Enter") {avatarOpen = true}}} on:click|stopPropagation={toggleAvatarModal} class="rounded-circle avatar" src={icon_url_resized} alt={altText} /> + {#if item && (item.avatar_url || item.webhook_avatar_url || item.icon)} + {if (event.key === "Enter") {avatarOpen = true}}} on:click|stopPropagation={toggleAvatarModal} class="rounded-circle avatar" src={icon_url} alt={altText} /> {:else} icon (default) {/if} diff --git a/dashboard/src/components/group/Edit.svelte b/dashboard/src/components/group/Edit.svelte index c1958454..d51ec2eb 100644 --- a/dashboard/src/components/group/Edit.svelte +++ b/dashboard/src/components/group/Edit.svelte @@ -25,6 +25,9 @@ err = []; success = false; + // trim all string fields + Object.keys(data).forEach(k => data[k] = typeof data[k] == "string" ? data[k].trim() : data[k]); + if (!data.name) err.push("Group name cannot be empty.") if (data.color && !/^#?[A-Fa-f0-9]{6}$/.test(input.color)) { @@ -35,9 +38,6 @@ } } - // trim all string fields - Object.keys(data).forEach(k => data[k] = typeof data[k] == "string" ? data[k].trim() : data[k]); - err = err; if (err.length > 0) return; diff --git a/dashboard/src/components/member/Edit.svelte b/dashboard/src/components/member/Edit.svelte index bf9a2734..ccf12590 100644 --- a/dashboard/src/components/member/Edit.svelte +++ b/dashboard/src/components/member/Edit.svelte @@ -24,6 +24,9 @@ let data = input; err = []; + // trim all string fields + Object.keys(data).forEach(k => data[k] = typeof data[k] == "string" ? data[k].trim() : data[k]); + if (!data.name) err.push("Member name cannot be empty.") if (data.color && !/^#?[A-Fa-f0-9]{6}$/.test(input.color)) { @@ -58,9 +61,6 @@ } } - // trim all string fields - Object.keys(data).forEach(k => data[k] = typeof data[k] == "string" ? data[k].trim() : data[k]); - err = err; if (err.length > 0) return; diff --git a/dashboard/src/main.ts b/dashboard/src/main.ts index bdf470ef..b569a6f7 100644 --- a/dashboard/src/main.ts +++ b/dashboard/src/main.ts @@ -7,7 +7,7 @@ if (!String.prototype.replaceAll) String.prototype.replaceAll = replaceAll; Sentry.init({ - dsn: "https://973beecd91934f9992c72c942770bdd2@sentry.pluralkit.me/3", + dsn: "https://79ba4b55fdce475ebc5d37df8b75d72a@gt.pluralkit.me/5", integrations: [new Integrations.BrowserTracing()], enabled: !window.location.origin.includes("localhost"), diff --git a/dashboard/styles/dark.scss b/dashboard/styles/dark.scss index c8ce5947..0b69b8e0 100644 --- a/dashboard/styles/dark.scss +++ b/dashboard/styles/dark.scss @@ -52,4 +52,8 @@ .d-spoiler:active { color: $body-color-alt; //overwrite } + + small { + color: #707070; + } } \ No newline at end of file diff --git a/dashboard/styles/generic.scss b/dashboard/styles/generic.scss index 51ecc3d0..c63d2507 100644 --- a/dashboard/styles/generic.scss +++ b/dashboard/styles/generic.scss @@ -193,6 +193,12 @@ code { vertical-align: -0.1125em; } +small { + display: block; + color: #808080; + font-size: 0.5rem; +} + //twemoji img.emoji { height: 1.125em; diff --git a/dashboard/yarn.lock b/dashboard/yarn.lock index 0bdde3b5..62761637 100644 --- a/dashboard/yarn.lock +++ b/dashboard/yarn.lock @@ -341,9 +341,9 @@ detect-indent@^6.0.0: resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-6.1.0.tgz#592485ebbbf6b3b1ab2be175c8393d04ca0d57e6" integrity sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA== -"discord-markdown@github:draconizations/discord-markdown#1f74a7094777d5bdfd123c0aac59d8b10db89b30": +"discord-markdown@github:draconizations/discord-markdown#9d25e45015766779916baea52c37ae0fe12aac73": version "2.5.1" - resolved "https://codeload.github.com/draconizations/discord-markdown/tar.gz/1f74a7094777d5bdfd123c0aac59d8b10db89b30" + resolved "https://codeload.github.com/draconizations/discord-markdown/tar.gz/9d25e45015766779916baea52c37ae0fe12aac73" dependencies: js-base64 "^3.7.7" simple-markdown "^0.7.3" diff --git a/docs/content/api/changelog.md b/docs/content/api/changelog.md index 453c01c2..e1acb195 100644 --- a/docs/content/api/changelog.md +++ b/docs/content/api/changelog.md @@ -5,7 +5,7 @@ permalink: /api/changelog # Version history -* 2024-08-84 +* 2024-08-04 * Added ratelimit scopes (separate limits for different sets of endpoints) * 2024-05-01 * Short IDs (the `id` field in system / member / group models) can now be either 5 or 6 characters. diff --git a/docs/content/command-list.md b/docs/content/command-list.md index 0df96f73..610b8858 100644 --- a/docs/content/command-list.md +++ b/docs/content/command-list.md @@ -123,7 +123,8 @@ You can have a space after `pk;`, e.g. `pk;system` and `pk; system` will do the ## Switching commands - `pk;switch [member...]` - Registers a switch with the given members. - `pk;switch out` - Registers a 'switch-out' - a switch with no associated members. -- `pk;switch edit ` - Edits the members in the latest switch. +- `pk;switch edit ` - Edits the members in the latest switch. +- `pk;switch add ` - Makes a new switch based off the current switch with the listed members added or removed. - `pk;switch move