1use base64::{Engine, prelude::BASE64_STANDARD};
2use reqwest::{Response, header::HeaderValue};
3use serde::{Deserialize, Serialize};
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, 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 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::InvalidUser,
211 "The user is invalid or does not exist." => ApiError::InvalidUser,
212 "The user ID is invalid." => ApiError::InvalidUserId,
213 "The gender provided is invalid." => ApiError::InvalidGender,
214 "The two step verification challenge code is invalid." => {
215 ApiError::InvalidTwoStepVerificationCode
216 }
217
218 "Invalid display name." => ApiError::InvalidDisplayName,
219
220 "Request must contain a birthdate" => {
221 ApiError::RequestMissingArgument("Birthdate".to_string())
222 }
223
224 "Ascending sort order is not supported for user's favorite games." => {
225 ApiError::UnsupportedSortOrder
226 }
227
228 "Token Validation Failed"
230 | "XSRF token invalid"
231 | "XSRF Token Validation Failed"
232 | "\"XSRF Token Validation Failed\"" => ApiError::TokenValidation,
233
234 "Not authorized." => ApiError::Unauthorized,
235
236 "Incorrect username or password. Please try again." => {
237 ApiError::InvalidCredentials
238 }
239
240 "You must pass the robot test before logging in." => {
241 ApiError::CaptchaFailed
242 }
243
244 "Account has been locked. Please request a password reset." => {
245 ApiError::AccontLocked
246 }
247
248 "Unable to login. Please use Social Network sign on." => {
249 ApiError::SocialNetworkLoginRequired
250 }
251
252 "Account issue. Please contact Support." => {
253 ApiError::AccountIssue
254 }
255
256 "Unable to login with provided credentials. Default login is required." => {
257 ApiError::DefaultLoginRequired
258 }
259
260 "Received credentials are unverified." => {
261 ApiError::UnverifiedCredentials
262 }
263
264 "Existing login session found. Please log out first." => {
265 ApiError::ExistingLoginSession
266 }
267
268 "The account is unable to log in. Please log in to the LuoBu app." => {
269 ApiError::LuoBuAppLoginRequired
270 }
271
272 "Too many attempts. Please wait a bit." => {
273 ApiError::Ratelimited
274 }
275
276 "The account is unable to login. Please log in with the VNG app." => {
277 ApiError::VNGAppLoginRequired
278 }
279
280 "PIN is locked." => ApiError::PinIsLocked,
281 "Invalid birthdate change." => ApiError::InvalidBirthdate,
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}