carddav: Implement DAV Push

This commit is contained in:
Lennart
2025-01-15 17:14:33 +01:00
parent 618ed3b327
commit 751c2d1ce7
15 changed files with 276 additions and 113 deletions

1
Cargo.lock generated
View File

@@ -2884,6 +2884,7 @@ name = "rustical_store_sqlite"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"derive_more 1.0.0",
"rustical_store", "rustical_store",
"serde", "serde",
"sqlx", "sqlx",

View File

@@ -2,35 +2,13 @@ use crate::Error;
use actix_web::http::header; use actix_web::http::header;
use actix_web::web::{Data, Path}; use actix_web::web::{Data, Path};
use actix_web::{HttpRequest, HttpResponse}; use actix_web::{HttpRequest, HttpResponse};
use rustical_dav::push::PushRegister;
use rustical_store::auth::User; use rustical_store::auth::User;
use rustical_store::{CalendarStore, Subscription, SubscriptionStore}; use rustical_store::{CalendarStore, Subscription, SubscriptionStore};
use rustical_xml::{XmlDeserialize, XmlDocument, XmlRootTag}; use rustical_xml::XmlDocument;
use tracing::instrument; use tracing::instrument;
use tracing_actix_web::RootSpan; use tracing_actix_web::RootSpan;
#[derive(XmlDeserialize, Clone, Debug, PartialEq)]
#[xml(ns = "rustical_dav::namespace::NS_DAVPUSH")]
struct WebPushSubscription {
#[xml(ns = "rustical_dav::namespace::NS_DAVPUSH")]
push_resource: String,
}
#[derive(XmlDeserialize, Clone, Debug, PartialEq)]
struct SubscriptionElement {
#[xml(ns = "rustical_dav::namespace::NS_DAVPUSH")]
pub web_push_subscription: WebPushSubscription,
}
#[derive(XmlDeserialize, XmlRootTag, Clone, Debug, PartialEq)]
#[xml(root = b"push-register")]
#[xml(ns = "rustical_dav::namespace::NS_DAVPUSH")]
struct PushRegister {
#[xml(ns = "rustical_dav::namespace::NS_DAVPUSH")]
subscription: SubscriptionElement,
#[xml(ns = "rustical_dav::namespace::NS_DAVPUSH")]
expires: Option<String>,
}
#[instrument(parent = root_span.id(), skip(store, subscription_store, root_span, req))] #[instrument(parent = root_span.id(), skip(store, subscription_store, root_span, req))]
pub async fn route_post<C: CalendarStore + ?Sized, S: SubscriptionStore + ?Sized>( pub async fn route_post<C: CalendarStore + ?Sized, S: SubscriptionStore + ?Sized>(
path: Path<(String, String)>, path: Path<(String, String)>,
@@ -79,37 +57,3 @@ pub async fn route_post<C: CalendarStore + ?Sized, S: SubscriptionStore + ?Sized
.append_header((header::EXPIRES, expires.to_rfc2822())) .append_header((header::EXPIRES, expires.to_rfc2822()))
.finish()) .finish())
} }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_xml_push_register() {
let push_register = PushRegister::parse_str(
r#"
<?xml version="1.0" encoding="utf-8" ?>
<push-register xmlns="https://bitfire.at/webdav-push">
<subscription>
<web-push-subscription>
<push-resource>https://up.example.net/yohd4yai5Phiz1wi</push-resource>
</web-push-subscription>
</subscription>
<expires>Wed, 20 Dec 2023 10:03:31 GMT</expires>
</push-register>
"#,
)
.unwrap();
assert_eq!(
push_register,
PushRegister {
subscription: SubscriptionElement {
web_push_subscription: WebPushSubscription {
push_resource: "https://up.example.net/yohd4yai5Phiz1wi".to_owned()
}
},
expires: Some("Wed, 20 Dec 2023 10:03:31 GMT".to_owned())
}
)
}
}

View File

