mod alliance;
pub use alliance::*;
mod assets;
pub use assets::*;
mod auth;
pub use auth::*;
mod character;
pub use character::*;
mod corporation;
pub use corporation::*;
mod killmails;
pub use killmails::*;
mod location;
pub use location::*;
mod market;
pub use market::*;
mod scope;
pub use scope::Scope;
mod error;
pub use error::Error;
mod universe;
pub use universe::*;
pub type EsiResult<T> = Result<T, Error>;
use oauth2::{
basic::{
BasicClient,
BasicErrorResponse,
BasicRevocationErrorResponse,
BasicTokenIntrospectionResponse,
BasicTokenResponse,
},
AuthUrl,
ClientId,
ClientSecret,
EndpointNotSet,
EndpointSet,
RedirectUrl,
StandardRevocableToken,
TokenUrl,
};
use reqwest::Client;
use serde::{de::DeserializeOwned, Serialize};
use std::{collections::HashMap, env, time::Duration};
const ESI_DATASOURCE: &str = "tranquility";
const AUTHORIZE_URL: &str = "https://login.eveonline.com/v2/oauth/authorize";
const TOKEN_URL: &str = "https://login.eveonline.com/v2/oauth/token";
const SSO_META_DATA_URL: &str =
"https://login.eveonline.com/.well-known/oauth-authorization-server";
const LOGIN_URLS: [&str; 2] = ["login.eveonline.com", "https://login.eveonline.com"];
const LOGIN_MEMBERS: [&str; 1] = ["EVE Online"];
pub type ClientType = oauth2::Client<
BasicErrorResponse,
BasicTokenResponse,
BasicTokenIntrospectionResponse,
StandardRevocableToken,
BasicRevocationErrorResponse,
EndpointSet,
EndpointNotSet,
EndpointNotSet,
EndpointNotSet,
EndpointSet,
>;
pub struct Esi {
client: Client,
token_client: ClientType,
base_url: String,
}
impl Esi {
pub fn new(
owner_id: impl Into<String>,
client_id: impl Into<String>,
client_secret: impl Into<String>,
callback_url: impl Into<String>,
timeout: u64,
) -> EsiResult<Self> {
let version = env!("CARGO_PKG_VERSION");
let user_agent = format!(
"{}/{}; {} ({} {})",
"eversal",
version,
owner_id.into(),
"ben@bensherriff.com",
"https://gitea.bensherriff.com/Eversal/eversal-esi"
);
let client = Client::builder()
.user_agent(&user_agent)
.timeout(Duration::from_secs(timeout))
.build()?;
let token_client = BasicClient::new(ClientId::new(client_id.into()))
.set_client_secret(ClientSecret::new(client_secret.into()))
.set_auth_uri(AuthUrl::new(AUTHORIZE_URL.to_string())?)
.set_token_uri(TokenUrl::new(TOKEN_URL.to_string())?)
.set_redirect_uri(RedirectUrl::new(callback_url.into())?);
Ok(Self {
client,
token_client,
base_url: "https://esi.evetech.net/latest/".to_string(),
})
}
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct Headers {
pub etag: String,
pub expires: String,
pub last_modified: String,
pub error_limit_remain: u16,
pub error_limit_reset: u16,
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct Paged<T> {
pub data: T,
pub page: i32,
pub total_pages: i32,
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct Response<T> {
pub data: T,
pub headers: Headers,
}
async fn process_single<T: DeserializeOwned>(
response: reqwest::Response,
) -> EsiResult<Response<T>> {
let headers = headers(&response);
let data: T = response.json().await?;
Ok(Response { data, headers })
}
async fn process_paged<T: DeserializeOwned>(
page: i32,
response: reqwest::Response,
) -> EsiResult<Response<Paged<T>>> {
let total_pages = total_pages(&response);
let headers = headers(&response);
let data: T = response.json().await?;
Ok(Response {
data: Paged {
data,
page,
total_pages,
},
headers,
})
}
async fn get_public_base(
path: &str,
esi: &Esi,
params: Option<HashMap<&str, String>>,
etag: Option<&str>,
) -> EsiResult<reqwest::Response> {
let url = build_url(&esi.base_url, path, params);
log::trace!("Requesting: {}", url);
let mut request = esi.client.get(url);
if let Some(etag) = etag {
request = request.header(reqwest::header::IF_NONE_MATCH, etag);
}
let response: reqwest::Response = request.send().await?;
response.error_for_status_ref()?;
if response.status().as_u16() == 304 {
return Err(Error::new(304, "Not Modified".to_string()));
}
Ok(response)
}
pub async fn get_public<T: DeserializeOwned>(
path: &str,
esi: &Esi,
params: Option<HashMap<&str, String>>,
etag: Option<&str>,
) -> EsiResult<Response<T>> {
let response = get_public_base(path, esi, params, etag).await?;
process_single(response).await
}
pub async fn get_public_paged<T: DeserializeOwned>(
path: &str,
esi: &Esi,
params: Option<HashMap<&str, String>>,
etag: Option<&str>,
) -> EsiResult<Response<Paged<T>>> {
let page = page(¶ms);
let response = get_public_base(path, esi, params, etag).await?;
process_paged(page, response).await
}
async fn get_authenticated_base(
path: &str,
esi: &Esi,
access_token: &str,
params: Option<HashMap<&str, String>>,
etag: Option<&str>,
) -> EsiResult<reqwest::Response> {
let url = build_url(&esi.base_url, path, params);
log::trace!("Requesting: {}", url);
let mut request = esi.client.get(url).header(
reqwest::header::AUTHORIZATION,
format!("Bearer {}", access_token),
);
if let Some(etag) = etag {
request = request.header(reqwest::header::IF_NONE_MATCH, etag);
}
let response = request.send().await?;
response.error_for_status_ref()?;
if response.status().as_u16() == 304 {
return Err(Error::new(304, "Not Modified".to_string()));
}
Ok(response)
}
pub async fn get_authenticated<T: DeserializeOwned>(
access_token: &str,
path: &str,
esi: &Esi,
params: Option<HashMap<&str, String>>,
etag: Option<&str>,
) -> EsiResult<Response<T>> {
let response = get_authenticated_base(path, esi, access_token, params, etag).await?;
process_single(response).await
}
pub async fn get_authenticated_paged<T: DeserializeOwned>(
access_token: &str,
path: &str,
esi: &Esi,
params: Option<HashMap<&str, String>>,
etag: Option<&str>,
) -> EsiResult<Response<Paged<T>>> {
let page = page(¶ms);
let response = get_authenticated_base(&path, esi, access_token, params, etag).await?;
process_paged(page, response).await
}
pub async fn post_public<T: DeserializeOwned, U: Serialize + ?Sized>(
path: &str,
esi: &Esi,
params: Option<HashMap<&str, String>>,
data: &U,
) -> Result<T, reqwest::Error> {
let url = build_url(&esi.base_url, path, params);
log::trace!(
"Requesting: {} with data: {}",
url,
serde_json::to_string(data).unwrap()
);
let response = esi.client.post(url).json(data).send().await?;
response.error_for_status_ref()?;
let result: T = response.json().await?;
Ok(result)
}
pub async fn post_authenticated<T: DeserializeOwned, U: Serialize + ?Sized>(
access_token: &str,
path: &str,
esi: &Esi,
params: Option<HashMap<&str, String>>,
data: &U,
) -> Result<T, reqwest::Error> {
let url = build_url(&esi.base_url, path, params);
log::trace!(
"Requesting: {} with data: {}",
url,
serde_json::to_string(data).unwrap()
);
let req = esi
.client
.post(url)
.header(
reqwest::header::AUTHORIZATION,
format!("Bearer {}", access_token),
)
.json(data)
.send()
.await?;
req.error_for_status_ref()?;
let result: T = req.json().await?;
Ok(result)
}
fn build_url(base_url: &str, path: &str, params: Option<HashMap<&str, String>>) -> String {
let mut url = format!("{}{}?datasource={}", base_url, path, ESI_DATASOURCE);
if let Some(params) = params {
for (key, value) in params {
url.push_str(&format!("&{}={}", key, value));
}
}
url
}
fn headers(response: &reqwest::Response) -> Headers {
Headers {
etag: response
.headers()
.get("ETag")
.unwrap()
.to_str()
.unwrap()
.to_string(),
expires: response
.headers()
.get("Expires")
.unwrap()
.to_str()
.unwrap()
.to_string(),
last_modified: response
.headers()
.get("Last-Modified")
.unwrap()
.to_str()
.unwrap()
.to_string(),
error_limit_remain: response
.headers()
.get("x-esi-error-limit-remain")
.unwrap()
.to_str()
.unwrap()
.to_string()
.parse()
.unwrap(),
error_limit_reset: response
.headers()
.get("x-esi-error-limit-reset")
.unwrap()
.to_str()
.unwrap()
.to_string()
.parse()
.unwrap(),
}
}
fn page(params: &Option<HashMap<&str, String>>) -> i32 {
match ¶ms {
Some(params) => match params.get("page") {
Some(page) => page.parse::<i32>().unwrap_or_else(|_| 1),
None => 1,
},
None => 1,
}
}
fn total_pages(response: &reqwest::Response) -> i32 {
match response.headers().get("X-Pages") {
Some(pages) => match pages.to_str() {
Ok(pages) => pages.parse::<i32>().unwrap_or_else(|_| 1),
Err(_) => 1,
},
None => 1,
}
}