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 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)]
21pub struct ErrorJson {
22 message: String,
24}
25
26#[derive(Debug, Deserialize)]
27pub struct ErrorsJson {
28 errors: Vec<ErrorJson>,
29}
30
31#[derive(Debug, Deserialize)]
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 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 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 pub(crate) async fn validate_response(
141 &mut self,
142 result: Result<Response, reqwest::Error>,
143 ) -> Result<Response, Error> {
144 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 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 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 message: error.message,
187 }],
188 }
189 } else {
190 ErrorsJson {
191 errors: vec![ErrorJson {
192 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 "The asset id is invalid." => ApiError::InvalidAssetId,
209 "Invalid challenge ID." => ApiError::InvalidChallengeId,
210 "User not found." => ApiError::UserNotFound,
211 "The user ID is invalid." => ApiError::InvalidUserId,
212 "The gender provided is invalid." => ApiError::InvalidGender,
213 "The two step verification challenge code is invalid." => {
214 ApiError::InvalidTwoStepVerificationCode
215 }
216
217 "Invalid display name." => ApiError::InvalidDisplayName,
218
219 "Request must contain a birthdate" => {
220 ApiError::RequestMissingArgument("Birthdate".to_string())
221 }
222
223 "Ascending sort order is not supported for user's favorite games." => {
224 ApiError::UnsupportedSortOrder
225 }
226
227 "Token Validation Failed"
229 | "XSRF token invalid"
230 | "XSRF Token Validation Failed"
231 | "\"XSRF Token Validation Failed\"" => ApiError::TokenValidation,
232
233 "Not authorized." => ApiError::Unauthorized,
234
235 "Incorrect username or password. Please try again." => {
236 ApiError::InvalidCredentials
237 }
238
239 "You must pass the robot test before logging in." => {
240 ApiError::CaptchaFailed
241 }
242
243 "Account has been locked. Please request a password reset." => {
244 ApiError::AccontLocked
245 }
246
247 "Unable to login. Please use Social Network sign on." => {
248 ApiError::SocialNetworkLoginRequired
249 }
250
251 "Account issue. Please contact Support." => {
252 ApiError::AccountIssue
253 }
254
255 "Unable to login with provided credentials. Default login is required." => {
256 ApiError::DefaultLoginRequired
257 }
258
259 "Received credentials are unverified." => {
260 ApiError::UnverifiedCredentials
261 }
262
263 "Existing login session found. Please log out first." => {
264 ApiError::ExistingLoginSession
265 }
266
267 "The account is unable to log in. Please log in to the LuoBu app." => {
268 ApiError::LuoBuAppLoginRequired
269 }
270
271 "Too many attempts. Please wait a bit." => {
272 ApiError::Ratelimited
273 }
274
275 "The account is unable to login. Please log in with the VNG app." => {
276 ApiError::VNGAppLoginRequired
277 }
278
279 "PIN is locked." => ApiError::PinIsLocked,
280 "Invalid birthdate change." => ApiError::InvalidBirthdate,
281
282 "Challenge is required to authorize the request" => {
286 let challenge = challenge_from_headers(
287 challenge_id.clone(),
288 challenge_type.clone(),
289 challenge_metadata_b64.clone(),
290 );
291 ApiError::ChallengeRequired(challenge.unwrap())
292 }
293
294 "Challenge failed to authorize request" => {
295 ApiError::ChallengeFailed
296 }
297
298 "You do not have permission to view the owners of this asset." => {
299 ApiError::PermissionError
300 }
301
302 "Request Context BrowserTrackerID is missing or invalid." => {
303 ApiError::InvalidBrowserTrackerId
304 }
305
306 "an internal error occurred" => ApiError::Internal,
307
308 "You are already a member of this group." => ApiError::AlreadyInGroup,
310 "You have already requested to join this group." => ApiError::AlreadyInGroupRequests,
311
312 _ => {
313 ApiError::Unknown(code)
314 },
315 }).collect();
316
317 if errors.len() == 1 {
318 Err(Error::ApiError(errors.first().unwrap().clone()))
319 } else {
320 Err(Error::ApiError(ApiError::Multiple(errors)))
321 }
322 }
323 }
324 }
325
326 Err(error) => Err(Error::ReqwestError(error)),
327 }
328 }
329}