ical: Work on calendar object data structure

This commit is contained in:
Lennart
2025-07-25 21:44:57 +02:00
parent 9910e4ee31
commit dd34dd23d1
9 changed files with 73 additions and 82 deletions

2
Cargo.lock generated
View File

@@ -1598,7 +1598,7 @@ dependencies = [
[[package]] [[package]]
name = "ical" name = "ical"
version = "0.11.0" version = "0.11.0"
source = "git+https://github.com/lennart-k/ical-rs#a911f2d500f9f422934c5ec6afc94586a8814fb5" source = "git+https://github.com/lennart-k/ical-rs#c5fa2217af23ba27ba80295a2c0eb922f08f6c97"
dependencies = [ dependencies = [
"chrono-tz", "chrono-tz",
"serde", "serde",

View File

@@ -74,10 +74,10 @@ pub async fn route_get<C: CalendarStore, S: SubscriptionStore>(
timezones.extend(object_timezones); timezones.extend(object_timezones);
ical_calendar_builder = ical_calendar_builder.add_event(event.clone()); ical_calendar_builder = ical_calendar_builder.add_event(event.clone());
} }
CalendarObjectComponent::Todo(TodoObject { todo, .. }) => { CalendarObjectComponent::Todo(TodoObject(todo)) => {
ical_calendar_builder = ical_calendar_builder.add_todo(todo.clone()); ical_calendar_builder = ical_calendar_builder.add_todo(todo.clone());
} }
CalendarObjectComponent::Journal(JournalObject { journal, .. }) => { CalendarObjectComponent::Journal(JournalObject(journal)) => {
ical_calendar_builder = ical_calendar_builder.add_journal(journal.clone()); ical_calendar_builder = ical_calendar_builder.add_journal(journal.clone());
} }
} }

View File

@@ -78,12 +78,13 @@ pub async fn put_event<C: CalendarStore>(
true true
}; };
let object = match CalendarObject::from_ics(object_id, body) { let object = match CalendarObject::from_ics(body) {
Ok(obj) => obj, Ok(obj) => obj,
Err(_) => { Err(_) => {
return Err(Error::PreconditionFailed(Precondition::ValidCalendarData)); return Err(Error::PreconditionFailed(Precondition::ValidCalendarData));
} }
}; };
assert_eq!(object.get_id(), object_id);
cal_store cal_store
.put_object(principal, calendar_id, object, overwrite) .put_object(principal, calendar_id, object, overwrite)
.await?; .await?;

View File

@@ -95,10 +95,8 @@ impl AddressObject {
let uid = format!("{}-anniversary", self.get_id()); let uid = format!("{}-anniversary", self.get_id());
let year_suffix = year.map(|year| format!(" ({year})")).unwrap_or_default(); let year_suffix = year.map(|year| format!(" ({year})")).unwrap_or_default();
Some(CalendarObject::from_ics( Some(CalendarObject::from_ics(format!(
uid.clone(), r#"BEGIN:VCALENDAR
format!(
r#"BEGIN:VCALENDAR
VERSION:2.0 VERSION:2.0
CALSCALE:GREGORIAN CALSCALE:GREGORIAN
PRODID:-//github.com/lennart-k/rustical birthday calendar//EN PRODID:-//github.com/lennart-k/rustical birthday calendar//EN
@@ -116,8 +114,7 @@ DESCRIPTION:💍 {fullname}{year_suffix}
END:VALARM END:VALARM
END:VEVENT END:VEVENT
END:VCALENDAR"#, END:VCALENDAR"#,
), ))?)
)?)
} else { } else {
None None
}, },
@@ -139,10 +136,8 @@ END:VCALENDAR"#,
let uid = format!("{}-birthday", self.get_id()); let uid = format!("{}-birthday", self.get_id());
let year_suffix = year.map(|year| format!(" ({year})")).unwrap_or_default(); let year_suffix = year.map(|year| format!(" ({year})")).unwrap_or_default();
Some(CalendarObject::from_ics( Some(CalendarObject::from_ics(format!(
uid.clone(), r#"BEGIN:VCALENDAR
format!(
r#"BEGIN:VCALENDAR
VERSION:2.0 VERSION:2.0
CALSCALE:GREGORIAN CALSCALE:GREGORIAN
PRODID:-//github.com/lennart-k/rustical birthday calendar//EN PRODID:-//github.com/lennart-k/rustical birthday calendar//EN
@@ -160,8 +155,7 @@ DESCRIPTION:🎂 {fullname}{year_suffix}
END:VALARM END:VALARM
END:VEVENT END:VEVENT
END:VCALENDAR"#, END:VCALENDAR"#,
), ))?)
)?)
} else { } else {
None None
}, },

