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;
10pub 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 let page = match ¶ms {
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 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 ¶ms {
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 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}