use crate::error::IdentityError;
pub struct Geo {
pub timezone: String,
pub country_code: Option<String>,
}
pub async fn resolve_geo(
explicit_tz: Option<&str>,
proxy: Option<&str>,
) -> Result<Geo, IdentityError> {
let fetched = fetch_from_ipapi(proxy).await;
match (explicit_tz, fetched) {
(Some(tz), Ok(geo)) => Ok(Geo {
timezone: tz.to_string(),
country_code: geo.country_code,
}),
(Some(tz), Err(_)) => Ok(Geo {
timezone: tz.to_string(),
country_code: None,
}),
(None, result) => result,
}
}
async fn fetch_from_ipapi(proxy: Option<&str>) -> Result<Geo, IdentityError> {
let mut builder = reqwest::Client::builder();
if let Some(proxy_url) = proxy {
builder = builder.proxy(
reqwest::Proxy::all(proxy_url)
.map_err(|e| IdentityError::TimezoneError(format!("invalid proxy: {e}")))?,
);
}
let client = builder
.build()
.map_err(|e| IdentityError::TimezoneError(format!("http client error: {e}")))?;
let resp: serde_json::Value = client
.get("http://ip-api.com/json/?fields=timezone,countryCode")
.send()
.await
.map_err(|e| IdentityError::TimezoneError(format!("ip-api request failed: {e}")))?
.json()
.await
.map_err(|e| IdentityError::TimezoneError(format!("ip-api parse failed: {e}")))?;
let timezone = resp
.get("timezone")
.and_then(|v| v.as_str())
.map(|s| s.to_string())
.ok_or_else(|| IdentityError::TimezoneError("ip-api returned no timezone".into()))?;
Ok(Geo {
timezone,
country_code: resp
.get("countryCode")
.and_then(|v| v.as_str())
.map(|s| s.to_uppercase()),
})
}