Take the pain out of keeping all your calendars together
at main 5.4 kB view raw
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}