Skip to main content

ramadan_cli/
geo.rs

1use reqwest::blocking::Client;
2use serde::Deserialize;
3
4#[derive(Debug, Clone, Deserialize)]
5pub struct GeoLocation {
6    pub city: String,
7    pub country: String,
8    pub latitude: f64,
9    pub longitude: f64,
10    pub timezone: String,
11}
12
13#[derive(Debug, Clone, Deserialize)]
14pub struct CityCountryGuess {
15    pub city: String,
16    pub country: String,
17    pub latitude: f64,
18    pub longitude: f64,
19    pub timezone: Option<String>,
20}
21
22#[derive(Debug, Deserialize)]
23struct IpApiResponse {
24    city: String,
25    country: String,
26    lat: f64,
27    lon: f64,
28    timezone: Option<String>,
29}
30
31#[derive(Debug, Deserialize)]
32struct IpapiCoResponse {
33    city: String,
34    country_name: String,
35    latitude: f64,
36    longitude: f64,
37    timezone: Option<String>,
38}
39
40#[derive(Debug, Deserialize)]
41struct IpWhoisTimezone {
42    id: Option<String>,
43}
44
45#[derive(Debug, Deserialize)]
46struct IpWhoisResponse {
47    success: bool,
48    city: String,
49    country: String,
50    latitude: f64,
51    longitude: f64,
52    timezone: Option<IpWhoisTimezone>,
53}
54
55#[derive(Debug, Deserialize)]
56struct OpenMeteoSearchResponse {
57    results: Option<Vec<OpenMeteoResult>>,
58}
59
60#[derive(Debug, Deserialize)]
61struct OpenMeteoResult {
62    name: String,
63    country: String,
64    latitude: f64,
65    longitude: f64,
66    timezone: Option<String>,
67}
68
69fn try_ip_api(client: &Client) -> Option<GeoLocation> {
70    let response = client
71        .get("http://ip-api.com/json/?fields=city,country,lat,lon,timezone")
72        .send()
73        .ok()?
74        .json::<IpApiResponse>()
75        .ok()?;
76
77    Some(GeoLocation {
78        city: response.city,
79        country: response.country,
80        latitude: response.lat,
81        longitude: response.lon,
82        timezone: response.timezone.unwrap_or_default(),
83    })
84}
85
86fn try_ipapi_co(client: &Client) -> Option<GeoLocation> {
87    let response = client
88        .get("https://ipapi.co/json/")
89        .send()
90        .ok()?
91        .json::<IpapiCoResponse>()
92        .ok()?;
93
94    Some(GeoLocation {
95        city: response.city,
96        country: response.country_name,
97        latitude: response.latitude,
98        longitude: response.longitude,
99        timezone: response.timezone.unwrap_or_default(),
100    })
101}
102
103fn try_ip_whois(client: &Client) -> Option<GeoLocation> {
104    let response = client
105        .get("https://ipwho.is/")
106        .send()
107        .ok()?
108        .json::<IpWhoisResponse>()
109        .ok()?;
110
111    if !response.success {
112        return None;
113    }
114
115    Some(GeoLocation {
116        city: response.city,
117        country: response.country,
118        latitude: response.latitude,
119        longitude: response.longitude,
120        timezone: response.timezone.and_then(|tz| tz.id).unwrap_or_default(),
121    })
122}
123
124pub fn guess_location(client: &Client) -> Option<GeoLocation> {
125    try_ip_api(client)
126        .or_else(|| try_ipapi_co(client))
127        .or_else(|| try_ip_whois(client))
128}
129
130pub fn guess_city_country(client: &Client, query: &str) -> Option<CityCountryGuess> {
131    let trimmed = query.trim();
132    if trimmed.is_empty() {
133        return None;
134    }
135
136    let response = client
137        .get("https://geocoding-api.open-meteo.com/v1/search")
138        .query(&[
139            ("name", trimmed),
140            ("count", "1"),
141            ("language", "en"),
142            ("format", "json"),
143        ])
144        .send()
145        .ok()?
146        .json::<OpenMeteoSearchResponse>()
147        .ok()?;
148
149    let result = response.results?.into_iter().next()?;
150
151    Some(CityCountryGuess {
152        city: result.name,
153        country: result.country,
154        latitude: result.latitude,
155        longitude: result.longitude,
156        timezone: result.timezone,
157    })
158}