i3status_rs/
geolocator.rs

1//! Geolocation service
2//!
3//! This global module can be used to provide geolocation information
4//! to blocks that support it.
5//!
6//! ipapi.co is the default geolocator service.
7//!
8//! # Configuration
9//!
10//! # ipapi.co Options
11//!
12//! Key | Values | Required | Default
13//! ----|--------|----------|--------
14//! `geolocator` | `ipapi` | Yes | None
15//!
16//! # Ip2Location.io Options
17//!
18//! Key | Values | Required | Default
19//! ----|--------|----------|--------
20//! `geolocator` | `ip2location` | Yes | None
21//! `api_key` | Your Ip2Location.io API key. | No | None
22//!
23//! An api key is not required to get back basic information from ip2location.io.
24//! However, to get more additional information, an api key is required.
25//! See [pricing](https://www.ip2location.io/pricing) for more information.
26//!
27//! The `api_key` option can be omitted from configuration, in which case it
28//! can be provided in the environment variable `IP2LOCATION_API_KEY`
29//!
30//!
31//! # Examples
32//!
33//! Use the default geolocator service:
34//!
35//! ```toml
36//! [geolocator]
37//! geolocator = "ipapi"
38//! ```
39//!
40//! Use Ip2Location.io
41//!
42//! ```toml
43//! [geolocator]
44//! geolocator = "ip2location"
45//! api_key = "XXX"
46//! ```
47
48use crate::errors::{Error, ErrorContext as _, Result, StdError};
49use std::borrow::Cow;
50use std::fmt;
51use std::sync::{Arc, Mutex};
52use std::time::{Duration, Instant};
53
54use serde::Deserialize;
55use smart_default::SmartDefault;
56
57mod ip2location;
58mod ipapi;
59
60#[derive(Debug)]
61struct AutolocateResult {
62    location: IPAddressInfo,
63    timestamp: Instant,
64}
65
66#[derive(Deserialize, Clone, Default, Debug)]
67pub struct IPAddressInfo {
68    // Required fields
69    pub ip: String,
70    pub latitude: f64,
71    pub longitude: f64,
72    pub city: String,
73
74    // Optional fields
75    pub version: Option<String>,
76    pub region: Option<String>,
77    pub region_code: Option<String>,
78    pub country: Option<String>,
79    pub country_name: Option<String>,
80    pub country_code: Option<String>,
81    pub country_code_iso3: Option<String>,
82    pub country_capital: Option<String>,
83    pub country_tld: Option<String>,
84    pub continent_code: Option<String>,
85    pub in_eu: Option<bool>,
86    pub postal: Option<String>,
87    pub timezone: Option<String>,
88    pub utc_offset: Option<String>,
89    pub country_calling_code: Option<String>,
90    pub currency: Option<String>,
91    pub currency_name: Option<String>,
92    pub languages: Option<String>,
93    pub country_area: Option<f64>,
94    pub country_population: Option<f64>,
95    pub asn: Option<String>,
96    pub org: Option<String>,
97}
98
99#[derive(Default, Debug, Deserialize)]
100#[serde(from = "GeolocatorBackend")]
101pub struct Geolocator {
102    backend: GeolocatorBackend,
103    last_autolocate: Mutex<Option<AutolocateResult>>,
104}
105
106impl Geolocator {
107    pub fn name(&self) -> Cow<'static, str> {
108        self.backend.name()
109    }
110
111    /// No-op if last API call was made in the last `interval` seconds.
112    pub async fn find_ip_location(
113        &self,
114        client: &reqwest::Client,
115        interval: Duration,
116    ) -> Result<IPAddressInfo> {
117        {
118            let guard = self.last_autolocate.lock().unwrap();
119            if let Some(cached) = &*guard {
120                if cached.timestamp.elapsed() < interval {
121                    return Ok(cached.location.clone());
122                }
123            }
124        }
125
126        let location = self.backend.get_info(client).await?;
127
128        {
129            let mut guard = self.last_autolocate.lock().unwrap();
130            *guard = Some(AutolocateResult {
131                location: location.clone(),
132                timestamp: Instant::now(),
133            });
134        }
135
136        Ok(location)
137    }
138}
139
140#[derive(Deserialize, Debug, SmartDefault, Clone)]
141#[serde(tag = "geolocator", rename_all = "lowercase", deny_unknown_fields)]
142pub enum GeolocatorBackend {
143    #[default]
144    Ipapi(ipapi::Config),
145    Ip2Location(ip2location::Config),
146}
147
148impl GeolocatorBackend {
149    fn name(&self) -> Cow<'static, str> {
150        match self {
151            GeolocatorBackend::Ipapi(_) => ipapi::Ipapi.name(),
152            GeolocatorBackend::Ip2Location(_) => ip2location::Ip2Location.name(),
153        }
154    }
155
156    async fn get_info(&self, client: &reqwest::Client) -> Result<IPAddressInfo> {
157        match self {
158            GeolocatorBackend::Ipapi(_) => ipapi::Ipapi.get_info(client).await,
159            GeolocatorBackend::Ip2Location(config) => {
160                ip2location::Ip2Location
161                    .get_info(client, &config.api_key)
162                    .await
163            }
164        }
165    }
166}
167
168impl From<GeolocatorBackend> for Geolocator {
169    fn from(backend: GeolocatorBackend) -> Self {
170        Self {
171            backend,
172            last_autolocate: Mutex::new(None),
173        }
174    }
175}