Compare commits

...

24 Commits
0.1.0 ... main

Author SHA1 Message Date
Dominic Grimm fdb385c5c6
Fix timestamp for 'getLatestImportTime' 2023-02-26 23:30:41 +01:00
Dominic Grimm 15af617614
Fix auth for 'getLatestImportTime' 2023-02-26 20:53:20 +01:00
Dominic Grimm 92cd3ccf97
Add support for 'getLatestImportTime' 2023-02-26 15:14:04 +01:00
Dominic Grimm e71c2ba4a2
Update 2023-02-24 19:27:11 +01:00
Dominic Grimm f6ecd49565
Update 2023-02-24 19:26:41 +01:00
Dominic Grimm ff11781e11
Update 2023-02-23 20:11:16 +01:00
Dominic Grimm 5f0d931d80
Update 2023-02-23 19:53:11 +01:00
Dominic Grimm 58e23cb089
Update 2023-01-27 22:12:28 +01:00
Dominic Grimm ff24aa7a2f
Update 2023-01-27 22:12:06 +01:00
Dominic Grimm f38d80d030
Update 2023-01-27 20:30:11 +01:00
Dominic Grimm e1515ea207
Update 2023-01-27 19:28:34 +01:00
Dominic Grimm b3b194cb7c
Update 2023-01-27 19:11:00 +01:00
Dominic Grimm 45feb2c5a5
Update 2023-01-27 18:55:44 +01:00
Dominic Grimm e4b5af4d64
Update 2023-01-27 18:45:43 +01:00
Dominic Grimm 25d59d5b87
Update 2023-01-27 18:35:43 +01:00
Dominic Grimm 80470c2fc9
Update 2023-01-27 18:31:44 +01:00
Dominic Grimm 171df04dad
Update 2023-01-27 18:24:11 +01:00
Dominic Grimm 15dfc5982c
Update 2023-01-27 18:13:53 +01:00
Dominic Grimm 51c09d0e5a
Update 2023-01-27 18:05:00 +01:00
Dominic Grimm 21009ada78
Update 2023-01-27 13:19:39 +01:00
Dominic Grimm 0ad1739d4f
Update 2023-01-27 12:57:03 +01:00
Dominic Grimm 00cabc308d
Update 2023-01-27 12:45:39 +01:00
Dominic Grimm 5d5724cac1
Update 2023-01-27 12:41:33 +01:00
Dominic Grimm d6b28aa323
Update 2023-01-27 12:33:38 +01:00
3 changed files with 199 additions and 11 deletions

Binary file not shown.

View File

@ -1,6 +1,6 @@
[package]
name = "untis"
version = "0.1.0"
version = "0.1.1"
description = "Rust client for WebUntis JSON-RPC and REST API"
authors = ["Dominic Grimm <dominic@dergrimm.net>"]
edition = "2021"
@ -15,6 +15,7 @@ publish = false
anyhow = { version = "1.0.66", features = ["backtrace"] }
chrono = { version = "0.4.23", features = ["serde"] }
cookie = "0.16.1"
csv = "1.1.6"
reqwest = { version = "0.11.13", features = ["json"] }
serde = { version = "1.0.150", features = ["derive"] }
serde_json = "1.0.89"

View File

@ -3,6 +3,7 @@ use chrono::Datelike;
use cookie::Cookie;
use serde::Deserialize;
use serde_json::json;
use url::Url;
fn deserialize_date<'de, D>(deserializer: D) -> Result<chrono::NaiveDate, D::Error>
where
@ -283,10 +284,19 @@ struct ApiTeachersResponse {
data: ApiTeachersResponseData,
}
#[derive(Deserialize, Debug)]
pub struct ApiReportUser {
pub name: String,
#[serde(rename = "longName")]
pub long_name: String,
#[serde(rename = "foreName")]
pub fore_name: String,
}
#[derive(Debug)]
pub struct Client {
pub api_url: url::Url,
pub rpc_url: url::Url,
pub webuntis_url: Url,
pub rpc_url: Url,
pub client_name: String,
pub user_agent: String,
pub username: String,
@ -296,10 +306,11 @@ pub struct Client {
}
impl Client {
pub fn gen_rpc_url(mut endpoint: url::Url, school: &str) -> url::Url {
endpoint.query_pairs_mut().append_pair("school", school);
pub fn gen_rpc_url(endpoint: &Url, school: &str) -> Result<Url> {
let mut x = endpoint.join("jsonrpc.do")?;
x.query_pairs_mut().append_pair("school", school);
endpoint
Ok(x)
}
pub async fn login_rpc(&mut self) -> Result<RpcLogin> {
@ -347,7 +358,7 @@ impl Client {
pub async fn login_api(&mut self) -> Result<String> {
let jwt = reqwest::Client::new()
.get(self.api_url.join("token/new")?)
.get(self.webuntis_url.join("api/token/new")?)
.header(reqwest::header::USER_AGENT, &self.user_agent)
.header(
reqwest::header::COOKIE,
@ -685,13 +696,44 @@ impl Client {
}
}
pub async fn last_import_time(&self) -> Result<chrono::NaiveDateTime> {
let resp: RpcResponse<i64> = reqwest::Client::new()
.get(self.rpc_url.as_str())
.header(reqwest::header::USER_AGENT, &self.user_agent)
.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": "getLatestImportTime",
"jsonrpc": "2.0"
}))
.send()
.await?
.json()
.await?;
if let Some(e) = resp.error {
bail!("RPC error: {:?}", e);
}
if let Some(x) = resp.result {
Ok(chrono::NaiveDateTime::from_timestamp_millis(x)
.context("Could not convert Unix timestamp to NaiveDateTime")?)
} 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()
.get(self.api_url.join("rest/view/v1/app/data")?)
.get(self.webuntis_url.join("api/rest/view/v1/app/data")?)
.header(reqwest::header::USER_AGENT, &self.user_agent)
.header(reqwest::header::ACCEPT, "application/json")
.header(
@ -711,7 +753,9 @@ impl Client {
}
pub async fn teachers(&self) -> Result<Vec<ApiTeacher>> {
let mut url = self.api_url.join("public/timetable/weekly/pageconfig")?;
let mut url = self
.webuntis_url
.join("api/public/timetable/weekly/pageconfig")?;
url.query_pairs_mut().append_pair("type", "2");
let resp: ApiTeachersResponse = reqwest::Client::new()
.get(url)
@ -733,7 +777,150 @@ impl Client {
Ok(resp.data.elements)
}
pub async fn exams(&self) -> Result<()> {
Ok(())
pub async fn student_reports(&self, class_id: i32) -> Result<Vec<ApiReportUser>> {
#[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", "Student")
.append_pair("format", "csv")
.append_pair("klasseId", &class_id.to_string())
.append_pair("studentsForDate", "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>()
.collect::<Result<Vec<_>, _>>()?)
}
pub async fn teacher_reports(&self) -> Result<Vec<ApiReportUser>> {
#[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>()
.collect::<Result<Vec<_>, _>>()?)
}
}