use crate::{responses, requests};
use crate::shared;
use serde::{Serialize, Deserialize};
use tokio::sync::Mutex;
use std::sync::Arc;
use reqwest::header::{HeaderName, HeaderValue};
use reqwest::{Method, Url, StatusCode};
use std::str::FromStr;
use tokio::time::Duration;
use std::fmt::{Debug, Formatter};
use std::collections::HashMap;
use crate::errors::SpaceTradersClientError;
pub type HttpClient = Arc<Mutex<SpaceTradersClient>>;
pub type PostRequestHook = fn(
method: &str,
url: &str,
request_body: Option<&str>,
response_status_code: Option<u16>,
response_headers: Option<&HashMap<String, String>>,
response_body: Option<&str>,
error: Option<&SpaceTradersClientError>,
);
#[derive(Clone)]
pub struct SpaceTradersClient {
client: reqwest::Client,
post_request_hook: Option<PostRequestHook>,
}
impl Debug for SpaceTradersClient {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SpaceTradersClient")
.field("client", &self.client)
.finish()
}
}
#[derive(Serialize)]
pub struct SpaceTradersClientRequest {
method: String,
url: String,
request_headers: HashMap<String, String>,
request_text: String,
}
#[derive(Serialize)]
pub struct SpaceTradersClientResponse {
status_code: u16,
response_headers: HashMap<String, String>,
response_text: String,
}
impl SpaceTradersClient {
fn new(proxy: Option<String>) -> Self {
let mut client_builder = reqwest::ClientBuilder::new();
if let Some(proxy) = proxy {
client_builder = client_builder.proxy(reqwest::Proxy::all(proxy).unwrap());
}
Self {
client: client_builder.build().unwrap(),
post_request_hook: None,
}
}
fn set_post_request_hook(&mut self, hook: PostRequestHook) {
self.post_request_hook = Some(hook);
}
async fn execute_request(
&self, method: &str, url: &str, body: Option<&str>, token: Option<&str>,
) -> Result<SpaceTradersClientResponse, SpaceTradersClientError> {
let mut request_builder = self.client.request(
Method::from_str(&method).unwrap(),
Url::parse(url).unwrap(),
);
if let Some(token) = token {
request_builder = request_builder.header(
HeaderName::from_lowercase(b"authorization").unwrap(),
HeaderValue::from_str(&format!("Bearer {}", &token)).unwrap(),
);
}
if let Some(body) = body {
request_builder = request_builder.header(HeaderName::from_lowercase(b"content-type").unwrap(), HeaderValue::from_static("application/json"));
request_builder = request_builder.body(body.to_owned());
}
let mut attempts = 0;
let request = request_builder.build().unwrap();
loop {
attempts += 1;
if attempts > 3 {
return Err(SpaceTradersClientError::TooManyRetries);
}
match self.client.execute(request.try_clone().unwrap()).await {
Ok(response) => {
let response_headers = response.headers()
.iter().fold(HashMap::new(), |mut acc, (h, v)| {
acc.insert(h.to_string(), v.to_str().unwrap().to_string());
acc
});
let response_status = response.status();
let response_text = response.text().await?;
if let Some(post_request_hook) = self.post_request_hook {
post_request_hook(
method, url, body,
Some(response_status.as_u16()),
Some(&response_headers),
Some(&response_text),
None,
);
}
if response_status == 429 {
let retry_after: f64 = response_headers
.get("retry-after").unwrap_or(&"1.0".to_string())
.parse().unwrap_or(1.0);
log::warn!("Rate limited... waiting for {} seconds before trying again. Request: \"{} {}\"", retry_after, request.method(), request.url());
tokio::time::sleep(Duration::from_secs_f64(retry_after)).await;
continue;
} else if response_status == 401 {
return Err(SpaceTradersClientError::Unauthorized);
} else if response_status == 500 {
log::error!("Caught internal server error retrying in 2 seconds. {}", response_text);
tokio::time::sleep(Duration::from_secs(2)).await;
continue;
} else {
return Ok(
SpaceTradersClientResponse {
status_code: response_status.as_u16(),
response_headers,
response_text,
}
);
}
}
Err(e) => {
let space_traders_client_error = SpaceTradersClientError::Http(e);
if let Some(post_request_hook) = self.post_request_hook {
post_request_hook(
method, url, body,
None, None, None,
Some(&space_traders_client_error)
);
}
return Err(space_traders_client_error);
}
}
}
}
}
pub fn get_http_client(proxy: Option<String>) -> HttpClient {
Arc::new(Mutex::new(SpaceTradersClient::new(proxy)))
}
pub fn get_http_client_with_hook(proxy: Option<String>, hook: PostRequestHook) -> HttpClient {
let mut client = SpaceTradersClient::new(proxy);
client.set_post_request_hook(hook);
Arc::new(Mutex::new(client))
}
fn parse_response<'a, T: Deserialize<'a>>(response_text: &'a str) -> Result<T, SpaceTradersClientError> {
match serde_json::from_str::<T>(&response_text) {
Ok(o) => Ok(o),
Err(e) => {
log::error!("Error processing type {:?}: {}", std::any::type_name::<T>(), e);
log::error!("Error response: {}", &response_text);
match serde_json::from_str::<shared::ErrorMessage>(&response_text) {
Ok(error_message) => Err(SpaceTradersClientError::ApiError(error_message)),
Err(e) => Err(SpaceTradersClientError::JsonParse(e)),
}
}
}
}
pub async fn claim_username(http_client: HttpClient, username: String) -> Result<responses::ClaimUsername, SpaceTradersClientError> {
let http_client = http_client.lock().await;
let response = http_client.execute_request(
"POST",
&format!("https://api.spacetraders.io/users/{}/token", username),
Some("{\"message\":\"this body doesn't actually matter\"}"),
None,
)
.await?;
parse_response::<responses::ClaimUsername>(&response.response_text)
}
pub async fn get_game_status(http_client: HttpClient) -> Result<responses::GameStatus, SpaceTradersClientError> {
let http_client = http_client.lock().await;
let response = http_client.execute_request(
"GET",
"https://api.spacetraders.io/game/status",
None,
None,
)
.await?;
if response.status_code == StatusCode::SERVICE_UNAVAILABLE.as_u16() {
return Err(SpaceTradersClientError::ServiceUnavailable);
}
parse_response::<responses::GameStatus>(&response.response_text)
}
pub async fn get_my_ip_address(http_client: HttpClient) -> Result<responses::MyIpAddress, SpaceTradersClientError> {
let http_client = http_client.lock().await;
let response = http_client.execute_request(
"GET",
"https://api.ipify.org?format=json",
None,
None,
)
.await?;
parse_response::<responses::MyIpAddress>(&response.response_text)
}
#[derive(Debug, Clone)]
pub struct Client {
http_client: HttpClient,
pub username: String,
pub token: String,
}
impl Client {
pub fn new(http_client: HttpClient, username: String, token: String) -> Client {
Client {
http_client,
username,
token,
}
}
pub async fn get_my_info(&self) -> Result<responses::UserInfo, SpaceTradersClientError> {
let http_client = self.http_client.lock().await;
let response = http_client.execute_request(
"GET",
"https://api.spacetraders.io/my/account",
None,
Some(&self.token),
)
.await?;
parse_response::<responses::UserInfo>(&response.response_text)
}
pub async fn get_flight_plan(&self, flight_plan_id: String) -> Result<responses::FlightPlan, SpaceTradersClientError> {
let http_client = self.http_client.lock().await;
let response = http_client.execute_request(
"GET",
&format!("https://api.spacetraders.io/my/flight-plans/{}", flight_plan_id),
None,
Some(&self.token),
)
.await?;
parse_response::<responses::FlightPlan>(&response.response_text)
}
pub async fn create_flight_plan(&self, ship_id: String, destination: String) -> Result<responses::FlightPlan, SpaceTradersClientError> {
let flight_plan_request = requests::FlightPlanRequest {
ship_id: ship_id.clone(),
destination: destination.clone(),
};
let http_client = self.http_client.lock().await;
let response = http_client.execute_request(
"POST",
"https://api.spacetraders.io/my/flight-plans",
Some(&serde_json::to_string(&flight_plan_request).unwrap()),
Some(&self.token),
)
.await?;
parse_response::<responses::FlightPlan>(&response.response_text)
}
pub async fn get_my_loans(&self) -> Result<responses::LoanInfo, SpaceTradersClientError> {
let http_client = self.http_client.lock().await;
let response = http_client.execute_request(
"GET",
"https://api.spacetraders.io/my/loans",
None,
Some(&self.token),
)
.await?;
parse_response::<responses::LoanInfo>(&response.response_text)
}
pub async fn pay_off_loan(&self, loan_id: &str) -> Result<responses::PayLoanResponse, SpaceTradersClientError> {
let http_client = self.http_client.lock().await;
let response = http_client.execute_request(
"PUT",
&format!("https://api.spacetraders.io/my/loans/{}", loan_id),
Some("{\"message\":\"this body doesn't actually matter\"}"),
Some(&self.token),
)
.await?;
parse_response::<responses::PayLoanResponse>(&response.response_text)
}
pub async fn request_new_loan(&self, loan_type: shared::LoanType) -> Result<responses::RequestLoan, SpaceTradersClientError> {
let request_new_loan_request = requests::RequestNewLoanRequest {
loan_type
};
let http_client = self.http_client.lock().await;
let response = http_client.execute_request(
"POST",
"https://api.spacetraders.io/my/loans",
Some(&serde_json::to_string(&request_new_loan_request).unwrap()),
Some(&self.token),
)
.await?;
parse_response::<responses::RequestLoan>(&response.response_text)
}
pub async fn get_location_info(&self, location_symbol: String) -> Result<responses::LocationInfo, SpaceTradersClientError> {
let http_client = self.http_client.lock().await;
let response = http_client.execute_request(
"GET",
&format!("https://api.spacetraders.io/locations/{}", location_symbol),
None,
Some(&self.token),
)
.await?;
parse_response::<responses::LocationInfo>(&response.response_text)
}
pub async fn get_location_marketplace(&self, location_symbol: &str) -> Result<responses::LocationMarketplace, SpaceTradersClientError> {
let http_client = self.http_client.lock().await;
let response = http_client.execute_request(
"GET",
&format!("https://api.spacetraders.io/locations/{}/marketplace", location_symbol),
None,
Some(&self.token),
)
.await?;
parse_response::<responses::LocationMarketplace>(&response.response_text)
}
pub async fn create_purchase_order(&self, ship_id: String, good: shared::Good, quantity: i32) -> Result<responses::PurchaseOrder, SpaceTradersClientError> {
let purchase_order_request = requests::PurchaseOrderRequest {
ship_id: ship_id.clone(),
good,
quantity,
};
let http_client = self.http_client.lock().await;
let response = http_client.execute_request(
"POST",
"https://api.spacetraders.io/my/purchase-orders",
Some(&serde_json::to_string(&purchase_order_request).unwrap()),
Some(&self.token),
)
.await?;
parse_response::<responses::PurchaseOrder>(&response.response_text)
}
pub async fn create_sell_order(&self, ship_id: String, good: shared::Good, quantity: i32) -> Result<responses::PurchaseOrder, SpaceTradersClientError> {
let sell_order_request = requests::SellOrderRequest {
ship_id: ship_id.clone(),
good,
quantity,
};
let http_client = self.http_client.lock().await;
let response = http_client.execute_request(
"POST",
"https://api.spacetraders.io/my/sell-orders",
Some(&serde_json::to_string(&sell_order_request).unwrap()),
Some(&self.token),
)
.await?;
parse_response::<responses::PurchaseOrder>(&response.response_text)
}
pub async fn purchase_ship(&self, location_symbol: String, ship_type: String) -> Result<responses::PurchaseShip, SpaceTradersClientError> {
let purchase_ship_request = requests::PurchaseShipRequest {
location: location_symbol.clone(),
ship_type: ship_type.clone(),
};
let http_client = self.http_client.lock().await;
let response = http_client.execute_request(
"POST",
"https://api.spacetraders.io/my/ships",
Some(&serde_json::to_string(&purchase_ship_request).unwrap()),
Some(&self.token),
)
.await?;
parse_response::<responses::PurchaseShip>(&response.response_text)
}
pub async fn get_my_ship(&self, ship_id: &str) -> Result<responses::MyShip, SpaceTradersClientError> {
let http_client = self.http_client.lock().await;
let response = http_client.execute_request(
"GET",
&format!("https://api.spacetraders.io/my/ships/{}", ship_id),
None,
Some(&self.token),
)
.await?;
parse_response::<responses::MyShip>(&response.response_text)
}
pub async fn get_my_ships(&self) -> Result<responses::MyShips, SpaceTradersClientError> {
let http_client = self.http_client.lock().await;
let response = http_client.execute_request(
"GET",
"https://api.spacetraders.io/my/ships",
None,
Some(&self.token),
)
.await?;
parse_response::<responses::MyShips>(&response.response_text)
}
pub async fn jettison_cargo(&self, ship_id: &str, good: shared::Good, quantity: i32) -> Result<responses::JettisonCargo, SpaceTradersClientError> {
let jettison_cargo_request = requests::JettisonCargo {
good,
quantity,
};
let http_client = self.http_client.lock().await;
let response = http_client.execute_request(
"POST",
&format!("https://api.spacetraders.io/my/ships/{}/jettison", ship_id),
Some(&serde_json::to_string(&jettison_cargo_request).unwrap()),
Some(&self.token),
)
.await?;
parse_response::<responses::JettisonCargo>(&response.response_text)
}
pub async fn get_ships_for_sale(&self) -> Result<responses::ShipsForSale, SpaceTradersClientError> {
let http_client = self.http_client.lock().await;
let response = http_client.execute_request(
"GET",
"https://api.spacetraders.io/game/ships",
None,
Some(&self.token),
)
.await?;
parse_response::<responses::ShipsForSale>(&response.response_text)
}
pub async fn get_systems_info(&self) -> Result<responses::SystemsInfo, SpaceTradersClientError> {
let http_client = self.http_client.lock().await;
let response = http_client.execute_request(
"GET",
"https://api.spacetraders.io/game/systems",
None,
Some(&self.token),
)
.await?;
parse_response::<responses::SystemsInfo>(&response.response_text)
}
pub async fn get_available_loans(&self) -> Result<responses::AvailableLoans, SpaceTradersClientError> {
let http_client = self.http_client.lock().await;
let response = http_client.execute_request(
"GET",
"https://api.spacetraders.io/types/loans",
None,
Some(&self.token),
)
.await?;
parse_response::<responses::AvailableLoans>(&response.response_text)
}
pub async fn attempt_warp_jump(&self, ship_id: String) -> Result<responses::FlightPlan, SpaceTradersClientError> {
let warp_jump_request = requests::WarpJump {
ship_id
};
let http_client = self.http_client.lock().await;
let response = http_client.execute_request(
"POST",
"https://api.spacetraders.io/my/warp-jumps",
Some(&serde_json::to_string(&warp_jump_request).unwrap()),
Some(&self.token),
)
.await?;
parse_response::<responses::FlightPlan>(&response.response_text)
}
}