mirror of
https://github.com/lennart-k/rustical.git
synced 2025-12-13 22:52:22 +00:00
store: Implement a contact birthday store
This commit is contained in:
@@ -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<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
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>);
|
||||
@@ -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 {
|
||||
|
||||
143
crates/store/src/contact_birthday_store.rs
Normal file
143
crates/store/src/contact_birthday_store.rs
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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};
|
||||
|
||||
Reference in New Issue
Block a user