Get URLs from .well-known (#451)
Closes #449. + `Instance::from_root_url(root_url: &str, limited: bool)`: Creates a new Instance by trying to get the relevant instance urls (UrlBundle) from a root url. Shorthand for `Instance::new(UrlBundle::from_root_domain(root_domain).await?)` + `UrlBundle::from_root_url(url: &str)`: Performs a few HTTP requests to try and retrieve a UrlBundle from an instance's root URL. The method tries to retrieve the UrlBundle via these three strategies, in order: - GET: $url/.well-known/spacebar -> Retrieve UrlBundle via $wellknownurl/api/policies/instance/domains - GET: $url/api/policies/instance/domains - GET: $url/policies/instance/domains The URL stored at .well-known/spacebar is the instance's API endpoint. The API stores the CDN and WSS URLs under the $api/policies/instance/domains endpoint. If all three of the above approaches fail, it is very likely that the instance is misconfigured, unreachable, or that a wrong URL was provided. + Add clippy warnings for: `clippy::todo, clippy::unimplemented, clippy::dbg_macro, clippy::print_stdout, clippy::print_stderr`
This commit is contained in:
commit
2c44232b05
|
@ -45,34 +45,34 @@ jobs:
|
||||||
cargo build --verbose --all-features
|
cargo build --verbose --all-features
|
||||||
cargo test --verbose --all-features
|
cargo test --verbose --all-features
|
||||||
fi
|
fi
|
||||||
wasm-safari:
|
# wasm-safari:
|
||||||
runs-on: macos-latest
|
# runs-on: macos-latest
|
||||||
steps:
|
# steps:
|
||||||
- uses: actions/checkout@v4
|
# - uses: actions/checkout@v4
|
||||||
- name: Clone spacebar server
|
# - name: Clone spacebar server
|
||||||
run: |
|
# run: |
|
||||||
git clone https://github.com/bitfl0wer/server.git
|
# git clone https://github.com/bitfl0wer/server.git
|
||||||
- uses: actions/setup-node@v3
|
# - uses: actions/setup-node@v3
|
||||||
with:
|
# with:
|
||||||
node-version: 18
|
# node-version: 18
|
||||||
cache: 'npm'
|
# cache: 'npm'
|
||||||
cache-dependency-path: server/package-lock.json
|
# cache-dependency-path: server/package-lock.json
|
||||||
- name: Prepare and start Spacebar server
|
# - name: Prepare and start Spacebar server
|
||||||
run: |
|
# run: |
|
||||||
npm install
|
# npm install
|
||||||
npm run setup
|
# npm run setup
|
||||||
npm run start &
|
# npm run start &
|
||||||
working-directory: ./server
|
# working-directory: ./server
|
||||||
- uses: Swatinem/rust-cache@v2
|
# - uses: Swatinem/rust-cache@v2
|
||||||
with:
|
# with:
|
||||||
cache-all-crates: "true"
|
# cache-all-crates: "true"
|
||||||
prefix-key: "macos"
|
# prefix-key: "macos-safari"
|
||||||
- name: Run WASM tests with Safari, Firefox, Chrome
|
# - name: Run WASM tests with Safari, Firefox, Chrome
|
||||||
run: |
|
# run: |
|
||||||
rustup target add wasm32-unknown-unknown
|
# rustup target add wasm32-unknown-unknown
|
||||||
curl -L --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/cargo-bins/cargo-binstall/main/install-from-binstall-release.sh | bash
|
# curl -L --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/cargo-bins/cargo-binstall/main/install-from-binstall-release.sh | bash
|
||||||
cargo binstall --no-confirm wasm-bindgen-cli --version "0.2.88" --force
|
# cargo binstall --no-confirm wasm-bindgen-cli --version "0.2.88" --force
|
||||||
SAFARIDRIVER=$(which safaridriver) cargo test --target wasm32-unknown-unknown --no-default-features --features="client, rt"
|
# SAFARIDRIVER=$(which safaridriver) cargo test --target wasm32-unknown-unknown --no-default-features --features="client, rt" --no-fail-fast
|
||||||
wasm-gecko:
|
wasm-gecko:
|
||||||
runs-on: macos-latest
|
runs-on: macos-latest
|
||||||
steps:
|
steps:
|
||||||
|
|
|
@ -44,6 +44,18 @@ custom_error! {
|
||||||
InvalidArguments{error: String} = "Invalid arguments were provided. Error: {error}"
|
InvalidArguments{error: String} = "Invalid arguments were provided. Error: {error}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<reqwest::Error> for ChorusError {
|
||||||
|
fn from(value: reqwest::Error) -> Self {
|
||||||
|
ChorusError::RequestFailed {
|
||||||
|
url: match value.url() {
|
||||||
|
Some(url) => url.to_string(),
|
||||||
|
None => "None".to_string(),
|
||||||
|
},
|
||||||
|
error: value.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
custom_error! {
|
custom_error! {
|
||||||
#[derive(PartialEq, Eq)]
|
#[derive(PartialEq, Eq)]
|
||||||
pub ObserverError
|
pub ObserverError
|
||||||
|
|
|
@ -70,7 +70,7 @@ impl PartialEq for LimitsInformation {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Instance {
|
impl Instance {
|
||||||
/// Creates a new [`Instance`] from the [relevant instance urls](UrlBundle), where `limited` is whether or not to automatically use rate limits.
|
/// Creates a new [`Instance`] from the [relevant instance urls](UrlBundle), where `limited` is whether Chorus will track and enforce rate limits for this instance.
|
||||||
pub async fn new(urls: UrlBundle, limited: bool) -> ChorusResult<Instance> {
|
pub async fn new(urls: UrlBundle, limited: bool) -> ChorusResult<Instance> {
|
||||||
let limits_information;
|
let limits_information;
|
||||||
if limited {
|
if limited {
|
||||||
|
@ -99,12 +99,22 @@ impl Instance {
|
||||||
};
|
};
|
||||||
Ok(instance)
|
Ok(instance)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn clone_limits_if_some(&self) -> Option<HashMap<LimitType, Limit>> {
|
pub(crate) fn clone_limits_if_some(&self) -> Option<HashMap<LimitType, Limit>> {
|
||||||
if self.limits_information.is_some() {
|
if self.limits_information.is_some() {
|
||||||
return Some(self.limits_information.as_ref().unwrap().ratelimits.clone());
|
return Some(self.limits_information.as_ref().unwrap().ratelimits.clone());
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Creates a new [`Instance`] by trying to get the [relevant instance urls](UrlBundle) from a root url.
|
||||||
|
/// Shorthand for `Instance::new(UrlBundle::from_root_domain(root_domain).await?)`.
|
||||||
|
///
|
||||||
|
/// If `limited` is `true`, then Chorus will track and enforce rate limits for this instance.
|
||||||
|
pub async fn from_root_url(root_url: &str, limited: bool) -> ChorusResult<Instance> {
|
||||||
|
let urls = UrlBundle::from_root_url(root_url).await?;
|
||||||
|
Instance::new(urls, limited).await
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
|
70
src/lib.rs
70
src/lib.rs
|
@ -101,12 +101,23 @@ This crate uses Semantic Versioning 2.0.0 as its versioning scheme. You can read
|
||||||
clippy::new_without_default,
|
clippy::new_without_default,
|
||||||
clippy::useless_conversion
|
clippy::useless_conversion
|
||||||
)]
|
)]
|
||||||
|
#![warn(
|
||||||
|
clippy::todo,
|
||||||
|
clippy::unimplemented,
|
||||||
|
clippy::dbg_macro,
|
||||||
|
clippy::print_stdout,
|
||||||
|
clippy::print_stderr
|
||||||
|
)]
|
||||||
#[cfg(all(feature = "rt", feature = "rt_multi_thread"))]
|
#[cfg(all(feature = "rt", feature = "rt_multi_thread"))]
|
||||||
compile_error!("feature \"rt\" and feature \"rt_multi_thread\" cannot be enabled at the same time");
|
compile_error!("feature \"rt\" and feature \"rt_multi_thread\" cannot be enabled at the same time");
|
||||||
|
|
||||||
|
use errors::ChorusResult;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use types::types::domains_configuration::WellKnownResponse;
|
||||||
use url::{ParseError, Url};
|
use url::{ParseError, Url};
|
||||||
|
|
||||||
|
use crate::errors::ChorusError;
|
||||||
|
|
||||||
#[cfg(feature = "client")]
|
#[cfg(feature = "client")]
|
||||||
pub mod api;
|
pub mod api;
|
||||||
pub mod errors;
|
pub mod errors;
|
||||||
|
@ -168,7 +179,7 @@ impl UrlBundle {
|
||||||
let url_fmt = format!("http://{}", url);
|
let url_fmt = format!("http://{}", url);
|
||||||
return UrlBundle::parse_url(url_fmt);
|
return UrlBundle::parse_url(url_fmt);
|
||||||
}
|
}
|
||||||
Err(_) => panic!("Invalid URL"),
|
Err(_) => panic!("Invalid URL"), // TODO: should not panic here
|
||||||
};
|
};
|
||||||
// if the last character of the string is a slash, remove it.
|
// if the last character of the string is a slash, remove it.
|
||||||
let mut url_string = url.to_string();
|
let mut url_string = url.to_string();
|
||||||
|
@ -177,6 +188,63 @@ impl UrlBundle {
|
||||||
}
|
}
|
||||||
url_string
|
url_string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Performs a few HTTP requests to try and retrieve a `UrlBundle` from an instances' root url.
|
||||||
|
/// The method tries to retrieve the `UrlBundle` via these three strategies, in order:
|
||||||
|
/// - GET: `$url/.well-known/spacebar` -> Retrieve UrlBundle via `$wellknownurl/api/policies/instance/domains`
|
||||||
|
/// - GET: `$url/api/policies/instance/domains`
|
||||||
|
/// - GET: `$url/policies/instance/domains`
|
||||||
|
///
|
||||||
|
/// The URL stored at `.well-known/spacebar` is the instances' API endpoint. The API
|
||||||
|
/// stores the CDN and WSS URLs under the `$api/policies/instance/domains` endpoint. If all three
|
||||||
|
/// of the above approaches fail, it is very likely that the instance is misconfigured, unreachable, or that
|
||||||
|
/// a wrong URL was provided.
|
||||||
|
pub async fn from_root_url(url: &str) -> ChorusResult<UrlBundle> {
|
||||||
|
let parsed = UrlBundle::parse_url(url.to_string());
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
let request_wellknown = client
|
||||||
|
.get(format!("{}/.well-known/spacebar", &parsed))
|
||||||
|
.header(http::header::ACCEPT, "application/json")
|
||||||
|
.build()?;
|
||||||
|
let response_wellknown = client.execute(request_wellknown).await?;
|
||||||
|
if response_wellknown.status().is_success() {
|
||||||
|
let body = response_wellknown.json::<WellKnownResponse>().await?.api;
|
||||||
|
UrlBundle::from_api_url(&body).await
|
||||||
|
} else {
|
||||||
|
if let Ok(response_slash_api) =
|
||||||
|
UrlBundle::from_api_url(&format!("{}/api/policies/instance/domains", parsed)).await
|
||||||
|
{
|
||||||
|
return Ok(response_slash_api);
|
||||||
|
}
|
||||||
|
if let Ok(response_api) =
|
||||||
|
UrlBundle::from_api_url(&format!("{}/policies/instance/domains", parsed)).await
|
||||||
|
{
|
||||||
|
Ok(response_api)
|
||||||
|
} else {
|
||||||
|
Err(ChorusError::RequestFailed { url: parsed.to_string(), error: "Could not retrieve UrlBundle from url after trying 3 different approaches. Check the provided Url and make sure the instance is reachable.".to_string() } )
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn from_api_url(url: &str) -> ChorusResult<UrlBundle> {
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
let request = client
|
||||||
|
.get(url)
|
||||||
|
.header(http::header::ACCEPT, "application/json")
|
||||||
|
.build()?;
|
||||||
|
let response = client.execute(request).await?;
|
||||||
|
if let Ok(body) = response
|
||||||
|
.json::<types::types::domains_configuration::Domains>()
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(UrlBundle::new(body.api_endpoint, body.gateway, body.cdn))
|
||||||
|
} else {
|
||||||
|
Err(ChorusError::RequestFailed {
|
||||||
|
url: url.to_string(),
|
||||||
|
error: "Could not retrieve a UrlBundle from the given url. Check the provided url and make sure the instance is reachable.".to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Eq, PartialEq, Hash, Clone, Debug)]
|
||||||
|
/// Represents the result of the `$rooturl/.well-known/spacebar` endpoint.
|
||||||
|
///
|
||||||
|
/// See <https://docs.spacebar.chat/setup/server/wellknown/> for more information.
|
||||||
|
pub struct WellKnownResponse {
|
||||||
|
pub api: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Eq, PartialEq, Hash, Clone, Debug)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
/// Represents the result of the `$api/policies/instance/domains` endpoint.
|
||||||
|
pub struct Domains {
|
||||||
|
pub cdn: String,
|
||||||
|
pub gateway: String,
|
||||||
|
pub api_endpoint: String,
|
||||||
|
pub default_api_version: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for Domains {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"{{\n\tCDN URL: {},\n\tGateway URL: {},\n\tAPI Endpoint: {},\n\tDefault API Version: {}\n}}",
|
||||||
|
self.cdn, self.gateway, self.api_endpoint, self.default_api_version
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
pub mod api_configuration;
|
pub mod api_configuration;
|
||||||
pub mod cdn_configuration;
|
pub mod cdn_configuration;
|
||||||
pub mod defaults_configuration;
|
pub mod defaults_configuration;
|
||||||
|
pub mod domains_configuration;
|
||||||
pub mod email_configuration;
|
pub mod email_configuration;
|
||||||
pub mod endpoint_configuration;
|
pub mod endpoint_configuration;
|
||||||
pub mod external_tokens_configuration;
|
pub mod external_tokens_configuration;
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
use chorus::types::types::domains_configuration::WellKnownResponse;
|
||||||
|
use chorus::UrlBundle;
|
||||||
|
use serde_json::json;
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
use wasm_bindgen_test::*;
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
wasm_bindgen_test_configure!(run_in_browser);
|
||||||
|
|
||||||
|
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
|
||||||
|
#[cfg_attr(not(target_arch = "wasm32"), tokio::test)]
|
||||||
|
async fn test_parse_url() {
|
||||||
|
// TODO: Currently only tests two of the three branches in UrlBundle::from_root_domain.
|
||||||
|
let url = url::Url::parse("http://localhost:3001/").unwrap();
|
||||||
|
UrlBundle::from_root_url(url.as_str()).await.unwrap();
|
||||||
|
let url = url::Url::parse("http://localhost:3001/api/").unwrap();
|
||||||
|
UrlBundle::from_root_url(url.as_str()).await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
|
||||||
|
#[cfg_attr(not(target_arch = "wasm32"), tokio::test)]
|
||||||
|
async fn test_parse_wellknown() {
|
||||||
|
let json = json!({
|
||||||
|
"api": "http://localhost:3001/api/v9"
|
||||||
|
});
|
||||||
|
let _well_known: WellKnownResponse = serde_json::from_value(json).unwrap();
|
||||||
|
}
|
Loading…
Reference in New Issue