use super::EventObject; use crate::CalDateTime; use crate::Error; use chrono::DateTime; use chrono::Utc; use derive_more::Display; use ical::generator::{Emitter, IcalCalendar}; use ical::parser::ical::component::IcalJournal; use ical::parser::ical::component::IcalTimeZone; use ical::parser::ical::component::IcalTodo; use ical::property::Property; use serde::Deserialize; use serde::Serialize; use sha2::{Digest, Sha256}; use std::{collections::HashMap, io::BufReader}; #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Display)] // specified in https://datatracker.ietf.org/doc/html/rfc5545#section-3.6 pub enum CalendarObjectType { #[serde(rename = "VEVENT")] Event = 0, #[serde(rename = "VTODO")] Todo = 1, #[serde(rename = "VJOURNAL")] Journal = 2, } impl CalendarObjectType { #[must_use] pub const fn as_str(&self) -> &'static str { match self { Self::Event => "VEVENT", Self::Todo => "VTODO", Self::Journal => "VJOURNAL", } } } impl rustical_xml::ValueSerialize for CalendarObjectType { fn serialize(&self) -> String { self.as_str().to_owned() } } impl rustical_xml::ValueDeserialize for CalendarObjectType { fn deserialize(val: &str) -> std::result::Result { match ::deserialize(val)?.as_str() { "VEVENT" => Ok(Self::Event), "VTODO" => Ok(Self::Todo), "VJOURNAL" => Ok(Self::Journal), _ => Err(rustical_xml::XmlError::InvalidValue( rustical_xml::ParseValueError::Other(format!( "Invalid value '{val}', must be VEVENT, VTODO, or VJOURNAL" )), )), } } } #[derive(Debug, Clone)] pub enum CalendarObjectComponent { Event(EventObject, Vec), Todo(IcalTodo, Vec), Journal(IcalJournal, Vec), } impl CalendarObjectComponent { #[must_use] pub fn get_uid(&self) -> &str { match &self { // We've made sure before that the first component exists and all components share the // same UID Self::Todo(todo, _) => todo.get_uid(), Self::Event(event, _) => event.event.get_uid(), Self::Journal(journal, _) => journal.get_uid(), } } } impl From<&CalendarObjectComponent> for CalendarObjectType { fn from(value: &CalendarObjectComponent) -> Self { match value { CalendarObjectComponent::Event(..) => Self::Event, CalendarObjectComponent::Todo(..) => Self::Todo, CalendarObjectComponent::Journal(..) => Self::Journal, } } } impl CalendarObjectComponent { fn from_events(mut events: Vec) -> Result { let main_event = events .extract_if(.., |event| event.event.get_recurrence_id().is_none()) .next() .expect("there must be one main event"); let overrides = events; for event in &overrides { if event.get_uid() != main_event.get_uid() { return Err(Error::InvalidData( "Calendar object contains multiple UIDs".to_owned(), )); } if event.event.get_recurrence_id().is_none() { return Err(Error::InvalidData( "Calendar object can only contain one main component".to_owned(), )); } } Ok(Self::Event(main_event, overrides)) } fn from_todos(mut todos: Vec) -> Result { let main_todo = todos .extract_if(.., |todo| todo.get_recurrence_id().is_none()) .next() .expect("there must be one main event"); let overrides = todos; for todo in &overrides { if todo.get_uid() != main_todo.get_uid() { return Err(Error::InvalidData( "Calendar object contains multiple UIDs".to_owned(), )); } if todo.get_recurrence_id().is_none() { return Err(Error::InvalidData( "Calendar object can only contain one main component".to_owned(), )); } } Ok(Self::Todo(main_todo, overrides)) } fn from_journals(mut journals: Vec) -> Result { let main_journal = journals .extract_if(.., |journal| journal.get_recurrence_id().is_none()) .next() .expect("there must be one main event"); let overrides = journals; for journal in &overrides { if journal.get_uid() != main_journal.get_uid() { return Err(Error::InvalidData( "Calendar object contains multiple UIDs".to_owned(), )); } if journal.get_recurrence_id().is_none() { return Err(Error::InvalidData( "Calendar object can only contain one main component".to_owned(), )); } } Ok(Self::Journal(main_journal, overrides)) } pub fn get_first_occurence(&self) -> Result, Error> { match &self { Self::Event(main_event, overrides) => Ok(overrides .iter() .chain(std::iter::once(main_event)) .map(super::event::EventObject::get_dtstart) .collect::, _>>()? .into_iter() .flatten() .min()), _ => Ok(None), } } pub fn get_last_occurence(&self) -> Result, Error> { match &self { Self::Event(main_event, overrides) => Ok(overrides .iter() .chain(std::iter::once(main_event)) .map(super::event::EventObject::get_last_occurence) .collect::, _>>()? .into_iter() .flatten() .max()), _ => Ok(None), } } } #[derive(Debug, Clone)] pub struct CalendarObject { data: CalendarObjectComponent, properties: Vec, id: String, ics: String, vtimezones: HashMap, } impl CalendarObject { pub fn from_ics(ics: String, id: Option) -> Result { let mut parser = ical::IcalParser::new(BufReader::new(ics.as_bytes())); let cal = parser.next().ok_or(Error::MissingCalendar)??; if parser.next().is_some() { return Err(Error::InvalidData( "multiple calendars, only one allowed".to_owned(), )); } if u8::from(!cal.events.is_empty()) + u8::from(!cal.todos.is_empty()) + u8::from(!cal.journals.is_empty()) + u8::from(!cal.free_busys.is_empty()) != 1 { // https://datatracker.ietf.org/doc/html/rfc4791#section-4.1 return Err(Error::InvalidData( "iCalendar object must have exactly one component type".to_owned(), )); } let timezones: HashMap> = cal .timezones .clone() .into_iter() .map(|timezone| (timezone.get_tzid().to_owned(), (&timezone).try_into().ok())) .collect(); let vtimezones = cal .timezones .clone() .into_iter() .map(|timezone| (timezone.get_tzid().to_owned(), timezone)) .collect(); let data = if !cal.events.is_empty() { CalendarObjectComponent::from_events( cal.events .into_iter() .map(|event| EventObject { event, timezones: timezones.clone(), }) .collect(), )? } else if !cal.todos.is_empty() { CalendarObjectComponent::from_todos(cal.todos)? } else if !cal.journals.is_empty() { CalendarObjectComponent::from_journals(cal.journals)? } else { return Err(Error::InvalidData( "iCalendar component type not supported :(".to_owned(), )); }; Ok(Self { id: id.unwrap_or_else(|| data.get_uid().to_owned()), data, properties: cal.properties, ics, vtimezones, }) } #[must_use] pub const fn get_vtimezones(&self) -> &HashMap { &self.vtimezones } #[must_use] pub const fn get_data(&self) -> &CalendarObjectComponent { &self.data } #[must_use] pub fn get_uid(&self) -> &str { self.data.get_uid() } #[must_use] pub fn get_id(&self) -> &str { &self.id } #[must_use] pub fn get_etag(&self) -> String { let mut hasher = Sha256::new(); hasher.update(self.get_uid()); hasher.update(self.get_ics()); format!("\"{:x}\"", hasher.finalize()) } #[must_use] pub fn get_ics(&self) -> &str { &self.ics } #[must_use] pub fn get_component_name(&self) -> &str { self.get_object_type().as_str() } #[must_use] pub fn get_object_type(&self) -> CalendarObjectType { (&self.data).into() } pub fn get_first_occurence(&self) -> Result, Error> { self.data.get_first_occurence() } pub fn get_last_occurence(&self) -> Result, Error> { self.data.get_last_occurence() } pub fn expand_recurrence( &self, start: Option>, end: Option>, ) -> Result { // Only events can be expanded match &self.data { CalendarObjectComponent::Event(main_event, overrides) => { let cal = IcalCalendar { properties: self.properties.clone(), events: main_event.expand_recurrence(start, end, overrides)?, ..Default::default() }; Ok(cal.generate()) } _ => Ok(self.get_ics().to_string()), } } #[must_use] pub fn get_property(&self, name: &str) -> Option<&Property> { self.properties .iter() .find(|property| property.name == name) } }