173 lines
5.9 KiB
Rust
173 lines
5.9 KiB
Rust
// SPDX-FileCopyrightText: 2022 Matteo Settenvini <matteo.settenvini@montecristosoftware.eu>
|
|
// 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,
|
|
pub(crate) rest_semaphore: Arc<Semaphore>, // TODO: wrap in dereferentiable struct
|
|
base_url: Url,
|
|
caldav_base_url: Url,
|
|
username: String,
|
|
}
|
|
|
|
impl ApiClient {
|
|
pub fn new(server_name: &str, configuration: &Config) -> Result<Self> {
|
|
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::<Vec<_>>()
|
|
)
|
|
})?;
|
|
|
|
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,
|
|
rest_semaphore: Arc::new(Semaphore::new(5)),
|
|
})
|
|
}
|
|
|
|
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<Tz>(
|
|
&self,
|
|
calendar_name: &str,
|
|
date_range: Range<DateTime<Tz>>,
|
|
) -> Result<Vec<icalendar::Event>>
|
|
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!(
|
|
"<c:calendar-query xmlns:d=\"DAV:\" xmlns:c=\"urn:ietf:params:xml:ns:caldav\">
|
|
<d:prop>
|
|
<c:calendar-data />
|
|
</d:prop>
|
|
<c:filter>
|
|
<c:comp-filter name=\"VCALENDAR\">
|
|
<c:prop-filter name=\"PRODID\">
|
|
<c:text-match>{}</c:text-match>
|
|
</c:prop-filter>
|
|
<c:comp-filter name=\"VEVENT\">
|
|
<c:prop-filter name=\"DTSTART\">
|
|
<time-range start=\"{}\" end=\"{}\" />
|
|
</c:prop-filter>
|
|
</c:comp-filter>
|
|
</c:comp-filter>
|
|
</c:filter>
|
|
</c:calendar-query>
|
|
",
|
|
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::<Vec<_>>();
|
|
|
|
let calendars = calendars_txt
|
|
.iter()
|
|
.map(|cal| icalendar::parser::read_calendar(cal).unwrap())
|
|
.collect::<Vec<_>>();
|
|
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::<Vec<_>>())
|
|
}
|
|
}
|