diff --git a/crates/caldav/src/resources/event.rs b/crates/caldav/src/resources/event.rs index 4127fb3..6f27d6a 100644 --- a/crates/caldav/src/resources/event.rs +++ b/crates/caldav/src/resources/event.rs @@ -4,7 +4,8 @@ use anyhow::{anyhow, Result}; use async_trait::async_trait; use rustical_auth::AuthInfo; use rustical_dav::resource::Resource; -use rustical_store::calendar::{CalendarStore, Event}; +use rustical_store::calendar::CalendarStore; +use rustical_store::event::Event; use std::sync::Arc; use tokio::sync::RwLock; diff --git a/crates/caldav/src/routes/calendar.rs b/crates/caldav/src/routes/calendar.rs index af0905c..d96c2c4 100644 --- a/crates/caldav/src/routes/calendar.rs +++ b/crates/caldav/src/routes/calendar.rs @@ -11,7 +11,8 @@ use rustical_auth::{AuthInfoExtractor, CheckAuthentication}; use rustical_dav::namespace::Namespace; use rustical_dav::resource::HandlePropfind; use rustical_dav::xml_snippets::generate_multistatus; -use rustical_store::calendar::{Calendar, CalendarStore, Event}; +use rustical_store::calendar::{Calendar, CalendarStore}; +use rustical_store::event::Event; use std::sync::Arc; use tokio::sync::RwLock; diff --git a/crates/store/src/calendar.rs b/crates/store/src/calendar.rs index 204ebad..357b29c 100644 --- a/crates/store/src/calendar.rs +++ b/crates/store/src/calendar.rs @@ -1,136 +1,7 @@ -use std::io::BufReader; - -use crate::timestamps::{parse_datetime, parse_duration}; -use anyhow::{anyhow, Result}; +use crate::event::Event; +use anyhow::Result; use async_trait::async_trait; -use chrono::{Duration, NaiveDateTime, Timelike}; -use ical::{ - generator::{Emitter, IcalCalendar}, - parser::Component, -}; use serde::{Deserialize, Serialize}; -use sha2::{Digest, Sha256}; - -#[derive(Debug, Clone)] -pub struct Event { - uid: String, - cal: IcalCalendar, -} - -// Custom implementation for Event (de)serialization -impl<'de> Deserialize<'de> for Event { - fn deserialize(deserializer: D) -> std::result::Result - where - D: serde::Deserializer<'de>, - { - #[derive(Deserialize)] - struct Inner { - uid: String, - ics: String, - } - let Inner { uid, ics } = Inner::deserialize(deserializer)?; - Self::from_ics(uid, ics).map_err(serde::de::Error::custom) - } -} -impl Serialize for Event { - fn serialize(&self, serializer: S) -> std::result::Result - where - S: serde::Serializer, - { - #[derive(Serialize)] - struct Inner { - uid: String, - ics: String, - } - Inner::serialize( - &Inner { - uid: self.get_uid().to_string(), - ics: self.get_ics().to_string(), - }, - serializer, - ) - } -} - -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() != 1 { - return Err(anyhow!("multiple or no events")); - } - 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 - } - pub fn get_etag(&self) -> String { - let mut hasher = Sha256::new(); - hasher.update(&self.uid); - hasher.update(self.get_ics()); - format!("{:x}", hasher.finalize()) - } - - 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")) - } -} #[derive(Debug, Default, Clone, Deserialize, Serialize)] pub struct Calendar { diff --git a/crates/store/src/event.rs b/crates/store/src/event.rs new file mode 100644 index 0000000..a8151b7 --- /dev/null +++ b/crates/store/src/event.rs @@ -0,0 +1,131 @@ +use crate::timestamps::{parse_datetime, parse_duration}; +use anyhow::{anyhow, Result}; +use chrono::{Duration, NaiveDateTime, Timelike}; +use ical::{ + generator::{Emitter, IcalCalendar}, + parser::Component, +}; +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; +use std::io::BufReader; + +#[derive(Debug, Clone)] +pub struct Event { + uid: String, + cal: IcalCalendar, +} + +// Custom implementation for Event (de)serialization +impl<'de> Deserialize<'de> for Event { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + #[derive(Deserialize)] + struct Inner { + uid: String, + ics: String, + } + let Inner { uid, ics } = Inner::deserialize(deserializer)?; + Self::from_ics(uid, ics).map_err(serde::de::Error::custom) + } +} +impl Serialize for Event { + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + #[derive(Serialize)] + struct Inner { + uid: String, + ics: String, + } + Inner::serialize( + &Inner { + uid: self.get_uid().to_string(), + ics: self.get_ics().to_string(), + }, + serializer, + ) + } +} + +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() != 1 { + return Err(anyhow!("multiple or no events")); + } + 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 + } + pub fn get_etag(&self) -> String { + let mut hasher = Sha256::new(); + hasher.update(&self.uid); + hasher.update(self.get_ics()); + format!("{:x}", hasher.finalize()) + } + + 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")) + } +} diff --git a/crates/store/src/lib.rs b/crates/store/src/lib.rs index ee58b8f..81bc558 100644 --- a/crates/store/src/lib.rs +++ b/crates/store/src/lib.rs @@ -1,3 +1,4 @@ pub mod calendar; +pub mod event; pub mod timestamps; pub mod toml_store; diff --git a/crates/store/src/toml_store.rs b/crates/store/src/toml_store.rs index 061a748..3270d7c 100644 --- a/crates/store/src/toml_store.rs +++ b/crates/store/src/toml_store.rs @@ -1,4 +1,5 @@ -use crate::calendar::{Calendar, CalendarStore, Event}; +use crate::calendar::{Calendar, CalendarStore}; +use crate::event::Event; use anyhow::{anyhow, Result}; use async_trait::async_trait; use serde::{Deserialize, Serialize};