@@ -80,24 +80,3 @@ impl Default for SupportedReportSet {
} }
} }
} }
#[derive(Debug, Clone, XmlSerialize, PartialEq)]
pub enum Transport {
#[xml(ns = "rustical_dav::namespace::NS_DAVPUSH")]
WebPush,
}
#[derive(Debug, Clone, XmlSerialize, PartialEq)]
pub struct Transports {
#[xml(flatten, ty = "untagged")]
#[xml(ns = "rustical_dav::namespace::NS_DAVPUSH")]
transports: Vec<Transport>,
}
impl Default for Transports {
fn default() -> Self {
Self {
transports: vec![Transport::WebPush],
}
}
}

View File

@@ -1,9 +1,7 @@
use super::methods::mkcalendar::route_mkcalendar; use super::methods::mkcalendar::route_mkcalendar;
use super::methods::post::route_post; use super::methods::post::route_post;
use super::methods::report::route_report_calendar; use super::methods::report::route_report_calendar;
use super::prop::{ use super::prop::{SupportedCalendarComponentSet, SupportedCalendarData, SupportedReportSet};
SupportedCalendarComponentSet, SupportedCalendarData, SupportedReportSet, Transports,
};
use crate::calendar_object::resource::CalendarObjectResource; use crate::calendar_object::resource::CalendarObjectResource;
use crate::principal::PrincipalResource; use crate::principal::PrincipalResource;
use crate::Error; use crate::Error;
@@ -13,6 +11,7 @@ use actix_web::web;
use async_trait::async_trait; 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::push::Transports;
use rustical_dav::resource::{Resource, ResourceService}; use rustical_dav::resource::{Resource, ResourceService};
use rustical_dav::xml::{HrefElement, Resourcetype, ResourcetypeInner}; use rustical_dav::xml::{HrefElement, Resourcetype, ResourcetypeInner};
use rustical_store::auth::User; use rustical_store::auth::User;

View File

@@ -1,2 +1,3 @@
pub mod mkcol; pub mod mkcol;
pub mod post;
pub mod report; pub mod report;

View File

@@ -0,0 +1,59 @@
use crate::Error;
use actix_web::http::header;
use actix_web::web::{Data, Path};
use actix_web::{HttpRequest, HttpResponse};
use rustical_dav::push::PushRegister;
use rustical_store::auth::User;
use rustical_store::{AddressbookStore, Subscription, SubscriptionStore};
use rustical_xml::XmlDocument;
use tracing::instrument;
use tracing_actix_web::RootSpan;
#[instrument(parent = root_span.id(), skip(store, subscription_store, root_span, req))]
pub async fn route_post<A: AddressbookStore + ?Sized, S: SubscriptionStore + ?Sized>(
path: Path<(String, String)>,
body: String,
user: User,
store: Data<A>,
subscription_store: Data<S>,
root_span: RootSpan,
req: HttpRequest,
) -> Result<HttpResponse, Error> {
let (principal, addressbook_id) = path.into_inner();
if principal != user.id {
return Err(Error::Unauthorized);
}
let addressbook = store.get_addressbook(&principal, &addressbook_id).await?;
let request = PushRegister::parse_str(&body)?;
let sub_id = uuid::Uuid::new_v4().to_string();
let expires = if let Some(expires) = request.expires {
chrono::DateTime::parse_from_rfc2822(&expires)
.map_err(|err| crate::Error::Other(err.into()))?
} else {
chrono::Utc::now().fixed_offset() + chrono::Duration::weeks(1)
};
let subscription = Subscription {
id: sub_id.to_owned(),
push_resource: request
.subscription
.web_push_subscription
.push_resource
.to_owned(),
topic: addressbook.push_topic,
expiration: expires.naive_local(),
};
subscription_store.upsert_subscription(subscription).await?;
let location = req
.resource_map()
.url_for(&req, "subscription", &[sub_id])
.unwrap();
Ok(HttpResponse::Created()
.append_header((header::LOCATION, location.to_string()))
.append_header((header::EXPIRES, expires.to_rfc2822()))
.finish())
}

View File

