diff --git a/src/api/instance.rs b/src/api/instance.rs
new file mode 100644
index 00000000..20680e41
--- /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
+ ),
+ });
+ }
+ };
+
+ 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
+ ),
+ });
+ }
+ };
+
+ 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/api/policies/instance/instance.rs b/src/api/policies/instance/instance.rs
index 584db338..f2a40bc1 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
+ ),
+ });
+ }
+ };
+
+ 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
+ ),
+ })
+ }
+ }
}
}
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..f0569a99 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,7 @@ impl Instance {
} else {
limit_information = None
}
+
let mut instance = Instance {
urls: urls.clone(),
// Will be overwritten in the next step
@@ -95,7 +99,10 @@ impl Instance {
limits_information: limit_information,
client: Client::new(),
gateway_options: options.unwrap_or_default(),
+ // Will also be detected soon
+ software: InstanceSoftware::Other,
};
+
instance.instance_info = match instance.general_configuration_schema().await {
Ok(schema) => schema,
Err(e) => {
@@ -103,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)
}
@@ -133,12 +147,98 @@ 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(&self) -> InstanceSoftware {
+
+ 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
+ // not really having a version endpoint
+ let ping = self.ping().await;
+
+ if ping.is_ok() {
+ return InstanceSoftware::SpacebarTypescript;
+ }
+
+ 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)]
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;
+}