From 357b115c623be94c271a3428a37a77804a7263e3 Mon Sep 17 00:00:00 2001 From: Lennart <18233294+lennart-k@users.noreply.github.com> Date: Mon, 6 Jan 2025 17:14:27 +0100 Subject: [PATCH] store: Implement a contact birthday store --- .../store/src/addressbook/address_object.rs | 51 ++++++- crates/store/src/calendar/timestamp.rs | 20 ++- crates/store/src/contact_birthday_store.rs | 143 ++++++++++++++++++ crates/store/src/error.rs | 4 + crates/store/src/lib.rs | 2 + 5 files changed, 218 insertions(+), 2 deletions(-) create mode 100644 crates/store/src/contact_birthday_store.rs diff --git a/crates/store/src/addressbook/address_object.rs b/crates/store/src/addressbook/address_object.rs index 21647cc..81e7e5c 100644 --- a/crates/store/src/addressbook/address_object.rs +++ b/crates/store/src/addressbook/address_object.rs @@ -1,6 +1,10 @@ use std::{collections::HashMap, io::BufReader}; -use crate::{calendar::CalDateTime, Error}; +use crate::{ + calendar::{CalDateTime, LOCAL_DATE}, + CalendarObject, Error, +}; +use chrono::Datelike; use ical::parser::{ vcard::{self, component::VcardContact}, Component, @@ -49,4 +53,49 @@ impl AddressObject { let prop = self.vcard.get_property("BDAY")?; CalDateTime::parse_prop(prop, &HashMap::default()).unwrap_or(None) } + + pub fn get_full_name(&self) -> Option<&String> { + let prop = self.vcard.get_property("FN")?; + prop.value.as_ref() + } + + pub fn get_birthday_object(&self) -> Result, Error> { + Ok(if let Some(birthday) = self.get_birthday() { + let fullname = if let Some(name) = self.get_full_name() { + name + } else { + return Ok(None); + }; + let birthday = birthday.date(); + let year = birthday.year(); + let birthday_start = birthday.format(LOCAL_DATE); + let birthday_end = birthday.succ_opt().unwrap_or(birthday).format(LOCAL_DATE); + Some(CalendarObject::from_ics( + self.get_id().to_owned(), + format!( + r#"BEGIN:VCALENDAR +VERSION:2.0 +CALSCALE:GREGORIAN +PRODID:-//github.com/lennart-k/rustical birthday calendar//EN +BEGIN:VEVENT +DTSTART;VALUE=DATE:{birthday_start} +DTEND;VALUE=DATE:{birthday_end} +UID:{uid} +RRULE:FREQ=YEARLY +SUMMARY:🎂 {fullname} ({year}) +TRANSP:TRANSPARENT +BEGIN:VALARM +TRIGGER;VALUE=DURATION:-PT0M +ACTION:DISPLAY +DESCRIPTION:🎂 {fullname} ({year}) +END:VALARM +END:VEVENT +END:VCALENDAR"#, + uid = self.get_id(), + ), + )?) + } else { + None + }) + } } diff --git a/crates/store/src/calendar/timestamp.rs b/crates/store/src/calendar/timestamp.rs index ac8588c..242970b 100644 --- a/crates/store/src/calendar/timestamp.rs +++ b/crates/store/src/calendar/timestamp.rs @@ -16,7 +16,7 @@ lazy_static! { const LOCAL_DATE_TIME: &str = "%Y%m%dT%H%M%S"; const UTC_DATE_TIME: &str = "%Y%m%dT%H%M%SZ"; -const LOCAL_DATE: &str = "%Y%m%d"; +pub const LOCAL_DATE: &str = "%Y%m%d"; #[derive(Debug, Clone, Deref, PartialEq)] pub struct UtcDateTime(DateTime); @@ -125,6 +125,24 @@ impl CalDateTime { Self::parse(&prop_value, timezone).map(Some) } + pub fn format(&self) -> String { + match self { + Self::Utc(utc) => utc.format(UTC_DATE_TIME).to_string(), + Self::Date(date) => date.format(LOCAL_DATE).to_string(), + Self::Local(datetime) => datetime.format(LOCAL_DATE_TIME).to_string(), + Self::OlsonTZ(datetime) => datetime.format(LOCAL_DATE_TIME).to_string(), + } + } + + pub fn date(&self) -> NaiveDate { + match self { + Self::Utc(utc) => utc.date_naive(), + Self::Date(date) => date.to_owned(), + Self::Local(datetime) => datetime.date(), + Self::OlsonTZ(datetime) => datetime.date_naive(), + } + } + pub fn parse(value: &str, timezone: Option) -> Result { if let Ok(datetime) = NaiveDateTime::parse_from_str(value, LOCAL_DATE_TIME) { if let Some(timezone) = timezone { diff --git a/crates/store/src/contact_birthday_store.rs b/crates/store/src/contact_birthday_store.rs new file mode 100644 index 0000000..5dccccb --- /dev/null +++ b/crates/store/src/contact_birthday_store.rs @@ -0,0 +1,143 @@ +use crate::{ + AddressObject, Addressbook, AddressbookStore, Calendar, CalendarObject, CalendarStore, Error, +}; +use async_trait::async_trait; + +pub struct ContactBirthdayStore(AS); + +fn birthday_calendar(addressbook: Addressbook) -> Calendar { + Calendar { + principal: addressbook.principal, + id: addressbook.id, + displayname: addressbook + .displayname + .map(|name| format!("{} birthdays", name)), + order: 0, + description: None, + color: None, + timezone: None, + timezone_id: None, + deleted_at: addressbook.deleted_at, + synctoken: addressbook.synctoken, + subscription_url: None, + } +} + +#[async_trait] +impl CalendarStore for ContactBirthdayStore { + async fn get_calendar(&self, principal: &str, id: &str) -> Result { + let addressbook = self.0.get_addressbook(principal, id).await?; + Ok(birthday_calendar(addressbook)) + } + async fn get_calendars(&self, principal: &str) -> Result, Error> { + let addressbooks = self.0.get_addressbooks(principal).await?; + Ok(addressbooks.into_iter().map(birthday_calendar).collect()) + } + + async fn get_deleted_calendars(&self, principal: &str) -> Result, Error> { + let addressbooks = self.0.get_deleted_addressbooks(principal).await?; + Ok(addressbooks.into_iter().map(birthday_calendar).collect()) + } + + async fn update_calendar( + &self, + _principal: String, + _id: String, + _calendar: Calendar, + ) -> Result<(), Error> { + Err(Error::ReadOnly) + } + + async fn insert_calendar(&self, _calendar: Calendar) -> Result<(), Error> { + Err(Error::ReadOnly) + } + async fn delete_calendar( + &self, + _principal: &str, + _name: &str, + _use_trashbin: bool, + ) -> Result<(), Error> { + Err(Error::ReadOnly) + } + + async fn restore_calendar(&self, _principal: &str, _name: &str) -> Result<(), Error> { + Err(Error::ReadOnly) + } + + async fn sync_changes( + &self, + principal: &str, + cal_id: &str, + synctoken: i64, + ) -> Result<(Vec, Vec, i64), Error> { + let (objects, deleted_objects, new_synctoken) = + self.0.sync_changes(principal, cal_id, synctoken).await?; + let objects: Result>, Error> = objects + .iter() + .map(AddressObject::get_birthday_object) + .collect(); + let objects = objects?.into_iter().flatten().collect(); + + Ok((objects, deleted_objects, new_synctoken)) + } + + async fn get_objects( + &self, + principal: &str, + cal_id: &str, + ) -> Result, Error> { + let objects: Result>, Error> = self + .0 + .get_objects(principal, cal_id) + .await? + .iter() + .map(AddressObject::get_birthday_object) + .collect(); + let objects = objects?.into_iter().flatten().collect(); + + Ok(objects) + } + + async fn get_object( + &self, + principal: &str, + cal_id: &str, + object_id: &str, + ) -> Result { + Ok(self + .0 + .get_object(principal, cal_id, object_id) + .await? + .get_birthday_object()? + .ok_or(Error::NotFound)?) + } + + async fn put_object( + &self, + _principal: String, + _cal_id: String, + _object: CalendarObject, + _overwrite: bool, + ) -> Result<(), Error> { + Err(Error::ReadOnly) + } + + async fn delete_object( + &self, + _principal: &str, + _cal_id: &str, + _object_id: &str, + _use_trashbin: bool, + ) -> Result<(), Error> { + Err(Error::ReadOnly) + } + + async fn restore_object( + &self, + _principal: &str, + _cal_id: &str, + _object_id: &str, + ) -> Result<(), Error> { + Err(Error::ReadOnly) + } +} diff --git a/crates/store/src/error.rs b/crates/store/src/error.rs index ad9a274..445bd54 100644 --- a/crates/store/src/error.rs +++ b/crates/store/src/error.rs @@ -12,6 +12,9 @@ pub enum Error { #[error("Invalid ics/vcf input: {0}")] InvalidData(String), + #[error("Read-only")] + ReadOnly, + #[error(transparent)] ParserError(#[from] ical::parser::ParserError), @@ -25,6 +28,7 @@ impl ResponseError for Error { Self::NotFound => StatusCode::NOT_FOUND, Self::AlreadyExists => StatusCode::CONFLICT, Self::InvalidData(_) => StatusCode::BAD_REQUEST, + Self::ReadOnly => StatusCode::FORBIDDEN, _ => StatusCode::INTERNAL_SERVER_ERROR, } } diff --git a/crates/store/src/lib.rs b/crates/store/src/lib.rs index 6f10041..8cb16ac 100644 --- a/crates/store/src/lib.rs +++ b/crates/store/src/lib.rs @@ -5,10 +5,12 @@ pub mod error; pub use error::Error; pub mod auth; pub mod calendar; +mod contact_birthday_store; pub mod synctoken; pub use addressbook_store::AddressbookStore; pub use calendar_store::CalendarStore; +pub use contact_birthday_store::ContactBirthdayStore; pub use addressbook::{AddressObject, Addressbook}; pub use calendar::{Calendar, CalendarObject};