i3status_rs/blocks/calendar/
caldav.rs1use std::{str::FromStr as _, time::Duration, vec};
2
3use chrono::{DateTime, Local, Utc};
4use icalendar::{Component as _, EventLike as _};
5use reqwest::{
6 self, ClientBuilder, Method, Url,
7 header::{CONTENT_TYPE, HeaderMap, HeaderValue},
8};
9use serde::Deserialize;
10
11use super::{
12 CalendarError,
13 auth::{Auth, Authorize},
14};
15
16#[derive(Clone, Debug)]
17pub struct Event {
18 pub uid: Option<String>,
19 pub summary: Option<String>,
20 pub description: Option<String>,
21 pub location: Option<String>,
22 pub url: Option<String>,
23 pub start_at: Option<DateTime<Utc>>,
24 pub end_at: Option<DateTime<Utc>>,
25}
26
27#[derive(Deserialize, Debug)]
28pub struct Calendar {
29 pub url: Url,
30 pub name: String,
31}
32
33pub struct Client {
34 url: Url,
35 client: reqwest::Client,
36 auth: Auth,
37}
38
39impl Client {
40 pub fn new(url: Url, auth: Auth) -> Self {
41 let mut headers = HeaderMap::new();
42 headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/xml"));
43 Self {
44 url,
45 client: ClientBuilder::new()
46 .timeout(Duration::from_secs(10))
47 .default_headers(headers)
48 .build()
49 .expect("A valid http client"),
50 auth,
51 }
52 }
53 async fn propfind_request(
54 &mut self,
55 url: Url,
56 depth: usize,
57 body: String,
58 ) -> Result<Multistatus, CalendarError> {
59 let request = self
60 .client
61 .request(Method::from_str("PROPFIND").expect("A valid method"), url)
62 .body(body.clone())
63 .headers(self.auth.headers().await)
64 .header("Depth", depth)
65 .build()
66 .expect("A valid propfind request");
67 self.call(request).await
68 }
69
70 async fn report_request(
71 &mut self,
72 url: Url,
73 depth: usize,
74 body: String,
75 ) -> Result<Multistatus, CalendarError> {
76 let request = self
77 .client
78 .request(Method::from_str("REPORT").expect("A valid method"), url)
79 .body(body)
80 .headers(self.auth.headers().await)
81 .header("Depth", depth)
82 .build()
83 .expect("A valid report request");
84 self.call(request).await
85 }
86
87 async fn call(&mut self, request: reqwest::Request) -> Result<Multistatus, CalendarError> {
88 let mut retries = 0;
89 loop {
90 let result = self
91 .client
92 .execute(request.try_clone().expect("Request to be cloneable"))
93 .await?;
94 match result.error_for_status() {
95 Err(err) if retries == 0 => {
96 self.auth.handle_error(err).await?;
97 retries += 1;
98 }
99 Err(err) => return Err(CalendarError::Http(err)),
100 Ok(result) => return Ok(quick_xml::de::from_str(result.text().await?.as_str())?),
101 };
102 }
103 }
104
105 async fn user_principal_url(&mut self) -> Result<Url, CalendarError> {
106 let multi_status = self
107 .propfind_request(self.url.clone(), 1, CURRENT_USER_PRINCIPAL.into())
108 .await?;
109 parse_href(multi_status, self.url.clone())
110 }
111
112 async fn home_set_url(&mut self, user_principal_url: Url) -> Result<Url, CalendarError> {
113 let multi_status = self
114 .propfind_request(user_principal_url, 0, CALENDAR_HOME_SET.into())
115 .await?;
116 parse_href(multi_status, self.url.clone())
117 }
118
119 async fn calendars_query(&mut self, home_set_url: Url) -> Result<Vec<Calendar>, CalendarError> {
120 let multi_status = self
121 .propfind_request(home_set_url, 1, CALENDAR_REQUEST.into())
122 .await?;
123 parse_calendars(multi_status, self.url.clone())
124 }
125
126 pub async fn calendars(&mut self) -> Result<Vec<Calendar>, CalendarError> {
127 let user_principal_url = self.user_principal_url().await?;
128 let home_set_url = self.home_set_url(user_principal_url).await?;
129 self.calendars_query(home_set_url).await
130 }
131
132 pub async fn events(
133 &mut self,
134 calendar: &Calendar,
135 start: DateTime<Utc>,
136 end: DateTime<Utc>,
137 ) -> Result<Vec<Event>, CalendarError> {
138 let multi_status = self
139 .report_request(calendar.url.clone(), 1, calendar_events_request(start, end))
140 .await?;
141 parse_events(multi_status)
142 }
143
144 pub async fn authorize(&mut self) -> Result<Authorize, CalendarError> {
145 self.auth.authorize().await
146 }
147
148 pub async fn ask_user(&mut self, authorize: Authorize) -> Result<(), CalendarError> {
149 match authorize {
150 Authorize::Completed => Ok(()),
151 Authorize::AskUser(authorize_url) => self.auth.ask_user(authorize_url).await,
152 }
153 }
154}
155
156#[derive(Debug, Deserialize)]
157#[serde(rename = "multistatus")]
158struct Multistatus {
159 #[serde(rename = "response", default)]
160 responses: Vec<Response>,
161}
162
163#[derive(Debug, Deserialize)]
164struct Response {
165 href: String,
166 #[serde(rename = "propstat", default)]
167 propstats: Vec<Propstat>,
168}
169
170impl Response {
171 fn valid_props(self) -> Vec<PropValue> {
172 self.propstats
173 .into_iter()
174 .filter(|p| p.status.contains("200"))
175 .flat_map(|p| p.prop.values.into_iter())
176 .collect()
177 }
178}
179
180#[derive(Debug, Deserialize)]
181struct Propstat {
182 status: String,
183 prop: Prop,
184}
185
186#[derive(Debug, Deserialize)]
187struct Prop {
188 #[serde(rename = "$value")]
189 pub values: Vec<PropValue>,
190}
191
192#[derive(Debug, Deserialize)]
193#[serde(rename_all = "kebab-case")]
194enum PropValue {
195 CurrentUserPrincipal(HrefProperty),
196 CalendarHomeSet(HrefProperty),
197 SupportedCalendarComponentSet(SupportedCalendarComponentSet),
198 #[serde(rename = "displayname")]
199 DisplayName(String),
200 #[serde(rename = "resourcetype")]
201 ResourceType(ResourceTypes),
202 CalendarData(String),
203}
204
205#[derive(Debug, Deserialize)]
206pub struct HrefProperty {
207 href: String,
208}
209
210#[derive(Debug, Deserialize)]
211struct ResourceTypes {
212 #[serde(rename = "$value")]
213 pub values: Vec<ResourceType>,
214}
215
216impl ResourceTypes {
217 fn is_calendar(&self) -> bool {
218 self.values.contains(&ResourceType::Calendar)
219 }
220}
221#[derive(Debug, Deserialize, PartialEq)]
222#[serde(rename_all = "kebab-case")]
223enum ResourceType {
224 Calendar,
225 #[serde(other)]
226 Unsupported,
227}
228
229#[derive(Debug, Deserialize)]
230struct SupportedCalendarComponentSet {
231 #[serde(rename = "$value", default)]
232 pub values: Vec<Comp>,
233}
234impl SupportedCalendarComponentSet {
235 fn supports_events(&self) -> bool {
236 self.values.iter().any(|v| v.name == "VEVENT")
237 }
238}
239
240#[derive(Debug, Deserialize)]
241struct Comp {
242 #[serde(rename = "@name", default)]
243 name: String,
244}
245
246fn parse_href(multi_status: Multistatus, base_url: Url) -> Result<Url, CalendarError> {
247 let props = multi_status
248 .responses
249 .into_iter()
250 .flat_map(|r| r.valid_props().into_iter())
251 .next();
252 match props.ok_or_else(|| CalendarError::Parsing("Property not found".into()))? {
253 PropValue::CurrentUserPrincipal(href) | PropValue::CalendarHomeSet(href) => base_url
254 .join(&href.href)
255 .map_err(|e| CalendarError::Parsing(e.to_string())),
256 _ => Err(CalendarError::Parsing("Invalid property".into())),
257 }
258}
259
260fn parse_calendars(
261 multi_status: Multistatus,
262 base_url: Url,
263) -> Result<Vec<Calendar>, CalendarError> {
264 let mut result = vec![];
265 for response in multi_status.responses {
266 let mut is_calendar = false;
267 let mut supports_events = false;
268 let mut name = None;
269 let href = response.href.clone();
270 for prop in response.valid_props() {
271 match prop {
272 PropValue::SupportedCalendarComponentSet(comp) => {
273 supports_events = comp.supports_events();
274 }
275 PropValue::DisplayName(display_name) => name = Some(display_name),
276 PropValue::ResourceType(ty) => is_calendar = ty.is_calendar(),
277 _ => {}
278 }
279 }
280 if is_calendar
281 && supports_events
282 && let Some(name) = name
283 {
284 result.push(Calendar {
285 name,
286 url: base_url
287 .join(&href)
288 .map_err(|_| CalendarError::Parsing("Malformed calendar url".into()))?,
289 });
290 }
291 }
292 Ok(result)
293}
294
295fn parse_events(multi_status: Multistatus) -> Result<Vec<Event>, CalendarError> {
296 let mut result = vec![];
297 for response in multi_status.responses {
298 for prop in response.valid_props() {
299 if let PropValue::CalendarData(data) = prop {
300 let calendar =
301 icalendar::Calendar::from_str(&data).map_err(CalendarError::Parsing)?;
302 for component in calendar.components {
303 if let icalendar::CalendarComponent::Event(event) = component {
304 let start_at = event.get_start().and_then(|d| match d {
305 icalendar::DatePerhapsTime::DateTime(dt) => dt.try_into_utc(),
306 icalendar::DatePerhapsTime::Date(d) => d
307 .and_hms_opt(0, 0, 0)
308 .and_then(|d| d.and_local_timezone(Local).earliest())
309 .map(|d| d.to_utc()),
310 });
311 let end_at = event.get_end().and_then(|d| match d {
312 icalendar::DatePerhapsTime::DateTime(dt) => dt.try_into_utc(),
313 icalendar::DatePerhapsTime::Date(d) => d
314 .and_hms_opt(23, 59, 59)
315 .and_then(|d| d.and_local_timezone(Local).earliest())
316 .map(|d| d.to_utc()),
317 });
318 result.push(Event {
319 uid: event.get_uid().map(Into::into),
320 summary: event.get_summary().map(Into::into),
321 description: event.get_description().map(Into::into),
322 location: event.get_location().map(Into::into),
323 url: event.get_url().map(Into::into),
324 start_at,
325 end_at,
326 });
327 }
328 }
329 }
330 }
331 }
332 Ok(result)
333}
334
335static CURRENT_USER_PRINCIPAL: &str = r#"<d:propfind xmlns:d="DAV:">
336 <d:prop>
337 <d:current-user-principal />
338 </d:prop>
339 </d:propfind>"#;
340
341static CALENDAR_HOME_SET: &str = r#"<d:propfind xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav" >
342 <d:prop>
343 <c:calendar-home-set />
344 </d:prop>
345 </d:propfind>"#;
346
347static CALENDAR_REQUEST: &str = r#"<d:propfind xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav" >
348 <d:prop>
349 <d:displayname />
350 <d:resourcetype />
351 <c:supported-calendar-component-set />
352 </d:prop>
353 </d:propfind>"#;
354
355pub fn calendar_events_request(start: DateTime<Utc>, end: DateTime<Utc>) -> String {
356 const DATE_FORMAT: &str = "%Y%m%dT%H%M%SZ";
357 let start = start.format(DATE_FORMAT);
358 let end = end.format(DATE_FORMAT);
359 format!(
360 r#"<?xml version="1.0" encoding="UTF-8"?>
361 <c:calendar-query xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav">
362 <d:prop>
363 <c:calendar-data/>
364 </d:prop>
365 <c:filter>
366 <c:comp-filter name="VCALENDAR">
367 <c:comp-filter name="VEVENT">
368 <c:time-range start="{start}" end="{end}" />
369 </c:comp-filter>
370 </c:comp-filter>
371 </c:filter>
372 </c:calendar-query>"#
373 )
374}