use chrono::{DateTime, NaiveDate, NaiveDateTime, Utc};
use serde::{Deserialize, Serialize};
use std::fmt;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Recurrence {
pub rrule: String,
pub exdates: Vec<EventTime>,
}
impl PartialEq for Recurrence {
fn eq(&self, other: &Self) -> bool {
if self.rrule != other.rrule {
return false;
}
let mut a = self.exdates.clone();
let mut b = other.exdates.clone();
a.sort_by_key(|x| x.to_utc());
b.sort_by_key(|x| x.to_utc());
a == b
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Event {
pub uid: String,
pub summary: String,
pub description: Option<String>,
pub location: Option<String>,
pub start: EventTime,
pub end: EventTime,
pub status: EventStatus,
pub recurrence: Option<Recurrence>,
pub recurrence_id: Option<EventTime>,
pub reminders: Reminders,
pub transparency: Transparency,
pub organizer: Option<Attendee>,
pub attendees: Vec<Attendee>,
pub conference_url: Option<String>,
pub updated: Option<DateTime<Utc>>,
pub sequence: Option<i64>,
pub custom_properties: Vec<(String, String)>,
}
impl PartialEq for Event {
fn eq(&self, other: &Self) -> bool {
self.uid == other.uid
&& self.summary == other.summary
&& self.description == other.description
&& self.location == other.location
&& self.start == other.start
&& self.end == other.end
&& self.status == other.status
&& self.recurrence == other.recurrence
&& self.recurrence_id == other.recurrence_id
&& self.reminders == other.reminders
&& self.transparency == other.transparency
&& self.organizer == other.organizer
&& self.attendees == other.attendees
&& self.conference_url == other.conference_url
}
}
impl fmt::Display for Event {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.summary.is_empty() {
write!(f, "(Unknown Event)")
} else {
write!(f, "{}", self.summary)
}
}
}
impl Event {
pub fn new(
summary: String,
start: EventTime,
end: EventTime,
description: Option<String>,
location: Option<String>,
recurrence: Option<Recurrence>,
reminders: Vec<Reminder>,
) -> Self {
let uid = format!("{}@caldir", uuid::Uuid::new_v4());
Event {
uid,
summary,
description,
location,
start,
end,
status: EventStatus::Confirmed,
recurrence,
recurrence_id: None,
reminders: Reminders(reminders),
transparency: Transparency::Opaque,
organizer: None,
attendees: Vec::new(),
conference_url: None,
updated: None,
sequence: None,
custom_properties: Vec::new(),
}
}
pub fn render_event_time(&self) -> String {
let recurring = if self.recurrence.is_some() {
" 🔁"
} else {
""
};
format!("{}{}", self.start, recurring)
}
pub fn unique_id(&self) -> String {
match &self.recurrence_id {
Some(rid) => format!("{}__{}", self.uid, rid.to_ics_string()),
None => self.uid.clone(),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Attendee {
pub name: Option<String>,
pub email: String,
pub response_status: Option<ParticipationStatus>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING-KEBAB-CASE")]
pub enum ParticipationStatus {
Accepted,
Declined,
Tentative,
NeedsAction,
}
impl ParticipationStatus {
pub fn as_ics_str(&self) -> &'static str {
match self {
Self::Accepted => "ACCEPTED",
Self::Declined => "DECLINED",
Self::Tentative => "TENTATIVE",
Self::NeedsAction => "NEEDS-ACTION",
}
}
pub fn from_ics_str(s: &str) -> Option<Self> {
match s {
"ACCEPTED" => Some(Self::Accepted),
"DECLINED" => Some(Self::Declined),
"TENTATIVE" => Some(Self::Tentative),
"NEEDS-ACTION" => Some(Self::NeedsAction),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Reminder {
pub minutes: i64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Reminders(pub Vec<Reminder>);
impl PartialEq for Reminders {
fn eq(&self, other: &Self) -> bool {
let mut a = self.0.clone();
let mut b = other.0.clone();
a.sort_by_key(|r| r.minutes);
b.sort_by_key(|r| r.minutes);
a == b
}
}
impl std::ops::Deref for Reminders {
type Target = Vec<Reminder>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl std::ops::DerefMut for Reminders {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl FromIterator<Reminder> for Reminders {
fn from_iter<I: IntoIterator<Item = Reminder>>(iter: I) -> Self {
Reminders(iter.into_iter().collect())
}
}
impl<'a> IntoIterator for &'a Reminders {
type Item = &'a Reminder;
type IntoIter = std::slice::Iter<'a, Reminder>;
fn into_iter(self) -> Self::IntoIter {
self.0.iter()
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum Transparency {
Opaque,
Transparent,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum EventTime {
Date(NaiveDate),
DateTimeUtc(DateTime<Utc>),
DateTimeFloating(NaiveDateTime),
DateTimeZoned {
datetime: NaiveDateTime,
tzid: String,
},
}
impl EventTime {
pub fn to_utc(&self) -> Option<DateTime<Utc>> {
match self {
EventTime::Date(d) => d.and_hms_opt(0, 0, 0).map(|dt| dt.and_utc()),
EventTime::DateTimeUtc(dt) => Some(*dt),
EventTime::DateTimeFloating(dt) => Some(dt.and_utc()),
EventTime::DateTimeZoned { datetime, .. } => Some(datetime.and_utc()),
}
}
pub fn is_date(&self) -> bool {
matches!(self, EventTime::Date(_))
}
pub fn to_ics_string(&self) -> String {
match self {
EventTime::Date(d) => d.format("%Y%m%d").to_string(),
EventTime::DateTimeUtc(dt) => dt.format("%Y%m%dT%H%M%SZ").to_string(),
EventTime::DateTimeFloating(dt) => dt.format("%Y%m%dT%H%M%S").to_string(),
EventTime::DateTimeZoned { datetime, .. } => {
datetime.format("%Y%m%dT%H%M%S").to_string()
}
}
}
pub fn to_iso_string(&self) -> String {
match self {
EventTime::Date(d) => d.format("%Y-%m-%d").to_string(),
EventTime::DateTimeUtc(dt) => dt.to_rfc3339(),
EventTime::DateTimeFloating(dt) => format!("{}Z", dt.format("%Y-%m-%dT%H:%M:%S")),
EventTime::DateTimeZoned { datetime, .. } => {
format!("{}Z", datetime.format("%Y-%m-%dT%H:%M:%S"))
}
}
}
}
impl fmt::Display for EventTime {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
EventTime::Date(d) => write!(f, "{}", d.format("%Y-%m-%d")),
EventTime::DateTimeUtc(dt) => write!(f, "{}", dt.format("%Y-%m-%d %H:%M")),
EventTime::DateTimeFloating(dt) => write!(f, "{}", dt.format("%Y-%m-%d %H:%M")),
EventTime::DateTimeZoned { datetime, .. } => {
write!(f, "{}", datetime.format("%Y-%m-%d %H:%M"))
}
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum EventStatus {
Confirmed,
Tentative,
Cancelled,
}