@@ -1,4 +1,5 @@
use super::methods::mkcol::route_mkcol; use super::methods::mkcol::route_mkcol;
use super::methods::post::route_post;
use super::methods::report::route_report_addressbook; use super::methods::report::route_report_addressbook;
use super::prop::{SupportedAddressData, SupportedReportSet}; use super::prop::{SupportedAddressData, SupportedReportSet};
use crate::address_object::resource::AddressObjectResource; use crate::address_object::resource::AddressObjectResource;
@@ -10,22 +11,29 @@ use actix_web::web;
use async_trait::async_trait; 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::push::Transports;
use rustical_dav::resource::{Resource, ResourceService}; use rustical_dav::resource::{Resource, ResourceService};
use rustical_dav::xml::{Resourcetype, ResourcetypeInner}; use rustical_dav::xml::{Resourcetype, ResourcetypeInner};
use rustical_store::auth::User; use rustical_store::auth::User;
use rustical_store::{Addressbook, AddressbookStore}; use rustical_store::{Addressbook, AddressbookStore, SubscriptionStore};
use rustical_xml::{XmlDeserialize, XmlSerialize}; use rustical_xml::{XmlDeserialize, XmlSerialize};
use std::marker::PhantomData;
use std::str::FromStr; use std::str::FromStr;
use std::sync::Arc; use std::sync::Arc;
use strum::{EnumDiscriminants, EnumString, IntoStaticStr, VariantNames}; use strum::{EnumDiscriminants, EnumString, IntoStaticStr, VariantNames};
pub struct AddressbookResourceService<AS: AddressbookStore + ?Sized> { pub struct AddressbookResourceService<AS: AddressbookStore + ?Sized, S: SubscriptionStore + ?Sized>
{
addr_store: Arc<AS>, addr_store: Arc<AS>,
__phantom_sub: PhantomData<S>,
} }
impl<A: AddressbookStore + ?Sized> AddressbookResourceService<A> { impl<A: AddressbookStore + ?Sized, S: SubscriptionStore + ?Sized> AddressbookResourceService<A, S> {
pub fn new(addr_store: Arc<A>) -> Self { pub fn new(addr_store: Arc<A>) -> Self {
Self { addr_store } Self {
addr_store,
__phantom_sub: PhantomData,
}
} }
} }
@@ -42,6 +50,13 @@ pub enum AddressbookProp {
#[xml(ns = "rustical_dav::namespace::NS_DAV", skip_deserializing)] #[xml(ns = "rustical_dav::namespace::NS_DAV", skip_deserializing)]
Getcontenttype(&'static str), Getcontenttype(&'static str),
// WebDav Push
#[xml(skip_deserializing)]
#[xml(ns = "rustical_dav::namespace::NS_DAVPUSH")]
Transports(Transports),
#[xml(ns = "rustical_dav::namespace::NS_DAVPUSH")]
Topic(String),
// CardDAV (RFC 6352) // CardDAV (RFC 6352)
#[xml(ns = "rustical_dav::namespace::NS_CARDDAV")] #[xml(ns = "rustical_dav::namespace::NS_CARDDAV")]
AddressbookDescription(Option<String>), AddressbookDescription(Option<String>),
@@ -90,6 +105,8 @@ impl Resource for AddressbookResource {
AddressbookPropName::Getcontenttype => { AddressbookPropName::Getcontenttype => {
AddressbookProp::Getcontenttype("text/vcard;charset=utf-8") AddressbookProp::Getcontenttype("text/vcard;charset=utf-8")
} }
AddressbookPropName::Transports => AddressbookProp::Transports(Default::default()),
AddressbookPropName::Topic => AddressbookProp::Topic(self.0.push_topic.to_owned()),
AddressbookPropName::MaxResourceSize => AddressbookProp::MaxResourceSize(10000000), AddressbookPropName::MaxResourceSize => AddressbookProp::MaxResourceSize(10000000),
AddressbookPropName::SupportedReportSet => { AddressbookPropName::SupportedReportSet => {
AddressbookProp::SupportedReportSet(SupportedReportSet::default()) AddressbookProp::SupportedReportSet(SupportedReportSet::default())
@@ -116,6 +133,8 @@ impl Resource for AddressbookResource {
Ok(()) Ok(())
} }
AddressbookProp::Getcontenttype(_) => Err(rustical_dav::Error::PropReadOnly), AddressbookProp::Getcontenttype(_) => Err(rustical_dav::Error::PropReadOnly),
AddressbookProp::Transports(_) => Err(rustical_dav::Error::PropReadOnly),
AddressbookProp::Topic(_) => Err(rustical_dav::Error::PropReadOnly),
AddressbookProp::MaxResourceSize(_) => Err(rustical_dav::Error::PropReadOnly), AddressbookProp::MaxResourceSize(_) => Err(rustical_dav::Error::PropReadOnly),
AddressbookProp::SupportedReportSet(_) => Err(rustical_dav::Error::PropReadOnly), AddressbookProp::SupportedReportSet(_) => Err(rustical_dav::Error::PropReadOnly),
AddressbookProp::SupportedAddressData(_) => Err(rustical_dav::Error::PropReadOnly), AddressbookProp::SupportedAddressData(_) => Err(rustical_dav::Error::PropReadOnly),
@@ -135,6 +154,8 @@ impl Resource for AddressbookResource {
Ok(()) Ok(())
} }
AddressbookPropName::Getcontenttype => Err(rustical_dav::Error::PropReadOnly), AddressbookPropName::Getcontenttype => Err(rustical_dav::Error::PropReadOnly),
AddressbookPropName::Transports => Err(rustical_dav::Error::PropReadOnly),
AddressbookPropName::Topic => Err(rustical_dav::Error::PropReadOnly),
AddressbookPropName::MaxResourceSize => Err(rustical_dav::Error::PropReadOnly), AddressbookPropName::MaxResourceSize => Err(rustical_dav::Error::PropReadOnly),
AddressbookPropName::SupportedReportSet => Err(rustical_dav::Error::PropReadOnly), AddressbookPropName::SupportedReportSet => Err(rustical_dav::Error::PropReadOnly),
AddressbookPropName::SupportedAddressData => Err(rustical_dav::Error::PropReadOnly), AddressbookPropName::SupportedAddressData => Err(rustical_dav::Error::PropReadOnly),
@@ -153,7 +174,9 @@ impl Resource for AddressbookResource {
} }
#[async_trait(?Send)] #[async_trait(?Send)]
impl<AS: AddressbookStore + ?Sized> ResourceService for AddressbookResourceService<AS> { impl<AS: AddressbookStore + ?Sized, S: SubscriptionStore + ?Sized> ResourceService
for AddressbookResourceService<AS, S>
{
type MemberType = AddressObjectResource; type MemberType = AddressObjectResource;
type PathComponents = (String, String); // principal, addressbook_id type PathComponents = (String, String); // principal, addressbook_id
type Resource = AddressbookResource; type Resource = AddressbookResource;
@@ -220,5 +243,6 @@ impl<AS: AddressbookStore + ?Sized> ResourceService for AddressbookResourceServi
let report_method = web::method(Method::from_str("REPORT").unwrap()); let report_method = web::method(Method::from_str("REPORT").unwrap());
res.route(mkcol_method.to(route_mkcol::<AS>)) res.route(mkcol_method.to(route_mkcol::<AS>))
.route(report_method.to(route_report_addressbook::<AS>)) .route(report_method.to(route_report_addressbook::<AS>))
.post(route_post::<AS, S>)
} }
} }

