use forge_core::ForgeError;
use std::net::IpAddr;
use std::path::Path;
use std::sync::Arc;
#[derive(Debug, Clone, Default)]
pub struct GeoInfo {
pub country: Option<String>,
pub city: Option<String>,
}
enum Backend {
Embedded(db_ip::DbIpDatabase<db_ip::CountryCode>),
Mmdb(maxminddb::Reader<Vec<u8>>),
}
#[derive(Clone)]
pub struct GeoIpResolver {
backend: Arc<Backend>,
}
impl Default for GeoIpResolver {
fn default() -> Self {
Self::new()
}
}
impl GeoIpResolver {
pub fn new() -> Self {
Self {
backend: Arc::new(Backend::Embedded(db_ip::include_country_code_database!())),
}
}
pub fn from_mmdb(path: &Path) -> Result<Self, ForgeError> {
let reader = maxminddb::Reader::open_readfile(path).map_err(|e| {
ForgeError::Config(format!(
"failed to load GeoIP database {}: {e}",
path.display()
))
})?;
Ok(Self {
backend: Arc::new(Backend::Mmdb(reader)),
})
}
pub fn lookup(&self, ip_str: &str) -> GeoInfo {
let ip: IpAddr = match ip_str.parse() {
Ok(ip) => ip,
Err(_) => return GeoInfo::default(),
};
match self.backend.as_ref() {
Backend::Embedded(db) => GeoInfo {
country: db.get(&ip).map(|c| c.as_str().to_string()),
city: None,
},
Backend::Mmdb(reader) => {
let result: Result<maxminddb::geoip2::City, _> = reader.lookup(ip);
match result {
Ok(record) => GeoInfo {
country: record
.country
.and_then(|c| c.iso_code)
.map(|s| s.to_string()),
city: record
.city
.and_then(|c| c.names)
.and_then(|n| n.get("en").copied())
.map(|s| s.to_string()),
},
Err(_) => GeoInfo::default(),
}
}
}
}
}