rosu_render/client/
mod.rs

1mod builder;
2mod connector;
3mod ratelimiter;
4
5pub mod error;
6
7use std::sync::Arc;
8
9use bytes::Bytes;
10use http_body_util::Full;
11use hyper::{
12    header::{CONTENT_LENGTH, CONTENT_TYPE, USER_AGENT},
13    http::HeaderValue,
14    Method, Request as HyperRequest,
15};
16use hyper_util::client::legacy::{Client as HyperClient, ResponseFuture};
17
18pub use self::builder::OrdrClientBuilder;
19pub(crate) use self::ratelimiter::RatelimiterKind;
20use self::{connector::Connector, error::ClientError, ratelimiter::Ratelimiter};
21
22use crate::{
23    model::{RenderSkinOption, Verification},
24    request::{
25        CommissionRender, GetRenderList, GetServerList, GetServerOnlineCount, GetSkinCustom,
26        GetSkinList, OrdrFuture, Request,
27    },
28    util::multipart::Form,
29};
30
31const BASE_URL: &str = "https://apis.issou.best/ordr/";
32const ROSU_RENDER_USER_AGENT: &str = concat!("rosu-render (", env!("CARGO_PKG_VERSION"), ")");
33
34/// Client to access the o!rdr API.
35///
36/// Cheap to clone.
37#[derive(Clone)]
38pub struct OrdrClient {
39    inner: Arc<OrdrRef>,
40}
41
42struct OrdrRef {
43    pub(super) http: HyperClient<Connector, Full<Bytes>>,
44    pub(super) ratelimiter: Ratelimiter,
45    pub(super) verification: Option<Verification>,
46}
47
48impl OrdrClient {
49    /// Create a new [`OrdrClient`] based on a default [`OrdrClientBuilder`].
50    #[must_use]
51    pub fn new() -> Self {
52        Self::default()
53    }
54
55    /// Create a new builder to create an [`OrdrClient`].
56    pub fn builder() -> OrdrClientBuilder {
57        OrdrClientBuilder::new()
58    }
59
60    /// Get info of a custom skin.
61    ///
62    /// You must provide the ID of the custom skin.
63    pub const fn custom_skin_info(&self, id: u32) -> GetSkinCustom<'_> {
64        GetSkinCustom::new(self, id)
65    }
66
67    /// Send a render request to o!rdr via replay file.
68    pub const fn render_with_replay_file<'a>(
69        &'a self,
70        replay_file: &'a [u8],
71        username: &'a str,
72        skin: &'a RenderSkinOption<'a>,
73    ) -> CommissionRender<'a> {
74        CommissionRender::with_file(self, replay_file, username, skin)
75    }
76
77    /// Send a render request to o!rdr via replay url.
78    pub const fn render_with_replay_url<'a>(
79        &'a self,
80        url: &'a str,
81        username: &'a str,
82        skin: &'a RenderSkinOption<'a>,
83    ) -> CommissionRender<'a> {
84        CommissionRender::with_url(self, url, username, skin)
85    }
86
87    /// Get a paginated list of all renders.
88    pub const fn render_list(&self) -> GetRenderList<'_> {
89        GetRenderList::new(self)
90    }
91
92    /// Get a list of available servers.
93    pub const fn server_list(&self) -> GetServerList<'_> {
94        GetServerList::new(self)
95    }
96
97    /// Get the amount of online servers.
98    pub const fn server_online_count(&self) -> GetServerOnlineCount<'_> {
99        GetServerOnlineCount::new(self)
100    }
101
102    /// Get a paginated list of all available skins.
103    pub const fn skin_list(&self) -> GetSkinList<'_> {
104        GetSkinList::new(self)
105    }
106
107    pub(crate) fn verification(&self) -> Option<&Verification> {
108        self.inner.verification.as_ref()
109    }
110
111    pub(crate) fn request<T>(&self, req: Request) -> OrdrFuture<T> {
112        self.try_request::<T>(req).unwrap_or_else(OrdrFuture::error)
113    }
114
115    fn try_request<T>(&self, req: Request) -> Result<OrdrFuture<T>, ClientError> {
116        let Request {
117            form,
118            method,
119            path,
120            ratelimiter,
121        } = req;
122
123        let inner = self.try_request_raw(form, method, &path)?;
124
125        Ok(OrdrFuture::new(
126            Box::pin(inner),
127            self.inner.ratelimiter.get(ratelimiter).acquire_owned(1),
128        ))
129    }
130
131    fn try_request_raw(
132        &self,
133        form: Option<Form>,
134        method: Method,
135        path: &str,
136    ) -> Result<ResponseFuture, ClientError> {
137        let mut url = String::with_capacity(BASE_URL.len() + path.len());
138        url.push_str(BASE_URL);
139        url.push_str(path);
140        debug!(?url);
141
142        debug_assert!(method != Method::POST || form.is_some());
143
144        let mut builder = HyperRequest::builder().method(method).uri(&url);
145
146        if let Some(headers) = builder.headers_mut() {
147            if let Some(ref form) = form {
148                headers.insert(CONTENT_LENGTH, HeaderValue::from(form.len()));
149
150                if let Ok(content_type) = HeaderValue::try_from(form.content_type()) {
151                    headers.insert(CONTENT_TYPE, content_type);
152                }
153            }
154
155            headers.insert(USER_AGENT, HeaderValue::from_static(ROSU_RENDER_USER_AGENT));
156        }
157
158        let try_req = if let Some(form) = form {
159            builder.body(Full::from(form.build()))
160        } else {
161            builder.body(Full::default())
162        };
163
164        let req = try_req.map_err(|source| ClientError::BuildingRequest {
165            source: Box::new(source),
166        })?;
167
168        Ok(self.inner.http.request(req))
169    }
170}
171
172impl Default for OrdrClient {
173    fn default() -> Self {
174        Self::builder().build()
175    }
176}