diff --git a/Cargo.lock b/Cargo.lock index dc556aa..7cd7644 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -818,6 +818,16 @@ dependencies = [ "version_check", ] +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -1105,6 +1115,21 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "form_urlencoded" version = "1.2.1" @@ -1442,6 +1467,23 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-rustls" +version = "0.27.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2" +dependencies = [ + "futures-util", + "http 1.2.0", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + [[package]] name = "hyper-timeout" version = "0.5.2" @@ -1455,6 +1497,22 @@ dependencies = [ "tower-service", ] +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + [[package]] name = "hyper-util" version = "0.1.10" @@ -1687,6 +1745,12 @@ dependencies = [ "generic-array", ] +[[package]] +name = "ipnet" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708" + [[package]] name = "is_terminal_polyfill" version = "1.70.1" @@ -1899,6 +1963,23 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e94e1e6445d314f972ff7395df2de295fe51b71821694f0b0e1e79c4f12c8577" +[[package]] +name = "native-tls" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + [[package]] name = "nom" version = "7.1.3" @@ -1993,6 +2074,50 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" +[[package]] +name = "openssl" +version = "0.10.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "opentelemetry" version = "0.27.1" @@ -2444,6 +2569,65 @@ version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" +[[package]] +name = "reqwest" +version = "0.12.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43e734407157c3c2034e0258f5e4473ddb361b1e85f95a66690d67264d7cd1da" +dependencies = [ + "base64 0.22.1", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2 0.4.7", + "http 1.2.0", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "system-configuration", + "tokio", + "tokio-native-tls", + "tower 0.5.2", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-registry", +] + +[[package]] +name = "ring" +version = "0.17.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +dependencies = [ + "cc", + "cfg-if", + "getrandom", + "libc", + "spin", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "rpassword" version = "7.3.1" @@ -2590,13 +2774,17 @@ dependencies = [ "opentelemetry_sdk", "password-hash", "pbkdf2", + "quick-xml", "rand", + "reqwest", "rpassword", "rustical_caldav", "rustical_carddav", + "rustical_dav", "rustical_frontend", "rustical_store", "rustical_store_sqlite", + "rustical_xml", "serde", "sqlx", "tokio", @@ -2736,6 +2924,7 @@ dependencies = [ "serde", "sqlx", "thiserror 2.0.11", + "tokio", "tracing", ] @@ -2761,6 +2950,45 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "rustls" +version = "0.23.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f287924602bf649d949c63dc8ac8b235fa5387d394020705b80c4eb597ce5b8" +dependencies = [ + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2bf47e6ff922db3825eb750c4e2ff784c6ff8fb9e13046ef6a1d1c5401b0b37" + +[[package]] +name = "rustls-webpki" +version = "0.102.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.19" @@ -2782,12 +3010,44 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "schannel" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +dependencies = [ + "windows-sys 0.59.0", +] + [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "semver" version = "1.0.24" @@ -3231,6 +3491,9 @@ name = "sync_wrapper" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] [[package]] name = "synstructure" @@ -3243,6 +3506,27 @@ dependencies = [ "syn", ] +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "tempfile" version = "3.15.0" @@ -3393,6 +3677,26 @@ dependencies = [ "syn", ] +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6d0975eaace0cf0fcadee4e4aaa5da15b5c079146f2cffb67c113be122bf37" +dependencies = [ + "rustls", + "tokio", +] + [[package]] name = "tokio-stream" version = "0.1.17" @@ -3511,6 +3815,7 @@ dependencies = [ "futures-util", "pin-project-lite", "sync_wrapper", + "tokio", "tower-layer", "tower-service", ] @@ -3687,6 +3992,12 @@ version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "url" version = "2.5.4" @@ -3800,6 +4111,19 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38176d9b44ea84e9184eff0bc34cc167ed044f816accfe5922e54d84cf48eca2" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.99" @@ -3829,6 +4153,16 @@ version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6" +[[package]] +name = "web-sys" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04dd7223427d52553d3702c004d3b2fe07c148165faa56313cb00211e31c12bc" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "web-time" version = "1.1.0" @@ -3889,6 +4223,36 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-registry" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" +dependencies = [ + "windows-result", + "windows-strings", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result", + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.48.0" diff --git a/Cargo.toml b/Cargo.toml index b7f59b6..74826bf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -133,3 +133,7 @@ rpassword.workspace = true argon2.workspace = true pbkdf2.workspace = true password-hash.workspace = true +reqwest = "0.12.12" +rustical_xml.workspace = true +rustical_dav.workspace = true +quick-xml.workspace = true diff --git a/crates/caldav/src/subscription.rs b/crates/caldav/src/subscription.rs index acf6f73..f948150 100644 --- a/crates/caldav/src/subscription.rs +++ b/crates/caldav/src/subscription.rs @@ -2,7 +2,11 @@ use actix_web::{ web::{self, Data, Path}, HttpResponse, }; +use rustical_dav::xml::multistatus::PropstatElement; use rustical_store::SubscriptionStore; +use rustical_xml::{XmlRootTag, XmlSerialize}; + +use crate::calendar::resource::CalendarProp; async fn handle_delete( store: Data, @@ -18,3 +22,9 @@ pub fn subscription_resource() -> actix_web::Reso .name("subscription") .delete(handle_delete::) } + +#[derive(XmlSerialize, XmlRootTag)] +#[xml(root = b"push-message", ns = "rustical_dav::namespace::NS_DAVPUSH")] +pub struct PushMessage { + propstat: PropstatElement, +} diff --git a/crates/dav/src/xml/multistatus.rs b/crates/dav/src/xml/multistatus.rs index fb9dd74..084cd76 100644 --- a/crates/dav/src/xml/multistatus.rs +++ b/crates/dav/src/xml/multistatus.rs @@ -14,10 +14,12 @@ pub struct PropTagWrapper(#[xml(flatten, ty = "untagged")] pub // RFC 2518 // -#[derive(XmlSerialize)] +#[derive(XmlSerialize, Debug)] pub struct PropstatElement { + #[xml(ns = "crate::namespace::NS_DAV")] pub prop: PropType, #[xml(serialize_with = "xml_serialize_status")] + #[xml(ns = "crate::namespace::NS_DAV")] pub status: StatusCode, } diff --git a/crates/store/src/lib.rs b/crates/store/src/lib.rs index dea7f87..081caea 100644 --- a/crates/store/src/lib.rs +++ b/crates/store/src/lib.rs @@ -16,3 +16,24 @@ pub use subscription_store::*; pub use addressbook::{AddressObject, Addressbook}; pub use calendar::{Calendar, CalendarObject}; + +#[derive(Debug, Clone)] +pub enum CollectionOperationType { + // Sync-Token increased + Object, + Delete, +} + +#[derive(Debug, Clone)] +pub enum CollectionOperationDomain { + Calendar, + Addressbook, +} + +#[derive(Debug, Clone)] +pub struct CollectionOperation { + pub r#type: CollectionOperationType, + pub domain: CollectionOperationDomain, + pub topic: String, + pub sync_token: Option, +} diff --git a/crates/store/src/subscription_store.rs b/crates/store/src/subscription_store.rs index 12f0f0f..1272b19 100644 --- a/crates/store/src/subscription_store.rs +++ b/crates/store/src/subscription_store.rs @@ -9,7 +9,7 @@ pub struct Subscription { pub push_resource: String, } -#[async_trait(?Send)] +#[async_trait] pub trait SubscriptionStore: Send + Sync + 'static { async fn get_subscriptions(&self, topic: &str) -> Result, Error>; async fn get_subscription(&self, id: &str) -> Result; diff --git a/crates/store_sqlite/Cargo.toml b/crates/store_sqlite/Cargo.toml index 27c4513..165d9ae 100644 --- a/crates/store_sqlite/Cargo.toml +++ b/crates/store_sqlite/Cargo.toml @@ -7,6 +7,7 @@ repository.workspace = true publish = false [dependencies] +tokio.workspace = true rustical_store = { workspace = true } async-trait = { workspace = true } serde = { workspace = true } diff --git a/crates/store_sqlite/src/calendar_store.rs b/crates/store_sqlite/src/calendar_store.rs index c74c97e..de564e0 100644 --- a/crates/store_sqlite/src/calendar_store.rs +++ b/crates/store_sqlite/src/calendar_store.rs @@ -1,8 +1,12 @@ -use super::{ChangeOperation, SqliteStore}; +use super::ChangeOperation; use async_trait::async_trait; +use rustical_store::synctoken::format_synctoken; use rustical_store::{Calendar, CalendarObject, CalendarStore, Error}; +use rustical_store::{CollectionOperation, CollectionOperationType}; use sqlx::Sqlite; +use sqlx::SqlitePool; use sqlx::Transaction; +use tokio::sync::mpsc::Sender; use tracing::instrument; #[derive(Debug, Clone)] @@ -26,16 +30,21 @@ async fn log_object_operation( cal_id: &str, object_id: &str, operation: ChangeOperation, -) -> Result<(), Error> { - sqlx::query!( +) -> Result { + struct Synctoken { + synctoken: i64, + } + let Synctoken { synctoken } = sqlx::query_as!( + Synctoken, r#" UPDATE calendars SET synctoken = synctoken + 1 - WHERE (principal, id) = (?1, ?2)"#, + WHERE (principal, id) = (?1, ?2) + RETURNING synctoken"#, principal, cal_id ) - .execute(&mut **tx) + .fetch_one(&mut **tx) .await .map_err(crate::Error::from)?; @@ -53,11 +62,23 @@ async fn log_object_operation( .execute(&mut **tx) .await .map_err(crate::Error::from)?; - Ok(()) + Ok(format_synctoken(synctoken)) +} + +#[derive(Debug)] +pub struct SqliteCalendarStore { + db: SqlitePool, + sender: Sender, +} + +impl SqliteCalendarStore { + pub fn new(db: SqlitePool, sender: Sender) -> Self { + Self { db, sender } + } } #[async_trait] -impl CalendarStore for SqliteStore { +impl CalendarStore for SqliteCalendarStore { #[instrument] async fn get_calendar(&self, principal: &str, id: &str) -> Result { let cal = sqlx::query_as!( @@ -157,6 +178,12 @@ impl CalendarStore for SqliteStore { id: &str, use_trashbin: bool, ) -> Result<(), Error> { + let cal = match self.get_calendar(principal, id).await { + Ok(cal) => Some(cal), + Err(Error::NotFound) => None, + Err(err) => return Err(err), + }; + match use_trashbin { true => { sqlx::query!( @@ -177,6 +204,16 @@ impl CalendarStore for SqliteStore { .map_err(crate::Error::from)?; } }; + + if let Some(cal) = cal { + // TODO: Watch for errors here? + let _ = self.sender.try_send(CollectionOperation { + r#type: CollectionOperationType::Delete, + domain: rustical_store::CollectionOperationDomain::Calendar, + topic: cal.push_topic, + sync_token: None, + }); + } Ok(()) } @@ -267,7 +304,7 @@ impl CalendarStore for SqliteStore { .await .map_err(crate::Error::from)?; - log_object_operation( + let synctoken = log_object_operation( &mut tx, &principal, &cal_id, @@ -276,6 +313,14 @@ impl CalendarStore for SqliteStore { ) .await?; + // TODO: Watch for errors here? + let _ = self.sender.try_send(CollectionOperation { + r#type: CollectionOperationType::Object, + domain: rustical_store::CollectionOperationDomain::Calendar, + topic: self.get_calendar(&principal, &cal_id).await?.push_topic, + sync_token: Some(synctoken), + }); + tx.commit().await.map_err(crate::Error::from)?; Ok(()) } @@ -312,8 +357,16 @@ impl CalendarStore for SqliteStore { .map_err(crate::Error::from)?; } }; - log_object_operation(&mut tx, principal, cal_id, id, ChangeOperation::Delete).await?; + let synctoken = + log_object_operation(&mut tx, principal, cal_id, id, ChangeOperation::Delete).await?; tx.commit().await.map_err(crate::Error::from)?; + // TODO: Watch for errors here? + let _ = self.sender.try_send(CollectionOperation { + r#type: CollectionOperationType::Object, + domain: rustical_store::CollectionOperationDomain::Calendar, + topic: self.get_calendar(principal, cal_id).await?.push_topic, + sync_token: Some(synctoken), + }); Ok(()) } @@ -335,8 +388,17 @@ impl CalendarStore for SqliteStore { .execute(&mut *tx) .await.map_err(crate::Error::from)?; - log_object_operation(&mut tx, principal, cal_id, object_id, ChangeOperation::Add).await?; + let synctoken = + log_object_operation(&mut tx, principal, cal_id, object_id, ChangeOperation::Add) + .await?; tx.commit().await.map_err(crate::Error::from)?; + // TODO: Watch for errors here? + let _ = self.sender.try_send(CollectionOperation { + r#type: CollectionOperationType::Object, + domain: rustical_store::CollectionOperationDomain::Calendar, + topic: self.get_calendar(principal, cal_id).await?.push_topic, + sync_token: Some(synctoken), + }); Ok(()) } diff --git a/crates/store_sqlite/src/subscription_store.rs b/crates/store_sqlite/src/subscription_store.rs index 5d57300..083df93 100644 --- a/crates/store_sqlite/src/subscription_store.rs +++ b/crates/store_sqlite/src/subscription_store.rs @@ -2,7 +2,7 @@ use crate::SqliteStore; use async_trait::async_trait; use rustical_store::{Error, Subscription, SubscriptionStore}; -#[async_trait(?Send)] +#[async_trait] impl SubscriptionStore for SqliteStore { async fn get_subscriptions(&self, topic: &str) -> Result, Error> { Ok(sqlx::query_as!( diff --git a/src/main.rs b/src/main.rs index f0407b3..27741c4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,17 +1,22 @@ use crate::config::Config; -use actix_web::http::KeepAlive; +use actix_web::http::{KeepAlive, StatusCode}; use actix_web::HttpServer; use anyhow::Result; use app::make_app; use clap::{Parser, Subcommand}; use commands::{cmd_gen_config, cmd_pwhash}; use config::{DataStoreConfig, SqliteDataStoreConfig}; +use rustical_dav::xml::multistatus::PropstatElement; use rustical_store::auth::StaticUserStore; -use rustical_store::{AddressbookStore, CalendarStore, SubscriptionStore}; +use rustical_store::{AddressbookStore, CalendarStore, CollectionOperation, SubscriptionStore}; +use rustical_store_sqlite::calendar_store::SqliteCalendarStore; use rustical_store_sqlite::{create_db_pool, SqliteStore}; +use rustical_xml::{XmlRootTag, XmlSerialize, XmlSerializeRoot}; use setup_tracing::setup_tracing; use std::fs; use std::sync::Arc; +use tokio::sync::mpsc::Receiver; +use tracing::{error, info}; mod app; mod commands; @@ -43,20 +48,43 @@ async fn get_data_stores( Arc, Arc, Arc, + Receiver, )> { Ok(match &config { DataStoreConfig::Sqlite(SqliteDataStoreConfig { db_url }) => { let db = create_db_pool(db_url, migrate).await?; - let sqlite_store = Arc::new(SqliteStore::new(db)); - ( - sqlite_store.clone(), - sqlite_store.clone(), - sqlite_store.clone(), - ) + // 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 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) } }) } +// TODO: Move this code somewhere else :) + +#[derive(XmlSerialize, Debug)] +struct PushMessageProp { + #[xml(ns = "rustical_dav::namespace::NS_DAV")] + topic: String, + #[xml(ns = "rustical_dav::namespace::NS_DAV")] + sync_token: Option, +} + +#[derive(XmlSerialize, XmlRootTag, Debug)] +#[xml(root = b"push-message", ns = "rustical_dav::namespace::NS_DAVPUSH")] +#[xml(ns_prefix( + rustical_dav::namespace::NS_DAVPUSH = b"", + rustical_dav::namespace::NS_DAV = b"D", +))] +struct PushMessage { + #[xml(ns = "rustical_dav::namespace::NS_DAV")] + propstat: PropstatElement, +} + #[tokio::main] async fn main() -> Result<()> { let args = Args::parse(); @@ -69,9 +97,59 @@ async fn main() -> Result<()> { setup_tracing(&config.tracing); - let (addr_store, cal_store, subscription_store) = + let (addr_store, cal_store, subscription_store, mut update_recv) = get_data_stores(!args.no_migrations, &config.data_store).await?; + let subscription_store_clone = subscription_store.clone(); + 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 + { + let status = match message.r#type { + rustical_store::CollectionOperationType::Object => StatusCode::OK, + rustical_store::CollectionOperationType::Delete => { + StatusCode::NOT_FOUND + } + }; + let push_message = PushMessage { + propstat: PropstatElement { + prop: PushMessageProp { + topic: message.topic, + sync_token: message.sync_token, + }, + status, + }, + }; + let mut output: Vec<_> = + b"\n".into(); + let mut writer = quick_xml::Writer::new_with_indent(&mut output, b' ', 4); + if let Err(err) = push_message.serialize_root(&mut writer) { + error!("Could not serialize push message: {}", err); + continue; + } + let payload = String::from_utf8(output).unwrap(); + for subscriber in subscribers { + info!( + "Sending a push message to {}: {}", + subscriber.push_resource, payload + ); + let client = reqwest::Client::new(); + if let Err(err) = client + .post(subscriber.push_resource) + .body(payload.to_owned()) + .send() + .await + { + error!("{err}"); + } + } + } + } + }); + let user_store = Arc::new(match config.auth { config::AuthConfig::Static(config) => StaticUserStore::new(config), });