caldav: add support for calendar subscriptions

This commit is contained in:
Lennart
2024-11-11 17:31:32 +01:00
parent dc4e0c7f28
commit 1d671ad266
16 changed files with 64 additions and 23 deletions

View File

@@ -1,6 +1,6 @@
{ {
"db_name": "SQLite", "db_name": "SQLite",
"query": "SELECT principal, id, synctoken, \"order\", displayname, description, color, timezone, deleted_at\n FROM calendars\n WHERE (principal, id) = (?, ?)", "query": "SELECT principal, id, synctoken, \"order\", displayname, description, color, timezone, deleted_at, subscription_url\n FROM calendars\n WHERE (principal, id) = (?, ?)",
"describe": { "describe": {
"columns": [ "columns": [
{ {
@@ -47,6 +47,11 @@
"name": "deleted_at", "name": "deleted_at",
"ordinal": 8, "ordinal": 8,
"type_info": "Datetime" "type_info": "Datetime"
},
{
"name": "subscription_url",
"ordinal": 9,
"type_info": "Text"
} }
], ],
"parameters": { "parameters": {
@@ -60,9 +65,10 @@
true, true,
true, true,
true, true,
false, true,
true,
true true
] ]
}, },
"hash": "b66f33bc98029e9bc6427e61d15484a776e8eccb2b72a2fb2d4a5edea90067e5" "hash": "38e44b7a271d9be098f00b233100fbce750a4e167ee547abccbefd36a089f399"
} }

View File

@@ -1,6 +1,6 @@
{ {
"db_name": "SQLite", "db_name": "SQLite",
"query": "SELECT principal, id, synctoken, displayname, \"order\", description, color, timezone, deleted_at\n FROM calendars\n WHERE principal = ? AND deleted_at IS NOT NULL", "query": "SELECT principal, id, synctoken, displayname, \"order\", description, color, timezone, deleted_at, subscription_url\n FROM calendars\n WHERE principal = ? AND deleted_at IS NULL",
"describe": { "describe": {
"columns": [ "columns": [
{ {
@@ -47,6 +47,11 @@
"name": "deleted_at", "name": "deleted_at",
"ordinal": 8, "ordinal": 8,
"type_info": "Datetime" "type_info": "Datetime"
},
{
"name": "subscription_url",
"ordinal": 9,
"type_info": "Text"
} }
], ],
"parameters": { "parameters": {
@@ -60,9 +65,10 @@
false, false,
true, true,
true, true,
false, true,
true,
true true
] ]
}, },
"hash": "52fcb1f2472a6e14da26a9733b3b9e8a79354f287b3c347bc343f9747f01f134" "hash": "7d27bdb54fbc8e65e5482fa9bc974e57c1fc7abcc5c7ca6bbb4944554381285c"
} }

View File

@@ -1,6 +1,6 @@
{ {
"db_name": "SQLite", "db_name": "SQLite",
"query": "SELECT principal, id, synctoken, displayname, \"order\", description, color, timezone, deleted_at\n FROM calendars\n WHERE principal = ? AND deleted_at IS NULL", "query": "SELECT principal, id, synctoken, displayname, \"order\", description, color, timezone, deleted_at, subscription_url\n FROM calendars\n WHERE principal = ? AND deleted_at IS NOT NULL",
"describe": { "describe": {
"columns": [ "columns": [
{ {
@@ -47,6 +47,11 @@
"name": "deleted_at", "name": "deleted_at",
"ordinal": 8, "ordinal": 8,
"type_info": "Datetime" "type_info": "Datetime"
},
{
"name": "subscription_url",
"ordinal": 9,
"type_info": "Text"
} }
], ],
"parameters": { "parameters": {
@@ -60,9 +65,10 @@
false, false,
true, true,
true, true,
false, true,
true,
true true
] ]
}, },
"hash": "f387e6ef026d8314e78c0672652fb3d6876598ecde4db98d7a303c9e4c676376" "hash": "97d66b4b76bca677badd9087a6bab978a54d36e4bb12ea77a6d52b9dfa08312c"
} }

View File

