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#[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 pub key_pair: LanisKeyPair,
51 pub client: Client,
52 pub cookie_store: Arc<CookieStoreMutex>,
53}
54
55#[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 pub student: Option<AccountInfoStudent>,
65 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#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
85pub struct AccountInfoStudent {
86 pub grade: String,
87 pub class: String,
88}
89
90#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
92pub struct AccountInfoTeacher {
93 pub personal_number: String,
94 pub classes: Vec<String>,
96 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 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 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(¶ms)
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 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(¶m).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(); let classes_sub = Vec::new(); 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 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 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#[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#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Serialize, Deserialize)]
474pub struct UntisSecrets {
475 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}