use reqwest::blocking::Client;
use serde::Deserialize;
#[derive(Debug, Clone, Deserialize)]
pub struct GeoLocation {
pub city: String,
pub country: String,
pub latitude: f64,
pub longitude: f64,
pub timezone: String,
}
#[derive(Debug, Clone, Deserialize)]
pub struct CityCountryGuess {
pub city: String,
pub country: String,
pub latitude: f64,
pub longitude: f64,
pub timezone: Option<String>,
}
#[derive(Debug, Deserialize)]
struct IpApiResponse {
city: String,
country: String,
lat: f64,
lon: f64,
timezone: Option<String>,
}
#[derive(Debug, Deserialize)]
struct IpapiCoResponse {
city: String,
country_name: String,
latitude: f64,
longitude: f64,
timezone: Option<String>,
}
#[derive(Debug, Deserialize)]
struct IpWhoisTimezone {
id: Option<String>,
}
#[derive(Debug, Deserialize)]
struct IpWhoisResponse {
success: bool,
city: String,
country: String,
latitude: f64,
longitude: f64,
timezone: Option<IpWhoisTimezone>,
}
#[derive(Debug, Deserialize)]
struct OpenMeteoSearchResponse {
results: Option<Vec<OpenMeteoResult>>,
}
#[derive(Debug, Deserialize)]
struct OpenMeteoResult {
name: String,
country: String,
latitude: f64,
longitude: f64,
timezone: Option<String>,
}
fn try_ip_api(client: &Client) -> Option<GeoLocation> {
let response = client
.get("http://ip-api.com/json/?fields=city,country,lat,lon,timezone")
.send()
.ok()?
.json::<IpApiResponse>()
.ok()?;
Some(GeoLocation {
city: response.city,
country: response.country,
latitude: response.lat,
longitude: response.lon,
timezone: response.timezone.unwrap_or_default(),
})
}
fn try_ipapi_co(client: &Client) -> Option<GeoLocation> {
let response = client
.get("https://ipapi.co/json/")
.send()
.ok()?
.json::<IpapiCoResponse>()
.ok()?;
Some(GeoLocation {
city: response.city,
country: response.country_name,
latitude: response.latitude,
longitude: response.longitude,
timezone: response.timezone.unwrap_or_default(),
})
}
fn try_ip_whois(client: &Client) -> Option<GeoLocation> {
let response = client
.get("https://ipwho.is/")
.send()
.ok()?
.json::<IpWhoisResponse>()
.ok()?;
if !response.success {
return None;
}
Some(GeoLocation {
city: response.city,
country: response.country,
latitude: response.latitude,
longitude: response.longitude,
timezone: response.timezone.and_then(|tz| tz.id).unwrap_or_default(),
})
}
pub fn guess_location(client: &Client) -> Option<GeoLocation> {
try_ip_api(client)
.or_else(|| try_ipapi_co(client))
.or_else(|| try_ip_whois(client))
}
pub fn guess_city_country(client: &Client, query: &str) -> Option<CityCountryGuess> {
let trimmed = query.trim();
if trimmed.is_empty() {
return None;
}
let response = client
.get("https://geocoding-api.open-meteo.com/v1/search")
.query(&[
("name", trimmed),
("count", "1"),
("language", "en"),
("format", "json"),
])
.send()
.ok()?
.json::<OpenMeteoSearchResponse>()
.ok()?;
let result = response.results?.into_iter().next()?;
Some(CityCountryGuess {
city: result.name,
country: result.country,
latitude: result.latitude,
longitude: result.longitude,
timezone: result.timezone,
})
}