@@ -79,6 +79,7 @@ pub async fn route_mkcalendar<C: CalendarStore + ?Sized>(
description: request.calendar_description, description: request.calendar_description,
deleted_at: None, deleted_at: None,
synctoken: 0, synctoken: 0,
subscription_url: None,
}; };
match store.get_calendar(&principal, &cal_id).await { match store.get_calendar(&principal, &cal_id).await {

View File

@@ -15,6 +15,7 @@ use async_trait::async_trait;
use derive_more::derive::{From, Into}; use derive_more::derive::{From, Into};
use rustical_dav::privileges::UserPrivilegeSet; use rustical_dav::privileges::UserPrivilegeSet;
use rustical_dav::resource::{Resource, ResourceService}; use rustical_dav::resource::{Resource, ResourceService};
use rustical_dav::xml::HrefElement;
use rustical_store::auth::User; use rustical_store::auth::User;
use rustical_store::{Calendar, CalendarStore}; use rustical_store::{Calendar, CalendarStore};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@@ -43,6 +44,7 @@ pub enum CalendarPropName {
SupportedReportSet, SupportedReportSet,
SyncToken, SyncToken,
Getctag, Getctag,
Source,
} }
#[derive(Default, Deserialize, Serialize, PartialEq)] #[derive(Default, Deserialize, Serialize, PartialEq)]
@@ -82,6 +84,8 @@ pub enum CalendarProp {
// CalendarServer // CalendarServer
#[serde(rename = "CS:getctag", alias = "getctag")] #[serde(rename = "CS:getctag", alias = "getctag")]
Getctag(String), Getctag(String),
#[serde(rename = "CS:source", alias = "source")]
Source(Option<HrefElement>),
#[serde(other)] #[serde(other)]
#[default] #[default]
@@ -97,8 +101,12 @@ impl Resource for CalendarResource {
type Error = Error; type Error = Error;
type PrincipalResource = PrincipalResource; type PrincipalResource = PrincipalResource;
fn get_resourcetype() -> &'static [&'static str] { fn get_resourcetype(&self) -> &'static [&'static str] {
&["collection", "C:calendar"] if self.0.subscription_url.is_none() {
&["collection", "C:calendar"]
} else {
&["collection", "CS:subscribed"]
}
} }
fn get_prop( fn get_prop(
@@ -144,6 +152,9 @@ impl Resource for CalendarResource {
} }
CalendarPropName::SyncToken => CalendarProp::SyncToken(self.0.format_synctoken()), CalendarPropName::SyncToken => CalendarProp::SyncToken(self.0.format_synctoken()),
CalendarPropName::Getctag => CalendarProp::Getctag(self.0.format_synctoken()), CalendarPropName::Getctag => CalendarProp::Getctag(self.0.format_synctoken()),
CalendarPropName::Source => {
CalendarProp::Source(self.0.subscription_url.to_owned().map(HrefElement::from))
}
}) })
} }
@@ -178,6 +189,8 @@ impl Resource for CalendarResource {
CalendarProp::SupportedReportSet(_) => Err(rustical_dav::Error::PropReadOnly), CalendarProp::SupportedReportSet(_) => Err(rustical_dav::Error::PropReadOnly),
CalendarProp::SyncToken(_) => Err(rustical_dav::Error::PropReadOnly), CalendarProp::SyncToken(_) => Err(rustical_dav::Error::PropReadOnly),
CalendarProp::Getctag(_) => Err(rustical_dav::Error::PropReadOnly), CalendarProp::Getctag(_) => Err(rustical_dav::Error::PropReadOnly),
// Converting between a calendar subscription calendar and a normal one would be weird
CalendarProp::Source(_) => Err(rustical_dav::Error::PropReadOnly),
CalendarProp::Invalid => Err(rustical_dav::Error::PropReadOnly), CalendarProp::Invalid => Err(rustical_dav::Error::PropReadOnly),
} }
} }
@@ -213,6 +226,8 @@ impl Resource for CalendarResource {
CalendarPropName::SupportedReportSet => Err(rustical_dav::Error::PropReadOnly), CalendarPropName::SupportedReportSet => Err(rustical_dav::Error::PropReadOnly),
CalendarPropName::SyncToken => Err(rustical_dav::Error::PropReadOnly), CalendarPropName::SyncToken => Err(rustical_dav::Error::PropReadOnly),
CalendarPropName::Getctag => Err(rustical_dav::Error::PropReadOnly), CalendarPropName::Getctag => Err(rustical_dav::Error::PropReadOnly),
// Converting a calendar subscription calendar into a normal one would be weird
CalendarPropName::Source => Err(rustical_dav::Error::PropReadOnly),
} }
} }
@@ -226,6 +241,7 @@ impl Resource for CalendarResource {
} }
fn get_user_privileges(&self, user: &User) -> Result<UserPrivilegeSet, Self::Error> { fn get_user_privileges(&self, user: &User) -> Result<UserPrivilegeSet, Self::Error> {
// TODO: read-only for subscription
Ok(UserPrivilegeSet::owner_only(self.0.principal == user.id)) Ok(UserPrivilegeSet::owner_only(self.0.principal == user.id))
} }
} }

