mirror of
https://github.com/lennart-k/rustical.git
synced 2025-12-13 22:52:22 +00:00
ical: Work on calendar object data structure
This commit is contained in:
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -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",
|
||||||
|
|||||||
@@ -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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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?;
|
||||||
|
|||||||
@@ -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
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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()),
|
||||||
|
|||||||
@@ -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,
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user