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 7b590da3..ef0a0c69 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -3,14 +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/pklib/**' + - 'lib/libpk/**' - 'services/api/**' + - 'services/gateway/**' + - 'services/avatars/**' + - '.github/workflows/rust.yml' + - 'ci/Dockerfile.rust' + - 'ci/rust-docker-target.sh' + - 'Cargo.toml' + - 'Cargo.lock' jobs: deploy: @@ -35,21 +40,15 @@ jobs: with: # https://github.com/docker/build-push-action/issues/378 context: . - file: Dockerfile.rust + file: ci/Dockerfile.rust push: false - cache-from: type=gha - cache-to: type=gha + cache-from: type=registry,ref=ghcr.io/pluralkit/docker-cache:rust + cache-to: type=registry,ref=ghcr.io/pluralkit/docker-cache:rust,mode=max outputs: .docker-bin # add more binaries here - run: | - for binary in "api"; 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 - if [ "${{ github.repository }}" == "PluralKit/PluralKit" ]; then - docker push ghcr.io/pluralkit/$binary:${{ env.BRANCH_NAME }} - docker push ghcr.io/pluralkit/$binary:${{ github.sha }} - [ "${{ env.BRANCH_NAME }}" == "main" ] && docker push ghcr.io/pluralkit/$binary:latest - fi - done + tag=${{ github.sha }} \ + branch=${{ env.BRANCH_NAME }} \ + push=$([ "${{ github.repository }}" == "PluralKit/PluralKit" ] && echo true || echo false) \ + ci/rust-docker-target.sh 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..8a232e04 100644 --- a/.github/workflows/scheduled_tasks.yml +++ b/.github/workflows/scheduled_tasks.yml @@ -2,8 +2,8 @@ name: Build scheduled tasks runner Docker image on: push: - branches: [main] paths: + - .github/workflows/scheduled_tasks.yml - 'services/scheduled_tasks/**' jobs: diff --git a/.gitignore b/.gitignore index 750f2afa..d3ef7ada 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ target/ .idea/ .run/ .vscode/ +.mono/ tags/ .DS_Store mono_crash* diff --git a/Cargo.lock b/Cargo.lock index c2a6e683..de637542 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3,10 +3,31 @@ version = 3 [[package]] -name = "ahash" -version = "0.7.6" +name = "addr2line" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" dependencies = [ "getrandom", "once_cell", @@ -14,14 +35,47 @@ dependencies = [ ] [[package]] -name = "aho-corasick" -version = "0.7.20" +name = "ahash" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] +[[package]] +name = "allocator-api2" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anyhow" version = "1.0.69" @@ -33,50 +87,70 @@ name = "api" version = "0.1.0" dependencies = [ "anyhow", - "axum", + "axum 0.7.5", "fred", - "http", - "hyper-reverse-proxy", + "hyper 1.5.0", + "hyper-util", "lazy_static", "libpk", "metrics", + "prost", + "reverse-proxy-service", + "serde", + "serde_json", + "sqlx", "tokio", "tower", + "tower-http 0.5.2", "tracing", ] [[package]] name = "arc-swap" -version = "1.6.0" +version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bddcadddf5e9015d310179a59bb28c4d4b9920ad0f11e8e14dbadf654890c9a6" - -[[package]] -name = "arcstr" -version = "1.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f907281554a3d0312bb7aab855a8e0ef6cbf1614d06de54105039ca8b34460e" +checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" [[package]] name = "async-trait" -version = "0.1.64" +version = "0.1.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cd7fce9ba8c3c042128ce72d8b2ddbf3a05747efb67ea0313c635e10bda47a2" +checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.66", ] [[package]] -name = "atty" -version = "0.2.14" +name = "atoi" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" dependencies = [ - "hermit-abi 0.1.19", - "libc", - "winapi", + "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]] @@ -86,19 +160,70 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] -name = "axum" -version = "0.6.4" +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 = "e5694b64066a2459918d8074c2ce0d5a88f409431994c2356617c8ae0c4721fc" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fb79c228270dcf2426e74864cabc94babb5dbab01a4314e702d2f16540e1591" dependencies = [ "async-trait", - "axum-core", - "bitflags", + "axum-core 0.3.4", + "bitflags 1.3.2", "bytes", "futures-util", - "http", - "http-body", - "hyper", + "http 0.2.8", + "http-body 0.4.5", + "hyper 0.14.24", "itoa", "matchit", "memchr", @@ -110,29 +235,99 @@ dependencies = [ "serde_json", "serde_path_to_error", "serde_urlencoded", - "sync_wrapper", + "sync_wrapper 0.1.2", "tokio", "tower", - "tower-http", + "tower-http 0.3.5", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a6c9af12842a67734c9a2e355436e5d03b22383ed60cf13cd0c18fbfe3dcbcf" +dependencies = [ + "async-trait", + "axum-core 0.4.3", + "bytes", + "futures-util", + "http 1.1.0", + "http-body 1.0.0", + "http-body-util", + "hyper 1.5.0", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper 1.0.1", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "759fa577a247914fd3f7f76d62972792636412fbfd634cd452f6a385a74d2d2c" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http 0.2.8", + "http-body 0.4.5", + "mime", + "rustversion", "tower-layer", "tower-service", ] [[package]] name = "axum-core" -version = "0.3.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cae3e661676ffbacb30f1a824089a8c9150e71017f7e1e38f2aa32009188d34" +checksum = "a15c63fd72d41492dc4f497196f5da1fb04fb7529e631d73630d1b491e47a2e3" dependencies = [ "async-trait", "bytes", "futures-util", - "http", - "http-body", + "http 1.1.0", + "http-body 1.0.0", + "http-body-util", "mime", + "pin-project-lite", "rustversion", + "sync_wrapper 0.1.2", "tower-layer", "tower-service", + "tracing", +] + +[[package]] +name = "backtrace" +version = "0.3.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide 0.7.4", + "object", + "rustc-demangle", ] [[package]] @@ -141,6 +336,24 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + [[package]] name = "bitflags" version = "1.3.2" @@ -148,12 +361,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] -name = "block-buffer" -version = "0.9.0" +name = "bitflags" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" dependencies = [ - "generic-array", + "serde", ] [[package]] @@ -172,10 +385,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535" [[package]] -name = "bytes" -version = "1.4.0" +name = "bytemuck" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" +checksum = "8334215b81e418a0a7bdb8ef0849474f40bb10c8b71f1c4ed315cff49f32494d" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da" [[package]] name = "bytes-utils" @@ -187,6 +412,17 @@ dependencies = [ "either", ] +[[package]] +name = "cc" +version = "1.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2e7962b54006dcfcc61cb72735f4d89bb97061dd6a7ed882ec6b8ee53714c6f" +dependencies = [ + "jobserver", + "libc", + "shlex", +] + [[package]] name = "cfg-if" version = "1.0.0" @@ -194,30 +430,111 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] -name = "config" -version = "0.13.3" +name = "chrono" +version = "0.4.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d379af7f68bfc21714c6c7dea883544201741d2ce8274bb12fa54f89507f52a7" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-targets 0.52.6", +] + +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "config" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7328b20597b53c2454f0b1919720c25c7339051c02b72b7e05409e00b14132be" dependencies = [ "async-trait", + "convert_case", "json5", "lazy_static", "nom", "pathdiff", "ron", - "rust-ini", + "rust-ini 0.19.0", "serde", "serde_json", "toml", "yaml-rust", ] +[[package]] +name = "const-oid" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" + [[package]] name = "cpufeatures" version = "0.2.5" @@ -227,12 +544,36 @@ dependencies = [ "libc", ] +[[package]] +name = "crc" +version = "3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + [[package]] name = "crc16" 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" @@ -247,14 +588,26 @@ dependencies = [ ] [[package]] -name = "crossbeam-utils" -version = "0.8.15" +name = "crossbeam-queue" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c063cd8cc95f5c377ed0d4b49a4b21f632396ff690e8470c29b3359b346984b" +checksum = "df0346b5d5e76ac2fe4e327c5fd1118d6be7c51dfb18f9b7922923f287471e35" dependencies = [ - "cfg-if", + "crossbeam-utils", ] +[[package]] +name = "crossbeam-utils" +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" @@ -266,22 +619,90 @@ dependencies = [ ] [[package]] -name = "digest" -version = "0.9.0" +name = "dashmap" +version = "5.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +checksum = "907076dfda823b0b36d2a1bb5f90c96660a5bbcd7729e10727f07858f22c4edc" dependencies = [ - "generic-array", + "cfg-if", + "hashbrown 0.12.3", + "lock_api", + "once_cell", + "parking_lot_core", +] + +[[package]] +name = "data-encoding" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" + +[[package]] +name = "der" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" +dependencies = [ + "const-oid", + "pem-rfc7468", + "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.10.6" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer 0.10.3", + "block-buffer", + "const-oid", "crypto-common", + "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" +dependencies = [ + "anyhow", + "axum 0.7.5", + "hickory-client", + "reqwest 0.12.8", + "serde", + "serde_json", + "tokio", + "tracing", + "tracing-subscriber", ] [[package]] @@ -290,34 +711,147 @@ 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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + [[package]] name = "either" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" +dependencies = [ + "serde", +] [[package]] -name = "env_logger" -version = "0.7.1" +name = "encoding_rs" +version = "0.8.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36" +checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" dependencies = [ - "atty", - "humantime", - "log", - "regex", - "termcolor", + "cfg-if", +] + +[[package]] +name = "endian-type" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" + +[[package]] +name = "enum-as-inner" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ffccbb6966c05b32ef8fbac435df276c4ae4d3dc55a8cd0eb9745e6c12f546a" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "syn 2.0.66", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "etcetera" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +dependencies = [ + "cfg-if", + "home", + "windows-sys 0.48.0", +] + +[[package]] +name = "event-listener" +version = "5.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "fastrand" +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" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1267f4ac4f343772758f7b1bdcbe767c218bbab93bb432acbf5162bbf85a6c4" +checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" dependencies = [ "num-traits", ] +[[package]] +name = "flume" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181" +dependencies = [ + "futures-core", + "futures-sink", + "spin 0.9.8", +] + [[package]] name = "fnv" version = "1.0.7" @@ -326,47 +860,58 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "form_urlencoded" -version = "1.1.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" dependencies = [ "percent-encoding", ] [[package]] name = "fred" -version = "5.2.0" +version = "9.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6be0137d9045288f9c0a0659da3b74c196ad0263d2eafa0f5a73785a907bad14" +checksum = "0ac76d6e24da83723c1d118a1d3b794d883eec94715eeaa611698558d5547048" dependencies = [ "arc-swap", - "arcstr", "async-trait", "bytes", "bytes-utils", - "cfg-if", + "crossbeam-queue", "float-cmp", + "fred-macros", "futures", - "lazy_static", "log", - "parking_lot 0.11.2", - "pretty_env_logger", + "parking_lot", "rand", "redis-protocol", "semver", "sha-1", + "socket2 0.5.7", "tokio", "tokio-stream", - "tokio-util 0.6.10", + "tokio-util", "tracing", "url", + "urlencoding", +] + +[[package]] +name = "fred-macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1458c6e22d36d61507034d5afecc64f105c1d39712b7ac6ec3b352c423f715cc" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.66", ] [[package]] name = "futures" -version = "0.3.26" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13e2792b0ff0340399d58445b88fd9770e3489eff258a4cbc1523418f12abf84" +checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" dependencies = [ "futures-channel", "futures-core", @@ -379,9 +924,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.26" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e5317663a9089767a1ec00a487df42e0ca174b61b4483213ac24448e4664df5" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" dependencies = [ "futures-core", "futures-sink", @@ -389,15 +934,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.26" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec90ff4d0fe1f57d600049061dc6bb68ed03c7d2fbd697274c41805dcb3f8608" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" [[package]] name = "futures-executor" -version = "0.3.26" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8de0a35a6ab97ec8869e32a2473f4b1324459e14c29275d14b10cb1fd19b50e" +checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" dependencies = [ "futures-core", "futures-task", @@ -405,39 +950,50 @@ dependencies = [ ] [[package]] -name = "futures-io" -version = "0.3.26" +name = "futures-intrusive" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfb8371b6fb2aeb2d280374607aeabfc99d95c72edfe51692e42d3d7f0d08531" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", +] + +[[package]] +name = "futures-io" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" [[package]] name = "futures-macro" -version = "0.3.26" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95a73af87da33b5acf53acfebdc339fe592ecf5357ac7c0a7734ab9d8c876a70" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.66", ] [[package]] name = "futures-sink" -version = "0.3.26" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f310820bb3e8cfd46c80db4d7fb8353e15dfff853a127158425f31e0be6c8364" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" [[package]] name = "futures-task" -version = "0.3.26" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcf79a1bf610b10f42aea489289c5a2c478a786509693b80cd39c44ccd936366" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" [[package]] name = "futures-util" -version = "0.3.26" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c1d6de3acfef38d2be4b1f543f553131788603495be83da675e180c8d6b7bd1" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" dependencies = [ "futures-channel", "futures-core", @@ -451,6 +1007,32 @@ dependencies = [ "slab", ] +[[package]] +name = "gateway" +version = "0.1.0" +dependencies = [ + "anyhow", + "axum 0.7.5", + "bytes", + "chrono", + "fred", + "futures", + "lazy_static", + "libpk", + "metrics", + "prost", + "serde_json", + "serde_variant", + "signal-hook", + "tokio", + "tracing", + "twilight-cache-inmemory", + "twilight-gateway", + "twilight-http", + "twilight-model", + "twilight-util", +] + [[package]] name = "generic-array" version = "0.14.6" @@ -461,43 +1043,74 @@ dependencies = [ "version_check", ] -[[package]] -name = "gethostname" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a329e22866dd78b35d2c639a4a23d7b950aeae300dfd79f4fb19f74055c2404" -dependencies = [ - "libc", - "windows", -] - [[package]] name = "getrandom" -version = "0.2.8" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi", ] [[package]] -name = "h2" -version = "0.3.15" +name = "gif" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f9f29bc9dda355256b2916cf526ab02ce0aeaaaf2bad60d65ef3f12f11dd0f4" +checksum = "3fb2d69b19215e18bb912fa30f7ce15846e301408695e44e0ef719f1da9e19f2" +dependencies = [ + "color_quant", + "weezl", +] + +[[package]] +name = "gimli" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" + +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + +[[package]] +name = "h2" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" dependencies = [ "bytes", "fnv", "futures-core", "futures-sink", "futures-util", - "http", + "http 0.2.8", "indexmap", "slab", "tokio", - "tokio-util 0.7.6", + "tokio-util", + "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", "tracing", ] @@ -507,18 +1120,46 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" dependencies = [ - "ahash", + "ahash 0.7.8", ] [[package]] -name = "hermit-abi" -version = "0.1.19" +name = "hashbrown" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" dependencies = [ - "libc", + "ahash 0.8.11", + "allocator-api2", ] +[[package]] +name = "hashlink" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" +dependencies = [ + "hashbrown 0.14.5", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "hermit-abi" version = "0.2.6" @@ -529,14 +1170,85 @@ dependencies = [ ] [[package]] -name = "hostname" -version = "0.3.1" +name = "hermit-abi" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hickory-client" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bab9683b08d8f8957a857b0236455d80e1886eaa8c6178af556aa7871fb61b55" dependencies = [ - "libc", - "match_cfg", - "winapi", + "cfg-if", + "data-encoding", + "futures-channel", + "futures-util", + "hickory-proto", + "once_cell", + "radix_trie", + "rand", + "thiserror", + "tokio", + "tracing", +] + +[[package]] +name = "hickory-proto" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07698b8420e2f0d6447a436ba999ec85d8fbf2a398bbd737b82cac4a2e96e512" +dependencies = [ + "async-trait", + "cfg-if", + "data-encoding", + "enum-as-inner", + "futures-channel", + "futures-io", + "futures-util", + "idna 0.4.0", + "ipnet", + "once_cell", + "rand", + "thiserror", + "tinyvec", + "tokio", + "tracing", + "url", +] + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "home" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" +dependencies = [ + "windows-sys 0.52.0", ] [[package]] @@ -550,6 +1262,17 @@ dependencies = [ "itoa", ] +[[package]] +name = "http" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + [[package]] name = "http-body" version = "0.4.5" @@ -557,15 +1280,38 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" dependencies = [ "bytes", - "http", + "http 0.2.8", + "pin-project-lite", +] + +[[package]] +name = "http-body" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643" +dependencies = [ + "bytes", + "http 1.1.0", +] + +[[package]] +name = "http-body-util" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" +dependencies = [ + "bytes", + "futures-util", + "http 1.1.0", + "http-body 1.0.0", "pin-project-lite", ] [[package]] name = "http-range-header" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bfe8eed0a9285ef776bb792479ea3834e8b94e13d615c2f66d03dd50a435a29" +checksum = "add0ab9360ddbd88cfeb3bd9574a1d85cfdfa14db10b3e21d3700dbc4328758f" [[package]] name = "httparse" @@ -579,15 +1325,6 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" -[[package]] -name = "humantime" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f" -dependencies = [ - "quick-error", -] - [[package]] name = "hyper" version = "0.14.24" @@ -598,14 +1335,14 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "h2", - "http", - "http-body", + "h2 0.3.26", + "http 0.2.8", + "http-body 0.4.5", "httparse", "httpdate", "itoa", "pin-project-lite", - "socket2", + "socket2 0.4.7", "tokio", "tower-service", "tracing", @@ -613,43 +1350,162 @@ dependencies = [ ] [[package]] -name = "hyper-reverse-proxy" -version = "0.5.1" +name = "hyper" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc1af9b1b483fb9f33bd1cda26b35eacf902f0d116fcf0d56075ea5e5923b935" +checksum = "bbbff0a806a4728c99295b254c8838933b5b082d75e3cb70c8dab21fdfbcfa9a" dependencies = [ - "hyper", - "lazy_static", - "unicase", + "bytes", + "futures-channel", + "futures-util", + "h2 0.4.6", + "http 1.1.0", + "http-body 1.0.0", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "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.5.0", + "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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333" +dependencies = [ + "futures-util", + "http 1.1.0", + "hyper 1.5.0", + "hyper-util", + "rustls 0.23.10", + "rustls-pki-types", + "tokio", + "tokio-rustls 0.26.0", + "tower-service", + "webpki-roots 0.26.6", +] + +[[package]] +name = "hyper-util" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http 1.1.0", + "http-body 1.0.0", + "hyper 1.5.0", + "pin-project-lite", + "socket2 0.5.7", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", ] [[package]] name = "idna" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" +checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" dependencies = [ "unicode-bidi", "unicode-normalization", ] [[package]] -name = "indexmap" -version = "1.9.2" +name = "idna" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" dependencies = [ - "autocfg", - "hashbrown", + "unicode-bidi", + "unicode-normalization", ] [[package]] -name = "instant" -version = "0.1.12" +name = "image" +version = "0.24.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +checksum = "5690139d2f55868e080017335e4b94cb7414274c74f1669c84fb5feba2c9f69d" dependencies = [ - "cfg-if", + "bytemuck", + "byteorder", + "color_quant", + "gif", + "jpeg-decoder", + "num-traits", + "png", + "tiff", +] + +[[package]] +name = "indexmap" +version = "2.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" +dependencies = [ + "equivalent", + "hashbrown 0.14.5", ] [[package]] @@ -658,6 +1514,15 @@ version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30e22bd8629359895450b59ea7a776c850561b96a3b1d31321c1949d9e6c9146" +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.5" @@ -665,10 +1530,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fad582f4b9e86b6caa621cabeb0963332d92eea04729ab12892c2533951e6440" [[package]] -name = "js-sys" -version = "0.3.61" +name = "jobserver" +version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "445dde2150c55e483f3d8416706b97ec8e8237c307e5b7b4b8dd15e6af2a0730" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" dependencies = [ "wasm-bindgen", ] @@ -689,12 +1569,21 @@ name = "lazy_static" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +dependencies = [ + "spin 0.5.2", +] [[package]] name = "libc" -version = "0.2.139" +version = "0.2.161" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" +checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1" + +[[package]] +name = "libm" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" [[package]] name = "libpk" @@ -702,15 +1591,63 @@ version = "0.1.0" dependencies = [ "anyhow", "config", - "gethostname", + "fred", "lazy_static", "metrics", "metrics-exporter-prometheus", + "prost", + "prost-build", + "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]] +name = "libsqlite3-sys" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +dependencies = [ + "cc", + "pkg-config", + "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]] @@ -719,6 +1656,12 @@ version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" +[[package]] +name = "linux-raw-sys" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" + [[package]] name = "lock_api" version = "0.4.9" @@ -731,27 +1674,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.17" +version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "mach" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b823e83b2affd8f40a9ee8c29dbc56404c1e34cd2710921f2801e2cf29527afa" -dependencies = [ - "libc", -] - -[[package]] -name = "match_cfg" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" [[package]] name = "matchers" @@ -759,7 +1684,7 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" dependencies = [ - "regex-automata", + "regex-automata 0.1.10", ] [[package]] @@ -768,6 +1693,33 @@ 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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + +[[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" @@ -785,58 +1737,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", - "metrics-macros", + "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", + "base64 0.22.1", + "http-body-util", + "hyper 1.5.0", + "hyper-util", "indexmap", "ipnet", "metrics", "metrics-util", - "parking_lot 0.12.1", - "portable-atomic", "quanta", "thiserror", "tokio", "tracing", ] -[[package]] -name = "metrics-macros" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "731f8ecebd9f3a4aa847dfe75455e4757a45da40a7793d2f0b1f9b6ed18b23f3" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[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", + "hashbrown 0.14.5", "metrics", "num_cpus", - "parking_lot 0.12.1", - "portable-atomic", "quanta", "sketches-ddsketch", ] @@ -854,15 +1793,49 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] -name = "mio" -version = "0.8.5" +name = "miniz_oxide" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5d732bc30207a6423068df043e3d02e0735b155ad7ce1a6f76fe2baa5b158de" +checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" 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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" +dependencies = [ + "hermit-abi 0.3.9", "libc", - "log", - "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.42.0", + "wasi", + "windows-sys 0.52.0", +] + +[[package]] +name = "multimap" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "defc4c55412d89136f966bbb339008b474350e5e6e78d2714439c386b3137a03" + +[[package]] +name = "nibble_vec" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43" +dependencies = [ + "smallvec", ] [[package]] @@ -885,6 +1858,49 @@ dependencies = [ "winapi", ] +[[package]] +name = "num-bigint-dig" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" +dependencies = [ + "byteorder", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand", + "smallvec", + "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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.15" @@ -892,6 +1908,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -905,16 +1922,34 @@ dependencies = [ ] [[package]] -name = "once_cell" -version = "1.17.0" +name = "object" +version = "0.36.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66" +checksum = "27b64972346851a39438c60b341ebc01bba47464ae329e55cf343eb93964efd9" +dependencies = [ + "memchr", +] [[package]] -name = "opaque-debug" -version = "0.3.0" +name = "once_cell" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[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" @@ -922,8 +1957,18 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccd746e37177e1711c20dd619a1620f34f5c8b569c53590a72dedd5344d8924a" dependencies = [ - "dlv-list", - "hashbrown", + "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]] @@ -933,15 +1978,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" [[package]] -name = "parking_lot" -version = "0.11.2" +name = "parking" +version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" -dependencies = [ - "instant", - "lock_api", - "parking_lot_core 0.8.6", -] +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" [[package]] name = "parking_lot" @@ -950,21 +1990,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" dependencies = [ "lock_api", - "parking_lot_core 0.9.7", -] - -[[package]] -name = "parking_lot_core" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc" -dependencies = [ - "cfg-if", - "instant", - "libc", - "redox_syscall", - "smallvec", - "winapi", + "parking_lot_core", ] [[package]] @@ -975,11 +2001,17 @@ checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.2.16", "smallvec", "windows-sys 0.45.0", ] +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + [[package]] name = "pathdiff" version = "0.2.1" @@ -987,10 +2019,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" [[package]] -name = "percent-encoding" -version = "2.2.0" +name = "pem-rfc7468" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pest" @@ -1022,7 +2063,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn", + "syn 1.0.107", ] [[package]] @@ -1036,6 +2077,16 @@ dependencies = [ "sha2", ] +[[package]] +name = "petgraph" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" +dependencies = [ + "fixedbitset", + "indexmap", +] + [[package]] name = "pin-project" version = "1.0.12" @@ -1053,14 +2104,14 @@ checksum = "069bdb1e05adc7a8990dce9cc75370895fbe4e3d58b9b73bf1aee56359344a55" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.107", ] [[package]] name = "pin-project-lite" -version = "0.2.9" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" [[package]] name = "pin-utils" @@ -1069,10 +2120,56 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] -name = "portable-atomic" -version = "0.3.19" +name = "pkcs1" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26f6a7b87c2e435a3241addceeeff740ff8b7e76b74c13bf9acb17fa454ea00b" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pkg-config" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" + +[[package]] +name = "png" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52f9d46a34a05a6a57566bc2bfae066ef07585a6e3fa30fbbdff5936380623f0" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide 0.8.0", +] + +[[package]] +name = "portable-atomic" +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" @@ -1081,55 +2178,169 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] -name = "pretty_env_logger" -version = "0.4.0" +name = "prettyplease" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "926d36b9553851b8b0005f1275891b392ee4d2d833852c417ed025477350fb9d" +checksum = "5f12335488a2f3b0a83b14edad48dca9879ce89b2edd10e80237e4e852dd645e" dependencies = [ - "env_logger", - "log", + "proc-macro2", + "syn 2.0.66", ] [[package]] name = "proc-macro2" -version = "1.0.51" +version = "1.0.84" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d727cae5b39d21da60fa540906919ad737832fe0b1c165da3a34d6548c849d6" +checksum = "ec96c6a92621310b51366f1e28d05ef11489516e93be030060e5fc12024a49d6" dependencies = [ "unicode-ident", ] [[package]] -name = "quanta" -version = "0.10.1" +name = "prost" +version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7e31331286705f455e56cca62e0e717158474ff02b7936c1fa596d983f4ae27" +checksum = "deb1435c188b76130da55f17a466d252ff7b1418b2ad3e037d127b94e3411f29" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-build" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22505a5c94da8e3b7c2996394d1c933236c4d743e81a410bcca4e6989fc066a4" +dependencies = [ + "bytes", + "heck 0.5.0", + "itertools", + "log", + "multimap", + "once_cell", + "petgraph", + "prettyplease", + "prost", + "prost-types", + "regex", + "syn 2.0.66", + "tempfile", +] + +[[package]] +name = "prost-derive" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81bddcdb20abf9501610992b6759a4c888aef7d1a7247ef75e2404275ac24af1" +dependencies = [ + "anyhow", + "itertools", + "proc-macro2", + "quote", + "syn 2.0.66", +] + +[[package]] +name = "prost-types" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9091c90b0a32608e984ff2fa4091273cbdd755d54935c51d520887f4a1dbd5b0" +dependencies = [ + "prost", +] + +[[package]] +name = "quanta" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5167a477619228a0b284fac2674e3c388cba90631d7b7de620e6f1fcd08da5" dependencies = [ "crossbeam-utils", "libc", - "mach", "once_cell", "raw-cpuid", - "wasi 0.10.2+wasi-snapshot-preview1", + "wasi", "web-sys", "winapi", ] [[package]] -name = "quick-error" -version = "1.2.3" +name = "quick-xml" +version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" +checksum = "7f50b1c63b38611e7d4d7f68b82d3ad0cc71a2ad2e7f61fc10f1328d917c93cd" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "quinn" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c7c5fdde3cdae7203427dc4f0a68fe0ed09833edc525a03456b153b79828684" +dependencies = [ + "bytes", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls 0.23.10", + "socket2 0.5.7", + "thiserror", + "tokio", + "tracing", +] + +[[package]] +name = "quinn-proto" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fadfaed2cd7f389d0161bb73eeb07b7b78f8691047a6f3e73caaeae55310a4a6" +dependencies = [ + "bytes", + "rand", + "ring 0.17.8", + "rustc-hash", + "rustls 0.23.10", + "slab", + "thiserror", + "tinyvec", + "tracing", +] + +[[package]] +name = "quinn-udp" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fe68c2e9e1a1234e218683dbdf9f9dfcb094113c5ac2b938dfcb9bab4c4140b" +dependencies = [ + "libc", + "once_cell", + "socket2 0.5.7", + "tracing", + "windows-sys 0.59.0", +] [[package]] name = "quote" -version = "1.0.23" +version = "1.0.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" dependencies = [ "proc-macro2", ] +[[package]] +name = "radix_trie" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c069c179fcdc6a2fe24d8d18305cf085fdbd4f922c041943e203685d6a1c58fd" +dependencies = [ + "endian-type", + "nibble_vec", +] + [[package]] name = "rand" version = "0.8.5" @@ -1162,18 +2373,18 @@ 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", + "bitflags 2.5.0", ] [[package]] name = "redis-protocol" -version = "4.1.0" +version = "5.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c31deddf734dc0a39d3112e73490e88b61a05e83e074d211f348404cee4d2c6" +checksum = "65deb7c9501fbb2b6f812a30d59c0253779480853545153a51d8e9e444ddc99f" dependencies = [ "bytes", "bytes-utils", @@ -1189,18 +2400,39 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" dependencies = [ - "bitflags", + "bitflags 1.3.2", +] + +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +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.7.1" +version = "1.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48aaa5748ba571fb95cd2c85c09f629215d3a6ece942baa100950af03a34f733" +checksum = "12de2eff854e5fa4b1295edd650e227e9d8fb0c9e90b12e7f36d6a6811791a29" dependencies = [ "aho-corasick", "memchr", - "regex-syntax", + "regex-automata 0.3.7", + "regex-syntax 0.7.5", ] [[package]] @@ -1209,7 +2441,18 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" dependencies = [ - "regex-syntax", + "regex-syntax 0.6.28", +] + +[[package]] +name = "regex-automata" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49530408a136e16e5b486e883fbb6ba058e8e4e8ae6621a77b048b314336e629" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.7.5", ] [[package]] @@ -1219,14 +2462,170 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" [[package]] -name = "ron" -version = "0.7.1" +name = "regex-syntax" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88073939a61e5b7680558e6be56b419e208420c2adb92be54921fa6b72283f1a" +checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" + +[[package]] +name = "reqwest" +version = "0.11.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" dependencies = [ - "base64", - "bitflags", + "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", + "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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f713147fbe92361e52392c73b8c9e48c04c6625bce969ef54dc901e58e042a7b" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "futures-util", + "http 1.1.0", + "http-body 1.0.0", + "http-body-util", + "hyper 1.5.0", + "hyper-rustls 0.27.3", + "hyper-util", + "ipnet", + "js-sys", + "log", + "mime", + "once_cell", + "percent-encoding", + "pin-project-lite", + "quinn", + "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 0.26.0", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots 0.26.6", + "windows-registry", +] + +[[package]] +name = "reverse-proxy-service" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7c5828ba3be8842e97b1c96b9df9449b326e5d252c5fb76d73605288b326b75" +dependencies = [ + "axum 0.6.7", + "http 0.2.8", + "hyper 0.14.24", + "log", + "regex", + "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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +dependencies = [ + "cc", + "cfg-if", + "getrandom", + "libc", + "spin 0.9.8", + "untrusted 0.9.0", + "windows-sys 0.52.0", +] + +[[package]] +name = "ron" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94" +dependencies = [ + "base64 0.21.7", + "bitflags 2.5.0", + "serde", + "serde_derive", +] + +[[package]] +name = "rsa" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e5124fcb30e76a7e79bfee683a2746db83784b86289f6251b54b7950a0dfc" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core", + "signature", + "spki", + "subtle", + "zeroize", ] [[package]] @@ -1236,7 +2635,184 @@ 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]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustc-hash" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152" + +[[package]] +name = "rustix" +version = "0.38.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" +dependencies = [ + "bitflags 2.5.0", + "errno", + "libc", + "linux-raw-sys", + "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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05cff451f60db80f490f3c182b77c35260baace73209e9cdbbe526bfe3a4d402" +dependencies = [ + "once_cell", + "ring 0.17.8", + "rustls-pki-types", + "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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d" +dependencies = [ + "base64 0.22.1", + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +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 0.17.8", + "rustls-pki-types", + "untrusted 0.9.0", ] [[package]] @@ -1251,12 +2827,54 @@ 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" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" +[[package]] +name = "sct" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +dependencies = [ + "ring 0.17.8", + "untrusted 0.9.0", +] + +[[package]] +name = "security-framework" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c627723fd09706bacdb5cf41499e95098555af3c3c29d014dc3c458ef6be11c0" +dependencies = [ + "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]] name = "semver" version = "1.0.16" @@ -1265,29 +2883,39 @@ checksum = "58bc9567378fc7690d6b2addae4e60ac2eeea07becb2c64b9f218b53865cba2a" [[package]] name = "serde" -version = "1.0.152" +version = "1.0.203" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb" +checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094" dependencies = [ "serde_derive", ] [[package]] -name = "serde_derive" -version = "1.0.152" +name = "serde-value" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e" +checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c" +dependencies = [ + "ordered-float", + "serde", +] + +[[package]] +name = "serde_derive" +version = "1.0.203" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.66", ] [[package]] name = "serde_json" -version = "1.0.93" +version = "1.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cad406b69c91885b5107daf2c29572f6c8cdb3c66826821e286c533490c0bc76" +checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3" dependencies = [ "itoa", "ryu", @@ -1303,6 +2931,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" @@ -1316,27 +2964,51 @@ dependencies = [ ] [[package]] -name = "sha-1" -version = "0.9.8" +name = "serde_variant" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99cd6713db3cf16b6c84e06321e049a9b9f699826e16096d23bbcc44d15d51a6" +checksum = "0a0068df419f9d9b6488fdded3f1c818522cdea328e02ce9d9f147380265a432" dependencies = [ - "block-buffer 0.9.0", - "cfg-if", - "cpufeatures", - "digest 0.9.0", - "opaque-debug", + "serde", ] [[package]] -name = "sha2" -version = "0.10.6" +name = "sha-1" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0" +checksum = "f5058ada175748e33390e40e872bd0fe59a19f265d0158daa551c5a88a76009c" dependencies = [ "cfg-if", "cpufeatures", - "digest 0.10.6", + "digest", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", ] [[package]] @@ -1348,6 +3020,22 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[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" @@ -1357,6 +3045,28 @@ dependencies = [ "libc", ] +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core", +] + +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +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" version = "0.2.0" @@ -1374,9 +3084,12 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.10.0" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +dependencies = [ + "serde", +] [[package]] name = "socket2" @@ -1388,6 +3101,270 @@ dependencies = [ "winapi", ] +[[package]] +name = "socket2" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "sqlformat" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f895e3734318cc55f1fe66258926c9b910c124d47520339efecbb6c59cec7c1f" +dependencies = [ + "nom", + "unicode_categories", +] + +[[package]] +name = "sqlx" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93334716a037193fac19df402f8571269c84a00852f6a7066b5d2616dcd64d3e" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-core" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4d8060b456358185f7d50c55d9b5066ad956956fddec42ee2e8567134a8936e" +dependencies = [ + "atoi", + "byteorder", + "bytes", + "crc", + "crossbeam-queue", + "either", + "event-listener", + "futures-channel", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashbrown 0.14.5", + "hashlink", + "hex", + "indexmap", + "log", + "memchr", + "once_cell", + "paste", + "percent-encoding", + "serde", + "serde_json", + "sha2", + "smallvec", + "sqlformat", + "thiserror", + "time", + "tokio", + "tokio-stream", + "tracing", + "url", + "uuid", +] + +[[package]] +name = "sqlx-macros" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cac0692bcc9de3b073e8d747391827297e075c7710ff6276d9f7a1f3d58c6657" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn 2.0.66", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1804e8a7c7865599c9c79be146dc8a9fd8cc86935fa641d3ea58e5f0688abaa5" +dependencies = [ + "dotenvy", + "either", + "heck 0.5.0", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2", + "sqlx-core", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", + "syn 2.0.66", + "tempfile", + "tokio", + "url", +] + +[[package]] +name = "sqlx-mysql" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64bb4714269afa44aef2755150a0fc19d756fb580a67db8885608cf02f47d06a" +dependencies = [ + "atoi", + "base64 0.22.1", + "bitflags 2.5.0", + "byteorder", + "bytes", + "crc", + "digest", + "dotenvy", + "either", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "generic-array", + "hex", + "hkdf", + "hmac", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "percent-encoding", + "rand", + "rsa", + "serde", + "sha1", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "time", + "tracing", + "uuid", + "whoami", +] + +[[package]] +name = "sqlx-postgres" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fa91a732d854c5d7726349bb4bb879bb9478993ceb764247660aee25f67c2f8" +dependencies = [ + "atoi", + "base64 0.22.1", + "bitflags 2.5.0", + "byteorder", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "hex", + "hkdf", + "hmac", + "home", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "rand", + "serde", + "serde_json", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "time", + "tracing", + "uuid", + "whoami", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5b2cf34a45953bfd3daaf3db0f7a7878ab9b7a6b91b422d24a7a9e4c857b680" +dependencies = [ + "atoi", + "flume", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "serde_urlencoded", + "sqlx-core", + "time", + "tracing", + "url", + "uuid", +] + +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + +[[package]] +name = "subtle" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" + [[package]] name = "syn" version = "1.0.107" @@ -1399,6 +3376,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn" +version = "2.0.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "sync_wrapper" version = "0.1.2" @@ -1406,32 +3394,65 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" [[package]] -name = "termcolor" -version = "1.2.0" +name = "sync_wrapper" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" +checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" dependencies = [ - "winapi-util", + "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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" +dependencies = [ + "cfg-if", + "fastrand", + "rustix", + "windows-sys 0.52.0", ] [[package]] name = "thiserror" -version = "1.0.38" +version = "1.0.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a9cd18aa97d5c45c6603caea1da6628790b37f7a34b6ca89522331c5180fed0" +checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.38" +version = "1.0.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f" +checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.66", ] [[package]] @@ -1443,6 +3464,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" @@ -1460,40 +3532,70 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.25.0" +version = "1.39.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8e00990ebabbe4c14c08aca901caed183ecd5c09562a12c824bb53d3c3fd3af" +checksum = "9babc99b9923bfa4804bd74722ff02c0381021eafa4db9949217e3be8e84fff5" dependencies = [ - "autocfg", + "backtrace", "bytes", "libc", - "memchr", "mio", - "num_cpus", - "parking_lot 0.12.1", + "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2", + "socket2 0.5.7", "tokio-macros", - "windows-sys 0.42.0", + "windows-sys 0.52.0", ] [[package]] name = "tokio-macros" -version = "1.8.2" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d266c00fde287f55d3f1c3e96c500c362a2b8c695076ec180f27918820bc6df8" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", - "syn", + "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 0.23.10", + "rustls-pki-types", + "tokio", ] [[package]] name = "tokio-stream" -version = "0.1.11" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d660770404473ccd7bc9f8b28494a811bc18542b915c0855c51e8f419d5223ce" +checksum = "4f4e6ce100d0eb49a2734f8c0812bcd324cf357d21810932c5df6b96ef2b86f1" dependencies = [ "futures-core", "pin-project-lite", @@ -1502,39 +3604,73 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.6.10" +version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36943ee01a6d67977dd3f84a5a1d2efeb4ada3a1ae771cadfaa535d9d9fc6507" +checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a" dependencies = [ "bytes", "futures-core", "futures-sink", - "log", "pin-project-lite", "tokio", ] [[package]] -name = "tokio-util" -version = "0.7.6" +name = "tokio-websockets" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc6a3b08b64e6dfad376fa2432c7b1f01522e37a623c3050bc95db2d3ff21583" +checksum = "988c6e20955aa5043e0822cb27093ebaabb430a126cda0223824b6d65ea900c1" dependencies = [ + "base64 0.21.7", "bytes", + "fastrand", "futures-core", "futures-sink", - "pin-project-lite", + "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", "tracing", ] [[package]] name = "toml" -version = "0.5.11" +version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +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]] @@ -1559,12 +3695,12 @@ version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f873044bf02dd1e8239e9c1293ea39dad76dc594ec16185d0a1bf31d8dc8d858" dependencies = [ - "bitflags", + "bitflags 1.3.2", "bytes", "futures-core", "futures-util", - "http", - "http-body", + "http 0.2.8", + "http-body 0.4.5", "http-range-header", "pin-project-lite", "tower", @@ -1572,6 +3708,24 @@ dependencies = [ "tower-service", ] +[[package]] +name = "tower-http" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" +dependencies = [ + "bitflags 2.5.0", + "bytes", + "futures-util", + "http 1.1.0", + "http-body 1.0.0", + "http-body-util", + "pin-project-lite", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "tower-layer" version = "0.3.2" @@ -1586,11 +3740,10 @@ checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" [[package]] name = "tracing" -version = "0.1.37" +version = "0.1.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ - "cfg-if", "log", "pin-project-lite", "tracing-attributes", @@ -1599,81 +3752,65 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.23" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.66", ] [[package]] name = "tracing-core" -version = "0.1.30" +version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" dependencies = [ "once_cell", "valuable", ] -[[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.6", - "tracing-core", - "tracing-futures", - "tracing-subscriber", -] - [[package]] name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-serde" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922" +checksum = "bc6b213177105856957181934e4920de57730fc69bf42c37ee5bb664d406d9e1" dependencies = [ - "lazy_static", - "log", + "serde", "tracing-core", ] [[package]] name = "tracing-subscriber" -version = "0.3.16" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6176eae26dd70d0c919749377897b54a9276bd7061339665dd68777926b5a70" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" dependencies = [ "matchers", "nu-ansi-term", "once_cell", "regex", + "serde", + "serde_json", "sharded-slab", "smallvec", "thread_local", "tracing", "tracing-core", "tracing-log", + "tracing-serde", ] [[package]] @@ -1682,6 +3819,106 @@ 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#b346757b3054d461f5652b5ba0148a70eda5697e" +dependencies = [ + "bitflags 2.5.0", + "dashmap", + "serde", + "tracing", + "twilight-model", + "twilight-util", +] + +[[package]] +name = "twilight-gateway" +version = "0.16.0-rc.1" +source = "git+https://github.com/pluralkit/twilight#b346757b3054d461f5652b5ba0148a70eda5697e" +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#b346757b3054d461f5652b5ba0148a70eda5697e" +dependencies = [ + "tokio", + "tracing", +] + +[[package]] +name = "twilight-http" +version = "0.16.0-rc.1" +source = "git+https://github.com/pluralkit/twilight#b346757b3054d461f5652b5ba0148a70eda5697e" +dependencies = [ + "fastrand", + "http 1.1.0", + "http-body-util", + "hyper 1.5.0", + "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#b346757b3054d461f5652b5ba0148a70eda5697e" +dependencies = [ + "tokio", + "tracing", +] + +[[package]] +name = "twilight-model" +version = "0.16.0-rc.1" +source = "git+https://github.com/pluralkit/twilight#b346757b3054d461f5652b5ba0148a70eda5697e" +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#b346757b3054d461f5652b5ba0148a70eda5697e" +dependencies = [ + "twilight-model", +] + +[[package]] +name = "twilight-validate" +version = "0.16.0-rc.1" +source = "git+https://github.com/pluralkit/twilight#b346757b3054d461f5652b5ba0148a70eda5697e" +dependencies = [ + "twilight-model", +] + [[package]] name = "typenum" version = "1.16.0" @@ -1694,15 +3931,6 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e79c4d996edb816c91e4308506774452e55e95c3c9de07b6729e17e15a5ef81" -[[package]] -name = "unicase" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" -dependencies = [ - "version_check", -] - [[package]] name = "unicode-bidi" version = "0.3.10" @@ -1725,22 +3953,73 @@ dependencies = [ ] [[package]] -name = "url" -version = "2.3.1" +name = "unicode-properties" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" +checksum = "e4259d9d4425d9f0661581b804cb85fe66a4c631cadd8f490d1c13a35d5d9291" + +[[package]] +name = "unicode-segmentation" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" + +[[package]] +name = "unicode_categories" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" dependencies = [ "form_urlencoded", - "idna", + "idna 0.5.0", "percent-encoding", ] +[[package]] +name = "urlencoding" +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" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version_check" version = "0.9.4" @@ -1757,12 +4036,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" @@ -1770,10 +4043,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] -name = "wasm-bindgen" -version = "0.2.84" +name = "wasite" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31f8dcbc21f30d9b8f2ea926ecb58f6b91192c17e9d33594b3df58b2007ca53b" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -1781,24 +4060,36 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.84" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95ce90fd5bcc06af55a641a86428ee4229e44e07033963a2290a8e241607ccb9" +checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn", + "syn 2.0.66", "wasm-bindgen-shared", ] [[package]] -name = "wasm-bindgen-macro" -version = "0.2.84" +name = "wasm-bindgen-futures" +version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c21f77c0bedc37fd5dc21f897894a5ca01e7bb159884559461862ae90c0b4c5" +checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1806,33 +4097,106 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.84" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6" +checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.66", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.84" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d" +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.61" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e33b99f4b23ba3eec1a53ac264e35a755f00e966e0065077d6027c0f575b0b97" +checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" dependencies = [ "js-sys", "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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841c67bff177718f1d4dfefde8d8f0e78f9b6589319ba88312f567fc5841a958" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "weezl" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082" + +[[package]] +name = "whoami" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a44ab49fad634e88f55bf8f9bb3abd2f27d7204172a112c7c9987e01c1c94ea9" +dependencies = [ + "redox_syscall 0.4.1", + "wasite", +] + [[package]] name = "winapi" version = "0.3.9" @@ -1849,15 +4213,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" -[[package]] -name = "winapi-util" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" -dependencies = [ - "winapi", -] - [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" @@ -1865,33 +4220,42 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] -name = "windows" -version = "0.43.0" +name = "windows-core" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04662ed0e3e5630dfa9b26e4cb823b817f1a9addda855d973a9458c236556244" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows-targets 0.52.6", ] [[package]] -name = "windows-sys" -version = "0.42.0" +name = "windows-registry" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" +checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows-result", + "windows-strings", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result", + "windows-targets 0.52.6", ] [[package]] @@ -1900,7 +4264,34 @@ version = "0.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" dependencies = [ - "windows-targets", + "windows-targets 0.42.1", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", ] [[package]] @@ -1909,13 +4300,44 @@ version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e2522491fbfcd58cc84d47aeb2958948c4b8982e9a2d8a2a35bbaed431390e7" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.42.1", + "windows_aarch64_msvc 0.42.1", + "windows_i686_gnu 0.42.1", + "windows_i686_msvc 0.42.1", + "windows_x86_64_gnu 0.42.1", + "windows_x86_64_gnullvm 0.42.1", + "windows_x86_64_msvc 0.42.1", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", ] [[package]] @@ -1924,42 +4346,151 @@ version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + [[package]] name = "windows_aarch64_msvc" version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7" +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + [[package]] name = "windows_i686_gnu" version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640" +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + [[package]] name = "windows_i686_msvc" version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605" +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + [[package]] name = "windows_x86_64_gnu" version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45" +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + [[package]] name = "windows_x86_64_gnullvm" version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + [[package]] name = "windows_x86_64_msvc" version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd" +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +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" @@ -1968,3 +4499,29 @@ checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" dependencies = [ "linked-hash-map", ] + +[[package]] +name = "zerocopy" +version = "0.7.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae87e3fcd617500e5d106f0380cf7b77f3c6092aae37191433159dda23cfb087" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.66", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" diff --git a/Cargo.toml b/Cargo.toml index 2d00f8ed..314c0cee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,14 +1,45 @@ [workspace] members = [ "./lib/libpk", - "./services/api" + "./services/api", + "./services/dispatch", + "./services/gateway", + "./services/avatars" ] [workspace.dependencies] anyhow = "1" -fred = { version = "5.2.0", default-features = false, features = ["tracing", "pool-prefer-active"] } +axum = "0.7.5" +axum-macros = "0.4.1" +bytes = "1.6.0" +chrono = "0.4" +fred = { version = "9.3.0", default-features = false, features = ["tracing", "i-keys", "i-hashes", "i-scripts", "sha-1"] } +futures = "0.3.30" lazy_static = "1.4.0" -metrics = "0.20.1" -serde = "1.0.152" -tokio = { version = "1.25.0", features = ["full"] } -tracing = "0.1.37" +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" +signal-hook = "0.3.17" +sqlx = { version = "0.8.2", 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" +prost-build = "0.12" diff --git a/Dockerfile.bin b/Dockerfile.bin deleted file mode 100644 index f12cd703..00000000 --- a/Dockerfile.bin +++ /dev/null @@ -1,5 +0,0 @@ -FROM alpine:latest - -COPY /.docker-bin/__BINARY__ /bin/__BINARY__ - -CMD ["/bin/__BINARY__"] diff --git a/Dockerfile.rust b/Dockerfile.rust deleted file mode 100644 index 092cd9cb..00000000 --- a/Dockerfile.rust +++ /dev/null @@ -1,34 +0,0 @@ -FROM alpine:latest AS builder - -WORKDIR /build - -RUN apk add rustup build-base -# todo: arm64 target -RUN rustup-init --default-host x86_64-unknown-linux-musl --default-toolchain stable --profile default -y - -ENV PATH=/root/.cargo/bin:$PATH -ENV RUSTFLAGS='-C link-arg=-s' - -RUN cargo install cargo-chef --locked - -# build dependencies first to cache -FROM builder AS recipe-builder -COPY . . -RUN cargo chef prepare --recipe-path recipe.json - -FROM builder AS binary-builder -COPY --from=recipe-builder /build/recipe.json recipe.json -RUN cargo chef cook --release --recipe-path recipe.json --target x86_64-unknown-linux-musl - -COPY Cargo.toml /build/ -COPY Cargo.lock /build/ - -# this needs to match workspaces in Cargo.toml -COPY lib/libpk /build/lib/libpk -COPY services/api/ /build/services/api - -RUN cargo build --bin api --release --target x86_64-unknown-linux-musl - -FROM scratch - -COPY --from=binary-builder /build/target/x86_64-unknown-linux-musl/release/api /api diff --git a/Myriad/Cache/DiscordCacheExtensions.cs b/Myriad/Cache/DiscordCacheExtensions.cs index eb1ff0c2..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 PermissionsIn(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.PermissionsFor(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..e6aac7ee --- /dev/null +++ b/Myriad/Cache/HTTPDiscordCache.cs @@ -0,0 +1,188 @@ +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; + // todo: there should not be infra-specific code here + if (cluster.Contains(".service.consul") || cluster.Contains("process.pluralkit-gateway.internal")) + // 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..84045784 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,14 @@ 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.TryGetChannel(guildId, channel.ParentId!.Value); + if (parent == null) throw new Exception($"failed to find parent channel for thread {channelOrThread} in cache"); return parent; } } \ No newline at end of file diff --git a/Myriad/Extensions/PermissionExtensions.cs b/Myriad/Extensions/PermissionExtensions.cs index 48fc9718..c55a73d3 100644 --- a/Myriad/Extensions/PermissionExtensions.cs +++ b/Myriad/Extensions/PermissionExtensions.cs @@ -31,31 +31,27 @@ public static class PermissionExtensions PermissionSet.AttachFiles | PermissionSet.EmbedLinks; - public static Task PermissionsFor(this IDiscordCache cache, MessageCreateEvent message) => - PermissionsFor(cache, message.ChannelId, message.Author.Id, message.Member, message.WebhookId != null); + public static Task PermissionsForMCE(this IDiscordCache cache, MessageCreateEvent message) => + PermissionsFor2(cache, message.GuildId ?? 0, message.ChannelId, message.Author.Id, message.Member, message.WebhookId != null); public static Task - PermissionsFor(this IDiscordCache cache, ulong channelId, GuildMember member) => - PermissionsFor(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 PermissionsFor(this IDiscordCache cache, ulong channelId, ulong userId, - GuildMemberPartial? member, bool isWebhook = false, - bool isThread = false) + 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); - if (isWebhook) - return EveryonePermissions(guild); - return PermissionsFor(guild, rootChannel, userId, member, isThread: isThread); } @@ -79,9 +75,6 @@ public static class PermissionExtensions return perms; } - public static PermissionSet PermissionsFor(Guild guild, Channel channel, MessageCreateEvent msg, bool isThread = false) => - PermissionsFor(guild, channel, msg.Author.Id, msg.Member, isThread: isThread); - public static PermissionSet PermissionsFor(Guild guild, Channel channel, ulong userId, GuildMemberPartial? member, bool isThread = false) { 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/DiscordApiClient.cs b/Myriad/Rest/DiscordApiClient.cs index db0c64d4..c413f5bd 100644 --- a/Myriad/Rest/DiscordApiClient.cs +++ b/Myriad/Rest/DiscordApiClient.cs @@ -159,6 +159,9 @@ public class DiscordApiClient public Task CreateDm(ulong recipientId) => _client.Post("/users/@me/channels", ("CreateDM", default), new CreateDmRequest(recipientId))!; + public Task RefreshUrls(string[] urls) => + _client.Post("/attachments/refresh-urls", ("RefreshUrls", default), new RefreshUrlsRequest(urls)); + private static string EncodeEmoji(Emoji emoji) => WebUtility.UrlEncode(emoji.Id != null ? $"{emoji.Name}:{emoji.Id}" : emoji.Name) ?? throw new ArgumentException("Could not encode emoji"); 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/Rest/Types/Requests/RefreshUrlsRequest.cs b/Myriad/Rest/Types/Requests/RefreshUrlsRequest.cs new file mode 100644 index 00000000..5f69bc6e --- /dev/null +++ b/Myriad/Rest/Types/Requests/RefreshUrlsRequest.cs @@ -0,0 +1,3 @@ +namespace Myriad.Rest.Types.Requests; + +public record RefreshUrlsRequest(string[] AttachmentUrls); \ No newline at end of file diff --git a/Myriad/Rest/Types/Requests/WebhookMessageEditRequest.cs b/Myriad/Rest/Types/Requests/WebhookMessageEditRequest.cs index e1765a6f..512464d3 100644 --- a/Myriad/Rest/Types/Requests/WebhookMessageEditRequest.cs +++ b/Myriad/Rest/Types/Requests/WebhookMessageEditRequest.cs @@ -15,4 +15,7 @@ public record WebhookMessageEditRequest [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public Optional Embeds { get; init; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public Optional Attachments { get; init; } } \ No newline at end of file diff --git a/Myriad/Types/Activity.cs b/Myriad/Types/Activity.cs index ce91c688..34a2e5e2 100644 --- a/Myriad/Types/Activity.cs +++ b/Myriad/Types/Activity.cs @@ -3,6 +3,7 @@ namespace Myriad.Types; public record Activity { public string Name { get; init; } + public string State { get; init; } public ActivityType Type { get; init; } public string? Url { get; init; } } 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/Myriad/Types/RefreshedUrl.cs b/Myriad/Types/RefreshedUrl.cs new file mode 100644 index 00000000..033f1942 --- /dev/null +++ b/Myriad/Types/RefreshedUrl.cs @@ -0,0 +1,11 @@ +namespace Myriad.Types; + +public record RefreshedUrlsResponse +{ + public record RefreshedUrl + { + public string Original; + public string Refreshed; + } + public RefreshedUrl[] RefreshedUrls; +} \ No newline at end of file diff --git a/PluralKit.API/ApiConfig.cs b/PluralKit.API/ApiConfig.cs index b3946168..fc34d515 100644 --- a/PluralKit.API/ApiConfig.cs +++ b/PluralKit.API/ApiConfig.cs @@ -5,4 +5,5 @@ public class ApiConfig public int Port { get; set; } = 5000; public string? ClientId { get; set; } public string? ClientSecret { get; set; } + public bool TrustAuth { get; set; } = false; } \ No newline at end of file diff --git a/PluralKit.API/AuthorizationTokenHandlerMiddleware.cs b/PluralKit.API/AuthorizationTokenHandlerMiddleware.cs index 3de763e0..a09c869e 100644 --- a/PluralKit.API/AuthorizationTokenHandlerMiddleware.cs +++ b/PluralKit.API/AuthorizationTokenHandlerMiddleware.cs @@ -13,19 +13,13 @@ public class AuthorizationTokenHandlerMiddleware _next = next; } - public async Task Invoke(HttpContext ctx, IDatabase db) + public async Task Invoke(HttpContext ctx, IDatabase db, ApiConfig cfg) { - ctx.Request.Headers.TryGetValue("authorization", out var authHeaders); - if (authHeaders.Count > 0) - { - var systemId = await db.Execute(conn => conn.QuerySingleOrDefaultAsync( - "select id from systems where token = @token", - new { token = authHeaders[0] } - )); - - if (systemId != null) - ctx.Items.Add("SystemId", systemId); - } + if (cfg.TrustAuth + && ctx.Request.Headers.TryGetValue("X-PluralKit-SystemId", out var sidHeaders) + && sidHeaders.Count > 0 + && int.TryParse(sidHeaders[0], out var systemId)) + ctx.Items.Add("SystemId", new SystemId(systemId)); await _next.Invoke(ctx); } diff --git a/PluralKit.API/Controllers/PKControllerBase.cs b/PluralKit.API/Controllers/PKControllerBase.cs index e20aeed5..3698377c 100644 --- a/PluralKit.API/Controllers/PKControllerBase.cs +++ b/PluralKit.API/Controllers/PKControllerBase.cs @@ -9,7 +9,6 @@ namespace PluralKit.API; public class PKControllerBase: ControllerBase { private readonly Guid _requestId = Guid.NewGuid(); - private readonly Regex _shortIdRegex = new("^[a-z]{5}$"); private readonly Regex _snowflakeRegex = new("^[0-9]{17,19}$"); private List? _memberLookupCache { get; set; } @@ -46,8 +45,8 @@ public class PKControllerBase: ControllerBase if (_snowflakeRegex.IsMatch(systemRef)) return _repo.GetSystemByAccount(ulong.Parse(systemRef)); - if (_shortIdRegex.IsMatch(systemRef)) - return _repo.GetSystemByHid(systemRef); + if (systemRef.TryParseHid(out var hid)) + return _repo.GetSystemByHid(hid); return Task.FromResult(null); } @@ -71,8 +70,8 @@ public class PKControllerBase: ControllerBase if (Guid.TryParse(memberRef, out var guid)) return await _repo.GetMemberByGuid(guid); - if (_shortIdRegex.IsMatch(memberRef)) - return await _repo.GetMemberByHid(memberRef); + if (memberRef.TryParseHid(out var hid)) + return await _repo.GetMemberByHid(hid); return null; } @@ -96,8 +95,8 @@ public class PKControllerBase: ControllerBase if (Guid.TryParse(groupRef, out var guid)) return await _repo.GetGroupByGuid(guid); - if (_shortIdRegex.IsMatch(groupRef)) - return await _repo.GetGroupByHid(groupRef); + if (groupRef.TryParseHid(out var hid)) + return await _repo.GetGroupByHid(hid); return null; } diff --git a/PluralKit.API/Controllers/v2/AutoproxyControllerV2.cs b/PluralKit.API/Controllers/v2/AutoproxyControllerV2.cs index d72c6d78..2f42b5f5 100644 --- a/PluralKit.API/Controllers/v2/AutoproxyControllerV2.cs +++ b/PluralKit.API/Controllers/v2/AutoproxyControllerV2.cs @@ -53,7 +53,7 @@ public class AutoproxyControllerV2: PKControllerBase private async Task Patch(PKSystem system, ulong? guildId, ulong? channelId, JObject data, AutoproxySettings oldData) { - var updateMember = data.ContainsKey("autoproxy_member"); + var updateMember = data.ContainsKey("autoproxy_member") && data.Value("autoproxy_member") != null; PKMember? member = null; if (updateMember) @@ -64,15 +64,37 @@ public class AutoproxyControllerV2: PKControllerBase } var patch = AutoproxyPatch.FromJson(data, member?.Id); - patch.AssertIsValid(); + + var newAutoproxyMode = patch.AutoproxyMode.IsPresent ? patch.AutoproxyMode : oldData.AutoproxyMode; + var newAutoproxyMember = patch.AutoproxyMember.IsPresent ? patch.AutoproxyMember : oldData.AutoproxyMember; + if (updateMember && member == null) + { patch.Errors.Add(new("autoproxy_member", "Member not found.")); - if (updateMember && !( - (patch.AutoproxyMode.IsPresent && patch.AutoproxyMode.Value == AutoproxyMode.Member) - || (!patch.AutoproxyMode.IsPresent && oldData.AutoproxyMode == AutoproxyMode.Member)) - ) - patch.Errors.Add(new("autoproxy_member", "Cannot update autoproxy member if autoproxy mode is set to latch")); + } + + if (newAutoproxyMode.Value == AutoproxyMode.Member) + { + if (!updateMember) + { + patch.Errors.Add(new("autoproxy_member", "An autoproxy member must be supplied for autoproxy mode 'member'")); + } + + patch.AutoproxyMode = newAutoproxyMode; + patch.AutoproxyMember = newAutoproxyMember; + } + else + { + if (updateMember) + { + patch.Errors.Add(new("autoproxy_member", "Cannot update autoproxy member if autoproxy mode is not set to 'member'")); + } + + patch.AutoproxyMode = newAutoproxyMode; + patch.AutoproxyMember = null; + } + if (patch.Errors.Count > 0) throw new ModelParseError(patch.Errors); diff --git a/PluralKit.API/Controllers/v2/GroupControllerV2.cs b/PluralKit.API/Controllers/v2/GroupControllerV2.cs index 62db4525..f3a6de67 100644 --- a/PluralKit.API/Controllers/v2/GroupControllerV2.cs +++ b/PluralKit.API/Controllers/v2/GroupControllerV2.cs @@ -21,9 +21,6 @@ public class GroupControllerV2: PKControllerBase var ctx = ContextFor(system); - if (with_members && !system.MemberListPrivacy.CanAccess(ctx)) - throw Errors.UnauthorizedMemberList; - if (!system.GroupListPrivacy.CanAccess(ContextFor(system))) throw Errors.UnauthorizedGroupList; @@ -31,20 +28,17 @@ public class GroupControllerV2: PKControllerBase var j_groups = await groups .Where(g => g.Visibility.CanAccess(ctx)) - .Select(g => g.ToJson(ctx, needsMembersArray: with_members)) + .Select(g => g.ToJson(ctx, needsMembersArray: with_members, systemStr: system.Hid)) .ToListAsync(); - if (with_members && !system.MemberListPrivacy.CanAccess(ctx)) - throw Errors.UnauthorizedMemberList; - if (with_members && j_groups.Count > 0) { var q = await _repo.GetGroupMemberInfo(await groups .Where(g => g.Visibility.CanAccess(ctx)) + .Where(g => g.ListPrivacy.CanAccess(ctx)) .Select(x => x.Id) .ToListAsync()); - foreach (var row in q) if (row.MemberVisibility.CanAccess(ctx)) ((JArray)j_groups.Find(x => x.Value("id") == row.Group)["members"]).Add(row.MemberUuid); @@ -86,7 +80,7 @@ public class GroupControllerV2: PKControllerBase await tx.CommitAsync(); - return Ok(newGroup.ToJson(LookupContext.ByOwner)); + return Ok(newGroup.ToJson(LookupContext.ByOwner, system.Hid)); } [HttpGet("groups/{groupRef}")] @@ -133,7 +127,7 @@ public class GroupControllerV2: PKControllerBase throw new ModelParseError(patch.Errors); var newGroup = await _repo.UpdateGroup(group.Id, patch); - return Ok(newGroup.ToJson(LookupContext.ByOwner)); + return Ok(newGroup.ToJson(LookupContext.ByOwner, system.Hid)); } [HttpDelete("groups/{groupRef}")] diff --git a/PluralKit.API/Controllers/v2/GroupMemberControllerV2.cs b/PluralKit.API/Controllers/v2/GroupMemberControllerV2.cs index 7a40f244..d2c8dd2e 100644 --- a/PluralKit.API/Controllers/v2/GroupMemberControllerV2.cs +++ b/PluralKit.API/Controllers/v2/GroupMemberControllerV2.cs @@ -19,17 +19,19 @@ public class GroupMemberControllerV2: PKControllerBase if (group == null) throw Errors.GroupNotFound; + var ctx = ContextFor(group); if (!group.ListPrivacy.CanAccess(ctx)) throw Errors.UnauthorizedGroupMemberList; + var system = await _repo.GetSystem(group.System); var members = _repo.GetGroupMembers(group.Id).Where(m => m.MemberVisibility.CanAccess(ctx)); var o = new JArray(); await foreach (var member in members) - o.Add(member.ToJson(ctx)); + o.Add(member.ToJson(ctx, systemStr: system.Hid)); return Ok(o); } @@ -147,6 +149,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); @@ -158,7 +162,7 @@ public class GroupMemberControllerV2: PKControllerBase var o = new JArray(); await foreach (var group in groups) - o.Add(group.ToJson(ctx)); + o.Add(group.ToJson(ctx, system.Hid)); return Ok(o); } diff --git a/PluralKit.API/Controllers/v2/MemberControllerV2.cs b/PluralKit.API/Controllers/v2/MemberControllerV2.cs index 6a517727..25163dff 100644 --- a/PluralKit.API/Controllers/v2/MemberControllerV2.cs +++ b/PluralKit.API/Controllers/v2/MemberControllerV2.cs @@ -28,7 +28,7 @@ public class MemberControllerV2: PKControllerBase var members = _repo.GetSystemMembers(system.Id); return Ok(await members .Where(m => m.MemberVisibility.CanAccess(ctx)) - .Select(m => m.ToJson(ctx)) + .Select(m => m.ToJson(ctx, systemStr: system.Hid)) .ToListAsync()); } @@ -64,7 +64,7 @@ public class MemberControllerV2: PKControllerBase await tx.CommitAsync(); - return Ok(newMember.ToJson(LookupContext.ByOwner)); + return Ok(newMember.ToJson(LookupContext.ByOwner, systemStr: system.Hid)); } [HttpGet("members/{memberRef}")] @@ -111,7 +111,7 @@ public class MemberControllerV2: PKControllerBase throw new ModelParseError(patch.Errors); var newMember = await _repo.UpdateMember(member.Id, patch); - return Ok(newMember.ToJson(LookupContext.ByOwner)); + return Ok(newMember.ToJson(LookupContext.ByOwner, systemStr: system.Hid)); } [HttpDelete("members/{memberRef}")] diff --git a/PluralKit.API/Controllers/v2/SwitchControllerV2.cs b/PluralKit.API/Controllers/v2/SwitchControllerV2.cs index e0b7ff63..192cf460 100644 --- a/PluralKit.API/Controllers/v2/SwitchControllerV2.cs +++ b/PluralKit.API/Controllers/v2/SwitchControllerV2.cs @@ -39,7 +39,7 @@ public class SwitchControllerV2: PKControllerBase var res = await _db.Execute(conn => conn.QueryAsync( @"select *, array( - select members.hid from switch_members, members + select trim(members.hid) from switch_members, members where switch_members.switch = switches.id and members.id = switch_members.member ) as members from switches where switches.system = @System and switches.timestamp < @Before @@ -70,7 +70,7 @@ public class SwitchControllerV2: PKControllerBase return Ok(new FrontersReturnNew { Timestamp = sw.Timestamp, - Members = await members.Select(m => m.ToJson(ctx)).ToListAsync(), + Members = await members.Select(m => m.ToJson(ctx, systemStr: system.Hid)).ToListAsync(), Uuid = sw.Uuid, }); } @@ -124,7 +124,7 @@ public class SwitchControllerV2: PKControllerBase { Uuid = newSwitch.Uuid, Timestamp = data.Timestamp != null ? data.Timestamp.Value : newSwitch.Timestamp, - Members = members.Select(x => x.ToJson(LookupContext.ByOwner)), + Members = members.Select(x => x.ToJson(LookupContext.ByOwner, systemStr: system.Hid)), }); } @@ -153,7 +153,7 @@ public class SwitchControllerV2: PKControllerBase { Uuid = sw.Uuid, Timestamp = sw.Timestamp, - Members = await members.Select(m => m.ToJson(ctx)).ToListAsync() + Members = await members.Select(m => m.ToJson(ctx, systemStr: system.Hid)).ToListAsync() }); } @@ -190,7 +190,7 @@ public class SwitchControllerV2: PKControllerBase { Uuid = sw.Uuid, Timestamp = sw.Timestamp, - Members = members.Select(x => x.ToJson(LookupContext.ByOwner)) + Members = members.Select(x => x.ToJson(LookupContext.ByOwner, systemStr: system.Hid)) }); } @@ -238,7 +238,7 @@ public class SwitchControllerV2: PKControllerBase { Uuid = sw.Uuid, Timestamp = sw.Timestamp, - Members = members.Select(x => x.ToJson(LookupContext.ByOwner)) + Members = members.Select(x => x.ToJson(LookupContext.ByOwner, systemStr: system.Hid)) }); } diff --git a/PluralKit.API/packages.lock.json b/PluralKit.API/packages.lock.json index 618878b9..0d41f905 100644 --- a/PluralKit.API/packages.lock.json +++ b/PluralKit.API/packages.lock.json @@ -368,8 +368,8 @@ }, "Microsoft.Extensions.Logging.Abstractions": { "type": "Transitive", - "resolved": "3.1.10", - "contentHash": "bKHbgzbGsPZbEaExRaJqBz3WQ1GfhMttM23e1nivLJ8HbA3Ad526mW2G2K350q3Dc3HG83I5W8uSZWG4Rv4IpA==" + "resolved": "6.0.0", + "contentHash": "/HggWBbTwy8TgebGSX5DBZ24ndhzi93sHUBDvP1IxbZD7FDokYzdAr6+vbWGjw2XAfR2EJ1sfKUotpjHnFWPxA==" }, "Microsoft.Extensions.Options": { "type": "Transitive", @@ -398,8 +398,8 @@ }, "Microsoft.NETCore.Platforms": { "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "VyPlqzH2wavqquTcYpkIIAQ6WdenuKoFN0BdYBbCWsclXacSOHNQn66Gt4z5NBqEYW0FAPm5rlvki9ZiCij5xQ==" + "resolved": "1.1.0", + "contentHash": "kz0PEW2lhqygehI/d6XsPCQzD7ff7gUJaVGPVETX611eadGsA3A877GdSlU0LRVMCTH/+P3o2iDTak+S08V2+A==" }, "Microsoft.NETCore.Targets": { "type": "Transitive", @@ -416,23 +416,6 @@ "System.Runtime": "4.3.0" } }, - "Microsoft.Win32.Registry": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==", - "dependencies": { - "System.Security.AccessControl": "5.0.0", - "System.Security.Principal.Windows": "5.0.0" - } - }, - "Microsoft.Win32.SystemEvents": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "Bh6blKG8VAKvXiLe2L+sEsn62nc1Ij34MrNxepD2OCrS5cpCwQa9MeLyhVQPQ/R4Wlzwuy6wMK8hLb11QPDRsQ==", - "dependencies": { - "Microsoft.NETCore.Platforms": "5.0.0" - } - }, "NETStandard.Library": { "type": "Transitive", "resolved": "1.6.1", @@ -516,8 +499,8 @@ }, "Npgsql": { "type": "Transitive", - "resolved": "4.1.5", - "contentHash": "juDlNse+SKfXRP0VSgpJkpdCcaVLZt8m37EHdRX+8hw+GG69Eat1Y0MdEfl+oetdOnf9E133GjIDEjg9AF6HSQ==", + "resolved": "4.1.13", + "contentHash": "p79cObfuRgS8KD5sFmQUqVlINEkJm39bCrzRclicZE1942mKcbLlc0NdoVKhBeZPv//prK/sVTUmRVxdnoPCoA==", "dependencies": { "System.Runtime.CompilerServices.Unsafe": "4.6.0" } @@ -533,10 +516,10 @@ }, "Pipelines.Sockets.Unofficial": { "type": "Transitive", - "resolved": "2.2.0", - "contentHash": "7hzHplEIVOGBl5zOQZGX/DiJDHjq+RVRVrYgDiqXb6RriqWAdacXxp+XO9WSrATCEXyNOUOQg9aqQArsjase/A==", + "resolved": "2.2.8", + "contentHash": "zG2FApP5zxSx6OcdJQLbZDk2AVlN2BNQD6MorwIfV6gVj0RRxWPEp2LXAxqDGZqeNV1Zp0BNPcNaey/GXmTdvQ==", "dependencies": { - "System.IO.Pipelines": "5.0.0" + "System.IO.Pipelines": "5.0.1" } }, "runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl": { @@ -797,11 +780,11 @@ }, "StackExchange.Redis": { "type": "Transitive", - "resolved": "2.2.88", - "contentHash": "JJi1jcO3/ZiamBhlsC/TR8aZmYf+nqpGzMi0HRRCy5wJkUPmMnRp0kBA6V84uhU8b531FHSdTDaFCAyCUJomjA==", + "resolved": "2.8.16", + "contentHash": "WaoulkOqOC9jHepca3JZKFTqndCWab5uYS7qCzmiQDlrTkFaDN7eLSlEfHycBxipRnQY9ppZM7QSsWAwUEGblw==", "dependencies": { - "Pipelines.Sockets.Unofficial": "2.2.0", - "System.Diagnostics.PerformanceCounter": "5.0.0" + "Microsoft.Extensions.Logging.Abstractions": "6.0.0", + "Pipelines.Sockets.Unofficial": "2.2.8" } }, "System.AppContext": { @@ -844,15 +827,6 @@ "System.Threading.Tasks": "4.3.0" } }, - "System.Configuration.ConfigurationManager": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "aM7cbfEfVNlEEOj3DsZP+2g9NRwbkyiAv2isQEzw7pnkDg9ekCU2m1cdJLM02Uq691OaCS91tooaxcEn8d0q5w==", - "dependencies": { - "System.Security.Cryptography.ProtectedData": "5.0.0", - "System.Security.Permissions": "5.0.0" - } - }, "System.Console": { "type": "Transitive", "resolved": "4.3.0", @@ -880,17 +854,6 @@ "resolved": "4.7.1", "contentHash": "j81Lovt90PDAq8kLpaJfJKV/rWdWuEk6jfV+MBkee33vzYLEUsy4gXK8laa9V2nZlLM9VM9yA/OOQxxPEJKAMw==" }, - "System.Diagnostics.PerformanceCounter": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "kcQWWtGVC3MWMNXdMDWfrmIlFZZ2OdoeT6pSNVRtk9+Sa7jwdPiMlNwb0ZQcS7NRlT92pCfmjRtkSWUW3RAKwg==", - "dependencies": { - "Microsoft.NETCore.Platforms": "5.0.0", - "Microsoft.Win32.Registry": "5.0.0", - "System.Configuration.ConfigurationManager": "5.0.0", - "System.Security.Principal.Windows": "5.0.0" - } - }, "System.Diagnostics.Tools": { "type": "Transitive", "resolved": "4.3.0", @@ -911,14 +874,6 @@ "System.Runtime": "4.3.0" } }, - "System.Drawing.Common": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "SztFwAnpfKC8+sEKXAFxCBWhKQaEd97EiOL7oZJZP56zbqnLpmxACWA8aGseaUExciuEAUuR9dY8f7HkTRAdnw==", - "dependencies": { - "Microsoft.Win32.SystemEvents": "5.0.0" - } - }, "System.Dynamic.Runtime": { "type": "Transitive", "resolved": "4.0.11", @@ -1058,8 +1013,8 @@ }, "System.IO.Pipelines": { "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "irMYm3vhVgRsYvHTU5b2gsT2CwT/SMM6LZFzuJjpIvT5Z4CshxNsaoBC1X/LltwuR3Opp8d6jOS/60WwOb7Q2Q==" + "resolved": "5.0.1", + "contentHash": "qEePWsaq9LoEEIqhbGe6D5J8c9IqQOUuTzzV6wn1POlfdLkJliZY3OlB0j0f17uMWlqZYjH7txj+2YbyrIA8Yg==" }, "System.Linq": { "type": "Transitive", @@ -1322,15 +1277,6 @@ "System.Runtime.Extensions": "4.3.0" } }, - "System.Security.AccessControl": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "dagJ1mHZO3Ani8GH0PHpPEe/oYO+rVdbQjvjJkBRNQkX4t0r1iaeGn8+/ybkSLEan3/slM0t59SVdHzuHf2jmw==", - "dependencies": { - "Microsoft.NETCore.Platforms": "5.0.0", - "System.Security.Principal.Windows": "5.0.0" - } - }, "System.Security.Cryptography.Algorithms": { "type": "Transitive", "resolved": "4.3.0", @@ -1443,11 +1389,6 @@ "System.Threading.Tasks": "4.3.0" } }, - "System.Security.Cryptography.ProtectedData": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "HGxMSAFAPLNoxBvSfW08vHde0F9uh7BjASwu6JF9JnXuEPhCY3YUqURn0+bQV/4UWeaqymmrHWV+Aw9riQCtCA==" - }, "System.Security.Cryptography.X509Certificates": { "type": "Transitive", "resolved": "4.3.0", @@ -1480,20 +1421,6 @@ "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" } }, - "System.Security.Permissions": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "uE8juAhEkp7KDBCdjDIE3H9R1HJuEHqeqX8nLX9gmYKWwsqk3T5qZlPx8qle5DPKimC/Fy3AFTdV7HamgCh9qQ==", - "dependencies": { - "System.Security.AccessControl": "5.0.0", - "System.Windows.Extensions": "5.0.0" - } - }, - "System.Security.Principal.Windows": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "t0MGLukB5WAVU9bO3MGzvlGnyJPgUlcwerXn1kzBRjwLKixT96XV0Uza41W49gVd8zEMFu9vQEFlv0IOrytICA==" - }, "System.Text.Encoding": { "type": "Transitive", "resolved": "4.3.0", @@ -1567,14 +1494,6 @@ "System.Runtime": "4.3.0" } }, - "System.Windows.Extensions": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "c1ho9WU9ZxMZawML+ssPKZfdnrg/OjR3pe0m9v8230z3acqphwvPJqzAkH54xRYm5ntZHGG1EPP3sux9H3qSPg==", - "dependencies": { - "System.Drawing.Common": "5.0.0" - } - }, "System.Xml.ReaderWriter": { "type": "Transitive", "resolved": "4.3.0", @@ -1637,7 +1556,7 @@ "Newtonsoft.Json": "[13.0.1, )", "NodaTime": "[3.0.3, )", "NodaTime.Serialization.JsonNet": "[3.0.0, )", - "Npgsql": "[4.1.5, )", + "Npgsql": "[4.1.13, )", "Npgsql.NodaTime": "[4.1.5, )", "Serilog": "[2.12.0, )", "Serilog.Extensions.Logging": "[3.0.1, )", @@ -1650,7 +1569,7 @@ "Serilog.Sinks.Seq": "[5.2.2, )", "SqlKata": "[2.3.7, )", "SqlKata.Execution": "[2.3.7, )", - "StackExchange.Redis": "[2.2.88, )", + "StackExchange.Redis": "[2.8.16, )", "System.Interactive.Async": "[5.0.0, )", "ipnetwork2": "[2.5.381, )" } diff --git a/PluralKit.Bot/ApplicationCommandMeta/ApplicationCommandTree.cs b/PluralKit.Bot/ApplicationCommandMeta/ApplicationCommandTree.cs index 04292a88..7f3600ae 100644 --- a/PluralKit.Bot/ApplicationCommandMeta/ApplicationCommandTree.cs +++ b/PluralKit.Bot/ApplicationCommandMeta/ApplicationCommandTree.cs @@ -12,7 +12,7 @@ public partial class ApplicationCommandTree else if (ctx.Event.Data!.Name == ProxiedMessageDelete.Name) return ctx.Execute(ProxiedMessageDelete, m => m.DeleteMessage(ctx)); else if (ctx.Event.Data!.Name == ProxiedMessagePing.Name) - return ctx.Execute(ProxiedMessageDelete, m => m.PingMessageAuthor(ctx)); + return ctx.Execute(ProxiedMessagePing, m => m.PingMessageAuthor(ctx)); return null; } diff --git a/PluralKit.Bot/ApplicationCommands/Message.cs b/PluralKit.Bot/ApplicationCommands/Message.cs index 2e56c4d0..a425b31c 100644 --- a/PluralKit.Bot/ApplicationCommands/Message.cs +++ b/PluralKit.Bot/ApplicationCommands/Message.cs @@ -48,11 +48,12 @@ public class ApplicationCommandProxiedMessage msg.System, msg.Member, guild, + ctx.Config, LookupContext.ByNonOwner, DateTimeZone.Utc )); - embeds.Add(await _embeds.CreateMessageInfoEmbed(msg, showContent)); + embeds.Add(await _embeds.CreateMessageInfoEmbed(msg, showContent, ctx.Config)); await ctx.Reply(embeds: embeds.ToArray()); } @@ -62,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; } @@ -77,10 +78,11 @@ 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 user has has a system and their system sent the message, or if user sent the message, do not error + if (!((ctx.System != null && message.System?.Id == ctx.System.Id) || message.Message.Sender == ctx.User.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; } @@ -88,9 +90,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.PermissionsIn(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."); @@ -100,6 +102,14 @@ public class ApplicationCommandProxiedMessage public async Task PingMessageAuthor(InteractionContext ctx) { + // if the command message was sent by a user account with bot usage disallowed, ignore it + var abuse_log = await _repo.GetAbuseLogByAccount(ctx.User.Id); + if (abuse_log != null && abuse_log.DenyBotUsage) + { + await ctx.Defer(); + return; + } + var messageId = ctx.Event.Data!.TargetId!.Value; var msg = await ctx.Repository.GetFullMessage(messageId); if (msg == null) @@ -109,7 +119,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.PermissionsFor(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 c1dbb718..96f1b568 100644 --- a/PluralKit.Bot/Bot.cs +++ b/PluralKit.Bot/Bot.cs @@ -67,8 +67,9 @@ public class Bot { new Activity { - Type = ActivityType.Game, - Name = BotStatus + Type = ActivityType.Custom, + Name = BotStatus, + State = BotStatus } } }; @@ -83,9 +84,16 @@ public class Bot // This *probably* doesn't matter in practice but I jut think it's neat, y'know. var timeNow = SystemClock.Instance.GetCurrentInstant(); var timeTillNextWholeMinute = TimeSpan.FromMilliseconds(60000 - timeNow.ToUnixTimeMilliseconds() % 60000 + 250); - _periodicTask = new Timer(_ => + _periodicTask = new Timer(async _ => { - var __ = UpdatePeriodic(); + try + { + await UpdatePeriodic(); + } + catch (Exception e) + { + _logger.Error(e, "failed to run once-per-minute scheduled task"); + } }, null, timeTillNextWholeMinute, TimeSpan.FromMinutes(1)); } @@ -93,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); } @@ -134,7 +140,8 @@ public class Bot new Activity { Name = "Restarting... (please wait)", - Type = ActivityType.Game + State = "Restarting... (please wait)", + Type = ActivityType.Custom } }, Status = GatewayStatusUpdate.UserStatus.Idle @@ -166,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 @@ -234,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) @@ -242,7 +258,7 @@ public class Bot return; } - var botPerms = await _cache.PermissionsIn(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()); } @@ -269,7 +285,8 @@ public class Bot new Activity { Name = BotStatus, - Type = ActivityType.Game + State = BotStatus, + Type = ActivityType.Custom, } }, Status = GatewayStatusUpdate.UserStatus.Online diff --git a/PluralKit.Bot/BotConfig.cs b/PluralKit.Bot/BotConfig.cs index 18c44b28..c5c05191 100644 --- a/PluralKit.Bot/BotConfig.cs +++ b/PluralKit.Bot/BotConfig.cs @@ -20,11 +20,14 @@ 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; } public string? DiscordBaseUrl { get; set; } + public string? AvatarServiceUrl { get; set; } public bool DisableErrorReporting { get; set; } = false; 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 abfb9e35..c4a87f60 100644 --- a/PluralKit.Bot/CommandMeta/CommandHelp.cs +++ b/PluralKit.Bot/CommandMeta/CommandHelp.cs @@ -30,6 +30,8 @@ public partial class CommandTree public static Command ConfigShowPrivate = new Command("config show private", "config show private [on|off]", "Sets whether private information is shown to linked accounts by default"); public static Command ConfigMemberDefaultPrivacy = new("config private member", "config private member [on|off]", "Sets whether member privacy is automatically set to private when creating a new member"); public static Command ConfigGroupDefaultPrivacy = new("config private group", "config private group [on|off]", "Sets whether group privacy is automatically set to private when creating a new group"); + public static Command ConfigProxySwitch = new Command("config proxyswitch", "config proxyswitch [on|off]", "Sets whether to log a switch every time a proxy tag is used"); + public static Command ConfigNameFormat = new Command("config nameformat", "config nameformat [format]", "Changes your system's username formatting"); public static Command AutoproxySet = new Command("autoproxy", "autoproxy [off|front|latch|member]", "Sets your system's autoproxy mode for the current server"); public static Command AutoproxyOff = new Command("autoproxy off", "autoproxy off", "Disables autoproxying for your system in the current server"); public static Command AutoproxyFront = new Command("autoproxy front", "autoproxy front", "Sets your system's autoproxy in this server to proxy the first member currently registered as front"); @@ -58,7 +60,7 @@ public partial class CommandTree public static Command MemberServerKeepProxy = new Command("member server keepproxy", "member serverkeepproxy [on|off|clear]", "Sets whether to include a member's proxy tags when proxying in the current server."); public static Command MemberRandom = new Command("system random", "system [system] random", "Shows the info card of a randomly selected member in a system."); public static Command MemberId = new Command("member id", "member [member] id", "Prints a member's id."); - public static Command MemberPrivacy = new Command("member privacy", "member privacy ", "Changes a members's privacy settings"); + public static Command MemberPrivacy = new Command("member privacy", "member privacy ", "Changes a members's privacy settings"); public static Command GroupInfo = new Command("group", "group ", "Looks up information about a group"); public static Command GroupNew = new Command("group new", "group new ", "Creates a new group"); public static Command GroupList = new Command("group list", "group list", "Lists all groups in this system"); @@ -82,6 +84,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,19 +95,22 @@ 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"); public static Command ProxyCheck = new Command("debug proxy", "debug proxy [link|reply]", "Checks why your message has not been proxied"); - public static Command LogChannel = new Command("log channel", "log channel ", "Designates a channel to post proxied messages to"); - public static Command LogChannelClear = new Command("log channel", "log channel -clear", "Clears the currently set log channel"); - public static Command LogEnable = new Command("log enable", "log enable all| [channel 2] [channel 3...]", "Enables message logging in certain channels"); - public static Command LogDisable = new Command("log disable", "log disable all| [channel 2] [channel 3...]", "Disables message logging in certain channels"); - public static Command LogShow = new Command("log show", "log show", "Displays the current list of channels where logging is disabled"); - public static Command LogClean = new Command("logclean", "logclean [on|off]", "Toggles whether to clean up other bots' log channels"); - public static Command BlacklistShow = new Command("blacklist show", "blacklist show", "Displays the current proxy blacklist"); - public static Command BlacklistAdd = new Command("blacklist add", "blacklist add all| [channel 2] [channel 3...]", "Adds certain channels to the proxy blacklist"); - public static Command BlacklistRemove = new Command("blacklist remove", "blacklist remove all| [channel 2] [channel 3...]", "Removes certain channels from the proxy blacklist"); + public static Command LogChannel = new Command("serverconfig log channel", "serverconfig log channel ", "Designates a channel to post proxied messages to"); + public static Command LogChannelClear = new Command("serverconfig log channel", "serverconfig log channel -clear", "Clears the currently set log channel"); + public static Command LogEnable = new Command("serverconfig log blacklist remove", "serverconfig log blacklist remove all| [channel 2] [channel 3...]", "Enables message logging in certain channels"); + public static Command LogDisable = new Command("serverconfig log blacklist add", "serverconfig log blacklist add all| [channel 2] [channel 3...]", "Disables message logging in certain channels"); + public static Command LogShow = new Command("serverconfig log blacklist", "serverconfig log blacklist", "Displays the current list of channels where logging is disabled"); + public static Command BlacklistShow = new Command("serverconfig proxy blacklist", "serverconfig proxy blacklist", "Displays the current list of channels where message proxying is disabled"); + public static Command BlacklistAdd = new Command("serverconfig proxy blacklist add", "serverconfig proxy blacklist add all| [channel 2] [channel 3...]", "Disables message proxying in certain channels"); + public static Command BlacklistRemove = new Command("serverconfig blacklist remove", "serverconfig blacklist remove all| [channel 2] [channel 3...]", "Enables message proxying in certain channels"); + public static Command ServerConfigLogClean = new Command("serverconfig log cleanup", "serverconfig log cleanup [on|off]", "Toggles whether to clean up other bots' log channels"); + public static Command ServerConfigInvalidCommandResponse = new Command("serverconfig invalid command error", "serverconfig invalid command error [on|off]", "Sets whether to show an error message when an unknown command is sent"); + public static Command ServerConfigRequireSystemTag = new Command("serverconfig require tag", "serverconfig require tag [on|off]", "Sets whether server users are required to have a system tag on proxied messages"); public static Command Invite = new Command("invite", "invite", "Gets a link to invite PluralKit to other servers"); public static Command PermCheck = new Command("permcheck", "permcheck ", "Checks whether a server's permission setup is correct"); public static Command Admin = new Command("admin", "admin", "Super secret admin commands (sshhhh)"); @@ -137,13 +143,21 @@ 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 = { ConfigAutoproxyAccount, ConfigAutoproxyTimeout, ConfigTimezone, ConfigPing, - ConfigMemberDefaultPrivacy, ConfigGroupDefaultPrivacy, ConfigShowPrivate + ConfigMemberDefaultPrivacy, ConfigGroupDefaultPrivacy, ConfigShowPrivate, + ConfigProxySwitch, ConfigNameFormat + }; + + public static Command[] ServerConfigCommands = + { + ServerConfigLogClean, ServerConfigInvalidCommandResponse, ServerConfigRequireSystemTag, + LogChannel, LogChannelClear, LogShow, LogDisable, LogEnable, + BlacklistShow, BlacklistAdd, BlacklistRemove }; public static Command[] AutoproxyCommands = diff --git a/PluralKit.Bot/CommandMeta/CommandTree.cs b/PluralKit.Bot/CommandMeta/CommandTree.cs index 07d183bf..a458e1d4 100644 --- a/PluralKit.Bot/CommandMeta/CommandTree.cs +++ b/PluralKit.Bot/CommandMeta/CommandTree.cs @@ -18,8 +18,10 @@ 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("serverconfig", "guildconfig", "scfg")) + return HandleServerConfigCommand(ctx); if (ctx.Match("list", "find", "members", "search", "query", "l", "f", "fd", "ls")) return ctx.Execute(SystemList, m => m.MemberList(ctx, ctx.System)); if (ctx.Match("link")) @@ -44,36 +46,36 @@ public partial class CommandTree else return ctx.Execute(Help, m => m.HelpRoot(ctx)); if (ctx.Match("explain")) return ctx.Execute(Explain, m => m.Explain(ctx)); - if (ctx.Match("message", "msg")) + if (ctx.Match("message", "msg", "messageinfo")) return ctx.Execute(Message, m => m.GetMessage(ctx)); if (ctx.Match("edit", "e")) - return ctx.Execute(MessageEdit, m => m.EditMessage(ctx)); - if (ctx.Match("reproxy", "rp", "crimes")) + return ctx.Execute(MessageEdit, m => m.EditMessage(ctx, false)); + if (ctx.Match("x")) + return ctx.Execute(MessageEdit, m => m.EditMessage(ctx, true)); + if (ctx.Match("reproxy", "rp", "crimes", "crime")) return ctx.Execute(MessageReproxy, m => m.ReproxyMessage(ctx)); if (ctx.Match("log")) if (ctx.Match("channel")) - return ctx.Execute(LogChannel, m => m.SetLogChannel(ctx)); + return ctx.Execute(LogChannel, m => m.SetLogChannel(ctx), true); else if (ctx.Match("enable", "on")) - return ctx.Execute(LogEnable, m => m.SetLogEnabled(ctx, true)); + return ctx.Execute(LogEnable, m => m.SetLogEnabled(ctx, true), true); else if (ctx.Match("disable", "off")) - return ctx.Execute(LogDisable, m => m.SetLogEnabled(ctx, false)); + return ctx.Execute(LogDisable, m => m.SetLogEnabled(ctx, false), true); else if (ctx.Match("list", "show")) - return ctx.Execute(LogShow, m => m.ShowLogDisabledChannels(ctx)); - else if (ctx.Match("commands")) - return PrintCommandList(ctx, "message logging", LogCommands); - else return PrintCommandExpectedError(ctx, LogCommands); + return ctx.Execute(LogShow, m => m.ShowLogDisabledChannels(ctx), true); + else + return ctx.Reply($"{Emojis.Warn} Message logging commands have moved to `pk;serverconfig`."); if (ctx.Match("logclean")) - return ctx.Execute(LogClean, m => m.SetLogCleanup(ctx)); + return ctx.Execute(ServerConfigLogClean, m => m.SetLogCleanup(ctx), true); if (ctx.Match("blacklist", "bl")) if (ctx.Match("enable", "on", "add", "deny")) - return ctx.Execute(BlacklistAdd, m => m.SetBlacklisted(ctx, true)); + return ctx.Execute(BlacklistAdd, m => m.SetProxyBlacklisted(ctx, true), true); else if (ctx.Match("disable", "off", "remove", "allow")) - return ctx.Execute(BlacklistRemove, m => m.SetBlacklisted(ctx, false)); + return ctx.Execute(BlacklistRemove, m => m.SetProxyBlacklisted(ctx, false), true); else if (ctx.Match("list", "show")) - return ctx.Execute(BlacklistShow, m => m.ShowBlacklisted(ctx)); - else if (ctx.Match("commands")) - return PrintCommandList(ctx, "channel blacklisting", BlacklistCommands); - else return PrintCommandExpectedError(ctx, BlacklistCommands); + return ctx.Execute(BlacklistShow, m => m.ShowProxyBlacklisted(ctx), true); + else + return ctx.Reply($"{Emojis.Warn} Blacklist commands have moved to `pk;serverconfig`."); if (ctx.Match("proxy")) if (ctx.Match("debug")) return ctx.Execute(ProxyCheck, m => m.MessageProxyCheck(ctx)); @@ -89,7 +91,7 @@ public partial class CommandTree if (ctx.Match("rool")) return ctx.Execute(null, m => m.Rool(ctx)); if (ctx.Match("sus")) return ctx.Execute(null, m => m.Sus(ctx)); if (ctx.Match("error")) return ctx.Execute(null, m => m.Error(ctx)); - if (ctx.Match("stats")) return ctx.Execute(null, m => m.Stats(ctx)); + if (ctx.Match("stats", "status")) return ctx.Execute(null, m => m.Stats(ctx)); if (ctx.Match("permcheck")) return ctx.Execute(PermCheck, m => m.PermCheckGuild(ctx)); if (ctx.Match("proxycheck")) @@ -98,17 +100,65 @@ public partial class CommandTree return HandleDebugCommand(ctx); if (ctx.Match("admin")) return HandleAdminCommand(ctx); - if (ctx.Match("random", "r")) + if (ctx.Match("random", "rand", "r")) if (ctx.Match("group", "g") || ctx.MatchFlag("group", "g")) return ctx.Execute(GroupRandom, r => r.Group(ctx, ctx.System)); else return ctx.Execute(MemberRandom, m => m.Member(ctx, ctx.System)); + if (ctx.Match("dashboard", "dash")) + return ctx.Execute(Dashboard, m => m.Dashboard(ctx)); + + // don't send an "invalid command" response if the guild has those turned off + if (ctx.GuildConfig != null && ctx.GuildConfig!.InvalidCommandResponseEnabled != true) + return Task.CompletedTask; // remove compiler warning return ctx.Reply( $"{Emojis.Error} Unknown command {ctx.PeekArgument().AsCode()}. For a list of possible commands, see ."); } + private async Task HandleAdminAbuseLogCommand(Context ctx) + { + ctx.AssertBotAdmin(); + + if (ctx.Match("n", "new", "create")) + await ctx.Execute(Admin, a => a.AbuseLogCreate(ctx)); + else + { + AbuseLog? abuseLog = null!; + var account = await ctx.MatchUser(); + if (account != null) + { + abuseLog = await ctx.Repository.GetAbuseLogByAccount(account.Id); + } + else + { + abuseLog = await ctx.Repository.GetAbuseLogByGuid(new Guid(ctx.PopArgument())); + } + + if (abuseLog == null) + { + await ctx.Reply($"{Emojis.Error} Could not find an existing abuse log entry for that query."); + return; + } + + if (!ctx.HasNext()) + await ctx.Execute(Admin, a => a.AbuseLogShow(ctx, abuseLog)); + else if (ctx.Match("au", "adduser")) + await ctx.Execute(Admin, a => a.AbuseLogAddUser(ctx, abuseLog)); + else if (ctx.Match("ru", "removeuser")) + await ctx.Execute(Admin, a => a.AbuseLogRemoveUser(ctx, abuseLog)); + else if (ctx.Match("desc", "description")) + await ctx.Execute(Admin, a => a.AbuseLogDescription(ctx, abuseLog)); + else if (ctx.Match("deny", "deny-bot-usage")) + await ctx.Execute(Admin, a => a.AbuseLogFlagDeny(ctx, abuseLog)); + else if (ctx.Match("yeet", "remove", "delete")) + await ctx.Execute(Admin, a => a.AbuseLogDelete(ctx, abuseLog)); + else + await ctx.Reply($"{Emojis.Error} Unknown subcommand {ctx.PeekArgument().AsCode()}."); + } + } + private async Task HandleAdminCommand(Context ctx) { if (ctx.Match("usid", "updatesystemid")) @@ -129,6 +179,10 @@ public partial class CommandTree await ctx.Execute(Admin, a => a.SystemGroupLimit(ctx)); else if (ctx.Match("sr", "systemrecover")) await ctx.Execute(Admin, a => a.SystemRecover(ctx)); + else if (ctx.Match("sd", "systemdelete")) + await ctx.Execute(Admin, a => a.SystemDelete(ctx)); + else if (ctx.Match("al", "abuselog")) + await HandleAdminAbuseLogCommand(ctx); else await ctx.Reply($"{Emojis.Error} Unknown command."); } @@ -161,12 +215,6 @@ public partial class CommandTree else if (ctx.Match("commands", "help")) await PrintCommandList(ctx, "systems", SystemCommands); - // these are deprecated (and not accessible by other users anyway), let's leave them out of new parsing - else if (ctx.Match("timezone", "tz")) - await ctx.Execute(ConfigTimezone, m => m.SystemTimezone(ctx), true); - else if (ctx.Match("ping")) - await ctx.Execute(ConfigPing, m => m.SystemPing(ctx), true); - // todo: these aren't deprecated but also shouldn't be here else if (ctx.Match("webhook", "hook")) await ctx.Execute(null, m => m.SystemWebhook(ctx)); @@ -203,7 +251,7 @@ public partial class CommandTree // if we *still* haven't matched anything, the user entered an invalid command name or system reference if (ctx.Parameters._ptr == previousPtr) { - if (ctx.Parameters.Peek().Length != 5 && !ctx.Parameters.Peek().TryParseMention(out _)) + if (!ctx.Parameters.Peek().TryParseHid(out _) && !ctx.Parameters.Peek().TryParseMention(out _)) { await PrintCommandNotFoundError(ctx, SystemCommands); return; @@ -227,9 +275,9 @@ public partial class CommandTree await ctx.CheckSystem(target).Execute(SystemTag, m => m.Tag(ctx, target)); else if (ctx.Match("servertag", "st", "stag", "deer")) await ctx.CheckSystem(target).Execute(SystemServerTag, m => m.ServerTag(ctx, target)); - else if (ctx.Match("description", "desc", "bio", "info", "text")) + else if (ctx.Match("description", "desc", "describe", "d", "bio", "info", "text", "intro")) await ctx.CheckSystem(target).Execute(SystemDesc, m => m.Description(ctx, target)); - else if (ctx.Match("pronouns", "prns")) + else if (ctx.Match("pronouns", "pronoun", "prns", "pn")) await ctx.CheckSystem(target).Execute(SystemPronouns, m => m.Pronouns(ctx, target)); else if (ctx.Match("color", "colour")) await ctx.CheckSystem(target).Execute(SystemColor, m => m.Color(ctx, target)); @@ -267,7 +315,7 @@ public partial class CommandTree await ctx.CheckSystem(target).Execute(SystemDelete, m => m.Delete(ctx, target)); else if (ctx.Match("id")) await ctx.CheckSystem(target).Execute(SystemId, m => m.DisplayId(ctx, target)); - else if (ctx.Match("random", "r")) + else if (ctx.Match("random", "rand", "r")) if (ctx.Match("group", "g") || ctx.MatchFlag("group", "g")) await ctx.CheckSystem(target).Execute(GroupRandom, r => r.Group(ctx, target)); else @@ -297,13 +345,13 @@ public partial class CommandTree // Commands that have a member target (eg. pk;member delete) if (ctx.Match("rename", "name", "changename", "setname", "rn")) await ctx.Execute(MemberRename, m => m.Name(ctx, target)); - else if (ctx.Match("description", "info", "bio", "text", "desc")) + else if (ctx.Match("description", "desc", "describe", "d", "bio", "info", "text", "intro")) await ctx.Execute(MemberDesc, m => m.Description(ctx, target)); else if (ctx.Match("pronouns", "pronoun", "prns", "pn")) await ctx.Execute(MemberPronouns, m => m.Pronouns(ctx, target)); else if (ctx.Match("color", "colour")) await ctx.Execute(MemberColor, m => m.Color(ctx, target)); - else if (ctx.Match("birthday", "bday", "birthdate", "cakeday", "bdate")) + else if (ctx.Match("birthday", "birth", "bday", "birthdate", "cakeday", "bdate", "bd")) await ctx.Execute(MemberBirthday, m => m.Birthday(ctx, target)); else if (ctx.Match("proxy", "tags", "proxytags", "brackets")) await ctx.Execute(MemberProxy, m => m.Proxy(ctx, target)); @@ -311,7 +359,7 @@ public partial class CommandTree await ctx.Execute(MemberDelete, m => m.Delete(ctx, target)); else if (ctx.Match("avatar", "profile", "picture", "icon", "image", "pfp", "pic")) await ctx.Execute(MemberAvatar, m => m.Avatar(ctx, target)); - else if (ctx.Match("proxyavatar", "proxypfp", "webhookavatar", "webhookpfp", "pa")) + else if (ctx.Match("proxyavatar", "proxypfp", "webhookavatar", "webhookpfp", "pa", "pavatar", "ppfp")) await ctx.Execute(MemberAvatar, m => m.WebhookAvatar(ctx, target)); else if (ctx.Match("banner", "splash", "cover")) await ctx.Execute(MemberBannerImage, m => m.BannerImage(ctx, target)); @@ -319,7 +367,7 @@ public partial class CommandTree if (ctx.Match("add", "a")) await ctx.Execute(MemberGroupAdd, m => m.AddRemoveGroups(ctx, target, Groups.AddRemoveOperation.Add)); - else if (ctx.Match("remove", "rem", "r")) + else if (ctx.Match("remove", "rem")) await ctx.Execute(MemberGroupRemove, m => m.AddRemoveGroups(ctx, target, Groups.AddRemoveOperation.Remove)); else @@ -346,7 +394,7 @@ public partial class CommandTree await ctx.Execute(MemberPrivacy, m => m.Privacy(ctx, target, null)); else if (ctx.Match("private", "hidden", "hide")) await ctx.Execute(MemberPrivacy, m => m.Privacy(ctx, target, PrivacyLevel.Private)); - else if (ctx.Match("public", "shown", "show")) + else if (ctx.Match("public", "shown", "show", "unhide", "unhidden")) await ctx.Execute(MemberPrivacy, m => m.Privacy(ctx, target, PrivacyLevel.Public)); else if (ctx.Match("soulscream")) await ctx.Execute(MemberInfo, m => m.Soulscream(ctx, target)); @@ -374,17 +422,17 @@ public partial class CommandTree await ctx.Execute(GroupRename, g => g.RenameGroup(ctx, target)); else if (ctx.Match("nick", "dn", "displayname", "nickname")) await ctx.Execute(GroupDisplayName, g => g.GroupDisplayName(ctx, target)); - else if (ctx.Match("description", "info", "bio", "text", "desc")) + else if (ctx.Match("description", "desc", "describe", "d", "bio", "info", "text", "intro")) await ctx.Execute(GroupDesc, g => g.GroupDescription(ctx, target)); else if (ctx.Match("add", "a")) await ctx.Execute(GroupAdd, g => g.AddRemoveMembers(ctx, target, Groups.AddRemoveOperation.Add)); - else if (ctx.Match("remove", "rem", "r")) + else if (ctx.Match("remove", "rem")) await ctx.Execute(GroupRemove, g => g.AddRemoveMembers(ctx, target, Groups.AddRemoveOperation.Remove)); else if (ctx.Match("members", "list", "ms", "l", "ls")) await ctx.Execute(GroupMemberList, g => g.ListGroupMembers(ctx, target)); - else if (ctx.Match("random")) + else if (ctx.Match("random", "rand", "r")) await ctx.Execute(GroupMemberRandom, r => r.GroupMember(ctx, target)); else if (ctx.Match("privacy")) await ctx.Execute(GroupPrivacy, g => g.GroupPrivacy(ctx, target, null)); @@ -428,13 +476,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) @@ -482,6 +532,11 @@ public partial class CommandTree case "cfg": await PrintCommandList(ctx, "settings", ConfigCommands); break; + case "serverconfig": + case "guildconfig": + case "scfg": + await PrintCommandList(ctx, "server settings", ServerConfigCommands); + break; case "autoproxy": case "ap": await PrintCommandList(ctx, "autoproxy", AutoproxyCommands); @@ -500,13 +555,6 @@ public partial class CommandTree if (ctx.System == null) return ctx.Reply($"{Emojis.Error} {Errors.NoSystemError.Message}"); - // todo: move this whole block to Autoproxy.cs when these are removed - - if (ctx.Match("account", "ac")) - return ctx.Execute(ConfigAutoproxyAccount, m => m.AutoproxyAccount(ctx), true); - if (ctx.Match("timeout", "tm")) - return ctx.Execute(ConfigAutoproxyTimeout, m => m.AutoproxyTimeout(ctx), true); - return ctx.Execute(AutoproxySet, m => m.SetAutoproxyMode(ctx)); } @@ -536,8 +584,56 @@ public partial class CommandTree return ctx.Execute(null, m => m.CaseSensitiveProxyTags(ctx)); if (ctx.MatchMultiple(new[] { "proxy" }, new[] { "error" }) || ctx.Match("pe")) return ctx.Execute(null, m => m.ProxyErrorMessageEnabled(ctx)); + if (ctx.MatchMultiple(new[] { "split" }, new[] { "id", "ids" }) || ctx.Match("sid", "sids")) + return ctx.Execute(null, m => m.HidDisplaySplit(ctx)); + if (ctx.MatchMultiple(new[] { "cap", "caps", "capitalize", "capitalise" }, new[] { "id", "ids" }) || ctx.Match("capid", "capids")) + return ctx.Execute(null, m => m.HidDisplayCaps(ctx)); + if (ctx.MatchMultiple(new[] { "pad" }, new[] { "id", "ids" }) || ctx.MatchMultiple(new[] { "id" }, new[] { "pad", "padding" }) || ctx.Match("idpad", "padid", "padids")) + return ctx.Execute(null, m => m.HidListPadding(ctx)); + if (ctx.MatchMultiple(new[] { "name" }, new[] { "format" }) || ctx.Match("nameformat", "nf")) + return ctx.Execute(null, m => m.NameFormat(ctx)); + if (ctx.MatchMultiple(new[] { "member", "group" }, new[] { "limit" }) || ctx.Match("limit")) + return ctx.Execute(null, m => m.LimitUpdate(ctx)); + if (ctx.MatchMultiple(new[] { "proxy" }, new[] { "switch" }) || ctx.Match("proxyswitch", "ps")) + return ctx.Execute(null, m => m.ProxySwitch(ctx)); // 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."); } + + private Task HandleServerConfigCommand(Context ctx) + { + if (!ctx.HasNext()) + return ctx.Execute(null, m => m.ShowConfig(ctx)); + + if (ctx.MatchMultiple(new[] { "log" }, new[] { "cleanup", "clean" }) || ctx.Match("logclean")) + return ctx.Execute(null, m => m.SetLogCleanup(ctx)); + if (ctx.MatchMultiple(new[] { "invalid", "unknown" }, new[] { "command" }, new[] { "error", "response" }) || ctx.Match("invalidcommanderror", "unknowncommanderror")) + return ctx.Execute(null, m => m.InvalidCommandResponse(ctx)); + if (ctx.MatchMultiple(new[] { "require", "enforce" }, new[] { "tag", "systemtag" }) || ctx.Match("requiretag", "enforcetag")) + return ctx.Execute(null, m => m.RequireSystemTag(ctx)); + if (ctx.MatchMultiple(new[] { "log" }, new[] { "channel" })) + return ctx.Execute(null, m => m.SetLogChannel(ctx)); + if (ctx.MatchMultiple(new[] { "log" }, new[] { "blacklist" })) + { + if (ctx.Match("enable", "on", "add", "deny")) + return ctx.Execute(null, m => m.SetLogBlacklisted(ctx, true)); + else if (ctx.Match("disable", "off", "remove", "allow")) + return ctx.Execute(null, m => m.SetLogBlacklisted(ctx, false)); + else + return ctx.Execute(null, m => m.ShowLogDisabledChannels(ctx)); + } + if (ctx.MatchMultiple(new[] { "proxy", "proxying" }, new[] { "blacklist" })) + { + if (ctx.Match("enable", "on", "add", "deny")) + return ctx.Execute(null, m => m.SetProxyBlacklisted(ctx, true)); + else if (ctx.Match("disable", "off", "remove", "allow")) + return ctx.Execute(null, m => m.SetProxyBlacklisted(ctx, false)); + else + return ctx.Execute(null, m => m.ShowProxyBlacklisted(ctx)); + } + + // todo: maybe add the list of configuration keys here? + return ctx.Reply($"{Emojis.Error} Could not find a setting with that name. Please see `pk;commands serverconfig` for the list of possible config settings."); + } } \ No newline at end of file diff --git a/PluralKit.Bot/CommandSystem/Context/Context.cs b/PluralKit.Bot/CommandSystem/Context/Context.cs index 9cc07d7d..17f5af15 100644 --- a/PluralKit.Bot/CommandSystem/Context/Context.cs +++ b/PluralKit.Bot/CommandSystem/Context/Context.cs @@ -29,11 +29,13 @@ public class Context private Command? _currentCommand; public Context(ILifetimeScope provider, int shardId, Guild? guild, Channel channel, MessageCreateEvent message, - int commandParseOffset, PKSystem senderSystem, SystemConfig config) + int commandParseOffset, PKSystem senderSystem, SystemConfig config, + GuildConfig? guildConfig) { Message = (Message)message; ShardId = shardId; Guild = guild; + GuildConfig = guildConfig; Channel = channel; System = senderSystem; Config = config; @@ -59,11 +61,12 @@ public class Context public readonly Message Message; public readonly Guild Guild; + public readonly GuildConfig? GuildConfig; public readonly int ShardId; public readonly Cluster Cluster; - public Task BotPermissions => Cache.PermissionsIn(Channel.Id); - public Task UserPermissions => Cache.PermissionsFor((MessageCreateEvent)Message); + public Task BotPermissions => Cache.BotPermissionsIn(Guild?.Id ?? 0, Channel.Id); + public Task UserPermissions => Cache.PermissionsForMCE((MessageCreateEvent)Message); public readonly PKSystem System; @@ -100,7 +103,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; @@ -112,8 +115,7 @@ public class Context if (deprecated && commandDef != null) { - await Reply($"{Emojis.Warn} This command has been removed. please use `pk;{commandDef.Key}` instead."); - return; + await Reply($"{Emojis.Warn} Server configuration has moved to `pk;serverconfig`. The command you are trying to run is now `pk;{commandDef.Key}`."); } try 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/ContextAvatarExt.cs b/PluralKit.Bot/CommandSystem/Context/ContextAvatarExt.cs index 825443da..ca2ff912 100644 --- a/PluralKit.Bot/CommandSystem/Context/ContextAvatarExt.cs +++ b/PluralKit.Bot/CommandSystem/Context/ContextAvatarExt.cs @@ -33,11 +33,14 @@ public static class ContextAvatarExt // If we have an attachment, use that if (ctx.Message.Attachments.FirstOrDefault() is { } attachment) { - // XXX: strip query params from attachment URLs because of new Discord CDN shenanigans + // XXX: discord attachment URLs are unable to be validated without their query params + // keep both the URL with query (for validation) and the clean URL (for storage) around var uriBuilder = new UriBuilder(attachment.ProxyUrl); - uriBuilder.Query = ""; - return new ParsedImage { Url = uriBuilder.Uri.AbsoluteUri, Source = AvatarSource.Attachment }; + ParsedImage img = new ParsedImage { Url = uriBuilder.Uri.AbsoluteUri, Source = AvatarSource.Attachment }; + uriBuilder.Query = ""; + img.CleanUrl = uriBuilder.Uri.AbsoluteUri; + return img; } // We should only get here if there are no arguments (which would get parsed as URL + throw if error) @@ -49,6 +52,7 @@ public static class ContextAvatarExt public struct ParsedImage { public string Url; + public string? CleanUrl; public AvatarSource Source; public User? SourceUser; } @@ -57,5 +61,6 @@ public enum AvatarSource { Url, User, - Attachment + Attachment, + HostedCdn } \ No newline at end of file diff --git a/PluralKit.Bot/CommandSystem/Context/ContextEntityArgumentsExt.cs b/PluralKit.Bot/CommandSystem/Context/ContextEntityArgumentsExt.cs index 2a79b7d2..533e374f 100644 --- a/PluralKit.Bot/CommandSystem/Context/ContextEntityArgumentsExt.cs +++ b/PluralKit.Bot/CommandSystem/Context/ContextEntityArgumentsExt.cs @@ -14,7 +14,11 @@ public static class ContextEntityArgumentsExt { var text = ctx.PeekArgument(); if (text.TryParseMention(out var id)) - return await ctx.Cache.GetOrFetchUser(ctx.Rest, id); + { + var user = await ctx.Cache.GetOrFetchUser(ctx.Rest, id); + if (user != null) ctx.PopArgument(); + return user; + } return null; } @@ -53,8 +57,10 @@ public static class ContextEntityArgumentsExt return await ctx.Repository.GetSystemByAccount(id); // Finally, try HID parsing - var system = await ctx.Repository.GetSystemByHid(input); - return system; + if (input.TryParseHid(out var hid)) + return await ctx.Repository.GetSystemByHid(hid); + + return null; } public static async Task PeekMember(this Context ctx, SystemId? restrictToSystem = null) @@ -83,7 +89,7 @@ public static class ContextEntityArgumentsExt // Finally (or if by-HID lookup is specified), check if input is a valid HID and then try member HID parsing: - if (!Regex.IsMatch(input, @"^[a-zA-Z]{5}$")) + if (!input.TryParseHid(out var hid)) return null; // For posterity: @@ -94,21 +100,21 @@ public static class ContextEntityArgumentsExt PKMember memberByHid = null; if (restrictToSystem != null) { - memberByHid = await ctx.Repository.GetMemberByHid(input, restrictToSystem); + memberByHid = await ctx.Repository.GetMemberByHid(hid, restrictToSystem); if (memberByHid != null) return memberByHid; } // otherwise we try the querier's system and if that doesn't work we do global else { - memberByHid = await ctx.Repository.GetMemberByHid(input, ctx.System?.Id); + memberByHid = await ctx.Repository.GetMemberByHid(hid, ctx.System?.Id); if (memberByHid != null) return memberByHid; // ff ctx.System was null then this would be a duplicate of above and we don't want to run it again if (ctx.System != null) { - memberByHid = await ctx.Repository.GetMemberByHid(input); + memberByHid = await ctx.Repository.GetMemberByHid(hid); if (memberByHid != null) return memberByHid; } @@ -148,7 +154,10 @@ public static class ContextEntityArgumentsExt return byDisplayName; } - if (await ctx.Repository.GetGroupByHid(input, restrictToSystem) is { } byHid) + if (!input.TryParseHid(out var hid)) + return null; + + if (await ctx.Repository.GetGroupByHid(hid, restrictToSystem) is { } byHid) return byHid; return null; @@ -164,17 +173,18 @@ public static class ContextEntityArgumentsExt public static string CreateNotFoundError(this Context ctx, string entity, string input) { var isIDOnlyQuery = ctx.System == null || ctx.MatchFlag("id", "by-id"); + var inputIsHid = HidUtils.ParseHid(input) != null; if (isIDOnlyQuery) { - if (input.Length == 5) + if (inputIsHid) return $"{entity} with ID \"{input}\" not found."; - return $"{entity} not found. Note that a {entity.ToLower()} ID is 5 characters long."; + return $"{entity} not found. Note that a {entity.ToLower()} ID is 5 or 6 characters long."; } - if (input.Length == 5) + if (inputIsHid) return $"{entity} with ID or name \"{input}\" not found."; - return $"{entity} with name \"{input}\" not found. Note that a {entity.ToLower()} ID is 5 characters long."; + return $"{entity} with name \"{input}\" not found. Note that a {entity.ToLower()} ID is 5 or 6 characters long."; } public static async Task MatchChannel(this Context ctx) @@ -182,7 +192,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/CommandSystem/Context/ContextPrivacyExt.cs b/PluralKit.Bot/CommandSystem/Context/ContextPrivacyExt.cs index ed7b2621..2f120f8a 100644 --- a/PluralKit.Bot/CommandSystem/Context/ContextPrivacyExt.cs +++ b/PluralKit.Bot/CommandSystem/Context/ContextPrivacyExt.cs @@ -6,10 +6,10 @@ public static class ContextPrivacyExt { public static PrivacyLevel PopPrivacyLevel(this Context ctx) { - if (ctx.Match("public", "show", "shown", "visible")) + if (ctx.Match("public", "pub", "show", "shown", "visible", "unhide", "unhidden")) return PrivacyLevel.Public; - if (ctx.Match("private", "hide", "hidden")) + if (ctx.Match("private", "priv", "hide", "hidden")) return PrivacyLevel.Private; if (!ctx.HasNext()) @@ -33,7 +33,7 @@ public static class ContextPrivacyExt { if (!MemberPrivacyUtils.TryParseMemberPrivacy(ctx.PeekArgument(), out var subject)) throw new PKSyntaxError( - $"Invalid privacy subject {ctx.PopArgument().AsCode()} (must be `name`, `description`, `avatar`, `birthday`, `pronouns`, `metadata`, `visibility`, or `all`)."); + $"Invalid privacy subject {ctx.PopArgument().AsCode()} (must be `name`, `description`, `avatar`, `birthday`, `pronouns`, `proxy`, `metadata`, `visibility`, or `all`)."); ctx.PopArgument(); return subject; diff --git a/PluralKit.Bot/Commands/Admin.cs b/PluralKit.Bot/Commands/Admin.cs index 374ab485..442e2205 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,95 @@ 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; + } + + private 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)); + } + + 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 ""; + } + + 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 CreateAbuseLogEmbed(Context ctx, AbuseLog abuseLog) + { + // Fetch/render info for all accounts simultaneously + var accounts = await ctx.Repository.GetAbuseLogAccounts(abuseLog.Id); + var systems = await Task.WhenAll(accounts.Select(x => ctx.Repository.GetSystemByAccount(x))); + var users = (await GetUsers(accounts)).Select(x => x.User?.NameAndMention() ?? $"(deleted: `{x.Id}`)"); + + List flagstr = new(); + if (abuseLog.DenyBotUsage) + flagstr.Add("- bot usage denied"); + + var eb = new EmbedBuilder() + .Title($"Abuse log: {abuseLog.Uuid.ToString()}") + .Color(DiscordUtils.Red) + .Footer(new Embed.EmbedFooter($"Created on {abuseLog.Created.FormatZoned(ctx.Zone)}")); + + if (systems.Any(x => x != null)) + { + var sysList = string.Join(", ", systems.Select(x => $"`{x.DisplayHid()}`")); + eb.Field(new Embed.Field($"{Emojis.Warn} Accounts have registered system(s)", sysList)); + } + + eb.Field(new Embed.Field("Accounts", string.Join("\n", users).Truncate(1000), true)); + eb.Field(new Embed.Field("Flags", flagstr.Any() ? string.Join("\n", flagstr) : "(none)", true)); + + if (abuseLog.Description != null) + eb.Field(new Embed.Field("Description", abuseLog.Description.Truncate(1000))); + + return eb.Build(); } public async Task UpdateSystemId(Context ctx) @@ -29,14 +117,16 @@ public class Admin if (target == null) throw new PKError("Unknown system."); - var newHid = ctx.PopArgument(); - if (!Regex.IsMatch(newHid, "^[a-z]{5}$")) - throw new PKError($"Invalid new system ID `{newHid}`."); + var input = ctx.PopArgument(); + if (!input.TryParseHid(out var newHid)) + throw new PKError($"Invalid new system ID `{input}`."); var existingSystem = await ctx.Repository.GetSystemByHid(newHid); if (existingSystem != null) throw new PKError($"Another system already exists with ID `{newHid}`."); + 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."); @@ -52,14 +142,17 @@ public class Admin if (target == null) throw new PKError("Unknown member."); - var newHid = ctx.PopArgument(); - if (!Regex.IsMatch(newHid, "^[a-z]{5}$")) - throw new PKError($"Invalid new member ID `{newHid}`."); + var input = ctx.PopArgument(); + if (!input.TryParseHid(out var newHid)) + throw new PKError($"Invalid new member ID `{input}`."); var existingMember = await ctx.Repository.GetMemberByHid(newHid); if (existingMember != null) throw new PKError($"Another member already exists with ID `{newHid}`."); + 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" @@ -78,14 +171,17 @@ public class Admin if (target == null) throw new PKError("Unknown group."); - var newHid = ctx.PopArgument(); - if (!Regex.IsMatch(newHid, "^[a-z]{5}$")) - throw new PKError($"Invalid new group ID `{newHid}`."); + var input = ctx.PopArgument(); + if (!input.TryParseHid(out var newHid)) + throw new PKError($"Invalid new group ID `{input}`."); var existingGroup = await ctx.Repository.GetGroupByHid(newHid); if (existingGroup != null) throw new PKError($"Another group already exists with ID `{newHid}`."); + 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 +199,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 +222,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 +249,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 +280,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 +288,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 +309,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 +317,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."); @@ -240,9 +346,10 @@ public class Admin var existingAccount = await ctx.Repository.GetSystemByAccount(account.Id); if (existingAccount != null) - throw Errors.AccountInOtherSystem(existingAccount); + 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."); @@ -266,4 +373,127 @@ public class Admin Color = DiscordUtils.Green, }); } + + public async Task SystemDelete(Context ctx) + { + ctx.AssertBotAdmin(); + + var target = await ctx.MatchSystem(); + if (target == null) + throw new PKError("Unknown system."); + + await ctx.Reply($"To delete the following system, reply with the system's UUID: `{target.Uuid.ToString()}`", + await CreateEmbed(ctx, target)); + if (!await ctx.ConfirmWithReply(target.Uuid.ToString())) + throw new PKError("System deletion cancelled."); + + await ctx.BusyIndicator(async () => + await ctx.Repository.DeleteSystem(target.Id)); + await ctx.Reply($"{Emojis.Success} System deletion succesful."); + } + + public async Task AbuseLogCreate(Context ctx) + { + var denyBotUsage = ctx.MatchFlag("deny", "deny-bot-usage"); + var account = await ctx.MatchUser(); + if (account == null) + throw new PKError("You must pass an account to associate the abuse log with (either ID or @mention)."); + + string? desc = null!; + if (ctx.HasNext(false)) + desc = ctx.RemainderOrNull(false).NormalizeLineEndSpacing(); + + var abuseLog = await ctx.Repository.CreateAbuseLog(desc, denyBotUsage); + await ctx.Repository.AddAbuseLogAccount(abuseLog.Id, account.Id); + + await ctx.Reply( + $"Created new abuse log with UUID `{abuseLog.Uuid.ToString()}`.", + await CreateAbuseLogEmbed(ctx, abuseLog)); + } + + public async Task AbuseLogShow(Context ctx, AbuseLog abuseLog) + { + await ctx.Reply(null, await CreateAbuseLogEmbed(ctx, abuseLog)); + } + + public async Task AbuseLogFlagDeny(Context ctx, AbuseLog abuseLog) + { + if (!ctx.HasNext()) + { + await ctx.Reply( + $"Bot usage is currently {(abuseLog.DenyBotUsage ? "denied" : "allowed")} " + + $"for accounts associated with abuse log `{abuseLog.Uuid}`."); + } + else + { + var value = ctx.MatchToggle(true); + if (abuseLog.DenyBotUsage != value) + await ctx.Repository.UpdateAbuseLog(abuseLog.Id, new AbuseLogPatch { DenyBotUsage = value }); + + await ctx.Reply( + $"Bot usage is now **{(value ? "denied" : "allowed")}** " + + $"for accounts associated with abuse log `{abuseLog.Uuid}`."); + } + } + + public async Task AbuseLogDescription(Context ctx, AbuseLog abuseLog) + { + if (ctx.MatchClear() && await ctx.ConfirmClear("this abuse log description")) + { + await ctx.Repository.UpdateAbuseLog(abuseLog.Id, new AbuseLogPatch { Description = null }); + await ctx.Reply($"{Emojis.Success} Abuse log description cleared."); + } + else if (ctx.HasNext()) + { + var desc = ctx.RemainderOrNull(false).NormalizeLineEndSpacing(); + await ctx.Repository.UpdateAbuseLog(abuseLog.Id, new AbuseLogPatch { Description = desc }); + await ctx.Reply($"{Emojis.Success} Abuse log description updated."); + } + else + { + var eb = new EmbedBuilder() + .Description($"Showing description for abuse log `{abuseLog.Uuid}`"); + await ctx.Reply(abuseLog.Description, eb.Build()); + } + } + + public async Task AbuseLogAddUser(Context ctx, AbuseLog abuseLog) + { + var account = await ctx.MatchUser(); + if (account == null) + throw new PKError("You must pass an account to associate the abuse log with (either ID or @mention)."); + + await ctx.Repository.AddAbuseLogAccount(abuseLog.Id, account.Id); + await ctx.Reply( + $"Added user {account.NameAndMention()} to the abuse log with UUID `{abuseLog.Uuid.ToString()}`.", + await CreateAbuseLogEmbed(ctx, abuseLog)); + } + + public async Task AbuseLogRemoveUser(Context ctx, AbuseLog abuseLog) + { + var account = await ctx.MatchUser(); + if (account == null) + throw new PKError("You must pass an account to remove from the abuse log (either ID or @mention)."); + + await ctx.Repository.UpdateAccount(account.Id, new() + { + AbuseLog = null, + }); + + await ctx.Reply( + $"Removed user {account.NameAndMention()} from the abuse log with UUID `{abuseLog.Uuid.ToString()}`.", + await CreateAbuseLogEmbed(ctx, abuseLog)); + } + + public async Task AbuseLogDelete(Context ctx, AbuseLog abuseLog) + { + if (!await ctx.PromptYesNo($"Really delete abuse log entry `{abuseLog.Uuid}`?", "Delete", matchFlag: false)) + { + await ctx.Reply($"{Emojis.Error} Deletion cancelled."); + return; + } + + await ctx.Repository.DeleteAbuseLog(abuseLog.Id); + await ctx.Reply($"{Emojis.Success} Successfully deleted abuse log entry."); + } } \ No newline at end of file diff --git a/PluralKit.Bot/Commands/Api.cs b/PluralKit.Bot/Commands/Api.cs index f4ac4caa..0f612f40 100644 --- a/PluralKit.Bot/Commands/Api.cs +++ b/PluralKit.Bot/Commands/Api.cs @@ -143,25 +143,33 @@ public class Api if (_webhookRegex.IsMatch(newUrl)) throw new PKError("PluralKit does not currently support setting a Discord webhook URL as your system's webhook URL."); - try - { - await _dispatch.DoPostRequest(ctx.System.Id, newUrl, null, true); - } - catch (Exception e) - { - throw new PKError($"Could not verify that the new URL is working: {e.Message}"); - } - var newToken = StringUtils.GenerateToken(); + await ctx.Reply($"{Emojis.Warn} The following token is used to authenticate requests from PluralKit to you." + + " If it is exposed publicly, you **must** clear and re-set the webhook URL to get a new token." + + "\n\n**Please review the security requirements at before continuing.**" + + "\n\nWhen the server is correctly validating the token, click or reply 'yes' to continue." + ); + if (!await ctx.PromptYesNo(newToken, "Continue", matchFlag: false)) + throw Errors.GenericCancelled(); + + var status = await _dispatch.TestUrl(ctx.System.Uuid, newUrl, newToken); + if (status != "OK") + { + var message = status switch + { + "BadData" => "the webhook url is invalid", + "NoIPs" => "could not find any valid IP addresses for the provided domain", + "InvalidIP" => "could not find any valid IP addresses for the provided domain", + "FetchFailed" => "unable to reach server", + "TestFailed" => "server failed to validate the signing token", + _ => $"an unknown error occurred ({status})" + }; + throw new PKError($"Failed to validate the webhook url: {message}"); + } + await ctx.Repository.UpdateSystem(ctx.System.Id, new SystemPatch { WebhookUrl = newUrl, WebhookToken = newToken }); - await ctx.Reply($"{Emojis.Success} Successfully the new webhook URL for your system." - + $"\n\n{Emojis.Warn} The following token is used to authenticate requests from PluralKit to you." - + " If it leaks, you should clear and re-set the webhook URL to get a new token." - + "\ntodo: add link to docs or something" - ); - - await ctx.Reply(newToken); + await ctx.Reply($"{Emojis.Success} Successfully the new webhook URL for your system."); } } \ No newline at end of file diff --git a/PluralKit.Bot/Commands/Autoproxy.cs b/PluralKit.Bot/Commands/Autoproxy.cs index c6bdc889..59bc3481 100644 --- a/PluralKit.Bot/Commands/Autoproxy.cs +++ b/PluralKit.Bot/Commands/Autoproxy.cs @@ -130,7 +130,7 @@ public class Autoproxy { if (relevantMember == null) throw new ArgumentException("Attempted to print member autoproxy status, but the linked member ID wasn't found in the database. Should be handled appropriately."); - eb.Description($"Autoproxy is currently set to **front mode** in this server. The current (first) fronter is **{relevantMember.NameFor(ctx).EscapeMarkdown()}** (`{relevantMember.Hid}`). To disable, type `pk;autoproxy off`."); + eb.Description($"Autoproxy is currently set to **front mode** in this server. The current (first) fronter is **{relevantMember.NameFor(ctx).EscapeMarkdown()}** (`{relevantMember.DisplayHid(ctx.Config)}`). To disable, type `pk;autoproxy off`."); } break; @@ -142,7 +142,7 @@ public class Autoproxy // ideally we would set it to off in the database though... eb.Description($"Autoproxy is currently **off** in this server. To enable it, use one of the following commands:\n{commandList}"); else - eb.Description($"Autoproxy is active for member **{relevantMember.NameFor(ctx)}** (`{relevantMember.Hid}`) in this server. To disable, type `pk;autoproxy off`."); + eb.Description($"Autoproxy is active for member **{relevantMember.NameFor(ctx)}** (`{relevantMember.DisplayHid(ctx.Config)}`) in this server. To disable, type `pk;autoproxy off`."); break; } @@ -150,7 +150,7 @@ public class Autoproxy if (relevantMember == null) eb.Description("Autoproxy is currently set to **latch mode**, meaning the *last-proxied member* will be autoproxied. **No member is currently latched.** To disable, type `pk;autoproxy off`."); else - eb.Description($"Autoproxy is currently set to **latch mode**, meaning the *last-proxied member* will be autoproxied. The currently latched member is **{relevantMember.NameFor(ctx)}** (`{relevantMember.Hid}`). To disable, type `pk;autoproxy off`."); + eb.Description($"Autoproxy is currently set to **latch mode**, meaning the *last-proxied member* will be autoproxied. The currently latched member is **{relevantMember.NameFor(ctx)}** (`{relevantMember.DisplayHid(ctx.Config)}`). To disable, type `pk;autoproxy off`."); break; diff --git a/PluralKit.Bot/Commands/Checks.cs b/PluralKit.Bot/Commands/Checks.cs index a7d61660..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 = PermissionExtensions.PermissionsFor(guild, channel, _botConfig.ClientId, guildMember); + // 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,15 +233,21 @@ 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(); + if (await ctx.Repository.GetSystemByAccount(msg.Author.Id) == null) + { + await ctx.Reply("Your account does not have a system registered."); + return; + } + // for now this is just server var autoproxySettings = await ctx.Repository.GetAutoproxySettings(ctx.System.Id, channel.GuildId.Value, null); diff --git a/PluralKit.Bot/Commands/Config.cs b/PluralKit.Bot/Commands/Config.cs index 37098c97..fd01bb6a 100644 --- a/PluralKit.Bot/Commands/Config.cs +++ b/PluralKit.Bot/Commands/Config.cs @@ -97,11 +97,46 @@ public class Config items.Add(new( "Proxy error", - "Whether to send an error message when proxying fails.", + "Whether to send an error message when proxying fails", EnabledDisabled(ctx.Config.ProxyErrorMessageEnabled), "enabled" )); + items.Add(new( + "Split IDs", + "Whether to display 6-character IDs split with a hyphen, to ease readability", + EnabledDisabled(ctx.Config.HidDisplaySplit), + "disabled" + )); + + items.Add(new( + "Capitalize IDs", + "Whether to display IDs as capital letters, to ease readability", + EnabledDisabled(ctx.Config.HidDisplayCaps), + "disabled" + )); + + items.Add(new( + "Pad IDs", + "Whether to pad 5-character IDs in lists (left/right)", + ctx.Config.HidListPadding.ToUserString(), + "off" + )); + + items.Add(new( + "Proxy Switch", + "Whether using a proxy tag logs a switch", + EnabledDisabled(ctx.Config.ProxySwitch), + "disabled" + )); + + items.Add(new( + "Name Format", + "Format string used to display a member's name https://pluralkit.me/guide/#setting-a-custom-name-format", + ctx.Config.NameFormat, + ProxyMember.DefaultFormat + )); + await ctx.Paginate( items.ToAsyncEnumerable(), items.Count, @@ -443,4 +478,115 @@ public class Config await ctx.Reply("Proxy error messages are now disabled. Messages that fail to proxy (due to message or attachment size) will not throw an error message."); } } + + public async Task HidDisplaySplit(Context ctx) + { + if (!ctx.HasNext()) + { + var msg = $"Splitting of 6-character IDs with a hyphen is currently **{EnabledDisabled(ctx.Config.HidDisplaySplit)}**."; + await ctx.Reply(msg); + return; + } + + var newVal = ctx.MatchToggle(false); + await ctx.Repository.UpdateSystemConfig(ctx.System.Id, new() { HidDisplaySplit = newVal }); + await ctx.Reply($"Splitting of 6-character IDs with a hyphen is now {EnabledDisabled(newVal)}."); + } + + public async Task HidDisplayCaps(Context ctx) + { + if (!ctx.HasNext()) + { + var msg = $"Displaying IDs as capital letters is currently **{EnabledDisabled(ctx.Config.HidDisplayCaps)}**."; + await ctx.Reply(msg); + return; + } + + var newVal = ctx.MatchToggle(false); + await ctx.Repository.UpdateSystemConfig(ctx.System.Id, new() { HidDisplayCaps = newVal }); + await ctx.Reply($"Displaying IDs as capital letters is now {EnabledDisabled(newVal)}."); + } + + public async Task HidListPadding(Context ctx) + { + if (!ctx.HasNext()) + { + string message; + switch (ctx.Config.HidListPadding) + { + case SystemConfig.HidPadFormat.None: message = "Padding 5-character IDs in lists is currently disabled."; break; + case SystemConfig.HidPadFormat.Left: message = "5-character IDs displayed in lists will have a padding space added to the beginning."; break; + case SystemConfig.HidPadFormat.Right: message = "5-character IDs displayed in lists will have a padding space added to the end."; break; + default: throw new Exception("unreachable"); + } + await ctx.Reply(message); + return; + } + + var badInputError = "Valid padding settings are `left`, `right`, or `off`."; + + var toggleOff = ctx.MatchToggleOrNull(false); + + switch (toggleOff) + { + case true: throw new PKError(badInputError); + case false: + { + await ctx.Repository.UpdateSystemConfig(ctx.System.Id, new() { HidListPadding = SystemConfig.HidPadFormat.None }); + await ctx.Reply("Padding 5-character IDs in lists has been disabled."); + return; + } + } + + if (ctx.Match("left", "l")) + { + await ctx.Repository.UpdateSystemConfig(ctx.System.Id, new() { HidListPadding = SystemConfig.HidPadFormat.Left }); + await ctx.Reply("5-character IDs displayed in lists will now have a padding space added to the beginning."); + } + else if (ctx.Match("right", "r")) + { + await ctx.Repository.UpdateSystemConfig(ctx.System.Id, new() { HidListPadding = SystemConfig.HidPadFormat.Right }); + await ctx.Reply("5-character IDs displayed in lists will now have a padding space added to the end."); + } + else throw new PKError(badInputError); + } + + public async Task ProxySwitch(Context ctx) + { + if (!ctx.HasNext()) + { + var msg = $"Logging a switch every time a proxy tag is used is currently **{EnabledDisabled(ctx.Config.ProxySwitch)}**."; + await ctx.Reply(msg); + return; + } + + var newVal = ctx.MatchToggle(false); + await ctx.Repository.UpdateSystemConfig(ctx.System.Id, new() { ProxySwitch = newVal }); + await ctx.Reply($"Logging a switch every time a proxy tag is used is now {EnabledDisabled(newVal)}."); + } + + public async Task NameFormat(Context ctx) + { + var clearFlag = ctx.MatchClear(); + if (!ctx.HasNext() && !clearFlag) + { + await ctx.Reply($"Member names are currently formatted as `{ctx.Config.NameFormat ?? ProxyMember.DefaultFormat}`"); + return; + } + + string formatString; + if (clearFlag) + formatString = ProxyMember.DefaultFormat; + else + formatString = ctx.RemainderOrNull(); + + await ctx.Repository.UpdateSystemConfig(ctx.System.Id, new() { NameFormat = formatString }); + await ctx.Reply($"Member names are now formatted as `{formatString}`"); + } + + 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 0df8c7e1..761d1e54 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.Hid}`] **{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 }))) @@ -109,6 +123,9 @@ public class GroupMember toAction = members .Where(m => existingMembersInGroup.Contains(m.Value)) .ToList(); + + if (ctx.MatchFlag("all", "a") && !await ctx.PromptYesNo($"Are you sure you want to remove all members from group {target.Reference(ctx)}?", "Empty Group")) throw Errors.GenericCancelled(); + await ctx.Repository.RemoveMembersFromGroup(target.Id, toAction); } else @@ -127,26 +144,26 @@ 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.Hid}`) in "); + var title = new StringBuilder($"Members of {target.DisplayName ?? target.Name} (`{target.DisplayHid(ctx.Config)}`) in "); if (ctx.Guild != null) { var guildSettings = await ctx.Repository.GetSystemGuild(ctx.Guild.Id, targetSystem.Id); if (guildSettings.DisplayName != null) - title.Append($"{guildSettings.DisplayName} (`{targetSystem.Hid}`)"); + title.Append($"{guildSettings.DisplayName} (`{targetSystem.DisplayHid(ctx.Config)}`)"); else if (targetSystem.NameFor(ctx) != null) - title.Append($"{targetSystem.NameFor(ctx)} (`{targetSystem.Hid}`)"); + title.Append($"{targetSystem.NameFor(ctx)} (`{targetSystem.DisplayHid(ctx.Config)}`)"); else - title.Append($"`{targetSystem.Hid}`"); + title.Append($"`{targetSystem.DisplayHid(ctx.Config)}`"); } else { if (targetSystem.NameFor(ctx) != null) - title.Append($"{targetSystem.NameFor(ctx)} (`{targetSystem.Hid}`)"); + title.Append($"{targetSystem.NameFor(ctx)} (`{targetSystem.DisplayHid(ctx.Config)}`)"); else - title.Append($"`{targetSystem.Hid}`"); + title.Append($"`{targetSystem.DisplayHid(ctx.Config)}`"); } if (opts.Search != null) title.Append($" matching **{opts.Search.Truncate(100)}**"); diff --git a/PluralKit.Bot/Commands/Groups.cs b/PluralKit.Bot/Commands/Groups.cs index 4041f17c..a6105275 100644 --- a/PluralKit.Bot/Commands/Groups.cs +++ b/PluralKit.Bot/Commands/Groups.cs @@ -21,13 +21,15 @@ public class Groups private readonly HttpClient _client; private readonly DispatchService _dispatch; private readonly EmbedService _embeds; + private readonly AvatarHostingService _avatarHosting; public Groups(EmbedService embeds, HttpClient client, - DispatchService dispatch) + DispatchService dispatch, AvatarHostingService avatarHosting) { _embeds = embeds; _client = client; _dispatch = dispatch; + _avatarHosting = avatarHosting; } public async Task CreateGroup(Context ctx) @@ -44,14 +46,14 @@ public class Groups var groupLimit = ctx.Config.GroupLimitOverride ?? Limits.MaxGroupCount; if (existingGroupCount >= groupLimit) throw new PKError( - $"System has reached the maximum number of groups ({groupLimit}). Please delete unused groups first in order to create new ones."); + $"System has reached the maximum number of groups ({groupLimit}). If you need to add more groups, you can either delete existing groups, or ask for your limit to be raised in the PluralKit support server: "); // Warn if there's already a group by this name var existingGroup = await ctx.Repository.GetGroupByName(ctx.System.Id, groupName); if (existingGroup != null) { var msg = - $"{Emojis.Warn} You already have a group in your system with the name \"{existingGroup.Name}\" (with ID `{existingGroup.Hid}`). Do you want to create another group with the same name?"; + $"{Emojis.Warn} You already have a group in your system with the name \"{existingGroup.Name}\" (with ID `{existingGroup.DisplayHid(ctx.Config)}`). Do you want to create another group with the same name?"; if (!await ctx.PromptYesNo(msg, "Create")) throw new PKError("Group creation cancelled."); } @@ -81,7 +83,7 @@ public class Groups var eb = new EmbedBuilder() .Description( - $"Your new group, **{groupName}**, has been created, with the group ID **`{newGroup.Hid}`**.\nBelow are a couple of useful commands:") + $"Your new group, **{groupName}**, has been created, with the group ID **`{newGroup.DisplayHid(ctx.Config)}`**.\nBelow are a couple of useful commands:") .Field(new Embed.Field("View the group card", $"> pk;group **{reference}**")) .Field(new Embed.Field("Add members to the group", $"> pk;group **{reference}** add **MemberName**\n> pk;group **{reference}** add **Member1** **Member2** **Member3** (and so on...)")) @@ -93,7 +95,7 @@ public class Groups if (existingGroupCount >= Limits.WarnThreshold(groupLimit)) await ctx.Reply( - $"{Emojis.Warn} You are approaching the per-system group limit ({existingGroupCount} / {groupLimit} groups). Please review your group list for unused or duplicate groups."); + $"{Emojis.Warn} You are approaching the per-system group limit ({existingGroupCount} / {groupLimit} groups). Once you reach this limit, you will be unable to create new groups until existing groups are deleted, or you can ask for your limit to be raised in the PluralKit support server: "); } public async Task RenameGroup(Context ctx, PKGroup target) @@ -111,7 +113,7 @@ public class Groups if (existingGroup != null && existingGroup.Id != target.Id) { var msg = - $"{Emojis.Warn} You already have a group in your system with the name \"{existingGroup.Name}\" (with ID `{existingGroup.Hid}`). Do you want to rename this group to that name too?"; + $"{Emojis.Warn} You already have a group in your system with the name \"{existingGroup.Name}\" (with ID `{existingGroup.DisplayHid(ctx.Config)}`). Do you want to rename this group to that name too?"; if (!await ctx.PromptYesNo(msg, "Rename")) throw new PKError("Group rename cancelled."); } @@ -130,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.NameFor(ctx)} (`{target.DisplayHid(ctx.Config)}`)"); + 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; } @@ -182,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); @@ -199,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.NameFor(ctx)} (`{target.DisplayHid(ctx.Config)}`)"); + 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; } @@ -251,6 +273,7 @@ public class Groups { async Task ClearIcon() { + await ctx.ConfirmClear("this group's icon"); ctx.CheckOwnGroup(target); await ctx.Repository.UpdateGroup(target.Id, new GroupPatch { Icon = null }); @@ -261,22 +284,24 @@ public class Groups { ctx.CheckOwnGroup(target); + img = await _avatarHosting.TryRehostImage(img, AvatarHostingService.RehostedImageType.Avatar, ctx.Author.Id, ctx.System); await AvatarUtils.VerifyAvatarOrThrow(_client, img.Url); - await ctx.Repository.UpdateGroup(target.Id, new GroupPatch { Icon = img.Url }); + await ctx.Repository.UpdateGroup(target.Id, new GroupPatch { Icon = img.CleanUrl ?? img.Url }); var msg = img.Source switch { AvatarSource.User => $"{Emojis.Success} Group icon changed to {img.SourceUser?.Username}'s avatar!\n{Emojis.Warn} If {img.SourceUser?.Username} changes their avatar, the group icon will need to be re-set.", AvatarSource.Url => $"{Emojis.Success} Group icon changed to the image at the given URL.", + AvatarSource.HostedCdn => $"{Emojis.Success} Group icon changed to attached image.", AvatarSource.Attachment => $"{Emojis.Success} Group icon changed to attached image.\n{Emojis.Warn} If you delete the message containing the attachment, the group icon will stop working.", _ => throw new ArgumentOutOfRangeException() }; // The attachment's already right there, no need to preview it. - var hasEmbed = img.Source != AvatarSource.Attachment; + var hasEmbed = img.Source != AvatarSource.Attachment && img.Source != AvatarSource.HostedCdn; await (hasEmbed ? ctx.Reply(msg, new EmbedBuilder().Image(new Embed.EmbedImage(img.Url)).Build()) : ctx.Reply(msg)); @@ -300,11 +325,11 @@ public class Groups else { throw new PKSyntaxError( - "This group does not have an icon set. Set one by attaching an image to this command, or by passing an image URL or @mention."); + "This group does not have an avatar set. Set one by attaching an image to this command, or by passing an image URL or @mention."); } } - if (ctx.MatchClear() && await ctx.ConfirmClear("this group's icon")) + if (ctx.MatchClear()) await ClearIcon(); else if (await ctx.MatchImage() is { } img) await SetIcon(img); @@ -316,6 +341,7 @@ public class Groups { async Task ClearBannerImage() { + await ctx.ConfirmClear("this group's banner image"); ctx.CheckOwnGroup(target); await ctx.Repository.UpdateGroup(target.Id, new GroupPatch { BannerImage = null }); @@ -326,13 +352,15 @@ public class Groups { ctx.CheckOwnGroup(target); + img = await _avatarHosting.TryRehostImage(img, AvatarHostingService.RehostedImageType.Banner, ctx.Author.Id, ctx.System); await AvatarUtils.VerifyAvatarOrThrow(_client, img.Url, true); - await ctx.Repository.UpdateGroup(target.Id, new GroupPatch { BannerImage = img.Url }); + await ctx.Repository.UpdateGroup(target.Id, new GroupPatch { BannerImage = img.CleanUrl ?? img.Url }); var msg = img.Source switch { AvatarSource.Url => $"{Emojis.Success} Group banner image changed to the image at the given URL.", + AvatarSource.HostedCdn => $"{Emojis.Success} Group banner image changed to attached image.", AvatarSource.Attachment => $"{Emojis.Success} Group banner image changed to attached image.\n{Emojis.Warn} If you delete the message containing the attachment, the banner image will stop working.", AvatarSource.User => throw new PKError("Cannot set a banner image to an user's avatar."), @@ -340,7 +368,7 @@ public class Groups }; // The attachment's already right there, no need to preview it. - var hasEmbed = img.Source != AvatarSource.Attachment; + var hasEmbed = img.Source != AvatarSource.Attachment && img.Source != AvatarSource.HostedCdn; await (hasEmbed ? ctx.Reply(msg, new EmbedBuilder().Image(new Embed.EmbedImage(img.Url)).Build()) : ctx.Reply(msg)); @@ -348,7 +376,7 @@ public class Groups async Task ShowBannerImage() { - ctx.CheckSystemPrivacy(target.System, target.DescriptionPrivacy); + ctx.CheckSystemPrivacy(target.System, target.BannerPrivacy); if ((target.BannerImage?.Trim() ?? "").Length > 0) { @@ -364,11 +392,11 @@ public class Groups else { throw new PKSyntaxError( - "This group does not have a banner image set. Set one by attaching an image to this command, or by passing an image URL or @mention."); + "This group does not have a banner image set. Set one by attaching an image to this command, or by passing an image URL."); } } - if (ctx.MatchClear() && await ctx.ConfirmClear("this group's banner image")) + if (ctx.MatchClear()) await ClearBannerImage(); else if (await ctx.MatchImage() is { } img) await SetBannerImage(img); @@ -379,7 +407,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)) @@ -387,8 +415,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") @@ -440,24 +470,24 @@ 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, - GetEmbedTitle(system, opts), + GetEmbedTitle(ctx, system, opts), system.Color, opts ); } - private string GetEmbedTitle(PKSystem target, ListOptions opts) + private string GetEmbedTitle(Context ctx, PKSystem target, ListOptions opts) { var title = new StringBuilder("Groups of "); - if (target.Name != null) - title.Append($"{target.Name} (`{target.Hid}`)"); + if (target.NameFor(ctx) != null) + title.Append($"{target.NameFor(ctx)} (`{target.DisplayHid(ctx.Config)}`)"); else - title.Append($"`{target.Hid}`"); + title.Append($"`{target.DisplayHid(ctx.Config)}`"); if (opts.Search != null) title.Append($" matching **{opts.Search}**"); @@ -481,12 +511,13 @@ public class Groups .Title($"Current privacy settings for {target.Name}") .Field(new Embed.Field("Name", target.NamePrivacy.Explanation())) .Field(new Embed.Field("Description", target.DescriptionPrivacy.Explanation())) + .Field(new Embed.Field("Banner", target.BannerPrivacy.Explanation())) .Field(new Embed.Field("Icon", target.IconPrivacy.Explanation())) .Field(new Embed.Field("Member list", target.ListPrivacy.Explanation())) .Field(new Embed.Field("Metadata (creation date)", target.MetadataPrivacy.Explanation())) .Field(new Embed.Field("Visibility", target.Visibility.Explanation())) .Description( - $"To edit privacy settings, use the command:\n> pk;group **{target.Reference(ctx)}** privacy **** ****\n\n- `subject` is one of `name`, `description`, `icon`, `members`, `metadata`, `visibility`, or `all`\n- `level` is either `public` or `private`.") + $"To edit privacy settings, use the command:\n> pk;group **{target.Reference(ctx)}** privacy **** ****\n\n- `subject` is one of `name`, `description`, `banner`, `icon`, `members`, `metadata`, `visibility`, or `all`\n- `level` is either `public` or `private`.") .Build()); return; } @@ -511,6 +542,7 @@ public class Groups { GroupPrivacySubject.Name => "name privacy", GroupPrivacySubject.Description => "description privacy", + GroupPrivacySubject.Banner => "banner privacy", GroupPrivacySubject.Icon => "icon privacy", GroupPrivacySubject.List => "member list", GroupPrivacySubject.Metadata => "metadata", @@ -524,6 +556,8 @@ public class Groups "This group's name is now hidden from other systems, and will be replaced by the group's display name.", (GroupPrivacySubject.Description, PrivacyLevel.Private) => "This group's description is now hidden from other systems.", + (GroupPrivacySubject.Banner, PrivacyLevel.Private) => + "This group's banner is now hidden from other systems.", (GroupPrivacySubject.Icon, PrivacyLevel.Private) => "This group's icon is now hidden from other systems.", (GroupPrivacySubject.Visibility, PrivacyLevel.Private) => @@ -537,6 +571,8 @@ public class Groups "This group's name is no longer hidden from other systems.", (GroupPrivacySubject.Description, PrivacyLevel.Public) => "This group's description is no longer hidden from other systems.", + (GroupPrivacySubject.Banner, PrivacyLevel.Public) => + "This group's banner is no longer hidden from other systems.", (GroupPrivacySubject.Icon, PrivacyLevel.Public) => "This group's icon is no longer hidden from other systems.", (GroupPrivacySubject.Visibility, PrivacyLevel.Public) => @@ -568,10 +604,10 @@ public class Groups ctx.CheckOwnGroup(target); await ctx.Reply( - $"{Emojis.Warn} Are you sure you want to delete this group? If so, reply to this message with the group's ID (`{target.Hid}`).\n**Note: this action is permanent.**"); - if (!await ctx.ConfirmWithReply(target.Hid)) + $"{Emojis.Warn} Are you sure you want to delete this group? If so, reply to this message with the group's ID (`{target.DisplayHid(ctx.Config)}`).\n**Note: this action is permanent.**"); + if (!await ctx.ConfirmWithReply(target.Hid, treatAsHid: true)) throw new PKError( - $"Group deletion cancelled. Note that you must reply with your group ID (`{target.Hid}`) *verbatim*."); + $"Group deletion cancelled. Note that you must reply with your group ID (`{target.DisplayHid(ctx.Config)}`) *verbatim*."); await ctx.Repository.DeleteGroup(target.Id); @@ -580,7 +616,7 @@ public class Groups public async Task DisplayId(Context ctx, PKGroup target) { - await ctx.Reply(target.Hid); + await ctx.Reply(target.DisplayHid(ctx.Config)); } private async Task GetGroupSystem(Context ctx, PKGroup target) diff --git a/PluralKit.Bot/Commands/Help.cs b/PluralKit.Bot/Commands/Help.cs index 50067fe0..3894c3f5 100644 --- a/PluralKit.Bot/Commands/Help.cs +++ b/PluralKit.Bot/Commands/Help.cs @@ -11,12 +11,32 @@ public class Help { Title = "PluralKit", Description = "PluralKit is a bot designed for plural communities on Discord, and is open for anyone to use. It allows you to register systems, maintain system information, set up message proxying, log switches, and more.", - Footer = new("By @Ske#6201 | Myriad design by @Layl#8888, art by https://twitter.com/sillyvizion | GitHub: https://github.com/PluralKit/PluralKit/ | Website: https://pluralkit.me/"), + Footer = new("By @ske | Myriad design by @layl, icon by @tedkalashnikov, banner by @fulmine | GitHub: https://github.com/PluralKit/PluralKit/ | Website: https://pluralkit.me/"), Color = DiscordUtils.Blue, }; 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[] @@ -29,9 +49,9 @@ public class Help ), new ( - "Why are people's names saying [BOT] next to them?", - "These people are not actually bots, this is just a Discord limitation. See [the documentation](https://pluralkit.me/guide#proxying) for an in-depth explanation." - ), + "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 } }); @@ -168,8 +190,10 @@ public class Help { "> **About PluralKit**\nPluralKit detects messages enclosed in specific tags associated with a profile, then replaces that message under a \"pseudo-account\" of that profile using Discord webhooks.", "This is useful for multiple people sharing one body (aka. *systems*), people who wish to role-play as different characters without having multiple Discord accounts, or anyone else who may want to post messages under a different identity from the same Discord account.", - "Due to Discord limitations, these messages will show up with the `[BOT]` tag - however, they are not bots." + "Due to Discord limitations, these messages will show up with the `[APP]` or `[BOT]` tag - however, they are not apps or bots." }); 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/ImportExport.cs b/PluralKit.Bot/Commands/ImportExport.cs index e5dbff70..0b318622 100644 --- a/PluralKit.Bot/Commands/ImportExport.cs +++ b/PluralKit.Bot/Commands/ImportExport.cs @@ -45,13 +45,20 @@ public class ImportExport try { var response = await _client.GetAsync(url); - if (!response.IsSuccessStatusCode) - throw Errors.InvalidImportFile; // hacky fix for discord api returning nonsense charsets sometimes response.Content.Headers.Remove("content-type"); response.Content.Headers.Add("content-type", "application/json; charset=UTF-8"); + var content = await response.Content.ReadAsStringAsync(); + if (content == "This content is no longer available.") + { + var refreshed = await ctx.Rest.RefreshUrls(new[] { url.ToString() }); + response = await _client.GetAsync(new Uri(refreshed.RefreshedUrls[0].Refreshed)); + content = await response.Content.ReadAsStringAsync(); + } + if (!response.IsSuccessStatusCode) + throw Errors.InvalidImportFile; data = JsonConvert.DeserializeObject( - await response.Content.ReadAsStringAsync(), + content, _settings ); if (data == null) diff --git a/PluralKit.Bot/Commands/Lists/ContextListExt.cs b/PluralKit.Bot/Commands/Lists/ContextListExt.cs index c4ac3b8f..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(); @@ -44,7 +44,7 @@ public static class ContextListExt p.SortProperty = SortProperty.LastSwitch; if (ctx.MatchFlag("by-last-message", "blm", "blp")) p.SortProperty = SortProperty.LastMessage; if (ctx.MatchFlag("by-birthday", "by-birthdate", "bbd")) p.SortProperty = SortProperty.Birthdate; - if (ctx.MatchFlag("random")) p.SortProperty = SortProperty.Random; + if (ctx.MatchFlag("random", "rand")) p.SortProperty = SortProperty.Random; // Sort reverse? if (ctx.MatchFlag("r", "rev", "reverse")) @@ -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.Hid}`] **{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)"; @@ -162,7 +168,7 @@ public static class ContextListExt { foreach (var m in page) { - var profile = new StringBuilder($"**ID**: {m.Hid}"); + var profile = new StringBuilder($"**ID**: {m.DisplayHid(ctx.Config)}"); if (m.DisplayName != null && m.NamePrivacy.CanAccess(lookupCtx)) profile.Append($"\n**Display name**: {m.DisplayName}"); @@ -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.Hid}`] **{g.NameFor(ctx)}** "; + var ret = $"[`{g.DisplayHid(ctx.Config, isList: true, shouldPad: shouldPad)}`] **{g.NameFor(ctx)}** "; switch (opts.SortProperty) { @@ -308,7 +317,7 @@ public static class ContextListExt { foreach (var g in page) { - var profile = new StringBuilder($"**ID**: {g.Hid}"); + var profile = new StringBuilder($"**ID**: {g.DisplayHid(ctx.Config)}"); if (g.DisplayName != null && g.NamePrivacy.CanAccess(lookupCtx)) profile.Append($"\n**Display name**: {g.DisplayName}"); 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/Member.cs b/PluralKit.Bot/Commands/Member.cs index 9bc5dc47..b25b969f 100644 --- a/PluralKit.Bot/Commands/Member.cs +++ b/PluralKit.Bot/Commands/Member.cs @@ -16,13 +16,15 @@ public class Member private readonly HttpClient _client; private readonly DispatchService _dispatch; private readonly EmbedService _embeds; + private readonly AvatarHostingService _avatarHosting; public Member(EmbedService embeds, HttpClient client, - DispatchService dispatch) + DispatchService dispatch, AvatarHostingService avatarHosting) { _embeds = embeds; _client = client; _dispatch = dispatch; + _avatarHosting = avatarHosting; } public async Task NewMember(Context ctx) @@ -38,7 +40,7 @@ public class Member var existingMember = await ctx.Repository.GetMemberByName(ctx.System.Id, memberName); if (existingMember != null) { - var msg = $"{Emojis.Warn} You already have a member in your system with the name \"{existingMember.NameFor(ctx)}\" (with ID `{existingMember.Hid}`). Do you want to create another member with the same name?"; + var msg = $"{Emojis.Warn} You already have a member in your system with the name \"{existingMember.NameFor(ctx)}\" (with ID `{existingMember.DisplayHid(ctx.Config)}`). Do you want to create another member with the same name?"; if (!await ctx.PromptYesNo(msg, "Create")) throw new PKError("Member creation cancelled."); } @@ -67,13 +69,24 @@ public class Member // Try to match an image attached to the message var avatarArg = ctx.Message.Attachments.FirstOrDefault(); Exception imageMatchError = null; + ParsedImage img = new(); if (avatarArg != null) try { - await AvatarUtils.VerifyAvatarOrThrow(_client, avatarArg.Url); - await ctx.Repository.UpdateMember(member.Id, new MemberPatch { AvatarUrl = avatarArg.Url }, conn); + // XXX: discord attachment URLs are unable to be validated without their query params + // keep both the URL with query (for validation) and the clean URL (for storage) around + var uriBuilder = new UriBuilder(avatarArg.ProxyUrl); + img = new ParsedImage { Url = uriBuilder.Uri.AbsoluteUri, Source = AvatarSource.Attachment }; - dispatchData.Add("avatar_url", avatarArg.Url); + uriBuilder.Query = ""; + img.CleanUrl = uriBuilder.Uri.AbsoluteUri; + + img = await _avatarHosting.TryRehostImage(img, AvatarHostingService.RehostedImageType.Avatar, ctx.Author.Id, ctx.System); + + await AvatarUtils.VerifyAvatarOrThrow(_client, img.Url); + await ctx.Repository.UpdateMember(member.Id, new MemberPatch { AvatarUrl = img.CleanUrl ?? img.Url }, conn); + + dispatchData.Add("avatar_url", img.CleanUrl); } catch (Exception e) { @@ -88,34 +101,34 @@ public class Member // Send confirmation and space hint await ctx.Reply( - $"{Emojis.Success} Member \"{memberName}\" (`{member.Hid}`) registered! Check out the getting started page for how to get a member up and running: https://pluralkit.me/start#create-a-member"); + $"{Emojis.Success} Member \"{memberName}\" (`{member.DisplayHid(ctx.Config)}`) registered! Check out the getting started page for how to get a member up and running: https://pluralkit.me/start#create-a-member"); // todo: move this to ModelRepository if (await ctx.Database.Execute(conn => conn.QuerySingleAsync("select has_private_members(@System)", new { System = ctx.System.Id })) && !ctx.Config.MemberDefaultPrivate) //if has private members await ctx.Reply( - $"{Emojis.Warn} This member is currently **public**. To change this, use `pk;member {member.Hid} private`."); + $"{Emojis.Warn} This member is currently **public**. To change this, use `pk;member {member.DisplayHid(ctx.Config)} private`."); if (avatarArg != null) if (imageMatchError == null) await ctx.Reply( - $"{Emojis.Success} Member avatar set to attached image.\n{Emojis.Warn} If you delete the message containing the attachment, the avatar will stop working."); + $"{Emojis.Success} Member avatar set to attached image." + (img.Source == AvatarSource.Attachment ? $"\n{Emojis.Warn} If you delete the message containing the attachment, the avatar will stop working." : "")); else await ctx.Reply($"{Emojis.Error} Couldn't set avatar: {imageMatchError.Message}"); if (memberName.Contains(" ")) await ctx.Reply( - $"{Emojis.Note} Note that this member's name contains spaces. You will need to surround it with \"double quotes\" when using commands referring to it, or just use the member's 5-character ID (which is `{member.Hid}`)."); + $"{Emojis.Note} Note that this member's name contains spaces. You will need to surround it with \"double quotes\" when using commands referring to it, or just use the member's short ID (which is `{member.DisplayHid(ctx.Config)}`)."); if (memberCount >= memberLimit) await ctx.Reply( - $"{Emojis.Warn} You have reached the per-system member limit ({memberLimit}). You will be unable to create additional members until existing members are deleted."); + $"{Emojis.Warn} You have reached the per-system member limit ({memberLimit}). If you need to add more members, you can either delete existing members, or ask for your limit to be raised in the PluralKit support server: "); else if (memberCount >= Limits.WarnThreshold(memberLimit)) await ctx.Reply( - $"{Emojis.Warn} You are approaching the per-system member limit ({memberCount} / {memberLimit} members). Please review your member list for unused or duplicate members."); + $"{Emojis.Warn} You are approaching the per-system member limit ({memberCount} / {memberLimit} members). Once you reach this limit, you will be unable to create new members until existing members are deleted, or you can ask for your limit to be raised in the PluralKit support server: "); } public async Task ViewMember(Context ctx, PKMember target) { var system = await ctx.Repository.GetSystem(target.System); await ctx.Reply( - embed: await _embeds.CreateMemberEmbed(system, target, ctx.Guild, ctx.LookupContextFor(system.Id), ctx.Zone)); + embed: await _embeds.CreateMemberEmbed(system, target, ctx.Guild, ctx.Config, ctx.LookupContextFor(system.Id), ctx.Zone)); } public async Task Soulscream(Context ctx, PKMember target) @@ -143,6 +156,6 @@ public class Member public async Task DisplayId(Context ctx, PKMember target) { - await ctx.Reply(target.Hid); + await ctx.Reply(target.DisplayHid(ctx.Config)); } } \ No newline at end of file diff --git a/PluralKit.Bot/Commands/MemberAvatar.cs b/PluralKit.Bot/Commands/MemberAvatar.cs index d5f5f544..66c17de1 100644 --- a/PluralKit.Bot/Commands/MemberAvatar.cs +++ b/PluralKit.Bot/Commands/MemberAvatar.cs @@ -9,10 +9,12 @@ namespace PluralKit.Bot; public class MemberAvatar { private readonly HttpClient _client; + private readonly AvatarHostingService _avatarHosting; - public MemberAvatar(HttpClient client) + public MemberAvatar(HttpClient client, AvatarHostingService avatarHosting) { _client = client; + _avatarHosting = avatarHosting; } private async Task AvatarClear(MemberAvatarLocation location, Context ctx, PKMember target, MemberGuildSettings? mgs) @@ -121,9 +123,10 @@ public class MemberAvatar MemberGuildSettings? guildData) { // First, see if we need to *clear* - if (ctx.MatchClear() && await ctx.ConfirmClear("this member's " + location.Name())) + if (ctx.MatchClear()) { ctx.CheckSystem().CheckOwnMember(target); + await ctx.ConfirmClear("this member's " + location.Name()); await AvatarClear(location, ctx, target, guildData); return; } @@ -138,8 +141,10 @@ public class MemberAvatar } ctx.CheckSystem().CheckOwnMember(target); + + avatarArg = await _avatarHosting.TryRehostImage(avatarArg.Value, AvatarHostingService.RehostedImageType.Avatar, ctx.Author.Id, ctx.System); await AvatarUtils.VerifyAvatarOrThrow(_client, avatarArg.Value.Url); - await UpdateAvatar(location, ctx, target, avatarArg.Value.Url); + await UpdateAvatar(location, ctx, target, avatarArg.Value.CleanUrl ?? avatarArg.Value.Url); await PrintResponse(location, ctx, target, avatarArg.Value, guildData); } @@ -170,13 +175,15 @@ public class MemberAvatar $"{Emojis.Success} Member {location.Name()} changed to {avatar.SourceUser?.Username}'s avatar!{serverFrag}\n{Emojis.Warn} If {avatar.SourceUser?.Username} changes their avatar, the member's avatar will need to be re-set.", AvatarSource.Url => $"{Emojis.Success} Member {location.Name()} changed to the image at the given URL.{serverFrag}", + AvatarSource.HostedCdn => + $"{Emojis.Success} Member {location.Name()} changed to attached image.{serverFrag}", AvatarSource.Attachment => $"{Emojis.Success} Member {location.Name()} changed to attached image.{serverFrag}\n{Emojis.Warn} If you delete the message containing the attachment, the avatar will stop working.", _ => throw new ArgumentOutOfRangeException() }; // The attachment's already right there, no need to preview it. - var hasEmbed = avatar.Source != AvatarSource.Attachment; + var hasEmbed = avatar.Source != AvatarSource.Attachment && avatar.Source != AvatarSource.HostedCdn; return hasEmbed ? ctx.Reply(msg, new EmbedBuilder().Image(new Embed.EmbedImage(avatar.Url)).Build()) : ctx.Reply(msg); diff --git a/PluralKit.Bot/Commands/MemberEdit.cs b/PluralKit.Bot/Commands/MemberEdit.cs index 71eb5301..b57f1bae 100644 --- a/PluralKit.Bot/Commands/MemberEdit.cs +++ b/PluralKit.Bot/Commands/MemberEdit.cs @@ -13,10 +13,12 @@ namespace PluralKit.Bot; public class MemberEdit { private readonly HttpClient _client; + private readonly AvatarHostingService _avatarHosting; - public MemberEdit(HttpClient client) + public MemberEdit(HttpClient client, AvatarHostingService avatarHosting) { _client = client; + _avatarHosting = avatarHosting; } public async Task Name(Context ctx, PKMember target) @@ -34,7 +36,7 @@ public class MemberEdit if (existingMember != null && existingMember.Id != target.Id) { var msg = - $"{Emojis.Warn} You already have a member in your system with the name \"{existingMember.NameFor(ctx)}\" (`{existingMember.Hid}`). Do you want to rename this member to that name too?"; + $"{Emojis.Warn} You already have a member in your system with the name \"{existingMember.NameFor(ctx)}\" (`{existingMember.DisplayHid(ctx.Config)}`). Do you want to rename this member to that name too?"; if (!await ctx.PromptYesNo(msg, "Rename")) throw new PKError("Member renaming cancelled."); } @@ -45,7 +47,7 @@ public class MemberEdit await ctx.Reply($"{Emojis.Success} Member renamed (using {newName.Length}/{Limits.MaxMemberNameLength} characters)."); if (newName.Contains(" ")) await ctx.Reply( - $"{Emojis.Note} Note that this member's name now contains spaces. You will need to surround it with \"double quotes\" when using commands referring to it."); + $"{Emojis.Note} Note that this member's name now contains spaces. You will need to surround it with \"double quotes\" when using commands referring to it, or just use the member's short ID (which is `{target.DisplayHid(ctx.Config)}`)."); if (target.DisplayName != null) await ctx.Reply( $"{Emojis.Note} Note that this member has a display name set ({target.DisplayName}), and will be proxied using that name instead."); @@ -68,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.NameFor(ctx)} (`{target.DisplayHid(ctx.Config)}`)"); + 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; } @@ -120,30 +133,41 @@ public class MemberEdit { var noPronounsSetMessage = "This member does not have pronouns set."; if (ctx.System?.Id == target.System) - noPronounsSetMessage += $"To set some, type `pk;member {target.Reference(ctx)} pronouns `."; + noPronounsSetMessage += $" To set some, type `pk;member {target.Reference(ctx)} pronouns `."; 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.NameFor(ctx)} (`{target.DisplayHid(ctx.Config)}`)"); + 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; } @@ -180,13 +204,15 @@ public class MemberEdit async Task SetBannerImage(ParsedImage img) { + img = await _avatarHosting.TryRehostImage(img, AvatarHostingService.RehostedImageType.Banner, ctx.Author.Id, ctx.System); await AvatarUtils.VerifyAvatarOrThrow(_client, img.Url, true); - await ctx.Repository.UpdateMember(target.Id, new MemberPatch { BannerImage = img.Url }); + await ctx.Repository.UpdateMember(target.Id, new MemberPatch { BannerImage = img.CleanUrl ?? img.Url }); var msg = img.Source switch { AvatarSource.Url => $"{Emojis.Success} Member banner image changed to the image at the given URL.", + AvatarSource.HostedCdn => $"{Emojis.Success} Member banner image changed to attached image.", AvatarSource.Attachment => $"{Emojis.Success} Member banner image changed to attached image.\n{Emojis.Warn} If you delete the message containing the attachment, the banner image will stop working.", AvatarSource.User => throw new PKError("Cannot set a banner image to an user's avatar."), @@ -194,7 +220,7 @@ public class MemberEdit }; // The attachment's already right there, no need to preview it. - var hasEmbed = img.Source != AvatarSource.Attachment; + var hasEmbed = img.Source != AvatarSource.Attachment && img.Source != AvatarSource.HostedCdn; await (hasEmbed ? ctx.Reply(msg, new EmbedBuilder().Image(new Embed.EmbedImage(img.Url)).Build()) : ctx.Reply(msg)); @@ -207,13 +233,13 @@ public class MemberEdit var eb = new EmbedBuilder() .Title($"{target.NameFor(ctx)}'s banner image") .Image(new Embed.EmbedImage(target.BannerImage)) - .Description($"To clear, use `pk;member {target.Hid} banner clear`."); + .Description($"To clear, use `pk;member {target.Reference(ctx)} banner clear`."); await ctx.Reply(embed: eb.Build()); } else { throw new PKSyntaxError( - "This member does not have a banner image set. Set one by attaching an image to this command, or by passing an image URL or @mention."); + "This member does not have a banner image set. Set one by attaching an image to this command, or by passing an image URL."); } } @@ -228,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)) @@ -236,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") @@ -335,7 +363,7 @@ public class MemberEdit var eb = new EmbedBuilder() .Title("Member names") .Footer(new Embed.EmbedFooter( - $"Member ID: {target.Hid} | Active name in bold. Server name overrides display name, which overrides base name." + $"Member ID: {target.DisplayHid(ctx.Config)} | Active name in bold. Server name overrides display name, which overrides base name." + (target.DisplayName != null && ctx.System?.Id == target.System ? $" Using {target.DisplayName.Length}/{Limits.MaxMemberNameLength} characters for the display name." : "") + (memberGuildConfig?.DisplayName != null ? $" Using {memberGuildConfig?.DisplayName.Length}/{Limits.MaxMemberNameLength} characters for the server name." : ""))); @@ -384,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.NameFor(ctx)} (`{target.DisplayHid(ctx.Config)}`)"); + await ctx.Reply(target.DisplayName, embed: eb.Build()); return; } @@ -446,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.NameFor(ctx)} (`{target.DisplayHid(ctx.Config)}`)"); + await ctx.Reply(memberGuildConfig.DisplayName, embed: eb.Build()); return; } @@ -700,6 +756,7 @@ public class MemberEdit .Field(new Embed.Field("Name (replaces name with display name if member has one)", target.NamePrivacy.Explanation())) .Field(new Embed.Field("Description", target.DescriptionPrivacy.Explanation())) + .Field(new Embed.Field("Banner", target.BannerPrivacy.Explanation())) .Field(new Embed.Field("Avatar", target.AvatarPrivacy.Explanation())) .Field(new Embed.Field("Birthday", target.BirthdayPrivacy.Explanation())) .Field(new Embed.Field("Pronouns", target.PronounPrivacy.Explanation())) @@ -708,7 +765,7 @@ public class MemberEdit target.MetadataPrivacy.Explanation())) .Field(new Embed.Field("Visibility", target.MemberVisibility.Explanation())) .Description( - "To edit privacy settings, use the command:\n`pk;member privacy `\n\n- `subject` is one of `name`, `description`, `avatar`, `birthday`, `pronouns`, `proxies`, `metadata`, `visibility`, or `all`\n- `level` is either `public` or `private`.") + "To edit privacy settings, use the command:\n`pk;member privacy `\n\n- `subject` is one of `name`, `description`, `banner`, `avatar`, `birthday`, `pronouns`, `proxies`, `metadata`, `visibility`, or `all`\n- `level` is either `public` or `private`.") .Build()); return; } @@ -738,6 +795,7 @@ public class MemberEdit { MemberPrivacySubject.Name => "name privacy", MemberPrivacySubject.Description => "description privacy", + MemberPrivacySubject.Banner => "banner privacy", MemberPrivacySubject.Avatar => "avatar privacy", MemberPrivacySubject.Pronouns => "pronoun privacy", MemberPrivacySubject.Birthday => "birthday privacy", @@ -753,6 +811,8 @@ public class MemberEdit "This member's name is now hidden from other systems, and will be replaced by the member's display name.", (MemberPrivacySubject.Description, PrivacyLevel.Private) => "This member's description is now hidden from other systems.", + (MemberPrivacySubject.Banner, PrivacyLevel.Private) => + "This member's banner is now hidden from other systems.", (MemberPrivacySubject.Avatar, PrivacyLevel.Private) => "This member's avatar is now hidden from other systems.", (MemberPrivacySubject.Birthday, PrivacyLevel.Private) => @@ -770,6 +830,8 @@ public class MemberEdit "This member's name is no longer hidden from other systems.", (MemberPrivacySubject.Description, PrivacyLevel.Public) => "This member's description is no longer hidden from other systems.", + (MemberPrivacySubject.Banner, PrivacyLevel.Public) => + "This member's banner is no longer hidden from other systems.", (MemberPrivacySubject.Avatar, PrivacyLevel.Public) => "This member's avatar is no longer hidden from other systems.", (MemberPrivacySubject.Birthday, PrivacyLevel.Public) => @@ -812,8 +874,8 @@ public class MemberEdit ctx.CheckSystem().CheckOwnMember(target); await ctx.Reply( - $"{Emojis.Warn} Are you sure you want to delete \"{target.NameFor(ctx)}\"? If so, reply to this message with the member's ID (`{target.Hid}`). __***This cannot be undone!***__"); - if (!await ctx.ConfirmWithReply(target.Hid)) throw Errors.MemberDeleteCancelled; + $"{Emojis.Warn} Are you sure you want to delete \"{target.NameFor(ctx)}\"? If so, reply to this message with the member's ID (`{target.DisplayHid(ctx.Config)}`). __***This cannot be undone!***__"); + if (!await ctx.ConfirmWithReply(target.Hid, treatAsHid: true)) throw Errors.MemberDeleteCancelled; await ctx.Repository.DeleteMember(target.Id); 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 6aac61eb..9a3b31fb 100644 --- a/PluralKit.Bot/Commands/Message.cs +++ b/PluralKit.Bot/Commands/Message.cs @@ -39,11 +39,12 @@ public class ProxiedMessage private readonly WebhookExecutorService _webhookExecutor; private readonly ProxyService _proxy; private readonly LastMessageCacheService _lastMessageCache; + private readonly RedisService _redisService; public ProxiedMessage(EmbedService embeds, DiscordApiClient rest, IMetrics metrics, ModelRepository repo, ProxyService proxy, WebhookExecutorService webhookExecutor, LogChannelService logChannel, IDiscordCache cache, - LastMessageCacheService lastMessageCache) + LastMessageCacheService lastMessageCache, RedisService redisService) { _embeds = embeds; _rest = rest; @@ -54,6 +55,7 @@ public class ProxiedMessage _metrics = metrics; _proxy = proxy; _lastMessageCache = lastMessageCache; + _redisService = redisService; } public async Task ReproxyMessage(Context ctx) @@ -91,7 +93,7 @@ public class ProxiedMessage } } - public async Task EditMessage(Context ctx) + public async Task EditMessage(Context ctx, bool useRegex) { var (msg, systemId) = await GetMessageToEdit(ctx, EditTimeout, false); @@ -103,7 +105,7 @@ public class ProxiedMessage throw new PKError("Could not edit message."); // Regex flag - var useRegex = ctx.MatchFlag("regex", "x"); + useRegex = useRegex || ctx.MatchFlag("regex", "x"); // Check if we should append or prepend var mutateSpace = ctx.MatchFlag("nospace", "ns") ? "" : " "; @@ -116,7 +118,8 @@ public class ProxiedMessage // Should we clear embeds? var clearEmbeds = ctx.MatchFlag("clear-embed", "ce"); - if (clearEmbeds && newContent == null) + var clearAttachments = ctx.MatchFlag("clear-attachments", "ca"); + if ((clearEmbeds || clearAttachments) && newContent == null) newContent = originalMsg.Content!; if (newContent == null) @@ -216,11 +219,13 @@ 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, clearAttachments); if (ctx.Guild == null) await _rest.CreateReaction(ctx.Channel.Id, ctx.Message.Id, new Emoji { Name = Emojis.Success }); + await _redisService.SetOriginalMid(ctx.Message.Id, editedMsg.Id); + if ((await ctx.BotPermissions).HasFlag(PermissionSet.ManageMessages)) await _rest.DeleteMessage(ctx.Channel.Id, ctx.Message.Id); @@ -348,7 +353,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) @@ -361,21 +368,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) @@ -383,7 +401,8 @@ public class ProxiedMessage if (!showContent) throw new PKError(noShowContentError); - if (message.System?.Id != ctx.System.Id && message.Message.Sender != ctx.Author.Id) + // if user has has a system and their system sent the message, or if user sent the message, do not error + if (!((ctx.System != null && message.System?.Id == ctx.System.Id) || message.Message.Sender == ctx.Author.Id)) throw new PKError("You can only delete your own messages."); await ctx.Rest.DeleteMessage(message.Message.Channel, message.Message.Mid); @@ -414,19 +433,19 @@ public class ProxiedMessage return; } - await ctx.Reply(embed: await _embeds.CreateMessageInfoEmbed(message, showContent)); + await ctx.Reply(embed: await _embeds.CreateMessageInfoEmbed(message, showContent, ctx.Config)); } 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 d5414c45..8f0c07b2 100644 --- a/PluralKit.Bot/Commands/Random.cs +++ b/PluralKit.Bot/Commands/Random.cs @@ -37,7 +37,7 @@ public class Random var randInt = randGen.Next(members.Count); await ctx.Reply(embed: await _embeds.CreateMemberEmbed(target, members[randInt], ctx.Guild, - ctx.LookupContextFor(target.Id), ctx.Zone)); + ctx.Config, ctx.LookupContextFor(target.Id), ctx.Zone)); } public async Task Group(Context ctx, PKSystem target) @@ -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())); @@ -93,6 +93,6 @@ public class Random var randInt = randGen.Next(ms.Count); await ctx.Reply(embed: await _embeds.CreateMemberEmbed(system, ms[randInt], ctx.Guild, - ctx.LookupContextFor(group.System), ctx.Zone)); + ctx.Config, ctx.LookupContextFor(group.System), ctx.Zone)); } } \ No newline at end of file diff --git a/PluralKit.Bot/Commands/ServerConfig.cs b/PluralKit.Bot/Commands/ServerConfig.cs index daadc0b2..922adcf7 100644 --- a/PluralKit.Bot/Commands/ServerConfig.cs +++ b/PluralKit.Bot/Commands/ServerConfig.cs @@ -18,6 +18,93 @@ public class ServerConfig _cache = cache; } + private record PaginatedConfigItem(string Key, string Description, string? CurrentValue, string DefaultValue); + private string EnabledDisabled(bool value) => value ? "enabled" : "disabled"; + + public async Task ShowConfig(Context ctx) + { + await ctx.CheckGuildContext().CheckAuthorPermission(PermissionSet.ManageGuild, "Manage Server"); + var items = new List(); + + // TODO: move log channel / blacklist into here + + items.Add(new( + "log cleanup", + "Whether to clean up other bots' log channels", + EnabledDisabled(ctx.GuildConfig!.LogCleanupEnabled), + "disabled" + )); + + items.Add(new( + "invalid command error", + "Whether to show an error message when an unknown command is sent", + EnabledDisabled(ctx.GuildConfig!.InvalidCommandResponseEnabled), + "enabled" + )); + + items.Add(new( + "require tag", + "Whether server users are required to have a system tag on proxied messages", + EnabledDisabled(ctx.GuildConfig!.RequireSystemTag), + "disabled" + )); + + items.Add(new( + "log channel", + "Channel to log proxied messages to", + ctx.GuildConfig!.LogChannel != null ? $"<#{ctx.GuildConfig.LogChannel}>" : "none", + "none" + )); + + string ChannelListMessage(int count, string cmd) => $"{count} channels, use `pk;scfg {cmd}` to view/update"; + + items.Add(new( + "log blacklist", + "Channels whose proxied messages will not be logged", + ChannelListMessage(ctx.GuildConfig!.LogBlacklist.Length, "log blacklist"), + ChannelListMessage(0, "log blacklist") + )); + + items.Add(new( + "proxy blacklist", + "Channels where message proxying is disabled", + ChannelListMessage(ctx.GuildConfig!.Blacklist.Length, "proxy blacklist"), + ChannelListMessage(0, "proxy blacklist") + )); + + await ctx.Paginate( + items.ToAsyncEnumerable(), + items.Count, + 10, + "Current settings for this server", + null, + (eb, l) => + { + var description = new StringBuilder(); + + foreach (var item in l) + { + description.Append(item.Key.AsCode()); + description.Append($" **({item.CurrentValue ?? item.DefaultValue})**"); + if (item.CurrentValue != null && item.CurrentValue != item.DefaultValue) + description.Append("\ud83d\udd39"); + + description.AppendLine(); + description.Append(item.Description); + description.AppendLine(); + description.AppendLine(); + } + + eb.Description(description.ToString()); + + // using *large* blue diamond here since it's easier to see in the small footer + eb.Footer(new("\U0001f537 means this setting was changed. Type `pk;serverconfig clear` to reset it to the default.")); + + return Task.CompletedTask; + } + ); + } + public async Task SetLogChannel(Context ctx) { await ctx.CheckGuildContext().CheckAuthorPermission(PermissionSet.ManageGuild, "Manage Server"); @@ -49,7 +136,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.PermissionsIn(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)) @@ -59,6 +146,8 @@ public class ServerConfig await ctx.Reply($"{Emojis.Success} Proxy logging channel set to <#{channel.Id}>."); } + // legacy behaviour: enable/disable logging for commands + // new behaviour is add/remove from log blacklist (see #LogBlacklistNew) public async Task SetLogEnabled(Context ctx, bool enable) { await ctx.CheckGuildContext().CheckAuthorPermission(PermissionSet.ManageGuild, "Manage Server"); @@ -92,11 +181,11 @@ public class ServerConfig await ctx.Reply( $"{Emojis.Success} Message logging for the given channels {(enable ? "enabled" : "disabled")}." + (logChannel == null - ? $"\n{Emojis.Warn} Please note that no logging channel is set, so there is nowhere to log messages to. You can set a logging channel using `pk;log channel #your-log-channel`." + ? $"\n{Emojis.Warn} Please note that no logging channel is set, so there is nowhere to log messages to. You can set a logging channel using `pk;serverconfig log channel #your-log-channel`." : "")); } - public async Task ShowBlacklisted(Context ctx) + public async Task ShowProxyBlacklisted(Context ctx) { await ctx.CheckGuildContext().CheckAuthorPermission(PermissionSet.ManageGuild, "Manage Server"); @@ -104,14 +193,14 @@ 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(); if (channels.Count == 0) { - await ctx.Reply("This server has no blacklisted channels."); + await ctx.Reply("This server has no channels where proxying is disabled."); return; } @@ -121,7 +210,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 +242,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 +261,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; @@ -197,14 +287,16 @@ public class ServerConfig } - public async Task SetBlacklisted(Context ctx, bool shouldAdd) + + public async Task SetProxyBlacklisted(Context ctx, bool shouldAdd) { await ctx.CheckGuildContext().CheckAuthorPermission(PermissionSet.ManageGuild, "Manage Server"); var affectedChannels = new List(); if (ctx.Match("all")) affectedChannels = (await _cache.GetGuildChannels(ctx.Guild.Id)) - .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()) @@ -229,6 +321,42 @@ public class ServerConfig $"{Emojis.Success} Channels {(shouldAdd ? "added to" : "removed from")} the proxy blacklist."); } + public async Task SetLogBlacklisted(Context ctx, bool shouldAdd) + { + await ctx.CheckGuildContext().CheckAuthorPermission(PermissionSet.ManageGuild, "Manage Server"); + + var affectedChannels = new List(); + if (ctx.Match("all")) + affectedChannels = (await _cache.GetGuildChannels(ctx.Guild.Id)) + // All the channel types you can proxy in + .Where(x => DiscordUtils.IsValidGuildChannel(x)).ToList(); + else if (!ctx.HasNext()) throw new PKSyntaxError("You must pass one or more #channels."); + else + while (ctx.HasNext()) + { + var channelString = ctx.PeekArgument(); + var channel = await ctx.MatchChannel(); + if (channel == null || channel.GuildId != ctx.Guild.Id) throw Errors.ChannelNotFound(channelString); + affectedChannels.Add(channel); + } + + var guild = await ctx.Repository.GetGuild(ctx.Guild.Id); + + var blacklist = guild.LogBlacklist.ToHashSet(); + if (shouldAdd) + blacklist.UnionWith(affectedChannels.Select(c => c.Id)); + else + blacklist.ExceptWith(affectedChannels.Select(c => c.Id)); + + await ctx.Repository.UpdateGuild(ctx.Guild.Id, new GuildPatch { LogBlacklist = blacklist.ToArray() }); + + await ctx.Reply( + $"{Emojis.Success} Channels {(shouldAdd ? "added to" : "removed from")} the logging blacklist." + + (guild.LogChannel == null + ? $"\n{Emojis.Warn} Please note that no logging channel is set, so there is nowhere to log messages to. You can set a logging channel using `pk;serverconfig log channel #your-log-channel`." + : "")); + } + public async Task SetLogCleanup(Context ctx) { var botList = string.Join(", ", LoggerCleanService.Bots.Select(b => b.Name).OrderBy(x => x.ToLowerInvariant())); @@ -244,20 +372,16 @@ public class ServerConfig } await ctx.CheckGuildContext().CheckAuthorPermission(PermissionSet.ManageGuild, "Manage Server"); - - var guild = await ctx.Repository.GetGuild(ctx.Guild.Id); - bool? newValue = ctx.MatchToggleOrNull(); if (newValue == null) { - var guildCfg = await ctx.Repository.GetGuild(ctx.Guild.Id); - if (guildCfg.LogCleanupEnabled) + if (ctx.GuildConfig!.LogCleanupEnabled) eb.Description( - "Log cleanup is currently **on** for this server. To disable it, type `pk;logclean off`."); + "Log cleanup is currently **on** for this server. To disable it, type `pk;serverconfig logclean off`."); else eb.Description( - "Log cleanup is currently **off** for this server. To enable it, type `pk;logclean on`."); + "Log cleanup is currently **off** for this server. To enable it, type `pk;serverconfig logclean on`."); await ctx.Reply(embed: eb.Build()); return; } @@ -270,4 +394,36 @@ public class ServerConfig else await ctx.Reply($"{Emojis.Success} Log cleanup has been **disabled** for this server."); } + + public async Task InvalidCommandResponse(Context ctx) + { + await ctx.CheckGuildContext().CheckAuthorPermission(PermissionSet.ManageGuild, "Manage Server"); + + if (!ctx.HasNext()) + { + var msg = $"Error responses for unknown/invalid commands are currently **{EnabledDisabled(ctx.GuildConfig!.InvalidCommandResponseEnabled)}**."; + await ctx.Reply(msg); + return; + } + + var newVal = ctx.MatchToggle(false); + await ctx.Repository.UpdateGuild(ctx.Guild.Id, new() { InvalidCommandResponseEnabled = newVal }); + await ctx.Reply($"Error responses for unknown/invalid commands are now {EnabledDisabled(newVal)}."); + } + + public async Task RequireSystemTag(Context ctx) + { + await ctx.CheckGuildContext().CheckAuthorPermission(PermissionSet.ManageGuild, "Manage Server"); + + if (!ctx.HasNext()) + { + var msg = $"System tags are currently **{(ctx.GuildConfig!.RequireSystemTag ? "required" : "not required")}** for PluralKit users in this server."; + await ctx.Reply(msg); + return; + } + + var newVal = ctx.MatchToggle(false); + await ctx.Repository.UpdateGuild(ctx.Guild.Id, new() { RequireSystemTag = newVal }); + await ctx.Reply($"System tags are now **{(newVal ? "required" : "not required")}** for PluralKit users in this server."); + } } \ No newline at end of file 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 0a24b3ff..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) @@ -39,6 +58,6 @@ public class System if (target == null) throw Errors.NoSystemError; - await ctx.Reply(target.Hid); + await ctx.Reply(target.DisplayHid(ctx.Config)); } } \ No newline at end of file diff --git a/PluralKit.Bot/Commands/SystemEdit.cs b/PluralKit.Bot/Commands/SystemEdit.cs index e6d3ddcd..264761ec 100644 --- a/PluralKit.Bot/Commands/SystemEdit.cs +++ b/PluralKit.Bot/Commands/SystemEdit.cs @@ -18,12 +18,14 @@ public class SystemEdit private readonly HttpClient _client; private readonly DataFileService _dataFiles; private readonly PrivateChannelService _dmCache; + private readonly AvatarHostingService _avatarHosting; - public SystemEdit(DataFileService dataFiles, HttpClient client, PrivateChannelService dmCache) + public SystemEdit(DataFileService dataFiles, HttpClient client, PrivateChannelService dmCache, AvatarHostingService avatarHosting) { _dataFiles = dataFiles; _client = client; _dmCache = dmCache; + _avatarHosting = avatarHosting; } public async Task Name(Context ctx, PKSystem target) @@ -35,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(ctx.Config)}`"); + 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; } @@ -89,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(ctx.Config)}`"); + 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; } @@ -141,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(ctx.Config)}`"); + 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; } @@ -189,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)) @@ -197,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") @@ -244,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(ctx.Config)}`"); + 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; } @@ -294,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(ctx.Config)}`"); + await ctx.Reply(settings.Tag, embed: eb.Build()); + return; + } var msg = $"Your current system tag in '{ctx.Guild.Name}' is {settings.Tag.AsCode()}"; if (!settings.TagEnabled) @@ -398,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 @@ -416,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(ctx.Config)}`"); + 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; } @@ -473,22 +539,24 @@ public class SystemEdit { ctx.CheckOwnSystem(target); + img = await _avatarHosting.TryRehostImage(img, AvatarHostingService.RehostedImageType.Avatar, ctx.Author.Id, ctx.System); await AvatarUtils.VerifyAvatarOrThrow(_client, img.Url); - await ctx.Repository.UpdateSystem(target.Id, new SystemPatch { AvatarUrl = img.Url }); + await ctx.Repository.UpdateSystem(target.Id, new SystemPatch { AvatarUrl = img.CleanUrl ?? img.Url }); var msg = img.Source switch { AvatarSource.User => $"{Emojis.Success} System icon changed to {img.SourceUser?.Username}'s avatar!\n{Emojis.Warn} If {img.SourceUser?.Username} changes their avatar, the system icon will need to be re-set.", AvatarSource.Url => $"{Emojis.Success} System icon changed to the image at the given URL.", + AvatarSource.HostedCdn => $"{Emojis.Success} System icon changed to attached image.", AvatarSource.Attachment => $"{Emojis.Success} System icon changed to attached image.\n{Emojis.Warn} If you delete the message containing the attachment, the system icon will stop working.", _ => throw new ArgumentOutOfRangeException() }; // The attachment's already right there, no need to preview it. - var hasEmbed = img.Source != AvatarSource.Attachment; + var hasEmbed = img.Source != AvatarSource.Attachment && img.Source != AvatarSource.HostedCdn; await (hasEmbed ? ctx.Reply(msg, new EmbedBuilder().Image(new Embed.EmbedImage(img.Url)).Build()) : ctx.Reply(msg)); @@ -541,9 +609,10 @@ public class SystemEdit { ctx.CheckOwnSystem(target); + img = await _avatarHosting.TryRehostImage(img, AvatarHostingService.RehostedImageType.Avatar, ctx.Author.Id, ctx.System); await AvatarUtils.VerifyAvatarOrThrow(_client, img.Url); - await ctx.Repository.UpdateSystemGuild(target.Id, ctx.Guild.Id, new SystemGuildPatch { AvatarUrl = img.Url }); + await ctx.Repository.UpdateSystemGuild(target.Id, ctx.Guild.Id, new SystemGuildPatch { AvatarUrl = img.CleanUrl ?? img.Url }); var msg = img.Source switch { @@ -551,13 +620,14 @@ public class SystemEdit $"{Emojis.Success} System icon for this server changed to {img.SourceUser?.Username}'s avatar! It will now be used for anything that uses system avatar in this server.\n{Emojis.Warn} If {img.SourceUser?.Username} changes their avatar, the system icon for this server will need to be re-set.", AvatarSource.Url => $"{Emojis.Success} System icon for this server changed to the image at the given URL. It will now be used for anything that uses system avatar in this server.", + AvatarSource.HostedCdn => $"{Emojis.Success} System icon for this server changed to attached image.", AvatarSource.Attachment => $"{Emojis.Success} System icon for this server changed to attached image. It will now be used for anything that uses system avatar in this server.\n{Emojis.Warn} If you delete the message containing the attachment, the system icon for this server will stop working.", _ => throw new ArgumentOutOfRangeException() }; // The attachment's already right there, no need to preview it. - var hasEmbed = img.Source != AvatarSource.Attachment; + var hasEmbed = img.Source != AvatarSource.Attachment && img.Source != AvatarSource.HostedCdn; await (hasEmbed ? ctx.Reply(msg, new EmbedBuilder().Image(new Embed.EmbedImage(img.Url)).Build()) : ctx.Reply(msg)); @@ -602,7 +672,7 @@ public class SystemEdit public async Task BannerImage(Context ctx, PKSystem target) { - ctx.CheckSystemPrivacy(target.Id, target.DescriptionPrivacy); + ctx.CheckSystemPrivacy(target.Id, target.BannerPrivacy); var isOwnSystem = target.Id == ctx.System?.Id; @@ -638,13 +708,15 @@ public class SystemEdit else if (await ctx.MatchImage() is { } img) { + img = await _avatarHosting.TryRehostImage(img, AvatarHostingService.RehostedImageType.Banner, ctx.Author.Id, ctx.System); await AvatarUtils.VerifyAvatarOrThrow(_client, img.Url, true); - await ctx.Repository.UpdateSystem(target.Id, new SystemPatch { BannerImage = img.Url }); + await ctx.Repository.UpdateSystem(target.Id, new SystemPatch { BannerImage = img.CleanUrl ?? img.Url }); var msg = img.Source switch { AvatarSource.Url => $"{Emojis.Success} System banner image changed to the image at the given URL.", + AvatarSource.HostedCdn => $"{Emojis.Success} System banner image changed to attached image.", AvatarSource.Attachment => $"{Emojis.Success} System banner image changed to attached image.\n{Emojis.Warn} If you delete the message containing the attachment, the banner image will stop working.", AvatarSource.User => throw new PKError("Cannot set a banner image to an user's avatar."), @@ -652,7 +724,7 @@ public class SystemEdit }; // The attachment's already right there, no need to preview it. - var hasEmbed = img.Source != AvatarSource.Attachment; + var hasEmbed = img.Source != AvatarSource.Attachment && img.Source != AvatarSource.HostedCdn; await (hasEmbed ? ctx.Reply(msg, new EmbedBuilder().Image(new Embed.EmbedImage(img.Url)).Build()) : ctx.Reply(msg)); @@ -662,19 +734,20 @@ public class SystemEdit public async Task Delete(Context ctx, PKSystem target) { ctx.CheckSystem().CheckOwnSystem(target); + var noExport = ctx.MatchFlag("ne", "no-export"); - await ctx.Reply( - $"{Emojis.Warn} Are you sure you want to delete your system? If so, reply to this message with your system's ID (`{target.Hid}`).\n" - + $"**Note: this action is permanent,** but you will get a copy of your system's data that can be re-imported into PluralKit at a later date sent to you in DMs." - + " If you don't want this to happen, use `pk;s delete -no-export` instead."); - if (!await ctx.ConfirmWithReply(target.Hid)) + var warnMsg = $"{Emojis.Warn} Are you sure you want to delete your system? If so, reply to this message with your system's ID (`{target.DisplayHid(ctx.Config)}`).\n"; + if (!noExport) + warnMsg += "**Note: this action is permanent,** but you will get a copy of your system's data that can be re-imported into PluralKit at a later date sent to you in DMs." + + " If you don't want this to happen, use `pk;s delete -no-export` instead."; + + await ctx.Reply(warnMsg); + if (!await ctx.ConfirmWithReply(target.Hid, treatAsHid: true)) throw new PKError( - $"System deletion cancelled. Note that you must reply with your system ID (`{target.Hid}`) *verbatim*."); + $"System deletion cancelled. Note that you must reply with your system ID (`{target.DisplayHid(ctx.Config)}`) *verbatim*."); // If the user confirms the deletion, export their system and send them the export file before actually // deleting their system, unless they specifically tell us not to do an export. - - var noExport = ctx.MatchFlag("ne", "no-export"); if (!noExport) { var json = await ctx.BusyIndicator(async () => @@ -762,13 +835,14 @@ public class SystemEdit .Field(new Embed.Field("Name", target.NamePrivacy.Explanation())) .Field(new Embed.Field("Avatar", target.AvatarPrivacy.Explanation())) .Field(new Embed.Field("Description", target.DescriptionPrivacy.Explanation())) + .Field(new Embed.Field("Banner", target.BannerPrivacy.Explanation())) .Field(new Embed.Field("Pronouns", target.PronounPrivacy.Explanation())) .Field(new Embed.Field("Member list", target.MemberListPrivacy.Explanation())) .Field(new Embed.Field("Group list", target.GroupListPrivacy.Explanation())) .Field(new Embed.Field("Current fronter(s)", target.FrontPrivacy.Explanation())) .Field(new Embed.Field("Front/switch history", target.FrontHistoryPrivacy.Explanation())) .Description( - "To edit privacy settings, use the command:\n`pk;system privacy `\n\n- `subject` is one of `name`, `avatar`, `description`, `list`, `front`, `fronthistory`, `groups`, or `all` \n- `level` is either `public` or `private`."); + "To edit privacy settings, use the command:\n`pk;system privacy `\n\n- `subject` is one of `name`, `avatar`, `description`, `banner`, `list`, `front`, `fronthistory`, `groups`, or `all` \n- `level` is either `public` or `private`."); return ctx.Reply(embed: eb.Build()); } @@ -788,6 +862,7 @@ public class SystemEdit SystemPrivacySubject.Name => "name", SystemPrivacySubject.Avatar => "avatar", SystemPrivacySubject.Description => "description", + SystemPrivacySubject.Banner => "banner", SystemPrivacySubject.Pronouns => "pronouns", SystemPrivacySubject.Front => "front", SystemPrivacySubject.FrontHistory => "front history", diff --git a/PluralKit.Bot/Commands/SystemFront.cs b/PluralKit.Bot/Commands/SystemFront.cs index 08fdf18c..adead8f5 100644 --- a/PluralKit.Bot/Commands/SystemFront.cs +++ b/PluralKit.Bot/Commands/SystemFront.cs @@ -76,7 +76,7 @@ public class SystemFront var members = await ctx.Database.Execute(c => ctx.Repository.GetSwitchMembers(c, sw.Id)).ToListAsync(); var membersStr = members.Any() - ? string.Join(", ", members.Select(m => $"**{m.NameFor(ctx)}**{(showMemberId ? $" (`{m.Hid}`)" : "")}")) + ? string.Join(", ", members.Select(m => $"**{m.NameFor(ctx)}**{(showMemberId ? $" (`{m.DisplayHid(ctx.Config)}`)" : "")}")) : "**no fronter**"; var switchSince = SystemClock.Instance.GetCurrentInstant() - sw.Timestamp; @@ -138,13 +138,13 @@ public class SystemFront if (ctx.Guild != null) guildSettings = await ctx.Repository.GetSystemGuild(ctx.Guild.Id, system.Id); if (group != null) - title.Append($"{group.NameFor(ctx)} (`{group.Hid}`)"); + title.Append($"{group.NameFor(ctx)} (`{group.DisplayHid(ctx.Config)}`)"); else if (ctx.Guild != null && guildSettings.DisplayName != null) - title.Append($"{guildSettings.DisplayName} (`{system.Hid}`)"); + title.Append($"{guildSettings.DisplayName} (`{system.DisplayHid(ctx.Config)}`)"); else if (system.NameFor(ctx) != null) - title.Append($"{system.NameFor(ctx)} (`{system.Hid}`)"); + title.Append($"{system.NameFor(ctx)} (`{system.DisplayHid(ctx.Config)}`)"); else - title.Append($"`{system.Hid}`"); + title.Append($"`{system.DisplayHid(ctx.Config)}`"); var frontpercent = await ctx.Database.Execute(c => ctx.Repository.GetFrontBreakdown(c, system.Id, group?.Id, rangeStart.Value.ToInstant(), now)); await ctx.Reply(embed: await _embeds.CreateFrontPercentEmbed(frontpercent, system, group, ctx.Zone, diff --git a/PluralKit.Bot/Commands/SystemLink.cs b/PluralKit.Bot/Commands/SystemLink.cs index 593f1ad1..330b701a 100644 --- a/PluralKit.Bot/Commands/SystemLink.cs +++ b/PluralKit.Bot/Commands/SystemLink.cs @@ -18,7 +18,7 @@ public class SystemLink var existingAccount = await ctx.Repository.GetSystemByAccount(account.Id); if (existingAccount != null) - throw Errors.AccountInOtherSystem(existingAccount); + throw Errors.AccountInOtherSystem(existingAccount, ctx.Config); var msg = $"{account.Mention()}, please confirm the link."; if (!await ctx.PromptYesNo(msg, "Confirm", account, false)) throw Errors.MemberLinkCancelled; diff --git a/PluralKit.Bot/Commands/SystemList.cs b/PluralKit.Bot/Commands/SystemList.cs index dd357c7f..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, @@ -33,11 +33,11 @@ public class SystemList var systemGuildSettings = ctx.Guild != null ? await ctx.Repository.GetSystemGuild(ctx.Guild.Id, target.Id) : null; if (systemGuildSettings != null && systemGuildSettings.DisplayName != null) - title.Append($"{systemGuildSettings.DisplayName} (`{target.Hid}`)"); + title.Append($"{systemGuildSettings.DisplayName} (`{target.DisplayHid(ctx.Config)}`)"); else if (target.NameFor(ctx) != null) - title.Append($"{target.NameFor(ctx)} (`{target.Hid}`)"); + title.Append($"{target.NameFor(ctx)} (`{target.DisplayHid(ctx.Config)}`)"); else - title.Append($"`{target.Hid}`"); + title.Append($"`{target.DisplayHid(ctx.Config)}`"); if (opts.Search != null) title.Append($" matching **{opts.Search.Truncate(100)}**"); diff --git a/PluralKit.Bot/Errors.cs b/PluralKit.Bot/Errors.cs index 04f3d33e..524ea65a 100644 --- a/PluralKit.Bot/Errors.cs +++ b/PluralKit.Bot/Errors.cs @@ -120,8 +120,8 @@ public static class Errors public static PKError UrlTooLong(string url) => new($"The given URL is too long ({url.Length}/{Limits.MaxUriLength} characters)."); - public static PKError AccountInOtherSystem(PKSystem system) => - new($"The mentioned account is already linked to another system (see `pk;system {system.Hid}`)."); + public static PKError AccountInOtherSystem(PKSystem system, SystemConfig config) => + new($"The mentioned account is already linked to another system (see `pk;system {system.DisplayHid(config)}`)."); public static PKError SameSwitch(ICollection members, LookupContext ctx) { @@ -162,7 +162,7 @@ public static class Errors $"The webhook's name, {name.AsCode()}, is shorter than two characters, and thus cannot be proxied. Please change the member name or use a longer system tag."); public static PKError ProxyNameTooLong(string name) => new( - $"The webhook's name, {name.AsCode()}, is too long ({name.Length} > {Limits.MaxProxyNameLength} characters), and thus cannot be proxied. Please change the member name, display name or server display name, or use a shorter system tag."); + $"The webhook's name, {name.AsCode()}, is too long ({name.Length} > {Limits.MaxProxyNameLength} characters), and thus cannot be proxied. Please change the member name, display name, server display name, system tag, or use a shorter name format"); public static PKError ProxyTagAlreadyExists(ProxyTag tagToAdd, PKMember member) => new( $"That member already has the proxy tag {tagToAdd.ProxyString.AsCode()}. The member currently has these tags: {member.ProxyTagsString()}"); 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/InteractionCreated.cs b/PluralKit.Bot/Handlers/InteractionCreated.cs index fb65a380..0a1c7be1 100644 --- a/PluralKit.Bot/Handlers/InteractionCreated.cs +++ b/PluralKit.Bot/Handlers/InteractionCreated.cs @@ -16,20 +16,23 @@ public class InteractionCreated: IEventHandler private readonly ApplicationCommandTree _commandTree; private readonly ILifetimeScope _services; private readonly ILogger _logger; + private readonly ModelRepository _repo; public InteractionCreated(InteractionDispatchService interactionDispatch, ApplicationCommandTree commandTree, - ILifetimeScope services, ILogger logger) + ILifetimeScope services, ILogger logger, ModelRepository repo) { _interactionDispatch = interactionDispatch; _commandTree = commandTree; _services = services; _logger = logger; + _repo = repo; } public async Task Handle(int shardId, InteractionCreateEvent evt) { - var system = await _services.Resolve().GetSystemByAccount(evt.Member?.User.Id ?? evt.User!.Id); - var ctx = new InteractionContext(_services, evt, system); + var system = await _repo.GetSystemByAccount(evt.Member?.User.Id ?? evt.User!.Id); + var config = system != null ? await _repo.GetSystemConfig(system!.Id) : null; + var ctx = new InteractionContext(_services, evt, system, config); switch (evt.Type) { diff --git a/PluralKit.Bot/Handlers/MessageCreated.cs b/PluralKit.Bot/Handlers/MessageCreated.cs index f4d84988..35edefa7 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.PermissionsIn(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) @@ -113,6 +114,11 @@ public class MessageCreated: IEventHandler if (!HasCommandPrefix(content, _config.ClientId, out var cmdStart) || cmdStart == content.Length) return false; + // if the command message was sent by a user account with bot usage disallowed, ignore it + var abuse_log = await _repo.GetAbuseLogByAccount(evt.Author.Id); + if (abuse_log != null && abuse_log.DenyBotUsage) + return false; + // Trim leading whitespace from command without actually modifying the string // This just moves the argPos pointer by however much whitespace is at the start of the post-argPos string var trimStartLengthDiff = @@ -123,7 +129,9 @@ public class MessageCreated: IEventHandler { var system = await _repo.GetSystemByAccount(evt.Author.Id); var config = system != null ? await _repo.GetSystemConfig(system.Id) : null; - await _tree.ExecuteCommand(new Context(_services, shardId, guild, channel, evt, cmdStart, system, config)); + var guildConfig = guild != null ? await _repo.GetGuild(guild.Id) : null; + + await _tree.ExecuteCommand(new Context(_services, shardId, guild, channel, evt, cmdStart, system, config, guildConfig)); } catch (PKError) { @@ -161,6 +169,9 @@ public class MessageCreated: IEventHandler using (_metrics.Measure.Timer.Time(BotMetrics.MessageContextQueryTime)) ctx = await _repo.GetMessageContext(evt.Author.Id, evt.GuildId ?? default, rootChannel, channel.Id != rootChannel ? channel.Id : default); + if (ctx.DenyBotUsage) + return false; + try { return await _proxy.HandleIncomingMessage(evt, ctx, guild, channel, true, botPermissions); diff --git a/PluralKit.Bot/Handlers/MessageEdited.cs b/PluralKit.Bot/Handlers/MessageEdited.cs index 2c869c7e..168feecb 100644 --- a/PluralKit.Bot/Handlers/MessageEdited.cs +++ b/PluralKit.Bot/Handlers/MessageEdited.cs @@ -52,11 +52,19 @@ public class MessageEdited: IEventHandler if (!evt.Content.HasValue || !evt.Author.HasValue || !evt.Member.HasValue) return; - var channel = await _cache.GetChannel(evt.ChannelId); + // we only use message edit event for proxying, so ignore messages from DMs + if (!evt.GuildId.HasValue || evt.GuildId.Value == null) return; + ulong guildId = evt.GuildId!.Value!.Value; + + var channel = await _cache.TryGetChannel(guildId, evt.ChannelId); // todo: is this correct for message update? + if (channel == null) + throw new Exception("could not find self channel in MessageEdited event"); if (!DiscordUtils.IsValidGuildChannel(channel)) return; - var rootChannel = await _cache.GetRootChannel(channel.Id); - var guild = await _cache.GetGuild(channel.GuildId!.Value); + var rootChannel = await _cache.GetRootChannel(guildId, channel.Id); + var guild = await _cache.TryGetGuild(channel.GuildId!.Value); + if (guild == null) + throw new Exception("could not find self guild in MessageEdited event"); var lastMessage = _lastMessageCache.GetLastMessage(evt.ChannelId)?.Current; // Only react to the last message in the channel @@ -67,9 +75,11 @@ public class MessageEdited: IEventHandler MessageContext ctx; using (_metrics.Measure.Timer.Time(BotMetrics.MessageContextQueryTime)) ctx = await _repo.GetMessageContext(evt.Author.Value!.Id, channel.GuildId!.Value, rootChannel.Id, evt.ChannelId); + if (ctx.DenyBotUsage) + return; var equivalentEvt = await GetMessageCreateEvent(evt, lastMessage, channel); - var botPermissions = await _cache.PermissionsIn(channel.Id); + var botPermissions = await _cache.BotPermissionsIn(guildId, channel.Id); try { @@ -91,7 +101,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 +128,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.PermissionsIn(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 db9381a8..5caa7d6f 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,16 +75,17 @@ 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; } } // Proxied messages only exist in guild text channels, so skip checking if we're elsewhere if (!DiscordUtils.IsValidGuildChannel(channel)) return; + var abuse_log = await _repo.GetAbuseLogByAccount(evt.Member!.User!.Id); switch (evt.Emoji.Name.Split("\U0000fe0f", 2)[0]) { @@ -113,6 +114,7 @@ public class ReactionAdded: IEventHandler case "\u23F0": // Alarm clock case "\u2757": // Exclamation mark { + if (abuse_log != null && abuse_log.DenyBotUsage) break; var msg = await _repo.GetFullMessage(evt.MessageId); if (msg != null) await HandlePingReaction(evt, msg); @@ -123,7 +125,7 @@ public class ReactionAdded: IEventHandler private async ValueTask HandleProxyDeleteReaction(MessageReactionAddEvent evt, PKMessage msg) { - if (!(await _cache.PermissionsIn(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 +152,7 @@ public class ReactionAdded: IEventHandler if (authorId != null && authorId != evt.UserId) return; - if (!((await _cache.PermissionsIn(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 @@ -175,6 +177,8 @@ public class ReactionAdded: IEventHandler private async ValueTask HandleQueryReaction(MessageReactionAddEvent evt, FullMessage msg) { var guild = await _cache.GetGuild(evt.GuildId!.Value); + var system = await _repo.GetSystemByAccount(evt.UserId); + var config = system != null ? await _repo.GetSystemConfig(system.Id) : null; // Try to DM the user info about the message try @@ -188,11 +192,12 @@ public class ReactionAdded: IEventHandler msg.System, msg.Member, guild, + config, LookupContext.ByNonOwner, DateTimeZone.Utc )); - embeds.Add(await _embeds.CreateMessageInfoEmbed(msg, true)); + embeds.Add(await _embeds.CreateMessageInfoEmbed(msg, true, config)); await _rest.CreateMessage(dm, new MessageRequest { Embeds = embeds.ToArray() }); } @@ -203,14 +208,14 @@ public class ReactionAdded: IEventHandler private async ValueTask HandlePingReaction(MessageReactionAddEvent evt, FullMessage msg) { - if (!(await _cache.PermissionsIn(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.PermissionsFor(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; @@ -263,7 +268,7 @@ public class ReactionAdded: IEventHandler private async Task TryRemoveOriginalReaction(MessageReactionAddEvent evt) { - if ((await _cache.PermissionsIn(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 2c4c0841..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(); @@ -136,6 +153,7 @@ public class BotModule: Module builder.RegisterType().AsSelf().SingleInstance(); builder.RegisterType().AsSelf().SingleInstance(); builder.RegisterType().AsSelf().SingleInstance(); + builder.RegisterType().AsSelf().SingleInstance(); // Sentry stuff builder.Register(_ => new Scope(null)).AsSelf().InstancePerLifetimeScope(); diff --git a/PluralKit.Bot/PluralKit.Bot.csproj b/PluralKit.Bot/PluralKit.Bot.csproj index 93098a0f..f2f35de6 100644 --- a/PluralKit.Bot/PluralKit.Bot.csproj +++ b/PluralKit.Bot/PluralKit.Bot.csproj @@ -26,8 +26,8 @@ - - + + diff --git a/PluralKit.Bot/Proxy/ProxyService.cs b/PluralKit.Bot/Proxy/ProxyService.cs index 67ae7c37..ebdb0fe3 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; @@ -102,7 +102,7 @@ public class ProxyService // Check if the sender account can mention everyone/here + embed links // we need to "mirror" these permissions when proxying to prevent exploits - var senderPermissions = PermissionExtensions.PermissionsFor(guild, rootChannel, message, isThread: rootChannel.Id != channel.Id); + var senderPermissions = PermissionExtensions.PermissionsFor(guild, rootChannel, message.Author.Id, message.Member, isThread: rootChannel.Id != channel.Id); var allowEveryone = senderPermissions.HasFlag(PermissionSet.MentionEveryone); var allowEmbeds = senderPermissions.HasFlag(PermissionSet.EmbedLinks); @@ -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 @@ -144,6 +123,21 @@ public class ProxyService return "PluralKit cannot proxy messages over 2000 characters in length."; } + if (ctx.RequireSystemTag) + { + if (!ctx.TagEnabled) + { + return "This server requires PluralKit users to have a system tag, but your system tag is disabled in this server. " + + "Use `pk;s servertag -enable` to enable it for this server."; + } + + if (!ctx.HasProxyableTag()) + { + return "This server requires PluralKit users to have a system tag, but you do not have one set. " + + "A system tag can be set for all servers with `pk;s tag`, or for just this server with `pk;s servertag`."; + } + } + var guild = await _cache.GetGuild(channel.GuildId.Value); var fileSizeLimit = guild.FileSizeLimit(); var bytesThreshold = fileSizeLimit * 1024 * 1024; @@ -159,6 +153,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 +184,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 +222,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 +237,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 +248,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 +307,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 +318,7 @@ public class ProxyService AllowEveryone = allowEveryone, Flags = originalMsg.Flags.HasFlag(Message.MessageFlags.VoiceMessage) ? Message.MessageFlags.VoiceMessage : null, Tts = tts, + Poll = originalMsg.Poll, }); @@ -497,10 +496,10 @@ public class ProxyService async Task SaveMessageInRedis() { // logclean info - await _redis.SetLogCleanup(triggerMessage.Author.Id, triggerMessage.GuildId.Value); + await _redis.SetLogCleanup(triggerMessage.Author.Id, proxyMessage.GuildId!.Value); // last message info (edit/reproxy) - await _redis.SetLastMessage(triggerMessage.Author.Id, triggerMessage.ChannelId, sentMessage.Mid); + await _redis.SetLastMessage(triggerMessage.Author.Id, proxyMessage.ChannelId, sentMessage.Mid); // "by original mid" lookup await _redis.SetOriginalMid(triggerMessage.Id, proxyMessage.Id); @@ -522,6 +521,10 @@ public class ProxyService Task DispatchWebhook() => _dispatch.Dispatch(ctx.SystemId.Value, sentMessage); + Task MaybeLogSwitch() => (ctx.ProxySwitch && !Array.Exists(ctx.LastSwitchMembers, element => element == match.Member.Id)) + ? _db.Execute(conn => _repo.AddSwitch(conn, (SystemId)ctx.SystemId, new[] { match.Member.Id })) + : Task.CompletedTask; + async Task DeleteProxyTriggerMessage() { if (!deletePrevious) @@ -555,7 +558,8 @@ public class ProxyService UpdateMemberForSentMessage(), LogMessageToChannel(), SaveLatchAutoproxy(), - DispatchWebhook() + DispatchWebhook(), + MaybeLogSwitch() ); } diff --git a/PluralKit.Bot/Services/AvatarHostingService.cs b/PluralKit.Bot/Services/AvatarHostingService.cs new file mode 100644 index 00000000..6e88c4af --- /dev/null +++ b/PluralKit.Bot/Services/AvatarHostingService.cs @@ -0,0 +1,79 @@ +using PluralKit.Core; +using System.Net; +using System.Net.Http.Json; + +namespace PluralKit.Bot; + +public class AvatarHostingService +{ + private readonly BotConfig _config; + private readonly HttpClient _client; + + public AvatarHostingService(BotConfig config) + { + _config = config; + _client = new HttpClient + { + Timeout = TimeSpan.FromSeconds(10), + }; + } + + public async Task TryRehostImage(ParsedImage input, RehostedImageType type, ulong userId, PKSystem? system) + { + try + { + 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; + } + 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) + { + if (!AvatarUtils.IsDiscordCdnUrl(avatarUrl)) + return null; + + if (_config.AvatarServiceUrl == null) + return null; + + var kind = type switch + { + RehostedImageType.Avatar => "avatar", + RehostedImageType.Banner => "banner", + _ => throw new ArgumentOutOfRangeException(nameof(type), type, null) + }; + + var response = await _client.PostAsJsonAsync(_config.AvatarServiceUrl + "/pull", + new { url = avatarUrl, kind, uploaded_by = userId, system_id = system?.Uuid.ToString() }); + if (response.StatusCode != HttpStatusCode.OK) + { + var error = await response.Content.ReadFromJsonAsync(); + throw new PKError($"Error uploading image to CDN: {error.Error}"); + } + + var success = await response.Content.ReadFromJsonAsync(); + return success.Url; + } + + public record ErrorResponse(string Error); + + public record SuccessResponse(string Url, bool New); + + public enum RehostedImageType + { + Avatar, + Banner, + } +} \ No newline at end of file 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 8077c0d2..308a347b 100644 --- a/PluralKit.Bot/Services/EmbedService.cs +++ b/PluralKit.Bot/Services/EmbedService.cs @@ -56,29 +56,18 @@ public class EmbedService var memberCount = await _repo.GetSystemMemberCount(system.Id, countctx == LookupContext.ByOwner ? null : PrivacyLevel.Public); - uint color; - try - { - color = system.Color?.ToDiscordColor() ?? DiscordUtils.Gray; - } - catch (ArgumentException) - { - // There's no API for system colors yet, but defaulting to a blank color in advance can't be a bad idea - color = DiscordUtils.Gray; - } - var eb = new EmbedBuilder() .Title(system.NameFor(ctx)) .Footer(new Embed.EmbedFooter( - $"System ID: {system.Hid} | Created on {system.Created.FormatZoned(cctx.Zone)}")) - .Color(color) + $"System ID: {system.DisplayHid(cctx.Config)} | Created on {system.Created.FormatZoned(cctx.Zone)}")) + .Color(system.Color?.ToDiscordColor()) .Url($"https://dash.pluralkit.me/profile/s/{system.Hid}"); var avatar = system.AvatarFor(ctx); if (avatar != null) eb.Thumbnail(new Embed.EmbedThumbnail(avatar)); - if (system.DescriptionPrivacy.CanAccess(ctx)) + if (system.BannerPrivacy.CanAccess(ctx)) eb.Image(new Embed.EmbedImage(system.BannerImage)); var latestSwitch = await _repo.GetLatestSwitch(system.Id); @@ -90,7 +79,7 @@ public class EmbedService { var memberStr = string.Join(", ", switchMembers.Select(m => m.NameFor(ctx))); if (memberStr.Length > 200) - memberStr = $"[too many to show, see `pk;system {system.Hid} fronters`]"; + memberStr = $"[too many to show, see `pk;system {system.DisplayHid(cctx.Config)} fronters`]"; eb.Field(new Embed.Field("Fronter".ToQuantity(switchMembers.Count, ShowQuantityAs.None), memberStr)); } } @@ -137,7 +126,7 @@ public class EmbedService { if (memberCount > 0) eb.Field(new Embed.Field($"Members ({memberCount})", - $"(see `pk;system {system.Hid} list` or `pk;system {system.Hid} list full`)", true)); + $"(see `pk;system {system.DisplayHid(cctx.Config)} list` or `pk;system {system.DisplayHid(cctx.Config)} list full`)", true)); else eb.Field(new Embed.Field($"Members ({memberCount})", "Add one with `pk;member new`!", true)); } @@ -175,7 +164,7 @@ public class EmbedService return embed.Build(); } - public async Task CreateMemberEmbed(PKSystem system, PKMember member, Guild guild, LookupContext ctx, DateTimeZone zone) + public async Task CreateMemberEmbed(PKSystem system, PKMember member, Guild guild, SystemConfig? ccfg, LookupContext ctx, DateTimeZone zone) { // string FormatTimestamp(Instant timestamp) => DateTimeFormats.ZonedDateTimeFormat.Format(timestamp.InZone(system.Zone)); @@ -188,19 +177,6 @@ public class EmbedService else name = $"{name}"; - uint color; - try - { - color = member.Color?.ToDiscordColor() ?? DiscordUtils.Gray; - } - catch (ArgumentException) - { - // Bad API use can cause an invalid color string - // this is now fixed in the API, but might still have some remnants in the database - // so we just default to a blank color, yolo - color = DiscordUtils.Gray; - } - var guildSettings = guild != null ? await _repo.GetMemberGuild(guild.Id, member.Id) : null; var guildDisplayName = guildSettings?.DisplayName; var webhook_avatar = guildSettings?.AvatarUrl ?? member.WebhookAvatarFor(ctx) ?? member.AvatarFor(ctx); @@ -213,12 +189,12 @@ public class EmbedService var eb = new EmbedBuilder() .Author(new Embed.EmbedAuthor(name, IconUrl: webhook_avatar.TryGetCleanCdnUrl(), Url: $"https://dash.pluralkit.me/profile/m/{member.Hid}")) - // .WithColor(member.ColorPrivacy.CanAccess(ctx) ? color : DiscordUtils.Gray) - .Color(color) + // .WithColor(member.ColorPrivacy.CanAccess(ctx) ? color : null) + .Color(member.Color?.ToDiscordColor()) .Footer(new Embed.EmbedFooter( - $"System ID: {system.Hid} | Member ID: {member.Hid} {(member.MetadataPrivacy.CanAccess(ctx) ? $"| Created on {member.Created.FormatZoned(zone)}" : "")}")); + $"System ID: {system.DisplayHid(ccfg)} | Member ID: {member.DisplayHid(ccfg)} {(member.MetadataPrivacy.CanAccess(ctx) ? $"| Created on {member.Created.FormatZoned(zone)}" : "")}")); - if (member.DescriptionPrivacy.CanAccess(ctx)) + if (member.BannerPrivacy.CanAccess(ctx)) eb.Image(new Embed.EmbedImage(member.BannerImage)); var description = ""; @@ -255,7 +231,7 @@ public class EmbedService // More than 5 groups show in "compact" format without ID var content = groups.Count > 5 ? string.Join(", ", groups.Select(g => g.DisplayName ?? g.Name)) - : string.Join("\n", groups.Select(g => $"[`{g.Hid}`] **{g.DisplayName ?? g.Name}**")); + : string.Join("\n", groups.Select(g => $"[`{g.DisplayHid(ccfg, isList: true)}`] **{g.DisplayName ?? g.Name}**")); eb.Field(new Embed.Field($"Groups ({groups.Count})", content.Truncate(1000))); } @@ -287,26 +263,15 @@ public class EmbedService else if (system.NameFor(ctx) != null) nameField = $"{nameField} ({system.NameFor(ctx)})"; else - nameField = $"{nameField} ({system.Name})"; - - uint color; - try - { - color = target.Color?.ToDiscordColor() ?? DiscordUtils.Gray; - } - catch (ArgumentException) - { - // There's no API for group colors yet, but defaulting to a blank color regardless - color = DiscordUtils.Gray; - } + nameField = $"{nameField}"; var eb = new EmbedBuilder() .Author(new Embed.EmbedAuthor(nameField, IconUrl: target.IconFor(pctx), Url: $"https://dash.pluralkit.me/profile/g/{target.Hid}")) - .Color(color); + .Color(target.Color?.ToDiscordColor()); - eb.Footer(new Embed.EmbedFooter($"System ID: {system.Hid} | Group ID: {target.Hid}{(target.MetadataPrivacy.CanAccess(pctx) ? $" | Created on {target.Created.FormatZoned(ctx.Zone)}" : "")}")); + eb.Footer(new Embed.EmbedFooter($"System ID: {system.DisplayHid(ctx.Config)} | Group ID: {target.DisplayHid(ctx.Config)}{(target.MetadataPrivacy.CanAccess(pctx) ? $" | Created on {target.Created.FormatZoned(ctx.Zone)}" : "")}")); - if (target.DescriptionPrivacy.CanAccess(pctx)) + if (target.BannerPrivacy.CanAccess(pctx)) eb.Image(new Embed.EmbedImage(target.BannerImage)); if (target.NamePrivacy.CanAccess(pctx) && target.DisplayName != null) @@ -324,7 +289,7 @@ public class EmbedService { var name = pctx == LookupContext.ByOwner ? target.Reference(ctx) - : target.Hid; + : target.DisplayHid(ctx.Config); eb.Field(new Embed.Field($"Members ({memberCount})", $"(see `pk;group {name} list`)")); } } @@ -362,16 +327,16 @@ public class EmbedService } return new EmbedBuilder() - .Color(members.FirstOrDefault()?.Color?.ToDiscordColor() ?? DiscordUtils.Gray) + .Color(members.FirstOrDefault()?.Color?.ToDiscordColor()) .Field(new Embed.Field($"Current {"fronter".ToQuantity(members.Count, ShowQuantityAs.None)}", memberStr)) .Field(new Embed.Field("Since", $"{sw.Timestamp.FormatZoned(zone)} ({timeSinceSwitch.FormatDuration()} ago)")) .Build(); } - public async Task CreateMessageInfoEmbed(FullMessage msg, bool showContent) + 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); @@ -424,27 +389,29 @@ public class EmbedService .Field(new Embed.Field("System", msg.System == null ? "*(deleted or unknown system)*" - : msg.System.NameFor(ctx) != null ? $"{msg.System.NameFor(ctx)} (`{msg.System.Hid}`)" : $"`{msg.System.Hid}`" + : msg.System.NameFor(ctx) != null ? $"{msg.System.NameFor(ctx)} (`{msg.System.DisplayHid(ccfg)}`)" : $"`{msg.System.DisplayHid(ccfg)}`" , true)) .Field(new Embed.Field("Member", msg.Member == null ? "*(deleted member)*" - : $"{msg.Member.NameFor(ctx)} (`{msg.Member.Hid}`)" + : $"{msg.Member.NameFor(ctx)} (`{msg.Member.DisplayHid(ccfg)}`)" , true)) .Field(new Embed.Field("Sent by", userStr, true)) - .Timestamp(DiscordUtils.SnowflakeToInstant(msg.Message.Mid).ToDateTimeOffset().ToString("O")); + .Timestamp(DiscordUtils.SnowflakeToInstant(msg.Message.Mid).ToDateTimeOffset().ToString("O")) + .Footer(new Embed.EmbedFooter($"Original Message ID: {msg.Message.OriginalMid}")); 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))); @@ -460,19 +427,9 @@ public class EmbedService var color = system.Color; if (group != null) color = group.Color; - uint embedColor; - try - { - embedColor = color?.ToDiscordColor() ?? DiscordUtils.Gray; - } - catch (ArgumentException) - { - embedColor = DiscordUtils.Gray; - } - var eb = new EmbedBuilder() .Title(embedTitle) - .Color(embedColor); + .Color(color?.ToDiscordColor()); var footer = $"Since {breakdown.RangeStart.FormatZoned(tz)} ({(breakdown.RangeEnd - breakdown.RangeStart).FormatDuration()} ago)"; 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 74a637dd..6810ee24 100644 --- a/PluralKit.Bot/Services/LoggerCleanService.cs +++ b/PluralKit.Bot/Services/LoggerCleanService.cs @@ -23,6 +23,8 @@ 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 _sapphireRegex = 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})"); @@ -62,6 +64,8 @@ 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("Sapphire", 678344927997853742, ExtractSapphire), // webhook new LoggerBot("Auttaja", 242730576195354624, ExtractAuttaja), // webhook new LoggerBot("GenericBot", 295329346590343168, ExtractGenericBot), new LoggerBot("blargbot", 134133271750639616, ExtractBlargBot), @@ -101,10 +105,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.PermissionsIn(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. @@ -239,6 +243,26 @@ 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") ?? false)) return null; + var match = _makiRegex.Match(embed.Footer.Text ?? ""); + return match.Success ? ulong.Parse(match.Groups[1].Value) : null; + } + + private static ulong? ExtractSapphire(Message msg) + { + // Embed, Message title field: "Message deleted", description contains "**Message ID:** [[id]]" + // Example: "**Message ID:** [1297549791927996598]" + var embed = msg.Embeds?.FirstOrDefault(); + if (embed == null) return null; + if (!(embed.Title?.StartsWith("Message deleted") ?? false)) return null; + var match = _sapphireRegex.Match(embed.Description); + 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/WebhookCacheService.cs b/PluralKit.Bot/Services/WebhookCacheService.cs index 8900d930..eff638f9 100644 --- a/PluralKit.Bot/Services/WebhookCacheService.cs +++ b/PluralKit.Bot/Services/WebhookCacheService.cs @@ -94,9 +94,9 @@ public class WebhookCacheService // We don't have one, so we gotta create a new one // but first, make sure we haven't hit the webhook cap yet... - if (webhooks.Length >= 10) + if (webhooks.Length >= 15) throw new PKError( - "This channel has the maximum amount of possible webhooks (10) already created. A server admin must delete one or more webhooks so PluralKit can create one for proxying."); + "This channel has the maximum amount of possible webhooks (15) already created. A server admin must delete one or more webhooks so PluralKit can create one for proxying."); return await DoCreateWebhook(channelId); } diff --git a/PluralKit.Bot/Services/WebhookExecutorService.cs b/PluralKit.Bot/Services/WebhookExecutorService.cs index fa38d22f..999920d6 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; @@ -17,7 +19,6 @@ using Newtonsoft.Json.Linq; using Serilog; -using PluralKit.Core; using Myriad.Utils; namespace PluralKit.Bot; @@ -35,6 +36,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 +47,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 +86,8 @@ 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, bool clearAttachments = 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; @@ -104,7 +108,10 @@ public class WebhookExecutorService { Content = newContent, AllowedMentions = allowedMentions, - Embeds = (clearEmbeds == true ? Optional.Some(new Embed[] { }) : Optional.None()), + Embeds = (clearEmbeds ? Optional.Some(new Embed[] { }) : Optional.None()), + Attachments = (clearAttachments + ? Optional.Some(new Message.Attachment[] { }) + : Optional.None()) }; return await _rest.EditWebhookMessage(webhook.Id, webhook.Token, messageId, editReq, threadId); @@ -154,6 +161,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/AvatarUtils.cs b/PluralKit.Bot/Utils/AvatarUtils.cs index 1fd573c4..6d986151 100644 --- a/PluralKit.Bot/Utils/AvatarUtils.cs +++ b/PluralKit.Bot/Utils/AvatarUtils.cs @@ -63,11 +63,23 @@ public static class AvatarUtils // This lets us add resizing parameters to "borrow" their media proxy server to downsize the image // which in turn makes it more likely to be underneath the size limit! private static readonly Regex DiscordCdnUrl = - new(@"^https?://(?:cdn\.discordapp\.com|media\.discordapp\.net)/attachments/(\d{17,19})/(\d{17,19})/([^/\\&\?]+)\.(png|jpg|jpeg|webp)(\?.*)?$"); + new(@"^https?://(?:cdn\.discordapp\.com|media\.discordapp\.net)/attachments/(\d{17,19})/(\d{17,19})/([^/\\&\?]+)\.(png|jpe?g|gif|webp)(?:\?(?.*))?$", RegexOptions.IgnoreCase); private static readonly string DiscordMediaUrlReplacement = "https://media.discordapp.net/attachments/$1/$2/$3.$4?width=256&height=256"; - public static string? TryRewriteCdnUrl(string? url) => - url == null ? null : DiscordCdnUrl.Replace(url, DiscordMediaUrlReplacement); + public static string? TryRewriteCdnUrl(string? url) + { + if (url == null) + return null; + + var match = DiscordCdnUrl.Match(url); + var newUrl = DiscordCdnUrl.Replace(url, DiscordMediaUrlReplacement); + if (match.Groups["query"].Success) + newUrl += "&" + match.Groups["query"].Value; + + return newUrl; + } + + public static bool IsDiscordCdnUrl(string? url) => url != null && DiscordCdnUrl.Match(url).Success; } \ No newline at end of file diff --git a/PluralKit.Bot/Utils/ContextUtils.cs b/PluralKit.Bot/Utils/ContextUtils.cs index 79b94c3a..ce353472 100644 --- a/PluralKit.Bot/Utils/ContextUtils.cs +++ b/PluralKit.Bot/Utils/ContextUtils.cs @@ -55,7 +55,7 @@ public static class ContextUtils .WaitFor(ReactionPredicate, timeout); } - public static async Task ConfirmWithReply(this Context ctx, string expectedReply) + public static async Task ConfirmWithReply(this Context ctx, string expectedReply, bool treatAsHid = false) { bool Predicate(MessageCreateEvent e) => e.Author.Id == ctx.Author.Id && e.ChannelId == ctx.Channel.Id; @@ -63,7 +63,11 @@ public static class ContextUtils var msg = await ctx.Services.Resolve>() .WaitFor(Predicate, Duration.FromMinutes(1)); - return string.Equals(msg.Content, expectedReply, StringComparison.InvariantCultureIgnoreCase); + var content = msg.Content; + if (treatAsHid) + content = content.ToLower().Replace("-", null); + + return string.Equals(content, expectedReply, StringComparison.InvariantCultureIgnoreCase); } public static async Task Paginate(this Context ctx, IAsyncEnumerable items, int totalCount, diff --git a/PluralKit.Bot/Utils/DiscordUtils.cs b/PluralKit.Bot/Utils/DiscordUtils.cs index b3ea5625..26fd099e 100644 --- a/PluralKit.Bot/Utils/DiscordUtils.cs +++ b/PluralKit.Bot/Utils/DiscordUtils.cs @@ -20,7 +20,6 @@ public static class DiscordUtils public const uint Blue = 0x1f99d8; public const uint Green = 0x00cc78; public const uint Red = 0xef4b3d; - public const uint Gray = 0x979c9f; private static readonly Regex USER_MENTION = new("<@!?(\\d{17,19})>"); private static readonly Regex ROLE_MENTION = new("<@&(\\d{17,19})>"); @@ -35,7 +34,7 @@ public static class DiscordUtils private static readonly Regex UNBROKEN_LINK_REGEX = new("?"); public static string NameAndMention(this User user) => - $"{user.Username}{(user.Discriminator == "0" ? "" : $"#{user.Discriminator}")} ({user.Mention()})"; + $"{user.Username.EscapeMarkdown()}{(user.Discriminator == "0" ? "" : $"#{user.Discriminator}")} ({user.Mention()})"; public static Instant SnowflakeToInstant(ulong snowflake) => Instant.FromUtc(2015, 1, 1, 0, 0, 0) + Duration.FromMilliseconds(snowflake >> 22); diff --git a/PluralKit.Bot/Utils/InteractionContext.cs b/PluralKit.Bot/Utils/InteractionContext.cs index 59b9aab1..444d39f4 100644 --- a/PluralKit.Bot/Utils/InteractionContext.cs +++ b/PluralKit.Bot/Utils/InteractionContext.cs @@ -16,10 +16,11 @@ public class InteractionContext private readonly ILifetimeScope _provider; private readonly IMetrics _metrics; - public InteractionContext(ILifetimeScope provider, InteractionCreateEvent evt, PKSystem system) + public InteractionContext(ILifetimeScope provider, InteractionCreateEvent evt, PKSystem system, SystemConfig config) { Event = evt; System = system; + Config = config; Cache = provider.Resolve(); Rest = provider.Resolve(); Repository = provider.Resolve(); @@ -31,6 +32,7 @@ public class InteractionContext internal readonly DiscordApiClient Rest; internal readonly ModelRepository Repository; public readonly PKSystem System; + public readonly SystemConfig Config; public InteractionCreateEvent Event { get; } @@ -74,12 +76,22 @@ public class InteractionContext }); } + public async Task Defer() + { + await Respond(InteractionResponse.ResponseType.DeferredChannelMessageWithSource, + new InteractionApplicationCommandCallbackData + { + Components = Array.Empty(), + Flags = Message.MessageFlags.Ephemeral, + }); + } + public async Task Ignore() { await Respond(InteractionResponse.ResponseType.DeferredUpdateMessage, new InteractionApplicationCommandCallbackData { - Components = Event.Message.Components + Components = Event.Message?.Components ?? Array.Empty() }); } diff --git a/PluralKit.Bot/Utils/MiscUtils.cs b/PluralKit.Bot/Utils/MiscUtils.cs index ed05fd01..5ee99901 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; @@ -89,12 +89,15 @@ public static class MiscUtils if (e is NpgsqlException tpe && tpe.InnerException is TimeoutException) return false; - // Ignore thread pool exhaustion errors - if (e is NpgsqlException npe && npe.Message.Contains("The connection pool has been exhausted")) - return false; - - // ignore "Exception while reading from stream" - if (e is NpgsqlException npe2 && npe2.Message.Contains("Exception while reading from stream")) + if (e is NpgsqlException npe && + ( + // Ignore thread pool exhaustion errors + npe.Message.Contains("The connection pool has been exhausted") + // ignore "Exception while reading from stream" + || npe.Message.Contains("Exception while reading from stream") + // ignore "Exception while connecting" + || npe.Message.Contains("Exception while connecting") + )) return false; return true; diff --git a/PluralKit.Bot/Utils/ModelUtils.cs b/PluralKit.Bot/Utils/ModelUtils.cs index 4df17c32..ddeedfee 100644 --- a/PluralKit.Bot/Utils/ModelUtils.cs +++ b/PluralKit.Bot/Utils/ModelUtils.cs @@ -24,8 +24,20 @@ public static class ModelUtils public static string DisplayName(this PKMember member) => member.DisplayName ?? member.Name; - public static string Reference(this PKMember member, Context ctx) => EntityReference(member.Hid, member.NameFor(ctx)); - public static string Reference(this PKGroup group, Context ctx) => EntityReference(group.Hid, group.NameFor(ctx)); + public static string Reference(this PKMember member, Context ctx) => EntityReference(member.DisplayHid(ctx.Config), member.NameFor(ctx)); + public static string Reference(this PKGroup group, Context ctx) => EntityReference(group.DisplayHid(ctx.Config), group.NameFor(ctx)); + + + 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, 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 && 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 d7a34fd7..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.PermissionsIn(channel.Value); + var botPermissions = await _cache.BotPermissionsIn(guildIdForCache, channel.Value); props.Add(new LogEventProperty("BotPermissions", new ScalarValue(botPermissions))); } } @@ -52,7 +54,7 @@ public class SerilogGatewayEnricherFactory props.Add(new LogEventProperty("UserId", new ScalarValue(user.Value))); if (evt is MessageCreateEvent mce) - props.Add(new LogEventProperty("UserPermissions", new ScalarValue(await _cache.PermissionsFor(mce)))); + props.Add(new LogEventProperty("UserPermissions", new ScalarValue(await _cache.PermissionsForMCE(mce)))); return new Inner(props); } diff --git a/PluralKit.Bot/packages.lock.json b/PluralKit.Bot/packages.lock.json index 078a19d1..6ef93128 100644 --- a/PluralKit.Bot/packages.lock.json +++ b/PluralKit.Bot/packages.lock.json @@ -36,15 +36,15 @@ }, "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", - "requested": "[3.0.1, )", - "resolved": "3.0.1", - "contentHash": "o0v/J6SJwp3RFrzR29beGx0cK7xcMRgOyIuw8ZNLQyNnBhiyL/vIQKn7cfycthcWUPG3XezUjFwBWzkcUUDFbg==" + "requested": "[3.1.5, )", + "resolved": "3.1.5", + "contentHash": "lNtlq7dSI/QEbYey+A0xn48z5w4XHSffF8222cC4F4YwTXfEImuiBavQcWjr49LThT/pRmtWJRcqA/PlL+eJ6g==" }, "App.Metrics": { "type": "Transitive", @@ -337,8 +337,8 @@ }, "Microsoft.Extensions.Logging.Abstractions": { "type": "Transitive", - "resolved": "3.1.10", - "contentHash": "bKHbgzbGsPZbEaExRaJqBz3WQ1GfhMttM23e1nivLJ8HbA3Ad526mW2G2K350q3Dc3HG83I5W8uSZWG4Rv4IpA==" + "resolved": "6.0.0", + "contentHash": "/HggWBbTwy8TgebGSX5DBZ24ndhzi93sHUBDvP1IxbZD7FDokYzdAr6+vbWGjw2XAfR2EJ1sfKUotpjHnFWPxA==" }, "Microsoft.Extensions.Options": { "type": "Transitive", @@ -356,8 +356,8 @@ }, "Microsoft.NETCore.Platforms": { "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "VyPlqzH2wavqquTcYpkIIAQ6WdenuKoFN0BdYBbCWsclXacSOHNQn66Gt4z5NBqEYW0FAPm5rlvki9ZiCij5xQ==" + "resolved": "1.1.0", + "contentHash": "kz0PEW2lhqygehI/d6XsPCQzD7ff7gUJaVGPVETX611eadGsA3A877GdSlU0LRVMCTH/+P3o2iDTak+S08V2+A==" }, "Microsoft.NETCore.Targets": { "type": "Transitive", @@ -374,23 +374,6 @@ "System.Runtime": "4.3.0" } }, - "Microsoft.Win32.Registry": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==", - "dependencies": { - "System.Security.AccessControl": "5.0.0", - "System.Security.Principal.Windows": "5.0.0" - } - }, - "Microsoft.Win32.SystemEvents": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "Bh6blKG8VAKvXiLe2L+sEsn62nc1Ij34MrNxepD2OCrS5cpCwQa9MeLyhVQPQ/R4Wlzwuy6wMK8hLb11QPDRsQ==", - "dependencies": { - "Microsoft.NETCore.Platforms": "5.0.0" - } - }, "NETStandard.Library": { "type": "Transitive", "resolved": "1.6.1", @@ -466,8 +449,8 @@ }, "Npgsql": { "type": "Transitive", - "resolved": "4.1.5", - "contentHash": "juDlNse+SKfXRP0VSgpJkpdCcaVLZt8m37EHdRX+8hw+GG69Eat1Y0MdEfl+oetdOnf9E133GjIDEjg9AF6HSQ==", + "resolved": "4.1.13", + "contentHash": "p79cObfuRgS8KD5sFmQUqVlINEkJm39bCrzRclicZE1942mKcbLlc0NdoVKhBeZPv//prK/sVTUmRVxdnoPCoA==", "dependencies": { "System.Runtime.CompilerServices.Unsafe": "4.6.0" } @@ -483,10 +466,10 @@ }, "Pipelines.Sockets.Unofficial": { "type": "Transitive", - "resolved": "2.2.0", - "contentHash": "7hzHplEIVOGBl5zOQZGX/DiJDHjq+RVRVrYgDiqXb6RriqWAdacXxp+XO9WSrATCEXyNOUOQg9aqQArsjase/A==", + "resolved": "2.2.8", + "contentHash": "zG2FApP5zxSx6OcdJQLbZDk2AVlN2BNQD6MorwIfV6gVj0RRxWPEp2LXAxqDGZqeNV1Zp0BNPcNaey/GXmTdvQ==", "dependencies": { - "System.IO.Pipelines": "5.0.0" + "System.IO.Pipelines": "5.0.1" } }, "Polly": { @@ -726,11 +709,11 @@ }, "StackExchange.Redis": { "type": "Transitive", - "resolved": "2.2.88", - "contentHash": "JJi1jcO3/ZiamBhlsC/TR8aZmYf+nqpGzMi0HRRCy5wJkUPmMnRp0kBA6V84uhU8b531FHSdTDaFCAyCUJomjA==", + "resolved": "2.8.16", + "contentHash": "WaoulkOqOC9jHepca3JZKFTqndCWab5uYS7qCzmiQDlrTkFaDN7eLSlEfHycBxipRnQY9ppZM7QSsWAwUEGblw==", "dependencies": { - "Pipelines.Sockets.Unofficial": "2.2.0", - "System.Diagnostics.PerformanceCounter": "5.0.0" + "Microsoft.Extensions.Logging.Abstractions": "6.0.0", + "Pipelines.Sockets.Unofficial": "2.2.8" } }, "System.AppContext": { @@ -773,15 +756,6 @@ "System.Threading.Tasks": "4.3.0" } }, - "System.Configuration.ConfigurationManager": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "aM7cbfEfVNlEEOj3DsZP+2g9NRwbkyiAv2isQEzw7pnkDg9ekCU2m1cdJLM02Uq691OaCS91tooaxcEn8d0q5w==", - "dependencies": { - "System.Security.Cryptography.ProtectedData": "5.0.0", - "System.Security.Permissions": "5.0.0" - } - }, "System.Console": { "type": "Transitive", "resolved": "4.3.0", @@ -809,17 +783,6 @@ "resolved": "4.7.1", "contentHash": "j81Lovt90PDAq8kLpaJfJKV/rWdWuEk6jfV+MBkee33vzYLEUsy4gXK8laa9V2nZlLM9VM9yA/OOQxxPEJKAMw==" }, - "System.Diagnostics.PerformanceCounter": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "kcQWWtGVC3MWMNXdMDWfrmIlFZZ2OdoeT6pSNVRtk9+Sa7jwdPiMlNwb0ZQcS7NRlT92pCfmjRtkSWUW3RAKwg==", - "dependencies": { - "Microsoft.NETCore.Platforms": "5.0.0", - "Microsoft.Win32.Registry": "5.0.0", - "System.Configuration.ConfigurationManager": "5.0.0", - "System.Security.Principal.Windows": "5.0.0" - } - }, "System.Diagnostics.Tools": { "type": "Transitive", "resolved": "4.3.0", @@ -840,14 +803,6 @@ "System.Runtime": "4.3.0" } }, - "System.Drawing.Common": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "SztFwAnpfKC8+sEKXAFxCBWhKQaEd97EiOL7oZJZP56zbqnLpmxACWA8aGseaUExciuEAUuR9dY8f7HkTRAdnw==", - "dependencies": { - "Microsoft.Win32.SystemEvents": "5.0.0" - } - }, "System.Globalization": { "type": "Transitive", "resolved": "4.3.0", @@ -965,8 +920,8 @@ }, "System.IO.Pipelines": { "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "irMYm3vhVgRsYvHTU5b2gsT2CwT/SMM6LZFzuJjpIvT5Z4CshxNsaoBC1X/LltwuR3Opp8d6jOS/60WwOb7Q2Q==" + "resolved": "5.0.1", + "contentHash": "qEePWsaq9LoEEIqhbGe6D5J8c9IqQOUuTzzV6wn1POlfdLkJliZY3OlB0j0f17uMWlqZYjH7txj+2YbyrIA8Yg==" }, "System.Linq": { "type": "Transitive", @@ -1229,15 +1184,6 @@ "System.Runtime.Extensions": "4.3.0" } }, - "System.Security.AccessControl": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "dagJ1mHZO3Ani8GH0PHpPEe/oYO+rVdbQjvjJkBRNQkX4t0r1iaeGn8+/ybkSLEan3/slM0t59SVdHzuHf2jmw==", - "dependencies": { - "Microsoft.NETCore.Platforms": "5.0.0", - "System.Security.Principal.Windows": "5.0.0" - } - }, "System.Security.Cryptography.Algorithms": { "type": "Transitive", "resolved": "4.3.0", @@ -1350,11 +1296,6 @@ "System.Threading.Tasks": "4.3.0" } }, - "System.Security.Cryptography.ProtectedData": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "HGxMSAFAPLNoxBvSfW08vHde0F9uh7BjASwu6JF9JnXuEPhCY3YUqURn0+bQV/4UWeaqymmrHWV+Aw9riQCtCA==" - }, "System.Security.Cryptography.X509Certificates": { "type": "Transitive", "resolved": "4.3.0", @@ -1387,20 +1328,6 @@ "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" } }, - "System.Security.Permissions": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "uE8juAhEkp7KDBCdjDIE3H9R1HJuEHqeqX8nLX9gmYKWwsqk3T5qZlPx8qle5DPKimC/Fy3AFTdV7HamgCh9qQ==", - "dependencies": { - "System.Security.AccessControl": "5.0.0", - "System.Windows.Extensions": "5.0.0" - } - }, - "System.Security.Principal.Windows": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "t0MGLukB5WAVU9bO3MGzvlGnyJPgUlcwerXn1kzBRjwLKixT96XV0Uza41W49gVd8zEMFu9vQEFlv0IOrytICA==" - }, "System.Text.Encoding": { "type": "Transitive", "resolved": "4.3.0", @@ -1474,14 +1401,6 @@ "System.Runtime": "4.3.0" } }, - "System.Windows.Extensions": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "c1ho9WU9ZxMZawML+ssPKZfdnrg/OjR3pe0m9v8230z3acqphwvPJqzAkH54xRYm5ntZHGG1EPP3sux9H3qSPg==", - "dependencies": { - "System.Drawing.Common": "5.0.0" - } - }, "System.Xml.ReaderWriter": { "type": "Transitive", "resolved": "4.3.0", @@ -1556,7 +1475,7 @@ "Newtonsoft.Json": "[13.0.1, )", "NodaTime": "[3.0.3, )", "NodaTime.Serialization.JsonNet": "[3.0.0, )", - "Npgsql": "[4.1.5, )", + "Npgsql": "[4.1.13, )", "Npgsql.NodaTime": "[4.1.5, )", "Serilog": "[2.12.0, )", "Serilog.Extensions.Logging": "[3.0.1, )", @@ -1569,7 +1488,7 @@ "Serilog.Sinks.Seq": "[5.2.2, )", "SqlKata": "[2.3.7, )", "SqlKata.Execution": "[2.3.7, )", - "StackExchange.Redis": "[2.2.88, )", + "StackExchange.Redis": "[2.8.16, )", "System.Interactive.Async": "[5.0.0, )", "ipnetwork2": "[2.5.381, )" } diff --git a/PluralKit.Core/CoreConfig.cs b/PluralKit.Core/CoreConfig.cs index 29fa664b..4adf815b 100644 --- a/PluralKit.Core/CoreConfig.cs +++ b/PluralKit.Core/CoreConfig.cs @@ -8,13 +8,14 @@ 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; } public string LogDir { get; set; } public string? ElasticUrl { get; set; } public string? SeqLogUrl { get; set; } + public string? DispatchProxyUrl { get; set; } + public string? DispatchProxyToken { get; set; } public LogEventLevel ConsoleLogLevel { get; set; } = LogEventLevel.Debug; public LogEventLevel ElasticLogLevel { get; set; } = LogEventLevel.Information; diff --git a/PluralKit.Core/Database/Database.cs b/PluralKit.Core/Database/Database.cs index d7625dd9..ec3a510a 100644 --- a/PluralKit.Core/Database/Database.cs +++ b/PluralKit.Core/Database/Database.cs @@ -83,10 +83,12 @@ internal partial class Database: IDatabase SqlMapper.AddTypeHandler(new NumericIdHandler(i => new MemberId(i))); SqlMapper.AddTypeHandler(new NumericIdHandler(i => new SwitchId(i))); SqlMapper.AddTypeHandler(new NumericIdHandler(i => new GroupId(i))); + SqlMapper.AddTypeHandler(new NumericIdHandler(i => new AbuseLogId(i))); SqlMapper.AddTypeHandler(new NumericIdArrayHandler(i => new SystemId(i))); SqlMapper.AddTypeHandler(new NumericIdArrayHandler(i => new MemberId(i))); SqlMapper.AddTypeHandler(new NumericIdArrayHandler(i => new SwitchId(i))); SqlMapper.AddTypeHandler(new NumericIdArrayHandler(i => new GroupId(i))); + SqlMapper.AddTypeHandler(new NumericIdArrayHandler(i => new AbuseLogId(i))); // Register our custom types to Npgsql // Without these it'll still *work* but break at the first launch + probably cause other small issues diff --git a/PluralKit.Core/Database/Functions/MessageContext.cs b/PluralKit.Core/Database/Functions/MessageContext.cs index 8d424dce..7f767b26 100644 --- a/PluralKit.Core/Database/Functions/MessageContext.cs +++ b/PluralKit.Core/Database/Functions/MessageContext.cs @@ -18,6 +18,7 @@ public class MessageContext public bool InBlacklist { get; } public bool InLogBlacklist { get; } public bool LogCleanupEnabled { get; } + public bool RequireSystemTag { get; } public bool ProxyEnabled { get; } public SwitchId? LastSwitch { get; } public MemberId[] LastSwitchMembers { get; } = new MemberId[0]; @@ -25,10 +26,13 @@ public class MessageContext public string? SystemTag { get; } public string? SystemGuildTag { get; } public bool TagEnabled { get; } + public string? NameFormat { get; } public string? SystemAvatar { get; } public string? SystemGuildAvatar { get; } public bool AllowAutoproxy { get; } public int? LatchTimeout { get; } public bool CaseSensitiveProxyTags { get; } public bool ProxyErrorMessageEnabled { get; } + public bool ProxySwitch { get; } + public bool DenyBotUsage { get; } } \ No newline at end of file diff --git a/PluralKit.Core/Database/Functions/MessageContextExt.cs b/PluralKit.Core/Database/Functions/MessageContextExt.cs new file mode 100644 index 00000000..bca26b2d --- /dev/null +++ b/PluralKit.Core/Database/Functions/MessageContextExt.cs @@ -0,0 +1,18 @@ +#nullable enable + +namespace PluralKit.Core; +public static class MessageContextExt +{ + public static bool HasProxyableTag(this MessageContext ctx) + { + var tag = ctx.SystemGuildTag ?? ctx.SystemTag; + if (!ctx.TagEnabled || tag == null) + return false; + + var format = ctx.NameFormat ?? ProxyMember.DefaultFormat; + if (!format.Contains("{tag}")) + return false; + + return true; + } +} \ No newline at end of file diff --git a/PluralKit.Core/Database/Functions/ProxyMember.cs b/PluralKit.Core/Database/Functions/ProxyMember.cs index 5221af32..0df7cf35 100644 --- a/PluralKit.Core/Database/Functions/ProxyMember.cs +++ b/PluralKit.Core/Database/Functions/ProxyMember.cs @@ -31,17 +31,21 @@ public class ProxyMember public bool AllowAutoproxy { get; } public string? Color { get; } + // If not set, this formatting will be applied to the proxy name + public static string DefaultFormat = "{name} {tag}"; + + public static string FormatTag(string template, string? tag, string name) => StringUtils.SafeFormat(template, new[] { + ("{tag}", tag ?? ""), + ("{name}", name) + }).Trim(); + public string ProxyName(MessageContext ctx) { var memberName = ServerName ?? DisplayName ?? Name; - if (!ctx.TagEnabled) - return memberName; + var tag = ctx.SystemGuildTag ?? ctx.SystemTag; + if (!ctx.TagEnabled) tag = null; - if (ctx.SystemGuildTag != null) - return $"{memberName} {ctx.SystemGuildTag}"; - if (ctx.SystemTag != null) - return $"{memberName} {ctx.SystemTag}"; - return memberName; + return FormatTag(ctx.NameFormat ?? DefaultFormat, tag, memberName); } public string? ProxyAvatar(MessageContext ctx) => ServerAvatar ?? WebhookAvatar ?? Avatar ?? ctx.SystemGuildAvatar ?? ctx.SystemAvatar; diff --git a/PluralKit.Core/Database/Functions/functions.sql b/PluralKit.Core/Database/Functions/functions.sql index 152e85b5..693dff5f 100644 --- a/PluralKit.Core/Database/Functions/functions.sql +++ b/PluralKit.Core/Database/Functions/functions.sql @@ -1,66 +1,87 @@ -create function message_context(account_id bigint, guild_id bigint, channel_id bigint, thread_id bigint) +create function message_context(account_id bigint, guild_id bigint, channel_id bigint, thread_id bigint) returns table ( + allow_autoproxy bool, + system_id int, + system_tag text, + system_avatar text, + + latch_timeout integer, + case_sensitive_proxy_tags bool, + proxy_error_message_enabled bool, + proxy_switch bool, + name_format text, + + tag_enabled bool, + proxy_enabled bool, + system_guild_tag text, + system_guild_avatar text, + + last_switch int, + last_switch_members int[], + last_switch_timestamp timestamp, + log_channel bigint, in_blacklist bool, in_log_blacklist bool, log_cleanup_enabled bool, - proxy_enabled bool, - last_switch int, - last_switch_members int[], - last_switch_timestamp timestamp, - system_tag text, - system_guild_tag text, - tag_enabled bool, - system_avatar text, - system_guild_avatar text, - allow_autoproxy bool, - latch_timeout integer, - case_sensitive_proxy_tags bool, - proxy_error_message_enabled bool + require_system_tag bool, + + deny_bot_usage bool ) as $$ - -- CTEs to query "static" (accessible only through args) data - with - system as (select systems.*, system_config.latch_timeout, system_guild.tag as guild_tag, system_guild.tag_enabled as tag_enabled, - system_guild.avatar_url as guild_avatar, - allow_autoproxy as account_autoproxy, system_config.case_sensitive_proxy_tags, system_config.proxy_error_message_enabled from accounts - left join systems on systems.id = accounts.system - left join system_config on system_config.system = accounts.system - left join system_guild on system_guild.system = accounts.system and system_guild.guild = guild_id - where accounts.uid = account_id), - guild as (select * from servers where id = guild_id) select - system.id as system_id, - guild.log_channel, - ((channel_id = any (guild.blacklist)) - or (thread_id = any (guild.blacklist))) as in_blacklist, - ((channel_id = any (guild.log_blacklist)) - or (thread_id = any (guild.log_blacklist))) as in_log_blacklist, - coalesce(guild.log_cleanup_enabled, false), - coalesce(system_guild.proxy_enabled, true) as proxy_enabled, - system_last_switch.switch as last_switch, - system_last_switch.members as last_switch_members, - system_last_switch.timestamp as last_switch_timestamp, - system.tag as system_tag, - system.guild_tag as system_guild_tag, - coalesce(system.tag_enabled, true) as tag_enabled, - system.avatar_url as system_avatar, - system.guild_avatar as system_guild_avatar, - system.account_autoproxy as allow_autoproxy, - system.latch_timeout as latch_timeout, - system.case_sensitive_proxy_tags as case_sensitive_proxy_tags, - system.proxy_error_message_enabled as proxy_error_message_enabled + -- accounts table + accounts.allow_autoproxy as allow_autoproxy, + + -- systems table + systems.id as system_id, + systems.tag as system_tag, + systems.avatar_url as system_avatar, + + -- system_config table + system_config.latch_timeout as latch_timeout, + system_config.case_sensitive_proxy_tags as case_sensitive_proxy_tags, + system_config.proxy_error_message_enabled as proxy_error_message_enabled, + system_config.proxy_switch as proxy_switch, + system_config.name_format as name_format, + + -- system_guild table + coalesce(system_guild.tag_enabled, true) as tag_enabled, + coalesce(system_guild.proxy_enabled, true) as proxy_enabled, + system_guild.tag as system_guild_tag, + system_guild.avatar_url as system_guild_avatar, + + -- system_last_switch view + system_last_switch.switch as last_switch, + system_last_switch.members as last_switch_members, + system_last_switch.timestamp as last_switch_timestamp, + + -- servers table + servers.log_channel as log_channel, + ((channel_id = any (servers.blacklist)) + or (thread_id = any (servers.blacklist))) as in_blacklist, + ((channel_id = any (servers.log_blacklist)) + or (thread_id = any (servers.log_blacklist))) as in_log_blacklist, + coalesce(servers.log_cleanup_enabled, false) as log_cleanup_enabled, + coalesce(servers.require_system_tag, false) as require_system_tag, + + -- abuse_logs table + coalesce(abuse_logs.deny_bot_usage, false) as deny_bot_usage + -- We need a "from" clause, so we just use some bogus data that's always present -- This ensure we always have exactly one row going forward, so we can left join afterwards and still get data from (select 1) as _placeholder - left join system on true - left join guild on true - left join system_last_switch on system_last_switch.system = system.id - left join system_guild on system_guild.system = system.id and system_guild.guild = guild_id + left join accounts on accounts.uid = account_id + left join servers on servers.id = guild_id + left join systems on systems.id = accounts.system + left join system_config on system_config.system = accounts.system + left join system_guild on system_guild.system = accounts.system + and system_guild.guild = guild_id + left join system_last_switch on system_last_switch.system = accounts.system + left join abuse_logs on abuse_logs.id = accounts.abuse_log $$ language sql stable rows 1; - -- Fetches info about proxying related to a given account/guild -- Returns one row per member in system, should be used in conjuction with `message_context` too create function proxy_members(account_id bigint, guild_id bigint) @@ -122,13 +143,13 @@ begin end $$ language plpgsql; -create function generate_hid() returns char(5) as $$ - select string_agg(substr('abcdefghijklmnopqrstuvwxyz', ceil(random() * 26)::integer, 1), '') from generate_series(1, 5) +create function generate_hid() returns char(6) as $$ + select string_agg(substr('abcefghjknoprstuvwxyz', ceil(random() * 21)::integer, 1), '') from generate_series(1, 6) $$ language sql volatile; -create function find_free_system_hid() returns char(5) as $$ -declare new_hid char(5); +create function find_free_system_hid() returns char(6) as $$ +declare new_hid char(6); begin loop new_hid := generate_hid(); @@ -138,8 +159,8 @@ end $$ language plpgsql volatile; -create function find_free_member_hid() returns char(5) as $$ -declare new_hid char(5); +create function find_free_member_hid() returns char(6) as $$ +declare new_hid char(6); begin loop new_hid := generate_hid(); @@ -148,12 +169,13 @@ begin end $$ language plpgsql volatile; -create function find_free_group_hid() returns char(5) as $$ -declare new_hid char(5); + +create function find_free_group_hid() returns char(6) as $$ +declare new_hid char(6); begin loop new_hid := generate_hid(); if not exists (select 1 from groups where hid = new_hid) then return new_hid; end if; end loop; end -$$ language plpgsql volatile; +$$ language plpgsql volatile; \ No newline at end of file diff --git a/PluralKit.Core/Database/Migrations/42.sql b/PluralKit.Core/Database/Migrations/42.sql new file mode 100644 index 00000000..9a2aaf81 --- /dev/null +++ b/PluralKit.Core/Database/Migrations/42.sql @@ -0,0 +1,11 @@ +-- database version 42 +-- move to 6 character HIDs, add HID display config setting + +alter table systems alter column hid type char(6) using rpad(hid, 6, ' '); +alter table members alter column hid type char(6) using rpad(hid, 6, ' '); +alter table groups alter column hid type char(6) using rpad(hid, 6, ' '); + +alter table system_config add column hid_display_split bool default false; +alter table system_config add column hid_display_caps bool default false; + +update info set schema_version = 42; \ No newline at end of file diff --git a/PluralKit.Core/Database/Migrations/43.sql b/PluralKit.Core/Database/Migrations/43.sql new file mode 100644 index 00000000..583bf9c9 --- /dev/null +++ b/PluralKit.Core/Database/Migrations/43.sql @@ -0,0 +1,6 @@ +-- database version 43 +-- add config setting for padding 5-character IDs in lists + +alter table system_config add column hid_list_padding int not null default 0; + +update info set schema_version = 43; diff --git a/PluralKit.Core/Database/Migrations/44.sql b/PluralKit.Core/Database/Migrations/44.sql new file mode 100644 index 00000000..a93597ab --- /dev/null +++ b/PluralKit.Core/Database/Migrations/44.sql @@ -0,0 +1,23 @@ +-- database version 44 +-- add abuse handling measures + +create table abuse_logs ( + id serial primary key, + uuid uuid default gen_random_uuid(), + description text, + deny_bot_usage bool not null default false, + created timestamp not null default (current_timestamp at time zone 'utc') +); + +alter table accounts add column abuse_log integer default null references abuse_logs (id) on delete set null; +create index abuse_logs_uuid_idx on abuse_logs (uuid); + +-- we now need to handle a row in "accounts" table being created with no +-- system (rather than just system being set to null after insert) +-- +-- set default null and drop the sequence (from the column being created +-- as type SERIAL) +alter table accounts alter column system set default null; +drop sequence accounts_system_seq; + +update info set schema_version = 44; diff --git a/PluralKit.Core/Database/Migrations/45.sql b/PluralKit.Core/Database/Migrations/45.sql new file mode 100644 index 00000000..50e16dd6 --- /dev/null +++ b/PluralKit.Core/Database/Migrations/45.sql @@ -0,0 +1,6 @@ +-- database version 45 +-- add new config setting "proxy_switch" + +alter table system_config add column proxy_switch bool default false; + +update info set schema_version = 45; \ No newline at end of file diff --git a/PluralKit.Core/Database/Migrations/46.sql b/PluralKit.Core/Database/Migrations/46.sql new file mode 100644 index 00000000..6c34fdd8 --- /dev/null +++ b/PluralKit.Core/Database/Migrations/46.sql @@ -0,0 +1,12 @@ +-- database version 46 +-- adds banner privacy + +alter table members add column banner_privacy int not null default 1 check (banner_privacy = ANY (ARRAY[1,2])); +alter table groups add column banner_privacy int not null default 1 check (banner_privacy = ANY (ARRAY[1,2])); +alter table systems add column banner_privacy int not null default 1 check (banner_privacy = ANY (ARRAY[1,2])); + +update members set banner_privacy = 2 where description_privacy = 2; +update groups set banner_privacy = 2 where description_privacy = 2; +update systems set banner_privacy = 2 where description_privacy = 2; + +update info set schema_version = 46; diff --git a/PluralKit.Core/Database/Migrations/47.sql b/PluralKit.Core/Database/Migrations/47.sql new file mode 100644 index 00000000..441d648e --- /dev/null +++ b/PluralKit.Core/Database/Migrations/47.sql @@ -0,0 +1,6 @@ +-- database version 47 +-- add config setting for supplying a custom tag format in names + +alter table system_config add column name_format text; + +update info set schema_version = 47; diff --git a/PluralKit.Core/Database/Migrations/48.sql b/PluralKit.Core/Database/Migrations/48.sql new file mode 100644 index 00000000..088a53a4 --- /dev/null +++ b/PluralKit.Core/Database/Migrations/48.sql @@ -0,0 +1,9 @@ +-- database version 48 +-- +-- add guild settings for disabling "invalid command" responses & +-- enforcing the presence of system tags + +alter table servers add column invalid_command_response_enabled bool not null default true; +alter table servers add column require_system_tag bool not null default false; + +update info set schema_version = 48; \ No newline at end of file diff --git a/PluralKit.Core/Database/Repository/ModelRepository.AbuseLog.cs b/PluralKit.Core/Database/Repository/ModelRepository.AbuseLog.cs new file mode 100644 index 00000000..b2276178 --- /dev/null +++ b/PluralKit.Core/Database/Repository/ModelRepository.AbuseLog.cs @@ -0,0 +1,70 @@ +using Dapper; + +using SqlKata; + +namespace PluralKit.Core; + +public partial class ModelRepository +{ + public Task GetAbuseLogByGuid(Guid id) + { + var query = new Query("abuse_logs").Where("uuid", id); + return _db.QueryFirst(query); + } + + public Task GetAbuseLogByAccount(ulong accountId) + { + var query = new Query("accounts") + .Select("abuse_logs.*") + .LeftJoin("abuse_logs", "abuse_logs.id", "accounts.abuse_log") + .Where("uid", accountId) + .WhereNotNull("abuse_log"); + + return _db.QueryFirst(query); + } + + public Task> GetAbuseLogAccounts(AbuseLogId id) + { + var query = new Query("accounts").Select("uid").Where("abuse_log", id); + return _db.Query(query); + } + + public async Task CreateAbuseLog(string? desc = null, bool? denyBotUsage = null, IPKConnection? conn = null) + { + var query = new Query("abuse_logs").AsInsert(new + { + description = desc, + deny_bot_usage = denyBotUsage, + }); + + var abuseLog = await _db.QueryFirst(conn, query, "returning *"); + _logger.Information("Created {AbuseLogId}", abuseLog.Id); + return abuseLog; + } + + public async Task AddAbuseLogAccount(AbuseLogId abuseLog, ulong accountId, IPKConnection? conn = null) + { + var query = new Query("accounts").AsInsert(new + { + abuse_log = abuseLog, + uid = accountId, + }); + await _db.ExecuteQuery(conn, query, "on conflict (uid) do update set abuse_log = @p0"); + + _logger.Information("Linked account {UserId} to {AbuseLogId}", accountId, abuseLog); + } + + public async Task UpdateAbuseLog(AbuseLogId id, AbuseLogPatch patch, IPKConnection? conn = null) + { + _logger.Information("Updated {AbuseLogId}: {@AbuseLogPatch}", id, patch); + var query = patch.Apply(new Query("abuse_logs").Where("id", id)); + return await _db.QueryFirst(conn, query, "returning *"); + } + + public async Task DeleteAbuseLog(AbuseLogId id) + { + var query = new Query("abuse_logs").AsDelete().Where("id", id); + await _db.ExecuteQuery(query); + _logger.Information("Deleted {AbuseLogId}", id); + } +} \ No newline at end of file diff --git a/PluralKit.Core/Database/Repository/ModelRepository.GroupMember.cs b/PluralKit.Core/Database/Repository/ModelRepository.GroupMember.cs index cecc7489..c69b4dc3 100644 --- a/PluralKit.Core/Database/Repository/ModelRepository.GroupMember.cs +++ b/PluralKit.Core/Database/Repository/ModelRepository.GroupMember.cs @@ -103,8 +103,20 @@ public partial class ModelRepository public class GroupMember { - public string Group { get; set; } - public string Member { get; set; } + private string _group = null!; + public string Group + { + set => _group = value.Trim(); + get => _group; + } + + private string _member = null!; + public string Member + { + set => _member = value.Trim(); + get => _member; + } + public Guid MemberUuid { get; set; } public PrivacyLevel MemberVisibility { get; set; } } \ No newline at end of file diff --git a/PluralKit.Core/Database/Utils/DatabaseMigrator.cs b/PluralKit.Core/Database/Utils/DatabaseMigrator.cs index 14a2370b..0aea679a 100644 --- a/PluralKit.Core/Database/Utils/DatabaseMigrator.cs +++ b/PluralKit.Core/Database/Utils/DatabaseMigrator.cs @@ -9,7 +9,7 @@ namespace PluralKit.Core; internal class DatabaseMigrator { private const string RootPath = "PluralKit.Core.Database"; // "resource path" root for SQL files - private const int TargetSchemaVersion = 41; + private const int TargetSchemaVersion = 48; private readonly ILogger _logger; public DatabaseMigrator(ILogger logger) diff --git a/PluralKit.Core/Database/Utils/HidUtils.cs b/PluralKit.Core/Database/Utils/HidUtils.cs new file mode 100644 index 00000000..ecc79212 --- /dev/null +++ b/PluralKit.Core/Database/Utils/HidUtils.cs @@ -0,0 +1,51 @@ +using System.Linq; +using System.Text.RegularExpressions; + +namespace PluralKit.Core; + +public static class HidUtils +{ + private static readonly Regex _hidRegex = new(@"^[a-zA-Z]{5,6}$"); + + public static string? ParseHid(string input) + { + input = input.ToLower().Replace("-", null); + if (!_hidRegex.IsMatch(input)) + return null; + + return input; + } + + public static bool TryParseHid(this string input, out string hid) + { + hid = ParseHid(input); + return hid != null; + } + + public static string HidTransform(string input, bool split, bool caps, SystemConfig.HidPadFormat pad) + { + if (split && input.Length > 5) + { + var len = (int)Math.Floor(input.Length / 2.0); + input = string.Concat(input.AsSpan(0, len), "-", input.AsSpan(len)); + } + + if (caps) + input = input.ToUpper(); + + if (input.Length == 5) + switch (pad) + { + case SystemConfig.HidPadFormat.Left: + input = " " + input; + if (split) input = " " + input; + break; + case SystemConfig.HidPadFormat.Right: + input = input + " "; + if (split) input = input + " "; + break; + } + + return input; + } +} \ No newline at end of file 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/Database/clean.sql b/PluralKit.Core/Database/clean.sql index 5e407a21..6badec2c 100644 --- a/PluralKit.Core/Database/clean.sql +++ b/PluralKit.Core/Database/clean.sql @@ -3,6 +3,7 @@ -- This does mean we can't use any functions in row triggers, etc. Still unsure how to handle this. drop view if exists system_last_switch; +drop view if exists system_fronters; drop view if exists member_list; drop view if exists group_list; diff --git a/PluralKit.Core/Dispatch/DispatchModels.cs b/PluralKit.Core/Dispatch/DispatchModels.cs index ce758a1d..f6ff8d52 100644 --- a/PluralKit.Core/Dispatch/DispatchModels.cs +++ b/PluralKit.Core/Dispatch/DispatchModels.cs @@ -28,7 +28,8 @@ public enum DispatchEvent UPDATE_SWITCH, DELETE_SWITCH, DELETE_ALL_SWITCHES, - SUCCESSFUL_IMPORT + SUCCESSFUL_IMPORT, + UPDATE_AUTOPROXY, } public struct UpdateDispatchData @@ -42,7 +43,7 @@ public struct UpdateDispatchData public static class DispatchExt { - public static StringContent GetPayloadBody(this UpdateDispatchData data) + public static string GetPayloadBody(this UpdateDispatchData data) { var o = new JObject(); @@ -52,7 +53,18 @@ public static class DispatchExt o.Add("id", data.EntityId); o.Add("data", data.EventData); - return new StringContent(JsonConvert.SerializeObject(o), Encoding.UTF8, "application/json"); + return JsonConvert.SerializeObject(o); + } + + public static string GetPingBody(string systemId, string token) + { + var o = new JObject(); + + o.Add("type", "PING"); + o.Add("signing_token", token); + o.Add("system_id", systemId); + + return JsonConvert.SerializeObject(o); } private static List _privateNetworks = new() @@ -70,6 +82,7 @@ public static class DispatchExt try { var uri = new Uri(url); + if (uri.Scheme != "https") return false; host = await Dns.GetHostEntryAsync(uri.DnsSafeHost); } catch (Exception) diff --git a/PluralKit.Core/Dispatch/DispatchService.cs b/PluralKit.Core/Dispatch/DispatchService.cs index 58f88b56..832e63d3 100644 --- a/PluralKit.Core/Dispatch/DispatchService.cs +++ b/PluralKit.Core/Dispatch/DispatchService.cs @@ -1,5 +1,10 @@ using Autofac; +using System.Text; + +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + using Serilog; namespace PluralKit.Core; @@ -8,40 +13,81 @@ public class DispatchService { private readonly HttpClient _client = new(); private readonly ILogger _logger; + private readonly CoreConfig _cfg; private readonly ILifetimeScope _provider; public DispatchService(ILogger logger, ILifetimeScope provider, CoreConfig cfg) { _logger = logger; + _cfg = cfg; _provider = provider; } - public async Task DoPostRequest(SystemId system, string webhookUrl, HttpContent content, bool isVerify = false) + public async Task TestUrl(Guid systemUuid, string newUrl, string newToken) { - if (!await DispatchExt.ValidateUri(webhookUrl)) + if (_cfg.DispatchProxyUrl == null || _cfg.DispatchProxyToken == null) + throw new Exception("tried to dispatch without a proxy set!"); + + var o = new JObject(); + o.Add("auth", _cfg.DispatchProxyToken); + o.Add("url", newUrl); + o.Add("payload", DispatchExt.GetPingBody(systemUuid.ToString(), newToken)); + o.Add("test", DispatchExt.GetPingBody(systemUuid.ToString(), StringUtils.GenerateToken())); + + var body = new StringContent(JsonConvert.SerializeObject(o), Encoding.UTF8, "application/json"); + + var res = await _client.PostAsync(_cfg.DispatchProxyUrl, body); + return await res.Content.ReadAsStringAsync(); + } + + public async Task DoPostRequest(SystemId system, string webhookUrl, string content) + { + if (_cfg.DispatchProxyUrl == null || _cfg.DispatchProxyToken == null) { - _logger.Warning( - "Failed to dispatch webhook for system {SystemId}: URL is invalid or points to a private address", - system); + _logger.Warning("tried to dispatch without a proxy set!"); return; } + var o = new JObject(); + o.Add("auth", _cfg.DispatchProxyToken); + o.Add("url", webhookUrl); + o.Add("payload", content); + + var body = new StringContent(JsonConvert.SerializeObject(o), Encoding.UTF8, "application/json"); + try { - await _client.PostAsync(webhookUrl, content); + await _client.PostAsync(_cfg.DispatchProxyUrl, body); + // todo: do something with proxy errors } catch (HttpRequestException e) { - if (isVerify) - throw; - _logger.Error("Could not dispatch webhook request!", e); + _logger.Error(e, "Could not dispatch webhook request!"); } } - public Task Dispatch(SystemId systemId, ulong? guildId, ulong? channelId, AutoproxyPatch patch) + public async Task Dispatch(SystemId systemId, ulong? guildId, ulong? channelId, AutoproxyPatch patch) { - // todo - return Task.CompletedTask; + var repo = _provider.Resolve(); + var system = await repo.GetSystem(systemId); + if (system.WebhookUrl == null) + return; + + var memberUuid = patch.AutoproxyMember.IsPresent && patch.AutoproxyMember.Value is MemberId id + ? (await repo.GetMember(id)).Uuid.ToString() + : null; + + var data = new UpdateDispatchData(); + data.Event = DispatchEvent.UPDATE_AUTOPROXY; + data.SigningToken = system.WebhookToken; + data.SystemId = system.Uuid.ToString(); + data.EventData = patch.ToJson(guildId, channelId, memberUuid); + + _logger.Debug( + "Dispatching webhook for system {SystemId} autoproxy update in guild {GuildId}/{ChannelId}", + system.Id, guildId, channelId + ); + await DoPostRequest(system.Id, system.WebhookUrl, data.GetPayloadBody()); } public async Task Dispatch(SystemId systemId, UpdateDispatchData data) @@ -201,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/Models/AbuseLog.cs b/PluralKit.Core/Models/AbuseLog.cs new file mode 100644 index 00000000..ffc11798 --- /dev/null +++ b/PluralKit.Core/Models/AbuseLog.cs @@ -0,0 +1,36 @@ +using NodaTime; + +namespace PluralKit.Core; + +public readonly struct AbuseLogId: INumericId +{ + public int Value { get; } + + public AbuseLogId(int value) + { + Value = value; + } + + public bool Equals(AbuseLogId other) => Value == other.Value; + + public override bool Equals(object obj) => obj is AbuseLogId other && Equals(other); + + public override int GetHashCode() => Value; + + public static bool operator ==(AbuseLogId left, AbuseLogId right) => left.Equals(right); + + public static bool operator !=(AbuseLogId left, AbuseLogId right) => !left.Equals(right); + + public int CompareTo(AbuseLogId other) => Value.CompareTo(other.Value); + + public override string ToString() => $"AbuseLog #{Value}"; +} + +public class AbuseLog +{ + public AbuseLogId Id { get; private set; } + public Guid Uuid { get; private set; } + public string Description { get; private set; } + public bool DenyBotUsage { get; private set; } + public Instant Created { get; private set; } +} \ No newline at end of file diff --git a/PluralKit.Core/Models/GuildConfig.cs b/PluralKit.Core/Models/GuildConfig.cs index 20ed0639..7e6b7501 100644 --- a/PluralKit.Core/Models/GuildConfig.cs +++ b/PluralKit.Core/Models/GuildConfig.cs @@ -7,4 +7,6 @@ public class GuildConfig public ulong[] LogBlacklist { get; } public ulong[] Blacklist { get; } public bool LogCleanupEnabled { get; } + public bool InvalidCommandResponseEnabled { get; } + public bool RequireSystemTag { get; } } \ No newline at end of file diff --git a/PluralKit.Core/Models/PKGroup.cs b/PluralKit.Core/Models/PKGroup.cs index 53f5f704..cfca68cf 100644 --- a/PluralKit.Core/Models/PKGroup.cs +++ b/PluralKit.Core/Models/PKGroup.cs @@ -32,7 +32,13 @@ public readonly struct GroupId: INumericId public class PKGroup { public GroupId Id { get; private set; } - public string Hid { get; private set; } = null!; + private string _hid = null!; + public string Hid + { + private set => _hid = value.Trim(); + get => _hid; + } + public Guid Uuid { get; private set; } public SystemId System { get; private set; } @@ -45,6 +51,7 @@ public class PKGroup public PrivacyLevel NamePrivacy { get; private set; } public PrivacyLevel DescriptionPrivacy { get; private set; } + public PrivacyLevel BannerPrivacy { get; private set; } public PrivacyLevel IconPrivacy { get; private set; } public PrivacyLevel ListPrivacy { get; private set; } public PrivacyLevel MetadataPrivacy { get; private set; } @@ -82,7 +89,7 @@ public static class PKGroupExt o.Add("display_name", group.NamePrivacy.CanAccess(ctx) ? group.DisplayName : null); o.Add("description", group.DescriptionPrivacy.Get(ctx, group.Description)); o.Add("icon", group.IconFor(ctx)); - o.Add("banner", group.DescriptionPrivacy.Get(ctx, group.BannerImage)); + o.Add("banner", group.BannerPrivacy.Get(ctx, group.BannerImage)); o.Add("color", group.Color); o.Add("created", group.CreatedFor(ctx)?.FormatExport()); @@ -96,6 +103,7 @@ public static class PKGroupExt p.Add("name_privacy", group.NamePrivacy.ToJsonString()); p.Add("description_privacy", group.DescriptionPrivacy.ToJsonString()); + p.Add("banner_privacy", group.BannerPrivacy.ToJsonString()); p.Add("icon_privacy", group.IconPrivacy.ToJsonString()); p.Add("list_privacy", group.ListPrivacy.ToJsonString()); p.Add("metadata_privacy", group.MetadataPrivacy.ToJsonString()); diff --git a/PluralKit.Core/Models/PKMember.cs b/PluralKit.Core/Models/PKMember.cs index 72ec3d07..c30381f8 100644 --- a/PluralKit.Core/Models/PKMember.cs +++ b/PluralKit.Core/Models/PKMember.cs @@ -35,7 +35,13 @@ public class PKMember // Dapper *can* figure out mapping to getter-only properties, but this doesn't work // when trying to map to *subclasses* (eg. ListedMember). Adding private setters makes it work anyway. public MemberId Id { get; private set; } - public string Hid { get; private set; } + private string _hid = null!; + public string Hid + { + private set => _hid = value.Trim(); + get => _hid; + } + public Guid Uuid { get; private set; } public SystemId System { get; private set; } public string Color { get; private set; } @@ -57,6 +63,7 @@ public class PKMember public PrivacyLevel MemberVisibility { get; private set; } public PrivacyLevel DescriptionPrivacy { get; private set; } + public PrivacyLevel BannerPrivacy { get; private set; } public PrivacyLevel AvatarPrivacy { get; private set; } public PrivacyLevel NamePrivacy { get; private set; } //ignore setting if no display name is set public PrivacyLevel BirthdayPrivacy { get; private set; } @@ -134,7 +141,7 @@ public static class PKMemberExt o.Add("pronouns", member.PronounsFor(ctx)); o.Add("avatar_url", member.AvatarFor(ctx).TryGetCleanCdnUrl()); o.Add("webhook_avatar_url", member.AvatarPrivacy.Get(ctx, member.WebhookAvatarUrl?.TryGetCleanCdnUrl())); - o.Add("banner", member.DescriptionPrivacy.Get(ctx, member.BannerImage).TryGetCleanCdnUrl()); + o.Add("banner", member.BannerPrivacy.Get(ctx, member.BannerImage).TryGetCleanCdnUrl()); o.Add("description", member.DescriptionFor(ctx)); o.Add("created", member.CreatedFor(ctx)?.FormatExport()); o.Add("keep_proxy", member.KeepProxy); @@ -160,6 +167,7 @@ public static class PKMemberExt p.Add("visibility", member.MemberVisibility.ToJsonString()); p.Add("name_privacy", member.NamePrivacy.ToJsonString()); p.Add("description_privacy", member.DescriptionPrivacy.ToJsonString()); + p.Add("banner_privacy", member.BannerPrivacy.ToJsonString()); p.Add("birthday_privacy", member.BirthdayPrivacy.ToJsonString()); p.Add("pronoun_privacy", member.PronounPrivacy.ToJsonString()); p.Add("avatar_privacy", member.AvatarPrivacy.ToJsonString()); diff --git a/PluralKit.Core/Models/PKSystem.cs b/PluralKit.Core/Models/PKSystem.cs index 1b91bae7..d14236d2 100644 --- a/PluralKit.Core/Models/PKSystem.cs +++ b/PluralKit.Core/Models/PKSystem.cs @@ -33,7 +33,13 @@ public readonly struct SystemId: INumericId public class PKSystem { [Key] public SystemId Id { get; } - public string Hid { get; } + private string _hid = null!; + public string Hid + { + private set => _hid = value.Trim(); + get => _hid; + } + public Guid Uuid { get; private set; } public string Name { get; } public string Description { get; } @@ -49,6 +55,7 @@ public class PKSystem public PrivacyLevel NamePrivacy { get; } public PrivacyLevel AvatarPrivacy { get; } public PrivacyLevel DescriptionPrivacy { get; } + public PrivacyLevel BannerPrivacy { get; } public PrivacyLevel MemberListPrivacy { get; } public PrivacyLevel FrontPrivacy { get; } public PrivacyLevel FrontHistoryPrivacy { get; } @@ -79,7 +86,7 @@ public static class PKSystemExt o.Add("pronouns", system.PronounPrivacy.Get(ctx, system.Pronouns)); o.Add("avatar_url", system.AvatarFor(ctx)); - o.Add("banner", system.DescriptionPrivacy.Get(ctx, system.BannerImage).TryGetCleanCdnUrl()); + o.Add("banner", system.BannerPrivacy.Get(ctx, system.BannerImage).TryGetCleanCdnUrl()); o.Add("color", system.Color); o.Add("created", system.Created.FormatExport()); @@ -94,6 +101,7 @@ public static class PKSystemExt p.Add("name_privacy", system.NamePrivacy.ToJsonString()); p.Add("avatar_privacy", system.AvatarPrivacy.ToJsonString()); p.Add("description_privacy", system.DescriptionPrivacy.ToJsonString()); + p.Add("banner_privacy", system.BannerPrivacy.ToJsonString()); p.Add("pronoun_privacy", system.PronounPrivacy.ToJsonString()); p.Add("member_list_privacy", system.MemberListPrivacy.ToJsonString()); p.Add("group_list_privacy", system.GroupListPrivacy.ToJsonString()); diff --git a/PluralKit.Core/Models/Patch/AbuseLogPatch.cs b/PluralKit.Core/Models/Patch/AbuseLogPatch.cs new file mode 100644 index 00000000..050523dc --- /dev/null +++ b/PluralKit.Core/Models/Patch/AbuseLogPatch.cs @@ -0,0 +1,16 @@ +using Newtonsoft.Json.Linq; + +using SqlKata; + +namespace PluralKit.Core; + +public class AbuseLogPatch: PatchObject +{ + public Partial Description { get; set; } + public Partial DenyBotUsage { get; set; } + + public override Query Apply(Query q) => q.ApplyPatch(wrapper => wrapper + .With("description", Description) + .With("deny_bot_usage", DenyBotUsage) + ); +} \ No newline at end of file diff --git a/PluralKit.Core/Models/Patch/AccountPatch.cs b/PluralKit.Core/Models/Patch/AccountPatch.cs index daba9b9c..60f036c7 100644 --- a/PluralKit.Core/Models/Patch/AccountPatch.cs +++ b/PluralKit.Core/Models/Patch/AccountPatch.cs @@ -8,10 +8,12 @@ public class AccountPatch: PatchObject { public Partial DmChannel { get; set; } public Partial AllowAutoproxy { get; set; } + public Partial AbuseLog { get; set; } public override Query Apply(Query q) => q.ApplyPatch(wrapper => wrapper .With("dm_channel", DmChannel) .With("allow_autoproxy", AllowAutoproxy) + .With("abuse_log", AbuseLog) ); public JObject ToJson() diff --git a/PluralKit.Core/Models/Patch/AutoproxyPatch.cs b/PluralKit.Core/Models/Patch/AutoproxyPatch.cs index 66d3c35c..529fda08 100644 --- a/PluralKit.Core/Models/Patch/AutoproxyPatch.cs +++ b/PluralKit.Core/Models/Patch/AutoproxyPatch.cs @@ -44,4 +44,23 @@ public class AutoproxyPatch: PatchObject return p; } + + public JObject ToJson(ulong? guild_id, ulong? channel_id, string? memberId = null) + { + var o = new JObject(); + + o.Add("guild_id", guild_id?.ToString()); + o.Add("channel_id", channel_id?.ToString()); + + if (AutoproxyMode.IsPresent) + o.Add("autoproxy_mode", AutoproxyMode.Value.ToString().ToLower()); + + if (AutoproxyMember.IsPresent) + o.Add("autoproxy_member", memberId); + + if (LastLatchTimestamp.IsPresent) + o.Add("last_latch_timestamp", LastLatchTimestamp.Value.FormatExport()); + + return o; + } } \ No newline at end of file diff --git a/PluralKit.Core/Models/Patch/GroupPatch.cs b/PluralKit.Core/Models/Patch/GroupPatch.cs index de90e9fc..bc1b6867 100644 --- a/PluralKit.Core/Models/Patch/GroupPatch.cs +++ b/PluralKit.Core/Models/Patch/GroupPatch.cs @@ -17,6 +17,7 @@ public class GroupPatch: PatchObject public Partial NamePrivacy { get; set; } public Partial DescriptionPrivacy { get; set; } + public Partial BannerPrivacy { get; set; } public Partial IconPrivacy { get; set; } public Partial ListPrivacy { get; set; } public Partial MetadataPrivacy { get; set; } @@ -32,6 +33,7 @@ public class GroupPatch: PatchObject .With("color", Color) .With("name_privacy", NamePrivacy) .With("description_privacy", DescriptionPrivacy) + .With("banner_privacy", BannerPrivacy) .With("icon_privacy", IconPrivacy) .With("list_privacy", ListPrivacy) .With("metadata_privacy", MetadataPrivacy) @@ -84,6 +86,9 @@ public class GroupPatch: PatchObject if (privacy.ContainsKey("description_privacy")) patch.DescriptionPrivacy = patch.ParsePrivacy(privacy, "description_privacy"); + if (privacy.ContainsKey("banner_privacy")) + patch.BannerPrivacy = patch.ParsePrivacy(privacy, "banner_privacy"); + if (privacy.ContainsKey("icon_privacy")) patch.IconPrivacy = patch.ParsePrivacy(privacy, "icon_privacy"); @@ -122,6 +127,7 @@ public class GroupPatch: PatchObject if ( NamePrivacy.IsPresent || DescriptionPrivacy.IsPresent + || BannerPrivacy.IsPresent || IconPrivacy.IsPresent || ListPrivacy.IsPresent || MetadataPrivacy.IsPresent @@ -136,6 +142,9 @@ public class GroupPatch: PatchObject if (DescriptionPrivacy.IsPresent) p.Add("description_privacy", DescriptionPrivacy.Value.ToJsonString()); + if (BannerPrivacy.IsPresent) + p.Add("banner_privacy", BannerPrivacy.Value.ToJsonString()); + if (IconPrivacy.IsPresent) p.Add("icon_privacy", IconPrivacy.Value.ToJsonString()); diff --git a/PluralKit.Core/Models/Patch/GuildPatch.cs b/PluralKit.Core/Models/Patch/GuildPatch.cs index 7380ff5e..df146859 100644 --- a/PluralKit.Core/Models/Patch/GuildPatch.cs +++ b/PluralKit.Core/Models/Patch/GuildPatch.cs @@ -8,11 +8,15 @@ public class GuildPatch: PatchObject public Partial LogBlacklist { get; set; } public Partial Blacklist { get; set; } public Partial LogCleanupEnabled { get; set; } + public Partial InvalidCommandResponseEnabled { get; set; } + public Partial RequireSystemTag { get; set; } public override Query Apply(Query q) => q.ApplyPatch(wrapper => wrapper .With("log_channel", LogChannel) .With("log_blacklist", LogBlacklist) .With("blacklist", Blacklist) .With("log_cleanup_enabled", LogCleanupEnabled) + .With("invalid_command_response_enabled", InvalidCommandResponseEnabled) + .With("require_system_tag", RequireSystemTag) ); } \ No newline at end of file diff --git a/PluralKit.Core/Models/Patch/MemberPatch.cs b/PluralKit.Core/Models/Patch/MemberPatch.cs index c4f40597..6ec122ad 100644 --- a/PluralKit.Core/Models/Patch/MemberPatch.cs +++ b/PluralKit.Core/Models/Patch/MemberPatch.cs @@ -27,6 +27,7 @@ public class MemberPatch: PatchObject public Partial Visibility { get; set; } public Partial NamePrivacy { get; set; } public Partial DescriptionPrivacy { get; set; } + public Partial BannerPrivacy { get; set; } public Partial PronounPrivacy { get; set; } public Partial BirthdayPrivacy { get; set; } public Partial AvatarPrivacy { get; set; } @@ -53,6 +54,7 @@ public class MemberPatch: PatchObject .With("member_visibility", Visibility) .With("name_privacy", NamePrivacy) .With("description_privacy", DescriptionPrivacy) + .With("banner_privacy", BannerPrivacy) .With("pronoun_privacy", PronounPrivacy) .With("birthday_privacy", BirthdayPrivacy) .With("avatar_privacy", AvatarPrivacy) @@ -140,6 +142,8 @@ public class MemberPatch: PatchObject if (o.ContainsKey("name_privacy")) patch.NamePrivacy = patch.ParsePrivacy(o, "name_privacy"); if (o.ContainsKey("description_privacy")) patch.DescriptionPrivacy = patch.ParsePrivacy(o, "description_privacy"); + if (o.ContainsKey("banner_privacy")) + patch.BannerPrivacy = patch.ParsePrivacy(o, "banner_privacy"); if (o.ContainsKey("avatar_privacy")) patch.AvatarPrivacy = patch.ParsePrivacy(o, "avatar_privacy"); if (o.ContainsKey("birthday_privacy")) @@ -172,6 +176,9 @@ public class MemberPatch: PatchObject if (privacy.ContainsKey("description_privacy")) patch.DescriptionPrivacy = patch.ParsePrivacy(privacy, "description_privacy"); + if (privacy.ContainsKey("banner_privacy")) + patch.BannerPrivacy = patch.ParsePrivacy(privacy, "banner_privacy"); + if (privacy.ContainsKey("avatar_privacy")) patch.AvatarPrivacy = patch.ParsePrivacy(privacy, "avatar_privacy"); @@ -233,6 +240,7 @@ public class MemberPatch: PatchObject Visibility.IsPresent || NamePrivacy.IsPresent || DescriptionPrivacy.IsPresent + || BannerPrivacy.IsPresent || PronounPrivacy.IsPresent || BirthdayPrivacy.IsPresent || AvatarPrivacy.IsPresent @@ -251,6 +259,9 @@ public class MemberPatch: PatchObject if (DescriptionPrivacy.IsPresent) p.Add("description_privacy", DescriptionPrivacy.Value.ToJsonString()); + if (BannerPrivacy.IsPresent) + p.Add("banner_privacy", BannerPrivacy.Value.ToJsonString()); + if (PronounPrivacy.IsPresent) p.Add("pronoun_privacy", PronounPrivacy.Value.ToJsonString()); diff --git a/PluralKit.Core/Models/Patch/SystemConfigPatch.cs b/PluralKit.Core/Models/Patch/SystemConfigPatch.cs index 366c8f6d..b8f4d27a 100644 --- a/PluralKit.Core/Models/Patch/SystemConfigPatch.cs +++ b/PluralKit.Core/Models/Patch/SystemConfigPatch.cs @@ -19,7 +19,11 @@ public class SystemConfigPatch: PatchObject public Partial DescriptionTemplates { get; set; } public Partial CaseSensitiveProxyTags { get; set; } public Partial ProxyErrorMessageEnabled { get; set; } - + public Partial HidDisplaySplit { get; set; } + public Partial HidDisplayCaps { get; set; } + public Partial NameFormat { get; set; } + public Partial HidListPadding { get; set; } + public Partial ProxySwitch { get; set; } public override Query Apply(Query q) => q.ApplyPatch(wrapper => wrapper .With("ui_tz", UiTz) @@ -33,6 +37,11 @@ public class SystemConfigPatch: PatchObject .With("description_templates", DescriptionTemplates) .With("case_sensitive_proxy_tags", CaseSensitiveProxyTags) .With("proxy_error_message_enabled", ProxyErrorMessageEnabled) + .With("hid_display_split", HidDisplaySplit) + .With("hid_display_caps", HidDisplayCaps) + .With("hid_list_padding", HidListPadding) + .With("proxy_switch", ProxySwitch) + .With("name_format", NameFormat) ); public new void AssertIsValid() @@ -88,6 +97,21 @@ public class SystemConfigPatch: PatchObject if (ProxyErrorMessageEnabled.IsPresent) o.Add("proxy_error_message_enabled", ProxyErrorMessageEnabled.Value); + if (HidDisplaySplit.IsPresent) + o.Add("hid_display_split", HidDisplaySplit.Value); + + if (HidDisplayCaps.IsPresent) + o.Add("hid_display_caps", HidDisplayCaps.Value); + + if (HidListPadding.IsPresent) + o.Add("hid_list_padding", HidListPadding.Value.ToUserString()); + + if (ProxySwitch.IsPresent) + o.Add("proxy_switch", ProxySwitch.Value); + + if (NameFormat.IsPresent) + o.Add("name_format", NameFormat.Value); + return o; } @@ -119,6 +143,18 @@ public class SystemConfigPatch: PatchObject if (o.ContainsKey("proxy_error_message_enabled")) patch.ProxyErrorMessageEnabled = o.Value("proxy_error_message_enabled"); + if (o.ContainsKey("hid_display_split")) + patch.HidDisplaySplit = o.Value("hid_display_split"); + + if (o.ContainsKey("hid_display_caps")) + patch.HidDisplayCaps = o.Value("hid_display_caps"); + + if (o.ContainsKey("proxy_switch")) + patch.ProxySwitch = o.Value("proxy_switch"); + + if (o.ContainsKey("name_format")) + patch.NameFormat = o.Value("name_format"); + return patch; } } \ No newline at end of file diff --git a/PluralKit.Core/Models/Patch/SystemPatch.cs b/PluralKit.Core/Models/Patch/SystemPatch.cs index fd9c5b6d..ee28d936 100644 --- a/PluralKit.Core/Models/Patch/SystemPatch.cs +++ b/PluralKit.Core/Models/Patch/SystemPatch.cs @@ -21,6 +21,7 @@ public class SystemPatch: PatchObject public Partial NamePrivacy { get; set; } public Partial AvatarPrivacy { get; set; } public Partial DescriptionPrivacy { get; set; } + public Partial BannerPrivacy { get; set; } public Partial MemberListPrivacy { get; set; } public Partial GroupListPrivacy { get; set; } public Partial FrontPrivacy { get; set; } @@ -42,6 +43,7 @@ public class SystemPatch: PatchObject .With("name_privacy", NamePrivacy) .With("avatar_privacy", AvatarPrivacy) .With("description_privacy", DescriptionPrivacy) + .With("banner_privacy", BannerPrivacy) .With("member_list_privacy", MemberListPrivacy) .With("group_list_privacy", GroupListPrivacy) .With("front_privacy", FrontPrivacy) @@ -86,6 +88,8 @@ public class SystemPatch: PatchObject { if (o.ContainsKey("description_privacy")) patch.DescriptionPrivacy = patch.ParsePrivacy(o, "description_privacy"); + if (o.ContainsKey("banner_privacy")) + patch.BannerPrivacy = patch.ParsePrivacy(o, "banner_privacy"); if (o.ContainsKey("member_list_privacy")) patch.MemberListPrivacy = patch.ParsePrivacy(o, "member_list_privacy"); if (o.ContainsKey("front_privacy")) patch.FrontPrivacy = patch.ParsePrivacy(o, "front_privacy"); @@ -106,6 +110,9 @@ public class SystemPatch: PatchObject if (privacy.ContainsKey("description_privacy")) patch.DescriptionPrivacy = patch.ParsePrivacy(privacy, "description_privacy"); + if (privacy.ContainsKey("banner_privacy")) + patch.BannerPrivacy = patch.ParsePrivacy(privacy, "banner_privacy"); + if (privacy.ContainsKey("pronoun_privacy")) patch.PronounPrivacy = patch.ParsePrivacy(privacy, "pronoun_privacy"); @@ -150,6 +157,7 @@ public class SystemPatch: PatchObject NamePrivacy.IsPresent || AvatarPrivacy.IsPresent || DescriptionPrivacy.IsPresent + || BannerPrivacy.IsPresent || PronounPrivacy.IsPresent || MemberListPrivacy.IsPresent || GroupListPrivacy.IsPresent @@ -168,6 +176,9 @@ public class SystemPatch: PatchObject if (DescriptionPrivacy.IsPresent) p.Add("description_privacy", DescriptionPrivacy.Value.ToJsonString()); + if (BannerPrivacy.IsPresent) + p.Add("banner_privacy", BannerPrivacy.Value.ToJsonString()); + if (PronounPrivacy.IsPresent) p.Add("pronoun_privacy", PronounPrivacy.Value.ToJsonString()); diff --git a/PluralKit.Core/Models/Privacy/GroupPrivacySubject.cs b/PluralKit.Core/Models/Privacy/GroupPrivacySubject.cs index 24e1f2b1..035a0f09 100644 --- a/PluralKit.Core/Models/Privacy/GroupPrivacySubject.cs +++ b/PluralKit.Core/Models/Privacy/GroupPrivacySubject.cs @@ -4,6 +4,7 @@ public enum GroupPrivacySubject { Name, Description, + Banner, Icon, List, Metadata, @@ -19,6 +20,7 @@ public static class GroupPrivacyUtils { GroupPrivacySubject.Name => group.NamePrivacy = level, GroupPrivacySubject.Description => group.DescriptionPrivacy = level, + GroupPrivacySubject.Banner => group.BannerPrivacy = level, GroupPrivacySubject.Icon => group.IconPrivacy = level, GroupPrivacySubject.List => group.ListPrivacy = level, GroupPrivacySubject.Metadata => group.MetadataPrivacy = level, @@ -45,10 +47,20 @@ public static class GroupPrivacyUtils break; case "description": case "desc": - case "text": + case "describe": + case "d": + case "bio": case "info": + case "text": + case "intro": subject = GroupPrivacySubject.Description; break; + case "banner": + case "b": + case "splash": + case "cover": + subject = GroupPrivacySubject.Banner; + break; case "avatar": case "pfp": case "pic": diff --git a/PluralKit.Core/Models/Privacy/MemberPrivacySubject.cs b/PluralKit.Core/Models/Privacy/MemberPrivacySubject.cs index 731fab2d..a15f1c0d 100644 --- a/PluralKit.Core/Models/Privacy/MemberPrivacySubject.cs +++ b/PluralKit.Core/Models/Privacy/MemberPrivacySubject.cs @@ -5,6 +5,7 @@ public enum MemberPrivacySubject Visibility, Name, Description, + Banner, Avatar, Birthday, Pronouns, @@ -21,6 +22,7 @@ public static class MemberPrivacyUtils { MemberPrivacySubject.Name => member.NamePrivacy = level, MemberPrivacySubject.Description => member.DescriptionPrivacy = level, + MemberPrivacySubject.Banner => member.BannerPrivacy = level, MemberPrivacySubject.Avatar => member.AvatarPrivacy = level, MemberPrivacySubject.Pronouns => member.PronounPrivacy = level, MemberPrivacySubject.Birthday => member.BirthdayPrivacy = level, @@ -49,10 +51,20 @@ public static class MemberPrivacyUtils break; case "description": case "desc": - case "text": + case "describe": + case "d": + case "bio": case "info": + case "text": + case "intro": subject = MemberPrivacySubject.Description; break; + case "banner": + case "b": + case "splash": + case "cover": + subject = MemberPrivacySubject.Banner; + break; case "avatar": case "pfp": case "pic": @@ -64,10 +76,14 @@ public static class MemberPrivacyUtils case "bday": case "birthdate": case "bdate": + case "cakeday": + case "bd": subject = MemberPrivacySubject.Birthday; break; case "pronouns": case "pronoun": + case "prns": + case "pn": subject = MemberPrivacySubject.Pronouns; break; case "meta": diff --git a/PluralKit.Core/Models/Privacy/SystemPrivacySubject.cs b/PluralKit.Core/Models/Privacy/SystemPrivacySubject.cs index 3bc8a2aa..33c21c54 100644 --- a/PluralKit.Core/Models/Privacy/SystemPrivacySubject.cs +++ b/PluralKit.Core/Models/Privacy/SystemPrivacySubject.cs @@ -5,6 +5,7 @@ public enum SystemPrivacySubject Name, Avatar, Description, + Banner, Pronouns, MemberList, GroupList, @@ -22,6 +23,7 @@ public static class SystemPrivacyUtils SystemPrivacySubject.Name => system.NamePrivacy = level, SystemPrivacySubject.Avatar => system.AvatarPrivacy = level, SystemPrivacySubject.Description => system.DescriptionPrivacy = level, + SystemPrivacySubject.Banner => system.BannerPrivacy = level, SystemPrivacySubject.Pronouns => system.PronounPrivacy = level, SystemPrivacySubject.Front => system.FrontPrivacy = level, SystemPrivacySubject.FrontHistory => system.FrontHistoryPrivacy = level, @@ -55,12 +57,24 @@ public static class SystemPrivacyUtils break; case "description": case "desc": - case "text": + case "describe": + case "d": + case "bio": case "info": + case "text": + case "intro": subject = SystemPrivacySubject.Description; break; + case "banner": + case "b": + case "splash": + case "cover": + subject = SystemPrivacySubject.Banner; + break; case "pronouns": + case "pronoun": case "prns": + case "pn": subject = SystemPrivacySubject.Pronouns; break; case "members": diff --git a/PluralKit.Core/Models/SystemConfig.cs b/PluralKit.Core/Models/SystemConfig.cs index 1482d0d8..2f8fc4cb 100644 --- a/PluralKit.Core/Models/SystemConfig.cs +++ b/PluralKit.Core/Models/SystemConfig.cs @@ -19,8 +19,20 @@ public class SystemConfig public DateTimeZone Zone => DateTimeZoneProviders.Tzdb.GetZoneOrNull(UiTz); - public bool CaseSensitiveProxyTags { get; set; } + public bool CaseSensitiveProxyTags { get; } public bool ProxyErrorMessageEnabled { get; } + public bool HidDisplaySplit { get; } + public bool HidDisplayCaps { get; } + public HidPadFormat HidListPadding { get; } + public bool ProxySwitch { get; } + public string NameFormat { get; } + + public enum HidPadFormat + { + None = 0, + Left = 1, + Right = 2, + } } public static class SystemConfigExt @@ -39,9 +51,20 @@ public static class SystemConfigExt o.Add("group_limit", cfg.GroupLimitOverride ?? Limits.MaxGroupCount); o.Add("case_sensitive_proxy_tags", cfg.CaseSensitiveProxyTags); o.Add("proxy_error_message_enabled", cfg.ProxyErrorMessageEnabled); + o.Add("hid_display_split", cfg.HidDisplaySplit); + o.Add("hid_display_caps", cfg.HidDisplayCaps); + o.Add("hid_list_padding", cfg.HidListPadding.ToUserString()); + o.Add("proxy_switch", cfg.ProxySwitch); + o.Add("name_format", cfg.NameFormat); o.Add("description_templates", JArray.FromObject(cfg.DescriptionTemplates)); return o; } + + public static string ToUserString(this SystemConfig.HidPadFormat val) + { + if (val == SystemConfig.HidPadFormat.None) return "off"; + return val.ToString().ToLower(); + } } \ No newline at end of file diff --git a/PluralKit.Core/PluralKit.Core.csproj b/PluralKit.Core/PluralKit.Core.csproj index 6e5402c5..a11d9333 100644 --- a/PluralKit.Core/PluralKit.Core.csproj +++ b/PluralKit.Core/PluralKit.Core.csproj @@ -35,7 +35,7 @@ - + @@ -48,7 +48,7 @@ - + diff --git a/PluralKit.Core/Utils/Emojis.cs b/PluralKit.Core/Utils/Emojis.cs index 3f8b34bb..1ce9aa3f 100644 --- a/PluralKit.Core/Utils/Emojis.cs +++ b/PluralKit.Core/Utils/Emojis.cs @@ -2,7 +2,7 @@ namespace PluralKit.Core; public static class Emojis { - public static readonly string Warn = "\u26A0"; + public static readonly string Warn = "\u26A0\uFE0F"; public static readonly string Success = "\u2705"; public static readonly string Error = "\u274C"; public static readonly string Note = "\U0001f4dd"; 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/PluralKit.Core/Utils/StringUtils.cs b/PluralKit.Core/Utils/StringUtils.cs index 9a4a0410..6fe1860a 100644 --- a/PluralKit.Core/Utils/StringUtils.cs +++ b/PluralKit.Core/Utils/StringUtils.cs @@ -86,4 +86,10 @@ public static class StringUtils return output; } + + // Lightweight formatting that intentionally is very basic to not have silly things like in-template for loops like other templating engines seem to have + // Currently doesn't handle escapes which might cause problems + public static string SafeFormat(string template, (string pattern, string arg)[] args) => + args + .Aggregate(template, (acc, x) => acc.Replace(x.pattern, x.arg)); } \ No newline at end of file diff --git a/PluralKit.Core/packages.lock.json b/PluralKit.Core/packages.lock.json index 95224c57..7f9fef36 100644 --- a/PluralKit.Core/packages.lock.json +++ b/PluralKit.Core/packages.lock.json @@ -183,9 +183,9 @@ }, "Npgsql": { "type": "Direct", - "requested": "[4.1.5, )", - "resolved": "4.1.5", - "contentHash": "juDlNse+SKfXRP0VSgpJkpdCcaVLZt8m37EHdRX+8hw+GG69Eat1Y0MdEfl+oetdOnf9E133GjIDEjg9AF6HSQ==", + "requested": "[4.1.13, )", + "resolved": "4.1.13", + "contentHash": "p79cObfuRgS8KD5sFmQUqVlINEkJm39bCrzRclicZE1942mKcbLlc0NdoVKhBeZPv//prK/sVTUmRVxdnoPCoA==", "dependencies": { "System.Runtime.CompilerServices.Unsafe": "4.6.0" } @@ -315,12 +315,12 @@ }, "StackExchange.Redis": { "type": "Direct", - "requested": "[2.2.88, )", - "resolved": "2.2.88", - "contentHash": "JJi1jcO3/ZiamBhlsC/TR8aZmYf+nqpGzMi0HRRCy5wJkUPmMnRp0kBA6V84uhU8b531FHSdTDaFCAyCUJomjA==", + "requested": "[2.8.16, )", + "resolved": "2.8.16", + "contentHash": "WaoulkOqOC9jHepca3JZKFTqndCWab5uYS7qCzmiQDlrTkFaDN7eLSlEfHycBxipRnQY9ppZM7QSsWAwUEGblw==", "dependencies": { - "Pipelines.Sockets.Unofficial": "2.2.0", - "System.Diagnostics.PerformanceCounter": "5.0.0" + "Microsoft.Extensions.Logging.Abstractions": "6.0.0", + "Pipelines.Sockets.Unofficial": "2.2.8" } }, "System.Interactive.Async": { @@ -463,8 +463,8 @@ }, "Microsoft.Extensions.Logging.Abstractions": { "type": "Transitive", - "resolved": "3.1.10", - "contentHash": "bKHbgzbGsPZbEaExRaJqBz3WQ1GfhMttM23e1nivLJ8HbA3Ad526mW2G2K350q3Dc3HG83I5W8uSZWG4Rv4IpA==" + "resolved": "6.0.0", + "contentHash": "/HggWBbTwy8TgebGSX5DBZ24ndhzi93sHUBDvP1IxbZD7FDokYzdAr6+vbWGjw2XAfR2EJ1sfKUotpjHnFWPxA==" }, "Microsoft.Extensions.Options": { "type": "Transitive", @@ -482,8 +482,8 @@ }, "Microsoft.NETCore.Platforms": { "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "VyPlqzH2wavqquTcYpkIIAQ6WdenuKoFN0BdYBbCWsclXacSOHNQn66Gt4z5NBqEYW0FAPm5rlvki9ZiCij5xQ==" + "resolved": "1.1.0", + "contentHash": "kz0PEW2lhqygehI/d6XsPCQzD7ff7gUJaVGPVETX611eadGsA3A877GdSlU0LRVMCTH/+P3o2iDTak+S08V2+A==" }, "Microsoft.NETCore.Targets": { "type": "Transitive", @@ -500,23 +500,6 @@ "System.Runtime": "4.3.0" } }, - "Microsoft.Win32.Registry": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==", - "dependencies": { - "System.Security.AccessControl": "5.0.0", - "System.Security.Principal.Windows": "5.0.0" - } - }, - "Microsoft.Win32.SystemEvents": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "Bh6blKG8VAKvXiLe2L+sEsn62nc1Ij34MrNxepD2OCrS5cpCwQa9MeLyhVQPQ/R4Wlzwuy6wMK8hLb11QPDRsQ==", - "dependencies": { - "Microsoft.NETCore.Platforms": "5.0.0" - } - }, "NETStandard.Library": { "type": "Transitive", "resolved": "1.6.1", @@ -570,10 +553,10 @@ }, "Pipelines.Sockets.Unofficial": { "type": "Transitive", - "resolved": "2.2.0", - "contentHash": "7hzHplEIVOGBl5zOQZGX/DiJDHjq+RVRVrYgDiqXb6RriqWAdacXxp+XO9WSrATCEXyNOUOQg9aqQArsjase/A==", + "resolved": "2.2.8", + "contentHash": "zG2FApP5zxSx6OcdJQLbZDk2AVlN2BNQD6MorwIfV6gVj0RRxWPEp2LXAxqDGZqeNV1Zp0BNPcNaey/GXmTdvQ==", "dependencies": { - "System.IO.Pipelines": "5.0.0" + "System.IO.Pipelines": "5.0.1" } }, "runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl": { @@ -739,15 +722,6 @@ "System.Threading.Tasks": "4.3.0" } }, - "System.Configuration.ConfigurationManager": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "aM7cbfEfVNlEEOj3DsZP+2g9NRwbkyiAv2isQEzw7pnkDg9ekCU2m1cdJLM02Uq691OaCS91tooaxcEn8d0q5w==", - "dependencies": { - "System.Security.Cryptography.ProtectedData": "5.0.0", - "System.Security.Permissions": "5.0.0" - } - }, "System.Console": { "type": "Transitive", "resolved": "4.3.0", @@ -775,17 +749,6 @@ "resolved": "4.7.1", "contentHash": "j81Lovt90PDAq8kLpaJfJKV/rWdWuEk6jfV+MBkee33vzYLEUsy4gXK8laa9V2nZlLM9VM9yA/OOQxxPEJKAMw==" }, - "System.Diagnostics.PerformanceCounter": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "kcQWWtGVC3MWMNXdMDWfrmIlFZZ2OdoeT6pSNVRtk9+Sa7jwdPiMlNwb0ZQcS7NRlT92pCfmjRtkSWUW3RAKwg==", - "dependencies": { - "Microsoft.NETCore.Platforms": "5.0.0", - "Microsoft.Win32.Registry": "5.0.0", - "System.Configuration.ConfigurationManager": "5.0.0", - "System.Security.Principal.Windows": "5.0.0" - } - }, "System.Diagnostics.Tools": { "type": "Transitive", "resolved": "4.3.0", @@ -806,14 +769,6 @@ "System.Runtime": "4.3.0" } }, - "System.Drawing.Common": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "SztFwAnpfKC8+sEKXAFxCBWhKQaEd97EiOL7oZJZP56zbqnLpmxACWA8aGseaUExciuEAUuR9dY8f7HkTRAdnw==", - "dependencies": { - "Microsoft.Win32.SystemEvents": "5.0.0" - } - }, "System.Globalization": { "type": "Transitive", "resolved": "4.3.0", @@ -923,8 +878,8 @@ }, "System.IO.Pipelines": { "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "irMYm3vhVgRsYvHTU5b2gsT2CwT/SMM6LZFzuJjpIvT5Z4CshxNsaoBC1X/LltwuR3Opp8d6jOS/60WwOb7Q2Q==" + "resolved": "5.0.1", + "contentHash": "qEePWsaq9LoEEIqhbGe6D5J8c9IqQOUuTzzV6wn1POlfdLkJliZY3OlB0j0f17uMWlqZYjH7txj+2YbyrIA8Yg==" }, "System.Linq": { "type": "Transitive", @@ -1187,15 +1142,6 @@ "System.Runtime.Extensions": "4.3.0" } }, - "System.Security.AccessControl": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "dagJ1mHZO3Ani8GH0PHpPEe/oYO+rVdbQjvjJkBRNQkX4t0r1iaeGn8+/ybkSLEan3/slM0t59SVdHzuHf2jmw==", - "dependencies": { - "Microsoft.NETCore.Platforms": "5.0.0", - "System.Security.Principal.Windows": "5.0.0" - } - }, "System.Security.Cryptography.Algorithms": { "type": "Transitive", "resolved": "4.3.0", @@ -1308,11 +1254,6 @@ "System.Threading.Tasks": "4.3.0" } }, - "System.Security.Cryptography.ProtectedData": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "HGxMSAFAPLNoxBvSfW08vHde0F9uh7BjASwu6JF9JnXuEPhCY3YUqURn0+bQV/4UWeaqymmrHWV+Aw9riQCtCA==" - }, "System.Security.Cryptography.X509Certificates": { "type": "Transitive", "resolved": "4.3.0", @@ -1345,20 +1286,6 @@ "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" } }, - "System.Security.Permissions": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "uE8juAhEkp7KDBCdjDIE3H9R1HJuEHqeqX8nLX9gmYKWwsqk3T5qZlPx8qle5DPKimC/Fy3AFTdV7HamgCh9qQ==", - "dependencies": { - "System.Security.AccessControl": "5.0.0", - "System.Windows.Extensions": "5.0.0" - } - }, - "System.Security.Principal.Windows": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "t0MGLukB5WAVU9bO3MGzvlGnyJPgUlcwerXn1kzBRjwLKixT96XV0Uza41W49gVd8zEMFu9vQEFlv0IOrytICA==" - }, "System.Text.Encoding": { "type": "Transitive", "resolved": "4.3.0", @@ -1432,14 +1359,6 @@ "System.Runtime": "4.3.0" } }, - "System.Windows.Extensions": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "c1ho9WU9ZxMZawML+ssPKZfdnrg/OjR3pe0m9v8230z3acqphwvPJqzAkH54xRYm5ntZHGG1EPP3sux9H3qSPg==", - "dependencies": { - "System.Drawing.Common": "5.0.0" - } - }, "System.Xml.ReaderWriter": { "type": "Transitive", "resolved": "4.3.0", diff --git a/PluralKit.Tests/packages.lock.json b/PluralKit.Tests/packages.lock.json index ea4c6ea5..122f7381 100644 --- a/PluralKit.Tests/packages.lock.json +++ b/PluralKit.Tests/packages.lock.json @@ -427,8 +427,8 @@ }, "Microsoft.Extensions.Logging.Abstractions": { "type": "Transitive", - "resolved": "3.1.10", - "contentHash": "bKHbgzbGsPZbEaExRaJqBz3WQ1GfhMttM23e1nivLJ8HbA3Ad526mW2G2K350q3Dc3HG83I5W8uSZWG4Rv4IpA==" + "resolved": "6.0.0", + "contentHash": "/HggWBbTwy8TgebGSX5DBZ24ndhzi93sHUBDvP1IxbZD7FDokYzdAr6+vbWGjw2XAfR2EJ1sfKUotpjHnFWPxA==" }, "Microsoft.Extensions.Options": { "type": "Transitive", @@ -457,8 +457,8 @@ }, "Microsoft.NETCore.Platforms": { "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "VyPlqzH2wavqquTcYpkIIAQ6WdenuKoFN0BdYBbCWsclXacSOHNQn66Gt4z5NBqEYW0FAPm5rlvki9ZiCij5xQ==" + "resolved": "1.1.0", + "contentHash": "kz0PEW2lhqygehI/d6XsPCQzD7ff7gUJaVGPVETX611eadGsA3A877GdSlU0LRVMCTH/+P3o2iDTak+S08V2+A==" }, "Microsoft.NETCore.Targets": { "type": "Transitive", @@ -492,23 +492,6 @@ "System.Runtime": "4.3.0" } }, - "Microsoft.Win32.Registry": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==", - "dependencies": { - "System.Security.AccessControl": "5.0.0", - "System.Security.Principal.Windows": "5.0.0" - } - }, - "Microsoft.Win32.SystemEvents": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "Bh6blKG8VAKvXiLe2L+sEsn62nc1Ij34MrNxepD2OCrS5cpCwQa9MeLyhVQPQ/R4Wlzwuy6wMK8hLb11QPDRsQ==", - "dependencies": { - "Microsoft.NETCore.Platforms": "5.0.0" - } - }, "NETStandard.Library": { "type": "Transitive", "resolved": "1.6.1", @@ -592,8 +575,8 @@ }, "Npgsql": { "type": "Transitive", - "resolved": "4.1.5", - "contentHash": "juDlNse+SKfXRP0VSgpJkpdCcaVLZt8m37EHdRX+8hw+GG69Eat1Y0MdEfl+oetdOnf9E133GjIDEjg9AF6HSQ==", + "resolved": "4.1.13", + "contentHash": "p79cObfuRgS8KD5sFmQUqVlINEkJm39bCrzRclicZE1942mKcbLlc0NdoVKhBeZPv//prK/sVTUmRVxdnoPCoA==", "dependencies": { "System.Runtime.CompilerServices.Unsafe": "4.6.0" } @@ -614,10 +597,10 @@ }, "Pipelines.Sockets.Unofficial": { "type": "Transitive", - "resolved": "2.2.0", - "contentHash": "7hzHplEIVOGBl5zOQZGX/DiJDHjq+RVRVrYgDiqXb6RriqWAdacXxp+XO9WSrATCEXyNOUOQg9aqQArsjase/A==", + "resolved": "2.2.8", + "contentHash": "zG2FApP5zxSx6OcdJQLbZDk2AVlN2BNQD6MorwIfV6gVj0RRxWPEp2LXAxqDGZqeNV1Zp0BNPcNaey/GXmTdvQ==", "dependencies": { - "System.IO.Pipelines": "5.0.0" + "System.IO.Pipelines": "5.0.1" } }, "Polly": { @@ -739,8 +722,8 @@ }, "Sentry": { "type": "Transitive", - "resolved": "3.11.1", - "contentHash": "T/NLfs6MMkUSYsPEDajB9ad0124T18I0uUod5MNOev3iwjvcnIEQBrStEX2olbIxzqfvGXzQ/QFqTfA2ElLPlA==" + "resolved": "4.12.1", + "contentHash": "OLf7885OKHWLaTLTyw884mwOT4XKCWj2Hz5Wuz/TJemJqXwCIdIljkJBIoeHviRUPvtB7ulDgeYXf/Z7ScToSA==" }, "Serilog": { "type": "Transitive", @@ -891,8 +874,8 @@ }, "SixLabors.ImageSharp": { "type": "Transitive", - "resolved": "3.0.1", - "contentHash": "o0v/J6SJwp3RFrzR29beGx0cK7xcMRgOyIuw8ZNLQyNnBhiyL/vIQKn7cfycthcWUPG3XezUjFwBWzkcUUDFbg==" + "resolved": "3.1.5", + "contentHash": "lNtlq7dSI/QEbYey+A0xn48z5w4XHSffF8222cC4F4YwTXfEImuiBavQcWjr49LThT/pRmtWJRcqA/PlL+eJ6g==" }, "SqlKata": { "type": "Transitive", @@ -914,11 +897,11 @@ }, "StackExchange.Redis": { "type": "Transitive", - "resolved": "2.2.88", - "contentHash": "JJi1jcO3/ZiamBhlsC/TR8aZmYf+nqpGzMi0HRRCy5wJkUPmMnRp0kBA6V84uhU8b531FHSdTDaFCAyCUJomjA==", + "resolved": "2.8.16", + "contentHash": "WaoulkOqOC9jHepca3JZKFTqndCWab5uYS7qCzmiQDlrTkFaDN7eLSlEfHycBxipRnQY9ppZM7QSsWAwUEGblw==", "dependencies": { - "Pipelines.Sockets.Unofficial": "2.2.0", - "System.Diagnostics.PerformanceCounter": "5.0.0" + "Microsoft.Extensions.Logging.Abstractions": "6.0.0", + "Pipelines.Sockets.Unofficial": "2.2.8" } }, "System.AppContext": { @@ -961,15 +944,6 @@ "System.Threading.Tasks": "4.3.0" } }, - "System.Configuration.ConfigurationManager": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "aM7cbfEfVNlEEOj3DsZP+2g9NRwbkyiAv2isQEzw7pnkDg9ekCU2m1cdJLM02Uq691OaCS91tooaxcEn8d0q5w==", - "dependencies": { - "System.Security.Cryptography.ProtectedData": "5.0.0", - "System.Security.Permissions": "5.0.0" - } - }, "System.Console": { "type": "Transitive", "resolved": "4.3.0", @@ -997,17 +971,6 @@ "resolved": "4.7.1", "contentHash": "j81Lovt90PDAq8kLpaJfJKV/rWdWuEk6jfV+MBkee33vzYLEUsy4gXK8laa9V2nZlLM9VM9yA/OOQxxPEJKAMw==" }, - "System.Diagnostics.PerformanceCounter": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "kcQWWtGVC3MWMNXdMDWfrmIlFZZ2OdoeT6pSNVRtk9+Sa7jwdPiMlNwb0ZQcS7NRlT92pCfmjRtkSWUW3RAKwg==", - "dependencies": { - "Microsoft.NETCore.Platforms": "5.0.0", - "Microsoft.Win32.Registry": "5.0.0", - "System.Configuration.ConfigurationManager": "5.0.0", - "System.Security.Principal.Windows": "5.0.0" - } - }, "System.Diagnostics.Tools": { "type": "Transitive", "resolved": "4.3.0", @@ -1028,14 +991,6 @@ "System.Runtime": "4.3.0" } }, - "System.Drawing.Common": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "SztFwAnpfKC8+sEKXAFxCBWhKQaEd97EiOL7oZJZP56zbqnLpmxACWA8aGseaUExciuEAUuR9dY8f7HkTRAdnw==", - "dependencies": { - "Microsoft.Win32.SystemEvents": "5.0.0" - } - }, "System.Dynamic.Runtime": { "type": "Transitive", "resolved": "4.0.11", @@ -1175,8 +1130,8 @@ }, "System.IO.Pipelines": { "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "irMYm3vhVgRsYvHTU5b2gsT2CwT/SMM6LZFzuJjpIvT5Z4CshxNsaoBC1X/LltwuR3Opp8d6jOS/60WwOb7Q2Q==" + "resolved": "5.0.1", + "contentHash": "qEePWsaq9LoEEIqhbGe6D5J8c9IqQOUuTzzV6wn1POlfdLkJliZY3OlB0j0f17uMWlqZYjH7txj+2YbyrIA8Yg==" }, "System.Linq": { "type": "Transitive", @@ -1439,15 +1394,6 @@ "System.Runtime.Extensions": "4.3.0" } }, - "System.Security.AccessControl": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "dagJ1mHZO3Ani8GH0PHpPEe/oYO+rVdbQjvjJkBRNQkX4t0r1iaeGn8+/ybkSLEan3/slM0t59SVdHzuHf2jmw==", - "dependencies": { - "Microsoft.NETCore.Platforms": "5.0.0", - "System.Security.Principal.Windows": "5.0.0" - } - }, "System.Security.Cryptography.Algorithms": { "type": "Transitive", "resolved": "4.3.0", @@ -1560,11 +1506,6 @@ "System.Threading.Tasks": "4.3.0" } }, - "System.Security.Cryptography.ProtectedData": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "HGxMSAFAPLNoxBvSfW08vHde0F9uh7BjASwu6JF9JnXuEPhCY3YUqURn0+bQV/4UWeaqymmrHWV+Aw9riQCtCA==" - }, "System.Security.Cryptography.X509Certificates": { "type": "Transitive", "resolved": "4.3.0", @@ -1597,20 +1538,6 @@ "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" } }, - "System.Security.Permissions": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "uE8juAhEkp7KDBCdjDIE3H9R1HJuEHqeqX8nLX9gmYKWwsqk3T5qZlPx8qle5DPKimC/Fy3AFTdV7HamgCh9qQ==", - "dependencies": { - "System.Security.AccessControl": "5.0.0", - "System.Windows.Extensions": "5.0.0" - } - }, - "System.Security.Principal.Windows": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "t0MGLukB5WAVU9bO3MGzvlGnyJPgUlcwerXn1kzBRjwLKixT96XV0Uza41W49gVd8zEMFu9vQEFlv0IOrytICA==" - }, "System.Text.Encoding": { "type": "Transitive", "resolved": "4.3.0", @@ -1684,14 +1611,6 @@ "System.Runtime": "4.3.0" } }, - "System.Windows.Extensions": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "c1ho9WU9ZxMZawML+ssPKZfdnrg/OjR3pe0m9v8230z3acqphwvPJqzAkH54xRYm5ntZHGG1EPP3sux9H3qSPg==", - "dependencies": { - "System.Drawing.Common": "5.0.0" - } - }, "System.Xml.ReaderWriter": { "type": "Transitive", "resolved": "4.3.0", @@ -1809,8 +1728,8 @@ "Humanizer.Core": "[2.8.26, )", "Myriad": "[1.0.0, )", "PluralKit.Core": "[1.0.0, )", - "Sentry": "[3.11.1, )", - "SixLabors.ImageSharp": "[3.0.1, )" + "Sentry": "[4.12.1, )", + "SixLabors.ImageSharp": "[3.1.5, )" } }, "pluralkit.core": { @@ -1834,7 +1753,7 @@ "Newtonsoft.Json": "[13.0.1, )", "NodaTime": "[3.0.3, )", "NodaTime.Serialization.JsonNet": "[3.0.0, )", - "Npgsql": "[4.1.5, )", + "Npgsql": "[4.1.13, )", "Npgsql.NodaTime": "[4.1.5, )", "Serilog": "[2.12.0, )", "Serilog.Extensions.Logging": "[3.0.1, )", @@ -1847,7 +1766,7 @@ "Serilog.Sinks.Seq": "[5.2.2, )", "SqlKata": "[2.3.7, )", "SqlKata.Execution": "[2.3.7, )", - "StackExchange.Redis": "[2.2.88, )", + "StackExchange.Redis": "[2.8.16, )", "System.Interactive.Async": "[5.0.0, )", "ipnetwork2": "[2.5.381, )" } diff --git a/ci/Dockerfile.rust b/ci/Dockerfile.rust new file mode 100644 index 00000000..9e6dad46 --- /dev/null +++ b/ci/Dockerfile.rust @@ -0,0 +1,46 @@ +FROM alpine:latest AS builder + +WORKDIR /build + +RUN apk add rustup build-base protoc +# todo: arm64 target +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' + +RUN cargo install cargo-chef --locked + +# build dependencies first to cache +FROM builder AS recipe-builder +COPY . . +RUN cargo chef prepare --recipe-path recipe.json + +FROM builder AS binary-builder +COPY --from=recipe-builder /build/recipe.json recipe.json +RUN cargo chef cook --release --recipe-path recipe.json --target x86_64-unknown-linux-musl + +COPY Cargo.toml /build/ +COPY Cargo.lock /build/ +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/dispatch/ /build/services/dispatch +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 dispatch --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 +RUN cargo build --bin avatar_cleanup --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/dispatch /dispatch +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 +COPY --from=binary-builder /build/target/x86_64-unknown-linux-musl/release/avatar_cleanup /avatar_cleanup diff --git a/ci/rust-docker-target.sh b/ci/rust-docker-target.sh new file mode 100755 index 00000000..9932abf8 --- /dev/null +++ b/ci/rust-docker-target.sh @@ -0,0 +1,43 @@ +#!/bin/bash + +set -e + +#tag= +#branch= +#push= + +build() { + bin=$1 + extra=$2 + + f=$(mktemp) + + cat > $f << EOF +FROM alpine:latest +COPY .docker-bin/$bin /bin/$bin +$extra +CMD ["/bin/$bin"] +EOF + + echo "building $dockerfile" + + $dockerfile | docker build -t ghcr.io/pluralkit/$bin:$tag -f $f . + + rm $f + + if [ "$push" == "true" ]; then + docker push ghcr.io/pluralkit/$bin:$tag + docker image tag ghcr.io/pluralkit/$bin:$tag ghcr.io/pluralkit/$bin:$branch + docker push ghcr.io/pluralkit/$bin:$branch + if [ "$branch" == "main" ]; then + docker image tag ghcr.io/pluralkit/$bin:$tag ghcr.io/pluralkit/$bin:latest + docker push ghcr.io/pluralkit/$bin:latest + fi + fi +} + +# add rust binaries here to build +build api +build dispatch +build gateway +build avatars "COPY .docker-bin/avatar_cleanup /bin/avatar_cleanup" diff --git a/dashboard/main.go b/dashboard/main.go index 294fe6dc..926b23fc 100644 --- a/dashboard/main.go +++ b/dashboard/main.go @@ -46,6 +46,7 @@ func main() { r.NotFound(notFoundHandler) r.Get("/profile/{type}/{id}", func(rw http.ResponseWriter, r *http.Request) { + rw.Header().Set("X-Robots-Tag", "noindex") defer func() { if a := recover(); a != nil { notFoundHandler(rw, r) @@ -74,6 +75,12 @@ func notFoundHandler(rw http.ResponseWriter, r *http.Request) { rw.Header().Set("content-type", "text/css") } else if strings.HasSuffix(r.URL.Path, ".map") { data, err = fs.ReadFile("dist" + r.URL.Path) + } else if strings.HasSuffix(r.URL.Path, ".ttf") { + data, err = fs.ReadFile("dist" + r.URL.Path) + rw.Header().Set("content-type", "application/x-font-ttf") + } else if strings.HasSuffix(r.URL.Path, ".woff2") { + data, err = fs.ReadFile("dist" + r.URL.Path) + rw.Header().Set("content-type", "application/font-woff2") } else { data, err = fs.ReadFile("dist/index.html") rw.Header().Set("content-type", "text/html") diff --git a/dashboard/package.json b/dashboard/package.json index 1160f37f..26725e70 100644 --- a/dashboard/package.json +++ b/dashboard/package.json @@ -11,6 +11,7 @@ "devDependencies": { "@sveltejs/vite-plugin-svelte": "^1.0.0-next.30", "@tsconfig/svelte": "^3.0.0", + "js-base64": "^3.7.7", "svelte": "^3.44.0", "svelte-check": "^2.2.7", "svelte-toggle": "^3.1.0", @@ -26,7 +27,7 @@ "bootstrap": "^5.1.3", "bootstrap-dark-5": "^1.1.3", "core-js-pure": "^3.23.4", - "discord-markdown": "https://github.com/Draconizations/discord-markdown#fe852ba7bf2f56744a632207a314d749aa12dd65", + "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/public/fonts/AtkinsonHyperlegible-Bold.woff2 b/dashboard/public/fonts/AtkinsonHyperlegible-Bold.woff2 new file mode 100644 index 00000000..28b97916 Binary files /dev/null and b/dashboard/public/fonts/AtkinsonHyperlegible-Bold.woff2 differ diff --git a/dashboard/public/fonts/AtkinsonHyperlegible-BoldItalic.woff2 b/dashboard/public/fonts/AtkinsonHyperlegible-BoldItalic.woff2 new file mode 100644 index 00000000..ae61bb66 Binary files /dev/null and b/dashboard/public/fonts/AtkinsonHyperlegible-BoldItalic.woff2 differ diff --git a/dashboard/public/fonts/AtkinsonHyperlegible-Italic.woff2 b/dashboard/public/fonts/AtkinsonHyperlegible-Italic.woff2 new file mode 100644 index 00000000..3b450b7e Binary files /dev/null and b/dashboard/public/fonts/AtkinsonHyperlegible-Italic.woff2 differ diff --git a/dashboard/public/fonts/AtkinsonHyperlegible-Regular.woff2 b/dashboard/public/fonts/AtkinsonHyperlegible-Regular.woff2 new file mode 100644 index 00000000..d75e2a94 Binary files /dev/null and b/dashboard/public/fonts/AtkinsonHyperlegible-Regular.woff2 differ diff --git a/dashboard/public/fonts/OpenDyslexic-Bold.ttf b/dashboard/public/fonts/OpenDyslexic-Bold.ttf new file mode 100644 index 00000000..6da12f24 Binary files /dev/null and b/dashboard/public/fonts/OpenDyslexic-Bold.ttf differ diff --git a/dashboard/public/fonts/OpenDyslexic-BoldItalic.ttf b/dashboard/public/fonts/OpenDyslexic-BoldItalic.ttf new file mode 100644 index 00000000..8259bb9c Binary files /dev/null and b/dashboard/public/fonts/OpenDyslexic-BoldItalic.ttf differ diff --git a/dashboard/public/fonts/OpenDyslexic-Italic.ttf b/dashboard/public/fonts/OpenDyslexic-Italic.ttf new file mode 100644 index 00000000..fe242496 Binary files /dev/null and b/dashboard/public/fonts/OpenDyslexic-Italic.ttf differ diff --git a/dashboard/public/fonts/OpenDyslexic-Regular.ttf b/dashboard/public/fonts/OpenDyslexic-Regular.ttf new file mode 100644 index 00000000..3232334d Binary files /dev/null and b/dashboard/public/fonts/OpenDyslexic-Regular.ttf differ diff --git a/dashboard/src/api/parse-markdown.ts b/dashboard/src/api/parse-markdown.ts index daf9b833..c9259ad8 100644 --- a/dashboard/src/api/parse-markdown.ts +++ b/dashboard/src/api/parse-markdown.ts @@ -1,377 +1,383 @@ -import { toHTML } from 'discord-markdown'; -import hljs from 'highlight.js/lib/core'; -import parseTimestamps from './parse-timestamps'; +import discordMarkdown from "discord-markdown"; +import hljs from "highlight.js/lib/core"; +import parseTimestamps from "./parse-timestamps"; +import { Base64 } from "js-base64"; -const languages: Record Promise> = { +const { toHTML } = discordMarkdown; + +const languages: Record< + string, + () => Promise +> = { "1c": () => import("highlight.js/lib/languages/1c"), - "abnf": () => import("highlight.js/lib/languages/abnf"), - "accesslog": () => import("highlight.js/lib/languages/accesslog"), - "actionscript": () => import("highlight.js/lib/languages/actionscript"), - "ada": () => import("highlight.js/lib/languages/ada"), - "angelscript": () => import("highlight.js/lib/languages/angelscript"), - "apache": () => import("highlight.js/lib/languages/apache"), - "applescript": () => import("highlight.js/lib/languages/applescript"), - "arcade": () => import("highlight.js/lib/languages/arcade"), - "arduino": () => import("highlight.js/lib/languages/arduino"), - "armasm": () => import("highlight.js/lib/languages/armasm"), - "xml": () => import("highlight.js/lib/languages/xml"), - "asciidoc": () => import("highlight.js/lib/languages/asciidoc"), - "aspectj": () => import("highlight.js/lib/languages/aspectj"), - "autohotkey": () => import("highlight.js/lib/languages/autohotkey"), - "autoit": () => import("highlight.js/lib/languages/autoit"), - "avrasm": () => import("highlight.js/lib/languages/avrasm"), - "awk": () => import("highlight.js/lib/languages/awk"), - "axapta": () => import("highlight.js/lib/languages/axapta"), - "bash": () => import("highlight.js/lib/languages/bash"), - "basic": () => import("highlight.js/lib/languages/basic"), - "bnf": () => import("highlight.js/lib/languages/bnf"), - "brainfuck": () => import("highlight.js/lib/languages/brainfuck"), - "c": () => import("highlight.js/lib/languages/c"), - "cal": () => import("highlight.js/lib/languages/cal"), - "capnproto": () => import("highlight.js/lib/languages/capnproto"), - "ceylon": () => import("highlight.js/lib/languages/ceylon"), - "clean": () => import("highlight.js/lib/languages/clean"), - "clojure": () => import("highlight.js/lib/languages/clojure"), + abnf: () => import("highlight.js/lib/languages/abnf"), + accesslog: () => import("highlight.js/lib/languages/accesslog"), + actionscript: () => import("highlight.js/lib/languages/actionscript"), + ada: () => import("highlight.js/lib/languages/ada"), + angelscript: () => import("highlight.js/lib/languages/angelscript"), + apache: () => import("highlight.js/lib/languages/apache"), + applescript: () => import("highlight.js/lib/languages/applescript"), + arcade: () => import("highlight.js/lib/languages/arcade"), + arduino: () => import("highlight.js/lib/languages/arduino"), + armasm: () => import("highlight.js/lib/languages/armasm"), + xml: () => import("highlight.js/lib/languages/xml"), + asciidoc: () => import("highlight.js/lib/languages/asciidoc"), + aspectj: () => import("highlight.js/lib/languages/aspectj"), + autohotkey: () => import("highlight.js/lib/languages/autohotkey"), + autoit: () => import("highlight.js/lib/languages/autoit"), + avrasm: () => import("highlight.js/lib/languages/avrasm"), + awk: () => import("highlight.js/lib/languages/awk"), + axapta: () => import("highlight.js/lib/languages/axapta"), + bash: () => import("highlight.js/lib/languages/bash"), + basic: () => import("highlight.js/lib/languages/basic"), + bnf: () => import("highlight.js/lib/languages/bnf"), + brainfuck: () => import("highlight.js/lib/languages/brainfuck"), + c: () => import("highlight.js/lib/languages/c"), + cal: () => import("highlight.js/lib/languages/cal"), + capnproto: () => import("highlight.js/lib/languages/capnproto"), + ceylon: () => import("highlight.js/lib/languages/ceylon"), + clean: () => import("highlight.js/lib/languages/clean"), + clojure: () => import("highlight.js/lib/languages/clojure"), "clojure-repl": () => import("highlight.js/lib/languages/clojure-repl"), - "cmake": () => import("highlight.js/lib/languages/cmake"), - "coffeescript": () => import("highlight.js/lib/languages/coffeescript"), - "coq": () => import("highlight.js/lib/languages/coq"), - "cos": () => import("highlight.js/lib/languages/cos"), - "cpp": () => import("highlight.js/lib/languages/cpp"), - "crmsh": () => import("highlight.js/lib/languages/crmsh"), - "crystal": () => import("highlight.js/lib/languages/crystal"), - "csharp": () => import("highlight.js/lib/languages/csharp"), - "csp": () => import("highlight.js/lib/languages/csp"), - "css": () => import("highlight.js/lib/languages/css"), - "d": () => import("highlight.js/lib/languages/d"), - "markdown": () => import("highlight.js/lib/languages/markdown"), - "dart": () => import("highlight.js/lib/languages/dart"), - "delphi": () => import("highlight.js/lib/languages/delphi"), - "diff": () => import("highlight.js/lib/languages/diff"), - "django": () => import("highlight.js/lib/languages/django"), - "dns": () => import("highlight.js/lib/languages/dns"), - "dockerfile": () => import("highlight.js/lib/languages/dockerfile"), - "dos": () => import("highlight.js/lib/languages/dos"), - "dsconfig": () => import("highlight.js/lib/languages/dsconfig"), - "dts": () => import("highlight.js/lib/languages/dts"), - "dust": () => import("highlight.js/lib/languages/dust"), - "ebnf": () => import("highlight.js/lib/languages/ebnf"), - "elixir": () => import("highlight.js/lib/languages/elixir"), - "elm": () => import("highlight.js/lib/languages/elm"), - "ruby": () => import("highlight.js/lib/languages/ruby"), - "erb": () => import("highlight.js/lib/languages/erb"), + cmake: () => import("highlight.js/lib/languages/cmake"), + coffeescript: () => import("highlight.js/lib/languages/coffeescript"), + coq: () => import("highlight.js/lib/languages/coq"), + cos: () => import("highlight.js/lib/languages/cos"), + cpp: () => import("highlight.js/lib/languages/cpp"), + crmsh: () => import("highlight.js/lib/languages/crmsh"), + crystal: () => import("highlight.js/lib/languages/crystal"), + csharp: () => import("highlight.js/lib/languages/csharp"), + csp: () => import("highlight.js/lib/languages/csp"), + css: () => import("highlight.js/lib/languages/css"), + d: () => import("highlight.js/lib/languages/d"), + markdown: () => import("highlight.js/lib/languages/markdown"), + dart: () => import("highlight.js/lib/languages/dart"), + delphi: () => import("highlight.js/lib/languages/delphi"), + diff: () => import("highlight.js/lib/languages/diff"), + django: () => import("highlight.js/lib/languages/django"), + dns: () => import("highlight.js/lib/languages/dns"), + dockerfile: () => import("highlight.js/lib/languages/dockerfile"), + dos: () => import("highlight.js/lib/languages/dos"), + dsconfig: () => import("highlight.js/lib/languages/dsconfig"), + dts: () => import("highlight.js/lib/languages/dts"), + dust: () => import("highlight.js/lib/languages/dust"), + ebnf: () => import("highlight.js/lib/languages/ebnf"), + elixir: () => import("highlight.js/lib/languages/elixir"), + elm: () => import("highlight.js/lib/languages/elm"), + ruby: () => import("highlight.js/lib/languages/ruby"), + erb: () => import("highlight.js/lib/languages/erb"), "erlang-repl": () => import("highlight.js/lib/languages/erlang-repl"), - "erlang": () => import("highlight.js/lib/languages/erlang"), - "excel": () => import("highlight.js/lib/languages/excel"), - "fix": () => import("highlight.js/lib/languages/fix"), - "flix": () => import("highlight.js/lib/languages/flix"), - "fortran": () => import("highlight.js/lib/languages/fortran"), - "fsharp": () => import("highlight.js/lib/languages/fsharp"), - "gams": () => import("highlight.js/lib/languages/gams"), - "gauss": () => import("highlight.js/lib/languages/gauss"), - "gcode": () => import("highlight.js/lib/languages/gcode"), - "gherkin": () => import("highlight.js/lib/languages/gherkin"), - "glsl": () => import("highlight.js/lib/languages/glsl"), - "gml": () => import("highlight.js/lib/languages/gml"), - "go": () => import("highlight.js/lib/languages/go"), - "golo": () => import("highlight.js/lib/languages/golo"), - "gradle": () => import("highlight.js/lib/languages/gradle"), - "graphql": () => import("highlight.js/lib/languages/graphql"), - "groovy": () => import("highlight.js/lib/languages/groovy"), - "haml": () => import("highlight.js/lib/languages/haml"), - "handlebars": () => import("highlight.js/lib/languages/handlebars"), - "haskell": () => import("highlight.js/lib/languages/haskell"), - "haxe": () => import("highlight.js/lib/languages/haxe"), - "hsp": () => import("highlight.js/lib/languages/hsp"), - "http": () => import("highlight.js/lib/languages/http"), - "hy": () => import("highlight.js/lib/languages/hy"), - "inform7": () => import("highlight.js/lib/languages/inform7"), - "ini": () => import("highlight.js/lib/languages/ini"), - "irpf90": () => import("highlight.js/lib/languages/irpf90"), - "isbl": () => import("highlight.js/lib/languages/isbl"), - "java": () => import("highlight.js/lib/languages/java"), - "javascript": () => import("highlight.js/lib/languages/javascript"), + erlang: () => import("highlight.js/lib/languages/erlang"), + excel: () => import("highlight.js/lib/languages/excel"), + fix: () => import("highlight.js/lib/languages/fix"), + flix: () => import("highlight.js/lib/languages/flix"), + fortran: () => import("highlight.js/lib/languages/fortran"), + fsharp: () => import("highlight.js/lib/languages/fsharp"), + gams: () => import("highlight.js/lib/languages/gams"), + gauss: () => import("highlight.js/lib/languages/gauss"), + gcode: () => import("highlight.js/lib/languages/gcode"), + gherkin: () => import("highlight.js/lib/languages/gherkin"), + glsl: () => import("highlight.js/lib/languages/glsl"), + gml: () => import("highlight.js/lib/languages/gml"), + go: () => import("highlight.js/lib/languages/go"), + golo: () => import("highlight.js/lib/languages/golo"), + gradle: () => import("highlight.js/lib/languages/gradle"), + graphql: () => import("highlight.js/lib/languages/graphql"), + groovy: () => import("highlight.js/lib/languages/groovy"), + haml: () => import("highlight.js/lib/languages/haml"), + handlebars: () => import("highlight.js/lib/languages/handlebars"), + haskell: () => import("highlight.js/lib/languages/haskell"), + haxe: () => import("highlight.js/lib/languages/haxe"), + hsp: () => import("highlight.js/lib/languages/hsp"), + http: () => import("highlight.js/lib/languages/http"), + hy: () => import("highlight.js/lib/languages/hy"), + inform7: () => import("highlight.js/lib/languages/inform7"), + ini: () => import("highlight.js/lib/languages/ini"), + irpf90: () => import("highlight.js/lib/languages/irpf90"), + isbl: () => import("highlight.js/lib/languages/isbl"), + java: () => import("highlight.js/lib/languages/java"), + javascript: () => import("highlight.js/lib/languages/javascript"), "jboss-cli": () => import("highlight.js/lib/languages/jboss-cli"), - "json": () => import("highlight.js/lib/languages/json"), - "julia": () => import("highlight.js/lib/languages/julia"), + json: () => import("highlight.js/lib/languages/json"), + julia: () => import("highlight.js/lib/languages/julia"), "julia-repl": () => import("highlight.js/lib/languages/julia-repl"), - "kotlin": () => import("highlight.js/lib/languages/kotlin"), - "lasso": () => import("highlight.js/lib/languages/lasso"), - "latex": () => import("highlight.js/lib/languages/latex"), - "ldif": () => import("highlight.js/lib/languages/ldif"), - "leaf": () => import("highlight.js/lib/languages/leaf"), - "less": () => import("highlight.js/lib/languages/less"), - "lisp": () => import("highlight.js/lib/languages/lisp"), - "livecodeserver": () => import("highlight.js/lib/languages/livecodeserver"), - "livescript": () => import("highlight.js/lib/languages/livescript"), - "llvm": () => import("highlight.js/lib/languages/llvm"), - "lsl": () => import("highlight.js/lib/languages/lsl"), - "lua": () => import("highlight.js/lib/languages/lua"), - "makefile": () => import("highlight.js/lib/languages/makefile"), - "mathematica": () => import("highlight.js/lib/languages/mathematica"), - "matlab": () => import("highlight.js/lib/languages/matlab"), - "maxima": () => import("highlight.js/lib/languages/maxima"), - "mel": () => import("highlight.js/lib/languages/mel"), - "mercury": () => import("highlight.js/lib/languages/mercury"), - "mipsasm": () => import("highlight.js/lib/languages/mipsasm"), - "mizar": () => import("highlight.js/lib/languages/mizar"), - "perl": () => import("highlight.js/lib/languages/perl"), - "mojolicious": () => import("highlight.js/lib/languages/mojolicious"), - "monkey": () => import("highlight.js/lib/languages/monkey"), - "moonscript": () => import("highlight.js/lib/languages/moonscript"), - "n1ql": () => import("highlight.js/lib/languages/n1ql"), - "nestedtext": () => import("highlight.js/lib/languages/nestedtext"), - "nginx": () => import("highlight.js/lib/languages/nginx"), - "nim": () => import("highlight.js/lib/languages/nim"), - "nix": () => import("highlight.js/lib/languages/nix"), + kotlin: () => import("highlight.js/lib/languages/kotlin"), + lasso: () => import("highlight.js/lib/languages/lasso"), + latex: () => import("highlight.js/lib/languages/latex"), + ldif: () => import("highlight.js/lib/languages/ldif"), + leaf: () => import("highlight.js/lib/languages/leaf"), + less: () => import("highlight.js/lib/languages/less"), + lisp: () => import("highlight.js/lib/languages/lisp"), + livecodeserver: () => import("highlight.js/lib/languages/livecodeserver"), + livescript: () => import("highlight.js/lib/languages/livescript"), + llvm: () => import("highlight.js/lib/languages/llvm"), + lsl: () => import("highlight.js/lib/languages/lsl"), + lua: () => import("highlight.js/lib/languages/lua"), + makefile: () => import("highlight.js/lib/languages/makefile"), + mathematica: () => import("highlight.js/lib/languages/mathematica"), + matlab: () => import("highlight.js/lib/languages/matlab"), + maxima: () => import("highlight.js/lib/languages/maxima"), + mel: () => import("highlight.js/lib/languages/mel"), + mercury: () => import("highlight.js/lib/languages/mercury"), + mipsasm: () => import("highlight.js/lib/languages/mipsasm"), + mizar: () => import("highlight.js/lib/languages/mizar"), + perl: () => import("highlight.js/lib/languages/perl"), + mojolicious: () => import("highlight.js/lib/languages/mojolicious"), + monkey: () => import("highlight.js/lib/languages/monkey"), + moonscript: () => import("highlight.js/lib/languages/moonscript"), + n1ql: () => import("highlight.js/lib/languages/n1ql"), + nestedtext: () => import("highlight.js/lib/languages/nestedtext"), + nginx: () => import("highlight.js/lib/languages/nginx"), + nim: () => import("highlight.js/lib/languages/nim"), + nix: () => import("highlight.js/lib/languages/nix"), "node-repl": () => import("highlight.js/lib/languages/node-repl"), - "nsis": () => import("highlight.js/lib/languages/nsis"), - "objectivec": () => import("highlight.js/lib/languages/objectivec"), - "ocaml": () => import("highlight.js/lib/languages/ocaml"), - "openscad": () => import("highlight.js/lib/languages/openscad"), - "oxygene": () => import("highlight.js/lib/languages/oxygene"), - "parser3": () => import("highlight.js/lib/languages/parser3"), - "pf": () => import("highlight.js/lib/languages/pf"), - "pgsql": () => import("highlight.js/lib/languages/pgsql"), - "php": () => import("highlight.js/lib/languages/php"), + nsis: () => import("highlight.js/lib/languages/nsis"), + objectivec: () => import("highlight.js/lib/languages/objectivec"), + ocaml: () => import("highlight.js/lib/languages/ocaml"), + openscad: () => import("highlight.js/lib/languages/openscad"), + oxygene: () => import("highlight.js/lib/languages/oxygene"), + parser3: () => import("highlight.js/lib/languages/parser3"), + pf: () => import("highlight.js/lib/languages/pf"), + pgsql: () => import("highlight.js/lib/languages/pgsql"), + php: () => import("highlight.js/lib/languages/php"), "php-template": () => import("highlight.js/lib/languages/php-template"), - "plaintext": () => import("highlight.js/lib/languages/plaintext"), - "pony": () => import("highlight.js/lib/languages/pony"), - "powershell": () => import("highlight.js/lib/languages/powershell"), - "processing": () => import("highlight.js/lib/languages/processing"), - "profile": () => import("highlight.js/lib/languages/profile"), - "prolog": () => import("highlight.js/lib/languages/prolog"), - "properties": () => import("highlight.js/lib/languages/properties"), - "protobuf": () => import("highlight.js/lib/languages/protobuf"), - "puppet": () => import("highlight.js/lib/languages/puppet"), - "purebasic": () => import("highlight.js/lib/languages/purebasic"), - "python": () => import("highlight.js/lib/languages/python"), + plaintext: () => import("highlight.js/lib/languages/plaintext"), + pony: () => import("highlight.js/lib/languages/pony"), + powershell: () => import("highlight.js/lib/languages/powershell"), + processing: () => import("highlight.js/lib/languages/processing"), + profile: () => import("highlight.js/lib/languages/profile"), + prolog: () => import("highlight.js/lib/languages/prolog"), + properties: () => import("highlight.js/lib/languages/properties"), + protobuf: () => import("highlight.js/lib/languages/protobuf"), + puppet: () => import("highlight.js/lib/languages/puppet"), + purebasic: () => import("highlight.js/lib/languages/purebasic"), + python: () => import("highlight.js/lib/languages/python"), "python-repl": () => import("highlight.js/lib/languages/python-repl"), - "q": () => import("highlight.js/lib/languages/q"), - "qml": () => import("highlight.js/lib/languages/qml"), - "r": () => import("highlight.js/lib/languages/r"), - "reasonml": () => import("highlight.js/lib/languages/reasonml"), - "rib": () => import("highlight.js/lib/languages/rib"), - "roboconf": () => import("highlight.js/lib/languages/roboconf"), - "routeros": () => import("highlight.js/lib/languages/routeros"), - "rsl": () => import("highlight.js/lib/languages/rsl"), - "ruleslanguage": () => import("highlight.js/lib/languages/ruleslanguage"), - "rust": () => import("highlight.js/lib/languages/rust"), - "sas": () => import("highlight.js/lib/languages/sas"), - "scala": () => import("highlight.js/lib/languages/scala"), - "scheme": () => import("highlight.js/lib/languages/scheme"), - "scilab": () => import("highlight.js/lib/languages/scilab"), - "scss": () => import("highlight.js/lib/languages/scss"), - "shell": () => import("highlight.js/lib/languages/shell"), - "smali": () => import("highlight.js/lib/languages/smali"), - "smalltalk": () => import("highlight.js/lib/languages/smalltalk"), - "sml": () => import("highlight.js/lib/languages/sml"), - "sqf": () => import("highlight.js/lib/languages/sqf"), - "sql": () => import("highlight.js/lib/languages/sql"), - "stan": () => import("highlight.js/lib/languages/stan"), - "stata": () => import("highlight.js/lib/languages/stata"), - "step21": () => import("highlight.js/lib/languages/step21"), - "stylus": () => import("highlight.js/lib/languages/stylus"), - "subunit": () => import("highlight.js/lib/languages/subunit"), - "swift": () => import("highlight.js/lib/languages/swift"), - "taggerscript": () => import("highlight.js/lib/languages/taggerscript"), - "yaml": () => import("highlight.js/lib/languages/yaml"), - "tap": () => import("highlight.js/lib/languages/tap"), - "tcl": () => import("highlight.js/lib/languages/tcl"), - "thrift": () => import("highlight.js/lib/languages/thrift"), - "tp": () => import("highlight.js/lib/languages/tp"), - "twig": () => import("highlight.js/lib/languages/twig"), - "typescript": () => import("highlight.js/lib/languages/typescript"), - "vala": () => import("highlight.js/lib/languages/vala"), - "vbnet": () => import("highlight.js/lib/languages/vbnet"), - "vbscript": () => import("highlight.js/lib/languages/vbscript"), + q: () => import("highlight.js/lib/languages/q"), + qml: () => import("highlight.js/lib/languages/qml"), + r: () => import("highlight.js/lib/languages/r"), + reasonml: () => import("highlight.js/lib/languages/reasonml"), + rib: () => import("highlight.js/lib/languages/rib"), + roboconf: () => import("highlight.js/lib/languages/roboconf"), + routeros: () => import("highlight.js/lib/languages/routeros"), + rsl: () => import("highlight.js/lib/languages/rsl"), + ruleslanguage: () => import("highlight.js/lib/languages/ruleslanguage"), + rust: () => import("highlight.js/lib/languages/rust"), + sas: () => import("highlight.js/lib/languages/sas"), + scala: () => import("highlight.js/lib/languages/scala"), + scheme: () => import("highlight.js/lib/languages/scheme"), + scilab: () => import("highlight.js/lib/languages/scilab"), + scss: () => import("highlight.js/lib/languages/scss"), + shell: () => import("highlight.js/lib/languages/shell"), + smali: () => import("highlight.js/lib/languages/smali"), + smalltalk: () => import("highlight.js/lib/languages/smalltalk"), + sml: () => import("highlight.js/lib/languages/sml"), + sqf: () => import("highlight.js/lib/languages/sqf"), + sql: () => import("highlight.js/lib/languages/sql"), + stan: () => import("highlight.js/lib/languages/stan"), + stata: () => import("highlight.js/lib/languages/stata"), + step21: () => import("highlight.js/lib/languages/step21"), + stylus: () => import("highlight.js/lib/languages/stylus"), + subunit: () => import("highlight.js/lib/languages/subunit"), + swift: () => import("highlight.js/lib/languages/swift"), + taggerscript: () => import("highlight.js/lib/languages/taggerscript"), + yaml: () => import("highlight.js/lib/languages/yaml"), + tap: () => import("highlight.js/lib/languages/tap"), + tcl: () => import("highlight.js/lib/languages/tcl"), + thrift: () => import("highlight.js/lib/languages/thrift"), + tp: () => import("highlight.js/lib/languages/tp"), + twig: () => import("highlight.js/lib/languages/twig"), + typescript: () => import("highlight.js/lib/languages/typescript"), + vala: () => import("highlight.js/lib/languages/vala"), + vbnet: () => import("highlight.js/lib/languages/vbnet"), + vbscript: () => import("highlight.js/lib/languages/vbscript"), "vbscript-html": () => import("highlight.js/lib/languages/vbscript-html"), - "verilog": () => import("highlight.js/lib/languages/verilog"), - "vhdl": () => import("highlight.js/lib/languages/vhdl"), - "vim": () => import("highlight.js/lib/languages/vim"), - "wasm": () => import("highlight.js/lib/languages/wasm"), - "wren": () => import("highlight.js/lib/languages/wren"), - "x86asm": () => import("highlight.js/lib/languages/x86asm"), - "xl": () => import("highlight.js/lib/languages/xl"), - "xquery": () => import("highlight.js/lib/languages/xquery"), - "zephir": () => import("highlight.js/lib/languages/zephir"), -} + verilog: () => import("highlight.js/lib/languages/verilog"), + vhdl: () => import("highlight.js/lib/languages/vhdl"), + vim: () => import("highlight.js/lib/languages/vim"), + wasm: () => import("highlight.js/lib/languages/wasm"), + wren: () => import("highlight.js/lib/languages/wren"), + x86asm: () => import("highlight.js/lib/languages/x86asm"), + xl: () => import("highlight.js/lib/languages/xl"), + xquery: () => import("highlight.js/lib/languages/xquery"), + zephir: () => import("highlight.js/lib/languages/zephir"), +}; // hljs.listLanguages().map(l => ([l, hljs.getLanguage(l).aliases])).filter(([, b]) => b).map(([n, a]) => a.map(al => ([al, n]))).flat().map(([a, n]) => `"${a}": languages["${n}"]`).join(",\n") -const aliases: Record = { - "as": languages["actionscript"], - "asc": languages["angelscript"], - "apacheconf": languages["apache"], - "osascript": languages["applescript"], - "ino": languages["arduino"], - "arm": languages["armasm"], - "html": languages["xml"], - "xhtml": languages["xml"], - "rss": languages["xml"], - "atom": languages["xml"], - "xjb": languages["xml"], - "xsd": languages["xml"], - "xsl": languages["xml"], - "plist": languages["xml"], - "wsf": languages["xml"], - "svg": languages["xml"], - "adoc": languages["asciidoc"], - "ahk": languages["autohotkey"], +const aliases: Record = { + as: languages["actionscript"], + asc: languages["angelscript"], + apacheconf: languages["apache"], + osascript: languages["applescript"], + ino: languages["arduino"], + arm: languages["armasm"], + html: languages["xml"], + xhtml: languages["xml"], + rss: languages["xml"], + atom: languages["xml"], + xjb: languages["xml"], + xsd: languages["xml"], + xsl: languages["xml"], + plist: languages["xml"], + wsf: languages["xml"], + svg: languages["xml"], + adoc: languages["asciidoc"], + ahk: languages["autohotkey"], "x++": languages["axapta"], - "sh": languages["bash"], - "bf": languages["brainfuck"], - "h": languages["c"], - "capnp": languages["capnproto"], - "icl": languages["clean"], - "dcl": languages["clean"], - "clj": languages["clojure"], - "edn": languages["clojure"], + sh: languages["bash"], + bf: languages["brainfuck"], + h: languages["c"], + capnp: languages["capnproto"], + icl: languages["clean"], + dcl: languages["clean"], + clj: languages["clojure"], + edn: languages["clojure"], "cmake.in": languages["cmake"], - "coffee": languages["coffeescript"], - "cson": languages["coffeescript"], - "iced": languages["coffeescript"], - "cls": languages["cos"], - "cc": languages["cpp"], + coffee: languages["coffeescript"], + cson: languages["coffeescript"], + iced: languages["coffeescript"], + cls: languages["cos"], + cc: languages["cpp"], "c++": languages["cpp"], "h++": languages["cpp"], - "hpp": languages["cpp"], - "hh": languages["cpp"], - "hxx": languages["cpp"], - "cxx": languages["cpp"], - "crm": languages["crmsh"], - "pcmk": languages["crmsh"], - "cr": languages["crystal"], - "cs": languages["csharp"], + hpp: languages["cpp"], + hh: languages["cpp"], + hxx: languages["cpp"], + cxx: languages["cpp"], + crm: languages["crmsh"], + pcmk: languages["crmsh"], + cr: languages["crystal"], + cs: languages["csharp"], "c#": languages["csharp"], - "md": languages["markdown"], - "mkdown": languages["markdown"], - "mkd": languages["markdown"], - "dpr": languages["delphi"], - "dfm": languages["delphi"], - "pas": languages["delphi"], - "pascal": languages["delphi"], - "patch": languages["diff"], - "jinja": languages["django"], - "bind": languages["dns"], - "zone": languages["dns"], - "docker": languages["dockerfile"], - "bat": languages["dos"], - "cmd": languages["dos"], - "dst": languages["dust"], - "ex": languages["elixir"], - "exs": languages["elixir"], - "rb": languages["ruby"], - "gemspec": languages["ruby"], - "podspec": languages["ruby"], - "thor": languages["ruby"], - "irb": languages["ruby"], - "erl": languages["erlang"], - "xlsx": languages["excel"], - "xls": languages["excel"], - "f90": languages["fortran"], - "f95": languages["fortran"], - "fs": languages["fsharp"], + md: languages["markdown"], + mkdown: languages["markdown"], + mkd: languages["markdown"], + dpr: languages["delphi"], + dfm: languages["delphi"], + pas: languages["delphi"], + pascal: languages["delphi"], + patch: languages["diff"], + jinja: languages["django"], + bind: languages["dns"], + zone: languages["dns"], + docker: languages["dockerfile"], + bat: languages["dos"], + cmd: languages["dos"], + dst: languages["dust"], + ex: languages["elixir"], + exs: languages["elixir"], + rb: languages["ruby"], + gemspec: languages["ruby"], + podspec: languages["ruby"], + thor: languages["ruby"], + irb: languages["ruby"], + erl: languages["erlang"], + xlsx: languages["excel"], + xls: languages["excel"], + f90: languages["fortran"], + f95: languages["fortran"], + fs: languages["fsharp"], "f#": languages["fsharp"], - "gms": languages["gams"], - "gss": languages["gauss"], - "nc": languages["gcode"], - "feature": languages["gherkin"], - "golang": languages["go"], - "gql": languages["graphql"], - "hbs": languages["handlebars"], + gms: languages["gams"], + gss: languages["gauss"], + nc: languages["gcode"], + feature: languages["gherkin"], + golang: languages["go"], + gql: languages["graphql"], + hbs: languages["handlebars"], "html.hbs": languages["handlebars"], "html.handlebars": languages["handlebars"], - "htmlbars": languages["handlebars"], - "hs": languages["haskell"], - "hx": languages["haxe"], - "https": languages["http"], - "hylang": languages["hy"], - "i7": languages["inform7"], - "toml": languages["ini"], - "jsp": languages["java"], - "js": languages["javascript"], - "jsx": languages["javascript"], - "mjs": languages["javascript"], - "cjs": languages["javascript"], + htmlbars: languages["handlebars"], + hs: languages["haskell"], + hx: languages["haxe"], + https: languages["http"], + hylang: languages["hy"], + i7: languages["inform7"], + toml: languages["ini"], + jsp: languages["java"], + js: languages["javascript"], + jsx: languages["javascript"], + mjs: languages["javascript"], + cjs: languages["javascript"], "wildfly-cli": languages["jboss-cli"], - "jldoctest": languages["julia-repl"], - "kt": languages["kotlin"], - "kts": languages["kotlin"], - "ls": languages["lasso"], - "lassoscript": languages["lasso"], - "tex": languages["latex"], - "mk": languages["makefile"], - "mak": languages["makefile"], - "make": languages["makefile"], - "mma": languages["mathematica"], - "wl": languages["mathematica"], - "m": languages["mercury"], - "moo": languages["mercury"], - "mips": languages["mipsasm"], - "pl": languages["perl"], - "pm": languages["perl"], - "moon": languages["moonscript"], - "nt": languages["nestedtext"], - "nginxconf": languages["nginx"], - "nixos": languages["nix"], - "mm": languages["objectivec"], - "objc": languages["objectivec"], + jldoctest: languages["julia-repl"], + kt: languages["kotlin"], + kts: languages["kotlin"], + ls: languages["lasso"], + lassoscript: languages["lasso"], + tex: languages["latex"], + mk: languages["makefile"], + mak: languages["makefile"], + make: languages["makefile"], + mma: languages["mathematica"], + wl: languages["mathematica"], + m: languages["mercury"], + moo: languages["mercury"], + mips: languages["mipsasm"], + pl: languages["perl"], + pm: languages["perl"], + moon: languages["moonscript"], + nt: languages["nestedtext"], + nginxconf: languages["nginx"], + nixos: languages["nix"], + mm: languages["objectivec"], + objc: languages["objectivec"], "obj-c": languages["objectivec"], "obj-c++": languages["objectivec"], "objective-c++": languages["objectivec"], - "ml": languages["ocaml"], - "scad": languages["openscad"], + ml: languages["ocaml"], + scad: languages["openscad"], "pf.conf": languages["pf"], - "postgres": languages["pgsql"], - "postgresql": languages["pgsql"], - "text": languages["plaintext"], - "txt": languages["plaintext"], - "pwsh": languages["powershell"], - "ps": languages["powershell"], - "ps1": languages["powershell"], - "pde": languages["processing"], - "pp": languages["puppet"], - "pb": languages["purebasic"], - "pbi": languages["purebasic"], - "py": languages["python"], - "gyp": languages["python"], - "ipython": languages["python"], - "pycon": languages["python-repl"], - "k": languages["q"], - "kdb": languages["q"], - "qt": languages["qml"], - "re": languages["reasonml"], - "graph": languages["roboconf"], - "instances": languages["roboconf"], - "mikrotik": languages["routeros"], - "rs": languages["rust"], - "scm": languages["scheme"], - "sci": languages["scilab"], - "console": languages["shell"], - "shellsession": languages["shell"], - "st": languages["smalltalk"], - "stanfuncs": languages["stan"], - "do": languages["stata"], - "ado": languages["stata"], - "p21": languages["step21"], - "step": languages["step21"], - "stp": languages["step21"], - "styl": languages["stylus"], - "yml": languages["yaml"], - "tk": languages["tcl"], - "craftcms": languages["twig"], - "ts": languages["typescript"], - "tsx": languages["typescript"], - "vb": languages["vbnet"], - "vbs": languages["vbscript"], - "v": languages["verilog"], - "sv": languages["verilog"], - "svh": languages["verilog"], - "tao": languages["xl"], - "xpath": languages["xquery"], - "xq": languages["xquery"], - "zep": languages["zephir"] -} + postgres: languages["pgsql"], + postgresql: languages["pgsql"], + text: languages["plaintext"], + txt: languages["plaintext"], + pwsh: languages["powershell"], + ps: languages["powershell"], + ps1: languages["powershell"], + pde: languages["processing"], + pp: languages["puppet"], + pb: languages["purebasic"], + pbi: languages["purebasic"], + py: languages["python"], + gyp: languages["python"], + ipython: languages["python"], + pycon: languages["python-repl"], + k: languages["q"], + kdb: languages["q"], + qt: languages["qml"], + re: languages["reasonml"], + graph: languages["roboconf"], + instances: languages["roboconf"], + mikrotik: languages["routeros"], + rs: languages["rust"], + scm: languages["scheme"], + sci: languages["scilab"], + console: languages["shell"], + shellsession: languages["shell"], + st: languages["smalltalk"], + stanfuncs: languages["stan"], + do: languages["stata"], + ado: languages["stata"], + p21: languages["step21"], + step: languages["step21"], + stp: languages["step21"], + styl: languages["stylus"], + yml: languages["yaml"], + tk: languages["tcl"], + craftcms: languages["twig"], + ts: languages["typescript"], + tsx: languages["typescript"], + vb: languages["vbnet"], + vbs: languages["vbscript"], + v: languages["verilog"], + sv: languages["verilog"], + svh: languages["verilog"], + tao: languages["xl"], + xpath: languages["xquery"], + xq: languages["xquery"], + zep: languages["zephir"], +}; interface ParseMarkdownOptions { parseTimestamps?: boolean; @@ -384,27 +390,41 @@ const parseMarkdown = async (raw: string, opts?: ParseMarkdownOptions) => { } const markdownUnparsed = toHTML(raw, { embed: opts?.embed }); - const markdownUnparsedDom = new DOMParser().parseFromString(markdownUnparsed, "text/html"); + const markdownUnparsedDom = new DOMParser().parseFromString( + markdownUnparsed, + "text/html" + ); - const codeBlocks = markdownUnparsedDom.querySelectorAll("pre code[data-code]"); + const codeBlocks = markdownUnparsedDom.querySelectorAll( + "pre code[data-code]" + ); const promies = Array.from(codeBlocks).map(async (codeBlock) => { - let code: string = window.atob(codeBlock.getAttribute("data-code")); + let code: string = Base64.decode( + codeBlock.getAttribute("data-code") ?? "" + ); codeBlock.classList.add("hljs"); - const specifiedLanguage = codeBlock.getAttribute("data-code-language"); - const languageImportFn = languages[specifiedLanguage] ?? aliases[specifiedLanguage]; + const specifiedLanguage = + codeBlock.getAttribute("data-code-language") ?? "plaintext"; + const languageImportFn = + languages[specifiedLanguage] ?? aliases[specifiedLanguage]; if (languageImportFn) { if (!hljs.getLanguage(specifiedLanguage)) { const languageImport = await languageImportFn(); - hljs.registerLanguage(specifiedLanguage, languageImport.default); + hljs.registerLanguage( + specifiedLanguage, + languageImport.default + ); } codeBlock.classList.add(specifiedLanguage); - codeBlock.innerHTML = hljs.highlight(code, {language: specifiedLanguage}).value; + codeBlock.innerHTML = hljs.highlight(code, { + language: specifiedLanguage, + }).value; } else { codeBlock.textContent = code; } @@ -416,6 +436,6 @@ const parseMarkdown = async (raw: string, opts?: ParseMarkdownOptions) => { await Promise.all(promies); return markdownUnparsedDom.body.innerHTML; -} +}; -export default parseMarkdown; \ No newline at end of file +export default parseMarkdown; diff --git a/dashboard/src/api/types.ts b/dashboard/src/api/types.ts index 04a92473..8a12a984 100644 --- a/dashboard/src/api/types.ts +++ b/dashboard/src/api/types.ts @@ -6,7 +6,8 @@ export interface SystemPrivacy { group_list_privacy?: string, pronoun_privacy?: string, avatar_privacy?: string, - name_privacy?: string + name_privacy?: string, + banner_privacy?: string, } export interface System { @@ -43,7 +44,8 @@ export interface MemberPrivacy { pronoun_privacy?: string, avatar_privacy?: string, metadata_privacy?: string, - proxy_privacy?: string + proxy_privacy?: string, + banner_privacy?: string, } interface proxytag { @@ -77,7 +79,8 @@ export interface GroupPrivacy { list_privacy?: string, visibility?: string, name_privacy?: string, - metadata_privacy?: string + metadata_privacy?: string, + banner_privacy?: string, } export interface Group { 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/Body.svelte b/dashboard/src/components/group/Body.svelte index b7404f33..17be205c 100644 --- a/dashboard/src/components/group/Body.svelte +++ b/dashboard/src/components/group/Body.svelte @@ -121,7 +121,11 @@
Description:
+ {#if group.description} + {:else} + (no description) + {/if}
{#if (group.banner && ((settings && settings.appearance.banner_bottom) || !settings))} group banner diff --git a/dashboard/src/components/group/Edit.svelte b/dashboard/src/components/group/Edit.svelte index bad47ab2..d51ec2eb 100644 --- a/dashboard/src/components/group/Edit.svelte +++ b/dashboard/src/components/group/Edit.svelte @@ -25,6 +25,11 @@ 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)) { err.push(`"${data.color}" is not a valid color, the color must be a 6-digit hex code. (example: #ff0000)`); } else if (data.color) { @@ -33,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/group/NewGroup.svelte b/dashboard/src/components/group/NewGroup.svelte index 7905c1cf..e952817c 100644 --- a/dashboard/src/components/group/NewGroup.svelte +++ b/dashboard/src/components/group/NewGroup.svelte @@ -21,7 +21,8 @@ list_privacy: "public", icon_privacy: "public", name_privacy: "public", - visibility: "public" + visibility: "public", + banner_privacy: "public", } } @@ -162,6 +163,13 @@ + + + + + + +
{/if} diff --git a/dashboard/src/components/group/Privacy.svelte b/dashboard/src/components/group/Privacy.svelte index fdaf0db6..ed81031c 100644 --- a/dashboard/src/components/group/Privacy.svelte +++ b/dashboard/src/components/group/Privacy.svelte @@ -32,7 +32,8 @@ icon_privacy: "Icon", list_privacy: "Member list", metadata_privacy: "Metadata", - visibility: "Visbility", + visibility: "Visibility", + banner_privacy: "Banner", }; async function submit() { diff --git a/dashboard/src/components/list/CardView.svelte b/dashboard/src/components/list/CardView.svelte index 9c568d7b..e3c34f5d 100644 --- a/dashboard/src/components/list/CardView.svelte +++ b/dashboard/src/components/list/CardView.svelte @@ -49,7 +49,7 @@ {#if pageOptions.type === "member"} - {#each currentList as item (item.uuid)} + {#each currentList as item, index (pageOptions.randomized ? item.uuid + '-' + index : item.uuid)}
{/each} {:else if pageOptions.type === "group"} - {#each currentList as item (item.uuid)} + {#each currentList as item, index (pageOptions.randomized ? item.uuid + '-' + index : item.uuid)}