View File

@@ -55,7 +55,7 @@ impl Resource for CalendarObjectResource {
type Error = Error; type Error = Error;
type PrincipalResource = PrincipalResource; type PrincipalResource = PrincipalResource;
fn get_resourcetype() -> &'static [&'static str] { fn get_resourcetype(&self) -> &'static [&'static str] {
&[] &[]
} }

View File

@@ -62,7 +62,7 @@ impl Resource for PrincipalResource {
type Error = Error; type Error = Error;
type PrincipalResource = PrincipalResource; type PrincipalResource = PrincipalResource;
fn get_resourcetype() -> &'static [&'static str] { fn get_resourcetype(&self) -> &'static [&'static str] {
&["collection", "principal"] &["collection", "principal"]
} }

View File

@@ -56,7 +56,7 @@ impl Resource for AddressObjectResource {
type Error = Error; type Error = Error;
type PrincipalResource = PrincipalResource; type PrincipalResource = PrincipalResource;
fn get_resourcetype() -> &'static [&'static str] { fn get_resourcetype(&self) -> &'static [&'static str] {
&[] &[]
} }

View File

@@ -81,7 +81,7 @@ impl Resource for AddressbookResource {
type Error = Error; type Error = Error;
type PrincipalResource = PrincipalResource; type PrincipalResource = PrincipalResource;
fn get_resourcetype() -> &'static [&'static str] { fn get_resourcetype(&self) -> &'static [&'static str] {
&["collection", "CARD:addressbook"] &["collection", "CARD:addressbook"]
} }

View File

@@ -62,7 +62,7 @@ impl Resource for PrincipalResource {
type Error = Error; type Error = Error;
type PrincipalResource = PrincipalResource; type PrincipalResource = PrincipalResource;
fn get_resourcetype() -> &'static [&'static str] { fn get_resourcetype(&self) -> &'static [&'static str] {
&["collection", "principal"] &["collection", "principal"]
} }

View File

