mirror of
https://github.com/lennart-k/rustical.git
synced 2025-12-13 20:32:48 +00:00
carddav: Implement DAV Push
This commit is contained in:
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -2884,6 +2884,7 @@ name = "rustical_store_sqlite"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"derive_more 1.0.0",
|
||||
"rustical_store",
|
||||
"serde",
|
||||
"sqlx",
|
||||
|
||||
@@ -2,35 +2,13 @@ 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::{CalendarStore, Subscription, SubscriptionStore};
|
||||
use rustical_xml::{XmlDeserialize, XmlDocument, XmlRootTag};
|
||||
use rustical_xml::XmlDocument;
|
||||
use tracing::instrument;
|
||||
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))]
|
||||
pub async fn route_post<C: CalendarStore + ?Sized, S: SubscriptionStore + ?Sized>(
|
||||
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()))
|
||||
.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())
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
use super::methods::mkcalendar::route_mkcalendar;
|
||||
use super::methods::post::route_post;
|
||||
use super::methods::report::route_report_calendar;
|
||||
use super::prop::{
|
||||
SupportedCalendarComponentSet, SupportedCalendarData, SupportedReportSet, Transports,
|
||||
};
|
||||
use super::prop::{SupportedCalendarComponentSet, SupportedCalendarData, SupportedReportSet};
|
||||
use crate::calendar_object::resource::CalendarObjectResource;
|
||||
use crate::principal::PrincipalResource;
|
||||
use crate::Error;
|
||||
@@ -13,6 +11,7 @@ use actix_web::web;
|
||||
use async_trait::async_trait;
|
||||
use derive_more::derive::{From, Into};
|
||||
use rustical_dav::privileges::UserPrivilegeSet;
|
||||
use rustical_dav::push::Transports;
|
||||
use rustical_dav::resource::{Resource, ResourceService};
|
||||
use rustical_dav::xml::{HrefElement, Resourcetype, ResourcetypeInner};
|
||||
use rustical_store::auth::User;
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
pub mod mkcol;
|
||||
pub mod post;
|
||||
pub mod report;
|
||||
|
||||
59
crates/carddav/src/addressbook/methods/post.rs
Normal file
59
crates/carddav/src/addressbook/methods/post.rs
Normal 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())
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
use super::methods::mkcol::route_mkcol;
|
||||
use super::methods::post::route_post;
|
||||
use super::methods::report::route_report_addressbook;
|
||||
use super::prop::{SupportedAddressData, SupportedReportSet};
|
||||
use crate::address_object::resource::AddressObjectResource;
|
||||
@@ -10,22 +11,29 @@ use actix_web::web;
|
||||
use async_trait::async_trait;
|
||||
use derive_more::derive::{From, Into};
|
||||
use rustical_dav::privileges::UserPrivilegeSet;
|
||||
use rustical_dav::push::Transports;
|
||||
use rustical_dav::resource::{Resource, ResourceService};
|
||||
use rustical_dav::xml::{Resourcetype, ResourcetypeInner};
|
||||
use rustical_store::auth::User;
|
||||
use rustical_store::{Addressbook, AddressbookStore};
|
||||
use rustical_store::{Addressbook, AddressbookStore, SubscriptionStore};
|
||||
use rustical_xml::{XmlDeserialize, XmlSerialize};
|
||||
use std::marker::PhantomData;
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
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>,
|
||||
__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 {
|
||||
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)]
|
||||
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)
|
||||
#[xml(ns = "rustical_dav::namespace::NS_CARDDAV")]
|
||||
AddressbookDescription(Option<String>),
|
||||
@@ -90,6 +105,8 @@ impl Resource for AddressbookResource {
|
||||
AddressbookPropName::Getcontenttype => {
|
||||
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::SupportedReportSet => {
|
||||
AddressbookProp::SupportedReportSet(SupportedReportSet::default())
|
||||
@@ -116,6 +133,8 @@ impl Resource for AddressbookResource {
|
||||
Ok(())
|
||||
}
|
||||
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::SupportedReportSet(_) => Err(rustical_dav::Error::PropReadOnly),
|
||||
AddressbookProp::SupportedAddressData(_) => Err(rustical_dav::Error::PropReadOnly),
|
||||
@@ -135,6 +154,8 @@ impl Resource for AddressbookResource {
|
||||
Ok(())
|
||||
}
|
||||
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::SupportedReportSet => Err(rustical_dav::Error::PropReadOnly),
|
||||
AddressbookPropName::SupportedAddressData => Err(rustical_dav::Error::PropReadOnly),
|
||||
@@ -153,7 +174,9 @@ impl Resource for AddressbookResource {
|
||||
}
|
||||
|
||||
#[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 PathComponents = (String, String); // principal, addressbook_id
|
||||
type Resource = AddressbookResource;
|
||||
@@ -220,5 +243,6 @@ impl<AS: AddressbookStore + ?Sized> ResourceService for AddressbookResourceServi
|
||||
let report_method = web::method(Method::from_str("REPORT").unwrap());
|
||||
res.route(mkcol_method.to(route_mkcol::<AS>))
|
||||
.route(report_method.to(route_report_addressbook::<AS>))
|
||||
.post(route_post::<AS, S>)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ use rustical_dav::resource::{NamedRoute, ResourceService};
|
||||
use rustical_dav::resources::RootResourceService;
|
||||
use rustical_store::{
|
||||
auth::{AuthenticationMiddleware, AuthenticationProvider},
|
||||
AddressbookStore,
|
||||
AddressbookStore, SubscriptionStore,
|
||||
};
|
||||
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());
|
||||
}
|
||||
|
||||
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,
|
||||
auth_provider: Arc<AP>,
|
||||
store: Arc<A>,
|
||||
subscription_store: Arc<S>,
|
||||
) {
|
||||
cfg.service(
|
||||
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(subscription_store))
|
||||
.service(RootResourceService::<PrincipalResource>::default().actix_resource())
|
||||
.service(
|
||||
web::scope("/user").service(
|
||||
@@ -70,7 +76,7 @@ pub fn configure_dav<AP: AuthenticationProvider, A: AddressbookStore + ?Sized>(
|
||||
.service(
|
||||
web::scope("/{addressbook}")
|
||||
.service(
|
||||
AddressbookResourceService::<A>::new(store.clone())
|
||||
AddressbookResourceService::<A, S>::new(store.clone())
|
||||
.actix_resource(),
|
||||
)
|
||||
.service(
|
||||
|
||||
@@ -2,6 +2,7 @@ pub mod depth_header;
|
||||
pub mod error;
|
||||
pub mod namespace;
|
||||
pub mod privileges;
|
||||
pub mod push;
|
||||
pub mod resource;
|
||||
pub mod resources;
|
||||
pub mod xml;
|
||||
|
||||
80
crates/dav/src/push/mod.rs
Normal file
80
crates/dav/src/push/mod.rs
Normal 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())
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -14,3 +14,4 @@ serde = { workspace = true }
|
||||
sqlx = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
derive_more.workspace = true
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
use super::{ChangeOperation, SqliteStore};
|
||||
use super::ChangeOperation;
|
||||
use async_trait::async_trait;
|
||||
use rustical_store::{AddressObject, Addressbook, AddressbookStore};
|
||||
use sqlx::{Sqlite, Transaction};
|
||||
use derive_more::derive::Constructor;
|
||||
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;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -25,16 +30,21 @@ async fn log_object_operation(
|
||||
addressbook_id: &str,
|
||||
object_id: &str,
|
||||
operation: ChangeOperation,
|
||||
) -> Result<(), sqlx::Error> {
|
||||
sqlx::query!(
|
||||
) -> Result<String, sqlx::Error> {
|
||||
struct Synctoken {
|
||||
synctoken: i64,
|
||||
}
|
||||
let Synctoken { synctoken } = sqlx::query_as!(
|
||||
Synctoken,
|
||||
r#"
|
||||
UPDATE addressbooks
|
||||
SET synctoken = synctoken + 1
|
||||
WHERE (principal, id) = (?1, ?2)"#,
|
||||
WHERE (principal, id) = (?1, ?2)
|
||||
RETURNING synctoken"#,
|
||||
principal,
|
||||
addressbook_id
|
||||
)
|
||||
.execute(&mut **tx)
|
||||
.fetch_one(&mut **tx)
|
||||
.await?;
|
||||
|
||||
sqlx::query!(
|
||||
@@ -50,11 +60,17 @@ async fn log_object_operation(
|
||||
)
|
||||
.execute(&mut **tx)
|
||||
.await?;
|
||||
Ok(())
|
||||
Ok(format_synctoken(synctoken))
|
||||
}
|
||||
|
||||
#[derive(Debug, Constructor)]
|
||||
pub struct SqliteAddressbookStore {
|
||||
db: SqlitePool,
|
||||
sender: Sender<CollectionOperation>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl AddressbookStore for SqliteStore {
|
||||
impl AddressbookStore for SqliteAddressbookStore {
|
||||
#[instrument]
|
||||
async fn get_addressbook(
|
||||
&self,
|
||||
@@ -165,6 +181,12 @@ impl AddressbookStore for SqliteStore {
|
||||
addressbook_id: &str,
|
||||
use_trashbin: bool,
|
||||
) -> 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 {
|
||||
true => {
|
||||
sqlx::query!(
|
||||
@@ -185,6 +207,17 @@ impl AddressbookStore for SqliteStore {
|
||||
.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(())
|
||||
}
|
||||
|
||||
@@ -320,7 +353,7 @@ impl AddressbookStore for SqliteStore {
|
||||
.await
|
||||
.map_err(crate::Error::from)?;
|
||||
|
||||
log_object_operation(
|
||||
let synctoken = log_object_operation(
|
||||
&mut tx,
|
||||
&principal,
|
||||
&addressbook_id,
|
||||
@@ -330,6 +363,17 @@ impl AddressbookStore for SqliteStore {
|
||||
.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),
|
||||
});
|
||||
|
||||
tx.commit().await.map_err(crate::Error::from)?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -366,7 +410,7 @@ impl AddressbookStore for SqliteStore {
|
||||
.map_err(crate::Error::from)?;
|
||||
}
|
||||
};
|
||||
log_object_operation(
|
||||
let synctoken = log_object_operation(
|
||||
&mut tx,
|
||||
principal,
|
||||
addressbook_id,
|
||||
@@ -375,7 +419,19 @@ impl AddressbookStore for SqliteStore {
|
||||
)
|
||||
.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(())
|
||||
}
|
||||
|
||||
@@ -397,7 +453,7 @@ impl AddressbookStore for SqliteStore {
|
||||
.execute(&mut *tx)
|
||||
.await.map_err(crate::Error::from)?;
|
||||
|
||||
log_object_operation(
|
||||
let synctoken = log_object_operation(
|
||||
&mut tx,
|
||||
principal,
|
||||
addressbook_id,
|
||||
@@ -407,6 +463,18 @@ impl AddressbookStore for SqliteStore {
|
||||
.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(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use super::ChangeOperation;
|
||||
use async_trait::async_trait;
|
||||
use derive_more::derive::Constructor;
|
||||
use rustical_store::synctoken::format_synctoken;
|
||||
use rustical_store::{Calendar, CalendarObject, CalendarStore, Error};
|
||||
use rustical_store::{CollectionOperation, CollectionOperationType};
|
||||
@@ -65,18 +66,12 @@ async fn log_object_operation(
|
||||
Ok(format_synctoken(synctoken))
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Constructor)]
|
||||
pub struct SqliteCalendarStore {
|
||||
db: SqlitePool,
|
||||
sender: Sender<CollectionOperation>,
|
||||
}
|
||||
|
||||
impl SqliteCalendarStore {
|
||||
pub fn new(db: SqlitePool, sender: Sender<CollectionOperation>) -> Self {
|
||||
Self { db, sender }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl CalendarStore for SqliteCalendarStore {
|
||||
#[instrument]
|
||||
|
||||
@@ -37,11 +37,16 @@ pub fn make_app<
|
||||
auth_provider.clone(),
|
||||
cal_store.clone(),
|
||||
addr_store.clone(),
|
||||
subscription_store,
|
||||
subscription_store.clone(),
|
||||
)
|
||||
}))
|
||||
.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(
|
||||
web::scope("/.well-known")
|
||||
|
||||
@@ -9,6 +9,7 @@ use config::{DataStoreConfig, SqliteDataStoreConfig};
|
||||
use rustical_dav::xml::multistatus::PropstatElement;
|
||||
use rustical_store::auth::StaticUserStore;
|
||||
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::{create_db_pool, SqliteStore};
|
||||
use rustical_xml::{XmlRootTag, XmlSerialize, XmlSerializeRoot};
|
||||
@@ -56,7 +57,7 @@ async fn get_data_stores(
|
||||
// Channel to watch for changes (for DAV Push)
|
||||
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 subscription_store = Arc::new(SqliteStore::new(db.clone()));
|
||||
(addressbook_store, cal_store, subscription_store, recv)
|
||||
@@ -107,7 +108,6 @@ async fn main() -> Result<()> {
|
||||
tokio::spawn(async move {
|
||||
let subscription_store = subscription_store_clone.clone();
|
||||
while let Some(message) = update_recv.recv().await {
|
||||
dbg!(&message);
|
||||
if let Ok(subscribers) =
|
||||
subscription_store.get_subscriptions(&message.topic).await
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user