mirror of
https://github.com/lennart-k/rustical.git
synced 2025-12-13 22:52:22 +00:00
caldav: add support for calendar subscriptions
This commit is contained in:
@@ -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"
|
||||||
}
|
}
|
||||||
@@ -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"
|
||||||
}
|
}
|
||||||
@@ -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"
|
||||||
}
|
}
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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] {
|
||||||
&[]
|
&[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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"]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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] {
|
||||||
&[]
|
&[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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"]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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"]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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"]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user