@@ -70,7 +70,7 @@ pub trait Resource: Clone + 'static {
type Error: ResponseError + From<crate::Error>; type Error: ResponseError + From<crate::Error>;
type PrincipalResource: Resource; type PrincipalResource: Resource;
fn get_resourcetype() -> &'static [&'static str]; fn get_resourcetype(&self) -> &'static [&'static str];
fn list_props() -> Vec<&'static str> { fn list_props() -> Vec<&'static str> {
[Self::PropName::VARIANTS, CommonPropertiesPropName::VARIANTS].concat() [Self::PropName::VARIANTS, CommonPropertiesPropName::VARIANTS].concat()
@@ -84,7 +84,7 @@ pub trait Resource: Clone + 'static {
) -> Result<CommonPropertiesProp, Self::Error> { ) -> Result<CommonPropertiesProp, Self::Error> {
Ok(match prop { Ok(match prop {
CommonPropertiesPropName::Resourcetype => { CommonPropertiesPropName::Resourcetype => {
CommonPropertiesProp::Resourcetype(Resourcetype(Self::get_resourcetype())) CommonPropertiesProp::Resourcetype(Resourcetype(self.get_resourcetype()))
} }
CommonPropertiesPropName::CurrentUserPrincipal => { CommonPropertiesPropName::CurrentUserPrincipal => {
CommonPropertiesProp::CurrentUserPrincipal( CommonPropertiesProp::CurrentUserPrincipal(

View File

@@ -35,7 +35,7 @@ impl<PR: Resource> Resource for RootResource<PR> {
type Error = PR::Error; type Error = PR::Error;
type PrincipalResource = PR; type PrincipalResource = PR;
fn get_resourcetype() -> &'static [&'static str] { fn get_resourcetype(&self) -> &'static [&'static str] {
&["collection"] &["collection"]
} }

View File

@@ -83,6 +83,8 @@ pub struct MultistatusElement<PropType: Serialize, MemberPropType: Serialize> {
pub ns_caldav: &'static str, pub ns_caldav: &'static str,
#[serde(rename = "@xmlns:IC")] #[serde(rename = "@xmlns:IC")]
pub ns_ical: &'static str, pub ns_ical: &'static str,
#[serde(rename = "@xmlns:CS")]
pub ns_calendarserver: &'static str,
#[serde(rename = "@xmlns:CARD")] #[serde(rename = "@xmlns:CARD")]
pub ns_carddav: &'static str, pub ns_carddav: &'static str,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
@@ -97,6 +99,7 @@ impl<T1: Serialize, T2: Serialize> Default for MultistatusElement<T1, T2> {
ns_dav: Namespace::Dav.as_str(), ns_dav: Namespace::Dav.as_str(),
ns_caldav: Namespace::CalDAV.as_str(), ns_caldav: Namespace::CalDAV.as_str(),
ns_ical: Namespace::ICal.as_str(), ns_ical: Namespace::ICal.as_str(),
ns_calendarserver: Namespace::CServer.as_str(),
ns_carddav: Namespace::CardDAV.as_str(), ns_carddav: Namespace::CardDAV.as_str(),
sync_token: None, sync_token: None,
} }

View File

@@ -13,6 +13,7 @@ pub struct Calendar {
pub timezone: Option<String>, pub timezone: Option<String>,
pub deleted_at: Option<NaiveDateTime>, pub deleted_at: Option<NaiveDateTime>,
pub synctoken: i64, pub synctoken: i64,
pub subscription_url: Option<String>,
} }
impl Calendar { impl Calendar {

View File

@@ -8,6 +8,7 @@ CREATE TABLE calendars (
color TEXT, color TEXT,
timezone TEXT, timezone TEXT,
deleted_at DATETIME, deleted_at DATETIME,
subscription_url TEXT,
PRIMARY KEY (principal, id) PRIMARY KEY (principal, id)
); );

View File

@@ -62,7 +62,7 @@ impl CalendarStore for SqliteStore {
async fn get_calendar(&self, principal: &str, id: &str) -> Result<Calendar, Error> { async fn get_calendar(&self, principal: &str, id: &str) -> Result<Calendar, Error> {
let cal = sqlx::query_as!( let cal = sqlx::query_as!(
Calendar, Calendar,
r#"SELECT principal, id, synctoken, "order", displayname, description, color, timezone, deleted_at r#"SELECT principal, id, synctoken, "order", displayname, description, color, timezone, deleted_at, subscription_url
FROM calendars FROM calendars
WHERE (principal, id) = (?, ?)"#, WHERE (principal, id) = (?, ?)"#,
principal, principal,
@@ -77,7 +77,7 @@ impl CalendarStore for SqliteStore {
async fn get_calendars(&self, principal: &str) -> Result<Vec<Calendar>, Error> { async fn get_calendars(&self, principal: &str) -> Result<Vec<Calendar>, Error> {
let cals = sqlx::query_as!( let cals = sqlx::query_as!(
Calendar, Calendar,
r#"SELECT principal, id, synctoken, displayname, "order", description, color, timezone, deleted_at r#"SELECT principal, id, synctoken, displayname, "order", description, color, timezone, deleted_at, subscription_url
FROM calendars FROM calendars
WHERE principal = ? AND deleted_at IS NULL"#, WHERE principal = ? AND deleted_at IS NULL"#,
principal principal
@@ -91,7 +91,7 @@ impl CalendarStore for SqliteStore {
async fn get_deleted_calendars(&self, principal: &str) -> Result<Vec<Calendar>, Error> { async fn get_deleted_calendars(&self, principal: &str) -> Result<Vec<Calendar>, Error> {
let cals = sqlx::query_as!( let cals = sqlx::query_as!(
Calendar, Calendar,
r#"SELECT principal, id, synctoken, displayname, "order", description, color, timezone, deleted_at r#"SELECT principal, id, synctoken, displayname, "order", description, color, timezone, deleted_at, subscription_url
FROM calendars FROM calendars
WHERE principal = ? AND deleted_at IS NOT NULL"#, WHERE principal = ? AND deleted_at IS NOT NULL"#,
principal principal
@@ -236,6 +236,7 @@ impl CalendarStore for SqliteStore {
object: CalendarObject, object: CalendarObject,
overwrite: bool, overwrite: bool,
) -> Result<(), Error> { ) -> Result<(), Error> {
// TODO: Prevent objects from being commited to a subscription calendar
let mut tx = self.db.begin().await.map_err(crate::Error::from)?; let mut tx = self.db.begin().await.map_err(crate::Error::from)?;
let (object_id, ics) = (object.get_id(), object.get_ics()); let (object_id, ics) = (object.get_id(), object.get_ics());
@@ -255,7 +256,7 @@ impl CalendarStore for SqliteStore {
principal, principal,
cal_id, cal_id,
object_id, object_id,
ics ics,
) )
}) })
.execute(&mut *tx) .execute(&mut *tx)