mirror of
https://github.com/lennart-k/rustical.git
synced 2025-12-14 11:42:25 +00:00
store: Implement a contact birthday store
This commit is contained in:
@@ -1,6 +1,10 @@
|
|||||||
use std::{collections::HashMap, io::BufReader};
|
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::{
|
use ical::parser::{
|
||||||
vcard::{self, component::VcardContact},
|
vcard::{self, component::VcardContact},
|
||||||
Component,
|
Component,
|
||||||
@@ -49,4 +53,49 @@ impl AddressObject {
|
|||||||
let prop = self.vcard.get_property("BDAY")?;
|
let prop = self.vcard.get_property("BDAY")?;
|
||||||
CalDateTime::parse_prop(prop, &HashMap::default()).unwrap_or(None)
|
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 LOCAL_DATE_TIME: &str = "%Y%m%dT%H%M%S";
|
||||||
const UTC_DATE_TIME: &str = "%Y%m%dT%H%M%SZ";
|
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)]
|
#[derive(Debug, Clone, Deref, PartialEq)]
|
||||||
pub struct UtcDateTime(DateTime<Utc>);
|
pub struct UtcDateTime(DateTime<Utc>);
|
||||||
@@ -125,6 +125,24 @@ impl CalDateTime {
|
|||||||
Self::parse(&prop_value, timezone).map(Some)
|
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> {
|
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 Ok(datetime) = NaiveDateTime::parse_from_str(value, LOCAL_DATE_TIME) {
|
||||||
if let Some(timezone) = timezone {
|
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}")]
|
#[error("Invalid ics/vcf input: {0}")]
|
||||||
InvalidData(String),
|
InvalidData(String),
|
||||||
|
|
||||||
|
#[error("Read-only")]
|
||||||
|
ReadOnly,
|
||||||
|
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
ParserError(#[from] ical::parser::ParserError),
|
ParserError(#[from] ical::parser::ParserError),
|
||||||
|
|
||||||
@@ -25,6 +28,7 @@ impl ResponseError for Error {
|
|||||||
Self::NotFound => StatusCode::NOT_FOUND,
|
Self::NotFound => StatusCode::NOT_FOUND,
|
||||||
Self::AlreadyExists => StatusCode::CONFLICT,
|
Self::AlreadyExists => StatusCode::CONFLICT,
|
||||||
Self::InvalidData(_) => StatusCode::BAD_REQUEST,
|
Self::InvalidData(_) => StatusCode::BAD_REQUEST,
|
||||||
|
Self::ReadOnly => StatusCode::FORBIDDEN,
|
||||||
_ => StatusCode::INTERNAL_SERVER_ERROR,
|
_ => StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,10 +5,12 @@ pub mod error;
|
|||||||
pub use error::Error;
|
pub use error::Error;
|
||||||
pub mod auth;
|
pub mod auth;
|
||||||
pub mod calendar;
|
pub mod calendar;
|
||||||
|
mod contact_birthday_store;
|
||||||
pub mod synctoken;
|
pub mod synctoken;
|
||||||
|
|
||||||
pub use addressbook_store::AddressbookStore;
|
pub use addressbook_store::AddressbookStore;
|
||||||
pub use calendar_store::CalendarStore;
|
pub use calendar_store::CalendarStore;
|
||||||
|
pub use contact_birthday_store::ContactBirthdayStore;
|
||||||
|
|
||||||
pub use addressbook::{AddressObject, Addressbook};
|
pub use addressbook::{AddressObject, Addressbook};
|
||||||
pub use calendar::{Calendar, CalendarObject};
|
pub use calendar::{Calendar, CalendarObject};
|
||||||
|
|||||||
Reference in New Issue
Block a user