rosu_render/client/
error.rs

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    /// The response of the server.
105    pub message: Box<str>,
106    /// The reason of the ban (if provided by admins).
107    pub reason: Option<Box<str>>,
108    /// The error code of the creation of this render.
109    #[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    /// Error codes as defined by o!rdr
172    ///
173    /// See <https://ordr.issou.best/docs/#section/Error-codes>
174    #[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}