diff --git a/Cargo.lock b/Cargo.lock index 4f6ad18..faf1b2e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1771,7 +1771,7 @@ dependencies = [ [[package]] name = "ical" version = "0.11.0" -source = "git+https://github.com/lennart-k/ical-rs?branch=dev#28e982f928a73f5af13f1e59e28da419567bf93f" +source = "git+https://github.com/lennart-k/ical-rs?branch=dev#626982a02647c3bee5c7d0828facd1b77df5722f" dependencies = [ "chrono", "chrono-tz", diff --git a/crates/ical/src/address_object.rs b/crates/ical/src/address_object.rs index 52c9db8..1cac936 100644 --- a/crates/ical/src/address_object.rs +++ b/crates/ical/src/address_object.rs @@ -1,7 +1,20 @@ use crate::{CalendarObject, Error}; +use chrono::{NaiveDate, Utc}; +use ical::component::{ + CalendarInnerDataBuilder, IcalAlarmBuilder, IcalCalendarObjectBuilder, IcalEventBuilder, +}; use ical::generator::Emitter; use ical::parser::vcard::{self, component::VcardContact}; +use ical::parser::{ + Calscale, ComponentMut, IcalCALSCALEProperty, IcalDTENDProperty, IcalDTSTAMPProperty, + IcalDTSTARTProperty, IcalPRODIDProperty, IcalRRULEProperty, IcalSUMMARYProperty, + IcalUIDProperty, IcalVERSIONProperty, IcalVersion, VcardANNIVERSARYProperty, VcardBDAYProperty, + VcardFNProperty, +}; +use ical::property::ContentLine; +use ical::types::{CalDate, PartialDate}; use sha2::{Digest, Sha256}; +use std::str::FromStr; use std::{collections::HashMap, io::BufReader}; #[derive(Debug, Clone)] @@ -36,24 +49,115 @@ impl AddressObject { &self.vcf } + fn get_significant_date_object( + &self, + date: &PartialDate, + summary_prefix: &str, + suffix: &str, + ) -> Result, Error> { + let Some(uid) = self.vcard.get_uid() else { + return Ok(None); + }; + let uid = format!("{uid}{suffix}"); + let year = date.get_year(); + let year_suffix = year.map(|year| format!(" {year}")).unwrap_or_default(); + let Some(month) = date.get_month() else { + return Ok(None); + }; + let Some(day) = date.get_day() else { + return Ok(None); + }; + let Some(dtstart) = NaiveDate::from_ymd_opt(year.unwrap_or(1900), month, day) else { + return Ok(None); + }; + let start_date = CalDate(dtstart, ical::types::Timezone::Local); + let Some(end_date) = start_date.succ_opt() else { + // start_date is MAX_DATE, this should never happen but FAPP also not raise an error + return Ok(None); + }; + let Some(VcardFNProperty(fullname, _)) = self.vcard.full_name.first() else { + return Ok(None); + }; + let summary = format!("{summary_prefix} {fullname}{year_suffix}"); + + let event = IcalEventBuilder { + properties: vec![ + IcalDTSTAMPProperty(Utc::now().into(), vec![].into()).into(), + IcalDTSTARTProperty(start_date.into(), vec![].into()).into(), + IcalDTENDProperty(end_date.into(), vec![].into()).into(), + IcalUIDProperty(uid, vec![].into()).into(), + IcalRRULEProperty( + rrule::RRule::from_str("FREQ=YEARLY").unwrap(), + vec![].into(), + ) + .into(), + IcalSUMMARYProperty(summary.clone(), vec![].into()).into(), + ContentLine { + name: "TRANSP".to_owned(), + value: Some("TRANSPARENT".to_owned()), + ..Default::default() + }, + ], + alarms: vec![IcalAlarmBuilder { + properties: vec![ + ContentLine { + name: "TRIGGER".to_owned(), + value: Some("-PT0M".to_owned()), + params: vec![("VALUE".to_owned(), vec!["DURATION".to_owned()])].into(), + }, + ContentLine { + name: "ACTION".to_owned(), + value: Some("DISPLAY".to_owned()), + ..Default::default() + }, + ContentLine { + name: "DESCRIPTION".to_owned(), + value: Some(summary), + ..Default::default() + }, + ], + }], + }; + + Ok(Some( + IcalCalendarObjectBuilder { + properties: vec![ + IcalVERSIONProperty(IcalVersion::Version2_0, vec![].into()).into(), + IcalCALSCALEProperty(Calscale::Gregorian, vec![].into()).into(), + IcalPRODIDProperty( + "-//github.com/lennart-k/rustical birthday calendar//EN".to_owned(), + vec![].into(), + ) + .into(), + ], + inner: Some(CalendarInnerDataBuilder::Event(vec![event])), + vtimezones: HashMap::default(), + } + .build(None)? + .into(), + )) + } + pub fn get_anniversary_object(&self) -> Result, Error> { - todo!(); + let Some(VcardANNIVERSARYProperty(anniversary, _)) = &self.vcard.anniversary else { + return Ok(None); + }; + let Some(date) = &anniversary.date else { + return Ok(None); + }; + + self.get_significant_date_object(date, "💍", "-anniversary") } pub fn get_birthday_object(&self) -> Result, Error> { - todo!(); - } + let Some(VcardBDAYProperty(bday, _)) = &self.vcard.birthday else { + return Ok(None); + }; + let Some(date) = &bday.date else { + return Ok(None); + }; - /// Get significant dates associated with this address object - pub fn get_significant_dates(&self) -> Result, Error> { - let mut out = HashMap::new(); - if let Some(birthday) = self.get_birthday_object()? { - out.insert("birthday", birthday); - } - if let Some(anniversary) = self.get_anniversary_object()? { - out.insert("anniversary", anniversary); - } - Ok(out) + self.get_significant_date_object(date, "🎂", "-birthday") } #[must_use] diff --git a/crates/store_sqlite/src/addressbook_store/birthday_calendar.rs b/crates/store_sqlite/src/addressbook_store/birthday_calendar.rs index 8f49cf3..2161d2c 100644 --- a/crates/store_sqlite/src/addressbook_store/birthday_calendar.rs +++ b/crates/store_sqlite/src/addressbook_store/birthday_calendar.rs @@ -279,7 +279,7 @@ impl CalendarStore for SqliteAddressbookStore { .strip_prefix(BIRTHDAYS_PREFIX) .ok_or(Error::NotFound)? .to_string(); - Self::_update_birthday_calendar(&self.db, &principal, &calendar).await + Self::_update_birthday_calendar(&self.db, principal, &calendar).await } #[instrument] @@ -330,14 +330,29 @@ impl CalendarStore for SqliteAddressbookStore { .ok_or(Error::NotFound)?; let (objects, deleted_objects, new_synctoken) = AddressbookStore::sync_changes(self, principal, cal_id, synctoken).await?; - todo!(); - // let objects: Result>, rustical_ical::Error> = objects - // .iter() - // .map(AddressObject::get_birthday_object) - // .collect(); - // let objects = objects?.into_iter().flatten().collect(); - // - // Ok((objects, deleted_objects, new_synctoken)) + + let mut out_objects = vec![]; + + for (object_id, object) in objects { + if let Some(birthday) = object.get_birthday_object()? { + out_objects.push((format!("{object_id}-birthday"), birthday)); + } + if let Some(anniversary) = object.get_anniversary_object()? { + out_objects.push((format!("{object_id}-anniversayr"), anniversary)); + } + } + + let deleted_objects = deleted_objects + .into_iter() + .flat_map(|object_id| { + [ + format!("{object_id}-birthday"), + format!("{object_id}-anniversary"), + ] + }) + .collect(); + + Ok((out_objects, deleted_objects, new_synctoken)) } #[instrument] @@ -358,22 +373,19 @@ impl CalendarStore for SqliteAddressbookStore { principal: &str, cal_id: &str, ) -> Result, Error> { - todo!() - // let cal_id = cal_id - // .strip_prefix(BIRTHDAYS_PREFIX) - // .ok_or(Error::NotFound)?; - // let objects: Result>, rustical_ical::Error> = - // AddressbookStore::get_objects(self, principal, cal_id) - // .await? - // .iter() - // .map(AddressObject::get_significant_dates) - // .collect(); - // let objects = objects? - // .into_iter() - // .flat_map(HashMap::into_values) - // .collect(); - // - // Ok(objects) + let mut objects = vec![]; + let cal_id = cal_id + .strip_prefix(BIRTHDAYS_PREFIX) + .ok_or(Error::NotFound)?; + for (object_id, object) in AddressbookStore::get_objects(self, principal, cal_id).await? { + if let Some(birthday) = object.get_birthday_object()? { + objects.push((format!("{object_id}-birthday"), birthday)); + } + if let Some(anniversary) = object.get_anniversary_object()? { + objects.push((format!("{object_id}-anniversayr"), anniversary)); + } + } + Ok(objects) } #[instrument] @@ -388,11 +400,14 @@ impl CalendarStore for SqliteAddressbookStore { .strip_prefix(BIRTHDAYS_PREFIX) .ok_or(Error::NotFound)?; let (addressobject_id, date_type) = object_id.rsplit_once('-').ok_or(Error::NotFound)?; - AddressbookStore::get_object(self, principal, cal_id, addressobject_id, show_deleted) - .await? - .get_significant_dates()? - .remove(date_type) - .ok_or(Error::NotFound) + let obj = + AddressbookStore::get_object(self, principal, cal_id, addressobject_id, show_deleted) + .await?; + match date_type { + "birthday" => Ok(obj.get_birthday_object()?.ok_or(Error::NotFound)?), + "anniversary" => Ok(obj.get_anniversary_object()?.ok_or(Error::NotFound)?), + _ => Err(Error::NotFound), + } } #[instrument] diff --git a/crates/store_sqlite/src/addressbook_store/mod.rs b/crates/store_sqlite/src/addressbook_store/mod.rs index 196c241..4d85ff8 100644 --- a/crates/store_sqlite/src/addressbook_store/mod.rs +++ b/crates/store_sqlite/src/addressbook_store/mod.rs @@ -509,7 +509,7 @@ impl AddressbookStore for SqliteAddressbookStore { ) -> Result<(), rustical_store::Error> { assert_eq!(principal, &addressbook.principal); assert_eq!(id, &addressbook.id); - Self::_update_addressbook(&self.db, &principal, &id, &addressbook).await + Self::_update_addressbook(&self.db, principal, id, &addressbook).await } #[instrument] @@ -648,9 +648,9 @@ impl AddressbookStore for SqliteAddressbookStore { let sync_token = Self::log_object_operation( &mut tx, - &principal, - &addressbook_id, - &object_id, + principal, + addressbook_id, + object_id, ChangeOperation::Add, ) .await diff --git a/crates/store_sqlite/src/calendar_store.rs b/crates/store_sqlite/src/calendar_store.rs index d0bba92..1aa301f 100644 --- a/crates/store_sqlite/src/calendar_store.rs +++ b/crates/store_sqlite/src/calendar_store.rs @@ -802,7 +802,7 @@ impl CalendarStore for SqliteCalendarStore { .await .map_err(crate::Error::from)?; - let calendar = Self::_get_calendar(&mut *tx, &principal, &cal_id, true).await?; + let calendar = Self::_get_calendar(&mut *tx, principal, cal_id, true).await?; if calendar.subscription_url.is_some() { // We cannot commit an object to a subscription calendar return Err(Error::ReadOnly); @@ -813,17 +813,14 @@ impl CalendarStore for SqliteCalendarStore { sync_token = Some( Self::log_object_operation( &mut tx, - &principal, - &cal_id, + principal, + cal_id, &object_id, ChangeOperation::Add, ) .await?, ); - Self::_put_object( - &mut *tx, &principal, &cal_id, &object_id, &object, overwrite, - ) - .await?; + Self::_put_object(&mut *tx, principal, cal_id, &object_id, &object, overwrite).await?; } tx.commit().await.map_err(crate::Error::from)?; @@ -831,9 +828,7 @@ impl CalendarStore for SqliteCalendarStore { if let Some(sync_token) = sync_token { self.send_push_notification( CollectionOperationInfo::Content { sync_token }, - self.get_calendar(&principal, &cal_id, true) - .await? - .push_topic, + self.get_calendar(principal, cal_id, true).await?.push_topic, ); } Ok(())