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 pub responsible: Option<CalendarEntryPerson>,
17 pub target_audience: Vec<CalendarEntryPerson>,
18 pub title: String,
19 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 pub study_group: Option<StudyGroup>,
27 pub category: Option<CalendarEntryCategory>,
28 pub new: bool,
30 pub public: bool,
32 pub private: bool,
34 pub secret: bool,
36 pub all_day: bool,
37}
38
39#[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 pub color: String,
108}
109
110pub 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, };
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 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 pub available_years: Vec<i32>,
474}
475
476#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
477pub enum CalendarExportFileType {
478 PDF(CalendarExportFileTypePDF),
479 CSV(i32),
482 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 YearSimple(i32),
496 YearDetailed(i32),
499 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 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 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}