use super::{CalendarUi, EventInstance, Parameters, SimpleDownload}; use crate::prelude::*; use chrono::DateTime; #[derive(Clone, Deserialize)] struct SillyDownloadable { /// URL to scrape to download the file from download_url: Url, /// Disk location to cache the file for debugging file_path: Utf8PathBuf, } #[derive(Clone, Deserialize)] pub(crate) struct Config { #[serde(flatten)] dl: SillyDownloadable, #[serde(flatten)] pub(crate) ui: CalendarUi, } impl Config { pub(crate) fn simple_download(&self, now: DateTime) -> SimpleDownload { let date = now.format("%Y-%m-%d").to_string(); let mut url = self.dl.download_url.clone(); url.query_pairs_mut().append_pair("date", &date); SimpleDownload { download_url: Some(url), file_path: self.dl.file_path.clone(), } } } #[derive(Deserialize)] struct Event { #[serde(alias = "durationInMinutes")] duration_in_minutes: u32, timestamp: i64, title: String, description: String, } #[derive(Deserialize)] struct Data { items: Vec, } /// The bit that we deserialize directly from JSON #[derive(Deserialize)] struct CalendarInner { data: Data, } pub(crate) struct Calendar { config: Config, inner: CalendarInner, } impl Calendar { fn to_event_instance(&self, params: &Parameters, ev: &Event) -> Result> { let dt = DateTime::from_timestamp_millis(ev.timestamp) .context("cannot represent timestamp as a date")? .with_timezone(¶ms.tz); let dtstart = crate::DatePerhapsTime { dt, all_day: false }; if dtstart.dt < params.output_start || dtstart.dt > params.output_stop { return Ok(None); } Ok(Some(EventInstance { calendar_ui: self.config.ui.clone(), dtstart, location: None, recurrence_id: None, summary: Some(ev.title.clone()), uid: None, url: None, })) } pub(crate) fn event_instances(&self, params: &Parameters) -> Result> { self.inner .data .items .iter() .filter_map(|ev| self.to_event_instance(params, ev).transpose()) .collect() } pub(crate) fn read_from_str(config: Config, s: &str) -> Result { let inner = serde_json::from_str(s)?; Ok(Self { config, inner }) } pub(crate) fn read_from_config(config: Config) -> Result { let s = std::fs::read_to_string(&config.dl.file_path)?; Self::read_from_str(config, &s) } } #[cfg(test)] mod tests { use super::*; fn example_config() -> Config { Config { dl: SillyDownloadable { download_url: Url::parse("https://www.commoninja.com/api/apps/calendar/get-monthly-events?widgetId=00000000-0000-0000-000000000000").unwrap(), file_path: ".".into(), }, ui: CalendarUi { html_url: None, short_name: "asdf".into(), }, } } #[test] fn end_to_end() { let s = r#" {"data":{"items":[{"timestamp":1748989800000,"durationInMinutes":90,"title":"Foo Bar","description":""},{"timestamp":1749999600000,"durationInMinutes":30,"title":"Snaf Oo","description":""}]},"success":true,"message":""} "#; let cfg = example_config(); let cal = Calendar::read_from_str(cfg, s).unwrap(); let params = Parameters::new( DateTime::from_timestamp(1748989700, 0) .unwrap() .with_timezone(&chrono_tz::America::Chicago), ) .unwrap(); let instances = cal.event_instances(¶ms).unwrap(); assert_eq!(instances.len(), 2); } #[test] fn unsillify() { let dt = DateTime::from_timestamp(1756190298, 0) .unwrap() .with_timezone(&chrono_tz::America::Chicago); let cfg = example_config(); assert_eq!( cfg.simple_download(dt).download_url.unwrap().to_string(), "https://www.commoninja.com/api/apps/calendar/get-monthly-events?widgetId=00000000-0000-0000-000000000000&date=2025-08-26" ); } }