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