eversal_lib/esi/
mod.rs

1use std::{collections::HashMap, sync::OnceLock, time::Duration};
2
3use log::{trace, warn};
4use reqwest::Client;
5use serde::{de::DeserializeOwned, Serialize};
6
7use crate::LibResult;
8
9pub mod auth;
10// ESI
11pub mod alliance;
12pub mod character;
13pub mod corporation;
14pub mod market;
15pub mod scope;
16pub mod universe;
17
18#[derive(Debug, serde::Serialize, serde::Deserialize)]
19pub struct Headers {
20  pub etag: String,
21  pub expires: String,
22  pub last_modified: String,
23}
24
25#[derive(Debug, serde::Serialize, serde::Deserialize)]
26pub struct Paged<T> {
27  pub data: T,
28  pub page: i32,
29  pub total_pages: i32,
30}
31
32#[derive(Debug, serde::Serialize, serde::Deserialize)]
33pub struct Response<T> {
34  pub data: T,
35  pub headers: Headers,
36}
37
38const ESI_URL: &str = "https://esi.evetech.net";
39const ESI_DATASOURCE: &str = "tranquility";
40const TIMEOUT: u64 = 10;
41const CLIENT: OnceLock<Client> = OnceLock::new();
42const USER_AGENT: OnceLock<String> = OnceLock::new();
43
44pub fn client() -> Client {
45  CLIENT.get_or_init(|| Client::new()).clone()
46}
47
48pub fn user_agent() -> String {
49  USER_AGENT
50    .get_or_init(|| format!("{} ({})", "eversal", "contact@eversal.io"))
51    .clone()
52}
53
54pub fn initialize(application_name: String, application_email: String) {
55  match CLIENT.set(Client::new()) {
56    Ok(_) => (),
57    Err(_) => warn!("Client already initialized"),
58  };
59  match USER_AGENT.set(format!("{} ({})", application_name, application_email)) {
60    Ok(_) => (),
61    Err(_) => warn!("User agent already initialized"),
62  };
63}
64
65async fn get_public_base(
66  path: &str,
67  params: Option<HashMap<&str, String>>,
68  etag: Option<&str>,
69) -> LibResult<reqwest::Response>{
70  let url = build_url(path, params);
71  trace!("Requesting: {}", url);
72  let mut req = client()
73    .get(url)
74    .header(reqwest::header::USER_AGENT, user_agent());
75  if let Some(etag) = etag {
76    req = req.header(reqwest::header::IF_NONE_MATCH, etag);
77  }
78  let req: reqwest::Response = req
79    .timeout(std::time::Duration::from_secs(TIMEOUT))
80    .send()
81    .await?;
82
83  req.error_for_status_ref()?;
84  if req.status().as_u16() == 304 {
85    return Err(crate::Error::new(304, "Not Modified".to_string()));
86  }
87  Ok(req)
88}
89
90pub async fn get_public<T: DeserializeOwned>(
91  path: &str,
92  params: Option<HashMap<&str, String>>,
93  etag: Option<&str>,
94) -> LibResult<Response<T>> {
95  let req = get_public_base(path, params, etag).await?;
96  let headers = headers(&req);
97  let data: T = req.json().await?;
98  Ok(Response {
99    data,
100    headers,
101  })
102}
103
104pub async fn get_public_paged<T: DeserializeOwned>(
105  path: &str,
106  params: Option<HashMap<&str, String>>,
107  etag: Option<&str>
108) -> LibResult<Response<Paged<T>>> {
109  // Verify page
110  let page = match &params {
111    Some(params) => match params.get("page") {
112      Some(page) => match page.parse::<i32>() {
113        Ok(page) => page,
114        Err(_) => 1,
115      },
116      None => 1,
117    },
118    None => 1,
119  };
120  let req = get_public_base(path, params, etag).await?;
121  // Pages header
122  let total_pages = match req.headers().get("X-Pages") {
123    Some(pages) => match pages.to_str() {
124      Ok(pages) => match pages.parse::<i32>() {
125        Ok(pages) => pages,
126        Err(_) => 1,
127      },
128      Err(_) => 1,
129    },
130    None => 1,
131  };
132  let headers = headers(&req);
133  let data: T = req.json().await?;
134  Ok(Response {
135    data: Paged {
136      data,
137      page,
138      total_pages,
139    },
140    headers,
141  })
142}
143
144pub async fn get_authenticated<T: DeserializeOwned>(
145  access_token: &str,
146  path: &str,
147  params: Option<HashMap<&str, String>>,
148  etag: Option<&str>,
149) -> LibResult<Response<T>> {
150  let url = build_url(path, params);
151  trace!("Requesting: {}", url);
152  let req = client()
153    .get(url)
154    .header(reqwest::header::USER_AGENT, user_agent())
155    .header(
156      reqwest::header::AUTHORIZATION,
157      format!("Bearer {}", access_token),
158    )
159    .header(reqwest::header::IF_NONE_MATCH, etag.unwrap_or(""))
160    .timeout(std::time::Duration::from_secs(TIMEOUT))
161    .send()
162    .await?;
163
164  req.error_for_status_ref()?;
165  if req.status().as_u16() == 304 {
166    return Err(crate::Error::new(304, "Not Modified".to_string()));
167  }
168  let headers = headers(&req);
169  let data: T = req.json().await?;
170  Ok(Response {
171    data,
172    headers,
173  })
174}
175
176pub async fn get_authenticated_paged<T: DeserializeOwned>(
177  access_token: &str,
178  path: &str,
179  params: Option<HashMap<&str, String>>,
180  etag: Option<&str>,
181) -> LibResult<Response<Paged<T>>> {
182  let page = match &params {
183    Some(params) => match params.get("page") {
184      Some(page) => match page.parse::<i32>() {
185        Ok(page) => page,
186        Err(_) => 1,
187      },
188      None => 1,
189    },
190    None => 1,
191  };
192  let url = build_url(path, params);
193  trace!("Requesting: {}", url);
194  let req = client()
195    .get(url)
196    .header(reqwest::header::USER_AGENT, user_agent())
197    .header(
198      reqwest::header::AUTHORIZATION,
199      format!("Bearer {}", access_token),
200    )
201    .header(reqwest::header::IF_NONE_MATCH, etag.unwrap_or(""))
202    .timeout(std::time::Duration::from_secs(TIMEOUT))
203    .send()
204    .await?;
205
206  req.error_for_status_ref()?;
207  if req.status().as_u16() == 304 {
208    return Err(crate::Error::new(304, "Not Modified".to_string()));
209  }
210  let total_pages = match req.headers().get("X-Pages") {
211    Some(pages) => match pages.to_str() {
212      Ok(pages) => match pages.parse::<i32>() {
213        Ok(pages) => pages,
214        Err(_) => 1,
215      },
216      Err(_) => 1,
217    },
218    None => 1,
219  };
220  let headers = headers(&req);
221  let data: T = req.json().await?;
222  Ok(Response {
223    data: Paged {
224      data,
225      page,
226      total_pages,
227    },
228    headers
229  })
230}
231
232pub async fn post_public<T: DeserializeOwned, U: Serialize + ?Sized>(
233  path: &str,
234  params: Option<HashMap<&str, String>>,
235  data: &U,
236) -> Result<T, reqwest::Error> {
237  let url = build_url(path, params);
238  trace!(
239    "Requesting: {} with data: {}",
240    url,
241    serde_json::to_string(data).unwrap()
242  );
243  let req = client()
244    .post(url)
245    .header(reqwest::header::USER_AGENT, user_agent())
246    .timeout(std::time::Duration::from_secs(TIMEOUT))
247    .json(data)
248    .send()
249    .await?;
250
251  req.error_for_status_ref()?;
252  let result: T = req.json().await?;
253  Ok(result)
254}
255
256pub async fn post_authenticated<T: DeserializeOwned, U: Serialize + ?Sized>(
257  access_token: &str,
258  path: &str,
259  params: Option<HashMap<&str, String>>,
260  data: &U,
261) -> Result<T, reqwest::Error> {
262  let url = build_url(path, params);
263  trace!(
264    "Requesting: {} with data: {}",
265    url,
266    serde_json::to_string(data).unwrap()
267  );
268  let req = client()
269    .post(url)
270    .header(reqwest::header::USER_AGENT, user_agent())
271    .header(
272      reqwest::header::AUTHORIZATION,
273      format!("Bearer {}", access_token),
274    )
275    .timeout(Duration::from_secs(TIMEOUT))
276    .json(data)
277    .send()
278    .await?;
279
280  req.error_for_status_ref()?;
281  let result: T = req.json().await?;
282  Ok(result)
283}
284
285fn build_url(path: &str, params: Option<HashMap<&str, String>>) -> String {
286  let mut url = format!("{}/latest/{}?datasource={}", ESI_URL, path, ESI_DATASOURCE);
287  if let Some(params) = params {
288    for (key, value) in params {
289      url.push_str(&format!("&{}={}", key, value));
290    }
291  }
292  url
293}
294
295fn headers(req: &reqwest::Response) -> Headers {
296  Headers {
297    etag: req.headers().get("ETag").unwrap().to_str().unwrap().to_string(),
298    // Parse from RFC 1123
299    expires: req.headers().get("Expires").unwrap().to_str().unwrap().to_string(),
300    last_modified: req.headers().get("Last-Modified").unwrap().to_str().unwrap().to_string(),
301  }
302}