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