Skip to content

Commit

Permalink
store: Implement a contact birthday store
Browse files Browse the repository at this point in the history
  • Loading branch information
lennart-k committed Jan 6, 2025
1 parent d582d0d commit 357b115
Show file tree
Hide file tree
Showing 5 changed files with 218 additions and 2 deletions.
51 changes: 50 additions & 1 deletion crates/store/src/addressbook/address_object.rs
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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<Option<CalendarObject>, 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
})
}
}
20 changes: 19 additions & 1 deletion crates/store/src/calendar/timestamp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Utc>);
Expand Down Expand Up @@ -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<Tz>) -> Result<Self, Error> {
if let Ok(datetime) = NaiveDateTime::parse_from_str(value, LOCAL_DATE_TIME) {
if let Some(timezone) = timezone {
Expand Down
143 changes: 143 additions & 0 deletions crates/store/src/contact_birthday_store.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
use crate::{
AddressObject, Addressbook, AddressbookStore, Calendar, CalendarObject, CalendarStore, Error,
};
use async_trait::async_trait;

pub struct ContactBirthdayStore<AS: AddressbookStore>(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<AS: AddressbookStore> CalendarStore for ContactBirthdayStore<AS> {
async fn get_calendar(&self, principal: &str, id: &str) -> Result<Calendar, Error> {
let addressbook = self.0.get_addressbook(principal, id).await?;
Ok(birthday_calendar(addressbook))
}
async fn get_calendars(&self, principal: &str) -> Result<Vec<Calendar>, 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<Vec<Calendar>, 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<CalendarObject>, Vec<String>, i64), Error> {
let (objects, deleted_objects, new_synctoken) =
self.0.sync_changes(principal, cal_id, synctoken).await?;
let objects: Result<Vec<Option<CalendarObject>>, 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<Vec<CalendarObject>, Error> {
let objects: Result<Vec<Option<CalendarObject>>, 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<CalendarObject, Error> {
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)
}
}
4 changes: 4 additions & 0 deletions crates/store/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),

Expand All @@ -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,
}
}
Expand Down
2 changes: 2 additions & 0 deletions crates/store/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

0 comments on commit 357b115

Please sign in to comment.