#![warn(missing_docs)]
use crate::auth::LoginError;
use crate::hwid::generate_hwid;
use crate::profile::ProfileError;
use crate::ragfair::RagfairError;
use crate::trading::TradingError;
use err_derive::Error;
use flate2::read::ZlibDecoder;
use hyper::body::Buf;
use hyper::client::connect::dns::GaiResolver;
use hyper::client::{Client, HttpConnector};
use hyper::Body;
use hyper::Request;
use hyper::{Method, StatusCode};
use hyper_tls::HttpsConnector;
use log::debug;
use serde::de::DeserializeOwned;
use serde::Deserialize;
use std::io::Read;
const GAME_VERSION: &str = "0.12.2.5633";
const LAUNCHER_VERSION: &str = "0.9.2.970";
const UNITY_VERSION: &str = "2018.4.13f1";
const LAUNCHER_ENDPOINT: &str = "https://launcher.escapefromtarkov.com";
const PROD_ENDPOINT: &str = "https://prod.escapefromtarkov.com";
const TRADING_ENDPOINT: &str = "https://trading.escapefromtarkov.com";
const RAGFAIR_ENDPOINT: &str = "https://ragfair.escapefromtarkov.com";
mod bad_json;
pub mod auth;
pub mod constant;
pub mod friend;
pub mod hwid;
pub mod inventory;
pub mod market_filter;
pub mod profile;
pub mod ragfair;
pub mod trading;
#[derive(Debug, Error)]
pub enum Error {
#[error(display = "io error: {}", _0)]
Io(#[error(source)] std::io::Error),
#[error(display = "http error: {}", _0)]
Http(#[error(source)] http::Error),
#[error(display = "hyper error: {}", _0)]
Hyper(#[error(source)] hyper::Error),
#[error(display = "json error: {}", _0)]
Json(#[error(source)] serde_json::error::Error),
#[error(display = "non-success response from api: {}", _0)]
Status(StatusCode),
#[error(display = "invalid or missing login parameters")]
InvalidParameters,
#[error(display = "unidentified login error with error code: {}", _0)]
UnknownAPIError(u64),
#[error(display = "not authorized or game profile not selected")]
NotAuthorized,
#[error(display = "api is down for maintenance")]
Maintenance,
#[error(display = "backend error")]
BackendError,
#[error(display = "login api error: {}", _0)]
LoginError(#[error(source)] LoginError),
#[error(display = "profile api error: {}", _0)]
ProfileError(#[error(source)] ProfileError),
#[error(display = "trading api error: {}", _0)]
TradingError(#[error(source)] TradingError),
#[error(display = "trading api error: {}", _0)]
RagfairError(#[error(source)] RagfairError),
}
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, Deserialize, Clone, PartialEq)]
struct ErrorResponse {
#[serde(rename = "err")]
code: u64,
#[serde(rename = "errmsg")]
message: Option<String>,
}
pub struct Tarkov {
client: Client<HttpsConnector<HttpConnector<GaiResolver>>, Body>,
pub hwid: String,
pub session: String,
}
impl Tarkov {
pub async fn login(email: &str, password: &str, hwid: &str) -> Result<Self> {
if email.is_empty() || password.is_empty() || hwid.is_empty() {
return Err(Error::InvalidParameters);
}
let https = HttpsConnector::new();
let client = Client::builder().build::<_, Body>(https);
let user = auth::login(&client, email, password, None, &hwid).await?;
let session = auth::exchange_access_token(&client, &user.access_token, &hwid).await?;
Ok(Tarkov {
client,
hwid: hwid.to_string(),
session: session.session,
})
}
pub async fn login_with_captcha(
email: &str,
password: &str,
captcha: &str,
hwid: &str,
) -> Result<Self> {
if email.is_empty() || password.is_empty() || captcha.is_empty() || hwid.is_empty() {
return Err(Error::InvalidParameters);
}
let https = HttpsConnector::new();
let client = Client::builder().build::<_, Body>(https);
let user = auth::login(&client, email, password, Some(captcha), &hwid).await?;
let session = auth::exchange_access_token(&client, &user.access_token, &hwid).await?;
Ok(Tarkov {
client,
hwid: hwid.to_string(),
session: session.session,
})
}
pub async fn login_with_2fa(
email: &str,
password: &str,
code: &str,
hwid: &str,
) -> Result<Self> {
if email.is_empty() || password.is_empty() || code.is_empty() || hwid.is_empty() {
return Err(Error::InvalidParameters);
}
let https = HttpsConnector::new();
let client = Client::builder().build::<_, Body>(https);
let _ = auth::activate_hardware(&client, email, code, &hwid).await?;
let user = auth::login(&client, email, password, None, &hwid).await?;
let session = auth::exchange_access_token(&client, &user.access_token, &hwid).await?;
Ok(Tarkov {
client,
hwid: hwid.to_string(),
session: session.session,
})
}
pub async fn from_access_token(access_token: &str, hwid: &str) -> Result<Self> {
if access_token.is_empty() || hwid.is_empty() {
return Err(Error::InvalidParameters);
}
let https = HttpsConnector::new();
let client = Client::builder().build::<_, Body>(https);
let session = auth::exchange_access_token(&client, &access_token, &hwid).await?;
Ok(Tarkov {
client,
hwid: hwid.to_string(),
session: session.session,
})
}
pub fn from_session(session: &str) -> Self {
let https = HttpsConnector::new();
let client = Client::builder().build::<_, Body>(https);
Tarkov {
client,
hwid: generate_hwid(),
session: session.to_string(),
}
}
async fn post_json<S: serde::Serialize + ?Sized + std::fmt::Debug, T: DeserializeOwned>(
&self,
url: &str,
body: &S,
) -> Result<T> {
debug!("Sending request to {} ({:?})", url, body);
let body = match serde_json::to_string(&body) {
Ok(body) => Ok(Body::from(if body == "null" {
"{}".to_string()
} else {
body
})),
Err(e) => Err(e),
}?;
let req = Request::builder()
.uri(url)
.method(Method::POST)
.header("Content-Type", "application/json")
.header(
"User-Agent",
format!(
"UnityPlayer/{} (UnityWebRequest/1.0, libcurl/7.52.0-DEV)",
UNITY_VERSION
),
)
.header("App-Version", format!("EFT Client {}", GAME_VERSION))
.header("X-Unity-Version", UNITY_VERSION)
.header("Cookie", format!("PHPSESSID={}", self.session))
.body(body)?;
let res = self.client.request(req).await?;
match res.status() {
StatusCode::OK => {
let body = hyper::body::to_bytes(res.into_body()).await?;
let mut decode = ZlibDecoder::new(body.bytes());
let mut body = String::new();
decode.read_to_string(&mut body)?;
debug!("Response: {}", body);
Ok(serde_json::from_slice::<T>(body.as_bytes())?)
}
_ => Err(Error::Status(res.status())),
}
}
}
pub(crate) fn handle_error<T: DeserializeOwned>(error: ErrorResponse, ret: Option<T>) -> Result<T> {
handle_error2(error)?;
Ok(ret.expect("API returned no errors but `data` is unavailable."))
}
pub(crate) fn handle_error2(error: ErrorResponse) -> Result<()> {
match error.code {
0 => Ok(()),
201 => Err(Error::NotAuthorized)?,
205 => Err(ProfileError::InvalidUserID)?,
206 => Err(LoginError::BadLogin)?,
207 => Err(Error::InvalidParameters)?,
209 => Err(LoginError::TwoFactorRequired)?,
211 => Err(LoginError::BadTwoFactorCode)?,
214 => Err(LoginError::CaptchaRequired)?,
228 => Err(RagfairError::InvalidBarterItems)?,
230 => Err(LoginError::RateLimited)?,
263 => Err(Error::Maintenance)?,
1000 => Err(Error::BackendError)?,
1501 => Err(RagfairError::MaxOfferCount)?,
1502 => Err(RagfairError::InsufficientTaxFunds)?,
1507 => Err(RagfairError::OfferNotFound)?,
1510 => Err(TradingError::BadLoyaltyLevel)?,
1512 => Err(RagfairError::OfferNotAvailableYet)?,
1514 => Err(TradingError::TransactionError)?,
_ => Err(Error::UnknownAPIError(error.code)),
}
}