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, GetUserPreset, 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    /// Get preset render settings of a discord user.
108    ///
109    /// `key` is a verified bot key and `discord_id` belongs to the user whose
110    /// preset should be fetched.
111    pub const fn user_preset<'a>(&'a self, key: &'a str, discord_id: u64) -> GetUserPreset<'a> {
112        GetUserPreset::new(self, key, discord_id)
113    }
114
115    pub(crate) fn verification(&self) -> Option<&Verification> {
116        self.inner.verification.as_ref()
117    }
118
119    pub(crate) fn request<T>(&self, req: Request) -> OrdrFuture<T> {
120        self.try_request::<T>(req).unwrap_or_else(OrdrFuture::error)
121    }
122
123    fn try_request<T>(&self, req: Request) -> Result<OrdrFuture<T>, ClientError> {
124        let Request {
125            form,
126            method,
127            path,
128            ratelimiter,
129        } = req;
130
131        let inner = self.try_request_raw(form, method, &path)?;
132
133        Ok(OrdrFuture::new(
134            Box::pin(inner),
135            self.inner.ratelimiter.get(ratelimiter).acquire_owned(1),
136        ))
137    }
138
139    fn try_request_raw(
140        &self,
141        form: Option<Form>,
142        method: Method,
143        path: &str,
144    ) -> Result<ResponseFuture, ClientError> {
145        let mut url = String::with_capacity(BASE_URL.len() + path.len());
146        url.push_str(BASE_URL);
147        url.push_str(path);
148        debug!(?url);
149
150        debug_assert!(method != Method::POST || form.is_some());
151
152        let mut builder = HyperRequest::builder().method(method).uri(&url);
153
154        if let Some(headers) = builder.headers_mut() {
155            if let Some(ref form) = form {
156                headers.insert(CONTENT_LENGTH, HeaderValue::from(form.len()));
157
158                if let Ok(content_type) = HeaderValue::try_from(form.content_type()) {
159                    headers.insert(CONTENT_TYPE, content_type);
160                }
161            }
162
163            headers.insert(USER_AGENT, HeaderValue::from_static(ROSU_RENDER_USER_AGENT));
164        }
165
166        let try_req = if let Some(form) = form {
167            builder.body(Full::from(form.build()))
168        } else {
169            builder.body(Full::default())
170        };
171
172        let req = try_req.map_err(|source| ClientError::BuildingRequest {
173            source: Box::new(source),
174        })?;
175
176        Ok(self.inner.http.request(req))
177    }
178}
179
180impl Default for OrdrClient {
181    fn default() -> Self {
182        Self::builder().build()
183    }
184}