From 900d43282808b3f5495fb46898c89e8e043bf278 Mon Sep 17 00:00:00 2001 From: Lennart <18233294+lennart-k@users.noreply.github.com> Date: Thu, 21 Sep 2023 15:16:58 +0200 Subject: [PATCH] Add half-baked implementation for first_ and last_occurence to Event --- crates/store/Cargo.toml | 3 + crates/store/src/calendar.rs | 131 +++++++++++++++++++++++++++++++++-- 2 files changed, 130 insertions(+), 4 deletions(-) diff --git a/crates/store/Cargo.toml b/crates/store/Cargo.toml index 68276f9..0fb9e6a 100644 --- a/crates/store/Cargo.toml +++ b/crates/store/Cargo.toml @@ -25,3 +25,6 @@ toml = "0.7.6" ical = { git = "https://github.com/Peltoche/ical-rs.git", rev = "4f7aeb0", features = [ "generator", ] } +chrono = "0.4.31" +regex = "1.9.5" +lazy_static = "1.4.0" diff --git a/crates/store/src/calendar.rs b/crates/store/src/calendar.rs index 7877dd4..232b165 100644 --- a/crates/store/src/calendar.rs +++ b/crates/store/src/calendar.rs @@ -2,10 +2,19 @@ use std::io::BufReader; use anyhow::{anyhow, Result}; use async_trait::async_trait; -use ical::generator::{Emitter, IcalCalendar}; +use chrono::{Duration, NaiveDateTime, Timelike}; +use ical::{ + generator::{Emitter, IcalCalendar}, + parser::Component, +}; +use lazy_static::lazy_static; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; +lazy_static! { + static ref RE_DURATION: regex::Regex = regex::Regex::new(r"^(?[+-])?P((?P\d+)W)?((?P\d+)D)?(T((?P\d+)H)?((?P\d+)M)?((?P\d+)S)?)?$").unwrap(); +} + #[derive(Debug, Clone)] pub struct Event { uid: String, @@ -48,16 +57,22 @@ impl Serialize for Event { } impl Event { + // https://datatracker.ietf.org/doc/html/rfc4791#section-4.1 + // MUST NOT contain more than one calendar objects (VEVENT, VTODO, VJOURNAL) pub fn from_ics(uid: String, ics: String) -> Result { let mut parser = ical::IcalParser::new(BufReader::new(ics.as_bytes())); let cal = parser.next().ok_or(anyhow!("no calendar :("))??; if parser.next().is_some() { return Err(anyhow!("multiple calendars!")); } - if cal.events.len() == 2 { - return Err(anyhow!("multiple events")); + if cal.events.len() != 1 { + return Err(anyhow!("multiple or no events")); } - Ok(Self { uid, cal }) + let event = Self { uid, cal }; + // Run getters now to validate the input and ensure that they'll work later on + event.get_first_occurence()?; + event.get_last_occurence()?; + Ok(event) } pub fn get_uid(&self) -> &str { &self.uid @@ -72,6 +87,114 @@ impl Event { pub fn get_ics(&self) -> String { self.cal.generate() } + + pub fn get_first_occurence(&self) -> Result { + // This is safe since we enforce the event's existance in the constructor + let event = &self.cal.events.get(0).unwrap(); + let dtstart = event + .get_property("DTSTART") + .ok_or(anyhow!("DTSTART property missing!"))? + .value + .to_owned() + .ok_or(anyhow!("DTSTART property has no value!"))?; + parse_datetime(&dtstart) + } + + pub fn get_last_occurence(&self) -> Result { + // This is safe since we enforce the event's existance in the constructor + let event = &self.cal.events.get(0).unwrap(); + + if event.get_property("RRULE").is_some() { + // TODO: understand recurrence rules + return Err(anyhow!("event is recurring, we cannot handle that yet")); + } + + if let Some(dtend_prop) = event.get_property("DTEND") { + let dtend = dtend_prop + .value + .to_owned() + .ok_or(anyhow!("DTEND property has no value!"))?; + return parse_datetime(&dtend); + } + + if let Some(dtend_prop) = event.get_property("DURATION") { + let duration = dtend_prop + .value + .to_owned() + .ok_or(anyhow!("DURATION property has no value!"))?; + let dtstart = self.get_first_occurence()?; + return Ok(dtstart + parse_duration(&duration)?); + } + + let dtstart = self.get_first_occurence()?; + if dtstart.num_seconds_from_midnight() == 0 { + // no explicit time given => whole-day event + return Ok(dtstart + Duration::days(1)); + }; + + Err(anyhow!("help, couldn't determine any last occurence")) + } +} + +pub fn parse_duration(string: &str) -> Result { + let captures = RE_DURATION + .captures(string) + .ok_or(anyhow!("invalid duration format"))?; + + let mut duration = Duration::zero(); + if let Some(weeks) = captures.name("W") { + duration = duration + Duration::weeks(weeks.as_str().parse()?); + } + if let Some(days) = captures.name("D") { + duration = duration + Duration::days(days.as_str().parse()?); + } + if let Some(hours) = captures.name("H") { + duration = duration + Duration::hours(hours.as_str().parse()?); + } + if let Some(minutes) = captures.name("M") { + duration = duration + Duration::minutes(minutes.as_str().parse()?); + } + if let Some(seconds) = captures.name("S") { + duration = duration + Duration::seconds(seconds.as_str().parse()?); + } + if let Some(sign) = captures.name("sign") { + if sign.as_str() == "-" { + duration = -duration; + } + } + + Ok(duration) +} + +#[test] +pub fn test_parse_duration() { + assert_eq!(parse_duration("P12W").unwrap(), Duration::weeks(12)); + assert_eq!(parse_duration("P12D").unwrap(), Duration::days(12)); + assert_eq!(parse_duration("PT12H").unwrap(), Duration::hours(12)); + assert_eq!(parse_duration("PT12M").unwrap(), Duration::minutes(12)); + assert_eq!(parse_duration("PT12S").unwrap(), Duration::seconds(12)); +} + +pub fn parse_datetime(string: &str) -> Result { + // TODO: respect timezones + // + // Format: ^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})(?PZ)?$ + // if Z? + // UTC time + // else + // if TZID given? + // time in TZ + // else + // local time of attendee (can be different actual times for different attendees) + // BUT for this implementation will be UTC for now since this case is annoying + // (sabre-dav does same) + let (datetime, _tz_remainder) = NaiveDateTime::parse_and_remainder(string, "%Y%m%dT%H%M%S")?; + Ok(datetime) +} + +#[test] +fn test_parse_datetime() { + dbg!(parse_datetime("19960329T133000Z").unwrap()); } #[derive(Debug, Default, Clone, Deserialize, Serialize)]