rustical_caldav/calendar/
resource.rs1use super::prop::{SupportedCalendarComponentSet, SupportedCalendarData};
2use crate::Error;
3use crate::calendar::prop::{ReportMethod, SupportedCollationSet};
4use caldata::IcalParser;
5use caldata::types::CalDateTime;
6use chrono::{DateTime, Utc};
7use derive_more::derive::{From, Into};
8use http::Uri;
9use rustical_dav::extensions::{
10 CommonPropertiesExtension, CommonPropertiesProp, SyncTokenExtension, SyncTokenExtensionProp,
11};
12use rustical_dav::namespace::{NS_CALDAV, NS_CALENDARSERVER, NS_DAV};
13use rustical_dav::privileges::UserPrivilegeSet;
14use rustical_dav::resource::{PrincipalUri, Resource, ResourceName};
15use rustical_dav::resourcetype;
16use rustical_dav::xml::{HrefElement, Resourcetype, SupportedReportSet};
17use rustical_dav_push::{DavPushExtension, DavPushExtensionProp, VapidPublicKeyB64};
18use rustical_store::Calendar;
19use rustical_store::auth::Principal;
20use rustical_xml::{EnumVariants, PropName};
21use rustical_xml::{XmlDeserialize, XmlSerialize};
22use serde::Deserialize;
23use std::borrow::Cow;
24use std::str::FromStr;
25
26#[derive(XmlDeserialize, XmlSerialize, PartialEq, Eq, Clone, EnumVariants, PropName)]
27#[xml(unit_variants_ident = "CalendarPropName")]
28pub enum CalendarProp {
29 #[xml(ns = "rustical_dav::namespace::NS_ICAL")]
31 CalendarColor(Option<String>),
32 #[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
33 CalendarDescription(Option<String>),
34 #[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
35 CalendarTimezone(Option<String>),
36 #[xml(ns = "rustical_dav::namespace::NS_CALDAV", skip_deserializing)]
38 TimezoneServiceSet(HrefElement),
39 #[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
40 CalendarTimezoneId(Option<String>),
41 #[xml(ns = "rustical_dav::namespace::NS_ICAL")]
42 CalendarOrder(Option<i64>),
43 #[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
44 SupportedCalendarComponentSet(SupportedCalendarComponentSet),
45 #[xml(ns = "rustical_dav::namespace::NS_CALDAV", skip_deserializing)]
46 SupportedCalendarData(SupportedCalendarData),
47 #[xml(ns = "rustical_dav::namespace::NS_CALDAV", skip_deserializing)]
48 SupportedCollationSet(SupportedCollationSet),
49 #[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
50 MaxResourceSize(i64),
51 #[xml(skip_deserializing)]
52 #[xml(ns = "rustical_dav::namespace::NS_DAV")]
53 SupportedReportSet(SupportedReportSet<ReportMethod>),
54 #[xml(ns = "rustical_dav::namespace::NS_CALENDARSERVER")]
55 Source(Option<HrefElement>),
56 #[xml(skip_deserializing)]
57 #[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
58 MinDateTime(String),
59 #[xml(skip_deserializing)]
60 #[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
61 MaxDateTime(String),
62}
63
64#[derive(XmlDeserialize, XmlSerialize, PartialEq, Eq, Clone, EnumVariants, PropName)]
65#[xml(unit_variants_ident = "CalendarPropWrapperName", untagged)]
66pub enum CalendarPropWrapper {
67 Calendar(CalendarProp),
68 SyncToken(SyncTokenExtensionProp),
69 DavPush(DavPushExtensionProp),
70 Common(CommonPropertiesProp),
71}
72
73#[derive(Clone, Debug, From, Into, Deserialize)]
74pub struct CalendarResource {
75 pub cal: Calendar,
76 pub read_only: bool,
77 pub vapid_pubkey: VapidPublicKeyB64,
78}
79
80impl ResourceName for CalendarResource {
81 fn get_name(&self) -> Cow<'_, str> {
82 Cow::from(&self.cal.id)
83 }
84}
85
86impl From<CalendarResource> for Calendar {
87 fn from(value: CalendarResource) -> Self {
88 value.cal
89 }
90}
91
92impl SyncTokenExtension for CalendarResource {
93 fn get_synctoken(&self) -> String {
94 self.cal.format_synctoken()
95 }
96}
97
98impl DavPushExtension for CalendarResource {
99 fn get_topic(&self) -> String {
100 self.cal.push_topic.clone()
101 }
102
103 fn get_vapid_public_key(&self) -> Option<VapidPublicKeyB64> {
104 Some(self.vapid_pubkey.clone())
105 }
106}
107
108impl Resource for CalendarResource {
109 type Prop = CalendarPropWrapper;
110 type Error = Error;
111 type Principal = Principal;
112
113 fn is_collection(&self) -> bool {
114 true
115 }
116
117 fn get_resourcetype(&self) -> Resourcetype {
118 if self.cal.subscription_url.is_none() {
119 resourcetype!((NS_DAV, "collection"), (NS_CALDAV, "calendar"),)
120 } else {
121 resourcetype!((NS_DAV, "collection"), (NS_CALENDARSERVER, "subscribed"),)
122 }
123 }
124
125 fn get_prop(
126 &self,
127 puri: &impl PrincipalUri,
128 user: &Principal,
129 prop: &CalendarPropWrapperName,
130 ) -> Result<Self::Prop, Self::Error> {
131 Ok(match prop {
132 CalendarPropWrapperName::Calendar(prop) => CalendarPropWrapper::Calendar(match prop {
133 CalendarPropName::CalendarColor => {
134 CalendarProp::CalendarColor(self.cal.meta.color.clone())
135 }
136 CalendarPropName::CalendarDescription => {
137 CalendarProp::CalendarDescription(self.cal.meta.description.clone())
138 }
139 CalendarPropName::CalendarTimezone => {
140 CalendarProp::CalendarTimezone(self.cal.timezone_id.as_ref().and_then(|tzid| {
141 vtimezones_rs::VTIMEZONES
142 .get(tzid)
143 .map(|tz| (*tz).to_string())
144 }))
145 }
146 CalendarPropName::TimezoneServiceSet => CalendarProp::TimezoneServiceSet(
148 Uri::from_static("https://www.iana.org/time-zones").into(),
149 ),
150 CalendarPropName::CalendarTimezoneId => {
151 CalendarProp::CalendarTimezoneId(self.cal.timezone_id.clone())
152 }
153 CalendarPropName::CalendarOrder => {
154 CalendarProp::CalendarOrder(Some(self.cal.meta.order))
155 }
156 CalendarPropName::SupportedCalendarComponentSet => {
157 CalendarProp::SupportedCalendarComponentSet(self.cal.components.clone().into())
158 }
159 CalendarPropName::SupportedCalendarData => {
160 CalendarProp::SupportedCalendarData(SupportedCalendarData::default())
161 }
162 CalendarPropName::SupportedCollationSet => {
163 CalendarProp::SupportedCollationSet(SupportedCollationSet::default())
164 }
165 CalendarPropName::MaxResourceSize => CalendarProp::MaxResourceSize(10_000_000),
166 CalendarPropName::SupportedReportSet => {
167 CalendarProp::SupportedReportSet(SupportedReportSet::all())
168 }
169 CalendarPropName::Source => CalendarProp::Source(
170 self.cal
171 .subscription_url
172 .as_deref()
173 .map(Uri::from_str)
174 .and_then(|res| {
175 res.map_or_else(
176 |_| {
177 tracing::error!(
178 "Invalid source url for calendar {}/{}",
179 self.cal.principal,
180 self.cal.id
181 );
182 None
183 },
184 Some,
185 )
186 })
187 .map(HrefElement::from),
188 ),
189 CalendarPropName::MinDateTime => {
190 CalendarProp::MinDateTime(CalDateTime::from(DateTime::<Utc>::MIN_UTC).format())
191 }
192 CalendarPropName::MaxDateTime => {
193 CalendarProp::MaxDateTime(CalDateTime::from(DateTime::<Utc>::MAX_UTC).format())
194 }
195 }),
196 CalendarPropWrapperName::SyncToken(prop) => {
197 CalendarPropWrapper::SyncToken(SyncTokenExtension::get_prop(self, prop)?)
198 }
199 CalendarPropWrapperName::DavPush(prop) => {
200 CalendarPropWrapper::DavPush(DavPushExtension::get_prop(self, prop)?)
201 }
202 CalendarPropWrapperName::Common(prop) => CalendarPropWrapper::Common(
203 CommonPropertiesExtension::get_prop(self, puri, user, prop)?,
204 ),
205 })
206 }
207
208 fn set_prop(&mut self, prop: Self::Prop) -> Result<(), rustical_dav::Error> {
209 match prop {
210 CalendarPropWrapper::Calendar(prop) => match prop {
211 CalendarProp::CalendarColor(color) => {
212 self.cal.meta.color = color;
213 Ok(())
214 }
215 CalendarProp::CalendarDescription(description) => {
216 self.cal.meta.description = description;
217 Ok(())
218 }
219 CalendarProp::CalendarTimezone(timezone) => {
220 if let Some(tz) = timezone {
221 let calendar = IcalParser::from_slice(tz.as_bytes())
223 .next()
224 .ok_or_else(|| {
225 rustical_dav::Error::BadRequest(
226 "No timezone data provided".to_owned(),
227 )
228 })?
229 .map_err(|_| {
230 rustical_dav::Error::BadRequest(
231 "No timezone data provided".to_owned(),
232 )
233 })?;
234
235 let timezone = calendar.vtimezones.values().next().ok_or_else(|| {
236 rustical_dav::Error::BadRequest("No timezone data provided".to_owned())
237 })?;
238 let timezone: Option<chrono_tz::Tz> = timezone.into();
239 let timezone = timezone.ok_or_else(|| {
240 rustical_dav::Error::BadRequest("No timezone data provided".to_owned())
241 })?;
242 self.cal.timezone_id = Some(timezone.name().to_owned());
243 }
244 Ok(())
245 }
246 CalendarProp::CalendarTimezoneId(timezone_id) => {
247 if let Some(tzid) = &timezone_id
248 && !vtimezones_rs::VTIMEZONES.contains_key(tzid)
249 {
250 return Err(rustical_dav::Error::BadRequest(format!(
251 "Invalid timezone-id: {tzid}"
252 )));
253 }
254 self.cal.timezone_id = timezone_id;
255 Ok(())
256 }
257 CalendarProp::CalendarOrder(order) => {
258 self.cal.meta.order = order.unwrap_or_default();
259 Ok(())
260 }
261 CalendarProp::SupportedCalendarComponentSet(comp_set) => {
262 self.cal.components = comp_set.into();
263 Ok(())
264 }
265 CalendarProp::TimezoneServiceSet(_)
266 | CalendarProp::SupportedCalendarData(_)
267 | CalendarProp::SupportedCollationSet(_)
268 | CalendarProp::MaxResourceSize(_)
269 | CalendarProp::SupportedReportSet(_)
270 | CalendarProp::Source(_)
271 | CalendarProp::MinDateTime(_)
272 | CalendarProp::MaxDateTime(_) => Err(rustical_dav::Error::PropReadOnly),
273 },
274 CalendarPropWrapper::SyncToken(prop) => SyncTokenExtension::set_prop(self, prop),
275 CalendarPropWrapper::DavPush(prop) => DavPushExtension::set_prop(self, prop),
276 CalendarPropWrapper::Common(prop) => CommonPropertiesExtension::set_prop(self, prop),
277 }
278 }
279
280 fn remove_prop(&mut self, prop: &CalendarPropWrapperName) -> Result<(), rustical_dav::Error> {
281 match prop {
282 CalendarPropWrapperName::Calendar(prop) => match prop {
283 CalendarPropName::CalendarColor => {
284 self.cal.meta.color = None;
285 Ok(())
286 }
287 CalendarPropName::CalendarDescription => {
288 self.cal.meta.description = None;
289 Ok(())
290 }
291 CalendarPropName::CalendarTimezone | CalendarPropName::CalendarTimezoneId => {
292 self.cal.timezone_id = None;
293 Ok(())
294 }
295 CalendarPropName::CalendarOrder => {
296 self.cal.meta.order = 0;
297 Ok(())
298 }
299 CalendarPropName::SupportedCalendarComponentSet => {
300 Err(rustical_dav::Error::PropReadOnly)
301 }
302 CalendarPropName::TimezoneServiceSet
303 | CalendarPropName::SupportedCalendarData
304 | CalendarPropName::SupportedCollationSet
305 | CalendarPropName::MaxResourceSize
306 | CalendarPropName::SupportedReportSet
307 | CalendarPropName::Source
308 | CalendarPropName::MinDateTime
309 | CalendarPropName::MaxDateTime => Err(rustical_dav::Error::PropReadOnly),
310 },
311 CalendarPropWrapperName::SyncToken(prop) => SyncTokenExtension::remove_prop(self, prop),
312 CalendarPropWrapperName::DavPush(prop) => DavPushExtension::remove_prop(self, prop),
313 CalendarPropWrapperName::Common(prop) => {
314 CommonPropertiesExtension::remove_prop(self, prop)
315 }
316 }
317 }
318
319 fn get_displayname(&self) -> Option<&str> {
320 self.cal.meta.displayname.as_deref()
321 }
322 fn set_displayname(&mut self, name: Option<String>) -> Result<(), rustical_dav::Error> {
323 self.cal.meta.displayname = name;
324 Ok(())
325 }
326
327 fn get_owner(&self) -> Option<&str> {
328 Some(&self.cal.principal)
329 }
330
331 fn get_user_privileges(&self, user: &Principal) -> Result<UserPrivilegeSet, Self::Error> {
332 if self.cal.subscription_url.is_some() || self.read_only {
333 return Ok(UserPrivilegeSet::owner_write_properties(
334 user.is_principal(&self.cal.principal),
335 ));
336 }
337
338 Ok(UserPrivilegeSet::owner_only(
339 user.is_principal(&self.cal.principal),
340 ))
341 }
342}
343
344#[cfg(test)]
345mod tests {
346 #[test]
347 fn test_tzdb_version() {
348 assert_eq!(
350 chrono_tz::IANA_TZDB_VERSION,
351 vtimezones_rs::IANA_TZDB_VERSION
352 );
353 }
354}