ramadhan-cli-rust 0.1.0

Ramadan-first CLI for Sehar and Iftar timings in your terminal
Documentation
use reqwest::blocking::Client;
use serde::Deserialize;

#[derive(Debug, Clone, Deserialize)]
pub struct GeoLocation {
    pub city: String,
    pub country: String,
    pub latitude: f64,
    pub longitude: f64,
    pub timezone: String,
}

#[derive(Debug, Clone, Deserialize)]
pub struct CityCountryGuess {
    pub city: String,
    pub country: String,
    pub latitude: f64,
    pub longitude: f64,
    pub timezone: Option<String>,
}

#[derive(Debug, Deserialize)]
struct IpApiResponse {
    city: String,
    country: String,
    lat: f64,
    lon: f64,
    timezone: Option<String>,
}

#[derive(Debug, Deserialize)]
struct IpapiCoResponse {
    city: String,
    country_name: String,
    latitude: f64,
    longitude: f64,
    timezone: Option<String>,
}

#[derive(Debug, Deserialize)]
struct IpWhoisTimezone {
    id: Option<String>,
}

#[derive(Debug, Deserialize)]
struct IpWhoisResponse {
    success: bool,
    city: String,
    country: String,
    latitude: f64,
    longitude: f64,
    timezone: Option<IpWhoisTimezone>,
}

#[derive(Debug, Deserialize)]
struct OpenMeteoSearchResponse {
    results: Option<Vec<OpenMeteoResult>>,
}

#[derive(Debug, Deserialize)]
struct OpenMeteoResult {
    name: String,
    country: String,
    latitude: f64,
    longitude: f64,
    timezone: Option<String>,
}

fn try_ip_api(client: &Client) -> Option<GeoLocation> {
    let response = client
        .get("http://ip-api.com/json/?fields=city,country,lat,lon,timezone")
        .send()
        .ok()?
        .json::<IpApiResponse>()
        .ok()?;

    Some(GeoLocation {
        city: response.city,
        country: response.country,
        latitude: response.lat,
        longitude: response.lon,
        timezone: response.timezone.unwrap_or_default(),
    })
}

fn try_ipapi_co(client: &Client) -> Option<GeoLocation> {
    let response = client
        .get("https://ipapi.co/json/")
        .send()
        .ok()?
        .json::<IpapiCoResponse>()
        .ok()?;

    Some(GeoLocation {
        city: response.city,
        country: response.country_name,
        latitude: response.latitude,
        longitude: response.longitude,
        timezone: response.timezone.unwrap_or_default(),
    })
}

fn try_ip_whois(client: &Client) -> Option<GeoLocation> {
    let response = client
        .get("https://ipwho.is/")
        .send()
        .ok()?
        .json::<IpWhoisResponse>()
        .ok()?;

    if !response.success {
        return None;
    }

    Some(GeoLocation {
        city: response.city,
        country: response.country,
        latitude: response.latitude,
        longitude: response.longitude,
        timezone: response.timezone.and_then(|tz| tz.id).unwrap_or_default(),
    })
}

pub fn guess_location(client: &Client) -> Option<GeoLocation> {
    try_ip_api(client)
        .or_else(|| try_ipapi_co(client))
        .or_else(|| try_ip_whois(client))
}

pub fn guess_city_country(client: &Client, query: &str) -> Option<CityCountryGuess> {
    let trimmed = query.trim();
    if trimmed.is_empty() {
        return None;
    }

    let response = client
        .get("https://geocoding-api.open-meteo.com/v1/search")
        .query(&[
            ("name", trimmed),
            ("count", "1"),
            ("language", "en"),
            ("format", "json"),
        ])
        .send()
        .ok()?
        .json::<OpenMeteoSearchResponse>()
        .ok()?;

    let result = response.results?.into_iter().next()?;

    Some(CityCountryGuess {
        city: result.name,
        country: result.country,
        latitude: result.latitude,
        longitude: result.longitude,
        timezone: result.timezone,
    })
}