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

View file

@ -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<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)]
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<String>,
/// 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
short_name: String,
/// URL to scrape to download the ics file
ics_url: Option<url::Url>,
}
#[derive(Deserialize)]
@ -50,6 +74,7 @@ struct ConfigOutput {
#[derive(Deserialize)]
struct Config {
campfires: Vec<ConfigCampfire>,
icals: Vec<ConfigIcal>,
output: ConfigOutput,
}
@ -315,8 +340,8 @@ impl ICal {
Ok(cal)
}
fn read_from_config(config: &ConfigIcal) -> Result<Self> {
let s = std::fs::read_to_string(&config.file_path)?;
fn read_from_downloadable(dl: &Downloadable) -> Result<Self> {
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<Data> {
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)?;