Skip to content

Commit

Permalink
Differentiate between instance softwares (#560)
Browse files Browse the repository at this point in the history
* Add InstanceOptions type

* feat: add /ping and /version, implement software differentiation

* 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
  • Loading branch information
kozabrada123 authored Sep 28, 2024
1 parent e7e3cb6 commit 333dad8
Show file tree
Hide file tree
Showing 8 changed files with 352 additions and 6 deletions.
104 changes: 104 additions & 0 deletions src/api/instance.rs
Original file line number Diff line number Diff line change
@@ -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 <https://docs.spacebar.chat/routes/#get-/ping/>
pub async fn ping(&self) -> ChorusResult<PingReturn> {
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::<PingReturn>(&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<VersionReturn> {
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::<VersionReturn>(&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) })
}
}
}
2 changes: 2 additions & 0 deletions src/api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@ pub use guilds::*;
pub use invites::*;
pub use policies::instance::instance::*;
pub use users::*;
pub use instance::*;

pub mod auth;
pub mod channels;
pub mod guilds;
pub mod invites;
pub mod policies;
pub mod users;
pub mod instance;
27 changes: 24 additions & 3 deletions src/api/policies/instance/instance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ impl Instance {
/// # Reference
/// See <https://docs.spacebar.chat/routes/#get-/policies/instance/>
pub async fn general_configuration_schema(&self) -> ChorusResult<GeneralConfiguration> {
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) => {
Expand All @@ -35,7 +35,28 @@ impl Instance {
});
}

let body = request.text().await.unwrap();
Ok(from_str::<GeneralConfiguration>(&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::<GeneralConfiguration>(&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
),
})
}
}
}
}
19 changes: 19 additions & 0 deletions src/gateway/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
///
Expand All @@ -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
Expand Down
104 changes: 102 additions & 2 deletions src/instance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<LimitsInformation>,
#[serde(skip)]
pub client: Client,
#[serde(skip)]
pub gateway_options: GatewayOptions,
pub(crate) gateway_options: GatewayOptions,
}

#[derive(Debug, Clone, Serialize, Deserialize, Default, Eq)]
Expand Down Expand Up @@ -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<GatewayOptions>,
Expand All @@ -88,21 +91,32 @@ impl Instance {
} 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,
};

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)
}

Expand Down Expand Up @@ -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 <https://github.com/spacebarchat/server>
SpacebarTypescript,
/// The Polyphony server written in rust, available at
/// at <https://github.com/polyphony-chat/symfonia>
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)]
Expand Down
Loading

0 comments on commit 333dad8

Please sign in to comment.