1use crate::error::IdentityError;
2
3pub struct Geo {
10 pub timezone: String,
11 pub country_code: Option<String>,
12}
13
14pub async fn resolve_geo(
17 explicit_tz: Option<&str>,
18 proxy: Option<&str>,
19) -> Result<Geo, IdentityError> {
20 let fetched = fetch_from_ipapi(proxy).await;
21
22 match (explicit_tz, fetched) {
23 (Some(tz), Ok(geo)) => Ok(Geo {
26 timezone: tz.to_string(),
27 country_code: geo.country_code,
28 }),
29 (Some(tz), Err(_)) => Ok(Geo {
30 timezone: tz.to_string(),
31 country_code: None,
32 }),
33 (None, result) => result,
34 }
35}
36
37async fn fetch_from_ipapi(proxy: Option<&str>) -> Result<Geo, IdentityError> {
40 let mut builder = reqwest::Client::builder();
41 if let Some(proxy_url) = proxy {
42 builder = builder.proxy(
43 reqwest::Proxy::all(proxy_url)
44 .map_err(|e| IdentityError::TimezoneError(format!("invalid proxy: {e}")))?,
45 );
46 }
47 let client = builder
48 .build()
49 .map_err(|e| IdentityError::TimezoneError(format!("http client error: {e}")))?;
50
51 let resp: serde_json::Value = client
52 .get("http://ip-api.com/json/?fields=timezone,countryCode")
53 .send()
54 .await
55 .map_err(|e| IdentityError::TimezoneError(format!("ip-api request failed: {e}")))?
56 .json()
57 .await
58 .map_err(|e| IdentityError::TimezoneError(format!("ip-api parse failed: {e}")))?;
59
60 let timezone = resp
61 .get("timezone")
62 .and_then(|v| v.as_str())
63 .map(|s| s.to_string())
64 .ok_or_else(|| IdentityError::TimezoneError("ip-api returned no timezone".into()))?;
65
66 Ok(Geo {
67 timezone,
68 country_code: resp
69 .get("countryCode")
70 .and_then(|v| v.as_str())
71 .map(|s| s.to_uppercase()),
72 })
73}