checkpoint
This commit is contained in:
parent
27470083dc
commit
3b79aa21d2
3 changed files with 56 additions and 15 deletions
10
Cargo.lock
generated
10
Cargo.lock
generated
|
@ -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",
|
||||||
|
|
|
@ -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"] }
|
||||||
|
|
60
src/main.rs
60
src/main.rs
|
@ -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)?;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue