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}