From 1c2873b3059f3e4d6bd02307ec5b22f761ce1c80 Mon Sep 17 00:00:00 2001 From: Dawid Rycerz Date: Mon, 14 Jul 2025 20:35:00 +0300 Subject: feat: Update routes and fix issues --- src/weather_poller.rs | 177 +++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 138 insertions(+), 39 deletions(-) (limited to 'src/weather_poller.rs') diff --git a/src/weather_poller.rs b/src/weather_poller.rs index 056cef8..4fd418d 100644 --- a/src/weather_poller.rs +++ b/src/weather_poller.rs @@ -1,13 +1,13 @@ -use crate::users::UserRepository; use crate::locations::LocationRepository; -use crate::weather_thresholds::{WeatherThresholdRepository, WeatherThreshold}; -use crate::notifications::{NtfySettingsRepository, SmtpSettingsRepository, NtfySettings, SmtpSettings}; -use serde_json::Value; -use tera::{Tera, Context}; +use crate::notifications::{ + NtfySettings, NtfySettingsRepository, SmtpSettings, SmtpSettingsRepository, +}; +use crate::users::UserRepository; +use crate::weather_thresholds::{WeatherThreshold, WeatherThresholdRepository}; use reqwest::Client; +use serde_json::Value; use std::sync::{Arc, Mutex}; -use tokio_task_scheduler::{Scheduler, Task}; -use tokio::time::Duration; +use tera::{Context, Tera}; const OWM_API_URL: &str = "https://api.openweathermap.org/data/2.5/forecast"; const ALERT_WINDOW_HOURS: i64 = 24; @@ -24,8 +24,15 @@ impl WeatherPoller { let loc_repo = LocationRepository { db: &self.db }; let users = user_repo.list_users().await.unwrap_or_default(); for user in users { - if let Some(location) = loc_repo.list_locations().await.unwrap_or_default().into_iter().find(|l| l.user_id == user.id) { - self.check_user_weather(user.id, location.latitude, location.longitude).await; + if let Some(location) = loc_repo + .list_locations() + .await + .unwrap_or_default() + .into_iter() + .find(|l| l.user_id == user.id) + { + self.check_user_weather(user.id, location.latitude, location.longitude) + .await; } } } @@ -33,16 +40,27 @@ impl WeatherPoller { pub async fn check_user_weather(&self, user_id: i64, lat: f64, lon: f64) { if let Ok(Some(forecast)) = self.fetch_forecast(lat, lon).await { let threshold_repo = WeatherThresholdRepository { db: &self.db }; - let thresholds = threshold_repo.list_thresholds(user_id).await.unwrap_or_default().into_iter().filter(|t| t.enabled).collect::>(); + let thresholds = threshold_repo + .list_thresholds(user_id) + .await + .unwrap_or_default() + .into_iter() + .filter(|t| t.enabled) + .collect::>(); if let Some(entry) = find_first_alert_entry(&forecast, &thresholds) { self.send_notifications(user_id, &entry).await; } } } - pub async fn fetch_forecast(&self, lat: f64, lon: f64) -> Result>, reqwest::Error> { + pub async fn fetch_forecast( + &self, + lat: f64, + lon: f64, + ) -> Result>, reqwest::Error> { let client = Client::new(); - let resp = client.get(OWM_API_URL) + let resp = client + .get(OWM_API_URL) .query(&[ ("lat", lat.to_string()), ("lon", lon.to_string()), @@ -73,7 +91,7 @@ impl WeatherPoller { } fn find_first_alert_entry(forecast: &[Value], thresholds: &[WeatherThreshold]) -> Option { - use chrono::{Utc, TimeZone}; + use chrono::{TimeZone, Utc}; let now = Utc::now(); for entry in forecast { if let Some(ts) = entry["dt"].as_i64() { @@ -91,9 +109,21 @@ fn find_first_alert_entry(forecast: &[Value], thresholds: &[WeatherThreshold]) - fn threshold_triggered(threshold: &WeatherThreshold, entry: &Value) -> bool { let value = match threshold.condition_type.as_str() { - "wind_speed" => entry.pointer("/wind/speed").and_then(|v| v.as_f64()).unwrap_or(0.0) * 3.6, - "rain" => entry.pointer("/rain/3h").and_then(|v| v.as_f64()).unwrap_or(0.0), - "temp_min" | "temp_max" => entry.pointer("/main/temp").and_then(|v| v.as_f64()).unwrap_or(0.0), + "wind_speed" => { + entry + .pointer("/wind/speed") + .and_then(|v| v.as_f64()) + .unwrap_or(0.0) + * 3.6 + } + "rain" => entry + .pointer("/rain/3h") + .and_then(|v| v.as_f64()) + .unwrap_or(0.0), + "temp_min" | "temp_max" => entry + .pointer("/main/temp") + .and_then(|v| v.as_f64()) + .unwrap_or(0.0), _ => return false, }; compare(value, &threshold.operator, threshold.threshold_value) @@ -105,28 +135,35 @@ fn compare(value: f64, op: &str, threshold: f64) -> bool { ">=" => value >= threshold, "<" => value < threshold, "<=" => value <= threshold, - "==" => (value - threshold).abs() < std::f64::EPSILON, + "==" => (value - threshold).abs() < f64::EPSILON, _ => false, } } -async fn send_ntfy_notification(ntfy: &NtfySettings, weather_entry: &Value, tera: Arc>) { +async fn send_ntfy_notification( + ntfy: &NtfySettings, + weather_entry: &Value, + tera: Arc>, +) { let mut ctx = Context::new(); add_weather_context(&mut ctx, weather_entry); let title = if let Some(tpl) = &ntfy.title_template { let mut tera = tera.lock().unwrap(); - tera.render_str(tpl, &ctx).unwrap_or_else(|_| "🚨 Weather Alert".to_string()) + tera.render_str(tpl, &ctx) + .unwrap_or_else(|_| "🚨 Weather Alert".to_string()) } else { "🚨 Weather Alert".to_string() }; let message = if let Some(tpl) = &ntfy.message_template { let mut tera = tera.lock().unwrap(); - tera.render_str(tpl, &ctx).unwrap_or_else(|_| "🚨 Weather alert for your location".to_string()) + tera.render_str(tpl, &ctx) + .unwrap_or_else(|_| "🚨 Weather alert for your location".to_string()) } else { default_weather_message(weather_entry) }; let client = Client::new(); - let _ = client.post(&format!("{}/{}", ntfy.server_url, ntfy.topic)) + let _ = client + .post(&format!("{}/{}", ntfy.server_url, ntfy.topic)) .header("Priority", ntfy.priority.to_string()) .header("Title", title) .body(message) @@ -134,38 +171,56 @@ async fn send_ntfy_notification(ntfy: &NtfySettings, weather_entry: &Value, tera .await; } -async fn send_smtp_notification(smtp: &SmtpSettings, weather_entry: &Value, tera: Arc>) { +async fn send_smtp_notification( + smtp: &SmtpSettings, + weather_entry: &Value, + tera: Arc>, +) { use lettre::{Message, SmtpTransport, Transport, transport::smtp::authentication::Credentials}; let mut ctx = Context::new(); add_weather_context(&mut ctx, weather_entry); let subject = if let Some(tpl) = &smtp.subject_template { let mut tera = tera.lock().unwrap(); - tera.render_str(tpl, &ctx).unwrap_or_else(|_| "⚠️ Weather Alert for Your Location".to_string()) + tera.render_str(tpl, &ctx) + .unwrap_or_else(|_| "⚠️ Weather Alert for Your Location".to_string()) } else { "⚠️ Weather Alert for Your Location".to_string() }; let body = if let Some(tpl) = &smtp.body_template { let mut tera = tera.lock().unwrap(); - tera.render_str(tpl, &ctx).unwrap_or_else(|_| default_weather_message(weather_entry)) + tera.render_str(tpl, &ctx) + .unwrap_or_else(|_| default_weather_message(weather_entry)) } else { default_weather_message(weather_entry) }; - let from = smtp.from_email.clone().unwrap_or_else(|| smtp.email.clone()); - let from_name = smtp.from_name.clone().unwrap_or_else(|| "Silmätaivas Alerts".to_string()); + let from = smtp + .from_email + .clone() + .unwrap_or_else(|| smtp.email.clone()); + let from_name = smtp + .from_name + .clone() + .unwrap_or_else(|| "Silmätaivas Alerts".to_string()); let email = Message::builder() .from(format!("{} <{}>", from_name, from).parse().unwrap()) .to(smtp.email.parse().unwrap()) .subject(subject) .body(body) .unwrap(); - let creds = smtp.username.as_ref().and_then(|u| smtp.password.as_ref().map(|p| Credentials::new(u.clone(), p.clone()))); + let creds = smtp.username.as_ref().and_then(|u| { + smtp.password + .as_ref() + .map(|p| Credentials::new(u.clone(), p.clone())) + }); let mailer = if let Some(creds) = creds { - SmtpTransport::relay(&smtp.smtp_server).unwrap() + SmtpTransport::relay(&smtp.smtp_server) + .unwrap() .port(smtp.smtp_port as u16) .credentials(creds) .build() } else { - SmtpTransport::relay(&smtp.smtp_server).unwrap() + SmtpTransport::relay(&smtp.smtp_server) + .unwrap() .port(smtp.smtp_port as u16) .build() }; @@ -173,20 +228,64 @@ async fn send_smtp_notification(smtp: &SmtpSettings, weather_entry: &Value, tera } fn add_weather_context(ctx: &mut Context, entry: &Value) { - ctx.insert("temp", &entry.pointer("/main/temp").and_then(|v| v.as_f64()).unwrap_or(0.0)); - ctx.insert("wind_speed", &(entry.pointer("/wind/speed").and_then(|v| v.as_f64()).unwrap_or(0.0) * 3.6)); - ctx.insert("rain", &entry.pointer("/rain/3h").and_then(|v| v.as_f64()).unwrap_or(0.0)); + ctx.insert( + "temp", + &entry + .pointer("/main/temp") + .and_then(|v| v.as_f64()) + .unwrap_or(0.0), + ); + ctx.insert( + "wind_speed", + &(entry + .pointer("/wind/speed") + .and_then(|v| v.as_f64()) + .unwrap_or(0.0) + * 3.6), + ); + ctx.insert( + "rain", + &entry + .pointer("/rain/3h") + .and_then(|v| v.as_f64()) + .unwrap_or(0.0), + ); ctx.insert("time", &entry["dt_txt"].as_str().unwrap_or("N/A")); - ctx.insert("humidity", &entry.pointer("/main/humidity").and_then(|v| v.as_f64()).unwrap_or(0.0)); - ctx.insert("pressure", &entry.pointer("/main/pressure").and_then(|v| v.as_f64()).unwrap_or(0.0)); + ctx.insert( + "humidity", + &entry + .pointer("/main/humidity") + .and_then(|v| v.as_f64()) + .unwrap_or(0.0), + ); + ctx.insert( + "pressure", + &entry + .pointer("/main/pressure") + .and_then(|v| v.as_f64()) + .unwrap_or(0.0), + ); } fn default_weather_message(entry: &Value) -> String { - let temp = entry.pointer("/main/temp").and_then(|v| v.as_f64()).unwrap_or(0.0); - let wind = entry.pointer("/wind/speed").and_then(|v| v.as_f64()).unwrap_or(0.0) * 3.6; - let rain = entry.pointer("/rain/3h").and_then(|v| v.as_f64()).unwrap_or(0.0); + let temp = entry + .pointer("/main/temp") + .and_then(|v| v.as_f64()) + .unwrap_or(0.0); + let wind = entry + .pointer("/wind/speed") + .and_then(|v| v.as_f64()) + .unwrap_or(0.0) + * 3.6; + let rain = entry + .pointer("/rain/3h") + .and_then(|v| v.as_f64()) + .unwrap_or(0.0); let time = entry["dt_txt"].as_str().unwrap_or("N/A"); - format!("🚨 Weather alert for your location ({}):\n\n🌬️ Wind: {:.1} km/h\n🌧️ Rain: {:.1} mm\n🌡️ Temperature: {:.1} °C\n\nStay safe,\n— Silmätaivas", time, wind, rain, temp) + format!( + "🚨 Weather alert for your location ({}):\n\n🌬️ Wind: {:.1} km/h\n🌧️ Rain: {:.1} mm\n🌡️ Temperature: {:.1} °C\n\nStay safe,\n— Silmätaivas", + time, wind, rain, temp + ) } -// Unit tests for threshold logic and template rendering can be added here. \ No newline at end of file +// Unit tests for threshold logic and template rendering can be added here. -- cgit v1.2.3