use std::collections::HashMap;
use serde::{de::Visitor, Deserialize};
pub struct Client {
client: reqwest::Client,
sesskey: String,
stinfo_token: String,
}
trait HtmlExt {
fn get_named_input(&self, name: &str) -> Option<&str>;
}
impl HtmlExt for scraper::Html {
fn get_named_input(&self, name: &str) -> Option<&str> {
let selector = scraper::Selector::parse(&format!(r#"[name="{name}"]"#)).ok()?;
let mut element_ref =
self.select(&selector)
.map(|e| e.value())
.filter_map(|e| match e.name() {
"input" => e.attr("value"),
"meta" => e.attr("content"),
_ => panic!("unknow named input"),
});
element_ref.next()
}
}
impl Client {
pub fn with_credential(username: &str, password: &str) -> Self {
let client = reqwest::ClientBuilder::new()
.cookie_store(true)
.build()
.expect("cannot build reqwest client");
const LOGIN_URL: &str = "https://sso.hcmut.edu.vn/cas/login?service=http%3A%2F%2Fe-learning.hcmut.edu.vn%2Flogin%2Findex.php%3FauthCAS%3DCAS";
let res = client.get(LOGIN_URL).send().unwrap();
let html = scraper::Html::parse_document(&res.text().unwrap());
let lt = html.get_named_input("lt").expect("lt not found");
let exec_id = html
.get_named_input("execution")
.expect("execution not found");
let event_id = html
.get_named_input("_eventId")
.expect("event id not found");
client
.post(LOGIN_URL)
.form(&[
("lt", lt),
("username", username),
("password", password),
("execution", exec_id),
("_eventId", event_id),
("submit", "Login"),
])
.send()
.expect("login unsuccessfully");
let res = client
.get("http://e-learning.hcmut.edu.vn/my/")
.send()
.unwrap();
let html = scraper::Html::parse_document(&res.text().unwrap());
let sesskey = html
.get_named_input("sesskey")
.expect("sesskey not found")
.to_owned();
let res = client
.get("https://mybk.hcmut.edu.vn/stinfo/")
.send()
.unwrap();
let html = scraper::Html::parse_document(&res.text().unwrap());
let stinfo_token = html
.get_named_input("_token")
.expect("sesskey not found")
.to_owned();
Self {
client,
sesskey,
stinfo_token,
}
}
pub fn get_courses(&self) -> Vec<Course> {
const COURSES_URL: &str = "http://e-learning.hcmut.edu.vn/lib/ajax/service.php";
const BODY: &str = r#"[
{
"args": {
"classification": "allincludinghidden",
"customfieldname": "",
"customfieldvalue": "",
"limit": 0,
"offset": 0,
"sort": "id desc"
},
"index": 0,
"methodname": "core_course_get_enrolled_courses_by_timeline_classification"
}
]"#;
let res = self
.client
.post(COURSES_URL)
.query(&[
(
"info",
"core_course_get_enrolled_courses_by_timeline_classification",
),
("sesskey", &self.sesskey),
])
.body(BODY)
.header(::reqwest::header::CONTENT_TYPE, "application/json")
.send()
.unwrap();
#[derive(Deserialize)]
struct Courses {
courses: Vec<Course>,
}
#[derive(Deserialize)]
struct Res {
data: Courses,
}
let [Res { data }] = res.json::<[Res; 1]>().expect("error parsing response");
data.courses
}
pub fn get_grade_sheet(&self) -> GradeSheet {
const GRADE_URL: &str = "https://mybk.hcmut.edu.vn/stinfo/grade/ajax_grade";
let res = self
.client
.post(GRADE_URL)
.form(&[("_token", &self.stinfo_token)])
.send()
.expect("grade error");
#[derive(Deserialize, Debug)]
struct SemesterGrade {
diem: GradeSheet,
}
let map = res.json::<HashMap<String, SemesterGrade>>().unwrap();
map.into_iter().flat_map(|(_, v)| v.diem).collect()
}
}
impl Drop for Client {
fn drop(&mut self) {
const LOGOUT_URL: &str = "https://sso.hcmut.edu.vn/cas/logout";
self.client.get(LOGOUT_URL).send().expect("log out failed");
}
}