Skip to main content

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, GetDynLink, GetRenderList, GetServerList, GetServerOnlineCount,
26        GetSkinCustom, GetSkinList, GetUserPreset, OrdrFuture, Request,
27    },
28    util::multipart::Form,
29};
30
31const API_HOST: &str = "https://apis.issou.best";
32const BASE_URL: &str = "https://apis.issou.best/ordr/";
33const ROSU_RENDER_USER_AGENT: &str = concat!("rosu-render (", env!("CARGO_PKG_VERSION"), ")");
34
35/// Client to access the o!rdr API.
36///
37/// Cheap to clone.
38#[derive(Clone)]
39pub struct OrdrClient {
40    inner: Arc<OrdrRef>,
41}
42
43struct OrdrRef {
44    pub(super) http: HyperClient<Connector, Full<Bytes>>,
45    pub(super) ratelimiter: Ratelimiter,
46    pub(super) verification: Option<Verification>,
47}
48
49impl OrdrClient {
50    /// Create a new [`OrdrClient`] based on a default [`OrdrClientBuilder`].
51    #[must_use]
52    pub fn new() -> Self {
53        Self::default()
54    }
55
56    /// Create a new builder to create an [`OrdrClient`].
57    pub fn builder() -> OrdrClientBuilder {
58        OrdrClientBuilder::new()
59    }
60
61    /// Get info of a custom skin.
62    ///
63    /// You must provide the ID of the custom skin.
64    pub const fn custom_skin_info(&self, id: u32) -> GetSkinCustom<'_> {
65        GetSkinCustom::new(self, id)
66    }
67
68    /// Generate a temporary video download link for a render.
69    pub const fn dyn_link(&self, render_id: u32) -> GetDynLink<'_> {
70        GetDynLink::new(self, render_id)
71    }
72
73    /// Send a render request to o!rdr via replay file.
74    pub const fn render_with_replay_file<'a>(
75        &'a self,
76        replay_file: &'a [u8],
77        username: &'a str,
78        skin: &'a RenderSkinOption<'a>,
79    ) -> CommissionRender<'a> {
80        CommissionRender::with_file(self, replay_file, username, skin)
81    }
82
83    /// Send a render request to o!rdr via replay url.
84    pub const fn render_with_replay_url<'a>(
85        &'a self,
86        url: &'a str,
87        username: &'a str,
88        skin: &'a RenderSkinOption<'a>,
89    ) -> CommissionRender<'a> {
90        CommissionRender::with_url(self, url, username, skin)
91    }
92
93    /// Get a paginated list of all renders.
94    pub const fn render_list(&self) -> GetRenderList<'_> {
95        GetRenderList::new(self)
96    }
97
98    /// Get a list of available servers.
99    pub const fn server_list(&self) -> GetServerList<'_> {
100        GetServerList::new(self)
101    }
102
103    /// Get the amount of online servers.
104    pub const fn server_online_count(&self) -> GetServerOnlineCount<'_> {
105        GetServerOnlineCount::new(self)
106    }
107
108    /// Get a paginated list of all available skins.
109    pub const fn skin_list(&self) -> GetSkinList<'_> {
110        GetSkinList::new(self)
111    }
112
113    /// Get preset render settings of a discord user.
114    ///
115    /// `key` is a verified bot key and `discord_id` belongs to the user whose
116    /// preset should be fetched.
117    pub const fn user_preset<'a>(&'a self, key: &'a str, discord_id: u64) -> GetUserPreset<'a> {
118        GetUserPreset::new(self, key, discord_id)
119    }
120
121    pub(crate) fn verification(&self) -> Option<&Verification> {
122        self.inner.verification.as_ref()
123    }
124
125    pub(crate) fn request<T>(&self, req: Request) -> OrdrFuture<T> {
126        self.try_request::<T>(req).unwrap_or_else(OrdrFuture::error)
127    }
128
129    fn try_request<T>(&self, req: Request) -> Result<OrdrFuture<T>, ClientError> {
130        let Request {
131            form,
132            method,
133            path,
134            ratelimiter,
135        } = req;
136
137        let inner = self.try_request_raw(form, method, &path)?;
138
139        Ok(OrdrFuture::new(
140            Box::pin(inner),
141            self.inner.ratelimiter.get(ratelimiter).acquire_owned(1),
142        ))
143    }
144
145    fn try_request_raw(
146        &self,
147        form: Option<Form>,
148        method: Method,
149        path: &str,
150    ) -> Result<ResponseFuture, ClientError> {
151        // Routes starting with `/` are absolute to the API host; all others are
152        // relative to the `/ordr/` base path.
153        let base = if path.starts_with('/') {
154            API_HOST
155        } else {
156            BASE_URL
157        };
158        let mut url = String::with_capacity(base.len() + path.len());
159        url.push_str(base);
160        url.push_str(path);
161        debug!(?url);
162
163        debug_assert!(method != Method::POST || form.is_some());
164
165        let mut builder = HyperRequest::builder().method(method).uri(&url);
166
167        if let Some(headers) = builder.headers_mut() {
168            if let Some(ref form) = form {
169                headers.insert(CONTENT_LENGTH, HeaderValue::from(form.len()));
170
171                if let Ok(content_type) = HeaderValue::try_from(form.content_type()) {
172                    headers.insert(CONTENT_TYPE, content_type);
173                }
174            }
175
176            headers.insert(USER_AGENT, HeaderValue::from_static(ROSU_RENDER_USER_AGENT));
177        }
178
179        let try_req = if let Some(form) = form {
180            builder.body(Full::from(form.build()))
181        } else {
182            builder.body(Full::default())
183        };
184
185        let req = try_req.map_err(|source| ClientError::BuildingRequest {
186            source: Box::new(source),
187        })?;
188
189        Ok(self.inner.http.request(req))
190    }
191}
192
193impl Default for OrdrClient {
194    fn default() -> Self {
195        Self::builder().build()
196    }
197}