build MVP for birthday calendar

This commit is contained in:
Lennart K
2026-01-13 12:41:03 +01:00
parent 7eecd95757
commit 5ec2787ecf
5 changed files with 172 additions and 58 deletions

2
Cargo.lock generated
View File

@@ -1771,7 +1771,7 @@ dependencies = [
[[package]] [[package]]
name = "ical" name = "ical"
version = "0.11.0" 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 = [ dependencies = [
"chrono", "chrono",
"chrono-tz", "chrono-tz",

View File

@@ -1,7 +1,20 @@
use crate::{CalendarObject, Error}; use crate::{CalendarObject, Error};
use chrono::{NaiveDate, Utc};
use ical::component::{
CalendarInnerDataBuilder, IcalAlarmBuilder, IcalCalendarObjectBuilder, IcalEventBuilder,
};
use ical::generator::Emitter; use ical::generator::Emitter;
use ical::parser::vcard::{self, component::VcardContact}; 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 sha2::{Digest, Sha256};
use std::str::FromStr;
use std::{collections::HashMap, io::BufReader}; use std::{collections::HashMap, io::BufReader};
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@@ -36,24 +49,115 @@ impl AddressObject {
&self.vcf &self.vcf
} }
fn get_significant_date_object(
&self,
date: &PartialDate,
summary_prefix: &str,
suffix: &str,
) -> Result<Option<CalendarObject>, 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<Option<CalendarObject>, Error> { pub fn get_anniversary_object(&self) -> Result<Option<CalendarObject>, 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<Option<CalendarObject>, Error> { pub fn get_birthday_object(&self) -> Result<Option<CalendarObject>, 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 self.get_significant_date_object(date, "🎂", "-birthday")
pub fn get_significant_dates(&self) -> Result<HashMap<&'static str, CalendarObject>, 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)
} }
#[must_use] #[must_use]

View File

@@ -279,7 +279,7 @@ impl CalendarStore for SqliteAddressbookStore {
.strip_prefix(BIRTHDAYS_PREFIX) .strip_prefix(BIRTHDAYS_PREFIX)
.ok_or(Error::NotFound)? .ok_or(Error::NotFound)?
.to_string(); .to_string();
Self::_update_birthday_calendar(&self.db, &principal, &calendar).await Self::_update_birthday_calendar(&self.db, principal, &calendar).await
} }
#[instrument] #[instrument]
@@ -330,14 +330,29 @@ impl CalendarStore for SqliteAddressbookStore {
.ok_or(Error::NotFound)?; .ok_or(Error::NotFound)?;
let (objects, deleted_objects, new_synctoken) = let (objects, deleted_objects, new_synctoken) =
AddressbookStore::sync_changes(self, principal, cal_id, synctoken).await?; AddressbookStore::sync_changes(self, principal, cal_id, synctoken).await?;
todo!();
// let objects: Result<Vec<Option<CalendarObject>>, rustical_ical::Error> = objects let mut out_objects = vec![];
// .iter()
// .map(AddressObject::get_birthday_object) for (object_id, object) in objects {
// .collect(); if let Some(birthday) = object.get_birthday_object()? {
// let objects = objects?.into_iter().flatten().collect(); out_objects.push((format!("{object_id}-birthday"), birthday));
// }
// Ok((objects, deleted_objects, new_synctoken)) 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] #[instrument]
@@ -358,22 +373,19 @@ impl CalendarStore for SqliteAddressbookStore {
principal: &str, principal: &str,
cal_id: &str, cal_id: &str,
) -> Result<Vec<(String, CalendarObject)>, Error> { ) -> Result<Vec<(String, CalendarObject)>, Error> {
todo!() let mut objects = vec![];
// let cal_id = cal_id let cal_id = cal_id
// .strip_prefix(BIRTHDAYS_PREFIX) .strip_prefix(BIRTHDAYS_PREFIX)
// .ok_or(Error::NotFound)?; .ok_or(Error::NotFound)?;
// let objects: Result<Vec<HashMap<&'static str, CalendarObject>>, rustical_ical::Error> = for (object_id, object) in AddressbookStore::get_objects(self, principal, cal_id).await? {
// AddressbookStore::get_objects(self, principal, cal_id) if let Some(birthday) = object.get_birthday_object()? {
// .await? objects.push((format!("{object_id}-birthday"), birthday));
// .iter() }
// .map(AddressObject::get_significant_dates) if let Some(anniversary) = object.get_anniversary_object()? {
// .collect(); objects.push((format!("{object_id}-anniversayr"), anniversary));
// let objects = objects? }
// .into_iter() }
// .flat_map(HashMap::into_values) Ok(objects)
// .collect();
//
// Ok(objects)
} }
#[instrument] #[instrument]
@@ -388,11 +400,14 @@ impl CalendarStore for SqliteAddressbookStore {
.strip_prefix(BIRTHDAYS_PREFIX) .strip_prefix(BIRTHDAYS_PREFIX)
.ok_or(Error::NotFound)?; .ok_or(Error::NotFound)?;
let (addressobject_id, date_type) = object_id.rsplit_once('-').ok_or(Error::NotFound)?; let (addressobject_id, date_type) = object_id.rsplit_once('-').ok_or(Error::NotFound)?;
let obj =
AddressbookStore::get_object(self, principal, cal_id, addressobject_id, show_deleted) AddressbookStore::get_object(self, principal, cal_id, addressobject_id, show_deleted)
.await? .await?;
.get_significant_dates()? match date_type {
.remove(date_type) "birthday" => Ok(obj.get_birthday_object()?.ok_or(Error::NotFound)?),
.ok_or(Error::NotFound) "anniversary" => Ok(obj.get_anniversary_object()?.ok_or(Error::NotFound)?),
_ => Err(Error::NotFound),
}
} }
#[instrument] #[instrument]

