use super::*;
const IP_API_URL: &str = "https://api.ip2location.io/";
pub(super) const API_KEY_ENV: &str = "IP2LOCATION_API_KEY";
#[derive(Deserialize, Debug, Default, Clone)]
#[serde(deny_unknown_fields)]
pub struct Config {
#[serde(default = "getenv_api_key")]
pub api_key: Option<String>,
}
fn getenv_api_key() -> Option<String> {
std::env::var(API_KEY_ENV).ok()
}
#[derive(Deserialize)]
struct ApiResponse {
#[serde(flatten)]
data: Option<Ip2LocationAddressInfo>,
#[serde(default)]
error: Option<ApiError>,
}
#[derive(Deserialize)]
struct Ip2LocationAddressInfo {
ip: String,
country_code: String,
country_name: String,
region_name: String,
city_name: String,
latitude: f64,
longitude: f64,
zip_code: String,
time_zone: String,
asn: String,
continent: Option<Continent>,
country: Option<Country>,
region: Option<Region>,
time_zone_info: Option<TimeZoneInfo>,
}
#[derive(Deserialize)]
struct Continent {
code: String,
}
#[derive(Deserialize)]
struct Country {
alpha3_code: String,
capital: String,
total_area: f64,
population: f64,
currency: Currency,
language: Language,
tld: String,
}
#[derive(Deserialize)]
struct Currency {
name: String,
code: String,
}
#[derive(Deserialize)]
struct Language {
code: String,
}
#[derive(Deserialize)]
struct Region {
code: String,
}
#[derive(Deserialize)]
struct TimeZoneInfo {
olson: String,
}
impl From<Ip2LocationAddressInfo> for IPAddressInfo {
fn from(val: Ip2LocationAddressInfo) -> Self {
let mut info = IPAddressInfo {
ip: val.ip,
city: val.city_name,
latitude: val.latitude,
longitude: val.longitude,
region: Some(val.region_name),
country: Some(val.country_code.clone()),
country_name: Some(val.country_name),
country_code: Some(val.country_code),
postal: Some(val.zip_code),
utc_offset: Some(val.time_zone),
asn: Some(val.asn),
..Default::default()
};
if let Some(region) = val.region {
info.region_code = Some(region.code);
}
if let Some(country) = val.country {
info.country_area = Some(country.total_area);
info.country_population = Some(country.population);
info.currency = Some(country.currency.code);
info.currency_name = Some(country.currency.name);
info.languages = Some(country.language.code);
info.country_tld = Some(country.tld);
info.country_capital = Some(country.capital);
info.country_code_iso3 = Some(country.alpha3_code);
}
if let Some(continent) = val.continent {
info.continent_code = Some(continent.code);
}
if let Some(time_zone_info) = val.time_zone_info {
info.timezone = Some(time_zone_info.olson);
}
info
}
}
#[derive(Deserialize, Default, Debug)]
struct ApiError {
error_code: u32,
error_message: String,
}
impl fmt::Display for ApiError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_fmt(format_args!(
"Error {}: {}",
self.error_code, self.error_message
))
}
}
impl StdError for ApiError {}
pub struct Ip2Location;
impl Ip2Location {
pub fn name(&self) -> Cow<'static, str> {
Cow::Borrowed("ip2location.io")
}
pub async fn get_info(
&self,
client: &reqwest::Client,
api_key: Option<&String>,
) -> Result<IPAddressInfo> {
let mut request_builder = client.get(IP_API_URL);
if let Some(api_key) = api_key {
request_builder = request_builder.query(&[("key", api_key)]);
}
let response: ApiResponse = request_builder
.send()
.await
.error("Failed during request for current location")?
.json()
.await
.error("Failed while parsing location API result")?;
if let Some(error) = response.error {
Err(Error {
message: Some("ip2location.io error".into()),
cause: Some(Arc::new(error)),
})
} else {
Ok(response
.data
.error("Failed while parsing location API result")?
.into())
}
}
}