Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Differentiate between instance softwares #560

Merged
merged 4 commits into from
Sep 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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