2022-12-14 14:29:10 +00:00
|
|
|
use anyhow::{bail, Context, Result};
|
|
|
|
use chrono::Datelike;
|
|
|
|
use cookie::Cookie;
|
|
|
|
use serde::Deserialize;
|
|
|
|
use serde_json::json;
|
2023-01-27 17:13:53 +00:00
|
|
|
use url::Url;
|
2022-12-14 14:29:10 +00:00
|
|
|
|
|
|
|
fn deserialize_date<'de, D>(deserializer: D) -> Result<chrono::NaiveDate, D::Error>
|
|
|
|
where
|
|
|
|
D: serde::de::Deserializer<'de>,
|
|
|
|
{
|
|
|
|
struct Visitor;
|
|
|
|
|
|
|
|
impl<'de> serde::de::Visitor<'de> for Visitor {
|
|
|
|
type Value = chrono::NaiveDate;
|
|
|
|
|
|
|
|
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
|
|
|
formatter.write_str("a naive date serialized as an unsigned integer")
|
|
|
|
}
|
|
|
|
|
|
|
|
fn visit_u64<E>(self, mut v: u64) -> Result<Self::Value, E>
|
|
|
|
where
|
|
|
|
E: serde::de::Error,
|
|
|
|
{
|
|
|
|
let y = (v / 10000) as i32;
|
|
|
|
v %= 10000;
|
|
|
|
let m = (v / 100) as u32;
|
|
|
|
v %= 100;
|
|
|
|
|
|
|
|
match chrono::NaiveDate::from_ymd_opt(y, m, v as u32) {
|
|
|
|
Some(x) => Ok(x),
|
|
|
|
None => Err(E::custom(format!("No such date: {}-{}-{}", y, m, v))),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
deserializer.deserialize_u64(Visitor)
|
|
|
|
}
|
|
|
|
|
|
|
|
fn serialize_date(date: chrono::NaiveDate) -> u64 {
|
|
|
|
date.year() as u64 * 10000 + date.month() as u64 * 100 + date.day() as u64
|
|
|
|
}
|
|
|
|
|
|
|
|
fn deserialize_time<'de, D>(deserializer: D) -> Result<chrono::NaiveTime, D::Error>
|
|
|
|
where
|
|
|
|
D: serde::de::Deserializer<'de>,
|
|
|
|
{
|
|
|
|
struct Visitor;
|
|
|
|
|
|
|
|
impl<'de> serde::de::Visitor<'de> for Visitor {
|
|
|
|
type Value = chrono::NaiveTime;
|
|
|
|
|
|
|
|
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
|
|
|
formatter.write_str("a naive time serialized as an unsigned integer")
|
|
|
|
}
|
|
|
|
|
|
|
|
fn visit_u64<E>(self, mut v: u64) -> Result<Self::Value, E>
|
|
|
|
where
|
|
|
|
E: serde::de::Error,
|
|
|
|
{
|
|
|
|
let h = v / 100;
|
|
|
|
v %= 100;
|
|
|
|
|
|
|
|
match chrono::NaiveTime::from_hms_opt(h as u32, v as u32, 0) {
|
|
|
|
Some(x) => Ok(x),
|
|
|
|
None => Err(E::custom(format!("No such time: {}:{}", h, v))),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
deserializer.deserialize_u64(Visitor)
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Deserialize, Debug)]
|
|
|
|
pub struct RpcError {
|
|
|
|
pub message: String,
|
|
|
|
pub code: i32,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Deserialize, Debug)]
|
|
|
|
pub struct RpcResponse<T> {
|
|
|
|
pub jsonrpc: String,
|
|
|
|
pub id: String,
|
|
|
|
pub result: Option<T>,
|
|
|
|
pub error: Option<RpcError>,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Deserialize, Debug)]
|
|
|
|
struct RpcEmptyResult;
|
|
|
|
|
|
|
|
#[derive(Deserialize, Debug)]
|
|
|
|
pub struct RpcLogin {
|
|
|
|
#[serde(rename = "sessionId")]
|
|
|
|
pub session_id: String,
|
|
|
|
#[serde(rename = "personType")]
|
|
|
|
pub person_type: i32,
|
|
|
|
#[serde(rename = "personId")]
|
|
|
|
pub person_id: i32,
|
|
|
|
#[serde(rename = "klasseId")]
|
|
|
|
pub class_id: i32,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Deserialize, Debug)]
|
|
|
|
pub struct RpcClass {
|
|
|
|
pub id: i32,
|
|
|
|
pub name: String,
|
|
|
|
#[serde(rename = "longName")]
|
|
|
|
pub long_name: String,
|
|
|
|
pub active: bool,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Deserialize, Debug)]
|
|
|
|
pub struct RpcSubject {
|
|
|
|
pub id: i32,
|
|
|
|
pub name: String,
|
|
|
|
#[serde(rename = "longName")]
|
|
|
|
pub long_name: String,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Deserialize, Debug)]
|
|
|
|
pub struct RpcRoom {
|
|
|
|
pub id: i32,
|
|
|
|
pub name: String,
|
|
|
|
#[serde(rename = "longName")]
|
|
|
|
pub long_name: String,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Deserialize, Debug)]
|
|
|
|
pub struct RpcDepartment {
|
|
|
|
pub id: i32,
|
|
|
|
pub name: String,
|
|
|
|
#[serde(rename = "longName")]
|
|
|
|
pub long_name: String,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Deserialize, Debug)]
|
|
|
|
pub struct RpcHoliday {
|
|
|
|
pub id: i32,
|
|
|
|
pub name: String,
|
|
|
|
#[serde(rename = "longName")]
|
|
|
|
pub long_name: String,
|
|
|
|
#[serde(rename = "startDate", deserialize_with = "deserialize_date")]
|
|
|
|
pub start_date: chrono::NaiveDate,
|
|
|
|
#[serde(rename = "endDate", deserialize_with = "deserialize_date")]
|
|
|
|
pub end_date: chrono::NaiveDate,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Deserialize, Debug)]
|
|
|
|
pub struct RpcTimegridDayTimeUnit {
|
|
|
|
#[serde(rename = "startTime", deserialize_with = "deserialize_time")]
|
|
|
|
pub start_time: chrono::NaiveTime,
|
|
|
|
#[serde(rename = "endTime", deserialize_with = "deserialize_time")]
|
|
|
|
pub end_time: chrono::NaiveTime,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Deserialize, Debug)]
|
|
|
|
pub struct RpcTimegridDay {
|
|
|
|
pub day: u8,
|
|
|
|
#[serde(rename = "timeUnits")]
|
|
|
|
pub time_units: Vec<RpcTimegridDayTimeUnit>,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Deserialize, Debug)]
|
|
|
|
pub struct RpcSchoolyear {
|
|
|
|
pub id: i32,
|
|
|
|
pub name: String,
|
|
|
|
#[serde(rename = "startDate", deserialize_with = "deserialize_date")]
|
|
|
|
pub start_date: chrono::NaiveDate,
|
|
|
|
#[serde(rename = "endDate", deserialize_with = "deserialize_date")]
|
|
|
|
pub end_date: chrono::NaiveDate,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Deserialize, Clone, Copy, Debug)]
|
|
|
|
pub enum RpcSubstitionType {
|
|
|
|
#[serde(rename = "cancel")]
|
|
|
|
Cancel,
|
|
|
|
#[serde(rename = "subst")]
|
|
|
|
Substitution,
|
|
|
|
#[serde(rename = "add")]
|
|
|
|
Additional,
|
|
|
|
#[serde(rename = "shift")]
|
|
|
|
Shifted,
|
|
|
|
#[serde(rename = "rmchg")]
|
|
|
|
RoomChange,
|
|
|
|
#[serde(rename = "rmlk")]
|
|
|
|
Locked,
|
|
|
|
#[serde(rename = "bs")]
|
|
|
|
BreakSupervision,
|
|
|
|
#[serde(rename = "oh")]
|
|
|
|
OfficeHour,
|
|
|
|
#[serde(rename = "sb")]
|
|
|
|
Standby,
|
|
|
|
#[serde(rename = "other")]
|
|
|
|
Other,
|
|
|
|
#[serde(rename = "free")]
|
|
|
|
Free,
|
|
|
|
#[serde(rename = "exam")]
|
|
|
|
Exam,
|
|
|
|
#[serde(rename = "ac")]
|
|
|
|
Activity,
|
|
|
|
#[serde(rename = "holi")]
|
|
|
|
Holiday,
|
|
|
|
#[serde(rename = "stxt")]
|
|
|
|
Text,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Deserialize, Debug)]
|
|
|
|
pub struct RpcSubstitutionId {
|
|
|
|
pub id: i32,
|
|
|
|
pub name: Option<String>,
|
|
|
|
#[serde(rename = "externalkey")]
|
|
|
|
pub external_key: Option<String>,
|
|
|
|
#[serde(rename = "orgid")]
|
|
|
|
pub original_id: Option<i32>,
|
|
|
|
#[serde(rename = "orgname")]
|
|
|
|
pub original_name: Option<String>,
|
|
|
|
#[serde(rename = "orgexternalkey")]
|
|
|
|
pub original_external_key: Option<String>,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Deserialize, Debug)]
|
|
|
|
pub struct RpcSubstitutionReschedule {
|
|
|
|
#[serde(deserialize_with = "deserialize_date")]
|
|
|
|
pub date: chrono::NaiveDate,
|
|
|
|
#[serde(rename = "startTime", deserialize_with = "deserialize_time")]
|
|
|
|
pub start_time: chrono::NaiveTime,
|
|
|
|
#[serde(rename = "endTime", deserialize_with = "deserialize_time")]
|
|
|
|
pub end_time: chrono::NaiveTime,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Deserialize, Debug)]
|
|
|
|
pub struct RpcSubstitution {
|
|
|
|
#[serde(rename = "type")]
|
|
|
|
pub subst_type: RpcSubstitionType,
|
|
|
|
#[serde(rename = "lsid")]
|
|
|
|
pub lesson_id: i32,
|
|
|
|
#[serde(rename = "startTime", deserialize_with = "deserialize_time")]
|
|
|
|
pub start_time: chrono::NaiveTime,
|
|
|
|
#[serde(rename = "endTime", deserialize_with = "deserialize_time")]
|
|
|
|
pub end_time: chrono::NaiveTime,
|
|
|
|
#[serde(rename = "txt")]
|
|
|
|
pub text: Option<String>,
|
|
|
|
#[serde(rename = "kl")]
|
|
|
|
pub classes: Vec<RpcSubstitutionId>,
|
|
|
|
#[serde(rename = "te")]
|
|
|
|
pub teachers: Vec<RpcSubstitutionId>,
|
|
|
|
#[serde(rename = "su")]
|
|
|
|
pub subjects: Vec<RpcSubstitutionId>,
|
|
|
|
#[serde(rename = "ro")]
|
|
|
|
pub rooms: Vec<RpcSubstitutionId>,
|
|
|
|
pub reschedule: Option<RpcSubstitutionReschedule>,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Deserialize, Debug)]
|
|
|
|
pub struct ApiTenant {
|
|
|
|
pub id: i32,
|
|
|
|
#[serde(rename = "displayName")]
|
|
|
|
pub display_name: String,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Deserialize, Debug)]
|
|
|
|
struct ApiDataResponse {
|
|
|
|
tenant: ApiTenant,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Deserialize, Debug)]
|
|
|
|
pub struct ApiTeacher {
|
|
|
|
pub id: i32,
|
|
|
|
pub name: String,
|
|
|
|
pub forename: String,
|
|
|
|
#[serde(rename = "longName")]
|
|
|
|
pub long_name: String,
|
|
|
|
#[serde(rename = "displayname")]
|
|
|
|
pub display_name: String,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Deserialize, Debug)]
|
|
|
|
struct ApiTeachersResponseData {
|
|
|
|
elements: Vec<ApiTeacher>,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Deserialize, Debug)]
|
|
|
|
struct ApiTeachersResponse {
|
|
|
|
data: ApiTeachersResponseData,
|
|
|
|
}
|
|
|
|
|
2023-01-27 18:28:34 +00:00
|
|
|
#[derive(Deserialize, Debug)]
|
2023-01-27 21:12:06 +00:00
|
|
|
pub struct ApiReportUser {
|
2023-01-27 18:28:34 +00:00
|
|
|
pub name: String,
|
|
|
|
#[serde(rename = "longName")]
|
|
|
|
pub long_name: String,
|
|
|
|
#[serde(rename = "foreName")]
|
|
|
|
pub fore_name: String,
|
|
|
|
}
|
|
|
|
|
2022-12-14 14:29:10 +00:00
|
|
|
#[derive(Debug)]
|
|
|
|
pub struct Client {
|
2023-01-27 17:13:53 +00:00
|
|
|
pub webuntis_url: Url,
|
|
|
|
pub rpc_url: Url,
|
2022-12-14 14:29:10 +00:00
|
|
|
pub client_name: String,
|
2022-12-20 17:44:55 +00:00
|
|
|
pub user_agent: String,
|
2022-12-14 14:29:10 +00:00
|
|
|
pub username: String,
|
|
|
|
pub password: String,
|
|
|
|
pub session: Option<String>,
|
|
|
|
pub authorization: Option<String>,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Client {
|
2023-01-27 17:13:53 +00:00
|
|
|
pub fn gen_rpc_url(endpoint: &Url, school: &str) -> Result<Url> {
|
2023-01-27 17:05:00 +00:00
|
|
|
let mut x = endpoint.join("jsonrpc.do")?;
|
|
|
|
x.query_pairs_mut().append_pair("school", school);
|
2022-12-14 14:29:10 +00:00
|
|
|
|
2023-01-27 17:05:00 +00:00
|
|
|
Ok(x)
|
2022-12-14 14:29:10 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
pub async fn login_rpc(&mut self) -> Result<RpcLogin> {
|
|
|
|
let resp: RpcResponse<RpcLogin> = reqwest::Client::new()
|
|
|
|
.get(self.rpc_url.as_str())
|
2022-12-20 17:44:55 +00:00
|
|
|
.header(reqwest::header::USER_AGENT, &self.user_agent)
|
2022-12-14 14:29:10 +00:00
|
|
|
.header(reqwest::header::CONTENT_TYPE, "application/json")
|
|
|
|
.header(reqwest::header::ACCEPT, "application/json")
|
|
|
|
.json(&json!({
|
|
|
|
"id": "ID",
|
|
|
|
"method": "authenticate",
|
|
|
|
"params": {
|
|
|
|
"user": self.username,
|
|
|
|
"password": self.password,
|
|
|
|
"client": self.client_name,
|
|
|
|
},
|
|
|
|
"jsonrpc": "2.0"
|
|
|
|
}))
|
|
|
|
.send()
|
|
|
|
.await?
|
|
|
|
.json()
|
|
|
|
.await?;
|
|
|
|
if let Some(e) = resp.error {
|
|
|
|
bail!("RPC error: {:?}", e);
|
|
|
|
}
|
|
|
|
|
|
|
|
self.session = Some(
|
|
|
|
Cookie::new(
|
|
|
|
"JSESSIONID",
|
|
|
|
&resp
|
|
|
|
.result
|
|
|
|
.as_ref()
|
|
|
|
.context("Result null event though error not")?
|
|
|
|
.session_id,
|
|
|
|
)
|
|
|
|
.to_string(),
|
|
|
|
);
|
|
|
|
|
|
|
|
if let Some(x) = resp.result {
|
|
|
|
Ok(x)
|
|
|
|
} else {
|
|
|
|
bail!("RPC result is null");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub async fn login_api(&mut self) -> Result<String> {
|
2022-12-19 20:00:56 +00:00
|
|
|
let jwt = reqwest::Client::new()
|
2023-01-27 17:05:00 +00:00
|
|
|
.get(self.webuntis_url.join("api/token/new")?)
|
2022-12-20 17:44:55 +00:00
|
|
|
.header(reqwest::header::USER_AGENT, &self.user_agent)
|
2022-12-19 20:00:56 +00:00
|
|
|
.header(
|
|
|
|
reqwest::header::COOKIE,
|
|
|
|
self.session.as_ref().context("Not logged in")?,
|
|
|
|
)
|
|
|
|
.send()
|
|
|
|
.await?
|
|
|
|
.text()
|
|
|
|
.await?;
|
|
|
|
self.authorization = Some(jwt.to_string());
|
|
|
|
|
|
|
|
Ok(jwt)
|
2022-12-14 14:29:10 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
pub async fn login(&mut self) -> Result<()> {
|
|
|
|
self.login_rpc().await?;
|
|
|
|
self.login_api().await?;
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
pub async fn logout(&mut self) -> Result<()> {
|
|
|
|
let resp: RpcResponse<RpcEmptyResult> = reqwest::Client::new()
|
|
|
|
.get(self.rpc_url.as_str())
|
2022-12-20 17:44:55 +00:00
|
|
|
.header(reqwest::header::USER_AGENT, &self.user_agent)
|
2022-12-14 14:29:10 +00:00
|
|
|
.header(reqwest::header::CONTENT_TYPE, "application/json")
|
|
|
|
.header(reqwest::header::ACCEPT, "application/json")
|
|
|
|
.header(
|
|
|
|
reqwest::header::COOKIE,
|
|
|
|
self.session.as_ref().context("Not logged in")?,
|
|
|
|
)
|
|
|
|
.json(&json!({
|
|
|
|
"id": "ID",
|
|
|
|
"method": "logout",
|
|
|
|
"params": {},
|
|
|
|
"jsonrpc": "2.0"
|
|
|
|
}))
|
|
|
|
.send()
|
|
|
|
.await?
|
|
|
|
.json()
|
|
|
|
.await?;
|
|
|
|
self.session = None;
|
|
|
|
if let Some(e) = resp.error {
|
|
|
|
bail!("RPC error: {:?}", e);
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
pub async fn classes(&self) -> Result<Vec<RpcClass>> {
|
|
|
|
let resp: RpcResponse<Vec<RpcClass>> = reqwest::Client::new()
|
|
|
|
.get(self.rpc_url.as_str())
|
2022-12-20 17:44:55 +00:00
|
|
|
.header(reqwest::header::USER_AGENT, &self.user_agent)
|
2022-12-14 14:29:10 +00:00
|
|
|
.header(reqwest::header::CONTENT_TYPE, "application/json")
|
|
|
|
.header(reqwest::header::ACCEPT, "application/json")
|
|
|
|
.header(
|
|
|
|
reqwest::header::COOKIE,
|
|
|
|
self.session.as_ref().context("Not logged in")?,
|
|
|
|
)
|
|
|
|
.json(&json!({
|
|
|
|
"id": "ID",
|
|
|
|
"method": "getKlassen",
|
|
|
|
"params": {},
|
|
|
|
"jsonrpc": "2.0"
|
|
|
|
}))
|
|
|
|
.send()
|
|
|
|
.await?
|
|
|
|
.json()
|
|
|
|
.await?;
|
|
|
|
if let Some(e) = resp.error {
|
|
|
|
bail!("RPC error: {:?}", e);
|
|
|
|
}
|
|
|
|
|
|
|
|
if let Some(x) = resp.result {
|
|
|
|
Ok(x)
|
|
|
|
} else {
|
|
|
|
bail!("RPC result is null");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub async fn subjects(&self) -> Result<Vec<RpcSubject>> {
|
|
|
|
let resp: RpcResponse<Vec<RpcSubject>> = reqwest::Client::new()
|
|
|
|
.get(self.rpc_url.as_str())
|
2022-12-20 17:44:55 +00:00
|
|
|
.header(reqwest::header::USER_AGENT, &self.user_agent)
|
2022-12-14 14:29:10 +00:00
|
|
|
.header(reqwest::header::CONTENT_TYPE, "application/json")
|
|
|
|
.header(reqwest::header::ACCEPT, "application/json")
|
|
|
|
.header(
|
|
|
|
reqwest::header::COOKIE,
|
|
|
|
self.session.as_ref().context("Not logged in")?,
|
|
|
|
)
|
|
|
|
.json(&json!({
|
|
|
|
"id": "ID",
|
|
|
|
"method": "getSubjects",
|
|
|
|
"params": {},
|
|
|
|
"jsonrpc": "2.0"
|
|
|
|
}))
|
|
|
|
.send()
|
|
|
|
.await?
|
|
|
|
.json()
|
|
|
|
.await?;
|
|
|
|
if let Some(e) = resp.error {
|
|
|
|
bail!("RPC error: {:?}", e);
|
|
|
|
}
|
|
|
|
|
|
|
|
if let Some(x) = resp.result {
|
|
|
|
Ok(x)
|
|
|
|
} else {
|
|
|
|
bail!("RPC result is null");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub async fn rooms(&self) -> Result<Vec<RpcRoom>> {
|
|
|
|
let resp: RpcResponse<Vec<RpcRoom>> = reqwest::Client::new()
|
|
|
|
.get(self.rpc_url.as_str())
|
2022-12-20 17:44:55 +00:00
|
|
|
.header(reqwest::header::USER_AGENT, &self.user_agent)
|
2022-12-14 14:29:10 +00:00
|
|
|
.header(reqwest::header::CONTENT_TYPE, "application/json")
|
|
|
|
.header(reqwest::header::ACCEPT, "application/json")
|
|
|
|
.header(
|
|
|
|
reqwest::header::COOKIE,
|
|
|
|
self.session.as_ref().context("Not logged in")?,
|
|
|
|
)
|
|
|
|
.json(&json!({
|
|
|
|
"id": "ID",
|
|
|
|
"method": "getRooms",
|
|
|
|
"params": {},
|
|
|
|
"jsonrpc": "2.0"
|
|
|
|
}))
|
|
|
|
.send()
|
|
|
|
.await?
|
|
|
|
.json()
|
|
|
|
.await?;
|
|
|
|
if let Some(e) = resp.error {
|
|
|
|
bail!("RPC error: {:?}", e);
|
|
|
|
}
|
|
|
|
|
|
|
|
if let Some(x) = resp.result {
|
|
|
|
Ok(x)
|
|
|
|
} else {
|
|
|
|
bail!("RPC result is null");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub async fn departments(&self) -> Result<Vec<RpcDepartment>> {
|
|
|
|
let resp: RpcResponse<Vec<RpcDepartment>> = reqwest::Client::new()
|
|
|
|
.get(self.rpc_url.as_str())
|
2022-12-20 17:44:55 +00:00
|
|
|
.header(reqwest::header::USER_AGENT, &self.user_agent)
|
2022-12-14 14:29:10 +00:00
|
|
|
.header(reqwest::header::CONTENT_TYPE, "application/json")
|
|
|
|
.header(reqwest::header::ACCEPT, "application/json")
|
|
|
|
.header(
|
|
|
|
reqwest::header::COOKIE,
|
|
|
|
self.session.as_ref().context("Not logged in")?,
|
|
|
|
)
|
|
|
|
.json(&json!({
|
|
|
|
"id": "ID",
|
|
|
|
"method": "getDepartments",
|
|
|
|
"params": {},
|
|
|
|
"jsonrpc": "2.0"
|
|
|
|
}))
|
|
|
|
.send()
|
|
|
|
.await?
|
|
|
|
.json()
|
|
|
|
.await?;
|
|
|
|
if let Some(e) = resp.error {
|
|
|
|
bail!("RPC error: {:?}", e);
|
|
|
|
}
|
|
|
|
|
|
|
|
if let Some(x) = resp.result {
|
|
|
|
Ok(x)
|
|
|
|
} else {
|
|
|
|
bail!("RPC result is null");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub async fn holidays(&self) -> Result<Vec<RpcHoliday>> {
|
|
|
|
let resp: RpcResponse<Vec<RpcHoliday>> = reqwest::Client::new()
|
|
|
|
.get(self.rpc_url.as_str())
|
2022-12-20 17:44:55 +00:00
|
|
|
.header(reqwest::header::USER_AGENT, &self.user_agent)
|
2022-12-14 14:29:10 +00:00
|
|
|
.header(reqwest::header::CONTENT_TYPE, "application/json")
|
|
|
|
.header(reqwest::header::ACCEPT, "application/json")
|
|
|
|
.header(
|
|
|
|
reqwest::header::COOKIE,
|
|
|
|
self.session.as_ref().context("Not logged in")?,
|
|
|
|
)
|
|
|
|
.json(&json!({
|
|
|
|
"id": "ID",
|
|
|
|
"method": "getHolidays",
|
|
|
|
"params": {},
|
|
|
|
"jsonrpc": "2.0"
|
|
|
|
}))
|
|
|
|
.send()
|
|
|
|
.await?
|
|
|
|
.json()
|
|
|
|
.await?;
|
|
|
|
if let Some(e) = resp.error {
|
|
|
|
bail!("RPC error: {:?}", e);
|
|
|
|
}
|
|
|
|
|
|
|
|
if let Some(x) = resp.result {
|
|
|
|
Ok(x)
|
|
|
|
} else {
|
|
|
|
bail!("RPC result is null");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub async fn timegrid(&self) -> Result<Vec<RpcTimegridDay>> {
|
|
|
|
let resp: RpcResponse<Vec<RpcTimegridDay>> = reqwest::Client::new()
|
|
|
|
.get(self.rpc_url.as_str())
|
2022-12-20 17:44:55 +00:00
|
|
|
.header(reqwest::header::USER_AGENT, &self.user_agent)
|
2022-12-14 14:29:10 +00:00
|
|
|
.header(reqwest::header::CONTENT_TYPE, "application/json")
|
|
|
|
.header(reqwest::header::ACCEPT, "application/json")
|
|
|
|
.header(
|
|
|
|
reqwest::header::COOKIE,
|
|
|
|
self.session.as_ref().context("Not logged in")?,
|
|
|
|
)
|
|
|
|
.json(&json!({
|
|
|
|
"id": "ID",
|
|
|
|
"method": "getTimegridUnits",
|
|
|
|
"params": {},
|
|
|
|
"jsonrpc": "2.0"
|
|
|
|
}))
|
|
|
|
.send()
|
|
|
|
.await?
|
|
|
|
.json()
|
|
|
|
.await?;
|
|
|
|
if let Some(e) = resp.error {
|
|
|
|
bail!("RPC error: {:?}", e);
|
|
|
|
}
|
|
|
|
|
|
|
|
if let Some(x) = resp.result {
|
|
|
|
Ok(x)
|
|
|
|
} else {
|
|
|
|
bail!("RPC result is null");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub async fn current_schoolyear(&self) -> Result<RpcSchoolyear> {
|
|
|
|
let resp: RpcResponse<RpcSchoolyear> = reqwest::Client::new()
|
|
|
|
.get(self.rpc_url.as_str())
|
2022-12-20 17:44:55 +00:00
|
|
|
.header(reqwest::header::USER_AGENT, &self.user_agent)
|
2022-12-14 14:29:10 +00:00
|
|
|
.header(reqwest::header::CONTENT_TYPE, "application/json")
|
|
|
|
.header(reqwest::header::ACCEPT, "application/json")
|
|
|
|
.header(
|
|
|
|
reqwest::header::COOKIE,
|
|
|
|
self.session.as_ref().context("Not logged in")?,
|
|
|
|
)
|
|
|
|
.json(&json!({
|
|
|
|
"id": "ID",
|
|
|
|
"method": "getCurrentSchoolyear",
|
|
|
|
"params": {},
|
|
|
|
"jsonrpc": "2.0"
|
|
|
|
}))
|
|
|
|
.send()
|
|
|
|
.await?
|
|
|
|
.json()
|
|
|
|
.await?;
|
|
|
|
if let Some(e) = resp.error {
|
|
|
|
bail!("RPC error: {:?}", e);
|
|
|
|
}
|
|
|
|
|
|
|
|
if let Some(x) = resp.result {
|
|
|
|
Ok(x)
|
|
|
|
} else {
|
|
|
|
bail!("RPC result is null");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub async fn schoolyears(&self) -> Result<Vec<RpcSchoolyear>> {
|
|
|
|
let resp: RpcResponse<Vec<RpcSchoolyear>> = reqwest::Client::new()
|
|
|
|
.get(self.rpc_url.as_str())
|
2022-12-20 17:44:55 +00:00
|
|
|
.header(reqwest::header::USER_AGENT, &self.user_agent)
|
2022-12-14 14:29:10 +00:00
|
|
|
.header(reqwest::header::CONTENT_TYPE, "application/json")
|
|
|
|
.header(reqwest::header::ACCEPT, "application/json")
|
|
|
|
.header(
|
|
|
|
reqwest::header::COOKIE,
|
|
|
|
self.session.as_ref().context("Not logged in")?,
|
|
|
|
)
|
|
|
|
.json(&json!({
|
|
|
|
"id": "ID",
|
|
|
|
"method": "getSchoolyears",
|
|
|
|
"params": {},
|
|
|
|
"jsonrpc": "2.0"
|
|
|
|
}))
|
|
|
|
.send()
|
|
|
|
.await?
|
|
|
|
.json()
|
|
|
|
.await?;
|
|
|
|
if let Some(e) = resp.error {
|
|
|
|
bail!("RPC error: {:?}", e);
|
|
|
|
}
|
|
|
|
|
|
|
|
if let Some(x) = resp.result {
|
|
|
|
Ok(x)
|
|
|
|
} else {
|
|
|
|
bail!("RPC result is null");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub async fn substitutions(
|
|
|
|
&self,
|
|
|
|
from: &chrono::NaiveDate,
|
|
|
|
to: &chrono::NaiveDate,
|
|
|
|
department: Option<i32>,
|
|
|
|
) -> Result<Vec<RpcSubstitution>> {
|
|
|
|
let resp: RpcResponse<Vec<RpcSubstitution>> = reqwest::Client::new()
|
|
|
|
.get(self.rpc_url.as_str())
|
2022-12-20 17:44:55 +00:00
|
|
|
.header(reqwest::header::USER_AGENT, &self.user_agent)
|
2022-12-14 14:29:10 +00:00
|
|
|
.header(reqwest::header::CONTENT_TYPE, "application/json")
|
|
|
|
.header(reqwest::header::ACCEPT, "application/json")
|
|
|
|
.header(
|
|
|
|
reqwest::header::COOKIE,
|
|
|
|
self.session.as_ref().context("Not logged in")?,
|
|
|
|
)
|
|
|
|
.json(&json!({
|
|
|
|
"id": "ID",
|
|
|
|
"method": "getSubstitutions",
|
|
|
|
"params": {
|
|
|
|
"startDate": serialize_date(*from),
|
|
|
|
"endDate": serialize_date(*to),
|
|
|
|
"departmentId": department.map_or(0, |d| d)
|
|
|
|
},
|
|
|
|
"jsonrpc": "2.0"
|
|
|
|
}))
|
|
|
|
.send()
|
|
|
|
.await?
|
|
|
|
.json()
|
|
|
|
.await?;
|
|
|
|
if let Some(e) = resp.error {
|
|
|
|
bail!("RPC error: {:?}", e);
|
|
|
|
}
|
|
|
|
|
|
|
|
if let Some(x) = resp.result {
|
|
|
|
Ok(x)
|
|
|
|
} else {
|
|
|
|
bail!("RPC result is null");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn construct_bearer(auth: &str) -> String {
|
|
|
|
format!("Bearer {}", auth)
|
|
|
|
}
|
|
|
|
|
|
|
|
pub async fn current_tenant(&self) -> Result<ApiTenant> {
|
|
|
|
let resp: ApiDataResponse = reqwest::Client::new()
|
2023-01-27 17:05:00 +00:00
|
|
|
.get(self.webuntis_url.join("api/rest/view/v1/app/data")?)
|
2022-12-20 17:44:55 +00:00
|
|
|
.header(reqwest::header::USER_AGENT, &self.user_agent)
|
2022-12-14 14:29:10 +00:00
|
|
|
.header(reqwest::header::ACCEPT, "application/json")
|
|
|
|
.header(
|
|
|
|
reqwest::header::COOKIE,
|
|
|
|
self.session.as_ref().context("Not logged in")?,
|
|
|
|
)
|
|
|
|
.header(
|
|
|
|
reqwest::header::AUTHORIZATION,
|
|
|
|
Client::construct_bearer(&self.authorization.as_ref().context("Not logged in")?),
|
|
|
|
)
|
|
|
|
.send()
|
|
|
|
.await?
|
|
|
|
.json()
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
Ok(resp.tenant)
|
|
|
|
}
|
|
|
|
|
|
|
|
pub async fn teachers(&self) -> Result<Vec<ApiTeacher>> {
|
2023-01-27 17:05:00 +00:00
|
|
|
let mut url = self
|
|
|
|
.webuntis_url
|
|
|
|
.join("api/public/timetable/weekly/pageconfig")?;
|
2022-12-14 14:29:10 +00:00
|
|
|
url.query_pairs_mut().append_pair("type", "2");
|
|
|
|
let resp: ApiTeachersResponse = reqwest::Client::new()
|
|
|
|
.get(url)
|
2022-12-20 17:44:55 +00:00
|
|
|
.header(reqwest::header::USER_AGENT, &self.user_agent)
|
2022-12-14 14:29:10 +00:00
|
|
|
.header(reqwest::header::ACCEPT, "application/json")
|
|
|
|
.header(
|
|
|
|
reqwest::header::COOKIE,
|
|
|
|
self.session.as_ref().context("Not logged in")?,
|
|
|
|
)
|
|
|
|
.header(
|
|
|
|
reqwest::header::AUTHORIZATION,
|
|
|
|
Client::construct_bearer(&self.authorization.as_ref().context("Not logged in")?),
|
|
|
|
)
|
|
|
|
.send()
|
|
|
|
.await?
|
|
|
|
.json()
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
Ok(resp.data.elements)
|
|
|
|
}
|
2023-01-27 17:24:11 +00:00
|
|
|
|
2023-01-27 21:12:06 +00:00
|
|
|
pub async fn student_reports(&self, class_id: i32) -> Result<Vec<ApiReportUser>> {
|
2023-01-27 17:31:44 +00:00
|
|
|
#[derive(Deserialize, Debug)]
|
|
|
|
struct Data {
|
|
|
|
finished: bool,
|
|
|
|
error: bool,
|
2023-01-27 17:35:43 +00:00
|
|
|
#[serde(rename = "messageId")]
|
|
|
|
message_id: i32,
|
2023-01-27 17:31:44 +00:00
|
|
|
#[serde(rename = "reportParams")]
|
|
|
|
report_params: String,
|
|
|
|
}
|
|
|
|
|
2023-01-27 17:45:43 +00:00
|
|
|
#[derive(Deserialize, Debug)]
|
|
|
|
struct Response {
|
|
|
|
data: Data,
|
|
|
|
}
|
|
|
|
|
2023-01-27 17:24:11 +00:00
|
|
|
let mut url = self.webuntis_url.join("reports.do")?;
|
|
|
|
url.query_pairs_mut()
|
|
|
|
.append_pair("name", "Student")
|
|
|
|
.append_pair("format", "csv")
|
|
|
|
.append_pair("klasseId", &class_id.to_string())
|
|
|
|
.append_pair("studentsForDate", "true");
|
2023-01-27 17:45:43 +00:00
|
|
|
let resp: Response = reqwest::Client::new()
|
2023-01-27 17:24:11 +00:00
|
|
|
.get(url)
|
|
|
|
.header(reqwest::header::USER_AGENT, &self.user_agent)
|
|
|
|
.header(reqwest::header::ACCEPT, "application/json")
|
|
|
|
.header(
|
|
|
|
reqwest::header::COOKIE,
|
|
|
|
self.session.as_ref().context("Not logged in")?,
|
|
|
|
)
|
|
|
|
.header(
|
|
|
|
reqwest::header::AUTHORIZATION,
|
|
|
|
Client::construct_bearer(&self.authorization.as_ref().context("Not logged in")?),
|
|
|
|
)
|
|
|
|
.send()
|
2023-01-27 17:31:44 +00:00
|
|
|
.await?
|
|
|
|
.json()
|
2023-01-27 17:24:11 +00:00
|
|
|
.await?;
|
2023-01-27 17:45:43 +00:00
|
|
|
if resp.data.error {
|
2023-01-27 17:35:43 +00:00
|
|
|
bail!("Error generating report");
|
2023-01-27 17:45:43 +00:00
|
|
|
} else if !resp.data.finished {
|
2023-01-27 17:35:43 +00:00
|
|
|
bail!("Report not finished");
|
|
|
|
}
|
2023-01-27 17:24:11 +00:00
|
|
|
|
2023-01-27 18:11:00 +00:00
|
|
|
let mut records_url = self.webuntis_url.join("reports.do")?;
|
|
|
|
records_url.set_query(Some(&resp.data.report_params));
|
2023-01-27 17:55:44 +00:00
|
|
|
records_url
|
|
|
|
.query_pairs_mut()
|
|
|
|
.append_pair("msgId", &resp.data.message_id.to_string());
|
|
|
|
let records = reqwest::Client::new()
|
|
|
|
.get(records_url)
|
|
|
|
.header(reqwest::header::USER_AGENT, &self.user_agent)
|
|
|
|
.header(reqwest::header::ACCEPT, "text/csv")
|
|
|
|
.header(
|
|
|
|
reqwest::header::COOKIE,
|
|
|
|
self.session.as_ref().context("Not logged in")?,
|
|
|
|
)
|
|
|
|
.header(
|
|
|
|
reqwest::header::AUTHORIZATION,
|
|
|
|
Client::construct_bearer(&self.authorization.as_ref().context("Not logged in")?),
|
|
|
|
)
|
|
|
|
.send()
|
|
|
|
.await?
|
|
|
|
.text()
|
|
|
|
.await?;
|
2023-01-27 18:11:00 +00:00
|
|
|
let mut reader = csv::ReaderBuilder::new()
|
|
|
|
.delimiter(b'\t')
|
|
|
|
.from_reader(records.as_bytes());
|
2023-01-27 17:55:44 +00:00
|
|
|
|
2023-01-27 18:28:34 +00:00
|
|
|
Ok(reader
|
2023-01-27 21:12:06 +00:00
|
|
|
.deserialize::<ApiReportUser>()
|
|
|
|
.collect::<Result<Vec<_>, _>>()?)
|
|
|
|
}
|
|
|
|
|
2023-01-27 21:12:28 +00:00
|
|
|
pub async fn teacher_reports(&self) -> Result<Vec<ApiReportUser>> {
|
2023-01-27 21:12:06 +00:00
|
|
|
#[derive(Deserialize, Debug)]
|
|
|
|
struct Data {
|
|
|
|
finished: bool,
|
|
|
|
error: bool,
|
|
|
|
#[serde(rename = "messageId")]
|
|
|
|
message_id: i32,
|
|
|
|
#[serde(rename = "reportParams")]
|
|
|
|
report_params: String,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Deserialize, Debug)]
|
|
|
|
struct Response {
|
|
|
|
data: Data,
|
|
|
|
}
|
|
|
|
|
|
|
|
let mut url = self.webuntis_url.join("reports.do")?;
|
|
|
|
url.query_pairs_mut()
|
|
|
|
.append_pair("name", "Teacher")
|
|
|
|
.append_pair("format", "csv")
|
|
|
|
.append_pair("elementsForDate", "true");
|
|
|
|
let resp: Response = reqwest::Client::new()
|
|
|
|
.get(url)
|
|
|
|
.header(reqwest::header::USER_AGENT, &self.user_agent)
|
|
|
|
.header(reqwest::header::ACCEPT, "application/json")
|
|
|
|
.header(
|
|
|
|
reqwest::header::COOKIE,
|
|
|
|
self.session.as_ref().context("Not logged in")?,
|
|
|
|
)
|
|
|
|
.header(
|
|
|
|
reqwest::header::AUTHORIZATION,
|
|
|
|
Client::construct_bearer(&self.authorization.as_ref().context("Not logged in")?),
|
|
|
|
)
|
|
|
|
.send()
|
|
|
|
.await?
|
|
|
|
.json()
|
|
|
|
.await?;
|
|
|
|
if resp.data.error {
|
|
|
|
bail!("Error generating report");
|
|
|
|
} else if !resp.data.finished {
|
|
|
|
bail!("Report not finished");
|
|
|
|
}
|
|
|
|
|
|
|
|
let mut records_url = self.webuntis_url.join("reports.do")?;
|
|
|
|
records_url.set_query(Some(&resp.data.report_params));
|
|
|
|
records_url
|
|
|
|
.query_pairs_mut()
|
|
|
|
.append_pair("msgId", &resp.data.message_id.to_string());
|
|
|
|
let records = reqwest::Client::new()
|
|
|
|
.get(records_url)
|
|
|
|
.header(reqwest::header::USER_AGENT, &self.user_agent)
|
|
|
|
.header(reqwest::header::ACCEPT, "text/csv")
|
|
|
|
.header(
|
|
|
|
reqwest::header::COOKIE,
|
|
|
|
self.session.as_ref().context("Not logged in")?,
|
|
|
|
)
|
|
|
|
.header(
|
|
|
|
reqwest::header::AUTHORIZATION,
|
|
|
|
Client::construct_bearer(&self.authorization.as_ref().context("Not logged in")?),
|
|
|
|
)
|
|
|
|
.send()
|
|
|
|
.await?
|
|
|
|
.text()
|
|
|
|
.await?;
|
|
|
|
let mut reader = csv::ReaderBuilder::new()
|
|
|
|
.delimiter(b'\t')
|
|
|
|
.from_reader(records.as_bytes());
|
|
|
|
|
|
|
|
Ok(reader
|
|
|
|
.deserialize::<ApiReportUser>()
|
2023-01-27 18:28:34 +00:00
|
|
|
.collect::<Result<Vec<_>, _>>()?)
|
2023-01-27 17:24:11 +00:00
|
|
|
}
|
2022-12-14 14:29:10 +00:00
|
|
|
}
|