checkpoint

This commit is contained in:
_ 2025-08-14 01:45:43 +00:00
parent 27470083dc
commit 3b79aa21d2
3 changed files with 56 additions and 15 deletions

10
Cargo.lock generated
View file

@ -148,6 +148,15 @@ version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
[[package]]
name = "camino"
version = "1.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d07aa9a93b00c76f71bc35d598bed923f6d4f3a9ca5c24b7737ae1a292841c0"
dependencies = [
"serde",
]
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.2.32" version = "1.2.32"
@ -1860,6 +1869,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"base64", "base64",
"camino",
"chrono", "chrono",
"chrono-tz", "chrono-tz",
"clap", "clap",

View file

@ -6,6 +6,7 @@ edition = "2024"
[dependencies] [dependencies]
anyhow = "1.0.98" anyhow = "1.0.98"
base64 = "0.22.1" base64 = "0.22.1"
camino = { version = "1.1.11", features = ["serde1"] }
chrono = "0.4.41" chrono = "0.4.41"
chrono-tz = { version = "0.10.4", features = ["serde"] } chrono-tz = { version = "0.10.4", features = ["serde"] }
clap = { version = "4.5.43", features = ["derive"] } clap = { version = "4.5.43", features = ["derive"] }

View file

@ -1,5 +1,6 @@
use anyhow::{Context as _, Result, anyhow, bail}; use anyhow::{Context as _, Result, anyhow, bail};
use base64::Engine as _; use base64::Engine as _;
use camino::Utf8PathBuf;
use chrono::{DateTime, TimeZone as _, Utc}; use chrono::{DateTime, TimeZone as _, Utc};
use clap::Parser as _; use clap::Parser as _;
use icalendar::{Component as _, EventLike as _}; use icalendar::{Component as _, EventLike as _};
@ -7,26 +8,49 @@ use serde::Deserialize;
use std::{ use std::{
collections::BTreeSet, io::Write as _, path::PathBuf, str::FromStr as _, time::Duration, collections::BTreeSet, io::Write as _, path::PathBuf, str::FromStr as _, time::Duration,
}; };
use url::Url;
#[cfg(test)] #[cfg(test)]
mod tests; mod tests;
#[derive(Clone, Deserialize)]
struct Downloadable {
/// URL to scrape to download the JSON
download_url: Option<Url>,
/// 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<Url>,
/// 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)] #[derive(Clone, Deserialize)]
struct ConfigIcal { struct ConfigIcal {
/// Disk location to cache the ics file for debugging #[serde(flatten)]
file_path: PathBuf, dl: Downloadable,
/// Magical ID we pass to Google to deep-link to Google Calendar events /// Magical ID we pass to Google to deep-link to Google Calendar events
google_id: Option<String>, google_id: Option<String>,
/// A canonical webpage we can direct users to /// A canonical webpage we can direct users to
html_url: Option<url::Url>, html_url: Option<Url>,
/// Very short name for putting on each event /// Very short name for putting on each event
short_name: String, short_name: String,
/// URL to scrape to download the ics file
ics_url: Option<url::Url>,
} }
#[derive(Deserialize)] #[derive(Deserialize)]
@ -50,6 +74,7 @@ struct ConfigOutput {
#[derive(Deserialize)] #[derive(Deserialize)]
struct Config { struct Config {
campfires: Vec<ConfigCampfire>,
icals: Vec<ConfigIcal>, icals: Vec<ConfigIcal>,
output: ConfigOutput, output: ConfigOutput,
} }
@ -315,8 +340,8 @@ impl ICal {
Ok(cal) Ok(cal)
} }
fn read_from_config(config: &ConfigIcal) -> Result<Self> { fn read_from_downloadable(dl: &Downloadable) -> Result<Self> {
let s = std::fs::read_to_string(&config.file_path)?; let s = std::fs::read_to_string(&dl.file_path)?;
Self::read_from_str(&s) Self::read_from_str(&s)
} }
@ -399,7 +424,7 @@ struct Data {
fn read_data_from_disk(config: &Config) -> Result<Data> { fn read_data_from_disk(config: &Config) -> Result<Data> {
let mut data = Data::default(); let mut data = Data::default();
for config in &config.icals { 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())); data.icals.push((cal, config.clone()));
} }
@ -628,20 +653,25 @@ async fn do_everything(cli: &CliAuto) -> Result<()> {
let client = reqwest::Client::builder() let client = reqwest::Client::builder()
.user_agent(APP_USER_AGENT) .user_agent(APP_USER_AGENT)
.build()?; .build()?;
for ical in &config.icals { for dl in config
let Some(ics_url) = &ical.ics_url else { .campfires
.iter()
.map(|cf| &cf.dl)
.chain(config.icals.iter().map(|ical| &ical.dl))
{
let Some(download_url) = &dl.download_url else {
continue; continue;
}; };
tracing::info!(url = ics_url.to_string(), "requesting..."); tracing::info!(url = download_url.to_string(), "requesting...");
let resp = client.get(ics_url.clone()).send().await?; let resp = client.get(download_url.clone()).send().await?;
if resp.status() != 200 { if resp.status() != 200 {
bail!("Bad status {}", resp.status()); bail!("Bad status {}", resp.status());
} }
let bytes = resp.bytes().await?; 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::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)?; let data = read_data_from_disk(&config)?;