1use crate::{CalendarObject, Error};
2use caldata::{
3 VcardParser,
4 component::{
5 CalendarInnerDataBuilder, ComponentMut, IcalAlarmBuilder, IcalCalendarObjectBuilder,
6 IcalEventBuilder, VcardContact,
7 },
8 generator::Emitter,
9 parser::{ContentLine, ParserOptions},
10 property::{
11 Calscale, IcalCALSCALEProperty, IcalDTENDProperty, IcalDTSTAMPProperty,
12 IcalDTSTARTProperty, IcalPRODIDProperty, IcalRRULEProperty, IcalSUMMARYProperty,
13 IcalUIDProperty, IcalVERSIONProperty, IcalVersion, VcardANNIVERSARYProperty,
14 VcardBDAYProperty, VcardFNProperty,
15 },
16 types::{CalDate, PartialDate, Tz},
17};
18use chrono::{NaiveDate, Utc};
19use hex::ToHex;
20use sha2::{Digest, Sha256};
21use std::collections::BTreeMap;
22use std::str::FromStr;
23
24#[derive(Debug, Clone)]
25pub struct AddressObject {
26 vcf: String,
27 vcard: VcardContact,
28}
29
30impl From<VcardContact> for AddressObject {
31 fn from(vcard: VcardContact) -> Self {
32 let vcf = vcard.generate();
33 Self { vcf, vcard }
34 }
35}
36
37impl AddressObject {
38 pub fn from_vcf(vcf: String) -> Result<Self, Error> {
39 let parser = VcardParser::from_slice(vcf.as_bytes());
40 let vcard = parser.expect_one()?;
41 Ok(Self { vcf, vcard })
42 }
43
44 #[must_use]
45 pub fn get_etag(&self) -> String {
46 let mut hasher = Sha256::new();
47 hasher.update(self.get_vcf());
48 format!(
49 "\"{}\"",
50 hasher.finalize().as_slice().encode_hex::<String>()
51 )
52 }
53
54 #[must_use]
55 pub fn get_vcf(&self) -> &str {
56 &self.vcf
57 }
58
59 fn get_significant_date_object(
60 &self,
61 date: &PartialDate,
62 summary_prefix: &str,
63 suffix: &str,
64 ) -> Result<Option<CalendarObject>, Error> {
65 let Some(uid) = self.vcard.get_uid() else {
66 return Ok(None);
67 };
68 let uid = format!("{uid}{suffix}");
69 let year = date.get_year();
70 let year_suffix = year.map(|year| format!(" {year}")).unwrap_or_default();
71 let Some(month) = date.get_month() else {
72 return Ok(None);
73 };
74 let Some(day) = date.get_day() else {
75 return Ok(None);
76 };
77 let Some(dtstart) = NaiveDate::from_ymd_opt(year.unwrap_or(1900), month, day) else {
78 return Ok(None);
79 };
80 let start_date = CalDate(dtstart, Tz::Local);
81 let Some(end_date) = start_date.succ_opt() else {
82 return Ok(None);
84 };
85 let Some(VcardFNProperty(fullname, _)) = self.vcard.full_name.first() else {
86 return Ok(None);
87 };
88 let summary = format!("{summary_prefix} {fullname}{year_suffix}");
89
90 let event = IcalEventBuilder {
91 properties: vec![
92 IcalDTSTAMPProperty(Utc::now().into(), vec![].into()).into(),
93 IcalDTSTARTProperty(start_date.into(), vec![].into()).into(),
94 IcalDTENDProperty(end_date.into(), vec![].into()).into(),
95 IcalUIDProperty(uid, vec![].into()).into(),
96 IcalRRULEProperty(
97 caldata::rrule::RRule::from_str("FREQ=YEARLY").unwrap(),
98 vec![].into(),
99 )
100 .into(),
101 IcalSUMMARYProperty(summary.clone(), vec![].into()).into(),
102 ContentLine {
103 name: "TRANSP".to_owned(),
104 value: "TRANSPARENT".to_owned(),
105 ..Default::default()
106 },
107 ],
108 alarms: vec![IcalAlarmBuilder {
109 properties: vec![
110 ContentLine {
111 name: "TRIGGER".to_owned(),
112 value: "-PT0M".to_owned(),
113 params: vec![("VALUE".to_owned(), vec!["DURATION".to_owned()])].into(),
114 },
115 ContentLine {
116 name: "ACTION".to_owned(),
117 value: "DISPLAY".to_owned(),
118 ..Default::default()
119 },
120 ContentLine {
121 name: "DESCRIPTION".to_owned(),
122 value: summary,
123 ..Default::default()
124 },
125 ],
126 }],
127 };
128
129 Ok(Some(
130 IcalCalendarObjectBuilder {
131 properties: vec![
132 IcalVERSIONProperty(IcalVersion::Version2_0, vec![].into()).into(),
133 IcalCALSCALEProperty(Calscale::Gregorian, vec![].into()).into(),
134 IcalPRODIDProperty(
135 "-//github.com/lennart-k/rustical birthday calendar//EN".to_owned(),
136 vec![].into(),
137 )
138 .into(),
139 ],
140 inner: Some(CalendarInnerDataBuilder::Event(vec![event])),
141 vtimezones: BTreeMap::default(),
142 }
143 .build(&ParserOptions::default(), None)?
144 .into(),
145 ))
146 }
147
148 pub fn get_anniversary_object(&self) -> Result<Option<CalendarObject>, Error> {
149 let Some(VcardANNIVERSARYProperty(anniversary, _)) = &self.vcard.anniversary else {
150 return Ok(None);
151 };
152 let Some(date) = &anniversary.date else {
153 return Ok(None);
154 };
155
156 self.get_significant_date_object(date, "💍", "-anniversary")
157 }
158
159 pub fn get_birthday_object(&self) -> Result<Option<CalendarObject>, Error> {
160 let Some(VcardBDAYProperty(bday, _)) = &self.vcard.birthday else {
161 return Ok(None);
162 };
163 let Some(date) = &bday.date else {
164 return Ok(None);
165 };
166
167 self.get_significant_date_object(date, "🎂", "-birthday")
168 }
169
170 #[must_use]
171 pub const fn get_vcard(&self) -> &VcardContact {
172 &self.vcard
173 }
174}