lanis_rs/base/
account.rs

1use crate::base::account::AccountType::{Student, Teacher};
2use crate::base::schools::{get_school, get_schools, School};
3use crate::utils::constants::URL;
4use crate::utils::crypt::{
5    decrypt_any, encrypt_any, generate_lanis_key_pair, CryptorError, LanisKeyPair,
6};
7use crate::utils::datetime::date_string_to_naivedate;
8use crate::Error;
9use crate::Feature;
10use chrono::NaiveDate;
11use reqwest::header::LOCATION;
12use reqwest::redirect::Policy;
13use reqwest::{Client, StatusCode};
14use reqwest_cookie_store::{CookieStore, CookieStoreMutex};
15use scraper::{Html, Selector};
16use serde::{Deserialize, Serialize};
17use std::collections::BTreeMap;
18use std::fmt::Display;
19use std::string::String;
20use std::sync::Arc;
21
22#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Serialize, Deserialize)]
23pub enum AccountType {
24    Student,
25    Teacher,
26    Parent,
27    Unknown,
28}
29
30impl Display for AccountType {
31    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
32        match self {
33            Student => write!(f, "Student"),
34            Teacher => write!(f, "Teacher"),
35            AccountType::Parent => write!(f, "Parent"),
36            AccountType::Unknown => write!(f, "Unknown"),
37        }
38    }
39}
40
41/// Stores everything that is needed at Runtime and related to the Account
42#[derive(Clone, Debug)]
43pub struct Account {
44    pub school: School,
45    pub secrets: AccountSecrets,
46    pub account_type: AccountType,
47    pub features: Vec<Feature>,
48    pub info: AccountInfo,
49    /// You can generate a new KeyPair by using the Ok result of [generate_lanis_key_pair()] <br> Make sure to not define anything larger than 151 (bits) as size
50    pub key_pair: LanisKeyPair,
51    pub client: Client,
52    pub cookie_store: Arc<CookieStoreMutex>,
53}
54
55/// The account info
56#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
57pub struct AccountInfo {
58    pub firstname: String,
59    pub lastname: String,
60    pub username: String,
61    pub birthdate: NaiveDate,
62    pub gender: Gender,
63    /// Should be Some if the Account is of type Student so safe to call unwrap on
64    pub student: Option<AccountInfoStudent>,
65    /// Should be Some if the Account is of type Teacher so safe to call unwrap on
66    pub teacher: Option<AccountInfoTeacher>,
67}
68
69impl AccountInfo {
70    pub fn empty() -> Self {
71        Self {
72            firstname: String::new(),
73            lastname: String::new(),
74            username: String::new(),
75            birthdate: NaiveDate::MIN,
76            gender: Gender::Unknown,
77            student: None,
78            teacher: None,
79        }
80    }
81}
82
83/// Student specifc infos. There is no gurantee for all fields to be filled (they may be empty)
84#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
85pub struct AccountInfoStudent {
86    pub grade: String,
87    pub class: String,
88}
89
90/// Teacher specifc infos. There is no gurantee for all fields to be filled (they may be empty)
91#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
92pub struct AccountInfoTeacher {
93    pub personal_number: String,
94    /// The "Klassenleitungen" list
95    pub classes: Vec<String>,
96    /// The "Stellvertretende Klassenleitungen" list
97    pub classes_sub: Vec<String>,
98}
99
100#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Serialize, Deserialize)]
101pub enum Gender {
102    Male,
103    Female,
104    Diverse,
105    Unknown,
106}
107
108impl Account {
109    /// Creates a new [Account] from a school_id, username and password <br>
110    /// When using [new] a session gets automatically created and all fields will be set
111    pub async fn new(secrets: AccountSecrets) -> Result<Account, Error> {
112        let cookie_store = CookieStore::new(None);
113        let cookie_store = CookieStoreMutex::new(cookie_store);
114        let cookie_store = Arc::new(cookie_store);
115
116        let client = Client::builder()
117            .redirect(Policy::none())
118            .cookie_provider(std::sync::Arc::clone(&cookie_store))
119            .gzip(true)
120            .build()
121            .unwrap();
122
123        let key_pair = generate_lanis_key_pair(128, &client).await?;
124
125        let schools = get_schools(&client).await?;
126        let school = get_school(&secrets.school_id, &schools).await?;
127
128        let mut account = Account {
129            school,
130            secrets,
131            account_type: AccountType::Unknown,
132            info: AccountInfo::empty(),
133            features: Vec::new(),
134            key_pair,
135            client,
136            cookie_store,
137        };
138
139        account.create_session().await?;
140        (account.info, account.account_type) = account.fetch_account_info().await?;
141        account.features = account.get_features().await?;
142
143        Ok(account)
144    }
145
146    /**
147     * Takes an account and a 'reqwest' client and generates a new session for lanis <br>
148     * Needs to be run on every new 'reqwest' client <br>
149     * Doesn't need to be run if [new] was used
150     */
151    pub async fn create_session(&self) -> Result<(), Error> {
152        let params = [
153            ("user2", self.secrets.username.clone()),
154            (
155                "user",
156                format!("{}.{}", self.school.id, self.secrets.username.clone()),
157            ),
158            ("password", self.secrets.password.clone()),
159        ];
160        let response = self
161            .client
162            .post(URL::LOGIN.to_owned() + &*format!("?i={}", self.school.id))
163            .form(&params)
164            .send();
165        match response.await {
166            Ok(response) => {
167                let response_status = response.status();
168
169                let text = response.text().await.map_err(|e| {
170                    Error::Parsing(format!("Failed to parse response as text: {}", e))
171                })?;
172                let html = Html::parse_document(&text);
173
174                let timeout_selector = Selector::parse("#authErrorLocktime").unwrap();
175                if let Some(timeout) = html.select(&timeout_selector).nth(0) {
176                    return Err(Error::LoginTimeout(
177                        timeout
178                            .text()
179                            .collect::<String>()
180                            .trim()
181                            .parse()
182                            .map_err(|e| {
183                                Error::Parsing(format!(
184                                    "Failed to parse timeout from response as u32: {}",
185                                    e
186                                ))
187                            })?,
188                    ));
189                }
190
191                if response_status == StatusCode::FOUND {
192                    match self.client.get(URL::CONNECT).send().await {
193                        Ok(response) => match response.headers().get(LOCATION) {
194                            Some(location) => {
195                                let location = location.to_str();
196                                if location.is_err() {
197                                    return Err(Error::Parsing(
198                                        "failed to parse location header to str".to_string(),
199                                    ));
200                                }
201                                let location = location.unwrap();
202
203                                match self.client.get(location).send().await {
204                                    Ok(_) => Ok(()),
205                                    Err(e) => Err(Error::Network(format!(
206                                        "error getting login URL header: {}",
207                                        e
208                                    ))),
209                                }
210                            }
211                            None => Err(Error::Network("error getting login URL".to_string())),
212                        },
213                        Err(e) => Err(Error::Network(format!("{}", e))),
214                    }
215                } else {
216                    Err(Error::Credentials("Wrong credentials!".to_string()))
217                }
218            }
219            Err(e) => Err(Error::Network(e.to_string())),
220        }
221    }
222
223    /**
224     *  Refreshes the session to prevent getting logged out
225     *  <br> Needs to be called periodically e.g. every 10 seconds
226     */
227    pub async fn prevent_logout(&self) -> Result<(), Error> {
228        let sid: String = {
229            let cs = self.cookie_store.lock().unwrap();
230            let mut result = "NONE".to_string();
231            for cookie in cs.iter_any() {
232                if cookie.name() == "sid" {
233                    result = cookie.value().to_string();
234                }
235            }
236            result
237        };
238        let param = [("name", sid)];
239        match self.client.get(URL::LOGIN_AJAX).form(&param).send().await {
240            Ok(_) => Ok(()),
241            Err(e) => Err(Error::Network(
242                format!("failed to refresh session: {}", e).to_string(),
243            )),
244        }
245    }
246
247    pub async fn fetch_account_info(&self) -> Result<(AccountInfo, AccountType), Error> {
248        match self
249            .client
250            .get(URL::USER_DATA)
251            .query(&[("a", "userData")])
252            .send()
253            .await
254        {
255            Ok(response) => {
256                let document = Html::parse_document(&response.text().await.unwrap());
257                let user_data_table_body_selector =
258                    Selector::parse("div.col-md-12 table.table.table-striped tbody").unwrap();
259
260                let row_selector = Selector::parse("tr").unwrap();
261                let key_selector = Selector::parse("td").unwrap();
262
263                let mut result = BTreeMap::new();
264
265                if let Some(user_data_table_body) =
266                    document.select(&user_data_table_body_selector).next()
267                {
268                    for row in user_data_table_body.select(&row_selector) {
269                        let cells: Vec<_> = row.select(&key_selector).collect();
270                        if cells.len() >= 2 {
271                            let key = cells[0].text().collect::<String>().trim().to_string();
272                            let value = cells[1].text().collect::<String>().trim().to_string();
273                            let key = key[..key.len() - 1].to_lowercase();
274                            result.insert(key, value);
275                        }
276                    }
277                }
278
279                let firstname = result.get("vorname").unwrap_or(&String::new()).to_owned();
280                let lastname = result.get("nachname").unwrap_or(&String::new()).to_owned();
281                let username = result.get("login").unwrap_or(&String::new()).to_owned();
282                let birthdate = {
283                    let s = result
284                        .get("geburtsdatum")
285                        .unwrap_or(&String::from("01.01.1970"))
286                        .to_owned();
287                    date_string_to_naivedate(&s).map_err(|e| {
288                        Error::DateTime(format!("failed to convert date to DateTime '{:?}'", e))
289                    })?
290                };
291                let gender = {
292                    let s = result
293                        .get("geschlecht")
294                        .unwrap_or(&String::new())
295                        .to_owned();
296                    match s.as_str() {
297                        "männlich" => Gender::Male,
298                        "weiblich" => Gender::Female,
299                        "divers" => Gender::Diverse,
300                        _ => Gender::Unknown,
301                    }
302                };
303                let account_type = if result.contains_key("stufe") {
304                    AccountType::Student
305                } else if result.contains_key("personalnummer") {
306                    AccountType::Teacher
307                } else {
308                    AccountType::Unknown
309                };
310
311                let info = match account_type {
312                    AccountType::Student => {
313                        let grade = result.get("stufe").unwrap_or(&String::new()).to_owned();
314                        let class = result.get("klasse").unwrap_or(&String::new()).to_owned();
315
316                        let student = Some(AccountInfoStudent { grade, class });
317
318                        AccountInfo {
319                            firstname,
320                            lastname,
321                            username,
322                            birthdate,
323                            gender,
324                            student,
325                            teacher: None,
326                        }
327                    }
328                    AccountType::Teacher => {
329                        let personal_number = result
330                            .get("personalnummer")
331                            .unwrap_or(&String::new())
332                            .to_owned();
333                        let classes = Vec::new(); // TODO: Parse bullet point list
334                        let classes_sub = Vec::new(); // TODO: Parse bullet point list
335                        let teacher = Some(AccountInfoTeacher {
336                            personal_number,
337                            classes,
338                            classes_sub,
339                        });
340
341                        AccountInfo {
342                            firstname,
343                            lastname,
344                            username,
345                            birthdate,
346                            gender,
347                            student: None,
348                            teacher,
349                        }
350                    }
351                    AccountType::Parent => {
352                        // TODO: Fetch Parent specifc info
353                        AccountInfo {
354                            firstname,
355                            lastname,
356                            username,
357                            birthdate,
358                            gender,
359                            student: None,
360                            teacher: None,
361                        }
362                    }
363                    AccountType::Unknown => AccountInfo {
364                        firstname,
365                        lastname,
366                        username,
367                        birthdate,
368                        gender,
369                        student: None,
370                        teacher: None,
371                    },
372                };
373
374                Ok((info, account_type))
375            }
376            Err(e) => Err(Error::Network(
377                format!("failed to fetch account data: {}", e).to_string(),
378            )),
379        }
380    }
381
382    pub async fn get_type(&self) -> AccountType {
383        self.account_type.to_owned()
384    }
385
386    /// Returns a vector of supported features (for the [Account])
387    pub async fn get_features(&self) -> Result<Vec<Feature>, Error> {
388        #[derive(Debug, Deserialize)]
389        #[serde(rename_all = "lowercase")]
390        struct Entry {
391            link: String,
392        }
393
394        #[derive(Debug, Deserialize)]
395        #[serde(rename_all = "lowercase")]
396        struct Entries {
397            entrys: Vec<Entry>,
398        }
399
400        match self
401            .client
402            .get(URL::START)
403            .query(&[("a", "ajax"), ("f", "apps")])
404            .send()
405            .await
406        {
407            Ok(response) => {
408                let text = response.text().await.unwrap();
409                let entries = serde_json::from_str::<Entries>(&text).unwrap();
410
411                let mut features = Vec::new();
412
413                for entry in entries.entrys {
414                    match entry.link.trim() {
415                        "meinunterricht.php" => features.push(Feature::MeinUnttericht),
416                        "stundenplan.php" => features.push(Feature::LanisTimetable),
417                        "dateispeicher.php" => features.push(Feature::FileStorage),
418                        "nachrichten.php" => features.push(Feature::MessagesBeta),
419                        "kalender.php" => features.push(Feature::Calendar),
420                        _ => continue,
421                    }
422                }
423
424                Ok(features)
425            }
426            Err(e) => Err(Error::Network(e.to_string())),
427        }
428    }
429
430    pub fn is_supported(&self, feature: Feature) -> bool {
431        if self.features.contains(&feature) {
432            true
433        } else {
434            false
435        }
436    }
437}
438
439/// Contains the account secrets for Lanis and maybe Untis <br>
440/// This will be used for re-login. <br>
441#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Serialize, Deserialize)]
442pub struct AccountSecrets {
443    pub school_id: i32,
444    pub username: String,
445    pub password: String,
446    pub untis_secrets: Option<UntisSecrets>,
447}
448
449impl AccountSecrets {
450    pub fn new(school_id: i32, username: String, password: String) -> AccountSecrets {
451        Self {
452            school_id,
453            username,
454            password,
455            untis_secrets: None,
456        }
457    }
458
459    pub async fn from_encrypted(
460        data: &[u8],
461        key: &[u8; 32],
462    ) -> Result<AccountSecrets, CryptorError> {
463        decrypt_any(data, key).await
464    }
465
466    pub async fn encrypt(&self, key: &[u8; 32]) -> Result<Vec<u8>, CryptorError> {
467        encrypt_any(&self, key).await
468    }
469}
470
471/// Contains the account secrets for Untis <br>
472/// This will be used for re-login. <br>
473#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Serialize, Deserialize)]
474pub struct UntisSecrets {
475    /// The internal school name from Untis and not the display name
476    pub school_name: String,
477    pub username: String,
478    pub password: String,
479}
480
481impl UntisSecrets {
482    pub fn new(school_name: String, username: String, password: String) -> UntisSecrets {
483        Self {
484            school_name,
485            username,
486            password,
487        }
488    }
489}