Support timezone ids from the Olson database

This commit is contained in:
Lennart
2024-10-08 18:06:26 +02:00
parent 26f6d7d72f
commit a75b9f213a
8 changed files with 224 additions and 76 deletions

View File

@@ -27,3 +27,4 @@ actix-web-httpauth = { workspace = true }
tracing = { workspace = true }
pbkdf2 = { workspace = true }
rand_core = { workspace = true }
chrono-tz = { workspace = true }

View File

@@ -1,31 +1,30 @@
use std::str::FromStr;
use anyhow::{anyhow, Result};
use chrono::Duration;
use ical::{generator::IcalEvent, parser::Component, property::Property};
use crate::{
timestamp::{parse_duration, CalDateTime},
Error,
};
use anyhow::{anyhow, Result};
use chrono::Duration;
use ical::{
generator::IcalEvent,
parser::{ical::component::IcalTimeZone, Component},
property::Property,
};
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct EventObject {
pub(crate) event: IcalEvent,
pub(crate) timezones: HashMap<String, IcalTimeZone>,
}
impl EventObject {
pub fn get_first_occurence(&self) -> Result<Option<CalDateTime>, Error> {
// This is safe since we enforce the event's existance in the constructor
let dtstart = if let Some(Property {
value: Some(value), ..
}) = self.event.get_property("DTSTART")
{
value
if let Some(dtstart) = self.event.get_property("DTSTART") {
CalDateTime::parse_prop(dtstart, &self.timezones)
} else {
return Ok(None);
};
Ok(Some(CalDateTime::from_str(dtstart)?))
Ok(None)
}
}
pub fn get_last_occurence(&self) -> Result<Option<CalDateTime>, Error> {
@@ -35,11 +34,8 @@ impl EventObject {
return Err(anyhow!("event is recurring, we cannot handle that yet").into());
}
if let Some(Property {
value: Some(dtend), ..
}) = self.event.get_property("DTEND")
{
return Ok(Some(CalDateTime::from_str(dtend)?));
if let Some(dtend) = self.event.get_property("DTEND") {
return CalDateTime::parse_prop(dtend, &self.timezones);
};
let duration = if let Some(Property {

View File

@@ -1,9 +1,10 @@
use super::{event::EventObject, todo::TodoObject};
use crate::{timestamp::CalDateTime, Error};
use anyhow::Result;
use ical::parser::{ical::component::IcalTimeZone, Component};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::io::BufReader;
use std::{collections::HashMap, io::BufReader};
#[derive(Debug, Clone)]
// specified in https://datatracker.ietf.org/doc/html/rfc5545#section-3.6
@@ -84,12 +85,24 @@ impl CalendarObject {
));
}
let timezones: HashMap<String, IcalTimeZone> = cal
.timezones
.clone()
.into_iter()
.filter_map(|timezone| {
let timezone_prop = timezone.get_property("TZID")?.to_owned();
let tzid = timezone_prop.value?;
Some((tzid, timezone))
})
.collect();
if let Some(event) = cal.events.first() {
return Ok(CalendarObject {
uid,
ics,
data: CalendarObjectComponent::Event(EventObject {
event: event.clone(),
timezones,
}),
});
}

View File

@@ -1,10 +1,14 @@
use std::{ops::Add, str::FromStr};
use crate::Error;
use anyhow::{anyhow, Result};
use chrono::{DateTime, Duration, NaiveDate, NaiveDateTime, NaiveTime, Utc};
use chrono_tz::Tz;
use ical::{
parser::{ical::component::IcalTimeZone, Component},
property::Property,
};
use lazy_static::lazy_static;
use serde::Deserialize;
use serde::{Deserialize, Deserializer};
use std::{collections::HashMap, ops::Add};
lazy_static! {
static ref RE_DURATION: regex::Regex = regex::Regex::new(r"^(?<sign>[+-])?P((?P<W>\d+)W)?((?P<D>\d+)D)?(T((?P<H>\d+)H)?((?P<M>\d+)M)?((?P<S>\d+)S)?)?$").unwrap();
@@ -21,19 +25,27 @@ pub enum CalDateTime {
// Form 2, example: 19980119T070000Z
Utc(DateTime<Utc>),
// Form 3, example: TZID=America/New_York:19980119T020000
// TODO: implement timezone parsing
ExplicitTZ((String, NaiveDateTime)),
// https://en.wikipedia.org/wiki/Tz_database
OlsonTZ(DateTime<Tz>),
Date(NaiveDate),
}
impl<'de> Deserialize<'de> for CalDateTime {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let input = String::deserialize(deserializer)?;
Self::from_str(&input).map_err(|_| serde::de::Error::custom("Invalid datetime format"))
}
pub fn deserialize_utc_datetime<'de, D>(deserializer: D) -> Result<Option<DateTime<Utc>>, D::Error>
where
D: Deserializer<'de>,
{
type Inner = Option<String>;
Ok(if let Some(input) = Inner::deserialize(deserializer)? {
dbg!(&input);
Some(
NaiveDateTime::parse_from_str(&input, UTC_DATE_TIME)
.map_err(|err| serde::de::Error::custom(err.to_string()))?
.and_utc(),
)
} else {
dbg!("NONE");
None
})
}
impl Add<Duration> for CalDateTime {
@@ -43,36 +55,95 @@ impl Add<Duration> for CalDateTime {
match self {
Self::Local(datetime) => Self::Local(datetime + duration),
Self::Utc(datetime) => Self::Utc(datetime + duration),
Self::ExplicitTZ((tz, datetime)) => Self::ExplicitTZ((tz, datetime + duration)),
Self::OlsonTZ(datetime) => Self::OlsonTZ(datetime + duration),
Self::Date(date) => Self::Local(date.and_time(NaiveTime::default()) + duration),
}
}
}
impl FromStr for CalDateTime {
type Err = Error;
impl CalDateTime {
pub fn parse_prop(
prop: &Property,
timezones: &HashMap<String, IcalTimeZone>,
) -> Result<Option<Self>, Error> {
let prop_value = if let Some(value) = &prop.value {
value.to_owned()
} else {
return Ok(None);
};
fn from_str(value: &str) -> Result<Self, Self::Err> {
let timezone = if let Some(tzid) = &prop
.params
.clone()
.unwrap_or_default()
.iter()
.filter(|(name, _values)| name == "TZID")
.map(|(_name, values)| values.first())
.next()
.unwrap_or_default()
{
if let Some(timezone) = timezones.get(tzid.to_owned()) {
// X-LIC-LOCATION is often used to refer to a standardised timezone from the Olson
// 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 {
// TODO: handle invalid timezone name
None
}
} else {
// No name, we would have to parse it ourselves :(
// TODO: implement
None
}
} else {
// ERROR: invalid timezone specified
// For now just assume naive datetime?
None
}
} else {
None
};
Self::parse(&prop_value, timezone).map(Some)
}
pub fn parse(value: &str, timezone: Option<Tz>) -> Result<Self, Error> {
if let Ok(datetime) = NaiveDateTime::parse_from_str(value, LOCAL_DATE_TIME) {
if let Some(timezone) = timezone {
let result = datetime.and_local_timezone(timezone);
if let Some(datetime) = result.earliest() {
return Ok(CalDateTime::OlsonTZ(datetime));
} else {
// This time does not exist because there's a gap in local time
return Err(Error::Other(anyhow!(
"Timestamp doesn't exist because of gap in local time"
)));
}
}
return Ok(CalDateTime::Local(datetime));
}
if let Ok(datetime) = NaiveDateTime::parse_from_str(value, UTC_DATE_TIME) {
return Ok(CalDateTime::Utc(datetime.and_utc()));
}
if let Ok(date) = NaiveDate::parse_from_str(value, LOCAL_DATE) {
return Ok(CalDateTime::Date(date));
}
Err(Error::Other(anyhow!("Invalid datetime format")))
}
}
impl CalDateTime {
pub fn utc(&self) -> DateTime<Utc> {
match &self {
CalDateTime::Local(local_datetime) => local_datetime.and_utc(),
CalDateTime::Utc(utc_datetime) => utc_datetime.to_owned(),
// TODO: respect timezone!
CalDateTime::ExplicitTZ((_tzid, datetime)) => datetime.and_utc(),
CalDateTime::OlsonTZ(datetime) => datetime.to_utc(),
CalDateTime::Date(date) => date.and_time(NaiveTime::default()).and_utc(),
}
}
@@ -84,13 +155,6 @@ impl From<CalDateTime> for DateTime<Utc> {
}
}
#[test]
fn test_parse_cal_datetime() {
CalDateTime::from_str("19980118T230000").unwrap();
CalDateTime::from_str("19980118T230000Z").unwrap();
CalDateTime::from_str("19980118").unwrap();
}
pub fn parse_duration(string: &str) -> Result<Duration, Error> {
let captures = RE_DURATION
.captures(string)