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 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 = "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 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 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 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 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 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 message: error.message,
181 }],
182 }
183 } else {
184 ErrorsJson {
185 errors: vec![ErrorJson {
186 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 "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 "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 "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 "Badge is invalid or does not exist." => ApiError::InvalidBadge,
315
316
317 "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}