roblox_api/
validation.rs

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