roblox_api/
validation.rs

1use base64::{Engine, prelude::BASE64_STANDARD};
2use reqwest::{Response, header::HeaderValue};
3use serde::{Deserialize, Serialize};
4
5use crate::{
6    ApiError, Error,
7    api::auth,
8    challenge::{
9        CHALLENGE_ID_HEADER, CHALLENGE_METADATA_HEADER, CHALLENGE_TYPE_HEADER, Challenge,
10        ChallengeMetadata, ChallengeType, ChefChallengeMetadata,
11    },
12    client::Client,
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 Client {
106    fn set_token(&mut self, token: &str) {
107        self.requestor
108            .default_headers
109            .insert(TOKEN_HEADER, HeaderValue::from_str(token).unwrap());
110    }
111
112    // NOTE: this doesn't work on all apis, since some apis expect a custom token,
113    // you'll know which ones are affected based on the `TokenValidation` error
114    pub async fn ensure_token(&mut self) -> Result<(), Error> {
115        let result = self
116            .requestor
117            .client
118            .post(format!("{}//", auth::URL))
119            .headers(self.requestor.default_headers.clone())
120            .send()
121            .await;
122
123        let result = self.validate_response(result).await;
124
125        if let Err(Error::ApiError(ApiError::TokenValidation)) = result {
126            return Ok(());
127        }
128
129        if result.is_err() {
130            return Err(result.err().unwrap());
131        }
132
133        Ok(())
134    }
135
136    // TODO: test if account is terminated
137    // TODO: add reactivate account function
138    // pub async fn test_account_status() {}
139
140    pub(crate) async fn validate_response(
141        &mut self,
142        result: Result<Response, reqwest::Error>,
143    ) -> Result<Response, Error> {
144        // remove all challenge headers after validation
145        self.remove_challenge();
146
147        match result {
148            Ok(response) => {
149                let code = response.status().as_u16();
150
151                let token = response.headers().get(TOKEN_HEADER);
152                if let Some(token) = token {
153                    // EVERYTHING must be mutable to do this, perhaps there's another datatype we can use
154                    self.set_token(String::from_utf8_lossy(token.as_bytes()).as_ref());
155                }
156
157                {
158                    let limit = response.headers().get(RATELIMIT_LIMIT_HEADER);
159                    let reset = response.headers().get(RATELIMIT_RESET_HEADER);
160                    let remaining = response.headers().get(RATELIMIT_REMAINING_HEADER);
161
162                    self.ratelimit = ratelimit_from_headers(limit, reset, remaining);
163                }
164
165                // TODO: some apis like the data api can return an error even with status_code 200
166                if code == 200 {
167                    return Ok(response);
168                }
169
170                let challenge_id = response.headers().get(CHALLENGE_ID_HEADER).cloned();
171                let challenge_type = response.headers().get(CHALLENGE_TYPE_HEADER).cloned();
172                let challenge_metadata_b64 =
173                    response.headers().get(CHALLENGE_METADATA_HEADER).cloned();
174
175                let bytes = response.bytes().await.unwrap().to_owned();
176                let errors = if let Ok(errors) = serde_json::from_slice::<ErrorsJson>(&bytes) {
177                    errors
178                } else if let Ok(error) = serde_json::from_slice::<ErrorJson>(&bytes) {
179                    ErrorsJson {
180                        errors: vec![error],
181                    }
182                } else if let Ok(error) = serde_json::from_slice::<DataErrorJson>(&bytes) {
183                    ErrorsJson {
184                        errors: vec![ErrorJson {
185                            //code: 0,
186                            message: error.message,
187                        }],
188                    }
189                } else {
190                    ErrorsJson {
191                        errors: vec![ErrorJson {
192                            //code: 0,
193                            message: String::from_utf8_lossy(&bytes).to_string(),
194                        }],
195                    }
196                };
197
198                match code {
199                    401 => Err(Error::ApiError(ApiError::Unauthorized)),
200                    429 => Err(Error::ApiError(ApiError::Ratelimited)),
201                    500 => Err(Error::ApiError(ApiError::Internal)),
202                    _ => {
203                        let errors: Vec<ApiError> = errors
204                            .errors
205                            .iter()
206                            .map(|x| match x.message.as_str() {
207                                // 400 
208                                "The asset id is invalid." => ApiError::InvalidAssetId,
209                                "Invalid challenge ID." => ApiError::InvalidChallengeId,
210                                "User not found." => ApiError::InvalidUser,
211                                "The user is invalid or does not exist." => ApiError::InvalidUser,
212                                "The user ID is invalid." => ApiError::InvalidUserId,
213                                "The gender provided is invalid." => ApiError::InvalidGender,
214                                "The two step verification challenge code is invalid." => {
215                                    ApiError::InvalidTwoStepVerificationCode
216                                }
217
218                                "Invalid display name." => ApiError::InvalidDisplayName,
219
220                                "Request must contain a birthdate" => {
221                                    ApiError::RequestMissingArgument("Birthdate".to_string())
222                                }
223
224                                "Ascending sort order is not supported for user's favorite games." => {
225                                    ApiError::UnsupportedSortOrder
226                                }
227
228                                // 403
229                                "Token Validation Failed"
230                                | "XSRF token invalid"
231                                | "XSRF Token Validation Failed"
232                                | "\"XSRF Token Validation Failed\"" => ApiError::TokenValidation,
233
234                                "Not authorized." => ApiError::Unauthorized,
235
236                                "Incorrect username or password. Please try again." => {
237                                    ApiError::InvalidCredentials
238                                }
239
240                                "You must pass the robot test before logging in." => {
241                                    ApiError::CaptchaFailed
242                                }
243
244                                "Account has been locked. Please request a password reset." => {
245                                    ApiError::AccontLocked
246                                }
247
248                                "Unable to login. Please use Social Network sign on." => {
249                                    ApiError::SocialNetworkLoginRequired
250                                }
251
252                                "Account issue. Please contact Support." => {
253                                    ApiError::AccountIssue
254                                }
255
256                                "Unable to login with provided credentials. Default login is required." => {
257                                    ApiError::DefaultLoginRequired
258                                }
259
260                                "Received credentials are unverified." => {
261                                    ApiError::UnverifiedCredentials
262                                }
263
264                                "Existing login session found. Please log out first." => {
265                                    ApiError::ExistingLoginSession
266                                }
267
268                                "The account is unable to log in. Please log in to the LuoBu app." => {
269                                    ApiError::LuoBuAppLoginRequired
270                                }
271
272                                "Too many attempts. Please wait a bit." => {
273                                    ApiError::Ratelimited
274                                }
275
276                                "The account is unable to login. Please log in with the VNG app." => {
277                                    ApiError::VNGAppLoginRequired
278                                }
279
280                                "PIN is locked." => ApiError::PinIsLocked,
281                                "Invalid birthdate change." => ApiError::InvalidBirthdate,
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}