use std::borrow::BorrowMut;
use std::collections::HashMap;
use std::error::Error;
use std::fmt;
use chrono::{DateTime, Utc};
use log::{debug, error, info, warn};
use reqwest::header::HeaderMap;
use reqwest::{Client, ClientBuilder, RequestBuilder, Response};
use serde::{Deserialize, Serialize};
use url::Url;
use types::asset_info::{AssetInfo, GameToken, OwnershipToken};
use types::asset_manifest::AssetManifest;
use types::download_manifest::DownloadManifest;
use types::entitlement::Entitlement;
use types::library::Library;
use crate::api::types::epic_asset::EpicAsset;
use std::str::FromStr;
pub mod types;
pub mod utils;
#[allow(missing_docs)]
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct UserData {
access_token: Option<String>,
pub expires_in: Option<i64>,
pub expires_at: Option<DateTime<Utc>>,
pub token_type: Option<String>,
refresh_token: Option<String>,
pub refresh_expires: Option<i64>,
pub refresh_expires_at: Option<DateTime<Utc>>,
pub account_id: Option<String>,
pub client_id: Option<String>,
pub internal_client: Option<bool>,
pub client_service: Option<String>,
#[serde(rename = "displayName")]
pub display_name: Option<String>,
pub app: Option<String>,
pub in_app_id: Option<String>,
pub device_id: Option<String>,
#[serde(rename = "errorMessage")]
pub error_message: Option<String>,
#[serde(rename = "errorCode")]
pub error_code: Option<String>,
}
impl UserData {
pub fn new() -> Self {
UserData {
access_token: None,
expires_in: None,
expires_at: None,
token_type: None,
refresh_token: None,
refresh_expires: None,
refresh_expires_at: None,
account_id: None,
client_id: None,
internal_client: None,
client_service: None,
display_name: None,
app: None,
in_app_id: None,
device_id: None,
error_message: None,
error_code: None,
}
}
pub fn update(&mut self, new: UserData) {
if let Some(n) = new.access_token {
self.access_token = Some(n)
}
if let Some(n) = new.expires_in {
self.expires_in = Some(n)
}
if let Some(n) = new.expires_at {
self.expires_at = Some(n)
}
if let Some(n) = new.token_type {
self.token_type = Some(n)
}
if let Some(n) = new.refresh_token {
self.refresh_token = Some(n)
}
if let Some(n) = new.refresh_expires {
self.refresh_expires = Some(n)
}
if let Some(n) = new.refresh_expires_at {
self.refresh_expires_at = Some(n)
}
if let Some(n) = new.account_id {
self.account_id = Some(n)
}
if let Some(n) = new.client_id {
self.client_id = Some(n)
}
if let Some(n) = new.internal_client {
self.internal_client = Some(n)
}
if let Some(n) = new.client_service {
self.client_service = Some(n)
}
if let Some(n) = new.display_name {
self.display_name = Some(n)
}
if let Some(n) = new.app {
self.app = Some(n)
}
if let Some(n) = new.in_app_id {
self.in_app_id = Some(n)
}
if let Some(n) = new.device_id {
self.device_id = Some(n)
}
if let Some(n) = new.error_message {
self.error_message = Some(n)
}
if let Some(n) = new.error_code {
self.error_code = Some(n)
}
}
}
#[derive(Default, Debug, Clone)]
pub(crate) struct EpicAPI {
client: Client,
pub(crate) user_data: UserData,
}
#[derive(Debug)]
pub enum EpicAPIError {
InvalidCredentials,
APIError(String),
Unknown,
InvalidParams,
Server,
}
impl fmt::Display for EpicAPIError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match &*self {
EpicAPIError::InvalidCredentials => {
write!(f, "Invalid Credentials")
}
EpicAPIError::Unknown => {
write!(f, "Unknown Error")
}
EpicAPIError::Server => {
write!(f, "Server Error")
}
EpicAPIError::APIError(e) => {
write!(f, "API Error: {}", e)
}
EpicAPIError::InvalidParams => {
write!(f, "Invalid Input Parameters")
}
}
}
}
impl Error for EpicAPIError {
fn description(&self) -> &str {
match *self {
EpicAPIError::InvalidCredentials => "Invalid Credentials",
EpicAPIError::Unknown => "Unknown Error",
EpicAPIError::Server => "Server Error",
EpicAPIError::APIError(_) => "API Error",
EpicAPIError::InvalidParams => "Invalid Input Parameters",
}
}
}
impl EpicAPI {
pub fn new() -> Self {
let client = EpicAPI::build_client().build().unwrap();
EpicAPI {
client,
user_data: Default::default(),
}
}
fn build_client() -> ClientBuilder {
let mut headers = HeaderMap::new();
headers.insert(
"User-Agent",
"UELauncher/12.0.5-15338009+++Portal+Release-Live Windows/6.1.7601.1.0.64bit"
.parse()
.unwrap(),
);
let client = reqwest::Client::builder()
.default_headers(headers)
.cookie_store(true);
client
}
pub async fn start_session(
&mut self,
exchange_token: Option<String>,
) -> Result<bool, EpicAPIError> {
let params = match exchange_token {
None => [
("grant_type".to_string(), "refresh_token".to_string()),
(
"refresh_token".to_string(),
self.user_data.refresh_token.clone().unwrap(),
),
("token_type".to_string(), "eg1".to_string()),
],
Some(exchange) => [
("grant_type".to_string(), "exchange_code".to_string()),
("exchange_code".to_string(), exchange),
("token_type".to_string(), "eg1".to_string()),
],
};
match self
.client
.post("https://account-public-service-prod03.ol.epicgames.com/account/api/oauth/token")
.form(¶ms)
.basic_auth(
"34a02cf8f4414e29b15921876da36f9a",
Some("daafbccc737745039dffe53d94fc76cf"),
)
.send()
.await
{
Ok(response) => {
return self.handle_login_response(response).await;
}
Err(e) => {
error!("{:?}", e);
Err(EpicAPIError::Unknown)
}
}
}
async fn handle_login_response(&mut self, response: Response) -> Result<bool, EpicAPIError> {
if response.status() == reqwest::StatusCode::INTERNAL_SERVER_ERROR {
error!("Server Error");
return Err(EpicAPIError::Server);
}
let new: UserData = match response.json().await {
Ok(data) => data,
Err(e) => {
error!("{:?}", e);
return Err(EpicAPIError::Unknown);
}
};
self.user_data.update(new);
match &self.user_data.error_message {
None => {}
Some(m) => {
error!("{}", m);
return Err(EpicAPIError::APIError(m.to_string()));
}
}
Ok(true)
}
fn authorized_get_client(&self, url: Url) -> RequestBuilder {
let client = EpicAPI::build_client().build().unwrap();
self.set_authorization_header(client.clone().get(url))
}
fn authorized_post_client(&self, url: Url) -> RequestBuilder {
let client = EpicAPI::build_client().build().unwrap();
self.set_authorization_header(client.clone().post(url))
}
fn set_authorization_header(&self, rb: RequestBuilder) -> RequestBuilder {
rb.header(
"Authorization",
format!(
"{} {}",
self.user_data
.token_type
.as_ref()
.unwrap_or(&"bearer".to_string()),
self.user_data
.access_token
.as_ref()
.unwrap_or(&"".to_string())
),
)
}
pub async fn resume_session(&mut self) -> Result<bool, EpicAPIError> {
match self.authorized_get_client(Url::parse("https://account-public-service-prod03.ol.epicgames.com/account/api/oauth/verify").unwrap()).send().await {
Ok(response) => {
return self.handle_login_response(response).await;
}
Err(e) => {
error!("{:?}", e);
Err(EpicAPIError::Unknown)
}
}
}
pub async fn invalidate_sesion(&mut self) -> bool {
match &self.user_data.access_token {
None => {}
Some(access_token) => {
let url = format!("https://account-public-service-prod03.ol.epicgames.com/account/api/oauth/sessions/kill/{}", access_token);
let client = EpicAPI::build_client().build().unwrap();
match client.delete(Url::from_str(&url).unwrap()).send().await {
Ok(_) => {
info!("Session invalidated");
return true;
}
Err(e) => {
warn!("Unable to invalidate session: {}", e)
}
}
}
};
return false;
}
pub async fn assets(
&mut self,
platform: Option<String>,
label: Option<String>,
) -> Result<Vec<EpicAsset>, EpicAPIError> {
let plat = platform.unwrap_or("Windows".to_string());
let lab = label.unwrap_or("Live".to_string());
let url = format!("https://launcher-public-service-prod06.ol.epicgames.com/launcher/api/public/assets/{}?label={}", plat, lab);
match self
.authorized_get_client(Url::parse(&url).unwrap())
.send()
.await
{
Ok(response) => {
if response.status() == reqwest::StatusCode::OK {
match response.json().await {
Ok(assets) => Ok(assets),
Err(e) => {
error!("{:?}", e);
Err(EpicAPIError::Unknown)
}
}
} else {
warn!(
"{} result: {}",
response.status(),
response.text().await.unwrap()
);
Err(EpicAPIError::Unknown)
}
}
Err(e) => {
error!("{:?}", e);
Err(EpicAPIError::Unknown)
}
}
}
pub async fn asset_manifest(
&self,
platform: Option<String>,
label: Option<String>,
namespace: Option<String>,
item_id: Option<String>,
app: Option<String>,
) -> Result<AssetManifest, EpicAPIError> {
if let None = namespace {
return Err(EpicAPIError::InvalidParams);
};
if let None = item_id {
return Err(EpicAPIError::InvalidParams);
};
if let None = app {
return Err(EpicAPIError::InvalidParams);
};
let url = format!("https://launcher-public-service-prod06.ol.epicgames.com/launcher/api/public/assets/v2/platform/{}/namespace/{}/catalogItem/{}/app/{}/label/{}",
platform.clone().unwrap_or("Windows".to_string()), namespace.clone().unwrap(), item_id.clone().unwrap(), app.clone().unwrap(), label.clone().unwrap_or("Live".to_string()));
match self
.authorized_get_client(Url::parse(&url).unwrap())
.send()
.await
{
Ok(response) => {
if response.status() == reqwest::StatusCode::OK {
match response.json::<AssetManifest>().await {
Ok(mut manifest) => {
manifest.platform = platform;
manifest.label = label;
manifest.namespace = namespace;
manifest.item_id = item_id;
manifest.app = app;
Ok(manifest)
}
Err(e) => {
error!("{:?}", e);
Err(EpicAPIError::Unknown)
}
}
} else {
warn!(
"{} result: {}",
response.status(),
response.text().await.unwrap()
);
Err(EpicAPIError::Unknown)
}
}
Err(e) => {
error!("{:?}", e);
Err(EpicAPIError::Unknown)
}
}
}
pub async fn asset_download_manifest(
&self,
asset_manifest: AssetManifest,
) -> Result<DownloadManifest, EpicAPIError> {
let base_urls = asset_manifest.url_csv();
for elem in asset_manifest.elements {
for manifest in elem.manifests {
let mut queries: Vec<String> = Vec::new();
debug!("{:?}", manifest);
for query in manifest.query_params {
queries.push(format!("{}={}", query.name, query.value));
}
let url = format!("{}?{}", manifest.uri.to_string(), queries.join("&"));
let client = EpicAPI::build_client().build().unwrap();
match client.get(Url::from_str(&url).unwrap()).send().await {
Ok(response) => {
if response.status() == reqwest::StatusCode::OK {
match response.bytes().await {
Ok(data) => match DownloadManifest::parse(data.to_vec()) {
None => {
error!("Unable to parse the Download Manifest");
return Err(EpicAPIError::Unknown);
}
Some(mut man) => {
let mut url = manifest.uri.clone();
url.set_path(&match url.path_segments() {
None => "".to_string(),
Some(segments) => {
let mut vec: Vec<&str> = segments.collect();
vec.remove(vec.len() - 1);
vec.join("/")
}
});
url.set_query(None);
url.set_fragment(None);
man.set_custom_field(
"BaseUrl".to_string(),
base_urls.clone(),
);
if let Some(id) = asset_manifest.item_id {
man.set_custom_field(
"CatalogItemId".to_string(),
id.clone(),
);
}
if let Some(label) = asset_manifest.label {
man.set_custom_field(
"BuildLabel".to_string(),
label.clone(),
);
}
if let Some(ns) = asset_manifest.namespace {
man.set_custom_field(
"CatalogNamespace".to_string(),
ns.clone(),
);
}
if let Some(app) = asset_manifest.app {
man.set_custom_field(
"CatalogAssetName".to_string(),
app.clone(),
);
}
println!("{:#?}", man.custom_fields);
man.set_custom_field(
"SourceURL".to_string(),
url.to_string(),
);
return Ok(man);
}
},
Err(e) => {
error!("{:?}", e);
return Err(EpicAPIError::Unknown);
}
}
} else {
warn!(
"{} result: {}",
response.status(),
response.text().await.unwrap()
);
return Err(EpicAPIError::Unknown);
}
}
Err(e) => {
error!("{:?}", e);
return Err(EpicAPIError::Unknown);
}
}
}
}
Err(EpicAPIError::Unknown)
}
pub async fn asset_info(
&self,
asset: EpicAsset,
) -> Result<HashMap<String, AssetInfo>, EpicAPIError> {
let url = format!("https://catalog-public-service-prod06.ol.epicgames.com/catalog/api/shared/namespace/{}/bulk/items?id={}&includeDLCDetails=true&includeMainGameDetails=true&country=us&locale=lc",
asset.namespace, asset.catalog_item_id);
match self
.authorized_get_client(Url::parse(&url).unwrap())
.send()
.await
{
Ok(response) => {
if response.status() == reqwest::StatusCode::OK {
match response.json().await {
Ok(info) => Ok(info),
Err(e) => {
error!("{:?}", e);
Err(EpicAPIError::Unknown)
}
}
} else {
warn!(
"{} result: {}",
response.status(),
response.text().await.unwrap()
);
Err(EpicAPIError::Unknown)
}
}
Err(e) => {
error!("{:?}", e);
Err(EpicAPIError::Unknown)
}
}
}
pub async fn game_token(&self) -> Result<GameToken, EpicAPIError> {
let url = format!(
"https://account-public-service-prod03.ol.epicgames.com/account/api/oauth/exchange"
);
match self
.authorized_get_client(Url::parse(&url).unwrap())
.send()
.await
{
Ok(response) => {
if response.status() == reqwest::StatusCode::OK {
match response.json().await {
Ok(token) => Ok(token),
Err(e) => {
error!("{:?}", e);
Err(EpicAPIError::Unknown)
}
}
} else {
warn!(
"{} result: {}",
response.status(),
response.text().await.unwrap()
);
Err(EpicAPIError::Unknown)
}
}
Err(e) => {
error!("{:?}", e);
Err(EpicAPIError::Unknown)
}
}
}
pub async fn ownership_token(&self, asset: EpicAsset) -> Result<OwnershipToken, EpicAPIError> {
let url = match &self.user_data.account_id {
None => {
return Err(EpicAPIError::InvalidCredentials);
}
Some(id) => {
format!("https://ecommerceintegration-public-service-ecomprod02.ol.epicgames.com/ecommerceintegration/api/public/platforms/EPIC/identities/{}/ownershipToken",
id)
}
};
match self
.authorized_post_client(Url::parse(&url).unwrap())
.form(&[(
"nsCatalogItemId".to_string(),
format!("{}:{}", asset.namespace, asset.catalog_item_id),
)])
.send()
.await
{
Ok(response) => {
if response.status() == reqwest::StatusCode::OK {
match response.json().await {
Ok(token) => Ok(token),
Err(e) => {
error!("{:?}", e);
Err(EpicAPIError::Unknown)
}
}
} else {
warn!(
"{} result: {}",
response.status(),
response.text().await.unwrap()
);
Err(EpicAPIError::Unknown)
}
}
Err(e) => {
error!("{:?}", e);
Err(EpicAPIError::Unknown)
}
}
}
pub async fn user_entitlements(&self) -> Result<Vec<Entitlement>, EpicAPIError> {
let url = match &self.user_data.account_id {
None => {
return Err(EpicAPIError::InvalidCredentials);
}
Some(id) => {
format!("https://entitlement-public-service-prod08.ol.epicgames.com/entitlement/api/account/{}/entitlements?start=0&count=5000",
id)
}
};
match self
.authorized_get_client(Url::parse(&url).unwrap())
.send()
.await
{
Ok(response) => {
if response.status() == reqwest::StatusCode::OK {
match response.json().await {
Ok(ent) => Ok(ent),
Err(e) => {
error!("{:?}", e);
Err(EpicAPIError::Unknown)
}
}
} else {
warn!(
"{} result: {}",
response.status(),
response.text().await.unwrap()
);
Err(EpicAPIError::Unknown)
}
}
Err(e) => {
error!("{:?}", e);
Err(EpicAPIError::Unknown)
}
}
}
pub async fn library_items(&mut self, include_metadata: bool) -> Result<Library, EpicAPIError> {
let mut library = Library {
records: vec![],
response_metadata: Default::default(),
};
let mut cursor: Option<String> = None;
loop {
let url = match &cursor {
None => {
format!("https://library-service.live.use1a.on.epicgames.com/library/api/public/items?includeMetadata={}", include_metadata)
}
Some(c) => {
format!("https://library-service.live.use1a.on.epicgames.com/library/api/public/items?includeMetadata={}&cursor={}", include_metadata, c)
}
};
match self
.authorized_get_client(Url::parse(&url).unwrap())
.send()
.await
{
Ok(response) => {
if response.status() == reqwest::StatusCode::OK {
match response.json::<Library>().await {
Ok(mut records) => {
library.records.append(records.records.borrow_mut());
match records.response_metadata {
None => {
break;
}
Some(meta) => match meta.next_cursor {
None => {
break;
}
Some(curs) => {
cursor = Some(curs);
}
},
}
}
Err(e) => {
error!("{:?}", e);
}
}
} else {
warn!(
"{} result: {}",
response.status(),
response.text().await.unwrap()
);
}
}
Err(e) => {
error!("{:?}", e);
}
};
if cursor.is_none() {
break;
}
}
Ok(library)
}
}