use crate::consts::{CLIENT_ID, TIMEOUT};
use crate::*;
use http::{header, HeaderMap, Response};
use serde::{Deserialize, Serialize};
use std::{fs::File, io::Write, path::PathBuf};
use ureq::typestate::{WithBody, WithoutBody};
use ureq::{Agent, Body, RequestBuilder};
/// log RequestBuilder, send it, log Response and return it
fn log_req_n_call(req: RequestBuilder<WithoutBody>) -> Res<Response<Body>> {
log::debug!("request to be sent: {req:?}");
let resp = req.call()?;
log::debug!("got response: {resp:?}");
Ok(resp)
}
/// log RequestBuilder, send it, log Response and return it
fn log_req_n_send_form<I, K, V>(req: RequestBuilder<WithBody>, form: I) -> Res<Response<Body>>
where
I: IntoIterator<Item = (K, V)>,
K: AsRef<str>,
V: AsRef<str>,
{
log::debug!("request to be sent: {req:?}");
let resp = req.send_form(form)?;
log::debug!("got response: {resp:?}");
Ok(resp)
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub struct User {
/// the username, usually the `oktatási azonosító szám`: "7" + 10 numbers `7XXXXXXXXXX`
pub username: String,
/// the password, usually it defaults to the date of birth of the user: `YYYY-MM-DD`
pub password: String,
/// the id of the school the user goes to, usually looks like: "klik" + 9 numbers: `klikXXXXXXXXX`
pub schoolid: String,
}
// fetch_.*
impl User {
pub fn fetch_info(&self, headers: &HeaderMap) -> Res<UserInfo> {
self.fetch_single::<UserInfo, UserInfo>((), headers)
}
pub fn fetch_evals(&self, interval: OptIrval, headers: &HeaderMap) -> Res<Vec<Evaluation>> {
self.fetch_vec(interval, headers)
}
pub fn fetch_timetable(
&self,
interval: (chrono::NaiveDate, chrono::NaiveDate),
headers: &HeaderMap,
) -> Res<Vec<Lesson>> {
self.fetch_vec(interval, headers)
}
pub fn fetch_absences(&self, interval: OptIrval, headers: &HeaderMap) -> Res<Vec<Absence>> {
self.fetch_vec(interval, headers)
}
pub fn fetch_classes(&self, headers: &HeaderMap) -> Res<Vec<Class>> {
self.fetch_vec((), headers)
}
pub fn fetch_announced_tests(
&self,
interval: OptIrval,
headers: &HeaderMap,
) -> Res<Vec<AnnouncedTest>> {
self.fetch_vec(interval, headers)
}
}
// messages
impl User {
pub fn fetch_full_msg(
&self,
msg_oview: Option<&MsgOview>,
headers: &HeaderMap,
) -> Res<MsgItem> {
let id = msg_oview.map(|mov| mov.azonosito);
self.fetch_single::<MsgItem, MsgItem>(id, headers)
}
pub fn fetch_note_msgs(&self, interval: OptIrval, headers: &HeaderMap) -> Res<Vec<NoteMsg>> {
self.fetch_vec(interval, headers)
}
pub fn fetch_msg_oview_of_kind(
&self,
msg_kind: MsgKind,
headers: &HeaderMap,
) -> Res<Vec<MsgOview>> {
self.fetch_vec(msg_kind, headers)
}
pub fn fetch_msg_oviews(&self, headers: &HeaderMap) -> Res<Vec<MsgOview>> {
Ok([
self.fetch_msg_oview_of_kind(MsgKind::Recv, headers)?,
self.fetch_msg_oview_of_kind(MsgKind::Sent, headers)?,
self.fetch_msg_oview_of_kind(MsgKind::Del, headers)?,
]
.concat())
}
pub fn download_attachment_to(
&self,
id: u32,
out_path: PathBuf,
headers: &HeaderMap,
) -> Res<()> {
let mut f = File::create(out_path)?;
let resp = self.get_response::<Attachment>(id, headers)?;
let content = resp.into_body().read_to_vec()?;
f.write_all(&content)?;
Ok(())
}
}
// token
impl User {
pub fn refresh_token_resp(schoolid: &str, refresh_token: &str) -> Res<Response<Body>> {
let form = [
("institute_code", schoolid),
("grant_type", "refresh_token"),
("client_id", consts::CLIENT_ID),
("refresh_token", refresh_token),
];
let req = ureq::post("https://idp.e-kreta.hu/connect/token")
.header(header::USER_AGENT, consts::USER_AGENT);
log_req_n_send_form(req, form)
}
pub fn refresh_token(&self, refresh_token: &str) -> Res<Token> {
let text = Self::refresh_token_resp(&self.schoolid, refresh_token)?
.into_body()
.read_to_string()?;
log::debug!("token as text/json: {text}");
let token = serde_json::from_str(&text)?;
log::debug!("token: {token:?}");
Ok(token)
}
pub fn get_token_resp(&self) -> Res<Response<Body>> {
let client = Agent::config_builder()
.max_redirects(0) // this is needed so the client doesnt follow redirects by itself like a dumb little sheep
.timeout_global(Some(TIMEOUT))
.build()
.new_agent();
// initial login page
let initial_url = format!("https://idp.e-kreta.hu/Account/Login?ReturnUrl=%2Fconnect%2Fauthorize%2Fcallback%3Fprompt%3Dlogin%26nonce%3DwylCrqT4oN6PPgQn2yQB0euKei9nJeZ6_ffJ-VpSKZU%26response_type%3Dcode%26code_challenge_method%3DS256%26scope%3Dopenid%2520email%2520offline_access%2520kreta-ellenorzo-webapi.public%2520kreta-eugyintezes-webapi.public%2520kreta-fileservice-webapi.public%2520kreta-mobile-global-webapi.public%2520kreta-dkt-webapi.public%2520kreta-ier-webapi.public%26code_challenge%3DHByZRRnPGb-Ko_wTI7ibIba1HQ6lor0ws4bcgReuYSQ%26redirect_uri%3Dhttps%253A%252F%252Fmobil.e-kreta.hu%252Fellenorzo-student%252Fprod%252Foauthredirect%26client_id%3D{CLIENT_ID}%26state%3Dkreten_student_mobile%26suppressed_prompt%3Dlogin");
let req = client.get(initial_url);
let resp = log_req_n_call(req)?;
let raw_login_page_html = resp.into_body().read_to_string()?;
// Parse RVT token from HTML
let login_page_html = scraper::Html::parse_document(&raw_login_page_html);
let selector = scraper::Selector::parse("input[name='__RequestVerificationToken']")
.map_err(|e| format!("Selector parse error: {e}"))?;
let rvt = login_page_html
.select(&selector)
.next()
.ok_or("RVT token not found in HTML")?
.value()
.attr("value")
.ok_or("RVT token value missing")?; // shouldn't really ever happen but still
// Perform login with credentials
let login_url = "https://idp.e-kreta.hu/account/login";
let query_data = (
self.username.clone(),
self.password.clone(),
self.schoolid.clone(),
rvt.to_string(),
);
// it's called query, but that doesn't matter
let form_data = Token::query(&query_data);
// let headers = Token::headers(&"")?.unwrap();
let req = client.post(login_url).header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36");
let resp = log_req_n_send_form(req, form_data)?;
// Check if the response status is 200 (OK)
if !resp.status().is_success() {
return Err(format!(
"Login failed: check your credentials. Status: {}",
resp.status()
)
.into());
}
let req = client.get(format!("https://idp.e-kreta.hu/connect/authorize/callback?prompt=login&nonce=wylCrqT4oN6PPgQn2yQB0euKei9nJeZ6_ffJ-VpSKZU&response_type=code&code_challenge_method=S256&scope=openid%20email%20offline_access%20kreta-ellenorzo-webapi.public%20kreta-eugyintezes-webapi.public%20kreta-fileservice-webapi.public%20kreta-mobile-global-webapi.public%20kreta-dkt-webapi.public%20kreta-ier-webapi.public&code_challenge=HByZRRnPGb-Ko_wTI7ibIba1HQ6lor0ws4bcgReuYSQ&redirect_uri=https%3A%2F%2Fmobil.e-kreta.hu%2Fellenorzo-student%2Fprod%2Foauthredirect&client_id={CLIENT_ID}&state=kreten_student_mobile&suppressed_prompt=login"));
let resp = log_req_n_call(req)?;
// Follow the redirect manually to get the code
let location = resp
.headers()
.get(header::LOCATION)
.ok_or("No Location header after login redirect")?
.to_str()?;
const NOCODE: &str = "auth code not found";
// Extract code from the location header
let uri: http::Uri = location.parse()?;
log::debug!("uri: {uri}");
let query = uri.query().ok_or(NOCODE)?;
log::debug!("query: {query}");
let code_start_ix = query.find("code").ok_or(NOCODE)? + 5;
log::debug!("code start index: {code_start_ix}");
let code_len = query[code_start_ix..].find('&').ok_or(NOCODE)?;
log::debug!("code len: {code_len}");
let code = &query[code_start_ix..code_start_ix + code_len];
log::debug!("code: {code:?}");
// .map(|v| v.to_owned())
// .ok_or("Authorization code not found")?; // this also shouldn't ever happen probably
// Exchange code for access token
let token_data = [
("code", code),
(
"code_verifier",
"DSpuqj_HhDX4wzQIbtn8lr8NLE5wEi1iVLMtMK0jY6c",
),
(
"redirect_uri",
"https://mobil.e-kreta.hu/ellenorzo-student/prod/oauthredirect",
),
("client_id", CLIENT_ID),
("grant_type", "authorization_code"),
];
log::info!("token data: {token_data:?}");
let token_url = [Token::base_url(""), Token::path(&query_data)].concat();
log::info!("token url: {token_url}");
let req = client.post(token_url);
log_req_n_send_form(req, token_data)
}
pub fn fetch_token(&self) -> Res<Token> {
let text = self.get_token_resp()?.into_body().read_to_string()?;
log::debug!("token as text/json: {text}");
let token = serde_json::from_str(&text)?;
log::debug!("token: {token:?}");
Ok(token)
}
}
impl User {
// /// get headers which are necessary for making certain requests
// pub fn headers(&self) -> Res<HeaderMap> {
// Ok(HeaderMap::from_iter([
// (
// header::AUTHORIZATION,
// format!("Bearer {}", self.fetch_token()?.access_token).parse()?,
// ),
// (header::USER_AGENT, consts::USER_AGENT.parse()?),
// ]))
// }
pub fn get_response<E>(&self, query: E::Args, headers: &HeaderMap) -> Res<Response<Body>>
where
E: Endpoint + for<'a> Deserialize<'a>,
{
if std::env::var("NO_NET").is_ok_and(|nn| nn == "1") {
log::info!("manually triggered 'no network connection' error");
return Err("no net".into());
}
let base = E::base_url(&self.schoolid);
let uri = [base, E::path(&query)].concat();
let query = E::query(&query);
let mut req = ureq::get(uri).query_pairs(query);
if let Some(req_headers) = req.headers_mut() {
*req_headers = headers.clone();
}
log_req_n_call(req)
}
pub fn fetch_single<E, D>(&self, query: E::Args, headers: &HeaderMap) -> Res<D>
where
E: Endpoint + for<'a> Deserialize<'a>,
D: for<'a> Deserialize<'a> + std::fmt::Debug,
{
let resp = self.get_response::<E>(query, headers)?;
let txt = resp.into_body().read_to_string()?;
log::debug!("fetched text: {txt:?}");
let deserd = serde_json::from_str(&txt)?;
log::debug!("deserialized fetched text to: {deserd:?}");
Ok(deserd)
}
pub fn fetch_vec<E>(&self, query: E::Args, headers: &HeaderMap) -> Res<Vec<E>>
where
E: Endpoint + for<'a> Deserialize<'a>,
{
let fetch_single = self.fetch_single::<E, Vec<E>>(query, headers);
log::debug!("fetched vec: {fetch_single:?}");
fetch_single
}
}