lanis_rs/modules/
calendar.rs

1use chrono::{DateTime, NaiveDate, Utc};
2use markup5ever::tendril::fmt::Slice;
3use regex::Regex;
4use reqwest::Client;
5use serde::{Deserialize, Serialize};
6
7use crate::utils::datetime::datetime_string_stupid_to_datetime;
8use crate::{utils::constants::URL, Error};
9
10#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
11pub struct CalendarEntry {
12    pub id: String,
13    pub school_id: Option<i32>,
14    pub external_uid: Option<String>,
15    /// The person / group who is responsible for the entry / event
16    pub responsible: Option<CalendarEntryPerson>,
17    pub target_audience: Vec<CalendarEntryPerson>,
18    pub title: String,
19    /// May be empty
20    pub description: String,
21    pub start: DateTime<Utc>,
22    pub end: DateTime<Utc>,
23    pub last_modified: Option<DateTime<Utc>>,
24    pub place: Option<String>,
25    /// The study group of the entry (Lerngruppe)
26    pub study_group: Option<StudyGroup>,
27    pub category: Option<CalendarEntryCategory>,
28    /// Indicates if an entry is new
29    pub new: bool,
30    /// Indicates if an entry is public
31    pub public: bool,
32    // Indicates if an entry is private
33    pub private: bool,
34    /// Indicates if an entry is secret (probably)
35    pub secret: bool,
36    pub all_day: bool,
37}
38
39/// May also be a group and not a single person
40#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
41pub struct CalendarEntryPerson {
42    pub id: String,
43    pub name: String,
44}
45
46impl CalendarEntry {
47    pub fn new(
48        id: String,
49        school_id: Option<i32>,
50        external_uid: Option<String>,
51        responsible: Option<CalendarEntryPerson>,
52        target_audience: Vec<CalendarEntryPerson>,
53        title: String,
54        description: String,
55        start: DateTime<Utc>,
56        end: DateTime<Utc>,
57        last_modified: Option<DateTime<Utc>>,
58        place: Option<String>,
59        study_group: Option<StudyGroup>,
60        category: Option<CalendarEntryCategory>,
61        new: bool,
62        public: bool,
63        private: bool,
64        secret: bool,
65        all_day: bool,
66    ) -> Self {
67        Self {
68            id,
69            school_id,
70            external_uid,
71            responsible,
72            target_audience,
73            title,
74            description,
75            start,
76            end,
77            last_modified,
78            place,
79            study_group,
80            category,
81            new,
82            public,
83            private,
84            secret,
85            all_day,
86        }
87    }
88}
89
90#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Serialize, Deserialize)]
91pub struct StudyGroup {
92    pub id: i32,
93    pub name: String,
94}
95
96impl StudyGroup {
97    pub fn new(id: i32, name: String) -> Self {
98        Self { id, name }
99    }
100}
101
102#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Serialize, Deserialize)]
103pub struct CalendarEntryCategory {
104    pub id: i32,
105    pub name: String,
106    /// a hexadecimal color (css)
107    pub color: String,
108}
109
110/// Get all calendar entries in an specific time frame <br>
111/// You can also use a optional search query to filter for events (this is server side)
112pub async fn get_entries(
113    from: NaiveDate,
114    to: NaiveDate,
115    search_query: Option<String>,
116    client: &Client,
117) -> Result<Vec<CalendarEntry>, Error> {
118    let categories = match client.get(URL::CALENDAR).send().await {
119        Ok(response) => {
120            let html = match response.text().await {
121                Ok(text) => text,
122                Err(e) => {
123                    return Err(Error::Html(format!(
124                        "failed to parse html of '{}' with error '{}'",
125                        URL::CALENDAR,
126                        e
127                    )))
128                }
129            };
130
131            let json_categories = match html.split("var categories = new Array();").nth(1) {
132                Some(part) => match part.split("var groups = new Array();").next() {
133                    Some(part) => {
134                        let content = part
135                            .trim()
136                            .replace("categories.push(", "")
137                            .replace(");", ",")
138                            .replace("id", "\"id\"")
139                            .replace("name", "\"name\"")
140                            .replace("color", "\"color\"")
141                            .replace("logo", "\"logo\"")
142                            .replace("\'", "\"");
143                        let final_content = match content.rsplit_once(",") {
144                            Some(split) => split.0.trim().to_string(),
145                            None => content, // Happens if no categories exist at all
146                        };
147
148                        format!("[{}]", final_content.trim())
149                    }
150                    None => return Err(Error::Parsing(String::from(
151                        "failed to parse json categories (missing first part of 'var groups...')",
152                    ))),
153                },
154                None => return Err(Error::Parsing(String::from(
155                    "failed to parse json categories (missing second part of 'var categories...')",
156                ))),
157            };
158
159            let categories: Vec<CalendarEntryCategory> =
160                match serde_json::from_str(json_categories.as_str()) {
161                    Ok(result) => result,
162                    Err(e) => {
163                        return Err(Error::Parsing(format!(
164                            "failed to parse json of categories with error '{}'",
165                            e
166                        )));
167                    }
168                };
169
170            categories
171        }
172        Err(e) => {
173            return Err(Error::Network(format!(
174                "failed to get '{}' with error '{}'",
175                URL::CALENDAR,
176                e
177            )))
178        }
179    };
180
181    let f = String::from("getEvents");
182    let s = search_query.unwrap_or_default();
183    let start = format!("{}", from);
184    let end = format!("{}", to);
185
186    let events_json = match client
187        .post(URL::CALENDAR)
188        .form(&[("f", f), ("s", s), ("start", start), ("end", end)])
189        .send()
190        .await
191    {
192        Ok(response) => {
193            // technically its a json but who cares
194            match response.text().await {
195                Ok(text) => text,
196                Err(e) => {
197                    return Err(Error::Html(format!(
198                        "failed to parse html of '{}' with error '{}'",
199                        URL::CALENDAR,
200                        e
201                    )))
202                }
203            }
204        }
205        Err(e) => {
206            return Err(Error::Network(format!(
207                "failed to post '{}' with error '{}'",
208                URL::CALENDAR,
209                e
210            )))
211        }
212    };
213
214    #[derive(Debug, Serialize, Deserialize)]
215    struct JsonEvent {
216        #[serde(rename = "Id")]
217        id: String,
218        #[serde(rename = "Institution")]
219        school_id: Option<String>,
220        #[serde(rename = "FremdUID")]
221        external_uid: Option<String>,
222        #[serde(rename = "Verantwortlich")]
223        responsible_id: Option<String>,
224        title: String,
225        description: String,
226        #[serde(rename = "Anfang")]
227        start: String,
228        #[serde(rename = "Ende")]
229        end: String,
230        #[serde(rename = "LetzteAenderung")]
231        last_modified: Option<String>,
232        #[serde(rename = "Ort")]
233        place: Option<String>,
234        #[serde(rename = "Lerngruppe")]
235        study_group: Option<serde_json::Value>,
236        category: Option<String>,
237        #[serde(rename = "Neu")]
238        new: String,
239        #[serde(rename = "Oeffentlich")]
240        public: String,
241        #[serde(rename = "Privat")]
242        private: String,
243        #[serde(rename = "Geheim")]
244        secret: String,
245        #[serde(rename = "allDay")]
246        all_day: bool,
247    }
248
249    let json_events: Vec<JsonEvent> = match serde_json::from_str(&events_json) {
250        Ok(events) => events,
251        Err(e) => {
252            return Err(Error::Parsing(format!(
253                "failed to parse json of events with error '{}'",
254                e
255            )));
256        }
257    };
258
259    let mut entries = Vec::new();
260    for json_event in json_events {
261        let school_id: Option<i32> = match json_event.school_id {
262            Some(id_string) => match id_string.parse() {
263                Ok(school_id) => Some(school_id),
264                Err(e) => {
265                    return Err(Error::Parsing(format!(
266                        "failed to parse school_id as i32 with error '{}'",
267                        e
268                    )));
269                }
270            },
271            None => None,
272        };
273
274        let start = datetime_string_stupid_to_datetime(&json_event.start)
275            .map_err(|e| {
276                Error::DateTime(format!(
277                    "failed to parse start datetime of entry with error '{}'",
278                    e
279                ))
280            })?
281            .to_utc();
282
283        let end = datetime_string_stupid_to_datetime(&json_event.end)
284            .map_err(|e| {
285                Error::DateTime(format!(
286                    "failed to parse end datetime of entry with error '{}'",
287                    e
288                ))
289            })?
290            .to_utc();
291
292        let last_modified = match json_event.last_modified {
293            Some(datetime_string) => Some(
294                datetime_string_stupid_to_datetime(&datetime_string)
295                    .map_err(|e| {
296                        Error::DateTime(format!(
297                            "failed to parse end datetime of entry with error '{}'",
298                            e
299                        ))
300                    })?
301                    .to_utc(),
302            ),
303            None => None,
304        };
305
306        let study_group = match json_event.study_group {
307            Some(study_group) => match study_group.as_str() {
308                Some(json_object) => {
309                    #[derive(Deserialize)]
310                    struct JsonStudyGroup {
311                        #[serde(rename = "Name")]
312                        name: String,
313                        #[serde(rename = "Id")]
314                        id: String,
315                    }
316
317                    let json_study_group: JsonStudyGroup = serde_json::from_str(&json_object)
318                        .map_err(|e| {
319                            Error::Parsing(format!(
320                                "failed to parse json of study group with error '{}'",
321                                e
322                            ))
323                        })?;
324
325                    let id: i32 = json_study_group.id.parse().map_err(|e| {
326                        Error::Parsing(format!(
327                            "failed to parse study group id ({}) as i32 with error '{}'",
328                            json_study_group.id, e
329                        ))
330                    })?;
331
332                    Some(StudyGroup::new(id, json_study_group.name))
333                }
334                None => None,
335            },
336            None => None,
337        };
338
339        let category = match json_event.category {
340            Some(json_object) => {
341                let id: i32 = json_object.parse().map_err(|e| {
342                    Error::Parsing(format!("failed to parse category id with error '{}'", e))
343                })?;
344                categories.iter().find(|&c| c.id == id).cloned()
345            }
346            None => None,
347        };
348
349        let new = json_event.new != "nein";
350        let public = json_event.public != "nein";
351        let private = json_event.private != "nein";
352        let secret = json_event.secret != "nein";
353
354        let (responsible_name, target_audience) = {
355            #[derive(Deserialize)]
356            struct JsonDetails {
357                properties: JsonDetailsProperties,
358            }
359
360            #[derive(Deserialize)]
361            struct JsonDetailsProperties {
362                #[serde(rename = "zielgruppen")]
363                target_audience: Option<serde_json::Value>,
364                #[serde(rename = "verantwortlich")]
365                responsible_name: Option<String>,
366            }
367
368            let json_details = match client
369                .post(URL::CALENDAR)
370                .form(&[("f", "getEvent"), ("id", json_event.id.as_str())])
371                .send()
372                .await
373            {
374                Ok(response) => response.text().await.map_err(|e| {
375                    Error::Html(format!(
376                        "failed to parse html / json of entry details as text with error '{}'",
377                        e
378                    ))
379                })?,
380                Err(e) => {
381                    return Err(Error::Network(format!(
382                        "failed to post '{}' with error '{}'",
383                        URL::CALENDAR,
384                        e
385                    )))
386                }
387            };
388
389            let details: JsonDetails = serde_json::from_str(&json_details).map_err(|e| {
390                Error::Parsing(format!(
391                    "failed to parse json of entry details ({}) with error '{}'",
392                    json_event.id, e
393                ))
394            })?;
395
396            let raw_target_audience = details.properties.target_audience.unwrap_or_default();
397            let json_target_audience = raw_target_audience.to_string();
398            let target_audience_split = json_target_audience.split(",");
399
400            let mut targets = Vec::new();
401            for target in target_audience_split {
402                let (broken_id, name) = target.split_once(":").unwrap_or_default();
403
404                let id = broken_id
405                    .replace("\"", "")
406                    .replacen("-", "", 1)
407                    .replacen("{", "", 1)
408                    .trim()
409                    .to_string();
410                let name = name.replace("\"", "").replace("}", "").trim().to_string();
411
412                if broken_id.is_empty() || name.is_empty() {
413                    continue;
414                }
415
416                targets.push(CalendarEntryPerson { id, name });
417            }
418
419            (
420                details
421                    .properties
422                    .responsible_name
423                    .unwrap_or_default()
424                    .trim()
425                    .to_string(),
426                targets,
427            )
428        };
429
430        let responsible = match json_event.responsible_id {
431            Some(id) => {
432                if id.is_empty() || responsible_name.is_empty() {
433                    None
434                } else {
435                    Some(CalendarEntryPerson {
436                        id,
437                        name: responsible_name,
438                    })
439                }
440            }
441            None => None,
442        };
443
444        entries.push(CalendarEntry::new(
445            json_event.id,
446            school_id,
447            json_event.external_uid,
448            responsible,
449            target_audience,
450            json_event.title,
451            json_event.description,
452            start,
453            end,
454            last_modified,
455            json_event.place,
456            study_group,
457            category,
458            new,
459            public,
460            private,
461            secret,
462            json_event.all_day,
463        ));
464    }
465
466    Ok(entries)
467}
468
469#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
470pub struct CalendarExports {
471    /// All available years (PDF and CSV) <br>
472    /// These years refer to School years so 2024 is 2024/2025
473    pub available_years: Vec<i32>,
474}
475
476#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
477pub enum CalendarExportFileType {
478    PDF(CalendarExportFileTypePDF),
479    /// The i32 represents the year <br>
480    /// NOTE: Make sure the year is available
481    CSV(i32),
482    /// The i32 represents the year <br>
483    /// NOTE: Make sure the year is available
484    ICS(i32),
485}
486
487#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
488pub enum CalendarExportFileTypePDF {
489    CurrentDay,
490    NextDay,
491    CurrentWeek,
492    NextWeek,
493    /// The i32 represents the year <br>
494    /// NOTE: Make sure the year is available
495    YearSimple(i32),
496    /// The i32 represents the year <br>
497    /// NOTE: Make sure the year is available
498    YearDetailed(i32),
499    /// The i32 represents the year <br>
500    /// NOTE: Make sure the year is available
501    YearMonthView(i32),
502}
503
504impl CalendarExports {
505    pub fn new(available_years: Vec<i32>) -> Self {
506        Self { available_years }
507    }
508
509    pub async fn get(client: &Client) -> Result<Self, Error> {
510        let response = client.get(URL::CALENDAR).send().await.map_err(|e| {
511            Error::Network(format!("failed to get {} with error {}", URL::CALENDAR, e))
512        })?;
513
514        let html = response.text().await.map_err(|e| {
515            Error::Html(format!(
516                "failed to parse HTML of response from '{}' with error '{}'",
517                URL::CALENDAR,
518                e
519            ))
520        })?;
521
522        let regex = Regex::new(r"year=(\d\d\d\d)")
523            .map_err(|e| Error::Parsing(format!("failed to create regex with error '{}'", e)))?;
524        let captures: Vec<_> = regex.captures_iter(&html).collect();
525
526        let mut years: Vec<i32> = Vec::new();
527        for capture_group in captures {
528            if let Some(year) = capture_group.get(1) {
529                if let Ok(year) = year.as_str().parse() {
530                    years.push(year);
531                }
532            }
533        }
534
535        Ok(Self::new(years))
536    }
537
538    /// Get the iCal url (automatic updates)
539    pub async fn get_ical(client: &Client) -> Result<String, Error> {
540        client
541            .post(URL::CALENDAR)
542            .form(&[("f", "iCalAbo")])
543            .send()
544            .await
545            .map_err(|e| {
546                Error::Network(format!(
547                    "failed to post '{}' with error '{}'",
548                    URL::CALENDAR,
549                    e
550                ))
551            })?
552            .text()
553            .await
554            .map_err(|e| {
555                Error::Parsing(format!(
556                    "failed to parse text of response with error '{}'",
557                    e
558                ))
559            })
560    }
561
562    /// Export a file with the specific type
563    pub async fn get_export(
564        &self,
565        client: &Client,
566        export_type: CalendarExportFileType,
567        path: &str,
568    ) -> Result<(), Error> {
569        let url = match export_type {
570            CalendarExportFileType::PDF(pdf_type) => match pdf_type {
571                CalendarExportFileTypePDF::CurrentDay => {
572                    "https://start.schulportal.hessen.de/kalender.php?a=export&export=pdf&day=1"
573                        .to_string()
574                }
575                CalendarExportFileTypePDF::NextDay => {
576                    "https://start.schulportal.hessen.de/kalender.php?a=export&export=pdf&day=2"
577                        .to_string()
578                }
579                CalendarExportFileTypePDF::CurrentWeek => {
580                    "https://start.schulportal.hessen.de/kalender.php?a=export&export=pdf&week=1"
581                        .to_string()
582                }
583                CalendarExportFileTypePDF::NextWeek => {
584                    "https://start.schulportal.hessen.de/kalender.php?a=export&export=pdf&week=2"
585                        .to_string()
586                }
587                CalendarExportFileTypePDF::YearSimple(year) => {
588                    match self.available_years.contains(&year) {
589                        true => format!("https://start.schulportal.hessen.de/kalender.php?a=export&export=pdf&year={}", year),
590                        false => return Err(Error::InvalidInput(format!("year '{}' is not available!", year)))
591                    }
592                }
593                CalendarExportFileTypePDF::YearDetailed(year) => {
594                    match self.available_years.contains(&year) {
595                        true => format!("https://start.schulportal.hessen.de/kalender.php?a=export&export=pdf-extended&year={}", year),
596                        false => return Err(Error::InvalidInput(format!("year '{}' is not available!", year)))
597                    }
598                }
599                CalendarExportFileTypePDF::YearMonthView(year) => {
600                    match self.available_years.contains(&year) {
601                        true => format!("https://start.schulportal.hessen.de/kalender.php?a=export&export=wandkalender&year={}", year),
602                        false => return Err(Error::InvalidInput(format!("year '{}' is not available!", year)))
603                    }
604                }
605            },
606            CalendarExportFileType::CSV(year) => match self.available_years.contains(&year) {
607                true => format!(
608                    "https://start.schulportal.hessen.de/kalender.php?a=export&export=csv&year={}",
609                    year
610                ),
611                false => {
612                    return Err(Error::InvalidInput(format!(
613                        "year '{}' is not available!",
614                        year
615                    )))
616                }
617            },
618            CalendarExportFileType::ICS(year) => match self.available_years.contains(&year) {
619                true => format!(
620                    "https://start.schulportal.hessen.de/kalender.php?a=export&export=ical&year={}",
621                    year
622                ),
623                false => {
624                    return Err(Error::InvalidInput(format!(
625                        "year '{}' is not available!",
626                        year
627                    )))
628                }
629            },
630        };
631
632        let response =
633            client.get(&url).send().await.map_err(|e| {
634                Error::Network(format!("failed to get '{}' with error '{}'", url, e))
635            })?;
636
637        let bytes = response.bytes().await.map_err(|e| {
638            Error::Parsing(format!(
639                "failed to parse response as bytes with error '{}'",
640                e
641            ))
642        })?;
643
644        let mut file = tokio::fs::File::create(path).await.map_err(|e| {
645            Error::FileSystem(format!(
646                "failed to create file at '{}' with error '{}'",
647                path, e
648            ))
649        })?;
650
651        tokio::io::copy(&mut bytes.as_bytes(), &mut file)
652            .await
653            .map_err(|e| Error::FileSystem(format!("failed to save file with error '{}'", e)))?;
654
655        Ok(())
656    }
657}