1use crate::base::account::Account;
2use crate::utils::constants::URL;
3use crate::utils::conversion::string_to_byte_size;
4use crate::utils::crypt::{decrypt_lanis_encoded_tags, encrypt_lanis_data};
5use crate::utils::datetime::date_time_string_to_datetime;
6use crate::{Error, LessonUploadError};
7use chrono::{DateTime, Datelike, Utc};
8use markup5ever::interface::tree_builder::TreeSink;
9use regex::Regex;
10use reqwest::header::HeaderMap;
11use reqwest::multipart::Part;
12use reqwest::Client;
13use scraper::{Element, ElementRef, Html, Selector};
14use serde::{Deserialize, Serialize};
15use std::collections::BTreeMap;
16use std::path::Path;
17use std::time::SystemTime;
18
19#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
20pub struct Lesson {
21 pub id: i32,
22 pub url: String,
23 pub name: String,
24 pub teacher: String,
25 pub teacher_short: Option<String>,
27 pub attendances: BTreeMap<String, String>,
28 pub entry_latest: Option<LessonEntry>,
30 pub entries: Option<Vec<LessonEntry>>,
32 pub marks: Option<Vec<LessonMark>>,
34 pub exams: Option<Vec<LessonExam>>,
36}
37
38#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
39pub struct LessonEntry {
40 pub id: i32,
41 pub date: DateTime<Utc>,
42 pub school_hours: Vec<i32>,
43 pub title: String,
44 pub details: Option<String>,
45 pub homework: Option<Homework>,
46 pub attachments: Option<Vec<Attachment>>,
47 pub attachment_number: i32,
48 pub uploads: Option<Vec<LessonUpload>>,
49}
50
51#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Serialize, Deserialize)]
52pub struct Attachment {
53 pub name: String,
54 pub size: u64,
55 pub url: String,
56}
57
58#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Serialize, Deserialize)]
59pub struct Homework {
60 pub description: String,
61 pub completed: bool,
62}
63
64#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
65pub struct LessonUpload {
66 pub id: i32,
67 pub name: String,
68 pub state: bool,
70 pub url: String,
71 pub uploaded: Option<String>,
72 pub date: Option<DateTime<Utc>>,
73 pub info: Option<LessonUploadInfo>,
74}
75
76#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
77pub struct LessonUploadInfo {
78 pub course_id: Option<i32>,
79 pub entry_id: Option<i32>,
80 pub start: Option<DateTime<Utc>>,
81 pub end: Option<DateTime<Utc>>,
82 pub multiple_files: bool,
84 pub unlimited_tries: bool,
86 pub visibility: Option<String>,
87 pub automatic_deletion: Option<String>,
88 pub allowed_file_types: Vec<String>,
89 pub max_file_size: String,
90 pub extra: Option<String>,
92 pub own_files: Vec<LessonUploadInfoOwnFile>,
93 pub public_files: Vec<LessonUploadInfoPublicFile>,
94}
95
96#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Serialize, Deserialize)]
97pub struct LessonUploadInfoOwnFile {
98 pub name: String,
99 pub url: String,
100 pub index: i32,
101 pub comment: Option<String>,
102}
103#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Serialize, Deserialize)]
104pub struct LessonUploadInfoPublicFile {
105 pub name: String,
106 pub url: String,
107 pub index: i32,
108 pub person: String,
109}
110
111#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Serialize, Deserialize)]
112pub struct LessonUploadFileStatus {
113 pub name: String,
114 pub status: String,
115 pub message: Option<String>,
116}
117
118#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
119pub struct LessonMark {
120 pub name: String,
121 pub date: DateTime<Utc>,
122 pub mark: String,
123 pub comment: Option<String>,
124}
125
126#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Serialize, Deserialize)]
127pub struct LessonExam {
128 pub date: String,
129 pub name: String,
130 pub finished: bool,
131}
132
133impl Lesson {
134 pub async fn set_data(&mut self, account: &Account) -> Result<(), Error> {
139 let client = &account.client;
140
141 match client
142 .get(format!("{}{}", URL::BASE, &self.url))
143 .send()
144 .await
145 {
146 Ok(response) => {
147 if !response.status().is_success() {
148 return Err(Error::Network(format!(
149 "Failed request with status code: {}",
150 response.status()
151 )));
152 }
153
154 let document = decrypt_lanis_encoded_tags(
155 response.text().await.unwrap().as_str(),
156 &account.key_pair.public_key_string,
157 )
158 .await;
159 let document = Html::parse_document(&document);
160
161 let mut history: Vec<LessonEntry> = vec![];
162
163 let history_doc_selector = Selector::parse("#history").unwrap();
164 let history_doc = document.select(&history_doc_selector);
165 let history_doc = history_doc.clone().next().unwrap().html();
166 let mut history_doc = Html::parse_document(&history_doc);
167
168 let history_table_rows_selector = Selector::parse("table>tbody>tr").unwrap();
169
170 let hidden_div_selector = Selector::parse(".hidden_encoded").unwrap();
171 let hidden_div_ids: Vec<_> = history_doc
172 .select(&hidden_div_selector)
173 .map(|x| x.id())
174 .collect();
175
176 for id in hidden_div_ids {
178 history_doc.remove_from_parent(&id);
179 }
180
181 let history_table_rows = history_doc.select(&history_table_rows_selector);
182
183 let title_selector = Selector::parse("td>b").unwrap();
185
186 let details_selector = Selector::parse("span.markup i.fa-comment-alt").unwrap();
187
188 let homework_selector =
189 Selector::parse("span.homework + br + span.markup").unwrap();
190 let homework_done_selector = Selector::parse("span.done.hidden").unwrap();
191
192 let file_alert_selector = Selector::parse("div.alert.alert-info>a").unwrap();
193 let files_selector = Selector::parse(".files").unwrap();
194
195 let upload_group_selector = Selector::parse("div.btn-group").unwrap();
196 let open_upload_selector = Selector::parse(".btn-warning").unwrap();
197 let closed_upload_selector = Selector::parse(".btn-default").unwrap();
198 let upload_url_selector = Selector::parse("ul.dropdown-menu li a").unwrap();
199 let upload_badge_selector = Selector::parse("span.badge").unwrap();
200 let small_selector = Selector::parse("small").unwrap();
201
202 for row in history_table_rows {
203 let id = row.attr("data-entry").unwrap().parse::<i32>().unwrap();
204
205 let title = {
206 row.child_elements()
207 .nth(1)
208 .unwrap()
209 .select(&title_selector)
210 .next()
211 .unwrap()
212 .text()
213 .next()
214 .unwrap()
215 .trim()
216 .to_string()
217 };
218
219 let details = {
220 let details = row.select(&details_selector).next();
221 if details.is_some() {
222 let details = details.unwrap();
223 let details = details
224 .parent_element()
225 .unwrap()
226 .text()
227 .next()
228 .unwrap()
229 .trim()
230 .to_string();
231 Some(details)
232 } else {
233 None
234 }
235 };
236
237 let homework = {
238 let homework_element = row.select(&homework_selector).next();
239 let mut description: String = String::new();
240
241 if homework_element.is_some() {
242 for text in homework_element.unwrap().text() {
243 description += &*format!("{}\n", text.trim()).to_string();
244 }
245 description =
246 description.rsplit_once('\n').unwrap().0.trim().to_string();
247 }
248
249 let completed = {
250 let element = row.select(&homework_done_selector).next();
251 !element.is_some()
252 };
253
254 if description.is_empty() {
255 None
256 } else {
257 Some(Homework {
258 description,
259 completed,
260 })
261 }
262 };
263
264 let attachments: Option<Vec<Attachment>> = {
265 if row
266 .child_elements()
267 .nth(1)
268 .unwrap()
269 .select(&file_alert_selector)
270 .next()
271 .is_some()
272 {
273 let mut attachments = vec![];
274 let url = format!(
275 "{}{}",
276 URL::BASE,
277 row.child_elements()
278 .nth(1)
279 .unwrap()
280 .select(&file_alert_selector)
281 .next()
282 .unwrap()
283 .value()
284 .attr("href")
285 .unwrap()
286 );
287 let url = url.replace("&b=zip", "").to_string();
288
289 for element in
290 row.select(&files_selector).nth(0).unwrap().child_elements()
291 {
292 let name = element.attr("data-file").unwrap().to_string();
293 let size = match element.select(&small_selector).nth(0) {
294 Some(element) => string_to_byte_size(
295 element
296 .text()
297 .collect::<String>()
298 .replace("(", "")
299 .replace(")", "")
300 .trim()
301 .to_string(),
302 )
303 .await
304 .map_err(|e| {
305 Error::Parsing(format!(
306 "failed to parse file size: '{}'",
307 e
308 ))
309 })?,
310 None => 0,
311 };
312 let url = format!("{}&f={}", url, name);
313 attachments.push(Attachment { name, size, url });
314 }
315 Some(attachments)
316 } else {
317 None
318 }
319 };
320
321 let uploads: Option<Vec<LessonUpload>> = {
322 let upload_groups = row
323 .child_elements()
324 .nth(1)
325 .unwrap()
326 .select(&upload_group_selector);
327 let mut uploads: Vec<LessonUpload> = vec![];
328
329 for group in upload_groups {
330 let open = group.select(&open_upload_selector).next();
331 let closed = group.select(&closed_upload_selector).next();
332
333 if open.is_some() {
334 let open = open.unwrap();
335
336 let name = open
337 .children()
338 .nth(2)
339 .unwrap()
340 .value()
341 .as_text()
342 .unwrap()
343 .replace("\n", "")
344 .trim()
345 .to_string();
346 let state = true;
347 let url = format!(
348 "{}{}",
349 URL::BASE,
350 group
351 .select(&upload_url_selector)
352 .next()
353 .unwrap()
354 .value()
355 .attr("href")
356 .unwrap()
357 );
358 let uploaded = {
359 match open.select(&upload_badge_selector).next() {
360 Some(element) => Some(
361 element.text().collect::<String>().trim().to_string(),
362 ),
363 None => None,
364 }
365 };
366 let date = {
367 let text = open
368 .select(&small_selector)
369 .next()
370 .unwrap()
371 .text()
372 .collect::<String>()
373 .trim()
374 .to_string();
375 let text = text.replace("\n", "").trim().to_string();
376 let text = text.replace(" ", "").trim().to_string();
377 let text = text.replace("bis ", "").trim().to_string();
378 let text = text.replace("um", "").trim().to_string();
379 let text = text.replace(",", "").trim().to_string();
380 let text = text.replace(" den", "").trim().to_string();
381 let text = text.replace(" Uhr", "").trim().to_string();
382 let split = text.split(" ");
383 let date = format!(
384 "{}{}",
385 split.clone().nth(1).unwrap_or_default(),
386 chrono::Local::now().year(),
387 );
388 let time = format!("{}:00", split.last().unwrap_or_default());
389 println!("text is: {}", text);
390
391 date_time_string_to_datetime(date.as_str(), time.as_str())
392 .map_err(|e| {
393 Error::DateTime(format!(
394 "failed to convert date to DateTime '{:?}'",
395 e
396 ))
397 })?
398 .to_utc()
399 };
400 let id = url.split("&id=").last().unwrap().parse::<i32>().unwrap();
401
402 uploads.push(LessonUpload {
403 id,
404 name,
405 state,
406 url,
407 uploaded: {
408 if uploaded.is_some() {
409 Some(uploaded.unwrap())
410 } else {
411 None
412 }
413 },
414 date: Some(date),
415 info: None,
416 });
417 } else if closed.is_some() {
418 let closed = closed.unwrap();
419
420 let name = closed
421 .children()
422 .nth(2)
423 .unwrap()
424 .value()
425 .as_text()
426 .unwrap()
427 .replace("\n", "")
428 .trim()
429 .to_string();
430 let state = false;
431 let url = format!(
432 "{}{}",
433 URL::BASE,
434 group
435 .select(&upload_url_selector)
436 .next()
437 .unwrap()
438 .value()
439 .attr("href")
440 .unwrap()
441 );
442 let uploaded = {
443 match closed.select(&upload_badge_selector).next() {
444 Some(element) => Some(
445 element.text().collect::<String>().trim().to_string(),
446 ),
447 None => None,
448 }
449 };
450 let id = url.split("&id=").last().unwrap().parse::<i32>().unwrap();
451
452 uploads.push(LessonUpload {
453 id,
454 name,
455 state,
456 url,
457 uploaded: {
458 if uploaded.is_some() {
459 Some(uploaded.unwrap())
460 } else {
461 None
462 }
463 },
464 date: None,
465 info: None,
466 })
467 }
468 }
469
470 if uploads.is_empty() {
471 None
472 } else {
473 Some(uploads)
474 }
475 };
476
477 let date = row
478 .child_elements()
479 .nth(0)
480 .unwrap()
481 .text()
482 .collect::<String>()
483 .split("\n")
484 .nth(0)
485 .unwrap()
486 .trim()
487 .to_string();
488 let date = date_time_string_to_datetime(date.as_str(), "02:00:00")
489 .map_err(|e| {
490 Error::DateTime(format!("failed to convert date to DateTime '{:?}'", e))
491 })?
492 .to_utc();
493 let school_hours = {
494 let mut school_hours = vec![];
495
496 let string = row
497 .child_elements()
498 .nth(0)
499 .unwrap()
500 .text()
501 .collect::<String>()
502 .split("\n")
503 .nth(2)
504 .unwrap()
505 .trim()
506 .replace(". ", "")
507 .replace("Stunde", "")
508 .replace("-", "")
509 .trim()
510 .to_string();
511
512 for hour in string.split(' ') {
513 school_hours.push(hour.parse::<i32>().unwrap_or_default())
514 }
515
516 school_hours
517 };
518
519 history.push(LessonEntry {
520 id,
521 date,
522 school_hours,
523 title,
524 details,
525 homework: {
526 if homework.is_some() {
527 Some(homework.unwrap())
528 } else {
529 None
530 }
531 },
532 attachments: {
533 if attachments.is_some() {
534 Some(attachments.clone().unwrap())
535 } else {
536 None
537 }
538 },
539 attachment_number: {
540 if attachments.is_some() {
541 attachments.unwrap().len() as i32
542 } else {
543 0
544 }
545 },
546 uploads: {
547 if uploads.is_some() {
548 Some(uploads.unwrap())
549 } else {
550 None
551 }
552 },
553 })
554 }
555 self.entries = Some(history);
556
557 let marks_section_selector = Selector::parse("#marks").unwrap();
559 let mut marks_doc = Html::parse_document(
560 &document
561 .select(&marks_section_selector)
562 .nth(0)
563 .unwrap()
564 .html(),
565 );
566
567 let encoded_elements: Vec<_> = marks_doc
568 .select(&hidden_div_selector)
569 .map(|x| x.id())
570 .collect();
571 for id in encoded_elements {
572 marks_doc.remove_from_parent(&id)
573 }
574
575 let marks_table_rows_selector = Selector::parse("table>tbody>tr").unwrap();
576 let td_selector = Selector::parse("td").unwrap();
577 let comment_info_selector = Selector::parse("span.fa.fa-comment").unwrap();
578 let marks_table_rows = marks_doc.select(&marks_table_rows_selector);
579
580 let mut marks = vec![];
581
582 for row in marks_table_rows {
583 if row.child_elements().count() == 3 {
584 let name = row
585 .child_elements()
586 .nth(0)
587 .unwrap()
588 .text()
589 .collect::<String>()
590 .trim()
591 .to_string();
592 let date = date_time_string_to_datetime(
593 &format!(
594 "{}{}",
595 row.child_elements()
596 .nth(1)
597 .unwrap()
598 .text()
599 .collect::<String>()
600 .trim()
601 .split_once(",")
602 .unwrap_or_default()
603 .1
604 .trim(),
605 chrono::Local::now().year()
606 ),
607 "02:00:00",
608 )
609 .map_err(|e| {
610 Error::DateTime(format!("failed to convert date to DateTime '{:?}'", e))
611 })?
612 .to_utc();
613 let mark = row
614 .child_elements()
615 .nth(2)
616 .unwrap()
617 .text()
618 .collect::<String>()
619 .trim()
620 .to_string();
621 let comment = match row.next_sibling_element() {
622 Some(element) => match element.select(&td_selector).nth(1) {
623 Some(comment_element) => {
624 if let Some(_) =
625 comment_element.select(&comment_info_selector).nth(0)
626 {
627 Some(
628 comment_element
629 .text()
630 .collect::<String>()
631 .trim()
632 .to_string()
633 .split(':')
634 .nth(1)
635 .unwrap_or_default()
636 .trim()
637 .to_string(),
638 )
639 } else {
640 None
641 }
642 }
643 None => None,
644 },
645 None => None,
646 };
647 marks.push(LessonMark {
648 name,
649 date,
650 mark,
651 comment,
652 });
653 }
654 }
655 self.marks = Some(marks);
656
657 let exam_section_selector = Selector::parse("#klausuren").unwrap();
659 let exam_section = document.select(&exam_section_selector).nth(0).unwrap();
660 let ul_selector = Selector::parse("ul").unwrap();
661 let li_selector = Selector::parse("li").unwrap();
662 let title_selector = Selector::parse("h2").unwrap();
663
664 let mut exams = vec![];
665
666 if !exam_section
667 .child_elements()
668 .nth(0)
669 .unwrap()
670 .html()
671 .contains("Diese Kursmappe beinhaltet leider noch keine Leistungskontrollen!")
672 {
673 for element in exam_section.child_elements() {
674 let elements = element.select(&ul_selector);
675 for element in elements {
676 let sibling_html = Html::parse_document(
677 &element.prev_sibling_element().unwrap().html(),
678 );
679 let title = sibling_html
680 .select(&title_selector)
681 .nth(0)
682 .unwrap()
683 .text()
684 .collect::<String>()
685 .trim()
686 .to_string();
687 let re = Regex::new(r"\s+\n").unwrap();
688
689 let li_elements = element.select(&li_selector);
690 for element in li_elements {
691 let exam = {
692 let text =
693 element.text().collect::<String>().trim().to_string();
694 let mut result =
695 re.replace_all(text.as_str(), "").trim().to_string();
696 let mut trimming = true;
697 while trimming {
698 let previous = result.clone();
699 result = result.replace(" ", " ").trim().to_string();
700 if result == previous {
701 trimming = false;
702 }
703 }
704 result = result.replace("\n", "").trim().to_string();
705 result
706 };
707 let split = exam.split(" ");
708 let date = split.clone().nth(0).unwrap().trim().to_string();
709 let name = {
710 let mut result = "".to_string();
711 for i in 1..split.clone().count() {
712 result =
713 format!("{} {}", result, split.clone().nth(i).unwrap());
714 }
715 result.trim().to_string()
716 };
717
718 exams.push(LessonExam {
719 date,
720 name,
721 finished: {
722 if title == "Alle Leistungskontrolle(n)" {
723 true
724 } else {
725 false
726 }
727 },
728 });
729 }
730 }
731 }
732 }
733 self.exams = Some(exams);
734
735 Ok(())
736 }
737 Err(error) => Err(Error::Network(format!(
738 "Failed to get '{}{}' with error: {}",
739 URL::BASE,
740 &self.url,
741 error
742 ))),
743 }
744 }
745}
746
747impl Homework {
748 pub async fn set_homework(
749 &mut self,
750 state: bool,
751 course_id: i32,
752 entry_id: i32,
753 client: &Client,
754 ) -> Result<(), Error> {
755 match client
756 .post(URL::MEIN_UNTERRICHT)
757 .header("X-Requested-With", "XMLHttpRequest")
758 .form(&[
759 ("a", "sus_homeworkDone"),
760 ("entry", entry_id.to_string().as_str()),
761 ("id", course_id.to_string().as_str()),
762 ("b", {
763 if state {
764 "done"
765 } else {
766 "undone"
767 }
768 }),
769 ])
770 .send()
771 .await
772 {
773 Ok(response) => {
774 let text = response.text().await.unwrap();
775 if text == "1" {
776 self.completed = state;
777 Ok(())
778 } else {
779 Err(Error::ServerSide(format!(
780 "Failed to set homework! Got instead of '1' '{}' as response",
781 text
782 )))
783 }
784 }
785 Err(e) => Err(Error::Network(format!(
786 "Failed to set homework with error: {}",
787 e
788 ))),
789 }
790 }
791}
792
793impl LessonUpload {
794 pub async fn get_info(&self, client: &Client) -> Result<LessonUploadInfo, Error> {
795 match client.get(&self.url).send().await {
796 Ok(response) => {
797 let document = Html::parse_document(&response.text().await.unwrap());
798
799 let requirements_selector =
800 Selector::parse("div#content div.row div.col-md-12").unwrap();
801 let requirements = document.select(&requirements_selector).nth(1).unwrap();
802
803 async fn select_option_string(
804 selector: &Selector,
805 element: &ElementRef<'_>,
806 ) -> Option<String> {
807 match element.select(&selector).nth(0) {
808 Some(element) => {
809 let result = element.text().collect::<String>().trim().to_string();
810 Some(result)
811 }
812 None => None,
813 }
814 }
815
816 let start_selector = Selector::parse("span.editable").unwrap();
817 let start = select_option_string(&start_selector, &requirements);
818
819 let end_selector = Selector::parse("b span.editable").unwrap();
820 let end = select_option_string(&end_selector, &requirements);
821
822 let bool_selector =
823 Selector::parse("i.fa.fa-check-square-o.fa-fw + span.label.label-success")
824 .unwrap();
825 let mut bool_select = requirements.select(&bool_selector);
826
827 let multiple_files = {
828 if bool_select
829 .clone()
830 .nth(0)
831 .unwrap()
832 .text()
833 .collect::<String>()
834 .trim()
835 == "erlaubt"
836 {
837 true
838 } else {
839 false
840 }
841 };
842
843 let unlimited_tries = {
844 match bool_select.nth(1) {
845 Some(option) => {
846 if option.text().collect::<String>().trim() == "erlaubt" {
847 true
848 } else {
849 false
850 }
851 }
852 None => false,
853 }
854 };
855
856 let visibility_selector_0 =
857 Selector::parse("i.fa.fa-eye.fa-fw + span.label").unwrap();
858 let visibility_selector_1 =
859 Selector::parse("i.fa.fa-eye-slash.fa-fw + span.label").unwrap();
860 let visibility = requirements
861 .select(&visibility_selector_0)
862 .nth(0)
863 .and_then(|e| Some(e.text().collect::<String>().trim().to_string()))
864 .or_else(|| {
865 requirements
866 .select(&visibility_selector_1)
867 .nth(0)
868 .and_then(|e| Some(e.text().collect::<String>().trim().to_string()))
869 .or_else(|| None)
870 });
871
872 let automatic_deletion_selector =
873 Selector::parse("i.fa.fa-trash-o.fa-fw + span.label.label-info").unwrap();
874 let automatic_deletion =
875 select_option_string(&automatic_deletion_selector, &requirements);
876
877 let string_select_selector =
878 Selector::parse("i.fa.fa-file.fa-fw + span.label.label-warning").unwrap();
879 let mut string_select = requirements.select(&string_select_selector);
880
881 let allowed_file_types = {
882 let mut result = vec![];
883 let s = string_select
884 .nth(0)
885 .unwrap()
886 .text()
887 .collect::<String>()
888 .trim()
889 .to_string();
890 let split = s.split(", ");
891
892 for s in split {
893 result.push(s.to_string());
894 }
895
896 result
897 };
898
899 let max_file_size = string_select
900 .nth(0)
901 .unwrap()
902 .text()
903 .collect::<String>()
904 .trim()
905 .to_string();
906
907 let extra_selector = Selector::parse("div.alert.alert-info").unwrap();
908 let extra = {
909 match select_option_string(&extra_selector, &requirements).await {
910 Some(s) => Some(s.split("\n").nth(1).unwrap().trim().to_string()),
911 None => None,
912 }
913 };
914
915 let own_files_element_selector =
916 Selector::parse("div#content div.row div.col-md-12").unwrap();
917 let own_files_element =
918 document.select(&own_files_element_selector).nth(2).unwrap();
919
920 let ul_ui_selector = Selector::parse("ul li").unwrap();
921 let own_files_element_for = own_files_element.select(&ul_ui_selector);
922
923 let mut own_files = vec![];
924 let file_index_re = Regex::new(r"f=(\d+)").unwrap();
925
926 let a_selector = Selector::parse("a").unwrap();
927 for element in own_files_element_for {
928 let a = element.select(&a_selector).nth(0).unwrap();
929 let href = a.value().attr("href").unwrap();
930 let name = a.text().collect::<String>().trim().to_string();
931 let url = format!("{}{}", URL::BASE, href);
932 let index = file_index_re
933 .captures(&href)
934 .unwrap()
935 .get(1)
936 .unwrap()
937 .as_str()
938 .to_string()
939 .parse::<i32>()
940 .map_err(|_| {
941 Error::Parsing("Failed to parse index of file as i32".to_string())
942 })?;
943 let comment = {
944 match element.children().nth(9) {
945 Some(node) => {
946 match node.value().as_text() {
948 Some(text) => Some(text.trim().to_string()),
949 None => None,
950 }
951 }
952 None => None,
953 }
954 };
955
956 own_files.push(LessonUploadInfoOwnFile {
957 name,
958 url,
959 index,
960 comment,
961 })
962 }
963
964 let upload_form_selector = Selector::parse("div.col-md-7 form").unwrap();
965
966 let course_id_selector = Selector::parse("input[name='b']").unwrap();
967 let mut course_id = None;
968
969 let entry_id_selector = Selector::parse("input[name='e']").unwrap();
970 let mut entry_id = None;
971
972 match document.select(&upload_form_selector).nth(0) {
973 Some(form) => {
974 course_id = Some(
975 form.select(&course_id_selector)
976 .nth(0)
977 .unwrap()
978 .attr("value")
979 .unwrap()
980 .parse::<i32>()
981 .unwrap(),
982 );
983 entry_id = Some(
984 form.select(&entry_id_selector)
985 .nth(0)
986 .unwrap()
987 .attr("value")
988 .unwrap()
989 .parse::<i32>()
990 .unwrap(),
991 );
992 }
993 None => (),
994 }
995
996 let mut public_files = vec![];
997
998 let public_files_selector =
999 Selector::parse("div#content div.row div.col-md-5").unwrap();
1000 let person_selector = Selector::parse("span.label.label-info").unwrap();
1001 match document.select(&public_files_selector).nth(0) {
1002 Some(public_files_element) => {
1003 for element in public_files_element.select(&ul_ui_selector) {
1004 let a = element.select(&a_selector).nth(0).unwrap();
1005 let href = a.value().attr("href").unwrap();
1006 let name = a.text().collect::<String>().trim().to_string();
1007 let url = format!("{}{}", URL::BASE, href);
1008 let person = element
1009 .select(&person_selector)
1010 .nth(0)
1011 .unwrap()
1012 .text()
1013 .collect::<String>()
1014 .trim()
1015 .to_string();
1016 let index = file_index_re
1017 .captures(&href)
1018 .unwrap()
1019 .get(1)
1020 .unwrap()
1021 .as_str()
1022 .to_string()
1023 .parse::<i32>()
1024 .map_err(|_| {
1025 Error::Parsing(
1026 "Failed to parse index of file as i32".to_string(),
1027 )
1028 })?;
1029
1030 public_files.push(LessonUploadInfoPublicFile {
1031 name,
1032 url,
1033 person,
1034 index,
1035 })
1036 }
1037 }
1038 None => (),
1039 }
1040
1041 let start = start.await;
1042 let end = end.await;
1043 let automatic_deletion = automatic_deletion.await;
1044
1045 async fn parse_date_time(s: String) -> Result<DateTime<Utc>, Error> {
1046 let ymd = format!("{}", &s.split(" ").nth(2).unwrap());
1047 let hms = format!("{}:{}", s.split(" ").nth(3).unwrap(), "00");
1048
1049 let result = date_time_string_to_datetime(&ymd, &hms);
1050 Ok(result
1051 .map_err(|_| {
1052 Error::DateTime("failed to convert lanis time to cron time".to_string())
1053 })?
1054 .to_utc())
1055 }
1056
1057 let start = {
1058 match start {
1059 Some(start) => {
1060 let s = start.replace(", ab", "");
1061 Some(parse_date_time(s).await?)
1062 }
1063 None => None,
1064 }
1065 };
1066
1067 let end = {
1068 match end {
1069 Some(end) => {
1070 let s = end.replace(", spätestens", "");
1071 Some(parse_date_time(s).await?)
1072 }
1073 None => None,
1074 }
1075 };
1076
1077 let result = LessonUploadInfo {
1078 course_id,
1079 entry_id,
1080 start,
1081 end,
1082 multiple_files,
1083 unlimited_tries,
1084 visibility,
1085 automatic_deletion,
1086 allowed_file_types,
1087 max_file_size,
1088 extra,
1089 own_files,
1090 public_files,
1091 };
1092
1093 Ok(result)
1094 }
1095 Err(e) => Err(Error::Network(format!(
1096 "Failed to fetch upload info with error: '{}'",
1097 e
1098 ))),
1099 }
1100 }
1101
1102 pub async fn upload(
1105 &self,
1106 files: Vec<&Path>,
1107 client: &Client,
1108 ) -> Result<Vec<LessonUploadFileStatus>, Error> {
1109 if self.info.is_none() {
1110 return Err(Error::Parsing("No info found in lessons!".to_string()));
1111 }
1112
1113 if files.is_empty() {
1114 return Err(Error::Parsing("No files found in lessons!".to_string()));
1115 }
1116
1117 let upload_info = self.info.clone().unwrap();
1118
1119 let course_id = upload_info.course_id.unwrap();
1120 let entry_id = upload_info.entry_id.unwrap();
1121
1122 let form = reqwest::multipart::Form::new()
1123 .part("a", Part::text("sus_abgabe"))
1124 .part("b", Part::text(course_id.to_string()))
1125 .part("e", Part::text(entry_id.to_string()))
1126 .part("id", Part::text(self.id.to_string()))
1127 .part("file1", {
1128 match files.get(0) {
1129 Some(path) => Part::file(path).await.unwrap(),
1130 None => Part::bytes(&[]),
1131 }
1132 })
1133 .part("file2", {
1134 match files.get(1) {
1135 Some(path) => Part::file(path).await.unwrap(),
1136 None => Part::bytes(&[]),
1137 }
1138 })
1139 .part("file3", {
1140 match files.get(2) {
1141 Some(path) => Part::file(path).await.unwrap(),
1142 None => Part::bytes(&[]),
1143 }
1144 })
1145 .part("file4", {
1146 match files.get(3) {
1147 Some(path) => Part::file(path).await.unwrap(),
1148 None => Part::bytes(&[]),
1149 }
1150 })
1151 .part("file5", {
1152 match files.get(4) {
1153 Some(path) => Part::file(path).await.unwrap(),
1154 None => Part::bytes(&[]),
1155 }
1156 });
1157
1158 let mut headers = HeaderMap::new();
1159 headers.insert("Accept", "*/*".parse().unwrap());
1160 headers.insert("Accept-Encoding", "text".parse().unwrap());
1161 headers.insert("Sec-Fetch-Dest", "document".parse().unwrap());
1162 headers.insert("Sec-Fetch-Mode", "navigate".parse().unwrap());
1163 headers.insert("Sec-Fetch-Site", "same-origin".parse().unwrap());
1164
1165 match client
1172 .post(URL::MEIN_UNTERRICHT)
1173 .headers(headers)
1174 .multipart(form)
1175 .send()
1176 .await
1177 {
1178 Ok(response) => {
1179 let text = response.text().await.unwrap();
1180 let document = Html::parse_document(&text);
1181
1182 let status_message_group_selector =
1183 Selector::parse("div#content div.col-md-12").unwrap();
1184 let status_message_group = document
1185 .select(&status_message_group_selector)
1186 .nth(2)
1187 .unwrap();
1188
1189 let ul_ui_selector = Selector::parse("ul li").unwrap();
1190 let b_selector = Selector::parse("b").unwrap();
1191 let span_label_selector = Selector::parse("span.label").unwrap();
1192
1193 let mut status_messages = vec![];
1194 for status_message in status_message_group.select(&ul_ui_selector) {
1195 let name = status_message.select(&b_selector).nth(0);
1196 if name.is_none() {
1197 return Err(Error::ServerSide("Failed to upload any file!".to_string()));
1198 }
1199 let status = status_message
1200 .select(&span_label_selector)
1201 .nth(0)
1202 .unwrap()
1203 .text()
1204 .collect::<String>()
1205 .trim()
1206 .to_string();
1207
1208 let message = {
1209 match status_message.children().nth(4) {
1210 Some(message) => match message.value().as_text() {
1211 Some(text) => {
1212 let result = text.trim().to_string();
1213 Some(result)
1214 }
1215 None => None,
1216 },
1217 None => None,
1218 }
1219 };
1220
1221 let name = {
1222 if message.is_some() {
1223 let message = message.clone().unwrap();
1224 if !message.contains(
1225 "Datei mit gleichem Namen schon vorhanden. Datei umbenannt in ",
1226 ) {
1227 name.unwrap().text().collect::<String>().trim().to_string()
1228 } else {
1229 message
1230 .split("\"")
1231 .nth(1)
1232 .unwrap()
1233 .replace("\"", "")
1234 .to_string()
1235 }
1236 } else {
1237 name.unwrap().text().collect::<String>().trim().to_string()
1238 }
1239 };
1240
1241 status_messages.push(LessonUploadFileStatus {
1242 name,
1243 status,
1244 message,
1245 })
1246 }
1247 Ok(status_messages)
1248 }
1249 Err(e) => Err(Error::Network(format!(
1250 "Failed to upload file with error: '{}'",
1251 e.to_string()
1252 ))),
1253 }
1254 }
1255
1256 pub async fn delete(&self, file: &i32, account: &Account) -> Result<(), Error> {
1258 let client = &account.client;
1259 let encrypted_password = encrypt_lanis_data(
1260 account.secrets.password.as_bytes(),
1261 &account.key_pair.public_key_string,
1262 );
1263
1264 if self.info.is_none() {
1265 return Err(Error::LessonUploadError(LessonUploadError::NoInfo));
1266 }
1267
1268 let info = self.info.clone().unwrap();
1269
1270 if info.course_id.is_none() || info.entry_id.is_none() {
1271 return Err(Error::LessonUploadError(LessonUploadError::NoInfo));
1272 }
1273
1274 let course_id = info.course_id.clone().unwrap();
1275 let entry_id = info.entry_id.clone().unwrap();
1276
1277 let encrypted_password = encrypted_password.await;
1278
1279 match client
1280 .post(URL::MEIN_UNTERRICHT)
1281 .form(&[
1282 ("a", "sus_abgabe"),
1283 ("d", "delete"),
1284 ("b", &course_id.to_string()),
1285 ("e", &entry_id.to_string()),
1286 ("id", &self.id.to_string()),
1287 ("f", &file.to_string()),
1288 ("pw", &encrypted_password),
1289 ])
1290 .send()
1291 .await
1292 {
1293 Ok(response) => match response.text().await.unwrap().parse::<i32>().unwrap() {
1294 -2 => Err(Error::LessonUploadError(LessonUploadError::DeletionFailed)),
1295 -1 => Err(Error::LessonUploadError(LessonUploadError::WrongPassword)),
1296 0 => Err(Error::LessonUploadError(
1297 LessonUploadError::UnknownServerError,
1298 )),
1299 1 => Ok(()),
1300 _ => Err(Error::LessonUploadError(LessonUploadError::Unknown)),
1301 },
1302 Err(e) => Err(Error::LessonUploadError(LessonUploadError::Network(
1303 e.to_string(),
1304 ))),
1305 }
1306 }
1307}
1308
1309pub async fn get_lessons(account: &Account) -> Result<Vec<Lesson>, Error> {
1310 let client = &account.client;
1311 let unix_time = SystemTime::UNIX_EPOCH.elapsed().unwrap().as_millis();
1312 match client
1313 .get(URL::BASE.to_owned() + &format!("meinunterricht.php?cacheBreaker={}", unix_time))
1314 .send()
1315 .await
1316 {
1317 Ok(response) => {
1318 match response.text().await {
1319 Ok(response) => {
1320 let response =
1321 decrypt_lanis_encoded_tags(&response, &account.key_pair.public_key_string)
1322 .await;
1323 let document = Html::parse_document(&response);
1324 let lesson_folders_selector = Selector::parse("#mappen").unwrap();
1325 let row_selector = Selector::parse(".row").unwrap();
1326 let h2_selector = Selector::parse("h2").unwrap();
1327 let button_selector = Selector::parse("div.btn-group > button").unwrap();
1328 let link_selector = Selector::parse("a.btn.btn-primary").unwrap();
1329
1330 if let Some(lesson_folders) = document.select(&lesson_folders_selector).next() {
1331 if let Some(row) = lesson_folders.select(&row_selector).next() {
1332 let mut lessons = Vec::new();
1333 for lesson in row.child_elements() {
1334 if let Some(url_element) = lesson.select(&link_selector).next() {
1335 let url = url_element.value().attr("href").unwrap().to_string();
1336 let id = url
1337 .split("id=")
1338 .nth(1)
1339 .unwrap()
1340 .to_string()
1341 .parse::<i32>()
1342 .unwrap();
1343 let name = lesson
1344 .select(&h2_selector)
1345 .next()
1346 .unwrap()
1347 .text()
1348 .collect::<String>()
1349 .trim()
1350 .to_string();
1351 let teacher: String = lesson
1352 .select(&button_selector)
1353 .next()
1354 .and_then(|btn| btn.value().attr("title"))
1355 .map(|s| s.to_string())
1356 .unwrap();
1357 let teacher: String =
1358 teacher.split(" (").next().unwrap().to_string();
1359 lessons.push(Lesson {
1360 id,
1361 url,
1362 name,
1363 teacher,
1364 teacher_short: None,
1365 attendances: BTreeMap::new(),
1366 entry_latest: None,
1367 entries: None,
1368 marks: None,
1369 exams: None,
1370 })
1371 }
1372 }
1373
1374 let school_classes_selector = Selector::parse("tr.printable").unwrap();
1376 let school_classes = document.select(&school_classes_selector);
1377 for school_class in school_classes {
1378 fn collect_text(
1379 element_ref: Option<ElementRef>,
1380 ) -> Result<String, ()> {
1381 match element_ref {
1382 Some(element_ref) => {
1383 let s = element_ref
1384 .text()
1385 .collect::<String>()
1386 .trim()
1387 .to_string();
1388 Ok(s)
1389 }
1390 None => Err(()),
1391 }
1392 }
1393 let topic_title_selector = Selector::parse(".thema").unwrap();
1394 let topic_title =
1395 collect_text(school_class.select(&topic_title_selector).next())
1396 .unwrap_or("".to_string());
1397 if topic_title.is_empty() {
1398 continue;
1399 }
1400
1401 let teacher_short_selector = Selector::parse(
1402 ".teacher .btn.btn-primary.dropdown-toggle.btn-xs",
1403 )
1404 .unwrap();
1405 let teacher_short = collect_text(
1406 school_class.select(&teacher_short_selector).next(),
1407 )
1408 .unwrap_or("".to_string());
1409
1410 let topic_date_selector = Selector::parse(".datum").unwrap();
1411 let topic_date =
1412 collect_text(school_class.select(&topic_date_selector).next())
1413 .unwrap_or("".to_string());
1414 let topic_date =
1415 date_time_string_to_datetime(topic_date.as_str(), "02:00:00")
1416 .map_err(|e| {
1417 Error::DateTime(format!(
1418 "failed to convert date to DateTime '{:?}'",
1419 e
1420 ))
1421 })?
1422 .to_utc();
1423
1424 let course_url_selector = Selector::parse("td>h3>a").unwrap();
1425 let course_url = school_class
1426 .select(&course_url_selector)
1427 .next()
1428 .map(|x| {
1429 x.value()
1430 .attr("href")
1431 .unwrap()
1432 .to_string()
1433 .trim()
1434 .to_string()
1435 })
1436 .unwrap_or("".to_string());
1437
1438 let file_count_selector = Selector::parse(".file").unwrap();
1439 let file_count: i32 =
1440 school_class.select(&file_count_selector).count() as i32;
1441
1442 let entry_id = school_class
1443 .value()
1444 .attr("data-entry")
1445 .unwrap_or("")
1446 .parse::<i32>()
1447 .unwrap();
1448
1449 let homework_selector = Selector::parse(".homework").unwrap();
1450 let homework =
1451 school_class.select(&homework_selector).next().map(|_| {
1452 let description_selector =
1453 Selector::parse(".realHomework").unwrap();
1454 let description = school_class
1455 .select(&description_selector)
1456 .next()
1457 .unwrap()
1458 .text()
1459 .collect::<String>()
1460 .trim()
1461 .to_string();
1462 let completed = school_class
1463 .select(&Selector::parse(".undone").unwrap())
1464 .next()
1465 .is_none();
1466 Homework {
1467 description,
1468 completed,
1469 }
1470 });
1471
1472 for lesson in lessons.iter_mut() {
1473 if lesson.url == course_url.to_owned() {
1474 lesson.entry_latest = Option::from(LessonEntry {
1475 id: entry_id.to_owned(),
1476 date: topic_date.to_owned(),
1477 school_hours: vec![-1],
1478 title: topic_title.to_owned(),
1479 details: None,
1480 homework: homework.clone(),
1481 attachments: None,
1482 attachment_number: file_count,
1483 uploads: None,
1484 });
1485 lesson.teacher_short = Some(teacher_short.to_owned());
1486 }
1487 }
1488 }
1489
1490 let attendance_selector = Selector::parse("#anwesend").unwrap();
1491 let thead_selector = Selector::parse("thead > tr").unwrap();
1492 let tbody_selector = Selector::parse("tbody > tr").unwrap();
1493 let link_selector = Selector::parse("a").unwrap();
1494
1495 let attendance_element =
1496 document.select(&attendance_selector).next().unwrap();
1497 let thead_element =
1498 attendance_element.select(&thead_selector).next().unwrap();
1499
1500 let keys: Vec<String> = thead_element
1501 .select(&Selector::parse("th").unwrap())
1502 .map(|el| el.text().collect::<String>().trim().to_string())
1503 .collect();
1504
1505 for row in attendance_element.select(&tbody_selector) {
1506 let mut text_elements: Vec<String> = vec![];
1507 let mut attendances: BTreeMap<String, String> = BTreeMap::new();
1508
1509 for element in row.child_elements() {
1510 if let Some(attr) = element.attr("class") {
1511 if attr.contains("hidden")
1512 && attr.contains("hidden_encoded")
1513 {
1514 continue;
1515 }
1516 }
1517 text_elements.push(
1518 element.text().collect::<String>().trim().to_string(),
1519 );
1520 }
1521
1522 for (i, key) in keys.iter().enumerate() {
1523 let key_lower = key.to_lowercase();
1524 let value =
1525 text_elements.get(i).unwrap_or(&"".to_string()).clone();
1526
1527 if ["kurs", "lehrkraft"].contains(&key_lower.as_str()) {
1528 continue;
1529 }
1530
1531 let mut value = value
1532 .lines()
1533 .skip(1)
1534 .next()
1535 .unwrap_or("")
1536 .trim()
1537 .to_string();
1538
1539 if value.is_empty() {
1540 value = "0".to_string();
1541 }
1542
1543 attendances.insert(key_lower, value);
1544 }
1545
1546 if let Some(hyperlink) = row.select(&link_selector).next() {
1547 let course_url = hyperlink.value().attr("href").unwrap_or("");
1548 for lesson in &mut lessons {
1549 if course_url.contains(&lesson.id.to_string()) {
1550 lesson.attendances = attendances;
1551 break;
1552 }
1553 }
1554 }
1555 }
1556
1557 Ok(lessons)
1558 } else {
1559 Err(Error::Parsing(
1560 "Failed to select rows from lesson folders".to_string(),
1561 ))
1562 }
1563 } else {
1564 Err(Error::Parsing(
1565 "Failed to select lesson folders".to_string(),
1566 ))
1567 }
1568 }
1569 Err(e) => Err(Error::Parsing(format!(
1570 "Failed converting response into text: {}",
1571 e
1572 ))),
1573 }
1574 }
1575 Err(e) => Err(Error::Network(format!(
1576 "Failed to fetch lessons from '{}?cacheBreaker={}':\n{}",
1577 URL::BASE,
1578 unix_time,
1579 e
1580 ))),
1581 }
1582}