i3status_rs/geolocator/
ipapi.rs

1use super::*;
2
3const IP_API_URL: &str = "https://ipapi.co/json";
4
5// This config is here just to make sure that no other config is provided
6#[derive(Deserialize, Debug, Default, Clone)]
7#[serde(deny_unknown_fields)]
8pub struct Config {}
9
10#[derive(Deserialize)]
11struct ApiResponse {
12    #[serde(flatten)]
13    data: Option<IpApiAddressInfo>,
14    #[serde(default)]
15    error: bool,
16    #[serde(default)]
17    reason: ApiError,
18}
19
20#[derive(Deserialize, Default)]
21#[serde(default)]
22struct IpApiAddressInfo {
23    ip: String,
24    version: String,
25    city: String,
26    region: String,
27    region_code: String,
28    country: String,
29    country_name: String,
30    country_code: String,
31    country_code_iso3: String,
32    country_capital: String,
33    country_tld: String,
34    continent_code: String,
35    in_eu: bool,
36    postal: Option<String>,
37    latitude: f64,
38    longitude: f64,
39    timezone: String,
40    utc_offset: String,
41    country_calling_code: String,
42    currency: String,
43    currency_name: String,
44    languages: String,
45    country_area: f64,
46    country_population: f64,
47    asn: String,
48    org: String,
49}
50
51impl From<IpApiAddressInfo> for IPAddressInfo {
52    fn from(val: IpApiAddressInfo) -> Self {
53        IPAddressInfo {
54            ip: val.ip,
55            version: Some(val.version),
56            city: val.city,
57            region: Some(val.region),
58            region_code: Some(val.region_code),
59            country: Some(val.country),
60            country_name: Some(val.country_name),
61            country_code: Some(val.country_code),
62            country_code_iso3: Some(val.country_code_iso3),
63            country_capital: Some(val.country_capital),
64            country_tld: Some(val.country_tld),
65            continent_code: Some(val.continent_code),
66            in_eu: Some(val.in_eu),
67            postal: val.postal,
68            latitude: val.latitude,
69            longitude: val.longitude,
70            timezone: Some(val.timezone),
71            utc_offset: Some(val.utc_offset),
72            country_calling_code: Some(val.country_calling_code),
73            currency: Some(val.currency),
74            currency_name: Some(val.currency_name),
75            languages: Some(val.languages),
76            country_area: Some(val.country_area),
77            country_population: Some(val.country_population),
78            asn: Some(val.asn),
79            org: Some(val.org),
80        }
81    }
82}
83
84#[derive(Deserialize, Default, Debug)]
85#[serde(transparent)]
86struct ApiError(Option<String>);
87
88impl fmt::Display for ApiError {
89    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
90        f.write_str(self.0.as_deref().unwrap_or("Unknown Error"))
91    }
92}
93impl StdError for ApiError {}
94
95pub struct Ipapi;
96
97impl Ipapi {
98    pub fn name(&self) -> Cow<'static, str> {
99        Cow::Borrowed("ipapi.co")
100    }
101
102    pub async fn get_info(&self, client: &reqwest::Client) -> Result<IPAddressInfo> {
103        let response: ApiResponse = client
104            .get(IP_API_URL)
105            .send()
106            .await
107            .error("Failed during request for current location")?
108            .json()
109            .await
110            .error("Failed while parsing location API result")?;
111
112        if response.error {
113            Err(Error {
114                message: Some("ipapi.co error".into()),
115                cause: Some(Arc::new(response.reason)),
116            })
117        } else {
118            Ok(response
119                .data
120                .error("Failed while parsing location API result")?
121                .into())
122        }
123    }
124}