feat: locale support, and get rough location using nominatim aswell

This commit is contained in:
Soph :3 2025-08-22 15:34:24 +03:00
parent 10cde9881d
commit a7aa030e75

View file

@ -37,6 +37,20 @@ struct Args {
/// Views detectors /// Views detectors
#[arg(short = 'f', long, default_value_t = false)] #[arg(short = 'f', long, default_value_t = false)]
detector_fetch: bool, detector_fetch: bool,
/// Notification languages, comma separated (e.g. "en,lv,ru")
#[arg(long = "lang", value_delimiter = ',', default_value = "en")]
langs: Vec<String>,
/// 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 { 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 r * c
} }
async fn get_location_name(lat: f64, lon: f64) -> Option<String> {
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] #[tokio::main]
async fn main() { async fn main() {
let args = Args::parse(); let args = Args::parse();
@ -67,6 +252,8 @@ async fn main() {
println!(" delta: {}", args.delta); println!(" delta: {}", args.delta);
println!(" ntfy_topic: {}", args.ntfy_topic); println!(" ntfy_topic: {}", args.ntfy_topic);
println!(" detector_fetch: {}", args.detector_fetch); println!(" detector_fetch: {}", args.detector_fetch);
println!(" langs: {:?}", args.langs);
println!(" priority: {}", args.priority);
CryptoProvider::install_default(rustls_rustcrypto::provider()) CryptoProvider::install_default(rustls_rustcrypto::provider())
.expect("install rustcrypto provider"); .expect("install rustcrypto provider");
@ -174,16 +361,28 @@ async fn main() {
let distance_km = let distance_km =
haversine_distance(args.center_lat, args.center_lon, lat, lon); 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!( println!(
"Delay: {}ms, offset to now: {}ms, status: {}, region: {} | \n Strike at: {}, {} | Distance to you: {:.2} 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 all.delay,
offset_ms,
all.status,
all.region,
lon,
lat,
distance_km,
location_name
); );
let mut detector_details: Vec<String> = Vec::new(); let mut detector_details: Vec<String> = Vec::new();
if args.detector_fetch { if args.detector_fetch {
println!("Used detectors:"); 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() { for feature in detector_features.clone().unwrap().features.iter() {
if sig.sta == feature.properties.station as i64 { if sig.sta == feature.properties.station as i64 {
let detail = format!( let detail = format!(
@ -213,51 +412,31 @@ async fn main() {
} }
} }
let message = format!( for lang in &args.langs {
"• Delay: {}ms\n\ let lang = lang.trim().to_lowercase();
Status: {}\n\ if lang != "en" && lang != "lv" && lang != "ru" {
Region: {}\n\ continue;
Location: {:.5}, {:.5}\n\ }
Distance to you: {:.2} km\n\ let (message, title) = get_localized_message(
Used detectors:\n{}\n\ &lang,
Stay safe! 🌩", &all,
all.delay,
all.status,
all.region,
lon, lon,
lat, lat,
distance_km, distance_km,
{ &detector_details,
if args.detector_fetch { args.detector_fetch,
let shown = 2; &location_name,
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()
}
}
); );
let action = get_localized_action(&lang, lon, lat);
let topic = format!("{}-{}", args.ntfy_topic, lang);
let json_payload = serde_json::json!({ let json_payload = serde_json::json!({
"topic": args.ntfy_topic, "topic": topic,
"message": message, "message": message,
"title": "⚡ Lightning Strike Detected", "title": title,
"priority": 4, "priority": args.priority,
"actions": [ "actions": [action]
{
"action": "view",
"label": "View in Google Maps",
"url": format!("geo:{:.5},{:.5}", lon, lat),
"clear": false
}
]
}); });
let client = reqwest::Client::new(); let client = reqwest::Client::new();
@ -268,8 +447,11 @@ async fn main() {
.await; .await;
match res { match res {
Ok(_) => println!("Notification sent to ntfy.sh!"), Ok(_) => println!("Notification sent to ntfy.sh topic {}!", topic),
Err(e) => println!("Failed to send notification: {}", e), Err(e) => {
println!("Failed to send notification to {}: {}", topic, e)
}
}
} }
} else { } else {
print!( print!(