i3status_rs/geolocator/
ip2location.rs1use super::*;
2
3const IP_API_URL: &str = "https://api.ip2location.io/";
4pub(super) const API_KEY_ENV: &str = "IP2LOCATION_API_KEY";
5
6#[derive(Deserialize, Debug, Default, Clone)]
7#[serde(deny_unknown_fields)]
8pub struct Config {
9 #[serde(default = "getenv_api_key")]
10 pub api_key: Option<String>,
11}
12
13fn getenv_api_key() -> Option<String> {
14 std::env::var(API_KEY_ENV).ok()
15}
16
17#[derive(Deserialize)]
18struct ApiResponse {
19 #[serde(flatten)]
20 data: Option<Ip2LocationAddressInfo>,
21 #[serde(default)]
22 error: Option<ApiError>,
23}
24
25#[derive(Deserialize)]
26struct Ip2LocationAddressInfo {
27 ip: String,
29 country_code: String,
30 country_name: String,
31 region_name: String,
32 city_name: String,
33 latitude: f64,
34 longitude: f64,
35 zip_code: String,
36 time_zone: String,
37 asn: String,
38 continent: Option<Continent>,
60 country: Option<Country>,
61 region: Option<Region>,
62 time_zone_info: Option<TimeZoneInfo>,
65 }
84
85#[derive(Deserialize)]
86struct Continent {
87 code: String,
89 }
92
93#[derive(Deserialize)]
94struct Country {
95 alpha3_code: String,
97 capital: String,
101 total_area: f64,
102 population: f64,
103 currency: Currency,
104 language: Language,
105 tld: String,
106 }
108
109#[derive(Deserialize)]
110struct Currency {
111 name: String,
112 code: String,
113 }
115#[derive(Deserialize)]
116struct Language {
117 code: String,
119}
120#[derive(Deserialize)]
121struct Region {
122 code: String,
124 }
126
127#[derive(Deserialize)]
128struct TimeZoneInfo {
129 olson: String,
130 }
136
137impl From<Ip2LocationAddressInfo> for IPAddressInfo {
138 fn from(val: Ip2LocationAddressInfo) -> Self {
139 let mut info = IPAddressInfo {
140 ip: val.ip,
141 city: val.city_name,
142 latitude: val.latitude,
143 longitude: val.longitude,
144 region: Some(val.region_name),
145 country: Some(val.country_code.clone()),
146 country_name: Some(val.country_name),
147 country_code: Some(val.country_code),
148 postal: Some(val.zip_code),
149 utc_offset: Some(val.time_zone),
150 asn: Some(val.asn),
151 ..Default::default()
152 };
153
154 if let Some(region) = val.region {
155 info.region_code = Some(region.code);
156 }
157
158 if let Some(country) = val.country {
159 info.country_area = Some(country.total_area);
160 info.country_population = Some(country.population);
161 info.currency = Some(country.currency.code);
162 info.currency_name = Some(country.currency.name);
163 info.languages = Some(country.language.code);
164 info.country_tld = Some(country.tld);
165 info.country_capital = Some(country.capital);
166 info.country_code_iso3 = Some(country.alpha3_code);
167 }
168
169 if let Some(continent) = val.continent {
170 info.continent_code = Some(continent.code);
171 }
172
173 if let Some(time_zone_info) = val.time_zone_info {
174 info.timezone = Some(time_zone_info.olson);
175 }
176
177 info
178 }
179}
180
181#[derive(Deserialize, Default, Debug)]
182struct ApiError {
183 error_code: u32,
184 error_message: String,
185}
186
187impl fmt::Display for ApiError {
188 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
189 f.write_fmt(format_args!(
190 "Error {}: {}",
191 self.error_code, self.error_message
192 ))
193 }
194}
195impl StdError for ApiError {}
196
197pub struct Ip2Location;
198
199impl Ip2Location {
200 pub fn name(&self) -> Cow<'static, str> {
201 Cow::Borrowed("ip2location.io")
202 }
203
204 pub async fn get_info(
205 &self,
206 client: &reqwest::Client,
207 api_key: &Option<String>,
208 ) -> Result<IPAddressInfo> {
209 let mut request_builder = client.get(IP_API_URL);
210
211 if let Some(api_key) = api_key {
212 request_builder = request_builder.query(&[("key", api_key)]);
213 }
214
215 let response: ApiResponse = request_builder
216 .send()
217 .await
218 .error("Failed during request for current location")?
219 .json()
220 .await
221 .error("Failed while parsing location API result")?;
222
223 if let Some(error) = response.error {
224 Err(Error {
225 message: Some("ip2location.io error".into()),
226 cause: Some(Arc::new(error)),
227 })
228 } else {
229 Ok(response
230 .data
231 .error("Failed while parsing location API result")?
232 .into())
233 }
234 }
235}