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",
"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": {
"columns": [
{
@@ -47,6 +47,11 @@
"name": "deleted_at",
"ordinal": 8,
"type_info": "Datetime"
},
{
"name": "subscription_url",
"ordinal": 9,
"type_info": "Text"
}
],
"parameters": {
@@ -60,9 +65,10 @@
true,
true,
true,
false,
true,
true,
true
]
},
"hash": "b66f33bc98029e9bc6427e61d15484a776e8eccb2b72a2fb2d4a5edea90067e5"
"hash": "38e44b7a271d9be098f00b233100fbce750a4e167ee547abccbefd36a089f399"
}

View File

@@ -1,6 +1,6 @@
{
"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": {
"columns": [
{
@@ -47,6 +47,11 @@
"name": "deleted_at",
"ordinal": 8,
"type_info": "Datetime"
},
{
"name": "subscription_url",
"ordinal": 9,
"type_info": "Text"
}
],
"parameters": {
@@ -60,9 +65,10 @@
false,
true,
true,
false,
true,
true,
true
]
},
"hash": "52fcb1f2472a6e14da26a9733b3b9e8a79354f287b3c347bc343f9747f01f134"
"hash": "7d27bdb54fbc8e65e5482fa9bc974e57c1fc7abcc5c7ca6bbb4944554381285c"
}

View File

@@ -1,6 +1,6 @@
{
"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": {
"columns": [
{
@@ -47,6 +47,11 @@
"name": "deleted_at",
"ordinal": 8,
"type_info": "Datetime"
},
{
"name": "subscription_url",
"ordinal": 9,
"type_info": "Text"
}
],
"parameters": {
@@ -60,9 +65,10 @@
false,
true,
true,
false,
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,
deleted_at: None,
synctoken: 0,
subscription_url: None,
};
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 rustical_dav::privileges::UserPrivilegeSet;
use rustical_dav::resource::{Resource, ResourceService};
use rustical_dav::xml::HrefElement;
use rustical_store::auth::User;
use rustical_store::{Calendar, CalendarStore};
use serde::{Deserialize, Serialize};
@@ -43,6 +44,7 @@ pub enum CalendarPropName {
SupportedReportSet,
SyncToken,
Getctag,
Source,
}
#[derive(Default, Deserialize, Serialize, PartialEq)]
@@ -82,6 +84,8 @@ pub enum CalendarProp {
// CalendarServer
#[serde(rename = "CS:getctag", alias = "getctag")]
Getctag(String),
#[serde(rename = "CS:source", alias = "source")]
Source(Option<HrefElement>),
#[serde(other)]
#[default]
@@ -97,8 +101,12 @@ impl Resource for CalendarResource {
type Error = Error;
type PrincipalResource = PrincipalResource;
fn get_resourcetype() -> &'static [&'static str] {
&["collection", "C:calendar"]
fn get_resourcetype(&self) -> &'static [&'static str] {
if self.0.subscription_url.is_none() {
&["collection", "C:calendar"]
} else {
&["collection", "CS:subscribed"]
}
}
fn get_prop(
@@ -144,6 +152,9 @@ impl Resource for CalendarResource {
}
CalendarPropName::SyncToken => CalendarProp::SyncToken(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::SyncToken(_) => 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),
}
}
@@ -213,6 +226,8 @@ impl Resource for CalendarResource {
CalendarPropName::SupportedReportSet => Err(rustical_dav::Error::PropReadOnly),
CalendarPropName::SyncToken => 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> {
// TODO: read-only for subscription
Ok(UserPrivilegeSet::owner_only(self.0.principal == user.id))
}
}

View File

@@ -55,7 +55,7 @@ impl Resource for CalendarObjectResource {
type Error = Error;
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 PrincipalResource = PrincipalResource;
fn get_resourcetype() -> &'static [&'static str] {
fn get_resourcetype(&self) -> &'static [&'static str] {
&["collection", "principal"]
}

View File

@@ -56,7 +56,7 @@ impl Resource for AddressObjectResource {
type Error = Error;
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 PrincipalResource = PrincipalResource;
fn get_resourcetype() -> &'static [&'static str] {
fn get_resourcetype(&self) -> &'static [&'static str] {
&["collection", "CARD:addressbook"]
}

View File

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

View File

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

View File

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

View File

@@ -83,6 +83,8 @@ pub struct MultistatusElement<PropType: Serialize, MemberPropType: Serialize> {
pub ns_caldav: &'static str,
#[serde(rename = "@xmlns:IC")]
pub ns_ical: &'static str,
#[serde(rename = "@xmlns:CS")]
pub ns_calendarserver: &'static str,
#[serde(rename = "@xmlns:CARD")]
pub ns_carddav: &'static str,
#[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_caldav: Namespace::CalDAV.as_str(),
ns_ical: Namespace::ICal.as_str(),
ns_calendarserver: Namespace::CServer.as_str(),
ns_carddav: Namespace::CardDAV.as_str(),
sync_token: None,
}

View File

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

View File

@@ -8,6 +8,7 @@ CREATE TABLE calendars (
color TEXT,
timezone TEXT,
deleted_at DATETIME,
subscription_url TEXT,
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> {
let cal = sqlx::query_as!(
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
WHERE (principal, id) = (?, ?)"#,
principal,
@@ -77,7 +77,7 @@ impl CalendarStore for SqliteStore {
async fn get_calendars(&self, principal: &str) -> Result<Vec<Calendar>, Error> {
let cals = sqlx::query_as!(
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
WHERE principal = ? AND deleted_at IS NULL"#,
principal
@@ -91,7 +91,7 @@ impl CalendarStore for SqliteStore {
async fn get_deleted_calendars(&self, principal: &str) -> Result<Vec<Calendar>, Error> {
let cals = sqlx::query_as!(
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
WHERE principal = ? AND deleted_at IS NOT NULL"#,
principal
@@ -236,6 +236,7 @@ impl CalendarStore for SqliteStore {
object: CalendarObject,
overwrite: bool,
) -> 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 (object_id, ics) = (object.get_id(), object.get_ics());
@@ -255,7 +256,7 @@ impl CalendarStore for SqliteStore {
principal,
cal_id,
object_id,
ics
ics,
)
})
.execute(&mut *tx)