use std::ops::{Deref, DerefMut};
use http::Method;
use hyper::{Body, Uri};
use log::debug;
use crate::auth::Auth;
use crate::dav::{check_status, DavError, GetResourceError};
use crate::dns::DiscoverableService;
use crate::xml::{ItemDetails, ReportField, ResponseVariant, SimplePropertyMeta, StringProperty};
use crate::{common_bootstrap, CheckSupportError, FetchedResource};
use crate::{dav::WebDavClient, BootstrapError, FindHomeSetError};
#[derive(Debug)]
pub struct CalDavClient {
dav_client: WebDavClient,
pub calendar_home_set: Option<Uri>, }
impl Deref for CalDavClient {
type Target = WebDavClient;
fn deref(&self) -> &Self::Target {
&self.dav_client
}
}
impl DerefMut for CalDavClient {
fn deref_mut(&mut self) -> &mut crate::dav::WebDavClient {
&mut self.dav_client
}
}
impl CalDavClient {
pub fn raw_client(base_url: Uri, auth: Auth) -> Self {
Self {
dav_client: WebDavClient::new(base_url, auth),
calendar_home_set: None,
}
}
pub async fn auto_bootstrap(base_url: Uri, auth: Auth) -> Result<Self, BootstrapError> {
let mut client = Self::raw_client(base_url, auth);
let port = client.default_port()?;
let service = client.service()?;
common_bootstrap(&mut client, port, service).await?;
let principal_url = client.principal.as_ref().unwrap_or(&client.base_url);
client.calendar_home_set = client.find_calendar_home_set(principal_url).await?;
Ok(client)
}
async fn find_calendar_home_set(&self, url: &Uri) -> Result<Option<Uri>, FindHomeSetError> {
let property_data = SimplePropertyMeta {
name: b"calendar-home-set".to_vec(),
namespace: crate::xml::CALDAV.to_vec(),
};
self.find_href_prop_as_uri(
url,
"<calendar-home-set xmlns=\"urn:ietf:params:xml:ns:caldav\"/>",
&property_data,
)
.await
.map_err(FindHomeSetError)
}
pub async fn find_calendars(
&self,
url: &Uri,
) -> Result<Vec<(String, Option<String>)>, DavError> {
let items = self
.propfind::<ItemDetails>(url, "<resourcetype/><getetag/>", 1, &())
.await
.map_err(DavError::from)?
.into_iter()
.filter_map(|c| match c.variant {
ResponseVariant::WithProps { propstats } => {
if propstats.iter().any(|p| p.prop.is_calendar) {
Some((c.href, propstats.into_iter().find_map(|p| p.prop.etag)))
} else {
None
}
}
ResponseVariant::WithoutProps { .. } => None,
})
.collect();
Ok(items)
}
pub async fn get_calendar_colour(&self, href: &str) -> Result<Option<String>, DavError> {
let url = self.relative_uri(href)?;
let property_data = SimplePropertyMeta {
name: b"calendar-color".to_vec(),
namespace: b"DAV:".to_vec(),
};
self.propfind::<StringProperty>(
&url,
"<calendar-color xmlns=\"http://apple.com/ns/ical/\"/>",
0,
&property_data,
)
.await?
.pop()
.ok_or(DavError::from(crate::xml::Error::MissingData(
"calendar-color",
)))
.map(Option::<String>::from)
}
pub async fn get_resources<Href>(
&self,
calendar_href: Href,
hrefs: Vec<Href>,
) -> Result<Vec<FetchedResource>, GetResourceError>
where
Href: AsRef<str>,
{
let mut body = String::from(
r#"
<C:calendar-multiget xmlns="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
<prop>
<getetag/>
<C:calendar-data/>
</prop>"#,
);
for href in hrefs {
body.push_str(&format!("<href>{}</href>", href.as_ref()));
}
body.push_str("</C:calendar-multiget>");
self.multi_get(calendar_href.as_ref(), body, &ReportField::CALENDAR_DATA)
.await
}
pub async fn check_support(&self, url: &Uri) -> Result<(), CheckSupportError> {
let request = self
.request_builder()?
.method(Method::OPTIONS)
.uri(url)
.body(Body::empty())?;
let (head, _body) = self.request(request).await?;
check_status(head.status)?;
let header = head
.headers
.get("DAV")
.ok_or(CheckSupportError::MissingHeader)?
.to_str()?;
debug!("DAV header: '{}'", header);
if header
.split(|c| c == ',')
.any(|part| part.trim() == "calendar-access")
{
Ok(())
} else {
Err(CheckSupportError::NotAdvertised)
}
}
#[inline]
fn default_port(&self) -> Result<u16, BootstrapError> {
if let Some(port) = self.base_url.port_u16() {
Ok(port)
} else {
match self.base_url.scheme() {
Some(scheme) if scheme == "https" => Ok(443),
Some(scheme) if scheme == "http" => Ok(80),
Some(scheme) if scheme == "caldavs" => Ok(443),
Some(scheme) if scheme == "caldav" => Ok(80),
_ => Err(BootstrapError::InvalidUrl("invalid scheme (and no port)")),
}
}
}
fn service(&self) -> Result<DiscoverableService, BootstrapError> {
let scheme = self
.base_url
.scheme()
.ok_or(BootstrapError::InvalidUrl("missing scheme"))?;
match scheme.as_ref() {
"https" | "caldavs" => Ok(DiscoverableService::CalDavs),
"http" | "caldav" => Ok(DiscoverableService::CalDav),
_ => Err(BootstrapError::InvalidUrl("scheme is invalid")),
}
}
}