From a7aa030e75e292aca8283c3c76581d7977a9ae3d Mon Sep 17 00:00:00 2001 From: yourfriendoss Date: Fri, 22 Aug 2025 15:34:24 +0300 Subject: [PATCH] feat: locale support, and get rough location using nominatim aswell --- src/main.rs | 298 ++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 240 insertions(+), 58 deletions(-) diff --git a/src/main.rs b/src/main.rs index 81500bd..98a4ab4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -37,6 +37,20 @@ struct Args { /// Views detectors #[arg(short = 'f', long, default_value_t = false)] detector_fetch: bool, + + /// Notification languages, comma separated (e.g. "en,lv,ru") + #[arg(long = "lang", value_delimiter = ',', default_value = "en")] + langs: Vec, + + /// Priority in which the Ntfy notifications are sent. + /// Possible values: + /// 5 - max/urgent: Really long vibration bursts, default notification sound with a pop-over notification. + /// 4 - high: Long vibration burst, default notification sound with a pop-over notification. + /// 3 - default: Short default vibration and sound. Default notification behavior. + /// 2 - low: No vibration or sound. Notification will not visibly show up until notification drawer is pulled down. + /// 1 - min: No vibration or sound. The notification will be under the fold in "Other notifications". + #[arg(short = 'p', long, default_value_t = 3)] + priority: i32, } fn in_bounds(lat: f64, lon: f64, center_lat: f64, center_lon: f64, delta: f64) -> bool { @@ -58,6 +72,177 @@ fn haversine_distance(lat1: f64, lon1: f64, lat2: f64, lon2: f64) -> f64 { r * c } +async fn get_location_name(lat: f64, lon: f64) -> Option { + let url = format!( + "https://nominatim.openstreetmap.org/reverse?format=jsonv2&lat={:.5}&lon={:.5}", + lat, lon + ); + let client = reqwest::Client::new(); + let res = client + .get(&url) + .header("User-Agent", "blitzortung-lightning-alert/1.0") + .send() + .await; + + match res { + Ok(resp) => { + let json: serde_json::Value = + match serde_json::from_str(&resp.text().await.unwrap_or_default()) { + Ok(j) => j, + Err(_) => return None, + }; + // Try to get display_name, fallback to address if available + if let Some(name) = json.get("display_name").and_then(|v| v.as_str()) { + Some(name.to_string()) + } else if let Some(addr) = json.get("address") { + Some(addr.to_string()) + } else { + None + } + } + Err(_) => None, + } +} + +fn get_localized_message( + lang: &str, + all: &TungMessage, + lon: f64, + lat: f64, + distance_km: f64, + detector_details: &[String], + detector_fetch: bool, + location_name: &str, +) -> (String, String) { + match lang { + "lv" => { + let detectors_str = if detector_fetch { + let shown = 2; + let total = detector_details.len(); + let mut lines = Vec::new(); + for d in detector_details.iter().take(shown) { + lines.push(format!(" - {}", d)); + } + if total > shown { + lines.push(format!(" (+{} pārējie..)", total - shown)); + } + lines.join("\n") + } else { + "".to_string() + }; + ( + format!( + "• Kavēšanās: {}ms\n\ + • Statuss: {}\n\ + • Reģions: {}\n\ + • Atrašanās vieta: {:.5}, {:.5}\n\ + • Aptuvenā vieta: {}\n\ + • Attālums līdz jums: {:.2} km\n\ + • Izmantotie detektori:\n{}\n\ + Esiet drošībā! 🌩️", + all.delay, + all.status, + all.region, + lon, + lat, + location_name, + distance_km, + detectors_str + ), + "⚡ Zibens trieciens konstatēts".to_string(), + ) + } + "ru" => { + let detectors_str = if detector_fetch { + let shown = 2; + let total = detector_details.len(); + let mut lines = Vec::new(); + for d in detector_details.iter().take(shown) { + lines.push(format!(" - {}", d)); + } + if total > shown { + lines.push(format!(" (+{} других..)", total - shown)); + } + lines.join("\n") + } else { + "".to_string() + }; + ( + format!( + "• Задержка: {}мс\n\ + • Статус: {}\n\ + • Регион: {}\n\ + • Местоположение: {:.5}, {:.5}\n\ + • Примерное место: {}\n\ + • Расстояние до вас: {:.2} км\n\ + • Использованные детекторы:\n{}\n\ + Будьте осторожны! 🌩️", + all.delay, + all.status, + all.region, + lon, + lat, + location_name, + distance_km, + detectors_str + ), + "⚡ Обнаружен удар молнии".to_string(), + ) + } + _ => { + let detectors_str = if detector_fetch { + let shown = 2; + let total = detector_details.len(); + let mut lines = Vec::new(); + for d in detector_details.iter().take(shown) { + lines.push(format!(" - {}", d)); + } + if total > shown { + lines.push(format!(" (+{} others..)", total - shown)); + } + lines.join("\n") + } else { + "".to_string() + }; + ( + format!( + "• Delay: {}ms\n\ + • Status: {}\n\ + • Region: {}\n\ + • Location: {:.5}, {:.5}\n\ + • Approximate place: {}\n\ + • Distance to you: {:.2} km\n\ + • Used detectors:\n{}\n\ + Stay safe! 🌩️", + all.delay, + all.status, + all.region, + lon, + lat, + location_name, + distance_km, + detectors_str + ), + "⚡ Lightning Strike Detected".to_string(), + ) + } + } +} + +fn get_localized_action(lang: &str, lon: f64, lat: f64) -> serde_json::Value { + let label = match lang { + "lv" => "Skatīt Google Maps", + "ru" => "Открыть в Google Maps", + _ => "View in Google Maps", + }; + serde_json::json!({ + "action": "view", + "label": label, + "url": format!("geo:{:.5},{:.5}", lon, lat), + "clear": false + }) +} + #[tokio::main] async fn main() { let args = Args::parse(); @@ -67,6 +252,8 @@ async fn main() { println!(" delta: {}", args.delta); println!(" ntfy_topic: {}", args.ntfy_topic); println!(" detector_fetch: {}", args.detector_fetch); + println!(" langs: {:?}", args.langs); + println!(" priority: {}", args.priority); CryptoProvider::install_default(rustls_rustcrypto::provider()) .expect("install rustcrypto provider"); @@ -174,16 +361,28 @@ async fn main() { let distance_km = haversine_distance(args.center_lat, args.center_lon, lat, lon); + let location_name = match get_location_name(lat, lon).await { + Some(name) => name, + None => "Unknown".to_string(), + }; + println!( - "Delay: {}ms, offset to now: {}ms, status: {}, region: {} | \n Strike at: {}, {} | Distance to you: {:.2} km", - all.delay, offset_ms, all.status, all.region, lon, lat, distance_km + "Delay: {}ms, offset to now: {}ms, status: {}, region: {} | \n Strike at: {}, {} | Distance to you: {:.2} km | Location: {}", + all.delay, + offset_ms, + all.status, + all.region, + lon, + lat, + distance_km, + location_name ); let mut detector_details: Vec = Vec::new(); if args.detector_fetch { println!("Used detectors:"); - for sig in all.sig.into_iter() { + for sig in all.sig.clone().into_iter() { for feature in detector_features.clone().unwrap().features.iter() { if sig.sta == feature.properties.station as i64 { let detail = format!( @@ -213,63 +412,46 @@ async fn main() { } } - let message = format!( - "• Delay: {}ms\n\ - • Status: {}\n\ - • Region: {}\n\ - • Location: {:.5}, {:.5}\n\ - • Distance to you: {:.2} km\n\ - • Used detectors:\n{}\n\ - Stay safe! 🌩️", - all.delay, - all.status, - all.region, - lon, - lat, - distance_km, - { - if args.detector_fetch { - let shown = 2; - let total = detector_details.len(); - let mut lines = Vec::new(); - for d in detector_details.iter().take(shown) { - lines.push(format!(" - {}", d)); - } - if total > shown { - lines.push(format!(" (+{} others..)", total - shown)); - } - lines.join("\n") - } else { - "".to_string() + for lang in &args.langs { + let lang = lang.trim().to_lowercase(); + if lang != "en" && lang != "lv" && lang != "ru" { + continue; + } + let (message, title) = get_localized_message( + &lang, + &all, + lon, + lat, + distance_km, + &detector_details, + args.detector_fetch, + &location_name, + ); + let action = get_localized_action(&lang, lon, lat); + + let topic = format!("{}-{}", args.ntfy_topic, lang); + + let json_payload = serde_json::json!({ + "topic": topic, + "message": message, + "title": title, + "priority": args.priority, + "actions": [action] + }); + + let client = reqwest::Client::new(); + let res = client + .post("https://ntfy.sh/") + .body(json_payload.to_string()) + .send() + .await; + + match res { + Ok(_) => println!("Notification sent to ntfy.sh topic {}!", topic), + Err(e) => { + println!("Failed to send notification to {}: {}", topic, e) } } - ); - - let json_payload = serde_json::json!({ - "topic": args.ntfy_topic, - "message": message, - "title": "⚡ Lightning Strike Detected", - "priority": 4, - "actions": [ - { - "action": "view", - "label": "View in Google Maps", - "url": format!("geo:{:.5},{:.5}", lon, lat), - "clear": false - } - ] - }); - - let client = reqwest::Client::new(); - let res = client - .post("https://ntfy.sh/") - .body(json_payload.to_string()) - .send() - .await; - - match res { - Ok(_) => println!("Notification sent to ntfy.sh!"), - Err(e) => println!("Failed to send notification: {}", e), } } else { print!(