From 678f0395884eb3bf82040616175eaf222a35482a Mon Sep 17 00:00:00 2001 From: kozabrada123 Date: Fri, 27 Sep 2024 18:38:42 +0200 Subject: [PATCH 1/4] Add InstanceOptions type --- src/gateway/options.rs | 19 ++++++++++ src/instance.rs | 83 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 100 insertions(+), 2 deletions(-) diff --git a/src/gateway/options.rs b/src/gateway/options.rs index 4ff6178b..b8ded327 100644 --- a/src/gateway/options.rs +++ b/src/gateway/options.rs @@ -2,6 +2,8 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. +use crate::instance::InstanceSoftware; + #[derive(Clone, PartialEq, Eq, Ord, PartialOrd, Debug, Default, Copy)] /// Options passed when initializing the gateway connection. /// @@ -22,6 +24,23 @@ pub struct GatewayOptions { } impl GatewayOptions { + /// Creates the ideal gateway options for an [InstanceSoftware], + /// based off which features it supports. + pub fn for_instance_software(software: InstanceSoftware) -> GatewayOptions { + // TODO: Support ETF + let encoding = GatewayEncoding::Json; + + let transport_compression = match software.supports_gateway_zlib() { + true => GatewayTransportCompression::ZLibStream, + false => GatewayTransportCompression::None, + }; + + GatewayOptions { + encoding, + transport_compression, + } + } + /// Adds the options to an existing gateway url /// /// Returns the new url diff --git a/src/instance.rs b/src/instance.rs index a8671e0a..64a74c4c 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -28,11 +28,12 @@ use crate::UrlBundle; pub struct Instance { pub urls: UrlBundle, pub instance_info: GeneralConfiguration, + pub(crate) software: InstanceSoftware, pub limits_information: Option, #[serde(skip)] pub client: Client, #[serde(skip)] - pub gateway_options: GatewayOptions, + pub(crate) gateway_options: GatewayOptions, } #[derive(Debug, Clone, Serialize, Deserialize, Default, Eq)] @@ -72,6 +73,8 @@ impl Instance { /// 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, @@ -88,6 +91,9 @@ impl Instance { } else { limit_information = None } + + let software = Instance::detect_software(&urls.api).await; + let mut instance = Instance { urls: urls.clone(), // Will be overwritten in the next step @@ -95,6 +101,7 @@ impl Instance { limits_information: limit_information, client: Client::new(), gateway_options: options.unwrap_or_default(), + software, }; instance.instance_info = match instance.general_configuration_schema().await { Ok(schema) => schema, @@ -133,12 +140,84 @@ impl Instance { } } - /// Sets the [`GatewayOptions`] the instance will use when spawning new connections. + /// Detects which [InstanceSoftware] the instance is running. + pub async fn detect_software(api_url: &str) -> InstanceSoftware { + // TODO: How? + // + // Spacebar has .well-known/spacebar, but that isn't on the API, but on the base + // which we aren't even guaranteed to have.. + InstanceSoftware::Other + } + + /// Returns the [`GatewayOptions`] the instance uses when spawning new connections. + /// + /// These options are used on the gateways created when logging in and registering. + pub fn gateway_options(&self) -> GatewayOptions { + self.gateway_options + } + + /// Manually sets the [`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) { self.gateway_options = options; } + + /// Returns which [`InstanceSoftware`] the instance is running. + pub fn software(&self) -> InstanceSoftware { + self.software + } + + /// 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. + /// + /// Manually setting the software is generally discouraged. Chorus should automatically detect + /// which type of software the instance is running. + pub fn set_software(&mut self, software: InstanceSoftware) { + self.software = software; + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] +/// The software implementation the spacebar-compatible instance is running. +/// +/// This is useful since some softwares may support additional features, +/// while other do not fully implement the api yet. +pub enum InstanceSoftware { + /// The official typescript Spacebar server, available + /// at + SpacebarTypescript, + /// The Polyphony server written in rust, available at + /// at + Symfonia, + /// 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. + #[default] + Other, +} + +impl InstanceSoftware { + /// Returns whether the software supports z-lib stream compression on the gateway + pub fn supports_gateway_zlib(self) -> bool { + match self { + InstanceSoftware::SpacebarTypescript => true, + InstanceSoftware::Symfonia => false, + InstanceSoftware::Other => true, + } + } + + /// Returns whether the software supports sending data in the Erlang external term format on the gateway + pub fn supports_gateway_etf(self) -> bool { + match self { + InstanceSoftware::SpacebarTypescript => true, + InstanceSoftware::Symfonia => false, + InstanceSoftware::Other => true, + } + } } #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] From 901ba371469f3ed088f8b368b7753d579cb1f4ac Mon Sep 17 00:00:00 2001 From: kozabrada123 Date: Sat, 28 Sep 2024 10:16:57 +0200 Subject: [PATCH 2/4] feat: add /ping and /version, implement software differentiation --- src/api/instance.rs | 104 +++++++++++++++++++++++++++++++++++ src/api/mod.rs | 2 + src/instance.rs | 41 +++++++++++--- src/types/schema/instance.rs | 84 ++++++++++++++++++++++++++++ src/types/schema/mod.rs | 4 +- tests/instance.rs | 14 +++++ 6 files changed, 240 insertions(+), 9 deletions(-) create mode 100644 src/api/instance.rs create mode 100644 src/types/schema/instance.rs diff --git a/src/api/instance.rs b/src/api/instance.rs new file mode 100644 index 00000000..14765d1e --- /dev/null +++ b/src/api/instance.rs @@ -0,0 +1,104 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +//! Contains miscellaneous api routes, such as /version and /ping +use serde_json::from_str; + +use crate::errors::{ChorusError, ChorusResult}; +use crate::instance::Instance; +use crate::types::{GeneralConfiguration, PingReturn, VersionReturn}; + +impl Instance { + /// Pings the instance, also fetches instance info. + /// + /// See: [PingReturn] + /// + /// # Notes + /// This is a Spacebar only endpoint. + /// + /// # Reference + /// See + pub async fn ping(&self) -> ChorusResult { + let endpoint_url = format!("{}/ping", self.urls.api.clone()); + + let request = match self.client.get(&endpoint_url).send().await { + Ok(result) => result, + Err(e) => { + return Err(ChorusError::RequestFailed { + url: endpoint_url, + error: e.to_string(), + }); + } + }; + + if !request.status().as_str().starts_with('2') { + return Err(ChorusError::ReceivedErrorCode { + error_code: request.status().as_u16(), + error: request.text().await.unwrap(), + }); + } + + let response_text = match request.text().await { + Ok(string) => string, + Err(e) => { + return Err(ChorusError::InvalidResponse { + error: format!( + "Error while trying to process the HTTP response into a String: {}", + e + ), + }); + } + }; + + return match from_str::(&response_text) { + Ok(return_value) => Ok(return_value), + Err(e) => Err(ChorusError::InvalidResponse { error: format!("Error while trying to deserialize the JSON response into requested type T: {}. JSON Response: {}", + e, response_text) }) + }; + } + + /// Fetches the instance's software implementation and version. + /// + /// See: [VersionReturn] + /// + /// # Notes + /// This is a Symfonia only endpoint. (For now, we hope that spacebar will adopt it as well) + pub async fn get_version(&self) -> ChorusResult { + let endpoint_url = format!("{}/version", self.urls.api.clone()); + + let request = match self.client.get(&endpoint_url).send().await { + Ok(result) => result, + Err(e) => { + return Err(ChorusError::RequestFailed { + url: endpoint_url, + error: e.to_string(), + }); + } + }; + + if !request.status().as_str().starts_with('2') { + return Err(ChorusError::ReceivedErrorCode { + error_code: request.status().as_u16(), + error: request.text().await.unwrap(), + }); + } + + let response_text = match request.text().await { + Ok(string) => string, + Err(e) => { + return Err(ChorusError::InvalidResponse { + error: format!( + "Error while trying to process the HTTP response into a String: {}", + e + ), + }); + } + }; + + return match from_str::(&response_text) { + Ok(return_value) => Ok(return_value), + Err(e) => Err(ChorusError::InvalidResponse { error: format!("Error while trying to deserialize the JSON response into requested type T: {}. JSON Response: {}", e, response_text) }) + }; + } +} diff --git a/src/api/mod.rs b/src/api/mod.rs index c9ca2792..5e2f7cab 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -10,6 +10,7 @@ pub use guilds::*; pub use invites::*; pub use policies::instance::instance::*; pub use users::*; +pub use instance::*; pub mod auth; pub mod channels; @@ -17,3 +18,4 @@ pub mod guilds; pub mod invites; pub mod policies; pub mod users; +pub mod instance; diff --git a/src/instance.rs b/src/instance.rs index 64a74c4c..4c6a800d 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -92,8 +92,6 @@ impl Instance { limit_information = None } - let software = Instance::detect_software(&urls.api).await; - let mut instance = Instance { urls: urls.clone(), // Will be overwritten in the next step @@ -101,8 +99,10 @@ impl Instance { limits_information: limit_information, client: Client::new(), gateway_options: options.unwrap_or_default(), - software, + // Will also be detected soon + software: InstanceSoftware::Other, }; + instance.instance_info = match instance.general_configuration_schema().await { Ok(schema) => schema, Err(e) => { @@ -110,6 +110,13 @@ impl Instance { GeneralConfiguration::default() } }; + + instance.software = instance.detect_software().await; + + if options.is_none() { + instance.gateway_options = GatewayOptions::for_instance_software(instance.software()); + } + Ok(instance) } @@ -141,11 +148,29 @@ impl Instance { } /// Detects which [InstanceSoftware] the instance is running. - pub async fn detect_software(api_url: &str) -> InstanceSoftware { - // TODO: How? - // - // Spacebar has .well-known/spacebar, but that isn't on the API, but on the base - // which we aren't even guaranteed to have.. + pub async fn detect_software(&self) -> InstanceSoftware { + let version_res = self.get_version().await; + + match version_res { + Ok(version) => { + match version.server.to_lowercase().as_str() { + "symfonia" => return InstanceSoftware::Symfonia, + // We can dream this will be implemented one day + "spacebar" => return InstanceSoftware::SpacebarTypescript, + _ => {} + } + } + _ => {} + } + + // We know it isn't a symfonia server now, work around spacebar + // not really having a version endpoint + let ping = self.ping().await; + + if ping.is_ok() { + return InstanceSoftware::SpacebarTypescript; + } + InstanceSoftware::Other } diff --git a/src/types/schema/instance.rs b/src/types/schema/instance.rs new file mode 100644 index 00000000..7d19483c --- /dev/null +++ b/src/types/schema/instance.rs @@ -0,0 +1,84 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +//! Contains schema for miscellaneous api routes, such as /version and /ping +//! +//! Implementations of those routes can be found in /api/instance.rs + +use serde::{Deserialize, Serialize}; + +use crate::types::{GeneralConfiguration, Snowflake}; + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +/// The return type of the spacebar-only /api/ping endpoint +pub struct PingReturn { + /// Note: always "pong!" + pub ping: String, + pub instance: PingInstance, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)] +#[serde(rename_all = "camelCase")] +/// [GeneralConfiguration] as returned from the /api/ping endpoint +pub struct PingInstance { + pub id: Option, + pub name: String, + pub description: Option, + pub image: Option, + pub correspondence_email: Option, + pub correspondence_user_id: Option, + pub front_page: Option, + pub tos_page: Option, +} + +impl PingInstance { + /// Converts self into the [GeneralConfiguration] type + pub fn into_general_configuration(self) -> GeneralConfiguration { + GeneralConfiguration { + instance_name: self.name, + instance_description: self.description, + front_page: self.front_page, + tos_page: self.tos_page, + correspondence_email: self.correspondence_email, + correspondence_user_id: self.correspondence_user_id, + image: self.image, + instance_id: self.id, + } + } + + /// Converts the [GeneralConfiguration] type into self + pub fn from_general_configuration(other: GeneralConfiguration) -> Self { + Self { + id: other.instance_id, + name: other.instance_name, + description: other.instance_description, + image: other.image, + correspondence_email: other.correspondence_email, + correspondence_user_id: other.correspondence_user_id, + front_page: other.front_page, + tos_page: other.tos_page, + } + } +} + +impl From for GeneralConfiguration { + fn from(value: PingInstance) -> Self { + value.into_general_configuration() + } +} + +impl From for PingInstance { + fn from(value: GeneralConfiguration) -> Self { + Self::from_general_configuration(value) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +/// The return type of the symfonia-only /version endpoint +pub struct VersionReturn { + /// The instance's software version, e. g. "0.1.0" + pub version: String, + /// The instance's software, e. g. "symfonia" or "spacebar" + pub server: String, +} diff --git a/src/types/schema/mod.rs b/src/types/schema/mod.rs index 09e542e4..2888046e 100644 --- a/src/types/schema/mod.rs +++ b/src/types/schema/mod.rs @@ -13,6 +13,7 @@ pub use role::*; pub use user::*; pub use invites::*; pub use voice_state::*; +pub use instance::*; mod apierror; mod audit_log; @@ -25,9 +26,10 @@ mod role; mod user; mod invites; mod voice_state; +mod instance; #[derive(Debug, serde::Deserialize, serde::Serialize, Clone, PartialEq, PartialOrd, Eq, Ord)] pub struct GenericSearchQueryWithLimit { pub query: String, pub limit: Option, -} \ No newline at end of file +} diff --git a/tests/instance.rs b/tests/instance.rs index eb5fc606..d83e5e74 100644 --- a/tests/instance.rs +++ b/tests/instance.rs @@ -3,6 +3,7 @@ // file, You can obtain one at http://mozilla.org/MPL/2.0/. mod common; +use chorus::instance::InstanceSoftware; #[cfg(target_arch = "wasm32")] use wasm_bindgen_test::*; #[cfg(target_arch = "wasm32")] @@ -19,3 +20,16 @@ async fn generate_general_configuration_schema() { .unwrap(); common::teardown(bundle).await; } + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), tokio::test)] +async fn detect_instance_software() { + let bundle = common::setup().await; + + let software = bundle.instance.detect_software().await; + assert_eq!(software, InstanceSoftware::SpacebarTypescript); + + assert_eq!(bundle.instance.software(), InstanceSoftware::SpacebarTypescript); + + common::teardown(bundle).await; +} From 3a3884e2c086675a55cb2be03764157ac0afe233 Mon Sep 17 00:00:00 2001 From: kozabrada123 Date: Sat, 28 Sep 2024 10:17:31 +0200 Subject: [PATCH 3/4] fix: better error handling, missing slash in /policies/instance - handle deserialization errors and not parsing the http content into a string - the route only works on spacebar if you add a trailing slash for some reason --- src/api/policies/instance/instance.rs | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/src/api/policies/instance/instance.rs b/src/api/policies/instance/instance.rs index 584db338..d05709d7 100644 --- a/src/api/policies/instance/instance.rs +++ b/src/api/policies/instance/instance.rs @@ -17,7 +17,7 @@ impl Instance { /// # Reference /// See pub async fn general_configuration_schema(&self) -> ChorusResult { - let endpoint_url = self.urls.api.clone() + "/policies/instance"; + let endpoint_url = self.urls.api.clone() + "/policies/instance/"; let request = match self.client.get(&endpoint_url).send().await { Ok(result) => result, Err(e) => { @@ -35,7 +35,28 @@ impl Instance { }); } - let body = request.text().await.unwrap(); - Ok(from_str::(&body).unwrap()) + let response_text = match request.text().await { + Ok(string) => string, + Err(e) => { + return Err(ChorusError::InvalidResponse { + error: format!( + "Error while trying to process the HTTP response into a String: {}", + e + ), + }); + } + }; + + return match from_str::(&response_text) { + Ok(object) => Ok(object), + Err(e) => { + Err(ChorusError::InvalidResponse { + error: format!( + "Error while trying to deserialize the JSON response into requested type T: {}. JSON Response: {}", + e, response_text + ), + }) + } + }; } } From fb97da3dcd87ced7b4f9fafba23d955f2ebebd6e Mon Sep 17 00:00:00 2001 From: kozabrada123 Date: Sat, 28 Sep 2024 10:27:44 +0200 Subject: [PATCH 4/4] clippy.. --- src/api/instance.rs | 8 ++++---- src/api/policies/instance/instance.rs | 4 ++-- src/instance.rs | 18 +++++++----------- 3 files changed, 13 insertions(+), 17 deletions(-) diff --git a/src/api/instance.rs b/src/api/instance.rs index 14765d1e..20680e41 100644 --- a/src/api/instance.rs +++ b/src/api/instance.rs @@ -51,11 +51,11 @@ impl Instance { } }; - return match from_str::(&response_text) { + match from_str::(&response_text) { Ok(return_value) => Ok(return_value), Err(e) => Err(ChorusError::InvalidResponse { error: format!("Error while trying to deserialize the JSON response into requested type T: {}. JSON Response: {}", e, response_text) }) - }; + } } /// Fetches the instance's software implementation and version. @@ -96,9 +96,9 @@ impl Instance { } }; - return match from_str::(&response_text) { + match from_str::(&response_text) { Ok(return_value) => Ok(return_value), Err(e) => Err(ChorusError::InvalidResponse { error: format!("Error while trying to deserialize the JSON response into requested type T: {}. JSON Response: {}", e, response_text) }) - }; + } } } diff --git a/src/api/policies/instance/instance.rs b/src/api/policies/instance/instance.rs index d05709d7..f2a40bc1 100644 --- a/src/api/policies/instance/instance.rs +++ b/src/api/policies/instance/instance.rs @@ -47,7 +47,7 @@ impl Instance { } }; - return match from_str::(&response_text) { + match from_str::(&response_text) { Ok(object) => Ok(object), Err(e) => { Err(ChorusError::InvalidResponse { @@ -57,6 +57,6 @@ impl Instance { ), }) } - }; + } } } diff --git a/src/instance.rs b/src/instance.rs index 4c6a800d..f0569a99 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -149,18 +149,14 @@ impl Instance { /// Detects which [InstanceSoftware] the instance is running. pub async fn detect_software(&self) -> InstanceSoftware { - let version_res = self.get_version().await; - - match version_res { - Ok(version) => { - match version.server.to_lowercase().as_str() { - "symfonia" => return InstanceSoftware::Symfonia, - // We can dream this will be implemented one day - "spacebar" => return InstanceSoftware::SpacebarTypescript, - _ => {} - } + + if let Ok(version) = self.get_version().await { + match version.server.to_lowercase().as_str() { + "symfonia" => return InstanceSoftware::Symfonia, + // We can dream this will be implemented one day + "spacebar" => return InstanceSoftware::SpacebarTypescript, + _ => {} } - _ => {} } // We know it isn't a symfonia server now, work around spacebar