// 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, }; pub struct ApiClient { rest: reqwest::Client, base_url: Url, caldav_base_url: Url, username: String, } 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::>() ) })?; 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() .clone(); Ok(ApiClient { base_url, caldav_base_url, username: server.login_name.clone(), rest: rest_client, }) } pub fn rest(&self) -> &reqwest::Client { &self.rest } pub fn base_url(&self) -> &Url { &self.base_url } pub fn username(&self) -> &str { &self.username } 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() .request( report_method, // TODO extract into helper method self.caldav_base_url.join(&format!( "calendars/{}/{}", self.username(), 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? .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::>()) } }