1use std::{
2 error::Error as StdError,
3 fmt::{Debug, Display, Formatter, Result as FmtResult},
4 str::from_utf8 as str_from_utf8,
5};
6
7use hyper::{
8 body::{Bytes, Incoming},
9 Response,
10};
11use serde::{
12 de::{Deserializer, Error as DeError, Unexpected, Visitor},
13 Deserialize,
14};
15use serde_json::Error as JsonError;
16use serde_urlencoded::ser::Error as UrlError;
17use thiserror::Error as ThisError;
18
19use crate::model::SkinDeleted;
20
21#[derive(Debug, ThisError)]
22#[non_exhaustive]
23pub enum ClientError {
24 #[error("Failed to build the request")]
25 BuildingRequest {
26 #[source]
27 source: Box<dyn StdError + Send + Sync + 'static>,
28 },
29 #[error("Failed to chunk the response")]
30 ChunkingResponse {
31 #[source]
32 source: hyper::Error,
33 },
34 #[error("Failed to deserialize response body: {body}")]
35 Parsing {
36 body: StringOrBytes,
37 #[source]
38 source: JsonError,
39 },
40 #[error("Parsing or sending the response failed")]
41 RequestError {
42 #[source]
43 source: hyper_util::client::legacy::Error,
44 },
45 #[error("Response error: status code {status_code}, {error}")]
46 Response {
47 body: Bytes,
48 error: ApiError,
49 status_code: u16,
50 },
51 #[error("Failed to serialize the query")]
52 SerdeQuery {
53 #[from]
54 source: UrlError,
55 },
56 #[error("API may be temporarily unavailable (received a 503)")]
57 ServiceUnavailable { response: Response<Incoming> },
58 #[error("Skin was not found (received a 404)")]
59 SkinDeleted { error: SkinDeleted },
60}
61
62impl ClientError {
63 pub(crate) fn response_error(bytes: Bytes, status_code: u16) -> Self {
64 match serde_json::from_slice(&bytes) {
65 Ok(error) => Self::Response {
66 body: bytes,
67 error,
68 status_code,
69 },
70 Err(source) => Self::Parsing {
71 body: bytes.into(),
72 source,
73 },
74 }
75 }
76}
77
78#[derive(Clone, Debug)]
79pub struct StringOrBytes {
80 bytes: Bytes,
81}
82
83impl Display for StringOrBytes {
84 fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
85 match str_from_utf8(&self.bytes) {
86 Ok(string) => f.write_str(string),
87 Err(_) => <[u8] as Debug>::fmt(&*self.bytes, f),
88 }
89 }
90}
91
92impl From<Bytes> for StringOrBytes {
93 fn from(bytes: Bytes) -> Self {
94 Self { bytes }
95 }
96}
97
98#[derive(Debug, Deserialize)]
99pub struct ApiError {
100 pub message: Box<str>,
102 pub reason: Option<Box<str>>,
104 #[serde(rename = "errorCode")]
106 pub code: Option<ErrorCode>,
107}
108
109impl Display for ApiError {
110 fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
111 if let Some(ref code) = self.code {
112 write!(f, "Error code {code}: ")?;
113 }
114
115 f.write_str(&self.message)?;
116
117 if let Some(ref reason) = self.reason {
118 write!(f, " (reason: {reason})")?;
119 }
120
121 Ok(())
122 }
123}
124
125#[derive(Copy, Clone, Debug, ThisError, PartialEq, Eq, Hash)]
129#[non_exhaustive]
130#[repr(u8)]
131pub enum ErrorCode {
132 #[error("Emergency stop (triggered manually)")]
133 EmergencyStop,
134 #[error("Replay download error (bad upload from the sender)")]
135 ReplayParsingError,
136 #[error("Replay download error (bad download from the server), can happen because of invalid characters")]
137 ReplayDownloadError,
138 #[error("All beatmap mirrors are unavailable")]
139 MirrorsUnavailable,
140 #[error("Replay file corrupted")]
141 ReplayFileCorrupted,
142 #[error("Invalid osu! gamemode (not 0 = std)")]
143 InvalidGameMode,
144 #[error("The replay has no input data")]
145 ReplayWithoutInputData,
146 #[error("Beatmap does not exist on osu! (probably because of custom difficulty or non-submitted map)")]
147 BeatmapNotFound,
148 #[error("Audio for the map is unavailable (because of copyright claim)")]
149 BeatmapAudioUnavailable,
150 #[error("Cannot connect to osu! api")]
151 OsuApiConnection,
152 #[error("The replay has the autoplay mod")]
153 ReplayIsAutoplay,
154 #[error("The replay username has invalid characters")]
155 InvalidReplayUsername,
156 #[error("The beatmap is longer than 15 minutes")]
157 BeatmapTooLong,
158 #[error("This player is banned from o!rdr")]
159 PlayerBannedFromOrdr,
160 #[error("Beatmap not found on all the beatmap mirrors")]
161 MapNotFound,
162 #[error("This IP is banned from o!rdr")]
163 IpBannedFromOrdr,
164 #[error("This username is banned from o!rdr")]
165 UsernameBannedFromOrdr,
166 #[error("Unknown error from the renderer")]
167 UnknownRendererError,
168 #[error("The renderer cannot download the map")]
169 CannotDownloadMap,
170 #[error("Beatmap version on the mirror is not the same as the replay")]
171 InconsistentMapVersion,
172 #[error("The replay is corrupted (danser cannot process it)")]
173 ReplayFileCorrupted2,
174 #[error("Server-side problem while finalizing the generated video")]
175 FailedFinalizing,
176 #[error("Server-side problem while preparing the render")]
177 ServerFailedPreparation,
178 #[error("The beatmap has no name")]
179 BeatmapHasNoName,
180 #[error("The replay is missing input data")]
181 ReplayMissingInputData,
182 #[error("The replay has incompatible mods")]
183 ReplayIncompatibleMods,
184 #[error(
185 "Something with the renderer went wrong: it probably has an unstable internet connection \
186 (multiple renders at the same time)"
187 )]
188 RendererIssue,
189 #[error("The renderer cannot download the replay")]
190 CannotDownloadReplay,
191 #[error("The replay is already rendering or in queue")]
192 ReplayAlreadyInQueue,
193 #[error("The star rating is greater than 20")]
194 StarRatingTooHigh,
195 #[error("The mapper is blacklisted")]
196 MapperIsBlacklisted,
197 #[error("The beatmapset is blacklisted")]
198 BeatmapsetIsBlacklisted,
199 #[error("The replay has already errored less than an hour ago")]
200 ReplayErroredRecently,
201 #[error("invalid replay URL or can't download the replay (if replayURL is provided)")]
202 InvalidReplayUrl,
203 #[error("a required field is missing (the missing field is shown in the message)")]
204 MissingField,
205 #[error("your last replays have a too high error rate (cannot be triggered when you're a verified bot)")]
206 ErrorRateTooHigh,
207 #[error("the replay username is inappropriate")]
208 InappropriateUsername,
209 #[error("this skin does not exist")]
210 SkinDoesNotExist,
211 #[error("this custom skin does not exist or has been deleted")]
212 CustomSkinDoesNotExist,
213 #[error("o!rdr is not ready to take render jobs at the moment")]
214 RenderJobsPaused,
215 #[error("o!rdr is not ready to take render jobs from unauthenticated users at the moment (verified bots are not authenticated users)")]
216 UnauthenticatedRenderJobsPaused,
217 #[error("replay accuracy is too bad and you're not authenticated")]
218 AccuracyTooLow,
219 #[error("this score does not exist")]
220 ScoreDoesNotExist,
221 #[error("the replay for this score isn't available")]
222 ReplayUnavailable,
223 #[error("invalid osu! ruleset score ID")]
224 InvalidRulesetId,
225 #[error("Unknown error code {0}")]
226 Other(u8),
227}
228
229impl ErrorCode {
230 #[must_use]
231 pub fn to_u8(self) -> u8 {
232 match self {
233 Self::EmergencyStop => 1,
234 Self::ReplayParsingError => 2,
235 Self::ReplayDownloadError => 3,
236 Self::MirrorsUnavailable => 4,
237 Self::ReplayFileCorrupted => 5,
238 Self::InvalidGameMode => 6,
239 Self::ReplayWithoutInputData => 7,
240 Self::BeatmapNotFound => 8,
241 Self::BeatmapAudioUnavailable => 9,
242 Self::OsuApiConnection => 10,
243 Self::ReplayIsAutoplay => 11,
244 Self::InvalidReplayUsername => 12,
245 Self::BeatmapTooLong => 13,
246 Self::PlayerBannedFromOrdr => 14,
247 Self::MapNotFound => 15,
248 Self::IpBannedFromOrdr => 16,
249 Self::UsernameBannedFromOrdr => 17,
250 Self::UnknownRendererError => 18,
251 Self::CannotDownloadMap => 19,
252 Self::InconsistentMapVersion => 20,
253 Self::ReplayFileCorrupted2 => 21,
254 Self::FailedFinalizing => 22,
255 Self::ServerFailedPreparation => 23,
256 Self::BeatmapHasNoName => 24,
257 Self::ReplayMissingInputData => 25,
258 Self::ReplayIncompatibleMods => 26,
259 Self::RendererIssue => 27,
260 Self::CannotDownloadReplay => 28,
261 Self::ReplayAlreadyInQueue => 29,
262 Self::StarRatingTooHigh => 30,
263 Self::MapperIsBlacklisted => 31,
264 Self::BeatmapsetIsBlacklisted => 32,
265 Self::ReplayErroredRecently => 33,
266 Self::InvalidReplayUrl => 34,
267 Self::MissingField => 35,
268 Self::ErrorRateTooHigh => 36,
269 Self::InappropriateUsername => 37,
270 Self::SkinDoesNotExist => 38,
271 Self::CustomSkinDoesNotExist => 39,
272 Self::RenderJobsPaused => 40,
273 Self::UnauthenticatedRenderJobsPaused => 41,
274 Self::AccuracyTooLow => 42,
275 Self::ScoreDoesNotExist => 43,
276 Self::ReplayUnavailable => 44,
277 Self::InvalidRulesetId => 45,
278 Self::Other(code) => code,
279 }
280 }
281}
282
283impl From<u8> for ErrorCode {
284 fn from(code: u8) -> Self {
285 match code {
286 1 => Self::EmergencyStop,
287 2 => Self::ReplayParsingError,
288 5 => Self::ReplayFileCorrupted,
289 6 => Self::InvalidGameMode,
290 7 => Self::ReplayWithoutInputData,
291 8 => Self::BeatmapNotFound,
292 9 => Self::BeatmapAudioUnavailable,
293 10 => Self::OsuApiConnection,
294 11 => Self::ReplayIsAutoplay,
295 12 => Self::InvalidReplayUsername,
296 13 => Self::BeatmapTooLong,
297 14 => Self::PlayerBannedFromOrdr,
298 16 => Self::IpBannedFromOrdr,
299 17 => Self::UsernameBannedFromOrdr,
300 23 => Self::ServerFailedPreparation,
301 24 => Self::BeatmapHasNoName,
302 25 => Self::ReplayMissingInputData,
303 26 => Self::ReplayIncompatibleMods,
304 29 => Self::ReplayAlreadyInQueue,
305 30 => Self::StarRatingTooHigh,
306 31 => Self::MapperIsBlacklisted,
307 32 => Self::BeatmapsetIsBlacklisted,
308 33 => Self::ReplayErroredRecently,
309 34 => Self::InvalidReplayUrl,
310 35 => Self::MissingField,
311 36 => Self::ErrorRateTooHigh,
312 37 => Self::InappropriateUsername,
313 38 => Self::SkinDoesNotExist,
314 39 => Self::CustomSkinDoesNotExist,
315 40 => Self::RenderJobsPaused,
316 41 => Self::UnauthenticatedRenderJobsPaused,
317 42 => Self::AccuracyTooLow,
318 43 => Self::ScoreDoesNotExist,
319 44 => Self::ReplayUnavailable,
320 45 => Self::InvalidRulesetId,
321 other => Self::Other(other),
322 }
323 }
324}
325
326impl<'de> Deserialize<'de> for ErrorCode {
327 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
328 struct ErrorCodeVisitor;
329
330 impl Visitor<'_> for ErrorCodeVisitor {
331 type Value = ErrorCode;
332
333 fn expecting(&self, f: &mut Formatter<'_>) -> FmtResult {
334 f.write_str("u8")
335 }
336
337 fn visit_u8<E: DeError>(self, v: u8) -> Result<Self::Value, E> {
338 Ok(ErrorCode::from(v))
339 }
340
341 fn visit_u64<E: DeError>(self, v: u64) -> Result<Self::Value, E> {
342 let code = u8::try_from(v).map_err(|_| {
343 DeError::invalid_value(Unexpected::Unsigned(v), &"a valid error code")
344 })?;
345
346 self.visit_u8(code)
347 }
348 }
349
350 d.deserialize_u8(ErrorCodeVisitor)
351 }
352}