From 41039242eeda72a15427271d32c6358ca0454d4c Mon Sep 17 00:00:00 2001 From: Lennart <18233294+lennart-k@users.noreply.github.com> Date: Wed, 11 Jun 2025 00:17:57 +0200 Subject: [PATCH] Some work on caldav imports --- crates/caldav/src/calendar/methods/get.rs | 7 ++ crates/caldav/src/calendar/methods/mod.rs | 1 + crates/caldav/src/calendar/methods/put.rs | 101 +++++++++++++++++++++ crates/store/src/calendar_store.rs | 7 ++ crates/store/src/contact_birthday_store.rs | 9 ++ crates/store_sqlite/src/calendar_store.rs | 27 ++++++ 6 files changed, 152 insertions(+) create mode 100644 crates/caldav/src/calendar/methods/put.rs diff --git a/crates/caldav/src/calendar/methods/get.rs b/crates/caldav/src/calendar/methods/get.rs index f24f67d..48a2181 100644 --- a/crates/caldav/src/calendar/methods/get.rs +++ b/crates/caldav/src/calendar/methods/get.rs @@ -58,6 +58,13 @@ pub async fn route_get( params: None, }); } + if calendar.color.is_some() { + ical_calendar_builder = ical_calendar_builder.set(Property { + name: "X-RUSTICAL-COLOR".to_owned(), + value: calendar.color, + params: None, + }); + } let mut ical_calendar = ical_calendar_builder.build(); for object in &objects { diff --git a/crates/caldav/src/calendar/methods/mod.rs b/crates/caldav/src/calendar/methods/mod.rs index 3acf321..32f58f2 100644 --- a/crates/caldav/src/calendar/methods/mod.rs +++ b/crates/caldav/src/calendar/methods/mod.rs @@ -1,4 +1,5 @@ pub mod mkcalendar; // pub mod post; pub mod get; +pub mod put; pub mod report; diff --git a/crates/caldav/src/calendar/methods/put.rs b/crates/caldav/src/calendar/methods/put.rs new file mode 100644 index 0000000..d26f446 --- /dev/null +++ b/crates/caldav/src/calendar/methods/put.rs @@ -0,0 +1,101 @@ +use std::collections::HashMap; + +use crate::calendar::prop::SupportedCalendarComponent; +use crate::calendar::{self, CalendarResourceService}; +use crate::{Error, calendar_set}; +use axum::{ + extract::{Path, State}, + response::{IntoResponse, Response}, +}; +use http::StatusCode; +use ical::generator::Emitter; +use ical::parser::ical::component::IcalTimeZone; +use ical::{IcalParser, parser::Component}; +use rustical_ical::CalendarObjectType; +use rustical_store::{Calendar, CalendarStore, SubscriptionStore, auth::User}; +use tracing::instrument; + +#[instrument(skip(cal_store))] +pub async fn route_put( + Path((principal, cal_id)): Path<(String, String)>, + State(CalendarResourceService { cal_store, .. }): State>, + user: User, + body: String, +) -> Result { + if !user.is_principal(&principal) { + return Err(crate::Error::Unauthorized); + } + + let mut parser = IcalParser::new(body.as_bytes()); + let cal = parser + .next() + .ok_or(rustical_ical::Error::MissingCalendar)? + .map_err(rustical_ical::Error::from)?; + if parser.next().is_some() { + return Err(rustical_ical::Error::InvalidData( + "multiple calendars, only one allowed".to_owned(), + ) + .into()); + } + if !cal.alarms.is_empty() || !cal.free_busys.is_empty() { + return Err(rustical_ical::Error::InvalidData( + "Importer does not support VALARM and VFREEBUSY components".to_owned(), + ) + .into()); + } + + let mut objects = vec![]; + for event in cal.events {} + for todo in cal.todos {} + for journal in cal.journals {} + + let timezones: HashMap = cal + .timezones + .clone() + .into_iter() + .filter_map(|timezone| { + let timezone_prop = timezone.get_property("TZID")?.to_owned(); + let tzid = timezone_prop.value?; + Some((tzid, timezone)) + }) + .collect(); + + let displayname = cal.get_property("X-WR-CALNAME").and_then(|prop| prop.value); + let description = cal.get_property("X-WR-CALDESC").and_then(|prop| prop.value); + let color = cal + .get_property("X-RUSTICAL-COLOR") + .and_then(|prop| prop.value); + let timezone_id = cal + .get_property("X-WR-TIMEZONE") + .and_then(|prop| prop.value); + let timezone = timezone_id + .and_then(|tzid| timezones.get(&tzid)) + .map(|timezone| timezone.generate()); + + let mut components = vec![CalendarObjectType::Event, CalendarObjectType::Todo]; + if !cal.journals.is_empty() { + components.push(CalendarObjectType::Journal); + } + + let calendar = Calendar { + principal: principal.clone(), + id: cal_id, + displayname, + description, + color, + timezone_id, + timezone, + components, + subscription_url: None, + push_topic: uuid::Uuid::new_v4().to_string(), + synctoken: 0, + deleted_at: None, + order: 0, + }; + + cal_store + .import_calendar(&principal, calendar, objects) + .await?; + + Ok(StatusCode::CREATED.into_response()) +} diff --git a/crates/store/src/calendar_store.rs b/crates/store/src/calendar_store.rs index aabaf07..d3fa3a1 100644 --- a/crates/store/src/calendar_store.rs +++ b/crates/store/src/calendar_store.rs @@ -81,4 +81,11 @@ pub trait CalendarStore: Send + Sync + 'static { ) -> Result<(), Error>; fn is_read_only(&self) -> bool; + + async fn import_calendar( + &self, + principal: &str, + calendar: Calendar, + objects: Vec, + ) -> Result<(), Error>; } diff --git a/crates/store/src/contact_birthday_store.rs b/crates/store/src/contact_birthday_store.rs index 7d7d881..75c9dc1 100644 --- a/crates/store/src/contact_birthday_store.rs +++ b/crates/store/src/contact_birthday_store.rs @@ -158,4 +158,13 @@ impl CalendarStore for ContactBirthdayStore { fn is_read_only(&self) -> bool { true } + + async fn import_calendar( + &self, + _principal: &str, + _calendar: Calendar, + _objects: Vec, + ) -> Result<(), Error> { + Err(Error::ReadOnly) + } } diff --git a/crates/store_sqlite/src/calendar_store.rs b/crates/store_sqlite/src/calendar_store.rs index e5d3e05..23a2856 100644 --- a/crates/store_sqlite/src/calendar_store.rs +++ b/crates/store_sqlite/src/calendar_store.rs @@ -673,6 +673,33 @@ impl CalendarStore for SqliteCalendarStore { fn is_read_only(&self) -> bool { false } + + #[instrument(skip(calendar, objects))] + async fn import_calendar( + &self, + principal: &str, + calendar: Calendar, + objects: Vec, + ) -> Result<(), Error> { + let mut tx = self.db.begin().await.map_err(crate::Error::from)?; + + let cal_id = calendar.id.clone(); + Self::_insert_calendar(&mut *tx, calendar).await?; + + for object in objects { + Self::_put_object( + &mut *tx, + principal.to_owned(), + cal_id.clone(), + object, + false, + ) + .await?; + } + + tx.commit().await.map_err(crate::Error::from)?; + Ok(()) + } } // Logs an operation to the events