rosu_render/client/
mod.rs1mod 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#[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 #[must_use]
52 pub fn new() -> Self {
53 Self::default()
54 }
55
56 pub fn builder() -> OrdrClientBuilder {
58 OrdrClientBuilder::new()
59 }
60
61 pub const fn custom_skin_info(&self, id: u32) -> GetSkinCustom<'_> {
65 GetSkinCustom::new(self, id)
66 }
67
68 pub const fn dyn_link(&self, render_id: u32) -> GetDynLink<'_> {
70 GetDynLink::new(self, render_id)
71 }
72
73 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 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 pub const fn render_list(&self) -> GetRenderList<'_> {
95 GetRenderList::new(self)
96 }
97
98 pub const fn server_list(&self) -> GetServerList<'_> {
100 GetServerList::new(self)
101 }
102
103 pub const fn server_online_count(&self) -> GetServerOnlineCount<'_> {
105 GetServerOnlineCount::new(self)
106 }
107
108 pub const fn skin_list(&self) -> GetSkinList<'_> {
110 GetSkinList::new(self)
111 }
112
113 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 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}