#![deny(missing_docs)]
use hyper::body::HttpBody;
use hyper::{Body, Client, Method, Request, Response};
use serde::{Deserialize, Serialize};
use serde_json::json;
#[derive(Clone, Debug)]
pub enum IpApiError {
InvalidQuery,
PrivateRange,
RateLimit(u8),
ReservedRange,
UnexpectedError(Option<String>),
}
#[derive(Clone, Debug)]
pub enum IpApiLanguage {
De,
En,
Es,
Fr,
Ja,
PtBr,
Ru,
ZhCn,
}
#[derive(Deserialize)]
struct IpApiMessage {
message: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct IpData {
pub continent: Option<String>,
pub continent_code: Option<String>,
pub country: Option<String>,
pub country_code: Option<String>,
pub region: Option<String>,
pub region_name: Option<String>,
pub city: Option<String>,
pub district: Option<String>,
pub zip: Option<String>,
pub lat: Option<f32>,
pub lon: Option<f32>,
pub timezone: Option<String>,
pub offset: Option<i32>,
pub currency: Option<String>,
pub isp: Option<String>,
pub org: Option<String>,
#[serde(rename = "as")]
pub as_field: Option<String>,
pub asname: Option<String>,
pub reverse: Option<String>,
pub mobile: Option<bool>,
pub proxy: Option<bool>,
pub hosting: Option<bool>,
pub query: Option<String>,
}
#[repr(u32)]
enum IpDataField {
Message = 1 << 15,
Continent = 1 << 20,
ContinentCode = 1 << 21,
Country = 1 << 0,
CountryCode = 1 << 1,
Region = 1 << 2,
RegionName = 1 << 3,
City = 1 << 4,
District = 1 << 19,
Zip = 1 << 5,
Lat = 1 << 6,
Lon = 1 << 7,
Timezone = 1 << 8,
Offset = 1 << 25,
Currency = 1 << 23,
Isp = 1 << 9,
Org = 1 << 10,
AsField = 1 << 11,
Asname = 1 << 22,
Reverse = 1 << 12,
Mobile = 1 << 16,
Proxy = 1 << 17,
Hosting = 1 << 24,
Query = 1 << 13,
}
#[derive(Clone, Debug)]
pub struct IpApiConfig {
numeric_field: u32,
language: IpApiLanguage,
}
impl IpApiConfig {
fn build_uri(
resource: &str,
target: Option<&str>,
fields: u32,
language: IpApiLanguage,
) -> String {
format!(
"http://ip-api.com/{}/{}?fields={}{}",
resource,
target.unwrap_or(""),
fields,
match language {
IpApiLanguage::De => "&lang=de",
IpApiLanguage::En => "",
IpApiLanguage::Es => "&lang=es",
IpApiLanguage::Fr => "&lang=fr",
IpApiLanguage::Ja => "&lang=ja",
IpApiLanguage::PtBr => "&lang=pt-BR",
IpApiLanguage::Ru => "&lang=ru",
IpApiLanguage::ZhCn => "&lang=zh-CN",
}
)
}
fn check_response(response: &Response<Body>) -> Result<(), IpApiError> {
if response.status() == 429 {
let Some(header) = response.headers().get("X-Ttl") else {
return Err(IpApiError::UnexpectedError(Some(
"Failed to get `X-Ttl` header from the response".into(),
)));
};
let Ok(header) = header.to_str() else {
return Err(IpApiError::UnexpectedError(Some(
"Failed to convert `X-Ttl` header from the response to &str".into(),
)));
};
let Ok(header) = header.parse() else {
return Err(IpApiError::UnexpectedError(Some(
"Failed to parse `X-Ttl` header from the response".into(),
)));
};
return Err(IpApiError::RateLimit(header));
}
Ok(())
}
fn check_error_message(message: Option<String>) -> Result<(), IpApiError> {
if let Some(message) = message {
return match message.as_str() {
"invalid query" => Err(IpApiError::InvalidQuery),
"private range" => Err(IpApiError::PrivateRange),
"reserved range" => Err(IpApiError::ReservedRange),
message => Err(IpApiError::UnexpectedError(Some(message.into()))),
};
}
Ok(())
}
async fn parse_response_body(response: &mut Response<Body>) -> Result<String, IpApiError> {
let Some(body) = response.body_mut().data().await else {
return Err(IpApiError::UnexpectedError(Some(
"Response is empty".into(),
)));
};
let Ok(body) = body else {
return Err(IpApiError::UnexpectedError(Some(
"Failed to retrieve body from the response".into(),
)));
};
let Ok(body) = String::from_utf8(body.to_vec()) else {
return Err(IpApiError::UnexpectedError(Some(
"Failed to convert body from the response to String".into(),
)));
};
Ok(body)
}
pub async fn make_request(self, target: &str) -> Result<IpData, IpApiError> {
let uri = Self::build_uri("json", Some(target), self.numeric_field, self.language);
let client = Client::new();
let Ok(uri) = uri.parse() else {
return Err(IpApiError::UnexpectedError(Some(
"Failed to parse request URI".into(),
)));
};
let Ok(response) = &mut client.get(uri).await else {
return Err(IpApiError::UnexpectedError(Some(
"Failed to make a request".into(),
)));
};
Self::check_response(response)?;
let body = Self::parse_response_body(response).await?;
let Ok(ip_data): Result<IpApiMessage, _> = serde_json::from_str(body.as_str()) else {
return Err(IpApiError::UnexpectedError(Some(
"Failed to parse body from the response".into(),
)));
};
Self::check_error_message(ip_data.message)?;
let Ok(ip_data): Result<IpData, _> = serde_json::from_str(body.as_str()) else {
return Err(IpApiError::UnexpectedError(Some(
"Failed to parse body from the response".into(),
)));
};
Ok(ip_data)
}
pub async fn make_batch_request(self, targets: Vec<&str>) -> Result<Vec<IpData>, IpApiError> {
let uri = Self::build_uri("batch", None, self.numeric_field, self.language);
let Ok(request) = Request::builder()
.method(Method::POST)
.uri(uri)
.header("content-type", "application/json")
.body(Body::from(json!(targets).to_string()))
else {
return Err(IpApiError::UnexpectedError(Some(
"Failed to build a request".into(),
)));
};
let client = Client::new();
let Ok(response) = &mut client.request(request).await else {
return Err(IpApiError::UnexpectedError(Some(
"Failed to make a request".into(),
)));
};
Self::check_response(response)?;
let body = Self::parse_response_body(response).await?;
let Ok(ip_batch_data): Result<Vec<IpApiMessage>, _> = serde_json::from_str(body.as_str())
else {
return Err(IpApiError::UnexpectedError(Some(
"Failed to parse body from the response".into(),
)));
};
for ip_data in ip_batch_data {
Self::check_error_message(ip_data.message)?;
}
let Ok(ip_batch_data): Result<Vec<IpData>, _> = serde_json::from_str(body.as_str()) else {
return Err(IpApiError::UnexpectedError(Some(
"Failed to parse body from the response".into(),
)));
};
Ok(ip_batch_data)
}
pub fn include_continent(mut self) -> Self {
self.numeric_field |= IpDataField::Continent as u32;
self
}
pub fn include_continent_code(mut self) -> Self {
self.numeric_field |= IpDataField::ContinentCode as u32;
self
}
pub fn include_country(mut self) -> Self {
self.numeric_field |= IpDataField::Country as u32;
self
}
pub fn include_country_code(mut self) -> Self {
self.numeric_field |= IpDataField::CountryCode as u32;
self
}
pub fn include_region(mut self) -> Self {
self.numeric_field |= IpDataField::Region as u32;
self
}
pub fn include_region_name(mut self) -> Self {
self.numeric_field |= IpDataField::RegionName as u32;
self
}
pub fn include_city(mut self) -> Self {
self.numeric_field |= IpDataField::City as u32;
self
}
pub fn include_district(mut self) -> Self {
self.numeric_field |= IpDataField::District as u32;
self
}
pub fn include_zip(mut self) -> Self {
self.numeric_field |= IpDataField::Zip as u32;
self
}
pub fn include_lat(mut self) -> Self {
self.numeric_field |= IpDataField::Lat as u32;
self
}
pub fn include_lon(mut self) -> Self {
self.numeric_field |= IpDataField::Lon as u32;
self
}
pub fn include_timezone(mut self) -> Self {
self.numeric_field |= IpDataField::Timezone as u32;
self
}
pub fn include_offset(mut self) -> Self {
self.numeric_field |= IpDataField::Offset as u32;
self
}
pub fn include_currency(mut self) -> Self {
self.numeric_field |= IpDataField::Currency as u32;
self
}
pub fn include_isp(mut self) -> Self {
self.numeric_field |= IpDataField::Isp as u32;
self
}
pub fn include_org(mut self) -> Self {
self.numeric_field |= IpDataField::Org as u32;
self
}
pub fn include_as_field(mut self) -> Self {
self.numeric_field |= IpDataField::AsField as u32;
self
}
pub fn include_asname(mut self) -> Self {
self.numeric_field |= IpDataField::Asname as u32;
self
}
pub fn include_reverse(mut self) -> Self {
self.numeric_field |= IpDataField::Reverse as u32;
self
}
pub fn include_mobile(mut self) -> Self {
self.numeric_field |= IpDataField::Mobile as u32;
self
}
pub fn include_proxy(mut self) -> Self {
self.numeric_field |= IpDataField::Proxy as u32;
self
}
pub fn include_hosting(mut self) -> Self {
self.numeric_field |= IpDataField::Hosting as u32;
self
}
pub fn include_query(mut self) -> Self {
self.numeric_field |= IpDataField::Query as u32;
self
}
pub fn exclude_continent(mut self) -> Self {
self.numeric_field &= !(IpDataField::Continent as u32);
self
}
pub fn exclude_continent_code(mut self) -> Self {
self.numeric_field &= !(IpDataField::ContinentCode as u32);
self
}
pub fn exclude_country(mut self) -> Self {
self.numeric_field &= !(IpDataField::Country as u32);
self
}
pub fn exclude_country_code(mut self) -> Self {
self.numeric_field &= !(IpDataField::CountryCode as u32);
self
}
pub fn exclude_region(mut self) -> Self {
self.numeric_field &= !(IpDataField::Region as u32);
self
}
pub fn exclude_region_name(mut self) -> Self {
self.numeric_field &= !(IpDataField::RegionName as u32);
self
}
pub fn exclude_city(mut self) -> Self {
self.numeric_field &= !(IpDataField::City as u32);
self
}
pub fn exclude_district(mut self) -> Self {
self.numeric_field &= !(IpDataField::District as u32);
self
}
pub fn exclude_zip(mut self) -> Self {
self.numeric_field &= !(IpDataField::Zip as u32);
self
}
pub fn exclude_lat(mut self) -> Self {
self.numeric_field &= !(IpDataField::Lat as u32);
self
}
pub fn exclude_lon(mut self) -> Self {
self.numeric_field &= !(IpDataField::Lon as u32);
self
}
pub fn exclude_timezone(mut self) -> Self {
self.numeric_field &= !(IpDataField::Timezone as u32);
self
}
pub fn exclude_offset(mut self) -> Self {
self.numeric_field &= !(IpDataField::Offset as u32);
self
}
pub fn exclude_currency(mut self) -> Self {
self.numeric_field &= !(IpDataField::Currency as u32);
self
}
pub fn exclude_isp(mut self) -> Self {
self.numeric_field &= !(IpDataField::Isp as u32);
self
}
pub fn exclude_org(mut self) -> Self {
self.numeric_field &= !(IpDataField::Org as u32);
self
}
pub fn exclude_as_field(mut self) -> Self {
self.numeric_field &= !(IpDataField::AsField as u32);
self
}
pub fn exclude_asname(mut self) -> Self {
self.numeric_field &= !(IpDataField::Asname as u32);
self
}
pub fn exclude_reverse(mut self) -> Self {
self.numeric_field &= !(IpDataField::Reverse as u32);
self
}
pub fn exclude_mobile(mut self) -> Self {
self.numeric_field &= !(IpDataField::Mobile as u32);
self
}
pub fn exclude_proxy(mut self) -> Self {
self.numeric_field &= !(IpDataField::Proxy as u32);
self
}
pub fn exclude_hosting(mut self) -> Self {
self.numeric_field &= !(IpDataField::Hosting as u32);
self
}
pub fn exclude_query(mut self) -> Self {
self.numeric_field &= !(IpDataField::Query as u32);
self
}
pub fn set_language(mut self, language: IpApiLanguage) -> Self {
self.language = language;
self
}
}
pub const fn generate_empty_config() -> IpApiConfig {
IpApiConfig {
numeric_field: IpDataField::Message as u32,
language: IpApiLanguage::En,
}
}
pub const fn generate_minimum_config() -> IpApiConfig {
IpApiConfig {
numeric_field: IpDataField::Message as u32
| IpDataField::CountryCode as u32
| IpDataField::City as u32
| IpDataField::Timezone as u32
| IpDataField::Offset as u32
| IpDataField::Currency as u32
| IpDataField::Isp as u32,
language: IpApiLanguage::En,
}
}
pub const fn generate_maximum_config() -> IpApiConfig {
IpApiConfig {
numeric_field: IpDataField::Message as u32
| IpDataField::Continent as u32
| IpDataField::ContinentCode as u32
| IpDataField::Country as u32
| IpDataField::CountryCode as u32
| IpDataField::Region as u32
| IpDataField::RegionName as u32
| IpDataField::City as u32
| IpDataField::District as u32
| IpDataField::Zip as u32
| IpDataField::Lat as u32
| IpDataField::Lon as u32
| IpDataField::Timezone as u32
| IpDataField::Offset as u32
| IpDataField::Currency as u32
| IpDataField::Isp as u32
| IpDataField::Org as u32
| IpDataField::AsField as u32
| IpDataField::Asname as u32
| IpDataField::Reverse as u32
| IpDataField::Mobile as u32
| IpDataField::Proxy as u32
| IpDataField::Hosting as u32
| IpDataField::Query as u32,
language: IpApiLanguage::En,
}
}