caldav_utils/availability/
mod.rs1use anyhow::anyhow;
2use chrono::TimeZone;
3use icalendar::Component;
4use rrule::{RRule, Tz, Unvalidated};
5use serde_with::DurationSeconds;
6use tracing::info;
7
8use crate::caldav::{calendar::Calendar, event::Event};
9use crate::error::{CaldavError, CaldavResult};
10
11#[serde_with::serde_as]
12#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
13pub struct AvailabilityRequest {
14 pub start: chrono::DateTime<chrono::Utc>,
15 pub end: chrono::DateTime<chrono::Utc>,
16}
17
18#[serde_with::serde_as]
23#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
24pub struct AvailabilityResponse {
25 pub start: chrono::DateTime<chrono::Utc>,
26 pub end: chrono::DateTime<chrono::Utc>,
27 #[serde_as(as = "DurationSeconds<i64>")]
31 pub granularity: chrono::Duration,
32 pub matrix: Vec<bool>,
35}
36
37fn parse_datetime_from_str(tz: &Tz, datetime_str: &str) -> CaldavResult<chrono::DateTime<Tz>> {
41 if datetime_str.ends_with('Z') {
42 let datetime = tz.datetime_from_str(datetime_str, "%Y%m%dT%H%M%SZ")?;
43 Ok(datetime)
44 } else {
45 let datetime = tz.datetime_from_str(datetime_str, "%Y%m%dT%H%M%S")?;
46 Ok(datetime)
47 }
48}
49
50pub fn get_num_slots(
51 start: chrono::DateTime<chrono::Utc>,
52 end: chrono::DateTime<chrono::Utc>,
53 granularity: chrono::Duration,
54) -> usize {
55 let duration = end - start;
56 (duration.num_minutes() / granularity.num_minutes()) as usize
57}
58
59pub fn generate_matrix_no_rrule(
61 range_start: chrono::DateTime<chrono::Utc>,
63 event_start: chrono::DateTime<chrono::Utc>,
65 event_end: chrono::DateTime<chrono::Utc>,
66 num_slots: i64,
67 granularity: chrono::Duration,
68) -> CaldavResult<Vec<bool>> {
69 let mut matrix = vec![false; num_slots as usize];
70
71 tracing::debug!(
72 r#"generating matrix for event with no RRULE,
73 range_start: {range_start},
74 event_start: {event_start},
75 event_end: {event_end},
76 num_slots: {num_slots},
77 granularity: {granularity}"#,
78 );
79
80 let start_index = {
81 if event_start == range_start {
82 0
83 } else {
84 let diff = event_start - range_start;
85 let index = diff.num_minutes() / granularity.num_minutes();
86 if index < 0 {
87 0
88 } else if index >= num_slots {
89 return Ok(matrix);
90 } else {
91 index + 1
92 }
93 }
94 } as usize;
95 let end_index = {
96 if event_end == range_start {
97 0
98 } else {
99 let diff = event_end - range_start;
100 let index = diff.num_minutes() / granularity.num_minutes();
101 tracing::debug!("diff: {:#?}", diff);
102 tracing::debug!("end_index: {}", index);
103 if index + 1 > num_slots {
104 num_slots
105 } else {
106 index + 1
107 }
108 }
109 } as usize;
110 tracing::debug!("start_index: {start_index}, end_index: {end_index}",);
111
112 matrix[start_index..end_index]
113 .iter_mut()
114 .for_each(|x| *x = true);
115 tracing::debug!("matrix: {:#?}", matrix);
116 Ok(matrix)
117}
118
119fn get_rruleset(event: &icalendar::Event, tz: &Tz) -> CaldavResult<Option<rrule::RRuleSet>> {
120 let rrule_str = match event.property_value("RRULE") {
121 None => return Ok(None),
122 Some(rule) => rule,
123 };
124
125 let dtstart = {
126 let as_str = event
127 .property_value("DTSTART")
128 .ok_or_else(|| CaldavError::Anyhow(anyhow!("DTSTART not found")))?;
129 let dtstart_local = parse_datetime_from_str(tz, as_str)?;
130 Tz::UTC.from_utc_datetime(&dtstart_local.naive_utc())
131 };
132
133 let rrule: RRule<Unvalidated> = rrule_str.parse()?;
134 let rrule = rrule.build(dtstart)?;
135 Ok(Some(rrule))
136}
137
138pub fn get_event_matrix(
139 start: chrono::DateTime<chrono::Utc>,
140 end: chrono::DateTime<chrono::Utc>,
141 granularity: chrono::Duration,
142 event: &Event,
143 timezone: Option<String>,
144) -> CaldavResult<Vec<bool>> {
145 if end < start {
146 tracing::warn!("end is before start");
147 return Ok(vec![]);
148 }
149
150 let num_slots = (end - start).num_minutes() / granularity.num_minutes();
151
152 tracing::debug!("generating matrix for event: {:#?}", event);
153 let comps = &event.ical.components;
156 let event_comp = comps
158 .iter()
159 .find(|c| matches!(c, icalendar::CalendarComponent::Event(_)));
160 if event_comp.is_none() {
161 return Err(CaldavError::Anyhow(anyhow!(
162 "no event component found in event"
163 )));
164 }
165 let event = event_comp.unwrap().as_event().unwrap();
166
167 let dtstart_str = event.property_value("DTSTART").unwrap();
169 let dtend_str = event.property_value("DTEND").unwrap();
170 tracing::debug!("dtstart_str: {:#?}", dtstart_str);
171 tracing::debug!("dtend_str: {:#?}", dtend_str);
172
173 let tz = match timezone {
174 Some(tz) => {
175 let tz_str = tz.as_str();
176 match tz_str {
177 "UTC" => Tz::UTC,
178 _ => unimplemented!(),
179 }
180 }
181 _ => Tz::UTC,
182 };
183
184 let str_has_z = dtstart_str.ends_with('Z');
187 let format = if str_has_z {
188 "%Y%m%dT%H%M%SZ"
189 } else {
190 "%Y%m%dT%H%M%S"
191 };
192
193 match event.property_value("RRULE") {
194 Some(_) => generate_matrix_rrule(event, &tz, start, end, num_slots, granularity),
195 None => {
196 let dtstart_local = tz.datetime_from_str(dtstart_str, format).unwrap();
197 let dtend_local = tz.datetime_from_str(dtend_str, format).unwrap();
198 let dtstart = chrono::Utc
200 .from_local_datetime(&dtstart_local.naive_utc())
201 .unwrap();
202 let dtend = chrono::Utc
203 .from_local_datetime(&dtend_local.naive_utc())
204 .unwrap();
205 tracing::debug!("dtstart_local: {:#?}", dtstart_local);
206 tracing::debug!("dtend_local: {:#?}", dtend_local);
207 tracing::debug!("dtstart: {:#?}", dtstart);
208 tracing::debug!("dtend: {:#?}", dtend);
209 generate_matrix_no_rrule(start, dtstart, dtend, num_slots, granularity)
210 }
211 }
212}
213
214pub async fn calendar_availability(
215 client: &reqwest::Client,
216 calendar: &Calendar,
217 start: chrono::DateTime<chrono::Utc>,
218 end: chrono::DateTime<chrono::Utc>,
219 granularity: chrono::Duration,
220) -> CaldavResult<Vec<bool>> {
221 let num_slots = get_num_slots(start, end, granularity);
222 let all_false = vec![false; num_slots];
223
224 Ok(calendar
225 .get_events(client, start, end)
226 .await?
227 .iter()
228 .map(|event| get_event_matrix(start, end, granularity, event, calendar.timezone.clone()))
229 .fold(all_false, |acc: Vec<bool>, x| {
230 let x = x.unwrap();
231 acc.iter().zip(x.iter()).map(|(a, b)| *a || *b).collect()
232 }))
233}
234
235pub async fn get_availability(
236 client: &reqwest::Client,
237 availability: &Calendar,
238 booked: &Calendar,
239 start: chrono::DateTime<chrono::Utc>,
240 end: chrono::DateTime<chrono::Utc>,
241 granularity: chrono::Duration,
242) -> CaldavResult<AvailabilityResponse> {
243 let duration = end - start;
244 let num_slots = duration.num_minutes() / granularity.num_minutes();
245
246 let events = availability.get_events(client, start, end).await?;
248 info!("found {} events", events.len());
249 tracing::debug!("events: {:#?}", events);
250
251 let all_false = vec![false; num_slots as usize];
252 let matrix = events
253 .iter()
254 .map(|event| {
255 get_event_matrix(
256 start,
257 end,
258 granularity,
259 event,
260 availability.timezone.clone(),
261 )
262 })
263 .fold(all_false, |acc, x| {
264 let x = x.unwrap();
265 acc.iter().zip(x.iter()).map(|(a, b)| *a || *b).collect()
266 });
267
268 let booked_events = booked.get_events(client, start, end).await?;
271 info!("found {} booked events", booked_events.len());
272 tracing::debug!("booked_events: {:#?}", booked_events);
273
274 let booked_matrix = booked_events
275 .iter()
276 .map(|event| get_event_matrix(start, end, granularity, event, booked.timezone.clone()))
277 .fold(matrix, |acc, x| {
278 let x = x.unwrap();
279 acc.iter().zip(x.iter()).map(|(a, b)| *a && !b).collect()
280 });
281
282 Ok(AvailabilityResponse {
283 start,
284 end,
285 granularity,
286 matrix: booked_matrix,
287 })
288}
289
290pub fn generate_matrix_rrule(
291 event: &icalendar::Event,
293 tz: &Tz,
294 start: chrono::DateTime<chrono::Utc>,
296 end: chrono::DateTime<chrono::Utc>,
298 num_slots: i64,
299 granularity: chrono::Duration,
300) -> CaldavResult<Vec<bool>> {
301 let dtstart = {
302 let dtstart_str = event.property_value("DTSTART").unwrap();
303 tracing::debug!("dtstart: {:#?}", dtstart_str);
304 let dtstart_local = parse_datetime_from_str(tz, dtstart_str)?;
305 chrono::Utc
306 .from_local_datetime(&dtstart_local.naive_utc())
307 .unwrap()
308 };
309 let dtend = {
310 let dtend_str = event.property_value("DTEND").unwrap();
311 let dtend_local = parse_datetime_from_str(tz, dtend_str)?;
312 chrono::Utc
313 .from_local_datetime(&dtend_local.naive_utc())
314 .unwrap()
315 };
316
317 let tz_start = Tz::UTC.from_utc_datetime(&dtstart.naive_utc());
318
319 let range_tz_end = Tz::UTC.from_utc_datetime(&end.naive_utc());
321
322 let rrule = get_rruleset(event, tz)?.unwrap();
323
324 let (detected_events, _) = rrule.after(tz_start).before(range_tz_end).all(100);
325 tracing::debug!("detected_events: {:#?}", detected_events);
326
327 let event_duration = dtend - dtstart;
329 let event_ranges = detected_events
330 .iter()
331 .map(|e| {
332 let start = chrono::Utc.from_local_datetime(&e.naive_utc()).unwrap();
333 let end = start + event_duration;
334 (start, end)
335 })
336 .collect::<Vec<(chrono::DateTime<chrono::Utc>, chrono::DateTime<chrono::Utc>)>>();
337 tracing::debug!("event_ranges: {:#?}", event_ranges);
338
339 let final_matrix = event_ranges
342 .iter()
343 .filter_map(|(begin, end)| {
344 if begin < end && end > &start {
346 Some(generate_matrix_no_rrule(
347 start,
348 *begin,
349 *end,
350 num_slots,
351 granularity,
352 ))
353 } else {
354 None
355 }
356 })
357 .fold(vec![false; num_slots as usize], |acc, x| {
358 let x = x.unwrap();
359 acc.iter().zip(x.iter()).map(|(a, b)| *a || *b).collect()
360 });
361
362 Ok(final_matrix)
363}