mirror of
https://github.com/lennart-k/rustical.git
synced 2025-12-23 04:19:24 +00:00
store: Refactoring to split calendar and addressbook
This commit is contained in:
35
crates/store/src/calendar/calendar.rs
Normal file
35
crates/store/src/calendar/calendar.rs
Normal file
@@ -0,0 +1,35 @@
|
||||
use chrono::NaiveDateTime;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Default, Clone, Deserialize, Serialize)]
|
||||
pub struct Calendar {
|
||||
pub principal: String,
|
||||
pub id: String,
|
||||
pub displayname: Option<String>,
|
||||
pub order: i64,
|
||||
pub description: Option<String>,
|
||||
pub color: Option<String>,
|
||||
pub timezone: Option<String>,
|
||||
pub deleted_at: Option<NaiveDateTime>,
|
||||
pub synctoken: i64,
|
||||
}
|
||||
|
||||
impl Calendar {
|
||||
pub fn format_synctoken(&self) -> String {
|
||||
format_synctoken(self.synctoken)
|
||||
}
|
||||
}
|
||||
|
||||
const SYNC_NAMESPACE: &str = "github.com/lennart-k/rustical/ns/";
|
||||
|
||||
pub fn format_synctoken(synctoken: i64) -> String {
|
||||
format!("{}{}", SYNC_NAMESPACE, synctoken)
|
||||
}
|
||||
|
||||
pub fn parse_synctoken(synctoken: &str) -> Option<i64> {
|
||||
if !synctoken.starts_with(SYNC_NAMESPACE) {
|
||||
return None;
|
||||
}
|
||||
let (_, synctoken) = synctoken.split_at(SYNC_NAMESPACE.len());
|
||||
synctoken.parse::<i64>().ok()
|
||||
}
|
||||
53
crates/store/src/calendar/event.rs
Normal file
53
crates/store/src/calendar/event.rs
Normal file
@@ -0,0 +1,53 @@
|
||||
use crate::{
|
||||
timestamp::{parse_duration, CalDateTime},
|
||||
Error,
|
||||
};
|
||||
use chrono::Duration;
|
||||
use ical::{
|
||||
generator::IcalEvent,
|
||||
parser::{ical::component::IcalTimeZone, Component},
|
||||
property::Property,
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct EventObject {
|
||||
pub(crate) event: IcalEvent,
|
||||
pub(crate) timezones: HashMap<String, IcalTimeZone>,
|
||||
}
|
||||
|
||||
impl EventObject {
|
||||
pub fn get_first_occurence(&self) -> Result<Option<CalDateTime>, Error> {
|
||||
// This is safe since we enforce the event's existance in the constructor
|
||||
if let Some(dtstart) = self.event.get_property("DTSTART") {
|
||||
CalDateTime::parse_prop(dtstart, &self.timezones)
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_last_occurence(&self) -> Result<Option<CalDateTime>, Error> {
|
||||
// This is safe since we enforce the event's existence in the constructor
|
||||
if let Some(_rrule) = self.event.get_property("RRULE") {
|
||||
// TODO: understand recurrence rules
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
if let Some(dtend) = self.event.get_property("DTEND") {
|
||||
return CalDateTime::parse_prop(dtend, &self.timezones);
|
||||
};
|
||||
|
||||
let duration = if let Some(Property {
|
||||
value: Some(duration),
|
||||
..
|
||||
}) = self.event.get_property("DURATION")
|
||||
{
|
||||
parse_duration(duration)?
|
||||
} else {
|
||||
Duration::days(1)
|
||||
};
|
||||
|
||||
let first_occurence = self.get_first_occurence()?;
|
||||
Ok(first_occurence.map(|first_occurence| first_occurence + duration))
|
||||
}
|
||||
}
|
||||
6
crates/store/src/calendar/journal.rs
Normal file
6
crates/store/src/calendar/journal.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
use ical::parser::ical::component::IcalJournal;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct JournalObject {
|
||||
pub(crate) journal: IcalJournal,
|
||||
}
|
||||
11
crates/store/src/calendar/mod.rs
Normal file
11
crates/store/src/calendar/mod.rs
Normal file
@@ -0,0 +1,11 @@
|
||||
mod calendar;
|
||||
mod event;
|
||||
mod journal;
|
||||
mod object;
|
||||
mod todo;
|
||||
|
||||
pub use calendar::*;
|
||||
pub use event::*;
|
||||
pub use journal::*;
|
||||
pub use object::*;
|
||||
pub use todo::*;
|
||||
167
crates/store/src/calendar/object.rs
Normal file
167
crates/store/src/calendar/object.rs
Normal file
@@ -0,0 +1,167 @@
|
||||
use super::{event::EventObject, journal::JournalObject, todo::TodoObject};
|
||||
use crate::{timestamp::CalDateTime, Error};
|
||||
use anyhow::Result;
|
||||
use ical::parser::{ical::component::IcalTimeZone, Component};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::{collections::HashMap, io::BufReader};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
// specified in https://datatracker.ietf.org/doc/html/rfc5545#section-3.6
|
||||
pub enum CalendarObjectType {
|
||||
Event,
|
||||
Journal,
|
||||
Todo,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum CalendarObjectComponent {
|
||||
Event(EventObject),
|
||||
Todo(TodoObject),
|
||||
Journal(JournalObject),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CalendarObject {
|
||||
id: String,
|
||||
ics: String,
|
||||
data: CalendarObjectComponent,
|
||||
}
|
||||
|
||||
// Custom implementation for CalendarObject (de)serialization
|
||||
impl<'de> Deserialize<'de> for CalendarObject {
|
||||
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
#[derive(Deserialize)]
|
||||
struct Inner {
|
||||
id: String,
|
||||
ics: String,
|
||||
}
|
||||
let Inner { id, ics } = Inner::deserialize(deserializer)?;
|
||||
Self::from_ics(id, ics).map_err(serde::de::Error::custom)
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for CalendarObject {
|
||||
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
#[derive(Serialize)]
|
||||
struct Inner {
|
||||
id: String,
|
||||
ics: String,
|
||||
}
|
||||
Inner::serialize(
|
||||
&Inner {
|
||||
id: self.get_id().to_string(),
|
||||
ics: self.get_ics().to_string(),
|
||||
},
|
||||
serializer,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl CalendarObject {
|
||||
pub fn from_ics(object_id: String, ics: String) -> Result<Self, Error> {
|
||||
let mut parser = ical::IcalParser::new(BufReader::new(ics.as_bytes()));
|
||||
let cal = parser.next().ok_or(Error::NotFound)??;
|
||||
if parser.next().is_some() {
|
||||
return Err(Error::InvalidData(
|
||||
"multiple calendars, only one allowed".to_owned(),
|
||||
));
|
||||
}
|
||||
if cal.events.len()
|
||||
+ cal.alarms.len()
|
||||
+ cal.todos.len()
|
||||
+ cal.journals.len()
|
||||
+ cal.free_busys.len()
|
||||
!= 1
|
||||
{
|
||||
// https://datatracker.ietf.org/doc/html/rfc4791#section-4.1
|
||||
return Err(Error::InvalidData(
|
||||
"iCalendar object is only allowed to have exactly one component".to_owned(),
|
||||
));
|
||||
}
|
||||
|
||||
let timezones: HashMap<String, IcalTimeZone> = cal
|
||||
.timezones
|
||||
.clone()
|
||||
.into_iter()
|
||||
.filter_map(|timezone| {
|
||||
let timezone_prop = timezone.get_property("TZID")?.to_owned();
|
||||
let tzid = timezone_prop.value?;
|
||||
Some((tzid, timezone))
|
||||
})
|
||||
.collect();
|
||||
|
||||
if let Some(event) = cal.events.first() {
|
||||
return Ok(CalendarObject {
|
||||
id: object_id,
|
||||
ics,
|
||||
data: CalendarObjectComponent::Event(EventObject {
|
||||
event: event.clone(),
|
||||
timezones,
|
||||
}),
|
||||
});
|
||||
}
|
||||
if let Some(todo) = cal.todos.first() {
|
||||
return Ok(CalendarObject {
|
||||
id: object_id,
|
||||
ics,
|
||||
data: CalendarObjectComponent::Todo(TodoObject { todo: todo.clone() }),
|
||||
});
|
||||
}
|
||||
if let Some(journal) = cal.journals.first() {
|
||||
return Ok(CalendarObject {
|
||||
id: object_id,
|
||||
ics,
|
||||
data: CalendarObjectComponent::Journal(JournalObject {
|
||||
journal: journal.clone(),
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
Err(Error::InvalidData(
|
||||
"iCalendar component type not supported :(".to_owned(),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn get_id(&self) -> &str {
|
||||
&self.id
|
||||
}
|
||||
pub fn get_etag(&self) -> String {
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(&self.id);
|
||||
hasher.update(self.get_ics());
|
||||
format!("{:x}", hasher.finalize())
|
||||
}
|
||||
|
||||
pub fn get_ics(&self) -> &str {
|
||||
&self.ics
|
||||
}
|
||||
|
||||
pub fn get_component_name(&self) -> &str {
|
||||
match self.data {
|
||||
CalendarObjectComponent::Todo(_) => "VTODO",
|
||||
CalendarObjectComponent::Event(_) => "VEVENT",
|
||||
CalendarObjectComponent::Journal(_) => "VJOURNAL",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_first_occurence(&self) -> Result<Option<CalDateTime>, Error> {
|
||||
match &self.data {
|
||||
CalendarObjectComponent::Event(event) => event.get_first_occurence(),
|
||||
_ => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_last_occurence(&self) -> Result<Option<CalDateTime>, Error> {
|
||||
match &self.data {
|
||||
CalendarObjectComponent::Event(event) => event.get_last_occurence(),
|
||||
_ => Ok(None),
|
||||
}
|
||||
}
|
||||
}
|
||||
6
crates/store/src/calendar/todo.rs
Normal file
6
crates/store/src/calendar/todo.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
use ical::parser::ical::component::IcalTodo;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TodoObject {
|
||||
pub(crate) todo: IcalTodo,
|
||||
}
|
||||
Reference in New Issue
Block a user