Take the pain out of keeping all your calendars together
1use std::ops::DerefMut;
2use axum::extract::Path;
3use fast_dav_rs::CalDavClient;
4use icalendar::Component;
5use leptos::prelude::ServerFnError;
6use leptos::server;
7use tokio::task::JoinSet;
8use uuid::Uuid;
9
10use crate::STATE;
11
12pub(crate) async fn route(
13 Path(id): Path<Uuid>
14) -> String {
15 events_by_calendar(id).await.unwrap()
16}
17
18#[server]
19async fn events_by_calendar(id: Uuid) -> anyhow::Result<String, ServerFnError> {
20 let state = STATE.get().expect("Failed to get STATE - is it set yet?");
21
22 let subscriptions = sqlx::query!(
23 r#"SELECT subscriptions.reference, dav_connections.url as "dav_connection_url?", dav_connections.username, dav_connections.password FROM subscriptions
24 JOIN calendars_subscriptions on subscriptions.id = calendars_subscriptions.subscriptions_id
25 JOIN calendars on calendars.id = calendars_subscriptions.calendars_id
26 LEFT JOIN dav_connections on subscriptions.dav_connection = dav_connections.id
27 WHERE calendars.id = $1"#,
28 id
29 ).fetch_all(state.sqlx_connection.lock().await.deref_mut()).await?;
30
31 let mut requests = JoinSet::new();
32
33 for subscription in subscriptions {
34 requests.spawn((async || {
35 if let (Some(dav_connection_url), username, password) = (subscription.dav_connection_url, subscription.username, subscription.password) {
36 // fast_dav_rs client doesn't follow redirects, so we must use reqwest to check first so, e.g., stalwart .well-known/caldav URLs work
37 let base_url = state.reqwest_client.head(&dav_connection_url).send().await
38 .and_then(|response| Ok(response.url().to_string()))
39 .unwrap_or_else(|_| dav_connection_url);
40
41 let Ok(client) = CalDavClient::new(&base_url, username.as_deref(), password.as_deref()) else {
42 return None
43 };
44
45 return get_dav_calendar(client, &subscription.reference).await
46 }
47 let result = state.reqwest_client.get(subscription.reference).send().await;
48
49 let Ok(response) = result else {
50 return None
51 };
52
53 let Ok(text) = response.text().await else {
54 return None
55 };
56
57 text.parse::<icalendar::Calendar>().ok()
58 })());
59 }
60
61 let calendars = requests.join_all().await;
62
63 let mut merged = icalendar::Calendar::new();
64
65 for maybe_calendar in calendars {
66 if let Some(calendar) = maybe_calendar {
67 merged.extend(calendar.components);
68 }
69 }
70
71 Ok(merged.to_string())
72}
73
74#[cfg(feature = "ssr")]
75async fn list_dav_calendars(client: &CalDavClient) -> anyhow::Result<Vec<fast_dav_rs::CalendarInfo>, anyhow::Error> {
76 let Some(principal) = client.discover_current_user_principal().await? else {
77 return Err(anyhow::anyhow!("Incompatible: CalDav server does not indicate principal"))
78 };
79
80 let home_sets = client.discover_calendar_home_set(&principal).await?;
81
82 let mut calendars = vec![];
83
84 for home_set in home_sets {
85 calendars.extend(client.list_calendars(&home_set).await?);
86 }
87
88 Ok(calendars)
89}
90
91fn get_colored_components(color: Option<&str>, calendar: icalendar::Calendar) -> Vec<icalendar::CalendarComponent> {
92 let Some(color) = calendar.property_value("COLOR").or_else(|| color) else {
93 return calendar.components
94 };
95
96 return calendar.components.iter().map(ToOwned::to_owned).map(|component| {
97 match component {
98 icalendar::CalendarComponent::Event(mut event) => {
99 if event.property_value("COLOR").is_none() {
100 event.add_property("COLOR", color);
101 }
102
103 icalendar::CalendarComponent::Event(event)
104 },
105 icalendar::CalendarComponent::Todo(mut todo) => {
106 if todo.property_value("COLOR").is_none() {
107 todo.add_property("COLOR", color);
108 }
109
110 icalendar::CalendarComponent::Todo(todo)
111 },
112 icalendar::CalendarComponent::Venue(mut venue) => {
113 if venue.property_value("COLOR").is_none() {
114 venue.add_property("COLOR", color);
115 }
116
117 icalendar::CalendarComponent::Venue(venue)
118 },
119 other => other
120 }
121 }).collect()
122}
123
124#[cfg(feature = "ssr")]
125async fn get_dav_calendar(client: fast_dav_rs::CalDavClient, reference: &str) -> Option<icalendar::Calendar> {
126 let all_calendars = list_dav_calendars(&client).await.unwrap_or_else(|_| vec![]);
127
128 let color = all_calendars
129 .iter()
130 .filter(|calendar| calendar.href == reference && calendar.color.is_some())
131 .next()
132 .and_then(|calendar| calendar.color.clone());
133
134 let Ok(result) = client.calendar_query_timerange(reference, "VEVENT", None, None, true).await else {
135 return None
136 };
137
138 let mut calendar = icalendar::Calendar::new();
139
140 for calendar_object in result {
141 let Some(calendar_data) = calendar_object.calendar_data else {
142 continue
143 };
144
145 let Ok(parsed) = calendar_data.parse::<icalendar::Calendar>() else {
146 continue
147 };
148
149 calendar.extend(get_colored_components(color.as_deref(), parsed))
150 }
151
152 Some(calendar)
153}