Skip to content

Commit

Permalink
Work on SignalR client
Browse files Browse the repository at this point in the history
  • Loading branch information
LJ authored and LJ committed Jul 28, 2024
1 parent 6026c27 commit 55af000
Show file tree
Hide file tree
Showing 20 changed files with 602 additions and 241 deletions.
7 changes: 6 additions & 1 deletion .config/extra.dic
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
40
50
Resonite
Resonite's
ProtoFlux
Expand Down Expand Up @@ -34,3 +34,8 @@ serializer
Deserializes
Versioning
usernames
WS
async
mutex
RPC
HTTPS
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
{
"rust-analyzer.cargo.features": "all"
"rust-analyzer.cargo.features": "all",
"editor.formatOnSave": true
}
23 changes: 18 additions & 5 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "resonite"
version = "0.2.0"
version = "0.3.0"
edition = "2021"
license = "MPL-2.0"
authors = ["ljoonal"]
Expand Down Expand Up @@ -29,6 +29,7 @@ bench = false
[features]
default = []
http_client = ["tokio", "governor", "reqwest", "racal/reqwest", "async-trait"]
signalr_client = ["http_client", "tokio", "ezsockets", "tokio-stream", "http", "tokio-tungstenite", "async-trait"]
rand_util = ["nanorand"]

[dependencies]
Expand All @@ -43,21 +44,33 @@ strum = { version = "0.26.3", features = ["derive"] }
# API client specifics
racal = "0.4.0"
#racal = { path = "../racal", features = ["reqwest"] }
governor = { version = "0.6.3", optional = true }
tokio = { version = "1.39.1", optional = true }
async-trait = { version = "0.1.81", optional = true }

nanorand = { version = "0.7.0", optional = true }

governor = { version = "0.6.3", optional = true }

tokio = { version = "1.39.2", optional = true, features = ["macros"]}
tokio-stream = { version = "0.1.15", optional = true}
http = { version = "1.1.0", optional = true }
async-trait = { version = "0.1.81", optional = true }
# Required to be defined by us since ezsockets doesn't expose a TLS feature
tokio-tungstenite = {version = "0.23.1", optional= true, default-features = false, features = ["rustls-tls-webpki-roots"] }

[dependencies.reqwest]
optional = true
version = "0.12.5"
default-features = false
features = ["json", "rustls-tls"]

[dependencies.ezsockets]
optional = true
version = "0.6.2"
default-features = false
features = ["client", "native_client"]

[dev-dependencies]
tokio-test = "0.4.4"
tokio = { version = "1.39.1", features = ["rt", "macros"] }
tokio = { version = "1.39.2", features = ["rt", "macros"] }

