i3status_rs/geolocator/
ip2location.rs

1use 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    // Provided without api key
28    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    // #[serde(rename = "as")]
39    // as_: String,
40    // is_proxy: bool,
41
42    // Requires api key
43    // isp
44    // domain
45    // net_speed
46    // idd_code
47    // area_code
48    // weather_station_code
49    // weather_station_name
50    // mcc
51    // mnc
52    // mobile_brand
53    // elevation
54    // usage_type
55    // address_type
56    // ads_category
57    // ads_category_name
58    // district
59    continent: Option<Continent>,
60    country: Option<Country>,
61    region: Option<Region>,
62    // city.name
63    // city.translation
64    time_zone_info: Option<TimeZoneInfo>,
65    // geotargeting.metro
66    // fraud_score
67    // proxy.last_seen
68    // proxy.proxy_type
69    // proxy.threat
70    // proxy.provider
71    // proxy.is_vpn
72    // proxy.is_tor
73    // proxy.is_data_center
74    // proxy.is_public_proxy
75    // proxy.is_web_proxy
76    // proxy.is_web_crawler
77    // proxy.is_residential_proxy
78    // proxy.is_spammer
79    // proxy.is_scanner
80    // proxy.is_botnet
81    // proxy.is_consumer_privacy_network
82    // proxy.is_enterprise_private_network
83}
84
85#[derive(Deserialize)]
86struct Continent {
87    // name,
88    code: String,
89    // hemisphere,
90    // translation,
91}
92
93#[derive(Deserialize)]
94struct Country {
95    // name: String,
96    alpha3_code: String,
97    // numeric_code: String,
98    // demonym: String,
99    // flag: String,
100    capital: String,
101    total_area: f64,
102    population: f64,
103    currency: Currency,
104    language: Language,
105    tld: String,
106    // translation,
107}
108
109#[derive(Deserialize)]
110struct Currency {
111    name: String,
112    code: String,
113    // translation,
114}
115#[derive(Deserialize)]
116struct Language {
117    // name: String,
118    code: String,
119}
120#[derive(Deserialize)]
121struct Region {
122    // name: String,
123    code: String,
124    // translation,
125}
126
127#[derive(Deserialize)]
128struct TimeZoneInfo {
129    olson: String,
130    // current_time: String
131    // gmt_offset: String
132    // is_dst: String
133    // sunrise: String
134    // sunset: String
135}
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}