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