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()).to_string());
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 chef challenge");
115                            }
116
117                            _ => {
118                                let metadata: ChallengeMetadata = serde_json::from_slice(
119                                    BASE64_STANDARD
120                                        .decode(metadata_b64.to_str().unwrap())
121                                        .unwrap()
122                                        .as_slice(),
123                                )
124                                .unwrap();
125
126                                Some(Challenge {
127                                    id: id.to_str().unwrap().to_string(),
128                                    kind,
129                                    metadata,
130                                })
131                            }
132                        }
133                    } else {
134                        None
135                    }
136                };
137
138                let bytes = response.bytes().await.unwrap().to_owned();
139                let errors = if let Ok(errors) = serde_json::from_slice::<ErrorsJson>(&bytes) {
140                    errors
141                } else if let Ok(error) = serde_json::from_slice::<ErrorJson>(&bytes) {
142                    ErrorsJson {
143                        errors: vec![error],
144                    }
145                } else if let Ok(error) = serde_json::from_slice::<DataErrorJson>(&bytes) {
146                    ErrorsJson {
147                        errors: vec![ErrorJson {
148                            code: 0,
149                            message: error.message,
150                        }],
151                    }
152                } else {
153                    ErrorsJson {
154                        errors: vec![ErrorJson {
155                            code: 0,
156                            message: String::from_utf8_lossy(&bytes).to_string(),
157                        }],
158                    }
159                };
160
161                match code {
162                    400 => {
163                        let errors: Vec<ApiError> = errors
164                            .errors
165                            .iter()
166                            .map(|x| match x.message.as_str() {
167                                "Invalid challenge ID." => ApiError::InvalidChallengeId,
168                                "User not found." => ApiError::UserNotFound,
169                                "The user ID is invalid." => ApiError::InvalidUserId,
170                                "The gender provided is invalid." => ApiError::InvalidGender,
171                                "The two step verification challenge code is invalid." => {
172                                    ApiError::InvalidTwoStepVerificationCode
173                                }
174
175                                "Invalid display name." => ApiError::InvalidDisplayName,
176
177                                "Request must contain a birthdate" => {
178                                    ApiError::RequestMissingArgument("Birthdate".to_string())
179                                }
180
181                                _ => ApiError::Unknown(code),
182                            })
183                            .collect();
184
185                        if errors.len() == 1 {
186                            Err(Error::ApiError(errors.first().unwrap().clone()))
187                        } else {
188                            Err(Error::ApiError(ApiError::Multiple(errors)))
189                        }
190                    }
191
192                    401 => Err(Error::ApiError(ApiError::Unauthorized)),
193                    403 => {
194                        let errors: Vec<ApiError> = errors
195                            .errors
196                            .iter()
197                            .map(|x| match x.message.as_str() {
198                                "Token Validation Failed"
199                                | "XSRF token invalid"
200                                | "XSRF Token Validation Failed"
201                                | "\"XSRF Token Validation Failed\"" => ApiError::TokenValidation,
202
203                                "PIN is locked." => ApiError::PinIsLocked,
204                                "Invalid birthdate change." => ApiError::InvalidBirthdate,
205
206                                "Challenge is required to authorize the request" => {
207                                    ApiError::ChallengeRequired(challenge.clone().unwrap())
208                                }
209
210                                "Challenge failed to authorize request" => {
211                                    ApiError::ChallengeFailed
212                                }
213
214                                "You do not have permission to view the owners of this asset." => {
215                                    ApiError::PermissionError
216                                }
217
218                                "an internal error occurred" => ApiError::Internal,
219
220                                // TODO: add missing challenge duplicate code
221                                _ => ApiError::Unknown(code),
222                            })
223                            .collect();
224
225                        if errors.len() == 1 {
226                            Err(Error::ApiError(errors.first().unwrap().clone()))
227                        } else {
228                            Err(Error::ApiError(ApiError::Multiple(errors)))
229                        }
230                    }
231
232                    429 => Err(Error::ApiError(ApiError::Ratelimited)),
233                    500 => Err(Error::ApiError(ApiError::Internal)),
234
235                    _ => Err(Error::ApiError(ApiError::Unknown(code))),
236                }
237            }
238
239            Err(error) => Err(Error::ReqwestError(error)),
240        }
241    }
242}