Move to own ical-rs fork and refactor timezone-related stuff

This commit is contained in:
Lennart
2025-07-25 18:21:36 +02:00
parent f9380ca7e4
commit f2899aec6b
8 changed files with 29 additions and 108 deletions

6
Cargo.lock generated
View File

@@ -1598,11 +1598,11 @@ dependencies = [
[[package]] [[package]]
name = "ical" name = "ical"
version = "0.11.0" version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "git+https://github.com/lennart-k/ical-rs#44209d8fbdde7a8f9ec42e90a91a90b2b46ed1f0"
checksum = "9b7cab7543a8b7729a19e2c04309f902861293dcdae6558dfbeb634454d279f6"
dependencies = [ dependencies = [
"chrono-tz",
"serde", "serde",
"thiserror 1.0.69", "thiserror 2.0.12",
] ]
[[package]] [[package]]

View File

@@ -95,7 +95,11 @@ strum = "0.27"
strum_macros = "0.27" strum_macros = "0.27"
serde_json = { version = "1.0", features = ["raw_value"] } serde_json = { version = "1.0", features = ["raw_value"] }
sqlx-sqlite = { version = "0.8", features = ["bundled"] } sqlx-sqlite = { version = "0.8", features = ["bundled"] }
ical = { version = "0.11", features = ["generator", "serde"] } ical = { git = "https://github.com/lennart-k/ical-rs", features = [
"generator",
"serde",
"chrono-tz",
] }
toml = "0.9" toml = "0.9"
tower = "0.5" tower = "0.5"
tower-http = { version = "0.6", features = [ tower-http = { version = "0.6", features = [

View File

@@ -63,7 +63,6 @@ pub async fn route_get<C: CalendarStore, S: SubscriptionStore>(
params: None, params: None,
}); });
} }
let mut ical_calendar = ical_calendar_builder.build();
for object in &objects { for object in &objects {
match object.get_data() { match object.get_data() {
@@ -73,17 +72,21 @@ pub async fn route_get<C: CalendarStore, S: SubscriptionStore>(
.. ..
}) => { }) => {
timezones.extend(object_timezones); timezones.extend(object_timezones);
ical_calendar.events.push(event.clone()); ical_calendar_builder = ical_calendar_builder.add_event(event.clone());
} }
CalendarObjectComponent::Todo(TodoObject { todo, .. }) => { CalendarObjectComponent::Todo(TodoObject { todo, .. }) => {
ical_calendar.todos.push(todo.clone()); ical_calendar_builder = ical_calendar_builder.add_todo(todo.clone());
} }
CalendarObjectComponent::Journal(JournalObject { journal, .. }) => { CalendarObjectComponent::Journal(JournalObject { journal, .. }) => {
ical_calendar.journals.push(journal.clone()); ical_calendar_builder = ical_calendar_builder.add_journal(journal.clone());
} }
} }
} }
let ical_calendar = ical_calendar_builder
.build()
.map_err(|parser_error| Error::IcalError(parser_error.into()))?;
let mut resp = Response::builder().status(StatusCode::OK); let mut resp = Response::builder().status(StatusCode::OK);
let hdrs = resp.headers_mut().unwrap(); let hdrs = resp.headers_mut().unwrap();
hdrs.typed_insert(ContentType::from_str("text/calendar").unwrap()); hdrs.typed_insert(ContentType::from_str("text/calendar").unwrap());

View File

