untis.rs/src/lib.rs

927 lines
28 KiB
Rust

use anyhow::{bail, Context, Result};
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
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,
}
#[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 webuntis_url: Url,
pub rpc_url: Url,
pub client_name: String,
pub user_agent: String,
pub username: String,
pub password: String,
pub session: Option<String>,
pub authorization: Option<String>,
}
impl Client {
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);
Ok(x)
}
pub async fn login_rpc(&mut self) -> Result<RpcLogin> {
let resp: RpcResponse<RpcLogin> = 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")
.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> {
let jwt = reqwest::Client::new()
.get(self.webuntis_url.join("api/token/new")?)
.header(reqwest::header::USER_AGENT, &self.user_agent)
.header(
reqwest::header::COOKIE,
self.session.as_ref().context("Not logged in")?,
)
.send()
.await?
.text()
.await?;
self.authorization = Some(jwt.to_string());
Ok(jwt)
}
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())
.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": "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())
.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": "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())
.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": "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())
.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": "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())
.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": "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())
.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": "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())
.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": "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())
.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": "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())
.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": "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())
.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": "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 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.webuntis_url.join("api/rest/view/v1/app/data")?)
.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?;
Ok(resp.tenant)
}
pub async fn teachers(&self) -> Result<Vec<ApiTeacher>> {
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)
.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?;
Ok(resp.data.elements)
}
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<_>, _>>()?)
}
}