// SPDX-FileCopyrightText: 2022 Matteo Settenvini // SPDX-License-Identifier: AGPL-3.0-or-later use { crate::config::Config, crate::constants, anyhow::anyhow, anyhow::Result, base64::write::EncoderWriter as Base64Encoder, chrono::DateTime, icalendar::CalendarComponent, reqwest::Url, std::io::Write, std::ops::Range, std::sync::Arc, tokio::sync::Semaphore, }; pub struct ApiClient { rest: reqwest::Client, rest_semaphore: Arc, base_url: Url, caldav_base_url: Url, webdav_base_url: Url, } impl ApiClient { pub fn new(server_name: &str, configuration: &Config) -> Result { let server = configuration .credentials .servers .get(server_name) .ok_or_else(|| { anyhow!( "Unknown server {}. Did you use '{} init' first? Known servers: {:#?}", server_name, env!("CARGO_BIN_NAME"), configuration.credentials.servers.keys().collect::>() ) })?; let username = server.login_name.clone(); use reqwest::header; let mut default_headers = header::HeaderMap::new(); let mut auth_header = b"Basic ".to_vec(); { let mut encoder = Base64Encoder::new(&mut auth_header, base64::STANDARD); write!(encoder, "{}:{}", server.login_name, server.password).unwrap(); } let mut auth_header = header::HeaderValue::from_bytes(&auth_header)?; auth_header.set_sensitive(true); default_headers.insert(header::AUTHORIZATION, auth_header); let rest_client = reqwest::Client::builder() .user_agent(constants::USER_AGENT) .default_headers(default_headers) .build()?; let base_url = Url::parse(&server.url)?; let caldav_base_url = futures::executor::block_on( rest_client .head(base_url.join("/.well-known/caldav")?) .send(), )? .url() .join(&format!("calendars/{}/", &username))?; let webdav_base_url = base_url.join(&format!("remote.php/dav/files/{}/", username))?; Ok(ApiClient { base_url, caldav_base_url, webdav_base_url, rest: rest_client, rest_semaphore: Arc::new(Semaphore::new(5)), }) } pub async fn rest<'a, F, R>(&'a self, submitter: F) -> R::Output where F: Fn(&'a reqwest::Client) -> R, R: std::future::Future>, { let _ = self.rest_semaphore.acquire().await.unwrap(); let mut retries = 0; loop { let result: R::Output = submitter(&self.rest).await; match result { Ok(response) => break Ok(response), Err(_) if retries < 10 => { retries += 1; std::thread::sleep(retries * std::time::Duration::from_millis(500)); continue; } err => break err, } } } pub fn base_url(&self) -> &Url { &self.base_url } pub fn caldav_base_url(&self) -> &Url { &self.caldav_base_url } pub async fn get_events( &self, calendar_name: &str, date_range: Range>, ) -> Result> where Tz: chrono::TimeZone, Tz::Offset: std::fmt::Display, { let report_method = reqwest::Method::from_bytes(b"REPORT").unwrap(); let events_xml = self .rest(|client| async { let response = client .request( report_method.clone(), self.caldav_base_url.join(&format!( "{}/", calendar_name.to_lowercase().replace(" ", "-") ))?, ) .header("Prefer", "return-minimal") .header("Content-Type", "application/xml; charset=utf-8") .header("Depth", 1) .body(format!( " {} ", constants::CALENDAR_PROVIDER, date_range .start .naive_utc() .format(constants::ICAL_UTCTIME_FMT) .to_string(), date_range .end .naive_utc() .format(constants::ICAL_UTCTIME_FMT) .to_string() )) .send() .await; Ok(response?) }) .await? .text() .await?; let xml_doc = libxml::parser::Parser::default().parse_string(events_xml.as_bytes())?; let mut xpath_ctx = libxml::xpath::Context::new(&xml_doc).unwrap(); xpath_ctx .register_namespace("cal", "urn:ietf:params:xml:ns:caldav") .unwrap(); let calendars_txt = xpath_ctx .findnodes("//cal:calendar-data/text()", None) .unwrap() .into_iter() .map(|n| icalendar::parser::unfold(&n.get_content())) .collect::>(); let calendars = calendars_txt .iter() .map(|cal| icalendar::parser::read_calendar(cal).unwrap()) .collect::>(); let events = calendars .into_iter() .flat_map(|cal| { cal.components .into_iter() .filter(|comp| comp.name == "VEVENT") }) .map(|comp| { let event: CalendarComponent = comp.into(); match event { CalendarComponent::Event(e) => e, _ => unreachable!(), } }); Ok(events.collect::>()) } pub fn webdav_base_url(&self) -> &Url { &self.webdav_base_url } }