roblox_api/
validation.rs

1use base64::{Engine, prelude::BASE64_STANDARD};
2use reqwest::{Response, header::HeaderValue};
3use serde::{Deserialize, Serialize};
4
5use crate::{
6    ApiError, Currency, Error,
7    api::auth,
8    challenge::{
9        CHALLENGE_ID_HEADER, CHALLENGE_METADATA_HEADER, CHALLENGE_TYPE_HEADER, Challenge,
10        ChallengeMetadata, ChallengeType, ChefChallengeMetadata,
11    },
12    client::ClientRequestor,
13    ratelimit::{
14        RATELIMIT_LIMIT_HEADER, RATELIMIT_REMAINING_HEADER, RATELIMIT_RESET_HEADER, Ratelimit,
15    },
16};
17
18const TOKEN_HEADER: &str = "x-csrf-token";
19
20#[derive(Debug, Deserialize, Serialize)]
21pub struct ErrorJson {
22    //code: u8,
23    message: String,
24}
25
26#[derive(Debug, Deserialize, Serialize)]
27pub struct ErrorsJson {
28    errors: Vec<ErrorJson>,
29}
30
31#[derive(Debug, Deserialize, Serialize)]
32pub struct DataErrorJson {
33    //#[serde(rename = "isValid")]
34    //is_valid: bool,
35    //data: Option<String>, // maybe, i always got null,
36    #[serde(rename = "error")]
37    message: String,
38}
39
40fn ratelimit_from_headers(
41    limit: Option<&HeaderValue>,
42    reset: Option<&HeaderValue>,
43    remaining: Option<&HeaderValue>,
44) -> Option<Ratelimit> {
45    if let (Some(_limit), Some(reset), Some(remaining)) = (limit, reset, remaining) {
46        let remaining = remaining.to_str().unwrap().parse::<u32>().unwrap();
47        let reset_in_seconds = reset.to_str().unwrap().parse::<u32>().unwrap();
48
49        Some(Ratelimit {
50            remaining,
51            reset_in_seconds,
52            // TODO: honestly i don't know how to parse this
53            windows: Vec::new(),
54        })
55    } else {
56        None
57    }
58}
59
60fn challenge_from_headers(
61    id: Option<HeaderValue>,
62    kind: Option<HeaderValue>,
63    metadata_b64: Option<HeaderValue>,
64) -> Option<Challenge> {
65    if let (Some(id), Some(kind), Some(metadata_b64)) = (id, kind, metadata_b64) {
66        let kind = ChallengeType::from(kind.to_str().unwrap());
67        match kind {
68            ChallengeType::Chef => {
69                let _metadata: ChefChallengeMetadata = serde_json::from_slice(
70                    BASE64_STANDARD
71                        .decode(metadata_b64.to_str().unwrap())
72                        .unwrap()
73                        .as_slice(),
74                )
75                .unwrap();
76
77                todo!("Unsupported challenge-type: \"chef\"");
78            }
79
80            ChallengeType::Captcha => {
81                todo!("Unsupported challenge-type: \"captcha\"")
82            }
83
84            _ => {
85                let metadata: ChallengeMetadata = serde_json::from_slice(
86                    BASE64_STANDARD
87                        .decode(metadata_b64.to_str().unwrap())
88                        .unwrap()
89                        .as_slice(),
90                )
91                .unwrap();
92
93                Some(Challenge {
94                    id: id.to_str().unwrap().to_string(),
95                    kind,
96                    metadata,
97                })
98            }
99        }
100    } else {
101        None
102    }
103}
104
105impl ClientRequestor {
106    fn set_token(&mut self, token: &str) {
107        self.default_headers
108            .insert(TOKEN_HEADER, HeaderValue::from_str(token).unwrap());
109    }
110
111    // NOTE: this doesn't work on all apis, since some apis expect a custom token,
112    // you'll know which ones are affected based on the `TokenValidation` error
113    pub(crate) async fn ensure_token(&mut self) -> Result<(), Error> {
114        let result = self
115            .client
116            .post(format!("{}//", auth::URL))
117            .headers(self.default_headers.clone())
118            .send()
119            .await;
120
121        let result = self.validate_response(result).await;
122
123        if let Err(Error::ApiError(ApiError::TokenValidation)) = result {
124            return Ok(());
125        }
126
127        if result.is_err() {
128            return Err(result.err().unwrap());
129        }
130
131        Ok(())
132    }
133
134    pub(crate) async fn validate_response(
135        &mut self,
136        result: Result<Response, reqwest::Error>,
137    ) -> Result<Response, Error> {
138        // remove all challenge headers after validation
139        self.remove_challenge();
140
141        match result {
142            Ok(response) => {
143                let code = response.status().as_u16();
144
145                let token = response.headers().get(TOKEN_HEADER);
146                if let Some(token) = token {
147                    // EVERYTHING must be mutable to do this, perhaps there's another datatype we can use
148                    self.set_token(String::from_utf8_lossy(token.as_bytes()).as_ref());
149                }
150
151                {
152                    let limit = response.headers().get(RATELIMIT_LIMIT_HEADER);
153                    let reset = response.headers().get(RATELIMIT_RESET_HEADER);
154                    let remaining = response.headers().get(RATELIMIT_REMAINING_HEADER);
155
156                    self.ratelimit = ratelimit_from_headers(limit, reset, remaining);
157                }
158
159                // TODO: some apis like the data api can return an error even with status_code 200
160                if code == 200 {
161                    return Ok(response);
162                }
163
164                let challenge_id = response.headers().get(CHALLENGE_ID_HEADER).cloned();
165                let challenge_type = response.headers().get(CHALLENGE_TYPE_HEADER).cloned();
166                let challenge_metadata_b64 =
167                    response.headers().get(CHALLENGE_METADATA_HEADER).cloned();
168
169                let bytes = response.bytes().await.unwrap().to_owned();
170                let errors = if let Ok(errors) = serde_json::from_slice::<ErrorsJson>(&bytes) {
171                    errors
172                } else if let Ok(error) = serde_json::from_slice::<ErrorJson>(&bytes) {
173                    ErrorsJson {
174                        errors: vec![error],
175                    }
176                } else if let Ok(error) = serde_json::from_slice::<DataErrorJson>(&bytes) {
177                    ErrorsJson {
178                        errors: vec![ErrorJson {
179                            //code: 0,
180                            message: error.message,
181                        }],
182                    }
183                } else {
184                    ErrorsJson {
185                        errors: vec![ErrorJson {
186                            //code: 0,
187                            message: String::from_utf8_lossy(&bytes).to_string(),
188                        }],
189                    }
190                };
191
192                match code {
193                    401 => Err(Error::ApiError(ApiError::Unauthorized)),
194                    429 => Err(Error::ApiError(ApiError::Ratelimited)),
195                    500 => Err(Error::ApiError(ApiError::Internal)),
196                    _ => {
197                        let errors: Vec<ApiError> = errors
198                            .errors
199                            .iter()
200                            .map(|x| match x.message.as_str() {
201                                // 400 
202                                "The asset id is invalid." => ApiError::InvalidAssetId,
203                                "Invalid challenge ID." => ApiError::InvalidChallengeId,
204
205                                "User not found." => ApiError::InvalidUser,
206                                "The user is invalid or does not exist." => ApiError::InvalidUser,
207                                "The target user is invalid or does not exist." => ApiError::InvalidUser,
208                                "The user ID is invalid." => ApiError::InvalidUserId,
209
210                                "The gender provided is invalid." => ApiError::InvalidGender,
211                                "The two step verification challenge code is invalid." => {
212                                    ApiError::InvalidTwoStepVerificationCode
213                                }
214
215                                "Invalid display name." => ApiError::InvalidDisplayName,
216
217                                "Request must contain a birthdate" => {
218                                    ApiError::RequestMissingArgument("Birthdate".to_string())
219                                }
220
221                                "Ascending sort order is not supported for user's favorite games." => {
222                                    ApiError::UnsupportedSortOrder
223                                }
224
225                                // 403
226                                "Token Validation Failed"
227                                | "XSRF token invalid"
228                                | "XSRF Token Validation Failed"
229                                | "\"XSRF Token Validation Failed\"" => ApiError::TokenValidation,
230
231                                "Not authorized." => ApiError::Unauthorized,
232
233                                "Incorrect username or password. Please try again." => {
234                                    ApiError::InvalidCredentials
235                                }
236
237                                "You must pass the robot test before logging in." => {
238                                    ApiError::CaptchaFailed
239                                }
240
241                                "Account has been locked. Please request a password reset." => {
242                                    ApiError::AccontLocked
243                                }
244
245                                "Unable to login. Please use Social Network sign on." => {
246                                    ApiError::SocialNetworkLoginRequired
247                                }
248
249                                "Account issue. Please contact Support." => {
250                                    ApiError::AccountIssue
251                                }
252
253                                "Unable to login with provided credentials. Default login is required." => {
254                                    ApiError::DefaultLoginRequired
255                                }
256
257                                "Received credentials are unverified." => {
258                                    ApiError::UnverifiedCredentials
259                                }
260
261                                "Existing login session found. Please log out first." => {
262                                    ApiError::ExistingLoginSession
263                                }
264
265                                "The account is unable to log in. Please log in to the LuoBu app." => {
266                                    ApiError::LuoBuAppLoginRequired
267                                }
268
269                                "Too many attempts. Please wait a bit." => {
270                                    ApiError::Ratelimited
271                                }
272
273                                "The account is unable to login. Please log in with the VNG app." => {
274                                    ApiError::VNGAppLoginRequired
275                                }
276
277                                "PIN is locked." => ApiError::PinIsLocked,
278
279                                "Invalid birthdate change." => ApiError::InvalidBirthdate,
280
281                                "Insufficient Robux funds." => ApiError::NotEnoughFunds(Currency::Robux),
282
283                                // TODO: not sure what this means please use more verbose todo messages
284                                // TODO: add missing challenge duplicate code
285
286                                "Challenge is required to authorize the request" => {
287                                    let challenge = challenge_from_headers(
288                                        challenge_id.clone(),
289                                        challenge_type.clone(),
290                                        challenge_metadata_b64.clone(),
291                                    );
292                                    ApiError::ChallengeRequired(challenge.unwrap())
293                                }
294
295                                "Challenge failed to authorize request" => {
296                                    ApiError::ChallengeFailed
297                                }
298
299                                "You do not have permission to view the owners of this asset." => {
300                                    ApiError::PermissionError
301                                }
302
303                                "Request Context BrowserTrackerID is missing or invalid." => {
304                                    ApiError::InvalidBrowserTrackerId
305                                }
306
307                                "Attempt to add non-friends to a conversation" => ApiError::ConversationUserAddFailed,
308                                "Attempt to create conversations with non-friends" => ApiError::ConversationCreationFailed,
309                                "{\"Error\":\"OneToOne conversations cannot be updated\"}" => ApiError::InvalidConversation,
310
311                                "an internal error occurred" => ApiError::Internal,
312
313                                // 404
314                                "Badge is invalid or does not exist." => ApiError::InvalidBadge,
315
316
317                                // 409
318                                "You are already a member of this group." => ApiError::AlreadyInGroup,
319                                "You have already requested to join this group." => ApiError::AlreadyInGroupRequests,
320
321                                _ => {
322                                    ApiError::Unknown(code, Some(x.message.to_owned()))
323                                },
324                            }).collect();
325
326                        if errors.len() == 1 {
327                            Err(Error::ApiError(errors.first().unwrap().clone()))
328                        } else {
329                            Err(Error::ApiError(ApiError::Multiple(errors)))
330                        }
331                    }
332                }
333            }
334
335            Err(error) => Err(Error::ReqwestError(error)),
336        }
337    }
338}