diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index 166d4c8..b192a3c 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -2,7 +2,7 @@ name: Build and Test on: push: - branches: [ "main" ] + branches: [ "main", "dev" ] pull_request: branches: [ "main" ] @@ -13,7 +13,7 @@ jobs: rust: runs-on: ubuntu-latest - + steps: - uses: actions/checkout@v3 - name: Clone spacebar server @@ -21,7 +21,7 @@ jobs: git clone https://github.com/bitfl0wer/server.git - uses: actions/setup-node@v3 with: - node-version: 16 + node-version: 18 cache: 'npm' cache-dependency-path: server/package-lock.json - name: Prepare and start Spacebar server @@ -31,7 +31,17 @@ jobs: npm run start & working-directory: ./server - uses: Swatinem/rust-cache@v2 - - name: Build - run: cargo build --verbose - - name: Run tests - run: cargo test --verbose + with: + cache-all-crates: "true" + - name: Build, Test and Publish Coverage + run: | + if [ -n "${{ secrets.COVERALLS_REPO_TOKEN }}" ]; then + curl -L --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/cargo-bins/cargo-binstall/main/install-from-binstall-release.sh | bash + cargo binstall --no-confirm cargo-tarpaulin --force + cargo tarpaulin --all-features --avoid-cfg-tarpaulin --tests --verbose --skip-clean --coveralls ${{ secrets.COVERALLS_REPO_TOKEN }} --timeout 120 + else + echo "Code Coverage step is skipped on forks!" + cargo build --verbose --all-features + cargo test --verbose --all-features + fi + diff --git a/.github/workflows/rust-clippy.yml b/.github/workflows/rust-clippy.yml index dbbba76..0c3840f 100644 --- a/.github/workflows/rust-clippy.yml +++ b/.github/workflows/rust-clippy.yml @@ -11,7 +11,7 @@ name: rust-clippy analyze on: push: - branches: [ "main", "preserve/*" ] + branches: [ "main", "preserve/*", "dev" ] pull_request: # The branches below must be a subset of the branches above branches: [ "main" ] diff --git a/.gitignore b/.gitignore index d3170e2..1bb4c89 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ # Generated by Cargo # will have compiled files and executables -/target/ +/**/target/ # These are backup files generated by rustfmt **/*.rs.bk diff --git a/Cargo.lock b/Cargo.lock index d193cf4..df89f42 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 3 [[package]] name = "addr2line" -version = "0.20.0" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4fa78e18c64fce05e902adecd7a5eed15a5e0a3439f7b0e169f0252214865e3" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" dependencies = [ "gimli", ] @@ -19,31 +19,21 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "ahash" -version = "0.7.6" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" dependencies = [ + "cfg-if", "getrandom", "once_cell", "version_check", ] -[[package]] -name = "ahash" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" -dependencies = [ - "cfg-if", - "once_cell", - "version_check", -] - [[package]] name = "aho-corasick" -version = "1.0.2" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41" +checksum = "0c378d78423fdad8089616f827526ee33c19f2fddbd5de1629152c9593ba4783" dependencies = [ "memchr", ] @@ -71,20 +61,20 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.71" +version = "0.1.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a564d521dd56509c4c47480d00b80ee55f7e385ae48db5744c67ad50c92d2ebf" +checksum = "bc00ceb34980c03614e35a3a4e218276a0a824e911d07651cd0d858a51e8c0f0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.26", + "syn 2.0.31", ] [[package]] name = "atoi" -version = "1.0.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7c57d12312ff59c811c0643f4d80830505833c9ffaebd193d819392b265be8e" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" dependencies = [ "num-traits", ] @@ -97,9 +87,9 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "backtrace" -version = "0.3.68" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4319208da049c43661739c5fade2ba182f09d1dc2299b32298d3a31692b17e12" +checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" dependencies = [ "addr2line", "cc", @@ -118,9 +108,9 @@ checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" [[package]] name = "base64" -version = "0.21.2" +version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d" +checksum = "414dcefbc63d77c526a76b3afcf6fbb9b5e2791c19c3aa2297733208750c6e53" [[package]] name = "base64ct" @@ -136,9 +126,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.3.3" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "630be753d4e58660abd17930c71b647fe46c27ea6b63cc59e1e3851406972e42" +checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" dependencies = [ "serde", ] @@ -172,9 +162,12 @@ checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" [[package]] name = "cc" -version = "1.0.79" +version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +dependencies = [ + "libc", +] [[package]] name = "cfg-if" @@ -184,11 +177,11 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chorus" -version = "0.1.0" +version = "0.9.0" dependencies = [ "async-trait", - "base64 0.21.2", - "bitflags 2.3.3", + "base64 0.21.3", + "bitflags 2.4.0", "chorus-macros", "chrono", "custom_error", @@ -218,17 +211,20 @@ dependencies = [ [[package]] name = "chorus-macros" -version = "0.1.0" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a81545a60b926f815517dadbbd40cd502294ae2baea25fa8194d854d607512b0" dependencies = [ + "async-trait", "quote", - "syn 2.0.26", + "syn 2.0.31", ] [[package]] name = "chrono" -version = "0.4.26" +version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec837a71355b28f6556dbd569b37b3f363091c0bd4b2e735674521b4c5fd9bc5" +checksum = "95ed24df0632f708f5f6d8082675bef2596f7084dee3dd55f632290bf35bfe0f" dependencies = [ "android-tzdata", "iana-time-zone", @@ -237,7 +233,7 @@ dependencies = [ "serde", "time 0.1.45", "wasm-bindgen", - "winapi", + "windows-targets", ] [[package]] @@ -251,9 +247,9 @@ dependencies = [ [[package]] name = "const-oid" -version = "0.7.1" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4c78c047431fee22c1a7bb92e00ad095a02a983affe4d8a72e2a2c62c1b94f3" +checksum = "28c122c3980598d243d63d9a704629a2d748d101f278052ff068be5a4423ab6f" [[package]] name = "core-foundation" @@ -314,16 +310,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "crypto-bigint" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03c6a1d5fa1de37e071642dfa44ec552ca5b299adb128fab16138e24b548fd21" -dependencies = [ - "generic-array", - "subtle", -] - [[package]] name = "crypto-common" version = "0.1.6" @@ -361,7 +347,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.26", + "syn 2.0.31", ] [[package]] @@ -372,7 +358,7 @@ checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5" dependencies = [ "darling_core", "quote", - "syn 2.0.26", + "syn 2.0.31", ] [[package]] @@ -383,13 +369,22 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" [[package]] name = "der" -version = "0.5.1" +version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6919815d73839e7ad218de758883aae3a257ba6759ce7a9992501efbb53d705c" +checksum = "fffa369a668c8af7dbf8b5e56c9f744fbd399949ed171606040001947de40b1c" dependencies = [ "const-oid", - "crypto-bigint", "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "deranged" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2696e8a945f658fd14dc3b87242e6b80cd0f36ff04ea560fa39082368847946" +dependencies = [ + "serde", ] [[package]] @@ -399,7 +394,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", + "const-oid", "crypto-common", + "subtle", ] [[package]] @@ -410,15 +407,18 @@ checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" [[package]] name = "either" -version = "1.8.1" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" +dependencies = [ + "serde", +] [[package]] name = "encoding_rs" -version = "0.8.32" +version = "0.8.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394" +checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" dependencies = [ "cfg-if", ] @@ -441,9 +441,9 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.1" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" +checksum = "136526188508e25c6fef639d7927dfb3e0e3084488bf202267829cf7fc23dbdd" dependencies = [ "errno-dragonfly", "libc", @@ -460,6 +460,17 @@ dependencies = [ "libc", ] +[[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", +] + [[package]] name = "event-listener" version = "2.5.3" @@ -468,12 +479,9 @@ checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" [[package]] name = "fastrand" -version = "1.9.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" -dependencies = [ - "instant", -] +checksum = "6999dc1837253364c2ebb0704ba97994bd874e8f195d665c50b7548f6ea92764" [[package]] name = "flume" @@ -552,15 +560,21 @@ dependencies = [ [[package]] name = "futures-intrusive" -version = "0.4.2" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a604f7a68fbf8103337523b1fadc8ade7361ee3f112f7c680ad179651616aed5" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" dependencies = [ "futures-core", "lock_api", - "parking_lot 0.11.2", + "parking_lot", ] +[[package]] +name = "futures-io" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" + [[package]] name = "futures-macro" version = "0.3.28" @@ -569,7 +583,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn 2.0.26", + "syn 2.0.31", ] [[package]] @@ -591,9 +605,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" dependencies = [ "futures-core", + "futures-io", "futures-macro", "futures-sink", "futures-task", + "memchr", "pin-project-lite", "pin-utils", "slab", @@ -631,15 +647,15 @@ dependencies = [ [[package]] name = "gimli" -version = "0.27.3" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e" +checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" [[package]] name = "h2" -version = "0.3.20" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97ec8491ebaf99c8eaa73058b045fe58073cd6be7f596ac993ced0b0a0c01049" +checksum = "91fc23aa11be92976ef4729127f1a74adf36d8436f7816b185d18df956790833" dependencies = [ "bytes", "fnv", @@ -666,27 +682,26 @@ version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" dependencies = [ - "ahash 0.8.3", + "ahash", "allocator-api2", ] [[package]] name = "hashlink" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "312f66718a2d7789ffef4f4b7b213138ed9f1eb3aa1d0d82fc99f88fb3ffd26f" +checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" dependencies = [ "hashbrown 0.14.0", ] [[package]] name = "headers" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3e372db8e5c0d213e0cd0b9be18be2aca3d44cf2fe30a9d46a65581cd454584" +checksum = "06683b93020a07e3dbcf5f8c0f6d40080d725bea7936fc01ad345c01b97dc270" dependencies = [ - "base64 0.13.1", - "bitflags 1.3.2", + "base64 0.21.3", "bytes", "headers-core", "http", @@ -725,6 +740,33 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hkdf" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791a029f6b9fc27657f6f188ec6e5e43f6911f6f878e0dc5501396e09809d437" +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.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5444c27eef6923071f7ebcc33e3444508466a76f7a2b93da00ed6e19f30c1ddb" +dependencies = [ + "windows-sys", +] + [[package]] name = "hostname" version = "0.3.1" @@ -766,9 +808,9 @@ checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" [[package]] name = "httpdate" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hyper" @@ -787,7 +829,7 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", - "socket2", + "socket2 0.4.9", "tokio", "tower-service", "tracing", @@ -865,26 +907,7 @@ checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" dependencies = [ "equivalent", "hashbrown 0.14.0", -] - -[[package]] -name = "instant" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "io-lifetimes" -version = "1.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" -dependencies = [ - "hermit-abi", - "libc", - "windows-sys", + "serde", ] [[package]] @@ -895,9 +918,12 @@ checksum = "28b29a3cd74f0f4598934efe3aeba42bae0eb4680554128851ebbecb02af14e6" [[package]] name = "ipnetwork" -version = "0.19.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f84f1612606f3753f205a4e9a2efd6fe5b4c573a6269b2cc6c3003d44a0d127" +checksum = "bf466541e9d546596ee94f9f69590f89473455f88372423e0008fc1a7daf100e" +dependencies = [ + "serde", +] [[package]] name = "itertools" @@ -929,7 +955,7 @@ version = "8.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6971da4d9c3aa03c3d8f3ff0f4155b534aad021292003895a469716b2a230378" dependencies = [ - "base64 0.21.2", + "base64 0.21.3", "pem", "ring", "serde", @@ -960,9 +986,9 @@ checksum = "f7012b1bbb0719e1097c47611d3898568c546d597c2e74d66f6087edd5233ff4" [[package]] name = "libsqlite3-sys" -version = "0.24.2" +version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "898745e570c7d0453cc1fbc4a701eb6c662ed54e8fec8b7d14be137ebeeb9d14" +checksum = "afc22eff61b133b115c6e8c74e818c628d6d5e7a502afea6f64dee076dd94326" dependencies = [ "cc", "pkg-config", @@ -971,9 +997,9 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.3.8" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" +checksum = "57bcfdad1b858c2db7c38303a6d2ad4dfaf5eb53dfeb0910128b2c26d6158503" [[package]] name = "lock_api" @@ -987,9 +1013,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.19" +version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" [[package]] name = "match_cfg" @@ -998,10 +1024,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" [[package]] -name = "memchr" -version = "2.5.0" +name = "md-5" +version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +checksum = "6365506850d44bff6e2fbcb5176cf63650e48bd45ef2fe2665ae1570e0f4b9ca" +dependencies = [ + "digest", +] + +[[package]] +name = "memchr" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c" [[package]] name = "mime" @@ -1081,9 +1116,9 @@ dependencies = [ [[package]] name = "num-bigint" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f" +checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0" dependencies = [ "autocfg", "num-integer", @@ -1130,9 +1165,9 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" dependencies = [ "autocfg", "libm", @@ -1150,9 +1185,9 @@ dependencies = [ [[package]] name = "object" -version = "0.31.1" +version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bda667d9f2b5051b8833f59f3bf748b28ef54f850f4fcb389a252aa383866d1" +checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" dependencies = [ "memchr", ] @@ -1165,11 +1200,11 @@ checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" [[package]] name = "openssl" -version = "0.10.55" +version = "0.10.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "345df152bc43501c5eb9e4654ff05f794effb78d4efe3d53abc158baddc0703d" +checksum = "bac25ee399abb46215765b1cb35bc0212377e58a061560d8b29b024fd0430e7c" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.4.0", "cfg-if", "foreign-types", "libc", @@ -1186,7 +1221,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.26", + "syn 2.0.31", ] [[package]] @@ -1197,9 +1232,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.90" +version = "0.9.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "374533b0e45f3a7ced10fcaeccca020e66656bc03dac384f852e4e5a7a8104a6" +checksum = "db7e971c2c2bba161b2d2fdf37080177eff520b3bc044787c7f1f5f9e78d869b" dependencies = [ "cc", "libc", @@ -1207,17 +1242,6 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "parking_lot" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" -dependencies = [ - "instant", - "lock_api", - "parking_lot_core 0.8.6", -] - [[package]] name = "parking_lot" version = "0.12.1" @@ -1225,21 +1249,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" dependencies = [ "lock_api", - "parking_lot_core 0.9.8", -] - -[[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 0.2.16", - "smallvec", - "winapi", + "parking_lot_core", ] [[package]] @@ -1250,7 +1260,7 @@ checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.3.5", + "redox_syscall", "smallvec", "windows-targets", ] @@ -1272,9 +1282,9 @@ dependencies = [ [[package]] name = "pem-rfc7468" -version = "0.3.1" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01de5d978f34aa4b2296576379fcc416034702fd94117c56ffd8a1a767cefb30" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" dependencies = [ "base64ct", ] @@ -1287,29 +1297,29 @@ checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" [[package]] name = "pin-project" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "030ad2bc4db10a8944cb0d837f158bdfec4d4a4873ab701a95046770d11f8842" +checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec2e072ecce94ec471b13398d5402c188e76ac03cf74dd1a975161b23a3f6d9c" +checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" dependencies = [ "proc-macro2", "quote", - "syn 2.0.26", + "syn 2.0.31", ] [[package]] name = "pin-project-lite" -version = "0.2.10" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c40d25201921e5ff0c862a505c6557ea88568a4e3ace775ab55e93f2f4f9d57" +checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" [[package]] name = "pin-utils" @@ -1319,24 +1329,23 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkcs1" -version = "0.3.3" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a78f66c04ccc83dd4486fd46c33896f4e17b24a7a3a6400dedc48ed0ddd72320" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" dependencies = [ "der", "pkcs8", - "zeroize", + "spki", ] [[package]] name = "pkcs8" -version = "0.8.0" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cabda3fb821068a9a4fab19a683eac3af12edf0f34b94a8be53c4972b8149d0" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" dependencies = [ "der", "spki", - "zeroize", ] [[package]] @@ -1347,9 +1356,9 @@ checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" [[package]] name = "poem" -version = "1.3.56" +version = "1.3.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a56df40b79ebdccf7986b337f9b0e51ac55cd5e9d21fb20b6aa7c7d49741854" +checksum = "ebc7ae19f3e791ae8108b08801abb3708d64d3a16490c720e0b81040cae87b5d" dependencies = [ "async-trait", "bytes", @@ -1358,7 +1367,7 @@ dependencies = [ "http", "hyper", "mime", - "parking_lot 0.12.1", + "parking_lot", "percent-encoding", "pin-project-lite", "poem-derive", @@ -1376,14 +1385,14 @@ dependencies = [ [[package]] name = "poem-derive" -version = "1.3.56" +version = "1.3.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1701f977a2d650a03df42c053686ea0efdb83554f34c7b026b89383c0a1b7846" +checksum = "2550a0bce7273b278894ef3ccc5a6869e7031b6870042f3cc6826ed9faa980a6" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.31", ] [[package]] @@ -1413,9 +1422,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.31" +version = "1.0.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fe8a65d69dd0808184ebb5f836ab526bb259db23c657efa38711b1072ee47f0" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" dependencies = [ "proc-macro2", ] @@ -1450,15 +1459,6 @@ dependencies = [ "getrandom", ] -[[package]] -name = "redox_syscall" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" -dependencies = [ - "bitflags 1.3.2", -] - [[package]] name = "redox_syscall" version = "0.3.5" @@ -1470,9 +1470,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.9.1" +version = "1.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2eae68fc220f7cf2532e4494aded17545fce192d59cd996e0fe7887f4ceb575" +checksum = "697061221ea1b4a94a624f67d0ae2bfe4e22b8a17b6a192afb11046542cc8c47" dependencies = [ "aho-corasick", "memchr", @@ -1482,9 +1482,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.3.3" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39354c10dd07468c2e73926b23bb9c2caca74c5501e38a35da70406f1d923310" +checksum = "c2f401f4955220693b56f8ec66ee9c78abffd8d1c4f23dc41a23839eb88f0795" dependencies = [ "aho-corasick", "memchr", @@ -1493,17 +1493,17 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5ea92a5b6195c6ef2a0295ea818b312502c6fc94dde986c5553242e18fd4ce2" +checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" [[package]] name = "reqwest" -version = "0.11.18" +version = "0.11.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cde824a14b7c14f85caff81225f411faacc04a2013f41670f41443742b1c1c55" +checksum = "3e9ad3fe7488d7e34558a2033d45a0c90b72d97b4f80705666fea71472e2e6a1" dependencies = [ - "base64 0.21.2", + "base64 0.21.3", "bytes", "encoding_rs", "futures-core", @@ -1561,11 +1561,12 @@ dependencies = [ [[package]] name = "rsa" -version = "0.6.1" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cf22754c49613d2b3b119f0e5d46e34a2c628a937e3024b8762de4e7d8c710b" +checksum = "6ab43bb47d23c1a631b4b680199a45255dce26fa9ab2fa902581f624ff13e6a8" dependencies = [ "byteorder", + "const-oid", "digest", "num-bigint-dig", "num-integer", @@ -1574,7 +1575,8 @@ dependencies = [ "pkcs1", "pkcs8", "rand_core", - "smallvec", + "signature", + "spki", "subtle", "zeroize", ] @@ -1587,13 +1589,12 @@ checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" [[package]] name = "rustix" -version = "0.37.23" +version = "0.38.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d69718bf81c6127a49dc64e44a742e8bb9213c0ff8869a22c308f84c1d4ab06" +checksum = "c0c3dde1fc030af041adc40e79c0e7fbcf431dd24870053d187d7c66e4b87453" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.4.0", "errno", - "io-lifetimes", "libc", "linux-raw-sys", "windows-sys", @@ -1634,9 +1635,9 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "security-framework" -version = "2.9.1" +version = "2.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fc758eb7bffce5b308734e9b0c1468893cae9ff70ebf13e7090be8dcbcc83a8" +checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" dependencies = [ "bitflags 1.3.2", "core-foundation", @@ -1647,9 +1648,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.9.0" +version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f51d0c0d83bec45f16480d0ce0058397a69e48fcdc52d1dc8855fb68acbd31a7" +checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" dependencies = [ "core-foundation-sys", "libc", @@ -1657,9 +1658,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.171" +version = "1.0.188" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30e27d1e4fd7659406c492fd6cfaf2066ba8773de45ca75e855590f856dc34a9" +checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e" dependencies = [ "serde_derive", ] @@ -1677,20 +1678,20 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.171" +version = "1.0.188" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389894603bd18c46fa56231694f8d827779c0951a667087194cf9de94ed24682" +checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.26", + "syn 2.0.31", ] [[package]] name = "serde_json" -version = "1.0.103" +version = "1.0.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d03b412469450d4404fe8499a268edd7f8b79fecb074b0d812ad64ca21f4031b" +checksum = "693151e1ac27563d6dbcec9dee9fbd5da8539b20fa14ad3752b2e6d363ace360" dependencies = [ "itoa", "ryu", @@ -1699,13 +1700,13 @@ dependencies = [ [[package]] name = "serde_repr" -version = "0.1.14" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d89a8107374290037607734c0b73a85db7ed80cae314b3c5791f192a496e731" +checksum = "8725e1dfadb3a50f7e5ce0b1a540466f6ed3fe7a0fca2ac2b8b831d31316bd00" dependencies = [ "proc-macro2", "quote", - "syn 2.0.26", + "syn 2.0.31", ] [[package]] @@ -1722,30 +1723,31 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.1.0" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21e47d95bc83ed33b2ecf84f4187ad1ab9685d18ff28db000c99deac8ce180e3" +checksum = "1ca3b16a3d82c4088f343b7480a93550b3eabe1a358569c2dfe38bbcead07237" dependencies = [ - "base64 0.21.2", + "base64 0.21.3", "chrono", "hex", "indexmap 1.9.3", + "indexmap 2.0.0", "serde", "serde_json", "serde_with_macros", - "time 0.3.23", + "time 0.3.28", ] [[package]] name = "serde_with_macros" -version = "3.1.0" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea3cee93715c2e266b9338b7544da68a9f24e227722ba482bd1c024367c77c65" +checksum = "2e6be15c453eb305019bfa438b1593c731f36a289a7853f7707ee29e870b3b3c" dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.26", + "syn 2.0.31", ] [[package]] @@ -1779,6 +1781,16 @@ dependencies = [ "libc", ] +[[package]] +name = "signature" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e1788eed21689f9cf370582dfc467ef36ed9c707f073528ddafa8d83e3b8500" +dependencies = [ + "digest", + "rand_core", +] + [[package]] name = "simple_asn1" version = "0.6.2" @@ -1788,14 +1800,14 @@ dependencies = [ "num-bigint", "num-traits", "thiserror", - "time 0.3.23", + "time 0.3.28", ] [[package]] name = "slab" -version = "0.4.8" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" dependencies = [ "autocfg", ] @@ -1816,6 +1828,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "socket2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2538b18701741680e0322a2302176d3253a35388e2e62f172f64f4f16605f877" +dependencies = [ + "libc", + "windows-sys", +] + [[package]] name = "spin" version = "0.5.2" @@ -1833,9 +1855,9 @@ dependencies = [ [[package]] name = "spki" -version = "0.5.4" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d01ac02a6ccf3e07db148d2be087da624fea0221a16152ed01f0496a6b0a27" +checksum = "9d1e996ef02c474957d681f1b05213dfb0abab947b446a62d37770b23500184a" dependencies = [ "base64ct", "der", @@ -1854,95 +1876,206 @@ dependencies = [ [[package]] name = "sqlx" -version = "0.6.3" -source = "git+https://github.com/zert3x/sqlx?branch=feature/skip#b417a65842442b177e2abb39f3940a7c95265d90" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e58421b6bc416714d5115a2ca953718f6c621a51b68e4f4922aea5a4391a721" dependencies = [ "sqlx-core", "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", ] [[package]] name = "sqlx-core" -version = "0.6.3" -source = "git+https://github.com/zert3x/sqlx?branch=feature/skip#b417a65842442b177e2abb39f3940a7c95265d90" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd4cef4251aabbae751a3710927945901ee1d97ee96d757f6880ebb9a79bfd53" dependencies = [ - "ahash 0.7.6", + "ahash", "atoi", - "bitflags 1.3.2", "byteorder", "bytes", "chrono", "crc", "crossbeam-queue", - "digest", "dotenvy", "either", "event-listener", + "futures-channel", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashlink", + "hex", + "indexmap 2.0.0", + "ipnetwork", + "log", + "memchr", + "native-tls", + "once_cell", + "paste", + "percent-encoding", + "serde", + "serde_json", + "sha2", + "smallvec", + "sqlformat", + "thiserror", + "tokio", + "tokio-stream", + "tracing", + "url", +] + +[[package]] +name = "sqlx-macros" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "208e3165167afd7f3881b16c1ef3f2af69fa75980897aac8874a0696516d12c2" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn 1.0.109", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a4a8336d278c62231d87f24e8a7a74898156e34c1c18942857be2acb29c7dfc" +dependencies = [ + "dotenvy", + "either", + "heck", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2", + "sqlx-core", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", + "syn 1.0.109", + "tempfile", + "tokio", + "url", +] + +[[package]] +name = "sqlx-mysql" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca69bf415b93b60b80dc8fda3cb4ef52b2336614d8da2de5456cc942a110482" +dependencies = [ + "atoi", + "base64 0.21.3", + "bitflags 2.4.0", + "byteorder", + "bytes", + "chrono", + "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", + "tracing", + "whoami", +] + +[[package]] +name = "sqlx-postgres" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0db2df1b8731c3651e204629dd55e52adbae0462fa1bdcbed56a2302c18181e" +dependencies = [ + "atoi", + "base64 0.21.3", + "bitflags 2.4.0", + "byteorder", + "chrono", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "hex", + "hkdf", + "hmac", + "home", + "ipnetwork", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "rand", + "serde", + "serde_json", + "sha1", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "tracing", + "whoami", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4c21bf34c7cae5b283efb3ac1bcc7670df7561124dc2f8bdc0b59be40f79a2" +dependencies = [ + "atoi", + "chrono", "flume", "futures-channel", "futures-core", "futures-executor", "futures-intrusive", "futures-util", - "generic-array", - "hashlink", - "hex", - "indexmap 1.9.3", - "ipnetwork", - "itoa", - "libc", "libsqlite3-sys", "log", - "memchr", - "num-bigint", - "once_cell", - "paste", "percent-encoding", - "rand", - "rsa", "serde", - "serde_json", - "sha1", - "sha2", - "smallvec", - "sqlformat", - "sqlx-rt", - "stringprep", - "thiserror", - "tokio-stream", - "url", -] - -[[package]] -name = "sqlx-macros" -version = "0.6.3" -source = "git+https://github.com/zert3x/sqlx?branch=feature/skip#b417a65842442b177e2abb39f3940a7c95265d90" -dependencies = [ - "dotenvy", - "either", - "heck", - "once_cell", - "proc-macro2", - "quote", - "serde_json", - "sha2", "sqlx-core", - "sqlx-rt", - "syn 1.0.109", + "tracing", "url", ] -[[package]] -name = "sqlx-rt" -version = "0.6.3" -source = "git+https://github.com/zert3x/sqlx?branch=feature/skip#b417a65842442b177e2abb39f3940a7c95265d90" -dependencies = [ - "native-tls", - "once_cell", - "tokio", - "tokio-native-tls", -] - [[package]] name = "stringprep" version = "0.1.3" @@ -1978,9 +2111,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.26" +version = "2.0.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45c3457aacde3c65315de5031ec191ce46604304d2446e803d71ade03308d970" +checksum = "718fa2415bcb8d8bd775917a1bf12a7931b6dfa890753378538118181e0cb398" dependencies = [ "proc-macro2", "quote", @@ -1989,36 +2122,35 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.6.0" +version = "3.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31c0432476357e58790aaa47a8efb0c5138f137343f3b5f23bd36a27e3b0a6d6" +checksum = "cb94d2f3cc536af71caac6b6fcebf65860b347e7ce0cc9ebe8f70d3e521054ef" dependencies = [ - "autocfg", "cfg-if", "fastrand", - "redox_syscall 0.3.5", + "redox_syscall", "rustix", "windows-sys", ] [[package]] name = "thiserror" -version = "1.0.43" +version = "1.0.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a35fc5b8971143ca348fa6df4f024d4d55264f3468c71ad1c2f365b0a4d58c42" +checksum = "9d6d7a740b8a666a7e828dd00da9c0dc290dff53154ea77ac109281de90589b7" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.43" +version = "1.0.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "463fe12d7993d3b327787537ce8dd4dfa058de32fc2b195ef3cde03dc4771e8f" +checksum = "49922ecae66cc8a249b77e68d1d0623c1b2c514f0060c27cdc68bd62a1219d35" dependencies = [ "proc-macro2", "quote", - "syn 2.0.26", + "syn 2.0.31", ] [[package]] @@ -2034,10 +2166,11 @@ dependencies = [ [[package]] name = "time" -version = "0.3.23" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59e399c068f43a5d116fedaf73b203fa4f9c519f17e2b34f63221d3792f81446" +checksum = "17f6bb557fd245c28e6411aa56b6403c689ad95061f50e4be16c274e70a17e48" dependencies = [ + "deranged", "itoa", "serde", "time-core", @@ -2052,9 +2185,9 @@ checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" [[package]] name = "time-macros" -version = "0.2.10" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96ba15a897f3c86766b757e5ac7221554c6750054d74d5b28844fce5fb36a6c4" +checksum = "1a942f44339478ef67935ab2bbaec2fb0322496cf3cbe84b261e06ac3814c572" dependencies = [ "time-core", ] @@ -2076,20 +2209,19 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.29.1" +version = "1.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "532826ff75199d5833b9d2c5fe410f29235e25704ee5f0ef599fb51c21f4a4da" +checksum = "17ed6077ed6cd6c74735e21f37eb16dc3935f96878b1fe961074089cc80893f9" dependencies = [ - "autocfg", "backtrace", "bytes", "libc", "mio", "num_cpus", - "parking_lot 0.12.1", + "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2", + "socket2 0.5.3", "tokio-macros", "windows-sys", ] @@ -2102,7 +2234,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.26", + "syn 2.0.31", ] [[package]] @@ -2128,9 +2260,9 @@ dependencies = [ [[package]] name = "tokio-tungstenite" -version = "0.19.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec509ac96e9a0c43427c74f003127d953a265737636129424288d27cb5c4b12c" +checksum = "2b2dbec703c26b00d74844519606ef15d09a7d6857860f84ad223dec002ddea2" dependencies = [ "futures-util", "log", @@ -2193,6 +2325,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" dependencies = [ "cfg-if", + "log", "pin-project-lite", "tracing-attributes", "tracing-core", @@ -2206,7 +2339,7 @@ checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" dependencies = [ "proc-macro2", "quote", - "syn 2.0.26", + "syn 2.0.31", ] [[package]] @@ -2226,9 +2359,9 @@ checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" [[package]] name = "tungstenite" -version = "0.19.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15fba1a6d6bb030745759a9a2a588bfe8490fc8b4751a277db3a0be1c9ebbf67" +checksum = "e862a1c4128df0112ab625f55cd5c934bcb4312ba80b39ae4b4835a3fd58e649" dependencies = [ "byteorder", "bytes", @@ -2261,9 +2394,9 @@ dependencies = [ [[package]] name = "unicase" -version = "2.6.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" +checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" dependencies = [ "version_check", ] @@ -2315,9 +2448,9 @@ checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" [[package]] name = "url" -version = "2.4.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50bff7831e19200a85b17131d085c25d7811bc4e186efdaf54bbd132994a88cb" +checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5" dependencies = [ "form_urlencoded", "idna", @@ -2384,7 +2517,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.26", + "syn 2.0.31", "wasm-bindgen-shared", ] @@ -2418,7 +2551,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.26", + "syn 2.0.31", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -2439,6 +2572,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "whoami" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22fc3756b8a9133049b26c7f61ab35416c130e8c09b660f5b3958b446f52cc50" + [[package]] name = "winapi" version = "0.3.9" @@ -2481,9 +2620,9 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.48.1" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ "windows_aarch64_gnullvm", "windows_aarch64_msvc", @@ -2496,62 +2635,63 @@ dependencies = [ [[package]] name = "windows_aarch64_gnullvm" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_msvc" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_i686_gnu" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_msvc" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_x86_64_gnu" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnullvm" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_msvc" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "winnow" -version = "0.5.0" +version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81fac9742fd1ad1bd9643b991319f72dd031016d44b77039a26977eb667141e7" +checksum = "7c2e3184b9c4e92ad5167ca73039d0c42476302ab603e2fec4487511f38ccefc" dependencies = [ "memchr", ] [[package]] name = "winreg" -version = "0.10.1" +version = "0.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" dependencies = [ - "winapi", + "cfg-if", + "windows-sys", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 8dbb172..42a5130 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,8 +1,14 @@ [package] name = "chorus" -version = "0.1.0" -license = "AGPL-3" +description = "A library for interacting with multiple Spacebar-compatible Instances at once." +version = "0.9.0" +license = "AGPL-3.0" edition = "2021" +repository = "https://github.com/polyphony-chat/chorus" +readme = "README.md" +keywords = ["spacebar", "discord", "polyphony"] +website = ["https://discord.com/invite/m3FpcapGDD"] + [features] default = ["client"] @@ -10,35 +16,43 @@ backend = ["poem", "sqlx"] client = [] [dependencies] -tokio = {version = "1.29.1", features = ["macros"]} -serde = {version = "1.0.171", features = ["derive"]} -serde_json = {version= "1.0.103", features = ["raw_value"]} +tokio = { version = "1.29.1", features = ["macros"] } +serde = { version = "1.0.188", features = ["derive", "rc"] } +serde_json = { version = "1.0.105", features = ["raw_value"] } serde-aux = "4.2.0" -serde_with = "3.0.0" -serde_repr = "0.1.14" -reqwest = {version = "0.11.18", features = ["multipart"]} +serde_with = "3.3.0" +serde_repr = "0.1.16" +reqwest = { version = "0.11.20", features = ["multipart", "json"] } url = "2.4.0" -chrono = {version = "0.4.26", features = ["serde"]} -regex = "1.9.1" +chrono = { version = "0.4.26", features = ["serde"] } +regex = "1.9.4" custom_error = "1.9.2" native-tls = "0.2.11" -tokio-tungstenite = {version = "0.19.0", features = ["native-tls"]} +tokio-tungstenite = { version = "0.20.0", features = ["native-tls"] } futures-util = "0.3.28" http = "0.2.9" -openssl = "0.10.55" -base64 = "0.21.2" +openssl = "0.10.56" +base64 = "0.21.3" hostname = "0.3.1" -bitflags = { version = "2.3.3", features = ["serde"] } +bitflags = { version = "2.4.0", features = ["serde"] } lazy_static = "1.4.0" -poem = { version = "1.3.56", optional = true } -sqlx = { git = "https://github.com/zert3x/sqlx", branch="feature/skip", features = ["mysql", "sqlite", "json", "chrono", "ipnetwork", "runtime-tokio-native-tls", "any"], optional = true } -thiserror = "1.0.43" +poem = { version = "1.3.57", optional = true } +sqlx = { version = "0.7.1", features = [ + "mysql", + "sqlite", + "json", + "chrono", + "ipnetwork", + "runtime-tokio-native-tls", + "any", +], optional = true } +thiserror = "1.0.47" jsonwebtoken = "8.3.0" -log = "0.4.19" -async-trait = "0.1.71" -chorus-macros = {path = "chorus-macros"} +log = "0.4.20" +async-trait = "0.1.73" +chorus-macros = "0.2.0" [dev-dependencies] -tokio = {version = "1.29.1", features = ["full"]} +tokio = { version = "1.32.0", features = ["full"] } lazy_static = "1.4.0" rusty-hook = "0.11.2" diff --git a/README.md b/README.md index 9292a46..74dff4c 100644 --- a/README.md +++ b/README.md @@ -2,10 +2,11 @@ [![Discord]][Discord-invite] [![Build][build-shield]][build-url] +[![Coverage][coverage-shield]][coverage-url] [![Contributors][contributors-shield]][contributors-url] [![Forks][forks-shield]][forks-url] [![Issues][issues-shield]][issues-url] - +
@@ -37,6 +38,7 @@ Chorus is a Rust library that allows developers to interact with multiple Spaceb ## Contributing +If you'd like to contribute new functionality, check out [The 'Meta'-issues.](https://github.com/polyphony-chat/chorus/issues?q=is%3Aissue+label%3A%22Type%3A+Meta%22+) They contain a comprehensive list of all features which are yet missing for full Discord.com compatibility. If you would like to contribute, please feel free to open an Issue with the idea you have, or a Pull Request. Please keep our [contribution guidelines](https://github.com/polyphony-chat/.github/blob/main/CONTRIBUTION_GUIDELINES.md) in mind. Your contribution might not be accepted, if it violates these guidelines or [our Code of Conduct](https://github.com/polyphony-chat/.github/blob/main/CODE_OF_CONDUCT.md). @@ -123,6 +125,8 @@ accepted, if it violates these guidelines or [our Code of Conduct](https://githu [clippy-url]: https://github.com/polyphony-chat/chorus/blob/main/.github/workflows/clippy.yml [contributors-shield]: https://img.shields.io/github/contributors/polyphony-chat/chorus.svg?style=flat [contributors-url]: https://github.com/polyphony-chat/chorus/graphs/contributors + [coverage-shield]: https://coveralls.io/repos/github/polyphony-chat/chorus/badge.svg?branch=main + [coverage-url]: https://coveralls.io/github/polyphony-chat/chorus?branch=main [forks-shield]: https://img.shields.io/github/forks/polyphony-chat/chorus.svg?style=flat [forks-url]: https://github.com/polyphony-chat/chorus/network/members [stars-shield]: https://img.shields.io/github/stars/polyphony-chat/chorus.svg?style=flat @@ -133,4 +137,4 @@ accepted, if it violates these guidelines or [our Code of Conduct](https://githu [license-url]: https://github.com/polyphony-chat/chorus/blob/master/LICENSE [Discord]: https://dcbadge.vercel.app/api/server/m3FpcapGDD?style=flat [Discord-invite]: https://discord.com/invite/m3FpcapGDD - \ No newline at end of file + diff --git a/chorus-macros/Cargo.lock b/chorus-macros/Cargo.lock index 4aa96d3..17404a2 100644 --- a/chorus-macros/Cargo.lock +++ b/chorus-macros/Cargo.lock @@ -2,10 +2,22 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "async-trait" +version = "0.1.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc00ceb34980c03614e35a3a4e218276a0a824e911d07651cd0d858a51e8c0f0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "chorus-macros" version = "0.1.0" dependencies = [ + "async-trait", "quote", "syn", ] @@ -21,18 +33,18 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.31" +version = "1.0.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fe8a65d69dd0808184ebb5f836ab526bb259db23c657efa38711b1072ee47f0" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" dependencies = [ "proc-macro2", ] [[package]] name = "syn" -version = "2.0.27" +version = "2.0.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b60f673f44a8255b9c8c657daf66a596d435f2da81a555b06dc644d080ba45e0" +checksum = "c324c494eba9d92503e6f1ef2e6df781e78f6a7705a0202d9801b198807d518a" dependencies = [ "proc-macro2", "quote", diff --git a/chorus-macros/Cargo.toml b/chorus-macros/Cargo.toml index cffffe1..272d99f 100644 --- a/chorus-macros/Cargo.toml +++ b/chorus-macros/Cargo.toml @@ -1,11 +1,14 @@ [package] name = "chorus-macros" -version = "0.1.0" +version = "0.2.0" edition = "2021" +license = "AGPL-3.0" +description = "Macros for the chorus crate." [lib] proc-macro = true [dependencies] -quote = "1" -syn = "2" +quote = "1.0.33" +syn = "2.0.29" +async-trait = "0.1.73" diff --git a/chorus-macros/src/lib.rs b/chorus-macros/src/lib.rs index c8d5e87..fd68df0 100644 --- a/chorus-macros/src/lib.rs +++ b/chorus-macros/src/lib.rs @@ -1,5 +1,6 @@ use proc_macro::TokenStream; use quote::quote; +use syn::{parse_macro_input, Data, DeriveInput, Field, Fields, FieldsNamed}; #[proc_macro_derive(Updateable)] pub fn updateable_macro_derive(input: TokenStream) -> TokenStream { @@ -16,3 +17,125 @@ pub fn updateable_macro_derive(input: TokenStream) -> TokenStream { } .into() } + +#[proc_macro_derive(JsonField)] +pub fn jsonfield_macro_derive(input: TokenStream) -> TokenStream { + let ast: syn::DeriveInput = syn::parse(input).unwrap(); + + let name = &ast.ident; + // No need for macro hygiene, we're only using this in chorus + quote! { + impl JsonField for #name { + fn get_json(&self) -> String { + self.json.clone() + } + fn set_json(&mut self, json: String) { + self.json = json; + } + } + } + .into() +} + +#[proc_macro_derive(SourceUrlField)] +pub fn source_url_macro_derive(input: TokenStream) -> TokenStream { + let ast: syn::DeriveInput = syn::parse(input).unwrap(); + + let name = &ast.ident; + // No need for macro hygiene, we're only using this in chorus + quote! { + impl SourceUrlField for #name { + fn get_source_url(&self) -> String { + self.source_url.clone() + } + fn set_source_url(&mut self, url: String) { + self.source_url = url; + } + } + } + .into() +} + +#[proc_macro_attribute] +pub fn observe_option(_args: TokenStream, input: TokenStream) -> TokenStream { + input +} + +#[proc_macro_attribute] +pub fn observe_option_vec(_args: TokenStream, input: TokenStream) -> TokenStream { + input +} + +#[proc_macro_attribute] +pub fn observe(_args: TokenStream, input: TokenStream) -> TokenStream { + input +} + +#[proc_macro_attribute] +pub fn observe_vec(_args: TokenStream, input: TokenStream) -> TokenStream { + input +} + +#[proc_macro_derive( + Composite, + attributes(observe_option_vec, observe_option, observe, observe_vec) +)] +pub fn composite_derive(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + + let process_field = |field: &Field| { + let field_name = &field.ident; + let attrs = &field.attrs; + + let observe_option = attrs + .iter() + .any(|attr| attr.path().is_ident("observe_option")); + let observe_option_vec = attrs + .iter() + .any(|attr| attr.path().is_ident("observe_option_vec")); + let observe = attrs.iter().any(|attr| attr.path().is_ident("observe")); + let observe_vec = attrs.iter().any(|attr| attr.path().is_ident("observe_vec")); + + match (observe_option, observe_option_vec, observe, observe_vec) { + (true, _, _, _) => quote! { + #field_name: Self::option_observe_fn(self.#field_name, gateway).await + }, + (_, true, _, _) => quote! { + #field_name: Self::option_vec_observe_fn(self.#field_name, gateway).await + }, + (_, _, true, _) => quote! { + #field_name: Self::value_observe_fn(self.#field_name, gateway).await + }, + (_, _, _, true) => quote! { + #field_name: Self::vec_observe_fn(self.#field_name, gateway).await + }, + _ => quote! { + #field_name: self.#field_name + }, + } + }; + + match &input.data { + Data::Struct(data) => match &data.fields { + Fields::Named(FieldsNamed { named, .. }) => { + let field_exprs = named.iter().map(process_field); + + let ident = &input.ident; + let expanded = quote! { + #[async_trait::async_trait(?Send)] + impl Composite for #ident { + async fn watch_whole(self, gateway: &GatewayHandle) -> Self { + Self { + #(#field_exprs,)* + } + } + } + }; + + TokenStream::from(expanded) + } + _ => panic!("Composite derive macro only supports named fields"), + }, + _ => panic!("Composite derive macro only supports structs"), + } +} diff --git a/src/api/auth/login.rs b/src/api/auth/login.rs index 68604d9..272ee04 100644 --- a/src/api/auth/login.rs +++ b/src/api/auth/login.rs @@ -1,30 +1,33 @@ -use std::cell::RefCell; -use std::rc::Rc; +use std::sync::{Arc, RwLock}; use reqwest::Client; use serde_json::to_string; -use crate::api::LimitType; use crate::errors::ChorusResult; use crate::gateway::Gateway; -use crate::instance::{Instance, UserMeta}; +use crate::instance::{ChorusUser, Instance}; use crate::ratelimiter::ChorusRequest; -use crate::types::{GatewayIdentifyPayload, LoginResult, LoginSchema}; +use crate::types::{GatewayIdentifyPayload, LimitType, LoginResult, LoginSchema}; impl Instance { - pub async fn login_account(&mut self, login_schema: &LoginSchema) -> ChorusResult { + /// Logs into an existing account on the spacebar server. + /// + /// # Reference + /// See + pub async fn login_account(mut self, login_schema: LoginSchema) -> ChorusResult { let endpoint_url = self.urls.api.clone() + "/auth/login"; let chorus_request = ChorusRequest { request: Client::new() .post(endpoint_url) - .body(to_string(login_schema).unwrap()), + .body(to_string(&login_schema).unwrap()) + .header("Content-Type", "application/json"), limit_type: LimitType::AuthLogin, }; // We do not have a user yet, and the UserRateLimits will not be affected by a login // request (since login is an instance wide limit), which is why we are just cloning the // instances' limits to pass them on as user_rate_limits later. let mut shell = - UserMeta::shell(Rc::new(RefCell::new(self.clone())), "None".to_string()).await; + ChorusUser::shell(Arc::new(RwLock::new(self.clone())), "None".to_string()).await; let login_result = chorus_request .deserialize_response::(&mut shell) .await?; @@ -36,12 +39,12 @@ impl Instance { let gateway = Gateway::new(self.urls.wss.clone()).await.unwrap(); identify.token = login_result.token.clone(); gateway.send_identify(identify).await; - let user = UserMeta::new( - Rc::new(RefCell::new(self.clone())), + let user = ChorusUser::new( + Arc::new(RwLock::new(self.clone())), login_result.token, self.clone_limits_if_some(), login_result.settings, - object, + Arc::new(RwLock::new(object)), gateway, ); Ok(user) diff --git a/src/api/auth/mod.rs b/src/api/auth/mod.rs index 663bf5e..3ad4a60 100644 --- a/src/api/auth/mod.rs +++ b/src/api/auth/mod.rs @@ -1,5 +1,41 @@ +use std::sync::{Arc, RwLock}; + pub use login::*; pub use register::*; +use crate::{ + errors::ChorusResult, + gateway::Gateway, + instance::{ChorusUser, Instance}, + types::{GatewayIdentifyPayload, User}, +}; + pub mod login; pub mod register; + +impl Instance { + /// Logs into an existing account on the spacebar server, using only a token. + pub async fn login_with_token(&mut self, token: String) -> ChorusResult { + let object_result = self.get_user(token.clone(), None).await; + if let Err(e) = object_result { + return Result::Err(e); + } + + let user_settings = User::get_settings(&token, &self.urls.api, &mut self.clone()) + .await + .unwrap(); + let mut identify = GatewayIdentifyPayload::common(); + let gateway = Gateway::new(self.urls.wss.clone()).await.unwrap(); + identify.token = token.clone(); + gateway.send_identify(identify).await; + let user = ChorusUser::new( + Arc::new(RwLock::new(self.clone())), + token.clone(), + self.clone_limits_if_some(), + Arc::new(RwLock::new(user_settings)), + Arc::new(RwLock::new(object_result.unwrap())), + gateway, + ); + Ok(user) + } +} diff --git a/src/api/auth/register.rs b/src/api/auth/register.rs index e818d82..d10915e 100644 --- a/src/api/auth/register.rs +++ b/src/api/auth/register.rs @@ -1,4 +1,4 @@ -use std::{cell::RefCell, rc::Rc}; +use std::sync::{Arc, RwLock}; use reqwest::Client; use serde_json::to_string; @@ -6,39 +6,35 @@ use serde_json::to_string; use crate::gateway::Gateway; use crate::types::GatewayIdentifyPayload; use crate::{ - api::policies::instance::LimitType, errors::ChorusResult, - instance::{Instance, Token, UserMeta}, + instance::{ChorusUser, Instance, Token}, ratelimiter::ChorusRequest, + types::LimitType, types::RegisterSchema, }; impl Instance { - /// Registers a new user on the Spacebar server. + /// Registers a new user on the server. /// - /// # Arguments - /// - /// * `register_schema` - The [`RegisterSchema`] that contains all the information that is needed to register a new user. - /// - /// # Errors - /// - /// * [`ChorusLibError`] - If the server does not respond. + /// # Reference + /// See pub async fn register_account( - &mut self, - register_schema: &RegisterSchema, - ) -> ChorusResult { + mut self, + register_schema: RegisterSchema, + ) -> ChorusResult { let endpoint_url = self.urls.api.clone() + "/auth/register"; let chorus_request = ChorusRequest { request: Client::new() .post(endpoint_url) - .body(to_string(register_schema).unwrap()), + .body(to_string(®ister_schema).unwrap()) + .header("Content-Type", "application/json"), limit_type: LimitType::AuthRegister, }; // We do not have a user yet, and the UserRateLimits will not be affected by a login // request (since register is an instance wide limit), which is why we are just cloning // the instances' limits to pass them on as user_rate_limits later. let mut shell = - UserMeta::shell(Rc::new(RefCell::new(self.clone())), "None".to_string()).await; + ChorusUser::shell(Arc::new(RwLock::new(self.clone())), "None".to_string()).await; let token = chorus_request .deserialize_response::(&mut shell) .await? @@ -47,17 +43,17 @@ impl Instance { self.limits_information.as_mut().unwrap().ratelimits = shell.limits.unwrap(); } let user_object = self.get_user(token.clone(), None).await.unwrap(); - let settings = UserMeta::get_settings(&token, &self.urls.api.clone(), self).await?; + let settings = ChorusUser::get_settings(&token, &self.urls.api.clone(), &mut self).await?; let mut identify = GatewayIdentifyPayload::common(); let gateway = Gateway::new(self.urls.wss.clone()).await.unwrap(); identify.token = token.clone(); gateway.send_identify(identify).await; - let user = UserMeta::new( - Rc::new(RefCell::new(self.clone())), + let user = ChorusUser::new( + Arc::new(RwLock::new(self.clone())), token.clone(), self.clone_limits_if_some(), - settings, - user_object, + Arc::new(RwLock::new(settings)), + Arc::new(RwLock::new(user_object)), gateway, ); Ok(user) diff --git a/src/api/channels/channels.rs b/src/api/channels/channels.rs index df8e290..9560d74 100644 --- a/src/api/channels/channels.rs +++ b/src/api/channels/channels.rs @@ -1,126 +1,165 @@ use reqwest::Client; use serde_json::to_string; -use crate::types::AddChannelRecipientSchema; +use crate::types::{AddChannelRecipientSchema, ModifyChannelPositionsSchema}; use crate::{ - api::LimitType, errors::{ChorusError, ChorusResult}, - instance::UserMeta, + instance::ChorusUser, ratelimiter::ChorusRequest, - types::{Channel, ChannelModifySchema, GetChannelMessagesSchema, Message, Snowflake}, + types::{ + Channel, ChannelModifySchema, GetChannelMessagesSchema, LimitType, Message, Snowflake, + }, }; impl Channel { - pub async fn get(user: &mut UserMeta, channel_id: Snowflake) -> ChorusResult { - let url = user.belongs_to.borrow().urls.api.clone(); - let chorus_request = ChorusRequest { - request: Client::new() - .get(format!("{}/channels/{}/", url, channel_id)) - .bearer_auth(user.token()), - limit_type: LimitType::Channel(channel_id), - }; + /// Retrieves a channel from the server. + /// + /// # Reference + /// See + pub async fn get(user: &mut ChorusUser, channel_id: Snowflake) -> ChorusResult { + let chorus_request = ChorusRequest::new( + http::Method::GET, + &format!( + "{}/channels/{}", + user.belongs_to.read().unwrap().urls.api.clone(), + channel_id + ), + None, + None, + None, + Some(user), + LimitType::Channel(channel_id), + ); + chorus_request.deserialize_response::(user).await } - /// Deletes a channel. + /// Deletes self. /// - /// # Arguments + /// Requires the [`MANAGE_CHANNELS`](crate::types::PermissionFlags::MANAGE_CHANNELS) permission in a guild, or + /// the [`MANAGE_THREADS`](crate::types::PermissionFlags::MANAGE_THREADS) permission if the channel is a thread. /// - /// * `token` - A string slice that holds the authorization token. - /// * `url_api` - A string slice that holds the URL of the API. - /// * `channel` - A `Channel` object that represents the channel to be deleted. - /// * `limits_user` - A mutable reference to a `Limits` object that represents the user's rate limits. - /// * `limits_instance` - A mutable reference to a `Limits` object that represents the instance's rate limits. - /// - /// # Returns - /// - /// A `Result` that contains a `ChorusLibError` if an error occurred during the request, or `()` if the request was successful. - pub async fn delete(self, user: &mut UserMeta) -> ChorusResult<()> { - let chorus_request = ChorusRequest { - request: Client::new() - .delete(format!( - "{}/channels/{}/", - user.belongs_to.borrow().urls.api, - self.id - )) - .bearer_auth(user.token()), - limit_type: LimitType::Channel(self.id), - }; - chorus_request.handle_request_as_result(user).await + /// # Reference + /// See + pub async fn delete( + self, + audit_log_reason: Option, + user: &mut ChorusUser, + ) -> ChorusResult<()> { + let url = format!( + "{}/channels/{}", + user.belongs_to.read().unwrap().urls.api, + self.id, + ); + + let request = ChorusRequest::new( + http::Method::DELETE, + &url, + None, + audit_log_reason.as_deref(), + None, + Some(user), + LimitType::Channel(self.id), + ); + + request.handle_request_as_result(user).await } - /// Modifies a channel. + /// Modifies a channel with the provided data. + /// Returns the new Channel. /// - /// # Arguments + /// Requires the [`MANAGE_CHANNELS`](crate::types::PermissionFlags::MANAGE_CHANNELS) permission in a guild. /// - /// * `modify_data` - A `ChannelModifySchema` object that represents the modifications to be made to the channel. - /// * `token` - A string slice that holds the authorization token. - /// * `url_api` - A string slice that holds the URL of the API. - /// * `channel_id` - A string slice that holds the ID of the channel to be modified. - /// * `limits_user` - A mutable reference to a `Limits` object that represents the user's rate limits. - /// * `limits_instance` - A mutable reference to a `Limits` object that represents the instance's rate limits. + /// If modifying permission overwrites, the [`MANAGE_ROLES`](crate::types::PermissionFlags::MANAGE_ROLES) permission is required. + /// Only permissions you have in the guild or parent channel (if applicable) can be allowed/denied + /// (unless you have a [`MANAGE_ROLES`](crate::types::PermissionFlags::MANAGE_ROLES) overwrite in the channel). /// - /// # Returns + /// If modifying a thread and setting `archived` to `false`, when `locked` is also `false`, only the [`SEND_MESSAGES`](crate::types::PermissionFlags::SEND_MESSAGES) permission is required. + /// Otherwise, requires the [`MANAGE_THREADS`](crate::types::PermissionFlags::MANAGE_THREADS) permission. Requires the thread to have `archived` set to `false` or be set to `false` in the request. /// - /// A `Result` that contains a `Channel` object if the request was successful, or an `ChorusLibError` if an error occurred during the request. + /// # Reference + /// See pub async fn modify( &self, modify_data: ChannelModifySchema, - channel_id: Snowflake, - user: &mut UserMeta, + audit_log_reason: Option, + user: &mut ChorusUser, ) -> ChorusResult { - let chorus_request = ChorusRequest { - request: Client::new() - .patch(format!( - "{}/channels/{}/", - user.belongs_to.borrow().urls.api, - channel_id - )) - .bearer_auth(user.token()) - .body(to_string(&modify_data).unwrap()), - limit_type: LimitType::Channel(channel_id), - }; - chorus_request.deserialize_response::(user).await + let channel_id = self.id; + let url = format!( + "{}/channels/{}", + user.belongs_to.read().unwrap().urls.api, + channel_id + ); + + let request = ChorusRequest::new( + http::Method::PATCH, + &url, + Some(to_string(&modify_data).unwrap()), + audit_log_reason.as_deref(), + None, + Some(user), + LimitType::Channel(channel_id), + ); + + request.deserialize_response::(user).await } + /// Fetches recent messages from a channel. + /// + /// If operating on a guild channel, this endpoint requires the [`VIEW_CHANNEL`](crate::types::PermissionFlags::VIEW_CHANNEL) permission. + /// + /// If the user is missing the [`READ_MESSAGE_HISTORY`](crate::types::PermissionFlags::READ_MESSAGE_HISTORY) permission, + /// this method returns an empty list. + /// + /// # Reference + /// See pub async fn messages( range: GetChannelMessagesSchema, channel_id: Snowflake, - user: &mut UserMeta, + user: &mut ChorusUser, ) -> Result, ChorusError> { - let chorus_request = ChorusRequest { - request: Client::new() - .get(format!( - "{}/channels/{}/messages", - user.belongs_to.borrow().urls.api, - channel_id - )) - .bearer_auth(user.token()) - .query(&range), - limit_type: Default::default(), - }; + let url = format!( + "{}/channels/{}/messages", + user.belongs_to.read().unwrap().urls.api, + channel_id + ); + + let mut chorus_request = ChorusRequest::new( + http::Method::GET, + &url, + None, + None, + None, + Some(user), + Default::default(), + ); + chorus_request.request = chorus_request.request.query(&range); chorus_request .deserialize_response::>(user) .await } + /// Adds a recipient to a group DM. + /// /// # Reference: - /// Read: + /// See pub async fn add_channel_recipient( &self, recipient_id: Snowflake, - user: &mut UserMeta, + user: &mut ChorusUser, add_channel_recipient_schema: Option, ) -> ChorusResult<()> { let mut request = Client::new() .put(format!( - "{}/channels/{}/recipients/{}/", - user.belongs_to.borrow().urls.api, + "{}/channels/{}/recipients/{}", + user.belongs_to.read().unwrap().urls.api, self.id, recipient_id )) - .bearer_auth(user.token()); + .header("Authorization", user.token()) + .header("Content-Type", "application/json"); if let Some(schema) = add_channel_recipient_schema { request = request.body(to_string(&schema).unwrap()); } @@ -132,26 +171,61 @@ impl Channel { .await } + /// Removes a recipient from a group DM. + /// /// # Reference: - /// Read: + /// See pub async fn remove_channel_recipient( &self, recipient_id: Snowflake, - user: &mut UserMeta, + user: &mut ChorusUser, ) -> ChorusResult<()> { - let request = Client::new() - .delete(format!( - "{}/channels/{}/recipients/{}/", - user.belongs_to.borrow().urls.api, - self.id, - recipient_id - )) - .bearer_auth(user.token()); - ChorusRequest { - request, - limit_type: LimitType::Channel(self.id), - } - .handle_request_as_result(user) - .await + let url = format!( + "{}/channels/{}/recipients/{}", + user.belongs_to.read().unwrap().urls.api, + self.id, + recipient_id + ); + + let request = ChorusRequest::new( + http::Method::DELETE, + &url, + None, + None, + None, + Some(user), + LimitType::Channel(self.id), + ); + + request.handle_request_as_result(user).await + } + + /// Modifies the positions of a set of channel objects for the guild. Requires the `MANAGE_CHANNELS` permission. + /// Only channels to be modified are required. + /// + /// # Reference: + /// See + pub async fn modify_positions( + schema: Vec, + guild_id: Snowflake, + user: &mut ChorusUser, + ) -> ChorusResult<()> { + let url = format!( + "{}/guilds/{}/channels", + user.belongs_to.read().unwrap().urls.api, + guild_id + ); + + let request = ChorusRequest::new( + http::Method::PATCH, + &url, + Some(to_string(&schema).unwrap()), + None, + None, + Some(user), + LimitType::Guild(guild_id), + ); + + request.handle_request_as_result(user).await } } diff --git a/src/api/channels/messages.rs b/src/api/channels/messages.rs index 6beec7f..6dfdfbf 100644 --- a/src/api/channels/messages.rs +++ b/src/api/channels/messages.rs @@ -1,27 +1,36 @@ use http::header::CONTENT_DISPOSITION; use http::HeaderMap; use reqwest::{multipart, Client}; -use serde_json::to_string; +use serde_json::{from_value, to_string, Value}; -use crate::api::LimitType; -use crate::instance::UserMeta; +use crate::errors::{ChorusError, ChorusResult}; +use crate::instance::ChorusUser; use crate::ratelimiter::ChorusRequest; -use crate::types::{Message, MessageSendSchema, Snowflake}; +use crate::types::{ + Channel, CreateGreetMessage, LimitType, Message, MessageAck, MessageModifySchema, + MessageSearchEndpoint, MessageSearchQuery, MessageSendSchema, Snowflake, +}; impl Message { + /// Sends a message in the channel with the provided channel_id. + /// Returns the sent message. + /// + /// # Reference + /// See pub async fn send( - user: &mut UserMeta, + user: &mut ChorusUser, channel_id: Snowflake, mut message: MessageSendSchema, - ) -> Result { - let url_api = user.belongs_to.borrow().urls.api.clone(); + ) -> ChorusResult { + let url_api = user.belongs_to.read().unwrap().urls.api.clone(); if message.attachments.is_none() { let chorus_request = ChorusRequest { request: Client::new() - .post(format!("{}/channels/{}/messages/", url_api, channel_id)) - .bearer_auth(user.token()) - .body(to_string(&message).unwrap()), + .post(format!("{}/channels/{}/messages", url_api, channel_id)) + .header("Authorization", user.token()) + .body(to_string(&message).unwrap()) + .header("Content-Type", "application/json"), limit_type: LimitType::Channel(channel_id), }; chorus_request.deserialize_response::(user).await @@ -55,22 +64,462 @@ impl Message { let chorus_request = ChorusRequest { request: Client::new() - .post(format!("{}/channels/{}/messages/", url_api, channel_id)) - .bearer_auth(user.token()) + .post(format!("{}/channels/{}/messages", url_api, channel_id)) + .header("Authorization", user.token()) .multipart(form), limit_type: LimitType::Channel(channel_id), }; chorus_request.deserialize_response::(user).await } } + + /// Returns messages without the reactions key that match a search query in the guild or channel. + /// The messages that are direct results will have an extra hit key set to true. + /// If operating on a guild channel, this endpoint requires the `READ_MESSAGE_HISTORY` + /// permission to be present on the current user. + /// + /// If the guild/channel you are searching is not yet indexed, the endpoint will return a 202 accepted response. + /// In this case, the method will return a [`ChorusError::InvalidResponse`] error. + /// + /// # Reference: + /// See + pub(crate) async fn search( + endpoint: MessageSearchEndpoint, + query: MessageSearchQuery, + user: &mut ChorusUser, + ) -> ChorusResult> { + let limit_type = match &endpoint { + MessageSearchEndpoint::Channel(id) => LimitType::Channel(*id), + MessageSearchEndpoint::GuildChannel(id) => LimitType::Guild(*id), + }; + let request = ChorusRequest { + limit_type, + request: Client::new() + .get(format!( + "{}/{}/messages/search", + &user.belongs_to.read().unwrap().urls.api, + endpoint + )) + .header("Authorization", user.token()) + .header("Content-Type", "application/json") + .body(to_string(&query).unwrap()), + }; + let result = request.send_request(user).await?; + let result_json = result.json::().await.unwrap(); + if !result_json.is_object() { + return Err(search_error(result_json.to_string())); + } + let value_map = result_json.as_object().unwrap(); + if let Some(messages) = value_map.get("messages") { + if let Ok(response) = from_value::>>(messages.clone()) { + let result_messages: Vec = response.into_iter().flatten().collect(); + return Ok(result_messages); + } + } + // The code below might be incorrect. We'll cross that bridge when we come to it + if !value_map.contains_key("code") || !value_map.contains_key("retry_after") { + return Err(search_error(result_json.to_string())); + } + let code = value_map.get("code").unwrap().as_u64().unwrap(); + let retry_after = value_map.get("retry_after").unwrap().as_u64().unwrap(); + Err(ChorusError::NotFound { + error: format!( + "Index not yet available. Try again later. Code: {}. Retry after {}s", + code, retry_after + ), + }) + } + + /// Returns all pinned messages in the channel as a Vector of message objects without the reactions key. + /// # Reference: + /// See: + pub async fn get_sticky( + channel_id: Snowflake, + user: &mut ChorusUser, + ) -> ChorusResult> { + let chorus_request = ChorusRequest::new( + http::Method::GET, + format!( + "{}/channels/{}/pins", + user.belongs_to.read().unwrap().urls.api, + channel_id + ) + .as_str(), + None, + None, + None, + Some(user), + LimitType::Channel(channel_id), + ); + chorus_request + .deserialize_response::>(user) + .await + } + + /// Pins a message in a channel. Requires the `MANAGE_MESSAGES` permission. Returns a 204 empty response on success. + /// The max pinned messages is 50. + /// + /// # Reference: + /// See: + pub async fn sticky( + channel_id: Snowflake, + message_id: Snowflake, + audit_log_reason: Option<&str>, + user: &mut ChorusUser, + ) -> ChorusResult<()> { + let request = ChorusRequest::new( + http::Method::PUT, + format!( + "{}/channels/{}/pins/{}", + user.belongs_to.read().unwrap().urls.api, + channel_id, + message_id + ) + .as_str(), + None, + audit_log_reason, + None, + Some(user), + LimitType::Channel(channel_id), + ); + request.handle_request_as_result(user).await + } + + /// Unpins a message in a channel. Requires the `MANAGE_MESSAGES` permission. Returns a 204 empty response on success. + /// # Reference: + /// See: + pub async fn unsticky( + channel_id: Snowflake, + message_id: Snowflake, + audit_log_reason: Option<&str>, + user: &mut ChorusUser, + ) -> ChorusResult<()> { + let request = ChorusRequest::new( + http::Method::DELETE, + format!( + "{}/channels/{}/pins/{}", + user.belongs_to.read().unwrap().urls.api, + channel_id, + message_id + ) + .as_str(), + None, + audit_log_reason, + None, + Some(user), + LimitType::Channel(channel_id), + ); + request.handle_request_as_result(user).await + } + + /// Returns a specific message object in the channel. + /// If operating on a guild channel, this endpoint requires the `READ_MESSAGE_HISTORY` permission to be present on the current user. + /// # Reference: + /// See: + pub async fn get( + channel_id: Snowflake, + message_id: Snowflake, + user: &mut ChorusUser, + ) -> ChorusResult { + let chorus_request = ChorusRequest { + request: Client::new() + .get(format!( + "{}/channels/{}/messages/{}", + user.belongs_to.read().unwrap().urls.api, + channel_id, + message_id + )) + .header("Authorization", user.token()) + .header("Content-Type", "application/json"), + limit_type: LimitType::Channel(channel_id), + }; + chorus_request.deserialize_response::(user).await + } + + /// Posts a greet message to a channel. This endpoint requires the channel is a DM channel or you reply to a system message. + /// # Reference: + /// See: + pub async fn create_greet( + channel_id: Snowflake, + schema: CreateGreetMessage, + user: &mut ChorusUser, + ) -> ChorusResult { + let request = ChorusRequest::new( + http::Method::POST, + format!( + "{}/channels/{}/messages/greet", + user.belongs_to.read().unwrap().urls.api, + channel_id, + ) + .as_str(), + Some(to_string(&schema).unwrap()), + None, + None, + Some(user), + LimitType::Channel(channel_id), + ); + request.deserialize_response::(user).await + } + + /// Sets the channel's latest acknowledged message (marks a message as read) for the current user. + /// The message ID parameter does not need to be a valid message ID, but it must be a valid snowflake. + /// If the message ID is being set to a message sent prior to the latest acknowledged one, + /// manual should be true or the resulting read state update should be ignored by clients (but is still saved), resulting in undefined behavior. + /// In this case, mention_count should also be set to the amount of mentions unacknowledged as it is not automatically calculated by Discord. + /// + /// Returns an optional token, which can be used as the new `ack` token for following `ack`s. + /// + /// # Reference: + /// See: + pub async fn acknowledge( + channel_id: Snowflake, + message_id: Snowflake, + schema: MessageAck, + user: &mut ChorusUser, + ) -> ChorusResult> { + let request = ChorusRequest::new( + http::Method::POST, + format!( + "{}/channels/{}/messages/{}/ack", + user.belongs_to.read().unwrap().urls.api, + channel_id, + message_id + ) + .as_str(), + Some(to_string(&schema).unwrap()), + None, + None, + Some(user), + LimitType::Channel(channel_id), + ); + request.deserialize_response::>(user).await + } + + /// Crossposts a message in a News Channel to following channels. + /// This endpoint requires the `SEND_MESSAGES` permission, if the current user sent the message, + /// or additionally the `MANAGE_MESSAGES` permission, for all other messages, to be present for the current user. + /// + /// # Reference: + /// See + pub async fn crosspost( + channel_id: Snowflake, + message_id: Snowflake, + user: &mut ChorusUser, + ) -> ChorusResult { + let request = ChorusRequest::new( + http::Method::POST, + format!( + "{}/channels/{}/messages/{}/crosspost", + user.belongs_to.read().unwrap().urls.api, + channel_id, + message_id + ) + .as_str(), + None, + None, + None, + Some(user), + LimitType::Channel(channel_id), + ); + request.deserialize_response::(user).await + } + + /// Hides a message from the feed of the guild the channel belongs to. Returns a 204 empty response on success. + /// + /// # Reference: + /// See + pub async fn hide_from_guild_feed( + channel_id: Snowflake, + message_id: Snowflake, + user: &mut ChorusUser, + ) -> ChorusResult<()> { + let url = format!( + "{}/channels/{}/messages/{}/hide-guild-feed", + user.belongs_to.read().unwrap().urls.api, + channel_id, + message_id + ); + let chorus_request = ChorusRequest::new( + http::Method::DELETE, + &url, + None, + None, + None, + Some(user), + LimitType::Channel(channel_id), + ); + chorus_request.handle_request_as_result(user).await + } + + /// Edits a previously sent message. All fields can be edited by the original message author. + /// Other users can only edit flags and only if they have the MANAGE_MESSAGES permission in the corresponding channel. + /// When specifying flags, ensure to include all previously set flags/bits in addition to ones that you are modifying. + /// When the content field is edited, the mentions array in the message object will be reconstructed from scratch based on the new content. + /// The allowed_mentions field of the edit request controls how this happens. + /// If there is no explicit allowed_mentions in the edit request, the content will be parsed with default allowances, that is, + /// without regard to whether or not an allowed_mentions was present in the request that originally created the message. + /// + /// # Reference: + /// See: + pub async fn modify( + channel_id: Snowflake, + message_id: Snowflake, + schema: MessageModifySchema, + user: &mut ChorusUser, + ) -> ChorusResult { + let url = format!( + "{}/channels/{}/messages/{}", + user.belongs_to.read().unwrap().urls.api, + channel_id, + message_id + ); + let chorus_request = ChorusRequest::new( + http::Method::PATCH, + &url, + Some(to_string(&schema).unwrap()), + None, + None, + Some(user), + LimitType::Channel(channel_id), + ); + chorus_request.deserialize_response::(user).await + } + + /// Deletes a message. If operating on a guild channel and trying to delete a message that was not sent by the current user, + /// this endpoint requires the `MANAGE_MESSAGES` permission. Returns a 204 empty response on success. + pub async fn delete( + channel_id: Snowflake, + message_id: Snowflake, + audit_log_reason: Option, + user: &mut ChorusUser, + ) -> ChorusResult<()> { + let url = format!( + "{}/channels/{}/messages/{}", + user.belongs_to.read().unwrap().urls.api, + channel_id, + message_id + ); + + let chorus_request = ChorusRequest::new( + http::Method::DELETE, + &url, + None, + audit_log_reason.as_deref(), + None, + Some(user), + LimitType::Channel(channel_id), + ); + + chorus_request.handle_request_as_result(user).await + } + + /// Deletes multiple messages in a single request. This endpoint can only be used on guild channels and requires the MANAGE_MESSAGES permission. + /// Returns a 204 empty response on success. + /// + /// **This endpoint will not delete messages older than 2 weeks, and will fail if any message provided is older than that or if any duplicate message IDs are provided.** + /// + /// **This endpoint is not usable by user accounts.** (At least according to Discord.com. Spacebar behaviour may differ.) + /// + /// # Reference: + /// See: + pub async fn bulk_delete( + channel_id: Snowflake, + messages: Vec, + audit_log_reason: Option, + user: &mut ChorusUser, + ) -> ChorusResult<()> { + if messages.len() < 2 { + return Err(ChorusError::InvalidArguments { + error: "`messages` must contain at least 2 entries.".to_string(), + }); + } + let request = ChorusRequest::new( + http::Method::POST, + format!( + "{}/channels/{}/messages/bulk-delete", + user.belongs_to.read().unwrap().urls.api, + channel_id, + ) + .as_str(), + Some(to_string(&messages).unwrap()), + audit_log_reason.as_deref(), + None, + Some(user), + LimitType::Channel(channel_id), + ); + request.handle_request_as_result(user).await + } + + /// Acknowledges the currently pinned messages in a channel. Returns a 204 empty response on success. + /// + /// # Reference: + /// See: + pub async fn acknowledge_pinned( + channel_id: Snowflake, + user: &mut ChorusUser, + ) -> ChorusResult<()> { + let chorus_request = ChorusRequest::new( + http::Method::POST, + format!( + "{}/channels/{}/pins/ack", + user.belongs_to.read().unwrap().urls.api, + channel_id, + ) + .as_str(), + None, + None, + None, + Some(user), + LimitType::Channel(channel_id), + ); + + chorus_request.handle_request_as_result(user).await + } } -impl UserMeta { +fn search_error(result_text: String) -> ChorusError { + ChorusError::InvalidResponse { + error: format!( + "Got unexpected Response, or Response which is not valid JSON. Response: \n{}", + result_text + ), + } +} + +impl ChorusUser { + /// Sends a message in the channel with the provided channel_id. + /// Returns the sent message. + /// + /// # Notes + /// Shorthand call for [`Message::send`] + /// + /// # Reference + /// See pub async fn send_message( &mut self, message: MessageSendSchema, channel_id: Snowflake, - ) -> Result { + ) -> ChorusResult { Message::send(self, channel_id, message).await } } + +impl Channel { + /// Returns messages without the reactions key that match a search query in the channel. + /// The messages that are direct results will have an extra hit key set to true. + /// If operating on a guild channel, this endpoint requires the `READ_MESSAGE_HISTORY` + /// permission to be present on the current user. + /// + /// If the guild/channel you are searching is not yet indexed, the endpoint will return a 202 accepted response. + /// In this case, the method will return a [`ChorusError::InvalidResponse`] error. + /// + /// # Reference: + /// See + pub async fn search_messages( + channel_id: Snowflake, + query: MessageSearchQuery, + user: &mut ChorusUser, + ) -> ChorusResult> { + Message::search(MessageSearchEndpoint::Channel(channel_id), query, user).await + } +} diff --git a/src/api/channels/permissions.rs b/src/api/channels/permissions.rs index bc666ff..5360890 100644 --- a/src/api/channels/permissions.rs +++ b/src/api/channels/permissions.rs @@ -2,33 +2,32 @@ use reqwest::Client; use serde_json::to_string; use crate::{ - api::LimitType, errors::{ChorusError, ChorusResult}, - instance::UserMeta, + instance::ChorusUser, ratelimiter::ChorusRequest, - types::{self, PermissionOverwrite, Snowflake}, + types::{self, LimitType, PermissionOverwrite, Snowflake}, }; impl types::Channel { - /// Edits the permission overwrites for a channel. + /// Edits the permission overwrites for a user or role in a channel. /// - /// # Arguments + /// Only usable for guild channels. /// - /// * `user` - A mutable reference to a [`UserMeta`] instance. - /// * `channel_id` - A string slice representing the ID of the channel. - /// * `overwrite` - A [`PermissionOverwrite`] instance representing the new permission overwrites. + /// Requires the [`MANAGE_ROLES`](crate::types::PermissionFlags::MANAGE_ROLES) permission. + /// Only permissions you have in the guild or parent channel (if applicable) can be allowed/denied + /// (unless you have a [`MANAGE_ROLES`](crate::types::PermissionFlags::MANAGE_ROLES) overwrite in the channel). /// - /// # Returns - /// - /// This function returns a result that is either [`Ok(())`] if the request is successful, or an [`Err(ChorusLibError)`]. - pub async fn edit_permissions( - user: &mut UserMeta, + /// # Reference + /// See + pub async fn modify_permissions( + user: &mut ChorusUser, channel_id: Snowflake, + audit_log_reason: Option, overwrite: PermissionOverwrite, ) -> ChorusResult<()> { let url = format!( "{}/channels/{}/permissions/{}", - user.belongs_to.borrow_mut().urls.api, + user.belongs_to.read().unwrap().urls.api, channel_id, overwrite.id ); @@ -40,39 +39,51 @@ impl types::Channel { }); } }; + let mut request = Client::new() + .put(url) + .header("Authorization", user.token()) + .header("Content-Type", "application/json") + .body(body); + if let Some(reason) = audit_log_reason { + request = request.header("X-Audit-Log-Reason", reason); + } let chorus_request = ChorusRequest { - request: Client::new().put(url).bearer_auth(user.token()).body(body), + request, limit_type: LimitType::Channel(channel_id), }; chorus_request.handle_request_as_result(user).await } - /// Deletes a permission overwrite for a channel. + /// Deletes a permission overwrite for a user or role in a channel. /// - /// # Arguments + /// Only usable for guild channels. /// - /// * `user` - A mutable reference to a [`UserMeta`] instance. - /// * `channel_id` - A string slice representing the ID of the channel. - /// * `overwrite_id` - A string slice representing the ID of the permission overwrite to delete. + /// Requires the [`MANAGE_ROLES`](crate::types::PermissionFlags::MANAGE_ROLES) permission. /// - /// # Returns - /// - /// This function returns a Result that is either [`Ok(())`] if the request is successfulm or an [`Err(ChorusLibError)`]. + /// # Reference + /// See pub async fn delete_permission( - user: &mut UserMeta, + user: &mut ChorusUser, channel_id: Snowflake, overwrite_id: Snowflake, ) -> ChorusResult<()> { let url = format!( "{}/channels/{}/permissions/{}", - user.belongs_to.borrow_mut().urls.api, + user.belongs_to.read().unwrap().urls.api, channel_id, overwrite_id ); - let chorus_request = ChorusRequest { - request: Client::new().delete(url).bearer_auth(user.token()), - limit_type: LimitType::Channel(channel_id), - }; - chorus_request.handle_request_as_result(user).await + + let request = ChorusRequest::new( + http::Method::DELETE, + &url, + None, + None, + None, + Some(user), + LimitType::Channel(channel_id), + ); + + request.handle_request_as_result(user).await } } diff --git a/src/api/channels/reactions.rs b/src/api/channels/reactions.rs index 35dbb94..94d3087 100644 --- a/src/api/channels/reactions.rs +++ b/src/api/channels/reactions.rs @@ -1,16 +1,12 @@ -use reqwest::Client; - use crate::{ - api::LimitType, errors::ChorusResult, - instance::UserMeta, + instance::ChorusUser, ratelimiter::ChorusRequest, - types::{self, PublicUser, Snowflake}, + types::{self, LimitType, PublicUser, Snowflake}, }; -/** -Useful metadata for working with [`types::Reaction`], bundled together nicely. - */ +/// Useful metadata for working with [`types::Reaction`], bundled together nicely. +#[derive(Debug, Clone, PartialEq, Eq)] pub struct ReactionMeta { pub message_id: types::Snowflake, pub channel_id: types::Snowflake, @@ -18,169 +14,189 @@ pub struct ReactionMeta { impl ReactionMeta { /// Deletes all reactions for a message. - /// This endpoint requires the `MANAGE_MESSAGES` permission to be present on the current user. - /// # Arguments - /// * `user` - A mutable reference to a [`UserMeta`] instance. - /// # Returns - /// A `Result` [`()`] [`crate::errors::ChorusLibError`] if something went wrong. - /// Fires a `Message Reaction Remove All` Gateway event. + /// + /// This endpoint requires the [`MANAGE_MESSAGES`](crate::types::PermissionFlags::MANAGE_MESSAGES) permission. + /// /// # Reference - /// See [https://discord.com/developers/docs/resources/channel#delete-all-reactions](https://discord.com/developers/docs/resources/channel#delete-all-reactions) - pub async fn delete_all(&self, user: &mut UserMeta) -> ChorusResult<()> { + /// See + pub async fn delete_all(&self, user: &mut ChorusUser) -> ChorusResult<()> { let url = format!( - "{}/channels/{}/messages/{}/reactions/", - user.belongs_to.borrow().urls.api, + "{}/channels/{}/messages/{}/reactions", + user.belongs_to.read().unwrap().urls.api, self.channel_id, self.message_id ); - let chorus_request = ChorusRequest { - request: Client::new().delete(url).bearer_auth(user.token()), - limit_type: LimitType::Channel(self.channel_id), - }; - chorus_request.handle_request_as_result(user).await + + let request = ChorusRequest::new( + http::Method::DELETE, + &url, + None, + None, + None, + Some(user), + LimitType::Channel(self.channel_id), + ); + + request.handle_request_as_result(user).await } /// Gets a list of users that reacted with a specific emoji to a message. - /// # Arguments - /// * `emoji` - A string slice containing the emoji to search for. The emoji must be URL Encoded or - /// the request will fail with 10014: Unknown Emoji. To use custom emoji, you must encode it in the - /// format name:id with the emoji name and emoji id. - /// * `user` - A mutable reference to a [`UserMeta`] instance. - /// # Returns - /// A Result that is [`Err(crate::errors::ChorusLibError)`] if something went wrong. - /// # Reference - /// See [https://discord.com/developers/docs/resources/channel#get-reactions](https://discord.com/developers/docs/resources/channel#get-reactions) - pub async fn get(&self, emoji: &str, user: &mut UserMeta) -> ChorusResult> { - let url = format!( - "{}/channels/{}/messages/{}/reactions/{}/", - user.belongs_to.borrow().urls.api, - self.channel_id, - self.message_id, - emoji - ); - let chorus_request = ChorusRequest { - request: Client::new().get(url).bearer_auth(user.token()), - limit_type: LimitType::Channel(self.channel_id), - }; - chorus_request - .deserialize_response::>(user) - .await - } - - /// Deletes all the reactions for a given `emoji` on a message. This endpoint requires the - /// MANAGE_MESSAGES permission to be present on the current user. - /// # Arguments - /// * `emoji` - A string slice containing the emoji to delete. The `emoji` must be URL Encoded or - /// the request will fail with 10014: Unknown Emoji. To use custom emoji, you must encode it in the - /// format name:id with the emoji name and emoji id. - /// * `user` - A mutable reference to a [`UserMeta`] instance. - /// # Returns - /// A Result that is [`Err(crate::errors::ChorusLibError)`] if something went wrong. - /// Fires a `Message Reaction Remove Emoji` Gateway event. - /// # Reference - /// See [https://discord.com/developers/docs/resources/channel#delete-all-reactions-for-emoji](https://discord.com/developers/docs/resources/channel#delete-all-reactions-for-emoji) - pub async fn delete_emoji(&self, emoji: &str, user: &mut UserMeta) -> ChorusResult<()> { - let url = format!( - "{}/channels/{}/messages/{}/reactions/{}/", - user.belongs_to.borrow().urls.api, - self.channel_id, - self.message_id, - emoji - ); - let chorus_request = ChorusRequest { - request: Client::new().delete(url).bearer_auth(user.token()), - limit_type: LimitType::Channel(self.channel_id), - }; - chorus_request.handle_request_as_result(user).await - } - - /// Create a reaction for the message. - /// This endpoint requires the READ_MESSAGE_HISTORY permission - /// to be present on the current user. Additionally, if nobody else has reacted to the message using - /// this emoji, this endpoint requires the ADD_REACTIONS permission to be present on the current - /// user. - /// # Arguments - /// * `emoji` - A string slice containing the emoji to delete. The `emoji` must be URL Encoded or - /// the request will fail with 10014: Unknown Emoji. To use custom emoji, you must encode it in the - /// format name:id with the emoji name and emoji id. - /// * `user` - A mutable reference to a [`UserMeta`] instance. - /// # Returns - /// A `Result` containing [`()`] or a [`crate::errors::ChorusLibError`]. - /// # Reference - /// See [https://discord.com/developers/docs/resources/channel#create-reaction](https://discord.com/developers/docs/resources/channel#create-reaction) /// - pub async fn create(&self, emoji: &str, user: &mut UserMeta) -> ChorusResult<()> { + /// The emoji must be URL Encoded or the request will fail with 10014: Unknown Emoji. + /// To use custom emoji, the format of the emoji string must be name:id. + /// + /// # Reference + /// See + pub async fn get(&self, emoji: &str, user: &mut ChorusUser) -> ChorusResult> { let url = format!( - "{}/channels/{}/messages/{}/reactions/{}/@me/", - user.belongs_to.borrow().urls.api, + "{}/channels/{}/messages/{}/reactions/{}", + user.belongs_to.read().unwrap().urls.api, self.channel_id, self.message_id, emoji ); - let chorus_request = ChorusRequest { - request: Client::new().put(url).bearer_auth(user.token()), - limit_type: LimitType::Channel(self.channel_id), - }; - chorus_request.handle_request_as_result(user).await + + let request = ChorusRequest::new( + http::Method::GET, + &url, + None, + None, + None, + Some(user), + LimitType::Channel(self.channel_id), + ); + + request.deserialize_response::>(user).await } - /// Delete a reaction the current user has made for the message. - /// # Arguments - /// * `emoji` - A string slice containing the emoji to delete. The `emoji` must be URL Encoded or - /// the request will fail with 10014: Unknown Emoji. To use custom emoji, you must encode it in the - /// format name:id with the emoji name and emoji id. - /// * `user` - A mutable reference to a [`UserMeta`] instance. - /// # Returns - /// A `Result` containing [`()`] or a [`crate::errors::ChorusLibError`]. - /// Fires a `Message Reaction Remove` Gateway event. + /// Deletes all the reactions for a given emoji on a message. + /// + /// This endpoint requires the [`MANAGE_MESSAGES`](crate::types::PermissionFlags::MANAGE_MESSAGES) permission. + /// + /// The emoji must be URL Encoded or the request will fail with 10014: Unknown Emoji. + /// To use custom emoji, the format of the emoji string must be name:id. + /// /// # Reference - /// See [https://discord.com/developers/docs/resources/channel#delete-own-reaction](https://discord.com/developers/docs/resources/channel#delete-own-reaction) - pub async fn remove(&self, emoji: &str, user: &mut UserMeta) -> ChorusResult<()> { + /// See + pub async fn delete_emoji(&self, emoji: &str, user: &mut ChorusUser) -> ChorusResult<()> { let url = format!( - "{}/channels/{}/messages/{}/reactions/{}/@me/", - user.belongs_to.borrow().urls.api, + "{}/channels/{}/messages/{}/reactions/{}", + user.belongs_to.read().unwrap().urls.api, self.channel_id, self.message_id, emoji ); - let chorus_request = ChorusRequest { - request: Client::new().delete(url).bearer_auth(user.token()), - limit_type: LimitType::Channel(self.channel_id), - }; - chorus_request.handle_request_as_result(user).await + + let request = ChorusRequest::new( + http::Method::DELETE, + &url, + None, + None, + None, + Some(user), + LimitType::Channel(self.channel_id), + ); + + request.handle_request_as_result(user).await } - /// Delete a user's reaction to a message. - /// This endpoint requires the MANAGE_MESSAGES permission to be present on the current user. - /// # Arguments - /// * `user_id` - ID of the user whose reaction is to be deleted. - /// * `emoji` - A string slice containing the emoji to delete. The `emoji` must be URL Encoded or - /// the request will fail with 10014: Unknown Emoji. To use custom emoji, you must encode it in the - /// format name:id with the emoji name and emoji id. - /// * `user` - A mutable reference to a [`UserMeta`] instance. - /// # Returns - /// A `Result` containing [`()`] or a [`crate::errors::ChorusLibError`]. - /// Fires a Message Reaction Remove Gateway event. + /// Create a reaction on a message. + /// + /// This endpoint requires the [`READ_MESSAGE_HISTORY`](crate::types::PermissionFlags::READ_MESSAGE_HISTORY) permission. + /// + /// Additionally, if nobody else has reacted to the message using this emoji, + /// this endpoint requires the [`ADD_REACTIONS`](crate::types::PermissionFlags::ADD_REACTIONS) permission. + /// + /// The emoji must be URL Encoded or the request will fail with 10014: Unknown Emoji. + /// To use custom emoji, the format of the emoji string must be `name:id`. + /// /// # Reference - /// See [https://discord.com/developers/docs/resources/channel#delete-own-reaction](https://discord.com/developers/docs/resources/channel#delete-own-reaction) + /// See + pub async fn create(&self, emoji: &str, user: &mut ChorusUser) -> ChorusResult<()> { + let url = format!( + "{}/channels/{}/messages/{}/reactions/{}/@me", + user.belongs_to.read().unwrap().urls.api, + self.channel_id, + self.message_id, + emoji + ); + + let request = ChorusRequest::new( + http::Method::PUT, + &url, + None, + None, + None, + Some(user), + LimitType::Channel(self.channel_id), + ); + + request.handle_request_as_result(user).await + } + + /// Deletes a reaction the current user has made to the message. + /// + /// The reaction emoji must be URL Encoded or the request will fail with 10014: Unknown Emoji. + /// To use custom emoji, the format of the emoji string must be name:id. + /// + /// # Reference + /// See + pub async fn remove(&self, emoji: &str, user: &mut ChorusUser) -> ChorusResult<()> { + let url = format!( + "{}/channels/{}/messages/{}/reactions/{}/@me", + user.belongs_to.read().unwrap().urls.api, + self.channel_id, + self.message_id, + emoji + ); + + let request = ChorusRequest::new( + http::Method::DELETE, + &url, + None, + None, + None, + Some(user), + LimitType::Channel(self.channel_id), + ); + + request.handle_request_as_result(user).await + } + + /// Deletes a user's reaction to a message. + /// + /// This endpoint requires the [`MANAGE_MESSAGES`](crate::types::PermissionFlags::MANAGE_MESSAGES) permission. + /// + /// The reaction emoji must be URL Encoded or the request will fail with 10014: Unknown Emoji. + /// To use custom emoji, the format of the emoji string must be name:id. + /// + /// # Reference + /// See pub async fn delete_user( &self, user_id: Snowflake, emoji: &str, - user: &mut UserMeta, + user: &mut ChorusUser, ) -> ChorusResult<()> { let url = format!( "{}/channels/{}/messages/{}/reactions/{}/{}", - user.belongs_to.borrow().urls.api, + user.belongs_to.read().unwrap().urls.api, self.channel_id, self.message_id, emoji, user_id ); - let chorus_request = ChorusRequest { - request: Client::new().delete(url).bearer_auth(user.token()), - limit_type: LimitType::Channel(self.channel_id), - }; - chorus_request.handle_request_as_result(user).await + + let request = ChorusRequest::new( + http::Method::DELETE, + &url, + None, + None, + None, + Some(user), + LimitType::Channel(self.channel_id), + ); + + request.handle_request_as_result(user).await } } diff --git a/src/api/guilds/guilds.rs b/src/api/guilds/guilds.rs index 3654698..b433c84 100644 --- a/src/api/guilds/guilds.rs +++ b/src/api/guilds/guilds.rs @@ -2,57 +2,41 @@ use reqwest::Client; use serde_json::from_str; use serde_json::to_string; -use crate::api::LimitType; use crate::errors::ChorusError; use crate::errors::ChorusResult; -use crate::instance::UserMeta; +use crate::instance::ChorusUser; use crate::ratelimiter::ChorusRequest; -use crate::types::Snowflake; -use crate::types::{Channel, ChannelCreateSchema, Guild, GuildCreateSchema}; +use crate::types::{ + Channel, ChannelCreateSchema, Guild, GuildBanCreateSchema, GuildBansQuery, GuildCreateSchema, + GuildMember, GuildMemberSearchSchema, GuildModifySchema, GuildPreview, LimitType, + ModifyGuildMemberProfileSchema, ModifyGuildMemberSchema, UserProfileMetadata, +}; +use crate::types::{GuildBan, Snowflake}; impl Guild { - /// Creates a new guild with the given parameters. - /// - /// # Arguments - /// - /// * `user` - A mutable reference to the user creating the guild. - /// * `instance` - A mutable reference to the instance where the guild will be created. - /// * `guild_create_schema` - A reference to the schema containing the guild creation parameters. - /// - /// # Returns - /// - /// A `Result` containing the object of the newly created guild, or an error if the request fails. - /// - /// # Errors - /// - /// Returns an `ChorusLibError` if the request fails. + /// Creates a new guild. /// + /// # Reference + /// See pub async fn create( - user: &mut UserMeta, + user: &mut ChorusUser, guild_create_schema: GuildCreateSchema, ) -> ChorusResult { - let url = format!("{}/guilds/", user.belongs_to.borrow().urls.api); + let url = format!("{}/guilds", user.belongs_to.read().unwrap().urls.api); let chorus_request = ChorusRequest { request: Client::new() .post(url.clone()) - .bearer_auth(user.token.clone()) + .header("Authorization", user.token.clone()) + .header("Content-Type", "application/json") .body(to_string(&guild_create_schema).unwrap()), limit_type: LimitType::Global, }; chorus_request.deserialize_response::(user).await } - /// Deletes a guild. + /// Deletes a guild by its id. /// - /// # Arguments - /// - /// * `user` - A mutable reference to a `User` instance. - /// * `instance` - A mutable reference to an `Instance` instance. - /// * `guild_id` - ID of the guild to delete. - /// - /// # Returns - /// - /// An `Result` containing an `ChorusLibError` if an error occurred during the request, otherwise `()`. + /// User must be the owner. /// /// # Example /// @@ -61,65 +45,63 @@ impl Guild { /// let mut instance = Instance::new(); /// let guild_id = String::from("1234567890"); /// - /// match Guild::delete(&mut user, &mut instance, guild_id) { - /// Some(e) => println!("Error deleting guild: {:?}", e), - /// None => println!("Guild deleted successfully"), + /// match Guild::delete(&mut user, guild_id) { + /// Err(e) => println!("Error deleting guild: {:?}", e), + /// Ok(_) => println!("Guild deleted successfully"), /// } /// ``` - pub async fn delete(user: &mut UserMeta, guild_id: Snowflake) -> ChorusResult<()> { + /// + /// # Reference + /// See + pub async fn delete(user: &mut ChorusUser, guild_id: Snowflake) -> ChorusResult<()> { let url = format!( - "{}/guilds/{}/delete/", - user.belongs_to.borrow().urls.api, + "{}/guilds/{}/delete", + user.belongs_to.read().unwrap().urls.api, guild_id ); let chorus_request = ChorusRequest { request: Client::new() .post(url.clone()) - .bearer_auth(user.token.clone()), + .header("Authorization", user.token.clone()) + .header("Content-Type", "application/json"), limit_type: LimitType::Global, }; chorus_request.handle_request_as_result(user).await } - /// Sends a request to create a new channel in the guild. + /// Creates a new channel in a guild. /// - /// # Arguments + /// Requires the [MANAGE_CHANNELS](crate::types::PermissionFlags::MANAGE_CHANNELS) permission. /// - /// * `url_api` - The base URL for the Discord API. - /// * `token` - A Discord bot token. - /// * `schema` - A `ChannelCreateSchema` struct containing the properties of the new channel. - /// * `limits_user` - A mutable reference to a `Limits` struct containing the user's rate limits. - /// * `limits_instance` - A mutable reference to a `Limits` struct containing the instance's rate limits. + /// # Notes + /// This method is a wrapper for [Channel::create]. /// - /// # Returns - /// - /// A `Result` containing a `reqwest::Response` if the request was successful, or an `ChorusLibError` if there was an error. + /// # Reference + /// See pub async fn create_channel( &self, - user: &mut UserMeta, + user: &mut ChorusUser, + audit_log_reason: Option, schema: ChannelCreateSchema, ) -> ChorusResult { - Channel::create(user, self.id, schema).await + Channel::create(user, self.id, audit_log_reason, schema).await } - /// Returns a `Result` containing a vector of `Channel` structs if the request was successful, or an `ChorusLibError` if there was an error. + /// Returns a list of the guild's channels. /// - /// # Arguments + /// Doesn't include threads. /// - /// * `url_api` - A string slice that holds the URL of the API. - /// * `token` - A string slice that holds the authorization token. - /// * `limits_user` - A mutable reference to a `Limits` struct containing the user's rate limits. - /// * `limits_instance` - A mutable reference to a `Limits` struct containing the instance's rate limits. - /// - pub async fn channels(&self, user: &mut UserMeta) -> ChorusResult> { + /// # Reference + /// See + pub async fn channels(&self, user: &mut ChorusUser) -> ChorusResult> { let chorus_request = ChorusRequest { request: Client::new() .get(format!( - "{}/guilds/{}/channels/", - user.belongs_to.borrow().urls.api, + "{}/guilds/{}/channels", + user.belongs_to.read().unwrap().urls.api, self.id )) - .bearer_auth(user.token()), + .header("Authorization", user.token()), limit_type: LimitType::Channel(self.id), }; let result = chorus_request.send_request(user).await?; @@ -141,61 +123,381 @@ impl Guild { }; } - /// Returns a `Result` containing a `Guild` struct if the request was successful, or an `ChorusLibError` if there was an error. + /// Fetches a guild by its id. /// - /// # Arguments - /// - /// * `url_api` - A string slice that holds the URL of the API. - /// * `guild_id` - ID of the guild. - /// * `token` - A string slice that holds the authorization token. - /// * `limits_user` - A mutable reference to a `Limits` struct containing the user's rate limits. - /// * `limits_instance` - A mutable reference to a `Limits` struct containing the instance's rate limits. - /// - pub async fn get(guild_id: Snowflake, user: &mut UserMeta) -> ChorusResult { + /// # Reference + /// See + pub async fn get(guild_id: Snowflake, user: &mut ChorusUser) -> ChorusResult { let chorus_request = ChorusRequest { request: Client::new() .get(format!( - "{}/guilds/{}/", - user.belongs_to.borrow().urls.api, + "{}/guilds/{}", + user.belongs_to.read().unwrap().urls.api, guild_id )) - .bearer_auth(user.token()), + .header("Authorization", user.token()), limit_type: LimitType::Guild(guild_id), }; let response = chorus_request.deserialize_response::(user).await?; Ok(response) } + + pub async fn create_ban( + guild_id: Snowflake, + user_id: Snowflake, + audit_log_reason: Option, + schema: GuildBanCreateSchema, + user: &mut ChorusUser, + ) -> ChorusResult<()> { + // FIXME: Return GuildBan instead of (). Requires to be resolved. + let request = ChorusRequest::new( + http::Method::PUT, + format!( + "{}/guilds/{}/bans/{}", + user.belongs_to.read().unwrap().urls.api, + guild_id, + user_id + ) + .as_str(), + Some(to_string(&schema).unwrap()), + audit_log_reason.as_deref(), + None, + Some(user), + LimitType::Guild(guild_id), + ); + request.handle_request_as_result(user).await + } + + /// # Reference + /// + pub async fn modify( + guild_id: Snowflake, + schema: GuildModifySchema, + user: &mut ChorusUser, + ) -> ChorusResult { + let chorus_request = ChorusRequest { + request: Client::new() + .patch(format!( + "{}/guilds/{}", + user.belongs_to.read().unwrap().urls.api, + guild_id, + )) + .header("Authorization", user.token()) + .header("Content-Type", "application/json") + .body(to_string(&schema).unwrap()), + limit_type: LimitType::Guild(guild_id), + }; + let response = chorus_request.deserialize_response::(user).await?; + Ok(response) + } + + /// Returns a guild preview object for the given guild ID. If the user is not in the guild, the guild must be discoverable. + /// # Reference: + /// + /// See + pub async fn get_preview( + guild_id: Snowflake, + user: &mut ChorusUser, + ) -> ChorusResult { + let chorus_request = ChorusRequest { + request: Client::new() + .patch(format!( + "{}/guilds/{}/preview", + user.belongs_to.read().unwrap().urls.api, + guild_id, + )) + .header("Authorization", user.token()) + .header("Content-Type", "application/json"), + limit_type: LimitType::Guild(guild_id), + }; + let response = chorus_request + .deserialize_response::(user) + .await?; + Ok(response) + } + + /// Returns a list of guild member objects that are members of the guild. + /// + /// # Reference + /// See + pub async fn get_members( + guild_id: Snowflake, + user: &mut ChorusUser, + ) -> ChorusResult> { + let request = ChorusRequest::new( + http::Method::GET, + format!( + "{}/guilds/{}/members", + user.belongs_to.read().unwrap().urls.api, + guild_id, + ) + .as_str(), + None, + None, + None, + Some(user), + LimitType::Guild(guild_id), + ); + request.deserialize_response::>(user).await + } + + /// Returns a list of guild member objects whose username or nickname starts with a provided string. + /// + /// # Reference: + /// See + pub async fn search_members( + guild_id: Snowflake, + query: GuildMemberSearchSchema, + user: &mut ChorusUser, + ) -> ChorusResult> { + let mut request = ChorusRequest::new( + http::Method::GET, + format!( + "{}/guilds/{}/members/search", + user.belongs_to.read().unwrap().urls.api, + guild_id, + ) + .as_str(), + None, + None, + None, + Some(user), + LimitType::Guild(guild_id), + ); + request.request = request + .request + .query(&[("query", to_string(&query).unwrap())]); + request.deserialize_response::>(user).await + } + + /// Removes a member from a guild. Requires the KICK_MEMBERS permission. Returns a 204 empty response on success. + /// + /// # Reference + /// See + pub async fn remove_member( + guild_id: Snowflake, + member_id: Snowflake, + audit_log_reason: Option, + user: &mut ChorusUser, + ) -> ChorusResult<()> { + let request = ChorusRequest::new( + http::Method::DELETE, + format!( + "{}/guilds/{}/members/{}", + user.belongs_to.read().unwrap().urls.api, + guild_id, + member_id, + ) + .as_str(), + None, + audit_log_reason.as_deref(), + None, + Some(user), + LimitType::Guild(guild_id), + ); + request.handle_request_as_result(user).await + } + + /// Modifies attributes of a guild member. Returns the updated guild member object on success. + /// For required Permissions and an API reference, see: + /// + /// # Reference: + /// + pub async fn modify_member( + guild_id: Snowflake, + member_id: Snowflake, + schema: ModifyGuildMemberSchema, + audit_log_reason: Option, + user: &mut ChorusUser, + ) -> ChorusResult { + let request = ChorusRequest::new( + http::Method::PATCH, + format!( + "{}/guilds/{}/members/{}", + user.belongs_to.read().unwrap().urls.api, + guild_id, + member_id, + ) + .as_str(), + Some(to_string(&schema).unwrap()), + audit_log_reason.as_deref(), + None, + Some(user), + LimitType::Guild(guild_id), + ); + request.deserialize_response::(user).await + } + + /// Modifies the current user's member in the guild. + /// + /// # Reference: + /// See + pub async fn modify_current_member( + guild_id: Snowflake, + schema: ModifyGuildMemberSchema, + audit_log_reason: Option, + user: &mut ChorusUser, + ) -> ChorusResult { + let request = ChorusRequest::new( + http::Method::PATCH, + format!( + "{}/guilds/{}/members/@me", + user.belongs_to.read().unwrap().urls.api, + guild_id, + ) + .as_str(), + Some(to_string(&schema).unwrap()), + audit_log_reason.as_deref(), + None, + Some(user), + LimitType::Guild(guild_id), + ); + request.deserialize_response::(user).await + } + + /// Modifies the current user's profile in the guild. + /// + /// # Reference: + /// See + pub async fn modify_current_member_profile( + guild_id: Snowflake, + schema: ModifyGuildMemberProfileSchema, + user: &mut ChorusUser, + ) -> ChorusResult { + let request = ChorusRequest::new( + http::Method::PATCH, + format!( + "{}/guilds/{}/profile/@me", + user.belongs_to.read().unwrap().urls.api, + guild_id, + ) + .as_str(), + Some(to_string(&schema).unwrap()), + None, + None, + Some(user), + LimitType::Guild(guild_id), + ); + request + .deserialize_response::(user) + .await + } + + /// Returns a list of ban objects for the guild. Requires the `BAN_MEMBERS` permission. + /// + /// # Reference: + /// See + pub async fn get_bans( + user: &mut ChorusUser, + guild_id: Snowflake, + query: Option, + ) -> ChorusResult> { + let url = format!( + "{}/guilds/{}/bans", + user.belongs_to.read().unwrap().urls.api, + guild_id, + ); + + let mut request = ChorusRequest::new( + http::Method::GET, + &url, + None, + None, + None, + Some(user), + LimitType::Guild(guild_id), + ); + if let Some(query) = query { + request.request = request.request.query(&to_string(&query).unwrap()); + } + request.deserialize_response::>(user).await + } + + /// Returns a ban object for the given user. Requires the `BAN_MEMBERS` permission. + /// + /// # Reference: + /// See + pub async fn get_ban( + user: &mut ChorusUser, + guild_id: Snowflake, + user_id: Snowflake, + ) -> ChorusResult { + let url = format!( + "{}/guilds/{}/bans/{}", + user.belongs_to.read().unwrap().urls.api, + guild_id, + user_id + ); + + let request = ChorusRequest::new( + http::Method::GET, + &url, + None, + None, + None, + Some(user), + LimitType::Guild(guild_id), + ); + request.deserialize_response::(user).await + } + + /// Removes the ban for a user. Requires the BAN_MEMBERS permissions. Returns a 204 empty response on success. + /// + /// # Reference: + /// See + pub async fn delete_ban( + user: &mut ChorusUser, + guild_id: Snowflake, + user_id: Snowflake, + audit_log_reason: Option, + ) -> ChorusResult<()> { + let url = format!( + "{}/guilds/{}/bans/{}", + user.belongs_to.read().unwrap().urls.api, + guild_id, + user_id + ); + + let request = ChorusRequest::new( + http::Method::DELETE, + &url, + None, + audit_log_reason.as_deref(), + None, + Some(user), + LimitType::Guild(guild_id), + ); + request.handle_request_as_result(user).await + } } impl Channel { - /// Sends a request to create a new channel in a guild. + /// Creates a new channel in a guild. /// - /// # Arguments + /// Requires the [MANAGE_CHANNELS](crate::types::PermissionFlags::MANAGE_CHANNELS) permission. /// - /// * `token` - A Discord bot token. - /// * `url_api` - The base URL for the Discord API. - /// * `guild_id` - The ID of the guild where the channel will be created. - /// * `schema` - A `ChannelCreateSchema` struct containing the properties of the new channel. - /// * `limits_user` - A mutable reference to a `Limits` struct containing the user's rate limits. - /// * `limits_instance` - A mutable reference to a `Limits` struct containing the instance's rate limits. - /// - /// # Returns - /// - /// A `Result` containing a `reqwest::Response` if the request was successful, or an `ChorusLibError` if there was an error. + /// # Reference + /// See pub async fn create( - user: &mut UserMeta, + user: &mut ChorusUser, guild_id: Snowflake, + audit_log_reason: Option, schema: ChannelCreateSchema, ) -> ChorusResult { + let mut request = Client::new() + .post(format!( + "{}/guilds/{}/channels", + user.belongs_to.read().unwrap().urls.api, + guild_id + )) + .header("Authorization", user.token()) + .header("Content-Type", "application/json") + .body(to_string(&schema).unwrap()); + if let Some(reason) = audit_log_reason { + request = request.header("X-Audit-Log-Reason", reason); + } let chorus_request = ChorusRequest { - request: Client::new() - .post(format!( - "{}/guilds/{}/channels/", - user.belongs_to.borrow().urls.api, - guild_id - )) - .bearer_auth(user.token()) - .body(to_string(&schema).unwrap()), + request, limit_type: LimitType::Guild(guild_id), }; chorus_request.deserialize_response::(user).await diff --git a/src/api/guilds/member.rs b/src/api/guilds/member.rs index 5fa99cd..885ddf9 100644 --- a/src/api/guilds/member.rs +++ b/src/api/guilds/member.rs @@ -1,72 +1,61 @@ use reqwest::Client; use crate::{ - api::LimitType, errors::ChorusResult, - instance::UserMeta, + instance::ChorusUser, ratelimiter::ChorusRequest, - types::{self, Snowflake}, + types::{self, GuildMember, LimitType, Snowflake}, }; impl types::GuildMember { - /// Retrieves a guild member by their ID. + /// Retrieves a guild member. /// - /// # Arguments - /// - /// * `user` - A mutable reference to a [`UserMeta`] instance. - /// * `guild_id` - The ID of the guild. - /// * `member_id` - The ID of the member. - /// - /// # Returns - /// - /// A [`Result`] containing a [`GuildMember`] if the request succeeds, or a [`ChorusLibError`] if the request fails. + /// # Reference + /// See pub async fn get( - user: &mut UserMeta, + user: &mut ChorusUser, guild_id: Snowflake, member_id: Snowflake, - ) -> ChorusResult { + ) -> ChorusResult { let url = format!( - "{}/guilds/{}/members/{}/", - user.belongs_to.borrow().urls.api, + "{}/guilds/{}/members/{}", + user.belongs_to.read().unwrap().urls.api, guild_id, member_id ); let chorus_request = ChorusRequest { - request: Client::new().get(url).bearer_auth(user.token()), + request: Client::new().get(url).header("Authorization", user.token()), limit_type: LimitType::Guild(guild_id), }; chorus_request - .deserialize_response::(user) + .deserialize_response::(user) .await } /// Adds a role to a guild member. /// - /// # Arguments + /// Requires the [`MANAGE_ROLES`](crate::types::PermissionFlags::MANAGE_ROLES) permission. /// - /// * `user` - A mutable reference to a `UserMeta` instance. - /// * `guild_id` - The ID of the guild. - /// * `member_id` - The ID of the member. - /// * `role_id` - The ID of the role to add. - /// - /// # Returns - /// - /// An `Result` containing a `ChorusLibError` if the request fails, or `()` if the request succeeds. + /// # Reference + /// See pub async fn add_role( - user: &mut UserMeta, + user: &mut ChorusUser, guild_id: Snowflake, member_id: Snowflake, role_id: Snowflake, ) -> ChorusResult<()> { let url = format!( - "{}/guilds/{}/members/{}/roles/{}/", - user.belongs_to.borrow().urls.api, + "{}/guilds/{}/members/{}/roles/{}", + user.belongs_to.read().unwrap().urls.api, guild_id, member_id, role_id ); let chorus_request = ChorusRequest { - request: Client::new().put(url).bearer_auth(user.token()), + request: Client::new() + .put(url) + .header("Authorization", user.token()) + .header("Content-Type", "application/json"), limit_type: LimitType::Guild(guild_id), }; chorus_request.handle_request_as_result(user).await @@ -74,31 +63,27 @@ impl types::GuildMember { /// Removes a role from a guild member. /// - /// # Arguments + /// Requires the [`MANAGE_ROLES`](crate::types::PermissionFlags::MANAGE_ROLES) permission. /// - /// * `user` - A mutable reference to a `UserMeta` instance. - /// * `guild_id` - The ID of the guild. - /// * `member_id` - The ID of the member. - /// * `role_id` - The ID of the role to remove. - /// - /// # Returns - /// - /// A `Result` containing a `ChorusLibError` if the request fails, or `()` if the request succeeds. + /// # Reference + /// See pub async fn remove_role( - user: &mut UserMeta, + user: &mut ChorusUser, guild_id: Snowflake, member_id: Snowflake, role_id: Snowflake, ) -> Result<(), crate::errors::ChorusError> { let url = format!( - "{}/guilds/{}/members/{}/roles/{}/", - user.belongs_to.borrow().urls.api, + "{}/guilds/{}/members/{}/roles/{}", + user.belongs_to.read().unwrap().urls.api, guild_id, member_id, role_id ); let chorus_request = ChorusRequest { - request: Client::new().delete(url).bearer_auth(user.token()), + request: Client::new() + .delete(url) + .header("Authorization", user.token()), limit_type: LimitType::Guild(guild_id), }; chorus_request.handle_request_as_result(user).await diff --git a/src/api/guilds/messages.rs b/src/api/guilds/messages.rs new file mode 100644 index 0000000..60fd4e3 --- /dev/null +++ b/src/api/guilds/messages.rs @@ -0,0 +1,28 @@ +use crate::errors::ChorusResult; +use crate::instance::ChorusUser; +use crate::types::{Guild, Message, MessageSearchQuery, Snowflake}; + +impl Guild { + /// Returns messages without the reactions key that match a search query in the guild. + /// The messages that are direct results will have an extra hit key set to true. + /// If operating on a guild channel, this endpoint requires the `READ_MESSAGE_HISTORY` + /// permission to be present on the current user. + /// + /// If the guild/channel you are searching is not yet indexed, the endpoint will return a 202 accepted response. + /// In this case, the method will return a [`ChorusError::InvalidResponse`] error. + /// + /// # Reference: + /// See + pub async fn search_messages( + guild_id: Snowflake, + query: MessageSearchQuery, + user: &mut ChorusUser, + ) -> ChorusResult> { + Message::search( + crate::types::MessageSearchEndpoint::GuildChannel(guild_id), + query, + user, + ) + .await + } +} diff --git a/src/api/guilds/mod.rs b/src/api/guilds/mod.rs index 7577479..f1fa039 100644 --- a/src/api/guilds/mod.rs +++ b/src/api/guilds/mod.rs @@ -1,7 +1,9 @@ pub use guilds::*; +pub use messages::*; pub use roles::*; pub use roles::*; pub mod guilds; pub mod member; +pub mod messages; pub mod roles; diff --git a/src/api/guilds/roles.rs b/src/api/guilds/roles.rs index 1d1bc68..f131367 100644 --- a/src/api/guilds/roles.rs +++ b/src/api/guilds/roles.rs @@ -2,79 +2,56 @@ use reqwest::Client; use serde_json::to_string; use crate::{ - api::LimitType, errors::{ChorusError, ChorusResult}, - instance::UserMeta, + instance::ChorusUser, ratelimiter::ChorusRequest, - types::{self, RoleCreateModifySchema, RoleObject, Snowflake}, + types::{ + self, LimitType, RoleCreateModifySchema, RoleObject, RolePositionUpdateSchema, Snowflake, + }, }; impl types::RoleObject { - /// Retrieves all roles for a given guild. + /// Retrieves a list of roles for a given guild. /// - /// # Arguments - /// - /// * `user` - A mutable reference to a [`UserMeta`] instance. - /// * `guild_id` - The ID of the guild to retrieve roles from. - /// - /// # Returns - /// - /// An `Option` containing a `Vec` of [`RoleObject`]s if roles were found, or `None` if no roles were found. - /// - /// # Errors - /// - /// Returns a [`ChorusLibError`] if the request fails or if the response is invalid. + /// # Reference + /// See pub async fn get_all( - user: &mut UserMeta, + user: &mut ChorusUser, guild_id: Snowflake, - ) -> ChorusResult>> { + ) -> ChorusResult> { let url = format!( - "{}/guilds/{}/roles/", - user.belongs_to.borrow().urls.api, + "{}/guilds/{}/roles", + user.belongs_to.read().unwrap().urls.api, guild_id ); let chorus_request = ChorusRequest { - request: Client::new().get(url).bearer_auth(user.token()), + request: Client::new().get(url).header("Authorization", user.token()), limit_type: LimitType::Guild(guild_id), }; let roles = chorus_request .deserialize_response::>(user) .await .unwrap(); - if roles.is_empty() { - return Ok(None); - } - Ok(Some(roles)) + Ok(roles) } /// Retrieves a single role for a given guild. /// - /// # Arguments - /// - /// * `user` - A mutable reference to a [`UserMeta`] instance. - /// * `guild_id` - The ID of the guild to retrieve the role from. - /// * `role_id` - The ID of the role to retrieve. - /// - /// # Returns - /// - /// A `Result` containing the retrieved [`RoleObject`] if successful, or a [`ChorusLibError`] if the request fails or if the response is invalid. - /// - /// # Errors - /// - /// Returns a [`ChorusLibError`] if the request fails or if the response is invalid. + /// # Reference + /// See pub async fn get( - user: &mut UserMeta, + user: &mut ChorusUser, guild_id: Snowflake, role_id: Snowflake, ) -> ChorusResult { let url = format!( - "{}/guilds/{}/roles/{}/", - user.belongs_to.borrow().urls.api, + "{}/guilds/{}/roles/{}", + user.belongs_to.read().unwrap().urls.api, guild_id, role_id ); let chorus_request = ChorusRequest { - request: Client::new().get(url).bearer_auth(user.token()), + request: Client::new().get(url).header("Authorization", user.token()), limit_type: LimitType::Guild(guild_id), }; chorus_request @@ -84,27 +61,18 @@ impl types::RoleObject { /// Creates a new role for a given guild. /// - /// # Arguments + /// Requires the [`MANAGE_ROLES`](crate::types::PermissionFlags::MANAGE_ROLES) permission. /// - /// * `user` - A mutable reference to a [`UserMeta`] instance. - /// * `guild_id` - The ID of the guild to create the role in. - /// * `role_create_schema` - A [`RoleCreateModifySchema`] instance containing the properties of the role to be created. - /// - /// # Returns - /// - /// A `Result` containing the newly created [`RoleObject`] if successful, or a [`ChorusLibError`] if the request fails or if the response is invalid. - /// - /// # Errors - /// - /// Returns a [`ChorusLibError`] if the request fails or if the response is invalid. + /// # Reference + /// See pub async fn create( - user: &mut UserMeta, + user: &mut ChorusUser, guild_id: Snowflake, role_create_schema: RoleCreateModifySchema, ) -> ChorusResult { let url = format!( - "{}/guilds/{}/roles/", - user.belongs_to.borrow().urls.api, + "{}/guilds/{}/roles", + user.belongs_to.read().unwrap().urls.api, guild_id ); let body = to_string::(&role_create_schema).map_err(|e| { @@ -113,7 +81,11 @@ impl types::RoleObject { } })?; let chorus_request = ChorusRequest { - request: Client::new().post(url).bearer_auth(user.token()).body(body), + request: Client::new() + .post(url) + .header("Authorization", user.token()) + .header("Content-Type", "application/json") + .body(body), limit_type: LimitType::Guild(guild_id), }; chorus_request @@ -121,29 +93,20 @@ impl types::RoleObject { .await } - /// Updates the position of a role in the guild's hierarchy. + /// Updates the position of a role in a given guild's hierarchy. /// - /// # Arguments + /// Requires the [`MANAGE_ROLES`](crate::types::PermissionFlags::MANAGE_ROLES) permission. /// - /// * `user` - A mutable reference to a [`UserMeta`] instance. - /// * `guild_id` - The ID of the guild to update the role position in. - /// * `role_position_update_schema` - A [`RolePositionUpdateSchema`] instance containing the new position of the role. - /// - /// # Returns - /// - /// A `Result` containing the updated [`RoleObject`] if successful, or a [`ChorusLibError`] if the request fails or if the response is invalid. - /// - /// # Errors - /// - /// Returns a [`ChorusLibError`] if the request fails or if the response is invalid. + /// # Reference + /// See pub async fn position_update( - user: &mut UserMeta, + user: &mut ChorusUser, guild_id: Snowflake, - role_position_update_schema: types::RolePositionUpdateSchema, + role_position_update_schema: RolePositionUpdateSchema, ) -> ChorusResult { let url = format!( - "{}/guilds/{}/roles/", - user.belongs_to.borrow().urls.api, + "{}/guilds/{}/roles", + user.belongs_to.read().unwrap().urls.api, guild_id ); let body = @@ -153,7 +116,8 @@ impl types::RoleObject { let chorus_request = ChorusRequest { request: Client::new() .patch(url) - .bearer_auth(user.token()) + .header("Authorization", user.token()) + .header("Content-Type", "application/json") .body(body), limit_type: LimitType::Guild(guild_id), }; @@ -162,31 +126,21 @@ impl types::RoleObject { .await } - /// Updates a role in a guild. + /// Modifies a role in a guild. /// - /// # Arguments + /// Requires the [`MANAGE_ROLES`](crate::types::PermissionFlags::MANAGE_ROLES) permission. /// - /// * `user` - A mutable reference to a [`UserMeta`] instance. - /// * `guild_id` - The ID of the guild to update the role in. - /// * `role_id` - The ID of the role to update. - /// * `role_create_schema` - A [`RoleCreateModifySchema`] instance containing the new properties of the role. - /// - /// # Returns - /// - /// A `Result` containing the updated [`RoleObject`] if successful, or a [`ChorusLibError`] if the request fails or if the response is invalid. - /// - /// # Errors - /// - /// Returns a [`ChorusLibError`] if the request fails or if the response is invalid. - pub async fn update( - user: &mut UserMeta, + /// # Reference + /// See + pub async fn modify( + user: &mut ChorusUser, guild_id: Snowflake, role_id: Snowflake, role_create_schema: RoleCreateModifySchema, ) -> ChorusResult { let url = format!( "{}/guilds/{}/roles/{}", - user.belongs_to.borrow().urls.api, + user.belongs_to.read().unwrap().urls.api, guild_id, role_id ); @@ -198,7 +152,8 @@ impl types::RoleObject { let chorus_request = ChorusRequest { request: Client::new() .patch(url) - .bearer_auth(user.token()) + .header("Authorization", user.token()) + .header("Content-Type", "application/json") .body(body), limit_type: LimitType::Guild(guild_id), }; @@ -206,4 +161,33 @@ impl types::RoleObject { .deserialize_response::(user) .await } + + /// Deletes a guild role. Requires the `MANAGE_ROLES` permission. Returns a 204 empty response on success. + /// + /// # Reference: + /// See + pub async fn delete_role( + user: &mut ChorusUser, + guild_id: Snowflake, + role_id: Snowflake, + audit_log_reason: Option, + ) -> ChorusResult<()> { + let url = format!( + "{}/guilds/{}/roles/{}", + user.belongs_to.read().unwrap().urls.api, + guild_id, + role_id + ); + + let request = ChorusRequest::new( + http::Method::DELETE, + &url, + None, + audit_log_reason.as_deref(), + None, + Some(user), + LimitType::Guild(guild_id), + ); + request.handle_request_as_result(user).await + } } diff --git a/src/api/invites/mod.rs b/src/api/invites/mod.rs index b766a5b..332570b 100644 --- a/src/api/invites/mod.rs +++ b/src/api/invites/mod.rs @@ -2,17 +2,17 @@ use reqwest::Client; use serde_json::to_string; use crate::errors::ChorusResult; -use crate::instance::UserMeta; +use crate::instance::ChorusUser; use crate::ratelimiter::ChorusRequest; -use crate::types::{CreateChannelInviteSchema, GuildInvite, Invite, Snowflake}; +use crate::types::{CreateChannelInviteSchema, GuildInvite, Invite, LimitType, Snowflake}; -impl UserMeta { - /// # Arguments - /// - invite_code: The invite code to accept the invite for. - /// - session_id: The session ID that is accepting the invite, required for guest invites. +impl ChorusUser { + /// Accepts an invite to a guild, group DM, or DM. + /// + /// Note that the session ID is required for guest invites. /// /// # Reference: - /// Read + /// See pub async fn accept_invite( &mut self, invite_code: &str, @@ -21,37 +21,52 @@ impl UserMeta { let mut request = ChorusRequest { request: Client::new() .post(format!( - "{}/invites/{}/", - self.belongs_to.borrow().urls.api, + "{}/invites/{}", + self.belongs_to.read().unwrap().urls.api, invite_code )) - .bearer_auth(self.token()), - limit_type: super::LimitType::Global, + .header("Authorization", self.token()), + limit_type: LimitType::Global, }; if session_id.is_some() { request.request = request .request + .header("Content-Type", "application/json") .body(to_string(session_id.unwrap()).unwrap()); } request.deserialize_response::(self).await } + + /// Creates a new friend invite. + /// /// Note: Spacebar does not yet implement this endpoint. + /// + /// # Reference: + /// See pub async fn create_user_invite(&mut self, code: Option<&str>) -> ChorusResult { ChorusRequest { request: Client::new() .post(format!( - "{}/users/@me/invites/", - self.belongs_to.borrow().urls.api + "{}/users/@me/invites", + self.belongs_to.read().unwrap().urls.api )) .body(to_string(&code).unwrap()) - .bearer_auth(self.token()), - limit_type: super::LimitType::Global, + .header("Authorization", self.token()) + .header("Content-Type", "application/json"), + limit_type: LimitType::Global, } .deserialize_response::(self) .await } - pub async fn create_guild_invite( + /// Creates a new invite for a guild channel or group DM. + /// + /// # Guild Channels + /// For guild channels, the endpoint requires the [`CREATE_INSTANT_INVITE`](crate::types::PermissionFlags::CREATE_INSTANT_INVITE) permission. + /// + /// # Reference + /// See + pub async fn create_channel_invite( &mut self, create_channel_invite_schema: CreateChannelInviteSchema, channel_id: Snowflake, @@ -59,13 +74,14 @@ impl UserMeta { ChorusRequest { request: Client::new() .post(format!( - "{}/channels/{}/invites/", - self.belongs_to.borrow().urls.api, + "{}/channels/{}/invites", + self.belongs_to.read().unwrap().urls.api, channel_id )) - .bearer_auth(self.token()) + .header("Authorization", self.token()) + .header("Content-Type", "application/json") .body(to_string(&create_channel_invite_schema).unwrap()), - limit_type: super::LimitType::Channel(channel_id), + limit_type: LimitType::Channel(channel_id), } .deserialize_response::(self) .await diff --git a/src/api/mod.rs b/src/api/mod.rs index 9fd954d..ab3f9b9 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -1,8 +1,8 @@ +//! All of the API's endpoints. pub use channels::messages::*; pub use guilds::*; pub use invites::*; pub use policies::instance::instance::*; -pub use policies::instance::ratelimits::*; pub use users::*; pub mod auth; diff --git a/src/api/policies/instance/instance.rs b/src/api/policies/instance/instance.rs index 75f832c..4de5fd8 100644 --- a/src/api/policies/instance/instance.rs +++ b/src/api/policies/instance/instance.rs @@ -6,16 +6,20 @@ use crate::types::GeneralConfiguration; impl Instance { /// Gets the instance policies schema. - /// # Errors - /// [`ChorusLibError`] - If the request fails. + /// + /// # Notes + /// This is a Spacebar only endpoint. + /// + /// # Reference + /// See pub async fn general_configuration_schema(&self) -> ChorusResult { - let endpoint_url = self.urls.api.clone() + "/policies/instance/"; + let endpoint_url = self.urls.api.clone() + "/policies/instance"; let request = match self.client.get(&endpoint_url).send().await { Ok(result) => result, Err(e) => { return Err(ChorusError::RequestFailed { url: endpoint_url, - error: e, + error: e.to_string(), }); } }; diff --git a/src/api/policies/instance/mod.rs b/src/api/policies/instance/mod.rs index 0a1f245..b3a9148 100644 --- a/src/api/policies/instance/mod.rs +++ b/src/api/policies/instance/mod.rs @@ -1,5 +1,3 @@ pub use instance::*; -pub use ratelimits::*; pub mod instance; -pub mod ratelimits; diff --git a/src/api/policies/mod.rs b/src/api/policies/mod.rs index d0c29f1..1d5ea99 100644 --- a/src/api/policies/mod.rs +++ b/src/api/policies/mod.rs @@ -1,3 +1 @@ -pub use instance::ratelimits::*; - pub mod instance; diff --git a/src/api/users/channels.rs b/src/api/users/channels.rs index 806bd9f..330b3e3 100644 --- a/src/api/users/channels.rs +++ b/src/api/users/channels.rs @@ -2,27 +2,33 @@ use reqwest::Client; use serde_json::to_string; use crate::{ - api::LimitType, errors::ChorusResult, - instance::UserMeta, + instance::ChorusUser, ratelimiter::ChorusRequest, - types::{Channel, PrivateChannelCreateSchema}, + types::{Channel, LimitType, PrivateChannelCreateSchema}, }; -impl UserMeta { +impl ChorusUser { /// Creates a DM channel or group DM channel. /// + /// One recipient creates or returns an existing DM channel, + /// none or multiple recipients create a group DM channel. + /// /// # Reference: - /// Read + /// See pub async fn create_private_channel( &mut self, create_private_channel_schema: PrivateChannelCreateSchema, ) -> ChorusResult { - let url = format!("{}/users/@me/channels", self.belongs_to.borrow().urls.api); + let url = format!( + "{}/users/@me/channels", + self.belongs_to.read().unwrap().urls.api + ); ChorusRequest { request: Client::new() .post(url) - .bearer_auth(self.token()) + .header("Authorization", self.token()) + .header("Content-Type", "application/json") .body(to_string(&create_private_channel_schema).unwrap()), limit_type: LimitType::Global, } diff --git a/src/api/users/guilds.rs b/src/api/users/guilds.rs index 735ed9e..6ffcdfc 100644 --- a/src/api/users/guilds.rs +++ b/src/api/users/guilds.rs @@ -2,29 +2,59 @@ use reqwest::Client; use serde_json::to_string; use crate::errors::ChorusResult; -use crate::instance::UserMeta; +use crate::instance::ChorusUser; use crate::ratelimiter::ChorusRequest; -use crate::types::Snowflake; +use crate::types::{GetUserGuildSchema, Guild, LimitType, Snowflake}; -impl UserMeta { - /// # Arguments: - /// - lurking: Whether the user is lurking in the guild +impl ChorusUser { + /// Leaves a given guild. /// /// # Reference: - /// Read + /// See + // TODO: Docs: What is "lurking" here? + // It is documented as "Whether the user is lurking in the guild", + // but that says nothing about what this field actually does / means pub async fn leave_guild(&mut self, guild_id: &Snowflake, lurking: bool) -> ChorusResult<()> { ChorusRequest { request: Client::new() .delete(format!( - "{}/users/@me/guilds/{}/", - self.belongs_to.borrow().urls.api, + "{}/users/@me/guilds/{}", + self.belongs_to.read().unwrap().urls.api, guild_id )) - .bearer_auth(self.token()) + .header("Authorization", self.token()) + .header("Content-Type", "application/json") .body(to_string(&lurking).unwrap()), - limit_type: crate::api::LimitType::Guild(*guild_id), + limit_type: LimitType::Guild(*guild_id), } .handle_request_as_result(self) .await } + + /// Returns a list of user guild objects representing the guilds the current user is a member of. + /// This endpoint returns 200 guilds by default + /// + /// # Reference: + /// See: + pub async fn get_guilds( + &mut self, + query: Option, + ) -> ChorusResult> { + let url = format!( + "{}/users/@me/guilds", + self.belongs_to.read().unwrap().urls.api, + ); + let chorus_request = ChorusRequest { + request: Client::new() + .get(url) + .header("Authorization", self.token()) + .header("Content-Type", "application/json") + .body(to_string(&query).unwrap()), + + limit_type: LimitType::Global, + }; + chorus_request + .deserialize_response::>(self) + .await + } } diff --git a/src/api/users/relationships.rs b/src/api/users/relationships.rs index 39c75d8..4f9602c 100644 --- a/src/api/users/relationships.rs +++ b/src/api/users/relationships.rs @@ -2,33 +2,31 @@ use reqwest::Client; use serde_json::to_string; use crate::{ - api::LimitType, errors::ChorusResult, - instance::UserMeta, + instance::ChorusUser, ratelimiter::ChorusRequest, - types::{self, CreateUserRelationshipSchema, RelationshipType, Snowflake}, + types::{ + self, CreateUserRelationshipSchema, FriendRequestSendSchema, LimitType, RelationshipType, + Snowflake, + }, }; -impl UserMeta { - /// Retrieves the mutual relationships between the authenticated user and the specified user. +impl ChorusUser { + /// Retrieves a list of mutual friends between the authenticated user and a given user. /// - /// # Arguments - /// - /// * `user_id` - ID of the user to retrieve the mutual relationships with. - /// - /// # Returns - /// This function returns a [`ChorusResult>`]. + /// # Reference + /// See pub async fn get_mutual_relationships( &mut self, user_id: Snowflake, ) -> ChorusResult> { let url = format!( - "{}/users/{}/relationships/", - self.belongs_to.borrow().urls.api, + "{}/users/{}/relationships", + self.belongs_to.read().unwrap().urls.api, user_id ); let chorus_request = ChorusRequest { - request: Client::new().get(url).bearer_auth(self.token()), + request: Client::new().get(url).header("Authorization", self.token()), limit_type: LimitType::Global, }; chorus_request @@ -36,17 +34,17 @@ impl UserMeta { .await } - /// Retrieves the authenticated user's relationships. + /// Retrieves the user's relationships. /// - /// # Returns - /// This function returns a [`ChorusResult>`]. + /// # Reference + /// See pub async fn get_relationships(&mut self) -> ChorusResult> { let url = format!( - "{}/users/@me/relationships/", - self.belongs_to.borrow().urls.api + "{}/users/@me/relationships", + self.belongs_to.read().unwrap().urls.api ); let chorus_request = ChorusRequest { - request: Client::new().get(url).bearer_auth(self.token()), + request: Client::new().get(url).header("Authorization", self.token()), limit_type: LimitType::Global, }; chorus_request @@ -56,54 +54,43 @@ impl UserMeta { /// Sends a friend request to a user. /// - /// # Arguments - /// - /// * `schema` - A [`FriendRequestSendSchema`] struct that holds the information about the friend request to be sent. - /// - /// # Returns - /// This function returns a [`Result`] that holds a [`ChorusLibError`] if the request fails. + /// # Reference + /// See pub async fn send_friend_request( &mut self, - schema: types::FriendRequestSendSchema, + schema: FriendRequestSendSchema, ) -> ChorusResult<()> { let url = format!( - "{}/users/@me/relationships/", - self.belongs_to.borrow().urls.api + "{}/users/@me/relationships", + self.belongs_to.read().unwrap().urls.api ); let body = to_string(&schema).unwrap(); let chorus_request = ChorusRequest { - request: Client::new().post(url).bearer_auth(self.token()).body(body), + request: Client::new() + .post(url) + .header("Authorization", self.token()) + .header("Content-Type", "application/json") + .body(body), limit_type: LimitType::Global, }; chorus_request.handle_request_as_result(self).await } - /// Modifies the relationship between the authenticated user and the specified user. + /// Modifies the relationship between the authenticated user and a given user. /// - /// # Arguments - /// - /// * `user_id` - ID of the user to modify the relationship with. - /// * `relationship_type` - A [`RelationshipType`] enum that specifies the type of relationship to modify. - /// * [`RelationshipType::None`]: Removes the relationship between the two users. - /// * [`RelationshipType::Friends`] | [`RelationshipType::Incoming`] | [`RelationshipType::Outgoing`]: - /// Either accepts an incoming friend request, or sends a new friend request, if there is no - /// incoming friend request from the specified `user_id`. - /// * [`RelationshipType::Blocked`]: Blocks the specified user_id. - /// - /// # Returns - /// This function returns an [`Result`] that holds a [`ChorusLibError`] if the request fails. + /// Can be used to unfriend users, accept or send friend requests and block or unblock users. pub async fn modify_user_relationship( &mut self, user_id: Snowflake, relationship_type: RelationshipType, ) -> ChorusResult<()> { - let api_url = self.belongs_to.borrow().urls.api.clone(); + let api_url = self.belongs_to.read().unwrap().urls.api.clone(); match relationship_type { RelationshipType::None => { let chorus_request = ChorusRequest { request: Client::new() - .delete(format!("{}/users/@me/relationships/{}/", api_url, user_id)) - .bearer_auth(self.token()), + .delete(format!("{}/users/@me/relationships/{}", api_url, user_id)) + .header("Authorization", self.token()), limit_type: LimitType::Global, }; chorus_request.handle_request_as_result(self).await @@ -116,8 +103,8 @@ impl UserMeta { }; let chorus_request = ChorusRequest { request: Client::new() - .put(format!("{}/users/@me/relationships/{}/", api_url, user_id)) - .bearer_auth(self.token()) + .put(format!("{}/users/@me/relationships/{}", api_url, user_id)) + .header("Authorization", self.token()) .body(to_string(&body).unwrap()), limit_type: LimitType::Global, }; @@ -131,8 +118,8 @@ impl UserMeta { }; let chorus_request = ChorusRequest { request: Client::new() - .put(format!("{}/users/@me/relationships/{}/", api_url, user_id)) - .bearer_auth(self.token()) + .put(format!("{}/users/@me/relationships/{}", api_url, user_id)) + .header("Authorization", self.token()) .body(to_string(&body).unwrap()), limit_type: LimitType::Global, }; @@ -142,22 +129,20 @@ impl UserMeta { } } - /// Removes the relationship between the authenticated user and the specified user. + /// Removes the relationship between the authenticated user and a given user. /// - /// # Arguments - /// - /// * `user_id` - ID of the user to remove the relationship with. - /// - /// # Returns - /// This function returns a [`Result`] that holds a [`ChorusLibError`] if the request fails. + /// # Reference + /// See pub async fn remove_relationship(&mut self, user_id: Snowflake) -> ChorusResult<()> { let url = format!( - "{}/users/@me/relationships/{}/", - self.belongs_to.borrow().urls.api, + "{}/users/@me/relationships/{}", + self.belongs_to.read().unwrap().urls.api, user_id ); let chorus_request = ChorusRequest { - request: Client::new().delete(url).bearer_auth(self.token()), + request: Client::new() + .delete(url) + .header("Authorization", self.token()), limit_type: LimitType::Global, }; chorus_request.handle_request_as_result(self).await diff --git a/src/api/users/users.rs b/src/api/users/users.rs index af6d2ce..0f31d6f 100644 --- a/src/api/users/users.rs +++ b/src/api/users/users.rs @@ -1,33 +1,32 @@ -use std::{cell::RefCell, rc::Rc}; +use std::sync::{Arc, RwLock}; use reqwest::Client; use serde_json::to_string; use crate::{ - api::LimitType, errors::{ChorusError, ChorusResult}, - instance::{Instance, UserMeta}, + instance::{ChorusUser, Instance}, ratelimiter::ChorusRequest, - types::{User, UserModifySchema, UserSettings}, + types::{LimitType, User, UserModifySchema, UserSettings}, }; -impl UserMeta { - /// Get a user object by id, or get the current user. +impl ChorusUser { + /// Gets a user by id, or if the id is None, gets the current user. /// - /// # Arguments + /// # Notes + /// This function is a wrapper around [`User::get`]. /// - /// * `token` - A valid access token for the API. - /// * `url_api` - The URL to the API. - /// * `id` - The id of the user that will be retrieved. If this is None, the current user will be retrieved. - /// * `instance_limits` - The [`Limits`] of the instance. - /// - /// # Errors - /// - /// * [`ChorusLibError`] - If the request fails. - pub async fn get(user: &mut UserMeta, id: Option<&String>) -> ChorusResult { - User::get(user, id).await + /// # Reference + /// See and + /// + pub async fn get_user(&mut self, id: Option<&String>) -> ChorusResult { + User::get(self, id).await } + /// Gets the user's settings. + /// + /// # Notes + /// This functions is a wrapper around [`User::get_settings`]. pub async fn get_settings( token: &String, url_api: &String, @@ -36,15 +35,10 @@ impl UserMeta { User::get_settings(token, url_api, instance).await } - /// Modify the current user's `UserObject`. + /// Modifies the current user's representation. (See [`User`]) /// - /// # Arguments - /// - /// * `modify_schema` - A `UserModifySchema` object containing the fields to modify. - /// - /// # Errors - /// - /// Returns an `ChorusLibError` if the request fails or if a password is required but not provided. + /// # Reference + /// See pub async fn modify(&mut self, modify_schema: UserModifySchema) -> ChorusResult { if modify_schema.new_password.is_some() || modify_schema.email.is_some() @@ -53,37 +47,32 @@ impl UserMeta { return Err(ChorusError::PasswordRequired); } let request = Client::new() - .patch(format!("{}/users/@me/", self.belongs_to.borrow().urls.api)) + .patch(format!( + "{}/users/@me", + self.belongs_to.read().unwrap().urls.api + )) .body(to_string(&modify_schema).unwrap()) - .bearer_auth(self.token()); + .header("Authorization", self.token()) + .header("Content-Type", "application/json"); let chorus_request = ChorusRequest { request, limit_type: LimitType::default(), }; - let user_updated = chorus_request - .deserialize_response::(self) - .await - .unwrap(); - let _ = std::mem::replace(&mut self.object, user_updated.clone()); - Ok(user_updated) + chorus_request.deserialize_response::(self).await } - /// Sends a request to the server which deletes the user from the Instance. + /// Deletes the user from the Instance. /// - /// # Arguments - /// - /// * `self` - The `User` object to delete. - /// - /// # Returns - /// - /// Returns `()` if the user was successfully deleted, or a `ChorusLibError` if an error occurred. + /// # Reference + /// See pub async fn delete(mut self) -> ChorusResult<()> { let request = Client::new() .post(format!( - "{}/users/@me/delete/", - self.belongs_to.borrow().urls.api + "{}/users/@me/delete", + self.belongs_to.read().unwrap().urls.api )) - .bearer_auth(self.token()); + .header("Authorization", self.token()) + .header("Content-Type", "application/json"); let chorus_request = ChorusRequest { request, limit_type: LimitType::default(), @@ -93,14 +82,21 @@ impl UserMeta { } impl User { - pub async fn get(user: &mut UserMeta, id: Option<&String>) -> ChorusResult { - let url_api = user.belongs_to.borrow().urls.api.clone(); + /// Gets a user by id, or if the id is None, gets the current user. + /// + /// # Reference + /// See and + /// + pub async fn get(user: &mut ChorusUser, id: Option<&String>) -> ChorusResult { + let url_api = user.belongs_to.read().unwrap().urls.api.clone(); let url = if id.is_none() { - format!("{}/users/@me/", url_api) + format!("{}/users/@me", url_api) } else { format!("{}/users/{}", url_api, id.unwrap()) }; - let request = reqwest::Client::new().get(url).bearer_auth(user.token()); + let request = reqwest::Client::new() + .get(url) + .header("Authorization", user.token()); let chorus_request = ChorusRequest { request, limit_type: LimitType::Global, @@ -114,16 +110,20 @@ impl User { } } + /// Gets the user's settings. + /// + /// # Reference + /// See pub async fn get_settings( token: &String, url_api: &String, instance: &mut Instance, ) -> ChorusResult { let request: reqwest::RequestBuilder = Client::new() - .get(format!("{}/users/@me/settings/", url_api)) - .bearer_auth(token); + .get(format!("{}/users/@me/settings", url_api)) + .header("Authorization", token); let mut user = - UserMeta::shell(Rc::new(RefCell::new(instance.clone())), token.clone()).await; + ChorusUser::shell(Arc::new(RwLock::new(instance.clone())), token.clone()).await; let chorus_request = ChorusRequest { request, limit_type: LimitType::Global, @@ -133,30 +133,36 @@ impl User { Err(e) => Err(e), }; if instance.limits_information.is_some() { - instance.limits_information.as_mut().unwrap().ratelimits = - user.belongs_to.borrow().clone_limits_if_some().unwrap(); + instance.limits_information.as_mut().unwrap().ratelimits = user + .belongs_to + .read() + .unwrap() + .clone_limits_if_some() + .unwrap(); } result } } impl Instance { - /** - Get a user object by id, or get the current user. - # Arguments - * `token` - A valid access token for the API. - * `id` - The id of the user that will be retrieved. If this is None, the current user will be retrieved. - # Errors - * [`ChorusLibError`] - If the request fails. - # Notes - This function is a wrapper around [`User::get`]. - */ + /// Gets a user by id, or if the id is None, gets the current user. + /// + /// # Notes + /// This function is a wrapper around [`User::get`]. + /// + /// # Reference + /// See and + /// pub async fn get_user(&mut self, token: String, id: Option<&String>) -> ChorusResult { - let mut user = UserMeta::shell(Rc::new(RefCell::new(self.clone())), token).await; + let mut user = ChorusUser::shell(Arc::new(RwLock::new(self.clone())), token).await; let result = User::get(&mut user, id).await; if self.limits_information.is_some() { - self.limits_information.as_mut().unwrap().ratelimits = - user.belongs_to.borrow().clone_limits_if_some().unwrap(); + self.limits_information.as_mut().unwrap().ratelimits = user + .belongs_to + .read() + .unwrap() + .clone_limits_if_some() + .unwrap(); } result } diff --git a/src/errors.rs b/src/errors.rs index 0a962b1..4a251c9 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -1,8 +1,10 @@ +//! Contains all the errors that can be returned by the library. use custom_error::custom_error; -use reqwest::Error; + +use crate::types::WebSocketEvent; custom_error! { - #[derive(PartialEq, Eq)] + #[derive(PartialEq, Eq, Clone, Hash)] pub RegistrationError Consent = "Consent must be 'true' to register.", } @@ -10,11 +12,12 @@ custom_error! { pub type ChorusResult = std::result::Result; custom_error! { + #[derive(Clone, Hash, PartialEq, Eq)] pub ChorusError /// Server did not respond. NoResponse = "Did not receive a response from the Server.", /// Reqwest returned an Error instead of a Response object. - RequestFailed{url:String, error: Error} = "An error occured while trying to GET from {url}: {error}", + RequestFailed{url:String, error: String} = "An error occured while trying to GET from {url}: {error}", /// Response received, however, it was not of the successful responses type. Used when no other, special case applies. ReceivedErrorCode{error_code: u16, error: String} = "Received the following error code while requesting from the route: {error_code}", /// Used when there is likely something wrong with the instance, the request was directed to. @@ -53,9 +56,10 @@ custom_error! { /// Supposed to be sent as numbers, though they are sent as string most of the time? /// /// Also includes errors when initiating a connection and unexpected opcodes - #[derive(Clone, PartialEq, Eq)] + #[derive(PartialEq, Eq, Default, Clone)] pub GatewayError // Errors we have received from the gateway + #[default] Unknown = "We're not sure what went wrong. Try reconnecting?", UnknownOpcode = "You sent an invalid Gateway opcode or an invalid payload for an opcode", Decode = "Gateway server couldn't decode payload", @@ -79,25 +83,29 @@ custom_error! { UnexpectedOpcodeReceived{opcode: u8} = "Received an opcode we weren't expecting to receive: {opcode}", } +impl WebSocketEvent for GatewayError {} + custom_error! { - // Like GatewayError for webrtc errors - // See https://discord.com/developers/docs/topics/opcodes-and-status-codes#voice; - // Also supposed to be sent by numbers, but discord is asdfghgfjkkjldf when it comes to their errors + /// Voice Gateway errors + /// + /// Similar to [GatewayError]. + /// + /// See https://discord.com/developers/docs/topics/opcodes-and-status-codes#voice; #[derive(Clone, PartialEq, Eq)] pub VoiceGatewayError // Errors we receive - UnknownOpcodeError = "You sent an invalid opcode", - FailedToDecodePayloadError = "You sent an invalid payload in your identifying to the (Webrtc) Gateway", - NotAuthenticatedError = "You sent a payload before identifying with the (Webrtc) Gateway", - AuthenticationFailedError = "The token you sent in your identify payload is incorrect", - AlreadyAuthenticatedError = "You sent more than one identify payload", - SessionNoLongerValidError = "Your session is no longer valid", - SessionTimeoutError = "Your session has timed out", - ServerNotFoundError = "We can't find the server you're trying to connect to", - UnknownProtocolError = "We didn't recognize the protocol you sent", - DisconnectedError = "Channel was deleted, you were kicked, voice server changed, or the main gateway session was dropped. Should not reconnect.", - VoiceServerCrashedError = "The server crashed, try resuming", - UnknownEncryptionModeError = "Server failed to decrypt data", + UnknownOpcode = "You sent an invalid opcode", + FailedToDecodePayload = "You sent an invalid payload in your identifying to the (Webrtc) Gateway", + NotAuthenticated = "You sent a payload before identifying with the (Webrtc) Gateway", + AuthenticationFailed = "The token you sent in your identify payload is incorrect", + AlreadyAuthenticated = "You sent more than one identify payload", + SessionNoLongerValid = "Your session is no longer valid", + SessionTimeout = "Your session has timed out", + ServerNotFound = "We can't find the server you're trying to connect to", + UnknownProtocol = "We didn't recognize the protocol you sent", + Disconnected = "Channel was deleted, you were kicked, voice server changed, or the main gateway session was dropped. Should not reconnect.", + VoiceServerCrashed = "The server crashed, try resuming", + UnknownEncryptionMode = "Server failed to decrypt data", // Errors when initiating a gateway connection CannotConnect{error: String} = "Cannot connect due to a tungstenite error: {error}", diff --git a/src/gateway.rs b/src/gateway.rs index 8e60dd0..def0df1 100644 --- a/src/gateway.rs +++ b/src/gateway.rs @@ -1,14 +1,18 @@ +//! Gateway connection, communication and handling, as well as object caching and updating. + use crate::errors::GatewayError; use crate::gateway::events::Events; -use crate::types::{self, Channel, ChannelUpdate, Snowflake}; -use crate::types::{UpdateMessage, WebSocketEvent}; +use crate::types::{ + self, AutoModerationRule, AutoModerationRuleUpdate, Channel, ChannelCreate, ChannelDelete, + ChannelUpdate, Composite, Guild, GuildRoleCreate, GuildRoleUpdate, JsonField, RoleObject, + Snowflake, SourceUrlField, ThreadUpdate, UpdateMessage, WebSocketEvent, +}; use async_trait::async_trait; use std::any::Any; use std::collections::HashMap; use std::fmt::Debug; -use std::sync::Arc; +use std::sync::{Arc, RwLock}; use std::time::Duration; -use tokio::sync::watch; use tokio::time::sleep_until; use futures_util::stream::SplitSink; @@ -78,7 +82,7 @@ const GATEWAY_LAZY_REQUEST: u8 = 14; /// The amount of time we wait for a heartbeat ack before resending our heartbeat in ms pub(crate) const HEARTBEAT_ACK_TIMEOUT: u64 = 2000; -/// Represents a messsage received from the gateway. This will be either a [GatewayReceivePayload], containing events, or a [GatewayError]. +/// Represents a messsage received from the gateway. This will be either a [types::GatewayReceivePayload], containing events, or a [GatewayError]. /// This struct is used internally when handling messages. #[derive(Clone, Debug)] pub struct GatewayMessage { @@ -148,11 +152,13 @@ impl GatewayMessage { } } +pub type ObservableObject = dyn Send + Sync + Any; + /// Represents a handle to a Gateway connection. A Gateway connection will create observable /// [`GatewayEvents`](GatewayEvent), which you can subscribe to. Gateway events include all currently -/// implemented [Types] with the trait [`WebSocketEvent`] +/// implemented types with the trait [`WebSocketEvent`] /// Using this handle you can also send Gateway Events directly. -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct GatewayHandle { pub url: String, pub events: Arc>, @@ -164,10 +170,9 @@ pub struct GatewayHandle { >, >, >, - pub handle: JoinHandle<()>, /// Tells gateway tasks to close kill_send: tokio::sync::broadcast::Sender<()>, - store: Arc>>>, + pub(crate) store: Arc>>>>, } /// An entity type which is supposed to be updateable via the Gateway. This is implemented for all such types chorus supports, implementing it for your own types is likely a mistake. @@ -196,27 +201,57 @@ impl GatewayHandle { .unwrap(); } - pub async fn observe(&self, object: T) -> watch::Receiver { + pub async fn observe>( + &self, + object: Arc>, + ) -> Arc> { let mut store = self.store.lock().await; - if let Some(channel) = store.get(&object.id()) { - let (_, rx) = channel - .downcast_ref::<(watch::Sender, watch::Receiver)>() + let id = object.read().unwrap().id(); + if let Some(channel) = store.get(&id) { + let object = channel.clone(); + drop(store); + object + .read() + .unwrap() + .downcast_ref::() .unwrap_or_else(|| { panic!( "Snowflake {} already exists in the store, but it is not of type T.", - object.id() + id ) }); - rx.clone() + let ptr = Arc::into_raw(object.clone()); + // SAFETY: + // - We have just checked that the typeid of the `dyn Any ...` matches that of `T`. + // - This operation doesn't read or write any shared data, and thus cannot cause a data race + // - The reference count is not being modified + let downcasted = unsafe { Arc::from_raw(ptr as *const RwLock).clone() }; + let object = downcasted.read().unwrap().clone(); + + let watched_object = object.watch_whole(self).await; + *downcasted.write().unwrap() = watched_object; + downcasted } else { - let id = object.id(); - let channel = watch::channel(object); - let receiver = channel.1.clone(); - store.insert(id, Box::new(channel)); - receiver + let id = object.read().unwrap().id(); + let object = object.read().unwrap().clone(); + let object = object.clone().watch_whole(self).await; + let wrapped = Arc::new(RwLock::new(object)); + store.insert(id, wrapped.clone()); + wrapped } } + /// Recursively observes and updates all updateable fields on the struct T. Returns an object `T` + /// with all of its observable fields being observed. + pub async fn observe_and_into_inner>( + &self, + object: Arc>, + ) -> T { + let channel = self.observe(object.clone()).await; + let object = channel.read().unwrap().clone(); + object + } + /// Sends an identify event to the gateway pub async fn send_identify(&self, to_send: types::GatewayIdentifyPayload) { let to_send_value = serde_json::to_value(&to_send).unwrap(); @@ -257,7 +292,7 @@ impl GatewayHandle { /// Sends an update voice state to the server pub async fn send_update_voice_state(&self, to_send: types::UpdateVoiceState) { - let to_send_value = serde_json::to_value(&to_send).unwrap(); + let to_send_value = serde_json::to_value(to_send).unwrap(); trace!("GW: Sending Update Voice State.."); @@ -293,6 +328,7 @@ impl GatewayHandle { } } +#[derive(Debug)] pub struct Gateway { events: Arc>, heartbeat_handler: HeartbeatHandler, @@ -306,7 +342,8 @@ pub struct Gateway { >, websocket_receive: SplitStream>>, kill_send: tokio::sync::broadcast::Sender<()>, - store: Arc>>>, + store: Arc>>>>, + url: String, } impl Gateway { @@ -370,10 +407,11 @@ impl Gateway { websocket_receive, kill_send: kill_send.clone(), store: store.clone(), + url: websocket_url.clone(), }; // Now we can continuously check for messages in a different task, since we aren't going to receive another hello - let handle: JoinHandle<()> = task::spawn(async move { + task::spawn(async move { gateway.gateway_listen_task().await; }); @@ -381,7 +419,6 @@ impl Gateway { url: websocket_url.clone(), events: shared_events, websocket_send: shared_websocket_send.clone(), - handle, kill_send: kill_send.clone(), store, }) @@ -444,13 +481,15 @@ impl Gateway { return; } - // Todo: handle errors in a good way, maybe observers like events? if msg.is_error() { - warn!("GW: Received error, connection will close.."); + let error = msg.error().unwrap(); - let _error = msg.error(); + warn!("GW: Received error {:?}, connection will close..", error); self.close().await; + + self.events.lock().await.error.notify(error).await; + return; } @@ -462,7 +501,7 @@ impl Gateway { GATEWAY_DISPATCH => { let Some(event_name) = gateway_payload.event_name else { warn!("Gateway dispatch op without event_name"); - return + return; }; trace!("Gateway: Received {event_name}"); @@ -472,16 +511,35 @@ impl Gateway { match event_name.as_str() { $($name => { let event = &mut self.events.lock().await.$($path).+; - match serde_json::from_str(gateway_payload.event_data.unwrap().get()) { + let json = gateway_payload.event_data.unwrap().get(); + match serde_json::from_str(json) { Err(err) => warn!("Failed to parse gateway event {event_name} ({err})"), Ok(message) => { $( - let message: $message_type = message; - if let Some(to_update) = self.store.lock().await.get(&message.id()) { - if let Some((tx, _)) = to_update.downcast_ref::<(watch::Sender<$update_type>, watch::Receiver<$update_type>)>() { - tx.send_modify(|object| message.update(object)); + let mut message: $message_type = message; + let store = self.store.lock().await; + let id = if message.id().is_some() { + message.id().unwrap() + } else { + event.notify(message).await; + return; + }; + if let Some(to_update) = store.get(&id) { + let object = to_update.clone(); + let inner_object = object.read().unwrap(); + if let Some(_) = inner_object.downcast_ref::<$update_type>() { + let ptr = Arc::into_raw(object.clone()); + // SAFETY: + // - We have just checked that the typeid of the `dyn Any ...` matches that of `T`. + // - This operation doesn't read or write any shared data, and thus cannot cause a data race + // - The reference count is not being modified + let downcasted = unsafe { Arc::from_raw(ptr as *const RwLock<$update_type>).clone() }; + drop(inner_object); + message.set_json(json.to_string()); + message.set_source_url(self.url.clone()); + message.update(downcasted.clone()); } else { - warn!("Received {} for {}, but it has been observed to be a different type!", $name, message.id()) + warn!("Received {} for {}, but it has been observed to be a different type!", $name, id) } } )? @@ -523,69 +581,70 @@ impl Gateway { "READY_SUPPLEMENTAL" => session.ready_supplemental, "APPLICATION_COMMAND_PERMISSIONS_UPDATE" => application.command_permissions_update, "AUTO_MODERATION_RULE_CREATE" =>auto_moderation.rule_create, - "AUTO_MODERATION_RULE_UPDATE" =>auto_moderation.rule_update, + "AUTO_MODERATION_RULE_UPDATE" =>auto_moderation.rule_update AutoModerationRuleUpdate: AutoModerationRule, "AUTO_MODERATION_RULE_DELETE" => auto_moderation.rule_delete, "AUTO_MODERATION_ACTION_EXECUTION" => auto_moderation.action_execution, - "CHANNEL_CREATE" => channel.create, + "CHANNEL_CREATE" => channel.create ChannelCreate: Guild, "CHANNEL_UPDATE" => channel.update ChannelUpdate: Channel, "CHANNEL_UNREAD_UPDATE" => channel.unread_update, - "CHANNEL_DELETE" => channel.delete, + "CHANNEL_DELETE" => channel.delete ChannelDelete: Guild, "CHANNEL_PINS_UPDATE" => channel.pins_update, "CALL_CREATE" => call.create, "CALL_UPDATE" => call.update, "CALL_DELETE" => call.delete, - "THREAD_CREATE" => thread.create, - "THREAD_UPDATE" => thread.update, - "THREAD_DELETE" => thread.delete, - "THREAD_LIST_SYNC" => thread.list_sync, - "THREAD_MEMBER_UPDATE" => thread.member_update, - "THREAD_MEMBERS_UPDATE" => thread.members_update, - "GUILD_CREATE" => guild.create, - "GUILD_UPDATE" => guild.update, - "GUILD_DELETE" => guild.delete, + "THREAD_CREATE" => thread.create, // TODO + "THREAD_UPDATE" => thread.update ThreadUpdate: Channel, + "THREAD_DELETE" => thread.delete, // TODO + "THREAD_LIST_SYNC" => thread.list_sync, // TODO + "THREAD_MEMBER_UPDATE" => thread.member_update, // TODO + "THREAD_MEMBERS_UPDATE" => thread.members_update, // TODO + "GUILD_CREATE" => guild.create, // TODO + "GUILD_UPDATE" => guild.update, // TODO + "GUILD_DELETE" => guild.delete, // TODO "GUILD_AUDIT_LOG_ENTRY_CREATE" => guild.audit_log_entry_create, - "GUILD_BAN_ADD" => guild.ban_add, - "GUILD_BAN_REMOVE" => guild.ban_remove, - "GUILD_EMOJIS_UPDATE" => guild.emojis_update, - "GUILD_STICKERS_UPDATE" => guild.stickers_update, + "GUILD_BAN_ADD" => guild.ban_add, // TODO + "GUILD_BAN_REMOVE" => guild.ban_remove, // TODO + "GUILD_EMOJIS_UPDATE" => guild.emojis_update, // TODO + "GUILD_STICKERS_UPDATE" => guild.stickers_update, // TODO "GUILD_INTEGRATIONS_UPDATE" => guild.integrations_update, "GUILD_MEMBER_ADD" => guild.member_add, "GUILD_MEMBER_REMOVE" => guild.member_remove, - "GUILD_MEMBER_UPDATE" => guild.member_update, - "GUILD_MEMBERS_CHUNK" => guild.members_chunk, - "GUILD_ROLE_CREATE" => guild.role_create, - "GUILD_ROLE_UPDATE" => guild.role_update, - "GUILD_ROLE_DELETE" => guild.role_delete, - "GUILD_SCHEDULED_EVENT_CREATE" => guild.role_scheduled_event_create, - "GUILD_SCHEDULED_EVENT_UPDATE" => guild.role_scheduled_event_update, - "GUILD_SCHEDULED_EVENT_DELETE" => guild.role_scheduled_event_delete, + "GUILD_MEMBER_UPDATE" => guild.member_update, // TODO + "GUILD_MEMBERS_CHUNK" => guild.members_chunk, // TODO + "GUILD_ROLE_CREATE" => guild.role_create GuildRoleCreate: Guild, + "GUILD_ROLE_UPDATE" => guild.role_update GuildRoleUpdate: RoleObject, + "GUILD_ROLE_DELETE" => guild.role_delete, // TODO + "GUILD_SCHEDULED_EVENT_CREATE" => guild.role_scheduled_event_create, // TODO + "GUILD_SCHEDULED_EVENT_UPDATE" => guild.role_scheduled_event_update, // TODO + "GUILD_SCHEDULED_EVENT_DELETE" => guild.role_scheduled_event_delete, // TODO "GUILD_SCHEDULED_EVENT_USER_ADD" => guild.role_scheduled_event_user_add, "GUILD_SCHEDULED_EVENT_USER_REMOVE" => guild.role_scheduled_event_user_remove, - "PASSIVE_UPDATE_V1" => guild.passive_update_v1, - "INTEGRATION_CREATE" => integration.create, - "INTEGRATION_UPDATE" => integration.update, - "INTEGRATION_DELETE" => integration.delete, - "INTERACTION_CREATE" => interaction.create, - "INVITE_CREATE" => invite.create, - "INVITE_DELETE" => invite.delete, + "PASSIVE_UPDATE_V1" => guild.passive_update_v1, // TODO + "INTEGRATION_CREATE" => integration.create, // TODO + "INTEGRATION_UPDATE" => integration.update, // TODO + "INTEGRATION_DELETE" => integration.delete, // TODO + "INTERACTION_CREATE" => interaction.create, // TODO + "INVITE_CREATE" => invite.create, // TODO + "INVITE_DELETE" => invite.delete, // TODO "MESSAGE_CREATE" => message.create, - "MESSAGE_UPDATE" => message.update, + "MESSAGE_UPDATE" => message.update, // TODO "MESSAGE_DELETE" => message.delete, "MESSAGE_DELETE_BULK" => message.delete_bulk, - "MESSAGE_REACTION_ADD" => message.reaction_add, - "MESSAGE_REACTION_REMOVE" => message.reaction_remove, - "MESSAGE_REACTION_REMOVE_ALL" => message.reaction_remove_all, - "MESSAGE_REACTION_REMOVE_EMOJI" => message.reaction_remove_emoji, + "MESSAGE_REACTION_ADD" => message.reaction_add, // TODO + "MESSAGE_REACTION_REMOVE" => message.reaction_remove, // TODO + "MESSAGE_REACTION_REMOVE_ALL" => message.reaction_remove_all, // TODO + "MESSAGE_REACTION_REMOVE_EMOJI" => message.reaction_remove_emoji, // TODO "MESSAGE_ACK" => message.ack, - "PRESENCE_UPDATE" => user.presence_update, + "PRESENCE_UPDATE" => user.presence_update, // TODO "RELATIONSHIP_ADD" => relationship.add, "RELATIONSHIP_REMOVE" => relationship.remove, "STAGE_INSTANCE_CREATE" => stage_instance.create, - "STAGE_INSTANCE_UPDATE" => stage_instance.update, + "STAGE_INSTANCE_UPDATE" => stage_instance.update, // TODO "STAGE_INSTANCE_DELETE" => stage_instance.delete, - "USER_UPDATE" => user.update, + "TYPING_START" => user.typing_start, + "USER_UPDATE" => user.update, // TODO "USER_GUILD_SETTINGS_UPDATE" => user.guild_settings_update, - "VOICE_STATE_UPDATE" => voice.state_update, + "VOICE_STATE_UPDATE" => voice.state_update, // TODO "VOICE_SERVER_UPDATE" => voice.server_update, "WEBHOOKS_UPDATE" => webhooks.update ); @@ -671,6 +730,7 @@ impl Gateway { /// Handles sending heartbeats to the gateway in another thread #[allow(dead_code)] // FIXME: Remove this, once HeartbeatHandler is used +#[derive(Debug)] struct HeartbeatHandler { /// How ofter heartbeats need to be sent at a minimum pub heartbeat_interval: Duration, @@ -857,7 +917,7 @@ impl GatewayEvent { } } -mod events { +pub mod events { use super::*; #[derive(Default, Debug)] @@ -880,6 +940,7 @@ mod events { pub webhooks: Webhooks, pub gateway_identify_payload: GatewayEvent, pub gateway_resume: GatewayEvent, + pub error: GatewayEvent, } #[derive(Default, Debug)] @@ -927,7 +988,7 @@ mod events { pub update: GatewayEvent, pub guild_settings_update: GatewayEvent, pub presence_update: GatewayEvent, - pub typing_start_event: GatewayEvent, + pub typing_start: GatewayEvent, } #[derive(Default, Debug)] diff --git a/src/instance.rs b/src/instance.rs index 9795557..72bf350 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -1,24 +1,23 @@ -use std::cell::RefCell; +//! Instance and ChorusUser objects. + use std::collections::HashMap; use std::fmt; -use std::rc::Rc; + +use std::sync::{Arc, RwLock}; use reqwest::Client; use serde::{Deserialize, Serialize}; -use crate::api::{Limit, LimitType}; use crate::errors::ChorusResult; use crate::gateway::{Gateway, GatewayHandle}; use crate::ratelimiter::ChorusRequest; use crate::types::types::subconfigs::limits::rates::RateLimits; -use crate::types::{GeneralConfiguration, User, UserSettings}; +use crate::types::{GeneralConfiguration, Limit, LimitType, User, UserSettings}; use crate::UrlBundle; -#[derive(Debug, Clone)] -/** -The [`Instance`] what you will be using to perform all sorts of actions on the Spacebar server. -If `limits_information` is `None`, then the instance will not be rate limited. - */ +#[derive(Debug, Clone, Default)] +/// The [`Instance`]; what you will be using to perform all sorts of actions on the Spacebar server. +/// If `limits_information` is `None`, then the instance will not be rate limited. pub struct Instance { pub urls: UrlBundle, pub instance_info: GeneralConfiguration, @@ -26,19 +25,14 @@ pub struct Instance { pub client: Client, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct LimitsInformation { pub ratelimits: HashMap, pub configuration: RateLimits, } impl Instance { - /// Creates a new [`Instance`]. - /// # Arguments - /// * `urls` - The [`URLBundle`] that contains all the URLs that are needed to connect to the Spacebar server. - /// * `requester` - The [`LimitedRequester`] that will be used to make requests to the Spacebar server. - /// # Errors - /// * [`InstanceError`] - If the instance cannot be created. + /// Creates a new [`Instance`] from the [relevant instance urls](UrlBundle), where `limited` is whether or not to automatically use rate limits. pub async fn new(urls: UrlBundle, limited: bool) -> ChorusResult { let limits_information; if limited { @@ -89,17 +83,20 @@ impl fmt::Display for Token { } } -#[derive(Debug)] -pub struct UserMeta { - pub belongs_to: Rc>, +#[derive(Debug, Clone)] +/// A ChorusUser is a representation of an authenticated user on an [Instance]. +/// It is used for most authenticated actions on a Spacebar server. +/// It also has its own [Gateway] connection. +pub struct ChorusUser { + pub belongs_to: Arc>, pub token: String, pub limits: Option>, - pub settings: UserSettings, - pub object: User, + pub settings: Arc>, + pub object: Arc>, pub gateway: GatewayHandle, } -impl UserMeta { +impl ChorusUser { pub fn token(&self) -> String { self.token.clone() } @@ -108,15 +105,20 @@ impl UserMeta { self.token = token; } + /// Creates a new [ChorusUser] from existing data. + /// + /// # Notes + /// This isn't the prefered way to create a ChorusUser. + /// See [Instance::login_account] and [Instance::register_account] instead. pub fn new( - belongs_to: Rc>, + belongs_to: Arc>, token: String, limits: Option>, - settings: UserSettings, - object: User, + settings: Arc>, + object: Arc>, gateway: GatewayHandle, - ) -> UserMeta { - UserMeta { + ) -> ChorusUser { + ChorusUser { belongs_to, token, limits, @@ -127,21 +129,22 @@ impl UserMeta { } /// Creates a new 'shell' of a user. The user does not exist as an object, and exists so that you have - /// a UserMeta object to make Rate Limited requests with. This is useful in scenarios like + /// a ChorusUser object to make Rate Limited requests with. This is useful in scenarios like /// registering or logging in to the Instance, where you do not yet have a User object, but still /// need to make a RateLimited request. To use the [`GatewayHandle`], you will have to identify /// first. - pub(crate) async fn shell(instance: Rc>, token: String) -> UserMeta { - let settings = UserSettings::default(); - let object = User::default(); - let wss_url = instance.borrow().urls.wss.clone(); + pub(crate) async fn shell(instance: Arc>, token: String) -> ChorusUser { + let settings = Arc::new(RwLock::new(UserSettings::default())); + let object = Arc::new(RwLock::new(User::default())); + let wss_url = instance.read().unwrap().urls.wss.clone(); // Dummy gateway object let gateway = Gateway::new(wss_url).await.unwrap(); - UserMeta { + ChorusUser { token, belongs_to: instance.clone(), limits: instance - .borrow() + .read() + .unwrap() .limits_information .as_ref() .map(|info| info.ratelimits.clone()), diff --git a/src/lib.rs b/src/lib.rs index 77425b8..e63c41d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,19 @@ +//! A library for interacting with one or multiple Spacebar-compatible APIs and Gateways. +//! +//! # About +//!Chorus is a Rust library that allows developers to interact with multiple Spacebar-compatible APIs and Gateways simultaneously. The library provides a simple and efficient way to communicate with these services, making it easier for developers to build applications that rely on them. Chorus is open-source and welcomes contributions from the community. +#![doc( + html_logo_url = "https://raw.githubusercontent.com/polyphony-chat/design/main/branding/polyphony-chorus-round-8bit.png" +)] #![allow(clippy::module_inception)] +#![deny( + missing_debug_implementations, + clippy::extra_unused_lifetimes, + clippy::from_over_into, + clippy::needless_borrow, + clippy::new_without_default, + clippy::useless_conversion +)] use url::{ParseError, Url}; @@ -15,16 +30,26 @@ pub mod types; #[cfg(feature = "client")] pub mod voice; -#[derive(Clone, Default, Debug, PartialEq, Eq)] -/// A URLBundle is a struct which bundles together the API-, Gateway- and CDN-URLs of a Spacebar -/// instance. +#[derive(Clone, Default, Debug, PartialEq, Eq, Hash)] +/// A URLBundle bundles together the API-, Gateway- and CDN-URLs of a Spacebar instance. +/// +/// # Notes +/// All the urls can be found on the /api/policies/instance/domains endpoint of a spacebar server pub struct UrlBundle { + /// The api's url. + /// Ex: `https://old.server.spacebar.chat/api` pub api: String, + /// The gateway websocket url. + /// Note that because this is a websocket url, it will always start with `wss://` or `ws://` + /// Ex: `wss://gateway.old.server.spacebar.chat` pub wss: String, + /// The CDN's url. + /// Ex: `https://cdn.old.server.spacebar.chat` pub cdn: String, } impl UrlBundle { + /// Creates a new UrlBundle from the relevant urls. pub fn new(api: String, wss: String, cdn: String) -> Self { Self { api: UrlBundle::parse_url(api), @@ -33,9 +58,10 @@ impl UrlBundle { } } - /// parse(url: String) parses a URL using the Url library and formats it in a standardized - /// way. If no protocol is given, HTTP (not HTTPS) is assumed. - /// # Example: + /// Parses a URL using the Url library and formats it in a standardized way. + /// If no protocol is given, HTTP (not HTTPS) is assumed. + /// + /// # Examples: /// ```rs /// let url = parse_url("localhost:3000"); /// ``` diff --git a/src/ratelimiter.rs b/src/ratelimiter.rs index 9227737..88d4a02 100644 --- a/src/ratelimiter.rs +++ b/src/ratelimiter.rs @@ -1,3 +1,5 @@ +//! Ratelimiter and request handling functionality. + use std::collections::HashMap; use log::{self, debug}; @@ -6,37 +8,81 @@ use serde::Deserialize; use serde_json::from_str; use crate::{ - api::{Limit, LimitType}, errors::{ChorusError, ChorusResult}, - instance::UserMeta, - types::{types::subconfigs::limits::rates::RateLimits, LimitsConfiguration}, + instance::ChorusUser, + types::{types::subconfigs::limits::rates::RateLimits, Limit, LimitType, LimitsConfiguration}, }; /// Chorus' request struct. This struct is used to send rate-limited requests to the Spacebar server. /// See for more information. +#[derive(Debug)] pub struct ChorusRequest { pub request: RequestBuilder, pub limit_type: LimitType, } impl ChorusRequest { + /// Makes a new [`ChorusRequest`]. + /// # Arguments + /// * `method` - The HTTP method to use. Must be one of the following: + /// * [`http::Method::GET`] + /// * [`http::Method::POST`] + /// * [`http::Method::PUT`] + /// * [`http::Method::DELETE`] + /// * [`http::Method::PATCH`] + /// * [`http::Method::HEAD`] + #[allow(unused_variables)] // TODO: Add mfa_token to request, once we figure out *how* to do so correctly + pub fn new( + method: http::Method, + url: &str, + body: Option, + audit_log_reason: Option<&str>, + mfa_token: Option<&str>, + chorus_user: Option<&mut ChorusUser>, + limit_type: LimitType, + ) -> ChorusRequest { + let request = Client::new(); + let mut request = match method { + http::Method::GET => request.get(url), + http::Method::POST => request.post(url), + http::Method::PUT => request.put(url), + http::Method::DELETE => request.delete(url), + http::Method::PATCH => request.patch(url), + http::Method::HEAD => request.head(url), + _ => panic!("Illegal state: Method not supported."), + }; + if let Some(user) = chorus_user { + request = request.header("Authorization", user.token()); + } + if let Some(body) = body { + // ONCE TOLD ME THE WORLD WAS GONNA ROLL ME + request = request + .body(body) + .header("Content-Type", "application/json"); + } + if let Some(reason) = audit_log_reason { + request = request.header("X-Audit-Log-Reason", reason); + } + + ChorusRequest { + request, + limit_type, + } + } + /// Sends a [`ChorusRequest`]. Checks if the user is rate limited, and if not, sends the request. /// If the user is not rate limited and the instance has rate limits enabled, it will update the /// rate limits. #[allow(clippy::await_holding_refcell_ref)] - pub(crate) async fn send_request(self, user: &mut UserMeta) -> ChorusResult { + pub(crate) async fn send_request(self, user: &mut ChorusUser) -> ChorusResult { if !ChorusRequest::can_send_request(user, &self.limit_type) { log::info!("Rate limit hit. Bucket: {:?}", self.limit_type); return Err(ChorusError::RateLimited { bucket: format!("{:?}", self.limit_type), }); } - let belongs_to = user.belongs_to.borrow(); - let result = match belongs_to - .client - .execute(self.request.build().unwrap()) - .await - { + let client = user.belongs_to.read().unwrap().client.clone(); + let result = match client.execute(self.request.build().unwrap()).await { Ok(result) => { debug!("Request successful: {:?}", result); result @@ -45,16 +91,17 @@ impl ChorusRequest { log::warn!("Request failed: {:?}", error); return Err(ChorusError::RequestFailed { url: error.url().unwrap().to_string(), - error, + error: error.to_string(), }); } }; - drop(belongs_to); + drop(client); if !result.status().is_success() { if result.status().as_u16() == 429 { log::warn!("Rate limit hit unexpectedly. Bucket: {:?}. Setting the instances' remaining global limit to 0 to have cooldown.", self.limit_type); user.belongs_to - .borrow_mut() + .write() + .unwrap() .limits_information .as_mut() .unwrap() @@ -73,9 +120,9 @@ impl ChorusRequest { Ok(result) } - fn can_send_request(user: &mut UserMeta, limit_type: &LimitType) -> bool { + fn can_send_request(user: &mut ChorusUser, limit_type: &LimitType) -> bool { log::trace!("Checking if user or instance is rate-limited..."); - let mut belongs_to = user.belongs_to.borrow_mut(); + let mut belongs_to = user.belongs_to.write().unwrap(); if belongs_to.limits_information.is_none() { log::trace!("Instance indicates no rate limits are configured. Continuing."); return true; @@ -236,7 +283,10 @@ impl ChorusRequest { /// set to the current unix timestamp + the rate limit window. The remaining rate limit is /// reset to the rate limit limit. /// 2. The remaining rate limit is decreased by 1. - fn update_rate_limits(user: &mut UserMeta, limit_type: &LimitType, response_was_err: bool) { + fn update_rate_limits(user: &mut ChorusUser, limit_type: &LimitType, response_was_err: bool) { + if user.belongs_to.read().unwrap().limits_information.is_none() { + return; + } let instance_dictated_limits = [ &LimitType::AuthLogin, &LimitType::AuthRegister, @@ -257,7 +307,7 @@ impl ChorusRequest { } let time: u64 = chrono::Utc::now().timestamp() as u64; for relevant_limit in relevant_limits.iter() { - let mut belongs_to = user.belongs_to.borrow_mut(); + let mut belongs_to = user.belongs_to.write().unwrap(); let limit = match relevant_limit.0 { LimitOrigin::Instance => { log::trace!( @@ -292,6 +342,13 @@ impl ChorusRequest { } } + /// Gets the ratelimit configuration. + /// + /// # Notes + /// This is a spacebar only endpoint. + /// + /// # Reference + /// See pub(crate) async fn get_limits_config(url_api: &str) -> ChorusResult { let request = Client::new() .get(format!("{}/policies/instance/limits/", url_api)) @@ -302,7 +359,7 @@ impl ChorusRequest { Err(e) => { return Err(ChorusError::RequestFailed { url: url_api.to_string(), - error: e, + error: e.to_string(), }) } }; @@ -419,7 +476,7 @@ impl ChorusRequest { /// Sends a [`ChorusRequest`] and returns a [`ChorusResult`] that contains nothing if the request /// was successful, or a [`ChorusError`] if the request failed. - pub(crate) async fn handle_request_as_result(self, user: &mut UserMeta) -> ChorusResult<()> { + pub(crate) async fn handle_request_as_result(self, user: &mut ChorusUser) -> ChorusResult<()> { match self.send_request(user).await { Ok(_) => Ok(()), Err(e) => Err(e), @@ -430,7 +487,7 @@ impl ChorusRequest { /// was successful, or a [`ChorusError`] if the request failed. pub(crate) async fn deserialize_response Deserialize<'a>>( self, - user: &mut UserMeta, + user: &mut ChorusUser, ) -> ChorusResult { let response = self.send_request(user).await?; debug!("Got response: {:?}", response); diff --git a/src/types/config/types/guild_configuration.rs b/src/types/config/types/guild_configuration.rs index 96e6ea8..65897ea 100644 --- a/src/types/config/types/guild_configuration.rs +++ b/src/types/config/types/guild_configuration.rs @@ -18,7 +18,7 @@ use crate::types::config::types::subconfigs::guild::{ }; use crate::types::{Error, GuildError}; -#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)] +#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize, Hash)] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum GuildFeatures { ActivitiesAlpha, @@ -139,7 +139,7 @@ pub enum GuildFeatures { InvitesClosed, } -#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize, Eq)] +#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize, Eq, Hash)] pub struct GuildFeaturesList(Vec); impl Deref for GuildFeaturesList { diff --git a/src/types/config/types/subconfigs/limits/rates.rs b/src/types/config/types/subconfigs/limits/rates.rs index ce1ea60..8fdd183 100644 --- a/src/types/config/types/subconfigs/limits/rates.rs +++ b/src/types/config/types/subconfigs/limits/rates.rs @@ -2,11 +2,9 @@ use std::collections::HashMap; use serde::{Deserialize, Serialize}; -use crate::{ - api::LimitType, - types::config::types::subconfigs::limits::ratelimits::{ - route::RouteRateLimit, RateLimitOptions, - }, +use crate::types::{ + config::types::subconfigs::limits::ratelimits::{route::RouteRateLimit, RateLimitOptions}, + LimitType, }; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] diff --git a/src/types/entities/application.rs b/src/types/entities/application.rs index 6cac20b..0b55626 100644 --- a/src/types/entities/application.rs +++ b/src/types/entities/application.rs @@ -1,3 +1,5 @@ +use std::sync::{Arc, RwLock}; + use bitflags::bitflags; use serde::{Deserialize, Serialize}; use serde_json::Value; @@ -6,8 +8,10 @@ use serde_repr::{Deserialize_repr, Serialize_repr}; use crate::types::utils::Snowflake; use crate::types::{Team, User}; -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] #[cfg_attr(feature = "sqlx", derive(sqlx::FromRow))] +/// # Reference +/// See pub struct Application { pub id: Snowflake, pub name: String, @@ -23,7 +27,7 @@ pub struct Application { pub bot_require_code_grant: bool, pub verify_key: String, #[cfg_attr(feature = "sqlx", sqlx(skip))] - pub owner: User, + pub owner: Arc>, pub flags: u64, #[cfg(feature = "sqlx")] pub redirect_uris: Option>>, @@ -45,7 +49,7 @@ pub struct Application { #[cfg(feature = "sqlx")] pub install_params: Option>, #[cfg(not(feature = "sqlx"))] - pub install_params: Option, + pub install_params: Option>>, pub terms_of_service_url: Option, pub privacy_policy_url: Option, #[cfg_attr(feature = "sqlx", sqlx(skip))] @@ -93,44 +97,64 @@ impl Application { } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +/// # Reference +/// See pub struct InstallParams { pub scopes: Vec, pub permissions: String, } bitflags! { - #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] + #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)] + /// # Reference + /// See pub struct ApplicationFlags: u64 { + /// Indicates if an app uses the Auto Moderation API const APPLICATION_AUTO_MODERATION_RULE_CREATE_BADGE = 1 << 6; + /// Intent required for bots in 100 or more servers to receive presence_update events const GATEWAY_PRESENCE = 1 << 12; + /// Intent required for bots in under 100 servers to receive presence_update events, found on the Bot page in your app's settings on discord.com const GATEWAY_PRESENCE_LIMITED = 1 << 13; + /// Intent required for bots in 100 or more servers to receive member-related events like guild_member_add. + /// See the list of member-related events under GUILD_MEMBERS const GATEWAY_GUILD_MEMBERS = 1 << 14; + /// Intent required for bots in under 100 servers to receive member-related events like guild_member_add, found on the Bot page in your app's settings on discord.com. + /// See the list of member-related events under GUILD_MEMBERS const GATEWAY_GUILD_MEMBERS_LIMITED = 1 << 15; + /// Indicates unusual growth of an app that prevents verification const VERIFICATION_PENDING_GUILD_LIMIT = 1 << 16; + /// Indicates if an app is embedded within the Discord client (currently unavailable publicly) const EMBEDDED = 1 << 17; + /// Intent required for bots in 100 or more servers to receive message content const GATEWAY_MESSAGE_CONTENT = 1 << 18; + /// Intent required for bots in under 100 servers to receive message content, found on the Bot page in your app's settings on discord.com const GATEWAY_MESSAGE_CONTENT_LIMITED = 1 << 19; + /// Indicates if an app has registered slash commands const APPLICATION_COMMAND_BADGE = 1 << 23; } } -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] +/// # Reference +/// See pub struct ApplicationCommand { pub id: Snowflake, pub application_id: Snowflake, pub name: String, pub description: String, - pub options: Vec, + pub options: Vec>>, } -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] +/// Reference +/// See pub struct ApplicationCommandOption { pub r#type: ApplicationCommandOptionType, pub name: String, pub description: String, pub required: bool, pub choices: Vec, - pub options: Vec, + pub options: Arc>>, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] @@ -139,45 +163,54 @@ pub struct ApplicationCommandOptionChoice { pub value: Value, } -#[derive(Debug, Clone, Copy, PartialEq, Serialize_repr, Deserialize_repr)] +#[derive(Debug, Clone, Copy, Serialize_repr, Deserialize_repr, PartialEq, Eq, Hash)] #[cfg_attr(feature = "sqlx", derive(sqlx::Type))] #[repr(i32)] +/// # Reference +/// See pub enum ApplicationCommandOptionType { SubCommand = 1, SubCommandGroup = 2, String = 3, + /// Any integer between -2^53 and 2^53 Integer = 4, Boolean = 5, User = 6, + /// Includes all channel types + categories Channel = 7, Role = 8, + /// Includes users and roles + Mentionable = 9, + /// Any double between -2^53 and 2^53 + Number = 10, + Attachment = 11, } -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct ApplicationCommandInteractionData { pub id: Snowflake, pub name: String, - pub options: Vec, + pub options: Vec>>, } -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct ApplicationCommandInteractionDataOption { pub name: String, pub value: Value, - pub options: Vec, + pub options: Vec>>, } -#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)] -/// See https://discord.com/developers/docs/interactions/application-commands#application-command-permissions-object-guild-application-command-permissions-structure +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +/// See pub struct GuildApplicationCommandPermissions { pub id: Snowflake, pub application_id: Snowflake, pub guild_id: Snowflake, - pub permissions: Vec, + pub permissions: Vec>>, } #[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)] -/// See https://discord.com/developers/docs/interactions/application-commands#application-command-permissions-object-application-command-permissions-structure +/// See pub struct ApplicationCommandPermission { pub id: Snowflake, #[serde(rename = "type")] @@ -186,10 +219,10 @@ pub struct ApplicationCommandPermission { pub permission: bool, } -#[derive(Serialize_repr, Deserialize_repr, Debug, Default, Clone, PartialEq)] +#[derive(Serialize_repr, Deserialize_repr, Debug, Default, Clone, PartialEq, Eq, Hash)] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] #[repr(u8)] -/// See https://discord.com/developers/docs/interactions/application-commands#application-command-permissions-object-application-command-permission-type +/// See pub enum ApplicationCommandPermissionType { #[default] Role = 1, diff --git a/src/types/entities/attachment.rs b/src/types/entities/attachment.rs index 7bd9d2b..ffbc520 100644 --- a/src/types/entities/attachment.rs +++ b/src/types/entities/attachment.rs @@ -2,11 +2,14 @@ use serde::{Deserialize, Serialize}; use crate::types::utils::Snowflake; -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, PartialOrd)] #[cfg_attr(feature = "sqlx", derive(sqlx::FromRow))] +/// # Reference +/// See pub struct Attachment { pub id: Snowflake, pub filename: String, + /// Max 1024 characters pub description: Option, pub content_type: Option, pub size: u64, @@ -15,17 +18,24 @@ pub struct Attachment { pub height: Option, pub width: Option, pub ephemeral: Option, + /// The duration of the audio file (only for voice messages) pub duration_secs: Option, + /// A Base64 encoded bytearray representing a sampled waveform (only for voice messages) + /// + /// # Notes + /// Note that this is computed on the client side. + /// This means it can be spoofed and isn't necessarily accurate. pub waveform: Option, #[serde(skip_serializing)] #[cfg_attr(feature = "sqlx", sqlx(default))] pub content: Option>, } -#[derive(Debug, Serialize, Deserialize, Clone)] +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] pub struct PartialDiscordFileAttachment { pub id: Option, pub filename: String, + /// Max 1024 characters pub description: Option, pub content_type: Option, pub size: Option, @@ -34,18 +44,20 @@ pub struct PartialDiscordFileAttachment { pub height: Option, pub width: Option, pub ephemeral: Option, + /// The duration of the audio file (only for voice messages) pub duration_secs: Option, + /// A Base64 encoded bytearray representing a sampled waveform (only for voice messages) + /// + /// # Notes + /// Note that this is computed on the client side. + /// This means it can be spoofed and isn't necessarily accurate. pub waveform: Option, #[serde(skip_serializing)] pub content: Vec, } impl PartialDiscordFileAttachment { - /** - Moves `self.content` out of `self` and returns it. - # Returns - Vec - */ + /// Moves `self.content` out of `self` and returns it. pub fn move_content(self) -> (Vec, PartialDiscordFileAttachment) { let content = self.content; let updated_struct = PartialDiscordFileAttachment { @@ -66,6 +78,7 @@ impl PartialDiscordFileAttachment { (content, updated_struct) } + /// Moves `self.filename` out of `self` and returns it. pub fn move_filename(self) -> (String, PartialDiscordFileAttachment) { let filename = self.filename; let updated_struct = PartialDiscordFileAttachment { @@ -87,6 +100,7 @@ impl PartialDiscordFileAttachment { (filename, updated_struct) } + /// Moves `self.content_type` out of `self` and returns it. pub fn move_content_type(self) -> (Option, PartialDiscordFileAttachment) { let content_type = self.content_type; let updated_struct = PartialDiscordFileAttachment { diff --git a/src/types/entities/audit_log.rs b/src/types/entities/audit_log.rs index 1e04e8b..be14f0f 100644 --- a/src/types/entities/audit_log.rs +++ b/src/types/entities/audit_log.rs @@ -1,12 +1,14 @@ +use std::sync::{Arc, RwLock}; + use serde::{Deserialize, Serialize}; use crate::types::utils::Snowflake; #[derive(Serialize, Deserialize, Debug, Default, Clone)] -/// See https://discord.com/developers/docs/resources/audit-log#audit-log-entry-object +/// See pub struct AuditLogEntry { pub target_id: Option, - pub changes: Option>, + pub changes: Option>>>, pub user_id: Option, pub id: Snowflake, // to:do implement an enum for these types @@ -17,7 +19,7 @@ pub struct AuditLogEntry { } #[derive(Serialize, Deserialize, Debug, Default, Clone)] -/// See https://discord.com/developers/docs/resources/audit-log#audit-log-change-object +/// See pub struct AuditLogChange { pub new_value: Option, pub old_value: Option, diff --git a/src/types/entities/auto_moderation.rs b/src/types/entities/auto_moderation.rs index 144aa4b..a8910b1 100644 --- a/src/types/entities/auto_moderation.rs +++ b/src/types/entities/auto_moderation.rs @@ -1,10 +1,19 @@ +use std::sync::{Arc, RwLock}; + +#[cfg(feature = "client")] +use crate::gateway::Updateable; + +#[cfg(feature = "client")] +use chorus_macros::Updateable; + use serde::{Deserialize, Serialize}; use serde_repr::{Deserialize_repr, Serialize_repr}; use crate::types::utils::Snowflake; +#[cfg_attr(feature = "client", derive(Updateable))] #[derive(Serialize, Deserialize, Debug, Default, Clone)] -/// See https://discord.com/developers/docs/resources/auto-moderation#auto-moderation-rule-object +/// See pub struct AutoModerationRule { pub id: Snowflake, pub guild_id: Snowflake, @@ -12,8 +21,8 @@ pub struct AutoModerationRule { pub creator_id: Snowflake, pub event_type: AutoModerationRuleEventType, pub trigger_type: AutoModerationRuleTriggerType, - pub trigger_metadata: AutoModerationRuleTriggerMetadata, - pub actions: Vec, + pub trigger_metadata: Arc>, + pub actions: Vec>>, pub enabled: bool, pub exempt_roles: Vec, pub exempt_channels: Vec, @@ -22,7 +31,7 @@ pub struct AutoModerationRule { #[derive(Serialize_repr, Deserialize_repr, Debug, Clone, Default)] #[repr(u8)] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] -/// See https://discord.com/developers/docs/resources/auto-moderation#auto-moderation-rule-object-event-types +/// See pub enum AutoModerationRuleEventType { #[default] MessageSend = 1, @@ -31,7 +40,7 @@ pub enum AutoModerationRuleEventType { #[derive(Serialize_repr, Deserialize_repr, Debug, Clone, Default)] #[repr(u8)] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] -/// See https://discord.com/developers/docs/resources/auto-moderation#auto-moderation-rule-object-trigger-types +/// See pub enum AutoModerationRuleTriggerType { #[default] Keyword = 1, @@ -42,7 +51,7 @@ pub enum AutoModerationRuleTriggerType { #[derive(Serialize, Deserialize, Debug, Clone, Default)] #[serde(untagged)] -/// See https://discord.com/developers/docs/resources/auto-moderation#auto-moderation-rule-object-trigger-metadata +/// See pub enum AutoModerationRuleTriggerMetadata { ForKeyword(AutoModerationRuleTriggerMetadataForKeyword), ForKeywordPreset(AutoModerationRuleTriggerMetadataForKeywordPreset), @@ -52,7 +61,7 @@ pub enum AutoModerationRuleTriggerMetadata { } #[derive(Serialize, Deserialize, Debug, Clone, Default)] -/// See https://discord.com/developers/docs/resources/auto-moderation#auto-moderation-rule-object-trigger-metadata +/// See pub struct AutoModerationRuleTriggerMetadataForKeyword { pub keyword_filter: Vec, pub regex_patterns: Vec, @@ -60,14 +69,14 @@ pub struct AutoModerationRuleTriggerMetadataForKeyword { } #[derive(Serialize, Deserialize, Debug, Clone, Default)] -/// See https://discord.com/developers/docs/resources/auto-moderation#auto-moderation-rule-object-trigger-metadata +/// See pub struct AutoModerationRuleTriggerMetadataForKeywordPreset { pub presets: Vec, pub allow_list: Vec, } #[derive(Serialize, Deserialize, Debug, Clone, Default)] -/// See https://discord.com/developers/docs/resources/auto-moderation#auto-moderation-rule-object-trigger-metadata +/// See pub struct AutoModerationRuleTriggerMetadataForMentionSpam { /// Max 50 pub mention_total_limit: u8, @@ -77,7 +86,7 @@ pub struct AutoModerationRuleTriggerMetadataForMentionSpam { #[derive(Serialize_repr, Deserialize_repr, Debug, Clone, Default)] #[repr(u8)] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] -/// See https://discord.com/developers/docs/resources/auto-moderation#auto-moderation-rule-object-keyword-preset-types +/// See pub enum AutoModerationRuleKeywordPresetType { #[default] Profanity = 1, @@ -86,17 +95,17 @@ pub enum AutoModerationRuleKeywordPresetType { } #[derive(Serialize, Deserialize, Debug, Clone, Default)] -/// See https://discord.com/developers/docs/resources/auto-moderation#auto-moderation-action-object +/// See pub struct AutoModerationAction { #[serde(rename = "type")] pub action_type: AutoModerationActionType, - pub metadata: Option, + pub metadata: Option>>, } #[derive(Serialize_repr, Deserialize_repr, Debug, Clone, Default)] #[repr(u8)] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] -/// See https://discord.com/developers/docs/resources/auto-moderation#auto-moderation-action-object-action-types +/// See pub enum AutoModerationActionType { #[default] BlockMessage = 1, @@ -106,7 +115,7 @@ pub enum AutoModerationActionType { #[derive(Serialize, Deserialize, Debug, Clone, Default)] #[serde(untagged)] -/// See https://discord.com/developers/docs/resources/auto-moderation#auto-moderation-action-object-action-metadata +/// See pub enum AutoModerationActionMetadata { ForBlockMessage(AutoModerationActionMetadataForBlockMessage), ForSendAlertMessage(AutoModerationActionMetadataForSendAlertMessage), @@ -116,19 +125,19 @@ pub enum AutoModerationActionMetadata { } #[derive(Serialize, Deserialize, Debug, Clone, Default)] -/// See https://discord.com/developers/docs/resources/auto-moderation#auto-moderation-action-object-action-metadata +/// See pub struct AutoModerationActionMetadataForBlockMessage { pub custom_message: Option, } #[derive(Serialize, Deserialize, Debug, Clone, Default)] -/// See https://discord.com/developers/docs/resources/auto-moderation#auto-moderation-action-object-action-metadata +/// See pub struct AutoModerationActionMetadataForSendAlertMessage { pub channel_id: Snowflake, } #[derive(Serialize, Deserialize, Debug, Clone, Default)] -/// See https://discord.com/developers/docs/resources/auto-moderation#auto-moderation-action-object-action-metadata +/// See pub struct AutoModerationActionMetadataForTimeout { /// Max 2419200 pub duration_seconds: u32, diff --git a/src/types/entities/channel.rs b/src/types/entities/channel.rs index 154b83c..280401c 100644 --- a/src/types/entities/channel.rs +++ b/src/types/entities/channel.rs @@ -1,17 +1,32 @@ -use chorus_macros::Updateable; -use chrono::Utc; +use std::sync::{Arc, RwLock}; + +use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use serde_aux::prelude::deserialize_string_from_number; use serde_repr::{Deserialize_repr, Serialize_repr}; +use std::fmt::Debug; -use crate::gateway::Updateable; use crate::types::{ entities::{GuildMember, User}, utils::Snowflake, }; -#[derive(Default, Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Updateable)] +#[cfg(feature = "client")] +use crate::types::Composite; + +#[cfg(feature = "client")] +use crate::gateway::{GatewayHandle, Updateable}; + +#[cfg(feature = "client")] +use chorus_macros::{observe_option_vec, Composite, Updateable}; + +#[derive(Default, Debug, Serialize, Deserialize, Clone)] #[cfg_attr(feature = "sqlx", derive(sqlx::FromRow))] +#[cfg_attr(feature = "client", derive(Updateable, Composite))] +/// Represents a guild or private channel +/// +/// # Reference +/// See pub struct Channel { pub application_id: Option, #[cfg(feature = "sqlx")] @@ -39,7 +54,7 @@ pub struct Channel { pub icon: Option, pub id: Snowflake, pub last_message_id: Option, - pub last_pin_timestamp: Option, + pub last_pin_timestamp: Option>, pub managed: Option, #[cfg_attr(feature = "sqlx", sqlx(skip))] pub member: Option, @@ -52,12 +67,14 @@ pub struct Channel { #[cfg(feature = "sqlx")] pub permission_overwrites: Option>>, #[cfg(not(feature = "sqlx"))] - pub permission_overwrites: Option>, + #[cfg_attr(feature = "client", observe_option_vec)] + pub permission_overwrites: Option>>>, pub permissions: Option, pub position: Option, pub rate_limit_per_user: Option, #[cfg_attr(feature = "sqlx", sqlx(skip))] - pub recipients: Option>, + #[cfg_attr(feature = "client", observe_option_vec)] + pub recipients: Option>>>, pub rtc_region: Option, #[cfg_attr(feature = "sqlx", sqlx(skip))] pub thread_metadata: Option, @@ -67,16 +84,57 @@ pub struct Channel { pub video_quality_mode: Option, } -#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] +impl PartialEq for Channel { + fn eq(&self, other: &Self) -> bool { + self.application_id == other.application_id + && self.bitrate == other.bitrate + && self.channel_type == other.channel_type + && self.created_at == other.created_at + && self.default_auto_archive_duration == other.default_auto_archive_duration + && self.default_forum_layout == other.default_forum_layout + && self.default_sort_order == other.default_sort_order + && self.default_thread_rate_limit_per_user == other.default_thread_rate_limit_per_user + && self.flags == other.flags + && self.guild_id == other.guild_id + && self.icon == other.icon + && self.id == other.id + && self.last_message_id == other.last_message_id + && self.last_pin_timestamp == other.last_pin_timestamp + && self.managed == other.managed + && self.member_count == other.member_count + && self.message_count == other.message_count + && self.name == other.name + && self.nsfw == other.nsfw + && self.owner_id == other.owner_id + && self.parent_id == other.parent_id + && self.permissions == other.permissions + && self.position == other.position + && self.rate_limit_per_user == other.rate_limit_per_user + && self.rtc_region == other.rtc_region + && self.topic == other.topic + && self.total_message_sent == other.total_message_sent + && self.user_limit == other.user_limit + && self.video_quality_mode == other.video_quality_mode + } +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +/// A tag that can be applied to a thread in a [ChannelType::GuildForum] or [ChannelType::GuildMedia] channel. +/// +/// # Reference +/// See pub struct Tag { pub id: Snowflake, + /// The name of the tag (max 20 characters) pub name: String, + /// Whether this tag can only be added to or removed from threads by members with the [MANAGE_THREADS](crate::types::PermissionFlags::MANAGE_THREADS) permission pub moderated: bool, pub emoji_id: Option, pub emoji_name: Option, } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, PartialOrd)] +#[cfg_attr(feature = "client", derive(Updateable, Composite))] pub struct PermissionOverwrite { pub id: Snowflake, #[serde(rename = "type")] @@ -91,6 +149,8 @@ pub struct PermissionOverwrite { } #[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] +/// # Reference +/// See pub struct ThreadMetadata { pub archived: bool, pub auto_archive_duration: i32, @@ -100,47 +160,93 @@ pub struct ThreadMetadata { pub create_timestamp: Option, } -#[derive(Default, Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] +#[derive(Default, Debug, Deserialize, Serialize, Clone)] +/// # Reference +/// See pub struct ThreadMember { pub id: Option, pub user_id: Option, pub join_timestamp: Option, pub flags: Option, - pub member: Option, + pub member: Option>>, } #[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] +/// Specifies the emoji to use as the default way to react to a [ChannelType::GuildForum] or [ChannelType::GuildMedia] channel post. +/// +/// # Reference +/// See pub struct DefaultReaction { #[serde(default)] pub emoji_id: Option, pub emoji_name: Option, } -#[derive(Default, Clone, Copy, Debug, Serialize_repr, Deserialize_repr, PartialEq, Eq)] +#[derive( + Default, + Clone, + Copy, + Debug, + Serialize_repr, + Deserialize_repr, + PartialEq, + Eq, + Hash, + PartialOrd, + Ord, +)] #[cfg_attr(feature = "sqlx", derive(sqlx::Type))] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] -#[repr(i32)] +#[repr(u32)] +/// # Reference +/// See pub enum ChannelType { #[default] + /// A text channel within a guild GuildText = 0, + /// A private channel between two users Dm = 1, + /// A voice channel within a guild GuildVoice = 2, + /// A private channel between multiple users GroupDm = 3, + /// An organizational category that contains up to 50 channels GuildCategory = 4, + /// Similar to [GuildText](ChannelType::GuildText), a channel that users can follow and crosspost into their own guild GuildNews = 5, + /// A channel in which game developers can sell their game on Discord + /// + /// # Note + /// Deprecated. GuildStore = 6, + // FIXME userdoccers says 7 is GuildLfg, is this a spacebar specific thing? Encrypted = 7, + // FIXME userdoccers says 8 is LfgGuildDm, is this a spacebar specific thing? EncryptedThreads = 8, + // FIXME userdoccers says 9 is ThreadAlpha, was this changed? Transactional = 9, + /// A thread within a [GuildNews](ChannelType::GuildNews) channel GuildNewsThread = 10, + /// A thread within a [GuildText](ChannelType::GuildText), [GuildForum](ChannelType::GuildForum), or [GuildMedia](ChannelType::GuildMedia) channel GuildPublicThread = 11, + /// A thread within a [GuildText](ChannelType::GuildText) channel, that is only viewable by those invited and those with the [MANAGE_THREADS](crate::types::entities::PermissionFlags::MANAGE_THREADS) permission GuildPrivateThread = 12, + /// A voice channel for hosting events with an audience in a guild GuildStageVoice = 13, + /// The main channel in a hub containing the listed guilds Directory = 14, + /// A channel that can only contain threads GuildForum = 15, + /// A channel that can only contain threads in a gallery view + GuildMedia = 16, + // TODO: Couldn't find reference TicketTracker = 33, + // TODO: Couldn't find reference Kanban = 34, + // TODO: Couldn't find reference VoicelessWhiteboard = 35, + // TODO: Couldn't find reference CustomStart = 64, + // TODO: Couldn't find reference Unhandled = 255, } diff --git a/src/types/entities/emoji.rs b/src/types/entities/emoji.rs index a0e8d14..4f56af5 100644 --- a/src/types/entities/emoji.rs +++ b/src/types/entities/emoji.rs @@ -1,21 +1,95 @@ +use std::fmt::Debug; +use std::sync::{Arc, RwLock}; + use serde::{Deserialize, Serialize}; use crate::types::entities::User; use crate::types::Snowflake; -#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize, Default)] +#[cfg(feature = "client")] +use crate::types::Composite; + +#[cfg(feature = "client")] +use crate::gateway::{GatewayHandle, Updateable}; + +#[cfg(feature = "client")] +use chorus_macros::{Composite, Updateable}; + +#[derive(Debug, Clone, Deserialize, Serialize, Default)] +#[cfg_attr(feature = "client", derive(Updateable, Composite))] #[cfg_attr(feature = "sqlx", derive(sqlx::FromRow))] +/// # Reference +/// See pub struct Emoji { - pub id: Option, + pub id: Snowflake, pub name: Option, #[cfg(feature = "sqlx")] pub roles: Option>>, #[cfg(not(feature = "sqlx"))] pub roles: Option>, #[cfg_attr(feature = "sqlx", sqlx(skip))] - pub user: Option, + pub user: Option>>, pub require_colons: Option, pub managed: Option, pub animated: Option, pub available: Option, } + +impl std::hash::Hash for Emoji { + fn hash(&self, state: &mut H) { + self.id.hash(state); + self.name.hash(state); + self.roles.hash(state); + self.roles.hash(state); + self.require_colons.hash(state); + self.managed.hash(state); + self.animated.hash(state); + self.available.hash(state); + } +} + +impl PartialEq for Emoji { + fn eq(&self, other: &Self) -> bool { + !(self.id != other.id + || self.name != other.name + || self.roles != other.roles + || self.require_colons != other.require_colons + || self.managed != other.managed + || self.animated != other.animated + || self.available != other.available) + } +} + +impl PartialOrd for Emoji { + fn partial_cmp(&self, other: &Self) -> Option { + match self.id.partial_cmp(&other.id) { + Some(core::cmp::Ordering::Equal) => {} + ord => return ord, + } + match self.name.partial_cmp(&other.name) { + Some(core::cmp::Ordering::Equal) => {} + ord => return ord, + } + match self.roles.partial_cmp(&other.roles) { + Some(core::cmp::Ordering::Equal) => {} + ord => return ord, + } + match self.roles.partial_cmp(&other.roles) { + Some(core::cmp::Ordering::Equal) => {} + ord => return ord, + } + match self.require_colons.partial_cmp(&other.require_colons) { + Some(core::cmp::Ordering::Equal) => {} + ord => return ord, + } + match self.managed.partial_cmp(&other.managed) { + Some(core::cmp::Ordering::Equal) => {} + ord => return ord, + } + match self.animated.partial_cmp(&other.animated) { + Some(core::cmp::Ordering::Equal) => {} + ord => return ord, + } + self.available.partial_cmp(&other.available) + } +} diff --git a/src/types/entities/guild.rs b/src/types/entities/guild.rs index 67e7f44..bb4db0c 100644 --- a/src/types/entities/guild.rs +++ b/src/types/entities/guild.rs @@ -1,3 +1,6 @@ +use std::fmt::Debug; +use std::sync::{Arc, RwLock}; + use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use serde_repr::{Deserialize_repr, Serialize_repr}; @@ -8,9 +11,22 @@ use crate::types::{ interfaces::WelcomeScreenObject, utils::Snowflake, }; +use bitflags::bitflags; -/// See https://discord.com/developers/docs/resources/guild -#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq)] +use super::PublicUser; + +#[cfg(feature = "client")] +use crate::gateway::{GatewayHandle, Updateable}; + +#[cfg(feature = "client")] +use chorus_macros::{observe_option_vec, observe_vec, Composite, Updateable}; + +#[cfg(feature = "client")] +use crate::types::Composite; + +/// See +#[derive(Serialize, Deserialize, Debug, Default, Clone)] +#[cfg_attr(feature = "client", derive(Updateable, Composite))] #[cfg_attr(feature = "sqlx", derive(sqlx::FromRow))] pub struct Guild { pub afk_channel_id: Option, @@ -25,13 +41,15 @@ pub struct Guild { #[cfg_attr(feature = "sqlx", sqlx(skip))] pub bans: Option>, #[cfg_attr(feature = "sqlx", sqlx(skip))] - pub channels: Option>, - pub default_message_notifications: Option, + #[cfg_attr(feature = "client", observe_option_vec)] + pub channels: Option>>>, + pub default_message_notifications: Option, pub description: Option, pub discovery_splash: Option, #[cfg_attr(feature = "sqlx", sqlx(skip))] + #[cfg_attr(feature = "client", observe_vec)] #[serde(default)] - pub emojis: Vec, + pub emojis: Vec>>, pub explicit_content_filter: Option, //#[cfg_attr(feature = "sqlx", sqlx(try_from = "String"))] pub features: Option, @@ -49,9 +67,9 @@ pub struct Guild { #[cfg_attr(feature = "sqlx", sqlx(skip))] pub max_stage_video_channel_users: Option, pub max_video_channel_users: Option, - pub mfa_level: Option, + pub mfa_level: Option, pub name: Option, - pub nsfw_level: Option, + pub nsfw_level: Option, #[cfg_attr(feature = "sqlx", sqlx(skip))] pub owner: Option, // True if requesting user is owner @@ -61,27 +79,30 @@ pub struct Guild { pub preferred_locale: Option, pub premium_progress_bar_enabled: Option, pub premium_subscription_count: Option, - pub premium_tier: Option, + pub premium_tier: Option, pub primary_category_id: Option, pub public_updates_channel_id: Option, pub region: Option, #[cfg_attr(feature = "sqlx", sqlx(skip))] - pub roles: Option>, + #[cfg_attr(feature = "client", observe_option_vec)] + pub roles: Option>>>, #[cfg_attr(feature = "sqlx", sqlx(skip))] pub rules_channel: Option, pub rules_channel_id: Option, pub splash: Option, #[cfg_attr(feature = "sqlx", sqlx(skip))] pub stickers: Option>, - pub system_channel_flags: Option, + pub system_channel_flags: Option, pub system_channel_id: Option, #[cfg_attr(feature = "sqlx", sqlx(skip))] pub vanity_url_code: Option, - pub verification_level: Option, + pub verification_level: Option, #[cfg_attr(feature = "sqlx", sqlx(skip))] - pub voice_states: Option>, + #[cfg_attr(feature = "client", observe_option_vec)] + pub voice_states: Option>>>, #[cfg_attr(feature = "sqlx", sqlx(skip))] - pub webhooks: Option>, + #[cfg_attr(feature = "client", observe_option_vec)] + pub webhooks: Option>>>, #[cfg(feature = "sqlx")] pub welcome_screen: Option>, #[cfg(not(feature = "sqlx"))] @@ -90,17 +111,121 @@ pub struct Guild { pub widget_enabled: Option, } -/// See https://docs.spacebar.chat/routes/#get-/guilds/-guild_id-/bans/-user- -#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, Eq)] +impl std::hash::Hash for Guild { + fn hash(&self, state: &mut H) { + self.afk_channel_id.hash(state); + self.afk_timeout.hash(state); + self.application_id.hash(state); + self.approximate_member_count.hash(state); + self.approximate_presence_count.hash(state); + self.banner.hash(state); + self.bans.hash(state); + self.default_message_notifications.hash(state); + self.description.hash(state); + self.discovery_splash.hash(state); + self.explicit_content_filter.hash(state); + self.features.hash(state); + self.icon.hash(state); + self.icon_hash.hash(state); + self.id.hash(state); + self.invites.hash(state); + self.joined_at.hash(state); + self.large.hash(state); + self.max_members.hash(state); + self.max_presences.hash(state); + self.max_stage_video_channel_users.hash(state); + self.max_video_channel_users.hash(state); + self.mfa_level.hash(state); + self.name.hash(state); + self.nsfw_level.hash(state); + self.owner.hash(state); + self.owner_id.hash(state); + self.permissions.hash(state); + self.preferred_locale.hash(state); + self.premium_progress_bar_enabled.hash(state); + self.premium_subscription_count.hash(state); + self.premium_tier.hash(state); + self.primary_category_id.hash(state); + self.public_updates_channel_id.hash(state); + self.region.hash(state); + self.rules_channel.hash(state); + self.rules_channel_id.hash(state); + self.splash.hash(state); + self.stickers.hash(state); + self.system_channel_flags.hash(state); + self.system_channel_id.hash(state); + self.vanity_url_code.hash(state); + self.verification_level.hash(state); + self.welcome_screen.hash(state); + self.welcome_screen.hash(state); + self.widget_channel_id.hash(state); + self.widget_enabled.hash(state); + } +} + +impl std::cmp::PartialEq for Guild { + fn eq(&self, other: &Self) -> bool { + self.afk_channel_id == other.afk_channel_id + && self.afk_timeout == other.afk_timeout + && self.application_id == other.application_id + && self.approximate_member_count == other.approximate_member_count + && self.approximate_presence_count == other.approximate_presence_count + && self.banner == other.banner + && self.bans == other.bans + && self.default_message_notifications == other.default_message_notifications + && self.description == other.description + && self.discovery_splash == other.discovery_splash + && self.explicit_content_filter == other.explicit_content_filter + && self.features == other.features + && self.icon == other.icon + && self.icon_hash == other.icon_hash + && self.id == other.id + && self.joined_at == other.joined_at + && self.large == other.large + && self.max_members == other.max_members + && self.max_presences == other.max_presences + && self.max_stage_video_channel_users == other.max_stage_video_channel_users + && self.max_video_channel_users == other.max_video_channel_users + && self.mfa_level == other.mfa_level + && self.name == other.name + && self.nsfw_level == other.nsfw_level + && self.owner == other.owner + && self.owner_id == other.owner_id + && self.permissions == other.permissions + && self.preferred_locale == other.preferred_locale + && self.premium_progress_bar_enabled == other.premium_progress_bar_enabled + && self.premium_subscription_count == other.premium_subscription_count + && self.premium_tier == other.premium_tier + && self.primary_category_id == other.primary_category_id + && self.public_updates_channel_id == other.public_updates_channel_id + && self.region == other.region + && self.rules_channel == other.rules_channel + && self.rules_channel_id == other.rules_channel_id + && self.splash == other.splash + && self.stickers == other.stickers + && self.system_channel_flags == other.system_channel_flags + && self.system_channel_id == other.system_channel_id + && self.vanity_url_code == other.vanity_url_code + && self.verification_level == other.verification_level + && self.welcome_screen == other.welcome_screen + && self.welcome_screen == other.welcome_screen + && self.widget_channel_id == other.widget_channel_id + && self.widget_enabled == other.widget_enabled + } +} + +impl std::cmp::Eq for Guild {} + +/// See +#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, Eq, Hash)] #[cfg_attr(feature = "sqlx", derive(sqlx::FromRow))] pub struct GuildBan { - pub user_id: Snowflake, - pub guild_id: Snowflake, + pub user: PublicUser, pub reason: Option, } -/// See https://docs.spacebar.chat/routes/#cmp--schemas-invite -#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq)] +/// See +#[derive(Serialize, Deserialize, Debug, Default, Clone)] #[cfg_attr(feature = "sqlx", derive(sqlx::FromRow))] pub struct GuildInvite { pub code: String, @@ -111,21 +236,40 @@ pub struct GuildInvite { pub created_at: DateTime, pub expires_at: Option>, pub guild_id: Snowflake, - pub guild: Option, + pub guild: Option>>, pub channel_id: Snowflake, - pub channel: Option, + pub channel: Option>>, pub inviter_id: Option, - pub inviter: Option, + pub inviter: Option>>, pub target_user_id: Option, pub target_user: Option, pub target_user_type: Option, pub vanity_url: Option, } -#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq)] +impl std::hash::Hash for GuildInvite { + fn hash(&self, state: &mut H) { + self.code.hash(state); + self.temporary.hash(state); + self.uses.hash(state); + self.max_uses.hash(state); + self.max_age.hash(state); + self.created_at.hash(state); + self.expires_at.hash(state); + self.guild_id.hash(state); + self.channel_id.hash(state); + self.inviter_id.hash(state); + self.target_user_id.hash(state); + self.target_user.hash(state); + self.target_user_type.hash(state); + self.vanity_url.hash(state); + } +} + +#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, Hash)] pub struct UnavailableGuild { - id: Snowflake, - unavailable: bool, + pub id: Snowflake, + pub unavailable: bool, } #[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq)] @@ -134,7 +278,7 @@ pub struct GuildCreateResponse { } #[derive(Serialize, Deserialize, Debug, Default, Clone)] -/// See https://discord.com/developers/docs/resources/guild-scheduled-event#guild-scheduled-event-object +/// See pub struct GuildScheduledEvent { pub id: Snowflake, pub guild_id: Snowflake, @@ -149,14 +293,14 @@ pub struct GuildScheduledEvent { pub entity_type: GuildScheduledEventEntityType, pub entity_id: Option, pub entity_metadata: Option, - pub creator: Option, + pub creator: Option>>, pub user_count: Option, pub image: Option, } #[derive(Serialize_repr, Deserialize_repr, Debug, Default, Clone)] #[repr(u8)] -/// See https://discord.com/developers/docs/resources/guild-scheduled-event#guild-scheduled-event-object-guild-scheduled-event-privacy-level +/// See pub enum GuildScheduledEventPrivacyLevel { #[default] GuildOnly = 2, @@ -164,7 +308,7 @@ pub enum GuildScheduledEventPrivacyLevel { #[derive(Serialize_repr, Deserialize_repr, Debug, Default, Clone)] #[repr(u8)] -/// See https://discord.com/developers/docs/resources/guild-scheduled-event#guild-scheduled-event-object-guild-scheduled-event-status +/// See pub enum GuildScheduledEventStatus { #[default] Scheduled = 1, @@ -175,7 +319,7 @@ pub enum GuildScheduledEventStatus { #[derive(Serialize_repr, Deserialize_repr, Debug, Default, Clone)] #[repr(u8)] -/// See https://discord.com/developers/docs/resources/guild-scheduled-event#guild-scheduled-event-object-guild-scheduled-event-entity-types +/// See pub enum GuildScheduledEventEntityType { #[default] StageInstance = 1, @@ -184,7 +328,99 @@ pub enum GuildScheduledEventEntityType { } #[derive(Serialize, Deserialize, Debug, Default, Clone)] -/// See https://discord.com/developers/docs/resources/guild-scheduled-event#guild-scheduled-event-object-guild-scheduled-event-entity-metadata +/// See pub struct GuildScheduledEventEntityMetadata { pub location: Option, } + +#[derive(Serialize, Deserialize, Debug, Default, Clone, Eq, PartialEq, Hash)] +pub struct VoiceRegion { + id: String, + name: String, + optimal: bool, + deprecated: bool, + custom: bool, +} + +#[derive(Serialize_repr, Deserialize_repr, Debug, Default, Clone, Eq, PartialEq, Hash, Copy)] +#[repr(u8)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +/// See +pub enum MessageNotificationLevel { + #[default] + AllMessages = 0, + OnlyMentions = 1, +} + +#[derive(Serialize_repr, Deserialize_repr, Debug, Default, Clone, Eq, PartialEq, Hash, Copy)] +#[repr(u8)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +/// See +pub enum ExplicitContentFilterLevel { + #[default] + Disabled = 0, + MembersWithoutRoles = 1, + AllMembers = 2, +} + +#[derive(Serialize_repr, Deserialize_repr, Debug, Default, Clone, Eq, PartialEq, Hash, Copy)] +#[repr(u8)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +/// See +pub enum VerificationLevel { + #[default] + None = 0, + Low = 1, + Medium = 2, + High = 3, + VeryHigh = 4, +} + +#[derive(Serialize_repr, Deserialize_repr, Debug, Default, Clone, Eq, PartialEq, Hash, Copy)] +#[repr(u8)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +/// See +pub enum MFALevel { + #[default] + None = 0, + Elevated = 1, +} + +#[derive(Serialize_repr, Deserialize_repr, Debug, Default, Clone, Eq, PartialEq, Hash, Copy)] +#[repr(u8)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +/// See +pub enum NSFWLevel { + #[default] + Default = 0, + Explicit = 1, + Safe = 2, + AgeRestricted = 3, +} + +#[derive(Serialize_repr, Deserialize_repr, Debug, Default, Clone, Eq, PartialEq, Hash, Copy)] +#[repr(u8)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +/// See +pub enum PremiumTier { + #[default] + None = 0, + Tier1 = 1, + Tier2 = 2, + Tier3 = 3, +} + +bitflags! { + #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)] + /// # Reference + /// See + pub struct SystemChannelFlags: u64 { + /// Indicates if an app uses the Auto Moderation API + const SUPPRESS_JOIN_NOTIFICATIONS = 1 << 0; + const SUPPRESS_PREMIUM_SUBSCRIPTIONS = 1 << 1; + const SUPPRESS_GUILD_REMINDER_NOTIFICATIONS = 1 << 2; + const SUPPRESS_JOIN_NOTIFICATION_REPLIES = 1 << 3; + const SUPPRESS_ROLE_SUBSCRIPTION_PURCHASE_NOTIFICATIONS = 1 << 4; + const SUPPRESS_ROLE_SUBSCRIPTION_PURCHASE_NOTIFICATIONS_REPLIES = 1 << 5; + } +} diff --git a/src/types/entities/guild_member.rs b/src/types/entities/guild_member.rs index 01f43de..bf2f93b 100644 --- a/src/types/entities/guild_member.rs +++ b/src/types/entities/guild_member.rs @@ -1,10 +1,16 @@ +use std::sync::{Arc, RwLock}; + use serde::{Deserialize, Serialize}; use crate::types::{entities::PublicUser, Snowflake}; -#[derive(Debug, Deserialize, Default, Serialize, Clone, PartialEq, Eq)] +#[derive(Debug, Deserialize, Default, Serialize, Clone)] +/// Represents a participating user in a guild. +/// +/// # Reference +/// See pub struct GuildMember { - pub user: Option, + pub user: Option>>, pub nick: Option, pub avatar: Option, pub roles: Vec, diff --git a/src/types/entities/integration.rs b/src/types/entities/integration.rs index 8076e70..0913213 100644 --- a/src/types/entities/integration.rs +++ b/src/types/entities/integration.rs @@ -1,3 +1,5 @@ +use std::sync::{Arc, RwLock}; + use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; @@ -8,7 +10,7 @@ use crate::types::{ #[derive(Default, Debug, Deserialize, Serialize, Clone)] #[cfg_attr(feature = "sqlx", derive(sqlx::FromRow))] -/// See https://discord.com/developers/docs/resources/guild#integration-object-integration-structure +/// See pub struct Integration { pub id: Snowflake, pub name: String, @@ -21,19 +23,19 @@ pub struct Integration { pub expire_behaviour: Option, pub expire_grace_period: Option, #[cfg_attr(feature = "sqlx", sqlx(skip))] - pub user: Option, + pub user: Option>>, #[cfg_attr(feature = "sqlx", sqlx(skip))] pub account: IntegrationAccount, pub synced_at: Option>, pub subscriber_count: Option, pub revoked: Option, #[cfg_attr(feature = "sqlx", sqlx(skip))] - pub application: Option, + pub application: Option>>, pub scopes: Option>, } #[derive(Default, Debug, Deserialize, Serialize, Clone)] -/// See https://discord.com/developers/docs/resources/guild#integration-account-object-integration-account-structure +/// See pub struct IntegrationAccount { pub id: String, pub name: String, diff --git a/src/types/entities/invite.rs b/src/types/entities/invite.rs index 6d7b570..842db3f 100644 --- a/src/types/entities/invite.rs +++ b/src/types/entities/invite.rs @@ -1,3 +1,5 @@ +use std::sync::{Arc, RwLock}; + use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; @@ -19,7 +21,7 @@ pub struct Invite { pub flags: Option, pub guild: Option, pub guild_id: Option, - pub guild_scheduled_event: Option, + pub guild_scheduled_event: Option>>, #[serde(rename = "type")] pub invite_type: Option, pub inviter: Option, @@ -68,7 +70,7 @@ pub enum NSFWLevel { /// See #[derive(Debug, Serialize, Deserialize)] pub struct InviteStageInstance { - pub members: Vec, + pub members: Vec>>, pub participant_count: i32, pub speaker_count: i32, pub topic: String, diff --git a/src/types/entities/message.rs b/src/types/entities/message.rs index 745d001..41c4d51 100644 --- a/src/types/entities/message.rs +++ b/src/types/entities/message.rs @@ -1,3 +1,5 @@ +use std::sync::{Arc, RwLock}; + use serde::{Deserialize, Serialize}; use crate::types::{ @@ -8,13 +10,17 @@ use crate::types::{ utils::Snowflake, }; -#[derive(Debug, Serialize, Deserialize, Default, Clone, PartialEq)] +#[derive(Debug, Serialize, Deserialize, Default, Clone)] #[cfg_attr(feature = "sqlx", derive(sqlx::FromRow))] +/// Represents a message sent in a channel. +/// +/// # Reference +/// See pub struct Message { pub id: Snowflake, pub channel_id: Snowflake, #[cfg_attr(feature = "sqlx", sqlx(skip))] - pub author: PublicUser, + pub author: Option, pub content: Option, pub timestamp: String, pub edited_timestamp: Option, @@ -23,15 +29,15 @@ pub struct Message { #[cfg_attr(feature = "sqlx", sqlx(skip))] pub mentions: Option>, #[cfg_attr(feature = "sqlx", sqlx(skip))] - pub mention_roles: Vec, + pub mention_roles: Option>, #[cfg_attr(feature = "sqlx", sqlx(skip))] pub mention_channels: Option>, #[cfg_attr(feature = "sqlx", sqlx(skip))] - pub attachments: Vec, + pub attachments: Option>, #[cfg(feature = "sqlx")] pub embeds: Vec>, #[cfg(not(feature = "sqlx"))] - pub embeds: Vec, + pub embeds: Option>, #[cfg(feature = "sqlx")] pub reactions: Option>>, #[cfg(not(feature = "sqlx"))] @@ -63,7 +69,44 @@ pub struct Message { pub role_subscription_data: Option, } -#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +impl PartialEq for Message { + fn eq(&self, other: &Self) -> bool { + self.id == other.id + && self.channel_id == other.channel_id + && self.author == other.author + && self.content == other.content + && self.timestamp == other.timestamp + && self.edited_timestamp == other.edited_timestamp + && self.tts == other.tts + && self.mention_everyone == other.mention_everyone + && self.mentions == other.mentions + && self.mention_roles == other.mention_roles + && self.mention_channels == other.mention_channels + && self.attachments == other.attachments + && self.embeds == other.embeds + && self.embeds == other.embeds + && self.nonce == other.nonce + && self.pinned == other.pinned + && self.webhook_id == other.webhook_id + && self.message_type == other.message_type + && self.activity == other.activity + && self.activity == other.activity + && self.application_id == other.application_id + && self.message_reference == other.message_reference + && self.message_reference == other.message_reference + && self.flags == other.flags + && self.referenced_message == other.referenced_message + && self.thread == other.thread + && self.components == other.components + && self.sticker_items == other.sticker_items + && self.position == other.position + && self.role_subscription_data == other.role_subscription_data + } +} + +#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Eq, Ord, PartialOrd)] +/// # Reference +/// See pub struct MessageReference { pub message_id: Snowflake, pub channel_id: Snowflake, @@ -71,17 +114,17 @@ pub struct MessageReference { pub fail_if_not_exists: Option, } -#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] +#[derive(Debug, Clone, Deserialize, Serialize)] pub struct MessageInteraction { pub id: Snowflake, #[serde(rename = "type")] pub interaction_type: u8, pub name: String, pub user: User, - pub member: Option, + pub member: Option>>, } -#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +#[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize, Eq, PartialOrd, Ord)] pub struct AllowedMention { parse: Vec, roles: Vec, @@ -89,7 +132,7 @@ pub struct AllowedMention { replied_user: bool, } -#[derive(Debug, PartialEq, Clone, Copy, Serialize, Deserialize)] +#[derive(Debug, PartialEq, Clone, Copy, Serialize, Deserialize, Eq, PartialOrd, Ord)] #[serde(rename_all = "snake_case")] pub enum AllowedMentionType { Roles, @@ -106,7 +149,7 @@ pub struct ChannelMention { name: String, } -#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, PartialOrd)] pub struct Embed { title: Option, #[serde(rename = "type")] @@ -124,14 +167,14 @@ pub struct Embed { fields: Option>, } -#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] pub struct EmbedFooter { text: String, icon_url: Option, proxy_icon_url: Option, } -#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize, PartialOrd, Ord)] pub struct EmbedImage { url: String, proxy_url: String, @@ -139,7 +182,7 @@ pub struct EmbedImage { width: Option, } -#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize, PartialOrd, Ord)] pub struct EmbedThumbnail { url: String, proxy_url: Option, @@ -147,7 +190,7 @@ pub struct EmbedThumbnail { width: Option, } -#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize, PartialOrd, Ord)] struct EmbedVideo { url: Option, proxy_url: Option, @@ -155,13 +198,13 @@ struct EmbedVideo { width: Option, } -#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize, PartialOrd, Ord)] pub struct EmbedProvider { name: Option, url: Option, } -#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize, PartialOrd, Ord)] pub struct EmbedAuthor { name: String, url: Option, @@ -169,21 +212,24 @@ pub struct EmbedAuthor { proxy_icon_url: Option, } -#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize, PartialOrd, Ord)] pub struct EmbedField { name: String, value: String, inline: Option, } -#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialOrd, PartialEq)] pub struct Reaction { - pub count: i32, + pub count: u32, + pub burst_count: u32, pub me: bool, + pub burst_me: bool, + pub burst_colors: Vec, pub emoji: Emoji, } -#[derive(Debug, PartialEq, Clone, Copy, Serialize, Deserialize)] +#[derive(Debug, PartialEq, Clone, Copy, Serialize, Deserialize, Eq, PartialOrd, Ord)] pub enum Component { ActionRow = 1, Button = 2, @@ -196,6 +242,8 @@ pub enum Component { } #[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +/// # Reference +/// See pub struct MessageActivity { #[serde(rename = "type")] pub activity_type: i64, diff --git a/src/types/entities/mod.rs b/src/types/entities/mod.rs index 6f1f58b..a14ef2c 100644 --- a/src/types/entities/mod.rs +++ b/src/types/entities/mod.rs @@ -10,6 +10,7 @@ pub use guild_member::*; pub use integration::*; pub use invite::*; pub use message::*; +pub use ratelimits::*; pub use relationship::*; pub use role::*; pub use security_key::*; @@ -22,6 +23,18 @@ pub use user_settings::*; pub use voice_state::*; pub use webhook::*; +#[cfg(feature = "client")] +use crate::gateway::{GatewayHandle, Updateable}; + +#[cfg(feature = "client")] +use async_trait::async_trait; + +#[cfg(feature = "client")] +use std::fmt::Debug; + +#[cfg(feature = "client")] +use std::sync::{Arc, RwLock}; + mod application; mod attachment; mod audit_log; @@ -34,6 +47,7 @@ mod guild_member; mod integration; mod invite; mod message; +mod ratelimits; mod relationship; mod role; mod security_key; @@ -45,3 +59,63 @@ mod user; mod user_settings; mod voice_state; mod webhook; + +#[cfg(feature = "client")] +#[async_trait(?Send)] +pub trait Composite { + async fn watch_whole(self, gateway: &GatewayHandle) -> Self; + + async fn option_observe_fn( + value: Option>>, + gateway: &GatewayHandle, + ) -> Option>> + where + T: Composite + Debug, + { + if let Some(value) = value { + let value = value.clone(); + Some(gateway.observe(value).await) + } else { + None + } + } + + async fn option_vec_observe_fn( + value: Option>>>, + gateway: &GatewayHandle, + ) -> Option>>> + where + T: Composite, + { + if let Some(value) = value { + let mut vec = Vec::new(); + for component in value.into_iter() { + vec.push(gateway.observe(component).await); + } + Some(vec) + } else { + None + } + } + + async fn value_observe_fn(value: Arc>, gateway: &GatewayHandle) -> Arc> + where + T: Composite, + { + gateway.observe(value).await + } + + async fn vec_observe_fn( + value: Vec>>, + gateway: &GatewayHandle, + ) -> Vec>> + where + T: Composite, + { + let mut vec = Vec::new(); + for component in value.into_iter() { + vec.push(gateway.observe(component).await); + } + vec + } +} diff --git a/src/api/policies/instance/ratelimits.rs b/src/types/entities/ratelimits.rs similarity index 89% rename from src/api/policies/instance/ratelimits.rs rename to src/types/entities/ratelimits.rs index e32a835..a95a2c6 100644 --- a/src/api/policies/instance/ratelimits.rs +++ b/src/types/entities/ratelimits.rs @@ -24,8 +24,6 @@ pub enum LimitType { } /// A struct that represents the current ratelimits, either instance-wide or user-wide. -/// Unlike [`RateLimits`], this struct shows the current ratelimits, not the rate limit -/// configuration for the instance. /// See for more information. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Limit { diff --git a/src/types/entities/relationship.rs b/src/types/entities/relationship.rs index a6abc09..576d99a 100644 --- a/src/types/entities/relationship.rs +++ b/src/types/entities/relationship.rs @@ -1,3 +1,5 @@ +use std::sync::{Arc, RwLock}; + use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use serde_repr::{Deserialize_repr, Serialize_repr}; @@ -6,20 +8,29 @@ use crate::types::Snowflake; use super::PublicUser; -#[derive(Debug, Deserialize, Serialize, Clone, Default, PartialEq, Eq)] -/// See https://discord-userdoccers.vercel.app/resources/user#relationship-structure +#[derive(Debug, Deserialize, Serialize, Clone, Default)] +/// See pub struct Relationship { pub id: Snowflake, #[serde(rename = "type")] pub relationship_type: RelationshipType, pub nickname: Option, - pub user: PublicUser, + pub user: Arc>, pub since: Option>, } +impl PartialEq for Relationship { + fn eq(&self, other: &Self) -> bool { + self.id == other.id + && self.relationship_type == other.relationship_type + && self.since == other.since + && self.nickname == other.nickname + } +} + #[derive(Serialize_repr, Deserialize_repr, Debug, Clone, Default, Eq, PartialEq)] #[repr(u8)] -/// See https://discord-userdoccers.vercel.app/resources/user#relationship-type +/// See pub enum RelationshipType { Suggestion = 6, Implicit = 5, diff --git a/src/types/entities/role.rs b/src/types/entities/role.rs index 1719d28..087a775 100644 --- a/src/types/entities/role.rs +++ b/src/types/entities/role.rs @@ -1,12 +1,23 @@ use bitflags::bitflags; use serde::{Deserialize, Serialize}; use serde_aux::prelude::{deserialize_option_number_from_string, deserialize_string_from_number}; +use std::fmt::Debug; use crate::types::utils::Snowflake; +#[cfg(feature = "client")] +use chorus_macros::{Composite, Updateable}; + +#[cfg(feature = "client")] +use crate::gateway::{GatewayHandle, Updateable}; + +#[cfg(feature = "client")] +use crate::types::Composite; + #[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq)] +#[cfg_attr(feature = "client", derive(Updateable, Composite))] #[cfg_attr(feature = "sqlx", derive(sqlx::FromRow))] -/// See https://discord.com/developers/docs/topics/permissions#role-object +/// See pub struct RoleObject { pub id: Snowflake, pub name: String, @@ -34,8 +45,8 @@ pub struct RoleSubscriptionData { pub is_renewal: bool, } -#[derive(Serialize, Deserialize, Debug, Default, Clone, Eq, PartialEq)] -/// See https://discord.com/developers/docs/topics/permissions#role-object-role-tags-structure +#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, Eq, Hash)] +/// See pub struct RoleTags { #[serde(default)] #[serde(deserialize_with = "deserialize_option_number_from_string")] @@ -53,57 +64,110 @@ pub struct RoleTags { } bitflags! { - #[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] + #[derive(Debug, Default, Clone, Hash, Serialize, Deserialize, PartialEq, Eq)] + /// Permissions limit what users of certain roles can do on a Guild to Guild basis. + /// + /// # Reference: + /// See pub struct PermissionFlags: u64 { + /// Allows creation of instant invites const CREATE_INSTANT_INVITE = 1 << 0; + /// Allows kicking members const KICK_MEMBERS = 1 << 1; + /// Allows banning members const BAN_MEMBERS = 1 << 2; + /// Allows all permissions and bypasses channel permission overwrites const ADMINISTRATOR = 1 << 3; + /// Allows management and editing of channels const MANAGE_CHANNELS = 1 << 4; + /// Allows management and editing of the guild and guild settings const MANAGE_GUILD = 1 << 5; + /// Allows for the addition of reactions to messages const ADD_REACTIONS = 1 << 6; + /// Allows viewing of the audit log const VIEW_AUDIT_LOG = 1 << 7; + /// Allows using priority speaker in a voice channel const PRIORITY_SPEAKER = 1 << 8; + /// Allows the user to go live and share their screen const STREAM = 1 << 9; + /// Allows guild members to view a channel, which includes reading messages in text channels and joining voice channels const VIEW_CHANNEL = 1 << 10; + /// Allows sending messages in a channel and creating threads in a forum (does not allow sending messages in threads) const SEND_MESSAGES = 1 << 11; + /// Allows sending /tts messages const SEND_TTS_MESSAGES = 1 << 12; + /// Allows deletion of other users' messages const MANAGE_MESSAGES = 1 << 13; + /// Links sent by users with this permission will be auto-embedded const EMBED_LINKS = 1 << 14; + /// Allows uploading images and files const ATTACH_FILES = 1 << 15; + /// Allows reading of message history const READ_MESSAGE_HISTORY = 1 << 16; + /// Allows using the @everyone tag to notify all users in a channel, and the @here tag to notify all online users in a channel const MENTION_EVERYONE = 1 << 17; + /// Allows the usage of custom emojis from other servers const USE_EXTERNAL_EMOJIS = 1 << 18; + /// Allows viewing guild insights const VIEW_GUILD_INSIGHTS = 1 << 19; + /// Allows joining of a voice channel const CONNECT = 1 << 20; + /// Allows speaking in a voice channel const SPEAK = 1 << 21; + /// Allows muting members in a voice channel const MUTE_MEMBERS = 1 << 22; + /// Allows deafening of members in a voice channel const DEAFEN_MEMBERS = 1 << 23; + /// Allows moving of members between voice channels const MOVE_MEMBERS = 1 << 24; + /// Allows using voice activity (VAD = voice-activity-detection) in a voice channel const USE_VAD = 1 << 25; + /// Allows modification of own nickname const CHANGE_NICKNAME = 1 << 26; + /// Allows modification of other users' nicknames const MANAGE_NICKNAMES = 1 << 27; + /// Allows management and editing of roles const MANAGE_ROLES = 1 << 28; + /// Allows management and editing of webhooks const MANAGE_WEBHOOKS = 1 << 29; + /// Allows management and editing of emojis, stickers, and soundboard sounds const MANAGE_GUILD_EXPRESSIONS = 1 << 30; + /// Allows members to use application commands, including slash commands and context menu commands. const USE_APPLICATION_COMMANDS = 1 << 31; + /// Allows requesting to speak in stage channels. (*This permission is under active development and may be changed or removed.*) const REQUEST_TO_SPEAK = 1 << 32; + /// Allows creating, editing, and deleting scheduled events const MANAGE_EVENTS = 1 << 33; + /// Allows deleting and archiving threads, and viewing all private threads const MANAGE_THREADS = 1 << 34; + /// Allows creating public and announcement threads const CREATE_PUBLIC_THREADS = 1 << 35; + /// Allows creating private threads const CREATE_PRIVATE_THREADS = 1 << 36; + /// Allows the usage of custom stickers from other servers const USE_EXTERNAL_STICKERS = 1 << 37; + /// Allows sending messages in threads const SEND_MESSAGES_IN_THREADS = 1 << 38; + /// Allows using Activities in a voice channel const USE_EMBEDDED_ACTIVITIES = 1 << 39; + /// Allows timing out users to prevent them from sending or reacting to messages in chat and threads, and from speaking in voice and stage channels const MODERATE_MEMBERS = 1 << 40; + /// Allows viewing role subscription insights const VIEW_CREATOR_MONETIZATION_ANALYTICS = 1 << 41; + /// Allows using the soundboard in a voice channel const USE_SOUNDBOARD = 1 << 42; + /// Allows using custom soundboard sounds from other servers const USE_EXTERNAL_SOUNDS = 1 << 45; + /// Allows sending voice messages const SEND_VOICE_MESSAGES = 1 << 46; } } impl PermissionFlags { + /// Returns if the PermissionFlags object has specific permissions + /// + /// # Notes + /// Note that if the object has the [PermissionFlags::ADMINISTRATOR] permission, this always returns true pub fn has_permission(&self, permission: PermissionFlags) -> bool { self.contains(permission) || self.contains(PermissionFlags::ADMINISTRATOR) } @@ -114,6 +178,7 @@ impl PermissionFlags { } /// Creates a String of Permissions from a given [`Vec`] of [`PermissionFlags`]. + /// /// # Example: /// ``` /// use chorus::types::{PermissionFlags}; diff --git a/src/types/entities/stage_instance.rs b/src/types/entities/stage_instance.rs index b2d19c3..8810f52 100644 --- a/src/types/entities/stage_instance.rs +++ b/src/types/entities/stage_instance.rs @@ -4,12 +4,12 @@ use serde_repr::{Deserialize_repr, Serialize_repr}; use crate::types::Snowflake; #[derive(Serialize, Deserialize, Debug, Default, Clone)] -/// See https://discord.com/developers/docs/resources/stage-instance +/// See pub struct StageInstance { pub id: Snowflake, pub guild_id: Snowflake, pub channel_id: Snowflake, - /// 1 - 120 chars + /// 1 - 120 characters pub topic: String, pub privacy_level: StageInstancePrivacyLevel, /// deprecated, apparently @@ -20,7 +20,7 @@ pub struct StageInstance { #[derive(Serialize_repr, Deserialize_repr, Debug, Clone, Default)] #[repr(u8)] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] -/// See https://discord.com/developers/docs/resources/stage-instance#stage-instance-object-privacy-level +/// See pub enum StageInstancePrivacyLevel { /// deprecated, apparently Public = 1, diff --git a/src/types/entities/sticker.rs b/src/types/entities/sticker.rs index 098394d..593206d 100644 --- a/src/types/entities/sticker.rs +++ b/src/types/entities/sticker.rs @@ -1,9 +1,15 @@ +use std::sync::{Arc, RwLock}; + use serde::{Deserialize, Serialize}; use crate::types::{entities::User, utils::Snowflake}; -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] +#[derive(Debug, Serialize, Deserialize, Clone, Default)] #[cfg_attr(feature = "sqlx", derive(sqlx::FromRow))] +/// Represents a sticker that can be sent in messages. +/// +/// # Reference +/// See pub struct Sticker { #[serde(default)] pub id: Snowflake, @@ -18,11 +24,95 @@ pub struct Sticker { pub available: Option, pub guild_id: Option, #[cfg_attr(feature = "sqlx", sqlx(skip))] - pub user: Option, + pub user: Option>>, pub sort_value: Option, } +impl std::hash::Hash for Sticker { + fn hash(&self, state: &mut H) { + self.id.hash(state); + self.pack_id.hash(state); + self.name.hash(state); + self.description.hash(state); + self.tags.hash(state); + self.asset.hash(state); + self.sticker_type.hash(state); + self.format_type.hash(state); + self.available.hash(state); + self.guild_id.hash(state); + self.sort_value.hash(state); + } +} + +impl PartialEq for Sticker { + fn eq(&self, other: &Self) -> bool { + self.id == other.id + && self.pack_id == other.pack_id + && self.name == other.name + && self.description == other.description + && self.tags == other.tags + && self.asset == other.asset + && self.sticker_type == other.sticker_type + && self.format_type == other.format_type + && self.available == other.available + && self.guild_id == other.guild_id + && self.sort_value == other.sort_value + } +} + +impl PartialOrd for Sticker { + fn partial_cmp(&self, other: &Self) -> Option { + match self.id.partial_cmp(&other.id) { + Some(core::cmp::Ordering::Equal) => {} + ord => return ord, + } + match self.pack_id.partial_cmp(&other.pack_id) { + Some(core::cmp::Ordering::Equal) => {} + ord => return ord, + } + match self.name.partial_cmp(&other.name) { + Some(core::cmp::Ordering::Equal) => {} + ord => return ord, + } + match self.description.partial_cmp(&other.description) { + Some(core::cmp::Ordering::Equal) => {} + ord => return ord, + } + match self.tags.partial_cmp(&other.tags) { + Some(core::cmp::Ordering::Equal) => {} + ord => return ord, + } + match self.asset.partial_cmp(&other.asset) { + Some(core::cmp::Ordering::Equal) => {} + ord => return ord, + } + match self.sticker_type.partial_cmp(&other.sticker_type) { + Some(core::cmp::Ordering::Equal) => {} + ord => return ord, + } + match self.format_type.partial_cmp(&other.format_type) { + Some(core::cmp::Ordering::Equal) => {} + ord => return ord, + } + match self.available.partial_cmp(&other.available) { + Some(core::cmp::Ordering::Equal) => {} + ord => return ord, + } + match self.guild_id.partial_cmp(&other.guild_id) { + Some(core::cmp::Ordering::Equal) => {} + ord => return ord, + } + self.sort_value.partial_cmp(&other.sort_value) + } +} + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +/// A partial sticker object. +/// +/// Represents the smallest amount of data required to render a sticker. +/// +/// # Reference +/// See pub struct StickerItem { pub id: Snowflake, pub name: String, diff --git a/src/types/entities/team.rs b/src/types/entities/team.rs index a6f2ef1..8e32f55 100644 --- a/src/types/entities/team.rs +++ b/src/types/entities/team.rs @@ -1,9 +1,11 @@ +use std::sync::{Arc, RwLock}; + use serde::{Deserialize, Serialize}; use crate::types::entities::User; use crate::types::Snowflake; -#[derive(Debug, PartialEq, Deserialize, Serialize, Clone)] +#[derive(Debug, Deserialize, Serialize, Clone)] #[cfg_attr(feature = "sqlx", derive(sqlx::FromRow))] pub struct Team { pub icon: Option, @@ -14,10 +16,10 @@ pub struct Team { pub owner_user_id: Snowflake, } -#[derive(Debug, PartialEq, Deserialize, Serialize, Clone)] +#[derive(Debug, Deserialize, Serialize, Clone)] pub struct TeamMember { pub membership_state: u8, pub permissions: Vec, pub team_id: Snowflake, - pub user: User, + pub user: Arc>, } diff --git a/src/types/entities/template.rs b/src/types/entities/template.rs index b6eb3e9..1305a98 100644 --- a/src/types/entities/template.rs +++ b/src/types/entities/template.rs @@ -1,3 +1,5 @@ +use std::sync::{Arc, RwLock}; + use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; @@ -6,8 +8,8 @@ use crate::types::{ utils::Snowflake, }; -/// See https://docs.spacebar.chat/routes/#cmp--schemas-template -#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq)] +/// See +#[derive(Serialize, Deserialize, Debug, Default, Clone)] #[cfg_attr(feature = "sqlx", derive(sqlx::FromRow))] pub struct GuildTemplate { pub code: String, @@ -16,13 +18,13 @@ pub struct GuildTemplate { pub usage_count: Option, pub creator_id: Snowflake, #[cfg_attr(feature = "sqlx", sqlx(skip))] - pub creator: User, + pub creator: Arc>, pub created_at: DateTime, pub updated_at: DateTime, pub source_guild_id: Snowflake, #[cfg_attr(feature = "sqlx", sqlx(skip))] - pub source_guild: Vec, + pub source_guild: Vec>>, // Unsure how a {recursive: Guild} looks like, might be a Vec? #[cfg_attr(feature = "sqlx", sqlx(skip))] - pub serialized_source_guild: Vec, + pub serialized_source_guild: Vec>>, } diff --git a/src/types/entities/user.rs b/src/types/entities/user.rs index 469c45e..eca5344 100644 --- a/src/types/entities/user.rs +++ b/src/types/entities/user.rs @@ -2,6 +2,18 @@ use crate::types::utils::Snowflake; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use serde_aux::prelude::deserialize_option_number_from_string; +use std::fmt::Debug; + +#[cfg(feature = "client")] +use crate::gateway::{GatewayHandle, Updateable}; + +#[cfg(feature = "client")] +use crate::types::Composite; + +#[cfg(feature = "client")] +use chorus_macros::{Composite, Updateable}; + +use super::Emoji; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] #[cfg_attr(feature = "sqlx", derive(sqlx::Type))] @@ -15,7 +27,8 @@ impl User { PublicUser::from(self) } } -#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, Eq)] +#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "client", derive(Updateable, Composite))] #[cfg_attr(feature = "sqlx", derive(sqlx::FromRow))] pub struct User { pub id: Snowflake, @@ -50,7 +63,7 @@ pub struct User { pub disabled: Option, } -#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)] pub struct PublicUser { pub id: Snowflake, pub username: Option, @@ -91,7 +104,7 @@ impl From for PublicUser { const CUSTOM_USER_FLAG_OFFSET: u64 = 1 << 32; bitflags::bitflags! { - #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] + #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)] #[cfg_attr(feature = "sqlx", derive(sqlx::Type))] pub struct UserFlags: u64 { const DISCORD_EMPLOYEE = 1 << 0; @@ -116,3 +129,15 @@ bitflags::bitflags! { const BOT_HTTP_INTERACTIONS = 1 << 19; } } + +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, PartialOrd)] +pub struct UserProfileMetadata { + pub guild_id: Option, + pub pronouns: String, + pub bio: Option, + pub banner: Option, + pub accent_color: Option, + pub theme_colors: Option>, + pub popout_animation_particle_type: Option, + pub emoji: Option, +} diff --git a/src/types/entities/user_settings.rs b/src/types/entities/user_settings.rs index 40b936a..e6db7e7 100644 --- a/src/types/entities/user_settings.rs +++ b/src/types/entities/user_settings.rs @@ -1,3 +1,5 @@ +use std::sync::{Arc, RwLock}; + use chrono::{serde::ts_milliseconds_option, Utc}; use serde::{Deserialize, Serialize}; @@ -28,7 +30,7 @@ pub enum UserTheme { Light, } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] #[cfg_attr(feature = "sqlx", derive(sqlx::FromRow))] pub struct UserSettings { pub afk_timeout: u16, @@ -51,15 +53,17 @@ pub struct UserSettings { pub friend_source_flags: sqlx::types::Json, #[cfg(not(feature = "sqlx"))] pub friend_source_flags: FriendSourceFlags, - pub gateway_connected: bool, + pub gateway_connected: Option, pub gif_auto_play: bool, #[cfg(feature = "sqlx")] pub guild_folders: sqlx::types::Json>, #[cfg(not(feature = "sqlx"))] pub guild_folders: Vec, #[cfg(feature = "sqlx")] + #[serde(default)] pub guild_positions: sqlx::types::Json>, #[cfg(not(feature = "sqlx"))] + #[serde(default)] pub guild_positions: Vec, pub inline_attachment_media: bool, pub inline_embed_media: bool, @@ -73,7 +77,7 @@ pub struct UserSettings { #[cfg(not(feature = "sqlx"))] pub restricted_guilds: Vec, pub show_current_game: bool, - pub status: UserStatus, + pub status: Arc>, pub stream_notifications_enabled: bool, pub theme: UserTheme, pub timezone_offset: i16, @@ -96,7 +100,7 @@ impl Default for UserSettings { enable_tts_command: false, explicit_content_filter: 0, friend_source_flags: Default::default(), - gateway_connected: false, + gateway_connected: Some(false), gif_auto_play: false, guild_folders: Default::default(), guild_positions: Default::default(), @@ -109,7 +113,7 @@ impl Default for UserSettings { render_reactions: true, restricted_guilds: Default::default(), show_current_game: true, - status: UserStatus::Online, + status: Arc::new(RwLock::new(UserStatus::Online)), stream_notifications_enabled: false, theme: UserTheme::Dark, timezone_offset: 0, @@ -138,7 +142,7 @@ impl Default for FriendSourceFlags { } } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct GuildFolder { pub color: u32, pub guild_ids: Vec, @@ -149,5 +153,5 @@ pub struct GuildFolder { #[derive(Debug, Serialize, Deserialize)] pub struct LoginResult { pub token: String, - pub settings: UserSettings, + pub settings: Arc>, } diff --git a/src/types/entities/voice_state.rs b/src/types/entities/voice_state.rs index aafc07f..e764296 100644 --- a/src/types/entities/voice_state.rs +++ b/src/types/entities/voice_state.rs @@ -1,20 +1,33 @@ +use std::sync::{Arc, RwLock}; + +#[cfg(feature = "client")] +use chorus_macros::{Composite, Updateable}; + +#[cfg(feature = "client")] +use crate::types::Composite; + +#[cfg(feature = "client")] +use crate::gateway::{GatewayHandle, Updateable}; + use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; +use std::fmt::Debug; use crate::types::{ entities::{Guild, GuildMember}, utils::Snowflake, }; -/// See https://docs.spacebar.chat/routes/#cmp--schemas-voicestate -#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq)] +/// See +#[derive(Serialize, Deserialize, Debug, Default, Clone)] #[cfg_attr(feature = "sqlx", derive(sqlx::FromRow))] +#[cfg_attr(feature = "client", derive(Updateable, Composite))] pub struct VoiceState { pub guild_id: Option, pub guild: Option, pub channel_id: Option, pub user_id: Snowflake, - pub member: Option, + pub member: Option>>, pub session_id: Snowflake, pub token: Option, pub deaf: bool, @@ -25,5 +38,5 @@ pub struct VoiceState { pub self_video: bool, pub suppress: bool, pub request_to_speak_timestamp: Option>, - pub id: Option, + pub id: Snowflake, } diff --git a/src/types/entities/webhook.rs b/src/types/entities/webhook.rs index 521b93f..b544ec9 100644 --- a/src/types/entities/webhook.rs +++ b/src/types/entities/webhook.rs @@ -1,12 +1,25 @@ +use std::fmt::Debug; +use std::sync::{Arc, RwLock}; + use serde::{Deserialize, Serialize}; +#[cfg(feature = "client")] +use crate::gateway::{GatewayHandle, Updateable}; + +#[cfg(feature = "client")] +use chorus_macros::{Composite, Updateable}; + +#[cfg(feature = "client")] +use crate::types::Composite; + use crate::types::{ entities::{Guild, User}, utils::Snowflake, }; -/// See https://docs.spacebar.chat/routes/#cmp--schemas-webhook -#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq)] +/// See +#[derive(Serialize, Deserialize, Debug, Default, Clone)] +#[cfg_attr(feature = "client", derive(Updateable, Composite))] #[cfg_attr(feature = "sqlx", derive(sqlx::FromRow))] pub struct Webhook { pub id: Snowflake, @@ -20,10 +33,10 @@ pub struct Webhook { pub application_id: Snowflake, #[serde(skip_serializing_if = "Option::is_none")] #[cfg_attr(feature = "sqlx", sqlx(skip))] - pub user: Option, + pub user: Option>>, #[serde(skip_serializing_if = "Option::is_none")] #[cfg_attr(feature = "sqlx", sqlx(skip))] - pub source_guild: Option, + pub source_guild: Option>>, #[serde(skip_serializing_if = "Option::is_none")] pub url: Option, } diff --git a/src/types/events/application.rs b/src/types/events/application.rs index 8afb374..7fee577 100644 --- a/src/types/events/application.rs +++ b/src/types/events/application.rs @@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize}; use crate::types::{GuildApplicationCommandPermissions, WebSocketEvent}; #[derive(Debug, Deserialize, Serialize, Default, Clone)] -/// See https://discord.com/developers/docs/topics/gateway-events#application-command-permissions-update +/// See pub struct ApplicationCommandPermissionsUpdate { #[serde(flatten)] pub permissions: GuildApplicationCommandPermissions, diff --git a/src/types/events/auto_moderation.rs b/src/types/events/auto_moderation.rs index d82aa02..2a2eb6b 100644 --- a/src/types/events/auto_moderation.rs +++ b/src/types/events/auto_moderation.rs @@ -1,3 +1,5 @@ +use crate::types::{JsonField, SourceUrlField}; +use chorus_macros::{JsonField, SourceUrlField}; use serde::{Deserialize, Serialize}; use crate::types::{ @@ -5,8 +7,11 @@ use crate::types::{ WebSocketEvent, }; +#[cfg(feature = "client")] +use super::UpdateMessage; + #[derive(Debug, Deserialize, Serialize, Default, Clone)] -/// See https://discord.com/developers/docs/topics/gateway-events#auto-moderation-rule-create +/// See pub struct AutoModerationRuleCreate { #[serde(flatten)] pub rule: AutoModerationRule, @@ -14,17 +19,28 @@ pub struct AutoModerationRuleCreate { impl WebSocketEvent for AutoModerationRuleCreate {} -#[derive(Debug, Deserialize, Serialize, Default, Clone)] -/// See https://discord.com/developers/docs/topics/gateway-events#auto-moderation-rule-update +#[derive(Debug, Deserialize, Serialize, Default, Clone, JsonField, SourceUrlField)] +/// See pub struct AutoModerationRuleUpdate { #[serde(flatten)] pub rule: AutoModerationRule, + #[serde(skip)] + pub json: String, + #[serde(skip)] + pub source_url: String, +} + +#[cfg(feature = "client")] +impl UpdateMessage for AutoModerationRuleUpdate { + fn id(&self) -> Option { + Some(self.rule.id) + } } impl WebSocketEvent for AutoModerationRuleUpdate {} #[derive(Debug, Deserialize, Serialize, Default, Clone)] -/// See https://discord.com/developers/docs/topics/gateway-events#auto-moderation-rule-delete +/// See pub struct AutoModerationRuleDelete { #[serde(flatten)] pub rule: AutoModerationRule, @@ -33,7 +49,7 @@ pub struct AutoModerationRuleDelete { impl WebSocketEvent for AutoModerationRuleDelete {} #[derive(Debug, Deserialize, Serialize, Default, Clone)] -/// See https://discord.com/developers/docs/topics/gateway-events#auto-moderation-action-execution +/// See pub struct AutoModerationActionExecution { pub guild_id: Snowflake, pub action: AutoModerationAction, diff --git a/src/types/events/call.rs b/src/types/events/call.rs index 67225fb..508aae2 100644 --- a/src/types/events/call.rs +++ b/src/types/events/call.rs @@ -21,7 +21,7 @@ pub struct CallCreate { impl WebSocketEvent for CallCreate {} -#[derive(Debug, Deserialize, Serialize, Default, Clone)] +#[derive(Debug, Deserialize, Serialize, Default, Clone, PartialEq, Eq)] /// Officially Undocumented; /// Updates the client on which calls are ringing, along with a specific call?; /// @@ -38,7 +38,7 @@ pub struct CallUpdate { impl WebSocketEvent for CallUpdate {} -#[derive(Debug, Deserialize, Serialize, Default, Clone)] +#[derive(Debug, Deserialize, Serialize, Default, Clone, PartialEq, Eq)] /// Officially Undocumented; /// Deletes a ringing call; /// Ex: {"t":"CALL_DELETE","s":8,"op":0,"d":{"channel_id":"837609115475771392"}} @@ -48,9 +48,9 @@ pub struct CallDelete { impl WebSocketEvent for CallDelete {} -#[derive(Debug, Deserialize, Serialize, Default, Clone)] +#[derive(Debug, Deserialize, Serialize, Default, Clone, PartialEq, Eq)] /// Officially Undocumented; -/// See https://unofficial-discord-docs.vercel.app/gateway/op13; +/// See ; /// /// Ex: {"op":13,"d":{"channel_id":"837609115475771392"}} pub struct CallSync { diff --git a/src/types/events/channel.rs b/src/types/events/channel.rs index b595d57..eb557d7 100644 --- a/src/types/events/channel.rs +++ b/src/types/events/channel.rs @@ -1,12 +1,20 @@ use crate::types::events::WebSocketEvent; -use crate::types::{entities::Channel, Snowflake}; +use crate::types::{entities::Channel, JsonField, Snowflake, SourceUrlField}; +use chorus_macros::{JsonField, SourceUrlField}; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; +#[cfg(feature = "client")] use super::UpdateMessage; +#[cfg(feature = "client")] +use std::sync::{Arc, RwLock}; + +#[cfg(feature = "client")] +use crate::types::Guild; + #[derive(Debug, Default, Deserialize, Serialize)] -/// See https://discord.com/developers/docs/topics/gateway-events#channel-pins-update +/// See pub struct ChannelPinsUpdate { pub guild_id: Option, pub channel_id: Snowflake, @@ -15,30 +23,57 @@ pub struct ChannelPinsUpdate { impl WebSocketEvent for ChannelPinsUpdate {} -#[derive(Debug, Default, Deserialize, Serialize, Clone)] -/// See https://discord.com/developers/docs/topics/gateway-events#channel-create +#[derive(Debug, Default, Deserialize, Serialize, Clone, JsonField, SourceUrlField)] +/// See pub struct ChannelCreate { #[serde(flatten)] pub channel: Channel, + #[serde(skip)] + pub json: String, + #[serde(skip)] + pub source_url: String, } impl WebSocketEvent for ChannelCreate {} -#[derive(Debug, Default, Deserialize, Serialize, Clone)] -/// See https://discord.com/developers/docs/topics/gateway-events#channel-update +#[cfg(feature = "client")] +impl UpdateMessage for ChannelCreate { + fn id(&self) -> Option { + self.channel.guild_id + } + + fn update(&mut self, object_to_update: Arc>) { + let mut write = object_to_update.write().unwrap(); + let update = Arc::new(RwLock::new(self.channel.clone())); + if write.channels.is_some() { + write.channels.as_mut().unwrap().push(update); + } else { + write.channels = Some(Vec::from([update])); + } + } +} + +#[derive(Debug, Default, Deserialize, Serialize, Clone, JsonField, SourceUrlField)] +/// See pub struct ChannelUpdate { #[serde(flatten)] pub channel: Channel, + #[serde(skip)] + pub json: String, + #[serde(skip)] + pub source_url: String, } impl WebSocketEvent for ChannelUpdate {} +#[cfg(feature = "client")] impl UpdateMessage for ChannelUpdate { - fn update(&self, object_to_update: &mut Channel) { - *object_to_update = self.channel.clone(); + fn update(&mut self, object_to_update: Arc>) { + let mut write = object_to_update.write().unwrap(); + *write = self.channel.clone(); } - fn id(&self) -> Snowflake { - self.channel.id + fn id(&self) -> Option { + Some(self.channel.id) } } @@ -53,7 +88,7 @@ pub struct ChannelUnreadUpdate { #[derive(Debug, Default, Deserialize, Serialize, Clone)] /// Contains very few fields from [Channel] -/// See also [ChannelUnreadUpdates] +/// See also [ChannelUnreadUpdate] pub struct ChannelUnreadUpdateObject { pub id: Snowflake, pub last_message_id: Snowflake, @@ -62,11 +97,38 @@ pub struct ChannelUnreadUpdateObject { impl WebSocketEvent for ChannelUnreadUpdate {} -#[derive(Debug, Default, Deserialize, Serialize, Clone)] -/// See https://discord.com/developers/docs/topics/gateway-events#channel-delete +#[derive(Debug, Default, Deserialize, Serialize, Clone, JsonField, SourceUrlField)] +/// See pub struct ChannelDelete { #[serde(flatten)] pub channel: Channel, + #[serde(skip)] + pub json: String, + #[serde(skip)] + pub source_url: String, +} + +#[cfg(feature = "client")] +impl UpdateMessage for ChannelDelete { + fn id(&self) -> Option { + self.channel.guild_id + } + + fn update(&mut self, object_to_update: Arc>) { + if self.id().is_none() { + return; + } + let mut write = object_to_update.write().unwrap(); + if write.channels.is_none() { + return; + } + for (iteration, item) in (0_u32..).zip(write.channels.as_mut().unwrap().iter()) { + if item.read().unwrap().id == self.id().unwrap() { + write.channels.as_mut().unwrap().remove(iteration as usize); + return; + } + } + } } impl WebSocketEvent for ChannelDelete {} diff --git a/src/types/events/guild.rs b/src/types/events/guild.rs index ae69681..0c6bb05 100644 --- a/src/types/events/guild.rs +++ b/src/types/events/guild.rs @@ -1,21 +1,43 @@ +use chorus_macros::{JsonField, SourceUrlField}; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use crate::types::entities::{Guild, PublicUser, UnavailableGuild}; use crate::types::events::WebSocketEvent; use crate::types::{ - AuditLogEntry, Emoji, GuildMember, GuildScheduledEvent, RoleObject, Snowflake, Sticker, + AuditLogEntry, Emoji, GuildMember, GuildScheduledEvent, JsonField, RoleObject, Snowflake, + SourceUrlField, Sticker, }; use super::PresenceUpdate; -#[derive(Debug, Deserialize, Serialize, Default, Clone)] -/// See https://discord.com/developers/docs/topics/gateway-events#guild-create; +#[cfg(feature = "client")] +use super::UpdateMessage; +#[cfg(feature = "client")] +use std::sync::{Arc, RwLock}; + +#[derive(Debug, Deserialize, Serialize, Default, Clone, SourceUrlField, JsonField)] +/// See ; /// Received to give data about a guild; // This one is particularly painful, it can be a Guild object with an extra field or an unavailable guild object pub struct GuildCreate { #[serde(flatten)] pub d: GuildCreateDataOption, + #[serde(skip)] + pub source_url: String, + #[serde(skip)] + pub json: String, +} + +impl UpdateMessage for GuildCreate { + fn id(&self) -> Option { + match &self.d { + GuildCreateDataOption::UnavailableGuild(unavailable) => Some(unavailable.id), + GuildCreateDataOption::Guild(guild) => Some(guild.id), + } + } + + fn update(&mut self, _: Arc>) {} } #[derive(Debug, Deserialize, Serialize, Clone)] @@ -34,7 +56,7 @@ impl Default for GuildCreateDataOption { impl WebSocketEvent for GuildCreate {} #[derive(Debug, Default, Deserialize, Serialize, Clone)] -/// See https://discord.com/developers/docs/topics/gateway-events#guild-ban-add-guild-ban-add-event-fields; +/// See ; /// Received to give info about a user being banned from a guild; pub struct GuildBanAdd { pub guild_id: Snowflake, @@ -44,7 +66,7 @@ pub struct GuildBanAdd { impl WebSocketEvent for GuildBanAdd {} #[derive(Debug, Default, Deserialize, Serialize, Clone)] -/// See https://discord.com/developers/docs/topics/gateway-events#guild-ban-remove; +/// See ; /// Received to give info about a user being unbanned from a guild; pub struct GuildBanRemove { pub guild_id: Snowflake, @@ -53,28 +75,49 @@ pub struct GuildBanRemove { impl WebSocketEvent for GuildBanRemove {} -#[derive(Debug, Default, Deserialize, Serialize, Clone)] -/// See https://discord.com/developers/docs/topics/gateway-events#guild-update; +#[derive(Debug, Default, Deserialize, Serialize, Clone, SourceUrlField, JsonField)] +/// See ; /// Received to give info about a guild being updated; pub struct GuildUpdate { #[serde(flatten)] pub guild: Guild, + #[serde(skip)] + pub source_url: String, + #[serde(skip)] + pub json: String, } impl WebSocketEvent for GuildUpdate {} -#[derive(Debug, Default, Deserialize, Serialize, Clone)] -/// See https://discord.com/developers/docs/topics/gateway-events#guild-delete; +impl UpdateMessage for GuildUpdate { + fn id(&self) -> Option { + Some(self.guild.id) + } +} + +#[derive(Debug, Default, Deserialize, Serialize, Clone, SourceUrlField, JsonField)] +/// See ; /// Received to tell the client about a guild being deleted; pub struct GuildDelete { #[serde(flatten)] pub guild: UnavailableGuild, + #[serde(skip)] + pub source_url: String, + #[serde(skip)] + pub json: String, +} + +impl UpdateMessage for GuildDelete { + fn id(&self) -> Option { + Some(self.guild.id) + } + fn update(&mut self, _: Arc>) {} } impl WebSocketEvent for GuildDelete {} #[derive(Debug, Default, Deserialize, Serialize, Clone)] -/// See https://discord.com/developers/docs/topics/gateway-events#guild-audit-log-entry-create; +/// See ; /// Received to the client about an audit log entry being added; pub struct GuildAuditLogEntryCreate { #[serde(flatten)] @@ -84,7 +127,7 @@ pub struct GuildAuditLogEntryCreate { impl WebSocketEvent for GuildAuditLogEntryCreate {} #[derive(Debug, Default, Deserialize, Serialize, Clone)] -/// See https://discord.com/developers/docs/topics/gateway-events#guild-emojis-update; +/// See ; /// Received to tell the client about a change to a guild's emoji list; pub struct GuildEmojisUpdate { pub guild_id: Snowflake, @@ -94,7 +137,7 @@ pub struct GuildEmojisUpdate { impl WebSocketEvent for GuildEmojisUpdate {} #[derive(Debug, Default, Deserialize, Serialize, Clone)] -/// See https://discord.com/developers/docs/topics/gateway-events#guild-stickers-update; +/// See ; /// Received to tell the client about a change to a guild's sticker list; pub struct GuildStickersUpdate { pub guild_id: Snowflake, @@ -104,7 +147,7 @@ pub struct GuildStickersUpdate { impl WebSocketEvent for GuildStickersUpdate {} #[derive(Debug, Default, Deserialize, Serialize, Clone)] -/// See https://discord.com/developers/docs/topics/gateway-events#guild-integrations-update +/// See pub struct GuildIntegrationsUpdate { pub guild_id: Snowflake, } @@ -112,7 +155,7 @@ pub struct GuildIntegrationsUpdate { impl WebSocketEvent for GuildIntegrationsUpdate {} #[derive(Debug, Default, Deserialize, Serialize, Clone)] -/// See https://discord.com/developers/docs/topics/gateway-events#guild-member-add; +/// See ; /// Received to tell the client about a user joining a guild; pub struct GuildMemberAdd { #[serde(flatten)] @@ -123,7 +166,7 @@ pub struct GuildMemberAdd { impl WebSocketEvent for GuildMemberAdd {} #[derive(Debug, Default, Deserialize, Serialize, Clone)] -/// See https://discord.com/developers/docs/topics/gateway-events#guild-member-remove; +/// See ; /// Received to tell the client about a user leaving a guild; pub struct GuildMemberRemove { pub guild_id: Snowflake, @@ -133,7 +176,7 @@ pub struct GuildMemberRemove { impl WebSocketEvent for GuildMemberRemove {} #[derive(Debug, Default, Deserialize, Serialize, Clone)] -/// See https://discord.com/developers/docs/topics/gateway-events#guild-member-update +/// See pub struct GuildMemberUpdate { pub guild_id: Snowflake, pub roles: Vec, @@ -151,7 +194,7 @@ pub struct GuildMemberUpdate { impl WebSocketEvent for GuildMemberUpdate {} #[derive(Debug, Default, Deserialize, Serialize, Clone)] -/// See https://discord.com/developers/docs/topics/gateway-events#guild-members-chunk +/// See pub struct GuildMembersChunk { pub guild_id: Snowflake, pub members: Vec, @@ -164,26 +207,66 @@ pub struct GuildMembersChunk { impl WebSocketEvent for GuildMembersChunk {} -#[derive(Debug, Default, Deserialize, Serialize, Clone)] -/// See https://discord.com/developers/docs/topics/gateway-events#guild-role-create +#[derive(Debug, Default, Deserialize, Serialize, Clone, JsonField, SourceUrlField)] +/// See pub struct GuildRoleCreate { pub guild_id: Snowflake, pub role: RoleObject, + #[serde(skip)] + pub json: String, + #[serde(skip)] + pub source_url: String, } impl WebSocketEvent for GuildRoleCreate {} -#[derive(Debug, Default, Deserialize, Serialize, Clone)] -/// See https://discord.com/developers/docs/topics/gateway-events#guild-role-update +#[cfg(feature = "client")] +impl UpdateMessage for GuildRoleCreate { + fn id(&self) -> Option { + Some(self.guild_id) + } + + fn update(&mut self, object_to_update: Arc>) { + let mut object_to_update = object_to_update.write().unwrap(); + if object_to_update.roles.is_some() { + object_to_update + .roles + .as_mut() + .unwrap() + .push(Arc::new(RwLock::new(self.role.clone()))); + } else { + object_to_update.roles = Some(Vec::from([Arc::new(RwLock::new(self.role.clone()))])); + } + } +} + +#[derive(Debug, Default, Deserialize, Serialize, Clone, JsonField, SourceUrlField)] +/// See pub struct GuildRoleUpdate { pub guild_id: Snowflake, pub role: RoleObject, + #[serde(skip)] + pub json: String, + #[serde(skip)] + pub source_url: String, } impl WebSocketEvent for GuildRoleUpdate {} +#[cfg(feature = "client")] +impl UpdateMessage for GuildRoleUpdate { + fn id(&self) -> Option { + Some(self.role.id) + } + + fn update(&mut self, object_to_update: Arc>) { + let mut write = object_to_update.write().unwrap(); + *write = self.role.clone(); + } +} + #[derive(Debug, Default, Deserialize, Serialize, Clone)] -/// See https://discord.com/developers/docs/topics/gateway-events#guild-role-delete +/// See pub struct GuildRoleDelete { pub guild_id: Snowflake, pub role_id: Snowflake, @@ -192,7 +275,7 @@ pub struct GuildRoleDelete { impl WebSocketEvent for GuildRoleDelete {} #[derive(Debug, Default, Deserialize, Serialize, Clone)] -/// See https://discord.com/developers/docs/topics/gateway-events#guild-scheduled-event-create +/// See pub struct GuildScheduledEventCreate { #[serde(flatten)] pub event: GuildScheduledEvent, @@ -201,7 +284,7 @@ pub struct GuildScheduledEventCreate { impl WebSocketEvent for GuildScheduledEventCreate {} #[derive(Debug, Default, Deserialize, Serialize, Clone)] -/// See https://discord.com/developers/docs/topics/gateway-events#guild-scheduled-event-update +/// See pub struct GuildScheduledEventUpdate { #[serde(flatten)] pub event: GuildScheduledEvent, @@ -210,7 +293,7 @@ pub struct GuildScheduledEventUpdate { impl WebSocketEvent for GuildScheduledEventUpdate {} #[derive(Debug, Default, Deserialize, Serialize, Clone)] -/// See https://discord.com/developers/docs/topics/gateway-events#guild-scheduled-event-delete +/// See pub struct GuildScheduledEventDelete { #[serde(flatten)] pub event: GuildScheduledEvent, @@ -219,7 +302,7 @@ pub struct GuildScheduledEventDelete { impl WebSocketEvent for GuildScheduledEventDelete {} #[derive(Debug, Default, Deserialize, Serialize, Clone)] -/// See https://discord.com/developers/docs/topics/gateway-events#guild-scheduled-event-user-add +/// See pub struct GuildScheduledEventUserAdd { pub guild_scheduled_event_id: Snowflake, pub user_id: Snowflake, @@ -229,7 +312,7 @@ pub struct GuildScheduledEventUserAdd { impl WebSocketEvent for GuildScheduledEventUserAdd {} #[derive(Debug, Default, Deserialize, Serialize, Clone)] -/// See https://discord.com/developers/docs/topics/gateway-events#guild-scheduled-event-user-remove +/// See pub struct GuildScheduledEventUserRemove { pub guild_scheduled_event_id: Snowflake, pub user_id: Snowflake, diff --git a/src/types/events/hello.rs b/src/types/events/hello.rs index 44f1e4f..fef3e22 100644 --- a/src/types/events/hello.rs +++ b/src/types/events/hello.rs @@ -2,7 +2,7 @@ use crate::types::WebSocketEvent; use serde::{Deserialize, Serialize}; /// Received on gateway init, tells the client how often to send heartbeats; -#[derive(Debug, Default, Deserialize, Serialize)] +#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq, Eq)] pub struct GatewayHello { pub op: i32, pub d: HelloData, @@ -10,7 +10,7 @@ pub struct GatewayHello { impl WebSocketEvent for GatewayHello {} -#[derive(Debug, Default, Deserialize, Serialize, Clone)] +#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq, Eq, Copy)] /// Contains info on how often the client should send heartbeats to the server; pub struct HelloData { /// How often a client should send heartbeats, in milliseconds diff --git a/src/types/events/identify.rs b/src/types/events/identify.rs index 4642fb6..12bc369 100644 --- a/src/types/events/identify.rs +++ b/src/types/events/identify.rs @@ -2,7 +2,7 @@ use crate::types::events::{PresenceUpdate, WebSocketEvent}; use serde::{Deserialize, Serialize}; use serde_with::serde_as; -#[derive(Debug, Deserialize, Serialize, Clone)] +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] pub struct GatewayIdentifyPayload { pub token: String, pub properties: GatewayIdentifyConnectionProps, @@ -68,7 +68,7 @@ impl GatewayIdentifyPayload { impl WebSocketEvent for GatewayIdentifyPayload {} -#[derive(Debug, Deserialize, Serialize, Clone)] +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] #[serde_as] pub struct GatewayIdentifyConnectionProps { /// Almost always sent @@ -144,7 +144,7 @@ impl GatewayIdentifyConnectionProps { referring_domain: None, referrer_current: None, release_channel: String::from("stable"), - client_build_number: 199933, + client_build_number: 0, } } @@ -159,7 +159,7 @@ impl GatewayIdentifyConnectionProps { system_locale: String::from("en-US"), os: String::from("Windows"), os_version: Some(String::from("10")), - client_build_number: 199933, + client_build_number: 222963, release_channel: String::from("stable"), ..Self::minimal() } diff --git a/src/types/events/integration.rs b/src/types/events/integration.rs index de55a5b..2423e78 100644 --- a/src/types/events/integration.rs +++ b/src/types/events/integration.rs @@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize}; use crate::types::{Integration, Snowflake, WebSocketEvent}; #[derive(Debug, Default, Deserialize, Serialize, Clone)] -/// See https://discord.com/developers/docs/topics/gateway-events#integration-create +/// See pub struct IntegrationCreate { #[serde(flatten)] pub integration: Integration, @@ -13,7 +13,7 @@ pub struct IntegrationCreate { impl WebSocketEvent for IntegrationCreate {} #[derive(Debug, Default, Deserialize, Serialize, Clone)] -/// See https://discord.com/developers/docs/topics/gateway-events#integration-update +/// See pub struct IntegrationUpdate { #[serde(flatten)] pub integration: Integration, @@ -23,7 +23,7 @@ pub struct IntegrationUpdate { impl WebSocketEvent for IntegrationUpdate {} #[derive(Debug, Default, Deserialize, Serialize, Clone)] -/// See https://discord.com/developers/docs/topics/gateway-events#integration-delete +/// See pub struct IntegrationDelete { pub id: Snowflake, pub guild_id: Snowflake, diff --git a/src/types/events/interaction.rs b/src/types/events/interaction.rs index e77ee7c..304e7d4 100644 --- a/src/types/events/interaction.rs +++ b/src/types/events/interaction.rs @@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize}; use crate::types::{Interaction, WebSocketEvent}; #[derive(Debug, Default, Deserialize, Serialize, Clone)] -/// See https://discord.com/developers/docs/topics/gateway-events#interaction-create +/// See pub struct InteractionCreate { #[serde(flatten)] pub interaction: Interaction, diff --git a/src/types/events/invite.rs b/src/types/events/invite.rs index f04134d..674cc62 100644 --- a/src/types/events/invite.rs +++ b/src/types/events/invite.rs @@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize}; use crate::types::{GuildInvite, Snowflake, WebSocketEvent}; #[derive(Debug, Default, Deserialize, Serialize, Clone)] -/// See https://discord.com/developers/docs/topics/gateway-events#invite-create +/// See pub struct InviteCreate { #[serde(flatten)] pub invite: GuildInvite, @@ -12,7 +12,7 @@ pub struct InviteCreate { impl WebSocketEvent for InviteCreate {} #[derive(Debug, Default, Deserialize, Serialize, Clone)] -/// See https://discord.com/developers/docs/topics/gateway-events#invite-delete +/// See pub struct InviteDelete { pub channel_id: Snowflake, pub guild_id: Option, diff --git a/src/types/events/lazy_request.rs b/src/types/events/lazy_request.rs index 2fd98af..fd53183 100644 --- a/src/types/events/lazy_request.rs +++ b/src/types/events/lazy_request.rs @@ -13,7 +13,7 @@ use super::WebSocketEvent; /// Sent by the official client when switching to a guild or channel; /// After this, you should recieve message updates /// -/// See https://luna.gitlab.io/discord-unofficial-docs/lazy_guilds.html#op-14-lazy-request +/// See /// /// {"op":14,"d":{"guild_id":"848582562217590824","typing":true,"activities":true,"threads":true}} pub struct LazyRequest { diff --git a/src/types/events/message.rs b/src/types/events/message.rs index 5a67417..fac083b 100644 --- a/src/types/events/message.rs +++ b/src/types/events/message.rs @@ -8,6 +8,8 @@ use crate::types::{ use super::WebSocketEvent; #[derive(Debug, Deserialize, Serialize, Default, Clone)] +/// # Reference +/// See pub struct TypingStartEvent { pub channel_id: Snowflake, pub guild_id: Option, @@ -19,92 +21,106 @@ pub struct TypingStartEvent { impl WebSocketEvent for TypingStartEvent {} #[derive(Debug, Serialize, Deserialize, Default, Clone)] -/// See https://discord.com/developers/docs/topics/gateway-events#message-create +/// See pub struct MessageCreate { #[serde(flatten)] - message: Message, - guild_id: Option, - member: Option, - mentions: Option>, + pub message: Message, + pub guild_id: Option, + pub member: Option, + pub mentions: Option>, } #[derive(Debug, Serialize, Deserialize, Default, Clone)] -/// See https://discord.com/developers/docs/topics/gateway-events#message-create-message-create-extra-fields +/// See pub struct MessageCreateUser { #[serde(flatten)] - user: PublicUser, - member: Option, + pub user: PublicUser, + pub member: Option, } impl WebSocketEvent for MessageCreate {} #[derive(Debug, Serialize, Deserialize, Default, Clone)] +/// # Reference +/// See pub struct MessageUpdate { #[serde(flatten)] - message: Message, - guild_id: Option, - member: Option, - mentions: Option>, + pub message: Message, + pub guild_id: Option, + pub member: Option, + pub mentions: Option>, } impl WebSocketEvent for MessageUpdate {} #[derive(Debug, Serialize, Deserialize, Default, Clone)] +/// # Reference +/// See pub struct MessageDelete { - id: Snowflake, - channel_id: Snowflake, - guild_id: Option, + pub id: Snowflake, + pub channel_id: Snowflake, + pub guild_id: Option, } impl WebSocketEvent for MessageDelete {} #[derive(Debug, Serialize, Deserialize, Default, Clone)] +/// # Reference +/// See pub struct MessageDeleteBulk { - ids: Vec, - channel_id: Snowflake, - guild_id: Option, + pub ids: Vec, + pub channel_id: Snowflake, + pub guild_id: Option, } impl WebSocketEvent for MessageDeleteBulk {} #[derive(Debug, Serialize, Deserialize, Default, Clone)] +/// # Reference +/// See pub struct MessageReactionAdd { - user_id: Snowflake, - channel_id: Snowflake, - message_id: Snowflake, - guild_id: Option, - member: Option, - emoji: Emoji, + pub user_id: Snowflake, + pub channel_id: Snowflake, + pub message_id: Snowflake, + pub guild_id: Option, + pub member: Option, + pub emoji: Emoji, } impl WebSocketEvent for MessageReactionAdd {} #[derive(Debug, Serialize, Deserialize, Default, Clone)] +/// # Reference +/// See pub struct MessageReactionRemove { - user_id: Snowflake, - channel_id: Snowflake, - message_id: Snowflake, - guild_id: Option, - emoji: Emoji, + pub user_id: Snowflake, + pub channel_id: Snowflake, + pub message_id: Snowflake, + pub guild_id: Option, + pub emoji: Emoji, } impl WebSocketEvent for MessageReactionRemove {} #[derive(Debug, Serialize, Deserialize, Default, Clone)] +/// # Reference +/// See pub struct MessageReactionRemoveAll { - channel_id: Snowflake, - message_id: Snowflake, - guild_id: Option, + pub channel_id: Snowflake, + pub message_id: Snowflake, + pub guild_id: Option, } impl WebSocketEvent for MessageReactionRemoveAll {} #[derive(Debug, Serialize, Deserialize, Default, Clone)] +/// # Reference +/// See pub struct MessageReactionRemoveEmoji { - channel_id: Snowflake, - message_id: Snowflake, - guild_id: Option, - emoji: Emoji, + pub channel_id: Snowflake, + pub message_id: Snowflake, + pub guild_id: Option, + pub emoji: Emoji, } impl WebSocketEvent for MessageReactionRemoveEmoji {} @@ -114,7 +130,7 @@ impl WebSocketEvent for MessageReactionRemoveEmoji {} /// /// Not documented anywhere unofficially /// -/// Apparently "Message ACK refers to marking a message as read for Discord's API." (https://github.com/Rapptz/discord.py/issues/1851) +/// Apparently "Message ACK refers to marking a message as read for Discord's API." () /// I suspect this is sent and recieved from the gateway to let clients on other devices know the user has read a message /// /// {"t":"MESSAGE_ACK","s":3,"op":0,"d":{"version":52,"message_id":"1107236673638633472","last_viewed":null,"flags":null,"channel_id":"967363950217936897"}} diff --git a/src/types/events/mod.rs b/src/types/events/mod.rs index 26dcd80..f4e926c 100644 --- a/src/types/events/mod.rs +++ b/src/types/events/mod.rs @@ -1,6 +1,5 @@ use serde::{Deserialize, Serialize}; -use crate::gateway::Updateable; pub use application::*; pub use auto_moderation::*; pub use call::*; @@ -28,8 +27,25 @@ pub use voice::*; pub use webhooks::*; pub use webrtc::*; +#[cfg(feature = "client")] use super::Snowflake; +#[cfg(feature = "client")] +use crate::gateway::Updateable; + +#[cfg(feature = "client")] +use serde_json::{from_str, from_value, to_value, Value}; + +#[cfg(feature = "client")] +use std::collections::HashMap; + +use std::fmt::Debug; +#[cfg(feature = "client")] +use std::sync::{Arc, RwLock}; + +#[cfg(feature = "client")] +use serde::de::DeserializeOwned; + mod application; mod auto_moderation; mod call; @@ -58,12 +74,12 @@ mod webhooks; mod webrtc; -pub trait WebSocketEvent {} +pub trait WebSocketEvent: Send + Sync + Debug {} #[derive(Debug, Default, Serialize, Clone)] /// The payload used for sending events to the gateway /// -/// Similar to [GatewayReceivePayload], except we send a [Value] for d whilst we receive a [serde_json::value::RawValue] +/// Similar to [GatewayReceivePayload], except we send a [serde_json::value::Value] for d whilst we receive a [serde_json::value::RawValue] /// Also, we never need to send the event name pub struct GatewaySendPayload { #[serde(rename = "op")] @@ -82,9 +98,6 @@ impl WebSocketEvent for GatewaySendPayload {} #[derive(Debug, Default, Deserialize, Clone)] /// The payload used for receiving events from the gateway -/// -/// Similar to [GatewaySendPayload], except we send a [Value] for d whilst we receive a [serde_json::value::RawValue] -/// Also, we never need to sent the event name pub struct GatewayReceivePayload<'a> { #[serde(rename = "op")] pub op_code: u8, @@ -102,6 +115,7 @@ pub struct GatewayReceivePayload<'a> { impl<'a> WebSocketEvent for GatewayReceivePayload<'a> {} +#[cfg(feature = "client")] /// An [`UpdateMessage`] represents a received Gateway Message which contains updated /// information for an [`Updateable`] of Type T. /// # Example: @@ -114,10 +128,37 @@ impl<'a> WebSocketEvent for GatewayReceivePayload<'a> {} /// This would imply, that the [`WebSocketEvent`] "[`ChannelUpdate`]" contains new/updated information /// about a [`Channel`]. The update method describes how this new information will be turned into /// a [`Channel`] object. -pub(crate) trait UpdateMessage: Clone +pub(crate) trait UpdateMessage: Clone + JsonField + SourceUrlField where - T: Updateable, + T: Updateable + Serialize + DeserializeOwned + Clone, { - fn update(&self, object_to_update: &mut T); - fn id(&self) -> Snowflake; + fn update(&mut self, object_to_update: Arc>) { + update_object(self.get_json(), object_to_update) + } + fn id(&self) -> Option; +} + +pub(crate) trait JsonField: Clone { + fn set_json(&mut self, json: String); + fn get_json(&self) -> String; +} + +pub trait SourceUrlField: Clone { + fn set_source_url(&mut self, url: String); + fn get_source_url(&self) -> String; +} + +#[cfg(feature = "client")] +/// Only applicable for events where the Update struct is the same as the Entity struct +pub(crate) fn update_object( + value: String, + object: Arc>, +) { + let data_from_event: HashMap = from_str(&value).unwrap(); + let mut original_data: HashMap = + from_value(to_value(object.clone()).unwrap()).unwrap(); + for (updated_entry_key, updated_entry_value) in data_from_event.into_iter() { + original_data.insert(updated_entry_key.clone(), updated_entry_value); + } + *object.write().unwrap() = from_value(to_value(original_data).unwrap()).unwrap(); } diff --git a/src/types/events/presence.rs b/src/types/events/presence.rs index ad06954..e9a7dee 100644 --- a/src/types/events/presence.rs +++ b/src/types/events/presence.rs @@ -4,9 +4,9 @@ use serde::{Deserialize, Serialize}; #[derive(Debug, Deserialize, Serialize, Default, Clone)] /// Sent by the client to update its status and presence; -/// See https://discord.com/developers/docs/topics/gateway-events#update-presence +/// See pub struct UpdatePresence { - /// unix time of when the client went idle, or none if client is not idle + /// Unix time of when the client went idle, or none if client is not idle. pub since: Option, /// the client's status (online, invisible, offline, dnd, idle..) pub status: UserStatus, @@ -14,9 +14,9 @@ pub struct UpdatePresence { pub afk: bool, } -#[derive(Debug, Deserialize, Serialize, Default, Clone)] +#[derive(Debug, Deserialize, Serialize, Default, Clone, PartialEq)] /// Received to tell the client that a user updated their presence / status -/// See https://discord.com/developers/docs/topics/gateway-events#presence-update-presence-update-event-fields +/// See pub struct PresenceUpdate { pub user: PublicUser, #[serde(default)] diff --git a/src/types/events/ready.rs b/src/types/events/ready.rs index 9b6eab9..ea46b69 100644 --- a/src/types/events/ready.rs +++ b/src/types/events/ready.rs @@ -8,7 +8,7 @@ use crate::types::{Activity, GuildMember, PresenceUpdate, VoiceState}; #[derive(Debug, Deserialize, Serialize, Default, Clone)] /// 1/2 half documented; /// Received after identifying, provides initial user info; -/// See https://discord.com/developers/docs/topics/gateway-events#ready; +/// See pub struct GatewayReady { pub analytics_token: Option, pub auth_session_id_hash: Option, @@ -16,7 +16,7 @@ pub struct GatewayReady { pub v: u8, pub user: User, - /// For bots these are [UnavailableGuild]s, for users they are [Guild] + /// For bots these are [crate::types::UnavailableGuild]s, for users they are [Guild] pub guilds: Vec, pub presences: Option>, pub sessions: Option>, diff --git a/src/types/events/relationship.rs b/src/types/events/relationship.rs index 441c74a..a1f75a5 100644 --- a/src/types/events/relationship.rs +++ b/src/types/events/relationship.rs @@ -2,7 +2,7 @@ use crate::types::{events::WebSocketEvent, Relationship, RelationshipType, Snowf use serde::{Deserialize, Serialize}; #[derive(Debug, Deserialize, Serialize, Default)] -/// See https://github.com/spacebarchat/server/issues/204 +/// See pub struct RelationshipAdd { #[serde(flatten)] pub relationship: Relationship, @@ -12,7 +12,7 @@ pub struct RelationshipAdd { impl WebSocketEvent for RelationshipAdd {} #[derive(Debug, Deserialize, Serialize, Default, Clone)] -/// See https://github.com/spacebarchat/server/issues/203 +/// See pub struct RelationshipRemove { pub id: Snowflake, #[serde(rename = "type")] diff --git a/src/types/events/request_members.rs b/src/types/events/request_members.rs index 2d537b9..526313b 100644 --- a/src/types/events/request_members.rs +++ b/src/types/events/request_members.rs @@ -2,7 +2,7 @@ use crate::types::{events::WebSocketEvent, Snowflake}; use serde::{Deserialize, Serialize}; #[derive(Debug, Deserialize, Serialize, Default)] -/// See https://discord.com/developers/docs/topics/gateway-events#request-guild-members-request-guild-members-structure +/// See pub struct GatewayRequestGuildMembers { pub guild_id: Snowflake, pub query: Option, diff --git a/src/types/events/session.rs b/src/types/events/session.rs index 574c5fa..868c8e8 100644 --- a/src/types/events/session.rs +++ b/src/types/events/session.rs @@ -13,7 +13,7 @@ pub struct SessionsReplace { #[derive(Debug, Deserialize, Serialize, Default, Clone)] /// Session info for the current user pub struct Session { - pub activities: Vec, + pub activities: Option>, pub client_info: ClientInfo, pub session_id: String, pub status: String, @@ -24,7 +24,7 @@ pub struct Session { /// {"client":"web","os":"other","version":0} // Note: I don't think this one exists yet? Though I might've made a mistake and this might be a duplicate pub struct ClientInfo { - pub client: String, + pub client: Option, pub os: String, pub version: u8, } diff --git a/src/types/events/stage_instance.rs b/src/types/events/stage_instance.rs index 0fe487b..c2bbc46 100644 --- a/src/types/events/stage_instance.rs +++ b/src/types/events/stage_instance.rs @@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize}; use crate::types::{StageInstance, WebSocketEvent}; #[derive(Debug, Deserialize, Serialize, Default, Clone)] -/// See https://discord.com/developers/docs/topics/gateway-events#stage-instance-create +/// See pub struct StageInstanceCreate { #[serde(flatten)] pub stage_instance: StageInstance, @@ -12,7 +12,7 @@ pub struct StageInstanceCreate { impl WebSocketEvent for StageInstanceCreate {} #[derive(Debug, Deserialize, Serialize, Default, Clone)] -/// See https://discord.com/developers/docs/topics/gateway-events#stage-instance-update +/// See pub struct StageInstanceUpdate { #[serde(flatten)] pub stage_instance: StageInstance, @@ -21,7 +21,7 @@ pub struct StageInstanceUpdate { impl WebSocketEvent for StageInstanceUpdate {} #[derive(Debug, Deserialize, Serialize, Default, Clone)] -/// See https://discord.com/developers/docs/topics/gateway-events#stage-instance-delete +/// See pub struct StageInstanceDelete { #[serde(flatten)] pub stage_instance: StageInstance, diff --git a/src/types/events/thread.rs b/src/types/events/thread.rs index e8276e7..cff5f6f 100644 --- a/src/types/events/thread.rs +++ b/src/types/events/thread.rs @@ -1,11 +1,15 @@ +use chorus_macros::{JsonField, SourceUrlField}; use serde::{Deserialize, Serialize}; use crate::types::entities::{Channel, ThreadMember}; use crate::types::events::WebSocketEvent; -use crate::types::Snowflake; +use crate::types::{JsonField, Snowflake, SourceUrlField}; + +#[cfg(feature = "client")] +use super::UpdateMessage; #[derive(Debug, Default, Deserialize, Serialize, Clone)] -/// See https://discord.com/developers/docs/topics/gateway-events#thread-create +/// See pub struct ThreadCreate { #[serde(flatten)] pub thread: Channel, @@ -13,17 +17,28 @@ pub struct ThreadCreate { impl WebSocketEvent for ThreadCreate {} -#[derive(Debug, Default, Deserialize, Serialize, Clone)] -/// See https://discord.com/developers/docs/topics/gateway-events#thread-update +#[derive(Debug, Default, Deserialize, Serialize, Clone, JsonField, SourceUrlField)] +/// See pub struct ThreadUpdate { #[serde(flatten)] pub thread: Channel, + #[serde(skip)] + pub json: String, + #[serde(skip)] + pub source_url: String, } impl WebSocketEvent for ThreadUpdate {} +#[cfg(feature = "client")] +impl UpdateMessage for ThreadUpdate { + fn id(&self) -> Option { + Some(self.thread.id) + } +} + #[derive(Debug, Default, Deserialize, Serialize, Clone)] -/// See https://discord.com/developers/docs/topics/gateway-events#thread-delete +/// See pub struct ThreadDelete { #[serde(flatten)] pub thread: Channel, @@ -32,7 +47,7 @@ pub struct ThreadDelete { impl WebSocketEvent for ThreadDelete {} #[derive(Debug, Default, Deserialize, Serialize, Clone)] -/// See https://discord.com/developers/docs/topics/gateway-events#thread-list-sync +/// See pub struct ThreadListSync { pub guild_id: Snowflake, pub channel_ids: Option>, @@ -43,7 +58,7 @@ pub struct ThreadListSync { impl WebSocketEvent for ThreadListSync {} #[derive(Debug, Default, Deserialize, Serialize, Clone)] -/// See https://discord.com/developers/docs/topics/gateway-events#thread-member-update +/// See /// The inner payload is a thread member object with an extra field. pub struct ThreadMemberUpdate { #[serde(flatten)] @@ -54,7 +69,7 @@ pub struct ThreadMemberUpdate { impl WebSocketEvent for ThreadMemberUpdate {} #[derive(Debug, Default, Deserialize, Serialize, Clone)] -/// See https://discord.com/developers/docs/topics/gateway-events#thread-members-update +/// See pub struct ThreadMembersUpdate { pub id: Snowflake, pub guild_id: Snowflake, diff --git a/src/types/events/user.rs b/src/types/events/user.rs index 18c8511..7165812 100644 --- a/src/types/events/user.rs +++ b/src/types/events/user.rs @@ -4,8 +4,8 @@ use crate::types::entities::PublicUser; use crate::types::events::WebSocketEvent; use crate::types::utils::Snowflake; -#[derive(Debug, Default, Deserialize, Serialize, Clone)] -/// See https://discord.com/developers/docs/topics/gateway-events#user-update; +#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq, Eq)] +/// See ; /// Sent to indicate updates to a user object; (name changes, discriminator changes, etc); pub struct UserUpdate { #[serde(flatten)] @@ -14,7 +14,7 @@ pub struct UserUpdate { impl WebSocketEvent for UserUpdate {} -#[derive(Debug, Default, Deserialize, Serialize, Clone)] +#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq, Eq)] /// Undocumented; /// /// Possibly an update for muted guild / channel settings for the current user; @@ -39,7 +39,7 @@ pub struct UserGuildSettingsUpdate { impl WebSocketEvent for UserGuildSettingsUpdate {} -#[derive(Debug, Default, Deserialize, Serialize, Clone)] +#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq, Eq)] /// Undocumented; /// /// Received in [UserGuildSettingsUpdate]; diff --git a/src/types/events/voice.rs b/src/types/events/voice.rs index 63e740a..2618ee1 100644 --- a/src/types/events/voice.rs +++ b/src/types/events/voice.rs @@ -1,7 +1,7 @@ use crate::types::{events::WebSocketEvent, Snowflake, VoiceState}; use serde::{Deserialize, Serialize}; -#[derive(Debug, Deserialize, Serialize, Default)] +#[derive(Debug, Deserialize, Serialize, Default, Clone, Copy, PartialEq, Eq)] /// /// Sent to the server to indicate an update of the voice state (leave voice channel, join voice channel, mute, deafen); /// @@ -16,7 +16,7 @@ pub struct UpdateVoiceState { impl WebSocketEvent for UpdateVoiceState {} #[derive(Debug, Deserialize, Serialize, Default, Clone)] -/// See https://discord.com/developers/docs/topics/gateway-events#voice-state-update; +/// See ; /// /// Received from the server to indicate an update in a user's voice state (leave voice channel, join voice channel, mute, deafen, etc); /// @@ -28,8 +28,8 @@ pub struct VoiceStateUpdate { impl WebSocketEvent for VoiceStateUpdate {} -#[derive(Debug, Deserialize, Serialize, Default, Clone)] -/// See https://discord.com/developers/docs/topics/gateway-events#voice-server-update; +#[derive(Debug, Deserialize, Serialize, Default, Clone, PartialEq, Eq)] +/// See ; /// /// Received to indicate which voice endpoint, token and guild_id to use; pub struct VoiceServerUpdate { diff --git a/src/types/events/webhooks.rs b/src/types/events/webhooks.rs index 3f0158e..518b332 100644 --- a/src/types/events/webhooks.rs +++ b/src/types/events/webhooks.rs @@ -5,7 +5,7 @@ use crate::types::Snowflake; use super::WebSocketEvent; #[derive(Debug, Deserialize, Serialize, Default, Clone)] -/// See https://discord.com/developers/docs/topics/gateway-events#webhooks-update +/// See pub struct WebhooksUpdate { pub guild_id: Snowflake, pub channel_id: Snowflake, diff --git a/src/types/events/webrtc/identify.rs b/src/types/events/webrtc/identify.rs index f41017d..45f1037 100644 --- a/src/types/events/webrtc/identify.rs +++ b/src/types/events/webrtc/identify.rs @@ -1,7 +1,7 @@ use crate::types::{Snowflake, WebSocketEvent}; use serde::{Deserialize, Serialize}; -#[derive(Debug, Deserialize, Serialize, Default, Clone)] +#[derive(Debug, Deserialize, Serialize, Default, Clone, PartialEq, Eq)] /// The identify payload for the webrtc stream; /// Contains info to begin a webrtc connection; /// See https://discord.com/developers/docs/topics/voice-connections#establishing-a-voice-websocket-connection-example-voice-identify-payload; diff --git a/src/types/events/webrtc/mod.rs b/src/types/events/webrtc/mod.rs index 53735e0..ecfe3da 100644 --- a/src/types/events/webrtc/mod.rs +++ b/src/types/events/webrtc/mod.rs @@ -48,7 +48,7 @@ impl<'a> WebSocketEvent for VoiceGatewayReceivePayload<'a> {} /// The modes of encryption available in webrtc connections; /// See https://discord.com/developers/docs/topics/voice-connections#establishing-a-voice-udp-connection-encryption-modes; -#[derive(Debug, Default, Serialize, Deserialize, Clone, Copy)] +#[derive(Debug, Default, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum WebrtcEncryptionMode { #[default] diff --git a/src/types/events/webrtc/ready.rs b/src/types/events/webrtc/ready.rs index ff57eae..5e7286d 100644 --- a/src/types/events/webrtc/ready.rs +++ b/src/types/events/webrtc/ready.rs @@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize}; use super::WebrtcEncryptionMode; -#[derive(Debug, Deserialize, Serialize, Clone)] +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] /// The ready event for the webrtc stream; /// Used to give info after the identify event; /// See https://discord.com/developers/docs/topics/voice-connections#establishing-a-voice-websocket-connection-example-voice-ready-payload; diff --git a/src/types/interfaces/activity.rs b/src/types/interfaces/activity.rs index 1a48dfd..0da4747 100644 --- a/src/types/interfaces/activity.rs +++ b/src/types/interfaces/activity.rs @@ -2,7 +2,7 @@ use serde::{Deserialize, Serialize}; use crate::types::{entities::Emoji, Snowflake}; -#[derive(Debug, Deserialize, Serialize, Clone)] +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] pub struct Activity { name: String, #[serde(rename = "type")] @@ -22,19 +22,19 @@ pub struct Activity { buttons: Option>, } -#[derive(Debug, Deserialize, Serialize, Clone)] +#[derive(Debug, Deserialize, Serialize, Clone, Copy, PartialEq, Eq)] struct ActivityTimestamps { start: Option, end: Option, } -#[derive(Debug, Deserialize, Serialize, Clone)] +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] struct ActivityParty { id: Option, size: Option>, } -#[derive(Debug, Deserialize, Serialize, Clone)] +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] struct ActivityAssets { large_image: Option, large_text: Option, @@ -42,7 +42,7 @@ struct ActivityAssets { small_text: Option, } -#[derive(Debug, Deserialize, Serialize, Clone)] +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] struct ActivitySecrets { join: Option, spectate: Option, @@ -50,7 +50,7 @@ struct ActivitySecrets { match_string: Option, } -#[derive(Debug, Deserialize, Serialize, Clone)] +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] struct ActivityButton { label: String, url: String, diff --git a/src/types/interfaces/guild_welcome_screen.rs b/src/types/interfaces/guild_welcome_screen.rs index 4912a78..dbeef0f 100644 --- a/src/types/interfaces/guild_welcome_screen.rs +++ b/src/types/interfaces/guild_welcome_screen.rs @@ -2,14 +2,14 @@ use serde::{Deserialize, Serialize}; use crate::types::utils::Snowflake; -#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq)] +#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, Hash)] pub struct WelcomeScreenObject { pub enabled: bool, pub description: Option, pub welcome_channels: Vec, } -#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq)] +#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, Hash)] pub struct WelcomeScreenChannel { pub channel_id: Snowflake, pub description: String, diff --git a/src/types/interfaces/interaction.rs b/src/types/interfaces/interaction.rs index dd131c8..2aa29fd 100644 --- a/src/types/interfaces/interaction.rs +++ b/src/types/interfaces/interaction.rs @@ -24,6 +24,7 @@ pub enum InteractionType { ApplicationCommand = 2, } +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub enum InteractionResponseType { SelfCommandResponse = 0, Pong = 1, @@ -33,6 +34,7 @@ pub enum InteractionResponseType { AcknowledgeWithSource = 5, } +#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)] pub struct InteractionApplicationCommandCallbackData { pub tts: bool, pub content: String, diff --git a/src/types/interfaces/status.rs b/src/types/interfaces/status.rs index c82e665..d5c07b6 100644 --- a/src/types/interfaces/status.rs +++ b/src/types/interfaces/status.rs @@ -1,7 +1,7 @@ use serde::{Deserialize, Serialize}; -#[derive(Debug, Deserialize, Serialize, Default, Clone)] -/// See https://discord.com/developers/docs/topics/gateway-events#client-status-object +#[derive(Debug, Deserialize, Serialize, Default, Clone, PartialEq, Eq)] +/// See pub struct ClientStatusObject { pub desktop: Option, pub mobile: Option, diff --git a/src/types/mod.rs b/src/types/mod.rs index 8554011..6f06ef0 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -1,3 +1,5 @@ +//! All the types, entities, events and interfaces of the Spacebar API. + pub use config::*; pub use entities::*; pub use errors::*; diff --git a/src/types/schema/auth.rs b/src/types/schema/auth.rs index 9a3b9d6..2159de9 100644 --- a/src/types/schema/auth.rs +++ b/src/types/schema/auth.rs @@ -1,6 +1,6 @@ use serde::{Deserialize, Serialize}; -#[derive(Debug, Default, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub struct RegisterSchema { pub username: String, @@ -15,7 +15,7 @@ pub struct RegisterSchema { pub promotional_email_opt_in: Option, } -#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub struct LoginSchema { /// For Discord, usernames must be between 2 and 32 characters, @@ -30,7 +30,7 @@ pub struct LoginSchema { pub gift_code_sku_id: Option, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub struct TotpSchema { code: String, diff --git a/src/types/schema/channel.rs b/src/types/schema/channel.rs index 27c78ee..354459c 100644 --- a/src/types/schema/channel.rs +++ b/src/types/schema/channel.rs @@ -1,6 +1,7 @@ use bitflags::bitflags; use serde::{Deserialize, Serialize}; +use crate::types::ChannelType; use crate::types::{entities::PermissionOverwrite, Snowflake}; #[derive(Debug, Deserialize, Serialize, Default, PartialEq, PartialOrd)] @@ -8,7 +9,7 @@ use crate::types::{entities::PermissionOverwrite, Snowflake}; pub struct ChannelCreateSchema { pub name: String, #[serde(rename = "type")] - pub channel_type: Option, + pub channel_type: Option, pub topic: Option, pub icon: Option, pub bitrate: Option, @@ -148,3 +149,12 @@ pub struct AddChannelRecipientSchema { pub access_token: Option, pub nick: Option, } + +/// See +#[derive(Debug, Deserialize, Serialize, Clone, Default, PartialOrd, Ord, PartialEq, Eq)] +pub struct ModifyChannelPositionsSchema { + pub id: Snowflake, + pub position: Option, + pub lock_permissions: Option, + pub parent_id: Option, +} diff --git a/src/types/schema/guild.rs b/src/types/schema/guild.rs index cb0dac8..56c0f9e 100644 --- a/src/types/schema/guild.rs +++ b/src/types/schema/guild.rs @@ -1,11 +1,18 @@ +use bitflags::bitflags; +use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use crate::types::entities::Channel; +use crate::types::types::guild_configuration::GuildFeatures; +use crate::types::{ + Emoji, ExplicitContentFilterLevel, MessageNotificationLevel, Snowflake, Sticker, + SystemChannelFlags, VerificationLevel, +}; -#[derive(Debug, Deserialize, Serialize)] +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] #[serde(rename_all = "snake_case")] /// Represents the schema which needs to be sent to create a Guild. -/// See: [https://docs.spacebar.chat/routes/#cmp--schemas-guildcreateschema](https://docs.spacebar.chat/routes/#cmp--schemas-guildcreateschema) +/// See: pub struct GuildCreateSchema { pub name: Option, pub region: Option, @@ -15,3 +22,144 @@ pub struct GuildCreateSchema { pub system_channel_id: Option, pub rules_channel_id: Option, } + +#[derive(Debug, Deserialize, Serialize, Default, Clone, Copy, Eq, PartialEq)] +#[serde(rename_all = "snake_case")] +/// Represents the schema which needs to be sent to create a Guild Ban. +/// See: +pub struct GuildBanCreateSchema { + pub delete_message_days: Option, + pub delete_message_seconds: Option, +} + +#[derive(Debug, Deserialize, Serialize, Default, Clone, Eq, PartialEq)] +#[serde(rename_all = "snake_case")] +pub struct GuildModifySchema { + pub name: Option, + pub icon: Option>, + pub banner: Option>, + pub home_header: Option>, + pub splash: Option>, + pub discovery_splash: Option>, + pub owner_id: Option, + pub description: Option, + pub region: Option, + pub afk_channel_id: Option, + pub afk_timeout: Option, + pub verification_level: Option, + pub default_message_notifications: Option, + pub explicit_content_filter: Option, + pub features: Option>, + pub system_channel_id: Option, + pub system_channel_flags: Option, + pub rules_channel_id: Option, + pub public_updates_channel_id: Option, + pub safety_alerts_channel_id: Option, + pub preferred_locale: Option, + pub premium_progress_bar_enabled: Option, +} + +#[derive(Debug, Deserialize, Serialize, Clone, Eq, PartialEq, Ord, PartialOrd)] +pub struct GetUserGuildSchema { + pub before: Option, + pub after: Option, + pub limit: Option, + pub with_counts: Option, +} + +impl std::default::Default for GetUserGuildSchema { + fn default() -> Self { + Self { + before: Default::default(), + after: Default::default(), + limit: Some(200), + with_counts: Some(false), + } + } +} + +#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq, PartialOrd)] +pub struct GuildPreview { + pub id: Snowflake, + pub name: String, + pub icon: Option, + pub description: Option, + pub splash: Option, + pub discovery_splash: Option, + pub home_header: Option, + pub features: Vec, + pub emojis: Vec, + pub stickers: Vec, + pub approximate_member_count: u32, + pub approximate_presence_count: u32, +} + +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, PartialOrd, Eq, Ord)] +pub struct GuildMemberSearchSchema { + pub query: String, + pub limit: Option, +} + +impl Default for GuildMemberSearchSchema { + fn default() -> Self { + Self { + query: Default::default(), + limit: Some(1), + } + } +} + +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, PartialOrd, Eq, Ord)] +pub struct ModifyGuildMemberSchema { + pub nick: Option, + pub roles: Option>, + pub mute: Option, + pub deaf: Option, + pub channel_id: Option, + pub communication_disabled_until: Option>, + pub flags: Option, +} + +bitflags! { + #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, PartialOrd, Ord)] + /// Represents the flags of a Guild Member. + /// + /// # Reference: + /// See + pub struct GuildMemberFlags: u64 { + const DID_REJOIN = 1 << 0; + const COMPLETED_ONBOARDING = 1 << 1; + const BYPASSES_VERIFICATION = 1 << 2; + const STARTED_ONBOARDING = 1 << 3; + const GUEST = 1 << 3; + const AUTOMOD_QUARANTINED_NAME = 1 << 7; + const AUTOMOD_QUARANTINED_BIO = 1 << 8; + } +} + +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, PartialOrd, Eq, Ord)] +pub struct ModifyCurrentGuildMemberSchema { + pub nick: Option, + pub avatar: Option, + pub bio: Option, + pub banner: Option, +} + +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, PartialOrd, Eq, Ord)] +pub struct ModifyGuildMemberProfileSchema { + pub pronouns: Option, + pub bio: Option, + pub banner: Option, + pub accent_color: Option, + pub theme_colors: Option>, + pub popout_animation_particle_type: Option, + pub emoji_id: Option, +} + +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, PartialOrd, Eq, Ord)] +/// The limit argument is a number between 1 and 1000. +pub struct GuildBansQuery { + pub before: Option, + pub after: Option, + pub limit: Option, +} diff --git a/src/types/schema/message.rs b/src/types/schema/message.rs index e1abdf7..4e34910 100644 --- a/src/types/schema/message.rs +++ b/src/types/schema/message.rs @@ -3,8 +3,9 @@ use serde::{Deserialize, Serialize}; use crate::types::entities::{ AllowedMention, Component, Embed, MessageReference, PartialDiscordFileAttachment, }; +use crate::types::{Attachment, Snowflake}; -#[derive(Debug, Default, Deserialize, Serialize)] +#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)] #[serde(rename_all = "snake_case")] pub struct MessageSendSchema { #[serde(rename = "type")] @@ -19,3 +20,107 @@ pub struct MessageSendSchema { pub sticker_ids: Option>, pub attachments: Option>, } + +#[derive(Debug)] +pub enum MessageSearchEndpoint { + GuildChannel(Snowflake), + Channel(Snowflake), +} + +impl std::fmt::Display for MessageSearchEndpoint { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + MessageSearchEndpoint::Channel(id) => { + write!(f, "channels/{}", &id.to_string()) + } + MessageSearchEndpoint::GuildChannel(id) => { + write!(f, "guilds/{}", &id.to_string()) + } + } + } +} + +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq, PartialOrd, Ord)] +/// Represents a Message Search Query JSON Body. +/// The `channel_id` field is not applicable when using the `GET /channels/{channel.id}/messages/search` endpoint. +/// +/// # Reference: +/// See +pub struct MessageSearchQuery { + pub attachment_extension: Option>, + pub attachment_filename: Option>, + pub author_id: Option>, + pub author_type: Option>, + pub channel_id: Option>, + pub command_id: Option>, + pub content: Option, + pub embed_provider: Option>, + pub embed_type: Option>, + pub has: Option>, + pub include_nsfw: Option, + pub limit: Option, + pub link_hostname: Option>, + pub max_id: Option, + pub mention_everyone: Option, + pub mentions: Option>, + pub min_id: Option, + pub offset: Option, + pub pinned: Option, + pub sort_by: Option, + pub sort_order: Option, +} + +impl std::default::Default for MessageSearchQuery { + fn default() -> Self { + Self { + attachment_extension: Default::default(), + attachment_filename: Default::default(), + author_id: Default::default(), + author_type: Default::default(), + channel_id: Default::default(), + command_id: Default::default(), + content: Default::default(), + embed_provider: Default::default(), + embed_type: Default::default(), + has: Default::default(), + include_nsfw: Some(false), + limit: Some(25), + link_hostname: Default::default(), + max_id: Default::default(), + mention_everyone: Default::default(), + mentions: Default::default(), + min_id: Default::default(), + offset: Some(0), + pinned: Default::default(), + sort_by: Default::default(), + sort_order: Default::default(), + } + } +} + +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct CreateGreetMessage { + pub sticker_ids: Vec, + pub allowed_mentions: Option, + pub message_reference: Option, +} + +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct MessageAck { + pub token: Option, + pub manual: Option, + pub mention_count: Option, +} + +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, PartialOrd)] +pub struct MessageModifySchema { + content: Option, + embeds: Option>, + embed: Option, + allowed_mentions: Option, + components: Option>, + flags: Option, + files: Option>, + payload_json: Option, + attachments: Option>, +} diff --git a/src/types/schema/role.rs b/src/types/schema/role.rs index 4ae300d..284f506 100644 --- a/src/types/schema/role.rs +++ b/src/types/schema/role.rs @@ -1,6 +1,6 @@ use serde::{Deserialize, Serialize}; -#[derive(Debug, Deserialize, Serialize)] +#[derive(Debug, Deserialize, Serialize, Clone)] #[serde(rename_all = "snake_case")] /// Represents the schema which needs to be sent to create or modify a Role. /// See: [https://docs.spacebar.chat/routes/#cmp--schemas-rolemodifyschema](https://docs.spacebar.chat/routes/#cmp--schemas-rolemodifyschema) diff --git a/src/types/schema/user.rs b/src/types/schema/user.rs index c8cf5bb..5584cf4 100644 --- a/src/types/schema/user.rs +++ b/src/types/schema/user.rs @@ -4,8 +4,9 @@ use serde::{Deserialize, Serialize}; use crate::types::Snowflake; -#[derive(Debug, Deserialize, Serialize)] +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] #[serde(rename_all = "snake_case")] +/// A schema used to modify a user. pub struct UserModifySchema { pub username: Option, pub avatar: Option, @@ -19,6 +20,8 @@ pub struct UserModifySchema { pub discriminator: Option, } +/// A schema used to create a private channel. +/// /// # Attributes: /// - recipients: The users to include in the private channel /// - access_tokens: The access tokens of users that have granted your app the `gdm.join` scope. Only usable for OAuth2 requests (which can only create group DMs). @@ -26,7 +29,7 @@ pub struct UserModifySchema { /// /// # Reference: /// Read: -#[derive(Debug, Deserialize, Serialize)] +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] pub struct PrivateChannelCreateSchema { pub recipients: Option>, pub access_tokens: Option>, diff --git a/src/types/utils/jwt.rs b/src/types/utils/jwt.rs index c9f1aa5..ca0aebb 100644 --- a/src/types/utils/jwt.rs +++ b/src/types/utils/jwt.rs @@ -1,8 +1,7 @@ +use crate::types::utils::Snowflake; use jsonwebtoken::{encode, EncodingKey, Header}; use serde::{Deserialize, Serialize}; -use crate::types::utils::Snowflake; - pub fn generate_token(id: &Snowflake, email: String, jwt_key: &str) -> String { let claims = Claims::new(&email, id); @@ -11,7 +10,9 @@ pub fn generate_token(id: &Snowflake, email: String, jwt_key: &str) -> String { #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct Claims { + /// When the token expires, unix epoch pub exp: i64, + /// When the token was issued pub iat: i64, pub email: String, pub id: String, diff --git a/src/types/utils/rights.rs b/src/types/utils/rights.rs index fecf268..5a3c373 100644 --- a/src/types/utils/rights.rs +++ b/src/types/utils/rights.rs @@ -1,56 +1,124 @@ use bitflags::bitflags; bitflags! { + /// Rights are instance-wide, per-user permissions for everything you may perform on the instance, + /// such as sending messages, editing messages, or shutting down the server. + /// They are separate from guild member permissions, which only apply to a given guild. + /// + /// # Notes + /// The default rights on Discord.com are 648540060672 ([source](https://github.com/spacebarchat/server/issues/878#issuecomment-1234669715)) + /// + /// # Reference + /// See pub struct Rights: u64 { + /// All rights const OPERATOR = 1 << 0; + /// Ability to alter or remove others' applications const MANAGE_APPLICATIONS = 1 << 1; + /// Same as the per-guild [MANAGE_GUILD] permission, but applies to all guilds and DM channels, can join any guild without invite const MANAGE_GUILDS = 1 << 2; + /// Can delete or edit any message they can read const MANAGE_MESSAGES = 1 << 3; + /// Can add, change, define rate limits of other users, + /// can also grant others [BYPASS_RATE_LIMITS] when combined + /// with [BYPASS_RATE_LIMITS] and [MANAGE_USERS]. const MANAGE_RATE_LIMITS = 1 << 4; + /// Can create, alter, enable and disable custom message routing rules in any channel/guild const MANAGE_ROUTING = 1 << 5; + /// Respond to or resolve other users' support tickets const MANAGE_TICKETS = 1 << 6; + /// Can create, alter, remove and ban users; can also create, modify and remove user groups const MANAGE_USERS = 1 << 7; + /// Can manually add members into their guilds and group DMs const ADD_MEMBERS = 1 << 8; + /// Makes the user exempt from all rate limits const BYPASS_RATE_LIMITS = 1 << 9; + /// Can create, edit and remove own applications const CREATE_APPLICATIONS = 1 << 10; + /// Can create guild channels and custom channels const CREATE_CHANNELS = 1 << 11; + /// Can create 1:1 DMs + /// + /// # Notes + /// A user without [SEND_MESSAGES] cannot be added to a DM const CREATE_DMS = 1 << 12; + /// Can create group DMs + /// + /// # Notes + /// A user without [SEND_MESSAGES] cannot be added to a DM const CREATE_DM_GROUPS = 1 << 13; + /// Can create guilds const CREATE_GUILDS = 1 << 14; + /// Can create mass invites in guilds where they have [CREATE_INSTANT_INVITE] const CREATE_INVITES = 1 << 15; + /// Can create roles and per-guild or per-channel permission + /// overrides in the guilds that they have permissions const CREATE_ROLES = 1 << 16; + /// Can create templates for guilds, custom channels and channels with custom routing const CREATE_TEMPLATES = 1 << 17; + /// Can create webhooks in the guilds that they have permissions const CREATE_WEBHOOKS = 1 << 18; + /// Can join guilds by using invites or vanity names const JOIN_GUILDS = 1 << 19; + /// Can modify the pinned messages in the guilds that they have permission const PIN_MESSAGES = 1 << 20; + /// Can react to messages, subject to permissions const SELF_ADD_REACTIONS = 1 << 21; + /// Can delete own messages const SELF_DELETE_MESSAGES = 1 << 22; + /// Can edit own messages const SELF_EDIT_MESSAGES = 1 << 23; + /// Can edit own username, nickname and avatar const SELF_EDIT_NAME = 1 << 24; + /// Can send messages in the channels that they have permissions const SEND_MESSAGES = 1 << 25; + /// Can use voice activities, such as watch together or whiteboard const USE_ACTIVITIES = 1 << 26; + /// Can use video and screenshare in guilds/channels that they have permissions const USE_VIDEO = 1 << 27; + /// Can use voice in guilds/channels that they have permissions const USE_VOICE = 1 << 28; + /// Can create user-specific invites in guilds that they have the [`INVITE_USERS`] right in. const INVITE_USERS = 1 << 29; + /// Can delete/disable own account const SELF_DELETE_DISABLE = 1 << 30; + /// Can use pay-to-use features once paid const DEBTABLE = 1 << 31; + /// Can earn money using monetization features in guilds that have [`MonetizationEnabled`](crate::types::types::guild_configuration::GuildFeatures::MonetizationEnabled) const CREDITABLE = 1 << 32; + /// Can kick or ban guild or group DM members in the guilds/groups where they have [`KICK_MEMBERS`](crate::types::PermissionFlags::KICK_MEMBERS) or [`BAN_MEMBERS`](crate::types::PermissionFlags::BAN_MEMBERS) const KICK_BAN_MEMBERS = 1 << 33; + /// Can leave the guilds or group DMs that they joined on their own (one can always leave a guild or group DMs where they have been force-added) const SELF_LEAVE_GROUPS = 1 << 34; + /// Inverts the presence confidentiality default ([`OPERATOR`]'s presence is not routed by default, others' are) for a given user const PRESENCE = 1 << 35; + /// Can mark discoverable guilds where they have permissions to mark as discoverable const SELF_ADD_DISCOVERABLE = 1 << 36; + /// Can change anything in the primary guild directory const MANAGE_GUILD_DIRECTORY = 1 << 37; + /// Can send confetti, screenshake and use the random user mention (@someone) const POGGERS = 1 << 38; + /// Can use achievements and cheers const USE_ACHIEVEMENTS = 1 << 39; + /// Can initiate interactions const INITIATE_INTERACTIONS = 1 << 40; + /// Can respond to interactions const RESPOND_TO_INTERACTIONS = 1 << 41; + /// Can send backdated events const SEND_BACKDATED_EVENTS = 1 << 42; + /// Can accept mass (guild) invites const USE_MASS_INVITES = 1 << 43; + /// Can accept user-specific invites and DM requests const ACCEPT_INVITES = 1 << 44; + /// Can modify own flags const SELF_EDIT_FLAGS = 1 << 45; + /// Can modify other's flags const EDIT_FLAGS = 1 << 46; + /// Can manage other's groups const MANAGE_GROUPS = 1 << 47; + /// Can view server stats at /api/policies/stats const VIEW_SERVER_STATS = 1 << 48; + /// Can resend verification emails using /auth/verify/resend const RESEND_VERIFICATION_EMAIL = 1 << 49; } } @@ -60,10 +128,16 @@ impl Rights { (check_operator && self.contains(Rights::OPERATOR)) || self.contains(permission) } + /// Returns whether or not the Rights object has specific rights pub fn has(&self, permission: Rights, check_operator: bool) -> bool { (check_operator && self.contains(Rights::OPERATOR)) || self.contains(permission) } + /// Returns whether or not the Rights object has specific rights. + /// + /// # Notes + /// Unlike has, this returns an Error if we are missing rights + /// and Ok(true) otherwise pub fn has_throw(&self, permission: Rights) -> Result { if self.has(permission, true) { Ok(true) diff --git a/src/types/utils/snowflake.rs b/src/types/utils/snowflake.rs index 77514a1..b4e1d9e 100644 --- a/src/types/utils/snowflake.rs +++ b/src/types/utils/snowflake.rs @@ -11,13 +11,16 @@ use sqlx::Type; const EPOCH: i64 = 1420070400000; /// Unique identifier including a timestamp. -/// See https://discord.com/developers/docs/reference#snowflakes +/// +/// # Reference +/// See #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] #[cfg_attr(feature = "sqlx", derive(Type))] #[cfg_attr(feature = "sqlx", sqlx(transparent))] -pub struct Snowflake(u64); +pub struct Snowflake(pub u64); impl Snowflake { + /// Generates a snowflake for the current timestamp, with worker id 0 and process id 1. pub fn generate() -> Self { const WORKER_ID: u64 = 0; const PROCESS_ID: u64 = 1; @@ -31,6 +34,7 @@ impl Snowflake { Self(time as u64 | worker | process | increment) } + /// Returns the snowflake's timestamp pub fn timestamp(self) -> DateTime { Utc.timestamp_millis_opt((self.0 >> 22) as i64 + EPOCH) .unwrap() @@ -49,6 +53,15 @@ impl Display for Snowflake { } } +impl From for Snowflake +where + T: Into, +{ + fn from(item: T) -> Self { + Self(item.into()) + } +} + impl serde::Serialize for Snowflake { fn serialize(&self, serializer: S) -> Result where diff --git a/src/voice.rs b/src/voice.rs index 1af8747..14e883d 100644 --- a/src/voice.rs +++ b/src/voice.rs @@ -47,24 +47,20 @@ impl VoiceGatewayMesssage { let processed_content = content.to_lowercase().replace('.', ""); match processed_content.as_str() { - "unknown opcode" | "4001" => Some(VoiceGatewayError::UnknownOpcodeError), + "unknown opcode" | "4001" => Some(VoiceGatewayError::UnknownOpcode), "decode error" | "failed to decode payload" | "4002" => { - Some(VoiceGatewayError::FailedToDecodePayloadError) - } - "not authenticated" | "4003" => Some(VoiceGatewayError::NotAuthenticatedError), - "authentication failed" | "4004" => Some(VoiceGatewayError::AuthenticationFailedError), - "already authenticated" | "4005" => Some(VoiceGatewayError::AlreadyAuthenticatedError), - "session no longer valid" | "4006" => { - Some(VoiceGatewayError::SessionNoLongerValidError) - } - "session timeout" | "4009" => Some(VoiceGatewayError::SessionTimeoutError), - "server not found" | "4011" => Some(VoiceGatewayError::ServerNotFoundError), - "unknown protocol" | "4012" => Some(VoiceGatewayError::UnknownProtocolError), - "disconnected" | "4014" => Some(VoiceGatewayError::DisconnectedError), - "voice server crashed" | "4015" => Some(VoiceGatewayError::VoiceServerCrashedError), - "unknown encryption mode" | "4016" => { - Some(VoiceGatewayError::UnknownEncryptionModeError) + Some(VoiceGatewayError::FailedToDecodePayload) } + "not authenticated" | "4003" => Some(VoiceGatewayError::NotAuthenticated), + "authentication failed" | "4004" => Some(VoiceGatewayError::AuthenticationFailed), + "already authenticated" | "4005" => Some(VoiceGatewayError::AlreadyAuthenticated), + "session no longer valid" | "4006" => Some(VoiceGatewayError::SessionNoLongerValid), + "session timeout" | "4009" => Some(VoiceGatewayError::SessionTimeout), + "server not found" | "4011" => Some(VoiceGatewayError::ServerNotFound), + "unknown protocol" | "4012" => Some(VoiceGatewayError::UnknownProtocol), + "disconnected" | "4014" => Some(VoiceGatewayError::Disconnected), + "voice server crashed" | "4015" => Some(VoiceGatewayError::VoiceServerCrashed), + "unknown encryption mode" | "4016" => Some(VoiceGatewayError::UnknownEncryptionMode), _ => None, } } @@ -180,6 +176,8 @@ impl VoiceGatewayHandle { self.websocket_send.lock().await.close().await.unwrap(); } } + +#[derive(Debug)] pub struct VoiceGateway { events: Arc>, heartbeat_handler: VoiceHeartbeatHandler, @@ -409,6 +407,7 @@ impl VoiceGateway { /// Handles sending heartbeats to the voice gateway in another thread #[allow(dead_code)] // FIXME: Remove this, once all fields of VoiceHeartbeatHandler are used +#[derive(Debug)] struct VoiceHeartbeatHandler { /// The heartbeat interval in milliseconds pub heartbeat_interval: Duration, diff --git a/tests/auth.rs b/tests/auth.rs index c26552f..f89e5e4 100644 --- a/tests/auth.rs +++ b/tests/auth.rs @@ -4,13 +4,13 @@ mod common; #[tokio::test] async fn test_registration() { - let mut bundle = common::setup().await; + let bundle = common::setup().await; let reg = RegisterSchema { username: "Hiiii".into(), date_of_birth: Some("2000-01-01".to_string()), consent: true, ..Default::default() }; - bundle.instance.register_account(®).await.unwrap(); + bundle.instance.clone().register_account(reg).await.unwrap(); common::teardown(bundle).await; } diff --git a/tests/channels.rs b/tests/channels.rs index c8564d7..1647652 100644 --- a/tests/channels.rs +++ b/tests/channels.rs @@ -8,7 +8,7 @@ mod common; #[tokio::test] async fn get_channel() { let mut bundle = common::setup().await; - let bundle_channel = bundle.channel.clone(); + let bundle_channel = bundle.channel.read().unwrap().clone(); let bundle_user = &mut bundle.user; assert_eq!( @@ -21,7 +21,8 @@ async fn get_channel() { #[tokio::test] async fn delete_channel() { let mut bundle = common::setup().await; - let result = Channel::delete(bundle.channel.clone(), &mut bundle.user).await; + let channel_guard = bundle.channel.write().unwrap().clone(); + let result = Channel::delete(channel_guard, None, &mut bundle.user).await; assert!(result.is_ok()); common::teardown(bundle).await } @@ -30,7 +31,7 @@ async fn delete_channel() { async fn modify_channel() { const CHANNEL_NAME: &str = "beepboop"; let mut bundle = common::setup().await; - let channel = &mut bundle.channel; + let channel = &mut bundle.channel.read().unwrap().clone(); let modify_data: types::ChannelModifySchema = types::ChannelModifySchema { name: Some(CHANNEL_NAME.to_string()), channel_type: None, @@ -50,7 +51,8 @@ async fn modify_channel() { default_thread_rate_limit_per_user: None, video_quality_mode: None, }; - let modified_channel = Channel::modify(channel, modify_data, channel.id, &mut bundle.user) + let modified_channel = channel + .modify(modify_data, None, &mut bundle.user) .await .unwrap(); assert_eq!(modified_channel.name, Some(CHANNEL_NAME.to_string())); @@ -59,22 +61,24 @@ async fn modify_channel() { PermissionFlags::MANAGE_CHANNELS, PermissionFlags::MANAGE_MESSAGES, ])); + let user_id: types::Snowflake = bundle.user.object.read().unwrap().id; let permission_override = PermissionOverwrite { - id: bundle.user.object.id, + id: user_id, overwrite_type: "1".to_string(), allow: permission_override, deny: "0".to_string(), }; - - Channel::edit_permissions( + let channel_id: Snowflake = bundle.channel.read().unwrap().id; + Channel::modify_permissions( &mut bundle.user, - bundle.channel.id, + channel_id, + None, permission_override.clone(), ) .await .unwrap(); - Channel::delete_permission(&mut bundle.user, bundle.channel.id, permission_override.id) + Channel::delete_permission(&mut bundle.user, channel_id, permission_override.id) .await .unwrap(); @@ -84,7 +88,7 @@ async fn modify_channel() { #[tokio::test] async fn get_channel_messages() { let mut bundle = common::setup().await; - + let channel_id: Snowflake = bundle.channel.read().unwrap().id; // First create some messages to read for _ in 0..10 { let _ = bundle @@ -94,7 +98,7 @@ async fn get_channel_messages() { content: Some("A Message!".to_string()), ..Default::default() }, - bundle.channel.id, + channel_id, ) .await .unwrap(); @@ -103,7 +107,7 @@ async fn get_channel_messages() { assert_eq!( Channel::messages( GetChannelMessagesSchema::before(Snowflake::generate()), - bundle.channel.id, + channel_id, &mut bundle.user, ) .await @@ -127,7 +131,7 @@ async fn get_channel_messages() { assert!(Channel::messages( GetChannelMessagesSchema::after(Snowflake::generate()), - bundle.channel.id, + channel_id, &mut bundle.user, ) .await @@ -143,7 +147,7 @@ async fn create_dm() { let other_user = bundle.create_user("integrationtestuser2").await; let user = &mut bundle.user; let private_channel_create_schema = PrivateChannelCreateSchema { - recipients: Some(Vec::from([other_user.object.id])), + recipients: Some(Vec::from([other_user.object.read().unwrap().id])), access_tokens: None, nicks: None, }; @@ -153,26 +157,47 @@ async fn create_dm() { .unwrap(); assert!(dm_channel.recipients.is_some()); assert_eq!( - dm_channel.recipients.as_ref().unwrap().get(0).unwrap().id, - other_user.object.id + dm_channel + .recipients + .as_ref() + .unwrap() + .get(0) + .unwrap() + .read() + .unwrap() + .id + .clone(), + other_user.object.read().unwrap().id ); assert_eq!( - dm_channel.recipients.as_ref().unwrap().get(1).unwrap().id, - user.object.id + dm_channel + .recipients + .as_ref() + .unwrap() + .get(1) + .unwrap() + .read() + .unwrap() + .id + .clone(), + user.object.read().unwrap().id.clone() ); common::teardown(bundle).await; } // #[tokio::test] -// This test currently is broken due to an issue with the Spacebar Server. +// TODO This test currently is broken due to an issue with the Spacebar Server. #[allow(dead_code)] async fn remove_add_person_from_to_dm() { let mut bundle = common::setup().await; let mut other_user = bundle.create_user("integrationtestuser2").await; let mut third_user = bundle.create_user("integrationtestuser3").await; + let third_user_id = third_user.object.read().unwrap().id; + let other_user_id = other_user.object.read().unwrap().id; + let user_id = bundle.user.object.read().unwrap().id; let user = &mut bundle.user; let private_channel_create_schema = PrivateChannelCreateSchema { - recipients: Some(Vec::from([other_user.object.id, third_user.object.id])), + recipients: Some(Vec::from([other_user_id, third_user_id])), access_tokens: None, nicks: None, }; @@ -181,36 +206,52 @@ async fn remove_add_person_from_to_dm() { .await .unwrap(); // Creates the Channel and stores the response Channel object dm_channel - .remove_channel_recipient(other_user.object.id, user) + .remove_channel_recipient(other_user_id, user) .await .unwrap(); assert!(dm_channel.recipients.as_ref().unwrap().get(1).is_none()); other_user - .modify_user_relationship(user.object.id, RelationshipType::Friends) + .modify_user_relationship(user_id, RelationshipType::Friends) .await .unwrap(); - user.modify_user_relationship(other_user.object.id, RelationshipType::Friends) + user.modify_user_relationship(other_user_id, RelationshipType::Friends) .await .unwrap(); third_user - .modify_user_relationship(user.object.id, RelationshipType::Friends) + .modify_user_relationship(user_id, RelationshipType::Friends) .await .unwrap(); - user.modify_user_relationship(third_user.object.id, RelationshipType::Friends) + user.modify_user_relationship(third_user_id, RelationshipType::Friends) .await .unwrap(); // Users 1-2 and 1-3 are now friends dm_channel - .add_channel_recipient(other_user.object.id, user, None) + .add_channel_recipient(other_user_id, user, None) .await .unwrap(); assert!(dm_channel.recipients.is_some()); assert_eq!( - dm_channel.recipients.as_ref().unwrap().get(0).unwrap().id, - other_user.object.id + dm_channel + .recipients + .as_ref() + .unwrap() + .get(0) + .unwrap() + .read() + .unwrap() + .id, + other_user_id ); assert_eq!( - dm_channel.recipients.as_ref().unwrap().get(1).unwrap().id, - user.object.id + dm_channel + .recipients + .as_ref() + .unwrap() + .get(1) + .unwrap() + .read() + .unwrap() + .id, + user_id ); } diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 747a8cd..ce42578 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -1,6 +1,8 @@ +use std::sync::{Arc, RwLock}; + use chorus::gateway::Gateway; use chorus::{ - instance::{Instance, UserMeta}, + instance::{ChorusUser, Instance}, types::{ Channel, ChannelCreateSchema, Guild, GuildCreateSchema, RegisterSchema, RoleCreateModifySchema, RoleObject, @@ -12,16 +14,16 @@ use chorus::{ #[derive(Debug)] pub(crate) struct TestBundle { pub urls: UrlBundle, - pub user: UserMeta, + pub user: ChorusUser, pub instance: Instance, - pub guild: Guild, - pub role: RoleObject, - pub channel: Channel, + pub guild: Arc>, + pub role: Arc>, + pub channel: Arc>, } #[allow(unused)] impl TestBundle { - pub(crate) async fn create_user(&mut self, username: &str) -> UserMeta { + pub(crate) async fn create_user(&mut self, username: &str) -> ChorusUser { let register_schema = RegisterSchema { username: username.to_string(), consent: true, @@ -29,12 +31,13 @@ impl TestBundle { ..Default::default() }; self.instance - .register_account(®ister_schema) + .clone() + .register_account(register_schema) .await .unwrap() } - pub(crate) async fn clone_user_without_gateway(&self) -> UserMeta { - UserMeta { + pub(crate) async fn clone_user_without_gateway(&self) -> ChorusUser { + ChorusUser { belongs_to: self.user.belongs_to.clone(), token: self.user.token.clone(), limits: self.user.limits.clone(), @@ -52,7 +55,7 @@ pub(crate) async fn setup() -> TestBundle { "ws://localhost:3001".to_string(), "http://localhost:3001".to_string(), ); - let mut instance = Instance::new(urls.clone(), true).await.unwrap(); + let instance = Instance::new(urls.clone(), true).await.unwrap(); // Requires the existance of the below user. let reg = RegisterSchema { username: "integrationtestuser".into(), @@ -71,7 +74,7 @@ pub(crate) async fn setup() -> TestBundle { }; let channel_create_schema = ChannelCreateSchema { name: "testchannel".to_string(), - channel_type: Some(0), + channel_type: Some(chorus::types::ChannelType::GuildText), topic: None, icon: None, bitrate: None, @@ -89,9 +92,9 @@ pub(crate) async fn setup() -> TestBundle { default_thread_rate_limit_per_user: Some(0), video_quality_mode: None, }; - let mut user = instance.register_account(®).await.unwrap(); + let mut user = instance.clone().register_account(reg).await.unwrap(); let guild = Guild::create(&mut user, guild_create_schema).await.unwrap(); - let channel = Channel::create(&mut user, guild.id, channel_create_schema) + let channel = Channel::create(&mut user, guild.id, None, channel_create_schema) .await .unwrap(); @@ -113,17 +116,16 @@ pub(crate) async fn setup() -> TestBundle { urls, user, instance, - guild, - role, - channel, + guild: Arc::new(RwLock::new(guild)), + role: Arc::new(RwLock::new(role)), + channel: Arc::new(RwLock::new(channel)), } } // Teardown method to clean up after a test. #[allow(dead_code)] pub(crate) async fn teardown(mut bundle: TestBundle) { - Guild::delete(&mut bundle.user, bundle.guild.id) - .await - .unwrap(); + let id = bundle.guild.read().unwrap().id; + Guild::delete(&mut bundle.user, id).await.unwrap(); bundle.user.delete().await.unwrap() } diff --git a/tests/gateway.rs b/tests/gateway.rs index 21a2018..991c9f2 100644 --- a/tests/gateway.rs +++ b/tests/gateway.rs @@ -1,7 +1,9 @@ mod common; +use std::sync::{Arc, RwLock}; + use chorus::gateway::*; -use chorus::types::{self, Channel}; +use chorus::types::{self, ChannelModifySchema, RoleCreateModifySchema, RoleObject}; #[tokio::test] /// Tests establishing a connection (hello and heartbeats) on the local gateway; @@ -29,18 +31,88 @@ async fn test_gateway_authenticate() { #[tokio::test] async fn test_self_updating_structs() { let mut bundle = common::setup().await; - let channel_updater = bundle.user.gateway.observe(bundle.channel.clone()).await; - let received_channel = channel_updater.borrow().clone(); - assert_eq!(received_channel, bundle.channel); - let channel = &mut bundle.channel; - let modify_data = types::ChannelModifySchema { - name: Some("beepboop".to_string()), + let received_channel = bundle + .user + .gateway + .observe_and_into_inner(bundle.channel.clone()) + .await; + + assert_eq!(received_channel, bundle.channel.read().unwrap().clone()); + + let modify_schema = ChannelModifySchema { + name: Some("selfupdating".to_string()), ..Default::default() }; - Channel::modify(channel, modify_data, channel.id, &mut bundle.user) + received_channel + .modify(modify_schema, None, &mut bundle.user) .await .unwrap(); - let received_channel = channel_updater.borrow(); - assert_eq!(received_channel.name.as_ref().unwrap(), "beepboop"); + assert_eq!( + bundle + .user + .gateway + .observe_and_into_inner(bundle.channel.clone()) + .await + .name + .unwrap(), + "selfupdating".to_string() + ); + common::teardown(bundle).await } + +#[tokio::test] +async fn test_recursive_self_updating_structs() { + // Setup + let mut bundle = common::setup().await; + let guild = bundle.guild.clone(); + // Observe Guild, make sure it has no channels + let guild = bundle.user.gateway.observe(guild.clone()).await; + let inner_guild = guild.read().unwrap().clone(); + assert!(inner_guild.roles.is_none()); + // Create Role + let permissions = types::PermissionFlags::CONNECT | types::PermissionFlags::MANAGE_EVENTS; + let permissions = Some(permissions.to_string()); + let mut role_create_schema: types::RoleCreateModifySchema = RoleCreateModifySchema { + name: Some("cool person".to_string()), + permissions, + hoist: Some(true), + icon: None, + unicode_emoji: Some("".to_string()), + mentionable: Some(true), + position: None, + color: None, + }; + let guild_id = inner_guild.id; + let role = RoleObject::create(&mut bundle.user, guild_id, role_create_schema.clone()) + .await + .unwrap(); + // Watch role; + bundle + .user + .gateway + .observe(Arc::new(RwLock::new(role.clone()))) + .await; + // Update Guild and check for Guild + let inner_guild = guild.read().unwrap().clone(); + assert!(inner_guild.roles.is_some()); + // Update the Role + role_create_schema.name = Some("yippieee".to_string()); + RoleObject::modify(&mut bundle.user, guild_id, role.id, role_create_schema) + .await + .unwrap(); + let role_inner = bundle + .user + .gateway + .observe_and_into_inner(Arc::new(RwLock::new(role.clone()))) + .await; + assert_eq!(role_inner.name, "yippieee"); + // Check if the change propagated + let guild = bundle.user.gateway.observe(bundle.guild.clone()).await; + let inner_guild = guild.read().unwrap().clone(); + let guild_roles = inner_guild.roles; + let guild_role = guild_roles.unwrap(); + let guild_role_inner = guild_role.get(0).unwrap().read().unwrap().clone(); + assert_eq!(guild_role_inner.name, "yippieee".to_string()); + common::teardown(bundle).await; +} diff --git a/tests/guilds.rs b/tests/guilds.rs index cc33b30..d7e2699 100644 --- a/tests/guilds.rs +++ b/tests/guilds.rs @@ -1,4 +1,6 @@ -use chorus::types::{Guild, GuildCreateSchema}; +use chorus::types::{ + CreateChannelInviteSchema, Guild, GuildBanCreateSchema, GuildCreateSchema, GuildModifySchema, +}; mod common; @@ -27,9 +29,84 @@ async fn guild_creation_deletion() { #[tokio::test] async fn get_channels() { let mut bundle = common::setup().await; - println!( - "{:?}", - bundle.guild.channels(&mut bundle.user).await.unwrap() - ); + let guild = bundle.guild.read().unwrap().clone(); + println!("{:?}", guild.channels(&mut bundle.user).await.unwrap()); common::teardown(bundle).await; } + +#[tokio::test] +async fn guild_create_ban() { + // TODO: When routes exist to check if user x is on guild y, add this as an assertion to check + // if Spacebar actually bans the user. + let mut bundle = common::setup().await; + let channel = bundle.channel.read().unwrap().clone(); + let mut other_user = bundle.create_user("testuser1312").await; + let user = &mut bundle.user; + let create_channel_invite_schema = CreateChannelInviteSchema::default(); + let guild = bundle.guild.read().unwrap().clone(); + let invite = user + .create_channel_invite(create_channel_invite_schema, channel.id) + .await + .unwrap(); + other_user.accept_invite(&invite.code, None).await.unwrap(); + let other_user_id = other_user.object.read().unwrap().id; + Guild::create_ban( + guild.id, + other_user_id, + None, + GuildBanCreateSchema::default(), + &mut bundle.user, + ) + .await + .unwrap(); + assert!(Guild::create_ban( + guild.id, + other_user_id, + None, + GuildBanCreateSchema::default(), + &mut bundle.user, + ) + .await + .is_err()); + common::teardown(bundle).await +} + +#[tokio::test] +async fn modify_guild() { + let mut bundle = common::setup().await; + let schema = GuildModifySchema { + name: Some("Mycoolguild".to_string()), + ..Default::default() + }; + let guild_id = bundle.guild.read().unwrap().id; + let result = Guild::modify(guild_id, schema, &mut bundle.user) + .await + .unwrap(); + assert_eq!(result.name.unwrap(), "Mycoolguild".to_string()); + common::teardown(bundle).await +} + +#[tokio::test] +async fn guild_remove_member() { + let mut bundle = common::setup().await; + let channel = bundle.channel.read().unwrap().clone(); + let mut other_user = bundle.create_user("testuser1312").await; + let user = &mut bundle.user; + let create_channel_invite_schema = CreateChannelInviteSchema::default(); + let guild = bundle.guild.read().unwrap().clone(); + let invite = user + .create_channel_invite(create_channel_invite_schema, channel.id) + .await + .unwrap(); + other_user.accept_invite(&invite.code, None).await.unwrap(); + let other_user_id = other_user.object.read().unwrap().id; + Guild::remove_member(guild.id, other_user_id, None, &mut bundle.user) + .await + .unwrap(); + assert!( + Guild::remove_member(guild.id, other_user_id, None, &mut bundle.user,) + .await + .is_err() + ); + common::teardown(bundle).await +} diff --git a/tests/invites.rs b/tests/invites.rs index d19be61..ab264d4 100644 --- a/tests/invites.rs +++ b/tests/invites.rs @@ -3,20 +3,21 @@ use chorus::types::CreateChannelInviteSchema; #[tokio::test] async fn create_accept_invite() { let mut bundle = common::setup().await; - let channel = bundle.channel.clone(); + let channel = bundle.channel.read().unwrap().clone(); let mut other_user = bundle.create_user("testuser1312").await; let user = &mut bundle.user; let create_channel_invite_schema = CreateChannelInviteSchema::default(); - assert!(chorus::types::Guild::get(bundle.guild.id, &mut other_user) + let guild = bundle.guild.read().unwrap().clone(); + assert!(chorus::types::Guild::get(guild.id, &mut other_user) .await .is_err()); let invite = user - .create_guild_invite(create_channel_invite_schema, channel.id) + .create_channel_invite(create_channel_invite_schema, channel.id) .await .unwrap(); other_user.accept_invite(&invite.code, None).await.unwrap(); - assert!(chorus::types::Guild::get(bundle.guild.id, &mut other_user) + assert!(chorus::types::Guild::get(guild.id, &mut other_user) .await .is_ok()); common::teardown(bundle).await; diff --git a/tests/members.rs b/tests/members.rs index 9710d7f..fbab772 100644 --- a/tests/members.rs +++ b/tests/members.rs @@ -5,9 +5,9 @@ mod common; #[tokio::test] async fn add_remove_role() -> ChorusResult<()> { let mut bundle = common::setup().await; - let guild = bundle.guild.id; - let role = bundle.role.id; - let member_id = bundle.user.object.id; + let guild = bundle.guild.read().unwrap().id; + let role = bundle.role.read().unwrap().id; + let member_id = bundle.user.object.read().unwrap().id; GuildMember::add_role(&mut bundle.user, guild, member_id, role).await?; let member = GuildMember::get(&mut bundle.user, guild, member_id) .await diff --git a/tests/messages.rs b/tests/messages.rs index af6ee5b..5ad9a89 100644 --- a/tests/messages.rs +++ b/tests/messages.rs @@ -1,7 +1,7 @@ use std::fs::File; use std::io::{BufReader, Read}; -use chorus::types; +use chorus::types::{self, Guild, Message, MessageSearchQuery}; mod common; @@ -12,11 +12,8 @@ async fn send_message() { content: Some("A Message!".to_string()), ..Default::default() }; - let _ = bundle - .user - .send_message(message, bundle.channel.id) - .await - .unwrap(); + let channel = bundle.channel.read().unwrap().clone(); + let _ = bundle.user.send_message(message, channel.id).await.unwrap(); common::teardown(bundle).await } @@ -50,13 +47,94 @@ async fn send_message_attachment() { attachments: Some(vec![attachment.clone()]), ..Default::default() }; - + let channel = bundle.channel.read().unwrap().clone(); let vec_attach = vec![attachment.clone()]; let _arg = Some(&vec_attach); - bundle - .user - .send_message(message, bundle.channel.id) - .await - .unwrap(); + bundle.user.send_message(message, channel.id).await.unwrap(); + common::teardown(bundle).await +} + +#[tokio::test] +async fn search_messages() { + let f = File::open("./README.md").unwrap(); + let mut reader = BufReader::new(f); + let mut buffer = Vec::new(); + let mut bundle = common::setup().await; + + reader.read_to_end(&mut buffer).unwrap(); + + let attachment = types::PartialDiscordFileAttachment { + id: None, + filename: "README.md".to_string(), + description: None, + content_type: None, + size: None, + url: None, + proxy_url: None, + width: None, + height: None, + ephemeral: None, + duration_secs: None, + waveform: None, + content: buffer, + }; + + let message = types::MessageSendSchema { + content: Some("trans rights now".to_string()), + attachments: Some(vec![attachment.clone()]), + ..Default::default() + }; + let channel = bundle.channel.read().unwrap().clone(); + let vec_attach = vec![attachment.clone()]; + let _arg = Some(&vec_attach); + let message = bundle.user.send_message(message, channel.id).await.unwrap(); + let query = MessageSearchQuery { + author_id: Some(Vec::from([bundle.user.object.read().unwrap().id])), + ..Default::default() + }; + let guild_id = bundle.guild.read().unwrap().id; + let query_result = Guild::search_messages(guild_id, query, &mut bundle.user) + .await + .unwrap(); + assert!(!query_result.is_empty()); + assert_eq!(query_result.get(0).unwrap().id, message.id); +} + +#[tokio::test] +async fn test_stickies() { + let mut bundle = common::setup().await; + let message = types::MessageSendSchema { + content: Some("A Message!".to_string()), + ..Default::default() + }; + let channel = bundle.channel.read().unwrap().clone(); + let message = bundle.user.send_message(message, channel.id).await.unwrap(); + assert_eq!( + Message::get_sticky(channel.id, &mut bundle.user) + .await + .unwrap(), + Vec::::new() + ); + Message::sticky(channel.id, message.id, None, &mut bundle.user) + .await + .unwrap(); + assert_eq!( + Message::get_sticky(channel.id, &mut bundle.user) + .await + .unwrap() + .get(0) + .unwrap() + .id, + message.id + ); + Message::unsticky(channel.id, message.id, None, &mut bundle.user) + .await + .unwrap(); + assert_eq!( + Message::get_sticky(channel.id, &mut bundle.user) + .await + .unwrap(), + Vec::::new() + ); common::teardown(bundle).await } diff --git a/tests/relationships.rs b/tests/relationships.rs index 00ef9cf..09ddab0 100644 --- a/tests/relationships.rs +++ b/tests/relationships.rs @@ -7,15 +7,18 @@ async fn test_get_mutual_relationships() { let mut bundle = common::setup().await; let mut other_user = bundle.create_user("integrationtestuser2").await; let user = &mut bundle.user; + let username = user.object.read().unwrap().username.clone(); + let discriminator = user.object.read().unwrap().discriminator.clone(); + let other_user_id: types::Snowflake = other_user.object.read().unwrap().id; let friend_request_schema = types::FriendRequestSendSchema { - username: user.object.username.clone(), - discriminator: Some(user.object.discriminator.clone()), + username, + discriminator: Some(discriminator), }; - let _ = other_user.send_friend_request(friend_request_schema).await; - let relationships = user - .get_mutual_relationships(other_user.object.id) + other_user + .send_friend_request(friend_request_schema) .await .unwrap(); + let relationships = user.get_mutual_relationships(other_user_id).await.unwrap(); println!("{:?}", relationships); common::teardown(bundle).await } @@ -25,16 +28,21 @@ async fn test_get_relationships() { let mut bundle = common::setup().await; let mut other_user = bundle.create_user("integrationtestuser2").await; let user = &mut bundle.user; + let username = user.object.read().unwrap().username.clone(); + let discriminator = user.object.read().unwrap().discriminator.clone(); let friend_request_schema = types::FriendRequestSendSchema { - username: user.object.username.clone(), - discriminator: Some(user.object.discriminator.clone()), + username, + discriminator: Some(discriminator), }; other_user .send_friend_request(friend_request_schema) .await .unwrap(); let relationships = user.get_relationships().await.unwrap(); - assert_eq!(relationships.get(0).unwrap().id, other_user.object.id); + assert_eq!( + relationships.get(0).unwrap().id, + other_user.object.read().unwrap().id + ); common::teardown(bundle).await } @@ -43,23 +51,33 @@ async fn test_modify_relationship_friends() { let mut bundle = common::setup().await; let mut other_user = bundle.create_user("integrationtestuser2").await; let user = &mut bundle.user; - let _ = other_user - .modify_user_relationship(user.object.id, types::RelationshipType::Friends) - .await; + let user_id: types::Snowflake = user.object.read().unwrap().id; + let other_user_id: types::Snowflake = other_user.object.read().unwrap().id; + + other_user + .modify_user_relationship(user_id, types::RelationshipType::Friends) + .await + .unwrap(); let relationships = user.get_relationships().await.unwrap(); - assert_eq!(relationships.get(0).unwrap().id, other_user.object.id); + assert_eq!( + relationships.get(0).unwrap().id, + other_user.object.read().unwrap().id + ); assert_eq!( relationships.get(0).unwrap().relationship_type, RelationshipType::Incoming ); let relationships = other_user.get_relationships().await.unwrap(); - assert_eq!(relationships.get(0).unwrap().id, user.object.id); + assert_eq!( + relationships.get(0).unwrap().id, + user.object.read().unwrap().id + ); assert_eq!( relationships.get(0).unwrap().relationship_type, RelationshipType::Outgoing ); let _ = user - .modify_user_relationship(other_user.object.id, RelationshipType::Friends) + .modify_user_relationship(other_user_id, RelationshipType::Friends) .await; assert_eq!( other_user @@ -71,7 +89,7 @@ async fn test_modify_relationship_friends() { .relationship_type, RelationshipType::Friends ); - let _ = user.remove_relationship(other_user.object.id).await; + let _ = user.remove_relationship(other_user_id).await; assert_eq!( other_user.get_relationships().await.unwrap(), Vec::::new() @@ -84,18 +102,24 @@ async fn test_modify_relationship_block() { let mut bundle = common::setup().await; let mut other_user = bundle.create_user("integrationtestuser2").await; let user = &mut bundle.user; - let _ = other_user - .modify_user_relationship(user.object.id, types::RelationshipType::Blocked) - .await; + let user_id: types::Snowflake = user.object.read().unwrap().id; + + other_user + .modify_user_relationship(user_id, types::RelationshipType::Blocked) + .await + .unwrap(); let relationships = user.get_relationships().await.unwrap(); assert_eq!(relationships, Vec::::new()); let relationships = other_user.get_relationships().await.unwrap(); - assert_eq!(relationships.get(0).unwrap().id, user.object.id); + assert_eq!( + relationships.get(0).unwrap().id, + user.object.read().unwrap().id + ); assert_eq!( relationships.get(0).unwrap().relationship_type, RelationshipType::Blocked ); - let _ = other_user.remove_relationship(user.object.id).await; + other_user.remove_relationship(user_id).await.unwrap(); assert_eq!( other_user.get_relationships().await.unwrap(), Vec::::new() diff --git a/tests/roles.rs b/tests/roles.rs index 1675367..8691138 100644 --- a/tests/roles.rs +++ b/tests/roles.rs @@ -1,4 +1,4 @@ -use chorus::types::{self, RoleCreateModifySchema}; +use chorus::types::{self, RoleCreateModifySchema, RoleObject}; mod common; @@ -17,14 +17,13 @@ async fn create_and_get_roles() { position: None, color: None, }; - let guild = bundle.guild.id; - let role = types::RoleObject::create(&mut bundle.user, guild, role_create_schema) + let guild_id = bundle.guild.read().unwrap().id; + let role = types::RoleObject::create(&mut bundle.user, guild_id, role_create_schema) .await .unwrap(); - let expected = types::RoleObject::get_all(&mut bundle.user, guild) + let expected = types::RoleObject::get_all(&mut bundle.user, guild_id) .await - .unwrap() .unwrap()[2] .clone(); @@ -33,14 +32,31 @@ async fn create_and_get_roles() { } #[tokio::test] -async fn get_singular_role() { +async fn get_and_delete_role() { let mut bundle = common::setup().await; - let guild_id = bundle.guild.id; - let role_id = bundle.role.id; - let role = bundle.role.clone(); + let guild_id = bundle.guild.read().unwrap().id; + let role_id = bundle.role.read().unwrap().id; + let role = bundle.role.read().unwrap().clone(); let same_role = chorus::types::RoleObject::get(&mut bundle.user, guild_id, role_id) .await .unwrap(); assert_eq!(role, same_role); + assert_eq!( + chorus::types::RoleObject::get_all(&mut bundle.user, guild_id) + .await + .unwrap() + .len(), + 2 + ); + RoleObject::delete_role(&mut bundle.user, guild_id, role_id, None) + .await + .unwrap(); + assert_eq!( + chorus::types::RoleObject::get_all(&mut bundle.user, guild_id) + .await + .unwrap() + .len(), + 1 + ); common::teardown(bundle).await }