From 2f559b139779aff3bc0161e9213a43c041cc8263 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Sun, 12 Nov 2023 17:20:32 +0100 Subject: [PATCH 01/16] Remove openssl, use rustls and rand instead --- Cargo.lock | 102 ++++++++++++++++-- Cargo.toml | 9 +- src/gateway.rs | 14 ++- .../config/types/security_configuration.rs | 10 +- 4 files changed, 120 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index df89f42..c35635e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -192,10 +192,12 @@ dependencies = [ "lazy_static", "log", "native-tls", - "openssl", "poem", + "rand", "regex", "reqwest", + "rustls", + "rustls-native-certs", "rusty-hook", "serde", "serde-aux", @@ -957,7 +959,7 @@ checksum = "6971da4d9c3aa03c3d8f3ff0f4155b534aad021292003895a469716b2a230378" dependencies = [ "base64 0.21.3", "pem", - "ring", + "ring 0.16.20", "serde", "serde_json", "simple_asn1", @@ -974,9 +976,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.147" +version = "0.2.150" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" +checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" [[package]] name = "libm" @@ -1554,11 +1556,25 @@ dependencies = [ "libc", "once_cell", "spin 0.5.2", - "untrusted", + "untrusted 0.7.1", "web-sys", "winapi", ] +[[package]] +name = "ring" +version = "0.17.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb0205304757e5d899b9c2e448b867ffd03ae7f988002e47cd24954391394d0b" +dependencies = [ + "cc", + "getrandom", + "libc", + "spin 0.9.8", + "untrusted 0.9.0", + "windows-sys", +] + [[package]] name = "rsa" version = "0.9.2" @@ -1600,6 +1616,49 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "rustls" +version = "0.21.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446e14c5cda4f3f30fe71863c34ec70f5ac79d6087097ad0bb433e1be5edf04c" +dependencies = [ + "log", + "ring 0.17.5", + "rustls-webpki", + "sct", +] + +[[package]] +name = "rustls-native-certs" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" +dependencies = [ + "openssl-probe", + "rustls-pemfile", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d3987094b1d07b653b7dfdc3f70ce9a1da9c51ac18c1b06b662e4f9a0e9f4b2" +dependencies = [ + "base64 0.21.3", +] + +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring 0.17.5", + "untrusted 0.9.0", +] + [[package]] name = "rusty-hook" version = "0.11.2" @@ -1633,6 +1692,16 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "sct" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" +dependencies = [ + "ring 0.16.20", + "untrusted 0.7.1", +] + [[package]] name = "security-framework" version = "2.9.2" @@ -2247,6 +2316,16 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +dependencies = [ + "rustls", + "tokio", +] + [[package]] name = "tokio-stream" version = "0.1.14" @@ -2266,9 +2345,10 @@ checksum = "2b2dbec703c26b00d74844519606ef15d09a7d6857860f84ad223dec002ddea2" dependencies = [ "futures-util", "log", - "native-tls", + "rustls", + "rustls-native-certs", "tokio", - "tokio-native-tls", + "tokio-rustls", "tungstenite", ] @@ -2369,8 +2449,8 @@ dependencies = [ "http", "httparse", "log", - "native-tls", "rand", + "rustls", "sha1", "thiserror", "url", @@ -2446,6 +2526,12 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "url" version = "2.4.1" diff --git a/Cargo.toml b/Cargo.toml index 42a5130..c1a1f0f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,10 +28,12 @@ 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.20.0", features = ["native-tls"] } +tokio-tungstenite = { version = "0.20.0", features = [ + "rustls-tls-native-roots", + "rustls-native-certs", +] } futures-util = "0.3.28" http = "0.2.9" -openssl = "0.10.56" base64 = "0.21.3" hostname = "0.3.1" bitflags = { version = "2.4.0", features = ["serde"] } @@ -51,6 +53,9 @@ jsonwebtoken = "8.3.0" log = "0.4.20" async-trait = "0.1.73" chorus-macros = "0.2.0" +rustls = "0.21.8" +rustls-native-certs = "0.6.3" +rand = "0.8.5" [dev-dependencies] tokio = { version = "1.32.0", features = ["full"] } diff --git a/src/gateway.rs b/src/gateway.rs index 8689406..edd402d 100644 --- a/src/gateway.rs +++ b/src/gateway.rs @@ -20,7 +20,6 @@ use futures_util::stream::SplitStream; use futures_util::SinkExt; use futures_util::StreamExt; use log::{info, trace, warn}; -use native_tls::TlsConnector; use tokio::net::TcpStream; use tokio::sync::mpsc::Sender; use tokio::sync::Mutex; @@ -349,12 +348,21 @@ pub struct Gateway { impl Gateway { #[allow(clippy::new_ret_no_self)] pub async fn new(websocket_url: String) -> Result { + let mut roots = rustls::RootCertStore::empty(); + for cert in rustls_native_certs::load_native_certs().expect("could not load platform certs") + { + roots.add(&rustls::Certificate(cert.0)).unwrap(); + } let (websocket_stream, _) = match connect_async_tls_with_config( &websocket_url, None, false, - Some(Connector::NativeTls( - TlsConnector::builder().build().unwrap(), + Some(Connector::Rustls( + rustls::ClientConfig::builder() + .with_safe_defaults() + .with_root_certificates(roots) + .with_no_client_auth() + .into(), )), ) .await diff --git a/src/types/config/types/security_configuration.rs b/src/types/config/types/security_configuration.rs index d025a4b..caeb72c 100644 --- a/src/types/config/types/security_configuration.rs +++ b/src/types/config/types/security_configuration.rs @@ -1,4 +1,5 @@ use base64::Engine; +use rand::Fill; use serde::{Deserialize, Serialize}; use crate::types::config::types::subconfigs::security::{ @@ -22,10 +23,15 @@ pub struct SecurityConfiguration { impl Default for SecurityConfiguration { fn default() -> Self { + let mut rng: rand::rngs::ThreadRng = rand::thread_rng(); let mut req_sig: [u8; 32] = [0; 32]; - let _ = openssl::rand::rand_bytes(&mut req_sig); let mut jwt_secret: [u8; 256] = [0; 256]; - let _ = openssl::rand::rand_bytes(&mut jwt_secret); + req_sig + .try_fill(&mut rng) + .expect("Unable to generate cryptographically safe secrets."); + jwt_secret + .try_fill(&mut rng) + .expect("Unable to generate cryptographically safe secrets."); Self { captcha: Default::default(), two_factor: Default::default(), From fac543bf5a0e0afc4deaf9451c1472c5bb5281d0 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Sun, 12 Nov 2023 17:30:49 +0100 Subject: [PATCH 02/16] bump actions version - bump checkout to @v4 - bump rust-toolchain to @v1 --- .github/workflows/build_and_test.yml | 2 +- .github/workflows/rust-clippy.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index b192a3c..2fa8343 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Clone spacebar server run: | git clone https://github.com/bitfl0wer/server.git diff --git a/.github/workflows/rust-clippy.yml b/.github/workflows/rust-clippy.yml index 0c3840f..560fd4e 100644 --- a/.github/workflows/rust-clippy.yml +++ b/.github/workflows/rust-clippy.yml @@ -26,10 +26,10 @@ jobs: actions: read # only required for a private repository by github/codeql-action/upload-sarif to get the Action run status steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Install Rust toolchain - uses: actions-rs/toolchain@16499b5e05bf2e26879000db0c1d13f7e13fa3af #@v1 + uses: actions-rs/toolchain@v1 with: profile: minimal toolchain: stable From 2edab4cc994ecb649db3cde8de1d8941c1fd48f7 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Sun, 12 Nov 2023 17:32:57 +0100 Subject: [PATCH 03/16] include "dev" branch in actions executions --- .github/workflows/build_and_test.yml | 2 +- .github/workflows/rust-clippy.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index 2fa8343..31962c2 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -4,7 +4,7 @@ on: push: branches: [ "main", "dev" ] pull_request: - branches: [ "main" ] + branches: [ "main", "dev" ] env: CARGO_TERM_COLOR: always diff --git a/.github/workflows/rust-clippy.yml b/.github/workflows/rust-clippy.yml index 560fd4e..15dedd4 100644 --- a/.github/workflows/rust-clippy.yml +++ b/.github/workflows/rust-clippy.yml @@ -14,7 +14,7 @@ on: branches: [ "main", "preserve/*", "dev" ] pull_request: # The branches below must be a subset of the branches above - branches: [ "main" ] + branches: [ "main", "dev" ] jobs: rust-clippy-analyze: From 279ac9d8ca06c274492c458f29ca942f7600c9d1 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Sun, 12 Nov 2023 17:41:59 +0100 Subject: [PATCH 04/16] Bump version for chorus --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c35635e..99ff172 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -177,7 +177,7 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chorus" -version = "0.9.0" +version = "0.10.0" dependencies = [ "async-trait", "base64 0.21.3", diff --git a/Cargo.toml b/Cargo.toml index c1a1f0f..9df79d0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "chorus" description = "A library for interacting with multiple Spacebar-compatible Instances at once." -version = "0.9.0" +version = "0.10.0" license = "AGPL-3.0" edition = "2021" repository = "https://github.com/polyphony-chat/chorus" From c3d69371711a5e06aa89be62f96808d250baba3f Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Sun, 12 Nov 2023 18:04:15 +0100 Subject: [PATCH 05/16] Remove openssl-dependency fully --- Cargo.lock | 1 - Cargo.toml | 1 - 2 files changed, 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 99ff172..cfd6a20 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -191,7 +191,6 @@ dependencies = [ "jsonwebtoken", "lazy_static", "log", - "native-tls", "poem", "rand", "regex", diff --git a/Cargo.toml b/Cargo.toml index 9df79d0..9467044 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,7 +27,6 @@ url = "2.4.0" 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.20.0", features = [ "rustls-tls-native-roots", "rustls-native-certs", From 6af267c7cf30a233fafe9769ee07e317aaa02624 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Sun, 12 Nov 2023 18:04:47 +0100 Subject: [PATCH 06/16] HOTFIX: remove openssl, like it was intended --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cfd6a20..20c2671 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -177,7 +177,7 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chorus" -version = "0.10.0" +version = "0.10.1" dependencies = [ "async-trait", "base64 0.21.3", diff --git a/Cargo.toml b/Cargo.toml index 9467044..c7ae062 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "chorus" description = "A library for interacting with multiple Spacebar-compatible Instances at once." -version = "0.10.0" +version = "0.10.1" license = "AGPL-3.0" edition = "2021" repository = "https://github.com/polyphony-chat/chorus" From 7e710072c52d7c3a94facc58d206f4bf9fccaf47 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Mon, 13 Nov 2023 22:20:50 +0100 Subject: [PATCH 07/16] Add login, instance examples --- examples/instance.rs | 16 ++++++++++++++++ examples/login.rs | 30 ++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 examples/instance.rs create mode 100644 examples/login.rs diff --git a/examples/instance.rs b/examples/instance.rs new file mode 100644 index 0000000..337482b --- /dev/null +++ b/examples/instance.rs @@ -0,0 +1,16 @@ +use chorus::instance::Instance; +use chorus::UrlBundle; + +#[tokio::main] +async fn main() { + let bundle = UrlBundle::new( + "https://example.com/api".to_string(), + "wss://example.com/".to_string(), + "https://example.com/cdn".to_string(), + ); + let instance = Instance::new(bundle, true) + .await + .expect("Failed to connect to the Spacebar server"); + dbg!(instance.instance_info); + dbg!(instance.limits_information); +} diff --git a/examples/login.rs b/examples/login.rs new file mode 100644 index 0000000..4595a06 --- /dev/null +++ b/examples/login.rs @@ -0,0 +1,30 @@ +use chorus::instance::Instance; +use chorus::types::LoginSchema; +use chorus::UrlBundle; + +#[tokio::main] +async fn main() { + let bundle = UrlBundle::new( + "https://example.com/api".to_string(), + "wss://example.com/".to_string(), + "https://example.com/cdn".to_string(), + ); + let instance = Instance::new(bundle, true) + .await + .expect("Failed to connect to the Spacebar server"); + // Assume, you already have an account created on this instance. Registering an account works + // the same way, but you'd use the Register-specific Structs and methods instead. + let login_schema = LoginSchema { + login: "user@example.com".to_string(), + password: "Correct-Horse-Battery-Staple".to_string(), + ..Default::default() + }; + // Each user connects to the Gateway. The Gateway connection lives on a seperate thread. Depending on + // the runtime feature you choose, this can potentially take advantage of all of your computers' threads. + let user = instance + .login_account(login_schema) + .await + .expect("An error occurred during the login process"); + dbg!(user.belongs_to); + dbg!(&user.object.read().unwrap().username); +} From 97e4db012a6409681ee85c94d49ebce41b9805e6 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Mon, 13 Nov 2023 22:20:57 +0100 Subject: [PATCH 08/16] Refactor Chorus library documentation and examples --- README.md | 92 ++++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 84 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 74dff4c..e654610 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,6 @@ [![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]
@@ -17,7 +15,6 @@

Chorus

- A rust library for interacting with (multiple) Spacebar-compatible APIs and Gateways (at the same time).
Explore the docs ยป
@@ -32,16 +29,95 @@ -## About +Chorus is a Rust library that allows developers to interact with multiple Spacebar-compatible APIs and Gateways (Including +Discord.com) 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. -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. +## A Tour of Chorus + +Chorus combines all the required functionalities of a user-centric Spacebar library into one package. The library +handles a lot of things for you, such as rate limiting, authentication, and more. This means that you can focus on +building your application, instead of worrying about the underlying implementation details. + +To get started with Chorus, import it into your project by adding the following to your `Cargo.toml` file: + +```toml +[dependencies] +chorus = "0" +``` + +### Establishing a Connection + +To connect to a Spacebar compatible server, you need to create an [`Instance`](https://docs.rs/chorus/latest/chorus/instance/struct.Instance.html) like this: + +```rs +use chorus::instance::Instance; +use chorus::UrlBundle; + +#[tokio::main] +async fn main() { + let bundle = UrlBundle::new( + "https://example.com/api".to_string(), + "wss://example.com/".to_string(), + "https://example.com/cdn".to_string(), + ); + let instance = Instance::new(bundle, true) + .await + .expect("Failed to connect to the Spacebar server"); + // You can create as many instances of `Instance` as you want, but each `Instance` should likely be unique. + dbg!(instance.instance_info); + dbg!(instance.limits_information); +} +``` + +This Instance can now be used to log in, register and from there on, interact with the server in all sorts of ways. + +### Logging In + +Logging in correctly provides you with an instance of `ChorusUser`, with which you can interact with the server and +manipulate the account. Assuming you already have an account on the server, you can log in like this: + +```rs +use chorus::types::LoginSchema; +// Assume, you already have an account created on this instance. Registering an account works +// the same way, but you'd use the Register-specific Structs and methods instead. +let login_schema = LoginSchema { + login: "user@example.com".to_string(), + password: "Correct-Horse-Battery-Staple".to_string(), + ..Default::default() +}; +// Each user connects to the Gateway. The Gateway connection lives on a seperate thread. Depending on +// the runtime feature you choose, this can potentially take advantage of all of your computers' threads. +let user = instance + .login_account(login_schema) + .await + .expect("An error occurred during the login process"); +dbg!(user.belongs_to); +dbg!(&user.object.read().unwrap().username); +``` + +## Supported Platforms + +All major desktop operating systems (Windows, macOS (aarch64/x86_64), Linux (aarch64/x86_64)) are supported. +We are currently working on adding full support for `wasm32-unknown-unknown`. This will allow you to use Chorus in +your browser, or in any other environment that supports WebAssembly. + +We recommend checking out the examples directory, as well as the documentation for more information. + +## MSRV (Minimum Supported Rust Version) + +Rust **1.67.1**. This number might change at any point while Chorus is not yet at version 1.0.0. + +## Versioning + +This crate uses Semantic Versioning 2.0.0 as its versioning scheme. You can read the specification [here](https://semver.org/spec/v2.0.0.html). ## Contributing +Chorus is currently missing voice support and a lot of API endpoints, many of which should be trivial to implement, +ever since [we streamlined the process of doing so](https://github.com/polyphony-chat/chorus/discussions/401). + 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). +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).

Progress Tracker/Roadmap From 81447c9ddaf83ca018ae049439b3d98671c12a61 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Tue, 14 Nov 2023 11:13:02 +0100 Subject: [PATCH 09/16] Split up gateway.rs into several files --- src/api/invites/mod.rs | 4 +- src/gateway/events.rs | 1 + src/{ => gateway}/gateway.rs | 576 +---------------------------------- src/gateway/handle.rs | 172 +++++++++++ src/gateway/heartbeat.rs | 146 +++++++++ src/gateway/message.rs | 73 +++++ src/gateway/mod.rs | 190 ++++++++++++ 7 files changed, 593 insertions(+), 569 deletions(-) create mode 100644 src/gateway/events.rs rename src/{ => gateway}/gateway.rs (55%) create mode 100644 src/gateway/handle.rs create mode 100644 src/gateway/heartbeat.rs create mode 100644 src/gateway/message.rs create mode 100644 src/gateway/mod.rs diff --git a/src/api/invites/mod.rs b/src/api/invites/mod.rs index 332570b..80b47d2 100644 --- a/src/api/invites/mod.rs +++ b/src/api/invites/mod.rs @@ -28,11 +28,11 @@ impl ChorusUser { .header("Authorization", self.token()), limit_type: LimitType::Global, }; - if session_id.is_some() { + if let Some(session_id) = session_id { request.request = request .request .header("Content-Type", "application/json") - .body(to_string(session_id.unwrap()).unwrap()); + .body(to_string(session_id).unwrap()); } request.deserialize_response::(self).await } diff --git a/src/gateway/events.rs b/src/gateway/events.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/gateway/events.rs @@ -0,0 +1 @@ + diff --git a/src/gateway.rs b/src/gateway/gateway.rs similarity index 55% rename from src/gateway.rs rename to src/gateway/gateway.rs index edd402d..026f92f 100644 --- a/src/gateway.rs +++ b/src/gateway/gateway.rs @@ -1,331 +1,11 @@ -//! Gateway connection, communication and handling, as well as object caching and updating. +use self::event::Events; -use crate::errors::GatewayError; -use crate::gateway::events::Events; +use super::*; use crate::types::{ self, AutoModerationRule, AutoModerationRuleUpdate, Channel, ChannelCreate, ChannelDelete, - ChannelUpdate, Composite, Guild, GuildRoleCreate, GuildRoleUpdate, JsonField, RoleObject, - Snowflake, SourceUrlField, ThreadUpdate, UpdateMessage, WebSocketEvent, + ChannelUpdate, Guild, GuildRoleCreate, GuildRoleUpdate, JsonField, RoleObject, 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, RwLock}; -use std::time::Duration; -use tokio::time::sleep_until; - -use futures_util::stream::SplitSink; -use futures_util::stream::SplitStream; -use futures_util::SinkExt; -use futures_util::StreamExt; -use log::{info, trace, warn}; -use tokio::net::TcpStream; -use tokio::sync::mpsc::Sender; -use tokio::sync::Mutex; -use tokio::task; -use tokio::task::JoinHandle; -use tokio::time; -use tokio::time::Instant; -use tokio_tungstenite::MaybeTlsStream; -use tokio_tungstenite::{connect_async_tls_with_config, Connector, WebSocketStream}; - -// Gateway opcodes -/// Opcode received when the server dispatches a [crate::types::WebSocketEvent] -const GATEWAY_DISPATCH: u8 = 0; -/// Opcode sent when sending a heartbeat -const GATEWAY_HEARTBEAT: u8 = 1; -/// Opcode sent to initiate a session -/// -/// See [types::GatewayIdentifyPayload] -const GATEWAY_IDENTIFY: u8 = 2; -/// Opcode sent to update our presence -/// -/// See [types::GatewayUpdatePresence] -const GATEWAY_UPDATE_PRESENCE: u8 = 3; -/// Opcode sent to update our state in vc -/// -/// Like muting, deafening, leaving, joining.. -/// -/// See [types::UpdateVoiceState] -const GATEWAY_UPDATE_VOICE_STATE: u8 = 4; -/// Opcode sent to resume a session -/// -/// See [types::GatewayResume] -const GATEWAY_RESUME: u8 = 6; -/// Opcode received to tell the client to reconnect -const GATEWAY_RECONNECT: u8 = 7; -/// Opcode sent to request guild member data -/// -/// See [types::GatewayRequestGuildMembers] -const GATEWAY_REQUEST_GUILD_MEMBERS: u8 = 8; -/// Opcode received to tell the client their token / session is invalid -const GATEWAY_INVALID_SESSION: u8 = 9; -/// Opcode received when initially connecting to the gateway, starts our heartbeat -/// -/// See [types::HelloData] -const GATEWAY_HELLO: u8 = 10; -/// Opcode received to acknowledge a heartbeat -const GATEWAY_HEARTBEAT_ACK: u8 = 11; -/// Opcode sent to get the voice state of users in a given DM/group channel -/// -/// See [types::CallSync] -const GATEWAY_CALL_SYNC: u8 = 13; -/// Opcode sent to get data for a server (Lazy Loading request) -/// -/// Sent by the official client when switching to a server -/// -/// See [types::LazyRequest] -const GATEWAY_LAZY_REQUEST: u8 = 14; - -/// The amount of time we wait for a heartbeat ack before resending our heartbeat in ms -const HEARTBEAT_ACK_TIMEOUT: u64 = 2000; - -/// 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 { - /// The message we received from the server - message: tokio_tungstenite::tungstenite::Message, -} - -impl GatewayMessage { - /// Creates self from a tungstenite message - pub fn from_tungstenite_message(message: tokio_tungstenite::tungstenite::Message) -> Self { - Self { message } - } - - /// Parses the message as an error; - /// Returns the error if succesfully parsed, None if the message isn't an error - pub fn error(&self) -> Option { - let content = self.message.to_string(); - - // Some error strings have dots on the end, which we don't care about - let processed_content = content.to_lowercase().replace('.', ""); - - match processed_content.as_str() { - "unknown error" | "4000" => Some(GatewayError::Unknown), - "unknown opcode" | "4001" => Some(GatewayError::UnknownOpcode), - "decode error" | "error while decoding payload" | "4002" => Some(GatewayError::Decode), - "not authenticated" | "4003" => Some(GatewayError::NotAuthenticated), - "authentication failed" | "4004" => Some(GatewayError::AuthenticationFailed), - "already authenticated" | "4005" => Some(GatewayError::AlreadyAuthenticated), - "invalid seq" | "4007" => Some(GatewayError::InvalidSequenceNumber), - "rate limited" | "4008" => Some(GatewayError::RateLimited), - "session timed out" | "4009" => Some(GatewayError::SessionTimedOut), - "invalid shard" | "4010" => Some(GatewayError::InvalidShard), - "sharding required" | "4011" => Some(GatewayError::ShardingRequired), - "invalid api version" | "4012" => Some(GatewayError::InvalidAPIVersion), - "invalid intent(s)" | "invalid intent" | "4013" => Some(GatewayError::InvalidIntents), - "disallowed intent(s)" | "disallowed intents" | "4014" => { - Some(GatewayError::DisallowedIntents) - } - _ => None, - } - } - - /// Returns whether or not the message is an error - pub fn is_error(&self) -> bool { - self.error().is_some() - } - - /// Parses the message as a payload; - /// Returns a result of deserializing - pub fn payload(&self) -> Result { - return serde_json::from_str(self.message.to_text().unwrap()); - } - - /// Returns whether or not the message is a payload - pub fn is_payload(&self) -> bool { - // close messages are never payloads, payloads are only text messages - if self.message.is_close() | !self.message.is_text() { - return false; - } - - return self.payload().is_ok(); - } - - /// Returns whether or not the message is empty - pub fn is_empty(&self) -> bool { - self.message.is_empty() - } -} - -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`] -/// Using this handle you can also send Gateway Events directly. -#[derive(Debug, Clone)] -pub struct GatewayHandle { - pub url: String, - pub events: Arc>, - pub websocket_send: Arc< - Mutex< - SplitSink< - WebSocketStream>, - tokio_tungstenite::tungstenite::Message, - >, - >, - >, - /// Tells gateway tasks to close - kill_send: tokio::sync::broadcast::Sender<()>, - 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. -pub trait Updateable: 'static + Send + Sync { - fn id(&self) -> Snowflake; -} - -impl GatewayHandle { - /// Sends json to the gateway with an opcode - async fn send_json_event(&self, op_code: u8, to_send: serde_json::Value) { - let gateway_payload = types::GatewaySendPayload { - op_code, - event_data: Some(to_send), - sequence_number: None, - }; - - let payload_json = serde_json::to_string(&gateway_payload).unwrap(); - - let message = tokio_tungstenite::tungstenite::Message::text(payload_json); - - self.websocket_send - .lock() - .await - .send(message) - .await - .unwrap(); - } - - pub async fn observe>( - &self, - object: Arc>, - ) -> Arc> { - let mut store = self.store.lock().await; - 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.", - id - ) - }); - 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.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(); - - trace!("GW: Sending Identify.."); - - self.send_json_event(GATEWAY_IDENTIFY, to_send_value).await; - } - - /// Sends a resume event to the gateway - pub async fn send_resume(&self, to_send: types::GatewayResume) { - let to_send_value = serde_json::to_value(&to_send).unwrap(); - - trace!("GW: Sending Resume.."); - - self.send_json_event(GATEWAY_RESUME, to_send_value).await; - } - - /// Sends an update presence event to the gateway - pub async fn send_update_presence(&self, to_send: types::UpdatePresence) { - let to_send_value = serde_json::to_value(&to_send).unwrap(); - - trace!("GW: Sending Update Presence.."); - - self.send_json_event(GATEWAY_UPDATE_PRESENCE, to_send_value) - .await; - } - - /// Sends a request guild members to the server - pub async fn send_request_guild_members(&self, to_send: types::GatewayRequestGuildMembers) { - let to_send_value = serde_json::to_value(&to_send).unwrap(); - - trace!("GW: Sending Request Guild Members.."); - - self.send_json_event(GATEWAY_REQUEST_GUILD_MEMBERS, to_send_value) - .await; - } - - /// 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(); - - trace!("GW: Sending Update Voice State.."); - - self.send_json_event(GATEWAY_UPDATE_VOICE_STATE, to_send_value) - .await; - } - - /// Sends a call sync to the server - pub async fn send_call_sync(&self, to_send: types::CallSync) { - let to_send_value = serde_json::to_value(&to_send).unwrap(); - - trace!("GW: Sending Call Sync.."); - - self.send_json_event(GATEWAY_CALL_SYNC, to_send_value).await; - } - - /// Sends a Lazy Request - pub async fn send_lazy_request(&self, to_send: types::LazyRequest) { - let to_send_value = serde_json::to_value(&to_send).unwrap(); - - trace!("GW: Sending Lazy Request.."); - - self.send_json_event(GATEWAY_LAZY_REQUEST, to_send_value) - .await; - } - - /// Closes the websocket connection and stops all gateway tasks; - /// - /// Esentially pulls the plug on the gateway, leaving it possible to resume; - pub async fn close(&self) { - self.kill_send.send(()).unwrap(); - self.websocket_send.lock().await.close().await.unwrap(); - } -} #[derive(Debug)] pub struct Gateway { @@ -709,10 +389,10 @@ impl Gateway { | GATEWAY_REQUEST_GUILD_MEMBERS | GATEWAY_CALL_SYNC | GATEWAY_LAZY_REQUEST => { - let error = GatewayError::UnexpectedOpcodeReceived { - opcode: gateway_payload.op_code, - }; - Err::<(), GatewayError>(error).unwrap(); + info!( + "Received unexpected opcode ({}) for current state. This might be due to a faulty server implementation and is likely not the fault of chorus.", + gateway_payload.op_code + ); } _ => { warn!("Received unrecognized gateway op code ({})! Please open an issue on the chorus github so we can implement it", gateway_payload.op_code); @@ -736,196 +416,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, - /// The send channel for the heartbeat thread - pub send: Sender, - /// The handle of the thread - handle: JoinHandle<()>, -} - -impl HeartbeatHandler { - pub fn new( - heartbeat_interval: Duration, - websocket_tx: Arc< - Mutex< - SplitSink< - WebSocketStream>, - tokio_tungstenite::tungstenite::Message, - >, - >, - >, - kill_rc: tokio::sync::broadcast::Receiver<()>, - ) -> HeartbeatHandler { - let (send, receive) = tokio::sync::mpsc::channel(32); - let kill_receive = kill_rc.resubscribe(); - - let handle: JoinHandle<()> = task::spawn(async move { - HeartbeatHandler::heartbeat_task( - websocket_tx, - heartbeat_interval, - receive, - kill_receive, - ) - .await; - }); - - Self { - heartbeat_interval, - send, - handle, - } - } - - /// The main heartbeat task; - /// - /// Can be killed by the kill broadcast; - /// If the websocket is closed, will die out next time it tries to send a heartbeat; - pub async fn heartbeat_task( - websocket_tx: Arc< - Mutex< - SplitSink< - WebSocketStream>, - tokio_tungstenite::tungstenite::Message, - >, - >, - >, - heartbeat_interval: Duration, - mut receive: tokio::sync::mpsc::Receiver, - mut kill_receive: tokio::sync::broadcast::Receiver<()>, - ) { - let mut last_heartbeat_timestamp: Instant = time::Instant::now(); - let mut last_heartbeat_acknowledged = true; - let mut last_seq_number: Option = None; - - loop { - if kill_receive.try_recv().is_ok() { - trace!("GW: Closing heartbeat task"); - break; - } - - let timeout = if last_heartbeat_acknowledged { - heartbeat_interval - } else { - // If the server hasn't acknowledged our heartbeat we should resend it - Duration::from_millis(HEARTBEAT_ACK_TIMEOUT) - }; - - let mut should_send = false; - - tokio::select! { - () = sleep_until(last_heartbeat_timestamp + timeout) => { - should_send = true; - } - Some(communication) = receive.recv() => { - // If we received a seq number update, use that as the last seq number - if communication.sequence_number.is_some() { - last_seq_number = communication.sequence_number; - } - - if let Some(op_code) = communication.op_code { - match op_code { - GATEWAY_HEARTBEAT => { - // As per the api docs, if the server sends us a Heartbeat, that means we need to respond with a heartbeat immediately - should_send = true; - } - GATEWAY_HEARTBEAT_ACK => { - // The server received our heartbeat - last_heartbeat_acknowledged = true; - } - _ => {} - } - } - } - } - - if should_send { - trace!("GW: Sending Heartbeat.."); - - let heartbeat = types::GatewayHeartbeat { - op: GATEWAY_HEARTBEAT, - d: last_seq_number, - }; - - let heartbeat_json = serde_json::to_string(&heartbeat).unwrap(); - - let msg = tokio_tungstenite::tungstenite::Message::text(heartbeat_json); - - let send_result = websocket_tx.lock().await.send(msg).await; - if send_result.is_err() { - // We couldn't send, the websocket is broken - warn!("GW: Couldnt send heartbeat, websocket seems broken"); - break; - } - - last_heartbeat_timestamp = time::Instant::now(); - last_heartbeat_acknowledged = false; - } - } - } -} - -/// Used for communications between the heartbeat and gateway thread. -/// Either signifies a sequence number update, a heartbeat ACK or a Heartbeat request by the server -#[derive(Clone, Copy, Debug)] -struct HeartbeatThreadCommunication { - /// The opcode for the communication we received, if relevant - op_code: Option, - /// The sequence number we got from discord, if any - sequence_number: Option, -} - -/// Trait which defines the behavior of an Observer. An Observer is an object which is subscribed to -/// an Observable. The Observer is notified when the Observable's data changes. -/// In this case, the Observable is a [`GatewayEvent`], which is a wrapper around a WebSocketEvent. -/// Note that `Debug` is used to tell `Observer`s apart when unsubscribing. -#[async_trait] -pub trait Observer: Sync + Send + std::fmt::Debug { - async fn update(&self, data: &T); -} - -/// GatewayEvent is a wrapper around a WebSocketEvent. It is used to notify the observers of a -/// change in the WebSocketEvent. GatewayEvents are observable. -#[derive(Default, Debug)] -pub struct GatewayEvent { - observers: Vec>>, -} - -impl GatewayEvent { - /// Returns true if the GatewayEvent is observed by at least one Observer. - pub fn is_observed(&self) -> bool { - !self.observers.is_empty() - } - - /// Subscribes an Observer to the GatewayEvent. - pub fn subscribe(&mut self, observable: Arc>) { - self.observers.push(observable); - } - - /// Unsubscribes an Observer from the GatewayEvent. - pub fn unsubscribe(&mut self, observable: &dyn Observer) { - // .retain()'s closure retains only those elements of the vector, which have a different - // pointer value than observable. - // The usage of the debug format to compare the generic T of observers is quite stupid, but the only thing to compare between them is T and if T == T they are the same - // anddd there is no way to do that without using format - let to_remove = format!("{:?}", observable); - self.observers - .retain(|obs| format!("{:?}", obs) != to_remove); - } - - /// Notifies the observers of the GatewayEvent. - async fn notify(&self, new_event_data: T) { - for observer in &self.observers { - observer.update(&new_event_data).await; - } - } -} - -pub mod events { +pub mod event { use super::*; #[derive(Default, Debug)] @@ -1086,52 +577,3 @@ pub mod events { pub update: GatewayEvent, } } - -#[cfg(test)] -mod example { - use super::*; - use std::sync::atomic::{AtomicI32, Ordering::Relaxed}; - - #[derive(Debug)] - struct Consumer { - _name: String, - events_received: AtomicI32, - } - - #[async_trait] - impl Observer for Consumer { - async fn update(&self, _data: &types::GatewayResume) { - self.events_received.fetch_add(1, Relaxed); - } - } - - #[tokio::test] - async fn test_observer_behavior() { - let mut event = GatewayEvent::default(); - - let new_data = types::GatewayResume { - token: "token_3276ha37am3".to_string(), - session_id: "89346671230".to_string(), - seq: "3".to_string(), - }; - - let consumer = Arc::new(Consumer { - _name: "first".into(), - events_received: 0.into(), - }); - event.subscribe(consumer.clone()); - - let second_consumer = Arc::new(Consumer { - _name: "second".into(), - events_received: 0.into(), - }); - event.subscribe(second_consumer.clone()); - - event.notify(new_data.clone()).await; - event.unsubscribe(&*consumer); - event.notify(new_data).await; - - assert_eq!(consumer.events_received.load(Relaxed), 1); - assert_eq!(second_consumer.events_received.load(Relaxed), 2); - } -} diff --git a/src/gateway/handle.rs b/src/gateway/handle.rs new file mode 100644 index 0000000..4e21f2b --- /dev/null +++ b/src/gateway/handle.rs @@ -0,0 +1,172 @@ +use super::event::Events; +use super::*; +use crate::types::{self, Composite}; + +/// 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`] +/// Using this handle you can also send Gateway Events directly. +#[derive(Debug, Clone)] +pub struct GatewayHandle { + pub url: String, + pub events: Arc>, + pub websocket_send: Arc< + Mutex< + SplitSink< + WebSocketStream>, + tokio_tungstenite::tungstenite::Message, + >, + >, + >, + /// Tells gateway tasks to close + pub(super) kill_send: tokio::sync::broadcast::Sender<()>, + pub(crate) store: Arc>>>>, +} + +impl GatewayHandle { + /// Sends json to the gateway with an opcode + async fn send_json_event(&self, op_code: u8, to_send: serde_json::Value) { + let gateway_payload = types::GatewaySendPayload { + op_code, + event_data: Some(to_send), + sequence_number: None, + }; + + let payload_json = serde_json::to_string(&gateway_payload).unwrap(); + + let message = tokio_tungstenite::tungstenite::Message::text(payload_json); + + self.websocket_send + .lock() + .await + .send(message) + .await + .unwrap(); + } + + pub async fn observe>( + &self, + object: Arc>, + ) -> Arc> { + let mut store = self.store.lock().await; + 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.", + id + ) + }); + 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.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(); + + trace!("GW: Sending Identify.."); + + self.send_json_event(GATEWAY_IDENTIFY, to_send_value).await; + } + + /// Sends a resume event to the gateway + pub async fn send_resume(&self, to_send: types::GatewayResume) { + let to_send_value = serde_json::to_value(&to_send).unwrap(); + + trace!("GW: Sending Resume.."); + + self.send_json_event(GATEWAY_RESUME, to_send_value).await; + } + + /// Sends an update presence event to the gateway + pub async fn send_update_presence(&self, to_send: types::UpdatePresence) { + let to_send_value = serde_json::to_value(&to_send).unwrap(); + + trace!("GW: Sending Update Presence.."); + + self.send_json_event(GATEWAY_UPDATE_PRESENCE, to_send_value) + .await; + } + + /// Sends a request guild members to the server + pub async fn send_request_guild_members(&self, to_send: types::GatewayRequestGuildMembers) { + let to_send_value = serde_json::to_value(&to_send).unwrap(); + + trace!("GW: Sending Request Guild Members.."); + + self.send_json_event(GATEWAY_REQUEST_GUILD_MEMBERS, to_send_value) + .await; + } + + /// 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(); + + trace!("GW: Sending Update Voice State.."); + + self.send_json_event(GATEWAY_UPDATE_VOICE_STATE, to_send_value) + .await; + } + + /// Sends a call sync to the server + pub async fn send_call_sync(&self, to_send: types::CallSync) { + let to_send_value = serde_json::to_value(&to_send).unwrap(); + + trace!("GW: Sending Call Sync.."); + + self.send_json_event(GATEWAY_CALL_SYNC, to_send_value).await; + } + + /// Sends a Lazy Request + pub async fn send_lazy_request(&self, to_send: types::LazyRequest) { + let to_send_value = serde_json::to_value(&to_send).unwrap(); + + trace!("GW: Sending Lazy Request.."); + + self.send_json_event(GATEWAY_LAZY_REQUEST, to_send_value) + .await; + } + + /// Closes the websocket connection and stops all gateway tasks; + /// + /// Esentially pulls the plug on the gateway, leaving it possible to resume; + pub async fn close(&self) { + self.kill_send.send(()).unwrap(); + self.websocket_send.lock().await.close().await.unwrap(); + } +} diff --git a/src/gateway/heartbeat.rs b/src/gateway/heartbeat.rs new file mode 100644 index 0000000..2f443c8 --- /dev/null +++ b/src/gateway/heartbeat.rs @@ -0,0 +1,146 @@ +use crate::types; + +use super::*; + +/// Handles sending heartbeats to the gateway in another thread +#[allow(dead_code)] // FIXME: Remove this, once HeartbeatHandler is used +#[derive(Debug)] +pub(super) struct HeartbeatHandler { + /// How ofter heartbeats need to be sent at a minimum + pub heartbeat_interval: Duration, + /// The send channel for the heartbeat thread + pub send: Sender, + /// The handle of the thread + handle: JoinHandle<()>, +} + +impl HeartbeatHandler { + pub fn new( + heartbeat_interval: Duration, + websocket_tx: Arc< + Mutex< + SplitSink< + WebSocketStream>, + tokio_tungstenite::tungstenite::Message, + >, + >, + >, + kill_rc: tokio::sync::broadcast::Receiver<()>, + ) -> HeartbeatHandler { + let (send, receive) = tokio::sync::mpsc::channel(32); + let kill_receive = kill_rc.resubscribe(); + + let handle: JoinHandle<()> = task::spawn(async move { + HeartbeatHandler::heartbeat_task( + websocket_tx, + heartbeat_interval, + receive, + kill_receive, + ) + .await; + }); + + Self { + heartbeat_interval, + send, + handle, + } + } + + /// The main heartbeat task; + /// + /// Can be killed by the kill broadcast; + /// If the websocket is closed, will die out next time it tries to send a heartbeat; + pub async fn heartbeat_task( + websocket_tx: Arc< + Mutex< + SplitSink< + WebSocketStream>, + tokio_tungstenite::tungstenite::Message, + >, + >, + >, + heartbeat_interval: Duration, + mut receive: tokio::sync::mpsc::Receiver, + mut kill_receive: tokio::sync::broadcast::Receiver<()>, + ) { + let mut last_heartbeat_timestamp: Instant = time::Instant::now(); + let mut last_heartbeat_acknowledged = true; + let mut last_seq_number: Option = None; + + loop { + if kill_receive.try_recv().is_ok() { + trace!("GW: Closing heartbeat task"); + break; + } + + let timeout = if last_heartbeat_acknowledged { + heartbeat_interval + } else { + // If the server hasn't acknowledged our heartbeat we should resend it + Duration::from_millis(HEARTBEAT_ACK_TIMEOUT) + }; + + let mut should_send = false; + + tokio::select! { + () = sleep_until(last_heartbeat_timestamp + timeout) => { + should_send = true; + } + Some(communication) = receive.recv() => { + // If we received a seq number update, use that as the last seq number + if communication.sequence_number.is_some() { + last_seq_number = communication.sequence_number; + } + + if let Some(op_code) = communication.op_code { + match op_code { + GATEWAY_HEARTBEAT => { + // As per the api docs, if the server sends us a Heartbeat, that means we need to respond with a heartbeat immediately + should_send = true; + } + GATEWAY_HEARTBEAT_ACK => { + // The server received our heartbeat + last_heartbeat_acknowledged = true; + } + _ => {} + } + } + } + } + + if should_send { + trace!("GW: Sending Heartbeat.."); + + let heartbeat = types::GatewayHeartbeat { + op: GATEWAY_HEARTBEAT, + d: last_seq_number, + }; + + let heartbeat_json = serde_json::to_string(&heartbeat).unwrap(); + + let msg = tokio_tungstenite::tungstenite::Message::text(heartbeat_json); + + let send_result = websocket_tx.lock().await.send(msg).await; + if send_result.is_err() { + // We couldn't send, the websocket is broken + warn!("GW: Couldnt send heartbeat, websocket seems broken"); + break; + } + + last_heartbeat_timestamp = time::Instant::now(); + last_heartbeat_acknowledged = false; + } + } + } +} + +/// Used for communications between the heartbeat and gateway thread. +/// Either signifies a sequence number update, a heartbeat ACK or a Heartbeat request by the server +#[derive(Clone, Copy, Debug)] +pub(super) struct HeartbeatThreadCommunication { + /// The opcode for the communication we received, if relevant + pub(super) op_code: Option, + /// The sequence number we got from discord, if any + pub(super) sequence_number: Option, +} diff --git a/src/gateway/message.rs b/src/gateway/message.rs new file mode 100644 index 0000000..edee9dd --- /dev/null +++ b/src/gateway/message.rs @@ -0,0 +1,73 @@ +use crate::types; + +use super::*; + +/// 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 { + /// The message we received from the server + pub(super) message: tokio_tungstenite::tungstenite::Message, +} + +impl GatewayMessage { + /// Creates self from a tungstenite message + pub fn from_tungstenite_message(message: tokio_tungstenite::tungstenite::Message) -> Self { + Self { message } + } + + /// Parses the message as an error; + /// Returns the error if succesfully parsed, None if the message isn't an error + pub fn error(&self) -> Option { + let content = self.message.to_string(); + + // Some error strings have dots on the end, which we don't care about + let processed_content = content.to_lowercase().replace('.', ""); + + match processed_content.as_str() { + "unknown error" | "4000" => Some(GatewayError::Unknown), + "unknown opcode" | "4001" => Some(GatewayError::UnknownOpcode), + "decode error" | "error while decoding payload" | "4002" => Some(GatewayError::Decode), + "not authenticated" | "4003" => Some(GatewayError::NotAuthenticated), + "authentication failed" | "4004" => Some(GatewayError::AuthenticationFailed), + "already authenticated" | "4005" => Some(GatewayError::AlreadyAuthenticated), + "invalid seq" | "4007" => Some(GatewayError::InvalidSequenceNumber), + "rate limited" | "4008" => Some(GatewayError::RateLimited), + "session timed out" | "4009" => Some(GatewayError::SessionTimedOut), + "invalid shard" | "4010" => Some(GatewayError::InvalidShard), + "sharding required" | "4011" => Some(GatewayError::ShardingRequired), + "invalid api version" | "4012" => Some(GatewayError::InvalidAPIVersion), + "invalid intent(s)" | "invalid intent" | "4013" => Some(GatewayError::InvalidIntents), + "disallowed intent(s)" | "disallowed intents" | "4014" => { + Some(GatewayError::DisallowedIntents) + } + _ => None, + } + } + + /// Returns whether or not the message is an error + pub fn is_error(&self) -> bool { + self.error().is_some() + } + + /// Parses the message as a payload; + /// Returns a result of deserializing + pub fn payload(&self) -> Result { + return serde_json::from_str(self.message.to_text().unwrap()); + } + + /// Returns whether or not the message is a payload + pub fn is_payload(&self) -> bool { + // close messages are never payloads, payloads are only text messages + if self.message.is_close() | !self.message.is_text() { + return false; + } + + return self.payload().is_ok(); + } + + /// Returns whether or not the message is empty + pub fn is_empty(&self) -> bool { + self.message.is_empty() + } +} diff --git a/src/gateway/mod.rs b/src/gateway/mod.rs new file mode 100644 index 0000000..fc0e21d --- /dev/null +++ b/src/gateway/mod.rs @@ -0,0 +1,190 @@ +pub mod gateway; +pub mod handle; +pub mod heartbeat; +pub mod message; + +pub use gateway::*; +pub use handle::*; +use heartbeat::*; +pub use message::*; + +use crate::errors::GatewayError; +use crate::types::{Snowflake, WebSocketEvent}; + +use async_trait::async_trait; +use std::any::Any; +use std::collections::HashMap; +use std::fmt::Debug; +use std::sync::{Arc, RwLock}; +use std::time::Duration; +use tokio::time::sleep_until; + +use futures_util::stream::SplitSink; +use futures_util::stream::SplitStream; +use futures_util::SinkExt; +use futures_util::StreamExt; +use log::{info, trace, warn}; +use tokio::net::TcpStream; +use tokio::sync::mpsc::Sender; +use tokio::sync::Mutex; +use tokio::task; +use tokio::task::JoinHandle; +use tokio::time; +use tokio::time::Instant; +use tokio_tungstenite::MaybeTlsStream; +use tokio_tungstenite::{connect_async_tls_with_config, Connector, WebSocketStream}; + +// Gateway opcodes +/// Opcode received when the server dispatches a [crate::types::WebSocketEvent] +const GATEWAY_DISPATCH: u8 = 0; +/// Opcode sent when sending a heartbeat +const GATEWAY_HEARTBEAT: u8 = 1; +/// Opcode sent to initiate a session +/// +/// See [types::GatewayIdentifyPayload] +const GATEWAY_IDENTIFY: u8 = 2; +/// Opcode sent to update our presence +/// +/// See [types::GatewayUpdatePresence] +const GATEWAY_UPDATE_PRESENCE: u8 = 3; +/// Opcode sent to update our state in vc +/// +/// Like muting, deafening, leaving, joining.. +/// +/// See [types::UpdateVoiceState] +const GATEWAY_UPDATE_VOICE_STATE: u8 = 4; +/// Opcode sent to resume a session +/// +/// See [types::GatewayResume] +const GATEWAY_RESUME: u8 = 6; +/// Opcode received to tell the client to reconnect +const GATEWAY_RECONNECT: u8 = 7; +/// Opcode sent to request guild member data +/// +/// See [types::GatewayRequestGuildMembers] +const GATEWAY_REQUEST_GUILD_MEMBERS: u8 = 8; +/// Opcode received to tell the client their token / session is invalid +const GATEWAY_INVALID_SESSION: u8 = 9; +/// Opcode received when initially connecting to the gateway, starts our heartbeat +/// +/// See [types::HelloData] +const GATEWAY_HELLO: u8 = 10; +/// Opcode received to acknowledge a heartbeat +const GATEWAY_HEARTBEAT_ACK: u8 = 11; +/// Opcode sent to get the voice state of users in a given DM/group channel +/// +/// See [types::CallSync] +const GATEWAY_CALL_SYNC: u8 = 13; +/// Opcode sent to get data for a server (Lazy Loading request) +/// +/// Sent by the official client when switching to a server +/// +/// See [types::LazyRequest] +const GATEWAY_LAZY_REQUEST: u8 = 14; + +/// The amount of time we wait for a heartbeat ack before resending our heartbeat in ms +const HEARTBEAT_ACK_TIMEOUT: u64 = 2000; + +pub type ObservableObject = dyn Send + Sync + Any; + +/// 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. +pub trait Updateable: 'static + Send + Sync { + fn id(&self) -> Snowflake; +} + +/// Trait which defines the behavior of an Observer. An Observer is an object which is subscribed to +/// an Observable. The Observer is notified when the Observable's data changes. +/// In this case, the Observable is a [`GatewayEvent`], which is a wrapper around a WebSocketEvent. +/// Note that `Debug` is used to tell `Observer`s apart when unsubscribing. +#[async_trait] +pub trait Observer: Sync + Send + std::fmt::Debug { + async fn update(&self, data: &T); +} + +/// GatewayEvent is a wrapper around a WebSocketEvent. It is used to notify the observers of a +/// change in the WebSocketEvent. GatewayEvents are observable. +#[derive(Default, Debug)] +pub struct GatewayEvent { + observers: Vec>>, +} + +impl GatewayEvent { + /// Returns true if the GatewayEvent is observed by at least one Observer. + pub fn is_observed(&self) -> bool { + !self.observers.is_empty() + } + + /// Subscribes an Observer to the GatewayEvent. + pub fn subscribe(&mut self, observable: Arc>) { + self.observers.push(observable); + } + + /// Unsubscribes an Observer from the GatewayEvent. + pub fn unsubscribe(&mut self, observable: &dyn Observer) { + // .retain()'s closure retains only those elements of the vector, which have a different + // pointer value than observable. + // The usage of the debug format to compare the generic T of observers is quite stupid, but the only thing to compare between them is T and if T == T they are the same + // anddd there is no way to do that without using format + let to_remove = format!("{:?}", observable); + self.observers + .retain(|obs| format!("{:?}", obs) != to_remove); + } + + /// Notifies the observers of the GatewayEvent. + async fn notify(&self, new_event_data: T) { + for observer in &self.observers { + observer.update(&new_event_data).await; + } + } +} + +#[cfg(test)] +mod example { + use crate::types; + + use super::*; + use std::sync::atomic::{AtomicI32, Ordering::Relaxed}; + + #[derive(Debug)] + struct Consumer { + _name: String, + events_received: AtomicI32, + } + + #[async_trait] + impl Observer for Consumer { + async fn update(&self, _data: &types::GatewayResume) { + self.events_received.fetch_add(1, Relaxed); + } + } + + #[tokio::test] + async fn test_observer_behavior() { + let mut event = GatewayEvent::default(); + + let new_data = types::GatewayResume { + token: "token_3276ha37am3".to_string(), + session_id: "89346671230".to_string(), + seq: "3".to_string(), + }; + + let consumer = Arc::new(Consumer { + _name: "first".into(), + events_received: 0.into(), + }); + event.subscribe(consumer.clone()); + + let second_consumer = Arc::new(Consumer { + _name: "second".into(), + events_received: 0.into(), + }); + event.subscribe(second_consumer.clone()); + + event.notify(new_data.clone()).await; + event.unsubscribe(&*consumer); + event.notify(new_data).await; + + assert_eq!(consumer.events_received.load(Relaxed), 1); + assert_eq!(second_consumer.events_received.load(Relaxed), 2); + } +} From 5243eee256e1573aa0625370f774004870b978dc Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Tue, 14 Nov 2023 11:13:21 +0100 Subject: [PATCH 10/16] Remove redundant `NSFWLevel` structdef --- src/types/entities/invite.rs | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/src/types/entities/invite.rs b/src/types/entities/invite.rs index 842db3f..d08ad1d 100644 --- a/src/types/entities/invite.rs +++ b/src/types/entities/invite.rs @@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize}; use crate::types::{Snowflake, WelcomeScreenObject}; use super::guild::GuildScheduledEvent; -use super::{Application, Channel, GuildMember, User}; +use super::{Application, Channel, GuildMember, NSFWLevel, User}; /// Represents a code that when used, adds a user to a guild or group DM channel, or creates a relationship between two users. /// See @@ -56,17 +56,6 @@ pub struct InviteGuild { pub welcome_screen: Option, } -/// See for an explanation on what -/// the levels mean. -#[derive(Debug, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "SCREAMING_SNAKE_CASE")] -pub enum NSFWLevel { - Default = 0, - Explicit = 1, - Safe = 2, - AgeRestricted = 3, -} - /// See #[derive(Debug, Serialize, Deserialize)] pub struct InviteStageInstance { From 564f5cb270eff7b8112ea7e9e93068144fba969b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 14 Nov 2023 10:22:09 +0000 Subject: [PATCH 11/16] Bump tungstenite from 0.20.0 to 0.20.1 Bumps [tungstenite](https://github.com/snapview/tungstenite-rs) from 0.20.0 to 0.20.1. - [Changelog](https://github.com/snapview/tungstenite-rs/blob/master/CHANGELOG.md) - [Commits](https://github.com/snapview/tungstenite-rs/compare/v0.20.0...v0.20.1) --- updated-dependencies: - dependency-name: tungstenite dependency-type: indirect ... Signed-off-by: dependabot[bot] --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 20c2671..a9208f2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2438,9 +2438,9 @@ checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" [[package]] name = "tungstenite" -version = "0.20.0" +version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e862a1c4128df0112ab625f55cd5c934bcb4312ba80b39ae4b4835a3fd58e649" +checksum = "9e3dac10fd62eaf6617d3a904ae222845979aec67c615d1c842b4002c7666fb9" dependencies = [ "byteorder", "bytes", From 5e8fe3d24b4616928c7595cfbe05e332ff736ea4 Mon Sep 17 00:00:00 2001 From: Flori <39242991+bitfl0wer@users.noreply.github.com> Date: Tue, 14 Nov 2023 12:33:12 +0100 Subject: [PATCH 12/16] Manually bump tungstenite to 0.20.1 in cargo.toml --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index c7ae062..ebdfb4a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,7 +27,7 @@ url = "2.4.0" chrono = { version = "0.4.26", features = ["serde"] } regex = "1.9.4" custom_error = "1.9.2" -tokio-tungstenite = { version = "0.20.0", features = [ +tokio-tungstenite = { version = "0.20.1", features = [ "rustls-tls-native-roots", "rustls-native-certs", ] } From 318007d1e7cb50b0618fe9edcdc23b2da1413227 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 14 Nov 2023 11:35:55 +0000 Subject: [PATCH 13/16] Bump rustix from 0.38.11 to 0.38.22 Bumps [rustix](https://github.com/bytecodealliance/rustix) from 0.38.11 to 0.38.22. - [Release notes](https://github.com/bytecodealliance/rustix/releases) - [Commits](https://github.com/bytecodealliance/rustix/compare/v0.38.11...v0.38.22) --- updated-dependencies: - dependency-name: rustix dependency-type: indirect ... Signed-off-by: dependabot[bot] --- Cargo.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 20c2671..69dea7a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -998,9 +998,9 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.4.5" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57bcfdad1b858c2db7c38303a6d2ad4dfaf5eb53dfeb0910128b2c26d6158503" +checksum = "969488b55f8ac402214f3f5fd243ebb7206cf82de60d3172994707a4bcc2b829" [[package]] name = "lock_api" @@ -1604,9 +1604,9 @@ checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" [[package]] name = "rustix" -version = "0.38.11" +version = "0.38.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0c3dde1fc030af041adc40e79c0e7fbcf431dd24870053d187d7c66e4b87453" +checksum = "80109a168d9bc0c7f483083244543a6eb0dba02295d33ca268145e6190d6df0c" dependencies = [ "bitflags 2.4.0", "errno", From 32b163a4c71da2347ad0fcd5ecd4989e19daca79 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Tue, 14 Nov 2023 15:43:08 +0100 Subject: [PATCH 14/16] Move HEARTBEAT_ACK_TIMEOUT Delete events.rs Unify import Move HEARTBEAT_ACK_TIMEOUT since its not an OPCODE --- src/gateway/events.rs | 1 - src/gateway/gateway.rs | 1 - src/gateway/handle.rs | 3 +-- src/gateway/heartbeat.rs | 3 +++ src/gateway/mod.rs | 3 --- 5 files changed, 4 insertions(+), 7 deletions(-) delete mode 100644 src/gateway/events.rs diff --git a/src/gateway/events.rs b/src/gateway/events.rs deleted file mode 100644 index 8b13789..0000000 --- a/src/gateway/events.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/gateway/gateway.rs b/src/gateway/gateway.rs index 026f92f..30d0610 100644 --- a/src/gateway/gateway.rs +++ b/src/gateway/gateway.rs @@ -1,5 +1,4 @@ use self::event::Events; - use super::*; use crate::types::{ self, AutoModerationRule, AutoModerationRuleUpdate, Channel, ChannelCreate, ChannelDelete, diff --git a/src/gateway/handle.rs b/src/gateway/handle.rs index 4e21f2b..1200b30 100644 --- a/src/gateway/handle.rs +++ b/src/gateway/handle.rs @@ -1,5 +1,4 @@ -use super::event::Events; -use super::*; +use super::{event::Events, *}; use crate::types::{self, Composite}; /// Represents a handle to a Gateway connection. A Gateway connection will create observable diff --git a/src/gateway/heartbeat.rs b/src/gateway/heartbeat.rs index 2f443c8..dd162b7 100644 --- a/src/gateway/heartbeat.rs +++ b/src/gateway/heartbeat.rs @@ -2,6 +2,9 @@ use crate::types; use super::*; +/// The amount of time we wait for a heartbeat ack before resending our heartbeat in ms +const HEARTBEAT_ACK_TIMEOUT: u64 = 2000; + /// Handles sending heartbeats to the gateway in another thread #[allow(dead_code)] // FIXME: Remove this, once HeartbeatHandler is used #[derive(Debug)] diff --git a/src/gateway/mod.rs b/src/gateway/mod.rs index fc0e21d..ebd06cc 100644 --- a/src/gateway/mod.rs +++ b/src/gateway/mod.rs @@ -82,9 +82,6 @@ const GATEWAY_CALL_SYNC: u8 = 13; /// See [types::LazyRequest] const GATEWAY_LAZY_REQUEST: u8 = 14; -/// The amount of time we wait for a heartbeat ack before resending our heartbeat in ms -const HEARTBEAT_ACK_TIMEOUT: u64 = 2000; - pub type ObservableObject = dyn Send + Sync + Any; /// 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. From e055d8f04c00d9efa8ab88ea1e3ad02e7d5dd43a Mon Sep 17 00:00:00 2001 From: Flori <39242991+bitfl0wer@users.noreply.github.com> Date: Tue, 14 Nov 2023 16:02:51 +0100 Subject: [PATCH 15/16] Bump version --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index ebdfb4a..33acc15 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "chorus" description = "A library for interacting with multiple Spacebar-compatible Instances at once." -version = "0.10.1" +version = "0.11.0" license = "AGPL-3.0" edition = "2021" repository = "https://github.com/polyphony-chat/chorus" From 7d8e980cc2c67e06d3ee03c027f7e0a9c9167a6a Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Tue, 14 Nov 2023 16:11:00 +0100 Subject: [PATCH 16/16] bump version --- Cargo.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index edb7696..576f2ec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -177,7 +177,7 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chorus" -version = "0.10.1" +version = "0.11.0" dependencies = [ "async-trait", "base64 0.21.3", @@ -2338,9 +2338,9 @@ dependencies = [ [[package]] name = "tokio-tungstenite" -version = "0.20.0" +version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b2dbec703c26b00d74844519606ef15d09a7d6857860f84ad223dec002ddea2" +checksum = "212d5dcb2a1ce06d81107c3d0ffa3121fe974b73f068c8282cb1c32328113b6c" dependencies = [ "futures-util", "log",