[package.metadata.docs.rs]
all-features = true
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@
[![Crates.io](https://img.shields.io/crates/v/resonite.svg)](https://crates.io/crates/resonite)
[![Docs](https://docs.rs/resonite/badge.svg)](https://docs.rs/crate/resonite/)

WIP Rust models of [Resonite's](https://resonite.com) API.
Rust models of [Resonite's](https://resonite.com) API.

Any official documentation of Resonite' API is lacking, and the API is still changing too.
So this crate can't guarantee correctness.

This crate provides an example API client with the optional `api_client` feature.
This crate provides an example API client with the optional `http_client` & `signalr_client` features.

## Testing

Expand Down
227 changes: 227 additions & 0 deletions src/api_client/http.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
use std::num::NonZeroU32;

use governor::{
clock::DefaultClock,
middleware::NoOpMiddleware,
state::{InMemoryState, NotKeyed},
Quota,
RateLimiter,
};
pub use racal::reqwest::ApiClient;
use reqwest::{
header::{HeaderMap, HeaderValue},
Client,
RequestBuilder,
};
use serde::{Deserialize, Serialize};

use super::ApiError;
use crate::query::{Authenticating, Authentication, NoAuthentication};

type NormalRateLimiter =
RateLimiter<NotKeyed, InMemoryState, DefaultClock, NoOpMiddleware>;

#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
/// Data needed to actually request an user session.
///
/// Mixes headers and actual body data together, not an actual Resonite model.
pub struct UserSessionQueryWithHeaders {
/// The actual body of the request
pub body: crate::query::UserSession,
#[serde(flatten)]
/// Headers & so on needed for authentication requests
pub data: Authenticating,
}

#[must_use]
fn http_rate_limiter() -> NormalRateLimiter {
// ~5 seconds per request sustained over one minute, allowing up to a request
// per second in bursts.
RateLimiter::direct(
Quota::per_minute(NonZeroU32::try_from(12).unwrap())
.allow_burst(NonZeroU32::try_from(5).unwrap()),
)
}

/// The main API client without authentication
pub struct UnauthenticatedResonite {
user_agent: String,
http: Client,
rate_limiter: NormalRateLimiter,
}

#[async_trait::async_trait]
impl ApiClient<NoAuthentication> for UnauthenticatedResonite {
fn state(&self) -> &NoAuthentication { &NoAuthentication {} }

fn client(&self) -> &reqwest::Client { &self.http }

async fn before_request(
&self, req: RequestBuilder,
) -> Result<RequestBuilder, racal::reqwest::ApiError> {
self.rate_limiter.until_ready().await;
Ok(req)
}
}

/// The main API client that's in the process of authentication
///
/// Created with a tuple of the unauthenticated client & authentication,
/// and can always be downgraded into an unauthenticated client.
pub struct AuthenticatingResonite {
base: UnauthenticatedResonite,
data: Authenticating,
}

impl From<(UnauthenticatedResonite, Authenticating)>
for AuthenticatingResonite
{
fn from(value: (UnauthenticatedResonite, Authenticating)) -> Self {
Self { base: value.0, data: value.1 }
}
}

impl From<AuthenticatingResonite> for UnauthenticatedResonite {
fn from(value: AuthenticatingResonite) -> Self { value.base }
}

#[async_trait::async_trait]
impl ApiClient<Authenticating> for AuthenticatingResonite {
fn state(&self) -> &Authenticating { &self.data }

fn client(&self) -> &reqwest::Client { &self.base.http }

async fn before_request(
&self, mut req: RequestBuilder,
) -> Result<RequestBuilder, racal::reqwest::ApiError> {
self.base.rate_limiter.until_ready().await;
req = req.header("UID", &self.data.unique_machine_identifier);
if let Some(second_factor_token) = &self.data.second_factor {
req = req.header("TOTP", second_factor_token);
}

Ok(dbg!(req))
}
}

/// The main API client with authentication
pub struct AuthenticatedResonite {
user_agent: String,
http: Client,
rate_limiter: NormalRateLimiter,
auth: Authentication,
}

#[async_trait::async_trait]
impl ApiClient<Authentication> for AuthenticatedResonite {
fn state(&self) -> &Authentication { &self.auth }

fn client(&self) -> &reqwest::Client { &self.http }

async fn before_request(
&self, req: RequestBuilder,
) -> Result<RequestBuilder, racal::reqwest::ApiError> {
self.rate_limiter.until_ready().await;
Ok(req)
}
}

impl AuthenticatedResonite {
/// Creates an API client
fn http_client(
user_agent: &str, auth: &Authentication,
) -> Result<Client, ApiError> {
use serde::ser::Error;

let builder = Client::builder();
let mut headers = HeaderMap::new();

let (header_name, header_value) = auth.to_header();

headers.insert(
header_name,
header_value.parse().map_err(|_| {
serde_json::Error::custom("Couldn't turn auth into a header")
})?,
);

Ok(builder.user_agent(user_agent).default_headers(headers).build()?)
}

/// Removes authentication to the API client
///
/// # Errors
///
/// If deserializing user agent fails.
pub fn downgrade(self) -> Result<UnauthenticatedResonite, ApiError> {
Ok(UnauthenticatedResonite {
http: UnauthenticatedResonite::http_client(&self.user_agent)?,
rate_limiter: self.rate_limiter,
user_agent: self.user_agent,
})
}

/// Creates a new authenticated Resonite API client
///
/// # Errors
///
/// If deserializing user agent into a header fails
pub fn new(
user_agent: String, auth: impl Into<Authentication> + Send,
) -> Result<Self, ApiError> {
let auth = auth.into();
Ok(Self {
http: Self::http_client(&user_agent, &auth)?,
rate_limiter: http_rate_limiter(),
user_agent,
auth,
})
}
}

impl UnauthenticatedResonite {
/// Creates an unauthenticated API client
fn http_client(user_agent: &str) -> Result<Client, ApiError> {
let mut default_headers = HeaderMap::new();
default_headers.insert(
reqwest::header::ACCEPT,
HeaderValue::from_static("application/json"),
);
Ok(
Client::builder()
.user_agent(user_agent)
.default_headers(default_headers)
.build()?,
)
}

/// Adds authentication to the API client
///
/// # Errors
///
/// If deserializing user agent or authentication fails.
pub fn upgrade(
self, auth: impl Into<Authentication> + Send,
) -> Result<AuthenticatedResonite, ApiError> {
let auth = auth.into();
Ok(AuthenticatedResonite {
http: AuthenticatedResonite::http_client(&self.user_agent, &auth)?,
rate_limiter: self.rate_limiter,
user_agent: self.user_agent,
auth,
})
}

/// Creates a new Resonite API client
///
/// # Errors
///
/// If deserializing user agent into a header fails
pub fn new(user_agent: String) -> Result<Self, ApiError> {
Ok(Self {
http: Self::http_client(&user_agent)?,
rate_limiter: http_rate_limiter(),
user_agent,
})
}
}
Loading

0 comments on commit 55af000

Please sign in to comment.