1use std::{
7 collections::HashMap,
8 fs::{self, File},
9 future::Future,
10 io::BufReader,
11 path::Path,
12 pin::Pin,
13 sync::Arc,
14 time::Duration,
15};
16
17use chrono::{NaiveDateTime, Utc};
18
19use ical::{
20 parser::{
21 ical::component::{IcalCalendar, IcalEvent},
22 Component,
23 },
24 property::Property,
25 IcalParser,
26};
27
28use google_calendar3::{
29 api::{Event, EventDateTime},
30 hyper_rustls::{self, HttpsConnector},
31 hyper_util::{self, client::legacy::connect::HttpConnector},
32 yup_oauth2::{self, read_application_secret},
33 CalendarHub,
34};
35
36use once_cell::sync::Lazy;
37use regex::Regex;
38use sanitize_filename::sanitize;
39use tokio::time::sleep;
40
41fn rfc5545_to_std_duration(rfc_duration: &str) -> Duration {
42 let duration_str = rfc_duration.trim_start_matches('P').replace('T', "");
43
44 let mut total_secs = 0u64;
45 let mut number = String::new();
46
47 for c in duration_str.chars() {
48 match c {
49 'W' => {
50 let weeks = number.parse::<u64>().unwrap_or(0);
51 total_secs += weeks * 7 * 24 * 60 * 60;
52 number.clear();
53 }
54 'D' => {
55 let days = number.parse::<u64>().unwrap_or(0);
56 total_secs += days * 24 * 60 * 60;
57 number.clear();
58 }
59 'H' => {
60 let hours = number.parse::<u64>().unwrap_or(0);
61 total_secs += hours * 60 * 60;
62 number.clear();
63 }
64 'M' => {
65 let minutes = number.parse::<u64>().unwrap_or(0);
66 total_secs += minutes * 60;
67 number.clear();
68 }
69 'S' => {
70 let seconds = number.parse::<u64>().unwrap_or(0);
71 total_secs += seconds;
72 number.clear();
73 }
74 digit if digit.is_ascii_digit() => {
75 number.push(digit);
76 }
77 '-' => {}
78 _ => {}
79 }
80 }
81
82 Duration::from_secs(total_secs)
83}
84
85fn changed_properties(event1: &IcalEvent, event2: &IcalEvent) -> Option<Vec<String>> {
86 let props1 = &event1.properties;
87 let props2 = &event2.properties;
88
89 let mut changed_props = Vec::new();
90
91 for prop1 in props1.iter().filter(|p| p.name != "DTSTAMP") {
93 let matching_prop = props2
94 .iter()
95 .filter(|p| p.name != "DTSTAMP")
96 .find(|prop2| prop1.name == prop2.name);
97
98 match matching_prop {
99 Some(prop2) => {
100 if prop1.value != prop2.value
102 || match (&prop1.params, &prop2.params) {
103 (Some(params1), Some(params2)) => params1 != params2,
104 (None, None) => false,
105 _ => true,
106 }
107 {
108 changed_props.push(prop1.name.clone());
109 }
110 }
111 None => {
112 changed_props.push(prop1.name.clone());
114 }
115 }
116 }
117
118 for prop2 in props2.iter().filter(|p| p.name != "DTSTAMP") {
120 if !props1.iter().any(|p| p.name == prop2.name) {
121 changed_props.push(prop2.name.clone());
122 }
123 }
124
125 if changed_props.is_empty() {
126 None
127 } else {
128 Some(changed_props)
129 }
130}
131
132#[derive(Debug, Clone)]
134pub struct EventData {
135 pub uid: String,
136 pub ical_data: IcalEvent,
137}
138
139#[derive(Debug, Clone)]
156pub struct PropertyChange {
157 pub key: String,
158 pub from: Option<Property>,
159 pub to: Option<Property>,
160}
161
162#[derive(Debug, Clone)]
170pub enum CalendarEvent {
171 Setup(EventData),
172 Created(EventData),
173 Updated {
174 event: EventData,
175 changed_properties: Vec<PropertyChange>,
176 },
177 Deleted(EventData),
178}
179
180#[derive(Debug)]
183pub struct CalendarChangeDetector {
184 pub name: Option<String>,
185 pub description: Option<String>,
186 pub ttl: Duration,
187 previous: HashMap<String, IcalEvent>,
188 initialized: bool,
189}
190
191impl CalendarChangeDetector {
192 pub fn new() -> Self {
193 CalendarChangeDetector {
194 name: None,
195 description: None,
196 ttl: rfc5545_to_std_duration("PT1H"),
197 previous: HashMap::new(),
198 initialized: false,
199 }
200 }
201
202 pub fn set_state(&mut self, state: HashMap<String, IcalEvent>) {
203 self.previous = state;
204 self.initialized = true;
205 }
206
207 pub fn compare(&mut self, calendar: IcalCalendar) -> Vec<CalendarEvent> {
208 self.name = calendar
209 .get_property("X-WR-CALNAME")
210 .and_then(|prop| prop.value.clone());
211
212 self.description = calendar
213 .get_property("X-WR-CALDESC")
214 .and_then(|prop| prop.value.clone());
215
216 self.ttl = calendar
217 .get_property("X-PUBLISHED-TTL")
218 .and_then(|prop| prop.value.as_ref())
219 .map(|value| value.as_str())
220 .and_then(|s| Some(rfc5545_to_std_duration(s)))
221 .unwrap_or_else(|| rfc5545_to_std_duration("PT1H"));
222
223 let mut new_previous = HashMap::new();
224 let mut result = Vec::with_capacity(calendar.events.len());
225
226 for event in calendar.events {
227 let event_uid_property = match event
228 .get_property("UID")
229 .and_then(|prop| prop.value.clone())
230 {
231 Some(uid) => uid,
232 None => {
233 println!("Warning: An event is missing a UID, skipping");
234 continue;
235 }
236 };
237 let event_uid = event_uid_property
238 + &event
239 .get_property("RECURRENCE-ID")
240 .map(|prop| match prop.value.clone() {
241 Some(v) => v,
242 None => String::from("R"),
243 })
244 .unwrap_or(String::from(""))
245 + &event
246 .get_property("X-CO-RECURRINGID")
247 .map(|prop| match prop.value.clone() {
248 Some(v) => v,
249 None => String::from("XR"),
250 })
251 .unwrap_or(String::from(""));
252
253 new_previous.insert(event_uid.clone(), event.clone());
254 if self.initialized {
255 if let Some(prev_event) = self.previous.get(&event_uid) {
256 match changed_properties(prev_event, &event) {
257 Some(properties) => {
258 result.push(CalendarEvent::Updated {
259 changed_properties: properties
260 .iter()
261 .map(|property| PropertyChange {
262 key: property.clone(),
263 from: self.previous[&event_uid]
264 .get_property(property)
265 .cloned(),
266 to: new_previous[&event_uid]
267 .get_property(property)
268 .cloned(),
269 })
270 .collect(),
271 event: EventData {
272 uid: event_uid,
273 ical_data: event,
274 },
275 });
276 }
277 None => (),
278 }
279 } else {
280 result.push(CalendarEvent::Created(EventData {
281 uid: event_uid,
282 ical_data: event,
283 }));
284 }
285 } else {
286 result.push(CalendarEvent::Setup(EventData {
287 uid: event_uid,
288 ical_data: event,
289 }));
290 }
291 }
292
293 for (uid, ical_data) in self.previous.drain() {
294 if !new_previous.contains_key(&uid) {
295 result.push(CalendarEvent::Deleted(EventData { uid, ical_data }));
296 }
297 }
298
299 self.previous = new_previous;
300 self.initialized = true;
301
302 result
303 }
304}
305
306pub type CalendarCallback = Box<
307 dyn Fn(
308 Option<String>,
309 Option<String>,
310 Vec<CalendarEvent>,
311 ) -> Pin<
312 Box<dyn Future<Output = Result<(), Box<dyn std::error::Error + Send + Sync>>> + Send>,
313 >,
314>;
315pub struct ICSWatcher<'a> {
339 ics_link: &'a str,
340 pub callbacks: Vec<CalendarCallback>,
341 change_detector: CalendarChangeDetector,
342}
343
344impl<'a> ICSWatcher<'a> {
345 pub fn new(ics_link: &'a str, callbacks: Vec<CalendarCallback>) -> Self {
346 ICSWatcher {
347 ics_link,
348 callbacks,
349 change_detector: CalendarChangeDetector::new(),
350 }
351 }
352
353 pub fn restore_state(&mut self, state: HashMap<String, IcalEvent>) {
354 self.change_detector.set_state(state);
355 }
356
357 pub fn get_state(&self) -> &HashMap<String, IcalEvent> {
358 &self.change_detector.previous
359 }
360
361 pub fn get_calendar_name(&self) -> Option<String> {
362 self.change_detector.name.clone()
363 }
364
365 pub fn create_backup(&self, name: &str) {
366 let backup_file_path = Path::new(".backups").join(sanitize(name) + ".cbor");
367
368 fs::create_dir_all(".backups").expect("Failed to create .backups folder");
369 let backup_file = File::create(backup_file_path).expect("Failed to create backup file");
370 ciborium::ser::into_writer(self.get_state(), backup_file)
371 .expect("Failed to create and write backup");
372 }
373
374 pub fn load_backup(&mut self, name: &str) -> Result<(), Box<dyn std::error::Error>> {
375 let backup_file_path = File::open(Path::new(".backups").join(sanitize(name) + ".cbor"))?;
376
377 let state = ciborium::de::from_reader(backup_file_path)?;
378 self.restore_state(state);
379
380 Ok(())
381 }
382
383 pub async fn update(&mut self) -> Result<(), Box<dyn std::error::Error>> {
384 let res = reqwest::get(self.ics_link).await?;
385 if let Err(error) = res.error_for_status_ref() {
387 return Err(error.into());
388 }
389 let res_text = res.text().await?;
390 let buf = BufReader::new(res_text.as_bytes());
391 let calendar = IcalParser::new(buf).next().ok_or("No Calendar present")??;
392
393 let events = self.change_detector.compare(calendar);
394
395 if !events.is_empty() {
396 let futures: Vec<_> = self
397 .callbacks
398 .iter()
399 .map(|callback| {
400 callback(
401 self.change_detector.name.clone(),
402 self.change_detector.description.clone(),
403 events.clone(),
404 )
405 })
406 .collect();
407
408 for future in futures {
409 match future.await {
410 Ok(()) => (),
411 Err(err) => eprintln!("Error in callback: {err:?}"),
412 }
413 }
414 }
415
416 Ok(())
417 }
418
419 pub async fn run(&mut self, backup: Option<&str>) -> Result<(), Box<dyn std::error::Error>> {
420 loop {
421 self.update().await?;
422 if let Some(path) = backup {
423 self.create_backup(path);
424 }
425 println!("Refreshing in {:?}", self.change_detector.ttl);
426 sleep(self.change_detector.ttl).await;
427 }
428 }
429}
430
431pub async fn log_events(
453 name: Option<String>,
454 description: Option<String>,
455 events: Vec<CalendarEvent>,
456) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
457 println!(
458 "Captured changes of {}{}:",
459 name.as_deref().unwrap_or("Unnamed Calendar"),
460 description
461 .as_deref()
462 .and_then(|desc| Some(format!(" ({desc})")))
463 .unwrap_or(String::from(""))
464 );
465 for event in events {
466 match event {
467 CalendarEvent::Setup(EventData { uid, ical_data }) => {
468 println!("Setup {uid}: {ical_data:?}\n")
469 }
470 CalendarEvent::Created(EventData { uid, ical_data }) => {
471 println!("Created {uid}: {ical_data:?}\n")
472 }
473 CalendarEvent::Updated {
474 event,
475 changed_properties,
476 } => {
477 println!("Updated {}: {:?}\n", event.uid, changed_properties)
478 }
479 CalendarEvent::Deleted(EventData { uid, ical_data }) => {
480 println!("Deleted {uid}: {ical_data:?}\n")
481 }
482 }
483 }
484
485 Ok(())
486}
487
488static REPLACEMENTS: Lazy<Arc<Vec<(String, String)>>> = Lazy::new(|| {
489 let courses_json = match fs::read_to_string("replacements.json") {
490 Ok(content) => content,
491 Err(_) => return Arc::new(Vec::new()),
492 };
493
494 let raw_replacements: HashMap<String, String> = match serde_json::from_str(&courses_json) {
495 Ok(parsed) => parsed,
496 Err(_) => return Arc::new(Vec::new()),
497 };
498
499 let mut replacements: Vec<(String, String)> = raw_replacements.into_iter().collect();
500 replacements.sort_by(|(a_key, _), (b_key, _)| {
501 if a_key.len() != b_key.len() {
502 b_key.len().cmp(&a_key.len())
503 } else {
504 a_key.cmp(b_key)
505 }
506 });
507
508 Arc::new(replacements)
509});
510
511static LV_ID_REGEX: Lazy<Regex> =
512 Lazy::new(|| Regex::new(r"\[(([A-Z]{2})(\d{4})(?:,\s*[A-Z]{2}\d{4})*)\]|\((([A-Z]{2})(\d{4})(?:,\s*[A-Z]{2}\d{4})*)\)").unwrap());
513
514fn remove_lv_id(text: &str) -> String {
515 LV_ID_REGEX.replace_all(text, "").to_string()
517}
518
519fn replace_courses(input: &str) -> String {
520 let mut result = input.to_string();
521 for (from, to) in REPLACEMENTS.iter() {
522 result = result.replace(from, to);
523 }
524 remove_lv_id(result.as_str())
525}
526
527fn convert_to_non_digits(str: String) -> String {
528 str.chars()
529 .map(|c| match c {
530 '0' => '𝟎',
531 '1' => '𝟏',
532 '2' => '𝟐',
533 '3' => '𝟑',
534 '4' => '𝟒',
535 '5' => '𝟓',
536 '6' => '𝟔',
537 '7' => '𝟕',
538 '8' => '𝟖',
539 '9' => '𝟗',
540 other => other,
541 })
542 .collect::<String>()
543}
544
545async fn create_event(
547 hub: &CalendarHub<HttpsConnector<HttpConnector>>,
548 uid: String,
549 event: IcalEvent,
550 calendar_id: &str,
551) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
552 let mut google_event = Event::default();
553
554 let start = NaiveDateTime::parse_from_str(
555 &event
556 .get_property("DTSTART")
557 .ok_or("Required property DTSTART missing")?
558 .value
559 .clone()
560 .ok_or("Required property value DTSTART missing")?[0..15],
561 "%Y%m%dT%H%M%S",
562 )?
563 .and_utc();
564 let end = NaiveDateTime::parse_from_str(
565 &event
566 .get_property("DTEND")
567 .ok_or("Required property DTEND missing")?
568 .value
569 .clone()
570 .ok_or("Required property value DTEND missing")?[0..15],
571 "%Y%m%dT%H%M%S",
572 )?
573 .and_utc();
574
575 google_event.start = Some(EventDateTime {
576 date_time: Some(start),
577 date: None,
578 time_zone: None,
579 });
580 google_event.end = Some(EventDateTime {
581 date_time: Some(end),
582 date: None,
583 time_zone: None,
584 });
585
586 let room = event
587 .get_property("LOCATION")
588 .and_then(|loc| loc.value.clone())
589 .map(|s| s.replace(r"\", ""))
590 .unwrap_or_else(|| "Kein Ort angegeben".to_string());
591
592 let i_cal_uid = convert_to_non_digits(uid.replace("@tum.de", "|").to_string());
593
594 if let Some(url) = event.get_property("URL").and_then(|url| url.value.clone()) {
596 google_event.source = Some(google_calendar3::api::EventSource {
597 title: Some("Link zur Lernveranstaltung".to_string()),
598 url: Some(url),
599 });
600 }
601
602 if let Some(status) = event
603 .get_property("STATUS")
604 .and_then(|status| status.value.clone())
605 {
606 google_event.status = Some(status.to_lowercase());
607 }
608
609 match event
610 .get_property("SUMMARY")
611 .and_then(|summary| summary.value.clone())
612 {
613 Some(summary) => {
614 google_event.summary = Some(replace_courses(summary.replace(r"\", "").as_str()));
615 if summary.contains("Prüfung") {
616 google_event.color_id = Some(String::from("11"));
618 }
619 }
620 None => {
621 google_event.summary = Some("Kein Titel angegeben".to_string());
622 }
623 }
624
625 google_event.location = Some(convert_to_non_digits(room.clone()));
626
627 let link = format!("https://nav.tum.de/search?q={}", room.clone());
628 let description = event
629 .get_property("DESCRIPTION")
630 .and_then(|prop| prop.value.clone())
631 .map(|desc| desc.split(r"\;").skip(2).collect())
632 .unwrap_or(String::new())
633 .as_str()
634 .replace(r"\n", "<br>")
635 .replace(r"\", "")
636 .trim()
637 .to_string();
638
639 let original_description = if !description.is_empty() {
640 format!("{}<br>", description)
641 } else {
642 description
643 };
644
645 let location_link = format!("<a href=\"{}\">Wo ist das?</a><br>", link);
646 let online_only = room.to_lowercase().contains("online");
647 let on_moodle = room.to_lowercase().contains("moodle");
648
649 google_event.description = Some(format!(
650 "{}{}<br><hr><small>uid:{}</small>",
651 original_description,
652 if online_only {
653 if on_moodle {
654 "<a href=\"https://www.moodle.tum.de/my/\">Online auf Moodle</a><br>".into()
655 } else {
656 "Online<br>".into()
657 }
658 } else {
659 location_link
660 },
661 i_cal_uid
662 ));
663
664 let results = hub
665 .events()
666 .list(calendar_id)
667 .q(&format!("uid:{}", i_cal_uid))
668 .doit()
669 .await?;
670
671 if let Some(event_id) = results
672 .1
673 .items
674 .and_then(|items| items.first().cloned())
675 .and_then(|event| event.id)
676 {
677 hub.events()
678 .update(google_event, calendar_id, &event_id)
679 .doit()
680 .await?
681 .0
682 } else {
683 hub.events()
684 .insert(google_event, calendar_id)
685 .doit()
686 .await?
687 .0
688 };
689
690 Ok(())
691}
692
693async fn update_event(
694 hub: &CalendarHub<HttpsConnector<HttpConnector>>,
695 uid: String,
696 event: IcalEvent,
697 property_changes: Vec<PropertyChange>,
698 calendar_id: &str,
699) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
700 println!("Updating event {uid}: {property_changes:?}");
701
702 let i_cal_uid = convert_to_non_digits(uid.replace("@tum.de", "|").to_string());
703 let results = hub
704 .events()
705 .list(calendar_id)
706 .q(&format!("uid:{}", i_cal_uid))
707 .doit()
708 .await?;
709
710 let oringinal_event = results
711 .1
712 .items
713 .and_then(|items| items.first().cloned())
714 .ok_or("Updating not possible as event is not present anymore")?;
715 let event_id = oringinal_event
716 .id
717 .ok_or("Google didn't provide the event with an ID")?;
718 let mut google_event = Event::default();
719
720 if property_changes
721 .iter()
722 .any(|property_change| property_change.key == "DTSTART" || property_change.key == "DTEND")
723 {
724 let start = NaiveDateTime::parse_from_str(
725 &event
726 .get_property("DTSTART")
727 .ok_or("Required property DTSTART missing")?
728 .value
729 .clone()
730 .ok_or("Required property value DTSTART missing")?[0..15],
731 "%Y%m%dT%H%M%S",
732 )?
733 .and_utc();
734 let end = NaiveDateTime::parse_from_str(
735 &event
736 .get_property("DTEND")
737 .ok_or("Required property DTEND missing")?
738 .value
739 .clone()
740 .ok_or("Required property DTEND value missing")?[0..15],
741 "%Y%m%dT%H%M%S",
742 )?
743 .and_utc();
744
745 google_event.start = Some(EventDateTime {
746 date_time: Some(start),
747 date: None,
748 time_zone: None,
749 });
750 google_event.end = Some(EventDateTime {
751 date_time: Some(end),
752 date: None,
753 time_zone: None,
754 });
755 } else {
756 google_event.start = oringinal_event.start;
757 google_event.end = oringinal_event.end;
758 }
759
760 if let Some(url) = event.get_property("URL").and_then(|url| url.value.clone()) {
762 google_event.source = Some(google_calendar3::api::EventSource {
763 title: Some("Link zur Lernveranstaltung".to_string()),
764 url: Some(url),
765 });
766 }
767
768 if property_changes
769 .iter()
770 .any(|property_change| property_change.key == "STATUS")
771 {
772 if let Some(status) = event
773 .get_property("STATUS")
774 .and_then(|status| status.value.clone())
775 {
776 google_event.status = Some(status.to_lowercase());
777 }
778 } else {
779 google_event.status = oringinal_event.status;
780 }
781
782 if property_changes
783 .iter()
784 .any(|property_change| property_change.key == "SUMMARY")
785 {
786 match event
787 .get_property("SUMMARY")
788 .and_then(|summary| summary.value.clone())
789 {
790 Some(summary) => {
791 google_event.summary = Some(replace_courses(summary.replace(r"\", "").as_str()));
792 if summary.contains("Prüfung") {
793 google_event.color_id = Some(String::from("11"));
795 }
796 }
797 None => {
798 google_event.summary = Some("Kein Titel angegeben".to_string());
799 }
800 }
801 } else {
802 if oringinal_event
803 .summary
804 .clone()
805 .is_some_and(|summary| summary.contains("Prüfung"))
806 {
807 google_event.color_id = Some(String::from("11"));
809 }
810 google_event.summary = oringinal_event.summary;
811 }
812
813 let room = event
815 .get_property("LOCATION")
816 .and_then(|loc| loc.value.clone())
817 .map(|s| s.replace(r"\", ""))
818 .unwrap_or_else(|| "Kein Ort angegeben".to_string());
819 if property_changes
820 .iter()
821 .any(|property_change| property_change.key == "LOCATION")
822 {
823 google_event.location = Some(convert_to_non_digits(room.clone()));
824 } else {
825 google_event.location = oringinal_event.location;
826 }
827
828 let link = format!(
829 "https://nav.tum.de/search?q={}",
830 google_event.location.clone().unwrap_or_default()
831 );
832
833 let description = event
834 .get_property("DESCRIPTION")
835 .and_then(|prop| prop.value.clone())
836 .map(|desc| desc.split(r"\;").skip(2).collect())
837 .unwrap_or(String::new())
838 .as_str()
839 .replace(r"\n", "<br>")
840 .replace(r"\", "")
841 .trim()
842 .to_string();
843
844 let original_description = if !description.is_empty() {
845 format!("{}<br>", description)
846 } else {
847 description
848 };
849
850 if property_changes.iter().any(|property_change| {
851 property_change.key == "DESCRIPTION" || property_change.key == "LOCATION"
852 }) {
853 let location_link = format!("<a href=\"{}\">Wo ist das?</a><br>", link);
854 let online_only = google_event
855 .location
856 .clone()
857 .unwrap_or_default()
858 .to_lowercase()
859 .contains("online");
860 let on_moodle = google_event
861 .location
862 .clone()
863 .unwrap_or_default()
864 .to_lowercase()
865 .contains("moodle");
866 google_event.description = Some(format!(
867 "{}{}<br><hr><small>uid:{}</small>",
868 original_description,
869 if online_only {
870 if on_moodle {
871 "<a href=\"https://www.moodle.tum.de/my/\">Online auf Moodle</a><br>".into()
872 } else {
873 "Online<br>".into()
874 }
875 } else {
876 location_link
877 },
878 i_cal_uid
879 ));
880 } else {
881 google_event.description = oringinal_event.description;
882 }
883
884 hub.events()
885 .update(google_event, calendar_id, &event_id)
886 .doit()
887 .await?
888 .0;
889
890 Ok(())
891}
892
893async fn delete_event(
894 hub: &CalendarHub<HttpsConnector<HttpConnector>>,
895 uid: String,
896 calendar_id: &str,
897) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
898 let i_cal_uid = convert_to_non_digits(uid.replace("@tum.de", "|").to_string());
899 let results = hub
900 .events()
901 .list(calendar_id)
902 .q(&format!("uid:{}", i_cal_uid))
903 .doit()
904 .await?;
905
906 if let Some(event_id) = results
907 .1
908 .items
909 .and_then(|items| items.first().cloned())
910 .and_then(|event| event.id)
911 {
912 hub.events().delete(calendar_id, &event_id).doit().await?;
913 }
914
915 Ok(())
916}
917
918pub async fn tum_google_sync(
944 calendar_id: &str,
945 _: Option<String>,
946 _: Option<String>,
947 events: Vec<CalendarEvent>,
948) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
949 let secret: yup_oauth2::ApplicationSecret =
950 read_application_secret(Path::new(".secrets/client_secret.json"))
951 .await
952 .expect("Failed to read client secret");
953
954 let auth = yup_oauth2::InstalledFlowAuthenticator::builder(
955 secret,
956 yup_oauth2::InstalledFlowReturnMethod::HTTPRedirect,
957 )
958 .persist_tokens_to_disk(".secrets/token_cache.json")
959 .build()
960 .await?;
961
962 auth.token(&["https://www.googleapis.com/auth/calendar"])
963 .await
964 .expect("Unable to get scope for calendar");
965
966 let client = hyper_util::client::legacy::Client::builder(hyper_util::rt::TokioExecutor::new())
967 .build(
968 hyper_rustls::HttpsConnectorBuilder::new()
969 .with_native_roots()?
970 .https_or_http()
971 .enable_http1()
972 .build(),
973 );
974 let hub = CalendarHub::new(client, auth);
975
976 for event in events {
977 let calendar_id = calendar_id;
978
979 let result = match event {
980 CalendarEvent::Setup(EventData { uid, ical_data }) => {
981 if ical_data
983 .get_property("DESCRIPTION")
984 .and_then(|prop| prop.value.clone())
985 .map(|desc| desc.contains("Videoübertragung aus"))
986 .unwrap_or(false)
987 {
988 Err(format!(
989 "Skipping video transmission event {:?}",
990 ical_data.get_property("SUMMARY"),
991 )
992 .into())
993 } else {
994 println!("Setting up event {uid}");
995 create_event(&hub, uid, ical_data, calendar_id).await
996 }
997 }
998 CalendarEvent::Created(EventData { uid, ical_data }) => {
999 if ical_data
1001 .get_property("DESCRIPTION")
1002 .and_then(|prop| prop.value.clone())
1003 .map(|desc| desc.contains("Videoübertragung aus"))
1004 .unwrap_or(false)
1005 {
1006 Ok(())
1008 } else {
1009 println!("Creating event {uid}");
1010 create_event(&hub, uid, ical_data, calendar_id).await
1011 }
1012 }
1013 CalendarEvent::Updated {
1014 event: EventData { uid, ical_data },
1015 changed_properties,
1016 } => {
1017 if changed_properties.len() == 1
1020 && changed_properties[0].key == "DESCRIPTION"
1021 && changed_properties[0]
1022 .from
1023 .as_ref()
1024 .and_then(|from| from.value.as_ref())
1025 .zip(
1026 changed_properties[0]
1027 .to
1028 .as_ref()
1029 .and_then(|to| to.value.as_ref()),
1030 )
1031 .map_or(false, |(from, to)| {
1032 from.split(";").skip(2).collect::<String>()
1033 == to.split(";").skip(2).collect::<String>()
1034 })
1035 {
1036 Ok(())
1038 } else {
1039 update_event(&hub, uid, ical_data, changed_properties, calendar_id).await
1040 }
1041 }
1042 CalendarEvent::Deleted(EventData { uid, ical_data }) => {
1043 println!("Deleting event {uid}");
1044 let end_date = ical_data
1047 .get_property("DTEND")
1048 .and_then(|prop| prop.value.clone())
1049 .and_then(|value| {
1050 if value.len() >= 15 {
1051 NaiveDateTime::parse_from_str(&value[0..15], "%Y%m%dT%H%M%S")
1052 .map(|dt| dt.and_utc())
1053 .ok()
1054 } else {
1055 None
1056 }
1057 });
1058
1059 match end_date {
1060 Some(end) if end < Utc::now() - Duration::from_secs(60 * 24 * 7) => {
1061 Ok(())
1063 }
1064 _ => delete_event(&hub, uid, calendar_id).await,
1065 }
1066 }
1067 };
1068
1069 match result {
1070 Ok(_) => (),
1071 Err(error) => eprintln!("Error on syncing event: {error:?}"),
1072 }
1073 }
1074
1075 Ok(())
1076}
1077
1078#[cfg(test)]
1079mod tests {
1080 use super::*;
1081
1082 #[test]
1083 fn cmp_no_properties() {
1084 let event1 = IcalEvent {
1085 properties: vec![],
1086 alarms: vec![],
1087 };
1088
1089 let event2 = IcalEvent {
1090 properties: vec![],
1091 alarms: vec![],
1092 };
1093
1094 let keys = changed_properties(&event1, &event2);
1095
1096 assert_eq!(keys, None);
1097 }
1098
1099 #[test]
1100 fn cmp_same_properties() {
1101 let prop1 = Property {
1102 name: String::from("prop1"),
1103 value: Some(String::from("prop1 value")),
1104 params: None,
1105 };
1106
1107 let prop2 = Property {
1108 name: String::from("prop2"),
1109 value: Some(String::from("prop2 value")),
1110 params: None,
1111 };
1112
1113 let event1 = IcalEvent {
1114 properties: vec![prop1.clone(), prop2.clone()],
1115 alarms: vec![],
1116 };
1117
1118 let event2 = IcalEvent {
1119 properties: vec![prop2.clone(), prop1.clone()],
1120 alarms: vec![],
1121 };
1122
1123 let keys = changed_properties(&event1, &event2);
1124
1125 assert_eq!(keys, None);
1126 }
1127
1128 #[test]
1129 fn cmp_different_properties() {
1130 let prop1 = Property {
1131 name: String::from("prop1"),
1132 value: Some(String::from("prop1 value")),
1133 params: None,
1134 };
1135
1136 let prop2 = Property {
1137 name: String::from("prop2"),
1138 value: Some(String::from("prop2 value")),
1139 params: None,
1140 };
1141
1142 let event1 = IcalEvent {
1143 properties: vec![prop1],
1144 alarms: vec![],
1145 };
1146
1147 let event2 = IcalEvent {
1148 properties: vec![prop2],
1149 alarms: vec![],
1150 };
1151
1152 let keys = changed_properties(&event1, &event2).expect("Keys should be Some");
1153
1154 assert_eq!(keys.len(), 2);
1155 assert!(keys.contains(&String::from("prop1")) && keys.contains(&String::from("prop2")));
1156 }
1157
1158 #[test]
1159 fn cmp_added_property() {
1160 let prop1 = Property {
1161 name: String::from("prop1"),
1162 value: Some(String::from("prop1 value")),
1163 params: None,
1164 };
1165
1166 let event1 = IcalEvent {
1167 properties: vec![],
1168 alarms: vec![],
1169 };
1170
1171 let event2 = IcalEvent {
1172 properties: vec![prop1],
1173 alarms: vec![],
1174 };
1175
1176 let keys = changed_properties(&event1, &event2).expect("Keys should be Some");
1177
1178 assert_eq!(keys.len(), 1);
1179 assert!(keys.contains(&String::from("prop1")));
1180 }
1181
1182 #[test]
1183 fn cmp_removed_property() {
1184 let prop1 = Property {
1185 name: String::from("prop1"),
1186 value: Some(String::from("prop1 value")),
1187 params: None,
1188 };
1189
1190 let event1 = IcalEvent {
1191 properties: vec![prop1],
1192 alarms: vec![],
1193 };
1194
1195 let event2 = IcalEvent {
1196 properties: vec![],
1197 alarms: vec![],
1198 };
1199
1200 let keys = changed_properties(&event1, &event2).expect("Keys should be Some");
1201
1202 assert_eq!(keys.len(), 1);
1203 assert!(keys.contains(&String::from("prop1")));
1204 }
1205
1206 #[test]
1207 fn cmp_different_properties_no_value() {
1208 let prop1 = Property {
1209 name: String::from("prop1"),
1210 value: None,
1211 params: None,
1212 };
1213
1214 let prop2 = Property {
1215 name: String::from("prop2"),
1216 value: None,
1217 params: None,
1218 };
1219
1220 let event1 = IcalEvent {
1221 properties: vec![prop1],
1222 alarms: vec![],
1223 };
1224
1225 let event2 = IcalEvent {
1226 properties: vec![prop2],
1227 alarms: vec![],
1228 };
1229
1230 let keys = changed_properties(&event1, &event2).expect("Keys should be Some");
1231
1232 assert_eq!(keys.len(), 2);
1233 assert!(keys.contains(&String::from("prop1")) && keys.contains(&String::from("prop2")));
1234 }
1235
1236 #[test]
1237 fn cmp_different_params() {
1238 let prop1 = Property {
1239 name: String::from("prop1"),
1240 value: None,
1241 params: Some(vec![(String::from("key"), vec![String::from("value")])]),
1242 };
1243
1244 let prop2 = Property {
1245 name: String::from("prop2"),
1246 value: None,
1247 params: Some(vec![(String::from("key"), vec![String::from("value")])]),
1248 };
1249
1250 let event1 = IcalEvent {
1251 properties: vec![prop1],
1252 alarms: vec![],
1253 };
1254
1255 let event2 = IcalEvent {
1256 properties: vec![prop2],
1257 alarms: vec![],
1258 };
1259
1260 let keys = changed_properties(&event1, &event2).expect("Keys should be Some");
1261
1262 assert_eq!(keys.len(), 2);
1263 assert!(keys.contains(&String::from("prop1")) && keys.contains(&String::from("prop2")));
1264 }
1265
1266 #[test]
1267 fn cmp_same_params() {
1268 let prop1 = Property {
1269 name: String::from("prop1"),
1270 value: None,
1271 params: Some(vec![(String::from("key"), vec![String::from("value")])]),
1272 };
1273
1274 let prop2 = Property {
1275 name: String::from("prop1"),
1276 value: None,
1277 params: Some(vec![(String::from("key"), vec![String::from("value")])]),
1278 };
1279
1280 let event1 = IcalEvent {
1281 properties: vec![prop1],
1282 alarms: vec![],
1283 };
1284
1285 let event2 = IcalEvent {
1286 properties: vec![prop2],
1287 alarms: vec![],
1288 };
1289
1290 let keys = changed_properties(&event1, &event2);
1291
1292 assert_eq!(keys, None);
1293 }
1294
1295 #[test]
1296 fn cmp_different_param_keys() {
1297 let prop1 = Property {
1298 name: String::from("prop1"),
1299 value: None,
1300 params: Some(vec![(String::from("key"), vec![String::from("value")])]),
1301 };
1302
1303 let prop2 = Property {
1304 name: String::from("prop1"),
1305 value: None,
1306 params: Some(vec![(String::from("key2"), vec![String::from("value")])]),
1307 };
1308
1309 let event1 = IcalEvent {
1310 properties: vec![prop1],
1311 alarms: vec![],
1312 };
1313
1314 let event2 = IcalEvent {
1315 properties: vec![prop2],
1316 alarms: vec![],
1317 };
1318
1319 let keys = changed_properties(&event1, &event2).expect("Keys should be Some");
1320
1321 assert_eq!(keys.len(), 1);
1322 assert!(keys.contains(&String::from("prop1")));
1323 }
1324
1325 #[test]
1326 fn cmp_different_param_values() {
1327 let prop1 = Property {
1328 name: String::from("prop1"),
1329 value: None,
1330 params: Some(vec![(String::from("key"), vec![String::from("value")])]),
1331 };
1332
1333 let prop2 = Property {
1334 name: String::from("prop1"),
1335 value: None,
1336 params: Some(vec![(String::from("key"), vec![String::from("value2")])]),
1337 };
1338
1339 let event1 = IcalEvent {
1340 properties: vec![prop1],
1341 alarms: vec![],
1342 };
1343
1344 let event2 = IcalEvent {
1345 properties: vec![prop2],
1346 alarms: vec![],
1347 };
1348
1349 let keys = changed_properties(&event1, &event2).expect("Keys should be Some");
1350
1351 assert_eq!(keys.len(), 1);
1352 assert!(keys.contains(&String::from("prop1")));
1353 }
1354
1355 #[test]
1356 fn cmp_added_param() {
1357 let prop1 = Property {
1358 name: String::from("prop1"),
1359 value: None,
1360 params: None,
1361 };
1362
1363 let prop2 = Property {
1364 name: String::from("prop1"),
1365 value: None,
1366 params: Some(vec![(String::from("key"), vec![String::from("value2")])]),
1367 };
1368
1369 let event1 = IcalEvent {
1370 properties: vec![prop1],
1371 alarms: vec![],
1372 };
1373
1374 let event2 = IcalEvent {
1375 properties: vec![prop2],
1376 alarms: vec![],
1377 };
1378
1379 let keys = changed_properties(&event1, &event2).expect("Keys should be Some");
1380
1381 assert_eq!(keys.len(), 1);
1382 assert!(keys.contains(&String::from("prop1")));
1383 }
1384
1385 #[test]
1386 fn cmp_removed_param() {
1387 let prop1 = Property {
1388 name: String::from("prop1"),
1389 value: None,
1390 params: Some(vec![(String::from("key"), vec![String::from("value2")])]),
1391 };
1392
1393 let prop2 = Property {
1394 name: String::from("prop1"),
1395 value: None,
1396 params: None,
1397 };
1398
1399 let event1 = IcalEvent {
1400 properties: vec![prop1],
1401 alarms: vec![],
1402 };
1403
1404 let event2 = IcalEvent {
1405 properties: vec![prop2],
1406 alarms: vec![],
1407 };
1408
1409 let keys = changed_properties(&event1, &event2).expect("Keys should be Some");
1410
1411 assert_eq!(keys.len(), 1);
1412 assert!(keys.contains(&String::from("prop1")));
1413 }
1414}