use base64::{Engine, prelude::BASE64_STANDARD};
use reqwest::{Response, header::HeaderValue};
use serde::{Deserialize, Serialize};
use crate::{
ApiError, Error,
api::auth,
challenge::{
CHALLENGE_ID_HEADER, CHALLENGE_METADATA_HEADER, CHALLENGE_TYPE_HEADER, Challenge,
ChallengeMetadata, ChallengeType, ChefChallengeMetadata,
},
client::Client,
ratelimit::{
RATELIMIT_LIMIT_HEADER, RATELIMIT_REMAINING_HEADER, RATELIMIT_RESET_HEADER, Ratelimit,
},
};
const TOKEN_HEADER: &str = "x-csrf-token";
#[derive(Debug, Deserialize, Serialize)]
pub struct ErrorJson {
message: String,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct ErrorsJson {
errors: Vec<ErrorJson>,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct DataErrorJson {
#[serde(rename = "error")]
message: String,
}
fn ratelimit_from_headers(
limit: Option<&HeaderValue>,
reset: Option<&HeaderValue>,
remaining: Option<&HeaderValue>,
) -> Option<Ratelimit> {
if let (Some(_limit), Some(reset), Some(remaining)) = (limit, reset, remaining) {
let remaining = remaining.to_str().unwrap().parse::<u32>().unwrap();
let reset_in_seconds = reset.to_str().unwrap().parse::<u32>().unwrap();
Some(Ratelimit {
remaining,
reset_in_seconds,
windows: Vec::new(),
})
} else {
None
}
}
fn challenge_from_headers(
id: Option<HeaderValue>,
kind: Option<HeaderValue>,
metadata_b64: Option<HeaderValue>,
) -> Option<Challenge> {
if let (Some(id), Some(kind), Some(metadata_b64)) = (id, kind, metadata_b64) {
let kind = ChallengeType::from(kind.to_str().unwrap());
match kind {
ChallengeType::Chef => {
let _metadata: ChefChallengeMetadata = serde_json::from_slice(
BASE64_STANDARD
.decode(metadata_b64.to_str().unwrap())
.unwrap()
.as_slice(),
)
.unwrap();
todo!("Unsupported challenge-type: \"chef\"");
}
ChallengeType::Captcha => {
todo!("Unsupported challenge-type: \"captcha\"")
}
_ => {
let metadata: ChallengeMetadata = serde_json::from_slice(
BASE64_STANDARD
.decode(metadata_b64.to_str().unwrap())
.unwrap()
.as_slice(),
)
.unwrap();
Some(Challenge {
id: id.to_str().unwrap().to_string(),
kind,
metadata,
})
}
}
} else {
None
}
}
impl Client {
fn set_token(&mut self, token: &str) {
self.requestor
.default_headers
.insert(TOKEN_HEADER, HeaderValue::from_str(token).unwrap());
}
pub async fn ensure_token(&mut self) -> Result<(), Error> {
let result = self
.requestor
.client
.post(format!("{}//", auth::URL))
.headers(self.requestor.default_headers.clone())
.send()
.await;
let result = self.validate_response(result).await;
if let Err(Error::ApiError(ApiError::TokenValidation)) = result {
return Ok(());
}
if result.is_err() {
return Err(result.err().unwrap());
}
Ok(())
}
pub(crate) async fn validate_response(
&mut self,
result: Result<Response, reqwest::Error>,
) -> Result<Response, Error> {
self.remove_challenge();
match result {
Ok(response) => {
let code = response.status().as_u16();
let token = response.headers().get(TOKEN_HEADER);
if let Some(token) = token {
self.set_token(String::from_utf8_lossy(token.as_bytes()).as_ref());
}
{
let limit = response.headers().get(RATELIMIT_LIMIT_HEADER);
let reset = response.headers().get(RATELIMIT_RESET_HEADER);
let remaining = response.headers().get(RATELIMIT_REMAINING_HEADER);
self.ratelimit = ratelimit_from_headers(limit, reset, remaining);
}
if code == 200 {
return Ok(response);
}
let challenge_id = response.headers().get(CHALLENGE_ID_HEADER).cloned();
let challenge_type = response.headers().get(CHALLENGE_TYPE_HEADER).cloned();
let challenge_metadata_b64 =
response.headers().get(CHALLENGE_METADATA_HEADER).cloned();
let bytes = response.bytes().await.unwrap().to_owned();
let errors = if let Ok(errors) = serde_json::from_slice::<ErrorsJson>(&bytes) {
errors
} else if let Ok(error) = serde_json::from_slice::<ErrorJson>(&bytes) {
ErrorsJson {
errors: vec![error],
}
} else if let Ok(error) = serde_json::from_slice::<DataErrorJson>(&bytes) {
ErrorsJson {
errors: vec![ErrorJson {
message: error.message,
}],
}
} else {
ErrorsJson {
errors: vec![ErrorJson {
message: String::from_utf8_lossy(&bytes).to_string(),
}],
}
};
match code {
401 => Err(Error::ApiError(ApiError::Unauthorized)),
429 => Err(Error::ApiError(ApiError::Ratelimited)),
500 => Err(Error::ApiError(ApiError::Internal)),
_ => {
let errors: Vec<ApiError> = errors
.errors
.iter()
.map(|x| match x.message.as_str() {
"The asset id is invalid." => ApiError::InvalidAssetId,
"Invalid challenge ID." => ApiError::InvalidChallengeId,
"User not found." => ApiError::InvalidUser,
"The user is invalid or does not exist." => ApiError::InvalidUser,
"The user ID is invalid." => ApiError::InvalidUserId,
"The gender provided is invalid." => ApiError::InvalidGender,
"The two step verification challenge code is invalid." => {
ApiError::InvalidTwoStepVerificationCode
}
"Invalid display name." => ApiError::InvalidDisplayName,
"Request must contain a birthdate" => {
ApiError::RequestMissingArgument("Birthdate".to_string())
}
"Ascending sort order is not supported for user's favorite games." => {
ApiError::UnsupportedSortOrder
}
"Token Validation Failed"
| "XSRF token invalid"
| "XSRF Token Validation Failed"
| "\"XSRF Token Validation Failed\"" => ApiError::TokenValidation,
"Not authorized." => ApiError::Unauthorized,
"Incorrect username or password. Please try again." => {
ApiError::InvalidCredentials
}
"You must pass the robot test before logging in." => {
ApiError::CaptchaFailed
}
"Account has been locked. Please request a password reset." => {
ApiError::AccontLocked
}
"Unable to login. Please use Social Network sign on." => {
ApiError::SocialNetworkLoginRequired
}
"Account issue. Please contact Support." => {
ApiError::AccountIssue
}
"Unable to login with provided credentials. Default login is required." => {
ApiError::DefaultLoginRequired
}
"Received credentials are unverified." => {
ApiError::UnverifiedCredentials
}
"Existing login session found. Please log out first." => {
ApiError::ExistingLoginSession
}
"The account is unable to log in. Please log in to the LuoBu app." => {
ApiError::LuoBuAppLoginRequired
}
"Too many attempts. Please wait a bit." => {
ApiError::Ratelimited
}
"The account is unable to login. Please log in with the VNG app." => {
ApiError::VNGAppLoginRequired
}
"PIN is locked." => ApiError::PinIsLocked,
"Invalid birthdate change." => ApiError::InvalidBirthdate,
"Challenge is required to authorize the request" => {
let challenge = challenge_from_headers(
challenge_id.clone(),
challenge_type.clone(),
challenge_metadata_b64.clone(),
);
ApiError::ChallengeRequired(challenge.unwrap())
}
"Challenge failed to authorize request" => {
ApiError::ChallengeFailed
}
"You do not have permission to view the owners of this asset." => {
ApiError::PermissionError
}
"Request Context BrowserTrackerID is missing or invalid." => {
ApiError::InvalidBrowserTrackerId
}
"Attempt to add non-friends to a conversation" => ApiError::ConversationUserAddFailed,
"Attempt to create conversations with non-friends" => ApiError::ConversationCreationFailed,
"{\"Error\":\"OneToOne conversations cannot be updated\"}" => ApiError::InvalidConversation,
"an internal error occurred" => ApiError::Internal,
"Badge is invalid or does not exist." => ApiError::InvalidBadge,
"You are already a member of this group." => ApiError::AlreadyInGroup,
"You have already requested to join this group." => ApiError::AlreadyInGroupRequests,
_ => {
ApiError::Unknown(code, Some(x.message.to_owned()))
},
}).collect();
if errors.len() == 1 {
Err(Error::ApiError(errors.first().unwrap().clone()))
} else {
Err(Error::ApiError(ApiError::Multiple(errors)))
}
}
}
}
Err(error) => Err(Error::ReqwestError(error)),
}
}
}