From 04d39297c763b15df7283cdc350047c619a3424d Mon Sep 17 00:00:00 2001 From: kozabrada123 Date: Sun, 29 Jun 2025 20:18:24 +0200 Subject: [PATCH 1/8] docs: make Events have any docs at all --- src/gateway/events.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/gateway/events.rs b/src/gateway/events.rs index dcd9b3e5..82062883 100644 --- a/src/gateway/events.rs +++ b/src/gateway/events.rs @@ -7,6 +7,11 @@ use pubserve::Publisher; use super::*; use crate::types; +/// Subscribable events the [Gateway] emits. +/// +/// Most of these are received via a websocket connection. +/// +/// Receiving a [GatewayError] from `error` means the connection was closed. #[derive(Default, Debug, Clone)] pub struct Events { pub application: Application, @@ -31,6 +36,13 @@ pub struct Events { pub error: Publisher, } +impl Events { + /// Returns a new [Events] struct with no subscribed observers + pub fn empty() -> Events { + Events::default() + } +} + #[derive(Default, Debug, Clone)] pub struct Application { pub command_permissions_update: Publisher, From f67ac561327b7491e208f62922915952aaa8bab4 Mon Sep 17 00:00:00 2001 From: kozabrada123 Date: Sun, 29 Jun 2025 20:18:53 +0200 Subject: [PATCH 2/8] feat!: add a builder pattern for Instance --- src/instance.rs | 360 +++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 297 insertions(+), 63 deletions(-) diff --git a/src/instance.rs b/src/instance.rs index 71c6adb3..34aa594d 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -14,7 +14,7 @@ use chrono::Utc; use reqwest::Client; use serde::{Deserialize, Serialize}; -use crate::errors::ChorusResult; +use crate::errors::{ChorusError, ChorusResult}; use crate::gateway::{events::Events, Gateway, GatewayHandle, GatewayOptions}; use crate::ratelimiter::ChorusRequest; use crate::types::types::subconfigs::limits::rates::RateLimits; @@ -24,6 +24,269 @@ use crate::types::{ }; use crate::UrlBundle; +/// A builder pattern type for [Instance] +#[derive(Debug, Clone, Default)] +pub struct InstanceBuilder { + /// The provided root URL, if any + /// + /// Usually set with [InstanceBuilder::new] + /// + /// One of this field or `urls` is required + pub root_url: Option, + + /// The provided full URLs, if any + /// + /// Usually set with [InstanceBuilder::from_url_bundle] + /// + /// One of this field or `root_url` is required + pub urls: Option, + + /// The custom provided [InstanceSoftware] + /// + /// See [InstanceBuilder::with_software] + pub software: Option, + + /// The custom provided [GatewayOptions] + /// + /// See [InstanceBuilder::with_gateway_options] + pub gateway_options: Option, + + /// Whether or not to skip trying to fetch the instance's ratelimit configuration. + /// + /// `false` by default. + /// + /// See [InstanceBuilder::skip_fetching_ratelimits] + pub should_skip_fetching_ratelimits: bool, + + /// Whether or not to skip trying to fetch the instance's general configuration (which contains + /// general information about the instance - see [GeneralConfiguration]). + /// + /// `false` by default. + /// + /// See [InstanceBuilder::skip_fetching_general_info] + pub should_skip_fetching_general_info: bool, + + /// The default gateway [`Events`] new gateway connections will inherit. + /// + /// This field can be used to subscribe to events that are received before we get access to the + /// gateway handle object on new [ChorusUser]s created with [Instance::login_account], + /// [Instance::login_with_token] and [Instance::register_account] + /// + /// You should subscribe your [`Error`](crate::errors::GatewayError) and [`Ready`](crate::types::GatewayReady) observers here, as well as any observers you want to receive from all connections. + pub default_gateway_events: Events, +} + +impl InstanceBuilder { + /// Creates an [`InstanceBuilder`] by providing only the root url of the [`Instance`]. + /// + /// Once you have set all the options you want, use [InstanceBuilder::build()]. + /// + /// When the [`Instance`] is built, it will try to automatically discover the remaining urls. + /// + /// Note that some [`Instance`]s don't support this. If that is the case, you will need to use + /// [InstanceBuilder::from_url_bundle] and provide the remaining urls manually. + pub fn new(root_url: String) -> InstanceBuilder { + InstanceBuilder { + root_url: Some(root_url.to_string()), + ..Default::default() + } + } + + /// Creates an [`InstanceBuilder`] by providing a [full set of URLs](UrlBundle). + /// + /// Once you have set all the options you want, use [InstanceBuilder::build()]. + /// + /// This is equivalent to [InstanceBuilder::new] and should be used if that method cannot + /// automatically find all the urls. + pub fn from_url_bundle(urls: UrlBundle) -> InstanceBuilder { + InstanceBuilder { + urls: Some(urls), + ..Default::default() + } + } + + /// Creates an [`InstanceBuilder`] by providing a [full set of URLs](UrlBundle). + /// + /// Once you have set all the options you want, use [InstanceBuilder::build()]. + /// + /// This is equivalent to [InstanceBuilder::new] and should be used if that method cannot + /// automatically find all the urls. + /// + /// Alias of [InstanceBuilder::from_url_bundle] + pub fn from_urls(urls: UrlBundle) -> InstanceBuilder { + Self::from_url_bundle(urls) + } + + /// Manually specifies the type of software the [`Instance`] is running. + /// + /// This should only be used if you're 100% sure, as setting it wrongly can cause + /// (de)serialization errors or undefined behaviours. + /// + /// Normally we'll ping a few endpoints to discover it automatically, but this + /// can reveal things about the client you may not want to. + /// + /// (To also skip other optional requests that may reveal too much, see + /// [Self::skip_optional_requests]) + /// + /// See [`InstanceSoftware`] for possible values + pub fn with_software(self, software: InstanceSoftware) -> InstanceBuilder { + let mut s = self; + s.software = Some(software); + s + } + + /// Manually sets the [`GatewayOptions`] the instance will use when spawning new connections + /// (when logging in and registering new accounts). + /// + /// These options impact the low-level workings of the gateway, such as the encoding and + /// compression method used. + /// + /// They are heavily dependent on what the instance supports and therefore the instance + /// [software](InstanceSoftware). + /// + /// They are usually optimally set automatically, but setting them manually may help for compatibility or development purposes. + /// + /// See [`GatewayOptions`] for possible values + pub fn with_gateway_options(self, options: GatewayOptions) -> InstanceBuilder { + let mut s = self; + s.gateway_options = Some(options); + s + } + + /// Sets whether or not to skip trying to fetch the instance's ratelimit configuration. + /// + /// `false` by default. + /// + /// You may consider setting this to `true` if you know your instance does not + /// have those endpoints and you want to avoid making the extra requests. + pub fn skip_fetching_ratelimits(self, should_skip: bool) -> InstanceBuilder { + let mut s = self; + s.should_skip_fetching_ratelimits = should_skip; + s + } + + /// Sets whether or not to skip trying to fetch the instance's general configuration (which contains + /// general information about the instance - see [GeneralConfiguration]). + /// + /// `false` by default. + /// + /// You may consider setting this to `true` if you know your instance does not + /// have those endpoints and you want to avoid making the extra requests. + pub fn skip_fetching_general_info(self, should_skip: bool) -> InstanceBuilder { + let mut s = self; + s.should_skip_fetching_general_info = should_skip; + s + } + + /// Sets whether or not to skip all optional requests when initalizing the instance. + /// + /// These requests are used to fetch info about the instance, such as its ratelimit + /// configuration and its name, description, tos page, ... (if those are publically + /// accessible). + /// + /// Note that even if this is set to `true`, certain requests may be performed to determine the + /// instance's [software](InstanceSoftware). This can be skipped by setting in manually using + /// [Self::with_software]. + /// + /// This method sets both [Self::skip_fetching_ratelimits] and [Self::skip_fetching_general_info]. + /// + /// `false` by default. + /// + /// You may consider setting this to `true` if you know your instance does not + /// have those endpoints and you want to avoid making the extra requests. + pub fn skip_optional_requests(self, should_skip: bool) -> InstanceBuilder { + self.skip_fetching_ratelimits(should_skip) + .skip_fetching_general_info(should_skip) + } + + /// Tries to build an [Instance] from the data provided to the builder. + /// + /// Requires one of `root_url` ([InstanceBuilder::new]) or `urls` + /// ([InstanceBuilder::from_urls]) to be provided. + /// + /// Note that it's recommended to store the resulting [Instance] + /// in the [Shared] wrapper (using `.into_shared()`). + pub async fn build(self) -> ChorusResult { + let urls; + + if let Some(url_bundle) = self.urls { + urls = url_bundle; + } else if let Some(root_url) = self.root_url { + log::trace!("Discovering instance URLs from root URL.."); + urls = UrlBundle::from_root_url(&root_url).await?; + } else { + return ChorusResult::Err(ChorusError::InvalidArguments { error: format!("One of root_url or urls is required. See InstanceBuilder::new or InstanceBuilder::from_urls") }); + } + + let limits_information; + + if self.should_skip_fetching_ratelimits { + log::trace!("Skipping instance ratelimit info fetch.."); + limits_information = None; + } else { + limits_information = match Instance::is_limited(&urls.api).await? { + Some(limits_configuration) => { + let limits = + ChorusRequest::limits_config_to_hashmap(&limits_configuration.rate); + + Some(LimitsInformation { + ratelimits: limits, + configuration: limits_configuration.rate, + }) + } + None => None, + }; + } + + // Create the object, so we can have potentially ratelimited requests + let mut instance = Instance { + client: Client::new(), + urls, + limits_information, + default_gateway_events: self.default_gateway_events, + + // Will all be overwritten soon + instance_info: GeneralConfiguration::default(), + gateway_options: GatewayOptions::default(), + software: InstanceSoftware::Other, + }; + + if self.should_skip_fetching_general_info { + log::trace!("Skipping general instance info fetch.."); + } else { + match instance.general_configuration_schema().await { + Ok(info) => instance.instance_info = info, + Err(e) => { + log::warn!("Could not get instance configuration schema: {}", e); + } + }; + } + + if let Some(manual_software) = self.software { + instance.software = manual_software; + log::trace!("Instance software manually set to {:?}", instance.software); + } else { + instance.software = instance.detect_software().await; + log::debug!( + "Instance software automatically detected as {:?}", + instance.software + ); + } + + if let Some(manual_options) = self.gateway_options { + instance.gateway_options = manual_options; + log::trace!("Instance gateway options manually set.."); + } else { + instance.gateway_options = GatewayOptions::for_instance_software(instance.software()); + log::trace!("Instance gateway options automatically set based off instance software."); + } + + log::trace!("Instance successfully built!"); + + Ok(instance) + } +} + #[derive(Debug, Clone, Default, Serialize, Deserialize)] /// Represents a Spacebar-compatible [`Instance`]. /// @@ -97,69 +360,40 @@ impl Instance { None } - /// Creates a new [`Instance`] from the [relevant instance urls](UrlBundle). - /// - /// If `options` is `None`, the default [`GatewayOptions`] will be used. - /// - /// To create an Instance from one singular url, use [`Instance::new()`]. - // Note: maybe make this just take urls and then add another method which creates an instance - // from urls and custom gateway options, since gateway options will be automatically generated? - pub async fn from_url_bundle( - urls: UrlBundle, - options: Option, - ) -> ChorusResult { - let is_limited: Option = Instance::is_limited(&urls.api).await?; - let limit_information; - - if let Some(limits_configuration) = is_limited { - let limits = ChorusRequest::limits_config_to_hashmap(&limits_configuration.rate); - limit_information = Some(LimitsInformation { - ratelimits: limits, - configuration: limits_configuration.rate, - }); - } else { - limit_information = None - } - - let mut instance = Instance { - urls: urls.clone(), - // Will be overwritten in the next step - instance_info: GeneralConfiguration::default(), - limits_information: limit_information, - client: Client::new(), - gateway_options: options.unwrap_or_default(), - // Will also be detected soon - software: InstanceSoftware::Other, - default_gateway_events: Events::default(), - }; - - instance.instance_info = match instance.general_configuration_schema().await { - Ok(schema) => schema, - Err(e) => { - log::warn!("Could not get instance configuration schema: {}", e); - GeneralConfiguration::default() - } - }; - - instance.software = instance.detect_software().await; - - if options.is_none() { - instance.gateway_options = GatewayOptions::for_instance_software(instance.software()); - } - - Ok(instance) + /// Creates a new [`Instance`] from only the [relevant instance urls](UrlBundle). + /// + /// Equivalent to doing + /// + /// ```no_run + /// # let urls = UrlBundle::new("", "", "", ""); + /// InstanceBuilder::from_url_bundle(urls).build().await + /// ``` + /// + /// If you need to set more options, use [InstanceBuilder]. + /// + /// To create an [Instance] from one singular url, use [`Instance::new()`] or + /// [InstanceBuilder::new]. + pub async fn from_url_bundle(urls: UrlBundle) -> ChorusResult { + InstanceBuilder::from_url_bundle(urls).build().await } /// Creates a new [`Instance`] by trying to get the [relevant instance urls](UrlBundle) from a root url. /// - /// If `options` is `None`, the default [`GatewayOptions`] will be used. + /// Equivalent to doing + /// + /// ```no_run + /// # let root_url = ""; + /// InstanceBuilder::new(root_url).build().await + /// ``` /// - /// Shorthand for `Instance::from_url_bundle(UrlBundle::from_root_domain(root_domain).await?)`. - pub async fn new(root_url: &str, options: Option) -> ChorusResult { - let urls = UrlBundle::from_root_url(root_url).await?; - Instance::from_url_bundle(urls, options).await + /// If you need to set more options, use [InstanceBuilder]. + pub async fn new(root_url: &str) -> ChorusResult { + InstanceBuilder::new(root_url.to_string()).build().await } + /// Tries to fetch the instance's ratelimits information + /// + /// Only supported on [InstanceSoftware::Symfonia] and [InstanceSoftware::SpacebarTypescript] pub async fn is_limited(api_url: &str) -> ChorusResult> { let api_url = UrlBundle::parse_url(api_url); let client = Client::new(); @@ -206,7 +440,7 @@ impl Instance { self.gateway_options } - /// Manually sets the [`GatewayOptions`] the instance should use when spawning new connections. + /// Manually sets the default [`GatewayOptions`] the instance should use when spawning new connections. /// /// These options are used on the gateways created when logging in and registering. pub fn set_gateway_options(&mut self, options: GatewayOptions) { @@ -220,11 +454,11 @@ impl Instance { /// Manually sets which [`InstanceSoftware`] the instance is running. /// - /// Note: you should only use this if you are absolutely sure about an instance (e. g. you run it). - /// If set to an incorrect value, this may cause unexpected errors or even undefined behaviours. + /// **Usage of this method is generally discouraged. This sets the software after + /// the [`Instance`] has already been built assuming a different value and will (!) + /// cause problems.** /// - /// Manually setting the software is generally discouraged. Chorus should automatically detect - /// which type of software the instance is running. + /// See [InstanceBuilder::with_software] for a safer way to do this. pub fn set_software(&mut self, software: InstanceSoftware) { self.software = software; } @@ -245,7 +479,7 @@ pub enum InstanceSoftware { /// We could not determine the instance software or it /// is one we don't specifically differentiate. /// - /// Assume it implements all features of the spacebar protocol. + /// Assume it implements all core features of the Spacebar API. #[default] Other, } From 59cb8ede99459f5cd85bb20c2bf79e612ae423de Mon Sep 17 00:00:00 2001 From: kozabrada123 Date: Sun, 29 Jun 2025 22:04:33 +0200 Subject: [PATCH 3/8] chore: update tests and examples with api changes --- README.md | 4 ++-- examples/instance.rs | 2 +- examples/login.rs | 2 +- src/lib.rs | 2 +- tests/common/mod.rs | 4 ++-- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 5457fee7..2bb782f9 100644 --- a/README.md +++ b/README.md @@ -64,10 +64,10 @@ async fn main() { // This instance will later need to be shared across threads and users, so we'll // store it inside of the `Shared` type (note the `into_shared()` method call) - let instance = Instance::new("https://example.com", None) + let instance = Instance::new("https://example.com") .await .expect("Failed to connect to the Spacebar server") - .into_shared(); + .into_shared(); // You can create as many instances of `Instance` as you want, but each `Instance` should likely be unique. diff --git a/examples/instance.rs b/examples/instance.rs index 27fc10ca..30b71f1b 100644 --- a/examples/instance.rs +++ b/examples/instance.rs @@ -8,7 +8,7 @@ use chorus::{instance::Instance, types::IntoShared}; async fn main() { // This instance will later need to be shared across threads and users, so we'll // store it inside of the `Shared` type (note the `into_shared()` method call) - let instance = Instance::new("https://example.com", None) + let instance = Instance::new("https://example.com") .await .expect("Failed to connect to the Spacebar server") .into_shared(); diff --git a/examples/login.rs b/examples/login.rs index 9eda06f5..72ed22a4 100644 --- a/examples/login.rs +++ b/examples/login.rs @@ -7,7 +7,7 @@ use chorus::types::{IntoShared, LoginSchema}; #[tokio::main(flavor = "current_thread")] async fn main() { - let instance = Instance::new("https://example.com/", None) + let instance = Instance::new("https:/le.com/") .await .expect("Failed to connect to the Spacebar server") .into_shared(); diff --git a/src/lib.rs b/src/lib.rs index fbb267ed..dc1fbd4f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -30,7 +30,7 @@ async fn main() { // This instance will later need to be shared across threads and users, so we'll // store it inside of the `Shared` type (note the `into_shared()` method call) - let instance = Instance::new(url, None) + let instance = Instance::new(url) .await .expect("Failed to connect to the Spacebar server") .into_shared(); diff --git a/tests/common/mod.rs b/tests/common/mod.rs index e9562d33..85433240 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -79,7 +79,7 @@ pub(crate) async fn setup() -> TestBundle { ) .init(); - let instance = Instance::new("http://localhost:3001/api", None) + let instance = Instance::new("http://localhost:3001/api") .await .unwrap() .into_shared(); @@ -158,7 +158,7 @@ pub(crate) async fn setup_with_mock_server(server: &httptest::Server) -> TestBun ) .init(); - let instance = Instance::new(server.url_str("/api").as_str(), None) + let instance = Instance::new(server.url_str("/api").as_str()) .await .unwrap() .into_shared(); From 94d33275ab8b179f08034bb8e392c53f6ad313e0 Mon Sep 17 00:00:00 2001 From: kozabrada123 Date: Sun, 29 Jun 2025 22:06:25 +0200 Subject: [PATCH 4/8] chore: clippy --- src/instance.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/instance.rs b/src/instance.rs index 34aa594d..3fd2ffe8 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -215,7 +215,7 @@ impl InstanceBuilder { log::trace!("Discovering instance URLs from root URL.."); urls = UrlBundle::from_root_url(&root_url).await?; } else { - return ChorusResult::Err(ChorusError::InvalidArguments { error: format!("One of root_url or urls is required. See InstanceBuilder::new or InstanceBuilder::from_urls") }); + return ChorusResult::Err(ChorusError::InvalidArguments { error: "One of root_url or urls is required. See InstanceBuilder::new or InstanceBuilder::from_urls".to_string() }); } let limits_information; @@ -257,7 +257,7 @@ impl InstanceBuilder { match instance.general_configuration_schema().await { Ok(info) => instance.instance_info = info, Err(e) => { - log::warn!("Could not get instance configuration schema: {}", e); + log::warn!("Could not get instance configuration schema: {e}"); } }; } From b42166d329bd28b8effc909420d1e7223742fd4a Mon Sep 17 00:00:00 2001 From: kozabrada123 Date: Sun, 29 Jun 2025 22:15:03 +0200 Subject: [PATCH 5/8] docs: add examples of InstanceBuilder note: not added to the readme yet, so people don't try it on older versions --- examples/instance.rs | 31 ++++++++++++++++++++++++++++++- examples/login.rs | 2 ++ src/lib.rs | 31 ++++++++++++++++++++++++++++++- 3 files changed, 62 insertions(+), 2 deletions(-) diff --git a/examples/instance.rs b/examples/instance.rs index 30b71f1b..3ec4daeb 100644 --- a/examples/instance.rs +++ b/examples/instance.rs @@ -2,7 +2,11 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. -use chorus::{instance::Instance, types::IntoShared}; +use chorus::{ + gateway::{GatewayEncoding, GatewayOptions, GatewayTransportCompression}, + instance::{Instance, InstanceBuilder, InstanceSoftware}, + types::IntoShared, +}; #[tokio::main(flavor = "current_thread")] async fn main() { @@ -21,4 +25,29 @@ async fn main() { dbg!(&instance_lock.instance_info); dbg!(&instance_lock.limits_information); + + // The above way is the easiest to create an instance, but you may want more options + // + // To do so, you can use InstanceBuilder: + let instance = InstanceBuilder::new("https://other-example.com".to_string()) + // Customize how our gateway connections will be made + .with_gateway_options(GatewayOptions { + encoding: GatewayEncoding::Json, + + // Explicitly disables Gateway compression, if we want to + transport_compression: GatewayTransportCompression::None, + }) + // Skip fetching ratelimits and instance info, we know we our sever doesn't support that + .skip_optional_requests(true) + // Skip automatically detecting the software, we know which it is + .with_software(InstanceSoftware::Other) + // Once we're ready we call build + .build() + .await + .expect("Failed to connect to the Spacebar server") + .into_shared(); + + let instance_lock = instance.read().unwrap(); + dbg!(&instance_lock.instance_info); + dbg!(&instance_lock.limits_information); } diff --git a/examples/login.rs b/examples/login.rs index 72ed22a4..09edecbc 100644 --- a/examples/login.rs +++ b/examples/login.rs @@ -11,6 +11,7 @@ async fn main() { .await .expect("Failed to connect to the Spacebar server") .into_shared(); + // 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 { @@ -18,6 +19,7 @@ async fn main() { password: "Correct-Horse-Battery-Staple".to_string(), ..Default::default() }; + // Each user connects to the Gateway. Each users' Gateway connection lives on a separate thread. Depending on // the runtime feature you choose, this can potentially take advantage of all of your computers' threads. // diff --git a/src/lib.rs b/src/lib.rs index dc1fbd4f..efbbecc7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -21,7 +21,11 @@ instead of worrying about the underlying implementation details. To connect to a Polyphony/Spacebar compatible server, you'll need to create an [`Instance`](https://docs.rs/chorus/latest/chorus/instance/struct.Instance.html) like this: ```rust -use chorus::{instance::Instance, types::IntoShared}; +use chorus::{ + gateway::{GatewayEncoding, GatewayOptions, GatewayTransportCompression}, + instance::{Instance, InstanceBuilder, InstanceSoftware}, + types::IntoShared, +}; #[tokio::main] async fn main() { @@ -43,6 +47,31 @@ async fn main() { dbg!(&instance_lock.instance_info); dbg!(&instance_lock.limits_information); + + // The above way is the easiest to create an instance, but you may want more options + // + // To do so, you can use InstanceBuilder: + let instance = InstanceBuilder::new("https://other-example.com".to_string()) + // Customize how our gateway connections will be made + .with_gateway_options(GatewayOptions { + encoding: GatewayEncoding::Json, + + // Explicitly disables Gateway compression, if we want to + transport_compression: GatewayTransportCompression::None, + }) + // Skip fetching ratelimits and instance info, we know we our sever doesn't support that + .skip_optional_requests(true) + // Skip automatically detecting the software, we know which it is + .with_software(InstanceSoftware::Other) + // Once we're ready we call build + .build() + .await + .expect("Failed to connect to the Spacebar server") + .into_shared(); + + let instance_lock = instance.read().unwrap(); + dbg!(&instance_lock.instance_info); + dbg!(&instance_lock.limits_information); } ``` From 5b365e2d67160bb1ecd371773ba6ab74d2f83536 Mon Sep 17 00:00:00 2001 From: kozabrada123 Date: Mon, 30 Jun 2025 08:00:05 +0200 Subject: [PATCH 6/8] feat: add default_client_properties to Instance & InstanceBuilder - allows settings client properties that are used in Instance requests - these are also passed down to new ChorusUsers --- src/api/auth/login.rs | 19 ++++-- src/api/auth/register.rs | 8 ++- src/api/instance.rs | 6 +- src/api/policies/instance/instance.rs | 3 +- src/instance.rs | 96 ++++++++++++++++----------- src/ratelimiter.rs | 7 +- src/types/entities/ratelimits.rs | 1 + 7 files changed, 91 insertions(+), 49 deletions(-) diff --git a/src/api/auth/login.rs b/src/api/auth/login.rs index d54691a2..d74fd1a4 100644 --- a/src/api/auth/login.rs +++ b/src/api/auth/login.rs @@ -26,13 +26,17 @@ impl Instance { instance: Shared, login_schema: LoginSchema, ) -> ChorusResult { - let endpoint_url = instance.read().unwrap().urls.api.clone() + "/auth/login"; + let instance_read = instance.read().unwrap(); + + let endpoint_url = instance_read.urls.api.clone() + "/auth/login"; let chorus_request = ChorusRequest { request: Client::new().post(endpoint_url).json(&login_schema), limit_type: LimitType::AuthLogin, } // Note: yes, this is still sent even for login and register - .with_client_properties(&ClientProperties::default()); + .with_client_properties(&instance_read.default_client_properties); + + drop(instance_read); // 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 @@ -58,15 +62,19 @@ impl Instance { authenticator: MfaAuthenticationType, schema: VerifyMFALoginSchema, ) -> ChorusResult { + let instance_read = instance.read().unwrap(); + let endpoint_url = - instance.read().unwrap().urls.api.clone() + "/auth/mfa/" + &authenticator.to_string(); + instance_read.urls.api.clone() + "/auth/mfa/" + &authenticator.to_string(); let chorus_request = ChorusRequest { request: Client::new().post(endpoint_url).json(&schema), limit_type: LimitType::AuthLogin, } // Note: yes, this is still sent even for login and register - .with_client_properties(&ClientProperties::default()); + .with_client_properties(&instance_read.default_client_properties); + + drop(instance_read); let mut user = ChorusUser::shell(instance, "None").await; @@ -108,7 +116,8 @@ impl Instance { .header("Content-Type", "application/json") .json(&schema), limit_type: LimitType::Ip, - }; + } + .with_client_properties(&self.default_client_properties); let send_mfa_sms_response = chorus_request .send_anonymous_and_deserialize_response::(self) diff --git a/src/api/auth/register.rs b/src/api/auth/register.rs index 2759344b..1187f234 100644 --- a/src/api/auth/register.rs +++ b/src/api/auth/register.rs @@ -26,13 +26,17 @@ impl Instance { instance: Shared, register_schema: RegisterSchema, ) -> ChorusResult { - let endpoint_url = instance.read().unwrap().urls.api.clone() + "/auth/register"; + let instance_read = instance.read().unwrap(); + + let endpoint_url = instance_read.urls.api.clone() + "/auth/register"; let chorus_request = ChorusRequest { request: Client::new().post(endpoint_url).json(®ister_schema), limit_type: LimitType::AuthRegister, } // Note: yes, this is still sent even for login and register - .with_client_properties(&ClientProperties::default()); + .with_client_properties(&instance_read.default_client_properties); + + drop(instance_read); // 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 diff --git a/src/api/instance.rs b/src/api/instance.rs index c5a00140..74f53d47 100644 --- a/src/api/instance.rs +++ b/src/api/instance.rs @@ -27,7 +27,8 @@ impl Instance { let chorus_request = ChorusRequest { request: Client::new().get(url), limit_type: LimitType::Global, - }; + } + .with_client_properties(&self.default_client_properties); chorus_request .send_anonymous_and_deserialize_response(self) @@ -46,7 +47,8 @@ impl Instance { let chorus_request = ChorusRequest { request: Client::new().get(url.clone()), limit_type: LimitType::Global, - }; + } + .with_client_properties(&self.default_client_properties); chorus_request .send_anonymous_and_deserialize_response(self) diff --git a/src/api/policies/instance/instance.rs b/src/api/policies/instance/instance.rs index c4fea72e..fef649bb 100644 --- a/src/api/policies/instance/instance.rs +++ b/src/api/policies/instance/instance.rs @@ -23,7 +23,8 @@ impl Instance { let chorus_request = ChorusRequest { request: self.client.get(&url), limit_type: LimitType::Global, - }; + } + .with_client_properties(&self.default_client_properties); chorus_request .send_anonymous_and_deserialize_response(self) diff --git a/src/instance.rs b/src/instance.rs index 3fd2ffe8..143a32fc 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -51,6 +51,13 @@ pub struct InstanceBuilder { /// See [InstanceBuilder::with_gateway_options] pub gateway_options: Option, + /// Custom provided [ClientProperties] (telemetry data), if any + /// + /// These will be used in the instance's requests, and will be inherited by new [ChorusUser]s. + /// + /// See [InstanceBuilder::with_client_properties] + pub default_client_properties: Option, + /// Whether or not to skip trying to fetch the instance's ratelimit configuration. /// /// `false` by default. @@ -153,6 +160,17 @@ impl InstanceBuilder { s } + /// Manually sets the instance's [ClientProperties] (telemetry data) + /// + /// These will be used in the instance's requests, and will be inherited by new [ChorusUser]s. + /// + /// See [`ClientProperties`] + pub fn with_client_properties(self, properties: ClientProperties) -> InstanceBuilder { + let mut s = self; + s.default_client_properties = Some(properties); + s + } + /// Sets whether or not to skip trying to fetch the instance's ratelimit configuration. /// /// `false` by default. @@ -218,13 +236,24 @@ impl InstanceBuilder { return ChorusResult::Err(ChorusError::InvalidArguments { error: "One of root_url or urls is required. See InstanceBuilder::new or InstanceBuilder::from_urls".to_string() }); } - let limits_information; + // Create the object, so we can send ChorusRequests + let mut instance = Instance { + client: Client::new(), + urls, + default_gateway_events: self.default_gateway_events, + default_client_properties: self.default_client_properties.unwrap_or_default(), + + // Will all be overwritten soon + limits_information: None, + instance_info: GeneralConfiguration::default(), + gateway_options: GatewayOptions::default(), + software: InstanceSoftware::Other, + }; if self.should_skip_fetching_ratelimits { log::trace!("Skipping instance ratelimit info fetch.."); - limits_information = None; } else { - limits_information = match Instance::is_limited(&urls.api).await? { + instance.limits_information = match instance.is_limited().await? { Some(limits_configuration) => { let limits = ChorusRequest::limits_config_to_hashmap(&limits_configuration.rate); @@ -238,19 +267,6 @@ impl InstanceBuilder { }; } - // Create the object, so we can have potentially ratelimited requests - let mut instance = Instance { - client: Client::new(), - urls, - limits_information, - default_gateway_events: self.default_gateway_events, - - // Will all be overwritten soon - instance_info: GeneralConfiguration::default(), - gateway_options: GatewayOptions::default(), - software: InstanceSoftware::Other, - }; - if self.should_skip_fetching_general_info { log::trace!("Skipping general instance info fetch.."); } else { @@ -325,6 +341,13 @@ pub struct Instance { /// gateway handle object on new [ChorusUser]s created with [Instance::login_account], /// [Instance::login_with_token] and [Instance::register_account] pub default_gateway_events: Events, + + #[serde(skip)] + /// The default [ClientProperties] (telemetry data) that the instance + /// uses for its requests and new [ChorusUser]s inherit. + /// + /// See [ClientProperties] for more info. + pub default_client_properties: ClientProperties, } #[derive(Debug, Clone, Serialize, Deserialize, Default, Eq)] @@ -394,20 +417,21 @@ impl Instance { /// Tries to fetch the instance's ratelimits information /// /// Only supported on [InstanceSoftware::Symfonia] and [InstanceSoftware::SpacebarTypescript] - pub async fn is_limited(api_url: &str) -> ChorusResult> { - let api_url = UrlBundle::parse_url(api_url); - let client = Client::new(); - let request = client - .get(format!("{}/policies/instance/limits", &api_url)) - .header(http::header::ACCEPT, "application/json") - .build()?; - let resp = match client.execute(request).await { - Ok(response) => response, + pub async fn is_limited(&mut self) -> ChorusResult> { + let request = ChorusRequest { + request: Client::new() + .get(format!("{}/policies/instance/limits", self.urls.api)) + .header(http::header::ACCEPT, "application/json"), + limit_type: LimitType::Global, + } + .with_client_properties(&self.default_client_properties); + + match request + .send_anonymous_and_deserialize_response::(self) + .await + { + Ok(limits) => return Ok(Some(limits)), Err(_) => return Ok(None), - }; - match resp.json::().await { - Ok(limits) => Ok(Some(limits)), - Err(_) => Ok(None), } } @@ -597,14 +621,12 @@ impl ChorusUser { ) -> ChorusResult<()> { self.token = token.clone(); - let instance_default_events = self - .belongs_to - .read() - .unwrap() - .default_gateway_events - .clone(); + let instance_read = self.belongs_to.read().unwrap(); + + *self.gateway.events.lock().await = instance_read.default_gateway_events.clone(); + self.client_properties = instance_read.default_client_properties.clone(); - *self.gateway.events.lock().await = instance_default_events; + drop(instance_read); let mut identify = GatewayIdentifyPayload::default_w_client_capabilities(); identify.token = token; @@ -660,7 +682,7 @@ impl ChorusUser { /// /// The JWT token expires after 5 minutes. /// - /// This route is usually used in response to [ChorusError::MfaRequired](crate::ChorusError::MfaRequired). + /// This route is usually used in response to [ChorusError::MfaRequired]. /// /// # Reference /// See diff --git a/src/ratelimiter.rs b/src/ratelimiter.rs index 373592e7..ec3e4b65 100644 --- a/src/ratelimiter.rs +++ b/src/ratelimiter.rs @@ -127,9 +127,12 @@ impl ChorusRequest { }); } - let request = self.request; + let mut request = self.request; - // TODO: maybe have a default Instance user agent? + request = request.header( + "User-Agent", + instance.default_client_properties.user_agent.clone().0, + ); let client = instance.client.clone(); let result = match client.execute(request.build().unwrap()).await { diff --git a/src/types/entities/ratelimits.rs b/src/types/entities/ratelimits.rs index 3d8885ac..f014b118 100644 --- a/src/types/entities/ratelimits.rs +++ b/src/types/entities/ratelimits.rs @@ -10,6 +10,7 @@ use crate::types::Snowflake; /// The different types of ratelimits that can be applied to a request. Includes "Baseline"-variants /// for when the Snowflake is not yet known. +/// /// See for more information. #[derive( Clone, Copy, Eq, PartialEq, Debug, Default, Hash, Serialize, Deserialize, PartialOrd, Ord, From ebf5d7ef292933a6ae9e018140ea39f29f2ac7da Mon Sep 17 00:00:00 2001 From: kozabrada123 Date: Tue, 22 Jul 2025 17:16:02 +0200 Subject: [PATCH 7/8] fix: guard across await point --- src/instance.rs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/instance.rs b/src/instance.rs index 143a32fc..dc4ce702 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -430,8 +430,8 @@ impl Instance { .send_anonymous_and_deserialize_response::(self) .await { - Ok(limits) => return Ok(Some(limits)), - Err(_) => return Ok(None), + Ok(limits) => Ok(Some(limits)), + Err(_) => Ok(None), } } @@ -622,12 +622,13 @@ impl ChorusUser { self.token = token.clone(); let instance_read = self.belongs_to.read().unwrap(); - - *self.gateway.events.lock().await = instance_read.default_gateway_events.clone(); - self.client_properties = instance_read.default_client_properties.clone(); - + let gateway_events = instance_read.default_gateway_events.clone(); + let client_properties = instance_read.default_client_properties.clone(); drop(instance_read); + *self.gateway.events.lock().await = gateway_events; + self.client_properties = client_properties; + let mut identify = GatewayIdentifyPayload::default_w_client_capabilities(); identify.token = token; identify.properties = self.client_properties.clone(); From a939ae5793bbe05499b7ef78b782dc505de8a51e Mon Sep 17 00:00:00 2001 From: kozabrada123 Date: Tue, 22 Jul 2025 17:35:10 +0200 Subject: [PATCH 8/8] fix: a few minor mistakes --- examples/login.rs | 2 +- src/instance.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/login.rs b/examples/login.rs index 09edecbc..cb34bf81 100644 --- a/examples/login.rs +++ b/examples/login.rs @@ -7,7 +7,7 @@ use chorus::types::{IntoShared, LoginSchema}; #[tokio::main(flavor = "current_thread")] async fn main() { - let instance = Instance::new("https:/le.com/") + let instance = Instance::new("https://example.com/") .await .expect("Failed to connect to the Spacebar server") .into_shared(); diff --git a/src/instance.rs b/src/instance.rs index dc4ce702..71d55cab 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -203,7 +203,7 @@ impl InstanceBuilder { /// accessible). /// /// Note that even if this is set to `true`, certain requests may be performed to determine the - /// instance's [software](InstanceSoftware). This can be skipped by setting in manually using + /// instance's [software](InstanceSoftware). This can be skipped by setting it manually using /// [Self::with_software]. /// /// This method sets both [Self::skip_fetching_ratelimits] and [Self::skip_fetching_general_info].