217 lines
5.9 KiB
Rust
217 lines
5.9 KiB
Rust
use std::collections::HashMap;
|
|
|
|
use anyhow::{Error, Result};
|
|
use bytes::Buf;
|
|
use chrono::{DateTime, Local, LocalResult, NaiveDateTime, TimeZone};
|
|
use ical::{parser::ical::component::IcalEvent, IcalParser};
|
|
use log::warn;
|
|
use reqwest::IntoUrl;
|
|
use rrule::{RRule, RRuleSet, RRuleSetIter, Tz, Unvalidated};
|
|
|
|
pub async fn fetch_next_appointment<U: IntoUrl>(url: U) -> Result<Option<Appointment<Tz>>> {
|
|
let response = reqwest::get(url).await?.bytes().await?;
|
|
|
|
let calendar = IcalParser::new(response.reader())
|
|
.next()
|
|
.ok_or_else(|| Error::msg("ical file does not contain any calendars"))??;
|
|
let events = calendar
|
|
.events
|
|
.into_iter()
|
|
.filter_map(|event| Event::parse(event).ok());
|
|
|
|
let now = Local::now();
|
|
let now = now.with_timezone(&now.timezone().into());
|
|
|
|
let mut series = HashMap::new();
|
|
let mut ends = HashMap::new();
|
|
for event in events {
|
|
ends.insert(event.uid.clone(), event.end);
|
|
if let Some(rrule) = event.rrule {
|
|
// TODO This is a workaround for https://github.com/fmeringdal/rust-rrule/issues/87
|
|
// Restore original once the bug is fixed
|
|
// let mut rrule_set = RRuleSet::new(now)
|
|
let mut rrule_set = RRuleSet::new(event.start)
|
|
.rrule(rrule.parse::<RRule<Unvalidated>>()?.validate(event.start)?);
|
|
for exdate in event.exdates {
|
|
rrule_set = rrule_set.exdate(exdate);
|
|
}
|
|
series.insert(event.uid, rrule_set);
|
|
} else if let Some(recurrence_id) = event.recurrence_id {
|
|
let uid = event.uid;
|
|
let (uid, rrule_set) = series.remove_entry(&uid).unwrap();
|
|
let rrule_set = rrule_set.exdate(recurrence_id).rdate(event.start);
|
|
series.insert(uid, rrule_set);
|
|
} else {
|
|
series.insert(event.uid, RRuleSet::new(now).rdate(event.start));
|
|
}
|
|
}
|
|
|
|
for (uid, start) in series.iter_appointments() {
|
|
if start < now {
|
|
continue;
|
|
}
|
|
|
|
let end = *ends.get(uid).unwrap();
|
|
// Move the end time to the day of the current series element
|
|
let end = start.date().and_time(end.naive_local().time()).unwrap();
|
|
|
|
return Ok(Some(Appointment { start, end }));
|
|
}
|
|
|
|
Ok(None)
|
|
}
|
|
|
|
struct AppointmentsIterator<'a> {
|
|
inner: Vec<(&'a String, DateTime<Tz>, RRuleSetIter<'a>)>,
|
|
}
|
|
|
|
impl<'a> Iterator for AppointmentsIterator<'a> {
|
|
type Item = (&'a String, DateTime<Tz>);
|
|
|
|
fn next(&mut self) -> Option<Self::Item> {
|
|
if self.inner.len() == 0 {
|
|
return None;
|
|
}
|
|
self.inner.sort_unstable_by(|a, b| b.1.cmp(&a.1));
|
|
let mut next_appointment_data = self.inner.pop().unwrap();
|
|
let uid = next_appointment_data.0;
|
|
let next_appointment = next_appointment_data.1;
|
|
if let Some(next_appointment) = next_appointment_data.2.next() {
|
|
next_appointment_data.1 = next_appointment;
|
|
self.inner.push(next_appointment_data);
|
|
}
|
|
Some((uid, next_appointment))
|
|
}
|
|
}
|
|
|
|
trait IterAppointments {
|
|
fn iter_appointments(&self) -> AppointmentsIterator<'_>;
|
|
}
|
|
|
|
impl IterAppointments for HashMap<String, RRuleSet> {
|
|
fn iter_appointments(&self) -> AppointmentsIterator {
|
|
let mut inner = vec![];
|
|
for (uid, rrule_set) in self {
|
|
let mut rrule_set_iterator = rrule_set.into_iter();
|
|
let next_appointment = rrule_set_iterator.next();
|
|
if let Some(next_appointment) = next_appointment {
|
|
inner.push((uid, next_appointment, rrule_set_iterator))
|
|
}
|
|
}
|
|
|
|
AppointmentsIterator { inner }
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct Appointment<Tz: TimeZone> {
|
|
pub start: DateTime<Tz>,
|
|
pub end: DateTime<Tz>,
|
|
}
|
|
|
|
impl<Tz: TimeZone> Appointment<Tz> {
|
|
pub fn with_timezone<Tz2: TimeZone>(&self, tz: &Tz2) -> Appointment<Tz2> {
|
|
Appointment {
|
|
start: self.start.with_timezone(tz),
|
|
end: self.end.with_timezone(tz),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct Event {
|
|
uid: String,
|
|
start: DateTime<Tz>,
|
|
end: DateTime<Tz>,
|
|
rrule: Option<String>,
|
|
recurrence_id: Option<DateTime<Tz>>,
|
|
exdates: Vec<DateTime<Tz>>,
|
|
}
|
|
|
|
impl Event {
|
|
fn parse(ical_event: IcalEvent) -> Result<Self> {
|
|
let mut uid = None;
|
|
let mut start = None;
|
|
let mut end = None;
|
|
let mut rrule = None;
|
|
let mut recurrence_id = None;
|
|
let mut exdates = vec![];
|
|
for property in ical_event.properties {
|
|
if let Some(value) = property.value {
|
|
match property.name.as_str() {
|
|
"UID" => uid = uid.or(Some(value)),
|
|
"RRULE" => rrule = rrule.or(Some(value)),
|
|
"DTSTART" => {
|
|
start = start.or(parse_dates(property.params, &value).into_iter().next())
|
|
}
|
|
"DTEND" => {
|
|
end = end.or(parse_dates(property.params, &value).into_iter().next())
|
|
}
|
|
"RECURRENCE-ID" => {
|
|
recurrence_id = recurrence_id
|
|
.or(parse_dates(property.params, &value).into_iter().next())
|
|
}
|
|
"EXDATE" => parse_dates(property.params, &value)
|
|
.into_iter()
|
|
.for_each(|date| exdates.push(date)),
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
let uid = uid.ok_or_else(|| Error::msg("Event has no uid"))?;
|
|
let start = start.ok_or_else(|| Error::msg("Event has no dtstart"))?;
|
|
let end = end.ok_or_else(|| Error::msg("Event has no dtend"))?;
|
|
Ok(Event {
|
|
uid,
|
|
start,
|
|
end,
|
|
rrule,
|
|
recurrence_id,
|
|
exdates,
|
|
})
|
|
}
|
|
}
|
|
|
|
// TODO This should return a result instead
|
|
fn parse_dates(params: Option<Vec<(String, Vec<String>)>>, value: &str) -> Vec<DateTime<Tz>> {
|
|
let params = if let Some(params) = params {
|
|
params
|
|
} else {
|
|
return vec![];
|
|
};
|
|
// Find TZID parameter and extract its singular value
|
|
let tz = params
|
|
.into_iter()
|
|
.filter(|(name, _)| name == "TZID")
|
|
.map(|(_, values)| values)
|
|
.filter(|values| values.len() == 1)
|
|
.next()
|
|
.unwrap()
|
|
.into_iter()
|
|
.next()
|
|
.unwrap();
|
|
let tz: chrono_tz::Tz = if let Ok(tz) = tz.parse() {
|
|
tz
|
|
} else {
|
|
return vec![];
|
|
};
|
|
|
|
value
|
|
.split(',')
|
|
.filter_map(|time_str| NaiveDateTime::parse_from_str(time_str, "%Y%m%dT%H%M%S").ok())
|
|
.filter_map(|datetime| match tz.from_local_datetime(&datetime) {
|
|
LocalResult::Single(datetime) => Some(datetime),
|
|
LocalResult::None => None,
|
|
LocalResult::Ambiguous(_, _) => {
|
|
warn!(
|
|
"Ignoring ambiguous datetime '{}' from timezone '{}'",
|
|
datetime,
|
|
tz.name()
|
|
);
|
|
None
|
|
}
|
|
})
|
|
.map(|datetime| datetime.with_timezone(&datetime.timezone().into()))
|
|
.collect::<Vec<_>>()
|
|
}
|