@@ -1,18 +1,15 @@
use crate::Error; use crate::Error;
use crate::{CalDateTime, ComponentMut, parse_duration}; use crate::{CalDateTime, parse_duration};
use chrono::{DateTime, Duration, Utc}; use chrono::{DateTime, Duration, Utc};
use ical::{ use ical::parser::ComponentMut;
generator::IcalEvent, use ical::{generator::IcalEvent, parser::Component, property::Property};
parser::{Component, ical::component::IcalTimeZone},
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)]
pub struct EventObject { pub struct EventObject {
pub event: IcalEvent, pub event: IcalEvent,
pub timezones: HashMap<String, IcalTimeZone>, pub timezones: HashMap<String, Option<chrono_tz::Tz>>,
pub(crate) ics: String, pub(crate) ics: String,
} }
@@ -128,7 +125,7 @@ impl EventObject {
} else { } else {
date.format() date.format()
}; };
let mut ev = self.event.clone(); let mut ev = self.event.clone().mutable();
ev.remove_property("RRULE"); ev.remove_property("RRULE");
ev.remove_property("RDATE"); ev.remove_property("RDATE");
ev.remove_property("EXDATE"); ev.remove_property("EXDATE");
@@ -163,7 +160,7 @@ impl EventObject {
params: dtstart_prop.params, params: dtstart_prop.params,
}); });
} }
events.push(ev); events.push(ev.verify()?);
} }
Ok(events) Ok(events)
} else { } else {

View File

@@ -4,10 +4,7 @@ use crate::Error;
use chrono::DateTime; use chrono::DateTime;
use chrono::Utc; use chrono::Utc;
use derive_more::Display; use derive_more::Display;
use ical::{ use ical::generator::{Emitter, IcalCalendar};
generator::{Emitter, IcalCalendar},
parser::{Component, ical::component::IcalTimeZone},
};
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};
@@ -90,15 +87,11 @@ impl CalendarObject {
)); ));
} }
let timezones: HashMap<String, IcalTimeZone> = cal let timezones: HashMap<String, Option<chrono_tz::Tz>> = cal
.timezones .timezones
.clone() .clone()
.into_iter() .into_iter()
.filter_map(|timezone| { .map(|timezone| (timezone.get_tzid().to_owned(), (&timezone).try_into().ok()))
let timezone_prop = timezone.get_property("TZID")?.to_owned();
let tzid = timezone_prop.value?;
Some((tzid, timezone))
})
.collect(); .collect();
if let Some(event) = cal.events.first() { if let Some(event) = cal.events.first() {
@@ -161,11 +154,7 @@ impl CalendarObject {
} }
pub fn get_component_name(&self) -> &str { pub fn get_component_name(&self) -> &str {
match self.data { self.get_object_type().as_str()
CalendarObjectComponent::Todo(_) => "VTODO",
CalendarObjectComponent::Event(_) => "VEVENT",
CalendarObjectComponent::Journal(_) => "VJOURNAL",
}
} }
pub fn get_object_type(&self) -> CalendarObjectType { pub fn get_object_type(&self) -> CalendarObjectType {

View File

@@ -1,6 +1,3 @@
mod property_ext;
pub use property_ext::*;
mod timestamp; mod timestamp;
mod timezone; mod timezone;
pub use timestamp::*; pub use timestamp::*;

View File

@@ -1,46 +0,0 @@
use ical::{generator::IcalEvent, property::Property};
pub trait IcalProperty {
fn get_param(&self, name: &str) -> Option<Vec<&str>>;
fn get_value_type(&self) -> Option<&str>;
fn get_tzid(&self) -> Option<&str>;
}
impl IcalProperty for ical::property::Property {
fn get_param(&self, name: &str) -> Option<Vec<&str>> {
self.params
.as_ref()?
.iter()
.find(|(key, _)| name == key)
.map(|(_, value)| value.iter().map(String::as_str).collect())
}
fn get_value_type(&self) -> Option<&str> {
self.get_param("VALUE")
.and_then(|params| params.into_iter().next())
}
fn get_tzid(&self) -> Option<&str> {
self.get_param("TZID")
.and_then(|params| params.into_iter().next())
}
}
pub trait ComponentMut {
fn remove_property(&mut self, name: &str);
fn set_property(&mut self, prop: Property);
fn push_property(&mut self, prop: Property);
}
impl ComponentMut for IcalEvent {
fn remove_property(&mut self, name: &str) {
self.properties.retain(|prop| prop.name != name);
}
fn set_property(&mut self, prop: Property) {
self.remove_property(&prop.name);
self.push_property(prop);
}
fn push_property(&mut self, prop: Property) {
self.properties.push(prop);
}
}

View File

@@ -1,12 +1,8 @@
use super::timezone::CalTimezone; use super::timezone::CalTimezone;
use crate::IcalProperty;
use chrono::{DateTime, Datelike, Duration, Local, NaiveDate, NaiveDateTime, NaiveTime, Utc}; use chrono::{DateTime, Datelike, Duration, Local, NaiveDate, NaiveDateTime, NaiveTime, Utc};
use chrono_tz::Tz; use chrono_tz::Tz;
use derive_more::derive::Deref; use derive_more::derive::Deref;
use ical::{ use ical::property::Property;
parser::{Component, ical::component::IcalTimeZone},
property::Property,
};
use lazy_static::lazy_static; use lazy_static::lazy_static;
use rustical_xml::{ValueDeserialize, ValueSerialize}; use rustical_xml::{ValueDeserialize, ValueSerialize};
use std::{borrow::Cow, collections::HashMap, ops::Add}; use std::{borrow::Cow, collections::HashMap, ops::Add};
@@ -136,7 +132,7 @@ impl Add<Duration> for CalDateTime {
impl CalDateTime { impl CalDateTime {
pub fn parse_prop( pub fn parse_prop(
prop: &Property, prop: &Property,
timezones: &HashMap<String, IcalTimeZone>, timezones: &HashMap<String, Option<chrono_tz::Tz>>,
) -> Result<Self, CalDateTimeError> { ) -> Result<Self, CalDateTimeError> {
let prop_value = prop let prop_value = prop
.value .value
@@ -145,28 +141,9 @@ impl CalDateTime {
"empty property".to_owned(), "empty property".to_owned(),
))?; ))?;
// Use the TZID parameter from the property let timezone = if let Some(tzid) = prop.get_param("TZID") {
let timezone = if let Some(tzid) = prop.get_tzid() {
if let Some(timezone) = timezones.get(tzid) { if let Some(timezone) = timezones.get(tzid) {
// X-LIC-LOCATION is often used to refer to a standardised timezone from the Olson timezone.to_owned()
// database
if let Some(olson_name) = timezone
.get_property("X-LIC-LOCATION")
.map(|prop| prop.value.to_owned())
.unwrap_or_default()
{
if let Ok(tz) = olson_name.parse::<Tz>() {
Some(tz)
} else {
return Err(CalDateTimeError::InvalidOlson(olson_name));
}
} else {
// If the TZID matches a name from the Olson database (e.g. Europe/Berlin) we
// guess that we can just use it
tzid.parse::<Tz>().ok()
// TODO: If None: Too bad, we need to manually parse it
// For now it's just treated as localtime
}
} else { } else {
// TZID refers to timezone that does not exist // TZID refers to timezone that does not exist
return Err(CalDateTimeError::InvalidTZID(tzid.to_string())); return Err(CalDateTimeError::InvalidTZID(tzid.to_string()));