summaryrefslogtreecommitdiff
path: root/src/weather_poller.rs
diff options
context:
space:
mode:
authorDawid Rycerz <dawid@rycerz.xyz>2025-07-16 23:03:40 +0300
committerDawid Rycerz <dawid@rycerz.xyz>2025-07-16 23:03:40 +0300
commit1aee0b802cad9fc9343b6c2966ba112f9b762f7c (patch)
tree53d9551fbfd3df01ac61ecd1128060a9a9727a84 /src/weather_poller.rs
parentdbb25297da61fe393ca1e8a6b6c6beace2513e0a (diff)
feat: refactor and remove lib usage
Diffstat (limited to 'src/weather_poller.rs')
-rw-r--r--src/weather_poller.rs291
1 files changed, 1 insertions, 290 deletions
diff --git a/src/weather_poller.rs b/src/weather_poller.rs
index 6a32661..ea211b7 100644
--- a/src/weather_poller.rs
+++ b/src/weather_poller.rs
@@ -1,290 +1 @@
-use crate::locations::LocationRepository;
-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 tera::{Context, Tera};
-
-const OWM_API_URL: &str = "https://api.openweathermap.org/data/2.5/forecast";
-const ALERT_WINDOW_HOURS: i64 = 24;
-
-pub struct WeatherPoller {
- pub db: Arc<sqlx::SqlitePool>,
- pub owm_api_key: String,
- pub tera: Arc<Mutex<Tera>>,
-}
-
-impl WeatherPoller {
- pub async fn check_all(&self) {
- let user_repo = UserRepository { db: &self.db };
- 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;
- }
- }
- }
-
- 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::<Vec<_>>();
- 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<Option<Vec<Value>>, reqwest::Error> {
- let client = Client::new();
- let resp = client
- .get(OWM_API_URL)
- .query(&[
- ("lat", lat.to_string()),
- ("lon", lon.to_string()),
- ("units", "metric".to_string()),
- ("appid", self.owm_api_key.clone()),
- ])
- .send()
- .await?;
- let json: Value = resp.json().await?;
- Ok(json["list"].as_array().cloned())
- }
-
- pub async fn send_notifications(&self, user_id: i64, weather_entry: &Value) {
- let ntfy_repo = NtfySettingsRepository { db: &self.db };
- let smtp_repo = SmtpSettingsRepository { db: &self.db };
- let tera = self.tera.clone();
- if let Some(ntfy) = ntfy_repo.get_by_user(user_id).await.unwrap_or(None) {
- if ntfy.enabled {
- send_ntfy_notification(&ntfy, weather_entry, tera.clone()).await;
- }
- }
- if let Some(smtp) = smtp_repo.get_by_user(user_id).await.unwrap_or(None) {
- if smtp.enabled {
- send_smtp_notification(&smtp, weather_entry, tera.clone()).await;
- }
- }
- }
-}
-
-fn find_first_alert_entry(forecast: &[Value], thresholds: &[WeatherThreshold]) -> Option<Value> {
- use chrono::{TimeZone, Utc};
- let now = Utc::now();
- for entry in forecast {
- if let Some(ts) = entry["dt"].as_i64() {
- let forecast_time = Utc.timestamp_opt(ts, 0).single()?;
- if (forecast_time - now).num_hours() > ALERT_WINDOW_HOURS {
- break;
- }
- if thresholds.iter().any(|t| threshold_triggered(t, entry)) {
- return Some(entry.clone());
- }
- }
- }
- None
-}
-
-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),
- _ => return false,
- };
- compare(value, &threshold.operator, threshold.threshold_value)
-}
-
-fn compare(value: f64, op: &str, threshold: f64) -> bool {
- match op {
- ">" => value > threshold,
- ">=" => value >= threshold,
- "<" => value < threshold,
- "<=" => value <= threshold,
- "==" => (value - threshold).abs() < f64::EPSILON,
- _ => false,
- }
-}
-
-async fn send_ntfy_notification(
- ntfy: &NtfySettings,
- weather_entry: &Value,
- tera: Arc<Mutex<Tera>>,
-) {
- 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())
- } 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())
- } else {
- default_weather_message(weather_entry)
- };
- let client = Client::new();
- let _ = client
- .post(format!("{}/{}", ntfy.server_url, ntfy.topic))
- .header("Priority", ntfy.priority.to_string())
- .header("Title", title)
- .body(message)
- .send()
- .await;
-}
-
-async fn send_smtp_notification(
- smtp: &SmtpSettings,
- weather_entry: &Value,
- tera: Arc<Mutex<Tera>>,
-) {
- 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())
- } 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))
- } 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 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 mailer = if let Some(creds) = creds {
- SmtpTransport::relay(&smtp.smtp_server)
- .unwrap()
- .port(smtp.smtp_port as u16)
- .credentials(creds)
- .build()
- } else {
- SmtpTransport::relay(&smtp.smtp_server)
- .unwrap()
- .port(smtp.smtp_port as u16)
- .build()
- };
- let _ = mailer.send(&email);
-}
-
-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("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),
- );
-}
-
-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 time = entry["dt_txt"].as_str().unwrap_or("N/A");
- format!(
- "🚨 Weather alert for your location ({time}):\n\n🌬️ Wind: {wind:.1} km/h\n🌧️ Rain: {rain:.1} mm\n🌡️ Temperature: {temp:.1} °C\n\nStay safe,\n— Silmätaivas"
- )
-}
-
-// Unit tests for threshold logic and template rendering can be added here.
+// Remove all unused imports at the top of the file.