lanis_rs/
lib.rs

1use serde::{Deserialize, Serialize};
2
3pub mod base;
4pub mod modules;
5pub mod utils;
6
7#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Serialize, Deserialize)]
8pub enum Feature {
9    LanisTimetable,
10    MeinUnttericht,
11    FileStorage,
12    MessagesBeta,
13    Calendar,
14}
15
16#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Serialize, Deserialize)]
17pub enum Error {
18    Network(String),
19    /// Happens if anything goes wrong with parsing
20    Parsing(String),
21    Crypto(String),
22    Html(String),
23    /// Happens if something goes wrong when logging into Untis
24    Credentials(String),
25    /// Happens if anything goes wrong while accessing the Untis API
26    UntisAPI(String),
27    /// Happens if anything goes wrong when processing Dates and/or Times
28    DateTime(String),
29    /// Happens if something goes wrong with Threads (like [tokio::task::spawn_blocking]
30    Threading(String),
31    /// Happens if no school with the provided id is found
32    SchoolNotFound(String),
33    /// Happens if key_pair generation fails
34    KeyPair,
35    /// Happens if the user tried to log in to often with the same password. The [u32] contains the timeout in seconds
36    LoginTimeout(u32),
37    /// Happens if anything goes wrong with uploading a file in Lessons
38    LessonUploadError(LessonUploadError),
39    /// Some other Error that may be an issue with the provided values or with the lanis backend
40    ServerSide(String),
41    /// Happens if anything goes wrong when interacting with the file system
42    FileSystem(String),
43    /// Happens if any input is invalid
44    InvalidInput(String),
45}
46
47impl std::fmt::Display for Error {
48    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
49        match self {
50            Error::Network(e) => write!(f, "Error::Network({e})"),
51            Error::Parsing(e) => write!(f, "Error::Parsing({e})"),
52            Error::Crypto(e) => write!(f, "Error::Crypto({e})"),
53            Error::Html(e) => write!(f, "Error::Html({e})"),
54            Error::Credentials(e) => write!(f, "Error::Credentials({e})"),
55            Error::UntisAPI(e) => write!(f, "Error::UntisAPI({e})"),
56            Error::DateTime(e) => write!(f, "Error::DateTime({e})"),
57            Error::Threading(e) => write!(f, "Error::Threading({e})"),
58            Error::SchoolNotFound(e) => write!(f, "Error::SchoolNotFound({e})"),
59            Error::KeyPair => write!(f, "Error::KeyPair"),
60            Error::LoginTimeout(e) => write!(f, "Error::LoginTimeout({e})"),
61            Error::LessonUploadError(e) => write!(f, "Error::LessonUploadError({e})"),
62            Error::ServerSide(e) => write!(f, "Error::ServerSide({e})"),
63            Error::FileSystem(e) => write!(f, "Error::FileSystem({e})"),
64            Error::InvalidInput(e) => write!(f, "Error::InvalidInput({e})"),
65        }
66    }
67}
68
69#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Serialize, Deserialize)]
70pub enum LessonUploadError {
71    /// Happens if 'info' in [LessonUpload] is None
72    NoInfo,
73    /// Happens if course_id or entry_id in [LessonUpload] is None
74    /// This can happen if no UploadForm exists
75    NoDetailedInfo,
76    Network(String),
77    WrongPassword,
78    EncryptionFailed(String),
79    /// Deletion was not possible (Server Side)
80    DeletionFailed,
81    Unknown,
82    UnknownServerError,
83}
84
85impl std::fmt::Display for LessonUploadError {
86    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
87        match self {
88            LessonUploadError::NoInfo => write!(f, "LessonUploadError::NoInfo"),
89            LessonUploadError::NoDetailedInfo => write!(f, "LessonUploadError::NoDetailedInfo"),
90            LessonUploadError::Network(e) => write!(f, "LessonUploadError::Network({e})"),
91            LessonUploadError::WrongPassword => write!(f, "LessonUploadError::WrongPassword"),
92            LessonUploadError::EncryptionFailed(e) => {
93                write!(f, "LessonUploadError::EncryptionFailed({})", e)
94            }
95            LessonUploadError::DeletionFailed => write!(f, "LessonUploadError::DeletionFailed"),
96            LessonUploadError::Unknown => write!(f, "LessonUploadError::Unknown"),
97            LessonUploadError::UnknownServerError => {
98                write!(f, "LessonUploadError::UnknownServerError")
99            }
100        }
101    }
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107
108    use crate::base::account::{Account, AccountSecrets, AccountType, UntisSecrets};
109    use crate::base::schools::{get_school_id, get_schools, School};
110    use crate::modules::lessons::get_lessons;
111    use crate::modules::timetable;
112    use crate::modules::timetable::{Provider, Week};
113
114    use crate::modules::file_storage::FileStoragePage;
115    use crate::modules::messages::{
116        can_choose_type, create_conversation, search_receiver, ConversationOverview,
117    };
118    use crate::utils::crypt::{decrypt_any, encrypt_any};
119    use base::account;
120    use modules::calendar::{
121        self, CalendarExportFileType, CalendarExportFileTypePDF, CalendarExports,
122    };
123    use std::path::Path;
124    use std::{env, fs};
125    use stopwatch_rs::StopWatch;
126
127    #[tokio::test]
128    async fn test_encryption() {
129        let text = fs::read_to_string("test_file.txt").unwrap();
130
131        #[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Serialize, Deserialize)]
132        struct TestText {
133            text: String,
134        }
135
136        let data = TestText { text };
137        let key = b"ILikeToast12!EncryptionIsSoNice!";
138
139        let encrypted = encrypt_any(&data, key).await.unwrap();
140        let decrypted: TestText = decrypt_any(&encrypted, key).await.unwrap();
141
142        assert_eq!(data, decrypted);
143    }
144
145    #[tokio::test]
146    async fn test_schools_get_school_id() {
147        let mut schools: Vec<School> = vec![];
148        schools.push(School {
149            id: 3120,
150            name: String::from("The Almighty Rust School"),
151            city: String::from("Rust City"),
152        });
153        schools.push(School {
154            id: 3920,
155            name: String::from("The Almighty Rust School"),
156            city: String::from("Rust City 2"),
157        });
158        schools.push(School {
159            id: 4031,
160            name: String::from("The Almighty Rust School 2"),
161            city: String::from("Rust City"),
162        });
163        let result = get_school_id("The Almighty Rust School", "Rust City 2", &schools).await;
164        assert_eq!(result, 3920);
165    }
166
167    #[tokio::test]
168    async fn test_schools_get_schools() {
169        let client = reqwest::Client::new();
170
171        let result = get_schools(&client).await.unwrap();
172        assert_eq!(result.get(0).unwrap().id, 3354);
173    }
174
175    async fn create_account() -> Account {
176        let mut stopwatch = StopWatch::start();
177
178        let account_secrets = AccountSecrets::new(
179            {
180                env::var("LANIS_SCHOOL_ID")
181                    .unwrap_or_else(|e| {
182                        println!("Error ({})\nDid you define 'LANIS_SCHOOL_ID' in env?", e);
183                        String::from("0")
184                    })
185                    .parse()
186                    .expect(
187                        "Couldn't parse 'LANIS_SCHOOL_ID'.\nDid you define SCHOOL_ID as an i32?",
188                    )
189            },
190            {
191                env::var("LANIS_USERNAME").unwrap_or_else(|e| {
192                    println!("Error ({})\nDid you define 'LANIS_USERNAME' in env?", e);
193                    String::from("")
194                })
195            },
196            {
197                env::var("LANIS_PASSWORD").unwrap_or_else(|e| {
198                    println!("Error ({})\nDid you define 'LANIS_PASSWORD' in env?", e);
199                    String::from("")
200                })
201            },
202        );
203        let account = Account::new(account_secrets).await.unwrap();
204        println!(
205            "account::new() took {}ms",
206            stopwatch.split().split.as_millis()
207        );
208
209        account
210    }
211
212    #[tokio::test]
213    async fn test_account() {
214        let account = create_account().await;
215
216        let mut stopwatch = StopWatch::start();
217        account.prevent_logout().await.unwrap();
218        println!(
219            "account.prevent_logout() took {}ms",
220            stopwatch.split().split.as_millis()
221        );
222        println!();
223
224        println!("Private Key:\n{}", account.key_pair.private_key_string);
225        println!("Public Key:\n{}", account.key_pair.public_key_string);
226
227        println!("Account Info: {:?}", account.info);
228        println!("Account Type: {:?}", account.account_type);
229
230        println!()
231    }
232
233    #[tokio::test]
234    async fn test_timetable() {
235        let mut account = create_account().await;
236
237        if account.is_supported(Feature::LanisTimetable) {
238            // Lanis (All)
239            let mut stopwatch = StopWatch::start();
240            let time_table_week = Week::new(
241                Provider::Lanis(timetable::LanisType::All),
242                &account.client,
243                chrono::Local::now().date_naive(),
244            )
245            .await
246            .unwrap();
247            let ms = stopwatch.split().split.as_millis();
248            println!("Lanis All: {:?}", time_table_week);
249            println!("Week::new() took {}ms", ms);
250            println!();
251
252            // Lanis (Own)
253            let mut stopwatch = StopWatch::start();
254            let time_table_week = Week::new(
255                Provider::Lanis(timetable::LanisType::Own),
256                &account.client,
257                chrono::Local::now().date_naive(),
258            )
259            .await
260            .unwrap();
261            let ms = stopwatch.split().split.as_millis();
262            println!("Lanis Own: {:?}", time_table_week);
263            println!("Week::new() took {}ms", ms);
264            println!();
265        } else {
266            println!("LanisTimetable is not supported by this account! Skipping.");
267        }
268
269        // Untis
270        if env::var("UNTIS_TEST_TIMETABLE")
271            .unwrap_or("FALSE".to_string())
272            .eq("TRUE")
273        {
274            let mut stopwatch = StopWatch::start();
275            let school_name = env::var("UNTIS_SCHOOL_NAME")
276                .expect("Couldn't find 'UNTIS_SCHOOL_NAME' in env! Did you set it?");
277            let username = env::var("UNTIS_USERNAME")
278                .expect("Couldn't find 'UNTIS_USERNAME' in env! Did you set it?");
279            let password = env::var("UNTIS_PASSWORD")
280                .expect("Couldn't find 'UNTIS_PASSWORD' in env! Did you set it?");
281
282            let secrets = UntisSecrets::new(school_name, username, password);
283            account.secrets.untis_secrets = Some(secrets);
284
285            let time_table_week = Week::new(
286                Provider::Untis(account.secrets.untis_secrets.as_ref().unwrap().clone()),
287                &account.client,
288                chrono::Local::now().date_naive() - chrono::Duration::weeks(1),
289            )
290            .await
291            .unwrap();
292            let ms = stopwatch.split().split.as_millis();
293            println!("Untis: {:?}", time_table_week);
294            println!("Week::new() took {}ms", ms);
295        }
296
297        println!();
298    }
299
300    #[tokio::test]
301    async fn test_lessons() {
302        let account = create_account().await;
303        if account.account_type != AccountType::Student {
304            println!("Not a student account! Skipping!");
305            return;
306        }
307
308        if account.is_supported(Feature::MeinUnttericht) {
309            let mut stopwatch = StopWatch::start();
310            let mut lessons = get_lessons(&account).await.unwrap();
311            println!(
312                "get_lessons() took {}ms",
313                stopwatch.split().split.as_millis()
314            );
315
316            let mut stopwatch = StopWatch::start();
317            for lesson in lessons.iter_mut() {
318                println!("\tid: {}", lesson.id);
319                println!("\turl: {}", lesson.url);
320                println!("\tname: {}", lesson.name);
321                println!("\tteacher: {}", lesson.teacher);
322                println!("\tteacher_short: {:?}", lesson.teacher_short);
323                println!("\tattendances: {:?}", lesson.attendances);
324                println!("\tentry_latest: {:?}", lesson.entry_latest);
325                let mut stopwatch = StopWatch::start();
326                lesson.set_data(&account).await.unwrap();
327                println!(
328                    "\tlesson.set_data() took {}ms",
329                    stopwatch.split().split.as_millis()
330                );
331                println!("\tmarks: {:?}", lesson.marks);
332                println!("\tentries:");
333                let mut stopwatch = StopWatch::start();
334                for mut entry in lesson.entries.clone().unwrap() {
335                    println!("\t\t{:?}", entry);
336                    if entry.homework.is_some() {
337                        let mut homework = entry.homework.clone().unwrap();
338                        let mut new_homework = !homework.completed;
339
340                        let mut stopwatch = StopWatch::start();
341                        homework
342                            .set_homework(new_homework, lesson.id, entry.id, &account.client)
343                            .await
344                            .unwrap();
345                        println!(
346                            "\t\t\tHomework was changed from {} to {} and took {}ms",
347                            !homework.completed,
348                            new_homework,
349                            stopwatch.split().split.as_millis()
350                        );
351                        entry.homework = Some(homework.to_owned());
352                        println!("\t\t\tHomework after change: {:?}", entry.homework);
353
354                        new_homework = !new_homework;
355
356                        let mut stopwatch = StopWatch::start();
357                        homework
358                            .set_homework(new_homework, lesson.id, entry.id, &account.client)
359                            .await
360                            .unwrap();
361                        println!(
362                            "\t\t\tHomework was changed from {} to {} and took {}",
363                            !homework.completed,
364                            new_homework,
365                            stopwatch.split().split.as_millis()
366                        );
367                        entry.homework = Some(homework);
368                        println!("\t\t\tHomework after change: {:?}", entry.homework);
369                    }
370                    if entry.uploads.is_some() {
371                        let mut uploads = entry.uploads.clone().unwrap();
372                        for upload in &mut uploads {
373                            let mut stopwatch = StopWatch::start();
374                            upload.info = Some(upload.get_info(&account.client).await.unwrap());
375                            println!(
376                                "\t\t\tupload.get_info() took {}ms",
377                                stopwatch.split().split.as_millis()
378                            );
379                            println!("\t\t\tUpload: {:?}", upload);
380                            if upload.state {
381                                let mut stopwatch = StopWatch::start();
382                                let path = env::var("LANIS_TEST_FILE").unwrap_or_else(|e| {
383                                    panic!(
384                                        "Error ({})\nDid you define 'LANIS_TEST_FILE' in env?",
385                                        e
386                                    )
387                                });
388                                let path = Path::new(&path);
389                                let status =
390                                    upload.upload(vec![path], &account.client).await.unwrap();
391                                let ms = stopwatch.split().split.as_millis();
392                                println!("\t\t\tUploaded test file: {}", upload.url);
393                                println!("\t\t\t\tUrl: {}", upload.url);
394                                println!("\t\t\t\tStatus: {:?}", status);
395                                println!("\t\t\tupload.upload() took {}ms", ms);
396
397                                let i = {
398                                    upload.info =
399                                        Some(upload.get_info(&account.client).await.unwrap());
400                                    let own_files = upload.info.clone().unwrap().own_files;
401                                    let mut i = -1;
402                                    for file in own_files {
403                                        if file.name == status.get(0).unwrap().name {
404                                            i = file.index;
405                                        }
406                                    }
407
408                                    i
409                                };
410
411                                // Delete uploaded file
412                                let mut stopwatch = StopWatch::start();
413                                if i != -1 {
414                                    upload.delete(&i, &account).await.unwrap();
415                                }
416                                println!(
417                                    "\t\t\tupload.delete() took {}ms",
418                                    stopwatch.split().split.as_millis()
419                                );
420                            }
421                        }
422                    }
423                }
424                println!(
425                    "\tIteration of all entries took {}ms",
426                    stopwatch.split().split.as_millis()
427                );
428                println!("\texams:");
429                for exam in lesson.exams.clone().unwrap() {
430                    println!("\t\t{:?}", exam)
431                }
432
433                println!(" ");
434            }
435            println!(
436                "Iteration of all lessons took {}ms",
437                stopwatch.split().split.as_millis()
438            );
439
440            println!()
441        } else {
442            println!("Lessons are not supported by this account! Skipping.");
443        }
444    }
445
446    #[tokio::test]
447    async fn test_file_storage() {
448        let account = create_account().await;
449
450        if !account.is_supported(Feature::FileStorage) {
451            println!("File Storage is not supported by this account! Skipping.");
452            return;
453        }
454
455        print!("Getting root page... ");
456        let mut stopwatch = StopWatch::start();
457        let root_page = FileStoragePage::get_root(&account.client).await.unwrap();
458        let ms = stopwatch.split().split.as_millis();
459        println!("Took {} ms", ms);
460        println!("Root page:\n{:#?}", root_page);
461        println!();
462
463        if let Some(node) = root_page.folder_nodes.get(0) {
464            print!("Getting folder node page... ");
465            let mut stopwatch = StopWatch::start();
466            let first_page = FileStoragePage::get(node.id, &account.client)
467                .await
468                .unwrap();
469            let ms = stopwatch.split().split.as_millis();
470            println!("Took {} ms", ms);
471            println!("First page:\n{:#?}", first_page);
472            println!();
473
474            if let Some(node) = first_page.file_nodes.get(0) {
475                let path = format!("/tmp/{}", node.name);
476                print!("Downloading first file node to '{}'... ", path);
477                let mut stopwatch = StopWatch::start();
478
479                node.download(&path, &account.client).await.unwrap();
480
481                let ms = stopwatch.split().split.as_millis();
482                println!("Took {}ms", ms);
483
484                print!("Deleting '{}'... ", path);
485                let mut stopwatch = StopWatch::start();
486
487                tokio::fs::remove_file(path).await.unwrap();
488
489                let ms = stopwatch.split().split.as_millis();
490                println!("Took {}ms", ms);
491            }
492        }
493
494        println!();
495    }
496
497    #[tokio::test]
498    async fn test_messages() {
499        let account = create_account().await;
500
501        print!("Getting root page of conversations... ");
502        let mut stopwatch = StopWatch::start();
503        let overviews = ConversationOverview::get_root(&account.client, &account.key_pair)
504            .await
505            .unwrap();
506        let ms = stopwatch.split().split.as_millis();
507        println!("Took {}ms", ms);
508        println!("Conversation overviews: {:#?}", overviews);
509
510        for mut overview in overviews.to_owned() {
511            println!("Current overview: {}", overview.subject);
512            if overview.visible {
513                println!("\tBefore: {}", overview.visible);
514                print!("\tHiding conversation overview... ");
515                let mut stopwatch = StopWatch::start();
516                let result = overview.hide(&account.client).await.unwrap();
517                let ms = stopwatch.split().split.as_millis();
518                println!("Took {}ms", ms);
519                println!("\tResult: {}", result);
520
521                println!("\tNow: {}", overview.visible);
522
523                print!("\tShowing conversation overview... ");
524                let mut stopwatch = StopWatch::start();
525                let result = overview.show(&account.client).await.unwrap();
526                let ms = stopwatch.split().split.as_millis();
527                println!("Took {}ms", ms);
528                println!("\tResult: {}", result);
529                println!("\tAfter: {}", overview.visible);
530            } else {
531                println!("\tBefore: {}", overview.visible);
532                print!("\tShowing conversation overview... ");
533                let mut stopwatch = StopWatch::start();
534                let result = overview.show(&account.client).await.unwrap();
535                let ms = stopwatch.split().split.as_millis();
536                println!("Took {}ms", ms);
537                println!("\tResult: {}", result);
538
539                println!("\tNow: {}", overview.visible);
540
541                print!("\tHiding conversation overview... ");
542                let mut stopwatch = StopWatch::start();
543                let result = overview.hide(&account.client).await.unwrap();
544                let ms = stopwatch.split().split.as_millis();
545                println!("Took {}ms", ms);
546                println!("\tResult: {}", result);
547                println!("\tAfter: {}", overview.visible);
548            }
549            println!();
550
551            print!("\tGetting full conversation... ");
552            let mut stopwatch = StopWatch::start();
553            let mut conversation = overview
554                .get(&account.client, &account.key_pair)
555                .await
556                .unwrap();
557            let ms = stopwatch.split().split.as_millis();
558            println!("Took {}ms", ms);
559            println!("{:#?}", conversation);
560            print!("\tRefreshing conversation... ");
561            let mut stopwatch = StopWatch::start();
562            conversation
563                .refresh(&account.client, &account.key_pair)
564                .await
565                .unwrap();
566            let ms = stopwatch.split().split.as_millis();
567            println!("Took {}ms", ms);
568        }
569
570        if let Ok(reply_number) = env::var("MESSAGES_REPLY_TO") {
571            let reply_number = reply_number.parse::<usize>().unwrap();
572            let overview = overviews.get(reply_number).unwrap();
573            let conversation = overview
574                .get(&account.client, &account.key_pair)
575                .await
576                .unwrap();
577
578            print!("Replying to conversation... ");
579            let mut stopwatch = StopWatch::start();
580            let result = conversation
581                .reply("Test reply", &account.client, &account.key_pair)
582                .await
583                .unwrap();
584            let ms = stopwatch.split().split.as_millis();
585            println!("Took {}ms", ms);
586            assert_eq!(result.is_some(), true);
587            println!("UID of new message: {}", result.unwrap());
588        }
589
590        println!(
591            "Can choose type: {}",
592            can_choose_type(&account.client).await.unwrap()
593        );
594
595        if let Ok(query) = env::var("MESSAGES_RECEIVER_QUERY") {
596            print!("Searching for receiver... ");
597            let mut stopwatch = StopWatch::start();
598            let results = search_receiver(&query, &account.client).await.unwrap();
599            let ms = stopwatch.split().split.as_millis();
600            println!("Took {}ms", ms);
601            println!("Search results: {:#?}", results);
602
603            if let Ok(person_pos) = env::var("MESSAGES_RECEIVER_POS_CREATE") {
604                let person_pos = person_pos.parse::<usize>().unwrap();
605                let content =
606                    fs::read_to_string("test_file.txt").unwrap_or("Test Message".to_string());
607                print!("Creating conversation... ");
608                let mut stopwatch = StopWatch::start();
609                let result = create_conversation(
610                    &vec![results.get(person_pos).unwrap().to_owned()],
611                    "Test Message",
612                    &content,
613                    &account.client,
614                    &account.key_pair,
615                )
616                .await
617                .unwrap();
618                let ms = stopwatch.split().split.as_millis();
619                println!("Took {}ms", ms);
620                assert_eq!(result.is_some(), true);
621                println!("UID of new Conversation: {}", result.unwrap());
622            }
623        }
624
625        println!()
626    }
627
628    #[tokio::test]
629    async fn test_calendar() {
630        let account = create_account().await;
631        match account.is_supported(Feature::Calendar) {
632            true => {
633                println!("Fetching calendar entries...");
634                let mut stopwatch = StopWatch::start();
635                let entries = calendar::get_entries(
636                    chrono::Local::now().date_naive(),
637                    chrono::Local::now().date_naive() + chrono::Duration::days(365),
638                    None,
639                    &account.client,
640                )
641                .await
642                .unwrap();
643                let ms = stopwatch.split().split.as_millis();
644                for entry in entries {
645                    println!("Entry: {:?}", entry);
646                }
647                println!("Took {}ms", ms);
648
649                let mut stopwatch = StopWatch::start();
650                println!("Downloading exports...");
651                println!(
652                    "iCal link: {}",
653                    CalendarExports::get_ical(&account.client).await.unwrap()
654                );
655                let exports = CalendarExports::get(&account.client).await.unwrap();
656                exports
657                    .get_export(
658                        &account.client,
659                        CalendarExportFileType::PDF(CalendarExportFileTypePDF::YearDetailed(
660                            *exports.available_years.first().unwrap(),
661                        )),
662                        "./target/temp.pdf",
663                    )
664                    .await
665                    .unwrap();
666                let ms = stopwatch.split().split.as_millis();
667                println!("Took {}ms", ms);
668            }
669            false => {
670                println!("Calendar is not support. Skipping...")
671            }
672        }
673    }
674}