chorus/src/lib.rs

286 lines
11 KiB
Rust
Raw Normal View History

2024-01-30 17:19:34 +01:00
// 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/.
2023-11-24 22:21:57 +01:00
/*!
Chorus combines all the required functionalities of a user-centric Spacebar library into one package.
The library handles various aspects on your behalf, such as rate limiting, authentication and maintaining
a WebSocket connection to the Gateway. This means that you can focus on building your application,
instead of worrying about the underlying implementation details.
### Establishing a Connection
To connect to a Spacebar compatible server, you need to create an [`Instance`](https://docs.rs/chorus/latest/chorus/instance/struct.Instance.html) like this:
2023-12-02 17:44:24 +01:00
```rs
2023-11-24 22:21:57 +01:00
use chorus::instance::Instance;
use chorus::UrlBundle;
#[tokio::main]
async fn main() {
let bundle = UrlBundle::new(
"https://example.com/api".to_string(),
"wss://example.com/".to_string(),
"https://example.com/cdn".to_string(),
);
2023-12-10 18:40:03 +01:00
let instance = Instance::new(bundle)
2023-11-24 22:21:57 +01:00
.await
.expect("Failed to connect to the Spacebar server");
// You can create as many instances of `Instance` as you want, but each `Instance` should likely be unique.
dbg!(instance.instance_info);
dbg!(instance.limits_information);
}
```
This Instance can now be used to log in, register and from there on, interact with the server in all sorts of ways.
### Logging In
Logging in correctly provides you with an instance of [`ChorusUser`](https://docs.rs/chorus/latest/chorus/instance/struct.ChorusUser.html), with which you can interact with the server and
manipulate the account. Assuming you already have an account on the server, you can log in like this:
2023-12-02 17:44:24 +01:00
```rs
2023-11-24 22:21:57 +01:00
use chorus::types::LoginSchema;
// Assume, you already have an account created on this instance. Registering an account works
// the same way, but you'd use the Register-specific Structs and methods instead.
let login_schema = LoginSchema {
login: "user@example.com".to_string(),
password: "Correct-Horse-Battery-Staple".to_string(),
..Default::default()
};
2024-01-31 22:27:53 +01:00
// Each user connects to the Gateway. The Gateway connection lives on a separate thread. Depending on
2023-11-24 22:21:57 +01:00
// the runtime feature you choose, this can potentially take advantage of all of your computers' threads.
let user = instance
.login_account(login_schema)
.await
.expect("An error occurred during the login process");
dbg!(user.belongs_to);
dbg!(&user.object.read().unwrap().username);
```
## Supported Platforms
All major desktop operating systems (Windows, macOS (aarch64/x86_64), Linux (aarch64/x86_64)) are supported.
`wasm32-unknown-unknown` is a supported compilation target on versions `0.12.0` and up. This allows you to use
Chorus in your browser, or in any other environment that supports WebAssembly.
We recommend checking out the examples directory, as well as the documentation for more information.
## MSRV (Minimum Supported Rust Version)
Rust **1.67.1**. This number might change at any point while Chorus is not yet at version 1.0.0.
## Development Setup
Make sure that you have at least Rust 1.67.1 installed. You can check your Rust version by running `cargo --version`
in your terminal. To compile for `wasm32-unknown-unknown`, you need to install the `wasm32-unknown-unknown` target.
You can do this by running `rustup target add wasm32-unknown-unknown`.
### Testing
In general, the tests will require you to run a local instance of the Spacebar server. You can find instructions on how
to do that [here](https://docs.spacebar.chat/setup/server/). You can find a pre-configured version of the server
[here](https://github.com/bitfl0wer/server). It is recommended to use the pre-configured version, as certain things
like "proxy connection checking" are already disabled on this version, which otherwise might break tests.
### wasm
To test for wasm, you will need to `cargo install wasm-pack`. You can then run
`wasm-pack test --<chrome/firefox/safari> --headless -- --target wasm32-unknown-unknown --features="rt, client" --no-default-features`
to run the tests for wasm.
## Versioning
This crate uses Semantic Versioning 2.0.0 as its versioning scheme. You can read the specification [here](https://semver.org/spec/v2.0.0.html).
!*/
2023-08-28 12:27:38 +02:00
#![doc(
html_logo_url = "https://raw.githubusercontent.com/polyphony-chat/design/main/branding/polyphony-chorus-round-8bit.png"
)]
2023-06-19 10:27:32 +02:00
#![allow(clippy::module_inception)]
2023-08-28 12:27:38 +02:00
#![deny(
clippy::extra_unused_lifetimes,
clippy::from_over_into,
clippy::needless_borrow,
clippy::new_without_default,
clippy::useless_conversion
)]
#![warn(
clippy::todo,
clippy::unimplemented,
clippy::dbg_macro,
clippy::print_stdout,
2024-07-18 23:04:35 +02:00
clippy::print_stderr,
missing_debug_implementations,
missing_copy_implementations
)]
#[cfg(all(feature = "rt", feature = "rt_multi_thread"))]
compile_error!("feature \"rt\" and feature \"rt_multi_thread\" cannot be enabled at the same time");
2023-06-19 10:27:32 +02:00
2023-12-03 12:49:22 +01:00
use errors::ChorusResult;
2023-12-02 17:36:36 +01:00
use serde::{Deserialize, Serialize};
2023-12-03 13:04:17 +01:00
use types::types::domains_configuration::WellKnownResponse;
use url::{ParseError, Url};
2023-12-03 12:49:22 +01:00
use crate::errors::ChorusError;
#[cfg(feature = "client")]
pub mod api;
pub mod errors;
#[cfg(feature = "client")]
pub mod gateway;
#[cfg(feature = "client")]
pub mod instance;
#[cfg(feature = "client")]
Ratelimiter overhaul (#144) * Rename limits and limit to have better names * Remove empty lines * Remove handle_request (moved to requestlimiter) * Start working on new ratelimiter * Make limits Option, add "limited?" to constructor * Add missing logic to send_request * Rename Limits * Create Ratelimits and Limit Struct * Define Limit * Import Ratelimits * Define get_rate_limits * Remove unused import * + check_rate_limits & limits_config_to_ratelimits * Remove Absolute Limits These limits are not meant to be tracked anyways. * add ratelimits is_exhausted * Add error handling and send request checking * change limits to option ratelimits * Add strum * Change Ratelimits to Hashmap * Remove ratelimits in favor of hashmap * Change code from struct to hashmap * start working on update rate limits * Remove wrong import * Rename ChorusLibError to ChorusError * Documented the chorus errors * Made error documentation docstring * Make ReceivedErrorCodeError have error string * Remove unneeded import * Match changes in errors.rs * Improve update_rate_limits and can_send_request * add ratelimits.to_hash_map() * use instances' client instead of new client * add LimitsConfiguration to instance * improve update_limits, change a method name * Fix un-updated errors * Get LimitConfiguration in a sane way * Move common.rs into ratelimiter::ChorusRequest * Delete common.rs * Make instance.rs use overhauled errors * Refactor to use new Rate limiting implementation * Refactor to use new Rate limiting implementation * Refactor to use new Rate limiting implementation * Refactor to use new Rate limiting implementation * Refactor to use new Rate limiting implementation * Refactor to use new Rate limiting implementation * update ratelimiter implementation across all files * Fix remaining errors post-refactor * Changed Enum case to be correct * Use result * Re-add missing body to request * Remove unneeded late initalization * Change visibility from pub to pub(crate) I feel like these core methods don't need to be exposed as public API. * Remove unnecessary import * Fix clippy warnings * Add docstring * Change Error names across all files * Update Cargo.toml Strum is not needed * Update ratelimits.rs * Update ratelimits.rs * Bug/discord instance info unavailable (#146) * Change text to be more ambigous * Use default Configuration instead of erroring out * Emit warning log if instance config cant be gotten * Remove import * Update src/instance.rs Co-authored-by: SpecificProtagonist <specificprotagonist@posteo.org> * Add missing closing bracket * Put limits and limits_configuration as one struct * Derive Hash * remove import * rename limits and limits_configuration * Save clone call * Change LimitsConfiguration to RateLimits `LimitsConfiguration` is in no way related to whether the instance has API rate limits enabled or not. Therefore, it has been replaced with what it should have been all along. * Add ensure_limit_in_map(), add `window` to `Limit` * Remove unneeded var * Remove import * Clean up unneeded things Dead code warnings have been supressed, but flagged as FIXME so they don't get forgotten. Anyone using tools like TODO Tree in VSCode can still see that they are there, however, they will not be shown as warnings anymore * Remove nested submodule `limit` * Add doc comments * Add more doc comments * Add some log messages to some methods --------- Co-authored-by: SpecificProtagonist <specificprotagonist@posteo.org>
2023-07-09 18:38:02 +02:00
pub mod ratelimiter;
2023-05-25 21:11:08 +02:00
pub mod types;
Primitive voice implementation (feature/voice) (#457) * Add Webrtc Identify & Ready * Add more webrtc typings * Attempt an untested voice gateway implementation * fmt * Merge with main * Same allow as for voice as normal gateway * Test error observer * Minor updates * More derives * Even more derives * Small types update * e * Minor doc fixes * Modernise voice gateway * Add default impl for voicegatewayerror * Make voice event fields pub * Event updates via the scientific method * ?? * Fix bad request in voice gateway init * Voice gateway updates * Fix error failing to 'deserialize' properly * Update voice identify * Clarify FIXME related to #430 * Update to v7 * Create seperate voice_gateway.rs and voice_udp.rs * Restructure voice to new module * fix: deserialization error in speaking bitflags * feat: kinda janky ip discovery impl * feat: return ip discovery data + minor update * feat: packet parsing! * fix: voice works again * feat: add voice_media_sink_wants (comitting uncommited changes to merge) * chore: rename events/webrtc to events/voice_gateway * Add UdpHandle * chore: clippy + other misc updates * fix: attempt to fix failing wasm build * chore: yes clippy, that is indeed an unneeded return statement * feat: add VoiceData struct * feat: add VoiceData reference to UdpHandler * feat: decryption? * chore: formatting * feat: add ssrc definition (op 12) * feat: add untested sending & asbtract nonce generation * feat: Public api! (sorta) * small updates * feat: add sequence number * chore: yes * feat: merge VoiceHandler into official development * chore: yes clippy, you are special * fix: duplicated gateway events * feat: first try at vgw wasm compat * fix: blunder * fix: gateway connect using wrong url * fix: properly using encrypted data, bad practice for buffer creation * chore: split voice udp * feat: udp error handling, create udp/backends * fix: its the same * chore: clarify UDP on WASM * api: split voice gateway and udp features, test for voice gateway in WASM * feat: new encryption modes, minor code quality * docs: document voice encryption modes * chore: unused imports * chore: update getrandom version to match wasm version * chore: update on packet size FIXME * drop buf asap * Okay can't do that actually * tests: add nonce test * normal tests work? * docs: fix doc warning, fix incorrect refrences to 'webrtc' * chore: json isn't a doc test * tests: better gateway auth test * testing tests * update voice heartbeat, fix the new test issue * committed too much * fix: unused import * fix: use ip discovery address as string, not as Vec<u8> * chore: less obnoxious logging * chore: better unimplemented voice modes handling * chore: remove unused variable * chore: use matches macro * add voice examples, make gateway ones clearer * rename voice example * chore: remove unused VoiceHandler * fix: implement gateway Reconnect and InvalidSession * Typo Co-authored-by: Flori <39242991+bitfl0wer@users.noreply.github.com> * Fix a bunch of typos Co-authored-by: Flori <39242991+bitfl0wer@users.noreply.github.com> * fix: error handling while loading native certs * fix: guh * use be for nonce bytes * fix: refactor gw and vgw closures * remove outdated docs --------- Co-authored-by: Flori <39242991+bitfl0wer@users.noreply.github.com>
2024-04-16 17:18:21 +02:00
#[cfg(all(
feature = "client",
any(feature = "voice_udp", feature = "voice_gateway")
))]
pub mod voice;
2023-04-05 21:54:27 +02:00
2023-12-02 17:35:47 +01:00
#[derive(Clone, Default, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
2023-07-29 10:23:04 +02:00
/// A URLBundle bundles together the API-, Gateway- and CDN-URLs of a Spacebar instance.
///
2023-07-10 17:22:31 +02:00
/// # Notes
/// All the urls can be found on the /api/policies/instance/domains endpoint of a spacebar server
2023-06-20 02:59:18 +02:00
pub struct UrlBundle {
2023-12-15 00:10:33 +01:00
/// The root url of an Instance. Usually, this would be the url where `.well-known/spacebar` can
/// be located under. If the instance you are connecting to for some reason does not have a
/// `.well-known` set up (for example, if it is a local/testing instance), you can use the api
/// url as a substitute.
/// Ex: `https://spacebar.chat`
pub root: String,
2023-07-10 17:22:31 +02:00
/// The api's url.
/// Ex: `https://old.server.spacebar.chat/api`
2023-04-05 21:54:27 +02:00
pub api: String,
2023-07-10 17:22:31 +02:00
/// The gateway websocket url.
2023-07-29 10:23:04 +02:00
/// Note that because this is a websocket url, it will always start with `wss://` or `ws://`
2023-07-10 17:22:31 +02:00
/// Ex: `wss://gateway.old.server.spacebar.chat`
2023-04-05 21:54:27 +02:00
pub wss: String,
2023-07-10 17:22:31 +02:00
/// The CDN's url.
/// Ex: `https://cdn.old.server.spacebar.chat`
2023-04-05 21:54:27 +02:00
pub cdn: String,
}
2023-06-20 02:59:18 +02:00
impl UrlBundle {
2023-07-29 11:26:00 +02:00
/// Creates a new UrlBundle from the relevant urls.
2023-12-15 00:10:33 +01:00
pub fn new(root: String, api: String, wss: String, cdn: String) -> Self {
2023-04-05 21:54:27 +02:00
Self {
2023-12-15 00:10:33 +01:00
root: UrlBundle::parse_url(root),
2023-06-20 02:59:18 +02:00
api: UrlBundle::parse_url(api),
wss: UrlBundle::parse_url(wss),
cdn: UrlBundle::parse_url(cdn),
2023-04-05 21:54:27 +02:00
}
}
2023-07-29 10:23:04 +02:00
/// Parses a URL using the Url library and formats it in a standardized way.
/// If no protocol is given, HTTP (not HTTPS) is assumed.
///
/// # Examples:
/// ```rs
2023-04-05 21:54:27 +02:00
/// let url = parse_url("localhost:3000");
/// ```
/// `-> Outputs "http://localhost:3000".`
pub fn parse_url(url: String) -> String {
let url = match Url::parse(&url) {
Ok(url) => {
if url.scheme() == "localhost" {
2023-06-20 02:59:18 +02:00
return UrlBundle::parse_url(format!("http://{}", url));
2023-04-05 21:54:27 +02:00
}
url
}
Err(ParseError::RelativeUrlWithoutBase) => {
let url_fmt = format!("http://{}", url);
2023-06-20 02:59:18 +02:00
return UrlBundle::parse_url(url_fmt);
2023-04-05 21:54:27 +02:00
}
2023-12-03 12:49:22 +01:00
Err(_) => panic!("Invalid URL"), // TODO: should not panic here
2023-04-05 21:54:27 +02:00
};
// if the last character of the string is a slash, remove it.
let mut url_string = url.to_string();
2023-04-25 17:41:14 +02:00
if url_string.ends_with('/') {
url_string.pop();
}
2023-04-25 17:41:14 +02:00
url_string
2023-04-05 21:54:27 +02:00
}
2023-12-03 12:49:22 +01:00
/// Performs a few HTTP requests to try and retrieve a `UrlBundle` from an instances' root url.
2023-12-03 12:49:22 +01:00
/// 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> {
2023-12-03 12:49:22 +01:00
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
2023-12-03 12:49:22 +01:00
{
return Ok(response_slash_api);
}
if let Ok(response_api) =
UrlBundle::from_api_url(&format!("{}/policies/instance/domains", parsed)).await
2023-12-03 12:49:22 +01:00
{
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() } )
2023-12-03 12:49:22 +01:00
}
}
}
async fn from_api_url(url: &str) -> ChorusResult<UrlBundle> {
2023-12-03 13:16:34 +01:00
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
{
2023-12-15 00:10:33 +01:00
Ok(UrlBundle::new(
url.to_string(),
body.api_endpoint,
body.gateway,
body.cdn,
))
2023-12-03 13:16:34 +01:00
} 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(),
})
}
2023-12-03 12:49:22 +01:00
}
}
2023-04-04 17:37:11 +02:00
#[cfg(test)]
mod lib {
2023-04-04 17:37:11 +02:00
use super::*;
#[test]
2023-04-05 21:54:27 +02:00
fn test_parse_url() {
2023-06-20 02:59:18 +02:00
let mut result = UrlBundle::parse_url(String::from("localhost:3000/"));
2023-04-05 21:54:27 +02:00
assert_eq!(result, String::from("http://localhost:3000"));
2023-06-20 02:59:18 +02:00
result = UrlBundle::parse_url(String::from("https://some.url.com/"));
assert_eq!(result, String::from("https://some.url.com"));
2023-06-20 02:59:18 +02:00
result = UrlBundle::parse_url(String::from("https://some.url.com/"));
assert_eq!(result, String::from("https://some.url.com"));
2023-06-20 02:59:18 +02:00
result = UrlBundle::parse_url(String::from("https://some.url.com"));
assert_eq!(result, String::from("https://some.url.com"));
2023-04-04 17:37:11 +02:00
}
}