use chrono::{Local, Months, NaiveDate, NaiveDateTime}; use reqwest::header::{ACCEPT, CONTENT_TYPE}; use serde::{Deserialize, Serialize}; #[derive(Clone, PartialEq, Eq)] pub struct RemainingPlace { pub id: String, pub description: String, pub date: String, pub free: usize, } impl Ord for RemainingPlace { fn cmp(&self, other: &Self) -> std::cmp::Ordering { self.id.cmp(&other.id) } } impl PartialOrd for RemainingPlace { fn partial_cmp(&self, other: &Self) -> Option { self.id.partial_cmp(&other.id) } } #[derive(Deserialize)] struct RemainsListResponse { data: Vec, } #[derive(Deserialize)] struct RemainsEntry { id: i32, courseName: String, coursePublicName: String, courseShortName: String, number: String, locationName: String, locationShortName: String, startDateIso: NaiveDate, endDateIso: NaiveDate, // naivedatetime mit custom format -> YYYY-MM-DDTHH:MM:SS+mm:mm start: String, end: String, startTimeInfo: String, endTimeInfo: String, numOfAvailablePlaces: u32, courseYear_id: u32, publicVisibility: u32, publicCommentForSendingOrganisation: Option, status: u32, courseYear_status: u32, numOfParticipants: u32, numOfPlaces: u32, language: String, faculty_id: u32, facultyName: Option, } #[derive(Serialize)] struct RemainsListRequest { courseYear_id: u32, useDateInterval: bool, selectedIntervalStart: NaiveDateTime, selectedIntervalEnd: NaiveDateTime, refreshParams: RefreshParams, ifField: String, } #[derive(Serialize, Deserialize)] struct RefreshParams { paginationInfo: PaginationInfo, sortItems: Vec, filterItems: Vec, aggregateParam: AggregateParam, } #[derive(Serialize, Deserialize)] struct PaginationInfo { currentPageIndex: u32, currentPageNumOfItems: u32, maxPageIndex: u32, totalNumOfItems: u32, pageSize: u32, } #[derive(Serialize, Deserialize)] struct AggregateParam { items: Vec, selectedIds: Vec, aggregatesOnly: bool, } pub fn get_current_places(url: &str) -> Result, reqwest::Error> { let client = reqwest::blocking::Client::new(); let req_body = RemainsListRequest { courseYear_id: 13, useDateInterval: true, selectedIntervalStart: Local::now().naive_utc(), selectedIntervalEnd: Local::now() .naive_utc() .checked_add_months(Months::new(12)) .unwrap(), refreshParams: RefreshParams { paginationInfo: PaginationInfo { currentPageIndex: 0, currentPageNumOfItems: 50, maxPageIndex: 0, totalNumOfItems: 9, pageSize: 50, }, sortItems: Vec::default(), filterItems: Vec::default(), aggregateParam: AggregateParam { items: Vec::default(), selectedIds: Vec::default(), aggregatesOnly: false, }, }, ifField: "id".to_string(), }; let req = client .post("https://sachsen.leveso.de/Api/Public/GetRemainsListCourseAppointmentList") .body(serde_json::to_string(&req_body).unwrap()) .header(ACCEPT, "application/json") .header(CONTENT_TYPE, "application/json") .build()?; let body = client.execute(req)?; let json_body: RemainsListResponse = body.json().unwrap(); let places = json_body .data .iter() .map(|r| RemainingPlace { id: create_id(&r), description: create_description(&r), date: r.startDateIso.format("%d.%m.%Y").to_string(), free: r.numOfAvailablePlaces as usize, }) .collect(); Ok(places) } fn create_id(entry: &RemainsEntry) -> String { if let Some((only_id, _)) = entry.number.split_once(' ') { return only_id.to_string(); } entry.number.to_string() } fn create_description(entry: &RemainsEntry) -> String { let title_without_number = if let Some((_, v)) = entry.courseName.split_once(' ') { v } else { &entry.courseName }; if entry.publicVisibility == 0 { return format!("{} (PLATZ NICHT ÖFFENTLICH!)", title_without_number); } title_without_number.to_string() }