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..3ec4daeb 100644 --- a/examples/instance.rs +++ b/examples/instance.rs @@ -2,13 +2,17 @@ // 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() { // 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(); @@ -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 9eda06f5..cb34bf81 100644 --- a/examples/login.rs +++ b/examples/login.rs @@ -7,10 +7,11 @@ 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://example.com/") .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/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/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, diff --git a/src/instance.rs b/src/instance.rs index 71c6adb3..71d55cab 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,285 @@ 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, + + /// 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. + /// + /// 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 + } + + /// 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. + /// + /// 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 it 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: "One of root_url or urls is required. See InstanceBuilder::new or InstanceBuilder::from_urls".to_string() }); + } + + // 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.."); + } else { + instance.limits_information = match instance.is_limited().await? { + Some(limits_configuration) => { + let limits = + ChorusRequest::limits_config_to_hashmap(&limits_configuration.rate); + + Some(LimitsInformation { + ratelimits: limits, + configuration: limits_configuration.rate, + }) + } + None => None, + }; + } + + 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`]. /// @@ -62,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)] @@ -97,81 +383,53 @@ 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 /// - /// 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 + /// ```no_run + /// # let root_url = ""; + /// InstanceBuilder::new(root_url).build().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 } - 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, - Err(_) => return Ok(None), - }; - match resp.json::().await { + /// Tries to fetch the instance's ratelimits information + /// + /// Only supported on [InstanceSoftware::Symfonia] and [InstanceSoftware::SpacebarTypescript] + 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) => Ok(Some(limits)), Err(_) => Ok(None), } @@ -206,7 +464,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 +478,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 +503,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, } @@ -363,14 +621,13 @@ 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(); + 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 = instance_default_events; + *self.gateway.events.lock().await = gateway_events; + self.client_properties = client_properties; let mut identify = GatewayIdentifyPayload::default_w_client_capabilities(); identify.token = token; @@ -426,7 +683,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/lib.rs b/src/lib.rs index fbb267ed..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() { @@ -30,7 +34,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(); @@ -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); } ``` 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, 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();