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(url: U) -> Result>> { 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::>()?.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, RRuleSetIter<'a>)>, } impl<'a> Iterator for AppointmentsIterator<'a> { type Item = (&'a String, DateTime); fn next(&mut self) -> Option { 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 { 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 { pub start: DateTime, pub end: DateTime, } impl Appointment { pub fn with_timezone(&self, tz: &Tz2) -> Appointment { Appointment { start: self.start.with_timezone(tz), end: self.end.with_timezone(tz), } } } #[derive(Debug)] struct Event { uid: String, start: DateTime, end: DateTime, rrule: Option, recurrence_id: Option>, exdates: Vec>, } impl Event { fn parse(ical_event: IcalEvent) -> Result { 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)>>, value: &str) -> Vec> { 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::>() }