diff --git a/Cargo.lock b/Cargo.lock index ac8669c..6b043cb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -61,6 +61,7 @@ dependencies = [ "sea-orm", "serde", "serde_json", + "time", "tokio", "tracing", "tracing-subscriber", diff --git a/Cargo.toml b/Cargo.toml index 42e3d07..c76230a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,7 @@ rumqttc = "0.24.0" sea-orm = { version = "0.12", features = [ "sqlx-postgres", "runtime-tokio-rustls", "macros" ] } serde = "1.0.199" serde_json = "1.0.116" +time = { version = "0.3.36", features = ["macros", "serde", "formatting", "parsing" ] } tokio = { version = "1.37.0", features = ["full"] } tracing = "0.1.40" tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } diff --git a/src/alert_swiss.rs b/src/alert_swiss.rs new file mode 100644 index 0000000..058a55c --- /dev/null +++ b/src/alert_swiss.rs @@ -0,0 +1,7 @@ +use crate::json::alerts::Alerts; + +const ALERT_SWISS_URL: &str = "https://www.alert.swiss/content/alertswiss-internet/en/home/_jcr_content/polyalert.alertswiss_alerts.actual.json"; + +pub async fn fetch_alerts() -> Result { + reqwest::get(ALERT_SWISS_URL).await?.json::().await +} diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 0000000..7e18eec --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,12 @@ +//! Cli interface. + +use clap::Parser; + +/// Push Alertswiss notifications to MQTT. +#[derive(Parser)] +#[command(version, about, long_about = None)] +pub struct Cli { + /// Update interval in seconds + #[arg(short, long, default_value_t = 10)] + pub interval: u64, +} diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..d10543c --- /dev/null +++ b/src/config.rs @@ -0,0 +1,15 @@ +use crate::cli::Cli; + +/// Application configuration. +pub struct Config { + /// Update interval in seconds + pub interval: u64, +} + +impl From for Config { + fn from(value: Cli) -> Self { + Self { + interval: value.interval, + } + } +} diff --git a/src/json/alert.rs b/src/json/alert.rs index 430ed66..26e802a 100644 --- a/src/json/alert.rs +++ b/src/json/alert.rs @@ -1,4 +1,5 @@ use serde::{Deserialize, Serialize}; +use time::PrimitiveDateTime; use super::{ area::Area, contact::Contact, description::Description, image::Image, @@ -11,7 +12,8 @@ use super::{ pub struct Alert { pub identifier: String, pub gtrans_origin: Option, - pub sent: String, + #[serde(deserialize_with = "super::datetime::deserialize_alert")] + pub sent: PrimitiveDateTime, pub title: Title, pub description: Description, pub nation_wide: bool, @@ -27,7 +29,8 @@ pub struct Alert { pub severity: String, pub test_alert: bool, pub technical_test_alert: bool, - pub publish_date: String, + #[serde(deserialize_with = "super::datetime::deserialize_alert")] + pub publish_date: PrimitiveDateTime, pub areas: Vec, pub images: Vec, pub image_titles: Vec, diff --git a/src/json/alerts.rs b/src/json/alerts.rs index 9863f2f..d175ef4 100644 --- a/src/json/alerts.rs +++ b/src/json/alerts.rs @@ -1,10 +1,13 @@ use super::alert::Alert; use serde::{Deserialize, Serialize}; +use time::OffsetDateTime; #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Alerts { pub heartbeat_age_in_millis: u64, - pub render_time: String, - pub alerts: Vec, + #[serde(deserialize_with = "super::datetime::deserialize_alerts")] + pub render_time: OffsetDateTime, + #[serde(rename = "alerts")] + pub list: Vec, } diff --git a/src/json/datetime.rs b/src/json/datetime.rs new file mode 100644 index 0000000..b27bdc7 --- /dev/null +++ b/src/json/datetime.rs @@ -0,0 +1,26 @@ +use serde::{Deserialize, Deserializer}; +use time::format_description::FormatItem; +use time::macros::format_description; +use time::{OffsetDateTime, PrimitiveDateTime}; + +const ALERT_FORMAT: &[FormatItem<'_>] = + format_description!("[weekday repr:short], [day].[month].[year], [hour]:[minute]"); +const ALERTS_FORMAT: &[FormatItem<'_>] = format_description!( + "[day].[month].[year] [hour]:[minute]:[second].[subsecond] [offset_hour sign:mandatory][offset_minute]" +); + +pub fn deserialize_alert<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + let s: &str = Deserialize::deserialize(deserializer)?; + PrimitiveDateTime::parse(s, ALERT_FORMAT).map_err(serde::de::Error::custom) +} + +pub fn deserialize_alerts<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + let s: &str = Deserialize::deserialize(deserializer)?; + OffsetDateTime::parse(s, ALERTS_FORMAT).map_err(serde::de::Error::custom) +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..aec1092 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,42 @@ +use std::time::Duration; + +use config::Config; +use tokio::time; +use tracing::debug; + +use crate::json::alert::Alert; + +pub mod alert_swiss; +pub mod cli; +pub mod config; +mod json { + pub mod alert; + pub mod alerts; + pub mod area; + pub mod circle; + pub mod contact; + pub mod coordinate; + pub mod datetime; + pub mod description; + pub mod image; + pub mod image_description; + pub mod image_title; + pub mod link; + pub mod polygon; + pub mod region; + pub mod text; + pub mod title; +} +pub mod logging; + +pub async fn run(config: Config) { + let mut interval = time::interval(Duration::from_secs(config.interval)); + + loop { + interval.tick().await; + let alerts = alert_swiss::fetch_alerts().await.unwrap(); + let new_alerts: Vec = alerts.list.into_iter().filter(|x| true).collect(); + + debug!("{new_alerts:#?}"); + } +} diff --git a/src/logging.rs b/src/logging.rs index 0b4a633..16bfc26 100644 --- a/src/logging.rs +++ b/src/logging.rs @@ -1,7 +1,7 @@ use tracing::debug; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; -pub(crate) fn setup(bin_name: &str) { +pub fn setup(bin_name: &str) { let default_config = format!("{}=info,tower_http=info", bin_name); tracing_subscriber::registry() .with( diff --git a/src/main.rs b/src/main.rs index 188dd98..40f7d14 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,42 +1,16 @@ -use std::time::Duration; - -use tokio::time; -use tracing::debug; - -use crate::json::alerts::Alerts; - -mod json { - pub mod alert; - pub mod alerts; - pub mod area; - pub mod circle; - pub mod contact; - pub mod coordinate; - pub mod description; - pub mod image; - pub mod image_description; - pub mod image_title; - pub mod link; - pub mod polygon; - pub mod region; - pub mod text; - pub mod title; -} -mod logging; +use alert_me::{cli::Cli, logging}; +use clap::Parser; #[tokio::main] async fn main() { + if std::env::var_os("RUST_LOG").is_none() { + std::env::set_var("RUST_LOG", "info"); + } + logging::setup("alert-me"); - let mut interval = time::interval(Duration::from_secs(5)); + let args = Cli::parse(); + let config = args.into(); - loop { - interval.tick().await; - let resp = reqwest::get("https://www.alert.swiss/content/alertswiss-internet/en/home/_jcr_content/polyalert.alertswiss_alerts.actual.json") - .await - .unwrap() - .json::() - .await.unwrap(); - debug!("{resp:#?}"); - } + alert_me::run(config).await }