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::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            // start_date is MAX_DATE, this should never happen but FAPP also not raise an error
83            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}