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 comp: Option<Comp>,
232}
233impl SupportedCalendarComponentSet {
234 fn supports_events(&self) -> bool {
235 self.comp.as_ref().is_some_and(|v| v.name == "VEVENT")
236 }
237}
238
239#[derive(Debug, Deserialize)]
240struct Comp {
241 #[serde(rename = "@name", default)]
242 name: String,
243}
244
245fn parse_href(multi_status: Multistatus, base_url: Url) -> Result<Url, CalendarError> {
246 let props = multi_status
247 .responses
248 .into_iter()
249 .flat_map(|r| r.valid_props().into_iter())
250 .next();
251 match props.ok_or_else(|| CalendarError::Parsing("Property not found".into()))? {
252 PropValue::CurrentUserPrincipal(href) | PropValue::CalendarHomeSet(href) => base_url
253 .join(&href.href)
254 .map_err(|e| CalendarError::Parsing(e.to_string())),
255 _ => Err(CalendarError::Parsing("Invalid property".into())),
256 }
257}
258
259fn parse_calendars(
260 multi_status: Multistatus,
261 base_url: Url,
262) -> Result<Vec<Calendar>, CalendarError> {
263 let mut result = vec![];
264 for response in multi_status.responses {
265 let mut is_calendar = false;
266 let mut supports_events = false;
267 let mut name = None;
268 let href = response.href.clone();
269 for prop in response.valid_props() {
270 match prop {
271 PropValue::SupportedCalendarComponentSet(comp) => {
272 supports_events = comp.supports_events();
273 }
274 PropValue::DisplayName(display_name) => name = Some(display_name),
275 PropValue::ResourceType(ty) => is_calendar = ty.is_calendar(),
276 _ => {}
277 }
278 }
279 if is_calendar && supports_events {
280 if let Some(name) = name {
281 result.push(Calendar {
282 name,
283 url: base_url
284 .join(&href)
285 .map_err(|_| CalendarError::Parsing("Malformed calendar url".into()))?,
286 });
287 }
288 }
289 }
290 Ok(result)
291}
292
293fn parse_events(multi_status: Multistatus) -> Result<Vec<Event>, CalendarError> {
294 let mut result = vec![];
295 for response in multi_status.responses {
296 for prop in response.valid_props() {
297 if let PropValue::CalendarData(data) = prop {
298 let calendar =
299 icalendar::Calendar::from_str(&data).map_err(CalendarError::Parsing)?;
300 for component in calendar.components {
301 if let icalendar::CalendarComponent::Event(event) = component {
302 let start_at = event.get_start().and_then(|d| match d {
303 icalendar::DatePerhapsTime::DateTime(dt) => dt.try_into_utc(),
304 icalendar::DatePerhapsTime::Date(d) => d
305 .and_hms_opt(0, 0, 0)
306 .and_then(|d| d.and_local_timezone(Local).earliest())
307 .map(|d| d.to_utc()),
308 });
309 let end_at = event.get_end().and_then(|d| match d {
310 icalendar::DatePerhapsTime::DateTime(dt) => dt.try_into_utc(),
311 icalendar::DatePerhapsTime::Date(d) => d
312 .and_hms_opt(23, 59, 59)
313 .and_then(|d| d.and_local_timezone(Local).earliest())
314 .map(|d| d.to_utc()),
315 });
316 result.push(Event {
317 uid: event.get_uid().map(Into::into),
318 summary: event.get_summary().map(Into::into),
319 description: event.get_description().map(Into::into),
320 location: event.get_location().map(Into::into),
321 url: event.get_url().map(Into::into),
322 start_at,
323 end_at,
324 });
325 }
326 }
327 }
328 }
329 }
330 Ok(result)
331}
332
333static CURRENT_USER_PRINCIPAL: &str = r#"<d:propfind xmlns:d="DAV:">
334 <d:prop>
335 <d:current-user-principal />
336 </d:prop>
337 </d:propfind>"#;
338
339static CALENDAR_HOME_SET: &str = r#"<d:propfind xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav" >
340 <d:prop>
341 <c:calendar-home-set />
342 </d:prop>
343 </d:propfind>"#;
344
345static CALENDAR_REQUEST: &str = r#"<d:propfind xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav" >
346 <d:prop>
347 <d:displayname />
348 <d:resourcetype />
349 <c:supported-calendar-component-set />
350 </d:prop>
351 </d:propfind>"#;
352
353pub fn calendar_events_request(start: DateTime<Utc>, end: DateTime<Utc>) -> String {
354 const DATE_FORMAT: &str = "%Y%m%dT%H%M%SZ";
355 let start = start.format(DATE_FORMAT);
356 let end = end.format(DATE_FORMAT);
357 format!(
358 r#"<?xml version="1.0" encoding="UTF-8"?>
359 <c:calendar-query xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav">
360 <d:prop>
361 <c:calendar-data/>
362 </d:prop>
363 <c:filter>
364 <c:comp-filter name="VCALENDAR">
365 <c:comp-filter name="VEVENT">
366 <c:time-range start="{start}" end="{end}" />
367 </c:comp-filter>
368 </c:comp-filter>
369 </c:filter>
370 </c:calendar-query>"#
371 )
372}