lanis_rs/modules/
lessons.rs

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    /// Should always be Some if nothing went wrong
26    pub teacher_short: Option<String>,
27    pub attendances: BTreeMap<String, String>,
28    /// If this is None there is no latest entry
29    pub entry_latest: Option<LessonEntry>,
30    /// Will be empty if no entries are found and None if this value wasn't initialized
31    pub entries: Option<Vec<LessonEntry>>,
32    /// Will be empty if no marks are found and None if this value wasn't initialized
33    pub marks: Option<Vec<LessonMark>>,
34    /// Will be empty if no exams are found and None if this value wasn't initialized
35    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    /// True if open and false if closed
69    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    /// Represents if multiple files can be uploaded
83    pub multiple_files: bool,
84    /// Represents if files can be uploaded unlimited times
85    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    /// Has some extra info
91    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    /**
135     *  Sets the data for a lesson. This data includes: <br>
136     *  Entries history, marks and class tests
137     */
138    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                // Remove encoded divs
177                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                // Selectors for loop
184                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                // Marks
558                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                // Exams
658                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                                // TODO: TEST
947                                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    /// Takes a vector of file paths (max. 5) and uploads these files to Lanis. <br>
1103    /// [LessonUpload::get_info] must be called before calling this function
1104    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        //return Ok(vec![LessonUploadFileStatus{
1166        //    name: "Not yet finished".to_string(),
1167        //    status: "Same".to_string(),
1168        //    message: Some("Same again".to_string()),
1169        //}]);
1170
1171        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    /// Deletes an already uploaded File (Takes a file id)
1257    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                            // Get latest lesson entry
1375                            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}