View File

@@ -6,11 +6,12 @@ use ical::{generator::IcalEvent, parser::Component, property::Property};
use rrule::{RRule, RRuleSet}; use rrule::{RRule, RRuleSet};
use std::{collections::HashMap, str::FromStr}; use std::{collections::HashMap, str::FromStr};
#[derive(Debug, Clone)] #[derive(Debug, Clone, Default)]
pub struct EventObject { pub struct EventObject {
pub event: IcalEvent, pub event: IcalEvent,
// If a timezone is None that means that in the VCALENDAR object there's a timezone defined
// with that name but its not from the Olson DB
pub timezones: HashMap<String, Option<chrono_tz::Tz>>, pub timezones: HashMap<String, Option<chrono_tz::Tz>>,
pub(crate) ics: String,
} }
impl EventObject { impl EventObject {
@@ -239,11 +240,7 @@ END:VEVENT\r\n",
#[test] #[test]
fn test_expand_recurrence() { fn test_expand_recurrence() {
let event = CalendarObject::from_ics( let event = CalendarObject::from_ics(ICS.to_string()).unwrap();
"318ec6503573d9576818daf93dac07317058d95c".to_string(),
ICS.to_string(),
)
.unwrap();
let event = event.event().unwrap(); let event = event.event().unwrap();
let events: Vec<String> = event let events: Vec<String> = event

View File

@@ -1,7 +1,5 @@
use derive_more::From;
use ical::parser::ical::component::IcalJournal; use ical::parser::ical::component::IcalJournal;
#[derive(Debug, Clone)] #[derive(Debug, Clone, From)]
pub struct JournalObject { pub struct JournalObject(pub IcalJournal);
pub journal: IcalJournal,
pub(crate) ics: String,
}

View File

@@ -5,6 +5,7 @@ use chrono::DateTime;
use chrono::Utc; use chrono::Utc;
use derive_more::Display; use derive_more::Display;
use ical::generator::{Emitter, IcalCalendar}; use ical::generator::{Emitter, IcalCalendar};
use ical::property::Property;
use serde::Serialize; use serde::Serialize;
use sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
use std::{collections::HashMap, io::BufReader}; use std::{collections::HashMap, io::BufReader};
@@ -58,15 +59,21 @@ pub enum CalendarObjectComponent {
Journal(JournalObject), Journal(JournalObject),
} }
#[derive(Debug, Clone)] impl Default for CalendarObjectComponent {
pub struct CalendarObject { fn default() -> Self {
id: String, Self::Event(EventObject::default())
}
}
#[derive(Debug, Clone, Default)]
pub struct CalendarObject<const VERIFIED: bool = true> {
data: CalendarObjectComponent, data: CalendarObjectComponent,
cal: IcalCalendar, properties: Vec<Property>,
ics: String,
} }
impl CalendarObject { impl CalendarObject {
pub fn from_ics(object_id: String, ics: String) -> Result<Self, Error> { pub fn from_ics(ics: String) -> Result<Self, Error> {
let mut parser = ical::IcalParser::new(BufReader::new(ics.as_bytes())); let mut parser = ical::IcalParser::new(BufReader::new(ics.as_bytes()));
let cal = parser.next().ok_or(Error::MissingCalendar)??; let cal = parser.next().ok_or(Error::MissingCalendar)??;
if parser.next().is_some() { if parser.next().is_some() {
@@ -94,41 +101,23 @@ impl CalendarObject {
.map(|timezone| (timezone.get_tzid().to_owned(), (&timezone).try_into().ok())) .map(|timezone| (timezone.get_tzid().to_owned(), (&timezone).try_into().ok()))
.collect(); .collect();
if let Some(event) = cal.events.first() { let data = if let Some(event) = cal.events.into_iter().next() {
return Ok(CalendarObject { CalendarObjectComponent::Event(EventObject { event, timezones })
id: object_id, } else if let Some(todo) = cal.todos.into_iter().next() {
cal: cal.clone(), CalendarObjectComponent::Todo(todo.into())
data: CalendarObjectComponent::Event(EventObject { } else if let Some(journal) = cal.journals.into_iter().next() {
event: event.clone(), CalendarObjectComponent::Journal(journal.into())
timezones, } else {
ics, return Err(Error::InvalidData(
}), "iCalendar component type not supported :(".to_owned(),
}); ));
} };
if let Some(todo) = cal.todos.first() {
return Ok(CalendarObject {
id: object_id,
cal: cal.clone(),
data: CalendarObjectComponent::Todo(TodoObject {
todo: todo.clone(),
ics,
}),
});
}
if let Some(journal) = cal.journals.first() {
return Ok(CalendarObject {
id: object_id,
cal: cal.clone(),
data: CalendarObjectComponent::Journal(JournalObject {
journal: journal.clone(),
ics,
}),
});
}
Err(Error::InvalidData( Ok(Self {
"iCalendar component type not supported :(".to_owned(), data,
)) properties: cal.properties,
ics,
})
} }
pub fn get_data(&self) -> &CalendarObjectComponent { pub fn get_data(&self) -> &CalendarObjectComponent {
@@ -136,21 +125,22 @@ impl CalendarObject {
} }
pub fn get_id(&self) -> &str { pub fn get_id(&self) -> &str {
&self.id match &self.data {
CalendarObjectComponent::Todo(todo) => todo.0.get_uid(),
CalendarObjectComponent::Event(event) => event.event.get_uid(),
CalendarObjectComponent::Journal(journal) => journal.0.get_uid(),
}
} }
pub fn get_etag(&self) -> String { pub fn get_etag(&self) -> String {
let mut hasher = Sha256::new(); let mut hasher = Sha256::new();
hasher.update(&self.id); hasher.update(self.get_id());
hasher.update(self.get_ics()); hasher.update(self.get_ics());
format!("\"{:x}\"", hasher.finalize()) format!("\"{:x}\"", hasher.finalize())
} }
pub fn get_ics(&self) -> &str { pub fn get_ics(&self) -> &str {
match &self.data { &self.ics
CalendarObjectComponent::Todo(todo) => &todo.ics,
CalendarObjectComponent::Event(event) => &event.ics,
CalendarObjectComponent::Journal(journal) => &journal.ics,
}
} }
pub fn get_component_name(&self) -> &str { pub fn get_component_name(&self) -> &str {
@@ -194,8 +184,11 @@ impl CalendarObject {
// Only events can be expanded // Only events can be expanded
match &self.data { match &self.data {
CalendarObjectComponent::Event(event) => { CalendarObjectComponent::Event(event) => {
let mut cal = self.cal.clone(); let cal = IcalCalendar {
cal.events = event.expand_recurrence(start, end)?; properties: self.properties.clone(),
events: event.expand_recurrence(start, end)?,
..Default::default()
};
Ok(cal.generate()) Ok(cal.generate())
} }
_ => Ok(self.get_ics().to_string()), _ => Ok(self.get_ics().to_string()),

View File

@@ -1,7 +1,5 @@
use derive_more::From;
use ical::parser::ical::component::IcalTodo; use ical::parser::ical::component::IcalTodo;
#[derive(Debug, Clone)] #[derive(Debug, Clone, From)]
pub struct TodoObject { pub struct TodoObject(pub IcalTodo);
pub todo: IcalTodo,
pub(crate) ics: String,
}

View File

@@ -22,7 +22,17 @@ impl TryFrom<CalendarObjectRow> for CalendarObject {
type Error = rustical_store::Error; type Error = rustical_store::Error;
fn try_from(value: CalendarObjectRow) -> Result<Self, Self::Error> { fn try_from(value: CalendarObjectRow) -> Result<Self, Self::Error> {
Ok(CalendarObject::from_ics(value.id, value.ics)?) let object = CalendarObject::from_ics(value.ics)?;
if object.get_id() != value.id {
return Err(rustical_store::Error::IcalError(
rustical_ical::Error::InvalidData(format!(
"object_id={} and UID={} don't match",
object.get_id(),
value.id
)),
));
}
Ok(object)
} }
} }