Skip to main content

rustenium_identity/
tz.rs

1use crate::error::IdentityError;
2
3/// Resolve timezone: use the explicit override if provided,
4/// otherwise query ip-api.com to get the timezone for the proxy IP.
5pub async fn resolve_timezone(
6    explicit: Option<&str>,
7    proxy: Option<&str>,
8) -> Result<String, IdentityError> {
9    if let Some(tz) = explicit {
10        return Ok(tz.to_string());
11    }
12
13    fetch_timezone_from_ipapi(proxy).await
14}
15
16/// Query ip-api.com to get the IANA timezone for the current IP
17/// (optionally routed through a proxy).
18async fn fetch_timezone_from_ipapi(proxy: Option<&str>) -> Result<String, IdentityError> {
19    let mut builder = reqwest::Client::builder();
20    if let Some(proxy_url) = proxy {
21        builder = builder.proxy(
22            reqwest::Proxy::all(proxy_url)
23                .map_err(|e| IdentityError::TimezoneError(format!("invalid proxy: {e}")))?,
24        );
25    }
26    let client = builder
27        .build()
28        .map_err(|e| IdentityError::TimezoneError(format!("http client error: {e}")))?;
29
30    let resp: serde_json::Value = client
31        .get("http://ip-api.com/json/?fields=timezone")
32        .send()
33        .await
34        .map_err(|e| IdentityError::TimezoneError(format!("ip-api request failed: {e}")))?
35        .json()
36        .await
37        .map_err(|e| IdentityError::TimezoneError(format!("ip-api parse failed: {e}")))?;
38
39    resp.get("timezone")
40        .and_then(|v| v.as_str())
41        .map(|s| s.to_string())
42        .ok_or_else(|| IdentityError::TimezoneError("ip-api returned no timezone".into()))
43}