Skip to main content

rustical_ical/
address_object.rs

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