View File

@@ -16,7 +16,7 @@ use rustical_dav::resource::{NamedRoute, ResourceService};
use rustical_dav::resources::RootResourceService; use rustical_dav::resources::RootResourceService;
use rustical_store::{ use rustical_store::{
auth::{AuthenticationMiddleware, AuthenticationProvider}, auth::{AuthenticationMiddleware, AuthenticationProvider},
AddressbookStore, AddressbookStore, SubscriptionStore,
}; };
use std::sync::Arc; use std::sync::Arc;
@@ -29,10 +29,15 @@ pub fn configure_well_known(cfg: &mut web::ServiceConfig, carddav_root: String)
cfg.service(web::redirect("/carddav", carddav_root).permanent()); cfg.service(web::redirect("/carddav", carddav_root).permanent());
} }
pub fn configure_dav<AP: AuthenticationProvider, A: AddressbookStore + ?Sized>( pub fn configure_dav<
AP: AuthenticationProvider,
A: AddressbookStore + ?Sized,
S: SubscriptionStore + ?Sized,
>(
cfg: &mut web::ServiceConfig, cfg: &mut web::ServiceConfig,
auth_provider: Arc<AP>, auth_provider: Arc<AP>,
store: Arc<A>, store: Arc<A>,
subscription_store: Arc<S>,
) { ) {
cfg.service( cfg.service(
web::scope("") web::scope("")
@@ -58,6 +63,7 @@ pub fn configure_dav<AP: AuthenticationProvider, A: AddressbookStore + ?Sized>(
}), }),
) )
.app_data(Data::from(store.clone())) .app_data(Data::from(store.clone()))
.app_data(Data::from(subscription_store))
.service(RootResourceService::<PrincipalResource>::default().actix_resource()) .service(RootResourceService::<PrincipalResource>::default().actix_resource())
.service( .service(
web::scope("/user").service( web::scope("/user").service(
@@ -70,7 +76,7 @@ pub fn configure_dav<AP: AuthenticationProvider, A: AddressbookStore + ?Sized>(
.service( .service(
web::scope("/{addressbook}") web::scope("/{addressbook}")
.service( .service(
AddressbookResourceService::<A>::new(store.clone()) AddressbookResourceService::<A, S>::new(store.clone())
.actix_resource(), .actix_resource(),
) )
.service( .service(

View File

@@ -2,6 +2,7 @@ pub mod depth_header;
pub mod error; pub mod error;
pub mod namespace; pub mod namespace;
pub mod privileges; pub mod privileges;
pub mod push;
pub mod resource; pub mod resource;
pub mod resources; pub mod resources;
pub mod xml; pub mod xml;

View File

@@ -0,0 +1,80 @@
use rustical_xml::{XmlDeserialize, XmlRootTag, XmlSerialize};
#[derive(Debug, Clone, XmlSerialize, PartialEq)]
pub enum Transport {
#[xml(ns = "crate::namespace::NS_DAVPUSH")]
WebPush,
}
#[derive(Debug, Clone, XmlSerialize, PartialEq)]
pub struct Transports {
#[xml(flatten, ty = "untagged")]
#[xml(ns = "crate::namespace::NS_DAVPUSH")]
transports: Vec<Transport>,
}
impl Default for Transports {
fn default() -> Self {
Self {
transports: vec![Transport::WebPush],
}
}
}
#[derive(XmlDeserialize, Clone, Debug, PartialEq)]
#[xml(ns = "crate::namespace::NS_DAVPUSH")]
pub struct WebPushSubscription {
#[xml(ns = "crate::namespace::NS_DAVPUSH")]
pub push_resource: String,
}
#[derive(XmlDeserialize, Clone, Debug, PartialEq)]
pub struct SubscriptionElement {
#[xml(ns = "crate::namespace::NS_DAVPUSH")]
pub web_push_subscription: WebPushSubscription,
}
#[derive(XmlDeserialize, XmlRootTag, Clone, Debug, PartialEq)]
#[xml(root = b"push-register")]
#[xml(ns = "crate::namespace::NS_DAVPUSH")]
pub struct PushRegister {
#[xml(ns = "crate::namespace::NS_DAVPUSH")]
pub subscription: SubscriptionElement,
#[xml(ns = "crate::namespace::NS_DAVPUSH")]
pub expires: Option<String>,
}
#[cfg(test)]
mod tests {
use super::*;
use rustical_xml::XmlDocument;
#[test]
fn test_xml_push_register() {
let push_register = PushRegister::parse_str(
r#"
<?xml version="1.0" encoding="utf-8" ?>
<push-register xmlns="https://bitfire.at/webdav-push">
<subscription>
<web-push-subscription>
<push-resource>https://up.example.net/yohd4yai5Phiz1wi</push-resource>
</web-push-subscription>
</subscription>
<expires>Wed, 20 Dec 2023 10:03:31 GMT</expires>
</push-register>
"#,
)
.unwrap();
assert_eq!(
push_register,
PushRegister {
subscription: SubscriptionElement {
web_push_subscription: WebPushSubscription {
push_resource: "https://up.example.net/yohd4yai5Phiz1wi".to_owned()
}
},
expires: Some("Wed, 20 Dec 2023 10:03:31 GMT".to_owned())
}
)
}
}

View File

@@ -14,3 +14,4 @@ serde = { workspace = true }
sqlx = { workspace = true } sqlx = { workspace = true }
thiserror = { workspace = true } thiserror = { workspace = true }
tracing = { workspace = true } tracing = { workspace = true }
derive_more.workspace = true

View File

@@ -1,7 +1,12 @@
use super::{ChangeOperation, SqliteStore}; use super::ChangeOperation;
use async_trait::async_trait; use async_trait::async_trait;
use rustical_store::{AddressObject, Addressbook, AddressbookStore}; use derive_more::derive::Constructor;
use sqlx::{Sqlite, Transaction}; use rustical_store::{
synctoken::format_synctoken, AddressObject, Addressbook, AddressbookStore, CollectionOperation,
CollectionOperationDomain, CollectionOperationType, Error,
};
use sqlx::{Sqlite, SqlitePool, Transaction};
use tokio::sync::mpsc::Sender;
use tracing::instrument; use tracing::instrument;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@@ -25,16 +30,21 @@ async fn log_object_operation(
addressbook_id: &str, addressbook_id: &str,
object_id: &str, object_id: &str,
operation: ChangeOperation, operation: ChangeOperation,
) -> Result<(), sqlx::Error> { ) -> Result<String, sqlx::Error> {
sqlx::query!( struct Synctoken {
synctoken: i64,
}
let Synctoken { synctoken } = sqlx::query_as!(
Synctoken,
r#" r#"
UPDATE addressbooks UPDATE addressbooks
SET synctoken = synctoken + 1 SET synctoken = synctoken + 1
WHERE (principal, id) = (?1, ?2)"#, WHERE (principal, id) = (?1, ?2)
RETURNING synctoken"#,
principal, principal,
addressbook_id addressbook_id
) )
.execute(&mut **tx) .fetch_one(&mut **tx)
.await?; .await?;
sqlx::query!( sqlx::query!(
@@ -50,11 +60,17 @@ async fn log_object_operation(
) )
.execute(&mut **tx) .execute(&mut **tx)
.await?; .await?;
Ok(()) Ok(format_synctoken(synctoken))
}
#[derive(Debug, Constructor)]
pub struct SqliteAddressbookStore {
db: SqlitePool,
sender: Sender<CollectionOperation>,
} }
#[async_trait] #[async_trait]
impl AddressbookStore for SqliteStore { impl AddressbookStore for SqliteAddressbookStore {
#[instrument] #[instrument]
async fn get_addressbook( async fn get_addressbook(
&self, &self,
@@ -165,6 +181,12 @@ impl AddressbookStore for SqliteStore {
addressbook_id: &str, addressbook_id: &str,
use_trashbin: bool, use_trashbin: bool,
) -> Result<(), rustical_store::Error> { ) -> Result<(), rustical_store::Error> {
let addressbook = match self.get_addressbook(principal, addressbook_id).await {
Ok(addressbook) => Some(addressbook),
Err(Error::NotFound) => None,
Err(err) => return Err(err),
};
match use_trashbin { match use_trashbin {
true => { true => {
sqlx::query!( sqlx::query!(
@@ -185,6 +207,17 @@ impl AddressbookStore for SqliteStore {
.map_err(crate::Error::from)?; .map_err(crate::Error::from)?;
} }
}; };
if let Some(addressbook) = addressbook {
// TODO: Watch for errors here?
let _ = self.sender.try_send(CollectionOperation {
r#type: CollectionOperationType::Delete,
domain: CollectionOperationDomain::Addressbook,
topic: addressbook.push_topic,
sync_token: None,
});
}
Ok(()) Ok(())
} }
@@ -320,7 +353,7 @@ impl AddressbookStore for SqliteStore {
.await .await
.map_err(crate::Error::from)?; .map_err(crate::Error::from)?;
log_object_operation( let synctoken = log_object_operation(
&mut tx, &mut tx,
&principal, &principal,
&addressbook_id, &addressbook_id,
@@ -330,6 +363,17 @@ impl AddressbookStore for SqliteStore {
.await .await
.map_err(crate::Error::from)?; .map_err(crate::Error::from)?;
// TODO: Watch for errors here?
let _ = self.sender.try_send(CollectionOperation {
r#type: CollectionOperationType::Object,
domain: CollectionOperationDomain::Addressbook,
topic: self
.get_addressbook(&principal, &addressbook_id)
.await?
.push_topic,
sync_token: Some(synctoken),
});
tx.commit().await.map_err(crate::Error::from)?; tx.commit().await.map_err(crate::Error::from)?;
Ok(()) Ok(())
} }
@@ -366,7 +410,7 @@ impl AddressbookStore for SqliteStore {
.map_err(crate::Error::from)?; .map_err(crate::Error::from)?;
} }
}; };
log_object_operation( let synctoken = log_object_operation(
&mut tx, &mut tx,
principal, principal,
addressbook_id, addressbook_id,
@@ -375,7 +419,19 @@ impl AddressbookStore for SqliteStore {
) )
.await .await
.map_err(crate::Error::from)?; .map_err(crate::Error::from)?;
tx.commit().await.map_err(crate::Error::from)?; tx.commit().await.map_err(crate::Error::from)?;
// TODO: Watch for errors here?
let _ = self.sender.try_send(CollectionOperation {
r#type: CollectionOperationType::Object,
domain: CollectionOperationDomain::Addressbook,
topic: self
.get_addressbook(principal, addressbook_id)
.await?
.push_topic,
sync_token: Some(synctoken),
});
Ok(()) Ok(())
} }
@@ -397,7 +453,7 @@ impl AddressbookStore for SqliteStore {
.execute(&mut *tx) .execute(&mut *tx)
.await.map_err(crate::Error::from)?; .await.map_err(crate::Error::from)?;
log_object_operation( let synctoken = log_object_operation(
&mut tx, &mut tx,
principal, principal,
addressbook_id, addressbook_id,
@@ -407,6 +463,18 @@ impl AddressbookStore for SqliteStore {
.await .await
.map_err(crate::Error::from)?; .map_err(crate::Error::from)?;
tx.commit().await.map_err(crate::Error::from)?; tx.commit().await.map_err(crate::Error::from)?;
// TODO: Watch for errors here?
let _ = self.sender.try_send(CollectionOperation {
r#type: CollectionOperationType::Object,
domain: CollectionOperationDomain::Addressbook,
topic: self
.get_addressbook(principal, addressbook_id)
.await?
.push_topic,
sync_token: Some(synctoken),
});
Ok(()) Ok(())
} }
} }

View File

@@ -1,5 +1,6 @@
use super::ChangeOperation; use super::ChangeOperation;
use async_trait::async_trait; use async_trait::async_trait;
use derive_more::derive::Constructor;
use rustical_store::synctoken::format_synctoken; use rustical_store::synctoken::format_synctoken;
use rustical_store::{Calendar, CalendarObject, CalendarStore, Error}; use rustical_store::{Calendar, CalendarObject, CalendarStore, Error};
use rustical_store::{CollectionOperation, CollectionOperationType}; use rustical_store::{CollectionOperation, CollectionOperationType};
@@ -65,18 +66,12 @@ async fn log_object_operation(
Ok(format_synctoken(synctoken)) Ok(format_synctoken(synctoken))
} }
#[derive(Debug)] #[derive(Debug, Constructor)]
pub struct SqliteCalendarStore { pub struct SqliteCalendarStore {
db: SqlitePool, db: SqlitePool,
sender: Sender<CollectionOperation>, sender: Sender<CollectionOperation>,
} }
impl SqliteCalendarStore {
pub fn new(db: SqlitePool, sender: Sender<CollectionOperation>) -> Self {
Self { db, sender }
}
}
#[async_trait] #[async_trait]
impl CalendarStore for SqliteCalendarStore { impl CalendarStore for SqliteCalendarStore {
#[instrument] #[instrument]

View File

@@ -37,11 +37,16 @@ pub fn make_app<
auth_provider.clone(), auth_provider.clone(),
cal_store.clone(), cal_store.clone(),
addr_store.clone(), addr_store.clone(),
subscription_store, subscription_store.clone(),
) )
})) }))
.service(web::scope("/carddav").configure(|cfg| { .service(web::scope("/carddav").configure(|cfg| {
rustical_carddav::configure_dav(cfg, auth_provider.clone(), addr_store.clone()) rustical_carddav::configure_dav(
cfg,
auth_provider.clone(),
addr_store.clone(),
subscription_store,
)
})) }))
.service( .service(
web::scope("/.well-known") web::scope("/.well-known")

View File

@@ -9,6 +9,7 @@ use config::{DataStoreConfig, SqliteDataStoreConfig};
use rustical_dav::xml::multistatus::PropstatElement; use rustical_dav::xml::multistatus::PropstatElement;
use rustical_store::auth::StaticUserStore; use rustical_store::auth::StaticUserStore;
use rustical_store::{AddressbookStore, CalendarStore, CollectionOperation, SubscriptionStore}; use rustical_store::{AddressbookStore, CalendarStore, CollectionOperation, SubscriptionStore};
use rustical_store_sqlite::addressbook_store::SqliteAddressbookStore;
use rustical_store_sqlite::calendar_store::SqliteCalendarStore; use rustical_store_sqlite::calendar_store::SqliteCalendarStore;
use rustical_store_sqlite::{create_db_pool, SqliteStore}; use rustical_store_sqlite::{create_db_pool, SqliteStore};
use rustical_xml::{XmlRootTag, XmlSerialize, XmlSerializeRoot}; use rustical_xml::{XmlRootTag, XmlSerialize, XmlSerializeRoot};
@@ -56,7 +57,7 @@ async fn get_data_stores(
// Channel to watch for changes (for DAV Push) // Channel to watch for changes (for DAV Push)
let (send, recv) = tokio::sync::mpsc::channel(1000); let (send, recv) = tokio::sync::mpsc::channel(1000);
let addressbook_store = Arc::new(SqliteStore::new(db.clone())); let addressbook_store = Arc::new(SqliteAddressbookStore::new(db.clone(), send.clone()));
let cal_store = Arc::new(SqliteCalendarStore::new(db.clone(), send)); let cal_store = Arc::new(SqliteCalendarStore::new(db.clone(), send));
let subscription_store = Arc::new(SqliteStore::new(db.clone())); let subscription_store = Arc::new(SqliteStore::new(db.clone()));
(addressbook_store, cal_store, subscription_store, recv) (addressbook_store, cal_store, subscription_store, recv)
@@ -107,7 +108,6 @@ async fn main() -> Result<()> {
tokio::spawn(async move { tokio::spawn(async move {
let subscription_store = subscription_store_clone.clone(); let subscription_store = subscription_store_clone.clone();
while let Some(message) = update_recv.recv().await { while let Some(message) = update_recv.recv().await {
dbg!(&message);
if let Ok(subscribers) = if let Ok(subscribers) =
subscription_store.get_subscriptions(&message.topic).await subscription_store.get_subscriptions(&message.topic).await
{ {