View File

@@ -509,7 +509,7 @@ impl AddressbookStore for SqliteAddressbookStore {
) -> Result<(), rustical_store::Error> { ) -> Result<(), rustical_store::Error> {
assert_eq!(principal, &addressbook.principal); assert_eq!(principal, &addressbook.principal);
assert_eq!(id, &addressbook.id); assert_eq!(id, &addressbook.id);
Self::_update_addressbook(&self.db, &principal, &id, &addressbook).await Self::_update_addressbook(&self.db, principal, id, &addressbook).await
} }
#[instrument] #[instrument]
@@ -648,9 +648,9 @@ impl AddressbookStore for SqliteAddressbookStore {
let sync_token = Self::log_object_operation( let sync_token = Self::log_object_operation(
&mut tx, &mut tx,
&principal, principal,
&addressbook_id, addressbook_id,
&object_id, object_id,
ChangeOperation::Add, ChangeOperation::Add,
) )
.await .await

View File

@@ -880,7 +880,7 @@ impl CalendarStore for SqliteCalendarStore {
.await .await
.map_err(crate::Error::from)?; .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() { if calendar.subscription_url.is_some() {
// We cannot commit an object to a subscription calendar // We cannot commit an object to a subscription calendar
return Err(Error::ReadOnly); return Err(Error::ReadOnly);
@@ -891,17 +891,14 @@ impl CalendarStore for SqliteCalendarStore {
sync_token = Some( sync_token = Some(
Self::log_object_operation( Self::log_object_operation(
&mut tx, &mut tx,
&principal, principal,
&cal_id, cal_id,
&object_id, &object_id,
ChangeOperation::Add, ChangeOperation::Add,
) )
.await?, .await?,
); );
Self::_put_object( Self::_put_object(&mut *tx, principal, cal_id, &object_id, &object, overwrite).await?;
&mut *tx, &principal, &cal_id, &object_id, &object, overwrite,
)
.await?;
} }
tx.commit().await.map_err(crate::Error::from)?; tx.commit().await.map_err(crate::Error::from)?;
@@ -909,9 +906,7 @@ impl CalendarStore for SqliteCalendarStore {
if let Some(sync_token) = sync_token { if let Some(sync_token) = sync_token {
self.send_push_notification( self.send_push_notification(
CollectionOperationInfo::Content { sync_token }, CollectionOperationInfo::Content { sync_token },
self.get_calendar(&principal, &cal_id, true) self.get_calendar(principal, cal_id, true).await?.push_topic,
.await?
.push_topic,
); );
} }
Ok(()) Ok(())