add support for CommonNinja calendars
This commit is contained in:
parent
4140010bf2
commit
097745aeed
5 changed files with 166 additions and 18 deletions
50
src/main.rs
50
src/main.rs
|
@ -1,23 +1,23 @@
|
||||||
use anyhow::{Context as _, Result, bail};
|
use chrono::DateTime;
|
||||||
use camino::Utf8PathBuf;
|
|
||||||
use chrono::{DateTime, Utc};
|
|
||||||
use clap::Parser as _;
|
use clap::Parser as _;
|
||||||
use serde::Deserialize;
|
|
||||||
use std::{collections::BTreeSet, io::Write as _, time::Duration};
|
use std::{collections::BTreeSet, io::Write as _, time::Duration};
|
||||||
use url::Url;
|
|
||||||
|
use prelude::*;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests;
|
mod tests;
|
||||||
|
|
||||||
|
mod prelude;
|
||||||
mod wac_campfire;
|
mod wac_campfire;
|
||||||
|
mod wac_common_ninja;
|
||||||
mod wac_ical;
|
mod wac_ical;
|
||||||
|
|
||||||
#[derive(Clone, Default, Deserialize)]
|
#[derive(Clone, Default, Deserialize)]
|
||||||
struct Downloadable {
|
struct SimpleDownload {
|
||||||
/// URL to scrape to download the JSON
|
/// URL to scrape to download the file from
|
||||||
download_url: Option<Url>,
|
download_url: Option<Url>,
|
||||||
|
|
||||||
/// Disk location to cache the JSON file for debugging
|
/// Disk location to cache the file for debugging
|
||||||
file_path: Utf8PathBuf,
|
file_path: Utf8PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,10 +52,21 @@ struct ConfigOutput {
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
struct Config {
|
struct Config {
|
||||||
campfires: Vec<wac_campfire::Config>,
|
campfires: Vec<wac_campfire::Config>,
|
||||||
|
common_ninjas: Vec<wac_common_ninja::Config>,
|
||||||
icals: Vec<wac_ical::Config>,
|
icals: Vec<wac_ical::Config>,
|
||||||
output: ConfigOutput,
|
output: ConfigOutput,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Config {
|
||||||
|
fn downloads(&self) -> impl Iterator<Item = SimpleDownload> {
|
||||||
|
self.campfires
|
||||||
|
.iter()
|
||||||
|
.map(|cf| cf.dl.clone())
|
||||||
|
.chain(self.common_ninjas.iter().map(|cn| cn.simple_download()))
|
||||||
|
.chain(self.icals.iter().map(|ical| ical.dl.clone()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(clap::Parser)]
|
#[derive(clap::Parser)]
|
||||||
struct CliAuto {
|
struct CliAuto {
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
|
@ -175,6 +186,7 @@ impl EventInstance {
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
struct Data {
|
struct Data {
|
||||||
campfires: Vec<wac_campfire::Calendar>,
|
campfires: Vec<wac_campfire::Calendar>,
|
||||||
|
common_ninjas: Vec<wac_common_ninja::Calendar>,
|
||||||
icals: Vec<wac_ical::Calendar>,
|
icals: Vec<wac_ical::Calendar>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -185,6 +197,11 @@ fn read_data_from_disk(config: &Config) -> Result<Data> {
|
||||||
.iter()
|
.iter()
|
||||||
.map(|cfg| wac_campfire::Calendar::read_from_config(cfg.clone()))
|
.map(|cfg| wac_campfire::Calendar::read_from_config(cfg.clone()))
|
||||||
.collect::<Result<Vec<_>, _>>()?,
|
.collect::<Result<Vec<_>, _>>()?,
|
||||||
|
common_ninjas: config
|
||||||
|
.common_ninjas
|
||||||
|
.iter()
|
||||||
|
.map(|cfg| wac_common_ninja::Calendar::read_from_config(cfg.clone()))
|
||||||
|
.collect::<Result<Vec<_>, _>>()?,
|
||||||
icals: config
|
icals: config
|
||||||
.icals
|
.icals
|
||||||
.iter()
|
.iter()
|
||||||
|
@ -212,6 +229,16 @@ fn process_data<'a>(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for common_ninja in &data.common_ninjas {
|
||||||
|
for ev in common_ninja
|
||||||
|
.event_instances(¶ms)?
|
||||||
|
.into_iter()
|
||||||
|
.filter(|x| x.filter(config_output))
|
||||||
|
{
|
||||||
|
instances.push(ev);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for ical in &data.icals {
|
for ical in &data.icals {
|
||||||
for ev in ical
|
for ev in ical
|
||||||
.event_instances(¶ms)?
|
.event_instances(¶ms)?
|
||||||
|
@ -412,12 +439,7 @@ 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 dl in config
|
for dl in config.downloads() {
|
||||||
.campfires
|
|
||||||
.iter()
|
|
||||||
.map(|cf| &cf.dl)
|
|
||||||
.chain(config.icals.iter().map(|ical| &ical.dl))
|
|
||||||
{
|
|
||||||
let Some(download_url) = &dl.download_url else {
|
let Some(download_url) = &dl.download_url else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
5
src/prelude.rs
Normal file
5
src/prelude.rs
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
pub(crate) use anyhow::{Context as _, Result, bail};
|
||||||
|
pub(crate) use camino::Utf8PathBuf;
|
||||||
|
pub(crate) use chrono::Utc;
|
||||||
|
pub(crate) use serde::Deserialize;
|
||||||
|
pub(crate) use url::Url;
|
|
@ -4,14 +4,14 @@
|
||||||
//!
|
//!
|
||||||
//! Note that recurring events aren't implemented for this cause I don't know how they work
|
//! Note that recurring events aren't implemented for this cause I don't know how they work
|
||||||
|
|
||||||
use super::{CalendarUi, DatePerhapsTime, Downloadable, EventInstance, Parameters};
|
use super::{CalendarUi, DatePerhapsTime, EventInstance, Parameters, SimpleDownload};
|
||||||
use anyhow::{Context as _, Result, bail};
|
use anyhow::{Context as _, Result, bail};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
#[derive(Clone, Default, Deserialize)]
|
#[derive(Clone, Default, Deserialize)]
|
||||||
pub(crate) struct Config {
|
pub(crate) struct Config {
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
pub(crate) dl: Downloadable,
|
pub(crate) dl: SimpleDownload,
|
||||||
|
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
pub(crate) ui: CalendarUi,
|
pub(crate) ui: CalendarUi,
|
||||||
|
|
121
src/wac_common_ninja.rs
Normal file
121
src/wac_common_ninja.rs
Normal file
|
@ -0,0 +1,121 @@
|
||||||
|
use super::{CalendarUi, EventInstance, Parameters, SimpleDownload};
|
||||||
|
use crate::prelude::*;
|
||||||
|
|
||||||
|
#[derive(Clone, Default, Deserialize)]
|
||||||
|
struct SillyDownloadable {
|
||||||
|
/// URL to scrape to download the file from
|
||||||
|
download_url: Option<Url>,
|
||||||
|
|
||||||
|
/// Disk location to cache the file for debugging
|
||||||
|
file_path: Utf8PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Default, Deserialize)]
|
||||||
|
pub(crate) struct Config {
|
||||||
|
#[serde(flatten)]
|
||||||
|
pub(crate) dl: SillyDownloadable,
|
||||||
|
|
||||||
|
#[serde(flatten)]
|
||||||
|
pub(crate) ui: CalendarUi,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Config {
|
||||||
|
pub(crate) fn simple_download(&self) -> SimpleDownload {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct Event {
|
||||||
|
#[serde(alias = "durationInMinutes")]
|
||||||
|
duration_in_minutes: u32,
|
||||||
|
timestamp: i64,
|
||||||
|
title: String,
|
||||||
|
description: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct Data {
|
||||||
|
items: Vec<Event>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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<Option<EventInstance>> {
|
||||||
|
let dt = chrono::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<Vec<EventInstance>> {
|
||||||
|
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<Self> {
|
||||||
|
let inner = serde_json::from_str(s)?;
|
||||||
|
Ok(Self { config, inner })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn read_from_config(config: Config) -> Result<Self> {
|
||||||
|
let s = std::fs::read_to_string(&config.dl.file_path)?;
|
||||||
|
Self::read_from_str(config, &s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[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 = Config {
|
||||||
|
dl: SillyDownloadable {
|
||||||
|
download_url: None,
|
||||||
|
file_path: ".".into(),
|
||||||
|
},
|
||||||
|
ui: CalendarUi {
|
||||||
|
html_url: None,
|
||||||
|
short_name: "asdf".into(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let cal = Calendar::read_from_str(cfg, s).unwrap();
|
||||||
|
let params =
|
||||||
|
Parameters::new(Utc::now().with_timezone(&chrono_tz::America::Chicago)).unwrap();
|
||||||
|
let instances = cal.event_instances(¶ms).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(instances.len(), 2);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
//! Structs and functions specific to gathering input from ics files, which is a popular format that Google Calendar happens to put out
|
//! Structs and functions specific to gathering input from ics files, which is a popular format that Google Calendar happens to put out
|
||||||
|
|
||||||
use super::{CalendarUi, DatePerhapsTime, Downloadable, EventInstance, Parameters};
|
use super::{CalendarUi, DatePerhapsTime, EventInstance, Parameters, SimpleDownload};
|
||||||
use anyhow::{Context as _, Result, anyhow};
|
use anyhow::{Context as _, Result, anyhow};
|
||||||
use base64::Engine as _;
|
use base64::Engine as _;
|
||||||
use chrono::TimeZone as _;
|
use chrono::TimeZone as _;
|
||||||
|
@ -12,7 +12,7 @@ use std::{collections::BTreeSet, str::FromStr as _};
|
||||||
#[derive(Clone, Default, Deserialize)]
|
#[derive(Clone, Default, Deserialize)]
|
||||||
pub(crate) struct Config {
|
pub(crate) struct Config {
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
pub(crate) dl: Downloadable,
|
pub(crate) dl: SimpleDownload,
|
||||||
|
|
||||||
/// 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>,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue