From 3b79aa21d24fcdd4f8151b4c38aeaaa7eaa3c33c Mon Sep 17 00:00:00 2001 From: _ <_@_> Date: Thu, 14 Aug 2025 01:45:43 +0000 Subject: [PATCH] checkpoint --- Cargo.lock | 10 +++++++++ Cargo.toml | 1 + src/main.rs | 60 +++++++++++++++++++++++++++++++++++++++-------------- 3 files changed, 56 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 04362c2..e81b035 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -148,6 +148,15 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +[[package]] +name = "camino" +version = "1.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d07aa9a93b00c76f71bc35d598bed923f6d4f3a9ca5c24b7737ae1a292841c0" +dependencies = [ + "serde", +] + [[package]] name = "cc" version = "1.2.32" @@ -1860,6 +1869,7 @@ version = "0.1.0" dependencies = [ "anyhow", "base64", + "camino", "chrono", "chrono-tz", "clap", diff --git a/Cargo.toml b/Cargo.toml index 4271d6c..8628ae0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ edition = "2024" [dependencies] anyhow = "1.0.98" base64 = "0.22.1" +camino = { version = "1.1.11", features = ["serde1"] } chrono = "0.4.41" chrono-tz = { version = "0.10.4", features = ["serde"] } clap = { version = "4.5.43", features = ["derive"] } diff --git a/src/main.rs b/src/main.rs index e182d7f..d1a7907 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,6 @@ use anyhow::{Context as _, Result, anyhow, bail}; use base64::Engine as _; +use camino::Utf8PathBuf; use chrono::{DateTime, TimeZone as _, Utc}; use clap::Parser as _; use icalendar::{Component as _, EventLike as _}; @@ -7,26 +8,49 @@ use serde::Deserialize; use std::{ collections::BTreeSet, io::Write as _, path::PathBuf, str::FromStr as _, time::Duration, }; +use url::Url; #[cfg(test)] mod tests; +#[derive(Clone, Deserialize)] +struct Downloadable { + /// URL to scrape to download the JSON + download_url: Option, + + /// Disk location to cache the JSON file for debugging + file_path: Utf8PathBuf, +} + +/// The Sierra Club uses a calendar software called Campfire, which luckily puts out nice JSON files +/// +/// It's also deterministic, which is lovely. You get the same JSON byte-for-byte unless the events changed +#[derive(Clone, Deserialize)] +struct ConfigCampfire { + #[serde(flatten)] + dl: Downloadable, + + /// A canonical webpage we can direct users to + html_url: Option, + + /// Very short name for putting on each event + short_name: String, +} + +/// Google Calendar has a public ics endpoint that we scrape for all upstream Google Calendars #[derive(Clone, Deserialize)] struct ConfigIcal { - /// Disk location to cache the ics file for debugging - file_path: PathBuf, + #[serde(flatten)] + dl: Downloadable, /// Magical ID we pass to Google to deep-link to Google Calendar events google_id: Option, /// A canonical webpage we can direct users to - html_url: Option, + html_url: Option, /// Very short name for putting on each event short_name: String, - - /// URL to scrape to download the ics file - ics_url: Option, } #[derive(Deserialize)] @@ -50,6 +74,7 @@ struct ConfigOutput { #[derive(Deserialize)] struct Config { + campfires: Vec, icals: Vec, output: ConfigOutput, } @@ -315,8 +340,8 @@ impl ICal { Ok(cal) } - fn read_from_config(config: &ConfigIcal) -> Result { - let s = std::fs::read_to_string(&config.file_path)?; + fn read_from_downloadable(dl: &Downloadable) -> Result { + let s = std::fs::read_to_string(&dl.file_path)?; Self::read_from_str(&s) } @@ -399,7 +424,7 @@ struct Data { fn read_data_from_disk(config: &Config) -> Result { let mut data = Data::default(); for config in &config.icals { - let cal = ICal::read_from_config(config)?; + let cal = ICal::read_from_downloadable(&config.dl)?; data.icals.push((cal, config.clone())); } @@ -628,20 +653,25 @@ async fn do_everything(cli: &CliAuto) -> Result<()> { let client = reqwest::Client::builder() .user_agent(APP_USER_AGENT) .build()?; - for ical in &config.icals { - let Some(ics_url) = &ical.ics_url else { + for dl in config + .campfires + .iter() + .map(|cf| &cf.dl) + .chain(config.icals.iter().map(|ical| &ical.dl)) + { + let Some(download_url) = &dl.download_url else { continue; }; - tracing::info!(url = ics_url.to_string(), "requesting..."); - let resp = client.get(ics_url.clone()).send().await?; + tracing::info!(url = download_url.to_string(), "requesting..."); + let resp = client.get(download_url.clone()).send().await?; if resp.status() != 200 { bail!("Bad status {}", resp.status()); } let bytes = resp.bytes().await?; - let temp_path = ical.file_path.with_extension(".ics.temp"); + let temp_path = dl.file_path.with_extension(".ics.temp"); std::fs::write(&temp_path, &bytes)?; - std::fs::rename(&temp_path, &ical.file_path)?; + std::fs::rename(&temp_path, &dl.file_path)?; } let data = read_data_from_disk(&config)?;