mirror of
https://github.com/lennart-k/rustical.git
synced 2026-01-30 11:48:18 +00:00
build MVP for birthday calendar
This commit is contained in:
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -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",
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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(())
|
||||||
|
|||||||
Reference in New Issue
Block a user